Skip to content

Battery API

The simses.battery module holds the top-level Battery simulator, its mutable state, the CellType ABC that chemistry implementations subclass, the per-cell property dataclasses, cell-format presets, and current-derating strategies. For the system-level framing — ECM, series-parallel scaling, step() lifecycle — see the Battery concept page.

Battery system

Top-level simulator. Composes a CellType with a (serial, parallel) circuit, solves the ECM for the equilibrium current at each step, clamps to hard limits, and optionally applies derating and degradation.

simses.battery.battery.Battery

Battery system composed of cells in a series-parallel circuit.

Models a pack of identical cells arranged as (serial, parallel) using an equivalent-circuit model (ECM): terminal voltage is OCV(SOC,T) + hysteresis + Rint × I. Each call to :meth:step solves the equilibrium current for a requested power setpoint, clamps it to the hard limits (C-rate, voltage window, SOC window), optionally applies a :class:~simses.battery.derating.CurrentDerating strategy, and updates the state.

Composition: a :class:~simses.battery.cell.CellType supplies the electrochemistry and per-cell physical properties; an optional :class:~simses.degradation.degradation.DegradationModel accumulates capacity fade and resistance rise; an optional CurrentDerating reduces current in voltage or temperature derating zones.

Sign convention: positive power / current = charging, negative = discharging.

