Contingency Analysis

Goal

This class aims to make faster (and easier) the computations of a security analysis (which is the results of some powerflow after the disconnection of one or more powerlines)

This function is much (much) faster than its pure grid2op counterpart. For example, on the case 118, to simulate all n-1 contingencies you can expect a ~20x speed ups compared to using the grid2op obs.simulate(…, time_step=0) while obtaining the exact same results (see section Benchmarks)

It can be used as:

import grid2op
from lightsim2grid import ContingencyAnalysis
from lightsim2grid import LightSimBackend
env_name = ...
env = grid2op.make(env_name, backend=LightSimBackend())

security_analysis = ContingencyAnalysis(env)
security_analysis.add_multiple_contingencies(...) # or security_analysis.add_single_contingency(...)
res_p, res_a, res_v = security_analysis.get_flows()

# in this results, then
# res_p[row_id] will be the active power flows (origin side), on all powerlines corresponding to the `row_id` contingency.
# res_a[row_id] will be the current flows, on all powerlines corresponding to step "row_id"
# res_v[row_id] will be the complex voltage, on all bus of the grid corresponding to the `row_id` contingency.
# you can retrieve which contingency is id'ed `row_id` with `security_analysis.contingency_order[row_id]`

For now this relies on grid2op, but we could imagine a version of this class that can read to / from other data sources.

Note

A more advanced usage is given in the examples\contingency_analysis.py file from the lightsim2grid package.

Note

If you need to vary the injection and a contingency per simulation (instead of one shared base case with add_single_contingency / add_multiple_contingencies), see Scenario Sweep – a related but deliberately different API, row-aligned rather than a set of distinct scenarios.

Note

Set security_analysis.init_from_n_powerflow = True before the computation actually runs (eg before get_flows / compute_V / run are called – setting it afterwards has no effect) to initialize each contingency with the voltage solution of the pre-contingency (“n”) case instead of a flat start – this is usually faster. See lightsim2grid.contingencyAnalysis.ContingencyAnalysis.init_from_n_powerflow().

Handling contingencies that split the grid

By default, a contingency that splits the grid into several connected components (an “islanding”) is not simulated: the corresponding row of the results is left at 0. (for the voltages) and NaN (for the flows). You can list which contingencies split the grid with lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.is_grid_connected_after_contingency().

Starting from lightsim2grid 1.0.0, you can opt in to a mode that does simulate these contingencies, on the largest connected component, by setting the handle_disconnected_grid attribute to True:

import grid2op
from lightsim2grid import ContingencyAnalysis
from lightsim2grid import LightSimBackend
env = grid2op.make(..., backend=LightSimBackend())

security_analysis = ContingencyAnalysis(env)
security_analysis.add_all_n1_contingencies()

# opt in: simulate the largest island instead of skipping split contingencies
security_analysis.handle_disconnected_grid = True

res_p, res_a, res_v = security_analysis.get_flows()

When this mode is enabled and a contingency splits the grid:

  • the largest connected component is solved as a regular powerflow;

  • the buses of the other component(s) are masked: their voltage is reported as 0. (same convention as a skipped contingency) and they do not influence the solved component;

  • if a slack generator ends up in a masked component, its slack weight is set to 0 and the remaining slack weights are rescaled so the slack power is shared only among the live slacks.

This is implemented without re-triggering the symbolic factorization of the linear solver (the Jacobian, resp. the DC matrix, sparsity pattern is left unchanged), so it stays compatible with the speed of the contingency analysis. To make it possible, the reference slack is chosen once, before the computation, so as to minimise the number of contingencies that still have to be skipped (those that would disconnect the chosen reference slack itself).

The mode works both in AC (with a Newton-Raphson algorithm) and in DC: in DC the masked buses’ rows of the reduced system are forced to the identity (so their angle is 0), the masked injections are dropped and the slack imbalance is computed on the live component only. In both cases the masked buses are reported as 0..

Note

This mode requires a Newton-Raphson algorithm (AC, the default) or the DC solver. Selecting an AC non Newton-Raphson algorithm (eg Gauss-Seidel or Fast-Decoupled) and enabling the mode raises an error.

Running the contingencies on multiple threads

Starting from lightsim2grid 1.0.0, the contingencies can be solved on several CPU threads at once. By default everything runs on a single thread (nb_thread = 1), reproducing the exact same behaviour (and results) as before. Set the nb_thread attribute to a value greater than 1 to split the work:

import grid2op
from lightsim2grid import ContingencyAnalysis
from lightsim2grid import LightSimBackend
env = grid2op.make(..., backend=LightSimBackend())

security_analysis = ContingencyAnalysis(env)
security_analysis.add_all_n1_contingencies()

# solve the contingencies on 4 threads
security_analysis.nb_thread = 4

res_p, res_a, res_v = security_analysis.get_flows()

Internally the contingency list is split into nb_thread contiguous ranges, and each range is solved by its own thread. To stay correct (and lock-free), every thread works on its own solver instance and its own copy of the admittance matrix, and writes to a distinct set of rows of the (shared) result matrix. As a consequence:

  • the results do not depend on the number of threads: nb_thread changes the timing, not the numbers. They match the sequential results up to the solver’s convergence tolerance (each thread keeps its own solver warm-start state, so the converged voltages agree to roughly 1e-13, far below the powerflow tolerance);

  • it works for the AC (Newton-Raphson), DC and handle_disconnected_grid modes alike;

  • there is a small per-thread set-up cost (one extra solver “warm-up” and one admittance-matrix copy per additional thread), so the speed-up is sub-linear and most useful when there are many contingencies to simulate.

This feature only relies on the C++ standard library (std::thread): no additional dependency (MPI, OpenMP, …) is required.

Note

nb_thread is also available on the lower-level ContingencyAnalysisCPP class (same semantics).

Benchmarks (nb_thread scaling)

The script below scans nb_thread from 1 to 8 on a single, reasonably large “real”-topology grid (case6515rte, up to 1001 n-1 contingencies), and is available by running, from the root of the lightsim2grid repository:

cd benchmarks
python3 benchmark_ca_nb_threads.py

Results, made with:

  • date: 2026-09-21 11:22 CEST

  • system: Linux 6.8.0-60-generic

  • OS: ubuntu 22.04

  • processor: 13th Gen Intel(R) Core(TM) i7-13700H

  • python version: 3.12.8.final.0 (64 bit)

  • numpy version: 2.4.6

  • pandas version: 2.3.3

  • pandapower version: 3.5.4

  • grid2op version: 1.12.5.dev0

  • lightsim2grid version: 1.1.0

  • lightsim2grid extra information:

    • klu_solver_available: True

    • nicslu_solver_available: True

    • cktso_solver_available: True

    • compiled_march_native: True

    • compiled_o3_optim: True

nb_thread

nb solved

time (ms)

pf / s

speed-up vs 1 thread

1

703

3585.64

196

1.00x

2

703

2071.7

339

1.73x

3

703

1639.31

429

2.19x

4

703

1345.77

522

2.66x

5

703

1199.29

586

2.99x

6

703

1071.66

656

3.35x

7

703

1107.51

635

3.24x

8

703

1082.07

650

3.31x

As documented above, speed-up is sub-linear (the per-thread set-up cost, plus the already-fast single-thread baseline, both eat into the theoretical nb_thread-x speed-up): most of the gain is captured by 4-5 threads, with diminishing returns beyond that on this grid / contingency count on the tested hardware.

Reporting limit violations

Starting from lightsim2grid 1.0.0, if you have set some operating limits on the grid model (per-bus voltage bounds with lightsim2grid.network.LSGrid.set_bus_voltage_limits(), per-side current limits on lines / trafos with lightsim2grid.network.LSGrid.set_line_current_limit_side1() / lightsim2grid.network.LSGrid.set_line_current_limit_side2() / lightsim2grid.network.LSGrid.set_trafo_current_limit_side1() / lightsim2grid.network.LSGrid.set_trafo_current_limit_side2()), ContingencyAnalysis can report, for the pre-contingency (“n”) case and for each simulated contingency, which of these limits are violated. The API is modeled after pypowsybl’s security analysis (res.pre_contingency_result.limit_violations / res.post_contingency_results), with the notable difference that post_contingency_results is a list (ordered like the contingencies were added), not a dictionary:

import numpy as np
import grid2op
from lightsim2grid import ContingencyAnalysis
from lightsim2grid import LightSimBackend
env = grid2op.make(..., backend=LightSimBackend())

# set some limits on the grid model (kV for buses, kA for lines / trafos)
grid = env.backend._grid
nb_bus = grid.get_bus_vn_kv().shape[0]
grid.set_bus_voltage_limits(0.95 * grid.get_bus_vn_kv(), 1.05 * grid.get_bus_vn_kv())
grid.set_line_current_limit_side1(line_limit_a1_ka)
grid.set_line_current_limit_side2(line_limit_a2_ka)
grid.set_trafo_current_limit_side1(trafo_limit_a1_ka)
grid.set_trafo_current_limit_side2(trafo_limit_a2_ka)

# the feature must be explicitly requested at construction time
security_analysis = ContingencyAnalysis(env, compute_limit_violations=True)
security_analysis.add_single_contingency(0, name="line_0")  # `name` is optional
security_analysis.add_all_n1_contingencies()

res = security_analysis.run()  # or run_ac() / run_dc() to also pick the algorithm family

for violation in res.pre_contingency_result.limit_violations:
    print(violation.element_type, violation.element_id, violation.side,
          violation.violation_type, violation.value, violation.limit)

for cont in res.post_contingency_results:  # a list, in the order contingencies were added
    print(cont.element_ids, cont.contingency_name, cont.converged, cont.limit_violations)

Each LimitViolation reports:

  • element_type: lightsim2grid.contingencyAnalysis.ViolationElementType (BUS, LINE, TRAFO, or GRID for a non-converged contingency, see below);

  • element_id: the grid-model bus id (for BUS) or the local line / trafo id (for LINE / TRAFO); unused (-1) for GRID;

  • side: 1 or 2 for LINE / TRAFO (unused, 0, for BUS / GRID);

  • violation_type: lightsim2grid.contingencyAnalysis.LimitViolationType (LOW_VOLTAGE, HIGH_VOLTAGE, CURRENT, NOT_SIMULATED or DIVERGENCE, see below);

  • value / limit: the value reached and the limit that was violated; unused (NaN) for NOT_SIMULATED / DIVERGENCE;

  • name: for LINE / TRAFO, the element’s own name (see LSGrid.set_line_names / set_trafo_names); for BUS, the name of the substation the violating bus belongs to (see LSGrid.set_substation_names – there is no per-bus name, only per-substation ones); empty string if names were never set on the grid for the relevant kind, or for GRID.

Each ContingencyResult reports element_ids (the branch ids disconnected by this contingency – always present, even without a name) and the corresponding element_names, the optional user-supplied contingency_name (set via add_single_contingency(..., name=...)), whether the contingency converged, and its limit_violations.

Warning

This feature is opt-in and must be requested at construction time (ContingencyAnalysis(env, compute_limit_violations=True), or by setting security_analysis.compute_limit_violations = True before running). Leaving it to its default (False) means run / run_ac / run_dc raise a RuntimeError, and the usual get_flows() is completely unaffected – there is no need to pay for the extra per-element voltage / current checks if you only want the flows.

Setting compute_limit_violations through the property (rather than at construction time) clears any contingency already registered (add_single_contingency / add_all_n1_contingencies / add_multiple_contingencies) – it is the exact same reset performed by clear() / change_algorithm. If you follow this page’s own example order (add contingencies, then flip the flag), you will silently end up running run() on zero contingencies. Either set compute_limit_violations=True at construction time (as in the example above), or re-add the contingencies after setting it:

security_analysis = ContingencyAnalysis(env)
security_analysis.add_all_n1_contingencies()
security_analysis.compute_limit_violations = True  # clears the contingencies above!
security_analysis.add_all_n1_contingencies()  # so they must be re-added

res = security_analysis.run()

Also, if a contingency does not converge, its limit_violations contains exactly one LimitViolation with element_type == ViolationElementType.GRID and violation_type either LimitViolationType.NOT_SIMULATED (a pre-check – eg it splits the grid in multiple connected components – skipped this contingency without ever invoking the solver) or LimitViolationType.DIVERGENCE (the solver ran but did not converge, eg reached max_iter). This is unlike a converged case with an empty limit_violations list, which genuinely means no violation was found.

The pre-contingency (“n”) case is different: if it does not converge, compute (and thus run / run_ac / run_dc) raises a RuntimeError instead, since every contingency is solved relative to that base case – a diverging base case makes the whole analysis meaningless.

Violation checking is fully compatible with the handle_disconnected_grid and nb_thread options described above: it is performed inline, per contingency, inside the thread that solves it (not as a separate post-processing pass), so it does not affect the multi-threading performance characteristics; and masked (disconnected-island) buses are correctly excluded from the voltage checks.

Adjusting the violation threshold

By default a violation is reported beyond the configured limit, by more than a relative tolerance violation_rel_tol (see Values sitting on their limit: violation_rel_tol): a value on its limit up to rounding, e.g. a bus a regulator holds at its vmax, is not a violation. The violation_threshold attribute (available both on ContingencyAnalysis and on the lower-level ContingencyAnalysisCPP) lets you tighten that margin, so that situations approaching a limit are reported before they actually breach it. It is a float in ]0., 1.] and defaults to 1.0, which reproduces the behaviour described above:

security_analysis = ContingencyAnalysis(env, compute_limit_violations=True)
security_analysis.add_all_n1_contingencies()

# report anything that reaches 95% of its limit, instead of only actual breaches
security_analysis.violation_threshold = 0.95

res = security_analysis.run()

Setting it below 1.0 makes every check stricter – more violations are reported, never fewer. Each of the three checks owns one interval, running from a “healthy” anchor to the limit that can be violated, and the threshold moves that limit towards its anchor – a linear interpolation, identical for all three:

effective_limit = threshold * limit + (1 - threshold) * anchor

check

anchor

limit

violates when

CURRENT

0

limit_a

value > threshold * limit_a

LOW_VOLTAGE

vn_kv

vmin_kv

v < threshold * vmin + (1 - threshold) * vn

HIGH_VOLTAGE

vn_kv

vmax_kv

v > threshold * vmax + (1 - threshold) * vn

A line’s usable range really is [0, limit_a], so its anchor is 0 and the rule reduces to plainly scaling the limit. A voltage bound has no such natural “zero” end – so the bus nominal voltage is used instead, since that is what operating limits are conventionally expressed around (\(\pm x\%\) of vn). Either way, each acceptable interval keeps a width of exactly threshold times its original one. For a \(\pm 5\%\) band around nominal:

threshold

effective band (pu)

band width

1.0

[0.9500, 1.0500]

0.100

0.95

[0.9525, 1.0475]

0.095

0.9

[0.9550, 1.0450]

0.090

0.5

[0.9750, 1.0250]

0.050

Note

The voltage anchor is really vn_kv clamped into [vmin_kv, vmax_kv]. An operating band is not required to bracket the nominal voltage, and real data does violate it: a 380 kV level declared with an operating range of [390, 450] kV is ordinary practice on the European 400 kV network. Such a grid loads and analyses normally; the clamp simply pins the anchor to the nearer bound, so that bound has no margin left to give while the opposite one still tightens as usual. Where the band does bracket the nominal voltage – the overwhelmingly common case – the clamp does nothing at all.

Note

Anchoring both voltage checks on the nominal voltage – rather than on each other – is what keeps them independent. The LOW_VOLTAGE verdict is a function of vmin_kv, the anchor and the threshold alone, so setting vmax_kv, changing it, or leaving it at NaN can never alter it; and symmetrically for HIGH_VOLTAGE. It also means the two effective bounds each converge towards the anchor from their own side and can therefore never cross: no bus is ever reported as both too low and too high, whatever the threshold. A useful corollary is that a bus sitting above its nominal voltage can never be reported as LOW_VOLTAGE, however small the threshold gets.

The reported value and limit of each LimitViolation are unaffected by all this: they remain the value actually reached and the limit exactly as you configured it. Only the test deciding whether a violation is reported at all is shifted.

Note

A bus whose limits are inconsistent – vmin_kv > vmax_kv, the only way the two effective bounds can still end up crossed – raises a RuntimeError rather than being reported as an arbitrary one of the two violation types, which would hide a genuine misconfiguration.

Note

Like handle_disconnected_grid / nb_thread, violation_threshold is a plain runtime knob and only affects the next run. There is one asymmetry worth knowing: lowering it discards any already-computed results (they were computed with a looser threshold and would under-report), while raising it keeps them (a stricter result still contains everything a looser one would find). Unlike compute_limit_violations, neither case clears the registered contingencies, so it is enough to run again:

res = security_analysis.run()
security_analysis.violation_threshold = 0.9  # drops the results above ...
res = security_analysis.run()                # ... no need to re-add anything

Values sitting on their limit: violation_rel_tol

Some values sit on their limit by construction – typically a bus a generator regulates exactly at its vmax. A solve leaves such a value a few ulps on either side of the limit, so with a bare > the last bit of rounding would decide whether it is reported, and two solvers (the one-contingency-at-a-time solve, the batch, gpusim2grid) would disagree. Every check therefore asks the value to clear its effective limit by a relative margin, violation_rel_tol (a float in [0., 1.[, default 1e-9 – about 0.4 mV on a 400 kV bus), on both ContingencyAnalysis / ScenarioSweep and their *CPP classes:

CURRENT        value > threshold * limit_a * (1 + violation_rel_tol)
LOW_VOLTAGE    v     < low_eff  * (1 - violation_rel_tol)
HIGH_VOLTAGE   v     > high_eff * (1 + violation_rel_tol)

0. gives the bare strict comparisons. The reported value / limit are unaffected. Changing it, in either direction, discards the already-computed results (the registered contingencies are kept). LSGrid.get_violations(threshold, ac, rel_tol) takes the same tolerance.

Benchmarks (Contingency Analysis)

Here are some benchmarks made with:

  • date: 2026-08-28 16:57 CEST

  • system: Linux 6.8.0-60-generic

  • OS: ubuntu 22.04

  • processor: 13th Gen Intel(R) Core(TM) i7-13700H

  • python version: 3.12.8.final.0 (64 bit)

  • numpy version: 2.3.5

  • pandas version: 2.3.3

  • pandapower version: 3.4.0

  • grid2op version: 1.12.5.dev0

  • lightsim2grid version: 1.0.0

  • lightsim2grid extra information:

    • klu_solver_available: True

    • nicslu_solver_available: True

    • cktso_solver_available: True

    • compiled_march_native: True

    • compiled_o3_optim: True

This benchmark is available by running, from the root of the lightsim2grid repository:

cd benchmarks
python3 contingency_analysis.py

For this setting the outputs are:

For environment: l2rpn_neurips_2020_track2_small (177 n-1 simulated)
Total time spent in "computer" to solve everything: 8.4ms (21151 pf / s), 0.05 ms / pf)
    - time to compute the coefficients to simulate line disconnection: 0.31ms
    - time to pre process Ybus: 0.03ms
    - time to perform powerflows: 7.99ms (22144 pf / s, 0.05 ms / pf)
In addition, it took 0.44 ms to retrieve the current from the complex voltages (in total 20101.6 pf /s, 0.05 ms / pf)

Comparison with raw grid2op timings
It took grid2op (with lightsim2grid, using obs.simulate): 0.27s to perform the same computation
    This is a 30.5 speed up from ContingencyAnalysis over raw grid2op (using obs.simulate and lightsim2grid)
It took grid2op (with pandapower, using obs.simulate): 11.32s to perform the same computation
    This is a 1286.1 speed up from ContingencyAnalysis over raw grid2op (using obs.simulate and pandapower)

In this case then, the `ContingencyAnalysis` module is 31 times faster than raw grid2op (with obs.simulate and lightsim2grid) and 1286 times faster than raw grid2op (with obs.simulate and pandapower)
All results match !

Detailed usage

Classes:

ContingencyAnalysis(grid2op_env[, ...])

This class allows to perform a "security analysis" from a given grid state.

ContingencyAnalysisCPP

Allows the computation of "security analysis", that consists in computing the flows that would result from the disconnection of one or multiple disconnections of some powerlines.

ContingencyResult(element_ids, ...)

Limit violations for a single simulated contingency.

LimitViolation

A single limit violation, as detected by ContingencyAnalysisCPP.

LimitViolationType

The kind of limit that was violated: LOW_VOLTAGE / HIGH_VOLTAGE (a bus voltage

PreContingencyResult(converged, ...)

Limit violations for the pre-contingency ("n", no disconnection) case.

SecurityAnalysisResult(...)

Result of ContingencyAnalysis.run / run_ac / run_dc, modeled after pypowsybl's security analysis result.

ViolationCategory

What KIND of statement a LimitViolation is -- a property of its violation_type, and the first thing to read: the three kinds do not mean the same thing and must not be acted on the same way.

ViolationElementType

The kind of element on which a limit was violated: BUS (a voltage limit), LINE /

class lightsim2grid.contingencyAnalysis.ContingencyAnalysis(grid2op_env, compute_limit_violations: bool = False)[source]

This class allows to perform a “security analysis” from a given grid state.

For now, you cannot change the grid state, and it only computes the security analysis with current flows at origin of powerlines.

Feel free to post a feature request if you want to extend it.

This class is used in 4 phases:

  1. you create it from a grid2op environment (the grid topology will not be modified from this environment)

  2. you add some contingencies to simulate

  3. you start the simulation

  4. you read back the results

Examples

An example is given here

import grid2op
from lightsim2grid import ContingencyAnalysis
from lightsim2grid import LightSimBackend
env_name = ...
env = grid2op.make(env_name, backend=LightSimBackend())

0) you create
contingency_analysis = ContingencyAnalysis(env)

1) you add some contingencies to simulate
contingency_analysis.add_multiple_contingencies(...) # or contingency_analysis.add_single_contingency(...)

2) you start the simulation (done automatically)
3) you read back the results
res_p, res_a, res_v = contingency_analysis.get_flows()

# in this results, then
# res_a[row_id] will be the flows, on all powerline corresponding to the `row_id` contingency.
# you can retrieve it with `contingency_analysis.contingency_order[row_id]`

Notes

Sometimes, the behaviour might differ from grid2op. For example, if simulating a contingency leads to a non connected grid, then this function will return “Nan” for the flows and 0. for the voltages.

In grid2op, it would be, in this case, 0. for the flows and 0. for the voltages.

By default, a contingency that splits the grid in multiple connected components is not simulated (its voltages are left at 0.). If you set the handle_disconnected_grid attribute to True, such contingencies are instead simulated on their largest connected component: the buses of the other component(s) are “masked” and their voltage is reported as 0. This is done without triggering any extra matrix re-factorization (the symbolic factorization of the solver is reused). It is supported by the Newton-Raphson family (AC) and by the DC solver; a non Newton-Raphson AC algorithm (eg Gauss-Seidel or Fast-Decoupled) is rejected.

Methods:

add_all_n1_contingencies()

This method registers as the contingencies that will be computed all the contingencies that disconnects 1 powerline

add_multiple_contingencies(*args)

This function will add multiple contingencies at the same time.

add_single_contingency(*args[, name])

This function allows to add a single contingency specified by either the powerlines names (which should match env.name_line) or by their ID.

clear([with_contlist])

Clear the list of contingencies to simulate

close()

permanently close the object

compute_A()

This function returns the current flows (in Amps, A) at the origin / high voltage side

compute_P()

This function returns the active power flows (in MW) at the origin / high voltage side

compute_V()

This function allows to retrieve the complex voltage at each bus of the grid for each contingency.

get_flows(*args)

Retrieve the flows after each contingencies has been simulated.

get_physical_violations()

Per contingency, in the C++-side order (my_defaults()): the list of LimitViolation of the physical limits that contingency's solution leaves.

get_physical_violations_n()

Same as get_physical_violations(), for the pre-contingency ("n") case.

run()

Run this contingency analysis and report, for the pre-contingency ("n") case and for each registered contingency, the list of limit violations (bus voltage out of [vmin_kv, vmax_kv], line/trafo current above limit_a1_ka / limit_a2_ka -- see LSGrid.set_bus_voltage_limits / set_line_current_limit_side1 / set_line_current_limit_side2 / set_trafo_current_limit_side1 / set_trafo_current_limit_side2).

run_ac()

Like run, but first makes sure an AC algorithm is selected (switches to NR_KLU / NR_SparseLU if the current algorithm is a DC one -- which clears any previously registered contingency, exactly like change_algorithm; does nothing if already AC).

run_dc()

Like run, but first makes sure the DC algorithm is selected (switches to DC_KLU / DC_SparseLU if the current algorithm is an AC one -- which clears any previously registered contingency, exactly like change_algorithm; does nothing if already DC).

Attributes:

compute_limit_violations

Whether limit violations are computed inline, per contingency, during run / run_ac / run_dc (see also get_violations on the underlying computer).

compute_physical_violations

Whether every converged contingency reports the PHYSICAL limits its solution leaves -- a state the grid cannot reach at all, as opposed to the operational limits compute_limit_violations reports (a voltage band, a thermal rating: states the grid does reach and should not sit in).

handle_disconnected_grid

Whether a contingency that splits the grid into several connected components is simulated on its largest component instead of being skipped.

init_from_n_powerflow

Whether to initialize the complex voltages of each contingency with the results of a "n" powerflow (a powerflow without any line disconnection) instead of a flat start.

keep_vinit_at_group_controlled_buses

Whether the buses a voltage-control group regulates (regulated from elsewhere, by an SVC, or by several controllers at least one of which is remote) keep the magnitude of the starting voltage instead of being set to their set-point before each solve.

nb_thread

1).

physical_violation_tol_mva

Absolute slack on every comparison compute_physical_violations makes, so that an element resting exactly on its limit is not reported over solver noise: a violation needs value > limit + tol (or value < limit - tol for LOW_Q and HVDC_AC_EMULATION_RELEASE).

physical_violation_tol_vm_pu

The same as physical_violation_tol_mva, in pu, for the comparisons compute_physical_violations makes on a voltage: the PQ -> PV release check reports a flagged PQ generator (LSGrid.set_gen_can_be_pv) whose regulated bus is below (at min_q) or above (at max_q) its target by more than this (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q), and the standby SVC check a flagged idle SVC (LSGrid.set_svc_standby) whose regulated bus is outside its automaton's thresholds by more than this (LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY), and the remote voltage control check a remote controller whose own bus is outside the realistic range by more than this (LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL).

redistribute_slack

Whether the active power a contingency loses (the elements of the island it cuts off, simulated with handle_disconnected_grid) is first shared on the remaining units of the distributed slack as OpenLoadFlow's DistributedSlack outer loop does: proportionally to their weight, each one clamped to its [min_p, max_p] and never crossing 0 MW, a clamped unit leaving the pool (and that contingency's distributed slack), the powerflow then only sharing what is left (the change in the losses) on the units that can still move.

violation_rel_tol

Relative tolerance (a float in [0., 1.[, default 1e-9) of every limit-violation check performed when compute_limit_violations is True: a value is reported only when it is beyond its effective limit (see violation_threshold) by more than this fraction of that limit.

violation_threshold

Threshold (a float in ]0., 1.], default 1.0) applied to every limit-violation check performed when compute_limit_violations is True.

add_all_n1_contingencies()[source]

This method registers as the contingencies that will be computed all the contingencies that disconnects 1 powerline

This is equivalent to:

for single_cont_id in range(env.n_line):
    self.add_single_contingency(single_cont_id)
add_multiple_contingencies(*args)[source]

This function will add multiple contingencies at the same time.

This code is equivalent to:

for single_cont in args:
    self.add_single_contingency(single_cont)

It does not accept any keword arguments.

Examples

import grid2op
from lightsim2grid import ContingencyAnalysis
from lightsim2grid import LightSimBackend
env_name = ...
env = grid2op.make(env_name, backend=LightSimBackend())

contingency_analysis = ContingencyAnalysis(env)

# add a single contingency that disconnect powerline 2 and 3 at the same time
contingency_analysis.add_single_contingency(env.name_line[2], 3)

# add a multiple contingencies the first one disconnect powerline 2 and
# and the second one disconnect powerline 3
contingency_analysis.add_multiple_contingencies(env.name_line[2], 3)
add_single_contingency(*args, name=None)[source]

This function allows to add a single contingency specified by either the powerlines names (which should match env.name_line) or by their ID.

The contingency added can be a “n-1” which will simulate a single powerline disconnection or a “n-k” which will simulate the disconnection of multiple powerlines.

It does not accept any positional keyword arguments, but accepts the keyword-only name argument: an optional, user-supplied string used to identify this contingency in the result of run / run_ac / run_dc (ContingencyResult.contingency_name). If not given, contingency_name is None for this contingency. If this exact contingency was already registered (same set of disconnected elements), name is ignored (the first registration wins).

Examples

import grid2op
from lightsim2grid import ContingencyAnalysis
from lightsim2grid import LightSimBackend
env_name = ...
env = grid2op.make(env_name, backend=LightSimBackend())

contingency_analysis = ContingencyAnalysis(env)
# the single (n-1) contingency "disconnect powerline 0" is added
contingency_analysis.add_single_contingency(0)

# add the single (n-1) contingency "disconnect line 1
contingency_analysis.add_single_contingency(env.name_line[1])

# add a single contingency that disconnect powerline 2 and 3 at the same time
contingency_analysis.add_single_contingency(env.name_line[2], 3)

Notes

If it raises an error for a given contingency, the object might be not properly initialized. In this case, we recommend you to clear it (using the clear() method and to attempt to add contingencies again.)

clear(with_contlist=True)[source]

Clear the list of contingencies to simulate

close()[source]

permanently close the object

compute_A()[source]

This function returns the current flows (in Amps, A) at the origin / high voltage side

Warning

Order of the results

The order in which the results are returned is NOT necessarily the order in which the contingencies have been entered. Please use get_flows() method for easier reading back of the results !

compute_P()[source]

This function returns the active power flows (in MW) at the origin / high voltage side

Warning

Order of the results

The order in which the results are returned is NOT necessarily the order in which the contingencies have been entered. Please use get_flows() method for easier reading back of the results !

compute_V()[source]

This function allows to retrieve the complex voltage at each bus of the grid for each contingency.

Warning

Order of the results

The order in which the results are returned is NOT necessarily the order in which the contingencies have been entered. Please use get_flows() method for easier reading back of the results

property compute_limit_violations

Whether limit violations are computed inline, per contingency, during run / run_ac / run_dc (see also get_violations on the underlying computer). Default: false. Computing violations means an extra per-element current / voltage check in every contingency’s solve, so leave this off if you only need get_flows. Can only be set at construction time (ContingencyAnalysis(env, compute_limit_violations=True)) or via this setter; changing it clears any previously-computed results.

property compute_physical_violations

Whether every converged contingency reports the PHYSICAL limits its solution leaves – a state the grid cannot reach at all, as opposed to the operational limits compute_limit_violations reports (a voltage band, a thermal rating: states the grid does reach and should not sit in). Default: False. See get_physical_violations() and ContingencyResult.physical_violations.

Each check is a condition a PowSyBl OpenLoadFlow outer loop acts on, and none enforced here (nothing is switched PV -> PQ or back, no droop is clamped, no machine leaves the slack distribution, no contingency is re-solved):

  • the reactive capability of every bus whose voltage is held by machines (LOW_Q / HIGH_Q on the BUS): did it need more reactive power than the sum of what its voltage-regulating generators, storage units, hvdc converter stations and voltage-mode SVCs can produce? Per bus, not per machine – the split between the machines of one bus is a sharing convention rather than something the solver decides. OpenLoadFlow’s ReactiveLimits.

  • the release of every PQ generator flagged as pinned at a reactive limit (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q on the GENERATOR, see lightsim2grid.network.LSGrid.set_gen_can_be_pv()): does the bus it would regulate sit below its target while the machine absorbs all it can (or above it while it produces all it can)? The other direction, PQ -> PV, of the same ReactiveLimits loop. value and limit in kV, compared with physical_violation_tol_vm_pu. A grid with no flagged generator reports nothing here.

  • the switch on of every idle SVC flagged as carrying a standby automaton (LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY on the SVC, see lightsim2grid.network.LSGrid.set_svc_standby()): does the bus it regulates sit outside the automaton’s voltage thresholds? OpenLoadFlow’s MonitoringVoltageOuterLoop. value and limit in kV, compared with physical_violation_tol_vm_pu. A grid with no flagged SVC reports nothing here.

  • the own bus of every generator regulating a remote bus, when a realistic range is set (LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL on the GENERATOR, see lightsim2grid.network.LSGrid.set_remote_voltage_control_vm_range()): does holding the remote target take it outside that range? OpenLoadFlow’s ReactiveLimits, in its robust remote voltage control mode. value and limit in kV, compared with physical_violation_tol_vm_pu.

  • the active power of every angle-droop (“AC emulation”) hvdc line still in the linear regime (HIGH_P on the HVDC): did p0 + k.(theta1 - theta2) leave pmax_1to2_mw / pmax_2to1_mw? OpenLoadFlow’s HvdcAcEmulationLimits.

  • the release of every angle-droop hvdc line flagged as frozen at its active power limit (HVDC_AC_EMULATION_RELEASE on the HVDC, see lightsim2grid.network.LSGrid.set_hvdc_ac_emulation_frozen()): would its droop ask for less than that limit? The other half of the same HvdcAcEmulationLimits loop. side the direction it is frozen in, value and limit in MW.

  • the active power of every generator and every storage unit carrying the distributed slack (LOW_P / HIGH_P on the GENERATOR / STORAGE): the slack is solved inside the Jacobian by fixed participation factors that know nothing about limits, so target_p + its share of the imbalance can land beyond min_p_mw / max_p_mw. Per machine, unlike the reactive check: the active split is not a convention, it is the participation factors the caller chose. Needs those limits, which are optional (lightsim2grid.network.LSGrid.set_gen_p_limits() / lightsim2grid.network.LSGrid.set_storage_p_limits()); a grid without them reports nothing here. A storage unit’s are read – and its violation reported – in the generator convention, unlike its target_p_mw. OpenLoadFlow’s DistributedSlack.

Independent of compute_limit_violations: either can be on without the other (though run still requires compute_limit_violations, and fills physical_violations only when this one is on too). The active-power checks (the hvdc ones and the slack one) work in DC; the reactive one and the voltage ones need an AC algorithm that publishes its per-bus mismatch (every built-in AC algorithm does) and run / compute_V raise for one that does not. Changing this flag invalidates any computed result but keeps the registered contingencies.

get_flows(*args)[source]

Retrieve the flows after each contingencies has been simulated.

Each row of the resulting flow matrix will correspond to a contingency simulated in the arguments.

You can require only the result on some contingencies with the args argument, but in each case, all the results will be computed. If you don’t specify anything, the results will be returned for all contingencies (which we recommend to do)

Examples

import grid2op
from lightsim2grid import ContingencyAnalysis
from lightsim2grid import LightSimBackend
env_name = ...
env = grid2op.make(env_name, backend=LightSimBackend())

contingency_analysis = ContingencyAnalysis(env)
contingency_analysis.add_multiple_contingencies(...) # or contingency_analysis.add_single_contingency(...)
res_p, res_a, res_v = contingency_analysis.get_flows()

# in this results, then
# res_a[row_id] will be the flows, on all powerline corresponding to the `row_id` contingency.
# you can retrieve it with `contingency_analysis.contingency_order[row_id]`
get_physical_violations()[source]

Per contingency, in the C++-side order (my_defaults()): the list of LimitViolation of the physical limits that contingency’s solution leaves. Prefer run(), which returns them per contingency in the caller’s own order and alongside the operational ones. Requires compute_physical_violations to be True (raises otherwise).

get_physical_violations_n()[source]

Same as get_physical_violations(), for the pre-contingency (“n”) case. Requires compute_physical_violations to be True (raises otherwise).

property handle_disconnected_grid

Whether a contingency that splits the grid into several connected components is simulated on its largest component instead of being skipped. Default: False, meaning such a contingency is not simulated at all (its voltages are left at 0., see the class-level note above). When True, the buses of the other component(s) are “masked” (their voltage reported as 0.) and the largest component is solved normally, without triggering any extra matrix re-factorization. Supported by the Newton-Raphson family (AC) and by the DC solver; a non Newton-Raphson AC algorithm (eg Gauss-Seidel or Fast-Decoupled) is rejected.

property init_from_n_powerflow

Whether to initialize the complex voltages of each contingency with the results of a “n” powerflow (a powerflow without any line disconnection) instead of a flat start. Default: False. Must be set before the computation actually runs (eg before get_flows / compute_V / run are called); it has no effect on a contingency that has already been solved.

property keep_vinit_at_group_controlled_buses

Whether the buses a voltage-control group regulates (regulated from elsewhere, by an SVC, or by several controllers at least one of which is remote) keep the magnitude of the starting voltage instead of being set to their set-point before each solve. The solution is the same; with MaxVoltageChange damping it can be reached in far fewer iterations. Defaults to the grid’s own setting (see lightsim2grid.lightsim2grid_cpp.LSGrid.set_keep_vinit_at_group_controlled_buses()), False unless set there.

property nb_thread

1).

With nb_thread == 1 the behaviour is identical to the legacy sequential computation. With nb_thread > 1 the contingencies are split across that many threads (each with its own solver and admittance matrix copy); the results do not depend on the number of threads.

Type:

