STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
coupled_optimizer.py
Go to the documentation of this file.
1"""Coupled optimizer for pintle injector and chamber geometry.
2
3This module solves the two-way coupling problem:
4- Pintle geometry affects chamber performance (spray quality, mixing, efficiency)
5- Chamber geometry affects pintle performance (pressure, flow rates, stability)
6
7The optimizer iterates between pintle and chamber optimization until convergence.
8"""
9
10from __future__ import annotations
11
12from typing import Dict, Any, Optional, List, Tuple
13import numpy as np
14from scipy.optimize import minimize, Bounds
15import copy
16
17from engine.pipeline.config_schemas import PintleEngineConfig
18from engine.core.runner import PintleEngineRunner
19from engine.pipeline.chamber_optimizer import ChamberOptimizer
20from engine.pipeline.comprehensive_optimizer import ComprehensivePintleOptimizer
21
22
24 """
25 Coupled optimizer that iteratively solves pintle and chamber optimization.
26
27 The optimization problem is:
28 - Pintle geometry → affects chamber pressure, efficiency, stability
29 - Chamber geometry → affects pintle flow rates, pressure drops, spray
30
31 Solution approach:
32 1. Initialize with initial guesses
33 2. Optimize pintle (with current chamber geometry)
34 3. Optimize chamber (with current pintle geometry)
35 4. Check convergence (geometry changes < tolerance)
36 5. Repeat until converged
37 """
38
39 def __init__(self, base_config: PintleEngineConfig):
40 self.base_config = base_config
44
46 self,
47 design_requirements: Dict[str, Any],
48 constraints: Dict[str, Any],
49 max_iterations: int = 10,
50 convergence_tolerance: float = 0.01,
51 use_time_varying: bool = True, # NEW: Optimize across entire burn time
52 ) -> Dict[str, Any]:
53 """
54 Optimize pintle and chamber geometry with full coupling.
55
56 Parameters:
57 -----------
58 design_requirements : dict
59 - target_thrust: float [N]
60 - target_burn_time: float [s]
61 - target_stability_margin: float
62 - P_tank_O: float [Pa]
63 - P_tank_F: float [Pa]
64 - target_Isp: Optional[float] [s]
65
66 constraints : dict
67 - max_chamber_length: float [m]
68 - max_chamber_diameter: float [m]
69 - min_Lstar: float [m]
70 - max_Lstar: float [m]
71 - min_expansion_ratio: float
72 - max_expansion_ratio: float
73 - max_engine_weight: Optional[float] [kg]
74 - max_vehicle_length: Optional[float] [m]
75 - max_vehicle_diameter: Optional[float] [m]
76
77 max_iterations : int
78 Maximum number of coupling iterations
79
80 convergence_tolerance : float
81 Relative change tolerance for convergence (e.g., 0.01 = 1%)
82
83 use_time_varying : bool
84 If True, optimize chamber across entire burn time (accounts for ablative recession)
85 If False, optimize at t=0 only
86
87 Returns:
88 --------
89 results : dict
90 - optimized_config: PintleEngineConfig
91 - iteration_history: List of iteration results
92 - convergence_info: Dict with convergence status
93 - performance: Final performance metrics
94 """
95 current_config = copy.deepcopy(self.base_config)
96 iteration_history = []
97
98 P_tank_O = design_requirements["P_tank_O"]
99 P_tank_F = design_requirements["P_tank_F"]
100 target_thrust = design_requirements["target_thrust"]
101 target_burn_time = design_requirements.get("target_burn_time", 10.0)
102
103 # Track previous geometry for convergence
104 prev_pintle_params = None
105 prev_chamber_params = None
106
107 # Track time-varying results from the last successful optimization
108 last_time_varying_results = None
109 last_time_varying_summary = None
110
111 for iteration in range(max_iterations):
112 iteration_result = {
113 "iteration": iteration,
114 "pintle_config": None,
115 "chamber_config": None,
116 "performance": None,
117 "convergence": None,
118 }
119
120 # Step 1: Optimize pintle geometry (with current chamber)
121 try:
122 pintle_results = self.pintle_optimizer.optimize_pintle_geometry(
123 target_thrust=target_thrust,
124 target_isp=design_requirements.get("target_Isp", None),
125 target_mr=current_config.combustion.MR if hasattr(current_config.combustion, 'MR') else 2.5,
126 P_tank_O=P_tank_O,
127 P_tank_F=P_tank_F,
128 constraints=constraints,
129 )
130
131 # Update config with optimized pintle
132 current_config = pintle_results["optimized_config"]
133 iteration_result["pintle_config"] = self._extract_pintle_params(current_config)
134 iteration_result["pintle_performance"] = pintle_results["performance"]
135
136 except Exception as e:
137 import warnings
138 warnings.warn(f"Pintle optimization failed at iteration {iteration}: {e}")
139 # Continue with current config
140 iteration_result["pintle_config"] = self._extract_pintle_params(current_config)
141
142 # Step 2: Optimize chamber geometry (with current pintle)
143 # CRITICAL: Use time-varying optimization if enabled
144 try:
145 chamber_design_reqs = {
146 "target_thrust": target_thrust,
147 "target_burn_time": target_burn_time,
148 "target_stability_margin": design_requirements.get("target_stability_margin", 1.2),
149 "P_tank_O": P_tank_O,
150 "P_tank_F": P_tank_F,
151 "target_Isp": design_requirements.get("target_Isp", None),
152 }
153
154 # Update chamber optimizer with current config
155 self.chamber_optimizer.base_config = current_config
156 self.chamber_optimizer.runner = PintleEngineRunner(current_config)
157
158 # Run chamber optimization (with time-varying if enabled)
159 if use_time_varying:
160 chamber_results = self._optimize_chamber_time_varying(
161 current_config,
162 chamber_design_reqs,
163 constraints,
164 target_burn_time,
165 P_tank_O,
166 P_tank_F,
167 )
168 else:
169 chamber_results = self.chamber_optimizer.optimize(
170 chamber_design_reqs,
171 constraints,
172 )
173
174 # Update config with optimized chamber
175 current_config = chamber_results["optimized_config"]
176 iteration_result["chamber_config"] = self._extract_chamber_params(current_config)
177 iteration_result["chamber_performance"] = chamber_results["performance"]
178
179 # Preserve time-varying results if available (from _optimize_chamber_time_varying)
180 if "time_varying_results" in chamber_results:
181 last_time_varying_results = chamber_results["time_varying_results"]
182 if "performance" in chamber_results and "time_varying" in chamber_results["performance"]:
183 last_time_varying_summary = chamber_results["performance"]["time_varying"]
184
185 except Exception as e:
186 import warnings
187 warnings.warn(f"Chamber optimization failed at iteration {iteration}: {e}")
188 iteration_result["chamber_config"] = self._extract_chamber_params(current_config)
189
190 # Step 3: Evaluate final performance
191 # Use chamber performance which includes time_varying data, augment with single-point eval
192 try:
193 runner = PintleEngineRunner(current_config)
194 final_performance = runner.evaluate(P_tank_O, P_tank_F)
195
196 # Merge with chamber performance to preserve time_varying data
197 if iteration_result.get("chamber_performance"):
198 merged_performance = dict(iteration_result["chamber_performance"])
199 # Update with single-point values but keep time_varying from chamber optimization
200 for key, value in final_performance.items():
201 if key != "time_varying": # Don't overwrite time_varying
202 merged_performance[key] = value
203 iteration_result["performance"] = merged_performance
204 else:
205 iteration_result["performance"] = final_performance
206 except Exception as e:
207 import warnings
208 warnings.warn(f"Performance evaluation failed at iteration {iteration}: {e}")
209
210 # Step 4: Check convergence
211 current_pintle = self._extract_pintle_params(current_config)
212 current_chamber = self._extract_chamber_params(current_config)
213
214 if prev_pintle_params is not None and prev_chamber_params is not None:
215 # Calculate relative changes
216 pintle_change = self._calculate_relative_change(prev_pintle_params, current_pintle)
217 chamber_change = self._calculate_relative_change(prev_chamber_params, current_chamber)
218 max_change = max(pintle_change, chamber_change)
219
220 iteration_result["convergence"] = {
221 "pintle_change": pintle_change,
222 "chamber_change": chamber_change,
223 "max_change": max_change,
224 "converged": max_change < convergence_tolerance,
225 }
226
227 if max_change < convergence_tolerance:
228 # Converged!
229 iteration_result["converged"] = True
230 iteration_history.append(iteration_result)
231 break
232 else:
233 iteration_result["convergence"] = {
234 "pintle_change": np.inf,
235 "chamber_change": np.inf,
236 "max_change": np.inf,
237 "converged": False,
238 }
239
240 prev_pintle_params = current_pintle
241 prev_chamber_params = current_chamber
242 iteration_history.append(iteration_result)
243
244 # Final evaluation
245 runner = PintleEngineRunner(current_config)
246 final_performance = runner.evaluate(P_tank_O, P_tank_F)
247
248 # Include time-varying summary in final performance if available
249 if last_time_varying_summary is not None:
250 final_performance["time_varying"] = last_time_varying_summary
251
252 result = {
253 "optimized_config": current_config,
254 "iteration_history": iteration_history,
255 "convergence_info": {
256 "converged": iteration_history[-1]["convergence"]["converged"] if iteration_history else False,
257 "iterations": len(iteration_history),
258 "final_change": iteration_history[-1]["convergence"]["max_change"] if iteration_history else np.inf,
259 },
260 "performance": final_performance,
261 "design_requirements": design_requirements,
262 "constraints": constraints,
263 }
264
265 # Include time-varying results (array data) if available for plotting
266 if last_time_varying_results is not None:
267 result["time_varying_results"] = last_time_varying_results
268
269 return result
270
272 self,
273 config: PintleEngineConfig,
274 design_requirements: Dict[str, Any],
275 constraints: Dict[str, Any],
276 burn_time: float,
277 P_tank_O: float,
278 P_tank_F: float,
279 ) -> Dict[str, Any]:
280 """
281 Optimize chamber geometry across entire burn time.
282
283 This accounts for:
284 - Ablative recession (chamber volume grows, throat area grows)
285 - Graphite recession (throat area may grow if graphite erodes)
286 - Time-varying Cf (thrust coefficient changes with geometry)
287 - Time-varying efficiency (L* changes affect combustion efficiency)
288
289 Objective: Minimize error in average thrust over burn time
290 """
291 target_thrust = design_requirements["target_thrust"]
292 target_stability_margin = design_requirements.get("target_stability_margin", 1.2)
293
294 # Optimization variables: [A_throat, A_exit, Lstar, chamber_diameter]
295 # Generate initial guess
296 initial_guess = self.chamber_optimizer._generate_initial_guess(target_thrust, constraints)
297 x0 = np.array([
298 initial_guess["A_throat"],
299 initial_guess["A_exit"],
300 initial_guess["Lstar"],
301 initial_guess.get("chamber_diameter", 0.1),
302 ])
303
304 # Set up bounds
305 bounds = self.chamber_optimizer._setup_bounds(constraints, initial_guess)
306
307 # Optimization history
308 history = []
309
310 def objective_time_varying(x: np.ndarray) -> float:
311 """Minimize error in average thrust over entire burn time."""
312 try:
313 # Update config with current geometry
314 config_current = self.chamber_optimizer._update_config_from_x(x, config)
315
316 # Run time-varying analysis
317 runner = PintleEngineRunner(config_current)
318
319 # Time array for burn analysis
320 n_time_points = 50 # Enough resolution
321 time_array = np.linspace(0.0, burn_time, n_time_points)
322 P_tank_O_array = np.full_like(time_array, P_tank_O)
323 P_tank_F_array = np.full_like(time_array, P_tank_F)
324
325 # Evaluate with time-varying solver (includes ablative recession)
326 results_array = runner.evaluate_arrays_with_time(
327 time_array,
328 P_tank_O_array,
329 P_tank_F_array,
330 track_ablative_geometry=True, # Enable ablative tracking
331 use_coupled_solver=True, # Use fully-coupled solver
332 )
333
334 # Calculate average thrust over burn time
335 thrust_array = results_array["F"]
336 avg_thrust = np.mean(thrust_array)
337
338 # Calculate thrust error (minimize deviation from target)
339 thrust_error = abs(avg_thrust - target_thrust) / target_thrust
340
341 # Calculate stability margin (worst case over burn time)
342 stability_margins = results_array.get("chugging_stability_margin", np.array([1.0]))
343 min_stability = np.min(stability_margins)
344 stability_error = max(0.0, target_stability_margin - min_stability) / target_stability_margin
345
346 # Penalty for large thrust variation (want steady performance)
347 thrust_std = np.std(thrust_array)
348 thrust_variation_penalty = (thrust_std / avg_thrust) if avg_thrust > 0 else 1.0
349
350 # Combined objective
351 objective_value = (
352 10.0 * thrust_error + # Thrust is most important
353 5.0 * stability_error + # Stability must be maintained
354 2.0 * thrust_variation_penalty # Penalize large variations
355 )
356
357 # Store history
358 history.append({
359 "x": x.copy(),
360 "avg_thrust": avg_thrust,
361 "thrust_error": thrust_error,
362 "stability_error": stability_error,
363 "thrust_variation": thrust_std,
364 "objective": objective_value,
365 })
366
367 return objective_value
368
369 except Exception as e:
370 import warnings
371 warnings.warn(f"Time-varying objective evaluation failed: {e}")
372 return 1e6
373
374 # Set up constraints (same as regular optimizer)
375 constraints_list = self.chamber_optimizer._setup_constraints(
376 constraints, design_requirements, P_tank_O, P_tank_F
377 )
378
379 # Run optimization
380 result = minimize(
381 objective_time_varying,
382 x0,
383 method="SLSQP",
384 bounds=bounds,
385 constraints=constraints_list,
386 options={"maxiter": 100, "ftol": 1e-6},
387 )
388
389 # Extract optimized configuration
390 optimized_config = self.chamber_optimizer._update_config_from_x(result.x, config)
391 optimized_runner = PintleEngineRunner(optimized_config)
392
393 # Final time-varying evaluation
394 time_array = np.linspace(0.0, burn_time, 100)
395 P_tank_O_array = np.full_like(time_array, P_tank_O)
396 P_tank_F_array = np.full_like(time_array, P_tank_F)
397
398 final_results_array = optimized_runner.evaluate_arrays_with_time(
399 time_array,
400 P_tank_O_array,
401 P_tank_F_array,
402 track_ablative_geometry=True,
403 use_coupled_solver=True,
404 )
405
406 # Calculate final performance metrics
407 final_results = optimized_runner.evaluate(P_tank_O, P_tank_F)
408
409 # Add time-varying metrics
410 final_results["time_varying"] = {
411 "avg_thrust": float(np.mean(final_results_array["F"])),
412 "min_thrust": float(np.min(final_results_array["F"])),
413 "max_thrust": float(np.max(final_results_array["F"])),
414 "thrust_std": float(np.std(final_results_array["F"])),
415 "avg_isp": float(np.mean(final_results_array["Isp"])),
416 "min_stability_margin": float(np.min(final_results_array.get("chugging_stability_margin", [1.0]))),
417 "max_recession_chamber": float(np.max(final_results_array.get("recession_chamber", [0.0]))),
418 "max_recession_throat": float(np.max(final_results_array.get("recession_throat", [0.0]))),
419 }
420
421 return {
422 "optimized_config": optimized_config,
423 "optimization_result": result,
424 "performance": final_results,
425 "convergence_history": history,
426 "time_varying_results": final_results_array,
427 }
428
429 def _extract_pintle_params(self, config: PintleEngineConfig) -> Dict[str, float]:
430 """Extract pintle geometry parameters for convergence checking."""
431 params = {}
432 if hasattr(config, 'injector') and config.injector.type == "pintle":
433 geometry = config.injector.geometry
434 if hasattr(geometry, 'fuel'):
435 params["d_pintle_tip"] = geometry.fuel.d_pintle_tip
436 params["h_gap"] = geometry.fuel.h_gap
437 if hasattr(geometry, 'lox'):
438 params["n_orifices"] = float(geometry.lox.n_orifices)
439 params["d_orifice"] = geometry.lox.d_orifice
440 params["theta_orifice"] = geometry.lox.theta_orifice
441 return params
442
443 def _extract_chamber_params(self, config: PintleEngineConfig) -> Dict[str, float]:
444 """Extract chamber geometry parameters for convergence checking."""
445 from engine.pipeline.config_schemas import ensure_chamber_geometry
446 cg = ensure_chamber_geometry(config)
447 return {
448 "A_throat": cg.A_throat,
449 "A_exit": cg.A_exit,
450 "Lstar": cg.Lstar,
451 "chamber_diameter": cg.chamber_diameter,
452 }
453
455 self, params_prev: Dict[str, float], params_current: Dict[str, float]
456 ) -> float:
457 """Calculate maximum relative change between parameter sets."""
458 max_change = 0.0
459 for key in set(params_prev.keys()) | set(params_current.keys()):
460 if key in params_prev and key in params_current:
461 val_prev = params_prev[key]
462 val_current = params_current[key]
463 if abs(val_prev) > 1e-10:
464 rel_change = abs(val_current - val_prev) / abs(val_prev)
465 max_change = max(max_change, rel_change)
466 return max_change
467
#define max(a, b)
Definition Arduino.h:43
Dict[str, float] _extract_chamber_params(self, PintleEngineConfig config)
Dict[str, Any] _optimize_chamber_time_varying(self, PintleEngineConfig config, Dict[str, Any] design_requirements, Dict[str, Any] constraints, float burn_time, float P_tank_O, float P_tank_F)
Dict[str, float] _extract_pintle_params(self, PintleEngineConfig config)
float _calculate_relative_change(self, Dict[str, float] params_prev, Dict[str, float] params_current)
Dict[str, Any] optimize_coupled(self, Dict[str, Any] design_requirements, Dict[str, Any] constraints, int max_iterations=10, float convergence_tolerance=0.01, bool use_time_varying=True)