Skip to content

Lumped-DC electrical networks

Manta's electrical model is a bounded lumped-DC plant for vehicle power systems. It is intended to answer questions such as:

  • Does a rail brown out when two loads start together?
  • Does a regulator hit its current or power rating?
  • How long does downstream hold-up capacitance survive an open contactor?
  • Which part's electrical loss should heat a ThermalMass?

It is not SPICE. AC behavior, arbitrary circuit loops, parallel source sharing, reverse current and charging require an implicit circuit solve and are outside this model.

Series battery packs

Battery chemistry, BMS latches, and fault injection are simulation-only Python state. They intentionally do not become Manta Part states: a navigation filter, controller, or generated deploy model should not acquire twelve SOC states merely because the simulation is realistic.

from manta.simulation import (
    BatteryCell, BatteryCellFaults, BatteryStepInput, OCVCurve,
    PassiveBalancer, SeriesBatteryPack,
)

cells = [BatteryCell(usable_capacity_ah=30.0) for _ in range(12)]
pack = SeriesBatteryPack(
    cells, initial_soc=[0.94] * 12,
    balancers=[PassiveBalancer(cell_index=i) for i in range(12)],
    seed=7,
)
tick = BatteryStepInput(
    requested_series_current=8.0,
    cell_temperatures=(298.15,) * 12,
    cell_faults=(BatteryCellFaults(),) * 12,
    balance_enabled=(False,) * 12,
)
telemetry = pack.step(0.01, tick)

OCVCurve is calibrated with piecewise-linear SOC/voltage points. Pack stepping rejects an SOC transition outside [0, 1] rather than silently clamping energy or propagating a NaN. checkpoint() / restore() include the seeded random stream for exact replay.

Cell failures are explicit immutable BatteryCellFaults values:

Input Plant behavior
open_cell Blocks the shared series current
short_cell Removes that cell's terminal OCV and creates an internal short current/heat path
high_resistance Applies high_resistance_multiplier
capacity_loss_fraction Directly reduces usable Ah
forced_overtemperature Forces the configured hot-cell resistance behavior

Temperature is a per-tick input. Crossing maximum_temperature changes cell resistance and telemetry but does not invent a second thermal state. The caller can send each cell's internal plus passive-balancer heat to an existing ThermalMass.heat_input using telemetry.thermal_inputs(...).

BatteryTelemetry exposes minimum/maximum cell voltage and SOC, imbalance, cell-open and overtemperature aggregates, pack current, and pack voltage. BMSPlant gives its commanded contactor an explicit latching trip_command / reset_command state. It does not decide when a vehicle should trip, abort, surface, or shed loads; the simulated/real BMS driver and Shiver/Voyage own those policies.

The integration with the electrical graph is explicit co-simulation. Attach a BatteryElectricalModel to a numpy simulation runtime; it drives ExternalDCSupply.supplied_voltage, reads that supply's output_current, and advances the pack in the same atomic simulation tick:

from manta import Sim, TargetNumpy
from manta.simulation import BatteryElectricalModel

runtime = TargetNumpy(Sim(world))
runtime.attach_model(BatteryElectricalModel(
    pack, craft="auv", supply="battery_supply",
    thermal_parts=("cell_0_thermal", "cell_1_thermal", ...)))
runtime.step(0.01)

The battery stays a separate non-spatial graph with no clock of its own. Its terminal voltage is a declared one-tick zero-order hold, not a hidden algebraic solve. Runtime checkpoint, restore, and failed-step rollback cover the spatial state, electrical/thermal Manta parts, noise stream, and battery state as one unit. Shiver still owns protection, load-shedding, and scheduling policy. When thermal_parts are supplied, prior-tick cell and balancing losses drive those ThermalMass.heat_input channels, and their updated temperatures feed the next battery evaluation on that same clock.

Two independent trees

Every electrical node is an ordinary Part, so its states and equations pass through the same Sim, EKF/UKF, Fit and code-generation pipeline as the rigid body. Electrical edges are separate from mechanical mounting:

from manta.parts import (
    ConstantPowerLoad, DCConverter, DCSource, ElectricalBus,
)

