Skip to main content
Code Review

Return to Answer

added 4996 characters in body
Source Link
Reinderien
  • 70.9k
  • 5
  • 76
  • 256

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()
add matplotlib
Source Link
Reinderien
  • 70.9k
  • 5
  • 76
  • 256

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

plot

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

plot

Source Link
Reinderien
  • 70.9k
  • 5
  • 76
  • 256

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
lang-py

AltStyle によって変換されたページ (->オリジナル) /