Number of OS threads used to solve the contingencies (default

property physical_violation_tol_mva

Absolute slack on every comparison compute_physical_violations makes, so that an element resting exactly on its limit is not reported over solver noise: a violation needs value > limit + tol (or value < limit - tol for LOW_Q and HVDC_AC_EMULATION_RELEASE). Default: 1e-4. In MVA – one noise floor for both halves, MW and MVAr being the same scale.

Unlike violation_threshold – a fraction, and one that only invalidates results when it is lowered – this is an absolute tolerance, and any change to it invalidates the computed results (in either direction: a smaller one reports violations the recorded results do not contain, a larger one leaves recorded ones that should no longer be reported). The registered contingencies are kept.

property physical_violation_tol_vm_pu

The same as physical_violation_tol_mva, in pu, for the comparisons compute_physical_violations makes on a voltage: the PQ -> PV release check reports a flagged PQ generator (LSGrid.set_gen_can_be_pv) whose regulated bus is below (at min_q) or above (at max_q) its target by more than this (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q), and the standby SVC check a flagged idle SVC (LSGrid.set_svc_standby) whose regulated bus is outside its automaton’s thresholds by more than this (LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY), and the remote voltage control check a remote controller whose own bus is outside the realistic range by more than this (LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL). Default: 1e-4. Changing it invalidates any previously-computed results.

property redistribute_slack

Whether the active power a contingency loses (the elements of the island it cuts off, simulated with handle_disconnected_grid) is first shared on the remaining units of the distributed slack as OpenLoadFlow’s DistributedSlack outer loop does: proportionally to their weight, each one clamped to its [min_p, max_p] and never crossing 0 MW, a clamped unit leaving the pool (and that contingency’s distributed slack), the powerflow then only sharing what is left (the change in the losses) on the units that can still move. Default: False. Needs LSGrid.set_gen_p_limits / set_storage_p_limits to clamp anything. Same as LSGrid.consider_only_main_component(redistribute_slack=True), contingency by contingency. Needs an algorithm with a distributed slack: compute refuses it with a single-slack one (NRSing_*, Gauss-Seidel).

run() → SecurityAnalysisResult[source]

Run this contingency analysis and report, for the pre-contingency (“n”) case and for each registered contingency, the list of limit violations (bus voltage out of [vmin_kv, vmax_kv], line/trafo current above limit_a1_ka / limit_a2_ka – see LSGrid.set_bus_voltage_limits / set_line_current_limit_side1 / set_line_current_limit_side2 / set_trafo_current_limit_side1 / set_trafo_current_limit_side2).

This requires compute_limit_violations=True (either passed at construction time or set via this_instance.compute_limit_violations = True), else a RuntimeError is raised. Prefer run_ac / run_dc if you want to also select the algorithm family.

A RuntimeError is also raised if the pre-contingency (“n”) powerflow itself does not converge – every contingency is solved relative to this base case, so a diverging base case makes the whole analysis meaningless.

The returned object mimics pypowsybl’s security analysis result:

res = contingency_analysis.run()
for v in res.pre_contingency_result.limit_violations:
    ...
for cont in res.post_contingency_results:  # a list, ordered like `add_single_contingency` calls
    cont.element_ids       # branch ids disconnected by this contingency (always present)
    cont.element_names     # names (env.name_line) of these same elements (always present)
    cont.contingency_name  # optional, user-supplied via add_single_contingency(..., name=...)
    cont.converged
    cont.limit_violations
    cont.physical_violations  # only if compute_physical_violations is on too

Note

limit_violations and physical_violations are kept apart because they are different KINDS of statement, not two flavours of the same one (see LimitViolation.category): an OPERATIONAL limit the grid can leave (a bus outside its voltage band, a branch above its rating) versus a PHYSICAL one it cannot (a bus needing reactive power its machines do not have, an hvdc converter transmitting more than it can – either makes the converged solution unreachable rather than merely undesirable). physical_violations is an empty list unless compute_physical_violations is also True.

Note

A converged == False post-contingency entry has exactly one LimitViolation in limit_violations, with element_type == ViolationElementType.GRID and violation_type either LimitViolationType.NOT_SIMULATED (a pre-check skipped it, eg it splits the grid) or LimitViolationType.DIVERGENCE (the solver ran but did not converge) – unlike a converged == True entry with an empty list, which genuinely means no violation was found.

run_ac() → SecurityAnalysisResult[source]

Like run, but first makes sure an AC algorithm is selected (switches to NR_KLU / NR_SparseLU if the current algorithm is a DC one – which clears any previously registered contingency, exactly like change_algorithm; does nothing if already AC).

run_dc() → SecurityAnalysisResult[source]

Like run, but first makes sure the DC algorithm is selected (switches to DC_KLU / DC_SparseLU if the current algorithm is an AC one – which clears any previously registered contingency, exactly like change_algorithm; does nothing if already DC).

property violation_rel_tol

Relative tolerance (a float in [0., 1.[, default 1e-9) of every limit-violation check performed when compute_limit_violations is True: a value is reported only when it is beyond its effective limit (see violation_threshold) by more than this fraction of that limit:

CURRENT        value > threshold * limit_a * (1 + violation_rel_tol)
LOW_VOLTAGE    v     < low_eff  * (1 - violation_rel_tol)
HIGH_VOLTAGE   v     > high_eff * (1 + violation_rel_tol)

It is there for the values that sit ON their limit by construction, typically a bus a generator regulates exactly at its vmax: a solve leaves such a value a few ulps on either side of the limit, and with a bare > the last bit of rounding decided whether it was reported (the one-contingency-at-a-time solve, the batch and gpusim2grid disagreed). 1e-9 is about 0.4 mV on a 400 kV bus. 0. gives the bare strict comparisons. The reported value / limit are unaffected.

Like violation_threshold, it only affects the next run / run_ac / run_dc; changing it (either way) invalidates any already-computed results, the registered contingencies being kept.

property violation_threshold

Threshold (a float in ]0., 1.], default 1.0) applied to every limit-violation check performed when compute_limit_violations is True. It is the fraction of the usable range that is still considered acceptable, so lowering it makes every check stricter (more violations reported, never fewer).

Each of the three checks owns one interval, running from a “healthy” anchor to the limit that can be violated, and the threshold moves that limit towards its anchor – a linear interpolation, identical for all three:

effective_limit = threshold * limit + (1 - threshold) * anchor

check

anchor

limit

violates when

CURRENT

0

limit_a

value > threshold * limit_a

LOW_VOLTAGE

vn_kv

vmin_kv

v < threshold * vmin + (1 - threshold) * vn

HIGH_VOLTAGE

vn_kv

vmax_kv

v > threshold * vmax + (1 - threshold) * vn

A line’s usable range really is [0, limit_a], so its anchor is 0 and the rule reduces to scaling the limit. A voltage bound has no such natural “zero” end, so the bus nominal voltage is used instead (operating limits are conventionally expressed as +/- x% of it), clamped into [vmin_kv, vmax_kv] for the rare real-world band that does not bracket it. Either way each acceptable interval keeps a width of exactly threshold times its original one.

Anchoring both voltage checks on vn_kv – rather than on each other – keeps them independent: the LOW_VOLTAGE verdict depends on vmin_kv, vn_kv and the threshold alone, so setting vmax_kv (or leaving it at NaN) can never change it, and vice versa. The two effective bounds also converge towards vn_kv from their own side and can never cross, so no bus is ever both too low and too high.

A bus with inconsistent limits (vmin_kv > vmax_kv) raises a RuntimeError rather than being reported as an arbitrary one of the two types. The reported value / limit are never rescaled by the threshold; only the test deciding whether to report is shifted.

On top of the effective limit, a value must clear the relative margin violation_rel_tol (default 1e-9) to be reported, so a value on its limit up to rounding is not a violation.

The default 1.0 reproduces the previous, threshold-less behaviour. Like nb_thread / handle_disconnected_grid, this is a plain runtime knob: it only affects the next run / run_ac / run_dc. Lowering it invalidates any already-computed results (the registered contingencies are kept, so it is enough to run again); raising it back up does not.

class lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP

Allows the computation of “security analysis”, that consists in computing the flows that would result from the disconnection of one or multiple disconnections of some powerlines.

This is a “raw” c++ class, for an easier to use interface, please refer to the python documentation of the lightsim2grid.contingencyAnalysis.ContingencyAnalysis class.

Warning

This function might give wrong result for lightsim2grid version 0.5.5 were they were a bug : when some contingencies made the grid non connex, it made all the other contingencies diverge. This bug has been fixed in version 0.6.0 and this is why we do not recommend to use this feature with lightsim2grid version < 0.6.0 !

Note

Even if you instruct it to simulate the same contingency multiple times, it will only do it once.

Note

You can only simulate disconnection of powerlines / transformers

At a glance, this class should be used in three steps:

  1. Modify the list of contingencies to simulate, with the functions:

2) Then you can start the computation of the security analysis with lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute() then optionally lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute_flows() .

3) And finally inspect the results with lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.get_flows() and lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.get_voltages() .

Methods:

add_all_n1(self)

This allows to add all the "n-1" in the contingency list to simulate.

add_multiple_n1(self, arg0)

This allows to add a multiple "n-1" in the contingency list to simulate (it will add as many contingency as the size of the list) and is equivalent to call multiple times lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.add_n1()

add_n1(self, arg0)

This allows to add a single "n-1" in the contingency list to simulate.

add_nk(self, arg0)

This allows to add a single "n-k" in the contingency list to simulate (it will only add at most one contingency)

adjoint_memory_bytes(self)

Bytes the kept Jacobians occupy after a compute() (0 when none were kept).

adjoint_row_ok(self)

Per row: True where the last solve_JT() actually solved that row's adjoint system.

adjoint_solver_stats(self)

Linear-solver counters and timings of the last solve_JT(), summed over its worker threads: how much of the backward pass went into refactorizing each row's Jacobian versus into the transposed solves themselves.

amps_computation_time(self)

Time spent in computing the flows (in amps) after the voltages have been computed at each nodes

available_algorithm_names(self)

Returns the names of all registered algorithms, including any loaded plugins, as a list of string.

available_default_algorithms(self)

Return the list of the names of the algorithm available on the current lightsim2grid installation.

base_case_was_reused(self)

Whether the last compute() kept a base case instead of building one.

change_algorithm(*args, **kwargs)

Overloaded function.

change_solver(*args, **kwargs)

Overloaded function.

clear(self)

Clear the list of all contingencies.

clear_results_only(self)

Clear the list of all contingencies.

close(self)

Clear the solver and to as if the class never performed any powerflow.

compute(self, arg0, arg1, arg2)

Compute the voltages (at each bus of the grid model) for some time series of injections (productions, loads, storage units, etc.)

compute_branch_results(self)

Active and reactive power at both ends of every branch, for every row of the last compute(), as an array of shape (nb_rows, nb_branch, 4): [..., 0] and [..., 1] are P (MW) and Q (MVAr) entering the branch at side 1, [..., 2] and [..., 3] the same at side 2.

compute_flows(self)

