Marketplace › From Ordinate 2D
Trebuchet: where the energy goes, where the ball lands
A 60 kg counterweight swings a beam; the script opens the sling's pouch when the ball flies 40° up: the ball's share of the counterweight's energy, and its landing point against the projectile formula.
Use in Ordinate 2DOpens in the Script tab: it runs when you press Run. Free, no account needed.
"""A counterweight trebuchet: where the energy goes, and where the ball lands.
A 60 kg counterweight hinged to the short arm of a beam falls and swings the long arm up and over. The sling, a rod from the
tip to a 1 kg ball, first drags the ball along the trough under the frame, then whips it round. The script opens the pouch
(switches the sling off) when the ball is flying RELEASE_DEG above the horizontal. From then on the ball flies free, so it lands at
x_r + v_x (v_y + sqrt(v_y^2 + 2 g (y_r - r))) / g, from its state at release; the script compares that with where it lands.
The counterweight's drop M g dh paid for everything: the ball's kinetic energy at release over it is the machine's efficiency.
Runs in the studio (Script tab -> Run) and locally: python3 trebuchet.py
"""
import math
import studio as s
M, m, G = 60.0, 1.0, 9.81 # counterweight, ball (kg)
H, A, B = 2.2, 2.6, 0.65 # axle height, long arm, short arm (m)
SLING, R = 2.0, 0.09 # sling length, ball radius (m)
START_DEG, RELEASE_DEG = 230.0, 40.0
s.clear()
s.gravity = (0, -G)
s.ground(0, x=35, width=90)
phi = math.radians(START_DEG)
ux, uy = math.cos(phi), math.sin(phi)
tip = (A * ux, H + A * uy)
heel = (-B * ux, H - B * uy)
# the frame (it collides with nothing) and the beam on its axle
for side in (-1, 1):
leg = math.atan2(H, -side * 0.9)
s.box(side * 0.45, H / 2, math.hypot(0.9, H), 0.12, static=True, angle=leg, collide=False, name="Front leg" if side < 0 else "Back leg", color="#7d8791")
beam = s.box((tip[0] + heel[0]) / 2, (tip[1] + heel[1]) / 2, A + B, 0.12, mass=6, angle=phi, collide=False, name="Beam", color="#f5a524")
s.pin(None, beam, at=(0, H), name="Axle")
# the counterweight hangs from the heel on a hinge
hanger = s.box(heel[0], heel[1] - 0.175, 0.05, 0.35, mass=0.3, collide=False, name="Hanger", color="#7d8791")
s.pin(beam, hanger, at=heel, name="Counterweight hinge")
weight = s.box(heel[0], heel[1] - 0.6, 0.55, 0.5, mass=M, collide=False, name="Counterweight", color="#8b939b")
s.pin(hanger, weight, at=(heel[0], heel[1] - 0.35), name="Hanger pin")
# the ball lies in the trough with the sling stretched from the tip
ball_x = tip[0] + math.sqrt(SLING ** 2 - (tip[1] - R) ** 2)
ball = s.circle(ball_x, R, R, mass=m, friction=0.05, restitution=0.0, name="Ball", color="#e5484d")
sling = s.rod(beam, ball, at_a=tip, at_b=(ball_x, R), name="Sling")
w_y0 = weight.y
release = None
landing = None
def beam_tip():
c, sn = math.cos(beam.angle), math.sin(beam.angle)
half = (A + B) / 2 # the tip is half the beam's length from its centre, along its axis
return beam.x + half * c, beam.y + half * sn
@s.on_step
def watch(t, dt):
global release, landing
flying_at = math.degrees(math.atan2(ball.vy, ball.vx))
if release is None and ball.vx > 0 and ball.vy > 0 and flying_at <= RELEASE_DEG:
sling.enabled = False
drop = M * G * (w_y0 - weight.y)
release = dict(t=t, x=ball.x, y=ball.y, vx=ball.vx, vy=ball.vy, ke=0.5 * m * (ball.vx ** 2 + ball.vy ** 2), drop=drop)
if release is not None and landing is None and ball.y <= R + 0.002 and ball.vy <= 0:
landing = dict(t=t, x=ball.x)
s.stop()
if round(t / dt) % 6 == 0:
s.record(t=t, x=ball.x, y=ball.y, speed=math.hypot(ball.vx, ball.vy), counterweight_drop_J=M * G * (w_y0 - weight.y),
ball_KE=0.5 * m * (ball.vx ** 2 + ball.vy ** 2), sling_on=0 if release else 1)
@s.on_end
def report():
if release is None:
print("the ball was never flying at the release angle")
return
r = release
v = math.hypot(r["vx"], r["vy"])
print(f"released at t = {r['t']:.3f} s, {v:.2f} m/s at {math.degrees(math.atan2(r['vy'], r['vx'])):.1f} deg, from ({r['x']:.2f}, {r['y']:.2f}) m")
print(f"the counterweight gave {r['drop']:.0f} J; the ball took {r['ke']:.0f} J: efficiency {100 * r['ke'] / r['drop']:.1f} %")
flight = (r["vy"] + math.sqrt(r["vy"] ** 2 + 2 * G * (r["y"] - R))) / G
predicted = r["x"] + r["vx"] * flight
if landing is None:
print(f"projectile formula: lands at x = {predicted:.2f} m after {flight:.3f} s (not reached yet)")
else:
print(f"landed at x = {landing['x']:.2f} m after {landing['t'] - r['t']:.3f} s of flight; projectile formula: {predicted:.2f} m after {flight:.3f} s")
s.run(8)