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-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, 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 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 |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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
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:
|
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. |
|
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. |
|
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.
Per contingency, in the C++-side order (my_defaults()): the list of
LimitViolationof the physical limits that contingency's solution leaves.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:
Whether limit violations are computed inline, per contingency, during run / run_ac / run_dc (see also get_violations on the underlying computer).
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).
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.
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.
1).
Absolute slack on every comparison
compute_physical_violationsmakes, so that an element resting exactly on its limit is not reported over solver noise: a violation needsvalue > limit + tol(orvalue < limit - tolforLOW_QandHVDC_AC_EMULATION_RELEASE).The same as
physical_violation_tol_mva, in pu, for the comparisonscompute_physical_violationsmakes on a voltage: the PQ -> PV release check reports a flagged PQ generator (LSGrid.set_gen_can_be_pv) whose regulated bus is below (atmin_q) or above (atmax_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).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'sDistributedSlackouter 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.Relative tolerance (a
floatin[0., 1.[, default1e-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.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.
- 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. Seeget_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_Qon theBUS): 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’sReactiveLimits.the release of every PQ generator flagged as pinned at a reactive limit (
LOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Qon theGENERATOR, seelightsim2grid.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 sameReactiveLimitsloop.valueandlimitin kV, compared withphysical_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_STANDBYon theSVC, seelightsim2grid.network.LSGrid.set_svc_standby()): does the bus it regulates sit outside the automaton’s voltage thresholds? OpenLoadFlow’sMonitoringVoltageOuterLoop.valueandlimitin kV, compared withphysical_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_CONTROLon theGENERATOR, seelightsim2grid.network.LSGrid.set_remote_voltage_control_vm_range()): does holding the remote target take it outside that range? OpenLoadFlow’sReactiveLimits, in its robust remote voltage control mode.valueandlimitin kV, compared withphysical_violation_tol_vm_pu.the active power of every angle-droop (“AC emulation”) hvdc line still in the linear regime (
HIGH_Pon theHVDC): didp0 + k.(theta1 - theta2)leavepmax_1to2_mw/pmax_2to1_mw? OpenLoadFlow’sHvdcAcEmulationLimits.the release of every angle-droop hvdc line flagged as frozen at its active power limit (
HVDC_AC_EMULATION_RELEASEon theHVDC, seelightsim2grid.network.LSGrid.set_hvdc_ac_emulation_frozen()): would its droop ask for less than that limit? The other half of the sameHvdcAcEmulationLimitsloop.sidethe direction it is frozen in,valueandlimitin MW.the active power of every generator and every storage unit carrying the distributed slack (
LOW_P/HIGH_Pon theGENERATOR/STORAGE): the slack is solved inside the Jacobian by fixed participation factors that know nothing about limits, sotarget_p + its share of the imbalancecan land beyondmin_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 itstarget_p_mw. OpenLoadFlow’sDistributedSlack.
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
LimitViolationof the physical limits that contingency’s solution leaves. Preferrun(), which returns them per contingency in the caller’s own order and alongside the operational ones. Requirescompute_physical_violationsto beTrue(raises otherwise).
- get_physical_violations_n()[source]
Same as
get_physical_violations(), for the pre-contingency (“n”) case. Requirescompute_physical_violationsto beTrue(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). 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 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
MaxVoltageChangedamping it can be reached in far fewer iterations. Defaults to the grid’s own setting (seelightsim2grid.lightsim2grid_cpp.LSGrid.set_keep_vinit_at_group_controlled_buses()),Falseunless set there.
- 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
- property physical_violation_tol_mva
Absolute slack on every comparison
compute_physical_violationsmakes, so that an element resting exactly on its limit is not reported over solver noise: a violation needsvalue > limit + tol(orvalue < limit - tolforLOW_QandHVDC_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 comparisonscompute_physical_violationsmakes on a voltage: the PQ -> PV release check reports a flagged PQ generator (LSGrid.set_gen_can_be_pv) whose regulated bus is below (atmin_q) or above (atmax_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’sDistributedSlackouter 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. NeedsLSGrid.set_gen_p_limits/set_storage_p_limitsto clamp anything. Same asLSGrid.consider_only_main_component(redistribute_slack=True), contingency by contingency. Needs an algorithm with a distributed slack:computerefuses 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
floatin[0., 1.[, default1e-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-9is about 0.4 mV on a 400 kV bus.0.gives the bare strict comparisons. The reportedvalue/limitare 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
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.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.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)
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
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.
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_vgradient,(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_vcarries: 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_vset-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 = 0of the voltage-control group whose set-point itsgen_vis (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.
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).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:
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.
Whether limit violations are computed inline, per contingency, during compute() (see converged / get_violations / converged_n / get_violations_n).
no bus is switched PV -> PQ or back, no droop is clamped, no machine leaves the slack distribution, no contingency is re-solved.
Whether to simulate the contingencies that split the grid in multiple connected components.
false, meaning each simulation is initialized with the given input vector
Whether each row's converged Jacobian is kept during compute(), so that solve_JT() can run afterwards.
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.
Number of OS threads used to solve the contingencies (default
1).a violation needs
value > limit + tol(orvalue < limit - tolfor LOW_Q and HVDC_AC_EMULATION_RELEASE).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).
without limits the converged state is unchanged.
True).Relative tolerance (a
floatin[0., 1.[, default1e-9) of every limit check performed when compute_limit_violations isTrue: a value is reported only when it is beyond its effective limit (see violation_threshold) by more than this fraction of that limit.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.
- 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.
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_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
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_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,HvdcAcEmulationLimitsandDistributedSlack. 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
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.
- 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_vgradient,(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 toxbar: 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 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_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().
How much of its bus’ derivative each generator’s
gen_vcarries: 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_vset-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_vis 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 = 0of the voltage-control group whose set-point itsgen_vis (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.
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.
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.
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.
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 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_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 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
- 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(),Falseunless 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(orvalue < limit - tolfor 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(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
- 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
Trueit is done once and kept.That covers the worker algorithms of a multi-threaded batch too: with
nb_thread > 1the 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
Falseto 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. 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_rel_tol
Relative tolerance (a
floatin[0., 1.[, default1e-9) of every limit check performed when compute_limit_violations isTrue: 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-9is 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
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. On top of the effective limit, a value must clear the relative margin violation_rel_tol (default1e-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 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: ~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
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
see PreContingencyResult.physical_violations
- 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:
a limit the grid may leave (OPERATIONAL), one it cannot (PHYSICAL), or the solver's own verdict (SOLVER).
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 (for
LOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Qthe generator's target voltage, forLOW_VOLTAGE_SVC_STANDBY/HIGH_VOLTAGE_SVC_STANDBYthe standby automaton's threshold, both in kV of the regulated bus; forLOW_VOLTAGE_REMOTE_CONTROL/HIGH_VOLTAGE_REMOTE_CONTROLthe realistic bound, in kV of the controller's own bus; forHVDC_AC_EMULATION_RELEASEthe limit the line is frozen at, in MW); 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) and for anHVDCone (the direction:1for a flow from side 1 to side 2); unused (0) forBUS/GRID.The value actually reached (the voltage magnitude or the current, matching
violation_type; forLOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Qthe voltage, in kV, of the bus the pinned generator would regulate; forLOW_VOLTAGE_SVC_STANDBY/HIGH_VOLTAGE_SVC_STANDBYthe voltage, in kV, of the bus the standby SVC regulates; forLOW_VOLTAGE_REMOTE_CONTROL/HIGH_VOLTAGE_REMOTE_CONTROLthe voltage, in kV, of the remote controller's own bus; forHVDC_AC_EMULATION_RELEASEthe flow, in MW, the frozen line's droop asks for in the directionsidenames); unused (NaN) forNOT_SIMULATED/DIVERGENCE.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 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 (for
LOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Qthe generator’s target voltage, forLOW_VOLTAGE_SVC_STANDBY/HIGH_VOLTAGE_SVC_STANDBYthe standby automaton’s threshold, both in kV of the regulated bus; forLOW_VOLTAGE_REMOTE_CONTROL/HIGH_VOLTAGE_REMOTE_CONTROLthe realistic bound, in kV of the controller’s own bus; forHVDC_AC_EMULATION_RELEASEthe limit the line is frozen at, in MW); 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) and for anHVDCone (the direction:1for a flow from side 1 to side 2); unused (0) forBUS/GRID.
- property value
The value actually reached (the voltage magnitude or the current, matching
violation_type; forLOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Qthe voltage, in kV, of the bus the pinned generator would regulate; forLOW_VOLTAGE_SVC_STANDBY/HIGH_VOLTAGE_SVC_STANDBYthe voltage, in kV, of the bus the standby SVC regulates; forLOW_VOLTAGE_REMOTE_CONTROL/HIGH_VOLTAGE_REMOTE_CONTROLthe voltage, in kV, of the remote controller’s own bus; forHVDC_AC_EMULATION_RELEASEthe flow, in MW, the frozen line’s droop asks for in the directionsidenames); 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);LOW_Q/HIGH_Q,LOW_P/HIGH_PandLOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Q,LOW_VOLTAGE_SVC_STANDBY/HIGH_VOLTAGE_SVC_STANDBY,LOW_VOLTAGE_REMOTE_CONTROL/HIGH_VOLTAGE_REMOTE_CONTROLandHVDC_AC_EMULATION_RELEASEfor the physical checks ofcompute_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:
- 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
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).Attributes:
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:
- 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), 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).
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:
- property name