3. Power Networks

This section describes how to use and analyze power networks in PFNET.

3.1. Overview

Power networks in PFNET are represented by objects of type Network. These objects are created from power network data files using a Parser, as described in the previous section. Once the network is created, it can be analyzed, modified, and used to construct network optimization problems.

An important attribute of the Network class is base_power. This quantity, which has units of MVA, is useful for converting power quantities in per unit system base power to MW or MVAr.

3.2. Components

Power networks have several components. These are buses, branches, generators, shunt devices, loads, variable generators, e.g., renewable energy sources, batteries, FACTS devices, HVDC voltage-source converters, HVDC current-source converters, HVDC buses, and HVDC branches. For obtaining an overview of the components that form a network, the class method show_components() can be used, as illustrated in the following example:

>>> import pfnet

>>> net = pfnet.PyParserMAT().parse('ieee14.m')
>>> net.show_components()

Network Components
------------------
buses            : 14
shunts           : 1
branches         : 20
generators       : 5
loads            : 11
vargens          : 0
batteries        : 0
facts            : 0
csc converters   : 0
vsc converters   : 0
dc buses         : 0
dc branches      : 0

Again, this and subsequent examples assume that the Python interpreter is started from a directory that contains the sample case ieee14.

3.2.1. Buses

Buses in a power network are objects of type Bus. Each bus has an index, a number, and a name attribute that can be used to identify this bus in a network. The index is associated with the location of the bus in the underlying C array of bus structures, while the number and name attributes come from input data. The index, number, or name can be used to extract a specific bus from a network using the Network class methods get_bus(), get_bus_from_number(), and get_bus_from_name(), respectively:

>>> bus = net.get_bus(10)

>>> print bus.index == 10
True

>>> other_bus = net.get_bus_from_number(bus.number)

>>> print bus == other_bus
True

For convenience, a list of all the buses in the network is contained in the buses attribute of the Network class.

Buses in a network can have different properties. For example, some buses can be slack buses and others can have their voltage magnitudes regulated by generators, tap-changing transformers, or switched shunt devices. The Bus class provides methods for checking whether a bus has specific properties. The following example shows how to get a list of all the buses whose voltage magnitudes are regulated by generators:

>>> reg_buses = [bus for bus in net.buses if bus.is_regulated_by_gen()]

>>> print len(reg_buses), net.get_num_buses_reg_by_gen()
5 5

A bus also has information about the devices that are connected to it or that are regulating its voltage magnitude. For example, the attributes generators and reg_trans contain a list of generators connected to the bus and a list of tap-changing transformers regulating its voltage magnitude, respectively.

More information about network buses can be found in the API reference.

3.2.2. Branches

Branches in a power network are objects of type Branch and are represented mathematically by the model described in Section 2.1.2 of [TT2015]. Each branch has an index and a name attribute that can be used to identify this branch in a network. The Network class methods get_branch() and get_branch_from_name_and_bus_numbers() can be used to get a specific branch of the network:

>>> branch = net.get_branch(5)

>>> print branch.index == 5
True

For convenience, a list of all the branches in the network is contained in the branches attribute of the Network class.

In PFNET, branches can represent transmission lines, fixed transformers, tap-changing transformers, or phase-shifting transformers. Tap-changing transformers can control the reactive power flowing through the branch or the voltage magnitude of a bus. The Branch class provides methods for checking whether a branch has specific properties. The following example shows how to get a list of all the branches that are transmission lines:

>>> lines = [br for br in net.branches if br.is_line()]

>>> print len(lines), net.get_num_lines()
17 17

For branches that are transformers, the Branch class attributes ratio and phase correspond to the transformer’s tap ratio and phase shift, respectively. These attributes correspond to the quantities \(a_{km}\) and \(\phi_{km}\) of the branch model described in Section 2.1.2 of [TT2015]. The quantity \(a_{mk}\) in this model is always one.

More information about network branches can be found in the API reference.