Source code in src/simses/battery/battery.py
class Battery:
    """Battery system composed of cells in a series-parallel circuit.

    Models a pack of identical cells arranged as ``(serial, parallel)`` using
    an equivalent-circuit model (ECM): terminal voltage is
    ``OCV(SOC,T) + hysteresis + Rint × I``. Each call to :meth:`step` solves
    the equilibrium current for a requested power setpoint, clamps it to the
    hard limits (C-rate, voltage window, SOC window), optionally applies a
    :class:`~simses.battery.derating.CurrentDerating` strategy, and updates
    the state.

    Composition: a :class:`~simses.battery.cell.CellType` supplies the
    electrochemistry and per-cell physical properties; an optional
    :class:`~simses.degradation.degradation.DegradationModel` accumulates
    capacity fade and resistance rise; an optional ``CurrentDerating``
    reduces current in voltage or temperature derating zones.

    Sign convention: positive power / current = charging, negative =
    discharging.
    """

    def __init__(
        self,
        cell: CellType,
        circuit: tuple[int, int],  # (s, p)
        initial_states: dict,
        soc_limits: tuple[float, float] = (0.0, 1.0),  # in p.u.
        degradation: DegradationModel | bool | None = None,
        derating: CurrentDerating | None = None,
        effective_cooling_area: float = 1.0,
    ) -> None:
        """
        Args:
            cell: Cell model defining OCV, Rint, and physical parameters.
            circuit: Series-parallel configuration as ``(s, p)``.
            initial_states: Dict with keys ``start_soc``, ``start_T``, and
                optionally ``start_soh_Q`` / ``start_soh_R``.
            soc_limits: ``(soc_min, soc_max)`` operating window in p.u.
            degradation: Degradation model, or ``True`` to use the cell's
                default (fresh battery, no prior aging history), or ``None`` /
                ``False`` to disable. To warm-start from a known degradation
                state, pass an explicit ``DegradationModel`` constructed with
                an ``initial_state``.
            derating: Optional current-derating strategy applied after hard
                limits.
            effective_cooling_area: Fraction of the total cell surface area
                that participates in heat exchange with the environment, in
                p.u. (default 1.0 = full surface area).  Use values below 1
                to model packs where only a portion of each cell face is
                exposed to coolant, e.g. 0.5 for a two-sided cooling plate
                that covers half the cell surface.
        """
        if degradation is False:
            degradation = None
        if degradation is True:
            initial_soc = initial_states["start_soc"]
            degradation = cell.default_degradation_model(initial_soc)
            if degradation is None:
                raise ValueError(
                    f"{type(cell).__name__} has no default degradation model. "
                    "Pass an explicit DegradationModel or use degradation=None."
                )
        self.cell = cell
        self.circuit = circuit
        self.soc_limits = soc_limits
        self.degradation = degradation
        self.derating = derating
        self.effective_cooling_area = effective_cooling_area
        self.state = self.initialize_state(**initial_states)

    def initialize_state(
        self, start_soc: float, start_T: float, start_soh_Q: float = 1.0, start_soh_R: float = 1.0
    ) -> BatteryState:
        """Create the initial battery state from starting conditions.

        Sets SOC, temperature, and SoH from the arguments, then evaluates
        OCV, hysteresis, Rint, and entropic coefficient at that initial
        state so the returned object is consistent before the first
        :meth:`step` call.

        Args:
            start_soc: Initial state of charge in p.u.
            start_T: Initial cell temperature in °C.
            start_soh_Q: Initial capacity SoH in p.u. (default 1.0 = fresh).
            start_soh_R: Initial resistance SoH in p.u. (default 1.0 = fresh).

        Returns:
            A fully-initialised :class:`BatteryState`.
        """
        state = BatteryState(
            v=0,  # uninitialized
            i=0,  # uninitialized
            T=start_T,
            power=0,
            power_setpoint=0,
            loss=0,
            heat=0,
            soc=start_soc,
            ocv=0,  # uninitialized
            hys=0,  # uninitialized
            entropy=0,  # uninitialized
            is_charge=True,
            rint=0,  # uninitialized
            soh_Q=start_soh_Q,
            soh_R=start_soh_R,
            i_max_charge=0.0,
            i_max_discharge=0.0,
        )
        state.ocv = state.v = self.open_circuit_voltage(state)
        state.hys = self.hysteresis_voltage(state)
        state.rint = self.internal_resistance(state)
        state.entropy = self.entropic_coefficient(state)
        return state

    def step(self, power_setpoint: float, dt: float) -> None:
        """Advance the battery state by one timestep.

        If the battery cannot fulfil the power setpoint due to hard limits
        (C-rate, voltage window, SOC window) or optional derating, the
        current is curtailed and ``state.power`` reflects what was actually
        delivered — not the original setpoint.

        Args:
            power_setpoint: Requested power in W. Positive = charging,
                negative = discharging.
            dt: Timestep in seconds.
        """
        state: BatteryState = self.state
        state.is_charge = power_setpoint > 0.0

        # --- phase 1: refresh derived cell properties from current soc/T ---
        # ocv, hys, rint are derived from inputs (soc, T, soh_R) that do not
        # change during this method, so updating them here is safe and ensures
        # all calculations — including derating — use consistent current values.
        ocv = state.ocv = self.open_circuit_voltage(state)
        hys = state.hys = self.hysteresis_voltage(state)
        rint = state.rint = self.internal_resistance(state)
        entropy = state.entropy = self.entropic_coefficient(state)
        Q = self.capacity(state)

        # 1. Calculate equilibrium current to meet power setpoint
        i = self.equilibrium_current(power_setpoint, ocv, hys, rint)

        # 2. Calculate hard current limits (C-rate, voltage, SOC)
        i_max_charge, i_max_discharge = self.calculate_max_currents(state, dt, ocv, hys, rint, Q)

        # 3. Curtail solved current to hard limits
        if i > 0:
            i = min(i, i_max_charge)
        elif i < 0:
            i = max(i, i_max_discharge)

        # 4. Apply derating (optional).
        # i_max_charge / i_max_discharge are only updated when derating actually reduces i,
        # so that the reported limits reflect the hard limits during normal operation and only
        # drop when the battery is genuinely in the derating zone.
        if self.derating is not None:
            i_derate = self.derating.derate(i, state)
            if i > 0 and i_derate < i:
                i = i_derate
                i_max_charge = min(i_max_charge, i_derate)
            elif i < 0 and i_derate > i:
                i = i_derate
                i_max_discharge = max(i_max_discharge, i_derate)

        # update soc
        (soc_min, soc_max) = self.soc_limits
        soc = state.soc + i * dt / Q / 3600
        soc = max(soc_min, min(soc, soc_max))

        # check current direction, maintain previous state if in rest
        is_charge = state.is_charge if i == 0 else i > 0

        # update terminal voltage and power
        v = ocv + hys + rint * i
        power = v * i

        # update losses
        loss_irr = (v - ocv) * i  # irreversible losses
        loss_rev = entropy * (state.T + 273.15) * i  # reversible losses (T must be absolute)
        heat = loss_irr + loss_rev  # internal heat generation

        # --- phase 2: write output state ---
        state.v = v
        state.i = i
        state.power = power
        state.power_setpoint = power_setpoint
        state.loss = loss_irr
        state.heat = heat
        state.soc = soc
        state.is_charge = is_charge
        state.i_max_charge = i_max_charge
        state.i_max_discharge = i_max_discharge

        if self.degradation is not None:
            self.degradation.step(self.state, dt)  # updates state.soh_Q and state.soh_R

    def equilibrium_current(self, power_setpoint: float, ocv: float, hys: float, rint: float) -> float:
        """Solve the ECM for the current that meets a power setpoint.

        Solves the quadratic ``P = I × (OCV + hys + Rint × I)`` for ``I``
        and returns the physically meaningful (positive-discriminant) root.

        Args:
            power_setpoint: Target power in W.
            ocv: System open-circuit voltage in V.
            hys: System hysteresis voltage in V.
            rint: System internal resistance in Ω.

        Returns:
            Equilibrium current in A. Positive = charging, negative =
            discharging.
        """
        ocv = ocv + hys  # include hysteresis in equilibrium calculation
        if power_setpoint == 0.0:
            return 0.0
        return -(ocv - math.sqrt(ocv**2 + 4 * rint * power_setpoint)) / (2 * rint)

    def calculate_max_currents(
        self, state: BatteryState, dt: float, ocv: float, hys: float, rint: float, Q: float
    ) -> tuple[float, float]:
        """Return the allowed current window for the next timestep.

        Each bound is the most restrictive of three limits: the C-rate
        limit (from cell ``max_charge_rate`` / ``max_discharge_rate``), the
        voltage limit (current that would drive terminal voltage to
        ``max_voltage`` or ``min_voltage`` this step), and the SOC limit
        (current that would drive SOC to the configured ``soc_limits``
        this step).

        Args:
            state: Current battery state (reads ``soc``).
            dt: Timestep in seconds.
            ocv: System open-circuit voltage in V.
            hys: System hysteresis voltage in V.
            rint: System internal resistance in Ω.
            Q: Current capacity in Ah (scaled by ``soh_Q``).

        Returns:
            Tuple ``(i_max_charge, i_max_discharge)`` in A. Charge bound
            is non-negative; discharge bound is non-positive.
        """
        (soc_min, soc_max) = self.soc_limits
        soc = state.soc

        # charge (all three values are positive; min = most restrictive)
        i_max_charge = min(
            self.max_charge_current,  # C-rate limit
            (self.max_voltage - ocv - hys) / rint,  # voltage limit
            (soc_max - soc) * Q / (dt / 3600),  # SOC limit
        )
        # discharge (all three values are negative; max = least negative = most restrictive)
        i_max_discharge = max(
            -self.max_discharge_current,  # C-rate limit
            (self.min_voltage - ocv - hys) / rint,  # voltage limit
            (soc_min - soc) * Q / (dt / 3600),  # SOC limit
        )
        return i_max_charge, i_max_discharge

    ## electrical properties
    def open_circuit_voltage(self, state: BatteryState) -> float:
        """Return the system-level open-circuit voltage in V."""
        (serial, parallel) = self.circuit

        return self.cell.open_circuit_voltage(state) * serial

    def hysteresis_voltage(self, state: BatteryState) -> float:
        """Return the system-level hysteresis voltage in V."""
        (serial, parallel) = self.circuit

        return self.cell.hysteresis_voltage(state) * serial

    def internal_resistance(self, state: BatteryState) -> float:
        """Return the system-level internal resistance in Ohms, scaled by SoH."""
        (serial, parallel) = self.circuit

        # state.i = state.i / parallel # <- should be scaled to the cell
        return self.cell.internal_resistance(state) / parallel * serial * state.soh_R

    def entropic_coefficient(self, state: BatteryState) -> float:
        """Return the system-level entropic coefficient in V/K."""
        (serial, parallel) = self.circuit
        return self.cell.entropic_coefficient(state) * serial

    def capacity(self, state: BatteryState) -> float:
        """Return the current capacity in Ah, scaled by SoH."""
        return self.nominal_capacity * state.soh_Q

    def energy_capacity(self, state: BatteryState) -> float:
        """Return the current energy capacity in Wh, scaled by SoH."""
        return self.nominal_energy_capacity * state.soh_Q

    @property
    def nominal_capacity(self) -> float:
        """Nominal capacity of the battery system in Ah."""
        (serial, parallel) = self.circuit

        return self.cell.electrical.nominal_capacity * parallel

    @property
    def nominal_voltage(self) -> float:
        """Nominal voltage of the battery system in V."""
        (serial, parallel) = self.circuit

        return self.cell.electrical.nominal_voltage * serial

    @property
    def nominal_energy_capacity(self) -> float:
        """Nominal energy capacity of the battery system in Wh."""
        return self.nominal_capacity * self.nominal_voltage

    @property
    def min_voltage(self) -> float:
        """Minimum allowed voltage of the battery system in V."""
        (serial, parallel) = self.circuit

        return self.cell.electrical.min_voltage * serial

    @property
    def max_voltage(self) -> float:
        """Maximum allowed voltage of the battery system in V."""
        (serial, parallel) = self.circuit

        return self.cell.electrical.max_voltage * serial

    @property
    def max_charge_current(self) -> float:
        """Maximum allowed charge current in A."""
        (serial, parallel) = self.circuit

        return self.cell.electrical.nominal_capacity * self.cell.electrical.max_charge_rate * parallel

    @property
    def max_discharge_current(self) -> float:
        """Maximum allowed discharge current in A."""
        (serial, parallel) = self.circuit

        return self.cell.electrical.nominal_capacity * self.cell.electrical.max_discharge_rate * parallel

    @property
    def coulomb_efficiency(self) -> float:
        """Coulomb efficiency of the cell in p.u."""
        return self.cell.electrical.coulomb_efficiency

    ## thermal properties
    @property
    def thermal_capacity(self) -> float:
        """Total thermal capacity of the battery system in J/K."""
        (serial, parallel) = self.circuit

        return self.cell.thermal.specific_heat * self.cell.thermal.mass * serial * parallel

    @property
    def convection_coefficient(self) -> float:
        """Convection coefficient of the cell in W/m2K."""
        return self.cell.thermal.convection_coefficient

    @property
    def thermal_resistance(self) -> float:
        """Thermal resistance of the battery system in K/W."""
        return 1 / (self.convection_coefficient * self.area)

    @property
    def min_temperature(self) -> float:
        """Minimum allowed temperature in °C."""
        return self.cell.thermal.min_temperature

    @property
    def max_temperature(self) -> float:
        """Maximum allowed temperature in °C."""
        return self.cell.thermal.max_temperature

    @property
    def area(self) -> float:
        """Effective cooling area of the pack in m².

        Equals the per-cell surface area (``cell.format.area``) scaled by the
        ``effective_cooling_area`` fraction and the pack size
        ``(serial × parallel)``. Used by :attr:`thermal_resistance` to compute
        the convective coupling between the pack and the thermal environment.
        """
        (serial, parallel) = self.circuit

        return self.cell.format.area * self.effective_cooling_area * serial * parallel

