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:
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
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:
The discharge report uses total stored RLC energy because the inductor may still hold magnetic energy after a pulse step:
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:
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:
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.mdfor lifecycle-consistent edits.