3.2.3. Generators

Generators in a power network are objects of type Generator. Each generator has an index and a name attribute that can be used to identify this generator in a network. The Network class methods get_generator() and get_generator_from_name_and_bus_number() can be used to get a specific generator of the network:

>>> gen = net.get_generator(2)

>>> print gen.index == 2
True

For convenience, a list of all the generators in the network is contained in the generators attribute of the Network class.

Generators in a power network can also have different properties. For example, some generators can be slack generators and others can provide bus voltage magnitude regulation. The Generator class provides methods for checking whether a generator has specific properties. The following example shows how to get a list of all the slack generators:

>>> slack_gens = [g for g in net.generators if g.is_slack()]

>>> print len(slack_gens), net.get_num_slack_gens()
1 1

The active and reactive powers that a generator injects into the bus to which it is connected are obtained from the P and Q attributes of the Generator class. These quantities are given in units of per unit system base power. The following example computes the total active power injected into the network by generators in units of MW:

>>> print sum([g.P for g in net.generators])*net.base_power
272.4

More information about network generators can be found in the API reference.

3.2.4. Shunt Devices

Shunt devices in a power network are objects of type Shunt. As other components, each shunt has an index and a name attribute as well as methods get_shunt() and get_shunt_from_name_and_bus_number():

>>> shunt = net.get_shunt(0)

>>> print shunt.index == 0
True

The attribute shunts of the Network contains a list of all the shunt devices in the network.

Some shunt devices can be fixed while others can be switchable and configured to regulate a bus voltage magnitude. The conductance and susceptance of a shunt device can be accessed through the attributes g and b of the Shunt class.

More information about network shunts can be found in the API reference.

3.2.5. Loads

Loads in a power network are objects of type Load. As with other components, the index and name attributes can be used to identify a load in the network. A list of all the loads in the network is contained in the loads attribute of the Network class.

As with generators, the active and reactive powers that a load consumes from the bus to which it is connected are obtained from the P and Q attributes of the Load class. They are also given in units of per unit system base power.

More information about network loads can be found in the API reference.

3.2.6. Variable Generators

Variable generators in a power network are objects of type VarGenerator. They represent non-dispatchable energy sources such as wind generators or farms and photovoltaic power plants. As with other components, the index and name attributes can be used to identify a variable generator in the network. Also, a list of all the variable generators in the network is contained in the var_generators attribute of the Network class.

As with generators, the active and reactive output powers of a variable generator are obtained from the P and Q attributes of the VarGenerator class in units of per unit system base power. Output limits are given by the attributes P_min, P_max, Q_min, and Q_max.

The output of variable generators in a network is subject to random variations that can be correlated, especially for devices that are “nearby”. The method create_var_generators_P_sigma() of the Network class allows constructing a covariance matrix for these variations based on a “correlation distance” N and a given correlation coefficient. The cross-covariance between the variation of any two devices that are connected to buses that are at most N branches away from each other is set in such a way that is consistent with the given correlation coefficient. For other devices, the cross-covariance is set to zero.

Lastly, since many power network input files do not have variable generator information, these devices can be conveniently added to a network using the add_var_generators_from_parameters() method of the Network class.

More information about network variable generators can be found in the API reference.

3.2.7. Batteries

Batteries are objects of type Battery and have an index and name attribute like all the other network components. Other important attributes of these objects are energy level E and charging power P. Since power network input files do not typically have battery information, these devices can be conveniently added to a network using the add_batteries_from_parameters() method of the Network class.

More information about network batteries can be found in the API reference.

3.2.8. FACTS Devices

Information about FACTS devices can be found in the API reference.

3.2.9. HVDC Voltage-Source Converters

Information about voltage-source converters can be found in the API reference.

3.2.10. HVDC Current-Source Converters

Information about current-source converters can be found in the API reference.

3.2.11. HVDC Buses

Information about HVDC buses can be found in the API reference.

