This site is version 0.0.1. Most of the code on this site is drafted by an agent and needs review. I apologize for the AI-speak, we are working on rewriting it!
viv-lang.org
Python Viv

An interconnection study on the IEEE 118-bus system

A 200 MW generator asks to connect at bus 38 of MATPOWER's case118, a 345 kV bus. vp.power.interconnect solves the base case and all 240 single outages (every branch and generator) without the project and with it, lists the limits the project breaks, gives its distribution factors on every branch, and finds its transfer limit: the largest output with no new violation, by bisection on the full AC N-1.

case118 has no branch ratings, so the limits here are bus voltages: 0.95 to 1.05 per unit in the base case and 0.90 to 1.10 after an outage.

Before and after

The project holds bus 38 at its pre-project voltage within a 0.95 power factor either way; its 200 MW is absorbed by the reference bus (69).

import numpy as np
import vipyr as vp
from vipyr import _power
from vipyr.figure import figure

r = vp.power.interconnect("case118", 38, 200.0, mw_max=1500.0, tol_mw=1.0)
print(
    "violations before:",
    len(r.violations_before),
    " after:",
    len(r.violations_after),
    " new:",
    len(r.new),
    " worsened:",
    len(r.worsened),
)
res = {
    "before": r.violations_before,
    "new": r.new,
    "kinds": r.kinds,
    "elems": r.elems,
}
print("before the project (contingency, violation, bus row, Vm, limit, %):")
for line in _power.format_violations(res, "before"):
    print("  " + line)
print("new with it:", _power.format_violations(res, "new") or "none")
violations before: 3  after: 3  new: 0  worsened: 0
before the project (contingency, violation, bus row, Vm, limit, %):
  base case        low voltage       52      0.946     0.950    99.6%
  base case        low voltage       75      0.943     0.950    99.3%
  base case        low voltage      117      0.949     0.950    99.9%
new with it: none

Distribution factors

The DC model's share of each MW of the project that flows on each branch, injected at bus 38 and taken out at bus 69. The largest ones mark the path the output takes.

top = np.argsort(-abs(r.df))[:5]
c, _ = _power.load_case("case118")
for k in top:
    print(
        "branch %3d (%3d-%3d): %+.3f MW per MW"
        % (k, c["branch"][k, 0], c["branch"][k, 1], r.df[k])
    )
branch 103 ( 65- 68): +0.630 MW per MW
branch  95 ( 38- 65): +0.595 MW per MW
branch 106 ( 68- 69): +0.524 MW per MW
branch  50 ( 38- 37): +0.243 MW per MW
branch  29 ( 23- 24): +0.182 MW per MW

The transfer limit

Each bisection step runs the whole N-1 again at one output. Above the limit a contingency drives a bus outside its emergency band.

print("transfer limit: %.1f MW" % r.transfer_limit_mw)
first = r.steps[r.steps[:, 1] > 0]
above = first[first[:, 0] > r.transfer_limit_mw]
print(
    "smallest output tried with a new violation: %.1f MW" % above[:, 0].min()
)
poi = int(np.where(c["bus"][:, 0] == 38)[0][0])
vset = vp.power.runpf("case118").vm[poi]
mw = np.linspace(0.0, 1400.0, 15)
worst_lo = []
for p in mw:
    q = p * np.tan(np.arccos(0.95))
    bus, gen = _power._api().inject_arrays(
        c["baseMVA"], c["bus"], c["gen"], c["branch"], poi, p, vset, -q, q, 1
    )
    n1 = vp.power.contingency(dict(c, bus=bus, gen=gen))
    vm = np.where(n1.vm[1:] > 0, n1.vm[1:], np.nan)
    worst_lo.append(np.nanmin(vm))
fig = figure(
    "Lowest post-contingency voltage against the project's output",
    "project output (MW)",
    "lowest Vm over all outages (per unit)",
)
fig.plot(mw, np.array(worst_lo), "lowest Vm")
fig.hline("emergency limit 0.90", 0.90)
fig.vline("transfer limit", r.transfer_limit_mw)
transfer limit: 971.2 MW
smallest output tried with a new violation: 971.9 MW
Lowest post-contingency voltage against the project's outputlowest Vm0.800.850.9005001000project output (MW)lowest Vm over all outages (per unit)

Credits

  • The methods follow MATPOWER's (BSD-3-Clause). The test cases are MATPOWER's case files, each with its source named in its header.

Source

gallery/power/plot_interconnection_study.py, run as written when these pages were built. The output and the figures above are from that run.

The Viv program it calls is lib/power/study.viv: its atlas, the defs and the calls between them, or open it in the workbench.