def set_address(self, models=None): """ Set addresses for differential and algebraic variables. """ if models is None: models = self._models_flag['pflow'] # set internal variable addresses for mdl in models.values(): if mdl.flags['address'] is True: logger.debug(f'{mdl.class_name:10s}: addresses exist.') continue if mdl.n == 0: continue n = mdl.n m0 = self.dae.m n0 = self.dae.n m_end = m0 + len(mdl.algebs) * n n_end = n0 + len(mdl.states) * n collate = mdl.flags['collate'] if not collate: for idx, item in enumerate(mdl.algebs.values()): item.set_address(np.arange(m0 + idx * n, m0 + (idx + 1) * n)) for idx, item in enumerate(mdl.states.values()): item.set_address(np.arange(n0 + idx * n, n0 + (idx + 1) * n)) else: for idx, item in enumerate(mdl.algebs.values()): item.set_address(np.arange(m0 + idx, m_end, len(mdl.algebs))) for idx, item in enumerate(mdl.states.values()): item.set_address(np.arange(n0 + idx, n_end, len(mdl.states))) self.dae.m = m_end self.dae.n = n_end mdl.flags['address'] = True # set external variable addresses for mdl in models.values(): # handle external groups for name, instance in mdl.cache.vars_ext.items(): ext_name = instance.model try: ext_model = self.__dict__[ext_name] except KeyError: raise KeyError(f'<{ext_name}> is not a model or group name.') instance.link_external(ext_model) # pre-allocate for names if len(self.dae.y_name) == 0: self.dae.x_name = [''] * self.dae.n self.dae.y_name = [''] * self.dae.m self.dae.x_tex_name = [''] * self.dae.n self.dae.y_tex_name = [''] * self.dae.m else: self.dae.x_name.extend([''] * (self.dae.n - len(self.dae.x_name))) self.dae.y_name.extend([''] * (self.dae.m - len(self.dae.y_name))) self.dae.x_tex_name.extend([''] * (self.dae.n - len(self.dae.x_tex_name))) self.dae.y_tex_name.extend([''] * (self.dae.m - len(self.dae.y_tex_name)))
def store_sparse_pattern(self, models: Optional[Union[str, List, OrderedDict]] = None): models = self._get_models(models) self._call_models_method('store_sparse_pattern', models) # add variable jacobian values for j_name in self.dae.jac_name: ii, jj, vv = list(), list(), list() # for `gy` matrix, always make sure the diagonal is reserved # It is a safeguard if the modeling user omitted the diagonal # term in the equations if j_name == 'gy': ii.extend(np.arange(self.dae.m)) jj.extend(np.arange(self.dae.m)) vv.extend(np.zeros(self.dae.m)) # logger.debug(f'Jac <{j_name}>, row={ii}') for mdl in models.values(): row_idx = mdl.row_of(f'{j_name}') col_idx = mdl.col_of(f'{j_name}') # logger.debug(f'Model <{name}>, row={row_idx}') ii.extend(row_idx) jj.extend(col_idx) vv.extend(np.zeros(len(np.array(row_idx)))) # add the constant jacobian values for row, col, val in mdl.zip_ijv(f'{j_name}c'): ii.extend(row) jj.extend(col) if isinstance(val, (float, int)): vv.extend(val * np.ones(len(row))) elif isinstance(val, (list, np.ndarray)): vv.extend(val) else: raise TypeError( f'Unknown type {type(val)} in constant jacobian {j_name}' ) if len(ii) > 0: ii = np.array(ii).astype(int) jj = np.array(jj).astype(int) vv = np.array(vv).astype(float) self.dae.store_sparse_ijv(j_name, ii, jj, vv) self.dae.build_pattern(j_name)
def _run_odeint(self, tspan, x0=None, asolver=None, verbose=False, h=0.05, hmax=0, hmin=0): """ Run integration with ``scipy.odeint``. Warnings -------- Function is NOT working. The time-based switching is not handled correctly. """ self._initialize() if x0 is None: x0 = self.system.dae.x times = np.arange(tspan[0], tspan[1], h) # build critical time list tcrit = np.hstack([np.linspace(i, i+0.5, 100) for i in self.system.switch_times]) ret = odeint(self._solve_ivp_wrapper, x0, times, tfirst=True, args=(asolver, verbose), full_output=True, hmax=hmax, hmin=hmin, tcrit=tcrit ) # store the last step algebraic variables self.system.dae.store_yt_single() self.system.dae.store_xt_array(ret[0], times) return ret
def link_external(self, ext_model): """ Update parameter values provided by external models. This needs to be called before pu conversion. Parameters ---------- ext_model : Model, Group Instance of the parent model or group, provided by the System calling this method. """ self.parent_model = ext_model if hasattr(ext_model, "_idx2model"): # copy properties from models in the group # TODO: the three `get` calls below is a bit inefficient - same loops for three times try: self.v = ext_model.get(src=self.src, idx=self.indexer.v, attr='v', allow_none=self.allow_none, default=self.default) except IndexError: pass try: self.vin = ext_model.get(src=self.src, idx=self.indexer.v, attr='vin', allow_none=self.allow_none, default=self.default) self.pu_coeff = ext_model.get(src=self.src, idx=self.indexer.v, attr='vin', allow_none=self.allow_none, default=self.default) except KeyError: # idx param without vin pass except TypeError: # vin or pu_coeff is None pass else: if self.allow_none: raise NotImplementedError(f"{self.name}: allow_none not implemented for Model") parent_instance = ext_model.__dict__[self.src] self.property = dict(parent_instance.property) if self.indexer is None: # if `idx` is None, retrieve all the values uid = np.arange(ext_model.n) else: if len(self.indexer.v) == 0: return else: uid = ext_model.idx2uid(self.indexer.v) # pull in values if isinstance(parent_instance.v, np.ndarray): self.v = parent_instance.v[uid] else: self.v = [parent_instance.v[i] for i in uid] try: self.vin = parent_instance.vin[uid] self.pu_coeff = parent_instance.pu_coeff[uid] except KeyError: pass except AttributeError: pass
def check_var(self, dae_t, *args, **kwargs): # Storage: # Output values is in the first col. # Latest values are stored in /appended to the last column self.rewind = False if dae_t == 0: self._v_mem[:] = self.u.v[:, None] elif dae_t < self.t[-1]: self.rewind = True self.t[-1] = dae_t self._v_mem[:, -1] = self.u.v elif dae_t == self.t[-1]: self._v_mem[:, -1] = self.u.v elif dae_t > self.t[-1]: if self.mode == 'step': self.t[:-1] = self.t[1:] self.t[-1] = dae_t self._v_mem[:, :-1] = self._v_mem[:, 1:] self._v_mem[:, -1] = self.u.v else: self.t = np.append(self.t, dae_t) self._v_mem = np.hstack((self._v_mem, self.u.v[:, None])) if dae_t - self.t[0] > self.delay: t_interp = dae_t - self.delay idx = np.argmax(self.t >= t_interp) - 1 v_interp = interp_n2(t_interp, self.t[idx:idx + 2], self._v_mem[:, idx:idx + 2]) self.t[idx] = t_interp self._v_mem[:, idx] = v_interp self.t = np.delete(self.t, np.arange(0, idx)) self._v_mem = np.delete(self._v_mem, np.arange(0, idx), axis=1) self.v[:] = self._v_mem[:, 0]
def store_sparse_pattern(self, models: Optional[Union[str, List, OrderedDict]] = None): """ Collect and store the sparsity pattern of Jacobian matrices. This is a runtime function specific to cases. Notes ----- For `gy` matrix, always make sure the diagonal is reserved. It is a safeguard if the modeling user omitted the diagonal term in the equations. """ models = self._get_models(models) self._call_models_method('store_sparse_pattern', models) # add variable jacobian values for jname in jac_names: ii, jj, vv = list(), list(), list() if jname == 'gy': ii.extend(np.arange(self.dae.m)) jj.extend(np.arange(self.dae.m)) vv.extend(np.zeros(self.dae.m)) for mdl in models.values(): for row, col, val in mdl.triplets.zip_ijv(jname): ii.extend(row) jj.extend(col) vv.extend(np.zeros_like(row)) for row, col, val in mdl.triplets.zip_ijv(jname + 'c'): # process constant Jacobians separately ii.extend(row) jj.extend(col) vv.extend(val * np.ones_like(row)) if len(ii) > 0: ii = np.array(ii, dtype=int) jj = np.array(jj, dtype=int) vv = np.array(vv, dtype=float) self.dae.store_sparse_ijv(jname, ii, jj, vv) self.dae.build_pattern(jname)
def link_external(self, ext_model): """ Update variable addresses provided by external models This method sets attributes including `parent_model`, `parent_instance`, `uid`, `a`, `n`, `e_code` and `v_code`. It initializes the `e` and `v` to zero. Returns ------- None Parameters ---------- ext_model : Model Instance of the parent model """ self.parent = ext_model if isinstance(ext_model, GroupBase): if self.indexer.n > 0 and isinstance(self.indexer.v[0], (list, np.ndarray)): self._n = [len(i) for i in self.indexer.v ] # number of elements in each sublist self._idx = np.concatenate( [np.array(i) for i in self.indexer.v]) else: self._n = [len(self.indexer.v)] self._idx = self.indexer.v self.a = ext_model.get(src=self.src, idx=self._idx, attr='a').astype(int) self.n = len(self.a) self.v = np.zeros(self.n) self.e = np.zeros(self.n) else: original_var = ext_model.__dict__[self.src] if self.indexer is not None: uid = ext_model.idx2uid(self.indexer.v) else: uid = np.arange(ext_model.n, dtype=int) self._n = [len(uid)] if len(uid) > 0: self.a = original_var.a[uid] else: self.a = np.array([], dtype=int) # set initial v and e values to zero self.n = len(self.a) self.v = np.zeros(self.n) self.e = np.zeros(self.n)
def plot(self, left=-6, right=0.5, ymin=-8, ymax=8, damping=0.05, linewidth=0.5, dpi=150): mpl.rc('font', family='Times New Roman', size=12) mu_real = self.mu.real() mu_imag = self.mu.imag() p_mu_real, p_mu_imag = list(), list() z_mu_real, z_mu_imag = list(), list() n_mu_real, n_mu_imag = list(), list() for re, im in zip(mu_real, mu_imag): if re == 0: z_mu_real.append(re) z_mu_imag.append(im) elif re > 0: p_mu_real.append(re) p_mu_imag.append(im) elif re < 0: n_mu_real.append(re) n_mu_imag.append(im) if len(p_mu_real) > 0: logger.warning( 'System is not stable due to {} positive eigenvalues.'.format( len(p_mu_real))) else: logger.info( 'System is small-signal stable in the initial neighbourhood.') mpl.rc('text', usetex=True) fig, ax = plt.subplots(dpi=dpi) ax.scatter(n_mu_real, n_mu_imag, marker='x', s=40, linewidth=0.5, color='black') ax.scatter(z_mu_real, z_mu_imag, marker='o', s=40, linewidth=0.5, facecolors='none', edgecolors='black') ax.scatter(p_mu_real, p_mu_imag, marker='x', s=40, linewidth=0.5, color='black') ax.axhline(linewidth=0.5, color='grey', linestyle='--') ax.axvline(linewidth=0.5, color='grey', linestyle='--') # plot 5% damping lines xin = np.arange(left, 0, 0.01) yneg = xin / damping ypos = - xin / damping ax.plot(xin, yneg, color='grey', linewidth=linewidth, linestyle='--') ax.plot(xin, ypos, color='grey', linewidth=linewidth, linestyle='--') ax.set_xlabel('Real') ax.set_ylabel('Imaginary') ax.set_xlim(left=left, right=right) ax.set_ylim(ymin, ymax) plt.show() return fig, ax
def link_external(self, ext_model): """ Update parameter values provided by external models. This needs to be called before pu conversion. TODO: Check if the pu conversion is correct or not. Parameters ---------- ext_model : Model, Group Instance of the parent model or group, provided by the System calling this method. """ self.parent_model = ext_model if isinstance(ext_model, GroupBase): # TODO: the three lines below is a bit inefficient - 3x same loops self.v = ext_model.get(src=self.src, idx=self.indexer.v, attr='v') try: self.vin = ext_model.get(src=self.src, idx=self.indexer.v, attr='vin') self.pu_coeff = ext_model.get(src=self.src, idx=self.indexer.v, attr='vin') except KeyError: # idx param without vin pass # TODO: copy properties from models in the group else: parent_instance = ext_model.__dict__[self.src] self.property = dict(parent_instance.property) if self.indexer is None: # if `idx` is None, retrieve all the values uid = np.arange(ext_model.n) else: if len(self.indexer.v) == 0: return else: uid = ext_model.idx2uid(self.indexer.v) # pull in values self.v = parent_instance.v[uid] try: self.vin = parent_instance.vin[uid] self.pu_coeff = parent_instance.pu_coeff[uid] except KeyError: pass
def link_external(self, ext_model): """ Update variable addresses provided by external models This method sets attributes including `parent_model`, `parent_instance`, `uid`, `a`, `n`, `e_code` and `v_code`. It initializes the `e` and `v` to zero. Returns ------- None Parameters ---------- ext_model : Model Instance of the parent model Warnings -------- `link_external` does not check if the ExtVar type is the same as the original variable to reduce performance overhead. It will be a silent error (a dimension too small error from `dae.build_pattern`) if a model uses `ExtAlgeb` to access a `State`, or vice versa. """ self.parent = ext_model if isinstance(ext_model, GroupBase): # determine the number of elements based on `indexer.v` if self.indexer.n > 0 and isinstance(self.indexer.v[0], (list, np.ndarray)): self._n = [len(i) for i in self.indexer.v ] # number of elements in each sublist self._idx = np.concatenate( [np.array(i) for i in self.indexer.v]) else: self._n = [len(self.indexer.v)] self._idx = self.indexer.v # use `0` for non-existent addresses (corr. to None in indexer) self.a = ext_model.get( src=self.src, idx=self._idx, attr='a', allow_none=self.allow_none, default=0, ).astype(int) # check if source var type is the same as this ExtVar vcodes = np.array( ext_model.get_field(src=self.src, idx=self._idx, field='v_code')) vcodes = vcodes[vcodes != np.array(None)].astype(str) if not all(vcodes == np.array(self.v_code)): raise TypeError( "ExtVar <%s.%s> is of type <%s>, but source Vars <%s.%s> may not." % (self.owner.class_name, self.name, self.v_code, ext_model.class_name, self.src)) self.n = len(self.a) else: original_var = ext_model.__dict__[self.src] if self.allow_none: raise NotImplementedError( f"{self.name}: allow_none not implemented for Model") if original_var.v_code != self.v_code: raise TypeError("Linking %s of %s to %s of %s is not allowed" % (self.name, self.class_name, original_var.name, original_var.class_name)) if self.indexer is not None: uid = ext_model.idx2uid(self.indexer.v) else: uid = np.arange(ext_model.n, dtype=int) self._n = [len(uid)] if len(uid) > 0: self.a = original_var.a[uid] else: self.a = np.array([], dtype=int) # set initial v and e values to zero self.n = len(self.a) self.v = np.zeros(self.n)
def set_address(self, models): """ Set addresses for differential and algebraic variables. """ # set internal variable addresses for mdl in models.values(): if mdl.flags.address is True: logger.debug(f'{mdl.class_name} address exists') continue if mdl.n == 0: continue logger.debug(f'Setting address for {mdl.class_name}') n = mdl.n m0 = self.dae.m n0 = self.dae.n m_end = m0 + len(mdl.algebs) * n n_end = n0 + len(mdl.states) * n collate = mdl.flags.collate if not collate: for idx, item in enumerate(mdl.algebs.values()): item.set_address(np.arange(m0 + idx * n, m0 + (idx + 1) * n), contiguous=True) for idx, item in enumerate(mdl.states.values()): item.set_address(np.arange(n0 + idx * n, n0 + (idx + 1) * n), contiguous=True) else: for idx, item in enumerate(mdl.algebs.values()): item.set_address(np.arange(m0 + idx, m_end, len(mdl.algebs)), contiguous=False) for idx, item in enumerate(mdl.states.values()): item.set_address(np.arange(n0 + idx, n_end, len(mdl.states)), contiguous=False) self.dae.m = m_end self.dae.n = n_end mdl.flags.address = True # set external variable addresses for mdl in models.values(): # handle external groups for name, instance in mdl.cache.vars_ext.items(): ext_name = instance.model try: ext_model = self.__dict__[ext_name] except KeyError: raise KeyError( f'<{ext_name}> is not a model or group name.') instance.link_external(ext_model) # allocate memory for DAE arrays self.dae.resize_arrays() # set `v` and `e` in variables self._set_var_arrays(models=models) # pre-allocate for names if len(self.dae.y_name) == 0: self.dae.x_name = [''] * self.dae.n self.dae.y_name = [''] * self.dae.m self.dae.x_tex_name = [''] * self.dae.n self.dae.y_tex_name = [''] * self.dae.m else: self.dae.x_name.extend([''] * (self.dae.n - len(self.dae.x_name))) self.dae.y_name.extend([''] * (self.dae.m - len(self.dae.y_name))) self.dae.x_tex_name.extend([''] * (self.dae.n - len(self.dae.x_tex_name))) self.dae.y_tex_name.extend([''] * (self.dae.m - len(self.dae.y_tex_name)))