jjreif's picture
Deploy roverdevkit @ 2676a67
b3d14e3
Raw
History Blame Contribute Delete
5.68 kB
"""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
@dataclass(frozen=True)
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``)."""
@dataclass(frozen=True)
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}