STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
evaluate.py
Go to the documentation of this file.
1"""Engine evaluation endpoints."""
2
3from fastapi import APIRouter, HTTPException, Depends
4from typing import Literal
5
6from pydantic import BaseModel, Field
7import math
8import numpy as np
9
10from backend.session import UserSession, get_session, run_compute
11
12router = APIRouter(prefix="/api/evaluate", tags=["evaluate"])
13
14
15# Constants
16PSI_TO_PA = 6894.76
17PA_TO_PSI = 1.0 / PSI_TO_PA
18
19
20class StabilityOverrides(BaseModel):
21 """Optional forward-mode knobs for rich stability re-evaluation."""
22 eta_inj_O: float | None = Field(default=None, gt=0, le=0.6, description="Oxidizer ΔP_inj/Pc")
23 smd_um: float | None = Field(default=None, gt=0, le=400, description="Oxidizer spray SMD [µm]")
24 smd_F_um: float | None = Field(default=None, gt=0, le=400, description="Fuel spray SMD [µm]")
25 eta_inj_F: float | None = Field(default=None, gt=0, le=0.6, description="Fuel ΔP_inj/Pc")
26 n_interaction: float | None = Field(default=None, gt=0, le=2, description="Combustion interaction index n")
27 chi_acoustic: float | None = Field(default=None, gt=0, le=1, description="Acoustic sensitive-fraction χ")
28 time_lag_model: Literal["leonardi_dtl", "d2_law"] | None = Field(
29 default=None,
30 description="Conversion-lag model for the chug loop. Overrides stability.time_lag_model for this run only.",
31 )
32 convection_model: Literal["none", "leonardi_eq8", "ranz_marshall"] | None = Field(
33 default=None, description="Convective speed-up applied to the droplet lifetime.",
34 )
35 mixing_lag_fraction: float | None = Field(
36 default=None, ge=0, le=3,
37 description="Mixing lag as a fraction of the rate-limiting vaporization lag.",
38 )
39
40
41class EvaluateRequest(BaseModel):
42 """Request body for forward evaluation."""
43 lox_pressure_psi: float = Field(..., gt=0, description="LOX tank pressure in psi")
44 fuel_pressure_psi: float = Field(..., gt=0, description="Fuel tank pressure in psi")
45 stability_overrides: StabilityOverrides | None = Field(
46 default=None, description="Optional stability sensitivity overrides (forward mode sliders)",
47 )
48
49
51 """Recursively convert numpy types to Python native types."""
52 if isinstance(obj, dict):
53 return {k: convert_numpy(v) for k, v in obj.items()}
54 elif isinstance(obj, (list, tuple)):
55 return [convert_numpy(item) for item in obj]
56 elif isinstance(obj, np.ndarray):
57 return obj.tolist()
58 elif isinstance(obj, np.floating):
59 # NaN/Inf are legal model outputs (a lag model that does not define K_v, a margin
60 # that could not be evaluated) but json.dumps rejects them outright --
61 # "Out of range float values are not JSON compliant" -- which surfaced as a blanket
62 # HTTP 500 on forward evaluation. Emit JSON null instead, so a missing number reads
63 # as missing rather than taking the whole response down.
64 v = obj.item()
65 return v if math.isfinite(v) else None
66 elif isinstance(obj, np.integer):
67 return obj.item()
68 elif isinstance(obj, float):
69 return obj if math.isfinite(obj) else None
70 elif isinstance(obj, np.bool_):
71 return bool(obj)
72 else:
73 return obj
74
75
76@router.post("")
77async def evaluate(request: EvaluateRequest, session: UserSession = Depends(get_session)):
78 """Run forward evaluation: tank pressures -> performance.
79
80 Takes LOX and fuel tank pressures in psi, returns the full results from runner.evaluate().
81 Ambient pressure is computed automatically by the runner from the config's environment elevation.
82 The results dict is passed through directly (with numpy conversion) - same format as Streamlit UI uses.
83
84 The solve runs in the thread pool (``run_compute``): Forward Mode evaluates every design it
85 opens, and on the event loop that second or two froze every other request.
86 """
87 return await run_compute(session, _evaluate, request, session)
88
89
90def _evaluate(request: EvaluateRequest, session: UserSession) -> dict:
91 # One config and its runner for the whole solve. Off the event loop a PUT /api/config can
92 # land while the solve runs; the report must describe the design the numbers came from.
93 config, runner = session.app_state.snapshot()
94 if config is None or runner is None:
95 raise HTTPException(
96 status_code=400,
97 detail="No config loaded. Upload a config file first."
98 )
99
100 # Convert psi to Pa
101 P_tank_O = request.lox_pressure_psi * PSI_TO_PA
102 P_tank_F = request.fuel_pressure_psi * PSI_TO_PA
103
104 try:
105 # Get raw results from runner - ambient pressure computed from config elevation
106 overrides = None
107 if request.stability_overrides is not None:
108 overrides = {
109 k: v for k, v in request.stability_overrides.model_dump().items() if v is not None
110 } or None
111 results = runner.evaluate(
112 P_tank_O, P_tank_F, debug=True, rich_stability=True, stability_overrides=overrides,
113 )
114
115 # "Warn + flag for re-solve": if the chamber was seeded for a different injector/propellant
116 # than is now live, surface a non-fatal warning so the forward result is read as a seed, not
117 # a validated design.
118 from engine.pipeline.config_switch import design_staleness
119 design_warning = design_staleness(config)
120
121 # Forward mode's view: every quantity once, with its basis and what it rests on.
122 from engine.pipeline.forward_report import forward_report
123 try:
124 from engine.pipeline.handcheck import handcheck
125 hc = handcheck(config, results, P_tank_O, P_tank_F)
126 except Exception as e: # the check is optional; a missing CEA must not fail the solve
127 hc = {"rows": [], "flags": [], "error": f"{type(e).__name__}: {e}"}
128 report = forward_report(config, results, handcheck_rows=hc)
129
130 # Convert numpy types to JSON-serializable and return directly
131 # Frontend uses the same field names as runner.py outputs
132 # P_ambient and elevation are now included in results from runner
133 return {
134 "status": "success",
135 "inputs": {
136 "lox_pressure_psi": request.lox_pressure_psi,
137 "fuel_pressure_psi": request.fuel_pressure_psi,
138 "ambient_pressure_pa": results.get("P_ambient", 101325.0),
139 "elevation_m": results.get("elevation", 0.0),
140 },
141 "design_warning": design_warning,
142 "results": convert_numpy(results),
143 "report": convert_numpy(report),
144 }
145
146 except Exception as e:
147 raise HTTPException(
148 status_code=500,
149 detail=f"Evaluation failed: {str(e)}"
150 )
dict _evaluate(EvaluateRequest request, UserSession session)
Definition evaluate.py:90