26 optimized_config: PintleEngineConfig,
27 time_array: np.ndarray,
28 P_tank_O_array: np.ndarray,
29 P_tank_F_array: np.ndarray,
30 full_time_results: Dict[str, Any],
32 update_progress: Callable,
34 objective_callback: Optional[Callable[[int, float, float],
None]] =
None,
35 optimization_method: str =
"gradient",
36) -> Tuple[PintleEngineConfig, Dict[str, Any], Dict[str, Any]]:
38 Run Layer 3: Thermal Protection Optimization.
40 Optimizes final thermal protection thicknesses to meet recession requirements
41 with margin while minimizing mass, while preserving the Layer 2 burn
42 quality (impulse, stability).
45 optimization_method: Optimization algorithm to use:
46 - "gradient": Fast gradient descent (recommended). Exploits monotonic
47 relationship between thickness and recession. ~5-10 evaluations.
48 - "cma": CMA-ES global optimizer. More thorough but slower. ~60-80 evals.
49 - "de": Differential Evolution fallback. Similar to CMA-ES.
52 Tuple of (optimized_config, updated_time_results, thermal_results)
54 from scipy.optimize
import minimize
as scipy_minimize
62 from scipy.optimize
import differential_evolution
67 timestamp = datetime.now().strftime(
"%Y%m%d_%H%M%S")
69 output_logs_dir = Path(__file__).resolve().parents[3] /
"output" /
"logs"
70 output_logs_dir.mkdir(parents=
True, exist_ok=
True)
71 log_file_path = output_logs_dir / f
"layer3_thermal_{timestamp}.log"
73 layer3_logger = logging.getLogger(
"layer3_thermal")
74 layer3_logger.setLevel(logging.INFO)
75 layer3_logger.handlers.clear()
77 file_handler = logging.FileHandler(log_file_path, mode=
"w", encoding=
"utf-8")
78 file_handler.setLevel(logging.INFO)
79 file_formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s")
80 file_handler.setFormatter(file_formatter)
81 layer3_logger.addHandler(file_handler)
82 layer3_logger.propagate =
False
84 layer3_logger.info(
"=" * 70)
85 layer3_logger.info(
"Layer 3: Thermal Protection Optimization")
86 layer3_logger.info(
"=" * 70)
87 layer3_logger.info(f
"Log file: {log_file_path}")
88 layer3_logger.info(f
"Time points: {n_time_points}")
90 ablative_cfg = optimized_config.ablative_cooling
if hasattr(optimized_config,
"ablative_cooling")
else None
91 graphite_cfg = optimized_config.graphite_insert
if hasattr(optimized_config,
"graphite_insert")
else None
94 "max_recession_chamber":
None,
95 "max_recession_throat":
None,
96 "ablative_adequate":
True,
97 "graphite_adequate":
True,
98 "thermal_protection_valid":
True,
99 "log_file": log_file_path,
105 if ablative_cfg
and ablative_cfg.enabled:
109 bounds_abl = (0.003, 0.025)
110 layer3_bounds.append(bounds_abl)
112 ablative_x0 = 0.5 * (bounds_abl[0] + bounds_abl[1])
113 layer3_x0.append(ablative_x0)
115 "Ablative enabled: search bounds=[%.3f, %.3f] mm, x0=%.3f mm",
116 bounds_abl[0] * 1000.0,
117 bounds_abl[1] * 1000.0,
118 ablative_x0 * 1000.0,
121 if graphite_cfg
and graphite_cfg.enabled:
123 bounds_gra = (0.003, 0.025)
124 layer3_bounds.append(bounds_gra)
125 graphite_x0 = 0.5 * (bounds_gra[0] + bounds_gra[1])
126 layer3_x0.append(graphite_x0)
128 "Graphite enabled: search bounds=[%.3f, %.3f] mm, x0=%.3f mm",
129 bounds_gra[0] * 1000.0,
130 bounds_gra[1] * 1000.0,
131 graphite_x0 * 1000.0,
136 updated_time_results = full_time_results
142 baseline_thrust = np.atleast_1d(full_time_results.get(
"F", np.zeros_like(time_array)))
143 if baseline_thrust.size > n_time_points:
144 baseline_thrust = baseline_thrust[:n_time_points]
145 baseline_impulse = float(np.trapezoid(baseline_thrust, time_array[: baseline_thrust.size]))
147 baseline_impulse = 0.0
150 baseline_min_stability =
None
152 if "chugging_stability_margin" in full_time_results:
153 chug = np.nan_to_num(
154 np.atleast_1d(full_time_results[
"chugging_stability_margin"]),
159 baseline_min_stability = float(np.min(chug))
160 elif "stability_score" in full_time_results:
161 stab = np.nan_to_num(
162 np.atleast_1d(full_time_results[
"stability_score"]),
167 baseline_min_stability = float(np.min(stab))
169 baseline_min_stability =
None
172 "Baseline metrics from Layer 2: impulse=%.3f kN·s, min_stability=%s",
173 baseline_impulse / 1000.0,
174 f
"{baseline_min_stability:.3f}" if baseline_min_stability
is not None else "N/A",
177 if len(layer3_x0) > 0:
178 layer3_x0 = np.array(layer3_x0, dtype=float)
181 layer3_state: Dict[str, Any] = {
183 "best_objective": float(
"inf"),
189 _objective_cache: Dict[Tuple[float, ...], float] = {}
191 def downsample_array(arr: np.ndarray, n_coarse: int) -> np.ndarray:
192 """Downsample array from fine grid to coarse grid using linear interpolation."""
194 if n_coarse >= n_fine:
197 indices = np.linspace(0, n_fine - 1, n_coarse)
198 return np.interp(indices, np.arange(n_fine), arr)
200 def layer3_objective(x_layer3: np.ndarray, n_eval_points: Optional[int] =
None) -> float:
201 """Optimize thermal protection to minimize mass while meeting recession requirements."""
203 layer3_state[
"eval_index"] += 1
204 eval_idx = int(layer3_state[
"eval_index"])
207 x_layer3 = np.asarray(x_layer3, dtype=float)
208 if not np.all(np.isfinite(x_layer3)):
209 layer3_logger.warning(
210 "Eval %d received non-finite thickness vector %s; returning large penalty.",
218 if hasattr(optimized_config,
'model_copy'):
219 config_layer3 = optimized_config.model_copy(deep=
True)
221 config_layer3 = copy.deepcopy(optimized_config)
223 if hasattr(config_layer3,
'combustion')
and hasattr(config_layer3.combustion,
'efficiency'):
224 config_layer3.combustion.efficiency.use_turbulence_coupling =
True
226 chosen_thicknesses: list[float] = []
233 if ablative_cfg
and ablative_cfg.enabled:
234 t_abl = float(np.clip(x_layer3[idx_param], layer3_bounds[idx_param][0], layer3_bounds[idx_param][1]))
235 t_abl = float(np.round(t_abl / quantum_m) * quantum_m)
236 config_layer3.ablative_cooling.initial_thickness = t_abl
237 chosen_thicknesses.append(t_abl)
240 if graphite_cfg
and graphite_cfg.enabled:
242 np.clip(x_layer3[idx_param], layer3_bounds[idx_param][0], layer3_bounds[idx_param][1])
244 t_gra = float(np.round(t_gra / quantum_m) * quantum_m)
245 config_layer3.graphite_insert.initial_thickness = t_gra
246 chosen_thicknesses.append(t_gra)
249 if ablative_cfg
and ablative_cfg.enabled:
250 if abs(config_layer3.ablative_cooling.initial_thickness - chosen_thicknesses[0]) > 1e-9:
251 layer3_logger.warning(
252 "Eval %03d: Config ablative thickness mismatch! Expected %.6f, got %.6f",
254 chosen_thicknesses[0],
255 config_layer3.ablative_cooling.initial_thickness,
257 if graphite_cfg
and graphite_cfg.enabled:
258 gra_idx = 0
if not ablative_cfg
or not ablative_cfg.enabled
else 1
259 if abs(config_layer3.graphite_insert.initial_thickness - chosen_thicknesses[gra_idx]) > 1e-9:
260 layer3_logger.warning(
261 "Eval %03d: Config graphite thickness mismatch! Expected %.6f, got %.6f",
263 chosen_thicknesses[gra_idx],
264 config_layer3.graphite_insert.initial_thickness,
268 cache_key = (tuple(chosen_thicknesses), n_eval_points)
269 if cache_key
in _objective_cache:
270 obj_cached = float(_objective_cache[cache_key])
273 "Eval %03d (cached): thicknesses=%s mm, n_pts=%s, obj=%.3f, dt=%.2fs",
275 [t * 1000.0
for t
in chosen_thicknesses],
287 if ablative_cfg
and ablative_cfg.enabled:
288 runner_abl_thick = runner_layer3.config.ablative_cooling.initial_thickness
289 if abs(runner_abl_thick - chosen_thicknesses[0]) > 1e-9:
291 "Eval %03d: Runner config ablative thickness mismatch! Expected %.6f, got %.6f",
293 chosen_thicknesses[0],
296 if graphite_cfg
and graphite_cfg.enabled:
297 gra_idx = 0
if not ablative_cfg
or not ablative_cfg.enabled
else 1
298 runner_gra_thick = runner_layer3.config.graphite_insert.initial_thickness
299 if abs(runner_gra_thick - chosen_thicknesses[gra_idx]) > 1e-9:
301 "Eval %03d: Runner config graphite thickness mismatch! Expected %.6f, got %.6f",
303 chosen_thicknesses[gra_idx],
309 if n_eval_points
is not None and n_eval_points < n_time_points:
310 eval_time_array = downsample_array(time_array, n_eval_points)
311 eval_P_tank_O = downsample_array(P_tank_O_array, n_eval_points)
312 eval_P_tank_F = downsample_array(P_tank_F_array, n_eval_points)
315 eval_time_array = time_array
316 eval_P_tank_O = P_tank_O_array
317 eval_P_tank_F = P_tank_F_array
321 results_layer3 = runner_layer3.evaluate_arrays_with_time(
325 track_ablative_geometry=
True,
326 use_coupled_solver=
True,
329 recession_chamber = float(np.max(np.atleast_1d(results_layer3.get(
"recession_chamber", [0.0]))))
330 recession_throat = float(np.max(np.atleast_1d(results_layer3.get(
"recession_throat", [0.0]))))
336 thrust_hist = np.atleast_1d(results_layer3.get(
"F", np.zeros_like(eval_time_array)))
337 n_eval = len(eval_time_array)
338 if thrust_hist.size > n_eval:
339 thrust_hist = thrust_hist[:n_eval]
340 thrust_hist = np.nan_to_num(
346 total_impulse = float(np.trapezoid(thrust_hist, eval_time_array[: thrust_hist.size]))
349 impulse_penalty = 0.0
350 if baseline_impulse > 0.0
and total_impulse < baseline_impulse:
354 frac_loss = (baseline_impulse - total_impulse) / baseline_impulse
355 impulse_penalty =
max(0.0, frac_loss) * 1e4
358 stability_penalty = 0.0
360 if "chugging_stability_margin" in results_layer3:
361 chug = np.nan_to_num(
362 np.atleast_1d(results_layer3[
"chugging_stability_margin"]),
367 min_stability = float(np.min(chug))
368 elif "stability_score" in results_layer3:
369 stab = np.nan_to_num(
370 np.atleast_1d(results_layer3[
"stability_score"]),
375 min_stability = float(np.min(stab))
379 if baseline_min_stability
is not None and min_stability
is not None:
383 if min_stability < baseline_min_stability:
384 stability_penalty = (baseline_min_stability - min_stability) * 5e3
394 recession_penalty = 0.0
400 if ablative_cfg
and ablative_cfg.enabled:
401 thickness = x_layer3[idx_param]
402 required_thickness = recession_chamber * margin_factor
403 if required_thickness > 0.0
and thickness < required_thickness:
404 deficit = required_thickness - thickness
405 frac = deficit / required_thickness
406 recession_penalty += penalty_scale * frac * frac
409 if graphite_cfg
and graphite_cfg.enabled:
410 thickness = x_layer3[idx_param]
411 required_thickness = recession_throat * margin_factor
412 if required_thickness > 0.0
and thickness < required_thickness:
413 deficit = required_thickness - thickness
414 frac = deficit / required_thickness
415 recession_penalty += penalty_scale * frac * frac
420 total_thickness = float(np.sum(x_layer3))
422 total_thickness * 1000.0
429 if objective_callback
is not None:
431 best_obj = float(layer3_state[
"best_objective"])
434 layer3_state[
"best_objective"] = best_obj
435 objective_callback(eval_idx, float(obj), float(best_obj))
442 _objective_cache[cache_key] = obj
445 "Eval %03d: thicknesses=%s mm, recession (chamber/throat)=%.3f/%.3f mm, "
446 "penalties: recession=%.3f, impulse=%.3f, stability=%.3f, "
447 "obj=%.3f, dt=%.2fs",
449 [t * 1000.0
for t
in chosen_thicknesses],
450 recession_chamber * 1000.0,
451 recession_throat * 1000.0,
460 except Exception
as exc:
462 layer3_logger.error(
"Exception in eval %03d (%.2fs): %r", eval_idx, eval_time, exc)
465 layer3_logger.error(traceback.format_exc())
470 if optimization_method ==
"gradient":
480 "Starting Layer 3 optimization using GRADIENT DESCENT (fast mode) with %d dims...",
484 "This exploits the monotonic relationship: more thickness = less recession ratio."
489 def evaluate_thickness_feasibility(thicknesses: np.ndarray) -> Tuple[bool, Dict[str, float]]:
491 Evaluate if given thicknesses meet the margin requirement.
492 Returns (is_feasible, recession_data).
495 if hasattr(optimized_config,
'model_copy'):
496 config_test = optimized_config.model_copy(deep=
True)
498 config_test = copy.deepcopy(optimized_config)
500 if hasattr(config_test,
'combustion')
and hasattr(config_test.combustion,
'efficiency'):
501 config_test.combustion.efficiency.use_turbulence_coupling =
True
504 if ablative_cfg
and ablative_cfg.enabled:
505 config_test.ablative_cooling.initial_thickness = float(thicknesses[idx])
507 if graphite_cfg
and graphite_cfg.enabled:
508 config_test.graphite_insert.initial_thickness = float(thicknesses[idx])
512 results_test = runner_test.evaluate_arrays_with_time(
516 track_ablative_geometry=
True,
517 use_coupled_solver=
True,
520 recession_chamber = float(np.max(np.atleast_1d(results_test.get(
"recession_chamber", [0.0]))))
521 recession_throat = float(np.max(np.atleast_1d(results_test.get(
"recession_throat", [0.0]))))
526 "recession_chamber": recession_chamber,
527 "recession_throat": recession_throat,
531 if ablative_cfg
and ablative_cfg.enabled:
532 required_abl = recession_chamber * margin_factor
533 recession_data[
"required_ablative"] = required_abl
534 if thicknesses[idx] < required_abl:
538 if graphite_cfg
and graphite_cfg.enabled:
539 required_gra = recession_throat * margin_factor
540 recession_data[
"required_graphite"] = required_gra
541 if thicknesses[idx] < required_gra:
544 return feasible, recession_data
547 lower_bounds = np.array([b[0]
for b
in layer3_bounds])
548 upper_bounds = np.array([b[1]
for b
in layer3_bounds])
550 layer3_logger.info(
"Step 1: Evaluating recession at lower bounds...")
551 layer3_state[
"eval_index"] += 1
552 _, recession_at_min = evaluate_thickness_feasibility(lower_bounds)
555 "Recession at min thickness: chamber=%.3f mm, throat=%.3f mm",
556 recession_at_min[
"recession_chamber"] * 1000,
557 recession_at_min[
"recession_throat"] * 1000,
563 initial_estimate = []
565 if ablative_cfg
and ablative_cfg.enabled:
566 est_abl = recession_at_min[
"recession_chamber"] * margin_factor * 1.1
567 est_abl = float(np.clip(est_abl, lower_bounds[idx], upper_bounds[idx]))
568 initial_estimate.append(est_abl)
570 if graphite_cfg
and graphite_cfg.enabled:
571 est_gra = recession_at_min[
"recession_throat"] * margin_factor * 1.1
572 est_gra = float(np.clip(est_gra, lower_bounds[idx], upper_bounds[idx]))
573 initial_estimate.append(est_gra)
575 initial_estimate = np.array(initial_estimate)
577 "Initial thickness estimate: %s mm",
578 [t * 1000
for t
in initial_estimate],
583 best_thicknesses = initial_estimate.copy()
585 for dim_idx
in range(len(layer3_bounds)):
586 lo = lower_bounds[dim_idx]
587 hi = upper_bounds[dim_idx]
589 dim_name =
"ablative" if dim_idx == 0
and ablative_cfg
and ablative_cfg.enabled
else "graphite"
590 layer3_logger.info(
"Binary search for %s thickness...", dim_name)
593 for iteration
in range(6):
595 test_thicknesses = best_thicknesses.copy()
596 test_thicknesses[dim_idx] = mid
598 layer3_state[
"eval_index"] += 1
599 eval_idx = int(layer3_state[
"eval_index"])
601 feasible, recession_data = evaluate_thickness_feasibility(test_thicknesses)
604 obj = float(np.sum(test_thicknesses)) * 1000
608 if objective_callback
is not None:
609 best_obj = float(layer3_state[
"best_objective"])
612 layer3_state[
"best_objective"] = best_obj
613 objective_callback(eval_idx, obj, best_obj)
616 "Eval %03d: %s=%.3f mm, feasible=%s, recession=%.3f mm",
621 (recession_data.get(
"recession_chamber", 0)
if dim_name ==
"ablative"
622 else recession_data.get(
"recession_throat", 0)) * 1000,
627 best_thicknesses[dim_idx] = mid
632 best_thicknesses[dim_idx] = hi * 1.02
633 best_thicknesses[dim_idx] = float(np.clip(
634 best_thicknesses[dim_idx],
635 lower_bounds[dim_idx],
636 upper_bounds[dim_idx]
640 "Gradient descent finished: optimal thicknesses = %s mm",
641 [t * 1000
for t
in best_thicknesses],
645 class GradientResult:
646 def __init__(self, x, fun):
651 final_obj = float(np.sum(best_thicknesses)) * 1000
652 result_layer3 = GradientResult(best_thicknesses, final_obj)
654 elif use_cma
and optimization_method !=
"de":
659 "Starting Layer 3 optimization using CMA-ES with %d dims...",
664 span = np.array([hi - lo
for lo, hi
in layer3_bounds])
665 sigma0 = float(np.mean(span) * 0.15)
666 if not np.isfinite(sigma0)
or sigma0 <= 0:
671 n_dims = len(layer3_x0)
672 cma_popsize =
max(8, 4 + int(3 * np.log(n_dims)))
676 cma_budget =
min(80, cma_popsize * 10)
679 cma_stds = span / np.mean(span)
if np.mean(span) > 0
else np.ones(n_dims)
682 lower_bounds = np.array([b[0]
for b
in layer3_bounds])
683 upper_bounds = np.array([b[1]
for b
in layer3_bounds])
687 "bounds": [lower_bounds.tolist(), upper_bounds.tolist()],
688 "popsize": cma_popsize,
699 opts[
"CMA_stds"] = cma_stds.tolist()
702 x0_clamped = np.clip(layer3_x0, lower_bounds, upper_bounds)
703 es = cma.CMAEvolutionStrategy(x0_clamped.tolist(), sigma0, opts)
708 "CMA-ES will use coarse %d-point grid (vs full %d-point grid) for ~8x speedup",
713 best_x_cma = x0_clamped.copy()
714 best_f_cma = float(layer3_objective(best_x_cma, n_eval_points=n_coarse_points))
718 while not es.stop()
and evals_used < cma_budget:
719 candidates = es.ask()
723 remaining_budget = cma_budget - evals_used
724 if remaining_budget < len(candidates):
725 candidates = candidates[:remaining_budget]
727 for candidate
in candidates:
728 x_candidate = np.asarray(candidate, dtype=float)
730 x_candidate = np.clip(x_candidate, lower_bounds, upper_bounds)
731 obj_val = float(layer3_objective(x_candidate, n_eval_points=n_coarse_points))
732 fitnesses.append(obj_val)
736 if obj_val < best_f_cma:
738 best_x_cma = x_candidate.copy()
741 es.tell(candidates, fitnesses)
744 if evals_used % (cma_popsize * 2) == 0:
746 "CMA-ES progress: evals=%d/%d, best_obj=%.6f, sigma=%.6f",
754 "CMA-ES finished: final_obj=%.6f, evals=%d, sigma=%.6f",
762 "Starting final local polish with L-BFGS-B on full %d-point grid...",
767 def layer3_objective_full_res(x):
768 return layer3_objective(x, n_eval_points=
None)
770 result_layer3 = scipy_minimize(
771 layer3_objective_full_res,
774 bounds=layer3_bounds,
783 if result_layer3.success
and np.isfinite(result_layer3.fun):
784 if result_layer3.fun < best_f_cma:
785 best_x_cma = np.asarray(result_layer3.x, dtype=float)
786 best_f_cma = float(result_layer3.fun)
788 "L-BFGS-B polish improved: final_obj=%.6f",
793 "L-BFGS-B polish did not improve (CMA-ES best was better)"
798 def __init__(self, x, fun):
803 result_layer3 = CMA_Result(best_x_cma, best_f_cma)
811 "Using differential_evolution with %d dims (method=%s, cma_available=%s)...",
816 de_result = differential_evolution(
825 "Global search finished: de_obj=%.6f, nfev=%s",
826 float(de_result.fun)
if np.isfinite(de_result.fun)
else float(
"nan"),
827 getattr(de_result,
"nfev",
"N/A"),
830 best_local_x = np.asarray(de_result.x, dtype=float)
831 best_local_obj = float(de_result.fun)
if np.isfinite(de_result.fun)
else float(
"inf")
834 coarse_step_m = 0.0002
835 offsets = [-2, -1, 0, 1, 2]
837 for delta_indices
in itertools.product(offsets, repeat=len(layer3_bounds)):
838 delta_vec = np.asarray(delta_indices, dtype=float) * coarse_step_m
839 candidate_x = best_local_x + delta_vec
840 for i, (lo, hi)
in enumerate(layer3_bounds):
841 candidate_x[i] = float(np.clip(candidate_x[i], lo, hi))
843 obj_val = float(layer3_objective(candidate_x))
844 if obj_val < best_local_obj:
845 best_local_obj = obj_val
846 best_local_x = candidate_x
849 "Coarse local sweep finished: best_obj=%.6f at x=%s mm",
851 [float(v) * 1000.0
for v
in np.atleast_1d(best_local_x)],
854 layer3_logger.info(
"Starting Layer 3 local optimization using L-BFGS-B...")
855 result_layer3 = scipy_minimize(
859 bounds=layer3_bounds,
867 "Local optimization finished: success=%s, final_obj=%.6f, nit=%s, nfev=%s",
868 result_layer3.success,
869 float(result_layer3.fun)
if np.isfinite(result_layer3.fun)
else float(
"nan"),
870 getattr(result_layer3,
"nit",
"N/A"),
871 getattr(result_layer3,
"nfev",
"N/A"),
876 if ablative_cfg
and ablative_cfg.enabled:
877 optimized_config.ablative_cooling.initial_thickness = float(
878 np.clip(result_layer3.x[idx_param], layer3_bounds[idx_param][0], layer3_bounds[idx_param][1])
880 thermal_results[
"optimized_ablative_thickness"] = optimized_config.ablative_cooling.initial_thickness
888 "Layer 3: Burn Analysis Optimization",
891 f
"✓ Optimized ablative: "
892 f
"{optimized_config.ablative_cooling.initial_thickness*1000:.2f}mm "
896 "Optimized ablative thickness: %.3f mm",
897 optimized_config.ablative_cooling.initial_thickness * 1000.0,
901 if graphite_cfg
and graphite_cfg.enabled:
902 optimized_config.graphite_insert.initial_thickness = float(
903 np.clip(result_layer3.x[idx_param], layer3_bounds[idx_param][0], layer3_bounds[idx_param][1])
905 thermal_results[
"optimized_graphite_thickness"] = optimized_config.graphite_insert.initial_thickness
907 "Layer 3: Burn Analysis Optimization",
910 f
"✓ Optimized graphite: "
911 f
"{optimized_config.graphite_insert.initial_thickness*1000:.2f}mm "
915 "Optimized graphite thickness: %.3f mm",
916 optimized_config.graphite_insert.initial_thickness * 1000.0,
918 except Exception
as e:
919 layer3_logger.error(
"Layer 3 optimization failed: %r", e)
922 layer3_logger.error(traceback.format_exc())
924 "Layer 3: Burn Analysis Optimization",
926 f
"⚠️ Layer 3 optimization failed: {e}, using current values",
930 update_progress(
"Layer 3: Burn Analysis", 0.74,
"Re-running time series with optimized thermal protection...")
933 if ablative_cfg
and ablative_cfg.enabled:
934 optimized_config.ablative_cooling.track_geometry_evolution =
True
935 if graphite_cfg
and graphite_cfg.enabled:
937 if hasattr(optimized_config.graphite_insert,
'enabled'):
938 optimized_config.graphite_insert.enabled =
True
941 if hasattr(optimized_config,
'combustion')
and hasattr(optimized_config.combustion,
'efficiency'):
942 optimized_config.combustion.efficiency.use_turbulence_coupling =
True
949 if ablative_cfg
and ablative_cfg.enabled:
950 runner_abl = optimized_runner_updated.config.ablative_cooling.initial_thickness
951 config_abl = optimized_config.ablative_cooling.initial_thickness
952 if abs(runner_abl - config_abl) > 1e-9:
954 "Post-opt runner ablative thickness mismatch! Config: %.6f, Runner: %.6f",
958 if graphite_cfg
and graphite_cfg.enabled:
959 runner_gra = optimized_runner_updated.config.graphite_insert.initial_thickness
960 config_gra = optimized_config.graphite_insert.initial_thickness
961 if abs(runner_gra - config_gra) > 1e-9:
963 "Post-opt runner graphite thickness mismatch! Config: %.6f, Runner: %.6f",
970 full_time_results_updated = optimized_runner_updated.evaluate_arrays_with_time(
974 track_ablative_geometry=
True,
975 use_coupled_solver=
True,
977 updated_time_results = full_time_results_updated
978 thermal_results[
"max_recession_chamber"] = float(
979 np.max(np.atleast_1d(full_time_results_updated.get(
"recession_chamber", [0.0])))
981 thermal_results[
"max_recession_throat"] = float(
982 np.max(np.atleast_1d(full_time_results_updated.get(
"recession_throat", [0.0])))
985 "Post-optimization max recession (chamber / throat): "
987 thermal_results[
"max_recession_chamber"] * 1000.0,
988 thermal_results[
"max_recession_throat"] * 1000.0,
998 if ablative_cfg
and ablative_cfg.enabled:
999 t_abl = optimized_config.ablative_cooling.initial_thickness
1000 required_abl = thermal_results[
"max_recession_chamber"] * margin_factor
1001 ablative_ok = t_abl >= required_abl
1003 if graphite_cfg
and graphite_cfg.enabled:
1004 t_gra = optimized_config.graphite_insert.initial_thickness
1005 required_gra = thermal_results[
"max_recession_throat"] * margin_factor
1006 graphite_ok = t_gra >= required_gra
1008 thermal_results[
"ablative_adequate"] = bool(ablative_ok)
1009 thermal_results[
"graphite_adequate"] = bool(graphite_ok)
1010 thermal_results[
"thermal_protection_valid"] = bool(ablative_ok
and graphite_ok)
1011 except Exception
as e:
1012 layer3_logger.error(
"Re-evaluation after optimization failed: %r", e)
1015 layer3_logger.error(traceback.format_exc())
1017 "Layer 3: Burn Analysis",
1019 f
"⚠️ Re-evaluation failed: {e}, using original results",
1023 "Completed | Ablative {:.2f} mm, Graphite {:.2f} mm, "
1024 "Max recession chamber {:.2f} mm, throat {:.2f} mm "
1025 "(valid: {}) (see {} for detailed log)".
format(
1026 (optimized_config.ablative_cooling.initial_thickness * 1000)
1027 if ablative_cfg
and ablative_cfg.enabled
1029 (optimized_config.graphite_insert.initial_thickness * 1000)
1030 if graphite_cfg
and graphite_cfg.enabled
1032 (thermal_results.get(
"max_recession_chamber")
or 0.0) * 1000,
1033 (thermal_results.get(
"max_recession_throat")
or 0.0) * 1000,
1034 thermal_results.get(
"thermal_protection_valid",
False),
1038 log_status(
"Layer 3", status_msg)
1042 if ablative_cfg
and ablative_cfg.enabled:
1043 if "optimized_ablative_thickness" in thermal_results:
1044 optimized_config.ablative_cooling.initial_thickness = thermal_results[
"optimized_ablative_thickness"]
1046 "Final optimized ablative thickness in config: %.3f mm",
1047 optimized_config.ablative_cooling.initial_thickness * 1000.0,
1050 if graphite_cfg
and graphite_cfg.enabled:
1051 if "optimized_graphite_thickness" in thermal_results:
1052 optimized_config.graphite_insert.initial_thickness = thermal_results[
"optimized_graphite_thickness"]
1054 "Final optimized graphite thickness in config: %.3f mm",
1055 optimized_config.graphite_insert.initial_thickness * 1000.0,
1058 layer3_logger.info(
"Layer 3 optimization complete. Log saved to: %s", log_file_path)
1060 layer3_logger.handlers.clear()
1062 return optimized_config, updated_time_results, thermal_results