STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
flight_altitude_optimizer.py
Go to the documentation of this file.
1"""Find minimum-fuel burn time to reach a target apogee for a fixed time-series curve.
2
3For each candidate burn time T we truncate the thrust/mdot curve, load exactly the
4propellant required to complete that burn (∫mdot dt), and run flight simulation.
5The optimal burn time is the shortest T whose apogee meets the altitude target —
6which minimizes fuel mass because fuel_required(T) increases monotonically with T.
7"""
8
9from __future__ import annotations
10
11from dataclasses import dataclass, field
12from typing import Callable, List, Optional, Tuple
13
14import numpy as np
15
16
17@dataclass
19 burn_time_s: float
20 lox_required_kg: float
21 fuel_required_kg: float
22 apogee_m: float
23 success: bool
24 error: Optional[str] = None
25
26
27@dataclass
29 success: bool
30 target_apogee_m: float
31 apogee_tolerance_m: float
32 optimal_burn_time_s: float
33 optimal_lox_kg: float
34 optimal_fuel_kg: float
35 achieved_apogee_m: float
36 apogee_error_m: float
37 total_impulse_Ns: float
38 simulations_run: int
39 infeasible_reason: Optional[str] = None
40 evaluations: List[BurnTimeEval] = field(default_factory=list)
41
42
43def _integrate(times: np.ndarray, values: np.ndarray) -> float:
44 if len(times) < 2:
45 return 0.0
46 if hasattr(np, "trapezoid"):
47 return float(np.trapezoid(values, times))
48 return float(np.trapz(values, times))
49
50
52 times: np.ndarray,
53 thrust: np.ndarray,
54 mdot_O: np.ndarray,
55 mdot_F: np.ndarray,
56 burn_time_s: float,
57) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
58 """Return curve arrays truncated to burn_time_s (interpolated endpoint)."""
59 times = np.asarray(times, dtype=float)
60 thrust = np.asarray(thrust, dtype=float)
61 mdot_O = np.asarray(mdot_O, dtype=float)
62 mdot_F = np.asarray(mdot_F, dtype=float)
63
64 if len(times) < 2:
65 raise ValueError("Time-series must contain at least two points")
66
67 t0 = float(times[0])
68 times = times - t0
69 t_end = float(np.clip(burn_time_s, times[0], times[-1]))
70 if t_end <= times[0]:
71 t_end = float(times[1])
72
73 mask = times <= t_end + 1e-12
74 t_out = times[mask]
75 thrust_out = thrust[mask]
76 mdot_O_out = mdot_O[mask]
77 mdot_F_out = mdot_F[mask]
78
79 if t_out[-1] < t_end - 1e-9:
80 t_out = np.append(t_out, t_end)
81 thrust_out = np.append(thrust_out, np.interp(t_end, times, thrust))
82 mdot_O_out = np.append(mdot_O_out, np.interp(t_end, times, mdot_O))
83 mdot_F_out = np.append(mdot_F_out, np.interp(t_end, times, mdot_F))
84
85 return t_out, thrust_out, mdot_O_out, mdot_F_out
86
87
89 times: np.ndarray,
90 thrust: np.ndarray,
91 mdot_O: np.ndarray,
92 mdot_F: np.ndarray,
93 burn_time_s: float,
94) -> Tuple[float, float, float]:
95 """Return (lox_kg, fuel_kg, total_impulse_Ns) for burn truncated to burn_time_s."""
96 t_out, thrust_out, mdot_O_out, mdot_F_out = truncate_time_series(
97 times, thrust, mdot_O, mdot_F, burn_time_s
98 )
99 lox_kg = _integrate(t_out, mdot_O_out)
100 fuel_kg = _integrate(t_out, mdot_F_out)
101 impulse = _integrate(t_out, thrust_out)
102 return lox_kg, fuel_kg, impulse
103
104
106 *,
107 times: np.ndarray,
108 thrust: np.ndarray,
109 mdot_O: np.ndarray,
110 mdot_F: np.ndarray,
111 target_apogee_m: float,
112 apogee_tolerance_m: float,
113 simulate_at_burn_time: Callable[[float], BurnTimeEval],
114 min_burn_time_s: Optional[float] = None,
115 max_burn_time_s: Optional[float] = None,
116 time_resolution_s: float = 0.05,
117 max_iterations: int = 24,
118) -> AltitudeOptimizeResult:
119 """Binary-search the shortest burn time that reaches target_apogee_m."""
120 times_norm = np.asarray(times, dtype=float)
121 times_norm = times_norm - times_norm[0]
122 t_max_curve = float(times_norm[-1])
123
124 t_lo = float(min_burn_time_s) if min_burn_time_s is not None else max(0.05, float(times_norm[1] - times_norm[0]))
125 t_hi = float(max_burn_time_s) if max_burn_time_s is not None else t_max_curve
126 t_lo = max(t_lo, 0.05)
127 t_hi = min(t_hi, t_max_curve)
128 if t_lo >= t_hi:
129 t_lo = max(0.05, t_hi * 0.1)
130
131 evaluations: List[BurnTimeEval] = []
132 sim_count = 0
133
134 def _eval(t: float) -> BurnTimeEval:
135 nonlocal sim_count
136 sim_count += 1
137 result = simulate_at_burn_time(t)
138 evaluations.append(result)
139 return result
140
141 full_eval = _eval(t_hi)
142 if not full_eval.success:
144 success=False,
145 target_apogee_m=target_apogee_m,
146 apogee_tolerance_m=apogee_tolerance_m,
147 optimal_burn_time_s=0.0,
148 optimal_lox_kg=0.0,
149 optimal_fuel_kg=0.0,
150 achieved_apogee_m=0.0,
151 apogee_error_m=target_apogee_m,
152 total_impulse_Ns=0.0,
153 simulations_run=sim_count,
154 infeasible_reason=full_eval.error or "Flight simulation failed at maximum burn time",
155 evaluations=evaluations,
156 )
157
158 target_min = target_apogee_m - apogee_tolerance_m
159 if full_eval.apogee_m < target_min:
160 lox_full, fuel_full, impulse_full = required_propellant_at_burn_time(
161 times, thrust, mdot_O, mdot_F, t_hi
162 )
164 success=False,
165 target_apogee_m=target_apogee_m,
166 apogee_tolerance_m=apogee_tolerance_m,
167 optimal_burn_time_s=t_hi,
168 optimal_lox_kg=lox_full,
169 optimal_fuel_kg=fuel_full,
170 achieved_apogee_m=full_eval.apogee_m,
171 apogee_error_m=target_apogee_m - full_eval.apogee_m,
172 total_impulse_Ns=impulse_full,
173 simulations_run=sim_count,
174 infeasible_reason=(
175 f"Full time-series burn ({t_hi:.2f}s) reaches only {full_eval.apogee_m:.0f} m "
176 f"(target {target_apogee_m:.0f} m). Re-run time-series with higher thrust or longer burn."
177 ),
178 evaluations=evaluations,
179 )
180
181 lo_eval = _eval(t_lo)
182 if lo_eval.success and lo_eval.apogee_m >= target_min:
183 best = lo_eval
184 else:
185 lo = t_lo
186 hi = t_hi
187 best = full_eval
188 for _ in range(max_iterations):
189 if hi - lo <= time_resolution_s:
190 break
191 mid = 0.5 * (lo + hi)
192 mid_eval = _eval(mid)
193 if not mid_eval.success:
194 lo = mid
195 continue
196 if mid_eval.apogee_m >= target_min:
197 hi = mid
198 best = mid_eval
199 else:
200 lo = mid
201
202 feasible = [
203 e for e in evaluations
204 if e.success and e.apogee_m >= target_min
205 ]
206 if feasible:
207 best = min(feasible, key=lambda e: e.fuel_required_kg)
208
209 lox_opt, fuel_opt, impulse_opt = required_propellant_at_burn_time(
210 times, thrust, mdot_O, mdot_F, best.burn_time_s
211 )
212
214 success=True,
215 target_apogee_m=target_apogee_m,
216 apogee_tolerance_m=apogee_tolerance_m,
217 optimal_burn_time_s=best.burn_time_s,
218 optimal_lox_kg=lox_opt,
219 optimal_fuel_kg=fuel_opt,
220 achieved_apogee_m=best.apogee_m,
221 apogee_error_m=best.apogee_m - target_apogee_m,
222 total_impulse_Ns=impulse_opt,
223 simulations_run=sim_count,
224 evaluations=evaluations,
225 )
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
Tuple[float, float, float] required_propellant_at_burn_time(np.ndarray times, np.ndarray thrust, np.ndarray mdot_O, np.ndarray mdot_F, float burn_time_s)
float _integrate(np.ndarray times, np.ndarray values)
Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] truncate_time_series(np.ndarray times, np.ndarray thrust, np.ndarray mdot_O, np.ndarray mdot_F, float burn_time_s)
AltitudeOptimizeResult optimize_minimum_fuel_burn_time(*, np.ndarray times, np.ndarray thrust, np.ndarray mdot_O, np.ndarray mdot_F, float target_apogee_m, float apogee_tolerance_m, Callable[[float], BurnTimeEval] simulate_at_burn_time, Optional[float] min_burn_time_s=None, Optional[float] max_burn_time_s=None, float time_resolution_s=0.05, int max_iterations=24)