nominal_capacity property

Nominal capacity of the battery system in Ah.

nominal_voltage property

Nominal voltage of the battery system in V.

nominal_energy_capacity property

Nominal energy capacity of the battery system in Wh.

min_voltage property

Minimum allowed voltage of the battery system in V.

max_voltage property

Maximum allowed voltage of the battery system in V.

max_charge_current property

Maximum allowed charge current in A.

max_discharge_current property

Maximum allowed discharge current in A.

coulomb_efficiency property

Coulomb efficiency of the cell in p.u.

thermal_capacity property

Total thermal capacity of the battery system in J/K.

convection_coefficient property

Convection coefficient of the cell in W/m2K.

thermal_resistance property

Thermal resistance of the battery system in K/W.

min_temperature property

Minimum allowed temperature in °C.

max_temperature property

Maximum allowed temperature in °C.

area property

Effective cooling area of the pack in m².

Equals the per-cell surface area (cell.format.area) scaled by the effective_cooling_area fraction and the pack size (serial × parallel). Used by :attr:thermal_resistance to compute the convective coupling between the pack and the thermal environment.

__init__(cell, circuit, initial_states, soc_limits=(0.0, 1.0), degradation=None, derating=None, effective_cooling_area=1.0)

Parameters:

Name Type Description Default
cell CellType

Cell model defining OCV, Rint, and physical parameters.

required
circuit tuple[int, int]

Series-parallel configuration as (s, p).

required
initial_states dict

Dict with keys start_soc, start_T, and optionally start_soh_Q / start_soh_R.

required
soc_limits tuple[float, float]

(soc_min, soc_max) operating window in p.u.

(0.0, 1.0)
degradation DegradationModel | bool | None

Degradation model, or True to use the cell's default (fresh battery, no prior aging history), or None / False to disable. To warm-start from a known degradation state, pass an explicit DegradationModel constructed with an initial_state.

None
derating CurrentDerating | None

Optional current-derating strategy applied after hard limits.

None
effective_cooling_area float

Fraction of the total cell surface area that participates in heat exchange with the environment, in p.u. (default 1.0 = full surface area). Use values below 1 to model packs where only a portion of each cell face is exposed to coolant, e.g. 0.5 for a two-sided cooling plate that covers half the cell surface.

1.0
Source code in src/simses/battery/battery.py
def __init__(
    self,
    cell: CellType,
    circuit: tuple[int, int],  # (s, p)
    initial_states: dict,
    soc_limits: tuple[float, float] = (0.0, 1.0),  # in p.u.
    degradation: DegradationModel | bool | None = None,
    derating: CurrentDerating | None = None,
    effective_cooling_area: float = 1.0,
) -> None:
    """
    Args:
        cell: Cell model defining OCV, Rint, and physical parameters.
        circuit: Series-parallel configuration as ``(s, p)``.
        initial_states: Dict with keys ``start_soc``, ``start_T``, and
            optionally ``start_soh_Q`` / ``start_soh_R``.
        soc_limits: ``(soc_min, soc_max)`` operating window in p.u.
        degradation: Degradation model, or ``True`` to use the cell's
            default (fresh battery, no prior aging history), or ``None`` /
            ``False`` to disable. To warm-start from a known degradation
            state, pass an explicit ``DegradationModel`` constructed with
            an ``initial_state``.
        derating: Optional current-derating strategy applied after hard
            limits.
        effective_cooling_area: Fraction of the total cell surface area
            that participates in heat exchange with the environment, in
            p.u. (default 1.0 = full surface area).  Use values below 1
            to model packs where only a portion of each cell face is
            exposed to coolant, e.g. 0.5 for a two-sided cooling plate
            that covers half the cell surface.
    """
    if degradation is False:
        degradation = None
    if degradation is True:
        initial_soc = initial_states["start_soc"]
        degradation = cell.default_degradation_model(initial_soc)
        if degradation is None:
            raise ValueError(
                f"{type(cell).__name__} has no default degradation model. "
                "Pass an explicit DegradationModel or use degradation=None."
            )
    self.cell = cell
    self.circuit = circuit
    self.soc_limits = soc_limits
    self.degradation = degradation
    self.derating = derating
    self.effective_cooling_area = effective_cooling_area
    self.state = self.initialize_state(**initial_states)

initialize_state(start_soc, start_T, start_soh_Q=1.0, start_soh_R=1.0)

Create the initial battery state from starting conditions.

Sets SOC, temperature, and SoH from the arguments, then evaluates OCV, hysteresis, Rint, and entropic coefficient at that initial state so the returned object is consistent before the first :meth:step call.

Parameters:

Name Type Description Default
start_soc float

Initial state of charge in p.u.

required
start_T float

Initial cell temperature in °C.

required
start_soh_Q float

Initial capacity SoH in p.u. (default 1.0 = fresh).

1.0
start_soh_R float

Initial resistance SoH in p.u. (default 1.0 = fresh).

1.0

Returns:

Type Description
BatteryState

A fully-initialised :class:BatteryState.

Source code in src/simses/battery/battery.py
def initialize_state(
    self, start_soc: float, start_T: float, start_soh_Q: float = 1.0, start_soh_R: float = 1.0
) -> BatteryState:
    """Create the initial battery state from starting conditions.

    Sets SOC, temperature, and SoH from the arguments, then evaluates
    OCV, hysteresis, Rint, and entropic coefficient at that initial
    state so the returned object is consistent before the first
    :meth:`step` call.

    Args:
        start_soc: Initial state of charge in p.u.
        start_T: Initial cell temperature in °C.
        start_soh_Q: Initial capacity SoH in p.u. (default 1.0 = fresh).
        start_soh_R: Initial resistance SoH in p.u. (default 1.0 = fresh).

    Returns:
        A fully-initialised :class:`BatteryState`.
    """
    state = BatteryState(
        v=0,  # uninitialized
        i=0,  # uninitialized
        T=start_T,
        power=0,
        power_setpoint=0,
        loss=0,
        heat=0,
        soc=start_soc,
        ocv=0,  # uninitialized
        hys=0,  # uninitialized
        entropy=0,  # uninitialized
        is_charge=True,
        rint=0,  # uninitialized
        soh_Q=start_soh_Q,
        soh_R=start_soh_R,
        i_max_charge=0.0,
        i_max_discharge=0.0,
    )
    state.ocv = state.v = self.open_circuit_voltage(state)
    state.hys = self.hysteresis_voltage(state)
    state.rint = self.internal_resistance(state)
    state.entropy = self.entropic_coefficient(state)
    return state

