From dc0d34cb2e638c56d66ca8fd7956e02c1cc898bc Mon Sep 17 00:00:00 2001 From: asc Date: Mon, 23 Aug 2021 20:27:00 -0500 Subject: [PATCH] final submission --- .gitignore | 2 + bicycle_drive_train.py | 107 +++++++++++++++++++++++------- tests/test_bicycle_drive_train.py | 16 +++-- 3 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..090a1f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +.DS_Store diff --git a/bicycle_drive_train.py b/bicycle_drive_train.py index aa366f4..b0201c0 100644 --- a/bicycle_drive_train.py +++ b/bicycle_drive_train.py @@ -25,12 +25,14 @@ class BikeDriveTrain: 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 + # 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. - self._front_cogs = front_cogs - self._rear_cogs = rear_cogs + # 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 @@ -39,6 +41,7 @@ class BikeDriveTrain: # 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)) @@ -75,26 +78,31 @@ class BikeDriveTrain: Find the gear combination and its respective gear ratio that is nearest (but not over) target_ratio Parameters ---------- - target_ratio + target_ratio: float 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) + 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. + # 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. - # #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] + 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 + # 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 @@ -110,9 +118,9 @@ class BikeDriveTrain: Parameters ---------- - target_ratio + target_ratio: float A float representing the desired gear ratio. - initial_gear_combination + 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. @@ -121,33 +129,82 @@ class BikeDriveTrain: 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 - target_gear_combination_and_ratio = self.get_gear_combination(target_ratio) + # 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.") - # TODO implement this method, using the rough steps below. - # first determine if it is a down-shift or an up-shift. + # 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) - # sort shifting steps (large cog first, then small cog) depending on whether it's a down-shift or up-shift. + # 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) - # filter the list depending on target ratio, starting with the initial gear combination and stopping when the - # closest ratio to the target is achieved. + # 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) - # return list + # 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] - # TODO In the meantime, this function will return a "not implemented" error. + 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] - return self.gear_combinations_and_ratios + 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 + target_ratio: float A float representing the desired gear ratio. - initial_gear_combination + 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 @@ -159,5 +216,7 @@ class BikeDriveTrain: 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}") + 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 diff --git a/tests/test_bicycle_drive_train.py b/tests/test_bicycle_drive_train.py index d8ddc27..5d2bb7c 100644 --- a/tests/test_bicycle_drive_train.py +++ b/tests/test_bicycle_drive_train.py @@ -10,12 +10,10 @@ def drive_train(): """ 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), @@ -60,9 +58,17 @@ def test_bike_drive_train_shift_seq(drive_train): (30, 19, 30 / 19) ] + # Test case an downshifting sequence + sequence = drive_train.get_shift_sequence(target_ratio=1.4, initial_gear_combination=[30, 19]) + + assert sequence == [ + (30, 19, 30 / 19), + (38, 19, 38 / 19), + (38, 23, 38 / 23), + (38, 28, 38 / 28), + ] 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]) - + drive_train.produce_formatted_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" + 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\n"