Skip to content

Simulation-only plants

These plants carry detailed truth state that is intentionally excluded from Manta's differentiable estimation/control model.

manta.simulation.OCVCurve dataclass

OCVCurve(soc_points=(0.0, 0.1, 0.5, 0.9, 1.0), voltage_points=(3.0, 3.45, 3.68, 4.0, 4.2))

Calibrated piecewise-linear cell OCV curve.

Evaluation extrapolates outside [0, 1] rather than clamping. Normal pack stepping rejects an SOC transition outside that range, while direct evaluation remains useful for diagnosing corrupt restored state.

integral

integral(soc)

Integral of OCV dSOC from zero to soc (V).

Source code in manta/simulation/battery.py
def integral(self, soc: float) -> float:
    """Integral of OCV dSOC from zero to ``soc`` (V)."""
    soc = _finite(soc, name="soc")
    xs, ys = self.soc_points, self.voltage_points
    if soc < 0.0:
        slope = (ys[1] - ys[0]) / (xs[1] - xs[0])
        return ys[0] * soc + 0.5 * slope * soc * soc
    total = 0.0
    for index in range(len(xs) - 1):
        left, right = xs[index], xs[index + 1]
        if soc <= left:
            break
        end = min(soc, right)
        width = end - left
        slope = (ys[index + 1] - ys[index]) / (right - left)
        total += ys[index] * width + 0.5 * slope * width * width
        if soc <= right:
            return total
    if soc > 1.0:
        width = soc - 1.0
        slope = (ys[-1] - ys[-2]) / (xs[-1] - xs[-2])
        total += ys[-1] * width + 0.5 * slope * width * width
    return total

manta.simulation.BatteryCellFaults dataclass

BatteryCellFaults(open_cell=False, short_cell=False, high_resistance=False, capacity_loss_fraction=0.0, forced_overtemperature=False)

Independent, composable fault injection for one simulation step.

manta.simulation.BatteryCellState dataclass

BatteryCellState(soc=1.0)

manta.simulation.BatteryCell dataclass

BatteryCell(usable_capacity_ah=20.0, internal_resistance=0.003, short_resistance=0.0005, high_resistance_multiplier=10.0, overtemperature_resistance_multiplier=2.0, reference_temperature=298.15, maximum_temperature=333.15, resistance_temperature_coefficient=0.003, maximum_short_current=1000.0, self_discharge_current=0.0, self_discharge_log_sigma=0.0, ocv_curve=OCVCurve())

Configuration for one cell; mutable SOC lives in BatteryPackState.

manta.simulation.PassiveBalancer dataclass

PassiveBalancer(cell_index, resistance=100.0, current_limit=math.inf)

One BMS-commanded passive shunt channel.

manta.simulation.BMSPlant

Commanded contactor and trip latch, without protection policy.

manta.simulation.BatteryStepInput dataclass

BatteryStepInput(requested_series_current, cell_temperatures, cell_faults, balance_enabled, contactor_command=True, trip_command=False, reset_command=False)

Complete deterministic input for one battery-plant tick.

manta.simulation.BatteryTelemetry dataclass

BatteryTelemetry(terminal_voltage, pack_ocv, requested_series_current, series_current, contactor_closed, tripped, cell_soc, cell_ocv, cell_terminal_voltage, cell_current, balance_current, cell_loss_power, balance_loss_power, pack_loss_power, chemical_power, output_power, energy_residual, minimum_soc, maximum_soc, soc_imbalance, any_open_cell, any_shorted_cell, any_overtemperature)

supply_inputs

supply_inputs(supply_name)

Control overlay for A3's ExternalDCSupply Manta boundary.

Source code in manta/simulation/battery.py
def supply_inputs(self, supply_name: str) -> dict[str, float]:
    """Control overlay for A3's ``ExternalDCSupply`` Manta boundary."""
    return {f"{supply_name}.supplied_voltage": self.terminal_voltage}

thermal_inputs

thermal_inputs(thermal_names)

Map per-cell losses into existing ThermalMass.heat_input.

Source code in manta/simulation/battery.py
def thermal_inputs(self, thermal_names: Sequence[str]) -> dict[str, float]:
    """Map per-cell losses into existing ``ThermalMass.heat_input``."""
    if len(thermal_names) != len(self.cell_loss_power):
        raise ValueError("thermal_names must contain one name per cell")
    return {f"{name}.heat_input": cell_heat + balance_heat
            for name, cell_heat, balance_heat in zip(
                thermal_names, self.cell_loss_power,
                self.balance_loss_power)}

manta.simulation.SeriesBatteryPack

SeriesBatteryPack(cells, *, initial_soc=1.0, balancers=(), seed=0)

Simulation-only series battery pack with explicit snapshot/replay.

Source code in manta/simulation/battery.py
def __init__(self, cells: Sequence[BatteryCell], *,
             initial_soc: Sequence[float] | float = 1.0,
             balancers: Sequence[PassiveBalancer] = (),
             seed: int = 0) -> None:
    if isinstance(cells, (str, bytes)) or not isinstance(cells, Sequence):
        raise TypeError("cells must be a BatteryCell sequence")
    self.cells = tuple(cells)
    if not self.cells or any(not isinstance(cell, BatteryCell)
                             for cell in self.cells):
        raise ValueError("cells must contain at least one BatteryCell")
    self.balancers = tuple(balancers)
    if any(not isinstance(item, PassiveBalancer) for item in self.balancers):
        raise TypeError("balancers must all be PassiveBalancer")
    if any(item.cell_index >= len(self.cells) for item in self.balancers):
        raise ValueError("balancer cell index is outside the pack")
    indices = [item.cell_index for item in self.balancers]
    if len(indices) != len(set(indices)):
        raise ValueError("only one passive balancer per cell is supported")
    if isinstance(initial_soc, Real) and not isinstance(initial_soc, bool):
        socs = (float(initial_soc),) * len(self.cells)
    else:
        socs = _tuple(initial_soc, name="initial_soc")
    if len(socs) != len(self.cells):
        raise ValueError("initial_soc must contain one value per cell")
    self._initial_state = BatteryPackState(
        cells=tuple(BatteryCellState(soc) for soc in socs))
    self._state = self._initial_state
    if isinstance(seed, bool) or not isinstance(seed, int):
        raise TypeError("seed must be an integer")
    self._seed = seed
    self._rng = np.random.default_rng(seed)
    self._last: BatteryTelemetry | None = None

preview

preview(inputs)

Evaluate terminal conditions without changing state or RNG.

Source code in manta/simulation/battery.py
def preview(self, inputs: BatteryStepInput) -> BatteryTelemetry:
    """Evaluate terminal conditions without changing state or RNG."""
    return self._evaluate(inputs, advance=False)

step

step(dt, inputs)

Advance exactly one explicit simulation tick.

Source code in manta/simulation/battery.py
def step(self, dt: float, inputs: BatteryStepInput) -> BatteryTelemetry:
    """Advance exactly one explicit simulation tick."""
    rng_state = copy.deepcopy(self._rng.bit_generator.state)
    try:
        return self._evaluate(inputs, advance=True, dt=dt)
    except Exception:
        # Invalid steps are atomic: neither physical state nor stochastic
        # stream advances, so correcting the input and retrying replays.
        self._rng.bit_generator.state = rng_state
        raise