STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
flight_1dof.py
Go to the documentation of this file.
1"""Vertical point-mass flight: apogee of a thrust curve, and the impulse a target apogee needs.
2
3 dv/dt = (F(t) + (p_ref - p(z)) A_e - 1/2 rho(z) v|v| Cd(M) A) / m - g, dm/dt = -mdot(t)
4
5integrated through the burn and the coast to v = 0, in the 1976 ISA from the pad elevation, with
6the same Cd(M) the RocketPy flight uses (engine/pipeline/vehicle_drag). With Cd = 0, constant
7mdot and constant exhaust velocity it is Sutton & Biblarz's vertical flight without drag (ch. 4):
8u_p = c ln R - g t_p, h_p = c t_p [1 - ln R / (R - 1)] - g t_p^2 / 2, apogee h_p + u_p^2 / 2g.
9
10The required impulse keeps the propellant mass, the burn time and the shape of the mass flow and
11scales the thrust (the effective exhaust velocity) until the apogee is the target: I = c m_p. It
12is the check Layer 2 makes on a pressure schedule, cheap enough to sit inside its objective.
13Fixed-step RK4 (10 ms burn, 50 ms coast) with the apogee interpolated in the last step.
14"""
15
16from __future__ import annotations
17
18from typing import Optional, Sequence
19
20import numpy as np
21from scipy.optimize import brentq
22
23from engine.pipeline.vehicle_drag import DragCurves, isa_troposphere
24
25G0 = 9.80665
26
27
29 time_s: Sequence[float],
30 thrust_n: Sequence[float],
31 mdot_kg_s: Sequence[float],
32 m0_kg: float,
33 *,
34 area_m2: float = 0.0,
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,
41) -> float:
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])
47 if drag is not None:
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)
52
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
56 D = 0.0
57 if air:
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
61 if drag is not None:
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:
66 acc = 0.0 # on the pad until thrust exceeds weight
67 return v, acc, -md
68
69 t, z, v, m = 0.0, 0.0, 0.0, float(m0_kg)
70 while True:
71 burning = t < t_b # a step never straddles burnout, so each stage sees one phase
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])
80 if v1 <= 0.0 < v:
81 # constant deceleration across the step: v reaches 0 at s*dt, having climbed v*s*dt/2
82 s = v / (v - v1)
83 return float(z + 0.5 * v * s * dt)
84 if not burning and v1 <= 0.0 and z1 <= 0.0:
85 return 0.0 # never left the pad
86 t, z, v, m = (t + dt if not burning or t + dt < t_b else t_b), z1, v1, m1
87 if t > 600.0:
88 raise RuntimeError("vertical_apogee_agl: no apogee within 600 s")
89
90
92 target_apogee_agl_m: float,
93 m_dry_kg: float,
94 m_prop_kg: float,
95 burn_time_s: float,
96 *,
97 time_s: Optional[Sequence[float]] = None,
98 mdot_kg_s: Optional[Sequence[float]] = None,
99 thrust_n: Optional[Sequence[float]] = None,
100 **flight: object,
101) -> float:
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)."""
105 if time_s is None:
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)
113
114 def miss(k: float) -> float:
115 return vertical_apogee_agl(t, k * F_shape, md, m0, **flight) - target_apogee_agl_m
116
117 k_lo = 1.02 * m0 * G0 / max(float(np.max(F_shape)), 1e-12) # peak thrust just lifts it
118 k_hi = 1.5 * k_lo
119 while miss(k_hi) < 0.0:
120 k_hi *= 1.5
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)
124 return k * I_shape
125
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
*brief create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer *sa const json_pointer & rhs
Definition json.hpp:13930
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)