Interconnection studies#
An interconnection study determines how much generation or load can connect at
a proposed location. A study file lists the planning cases, requests, candidate
locations, and monitored equipment. gpf selects the applicable calculation
from each request's service type.
The network#
This example uses a five-bus, 230 kV system. Buses 1, 2, and 3 form a triangle. The 1-2 branch has a 200 MVA rating, while the other two branches have 300 MVA ratings. The 1-2 branch is therefore the restrictive corridor. Buses 4 and 5 connect to bus 1 through higher-rated lines. Bus 1 has the existing generator and is the reference bus.
import pathlib, tempfile
import gpf
from gpf import GpfCase, Bus, Branch, Generator, Load, save_case
from gpf.scenario import Study
def build_case(load_scale: float) -> GpfCase:
pl = {2: 60.0, 3: 60.0, 4: 80.0, 5: 40.0}
return GpfCase(
sbase=100.0,
buses=[
Bus(ibus=1, ide=3, vm=1.02, baskv=230.0, area=1, name="STATION A"),
Bus(ibus=2, ide=1, vm=1.00, baskv=230.0, area=1, name="STATION B"),
Bus(ibus=3, ide=1, vm=1.00, baskv=230.0, area=1, name="STATION C"),
Bus(ibus=4, ide=1, vm=1.00, baskv=230.0, area=2, name="STATION D"),
Bus(ibus=5, ide=1, vm=1.00, baskv=230.0, area=2, name="STATION E"),
],
generators=[Generator(ibus=1, machid="1", pg=sum(pl.values()) * load_scale,
pt=1200.0, pb=-600.0, qt=600.0, qb=-600.0,
vs=1.02, mbase=1200.0, stat=1)],
loads=[Load(ibus=b, loadid="1", pl=p * load_scale, ql=p * load_scale * 0.3,
stat=1, area=(1 if b < 4 else 2))
for b, p in pl.items()],
branches=[
Branch(ibus=1, jbus=2, ckt="1", r=0.004, x=0.10, b=0.02,
rate1=200.0, rate2=200.0, rate3=200.0, stat=1),
Branch(ibus=1, jbus=3, ckt="1", r=0.004, x=0.10, b=0.02,
rate1=300.0, rate2=300.0, rate3=300.0, stat=1),
Branch(ibus=2, jbus=3, ckt="1", r=0.004, x=0.10, b=0.02,
rate1=300.0, rate2=300.0, rate3=300.0, stat=1),
Branch(ibus=1, jbus=4, ckt="1", r=0.003, x=0.08, b=0.015,
rate1=400.0, rate2=400.0, rate3=400.0, stat=1),
Branch(ibus=4, jbus=5, ckt="1", r=0.003, x=0.08, b=0.015,
rate1=300.0, rate2=300.0, rate3=300.0, stat=1),
Branch(ibus=1, jbus=5, ckt="1", r=0.005, x=0.12, b=0.02,
rate1=300.0, rate2=300.0, rate3=300.0, stat=1),
],
)
work = pathlib.Path(tempfile.mkdtemp())
for season, scale in (("summer_peak", 1.0), ("winter_peak", 0.9), ("light_load", 0.5)):
save_case(build_case(scale), work / f"{season}.raw")
The study file#
The study file uses these blocks:
Block |
Purpose |
|---|---|
|
Names a planning case and project output assumptions. |
|
Defines a generator request and its connection point. |
|
Defines a load request and its connection point. |
|
Lists locations to screen without a filed request. |
A project's service selects its study treatment. Network-resource projects
are studied together for simultaneous delivery. Other projects are studied
individually.
STUDY_TOML = """
[study]
name = "Interconnection queue screen"
format = 5
[[case]]
name = "summer_peak"
file = "summer_peak.raw"
dispatch = { solar = 1.0, wind = 0.35, load = 1.0, "wind/network" = 0.40 }
[[case]]
name = "winter_peak"
file = "winter_peak.raw"
dispatch = { solar = 0.4, wind = 0.80, load = 1.0, "wind/network" = 0.45 }
[[case]]
name = "light_load"
file = "light_load.raw"
dispatch = { solar = 0.9, wind = 0.70, load = 0.4, "wind/network" = 0.80 }
[[project]]
label = "P1"
poi_bus = 2
request_mw = 300.0
resources = [ { technology = "wind", mw = 300.0, power_factor = 0.95 } ]
service = "network"
plant_bus = 90001
plant_kv = 34.5
gsu = { mva = 330.0, x_pct = 7.5 }
[[project]]
label = "P2"
poi_bus = 3
request_mw = 200.0
resources = [ { technology = "wind", mw = 200.0, power_factor = 0.95 } ]
service = "network"
plant_bus = 90002
plant_kv = 34.5
gsu = { mva = 220.0, x_pct = 7.5 }
[[project]]
label = "P3"
poi_bus = 4
request_mw = 120.0
resources = [ { technology = "solar", mw = 120.0, power_factor = 0.95 } ]
service = "energy"
[[load]]
label = "L1"
poi_bus = 5
id = "2"
mw = 90.0
power_factor = 0.98
served_mw = 0.0
service = "energy"
[candidates]
buses = [3, 4]
taps = [ { from = 4, to = 5, ckt = "1", pct = 40.0 } ]
[monitor]
voltage = { scope = { areas = [1, 2], kv_min = 100.0, kv_max = 999.0 }, voltage_band = [0.95, 1.05] }
thermal = { scope = { areas = [1, 2], kv_min = 100.0, kv_max = 999.0 }, base_rating_set = 1, contingency_rating_set = 1 }
[[sweep]]
family = "branch"
[[contingency]]
label = "loss of 1-3"
branches = [ { from = 1, to = 3, ckt = "1" } ]
"""
(work / "study.toml").write_text(STUDY_TOML)
study = Study.from_toml(work / "study.toml")
[(c.name, c.file) for c in study.defn.cases]
[('summer_peak', 'summer_peak.raw'),
('winter_peak', 'winter_peak.raw'),
('light_load', 'light_load.raw')]
The two network-resource projects use the simultaneous-delivery calculation.
[(p.label, " + ".join(p.technologies), p.service, p.request_mw)
for p in study.defn.projects] + \
[(p.label, "load", p.service, p.mw) for p in study.defn.load_projects]
[('P1', 'wind', 'network', 300.0),
('P2', 'wind', 'network', 200.0),
('P3', 'solar', 'energy', 120.0),
('L1', 'load', 'energy', 90.0)]
The queue screen#
run_screen studies each applicable request by itself. It checks the request
alone and with the other queued projects in service. Each row retains both DC
answers and the AC answer when verification runs, and selected names the
answer the report ranks. Network-resource projects do not appear in this table
because the group calculation studies them. P3 is not network-limited in these
cases, so the request sets its deliverable output of 120 MW. L1 receives its
full 90 MW in both peak cases. In the light-load case the queued generation
already overloads the 1-2 corridor, so the network blocks the load request.
screen = gpf.run_screen(study)
[(c.name, r.label, round(r.alone.headroom_mw, 1), round(r.queued.headroom_mw, 1),
r.selected.limited_by)
for c in screen.cases for r in c.rows]
[('summer_peak', 'P3', 120.0, 120.0, 'request'),
('summer_peak', 'L1', 90.0, 90.0, 'request'),
('winter_peak', 'P3', 120.0, 120.0, 'request'),
('winter_peak', 'L1', 90.0, 90.0, 'request'),
('light_load', 'P3', 120.0, 120.0, 'request'),
('light_load', 'L1', 90.0, 0.0, 'network')]
The result reports the most restrictive planning case for each request.
[(r.label, r.case, round(r.deliverable_mw, 1)) for r in screen.binding]
[('P3', 'light_load', 120.0), ('L1', 'light_load', 0.0)]
Project headroom#
solve_project_headroom balances each request against the system instead of one
specified bus. A generation request reduces existing generation in proportion
to participation factors. A load request increases generation by the same rule.
The load request has its full 90 MW headroom in both peak cases. In the light-load
case, queued generation already overloads the 1-2 corridor. The result therefore
reports negative thermal headroom.
headroom = gpf.solve_project_headroom(study)
[(c.name, r.label, round(r.headroom_mw, 1), r.limit_type, r.congested)
for c in headroom.cases for r in c.rows]
[('summer_peak', 'P3', 120.0, 'request', False),
('summer_peak', 'L1', 90.0, 'request', False),
('winter_peak', 'P3', 120.0, 'request', False),
('winter_peak', 'L1', 90.0, 'request', False),
('light_load', 'P3', 120.0, 'request', False),
('light_load', 'L1', 0.0, 'thermal', True)]
Group deliverability#
The group study checks whether the two network-resource projects can deliver power at the same time. In the light-load case, P1 can individually deliver 230 MW of its assumed 240 MW output. P2 can individually deliver its full 160 MW output.
def only(label: str) -> Study:
"""The study with one network-resource member kept and the other withdrawn."""
one = Study.from_toml(work / "study.toml")
one.defn.projects = [p for p in one.defn.projects
if p.service != "network" or p.label == label]
return one
alone = {label: {c.name: round(c.rows[0].group_mw, 1)
for c in gpf.solve_group_deliverability(only(label)).cases}
for label in ("P1", "P2")}
alone
{'P1': {'summer_peak': 120.0, 'winter_peak': 135.0, 'light_load': 230.0},
'P2': {'summer_peak': 80.0, 'winter_peak': 90.0, 'light_load': 160.0}}
The combined transfer uses both project buses and their assumed output weights. The projects share the same corridor, so the group can deliver 260 MW in the light-load case. This group result differs from both the 390 MW sum and the 160 MW minimum of the individual results.
group = gpf.solve_group_deliverability(study)
light = [c for c in group.cases if c.name == "light_load"][0]
{"alone": alone["P1"]["light_load"] + alone["P2"]["light_load"],
"minimum": min(alone["P1"]["light_load"], alone["P2"]["light_load"]),
"together": round(light.rows[0].group_mw, 1)}
{'alone': 390.0, 'minimum': 160.0, 'together': 260.1}
The result allocates the 260 MW group capability to the projects by their declared weights. The 1-2 corridor binds after the loss of the 1-3 branch. Both peak cases can serve the full group output.
[(c.name, r.label, round(r.weight, 2), round(r.request_mw, 1),
round(r.headroom_mw, 1), r.verdict)
for c in group.cases for r in c.rows]
[('summer_peak', 'P1', 0.6, 120.0, 120.0, 'full'),
('summer_peak', 'P2', 0.4, 80.0, 80.0, 'full'),
('winter_peak', 'P1', 0.6, 135.0, 135.0, 'full'),
('winter_peak', 'P2', 0.4, 90.0, 90.0, 'full'),
('light_load', 'P1', 0.6, 240.0, 156.0, 'partial'),
('light_load', 'P2', 0.4, 160.0, 104.0, 'partial')]
Candidate points and line taps#
solve_poi_headroom screens locations that do not yet have a project request.
A candidate can be an existing bus or a tap on a line. This example places a
tap 40 percent of the distance from bus 4 to bus 5. In the summer case, bus 4
has 380 MW of headroom, bus 3 has 320 MW, and the tap has 300 MW. The line
segment beyond the tap limits the tap result.
poi = gpf.solve_poi_headroom(study)
summer = [c for c in poi.cases if c.name == "summer_peak"][0]
[(r.bus, round(r.dc.atc_mw, 1), r.dc.limiter.monitored, r.dc.limiter.contingency)
for r in summer.rows]
[(4, 380.2, (4, 5, '1'), '1-4-1'),
(3, 320.3, (1, 2, '1'), 'loss of 1-3'),
(6, 300.0, (6, 5, '1'), '4-6-1')]
Next steps#
The API reference describes gpf.run_screen, gpf.solve_project_headroom,
gpf.solve_group_deliverability, and gpf.solve_poi_headroom. The CLI
reference describes the gpf study screen, headroom, and group commands.
See Transfer capability for point-to-point transfers, Scenario study workflow for contingency studies, and Transmission rights for congestion pricing.