STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
burn_analysis.py
Go to the documentation of this file.
1"""Comprehensive burn analysis for time-varying engine performance.
2
3This module provides:
41. Performance degradation tracking
52. Ablative geometry evolution
63. Thrust curve analysis
74. Failure prediction
85. Mission performance metrics
9"""
10
11from __future__ import annotations
12
13from typing import Dict, List, Tuple, Optional
14import numpy as np
15from engine.pipeline.config_schemas import PintleEngineConfig
16
17
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,
27) -> Dict[str, any]:
28 """
29 Analyze performance degradation over a burn.
30
31 Parameters:
32 -----------
33 time_history : array
34 Time points [s]
35 thrust_history : array
36 Thrust [N]
37 Pc_history : array
38 Chamber pressure [Pa]
39 Isp_history : array
40 Specific impulse [s]
41 MR_history : array
42 Mixture ratio
43 mdot_history : array
44 Mass flow rate [kg/s]
45 A_throat_history : array, optional
46 Throat area evolution [m²]
47 recession_history : array, optional
48 Cumulative recession [m]
49
50 Returns:
51 --------
52 results : dict
53 Degradation metrics and analysis
54 """
55 if len(time_history) < 2:
56 return {"error": "Need at least 2 time points"}
57
58 # Initial and final values
59 initial = {
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]),
65 }
66
67 final = {
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]),
73 }
74
75 # Percentage changes
76 changes = {
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,
82 }
83
84 # Throat area change
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
89 else:
90 changes["A_throat_pct"] = np.nan
91
92 # Total impulse
93 burn_time = float(time_history[-1] - time_history[0])
94 total_impulse = float(np.trapezoid(thrust_history, time_history)) # N·s
95
96 # Average performance
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))
102
103 # Peak values
104 peak_thrust = float(np.max(thrust_history))
105 peak_Pc = float(np.max(Pc_history))
106
107 # Minimum values
108 min_thrust = float(np.min(thrust_history))
109 min_Isp = float(np.min(Isp_history))
110
111 # Degradation rate (linear fit)
112 if len(time_history) > 2:
113 # Linear regression for degradation rate
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), # N/s
118 "Pc_rate": float(Pc_slope), # Pa/s
119 }
120 else:
121 degradation_rates = {
122 "thrust_rate": 0.0,
123 "Pc_rate": 0.0,
124 }
125
126 # Recession analysis
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",
135 }
136
137 # Failure prediction (simplified)
138 # Predict when thrust drops below threshold or geometry limits exceeded
139 failure_analysis = {
140 "thrust_drop_50pct_time": None, # Time when thrust drops 50%
141 "thrust_drop_80pct_time": None, # Time when thrust drops 80%
142 "geometry_limit_exceeded": False,
143 }
144
145 if len(time_history) > 2:
146 thrust_50pct = initial["thrust"] * 0.5
147 thrust_80pct = initial["thrust"] * 0.8
148
149 # Find when thresholds crossed
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])
155
156 # Check geometry limits (if throat area grows too much)
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: # More than 20% growth
160 failure_analysis["geometry_limit_exceeded"] = True
161
162 return {
163 "burn_time": float(burn_time),
164 "total_impulse": float(total_impulse),
165 "initial": initial,
166 "final": final,
167 "changes": changes,
168 "average": {
169 "thrust": avg_thrust,
170 "Pc": avg_Pc,
171 "Isp": avg_Isp,
172 "MR": avg_MR,
173 "mdot": avg_mdot,
174 },
175 "peak": {
176 "thrust": peak_thrust,
177 "Pc": peak_Pc,
178 },
179 "minimum": {
180 "thrust": min_thrust,
181 "Isp": min_Isp,
182 },
183 "degradation_rates": degradation_rates,
184 "recession_analysis": recession_analysis,
185 "failure_analysis": failure_analysis,
186 }
187
188
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,
195) -> Dict[str, any]:
196 """
197 Calculate mission performance metrics.
198
199 Parameters:
200 -----------
201 time_history : array
202 Time points [s]
203 thrust_history : array
204 Thrust [N]
205 mdot_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
210 Target altitude [m]
211
212 Returns:
213 --------
214 results : dict
215 Mission performance metrics
216 """
217 burn_time = float(time_history[-1] - time_history[0])
218
219 # Total impulse
220 total_impulse = float(np.trapezoid(thrust_history, time_history)) # N·s
221
222 # Total propellant consumed
223 total_propellant = float(np.trapezoid(mdot_history, time_history)) # kg
224
225 # Average thrust
226 avg_thrust = float(np.mean(thrust_history))
227
228 # Thrust-to-weight ratio (requires vehicle mass - placeholder)
229 # TWR = thrust / (mass × g)
230 # This would need vehicle mass as input
231
232 # Specific impulse (time-averaged)
233 if total_propellant > 0:
234 avg_Isp = total_impulse / (total_propellant * 9.80665)
235 else:
236 avg_Isp = 0.0
237
238 # Thrust coefficient (time-averaged, simplified)
239 # Would need Pc and At for accurate calculation
240
241 results = {
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),
247 }
248
249 # Altitude-specific analysis
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))
253
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
257
258 return results
259
260
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,
267) -> Dict[str, any]:
268 """
269 Predict when engine will burn out (propellant depletion or thrust threshold).
270
271 Parameters:
272 -----------
273 time_history : array
274 Time points [s]
275 thrust_history : array
276 Thrust [N]
277 mdot_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]
283
284 Returns:
285 --------
286 results : dict
287 Burnout predictions
288 """
289 current_time = float(time_history[-1])
290 current_mdot = float(mdot_history[-1])
291
292 # Propellant depletion time
293 if current_mdot > 0:
294 time_to_depletion = propellant_mass_remaining / current_mdot
295 depletion_time = current_time + time_to_depletion
296 else:
297 time_to_depletion = np.inf
298 depletion_time = np.inf
299
300 # Thrust threshold burnout
301 if threshold_thrust is not None:
302 current_thrust = float(thrust_history[-1])
303
304 if current_thrust <= threshold_thrust:
305 threshold_time = current_time
306 time_to_threshold = 0.0
307 else:
308 # Extrapolate using degradation rate
309 if len(time_history) > 2:
310 # Linear fit
311 slope, intercept = np.polyfit(time_history[-10:], thrust_history[-10:], 1)
312 if slope < 0: # Degrading
313 time_to_threshold = (threshold_thrust - intercept) / slope - current_time
314 threshold_time = current_time + time_to_threshold
315 else:
316 threshold_time = np.inf
317 time_to_threshold = np.inf
318 else:
319 threshold_time = np.inf
320 time_to_threshold = np.inf
321 else:
322 threshold_time = np.nan
323 time_to_threshold = np.nan
324
325 # Use whichever comes first
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"
329 else:
330 burnout_time = depletion_time
331 burnout_type = "depletion"
332
333 return {
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,
341 }
342
343
345 time_history: np.ndarray,
346 results_dict: Dict[str, np.ndarray],
347 config: PintleEngineConfig,
348) -> str:
349 """
350 Generate a comprehensive text report of burn analysis.
351
352 Parameters:
353 -----------
354 time_history : array
355 Time points [s]
356 results_dict : dict
357 Results dictionary from evaluate_arrays_with_time
358 config : PintleEngineConfig
359 Engine configuration
360
361 Returns:
362 --------
363 report : str
364 Formatted text report
365 """
366 degradation = analyze_burn_degradation(
367 time_history,
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),
375 )
376
378 time_history,
379 results_dict.get("F", np.zeros_like(time_history)),
380 results_dict.get("mdot_total", np.zeros_like(time_history)),
381 )
382
383 report_lines = [
384 "=" * 80,
385 "BURN ANALYSIS REPORT",
386 "=" * 80,
387 "",
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",
391 "",
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}%",
396 "",
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}%",
400 "",
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}%",
404 "",
405 ]
406
407 if not np.isnan(degradation['changes'].get('A_throat_pct', np.nan)):
408 report_lines.append(
409 f" Throat Area Growth: {degradation['changes']['A_throat_pct']:+.2f}%"
410 )
411
412 if degradation['recession_analysis']:
413 report_lines.extend([
414 "",
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",
418 ])
419
420 report_lines.extend([
421 "",
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",
425 ])
426
427 if degradation['failure_analysis']['thrust_drop_50pct_time']:
428 report_lines.append(
429 f"\n⚠️ Thrust dropped 50% at t = {degradation['failure_analysis']['thrust_drop_50pct_time']:.2f} s"
430 )
431
432 report_lines.append("\n" + "=" * 80)
433
434 return "\n".join(report_lines)
435
#define min(a, b)
Definition Arduino.h:42
str generate_burn_report(np.ndarray time_history, Dict[str, np.ndarray] results_dict, PintleEngineConfig config)
Dict[str, any] calculate_mission_performance(np.ndarray time_history, np.ndarray thrust_history, np.ndarray mdot_history, Optional[np.ndarray] altitude_history=None, Optional[float] target_altitude=None)
Dict[str, any] predict_burnout_time(np.ndarray time_history, np.ndarray thrust_history, np.ndarray mdot_history, float propellant_mass_remaining, Optional[float] threshold_thrust=None)
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)