Skip to content

Commit

Permalink
Merge pull request #576 from jinningwang/conm
Browse files Browse the repository at this point in the history
Development of Connectivity Manager
  • Loading branch information
cuihantao authored Nov 23, 2024
2 parents 2b17faa + 25318c6 commit 12135ed
Show file tree
Hide file tree
Showing 13 changed files with 1,019 additions and 4 deletions.
Binary file added andes/cases/ieee14/ieee14_conn.xlsx
Binary file not shown.
1 change: 1 addition & 0 deletions andes/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
160 changes: 160 additions & 0 deletions andes/core/connman.py
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions andes/models/bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 25 additions & 0 deletions andes/routines/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion andes/routines/eig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -492,6 +492,7 @@ def _pre_check(self):

return status

@check_conn_before_init
def run(self, **kwargs):
"""
Run small-signal stability analysis.
Expand Down
3 changes: 2 additions & 1 deletion andes/routines/pflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion andes/routines/tds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion andes/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions docs/source/release-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
-------------------
Expand Down
Loading

0 comments on commit 12135ed

Please sign in to comment.