347 thrust_tolerance: float = 0.1,
348 target_burn_time: float |
None =
None,
349 report_every_n: int = 1,
350 session: UserSession = Depends(get_session),
351 _:
None = DesignCheckout,
353 """Run Layer 1 optimization with Server-Sent Events for progress updates.
355 Note: max_iterations is hardcoded in the optimizer for consistent robust convergence.
357 Returns a stream of progress updates in SSE format.
359 if not session.app_state.has_config():
362 detail=
"No config loaded. Upload a config file first."
365 if not session.app_state.runner:
368 detail=
"Runner not initialized. Please check config."
372 if session.app_state.config.design_requirements
is None:
375 detail=
"No design requirements set. Save design requirements first."
379 if session.optimizer.optimization_status[
"running"]:
382 detail=
"Optimization already running. Please wait for it to complete."
385 async def event_generator():
386 """Generate SSE events for optimization progress."""
389 with session.optimizer.stop_event_lock:
390 session.optimizer.stop_event = threading.Event()
392 session.optimizer.optimization_status[
"running"] =
True
393 session.optimizer.optimization_status[
"progress"] = 0.0
394 session.optimizer.optimization_status[
"stage"] =
"Initializing"
395 session.optimizer.optimization_status[
"message"] =
"Starting Layer 1 optimization..."
396 session.optimizer.optimization_status[
"results"] =
None
397 session.optimizer.optimization_status[
"error"] =
None
400 yield f
"data: {safe_json_dumps({'type': 'status', 'progress': 0.0, 'stage': 'Initializing', 'message': 'Starting optimization...'})}\n\n"
404 requirements = session.app_state.config.design_requirements.model_dump()
406 if "report_every_n" not in requirements:
407 requirements[
"report_every_n"] = report_every_n
408 burn_time = target_burn_time
or requirements.get(
"target_burn_time", 10.0)
412 "mode":
"optimizer_controlled",
413 "max_lox_pressure_psi": requirements.get(
"max_lox_tank_pressure_psi", 700.0),
414 "max_fuel_pressure_psi": requirements.get(
"max_fuel_tank_pressure_psi", 850.0),
415 "target_burn_time": burn_time,
421 "thrust": thrust_tolerance,
426 objective_history = []
427 objective_history_lock = threading.Lock()
428 last_sent_objective_count = 0
431 def update_progress(stage: str, progress: float, message: str):
432 session.optimizer.optimization_status[
"progress"] = progress
433 session.optimizer.optimization_status[
"stage"] = stage
434 session.optimizer.optimization_status[
"message"] = message
437 def objective_callback(iteration: int, objective: float, best_objective: float):
438 with objective_history_lock:
439 objective_history.append({
440 "iteration": int(iteration),
441 "objective": float(objective),
442 "best_objective": float(best_objective),
448 import concurrent.futures
450 def run_optimization():
451 return run_layer1_optimization(
452 config_obj=session.app_state.config,
453 runner=session.app_state.runner,
454 requirements=requirements,
455 target_burn_time=burn_time,
456 tolerances=tolerances,
457 pressure_config=pressure_config,
458 update_progress=update_progress,
459 log_status=
lambda stage, msg:
None,
460 objective_callback=objective_callback,
461 stop_event=session.optimizer.stop_event,
465 loop = asyncio.get_event_loop()
466 async with JOB_SEMAPHORE:
467 with concurrent.futures.ThreadPoolExecutor()
as pool:
469 future = loop.run_in_executor(pool, run_optimization)
471 while not future.done():
475 'progress': session.optimizer.optimization_status[
'progress'],
476 'stage': session.optimizer.optimization_status[
'stage'],
477 'message': session.optimizer.optimization_status[
'message']
479 yield f
"data: {safe_json_dumps(progress_data)}\n\n"
482 with objective_history_lock:
483 if len(objective_history) > last_sent_objective_count:
485 new_entries = objective_history[last_sent_objective_count:]
486 last_sent_objective_count = len(objective_history)
491 'objective_history': new_entries,
492 'total_count': last_sent_objective_count,
494 yield f
"data: {safe_json_dumps(objective_data)}\n\n"
496 await asyncio.sleep(0.5)
500 optimized_config, results = future.result()
502 with session.optimizer.stop_event_lock:
503 if session.optimizer.stop_event
and session.optimizer.stop_event.is_set():
504 session.optimizer.optimization_status[
"running"] =
False
505 session.optimizer.optimization_status[
"progress"] = 0.0
506 session.optimizer.optimization_status[
"stage"] =
"Stopped"
507 session.optimizer.optimization_status[
"message"] =
"Optimization stopped by user"
508 yield f
"data: {safe_json_dumps({'type': 'error', 'error': 'Optimization stopped by user'})}\n\n"
510 except Exception
as e:
512 with session.optimizer.stop_event_lock:
513 if session.optimizer.stop_event
and session.optimizer.stop_event.is_set():
514 session.optimizer.optimization_status[
"running"] =
False
515 session.optimizer.optimization_status[
"progress"] = 0.0
516 session.optimizer.optimization_status[
"stage"] =
"Stopped"
517 session.optimizer.optimization_status[
"message"] =
"Optimization stopped by user"
518 yield f
"data: {safe_json_dumps({'type': 'error', 'error': 'Optimization stopped by user'})}\n\n"
529 session.app_state.set_config(optimized_config)
530 persisted_config = optimized_config
532 persisted_config = session.app_state.config
534 "⚠ Layer 1 result did NOT validate — keeping the previous geometry as the "
535 f
"seed rather than saving it. Reasons: {invalid_reasons}"
539 performance = results.get(
"performance", {})
541 exit_pressure_targeting = results.get(
"exit_pressure_targeting", {})
542 if exit_pressure_targeting.get(
"target_P_exit")
is not None:
543 performance[
"target_P_exit"] = exit_pressure_targeting[
"target_P_exit"]
549 "performance": performance,
552 "validation": results.get(
"validation")
or {
553 "valid": design_valid,
554 "failure_reasons": invalid_reasons,
556 "design_saved": design_valid,
557 "geometry": results.get(
"optimized_parameters", {}),
558 "objective_history": objective_history,
559 "iteration_history": results.get(
"iteration_history", []),
560 "convergence_info": results.get(
"convergence_info", {}),
561 "config": config_to_dict(persisted_config),
562 "config_yaml": yaml.dump(config_to_dict(persisted_config), default_flow_style=
False),
564 session.optimizer.optimization_status[
"results"] = results_dict
565 session.optimizer.optimization_status[
"progress"] = 1.0
566 session.optimizer.optimization_status[
"stage"] =
"Complete"
567 session.optimizer.optimization_status[
"message"] =
"Optimization completed successfully"
570 yield f
"data: {safe_json_dumps({'type': 'complete', 'results': results_dict})}\n\n"
572 except Exception
as e:
574 error_str = str(e).lower()
575 if "stopped by user" in error_str
or "optimization stopped" in error_str:
576 error_msg =
"Optimization stopped by user"
577 session.optimizer.optimization_status[
"error"] =
None
578 session.optimizer.optimization_status[
"message"] = error_msg
579 session.optimizer.optimization_status[
"stage"] =
"Stopped"
581 yield f
"data: {safe_json_dumps({'type': 'error', 'error': error_msg})}\n\n"
583 error_msg = f
"Optimization failed: {str(e)}"
584 error_trace = traceback.format_exc()
585 session.optimizer.optimization_status[
"error"] = error_msg
586 session.optimizer.optimization_status[
"message"] = error_msg
588 yield f
"data: {safe_json_dumps({'type': 'error', 'error': error_msg, 'traceback': error_trace})}\n\n"
591 session.optimizer.optimization_status[
"running"] =
False
593 with session.optimizer.stop_event_lock:
594 session.optimizer.stop_event =
None
596 return StreamingResponse(
598 media_type=
"text/event-stream",
600 "Cache-Control":
"no-cache",
601 "Connection":
"keep-alive",
602 "X-Accel-Buffering":
"no",
607@router.get("/layer2")
609 max_iterations: int = 20,
610 save_plots: bool =
False,
613 de_n_time_points: int = 25,
614 session: UserSession = Depends(get_session),
615 _:
None = DesignCheckout,
617 """Run Layer 2 optimization with Server-Sent Events for progress updates."""
618 if not session.app_state.has_config():
621 detail=
"No config loaded. Run Layer 1 or upload a config first."
624 if not session.app_state.runner:
627 detail=
"Runner not initialized."
631 if session.app_state.config.design_requirements
is None:
634 detail=
"No design requirements set."
638 if session.optimizer.layer2_status[
"running"]:
641 detail=
"Layer 2 optimization already running."
644 async def event_generator():
646 with session.optimizer.stop_event_lock:
647 session.optimizer.stop_event = threading.Event()
649 session.optimizer.layer2_status.update({
652 "stage":
"Initializing",
653 "message":
"Starting Layer 2 optimization...",
658 yield f
"data: {safe_json_dumps({'type': 'status', 'progress': 0.0, 'stage': 'Initializing', 'message': 'Starting optimization...'})}\n\n"
661 reqs = session.app_state.config.design_requirements
662 burn_time = reqs.target_burn_time
666 initial_lox_p = 500.0 * 6894.76
667 if session.app_state.config.lox_tank
and session.app_state.config.lox_tank.initial_pressure_psi:
668 initial_lox_p = session.app_state.config.lox_tank.initial_pressure_psi * 6894.76
670 initial_fuel_p = 500.0 * 6894.76
671 if session.app_state.config.fuel_tank
and session.app_state.config.fuel_tank.initial_pressure_psi:
672 initial_fuel_p = session.app_state.config.fuel_tank.initial_pressure_psi * 6894.76
675 target_thrust = reqs.target_thrust
676 target_apogee = reqs.target_apogee
or 3048.0
679 rocket_dry_mass_kg = 50.0
680 if session.app_state.config.rocket:
681 r = session.app_state.config.rocket
682 if r.airframe_mass
is not None and r.engine_mass
is not None:
684 rocket_dry_mass_kg = (
685 (r.airframe_mass
or 0) +
686 (r.engine_mass
or 0) +
687 (r.lox_tank_structure_mass
or 0) +
688 (r.fuel_tank_structure_mass
or 0) +
689 (r.copv_dry_mass
or 0)
691 elif r.propulsion_dry_mass
is not None:
692 rocket_dry_mass_kg = (r.airframe_mass
or 0) + r.propulsion_dry_mass
695 lox_capacity = reqs.lox_tank_capacity_kg
or 25.0
696 fuel_capacity = reqs.fuel_tank_capacity_kg
or 15.0
698 objective_history = []
699 objective_history_lock = threading.Lock()
703 best_pressure_curves = {
"time":
None,
"lox":
None,
"fuel":
None,
"copv_pressure":
None,
"copv_time":
None}
704 pressure_curves_lock = threading.Lock()
705 pressure_curves_updated = threading.Event()
707 def update_progress(stage: str, progress: float, message: str):
708 session.optimizer.layer2_status.update({
"progress": progress,
"stage": stage,
"message": message})
710 def objective_callback(iteration: int, objective: float, best_objective: float):
711 with objective_history_lock:
712 objective_history.append({
713 "iteration": int(iteration),
714 "objective": float(objective),
715 "best_objective": float(best_objective),
718 def pressure_curve_callback(time_arr, P_lox, P_fuel, copv_pressure=None, copv_time=None):
719 """Called when a new best solution is found - stream pressure curves to UI."""
720 with pressure_curves_lock:
721 best_pressure_curves[
"time"] = time_arr
722 best_pressure_curves[
"lox"] = P_lox
723 best_pressure_curves[
"fuel"] = P_fuel
724 best_pressure_curves[
"copv_pressure"] = copv_pressure
725 best_pressure_curves[
"copv_time"] = copv_time
726 pressure_curves_updated.set()
730 import concurrent.futures
733 current_stop_event = session.optimizer.stop_event
736 return run_layer2_pressure(
737 optimized_config=session.app_state.config,
738 initial_lox_pressure_pa=initial_lox_p,
739 initial_fuel_pressure_pa=initial_fuel_p,
740 peak_thrust=target_thrust,
741 target_apogee_m=target_apogee,
742 rocket_dry_mass_kg=rocket_dry_mass_kg,
743 max_lox_tank_capacity_kg=lox_capacity,
744 max_fuel_tank_capacity_kg=fuel_capacity,
745 target_burn_time=burn_time,
747 update_progress=update_progress,
748 objective_callback=objective_callback,
749 pressure_curve_callback=pressure_curve_callback,
750 max_iterations=max_iterations,
751 save_evaluation_plots=save_plots,
752 stop_event=current_stop_event,
753 de_maxiter=de_maxiter,
754 de_popsize=de_popsize,
755 de_n_time_points=de_n_time_points,
762 loop = asyncio.get_event_loop()
763 async with JOB_SEMAPHORE:
764 with concurrent.futures.ThreadPoolExecutor()
as pool:
765 future = loop.run_in_executor(pool, run_opt)
767 while not future.done():
768 yield f
"data: {safe_json_dumps({'type': 'progress', 'progress': session.optimizer.layer2_status['progress'], 'stage': session.optimizer.layer2_status['stage'], 'message': session.optimizer.layer2_status['message']})}\n\n"
770 with objective_history_lock:
771 if len(objective_history) > last_sent_count:
772 new_entries = objective_history[last_sent_count:]
773 last_sent_count = len(objective_history)
774 yield f
"data: {safe_json_dumps({'type': 'objective', 'objective_history': new_entries, 'total_count': last_sent_count})}\n\n"
777 if pressure_curves_updated.is_set():
778 with pressure_curves_lock:
779 if best_pressure_curves[
"time"]
is not None:
782 'type':
'pressure_curves',
783 'time_array': best_pressure_curves[
"time"],
784 'lox_pressure': best_pressure_curves[
"lox"],
785 'fuel_pressure': best_pressure_curves[
"fuel"],
786 'copv_pressure': best_pressure_curves[
"copv_pressure"],
787 'copv_time': best_pressure_curves[
"copv_time"],
789 yield f
"data: {safe_json_dumps(curves_data)}\n\n"
790 pressure_curves_updated.clear()
793 await asyncio.sleep(0.5)
795 optimized_config, time_array, P_lox, P_fuel, summary, success = future.result()
798 stopped_by_user =
False
799 with session.optimizer.stop_event_lock:
800 if session.optimizer.stop_event
and session.optimizer.stop_event.is_set():
801 stopped_by_user =
True
804 session.app_state.set_config(optimized_config)
807 "performance": summary,
809 "objective_history": objective_history,
810 "time_array": time_array,
811 "lox_pressure": P_lox,
812 "fuel_pressure": P_fuel,
813 "config": config_to_dict(optimized_config),
814 "config_yaml": yaml.dump(config_to_dict(optimized_config), default_flow_style=
False),
817 session.optimizer.layer2_status.update({
818 "results": results_dict,
820 "stage":
"Complete" if not stopped_by_user
else "Stopped",
821 "message":
"Layer 2 optimization complete" if not stopped_by_user
else "Stopped by user - using best solution found",
824 yield f
"data: {safe_json_dumps({'type': 'complete', 'results': results_dict, 'stopped_by_user': stopped_by_user})}\n\n"
826 except Exception
as e:
827 session.optimizer.layer2_status[
"error"] = str(e)
828 yield f
"data: {safe_json_dumps({'type': 'error', 'error': str(e), 'traceback': traceback.format_exc()})}\n\n"
830 session.optimizer.layer2_status[
"running"] =
False
834 with session.optimizer.stop_event_lock:
835 if session.optimizer.stop_event
is not None:
836 session.optimizer.stop_event.set()
837 session.optimizer.stop_event =
None
839 return StreamingResponse(
841 media_type=
"text/event-stream",
842 headers={
"Cache-Control":
"no-cache",
"Connection":
"keep-alive",
"X-Accel-Buffering":
"no"}
985 max_iterations: int = 20,
986 save_plots: bool =
False,
987 optimization_method: str =
"gradient",
988 session: UserSession = Depends(get_session),
989 _:
None = DesignCheckout,
991 """Run Layer 3 thermal protection optimization with Server-Sent Events for progress updates.
994 optimization_method: "gradient" (fast, ~5-15 evals), "cma" (thorough, ~60-80 evals), or "de" (fallback)
996 if not session.app_state.has_config():
999 detail=
"No config loaded. Run Layer 2 or upload a config first."
1002 if not session.app_state.runner:
1003 raise HTTPException(
1005 detail=
"Runner not initialized."
1009 if not hasattr(session.app_state.config,
'pressure_curves')
or session.app_state.config.pressure_curves
is None:
1010 raise HTTPException(
1012 detail=
"Config must have pressure_curves from Layer 2 optimization."
1016 if session.optimizer.layer3_status[
"running"]:
1017 raise HTTPException(
1019 detail=
"Layer 3 optimization already running."
1022 async def event_generator():
1024 with session.optimizer.stop_event_lock:
1025 session.optimizer.stop_event = threading.Event()
1027 session.optimizer.layer3_status.update({
1030 "stage":
"Initializing",
1031 "message":
"Starting Layer 3 thermal protection optimization...",
1036 yield f
"data: {safe_json_dumps({'type': 'status', 'progress': 0.0, 'stage': 'Initializing', 'message': 'Starting optimization...'})}\n\n"
1040 pressure_curves = session.app_state.config.pressure_curves
1041 burn_time = pressure_curves.target_burn_time_s
1042 n_points = pressure_curves.n_points
or 200
1047 time_array = np.linspace(0, burn_time, n_points)
1050 lox_segments_dicts = [
1052 "length_ratio": seg.length_ratio,
1054 "start_pressure": seg.start_pressure_pa,
1055 "end_pressure": seg.end_pressure_pa,
1056 "k": seg.k
if seg.k
is not None else 0.3,
1058 for seg
in pressure_curves.lox_segments
1061 fuel_segments_dicts = [
1063 "length_ratio": seg.length_ratio,
1065 "start_pressure": seg.start_pressure_pa,
1066 "end_pressure": seg.end_pressure_pa,
1067 "k": seg.k
if seg.k
is not None else 0.3,
1069 for seg
in pressure_curves.fuel_segments
1073 P_tank_O_array = generate_pressure_curve_from_segments(
1079 P_tank_F_array = generate_pressure_curve_from_segments(
1080 fuel_segments_dicts,
1086 baseline_results = session.app_state.runner.evaluate_arrays_with_time(
1090 track_ablative_geometry=
True,
1091 use_coupled_solver=
True,
1094 objective_history = []
1095 objective_history_lock = threading.Lock()
1099 best_pressure_curves = {
"time":
None,
"lox":
None,
"fuel":
None,
"copv_pressure":
None,
"copv_time":
None}
1100 pressure_curves_lock = threading.Lock()
1101 pressure_curves_updated = threading.Event()
1103 def update_progress(stage: str, progress: float, message: str):
1104 session.optimizer.layer3_status.update({
"progress": progress,
"stage": stage,
"message": message})
1106 def log_status(stage: str, message: str):
1110 def objective_callback(iteration: int, objective: float, best_objective: float):
1111 with objective_history_lock:
1112 objective_history.append({
1113 "iteration": int(iteration),
1114 "objective": float(objective),
1115 "best_objective": float(best_objective),
1118 import concurrent.futures
1120 current_stop_event = session.optimizer.stop_event
1123 return run_layer3_thermal_protection(
1124 optimized_config=session.app_state.config,
1125 time_array=time_array,
1126 P_tank_O_array=P_tank_O_array,
1127 P_tank_F_array=P_tank_F_array,
1128 full_time_results=baseline_results,
1129 n_time_points=n_points,
1130 update_progress=update_progress,
1131 log_status=log_status,
1132 objective_callback=objective_callback,
1133 optimization_method=optimization_method,
1136 loop = asyncio.get_event_loop()
1137 async with JOB_SEMAPHORE:
1138 with concurrent.futures.ThreadPoolExecutor()
as pool:
1139 future = loop.run_in_executor(pool, run_opt)
1141 while not future.done():
1142 yield f
"data: {safe_json_dumps({'type': 'progress', 'progress': session.optimizer.layer3_status['progress'], 'stage': session.optimizer.layer3_status['stage'], 'message': session.optimizer.layer3_status['message']})}\n\n"
1144 with objective_history_lock:
1145 if len(objective_history) > last_sent_count:
1146 new_entries = objective_history[last_sent_count:]
1147 last_sent_count = len(objective_history)
1148 yield f
"data: {safe_json_dumps({'type': 'objective', 'objective_history': new_entries, 'total_count': last_sent_count})}\n\n"
1151 if not pressure_curves_updated.is_set():
1152 with pressure_curves_lock:
1153 best_pressure_curves[
"time"] = time_array
1154 best_pressure_curves[
"lox"] = P_tank_O_array
1155 best_pressure_curves[
"fuel"] = P_tank_F_array
1156 pressure_curves_updated.set()
1158 if pressure_curves_updated.is_set():
1159 with pressure_curves_lock:
1160 if best_pressure_curves[
"time"]
is not None:
1162 'type':
'pressure_curves',
1163 'time_array': best_pressure_curves[
"time"],
1164 'lox_pressure': best_pressure_curves[
"lox"],
1165 'fuel_pressure': best_pressure_curves[
"fuel"],
1166 'copv_pressure': best_pressure_curves[
"copv_pressure"],
1167 'copv_time': best_pressure_curves[
"copv_time"],
1169 yield f
"data: {safe_json_dumps(curves_data)}\n\n"
1170 pressure_curves_updated.clear()
1172 await asyncio.sleep(0.5)
1174 optimized_config, updated_time_results, thermal_results = future.result()
1177 stopped_by_user =
False
1178 with session.optimizer.stop_event_lock:
1179 if session.optimizer.stop_event
and session.optimizer.stop_event.is_set():
1180 stopped_by_user =
True
1183 session.app_state.set_config(optimized_config)
1188 thrust_array = np.atleast_1d(updated_time_results.get(
"F", []))
1189 isp_array = np.atleast_1d(updated_time_results.get(
"Isp", []))
1190 mr_array = np.atleast_1d(updated_time_results.get(
"MR", []))
1191 pc_array = np.atleast_1d(updated_time_results.get(
"Pc", []))
1194 safe_time_array = time_array
1195 if len(thrust_array) != len(time_array):
1196 if len(thrust_array) > 0:
1198 safe_time_array = np.linspace(0, burn_time, len(thrust_array))
1200 safe_time_array = np.array([])
1203 total_impulse =
None
1204 if len(thrust_array) > 0
and len(safe_time_array) == len(thrust_array):
1205 if hasattr(np,
'trapezoid'):
1206 total_impulse = float(np.trapezoid(thrust_array, safe_time_array))
1208 total_impulse = float(np.trapz(thrust_array, safe_time_array))
1210 avg_thrust = float(np.mean(thrust_array))
if len(thrust_array) > 0
else None
1211 peak_thrust = float(np.max(thrust_array))
if len(thrust_array) > 0
else None
1212 avg_isp = float(np.mean(isp_array))
if len(isp_array) > 0
else None
1213 avg_of = float(np.mean(mr_array))
if len(mr_array) > 0
else None
1214 min_of = float(np.min(mr_array))
if len(mr_array) > 0
else None
1215 max_of = float(np.max(mr_array))
if len(mr_array) > 0
else None
1216 avg_pc = float(np.mean(pc_array))
if len(pc_array) > 0
else None
1220 "optimized_ablative_thickness": thermal_results.get(
"optimized_ablative_thickness"),
1221 "optimized_graphite_thickness": thermal_results.get(
"optimized_graphite_thickness"),
1222 "max_recession_chamber": thermal_results.get(
"max_recession_chamber"),
1223 "max_recession_throat": thermal_results.get(
"max_recession_throat"),
1224 "thermal_protection_valid": thermal_results.get(
"thermal_protection_valid"),
1225 "ablative_adequate": thermal_results.get(
"ablative_adequate"),
1226 "graphite_adequate": thermal_results.get(
"graphite_adequate"),
1229 "total_impulse_Ns": total_impulse,
1230 "avg_thrust_N": avg_thrust,
1231 "peak_thrust_N": peak_thrust,
1232 "avg_isp_s": avg_isp,
1233 "avg_chamber_pressure_Pa": avg_pc,
1234 "burn_time_s": burn_time,
1237 "avg_of_ratio": avg_of,
1238 "min_of_ratio": min_of,
1239 "max_of_ratio": max_of,
1242 "min_stability_margin": float(np.min(updated_time_results.get(
"chugging_stability_margin", [0])))
if "chugging_stability_margin" in updated_time_results
else None,
1244 except Exception
as e:
1246 print(f
"Error calculating Layer 3 summary: {e}")
1247 traceback.print_exc()
1250 "optimized_ablative_thickness": thermal_results.get(
"optimized_ablative_thickness"),
1251 "optimized_graphite_thickness": thermal_results.get(
"optimized_graphite_thickness"),
1252 "max_recession_chamber": thermal_results.get(
"max_recession_chamber"),
1253 "max_recession_throat": thermal_results.get(
"max_recession_throat"),
1254 "thermal_protection_valid": thermal_results.get(
"thermal_protection_valid"),
1255 "ablative_adequate": thermal_results.get(
"ablative_adequate"),
1256 "graphite_adequate": thermal_results.get(
"graphite_adequate"),
1257 "error": f
"Summary calculation failed: {str(e)}"
1261 "performance": updated_time_results,
1263 "objective_history": objective_history,
1264 "time_array": time_array,
1265 "lox_pressure": P_tank_O_array,
1266 "fuel_pressure": P_tank_F_array,
1267 "config": config_to_dict(optimized_config),
1268 "config_yaml": yaml.dump(config_to_dict(optimized_config), default_flow_style=
False),
1271 session.optimizer.layer3_status.update({
1272 "results": results_dict,
1274 "stage":
"Complete" if not stopped_by_user
else "Stopped",
1275 "message":
"Layer 3 thermal protection optimization complete" if not stopped_by_user
else "Stopped by user - using best solution found",
1278 yield f
"data: {safe_json_dumps({'type': 'complete', 'results': results_dict, 'stopped_by_user': stopped_by_user})}\n\n"
1280 except Exception
as e:
1281 session.optimizer.layer3_status[
"error"] = str(e)
1282 yield f
"data: {safe_json_dumps({'type': 'error', 'error': str(e), 'traceback': traceback.format_exc()})}\n\n"
1284 session.optimizer.layer3_status[
"running"] =
False
1285 with session.optimizer.stop_event_lock:
1286 if session.optimizer.stop_event
is not None:
1287 session.optimizer.stop_event.set()
1288 session.optimizer.stop_event =
None
1290 return StreamingResponse(
1292 media_type=
"text/event-stream",
1293 headers={
"Cache-Control":
"no-cache",
"Connection":
"keep-alive",
"X-Accel-Buffering":
"no"}