Beispiel #1
0
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))
Beispiel #2
0
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.')
Beispiel #3
0
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)
Beispiel #4
0
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