Sensitivities#
Sensitivity factors estimate how network flows and voltages respond to a
change. A power transfer distribution factor (PTDF) describes a change in
injection. A line outage distribution factor (LODF) describes a branch outage.
An outage transfer distribution factor (OTDF) describes an injection change
during an outage. Use gpf.sensitivity to calculate these factors.
The calculation uses a solved AC operating point. It therefore includes the case's tap positions, shunts, and voltage profile. This example creates a case in Python, but you can also load a RAW, RAWX, or MATPOWER case.
Building the example#
A three-bus network is used. Bus 1 is the reference and carries a generator, bus 2 carries a second generator, and bus 3 carries the load. Bus 1 and bus 3 are joined by two parallel circuits, so a single circuit outage still leaves the load connected.
from gpf import (Bus, Branch, Generator, GenCost, Load, GpfCase)
def build_case(pl=180.0, pg1=120.0, pg2=60.0):
"""A three-bus test system with two parallel 1-3 circuits (60 MW normal,
90 MW emergency). Bus 1 is the reference and carries the cheap generator,
bus 2 the expensive one, bus 3 the load."""
buses = [Bus(ibus=1, ide=3, baskv=138.0),
Bus(ibus=2, ide=2, baskv=138.0),
Bus(ibus=3, ide=1, baskv=138.0)]
gens = [Generator(ibus=1, machid="1", pg=pg1, pt=250.0, pb=0.0,
qt=200, qb=-200, vs=1.0, stat=1),
Generator(ibus=2, machid="1", pg=pg2, pt=150.0, pb=0.0,
qt=200, qb=-200, vs=1.0, stat=1)]
branches = [
Branch(ibus=1, jbus=2, ckt="1", r=0.005, x=0.1, rate1=200, rate3=200, stat=1),
Branch(ibus=1, jbus=3, ckt="1", r=0.005, x=0.1, rate1=60, rate3=90, stat=1),
Branch(ibus=1, jbus=3, ckt="2", r=0.005, x=0.1, rate1=60, rate3=90, stat=1),
Branch(ibus=2, jbus=3, ckt="1", r=0.005, x=0.1, rate1=200, rate3=200, stat=1),
]
loads = [Load(ibus=3, loadid="1", pl=pl, ql=0.0, stat=1)]
costs = [GenCost(ibus=1, machid="1", model=2, cost=[10.0, 0.0]),
GenCost(ibus=2, machid="1", model=2, cost=[40.0, 0.0])]
return GpfCase(sbase=100.0, rev=33, basfrq=60.0, buses=buses,
generators=gens, branches=branches, loads=loads,
fixed_shunts=[], transformers=[], gen_costs=costs)
case = build_case()
MONITORED = [(1, 3, "1"), (1, 3, "2"), (1, 2, "1"), (2, 3, "1")]
len(case.buses), len(case.branches)
(3, 4)
Power transfer distribution factors#
The ptdf method returns the fraction of an injection at a target bus that
flows on each monitored branch. The swing bus, bus 1, supplies the matching
withdrawal.
An injection at bus 3 with a withdrawal at bus 1 flows toward bus 1. The PTDF values on the 1-3 circuits are therefore negative. Each parallel circuit carries approximately 0.40 of the transfer, and the remaining flow uses bus 2. The table uses target buses as columns and monitored branches as rows.
import gpf
with gpf.sensitivity(case) as snap:
ptdf = snap.ptdf(monitored=MONITORED, targets=[2, 3])
{row: {bus: round(float(v), 3) for bus, v in zip(ptdf.col_labels, vals)}
for row, vals in zip(ptdf.row_labels, ptdf.values)}
{'1-3-1': {2: -0.2, 3: -0.402},
'1-3-2': {2: -0.2, 3: -0.402},
'1-2-1': {2: -0.601, 3: -0.202},
'2-3-1': {2: 0.399, 3: -0.202}}
Line outage distribution factors#
The lodf method shows how an outaged branch's previous flow moves to other
branches. If the second 1-3 circuit opens, approximately two thirds of its flow
moves to the remaining 1-3 circuit. The other third uses the 1-2 and 2-3 path.
The outaged branch has an LODF of -1 because its flow becomes zero.
with gpf.sensitivity(case) as snap:
lodf = snap.lodf(monitored=MONITORED, outage=[(1, 3, "2")])
{row: round(float(vals[0]), 3) for row, vals in zip(lodf.row_labels, lodf.values)}
{'1-3-1': 0.673, '1-3-2': -1.0, '1-2-1': 0.337, '2-3-1': 0.337}
Outage transfer distribution factors#
The otdf method shows how an injection changes monitored flows while a branch
is out of service. The result groups factors by outage label.
The surviving 1-3 circuit has a PTDF of -0.402 before the outage and an OTDF of -0.673 after the parallel circuit opens. The larger magnitude shows that the surviving circuit carries the full direct-path transfer.
with gpf.sensitivity(case) as snap:
otdf = snap.otdf(monitored=MONITORED, outage=[(1, 3, "2")], targets=[3])
post = dict(zip(otdf["1-3-2"].row_labels, otdf["1-3-2"].values[:, 0]))
pre = dict(zip(ptdf.row_labels, ptdf.values[:, 1]))
lo = dict(zip(lodf.row_labels, lodf.values[:, 0]))
{"otdf": round(float(post["1-3-1"]), 3),
"ptdf + lodf * ptdf(outaged)":
round(float(pre["1-3-1"] + lo["1-3-1"] * pre["1-3-2"]), 3)}
{'otdf': -0.673, 'ptdf + lodf * ptdf(outaged)': -0.673}
Voltage sensitivities#
vsf returns each bus voltage's sensitivity to an injection, tap change, or
shunt change. Generators regulate the voltages at buses 1 and 2, so a shunt at
bus 3 does not change those voltages. Their sensitivities are zero. Bus 3 is not
regulated, so it shows the voltage response.
with gpf.sensitivity(case) as snap:
vsf = snap.vsf(buses=[1, 2, 3], shunts=[3])
{bus: round(float(v), 5) for bus, v in zip(vsf.row_labels, vsf.values[:, 0])}
{'1': 0.0, '2': 0.0, '3': 0.00033}
Next steps#
Transfer capability uses sensitivity factors to calculate
megawatt headroom. Transmission rights uses them for
network feasibility, and optimal power flow uses them for
security constraints. The API reference describes gpf.sensitivity and
DcSensitivity.