step(power_setpoint, dt)

Advance the battery state by one timestep.

If the battery cannot fulfil the power setpoint due to hard limits (C-rate, voltage window, SOC window) or optional derating, the current is curtailed and state.power reflects what was actually delivered — not the original setpoint.

Parameters:

Name Type Description Default
power_setpoint float

Requested power in W. Positive = charging, negative = discharging.

required
dt float

Timestep in seconds.

required
Source code in src/simses/battery/battery.py
def step(self, power_setpoint: float, dt: float) -> None:
    """Advance the battery state by one timestep.

    If the battery cannot fulfil the power setpoint due to hard limits
    (C-rate, voltage window, SOC window) or optional derating, the
    current is curtailed and ``state.power`` reflects what was actually
    delivered — not the original setpoint.

    Args:
        power_setpoint: Requested power in W. Positive = charging,
            negative = discharging.
        dt: Timestep in seconds.
    """
    state: BatteryState = self.state
    state.is_charge = power_setpoint > 0.0

    # --- phase 1: refresh derived cell properties from current soc/T ---
    # ocv, hys, rint are derived from inputs (soc, T, soh_R) that do not
    # change during this method, so updating them here is safe and ensures
    # all calculations — including derating — use consistent current values.
    ocv = state.ocv = self.open_circuit_voltage(state)
    hys = state.hys = self.hysteresis_voltage(state)
    rint = state.rint = self.internal_resistance(state)
    entropy = state.entropy = self.entropic_coefficient(state)
    Q = self.capacity(state)

    # 1. Calculate equilibrium current to meet power setpoint
    i = self.equilibrium_current(power_setpoint, ocv, hys, rint)

    # 2. Calculate hard current limits (C-rate, voltage, SOC)
    i_max_charge, i_max_discharge = self.calculate_max_currents(state, dt, ocv, hys, rint, Q)

    # 3. Curtail solved current to hard limits
    if i > 0:
        i = min(i, i_max_charge)
    elif i < 0:
        i = max(i, i_max_discharge)

    # 4. Apply derating (optional).
    # i_max_charge / i_max_discharge are only updated when derating actually reduces i,
    # so that the reported limits reflect the hard limits during normal operation and only
    # drop when the battery is genuinely in the derating zone.
    if self.derating is not None:
        i_derate = self.derating.derate(i, state)
        if i > 0 and i_derate < i:
            i = i_derate
            i_max_charge = min(i_max_charge, i_derate)
        elif i < 0 and i_derate > i:
            i = i_derate
            i_max_discharge = max(i_max_discharge, i_derate)

    # update soc
    (soc_min, soc_max) = self.soc_limits
    soc = state.soc + i * dt / Q / 3600
    soc = max(soc_min, min(soc, soc_max))

    # check current direction, maintain previous state if in rest
    is_charge = state.is_charge if i == 0 else i > 0

    # update terminal voltage and power
    v = ocv + hys + rint * i
    power = v * i

    # update losses
    loss_irr = (v - ocv) * i  # irreversible losses
    loss_rev = entropy * (state.T + 273.15) * i  # reversible losses (T must be absolute)
    heat = loss_irr + loss_rev  # internal heat generation

    # --- phase 2: write output state ---
    state.v = v
    state.i = i
    state.power = power
    state.power_setpoint = power_setpoint
    state.loss = loss_irr
    state.heat = heat
    state.soc = soc
    state.is_charge = is_charge
    state.i_max_charge = i_max_charge
    state.i_max_discharge = i_max_discharge

    if self.degradation is not None:
        self.degradation.step(self.state, dt)  # updates state.soh_Q and state.soh_R

equilibrium_current(power_setpoint, ocv, hys, rint)

Solve the ECM for the current that meets a power setpoint.

Solves the quadratic P = I × (OCV + hys + Rint × I) for I and returns the physically meaningful (positive-discriminant) root.

Parameters:

Name Type Description Default
power_setpoint float

Target power in W.

required
ocv float

System open-circuit voltage in V.

required
hys float

System hysteresis voltage in V.

required
rint float

System internal resistance in Ω.

required

Returns:

Type Description
float

Equilibrium current in A. Positive = charging, negative =

float

discharging.

Source code in src/simses/battery/battery.py
def equilibrium_current(self, power_setpoint: float, ocv: float, hys: float, rint: float) -> float:
    """Solve the ECM for the current that meets a power setpoint.

    Solves the quadratic ``P = I × (OCV + hys + Rint × I)`` for ``I``
    and returns the physically meaningful (positive-discriminant) root.

    Args:
        power_setpoint: Target power in W.
        ocv: System open-circuit voltage in V.
        hys: System hysteresis voltage in V.
        rint: System internal resistance in Ω.

    Returns:
        Equilibrium current in A. Positive = charging, negative =
        discharging.
    """
    ocv = ocv + hys  # include hysteresis in equilibrium calculation
    if power_setpoint == 0.0:
        return 0.0
    return -(ocv - math.sqrt(ocv**2 + 4 * rint * power_setpoint)) / (2 * rint)

calculate_max_currents(state, dt, ocv, hys, rint, Q)

Return the allowed current window for the next timestep.

Each bound is the most restrictive of three limits: the C-rate limit (from cell max_charge_rate / max_discharge_rate), the voltage limit (current that would drive terminal voltage to max_voltage or min_voltage this step), and the SOC limit (current that would drive SOC to the configured soc_limits this step).

Parameters:

Name Type Description Default
state BatteryState

Current battery state (reads soc).

required
dt float

Timestep in seconds.

required
ocv float

System open-circuit voltage in V.

required
hys float

System hysteresis voltage in V.

required
rint float

System internal resistance in Ω.

required
Q float

Current capacity in Ah (scaled by soh_Q).

required

Returns:

Type Description
float

Tuple (i_max_charge, i_max_discharge) in A. Charge bound

float

is non-negative; discharge bound is non-positive.

Source code in src/simses/battery/battery.py
def calculate_max_currents(
    self, state: BatteryState, dt: float, ocv: float, hys: float, rint: float, Q: float
) -> tuple[float, float]:
    """Return the allowed current window for the next timestep.

    Each bound is the most restrictive of three limits: the C-rate
    limit (from cell ``max_charge_rate`` / ``max_discharge_rate``), the
    voltage limit (current that would drive terminal voltage to
    ``max_voltage`` or ``min_voltage`` this step), and the SOC limit
    (current that would drive SOC to the configured ``soc_limits``
    this step).

    Args:
        state: Current battery state (reads ``soc``).
        dt: Timestep in seconds.
        ocv: System open-circuit voltage in V.
        hys: System hysteresis voltage in V.
        rint: System internal resistance in Ω.
        Q: Current capacity in Ah (scaled by ``soh_Q``).

    Returns:
        Tuple ``(i_max_charge, i_max_discharge)`` in A. Charge bound
        is non-negative; discharge bound is non-positive.
    """
    (soc_min, soc_max) = self.soc_limits
    soc = state.soc

    # charge (all three values are positive; min = most restrictive)
    i_max_charge = min(
        self.max_charge_current,  # C-rate limit
        (self.max_voltage - ocv - hys) / rint,  # voltage limit
        (soc_max - soc) * Q / (dt / 3600),  # SOC limit
    )
    # discharge (all three values are negative; max = least negative = most restrictive)
    i_max_discharge = max(
        -self.max_discharge_current,  # C-rate limit
        (self.min_voltage - ocv - hys) / rint,  # voltage limit
        (soc_min - soc) * Q / (dt / 3600),  # SOC limit
    )
    return i_max_charge, i_max_discharge

