Source code for routee.powertrain.core.model_config

from __future__ import annotations

from typing import List, Optional

from pydantic import BaseModel, field_validator, model_validator

from routee.powertrain.core.features import (
    DataColumn,
    FeatureSet,
    TargetSet,
)
from routee.powertrain.core.powertrain_type import PowertrainType
from routee.powertrain.core.predict_method import PredictMethod
from routee.powertrain.core.real_world_adjustments import ADJUSTMENT_FACTORS
from routee.powertrain.core.provenance import (
    FastSimSource,
    LegacySource,
    Provenance,
    RealWorldSource,
    TrainingConfig,
    TrainingMethod,
    TrainingSource,
)
from routee.powertrain.core.pydantic_fields import (
    DrivetrainField,
    FuelTypeField,
    PowertrainTypeField,
    PredictMethodField,
    YearField,
)

# ``PredictMethod`` is re-exported for backwards compatibility (it used to live
# here); the provenance types are re-exported because ``TrainingConfig`` used to
# be defined here and callers expect its neighbors alongside it.
__all__ = [
    "ModelConfig",
    "PredictMethod",
    "Vehicle",
    "Contract",
    "Provenance",
    "TrainingConfig",
    "TrainingMethod",
    "TrainingSource",
    "FastSimSource",
    "RealWorldSource",
    "LegacySource",
]


