223 lines
11 KiB
Python
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
|