open_circuit_voltage(state)

Return the system-level open-circuit voltage in V.

Source code in src/simses/battery/battery.py
def open_circuit_voltage(self, state: BatteryState) -> float:
    """Return the system-level open-circuit voltage in V."""
    (serial, parallel) = self.circuit

    return self.cell.open_circuit_voltage(state) * serial

hysteresis_voltage(state)

Return the system-level hysteresis voltage in V.

Source code in src/simses/battery/battery.py
def hysteresis_voltage(self, state: BatteryState) -> float:
    """Return the system-level hysteresis voltage in V."""
    (serial, parallel) = self.circuit

    return self.cell.hysteresis_voltage(state) * serial

internal_resistance(state)

Return the system-level internal resistance in Ohms, scaled by SoH.

Source code in src/simses/battery/battery.py
def internal_resistance(self, state: BatteryState) -> float:
    """Return the system-level internal resistance in Ohms, scaled by SoH."""
    (serial, parallel) = self.circuit

    # state.i = state.i / parallel # <- should be scaled to the cell
    return self.cell.internal_resistance(state) / parallel * serial * state.soh_R

entropic_coefficient(state)

Return the system-level entropic coefficient in V/K.

Source code in src/simses/battery/battery.py
def entropic_coefficient(self, state: BatteryState) -> float:
    """Return the system-level entropic coefficient in V/K."""
    (serial, parallel) = self.circuit
    return self.cell.entropic_coefficient(state) * serial

capacity(state)

Return the current capacity in Ah, scaled by SoH.

Source code in src/simses/battery/battery.py
def capacity(self, state: BatteryState) -> float:
    """Return the current capacity in Ah, scaled by SoH."""
    return self.nominal_capacity * state.soh_Q

energy_capacity(state)

Return the current energy capacity in Wh, scaled by SoH.

Source code in src/simses/battery/battery.py
def energy_capacity(self, state: BatteryState) -> float:
    """Return the current energy capacity in Wh, scaled by SoH."""
    return self.nominal_energy_capacity * state.soh_Q

State

Plain mutable dataclass mutated in place by Battery.step(). No methods — all logic is in Battery.

simses.battery.state.BatteryState dataclass

Dataclass representing the state of a battery.

float

Terminal voltage of the battery in volts (V).

i: float Current flowing into the battery in amperes (A). Positive for charging, negative for discharging. T: float Temperature of the battery in degrees Celsius (°C). power: float Power of the battery in watts (W). power_setpoint: float Desired power setpoint for the battery in watts (W). soc: float State of charge of the battery in per unit (p.u.). ocv: float Open-circuit voltage of the battery in volts (V). hys: float Hysteresis voltage of the battery in volts (V). rint: float Internal resistance of the battery in ohms (Ω). entropy: float Entropic coefficient of the battery in volts per Kelvin (V/K). soh_Q: float State of health of the battery in terms of capacity in per unit (p.u.). soh_R: float State of health of the battery in terms of resistance in per unit (p.u.). is_charge: bool True if the battery is charging, False if discharging. loss: float Power loss of the battery in watts (W). heat: float Internal heat generation of the battery in watts (W). i_max_charge: float Theoretical maximum charge current this timestep in amperes (A). i_max_discharge: float Theoretical maximum discharge current this timestep in amperes (A). Negative value.

Source code in src/simses/battery/state.py
@dataclass(slots=True)
class BatteryState:
    """
    Dataclass representing the state of a battery.

    v: float
        Terminal voltage of the battery in volts (V).
    i: float
        Current flowing into the battery in amperes (A). Positive for charging, negative for discharging.
    T: float
        Temperature of the battery in degrees Celsius (°C).
    power: float
        Power of the battery in watts (W).
    power_setpoint: float
        Desired power setpoint for the battery in watts (W).
    soc: float
        State of charge of the battery in per unit (p.u.).
    ocv: float
        Open-circuit voltage of the battery in volts (V).
    hys: float
        Hysteresis voltage of the battery in volts (V).
    rint: float
        Internal resistance of the battery in ohms (Ω).
    entropy: float
        Entropic coefficient of the battery in volts per Kelvin (V/K).
    soh_Q: float
        State of health of the battery in terms of capacity in per unit (p.u.).
    soh_R: float
        State of health of the battery in terms of resistance in per unit (p.u.).
    is_charge: bool
        True if the battery is charging, False if discharging.
    loss: float
        Power loss of the battery in watts (W).
    heat: float
        Internal heat generation of the battery in watts (W).
    i_max_charge: float
        Theoretical maximum charge current this timestep in amperes (A).
    i_max_discharge: float
        Theoretical maximum discharge current this timestep in amperes (A). Negative value.
    """

    v: float  # V
    i: float  # A (positive if charging)
    T: float  # °C
    power: float  # W
    power_setpoint: float  # W
    soc: float  # p.u.
    ocv: float  # V
    hys: float  # V
    rint: float  # ohm
    entropy: float  # V/K
    soh_Q: float  # p.u.
    soh_R: float  # p.u.
    is_charge: bool
    loss: float  # W
    heat: float  # W
    i_max_charge: float  # A
    i_max_discharge: float  # A

Cell interface

Abstract base class for cell chemistries — the "datasheet" side of the composition. Required methods: open_circuit_voltage, internal_resistance. Optional overrides for hysteresis, entropic coefficient, and a default degradation model. See Extending Cell Models.

simses.battery.cell.CellType

Bases: ABC

Abstract base class defining the interface and common properties for different types of battery cells. Subclasses must implement methods for calculating open-circuit voltage and internal resistance. Optionally, subclasses can override default_degradation_model() to ship a built-in degradation model.

Attributes:

Name Type Description
electrical ElectricalCellProperties

Electrical properties of the cell.

thermal ThermalCellProperties

Thermal properties of the cell.

format CellFormat

Physical format of the cell.

Methods: open_circuit_voltage(state: BatteryState) -> float: Abstract method to compute the open-circuit voltage for a given battery state. hysteresis_voltage(state: BatteryState) -> float: Returns the hysteresis voltage for a given battery state. Default is 0. internal_resistance(state: BatteryState) -> float: Abstract method to compute the internal resistance (beginning of life) for a given battery state. default_degradation_model(initial_soc: float) -> DegradationModel | None: Returns the cell's built-in default degradation model, or None if not defined.

