Transient simulations¶
The groundinsight.simulation.transient module extends the package
beyond the steady-state phasor solve into the time domain. A
TransientStudy binds a network, an active fault and one or more
user-defined source waveforms together and produces a
ResultTransient with EPR and shield-current time series at the
declared observation points.
Physical / modelling context¶
The frequency-domain solver answers "given a sinusoidal injection at frequency \(f\), what is the EPR?". Real fault currents are non-sinusoidal: they switch on at fault inception, may carry an exponentially decaying DC offset, and switch off again at clearing. Two complementary solver paths are implemented to capture that behaviour:
- FFT solver (
solver="fft") — samples the user waveform on a regular time grid, transforms to the frequency domain via NumPy's real-valued FFT, evaluates the existing nodal-admittance solve at every FFT bin, and transforms the bus voltages back via IFFT. It reusesBusType.impedance_formulaandBranchType.self_impedance_formulaand is therefore consistent with the stationary results bin-by-bin. Only current sources are accepted, and mutual coupling is not evaluated by this path. - State-space solver (
solver="state_space") — assembles a modified-nodal-analysis ODE system \(\dot x = A x + B u\), \(y = C x + D u\) from the lumped RLC fields onBusTypeandBranchType(R_formula,L_formula,C_formula,R_self_formula,L_self_formula,C_self_formula,R_mutual_formula,M_mutual_formula) and integrates withscipy.signal.lsim. Voltage sources, Carson-style mutual coupling and pi-section branch capacitance are supported.
Source waveforms are produced by the small library in
groundinsight.simulation.waveforms: step,
sinusoidal_with_dc_offset (the textbook
single-line-to-ground fault current with DC asymmetry) and
damped_oscillation. Custom waveforms are any vectorised callable
f(t) -> values.
Example¶
import groundinsight as gi
from groundinsight import waveforms
# Assume `net` is a built network with one current source 'infeed'
# and a fault 'fault1'.
study = gi.TransientStudy(network=net, fault_name="fault1")
study.set_source_waveform(
"infeed",
waveforms.sinusoidal_with_dc_offset(
amplitude=1e3, frequency_hz=50.0,
t_on=0.02, t_off=0.12,
dc_amplitude=500.0, dc_decay_tau=0.05,
),
)
study.set_observation(buses=["bus_fault"], branches=["cable_1"])
result = study.solve(t_end=0.2, dt=1e-4, solver="fft")
# Plot the time series
gi.plot_epr_transient(result=result, title="EPR transient")
gi.plot_branch_current_transient(result=result, title="Shield current")
# Long-format DataFrame for further post-processing
df = result.to_polars()
Switching to the state-space solver only requires changing the
solver argument and ensuring the network's bus and branch types
carry the lumped RLC formulas required by the ODE form.
API reference¶
Transient study and result¶
transient ¶
Transient Simulation Layer.
This module hosts the high-level :class:TransientStudy workflow and the
matching :class:ResultTransient Pydantic model. The first solver path
implemented here is FFT-based: a user-defined source waveform is sampled
on a regular time grid, transformed to a frequency spectrum via NumPy's
real-valued FFT, fed into the existing per-frequency network solve, and
transformed back via IFFT. The state-space solver path is reserved for
the next release; it will reuse the same study object and observation
contract.
Design choices recorded in the Phase 3 discussion:
- Default and currently only supported source mode for the FFT solver is
the legacy current source (
Source.source_type='current'). A current source's frequency-dependentvaluesare replaced by the FFT spectrum of the user-supplied waveform; the rest of the network remains untouched. Voltage-mode sources will be supported by the state-space solver in Phase 4 (where the loop closure is naturally part of the ODE system). - Observation points (buses for EPR, branches for shield current) are
passed explicitly to
set_observation-- there is no "all" default, to keep memory and post-processing tractable on large networks. - Mutual coupling is not yet evaluated by the FFT solver; the phase current along each path would itself be time-dependent and is more natural to handle in the state-space formulation. The FFT solver ignores mutual impedance and emits no warning -- it is documented as a known limitation and the demo notebook shows how to interpret the resulting EPR.
ResultTransient ¶
Bases: BaseModel
Container for the time-domain results of a transient simulation.
Attributes:
| Name | Type | Description |
|---|---|---|
time_s |
list of float
|
Time samples in seconds, equally spaced. |
epr_t |
dict of str to list of float
|
Mapping of observed bus name to its EPR time series in volts. |
i_branch_t |
dict of str to list of float
|
Mapping of observed branch name to its branch current time series in amperes. |
source_t |
dict of str to list of float
|
The sampled source waveforms keyed by source name, in the natural unit of the source (amperes for current sources, volts for voltage sources). |
fault |
str
|
Name of the fault that was active during the simulation. |
solver |
str
|
Identifier of the solver that produced this result
( |
Notes
The list-typed fields use plain Python lists for JSON
serialisability. Convert to numpy.ndarray at the call site if you
need vectorised post-processing.
to_polars ¶
Convert the result to a Polars DataFrame in long form.
Returns:
| Type | Description |
|---|---|
DataFrame
|
A DataFrame with columns |
Source code in src/groundinsight/simulation/transient.py
TransientStudy ¶
High-level entry point for transient simulations.
A study binds together a :class:Network, the active fault to study,
a set of source waveforms (one per source contributing to the fault),
and the list of observation points (buses and branches) that should be
returned as time series. Calling :meth:solve produces a
:class:ResultTransient.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
The network model. Buses, branches, sources and the active fault are taken from this object. The network is not mutated. |
required |
fault_name
|
str
|
Name of the fault to activate for this study. |
required |
Examples:
>>> import groundinsight as gi
>>> from groundinsight.simulation import waveforms
>>> study = gi.TransientStudy(network, fault_name='F1')
>>> study.set_source_waveform(
... 'src',
... waveforms.sinusoidal_with_dc_offset(
... amplitude=1e3, frequency_hz=50.0,
... t_on=0.02, t_off=0.12,
... dc_amplitude=500.0, dc_decay_tau=0.05,
... ),
... )
>>> study.set_observation(buses=['bus_fault'], branches=['line1'])
>>> result = study.solve(t_end=0.2, dt=1e-4)
Source code in src/groundinsight/simulation/transient.py
set_observation ¶
Declare which buses and branches should be returned as time series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
buses
|
list of str
|
Names of buses whose EPR |
None
|
branches
|
list of str
|
Names of branches whose shield current |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a name is not present in the network. |
Source code in src/groundinsight/simulation/transient.py
set_source_waveform ¶
Bind a time-domain waveform to a network source.
For source_type='current' the waveform is the injected current
in amperes. For source_type='voltage' the waveform is the
Thevenin EMF in volts (the source_impedance is taken from the
:class:Source definition). Voltage sources are accepted by the
state-space solver but rejected by the FFT solver, which still
only supports current sources — the relevant solver checks the
type at solve() time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source_name
|
str
|
Name of a source defined on the network. |
required |
waveform
|
callable
|
Vectorised function |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the source does not exist on the network. |
Source code in src/groundinsight/simulation/transient.py
solve ¶
Run the transient solve and return the resulting time series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t_end
|
float
|
End time of the simulation in seconds. |
required |
dt
|
float
|
Time-step in seconds. For the FFT solver this determines the
Nyquist frequency |
required |
solver
|
(fft, state_space)
|
Solver to use. |
'fft'
|
Returns:
| Type | Description |
|---|---|
ResultTransient
|
Time-domain results at the observation points. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no source waveform is set, if the time parameters are invalid, or if the network is missing the lumped RLC fields required by the state-space solver. |
NotImplementedError
|
If |
Source code in src/groundinsight/simulation/transient.py
Waveforms¶
waveforms ¶
Waveform Library for Transient Simulations.
This module provides a small library of factory functions that return
Callable[[np.ndarray], np.ndarray] waveforms suitable for the
:class:groundinsight.simulation.transient.TransientStudy solver. Each
factory captures its own parameters and returns a vectorised function
f(t) -> values that can be called with an array of time samples.
The included waveforms cover the typical fault-current scenarios for low- and medium-voltage grounding studies:
- :func:
step-- a Heaviside-style on/off pulse, the simplest fault model (the source comes on att_onand off again att_off). - :func:
sinusoidal_with_dc_offset-- a power-frequency current with an exponentially decaying DC component, the textbook representation of a single-line-to-ground fault current including the asymmetry caused by the inductive loop. - :func:
damped_oscillation-- a damped sinusoid useful for switching transients and ringing studies.
Custom waveforms can be defined as any user function that accepts a 1-D
np.ndarray of time samples and returns an array of the same shape.
damped_oscillation ¶
damped_oscillation(
amplitude: float,
frequency_hz: float,
decay_tau: float,
*,
phase_rad: float = 0.0,
t_on: float = 0.0,
t_off: Optional[float] = None
) -> Callable[[np.ndarray], np.ndarray]
Construct a damped sinusoid, useful for switching-transient studies.
The waveform is::
x(t) = A * exp(-(t - t_on)/tau) * sin(omega*(t - t_on) + phi)
inside the on-window [t_on, t_off), and zero outside.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amplitude
|
float
|
Initial peak amplitude. |
required |
frequency_hz
|
float
|
Oscillation frequency in Hz. |
required |
decay_tau
|
float
|
Decay time constant in seconds. |
required |
phase_rad
|
float
|
Phase offset of the sinusoid, in radians. Defaults to |
0.0
|
t_on
|
float
|
Onset time, in seconds. Defaults to |
0.0
|
t_off
|
float
|
Cut-off time, in seconds. |
None
|
Returns:
| Type | Description |
|---|---|
callable
|
A vectorised waveform. |
Examples:
>>> w = damped_oscillation(
... amplitude=1e3, frequency_hz=500.0, decay_tau=2e-3,
... t_on=0.01,
... )
Source code in src/groundinsight/simulation/waveforms.py
sinusoidal_with_dc_offset ¶
sinusoidal_with_dc_offset(
amplitude: float,
frequency_hz: float,
*,
phase_rad: float = 0.0,
t_on: float = 0.0,
t_off: Optional[float] = None,
dc_amplitude: float = 0.0,
dc_decay_tau: Optional[float] = None
) -> Callable[[np.ndarray], np.ndarray]
Construct a windowed sinusoid with optional exponentially decaying DC.
Models the typical single-line-to-ground fault current::
i(t) = A * sin(omega*(t - t_on) + phi) + I_dc * exp(-(t - t_on)/tau)
multiplied by a rectangular window between t_on and t_off.
The DC component captures the asymmetric peak that arises when the
fault occurs at a non-zero-crossing instant of the source voltage
and the loop has finite inductance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amplitude
|
float
|
Peak value of the AC component. |
required |
frequency_hz
|
float
|
Power frequency in Hz (e.g. 50 or 60). |
required |
phase_rad
|
float
|
Initial phase of the AC component, in radians. Defaults to |
0.0
|
t_on
|
float
|
Fault initiation time, in seconds. Defaults to |
0.0
|
t_off
|
float
|
Fault clearing time, in seconds. |
None
|
dc_amplitude
|
float
|
Initial value of the DC component. Defaults to |
0.0
|
dc_decay_tau
|
float
|
Time constant |
None
|
Returns:
| Type | Description |
|---|---|
callable
|
A vectorised waveform |
Examples:
>>> w = sinusoidal_with_dc_offset(
... amplitude=20e3, frequency_hz=50.0,
... t_on=0.02, t_off=0.12,
... dc_amplitude=10e3, dc_decay_tau=0.05,
... )
Source code in src/groundinsight/simulation/waveforms.py
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | |
step ¶
step(
amplitude: float,
*,
t_on: float = 0.0,
t_off: Optional[float] = None
) -> Callable[[np.ndarray], np.ndarray]
Construct a rectangular pulse waveform.
The output is zero before t_on, equals amplitude between
t_on and t_off and is zero again after t_off. With
t_off=None the pulse extends to the end of the time grid
(a classic Heaviside step).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amplitude
|
float
|
Plateau value of the pulse. |
required |
t_on
|
float
|
Time at which the pulse switches on, in seconds. Defaults to
|
0.0
|
t_off
|
float
|
Time at which the pulse switches off, in seconds. |
None
|
Returns:
| Type | Description |
|---|---|
callable
|
A vectorised waveform function |
Examples:
>>> import numpy as np
>>> w = step(amplitude=100.0, t_on=0.02, t_off=0.12)
>>> y = w(np.linspace(0.0, 0.2, 5))
Source code in src/groundinsight/simulation/waveforms.py
The matching matplotlib helpers plot_epr_transient and
plot_branch_current_transient are documented on the
Plotting page.