diff --git a/andes/cases/ieee14/ieee14_conn.xlsx b/andes/cases/ieee14/ieee14_conn.xlsx new file mode 100644 index 000000000..ecc2b9b4e Binary files /dev/null and b/andes/cases/ieee14/ieee14_conn.xlsx differ diff --git a/andes/core/__init__.py b/andes/core/__init__.py index e4155d027..45c67c388 100644 --- a/andes/core/__init__.py +++ b/andes/core/__init__.py @@ -17,3 +17,4 @@ from andes.core.var import (Algeb, BaseVar, ExtAlgeb, ExtState, ExtVar, # NOQA State,) from andes.core.symprocessor import SymProcessor # NOQA +from andes.core.connman import ConnMan # NOQA diff --git a/andes/core/connman.py b/andes/core/connman.py new file mode 100644 index 000000000..d688b2d4d --- /dev/null +++ b/andes/core/connman.py @@ -0,0 +1,160 @@ +""" +Module for Connectivity Manager. +""" + +import logging +from collections import OrderedDict + +from andes.utils.func import list_flatten +from andes.shared import np + +logger = logging.getLogger(__name__) + + +# connectivity dependencies of `Bus` +# NOTE: only include PFlow models and measurements models +# cause online status of dynamic models are expected to be handled by their +# corresponding static models +# TODO: DC Topologies are not included yet, `Node`, etc +bus_deps = OrderedDict([ + ('ACLine', ['bus1', 'bus2']), + ('ACShort', ['bus1', 'bus2']), + ('FreqMeasurement', ['bus']), + ('Interface', ['bus']), + ('Motor', ['bus']), + ('PhasorMeasurement', ['bus']), + ('StaticACDC', ['bus']), + ('StaticGen', ['bus']), + ('StaticLoad', ['bus']), + ('StaticShunt', ['bus']), +]) + + +class ConnMan: + """ + Define a Connectivity Manager class for System. + + Connectivity Manager is used to automatically **turn off** + attached devices when a ``Bus`` is turned off **after** system + setup and **before** TDS initializtion. + + Attributes + ---------- + system: system object + System object to manage the connectivity. + busu0: ndarray + Last recorded bus connection status. + is_needed: bool + Flag to indicate if connectivity update is needed. + changes: dict + Dictionary to record bus connectivity changes ('on' and 'off'). + 'on' means the bus is previous offline and now online. + 'off' means the bus is previous online and now offline. + """ + + def __init__(self, system=None): + """ + Initialize the connectivity manager. + + Parameters + ---------- + system: system object + System object to manage the connectivity. + """ + self.system = system + self.busu0 = None # placeholder for Bus.u.v + self.is_needed = False # flag to indicate if check is needed + self.changes = {'on': None, 'off': None} # dict of bus connectivity changes + + def init(self): + """ + Initialize the connectivity. + + `ConnMan` is initialized in `System.setup()`, where all buses are considered online + by default. This method records the initial bus connectivity. + """ + # NOTE: here, we expect all buses are online before the system setup + self.busu0 = np.ones(self.system.Bus.n, dtype=int) + self.changes['on'] = np.zeros(self.system.Bus.n, dtype=int) + self.changes['off'] = np.logical_and(self.busu0 == 1, self.system.Bus.u.v == 0).astype(int) + + if np.any(self.changes['off']): + self.is_needed = True + + self.act() + + return True + + def _update(self): + """ + Helper function for in-place update of bus connectivity. + """ + self.changes['on'][...] = np.logical_and(self.busu0 == 0, self.system.Bus.u.v == 1) + self.changes['off'][...] = np.logical_and(self.busu0 == 1, self.system.Bus.u.v == 0) + self.busu0[...] = self.system.Bus.u.v + + def record(self): + """ + Record the bus connectivity in-place. + + This method should be called if `Bus.set()` or `Bus.alter()` is called. + """ + self._update() + + if np.any(self.changes['on']): + onbus_idx = [self.system.Bus.idx.v[i] for i in np.nonzero(self.changes["on"])[0]] + logger.warning(f'Bus turned on: {onbus_idx}') + self.is_needed = True + if len(onbus_idx) > 0: + raise NotImplementedError('Turning on bus after system setup is not supported yet!') + + if np.any(self.changes['off']): + offbus_idx = [self.system.Bus.idx.v[i] for i in np.nonzero(self.changes["off"])[0]] + logger.warning(f'Bus turned off: {offbus_idx}') + self.is_needed = True + + return self.changes + + def act(self): + """ + Update the connectivity. + """ + if not self.is_needed: + logger.debug('No need to update connectivity.') + return True + + if self.system.TDS.initialized: + raise NotImplementedError('Bus connectivity update during TDS is not supported yet!') + + # --- action --- + offbus_idx = [self.system.Bus.idx.v[i] for i in np.nonzero(self.changes["off"])[0]] + + # skip if no bus is turned off + if len(offbus_idx) == 0: + return True + + logger.warning('Entering connectivity update.') + logger.warning(f'Following bus(es) are turned off: {offbus_idx}') + + logger.warning('-> System connectivity update results:') + for grp_name, src_list in bus_deps.items(): + devices = [] + for src in src_list: + grp_devs = self.system.__dict__[grp_name].find_idx(keys=src, values=offbus_idx, + allow_none=True, allow_all=True, + default=None) + grp_devs_flat = list_flatten(grp_devs) + if grp_devs_flat != [None]: + devices.append(grp_devs_flat) + + devices_flat = list_flatten(devices) + + if len(devices_flat) > 0: + self.system.__dict__[grp_name].set(src='u', attr='v', + idx=devices_flat, value=0) + logger.warning(f'In <{grp_name}>, turn off {devices_flat}') + + self.is_needed = False # reset the action flag + self._update() # update but not record + self.system.connectivity(info=True) + return True diff --git a/andes/models/bus.py b/andes/models/bus.py index 643d172f2..063c299dd 100644 --- a/andes/models/bus.py +++ b/andes/models/bus.py @@ -124,3 +124,32 @@ def __init__(self, system=None, config=None): '(1-flat_start)*a0' self.v.v_str = 'flat_start*1 + ' \ '(1-flat_start)*v0' + + def set(self, src, idx, attr, value): + super().set(src=src, idx=idx, attr=attr, value=value) + _check_conn_status(system=self.system, src=src, attr=attr) + + +def _check_conn_status(system, src, attr): + """ + Helper function to determine if connectivity update is needed. + + Parameters + ---------- + system : System + The system object. + src : str + Name of the model property + attr : str + The internal attribute of the property to get. + """ + # Check if connectivity update is required + if src == 'u' and attr == 'v': + if system.is_setup: + system.conn.record() # Record connectivity once setup is confirmed + + if not system.TDS.initialized: + # Log a warning if Power Flow needs resolution before EIG or TDS + if system.PFlow.converged: + logger.warning('Bus connectivity is touched, resolve PFlow before running EIG or TDS!') + system.PFlow.converged = False # Flag Power Flow as not converged diff --git a/andes/routines/base.py b/andes/routines/base.py index d27f0d942..e3e637186 100644 --- a/andes/routines/base.py +++ b/andes/routines/base.py @@ -7,6 +7,31 @@ from collections import OrderedDict +def check_conn_before_init(func): + """ + A decorator that ensures the connection is active before calling the `init` or `run` + method of a `BaseRoutine` derived class. + + This decorator calls the `act` method on `self.system.conn` to ensure the connection + is active before proceeding with the initialization. + + Parameters + ---------- + func : function + The `init` method of a `BaseRoutine` derived class. + + Returns + ------- + function + The wrapped function with connection check. + """ + + def wrapper(self, *args, **kwargs): + self.system.conn.act() + return func(self, *args, **kwargs) + return wrapper + + class BaseRoutine: """ Base routine class. diff --git a/andes/routines/eig.py b/andes/routines/eig.py index 94363688d..9f0b04787 100644 --- a/andes/routines/eig.py +++ b/andes/routines/eig.py @@ -12,7 +12,7 @@ from andes.io.txt import dump_data from andes.plot import set_latex, set_style -from andes.routines.base import BaseRoutine +from andes.routines.base import BaseRoutine, check_conn_before_init from andes.shared import div, matrix, plt, sparse, spdiag, spmatrix from andes.utils.misc import elapsed from andes.variables.report import report_info @@ -492,6 +492,7 @@ def _pre_check(self): return status + @check_conn_before_init def run(self, **kwargs): """ Run small-signal stability analysis. diff --git a/andes/routines/pflow.py b/andes/routines/pflow.py index 54d01c974..452da9133 100644 --- a/andes/routines/pflow.py +++ b/andes/routines/pflow.py @@ -6,7 +6,7 @@ from collections import OrderedDict from andes.utils.misc import elapsed -from andes.routines.base import BaseRoutine +from andes.routines.base import BaseRoutine, check_conn_before_init from andes.variables.report import Report from andes.shared import np, matrix, sparse, newton_krylov @@ -63,6 +63,7 @@ def __init__(self, system=None, config=None): self.x_sol = None self.y_sol = None + @check_conn_before_init def init(self): """ Initialize variables for power flow. diff --git a/andes/routines/tds.py b/andes/routines/tds.py index d55da6f8c..06523c8a7 100644 --- a/andes/routines/tds.py +++ b/andes/routines/tds.py @@ -9,7 +9,7 @@ import time from collections import OrderedDict -from andes.routines.base import BaseRoutine +from andes.routines.base import BaseRoutine, check_conn_before_init from andes.routines.daeint import Trapezoid, method_map from andes.routines.criteria import deltadelta from andes.shared import matrix, np, pd, spdiag, tqdm, tqdm_nb @@ -174,6 +174,7 @@ def __init__(self, system=None, config=None): self.method = Trapezoid() self.set_method(self.config.method) + @check_conn_before_init def init(self): """ Initialize the status, storage and values for TDS. diff --git a/andes/system.py b/andes/system.py index 2497fc1a6..00682baac 100644 --- a/andes/system.py +++ b/andes/system.py @@ -23,7 +23,7 @@ from typing import Dict, Optional, Tuple, Union import andes.io -from andes.core import AntiWindup, Config, Model +from andes.core import AntiWindup, Config, Model, ConnMan from andes.io.streaming import Streaming from andes.models import file_classes from andes.models.group import GroupBase @@ -193,6 +193,7 @@ def __init__(self, self.files = FileMan(case=case, **self.options) # file path manager self.dae = DAE(system=self) # numerical DAE storage self.streaming = Streaming(self) # Dime2 streaming + self.conn = ConnMan(system=self) # connectivity manager # dynamic imports of groups, models and routines self.import_groups() @@ -488,6 +489,9 @@ def setup(self): self.store_sparse_pattern(self.exist.pflow) self.store_adder_setter(self.exist.pflow) + # init connectivity manager + self.conn.init() + if ret is True: self.is_setup = True # set `is_setup` if no error occurred else: diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 75602f755..64d2992a9 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -11,6 +11,15 @@ v1.9 Notes v1.9.3 (2024-04-XX) ------------------- +Development of connectivity manager `ConnMan`: + +- Add case `ieee14_conn.xlsx` for demonstration. +- Add `ConnMan` class to manage connectivity. +- Add `ConnMan` to `System` to as an attribute `conn`. +- Add a demo notebook for `ConnMan`. + +Other changes: + - In the ``dae`` module, change `self.t.itemset` to array assignment to ensure compatibility with NumPy 2.0. - Follow RTD's deprecation of Sphinx context injection at build time - In symbolic processor, most variables are assumed to be real, except some @@ -22,6 +31,7 @@ v1.9.3 (2024-04-XX) - Add parameter `allow_all=False` to `ModelData.find_idx()` `GroupBase.find_idx()` to allow searching all matches. - Add method `GroupBase.get_all_idxes()` to get all indices of a group. - Enhanced three-winding transformer parsing in PSS/E raw files by assigning the equivalent star bus `area`, `owner`, and `zone` using the high-voltage bus values. +- Specify `multiprocess <=0.70.16` in requirements as 0.70.17 does not support Linux. v1.9.2 (2024-03-25) ------------------- diff --git a/examples/demonstration/ConnMan.ipynb b/examples/demonstration/ConnMan.ipynb new file mode 100644 index 000000000..7d59bf3cd --- /dev/null +++ b/examples/demonstration/ConnMan.ipynb @@ -0,0 +1,715 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Connectivity Manager" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In version 1.9.3, `ConnMan` is introduced to manage the bus related connectivity changes.\n", + "\n", + "It is an attribute `conn` of the `System` instance." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import andes" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "andes.config_logger(stream_level=20)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scenario 1\n", + "\n", + "For buses that are turned off by case file, the connectivity is updated in the `System.setup()`.\n", + "\n", + "We can see that the connected devices to off-line buses are turned off." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Working directory: \"/Users/jinningwang/work/andes/examples/demonstration\"\n", + "> Loaded generated Python code in \"/Users/jinningwang/.andes/pycode\".\n", + "Generated code for is stale.\n", + "Numerical code generation (rapid incremental mode) started...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating code for 1 models on 12 processes.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Saved generated pycode to \"/Users/jinningwang/.andes/pycode\"\n", + "> Reloaded generated Python code of module \"pycode\".\n", + "Generated numerical code for 1 models in 0.1019 seconds.\n", + "Parsing input file \"/Users/jinningwang/work/andes/andes/cases/ieee14/ieee14_conn.xlsx\"...\n", + "Input file parsed in 0.2000 seconds.\n", + "Entering connectivity update.\n", + "Following bus(es) are turned off: [15]\n", + "-> System connectivity update results:\n", + "In , turn off ['Line_21']\n", + "In , turn off [6]\n", + "In , turn off ['PQ_12']\n", + "In , turn off ['Shunt_3']\n", + "-> System connectivity check results:\n", + " 1 islanded bus detected.\n", + " System is interconnected.\n", + " Each island has a slack bus correctly defined and enabled.\n", + "System internal structure set up in 0.0207 seconds.\n" + ] + } + ], + "source": [ + "ss = andes.load(andes.get_case('ieee14/ieee14_conn.xlsx'),\n", + " setup=True, no_output=True, default_config=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "-> System connectivity check results:\n", + " 1 islanded bus detected.\n", + " System is interconnected.\n", + " Each island has a slack bus correctly defined and enabled.\n", + "\n", + "-> Power flow calculation\n", + " Numba: Off\n", + " Sparse solver: KLU\n", + " Solution method: NR method\n", + "Power flow initialized in 0.0109 seconds.\n", + "0: |F(x)| = 0.5605182134\n", + "1: |F(x)| = 0.006202200332\n", + "2: |F(x)| = 5.819382825e-06\n", + "3: |F(x)| = 6.964193111e-12\n", + "Converged in 4 iterations in 0.0106 seconds.\n" + ] + } + ], + "source": [ + "_ = ss.PFlow.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected, there is no power injection from the turned off devices.\n", + "\n", + "Note: `Line.a1.e` is the active power injection at the from_bus (`bus1`)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ss.Line.get(src='a1', attr='e', idx=['Line_21'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`PV.a.e` is the active power injection at the connected bus." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([-0.])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ss.PV.get(src='a', attr='e', idx=[6])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similarly, `PQ.a.e` is the active power injection at the connected bus." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.])" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ss.PQ.get(src='a', attr='e', idx=['PQ_12'])" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Initialization for dynamics completed in 0.0181 seconds.\n", + "Initialization was successful.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d2b6e221b5034f9a914bd9fd1bbc5c70", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/100 [00:00 Reloaded generated Python code of module \"pycode\".\n", + "Generated code for is stale.\n", + "Numerical code generation (rapid incremental mode) started...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generating code for 1 models on 12 processes.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Saved generated pycode to \"/Users/jinningwang/.andes/pycode\"\n", + "> Reloaded generated Python code of module \"pycode\".\n", + "Generated numerical code for 1 models in 0.0971 seconds.\n", + "Parsing input file \"/Users/jinningwang/work/andes/andes/cases/ieee14/ieee14_conn.xlsx\"...\n", + "Input file parsed in 0.0676 seconds.\n" + ] + } + ], + "source": [ + "ss = andes.load(andes.get_case('ieee14/ieee14_conn.xlsx'),\n", + " setup=False, no_output=True, default_config=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, let's turn on the bus manually.\n", + "\n", + "> Note: it will ***not*** touch the connectivity manager to update bus online status before system setup." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "ss.Bus.set(src='u', attr='v', idx=15, value=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "System internal structure set up in 0.0202 seconds.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ss.setup()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "-> System connectivity check results:\n", + " No islanded bus detected.\n", + " System is interconnected.\n", + " Each island has a slack bus correctly defined and enabled.\n", + "\n", + "-> Power flow calculation\n", + " Numba: Off\n", + " Sparse solver: KLU\n", + " Solution method: NR method\n", + "Power flow initialized in 0.0179 seconds.\n", + "0: |F(x)| = 0.7800160444\n", + "1: |F(x)| = 0.03157368515\n", + "2: |F(x)| = 4.343734372e-05\n", + "3: |F(x)| = 8.270892304e-11\n", + "Converged in 4 iterations in 0.0045 seconds.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ss.PFlow.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, let's make a change on the parameter `Bus.name`." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "ss.Bus.set(src='name', attr='v', idx=15, value='BUSTEST')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Such a change will not set the connectivity action flag." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ss.conn.is_needed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In contrast, if changes are made to `Bus.u.v`, the connectivity action flag is set." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Bus turned off: [15]\n", + "Bus connectivity is touched, resolve PFlow before running EIG or TDS!\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ss.Bus.set(src='u', attr='v', idx=15, value=0)\n", + "ss.conn.is_needed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Remember to run the `PFlow` again if updates are made to the bus online status,\n", + "since this will reset the `PFlow` convergence flag." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ss.PFlow.converged" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "-> System connectivity check results:\n", + " No islanded bus detected.\n", + " System is interconnected.\n", + " Each island has a slack bus correctly defined and enabled.\n", + "\n", + "-> Power flow calculation\n", + " Numba: Off\n", + " Sparse solver: KLU\n", + " Solution method: NR method\n", + "Entering connectivity update.\n", + "Following bus(es) are turned off: [15]\n", + "-> System connectivity update results:\n", + "In , turn off ['Line_21']\n", + "In , turn off [6]\n", + "In , turn off ['PQ_12']\n", + "In , turn off ['Shunt_3']\n", + "-> System connectivity check results:\n", + " 1 islanded bus detected.\n", + " System is interconnected.\n", + " Each island has a slack bus correctly defined and enabled.\n", + "Power flow initialized in 0.0051 seconds.\n", + "0: |F(x)| = 0.5605182134\n", + "1: |F(x)| = 0.006202200332\n", + "2: |F(x)| = 5.819382825e-06\n", + "3: |F(x)| = 6.964193111e-12\n", + "Converged in 4 iterations in 0.0096 seconds.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ss.PFlow.run()" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Initialization for dynamics completed in 0.0208 seconds.\n", + "Initialization was successful.\n", + "\n", + "-> Eigenvalue Analysis:\n", + "4 states are associated with zero time constants. \n", + " Positive 0\n", + " Zeros 1\n", + " Negative 62\n", + "Eigenvalue analysis finished in 0.0017 seconds.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ss.EIG.run()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "-> Time Domain Simulation Summary:\n", + "Sparse Solver: KLU\n", + "Simulation time: 0.0-20.0 s.\n", + "Fixed step size: h=33.33 ms. Shrink if not converged.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "196c92bd10734959b35aa9a27743f2d0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/100 [00:00 **NOTE:** Turn on a bus will not automatically turn on the connected devices." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are two cases that are not supported yet:\n", + "\n", + "1. It is not supported to turn on a bus after successfully setting up a system.\n", + "1. It is not supported to turn on or off a bus after TDS initialization." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ams", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/requirements.txt b/requirements.txt index b7f2ef4c5..e2474fd24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ chardet psutil texttable numba +multiprocess <=0.70.16 diff --git a/tests/test_conn.py b/tests/test_conn.py new file mode 100644 index 000000000..1e5db8686 --- /dev/null +++ b/tests/test_conn.py @@ -0,0 +1,67 @@ +import unittest + +import numpy as np + +import andes + + +class TestConnMan(unittest.TestCase): + """ + Test class `ConnMan`. + """ + + def setUp(self) -> None: + self.ss = andes.load(andes.get_case("ieee14/ieee14_conn.xlsx"), + setup=True, default_config=True, no_output=True) + + def test_conn_init(self): + """ + Test `ConnMan` initialization. + """ + # normally, flag `is_needed` should be False after successful init + self.assertFalse(self.ss.conn.is_needed) + + self.assertIsInstance(self.ss.conn.busu0, np.ndarray) + self.assertEqual(self.ss.conn.busu0.shape, (self.ss.Bus.n,)) + + self.assertIsInstance(self.ss.conn.changes, dict) + self.assertEqual(self.ss.conn.changes['on'].shape, (self.ss.Bus.n,)) + self.assertEqual(self.ss.conn.changes['off'].shape, (self.ss.Bus.n,)) + + def test_turn_off(self): + """ + Test if connected devices are turned off. + """ + # assert there is an offline bus + self.assertEqual(self.ss.Bus.get(src='u', attr='v', idx=15), 0) + + # assert connected devices are turned off + self.assertEqual(self.ss.Line.get(src='u', attr='v', idx='Line_21'), 0) + self.assertEqual(self.ss.StaticGen.get(src='u', attr='v', idx=6), 0) + self.assertEqual(self.ss.StaticLoad.get(src='u', attr='v', idx='PQ_12'), 0) + self.assertEqual(self.ss.StaticShunt.get(src='u', attr='v', idx='Shunt_3'), 0) + + def test_turn_off_after_pflow(self): + """ + Test if `ConnMan` works after solving PFlow. + """ + ss = andes.load(andes.get_case('ieee14/ieee14_conn.xlsx'), + setup=False, no_output=True, default_config=True) + ss.Bus.set(src='u', attr='v', idx=15, value=1) + ss.setup() + + ss.PFlow.run() + self.assertTrue(ss.PFlow.converged) + + # turn off a bus + ss.Bus.alter(src='u', idx=15, value=0) + # flag PFlow.converged should be reset as False by `Bus.set()` + self.assertFalse(ss.PFlow.converged) + self.assertTrue(ss.conn.is_needed) + + def test_turn_on_after_setup(self): + """ + Test if raise NotImplementedError when turning on a bus after system setup. + """ + with self.assertRaises(NotImplementedError): + self.ss.Bus.set(src='u', attr='v', idx=15, value=1)