https://github.com/PyPSA/PyPSA
Raw File
Tip revision: 63b40c578b3f3381a4d0c9b43eb80783b3e61276 authored by Tom Brown on 21 March 2016, 18:55:20 UTC
Version 0.4.0
Tip revision: 63b40c5
pf.py
## Copyright 2015-2016 Tom Brown (FIAS), Jonas Hoersch (FIAS)

## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation; either version 3 of the
## License, or (at your option) any later version.

## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.

## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Power flow functionality.
"""


# make the code as Python 3 compatible as possible
from __future__ import print_function, division
from __future__ import absolute_import
from six.moves import range

__author__ = "Tom Brown (FIAS), Jonas Hoersch (FIAS)"
__copyright__ = "Copyright 2015-2016 Tom Brown (FIAS), Jonas Hoersch (FIAS), GNU GPL 3"



from scipy.sparse import issparse, csr_matrix, csc_matrix, hstack as shstack, vstack as svstack, dok_matrix

from numpy import r_, ones, zeros, newaxis
from scipy.sparse.linalg import spsolve

import numpy as np
import pandas as pd
import networkx as nx

from itertools import chain


from numpy.linalg import norm

import time

def _as_snapshots(network, snapshots, now):
    if snapshots is None:
        if now is None:
            now = network.now
        snapshots = [now]
    try:
        return pd.Index(snapshots)
    except TypeError:
        return pd.Index([snapshots])

def network_pf(network,now=None,verbose=True,skip_pre=False,x_tol=1e-6):
    """
    Full non-linear power flow for generic network.

    Parameters
    ----------
    now : object
        A member of network.snapshots on which to run the power flow, defaults to network.now
    verbose: bool, default True
    skip_pre: bool, default False
        Skip the preliminary steps of computing topology, calculating dependent values and finding bus controls.
    x_tol: float
        Tolerance for Newton-Raphson power flow.

    Returns
    -------
    None
    """




    if not skip_pre:
        network.determine_network_topology()
        calculate_dependent_values(network)

    if now is None:
        now=network.now


    #deal with transport links and converters
    network.converters_t.p0.loc[now] = network.converters_t.p_set.loc[now]
    network.converters_t.p1.loc[now] = -network.converters_t.p_set.loc[now]
    network.transport_links_t.p0.loc[now] = network.transport_links_t.p_set.loc[now]
    network.transport_links_t.p1.loc[now] = -network.transport_links_t.p_set.loc[now]


    for sub_network in network.sub_networks.obj:

        if sub_network.current_type == "DC":
            raise NotImplementedError("Non-linear power flow for DC networks not supported yet.")
            continue

        if verbose:
            print("Performing full non-linear load-flow on %s sub-network %s" % (sub_network.current_type,sub_network))


        if not skip_pre:
            find_bus_controls(sub_network,verbose=verbose)

            branches = sub_network.branches()

            if len(branches) > 0:
                calculate_Y(sub_network,verbose=verbose,skip_pre=True)

        sub_network.pf(now,verbose=verbose,skip_pre=True)




def newton_raphson_sparse(f,guess,dfdx,x_tol=1e-10,lim_iter=100,verbose=True):
    """Solve f(x) = 0 with initial guess for x and dfdx(x). dfdx(x) should
    return a sparse Jacobian.  Terminate if error on norm of f(x) is <
    x_tol or there were more than lim_iter iterations.

    """

    n_iter = 0
    F = f(guess)
    diff = norm(F,np.Inf)

    if verbose:
        print("Error at iteration %d: %f" % (n_iter,diff))

    while diff > x_tol and n_iter < lim_iter:

        n_iter +=1

        guess = guess - spsolve(dfdx(guess),F)

        F = f(guess)
        diff = norm(F,np.Inf)

        if verbose:
            print("Error at iteration %d: %f" % (n_iter,diff))

    if verbose and diff > x_tol:
        print("Warning, looks like we didn't reach the required tolerance within %d iterations" % (n_iter,))

    return guess,n_iter,diff



def sub_network_pf(sub_network,now=None,verbose=True,skip_pre=False,x_tol=1e-6):
    """
    Non-linear power flow for connected sub-network.

    Parameters
    ----------
    now : object
        A member of network.snapshots on which to run the power flow, defaults to network.now
    verbose: bool, default True
    skip_pre: bool, default False
        Skip the preliminary steps of computing topology, calculating dependent values and finding bus controls.
    x_tol: float
        Tolerance for Newton-Raphson power flow.

    Returns
    -------
    None
    """

    from .components import passive_branch_types

    network = sub_network.network

    if now is None:
        now = network.now

    if verbose:
        print("Performing full non-linear load-flow for snapshot %s" % (now))

    if not skip_pre:
        calculate_dependent_values(network)
        find_bus_controls(sub_network,verbose=verbose)

    branches = sub_network.branches()
    buses = sub_network.buses_o

    if not skip_pre and len(branches) > 0:
        calculate_Y(sub_network,verbose=verbose,skip_pre=True)




    #set the power injection at each node
    network.buses_t.p.loc[now,buses.index] =  pd.DataFrame({list_name :
                          (getattr(network,list_name+"_t").p_set.loc[now]*getattr(network,list_name).sign)
                          .groupby(getattr(network,list_name).bus).sum()
    for list_name in ["generators","loads","storage_units"]})\
              .sum(axis=1).reindex(buses.index,fill_value = 0.)


    network.buses_t.q.loc[now,buses.index] =  pd.DataFrame({list_name:
                          (getattr(network,list_name+"_t").q_set.loc[now]*getattr(network,list_name).sign)
                          .groupby(getattr(network,list_name).bus).sum()
    for list_name in ["generators","loads","storage_units"]})\
              .sum(axis=1).reindex(buses.index,fill_value = 0.)

    network.buses_t.p.loc[now,buses.index] -=  pd.DataFrame({list_name+str(i) :
                          getattr(getattr(network,list_name+"_t"),"p"+str(i)).loc[now]
                          .groupby(getattr(getattr(network,list_name),"bus"+str(i))).sum()
                                    for list_name in ["transport_links","converters"] for i in [0,1]})\
              .sum(axis=1).reindex(buses.index,fill_value = 0.)


    p = network.buses_t.p.loc[now,buses.index]
    q = network.buses_t.q.loc[now,buses.index]

    s = p + 1j*q



    def f(guess):
        network.buses_t.v_ang.loc[now,sub_network.pvpqs.index] = guess[:len(sub_network.pvpqs)]

        network.buses_t.v_mag_pu.loc[now,sub_network.pqs.index] = guess[len(sub_network.pvpqs):]

        v_mag_pu = network.buses_t.v_mag_pu.loc[now,buses.index]
        v_ang = network.buses_t.v_ang.loc[now,buses.index]
        V = v_mag_pu*np.exp(1j*v_ang)

        mismatch = V*np.conj(sub_network.Y*V) - s

        F = r_[mismatch.real[1:],mismatch.imag[1+len(sub_network.pvs):]]

        return F


    def dfdx(guess):

        network.buses_t.v_ang.loc[now,sub_network.pvpqs.index] = guess[:len(sub_network.pvpqs)]

        network.buses_t.v_mag_pu.loc[now,sub_network.pqs.index] = guess[len(sub_network.pvpqs):]

        v_mag_pu = network.buses_t.v_mag_pu.loc[now,buses.index]
        v_ang = network.buses_t.v_ang.loc[now,buses.index]

        V = v_mag_pu*np.exp(1j*v_ang)

        index = r_[:len(buses)]

        #make sparse diagonal matrices
        V_diag = csr_matrix((V,(index,index)))
        V_norm_diag = csr_matrix((V/abs(V),(index,index)))
        I_diag = csr_matrix((sub_network.Y*V,(index,index)))

        dS_dVa = 1j*V_diag*np.conj(I_diag - sub_network.Y*V_diag)

        dS_dVm = V_norm_diag*np.conj(I_diag) + V_diag * np.conj(sub_network.Y*V_norm_diag)

        J00 = dS_dVa[1:,1:].real
        J01 = dS_dVm[1:,1+len(sub_network.pvs):].real
        J10 = dS_dVa[1+len(sub_network.pvs):,1:].imag
        J11 = dS_dVm[1+len(sub_network.pvs):,1+len(sub_network.pvs):].imag

        J = svstack([
            shstack([J00, J01]),
            shstack([J10, J11])
        ], format="csr")

        return J


    #Set what we know: slack V and v_mag_pu for PV buses
    network.buses_t.v_mag_pu.loc[now,sub_network.pvs.index] = network.buses_t.v_mag_pu_set.loc[now,sub_network.pvs.index]

    network.buses_t.v_mag_pu.loc[now,sub_network.slack_bus] = network.buses_t.v_mag_pu_set.loc[now,sub_network.slack_bus]

    network.buses_t.v_ang.loc[now,sub_network.slack_bus] = 0.

    #Make a guess for what we don't know: V_ang for PV and PQs and v_mag_pu for PQ buses
    guess = r_[zeros(len(sub_network.pvpqs)),ones(len(sub_network.pqs))]

    #Now try and solve
    start = time.time()
    roots,n_iter,diff = newton_raphson_sparse(f,guess,dfdx,x_tol=x_tol,verbose=verbose)
    if verbose:
        print("Newton-Raphson solved in %d iterations with error of %f in %f seconds" % (n_iter,diff,time.time()-start))


    #now set everything

    network.buses_t.v_ang.loc[now,sub_network.pvpqs.index] = roots[:len(sub_network.pvpqs)]
    network.buses_t.v_mag_pu.loc[now,sub_network.pqs.index] = roots[len(sub_network.pvpqs):]

    v_mag_pu = network.buses_t.v_mag_pu.loc[now,buses.index]
    v_ang = network.buses_t.v_ang.loc[now,buses.index]

    V = v_mag_pu*np.exp(1j*v_ang)

    #add voltages to branches
    branches = pd.merge(branches,pd.DataFrame({"v0" :V}),how="left",left_on="bus0",right_index=True)
    branches = pd.merge(branches,pd.DataFrame({"v1" :V}),how="left",left_on="bus1",right_index=True)

    i0 = sub_network.Y0*V
    i1 = sub_network.Y1*V

    branches["s0"] = branches["v0"]*np.conj(i0)
    branches["s1"] = branches["v1"]*np.conj(i1)

    for t in network.iterate_components(passive_branch_types):
        df = branches.loc[t.name]
        t.pnl.loc["p0",now,df.index] = df["s0"].real
        t.pnl.loc["q0",now,df.index] = df["s0"].imag
        t.pnl.loc["p1",now,df.index] = df["s1"].real
        t.pnl.loc["q1",now,df.index] = df["s1"].imag


    s_calc = V*np.conj(sub_network.Y*V)

    network.buses_t.p.loc[now,sub_network.slack_bus] = s_calc[sub_network.slack_bus].real
    network.buses_t.q.loc[now,sub_network.slack_bus] = s_calc[sub_network.slack_bus].imag
    network.buses_t.q.loc[now,sub_network.pvs.index] = s_calc[sub_network.pvs.index].imag

    #allow all loads to dispatch as set
    loads = sub_network.loads()
    network.loads_t.p.loc[now,loads.index] = network.loads_t.p_set.loc[now,loads.index]
    network.loads_t.q.loc[now,loads.index] = network.loads_t.q_set.loc[now,loads.index]

    #set shunt impedance powers
    shunt_impedances = sub_network.shunt_impedances()
    #add voltages
    shunt_impedances = pd.merge(shunt_impedances,pd.DataFrame({"v_mag_pu" :v_mag_pu}),how="left",left_on="bus",right_index=True)
    network.shunt_impedances_t.p.loc[now,shunt_impedances.index] = (shunt_impedances.v_mag_pu**2)*shunt_impedances.g_pu
    network.shunt_impedances_t.q.loc[now,shunt_impedances.index] = (shunt_impedances.v_mag_pu**2)*shunt_impedances.b_pu

    #allow all generators to dispatch as set
    generators = sub_network.generators()
    network.generators_t.p.loc[now,generators.index] = network.generators_t.p_set.loc[now,generators.index]
    network.generators_t.q.loc[now,generators.index] = network.generators_t.q_set.loc[now,generators.index]

    #let slack generator take up the slack
    network.generators_t.p.loc[now,sub_network.slack_generator] += network.buses_t.p.loc[now,sub_network.slack_bus] - s[sub_network.slack_bus].real
    network.generators_t.q.loc[now,sub_network.slack_generator] += network.buses_t.q.loc[now,sub_network.slack_bus] - s[sub_network.slack_bus].imag

    #set the Q of the PV generators
    network.generators_t.q.loc[now,sub_network.pvs.generator] += network.buses_t.q.loc[now,sub_network.pvs.index] - s[sub_network.pvs.index].imag


def network_lpf(network, snapshots=None, verbose=True, skip_pre=False, now=None):
    """
    Linear power flow for generic network.

    Parameters
    ----------
    snapshots : list-like|single snapshot
        A subset or an elements of network.snapshots on which to run
        the power flow, defaults to [now]
    verbose: bool, default True
    skip_pre: bool, default False
        Skip the preliminary steps of computing topology, calculating
        dependent values and finding bus controls.
    now : object
        Deprecated: A member of network.snapshots on which to run the
        power flow, defaults to network.now

    Returns
    -------
    None
    """

    if not skip_pre:
        network.determine_network_topology()
        calculate_dependent_values(network)

    snapshots = _as_snapshots(network, snapshots, now=now)

    #deal with transport links and converters
    network.converters_t.p0.loc[snapshots] = network.converters_t.p_set.loc[snapshots]
    network.converters_t.p1.loc[snapshots] = -network.converters_t.p_set.loc[snapshots]
    network.transport_links_t.p0.loc[snapshots] = network.transport_links_t.p_set.loc[snapshots]
    network.transport_links_t.p1.loc[snapshots] = -network.transport_links_t.p_set.loc[snapshots]

    for sub_network in network.sub_networks.obj:
        if not skip_pre:
            find_bus_controls(sub_network, verbose=verbose)

            branches = sub_network.branches()
            if len(branches) > 0:
                calculate_B_H(sub_network, verbose=verbose, skip_pre=True)
        sub_network.lpf(snapshots=snapshots, verbose=verbose, skip_pre=True)


def find_slack_bus(sub_network,verbose=True):
    """Find the slack bus in a connected sub-network."""

    gens = sub_network.generators()

    if len(gens) == 0:
        if verbose:
            print("No generators in %s, better hope power is already balanced" % sub_network)
        sub_network.slack_generator = None
        sub_network.slack_bus = sub_network.buses().index[0]

    else:

        slacks = gens[gens.control == "Slack"]

        if len(slacks) == 0:
            sub_network.slack_generator = gens.index[0]
            sub_network.network.generators.loc[sub_network.slack_generator,"control"] = "Slack"
            if verbose:
                print("No slack generator found, using %s as the slack generator" % sub_network.slack_generator)

        elif len(slacks) == 1:
            sub_network.slack_generator = slacks.index[0]
        else:
            sub_network.slack_generator = slacks.index[0]
            sub_network.network.generators.loc[slacks.index[1:],"control"] = "PV"
            if verbose:
                print("More than one slack generator found, taking %s to be the slack generator" % sub_network.slack_generator)

        sub_network.slack_bus = gens.bus[sub_network.slack_generator]

    if verbose:
        print("Slack bus is %s" % sub_network.slack_bus)


def find_bus_controls(sub_network,verbose=True):
    """Find slack and all PV and PQ buses for a sub_network.
    This function also fixes sub_network.buses_o, a DataFrame
    ordered by control type."""

    network = sub_network.network

    find_slack_bus(sub_network,verbose)

    gens = sub_network.generators()
    buses = sub_network.buses()

    #default bus control is PQ
    network.buses.loc[buses.index,"control"] = "PQ"

    #find all buses with one or more gens with PV
    gen_pvs = pd.DataFrame(data=0.,index=gens.index,columns=["pvs"])
    gen_pvs.loc[gens.control=="PV","pvs"] = 1.
    gen_pvs["name"] = gens.index

    bus_pvs = gen_pvs["pvs"].groupby(gens.bus).sum().reindex(buses.index)
    bus_pv_names = gen_pvs["name"].groupby(gens.bus).first().reindex(buses.index)

    pvs = bus_pv_names[bus_pvs>0]

    network.buses.loc[pvs.index,"control"] = "PV"
    network.buses.loc[pvs.index,"generator"] = pvs

    network.buses.loc[sub_network.slack_bus,"control"] = "Slack"
    network.buses.loc[sub_network.slack_bus,"generator"] = sub_network.slack_generator

    buses = sub_network.buses()

    sub_network.pvs = buses[buses.control == "PV"]
    sub_network.pqs = buses[buses.control == "PQ"]

    sub_network.pvpqs = pd.concat((sub_network.pvs,sub_network.pqs))

    #order buses
    sub_network.buses_o = pd.concat((buses.loc[[sub_network.slack_bus]],sub_network.pvpqs))
    sub_network.buses_o["i"] = list(range(len(sub_network.buses_o)))



def calculate_dependent_values(network):
    """Calculate per unit impedances and append voltages to lines and shunt impedances."""

    network.lines["v_nom"] = network.lines.bus0.map(network.buses.v_nom)

    network.lines["x_pu"] = network.lines.x/(network.lines.v_nom**2)
    network.lines["r_pu"] = network.lines.r/(network.lines.v_nom**2)
    network.lines["b_pu"] = network.lines.b*network.lines.v_nom**2
    network.lines["g_pu"] = network.lines.g*network.lines.v_nom**2

    #convert transformer impedances from base power s_nom to base = 1 MVA
    network.transformers["x_pu"] = network.transformers.x/network.transformers.s_nom
    network.transformers["r_pu"] = network.transformers.r/network.transformers.s_nom
    network.transformers["b_pu"] = network.transformers.b*network.transformers.s_nom
    network.transformers["g_pu"] = network.transformers.g*network.transformers.s_nom

    network.shunt_impedances["v_nom"] = network.shunt_impedances["bus"].map(network.buses.v_nom)
    network.shunt_impedances["b_pu"] = network.shunt_impedances.b*network.shunt_impedances.v_nom**2
    network.shunt_impedances["g_pu"] = network.shunt_impedances.g*network.shunt_impedances.v_nom**2


def calculate_B_H(sub_network,verbose=True,skip_pre=False):
    """Calculate B and H matrices for AC or DC sub-networks."""

    if not skip_pre:
        calculate_dependent_values(sub_network.network)
        find_bus_controls(sub_network,verbose)

    if sub_network.current_type == "DC":
        attribute="r_pu"
    elif sub_network.current_type == "AC":
        attribute="x_pu"

    branches = sub_network.branches()
    buses = sub_network.buses_o

    #following leans heavily on pypower.makeBdc

    num_branches = len(branches)
    num_buses = len(buses)

    index = r_[:num_branches,:num_branches]

    if verbose and (branches[attribute] == 0).any():
        print("Warning! Some series impedances are zero - this will cause a singularity in LPF!")

    #susceptances
    b = 1/branches[attribute]

    b_diag = csr_matrix((b.values,(r_[:num_branches],r_[:num_branches])))

    from_bus = branches.bus0.map(buses["i"]).values
    to_bus = branches.bus1.map(buses["i"]).values

    #incidence matrix
    sub_network.K = csr_matrix((r_[ones(num_branches),-ones(num_branches)],(r_[from_bus,to_bus],index)),(num_buses,num_branches))

    sub_network.H = b_diag*sub_network.K.T

    #weighted Laplacian
    sub_network.B = sub_network.K * sub_network.H



def calculate_PTDF(sub_network,verbose=True,skip_pre=False):
    """
    Calculate the Power Transfer Distribution Factor (PTDF) for
    sub_network.

    Sets sub_network.PTDF as a (dense) numpy array.

    Parameters
    ----------
    sub_network : pypsa.SubNetwork
    verbose: bool, default True
    skip_pre: bool, default False
        Skip the preliminary steps of computing topology, calculating dependent values,
        finding bus controls and computing B and H.

    """

    if not skip_pre:
        calculate_B_H(sub_network,verbose)

    #calculate inverse of B with slack removed

    n_pvpq = sub_network.pvpqs.shape[0]
    index = np.r_[:n_pvpq]

    I = csc_matrix((np.ones((n_pvpq)),(index,index)))

    B_inverse = spsolve(sub_network.B[1:, 1:],I)

    #exception for two-node networks, where B_inverse is a 1d array
    if issparse(B_inverse):
        B_inverse = B_inverse.toarray()
    elif B_inverse.shape == (1,):
        B_inverse = B_inverse.reshape((1,1))

    #add back in zeroes for slack
    B_inverse = np.hstack((np.zeros((n_pvpq,1)),B_inverse))
    B_inverse = np.vstack((np.zeros(n_pvpq+1),B_inverse))

    sub_network.PTDF = sub_network.H*B_inverse


def calculate_Y(sub_network,verbose=True,skip_pre=False):
    """Calculate bus admittance matrices for AC sub-networks."""

    if not skip_pre:
        calculate_dependent_values(sub_network.network)

    if sub_network.current_type == "DC":
        print("DC networks not supported for Y!")
        return

    branches = sub_network.branches()
    buses = sub_network.buses_o

    network = sub_network.network

    #following leans heavily on pypower.makeYbus
    #Copyright Richard Lincoln, Ray Zimmerman, BSD-style licence

    num_branches = len(branches)
    num_buses = len(buses)

    y_se = 1/(branches["r_pu"] + 1.j*branches["x_pu"])

    y_sh = branches["g_pu"]+ 1.j*branches["b_pu"]

    tau = branches["tap_ratio"].fillna(1.)

    #catch some transformers falsely set with tau = 0 by pypower
    tau[tau==0] = 1.

    phase_shift = np.exp(1.j*branches["phase_shift"].fillna(0.)*np.pi/180.)

    #build the admittance matrix elements for each branch
    Y11 = y_se + 0.5*y_sh
    Y01 = -y_se/tau/phase_shift
    Y10 = -y_se/tau/np.conj(phase_shift)
    Y00 = Y11/tau**2

    #bus shunt impedances
    b_sh = network.shunt_impedances.b_pu.groupby(network.shunt_impedances.bus).sum().reindex(buses.index,fill_value = 0.)
    g_sh = network.shunt_impedances.g_pu.groupby(network.shunt_impedances.bus).sum().reindex(buses.index,fill_value = 0.)
    Y_sh = g_sh + 1.j*b_sh

    #get bus indices
    join = pd.merge(branches,buses,how="left",left_on="bus0",right_index=True,suffixes=("","_0"))
    join = pd.merge(join,buses,how="left",left_on="bus1",right_index=True,suffixes=("","_1"))
    bus0 = join.i
    bus1 = join.i_1

    #connection matrices
    C0 = csr_matrix((ones(num_branches), (np.arange(num_branches), bus0)), (num_branches, num_buses))
    C1 = csr_matrix((ones(num_branches), (np.arange(num_branches), bus1)), (num_branches, num_buses))

    #build Y{0,1} such that Y{0,1} * V is the vector complex branch currents

    i = r_[np.arange(num_branches), np.arange(num_branches)]
    sub_network.Y0 = csr_matrix((r_[Y00,Y01],(i,r_[bus0,bus1])), (num_branches,num_buses))
    sub_network.Y1 = csr_matrix((r_[Y10,Y11],(i,r_[bus0,bus1])), (num_branches,num_buses))

    #now build bus admittance matrix
    sub_network.Y = C0.T * sub_network.Y0 + C1.T * sub_network.Y1 + \
       csr_matrix((Y_sh, (np.arange(num_buses), np.arange(num_buses))))



def aggregate_multi_graph(sub_network,verbose=True):
    """Aggregate branches between same buses and replace with a single
