STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
chamber_optimizer.py
Go to the documentation of this file.
1"""Iterative optimization pipeline for chamber geometry design.
2
3This module provides a comprehensive optimization system that:
4- Solves for optimal chamber geometry given design requirements
5- Considers manufacturing and structural constraints
6- Ensures stability margins
7- Sets up ablative cooling for burn time
8- Iterates until convergence with feedforward dynamics
9"""
10
11import numpy as np
12from typing import Dict, Any, Optional, List, Tuple, Callable
13from scipy.optimize import minimize, differential_evolution, Bounds
14from engine.pipeline.config_schemas import PintleEngineConfig, ensure_chamber_geometry, ChamberGeometryConfig
15from engine.core.runner import PintleEngineRunner
16from engine.pipeline.system_diagnostics import SystemDiagnostics
17
18
20 """Iterative optimizer for chamber geometry design."""
21
22 def __init__(self, base_config: PintleEngineConfig):
23 self.base_config = base_config
24 self.runner = PintleEngineRunner(base_config)
25 self.diagnostics = SystemDiagnostics(base_config)
26
28 self,
29 design_requirements: Dict[str, Any],
30 constraints: Dict[str, Any],
31 initial_guess: Optional[Dict[str, float]] = None,
32 ) -> Dict[str, Any]:
33 """
34 Optimize chamber geometry to meet design requirements.
35
36 Parameters:
37 -----------
38 design_requirements : dict
39 - target_thrust: float [N] - Desired thrust
40 - target_burn_time: float [s] - Desired burn time
41 - target_stability_margin: float - Minimum stability margin (e.g., 1.2 = 20% margin)
42 - P_tank_O: float [Pa] - Oxidizer tank pressure
43 - P_tank_F: float [Pa] - Fuel tank pressure
44 - target_Isp: Optional[float] [s] - Desired specific impulse (optional)
45
46 constraints : dict
47 - max_chamber_length: float [m] - Maximum chamber length
48 - max_chamber_diameter: float [m] - Maximum chamber diameter
49 - min_Lstar: float [m] - Minimum L* for combustion
50 - max_Lstar: float [m] - Maximum L* (weight/structure)
51 - min_expansion_ratio: float - Minimum expansion ratio
52 - max_expansion_ratio: float - Maximum expansion ratio
53 - manufacturing_tolerance: float [m] - Manufacturing tolerance for areas
54 - max_wall_thickness: float [m] - Maximum wall thickness (structural)
55 - min_wall_thickness: float [m] - Minimum wall thickness (structural)
56
57 initial_guess : dict, optional
58 - A_throat: float [m²]
59 - A_exit: float [m²]
60 - Lstar: float [m]
61 - chamber_diameter: float [m]
62
63 Returns:
64 --------
65 results : dict
66 - optimized_config: PintleEngineConfig - Optimized configuration
67 - performance: dict - Performance metrics
68 - convergence_history: list - Optimization history
69 - diagnostics: dict - System diagnostics
70 """
71 # Extract requirements
72 target_thrust = design_requirements["target_thrust"]
73 target_burn_time = design_requirements.get("target_burn_time", 10.0)
74 target_stability_margin = design_requirements.get("target_stability_margin", 1.2)
75 P_tank_O = design_requirements["P_tank_O"]
76 P_tank_F = design_requirements["P_tank_F"]
77 target_Isp = design_requirements.get("target_Isp", None)
78
79 # Set up initial guess
80 if initial_guess is None:
81 initial_guess = self._generate_initial_guess(target_thrust, constraints)
82
83 # Optimization variables: [A_throat, A_exit, Lstar, chamber_diameter]
84 x0 = np.array([
85 initial_guess["A_throat"],
86 initial_guess["A_exit"],
87 initial_guess["Lstar"],
88 initial_guess.get("chamber_diameter", 0.1),
89 ])
90
91 # Set up bounds
92 bounds = self._setup_bounds(constraints, initial_guess)
93
94 # Optimization history
95 history = []
96
97 # Objective function
98 def objective(x: np.ndarray) -> float:
99 """Minimize error in meeting design requirements."""
100 try:
101 # Update config with current geometry
102 config = self._update_config_from_x(x, self.base_config)
103
104 # Evaluate engine
105 runner = PintleEngineRunner(config)
106 results = runner.evaluate(P_tank_O, P_tank_F)
107
108 # Calculate errors (safe division)
109 F_actual = results.get("F", 0.0)
110 thrust_error = abs(F_actual - target_thrust) / target_thrust if target_thrust > 0 else 1.0
111
112 Isp_actual = results.get("Isp", 0.0)
113 isp_error = 0.0
114 if target_Isp is not None and target_Isp > 0:
115 isp_error = abs(Isp_actual - target_Isp) / target_Isp
116
117 # Stability margin error (safe division)
118 stability = results.get("stability_results", {})
119 chugging = stability.get("chugging", {})
120 stability_margin = chugging.get("stability_margin", 0.0)
121 if target_stability_margin > 0:
122 stability_error = max(0.0, target_stability_margin - stability_margin) / target_stability_margin
123 else:
124 stability_error = 0.0 if stability_margin >= 0.5 else 1.0
125
126 # Combined objective (weighted)
127 objective_value = (
128 10.0 * thrust_error + # Thrust is most important
129 5.0 * isp_error +
130 3.0 * stability_error
131 )
132
133 # Store history
134 history.append({
135 "x": x.copy(),
136 "F": F_actual,
137 "Isp": Isp_actual,
138 "thrust_error": thrust_error,
139 "isp_error": isp_error,
140 "stability_error": stability_error,
141 "objective": objective_value,
142 })
143
144 return objective_value
145
146 except Exception as e:
147 # Return large penalty for invalid configurations
148 return 1e6
149
150 # Constraint functions
151 constraints_list = self._setup_constraints(constraints, design_requirements, P_tank_O, P_tank_F)
152
153 # Run optimization
154 result = minimize(
155 objective,
156 x0,
157 method="SLSQP", # Sequential Least Squares Programming
158 bounds=bounds,
159 constraints=constraints_list,
160 options={"maxiter": 100, "ftol": 1e-6},
161 )
162
163 # Extract optimized configuration
164 optimized_config = self._update_config_from_x(result.x, self.base_config)
165 optimized_runner = PintleEngineRunner(optimized_config)
166 final_results = optimized_runner.evaluate(P_tank_O, P_tank_F)
167
168 # Run diagnostics
169 diagnostics = self.diagnostics.diagnose_all(P_tank_O, P_tank_F)
170
171 # Calculate burn analysis with time-varying dynamics
172 burn_analysis = self._calculate_burn_analysis(
173 optimized_config, optimized_runner, target_burn_time, P_tank_O, P_tank_F
174 )
175
176 # Extract optimized parameters for display
177 cg = ensure_chamber_geometry(optimized_config)
178 optimized_params = {
179 "A_throat": cg.A_throat,
180 "A_exit": cg.A_exit,
181 "Lstar": cg.Lstar,
182 "chamber_diameter": cg.chamber_diameter,
183 "chamber_length": cg.length,
184 "expansion_ratio": cg.expansion_ratio,
185 }
186
187 return {
188 "optimized_config": optimized_config,
189 "optimization_result": result,
190 "performance": final_results,
191 "convergence_history": history,
192 "diagnostics": diagnostics,
193 "burn_analysis": burn_analysis,
194 "design_requirements": design_requirements,
195 "constraints": constraints,
196 "optimized_parameters": optimized_params, # NEW: For UI display
197 }
198
199 def _generate_initial_guess(self, target_thrust: float, constraints: Dict) -> Dict[str, float]:
200 """Generate initial guess for optimization."""
201 # Rough estimates based on target thrust
202 # Typical: F ≈ 0.7 * Pc * A_throat * Cf (for sea level)
203 # Assume Pc ≈ 2-3 MPa, Cf ≈ 1.5
204 Pc_estimate = 2.5e6 # Pa
205 Cf_estimate = 1.5
206
207 A_throat_estimate = target_thrust / (0.7 * Pc_estimate * Cf_estimate)
208
209 # Expansion ratio estimate (typical: 5-20)
210 eps_estimate = 10.0
211 eps_estimate = np.clip(eps_estimate, constraints.get("min_expansion_ratio", 3.0),
212 constraints.get("max_expansion_ratio", 30.0))
213
214 A_exit_estimate = A_throat_estimate * eps_estimate
215
216 # L* estimate (typical: 0.8-1.5 m for pintle, but small engines can be 1.0-1.5 m)
217 # For small-scale engines (20mm throat), L* is typically 1.0-1.5 m (40-60 inches)
218 Lstar_estimate = 1.27 # 50 inches - good default for small engines
219 Lstar_estimate = np.clip(Lstar_estimate, constraints.get("min_Lstar", 0.5),
220 constraints.get("max_Lstar", 2.5))
221
222 # Chamber diameter estimate (from volume and length)
223 # V = L* * A_throat, and V = π * (D/2)² * L_chamber
224 # Rough estimate: D ≈ 3 * sqrt(A_throat)
225 chamber_diameter_estimate = 3.0 * np.sqrt(A_throat_estimate)
226 chamber_diameter_estimate = np.clip(chamber_diameter_estimate, 0.05,
227 constraints.get("max_chamber_diameter", 0.3))
228
229 return {
230 "A_throat": A_throat_estimate,
231 "A_exit": A_exit_estimate,
232 "Lstar": Lstar_estimate,
233 "chamber_diameter": chamber_diameter_estimate,
234 }
235
236 def _setup_bounds(self, constraints: Dict, initial_guess: Dict) -> Bounds:
237 """Set up optimization bounds."""
238 # Bounds: [A_throat, A_exit, Lstar, chamber_diameter]
239 # For small-scale engines: 20mm throat = 0.000314 m², so allow down to 5mm = 0.00002 m²
240 A_throat_min = 1e-6 # m² (1 mm²) - allows very small engines
241 A_throat_max = 0.01 # m² (100 cm²)
242
243 min_eps = constraints.get("min_expansion_ratio", 3.0)
244 max_eps = constraints.get("max_expansion_ratio", 30.0)
245
246 A_exit_min = A_throat_min * min_eps
247 A_exit_max = A_throat_max * max_eps
248
249 Lstar_min = constraints.get("min_Lstar", 0.5)
250 Lstar_max = constraints.get("max_Lstar", 2.5)
251
252 chamber_diameter_min = 0.05 # m
253 chamber_diameter_max = constraints.get("max_chamber_diameter", 0.3)
254
255 return Bounds(
256 [A_throat_min, A_exit_min, Lstar_min, chamber_diameter_min],
257 [A_throat_max, A_exit_max, Lstar_max, chamber_diameter_max],
258 )
259
261 self, constraints: Dict, design_requirements: Dict, P_tank_O: float, P_tank_F: float
262 ) -> List[Dict]:
263 """Set up optimization constraints including airframe and system constraints."""
264 constraint_list = []
265
266 # Constraint: Expansion ratio within bounds
267 def expansion_ratio_constraint(x):
268 A_throat, A_exit = x[0], x[1]
269 eps = A_exit / A_throat if A_throat > 0 else 1.0
270 min_eps = constraints.get("min_expansion_ratio", 3.0)
271 max_eps = constraints.get("max_expansion_ratio", 30.0)
272 return [eps - min_eps, max_eps - eps]
273
274 constraint_list.append({
275 "type": "ineq",
276 "fun": lambda x: expansion_ratio_constraint(x)[0], # eps >= min_eps
277 })
278 constraint_list.append({
279 "type": "ineq",
280 "fun": lambda x: expansion_ratio_constraint(x)[1], # eps <= max_eps
281 })
282
283 # Constraint: Chamber length within bounds
284 def chamber_length_constraint(x):
285 Lstar, chamber_diameter = x[2], x[3]
286 # Estimate chamber length from L* and geometry
287 A_throat = x[0]
288 V_chamber = Lstar * A_throat
289 A_chamber = np.pi * (chamber_diameter / 2) ** 2
290 L_chamber = V_chamber / A_chamber if A_chamber > 0 else 0.0
291 max_length = constraints.get("max_chamber_length", 1.0)
292 # Also check airframe max_length if provided
293 airframe_max_length = constraints.get("max_vehicle_length", None)
294 if airframe_max_length is not None:
295 max_length = min(max_length, airframe_max_length)
296 return max_length - L_chamber
297
298 constraint_list.append({
299 "type": "ineq",
300 "fun": chamber_length_constraint,
301 })
302
303 # Constraint: Chamber diameter within airframe limits
304 def chamber_diameter_constraint(x):
305 chamber_diameter = x[3]
306 max_diameter = constraints.get("max_chamber_diameter", 0.3)
307 # Also check airframe max_diameter if provided
308 airframe_max_diameter = constraints.get("max_vehicle_diameter", None)
309 if airframe_max_diameter is not None:
310 max_diameter = min(max_diameter, airframe_max_diameter)
311 return max_diameter - chamber_diameter
312
313 constraint_list.append({
314 "type": "ineq",
315 "fun": chamber_diameter_constraint,
316 })
317
318 # Constraint: Total engine weight (if weight limits provided)
319 def weight_constraint(x):
320 try:
321 config = self._update_config_from_x(x, self.base_config)
322 runner = PintleEngineRunner(config)
323 results = runner.evaluate(P_tank_O, P_tank_F)
324
325 # Estimate engine weight (chamber + nozzle + injector)
326 # Rough estimates: chamber wall, ablative, graphite, nozzle
327 A_throat, A_exit, Lstar, chamber_diameter = x[0], x[1], x[2], x[3]
328
329 # Chamber wall weight (stainless steel, ~3mm thick)
330 L_chamber = (Lstar * A_throat) / (np.pi * (chamber_diameter / 2) ** 2) if chamber_diameter > 0 else 0.25
331 chamber_wall_area = np.pi * chamber_diameter * L_chamber
332 chamber_wall_volume = chamber_wall_area * 0.003 # 3mm thickness
333 chamber_wall_mass = chamber_wall_volume * 8000.0 # Stainless density [kg/m³]
334
335 # Ablative weight (if enabled)
336 ablative_mass = 0.0
337 if config.ablative_cooling and config.ablative_cooling.enabled:
338 ablative_thickness = config.ablative_cooling.initial_thickness
339 ablative_area = chamber_wall_area * config.ablative_cooling.coverage_fraction
340 ablative_volume = ablative_area * ablative_thickness
341 ablative_mass = ablative_volume * config.ablative_cooling.material_density
342
343 # Graphite insert weight (if enabled)
344 graphite_mass = 0.0
345 if hasattr(config, 'graphite_insert') and config.graphite_insert and config.graphite_insert.enabled:
346 graphite_thickness = config.graphite_insert.initial_thickness
347 graphite_area = np.pi * (np.sqrt(4.0 * A_throat / np.pi) / 2.0) ** 2 # Throat area
348 graphite_volume = graphite_area * graphite_thickness
349 graphite_mass = graphite_volume * config.graphite_insert.material_density
350
351 # Nozzle weight (simplified - conical nozzle)
352 nozzle_length = constraints.get("nozzle_length", 0.1) # Default 10cm
353 nozzle_wall_thickness = 0.002 # 2mm
354 D_exit = np.sqrt(4.0 * A_exit / np.pi)
355 D_throat = np.sqrt(4.0 * A_throat / np.pi)
356 nozzle_avg_diameter = (D_exit + D_throat) / 2.0
357 nozzle_wall_area = np.pi * nozzle_avg_diameter * nozzle_length
358 nozzle_wall_volume = nozzle_wall_area * nozzle_wall_thickness
359 nozzle_wall_mass = nozzle_wall_volume * 8000.0 # Stainless
360
361 # Total engine mass
362 total_mass = chamber_wall_mass + ablative_mass + graphite_mass + nozzle_wall_mass
363
364 # Check against weight limit
365 max_weight = constraints.get("max_engine_weight", None)
366 if max_weight is not None:
367 return max_weight - total_mass
368 else:
369 return 1.0 # No constraint
370
371 except Exception:
372 return -1e6 # Penalty for invalid configuration
373
374 max_weight = constraints.get("max_engine_weight", None)
375 if max_weight is not None:
376 constraint_list.append({
377 "type": "ineq",
378 "fun": weight_constraint,
379 })
380
381 # Constraint: Stability margin (must meet minimum for all three types)
382 def stability_constraint(x):
383 try:
384 config = self._update_config_from_x(x, self.base_config)
385 runner = PintleEngineRunner(config)
386 results = runner.evaluate(P_tank_O, P_tank_F)
387
388 stability = results.get("stability_results", {})
389
390 # Chugging stability
391 chugging = stability.get("chugging", {})
392 chugging_margin = chugging.get("stability_margin", 0.0)
393
394 # Acoustic stability
395 acoustic = stability.get("acoustic", {})
396 acoustic_margin = acoustic.get("stability_margin", 0.0)
397
398 # Feed system stability
399 feed_system = stability.get("feed_system", {})
400 feed_margin = feed_system.get("stability_margin", 0.0)
401
402 # Get minimum requirements (with fallback to general target_stability_margin)
403 min_stability = design_requirements.get("target_stability_margin", 1.2)
404 chugging_min = design_requirements.get("chugging_margin_min", min_stability)
405 acoustic_min = design_requirements.get("acoustic_margin_min", min_stability)
406 feed_min = design_requirements.get("feed_stability_min", min_stability)
407
408 # Return minimum margin (all must pass, so use the worst case)
409 min_margin = min(
410 chugging_margin - chugging_min,
411 acoustic_margin - acoustic_min,
412 feed_margin - feed_min
413 )
414
415 return min_margin
416
417 except Exception:
418 return -1e6 # Penalty for invalid configuration
419
420 constraint_list.append({
421 "type": "ineq",
422 "fun": stability_constraint,
423 })
424
425 # Constraint: Manufacturing tolerances (throat and exit areas)
426 def manufacturing_constraint(x):
427 A_throat, A_exit = x[0], x[1]
428 tolerance = constraints.get("manufacturing_tolerance", 0.0001) # 0.1mm default
429
430 # Minimum area based on manufacturing capability
431 min_area = np.pi * (tolerance / 2.0) ** 2
432 return min(A_throat - min_area, A_exit - min_area)
433
434 constraint_list.append({
435 "type": "ineq",
436 "fun": manufacturing_constraint,
437 })
438
439 return constraint_list
440
441 def _update_config_from_x(self, x: np.ndarray, base_config: PintleEngineConfig) -> PintleEngineConfig:
442 """Update configuration from optimization variables."""
443 import copy
444 config = copy.deepcopy(base_config)
445
446 A_throat, A_exit, Lstar, chamber_diameter = x[0], x[1], x[2], x[3]
447
448 # Ensure chamber_geometry exists
449 if config.chamber_geometry is None:
450 cg = ensure_chamber_geometry(config)
451 else:
452 cg = config.chamber_geometry
453
454 # Calculate chamber length from volume and diameter
455 volume = Lstar * A_throat # V = L* * A_throat
456 A_chamber = np.pi * (chamber_diameter / 2.0) ** 2
457 if A_chamber > 0:
458 length = volume / A_chamber
459 else:
460 length = 0.25 # Default fallback
461
462 # Calculate exit diameter
463 D_exit = np.sqrt(4.0 * A_exit / np.pi) if A_exit > 0 else 0.0
464 expansion_ratio = A_exit / A_throat if A_throat > 0 else 1.0
465
466 # Update chamber_geometry
467 cg.A_throat = A_throat
468 cg.A_exit = A_exit
469 cg.volume = volume
470 cg.length = length
471 cg.Lstar = Lstar
472 cg.chamber_diameter = chamber_diameter
473 cg.exit_diameter = D_exit
474 cg.expansion_ratio = expansion_ratio
475
476 # Also update legacy sections if they exist (for backward compatibility)
477 if config.chamber is not None:
478 config.chamber.A_throat = A_throat
479 config.chamber.volume = volume
480 config.chamber.Lstar = Lstar
481 config.chamber.chamber_inner_diameter = chamber_diameter
482 config.chamber.length = length
483 if config.nozzle is not None:
484 config.nozzle.A_throat = A_throat
485 config.nozzle.A_exit = A_exit
486 config.nozzle.expansion_ratio = expansion_ratio
487 config.nozzle.exit_diameter = D_exit
488
489 # Update CEA cache expansion ratio
490 if hasattr(config.combustion, 'cea'):
491 config.combustion.cea.expansion_ratio = expansion_ratio
492
493 # Update regen cooling chamber diameter if enabled
494 if config.regen_cooling is not None and config.regen_cooling.enabled:
495 config.regen_cooling.chamber_inner_diameter = chamber_diameter
496
497 return config
498
500 self, config: PintleEngineConfig, runner: PintleEngineRunner,
501 burn_time: float, P_tank_O: float, P_tank_F: float
502 ) -> Dict[str, Any]:
503 """Calculate burn analysis for the optimized configuration with full dynamics."""
504 from engine.pipeline.burn_analysis import analyze_burn_degradation
505
506 # Run time-series evaluation with coupled solver (includes chamber/nozzle dynamics and ablative)
507 time_array = np.linspace(0.0, burn_time, 100)
508
509 # Use constant tank pressures for now (could be made time-varying)
510 P_tank_O_array = np.full_like(time_array, P_tank_O)
511 P_tank_F_array = np.full_like(time_array, P_tank_F)
512
513 # Use fully-coupled solver with ablative geometry tracking
514 results_array = runner.evaluate_arrays_with_time(
515 time_array,
516 P_tank_O_array,
517 P_tank_F_array,
518 track_ablative_geometry=True, # Enable ablative geometry tracking
519 use_coupled_solver=True, # Use fully-coupled solver (chamber + nozzle dynamics)
520 )
521
522 # Analyze degradation
523 burn_results = analyze_burn_degradation(results_array, time_array)
524
525 return burn_results
526
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
Dict[str, Any] _calculate_burn_analysis(self, PintleEngineConfig config, PintleEngineRunner runner, float burn_time, float P_tank_O, float P_tank_F)
Dict[str, Any] optimize(self, Dict[str, Any] design_requirements, Dict[str, Any] constraints, Optional[Dict[str, float]] initial_guess=None)
Bounds _setup_bounds(self, Dict constraints, Dict initial_guess)
__init__(self, PintleEngineConfig base_config)
PintleEngineConfig _update_config_from_x(self, np.ndarray x, PintleEngineConfig base_config)
List[Dict] _setup_constraints(self, Dict constraints, Dict design_requirements, float P_tank_O, float P_tank_F)
Dict[str, float] _generate_initial_guess(self, float target_thrust, Dict constraints)