Compute the current flows (in amps, at the origin of each powerlines / high voltage size of each transformers.

compute_power_flows(self)

Compute the current flows (in MW, at the origin of each powerlines / high voltage size of each transformers.

converged(self)

Per contingency (row order matches my_defaults()): whether it converged / was actually simulated (False for skipped or diverged contingencies).

converged_mask(self)

Per-row convergence, as a list[bool] of length nb_steps() (row order matches the row order of whatever was solved: my_defaults() for ContingencyAnalysis, the injection / contingency matrices otherwise).

converged_n(self)

Whether the pre-contingency ('n') powerflow converged.

dim_J(self)

Dimension of the augmented Newton-Raphson system -- the length of one cotangent handed to solve_JT, and of one row of its result.

gen_v_indirect_grad(self, lambda_)

The indirect half of the gen_v gradient, (n_scenarios, n_gen): -lambda^T dF/dv, zero on a row that did not converge.

get_Ybus(self, row[, solver_numbering])

The admittance matrix row row of the last compute() was solved with (AC algorithm only): the base one without the branches that row disconnects.

get_algo_config(self)

Config (eg ScalingPolicyType / damping parameters) of the internal solver used for the pre-contingency ('n') and every per-contingency powerflow.

get_algo_name(self)

Registry name of the currently selected algorithm.

get_algo_type(self)

Return the type of the solver currently used.

get_branch_results(self)

The array of the last compute_branch_results(), without computing it again.

get_dcYbus(self, row[, solver_numbering])

The DC counterpart of get_Ybus (DC algorithm only): the real Bbus of row row.

get_flows(self)

Get the flows (in kA) at the origin side / high voltage side of each transformers / powerlines.

get_gen_results(self)

Generator results of the last compute(), shape (nb_rows, nb_gen, 2): [..., 0] is P (MW), [..., 1] is Q (MVAr, 0 in DC).

get_gen_v_share(self)

How much of its bus' derivative each generator's gen_v carries: 1 where it is the only regulator of that bus (or of that voltage-control group), 1/n where n of them share it, 0 where it carries none (-1 in both get_gen_v_target_bus and get_gen_v_vc_row).

get_gen_v_target_bus(self)

Per generator, the GRID bus whose voltage magnitude its gen_v set-point actually fixes -- or -1 where it fixes none, which is where its gradient is zero.

get_gen_v_vc_row(self)

Per generator, the Jacobian row of the voltage constraint |V_reg| + sum s.Q_c - v_set = 0 of the voltage-control group whose set-point its gen_v is (a generator regulating a bus a group holds: a remote regulator, or a local one on a group-controlled bus), -1 otherwise -- and -1 for a group holding an SVC or an hvdc station, whose set-point no batch moves.

get_linear_solver_stats(self)

Linear-solver counters of the whole compute() (nb_analyze, nb_factorize, nb_refactorize, ...), summed over the member algorithm and every worker thread's own.

get_linear_solver_stats_per_algo(self)

The same counters kept apart, one entry per algorithm: index 0 is the member one (which solves the 'n' warm-up case, and the whole batch when single-threaded), then one per worker thread of the last multi-threaded compute().

get_p_row_of_bus(self)

For each grid bus, the row of the Jacobian holding its ACTIVE power mismatch equation, or -1 where it has none.

get_physical_violations(*args, **kwargs)

Overloaded function.

get_physical_violations_n(*args, **kwargs)

Overloaded function.

get_power_flows(self)

Get the active flows (in MW) at the origin side / high voltage side of each transformers / powerlines.

get_q_row_of_bus(self)

Same as get_p_row_of_bus for the REACTIVE power mismatch equation.

get_row_nb_iter(self)

Per row of the last compute(): the number of iterations the algorithm reported (0 for a row never handed to the solver).

get_row_solve_times(self)

Per row of the last compute(): the time the algorithm reported for its solve, in seconds.

get_theta_col_of_bus(self)

For each grid bus (same numbering as the columns of get_voltages()), the column of the Jacobian holding its voltage-ANGLE unknown, or -1 where it has none (a reference slack, a bus outside the solver).

get_violations(self)

Per contingency (row order matches my_defaults()): list of LimitViolation.

get_violations_n(self)

List of LimitViolation for the pre-contingency ('n') case.

get_vm_col_of_bus(self)

Same as get_theta_col_of_bus for the voltage-MAGNITUDE unknown: -1 at a bus whose magnitude is held fixed (a PV bus, unless a generator contingency may release it).

get_voltages(self)

Get the complex voltage angles at each bus of the powergrid.

invalidate_base_case(self)

Drop the kept base case, so the next compute() builds a fresh one (see reuse_base_case).

is_grid_connected_after_contingency(self)

Low-level / internal primitive, not part of the stable public API: called directly by lightsim2grid's own machinery (eg LightSimBackend, the grid loaders, or another part of the C++ core) rather than meant for everyday use.

modif_Ybus_time(self)

Time spent to modify the Ybus matrix before simulating each contingency.

my_defaults(self)

Allows to inspect the contingency list that will be simulated.

nb_converged(self)

Number of powerflows, among those nb_solved() attempted, that actually converged.

nb_solved(self)

Total number of powerflows solved.

pick_reference_slack(self)

Over the registered contingencies, return the slack bus (gridmodel id) stranded by the fewest of them — feed it to LSGrid.set_reference_slack_bus (before building this object: it works on a copy of the grid) so handle_disconnected_grid skips as few contingencies as possible.

preprocessing_time(self)

Time spent in pre processing the data (this involves, the checking whether the grid would be still connex after the contingency for example)

remove_multiple_n1(self, arg0)

Remove multiple "n-1" contingency from the contingency list to simulate.

remove_n1(self, arg0)

Remove a single "n-1" contingency from the contingency list to simulate.

remove_nk(self, arg0)

Remove a single "n-k" contingency from the contingency list to simulate.

reset(self)

Clear the list of all contingencies.

set_algo_config(self, config)

See get_algo_config().

solve_JT(self, xbar)

Solve the adjoint system `J_i^T .

solve_time(self)

TODO

solver_time(self)

Total time spent only in solving the powerflows (excluding pre processing the data, post processing them, initializing everything etc.)

thread_algos_were_reused(self)

Whether the last compute() also kept the WORKER algorithms of the multi-threaded path, instead of building and analyzing one per thread.

thread_init_time(self)

Time (in seconds, float) spent building the per thread solvers.

total_time(self)

Total time spent in solving the powerflows, pre processing the data, post processing them, initializing everything etc.

Attributes:

compute_gen_results

the setpoint plus the share of the distributed slack for P; for Q the setpoint of a non-regulating machine, the algorithm's own value for a controller it solved, or a share of the bus' reactive residual proportional to each machine's reactive range.

compute_limit_violations

Whether limit violations are computed inline, per contingency, during compute() (see converged / get_violations / converged_n / get_violations_n).

compute_physical_violations

no bus is switched PV -> PQ or back, no droop is clamped, no machine leaves the slack distribution, no contingency is re-solved.

handle_disconnected_grid

Whether to simulate the contingencies that split the grid in multiple connected components.

init_from_n_powerflow

false, meaning each simulation is initialized with the given input vector

keep_jacobian

Whether each row's converged Jacobian is kept during compute(), so that solve_JT() can run afterwards.

keep_vinit_at_group_controlled_buses

Whether the buses a voltage-control group regulates (a bus regulated from elsewhere, by an SVC, or by several controllers at least one of which is remote) keep the magnitude of the starting voltage, in the 'n' solve and in every row, instead of being set to their set-point first.

nb_thread

Number of OS threads used to solve the contingencies (default 1).

physical_violation_tol_mva

a violation needs value > limit + tol (or value < limit - tol for LOW_Q and HVDC_AC_EMULATION_RELEASE).

physical_violation_tol_vm_pu

The same, in pu, for the voltage comparisons of the PQ -> PV release check (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q), of the standby SVC check (LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY) and of the remote voltage control check (LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL).

redistribute_slack

without limits the converged state is unchanged.

reuse_base_case

True).

violation_rel_tol

Relative tolerance (a float in [0., 1.[, default 1e-9) of every limit check performed when compute_limit_violations is True: a value is reported only when it is beyond its effective limit (see violation_threshold) by more than this fraction of that limit.

violation_threshold

Threshold (a float in ]0., 1.], default 1.0) applied to every limit check performed when compute_limit_violations is True.

add_all_n1(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → None

This allows to add all the “n-1” in the contingency list to simulate.

See also

lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.add_multiple_n1() to add multiple single contingencies in the same call (but not necessarily all)

add_multiple_n1(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: collections.abc.Sequence[SupportsInt | SupportsIndex]) → None

This allows to add a multiple “n-1” in the contingency list to simulate (it will add as many contingency as the size of the list) and is equivalent to call multiple times lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.add_n1()

See also

lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.add_all_n1() to add all the “n-1” contingencies.

Warning

A “n-k” will disconnect multiple powerlines at the same time. It’s not the same as adding muliple “n-1” contingencies, where powerlines will be disconnected one after the other.

Parameters:

vect_n1 (list (of int)) – The lines id you want to add to the contingency list

add_n1(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: SupportsInt | SupportsIndex) → None

This allows to add a single “n-1” in the contingency list to simulate.

See also

lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.add_all_n1() to add all contingencies at the same time

See also

lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.add_multiple_n1() to add multiple single contingencies in the same call.

Parameters:

line_id (int) – The line id you would like to see disconnected

add_nk(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: collections.abc.Sequence[SupportsInt | SupportsIndex]) → None

This allows to add a single “n-k” in the contingency list to simulate (it will only add at most one contingency)

Warning

A “n-k” will disconnect multiple powerlines at the same time. It’s not the same as adding muliple “n-1” contingencies, where powerlines will be disconnected one after the other.

Parameters:

vect_nk (list (of int)) – The lines id you want to add in the single contingency added.

adjoint_memory_bytes(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → int

Bytes the kept Jacobians occupy after a compute() (0 when none were kept). Grows as nb_rows * nnz(J): worth reading before scaling a batch up.

adjoint_row_ok(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → list[bool]

Per row: True where the last solve_JT() actually solved that row’s adjoint system. False for a row the batch never solved (it diverged, or a contingency islanded it), whose lambda is zero.

adjoint_solver_stats(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → lightsim2grid.lightsim2grid_cpp.LinearSolverStats

Linear-solver counters and timings of the last solve_JT(), summed over its worker threads: how much of the backward pass went into refactorizing each row’s Jacobian versus into the transposed solves themselves.

amps_computation_time(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → float

Time spent in computing the flows (in amps) after the voltages have been computed at each nodes

It is given in seconds (float).

available_algorithm_names(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → list[str]

Returns the names of all registered algorithms, including any loaded plugins, as a list of string.

available_default_algorithms(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → list[lightsim2grid.lightsim2grid_cpp.AlgorithmType]

Return the list of the names of the algorithm available on the current lightsim2grid installation.

This is a list of lightsim2grid.algorithm.AlgorithmType.

base_case_was_reused(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → bool

Whether the last compute() kept a base case instead of building one. Mostly of interest when measuring where a batch’s time goes.

change_algorithm(*args, **kwargs)

Overloaded function.

  1. change_algorithm(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: lightsim2grid.lightsim2grid_cpp.AlgorithmType) -> None

    This function allows to control which solver is used during the powerflow. See the section Available powerflow algorithms for more information about them.

    See also

    lightsim2grid.algorithm.AlgorithmType for a list of the available algorithms (NB: some algorithms might not be available on all platform)

    Note

    If the algorithm type entered is a DC algorithm (eg from lightsim2grid.algorithm.AlgorithmType, DC_SparseLU, DC_KLU or DC_NICSLU), it will change the _dc_solver otherwise the regular _solver is modified.

    Examples

    from lightsim2grid.algorithm import AlgorithmType
    # init the grid model
    from lightsim2grid.network import init_from_pandapower
    pp_net = ...  # any pandapower grid
    lightsim_grid_model = init_from_pandapower(pp_net)  # some warnings might be issued as well as some warnings
    
    # change the algorithm used for the powerflow
    # to use internally a Newton Raphson algorithm with the Eigen sparse LU linear solver
    lightsim_grid_model.change_algorithm(AlgorithmType.NR_SparseLU)
    
  2. change_algorithm(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: str) -> None

    Change the AC (or DC) algorithm by registry name. Accepts built-in names and plugin names registered via load_solver_plugin().

    See also

    change_algorithm() to change it by lightsim2grid.algorithm.AlgorithmType instead.

change_solver(*args, **kwargs)

Overloaded function.

  1. change_solver(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: lightsim2grid.lightsim2grid_cpp.AlgorithmType) -> None

DEPRECATED: use ‘change_algorithm’ instead

  1. change_solver(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: str) -> None

DEPRECATED: use ‘change_algorithm’ instead

clear(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → None

Clear the list of all contingencies. After a call to this method, you will need to re add some contingencies with

clear_results_only(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → None

Clear the list of all contingencies. After a call to this method, you will need to re add some contingencies with

close(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → None

Clear the solver and to as if the class never performed any powerflow.

compute(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: Annotated[numpy.typing.NDArray[numpy.complex128], '[m, 1]'], arg1: SupportsInt | SupportsIndex, arg2: SupportsFloat | SupportsIndex) → None

Compute the voltages (at each bus of the grid model) for some time series of injections (productions, loads, storage units, etc.)

Note

During this computation, the GIL is released, allowing easier parrallel computation

Parameters:
  • Vinit (numy.ndarray, complex) – First voltage at each bus of the grid model (including the disconnected buses)

  • max_iter (int) – Total number of iteration (>0 integer)

  • tol (float) – Solver tolerance (> 0. float)

compute_branch_results(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → numpy.typing.NDArray[numpy.float64]

Active and reactive power at both ends of every branch, for every row of the last compute(), as an array of shape (nb_rows, nb_branch, 4): [..., 0] and [..., 1] are P (MW) and Q (MVAr) entering the branch at side 1, [..., 2] and [..., 3] the same at side 2. Branches are numbered like get_flows: the lines, then the trafos.

Computed with the formulas the grid uses after a single powerflow, so a row reads what get_line_res1/2 and get_trafo_res1/2 would after a one-at-a-time solve of it. A branch out of service – in the grid, or disconnected by the row’s own contingency – reads 0, and so does every branch of a row that did not converge. In DC, Q is 0 and side 2 carries the opposite of side 1.

compute_flows(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.writeable', 'flags.c_contiguous']

Compute the current flows (in amps, at the origin of each powerlines / high voltage size of each transformers.

Warning

This function must be called after lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute() has been called.

Note

During this computation, the GIL is released, allowing easier parrallel computation

property compute_gen_results

the setpoint plus the share of the distributed slack for P; for Q the setpoint of a non-regulating machine, the algorithm’s own value for a controller it solved, or a share of the bus’ reactive residual proportional to each machine’s reactive range. A generator the row disconnects, or strands on a bus it masks, reads 0, and so does every generator of a row that did not converge. Takes effect at the next compute().

Type:

Whether every converged row records the active and reactive output of every generator (read with get_gen_results). Off by default. They follow the rules the grid uses after a single powerflow

property compute_limit_violations

Whether limit violations are computed inline, per contingency, during compute() (see converged / get_violations / converged_n / get_violations_n). Defaults to False. Computing violations means an extra per-element current / voltage check in every contingency’s solve, so users who only need compute_flows() / get_flows() should leave this off. Changing this flag clears any previously-computed results.

property compute_physical_violations

no bus is switched PV -> PQ or back, no droop is clamped, no machine leaves the slack distribution, no contingency is re-solved. The active-power checks (the hvdc ones and the slack one) work in DC too; the reactive one, the release one, the standby SVC one and the remote voltage control one need an AC algorithm that publishes its per-bus mismatch and compute() raises for one that does not.

Type:

Whether every converged contingency reports the PHYSICAL limits its solution leaves – a state the grid cannot reach, as opposed to the operational limits compute_limit_violations reports. Defaults to False. The checks

Type:

the reactive capability of every bus whose voltage is held by machines (LOW_Q / HIGH_Q on the BUS, summed over its generators, storage units, hvdc converter stations and voltage-mode SVCs), the release of every PQ generator flagged as pinned at a reactive limit whose regulated bus sits on the wrong side of its target (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q on the GENERATOR, see LSGrid.set_gen_can_be_pv), the switch on of every idle SVC flagged as carrying a standby automaton whose regulated bus sits outside its thresholds (LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY on the SVC, see LSGrid.set_svc_standby), the own bus of every generator regulating a remote bus, when a realistic range is set (LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL on the GENERATOR, see LSGrid.set_remote_voltage_control_vm_range), the active power of every angle-droop hvdc line in the linear regime (HIGH_P on the HVDC, against pmax_1to2_mw / pmax_2to1_mw), the release of every angle-droop hvdc line flagged as frozen at its active power limit whose droop would ask for less (HVDC_AC_EMULATION_RELEASE on the HVDC, see LSGrid.set_hvdc_ac_emulation_frozen), and the active power of every generator carrying the distributed slack (LOW_P / HIGH_P on the GENERATOR, against the optional min_p_mw / max_p_mw) – OpenLoadFlow’s ReactiveLimits (both directions), MonitoringVoltageOuterLoop, HvdcAcEmulationLimits and DistributedSlack. Detection only

compute_power_flows(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.writeable', 'flags.c_contiguous']

Compute the current flows (in MW, at the origin of each powerlines / high voltage size of each transformers.

Warning

This function must be called after lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute() has been called.

Note

During this computation, the GIL is released, allowing easier parrallel computation

converged(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → list[bool]

Per contingency (row order matches my_defaults()): whether it converged / was actually simulated (False for skipped or diverged contingencies).

converged_mask(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → list[bool]

Per-row convergence, as a list[bool] of length nb_steps() (row order matches the row order of whatever was solved: my_defaults() for ContingencyAnalysis, the injection / contingency matrices otherwise). Unlike converged() (ContingencyAnalysis-only, and only usable if compute_limit_violations=True was set), this is available on every batch class – TimeSeries, InjectionSweep, ContingencyAnalysis and ScenarioSweep alike – unconditionally, no setup required.

Row i is True iff that row was both invertible and reported convergence by the solver. A row that was never attempted at all (eg every row after the first failure in a TimeSeries, whose steps are chained and so stop there) is False too – indistinguishable from an outright divergence, the same “never attempted” == “did not converge” convention nb_solved() / nb_converged() already use.

converged_n(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → bool

Whether the pre-contingency (‘n’) powerflow converged.

dim_J(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → int

Dimension of the augmented Newton-Raphson system – the length of one cotangent handed to solve_JT, and of one row of its result. 0 until a compute() that kept the Jacobians.

gen_v_indirect_grad(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, lambda_: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous']) → Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]']

The indirect half of the gen_v gradient, (n_scenarios, n_gen): -lambda^T dF/dv, zero on a row that did not converge. For a generator fixing a bus magnitude (get_gen_v_target_bus) it is the dS/d|V| contraction; for one setting a voltage-control group’s set-point (get_gen_v_vc_row) it is lambda at the group’s voltage row, and the WHOLE gradient (zero on a row where handle_disconnected_grid stranded the group); zero elsewhere. Each already carries its get_gen_v_share.

lambda_ is what solve_JT() returned for this batch, so the adjoint system is solved once and both halves of the gradient read it. The other half is direct – the loss depends on V_k = v_k . exp(j.theta_k) explicitly – and is Re(conj(V_k/|V_k|) . dL/dV_k), the very quantity a PQ bus instead contributes to xbar: a bus whose magnitude is an unknown hands it to the adjoint, a bus whose magnitude is a set-point hands it to that set-point’s gradient.

AC Newton-Raphson only; raises on a DC algorithm.

get_Ybus(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, row: SupportsInt | SupportsIndex, solver_numbering: bool = False) → object

The admittance matrix row row of the last compute() was solved with (AC algorithm only): the base one without the branches that row disconnects. An entry only those branches contributed to is removed, not kept as an explicit zero, so the pattern is the one a one-at-a-time solve of that topology builds. Grid bus numbering, shape (total_bus, total_bus) like LSGrid.get_Ybus, or the solver one with solver_numbering=True.

get_algo_config(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → lightsim2grid.lightsim2grid_cpp.AlgoConfig

Config (eg ScalingPolicyType / damping parameters) of the internal solver used for the pre-contingency (‘n’) and every per-contingency powerflow. Copied once from the grid model’s own get_ac_algo_config() at construction time, then independent of it; re-apply with set_algo_config() if you change the grid model’s config afterwards, or after change_algorithm().

get_algo_name(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → str

Registry name of the currently selected algorithm. Unlike get_algo_type(), stays meaningful for plugin solvers (and built-ins with no dedicated AlgorithmType member).

get_algo_type(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → lightsim2grid.lightsim2grid_cpp.AlgorithmType

Return the type of the solver currently used.

This is equivalent to the get_type of the lightsim2grid.algorithm.AlgorithmSelector.get_type() of the solver used.

get_branch_results(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → numpy.typing.NDArray[numpy.float64]

The array of the last compute_branch_results(), without computing it again.

get_dcYbus(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, row: SupportsInt | SupportsIndex, solver_numbering: bool = False) → object

The DC counterpart of get_Ybus (DC algorithm only): the real Bbus of row row.

get_flows(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]']

Get the flows (in kA) at the origin side / high voltage side of each transformers / powerlines.

Each rows correspond to a contingency, each column to a powerline / transformer

Warning

This function must be called after lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute_flows() has been called. (compute_flows also requires that lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute() has been caleed)

Warning

The order in which the contingencies are computed is NOT (in this c++ class) the order in which you enter them. They are computed in the order given by lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.my_defaults(). For an easier, more “human readable” please use the lightsim2grid.contingencyAnalysis.ContingencyAnalysis.get_flows() method.

Returns:

As – The flows (in kA) at the origin side / high voltage side of each transformers / powerlines.

Return type:

numpy.ndarray (matrix)

get_gen_results(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → numpy.typing.NDArray[numpy.float64]

Generator results of the last compute(), shape (nb_rows, nb_gen, 2): [..., 0] is P (MW), [..., 1] is Q (MVAr, 0 in DC). Empty unless compute_gen_results was set before that compute().

get_gen_v_share(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.float64], '[m, 1]']

How much of its bus’ derivative each generator’s gen_v carries: 1 where it is the only regulator of that bus (or of that voltage-control group), 1/n where n of them share it, 0 where it carries none (-1 in both get_gen_v_target_bus and get_gen_v_vc_row).

Generators regulating one bus must be given the SAME set-point, so the loss is a function only on the diagonal v_1 = ... = v_n; off it there is no value to compare against, because such a row is refused rather than solved differently. The partial derivative of one set-point with the others held fixed therefore does not exist, and these n numbers are not a gradient in the usual sense – what exists is the derivative along the tie, and it is what they SUM to.

Split equally because that is what behaves: tie them (one parameter driving the group, which is what the degree of freedom really is) and the chain rule adds the shares back to the true derivative; treat them as separate parameters and step on all of them, and they move together, so the iterate stays where the function is defined. And no generator is privileged by the order it happens to sit in.

get_gen_v_target_bus(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']

Per generator, the GRID bus whose voltage magnitude its gen_v set-point actually fixes – or -1 where it fixes none, which is where its gradient is zero.

A generator fixes nothing when it is disconnected, not regulating, or treated as off; when the bus it regulates is pinned by an SVC or an hvdc converter station (whose set-point no batch moves); or when that bus keeps a magnitude unknown because a voltage-control group holds it (a remote regulator): its gen_v is then the group’s set-point, and its gradient is read at the group’s voltage row instead – see get_gen_v_vc_row.

get_gen_v_vc_row(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']

Per generator, the Jacobian row of the voltage constraint |V_reg| + sum s.Q_c - v_set = 0 of the voltage-control group whose set-point its gen_v is (a generator regulating a bus a group holds: a remote regulator, or a local one on a group-controlled bus), -1 otherwise – and -1 for a group holding an SVC or an hvdc station, whose set-point no batch moves. dF_v/dv_set = -1, so that generator’s gradient is lambda at that row (times its get_gen_v_share), which gen_v_indirect_grad already returns; it has no direct half.

get_linear_solver_stats(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → lightsim2grid.lightsim2grid_cpp.LinearSolverStats

Linear-solver counters of the whole compute() (nb_analyze, nb_factorize, nb_refactorize, …), summed over the member algorithm and every worker thread’s own.

A batch keeps the Jacobian’s sparsity pattern fixed for the whole run, so each algorithm analyzes ONCE and every row after its first only refactorizes. That is 1 analyze per algorithm used: 1 single-threaded, and with nb_thread > 1 one per worker plus the member one that solved the ‘n’ warm-up case. More than that means a row changed the sparsity pattern.

Use get_linear_solver_stats_per_algo() to tell ‘every algorithm analyzed once’ from ‘one algorithm analyzed several times’ – the sum alone cannot.

get_linear_solver_stats_per_algo(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → list[lightsim2grid.lightsim2grid_cpp.LinearSolverStats]

The same counters kept apart, one entry per algorithm: index 0 is the member one (which solves the ‘n’ warm-up case, and the whole batch when single-threaded), then one per worker thread of the last multi-threaded compute().

get_p_row_of_bus(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']

For each grid bus, the row of the Jacobian holding its ACTIVE power mismatch equation, or -1 where it has none. Where that bus’s active injection gradient is read out of solve_JT’s result.

get_physical_violations(*args, **kwargs)

Overloaded function.

  1. get_physical_violations(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) -> list[list[lightsim2grid.lightsim2grid_cpp.LimitViolation]]

Per row: the list of LimitViolation of the physical limits that row’s solution leaves. Every entry has category ViolationCategory.PHYSICAL and one of these shapes: element_type BUS with violation_type LOW_Q / HIGH_Q (element_id the grid bus id, value the reactive power the machines holding it had to produce in MVAr, limit their summed capability), element_type GENERATOR with violation_type LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q (element_id the generator id, value the voltage of the bus that flagged PQ machine would regulate and limit its target, in kV), element_type SVC with violation_type LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY (element_id the svc id, value the voltage of the bus that flagged idle standby SVC regulates and limit the automaton’s threshold, in kV), element_type GENERATOR with violation_type LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL (element_id the generator id, value the voltage of that remote controller’s own bus and limit the realistic bound, in kV), element_type HVDC with violation_type HIGH_P (element_id the hvdc line id, side the direction – 1 for 1 -> 2 – value the active power leaving that side in MW, limit that direction’s pmax), element_type HVDC with violation_type HVDC_AC_EMULATION_RELEASE (element_id the hvdc line id, side the direction it is frozen in, value the flow its droop asks for in that direction and limit the limit it is frozen at, in MW), or element_type GENERATOR with violation_type LOW_P / HIGH_P (element_id the generator id, value its converged active power in MW – its target plus its share of the distributed slack – limit its min_p_mw / max_p_mw). A row that did not converge has an EMPTY entry, not a sentinel – use converged_mask() to tell that from ‘converged, no violation’. Requires compute_physical_violations=True.

  1. get_physical_violations(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) -> list[list[lightsim2grid.lightsim2grid_cpp.LimitViolation]]

Per contingency: the list of LimitViolation of the physical limits that contingency’s solution leaves (category ViolationCategory.PHYSICAL) – element_type BUS with LOW_Q / HIGH_Q, element_type GENERATOR with LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q (value and limit in kV), element_type SVC with LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY (value and limit in kV), element_type GENERATOR with LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL (value and limit in kV), element_type HVDC with HIGH_P and side naming the direction, element_type HVDC with HVDC_AC_EMULATION_RELEASE and side the direction it is frozen in (value and limit in MW), or element_type GENERATOR with LOW_P / HIGH_P. A contingency that did not converge, or that was never simulated, has an EMPTY entry – use converged() to tell that from ‘converged, no violation’. Requires compute_physical_violations=True.

get_physical_violations_n(*args, **kwargs)

Overloaded function.

  1. get_physical_violations_n(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) -> list[lightsim2grid.lightsim2grid_cpp.LimitViolation]

The same, for the base (“n”) case every row is solved from (no injection change, no contingency). Empty if that solve did not converge. Requires compute_physical_violations=True.

  1. get_physical_violations_n(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) -> list[lightsim2grid.lightsim2grid_cpp.LimitViolation]

The same, for the pre-contingency (“n”) case. Requires compute_physical_violations=True.

get_power_flows(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]']

Get the active flows (in MW) at the origin side / high voltage side of each transformers / powerlines.

Each rows correspond to a contingency, each column to a powerline / transformer

Warning

This function must be called after lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute_power_flows() has been called. (compute_flows also requires that lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute() has been caleed)

Warning

The order in which the contingencies are computed is NOT (in this c++ class) the order in which you enter them. They are computed in the order given by lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.my_defaults(). For an easier, more “human readable” please use the lightsim2grid.contingencyAnalysis.ContingencyAnalysis.get_flows() method.

Returns:

As – The flows (in kA) at the origin side / high voltage side of each transformers / powerlines.

Return type:

numpy.ndarray (matrix)

get_q_row_of_bus(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']

Same as get_p_row_of_bus for the REACTIVE power mismatch equation.

get_row_nb_iter(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']

Per row of the last compute(): the number of iterations the algorithm reported (0 for a row never handed to the solver).

get_row_solve_times(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.float64], '[m, 1]']

Per row of the last compute(): the time the algorithm reported for its solve, in seconds. 0 for a row never handed to the solver (skipped before it, as an islanding contingency is, or never reached).

get_theta_col_of_bus(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']

For each grid bus (same numbering as the columns of get_voltages()), the column of the Jacobian holding its voltage-ANGLE unknown, or -1 where it has none (a reference slack, a bus outside the solver). Where the angle part of that bus’s cotangent goes in solve_JT’s input.

get_violations(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → list[list[lightsim2grid.lightsim2grid_cpp.LimitViolation]]

Per contingency (row order matches my_defaults()): list of LimitViolation. A non-converged contingency (converged() is False) has exactly one LimitViolation here, with element_type ViolationElementType.GRID and violation_type either LimitViolationType.NOT_SIMULATED (a pre-check skipped it, eg it splits the grid) or LimitViolationType.DIVERGENCE (the solver ran but did not converge).

get_violations_n(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → list[lightsim2grid.lightsim2grid_cpp.LimitViolation]

List of LimitViolation for the pre-contingency (‘n’) case.

get_vm_col_of_bus(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']

Same as get_theta_col_of_bus for the voltage-MAGNITUDE unknown: -1 at a bus whose magnitude is held fixed (a PV bus, unless a generator contingency may release it).

get_voltages(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.complex128], '[m, n]']

Get the complex voltage angles at each bus of the powergrid.

Each rows correspond to a contingency, each column to a bus.

Warning

This function must be called after lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute().

Warning

The order in which the contingencies are computed is NOT (in this c++ class) the order in which you enter them. They are computed in the order given by lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.my_defaults(). For an easier, more “human readable” please use the lightsim2grid.contingencyAnalysis.ContingencyAnalysis.get_flows() method.

Returns:

Vs – The complex voltage angles at each bus of the powergrid.

Return type:

numpy.ndarray (matrix)

property handle_disconnected_grid

Whether to simulate the contingencies that split the grid in multiple connected components. When False (default) such contingencies are skipped (their voltages are left at 0), reproducing the legacy behaviour. When True, the largest connected component is solved while the buses of the other component(s) are masked (their voltage is reported as 0). Supported by the Newton-Raphson family (AC) and the DC solver; a non Newton-Raphson AC algorithm is rejected.

property init_from_n_powerflow

false, meaning each simulation is initialized with the given input vector

Type:

Whether to initialize the complex voltages of each contingencies with the results of a n-powerflow (ie a powerflow without any line disconnection) or not. Default

invalidate_base_case(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → None

Drop the kept base case, so the next compute() builds a fresh one (see reuse_base_case). This object already does this itself whenever it changes anything the base case is made of, so there is normally no reason to call it.

is_grid_connected_after_contingency(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']

Low-level / internal primitive, not part of the stable public API: called directly by lightsim2grid’s own machinery (eg LightSimBackend, the grid loaders, or another part of the C++ core) rather than meant for everyday use.

Warning

Argument validation here is deliberately minimal or absent, and its exact behavior / signature may change between releases without notice. Prefer the higher-level methods documented elsewhere on this class (or grid2op’s own API) unless you specifically need this one and understand what it does internally.

property keep_jacobian

Whether each row’s converged Jacobian is kept during compute(), so that solve_JT() can run afterwards. Defaults to False; must be set BEFORE compute().

This is what makes a batch differentiable: with the Jacobians kept, one transposed solve per row turns a loss’s sensitivity to the voltages into its gradient with respect to every injection of every row (the adjoint method / the implicit function theorem).

It costs nb_rows * nnz(J) floats – see adjoint_memory_bytes(), and mind that on a large grid with many rows this is gigabytes – plus one extra Jacobian evaluation per row (the Newton-Raphson loop stops on the Jacobian of its previous iterate, which is not quite the one at the solution). Only the AC Newton-Raphson algorithms build a Jacobian at all; compute() raises with any other.

property keep_vinit_at_group_controlled_buses

Whether the buses a voltage-control group regulates (a bus regulated from elsewhere, by an SVC, or by several controllers at least one of which is remote) keep the magnitude of the starting voltage, in the ‘n’ solve and in every row, instead of being set to their set-point first.

Defaults to the value of the grid this object was built from (see LSGrid.set_keep_vinit_at_group_controlled_buses(), False unless set there). Changing it drops the kept base case.

modif_Ybus_time(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → float

Time spent to modify the Ybus matrix before simulating each contingency.

It is given in seconds (float).

my_defaults(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → list[list[int]]

Allows to inspect the contingency list that will be simulated.

Returns:

my_defaults_vect – The list (of list) of all the current contingencies. Its length corresponds to the number of contingencies simulated. For each contingency, it gives which powerline will be disconnected.

Return type:

list

nb_converged(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → int

Number of powerflows, among those nb_solved() attempted, that actually converged. Always <= nb_solved(): a row skipped outright (eg a non-invertible / islanding admittance matrix, on ContingencyAnalysis / ScenarioSweep) never reaches the solver at all, so it counts towards neither.

nb_solved(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → int

Total number of powerflows solved.

property nb_thread

Number of OS threads used to solve the contingencies (default 1). With nb_thread == 1 the behaviour is identical to the legacy sequential computation. With nb_thread > 1 the contingency list is split into contiguous ranges, each solved by its own thread (each with its own solver and admittance matrix copy), writing to disjoint rows of the result matrix. The results do not depend on the number of threads. Values < 1 are clamped to 1.

property physical_violation_tol_mva

a violation needs value > limit + tol (or value < limit - tol for LOW_Q and HVDC_AC_EMULATION_RELEASE). Defaults to 1e-4.

Type:

Absolute slack (MVA) on every comparison compute_physical_violations makes

property physical_violation_tol_vm_pu

The same, in pu, for the voltage comparisons of the PQ -> PV release check (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q), of the standby SVC check (LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY) and of the remote voltage control check (LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL). Defaults to 1e-4.

pick_reference_slack(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → int

Over the registered contingencies, return the slack bus (gridmodel id) stranded by the fewest of them — feed it to LSGrid.set_reference_slack_bus (before building this object: it works on a copy of the grid) so handle_disconnected_grid skips as few contingencies as possible. A suggestion only: it ignores a reference already forced on the grid, and does not change the reference the next compute() uses (a forced one, always kept, or else this same automatic choice).

preprocessing_time(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → float

Time spent in pre processing the data (this involves, the checking whether the grid would be still connex after the contingency for example)

It is given in seconds (float).

property redistribute_slack

without limits the converged state is unchanged. Same as LSGrid.consider_only_main_component(redistribute_slack=True), contingency by contingency.

Type:

Whether the active power a contingency loses – the elements of the island it cuts off, simulated with handle_disconnected_grid – is first shared on the remaining units of the distributed slack as OpenLoadFlow’s DistributedSlack outer loop does (proportionally to their weight, each one clamped to its [min_p, max_p], a clamped unit leaving the pool and the slack), the solve then only sharing what is left (the change in the losses) on the units that can still move. Off by default; compute() refuses it with a single-slack algorithm (NRSing_*, Gauss-Seidel). Needs LSGrid.set_gen_p_limits / set_storage_p_limits to clamp anything

remove_multiple_n1(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: collections.abc.Sequence[SupportsInt | SupportsIndex]) → int

Remove multiple “n-1” contingency from the contingency list to simulate. This can remove up to len(vect_n1) single contingencies from the contingency list.

Parameters:

vect_n1 (list (of int)) – The lines id you want to remove from contingency list (will remove multiple “n-1” single contingency)

Returns:

success – Whether or not the contingency has been properly removed

Return type:

bool

remove_n1(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: SupportsInt | SupportsIndex) → bool

Remove a single “n-1” contingency from the contingency list to simulate.

Parameters:

line_id (int) – The line id you would like to remove from contingency list (will remove a single “n-k” contingencies)

Returns:

success – Whether or not the contingency has been properly removed

Return type:

bool

remove_nk(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: collections.abc.Sequence[SupportsInt | SupportsIndex]) → bool

Remove a single “n-k” contingency from the contingency list to simulate. This removes at much one single contingency

Parameters:

vect_nk (list (of int)) – The lines id you want to remove from contingency list.

Returns:

nb_removed – The total number of contingencies removed from the contingency list

Return type:

int

reset(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → None

Clear the list of all contingencies. After a call to this method, you will need to re add some contingencies with

property reuse_base_case

True).

Before any row is solved, a batch has a base case to establish: read the grid (admittance matrix, bus labelling, pv/pq split), walk the graph to settle what each contingency strands, solve one ‘n’ powerflow, and analyze and factorize the Jacobian. None of that depends on the injections, so calling compute() again – with new injections, which is what a loop over scenarios does – repeats all of it for nothing. With this True it is done once and kept.

That covers the worker algorithms of a multi-threaded batch too: with nb_thread > 1 the rows are run by one algorithm per thread, and those are kept – and re-analyzed – on exactly the same terms as the member one.

It is dropped, and rebuilt on the next compute(), whenever something it is made of changes: clear(), change_algorithm(), algo_config, any contingency registration, handle_disconnected_grid, nb_thread, init_from_n_powerflow, keep_vinit_at_group_controlled_buses, or a different number of simulations. Every such modifier says so itself – internally each names one of three nested cache levels (the grid, this batch’s inputs, the results) and dropping one drops the levels below it. The grid cannot change underneath – this object holds its own copy of it, taken when it was built, and offers no way to modify it.

Set it to False to make every compute() rebuild everything, as it did before this existed: useful to tell a suspected caching problem from a real one.

Type:

Whether the base case is kept between two compute() calls on this object (default

set_algo_config(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, config: lightsim2grid.lightsim2grid_cpp.AlgoConfig) → None

See get_algo_config().

solve_JT(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, xbar: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous']) → Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]']

Solve the adjoint system J_i^T . lambda_i = xbar_i of every row, and return lambda. Requires keep_jacobian to have been True during compute().

xbar has shape (nb_rows, k * dim_J): one row per simulation, holding k cotangents of dim_J coefficients laid end to end. Differentiating a scalar loss uses k = 1; several directions are only needed for a full Jacobian, and cost one triangular solve each rather than one factorization each.

lambda is the gradient with respect to the per-unit bus injection: its real part sits at get_p_row_of_bus()[bus], its imaginary part at get_q_row_of_bus()[bus]. Rows that did not converge come back as zeros – see adjoint_row_ok().

solve_time(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → float

TODO

solver_time(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → float

Total time spent only in solving the powerflows (excluding pre processing the data, post processing them, initializing everything etc.)

It is given in seconds (float).

thread_algos_were_reused(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → bool

Whether the last compute() also kept the WORKER algorithms of the multi-threaded path, instead of building and analyzing one per thread. Always False for a single-threaded batch: it has no workers (the member algorithm runs the rows itself, and keeping that is base_case_was_reused).

thread_init_time(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → float

Time (in seconds, float) spent building the per thread solvers. It is 0. when nb_thread is 1, in which case the (already initialized) internal solver is reused.

total_time(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) → float

Total time spent in solving the powerflows, pre processing the data, post processing them, initializing everything etc.

It is given in seconds (float).

property violation_rel_tol

Relative tolerance (a float in [0., 1.[, default 1e-9) of every limit check performed when compute_limit_violations is True: a value is reported only when it is beyond its effective limit (see violation_threshold) by more than this fraction of that limit:

CURRENT        value > threshold * limit_a * (1 + violation_rel_tol)
LOW_VOLTAGE    v     < low_eff  * (1 - violation_rel_tol)
HIGH_VOLTAGE   v     > high_eff * (1 + violation_rel_tol)

It exists for the values that sit ON their limit by construction – typically a bus a generator regulates exactly at its vmax. A solve leaves such a value a few ulps on either side of the limit, and with a bare > the last bit of rounding decided whether it was reported: the one-contingency-at-a-time solve, the batch and gpusim2grid did not agree. 1e-9 is about 0.4 mV on a 400 kV bus, far below anything physically meaningful and far above rounding. 0. gives the bare strict comparisons.

The reported value and limit are unaffected. Like violation_threshold it only affects the next lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute(); changing it (in either direction) invalidates any already-computed results, the registered contingencies being kept.

property violation_threshold

Threshold (a float in ]0., 1.], default 1.0) applied to every limit check performed when compute_limit_violations is True. It is the fraction of the usable range that is still considered acceptable, so lowering it makes every check stricter (more violations are reported, never fewer).

Each of the three checks owns one interval, running from a “healthy” anchor to the limit that can be violated, and the threshold simply moves that limit towards its anchor – a linear interpolation, identical for all three:

effective_limit = threshold * limit + (1 - threshold) * anchor

check

anchor

limit

violates when

CURRENT

0

limit_a

value > threshold * limit_a

LOW_VOLTAGE

vn_kv

vmin_kv

v < threshold * vmin + (1 - threshold) * vn

HIGH_VOLTAGE

vn_kv

vmax_kv

v > threshold * vmax + (1 - threshold) * vn

A line’s usable range really is [0, limit_a], so its anchor is 0 and the rule reduces to scaling the limit. A voltage bound has no such natural “zero” end, so the bus NOMINAL voltage is used instead – it is what operating limits are conventionally expressed around (+/- x% of vn). Either way each acceptable interval keeps a width of exactly threshold times its original one.

The voltage anchor is the nominal voltage clamped into [vmin_kv, vmax_kv]. A band is not guaranteed to bracket it: real data does violate this (a 380 kV level declared with an operating range of [390, 450] kV is ordinary on the European 400 kV network), and an anchor outside the band would push a bound the wrong way. Clamping keeps each bound moving inwards only. Where the band does bracket the nominal voltage – the overwhelmingly common case – the clamp does nothing and the anchor is exactly vn_kv.

Anchoring both voltage checks on vn_kv – rather than on each other – is what keeps them independent: the LOW_VOLTAGE verdict is a function of vmin_kv, vn_kv and the threshold alone, so configuring vmax_kv (or leaving it at NaN) can never change it, and vice versa. It also means the two effective bounds each converge towards vn_kv from their own side and can never cross, so no bus is ever reported as both too low and too high, whatever the threshold.

A bus whose limits are inconsistent (vmin_kv > vmax_kv – the only way the two effective bounds can still end up crossed) raises a RuntimeError rather than being reported as an arbitrary one of the two violation types.

The reported value and limit of each violation are unaffected by the threshold: they remain the value actually reached and the limit exactly as configured. Only the test deciding whether to report at all is shifted.

The default 1.0 reproduces the previous, threshold-less behaviour. On top of the effective limit, a value must clear the relative margin violation_rel_tol (default 1e-9) to be reported, so a value on its limit up to rounding is not a violation.

Like nb_thread / handle_disconnected_grid, this is a plain runtime knob: it only affects the next lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute() . Lowering it invalidates any already-computed results (get_violations, get_violations_n, converged, converged_n), exactly as lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.clear_results_only() would – the registered contingencies are kept, so it is enough to call compute again. Raising it back up does not clear anything.

class lightsim2grid.contingencyAnalysis.ContingencyResult(element_ids: ~typing.List[int], element_names: ~typing.List[str], contingency_name: str | None, converged: bool, limit_violations: ~typing.List[~lightsim2grid.lightsim2grid_cpp.LimitViolation], physical_violations: ~typing.List[~lightsim2grid.lightsim2grid_cpp.LimitViolation] = <factory>)[source]

Limit violations for a single simulated contingency.

Note

If converged is False, limit_violations contains exactly one LimitViolation with element_type == ViolationElementType.GRID and violation_type either LimitViolationType.NOT_SIMULATED (a pre-check skipped this contingency, eg it splits the grid, without ever invoking the solver) or LimitViolationType.DIVERGENCE (the solver ran but did not converge).

Attributes:

contingency_name

user-supplied name, see add_single_contingency

element_ids

branch ids (lines then trafos) disconnected by this contingency

element_names

names (env.name_line) of the elements disconnected by this contingency

physical_violations

see PreContingencyResult.physical_violations

contingency_name: str | None

user-supplied name, see add_single_contingency

element_ids: List[int]

branch ids (lines then trafos) disconnected by this contingency

element_names: List[str]

names (env.name_line) of the elements disconnected by this contingency

physical_violations: List[LimitViolation]

see PreContingencyResult.physical_violations

class lightsim2grid.contingencyAnalysis.LimitViolation

A single limit violation, as detected by ContingencyAnalysisCPP.

See also

get_violations() / get_violations_n(), which return a list of these per contingency.

Attributes:

category

a limit the grid may leave (OPERATIONAL), one it cannot (PHYSICAL), or the solver's own verdict (SOLVER).

element_id

Which element this violation is about -- the grid-model bus id for BUS; the local (0-based, own-type) line / transformer id for LINE / TRAFO; unused (-1) for GRID.

element_type

The kind of element this violation is about, as a ViolationElementType.

limit

The limit that was violated (for LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q the generator's target voltage, for LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY the standby automaton's threshold, both in kV of the regulated bus; for LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL the realistic bound, in kV of the controller's own bus; for HVDC_AC_EMULATION_RELEASE the limit the line is frozen at, in MW); unused (NaN) for NOT_SIMULATED / DIVERGENCE.

name

The violating element's name -- for LINE / TRAFO, as set by lightsim2grid.network.LSGrid.set_line_names() / lightsim2grid.network.LSGrid.set_trafo_names(); for BUS, the name of the substation the violating bus belongs to (see lightsim2grid.network.LSGrid.set_substation_names() -- there is no per-bus name in LSGrid, only per-substation ones).

side

1 or 2 for a LINE / TRAFO violation (which side's current limit was violated) and for an HVDC one (the direction: 1 for a flow from side 1 to side 2); unused (0) for BUS / GRID.

value

The value actually reached (the voltage magnitude or the current, matching violation_type; for LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q the voltage, in kV, of the bus the pinned generator would regulate; for LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY the voltage, in kV, of the bus the standby SVC regulates; for LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL the voltage, in kV, of the remote controller's own bus; for HVDC_AC_EMULATION_RELEASE the flow, in MW, the frozen line's droop asks for in the direction side names); unused (NaN) for NOT_SIMULATED / DIVERGENCE.

violation_type

The kind of limit that was violated, as a LimitViolationType.

property category

a limit the grid may leave (OPERATIONAL), one it cannot (PHYSICAL), or the solver’s own verdict (SOLVER). Derived from violation_type.

Type:

The ViolationCategory of this violation, ie what kind of statement it is

property element_id

Which element this violation is about – the grid-model bus id for BUS; the local (0-based, own-type) line / transformer id for LINE / TRAFO; unused (-1) for GRID.

property element_type

The kind of element this violation is about, as a ViolationElementType.

property limit

The limit that was violated (for LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q the generator’s target voltage, for LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY the standby automaton’s threshold, both in kV of the regulated bus; for LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL the realistic bound, in kV of the controller’s own bus; for HVDC_AC_EMULATION_RELEASE the limit the line is frozen at, in MW); unused (NaN) for NOT_SIMULATED / DIVERGENCE.

property name

The violating element’s name – for LINE / TRAFO, as set by lightsim2grid.network.LSGrid.set_line_names() / lightsim2grid.network.LSGrid.set_trafo_names(); for BUS, the name of the substation the violating bus belongs to (see lightsim2grid.network.LSGrid.set_substation_names() – there is no per-bus name in LSGrid, only per-substation ones). Empty string if names were never set on the grid for the relevant kind, or for GRID.

property side

1 or 2 for a LINE / TRAFO violation (which side’s current limit was violated) and for an HVDC one (the direction: 1 for a flow from side 1 to side 2); unused (0) for BUS / GRID.

property value

The value actually reached (the voltage magnitude or the current, matching violation_type; for LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q the voltage, in kV, of the bus the pinned generator would regulate; for LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY the voltage, in kV, of the bus the standby SVC regulates; for LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL the voltage, in kV, of the remote controller’s own bus; for HVDC_AC_EMULATION_RELEASE the flow, in MW, the frozen line’s droop asks for in the direction side names); unused (NaN) for NOT_SIMULATED / DIVERGENCE.

property violation_type

The kind of limit that was violated, as a LimitViolationType.

class lightsim2grid.contingencyAnalysis.LimitViolationType

The kind of limit that was violated: LOW_VOLTAGE / HIGH_VOLTAGE (a bus voltage magnitude limit) or CURRENT (a line / transformer thermal limit) for an ordinary, element-level violation; NOT_SIMULATED or DIVERGENCE for a contingency-level one (see ViolationElementType’s GRID); LOW_Q / HIGH_Q, LOW_P / HIGH_P and LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q, LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY, LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL and HVDC_AC_EMULATION_RELEASE for the physical checks of compute_physical_violations (see each value’s own documentation):

  • NOT_SIMULATED: a pre-check (eg graph connectivity) skipped this contingency – the solver was never invoked for it.

  • DIVERGENCE: the solver was invoked for this contingency but did not converge.

Members:

LOW_VOLTAGE

HIGH_VOLTAGE

CURRENT

NOT_SIMULATED : A pre-check (graph connectivity) skipped this contingency: the solver was never invoked (element_type is ViolationElementType.GRID).

LOW_Q : The reactive power the machines holding a BUS’ voltage had to produce went BELOW the sum of what they can absorb (element_type is ViolationElementType.BUS). Category PHYSICAL: a machine cannot absorb reactive power it does not have, so the converged solution is not a state the grid can reach. Reported by compute_physical_violations, never enforced.

HIGH_Q : The reactive power the machines holding a BUS’ voltage had to produce went ABOVE the sum of what they can produce (element_type is ViolationElementType.BUS). Category PHYSICAL, see LOW_Q. Reported by compute_physical_violations, never enforced.

HIGH_P : The active power of an angle-droop (“AC emulation”) hvdc line left what its converters can transmit (element_type is ViolationElementType.HVDC). side says which direction and therefore which limit: 1 for 1 -> 2 against pmax_1to2_mw, 2 for the other way against pmax_2to1_mw. Category PHYSICAL: an hvdc converter does not transmit more than it can, its own control saturates first (which is what status_droop models). Reported by compute_physical_violations, never enforced.

Also reported on a GENERATOR or a STORAGE unit (element_type ViolationElementType.GENERATOR / STORAGE, side unused): the distributed slack – solved inside the Jacobian, by participation factors that know nothing about limits – asked a machine for more than its max_p_mw.

LOW_P : The distributed slack pushed a generator’s or a storage unit’s active power BELOW its min_p_mw (element_type is ViolationElementType.GENERATOR / STORAGE). Category PHYSICAL, see HIGH_P: a machine does not deliver power it does not have, so the converged solution assumes a distribution that cannot happen – which is exactly what OpenLoadFlow’s DistributedSlack outer loop re-shares. Reported by compute_physical_violations, never enforced.

DIVERGENCE : The solver was invoked for this contingency but did not converge (element_type is ViolationElementType.GRID).

LOW_VOLTAGE_AT_MIN_Q : A PQ generator flagged as pinned at its MINIMUM reactive power (see LSGrid.set_gen_can_be_pv) whose regulated bus sits BELOW the target it would hold (element_type is ViolationElementType.GENERATOR): it absorbs too much for that target, and OpenLoadFlow’s ReactiveLimits loop would switch it back to PV – the PQ -> PV direction, the mirror of LOW_Q / HIGH_Q. Category PHYSICAL: the converged solution assumes a control the loop would not leave in place. value the regulated voltage and limit the target, both in kV; the ‘LOW’ in the name says the value is below the limit. Reported by compute_physical_violations, never enforced. Also reported on an SVC (element_type ViolationElementType.SVC) flagged with LSGrid.set_svc_can_be_pv, frozen at the absorbing end of its susceptance range.

HIGH_VOLTAGE_AT_MAX_Q : The mirror of LOW_VOLTAGE_AT_MIN_Q: a PQ generator flagged as pinned at its MAXIMUM reactive power whose regulated bus sits ABOVE the target it would hold (element_type is ViolationElementType.GENERATOR, value above limit, both in kV). Category PHYSICAL. Reported by compute_physical_violations, never enforced.

LOW_VOLTAGE_SVC_STANDBY : A non-regulating SVC flagged as left idle under its standby automaton (see LSGrid.set_svc_standby) whose regulated bus sits BELOW the automaton’s low voltage threshold (element_type is ViolationElementType.SVC): OpenLoadFlow’s MonitoringVoltageOuterLoop would switch it to voltage control. Category PHYSICAL, like LOW_VOLTAGE_AT_MIN_Q: the converged solution assumes a control the loop would not leave in place. value the regulated voltage and limit the threshold, both in kV (value below limit). Reported by compute_physical_violations, never enforced.

HIGH_VOLTAGE_SVC_STANDBY : The mirror of LOW_VOLTAGE_SVC_STANDBY: the regulated bus sits ABOVE the automaton’s high voltage threshold (element_type is ViolationElementType.SVC, value above limit, both in kV). Category PHYSICAL. Reported by compute_physical_violations, never enforced.

LOW_VOLTAGE_REMOTE_CONTROL : A generator regulating a REMOTE bus whose own bus sits BELOW the realistic range set with LSGrid.set_remote_voltage_control_vm_range (element_type is ViolationElementType.GENERATOR): OpenLoadFlow’s ReactiveLimits loop, in its robust remote voltage control mode, would switch it to PQ at its target reactive power. Category PHYSICAL, like LOW_VOLTAGE_AT_MIN_Q. value the generator’s own bus voltage and limit the bound, both in kV (value below limit). Reported by compute_physical_violations, never enforced.

HIGH_VOLTAGE_REMOTE_CONTROL : The mirror of LOW_VOLTAGE_REMOTE_CONTROL: the generator’s own bus sits ABOVE the realistic range (element_type is ViolationElementType.GENERATOR, value above limit, both in kV). Category PHYSICAL. Reported by compute_physical_violations, never enforced.

HVDC_AC_EMULATION_RELEASE : An angle-droop (AC emulation) hvdc line an outer loop froze at its active power limit (LSGrid.set_hvdc_ac_emulation_frozen) whose droop would now ask for LESS than that limit (element_type is ViolationElementType.HVDC, side the direction it is frozen in): OpenLoadFlow’s AcHvdcAcEmulationLimits loop would leave it in AC emulation. Category PHYSICAL. value the flow its droop asks for in that direction and limit the limit, both in MW (value below limit). Reported by compute_physical_violations, never enforced.

Attributes:

name

property name
class lightsim2grid.contingencyAnalysis.PreContingencyResult(converged: bool, limit_violations: ~typing.List[~lightsim2grid.lightsim2grid_cpp.LimitViolation], physical_violations: ~typing.List[~lightsim2grid.lightsim2grid_cpp.LimitViolation] = <factory>)[source]

Limit violations for the pre-contingency (“n”, no disconnection) case.

Note

converged is always True here: ContingencyAnalysisCPP.compute raises a RuntimeError if the pre-contingency powerflow itself does not converge (every contingency is solved relative to this base case, so a diverging base case makes the whole analysis meaningless).

Attributes:

physical_violations

the PHYSICAL limits this case's solution leaves, when ContingencyAnalysis.compute_physical_violations is on (an empty list otherwise): a bus needing reactive power its machines do not have (LOW_Q / HIGH_Q), a PQ generator flagged as pinned at a reactive limit that would regulate again (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q), an idle SVC flagged as carrying a standby automaton that would switch it on (LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY), a generator holding a remote bus from an unrealistic voltage on its own bus (LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL), an angle-droop hvdc line beyond what its converters can transmit (HIGH_P), one flagged as frozen at its limit whose droop would ask for less (HVDC_AC_EMULATION_RELEASE), or a generator the distributed slack pushed outside its active power limits (LOW_P / HIGH_P).

physical_violations: List[LimitViolation]

the PHYSICAL limits this case’s solution leaves, when ContingencyAnalysis.compute_physical_violations is on (an empty list otherwise): a bus needing reactive power its machines do not have (LOW_Q / HIGH_Q), a PQ generator flagged as pinned at a reactive limit that would regulate again (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q), an idle SVC flagged as carrying a standby automaton that would switch it on (LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY), a generator holding a remote bus from an unrealistic voltage on its own bus (LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL), an angle-droop hvdc line beyond what its converters can transmit (HIGH_P), one flagged as frozen at its limit whose droop would ask for less (HVDC_AC_EMULATION_RELEASE), or a generator the distributed slack pushed outside its active power limits (LOW_P / HIGH_P). Kept apart from limit_violations because it is a different KIND of statement: every entry here has category == ViolationCategory.PHYSICAL – a state the grid cannot reach – where limit_violations carries OPERATIONAL limits it can leave (and the SOLVER sentinel of a non-converged case).

class lightsim2grid.contingencyAnalysis.SecurityAnalysisResult(pre_contingency_result: PreContingencyResult, post_contingency_results: List[ContingencyResult])[source]

Result of ContingencyAnalysis.run / run_ac / run_dc, modeled after pypowsybl’s security analysis result.

class lightsim2grid.contingencyAnalysis.ViolationCategory

What KIND of statement a LimitViolation is – a property of its violation_type, and the first thing to read: the three kinds do not mean the same thing and must not be acted on the same way.

Members:

OPERATIONAL : A limit chosen by an operator, which the grid CAN leave: a bus outside its voltage range, a branch above its current rating. The solution is a state the grid can reach – it is just a state nobody wants to sit in, and things eventually break. LOW_VOLTAGE, HIGH_VOLTAGE, CURRENT.

PHYSICAL : A limit of the equipment itself, which nothing can leave. A violation here says the converged solution is NOT physically realizable, whatever anyone decides: the control it assumes (a voltage set-point held by machines that would have to produce reactive power they do not have, an hvdc converter transmitting more than it can, a distributed slack asking a machine for power it does not have) cannot happen. A statement about the model’s assumptions, not about how the grid is operated. LOW_Q, HIGH_Q, LOW_P, HIGH_P, LOW_VOLTAGE_AT_MIN_Q, HIGH_VOLTAGE_AT_MAX_Q, LOW_VOLTAGE_SVC_STANDBY, HIGH_VOLTAGE_SVC_STANDBY, LOW_VOLTAGE_REMOTE_CONTROL, HIGH_VOLTAGE_REMOTE_CONTROL, HVDC_AC_EMULATION_RELEASE.

SOLVER : Not a limit at all: what the solver did. A divergence in particular says nothing about the grid – the state may be perfectly feasible and the algorithm simply failed to find it, or there may be no solution; this does not distinguish the two. NOT_SIMULATED, DIVERGENCE.

Attributes:

name

property name
class lightsim2grid.contingencyAnalysis.ViolationElementType

The kind of element on which a limit was violated: BUS (a voltage limit), LINE / TRAFO (a current limit), or GRID – the whole grid / contingency rather than a specific element, used when the contingency itself could not be simulated at all (see LimitViolationType’s NOT_SIMULATED / DIVERGENCE).

Members:

BUS

LINE

TRAFO

GRID : The whole grid / contingency, not a specific element (see LimitViolationType.NOT_SIMULATED / LimitViolationType.DIVERGENCE).

HVDC : An hvdc line, by its own id (see LimitViolationType.HIGH_P) – also the release of one of its VSC stations frozen at a reactive limit (LimitViolationType.LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q, side the station’s end, see LSGrid.set_hvdc_can_be_pv), and the release of a line frozen at its active power limit (LimitViolationType.HVDC_AC_EMULATION_RELEASE, see LSGrid.set_hvdc_ac_emulation_frozen).

GENERATOR : A generator, by its own id – its ACTIVE power (LimitViolationType.LOW_P / HIGH_P) and, for a PQ machine flagged as pinned at a reactive limit, the voltage of the bus it would regulate (LimitViolationType.LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q) and, for a machine regulating a remote bus, the voltage of its own bus (LOW_VOLTAGE_REMOTE_CONTROL / HIGH_VOLTAGE_REMOTE_CONTROL). A REGULATING machine’s reactive violation is reported on the BUS instead, because how a bus’ reactive power is divided between its machines is a modelling convention, while the active one is divided by the participation factors the caller chose.

STORAGE : A storage unit, by its own id – the same statement as GENERATOR, since a storage unit takes a share of the distributed slack under the same rule.

/!value and limit are in the GENERATOR convention (positive = injected into the grid), like the unit’s min_q_mvar / max_q_mvar and unlike its target_p_mw / res_p_mw, which lightsim2grid stores in the load convention.

SVC : A static var compensator, by its own id: an idle SVC flagged as carrying a standby automaton, which the voltage of the bus it regulates would switch on (LimitViolationType.LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY), or a fixed-Q SVC flagged as frozen at a reactive limit (LSGrid.set_svc_can_be_pv) that would regulate again (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q).

Attributes:

name

property name