Source code for routee.powertrain.core.features
from __future__ import annotations
from typing import Dict, List, Optional
from pydantic import (
BaseModel,
Field,
SerializationInfo,
field_validator,
model_serializer,
model_validator,
)
[docs]
class Constraints(BaseModel):
lower: Optional[float] = None
upper: Optional[float] = None
@model_validator(mode="after")
def _check_bounds(self) -> Constraints:
if (
self.lower is not None
and self.upper is not None
and self.lower >= self.upper
):
raise ValueError("lower bound must be less than upper bound")
return self
[docs]
class DataColumn(BaseModel):
name: str
units: str
dtype: str = "float32"
constraints: Constraints = Field(default_factory=Constraints)
@field_validator("name")
@classmethod
def _name_no_ampersand(cls, v: str) -> str:
if "&" in v:
raise ValueError("feature name cannot contain '&'")
return v
FeatureSetId = str
[docs]
def feature_names_to_id(feature_names: List[str]) -> FeatureSetId:
"""
Returns a string that uniquely identifies a feature set.
The names are sorted to provide a consistent id.
"""
sorted_names = sorted(feature_names)
return "&".join(sorted_names)
[docs]
def feature_id_to_names(feature_id: FeatureSetId) -> List[str]:
"""
Returns a list of feature names from a feature set id.
"""
return feature_id.split("&")
[docs]
class FeatureSet(BaseModel):
features: List[DataColumn]
@field_validator("features", mode="before")
@classmethod
def _wrap_single(cls, v: object) -> object:
if isinstance(v, DataColumn):
return [v]
return v
@model_serializer
def _serialize(self, info: SerializationInfo) -> list:
# Serialize as a bare list of feature columns (no nested "features" key);
# a model holds a single feature set, so the wrapper adds no information.
return [f.model_dump(mode=info.mode) for f in self.features]
def __repr__(self) -> str:
summary_lines = []
for feature in self.features:
summary_lines.append(f"{feature.name} ({feature.units})")
return "\n".join(summary_lines)
@property
def features_id(self) -> FeatureSetId:
"""
Returns a string that uniquely identifies this feature set.
The names are sorted to provide a consistent id.
"""
return feature_names_to_id(self.feature_name_list)
@property
def feature_map(self) -> Dict[str, DataColumn]:
return {f.name: f for f in self.features}
@property
def feature_name_list(self) -> List[str]:
"""
Returns a list of feature names in the order they
appear in the feature set.
Order is important since the underlying estimator might
expect it.
"""
return [f.name for f in self.features]
[docs]
class TargetSet(BaseModel):
targets: List[DataColumn]
@field_validator("targets", mode="before")
@classmethod
def _wrap_single(cls, v: object) -> object:
if isinstance(v, DataColumn):
return [v]
return v
@model_serializer
def _serialize(self, info: SerializationInfo) -> list:
# Serialize as a bare list of target columns (no nested "targets" key).
return [t.model_dump(mode=info.mode) for t in self.targets]
@property
def target_map(self) -> Dict[str, DataColumn]:
return {t.name: t for t in self.targets}
@property
def target_name_list(self) -> List[str]:
return [t.name for t in self.targets]
@property
def target_rate_name_list(self) -> List[str]:
return [f"{t.name}_rate" for t in self.targets]