3.2.12. HVDC Branches

Information about HVDC branches can be found in the API reference.

3.3. Properties

A Network object has several quantities or properties that provide information about the state of the network. The following table provides a description of each of these properties.

Names Description Units
bus_v_max Maximum bus voltage magnitude per unit
bus_v_min Minimum bus voltage magnitude per unit
bus_v_vio Maximum bus voltage magnitude limit violation per unit
bus_P_mis Maximum absolute bus active power mismatch MW
bus_Q_mis Maximum absolute bus reactive power mismatch MVAr
gen_P_cost Total active power generation cost $/hour
gen_v_dev Maximum set point deviation of generator-regulated voltage per unit
gen_Q_vio Maximum generator reactive power limit violation MVAr
gen_P_vio Maximum generator active power limit violation MW
tran_v_vio Maximum band violation of transformer-regulated voltage per unit
tran_r_vio Maximum tap ratio limit violation of tap-changing transformer unitless
tran_p_vio Maximum phase shift limit violation of phase-shifting transformer radians
shunt_v_vio Maximum band violation of shunt-regulated voltage per unit
shunt_b_vio Maximum susceptance limit violation of switched shunt device per unit
load_P_util Total active power consumption utility $/hour
load_P_vio Maximum load active power limit violation MW

All of these properties are attributes of the Network class. If there is a change in the network, e.g., the voltage magnitude v_mag of a bus is changed, the class method update_properties() needs to be called in order for the network properties to reflect the change. The following example shows how to update and extract properties:

>>> print net.bus_v_max
1.09

>>> for bus in net.buses:
...     bus.v_mag = bus.v_mag + 0.1
...

>>> print net.bus_v_max
1.09

>>> net.update_properties()

>>> print net.bus_v_max
1.19

For convenience, all the network properties can be extracted at once in a dictionary using the get_properties() class method:

>>> properties = net.get_properties()

>>> print properties['bus_v_max']
1.19

3.4. Variables

Network quantities can be specified to be variables. This is useful to represent network quantities with vectors and move to the linear algebra domain to do some computations.

To set network quantities as variables, the Network class method set_flags() is used. This method takes as arguments a component name, one or more flag names, one or more component properties, and one or more component quantities.

Component properties are component-specific. They can be combined into a list to make properties more complex and target a specific subset of components of a given type. More information can be found in the following sections:

Component quantities are also component-specific. They can be combined into a list to specify all quantities that should be affected by the method set_flags(). More information can be found in the following sections:

The following example shows how to set as variables all the voltage magnitudes and angles of buses regulated by generators:

>>> import pfnet

>>> net = pfnet.PyParserMAT().parse('ieee14.mat')

>>> print net.num_vars
0

>>> net.set_flags('bus',
...               'variable',
...               'regulated by generator',
...               ['voltage magnitude', 'voltage angle'])

>>> print net.num_vars, 2*net.get_num_buses_reg_by_gen()
10 10

Network components have a has_flags() method that allows checking whether flags of a certain type associated with specific quantities are set.

Once variables have been set, the vector containing all the current variable values can be extracted using get_var_values():

>>> values = net.get_var_values()

>>> print type(values)
<type 'numpy.ndarray'>

>>> print values.shape
(10,)

The network components that have quantities set as variables have indices that can be used to locate these quantities in the vector of all variable values:

>>> bus = [bus for bus in net.buses if bus.is_reg_by_gen()][0]

>>> print bus.has_flags('variable','voltage magnitude')
True

>>> bus.has_flags('variable','voltage angle')
True

>>> print bus.v_mag, net.get_var_values()[bus.index_v_mag]
1.09 1.09

>>> print bus.v_ang, net.get_var_values()[bus.index_v_ang]
-0.23 -0.23

A vector of variable values can be used to update the corresponding network quantities. This is done with the Network class method set_var_values():

>>> bus.has_flags('variable','voltage angle')
True

>>> values = net.get_var_values()

