323 if session.controller.controller
is None:
324 raise HTTPException(status_code=400, detail=
"Controller not initialized. Call /init first.")
329 P_copv=request.meas.P_copv,
330 P_reg=request.meas.P_reg,
331 P_u_fuel=request.meas.P_u_fuel,
332 P_u_ox=request.meas.P_u_ox,
333 P_d_fuel=request.meas.P_d_fuel,
334 P_d_ox=request.meas.P_d_ox,
335 timestamp=request.meas.timestamp,
341 theta=request.nav.theta,
342 mass_estimate=request.nav.mass_estimate,
346 command_type=
CommandType(request.cmd.command_type),
347 thrust_desired=request.cmd.thrust_desired,
348 altitude_goal=request.cmd.altitude_goal,
352 actuation_cmd, diagnostics = session.controller.controller.step(measurement, nav_state, command)
355 def safe_float_val(val, default=0.0):
357 if isinstance(val, (int, float)):
359 return fval
if not (np.isnan(fval)
or np.isinf(fval))
else default
360 elif isinstance(val, np.number):
362 return fval
if not (np.isnan(fval)
or np.isinf(fval))
else default
363 elif isinstance(val, np.ndarray):
371 diagnostics_dict = {}
373 diagnostics_dict[
"F_ref"] = safe_float_val(diagnostics.get(
"F_ref", 0.0))
374 diagnostics_dict[
"MR_ref"] = safe_float_val(diagnostics.get(
"MR_ref", 0.0))
375 diagnostics_dict[
"F_estimated"] = safe_float_val(diagnostics.get(
"F_hat", 0.0))
376 diagnostics_dict[
"MR_estimated"] = safe_float_val(diagnostics.get(
"MR_hat", 0.0))
377 diagnostics_dict[
"P_ch"] = safe_float_val(diagnostics.get(
"P_ch", 0.0))
378 diagnostics_dict[
"cost"] = 0.0
379 diagnostics_dict[
"solver_iters"] = 0
380 diagnostics_dict[
"safety_filtered"] =
False
381 diagnostics_dict[
"cutoff_active"] =
False
384 solution = diagnostics.get(
"solution")
386 if hasattr(solution,
'objective'):
388 obj_val = solution.objective
389 if isinstance(obj_val, (int, float))
and not isinstance(obj_val, np.ndarray):
390 diagnostics_dict[
"cost"] = safe_float_val(obj_val)
393 if hasattr(solution,
'iterations'):
395 iter_val = solution.iterations
396 if isinstance(iter_val, (int, np.integer)):
397 diagnostics_dict[
"solver_iters"] = int(iter_val)
400 except Exception
as e:
403 "F_ref": 0.0,
"MR_ref": 0.0,
"F_estimated": 0.0,
"MR_estimated": 0.0,
404 "P_ch": 0.0,
"cost": 0.0,
"solver_iters": 0,
"safety_filtered":
False,
"cutoff_active":
False
409 "duty_F": float(getattr(actuation_cmd,
'duty_F', 0.0)),
410 "duty_O": float(getattr(actuation_cmd,
'duty_O', 0.0)),
411 "u_F_onoff": bool(getattr(actuation_cmd,
'u_F_onoff',
False)),
412 "u_O_onoff": bool(getattr(actuation_cmd,
'u_O_onoff',
False)),
417 "actuation": actuation_dict,
418 "diagnostics": diagnostics_dict,
420 except Exception
as e:
421 raise HTTPException(status_code=500, detail=str(e))
424@router.post("/simulate")
436 if session.controller.controller
is None:
438 cfg = get_default_config()
439 if not session.app_state.has_config():
442 detail=
"No engine config loaded. Load a config first."
444 engine_config = session.app_state.config
446 except Exception
as e:
447 raise HTTPException(status_code=500, detail=f
"Failed to initialize controller: {e}")
451 n_steps = int(request.duration / request.dt)
452 time_array = np.linspace(0, request.duration, n_steps + 1)
456 P_copv=request.initial_meas.P_copv,
457 P_reg=request.initial_meas.P_reg,
458 P_u_fuel=request.initial_meas.P_u_fuel,
459 P_u_ox=request.initial_meas.P_u_ox,
460 P_d_fuel=request.initial_meas.P_d_fuel,
461 P_d_ox=request.initial_meas.P_d_ox,
465 h=request.initial_nav.h,
466 vz=request.initial_nav.vz,
467 theta=request.initial_nav.theta,
468 mass_estimate=request.initial_nav.mass_estimate,
472 if request.cmd.command_type ==
"thrust_desired" and request.thrust_curve:
474 if request.time_array:
475 thrust_schedule = list(zip(request.time_array, request.thrust_curve))
478 thrust_schedule = list(zip(time_array.tolist(), request.thrust_curve))
480 command_type=CommandType.THRUST_DESIRED,
481 thrust_desired=thrust_schedule,
485 command_type=
CommandType(request.cmd.command_type),
486 thrust_desired=request.cmd.thrust_desired,
487 altitude_goal=request.cmd.altitude_goal,
495 dynamics_params = DynamicsParams.from_config(session.controller.controller.cfg)
498 x = session.controller.controller._build_state(meas)
501 if len(x) != N_STATE:
502 raise ValueError(f
"Initial state vector has wrong size: expected {N_STATE}, got {len(x)}. "
529 "value_function": [],
530 "control_effort": [],
539 for i, t
in enumerate(time_array):
541 if request.cmd.command_type ==
"thrust_desired" and request.thrust_curve:
543 if request.time_array
and len(request.time_array) == len(request.thrust_curve):
545 thrust_val = np.interp(t, request.time_array, request.thrust_curve)
547 command_type=CommandType.THRUST_DESIRED,
548 thrust_desired=float(thrust_val),
552 actuation, diagnostics = session.controller.controller.step(meas, nav, cmd)
555 solution = diagnostics.get(
"solution")
556 value_func = solution.objective
if solution
and hasattr(solution,
'objective')
else 0.0
559 u_relaxed = diagnostics.get(
"u_relaxed", np.array([0.0, 0.0]))
560 if u_prev
is not None:
561 control_effort = float(np.linalg.norm(u_relaxed - u_prev))
563 control_effort = float(np.linalg.norm(u_relaxed))
564 u_prev = u_relaxed.copy()
567 eng_est = diagnostics.get(
"eng_est")
570 x_current = diagnostics.get(
"x", x)
573 results[
"time"].append(float(t))
574 results[
"thrust_ref"].append(diagnostics.get(
"F_ref", 0.0)
if isinstance(diagnostics.get(
"F_ref"), (int, float))
else 0.0)
575 results[
"thrust_actual"].append(diagnostics.get(
"F_hat", 0.0)
if isinstance(diagnostics.get(
"F_hat"), (int, float))
else 0.0)
576 results[
"MR"].append(diagnostics.get(
"MR_hat", 0.0)
if isinstance(diagnostics.get(
"MR_hat"), (int, float))
else 0.0)
577 results[
"P_copv"].append(float(meas.P_copv))
578 results[
"P_reg"].append(float(meas.P_reg))
579 results[
"P_u_fuel"].append(float(meas.P_u_fuel))
580 results[
"P_u_ox"].append(float(meas.P_u_ox))
581 results[
"P_d_fuel"].append(float(meas.P_d_fuel))
582 results[
"P_d_ox"].append(float(meas.P_d_ox))
583 results[
"P_ch"].append(diagnostics.get(
"P_ch", 0.0)
if isinstance(diagnostics.get(
"P_ch"), (int, float))
else 0.0)
584 results[
"duty_F"].append(float(actuation.duty_F))
585 results[
"duty_O"].append(float(actuation.duty_O))
586 results[
"altitude"].append(float(nav.h))
587 results[
"velocity"].append(float(nav.vz))
588 results[
"value_function"].append(value_func)
589 results[
"control_effort"].append(control_effort)
590 results[
"V_u_fuel"].append(float(x_current[IDX_V_U_F]))
591 results[
"V_u_ox"].append(float(x_current[IDX_V_U_O]))
592 results[
"mdot_F"].append(float(eng_est.mdot_F)
if eng_est
and hasattr(eng_est,
'mdot_F')
and np.isfinite(eng_est.mdot_F)
else 0.0)
593 results[
"mdot_O"].append(float(eng_est.mdot_O)
if eng_est
and hasattr(eng_est,
'mdot_O')
and np.isfinite(eng_est.mdot_O)
else 0.0)
600 mdot_F = float(eng_est.mdot_F)
if eng_est
and hasattr(eng_est,
'mdot_F')
and np.isfinite(eng_est.mdot_F)
else 0.0
601 mdot_O = float(eng_est.mdot_O)
if eng_est
and hasattr(eng_est,
'mdot_O')
and np.isfinite(eng_est.mdot_O)
else 0.0
610 duty_F = float(actuation.duty_F)
611 duty_O = float(actuation.duty_O)
618 u_effective = np.array([duty_F, duty_O], dtype=np.float64)
625 dt_step = float(request.dt)
if request.dt
is not None else 0.01
628 if len(x) != N_STATE:
629 raise ValueError(f
"State vector has wrong size before step: expected {N_STATE}, got {len(x)}")
632 x_next = dynamics_step(x, u_effective, dt_step, dynamics_params, mdot_F, mdot_O)
635 if len(x_next) != N_STATE:
636 raise ValueError(f
"State vector has wrong size after step: expected {N_STATE}, got {len(x_next)}")
639 meas.P_copv = x_next[IDX_P_COPV]
640 meas.P_reg = x_next[IDX_P_REG]
641 meas.P_u_fuel = x_next[IDX_P_U_F]
642 meas.P_u_ox = x_next[IDX_P_U_O]
643 meas.P_d_fuel = x_next[IDX_P_D_F]
644 meas.P_d_ox = x_next[IDX_P_D_O]
647 session.controller.controller.V_u_F = x_next[IDX_V_U_F]
648 session.controller.controller.V_u_O = x_next[IDX_V_U_O]
652 F_actual = diagnostics.get(
"F_hat", 0.0)
653 if isinstance(F_actual, (int, float))
and F_actual > 0:
656 mass = nav.mass_estimate
or 100.0
658 mdot_total = (eng_est.mdot_F
or 0.0) + (eng_est.mdot_O
or 0.0)
659 mass -= mdot_total * dt_step
660 nav.mass_estimate =
max(mass, 50.0)
665 acceleration = (F_actual / nav.mass_estimate) - 9.81
666 nav.vz += acceleration * dt_step
667 nav.h += nav.vz * dt_step
670 nav.vz -= 9.81 * dt_step
671 nav.h += nav.vz * dt_step
680 except HTTPException:
682 except Exception
as e:
683 _log.exception(
"controller simulation failed")
684 raise HTTPException(status_code=500, detail=f
"Controller simulation failed: {e}")
687@router.post("/reset")
709 dt = float(request.dt)
if request.dt
is not None else 0.01
712 if session.controller.controller
is None:
714 cfg = get_default_config()
715 if not session.app_state.has_config():
718 detail=
"No engine config loaded. Load a config first."
720 engine_config = session.app_state.config
722 except Exception
as e:
723 raise HTTPException(status_code=500, detail=f
"Failed to initialize controller: {e}")
727 time_array = np.array(request.thrust_curve_time)
728 thrust_curve = np.array(request.thrust_curve_values)
731 if len(time_array) == 0:
732 raise HTTPException(status_code=400, detail=
"Time array is empty")
733 if len(thrust_curve) == 0:
734 raise HTTPException(status_code=400, detail=
"Thrust curve is empty")
735 if len(time_array) != len(thrust_curve):
736 raise HTTPException(status_code=400, detail=f
"Time array length ({len(time_array)}) doesn't match thrust curve length ({len(thrust_curve)})")
738 duration = float(time_array[-1])
742 if len(time_array) > 1:
744 dt = float(np.mean(np.diff(time_array)))
745 except (ValueError, TypeError):
747 dt = float(request.dt)
if request.dt
is not None else 0.01
750 if request.initial_meas
is None:
762 P_copv=request.initial_meas.P_copv,
763 P_reg=request.initial_meas.P_reg,
764 P_u_fuel=request.initial_meas.P_u_fuel,
765 P_u_ox=request.initial_meas.P_u_ox,
766 P_d_fuel=request.initial_meas.P_d_fuel,
767 P_d_ox=request.initial_meas.P_d_ox,
770 if request.initial_nav
is None:
771 nav =
NavState(h=0.0, vz=0.0, theta=0.0, mass_estimate=100.0)
774 h=request.initial_nav.h,
775 vz=request.initial_nav.vz,
776 theta=request.initial_nav.theta,
777 mass_estimate=request.initial_nav.mass_estimate,
781 thrust_schedule = list(zip(time_array.tolist(), thrust_curve.tolist()))
783 command_type=CommandType.THRUST_DESIRED,
784 thrust_desired=thrust_schedule,
792 dynamics_params = DynamicsParams.from_config(session.controller.controller.cfg)
795 x = session.controller.controller._build_state(meas)
798 n_steps = len(time_array)
815 "value_function": [],
816 "control_effort": [],
822 "constraint_margins": [],
827 for i, t
in enumerate(time_array):
829 if i < len(thrust_curve):
830 thrust_ref = float(thrust_curve[i])
832 thrust_ref = float(thrust_curve[-1])
if len(thrust_curve) > 0
else 0.0
836 command_type=CommandType.THRUST_DESIRED,
837 thrust_desired=thrust_ref,
841 actuation, diagnostics = session.controller.controller.step(meas, nav, cmd)
844 eng_est = diagnostics.get(
"eng_est")
845 solution = diagnostics.get(
"solution")
846 value_func = solution.objective
if solution
and hasattr(solution,
'objective')
else 0.0
849 u_relaxed = diagnostics.get(
"u_relaxed", np.array([0.0, 0.0]))
850 if u_prev
is not None:
851 control_effort = float(np.linalg.norm(u_relaxed - u_prev))
853 control_effort = float(np.linalg.norm(u_relaxed))
854 u_prev = u_relaxed.copy()
858 w_bar = get_w_bar_array(session.controller.controller.state).tolist()
861 constraint_margins = diagnostics.get(
"constraint_margins", {})
864 results[
"time"].append(float(t))
865 results[
"thrust_ref"].append(thrust_ref)
866 results[
"thrust_actual"].append(float(diagnostics.get(
"F_hat", 0.0)))
867 results[
"MR"].append(float(eng_est.MR)
if eng_est
and eng_est.MR
else 0.0)
868 results[
"P_copv"].append(float(meas.P_copv))
869 results[
"P_reg"].append(float(meas.P_reg))
870 results[
"P_u_fuel"].append(float(meas.P_u_fuel))
871 results[
"P_u_ox"].append(float(meas.P_u_ox))
872 results[
"P_d_fuel"].append(float(meas.P_d_fuel))
873 results[
"P_d_ox"].append(float(meas.P_d_ox))
874 results[
"P_ch"].append(float(eng_est.P_ch)
if eng_est
and eng_est.P_ch
else 0.0)
875 results[
"duty_F"].append(float(actuation.duty_F))
876 results[
"duty_O"].append(float(actuation.duty_O))
877 results[
"altitude"].append(float(nav.h))
878 results[
"velocity"].append(float(nav.vz))
879 results[
"value_function"].append(value_func)
880 results[
"control_effort"].append(control_effort)
881 results[
"V_u_fuel"].append(float(session.controller.controller.V_u_F))
882 results[
"V_u_ox"].append(float(session.controller.controller.V_u_O))
883 results[
"mdot_F"].append(float(eng_est.mdot_F)
if eng_est
and eng_est.mdot_F
else 0.0)
884 results[
"mdot_O"].append(float(eng_est.mdot_O)
if eng_est
and eng_est.mdot_O
else 0.0)
885 results[
"w_bar"].append(w_bar)
886 results[
"constraint_margins"].append(constraint_margins)
890 if i < len(time_array) - 1:
891 dt_step = float(time_array[i+1] - time_array[i])
896 u_relaxed_array = np.array([actuation.duty_F, actuation.duty_O])
899 mdot_F = float(eng_est.mdot_F)
if eng_est
and hasattr(eng_est,
'mdot_F')
and eng_est.mdot_F
is not None else 0.0
900 mdot_O = float(eng_est.mdot_O)
if eng_est
and hasattr(eng_est,
'mdot_O')
and eng_est.mdot_O
is not None else 0.0
902 x_next = dynamics_step(x, u_relaxed_array, dt_step, dynamics_params, mdot_F, mdot_O)
905 meas.P_copv = x_next[IDX_P_COPV]
906 meas.P_reg = x_next[IDX_P_REG]
907 meas.P_u_fuel = x_next[IDX_P_U_F]
908 meas.P_u_ox = x_next[IDX_P_U_O]
909 meas.P_d_fuel = x_next[IDX_P_D_F]
910 meas.P_d_ox = x_next[IDX_P_D_O]
913 session.controller.controller.V_u_F = x_next[IDX_V_U_F]
914 session.controller.controller.V_u_O = x_next[IDX_V_U_O]
917 F_actual = diagnostics.get(
"F_hat", 0.0)
918 if isinstance(F_actual, (int, float))
and F_actual > 0:
919 mass = nav.mass_estimate
or 100.0
921 mdot_total = (eng_est.mdot_F
or 0.0) + (eng_est.mdot_O
or 0.0)
922 mass -= mdot_total * dt_step
923 nav.mass_estimate =
max(mass, 50.0)
925 acceleration = (F_actual / nav.mass_estimate) - 9.81
926 nav.vz += acceleration * dt_step
927 nav.h += nav.vz * dt_step
929 nav.vz -= 9.81 * dt_step
930 nav.h += nav.vz * dt_step
939 except HTTPException:
941 except Exception
as e:
942 _log.exception(
"controller simulation failed")
943 raise HTTPException(status_code=500, detail=f
"Controller simulation failed: {e}")
946@router.post("/simulate-layer2-stream")
948 """Stream controller simulation in real-time using Layer 2 optimized thrust curve."""
950 def safe_json_dumps(obj):
951 """Safely serialize to JSON, handling non-serializable types."""
954 async def event_generator():
957 dt = float(request.dt)
if request.dt
is not None else 0.01
961 cfg = get_default_config()
962 if not session.app_state.has_config():
963 yield f
"data: {safe_json_dumps({'type': 'error', 'error': 'No engine config loaded. Load a config first.'})}\n\n"
965 engine_config = session.app_state.config
966 await run_compute(session, _fresh_controller, session, cfg, engine_config)
967 except Exception
as e:
968 yield f
"data: {safe_json_dumps({'type': 'error', 'error': f'Failed to initialize controller: {e}'})}\n\n"
973 time_array = np.array(request.thrust_curve_time)
974 thrust_curve = np.array(request.thrust_curve_values)
977 if len(time_array) == 0:
978 yield f
"data: {safe_json_dumps({'type': 'error', 'error': 'Time array is empty'})}\n\n"
980 if len(thrust_curve) == 0:
981 yield f
"data: {safe_json_dumps({'type': 'error', 'error': 'Thrust curve is empty'})}\n\n"
983 if len(time_array) != len(thrust_curve):
984 error_msg = f
"Time array length ({len(time_array)}) doesn't match thrust curve length ({len(thrust_curve)})"
985 yield f
"data: {safe_json_dumps({'type': 'error', 'error': error_msg})}\n\n"
988 duration = float(time_array[-1])
989 n_steps = len(time_array)
992 if len(time_array) > 1:
994 dt = float(np.mean(np.diff(time_array)))
995 except (ValueError, TypeError):
996 dt = float(request.dt)
if request.dt
is not None else 0.01
999 yield f
"data: {safe_json_dumps({'type': 'status', 'progress': 0.0, 'stage': 'Initializing', 'message': f'Starting simulation for {n_steps} steps...'})}\n\n"
1002 if request.initial_meas
is None:
1004 P_copv=18.96e6, P_reg=6.89e6,
1005 P_u_fuel=6.89e6, P_u_ox=6.89e6,
1006 P_d_fuel=6.89e6, P_d_ox=6.89e6,
1010 P_copv=request.initial_meas.P_copv,
1011 P_reg=request.initial_meas.P_reg,
1012 P_u_fuel=request.initial_meas.P_u_fuel,
1013 P_u_ox=request.initial_meas.P_u_ox,
1014 P_d_fuel=request.initial_meas.P_d_fuel,
1015 P_d_ox=request.initial_meas.P_d_ox,
1018 if request.initial_nav
is None:
1019 nav =
NavState(h=0.0, vz=0.0, theta=0.0, mass_estimate=100.0)
1022 h=request.initial_nav.h,
1023 vz=request.initial_nav.vz,
1024 theta=request.initial_nav.theta,
1025 mass_estimate=request.initial_nav.mass_estimate,
1032 dynamics_params = DynamicsParams.from_config(session.controller.controller.cfg)
1033 x = session.controller.controller._build_state(meas)
1037 for i, t
in enumerate(time_array):
1038 progress = (i + 1) / n_steps
1041 if i < len(thrust_curve):
1042 thrust_ref = float(thrust_curve[i])
1044 thrust_ref = float(thrust_curve[-1])
if len(thrust_curve) > 0
else 0.0
1047 command_type=CommandType.THRUST_DESIRED,
1048 thrust_desired=thrust_ref,
1052 actuation, diagnostics = await run_compute(session, session.controller.controller.step, meas, nav, cmd)
1055 eng_est = diagnostics.get(
"eng_est")
1056 solution = diagnostics.get(
"solution")
1057 value_func = solution.objective
if solution
and hasattr(solution,
'objective')
else 0.0
1059 u_relaxed = diagnostics.get(
"u_relaxed", np.array([0.0, 0.0]))
1060 if u_prev
is not None:
1061 control_effort = float(np.linalg.norm(u_relaxed - u_prev))
1063 control_effort = float(np.linalg.norm(u_relaxed))
1064 u_prev = u_relaxed.copy()
1068 w_bar = get_w_bar_array(session.controller.controller.state).tolist()
1069 constraint_margins = diagnostics.get(
"constraint_margins", {})
1076 "thrust_actual":
safe_float(diagnostics.get(
"F_hat", 0.0)),
1077 "MR":
safe_float(eng_est.MR
if eng_est
and eng_est.MR
else 0.0),
1084 "P_ch":
safe_float(eng_est.P_ch
if eng_est
and eng_est.P_ch
else 0.0),
1090 "control_effort":
safe_float(control_effort),
1091 "V_u_fuel":
safe_float(session.controller.controller.V_u_F),
1092 "V_u_ox":
safe_float(session.controller.controller.V_u_O),
1093 "mdot_F":
safe_float(eng_est.mdot_F
if eng_est
and eng_est.mdot_F
else 0.0),
1094 "mdot_O":
safe_float(eng_est.mdot_O
if eng_est
and eng_est.mdot_O
else 0.0),
1098 yield f
"data: {safe_json_dumps(data_point)}\n\n"
1101 if (i + 1) % 10 == 0
or i == n_steps - 1:
1102 yield f
"data: {safe_json_dumps({'type': 'progress', 'progress': progress, 'stage': 'Simulating', 'message': f'Step {i+1}/{n_steps}'})}\n\n"
1105 if i < len(time_array) - 1:
1106 dt_step = float(time_array[i+1] - time_array[i])
1110 u_relaxed_array = np.array([actuation.duty_F, actuation.duty_O])
1111 mdot_F = float(eng_est.mdot_F)
if eng_est
and hasattr(eng_est,
'mdot_F')
and eng_est.mdot_F
is not None else 0.0
1112 mdot_O = float(eng_est.mdot_O)
if eng_est
and hasattr(eng_est,
'mdot_O')
and eng_est.mdot_O
is not None else 0.0
1114 x_next = dynamics_step(x, u_relaxed_array, dt_step, dynamics_params, mdot_F, mdot_O)
1117 meas.P_copv = x_next[IDX_P_COPV]
1118 meas.P_reg = x_next[IDX_P_REG]
1119 meas.P_u_fuel = x_next[IDX_P_U_F]
1120 meas.P_u_ox = x_next[IDX_P_U_O]
1121 meas.P_d_fuel = x_next[IDX_P_D_F]
1122 meas.P_d_ox = x_next[IDX_P_D_O]
1124 session.controller.controller.V_u_F = x_next[IDX_V_U_F]
1125 session.controller.controller.V_u_O = x_next[IDX_V_U_O]
1128 F_actual = diagnostics.get(
"F_hat", 0.0)
1129 if isinstance(F_actual, (int, float))
and F_actual > 0:
1130 mass = nav.mass_estimate
or 100.0
1132 mdot_total = (eng_est.mdot_F
or 0.0) + (eng_est.mdot_O
or 0.0)
1133 mass -= mdot_total * dt_step
1134 nav.mass_estimate =
max(mass, 50.0)
1136 acceleration = (F_actual / nav.mass_estimate) - 9.81
1137 nav.vz += acceleration * dt_step
1138 nav.h += nav.vz * dt_step
1140 nav.vz -= 9.81 * dt_step
1141 nav.h += nav.vz * dt_step
1146 await asyncio.sleep(0.01)
1149 yield f
"data: {safe_json_dumps({'type': 'complete', 'progress': 1.0, 'stage': 'Complete', 'message': 'Simulation complete'})}\n\n"
1151 except Exception
as e:
1152 _log.exception(
"controller simulation failed")
1153 error_detail = f
"Controller simulation failed: {e}"
1154 yield f
"data: {safe_json_dumps({'type': 'error', 'error': error_detail})}\n\n"
1156 return StreamingResponse(
1157 gated_stream(event_generator()),
1158 media_type=
"text/event-stream",
1159 headers={
"Cache-Control":
"no-cache",
"Connection":
"keep-alive",
"X-Accel-Buffering":
"no"}
1163@router.post("/simulate-from-config")
1176 if request.config_path:
1180 if not session.app_state.has_config():
1181 raise HTTPException(
1183 detail=
"No config loaded. Provide config_path or load a config first."
1185 config = session.app_state.config
1188 if not config.pressure_curves:
1189 raise HTTPException(
1191 detail=
"Config does not have pressure_curves. This must be a Layer 2 optimized config."
1199 pc = config.pressure_curves
1200 n_points = pc.n_points
1201 burn_time = pc.target_burn_time_s
1206 "length_ratio": seg.length_ratio,
1208 "start_pressure": seg.start_pressure_pa,
1209 "end_pressure": seg.end_pressure_pa,
1212 for seg
in pc.lox_segments
1216 "length_ratio": seg.length_ratio,
1218 "start_pressure": seg.start_pressure_pa,
1219 "end_pressure": seg.end_pressure_pa,
1222 for seg
in pc.fuel_segments
1226 P_lox = generate_pressure_curve_from_segments(lox_segments, n_points)
1227 P_fuel = generate_pressure_curve_from_segments(fuel_segments, n_points)
1230 time_array = np.linspace(0, burn_time, n_points)
1233 runner = PintleEngineRunner(config)
1236 for i
in range(n_points):
1238 results = runner.evaluate(
1243 thrust_curve.append(float(results.get(
"F", 0.0)))
1244 except Exception
as e:
1246 thrust_curve.append(thrust_curve[-1]
if thrust_curve
else 0.0)
1251 thrust_curve_time=time_array.tolist(),
1252 thrust_curve_values=thrust_curve,
1253 initial_meas=request.initial_meas,
1254 initial_nav=request.initial_nav,
1264 except HTTPException:
1266 except Exception
as e:
1267 _log.exception(
"simulate from config failed")
1268 raise HTTPException(status_code=500, detail=f
"Failed to simulate from config: {e}")
1271@router.post("/simulate-from-config-stream")
1273 """Stream controller simulation from a Layer 2 config file in real-time."""
1275 def safe_json_dumps(obj):
1276 """Safely serialize to JSON, handling non-serializable types."""
1279 async def event_generator():
1283 if request.config_path:
1286 except HTTPException
as e:
1287 yield f
"data: {safe_json_dumps({'type': 'error', 'error': e.detail})}\n\n"
1289 config = load_engine_config(str(path))
1292 if not session.app_state.has_config():
1293 yield f
"data: {safe_json_dumps({'type': 'error', 'error': 'No config loaded. Provide config_path or load a config first.'})}\n\n"
1295 config = session.app_state.config
1298 if not config.pressure_curves:
1299 yield f
"data: {safe_json_dumps({'type': 'error', 'error': 'Config does not have pressure_curves. This must be a Layer 2 optimized config.'})}\n\n"
1302 yield f
"data: {safe_json_dumps({'type': 'status', 'progress': 0.0, 'stage': 'Extracting Thrust Curve', 'message': 'Generating pressure curves from config...'})}\n\n"
1309 pc = config.pressure_curves
1310 n_points = pc.n_points
1311 burn_time = pc.target_burn_time_s
1315 "length_ratio": seg.length_ratio,
1317 "start_pressure": seg.start_pressure_pa,
1318 "end_pressure": seg.end_pressure_pa,
1321 for seg
in pc.lox_segments
1325 "length_ratio": seg.length_ratio,
1327 "start_pressure": seg.start_pressure_pa,
1328 "end_pressure": seg.end_pressure_pa,
1331 for seg
in pc.fuel_segments
1334 P_lox = generate_pressure_curve_from_segments(lox_segments, n_points)
1335 P_fuel = generate_pressure_curve_from_segments(fuel_segments, n_points)
1336 time_array = np.linspace(0, burn_time, n_points)
1338 yield f
"data: {safe_json_dumps({'type': 'status', 'progress': 0.2, 'stage': 'Evaluating Engine', 'message': 'Computing thrust curve from pressure curves...'})}\n\n"
1341 runner = await run_in_threadpool(PintleEngineRunner, config)
1344 for i
in range(n_points):
1346 results = await run_in_threadpool(
1352 thrust_curve.append(float(results.get(
"F", 0.0)))
1354 thrust_curve.append(thrust_curve[-1]
if thrust_curve
else 0.0)
1357 if (i + 1) % 20 == 0:
1358 progress = 0.2 + 0.3 * ((i + 1) / n_points)
1359 yield f
"data: {safe_json_dumps({'type': 'progress', 'progress': progress, 'stage': 'Evaluating Engine', 'message': f'Evaluated {i+1}/{n_points} points'})}\n\n"
1361 yield f
"data: {safe_json_dumps({'type': 'status', 'progress': 0.5, 'stage': 'Starting Controller', 'message': 'Thrust curve extracted. Starting controller simulation...'})}\n\n"
1365 thrust_curve_time=time_array.tolist(),
1366 thrust_curve_values=thrust_curve,
1367 initial_meas=request.initial_meas,
1368 initial_nav=request.initial_nav,
1374 dt = float(request.dt)
if request.dt
is not None else 0.01
1378 cfg = get_default_config()
1379 if not session.app_state.has_config():
1380 yield f
"data: {safe_json_dumps({'type': 'error', 'error': 'No engine config loaded. Load a config first.'})}\n\n"
1382 engine_config = session.app_state.config
1383 await run_compute(session, _fresh_controller, session, cfg, engine_config)
1392 time_array_np = np.array(layer2_request.thrust_curve_time)
1393 thrust_curve_np = np.array(layer2_request.thrust_curve_values)
1394 n_steps = len(time_array_np)
1397 if request.initial_meas
is None:
1399 P_copv=18.96e6, P_reg=6.89e6,
1400 P_u_fuel=pc.initial_fuel_pressure_pa,
1401 P_u_ox=pc.initial_lox_pressure_pa,
1402 P_d_fuel=pc.initial_fuel_pressure_pa,
1403 P_d_ox=pc.initial_lox_pressure_pa,
1407 P_copv=request.initial_meas.P_copv,
1408 P_reg=request.initial_meas.P_reg,
1409 P_u_fuel=request.initial_meas.P_u_fuel,
1410 P_u_ox=request.initial_meas.P_u_ox,
1411 P_d_fuel=request.initial_meas.P_d_fuel,
1412 P_d_ox=request.initial_meas.P_d_ox,
1415 if request.initial_nav
is None:
1416 nav =
NavState(h=0.0, vz=0.0, theta=0.0, mass_estimate=100.0)
1419 h=request.initial_nav.h,
1420 vz=request.initial_nav.vz,
1421 theta=request.initial_nav.theta,
1422 mass_estimate=request.initial_nav.mass_estimate,
1428 dynamics_params = DynamicsParams.from_config(session.controller.controller.cfg)
1429 x = session.controller.controller._build_state(meas)
1433 for i, t
in enumerate(time_array_np):
1434 progress = 0.5 + 0.5 * ((i + 1) / n_steps)
1436 if i < len(thrust_curve_np):
1437 thrust_ref = float(thrust_curve_np[i])
1439 thrust_ref = float(thrust_curve_np[-1])
if len(thrust_curve_np) > 0
else 0.0
1442 command_type=CommandType.THRUST_DESIRED,
1443 thrust_desired=thrust_ref,
1446 actuation, diagnostics = await run_compute(session, session.controller.controller.step, meas, nav, cmd)
1448 eng_est = diagnostics.get(
"eng_est")
1449 solution = diagnostics.get(
"solution")
1450 value_func = solution.objective
if solution
and hasattr(solution,
'objective')
else 0.0
1452 u_relaxed = diagnostics.get(
"u_relaxed", np.array([0.0, 0.0]))
1453 if u_prev
is not None:
1454 control_effort = float(np.linalg.norm(u_relaxed - u_prev))
1456 control_effort = float(np.linalg.norm(u_relaxed))
1457 u_prev = u_relaxed.copy()
1461 w_bar = get_w_bar_array(session.controller.controller.state).tolist()
1462 constraint_margins = diagnostics.get(
"constraint_margins", {})
1470 "thrust_actual":
safe_float(diagnostics.get(
"F_hat", 0.0)),
1471 "MR":
safe_float(eng_est.MR
if eng_est
and eng_est.MR
else 0.0),
1478 "P_ch":
safe_float(eng_est.P_ch
if eng_est
and eng_est.P_ch
else 0.0),
1484 "control_effort":
safe_float(control_effort),
1485 "V_u_fuel":
safe_float(session.controller.controller.V_u_F),
1486 "V_u_ox":
safe_float(session.controller.controller.V_u_O),
1487 "mdot_F":
safe_float(eng_est.mdot_F
if eng_est
and eng_est.mdot_F
else 0.0),
1488 "mdot_O":
safe_float(eng_est.mdot_O
if eng_est
and eng_est.mdot_O
else 0.0),
1492 yield f
"data: {safe_json_dumps(data_point)}\n\n"
1494 if (i + 1) % 10 == 0
or i == n_steps - 1:
1495 yield f
"data: {safe_json_dumps({'type': 'progress', 'progress': progress, 'stage': 'Simulating', 'message': f'Step {i+1}/{n_steps}'})}\n\n"
1498 if i < len(time_array_np) - 1:
1499 dt_step = float(time_array_np[i+1] - time_array_np[i])
1503 u_relaxed_array = np.array([actuation.duty_F, actuation.duty_O])
1504 mdot_F = float(eng_est.mdot_F)
if eng_est
and hasattr(eng_est,
'mdot_F')
and eng_est.mdot_F
is not None else 0.0
1505 mdot_O = float(eng_est.mdot_O)
if eng_est
and hasattr(eng_est,
'mdot_O')
and eng_est.mdot_O
is not None else 0.0
1507 x_next = dynamics_step(x, u_relaxed_array, dt_step, dynamics_params, mdot_F, mdot_O)
1509 meas.P_copv = x_next[IDX_P_COPV]
1510 meas.P_reg = x_next[IDX_P_REG]
1511 meas.P_u_fuel = x_next[IDX_P_U_F]
1512 meas.P_u_ox = x_next[IDX_P_U_O]
1513 meas.P_d_fuel = x_next[IDX_P_D_F]
1514 meas.P_d_ox = x_next[IDX_P_D_O]
1516 session.controller.controller.V_u_F = x_next[IDX_V_U_F]
1517 session.controller.controller.V_u_O = x_next[IDX_V_U_O]
1519 F_actual = diagnostics.get(
"F_hat", 0.0)
1520 if isinstance(F_actual, (int, float))
and F_actual > 0:
1521 mass = nav.mass_estimate
or 100.0
1523 mdot_total = (eng_est.mdot_F
or 0.0) + (eng_est.mdot_O
or 0.0)
1524 mass -= mdot_total * dt_step
1525 nav.mass_estimate =
max(mass, 50.0)
1527 acceleration = (F_actual / nav.mass_estimate) - 9.81
1528 nav.vz += acceleration * dt_step
1529 nav.h += nav.vz * dt_step
1531 nav.vz -= 9.81 * dt_step
1532 nav.h += nav.vz * dt_step
1535 await asyncio.sleep(0.01)
1537 yield f
"data: {safe_json_dumps({'type': 'complete', 'progress': 1.0, 'stage': 'Complete', 'message': 'Simulation complete'})}\n\n"
1539 except Exception
as e:
1540 _log.exception(
"simulate from config failed")
1541 error_detail = f
"Failed to simulate from config: {e}"
1542 yield f
"data: {safe_json_dumps({'type': 'error', 'error': error_detail})}\n\n"
1544 return StreamingResponse(
1545 gated_stream(event_generator()),
1546 media_type=
"text/event-stream",
1547 headers={
"Cache-Control":
"no-cache",
"Connection":
"keep-alive",
"X-Accel-Buffering":
"no"}
1551@router.post("/upload-config-and-simulate")
1553 file: UploadFile = File(...),
1555 session: UserSession = Depends(get_session),
1557 """Upload a Layer 2 config file and immediately run controller simulation."""
1558 def safe_json_dumps(obj):
1559 """Safely serialize to JSON, handling non-serializable types."""
1562 async def event_generator():
1566 contents = await file.read()
1567 config_dict = yaml.safe_load(contents.decode(
"utf-8"))
1571 config = PintleEngineConfig(**config_dict)
1574 if not config.pressure_curves:
1575 yield f
"data: {safe_json_dumps({'type': 'error', 'error': 'Config does not have pressure_curves. This must be a Layer 2 optimized config.'})}\n\n"
1578 yield f
"data: {safe_json_dumps({'type': 'status', 'progress': 0.0, 'stage': 'Extracting Thrust Curve', 'message': 'Generating pressure curves from config...'})}\n\n"
1585 pc = config.pressure_curves
1586 n_points = pc.n_points
1587 burn_time = pc.target_burn_time_s
1591 "length_ratio": seg.length_ratio,
1593 "start_pressure": seg.start_pressure_pa,
1594 "end_pressure": seg.end_pressure_pa,
1597 for seg
in pc.lox_segments
1601 "length_ratio": seg.length_ratio,
1603 "start_pressure": seg.start_pressure_pa,
1604 "end_pressure": seg.end_pressure_pa,
1607 for seg
in pc.fuel_segments
1610 P_lox = generate_pressure_curve_from_segments(lox_segments, n_points)
1611 P_fuel = generate_pressure_curve_from_segments(fuel_segments, n_points)
1612 time_array = np.linspace(0, burn_time, n_points)
1614 yield f
"data: {safe_json_dumps({'type': 'status', 'progress': 0.2, 'stage': 'Evaluating Engine', 'message': 'Computing thrust curve from pressure curves...'})}\n\n"
1622 runner = await run_in_threadpool(PintleEngineRunner, config)
1625 for i
in range(n_points):
1627 results = await run_in_threadpool(
1633 thrust_curve.append(float(results.get(
"F", 0.0)))
1635 thrust_curve.append(thrust_curve[-1]
if thrust_curve
else 0.0)
1638 if (i + 1) % 20 == 0:
1639 progress = 0.2 + 0.3 * ((i + 1) / n_points)
1640 yield f
"data: {safe_json_dumps({'type': 'progress', 'progress': progress, 'stage': 'Evaluating Engine', 'message': f'Evaluated {i+1}/{n_points} points'})}\n\n"
1642 yield f
"data: {safe_json_dumps({'type': 'status', 'progress': 0.5, 'stage': 'Starting Controller', 'message': 'Thrust curve extracted. Starting controller simulation...'})}\n\n"
1647 if session.controller.controller
is None:
1648 cfg = get_default_config()
1649 engine_config = config
1650 session.controller.controller = await run_compute(session, RobustDDPController, cfg, engine_config, logger=
None)
1654 P_copv=18.96e6, P_reg=6.89e6,
1655 P_u_fuel=pc.initial_fuel_pressure_pa,
1656 P_u_ox=pc.initial_lox_pressure_pa,
1657 P_d_fuel=pc.initial_fuel_pressure_pa,
1658 P_d_ox=pc.initial_lox_pressure_pa,
1661 nav =
NavState(h=0.0, vz=0.0, theta=0.0, mass_estimate=100.0)
1666 dynamics_params = DynamicsParams.from_config(session.controller.controller.cfg)
1667 x = session.controller.controller._build_state(meas)
1671 for i, t
in enumerate(time_array):
1672 progress = 0.5 + 0.5 * ((i + 1) / n_points)
1674 if i < len(thrust_curve):
1675 thrust_ref = float(thrust_curve[i])
1677 thrust_ref = float(thrust_curve[-1])
if len(thrust_curve) > 0
else 0.0
1680 command_type=CommandType.THRUST_DESIRED,
1681 thrust_desired=thrust_ref,
1684 actuation, diagnostics = await run_compute(session, session.controller.controller.step, meas, nav, cmd)
1686 eng_est = diagnostics.get(
"eng_est")
1687 solution = diagnostics.get(
"solution")
1688 value_func = solution.objective
if solution
and hasattr(solution,
'objective')
else 0.0
1690 u_relaxed = diagnostics.get(
"u_relaxed", np.array([0.0, 0.0]))
1691 if u_prev
is not None:
1692 control_effort = float(np.linalg.norm(u_relaxed - u_prev))
1694 control_effort = float(np.linalg.norm(u_relaxed))
1695 u_prev = u_relaxed.copy()
1699 w_bar = get_w_bar_array(session.controller.controller.state).tolist()
1700 constraint_margins = diagnostics.get(
"constraint_margins", {})
1707 "thrust_actual":
safe_float(diagnostics.get(
"F_hat", 0.0)),
1708 "MR":
safe_float(eng_est.MR
if eng_est
and eng_est.MR
else 0.0),
1715 "P_ch":
safe_float(eng_est.P_ch
if eng_est
and eng_est.P_ch
else 0.0),
1721 "control_effort":
safe_float(control_effort),
1722 "V_u_fuel":
safe_float(session.controller.controller.V_u_F),
1723 "V_u_ox":
safe_float(session.controller.controller.V_u_O),
1724 "mdot_F":
safe_float(eng_est.mdot_F
if eng_est
and eng_est.mdot_F
else 0.0),
1725 "mdot_O":
safe_float(eng_est.mdot_O
if eng_est
and eng_est.mdot_O
else 0.0),
1729 yield f
"data: {safe_json_dumps(data_point)}\n\n"
1731 if (i + 1) % 10 == 0
or i == n_points - 1:
1732 yield f
"data: {safe_json_dumps({'type': 'progress', 'progress': progress, 'stage': 'Simulating', 'message': f'Step {i+1}/{n_points}'})}\n\n"
1735 if i < len(time_array) - 1:
1736 dt_step = float(time_array[i+1] - time_array[i])
1740 u_relaxed_array = np.array([actuation.duty_F, actuation.duty_O])
1741 mdot_F = float(eng_est.mdot_F)
if eng_est
and hasattr(eng_est,
'mdot_F')
and eng_est.mdot_F
is not None else 0.0
1742 mdot_O = float(eng_est.mdot_O)
if eng_est
and hasattr(eng_est,
'mdot_O')
and eng_est.mdot_O
is not None else 0.0
1744 x_next = dynamics_step(x, u_relaxed_array, dt_step, dynamics_params, mdot_F, mdot_O)
1746 meas.P_copv = x_next[IDX_P_COPV]
1747 meas.P_reg = x_next[IDX_P_REG]
1748 meas.P_u_fuel = x_next[IDX_P_U_F]
1749 meas.P_u_ox = x_next[IDX_P_U_O]
1750 meas.P_d_fuel = x_next[IDX_P_D_F]
1751 meas.P_d_ox = x_next[IDX_P_D_O]
1753 session.controller.controller.V_u_F = x_next[IDX_V_U_F]
1754 session.controller.controller.V_u_O = x_next[IDX_V_U_O]
1756 F_actual = diagnostics.get(
"F_hat", 0.0)
1757 if isinstance(F_actual, (int, float))
and F_actual > 0:
1758 mass = nav.mass_estimate
or 100.0
1760 mdot_total = (eng_est.mdot_F
or 0.0) + (eng_est.mdot_O
or 0.0)
1761 mass -= mdot_total * dt_step
1762 nav.mass_estimate =
max(mass, 50.0)
1764 acceleration = (F_actual / nav.mass_estimate) - 9.81
1765 nav.vz += acceleration * dt_step
1766 nav.h += nav.vz * dt_step
1768 nav.vz -= 9.81 * dt_step
1769 nav.h += nav.vz * dt_step
1772 await asyncio.sleep(0.01)
1774 yield f
"data: {safe_json_dumps({'type': 'complete', 'progress': 1.0, 'stage': 'Complete', 'message': 'Simulation complete'})}\n\n"
1777 except Exception
as e:
1778 _log.exception(
"upload and simulate failed")
1779 error_detail = f
"Failed to upload and simulate: {e}"
1780 yield f
"data: {safe_json_dumps({'type': 'error', 'error': error_detail})}\n\n"
1782 return StreamingResponse(
1783 gated_stream(event_generator()),
1784 media_type=
"text/event-stream",
1785 headers={
"Cache-Control":
"no-cache",
"Connection":
"keep-alive",
"X-Accel-Buffering":
"no"}
1789@router.get("/status")