Initial Commit, first submission to SRAM
This commit is contained in:
163
bicycle_drive_train.py
Normal file
163
bicycle_drive_train.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class BikeDriveTrain:
|
||||||
|
"""
|
||||||
|
Class representing a bicycle drive train. Only a single cog on the front and single cog on the rear can be selected
|
||||||
|
at one time.
|
||||||
|
|
||||||
|
Note
|
||||||
|
----
|
||||||
|
Gear combination and ratio types are represented as a Tuple[int, int, float] with the integers representing front
|
||||||
|
cog, and rear cog respectively, and the float representing the gear ratio. In the future, it would make sense for
|
||||||
|
this to be a named tuple, alias of namedtuple, or even class of its own to be more explicit.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, front_cogs: List[int], rear_cogs: List[int]):
|
||||||
|
"""
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
front_cogs: list[int]
|
||||||
|
List of integers representing tooth counts for cogs on front crank.
|
||||||
|
rear_cogs: list[int]
|
||||||
|
List of integers representing tooth counts for cogs on rear cassette.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#TODO Error handling for invalid inputs. Consider adding warnings for nonsensical gears/gear combinations
|
||||||
|
|
||||||
|
# Assign respective cogs to instance.
|
||||||
|
# ASSUMPTION: Gear sets won't need change after object creation, enforced as a read-only property.
|
||||||
|
self._front_cogs = front_cogs
|
||||||
|
self._rear_cogs = rear_cogs
|
||||||
|
|
||||||
|
# Calculate the ratios for this drive train.
|
||||||
|
# ASSUMPTION: Since front_cogs and rear_cogs won't change after instantiation, neither will the gear ratios. As
|
||||||
|
# such, we only have to do this once, so it makes sense to do it at instantiation.
|
||||||
|
|
||||||
|
# ASSUMPTION: Practically, real bicycles will have a small set of gearing combinations, so this method is
|
||||||
|
# computationally trivial.
|
||||||
|
gear_combinations_and_ratios = []
|
||||||
|
for front_cog in front_cogs:
|
||||||
|
for rear_cog in rear_cogs:
|
||||||
|
gear_combinations_and_ratios.append((front_cog, rear_cog, front_cog / rear_cog))
|
||||||
|
|
||||||
|
self._gear_combinations_and_ratios = gear_combinations_and_ratios
|
||||||
|
|
||||||
|
# Getters for the cogs
|
||||||
|
@property
|
||||||
|
def front_cogs(self):
|
||||||
|
"""
|
||||||
|
list of int: List of integers representing tooth counts for cogs on front crank.
|
||||||
|
"""
|
||||||
|
return self._front_cogs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rear_cogs(self):
|
||||||
|
"""
|
||||||
|
list of int: List of integers representing tooth counts for cogs on rear cassette.
|
||||||
|
"""
|
||||||
|
return self._rear_cogs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gear_combinations_and_ratios(self) -> List[Tuple[int, int, float]]:
|
||||||
|
"""
|
||||||
|
list of tuple of (int, int, float): List of tuples describing the possible gear combinations and their
|
||||||
|
respective gear ratio in the form (front_cog, rear_cog, gear ratio).
|
||||||
|
"""
|
||||||
|
# ASSUMPTION: This information will be useful to uses of the class. It is a convenience function for the
|
||||||
|
# class itself
|
||||||
|
return self._gear_combinations_and_ratios
|
||||||
|
|
||||||
|
def get_gear_combination(self, target_ratio: float) -> Tuple[int, int, float]:
|
||||||
|
"""
|
||||||
|
Find the gear combination and its respective gear ratio that is nearest (but not over) target_ratio
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
target_ratio
|
||||||
|
A float representing the desired gear ratio.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
A tuple describing the gear combination and its respective gear ratio that is nearest (but not over)
|
||||||
|
target_ratio in the form (front_cog, rear_cog, gear ratio).
|
||||||
|
"""
|
||||||
|
# first sort gear_combination_and_ratios from smallest to largest.
|
||||||
|
candidate_gear_combinations_and_ratios = sorted(self.gear_combinations_and_ratios,
|
||||||
|
key=lambda gear_combination_and_ratio:
|
||||||
|
gear_combination_and_ratio[2])
|
||||||
|
|
||||||
|
# then eliminate gear ratios that are greater than the target.
|
||||||
|
# #TODO error handling if all elements are eliminated
|
||||||
|
candidate_gear_combinations_and_ratios = [(front_cog, rear_cog, gear_ratio) for
|
||||||
|
(front_cog, rear_cog, gear_ratio) in
|
||||||
|
candidate_gear_combinations_and_ratios if gear_ratio < target_ratio]
|
||||||
|
|
||||||
|
# and the nearest without going over ratio is the last element of the candidate list
|
||||||
|
target_gear_combination_and_ratio = candidate_gear_combinations_and_ratios[-1]
|
||||||
|
|
||||||
|
return target_gear_combination_and_ratio
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_shift_sequence(self, target_ratio: float, initial_gear_combination: Tuple[int, int]) -> \
|
||||||
|
List[Tuple[int, int, float]]:
|
||||||
|
"""
|
||||||
|
A method that returns a shift sequence to traverse from an initial gear combination to a gear combination with
|
||||||
|
the closest ratio that is less than or equal to the target ratio, following first shifting the front to the
|
||||||
|
final gear, then shift the rear to the final gear.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
target_ratio
|
||||||
|
A float representing the desired gear ratio.
|
||||||
|
initial_gear_combination
|
||||||
|
The starting gear combination in the form of (front_gear, rear_gear) where front_gear and rear_gear are
|
||||||
|
integers describing the number of teeth in specified gear.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List of tuple of int, int, float: Steps in gear shifting sequence in the form
|
||||||
|
(front_cog, rear_cog, gear ratio)
|
||||||
|
"""
|
||||||
|
|
||||||
|
target_gear_combination_and_ratio = self.get_gear_combination(target_ratio)
|
||||||
|
|
||||||
|
# TODO implement this method, using the rough steps below.
|
||||||
|
# first determine if it is a down-shift or an up-shift.
|
||||||
|
|
||||||
|
|
||||||
|
# sort shifting steps (large cog first, then small cog) depending on whether it's a down-shift or up-shift.
|
||||||
|
|
||||||
|
# filter the list depending on target ratio, starting with the initial gear combination and stopping when the
|
||||||
|
# closest ratio to the target is achieved.
|
||||||
|
|
||||||
|
# return list
|
||||||
|
|
||||||
|
# TODO In the meantime, this function will return a "not implemented" error.
|
||||||
|
|
||||||
|
return self.gear_combinations_and_ratios
|
||||||
|
|
||||||
|
def produce_formatted_shift_sequence(self, target_ratio: float, initial_gear_combination: Tuple[int, int]):
|
||||||
|
"""
|
||||||
|
A method to produce a formatted shift sequence for a given target ratio and initial gear combination.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
target_ratio
|
||||||
|
A float representing the desired gear ratio.
|
||||||
|
initial_gear_combination
|
||||||
|
The starting gear combination in the form of (front_gear, rear_gear) where front_gear and rear_gear are
|
||||||
|
integers describing the number of teeth in specified gear.
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None: Method only prints out the sequence to the console.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# get the sequence using method.
|
||||||
|
sequence = self.get_shift_sequence(target_ratio, initial_gear_combination)
|
||||||
|
|
||||||
|
# print the sequence using string formatter
|
||||||
|
for i, (front_gear, rear_gear, ratio) in enumerate(sequence):
|
||||||
|
print(f"{i}: F:{front_gear}, R:{rear_gear}, {ratio:3f}")
|
||||||
68
tests/test_bicycle_drive_train.py
Normal file
68
tests/test_bicycle_drive_train.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import pytest
|
||||||
|
from bicycle_drive_train import BikeDriveTrain
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def drive_train():
|
||||||
|
"""
|
||||||
|
list of front cog tooth counts. For example it could be initialized with [38,30]
|
||||||
|
list of rear cog tooth counts. For example it could be initialized with [28, 23, 19, 16]
|
||||||
|
"""
|
||||||
|
return BikeDriveTrain([38, 30], [28, 23, 19, 16])
|
||||||
|
|
||||||
|
|
||||||
|
def test_bike_drive_train(drive_train):
|
||||||
|
assert drive_train.front_cogs == [38, 30]
|
||||||
|
assert drive_train.rear_cogs == [28, 23, 19, 16]
|
||||||
|
|
||||||
|
|
||||||
|
def test_bike_drive_train_ratios(drive_train):
|
||||||
|
# generate list of the gear ratios for the given front crank and rear cassette
|
||||||
|
ratios = [(38, 28, 38 / 28),
|
||||||
|
(38, 23, 38 / 23),
|
||||||
|
(38, 19, 38 / 19),
|
||||||
|
(38, 16, 38 / 16),
|
||||||
|
(30, 28, 30 / 28),
|
||||||
|
(30, 23, 30 / 23),
|
||||||
|
(30, 19, 30 / 19),
|
||||||
|
(30, 16, 30 / 16)
|
||||||
|
]
|
||||||
|
|
||||||
|
for ratio in ratios:
|
||||||
|
assert ratio in drive_train.gear_combinations_and_ratios
|
||||||
|
|
||||||
|
|
||||||
|
def test_bike_drive_train_ratio(drive_train):
|
||||||
|
"""
|
||||||
|
If the drivetrain was initialized with the example values above and passed a target_ratio of 1.6
|
||||||
|
It should return a data type that contains the information:
|
||||||
|
Front: 30, Rear: 19, Ratio: 1.579
|
||||||
|
"""
|
||||||
|
assert drive_train.get_gear_combination(1.6) == (30, 19, 30 / 19)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bike_drive_train_shift_seq(drive_train):
|
||||||
|
"""
|
||||||
|
For an example input the same as above plus: initial_gear_combination = [38, 28]
|
||||||
|
|
||||||
|
1 - F:38 R:28 Ratio 1.357
|
||||||
|
2 - F:30 R:28 Ratio 1.071
|
||||||
|
3 - F:30 R:23 Ratio 1.304
|
||||||
|
4 - F:30 R:19 Ratio 1.579
|
||||||
|
"""
|
||||||
|
|
||||||
|
sequence = drive_train.get_shift_sequence(target_ratio=1.6, initial_gear_combination=[38, 28])
|
||||||
|
|
||||||
|
assert sequence == [
|
||||||
|
(38, 28, 38 / 28),
|
||||||
|
(30, 28, 30 / 28),
|
||||||
|
(30, 23, 30 / 23),
|
||||||
|
(30, 19, 30 / 19)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_bike_drive_train_shift_seq_output(drive_train, capsys):
|
||||||
|
drive_train.get_shift_sequence(target_ratio=1.6, initial_gear_combination=[38, 28])
|
||||||
|
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert out == "1 - F:38 R:28 Ratio 1.357\n2 - F:30 R:28 Ratio 1.071\n3 - F:30 R:23 Ratio 1.304\n4 - F:30 R:19 Ratio 1.579"
|
||||||
Reference in New Issue
Block a user