63 config_obj: PintleEngineConfig,
64 runner: PintleEngineRunner,
65 requirements: Dict[str, Any],
66 target_burn_time: float,
68 tolerances: Dict[str, float],
69 pressure_config: Dict[str, Any],
70 progress_callback: Optional[callable] =
None,
71 use_time_varying: bool =
True,
72) -> Tuple[PintleEngineConfig, Dict[str, Any]]:
74 Full engine optimization with real iterative optimization and progress tracking.
77 - Real scipy optimization with progress callback
78 - Flexible independent pressure curves for LOX/Fuel
79 - Tolerances for early stopping
80 - 200-point pressure curves
81 - COPV pressure curve calculation (260K temperatures)
82 - Flight sim validation for good candidates
83 - Time-varying analysis (ablative recession, geometry evolution) if enabled
86 from scipy.optimize
import minimize, differential_evolution
87 from pathlib
import Path
88 from datetime
import datetime
102 obj_tolerance = requirements.get(
"objective_tolerance", 2.0)
105 opt_state: Dict[str, Any] = {
107 "best_objective": float(
'inf'),
111 "objective_satisfied":
False,
112 "satisfied_obj": float(
'inf'),
113 "satisfied_logged":
False,
114 "stop_optimization":
False,
115 "satisfied_eval_count": 0,
117 log_flags: Dict[str, bool] = {
118 "promoted_state_logged":
False,
119 "marginal_candidate_logged":
False,
124 workspace_root = Path(__file__).resolve().parents[2]
125 log_file_path = workspace_root /
"output" /
"logs" /
"full_engine_optimizer.log"
127 def log_status(stage: str, message: str) ->
None:
128 """Persist layer status updates to a root-level log for offline analysis."""
129 timestamp = datetime.utcnow().isoformat()
130 entry = f
"[{timestamp}] {stage}: {message}\n"
132 with log_file_path.open(
"a", encoding=
"utf-8")
as log_file:
133 log_file.write(entry)
138 def update_progress(stage: str, progress: float, message: str):
139 if progress_callback:
140 progress_callback(stage, progress, message)
143 log_status(
"Run",
"-" * 80)
145 update_progress(
"Initialization", 0.02,
"Extracting requirements...")
148 target_thrust = requirements.get(
"target_thrust", 7000.0)
149 target_apogee = requirements.get(
"target_apogee", 3048.0)
150 optimal_of = requirements.get(
"optimal_of_ratio", 2.3)
151 min_Lstar = requirements.get(
"min_Lstar", 0.95)
152 max_Lstar = requirements.get(
"max_Lstar", 1.27)
153 min_stability = requirements.get(
"min_stability_margin", 1.2)
154 max_chamber_od = requirements.get(
"max_chamber_outer_diameter", 0.15)
155 max_nozzle_exit = requirements.get(
"max_nozzle_exit_diameter", 0.101)
156 max_engine_length = requirements.get(
"max_engine_length", 0.5)
157 copv_volume_m3 = requirements.get(
"copv_free_volume_m3", 0.0045)
161 f
"Starting optimization | Thrust={target_thrust:.0f}N, Apogee={target_apogee:.0f}m, O/F={optimal_of:.2f}"
165 thrust_tol = tolerances.get(
"thrust", 0.10)
166 apogee_tol = tolerances.get(
"apogee", 0.15)
170 lox_P_start = pressure_config.get(
"lox_start_psi", 500) * psi_to_Pa
171 lox_P_end_ratio = pressure_config.get(
"lox_end_pct", 0.70)
172 fuel_P_start = pressure_config.get(
"fuel_start_psi", 500) * psi_to_Pa
173 fuel_P_end_ratio = pressure_config.get(
"fuel_end_pct", 0.70)
179 P_atm_default = 101325.0
180 P_amb_launch = P_atm_default
182 env_cfg = getattr(config_obj,
"environment",
None)
183 if env_cfg
is not None and getattr(env_cfg,
"elevation",
None)
is not None:
188 P_amb_launch = float(compute_ambient_pressure_from_elevation(float(env_cfg.elevation)))
190 P_amb_launch = P_atm_default
193 target_P_exit = 1 * P_amb_launch
196 pressure_mode = pressure_config.get(
"mode",
"optimizer_controlled")
204 update_progress(
"Layer 1", 0.05,
"Running Layer 1 optimization...")
207 optimized_config, layer1_results = run_layer1_optimization(
208 config_obj=config_obj,
210 requirements=requirements,
211 target_burn_time=target_burn_time,
212 tolerances=tolerances,
213 pressure_config=pressure_config,
214 update_progress=update_progress,
215 log_status=log_status,
219 layer1_performance = layer1_results.get(
"performance", {})
220 initial_thrust = layer1_performance.get(
"F", 0)
221 initial_thrust_error = layer1_performance.get(
"initial_thrust_error", 1.0)
222 initial_MR_error = layer1_performance.get(
"initial_MR_error", 1.0)
223 pressure_candidate_valid = layer1_performance.get(
"pressure_candidate_valid",
False)
226 final_performance = layer1_performance.copy()
229 optimized_pressure_curves = layer1_results.get(
"optimized_pressure_curves", {})
230 P_O_start_psi = optimized_pressure_curves.get(
"lox_start_psi", max_lox_P_psi * 0.8)
231 P_F_start_psi = optimized_pressure_curves.get(
"fuel_start_psi", max_fuel_P_psi * 0.8)
234 optimized_config_runner = copy.deepcopy(optimized_config)
235 if hasattr(optimized_config_runner,
"ablative_cooling")
and optimized_config_runner.ablative_cooling:
236 optimized_config_runner.ablative_cooling.enabled =
False
237 if hasattr(optimized_config_runner,
"graphite_insert")
and optimized_config_runner.graphite_insert:
238 optimized_config_runner.graphite_insert.enabled =
False
242 P_O_initial = P_O_start_psi * psi_to_Pa
243 P_F_initial = P_F_start_psi * psi_to_Pa
247 time_array = np.linspace(0.0, target_burn_time, n_time_points)
248 P_tank_O_array = np.full(n_time_points, P_O_start_psi * psi_to_Pa)
249 P_tank_F_array = np.full(n_time_points, P_F_start_psi * psi_to_Pa)
253 "iteration_history": layer1_results.get(
"iteration_history", []),
254 "convergence_info": layer1_results.get(
"convergence_info", {}),
255 "optimized_pressure_curves": optimized_pressure_curves,
259 time_varying_results =
None
260 burn_candidate_valid =
False
261 pressure_curves =
None
279 layer1_thrust_error_pct = initial_thrust_error * 100
280 layer1_acceptable = (layer1_thrust_error_pct < 50.0)
and (initial_thrust > 0)
312 if use_time_varying
and layer1_acceptable:
318 P_O_start_pa = P_tank_O_array[0]
if len(P_tank_O_array) > 0
else max_lox_P_psi * psi_to_Pa * 0.8
319 P_F_start_pa = P_tank_F_array[0]
if len(P_tank_F_array) > 0
else max_fuel_P_psi * psi_to_Pa * 0.8
321 optimized_config, time_array_2a, P_tank_O_optimized, P_tank_F_optimized, pressure_summary, pressure_success = run_layer2_pressure(
322 optimized_config=optimized_config,
323 initial_lox_pressure_pa=P_O_start_pa,
324 initial_fuel_pressure_pa=P_F_start_pa,
325 target_burn_time=target_burn_time,
326 requirements=requirements,
328 progress_callback=progress_callback,
330 except Exception
as layer2_error:
331 log_status(
"Layer 2 Error", f
"Layer 2 optimization failed: {layer2_error}")
353 if use_time_varying
and layer1_acceptable:
358 max_lox_P_psi = pressure_config.get(
"max_lox_pressure_psi", 500)
359 max_fuel_P_psi = pressure_config.get(
"max_fuel_pressure_psi", 500)
363 rocket_dry_mass_kg = getattr(config_obj.rocket,
'dry_mass_kg',
None)
if hasattr(config_obj,
'rocket')
else None
364 max_lox_tank_capacity_kg = getattr(config_obj.rocket,
'lox_tank_capacity_kg',
None)
if hasattr(config_obj,
'rocket')
else None
365 max_fuel_tank_capacity_kg = getattr(config_obj.rocket,
'fuel_tank_capacity_kg',
None)
if hasattr(config_obj,
'rocket')
else None
368 if rocket_dry_mass_kg
is None:
370 rocket_dry_mass_kg = 50.0
371 if max_lox_tank_capacity_kg
is None:
373 max_lox_tank_capacity_kg = 20.0
374 if max_fuel_tank_capacity_kg
is None:
375 max_fuel_tank_capacity_kg = 10.0
378 update_progress(
"Layer 2a: Pressure Curve Optimization", 0.60,
"Optimizing pressure curves for full burn...")
380 optimized_config, time_array_2a, P_tank_O_optimized, P_tank_F_optimized, pressure_summary, pressure_success = run_layer2_pressure(
381 optimized_config=optimized_config,
382 initial_lox_pressure_pa=P_O_start_pa,
383 initial_fuel_pressure_pa=P_F_start_pa,
384 target_burn_time=target_burn_time,
385 requirements=requirements,
387 progress_callback=progress_callback,
389 except Exception
as layer2_error:
390 log_status(
"Layer 2 Error", f
"Layer 2 optimization failed: {layer2_error}")
412 if use_time_varying
and layer1_acceptable:
425 rocket_dry_mass_kg = getattr(config_obj.rocket,
'dry_mass_kg',
None)
if hasattr(config_obj,
'rocket')
else None
426 max_lox_tank_capacity_kg = getattr(config_obj.rocket,
'lox_tank_capacity_kg',
None)
if hasattr(config_obj,
'rocket')
else None
427 max_fuel_tank_capacity_kg = getattr(config_obj.rocket,
'fuel_tank_capacity_kg',
None)
if hasattr(config_obj,
'rocket')
else None
430 if rocket_dry_mass_kg
is None:
431 rocket_dry_mass_kg = 50.0
432 if max_lox_tank_capacity_kg
is None:
433 max_lox_tank_capacity_kg = 20.0
434 if max_fuel_tank_capacity_kg
is None:
435 max_fuel_tank_capacity_kg = 10.0
438 update_progress(
"Layer 2a: Pressure Curve Optimization", 0.60,
"Optimizing pressure curves for full burn...")
439 optimized_config, time_array_2a, P_tank_O_optimized, P_tank_F_optimized, pressure_summary, pressure_success = run_layer2_pressure(
440 optimized_config=optimized_config,
441 initial_lox_pressure_pa=P_O_start_pa,
442 initial_fuel_pressure_pa=P_F_start_pa,
443 target_burn_time=target_burn_time,
444 requirements=requirements,
446 progress_callback=progress_callback,
448 except Exception
as layer2_error:
449 log_status(
"Layer 2 Error", f
"Layer 2 optimization failed: {layer2_error}")
490 layer1_thrust_error_pct = initial_thrust_error * 100
491 layer1_acceptable = (layer1_thrust_error_pct < 50.0)
and (initial_thrust > 0)
493 if use_time_varying
and layer1_acceptable:
501 P_O_start_pa = P_tank_O_array[0]
if len(P_tank_O_array) > 0
else max_lox_P_psi * psi_to_Pa * 0.8
502 P_F_start_pa = P_tank_F_array[0]
if len(P_tank_F_array) > 0
else max_fuel_P_psi * psi_to_Pa * 0.8
506 rocket_dry_mass_kg = getattr(config_obj.rocket,
'dry_mass_kg',
None)
if hasattr(config_obj,
'rocket')
else None
507 max_lox_tank_capacity_kg = getattr(config_obj.rocket,
'lox_tank_capacity_kg',
None)
if hasattr(config_obj,
'rocket')
else None
508 max_fuel_tank_capacity_kg = getattr(config_obj.rocket,
'fuel_tank_capacity_kg',
None)
if hasattr(config_obj,
'rocket')
else None
511 if rocket_dry_mass_kg
is None:
513 rocket_dry_mass_kg = 50.0
514 if max_lox_tank_capacity_kg
is None:
516 max_lox_tank_capacity_kg = 20.0
517 if max_fuel_tank_capacity_kg
is None:
518 max_fuel_tank_capacity_kg = 10.0
521 update_progress(
"Layer 2a: Pressure Curve Optimization", 0.60,
"Optimizing pressure curves for full burn...")
523 optimized_config, time_array_2a, P_tank_O_optimized, P_tank_F_optimized, pressure_summary, pressure_success = run_layer2_pressure(
524 optimized_config=optimized_config,
525 initial_lox_pressure_pa=P_O_start_pa,
526 initial_fuel_pressure_pa=P_F_start_pa,
527 peak_thrust=target_thrust,
528 target_apogee_m=target_apogee,
529 rocket_dry_mass_kg=rocket_dry_mass_kg,
530 max_lox_tank_capacity_kg=max_lox_tank_capacity_kg,
531 max_fuel_tank_capacity_kg=max_fuel_tank_capacity_kg,
532 target_burn_time=target_burn_time,
533 n_time_points=n_time_points,
534 update_progress=update_progress,
535 log_status=log_status,
537 optimal_of_ratio=optimal_of,
538 min_stability_margin=min_stability,
542 if pressure_success
and P_tank_O_optimized
is not None and P_tank_F_optimized
is not None:
543 P_tank_O_array = P_tank_O_optimized
544 P_tank_F_array = P_tank_F_optimized
545 time_array = time_array_2a
546 log_status(
"Layer 2a", f
"✓ Pressure curves optimized successfully")
548 log_status(
"Layer 2a", f
"⚠ Pressure optimization failed, using Layer 1 curves")
549 except Exception
as e:
550 log_status(
"Layer 2a Error", f
"Pressure optimization failed: {repr(e)[:200]}, using Layer 1 curves")
559 optimized_config=optimized_config,
560 time_array=time_array,
561 P_tank_O_array=P_tank_O_array,
562 P_tank_F_array=P_tank_F_array,
563 target_thrust=target_thrust,
564 thrust_tol=thrust_tol,
565 optimal_of=optimal_of,
566 n_time_points=n_time_points,
567 update_progress=update_progress,
568 log_status=log_status,
569 max_lox_P_psi=max_lox_P_psi,
570 max_fuel_P_psi=max_fuel_P_psi,
578 "P_tank_O": P_tank_O_array,
579 "P_tank_F": P_tank_F_array,
580 "thrust": full_time_results.get(
"F", np.full(n_time_points, final_performance.get(
"F", target_thrust))),
581 "Isp": full_time_results.get(
"Isp", np.full(n_time_points, final_performance.get(
"Isp", 250))),
582 "Pc": full_time_results.get(
"Pc", np.full(n_time_points, final_performance.get(
"Pc", 2e6))),
583 "mdot_O": full_time_results.get(
"mdot_O", np.full(n_time_points, final_performance.get(
"mdot_O", 1.0))),
584 "mdot_F": full_time_results.get(
"mdot_F", np.full(n_time_points, final_performance.get(
"mdot_F", 0.4))),
588 time_varying_results = full_time_results
593 chugging_stability_history = full_time_results.get(
"chugging_stability_margin", np.array([1.0]))
594 min_time_stability_margin = float(np.min(chugging_stability_history))
598 stability_states = full_time_results.get(
"stability_state",
None)
599 stability_scores = full_time_results.get(
"stability_score",
None)
602 if stability_scores
is None:
605 min_stability_score_time =
max(0.0,
min(1.0, (min_time_stability_margin - 0.3) * 1.5))
607 min_stability_score_time = float(np.min(stability_scores))
609 if stability_states
is None:
611 if min_stability_score_time >= 0.75:
612 min_stability_state_time =
"stable"
613 elif min_stability_score_time >= 0.4:
614 min_stability_state_time =
"marginal"
616 min_stability_state_time =
"unstable"
619 if isinstance(stability_states, (list, np.ndarray)):
620 if all(s ==
"stable" for s
in stability_states):
621 min_stability_state_time =
"stable"
622 elif any(s ==
"unstable" for s
in stability_states):
623 min_stability_state_time =
"unstable"
625 min_stability_state_time =
"marginal"
627 min_stability_state_time = str(stability_states)
629 time_varying_summary = {
630 "avg_thrust": float(np.mean(full_time_results.get(
"F", [target_thrust]))),
631 "min_thrust": float(np.min(full_time_results.get(
"F", [target_thrust]))),
632 "max_thrust": float(np.max(full_time_results.get(
"F", [target_thrust]))),
633 "thrust_std": float(np.std(full_time_results.get(
"F", [0]))),
634 "avg_isp": float(np.mean(full_time_results.get(
"Isp", [250]))),
635 "min_stability_margin": min_time_stability_margin,
636 "min_stability_state": min_stability_state_time,
637 "min_stability_score": min_stability_score_time,
638 "max_recession_chamber": float(np.max(full_time_results.get(
"recession_chamber", [0.0]))),
639 "max_recession_throat": float(np.max(full_time_results.get(
"recession_throat", [0.0]))),
641 final_performance[
"time_varying"] = time_varying_summary
646 min_stability_score = requirements.get(
"min_stability_score", 0.75)
647 require_stable_state = requirements.get(
"require_stable_state",
True)
650 thrust_history = np.atleast_1d(full_time_results.get(
"F", np.full(n_time_points, target_thrust)))
651 MR_history = np.atleast_1d(full_time_results.get(
"MR", np.full(n_time_points, optimal_of)))
652 stability_scores_array = full_time_results.get(
"stability_score",
None)
653 stability_states_array = full_time_results.get(
"stability_state",
None)
657 thrust_history.shape[0],
664 burn_candidate_valid = pressure_candidate_valid
665 max_thrust_error = float(
666 abs(final_performance.get(
"F", target_thrust) - target_thrust) /
max(target_thrust, 1e-9)
668 max_of_error = float(
669 abs(final_performance.get(
"MR", optimal_of) - optimal_of) /
max(optimal_of, 1e-9)
670 )
if optimal_of > 0
else 0.0
671 min_stability_score_time = float(time_varying_summary.get(
"min_stability_score", min_stability_score))
672 min_stability_state_time = time_varying_summary.get(
"min_stability_state",
"stable")
676 thrust_actual_len = len(thrust_history)
if hasattr(thrust_history,
'__len__')
else 0
677 MR_actual_len = len(MR_history)
if hasattr(MR_history,
'__len__')
else 0
678 actual_available_n =
min(available_n, thrust_actual_len, MR_actual_len)
680 if actual_available_n < 2:
682 burn_candidate_valid = pressure_candidate_valid
684 thrust_val = float(thrust_history[0])
if actual_available_n >= 1
else target_thrust
685 MR_val = float(MR_history[0])
if actual_available_n >= 1
else optimal_of
686 if np.isnan(thrust_val)
or not np.isfinite(thrust_val):
687 thrust_val = target_thrust
688 if np.isnan(MR_val)
or not np.isfinite(MR_val):
690 max_thrust_error = float(abs(thrust_val - target_thrust) /
max(target_thrust, 1e-9))
691 max_of_error = float(abs(MR_val - optimal_of) /
max(optimal_of, 1e-9))
if optimal_of > 0
else 1.0
692 min_stability_score_time = float(time_varying_summary.get(
"min_stability_score", min_stability_score))
693 min_stability_state_time = time_varying_summary.get(
"min_stability_state",
"stable")
696 check_indices = np.arange(actual_available_n - 1)
699 thrust_history = thrust_history[:actual_available_n]
700 MR_history = MR_history[:actual_available_n]
703 if len(check_indices) == 0
or np.any(check_indices >= len(thrust_history)):
705 check_indices = np.array([0])
if actual_available_n >= 1
else np.array([])
708 if len(check_indices) > 0:
710 thrust_check = np.array([float(x)
for x
in thrust_history[check_indices]
if np.isfinite(x)])
711 if len(thrust_check) > 0:
712 thrust_errors = np.abs(thrust_check - target_thrust) /
max(target_thrust, 1e-9)
713 max_thrust_error = float(np.max(thrust_errors))
714 avg_thrust_error = float(np.mean(thrust_errors))
716 max_thrust_error = 1.0
717 avg_thrust_error = 1.0
719 max_thrust_error = 1.0
720 avg_thrust_error = 1.0
723 if len(check_indices) > 0:
725 MR_check = np.array([float(x)
for x
in MR_history[check_indices]
if np.isfinite(x)
and optimal_of > 0])
726 if len(MR_check) > 0:
727 of_errors = np.abs(MR_check - optimal_of) /
max(optimal_of, 1e-9)
728 max_of_error = float(np.max(of_errors))
735 if stability_scores_array
is not None and isinstance(stability_scores_array, np.ndarray):
736 stability_scores_array = np.atleast_1d(stability_scores_array)
737 stability_scores_array = stability_scores_array[:actual_available_n]
738 if len(check_indices) > 0
and len(stability_scores_array) > 0:
740 valid_indices = check_indices[check_indices < len(stability_scores_array)]
741 if len(valid_indices) > 0:
742 stability_scores_check = stability_scores_array[valid_indices]
743 min_stability_score_time = float(np.min(stability_scores_check))
745 min_stability_score_time = float(stability_scores_array[0])
if len(stability_scores_array) > 0
else 0.5
747 min_stability_score_time = float(stability_scores_array[0])
if len(stability_scores_array) > 0
else 0.5
750 chugging_history = np.atleast_1d(
751 full_time_results.get(
"chugging_stability_margin", np.array([1.0]))
753 chugging_history = chugging_history[:actual_available_n]
754 if len(check_indices) > 0
and len(chugging_history) > 0:
756 valid_indices = check_indices[check_indices < len(chugging_history)]
757 if len(valid_indices) > 0:
758 min_time_stability_margin = float(np.min(chugging_history[valid_indices]))
760 min_time_stability_margin = float(chugging_history[0])
if len(chugging_history) > 0
else 1.0
762 min_time_stability_margin = float(chugging_history[0])
if len(chugging_history) > 0
else 1.0
763 min_stability_score_time =
max(
764 0.0,
min(1.0, (min_time_stability_margin - 0.3) * 1.5)
767 if stability_states_array
is not None and isinstance(stability_states_array, (list, np.ndarray)):
768 stability_states_array = np.asarray(stability_states_array)
769 stability_states_array = stability_states_array[:actual_available_n]
770 if len(check_indices) > 0
and len(stability_states_array) > 0:
772 valid_indices = check_indices[check_indices < len(stability_states_array)]
773 if len(valid_indices) > 0:
774 stability_states_check = stability_states_array[valid_indices]
776 stability_states_check = np.array([stability_states_array[0]])
if len(stability_states_array) > 0
else np.array([
"stable"])
778 stability_states_check = np.array([stability_states_array[0]])
if len(stability_states_array) > 0
else np.array([
"stable"])
780 has_unstable = np.any(stability_states_check ==
"unstable")
781 all_stable = np.all(stability_states_check ==
"stable")
783 min_stability_state_time =
"stable"
785 min_stability_state_time =
"unstable"
787 min_stability_state_time =
"marginal"
790 if min_stability_score_time >= 0.75:
791 min_stability_state_time =
"stable"
792 elif min_stability_score_time >= 0.4:
793 min_stability_state_time =
"marginal"
795 min_stability_state_time =
"unstable"
798 if require_stable_state:
800 stability_valid_time = (min_stability_state_time !=
"unstable")
and (min_stability_score_time >= min_stability_score * 0.7)
803 stability_valid_time = (min_stability_state_time !=
"unstable")
and (min_stability_score_time >= min_stability_score * 0.7)
810 avg_thrust_error = float(np.mean(np.abs(thrust_history[:actual_available_n] - target_thrust) /
max(target_thrust, 1e-9)))
if actual_available_n > 0
else 1.0
817 burn_candidate_valid = (
818 stability_valid_time
and
819 avg_thrust_error < 0.25
and
820 max_thrust_error < 0.40
and
823 final_performance[
"burn_candidate_valid"] = burn_candidate_valid
825 max_thrust_error = float(max_thrust_error)
if np.isfinite(max_thrust_error)
else 1.0
826 avg_thrust_error = float(avg_thrust_error)
if np.isfinite(avg_thrust_error)
else 1.0
827 max_of_error = float(max_of_error)
if np.isfinite(max_of_error)
else 1.0
829 final_performance[
"max_thrust_error_time"] = max_thrust_error
830 final_performance[
"avg_thrust_error_time"] = avg_thrust_error
831 final_performance[
"max_of_error_time"] = max_of_error
834 "Layer 2: Burn Candidate",
836 f
"Burn candidate {'✓ VALID' if burn_candidate_valid else '✗ INVALID'} - Stability: {min_stability_state_time} (score: {min_stability_score_time:.2f})",
840 f
"{'VALID' if burn_candidate_valid else 'INVALID'} | Stability {min_stability_state_time} (score {min_stability_score_time:.2f}), "
841 f
"max thrust err {max_thrust_error*100:.1f}%, max O/F err {max_of_error*100:.1f}%"
856 if full_time_results
and len(full_time_results) > 0:
857 update_progress(
"Layer 3: Burn Analysis Optimization", 0.68,
"Optimizing ablative and graphite parameters...")
860 ablative_cfg = optimized_config.ablative_cooling
if hasattr(optimized_config,
'ablative_cooling')
else None
861 graphite_cfg = optimized_config.graphite_insert
if hasattr(optimized_config,
'graphite_insert')
else None
864 recession_chamber_history = full_time_results.get(
"recession_chamber", np.zeros(n_time_points))
865 recession_throat_history = full_time_results.get(
"recession_throat", np.zeros(n_time_points))
866 max_recession_chamber = float(np.max(recession_chamber_history))
867 max_recession_throat = float(np.max(recession_throat_history))
870 from scipy.optimize
import minimize
as scipy_minimize
875 if ablative_cfg
and ablative_cfg.enabled:
877 target_ablative = max_recession_chamber * 1.2
878 layer3_bounds.append((
max(0.003, target_ablative * 0.8),
min(0.020, target_ablative * 1.5)))
879 layer3_x0.append(ablative_cfg.initial_thickness)
881 if graphite_cfg
and graphite_cfg.enabled:
883 target_graphite = max_recession_throat * 1.2
884 layer3_bounds.append((
max(0.003, target_graphite * 0.8),
min(0.015, target_graphite * 1.5)))
885 layer3_x0.append(graphite_cfg.initial_thickness)
887 if len(layer3_x0) > 0:
888 layer3_x0 = np.array(layer3_x0)
890 def layer3_objective(x_layer3):
891 """Optimize thermal protection to minimize mass while meeting recession requirements."""
894 config_layer3 = copy.deepcopy(optimized_config)
896 if ablative_cfg
and ablative_cfg.enabled:
897 config_layer3.ablative_cooling.initial_thickness = float(np.clip(x_layer3[idx], layer3_bounds[idx][0], layer3_bounds[idx][1]))
899 if graphite_cfg
and graphite_cfg.enabled:
900 config_layer3.graphite_insert.initial_thickness = float(np.clip(x_layer3[idx], layer3_bounds[idx][0], layer3_bounds[idx][1]))
906 results_layer3 = runner_layer3.evaluate_arrays_with_time(
910 track_ablative_geometry=
True,
911 use_coupled_solver=
True,
915 results_layer3 = runner_layer3.evaluate_arrays_with_time(
919 track_ablative_geometry=
True,
920 use_coupled_solver=
False,
924 recession_chamber = float(np.max(results_layer3.get(
"recession_chamber", [0.0])))
925 recession_throat = float(np.max(results_layer3.get(
"recession_throat", [0.0])))
929 recession_penalty = 0.0
930 if ablative_cfg
and ablative_cfg.enabled:
931 thickness = x_layer3[idx]
934 if recession_chamber > thickness * 0.95:
935 recession_penalty += 1000.0 * (recession_chamber - thickness * 0.95)
937 if graphite_cfg
and graphite_cfg.enabled:
938 thickness = x_layer3[idx]
939 if recession_throat > thickness * 0.95:
940 recession_penalty += 1000.0 * (recession_throat - thickness * 0.95)
943 total_thickness = np.sum(x_layer3)
944 obj = total_thickness * 1000 + recession_penalty
946 except Exception
as e:
951 result_layer3 = scipy_minimize(
955 bounds=layer3_bounds,
956 options={
'maxiter': 30,
'ftol': 1e-5}
961 if ablative_cfg
and ablative_cfg.enabled:
962 optimized_config.ablative_cooling.initial_thickness = float(np.clip(result_layer3.x[idx], layer3_bounds[idx][0], layer3_bounds[idx][1]))
963 update_progress(
"Layer 3: Burn Analysis Optimization", 0.70,
964 f
"✓ Optimized ablative: {optimized_config.ablative_cooling.initial_thickness*1000:.2f}mm (recession: {max_recession_chamber*1000:.2f}mm)")
966 if graphite_cfg
and graphite_cfg.enabled:
967 optimized_config.graphite_insert.initial_thickness = float(np.clip(result_layer3.x[idx], layer3_bounds[idx][0], layer3_bounds[idx][1]))
968 update_progress(
"Layer 3: Burn Analysis Optimization", 0.72,
969 f
"✓ Optimized graphite: {optimized_config.graphite_insert.initial_thickness*1000:.2f}mm (recession: {max_recession_throat*1000:.2f}mm)")
970 except Exception
as e:
971 update_progress(
"Layer 3: Burn Analysis Optimization", 0.72, f
"⚠️ Layer 3 optimization failed: {e}, using current values")
974 update_progress(
"Layer 3: Burn Analysis", 0.74,
"Re-running time series with optimized thermal protection...")
976 full_time_results_updated = full_time_results
if 'full_time_results' in locals()
else {}
979 full_time_results_updated = optimized_runner_updated.evaluate_arrays_with_time(
983 track_ablative_geometry=
True,
984 use_coupled_solver=
True,
987 time_varying_results = full_time_results_updated
988 time_varying_summary[
"max_recession_chamber"] = float(np.max(full_time_results_updated.get(
"recession_chamber", [0.0])))
989 time_varying_summary[
"max_recession_throat"] = float(np.max(full_time_results_updated.get(
"recession_throat", [0.0])))
992 pressure_curves[
"thrust"] = full_time_results_updated.get(
"F", pressure_curves[
"thrust"])
993 pressure_curves[
"mdot_O"] = full_time_results_updated.get(
"mdot_O", pressure_curves[
"mdot_O"])
994 pressure_curves[
"mdot_F"] = full_time_results_updated.get(
"mdot_F", pressure_curves[
"mdot_F"])
995 except Exception
as e:
996 update_progress(
"Layer 3: Burn Analysis", 0.74, f
"⚠️ Re-evaluation failed: {e}, using original results")
1003 if ablative_cfg
and ablative_cfg.enabled:
1005 max_recession_chamber = float(np.max(full_time_results_updated.get(
"recession_chamber", [0.0])))
1006 thickness = optimized_config.ablative_cooling.initial_thickness
1008 ablative_ok = max_recession_chamber <= thickness * 0.95
1009 if graphite_cfg
and graphite_cfg.enabled:
1011 max_recession_throat = float(np.max(full_time_results_updated.get(
"recession_throat", [0.0])))
1012 thickness = optimized_config.graphite_insert.initial_thickness
1014 graphite_ok = max_recession_throat <= thickness * 0.95
1016 final_performance[
"ablative_adequate"] = ablative_ok
1017 final_performance[
"graphite_adequate"] = graphite_ok
1020 final_performance[
"thermal_protection_valid"] =
True
1021 final_performance[
"optimized_ablative_thickness"] = optimized_config.ablative_cooling.initial_thickness
if ablative_cfg
and ablative_cfg.enabled
else None
1022 final_performance[
"optimized_graphite_thickness"] = optimized_config.graphite_insert.initial_thickness
if graphite_cfg
and graphite_cfg.enabled
else None
1025 "Completed | Ablative {:.2f} mm, Graphite {:.2f} mm, Max recession chamber {:.2f} mm, throat {:.2f} mm".
format(
1026 (optimized_config.ablative_cooling.initial_thickness * 1000)
if ablative_cfg
and ablative_cfg.enabled
else 0.0,
1027 (optimized_config.graphite_insert.initial_thickness * 1000)
if graphite_cfg
and graphite_cfg.enabled
else 0.0,
1028 time_varying_summary.get(
"max_recession_chamber", 0.0) * 1000,
1029 time_varying_summary.get(
"max_recession_throat", 0.0) * 1000,
1032 except Exception
as e:
1034 warnings.warn(f
"Layer 3 optimization failed: {e}")
1037 f
"Layer 3 optimization failed: {repr(e)[:200]}"
1041 if not use_time_varying:
1042 log_status(
"Layer 2",
"Skipped | Time-varying analysis disabled")
1043 elif not layer1_acceptable:
1044 log_status(
"Layer 2", f
"Skipped | Layer 1 thrust error {layer1_thrust_error_pct:.1f}% too high (>50%)")
1046 if not use_time_varying
or pressure_curves
is None:
1048 sample_indices = [0, n_time_points//4, n_time_points//2, 3*n_time_points//4, n_time_points-1]
1055 for idx
in sample_indices:
1057 results = optimized_runner.evaluate(P_tank_O_array[idx], P_tank_F_array[idx])
1058 sample_F.append(results.get(
"F", 0))
1059 sample_Isp.append(results.get(
"Isp", 0))
1060 sample_Pc.append(results.get(
"Pc", 0))
1061 sample_mdot_O.append(results.get(
"mdot_O", 0))
1062 sample_mdot_F.append(results.get(
"mdot_F", 0))
1065 sample_F.append(final_performance.get(
"F", target_thrust))
1066 sample_Isp.append(final_performance.get(
"Isp", 250))
1067 sample_Pc.append(final_performance.get(
"Pc", 2e6))
1068 sample_mdot_O.append(final_performance.get(
"mdot_O", 1.0))
1069 sample_mdot_F.append(final_performance.get(
"mdot_F", 0.4))
1072 from scipy.interpolate
import interp1d
1073 sample_times = [time_array[i]
for i
in sample_indices]
1075 thrust_interp = interp1d(sample_times, sample_F, kind=
'linear', fill_value=
'extrapolate')
1076 isp_interp = interp1d(sample_times, sample_Isp, kind=
'linear', fill_value=
'extrapolate')
1077 pc_interp = interp1d(sample_times, sample_Pc, kind=
'linear', fill_value=
'extrapolate')
1078 mdot_O_interp = interp1d(sample_times, sample_mdot_O, kind=
'linear', fill_value=
'extrapolate')
1079 mdot_F_interp = interp1d(sample_times, sample_mdot_F, kind=
'linear', fill_value=
'extrapolate')
1083 "P_tank_O": P_tank_O_array,
1084 "P_tank_F": P_tank_F_array,
1085 "thrust": thrust_interp(time_array),
1086 "Isp": isp_interp(time_array),
1087 "Pc": pc_interp(time_array),
1088 "mdot_O": mdot_O_interp(time_array),
1089 "mdot_F": mdot_F_interp(time_array),
1092 update_progress(
"COPV Calculation", 0.65,
"Calculating COPV pressure curve (T=260K)...")
1095 copv_results = calculate_copv_pressure_curve(
1097 pressure_curves[
"mdot_O"],
1098 pressure_curves[
"mdot_F"],
1107 update_progress(
"Validation", 0.70,
"Running stability checks at initial conditions...")
1111 diagnostics = SystemDiagnostics(optimized_config, optimized_runner)
1112 validation_results = diagnostics.run_full_diagnostics(P_O_initial, P_F_initial)
1113 except Exception
as e:
1114 validation_results = {
"error": str(e)}
1128 flight_sim_result: Dict[str, Any] = {
1131 "max_velocity": 0.0,
1133 "flight_candidate_valid":
False,
1141 should_run_flight = (
1143 and pressure_candidate_valid
1144 and pressure_curves
is not None
1147 if should_run_flight:
1149 "Layer 4: Flight Candidate",
1151 "Running flight simulation with tank-fill iteration...",
1156 flight_sim_result = run_layer4_flight_simulation(
1157 optimized_config=optimized_config,
1158 pressure_curves=pressure_curves,
1159 time_array=time_array,
1160 P_tank_O_array=P_tank_O_array,
1161 P_tank_F_array=P_tank_F_array,
1162 target_burn_time=target_burn_time,
1163 target_apogee=target_apogee,
1164 apogee_tol=apogee_tol,
1165 update_progress=update_progress,
1166 log_status=log_status,
1167 run_flight_simulation_func=run_flight_simulation,
1169 except Exception
as e:
1170 flight_sim_result = {
1174 "max_velocity": 0.0,
1176 "flight_candidate_valid":
False,
1179 "Layer 4: Flight Candidate",
1181 f
"Flight sim error: {e}",
1185 if not pressure_candidate_valid:
1186 reason =
"pressure candidate invalid"
1187 elif pressure_curves
is None:
1188 reason =
"no pressure curves available"
1192 "Layer 4: Flight Candidate",
1194 f
"Skipping flight sim ({reason})",
1196 log_status(
"Layer 4", f
"Skipped | Reason: {reason}")
1197 flight_sim_result = {
1202 "max_velocity": 0.0,
1204 "flight_candidate_valid":
False,
1208 final_performance[
"flight_candidate_valid"] = flight_sim_result.get(
1209 "flight_candidate_valid",
False
1212 update_progress(
"Finalization", 0.90,
"Assembling results...")
1215 design_requirements = {
1216 "target_thrust": target_thrust,
1217 "target_apogee": target_apogee,
1218 "target_burn_time": target_burn_time,
1219 "target_stability_margin": min_stability,
1220 "P_tank_O_start": lox_P_start,
1221 "P_tank_F_start": fuel_P_start,
1222 "target_MR": optimal_of,
1227 "min_Lstar": min_Lstar,
1228 "max_Lstar": max_Lstar,
1229 "max_chamber_diameter": max_chamber_od,
1230 "max_nozzle_exit_diameter": max_nozzle_exit,
1231 "thrust_tolerance": thrust_tol,
1232 "apogee_tolerance": apogee_tol,
1236 coupled_results[
"performance"] = final_performance
1237 coupled_results[
"validation"] = validation_results
1238 coupled_results[
"design_requirements"] = design_requirements
1239 coupled_results[
"constraints"] = constraints
1240 coupled_results[
"optimized_parameters"] = extract_all_parameters(optimized_config)
1241 coupled_results[
"pressure_curves"] = pressure_curves
1242 coupled_results[
"copv_results"] = copv_results
1243 coupled_results[
"flight_sim_result"] = flight_sim_result
1244 coupled_results[
"time_array"] = time_array
1246 coupled_results[
"exit_pressure_targeting"] = {
1247 "P_ambient_launch": P_amb_launch,
1248 "target_P_exit": target_P_exit,
1252 if time_varying_results
is not None:
1253 coupled_results[
"time_varying_results"] = time_varying_results
1256 coupled_results[
"layer_status"] = {
1257 "layer_1_pressure_candidate": pressure_candidate_valid,
1258 "layer_2_burn_candidate": burn_candidate_valid
if use_time_varying
else None,
1259 "layer_3_thermal_protection": final_performance.get(
"thermal_protection_valid",
None),
1260 "layer_4_flight_candidate": final_performance.get(
"flight_candidate_valid",
False),
1261 "all_layers_passed": (
1262 pressure_candidate_valid
and
1263 (burn_candidate_valid
or not use_time_varying)
and
1264 final_performance.get(
"flight_candidate_valid",
False)
1267 layer_summary = coupled_results[
"layer_status"]
1270 "Summary | L1={layer_1_pressure_candidate}, L2={layer_2_burn_candidate}, "
1271 "L3={layer_3_thermal_protection}, L4={layer_4_flight_candidate}".
format(**layer_summary)
1275 coupled_results[
"pressure_curve_config"] = {
1276 "mode": pressure_mode,
1277 "max_lox_pressure_psi": max_lox_P_psi,
1278 "max_fuel_pressure_psi": max_fuel_P_psi,
1281 update_progress(
"Complete", 1.0,
"Optimization complete!")
1283 return optimized_config, coupled_results