Spaces:
Running
Running
| """``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"]) | |
| 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, | |
| ) | |