Source code in src/simses/battery/cell.py
class CellType(ABC):
    """
    Abstract base class defining the interface and common properties for different types of battery cells.
    Subclasses must implement methods for calculating open-circuit voltage and internal resistance.
    Optionally, subclasses can override default_degradation_model() to ship a built-in degradation model.

    Attributes:
        electrical (ElectricalCellProperties): Electrical properties of the cell.
        thermal (ThermalCellProperties): Thermal properties of the cell.
        format (CellFormat): Physical format of the cell.
    Methods:
        open_circuit_voltage(state: BatteryState) -> float:
            Abstract method to compute the open-circuit voltage for a given battery state.
        hysteresis_voltage(state: BatteryState) -> float:
            Returns the hysteresis voltage for a given battery state. Default is 0.
        internal_resistance(state: BatteryState) -> float:
            Abstract method to compute the internal resistance (beginning of life) for a given battery state.
        default_degradation_model(initial_soc: float) -> DegradationModel | None:
            Returns the cell's built-in default degradation model, or None if not defined.
    """

    def __init__(
        self,
        electrical: ElectricalCellProperties,
        thermal: ThermalCellProperties,
        cell_format: CellFormat,
    ) -> None:
        super().__init__()
        self.electrical = electrical
        self.thermal = thermal
        self.format = cell_format

    @abstractmethod
    def open_circuit_voltage(self, state: BatteryState) -> float:
        """Compute the open-circuit voltage (in V) for a given battery state."""
        pass

    def hysteresis_voltage(self, state: BatteryState) -> float:
        """Compute the hysteresis voltage (in V) for a given battery state. Default is 0."""
        return 0.0

    @abstractmethod
    def internal_resistance(self, state: BatteryState) -> float:
        """Compute the beginning-of-life internal resistance (in Ohms) for a given battery state."""
        pass

    def entropic_coefficient(self, state: BatteryState) -> float:
        """Compute entropic coefficient (in V/K) for a given battery state. Default is 0."""
        return 0.0

    @classmethod
    def default_degradation_model(
        cls,
        initial_soc: float,
        initial_state: DegradationState | None = None,
    ) -> DegradationModel | None:
        """Return the cell's built-in default degradation model, or None if not defined."""
        return None

open_circuit_voltage(state) abstractmethod

Compute the open-circuit voltage (in V) for a given battery state.

Source code in src/simses/battery/cell.py
@abstractmethod
def open_circuit_voltage(self, state: BatteryState) -> float:
    """Compute the open-circuit voltage (in V) for a given battery state."""
    pass

hysteresis_voltage(state)

Compute the hysteresis voltage (in V) for a given battery state. Default is 0.

Source code in src/simses/battery/cell.py
def hysteresis_voltage(self, state: BatteryState) -> float:
    """Compute the hysteresis voltage (in V) for a given battery state. Default is 0."""
    return 0.0

internal_resistance(state) abstractmethod

Compute the beginning-of-life internal resistance (in Ohms) for a given battery state.

Source code in src/simses/battery/cell.py
@abstractmethod
def internal_resistance(self, state: BatteryState) -> float:
    """Compute the beginning-of-life internal resistance (in Ohms) for a given battery state."""
    pass

entropic_coefficient(state)

Compute entropic coefficient (in V/K) for a given battery state. Default is 0.

Source code in src/simses/battery/cell.py
def entropic_coefficient(self, state: BatteryState) -> float:
    """Compute entropic coefficient (in V/K) for a given battery state. Default is 0."""
    return 0.0

default_degradation_model(initial_soc, initial_state=None) classmethod

Return the cell's built-in default degradation model, or None if not defined.

Source code in src/simses/battery/cell.py
@classmethod
def default_degradation_model(
    cls,
    initial_soc: float,
    initial_state: DegradationState | None = None,
) -> DegradationModel | None:
    """Return the cell's built-in default degradation model, or None if not defined."""
    return None

Cell properties

Per-cell electrical and thermal parameter dataclasses passed to CellType.__init__. Every chemistry supplies one of each.

simses.battery.properties.ElectricalCellProperties dataclass

Electrical parameters of a single cell.

Attributes:

Name Type Description
nominal_capacity float

Nominal capacity in Ah.

nominal_voltage float

Nominal voltage in V.

min_voltage float

Minimum allowed terminal voltage in V.

max_voltage float

Maximum allowed terminal voltage in V.

max_charge_rate float

Maximum charge C-rate in 1/h.

max_discharge_rate float

Maximum discharge C-rate in 1/h.

self_discharge_rate float

Self-discharge rate in p.u. SOC per day (e.g. 0.015 for 1.5% SOC loss per day). Default: 0.

coulomb_efficiency float

Coulomb efficiency in p.u. Default: 1.0.

charge_derate_voltage_start float | None

Terminal voltage at which charge current derating begins, in V. Current is linearly reduced from the C-rate limit at this voltage down to 0 at max_voltage. None disables derating (default).

discharge_derate_voltage_start float | None

Terminal voltage at which discharge current derating begins, in V. Current is linearly reduced from the C-rate limit at this voltage down to 0 at min_voltage. None disables derating (default).

Source code in src/simses/battery/properties.py
@dataclass
class ElectricalCellProperties:
    """Electrical parameters of a single cell.

    Attributes:
        nominal_capacity: Nominal capacity in Ah.
        nominal_voltage: Nominal voltage in V.
        min_voltage: Minimum allowed terminal voltage in V.
        max_voltage: Maximum allowed terminal voltage in V.
        max_charge_rate: Maximum charge C-rate in 1/h.
        max_discharge_rate: Maximum discharge C-rate in 1/h.
        self_discharge_rate: Self-discharge rate in p.u. SOC per day
            (e.g. ``0.015`` for 1.5% SOC loss per day). Default: 0.
        coulomb_efficiency: Coulomb efficiency in p.u. Default: 1.0.
        charge_derate_voltage_start: Terminal voltage at which charge
            current derating begins, in V. Current is linearly reduced
            from the C-rate limit at this voltage down to 0 at
            ``max_voltage``. ``None`` disables derating (default).
        discharge_derate_voltage_start: Terminal voltage at which
            discharge current derating begins, in V. Current is linearly
            reduced from the C-rate limit at this voltage down to 0 at
            ``min_voltage``. ``None`` disables derating (default).
    """

    nominal_capacity: float
    nominal_voltage: float
    min_voltage: float
    max_voltage: float
    max_charge_rate: float
    max_discharge_rate: float
    self_discharge_rate: float = 0.0
    coulomb_efficiency: float = 1.0
    charge_derate_voltage_start: float | None = None
    discharge_derate_voltage_start: float | None = None

simses.battery.properties.ThermalCellProperties dataclass

Thermal parameters of a single cell.

Attributes:

Name Type Description
min_temperature float

Minimum allowed cell temperature in °C.

max_temperature float

Maximum allowed cell temperature in °C.

mass float

Mass of one cell in kg.

specific_heat float

Specific heat capacity in J/kgK.

convection_coefficient float

Convective heat transfer coefficient between the cell surface and the thermal environment, in W/m²K.

Source code in src/simses/battery/properties.py
@dataclass
class ThermalCellProperties:
    """Thermal parameters of a single cell.

    Attributes:
        min_temperature: Minimum allowed cell temperature in °C.
        max_temperature: Maximum allowed cell temperature in °C.
        mass: Mass of one cell in kg.
        specific_heat: Specific heat capacity in J/kgK.
        convection_coefficient: Convective heat transfer coefficient
            between the cell surface and the thermal environment, in
            W/m²K.
    """

    min_temperature: float
    max_temperature: float
    mass: float
    specific_heat: float
    convection_coefficient: float

Cell formats

Physical format descriptors used to compute surface area and volume for thermal coupling. The two 26650 / 18650 presets bundle common dimensions.

simses.battery.format.CellFormat dataclass

Base class for cell geometries, providing volume and area.

