Marketplace › From Ordinate 2D
The best kick with air drag (a golden-section search)
A football kicked at 25 m/s in air: the script searches for the angle of the longest kick, which is not 45°.
Use in Ordinate 2DOpens in the Script tab: it runs when you press Run. Free, no account needed.
"""The best launch angle with air drag: a golden-section search for the longest kick.
In a vacuum a ball flies furthest at 45°. With air drag it does not: the script kicks a football (0.43 kg, 11 cm radius)
at 25 m/s, measures how far it lands, and narrows down the best angle with a golden-section search, each kick telling it
which part of the interval to drop. Every kick is a row in the Script sheet; the last line says where the best angle is.
Runs in the studio (Script tab → Run) and locally: python3 optimal_angle_drag.py
"""
import math
import studio as s
V0 = 25.0 # m/s
LO, HI = 20.0, 60.0 # search interval, degrees
TOL = 0.5 # stop when the interval is this narrow, degrees
PHI = (math.sqrt(5) - 1) / 2
s.clear()
s.gravity = (0, -9.81)
s.air_density = 1.225
s.ground(-0.2, width=200)
ball = s.circle(0, 0, 0.11, mass=0.43, name="Football", color="#e5484d", collide=False)
def search():
"""A golden-section search for the maximum: yields an angle, is sent back its range."""
a, b = LO, HI
c, d = b - PHI * (b - a), a + PHI * (b - a)
fc, fd = (yield c), (yield d)
while b - a > TOL:
if fc > fd:
b, d, fd = d, c, fc
c = b - PHI * (b - a)
fc = yield c
else:
a, c, fc = c, d, fd
d = a + PHI * (b - a)
fd = yield d
print(f"best angle ≈ {(a + b) / 2:.1f}° (in a vacuum 45°; vacuum range {V0 ** 2 / 9.81:.1f} m)")
searcher = search()
angle = next(searcher)
kicks = 0
def kick(deg):
a = math.radians(deg)
ball.position = (0, 0)
ball.velocity = (V0 * math.cos(a), V0 * math.sin(a))
kick(angle)
@s.on_step
def watch(t, dt):
global angle, kicks
if ball.vy < 0 and ball.y <= 0: # back down at the launch height
rng = ball.x - ball.y * ball.vx / ball.vy
kicks += 1
s.record(kick=kicks, angle=angle, range=rng)
print(f"kick {kicks}: {angle:5.2f}° → {rng:6.2f} m")
try:
angle = searcher.send(rng)
except StopIteration:
s.stop()
return
kick(angle)
s.run(120)