Spaces:
Running
Running
| """Corrected mission evaluator dispatch for ``POST /evaluate``. | |
| This service is a thin wrapper around | |
| :func:`roverdevkit.mission.evaluator.evaluate_verbose`. The single-design | |
| panel calls it for the deterministic median of each performance metric so | |
| the chart's diamond marker is the ground-truth physics output rather | |
| than the surrogate's regression of it; the surrogate's quantile heads | |
| still supply the prediction interval around that median. | |
| We use ``evaluate_verbose`` (rather than the lighter ``evaluate``) so we | |
| can surface the *why* behind the constraint flags: the peak / cold | |
| enclosure temperatures from the lumped-parameter thermal model and the | |
| explicit drivetrain stall gate (peak per-wheel hub torque demand vs | |
| ``DesignVector.peak_wheel_torque_nm``). The cost of the verbose path is | |
| identical -- the underlying physics call is the same -- and the extra | |
| fields are dropped on the floor for callers that only want | |
| ``MissionMetrics``. | |
| Schema v6 (v6 schema update): the previous ``MotorTorqueDiagnostic`` was | |
| replaced by :class:`StallDiagnostic`. The pre-v6 diagnostic compared the | |
| peak observed torque to a closed-form per-wheel ceiling derived from | |
| ``mass × g / N × R × sf × μ`` inside the mass model; v6 makes the | |
| ceiling an explicit design input (``peak_wheel_torque_nm``) and the | |
| stall gate is an explicit slip-balance comparison inside | |
| :mod:`roverdevkit.drivetrain.motor`. | |
| """ | |
| from __future__ import annotations | |
| import time | |
| from dataclasses import dataclass | |
| from roverdevkit.mission.evaluator import evaluate_verbose | |
| from roverdevkit.power.thermal import ThermalResult | |
| from roverdevkit.schema import DesignVector, MissionMetrics, MissionScenario | |
| from roverdevkit.surrogate.features import PRIMARY_REGRESSION_TARGETS | |
| class StallDiagnostic: | |
| """Drivetrain stall status (schema v6). | |
| Encodes the explicit stall gate: | |
| ``stalled = (T_req_per_wheel_nm > peak_wheel_torque_nm) or | |
| slip_solver_failed``. ``peak_torque_demand_nm`` is the slip-balance | |
| torque the wheel-level solve computed; ``peak_torque_capacity_nm`` | |
| is the design input (``DesignVector.peak_wheel_torque_nm``) echoed | |
| back so the frontend can render both numbers side-by-side. | |
| """ | |
| stalled: bool | |
| """``True`` iff the rover stalled under the scenario's worst-case | |
| load (drives :data:`MissionMetrics.stalled`).""" | |
| peak_torque_demand_nm: float | |
| """Largest absolute per-wheel torque the slip-balance solve | |
| demanded during the traverse.""" | |
| peak_torque_capacity_nm: float | |
| """Design-input drivetrain capacity | |
| (``DesignVector.peak_wheel_torque_nm``).""" | |
| class EvaluatorOutput: | |
| """Container the evaluate route translates into the HTTP response. | |
| Splitting this off from the Pydantic ``EvaluateResponse`` keeps the | |
| service layer dependency-free (it only knows core types) and makes | |
| the route a one-liner. | |
| """ | |
| metrics: MissionMetrics | |
| thermal: ThermalResult | |
| stall: StallDiagnostic | |
| effective_duty_cycle: float | |
| cruise_speed_mps: float | |
| elapsed_ms: float | |
| def evaluate_design( | |
| design: DesignVector, | |
| scenario: MissionScenario, | |
| *, | |
| operational_duty_cycle: float | None = None, | |
| required_obstacle_height_m: float | None = None, | |
| ) -> EvaluatorOutput: | |
| """Run the analytical mission evaluator on one design × one scenario. | |
| Parameters | |
| ---------- | |
| design | |
| Validated 11-D design vector (Pydantic has already enforced the | |
| bounds at the HTTP boundary). | |
| scenario | |
| One of the canonical scenarios resolved server-side. | |
| operational_duty_cycle | |
| Schema v6 (v6 schema update): per-call override of | |
| ``MissionScenario.operational_duty_cycle``. ``None`` (default) | |
| uses the scenario's calibrated value. Schema v7 (v6 schema update | |
| follow-up): used directly as ``δ_eff`` (clamped to ``[0, 1]``). | |
| Returns | |
| ------- | |
| EvaluatorOutput | |
| :class:`MissionMetrics` plus a wall-clock measurement, the | |
| :class:`ThermalResult`, the :class:`StallDiagnostic`, and the | |
| runtime-resolved ``effective_duty_cycle`` / ``cruise_speed_mps``. | |
| """ | |
| t0 = time.perf_counter() | |
| detailed = evaluate_verbose( | |
| design, | |
| scenario, | |
| operational_duty_cycle=operational_duty_cycle, | |
| required_obstacle_height_m=required_obstacle_height_m, | |
| ) | |
| elapsed_ms = (time.perf_counter() - t0) * 1000.0 | |
| stall = StallDiagnostic( | |
| stalled=bool(detailed.metrics.stalled), | |
| peak_torque_demand_nm=float(detailed.log.peak_torque_demand_nm), | |
| peak_torque_capacity_nm=float(detailed.log.peak_torque_capacity_nm), | |
| ) | |
| return EvaluatorOutput( | |
| metrics=detailed.metrics, | |
| thermal=detailed.thermal, | |
| stall=stall, | |
| effective_duty_cycle=float(detailed.log.effective_duty_cycle), | |
| cruise_speed_mps=float(detailed.log.cruise_speed_mps), | |
| elapsed_ms=elapsed_ms, | |
| ) | |
| def metrics_as_primary_dict(metrics: MissionMetrics) -> dict[str, float]: | |
| """Project ``MissionMetrics`` onto the four primary regression targets. | |
| The primary subset is what the surrogate predicts and what the | |
| chart renders, so the projection lives next to the dispatch to | |
| keep the column ordering aligned with | |
| :data:`roverdevkit.surrogate.features.PRIMARY_REGRESSION_TARGETS`. | |
| """ | |
| src = { | |
| "range_km": metrics.range_km, | |
| "energy_margin_raw_pct": metrics.energy_margin_raw_pct, | |
| "slope_capability_deg": metrics.slope_capability_deg, | |
| "total_mass_kg": metrics.total_mass_kg, | |
| } | |
| return {target: float(src[target]) for target in PRIMARY_REGRESSION_TARGETS} | |