Controls and Regulation#
This tutorial shows how to define and inspect generator voltage control. It
covers remote regulation, voltage setpoints, and conflicting controls.
analyze_case reports the controls before you solve the case.
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 uses ireg to regulate the voltage at bus 3. The
generator at bus 4 regulates its own bus. The is_local field distinguishes
local and remote control. The filtered result therefore shows bus 3 as the only
remote target.
ca = gpf.analyze_case(case)
{t: [(c.device_type, c.source_bus) 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: [('generator', 2)]}
The setpoint is held#
The solver maintains the remote-bus setpoint while the regulating generator remains within its reactive-power limits. See Recipes for the complete solve example.
A setpoint conflict#
A conflict occurs when the generator at bus 4 also regulates bus 3 at a
different setpoint. analyze_case reports the conflict and both setpoints
before the solve.
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 creates a text summary of the controllers and conflicts.
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