def _v_to_dae(self, v_name): """ Helper function for collecting variable values into dae structures `x` and `y`. This function must be called with x and y both being zeros. Otherwise, adders will be summed again, causing an error. Parameters ---------- v_name Returns ------- """ if v_name not in ('x', 'y'): raise KeyError(f'{v_name} is not a valid var name') for var in self.__dict__[f'{v_name}_adders']: # NOTE: # For power flow, they will be initialized to zero. # For TDS initialization, they will remain their value. if var.n == 0: continue if (var.v_str is None) and isinstance(var, ExtVar): continue if var.owner.flags['initialized'] is False: continue np.add.at(self.dae.__dict__[v_name], var.a, var.v) for var in self.__dict__[f'{v_name}_setters']: if var.owner.flags['initialized'] is False: continue if var.n > 0: np.put(self.dae.__dict__[v_name], var.a, var.v)
def _v_to_dae(self, v_code, model): """ Helper function for collecting variable values into dae structures `x` and `y`. This function must be called with x and y both being zeros. Otherwise, adders will be summed again, causing an error. Parameters ---------- v_code : 'x' or 'y' Variable type name """ if model.n == 0: return if model.flags.initialized is False: return for var in model.cache.v_adders.values(): if var.v_code != v_code: continue np.add.at(self.dae.__dict__[v_code], var.a, var.v) for var in self._setters[v_code]: if var.owner.flags.initialized is False: continue if var.n > 0: np.put(self.dae.__dict__[v_code], var.a, var.v)
def _store_tf(self, models): """ Store the inverse time constant associated with equations. """ for mdl in models.values(): for var in mdl.cache.states_and_ext.values(): if var.t_const is not None: np.put(self.dae.Tf, var.a, var.t_const.v)
def fg_to_dae(self): self._e_to_dae('f') self._e_to_dae('g') # update variable values set by anti-windup limiters for item in self.antiwindups: if len(item.x_set) > 0: for key, val in item.x_set: np.put(self.dae.x, key, val)
def _store_Tf(self): """ Store the inverse time constant associated with equations """ for var in self._adders['f']: if var.t_const is not None: np.put(self.dae.Tf, var.a, var.t_const.v) for var in self._setters['f']: if var.t_const is not None: np.put(self.dae.Tf, var.a, var.t_const.v)
def _e_to_dae(self, eq_name: str): """ Helper function for collecting equation values into `System.dae.f` and `System.dae.g`. Parameters ---------- eq_name : 'x' or 'y' Equation type name """ for var in self._adders[eq_name]: np.add.at(self.dae.__dict__[eq_name], var.a, var.e) for var in self._setters[eq_name]: np.put(self.dae.__dict__[eq_name], var.a, var.e)
def fg_to_dae(self): """ Collect equation values into the DAE arrays. Additionally, the function resets the differential equations associated with variables pegged by anti-windup limiters. """ self._e_to_dae(('f', 'g')) # update variable values set by anti-windup limiters for item in self.antiwindups: if len(item.x_set) > 0: for key, val, _ in item.x_set: np.put(self.dae.x, key, val)
def _e_to_dae(self, eq_name: Union[str, Tuple] = ('f', 'g')): """ Helper function for collecting equation values into `System.dae.f` and `System.dae.g`. Parameters ---------- eq_name : 'x' or 'y' or tuple Equation type name """ if isinstance(eq_name, str): eq_name = [eq_name] for name in eq_name: for var in self._adders[name]: np.add.at(self.dae.__dict__[name], var.a, var.e) for var in self._setters[name]: np.put(self.dae.__dict__[name], var.a, var.e)
def _e_to_dae(self, eq_name): """ Helper function for collecting equation values into dae structures `f` and `g` Parameters ---------- eq_name : 'x' or 'y' Equation type name """ if eq_name not in ('f', 'g'): raise KeyError(f'{eq_name} is not a valid eq name') for var in self._adders[eq_name]: if var.n > 0: np.add.at(self.dae.__dict__[eq_name], var.a, var.e) for var in self._setters[eq_name]: if var.n > 0: np.put(self.dae.__dict__[eq_name], var.a, var.e)
def _implicit_step(self): """ Integrate for a single given step. This function has an internal Newton-Raphson loop for algebraized semi-explicit DAE. The function returns the convergence status when done but does NOT progress simulation time. Returns ------- bool Convergence status in ``self.converged``. """ system = self.system dae = self.system.dae self.mis = [] self.niter = 0 self.converged = False self.x0 = np.array(dae.x) self.y0 = np.array(dae.y) self.f0 = np.array(dae.f) while True: system.e_clear(models=self.pflow_tds_models) system.l_update_var(models=self.pflow_tds_models) system.f_update(models=self.pflow_tds_models) system.g_update(models=self.pflow_tds_models) system.l_check_eq(models=self.pflow_tds_models) system.l_set_eq(models=self.pflow_tds_models) system.fg_to_dae() # lazy jacobian update if dae.t == 0 or self.niter > 3 or (dae.t - self._last_switch_t < 0.2): system.j_update(models=self.pflow_tds_models) self.solver.factorize = True # solve trapezoidal rule integration In = spdiag([1] * dae.n) self.Ac = sparse([[In - self.h * 0.5 * dae.fx, dae.gx], [-self.h * 0.5 * dae.fy, dae.gy]], 'd') # reset q as well q = dae.x - self.x0 - self.h * 0.5 * (dae.f + self.f0) for item in system.antiwindups: if len(item.x_set) > 0: for key, val in item.x_set: np.put(q, key[np.where(item.zi == 0)], 0) qg = np.hstack((q, dae.g)) inc = self.solver.solve(self.Ac, -matrix(qg)) # check for np.nan first if np.isnan(inc).any(): logger.error(f'NaN found in solution. Convergence not likely') self.niter = self.config.max_iter + 1 self.busted = True break # reset really small values to avoid anti-windup limiter flag jumps inc[np.where(np.abs(inc) < 1e-12)] = 0 # set new values dae.x += np.ravel(np.array(inc[:dae.n])) dae.y += np.ravel(np.array(inc[dae.n: dae.n + dae.m])) system.vars_to_models() # calculate correction mis = np.max(np.abs(inc)) self.mis.append(mis) self.niter += 1 # converged if mis <= self.config.tol: self.converged = True break # non-convergence cases if self.niter > self.config.max_iter: logger.debug(f'Max. iter. {self.config.max_iter} reached for t={dae.t:.6f}, ' f'h={self.h:.6f}, mis={mis:.4g} ' f'({system.dae.xy_name[np.argmax(inc)]})') break if mis > 1000 and (mis > 1e8 * self.mis[0]): logger.error(f'Error increased too quickly. Convergence not likely.') self.busted = True break if not self.converged: dae.x = np.array(self.x0) dae.y = np.array(self.y0) dae.f = np.array(self.f0) system.vars_to_models() return self.converged
def _itm_step(self): """ Integrate with Implicit Trapezoidal Method (ITM) to the current time. This function has an internal Newton-Raphson loop for algebraized semi-explicit DAE. The function returns the convergence status when done but does NOT progress simulation time. Returns ------- bool Convergence status in ``self.converged``. """ system = self.system dae = self.system.dae self.mis = 1 self.niter = 0 self.converged = False self.x0 = np.array(dae.x) self.y0 = np.array(dae.y) self.f0 = np.array(dae.f) while True: self._fg_update(models=system.exist.pflow_tds) # lazy Jacobian update if dae.t == 0 or \ self.config.honest or \ self.custom_event or \ not self.last_converged or \ self.niter > 4 or \ (dae.t - self._last_switch_t < 0.1): system.j_update(models=system.exist.pflow_tds) # set flag in `solver.worker.factorize`, not `solver.factorize`. self.solver.worker.factorize = True # `Tf` should remain constant throughout the simulation, even if the corresponding diff. var. # is pegged by the anti-windup limiters. # solve implicit trapezoidal method (ITM) integration self.Ac = sparse([[self.Teye - self.h * 0.5 * dae.fx, dae.gx], [-self.h * 0.5 * dae.fy, dae.gy]], 'd') # equation `self.qg[:dae.n] = 0` is the implicit form of differential equations using ITM self.qg[:dae.n] = dae.Tf * (dae.x - self.x0) - self.h * 0.5 * (dae.f + self.f0) # reset the corresponding q elements for pegged anti-windup limiter for item in system.antiwindups: for key, _, eqval in item.x_set: np.put(self.qg, key, eqval) self.qg[dae.n:] = dae.g if not self.config.linsolve: inc = self.solver.solve(self.Ac, matrix(self.qg)) else: inc = self.solver.linsolve(self.Ac, matrix(self.qg)) # check for np.nan first if np.isnan(inc).any(): self.err_msg = 'NaN found in solution. Convergence is not likely' self.niter = self.config.max_iter + 1 self.busted = True break # reset small values to reduce chattering inc[np.where(np.abs(inc) < self.tol_zero)] = 0 # set new values dae.x -= inc[:dae.n].ravel() dae.y -= inc[dae.n: dae.n + dae.m].ravel() # store `inc` to self for debugging self.inc = inc system.vars_to_models() # calculate correction mis = np.max(np.abs(inc)) # store initial maximum mismatch if self.niter == 0: self.mis = mis self.niter += 1 # converged if mis <= self.config.tol: self.converged = True break # non-convergence cases if self.niter > self.config.max_iter: tqdm.write(f'* Max. iter. {self.config.max_iter} reached for t={dae.t:.6f}, ' f'h={self.h:.6f}, mis={mis:.4g} ') # debug helpers g_max = np.argmax(abs(dae.g)) inc_max = np.argmax(abs(inc)) self._debug_g(g_max) self._debug_ac(inc_max) break if mis > 1e6 and (mis > 1e6 * self.mis): self.err_msg = 'Error increased too quickly. Convergence not likely.' self.busted = True break if not self.converged: dae.x[:] = np.array(self.x0) dae.y[:] = np.array(self.y0) dae.f[:] = np.array(self.f0) system.vars_to_models() self.last_converged = self.converged return self.converged
def init(self): """ Initialize the status, storage and values for TDS. Returns ------- array-like The initial values of xy. """ t0, _ = elapsed() system = self.system if self.initialized: return system.dae.xy self.reset() self._load_pert() # restore power flow solutions system.dae.x[:len(system.PFlow.x_sol)] = system.PFlow.x_sol system.dae.y[:len(system.PFlow.y_sol)] = system.PFlow.y_sol # Note: # calling `set_address` on `system.exist.pflow_tds` will point all variables # to the new array after extending `dae.y`. system.set_address(models=system.exist.pflow_tds) system.set_dae_names(models=system.exist.tds) system.set_output_subidx(models=system.exist.pflow_tds) system.dae.clear_ts() system.store_sparse_pattern(models=system.exist.pflow_tds) system.store_adder_setter(models=system.exist.pflow_tds) system.store_no_check_init(models=system.exist.pflow_tds) system.vars_to_models() system.init(system.exist.tds, routine='tds') self.fg_update(system.exist.tds, init=True) # reset diff. equation RHS for binding antiwindups for item in system.antiwindups: for key, _, eqval in item.x_set: np.put(system.dae.f, key, eqval) # only store switch times when not replaying CSV data if self.data_csv is None: system.store_switch_times(system.exist.tds) # Build mass matrix into `self.Teye` self.Teye = spdiag(system.dae.Tf.tolist()) self.qg = np.zeros(system.dae.n + system.dae.m) self.initialized = True # test if residuals are close enough to zero if self.config.test_init: self.test_ok = self.test_init() # discard initialized values and use that from CSV if provided if self.data_csv is not None: system.dae.x[:] = self.data_csv[0, 1:system.dae.n + 1] system.dae.y[:] = self.data_csv[0, system.dae.n + 1:system.dae.n + system.dae.m + 1] system.vars_to_models() # connect to data streaming server if system.streaming.dimec is None: system.streaming.connect() if system.config.dime_enabled: # send out system data using DiME self.streaming_init() self.streaming_step() # if `dae.n == 1`, `calc_h_first` depends on new `dae.gy` self.calc_h() # allocate for internal variables self.x0 = np.zeros_like(system.dae.x) self.y0 = np.zeros_like(system.dae.y) self.f0 = np.zeros_like(system.dae.f) _, s1 = elapsed(t0) logger.info("Initialization for dynamics completed in %s.", s1) if self.test_ok is True: logger.info("Initialization was successful.") elif self.test_ok is False: logger.error("Initialization failed!!") logger.error( "If you are developing a new model, check the initialization with" ) logger.error(" andes -v 10 run -r tds --init %s", self.system.files.case) logger.error( "Otherwise, check the variables that are initialized out of limits." ) else: logger.warning("Initialization results were not verified.") if system.dae.n == 0: tqdm.write('No differential equation detected.') return system.dae.xy
def _itm_step(self): """ Integrate with Implicit Trapezoidal Method (ITM) to the current time. This function has an internal Newton-Raphson loop for algebraized semi-explicit DAE. The function returns the convergence status when done but does NOT progress simulation time. Returns ------- bool Convergence status in ``self.converged``. """ system = self.system dae = self.system.dae self.mis = 1 self.niter = 0 self.converged = False self.x0 = np.array(dae.x) self.y0 = np.array(dae.y) self.f0 = np.array(dae.f) while True: self._fg_update(models=system.exist.pflow_tds) # lazy Jacobian update if dae.t == 0 or self.niter > 3 or (dae.t - self._last_switch_t < 0.2): system.j_update(models=system.exist.pflow_tds) self.solver.factorize = True # TODO: set the `Tf` corresponding to the pegged anti-windup limiters to zero. # Although this should not affect anything since corr. mismatches in `self.qg` are reset to zero # solve implicit trapezoidal method (ITM) integration self.Ac = sparse([[self.Teye - self.h * 0.5 * dae.fx, dae.gx], [-self.h * 0.5 * dae.fy, dae.gy]], 'd') # equation `self.qg[:dae.n] = 0` is the implicit form of differential equations using ITM self.qg[:dae.n] = dae.Tf * (dae.x - self.x0) - self.h * 0.5 * (dae.f + self.f0) # reset the corresponding q elements for pegged anti-windup limiter for item in system.antiwindups: for key, val in item.x_set: np.put(self.qg, key, 0) self.qg[dae.n:] = dae.g if not self.config.linsolve: inc = self.solver.solve(self.Ac, -matrix(self.qg)) else: inc = self.solver.linsolve(self.Ac, -matrix(self.qg)) # check for np.nan first if np.isnan(inc).any(): self.err_msg = 'NaN found in solution. Convergence not likely' self.niter = self.config.max_iter + 1 self.busted = True break # reset small values to reduce chattering inc[np.where(np.abs(inc) < self.tol_zero)] = 0 # set new values dae.x += inc[:dae.n].ravel() dae.y += inc[dae.n: dae.n + dae.m].ravel() system.vars_to_models() # calculate correction mis = np.max(np.abs(inc)) if self.niter == 0: self.mis = mis self.niter += 1 # converged if mis <= self.config.tol: self.converged = True break # non-convergence cases if self.niter > self.config.max_iter: logger.debug(f'Max. iter. {self.config.max_iter} reached for t={dae.t:.6f}, ' f'h={self.h:.6f}, mis={mis:.4g} ') # debug helpers g_max = np.argmax(abs(dae.g)) inc_max = np.argmax(abs(inc)) self._debug_g(g_max) self._debug_ac(inc_max) break if mis > 1000 and (mis > 1e8 * self.mis): self.err_msg = 'Error increased too quickly. Convergence not likely.' self.busted = True break if not self.converged: dae.x = np.array(self.x0) dae.y = np.array(self.y0) dae.f = np.array(self.f0) system.vars_to_models() return self.converged