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