LSGrid module
====================================
The main class of the lightsim2grid python package is the `LSGrid` class, that is a python class created
from the c++ `LSGrid` (thanks fo pybind11).
This class basically represents a powergrid (what elements it is made for, their electro technical properties etc.)
.. _network-init-formats:
Supported source formats
--------------------------
An `LSGrid` can be built from several source formats, each with a dedicated ``init_from_*`` function in
``lightsim2grid.network`` (none of them model every element the source format itself supports):
.. list-table::
:header-rows: 1
:widths: 30 70
* - Function
- Source format
* - :func:`~lightsim2grid.network.init_from_pandapower`
- a `pandapower `_ network (``pandapowerNet``)
* - :func:`~lightsim2grid.network.init_from_pypowsybl`
- a `pypowsybl `_ network (iidm format)
* - :func:`~lightsim2grid.network.init_from_matpower`
- a `MATPOWER `_ case (``.m`` or ``.mat`` file, or an
already-parsed dict)
* - :func:`~lightsim2grid.network.init_from_powermodels`
- a `PowerModels.jl `_ network
data dictionary
* - :func:`~lightsim2grid.network.init_from_pf_delta`
- a row of the PFΔ dataset (either already parsed into a dict, or a path to its
``.json`` file) -- wraps a PowerModels network dict under a ``"network"`` key
and delegates to ``init_from_powermodels``
See the "Detailed documentation" section below for the full signature and caveats of each.
For example, you can init it from a pandapower grid like (NOT RECOMMENDED, though sometimes needed):
.. code-block:: python
from lightsim2grid.network import init_from_pandapower
pp_net = ... # any pandapower grid eg. pp_net = pn.case118()
lightsim_grid_model = init_from_pandapower(pp_net) # some warnings might be issued as well as some warnings
A better initialization is through the :class:`lightsim2grid.lightSimBackend.LightSimBackend` class:
.. code-block:: python
import grid2op
from lightsim2grid import LightSimBackend
# create a lightsim2grid "LSGrid"
env_name = ... # eg. "l2rpn_case14_sandbox"
env = grid2op.make(env_name, backend=LightSimBackend())
grid_model = env.backend._grid
.. warning::
We do not recommend to manipulate directly the :class:`lightsim2grid.network.LSGrid` directy, but to use
it via the backend class. This is much more tested this way.
.. _bus-labelling:
Bus labelling conventions
-------------------------
A recurring source of confusion is that lightsim2grid manipulates bus ids in
**three different conventions**. A given integer (say ``2``) does *not* refer to
the same bus in all of them, so it is important to know which convention a given
method expects or returns.
.. note::
Internally (c++ side) these conventions are even distinct *types*
(``LocalBusId``, ``GridModelBusId`` / ``GlobalBusId`` and ``SolverBusId``), so
that an accidental conversion between them is caught at compile time. Python
only sees plain integers, hence this section.
1. **Local bus id** — the busbar number *inside a substation*. It is ``-1`` for a
disconnected element, or between ``1`` and ``n_busbar_per_sub``. This is the
grid2op convention: the value you put in a ``set_bus`` action and what you read
in grid2op's ``topo_vect``. It is the convention of
:func:`lightsim2grid.network.LSGrid.update_topo` (the bulk topology update used
by :class:`lightsim2grid.lightSimBackend.LightSimBackend`), whose ``new_values``
array is indexed by the position in the topology vector (``pos_topo_vect``) and
holds *local* busbar ids. An element's substation is given by its ``sub_id`` and
its slot in ``topo_vect`` by ``pos_topo_vect``.
2. **GridModel bus id** (a.k.a. *global* bus id) — the index of a bus in the whole
``LSGrid``, between ``0`` and ``n_sub * n_busbar_per_sub - 1``. **This is the
convention of essentially every "by id" public ``LSGrid`` method** (``change_bus_*``
/ ``get_bus_*``, ``deactivate_bus`` / ``reactivate_bus``, ``set_gen_regulated_bus``,
the ``bus_id`` / ``bus1_id`` / ``bus2_id`` fields of the ``*Info`` objects, …) and
of the "user facing" matrices/vectors (``get_Ybus``, ``get_Sbus``, ``get_V``,
``get_pv``, …).
3. **Solver bus id** — a *compact* index (``0`` … ``nb_connected_bus() - 1``) that depends
on the current topology: only the buses actually in service get a solver id.
It is what is passed to the linear/powerflow solver, so it is the convention of
everything with a ``_solver`` suffix (``get_Ybus_solver``, ``get_V_solver``,
``get_pv_solver``, ``get_J_solver``, …) and of the Jacobian-column mappings
returned by the solver itself (``get_theta_to_J_col`` / ``get_vm_to_J_col`` /
``get_q_to_J_col``, see :ref:`use-solver`).
The mapping between conventions 2 and 3 is available (as numpy arrays) through:
================================ ===========================================================
Method Meaning
================================ ===========================================================
``lsgrid.id_ac_solver_to_me()`` array indexed by *AC solver* bus id -> *GridModel* bus id
``lsgrid.id_me_to_ac_solver()`` array indexed by *GridModel* bus id -> *AC solver* bus id
``lsgrid.id_dc_solver_to_me()`` array indexed by *DC solver* bus id -> *GridModel* bus id
``lsgrid.id_me_to_dc_solver()`` array indexed by *GridModel* bus id -> *DC solver* bus id
``lsgrid.total_bus()`` total number of buses (``n_sub * n_busbar_per_sub``)
``lsgrid.nb_connected_bus()`` number of buses currently seen by the solver
================================ ===========================================================
Which convention each "by id" method uses:
.. list-table::
:header-rows: 1
:widths: 50 18 32
* - Method(s)
- get / set
- Bus id convention
* - ``update_topo(has_changed, new_values)``
- set (bulk)
- Local (per-substation)
* - ``change_bus_load`` / ``change_bus_gen`` / ``change_bus_sgen`` /
``change_bus_shunt`` / ``change_bus_storage`` / ``change_bus_svc``
- set
- GridModel (global)
* - ``change_bus1_powerline`` / ``change_bus2_powerline`` /
``change_bus1_trafo`` / ``change_bus2_trafo`` /
``change_bus1_dcline`` / ``change_bus2_dcline``
- set
- GridModel (global)
* - ``deactivate_bus`` / ``reactivate_bus``
- set
- GridModel (global)
* - ``set_gen_regulated_bus(gen_id, regulated_bus)``
- set
- GridModel (global)
* - ``get_bus_load`` / ``get_bus_gen`` / ``get_bus_sgen`` /
``get_bus_shunt`` / ``get_bus_storage`` / ``get_bus_svc`` /
``get_bus1_powerline`` / ``get_bus2_powerline`` / ...
- get
- GridModel (global)
* - ``LoadInfo.bus_id`` (and ``bus1_id`` / ``bus2_id`` / ...,
``regulated_bus_id`` of the ``*Info`` objects)
- get (read-only)
- GridModel (global)
* - ``get_Ybus`` / ``get_Sbus`` / ``get_V`` / ``get_Va`` / ``get_Vm`` /
``get_pv`` / ``get_pq`` / ``get_slack_ids``
- get
- GridModel (global)
* - ``get_Ybus_solver`` / ``get_Sbus_solver`` / ``get_V_solver`` /
``get_pv_solver`` / ``get_pq_solver`` / ``get_slack_ids_solver`` /
``get_J_solver``
- get
- Solver
* - ``solver.get_J`` / ``solver.get_theta_to_J_col`` /
``solver.get_vm_to_J_col`` / ``solver.get_q_to_J_col``
- get
- Solver
.. _elements-modeled:
Elements modeled
------------------
Substations
+++++++++++++++++++++
:func:`~lightsim2grid.network.LSGrid.get_substations` (alias ``get_voltage_levels``) returns a
:class:`lightsim2grid.elements.SubstationContainer`: like every other ``*Container`` on this page it
supports ``len(...)``, indexing and iteration over :class:`lightsim2grid.elements.SubstationInfo`
objects.
.. autoclass:: lightsim2grid.elements.SubstationContainer
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.SubstationInfo
:members:
:autosummary:
Generators (standard)
+++++++++++++++++++++
.. autoclass:: lightsim2grid.elements.GeneratorContainer
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.GenInfo
:members:
:autosummary:
A generator can also perform **remote voltage control**, *ie* regulate the
voltage of a bus different from the one it is connected to. Use
:func:`lightsim2grid.network.LSGrid.set_gen_regulated_bus` to set the regulated
bus (it defaults to the generator's own bus, which corresponds to local control).
This is read automatically when initializing the grid from pypowsybl. The same
mechanism is used by the :ref:`svc-section` below.
.. warning::
When the grid is read from pypowsybl, the regulated bus is resolved **once**, at
import time, and stored by its (fixed) lightsim2grid global bus id. If the
regulated element is later moved to another bus *inside lightsim2grid* (e.g.
through a ``change_bus_*`` / topology change), the controller keeps regulating the
bus resolved at import: the lightsim2grid grid and the original pypowsybl grid then
desynchronise. Re-import the grid (or call ``set_gen_regulated_bus`` again) if you
need to follow such a topology change.
Static Generators (more exotic)
++++++++++++++++++++++++++++++++
.. autoclass:: lightsim2grid.elements.SGenContainer
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.SGenInfo
:members:
:autosummary:
Loads and Storage Units
++++++++++++++++++++++++
.. autoclass:: lightsim2grid.elements.LoadContainer
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.LoadInfo
:members:
:autosummary:
Storage units (batteries) are modeled as PQ injections too, but exposed through a
dedicated container. They use the **load convention**: a positive ``target_p`` means
the unit is charging (power drawn from the grid), a negative ``target_p`` means it is
discharging (power injected in the grid). Note that this is the opposite of the
PowSyBl / IIDM (generator) convention; :func:`lightsim2grid.network.init_from_pypowsybl`
negates the battery setpoints accordingly.
.. autoclass:: lightsim2grid.elements.StorageContainer
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.StorageInfo
:members:
:autosummary:
.. _svc-section:
Static Var Compensators (SVC)
+++++++++++++++++++++++++++++++
Static Var Compensators (SVC) are shunt-connected devices that can regulate
voltage (or reactive power). Each SVC has a ``regulation_mode``:
- ``0`` (OFF): the device does not regulate anything;
- ``1`` (VOLTAGE): it maintains ``target_vm_pu`` at its regulated bus, possibly
with a non-zero ``slope_pu`` (droop);
- ``2`` (REACTIVE_POWER): it injects ``target_q_mvar``.
Like generators, an SVC can regulate a remote bus (see ``regulated_bus_id``).
The susceptance limits ``b_min`` / ``b_max`` are stored for information but are
**never enforced** by the powerflow.
.. autoclass:: lightsim2grid.elements.SvcContainer
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.SvcInfo
:members:
:autosummary:
Shunts
++++++++++++++++++++++++
.. autoclass:: lightsim2grid.elements.ShuntContainer
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.ShuntInfo
:members:
:autosummary:
Lines
++++++
.. autoclass:: lightsim2grid.elements.LineContainer
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.LineInfo
:members:
:autosummary:
Transformers
+++++++++++++
.. autoclass:: lightsim2grid.elements.TrafoContainer
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.TrafoInfo
:members:
:autosummary:
HVDC Lines (more exotic)
++++++++++++++++++++++++++++
HVDC links are modeled inside the AC (Newton-Raphson) and DC powerflow. Each
link is made of two converter stations (VSC or LCC, see
:class:`lightsim2grid.elements.ConverterStationInfo`) and can operate either at
a fixed active power setpoint or in angle-droop (AC emulation) mode. The droop
regime can be inspected / forced with
:func:`lightsim2grid.network.LSGrid.set_status_droop_hvdc` and
:func:`lightsim2grid.network.LSGrid.get_status_droop_hvdc`.
.. note::
The container used to be called ``DCLineContainer`` (and the info object
``DCLineInfo``). These names are still importable from
``lightsim2grid.elements`` as deprecated aliases of ``HvdcLineContainer`` /
``HvdcLineInfo``.
.. autoclass:: lightsim2grid.elements.HvdcLineContainer
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.HvdcLineInfo
:members:
:autosummary:
.. autoclass:: lightsim2grid.elements.ConverterStationInfo
:members:
:autosummary:
.. _ptdf-lodf-section:
PTDF / LODF
------------
As long as the topology of the grid is not modified, a DC powerflow is a *linear* function of the
bus injections, so it can be replaced by a matrix multiplication -- much faster than solving the
linear system again for every new injection or contingency (see :ref:`benchmark-dc-solvers` for
numbers).
- :func:`~lightsim2grid.network.LSGrid.get_ptdf` (or :func:`~lightsim2grid.network.LSGrid.get_ptdf_solver`
for the *solver* bus labelling) returns the Power Transfer Distribution Factor matrix: how much the
flow on each powerline / transformer changes for a 1 MW injection change at each bus.
- :func:`~lightsim2grid.network.LSGrid.get_lodf` returns the Line Outage Distribution Factor matrix:
how much the flow on each powerline / transformer changes when another one is disconnected --
the tool of choice for an n-1 contingency analysis restricted to DC (see also
:class:`lightsim2grid.contingencyAnalysis.ContingencyAnalysis` for the general AC/DC case).
- :func:`~lightsim2grid.network.LSGrid.get_Bf` returns the sparse "bus to branch" susceptance
matrix these are built from.
Both ``get_ptdf`` and ``get_lodf`` require a DC powerflow (``dc_pf``) to have been run first, and
are only valid for the topology that was in place when that powerflow was solved -- any topology
change invalidates them. See each function's own documentation below for a full worked example.
.. _cache-reuse-section:
Solver cache reuse
-------------------
Solving a powerflow is not only the linear algebra: the grid must first be turned into what the
solver consumes -- a compact bus labelling, the admittance matrix ``Ybus``, the injection vector
``Sbus``, the PV / PQ split, the slack weights. On a small grid that assembly is worth a good
fifth of the total time, and it is almost entirely redundant between two consecutive powerflows:
changing one load's setpoint does not move a single admittance coefficient.
So ``LSGrid`` keeps what it built and re-stamps only the parts that changed. Every method that
modifies the grid (``change_*``, ``deactivate_*``, ``reactivate_*``, …) records what it
invalidated, and each powerflow rebuilds exactly that much.
**This is on by default and needs nothing from you.** Every powerflow marks its own solver
family "in sync" on the way out.
.. versionchanged:: 1.0.0
Before 1.0.0 you had to call ``lsgrid.unset_changes()`` yourself after each powerflow, or
silently pay for a full rebuild every time. That call is now unnecessary (it does nothing
when cache reuse is enabled, which is the default) and it is kept only for backward
compatibility.
The AC and the DC solver cache **independently**: each has its own bus labelling, its own matrix
(``Ybus`` / ``Bbus``), its own injections, its own PV / PQ split and slack weights. An AC
powerflow never marks, invalidates or overwrites anything belonging to the DC family, and the
reverse. Hence the per-family accessors :func:`~lightsim2grid.network.LSGrid.get_ac_pv_solver`,
:func:`~lightsim2grid.network.LSGrid.get_dc_pv_solver`, and their ``pq`` / ``slack_weights``
counterparts.
Controlling it
+++++++++++++++
====================================================== =============================================================
Method Meaning
====================================================== =============================================================
``lsgrid.allow_cache_reuse(bool)`` turn reuse on (default) or off, for both families
``lsgrid.allow_ac_cache_reuse(bool)`` ... for the AC family only
``lsgrid.allow_dc_cache_reuse(bool)`` ... for the DC family only
``lsgrid.get_allow_cache_reuse()`` ``True`` iff **both** families may reuse
``lsgrid.get_allow_ac_cache_reuse()`` is the AC family allowed to reuse?
``lsgrid.get_allow_dc_cache_reuse()`` is the DC family allowed to reuse?
``lsgrid.prevent_cache_reuse()`` drop what both families cached, once
``lsgrid.prevent_ac_cache_reuse()`` ... for the AC family only
``lsgrid.prevent_dc_cache_reuse()`` ... for the DC family only
====================================================== =============================================================
Note the difference: ``allow_*`` is a **mode** (it stays until you change it back), while
``prevent_*`` is a **one-shot invalidation** -- the family throws away what it had and caches
again from the next powerflow on. ``prevent_cache_reuse()`` is the function historically called
``tell_solver_need_reset()``, which still works.
You should not normally need either. The two cases that call for them are:
- **You suspect a caching bug.** ``allow_cache_reuse(False)`` makes every powerflow rebuild
everything from the containers; the two runs must agree to the last bit.
.. code-block:: python
v_cached = lsgrid.ac_pf(v_init, 10, 1e-8)
lsgrid.allow_cache_reuse(False)
v_rebuilt = lsgrid.ac_pf(v_init, 10, 1e-8)
assert (abs(v_cached - v_rebuilt) < 1e-12).all()
- **You modified the grid behind ``LSGrid``'s back**, through something other than its own
``change_*`` / ``deactivate_*`` / ``reactivate_*`` methods -- then nothing recorded the
invalidation, and ``prevent_cache_reuse()`` (or the narrower ``tell_recompute_ybus`` /
``tell_recompute_sbus``) is how you say so.
.. note::
A wrong "nothing changed" claim can cost you a rebuild you were trying to avoid; it can never
make lightsim2grid read memory it does not own. Every powerflow checks that the data the flags
describe is actually there before reusing it, and rebuilds from scratch otherwise.
What is never cached across
++++++++++++++++++++++++++++
**Serialization.** Nothing the solvers cache is written to a pickle or a binary file, and nothing
is read back: a grid restored through :func:`~lightsim2grid.network.LSGrid.load_binary` or
``pickle.loads`` always starts cold and rebuilds on its first powerflow. This is a security
property rather than a performance one. A cache is a second copy of state the elements already
determine; read back from a file it becomes a copy that cannot be checked against the elements it
claims to describe. ``check_grid()`` can validate that an index is in range -- it cannot validate
that a matrix really is the admittance matrix of the grid stored next to it, and one that merely
looked well-formed would be solved without complaint. Files are not trusted input, so the cache is
rebuilt, once, from data that is.
**Copying.** :func:`~lightsim2grid.network.LSGrid.copy` does not carry the cache either: the copy
starts cold and rebuilds on its first powerflow. Unlike the serialization case this is not a safety
requirement -- a copy is the same grid, in the same process, so its cache would be perfectly valid
-- and it may change in a future version. The ``allow_*_cache_reuse`` settings *are* copied.
Detailed documentation
--------------------------
.. automodule:: lightsim2grid.network
:members:
:autosummary:
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`