STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
engine_wrapper.py
Go to the documentation of this file.
1"""Engine physics wrapper for robust DDP controller.
2
3Wraps the existing PintleEngineRunner to provide estimates from feed pressures
4with caching for efficient DDP rollouts.
5"""
6
7from __future__ import annotations
8
9from typing import Dict, Any, Optional, Tuple
10from dataclasses import dataclass
11import numpy as np
12from functools import lru_cache
13import hashlib
14
15from engine.core.runner import PintleEngineRunner
16from engine.pipeline.config_schemas import PintleEngineConfig
17
18
19@dataclass
21 """Engine performance estimate from feed pressures."""
22 # Chamber pressure
23 P_ch: float # [Pa]
24
25 # Thrust and performance
26 F: float # Thrust [N]
27 mdot_F: float # Fuel mass flow [kg/s]
28 mdot_O: float # Oxidizer mass flow [kg/s]
29 MR: float # Mixture ratio (O/F)
30
31 # Injector pressure drops
32 injector_dp_F: float # P_d_F - P_ch [Pa]
33 injector_dp_O: float # P_d_O - P_ch [Pa]
34
35 # Stability metrics (if available)
36 stability_metrics: Optional[Dict[str, Any]] = None
37
38 # Additional diagnostics
39 diagnostics: Optional[Dict[str, Any]] = None
40
41
43 """Wrapper around PintleEngineRunner with caching for DDP rollouts."""
44
45 def __init__(self, config: PintleEngineConfig, cache_size: int = 128):
46 """
47 Initialize engine wrapper.
48
49 Parameters:
50 -----------
51 config : PintleEngineConfig
52 Engine configuration
53 cache_size : int
54 Maximum number of cached results (LRU cache size)
55 """
56 self.config = config
58 self.cache_size = cache_size
59
60 # Create cached evaluation function
62
64 """Create LRU-cached evaluator function."""
65 @lru_cache(maxsize=self.cache_size)
66 def cached_evaluate(cache_key: str) -> Dict[str, Any]:
67 """Cached evaluation (key is hash of pressures)."""
68 # This will be called by estimate_from_pressures after cache key lookup
69 # The actual evaluation happens there
70 raise RuntimeError("This should not be called directly")
71
72 return cached_evaluate
73
74 def _make_cache_key(self, P_d_F: float, P_d_O: float, precision: int = 3) -> str:
75 """
76 Create cache key from feed pressures.
77
78 Parameters:
79 -----------
80 P_d_F : float
81 Fuel feed pressure [Pa]
82 P_d_O : float
83 Oxidizer feed pressure [Pa]
84 precision : int
85 Decimal places for rounding (reduces cache misses from floating point)
86
87 Returns:
88 --------
89 key : str
90 Cache key string
91 """
92 # Round to reduce cache misses from floating point precision
93 P_d_F_rounded = round(P_d_F, precision)
94 P_d_O_rounded = round(P_d_O, precision)
95
96 # Create hash key
97 key_str = f"{P_d_F_rounded:.{precision}f}_{P_d_O_rounded:.{precision}f}"
98 return hashlib.md5(key_str.encode()).hexdigest()
99
101 self,
102 P_d_F: float,
103 P_d_O: float,
104 use_cache: bool = True,
105 ) -> EngineEstimate:
106 """
107 Estimate engine performance from tank/ullage pressures.
108
109 **IMPORTANT**: Despite the parameter names (P_d_F, P_d_O), these represent
110 TANK/ULLAGE PRESSURES (P_u_F, P_u_O), NOT feed/downstream pressures.
111 The engine physics pipeline expects tank pressures and internally calculates
112 feed line losses.
113
114 Parameters:
115 -----------
116 P_d_F : float
117 Fuel tank/ullage pressure [Pa] (P_u_F in state vector)
118 NOTE: Parameter name is misleading - this is actually tank pressure, not feed pressure
119 P_d_O : float
120 Oxidizer tank/ullage pressure [Pa] (P_u_O in state vector)
121 NOTE: Parameter name is misleading - this is actually tank pressure, not feed pressure
122 use_cache : bool
123 Whether to use caching (default: True)
124
125 Returns:
126 --------
127 estimate : EngineEstimate
128 Engine performance estimate with validated thrust, MR, and mass flows
129
130 Notes:
131 ------
132 The engine physics pipeline (PintleEngineRunner.evaluate) expects tank pressures
133 as input. The controller state uses P_u_F and P_u_O for tank/ullage pressures,
134 which are passed directly to this function. Feed pressures (P_d_F, P_d_O) are
135 downstream and lag behind tank pressures due to line dynamics.
136
137 The estimate is validated to ensure consistency:
138 - If mass flows are invalid, MR is set to NaN
139 - If MR is invalid, thrust and mass flows are set to NaN
140 - If thrust is invalid, MR and mass flows are set to NaN
141 This prevents inconsistent estimates like non-zero thrust with zero MR.
142 """
143 # Check cache first
144 cache_key = self._make_cache_key(P_d_F, P_d_O)
145
146 if use_cache and hasattr(self, '_result_cache'):
147 if cache_key in self._result_cache:
148 return self._result_cache[cache_key]
149 else:
150 # Initialize cache dict if not exists
151 if not hasattr(self, '_result_cache'):
152 self._result_cache = {}
153
154 # Call existing pipeline
155 # NOTE: P_d_F and P_d_O parameters represent tank/ullage pressures (P_u_F, P_u_O)
156 # The engine physics expects tank pressures and internally calculates feed losses
157 try:
158 results = self.runner.evaluate(
159 P_tank_O=P_d_O, # Tank/ullage pressure (parameter name is misleading)
160 P_tank_F=P_d_F, # Tank/ullage pressure (parameter name is misleading)
161 silent=True, # Suppress logging for DDP rollouts
162 )
163 except Exception as e:
164 # If evaluation fails, return a minimal estimate with NaN values
165 # This allows DDP to handle infeasible pressure combinations
166 return EngineEstimate(
167 P_ch=np.nan,
168 F=np.nan,
169 mdot_F=np.nan,
170 mdot_O=np.nan,
171 MR=np.nan,
172 injector_dp_F=np.nan,
173 injector_dp_O=np.nan,
174 stability_metrics=None,
175 diagnostics={"error": str(e)},
176 )
177
178 # Extract chamber pressure
179 P_ch = results.get("Pc", np.nan)
180
181 # Extract mass flows
182 mdot_F = results.get("mdot_F", np.nan)
183 mdot_O = results.get("mdot_O", np.nan)
184
185 # Extract mixture ratio
186 MR = results.get("MR", np.nan)
187
188 # Extract thrust
189 F = results.get("F", np.nan)
190
191 # Validate engine estimate consistency
192 # Rule 1: If either mass flow is zero/NaN, MR should be NaN
193 if (not np.isfinite(mdot_F) or mdot_F <= 0) or (not np.isfinite(mdot_O) or mdot_O <= 0):
194 if np.isfinite(MR) and MR > 0:
195 # Inconsistent: mass flows invalid but MR is valid
196 MR = np.nan
197
198 # Rule 2: If MR is invalid, thrust should also be invalid (unless engine is off)
199 # Actually, allow thrust to be zero when MR is NaN (engine off state)
200 # But if thrust is non-zero and MR is NaN, that's inconsistent
201 if not np.isfinite(MR) or MR <= 0:
202 if np.isfinite(F) and F > 0:
203 # Inconsistent: non-zero thrust but invalid MR
204 F = np.nan
205 # Also invalidate mass flows
206 mdot_F = np.nan
207 mdot_O = np.nan
208
209 # Rule 3: If thrust is invalid, MR and mass flows should also be invalid
210 if not np.isfinite(F) or F <= 0:
211 if np.isfinite(MR) and MR > 0:
212 # Inconsistent: invalid thrust but valid MR
213 MR = np.nan
214 if np.isfinite(mdot_F) and mdot_F > 0:
215 mdot_F = np.nan
216 if np.isfinite(mdot_O) and mdot_O > 0:
217 mdot_O = np.nan
218
219 # Rule 4: If MR is valid, both mass flows should be valid and non-zero
220 if np.isfinite(MR) and MR > 0:
221 if not np.isfinite(mdot_F) or mdot_F <= 0:
222 # Inconsistent: valid MR but invalid fuel flow
223 MR = np.nan
224 F = np.nan
225 if not np.isfinite(mdot_O) or mdot_O <= 0:
226 # Inconsistent: valid MR but invalid oxidizer flow
227 MR = np.nan
228 F = np.nan
229
230 # Compute injector pressure drops
231 injector_dp_F = P_d_F - P_ch if np.isfinite(P_d_F) and np.isfinite(P_ch) else np.nan
232 injector_dp_O = P_d_O - P_ch if np.isfinite(P_d_O) and np.isfinite(P_ch) else np.nan
233
234 # Extract stability metrics if available
235 stability_metrics = None
236 diagnostics = results.get("diagnostics", {})
237 stability_results = results.get("stability", None) or results.get("stability_results", None)
238
239 if stability_results is not None:
240 # Use comprehensive stability results if available
241 stability_metrics = {
242 "stability_state": stability_results.get("stability_state", "unknown"),
243 "stability_score": stability_results.get("stability_score", np.nan),
244 "chugging": stability_results.get("chugging", {}),
245 "acoustic": stability_results.get("acoustic", {}),
246 "feed_system": stability_results.get("feed_system", {}),
247 }
248 else:
249 # Compute minimal injector stiffness constraints
250 # These are based on injector pressure drop requirements
251 injector_dp_frac_F = injector_dp_F / P_d_F if P_d_F > 0 else np.nan
252 injector_dp_frac_O = injector_dp_O / P_d_O if P_d_O > 0 else np.nan
253
254 stability_metrics = {
255 "stability_state": "unknown",
256 "stability_score": np.nan,
257 "injector_dp_frac_F": injector_dp_frac_F,
258 "injector_dp_frac_O": injector_dp_frac_O,
259 "injector_stiffness_ok": (
260 np.isfinite(injector_dp_frac_F) and injector_dp_frac_F > 0.1 and
261 np.isfinite(injector_dp_frac_O) and injector_dp_frac_O > 0.1
262 ),
263 }
264
265 # Create estimate
266 estimate = EngineEstimate(
267 P_ch=P_ch,
268 F=F,
269 mdot_F=mdot_F,
270 mdot_O=mdot_O,
271 MR=MR,
272 injector_dp_F=injector_dp_F,
273 injector_dp_O=injector_dp_O,
274 stability_metrics=stability_metrics,
275 diagnostics=diagnostics,
276 )
277
278 # Cache result
279 if use_cache:
280 # Limit cache size (simple LRU by keeping only recent N entries)
281 if len(self._result_cache) >= self.cache_size:
282 # Remove oldest entry (simple FIFO)
283 oldest_key = next(iter(self._result_cache))
284 del self._result_cache[oldest_key]
285
286 self._result_cache[cache_key] = estimate
287
288 return estimate
289
290 def clear_cache(self) -> None:
291 """Clear the evaluation cache."""
292 if hasattr(self, '_result_cache'):
293 self._result_cache.clear()
294
295 def get_cache_stats(self) -> Dict[str, Any]:
296 """Get cache statistics."""
297 if not hasattr(self, '_result_cache'):
298 return {"size": 0, "max_size": self.cache_size}
299
300 return {
301 "size": len(self._result_cache),
302 "max_size": self.cache_size,
303 "usage": len(self._result_cache) / self.cache_size if self.cache_size > 0 else 0.0,
304 }
305
306
307# Convenience function for direct use
309 P_d_F: float,
310 P_d_O: float,
311 config: PintleEngineConfig,
312 use_cache: bool = True,
313) -> EngineEstimate:
314 """
315 Estimate engine performance from tank/ullage pressures (convenience function).
316
317 **IMPORTANT**: Despite parameter names (P_d_F, P_d_O), these represent
318 TANK/ULLAGE PRESSURES (P_u_F, P_u_O), NOT feed/downstream pressures.
319
320 Parameters:
321 -----------
322 P_d_F : float
323 Fuel tank/ullage pressure [Pa] (P_u_F - parameter name is misleading)
324 P_d_O : float
325 Oxidizer tank/ullage pressure [Pa] (P_u_O - parameter name is misleading)
326 config : PintleEngineConfig
327 Engine configuration
328 use_cache : bool
329 Whether to use caching (default: True)
330
331 Returns:
332 --------
333 estimate : EngineEstimate
334 Engine performance estimate with validated thrust, MR, and mass flows
335
336 Note:
337 -----
338 This function creates a temporary EngineWrapper. For repeated calls,
339 create an EngineWrapper instance and reuse it.
340 """
341 wrapper = EngineWrapper(config, cache_size=128)
342 return wrapper.estimate_from_pressures(P_d_F, P_d_O, use_cache=use_cache)
343
str _make_cache_key(self, float P_d_F, float P_d_O, int precision=3)
__init__(self, PintleEngineConfig config, int cache_size=128)
EngineEstimate estimate_from_pressures(self, float P_d_F, float P_d_O, bool use_cache=True)
EngineEstimate estimate_from_pressures(float P_d_F, float P_d_O, PintleEngineConfig config, bool use_cache=True)