Skip to content

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,

\[ E_\mathrm{stored}(t) = \frac{1}{2} C v_C(t)^2 + \frac{1}{2} L i_L(t)^2, \]

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:

\[ \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 \(\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,

\[ \left( I - \frac{\Delta t}{2} A \right) y_{n+1} = \left( I + \frac{\Delta t}{2} A \right) y_n + \Delta t \, b, \qquad A = \begin{pmatrix} 0 & -1/C \\ 1/L & -R/L \end{pmatrix}, \qquad b = \begin{pmatrix} -i_\mathrm{load} / C \\ 0 \end{pmatrix}, \]

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:

  1. 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^2 is recoverable (returned by the coil at pulse end), so it is not counted against the available energy.
  2. 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).

free_response(spec, t, v0)

Return (v_C(t), i(t)) of the series RLC natural response at time t.

Dispatches to the analytical formulas for the regime implied by spec.regime. The initial conditions are v_C(0) = v0 and i(0) = 0.

Raises

ValueError If t is negative.

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.2 lands at scpn-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 a BURN transition.
  • Consumed by PulsedShotMPCAdapter (CON-C.5) as the constraint source on NMPC-issued burn commands.

References

  1. Maron, Y., et al. (2018). Pulsed power and ultra-high-current discharge dynamics. Physical Review X 8, 041018.