from typing import Optional, Tuple
import numpy as np
import numpy.typing as npt
from pydantic import BaseModel, ConfigDict, Field, computed_field
from ..compsim.models import CompetitionData
from .models import SweepParamConfig
[docs]
class EnergyGridConfig(BaseModel):
"""Configuration for energy-constrained grid characterization.
Sweeps two design variables on a regular grid. At each grid point,
coast_trigger is solved via bisection so that 22-lap endurance energy
equals (pack capacity - buffer).
"""
model_config = ConfigDict(frozen=True, extra="forbid")
var_1: SweepParamConfig = Field(description="First grid variable (x-axis)")
var_2: SweepParamConfig = Field(description="Second grid variable (y-axis)")
coast_trigger_bounds: Tuple[float, float] = Field(
default=(5.0, 50.0),
description="Search range for coast_trigger bisection (m/s)",
)
capacity_buffer_kwh: float = Field(
default=0.1,
description="Energy safety margin subtracted from pack capacity (kWh)",
ge=0,
)
bisection_tol: float = Field(
default=0.05,
description="Convergence tolerance on coast_trigger (m/s)",
gt=0,
)
bisection_max_iter: int = Field(
default=40,
description="Maximum bisection iterations per grid point",
gt=0,
)
[docs]
class EnergyGridProcessOutput(BaseModel):
"""Frozen output returned from each worker process."""
model_config = ConfigDict(frozen=True)
x_idx: int = Field(description="Grid x index")
y_idx: int = Field(description="Grid y index")
optimal_coast_trigger: float = Field(description="Solved coast_trigger (m/s)")
feasible: bool = Field(description="Whether energy budget is satisfiable")
total_points: float = Field(description="Sum of all event points")
accel_pts: float = Field(default=0)
skidpad_pts: float = Field(default=0)
autoX_pts: float = Field(default=0)
endurance_pts: float = Field(default=0)
efficiency_pts: float = Field(default=0)
accel_t: float = Field(default=0)
skidpad_t: float = Field(default=0)
autoX_t: float = Field(default=0)
endurance_t: float = Field(default=0)
net_energy_kwh: float = Field(default=0, description="22-lap net energy (kWh)")
capacity_kwh: float = Field(default=0, description="Available pack capacity (kWh)")
vehicle_mass: float = Field(default=0, description="Effective vehicle mass (kg)")
bisection_iters: int = Field(default=0, description="Bisection iterations used")
warnings: str = Field(default="")
error: Optional[str] = Field(default=None)
[docs]
class EnergyGridData(BaseModel):
"""Collected 2D grid results for all metrics."""
model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True)
var_1_name: str = Field(description="First variable name (x-axis)")
var_1_list: npt.NDArray[np.float64] = Field(description="First variable values")
var_2_name: str = Field(description="Second variable name (y-axis)")
var_2_list: npt.NDArray[np.float64] = Field(description="Second variable values")
# Nominal pack parameters for kg to kWh axis conversion
nominal_pack_weight: float = Field(description="Baseline pack weight (kg)")
nominal_capacity: float = Field(description="Baseline pack capacity (kWh)")
energy_density: float = Field(description="Pack energy density (kWh/kg)")
total_pts: npt.NDArray[np.float64] = Field(
description="2D list of total_pts, corresponding to grid generated from var_1_list x var_2_list"
)
accel_pts: npt.NDArray[np.float64] = Field(
description="2D list of accel event points, same shape as total_pts"
)
skidpad_pts: npt.NDArray[np.float64] = Field(
description="2D list of skidpad event points, same shape as total_pts"
)
autoX_pts: npt.NDArray[np.float64] = Field(
description="2D list of autoX event points, same shape as total_pts"
)
endurance_pts: npt.NDArray[np.float64] = Field(
description="2D list of endurance event points, same shape as total_pts"
)
efficiency_pts: npt.NDArray[np.float64] = Field(
description="2D list of efficiency event points, same shape as total_pts"
)
accel_t: npt.NDArray[np.float64] = Field(
description="2D list of accel event times, same shape as total_pts"
)
skidpad_t: npt.NDArray[np.float64] = Field(
description="2D list of skidpad event times, same shape as total_pts"
)
autoX_t: npt.NDArray[np.float64] = Field(
description="2D list of autoX event times, same shape as total_pts"
)
endurance_t: npt.NDArray[np.float64] = Field(
description="2D list of endurance event times, same shape as total_pts"
)
optimal_coast_trigger: npt.NDArray[np.float64] = Field(
description="2D list of optimal coast triggers, same shape as total_pts"
)
net_energy_kwh: npt.NDArray[np.float64] = Field(
description="2D list of net energy (kWh), same shape as total_pts"
)
capacity_kwh: npt.NDArray[np.float64] = Field(
description="2D list of available pack capacity (kWh), same shape as total_pts"
)
vehicle_mass: npt.NDArray[np.float64] = Field(
description="2D list of effective vehicle masses (kg), same shape as total_pts"
)
feasible: npt.NDArray[np.bool_] = Field(
description="2D list of feasibility flags, same shape as total_pts"
)
bisection_iters: npt.NDArray[np.int_] = Field(
description="2D list of bisection iterations, same shape as total_pts"
)
[docs]
@classmethod
def create(
cls,
var_1_name: str,
var_1_list: np.ndarray,
var_2_name: str,
var_2_list: np.ndarray,
nominal_pack_weight: float,
nominal_capacity: float,
energy_density: float,
) -> "EnergyGridData":
d1, d2 = len(var_1_list), len(var_2_list)
z = lambda: np.zeros((d1, d2), dtype=float)
return cls(
var_1_name=var_1_name,
var_1_list=var_1_list,
var_2_name=var_2_name,
var_2_list=var_2_list,
nominal_pack_weight=nominal_pack_weight,
nominal_capacity=nominal_capacity,
energy_density=energy_density,
total_pts=z(),
accel_pts=z(),
skidpad_pts=z(),
autoX_pts=z(),
endurance_pts=z(),
efficiency_pts=z(),
accel_t=z(),
skidpad_t=z(),
autoX_t=z(),
endurance_t=z(),
optimal_coast_trigger=z(),
net_energy_kwh=z(),
capacity_kwh=z(),
vehicle_mass=z(),
feasible=np.zeros((d1, d2), dtype=bool),
bisection_iters=np.zeros((d1, d2), dtype=int),
)