Source code for andes.core.model.modeldata

"""
Module for ModelData.
"""

import logging
from collections import OrderedDict

import numpy as np
from andes.core.model.modelcache import ModelCache
from andes.core.param import (BaseParam, DataParam, IdxParam, NumParam,
                              TimerParam)
from andes.shared import pd
from andes.utils.func import validate_keys_values

logger = logging.getLogger(__name__)


[docs]class ModelData: r""" Class for holding parameter data for a model. This class is designed to hold the parameter data separately from model equations. Models should inherit this class to define the parameters from input files. Inherit this class to create the specific class for holding input parameters for a new model. The recommended name for the derived class is the model name with ``Data``. For example, data for `GENROU` should be named `GENROUData`. Parameters should be defined in the ``__init__`` function of the derived class. Refer to :py:mod:`andes.core.param` for available parameter types. Attributes ---------- cache A cache instance for different views of the internal data. flags : dict Flags to control the routine and functions that get called. If the model is using user-defined numerical calls, set `f_num`, `g_num` and `j_num` properly. Notes ----- Three default parameters are pre-defined in ``ModelData`` and will be inherited by all models. They are - ``idx``, unique device idx of type :py:class:`andes.core.param.DataParam` - ``u``, connection status of type :py:class:`andes.core.param.NumParam` - ``name``, (device name of type :py:class:`andes.core.param.DataParam` In rare cases one does not want to define these three parameters, one can pass `three_params=True` to the constructor of ``ModelData``. Examples -------- If we want to build a class ``PQData`` (for static PQ load) with three parameters, `Vn`, `p0` and `q0`, we can use the following :: from andes.core.model import ModelData, Model from andes.core.param import IdxParam, NumParam class PQData(ModelData): super().__init__() self.Vn = NumParam(default=110, info="AC voltage rating", unit='kV', non_zero=True, tex_name=r'V_n') self.p0 = NumParam(default=0, info='active power load in system base', tex_name=r'p_0', unit='p.u.') self.q0 = NumParam(default=0, info='reactive power load in system base', tex_name=r'q_0', unit='p.u.') In this example, all the three parameters are defined as :py:class:`andes.core.param.NumParam`. In the full `PQData` class, other types of parameters also exist. For example, to store the idx of `owner`, `PQData` uses :: self.owner = IdxParam(model='Owner', info="owner idx") """
[docs] def __init__(self, *args, three_params=True, **kwargs): self.params = OrderedDict() self.num_params = OrderedDict() self.idx_params = OrderedDict() self.timer_params = OrderedDict() self.n = 0 self.uid = {} # indexing bases. Most vectorized models only have one base: self.idx self.index_bases = [] if not hasattr(self, 'cache'): self.cache = ModelCache() self.cache.add_callback('dict', self.as_dict) self.cache.add_callback('df', lambda: self.as_df()) self.cache.add_callback('dict_in', lambda: self.as_dict(True)) self.cache.add_callback('df_in', lambda: self.as_df(vin=True)) if three_params is True: self.idx = DataParam(info='unique device idx') self.u = NumParam(default=1, info='connection status', unit='bool', tex_name='u') self.name = DataParam(info='device name') self.index_bases.append(self.idx)
def __len__(self): return self.n def __setattr__(self, key, value): if isinstance(value, BaseParam): value.owner = self if not value.name: value.name = key if key in self.__dict__: logger.warning("%s: redefining <%s>. This is likely a modeling error.", self.class_name, key) self.params[key] = value if isinstance(value, NumParam): self.num_params[key] = value elif isinstance(value, IdxParam): self.idx_params[key] = value # `TimerParam` is a subclass of `NumParam` and thus tested separately if isinstance(value, TimerParam): self.timer_params[key] = value super(ModelData, self).__setattr__(key, value)
[docs] def add(self, **kwargs): """ Add a device (an instance) to this model. Warnings -------- This function is not intended to be used directly. Use the ``add`` method from System so that the index can be registered correctly. Parameters ---------- kwargs model parameters are collected into the kwargs dictionary """ idx = kwargs['idx'] self.uid[idx] = self.n self.n += 1 if "name" in self.params: name = kwargs.get("name") if (name is None) or (not isinstance(name, str) and np.isnan(name)): kwargs["name"] = idx if "idx" not in self.params: kwargs.pop("idx") for name, instance in self.params.items(): value = kwargs.pop(name, None) instance.add(value) if len(kwargs) > 0: logger.warning("%s: unused data %s", self.class_name, str(kwargs))
[docs] def as_dict(self, vin=False): """ Export all parameters as a dict. Returns ------- dict a dict with the keys being the `ModelData` parameter names and the values being an array-like of data in the order of adding. An additional `uid` key is added with the value default to range(n). """ out = dict() # append 0-indexed `uid` out['uid'] = np.arange(self.n) for name, instance in self.params.items(): # skip non-exported parameters if instance.export is False: continue out[name] = instance.v # use the original input if `vin` is True if (vin is True) and hasattr(instance, 'vin') and (instance.vin is not None): out[name] = instance.vin conv = instance.oconvert if conv is not None: out[name] = np.array([conv(item) for item in out[name]]) return out
[docs] def as_df(self, vin=False): """ Export all parameters as a `pandas.DataFrame` object. This function utilizes `as_dict` for preparing data. 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. """ if vin is False: out = pd.DataFrame(self.as_dict()).set_index('uid') else: out = pd.DataFrame(self.as_dict(vin=True)).set_index('uid') return out
[docs] def as_df_local(self): """ Export local variable values and services to a DataFrame. """ out = dict() out['uid'] = np.arange(self.n) out['idx'] = self.idx.v for name, instance in self.cache.all_vars.items(): out[name] = instance.v for name, instance in self.services.items(): out[name] = instance.v return pd.DataFrame(out).set_index('uid')
[docs] def update_from_df(self, df, vin=False): """ Update parameter values from a DataFrame. Adding devices are not allowed. """ if vin is False: for name, instance in self.params.items(): if instance.export is False: continue instance.set_all('v', df[name]) else: for name, instance in self.params.items(): if instance.export is False: continue try: instance.set_all('vin', df[name]) instance.v[:] = instance.vin * instance.pu_coeff except KeyError: # fall back to `v` instance.set_all('v', df[name]) return True
[docs] def find_param(self, prop): """ Find params with the given property and return in an OrderedDict. Parameters ---------- prop : str Property name Returns ------- OrderedDict """ out = OrderedDict() for name, instance in self.params.items(): if instance.get_property(prop) is True: out[name] = instance return out
[docs] def find_idx(self, keys, values, allow_none=False, default=False, allow_all=False): """ Find `idx` of devices whose values match the given pattern. 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 corresponds to the key. allow_none : bool, Sized, optional Allow key, value to be not found. Used by groups. default : bool, optional Default idx to return if not found (missing) allow_all : bool, optional If True, returns a list of lists where each nested list contains all the matches for the corresponding search criteria. Returns ------- list indices of devices Notes ----- - Only the first match is returned by default. - If all matches are needed, set `allow_all` to True. Examples -------- >>> # Use example case of IEEE 14-bus system with PVD1 >>> ss = andes.load(andes.get_case('ieee14/ieee14_pvd1.xlsx')) >>> # To find the idx of `PVD1` with `name` of 'PVD1_1' and 'PVD1_2' >>> ss.PVD1.find_idx(keys='name', values=['PVD1_1', 'PVD1_2']) [1, 2] >>> # To find the idx of `PVD1` connected to bus 4 >>> ss.PVD1.find_idx(keys='bus', values=[4]) [1] >>> # To find ALL the idx of `PVD1` with `gammap` equals to 0.1 >>> ss.PVD1.find_idx(keys='gammap', values=[0.1], allow_all=True) [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]] >>> # To find the idx of `PVD1` with `gammap` equals to 0.1 and `name` of 'PVD1_1' >>> ss.PVD1.find_idx(keys=['gammap', 'name'], values=[[0.1], ['PVD1_1']]) [1] """ keys, values = validate_keys_values(keys, values) v_attrs = [self.__dict__[key].v for key in keys] idxes = [] for v_search in zip(*values): v_idx = [] for pos, v_attr in enumerate(zip(*v_attrs)): if all([i == j for i, j in zip(v_search, v_attr)]): v_idx.append(self.idx.v[pos]) if not v_idx: if allow_none is False: raise IndexError(f'{list(keys)}={v_search} not found in {self.class_name}') else: v_idx = [default] if allow_all: idxes.append(v_idx) else: idxes.append(v_idx[0]) return idxes