Source code for lightsim2grid.contingencyAnalysis

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

__all__ = ["ContingencyAnalysisCPP", "LimitViolation", "ViolationElementType",
           "LimitViolationType", "PreContingencyResult",
           "ContingencyResult", "SecurityAnalysisResult"]

import copy
from dataclasses import dataclass
from typing import List, Optional, Tuple
import numpy as np
from collections.abc import Iterable

from lightsim2grid.algorithm import AlgorithmType
from .lightsim2grid_cpp import (ContingencyAnalysisCPP, LimitViolation,
                                 ViolationElementType, LimitViolationType)

try:
    from lightsim2grid.lightSimBackend import LightSimBackend
    __all__.append("ContingencyAnalysis")
    GRID2OP_INSTALLED = True
except ImportError as exc_:  # noqa: F841
    # grid2op is not installed
    GRID2OP_INSTALLED = False


[docs] @dataclass class PreContingencyResult: """Limit violations for the pre-contingency ("n", no disconnection) case. .. note:: ``converged`` is always True here: `ContingencyAnalysisCPP.compute` raises a ``RuntimeError`` if the pre-contingency powerflow itself does not converge (every contingency is solved relative to this base case, so a diverging base case makes the whole analysis meaningless). """ converged: bool limit_violations: List[LimitViolation]
[docs] @dataclass class ContingencyResult: """Limit violations for a single simulated contingency. .. note:: If ``converged`` is False, ``limit_violations`` contains exactly one ``LimitViolation`` with ``element_type == ViolationElementType.GRID`` and ``violation_type`` either ``LimitViolationType.NOT_SIMULATED`` (a pre-check skipped this contingency, eg it splits the grid, without ever invoking the solver) or ``LimitViolationType.DIVERGENCE`` (the solver ran but did not converge). """ element_ids: List[int] #: branch ids (lines then trafos) disconnected by this contingency element_names: List[str] #: names (`env.name_line`) of the elements disconnected by this contingency contingency_name: Optional[str] #: user-supplied name, see `add_single_contingency` converged: bool limit_violations: List[LimitViolation]
[docs] @dataclass class SecurityAnalysisResult: """Result of `ContingencyAnalysis.run` / `run_ac` / `run_dc`, modeled after pypowsybl's security analysis result.""" pre_contingency_result: PreContingencyResult post_contingency_results: List[ContingencyResult]
[docs] class ContingencyAnalysis(object): """ This class allows to perform a "security analysis" from a given grid state. For now, you cannot change the grid state, and it only computes the security analysis with current flows at origin of powerlines. Feel free to post a feature request if you want to extend it. This class is used in 4 phases: 0) you create it from a grid2op environment (the grid topology will not be modified from this environment) 1) you add some contingencies to simulate 2) you start the simulation 3) you read back the results Examples -------- An example is given here .. code-block:: python import grid2op from lightsim2grid import ContingencyAnalysis from lightsim2grid import LightSimBackend env_name = ... env = grid2op.make(env_name, backend=LightSimBackend()) 0) you create contingency_analysis = ContingencyAnalysis(env) 1) you add some contingencies to simulate contingency_analysis.add_multiple_contingencies(...) # or contingency_analysis.add_single_contingency(...) 2) you start the simulation (done automatically) 3) you read back the results res_p, res_a, res_v = contingency_analysis.get_flows() # 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 `contingency_analysis.contingency_order[row_id]` Notes ------ Sometimes, the behaviour might differ from grid2op. For example, if simulating a contingency leads to a non connected grid, then this function will return "Nan" for the flows and 0. for the voltages. In grid2op, it would be, in this case, 0. for the flows and 0. for the voltages. By default, a contingency that splits the grid in multiple connected components is not simulated (its voltages are left at 0.). If you set the `handle_disconnected_grid` attribute to ``True``, such contingencies are instead simulated on their largest connected component: the buses of the other component(s) are "masked" and their voltage is reported as 0. This is done without triggering any extra matrix re-factorization (the symbolic factorization of the solver is reused). It is supported by the Newton-Raphson family (AC) and by the DC solver; a non Newton-Raphson AC algorithm (*eg* Gauss-Seidel or Fast-Decoupled) is rejected. """ STR_TYPES = (str, np.str_) # np.str deprecated in numpy 1.20 and earlier versions not supported anyway def __init__(self, grid2op_env, compute_limit_violations: bool = False): if not GRID2OP_INSTALLED: raise RuntimeError("Impossible to use the python wrapper `ContingencyAnalysis` " "when grid2op is not installed. Please fall back to the " "c++ version (available in python) with:\n" "\tfrom lightsim2grid.contingencyAnalysis import ContingencyAnalysisCPP\n" "and refer to the appropriate documentation.") from grid2op.Environment import Environment # type: ignore self.__is_closed = False if isinstance(grid2op_env, Environment): if not isinstance(grid2op_env.backend, LightSimBackend): raise RuntimeError("This class only works with LightSimBackend") self._ls_backend = grid2op_env.backend.copy() elif isinstance(grid2op_env, LightSimBackend): if hasattr(grid2op_env, "_is_loaded") and not grid2op_env._is_loaded: raise RuntimeError("Impossible to init a `ContingencyAnalysis` " "with a backend that has not been initialized.") self._ls_backend = grid2op_env.copy() else: raise RuntimeError("`ContingencyAnalysis` can only be created " "with a grid2op `Environment` or a `LightSimBackend`") self.computer = ContingencyAnalysisCPP(self._ls_backend._grid, bool(compute_limit_violations)) self._contingency_order = {} # key: contingency (as tuple), value: order in which it is entered self._all_contingencies = [] self._contingency_names = {} # key: contingency (as tuple), value: user-supplied name (or None) self.__computed = False self._vs = None self._ampss = None self._mws = None self.available_default_algorithms = self.computer.available_default_algorithms() if AlgorithmType.NR_KLU in self.available_default_algorithms: # use the faster KLU if available self.computer.change_algorithm(AlgorithmType.NR_KLU) @property def all_contingencies(self): return copy.deepcopy(self._all_contingencies) @all_contingencies.setter def all_contingencies(self, val): raise RuntimeError("Impossible to add new topologies like this. Please use `add_single_contingency` " "or `add_multiple_contingencies`.") @property def init_from_n_powerflow(self): """Whether to initialize the complex voltages of each contingency with the results of a "n" powerflow (a powerflow without any line disconnection) instead of a flat start. Default: ``False``. Must be set before the computation actually runs (eg before ``get_flows`` / ``compute_V`` / ``run`` are called); it has no effect on a contingency that has already been solved. """ return self.computer.init_from_n_powerflow @init_from_n_powerflow.setter def init_from_n_powerflow(self, val: bool): if bool(val) != val: raise ValueError("The `init_from_n_powerflow` attribute must be a boolean.") self.computer.init_from_n_powerflow = bool(val) @property def handle_disconnected_grid(self): """Whether a contingency that splits the grid into several connected components is simulated on its largest component instead of being skipped. Default: ``False``, meaning such a contingency is not simulated at all (its voltages are left at 0., see the class-level note above). When ``True``, the buses of the other component(s) are "masked" (their voltage reported as 0.) and the largest component is solved normally, without triggering any extra matrix re-factorization. Supported by the Newton-Raphson family (AC) and by the DC solver; a non Newton-Raphson AC algorithm (*eg* Gauss-Seidel or Fast-Decoupled) is rejected. """ return self.computer.handle_disconnected_grid @handle_disconnected_grid.setter def handle_disconnected_grid(self, val: bool): if bool(val) != val: raise ValueError("The `handle_disconnected_grid` attribute must be a boolean.") self.computer.handle_disconnected_grid = bool(val) @property def compute_limit_violations(self): """Whether limit violations are computed inline, per contingency, during `run` / `run_ac` / `run_dc` (see also `get_violations` on the underlying `computer`). Default: false. Computing violations means an extra per-element current / voltage check in every contingency's solve, so leave this off if you only need `get_flows`. Can only be set at construction time (`ContingencyAnalysis(env, compute_limit_violations=True)`) or via this setter; changing it clears any previously-computed results. """ return self.computer.compute_limit_violations @compute_limit_violations.setter def compute_limit_violations(self, val: bool): if bool(val) != val: raise ValueError("The `compute_limit_violations` attribute must be a boolean.") val = bool(val) if val == self.computer.compute_limit_violations: return # no-op, matches the C++ side (which also no-ops and does not clear) self.computer.compute_limit_violations = val # this clears the C++-side results / contingencies self.clear() # keep the python-side bookkeeping (contingency order / names) in sync @property def violation_threshold(self): """Threshold (a ``float`` in ``]0., 1.]``, default ``1.0``) applied to every limit-violation check performed when `compute_limit_violations` is `True`. It is the fraction of the usable range that is still considered acceptable, so lowering it makes every check stricter (more violations reported, never fewer). Each of the three checks owns one interval, running from a "healthy" anchor to the limit that can be violated, and the threshold moves that limit towards its anchor -- a linear interpolation, identical for all three:: effective_limit = threshold * limit + (1 - threshold) * anchor ============== ======== ========= =============================================== check anchor limit violates when ============== ======== ========= =============================================== CURRENT 0 limit_a ``value >= threshold * limit_a`` LOW_VOLTAGE vn_kv vmin_kv ``v <= threshold * vmin + (1 - threshold) * vn`` HIGH_VOLTAGE vn_kv vmax_kv ``v >= threshold * vmax + (1 - threshold) * vn`` ============== ======== ========= =============================================== A line's usable range really is ``[0, limit_a]``, so its anchor is ``0`` and the rule reduces to scaling the limit. A voltage bound has no such natural "zero" end, so the bus nominal voltage is used instead (operating limits are conventionally expressed as +/- x% of it), **clamped into** ``[vmin_kv, vmax_kv]`` for the rare real-world band that does not bracket it. Either way each acceptable interval keeps a width of exactly ``threshold`` times its original one. Anchoring both voltage checks on ``vn_kv`` -- rather than on each other -- keeps them **independent**: the ``LOW_VOLTAGE`` verdict depends on ``vmin_kv``, ``vn_kv`` and the threshold alone, so setting ``vmax_kv`` (or leaving it at ``NaN``) can never change it, and vice versa. The two effective bounds also converge towards ``vn_kv`` from their own side and can never cross, so no bus is ever both too low and too high. A bus with inconsistent limits (``vmin_kv > vmax_kv``) raises a ``RuntimeError`` rather than being reported as an arbitrary one of the two types. The reported ``value`` / ``limit`` are never rescaled by the threshold; only the test deciding whether to report is shifted. The default ``1.0`` reproduces the previous, threshold-less behaviour. Like `nb_thread` / `handle_disconnected_grid`, this is a plain runtime knob: it only affects the next `run` / `run_ac` / `run_dc`. *Lowering* it invalidates any already-computed results (the registered contingencies are kept, so it is enough to run again); *raising* it back up does not. """ return self.computer.violation_threshold @violation_threshold.setter def violation_threshold(self, val): try: val = float(val) except (TypeError, ValueError): raise ValueError("The `violation_threshold` attribute must be a real number.") if not (0. < val <= 1.): raise ValueError("The `violation_threshold` attribute must be in the range " f"]0., 1.] (got {val}).") if val < self.computer.violation_threshold: # mirrors the c++-side `set_violation_threshold`, which calls # `clear_results_only()` when the threshold is tightened (results computed under # the previous, looser threshold would silently under-report). Without resetting # the python-side cache too, `self.__computed` would stay True while the c++-side # results are gone, and the next `run()` would skip recomputing and then index # empty result arrays. Keep the registered contingencies (with_contlist=False). self.clear(with_contlist=False) self.computer.violation_threshold = val @property def nb_thread(self): """Number of OS threads used to solve the contingencies (default: 1). With ``nb_thread == 1`` the behaviour is identical to the legacy sequential computation. With ``nb_thread > 1`` the contingencies are split across that many threads (each with its own solver and admittance matrix copy); the results do not depend on the number of threads. """ return self.computer.nb_thread @nb_thread.setter def nb_thread(self, val: int): if int(val) != val: raise ValueError("The `nb_thread` attribute must be an integer.") self.computer.nb_thread = int(val) # TODO implement that ! def __update_grid(self, backend_act): raise NotImplementedError("TODO !") self.clear(with_contlist=False) self._ls_backend.apply_action(backend_act) # run the powerflow self._ls_backend.runpf() # update the computer # self.computer = ContingencyAnalysisCPP(self._ls_backend._grid) # self.computer.update_grid(...) # not implemented
[docs] def clear(self, with_contlist=True): """ Clear the list of contingencies to simulate """ if self.__is_closed: raise RuntimeError("This is closed, you cannot use it.") self.__computed = False self._vs = None self._ampss = None self._mws = None if with_contlist: self.computer.clear() self._contingency_order = {} self._all_contingencies = [] self._contingency_names = {} else: self.computer.clear_results_only()
def _single_cont_to_li_int(self, single_cont): li_disc = [] if isinstance(single_cont, int): single_cont = [single_cont] for stuff in single_cont: if isinstance(stuff, type(self).STR_TYPES): stuff = (type(self._ls_backend).name_line == stuff).nonzero() stuff = stuff[0] if stuff.size == 0: # name is not found raise RuntimeError(f"Impossible to find a powerline named \"{stuff}\" in the environment") stuff = int(stuff[0]) else: stuff = int(stuff) li_disc.append(stuff) return li_disc
[docs] def add_single_contingency(self, *args, name=None): """ This function allows to add a single contingency specified by either the powerlines names (which should match env.name_line) or by their ID. The contingency added can be a "n-1" which will simulate a single powerline disconnection or a "n-k" which will simulate the disconnection of multiple powerlines. It does not accept any positional keyword arguments, but accepts the keyword-only `name` argument: an optional, user-supplied string used to identify this contingency in the result of `run` / `run_ac` / `run_dc` (`ContingencyResult.contingency_name`). If not given, `contingency_name` is `None` for this contingency. If this exact contingency was already registered (same set of disconnected elements), `name` is ignored (the first registration wins). Examples -------- .. code-block:: python import grid2op from lightsim2grid import ContingencyAnalysis from lightsim2grid import LightSimBackend env_name = ... env = grid2op.make(env_name, backend=LightSimBackend()) contingency_analysis = ContingencyAnalysis(env) # the single (n-1) contingency "disconnect powerline 0" is added contingency_analysis.add_single_contingency(0) # add the single (n-1) contingency "disconnect line 1 contingency_analysis.add_single_contingency(env.name_line[1]) # add a single contingency that disconnect powerline 2 and 3 at the same time contingency_analysis.add_single_contingency(env.name_line[2], 3) Notes ----- If it raises an error for a given contingency, the object might be not properly initialized. In this case, we recommend you to clear it (using the `clear()` method and to attempt to add contingencies again.) """ if self.__is_closed: raise RuntimeError("This is closed, you cannot use it.") li_disc = self._single_cont_to_li_int(args) li_disc_tup = tuple(li_disc) if li_disc_tup not in self._contingency_order: # this is really the first time this contingency is seen try: self.computer.add_nk(li_disc) my_id = len(self._contingency_order) self._contingency_order[li_disc_tup] = my_id self._all_contingencies.append(li_disc_tup) self._contingency_names[li_disc_tup] = name except Exception as exc_: raise RuntimeError(f"Impossible to add the contingency {args}. The most likely cause " f"is that you try to disconnect a powerline that is not present " f"on the grid") from exc_
[docs] def add_multiple_contingencies(self, *args): """ This function will add multiple contingencies at the same time. This code is equivalent to: .. code-block:: python for single_cont in args: self.add_single_contingency(single_cont) It does not accept any keword arguments. Examples -------- .. code-block:: python import grid2op from lightsim2grid import ContingencyAnalysis from lightsim2grid import LightSimBackend env_name = ... env = grid2op.make(env_name, backend=LightSimBackend()) contingency_analysis = ContingencyAnalysis(env) # add a single contingency that disconnect powerline 2 and 3 at the same time contingency_analysis.add_single_contingency(env.name_line[2], 3) # add a multiple contingencies the first one disconnect powerline 2 and # and the second one disconnect powerline 3 contingency_analysis.add_multiple_contingencies(env.name_line[2], 3) """ if self.__is_closed: raise RuntimeError("This is closed, you cannot use it.") for single_cont in args: if isinstance(single_cont, Iterable) and not isinstance(single_cont, type(self).STR_TYPES): # this is a contingency consisting in cutting multiple powerlines self.add_single_contingency(*single_cont) else: # this is likely an int or a string representing a contingency self.add_single_contingency(single_cont)
[docs] def add_all_n1_contingencies(self): """ This method registers as the contingencies that will be computed all the contingencies that disconnects 1 powerline This is equivalent to: .. code-block:: python for single_cont_id in range(env.n_line): self.add_single_contingency(single_cont_id) """ if self.__is_closed: raise RuntimeError("This is closed, you cannot use it.") for single_cont_id in range(type(self._ls_backend).n_line): self.add_single_contingency(single_cont_id)
[docs] def get_flows(self, *args): """ Retrieve the flows after each contingencies has been simulated. Each row of the resulting flow matrix will correspond to a contingency simulated in the arguments. You can require only the result on some contingencies with the `args` argument, but in each case, all the results will be computed. If you don't specify anything, the results will be returned for all contingencies (which we recommend to do) Examples -------- .. code-block:: python import grid2op from lightsim2grid import ContingencyAnalysis from lightsim2grid import LightSimBackend env_name = ... env = grid2op.make(env_name, backend=LightSimBackend()) contingency_analysis = ContingencyAnalysis(env) contingency_analysis.add_multiple_contingencies(...) # or contingency_analysis.add_single_contingency(...) res_p, res_a, res_v = contingency_analysis.get_flows() # 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 `contingency_analysis.contingency_order[row_id]` """ if self.__is_closed: raise RuntimeError("This is closed, you cannot use it.") all_defaults = self.computer.my_defaults() if len(args) == 0: # default: i consider all contingencies orders_ = np.zeros(len(all_defaults), dtype=int) for id_cpp, cont_ in enumerate(all_defaults): tup_ = tuple(cont_) orders_[self._contingency_order[tup_]] = id_cpp else: # a list of interesting contingencies has been provided orders_ = np.zeros(len(args), dtype=int) all_defaults = [tuple(cont) for cont in all_defaults] for id_me, cont_ in enumerate(args): cont_li = self._single_cont_to_li_int(cont_) tup_ = tuple(cont_li) if tup_ not in self._contingency_order: raise RuntimeError(f"Contingency {cont_} is not simulated by this class. Have you called " f"`add_single_contingency` or `add_multiple_contingencies` ?") id_cpp = all_defaults.index(tup_) orders_[id_me] = id_cpp if not self.__computed: self.compute_V() self.compute_A() self.compute_P() return self._mws[orders_], self._ampss[orders_], self._vs[orders_]
[docs] def run(self) -> SecurityAnalysisResult: """ Run this contingency analysis and report, for the pre-contingency ("n") case and for each registered contingency, the list of limit violations (bus voltage out of [vmin_kv, vmax_kv], line/trafo current above limit_a1_ka / limit_a2_ka -- see `LSGrid.set_bus_voltage_limits` / `set_line_current_limit_side1` / `set_line_current_limit_side2` / `set_trafo_current_limit_side1` / `set_trafo_current_limit_side2`). This requires `compute_limit_violations=True` (either passed at construction time or set via ``this_instance.compute_limit_violations = True``), else a `RuntimeError` is raised. Prefer `run_ac` / `run_dc` if you want to also select the algorithm family. A `RuntimeError` is also raised if the pre-contingency ("n") powerflow itself does not converge -- every contingency is solved relative to this base case, so a diverging base case makes the whole analysis meaningless. The returned object mimics pypowsybl's security analysis result: .. code-block:: python res = contingency_analysis.run() for v in res.pre_contingency_result.limit_violations: ... for cont in res.post_contingency_results: # a list, ordered like `add_single_contingency` calls cont.element_ids # branch ids disconnected by this contingency (always present) cont.element_names # names (env.name_line) of these same elements (always present) cont.contingency_name # optional, user-supplied via add_single_contingency(..., name=...) cont.converged cont.limit_violations .. note:: A `converged == False` post-contingency entry has exactly one `LimitViolation` in `limit_violations`, with `element_type == ViolationElementType.GRID` and `violation_type` either `LimitViolationType.NOT_SIMULATED` (a pre-check skipped it, eg it splits the grid) or `LimitViolationType.DIVERGENCE` (the solver ran but did not converge) -- unlike a `converged == True` entry with an empty list, which genuinely means no violation was found. """ if self.__is_closed: raise RuntimeError("This is closed, you cannot use it.") if not self.computer.compute_limit_violations: raise RuntimeError("`run` (and `run_ac` / `run_dc`) require `compute_limit_violations=True`, " "set either at construction time (`ContingencyAnalysis(env, " "compute_limit_violations=True)`) or via " "`this_instance.compute_limit_violations = True`.") if not self.__computed: self.compute_V() all_defaults = self.computer.my_defaults() orders_ = np.zeros(len(all_defaults), dtype=int) for id_cpp, cont_ in enumerate(all_defaults): tup_ = tuple(cont_) orders_[self._contingency_order[tup_]] = id_cpp converged = self.computer.converged() violations = self.computer.get_violations() pre_contingency_result = PreContingencyResult( converged=self.computer.converged_n(), limit_violations=list(self.computer.get_violations_n()), ) post_contingency_results = [ ContingencyResult( element_ids=list(all_defaults[id_cpp]), element_names=[str(self._ls_backend.name_line[el_id]) for el_id in all_defaults[id_cpp]], contingency_name=self._contingency_names.get(self._all_contingencies[id_me]), converged=bool(converged[id_cpp]), limit_violations=list(violations[id_cpp]), ) for id_me, id_cpp in enumerate(orders_) ] return SecurityAnalysisResult(pre_contingency_result, post_contingency_results)
def _change_algorithm_family(self, want_dc: bool): """internal: switch to a default AC / DC algorithm, but only if the current one is not already of the requested family, so this does not needlessly `clear()` (and thus lose the registered contingencies) when it is already the case.""" current_is_dc = "DC_" in self.computer.get_algo_type().name if current_is_dc == want_dc: return preferred = (["DC_KLU", "DC_SparseLU"] if want_dc else ["NR_KLU", "NR_SparseLU"]) available = {a.name: a for a in self.computer.available_default_algorithms()} for name in preferred: if name in available: self.change_algorithm(available[name]) return raise RuntimeError(f"Impossible to find a default {'DC' if want_dc else 'AC'} algorithm " f"among {list(available.keys())}.")
[docs] def run_ac(self) -> SecurityAnalysisResult: """Like `run`, but first makes sure an AC algorithm is selected (switches to NR_KLU / NR_SparseLU if the current algorithm is a DC one -- which clears any previously registered contingency, exactly like `change_algorithm`; does nothing if already AC).""" self._change_algorithm_family(want_dc=False) return self.run()
[docs] def run_dc(self) -> SecurityAnalysisResult: """Like `run`, but first makes sure the DC algorithm is selected (switches to DC_KLU / DC_SparseLU if the current algorithm is an AC one -- which clears any previously registered contingency, exactly like `change_algorithm`; does nothing if already DC).""" self._change_algorithm_family(want_dc=True) return self.run()
[docs] def compute_V(self): """ This function allows to retrieve the complex voltage at each bus of the grid for each contingency. .. warning:: Order of the results The order in which the results are returned is NOT necessarily the order in which the contingencies have been entered. Please use `get_flows()` method for easier reading back of the results """ if self.__is_closed: raise RuntimeError("This is closed, you cannot use it.") if self._ls_backend._debug_Vdc is not None: v_init = self._ls_backend._debug_Vdc.copy() else: v_init = self._ls_backend.V.copy() self.computer.compute(v_init, self._ls_backend.max_it, self._ls_backend.tol) self._vs = self.computer.get_voltages().copy() self.__computed = True return self._vs
[docs] def compute_A(self): """ This function returns the current flows (in Amps, A) at the origin / high voltage side .. warning:: Order of the results The order in which the results are returned is NOT necessarily the order in which the contingencies have been entered. Please use `get_flows()` method for easier reading back of the results ! """ if self.__is_closed: raise RuntimeError("This is closed, you cannot use it.") if not self.__computed: raise RuntimeError("This function can only be used if compute_V has been sucessfully called") self.computer.compute_flows() self._ampss = 1e3 * self.computer.get_flows() return self._ampss
[docs] def compute_P(self): """ This function returns the active power flows (in MW) at the origin / high voltage side .. warning:: Order of the results The order in which the results are returned is NOT necessarily the order in which the contingencies have been entered. Please use `get_flows()` method for easier reading back of the results ! """ if self.__is_closed: raise RuntimeError("This is closed, you cannot use it.") if not self.__computed: raise RuntimeError("This function can only be used if compute_V has been sucessfully called") self.computer.compute_power_flows() self._mws = self.computer.get_power_flows().copy() return self._mws
[docs] def close(self): """permanently close the object""" if self.__is_closed: return self.clear() self.computer.close() self._ls_backend.close() self.__is_closed = True
def change_algorithm(self, solver_type): if self.__is_closed: raise RuntimeError("This is closed, you cannot use it.") self.computer.change_algorithm(solver_type) self.clear() def change_solver(self, solver_type): # kept as a backward-compatible alias of `change_algorithm` self.change_algorithm(solver_type)