29 time_s: Sequence[float],
30 thrust_n: Sequence[float],
31 mdot_kg_s: Sequence[float],
35 drag: Optional[DragCurves] =
None,
36 elevation_m: float = 0.0,
37 reference_pressure_pa: Optional[float] =
None,
38 nozzle_exit_area_m2: float = 0.0,
39 dt_burn: float = 0.01,
40 dt_coast: float = 0.05,
42 """Apogee above the pad [m] of a vertical flight on this thrust and mass-flow history."""
43 t_arr = np.asarray(time_s, dtype=float) - float(time_s[0])
44 F_arr = np.asarray(thrust_n, dtype=float)
45 md_arr = np.asarray(mdot_kg_s, dtype=float)
46 t_b = float(t_arr[-1])
48 mach_tab = np.asarray(drag.mach)
49 cd_on, cd_off = np.asarray(drag.cd_power_on), np.asarray(drag.cd_power_off)
50 p_ref = reference_pressure_pa
51 air = (drag
is not None and area_m2 > 0.0)
or (p_ref
is not None and nozzle_exit_area_m2 > 0.0)
53 def rhs(t: float, z: float, v: float, m: float, burning: bool):
54 F = float(np.interp(t, t_arr, F_arr))
if burning
else 0.0
55 md = float(np.interp(t, t_arr, md_arr))
if burning
else 0.0
58 _, p, rho, a, _ = isa_troposphere(elevation_m +
max(z, 0.0))
59 if burning
and p_ref
is not None:
60 F += (p_ref - p) * nozzle_exit_area_m2
62 cd = float(np.interp(abs(v) / a, mach_tab, cd_on
if burning
else cd_off))
63 D = 0.5 * rho * v * abs(v) * cd * area_m2
64 acc = (F - D) / m - G0
65 if z <= 0.0
and v <= 0.0
and acc < 0.0:
69 t, z, v, m = 0.0, 0.0, 0.0, float(m0_kg)
72 dt =
min(dt_burn, t_b - t)
if burning
else dt_coast
73 k1 =
rhs(t, z, v, m, burning)
74 k2 =
rhs(t + dt / 2, z + dt / 2 * k1[0], v + dt / 2 * k1[1], m + dt / 2 * k1[2], burning)
75 k3 =
rhs(t + dt / 2, z + dt / 2 * k2[0], v + dt / 2 * k2[1], m + dt / 2 * k2[2], burning)
76 k4 =
rhs(t + dt, z + dt * k3[0], v + dt * k3[1], m + dt * k3[2], burning)
77 z1 = z + dt / 6 * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0])
78 v1 = v + dt / 6 * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1])
79 m1 = m + dt / 6 * (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2])
83 return float(z + 0.5 * v * s * dt)
84 if not burning
and v1 <= 0.0
and z1 <= 0.0:
86 t, z, v, m = (t + dt
if not burning
or t + dt < t_b
else t_b), z1, v1, m1
88 raise RuntimeError(
"vertical_apogee_agl: no apogee within 600 s")
92 target_apogee_agl_m: float,
97 time_s: Optional[Sequence[float]] =
None,
98 mdot_kg_s: Optional[Sequence[float]] =
None,
99 thrust_n: Optional[Sequence[float]] =
None,
102 """Total impulse [N s] that puts this vehicle at the target apogee, holding m_prop, the burn
103 time and the mass-flow shape, and scaling the thrust shape (flat mdot and constant exhaust
104 velocity when no history is given). ``flight`` goes to vertical_apogee_agl (drag, area, pad)."""
106 time_s = np.linspace(0.0, burn_time_s, 201)
107 t = np.asarray(time_s, dtype=float) - float(time_s[0])
108 md = np.full_like(t, 1.0)
if mdot_kg_s
is None else np.asarray(mdot_kg_s, dtype=float)
109 md = md * (m_prop_kg / float(np.trapezoid(md, t)))
110 F_shape = md
if thrust_n
is None else np.asarray(thrust_n, dtype=float)
111 I_shape = float(np.trapezoid(F_shape, t))
112 m0 = float(m_dry_kg) + float(m_prop_kg)
114 def miss(k: float) -> float:
117 k_lo = 1.02 * m0 * G0 /
max(float(np.max(F_shape)), 1e-12)
119 while miss(k_hi) < 0.0:
121 if k_hi > 1e3 * k_lo:
122 raise ValueError(
"required_impulse: target apogee is out of reach of this vehicle")
123 k = brentq(miss, k_lo, k_hi, xtol=1e-6 * k_hi, rtol=1e-8)
float vertical_apogee_agl(Sequence[float] time_s, Sequence[float] thrust_n, Sequence[float] mdot_kg_s, float m0_kg, *, float area_m2=0.0, Optional[DragCurves] drag=None, float elevation_m=0.0, Optional[float] reference_pressure_pa=None, float nozzle_exit_area_m2=0.0, float dt_burn=0.01, float dt_coast=0.05)
float required_impulse(float target_apogee_agl_m, float m_dry_kg, float m_prop_kg, float burn_time_s, *, Optional[Sequence[float]] time_s=None, Optional[Sequence[float]] mdot_kg_s=None, Optional[Sequence[float]] thrust_n=None, **object flight)