branch with aggregated properties (e.g. s_nom is summed, length is
averaged).

    """

    network = sub_network.network

    count = 0
    seen = []
    for u,v in sub_network.graph.edges():
        if (u,v) in seen:
            continue
        line_objs = list(sub_network.graph.adj[u][v].keys())
        if len(line_objs) > 1:
            lines = network.lines.loc[[l.name for l in line_objs]]
            aggregated = {}

            attr_inv = ["x","r"]
            attr_sum = ["s_nom","b","g","s_nom_max","s_nom_min"]
            attr_mean = ["capital_cost","length","terrain_factor"]

            for attr in attr_inv:
                aggregated[attr] = 1/(1/lines[attr]).sum()

            for attr in attr_sum:
                aggregated[attr] = lines[attr].sum()

            for attr in attr_mean:
                aggregated[attr] = lines[attr].mean()

            count += len(line_objs) - 1

            #remove all but first line
            for line in line_objs[1:]:
                network.remove("Line",line.name)

            rep = line_objs[0]

            for key,value in aggregated.items():
                setattr(rep,key,value)

            seen.append((u,v))

    if verbose:
        print("Removed %d excess lines from sub-network %s and replaced with aggregated lines" % (count,sub_network.name))




def find_tree(sub_network,verbose=True):
    """Get the spanning tree of the graph, choose the node with the
    highest degree as a central "tree slack" and then see for each
    branch which paths from the slack to each node go through the
    branch.

    """

    branches = sub_network.branches()
    buses = sub_network.buses()

    sub_network.tree = nx.minimum_spanning_tree(sub_network.graph)

    #find bus with highest degree to use as slack

    tree_slack_bus = None
    slack_degree = -1

    for bus,degree in sub_network.tree.degree_iter():
        if degree > slack_degree:
            tree_slack_bus = bus
            slack_degree = degree

    if verbose:
        print("Tree slack bus is %s with degree %d." % (tree_slack_bus,slack_degree))


    #determine which buses are supplied in tree through branch from slack

    #matrix to store tree structure
    sub_network.T = dok_matrix((len(branches),len(buses)))


    for j,bus in enumerate(buses.index):
        path = nx.shortest_path(sub_network.tree,bus,tree_slack_bus)
        for i in range(len(path)-1):
            branch = list(sub_network.graph[path[i]][path[i+1]].keys())[0]
            if branch.bus0 == path[i]:
                sign = +1
            else:
                sign = -1

            branch_i = branches.index.get_loc((branch.__class__.__name__,branch.name))

            sub_network.T[branch_i,j] = sign


def find_cycles(sub_network,verbose=True):
    """
    Find all cycles in the sub_network and record them in sub_network.C.

    networkx collects the cycles with more than 2 edges; then the 2-edge cycles
    from the MultiGraph must be collected separately (for cases where there
    are multiple lines between the same pairs of buses).

    """

    branches = sub_network.branches()

    #reduce to a non-multi-graph for cycles with > 2 edges
    graph = nx.OrderedGraph(sub_network.graph)

    cycles = nx.cycle_basis(graph)

    #number of 2-edge cycles
    num_multi = len(sub_network.graph.edges()) - len(graph.edges())

    sub_network.C = dok_matrix((len(branches),len(cycles)+num_multi))


    for j,cycle in enumerate(cycles):

        for i in range(len(cycle)):
            branch = list(sub_network.graph[cycle[i]][cycle[(i+1)%len(cycle)]].keys())[0]
            if branch.bus0 == cycle[i]:
                sign = +1
            else:
                sign = -1

            branch_i = branches.index.get_loc((branch.__class__.__name__,branch.name))
            sub_network.C[branch_i,j] = sign

    #counter for multis
    c = len(cycles)

    #add multi-graph 2-edge cycles for multiple branches between same pairs of buses
    for u,v in graph.edges():
        bs = list(sub_network.graph[u][v].keys())
        if len(bs) > 1:
            first = bs[0]
            first_i = branches.index.get_loc((first.__class__.__name__,first.name))
            for b in bs[1:]:
                b_i = branches.index.get_loc((b.__class__.__name__,b.name))
                if b.bus0 == first.bus0:
                    sign = -1
                else:
                    sign = 1

                sub_network.C[first_i,c] = 1
                sub_network.C[b_i,c] = sign
                c+=1

def sub_network_lpf(sub_network, snapshots=None, verbose=True, skip_pre=False, now=None):
    """
    Linear power flow for connected sub-network.

    Parameters
    ----------
    snapshots : list-like|single snapshot
        A subset or an elements of network.snapshots on which to run
        the power flow, defaults to [now]
    verbose: bool, default True
    skip_pre: bool, default False
        Skip the preliminary steps of computing topology, calculating
        dependent values and finding bus controls.
    now : object
        Deprecated: A member of network.snapshots on which to run the
        power flow, defaults to network.now

    Returns
    -------
    None
    """

    from .components import \
        one_port_types, controllable_one_port_types, \
        passive_branch_types, controllable_branch_types

    network = sub_network.network

    snapshots = _as_snapshots(network, snapshots, now=now)

    if verbose:
        print("Performing linear load-flow on {} sub-network {} for snapshot(s) {}"
              .format(sub_network.current_type,sub_network,snapshots))

    if not skip_pre:
        calculate_dependent_values(network)
        find_bus_controls(sub_network,verbose=verbose)

    # get indices for the components on this subnetwork
    buses_i = sub_network.buses_o.index
    branches_i = sub_network.branches().index

    # allow all shunt impedances to dispatch as set
    shunt_impedances_i = sub_network.shunt_impedances_i()
    network.shunt_impedances_t.p.loc[snapshots, shunt_impedances_i] = \
        network.shunt_impedances.g_pu.loc[shunt_impedances_i].values

    # allow all one ports to dispatch as set
    for t in sub_network.iterate_components(controllable_one_port_types):
        t.pnl.p.loc[snapshots, t.ind] = t.pnl.p_set.loc[snapshots, t.ind]

    # set the power injection at each node
    network.buses_t.p.loc[snapshots, buses_i] = \
        sum([((t.pnl.p.loc[snapshots, t.ind] * t.df.loc[t.ind, 'sign'])
              .groupby(t.df.loc[t.ind, 'bus'], axis=1).sum()
              .reindex(columns=buses_i, fill_value=0.))
             for t in sub_network.iterate_components(one_port_types)]
            +
            [(- t.pnl.loc["p"+str(i), snapshots].groupby(t.df["bus"+str(i)], axis=1).sum()
              .reindex(columns=buses_i, fill_value=0))
             for t in network.iterate_components(controllable_branch_types)
             for i in [0,1]])

    if not skip_pre and len(branches_i) > 0:
        calculate_B_H(sub_network, verbose=verbose, skip_pre=True)

    v_diff = np.zeros((len(snapshots), len(buses_i)))
    if len(branches_i) > 0:
        p = network.buses_t.loc['p', snapshots, buses_i].values
        v_diff[:,1:] = spsolve(sub_network.B[1:, 1:], p[:,1:].T).T
        flows = pd.DataFrame(v_diff * sub_network.H.T,
                             columns=branches_i, index=snapshots)

        for t in network.iterate_components(passive_branch_types):
            f = flows.loc[:, t.name]
            t.pnl.p0.loc[snapshots, f.columns] = f
            t.pnl.p1.loc[snapshots, f.columns] = -f

    if sub_network.current_type == "AC":
        network.buses_t.v_ang.loc[snapshots, buses_i] = v_diff
        network.buses_t.v_mag_pu.loc[snapshots, buses_i] = 1.
    elif sub_network.current_type == "DC":
        network.buses_t.v_mag_pu.loc[snapshots, buses_i] = 1 + v_diff
        network.buses_t.v_ang.loc[snapshots, buses_i] = 0.

    # set slack bus power to pick up remained
    slack_adjustment = (- network.buses_t.loc['p', snapshots, buses_i[1:]].sum(axis=1)
                        - network.buses_t.loc['p', snapshots, buses_i[0]])
    network.buses_t.loc["p", snapshots, buses_i[0]] += slack_adjustment

    # let slack generator take up the slack
    if sub_network.slack_generator is not None:
        network.generators_t.p.loc[snapshots, sub_network.slack_generator] += slack_adjustment



def network_batch_lpf(network,snapshots=None):
    """Batched linear power flow with numpy.dot for several snapshots."""

    raise NotImplementedError("Batch linear power flow not supported yet.")
back to top