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