class TDS(RoutineBase): """ Time domain simulation (TDS) routine """ def __init__(self, system, rc=None): self.system = system self.config = Tds(rc=rc) self.solver = Solver(system.config.sparselib) # internal states self.F = None self.initialized = False self.switch = False self.next_pc = 0.1 self.step = 0 self.t = self.config.t0 self.h = 0 self.headroom = 0 self.t_jac = 0 self.inc = None self.callpert = None self.solved = False self.fixed_times = [] self.convergence = True self.niter = 0 self.err = 1 self.x0 = None self.y0 = None self.f0 = None self.success = False def reset(self): self.F = None self.initialized = False self.switch = False self.next_pc = 0.1 self.step = 0 self.t = self.config.t0 self.h = 0 self.headroom = 0 self.t_jac = 0 self.inc = None self.callpert = None self.solved = False self.fixed_times = [] self.convergence = True self.niter = 0 self.err = 1 self.x0 = None self.y0 = None self.f0 = None self.success = False def _calc_time_step_first(self): """ Compute the first time step and save to ``self.h`` Returns ------- None """ system = self.system config = self.config if not system.dae.n: freq = 1.0 elif system.dae.n == 1: B = matrix(system.dae.Gx) self.solver.linsolve(system.dae.Gy, B) As = system.dae.Fx - system.dae.Fy * B freq = abs(As[0, 0]) else: freq = 20.0 if freq > system.freq: freq = float(system.freq) tspan = abs(config.tf - config.t0) tcycle = 1 / freq config.deltatmax = min(5 * tcycle, tspan / 100.0) config.deltat = min(tcycle, tspan / 100.0) config.deltatmin = min(tcycle / 64, config.deltatmax / 20) if config.fixt: if config.tstep <= 0: logger.warning('Fixed time step is negative or zero') logger.warning('Switching to automatic time step') config.fixt = False else: config.deltat = config.tstep if config.tstep < config.deltatmin: logger.warning( 'Fixed time step is below the estimated minimum') self.h = config.deltat def calc_time_step(self): """ Set the time step during time domain simulations Parameters ---------- convergence: bool truth value of the convergence of the last step niter: int current iteration count t: float current simulation time Returns ------- float computed time step size """ system = self.system config = self.config convergence = self.convergence niter = self.niter t = self.t if t == 0: self._calc_time_step_first() return if convergence: if niter >= 15: config.deltat = max(config.deltat * 0.5, config.deltatmin) elif niter <= 6: config.deltat = min(config.deltat * 1.1, config.deltatmax) else: config.deltat = max(config.deltat * 0.95, config.deltatmin) # adjust fixed time step if niter is high if config.fixt: config.deltat = min(config.tstep, config.deltat) else: config.deltat *= 0.9 if config.deltat < config.deltatmin: config.deltat = 0 if system.Fault.is_time(t) or system.Breaker.is_time(t): config.deltat = min(config.deltat, 0.002778) elif system.check_event(t): config.deltat = min(config.deltat, 0.002778) if config.method == 'fwdeuler': config.deltat = min(config.deltat, config.tstep) # last step size if self.t + config.deltat > config.tf: config.deltat = config.tf - self.t # reduce time step for fixed_times events for fixed_t in self.fixed_times: if (fixed_t > self.t) and (fixed_t <= self.t + config.deltat): config.deltat = fixed_t - self.t self.switch = True break self.h = config.deltat def init(self): """ Initialize time domain simulation Returns ------- None """ system = self.system if self.t != self.config.t0: logger.warning( 'TDS has been previously run and cannot be re-initialized. Please reload the system.' ) return False if system.pflow.solved is False: logger.info('Attempting to solve power flow before TDS.') if not system.pflow.run(): return False t, s = elapsed() # Assign indices for post-powerflow device variables system.xy_addr1() # Assign variable names for bus injections and line flows if enabled system.varname.resize_for_flows() system.varname.bus_line_names() # Reshape dae to retain power flow solutions system.dae.init1() # Initialize post-powerflow device variables for device, init1 in zip(system.devman.devices, system.call.init1): if init1: system.__dict__[device].init1(system.dae) t, s = elapsed(t) if system.dae.n: logger.debug('Dynamic models initialized in {:s}.'.format(s)) else: logger.debug('No dynamic model loaded.') # system.dae flags initialize system.dae.factorize = True system.dae.mu = 1.0 system.dae.kg = 0.0 self.initialized = True return True def run(self, **kwargs): """ Run time domain simulation Returns ------- bool Success flag """ # check if initialized if not self.initialized: if self.init() is False: logger.info( 'Call to TDS initialization failed in `tds.run()`.') ret = False system = self.system config = self.config dae = self.system.dae # maxit = config.maxit # tol = config.tol if system.pflow.solved is False: logger.warning( 'Power flow not solved. Simulation cannot continue.') return ret t0, _ = elapsed() t1 = t0 self.streaming_init() logger.info('') logger.info('-> Time Domain Simulation: {} method, t={} s'.format( self.config.method, self.config.tf)) self.load_pert() self.run_step0() config.qrtstart = time() while self.t < config.tf: self.check_fixed_times() self.calc_time_step() if self.callpert is not None: self.callpert(self.t, self.system) if self.h == 0: break # progress time and set time in dae self.t += self.h dae.t = self.t # backup actual variables self.x0 = matrix(dae.x) self.y0 = matrix(dae.y) self.f0 = matrix(dae.f) # apply fixed_time interventions and perturbations self.event_actions() # reset flags used in each step self.err = 1 self.niter = 0 self.convergence = False self.implicit_step() if self.convergence is False: try: self.restore_values() continue except ValueError: self.t = config.tf ret = False break self.step += 1 self.compute_flows() system.varout.store(self.t, self.step) self.streaming_step() # plot variables and display iteration status perc = max( min((self.t - config.t0) / (config.tf - config.t0) * 100, 100), 0) # show iteration info every 30 seconds or every 20% t2, _ = elapsed(t1) if t2 - t1 >= 30: t1 = t2 logger.info( ' ({:.0f}%) time = {:.4f}s, step = {}, niter = {}'.format( 100 * self.t / config.tf, self.t, self.step, self.niter)) if perc > self.next_pc or self.t == config.tf: self.next_pc += 20 logger.info( ' ({:.0f}%) time = {:.4f}s, step = {}, niter = {}'.format( 100 * self.t / config.tf, self.t, self.step, self.niter)) # compute max rotor angle difference # diff_max = anglediff() # quasi-real-time check and wait rt_end = config.qrtstart + (self.t - config.t0) * config.kqrt if config.qrt: # the ending time has passed if time() - rt_end > 0: # simulation is too slow if time() - rt_end > config.kqrt: logger.debug( 'Simulation over-run at t={:4.4g} s.'.format( self.t)) # wait to finish else: self.headroom += (rt_end - time()) while time() - rt_end < 0: sleep(1e-5) if config.qrt: logger.debug('RT headroom time: {} s.'.format(str(self.headroom))) if self.t != config.tf: logger.error( 'Reached minimum time step. Convergence is not likely.') ret = False else: ret = True if system.config.dime_enable: system.streaming.finalize() _, s = elapsed(t0) if ret is True: logger.info(' Time domain simulation finished in {:s}.'.format(s)) else: logger.info(' Time domain simulation failed in {:s}.'.format(s)) self.success = ret self.dump_results(success=self.success) return ret def restore_values(self): """ Restore x, y, and f values if not converged Returns ------- None """ if self.convergence is True: return dae = self.system.dae system = self.system inc_g = self.inc[dae.n:dae.m + dae.n] max_g_err_sign = 1 if abs(max(inc_g)) > abs(min(inc_g)) else -1 if max_g_err_sign == 1: max_g_err_idx = list(inc_g).index(max(inc_g)) else: max_g_err_idx = list(inc_g).index(min(inc_g)) logger.debug('Maximum mismatch = {:.4g} at equation <{}>'.format( max(abs(inc_g)), system.varname.unamey[max_g_err_idx])) logger.debug('Reducing time step h={:.4g}s for t={:.4g}'.format( self.h, self.t)) # restore initial variable data dae.x = matrix(self.x0) dae.y = matrix(self.y0) dae.f = matrix(self.f0) def implicit_step(self): """ Integrate one step using trapezoidal method. Sets convergence and niter flags. Returns ------- None """ config = self.config system = self.system dae = self.system.dae # constant short names In = spdiag([1] * dae.n) h = self.h while self.err > config.tol and self.niter < config.maxit: if self.t - self.t_jac >= 5: dae.rebuild = True self.t_jac = self.t elif self.niter > 4: dae.rebuild = True elif dae.factorize: dae.rebuild = True # rebuild Jacobian if dae.rebuild: system.call.int() dae.rebuild = False else: system.call.int_fg() # complete Jacobian matrix dae.Ac if config.method == 'euler': dae.Ac = sparse( [[In - h * dae.Fx, dae.Gx], [-h * dae.Fy, dae.Gy]], 'd') dae.q = dae.x - self.x0 - h * dae.f elif config.method == 'trapezoidal': dae.Ac = sparse([[In - h * 0.5 * dae.Fx, dae.Gx], [-h * 0.5 * dae.Fy, dae.Gy]], 'd') dae.q = dae.x - self.x0 - h * 0.5 * (dae.f + self.f0) # windup limiters dae.reset_Ac() if dae.factorize: try: self.F = self.solver.symbolic(dae.Ac) dae.factorize = False except NotImplementedError: pass self.inc = -matrix([dae.q, dae.g]) try: N = self.solver.numeric(dae.Ac, self.F) self.solver.solve(dae.Ac, self.F, N, self.inc) except ArithmeticError: logger.error('Singular matrix') dae.check_diag(dae.Gy, 'unamey') dae.check_diag(dae.Fx, 'unamex') # force quit self.niter = config.maxit + 1 break except ValueError: logger.warning('Unexpected symbolic factorization') dae.factorize = True continue except NotImplementedError: self.inc = self.solver.linsolve(dae.Ac, self.inc) inc_x = self.inc[:dae.n] inc_y = self.inc[dae.n:dae.m + dae.n] dae.x += inc_x dae.y += inc_y self.err = max(abs(self.inc)) if np.isnan(self.inc).any(): logger.error('Iteration error: NaN detected.') self.niter = config.maxit + 1 break self.niter += 1 if self.niter <= config.maxit: self.convergence = True def event_actions(self): """ Take actions for timed events Returns ------- None """ system = self.system dae = system.dae if self.switch: system.Breaker.apply(self.t) for item in system.check_event(self.t): system.__dict__[item].apply(self.t) dae.rebuild = True self.switch = False def check_fixed_times(self): """ Check for fixed times and store in ``self.fixed_times``. Returns ------- None """ self.fixed_times = self.system.get_event_times() def load_pert(self): """ Load perturbation files to ``self.callpert`` Returns ------- None """ system = self.system if system.files.pert: try: sys.path.append(system.files.case_path) module = importlib.import_module(system.files.pert[:-3]) self.callpert = getattr(module, 'pert') except ImportError: logger.warning('Pert file is discarded due to import errors.') self.callpert = None def run_step0(self): """ For the 0th step, store the data and stream data Returns ------- None """ dae = self.system.dae system = self.system config = self.config # compute line and area flow if config.compute_flows: dae.init_fg() self.compute_flows() # TODO: move to PowerSystem self.inc = zeros(dae.m + dae.n, 1) system.varout.store(self.t, self.step) self.streaming_step() def streaming_step(self): """ Sync, handle and streaming for each integration step Returns ------- None """ system = self.system if system.config.dime_enable: system.streaming.sync_and_handle() system.streaming.vars_to_modules() system.streaming.vars_to_pmu() def streaming_init(self): """ Send out initialization variables and process init from modules Returns ------- None """ system = self.system config = self.config if system.config.dime_enable: config.compute_flows = True system.streaming.send_init(recepient='all') logger.info('Waiting for modules to send init info...') sleep(0.5) system.streaming.sync_and_handle() def angle_diff(self): """ Compute the maximum angle difference between generators Returns ------- float maximum angular difference """ return 0 def compute_flows(self): """ If enabled, compute the line flows after each step Returns ------- None """ system = self.system config = self.config dae = system.dae if config.compute_flows: # compute and append series injections on buses system.call.bus_injection() bus_inj = dae.g[:2 * system.Bus.n] system.call.seriesflow() system.Area.seriesflow(system.dae) system.Area.interchange_varout() dae.y = matrix([ dae.y[:system.dae.m], bus_inj, system.Line._line_flows, system.Area.inter_varout ]) def dump_results(self, success): """ Dump simulation results to ``dat`` and ``lst`` files Returns ------- None """ system = self.system t, _ = elapsed() if success and (not system.files.no_output): # system.varout.dump() system.varout.dump_np_vars() _, s = elapsed(t) logger.info('Simulation data dumped in {:s}.'.format(s))
class EIG(RoutineBase): """ Eigenvalue analysis routine """ def __init__(self, system, rc=None): self.system = system self.solver = Solver(system.config.sparselib) self.config = Eig(rc=rc) # internal flags and storages self.As = None self.eigs = None self.mu = None self.part_fact = None def calc_state_matrix(self): """ Return state matrix and store to ``self.As`` Returns ------- matrix state matrix """ system = self.system Gyx = matrix(system.dae.Gx) self.solver.linsolve(system.dae.Gy, Gyx) self.As = matrix(system.dae.Fx - system.dae.Fy * Gyx) # ------------------------------------------------------ # TODO: use scipy eigs # self.As = sparse(self.As) # I = np.array(self.As.I).reshape((-1,)) # J = np.array(self.As.J).reshape((-1,)) # V = np.array(self.As.V).reshape((-1,)) # self.As = csr_matrix((V, (I, J)), shape=self.As.size) # ------------------------------------------------------ return self.As def calc_eigvals(self): """ Solve eigenvalues of the state matrix ``self.As`` Returns ------- None """ self.eigs = numpy.linalg.eigvals(self.As) # TODO: use scipy.sparse.linalg.eigs(self.As) return self.eigs def calc_part_factor(self): """ Compute participation factor of states in eigenvalues Returns ------- """ mu, N = numpy.linalg.eig(self.As) # TODO: use scipy.sparse.linalg.eigs(self.As) N = matrix(N) n = len(mu) idx = range(n) W = matrix(spmatrix(1.0, idx, idx, (n, n), N.typecode)) gesv(N, W) partfact = mul(abs(W.T), abs(N)) b = matrix(1.0, (1, n)) WN = b * partfact partfact = partfact.T for item in idx: mu_real = mu[item].real mu_imag = mu[item].imag mu[item] = complex(round(mu_real, 4), round(mu_imag, 4)) partfact[item, :] /= WN[item] # participation factor self.mu = matrix(mu) self.part_fact = matrix(partfact) return self.mu, self.part_fact def run(self): ret = False system = self.system if system.pflow.solved is False: logger.warning( 'Power flow not solved. Eig analysis will not continue.') return ret elif system.dae.n == 0: logger.warning('No dynamic model. Eig analysis will not continue.') return ret t1, s = elapsed() logger.info('-> Eigenvalue Analysis:') system.dae.factorize = True exec(system.call.int) self.calc_state_matrix() self.calc_part_factor() self.dump_results() self.plot_results() ret = True t2, s = elapsed(t1) logger.info('Eigenvalue analysis finished in {:s}.'.format(s)) return ret def plot_results(self): try: plt = importlib.import_module('matplotlib.pyplot') except ImportError: plt = None if plt is None: logger.warning('Install matplotlib to plot eigenvalue map.') return 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.') if self.config.plot and len(p_mu_real) > 0: fig, ax = plt.subplots() ax.scatter(n_mu_real, n_mu_imag, marker='x', s=26, color='green') ax.scatter(z_mu_real, z_mu_imag, marker='o', s=26, color='orange') ax.scatter(p_mu_real, p_mu_imag, marker='x', s=26, color='red') plt.show() def dump_results(self): """ Save eigenvalue analysis reports Returns ------- None """ system = self.system mu = self.mu partfact = self.part_fact if system.files.no_output: return text = [] header = [] rowname = [] data = [] neig = len(mu) mu_real = mu.real() mu_imag = mu.imag() npositive = sum(1 for x in mu_real if x > 0) nzero = sum(1 for x in mu_real if x == 0) nnegative = sum(1 for x in mu_real if x < 0) numeral = [] for idx, item in enumerate(range(neig)): if mu_real[idx] == 0: marker = '*' elif mu_real[idx] > 0: marker = '**' else: marker = '' numeral.append('#' + str(idx + 1) + marker) # compute frequency, undamped frequency and damping freq = [0] * neig ufreq = [0] * neig damping = [0] * neig for idx, item in enumerate(mu): if item.imag == 0: freq[idx] = 0 ufreq[idx] = 0 damping[idx] = 0 else: freq[idx] = abs(item) / 2 / pi ufreq[idx] = abs(item.imag / 2 / pi) damping[idx] = -div(item.real, abs(item)) * 100 # obtain most associated variables var_assoc = [] for prow in range(neig): temp_row = partfact[prow, :] name_idx = list(temp_row).index(max(temp_row)) var_assoc.append(system.varname.unamex[name_idx]) pf = [] for prow in range(neig): temp_row = [] for pcol in range(neig): temp_row.append(round(partfact[prow, pcol], 5)) pf.append(temp_row) text.append(system.report.info) header.append(['']) rowname.append(['EIGENVALUE ANALYSIS REPORT']) data.append('') text.append('STATISTICS\n') header.append(['']) rowname.append(['Positives', 'Zeros', 'Negatives']) data.append([npositive, nzero, nnegative]) text.append('EIGENVALUE DATA\n') header.append([ 'Most Associated', 'Real', 'Imag', 'Damped Freq.', 'Frequency', 'Damping [%]' ]) rowname.append(numeral) data.append( [var_assoc, list(mu_real), list(mu_imag), ufreq, freq, damping]) cpb = 7 # columns per block nblock = int(ceil(neig / cpb)) if nblock <= 100: for idx in range(nblock): start = cpb * idx end = cpb * (idx + 1) text.append('PARTICIPATION FACTORS [{}/{}]\n'.format( idx + 1, nblock)) header.append(numeral[start:end]) rowname.append(system.varname.unamex) data.append(pf[start:end]) dump_data(text, header, rowname, data, system.files.eig) logger.info('report saved.')
class AGCMPC(ModelBase): """MPC based AGC using TG and VSC""" def __init__(self, system, name): super(AGCMPC, self).__init__(system, name) if platform.system() == 'Darwin': logger.error( "** AGCMPC optimization does not work correctly on macOS!!!") self._group = "AGCGroup" self._name = "AGCMPC" self.param_remove('Vn') self.param_remove('Sn') self._data.update({ 'tg': None, 'avr': None, 'vsc': None, 'qw': 15000, 'qu': 10, }) self._params.extend(['qw', 'qu']) self._descr.update({ 'tg': 'idx for turbine governors', 'vsc': 'idx for VSC dynamic models', 'qw': 'the coeff for minimizing frequency deviation', 'qu': 'the coeff for minimizing input deviation' }) self._units.update({'tg': 'list', 'vsc': 'list'}) self._mandatory.extend(['tg', 'avr']) self.calls.update({ 'init1': True, 'gcall': True, 'jac0': True, 'fxcall': True }) self._service.extend([ 'xg10', 'pin0', 'delta0', 'omega0', 't', 'dpin0', 'x0', 'xlast' 'xidx', 'uidx', 'yxidx', 'sfx', 'sfu', 'sfy', 'sgx', 'sgu', 'sgy', 'A', 'B', 'Aa', 'Ba', 'obj', 'domega', 'du', 'dx', 'x', 'xpred' 'xa' ]) self._algebs.extend(['dpin']) self._fnamey.extend(r'\Delta P_{in}') self.solver = Solver(system.config.sparselib) self.H = 6 self.uvar = None self.op = None self._linearized = False self._interval = 0 # AGC apply interval in seconds. 0 - continuous self._init() def init1(self, dae): if globals()['cp'] is None: try: globals()['cp'] = importlib.import_module('cvxpy') except ImportError: logger.error( 'CVXPY import error. Install optional package `cvxpy` to use AGCMPC' ) sys.exit(1) self.t = -1 self.tlast = -1 # state array x = [delta, omega, xg1] # input array u = [dpin] self.copy_data_ext('Governor', field='gen', dest='syn', idx=self.tg) self.copy_data_ext('Synchronous', field='delta', dest='delta', idx=self.syn) self.copy_data_ext('Synchronous', field='omega', dest='omega', idx=self.syn) self.copy_data_ext('Synchronous', field='e1d', dest='e1d', idx=self.syn) self.copy_data_ext('Synchronous', field='e1q', dest='e1q', idx=self.syn) self.copy_data_ext('Synchronous', field='e2d', dest='e2d', idx=self.syn) self.copy_data_ext('Synchronous', field='e2q', dest='e2q', idx=self.syn) self.copy_data_ext('Governor', field='xg1', dest='xg1', idx=self.tg) self.copy_data_ext('Governor', field='xg2', dest='xg2', idx=self.tg) self.copy_data_ext('Governor', field='xg3', dest='xg3', idx=self.tg) self.copy_data_ext('Governor', field='pin', dest='pin', idx=self.tg) self.copy_data_ext('AVR', field='vm', dest='vm', idx=self.avr) self.copy_data_ext('AVR', field='vr1', dest='vr1', idx=self.avr) self.copy_data_ext('AVR', field='vr2', dest='vr2', idx=self.avr) self.copy_data_ext('AVR', field='vfout', dest='vfout', idx=self.avr) dae.y[self.dpin] = 0 self.dpin0 = zeros(self.n, 1) # build state/ input /other algebraic idx array self.xidx = matrix([ self.delta, self.omega, self.e1d, self.e1q, self.e2d, self.e2q, self.xg1, self.xg2, self.xg3, self.vm, self.vr1, self.vr2, self.vfout ]) self.x0 = dae.x[self.xidx] self.x = zeros(len(self.xidx), 1) self.dx = zeros(len(self.xidx), 1) self.xlast = dae.x[self.xidx] self.uidx = matrix([self.dpin]) self.ulast = zeros(self.n, 1) self.dpin_calc = zeros(self.n, 1) self.widx = self.system.PQ.a self.w0 = self.system.PQ.p0 self.wlast = matrix(self.w0) self.yidx = self.omega self.yidx_in_x = [index(self.xidx, y)[0] for y in self.yidx] yidx = np.delete(np.arange(dae.m), np.array(self.uidx)) self.yxidx = matrix(yidx) # optimization problem self.uvar = cp.Variable((len(self.uidx), self.H + 1), 'u') self.uzero = cp.Parameter((len(self.uidx), ), 'u0') self.xazero = cp.Parameter((2 * len(self.xidx), 1), 'xa') self.prob = None self.t_store = [] self.xpred_store = [] def gcall(self, dae): if self.t == -1: self.t = dae.t return if not self._linearized: # update the linearization points self._linearized = True self.t = dae.t self.tlast = dae.t self.sfx = dae.Fx[self.xidx, self.xidx] self.sfu = dae.Fy[self.xidx, self.uidx] self.sfy = dae.Fy[self.xidx, self.yxidx] self.sgx = dae.Gx[self.yxidx, self.xidx] self.sgu = dae.Gy[self.yxidx, self.uidx] self.sgw = spmatrix(1, self.widx, list(range(len(self.widx))), (len(self.yxidx), len(self.widx))) self.sgy = dae.Gy[self.yxidx, self.yxidx] # create state matrices self.gyigx = matrix(self.sgx) self.gyigu = matrix(self.sgu) self.gyigw = matrix(self.sgw) self.solver.linsolve(self.sgy, self.gyigx) self.solver.linsolve(self.sgy, self.gyigu) self.solver.linsolve(self.sgy, self.gyigw) self.A = (self.sfx - self.sfy * self.gyigx) self.B = (self.sfu - self.sfy * self.gyigu) self.C = -(self.sfy * self.gyigw) self.A = self.system.tds.h * self.A self.Aa = sparse([[self.A, self.A], [ spmatrix([], [], [], (self.A.size[0], self.A.size[1])), spdiag([1] * len(self.xidx)) ]]) self.Ba = sparse([self.B, self.B]) self.Ca = sparse([self.C, self.C]) # formulate optimization problem nx = len(self.xidx) nu = len(self.uidx) obj_x = 0 xa_0 = self.xazero for i in range(self.H): # calculate Xa for each step in horizon H # du = cp.reshape(self.uvar[:, i+1], (nu, 1)) - self.uvar[:,i] du = cp.reshape(self.uvar[:, i + 1] - self.uvar[:, i], (nu, 1)) xa_i = matrix(self.Aa) * xa_0 + matrix(self.Ba) * du obj_x += cp.multiply( self.qw, cp.square(xa_i[nx:][self.yidx_in_x] - self.x0[self.yidx_in_x])) xa_0 = xa_i # construct the optimization problem self.obj_x = cp.sum(obj_x) self.obj_u = 0 self.obj_u += cp.sum( cp.multiply( np.array(self.qu).reshape((nu, )), cp.sum(cp.square(self.uvar[:, 1:] - self.uvar[:, :-1]), axis=1))) constraints = [ self.uvar[:, 0] == self.uzero, self.uvar[:, 1:] - self.uvar[:, :-1] <= 0.5, self.uvar[:, 1:] - self.uvar[:, :-1] >= -0.5 ] self.prob = cp.Problem(cp.Minimize(self.obj_x + self.obj_u), constraints) if dae.t != self.t: self.t = dae.t nx = len(self.xidx) nu = len(self.uidx) # # update Delta x and x for current step self.x = dae.x[self.xidx] self.dx = self.x - self.xlast self.xa = matrix([self.dx, self.x]) # assign values to self.uzero and self.xazero self.uzero.value = np.array(self.ulast).reshape((-1, )) self.xazero.value = np.array(self.xa).reshape((-1, 1)) # use warm_start when possible if dae.t == 0: self.prob.solve() else: self.prob.solve(warm_start=1) self.dpin_calc = matrix(self.uvar.value[:, 1]) # update every interval if (self.t - self.tlast) >= self._interval: self.tlast = self.t self.dpin0 = self.dpin_calc opt_val = self.prob.solution.opt_val logger.debug("t={:.4f}, obj={:.6f}, u[0]={:.6f}".format( dae.t, opt_val, self.uvar.value[0, 0])) self.t_store.append(self.t) xa_post = matrix(self.Aa) * self.xa + matrix( self.Ba) * (matrix(self.uvar.value[:, 0]) - self.ulast) self.xpred_store.append(xa_post[nx:][self.yidx_in_x][0]) # # post-optimization evaluator # # u_val = matrix([[0, 0], [0, 0], [0, 0]]) # u_val = matrix(self.uvar.value) # u_val = zeros(2, self.H) # obj_x = 0 # xa_0 = self.xa # u_0 = self.ulast # for i in range(self.H): # # calculate Xa for each step in horizon H # du = np.reshape(u_val[:, i], (-1, 1)) - u_0 # xa_i = matrix(self.Aa) * xa_0 + matrix(self.Ba) * matrix(du) #+ matrix(self.Ca) * self.dw # obj_x += mul(self.qw, (xa_i[nx:][self.yidx_in_x] - self.x0[self.yidx_in_x]) ** 2) # xa_0 = xa_i # u_0 = np.reshape(u_val[:, i], (-1, 1)) # self.obj_x = sum(obj_x) # u2 = np.array(mul(u_val, u_val)) # self.obj_u = sum(mul(self.qu, matrix(np.sum(u2, 1)))) # # eval_obj = self.obj_x + self.obj_u # print("Post eval, t={:.4f} obj = {:.6f}, u = {:.6f}, {:.6f}".format(self.t, eval_obj, u_val[0, 0], # u_val[1, 0])) # print(" obj_x = {}, obj_u = {}".format(self.obj_x, self.obj_u)) # record data for the current step self.ulast = self.dpin_calc self.xlast = dae.x[self.xidx] dae.g[self.dpin] = dae.y[self.dpin] - self.dpin0 dae.g[self.pin] += dae.y[ self.dpin] # positive `dpin` increases the `pin` reference def jac0(self, dae): dae.add_jac(Gy0, 1, self.dpin, self.dpin) dae.add_jac(Gy0, 1, self.pin, self.dpin)
class PFLOW(RoutineBase): """ Power flow calculation routine """ def __init__(self, system, rc=None): self.system = system self.config = Pflow(rc=rc) self.solver = Solver(system.config.sparselib) # store status and internal flags self.solved = False self.niter = 0 self.iter_mis = [] self.F = None def reset(self): """ Reset all internal storage to initial status Returns ------- None """ self.solved = False self.niter = 0 self.iter_mis = [] self.F = None self.system.dae.factorize = True def pre(self): """ Initialize system for power flow study Returns ------- None """ if self.solved and self.system.tds.initialized: logger.error( 'TDS has been initialized. Cannot solve power flow again.') return False logger.info('-> Power flow study: {} method, {} start'.format( self.config.method.upper(), 'flat' if self.config.flatstart else 'non-flat')) t, s = elapsed() system = self.system dae = self.system.dae system.dae.init_xy() for device, pflow, init0 in zip(system.devman.devices, system.call.pflow, system.call.init0): if pflow and init0: system.__dict__[device].init0(dae) # check for islands system.check_islands(show_info=True) # reset internal storage self.reset() t, s = elapsed(t) logger.debug('Power flow initialized in {:s}.'.format(s)) return True def run(self, **kwargs): """ call the power flow solution routine Returns ------- bool True for success, False for fail """ ret = None # initialization Y matrix and inital guess if not self.pre(): return False t, _ = elapsed() # call solution methods if self.config.method == 'NR': ret = self.newton() elif self.config.method == 'DCPF': ret = self.dcpf() elif self.config.method in ('FDPF', 'FDBX', 'FDXB'): ret = self.fdpf() self.post() _, s = elapsed(t) if self.solved: logger.info(' Solution converged in {} in {} iterations'.format( s, self.niter)) else: logger.warning(' Solution failed in {} in {} iterations'.format( s, self.niter)) return ret def newton(self): """ Newton power flow routine Returns ------- bool success flag """ dae = self.system.dae while True: inc = self.calc_inc() dae.x += inc[:dae.n] dae.y += inc[dae.n:dae.n + dae.m] self.niter += 1 max_mis = max(abs(inc)) self.iter_mis.append(max_mis) self._iter_info(self.niter) if max_mis < self.config.tol: self.solved = True break elif self.niter > 5 and max_mis > 1000 * self.iter_mis[0]: logger.warning('Blown up in {0} iterations.'.format( self.niter)) break if self.niter > self.config.maxit: logger.warning('Reached maximum number of iterations.') break return self.solved def dcpf(self): """ Calculate linearized power flow Returns ------- bool success flag, number of iterations """ dae = self.system.dae self.system.Bus.init0(dae) self.system.dae.init_g() Va0 = self.system.Bus.angle for model, pflow, gcall in zip(self.system.devman.devices, self.system.call.pflow, self.system.call.gcall): if pflow and gcall: self.system.__dict__[model].gcall(dae) sw = self.system.SW.a sw.sort(reverse=True) no_sw = self.system.Bus.a[:] no_swv = self.system.Bus.v[:] for item in sw: no_sw.pop(item) no_swv.pop(item) Bp = self.system.Line.Bp[no_sw, no_sw] p = matrix(self.system.dae.g[no_sw], (no_sw.__len__(), 1)) p = p - self.system.Line.Bp[no_sw, sw] * Va0[sw] Sp = self.solver.symbolic(Bp) N = self.solver.numeric(Bp, Sp) self.solver.solve(Bp, Sp, N, p) self.system.dae.y[no_sw] = p self.solved = True self.niter = 1 return self.solved def _iter_info(self, niter, level=logging.INFO): """ Log iteration number and mismatch Parameters ---------- level logging level Returns ------- None """ max_mis = self.iter_mis[niter - 1] msg = ' Iter {:<d}. max mismatch = {:8.7f}'.format(niter, max_mis) logger.info(msg) def calc_inc(self): """ Calculate the Newton incrementals for each step Returns ------- matrix The solution to ``x = -A\\b`` """ system = self.system self.newton_call() A = sparse([[system.dae.Fx, system.dae.Gx], [system.dae.Fy, system.dae.Gy]]) inc = matrix([system.dae.f, system.dae.g]) if system.dae.factorize: try: self.F = self.solver.symbolic(A) system.dae.factorize = False except NotImplementedError: pass try: N = self.solver.numeric(A, self.F) self.solver.solve(A, self.F, N, inc) except ValueError: logger.warning('Unexpected symbolic factorization.') system.dae.factorize = True except ArithmeticError: logger.warning('Jacobian matrix is singular.') system.dae.check_diag(system.dae.Gy, 'unamey') except NotImplementedError: inc = self.solver.linsolve(A, inc) return -inc def newton_call(self): """ Function calls for Newton power flow Returns ------- None """ # system = self.system # exec(system.call.newton) system = self.system dae = self.system.dae system.dae.init_fg() system.dae.reset_small_g() # evaluate algebraic equation mismatches for model, pflow, gcall in zip(system.devman.devices, system.call.pflow, system.call.gcall): if pflow and gcall: system.__dict__[model].gcall(dae) # eval differential equations for model, pflow, fcall in zip(system.devman.devices, system.call.pflow, system.call.fcall): if pflow and fcall: system.__dict__[model].fcall(dae) # reset islanded buses mismatches system.Bus.gisland(dae) if system.dae.factorize: system.dae.init_jac0() # evaluate constant Jacobian elements for model, pflow, jac0 in zip(system.devman.devices, system.call.pflow, system.call.jac0): if pflow and jac0: system.__dict__[model].jac0(dae) dae.temp_to_spmatrix('jac0') dae.setup_FxGy() # evaluate Gy for model, pflow, gycall in zip(system.devman.devices, system.call.pflow, system.call.gycall): if pflow and gycall: system.__dict__[model].gycall(dae) # evaluate Fx for model, pflow, fxcall in zip(system.devman.devices, system.call.pflow, system.call.fxcall): if pflow and fxcall: system.__dict__[model].fxcall(dae) # reset islanded buses Jacobians system.Bus.gyisland(dae) dae.temp_to_spmatrix('jac') def post(self): """ Post processing for solved systems. Store load, generation data on buses. Store reactive power generation on PVs and slack generators. Calculate series flows and area flows. Returns ------- None """ if not self.solved: return system = self.system exec(system.call.pfload) system.Bus.Pl = system.dae.g[system.Bus.a] system.Bus.Ql = system.dae.g[system.Bus.v] exec(system.call.pfgen) system.Bus.Pg = system.dae.g[system.Bus.a] system.Bus.Qg = system.dae.g[system.Bus.v] if system.PV.n: system.PV.qg = system.dae.y[system.PV.q] if system.SW.n: system.SW.pg = system.dae.y[system.SW.p] system.SW.qg = system.dae.y[system.SW.q] exec(system.call.seriesflow) system.Area.seriesflow(system.dae) def fdpf(self): """ Fast Decoupled Power Flow Returns ------- bool Success flag """ system = self.system # general settings self.niter = 1 iter_max = self.config.maxit self.solved = True tol = self.config.tol error = tol + 1 self.iter_mis = [] if (not system.Line.Bp) or (not system.Line.Bpp): system.Line.build_b() # initialize indexing and Jacobian # ngen = system.SW.n + system.PV.n sw = system.SW.a sw.sort(reverse=True) no_sw = system.Bus.a[:] no_swv = system.Bus.v[:] for item in sw: no_sw.pop(item) no_swv.pop(item) gen = system.SW.a + system.PV.a gen.sort(reverse=True) no_g = system.Bus.a[:] no_gv = system.Bus.v[:] for item in gen: no_g.pop(item) no_gv.pop(item) Bp = system.Line.Bp[no_sw, no_sw] Bpp = system.Line.Bpp[no_g, no_g] # Fp = self.solver.symbolic(Bp) # Fpp = self.solver.symbolic(Bpp) # Np = self.solver.numeric(Bp, Fp) # Npp = self.solver.numeric(Bpp, Fpp) exec(system.call.fdpf) # main loop while error > tol: # P-theta da = matrix(div(system.dae.g[no_sw], system.dae.y[no_swv])) # self.solver.solve(Bp, Fp, Np, da) da = self.solver.linsolve(Bp, da) system.dae.y[no_sw] += da exec(system.call.fdpf) normP = max(abs(system.dae.g[no_sw])) # Q-V dV = matrix(div(system.dae.g[no_gv], system.dae.y[no_gv])) # self.solver.solve(Bpp, Fpp, Npp, dV) dV = self.solver.linsolve(Bpp, dV) system.dae.y[no_gv] += dV exec(system.call.fdpf) normQ = max(abs(system.dae.g[no_gv])) err = max([normP, normQ]) self.iter_mis.append(err) error = err self._iter_info(self.niter) self.niter += 1 if self.niter > 4 and self.iter_mis[-1] > 1000 * self.iter_mis[0]: logger.warning('Blown up in {0} iterations.'.format( self.niter)) self.solved = False break if self.niter > iter_max: logger.warning('Reached maximum number of iterations.') self.solved = False break return self.solved