Simulating a geartrain (and revisiting the pendulum clock)
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