Source code in src/simses/battery/format.py
@dataclass
class CellFormat:
    """Base class for cell geometries, providing volume and area."""

    volume: float = field(init=False)  # in m³
    area: float = field(init=False)  # in m²

simses.battery.format.PrismaticCell dataclass

Bases: CellFormat

Prismatic cell format with height, width, and length in mm.

Source code in src/simses/battery/format.py
@dataclass
class PrismaticCell(CellFormat):
    """Prismatic cell format with height, width, and length in mm."""

    height: float
    width: float
    length: float

    def __post_init__(self):
        h = self.height
        w = self.width
        l = self.length

        self.volume = h * w * l * 1e-9  # m³
        self.area = 2 * (l * h + l * w + w * h) * 1e-6  # m²

simses.battery.format.RoundCell dataclass

Bases: CellFormat

Cylindrical cell format with diameter and length in mm.

Source code in src/simses/battery/format.py
@dataclass
class RoundCell(CellFormat):
    """Cylindrical cell format with diameter and length in mm."""

    diameter: float  # in mm
    length: float  # in mm

    def __post_init__(self):
        d = self.diameter
        l = self.length
        self.volume = math.pi * (d / 2) ** 2 * l * 1e-9  # m³
        self.area = (math.pi * d * l + math.pi * (d / 2) ** 2) * 1e-6  # m²

simses.battery.format.RoundCell18650 dataclass

Bases: RoundCell

Standard 18650 cylindrical cell (18 mm x 65 mm).

Source code in src/simses/battery/format.py
@dataclass
class RoundCell18650(RoundCell):
    """Standard 18650 cylindrical cell (18 mm x 65 mm)."""

    diameter: float = 18  # mm
    length: float = 65  # mm

simses.battery.format.RoundCell26650 dataclass

Bases: RoundCell

Standard 26650 cylindrical cell (26 mm x 65 mm).

Source code in src/simses/battery/format.py
@dataclass
class RoundCell26650(RoundCell):
    """Standard 26650 cylindrical cell (26 mm x 65 mm)."""

    diameter: float = 26  # mm
    length: float = 65  # mm

Derating

Optional current-curtailment strategies applied after hard limits. Protocol-based — use the shipped linear voltage / thermal strategies, compose them with DeratingChain, or implement your own.

simses.battery.derating.CurrentDerating

Bases: Protocol

Protocol for current derating strategies.

A derating function receives the candidate current (already clamped to hard limits) and the current battery state, and returns a (possibly reduced) current.

Sign convention: - If i > 0 (charging), return a value in [0, i] - If i < 0 (discharging), return a value in [i, 0] - If i == 0, return 0

Source code in src/simses/battery/derating.py
class CurrentDerating(Protocol):
    """Protocol for current derating strategies.

    A derating function receives the candidate current (already clamped to hard
    limits) and the current battery state, and returns a (possibly reduced) current.

    Sign convention:
    - If i > 0 (charging), return a value in [0, i]
    - If i < 0 (discharging), return a value in [i, 0]
    - If i == 0, return 0
    """

    def derate(self, i: float, state: BatteryState) -> float:
        """Return the derated current."""
        ...

derate(i, state)

Return the derated current.

Source code in src/simses/battery/derating.py
def derate(self, i: float, state: BatteryState) -> float:
    """Return the derated current."""
    ...

simses.battery.derating.DeratingChain

Applies multiple derating strategies in sequence.

Each strategy receives the output of the previous one. Since each step can only reduce |i|, the most restrictive combination wins naturally. The chain short-circuits when i reaches 0.

DeratingChain itself satisfies the CurrentDerating protocol and can be nested.

Source code in src/simses/battery/derating.py
class DeratingChain:
    """Applies multiple derating strategies in sequence.

    Each strategy receives the output of the previous one. Since each step can
    only reduce |i|, the most restrictive combination wins naturally. The chain
    short-circuits when i reaches 0.

    DeratingChain itself satisfies the CurrentDerating protocol and can be nested.
    """

    def __init__(self, strategies: list) -> None:
        self._strategies = list(strategies)

    def derate(self, i: float, state: BatteryState) -> float:
        """Apply each strategy in sequence, short-circuiting at zero.

        Args:
            i: Candidate current in A.
            state: Current battery state.

        Returns:
            Current after all strategies have been applied in order.
        """
        for strategy in self._strategies:
            i = strategy.derate(i, state)
            if i == 0.0:
                break
        return i

derate(i, state)

Apply each strategy in sequence, short-circuiting at zero.

Parameters:

Name Type Description Default
i float

Candidate current in A.

required
state BatteryState

Current battery state.

required

Returns:

Type Description
float

Current after all strategies have been applied in order.

Source code in src/simses/battery/derating.py
def derate(self, i: float, state: BatteryState) -> float:
    """Apply each strategy in sequence, short-circuiting at zero.

    Args:
        i: Candidate current in A.
        state: Current battery state.

    Returns:
        Current after all strategies have been applied in order.
    """
    for strategy in self._strategies:
        i = strategy.derate(i, state)
        if i == 0.0:
            break
    return i

simses.battery.derating.LinearVoltageDerating

Linearly reduce current when terminal voltage enters the derating zone.

Charge: ramp from full current at charge_start_voltage down to 0 at max_voltage. Discharge: ramp from full current at discharge_start_voltage down to 0 at min_voltage.

All voltage values must be at the system level (cell voltage * serial count).

Source code in src/simses/battery/derating.py
class LinearVoltageDerating:
    """Linearly reduce current when terminal voltage enters the derating zone.

    Charge:    ramp from full current at charge_start_voltage down to 0 at max_voltage.
    Discharge: ramp from full current at discharge_start_voltage down to 0 at min_voltage.

    All voltage values must be at the system level (cell voltage * serial count).
    """

    def __init__(
        self,
        max_voltage: float,
        min_voltage: float,
        charge_start_voltage: float | None = None,
        discharge_start_voltage: float | None = None,
    ) -> None:
        self.max_voltage = max_voltage
        self.min_voltage = min_voltage
        self.charge_start_voltage = charge_start_voltage
        self.discharge_start_voltage = discharge_start_voltage

    @classmethod
    def from_cell(cls, cell, serial: int) -> "LinearVoltageDerating | None":
        """Build from cell electrical properties, scaling voltages to system level.

        Returns None if neither derating threshold is configured on the cell.
        """
        e = cell.electrical
        if e.charge_derate_voltage_start is None and e.discharge_derate_voltage_start is None:
            return None
        return cls(
            max_voltage=e.max_voltage * serial,
            min_voltage=e.min_voltage * serial,
            charge_start_voltage=e.charge_derate_voltage_start * serial
            if e.charge_derate_voltage_start is not None
            else None,
            discharge_start_voltage=e.discharge_derate_voltage_start * serial
            if e.discharge_derate_voltage_start is not None
            else None,
        )

    def derate(self, i: float, state: BatteryState) -> float:
        """Return the current scaled down if the derating zone is active.

        The terminal voltage is computed from the incoming current and the
        state's OCV / hysteresis / Rint, then:

        * Charge (``i > 0``): full current below ``charge_start_voltage``,
          zero at ``max_voltage``, linear in between.
        * Discharge (``i < 0``): full current above
          ``discharge_start_voltage``, zero at ``min_voltage``, linear in
          between.

        If the corresponding start voltage is ``None``, no derating is
        applied for that direction.

        Args:
            i: Candidate current in A (already clamped to hard limits).
            state: Current battery state.

        Returns:
            Derated current in A (same sign as ``i``, magnitude ≤ ``|i|``).
        """
        if i == 0.0:
            return i

        v = state.ocv + state.hys + state.rint * i

        if i > 0:  # charge
            dv = self.charge_start_voltage
            if dv is None or v <= dv:
                return i
            if v >= self.max_voltage:
                return 0.0
            return i * (self.max_voltage - v) / (self.max_voltage - dv)

        else:  # discharge
            dv = self.discharge_start_voltage
            if dv is None or v >= dv:
                return i
            if v <= self.min_voltage:
                return 0.0
            return i * (v - self.min_voltage) / (dv - self.min_voltage)

