def clear_fault(self, is_time: np.ndarray): """Clear fault and restore pre-fault bus voltages.""" for i in range(self.n): if is_time[i] and (self.u.v[i] == 1): self.uf.v[i] = 0 self.system.dae.y[self.system.Bus.v.a] = self._vstore tqdm.write( f'<Fault {i}>: Clearing fault on {self.bus.v[i]} at {self.tc.v[i]}.' ) return True return False
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() # 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.dae.clear_ts() system.store_sparse_pattern(models=system.exist.pflow_tds) system.store_adder_setter(models=system.exist.pflow_tds) system.vars_to_models() # Initialize `system.exist.tds` only to avoid Bus overwriting power flow solutions system.init(system.exist.tds) 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 = self.test_init() # if `dae.n == 1`, `calc_h_first` depends on new `dae.gy` self.calc_h() _, s1 = elapsed(t0) if self.initialized is True: logger.info(f"Initialization was successful in {s1}.") else: logger.error(f"Initialization failed in {s1}.") if system.dae.n == 0: tqdm.write('No dynamic component loaded.') return system.dae.xy
def apply_fault(self, is_time: np.ndarray): """Apply fault and store pre-fault bus voltages to ``self._vstore``.""" for i in range(self.n): if is_time[i] and (self.u.v[i] == 1): self.uf.v[i] = 1 self._vstore = np.array(self.system.Bus.v.v) tqdm.write( f'<Fault {i}>: Applying fault on {self.bus.v[i]} at {self.tf.v[i]}.' ) return True return False
def _u_switch(self, is_time: np.ndarray): action = False for i in range(self.n): if (is_time[i] == 0) or (self.u.v[i] == 0): continue instance = self.system.__dict__[self.model.v[i]] u0 = instance.get(src='u', attr='v', idx=self.dev.v[i]) instance.set(src='u', attr='v', idx=self.dev.v[i], value=1 - u0) action = True tqdm.write(f'<Toggle {self.idx.v[i]}>: ' f'{self.model.v[i]}.{self.dev.v[i]} status ' f'changed to {1-u0:g} at t={self.t.v[i]} sec.') return action
def clear_fault(self, is_time: np.ndarray): """ Clear fault and restore pre-fault bus algebraic variables (voltages and others). """ action = False for i in range(self.n): if is_time[i] and (self.u.v[i] == 1): self.uf.v[i] = 0 if self.config.restore: if self.config.mode == 1: self.system.dae.y[ self.system.Bus. n:] = self._vstore * self.config.scale logger.debug( "All algebraic variables restored after fault clearance at t=%.6f", self.system.dae.t) # TODO: neither mode 2 or 3 works. Pending further investigation. elif self.config.mode == 2: v_addr = self.system.Bus.get(src='v', idx=self.bus.v[i], attr='a') bus_uid = self.system.Bus.idx2uid(self.bus.v[i]) self.system.dae.y[ v_addr] = self._vstore[bus_uid] * self.config.scale logger.debug( "Voltage on bus %s restored after fault clearance at t=%.6f", self.bus.v[i], self.system.dae.t) elif self.config.mode == 3: nbus = self.system.Bus.n self.system.dae.y[ nbus:2 * nbus] = self._vstore[:nbus] * self.config.scale logger.debug( "All bus voltages restored after fault clearance at t=%.6f", self.system.dae.t) else: logger.error( "Unsupport fault voltage restoration mode") tqdm.write( f'<Fault {self.idx.v[i]}>: ' f'Clearing fault on Bus (idx={self.bus.v[i]}) at t={self.tc.v[i]} sec.' ) action = True return action
def _u_switch(self, is_time: np.ndarray): action = False for i in range(self.n): if is_time[i] and (self.u.v[i] == 1): instance = self.system.__dict__[self.model.v[i]] u0 = instance.get(src='u', attr='v', idx=self.dev.v[i]) instance.set(src='u', attr='v', idx=self.dev.v[i], value=1 - u0) action = True tqdm.write( f'<Toggle {i}>: Status of {self.model.v[i]}.{self.dev.v[i]} changed to {1-u0}.' ) return action
def clear_fault(self, is_time: np.ndarray): """ Clear fault and restore pre-fault bus algebraic variables (voltages and others). """ action = False for i in range(self.n): if is_time[i] and (self.u.v[i] == 1): self.uf.v[i] = 0 self.system.dae.y[self.system.Bus.n:] = self._vstore tqdm.write( f'<Fault {self.idx.v[i]}>: ' f'Clearing fault on Bus (idx={self.bus.v[i]}) at t={self.tc.v[i]}sec.' ) action = True return action
def apply_fault(self, is_time: np.ndarray): """ Apply fault and store pre-fault algebraic variables (voltages and other algebs) to `self._vstore`. """ action = False for i in range(self.n): if is_time[i] and (self.u.v[i] == 1): self.uf.v[i] = 1 self._vstore = np.array(self.system.dae.y[self.system.Bus.n:]) tqdm.write( f'<Fault {self.idx.v[i]}>: ' f'Applying fault on Bus (idx={self.bus.v[i]}) at t={self.tf.v[i]}sec.' ) action = True return action
def apply_exact(self, t): """ Apply the timeseries data at the exact time. Parameters ---------- t : float the current time """ # convert from numpy scalar to float t = t.tolist() for ii in range(self.n): # skip offline devices if self.u.v[ii] == 0: continue # check mode if self.SW.s1[ii] != 1: continue idx = self.idx.v[ii] df = self._data[idx] tkey = self.tkey.v[ii] # check if current time is a valid time stamp if t not in df[tkey].values: continue print("here 2") fields = self.fields.v[ii] dests = self.dests.v[ii] model = self.model.v[ii] dev_idx = self.dev.v[ii] # apply the value change for field, dest in zip(fields, dests): value = df.loc[df[tkey] == t, field].values if len(value) == 0: continue value = value[0] self.system.__dict__[model].set(dest, dev_idx, 'v', value) if not self.config.silent: tqdm.write("<TimeSeries %s> set %s=%g for %s.%s at t=%g" % (idx, dest, value, model, dev_idx, t))
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() system.set_address(models=system.exist.tds) system.set_dae_names(models=system.exist.tds) system.dae.clear_ts() system.store_sparse_pattern(models=system.exist.pflow_tds) system.store_adder_setter(models=system.exist.pflow_tds) system.vars_to_models() system.init(system.exist.tds) system.store_switch_times(system.exist.tds) self.eye = spdiag([1] * system.dae.n) self.Teye = spdiag(system.dae.Tf.tolist()) * self.eye self.qg = np.zeros(system.dae.n + system.dae.m) self.calc_h() self.initialized = self.test_init() _, s1 = elapsed(t0) if self.initialized is True: logger.info(f"Initialization was successful in {s1}.") else: logger.error(f"Initialization failed in {s1}.") if system.dae.n == 0: tqdm.write('No dynamic component loaded.') return system.dae.xy
def clear_fault(self, is_time: np.ndarray): """ Clear fault and restore pre-fault bus algebraic variables (voltages and others). """ action = False for i in range(self.n): if is_time[i] and (self.u.v[i] == 1): self.uf.v[i] = 0 if self.config.restore: self.system.dae.y[self.system.Bus. n:] = self._vstore * self.config.scale logger.debug( f"Voltage restored after fault clearance at t={self.system.dae.t:.6f}" ) tqdm.write( f'<Fault {self.idx.v[i]}>: ' f'Clearing fault on Bus (idx={self.bus.v[i]}) at t={self.tc.v[i]} sec.' ) action = True return action
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.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') # 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!!") 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.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 step(tds): """ 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 ``tds.converged``. """ system = tds.system dae = tds.system.dae if tds.h == 0: logger.error( "Current step size is zero. Integration is not permitted.") return False tds.mis = [1, 1] tds.niter = 0 tds.converged = False tds.x0[:] = dae.x tds.y0[:] = dae.y tds.f0[:] = dae.f while True: tds.fg_update(models=system.exist.pflow_tds) # lazy Jacobian update reason = '' if dae.t == 0: reason = 't=0' elif tds.config.honest: reason = 'using honest method' elif tds.custom_event: reason = 'custom event set' elif not tds.last_converged: reason = 'non-convergence in the last step' elif tds.niter > 4 and (tds.niter + 1) % 3 == 0: reason = 'every 3 iterations beyond 4 iterations' elif dae.t - tds._last_switch_t < 0.1: reason = 'within 0.1s of event' if reason: system.j_update(models=system.exist.pflow_tds, info=reason) # set flag in `solver.worker.factorize`, not `solver.factorize`. tds.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 if tds.config.g_scale > 0: gxs = tds.config.g_scale * tds.h * dae.gx gys = tds.config.g_scale * tds.h * dae.gy else: gxs = dae.gx gys = dae.gy # calculate complete Jacobian matrix ``Ac``` tds.Ac = tds.method.calc_jac(tds, gxs, gys) # equation `tds.qg[:dae.n] = 0` is the implicit form of differential equations using ITM tds.qg[:dae.n] = tds.method.calc_q(dae.x, dae.f, dae.Tf, tds.h, tds.x0, tds.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(tds.qg, key, eqval) # set or scale the algebraic residuals if tds.config.g_scale > 0: tds.qg[dae.n:] = tds.config.g_scale * tds.h * dae.g else: tds.qg[dae.n:] = dae.g # calculate variable corrections if not tds.config.linsolve: inc = tds.solver.solve(tds.Ac, matrix(tds.qg)) else: inc = tds.solver.linsolve(tds.Ac, matrix(tds.qg)) # check for np.nan first if np.isnan(inc).any(): tds.err_msg = 'NaN found in solution. Convergence is not likely' tds.niter = tds.config.max_iter + 1 tds.busted = True break # reset tiny values to reduce chattering if tds.config.reset_tiny: inc[np.where(np.abs(inc) < tds.tol_zero)] = 0 # set new values dae.x -= inc[:dae.n].ravel() dae.y -= inc[dae.n:dae.n + dae.m].ravel() # synchronize solutions to model internal storage system.vars_to_models() # store `inc` to tds for debugging tds.inc = inc mis = np.max(np.abs(inc)) # store initial maximum mismatch if tds.niter == 0: tds.mis[0] = mis else: tds.mis[-1] = mis tds.niter += 1 # converged if mis <= tds.config.tol: tds.converged = True break # non-convergence cases if tds.niter > tds.config.max_iter: if system.options.get("verbose", 20) <= 10: tqdm.write( f'* Max. iter. {tds.config.max_iter} reached for t={dae.t:.6f}s, ' f'h={tds.h:.6f}s, max inc={mis:.4g} ') # debug helpers g_max = np.argmax(abs(dae.g)) inc_max = np.argmax(abs(inc)) tds._debug_g(g_max) tds._debug_ac(inc_max) break if (mis > 1e6) and (mis > 1e6 * tds.mis[0]): tds.err_msg = 'Error increased too quickly.' break if not tds.converged: dae.x[:] = np.array(tds.x0) dae.y[:] = np.array(tds.y0) dae.f[:] = np.array(tds.f0) system.vars_to_models() tds.last_converged = tds.converged return tds.converged
def _alter_field(self, is_time): """ Actuation of the alteration. """ action = False for ii in range(self.n): if (not is_time[ii]) or (self.u.v[ii] == 0): continue model = self.system.__dict__[self.model.v[ii]] idx = self.dev.v[ii] src = self.src.v[ii] attr = self.attr.v[ii] amount = self.amount.v[ii] if self.rand.v[ii] == 1: amount = np.random.uniform(low=self.lb.v[ii], high=self.ub.v[ii]) try: v0 = model.get(src=src, idx=idx, attr=attr) except KeyError as e: tqdm.write( "\nError: <%s %s> cannot find idx=%s or src=%s in model <%s>. " % ( self.class_name, self.idx.v[ii], idx, src, self.model.v[ii], )) tqdm.write("<%s %s> disabled due to %s.\n" % (self.class_name, self.idx.v[ii], repr(e))) self.u.v[ii] = 0 continue vnew = v0 if self.SW.s0[ii] == 1: vnew = v0 + amount elif self.SW.s1[ii] == 1: vnew = v0 - amount elif self.SW.s2[ii] == 1: vnew = v0 * amount elif self.SW.s3[ii] == 1: vnew = v0 / amount elif self.SW.s4[ii] == 1: vnew = amount else: tqdm.write( 'Error: <%s %s>: undefined method "%s". <%s, %s> disabled.' % (self.class_name, self.idx.v[ii], self.method.v[ii], self.class_name, self.idx.v[ii])) self.u.v[ii] = 0 continue model.set(src=src, idx=idx, attr=attr, value=vnew) tqdm.write( '<Alter %s>: set %s.%s.%s.%s=%.6g at t=%.6g. Previous value was %.6g.' % (self.idx.v[ii], self.model.v[ii], idx, src, attr, vnew, self.t.v[ii], v0)) action = True return action
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.dae.clear_ts() system.store_sparse_pattern(models=system.exist.pflow_tds) system.store_adder_setter(models=system.exist.pflow_tds) system.vars_to_models() system.init(system.exist.tds, routine='tds') 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 = self.test_init() # connect to dime server if system.config.dime_enabled: if system.streaming.dimec is None: system.streaming.connect() # 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() _, s1 = elapsed(t0) if self.initialized is True: logger.info(f"Initialization for dynamics was successful in {s1}.") else: logger.error(f"Initialization for dynamics failed in {s1}.") if system.dae.n == 0: tqdm.write('No dynamic component loaded.') return system.dae.xy