19 time_history: np.ndarray,
20 thrust_history: np.ndarray,
21 Pc_history: np.ndarray,
22 Isp_history: np.ndarray,
23 MR_history: np.ndarray,
24 mdot_history: np.ndarray,
25 A_throat_history: Optional[np.ndarray] =
None,
26 recession_history: Optional[np.ndarray] =
None,
29 Analyze performance degradation over a burn.
35 thrust_history : array
45 A_throat_history : array, optional
46 Throat area evolution [m²]
47 recession_history : array, optional
48 Cumulative recession [m]
53 Degradation metrics and analysis
55 if len(time_history) < 2:
56 return {
"error":
"Need at least 2 time points"}
60 "thrust": float(thrust_history[0]),
61 "Pc": float(Pc_history[0]),
62 "Isp": float(Isp_history[0]),
63 "MR": float(MR_history[0]),
64 "mdot": float(mdot_history[0]),
68 "thrust": float(thrust_history[-1]),
69 "Pc": float(Pc_history[-1]),
70 "Isp": float(Isp_history[-1]),
71 "MR": float(MR_history[-1]),
72 "mdot": float(mdot_history[-1]),
77 "thrust_pct": ((final[
"thrust"] - initial[
"thrust"]) / initial[
"thrust"]) * 100.0,
78 "Pc_pct": ((final[
"Pc"] - initial[
"Pc"]) / initial[
"Pc"]) * 100.0,
79 "Isp_pct": ((final[
"Isp"] - initial[
"Isp"]) / initial[
"Isp"]) * 100.0,
80 "MR_pct": ((final[
"MR"] - initial[
"MR"]) / initial[
"MR"]) * 100.0,
81 "mdot_pct": ((final[
"mdot"] - initial[
"mdot"]) / initial[
"mdot"]) * 100.0,
85 if A_throat_history
is not None and len(A_throat_history) > 0:
86 A_throat_initial = float(A_throat_history[0])
87 A_throat_final = float(A_throat_history[-1])
88 changes[
"A_throat_pct"] = ((A_throat_final - A_throat_initial) / A_throat_initial) * 100.0
90 changes[
"A_throat_pct"] = np.nan
93 burn_time = float(time_history[-1] - time_history[0])
94 total_impulse = float(np.trapezoid(thrust_history, time_history))
97 avg_thrust = float(np.mean(thrust_history))
98 avg_Pc = float(np.mean(Pc_history))
99 avg_Isp = float(np.mean(Isp_history))
100 avg_MR = float(np.mean(MR_history))
101 avg_mdot = float(np.mean(mdot_history))
104 peak_thrust = float(np.max(thrust_history))
105 peak_Pc = float(np.max(Pc_history))
108 min_thrust = float(np.min(thrust_history))
109 min_Isp = float(np.min(Isp_history))
112 if len(time_history) > 2:
114 thrust_slope, thrust_intercept = np.polyfit(time_history, thrust_history, 1)
115 Pc_slope, Pc_intercept = np.polyfit(time_history, Pc_history, 1)
116 degradation_rates = {
117 "thrust_rate": float(thrust_slope),
118 "Pc_rate": float(Pc_slope),
121 degradation_rates = {
127 recession_analysis = {}
128 if recession_history
is not None and len(recession_history) > 0:
129 total_recession = float(recession_history[-1] - recession_history[0])
130 avg_recession_rate = total_recession / burn_time
if burn_time > 0
else 0.0
131 recession_analysis = {
132 "total_recession": float(total_recession),
133 "avg_recession_rate": float(avg_recession_rate),
134 "recession_rate_units":
"m/s",
140 "thrust_drop_50pct_time":
None,
141 "thrust_drop_80pct_time":
None,
142 "geometry_limit_exceeded":
False,
145 if len(time_history) > 2:
146 thrust_50pct = initial[
"thrust"] * 0.5
147 thrust_80pct = initial[
"thrust"] * 0.8
150 for i, thrust
in enumerate(thrust_history):
151 if thrust < thrust_50pct
and failure_analysis[
"thrust_drop_50pct_time"]
is None:
152 failure_analysis[
"thrust_drop_50pct_time"] = float(time_history[i])
153 if thrust < thrust_80pct
and failure_analysis[
"thrust_drop_80pct_time"]
is None:
154 failure_analysis[
"thrust_drop_80pct_time"] = float(time_history[i])
157 if A_throat_history
is not None:
158 A_throat_growth = (A_throat_history[-1] / A_throat_history[0] - 1.0) * 100.0
159 if A_throat_growth > 20.0:
160 failure_analysis[
"geometry_limit_exceeded"] =
True
163 "burn_time": float(burn_time),
164 "total_impulse": float(total_impulse),
169 "thrust": avg_thrust,
176 "thrust": peak_thrust,
180 "thrust": min_thrust,
183 "degradation_rates": degradation_rates,
184 "recession_analysis": recession_analysis,
185 "failure_analysis": failure_analysis,
190 time_history: np.ndarray,
191 thrust_history: np.ndarray,
192 mdot_history: np.ndarray,
193 altitude_history: Optional[np.ndarray] =
None,
194 target_altitude: Optional[float] =
None,
197 Calculate mission performance metrics.
203 thrust_history : array
206 Mass flow rate [kg/s]
207 altitude_history : array, optional
208 Altitude [m] (for altitude-specific analysis)
209 target_altitude : float, optional
215 Mission performance metrics
217 burn_time = float(time_history[-1] - time_history[0])
220 total_impulse = float(np.trapezoid(thrust_history, time_history))
223 total_propellant = float(np.trapezoid(mdot_history, time_history))
226 avg_thrust = float(np.mean(thrust_history))
233 if total_propellant > 0:
234 avg_Isp = total_impulse / (total_propellant * 9.80665)
242 "burn_time": float(burn_time),
243 "total_impulse": float(total_impulse),
244 "total_propellant": float(total_propellant),
245 "avg_thrust": float(avg_thrust),
246 "avg_Isp": float(avg_Isp),
250 if altitude_history
is not None and len(altitude_history) == len(time_history):
251 results[
"altitude_gained"] = float(altitude_history[-1] - altitude_history[0])
252 results[
"max_altitude"] = float(np.max(altitude_history))
254 if target_altitude
is not None:
255 results[
"target_reached"] = results[
"max_altitude"] >= target_altitude
256 results[
"altitude_margin"] = results[
"max_altitude"] - target_altitude
262 time_history: np.ndarray,
263 thrust_history: np.ndarray,
264 mdot_history: np.ndarray,
265 propellant_mass_remaining: float,
266 threshold_thrust: Optional[float] =
None,
269 Predict when engine will burn out (propellant depletion or thrust threshold).
275 thrust_history : array
278 Mass flow rate [kg/s]
279 propellant_mass_remaining : float
280 Remaining propellant mass [kg]
281 threshold_thrust : float, optional
282 Thrust threshold for "burnout" [N]
289 current_time = float(time_history[-1])
290 current_mdot = float(mdot_history[-1])
294 time_to_depletion = propellant_mass_remaining / current_mdot
295 depletion_time = current_time + time_to_depletion
297 time_to_depletion = np.inf
298 depletion_time = np.inf
301 if threshold_thrust
is not None:
302 current_thrust = float(thrust_history[-1])
304 if current_thrust <= threshold_thrust:
305 threshold_time = current_time
306 time_to_threshold = 0.0
309 if len(time_history) > 2:
311 slope, intercept = np.polyfit(time_history[-10:], thrust_history[-10:], 1)
313 time_to_threshold = (threshold_thrust - intercept) / slope - current_time
314 threshold_time = current_time + time_to_threshold
316 threshold_time = np.inf
317 time_to_threshold = np.inf
319 threshold_time = np.inf
320 time_to_threshold = np.inf
322 threshold_time = np.nan
323 time_to_threshold = np.nan
326 if threshold_thrust
is not None and not np.isnan(threshold_time):
327 burnout_time =
min(depletion_time, threshold_time)
328 burnout_type =
"threshold" if threshold_time < depletion_time
else "depletion"
330 burnout_time = depletion_time
331 burnout_type =
"depletion"
334 "burnout_time": float(burnout_time),
335 "burnout_type": burnout_type,
336 "time_to_burnout": float(burnout_time - current_time),
337 "depletion_time": float(depletion_time),
338 "time_to_depletion": float(time_to_depletion),
339 "threshold_time": float(threshold_time)
if threshold_thrust
is not None else np.nan,
340 "time_to_threshold": float(time_to_threshold)
if threshold_thrust
is not None else np.nan,
345 time_history: np.ndarray,
346 results_dict: Dict[str, np.ndarray],
347 config: PintleEngineConfig,
350 Generate a comprehensive text report of burn analysis.
357 Results dictionary from evaluate_arrays_with_time
358 config : PintleEngineConfig
364 Formatted text report
368 results_dict.get(
"F", np.zeros_like(time_history)),
369 results_dict.get(
"Pc", np.zeros_like(time_history)),
370 results_dict.get(
"Isp", np.zeros_like(time_history)),
371 results_dict.get(
"MR", np.zeros_like(time_history)),
372 results_dict.get(
"mdot_total", np.zeros_like(time_history)),
373 results_dict.get(
"A_throat",
None),
374 results_dict.get(
"recession_throat",
None),
379 results_dict.get(
"F", np.zeros_like(time_history)),
380 results_dict.get(
"mdot_total", np.zeros_like(time_history)),
385 "BURN ANALYSIS REPORT",
388 f
"Burn Duration: {degradation['burn_time']:.2f} s",
389 f
"Total Impulse: {mission['total_impulse']/1000:.2f} kN·s",
390 f
"Total Propellant: {mission['total_propellant']:.2f} kg",
392 "PERFORMANCE SUMMARY:",
393 f
" Initial Thrust: {degradation['initial']['thrust']/1000:.2f} kN",
394 f
" Final Thrust: {degradation['final']['thrust']/1000:.2f} kN",
395 f
" Change: {degradation['changes']['thrust_pct']:+.2f}%",
397 f
" Initial Isp: {degradation['initial']['Isp']:.1f} s",
398 f
" Final Isp: {degradation['final']['Isp']:.1f} s",
399 f
" Change: {degradation['changes']['Isp_pct']:+.2f}%",
401 f
" Initial Pc: {degradation['initial']['Pc']/1e6:.2f} MPa",
402 f
" Final Pc: {degradation['final']['Pc']/1e6:.2f} MPa",
403 f
" Change: {degradation['changes']['Pc_pct']:+.2f}%",
407 if not np.isnan(degradation[
'changes'].get(
'A_throat_pct', np.nan)):
409 f
" Throat Area Growth: {degradation['changes']['A_throat_pct']:+.2f}%"
412 if degradation[
'recession_analysis']:
413 report_lines.extend([
415 "ABLATIVE RECESSION:",
416 f
" Total Recession: {degradation['recession_analysis']['total_recession']*1e6:.2f} µm",
417 f
" Avg Rate: {degradation['recession_analysis']['avg_recession_rate']*1e6:.3f} µm/s",
420 report_lines.extend([
422 "DEGRADATION RATES:",
423 f
" Thrust: {degradation['degradation_rates']['thrust_rate']:.2f} N/s",
424 f
" Pc: {degradation['degradation_rates']['Pc_rate']/1e3:.2f} kPa/s",
427 if degradation[
'failure_analysis'][
'thrust_drop_50pct_time']:
429 f
"\n⚠️ Thrust dropped 50% at t = {degradation['failure_analysis']['thrust_drop_50pct_time']:.2f} s"
432 report_lines.append(
"\n" +
"=" * 80)
434 return "\n".join(report_lines)
Dict[str, any] analyze_burn_degradation(np.ndarray time_history, np.ndarray thrust_history, np.ndarray Pc_history, np.ndarray Isp_history, np.ndarray MR_history, np.ndarray mdot_history, Optional[np.ndarray] A_throat_history=None, Optional[np.ndarray] recession_history=None)