[docs] class ModelConfig(BaseModel): ## vehicle information vehicle_description: str powertrain_type: PowertrainTypeField ## estimator information feature_set: FeatureSet distance: DataColumn target: TargetSet ## structured vehicle identification make: str model: str year: YearField #: Short label distinguishing configs that share the same architecture and #: feature set (e.g. ``"steady"`` vs ``"warmup"`` thermal regimes). Feeds the #: derived ``config_slug``; leave ``None`` when no such distinction is needed. variant: Optional[str] = None predict_method: PredictMethodField = PredictMethod.RATE test_size: Optional[float] = None validation_size: Optional[float] = None random_seed: int = 42 trip_column: str = "trip_id" #: What produced the training data — a ``FastSimSource``, #: ``RealWorldSource``, or ``LegacySource``. Carries the dataset labels #: (``dataset_name`` / ``dataset_hash``) alongside the source-specific #: fields. Stored in the persisted ``provenance`` section; descriptive, so #: it does not feed the model digest. training_source: Optional[TrainingSource] = None #: Multiplicative factor applied to predicted energy to correct for #: real-world conditions (e.g. temperature). Defaults to the #: powertrain-type factor in ``ADJUSTMENT_FACTORS``; set to ``1.0`` to #: apply no adjustment. real_world_adjustment_factor: float = 1.0 mass_lbs: Optional[float] = None fuel_type: Optional[FuelTypeField] = None drivetrain: Optional[DrivetrainField] = None engine: Optional[str] = None trim: Optional[str] = None @model_validator(mode="before") @classmethod def _default_adjustment_factor(cls, data: object) -> object: # When no factor is supplied, derive it from the powertrain type so the # real-world adjustment matches the vehicle's default behavior. if isinstance(data, dict) and data.get("real_world_adjustment_factor") is None: pt_val = data.get("powertrain_type") try: pt = ( pt_val if isinstance(pt_val, PowertrainType) else PowertrainType.from_string(pt_val) ) except Exception: pt = PowertrainType.UNDEFINED data = dict(data) data["real_world_adjustment_factor"] = float( ADJUSTMENT_FACTORS.get(pt, 1.0) ) return data @field_validator("make", "model", mode="after") @classmethod def _lowercase(cls, v: str) -> str: return v.lower() @field_validator("variant", mode="after") @classmethod def _slug_safe_variant(cls, v: Optional[str]) -> Optional[str]: if v is None: return None v = v.strip().lower() if not v: return None if "/" in v or any(c.isspace() for c in v): raise ValueError( f"variant '{v}' must not contain '/' or whitespace; " "use a short slug-safe label like 'steady' or 'warmup'" ) return v @field_validator("feature_set", mode="before") @classmethod def _coerce_feature_set(cls, v: object) -> object: # accept a bare list of columns; dicts/FeatureSet handled natively if isinstance(v, list): return {"features": v} return v @field_validator("target", mode="before") @classmethod def _coerce_target(cls, v: object) -> object: # accept a single column or a bare list; dicts/TargetSet handled natively if isinstance(v, DataColumn): return {"targets": [v]} if isinstance(v, list): return {"targets": v} return v @property def feature_names(self) -> List[str]: """ Returns the list of feature names from the feature set. """ return self.feature_set.feature_name_list @property def all_feature_names(self) -> List[str]: """ Returns the list of feature names, including distance if predict method is RAW. """ names = list(self.feature_set.feature_name_list) if self.predict_method == PredictMethod.RAW: names.append(self.distance.name) return names @property def all_features(self) -> List[DataColumn]: """ Returns the list of features, including distance if predict method is RAW. """ features = list(self.feature_set.features) if self.predict_method == PredictMethod.RAW: features.append(self.distance) return features
# --------------------------------------------------------------------------- # Grouped metadata sections # # ``ModelConfig`` is the flat object a user builds to train a model. When the # model is persisted, its fields are stored decomposed into the sections below, # grouped by the job a reader needs them for: identity (``Vehicle``), # input/output contract (``Contract``), and where the model came from # (``Provenance``, defined in ``core.provenance``). ``Metadata.config`` # reconstructs a flat ``ModelConfig`` from these on demand, so nothing is # stored twice. # ---------------------------------------------------------------------------
[docs] class Vehicle(BaseModel): """The vehicle a model describes — identity plus descriptive attributes. ``make``/``model``/``year``/``variant`` feed the derived ``config_slug`` and ``ModelKey``; the remaining fields are descriptive and registry-filterable. """ vehicle_description: str powertrain_type: PowertrainTypeField make: str model: str year: YearField variant: Optional[str] = None mass_lbs: Optional[float] = None fuel_type: Optional[FuelTypeField] = None drivetrain: Optional[DrivetrainField] = None engine: Optional[str] = None trim: Optional[str] = None @field_validator("make", "model", mode="after") @classmethod def _lowercase(cls, v: str) -> str: return v.lower() @field_validator("variant", mode="after") @classmethod def _slug_safe_variant(cls, v: Optional[str]) -> Optional[str]: if v is None: return None v = v.strip().lower() if not v: return None if "/" in v or any(c.isspace() for c in v): raise ValueError( f"variant '{v}' must not contain '/' or whitespace; " "use a short slug-safe label like 'steady' or 'warmup'" ) return v @classmethod def from_config(cls, config: ModelConfig) -> Vehicle: return cls( vehicle_description=config.vehicle_description, powertrain_type=config.powertrain_type, make=config.make, model=config.model, year=config.year, variant=config.variant, mass_lbs=config.mass_lbs, fuel_type=config.fuel_type, drivetrain=config.drivetrain, engine=config.engine, trim=config.trim, )
[docs] class Contract(BaseModel): """A model's input/output contract — everything needed to interpret a prediction: the feature columns it consumes, the distance column, the energy target(s) it emits, how the raw estimator output maps to energy (``predict_method``), and the real-world correction applied afterward. """ feature_set: FeatureSet distance: DataColumn target: TargetSet predict_method: PredictMethodField = PredictMethod.RATE #: Multiplicative factor applied to predicted energy to correct for #: real-world conditions. Resolved on the source ``ModelConfig`` (defaulting #: from the powertrain type) and stored concretely here. real_world_adjustment_factor: float = 1.0 @field_validator("feature_set", mode="before") @classmethod def _coerce_feature_set(cls, v: object) -> object: if isinstance(v, list): return {"features": v} return v @field_validator("target", mode="before") @classmethod def _coerce_target(cls, v: object) -> object: if isinstance(v, DataColumn): return {"targets": [v]} if isinstance(v, list): return {"targets": v} return v @classmethod def from_config(cls, config: ModelConfig) -> Contract: return cls( feature_set=config.feature_set, distance=config.distance, target=config.target, predict_method=config.predict_method, real_world_adjustment_factor=config.real_world_adjustment_factor, )