>>> print bus.v_mag
1.09

>>> values[bus.index_v_mag] = 1.20
>>> net.set_var_values(values)

>>> print bus.v_mag
1.20

As will be seen later, variables are also useful for constructing network optimization problems.

In addition to the class method set_flags(), which allows specifying variables of components having certain common properties, one can also use the Network class method set_flags_of_component() to specify variables of individual components. This is useful when the desired components cannot be targeted using the available component properties. For example, the following code illustrates how to set as variables the voltage magnitudes of buses whose indices are multiples of three:

>>> net.clear_flags()

>>> for bus in net.buses:
...     if bus.index % 3 == 0:
...         net.set_flags_of_component(bus,'variable','voltage magnitude')

>>> print net.num_vars, len([bus for bus in net.buses if bus.index % 3 == 0]), net.num_buses
5 5 14

Lastly, a very useful method of the Network class is the method get_var_info_string(). This method takes as argument an index of the vector of all variable values, and returns a string with information about the network quantity associated with it.

3.5. Projections

As explained above, once the network variables have been set, a vector with the current values of the selected variables is obtained with the class method get_var_values(). To extract subvectors that contain values of specific variables, projection matrices can be used. These matrices can be obtained using the class method get_var_projection(), which takes as arguments a component name, one or more component properties, e.g., Bus Properties, and one or more component quantities, e.g., Bus Quantities. The next example sets the variables of the network to be the bus voltage magnitudes and angles of all the buses, extracts the vector of values of all variables, and then extracts two subvectors having only voltage magnitudes and only voltage angles, respectively:

>>> import pfnet
>>> import numpy as np

>>> net = pfnet.PyParserMAT().parse('ieee14.m')

>>> net.set_flags('bus',
...               'variable',
...               'any',
...               ['voltage magnitude','voltage angle'])

>>> print net.num_vars, 2*net.num_buses
28 28

>>> P1 = net.get_var_projection('bus', 'any', 'voltage magnitude')
>>> P2 = net.get_var_projection('bus', 'any', 'voltage angle')

>>> print type(P1)
<class 'scipy.sparse.coo.coo_matrix'>

>>> x = net.get_var_values()
>>> v_mags = P1*x
>>> v_angs = P2*x

>>> print v_mags
[ 1.036  1.05   1.055  1.057  1.051  1.056  1.09   1.062  1.07   1.02
  1.019  1.01   1.045  1.06 ]

>>> print v_angs
[-0.27995081 -0.26459191 -0.26302112 -0.2581342  -0.26354472 -0.26075219
 -0.23317599 -0.23335052 -0.24818582 -0.15323991 -0.18029251 -0.22200588
 -0.0869174   0. ]

>>> print np.linalg.norm(x - (P1.T*v_mags+P2.T*v_angs))
0.0

3.6. Outages and Contingencies

PFNET provides a way to set components out of service and analyze network contingencies. This can be done by setting the in_service attribute to False, as the next example shows:

>>> net = pfnet.PyParserMAT().parse('ieee14.m')

>>> gen = net.get_generator(3)
>>> branch = net.get_branch(2)

>>> gen.in_service = False
>>> branch.in_service = False

>>> print net.get_num_generators_out_of_service(), net.get_num_branches_out_of_service()
1 1

A contingency is represented by an object of type Contingency, and is characterized by one or more generator or branch outages. The lists of generator and branch outages of a contingency can be specified at construction, or by using the class methods add_generator_outage() and add_branch_outage(), respectively. The following example shows how to construct a contingency:

>>> net.make_all_in_service()

>>> gen = net.get_generator(3)
>>> branch = net.get_branch(2)

>>> c1 = pf.Contingency(generators=[gen],branches=[branch])

>>> print c1.num_generator_outages, c1.num_branch_outages
1 1

>>> print c1.outages
[('branch', 2), ('generator', 3)]

