Geartrain simulation combined with pendulum clock mechanics - torque is supplied to the leftmost gear and is conveyed through the geartrain to the pendulum, via interactions in the escapement

Gears#

In previous attempts to simulate a pendulum clock, I was able to get a basic escapement working. However, the absence of the clock’s geartrain left it feeling incomplete.

In this experiment, we develop a simple means to simulate the mechanics of a geartrain. Some objectives:

  • Resolve torque as it is conveyed through the geartrain
  • Calculate the angular acceleration and velocities of each component
  • Calculate angular inertia of the geartrain as a whole
  • Resolve interactions with other bodies

To accomplish this, we develop a Gear abstraction that:

  • Treats each Gear as a Gearset
    • If I apply torque to a gear, I want to accelerate it according to the combined angular moment of inertia of it and any enmeshed gears.
    • I also want to propogate that torque through any enmeshed gears (respecting gear ratios), so that each gear’s interaction with other bodies can obey the laws of physics.
  • Enables interactions throughout the Gearset
    • I can apply torque to any gear in the gearset, whether exogenously or through interactions with other bodies. We can then resolve the physics on the gearset as a whole.

Above you can see an 8-gear set in action (there are two gears on each axle). Gears on the same axle have a ratio of 1:1, whereas enmeshed gears will have a ratio like 12:-32 (the second gear spins in the opposite direction).

Whilst the escapement’s collision resolution is rudimentary compared to our last physics engine, we can observe the gearset working perfectly: torque is conveyed from the leftmost driven wheel through many gear multiplications to the escapement wheel, where interactions with the pendulum’s arms propagate back through the gearset.


Proof of Concept Gear class#

class Gear:

    gear_classes = ['Axle', 'StandardGear', 'Wheel']

    def __init__(self, id, x=0, y=0):
        self.id = id
        self.x = x
        self.y = y

        self.angle = 0
        self.angular_velocity = 0
        
        self.torque = 0
        self.angular_acceleration = 0

        self.meshed_to = set()
        self.coaxial_with = set()

    def __call__(self, *args, **kwargs):
        for arg in args:
            if arg.__class__.__name__ in self.gear_classes:
                self.meshed_to.add(arg)
                arg.meshed_to.add(self)
            # can come up with a neater way of doing this
            elif type(arg) == tuple:
                gear = arg[0]
                self.coaxial_with.add(gear)
                gear.coaxial_with.add(self)
        return self
     
    def kinetic_energy(self):
        return 0.5 * self.moment_of_inertia * self.angular_velocity**2

    # could cache this / rebuild when it changes
    def gearset(self):
        """
        From the perspective of any gear, traverse the system of gears
        enmeshed together in a system. Returns gear and distance.
        Assumes that gears cannot create impossible systems (eg locking).
        - Returns (gear, dir, ratio)
        - Gear ratio needs to be relative to account for axles
        """
        q = deque([(self, 1)])
        visited_gear_ids = set([id(self)])

        while len(q) > 0:
            gear, ratio = q.popleft()

            for coaxial_gear in gear.coaxial_with:
                if id(coaxial_gear) in visited_gear_ids:
                    continue
                visited_gear_ids.add(id(coaxial_gear))
                q.append((coaxial_gear, ratio))

            for meshed_gear in gear.meshed_to:
                if id(meshed_gear) in visited_gear_ids:
                    continue
                visited_gear_ids.add(id(meshed_gear))
                meshed_gear_ratio = gear.n_teeth / meshed_gear.n_teeth * -1
                q.append((meshed_gear, ratio * meshed_gear_ratio))

            yield gear, ratio

    def equivalent_moment_of_inertia(self) -> float:
        """
        Calculates equivalent moment of enmeshed system from the perspective
        of starting gear.
        """
        total_moment_of_inertia = 0
        for gear, ratio in self.gearset():
            total_moment_of_inertia += gear.moment_of_inertia * ratio**2
        return total_moment_of_inertia
    
    def equivalent_torque(self) -> float:
        total_torque = 0
        for gear, ratio in self.gearset():
            total_torque += gear.torque * ratio
        return total_torque
    
    def enmeshed_kinetic_energy(self) -> float:
        kinetic_energy = 0
        for gear in self.gearset():
            kinetic_energy += gear.kinetic_energy()
        return kinetic_energy

    def set_enmeshed_angular_accelerations(self, torque) -> float:
        """
        Calculates the angular acceleration experienced by the starting gear
        as a result of forces acting on enmeshed system.
        Resets torques
        """
        angular_acceleration = torque / self.equivalent_moment_of_inertia()
        angular_accelerations = {}
        for gear, ratio in self.gearset():
            gear.angular_acceleration = angular_acceleration * ratio
            angular_accelerations[gear.id] = gear.angular_acceleration
            gear.torque = 0
        return angular_accelerations
    
    def accelerate_gearset(self, t: float) -> dict:
        angular_velocities = {}
        for gear, ratio in self.gearset():
            gear.angular_velocity += gear.angular_acceleration * t
            angular_velocities[gear.id] = gear.angular_velocity
        return angular_velocities
    
    def meshed_angular_velocities(self) -> dict[int, float]:
        angular_velocities = {}
        for gear, ratio in self.gearset():
            angular_velocities[gear.id] = gear.angular_velocity
        return angular_velocities

    def set_meshed_angular_velocities(self) -> dict[int, float]:
        """
        Sets and returns angular velocities for all gears in enmeshed system
        from angular velocity of starting gear.
        """
        angular_velocities = {}
        for gear, ratio in self.gearset():
            gear.angular_velocity = self.angular_velocity * ratio
            angular_velocities[gear.id] = gear.angular_velocity
        return angular_velocities