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_threadchanges 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 roughly1e-13, far below the powerflow tolerance);it works for the AC (Newton-Raphson), DC and
handle_disconnected_gridmodes 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-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
nb_thread |
nb solved |
time (ms) |
pf / s |
speed-up vs 1 thread |
|---|---|---|---|---|
1 |
703 |
4144.41 |
170 |
1.00x |
2 |
703 |
2307.1 |
305 |
1.80x |
3 |
703 |
1759.85 |
399 |
2.35x |
4 |
703 |
1425.17 |
493 |
2.91x |
5 |
703 |
1235.88 |
569 |
3.35x |
6 |
703 |
1103.81 |
637 |
3.75x |
7 |
703 |
1183.16 |
594 |
3.50x |
8 |
703 |
1065.82 |
660 |
3.89x |
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, orGRIDfor a non-converged contingency, see below);element_id: the grid-model bus id (forBUS) or the local line / trafo id (forLINE/TRAFO); unused (-1) forGRID;side:1or2forLINE/TRAFO(unused,0, forBUS/GRID);violation_type:lightsim2grid.contingencyAnalysis.LimitViolationType(LOW_VOLTAGE,HIGH_VOLTAGE,CURRENT,NOT_SIMULATEDorDIVERGENCE, see below);value/limit: the value reached and the limit that was violated; unused (NaN) forNOT_SIMULATED/DIVERGENCE;name: forLINE/TRAFO, the element’s own name (seeLSGrid.set_line_names/set_trafo_names); forBUS, the name of the substation the violating bus belongs to (seeLSGrid.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 forGRID.
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 exactly at the configured limit. 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 |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
effective band (pu) |
band width |
|---|---|---|
1.0 |
|
|
0.95 |
|
|
0.9 |
|
|
0.5 |
|
|
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
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: 11.4ms (15514 pf / s), 0.06 ms / pf)
- time to compute the coefficients to simulate line disconnection: 0.35ms
- time to pre process Ybus: 0.32ms
- time to perform powerflows: 10.68ms (16567 pf / s, 0.06 ms / pf)
In addition, it took 0.43 ms to retrieve the current from the complex voltages (in total 14949.6 pf /s, 0.07 ms / pf)
Comparison with raw grid2op timings
It took grid2op (with lightsim2grid, using obs.simulate): 0.36s to perform the same computation
This is a 30.7 speed up from ContingencyAnalysis over raw grid2op (using obs.simulate and lightsim2grid)
It took grid2op (with pandapower, using obs.simulate): 11.12s to perform the same computation
This is a 939.3 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 939 times faster than raw grid2op (with obs.simulate and pandapower)
All results match !
Detailed usage
Classes:
|
This class allows to perform a "security analysis" from a given grid state. |
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. |
|
|
Limit violations for a single simulated contingency. |
A single limit violation, as detected by |
|
The kind of limit that was violated: |
|
|
Limit violations for the pre-contingency ("n", no disconnection) case. |
Result of ContingencyAnalysis.run / run_ac / run_dc, modeled after pypowsybl's security analysis result. |
|
The kind of element on which a limit was violated: |
- 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:
you create it from a grid2op environment (the grid topology will not be modified from this environment)
you add some contingencies to simulate
you start the simulation
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:
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
This function returns the current flows (in Amps, A) at the origin / high voltage side
This function returns the active power flows (in MW) at the origin / high voltage side
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.
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:
Whether limit violations are computed inline, per contingency, during run / run_ac / run_dc (see also get_violations on the underlying computer).
Whether a contingency that splits the grid into several connected components is simulated on its largest component instead of being skipped.
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.
1).
Threshold (a
floatin]0., 1.], default1.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.)
- 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.
- 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]`
- 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). WhenTrue, 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 beforeget_flows/compute_V/runare called); it has no effect on a contingency that has already been solved.
- property nb_thread
1).
With
nb_thread == 1the behaviour is identical to the legacy sequential computation. Withnb_thread > 1the 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
- 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
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_threshold
Threshold (a
floatin]0., 1.], default1.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_aLOW_VOLTAGE
vn_kv
vmin_kv
v <= threshold * vmin + (1 - threshold) * vnHIGH_VOLTAGE
vn_kv
vmax_kv
v >= threshold * vmax + (1 - threshold) * vnA line’s usable range really is
[0, limit_a], so its anchor is0and 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 exactlythresholdtimes its original one.Anchoring both voltage checks on
vn_kv– rather than on each other – keeps them independent: theLOW_VOLTAGEverdict depends onvmin_kv,vn_kvand the threshold alone, so settingvmax_kv(or leaving it atNaN) can never change it, and vice versa. The two effective bounds also converge towardsvn_kvfrom 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 aRuntimeErrorrather than being reported as an arbitrary one of the two types. The reportedvalue/limitare never rescaled by the threshold; only the test deciding whether to report is shifted.The default
1.0reproduces 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.ContingencyAnalysisclass.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:
Modify the list of contingencies to simulate, with the functions:
lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.add_n1()lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.add_all_n1()lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.add_nk()lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.add_multiple_n1()lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.clear()lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.remove_n1()lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.remove_nk()lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.remove_multiple_n1()
2) Then you can start the computation of the security analysis with
lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute()then optionallylightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute_flows().3) And finally inspect the results with
lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.get_flows()andlightsim2grid.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)
amps_computation_time(self)Time spent in computing the flows (in amps) after the voltages have been computed at each nodes
Returns the names of all registered algorithms, including any loaded plugins, as a list of string.
Return the list of the names of the algorithm available on the current lightsim2grid installation.
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_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.
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_flows(self)Get the flows (in kA) at the origin side / high voltage side of each transformers / powerlines.
get_power_flows(self)Get the active flows (in MW) at the origin side / high voltage side of each transformers / powerlines.
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_voltages(self)Get the complex voltage angles at each bus of the powergrid.
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 ac_pf 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_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_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:
Whether limit violations are computed inline, per contingency, during compute() (see converged / get_violations / converged_n / get_violations_n).
Whether to simulate the contingencies that split the grid in multiple connected components.
false, meaning each simulation is initialized with the given input vector
Number of OS threads used to solve the contingencies (default
1).Threshold (a
floatin]0., 1.], default1.0) applied to every limit check performed when compute_limit_violations isTrue.- 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_n1()to add only a single lineSee 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(ofint)) – 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 timeSee 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(ofint)) – The lines id you want to add in the single contingency added.
- 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.
- change_algorithm(*args, **kwargs)
Overloaded function.
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.AlgorithmTypefor 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)
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 bylightsim2grid.algorithm.AlgorithmTypeinstead.
- change_solver(*args, **kwargs)
Overloaded function.
change_solver(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, arg0: lightsim2grid.lightsim2grid_cpp.AlgorithmType) -> None
DEPRECATED: use ‘change_algorithm’ instead
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
This function must be called before
lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.compute_flows()andlightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.get_flows()orlightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.get_voltages().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_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
This function must be called before
lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.get_flows()Note
During this computation, the GIL is released, allowing easier parrallel computation
- 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.
- 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
This function must be called before
lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP.get_flows()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.
- 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_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 thatlightsim2grid.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 thelightsim2grid.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_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 thatlightsim2grid.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 thelightsim2grid.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_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_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 thelightsim2grid.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
- 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.
- 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.
- 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 ac_pf so handle_disconnected_grid skips as few contingencies as possible.
- 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).
- 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(ofint)) – 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(ofint)) – 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
- set_algo_config(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP, config: lightsim2grid.lightsim2grid_cpp.AlgoConfig) None
See get_algo_config().
- 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_init_time(self: lightsim2grid.lightsim2grid_cpp.ContingencyAnalysisCPP) float
Time (in seconds,
float) spent building the per thread solvers. It is 0. whennb_threadis 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_threshold
Threshold (a
floatin]0., 1.], default1.0) applied to every limit check performed when compute_limit_violations isTrue. 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_aLOW_VOLTAGE
vn_kv
vmin_kv
v <= threshold * vmin + (1 - threshold) * vnHIGH_VOLTAGE
vn_kv
vmax_kv
v >= threshold * vmax + (1 - threshold) * vnA line’s usable range really is
[0, limit_a], so its anchor is0and 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 exactlythresholdtimes 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 exactlyvn_kv.Anchoring both voltage checks on
vn_kv– rather than on each other – is what keeps them independent: theLOW_VOLTAGEverdict is a function ofvmin_kv,vn_kvand the threshold alone, so configuringvmax_kv(or leaving it atNaN) can never change it, and vice versa. It also means the two effective bounds each converge towardsvn_kvfrom 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 aRuntimeErrorrather 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.0reproduces the previous, threshold-less behaviour exactly (modulo the strict>/<comparisons becoming>=/<=, which only differ when a value lands exactly on its limit – negligible in floating point).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 aslightsim2grid.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: List[int], element_names: List[str], contingency_name: str | None, converged: bool, limit_violations: List[LimitViolation])[source]
Limit violations for a single simulated contingency.
Note
If
convergedis False,limit_violationscontains exactly oneLimitViolationwithelement_type == ViolationElementType.GRIDandviolation_typeeitherLimitViolationType.NOT_SIMULATED(a pre-check skipped this contingency, eg it splits the grid, without ever invoking the solver) orLimitViolationType.DIVERGENCE(the solver ran but did not converge).Attributes:
user-supplied name, see add_single_contingency
branch ids (lines then trafos) disconnected by this contingency
names (env.name_line) of the elements disconnected by this contingency
- 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:
Which element this violation is about -- the grid-model bus id for
BUS; the local (0-based, own-type) line / transformer id forLINE/TRAFO; unused (-1) forGRID.The kind of element this violation is about, as a
ViolationElementType.The limit that was violated; unused (
NaN) forNOT_SIMULATED/DIVERGENCE.The violating element's name -- for
LINE/TRAFO, as set bylightsim2grid.network.LSGrid.set_line_names()/lightsim2grid.network.LSGrid.set_trafo_names(); forBUS, the name of the substation the violating bus belongs to (seelightsim2grid.network.LSGrid.set_substation_names()-- there is no per-bus name inLSGrid, only per-substation ones).1or2for aLINE/TRAFOviolation (which side's current limit was violated); unused (0) forBUS/GRID.The value actually reached (the voltage magnitude or the current, matching
violation_type); unused (NaN) forNOT_SIMULATED/DIVERGENCE.The kind of limit that was violated, as a
LimitViolationType.- 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 forLINE/TRAFO; unused (-1) forGRID.
- property element_type
The kind of element this violation is about, as a
ViolationElementType.
- property limit
The limit that was violated; unused (
NaN) forNOT_SIMULATED/DIVERGENCE.
- property name
The violating element’s name – for
LINE/TRAFO, as set bylightsim2grid.network.LSGrid.set_line_names()/lightsim2grid.network.LSGrid.set_trafo_names(); forBUS, the name of the substation the violating bus belongs to (seelightsim2grid.network.LSGrid.set_substation_names()– there is no per-bus name inLSGrid, only per-substation ones). Empty string if names were never set on the grid for the relevant kind, or forGRID.
- property side
1or2for aLINE/TRAFOviolation (which side’s current limit was violated); unused (0) forBUS/GRID.
- property value
The value actually reached (the voltage magnitude or the current, matching
violation_type); unused (NaN) forNOT_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) orCURRENT(a line / transformer thermal limit) for an ordinary, element-level violation;NOT_SIMULATEDorDIVERGENCEfor a contingency-level one (seeViolationElementType’sGRID):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).
DIVERGENCE : The solver was invoked for this contingency but did not converge (element_type is ViolationElementType.GRID).
Attributes:
- property name
- class lightsim2grid.contingencyAnalysis.PreContingencyResult(converged: bool, limit_violations: List[LimitViolation])[source]
Limit violations for the pre-contingency (“n”, no disconnection) case.
Note
convergedis always True here: ContingencyAnalysisCPP.compute raises aRuntimeErrorif 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).
- 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.ViolationElementType
The kind of element on which a limit was violated:
BUS(a voltage limit),LINE/TRAFO(a current limit), orGRID– the whole grid / contingency rather than a specific element, used when the contingency itself could not be simulated at all (seeLimitViolationType’sNOT_SIMULATED/DIVERGENCE).Members:
BUS
LINE
TRAFO
GRID : The whole grid / contingency, not a specific element (see LimitViolationType.NOT_SIMULATED / LimitViolationType.DIVERGENCE).
Attributes:
- property name