STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
numerical_robustness.py
Go to the documentation of this file.
1"""Numerical robustness and validation framework for physics calculations.
2
3This module provides:
41. Dimensional consistency checking
52. Numerical stability validation
63. Physical constraint validation
74. Convergence diagnostics
85. Error propagation analysis
9"""
10
11from __future__ import annotations
12
13from typing import Dict, List, Tuple, Optional, Any
14import numpy as np
15from dataclasses import dataclass
16
17
18@dataclass
20 """Result of a validation check"""
21 passed: bool
22 message: str
23 value: Optional[float] = None
24 expected_range: Optional[Tuple[float, float]] = None
25 severity: str = "warning" # "error", "warning", "info"
26
27
29 """Validates dimensional consistency of physical quantities"""
30
31 # Base SI units: [m, kg, s, K, A, mol, cd]
32 DIMENSIONS = {
33 "length": [1, 0, 0, 0, 0, 0, 0],
34 "mass": [0, 1, 0, 0, 0, 0, 0],
35 "time": [0, 0, 1, 0, 0, 0, 0],
36 "temperature": [0, 0, 0, 1, 0, 0, 0],
37 "pressure": [-1, 1, -2, 0, 0, 0, 0], # Pa = kg/(m·s²)
38 "density": [-3, 1, 0, 0, 0, 0, 0], # kg/m³
39 "velocity": [1, 0, -1, 0, 0, 0, 0], # m/s
40 "force": [1, 1, -2, 0, 0, 0, 0], # N = kg·m/s²
41 "energy": [2, 1, -2, 0, 0, 0, 0], # J = kg·m²/s²
42 "power": [2, 1, -3, 0, 0, 0, 0], # W = kg·m²/s³
43 "mass_flow": [0, 1, -1, 0, 0, 0, 0], # kg/s
44 "heat_flux": [0, 1, -3, 0, 0, 0, 0], # W/m² = kg/s³
45 "viscosity": [-1, 1, -1, 0, 0, 0, 0], # Pa·s = kg/(m·s)
46 "thermal_conductivity": [1, 1, -3, -1, 0, 0, 0], # W/(m·K)
47 "specific_heat": [2, 0, -2, -1, 0, 0, 0], # J/(kg·K)
48 }
49
50 @classmethod
51 def validate_equation(cls, left_side: Dict[str, float], right_side: Dict[str, float]) -> bool:
52 """
53 Validate that left_side and right_side have same dimensions.
54
55 Parameters:
56 -----------
57 left_side : dict
58 {quantity_name: value} where quantity_name is a key in DIMENSIONS
59 right_side : dict
60 Same format as left_side
61
62 Returns:
63 --------
64 is_valid : bool
65 """
66 left_dims = np.zeros(7)
67 right_dims = np.zeros(7)
68
69 for name, value in left_side.items():
70 if name in cls.DIMENSIONS:
71 left_dims += np.array(cls.DIMENSIONS[name]) * value
72
73 for name, value in right_side.items():
74 if name in cls.DIMENSIONS:
75 right_dims += np.array(cls.DIMENSIONS[name]) * value
76
77 return np.allclose(left_dims, right_dims, atol=1e-10)
78
79
81 """Validates physical constraints and bounds"""
82
83 @staticmethod
84 def validate_pressure(P: float, name: str = "pressure") -> ValidationResult:
85 """Validate pressure is physically reasonable"""
86 if not np.isfinite(P):
87 return ValidationResult(False, f"{name} is not finite", P, severity="error")
88 if P < 0:
89 return ValidationResult(False, f"{name} is negative: {P:.2e} Pa", P, (0, np.inf), "error")
90 if P > 1e9: # 1 GPa
91 return ValidationResult(False, f"{name} is extremely high: {P/1e6:.2f} MPa", P, (0, 1e9), "warning")
92 return ValidationResult(True, f"{name} is valid: {P/1e6:.2f} MPa", P)
93
94 @staticmethod
95 def validate_temperature(T: float, name: str = "temperature") -> ValidationResult:
96 """Validate temperature is physically reasonable"""
97 if not np.isfinite(T):
98 return ValidationResult(False, f"{name} is not finite", T, severity="error")
99 if T < 0:
100 return ValidationResult(False, f"{name} is negative: {T:.2f} K", T, (0, np.inf), "error")
101 if T > 5000: # Very high for rocket engines
102 return ValidationResult(False, f"{name} is extremely high: {T:.2f} K", T, (0, 5000), "warning")
103 return ValidationResult(True, f"{name} is valid: {T:.2f} K", T)
104
105 @staticmethod
106 def validate_mass_flow(mdot: float, name: str = "mass_flow") -> ValidationResult:
107 """Validate mass flow is physically reasonable"""
108 if not np.isfinite(mdot):
109 return ValidationResult(False, f"{name} is not finite", mdot, severity="error")
110 if mdot < 0:
111 return ValidationResult(False, f"{name} is negative: {mdot:.4f} kg/s", mdot, (0, np.inf), "error")
112 if mdot > 1000: # Very high for typical engines
113 return ValidationResult(False, f"{name} is extremely high: {mdot:.2f} kg/s", mdot, (0, 1000), "warning")
114 return ValidationResult(True, f"{name} is valid: {mdot:.4f} kg/s", mdot)
115
116 @staticmethod
117 def validate_mixture_ratio(MR: float) -> ValidationResult:
118 """Validate mixture ratio is physically reasonable"""
119 if not np.isfinite(MR):
120 return ValidationResult(False, "MR is not finite", MR, severity="error")
121 if MR <= 0:
122 return ValidationResult(False, f"MR is non-positive: {MR:.4f}", MR, (0.1, 20), "error")
123 if MR > 20:
124 return ValidationResult(False, f"MR is extremely high: {MR:.2f}", MR, (0.1, 20), "warning")
125 return ValidationResult(True, f"MR is valid: {MR:.4f}", MR)
126
127 @staticmethod
128 def validate_area(A: float, name: str = "area") -> ValidationResult:
129 """Validate area is physically reasonable"""
130 if not np.isfinite(A):
131 return ValidationResult(False, f"{name} is not finite", A, severity="error")
132 if A <= 0:
133 return ValidationResult(False, f"{name} is non-positive: {A:.2e} m²", A, (1e-6, 1.0), "error")
134 if A > 1.0: # 1 m² is very large
135 return ValidationResult(False, f"{name} is extremely large: {A:.4f} m²", A, (1e-6, 1.0), "warning")
136 return ValidationResult(True, f"{name} is valid: {A:.2e} m²", A)
137
138 @staticmethod
139 def validate_velocity(v: float, name: str = "velocity") -> ValidationResult:
140 """Validate velocity is physically reasonable"""
141 if not np.isfinite(v):
142 return ValidationResult(False, f"{name} is not finite", v, severity="error")
143 if v < 0:
144 return ValidationResult(False, f"{name} is negative: {v:.2f} m/s", v, (0, 5000), "error")
145 if v > 5000: # Very high for rocket exhaust
146 return ValidationResult(False, f"{name} is extremely high: {v:.2f} m/s", v, (0, 5000), "warning")
147 return ValidationResult(True, f"{name} is valid: {v:.2f} m/s", v)
148
149 @staticmethod
150 def validate_gamma(gamma: float) -> ValidationResult:
151 """Validate specific heat ratio"""
152 if not np.isfinite(gamma):
153 return ValidationResult(False, "gamma is not finite", gamma, severity="error")
154 if gamma <= 1.0:
155 return ValidationResult(False, f"gamma <= 1.0: {gamma:.4f}", gamma, (1.0, 2.0), "error")
156 if gamma > 2.0:
157 return ValidationResult(False, f"gamma > 2.0: {gamma:.4f}", gamma, (1.0, 2.0), "warning")
158 return ValidationResult(True, f"gamma is valid: {gamma:.4f}", gamma)
159
160 @staticmethod
161 def validate_cstar(cstar: float) -> ValidationResult:
162 """Validate characteristic velocity"""
163 if not np.isfinite(cstar):
164 return ValidationResult(False, "c* is not finite", cstar, severity="error")
165 if cstar <= 0:
166 return ValidationResult(False, f"c* is non-positive: {cstar:.2f} m/s", cstar, (500, 3000), "error")
167 if cstar < 500 or cstar > 3000:
168 return ValidationResult(False, f"c* is outside typical range: {cstar:.2f} m/s", cstar, (500, 3000), "warning")
169 return ValidationResult(True, f"c* is valid: {cstar:.2f} m/s", cstar)
170
171 @staticmethod
172 def validate_isp(Isp: float) -> ValidationResult:
173 """Validate specific impulse"""
174 if not np.isfinite(Isp):
175 return ValidationResult(False, "Isp is not finite", Isp, severity="error")
176 if Isp <= 0:
177 return ValidationResult(False, f"Isp is non-positive: {Isp:.2f} s", Isp, (100, 500), "error")
178 if Isp < 100 or Isp > 500:
179 return ValidationResult(False, f"Isp is outside typical range: {Isp:.2f} s", Isp, (100, 500), "warning")
180 return ValidationResult(True, f"Isp is valid: {Isp:.2f} s", Isp)
181
182
184 """Numerical stability checks and improvements"""
185
186 @staticmethod
187 def check_condition_number(A: np.ndarray, name: str = "matrix") -> ValidationResult:
188 """Check condition number of a matrix"""
189 if A.size == 0:
190 return ValidationResult(False, f"{name} is empty", severity="error")
191
192 try:
193 cond = np.linalg.cond(A)
194 if cond > 1e12:
195 return ValidationResult(False, f"{name} is ill-conditioned: κ = {cond:.2e}", cond, severity="error")
196 elif cond > 1e8:
197 return ValidationResult(False, f"{name} is poorly conditioned: κ = {cond:.2e}", cond, severity="warning")
198 return ValidationResult(True, f"{name} condition number: {cond:.2e}", cond)
199 except np.linalg.LinAlgError:
200 return ValidationResult(False, f"{name} is singular", severity="error")
201
202 @staticmethod
203 def safe_divide(numerator: float, denominator: float, default: float = 0.0, name: str = "ratio") -> Tuple[float, ValidationResult]:
204 """Safely divide with validation"""
205 if not np.isfinite(numerator) or not np.isfinite(denominator):
206 return default, ValidationResult(False, f"{name}: non-finite inputs", severity="error")
207
208 if abs(denominator) < 1e-12:
209 return default, ValidationResult(False, f"{name}: division by near-zero ({denominator:.2e})", severity="error")
210
211 result = numerator / denominator
212 if not np.isfinite(result):
213 return default, ValidationResult(False, f"{name}: result is not finite", result, severity="error")
214
215 return result, ValidationResult(True, f"{name} = {result:.6e}", result)
216
217 @staticmethod
218 def safe_sqrt(value: float, name: str = "sqrt") -> Tuple[float, ValidationResult]:
219 """Safely compute square root with validation"""
220 if not np.isfinite(value):
221 return 0.0, ValidationResult(False, f"{name}: input is not finite", value, severity="error")
222
223 if value < 0:
224 return 0.0, ValidationResult(False, f"{name}: negative input ({value:.2e})", value, severity="error")
225
226 result = np.sqrt(value)
227 return result, ValidationResult(True, f"{name} = {result:.6e}", result)
228
229 @staticmethod
230 def safe_log(value: float, name: str = "log") -> Tuple[float, ValidationResult]:
231 """Safely compute logarithm with validation"""
232 if not np.isfinite(value):
233 return 0.0, ValidationResult(False, f"{name}: input is not finite", value, severity="error")
234
235 if value <= 0:
236 return 0.0, ValidationResult(False, f"{name}: non-positive input ({value:.2e})", value, severity="error")
237
238 result = np.log(value)
239 if not np.isfinite(result):
240 return 0.0, ValidationResult(False, f"{name}: result is not finite", result, severity="error")
241
242 return result, ValidationResult(True, f"{name} = {result:.6e}", result)
243
244 @staticmethod
246 residuals: List[float],
247 tolerance: float,
248 min_iterations: int = 3,
249 ) -> ValidationResult:
250 """Check convergence of iterative method"""
251 if len(residuals) < min_iterations:
252 return ValidationResult(False, f"Too few iterations: {len(residuals)}", severity="warning")
253
254 if not all(np.isfinite(r) for r in residuals):
255 return ValidationResult(False, "Non-finite residuals", severity="error")
256
257 final_residual = abs(residuals[-1])
258 if final_residual > tolerance:
259 return ValidationResult(False, f"Not converged: |residual| = {final_residual:.2e} > {tolerance:.2e}",
260 final_residual, severity="error")
261
262 # Check for monotonic decrease (good convergence behavior)
263 if len(residuals) >= 3:
264 recent = residuals[-3:]
265 if not all(abs(recent[i]) <= abs(recent[i-1]) * 1.1 for i in range(1, len(recent))):
266 return ValidationResult(False, "Non-monotonic convergence", severity="warning")
267
268 return ValidationResult(True, f"Converged in {len(residuals)} iterations", final_residual)
269
270 @staticmethod
271 def check_bracket(f: callable, a: float, b: float) -> ValidationResult:
272 """Check that bracket [a, b] contains a root"""
273 if a >= b:
274 return ValidationResult(False, f"Invalid bracket: a ({a:.2e}) >= b ({b:.2e})", severity="error")
275
276 try:
277 f_a = f(a)
278 f_b = f(b)
279 except Exception as e:
280 return ValidationResult(False, f"Error evaluating bracket: {e}", severity="error")
281
282 if not (np.isfinite(f_a) and np.isfinite(f_b)):
283 return ValidationResult(False, "Non-finite function values at bracket", severity="error")
284
285 if np.sign(f_a) == np.sign(f_b):
286 return ValidationResult(False,
287 f"Bracket does not contain root: f(a)={f_a:.2e}, f(b)={f_b:.2e}",
288 severity="error")
289
290 return ValidationResult(True, f"Valid bracket: f(a)={f_a:.2e}, f(b)={f_b:.2e}")
291
292
294 """Validates physical relationships and conservation laws"""
295
296 @staticmethod
298 mdot_in: float,
299 mdot_out: float,
300 tolerance: float = 1e-6,
301 ) -> ValidationResult:
302 """Validate mass conservation"""
303 if not (np.isfinite(mdot_in) and np.isfinite(mdot_out)):
304 return ValidationResult(False, "Non-finite mass flows", severity="error")
305
306 error = abs(mdot_in - mdot_out)
307 relative_error = error / max(abs(mdot_in), abs(mdot_out), 1e-12)
308
309 if relative_error > tolerance:
310 return ValidationResult(False,
311 f"Mass not conserved: error = {error:.2e} kg/s ({relative_error*100:.2f}%)",
312 error, severity="error")
313
314 return ValidationResult(True, f"Mass conserved: error = {error:.2e} kg/s", error)
315
316 @staticmethod
318 energy_in: float,
319 energy_out: float,
320 tolerance: float = 1e-3, # 0.1% for energy (less strict)
321 ) -> ValidationResult:
322 """Validate energy balance"""
323 if not (np.isfinite(energy_in) and np.isfinite(energy_out)):
324 return ValidationResult(False, "Non-finite energies", severity="error")
325
326 error = abs(energy_in - energy_out)
327 relative_error = error / max(abs(energy_in), abs(energy_out), 1e-12)
328
329 if relative_error > tolerance:
330 return ValidationResult(False,
331 f"Energy not balanced: error = {error:.2e} J ({relative_error*100:.2f}%)",
332 error, severity="warning") # Warning, not error, as some loss is expected
333
334 return ValidationResult(True, f"Energy balanced: error = {error:.2e} J ({relative_error*100:.4f}%)", error)
335
336 @staticmethod
338 Pc: float,
339 P_exit: float,
340 gamma: float,
341 ) -> ValidationResult:
342 """Validate that flow is choked at throat"""
343 # For choked flow: P_exit / Pc <= (2/(gamma+1))^(gamma/(gamma-1))
344 critical_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
345 actual_ratio = P_exit / Pc if Pc > 0 else 0.0
346
347 if actual_ratio > critical_ratio * 1.01: # 1% tolerance
348 return ValidationResult(False,
349 f"Flow may not be choked: P_exit/Pc = {actual_ratio:.4f} > {critical_ratio:.4f}",
350 actual_ratio, severity="warning")
351
352 return ValidationResult(True, f"Flow is choked: P_exit/Pc = {actual_ratio:.4f}", actual_ratio)
353
354 @staticmethod
356 F_momentum: float,
357 F_pressure: float,
358 F_total: float,
359 tolerance: float = 1e-3,
360 ) -> ValidationResult:
361 """Validate thrust equation: F = F_momentum + F_pressure"""
362 if not all(np.isfinite([F_momentum, F_pressure, F_total])):
363 return ValidationResult(False, "Non-finite thrust components", severity="error")
364
365 F_calculated = F_momentum + F_pressure
366 error = abs(F_total - F_calculated)
367 relative_error = error / max(abs(F_total), abs(F_calculated), 1e-12)
368
369 if relative_error > tolerance:
370 return ValidationResult(False,
371 f"Thrust equation error: {error:.2e} N ({relative_error*100:.2f}%)",
372 error, severity="error")
373
374 return ValidationResult(True, f"Thrust equation valid: error = {error:.2e} N", error)
375
376
378 Pc: float,
379 MR: float,
380 mdot_total: float,
381 cstar: float,
382 gamma: float,
383 Tc: float,
384 Isp: float,
385 F: float,
386) -> List[ValidationResult]:
387 """
388 Comprehensive validation of engine state.
389
390 Returns list of validation results (errors first, then warnings, then info).
391 """
392 results = []
393
394 # Physical constraints
395 results.append(PhysicalConstraints.validate_pressure(Pc, "Pc"))
396 results.append(PhysicalConstraints.validate_temperature(Tc, "Tc"))
397 results.append(PhysicalConstraints.validate_mass_flow(mdot_total, "mdot_total"))
398 results.append(PhysicalConstraints.validate_mixture_ratio(MR))
399 results.append(PhysicalConstraints.validate_gamma(gamma))
400 results.append(PhysicalConstraints.validate_cstar(cstar))
401 results.append(PhysicalConstraints.validate_isp(Isp))
402
403 # Physics relationships
404 # Check: mdot = Pc * At / cstar (approximately, for choked flow)
405 # This would require At, so we skip it here
406
407 # Sort by severity: error > warning > info
408 severity_order = {"error": 0, "warning": 1, "info": 2}
409 results.sort(key=lambda r: severity_order.get(r.severity, 3))
410
411 return results
412
#define max(a, b)
Definition Arduino.h:43
bool validate_equation(cls, Dict[str, float] left_side, Dict[str, float] right_side)
ValidationResult check_bracket(callable f, float a, float b)
Tuple[float, ValidationResult] safe_sqrt(float value, str name="sqrt")
ValidationResult check_convergence(List[float] residuals, float tolerance, int min_iterations=3)
ValidationResult check_condition_number(np.ndarray A, str name="matrix")
Tuple[float, ValidationResult] safe_log(float value, str name="log")
Tuple[float, ValidationResult] safe_divide(float numerator, float denominator, float default=0.0, str name="ratio")
ValidationResult validate_temperature(float T, str name="temperature")
ValidationResult validate_area(float A, str name="area")
ValidationResult validate_mass_flow(float mdot, str name="mass_flow")
ValidationResult validate_pressure(float P, str name="pressure")
ValidationResult validate_velocity(float v, str name="velocity")
ValidationResult validate_thrust_equation(float F_momentum, float F_pressure, float F_total, float tolerance=1e-3)
ValidationResult validate_mass_conservation(float mdot_in, float mdot_out, float tolerance=1e-6)
ValidationResult validate_energy_balance(float energy_in, float energy_out, float tolerance=1e-3)
ValidationResult validate_choked_flow(float Pc, float P_exit, float gamma)
List[ValidationResult] validate_engine_state(float Pc, float MR, float mdot_total, float cstar, float gamma, float Tc, float Isp, float F)