Scenario Sweep
Goal
This class varies both the injection (generators / static generators / loads) and a contingency (a line / transformer disconnection) per simulation, independently, row-aligned: row i of every injection input is solved together with row i of the contingency mask.
It is the 4th instantiation of the same underlying C++ template as
lightsim2grid.timeSerie.TimeSerie / lightsim2grid.injectionSweep.InjectionSweep
(which vary only the injection) and lightsim2grid.contingencyAnalysis.ContingencyAnalysis
(which varies only the topology, over one shared base injection) – see Time Series
and Contingency Analysis.
It can be used as:
import numpy as np
import grid2op
from lightsim2grid import ScenarioSweep
from lightsim2grid import LightSimBackend
env_name = ...
env = grid2op.make(env_name, backend=LightSimBackend())
obs = env.reset()
n_simul = 10
load_p = np.tile(obs.load_p, (n_simul, 1))
load_p *= 1 + 0.01 * np.arange(n_simul).reshape(-1, 1) # vary the load a bit per row
# lightsim2grid's own line / trafo counts -- NOT grid2op's flat `env.n_line`
# (grid2op merges lines and trafos into one "powerline" list; lightsim2grid keeps
# them as two separate containers, and the contingency masks below follow that split)
n_line = len(env.backend._grid.get_lines())
n_trafo = len(env.backend._grid.get_trafos())
line_mask = np.zeros((n_simul, n_line), dtype=bool)
line_mask[5:, 3] = True # disconnect line 3 for the last 5 simulations
sweep = ScenarioSweep(env)
sweep.modify_load_p(load_p)
sweep.set_contingency_lines(line_mask)
# modify_gen_p / modify_sgen_p / modify_load_q / set_contingency_trafos were never
# called: those axes default to the grid's own current state, broadcast across
# every row (see "Unset axes" below).
sweep.compute()
res_v = sweep.get_voltages()
res_a = sweep.compute_A()
res_p = sweep.compute_P()
# res_p[row_id] / res_a[row_id] / res_v[row_id] are the flows / voltages for
# simulation `row_id`, combining that row's injections and that row's contingency.
The setter-based API
Unlike TimeSerie / InjectionSweep’s single bundled compute_V_from_inj call, ScenarioSweep is built up incrementally: call as many of modify_gen_p / modify_sgen_p / modify_load_p / modify_load_q / modify_gen_v / set_contingency_lines / set_contingency_trafos / set_contingency_gens as are relevant, then compute().
Row-count locking. The first setter you call fixes the number of simulations for the whole object; every later setter is checked against it immediately – a shape mistake raises right at the call that made it, not later inside compute().
Unset axes. Any injection axis you never call (eg modify_gen_p) defaults to the grid’s own current target value, broadcast across every row – not zero. Any contingency mask you never call (set_contingency_lines / set_contingency_trafos / set_contingency_gens) defaults to all-False – nothing disconnected. compute() raises if you never call any setter (a degenerate call – use ac_pf() / dc_pf() directly for a single powerflow).
`modify_gen_v` is different from the other four `modify_*` setters. It does not feed the injection (Sbus) at all – a PV bus’s voltage magnitude is never part of what Newton-Raphson solves for, only of what it starts from. modify_gen_v instead re-seeds |V| at each voltage-regulating generator’s regulated bus immediately before that row’s solve, shape (n_simul, n_gen), in pu (vm_pu), NOT kV. Left unset, every row keeps using the grid’s own target_vm_pu, exactly as if it had never been called.
TimeSerie / InjectionSweep also gained this same setter API (modify_* + compute()) alongside their legacy compute_V_from_inj call, which remains available (deprecated, not removed).
Two contingency APIs, on purpose
ScenarioSweep.set_contingency_lines / set_contingency_trafos and ContingencyAnalysis.add_n1 / add_nk are deliberately two different APIs, not a naming inconsistency:
ContingencyAnalysis applies an arbitrary set of distinct contingencies to one shared base injection – add_n1(3) registers “disconnect line 3”, independent of any row ordering or count.
ScenarioSweep pairs each contingency with that same row’s own injection, so a dense, row-aligned boolean mask (shape (n_simul, n_line) / (n_simul, n_trafo), True = “deactivate this branch for this simulation”) is the natural fit instead.
ScenarioSweep does not have add_n1 / add_nk; ContingencyAnalysis does not have set_contingency_lines / set_contingency_trafos.
Note
Dense contingency masks cost real memory at scale (eg 10k simulations x 3k branches ~= 30 MB) – fine for typical use, a lighter-weight representation may be added in a future release if that becomes a bottleneck.
Generator contingencies
set_contingency_gens(mask) takes a (n_simul, n_gen) boolean mask, True meaning “disconnect this generator for this simulation”. Unlike the two branch masks it does not edit the admittance matrix – a generator carries no admittance. It does three things instead:
takes the generator’s active power out of that row’s injection (and its reactive setpoint too, if it is not a voltage-regulating machine);
re-weights the distributed slack without it, renormalised, so a row that drops a participating machine redistributes its share over the others. The set of slack buses is not touched: the angle reference is a property of the batch, chosen once;
and, when the last generator regulating its own bus is taken out, turns that bus from PV to PQ for that row. Its voltage magnitude is then solved for rather than held at the setpoint, and the reactive power that machine was supplying is no longer available there.
The lost MW is picked up by the slack. If you want a redispatch instead, express it with modify_gen_p – that is what it is for.
With redistribute_slack = True, the active power a row takes out of the balance of the grid’s own targets is first shared on the units of the distributed slack, OpenLoadFlow style (proportionally to their weight, each one clamped to its [min_p, max_p], a clamped unit leaving that row’s distributed slack), before the powerflow. It counts:
the set-points of the generators the row disconnects;
the net injection of the islands cut off with handle_disconnected_grid;
the imbalance of the row’s own injections: sum(gen_p - target) over the generators and static generators minus sum(load_p - target) over the loads.
A line or transformer contingency that leaves the grid connected loses no injection: the change in the losses it causes is left to the powerflow’s distributed slack.
import numpy as np
from lightsim2grid import ScenarioSweep
sweep = ScenarioSweep(env)
gen_mask = np.zeros((env.n_gen, env.n_gen), dtype=bool)
np.fill_diagonal(gen_mask, True) # one row per generator: the "generator N-1" sweep
sweep.set_contingency_gens(gen_mask)
sweep.compute()
v = sweep.get_voltages()
Note
The PV -> PQ relabelling would normally force a fresh symbolic factorization on every row, which is exactly what makes ScenarioSweep fast (one analysis, one factorization, refactorizations after that). It does not here: every bus that can flip is given a voltage-magnitude unknown and a reactive equation once, up front, so the Jacobian’s sparsity is the union over all the rows; a row where the bus is still PV simply masks that reactive equation to an identity row, freezing |V| at the setpoint. That is a value-level edit, and the factorization survives it.
The cost is one extra row and column per switchable bus, on every row of the sweep. A handful of candidate generators is negligible; masking every generator on the grid grows the Jacobian’s dimension by roughly the number of PV buses.
Warning
Only generators that regulate their own bus are supported for now. compute() raises if the mask names a generator that regulates a remote bus, or one standing on a bus whose voltage a control group holds (a remote generator, an SVC or an HVDC converter station). Those go through a different part of the Jacobian, with columns and rows of their own that this feature does not yet reserve and mask.
Topological actions
A row can also carry a topological action, the same object the light environment plays
(lightsim2grid.lightEnv.TopoAction, grid2op semantics), given as a grid2op action or
as a TopoAction, one per row:
import grid2op
from lightsim2grid import LightSimBackend
from lightsim2grid.scenarioSweep import ScenarioSweep
env = grid2op.make("l2rpn_case14_sandbox", backend=LightSimBackend())
obs = env.reset()
actions = [
env.action_space({}), # a plain row
env.action_space({"set_line_status": [(3, -1)]}), # line 3 off
env.action_space({"set_bus": {"loads_id": [(0, -1)]}}), # load 0 off
env.action_space({"set_bus": {"generators_id": [(1, -1)]}}), # gen 1 off: its bus turns PQ
]
sweep = ScenarioSweep(env)
sweep.set_topo_actions(actions)
sweep.compute()
Vs = sweep.get_voltages()
Every action is checked against the grid when it is registered (the element exists, the
busbar exists, no contradiction) and an invalid one raises a ValueError naming the row.
A row plays:
disconnections: a line or a trafo (
set_line_status -1, orset_bus -1on one of its ends), a generator, a load or a storage unit (set_bus -1). A branch or a generator is disconnected exactly as theset_contingency_*masks do it – the same admittance edit, the same PV -> PQ handling – so a row is free to combine an action with injections and masks, as long as it does not name one element in both a mask and its action (computerefuses that);reconnections and moves: an element put on a busbar (
set_busto it), a branch reconnected (set_line_status 1, orset_buson its ends). This is what creates a bus (a busbar nothing stood on, now used) or merges two (a busbar left empty).
The whole sweep still runs on one symbolic analysis. The solver labelling is built once
for the union of the buses any row uses, the admittance entries a row writes are reserved as
stored zeros before the base case is solved, and each row is then a set of value edits: the
coefficients of the branches it moves, the injections of the elements it moves, the PV pinning
of the buses whose generators it moves (a bus a generator lands on is held at the set-point,
a bus its last generator leaves is solved for), and the masking of the buses of the union
it does not use, exactly as handle_disconnected_grid masks a stranded bus. A row that
leaves an element alone on a busbar (an island of one bus) strands it, wherever that busbar
is: without handle_disconnected_grid the row is NOT_SIMULATED, as for a contingency
that strands a bus; with it, the bus is masked and the element’s injection left out. A busbar
the row leaves with no element at all (a merge), or does not use, strands nothing: it is
masked, and the row solved, in either mode.
Warning
Refused by compute for now: moving or reactivating a slack participant, a generator
on a slack bus, a generator that regulates a remote bus or whose bus a control group
holds, a storage unit that regulates voltage; a branch with one end open in the base grid
put back on (taking it out works); keep_jacobian on a batch that moves or reactivates a
generator; and the DC algorithm. See the TODO section of the changelog.
compute_physical_violations follows the row: a generator the row moves or reactivates
is checked on the bus the row gives it.
Per-row results
Besides the voltages, the C++ object (ScenarioSweepCPP, or sweep.computer from the Python wrapper) reports, for every row of the last compute():
compute_branch_results(): active and reactive power at both ends of every branch, as an array of shape (n_simul, n_branch, 4) – P and Q at side 1, then at side 2, in MW and MVAr, branches numbered lines then transformers like get_flows. They are what get_line_res1/2 and get_trafo_res1/2 would read after solving that row’s topology on its own. A branch the row disconnects reads 0, and so does a row that did not converge. In DC, Q is 0 and side 2 carries the opposite of side 1.
get_Ybus(row) (AC) / get_dcYbus(row) (DC): the admittance matrix that row was solved with, in grid bus numbering like LSGrid.get_Ybus (solver_numbering=True for the solver one). An entry only the disconnected branches contributed to is removed from the pattern rather than stored as an explicit zero.
get_row_solve_times() / get_row_nb_iter(): what the algorithm reported for each row’s solve, 0 for a row that never reached it.
get_gen_results(), once compute_gen_results = True was set before compute(): the active and reactive output of every generator, shape (n_simul, n_gen, 2), MW and MVAr. The rules are the ones get_gen_res follows after a single powerflow: the setpoint plus the generator’s share of the distributed slack (re-weighted without the participants the row disconnects); for Q, the setpoint of a machine that does not regulate, the solver’s own value for a controller it solved (a remote voltage control group), or a share of its bus’ reactive residual proportional to each machine’s reactive range. A generator the row disconnects reads 0. In DC, Q is 0.
With set_topo_actions, the branch and generator results follow the row’s action: an element it moves, reconnects or reactivates is reported on the buses the row gives it.
compute_branch_results, get_gen_results, get_row_solve_times and get_row_nb_iter exist on every batch class (TimeSeriesCPP, InjectionSweepCPP, ContingencyAnalysisCPP too); get_Ybus / get_dcYbus only where the topology varies per row (ScenarioSweepCPP, ContingencyAnalysisCPP).
computer = sweep.computer
res = computer.compute_branch_results() # (n_simul, n_branch, 4)
p_or, q_or, p_ex, q_ex = res[..., 0], res[..., 1], res[..., 2], res[..., 3]
ybus_row_3 = computer.get_Ybus(3) # scipy.sparse, (total_bus, total_bus)
computer.compute_gen_results = True # before compute()
computer.compute(v_init, max_iter, tol)
gen = computer.get_gen_results() # (n_simul, n_gen, 2): P, Q
Outside a batch, LSGrid.get_gen_slack_shares() gives each generator’s share of the distributed slack (P = target_p + share * slack absorbed). The weights it reads are set when the grid is built and by update_slack_weights; change_p_gen does not update them.
Many sweeps in a row
A sweep holds its own copy of the grid, taken at construction: changing the grid afterwards (change_p_gen, change_p_load, …) does not reach it. A scenario that differs from the grid by its injections is therefore expressed through the sweep, not by editing the grid and building a new sweep:
computer = ScenarioSweepCPP(grid)
computer.set_contingency_lines(line_mask) # the outages, once
for scenario in scenarios:
computer.modify_gen_p(scenario.gen_p) # one row per simulation
computer.modify_load_p(scenario.load_p)
computer.modify_load_q(scenario.load_q)
computer.compute(v_init, max_iter, tol)
assert computer.base_case_was_reused() # nothing rebuilt but the injections
What one compute() builds is kept for the next, and only what an input changed is rebuilt:
new injections (modify_*) keep everything: the admittance matrix, the base-case powerflow, the symbolic analysis of the Jacobian;
new contingency masks (set_contingency_*) rebuild the base case (which branches each row disconnects is part of it) but keep what was read off the grid. A fixed outage list across scenarios is the fastest way to use one sweep.
Building a new sweep per scenario stays correct, only slower; it is what to do when the grid itself changes (its parameters, its topology).
Threads and processes
compute() releases Python’s GIL. Two ways to use several cores follow from that:
a pool of Python threads, each running its own sweep (one object per thread: a sweep is not meant to be used by two threads at once). This works because the GIL is released for the whole solve;
nb_thread on one sweep, which splits its rows over C++ threads. It defaults to 1.
In a caller that already runs one process per core, keep nb_thread = 1: more threads than cores only adds contention. nb_thread > 1 pays one symbolic analysis per thread, so it is worth it for many rows rather than for a few.
Handling disconnected grids and limit violations
ScenarioSweep has the same handle_disconnected_grid mode and inline limit-violation checking (compute_limit_violations / violation_threshold / violation_rel_tol / get_violations / get_violations_n) as ContingencyAnalysis – see Contingency Analysis for the full description of what each does. Same names, same semantics, and both classes’ get_violations / get_violations_n return the same LimitViolation objects.
sweep = ScenarioSweep(env)
sweep.compute_limit_violations = True # see warning below: set this FIRST
sweep.handle_disconnected_grid = True
sweep.modify_load_p(load_p)
sweep.set_contingency_lines(line_mask)
sweep.compute()
row_violations = sweep.get_violations() # one list of LimitViolation per row
n_violations = sweep.get_violations_n() # violations of the shared pre-batch base case
sa_res = sweep.run() # PreContingencyResult / ContingencyResult / SecurityAnalysisResult,
# reusing lightsim2grid.contingencyAnalysis's dataclasses --
# contingency_name is always None here (no such concept on ScenarioSweep)
Warning
Just like on ContingencyAnalysis, setting compute_limit_violations through the property clears the whole object – registered injections, contingency masks, handle_disconnected_grid, everything, the same reset clear() performs. Set it first, before calling any modify_* / set_contingency_* setter or handle_disconnected_grid, or those calls will be silently wiped.
Unlike ContingencyAnalysis, ScenarioSweep has no (grid, compute_limit_violations) constructor shortcut to dodge this ordering – the property is the only way to set it.
Note
There is deliberately no converged / converged_n on ScenarioSweep (unlike ContingencyAnalysis). A non-converged row’s get_violations() entry already carries a single LimitViolation with element_type == ViolationElementType.GRID and violation_type either NOT_SIMULATED (skipped before the solver ran, eg it splits the grid and handle_disconnected_grid is off) or DIVERGENCE (the solver ran but did not converge) – a separate convergence flag would be redundant. If the shared pre-batch “n” powerflow itself diverges, that same sentinel is stamped into get_violations_n() and into every row of get_violations(), rather than leaving them as empty lists indistinguishable from “converged, nothing found”.
A future release may add a “combine mode” axis (this row-aligned / zipped behavior vs. a cartesian “every contingency x every injection profile”) – not yet available.
Detailed usage
Classes:
|
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. |
|
Batch powerflow that varies both the injection and a contingency (line, trafo or generator disconnection) per simulation, independently, row-aligned: row i of every |
Batch powerflow varying both the injection AND a contingency per simulation, row-aligned: row i of every modify_* input is solved together with row i of set_contingency_lines / set_contingency_trafos. |
|
Result of ContingencyAnalysis.run / run_ac / run_dc, modeled after pypowsybl's security analysis result. |
|
The kind of element on which a limit was violated: |
- class lightsim2grid.scenarioSweep.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.scenarioSweep.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.scenarioSweep.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.scenarioSweep.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.scenarioSweep.ScenarioSweep(grid2op_env)[source]
Batch powerflow that varies both the injection and a contingency (line, trafo or generator disconnection) per simulation, independently, row-aligned: row i of every
modify_*input is solved together with row i ofset_contingency_lines/set_contingency_trafos/set_contingency_gens.Unlike
lightsim2grid.timeSerie.TimeSerie/lightsim2grid.injectionSweep.InjectionSweep(a single bundledcompute_V_from_injcall), this class uses a setter-based API: call as many ofmodify_gen_p/modify_sgen_p/modify_load_p/modify_load_q/modify_gen_v/set_contingency_lines/set_contingency_trafos/set_contingency_gensas are relevant, thencompute(). Any axis you never set defaults to the grid’s own state for every row (its own target injection / voltage setpoint, or “nothing disconnected”). The first setter you call fixes the number of simulations; every later setter is checked against it immediately. Notemodify_gen_vis different from the othermodify_*setters: it does not feed the injection (Sbus), it only re-seeds the voltage magnitude (in pu, NOT kV) at each voltage-regulating generator’s regulated bus before that row’s solve.This is deliberately a different API from
lightsim2grid.contingencyAnalysis.ContingencyAnalysis’sadd_n1/add_nk(a set of distinct scenarios applied to one shared base case): here each row pairs its own injection with its own contingency, so a dense, row-aligned mask is the natural shape – the two APIs are not interchangeable.Examples
import numpy as np import grid2op from lightsim2grid import ScenarioSweep from lightsim2grid import LightSimBackend env_name = ... env = grid2op.make(env_name, backend=LightSimBackend()) sweep = ScenarioSweep(env) sweep.modify_load_p(load_p) # shape (n_simul, n_load) line_mask = np.zeros((load_p.shape[0], env.n_line), dtype=bool) line_mask[:, 3] = True # disconnect line 3 for every simulation sweep.set_contingency_lines(line_mask) sweep.compute() Vs = sweep.get_voltages() Ps, amps = sweep.compute_P(), sweep.compute_A()
Methods:
clear()Clear everything, as if nothing had ever been set / computed.
close()permanently close the object
compute([v_init, max_iter, tol, ignore_errors])Run the batch: one powerflow per simulation, using whatever was set by the
modify_*/set_contingency_*setters.Current flows (in Amps, A) at the origin (for powerline) / high voltage (for transformer) side, per simulation.
Active power flows (in MW) at the origin (for powerline) / high voltage (for transformer) side, per simulation.
get_flows([v_init, max_iter, tol, ignore_errors])Run
compute()(using whatever was set by the setters) then retrieve the active power / current / voltage results for every simulation, in one call.Per row: the list of
LimitViolationof the physical limits that row's solution leaves --element_typeBUSwithviolation_typeLOW_Q/HIGH_Q(the reactive capability of the machines holding that bus),element_typeGENERATORwithLOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Q(a flagged PQ generator that would regulate again,value/limitin kV),element_typeSVCwithLOW_VOLTAGE_SVC_STANDBY/HIGH_VOLTAGE_SVC_STANDBY(a flagged idle standby SVC its automaton would switch on,value/limitin kV),element_typeGENERATORwithLOW_VOLTAGE_REMOTE_CONTROL/HIGH_VOLTAGE_REMOTE_CONTROL(a remote voltage controller whose own bus leaves the realistic range,value/limitin kV),element_typeHVDCwithHIGH_Pandsidenaming the direction (the active power of an angle-droop hvdc line),element_typeHVDCwithHVDC_AC_EMULATION_RELEASEandsidethe direction it is frozen in (a flagged frozen line whose droop would ask for less than its limit,value/limitin MW), orelement_typeGENERATOR/STORAGEwithLOW_P/HIGH_P(the distributed slack).Same as
get_physical_violations(), for the pre-batch ("n") case.Per row (same order as every
modify_*/set_contingency_*input): list ofLimitViolation.List of
LimitViolationfor the pre-batch ("n") case (no injection change, no contingency) shared by every row.Complex voltage, at each bus, for each simulation.
modify_gen_p(gen_p)Per-step active generator setpoints, shape
(n_simul, n_gen).modify_gen_v(gen_v)Per-step generator target voltage magnitude, shape
(n_simul, n_gen), in pu (vm_pu), NOT kV.modify_load_p(load_p)Per-step active load setpoints, shape
(n_simul, n_load).modify_load_q(load_q)Per-step reactive load setpoints, shape
(n_simul, n_load).modify_sgen_p(sgen_p)Per-step active static generator setpoints, shape
(n_simul, n_sgen).run()Run this batch (calling
compute()if not already done) and report, for the pre-batch ("n") case and for each row, the list of limit violations -- same return type aslightsim2grid.contingencyAnalysis.ContingencyAnalysis.run(), reusing the very samePreContingencyResult/ContingencyResult/SecurityAnalysisResultdataclasses so callers can handle either result the same way.set_contingency_gens(mask)Per-step generator contingency mask, shape
(n_simul, n_gen), dtype bool.set_contingency_lines(mask)Per-step powerline contingency mask, shape
(n_simul, n_line), dtype bool.set_contingency_trafos(mask)Per-step trafo contingency mask, shape
(n_simul, n_trafo), dtype bool.set_topo_actions(actions)One topological action per row, played by that row on top of its injections and contingency masks.
Attributes:
Whether limit violations are computed inline, per row, during
compute()/run()(seeget_violations()/get_violations_n()/run()-- there is noconverged/converged_nhere, unlikelightsim2grid.contingencyAnalysis.ContingencyAnalysis: a non-converged row's violations already carry aViolationElementType.GRID-typed sentinel that fully encodes that row's status by itself).Whether every converged row reports the PHYSICAL limits its solution leaves -- a state the grid cannot reach at all (
ViolationCategory.PHYSICAL), as opposed to the operational limitscompute_limit_violationsreports (a voltage band, a thermal rating: states the grid does reach and should not sit in).Whether a row whose contingency 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 simulation with the results of a "n" powerflow (a powerflow with no injection change and no contingency) instead of the vector given to
compute.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 (MVA) on every comparison
compute_physical_violationsmakes: 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 row loses (the generators its generator contingency disconnects, the elements of an island cut off with
handle_disconnected_grid, and the imbalance its ownmodify_gen_p()/modify_sgen_p()/modify_load_p()create against the grid's targets) 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 row'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 whencompute_limit_violationsisTrue: a value is reported only when it is beyond its effective limit by more than this fraction of it, so a value on its limit up to rounding (a bus a generator holds at itsvmax) is not a violation.Threshold (a
floatin]0., 1.], default1.0) applied to every limit-violation check performed whencompute_limit_violationsisTrue.- compute(v_init=None, max_iter=None, tol=None, ignore_errors=False)[source]
Run the batch: one powerflow per simulation, using whatever was set by the
modify_*/set_contingency_*setters. Raises if nothing was ever set.max_iter/toldefault to the backend’s own values (self.grid2op_env.backend.max_it/.tol) when not given.
- compute_A()[source]
Current flows (in Amps, A) at the origin (for powerline) / high voltage (for transformer) side, per simulation. Does not recompute the voltages; you must call
compute()first.
- compute_P()[source]
Active power flows (in MW) at the origin (for powerline) / high voltage (for transformer) side, per simulation. Does not recompute the voltages; you must call
compute()first.
- property compute_limit_violations
Whether limit violations are computed inline, per row, during
compute()/run()(seeget_violations()/get_violations_n()/run()– there is noconverged/converged_nhere, unlikelightsim2grid.contingencyAnalysis.ContingencyAnalysis: a non-converged row’s violations already carry aViolationElementType.GRID-typed sentinel that fully encodes that row’s status by itself). Default:False. Computing violations means an extra per-element current / voltage check in every row’s solve, so leave this off if you only needcompute_flows()/get_flows(). Changing this flag clears any previously-computed results.
- property compute_physical_violations
Whether every converged row reports the PHYSICAL limits its solution leaves – a state the grid cannot reach at all (
ViolationCategory.PHYSICAL), as opposed to the operational limitscompute_limit_violationsreports (a voltage band, a thermal rating: states the grid does reach and should not sit in). Default:False. Same meaning aslightsim2grid.timeSerie.TimeSerie.compute_physical_violations, which documents the checks (the reactive capability of a bus, the release of a PQ generator flagged as pinned at a reactive limit, the switch on of an idle SVC flagged as carrying a standby automaton, the own bus of a remote voltage controller, the active power of an angle-droop hvdc line, the release of one flagged as frozen at its limit, and the active power of a machine carrying the distributed slack) in full.Detection only: nothing is switched PV -> PQ or back, no droop is clamped, no row is re-solved. Unlike
compute_limit_violations, changing this flag keeps the registered injections and contingency masks – only the results go.
- get_flows(v_init=None, max_iter=None, tol=None, ignore_errors=False)[source]
Run
compute()(using whatever was set by the setters) then retrieve the active power / current / voltage results for every simulation, in one call.Each row of the resulting flow matrix corresponds to a simulation.
- get_physical_violations()[source]
Per row: the list of
LimitViolationof the physical limits that row’s solution leaves –element_typeBUSwithviolation_typeLOW_Q/HIGH_Q(the reactive capability of the machines holding that bus),element_typeGENERATORwithLOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Q(a flagged PQ generator that would regulate again,value/limitin kV),element_typeSVCwithLOW_VOLTAGE_SVC_STANDBY/HIGH_VOLTAGE_SVC_STANDBY(a flagged idle standby SVC its automaton would switch on,value/limitin kV),element_typeGENERATORwithLOW_VOLTAGE_REMOTE_CONTROL/HIGH_VOLTAGE_REMOTE_CONTROL(a remote voltage controller whose own bus leaves the realistic range,value/limitin kV),element_typeHVDCwithHIGH_Pandsidenaming the direction (the active power of an angle-droop hvdc line),element_typeHVDCwithHVDC_AC_EMULATION_RELEASEandsidethe direction it is frozen in (a flagged frozen line whose droop would ask for less than its limit,value/limitin MW), orelement_typeGENERATOR/STORAGEwithLOW_P/HIGH_P(the distributed slack). Every entry hascategory == ViolationCategory.PHYSICAL.A row that did not converge has an empty entry, not a sentinel (unlike
get_violations()) – useself.computer.converged_mask()to tell that from “converged, no violation”. Requirescompute_physical_violationsto beTrue(raises otherwise);run()returns the same lists in a structured result.
- get_physical_violations_n()[source]
Same as
get_physical_violations(), for the pre-batch (“n”) case. Empty if that solve did not converge. Requirescompute_physical_violationsto beTrue.
- get_violations()[source]
Per row (same order as every
modify_*/set_contingency_*input): list ofLimitViolation. Requirescompute_limit_violationsto beTrue(raises otherwise). Preferrun()for a structured result – this is the raw passthrough. Seelightsim2grid.contingencyAnalysis.ContingencyAnalysis.compute_limit_violations’s note on the sentinel entry a non-converged row carries (there is no separateconvergedhere, by design – seecompute_limit_violations).
- get_violations_n()[source]
List of
LimitViolationfor the pre-batch (“n”) case (no injection change, no contingency) shared by every row. Requirescompute_limit_violationsto beTrue(raises otherwise).
- get_voltages()[source]
Complex voltage, at each bus, for each simulation. Must be called after
compute().
- property handle_disconnected_grid
Whether a row whose contingency splits the grid into several connected components is simulated on its largest component instead of being skipped. Default:
False, meaning such a row is not simulated at all (its voltages are left at 0.). 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. Same name/semantics aslightsim2grid.contingencyAnalysis.ContingencyAnalysis.handle_disconnected_grid.
- property init_from_n_powerflow
Whether to initialize the complex voltages of each simulation with the results of a “n” powerflow (a powerflow with no injection change and no contingency) instead of the vector given to
compute. Default:False. Must be set beforecompute()actually runs.
- 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.
- modify_gen_v(gen_v)[source]
Per-step generator target voltage magnitude, shape
(n_simul, n_gen), in pu (vm_pu), NOT kV. Unlikemodify_gen_p/modify_load_p/modify_load_q, this does NOT feed the injection (Sbus) – it only re-seeds|V|at each voltage-regulating generator’s regulated bus before that step’s solve.
- property nb_thread
1).The steps are split into contiguous ranges, each solved by its own thread with its own solver (and its own admittance-matrix copy, since a contingency edits it), writing to disjoint rows of the result matrix: the results do not depend on the number of threads. Values
< 1are clamped to1.Must be set before
compute()actually runs; it has no effect on a batch that has already been computed.- Type:
Number of OS threads used to compute the steps (default
- property physical_violation_tol_mva
Absolute slack (MVA) on every comparison
compute_physical_violationsmakes: a violation needsvalue > limit + tol(orvalue < limit - tolforLOW_QandHVDC_AC_EMULATION_RELEASE). Default:1e-4. Changing it invalidates any previously-computed results.
- 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 row loses (the generators its generator contingency disconnects, the elements of an island cut off with
handle_disconnected_grid, and the imbalance its ownmodify_gen_p()/modify_sgen_p()/modify_load_p()create against the grid’s targets) 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 row’s distributed slack), the powerflow then only sharing what is left (the change in the losses) on the units that can still move. A line / trafo contingency that leaves the grid connected loses no injection: the change in the losses it causes is left to the powerflow. Default:False. NeedsLSGrid.set_gen_p_limits/set_storage_p_limitsto clamp anything. Needs an algorithm with a distributed slack:computerefuses it with a single-slack one (NRSing_*, Gauss-Seidel). Same name/semantics aslightsim2grid.contingencyAnalysis.ContingencyAnalysis.redistribute_slack.
- run() SecurityAnalysisResult[source]
Run this batch (calling
compute()if not already done) and report, for the pre-batch (“n”) case and for each row, the list of limit violations – same return type aslightsim2grid.contingencyAnalysis.ContingencyAnalysis.run(), reusing the very samePreContingencyResult/ContingencyResult/SecurityAnalysisResultdataclasses so callers can handle either result the same way.Requires
compute_limit_violationsto beTrue(set viathis_instance.compute_limit_violations = True– there is no constructor argument for it, unlikelightsim2grid.contingencyAnalysis.ContingencyAnalysis), else aRuntimeErroris raised.physical_violationsis filled as well whencompute_physical_violationsis on, and is an empty list otherwise – it is kept apart fromlimit_violationsbecause those are PHYSICAL statements (the solution is unreachable) rather than operational ones.Unlike
lightsim2grid.contingencyAnalysis.ContingencyAnalysis.run(), rows are already in caller-set order here (no dedup / reordering concept, sinceScenarioSweeprows are independent scenarios, not a set of contingencies) – and everyContingencyResult.contingency_nameisNone(no such concept onScenarioSweep);element_ids/element_namesare instead the branches that row disconnects, through itsset_contingency_lines/set_contingency_trafosmasks and its topological action together.
- set_contingency_gens(mask)[source]
Per-step generator contingency mask, shape
(n_simul, n_gen), dtype bool.Truemeans “disconnect this generator for this simulation”.Unlike
set_contingency_lines()this does not edit the admittance matrix – a generator carries no admittance. It removes the generator’s active power (and, if it does not regulate voltage, its reactive setpoint) from that step’s injection, re-weights the distributed slack without it, and – when the last generator regulating its own bus is taken out – turns that bus from PV to PQ for the step, so its voltage magnitude is solved for instead of held at the setpoint. The lost MW is picked up by the slack; usemodify_gen_p()to express a redispatch instead.The PV/PQ relabelling costs no extra symbolic factorization: every bus that can flip is given a voltage-magnitude unknown and a reactive equation once, up front, so the Jacobian’s sparsity is the union over all the steps, and each step merely masks the equation of the buses that are still PV. The whole sweep keeps running on a single analysis, as it did before this axis existed.
Only generators regulating their own bus are supported for now.
compute()raises if the mask names a generator that regulates a remote bus, or one whose bus a control group holds (a remote generator, an SVC or an HVDC converter station).
- set_contingency_lines(mask)[source]
Per-step powerline contingency mask, shape
(n_simul, n_line), dtype bool.Truemeans “deactivate this powerline for this simulation”.
- set_contingency_trafos(mask)[source]
Per-step trafo contingency mask, shape
(n_simul, n_trafo), dtype bool. Seeset_contingency_lines().
- set_topo_actions(actions)[source]
One topological action per row, played by that row on top of its injections and contingency masks.
actionsis a list (one entry per simulation) of grid2op actions –set_busandset_line_statusonly, seelightsim2grid.lightEnv.topo_action_from_grid2op()– or oflightsim2grid.lightEnv.TopoAction; mixing both is fine, and an action that changes nothing is a plain row. Every action is checked against the grid: an element that does not exist, a busbar that does not exist (eg-2), an unsupported modification (change_bus,redispatch, …) or a contradiction raises aValueErrornaming the action, and nothing is registered.A row plays disconnections (a line or trafo by
set_line_status-1orset_bus-1on one of its ends, a generator, a load or a storage unit byset_bus-1), reconnections and moves between busbars (set_busto a busbar,set_line_status1): a bus created or merged. A disconnection is played exactly asset_contingency_lines()/set_contingency_trafos()/set_contingency_gens()do it, andcompute()raises if a row names an element in both a mask and its action.The whole sweep keeps running on one symbolic analysis: the solver labelling is built for the union of the buses the rows use, the admittance entries a row writes are reserved up front, and a row is value edits only – its coefficients, its injections, the PV pinning of the buses whose generators it moves, and the masking of the union buses it leaves empty (an element alone on a busbar is an island of one: that bus is masked and its injection left out). Refused by
compute()for now: moving or reactivating a slack participant, a generator on a slack bus or one that regulates a remote bus, a storage unit that regulates voltage, a branch with one end open in the base grid put back on,keep_jacobianwith a generator move or reactivation, and the DC algorithm.compute_physical_violationsfollows the row: a generator the row moves or reactivates is checked on the bus the row gives it.
- property violation_rel_tol
Relative tolerance (a
floatin[0., 1.[, default1e-9) of every limit-violation check performed whencompute_limit_violationsisTrue: a value is reported only when it is beyond its effective limit by more than this fraction of it, so a value on its limit up to rounding (a bus a generator holds at itsvmax) is not a violation. Same meaning aslightsim2grid.contingencyAnalysis.ContingencyAnalysis.violation_rel_tol– see that docstring for the formulas.0.gives the bare strict comparisons. Changing it invalidates any already-computed results.
- property violation_threshold
Threshold (a
floatin]0., 1.], default1.0) applied to every limit-violation check performed whencompute_limit_violationsisTrue. Same meaning aslightsim2grid.contingencyAnalysis.ContingencyAnalysis.violation_threshold– see that docstring for the full formulas. Lowering it makes every check stricter (more violations reported, never fewer) and invalidates any already-computed results; raising it back up does not.
- class lightsim2grid.scenarioSweep.ScenarioSweepCPP
Batch powerflow varying both the injection AND a contingency per simulation, row-aligned: row i of every modify_* input is solved together with row i of set_contingency_lines / set_contingency_trafos. Build up the batch with modify_gen_p / modify_sgen_p / modify_load_p / modify_load_q and set_contingency_lines / set_contingency_trafos (any axis never set defaults to the grid’s own state for every row), then call compute(). Unlike ContingencyAnalysisCPP’s add_n1/add_nk (a set of distinct scenarios applied to one shared base case), set_contingency_lines/trafos are dense boolean masks of shape (n_simul, n_lines) / (n_simul, n_trafos) – True means ‘deactivate this branch for this simulation’; the two APIs are deliberately not unified, they serve different usages.
Methods:
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 solver and to as if the class never performed any powerflow.
close(self)Clear the solver and to as if the class never performed any powerflow.
compute(self, Vinit, max_iter, tol)Run the batch: one powerflow per simulation, using whatever was set by modify_* (and, on ScenarioSweep, set_contingency_lines / set_contingency_trafos).
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)Retrieve the flows (in amps, at the origin of each powerlines / high voltage size of each transformers.
compute_power_flows(self)Retrieve the active flows (in MW, at the origin of each powerlines / high voltage size of each transformers.
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).
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 every step.
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 current 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(self)Per row: the list of LimitViolation of the physical limits that row's solution leaves.
The same, for the base ("n") case every row is solved from (no injection change, no contingency).
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_disconnected_branches(self, row)The branches (gridmodel numbering, powerlines then transformers, sorted) row row disconnects, by its masks and by its topological action together.
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_status(self)Status of the solvers (1: success, 0: failure).
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_topo_actions(self)The (checked) actions registered with set_topo_actions.
get_violations(self)Per row (same order as every modify_* / set_contingency_* input): list of LimitViolation.
get_violations_n(self)List of LimitViolation for the pre-batch ("n") case (no injection change, no contingency) shared by every row.
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).modify_gen_p(self, gen_p)Per-step active generator setpoints, shape (n_simul, n_gen).
modify_gen_v(self, gen_v)Per-step generator target voltage magnitude, shape (n_simul, n_gen), in pu (vm_pu), NOT kV.
modify_load_p(self, load_p)Per-step active load setpoints, shape (n_simul, n_load).
modify_load_q(self, load_q)Per-step reactive load setpoints, shape (n_simul, n_load).
modify_sgen_p(self, sgen_p)Per-step active static generator setpoints, shape (n_simul, n_sgen).
nb_converged(self)Number of powerflows, among those nb_solved() attempted, that actually converged.
nb_solved(self)Total number of powerflows solved.
preprocessing_time(self)Time spent in pre processing the data (this involves, but is not limited to the computation of the Sbus)
set_algo_config(self, config)See get_algo_config().
set_contingency_gens(self, mask)Per-step generator contingency mask, shape (n_simul, n_gen), dtype bool.
set_contingency_lines(self, mask)Per-step powerline contingency mask, shape (n_simul, n_line), dtype bool.
set_contingency_trafos(self, mask)Per-step trafo contingency mask, shape (n_simul, n_trafo), dtype bool.
set_topo_actions(self, actions)One topological action (TopoAction, grid2op semantics: set_bus on loads / generators / storage units / line and trafo ends, set_line_status) per row, played by that row on top of its injections and masks; an action changing nothing is a plain row.
solve_JT(self, xbar)Solve the adjoint system `J_i^T .
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.
a non-converged row's get_violations() entry already carries a GRID-type NOT_SIMULATED / DIVERGENCE LimitViolation, which fully encodes that row's status by itself.
states the grid does reach and should not sit in).
Whether to simulate a row whose contingency splits the grid into 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 compute the steps (default
1).one noise floor for both halves, MW and MVAr being the same scale.
the PQ -> PV release check reports a flagged machine whose regulated voltage is below (at min_q) or above (at max_q) its target by more than this, in pu, and the standby SVC check a flagged SVC whose regulated voltage is outside its automaton's thresholds by more than this, and the remote voltage control check a remote controller whose own voltage is outside the realistic range by more than this.
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.- adjoint_memory_bytes(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP, 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.ScenarioSweepCPP, 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.ScenarioSweepCPP, arg0: lightsim2grid.lightsim2grid_cpp.AlgorithmType) -> None
DEPRECATED: use ‘change_algorithm’ instead
change_solver(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, arg0: str) -> None
DEPRECATED: use ‘change_algorithm’ instead
- clear(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) None
Clear the solver and to as if the class never performed any powerflow.
- close(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) None
Clear the solver and to as if the class never performed any powerflow.
- compute(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, Vinit: Annotated[numpy.typing.NDArray[numpy.complex128], '[m, 1]'], max_iter: SupportsInt | SupportsIndex, tol: SupportsFloat | SupportsIndex) None
Run the batch: one powerflow per simulation, using whatever was set by modify_* (and, on ScenarioSweep, set_contingency_lines / set_contingency_trafos). Raises if nothing was ever set.
- compute_branch_results(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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.ScenarioSweepCPP) Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.writeable', 'flags.c_contiguous']
Retrieve the flows (in amps, at the origin of each powerlines / high voltage size of each transformers.
Warning
This function must be called after
lightsim2grid.timeSerie.TimeSeriesCPP.compute_Vs()has been called.Note
This function must be called before
lightsim2grid.timeSerie.TimeSeriesCPP.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
a non-converged row’s get_violations() entry already carries a GRID-type NOT_SIMULATED / DIVERGENCE LimitViolation, which fully encodes that row’s status by itself.
- Type:
Whether limit violations are computed inline, per row, during compute() (see get_violations() / get_violations_n()). Defaults to
False. Computing violations means an extra per-element current / voltage check in every row’s solve, so users who only need compute_flows() / get_flows() should leave this off. Changing this flag clears any previously-computed results. Unlike ContingencyAnalysisCPP, there is no converged() / converged_n() here
- property compute_physical_violations
states the grid does reach and should not sit in). Defaults to
False. See get_physical_violations().Each check is a condition a PowSyBl OpenLoadFlow outer loop acts on, and none enforced here (no bus is switched PV -> PQ or back, no droop is clamped, no machine leaves the slack distribution, no row is re-solved):
the REACTIVE CAPABILITY of every bus whose voltage is held by machines (LOW_Q / HIGH_Q on the BUS): did it need more reactive power than the SUM of what its voltage-regulating generators, storage units, hvdc converter stations and voltage-mode SVCs can produce? A machine has no reactive setpoint – its output is solved for and never clamped – so a row can converge asking for reactive power that does not exist. Per bus, not per machine: the split between the machines of one bus is a sharing convention rather than something the solver decides. OpenLoadFlow’s
ReactiveLimits.the RELEASE of every PQ generator flagged as pinned at a reactive limit (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q on the GENERATOR, see LSGrid.set_gen_can_be_pv): is the bus it would regulate below its target while the machine absorbs all it can (or above it while it produces all it can)? The other direction, PQ -> PV, of the same
ReactiveLimitsloop. value and limit in kV, compared with physical_violation_tol_vm_pu.the SWITCH ON of every idle SVC flagged as carrying a standby automaton (LOW_VOLTAGE_SVC_STANDBY / HIGH_VOLTAGE_SVC_STANDBY on the SVC, see LSGrid.set_svc_standby): is the bus it regulates outside the automaton’s voltage thresholds? OpenLoadFlow’s
MonitoringVoltageOuterLoop. value and limit in kV, compared with physical_violation_tol_vm_pu.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): does holding the remote target take it outside that range? OpenLoadFlow’s
ReactiveLimits, in its robust remote voltage control mode. value and limit in kV, compared with physical_violation_tol_vm_pu.the ACTIVE POWER of every angle-droop (“AC emulation”) hvdc line still in the linear regime (HIGH_P on the HVDC): did
p0 + k.(theta1 - theta2)leavepmax_1to2_mw/pmax_2to1_mw?status_droopis an INPUT of the solve, so nothing saturates the droop on its own. OpenLoadFlow’sHvdcAcEmulationLimits.the RELEASE of every angle-droop hvdc line flagged as frozen at its active power limit (HVDC_AC_EMULATION_RELEASE on the HVDC, see LSGrid.set_hvdc_ac_emulation_frozen): would its droop ask for less than that limit? The other half of the same
HvdcAcEmulationLimitsloop. side the direction it is frozen in, value and limit in MW.the ACTIVE POWER of every generator and every storage unit carrying the DISTRIBUTED SLACK (LOW_P / HIGH_P on the GENERATOR / STORAGE): the slack is solved inside the Jacobian by fixed participation factors that know nothing about limits, so
target_p + its share of the 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 (LSGrid.set_gen_p_limits / LSGrid.set_storage_p_limits): a grid without them reports nothing here. OpenLoadFlow’sDistributedSlack.
The hvdc and active-power checks need only the bus angles and the slack the row distributed, so they work in DC too; the reactive one needs an AC algorithm that publishes its per-bus mismatch (every built-in AC algorithm does, a plugin solver has to opt in) and compute() raises for one that does not. A DC batch reports the active-power checks alone (the hvdc ones and the slack one) – a DC powerflow has no reactive power at all, and no voltage magnitude for the release, standby SVC and remote voltage control checks, so nothing is hidden by that.
Setting this drops this batch’s base case and results, but not the registered contingencies / injections (unlike compute_limit_violations, which clears everything), so it can be set at any point before compute().
- Type:
Whether every converged row reports the PHYSICAL limits its solution leaves – the ones whose violation means the row is not a state the grid can reach at all (ViolationCategory.PHYSICAL), as opposed to the operational limits compute_limit_violations reports (a voltage band, a thermal rating
- compute_power_flows(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.writeable', 'flags.c_contiguous']
Retrieve the active flows (in MW, at the origin of each powerlines / high voltage size of each transformers.
Warning
This function must be called after
lightsim2grid.timeSerie.TimeSeriesCPP.compute_Vs()has been called.Note
This function must be called before
lightsim2grid.timeSerie.TimeSeriesCPP.get_flows()Note
During this computation, the GIL is released, allowing easier parrallel computation
- converged_mask(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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.
- dim_J(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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.ScenarioSweepCPP, 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.ScenarioSweepCPP, 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.ScenarioSweepCPP) lightsim2grid.lightsim2grid_cpp.AlgoConfig
Config (eg ScalingPolicyType / damping parameters) of the internal solver used for every step. 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) numpy.typing.NDArray[numpy.float64]
The array of the last compute_branch_results(), without computing it again.
- get_dcYbus(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, 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.ScenarioSweepCPP) Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]']
Get the current flows (in kA) at the origin side / high voltage side of each transformers / powerlines.
Each rows correspond to a time step, each column to a powerline / transformer
Warning
This function must be called after
lightsim2grid.timeSerie.TimeSeriesCPP.compute_flows()has been called. (compute_flows also requires thatlightsim2grid.timeSerie.TimeSeriesCPP.compute_Vs()has been caleed)- Returns:
As – The flows (in kA) at the origin side / high voltage side of each transformers / powerlines.
- Return type:
numpy.ndarry(matrix)
- get_gen_results(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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_n(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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_power_flows(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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 time step, each column to a powerline / transformer
Warning
This function must be called after
lightsim2grid.timeSerie.TimeSeriesCPP.compute_power_flows()has been called. (compute_power_flows also requires thatlightsim2grid.timeSerie.TimeSeriesCPP.compute_Vs()has been caleed)- Returns:
Ps – The active flows (in MW) at the origin side / high voltage side of each transformers / powerlines.
- Return type:
numpy.ndarry(matrix)
- get_q_row_of_bus(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']
Same as get_p_row_of_bus for the REACTIVE power mismatch equation.
- get_row_disconnected_branches(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, row: SupportsInt | SupportsIndex) list[int]
The branches (gridmodel numbering, powerlines then transformers, sorted) row row disconnects, by its masks and by its topological action together. Needs a compute() when topological actions are registered.
- get_row_nb_iter(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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_status(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) int
Status of the solvers (1: success, 0: failure).
Note
Even if the solver failed at some point, some results might still be available (but not totally).
- get_theta_col_of_bus(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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_topo_actions(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) list[ls2g::TopoAction]
The (checked) actions registered with set_topo_actions.
- get_violations(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) list[list[lightsim2grid.lightsim2grid_cpp.LimitViolation]]
Per row (same order as every modify_* / set_contingency_* input): list of LimitViolation. A row that did not converge 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 with handle_disconnected_grid off) or LimitViolationType.DIVERGENCE (the solver ran but did not converge, including a diverging pre-batch “n” powerflow, which stamps every row this way). Requires compute_limit_violations=True.
- get_violations_n(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) list[lightsim2grid.lightsim2grid_cpp.LimitViolation]
List of LimitViolation for the pre-batch (“n”) case (no injection change, no contingency) shared by every row. Requires compute_limit_violations=True.
- get_vm_col_of_bus(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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.ScenarioSweepCPP) Annotated[numpy.typing.NDArray[numpy.complex128], '[m, n]']
Get the complex voltage angles at each bus of the powergrid.
Each rows correspond to a time step, each column to a bus.
Warning
This function must be called after
lightsim2grid.timeSerie.TimeSeriesCPP.compute_Vs().- Returns:
Vs – The complex voltage angles at each bus of the powergrid.
- Return type:
numpy.ndarry(matrix)
- property handle_disconnected_grid
Whether to simulate a row whose contingency splits the grid into multiple connected components. When False (default) such a row is skipped (its 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 simulation with the results of a n-powerflow (ie a powerflow with no injection change and no contingency) or not. Default
- invalidate_base_case(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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.
- 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.
- modify_gen_p(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, gen_p: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous']) None
Per-step active generator setpoints, shape (n_simul, n_gen). See the class docstring: locks / checks the number of simulations against any other modify_* / set_contingency_* call already made on this object.
- modify_gen_v(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, gen_v: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous']) None
Per-step generator target voltage magnitude, shape (n_simul, n_gen), in pu (vm_pu), NOT kV. Unlike modify_gen_p/modify_sgen_p/modify_load_p/modify_load_q, this does NOT feed the injection (Sbus) – it only re-seeds |V| at each voltage-regulating generator’s regulated bus before that step’s solve. See modify_gen_p() for the shared row-count-lock behavior.
A bus has ONE voltage magnitude, so a row cannot ask one bus for two. Two cases are therefore refused – such a row is not solved, and reports like any row skipped before the solver (
converged_maskFalse, zero voltages, no gradient) rather than silently taking whichever set-point was written last:several generators regulating the same bus given different targets: give them the same one;
a generator whose regulated bus is also regulated by a voltage-mode SVC or an hvdc converter station, given anything other than THAT element’s target.
The second is a limitation, not just a rule: only generator set-points vary per row. There is no modify_svc_v and no modify_hvdc_v, so a generator sharing its regulated bus with either is effectively fixed for the whole sweep and its gen_v gradient is zero. See the TODO at the top of the changelog.
- modify_load_p(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, load_p: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous']) None
Per-step active load setpoints, shape (n_simul, n_load). See modify_gen_p().
- modify_load_q(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, load_q: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous']) None
Per-step reactive load setpoints, shape (n_simul, n_load). See modify_gen_p().
- modify_sgen_p(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, sgen_p: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous']) None
Per-step active static generator setpoints, shape (n_simul, n_sgen). See modify_gen_p().
- nb_converged(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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.ScenarioSweepCPP) int
Total number of powerflows solved.
- property nb_thread
Number of OS threads used to compute the steps (default
1).With
nb_thread == 1the behaviour is the legacy sequential one. Withnb_thread > 1the steps are split into contiguous ranges, each solved by its own thread (each with its own solver), writing to disjoint rows of the result matrix: the results do NOT depend on the number of threads. Values< 1are clamped to1.Warning
This is only available on
lightsim2grid.injectionSweep.InjectionSweepCPP(and onlightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP), whose computations are independent of one another.On
lightsim2grid.timeSerie.TimeSeriesCPPeach step is initialized with the result of the previous one, so the steps cannot be split over threads without making the results depend on how they were split: setting it to anything but 1 raises aRuntimeError. UseInjectionSweepCPPif you want the very same injections computed in parallel.
- property physical_violation_tol_mva
one noise floor for both halves, MW and MVAr being the same scale.
- Type:
Absolute slack on every comparison compute_physical_violations makes, so that an element resting exactly on its limit is not reported over solver noise
- Type:
a violation needs
value > limit + tol(orvalue < limit - tolfor LOW_Q and HVDC_AC_EMULATION_RELEASE). Defaults to 1e-4. In MVA
- property physical_violation_tol_vm_pu
the PQ -> PV release check reports a flagged machine whose regulated voltage is below (at min_q) or above (at max_q) its target by more than this, in pu, and the standby SVC check a flagged SVC whose regulated voltage is outside its automaton’s thresholds by more than this, and the remote voltage control check a remote controller whose own voltage is outside the realistic range by more than this. Defaults to 1e-4.
- Type:
The same, for the comparisons compute_physical_violations makes on a voltage
- preprocessing_time(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) float
Time spent in pre processing the data (this involves, but is not limited to the computation of the Sbus)
It is given in seconds (
float).
- property redistribute_slack
without limits the converged state is unchanged. Same as LSGrid.redistribute_active_power, row by row.
- Type:
Whether the active power a row loses – the generators its generator contingency disconnects, the elements of an island cut off with handle_disconnected_grid, and the imbalance its own modify_gen_p / modify_sgen_p / modify_load_p create against the grid’s targets – 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
- 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.ScenarioSweepCPP, config: lightsim2grid.lightsim2grid_cpp.AlgoConfig) None
See get_algo_config().
- set_contingency_gens(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, mask: Annotated[numpy.typing.NDArray[numpy.bool], '[m, n]', 'flags.c_contiguous']) None
Per-step generator contingency mask, shape (n_simul, n_gen), dtype bool. True means ‘disconnect this generator for this simulation’.
Unlike the two branch masks this does not edit Ybus – a generator has no admittance. It removes the generator’s active power (and, if it does not regulate voltage, its reactive setpoint) from that step’s injection, re-weights the distributed slack without it, and – when the LAST generator regulating its own bus is taken out – turns that bus from PV to PQ for the step, so its voltage magnitude is solved for instead of held at the setpoint. The lost MW is picked up by the slack; use modify_gen_p to express a redispatch instead.
The PV/PQ relabelling costs no extra symbolic factorization: every bus that can flip is given a voltage-magnitude unknown and a reactive equation once, up front, and each step merely masks the equation of the buses that are still PV. The whole sweep keeps running on one analysis.
It is NOT free, though: those reserved unknowns and equations make the Jacobian bigger for EVERY step, whether or not that step disconnects anything – one extra row and column per bus that can flip. A handful of candidate generators is negligible; masking every generator on the grid grows the Jacobian’s dimension by roughly the number of PV buses, and every factorization and solve pays for it.
Only generators regulating their OWN bus are supported. compute() raises if the mask names a generator that regulates a remote bus, or one whose bus a control group holds (a remote generator, an SVC or an HVDC converter station): remote voltage control is not supported yet.
- set_contingency_lines(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, mask: Annotated[numpy.typing.NDArray[numpy.bool], '[m, n]', 'flags.c_contiguous']) None
Per-step powerline contingency mask, shape (n_simul, n_line), dtype bool. True means ‘deactivate this powerline for this simulation’. See the class docstring: locks / checks the number of simulations, and is a different API from ContingencyAnalysisCPP’s add_n1/add_nk on purpose.
- set_contingency_trafos(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, mask: Annotated[numpy.typing.NDArray[numpy.bool], '[m, n]', 'flags.c_contiguous']) None
Per-step trafo contingency mask, shape (n_simul, n_trafo), dtype bool. See set_contingency_lines().
- set_topo_actions(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, actions: collections.abc.Sequence[ls2g::TopoAction]) None
One topological action (TopoAction, grid2op semantics: set_bus on loads / generators / storage units / line and trafo ends, set_line_status) per row, played by that row on top of its injections and masks; an action changing nothing is a plain row. Locks / checks the number of simulations. Every action is checked against the grid here (element and busbar exist, no contradiction) and a ValueError naming the row is raised, nothing registered, if one is invalid.
A row plays disconnections (as set_contingency_lines / trafos / gens do it), reconnections and moves between busbars: a bus created or merged. One symbolic analysis for the whole sweep: the labelling is built for the union of the buses the rows use, the Ybus entries a row writes are reserved as stored zeros, and a row is value edits (coefficients, injections, PV pinning, masking of the union buses it leaves empty). compute() refuses a row naming an element in both a mask and its action, a slack participant or a remote / group-held controller moved or reactivated, a regulating storage unit moved, a branch with one end open in the base grid put on, keep_jacobian with a generator move or reactivation, and the DC algorithm. compute_physical_violations follows the row (a generator the row places is checked on that bus).
- solve_JT(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP, 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().
- solver_time(self: lightsim2grid.lightsim2grid_cpp.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.ScenarioSweepCPP) 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.scenarioSweep.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.scenarioSweep.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