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:

Computers

alias of TimeSeriesCPP

TimeSerie(grid2op_env)

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).

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.

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 bundled compute_V_from_inj() call).

compute_A()

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

compute_P()

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.

get_physical_violations()

Per step (same order as the modify_* inputs): the list of LimitViolation of the physical limits that step's solution leaves.

get_physical_violations_n()

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:

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).

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.

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.

physical_violation_tol_mva

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

physical_violation_tol_vm_pu

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

clear()[source]

Clear everything, as if nothing has been computed

close()[source]

permanently close the object

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 bundled compute_V_from_inj() call). max_iter / tol default 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. See get_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_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 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’s ReactiveLimits.

  • the release of every PQ generator flagged as pinned at a reactive limit (LOW_VOLTAGE_AT_MIN_Q / HIGH_VOLTAGE_AT_MAX_Q on the GENERATOR, see lightsim2grid.network.LSGrid.set_gen_can_be_pv()): does the bus it would regulate sit below its target while the machine absorbs all it can (or above it while it produces all it can)? The other direction, PQ -> PV, of the same ReactiveLimits loop. value and limit in kV, compared with physical_violation_tol_vm_pu; a 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_STANDBY on the SVC, see lightsim2grid.network.LSGrid.set_svc_standby()): does the bus it regulates sit outside the automaton’s voltage thresholds? OpenLoadFlow’s MonitoringVoltageOuterLoop. value and limit in kV, compared with physical_violation_tol_vm_pu. A grid with no flagged SVC reports nothing here.

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

  • the active power of every angle-droop (“AC emulation”) hvdc line still in the linear regime (HIGH_P on the HVDC): did p0 + k.(theta1 - theta2) leave pmax_1to2_mw / pmax_2to1_mw? status_droop is an input of the solve, so nothing saturates the droop on its own. OpenLoadFlow’s HvdcAcEmulationLimits.

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

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

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 compute raises 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 of LimitViolation of the physical limits that step’s solution leaves. Every entry has category == ViolationCategory.PHYSICAL and one of these shapes:

  • element_type BUS, violation_type LOW_Q / HIGH_Q, element_id the grid bus id, value the reactive power the machines holding that bus had to produce (MVAr) and limit their summed capability;

  • element_type GENERATOR, 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, 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, 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, violation_type HIGH_P, element_id the hvdc line id, side the direction (1 for 1 -> 2), value the active power leaving that side (MW, positive) and limit that direction’s pmax;

  • element_type HVDC, 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;

  • element_type GENERATOR / STORAGE, violation_type LOW_P / HIGH_P, value the machine’s converged active power (MW, generator convention) and limit its min_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”. Requires compute_physical_violations to be True (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. Requires compute_physical_violations to be True.

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 before compute_V is 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 MaxVoltageChange damping it can be reached in far fewer iterations. Defaults to the grid’s own setting (see lightsim2grid.lightsim2grid_cpp.LSGrid.set_keep_vinit_at_group_controlled_buses()), False unless set there.

modify_gen_p(gen_p)[source]

Per-step active generator setpoints, shape (n_simul, n_gen). Part of the new setter-based API (see compute()); an alternative to compute_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. Unlike modify_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. See modify_gen_p().

modify_load_p(load_p)[source]

Per-step active load setpoints, shape (n_simul, n_load). See modify_gen_p().

modify_load_q(load_q)[source]

Per-step reactive load setpoints, shape (n_simul, n_load). See modify_gen_p().

modify_sgen_p(sgen_p)[source]

Per-step active static generator setpoints, shape (n_simul, n_sgen). See modify_gen_p().

property physical_violation_tol_mva

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

property physical_violation_tol_vm_pu

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

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.TimeSerie class.

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

available_algorithm_names(self)

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

available_default_algorithms(self)

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

base_case_was_reused(self)

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

change_algorithm(*args, **kwargs)

Overloaded function.

change_solver(*args, **kwargs)

Overloaded function.

clear(self)

Clear the 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_v gradient, (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_v carries: 1 where it is the only regulator of that bus (or of that voltage-control group), 1/n where n of them share it, 0 where it carries none (-1 in both get_gen_v_target_bus and get_gen_v_vc_row).

get_gen_v_target_bus(self)

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

get_gen_v_vc_row(self)

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

get_linear_solver_stats(self)

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

get_linear_solver_stats_per_algo(self)

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

get_p_row_of_bus(self)

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

get_physical_violations(self)

Per row: the list of LimitViolation of the physical limits that row's solution leaves.

get_physical_violations_n(self)

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:

compute_gen_results

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

compute_physical_violations

states the grid does reach and should not sit in).

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.

keep_jacobian

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

keep_vinit_at_group_controlled_buses

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

nb_thread

Number of OS threads used to compute the steps (default 1).

physical_violation_tol_mva

one noise floor for both halves, MW and MVAr being the same scale.

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.

reuse_base_case

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.

  1. 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.AlgorithmType for a list of the available algorithms (NB: some algorithms might not be available on all platform)

    Note

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

    Examples

    from lightsim2grid.algorithm import AlgorithmType
    # init the grid model
    from lightsim2grid.network import init_from_pandapower
    pp_net = ...  # any pandapower grid
    lightsim_grid_model = init_from_pandapower(pp_net)  # some warnings might be issued as well as some warnings
    
    # change the algorithm used for the powerflow
    # to use internally a Newton Raphson algorithm with the Eigen sparse LU linear solver
    lightsim_grid_model.change_algorithm(AlgorithmType.NR_SparseLU)
    
  2. change_algorithm(self: lightsim2grid.lightsim2grid_cpp.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 by lightsim2grid.algorithm.AlgorithmType instead.

change_solver(*args, **kwargs)

Overloaded function.

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

DEPRECATED: use ‘change_algorithm’ instead

  1. 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

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.

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 ReactiveLimits loop. 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) leave pmax_1to2_mw / pmax_2to1_mw? status_droop is an INPUT of the solve, so nothing saturates the droop on its own. OpenLoadFlow’s HvdcAcEmulationLimits.

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

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

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

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

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

get_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 that lightsim2grid.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().

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

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

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

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

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

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

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

get_gen_v_vc_row(self: lightsim2grid.lightsim2grid_cpp.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 = 0 of the voltage-control group whose set-point its gen_v is (a generator regulating a bus a group holds: a remote regulator, or a local one on a group-controlled bus), -1 otherwise – and -1 for a group holding an SVC or an hvdc station, whose set-point no batch moves. dF_v/dv_set = -1, so that generator’s gradient is lambda at that row (times its get_gen_v_share), which gen_v_indirect_grad already returns; it has no direct half.

get_linear_solver_stats(self: lightsim2grid.lightsim2grid_cpp.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 that lightsim2grid.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() and lightsim2grid.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 to lightsim2grid.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(), False unless 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_mask False, 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 == 1 the behaviour is the legacy sequential one. With nb_thread > 1 the 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 < 1 are clamped to 1.

Warning

This is only available on lightsim2grid.injectionSweep.InjectionSweepCPP (and on lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP), whose computations are independent of one another.

On lightsim2grid.timeSerie.TimeSeriesCPP each 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 a RuntimeError. Use InjectionSweepCPP if 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 (or value < limit - tol for 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 True it is done once and kept.

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

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

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

Type:

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

set_algo_config(self: lightsim2grid.lightsim2grid_cpp.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. when nb_thread is 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:

InjectionSweep(grid2op_env)

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.

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.

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_thread below (lightsim2grid.timeSerie.TimeSerie cannot: 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:

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.

nb_thread

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 < 1 are clamped to 1.

Must be set before the computation actually runs (eg before compute_V is 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. TimeSeriesCPP initializes 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). InjectionSweepCPP initializes every step with the very same voltage - the one you provide, or the result of the “n” powerflow if init_from_n_powerflow is set - exactly like lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP does 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, which TimeSeriesCPP cannot support).

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

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

available_algorithm_names(self)

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

available_default_algorithms(self)

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

base_case_was_reused(self)

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

change_algorithm(*args, **kwargs)

Overloaded function.

change_solver(*args, **kwargs)

Overloaded function.

clear(self)

Clear the 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_v gradient, (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_v carries: 1 where it is the only regulator of that bus (or of that voltage-control group), 1/n where n of them share it, 0 where it carries none (-1 in both get_gen_v_target_bus and get_gen_v_vc_row).

get_gen_v_target_bus(self)

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

get_gen_v_vc_row(self)

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

get_linear_solver_stats(self)

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

get_linear_solver_stats_per_algo(self)

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

get_p_row_of_bus(self)

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

get_physical_violations(self)

Per row: the list of LimitViolation of the physical limits that row's solution leaves.

get_physical_violations_n(self)

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:

compute_gen_results

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

compute_physical_violations

states the grid does reach and should not sit in).

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.

keep_jacobian

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

keep_vinit_at_group_controlled_buses

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

nb_thread

Number of OS threads used to compute the steps (default 1).

physical_violation_tol_mva

one noise floor for both halves, MW and MVAr being the same scale.

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.

reuse_base_case

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.

  1. 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.AlgorithmType for a list of the available algorithms (NB: some algorithms might not be available on all platform)

    Note

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

    Examples

    from lightsim2grid.algorithm import AlgorithmType
    # init the grid model
    from lightsim2grid.network import init_from_pandapower
    pp_net = ...  # any pandapower grid
    lightsim_grid_model = init_from_pandapower(pp_net)  # some warnings might be issued as well as some warnings
    
    # change the algorithm used for the powerflow
    # to use internally a Newton Raphson algorithm with the Eigen sparse LU linear solver
    lightsim_grid_model.change_algorithm(AlgorithmType.NR_SparseLU)
    
  2. change_algorithm(self: lightsim2grid.lightsim2grid_cpp.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 by lightsim2grid.algorithm.AlgorithmType instead.

change_solver(*args, **kwargs)

Overloaded function.

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

DEPRECATED: use ‘change_algorithm’ instead

  1. 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

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.

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 ReactiveLimits loop. 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) leave pmax_1to2_mw / pmax_2to1_mw? status_droop is an INPUT of the solve, so nothing saturates the droop on its own. OpenLoadFlow’s HvdcAcEmulationLimits.

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

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

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

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

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

get_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 that lightsim2grid.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().

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

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

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

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

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

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

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

get_gen_v_vc_row(self: lightsim2grid.lightsim2grid_cpp.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 = 0 of the voltage-control group whose set-point its gen_v is (a generator regulating a bus a group holds: a remote regulator, or a local one on a group-controlled bus), -1 otherwise – and -1 for a group holding an SVC or an hvdc station, whose set-point no batch moves. dF_v/dv_set = -1, so that generator’s gradient is lambda at that row (times its get_gen_v_share), which gen_v_indirect_grad already returns; it has no direct half.

get_linear_solver_stats(self: lightsim2grid.lightsim2grid_cpp.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 that lightsim2grid.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() and lightsim2grid.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 to lightsim2grid.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(), False unless 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_mask False, 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 == 1 the behaviour is the legacy sequential one. With nb_thread > 1 the 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 < 1 are clamped to 1.

Warning

This is only available on lightsim2grid.injectionSweep.InjectionSweepCPP (and on lightsim2grid.contingencyAnalysis.ContingencyAnalysisCPP), whose computations are independent of one another.

On lightsim2grid.timeSerie.TimeSeriesCPP each 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 a RuntimeError. Use InjectionSweepCPP if 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 (or value < limit - tol for 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 True it is done once and kept.

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

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

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

Type:

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

set_algo_config(self: lightsim2grid.lightsim2grid_cpp.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. when nb_thread is 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).