# Copyright (c) 2020, RTE (https://www.rte-france.com)
# See AUTHORS.txt
# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
# you can obtain one at http://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0
# This file is part of LightSim2grid, LightSim2grid implements a c++ backend targeting the Grid2Op platform.
import warnings
import numpy as np
from scipy import sparse
from lightsim2grid_cpp import SparseLUSolver, SparseLUSolverSingleSlack
try:
from lightsim2grid_cpp import KLUSolver, KLUSolverSingleSlack
KLU_solver_available = True
except ImportError:
KLU_solver_available = False
_PP_VERSION_MAX = "2.7.0"
def _isolate_slack_ids(Sbus, pv, pq):
# extract the slack bus
ref = set(np.arange(Sbus.shape[0])) - set(pv) - set(pq)
ref = np.array(list(ref))
# build the slack weights
slack_weights = np.zeros(Sbus.shape[0])
slack_weights[ref] = 1.0 / ref.shape[0]
return ref, slack_weights
def _get_valid_solver(options, Ybus):
# initialize the solver
# TODO have that in options maybe (can use GaussSeidel, and NR with KLU -faster- or SparseLU)
if options.get("distributed_slack", False):
solver = KLUSolver() if KLU_solver_available else SparseLUSolver()
else:
solver = KLUSolverSingleSlack() if KLU_solver_available else SparseLUSolverSingleSlack()
if not sparse.isspmatrix_csc(Ybus):
Ybus = sparse.csc_matrix(Ybus)
if not Ybus.has_canonical_format:
raise RuntimeError("Your matrix should be in a canonical format. See "
"https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csc_matrix.has_canonical_format.html"
" for more information.")
return solver
[docs]def newtonpf(*args, **kwargs):
"""
Pandapower changed the interface when they introduced the distributed slack functionality.
(around pandapower 2.7.0, pandapower 2.7.0 does not support distributed slack)
As of lightsim2grid version 0.6.0 we tried to reflect this change.
However we want to keep the full compatibility with different pandapower version.
This wrapper tries to select the proper version among:
- :func:`lightsim2grid.newtonpf.newtonpf_old` for older version of pandapower (<= 2.7.0)
- :func:`lightsim2grid.newtonpf.newtonpf_new` for newer version of pandapower (> 2.7.0)
.. versionchanged:: 0.6.0
Before this version, the function was :func:`lightsim2grid.newtonpf.newtonpf_old` ,
now it's a wrapper.
Examples
----------
.. code-block::
from lightsim2grid.newtonpf import newtonpf
# when pandapower version <= 2.7.0
V, converged, iterations, J, Vm_it, Va_it = newtonpf(Ybus, Sbus, V0, pv, pq, ppci, options)
# when pandapower version > 2.7.0
V, converged, iterations, J, Vm_it, Va_it = newtonpf(Ybus, Sbus, V0, ref, pv, pq, ppci, options)
"""
import pandapower as pp
if pp.__version__ <= _PP_VERSION_MAX:
try:
# should be the old version
return newtonpf_old(*args, **kwargs)
except TypeError as exc_:
# old version does not work, I try the new one
try:
return newtonpf_new(*args, **kwargs)
except TypeError as exc2_:
# nothing work, I stop here
raise exc2_ from exc_
else:
try:
# should be the new version
return newtonpf_new(*args, **kwargs)
except TypeError as exc_:
# new version does not work, I try the old one
try:
return newtonpf_old(*args, **kwargs)
except TypeError as exc2_:
# nothing work, I stop here
raise exc2_ from exc_
[docs]def newtonpf_old(Ybus, Sbus, V0, pv, pq, ppci, options):
"""
Perform the Newton scheme to compute the AC powerflow of the system provided as input.
It supports only one single slack bus.
It is main as being integrated into pandapower as a replacement of the pypower implementation of "newtonpf"
.. seealso::
:func:`lightsim2grid.newtonpf.newtonpf` for a compatibility wrapper that tries to find
the best option among :func:`lightsim2grid.newtonpf.newtonpf_old` and
:func:`lightsim2grid.newtonpf.newtonpf_new` depending on pandapower version
.. versionadded:: 0.6.0
Added as a way to retrieve the "old" signature for compatibility with older pandapower version
.. note::
This is a legacy code mainly present for compatibility with older pandapower versions.
.. warning::
It considers that all nodes non pv, non pq are slack nodes and assign the same weights
for all of them even though it was not possible to perform such computation in earlier versions.
Parameters
----------
Ybus: ``numpy.ndarray``, ``numpy.sparmatrix``, dtype:complex
The admittance matrix. If not in a sparse CSC format, it will be converted to it.
Sbus: ``numpy.ndarray``, dtype:complex
The power injected at each bus.
V0: ``numpy.ndarray``, dtype:complex
The initial voltage
pv: ``numpy.ndarray``, dtype:np.int
Index of the pv buses (slack bus must NOT be on this list)
pq: ``numpy.ndarray``, dtype:np.int
Index of the pq buses (slack bus must NOT be on this list)
ppci: ``dict``
pandapower internal "ppc", ignored.
options: ``dict``
Dictionnary of various pandapower option. Only "max_iteration" and "tolerance_mva" are used at the moment.
Returns
-------
V: ``numpy.ndarray``, dtype:complex
The final complex voltage vector
converged: ``bool``
Whether the powerflow converged or not
iterations: ``int``
The number of iterations the solver performed
J: ``scipy.sparsematrix```, dtype:float
The csc scipy sparse matrix of the jacobian matrix of the system.
Notes
-----
J has the shape::
| s | slack_bus | | (pvpq+1,1) | (1, pvpq) | (1, pq) |
| l | ------- | | | ------------------------- |
| a | J11 | J12 | = dimensions: | | (pvpq, pvpq) | (pvpq, pq) |
| c | --------- | | ------ | ------------------------- |
| k | J21 | J22 | | (pq, 1) | (pq, pvpq) | (pq, pq) |
With:
- `J11` = dS_dVa[array([pvpq]).T, pvpq].real (= real part of dS / dVa for all pv and pq buses)
- `J12` = dS_dVm[array([pvpq]).T, pq].real
- `J21` = dS_dVa[array([pq]).T, pvpq].imag
- `J22` = dS_dVm[array([pq]).T, pq].imag (= imaginary part of dS / dVm for all pq buses)
- `slack_bus` = is the representation of the equation for the reference slack bus dS_dVa[slack_bus_id, pvpq].real
and dS_dVm[slack_bus_id, pq].real
- `slack` is the representation of the equation connecting together the slack buses (represented by slack_weights)
the remaining pq components are all 0.
.. note::
By default (and this cannot be changed at the moment), all buses in `ref` will be pv buses except the first one.
"""
max_it = options["max_iteration"]
tol = options['tolerance_mva']
# extract the slack bus
ref, slack_weights = _isolate_slack_ids(Sbus, pv, pq)
# initialize the solver and perform some sanity checks
solver = _get_valid_solver(options, Ybus)
# do the newton raphson algorithm
solver.solve(Ybus, V0, Sbus, ref, slack_weights, pv, pq, max_it, tol)
# extract the results
Va = solver.get_Va()
Vm = solver.get_Vm()
V = Vm * np.exp(1j * Va)
J = solver.get_J()
converged = solver.converged()
iterations = solver.get_nb_iter()
Vm_it = None
Va_it = None
return V, converged, iterations, J, Vm_it, Va_it
[docs]def newtonpf_new(Ybus, Sbus, V0, ref, pv, pq, ppci, options):
"""
Perform the Newton scheme to compute the AC powerflow of the system provided as input.
It supports only one single slack bus.
It is main as being integrated into pandapower as a replacement of the pypower implementation of "newtonpf"
.. versionadded:: 0.6.0
.. seealso::
:func:`lightsim2grid.newtonpf.newtonpf` for a compatibility wrapper that tries to find
the best option among :func:`lightsim2grid.newtonpf.newtonpf_old` and
:func:`lightsim2grid.newtonpf.newtonpf_new` depending on pandapower version
.. note::
It has been updated in version 0.6.0 to match pandapower new signature (addition of the `ref`
parameter)
If you want the old behaviour, please use the `newtonpf_old` function.
Parameters
----------
Ybus: ``numpy.ndarray``, ``numpy.sparmatrix``, dtype:complex
The admittance matrix. If not in a sparse CSC format, it will be converted to it.
Sbus: ``numpy.ndarray``, dtype:complex
The power injected at each bus.
V0: ``numpy.ndarray``, dtype:complex
The initial voltage
ref: ``numpy.ndarray``, dtype:np.int
Ids of the slack buses (added in version 0.5.6 to match pandapower changes)
pv: ``numpy.ndarray``, dtype:np.int
Index of the pv buses (slack bus must NOT be on this list)
pq: ``numpy.ndarray``, dtype:np.int
Index of the pq buses (slack bus must NOT be on this list)
ppci: ``dict``
pandapower internal "ppc", ignored.
options: ``dict``
Dictionnary of various pandapower option. Only "max_iteration" and "tolerance_mva" are used at the moment.
Returns
-------
V: ``numpy.ndarray``, dtype:complex
The final complex voltage vector
converged: ``bool``
Whether the powerflow converged or not
iterations: ``int``
The number of iterations the solver performed
J: ``scipy.sparsematrix``, dtype:float
The csc scipy sparse matrix of the jacobian matrix of the system.
Notes
-----
J has the shape::
| s | slack_bus | | (pvpq+1,1) | (1, pvpq) | (1, pq) |
| l | ------- | | | ------------------------- |
| a | J11 | J12 | = dimensions: | | (pvpq, pvpq) | (pvpq, pq) |
| c | --------- | | ------ | ------------------------- |
| k | J21 | J22 | | (pq, 1) | (pq, pvpq) | (pq, pq) |
With:
- `J11` = dS_dVa[array([pvpq]).T, pvpq].real (= real part of dS / dVa for all pv and pq buses)
- `J12` = dS_dVm[array([pvpq]).T, pq].real
- `J21` = dS_dVa[array([pq]).T, pvpq].imag
- `J22` = dS_dVm[array([pq]).T, pq].imag (= imaginary part of dS / dVm for all pq buses)
- `slack_bus` = is the representation of the equation for the reference slack bus dS_dVa[slack_bus_id, pvpq].real
and dS_dVm[slack_bus_id, pq].real
- `slack` is the representation of the equation connecting together the slack buses (represented by slack_weights)
the remaining pq components are all 0.
.. note::
By default (and this cannot be changed at the moment), all buses in `ref` will be pv buses except the first one.
"""
max_iteration = options["max_iteration"]
tolerance_pu = options['tolerance_mva'] # / ppci["baseMVA"]
try:
# lazy import for earlier pandapower version (without distributed slack):
from pandapower.pypower.idx_bus import SL_FAC
# contribution factors for distributed slack:
slack_weights = ppci['bus'][:, SL_FAC].astype(np.float64)
except ImportError:
# earlier version of pandapower
warnings.warn("You are using a pandapower version that does not support distributed slack. We will attempt to "
"replicate this with lightsim2grid.")
ref, slack_weights = _isolate_slack_ids(Sbus, pv, pq)
# initialize the solver and perform some sanity checks
solver = _get_valid_solver(options, Ybus)
# do the newton raphson algorithm
solver.solve(Ybus, V0, Sbus, ref, slack_weights, pv, pq, max_iteration, tolerance_pu)
# extract the results
Va = solver.get_Va()
Vm = solver.get_Vm()
V = Vm * np.exp(1j * Va)
J = solver.get_J()
converged = solver.converged()
iterations = solver.get_nb_iter()
Vm_it = None
Va_it = None
return V, converged, iterations, J, Vm_it, Va_it