Source code for andes.models.group

import logging
import inspect
import warnings
from collections import OrderedDict

import numpy as np

from andes.core.service import BackRef
from andes.shared import pd
from andes.utils.func import list_flatten, validate_keys_values

logger = logging.getLogger(__name__)


[docs]class GroupBase: """ Base class for groups. """
[docs] def __init__(self): self.common_params = ['u', 'name'] self.common_vars = [] self.models = OrderedDict() # model name: model instance self._idx2model = OrderedDict() # element idx: model instance self.uid = {} # idx - group internal 0-indexed uid self.services_ref = OrderedDict() # BackRef
def __setattr__(self, key, value): if hasattr(value, 'owner'): if value.owner is None: value.owner = self if hasattr(value, 'name'): if value.name is None: value.name = key if isinstance(value, BackRef): self.services_ref[key] = value super().__setattr__(key, value) @property def class_name(self): return self.__class__.__name__ @property def n(self): """ Total number of devices. """ return len(self._idx2model)
[docs] def add_model(self, name: str, instance): """ Add a Model instance to group. Parameters ---------- name : str Model name instance : Model Model instance Returns ------- None """ if name not in self.models: self.models[name] = instance else: raise KeyError(f"{self.class_name}: Duplicate model registration of {name}")
[docs] def add(self, idx, model): """ Register an idx from model_name to the group Parameters ---------- idx: Union[str, float, int] Register an element to a model model: Model instance of the model Returns ------- """ if idx in self._idx2model: raise KeyError(f'Group <{self.class_name}> already contains <{repr(idx)}> from ' f'<{self._idx2model[idx].class_name}>') self.uid[idx] = self.n self._idx2model[idx] = model
[docs] def idx2model(self, idx, allow_none=False): """ Find model name for the given idx. Parameters ---------- idx : float, int, str, array-like idx or idx-es of devices. allow_none : bool If True, return `None` at the positions where idx is not found. Returns ------- If `idx` is a list, return a list of model instances. If `idx` is a single element, return a model instance. """ ret = [] idx, single = self._1d_vectorize(idx) for i in idx: try: if i is None and allow_none: ret.append(None) else: ret.append(self._idx2model[i]) except KeyError: raise KeyError(f'Group <{self.class_name}> does not contain device with idx={i}') if single: ret = ret[0] return ret
[docs] def idx2uid(self, idx): """ Convert idx to the 0-indexed unique index. Parameters ---------- idx : array-like, numbers, or str idx of devices Returns ------- list A list containing the unique indices of the devices """ vec_idx, single = self._1d_vectorize(idx) out = [self.uid[i] if i is not None else None for i in vec_idx] if single: out = out[0] return out
[docs] def get(self, src: str, idx, attr: str = 'v', allow_none=False, default=0.0): """ Based on the indexer, get the `attr` field of the `src` parameter or variable. Parameters ---------- src : str param or var name idx : array-like device idx attr The attribute of the param or var to retrieve allow_none : bool True to allow None values in the indexer default : float If `allow_none` is true, the default value to use for None indexer. Returns ------- The requested param or variable attribute. If `idx` is a list, return a list of values. If `idx` is a single element, return a single value. """ self._check_src(src) self._check_idx(idx) idx, single = self._1d_vectorize(idx) n = len(idx) if n == 0: return np.zeros(0) ret = [''] * n _type_set = False models = self.idx2model(idx, allow_none=allow_none) for i, idx in enumerate(idx): if models[i] is not None: uid = models[i].idx2uid(idx) instance = models[i].__dict__[src] val = instance.__dict__[attr][uid] else: val = default # deduce the type for ret if not _type_set: if isinstance(val, str): ret = [''] * n else: ret = np.zeros(n) _type_set = True ret[i] = val if single: ret = ret[0] return ret
# Sentinel for distinguishing "not passed" from None/0/False _SENTINEL = object()
[docs] def set(self, src: str, idx, *args, value=_SENTINEL, attr='v', base=None): """ Set the value of a group property, dispatching to the correct model. Delegates to each model's :meth:`Model.set`. See its docstring for details on ``base`` and ``attr``. Parameters ---------- src : str Name of property. idx : str, int, float, array-like Indices of devices. value : float or array-like New values to be set. Can be passed as the third positional argument or as a keyword. attr : str, optional Attribute to write (default ``'v'``). Ignored when ``base='device'``. base : ``None`` or ``'device'``, optional ``None`` (default): system-base direct write. ``'device'``: device/input-base with per-unit conversion. Returns ------- bool True when successful. """ # ---- Parse arguments (backward compat + new style) ---- _VALID_ATTRS = {'v', 'a', 'e', 'vin'} _S = self._SENTINEL if len(args) == 2 and isinstance(args[0], str) and args[0] in _VALID_ATTRS: # Old-style positional: set('M', 1, 'v', 10.0) warnings.warn( "set(src, idx, attr, value) is deprecated. " f"Use set('{src}', {idx!r}, {args[1]!r}, attr='{args[0]}') instead.", FutureWarning, stacklevel=2 ) attr = args[0] actual_value = args[1] elif len(args) == 1 and value is _S: actual_value = args[0] elif len(args) == 0 and value is not _S: actual_value = value elif len(args) == 0 and value is _S: raise TypeError( f"set() missing 'value'. " f"Usage: ss.<Group>.set('{src}', {idx!r}, <value>)" ) else: raise TypeError( "set() got unexpected arguments. " "Usage: set(src, idx, value, *, attr='v', base=None)" ) self._check_src(src) self._check_idx(idx) idx, _ = self._1d_vectorize(idx) models = self.idx2model(idx) if isinstance(actual_value, (str, int, float, np.integer, np.floating)): actual_value = [actual_value] * len(idx) for mdl, ii, val in zip(models, idx, actual_value): mdl.set(src, ii, val, attr=attr, base=base) return True
[docs] def set_status(self, idx, value): """ Set the online status of a device and propagate to dependents. Delegates to the concrete model's :meth:`set_status` which calls :meth:`System.set_status` for propagation. Parameters ---------- idx : str, int, float Device idx. value : int or float New status value (0 or 1). """ mdl = self.idx2model(idx) return mdl.set_status(idx, value)
[docs] def get_status(self, idx): """ Get the effective online status of a device. Parameters ---------- idx : str, int, float Device idx. Returns ------- float Effective status value (0 or 1). """ mdl = self.idx2model(idx) return mdl.get_status(idx)
[docs] def alter(self, src, idx, value, attr='v'): """ Alter values of input parameters or constant service for a group of models. .. deprecated:: Use :meth:`set` with ``base='device'`` instead. Parameters ---------- src : str The parameter name to alter idx : str, float, int The unique identifier for the device to alter value : float The desired value attr : str, optional The attribute to alter. Default is 'v'. """ warnings.warn( "alter() is deprecated. Use set() with base='device' instead.", FutureWarning, stacklevel=2 ) self._check_src(src) self._check_idx(idx) idx, _ = self._1d_vectorize(idx) models = self.idx2model(idx) if isinstance(value, (str, int, float, np.integer, np.floating)): value = [value] * len(idx) for mdl, ii, val in zip(models, idx, value): # Call Model.alter directly (which handles the attr='vin' legacy) mdl.alter(src, ii, val, attr=attr) return True
[docs] def find_idx(self, keys, values, allow_none=False, default=None, allow_all=False): """ Find indices of devices that satisfy the given `key=value` condition. This method iterates over all models in this group. Parameters ---------- keys : str, array-like, Sized A string or an array-like of strings containing the names of parameters for the search criteria. values : array, array of arrays, Sized Values for the corresponding key to search for. If keys is a str, values should be an array of elements. If keys is a list, values should be an array of arrays, each corresponding to the key. allow_none : bool, optional Allow key, value to be not found. Used by groups. Default is False. default : bool, optional Default idx to return if not found (missing). Default is None. allow_all : bool, optional Return all matches if set to True. Default is False. Returns ------- list Indices of devices. """ keys, values = validate_keys_values(keys, values) n_mdl, n_pair = len(self.models), len(values[0]) indices_found = [] # `indices_found` contains found indices returned from all models of this group for model in self.models.values(): indices_found.append(model.find_idx(keys, values, allow_none=True, default=default, allow_all=True)) # --- find missing pairs --- i_val_miss = [] for i in range(n_pair): idx_cross_mdls = [indices_found[j][i] for j in range(n_mdl)] if all(item == [default] for item in idx_cross_mdls): i_val_miss.append(i) if (not allow_none) and i_val_miss: miss_pairs = [] for i in i_val_miss: miss_pairs.append([values[j][i] for j in range(len(keys))]) raise IndexError(f'No {self.class_name} device found matching {list(keys)} = {miss_pairs}') # --- output --- out_pre = [] for i in range(n_pair): idx_cross_mdls = [indices_found[j][i] for j in range(n_mdl)] if all(item == [default] for item in idx_cross_mdls): out_pre.append([default]) continue for item in idx_cross_mdls: if item != [default]: out_pre.append(item) break if allow_all: out = out_pre else: out = [item[0] for item in out_pre] return out
def _check_src(self, src: str): """ Helper function for checking if ``src`` is a shared field. The requirement is not strictly enforced and is only for debugging purposed. """ if src not in self.common_vars + self.common_params: logger.debug(f'Group <{self.class_name}> does not share property <{src}>.') def _check_idx(self, idx): """ Helper function for checking if ``idx`` is None. Raises IndexError if idx is None. """ if idx is None: raise IndexError(f'{self.class_name}: idx cannot be None') def _1d_vectorize(self, idx): """ Helper function to convert a single element, list, or nested lists into a list. If the input is a nested list, flatten it into a 1-dimensional list. Returns ------- idx : list List of indices. single : bool True if the input is a single element. """ single = False list_alike = (list, tuple, np.ndarray) if not isinstance(idx, list_alike): idx = [idx] single = True elif len(idx) > 0 and isinstance(idx[0], list_alike): idx = list_flatten(idx) return idx, single
[docs] def get_field(self, src: str, idx, field: str): """ Helper function for retrieving an attribute of a member variable shared by models in this group. Returns ------- list A list with the length equal to ``len(idx)``. """ self._check_src(src) self._check_idx(idx) idx, _ = self._1d_vectorize(idx) models = self.idx2model(idx, allow_none=True) ret = [None] * len(models) for ii, model in enumerate(models): if model is not None: ret[ii] = getattr(model.__dict__[src], field) return ret
def _find_controller(self, system, idx, controller_group, param_name): """ Find the controller device in ``controller_group`` whose ``param_name`` points to ``idx`` in this group. Uses a lazily-built reverse lookup cache for O(1) repeated calls. Parameters ---------- system : System The system instance. idx : str, int, float Device idx in this group. controller_group : str Name of the controller group (e.g., ``'TurbineGov'``). param_name : str Name of the IdxParam on the controller that references this group (e.g., ``'syn'``). Returns ------- tuple ``(model_instance, controller_idx)`` if found, ``(None, None)`` otherwise. """ if not hasattr(self, '_ctrl_cache'): self._ctrl_cache = {} cache_key = (controller_group, param_name) if cache_key not in self._ctrl_cache: lookup = {} grp = system.groups.get(controller_group) if grp is not None and grp.n > 0: for mdl in grp.models.values(): param = mdl.__dict__.get(param_name) if param is None: continue for uid, ctrl_idx in enumerate(mdl.idx.v): target_idx = param.v[uid] lookup[target_idx] = ctrl_idx self._ctrl_cache[cache_key] = lookup ctrl_idx = self._ctrl_cache[cache_key].get(idx) if ctrl_idx is None: return None, None grp = system.groups[controller_group] ctrl_mdl = grp.idx2model(ctrl_idx) return ctrl_mdl, ctrl_idx def _resolve_setpoint(self, system, idx, name): """ Resolve the controller chain for a setpoint and return the target ``(model, device_idx)``. Searches ``_setpoint_priority`` (defined on subclasses) for a connected controller that declares ``name`` in its ``_setpoints``. Falls back to the device itself. ``_setpoint_priority`` is a class-level dict on group subclasses that maps setpoint names to a list of ``(controller_group, param_name)`` tuples to search, in order. ``param_name`` is the IdxParam on the controller that references this group (e.g., ``'syn'`` for TurbineGov/Exciter, ``'reg'`` for RenExciter). These param names are enforced by each controller group's ``common_params``. Example on SynGen:: _setpoint_priority = { 'pref': [('TurbineGov', 'syn')], 'vref': [('Exciter', 'syn')], } Parameters ---------- system : System The system instance. idx : str, int, float Device idx in this group. name : str Setpoint key (e.g., ``'pref'``, ``'vref'``). Returns ------- tuple ``(model_instance, device_idx)`` Raises ------ KeyError If neither a controller nor the device itself declares the requested setpoint. """ priority = getattr(self, '_setpoint_priority', {}) for ctrl_group, ctrl_param in priority.get(name, []): ctrl_mdl, ctrl_idx = self._find_controller( system, idx, ctrl_group, ctrl_param ) if ctrl_mdl is not None: sp_map = getattr(ctrl_mdl, '_setpoints', {}) if name in sp_map: return ctrl_mdl, ctrl_idx # Fallback: the device itself mdl = self.idx2model(idx) sp_map = getattr(mdl, '_setpoints', {}) if name not in sp_map: raise KeyError( f"No controller found for {self.class_name} idx={idx!r} " f"setpoint '{name}', and <{mdl.class_name}> does not " f"declare '{name}' in _setpoints." ) if priority.get(name): logger.warning( "No %s controller found for %s idx=%r. " "Writing '%s' directly to <%s>.", priority[name][0][0], self.class_name, idx, name, mdl.class_name, ) return mdl, idx
[docs] def set_setpoint(self, system, idx, name, value): """ Set a setpoint value by resolving the controller chain. Searches for a connected controller (e.g., TurbineGov for ``'pref'``, Exciter for ``'vref'``) and writes there. Falls back to the device itself with a warning. .. note:: The physical meaning of a setpoint depends on the specific controller model that owns it. For example, ``'vref'`` writes to the exciter's voltage reference input, which may include compensation terms (e.g., EXAC4 initializes ``vref0 = v + vf0/KA``). It is **not** necessarily the exact terminal voltage. Similarly, ``'pref'`` is the governor's power reference in system-base per-unit. Use :meth:`get_setpoint` to read the current value before applying incremental changes. Parameters ---------- system : System The system instance. idx : str, int, float Device idx in this group. name : str Setpoint key (e.g., ``'pref'``, ``'vref'``, ``'qref'``). value : float Value to write (absolute, in system-base per-unit). """ mdl, dev_idx = self._resolve_setpoint(system, idx, name) attr_name = mdl._setpoints[name] uid = mdl.idx2uid(dev_idx) mdl.__dict__[attr_name].v[uid] = value
[docs] def get_setpoint(self, system, idx, name): """ Get the current setpoint value by resolving the controller chain. See :meth:`set_setpoint` for notes on the physical meaning of setpoint values. Parameters ---------- system : System The system instance. idx : str, int, float Device idx in this group. name : str Setpoint key (e.g., ``'pref'``, ``'vref'``, ``'qref'``). Returns ------- float Current value of the setpoint (system-base per-unit). """ mdl, dev_idx = self._resolve_setpoint(system, idx, name) attr_name = mdl._setpoints[name] uid = mdl.idx2uid(dev_idx) return mdl.__dict__[attr_name].v[uid]
[docs] def set_backref(self, name, from_idx, to_idx): """ Set idxes to ``BackRef``, and set them to models. """ uid = self.idx2uid(to_idx) self.services_ref[name].v[uid].append(from_idx) model = self.idx2model(to_idx) model.set_backref(name, from_idx, to_idx)
[docs] def get_next_idx(self, idx=None, model_name=None): """ Get a no-conflict idx for a new device. Use the provided ``idx`` if no conflict. Generate a new one otherwise. Parameters ---------- idx : str or None Proposed idx. If None, assign a new one. model_name : str or None Model name. If not, prepend the group name. Returns ------- str New device name. """ if model_name is None: model_name = self.class_name need_new = False if idx is not None: if idx not in self._idx2model: # name is good pass else: logger.warning("Group <%s>: idx=%s is used by %s. Data may be inconsistent.", self.class_name, idx, self.idx2model(idx).class_name) need_new = True else: need_new = True if need_new is True: count = self.n while True: # IMPORTANT: automatically assigned index is 1-indexed. Namely, `GENCLS_1` is the first generator. # This is because when we say, for example, `GENCLS_10`, people usually assume it starts at 1. idx = model_name + '_' + str(count + 1) if idx not in self._idx2model: break else: count += 1 return idx
[docs] def get_all_idxes(self): """ Return all the devices idx in this group. .. note:: New in version 1.9.3. Returns ------- list List of indices. Notes ----- The default models sequence depends on the order of the models in the group, which comes from OrderedDict `file_classes` in `models.__init__.py`. Examples -------- >>> ss = andes.load(andes.get_case('ieee14/ieee14_pvd1.xlsx')) >>> ss.DG.get_all_idxes() [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> ss.StaticGen.get_all_idxes() [2, 3, 4, 5, 6, 1] """ return list(self._idx2model.keys())
[docs] def as_dict(self, vin=False): """ Export group common parameters as a dictionary. .. note:: New in version 1.9.3. This method returns a dictionary where the keys are the `ModelData` parameter names and the values are array-like structures containing the data in the order they were added. Unlike `ModelData.as_dict()`, this dictionary does not include the `uid` field. Parameters ---------- vin : bool, optional If True, includes the `vin` attribute in the dictionary. Default is False. Returns ------- dict A dictionary of common parameters. """ out_all = [] out_params = self.common_params.copy() out_params.insert(2, 'idx') for mdl in self.models.values(): if mdl.n <= 0: continue mdl_data = mdl.as_df(vin=True) if vin else mdl.as_dict() mdl_dict = {k: mdl_data.get(k) for k in out_params if k in mdl_data} out_all.append(mdl_dict) if not out_all: return {} out = {key: np.concatenate([item[key] for item in out_all]) for key in out_all[0].keys()} return out
[docs] def as_df(self, vin=False): """ Export group common parameters as a `pandas.DataFrame` object. .. note:: New in version 1.9.3. Parameters ---------- vin : bool If True, export all parameters from original input (``vin``). Returns ------- DataFrame A dataframe containing all model data. An `uid` column is added. """ return pd.DataFrame(self.as_dict(vin=vin))
[docs] def doc(self, export='plain'): """ Return the documentation of the group in a string. """ out = '' if export == 'rest': out += f'.. _{self.class_name}:\n\n' group_header = '=' * 80 + '\n' else: group_header = '' if export == 'rest': out += group_header + f'{self.class_name}\n' + group_header else: out += group_header + f'Group <{self.class_name}>\n' + group_header if self.__doc__ is not None: out += inspect.cleandoc(self.__doc__) + '\n\n' if len(self.common_params): out += 'Common Parameters: ' + ', '.join(self.common_params) out += '\n\n' if len(self.common_vars): out += 'Common Variables: ' + ', '.join(self.common_vars) out += '\n\n' if len(self.models): out += 'Available models:\n' model_name_list = list(self.models.keys()) if export == 'rest': def add_reference(name_list): return [f'{item}_' for item in name_list] model_name_list = add_reference(model_name_list) out += ',\n'.join(model_name_list) + '\n' return out
[docs] def doc_all(self, export='plain'): """ Return documentation of the group and its models. Parameters ---------- export : 'plain' or 'rest' Export format, plain-text or RestructuredText Returns ------- str """ out = self.doc(export=export) out += '\n' for instance in self.models.values(): out += instance.doc(export=export) out += '\n' return out
class Undefined(GroupBase): """ The undefined group. Holds models with no ``group``. """ pass class ACNode(GroupBase): def __init__(self): super().__init__() self.common_vars.extend(('a', 'v')) class DCTopology(GroupBase): def __init__(self): super().__init__() self.common_vars.extend(('v',)) class Collection(GroupBase): """Collection of topology models""" pass class Calculation(GroupBase): """Group of classes that calculates based on other models.""" pass class StaticGen(GroupBase): """ Static generator group. Static generators include PV and Slack, which are used to impose algebraic equations. Static generators are used primarily for power flow. Static generators do not have the modeling details for stability simulation. Although some of them can stay for time-domain simulation, most of them should be substituted by dynamic generators, including synchronous generators and inverter-based resources upon TDS initialization. The substitution is done by setting the ``gen`` field of a dynamic generator to refer to the static generator. To replace one StaticGen by multiple dynamic generators, see the notes in :ref:`SynGen`. Generators connected to the same bus need to have their `gammap` and `gammaq`, respectively, summed up to be exactly 1.0. TDS initialization will ensure that the dynamic generators impose the same amount of power as the static generator. At the end of initialization, `StaticGen`'s that have been substituted will have their connectivity status ``u`` changed to ``0``. """ def __init__(self): super().__init__() self.common_params.extend(('bus', 'Sn', 'Vn', 'p0', 'q0', 'ra', 'xs', 'subidx')) self.common_vars.extend(('q', 'a', 'v')) self.SynGen = BackRef() class ACLine(GroupBase): def __init__(self): super(ACLine, self).__init__() self.common_params.extend(('bus1', 'bus2', 'r', 'x')) self.common_vars.extend(('v1', 'v2', 'a1', 'a2')) class ACShort(GroupBase): def __init__(self): super(ACShort, self).__init__() self.common_params.extend(('bus1', 'bus2')) self.common_vars.extend(('v1', 'v2', 'a1', 'a2')) class StaticLoad(GroupBase): """ Static load group. """ pass class StaticShunt(GroupBase): """ Static shunt compensator group. """ pass class DynLoad(GroupBase): """ Dynamic load group. """ pass class SynGen(GroupBase): """ Synchronous generator group. SynGen replaces StaticGen upon the initialization of dynamic studies. SynGen and inverter-based resources contain parameters ``gammap`` and ``gammaq`` for splitting the initial power of a StaticGen into multiple dynamic ones. ``gammap``, for example, is the active power ratio of the dynamic generator to the static one. If a StaticGen is supposed to be replaced by one SynGen, the ``gammap`` and ``gammaq`` should both be ``1``. It is critical to ensure that ``gammap`` and ``gammaq``, respectively, of all dynamic power sources sum up to 1.0. Otherwise, the initial power injections imposed by dynamic sources will differ from the static ones. The initialization will then fail with mismatches power injection equations corresponding to bus ``a`` and ``v``. """ _setpoint_priority = { 'pref': [('TurbineGov', 'syn')], 'vref': [('Exciter', 'syn')], 'paux': [('TurbineGov', 'syn')], } def __init__(self): super().__init__() self.common_params.extend(('bus', 'gen', 'Sn', 'Vn', 'fn', 'M', 'D', 'subidx')) self.common_vars.extend(('omega', 'delta', )) self.idx_island = [] self.uid_island = [] self.delta_addr = [] def set_pref(self, system, idx, value): """ Set active power reference for a synchronous generator. Routes to the turbine governor's ``pref0`` if one is connected; otherwise falls back to the generator's ``tm0`` with a warning. The value is in system-base per-unit. Use :meth:`get_pref` to read the current value before applying incremental changes. """ self.set_setpoint(system, idx, 'pref', value) def get_pref(self, system, idx): """ Get active power reference. See :meth:`set_pref` for semantics. """ return self.get_setpoint(system, idx, 'pref') def set_vref(self, system, idx, value): """ Set voltage reference for a synchronous generator. Routes to the exciter's ``vref0`` if one is connected; otherwise falls back to the generator's ``vf0`` with a warning. .. warning:: The exciter voltage reference is **not** necessarily the terminal voltage. Its meaning depends on the exciter model. Most exciters initialize ``vref0 = vref`` (the pre-fault voltage error input), but some (e.g., EXAC4) use a model-specific formula. Use :meth:`get_vref` to read the current value before applying incremental changes. """ self.set_setpoint(system, idx, 'vref', value) def get_vref(self, system, idx): """ Get voltage reference. See :meth:`set_vref` for semantics. """ return self.get_setpoint(system, idx, 'vref') def set_paux(self, system, idx, value): """ Set auxiliary power input for a synchronous generator. Routes to the turbine governor's ``paux0``. Raises ``KeyError`` if no governor is connected (generators do not have an auxiliary power input on their own). The auxiliary signal is additive to the power reference inside the governor equations. The value is in system-base per-unit. Commonly used for AGC signals or reinforcement-learning inputs. """ self.set_setpoint(system, idx, 'paux', value) def get_paux(self, system, idx): """ Get auxiliary power input. See :meth:`set_paux` for semantics. """ return self.get_setpoint(system, idx, 'paux') def store_idx_island(self, bus_idx): """ Get ``idx`` of generators in the given islanded. Also store the addresses of the ``delta`` variable in the largest island. This function can only be called after initializing dynamic devices. Parameters ---------- bus_idx : list A list of bus idx in the largest island """ idx_gen = list(self.uid.keys()) bus_gen = self.get(src='bus', idx=idx_gen, attr='v') def intersect(lst1, lst2): return list(set(lst1) & set(lst2)) bus_gen_island = intersect(bus_idx, bus_gen) self.idx_island = self.find_idx(keys='bus', values=bus_gen_island) self.uid_island = self.idx2uid(self.idx_island) self.delta_addr = self.get('delta', self.idx_island, 'a').astype(int) class RenGen(GroupBase): """ Renewable generator (converter) group. See :ref:`SynGen` for the notes on replacing StaticGen and setting the power ratio parameters. Attention is needed for the power base ``Sn``. When replacing a synchronous generator, the renewable generator should have the same or larger ``Sn``. Improper ``Sn`` will cause the initial values to exceed typical per-unit ranges and cause the initialization to fail. """ _setpoint_priority = { 'pref': [('RenExciter', 'reg')], 'qref': [('RenExciter', 'reg')], } def __init__(self): super().__init__() self.common_params.extend(('bus', 'gen', 'Sn')) self.common_vars.extend(('Pe', 'Qe')) def set_pref(self, system, idx, value): """ Set active power reference for a renewable generator. Routes to the renewable exciter's ``Pref0`` (e.g., REECA1, REECB1) if one is connected; otherwise falls back to the converter model with a warning. The value is in system-base per-unit. For REECA1 with ``PFLAG=1``, the internal equation normalizes by generator speed (``wg``), so the setpoint represents electrical power, not mechanical power. Use :meth:`get_pref` to read the current value before applying incremental changes. """ self.set_setpoint(system, idx, 'pref', value) def get_pref(self, system, idx): """ Get active power reference. See :meth:`set_pref` for semantics. """ return self.get_setpoint(system, idx, 'pref') def set_qref(self, system, idx, value): """ Set reactive power reference for a renewable generator. Routes to the renewable exciter's ``qref0`` (e.g., REECA1, REECB1) if one is connected; otherwise falls back to the converter model with a warning. The value is in system-base per-unit. The interpretation depends on the exciter's reactive power control mode (voltage regulation vs. constant Q). Use :meth:`get_qref` to read the current value before applying incremental changes. """ self.set_setpoint(system, idx, 'qref', value) def get_qref(self, system, idx): """ Get reactive power reference. See :meth:`set_qref` for semantics. """ return self.get_setpoint(system, idx, 'qref') class RenExciter(GroupBase): """ Renewable electrical control (exciter) group. """ def __init__(self): super().__init__() self.common_params.extend(('reg',)) self.common_vars.extend(('Pref', 'Qref', 'wg', 'Pord')) class RenPlant(GroupBase): """ Renewable plant control group. """ def __init__(self): super().__init__() self.common_params.extend(('ree',)) class RenGovernor(GroupBase): """ Renewable turbine governor group. """ def __init__(self): super().__init__() self.common_params.extend(('ree', 'w0', 'Sn', 'Pe0')) self.common_vars.extend(('Pm', 'wr0', 'wt', 'wg', 's3_y')) class RenAerodynamics(GroupBase): """ Renewable aerodynamics group. """ def __init__(self): super().__init__() self.common_params.extend(('rego',)) self.common_vars.extend(('theta',)) class RenPitch(GroupBase): """ Renewable generator pitch controller group. """ def __init__(self): super().__init__() self.common_params.extend(('rea',)) class RenTorque(GroupBase): """ Renewable torque (Pref) controller. """ def __init__(self): super().__init__() class DG(GroupBase): """ Distributed generation (small-scale). See :ref:`SynGen` for the notes on replacing StaticGen and setting the power ratio parameters. """ # No external controller chain; setpoints live on the device itself. _setpoint_priority = {} def __init__(self): super().__init__() self.common_params.extend(('bus', 'fn')) def set_pref(self, system, idx, value): """ Set active power reference for a distributed generator. Writes to the device's ``pref0`` directly. The value is in system-base per-unit. Use :meth:`get_pref` to read the current value before applying incremental changes. """ self.set_setpoint(system, idx, 'pref', value) def get_pref(self, system, idx): """ Get active power reference. See :meth:`set_pref` for semantics. """ return self.get_setpoint(system, idx, 'pref') def set_qref(self, system, idx, value): """ Set reactive power reference for a distributed generator. Writes to the device's ``qref0`` directly. The value is in system-base per-unit. Use :meth:`get_qref` to read the current value before applying incremental changes. """ self.set_setpoint(system, idx, 'qref', value) def get_qref(self, system, idx): """ Get reactive power reference. See :meth:`set_qref` for semantics. """ return self.get_setpoint(system, idx, 'qref') def set_paux(self, system, idx, value): """ Set auxiliary power input for a distributed generator. Writes to the device's ``Pext0`` (external power signal). The auxiliary signal is additive to the power reference inside the model equations. Commonly used for AGC signals. The value is in system-base per-unit. """ self.set_setpoint(system, idx, 'paux', value) def get_paux(self, system, idx): """ Get auxiliary power input. See :meth:`set_paux` for semantics. """ return self.get_setpoint(system, idx, 'paux') class DGProtection(GroupBase): """ Protection model for DG. """ def __init__(self): super().__init__() class TurbineGov(GroupBase): """ Turbine governor group for synchronous generator. """ def __init__(self): super().__init__() self.common_params.extend(('syn',)) self.common_vars.extend(('pout',)) class DynShaft(GroupBase): """ Dynamic shaft model group for multi-mass torsional models. """ def __init__(self): super().__init__() self.common_params.extend(('syn',)) class Exciter(GroupBase): """ Exciter group for synchronous generators. """ def __init__(self): super().__init__() self.common_params.extend(('syn',)) self.common_vars.extend(('vout', 'vi',)) class VoltComp(GroupBase): """ Voltage compensator group for synchronous generators. """ def __init__(self): super().__init__() self.common_params.extend(('avr', 'rc', 'xc',)) self.common_vars.extend(('vcomp',)) class PSS(GroupBase): """Power system stabilizer group.""" def __init__(self): super().__init__() self.common_params.extend(('avr',)) self.common_vars.extend(('vsout',)) class Experimental(GroupBase): """Experimental group""" pass class DCLink(GroupBase): """Basic DC links""" pass class StaticACDC(GroupBase): """AC DC device for power flow""" pass class TimedEvent(GroupBase): """Timed event group""" pass class FreqMeasurement(GroupBase): """Frequency measurements.""" def __init__(self): super().__init__() self.common_params.extend(('bus',)) self.common_vars.extend(('f',)) class PhasorMeasurement(GroupBase): """Phasor measurements""" def __init__(self): super().__init__() self.common_params.extend(('bus',)) self.common_vars.extend(('am', 'vm')) class PLL(GroupBase): """Phase-locked loop models.""" def __init__(self): super().__init__() self.common_params.extend(('bus',)) self.common_vars.extend(('am',)) class Motor(GroupBase): """Induction Motor group """ def __init__(self): super().__init__() self.common_params.extend(('bus',)) class Information(GroupBase): """ Group for information container models. """ def __init__(self): GroupBase.__init__(self) self.common_params = [] class OutputSelect(GroupBase): """ Group for selecting outputs. """ def __init__(self): super().__init__() self.common_params = [] class Interface(GroupBase): """ Group for interface models. """ def __init__(self): super().__init__() self.common_params = [] class DataSeries(GroupBase): """ Group for TimeSeries models. """ def __init__(self): super().__init__() self.common_params = []