Spaces:
Sleeping
Sleeping
| """``POST /predict`` — surrogate point prediction with 90 % PI.""" | |
| 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, | |
| get_quantile_bundles, | |
| get_soil_for_simulant, | |
| ) | |
| from webapp.backend.schemas import ( | |
| FeatureRow, | |
| PredictRequest, | |
| PredictResponse, | |
| PredictTarget, | |
| ) | |
| from webapp.backend.services import apply_scenario_overrides | |
| from webapp.backend.services.predict import build_feature_row, predict_quantiles | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter(tags=["predict"]) | |
| def predict(req: PredictRequest) -> PredictResponse: | |
| """Return median + 90 % prediction intervals for the four primary targets. | |
| Pipeline | |
| -------- | |
| 1. Resolve the scenario from the canonical four (404 if unknown). | |
| 2. Look up nominal Bekker-Wong soil parameters for the scenario's | |
| simulant. | |
| 3. Assemble the 27-D feature row in the surrogate's training-time | |
| column order, applying any per-call ``operational_duty_cycle`` | |
| and schema-v9 payload (``payload_mass_kg`` / ``payload_power_w``) | |
| mission-requirement overrides before flattening so the surrogate | |
| sees the same scenario inputs the deterministic evaluator would. | |
| 4. Dispatch to every primary target's ``QuantileHeads`` head and | |
| collect ``(q05, q50, q95)`` triples. | |
| The surrogate is the quantile-calibration ``quantile_bundles.joblib``; | |
| ``q50`` is within R² 0.005 of the tuned-median tuned median (see | |
| ``reports/intervals_v4/SUMMARY.md`` for the median sanity | |
| guardrail), so this single artifact powers both point estimates | |
| and PI envelopes. | |
| Schema v7_1 (v7_1 schema follow-on): ``operational_duty_cycle`` is | |
| a true surrogate input feature (LHS-sampled per row over [0, 0.6]), | |
| so any in-bounds δ_ops is in-distribution and the calibrated PIs | |
| apply across the full frontend slider range. The pre-v7_1 | |
| "evaluator-only fallback when override differs from default" gate | |
| has been removed; ``mode`` is always ``"surrogate"``. | |
| """ | |
| scenarios = get_canonical_scenarios() | |
| if req.scenario_name not in scenarios: | |
| raise HTTPException( | |
| status_code=404, | |
| detail=( | |
| f"unknown scenario {req.scenario_name!r}. Pick one of {sorted(scenarios.keys())}." | |
| ), | |
| ) | |
| scenario = apply_scenario_overrides( | |
| scenarios[req.scenario_name], | |
| operational_duty_cycle=req.operational_duty_cycle, | |
| 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, | |
| ) | |
| soil = get_soil_for_simulant(scenario.soil_simulant) | |
| X = build_feature_row(req.design, scenario, soil) | |
| try: | |
| bundles = get_quantile_bundles() | |
| except FileNotFoundError as exc: | |
| raise HTTPException( | |
| status_code=503, | |
| detail=( | |
| "surrogate artifact not loaded; run scripts/calibrate_intervals.py first." | |
| ), | |
| ) from exc | |
| preds = predict_quantiles(bundles, X, repair_crossings=req.repair_crossings) | |
| targets = [ | |
| PredictTarget( | |
| target=t, # type: ignore[arg-type] | |
| q05=preds[t]["q05"], | |
| q50=preds[t]["q50"], | |
| q95=preds[t]["q95"], | |
| ) | |
| for t in PRIMARY_REGRESSION_TARGETS | |
| ] | |
| feature_row = FeatureRow( | |
| columns=list(X.columns), | |
| values=[v.item() if hasattr(v, "item") else v for v in X.iloc[0].tolist()], | |
| ) | |
| return PredictResponse( | |
| scenario_name=req.scenario_name, | |
| predictions=targets, | |
| feature_row=feature_row, | |
| mode="surrogate", | |
| ) | |