Skip to content

Capacitor-Bank State Model

The capacitor-bank module is a CONTROL-owned admission model for pulsed-shot planning. It provides a bounded series-RLC state surface that can be used by the pulsed-scenario scheduler, pulsed MPC adapter, replay reports, and optional Rust/PyO3 runtime paths.

It is not a facility driver, relay model, switch model, insulation model, or hardware protection system. Those claims require target hardware evidence and plant-specific interlock validation.

State equation

The model advances the capacitor voltage and series current with the series-RLC state equation:

d/dt [v_C, i]^T = [[0, -1/C], [1/L, -R/L]] [v_C, i]^T + [-i_load/C, 0]^T

Where:

Symbol Meaning
C bank capacitance in farads
L series inductance in henries
R series resistance in ohms
v_C capacitor voltage in volts
i series current in amperes
i_load prescribed external load current in amperes

The Python and Rust implementations use the same exact zero-order-hold update and the same midpoint-sampled load-current waveforms: rect, half_sine, and exp_decay.

Exact discretisation

For a load current held constant over a step dt, the linear time-invariant series-RLC system has the exact discrete solution

x_{n+1} = Phi x_n + Gamma u,   Phi = exp(A dt),   Gamma = A^{-1}(Phi - I) B

The state-transition matrix Phi is assembled column by column from the closed-form free_response, so the stepper reproduces the analytic homogeneous solution to accumulated floating-point precision across the underdamped, critical, and overdamped regimes — it does not carry the second-order truncation of the Crank-Nicolson update it replaces. The energy ledger is closed in closed form as well: each step integrates int_0^dt v dt from the same discretisation matrices and the ohmic dissipation int_0^dt i(t)^2 dt from the finite-horizon current gramian of the augmented state [v, i, u] (Van Loan's augmented matrix exponential). With exact dynamics and exact energy integrals the discharge ledger closes to machine precision rather than the previous quadrature tolerance.

Energy surfaces

CapacitorBankState.energy_J remains the scheduler-facing capacitor electric energy:

E_C = 0.5 C v_C^2

The discharge report uses total stored RLC energy because the inductor may still hold magnetic energy after a pulse step:

E_total = 0.5 C v_C^2 + 0.5 L i^2

A discharge ledger records:

Field Meaning
energy_initial_J initial total RLC stored energy
energy_remaining_J final total RLC stored energy
capacitor_energy_remaining_J final capacitor electric energy
inductor_energy_remaining_J final inductor magnetic energy
energy_delivered_J energy_initial_J - energy_remaining_J
resistive_loss_J integrated ohmic dissipation
load_energy_J integrated energy extracted by the prescribed load
energy_balance_residual_J ledger residual
energy_balance_relative_error scale-normalised residual
energy_balance_passed admission flag for the residual tolerance

The admission residual is:

energy_initial_J - energy_remaining_J
  = resistive_loss_J + load_energy_J + energy_balance_residual_J

This contract catches drift between the RLC state update and the energy ledger. It does not replace hardware metrology, relay timing, or plant protection logic.

Pulse admissibility

CapacitorBank.feasibility() is a conservative pre-admission guard for controller planning. It bounds requested pulse peak current from total stored series-RLC energy, not capacitor voltage alone:

I_bound = sqrt(2 E_total / L)

This matters after a prior pulse step because the series inductor may still hold magnetic energy even when capacitor voltage is low. The same total-energy ledger is used for the rough resistive-loss guard:

R I_peak^2 f_waveform duration <= E_total

The guard is intentionally not a switch, relay, insulation, or interlock model. It only admits whether the bounded CONTROL model has enough stored RLC energy for the requested prescribed waveform before scheduler or pulsed-MPC logic uses the state.

Python example

from scpn_control.control.capacitor_bank_state import CapacitorBank, CapacitorBankSpec, PulseSpec

spec = CapacitorBankSpec(
    capacitance_F=100e-6,
    inductance_H=100e-6,
    series_resistance_ohm=0.5,
    voltage_max_V=10_000.0,
    recharge_power_kW=20.0,
)
bank = CapacitorBank(spec, initial_voltage_V=5_000.0, initial_current_A=12.0)
report = bank.discharge(
    PulseSpec(peak_current_A=500.0, duration_s=20e-6, waveform="half_sine"),
    dt=100e-9,
    n_steps=200,
)
assert report.energy_balance_passed
assert bank.feasibility(PulseSpec(peak_current_A=200.0, duration_s=10e-6))[0]

Rust example

use control_control::capacitor_bank::{CapacitorBank, CapacitorBankSpec, PulseSpec, PulseWaveform};

let spec = CapacitorBankSpec::new(100e-6, 100e-6, 0.5, 10_000.0, 20.0).expect("valid spec");
let mut bank = CapacitorBank::new(spec, 5_000.0, 12.0).expect("valid bank");
let pulse = PulseSpec::new(500.0, 20e-6, PulseWaveform::HalfSine).expect("valid pulse");
let report = bank.discharge(pulse, 100e-9, 200).expect("discharge evaluates");
assert!(report.energy_balance_passed);
let admission = PulseSpec::new(200.0, 10e-6, PulseWaveform::HalfSine).expect("valid pulse");
assert!(bank.feasibility(admission).expect("feasibility evaluates").0);

Benchmarking

The benchmark harness times the Python discharge ledger and, when the compiled scpn_control_rs extension is installed, the Rust discharge through the PyO3 bridge in the same run. It reports per-language timing percentiles, the Rust speedup against the Python reference, and the cross-language ledger parity (maximum relative difference between the two implementations):

PYTHONPATH=src python benchmarks/bench_capacitor_bank_energy.py \
  --steps 500 --warmup 50 --discharge-steps 200 --dt-s 1.0e-7 \
  --json-out benchmarks/results/capacitor_bank_energy.json \
  --markdown-out benchmarks/results/capacitor_bank_energy.md

The exact discretisation makes the two implementations agree on the energy ledger to machine precision, so the cross-language parity is a tamper check on the polyglot chain. Reports generated without hard CPU isolation are local regression evidence only; production timing claims require isolated-core runs on declared target hardware.

Practical use and scope

Use this page to understand admissible capacitor-bank state transitions in pulsed workflows.

  • Read before changing series-RLC assumptions or scheduler interactions for shot campaigns.
  • Keep model parameter updates synchronized with pulsed scenario and replay artifacts.
  • Use it alongside docs/control/pulsed_scenario_scheduler.md for lifecycle-consistent edits.