Capacitor bank — scpn_mif_core.lifecycle.capacitor_bank¶
Module ID: MIF-005.
Sync state: upstream-pending for SCPN-CONTROL v0.21.0 (CON-C.2); see
the bidirectional sync protocol (internal, docs/internal/bidirectional_sync_protocol.md).
Reference: Maron, Y., et al. (2018). Pulsed power and ultra-high-current
discharge dynamics. Physical Review X 8, 041018.
The capacitor-bank model is the energy reservoir behind every pulsed-shot
trigger in SCPN-MIF-CORE. It tracks the bank voltage v_C(t) and inductor
current i_L(t) under the natural-response dynamics of a series RLC loop and
under a prescribed external load. The matching Lean proof surface records the
non-negativity of capacitor energy, inductor energy, total stored energy, and
linear recharge energy under the physical parameter ranges enforced by the
runtime constructors.
The observable CapacitorBankState.energy_J is the total stored
electromagnetic energy,
matching the Lean storedEnergy contract. The component fields
capacitor_energy_J and inductor_energy_J expose the two addends for
audits and cross-language parity checks.
All runtime surfaces fail closed if finite input parameters would derive
non-finite stored energy. CapacitorBankSpec validates the maximum capacitor
energy implied by capacitance_F and voltage_max_V, while CapacitorBank
validates each emitted observable state before construction, reset, or
Crank-Nicolson step results are exposed.
Carrier equations¶
For a series RLC circuit driven by an initial bank voltage v_C(0) = V_0 and
zero initial inductor current i_L(0) = 0:
The damping factor \(\alpha = R / (2L)\) and the undamped resonant frequency \(\omega_0 = 1 / \sqrt{L C}\) together fix the regime via the critical-resistance threshold \(R_\mathrm{crit} = 2 \sqrt{L/C}\):
| Regime | Condition | Time constant |
|---|---|---|
| Overdamped | \(R > R_\mathrm{crit}\) | $ |
| Critically damped | \(R = R_\mathrm{crit}\) | \(\alpha^{-1}\) |
| Underdamped | \(R < R_\mathrm{crit}\) | \(\alpha^{-1}\) exponential envelope on \(\omega_d = \sqrt{\omega_0^2 - \alpha^2}\) oscillation |
Parameter dictionary¶
| Symbol | Field | Units | Range | Notes |
|---|---|---|---|---|
| \(C\) | capacitance_F |
F | \(> 0\) | Total bank capacitance. |
| \(L\) | inductance_H |
H | \(> 0\) | Loop inductance (bank + leads). |
| \(R\) | series_resistance_ohm |
Ω | \(\ge 0\) | ESR + lead resistance. |
| \(V_\mathrm{max}\) | voltage_max_V |
V | \(> 0\) | Hard upper bound on bank voltage. |
| \(P_\mathrm{rec}\) | recharge_power_kW |
kW | \(\ge 0\) | Linear recharge-power budget. |
| — | safety_envelope |
dict | — | Named operational margins (consumer-defined). |
Numerical scheme¶
The stateful integrator advances the pair \((v_C, i_L)\) by Crank-Nicolson,
with \(i_\mathrm{load}(t)\) sampled at the midpoint \(t + \Delta t / 2\) inside
CapacitorBank.discharge. The 2×2 system is solved via the closed-form
matrix inverse so the Rust and Python paths agree to better than \(10^{-12}\)
relative tolerance across the validation suite.
Public Python API¶
Series RLC capacitor-bank energy state model (MIF-005).
Implements the natural-response dynamics of a series RLC capacitor bank through the three classical damping regimes (overdamped, critically damped, underdamped) and an unconditionally stable Crank-Nicolson semi-implicit integrator over the (v_C, i_L) state pair.
Reference¶
Maron, Y., et al. (2018). Pulsed power and ultra-high-current discharge dynamics. Physical Review X 8, 041018.
Carrier equations¶
For a series RLC circuit driven by an initial bank voltage v_C(0) = V_0 and zero initial inductor current i_L(0) = 0:
.. math::
\frac{d v_C}{d t} = -\frac{i_L}{C}
\qquad
\frac{d i_L}{d t} = \frac{v_C - R \, i_L}{L}
The damping factor :math:\alpha = R / (2L) and the undamped resonant
frequency :math:\omega_0 = 1 / \sqrt{L C} together fix the regime via
the critical-resistance threshold :math:R_\mathrm{crit} = 2 \sqrt{L/C}.
Status¶
SYNC-STATE: upstream-pending — this module is the temporary MIF-CORE
home of the canonical implementation; it is planned to upstream to
SCPN-CONTROL as CON-C.2 in scpn-control == 0.21.0. See
docs/internal/upstream_contracts/03_scpn_control.md §C.2.
RLCRegime
¶
Bases: StrEnum
Classification of the series RLC natural response.
CapacitorBankSpec(capacitance_F, inductance_H, series_resistance_ohm, voltage_max_V, recharge_power_kW, safety_envelope=dict())
dataclass
¶
Immutable physical and operational specification of a capacitor bank.
Attributes¶
capacitance_F : float Total bank capacitance in farads. Must be strictly positive. inductance_H : float Loop inductance (bank plus leads) in henries. Must be strictly positive. series_resistance_ohm : float Total series resistance (ESR plus lead resistance) in ohms. Must be non-negative. voltage_max_V : float Hard upper bound on the bank voltage. Strictly positive. recharge_power_kW : float Linear recharging-power budget in kilowatts. Non-negative. safety_envelope : dict[str, float] Optional named operational margins consumed by external guards.
damping_factor
property
¶
Damping factor :math:\alpha = R / (2 L) (rad s⁻¹).
resonant_frequency
property
¶
Undamped resonant frequency :math:\omega_0 = 1 / \sqrt{L C} (rad s⁻¹).
critical_resistance
property
¶
Critical-damping resistance :math:R_\mathrm{crit} = 2 \sqrt{L / C} (ohm).
regime
property
¶
Damping regime implied by series_resistance_ohm vs critical_resistance.
__post_init__()
¶
Validate capacitor-bank circuit constants and safety envelope values.
CapacitorBankState(t, voltage_V, energy_J, capacitor_energy_J, inductor_energy_J, current_A, di_dt_A_s, discharge_active, recharge_active)
dataclass
¶
Immutable observable state of the bank at time t.
PulseSpec(peak_current_A, duration_s, waveform='half_sine')
dataclass
¶
Specification of a requested discharge pulse.
__post_init__()
¶
Validate compression-pulse current, duration, and waveform label.
EnergyReport(energy_delivered_J, energy_remaining_J, peak_voltage_V, peak_current_A, discharge_duration_s, rlc_regime)
dataclass
¶
Summary of a completed discharge sequence.
CapacitorBank(spec, initial_voltage_V=0.0)
¶
Series RLC capacitor bank with Crank-Nicolson numerical integration.
The bank tracks the state pair (v_C, i_L). The free-response dynamics
obey the linear ODE :math:\dot y = A y with
:math:A = \begin{pmatrix} 0 & -1/C \\ 1/L & -R/L \end{pmatrix}.
:meth:step advances the state via the unconditionally stable
Crank-Nicolson scheme
.. math::
\left(I - \frac{\Delta t}{2} A\right) y_{n+1}
= \left(I + \frac{\Delta t}{2} A\right) y_n.
Parameters¶
spec : CapacitorBankSpec
Physical specification of the bank.
initial_voltage_V : float, default 0.0
Initial bank voltage. Must lie in [0, spec.voltage_max_V].
spec
property
¶
Underlying immutable bank specification.
state
property
¶
Current observable state.
natural_peak_current_a
property
¶
Return the instantaneous short-circuit current bound.
Returns¶
float
Conservative natural-response bound |v_C| / sqrt(L / C) in
amperes for an initially charged, zero-current series LC bank.
reset(voltage_V=0.0)
¶
Reset the bank to voltage_V with zero current and t = 0.
step(dt, external_load_current_A=0.0)
¶
Advance the bank state by dt using Crank-Nicolson.
Parameters¶
dt : float Time step in seconds. Must be strictly positive. external_load_current_A : float, default 0.0 Instantaneous current drawn by an external load attached to the capacitor, in amperes. Zero recovers the natural response.
Raises¶
ValueError
If dt is not strictly positive.
discharge(pulse, dt, n_steps)
¶
Drive the bank with a prescribed load-current waveform.
Steps n_steps times with the load current sampled from the pulse
waveform at the centre of each interval, tracks peak voltage and
peak current observed during the run, and returns an
:class:EnergyReport summarising the energy budget.
Energy bookkeeping always satisfies the sampled invariant
energy_delivered + energy_remaining == initial_total_energy where
total energy is :math:\tfrac12 C v_C^2 + \tfrac12 L i_L^2. The
delivered amount is the electromagnetic energy removed from storage by
the external load and the series resistance during the sampled pulse.
Parameters¶
pulse : PulseSpec Prescribed load-current waveform descriptor. dt : float Integration step in seconds; must be strictly positive. n_steps : int Number of integration steps; must be strictly positive.
Raises¶
ValueError
If dt or n_steps is not strictly positive.
feasibility(pulse)
¶
Cheap admissibility check for a candidate pulse against bank state.
Returns a tuple (feasible, reason). The check is conservative;
a pulse failing this check definitely cannot run, but a passing
pulse may still under-deliver under detailed simulation.
Two guards are applied, in order:
- The requested peak current must not exceed the natural
short-circuit peak :math:
V_0 / Z_0(with :math:Z_0 = \sqrt{L/C}), for non-zero initial voltage. Inductive storage :math:\tfrac{1}{2} L i^2is recoverable (returned by the coil at pulse end), so it is not counted against the available energy. - The bank's stored energy must cover the resistive dissipation
:math:
R \, \langle i^2 \rangle \, \tau— the irreversible Joule heating in the series resistance.
recharge_status(t)
¶
Project bank state after t seconds of linear-power recharge.
The bank is modelled as accepting constant electrical power
recharge_power_kW (clipped at voltage_max_V); the energy
balance gives
:math:V(t) = \sqrt{V_0^2 + 2 P_\mathrm{recharge} t / C}.
Returns a dictionary with keys target_voltage_V,
projected_voltage_V, and time_to_full_s. When
recharge_power_kW is zero the projected voltage stays at the
current value and time_to_full_s is +inf.
Raises¶
ValueError
If t is negative.
analytical_voltage_underdamped(spec, t, v0)
¶
Underdamped voltage closed form.
:math:v_C(t) = V_0 \, e^{-\alpha t} \left[ \cos(\omega_d t) + (\alpha / \omega_d) \sin(\omega_d t) \right],
valid when spec.regime is RLCRegime.UNDERDAMPED.
analytical_current_underdamped(spec, t, v0)
¶
Underdamped current closed form.
:math:i(t) = \dfrac{V_0}{\omega_d L} \, e^{-\alpha t} \sin(\omega_d t).
analytical_voltage_critically_damped(spec, t, v0)
¶
Critically damped voltage closed form: :math:v_C(t) = V_0 \, e^{-\alpha t} (1 + \alpha t).
analytical_current_critically_damped(spec, t, v0)
¶
Critically damped current closed form: :math:i(t) = (V_0 / L) \, t \, e^{-\alpha t}.
analytical_voltage_overdamped(spec, t, v0)
¶
Overdamped voltage closed form.
With :math:\beta = \sqrt{\alpha^2 - \omega_0^2} and
:math:s_{1,2} = -\alpha \pm \beta,
:math:v_C(t) = V_0 \, \dfrac{s_1 e^{s_2 t} - s_2 e^{s_1 t}}{s_1 - s_2}.
analytical_current_overdamped(spec, t, v0)
¶
Overdamped current closed form.
:math:i(t) = \dfrac{V_0}{L (s_1 - s_2)} \left( e^{s_1 t} - e^{s_2 t} \right).
Worked example¶
A high-Q bank (R = 0.5 Ω, L = 100 µH, C = 100 µF, V_max = 10 kV) initialised at 5 kV, advanced 100 µs at 1 µs steps:
from scpn_mif_core.lifecycle import CapacitorBank, CapacitorBankSpec
spec = CapacitorBankSpec(
capacitance_F=100e-6,
inductance_H=100e-6,
series_resistance_ohm=0.5,
voltage_max_V=10_000.0,
recharge_power_kW=10.0,
)
bank = CapacitorBank(spec, initial_voltage_V=5000.0)
for _ in range(100):
bank.step(1e-6)
print(f"voltage = {bank.state.voltage_V:.3f} V, current = {bank.state.current_A:.3f} A")
Validation summary¶
| Check | Result |
|---|---|
| Spec invariants (immutability and seven rejection paths, including non-finite parameters and non-finite maximum capacitor energy) | 8 unit tests pass |
| Regime classification across the three branches | 5 unit tests pass |
Analytical closed-form boundary conditions at t = 0 |
3 unit tests pass |
| Free-response dispatch matches the closed forms | 3 unit tests pass |
| Crank-Nicolson agreement with the analytical free response within 1e-3 over 100 µs | 3 parametric tests pass |
| Underdamped half-cycle swing below zero | 1 unit test passes |
| Overdamped monotone decay over 1 ms | 1 unit test passes |
| Constructor and reset guards (4 negative paths) | 4 unit tests pass |
| Total-energy bookkeeping (½ C V² + ½ L I²) and component exposure | 2 unit tests pass |
| Lean proof surface | Stored-energy and recharge-energy sign contracts build with lake build |
| State immutability | 2 unit tests pass |
| Pulse-spec invariants | 3 unit tests pass |
| Waveform helpers across boundary conditions and unknown waveform rejection | 5 unit tests pass |
| Discharge energy conservation at 1e-12 relative tolerance | 1 unit test passes |
| Discharge regime, duration, peak-current recording | 3 unit tests pass |
| Load-drained-more-than-natural contrast | 1 unit test passes |
| Discharge guards (zero dt, zero n_steps) | 2 unit tests pass |
| Feasibility happy path, energy-rejection, Z₀-rejection, and named natural-peak formula | 6 unit tests pass |
| Recharge status (target, zero-t, long-t saturation, linear energy, negative-t, zero-power) | 6 unit tests pass |
| Hypothesis property — natural response stays in the initial-voltage envelope | 80 randomised examples pass |
| Python ↔ Rust parity across the regime grid and 16 random seeds | 144 parity tests pass at 1e-12 |
| Julia reference — three regimes, free response, Crank-Nicolson stepping, total-energy components, external load, reset, finite-energy rejection paths | 22 tests pass |
| Total | Python/Rust core suite plus Julia reference tests pass under their dedicated gates |
Benchmarks¶
The benchmark harness ships at bench/kernels/bench_capacitor_bank.py (in
the repository tree) and measures three operation classes:
| Operation | Group |
|---|---|
Single Crank-Nicolson step call |
capacitor_bank.step_single |
| 1 000-step batch (steady-state) | capacitor_bank.step_batch_1000 |
free_response analytical dispatch |
capacitor_bank.free_response |
Run locally with:
make bridge # build the Rust extension
pytest bench/kernels/bench_capacitor_bank.py --benchmark-only
Results land in bench/results/capacitor_bank.json; the multi-language
dispatch table at bench/dispatch.toml is updated to track the fastest
measured backend.
The named natural-peak current bound is not a benchmarked kernel path. It is a
single state observable used by the Python feasibility guard
(natural_peak_current_a) and mirrored in the Rust (natural_peak_current_a)
and Julia (natural_peak_current_A) lifecycle surfaces for formula parity.
Measured on the local workstation with Python 3.12.3, Rust 1.85.0, and Julia 1.12.6. This is local comparison evidence, not CPU-isolated production benchmark evidence. Julia is measured through CLI startup, so those entries validate parity and dispatch ordering rather than in-process kernel latency.
| Operation | Python (NumPy) | Rust | Julia CLI | Dispatch order |
|---|---|---|---|---|
step single Crank-Nicolson update |
11.3 µs | 294 ns | 462 ms | Rust, Python, Julia |
step 1 000-step batch |
10.4 ms | 87.2 µs | 2.86 s | Rust, Python, Julia |
free_response analytical dispatch |
908 ns | 130 ns | 233 ms | Rust, Python, Julia |
The Python and Rust paths agree at machine epsilon (≤ 10⁻¹² relative
tolerance, verified across 144 parity tests in
tests/unit/lifecycle/test_capacitor_bank_rust_parity.py), including total
stored energy and the capacitor/inductor component fields. The Julia
reference is covered by julia/SCPNMIFCore/test/runtests.jl, including all
three damping regimes, natural free response, Crank-Nicolson stepping,
total-energy component bookkeeping, external-load contrast, reset, and
rejection paths.
Cross-repository touch points¶
- Consumed by SCPN-CONTROL once
CON-C.2lands atscpn-control == 0.21.0. The Python facade signature in this module is the contract surface the CONTROL adapter must preserve. See the upstream contract (internal,docs/internal/upstream_contracts/03_scpn_control.md). - Consumed by
PulsedScenarioScheduler(MIF-004) as the bank feasibility gate prior to aBURNtransition. - Consumed by
PulsedShotMPCAdapter(CON-C.5) as the constraint source on NMPC-issued burn commands.
References¶
- Maron, Y., et al. (2018). Pulsed power and ultra-high-current discharge dynamics. Physical Review X 8, 041018.