source = craft.add(DCSource(
    "battery_terminal", open_circuit_voltage=50.4,
    source_resistance=0.06, capacitance=5.0, current_limit=80.0,
    brownout_voltage=34.0, recovery_voltage=38.0,
))
bus = craft.add(ElectricalBus(
    "main_bus", rail_voltage=48.0, capacitance=0.5,
    series_resistance=0.01, input_current_limit=60.0,
))
regulator = craft.add(DCConverter(
    "computer_regulator", output_voltage=12.0, capacitance=0.2,
    efficiency=0.92, output_current_limit=15.0,
    output_power_limit=160.0, input_power_limit=180.0,
    brownout_voltage=9.0, recovery_voltage=10.0,
))
computer = craft.add(ConstantPowerLoad(
    "computer", power=120.0, current_limit=15.0,
    voltage_floor=1.0, brownout_voltage=8.0, recovery_voltage=10.0,
))

source.connect(bus)
bus.connect(regulator)
regulator.connect(computer)

The mechanical parts may be siblings, nested composites, or mounted far apart. connect alone defines the electrical graph. A transform snapshot rejects:

  • a non-source without exactly one upstream supply;
  • a second upstream supply;
  • a cycle;
  • a source used as a child;
  • an endpoint load used as a supply; and
  • an electrical edge between two craft.

These constraints make the topology a directed radial forest. Demand can be evaluated from leaves to roots without an algebraic loop, keeping the generated tick compact and deterministic.

Equations and diagnostics

Each energized rail owns a capacitor state:

[ C\dot V = I_{in} + I_{noise} - I_{out}. ]

DCSource charges its terminal capacitor from a current-limited Thevenin reservoir. ElectricalBus, Contactor, and Fuse receive current through a series resistance. DCConverter injects bounded current into its output capacitor while respecting dropout, efficiency, input power, output power and output current limits.

Endpoint loads are available as constant resistance, current and power. Every load has a C1 brownout gate: it is exactly off below brownout_voltage, exactly on above recovery_voltage, and smoothly transitions between them. Constant power loads also have a denominator floor and optional current limit, so their current cannot become singular during voltage collapse.

Every node exposes the same outputs:

Output Meaning
voltage Local terminal or output-rail voltage, V
input_current, input_power Flow entering from the parent/reservoir, A/W
output_current, output_power Flow delivered to children or consumed by a load, A/W
loss_power Series/conversion loss, W
brownout, open, tripped Explicit unit-valued condition signals
kcl_residual Capacitor current-balance residual, A
energy_residual Input minus output, loss and stored power, W

The residual outputs are useful assertions in simulation and Monte Carlo runs. dissipated_heat() returns the node's loss for ThermalMass(source=electrical_part). Loads also accept a heat_fraction for the share of useful endpoint power that becomes local heat.

Fuse state is a normalized I²t accumulator. Once it reaches one the fuse latches open. A contactor's closed input and a source/converter's enabled input are normalized commands. These switching surfaces and current limits are piecewise differentiable: CasADi provides the derivative on either side, while the derivative at the exact hybrid event is not physically meaningful.

Timestep and positivity

Rail voltage uses explicit Euler. Manta does not silently clip a negative voltage or capacitor energy: that would conceal an invalid integration step and produce a false conservation result. Choose dt against the fastest electrical time constant.

For a rail with capacitance C, lowest resolved voltage V_min, and maximum net discharge I_max, the conservative one-step positivity condition is:

[ dt < C V_{min} / I_{max}. ]

rail.stable_timestep_hint(minimum_voltage=..., maximum_net_current=...) calculates that local bound. For a linear RC mode, explicit Euler's absolute stability limit is dt < 2 R C; staying below R C also avoids a one-step sign change. Interconnected rails must use the fastest effective edge time constant, not merely the slowest load.

The representative 50.4 V (12-series-equivalent) network benchmark uses a 1 ms step. Faster switching detail is intentionally averaged into regulator efficiency and capacitance; modeling PWM edges belongs in a different tool.

Electrical diagnostic outputs are plant observables, not noisy device sensors. Construct an EKF/UKF with sensors=[] (or an explicit real sensor subset) when estimating rail states. Add a separate voltage/current sensor part when a physical measurement and covariance are required.