from_cell(cell, serial) classmethod

Build from cell electrical properties, scaling voltages to system level.

Returns None if neither derating threshold is configured on the cell.

Source code in src/simses/battery/derating.py
@classmethod
def from_cell(cls, cell, serial: int) -> "LinearVoltageDerating | None":
    """Build from cell electrical properties, scaling voltages to system level.

    Returns None if neither derating threshold is configured on the cell.
    """
    e = cell.electrical
    if e.charge_derate_voltage_start is None and e.discharge_derate_voltage_start is None:
        return None
    return cls(
        max_voltage=e.max_voltage * serial,
        min_voltage=e.min_voltage * serial,
        charge_start_voltage=e.charge_derate_voltage_start * serial
        if e.charge_derate_voltage_start is not None
        else None,
        discharge_start_voltage=e.discharge_derate_voltage_start * serial
        if e.discharge_derate_voltage_start is not None
        else None,
    )

derate(i, state)

Return the current scaled down if the derating zone is active.

The terminal voltage is computed from the incoming current and the state's OCV / hysteresis / Rint, then:

  • Charge (i > 0): full current below charge_start_voltage, zero at max_voltage, linear in between.
  • Discharge (i < 0): full current above discharge_start_voltage, zero at min_voltage, linear in between.

If the corresponding start voltage is None, no derating is applied for that direction.

Parameters:

Name Type Description Default
i float

Candidate current in A (already clamped to hard limits).

required
state BatteryState

Current battery state.

required

Returns:

Type Description
float

Derated current in A (same sign as i, magnitude ≤ |i|).

Source code in src/simses/battery/derating.py
def derate(self, i: float, state: BatteryState) -> float:
    """Return the current scaled down if the derating zone is active.

    The terminal voltage is computed from the incoming current and the
    state's OCV / hysteresis / Rint, then:

    * Charge (``i > 0``): full current below ``charge_start_voltage``,
      zero at ``max_voltage``, linear in between.
    * Discharge (``i < 0``): full current above
      ``discharge_start_voltage``, zero at ``min_voltage``, linear in
      between.

    If the corresponding start voltage is ``None``, no derating is
    applied for that direction.

    Args:
        i: Candidate current in A (already clamped to hard limits).
        state: Current battery state.

    Returns:
        Derated current in A (same sign as ``i``, magnitude ≤ ``|i|``).
    """
    if i == 0.0:
        return i

    v = state.ocv + state.hys + state.rint * i

    if i > 0:  # charge
        dv = self.charge_start_voltage
        if dv is None or v <= dv:
            return i
        if v >= self.max_voltage:
            return 0.0
        return i * (self.max_voltage - v) / (self.max_voltage - dv)

    else:  # discharge
        dv = self.discharge_start_voltage
        if dv is None or v >= dv:
            return i
        if v <= self.min_voltage:
            return 0.0
        return i * (v - self.min_voltage) / (dv - self.min_voltage)

simses.battery.derating.LinearThermalDerating

Linearly reduce current when temperature enters the derating zone.

Between T_start and T_max, current is scaled linearly from 100% down to 0%. Below T_start: no derating. At or above T_max: current is forced to 0.

All temperatures must be in °C. Separate thresholds can be configured for charge and discharge; if omitted, discharge reuses the charge thresholds.

Source code in src/simses/battery/derating.py
class LinearThermalDerating:
    """Linearly reduce current when temperature enters the derating zone.

    Between T_start and T_max, current is scaled linearly from 100% down to 0%.
    Below T_start: no derating. At or above T_max: current is forced to 0.

    All temperatures must be in °C. Separate thresholds can be configured for
    charge and discharge; if omitted, discharge reuses the charge thresholds.
    """

    def __init__(
        self,
        charge_T_start: float,
        charge_T_max: float,
        discharge_T_start: float | None = None,
        discharge_T_max: float | None = None,
    ) -> None:
        self.charge_T_start = charge_T_start
        self.charge_T_max = charge_T_max
        self.discharge_T_start = discharge_T_start if discharge_T_start is not None else charge_T_start
        self.discharge_T_max = discharge_T_max if discharge_T_max is not None else charge_T_max

    def derate(self, i: float, state: BatteryState) -> float:
        """Return the current scaled down if the temperature derating zone is active.

        Below the start temperature no derating is applied; between start
        and max the current is scaled linearly from 100% down to 0%; at or
        above the max temperature the current is forced to 0.

        Args:
            i: Candidate current in A (already clamped to hard limits).
            state: Current battery state (reads ``T``).

        Returns:
            Derated current in A (same sign as ``i``, magnitude ≤ ``|i|``).
        """
        if i == 0.0:
            return i

        T = state.T

        if i > 0:  # charge
            if T <= self.charge_T_start:
                return i
            if T >= self.charge_T_max:
                return 0.0
            return i * (self.charge_T_max - T) / (self.charge_T_max - self.charge_T_start)

        else:  # discharge
            if T <= self.discharge_T_start:
                return i
            if T >= self.discharge_T_max:
                return 0.0
            return i * (self.discharge_T_max - T) / (self.discharge_T_max - self.discharge_T_start)

derate(i, state)

Return the current scaled down if the temperature derating zone is active.

Below the start temperature no derating is applied; between start and max the current is scaled linearly from 100% down to 0%; at or above the max temperature the current is forced to 0.

Parameters:

Name Type Description Default
i float

Candidate current in A (already clamped to hard limits).

required
state BatteryState

Current battery state (reads T).

required

Returns:

Type Description
float

Derated current in A (same sign as i, magnitude ≤ |i|).

Source code in src/simses/battery/derating.py
def derate(self, i: float, state: BatteryState) -> float:
    """Return the current scaled down if the temperature derating zone is active.

    Below the start temperature no derating is applied; between start
    and max the current is scaled linearly from 100% down to 0%; at or
    above the max temperature the current is forced to 0.

    Args:
        i: Candidate current in A (already clamped to hard limits).
        state: Current battery state (reads ``T``).

    Returns:
        Derated current in A (same sign as ``i``, magnitude ≤ ``|i|``).
    """
    if i == 0.0:
        return i

    T = state.T

    if i > 0:  # charge
        if T <= self.charge_T_start:
            return i
        if T >= self.charge_T_max:
            return 0.0
        return i * (self.charge_T_max - T) / (self.charge_T_max - self.charge_T_start)

    else:  # discharge
        if T <= self.discharge_T_start:
            return i
        if T >= self.discharge_T_max:
            return 0.0
        return i * (self.discharge_T_max - T) / (self.discharge_T_max - self.discharge_T_start)