Once a contingency has been constructed, it can be applied and later cleared. This is done using the class methods apply() and clear(). The apply() method sets the specified generator and branches on outage. The clear() method undoes the changes made by the apply() method. The following example shows how to apply and clear contingencies, and illustrates some of the side effects:

>>> print gen.is_in_service(), branch.is_in_service()
True True

>>> c1.apply(net)

>>> print gen.is_in_service(), branch.is_in_service()
False False

>>> c1.clear(net)

>>> print gen.is_in_service(), branch.is_in_service()
True True

More information about network contingencies can be found in the API reference.

3.7. Multiple Time Periods

PFNET can also be used to represent and analyze power networks over multiple time periods. By default, the networks created using most parsers, as in all the examples above, have data corresponding to a single time period. To consider multiple time periods, an argument needs to be passed to the parse method of a Parser:

>>> net = pfnet.PyParserMAT().parse('ieee14.m', num_periods=5)

>>> print net.num_periods
5

In “multi-period” networks, certain quantities can vary over time and hence are represented by vectors. Examples of such quantities are the network properties, generators powers, load powers, battery energy levels, bus voltage magnitudes, etc. The example below shows how to set the load profile over the time periods and extract the maximum active power mismatches in the network for each time:

>>> import numpy as np

>>> for load in net.loads:
...     load.P = np.random.rand(5)

>>> print net.loads[0].P
[0.54  0.71 0.60 0.54 0.42]

>>> net.update_properties()

>>> print [net.bus_P_mis[t] for t in range(5)]
[88.77, 73.23, 81.38, 86.64, 83.56]

Lastly, for component quantities that can potentially vary over time, setting these quantities to be variables results in one variable for each time. For example, selecting the bus voltage magnitude of a bus to be variable leads to having one variable for each time period:

>>> bus = net.buses[3]

>>> net.set_flags_of_component(bus, 'variable', 'voltage magnitude')

>>> print(net.num_vars)
5

>>> print bus.index_v_mag
[0 1 2 3 4]

3.8. Subnetworks

It is also possible in PFNET to extract subnetworks of Network objects. This can be done using the class method extract_subnetwork(), which takes as argument a list of Bus objects that correspond to the subnetwork buses.

3.9. Network Modifications

Bus connections can be modified either from the Bus object or the components connected to it. The following example removes a generator and a load from a bus, and then adds them back using the two different ways:

>>> net = pfnet.PyParserMAT().parse('ieee14.m')

>>> bus = net.buses[8]

>>> print len(bus.generators), len(bus.loads)
1 1

>>> gen = bus.generators[0]
>>> load = bus.loads[0]

>>> print gen.bus == bus, load.bus == bus
True True

>>> gen.bus = None
>>> bus.remove_load(load)

>>> print len(bus.generators), len(bus.loads)
0 0

>>> bus.add_generator(gen)
>>> load.bus = bus

>>> print len(bus.generators), len(bus.loads)
1 1

>>> print gen.bus == bus, load.bus == bus
True True

It is also possible to add and remove components to a network. This can be done using the Network class methods add_buses(), add_generators(), etc and remove_buses(), remove_generators(), etc:

>>> new_gen = pfnet.Generator()
>>> new_gen.bus = bus

>>> net.add_generators([new_gen])
>>> print new_gen == net.generators[-1]
True

>>> print len(bus.generators), len(bus.loads)
2 1

>>> net.remove_generators([new_gen])
>>> print len(bus.generators), len(bus.loads)
1 1

Warning

Making Network modifications programmatically is not yet guaranteed to be safe. One can easily leave out components with no bus connections, which could lead to errors in the underlying PFNET C library. Also, when adding or removing components from the network, the underlying PFNET C library copies existing data to new memory locations and hence any existing PFNET Python network components can point to invalid C memory locations. It is therefore recommended not to use existing PFNET Python network components after adding or removing components of the same type from the network, but instead re-extract them from the updated network using get_bus(), get_generator(), etc.