Bonus round: generalised vectors
A different answer suggests performing generalised vector math. While I don't agree with that class-based approach I do agree with the idea: the most direct way to implement this is to use the complex numbers built into Python. Among other benefits, this significantly simplifies the best-angle calculation since it can be expressed in rectilinear space.
import cmath
import math
from typing import Iterator
from matplotlib import pyplot as plt
class ProjectileMotion:
"""
This class calculates the movement of a projectile using its initial velocity
and the angle at which it is thrown/shot. The initial height of the projectile
is 0 by default but can be set by the user when creating an instance.
The friction forces are neglected, thus the only formula used is the
following: # △しろさんかくX = x0 + v_x⋅△しろさんかくt - a_x/2⋅△しろさんかくt^2 (and some of its variations)
"""
G = -9.80665j
def __init__(self, v0: complex, p0: complex) -> None:
self.v0 = v0
self.p0 = p0
self.t_apex = self._get_apex_time()
self.t1 = self._get_t1()
@classmethod
def from_polar(cls, speed0: float, angle0_degrees: float, x0: float = 0, y0: float = 0) -> 'ProjectileMotion':
return cls(
p0=x0 + y0*1j, v0=cmath.rect(speed0, math.radians(angle0_degrees))
)
def _get_apex_time(self) -> float:
"""
max_y attained when vy - g*t = 0 (when the velocity induced by gravity = vy)
"""
return -self.v0.imag / self.G.imag
def _get_t1(self) -> float:
"""
t_total = time taken for the object to fall on the ground (y=0)
t_total = (vy + sqrt(vy**2 + 2*y0*g)) / g
"""
y0 = self.p0.imag
vy0 = self.v0.imag
ay = -self.G.imag
return (
vy0 + math.sqrt(vy0**2 + 2*ay*y0)
) / ay
def get_t(self, n_steps: int) -> Iterator[float]:
"""
t_vect contains the time at n_steps, the first one being 0
=> need to divide by n_steps-1
"""
for i in range(n_steps):
yield self.t1 * i / (n_steps - 1)
def p_for_t(self, t: float) -> complex:
return self.p0 + self.v0*t + self.G*t**2/2
def v_for_t(self, t: float) -> complex:
return self.v0 + self.G*t
def angle_for_t(self, t: float) -> float:
speed, angle = cmath.polar(self.v_for_t(t))
return math.degrees(angle)
@property
def x1(self) -> float:
return self.p_for_t(self.t1).real
def desc(self, n_steps: int = 10) -> str:
return (
f'Curve over {n_steps} steps:'
f'\n{"t":>5} {"x":>5} {"y":>5} {"angle°":>6}'
f'\n'
) + '\n'.join(
f'{t:>5.2f}'
f' {(p := self.p_for_t(t)).real:>5.2f}'
f' {p.imag:>5.2f}'
f' {self.angle_for_t(t):>6.1f}'
for t in self.get_t(n_steps)
)
@classmethod
def with_best_angle(cls, speed0: float, y0: float) -> 'ProjectileMotion':
"""
https://www.whitman.edu/Documents/Academics/Mathematics/2016/Henelsmith.pdf
equations 12 & 13
h: initial height = y0
v: initial velocity magnitude = speed
Impact coefficients m = 0, a = 0
arccotangent(x) = atan(1/x), but do this in rectilinear space, not polar space
"""
g = ProjectileMotion.G.imag
v = speed0
tan2 = 1/(
1 - 2*y0*g/v/v
)
vx = v/math.sqrt(1 + tan2)
vy = vx*math.sqrt(tan2)
return cls(
v0=vx + 1j*vy,
p0=y0*1j,
)
def graph(motion: ProjectileMotion) -> plt.Figure:
fig, ax_x = plt.subplots()
ax_t: plt.Axes = ax_x.twiny()
ax_x.set_title('Projectile Motion')
pos = [motion.p_for_t(t) for t in motion.get_t(n_steps=100)]
ax_x.plot(
[p.real for p in pos],
[p.imag for p in pos],
)
ax_x.set_xlabel('x')
ax_t.set_xlabel('t')
ax_x.set_ylabel('y')
ax_x.set_xlim(left=0, right=motion.p_for_t(motion.t1).real)
ax_t.set_xlim(left=0, right=motion.t1)
ax_x.set_ylim(bottom=0)
return fig
def simple_test() -> None:
motion = ProjectileMotion.from_polar(speed0=10, angle0_degrees=30, y0=5)
print(motion.desc())
graph(motion)
plt.show()
def optimisation_test() -> None:
for y0 in range(1, 102, 10):
best = ProjectileMotion.with_best_angle(y0=y0, speed0=y0)
speed0, angle0 = cmath.polar(best.v0)
print(f'speed0={y0:3} y0={y0:3} x1={best.x1:6.1f} '
f'angle0={math.degrees(angle0):.1f}')
if __name__ == '__main__':
print('Simple projectile test:')
simple_test()
print()
print('Angle optimisation test:')
optimisation_test()
Bonus round: generalised vectors
A different answer suggests performing generalised vector math. While I don't agree with that class-based approach I do agree with the idea: the most direct way to implement this is to use the complex numbers built into Python. Among other benefits, this significantly simplifies the best-angle calculation since it can be expressed in rectilinear space.
import cmath
import math
from typing import Iterator
from matplotlib import pyplot as plt
class ProjectileMotion:
"""
This class calculates the movement of a projectile using its initial velocity
and the angle at which it is thrown/shot. The initial height of the projectile
is 0 by default but can be set by the user when creating an instance.
The friction forces are neglected, thus the only formula used is the
following: # △しろさんかくX = x0 + v_x⋅△しろさんかくt - a_x/2⋅△しろさんかくt^2 (and some of its variations)
"""
G = -9.80665j
def __init__(self, v0: complex, p0: complex) -> None:
self.v0 = v0
self.p0 = p0
self.t_apex = self._get_apex_time()
self.t1 = self._get_t1()
@classmethod
def from_polar(cls, speed0: float, angle0_degrees: float, x0: float = 0, y0: float = 0) -> 'ProjectileMotion':
return cls(
p0=x0 + y0*1j, v0=cmath.rect(speed0, math.radians(angle0_degrees))
)
def _get_apex_time(self) -> float:
"""
max_y attained when vy - g*t = 0 (when the velocity induced by gravity = vy)
"""
return -self.v0.imag / self.G.imag
def _get_t1(self) -> float:
"""
t_total = time taken for the object to fall on the ground (y=0)
t_total = (vy + sqrt(vy**2 + 2*y0*g)) / g
"""
y0 = self.p0.imag
vy0 = self.v0.imag
ay = -self.G.imag
return (
vy0 + math.sqrt(vy0**2 + 2*ay*y0)
) / ay
def get_t(self, n_steps: int) -> Iterator[float]:
"""
t_vect contains the time at n_steps, the first one being 0
=> need to divide by n_steps-1
"""
for i in range(n_steps):
yield self.t1 * i / (n_steps - 1)
def p_for_t(self, t: float) -> complex:
return self.p0 + self.v0*t + self.G*t**2/2
def v_for_t(self, t: float) -> complex:
return self.v0 + self.G*t
def angle_for_t(self, t: float) -> float:
speed, angle = cmath.polar(self.v_for_t(t))
return math.degrees(angle)
@property
def x1(self) -> float:
return self.p_for_t(self.t1).real
def desc(self, n_steps: int = 10) -> str:
return (
f'Curve over {n_steps} steps:'
f'\n{"t":>5} {"x":>5} {"y":>5} {"angle°":>6}'
f'\n'
) + '\n'.join(
f'{t:>5.2f}'
f' {(p := self.p_for_t(t)).real:>5.2f}'
f' {p.imag:>5.2f}'
f' {self.angle_for_t(t):>6.1f}'
for t in self.get_t(n_steps)
)
@classmethod
def with_best_angle(cls, speed0: float, y0: float) -> 'ProjectileMotion':
"""
https://www.whitman.edu/Documents/Academics/Mathematics/2016/Henelsmith.pdf
equations 12 & 13
h: initial height = y0
v: initial velocity magnitude = speed
Impact coefficients m = 0, a = 0
arccotangent(x) = atan(1/x), but do this in rectilinear space, not polar space
"""
g = ProjectileMotion.G.imag
v = speed0
tan2 = 1/(
1 - 2*y0*g/v/v
)
vx = v/math.sqrt(1 + tan2)
vy = vx*math.sqrt(tan2)
return cls(
v0=vx + 1j*vy,
p0=y0*1j,
)
def graph(motion: ProjectileMotion) -> plt.Figure:
fig, ax_x = plt.subplots()
ax_t: plt.Axes = ax_x.twiny()
ax_x.set_title('Projectile Motion')
pos = [motion.p_for_t(t) for t in motion.get_t(n_steps=100)]
ax_x.plot(
[p.real for p in pos],
[p.imag for p in pos],
)
ax_x.set_xlabel('x')
ax_t.set_xlabel('t')
ax_x.set_ylabel('y')
ax_x.set_xlim(left=0, right=motion.p_for_t(motion.t1).real)
ax_t.set_xlim(left=0, right=motion.t1)
ax_x.set_ylim(bottom=0)
return fig
def simple_test() -> None:
motion = ProjectileMotion.from_polar(speed0=10, angle0_degrees=30, y0=5)
print(motion.desc())
graph(motion)
plt.show()
def optimisation_test() -> None:
for y0 in range(1, 102, 10):
best = ProjectileMotion.with_best_angle(y0=y0, speed0=y0)
speed0, angle0 = cmath.polar(best.v0)
print(f'speed0={y0:3} y0={y0:3} x1={best.x1:6.1f} '
f'angle0={math.degrees(angle0):.1f}')
if __name__ == '__main__':
print('Simple projectile test:')
simple_test()
print()
print('Angle optimisation test:')
optimisation_test()
Your g = 9.81
is technically not the standard gravitational acceleration constant at sea level, and should instead be 9.80665.
Could then be processed to make graphs and whatnot
Indeed; matplotlib
makes this easy.
import math
from numbers import Real
from typing import Iterator
from matplotlib import pyplot as plt
class ProjectileMotion:
"""
This class calculates the movement of a projectile using its initial velocity
and the angle at which it is thrown/shot. The initial height of the projectile
is 0 by default but can be set by the user when creating an instance.
The friction forces are neglected, thus the only formula used is the
following: # △しろさんかくX = x0 + v_x⋅△しろさんかくt - a_x/2⋅△しろさんかくt^2 (and some of its variations)
"""
G = 9.80665
def __init__(self, speed0: Real = 1, angle0_degrees: Real = 45, y0: Real = 0) -> None:
self.y0 = y0
self.angle0 = math.radians(angle0_degrees)
self.vx0, self.vy0 = self._get_v0(speed0)
self.t_apex = self._get_apex_time()
self.t1 = self._get_t1()
def _get_v0(self, speed0: Real) -> tuple[float, float]:
return (
speed0 * math.cos(self.angle0),
speed0 * math.sin(self.angle0),
)
def _get_t1(self) -> float:
"""
t_total = time taken for the object to fall on the ground (y=0)
t_total = (vy + sqrt(vy**2 + 2*y0*g)) / g
"""
return (
self.vy0 + math.sqrt(self.vy0**2 + 2*self.y0*self.G)
) / self.G
def get_t(self, n_steps: int) -> Iterator[float]:
"""
t_vect contains the time at n_steps, the first one being 0
=> need to divide by n_steps-1
"""
for i in range(n_steps):
yield self.t1 * i / (n_steps - 1)
def x_for_t(self, t: float) -> float:
return self.vx0 * t
def y_for_t(self, t: float) -> float:
return self.y0 + self.vy0*t - self.G/2*t**2
def angle_for_t(self, t: float) -> float:
return math.degrees(math.atan2(
self.vy0 - t*self.G,
self.vx0
))
@property
def x1(self) -> float:
return self.x_for_t(self.t1)
def _get_apex_time(self) -> float:
"""
max_y attained when vy - g*t = 0 (when the velocity induced by gravity = vy)
"""
return self.vy0 / self.G
def desc(self, n_steps: int = 10) -> str:
return (
f'Curve over {n_steps} steps:'
f'\n{"t":>5} {"x":>5} {"y":>5} {"angle°":>6}'
f'\n'
) + '\n'.join(
f'{t:>5.2f}'
f' {self.x_for_t(t):>5.2f}'
f' {self.y_for_t(t):>5.2f}'
f' {self.angle_for_t(t):>6.1f}'
for t in self.get_t(n_steps)
)
@classmethod
def with_best_angle(cls, speed0: float, y0: float) -> 'ProjectileMotion':
"""
https://www.whitman.edu/Documents/Academics/Mathematics/2016/Henelsmith.pdf
equations 12 & 13
h: initial height = y0
v: initial velocity magnitude = speed
Impact coefficients m = 0, a = 0
arccotangent(x) = atan(1/x)
"""
g = ProjectileMotion.G
v = speed0
angle = math.atan(
1/math.sqrt(
2*y0*g/v/v + 1
)
)
return cls(
speed0=speed0, angle0_degrees=math.degrees(angle), y0=y0,
)
def find_best_angle_brute(speed0: float, y0: float, n: int = 50) -> ProjectileMotion:
"""
Find the angle at which the throw goes the farthest
"""
best_motion = None
best_distance = -float('inf')
for angle indef rangegraph(n):
motion =: ProjectileMotion(angle0_degrees=angle, speed0=speed0, y0=y0) -> plt.Figure:
fig, distanceax_x = motionplt.x_for_tsubplots(motion.t1)
if best_distance < distanceax_t: best_distanceplt.Axes = distanceax_x.twiny()
best_motion =ax_x.set_title('Projectile motionMotion')
returntimes best_motion= tuple(motion.get_t(n_steps=100))
ax_x.plot(
[motion.x_for_t(t) for t in times],
[motion.y_for_t(t) for t in times],
)
ax_x.set_xlabel('x')
ax_t.set_xlabel('t')
ax_x.set_ylabel('y')
ax_x.set_xlim(left=0, right=motion.x_for_t(motion.t1))
ax_t.set_xlim(left=0, right=motion.t1)
ax_x.set_ylim(bottom=0)
def find_best_angle_analytic(speed0: float, y0: float) -> ProjectileMotion:
"""
https://www.whitman.edu/Documents/Academics/Mathematics/2016/Henelsmith.pdf
equations 12 & 13
h: initial height = y0
v: initial velocity magnitude = speed
Impact coefficients m = 0, a = 0
arccotangent(x) = atan(1/x)
"""
g = ProjectileMotion.G
v = speed0
angle = math.atan(
1/math.sqrt(
2*y0*g/v/v + 1
)
)
return ProjectileMotion(
speed0=speed0, angle0_degrees=math.degrees(angle), y0=y0,
)fig
def simple_test() -> None:
motion = ProjectileMotion(speed0=10, angle0_degrees=30, y0=5)
print(motion.desc())
graph(motion)
plt.show()
def optimisation_test() -> None:
for speed0y0 in range(1, 102, 10):
y0 = speed0
brute = find_best_angle_brute(speed0, y0)
analyticbest = find_best_angle_analyticProjectileMotion.with_best_angle(speed0y0=y0, y0speed0=y0)
print(f'speed0={speed0y0:3} y0={y0:3}'
f' Brute: x1={brutebest.x1:8.3f} angle0={math.degrees(brute.angle0):6.1f}'
f' Analytic: x1={analytic.x1:4.3f} angle0=f'angle0={math.degrees(analyticbest.angle0):.1f}')
# Could then be processed to make graphs and whatnot
if __name__ == '__main__':
print('Simple projectile test:')
simple_test()
print()
print('Angle optimisation test:')
optimisation_test()
Simple projectile test:
Curve over 10 steps:
t x y angle°
0.00 0.00 5.00 30.0
0.18 1.58 5.75 20.3
0.36 3.16 6.17 9.3
0.55 4.74 6.27 -2.4
0.73 6.32 6.04 -14.0
0.91 7.90 5.48 -24.5
1.09 9.47 4.60 -33.5
1.28 11.05 3.39 -41.0
1.46 12.63 1.86 -47.1
1.64 14.21 0.00 -52.0
Angle optimisation test:
speed0= 1 y0= 1 Brute: x1= 0.46 angle0=12.0 Analytic: x1= 0.465 angle0=12.4
speed0= 11 y0= 11 Brute: x1= 20.58 angle0=31.0 Analytic: x1= 20.586 angle0=30.9
speed0= 21 y0= 21 Brute: x1= 62.54 angle0=36.0 Analytic: x1= 62.545 angle0=35.7
speed0= 31 y0= 31 Brute: x1= 125.21 angle0=38.0 Analytic: x1= 125.212 angle0=38.0
speed0= 41 y0= 41 Brute: x1= 208.40 angle0=39.0 Analytic: x1= 208.424 angle0=39.4
speed0= 51 y0= 51 Brute: x1= 312.07 angle0=40.0 Analytic: x1= 312.091 angle0=40.4
speed0= 61 y0= 61 Brute: x1= 436.19 angle0=41.0 Analytic: x1= 436.192 angle0=41.0
speed0= 71 y0= 71 Brute: x1= 580.65 angle0=42.0 Analytic: x1= 580.717 angle0=41.5
speed0= 81 y0= 81 Brute: x1= 745.65 angle0=42.0 Analytic: x1= 745.656 angle0=41.9
speed0= 91 y0= 91 Brute: x1= 930.97 angle0=42931.0 Analytic: x1= 930.99 angle0=42.2
speed0=101 y0=101 Brute: x1=1136.61 angle0=42.0 Analytic: x1=1136.737 angle0=42.5
import math
from numbers import Real
from typing import Iterator
class ProjectileMotion:
"""
This class calculates the movement of a projectile using its initial velocity
and the angle at which it is thrown/shot. The initial height of the projectile
is 0 by default but can be set by the user when creating an instance.
The friction forces are neglected, thus the only formula used is the
following: # △しろさんかくX = x0 + v_x⋅△しろさんかくt - a_x/2⋅△しろさんかくt^2 (and some of its variations)
"""
G = 9.80665
def __init__(self, speed0: Real = 1, angle0_degrees: Real = 45, y0: Real = 0) -> None:
self.y0 = y0
self.angle0 = math.radians(angle0_degrees)
self.vx0, self.vy0 = self._get_v0(speed0)
self.t_apex = self._get_apex_time()
self.t1 = self._get_t1()
def _get_v0(self, speed0: Real) -> tuple[float, float]:
return (
speed0 * math.cos(self.angle0),
speed0 * math.sin(self.angle0),
)
def _get_t1(self) -> float:
"""
t_total = time taken for the object to fall on the ground (y=0)
t_total = (vy + sqrt(vy**2 + 2*y0*g)) / g
"""
return (
self.vy0 + math.sqrt(self.vy0**2 + 2*self.y0*self.G)
) / self.G
def get_t(self, n_steps: int) -> Iterator[float]:
"""
t_vect contains the time at n_steps, the first one being 0
=> need to divide by n_steps-1
"""
for i in range(n_steps):
yield self.t1 * i / (n_steps - 1)
def x_for_t(self, t: float) -> float:
return self.vx0 * t
def y_for_t(self, t: float) -> float:
return self.y0 + self.vy0*t - self.G/2*t**2
def angle_for_t(self, t: float) -> float:
return math.degrees(math.atan2(
self.vy0 - t*self.G,
self.vx0
))
@property
def x1(self) -> float:
return self.x_for_t(self.t1)
def _get_apex_time(self) -> float:
"""
max_y attained when vy - g*t = 0 (when the velocity induced by gravity = vy)
"""
return self.vy0 / self.G
def desc(self, n_steps: int = 10) -> str:
return (
f'Curve over {n_steps} steps:'
f'\n{"t":>5} {"x":>5} {"y":>5} {"angle°":>6}'
f'\n'
) + '\n'.join(
f'{t:>5.2f}'
f' {self.x_for_t(t):>5.2f}'
f' {self.y_for_t(t):>5.2f}'
f' {self.angle_for_t(t):>6.1f}'
for t in self.get_t(n_steps)
)
def find_best_angle_brute(speed0: float, y0: float, n: int = 50) -> ProjectileMotion:
"""
Find the angle at which the throw goes the farthest
"""
best_motion = None
best_distance = -float('inf')
for angle in range(n):
motion = ProjectileMotion(angle0_degrees=angle, speed0=speed0, y0=y0) distance = motion.x_for_t(motion.t1)
if best_distance < distance: best_distance = distance
best_motion = motion
return best_motion
def find_best_angle_analytic(speed0: float, y0: float) -> ProjectileMotion:
"""
https://www.whitman.edu/Documents/Academics/Mathematics/2016/Henelsmith.pdf
equations 12 & 13
h: initial height = y0
v: initial velocity magnitude = speed
Impact coefficients m = 0, a = 0
arccotangent(x) = atan(1/x)
"""
g = ProjectileMotion.G
v = speed0
angle = math.atan(
1/math.sqrt(
2*y0*g/v/v + 1
)
)
return ProjectileMotion(
speed0=speed0, angle0_degrees=math.degrees(angle), y0=y0,
)
def simple_test() -> None:
motion = ProjectileMotion(speed0=10, angle0_degrees=30, y0=5)
print(motion.desc())
def optimisation_test() -> None:
for speed0 in range(1, 102, 10):
y0 = speed0
brute = find_best_angle_brute(speed0, y0)
analytic = find_best_angle_analytic(speed0, y0)
print(f'speed0={speed0:3} y0={y0:3}'
f' Brute: x1={brute.x1:8.3f} angle0={math.degrees(brute.angle0):.1f}'
f' Analytic: x1={analytic.x1:4.3f} angle0={math.degrees(analytic.angle0):.1f}')
# Could then be processed to make graphs and whatnot
if __name__ == '__main__':
print('Simple projectile test:')
simple_test()
print()
print('Angle optimisation test:')
optimisation_test()
Simple projectile test:
Curve over 10 steps:
t x y angle°
0.00 0.00 5.00 30.0
0.18 1.58 5.75 20.3
0.36 3.16 6.17 9.3
0.55 4.74 6.27 -2.4
0.73 6.32 6.04 -14.0
0.91 7.90 5.48 -24.5
1.09 9.47 4.60 -33.5
1.28 11.05 3.39 -41.0
1.46 12.63 1.86 -47.1
1.64 14.21 0.00 -52.0
Angle optimisation test:
speed0= 1 y0= 1 Brute: x1= 0.46 angle0=12.0 Analytic: x1= 0.46 angle0=12.4
speed0= 11 y0= 11 Brute: x1= 20.58 angle0=31.0 Analytic: x1= 20.58 angle0=30.9
speed0= 21 y0= 21 Brute: x1= 62.54 angle0=36.0 Analytic: x1= 62.54 angle0=35.7
speed0= 31 y0= 31 Brute: x1= 125.21 angle0=38.0 Analytic: x1= 125.21 angle0=38.0
speed0= 41 y0= 41 Brute: x1= 208.40 angle0=39.0 Analytic: x1= 208.42 angle0=39.4
speed0= 51 y0= 51 Brute: x1= 312.07 angle0=40.0 Analytic: x1= 312.09 angle0=40.4
speed0= 61 y0= 61 Brute: x1= 436.19 angle0=41.0 Analytic: x1= 436.19 angle0=41.0
speed0= 71 y0= 71 Brute: x1= 580.65 angle0=42.0 Analytic: x1= 580.71 angle0=41.5
speed0= 81 y0= 81 Brute: x1= 745.65 angle0=42.0 Analytic: x1= 745.65 angle0=41.9
speed0= 91 y0= 91 Brute: x1= 930.97 angle0=42.0 Analytic: x1= 930.99 angle0=42.2
speed0=101 y0=101 Brute: x1=1136.61 angle0=42.0 Analytic: x1=1136.73 angle0=42.5
Your g = 9.81
is technically not the standard gravitational acceleration constant at sea level, and should instead be 9.80665.
Could then be processed to make graphs and whatnot
Indeed; matplotlib
makes this easy.
import math
from numbers import Real
from typing import Iterator
from matplotlib import pyplot as plt
class ProjectileMotion:
"""
This class calculates the movement of a projectile using its initial velocity
and the angle at which it is thrown/shot. The initial height of the projectile
is 0 by default but can be set by the user when creating an instance.
The friction forces are neglected, thus the only formula used is the
following: # △しろさんかくX = x0 + v_x⋅△しろさんかくt - a_x/2⋅△しろさんかくt^2 (and some of its variations)
"""
G = 9.80665
def __init__(self, speed0: Real = 1, angle0_degrees: Real = 45, y0: Real = 0) -> None:
self.y0 = y0
self.angle0 = math.radians(angle0_degrees)
self.vx0, self.vy0 = self._get_v0(speed0)
self.t_apex = self._get_apex_time()
self.t1 = self._get_t1()
def _get_v0(self, speed0: Real) -> tuple[float, float]:
return (
speed0 * math.cos(self.angle0),
speed0 * math.sin(self.angle0),
)
def _get_t1(self) -> float:
"""
t_total = time taken for the object to fall on the ground (y=0)
t_total = (vy + sqrt(vy**2 + 2*y0*g)) / g
"""
return (
self.vy0 + math.sqrt(self.vy0**2 + 2*self.y0*self.G)
) / self.G
def get_t(self, n_steps: int) -> Iterator[float]:
"""
t_vect contains the time at n_steps, the first one being 0
=> need to divide by n_steps-1
"""
for i in range(n_steps):
yield self.t1 * i / (n_steps - 1)
def x_for_t(self, t: float) -> float:
return self.vx0 * t
def y_for_t(self, t: float) -> float:
return self.y0 + self.vy0*t - self.G/2*t**2
def angle_for_t(self, t: float) -> float:
return math.degrees(math.atan2(
self.vy0 - t*self.G,
self.vx0
))
@property
def x1(self) -> float:
return self.x_for_t(self.t1)
def _get_apex_time(self) -> float:
"""
max_y attained when vy - g*t = 0 (when the velocity induced by gravity = vy)
"""
return self.vy0 / self.G
def desc(self, n_steps: int = 10) -> str:
return (
f'Curve over {n_steps} steps:'
f'\n{"t":>5} {"x":>5} {"y":>5} {"angle°":>6}'
f'\n'
) + '\n'.join(
f'{t:>5.2f}'
f' {self.x_for_t(t):>5.2f}'
f' {self.y_for_t(t):>5.2f}'
f' {self.angle_for_t(t):>6.1f}'
for t in self.get_t(n_steps)
)
@classmethod
def with_best_angle(cls, speed0: float, y0: float) -> 'ProjectileMotion':
"""
https://www.whitman.edu/Documents/Academics/Mathematics/2016/Henelsmith.pdf
equations 12 & 13
h: initial height = y0
v: initial velocity magnitude = speed
Impact coefficients m = 0, a = 0
arccotangent(x) = atan(1/x)
"""
g = ProjectileMotion.G
v = speed0
angle = math.atan(
1/math.sqrt(
2*y0*g/v/v + 1
)
)
return cls(
speed0=speed0, angle0_degrees=math.degrees(angle), y0=y0,
)
def graph(motion: ProjectileMotion) -> plt.Figure:
fig, ax_x = plt.subplots()
ax_t: plt.Axes = ax_x.twiny()
ax_x.set_title('Projectile Motion')
times = tuple(motion.get_t(n_steps=100))
ax_x.plot(
[motion.x_for_t(t) for t in times],
[motion.y_for_t(t) for t in times],
)
ax_x.set_xlabel('x')
ax_t.set_xlabel('t')
ax_x.set_ylabel('y')
ax_x.set_xlim(left=0, right=motion.x_for_t(motion.t1))
ax_t.set_xlim(left=0, right=motion.t1)
ax_x.set_ylim(bottom=0)
return fig
def simple_test() -> None:
motion = ProjectileMotion(speed0=10, angle0_degrees=30, y0=5)
print(motion.desc())
graph(motion)
plt.show()
def optimisation_test() -> None:
for y0 in range(1, 102, 10):
best = ProjectileMotion.with_best_angle(y0=y0, speed0=y0)
print(f'speed0={y0:3} y0={y0:3} x1={best.x1:6.1f}'
f'angle0={math.degrees(best.angle0):.1f}')
if __name__ == '__main__':
print('Simple projectile test:')
simple_test()
print()
print('Angle optimisation test:')
optimisation_test()
Simple projectile test:
Curve over 10 steps:
t x y angle°
0.00 0.00 5.00 30.0
0.18 1.58 5.75 20.3
0.36 3.16 6.17 9.3
0.55 4.74 6.27 -2.4
0.73 6.32 6.04 -14.0
0.91 7.90 5.48 -24.5
1.09 9.47 4.60 -33.5
1.28 11.05 3.39 -41.0
1.46 12.63 1.86 -47.1
1.64 14.21 0.00 -52.0
Angle optimisation test:
speed0= 1 y0= 1 x1= 0.5 angle0=12.4
speed0= 11 y0= 11 x1= 20.6 angle0=30.9
speed0= 21 y0= 21 x1= 62.5 angle0=35.7
speed0= 31 y0= 31 x1= 125.2 angle0=38.0
speed0= 41 y0= 41 x1= 208.4 angle0=39.4
speed0= 51 y0= 51 x1= 312.1 angle0=40.4
speed0= 61 y0= 61 x1= 436.2 angle0=41.0
speed0= 71 y0= 71 x1= 580.7 angle0=41.5
speed0= 81 y0= 81 x1= 745.6 angle0=41.9
speed0= 91 y0= 91 x1= 931.0 angle0=42.2
speed0=101 y0=101 x1=1136.7 angle0=42.5
projectileMotion
should be ProjectileMotion
by PEP8.
Don't alias math
as m
; math
should be fine for your purposes.
You need to somehow document - either by comment or by variable name - that the angle you accept is in degrees.
Technically you don't accept "velocity", since velocity is a vector; you're accepting speed. When you break it down into vector components, then it's velocity.
Your class currently stores more than it should as state. It should only contain enough to "define the curve", rather than storing the entire curve itself.
Add PEP484 type hints.
Consider using generator functions instead of lists when you do iterate over time.
"Coordinates when Y is maximum" is just called the "apex", which is more terse.
Grammar: "this class calculate" -> "this class calculates"; "witch" -> "which".
Your best-angle calculation is brute-force, and an analytical solution is feasible (though I don't know what your level of math is, so you might find it tricky to derive on your own). I adapted a solution from Henelsmith 2016 which describes the problem in great detail.
I don't think it's a good idea to dump your object via blind __dict__
iteration. Make a proper description method.
Suggested
import math
from numbers import Real
from typing import Iterator
class ProjectileMotion:
"""
This class calculates the movement of a projectile using its initial velocity
and the angle at which it is thrown/shot. The initial height of the projectile
is 0 by default but can be set by the user when creating an instance.
The friction forces are neglected, thus the only formula used is the
following: # △しろさんかくX = x0 + v_x⋅△しろさんかくt - a_x/2⋅△しろさんかくt^2 (and some of its variations)
"""
G = 9.80665
def __init__(self, speed0: Real = 1, angle0_degrees: Real = 45, y0: Real = 0) -> None:
self.y0 = y0
self.angle0 = math.radians(angle0_degrees)
self.vx0, self.vy0 = self._get_v0(speed0)
self.t_apex = self._get_apex_time()
self.t1 = self._get_t1()
def _get_v0(self, speed0: Real) -> tuple[float, float]:
return (
speed0 * math.cos(self.angle0),
speed0 * math.sin(self.angle0),
)
def _get_t1(self) -> float:
"""
t_total = time taken for the object to fall on the ground (y=0)
t_total = (vy + sqrt(vy**2 + 2*y0*g)) / g
"""
return (
self.vy0 + math.sqrt(self.vy0**2 + 2*self.y0*self.G)
) / self.G
def get_t(self, n_steps: int) -> Iterator[float]:
"""
t_vect contains the time at n_steps, the first one being 0
=> need to divide by n_steps-1
"""
for i in range(n_steps):
yield self.t1 * i / (n_steps - 1)
def x_for_t(self, t: float) -> float:
return self.vx0 * t
def y_for_t(self, t: float) -> float:
return self.y0 + self.vy0*t - self.G/2*t**2
def angle_for_t(self, t: float) -> float:
return math.degrees(math.atan2(
self.vy0 - t*self.G,
self.vx0
))
@property
def x1(self) -> float:
return self.x_for_t(self.t1)
def _get_apex_time(self) -> float:
"""
max_y attained when vy - g*t = 0 (when the velocity induced by gravity = vy)
"""
return self.vy0 / self.G
def desc(self, n_steps: int = 10) -> str:
return (
f'Curve over {n_steps} steps:'
f'\n{"t":>5} {"x":>5} {"y":>5} {"angle°":>6}'
f'\n'
) + '\n'.join(
f'{t:>5.2f}'
f' {self.x_for_t(t):>5.2f}'
f' {self.y_for_t(t):>5.2f}'
f' {self.angle_for_t(t):>6.1f}'
for t in self.get_t(n_steps)
)
def find_best_angle_brute(speed0: float, y0: float, n: int = 50) -> ProjectileMotion:
"""
Find the angle at which the throw goes the farthest
"""
best_motion = None
best_distance = -float('inf')
for angle in range(n):
motion = ProjectileMotion(angle0_degrees=angle, speed0=speed0, y0=y0)
distance = motion.x_for_t(motion.t1)
if best_distance < distance:
best_distance = distance
best_motion = motion
return best_motion
def find_best_angle_analytic(speed0: float, y0: float) -> ProjectileMotion:
"""
https://www.whitman.edu/Documents/Academics/Mathematics/2016/Henelsmith.pdf
equations 12 & 13
h: initial height = y0
v: initial velocity magnitude = speed
Impact coefficients m = 0, a = 0
arccotangent(x) = atan(1/x)
"""
g = ProjectileMotion.G
v = speed0
angle = math.atan(
1/math.sqrt(
2*y0*g/v/v + 1
)
)
return ProjectileMotion(
speed0=speed0, angle0_degrees=math.degrees(angle), y0=y0,
)
def simple_test() -> None:
motion = ProjectileMotion(speed0=10, angle0_degrees=30, y0=5)
print(motion.desc())
def optimisation_test() -> None:
for speed0 in range(1, 102, 10):
y0 = speed0
brute = find_best_angle_brute(speed0, y0)
analytic = find_best_angle_analytic(speed0, y0)
print(
f'speed0={speed0:3} y0={y0:3}'
f' Brute: x1={brute.x1:8.3f} angle0={math.degrees(brute.angle0):.1f}'
f' Analytic: x1={analytic.x1:4.3f} angle0={math.degrees(analytic.angle0):.1f}')
# Could then be processed to make graphs and whatnot
if __name__ == '__main__':
print('Simple projectile test:')
simple_test()
print()
print('Angle optimisation test:')
optimisation_test()
Output
Simple projectile test:
Curve over 10 steps:
t x y angle°
0.00 0.00 5.00 30.0
0.18 1.58 5.75 20.3
0.36 3.16 6.17 9.3
0.55 4.74 6.27 -2.4
0.73 6.32 6.04 -14.0
0.91 7.90 5.48 -24.5
1.09 9.47 4.60 -33.5
1.28 11.05 3.39 -41.0
1.46 12.63 1.86 -47.1
1.64 14.21 0.00 -52.0
Angle optimisation test:
speed0= 1 y0= 1 Brute: x1= 0.46 angle0=12.0 Analytic: x1= 0.46 angle0=12.4
speed0= 11 y0= 11 Brute: x1= 20.58 angle0=31.0 Analytic: x1= 20.58 angle0=30.9
speed0= 21 y0= 21 Brute: x1= 62.54 angle0=36.0 Analytic: x1= 62.54 angle0=35.7
speed0= 31 y0= 31 Brute: x1= 125.21 angle0=38.0 Analytic: x1= 125.21 angle0=38.0
speed0= 41 y0= 41 Brute: x1= 208.40 angle0=39.0 Analytic: x1= 208.42 angle0=39.4
speed0= 51 y0= 51 Brute: x1= 312.07 angle0=40.0 Analytic: x1= 312.09 angle0=40.4
speed0= 61 y0= 61 Brute: x1= 436.19 angle0=41.0 Analytic: x1= 436.19 angle0=41.0
speed0= 71 y0= 71 Brute: x1= 580.65 angle0=42.0 Analytic: x1= 580.71 angle0=41.5
speed0= 81 y0= 81 Brute: x1= 745.65 angle0=42.0 Analytic: x1= 745.65 angle0=41.9
speed0= 91 y0= 91 Brute: x1= 930.97 angle0=42.0 Analytic: x1= 930.99 angle0=42.2
speed0=101 y0=101 Brute: x1=1136.61 angle0=42.0 Analytic: x1=1136.73 angle0=42.5