Controls and Regulation#
This notebook covers voltage control in the data model: declaring remote
generator regulation, inspecting which device regulates which bus, detecting a
setpoint conflict, and confirming that a held setpoint is achieved. The control
model is read through analyze_case, which inspects the case before a solve. A
small network is built in memory so that the notebook is self-contained.
import contextlib, os
import gpf
from gpf import GpfCase, Bus, Generator, Load, Branch
def base_case() -> GpfCase:
return GpfCase(
sbase=100.0,
buses=[
Bus(ibus=1, ide=3, vm=1.02, area=1, baskv=230.0), # slack
Bus(ibus=2, ide=2, vm=1.01, area=1, baskv=230.0), # PV, regulates bus 3
Bus(ibus=3, ide=1, vm=1.0, area=1, baskv=230.0),
Bus(ibus=4, ide=2, vm=1.0, area=1, baskv=230.0), # PV
],
generators=[
Generator(ibus=1, machid="1", pg=0.0, vs=1.02, qb=-500, qt=500),
Generator(ibus=2, machid="1", pg=60.0, vs=1.05, ireg=3, qb=-400, qt=400),
Generator(ibus=4, machid="1", pg=40.0, vs=1.0, qb=-200, qt=200),
],
loads=[Load(ibus=3, pl=80, ql=25), Load(ibus=4, pl=30, ql=10)],
branches=[
Branch(ibus=1, jbus=2, r=0.01, x=0.08),
Branch(ibus=2, jbus=3, r=0.01, x=0.08),
Branch(ibus=3, jbus=4, r=0.01, x=0.08),
Branch(ibus=1, jbus=3, r=0.01, x=0.10),
],
)
case = base_case()
Remote regulation#
The generator at bus 2 regulates the remote bus 3 through its ireg field,
while the generator at bus 4 regulates its own bus locally. Each controller
carries an is_local flag, so filtering to remote control is expected to leave
bus 3 as the single remotely controlled target, held by the bus-2 machine.
ca = gpf.analyze_case(case)
{t: [c.device_key for c in cs if not c.is_local]
for t, cs in ca.controllers_by_target.items()
if any(not c.is_local for c in cs)}
{3: [(2, '1')]}
The setpoint is held#
When the regulating generator stays within its reactive limits, the solve holds the remote bus at its setpoint rather than at the flat value. This is shown as a recipe in Recipes.
A setpoint conflict#
When a second generator at bus 4 also regulates bus 3, but to a different
setpoint, the two controllers disagree. analyze_case is expected to report one
conflict at bus 3 carrying both setpoints, before any solve is attempted.
conflicted = base_case()
conflicted.generators[2].ireg = 3 # bus-4 machine now regulates bus 3 ...
conflicted.generators[2].vs = 0.98 # ... to 0.98, against the bus-2 machine's 1.05
conflicts = gpf.analyze_case(conflicted).conflicts
[(c.target_bus, sorted(c.setpoints), c.has_setpoint_mismatch) for c in conflicts]
[(3, [0.98, 1.05], True)]
The control report#
render_report summarizes the controllers and any conflicts as text.
print(gpf.analyze_case(conflicted).render_report())
============================================================
Case Analysis Report
============================================================
Summary: 4 buses, 3 gen, 4 branches, 2 remote-reg
Voltage Control Compiler Warnings:
- regulator group at target 3: setpoints differ by 7.000000e-02 pu (> 1e-06); range 0.980000 to 1.050000; using first active member 1.050000
Voltage Setpoint Contentions (for reference):
Bus 3: setpoints [0.9800, 1.0500]
- generator (2, '1'): V=1.0500 (from bus 2)
- generator (4, '1'): V=0.9800 (from bus 4)
Feature Counts:
Generators: 3 total
- 2 with remote regulation
Loads: 2 total
Branches: 4 lines, 0 2W xfmr, 0 3W xfmr