Time Series
Goal
This class aims to make faster (and easier) the computations of the current flows (measured in Amps) at a certain side of a powerline / transformer when the topology is not modified.
It can be used as:
from lightsim2grid import TimeSerie
import grid2op
from lightsim2grid.lightSimBackend import LightSimBackend
env_name = ...
env = grid2op.make(env_name, backend=LightSimBackend())
time_series = TimeSerie(env)
res_p, res_a, res_v = time_series.get_flows(scenario_id=..., seed=...)
# we have:
# res_p[row_id] will be the active power flows (origin side), on all powerlines corresponding to step "row_id"
# res_a[row_id] will be the current flows, on all powerlines corresponding to step "row_id"
# res_v[row_id] will be the complex voltage, on all bus of the grid at step "row_id"
For now this relies on grid2op, but we could imagine a version of this class that can read
to / from other data sources (for now please use the more basic lightsim2grid.timeSerie.TimeSeriesCPP for such purpose)
Importantly, this method is around 11x faster than simulating “do nothing” (or “one change then nothing”) with grid2op (and lightsim2grid, see section Benchmarks (Time Series) )
Note
A more detailed example is given in the examples\time_serie.py file from the lightsim2grid package.
Warning
Topology and injections
The topology is taken from the initial provided grid and cannot be changed when evaluating a given “time serie”.
Then, the call to time_series.compute_V(scenario_id=…, seed=…) will only read the injections (productions and loads) from grid2op to compute the voltages.
Note
As this class calls a long c++ function, it is possible to use the python Threading module to achieve high efficient parrallelism. An example is provided in the examples\timeseries_with_grid2op_multithreading.py file.
Note
Set time_series.init_from_n_powerflow = True before calling compute_V (setting it
afterwards has no effect) to initialize the first step of the batch with the voltage
solution of the grid’s current (“n”) state instead of a flat start – this is usually
faster. See lightsim2grid.timeSerie.TimeSerie.init_from_n_powerflow().
Independent scenarios: InjectionSweep
TimeSerie initializes each step with the solution of the step before it. That is the right thing to do for a time series – two consecutive instants are close to one another, so the previous solution is an excellent starting point – but it makes the steps a chain: step i cannot be computed before step i-1, and its result depends on it.
If your “steps” are unrelated scenarios rather than consecutive instants (a sample of load /
generation patterns, a set of “what if” injections, a Monte-Carlo draw…), use
lightsim2grid.injectionSweep.InjectionSweep instead. It computes exactly the same
thing, with exactly the same interface, but starts every step from the same voltage – the
one you provide, or the “n” powerflow result if init_from_n_powerflow is set – just like
lightsim2grid.contingencyAnalysis.ContingencyAnalysis does for each contingency.
Two things follow:
the results do not depend on the order the steps are given in;
the batch can be spread over several OS threads, in c++, with sweep.nb_thread = … (the results do not depend on that either). TimeSerie cannot: splitting a chain into per-thread ranges would break it, so setting nb_thread to anything but 1 raises there.
import grid2op
from lightsim2grid import InjectionSweep, LightSimBackend
env_name = ...
env = grid2op.make(env_name, backend=LightSimBackend())
sweep = InjectionSweep(env)
sweep.nb_thread = 4
res_p, res_a, res_v = sweep.get_flows(scenario_id=..., seed=...)
Note
On a genuine time series TimeSerie usually needs fewer solver iterations, precisely because each step starts from its neighbour’s solution. InjectionSweep trades that away for independence – and buys back much more than it costs as soon as you use several threads.
Note
TimeSerie / InjectionSweep also expose a newer, setter-based API (modify_gen_p / modify_sgen_p / modify_load_p / modify_load_q / modify_gen_v + compute()) as an alternative to the single bundled compute_V_from_inj call – see Scenario Sweep (which uses that same API, plus a per-step contingency) for details on how it behaves when an axis is never set. modify_gen_v (per-step generator target voltage magnitude, in pu – vm_pu, NOT kV) is different from the other four: it does not feed the injection (Sbus) at all, it only re-seeds |V| at each voltage-regulating generator’s regulated bus before that step’s solve.
Benchmarks (Time Series)
Here are some benchmarks made with:
date: 2026-09-21 11:22 CEST
system: Linux 6.8.0-60-generic
OS: ubuntu 22.04
processor: 13th Gen Intel(R) Core(TM) i7-13700H
python version: 3.12.8.final.0 (64 bit)
numpy version: 2.4.6
pandas version: 2.3.3
pandapower version: 3.5.4
grid2op version: 1.12.5.dev0
lightsim2grid version: 1.1.0
lightsim2grid extra information:
klu_solver_available: True
nicslu_solver_available: True
cktso_solver_available: True
compiled_march_native: True
compiled_o3_optim: True
This benchmark is available by running, from the root of the lightsim2grid repository:
cd benchmarks
python3 time_serie.py
For this setting the outputs are:
For environment: l2rpn_neurips_2020_track2
Total time spent in "computer" to solve everything: 0.02s (27236 pf / s), 0.04 ms / pf)
- time to pre process the injections: 0.00s
- time to perform powerflows: 0.02s (28453 pf / s, 0.04 ms / pf)
In addition, it took 0.00 s to retrieve the current from the complex voltages (in total 25487.2 pf /s, 0.04 ms / pf)
Comparison with raw grid2op timings
It took grid2op (with lightsim2grid): 0.31s to perform the same computation
This is a 13.9 speed up from TimeSerie over raw grid2op (lightsim2grid)
It took grid2op (with pandapower): 7.76s to perform the same computation
This is a 343.1 speed up from TimeSerie over raw grid2op (pandapower)
In this case then, the `TimeSerie` module is 14 times faster than raw grid2op (lightsim2grid) and 343 times faster than raw grid2op (pandapower)
All results match !
Detailed usage
Classes:
alias of |
|
|
This helper class, that only works with grid2op when using a LightSimBackend allows to compute the flows (at the origin side of the powerline / transformers). |
Allows the computation of time series, that is, the same grid topology is used while the active / reactive power injected at each buse vary. |
- lightsim2grid.timeSerie.Computers
alias of
TimeSeriesCPP
- class lightsim2grid.timeSerie.TimeSerie(grid2op_env)[source]
This helper class, that only works with grid2op when using a LightSimBackend allows to compute the flows (at the origin side of the powerline / transformers). It is roughly equivalent to the grid2op code:
import grid2op import numpy as np from grid2op.Parameters import Parameters from lightsim2grid import LightSimBackend env_name = ... param = Parameters() param.NO_OVERFLOW_DISCONNECTION = True env = grid2op.make(env_name, param=param, backend=LightSimBackend()) done = False obs = env.reset() nb_step = obs.max_step Vs = np.zeros((nb_step, 2 * env.n_sub), dtype=complex) As = np.zeros((nb_step, env.n_line), dtype=float) while not done: obs, reward, done, info = env.step(env.action_space()) Vs[i, :env.n_sub] = env.backend.V As[i] = obs.a_or
Compare to the previous code, it avoid all grid2op code and can be more than 15 times faster (on the case 118).
It also allows to use python threading module, as the c++ computation can be done in different python threads (the GIL is not locked during the c++ computation).
Examples
It can be used as:
from lightsim2grid import TimeSerie import grid2op from lightsim2grid import LightSimBackend env_name = ... env = grid2op.make(env_name, param=param, backend=LightSimBackend()) time_series = TimeSerie(env) res_p, res_a, res_v = time_series.get_flows(scenario_id=..., seed=...)
Methods:
clear()Clear everything, as if nothing has been computed
close()permanently close the object
compute([v_init, max_iter, tol, ignore_errors])Run the batch using whatever was set by
modify_gen_p()/modify_sgen_p()/modify_load_p()/modify_load_q()(the new setter-based API -- an alternative to the single bundledcompute_V_from_inj()call).This function returns the current flows (in Amps, A) at the origin (for powerline) / high voltage (for transformer) side
This function returns the active power flows (in MW) at the origin (for powerline) / high voltage (for transformer) side
compute_V([scenario_id, seed, v_init, ...])This function allows to retrieve the complex voltage at each bus of the grid for each step.
compute_V_from_inj(prod_p, load_p, load_q[, ...])This function allows to compute the voltages, at each bus given a list of productions and loads.
get_flows([scenario_id, seed, v_init, ...])Retrieve the flows for each step simulated.
get_injections([scenario_id, seed])This function allows to retrieve the injection of the given scenario, for the given seed from the grid2op internal environment.
Per step (same order as the
modify_*inputs): the list ofLimitViolationof the physical limits that step's solution leaves.Same as
get_physical_violations(), for the base ("n") case every step is solved from (the grid's own state, no injection change).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).Attributes:
Whether every converged step reports the PHYSICAL limits its solution leaves -- the ones whose violation means the step is not a state the grid can reach at all (
ViolationCategory.PHYSICAL).Whether to initialize the complex voltages of the first step of the batch with the results of a "n" powerflow (a powerflow at the current state of the grid) instead of a flat start.
Whether the buses a voltage-control group regulates (regulated from elsewhere, by an SVC, or by several controllers at least one of which is remote) keep the magnitude of the starting voltage instead of being set to their set-point before each solve.
Absolute slack on every comparison
compute_physical_violationsmakes, so that an element resting exactly on its limit is not reported over solver noise: a violation needsvalue > limit + tol(orvalue < limit - tolforLOW_QandHVDC_AC_EMULATION_RELEASE).The same as
physical_violation_tol_mva, in pu, for the comparisonscompute_physical_violationsmakes on a voltage: the PQ -> PV release check reports a flagged PQ generator (LSGrid.set_gen_can_be_pv) whose regulated bus is below (atmin_q) or above (atmax_q) its target by more than this (LOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Q), and the standby SVC check a flagged idle SVC (LSGrid.set_svc_standby) whose regulated bus is outside its automaton's thresholds by more than this (LOW_VOLTAGE_SVC_STANDBY/HIGH_VOLTAGE_SVC_STANDBY), and the remote voltage control check a remote controller whose own bus is outside the realistic range by more than this (LOW_VOLTAGE_REMOTE_CONTROL/HIGH_VOLTAGE_REMOTE_CONTROL).- compute(v_init=None, max_iter=None, tol=None, ignore_errors=False)[source]
Run the batch using whatever was set by
modify_gen_p()/modify_sgen_p()/modify_load_p()/modify_load_q()(the new setter-based API – an alternative to the single bundledcompute_V_from_inj()call).max_iter/toldefault to the backend’s own values when not given.
- compute_A()[source]
This function returns the current flows (in Amps, A) at the origin (for powerline) / high voltage (for transformer) side
It does not recompute the voltages at each buses, it uses the information get from compute_V and This is why you must call compute_V(…) first !
- compute_P()[source]
This function returns the active power flows (in MW) at the origin (for powerline) / high voltage (for transformer) side
It does not recompute the voltages at each buses, it uses the information get from compute_V and This is why you must call compute_V(…) first !
- compute_V(scenario_id=None, seed=None, v_init=None, ignore_errors=False)[source]
This function allows to retrieve the complex voltage at each bus of the grid for each step.
Warning
Topology fixed = no maintenance, no attacks, etc.
As the topology is fixed, this class does not allow to simulate the effect of maintenance or attacks !
- compute_V_from_inj(prod_p, load_p, load_q, v_init=None, ignore_errors=False)[source]
This function allows to compute the voltages, at each bus given a list of productions and loads.
We do not recommend to use it directly, as the order of the load or generators might vary !
- property compute_physical_violations
Whether every converged step reports the PHYSICAL limits its solution leaves – the ones whose violation means the step is not a state the grid can reach at all (
ViolationCategory.PHYSICAL). Default:False. Seeget_physical_violations().Each check is a condition a PowSyBl OpenLoadFlow outer loop acts on, and none enforced here (nothing is switched PV -> PQ or back, nothing is clamped, no machine leaves the slack distribution, no step is re-solved):
the reactive capability of every bus whose voltage is held by machines (
LOW_Q/HIGH_Qon theBUS): did it need more reactive power than the sum of what its voltage-regulating generators, storage units, hvdc converter stations and voltage-mode SVCs can produce? A machine has no reactive setpoint – its output is solved for and never clamped – so a step 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’sReactiveLimits.the release of every PQ generator flagged as pinned at a reactive limit (
LOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Qon theGENERATOR, seelightsim2grid.network.LSGrid.set_gen_can_be_pv()): does the bus it would regulate sit below its target while the machine absorbs all it can (or above it while it produces all it can)? The other direction, PQ -> PV, of the sameReactiveLimitsloop.valueandlimitin kV, compared withphysical_violation_tol_vm_pu; a step that varies that generator’s target (modify_gen_v) is checked against its own target. A grid with no flagged generator reports nothing here.the switch on of every idle SVC flagged as carrying a standby automaton (
LOW_VOLTAGE_SVC_STANDBY/HIGH_VOLTAGE_SVC_STANDBYon theSVC, seelightsim2grid.network.LSGrid.set_svc_standby()): does the bus it regulates sit outside the automaton’s voltage thresholds? OpenLoadFlow’sMonitoringVoltageOuterLoop.valueandlimitin kV, compared withphysical_violation_tol_vm_pu. A grid with no flagged SVC reports nothing here.the own bus of every generator regulating a remote bus, when a realistic range is set (
LOW_VOLTAGE_REMOTE_CONTROL/HIGH_VOLTAGE_REMOTE_CONTROLon theGENERATOR, seelightsim2grid.network.LSGrid.set_remote_voltage_control_vm_range()): does holding the remote target take it outside that range? OpenLoadFlow’sReactiveLimits, in its robust remote voltage control mode.valueandlimitin kV, compared withphysical_violation_tol_vm_pu.the active power of every angle-droop (“AC emulation”) hvdc line still in the linear regime (
HIGH_Pon theHVDC): didp0 + k.(theta1 - theta2)leavepmax_1to2_mw/pmax_2to1_mw?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_RELEASEon theHVDC, seelightsim2grid.network.LSGrid.set_hvdc_ac_emulation_frozen()): would its droop ask for less than that limit? The other half of the sameHvdcAcEmulationLimitsloop.sidethe direction it is frozen in,valueandlimitin MW.the active power of every generator and every storage unit carrying the distributed slack (
LOW_P/HIGH_Pon theGENERATOR/STORAGE): the slack is solved inside the Jacobian by fixed participation factors that know nothing about limits, sotarget_p + its share of the imbalancecan land beyondmin_p_mw/max_p_mw. Per machine, unlike the reactive check: the active split is not a convention, it is the participation factors the caller chose. Needs those limits, which are optional (lightsim2grid.network.LSGrid.set_gen_p_limits()/lightsim2grid.network.LSGrid.set_storage_p_limits()); a grid without them reports nothing here. A storage unit’s are read – and its violation reported – in the generator convention, unlike itstarget_p_mw. OpenLoadFlow’sDistributedSlack.
The hvdc and active-power checks need only the bus angles and the slack the step distributed, so they work in DC too; the reactive one and the voltage ones need an AC algorithm that publishes its per-bus mismatch (every built-in AC algorithm does) and
computeraises for one that does not. A DC batch reports the active-power checks alone – 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.Changing this flag invalidates any previously-computed results, but not the injections already given to
modify_*.
- get_flows(scenario_id=None, seed=None, v_init=None, ignore_errors=False)[source]
Retrieve the flows for each step simulated.
Each row of the resulting flow matrix will correspond to a step.
Examples
import grid2op from lightsim2grid import TimeSerie from lightsim2grid import LightSimBackend env_name = ... env = grid2op.make(env_name, backend=LightSimBackend()) timeserie = TimeSerie(env) res_p, res_a, res_v = timeserie.get_flows(scenario_id, seed, v_init, ignore_errors) # in this results, then # res_a[row_id] will be the flows, on all powerline corresponding to the `row_id` contingency. # you can retrieve it with `security_analysis.contingency_order[row_id]`
- get_injections(scenario_id=None, seed=None)[source]
This function allows to retrieve the injection of the given scenario, for the given seed from the grid2op internal environment.
- get_physical_violations()[source]
Per step (same order as the
modify_*inputs): the list ofLimitViolationof the physical limits that step’s solution leaves. Every entry hascategory == ViolationCategory.PHYSICALand one of these shapes:element_typeBUS,violation_typeLOW_Q/HIGH_Q,element_idthe grid bus id,valuethe reactive power the machines holding that bus had to produce (MVAr) andlimittheir summed capability;element_typeGENERATOR,violation_typeLOW_VOLTAGE_AT_MIN_Q/HIGH_VOLTAGE_AT_MAX_Q,element_idthe generator id,valuethe voltage of the bus that flagged PQ machine would regulate andlimitits target, in kV;element_typeSVC,violation_typeLOW_VOLTAGE_SVC_STANDBY/HIGH_VOLTAGE_SVC_STANDBY,element_idthe svc id,valuethe voltage of the bus that flagged idle standby SVC regulates andlimitthe automaton’s threshold, in kV;element_typeGENERATOR,violation_typeLOW_VOLTAGE_REMOTE_CONTROL/HIGH_VOLTAGE_REMOTE_CONTROL,element_idthe generator id,valuethe voltage of that remote controller’s own bus andlimitthe realistic bound, in kV;element_typeHVDC,violation_typeHIGH_P,element_idthe hvdc line id,sidethe direction (1 for 1 -> 2),valuethe active power leaving that side (MW, positive) andlimitthat direction’spmax;element_typeHVDC,violation_typeHVDC_AC_EMULATION_RELEASE,element_idthe hvdc line id,sidethe direction it is frozen in,valuethe flow its droop asks for in that direction andlimitthe limit it is frozen at, in MW;element_typeGENERATOR/STORAGE,violation_typeLOW_P/HIGH_P,valuethe machine’s converged active power (MW, generator convention) andlimititsmin_p_mw/max_p_mw.
A step that did not converge has an empty entry, not a sentinel – use
self.computer.converged_mask()to tell that from “converged, no violation”. Requirescompute_physical_violationsto beTrue(raises otherwise).
- get_physical_violations_n()[source]
Same as
get_physical_violations(), for the base (“n”) case every step is solved from (the grid’s own state, no injection change). Empty if that solve did not converge. Requirescompute_physical_violationsto beTrue.
- property init_from_n_powerflow
Whether to initialize the complex voltages of the first step of the batch with the results of a “n” powerflow (a powerflow at the current state of the grid) instead of a flat start. Default:
False. Must be set before the computation actually runs (eg beforecompute_Vis called); it has no effect on a powerflow that has already been solved.
- property keep_vinit_at_group_controlled_buses
Whether the buses a voltage-control group regulates (regulated from elsewhere, by an SVC, or by several controllers at least one of which is remote) keep the magnitude of the starting voltage instead of being set to their set-point before each solve. The solution is the same; with
MaxVoltageChangedamping it can be reached in far fewer iterations. Defaults to the grid’s own setting (seelightsim2grid.lightsim2grid_cpp.LSGrid.set_keep_vinit_at_group_controlled_buses()),Falseunless set there.
- modify_gen_p(gen_p)[source]
Per-step active generator setpoints, shape
(n_simul, n_gen). Part of the new setter-based API (seecompute()); an alternative tocompute_V_from_inj(), not required if you use that call instead.
- 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. Seemodify_gen_p().
- modify_load_p(load_p)[source]
Per-step active load setpoints, shape
(n_simul, n_load). Seemodify_gen_p().
- modify_load_q(load_q)[source]
Per-step reactive load setpoints, shape
(n_simul, n_load). Seemodify_gen_p().
- modify_sgen_p(sgen_p)[source]
Per-step active static generator setpoints, shape
(n_simul, n_sgen). Seemodify_gen_p().
- property physical_violation_tol_mva
Absolute slack on every comparison
compute_physical_violationsmakes, so that an element resting exactly on its limit is not reported over solver noise: a violation needsvalue > limit + tol(orvalue < limit - tolforLOW_QandHVDC_AC_EMULATION_RELEASE). Default:1e-4. In MVA – one noise floor for both halves, MW and MVAr being the same scale. 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.
- class lightsim2grid.timeSerie.TimeSeriesCPP
Allows the computation of time series, that is, the same grid topology is used while the active / reactive power injected at each buse vary. The grid topology is fixed, the injections vary.
This is a “raw” c++ class, for an easier to use interface, please refer to the python documentation of the
lightsim2grid.timeSerie.TimeSerieclass.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_Vs(self, arg0, arg1, arg2, arg3, ...)Compute the voltages (at each bus of the grid model) for some time series of injections (productions, loads, storage units, etc.)
compute_branch_results(self)Active and reactive power at both ends of every branch, for every row of the last compute(), as an array of shape (nb_rows, nb_branch, 4):
[..., 0]and[..., 1]are P (MW) and Q (MVAr) entering the branch at side 1,[..., 2]and[..., 3]the same at side 2.compute_flows(self)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_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_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_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_sbuses(self)Get the complex power injected at each (solver id) bus of the powergrid.
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_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().
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.
states the grid does reach and should not sit in).
Whether to initialize the complex voltages of the first step with the results of a "n" powerflow (ie a powerflow run at the start of the computation, on the injections of the grid model) or not.
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.
True).- adjoint_memory_bytes(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP, 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.TimeSeriesCPP, 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.TimeSeriesCPP, arg0: lightsim2grid.lightsim2grid_cpp.AlgorithmType) -> None
DEPRECATED: use ‘change_algorithm’ instead
change_solver(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP, arg0: str) -> None
DEPRECATED: use ‘change_algorithm’ instead
- clear(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) None
Clear the solver and to as if the class never performed any powerflow.
- close(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) None
Clear the solver and to as if the class never performed any powerflow.
- compute(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP, 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_Vs(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP, arg0: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous'], arg1: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous'], arg2: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous'], arg3: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous'], arg4: Annotated[numpy.typing.NDArray[numpy.complex128], '[m, 1]'], arg5: SupportsInt | SupportsIndex, arg6: SupportsFloat | SupportsIndex) int
Compute the voltages (at each bus of the grid model) for some time series of injections (productions, loads, storage units, etc.)
Note
This function must be called before
lightsim2grid.timeSerie.TimeSeriesCPP.compute_flows()andlightsim2grid.timeSerie.TimeSeriesCPP.get_flows(),lightsim2grid.timeSerie.TimeSeriesCPP.get_voltages()orlightsim2grid.timeSerie.TimeSeriesCPP.get_sbuses().Note
During this computation, the GIL is released, allowing easier parrallel computation
- gen_p:
numy.ndarray, float Active generation for each generators. Its counts as many column as the number of generators on the grid and as many rows as the number of steps to compute.
- sgen_p:
numy.ndarray, float Active generation for each static generator. Its counts as many column as the number of static generators on the grid and as many rows as the number of steps to compute.
- load_p:
numy.ndarray, float Active consumption for each loads. Its counts as many column as the number of loads on the grid and as many rows as the number of steps to compute.
- load_q:
numy.ndarray, float Reactive consumption for each loads. Its counts as many column as the number of loads on the grid and as many rows as the number of steps to compute.
- Vinit:
numy.ndarray, complex First voltage at each bus of the grid model (including the disconnected buses)
- max_iter:
int Total number of iteration (>0 integer)
- tol:
float Solver tolerance (> 0. float)
- status:
int The status of the computation. 1 means “success”: all powerflows were computed sucessfully, 0 means there were some errors and that the computation stopped after a certain number of steps.
DEPRECATED: prefer modify_gen_p / modify_sgen_p / modify_load_p / modify_load_q + compute() instead.
- gen_p:
- compute_branch_results(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) 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.TimeSeriesCPP) 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_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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP, 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_algo_config(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) numpy.typing.NDArray[numpy.float64]
The array of the last compute_branch_results(), without computing it again.
- get_flows(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']
Same as get_p_row_of_bus for the REACTIVE power mismatch equation.
- get_row_nb_iter(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) 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.TimeSeriesCPP) 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_sbuses(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) Annotated[numpy.typing.NDArray[numpy.complex128], '[m, n]', 'flags.c_contiguous']
Get the complex power injected at each (solver id) bus of the powergrid. Results are given in pair unit. We do not recommend to use it as it uses the solver id and NOT the powergrid bus id (you can refer to
lightsim2grid.network.LSGrid.id_me_to_ac_solver()andlightsim2grid.network.LSGrid.id_ac_solver_to_me()for more information)Each rows correspond to a time step, each column to a bus (bus are identified by their solver id !)
Warning
This function must be called after
lightsim2grid.timeSerie.TimeSeriesCPP.compute_Vs().- Returns:
Sbuses – The complex power injected at each bus (pair unit, load sign convention)
- Return type:
numpy.ndarry(matrix)
- get_status(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) 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.TimeSeriesCPP) 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_vm_col_of_bus(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) 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.TimeSeriesCPP) 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 init_from_n_powerflow
Whether to initialize the complex voltages of the first step with the results of a “n” powerflow (ie a powerflow run at the start of the computation, on the injections of the grid model) or not.
Default:
False, meaning the first step is initialized with the input vector given tolightsim2grid.timeSerie.TimeSeriesCPP.compute_Vs(). Every other step is initialized with the result of the step before it either way.
- invalidate_base_case(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP) 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.TimeSeriesCPP, 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.TimeSeriesCPP, 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.TimeSeriesCPP, 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.TimeSeriesCPP, 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.TimeSeriesCPP, 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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 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.TimeSeriesCPP, config: lightsim2grid.lightsim2grid_cpp.AlgoConfig) None
See get_algo_config().
- solve_JT(self: lightsim2grid.lightsim2grid_cpp.TimeSeriesCPP, 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) 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.TimeSeriesCPP) float
Total time spent in solving the powerflows, pre processing the data, post processing them, initializing everything etc.
It is given in seconds (
float).
Classes:
|
Same computation as |
Allows the computation of many powerflows on the same grid topology while the active / reactive power injected at each bus vary: exactly the same inputs, the same results and the same interface as |
- class lightsim2grid.injectionSweep.InjectionSweep(grid2op_env)[source]
Same computation as
lightsim2grid.timeSerie.TimeSerie– a fixed grid topology, one powerflow per set of injections – but every powerflow starts from the same voltage instead of from the result of the previous one.That makes the steps independent of one another: the result of a step does not depend on the steps computed before it, nor on the order in which they were given. Use this class when the “steps” are unrelated scenarios rather than consecutive instants of a time series. Two practical consequences:
the computation can be spread over several OS threads, see
nb_threadbelow (lightsim2grid.timeSerie.TimeSeriecannot: splitting a chained computation would make its results depend on how it was split);a step that is far from its neighbours does not inherit a bad starting point from them – but a step that IS close to its neighbours no longer benefits from their solution, so a genuine time series usually converges in fewer iterations with
lightsim2grid.timeSerie.TimeSerie.
Examples
It is used exactly like
lightsim2grid.timeSerie.TimeSerie:import grid2op from lightsim2grid import InjectionSweep from lightsim2grid import LightSimBackend env_name = ... env = grid2op.make(env_name, backend=LightSimBackend()) sweep = InjectionSweep(env) sweep.nb_thread = 4 # optional, the results do not depend on it res_p, res_a, res_v = sweep.get_flows(scenario_id=..., seed=...)
Attributes:
Whether to initialize the complex voltages of each step of the batch with the results of a "n" powerflow (a powerflow at the current state of the grid) instead of the vector given to
compute_V.1).- property init_from_n_powerflow
Whether to initialize the complex voltages of each step of the batch with the results of a “n” powerflow (a powerflow at the current state of the grid) instead of the vector given to
compute_V. Default:False.Unlike
lightsim2grid.timeSerie.TimeSerie.init_from_n_powerflow, this applies to every step and not only the first one – here every step starts from that same voltage. Must be set before the computation actually runs.
- property nb_thread
1).The steps are split into contiguous ranges, each solved by its own thread 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.Must be set before the computation actually runs (eg before
compute_Vis called); it has no effect on a batch that has already been computed.- Type:
Number of OS threads used to compute the steps (default
- class lightsim2grid.injectionSweep.InjectionSweepCPP
Allows the computation of many powerflows on the same grid topology while the active / reactive power injected at each bus vary: exactly the same inputs, the same results and the same interface as
lightsim2grid.timeSerie.TimeSeriesCPP.The difference is how each computation is initialized.
TimeSeriesCPPinitializes a step with the solution of the step before it (they are consecutive instants of a time series, so they are expected to be close to one another).InjectionSweepCPPinitializes every step with the very same voltage - the one you provide, or the result of the “n” powerflow ifinit_from_n_powerflowis set - exactly likelightsim2grid.contingencyAnalysis.ContingencyAnalysisCPPdoes for each of its contingencies.Use it when the “steps” are independent scenarios rather than consecutive instants. The result of a step then does not depend on the steps computed before it, nor on the order in which they are given, and the computation can be spread over several threads (see
nb_thread, whichTimeSeriesCPPcannot support).This is a “raw” c++ class, for an easier to use interface, please refer to the python documentation of the
lightsim2grid.injectionSweep.InjectionSweepclass.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_Vs(self, arg0, arg1, arg2, arg3, ...)Compute the voltages (at each bus of the grid model) for some time series of injections (productions, loads, storage units, etc.)
compute_branch_results(self)Active and reactive power at both ends of every branch, for every row of the last compute(), as an array of shape (nb_rows, nb_branch, 4):
[..., 0]and[..., 1]are P (MW) and Q (MVAr) entering the branch at side 1,[..., 2]and[..., 3]the same at side 2.compute_flows(self)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_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_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_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_sbuses(self)Get the complex power injected at each (solver id) bus of the powergrid.
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_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().
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.
states the grid does reach and should not sit in).
Whether to initialize the complex voltages of each step with the results of a "n" powerflow (ie a powerflow run at the start of the computation, on the injections of the grid model) or not.
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.
True).- adjoint_memory_bytes(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP, 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.InjectionSweepCPP, 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.InjectionSweepCPP, arg0: lightsim2grid.lightsim2grid_cpp.AlgorithmType) -> None
DEPRECATED: use ‘change_algorithm’ instead
change_solver(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP, arg0: str) -> None
DEPRECATED: use ‘change_algorithm’ instead
- clear(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) None
Clear the solver and to as if the class never performed any powerflow.
- close(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) None
Clear the solver and to as if the class never performed any powerflow.
- compute(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP, 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_Vs(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP, arg0: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous'], arg1: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous'], arg2: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous'], arg3: Annotated[numpy.typing.NDArray[numpy.float64], '[m, n]', 'flags.c_contiguous'], arg4: Annotated[numpy.typing.NDArray[numpy.complex128], '[m, 1]'], arg5: SupportsInt | SupportsIndex, arg6: SupportsFloat | SupportsIndex) int
Compute the voltages (at each bus of the grid model) for some time series of injections (productions, loads, storage units, etc.)
Note
This function must be called before
lightsim2grid.timeSerie.TimeSeriesCPP.compute_flows()andlightsim2grid.timeSerie.TimeSeriesCPP.get_flows(),lightsim2grid.timeSerie.TimeSeriesCPP.get_voltages()orlightsim2grid.timeSerie.TimeSeriesCPP.get_sbuses().Note
During this computation, the GIL is released, allowing easier parrallel computation
- gen_p:
numy.ndarray, float Active generation for each generators. Its counts as many column as the number of generators on the grid and as many rows as the number of steps to compute.
- sgen_p:
numy.ndarray, float Active generation for each static generator. Its counts as many column as the number of static generators on the grid and as many rows as the number of steps to compute.
- load_p:
numy.ndarray, float Active consumption for each loads. Its counts as many column as the number of loads on the grid and as many rows as the number of steps to compute.
- load_q:
numy.ndarray, float Reactive consumption for each loads. Its counts as many column as the number of loads on the grid and as many rows as the number of steps to compute.
- Vinit:
numy.ndarray, complex First voltage at each bus of the grid model (including the disconnected buses)
- max_iter:
int Total number of iteration (>0 integer)
- tol:
float Solver tolerance (> 0. float)
- status:
int The status of the computation. 1 means “success”: all powerflows were computed sucessfully, 0 means there were some errors and that the computation stopped after a certain number of steps.
DEPRECATED: prefer modify_gen_p / modify_sgen_p / modify_load_p / modify_load_q + compute() instead.
- gen_p:
- compute_branch_results(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) 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.InjectionSweepCPP) 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_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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP, 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_algo_config(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) numpy.typing.NDArray[numpy.float64]
The array of the last compute_branch_results(), without computing it again.
- get_flows(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) Annotated[numpy.typing.NDArray[numpy.int32], '[m, 1]']
Same as get_p_row_of_bus for the REACTIVE power mismatch equation.
- get_row_nb_iter(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) 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.InjectionSweepCPP) 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_sbuses(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) Annotated[numpy.typing.NDArray[numpy.complex128], '[m, n]', 'flags.c_contiguous']
Get the complex power injected at each (solver id) bus of the powergrid. Results are given in pair unit. We do not recommend to use it as it uses the solver id and NOT the powergrid bus id (you can refer to
lightsim2grid.network.LSGrid.id_me_to_ac_solver()andlightsim2grid.network.LSGrid.id_ac_solver_to_me()for more information)Each rows correspond to a time step, each column to a bus (bus are identified by their solver id !)
Warning
This function must be called after
lightsim2grid.timeSerie.TimeSeriesCPP.compute_Vs().- Returns:
Sbuses – The complex power injected at each bus (pair unit, load sign convention)
- Return type:
numpy.ndarry(matrix)
- get_status(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) 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.InjectionSweepCPP) 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_vm_col_of_bus(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) 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.InjectionSweepCPP) 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 init_from_n_powerflow
Whether to initialize the complex voltages of each step with the results of a “n” powerflow (ie a powerflow run at the start of the computation, on the injections of the grid model) or not.
Default:
False, meaning every step is initialized with the input vector given tolightsim2grid.injectionSweep.InjectionSweepCPP.compute_Vs().
- invalidate_base_case(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP) 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.InjectionSweepCPP, 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.InjectionSweepCPP, 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.InjectionSweepCPP, 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.InjectionSweepCPP, 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.InjectionSweepCPP, 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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 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.InjectionSweepCPP, config: lightsim2grid.lightsim2grid_cpp.AlgoConfig) None
See get_algo_config().
- solve_JT(self: lightsim2grid.lightsim2grid_cpp.InjectionSweepCPP, 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) 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.InjectionSweepCPP) float
Total time spent in solving the powerflows, pre processing the data, post processing them, initializing everything etc.
It is given in seconds (
float).