commit 71fffe1cb6613eb954eea560088464fcc318d820 Author: asc Date: Mon Aug 23 18:06:05 2021 -0500 Initial Commit, first submission to SRAM diff --git a/bicycle_drive_train.py b/bicycle_drive_train.py new file mode 100644 index 0000000..aa366f4 --- /dev/null +++ b/bicycle_drive_train.py @@ -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}") diff --git a/tests/test_bicycle_drive_train.py b/tests/test_bicycle_drive_train.py new file mode 100644 index 0000000..d8ddc27 --- /dev/null +++ b/tests/test_bicycle_drive_train.py @@ -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"