Files
sram-techassessment/bicycle_drive_train.py
2021-08-23 20:27:00 -05:00

223 lines
11 KiB
Python

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.
"""
# IDEA 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.
# Let's also guarantee that the cogs are in order here, as they are in real life. Gear-sets are defined from
# largest to smallest.
self._front_cogs = sorted(front_cogs, reverse=True)
self._rear_cogs = sorted(rear_cogs, reverse=True)
# 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: float
A float representing the desired gear ratio.
Returns
-------
tuple: description of 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. Gear ratio is the third element of the
# gear_combination_and_ratio tuple
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.
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]
# Raise an value error if there are no candidates, meaning there is no gear ratio that is less than or equal
# to the target value.
if not candidate_gear_combinations_and_ratios:
raise ValueError(f"There is no gear ratio less than or equal to target gear ratio {target_ratio}")
# otherwise the nearest without going over ratio is the last element of the sorted 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: float
A float representing the desired gear ratio.
initial_gear_combination: Tuple[int, int]
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)
"""
# Unpack the target gears and ratio
initial_front_gear, initial_rear_gear = initial_gear_combination
# Make sure that the front initial gear is in the drivetrain.
if initial_front_gear not in self.front_cogs:
raise ValueError(f"Initial front gear '{initial_front_gear}' not in drivetrain.")
# Make sure that the rear initial gear is in the drivetrain.
if initial_rear_gear not in self.rear_cogs:
raise ValueError(f"Initial rear '{initial_rear_gear}' gear not in drivetrain.")
# Get the position of the initial gears
initial_front_gear_index = self.front_cogs.index(initial_front_gear)
initial_rear_gear_index = self.rear_cogs.index(initial_rear_gear)
# Unpack the target gears and ratio. get_gear_combination will either return a valid gear combination or an
# error, so there is no need for checks here.
target_front_gear, target_rear_gear, target_ratio = self.get_gear_combination(target_ratio)
# Get the position of the target gears
target_front_gear_index = self.front_cogs.index(target_front_gear)
target_rear_gear_index = self.rear_cogs.index(target_rear_gear)
# Determine if a down-shift or an up-shift is required.
if initial_front_gear > target_front_gear: # Requires an downshift
# Filter gears starting at initial and ending at target. target_index + 1 is necessary to ensure inclusion
# of the target in the slice.
front_shift_sequence = self.front_cogs[initial_front_gear_index:target_front_gear_index+1]
elif initial_front_gear < target_front_gear: # Requires a up-shift
# Since the gears are ordered largest to smallest by definition, for an up-shift we will need to start with
# target index, end with initial and then flip the list to filter gears starting at target and then reverse
# the list.
# TODO this is a little convoluted, and there is probably a more legible way to do this slicing.
front_shift_sequence = self.front_cogs[target_front_gear_index:initial_front_gear_index + 1]
front_shift_sequence.reverse()
else: # No shift required
front_shift_sequence = [initial_front_gear]
if initial_rear_gear > target_rear_gear: # Requires an down-shift.
# Filter gears starting at initial and ending at target. target_index + 1 is necessary to ensure inclusion
# of the target in the slice.
rear_shift_sequence = self.rear_cogs[initial_rear_gear_index:target_rear_gear_index+1]
elif initial_rear_gear < target_rear_gear: # Requires a up-shift, so flip the order of the shift
# Since the gears are ordered largest to smallest by definition, for an up-shift we will need to start with
# target index, end with initial and then flip the list to filter gears starting at target and then reverse
# the list.
# TODO this is a little convoluted, and there is probably a more legible way to do this slicing.
rear_shift_sequence = self.rear_cogs[target_rear_gear_index:initial_rear_gear_index + 1]
rear_shift_sequence.reverse()
else: # No shift required
rear_shift_sequence = [initial_rear_gear]
shift_sequence = []
# Shift front gear first.
for front_shift in front_shift_sequence:
shift_sequence.append((front_shift, initial_rear_gear, front_shift/initial_rear_gear))
# Then shift rear gear
# This method necessarily starts with the initial rear gear, so we should skip the first element
# of the rear_shift_sequence, since it starts with the initial rear gear.
for rear_shift in rear_shift_sequence[1:]:
shift_sequence.append((target_front_gear, rear_shift, target_front_gear/rear_shift))
return shift_sequence
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.
Prints in the format f"{step} - F:{front_gear} R:{rear_gear} Ratio {ratio}"
Parameters
----------
target_ratio: float
A float representing the desired gear ratio.
initial_gear_combination: Tuple[int, int]
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 step, (front_gear, rear_gear, ratio) in enumerate(sequence, start=1):
print(f"{step} - F:{front_gear} R:{rear_gear} Ratio {ratio:.3f}")
return None