roverdevkit / webapp /backend /routes /evaluate.py
jjreif's picture
Deploy roverdevkit @ 2676a67
b3d14e3
Raw
History Blame Contribute Delete
5.29 kB
"""``POST /evaluate`` — deterministic analytical mission evaluator.
This is the *single-shot* counterpart to ``/predict``. It runs the same
physics pipeline that produced the surrogate's training corpus, so the
returned values are the ground truth the surrogate is regressing
against. The single-design panel uses ``/evaluate`` for the median
value of each metric (and for real-rover overlays) and ``/predict``
only for the surrogate's calibrated 90 % prediction-interval band.
The analytical evaluator runs in ~30 ms after the traverse-loop
lift-out, which is imperceptible for one-click UX. The 50k+-evaluation
inner loops (NSGA-II, feasibility heatmaps) keep using the surrogate
because even 30 ms × 50k is ~25 minutes of wall-clock.
"""
from __future__ import annotations
import logging
from fastapi import APIRouter, HTTPException
from roverdevkit.surrogate.features import PRIMARY_REGRESSION_TARGETS
from webapp.backend.loaders import get_canonical_scenarios
from webapp.backend.services import apply_scenario_overrides
from webapp.backend.schemas import (
ArchitectureDiagnosticOut,
EvaluateMetric,
EvaluateRequest,
EvaluateResponse,
StallDiagnosticOut,
ThermalDiagnosticOut,
)
from webapp.backend.services.evaluate import (
evaluate_design,
metrics_as_primary_dict,
)
logger = logging.getLogger(__name__)
router = APIRouter(tags=["evaluate"])
@router.post("/evaluate", response_model=EvaluateResponse)
def evaluate_route(req: EvaluateRequest) -> EvaluateResponse:
"""Run the analytical mission evaluator on one design × one scenario.
Pipeline
--------
1. Resolve the scenario from the canonical four (404 if unknown).
2. Dispatch to :func:`roverdevkit.mission.evaluator.evaluate_verbose`.
3. Project ``MissionMetrics`` onto the four primary targets and
attach structured ``thermal`` / ``stall`` diagnostics (schema
v6) plus the runtime-resolved ``effective_duty_cycle`` and
``cruise_speed_mps`` so the panel chip can explain *why* a
survival flag fired.
"""
scenarios = get_canonical_scenarios()
if req.scenario_name not in scenarios:
raise HTTPException(
status_code=404,
detail=(
f"unknown scenario {req.scenario_name!r}. "
f"Pick one of {sorted(scenarios.keys())}."
),
)
scenario = apply_scenario_overrides(
scenarios[req.scenario_name],
payload_mass_kg=req.payload_mass_kg,
payload_power_w=req.payload_power_w,
mission_duration_earth_days=req.mission_duration_earth_days,
required_obstacle_height_m=req.required_obstacle_height_m,
)
output = evaluate_design(
req.design,
scenario,
operational_duty_cycle=req.operational_duty_cycle,
required_obstacle_height_m=req.required_obstacle_height_m,
)
primary = metrics_as_primary_dict(output.metrics)
metrics = [
EvaluateMetric(target=t, value=primary[t]) # type: ignore[arg-type]
for t in PRIMARY_REGRESSION_TARGETS
]
arch = output.thermal # ThermalResult
thermal_out = ThermalDiagnosticOut(
survives=bool(arch.survives),
peak_sun_temp_c=float(arch.peak_sun_temp_c),
lunar_night_temp_c=float(arch.lunar_night_temp_c),
# The default architecture used by the evaluator pins these
# limits at -30 / +50 °C; we re-state them here so the frontend
# never has to hardcode a number.
min_operating_temp_c=-30.0,
max_operating_temp_c=50.0,
rhu_power_w=0.0,
hibernation_power_w=2.0,
# Surface area is rebuilt from the chassis mass via the same
# cube-root proxy used inside `evaluate_verbose`; we reproduce
# it for the response so the dialog can show users what
# radiating area the model assumed.
surface_area_m2=0.02 * (req.design.chassis_mass_kg ** (2.0 / 3.0)) + 0.05,
hot_case_ok=arch.peak_sun_temp_c <= 50.0,
cold_case_ok=arch.lunar_night_temp_c >= -30.0,
)
st = output.stall
stall_out = StallDiagnosticOut(
stalled=bool(st.stalled),
peak_torque_demand_nm=float(st.peak_torque_demand_nm),
peak_torque_capacity_nm=float(st.peak_torque_capacity_nm),
)
return EvaluateResponse(
scenario_name=req.scenario_name,
metrics=metrics,
thermal=thermal_out,
stall=stall_out,
architecture=ArchitectureDiagnosticOut(
mobility_architecture=req.design.mobility_architecture,
obstacle_capability_m=float(output.metrics.obstacle_capability_m),
required_obstacle_height_m=float(
req.required_obstacle_height_m
if req.required_obstacle_height_m is not None
else scenario.required_obstacle_height_m
),
obstacle_margin_m=float(output.metrics.obstacle_margin_m),
obstacle_requirement_met=bool(output.metrics.obstacle_requirement_met),
architecture_mass_kg=float(output.metrics.architecture_mass_kg),
),
effective_duty_cycle=float(output.effective_duty_cycle),
cruise_speed_mps=float(output.cruise_speed_mps),
elapsed_ms=output.elapsed_ms,
)