Copyright 2023-2026 Udo Schmitz

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

file: docs/lit/workshoptouradvanced.jl purpose: Literate.jl source of the ADVANCED workshop tour (Expert and Beyond tiers): remote voltage control, HVDC pairing, state estimation, FACTS limits, N-1 contingency batches, and threaded sweeps. Companion of workshop_tour.jl (Newcomer to Advanced).

The Sparlectra workshop tour, advanced

Open in Colab

Note: This workshop was created with AI assistance and is reviewed and curated by the maintainer; it is not a fully machine-generated text.

This is the second half of the Sparlectra workshop: it picks up where the basic tour (Newcomer to Advanced: first network, model work, slack types and short circuit, tap control, Q(U)) leaves off. You should be comfortable building a net and reading a power-flow result; everything else is introduced here.

After the warm-up the chapters climb two tiers:

Expert

  1. Remote voltage control by a machine
  2. A steerable HVDC link (back-to-back pairing, incl. meshed operation)
  3. State estimation
  4. FACTS devices and their limits (STATCOM vs SVC, switched banks, TCSC vs SSSC)
  5. N-1 contingency analysis

Beyond

  1. Using your cores: parallel sweeps on Julia threads

Note: On Google Colab the install cell takes a few minutes on a fresh session (package download and precompilation). Colab's Julia version may change over time; this notebook targets Julia ≥ 1.12.

Warm-up and shared helpers

Julia compiles each function on first use. This one cell warms EVERY path the chapters exercise, so nothing stalls mid-tour: the Newton-Raphson solver, the HVDC pair controller (chapter 2), the WLS state estimator (chapter 3), the contingency batch (chapter 5), and the IEC 60909 short circuit (chapter 6 sweeps it under threads). The using clauses and the small helpers of the whole tour live here too, collected up top so they cannot be missed.

using Sparlectra
using Random

# solve helper used by all chapters (25 iterations, tolerance 1e-8)
function solve!(net; kwargs...)
  etime = @elapsed begin
    ite, erg = runpf!(net, 25, 1e-8, 0; kwargs...)
  end
  erg == 0 || error("Power flow did not converge (status = $erg)")
  calcNetLosses!(net)
  return etime, ite
end

# peek into the solved state (chapter 2 reads bus angles with it)
bus_va_deg(net, bus) = net.nodeVec[net.busDict[bus]]._va_deg

# the 7-bus double ring of the basic tour's Example 1.1, packed as a
# helper: two chapters here reuse it (Examples 3.1 and 5.1)
function build_ring7(name::String)
  net = Net(name = name, baseMVA = 100.0)
  addBus!(net = net, busName = "B1", vn_kV = 110.0, vm_pu = 1.02, va_deg = 0.0)
  for i in 2:7
    addBus!(net = net, busName = "B$(i)", vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)
  end
  addPIModelACLine!(net = net, fromBus = "B1", toBus = "B2", r_pu = 0.010, x_pu = 0.080, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "B2", toBus = "B3", r_pu = 0.011, x_pu = 0.085, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "B3", toBus = "B4", r_pu = 0.012, x_pu = 0.090, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "B4", toBus = "B5", r_pu = 0.010, x_pu = 0.080, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "B5", toBus = "B6", r_pu = 0.011, x_pu = 0.085, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "B6", toBus = "B7", r_pu = 0.012, x_pu = 0.090, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "B7", toBus = "B1", r_pu = 0.010, x_pu = 0.080, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "B2", toBus = "B5", r_pu = 0.009, x_pu = 0.070, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "B3", toBus = "B6", r_pu = 0.009, x_pu = 0.070, b_pu = 0.0, status = 1)
  addProsumer!(net = net, busName = "B1", type = "EXTERNALNETWORKINJECTION", referencePri = "B1", vm_pu = 1.02, va_deg = 0.0)
  addProsumer!(net = net, busName = "B3", type = "GENERATOR", p = 60.0, q = 10.0)
  addProsumer!(net = net, busName = "B2", type = "LOAD", p = 35.0, q = 10.0)
  addProsumer!(net = net, busName = "B4", type = "LOAD", p = 45.0, q = 15.0)
  addProsumer!(net = net, busName = "B5", type = "LOAD", p = 25.0, q = 8.0)
  addProsumer!(net = net, busName = "B6", type = "LOAD", p = 30.0, q = 10.0)
  addProsumer!(net = net, busName = "B7", type = "LOAD", p = 20.0, q = 6.0)
  ok, msg = validate!(net = net)
  ok || error("Network validation failed: $msg")
  return net
end

# tiny warm-up net: a grid connection WITH declared short-circuit data
wnet = Net(name = "warmup", baseMVA = 100.0)
addBus!(net = wnet, busName = "A", vn_kV = 110.0)
addBus!(net = wnet, busName = "B", vn_kV = 110.0)
addExternalGrid!(net = wnet, busName = "A", vm_pu = 1.0, sk_max_MVA = 2000.0, sk_min_MVA = 1500.0, rx_max = 0.1, internal_impedance = false)
addProsumer!(net = wnet, busName = "B", type = "ENERGYCONSUMER", p = 10.0, q = 3.0)
addPIModelACLine!(net = wnet, fromBus = "A", toBus = "B", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)

t_first = @elapsed runpf!(wnet, 10, 1e-8, 0)
t_second = @elapsed runpf!(wnet, 10, 1e-8, 0)
println("power flow     : first solve ", round(t_first; digits = 2), " s (compiles), second ", round(t_second * 1000; digits = 2), " ms")

t_sc = @elapsed runShortCircuit!(wnet; case = :max)
println("short circuit  : ", round(t_sc; digits = 2), " s")

# HVDC pair path: two 2-bus islands coupled by a controller
whv = Net(name = "warmup_hvdc", baseMVA = 100.0)
for b in ("W1", "W2", "W3", "W4")
  addBus!(net = whv, busName = b, vn_kV = 110.0)
end
addProsumer!(net = whv, busName = "W1", type = "EXTERNALNETWORKINJECTION", referencePri = "W1", vm_pu = 1.0, va_deg = 0.0)
addProsumer!(net = whv, busName = "W3", type = "EXTERNALNETWORKINJECTION", referencePri = "W3", vm_pu = 1.0, va_deg = 0.0)
addProsumer!(net = whv, busName = "W2", type = "ENERGYCONSUMER", p = 5.0, q = 1.0)
addProsumer!(net = whv, busName = "W4", type = "ENERGYCONSUMER", p = 5.0, q = 1.0)
addPIModelACLine!(net = whv, fromBus = "W1", toBus = "W2", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
addPIModelACLine!(net = whv, fromBus = "W3", toBus = "W4", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
addProsumer!(net = whv, busName = "W2", type = "GENERATOR", p = -5.0, q = 0.0)
addProsumer!(net = whv, busName = "W4", type = "GENERATOR", p = 5.0, q = 0.0)
addHvdcPairControl!(whv; from_bus = "W2", to_bus = "W4", p_transfer_mw = 5.0)
t_hvdc = @elapsed run_control!(whv; controllers = collect_outer_controllers(whv), pf_config = PowerFlowConfig(method = :rectangular, max_iter = 15, tol = 1e-8), control_config = ControlConfig(max_outer_iterations = 4, trace = false))
println("HVDC control   : ", round(t_hvdc; digits = 2), " s")

# state-estimation path: synthetic measurements plus one WLS run
setMeasurementsFromPF!(wnet; includeVm = true, includePinj = true, includeQinj = true, includePflow = true, includeQflow = true, noise = false)
t_se = @elapsed runse!(wnet; maxIte = 8, tol = 1e-6, flatstart = true, jacEps = 1e-6, updateNet = false)
println("state estimator: ", round(t_se; digits = 2), " s")

# chapter-5 path: one single-case contingency batch on the warm-up net
t_n1 = @elapsed runContingencies!(wnet, generateN1Branches(wnet))
println("contingency    : ", round(t_n1; digits = 2), " s, everything warm")
power flow     : first solve 0.0 s (compiles), second 0.17 ms
short circuit  : 0.0 s
AC island detection:
  islands: 2
  island 1: buses=2, branches=1, ref=1, status=has_ref
  island 2: buses=2, branches=1, ref=3, status=has_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
AC island detection:
  islands: 2
  island 1: buses=2, branches=1, ref=1, status=has_ref
  island 2: buses=2, branches=1, ref=3, status=has_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
HVDC control   : 2.18 s
state estimator: 0.0 s
contingency    : 1.09 s, everything warm

Part I: Expert

Chapter 1: remote voltage control by a machine

A machine regulates the voltage at a different bus via its reactive output, the counterpart of a CGMES RegulatingControl at a foreign terminal. The outer loop drives the machine's Q until the remote target is met, and parks honestly at_limit when the reactive range is too small. Details: Remote Voltage Control.

Example 1.1: a machine holding a remote bus. The corridor: slack, machine bus, and the remote target bus at the end; first uncontrolled, then with the controller attached (Q range ±50 MVAr):

 (slack, 1.02 pu)
   Slack -------- GenBus -------- Load
             machine 30 MW,   controlled bus,
             Q range varied   target 1.05 pu
function build_rvc(qmin_mvar::Float64, qmax_mvar::Float64)
  net = Net(name = "tour_rvc", baseMVA = 100.0)
  addBus!(net = net, busName = "Slack", vn_kV = 110.0)
  addBus!(net = net, busName = "GenBus", vn_kV = 110.0)
  addBus!(net = net, busName = "Load", vn_kV = 110.0)
  addProsumer!(net = net, busName = "Slack", type = "EXTERNALNETWORKINJECTION", vm_pu = 1.02, va_deg = 0.0, referencePri = "Slack")
  addProsumer!(net = net, busName = "GenBus", type = "SYNCHRONOUSMACHINE", p = 30.0, q = 0.0, qMin = qmin_mvar, qMax = qmax_mvar)
  addProsumer!(net = net, busName = "Load", type = "ENERGYCONSUMER", p = -70.0, q = -20.0)
  addPIModelACLine!(net = net, fromBus = "Slack", toBus = "GenBus", r_pu = 0.02, x_pu = 0.12, b_pu = 0.01, status = 1)
  addPIModelACLine!(net = net, fromBus = "GenBus", toBus = "Load", r_pu = 0.02, x_pu = 0.12, b_pu = 0.01, status = 1)
  return net
end

pf = PowerFlowConfig(max_iter = 30, tol = 1e-9)

net_rvc = build_rvc(-50.0, 50.0)
runpf!(net_rvc; config = pf, verbose = 0)
println("uncontrolled: Vm(Load) = ", round(get_bus_vm_pu(net_rvc, "Load"); digits = 4), " pu")

addMachineVoltageControl!(net_rvc; bus = "GenBus", target_bus = "Load", target_vm_pu = 1.05, deadband_vm_pu = 5e-4)
result = run_control!(net_rvc; controllers = collect_outer_controllers(net_rvc), pf_config = pf, control_config = ControlConfig(max_outer_iterations = 15), verbose = 0)
println("controlled:   Vm(Load) = ", round(get_bus_vm_pu(net_rvc, "Load"); digits = 4), " pu (target 1.05)")
println("loop: status = ", result.status, ", outer iterations = ", result.outer_iterations, ", pf solves = ", result.powerflow_solves)
printMachineControllerSummary(stdout, net_rvc)
uncontrolled: Vm(Load) = 1.0846 pu
controlled:   Vm(Load) = 1.05 pu (target 1.05)
loop: status = converged, outer iterations = 4, pf solves = 4

Machine Voltage Control Summary
-------------------------------
machine@GenBus RVC (machine at GenBus -> bus Load)
  target Vm          : 1.0500 pu
  achieved Vm        : 1.0500 pu
  reactive output    : -29.674 MVAr
  reactive range     : -50.000 .. 50.000 MVAr
  deadband           : 0.0005 pu
  converged          : true
  at_limit           : false
  status             : converged

Example 1.2: the honest failure mode. Cut the reactive range to ±2 MVAr on the same corridor as Example 1.1 and the same target is out of reach. The controller parks at its limit and says so instead of pretending convergence.

net_rvc2 = build_rvc(-2.0, 2.0)
runpf!(net_rvc2; config = pf, verbose = 0)
addMachineVoltageControl!(net_rvc2; bus = "GenBus", target_bus = "Load", target_vm_pu = 1.05, deadband_vm_pu = 5e-4)
result2 = run_control!(net_rvc2; controllers = collect_outer_controllers(net_rvc2), pf_config = pf, control_config = ControlConfig(max_outer_iterations = 15), verbose = 0)
println("limited:      Vm(Load) = ", round(get_bus_vm_pu(net_rvc2, "Load"); digits = 4), " pu (target 1.05)")
printMachineControllerSummary(stdout, net_rvc2)
limited:      Vm(Load) = 1.0823 pu (target 1.05)

Machine Voltage Control Summary
-------------------------------
machine@GenBus RVC (machine at GenBus -> bus Load)
  target Vm          : 1.0500 pu
  achieved Vm        : 1.0823 pu
  reactive output    : -2.000 MVAr
  reactive range     : -2.000 .. 2.000 MVAr
  deadband           : 0.0005 pu
  converged          : false
  at_limit           : true
  status             : at_limit
  status detail      : target not reached because a reactive limit was hit

Two AC areas joined ONLY by an HVDC converter pair: no AC tie, no angle coupling, so the areas stay two separate electrical islands with their own references. The transfer through the link is a control setpoint, not the result of an angle difference. Example 2.1: the Stage-0 snapshot. First the Stage-0 view: two fixed injections reproduce a snapshot of 80 MW transfer with 4 MW converter loss.

Note that EACH island keeps a classical reference of its own (A1 and C1); the converters at A2/C2 are plain injections the controller will steer. The result header therefore reports Slack: 2, and both reference buses appear as SLACK rows in the bus table:

     island A                          island C
  A1 -------- A2  ===== DC link =====  C2 -------- C1
(slack)    load 40 MW              load 50 MW    (slack)
           + converter             + converter
           (from side)             (to side)
function build_b2b(name::String)
  net = Net(name = name, baseMVA = 100.0)
  for b in ("A1", "A2", "C1", "C2")
    addBus!(net = net, busName = b, vn_kV = 380.0)
  end
  addPIModelACLine!(net = net, fromBus = "A1", toBus = "A2", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "C1", toBus = "C2", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
  addProsumer!(net = net, busName = "A1", type = "EXTERNALNETWORKINJECTION", referencePri = "A1", vm_pu = 1.0, va_deg = 0.0)
  addProsumer!(net = net, busName = "C1", type = "EXTERNALNETWORKINJECTION", referencePri = "C1", vm_pu = 1.0, va_deg = 0.0)
  addProsumer!(net = net, busName = "A2", type = "ENERGYCONSUMER", p = 40.0, q = 10.0)
  addProsumer!(net = net, busName = "C2", type = "ENERGYCONSUMER", p = 50.0, q = 12.0)
  addProsumer!(net = net, busName = "A2", type = "GENERATOR", p = -80.0, q = 0.0)  ## converter, exports
  addProsumer!(net = net, busName = "C2", type = "GENERATOR", p = 76.0, q = 0.0)   ## converter, receives 80 - 4
  return net
end

net6 = build_b2b("tour_b2b")
# register the hand-built link so the result tables report it (importers
# and addHvdcPairControl! do this automatically)
addHvdcLink!(net6; from_bus = "A2", to_bus = "C2")
etime, ite = solve!(net6; islands_enabled = true)
println("two islands solved in ", ite, " iteration(s)")
printACPFlowResults(net6, etime, ite, 1e-8)
AC island detection:
  islands: 2
  island 1: buses=2, branches=1, ref=1, status=has_ref
  island 2: buses=2, branches=1, ref=3, status=has_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
two islands solved in 9 iteration(s)
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :  25-Aug-26 16:19:22
Iterations     :         9
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.001417 s
Case           :       tour_b2b
Cooldown iters :         0
Q-hysteresis   :    0.0000 pu
Jacobian cond. : kappa1(J) = 1.18, attainable accuracy ~ 2.6e-16, well conditioned (tol 1.0e-8 reachable)
BaseMVA        :       100
Nodes          :         4 (PV: 0 PQ: 2 Slack: 2)
Grid connection: slack bus A1; slack bus C1
Branches       :         2
Links          :         0
HVDC links     :         1
Lines          :         2
Trafos         :         0
Generators     :         4
Loads          :         2
Shunts         :         0
Controllers    :         0 (Tap: 0, Q(U): 0, P(U): 0)
PV→PQ locks    :         0
PV→PQ events   :         0

total network power balance (Σ S_branch): P =      1.609 [MW], Q =     12.875 [MVar]
converter losses (HVDC): P =      4.000 [MW]

==========================================================================================================================================================================================================================
| Nr    | Bus                  | Vn [kV]    | V [kV]     | V [pu]     | phi [deg]  | Pg [MW]    | Qg [MVar]  | Pl [MW]    | Ql [MVar]  | Ps [MW]    | Qs [MVar]  | Type       | Control      | Tap Vm tgt   |
==========================================================================================================================================================================================================================
| 1     | A1                   | 380.0      | 380.000    | 1.000      | 0.000      |    121.526 |     22.210 |            |            |            |            | SLACK      | -            |              |
| 2     | A2                   | 380.0      | 370.394    | 0.975      | -5.593     |    -80.000 |            |     40.000 |     10.000 |            |            | PQ         | -            |              |
| 3     | C1                   | 380.0      | 380.000    | 1.000      | 0.000      |    -25.917 |     12.666 |            |            |            |            | SLACK      | -            |              |
| 4     | C2                   | 380.0      | 377.227    | 0.993      | 1.270      |     76.000 |            |     50.000 |     12.000 |            |            | PQ         | -            |              |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

==========================================================================================================================================================================================================================
| Branch                    | Type   | Connection                | P [MW]     | Q [MVar]   | P [MW]     | Q [MVar]   | Pv [MW]    | Qv [MVar]  | Ctrl       | P_tgt      | TapPos    | Ctrl status            |
==========================================================================================================================================================================================================================
| B_ACL_380_1_2             | Line   | A1 -> A2                  | 121.526    | 22.210     | -120.000   | -10.000    | 1.526      | 12.210     | -          | -          | -         | -                      |
| B_ACL_380_3_4             | Line   | C1 -> C2                  | -25.917    | 12.666     | 26.000     | -12.000    | 0.083      | 0.666      | -          | -          | -         | -                      |
| HVDC_A2_C2                | Link   | A2 -> C2                  | 80.000     | 0.000      | -76.000    | 0.000      | 4.000      | 0.000      | -          | -          | -         | HVDC, not a branch     |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


--------------------------------------------------------------------------- HVDC Link Flows ---------------------------------------------------------------------------
| Nr    | Name           | From         | To           | Mode        | P_from [MW] | P_to [MW] | Loss [MW] | Q_from [MVar] | Q_to [MVar] | Rating  | Status           |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 1     | HVDC_A2_C2     | A2           | C2           | fixed       | 80.000      | 76.000    | 4.000     | 0.000         | 0.000       | -       | -                |

Control
-------
Transformer controls: none

Reading aid (Example 2.1): both areas balance on their own reference; the link carries whatever the snapshot says. Example 2.2: pairing the converters. To make it steerable on the same net, pair the two converter injections: addHvdcPairControl! enforces the invariant $P_\text{to} = P_\text{transfer} - P_\text{loss}$ exactly and lets you retarget the transfer.

addHvdcPairControl!(net6; from_bus = "A2", to_bus = "C2", p_transfer_mw = 120.0, loss_mw = 4.0, p_rating_mw = 150.0)
result6 = run_control!(net6; controllers = collect_outer_controllers(net6), pf_config = PowerFlowConfig(method = :rectangular, max_iter = 25, tol = 1e-8), control_config = ControlConfig(max_outer_iterations = 8, trace = false))
calcNetLosses!(net6)
printACPFlowResults(net6, etime, result6.last_pf_iterations, 1e-8)
AC island detection:
  islands: 2
  island 1: buses=2, branches=1, ref=1, status=has_ref
  island 2: buses=2, branches=1, ref=3, status=has_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
AC island detection:
  islands: 2
  island 1: buses=2, branches=1, ref=1, status=has_ref
  island 2: buses=2, branches=1, ref=3, status=has_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :  25-Aug-26 16:19:23
Iterations     :         8
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.001417 s
Case           :       tour_b2b
Cooldown iters :         0
Q-hysteresis   :    0.0000 pu
Jacobian cond. : kappa1(J) = 1.11, attainable accuracy ~ 2.5e-16, well conditioned (tol 1.0e-8 reachable)
BaseMVA        :       100
Nodes          :         4 (PV: 0 PQ: 2 Slack: 2)
Grid connection: slack bus A1; slack bus C1
Branches       :         2
Links          :         0
HVDC links     :         1
Lines          :         2
Trafos         :         0
Generators     :         4
Loads          :         2
Shunts         :         0
Controllers    :         1 (Tap: 0, Q(U): 0, P(U): 0, HVDC: 1)
PV→PQ locks    :         0
PV→PQ events   :         0

total network power balance (Σ S_branch): P =      3.205 [MW], Q =     25.643 [MVar]
converter losses (HVDC): P =      4.000 [MW]

==========================================================================================================================================================================================================================
| Nr    | Bus                  | Vn [kV]    | V [kV]     | V [pu]     | phi [deg]  | Pg [MW]    | Qg [MVar]  | Pl [MW]    | Ql [MVar]  | Ps [MW]    | Qs [MVar]  | Type       | Control      | Tap Vm tgt   |
==========================================================================================================================================================================================================================
| 1     | A1                   | 380.0      | 380.000    | 1.000      | 0.000      |    162.751 |     32.010 |            |            |            |            | SLACK      | -            |              |
| 2     | A2                   | 380.0      | 367.269    | 0.966      | -7.551     |   -120.000 |            |     40.000 |     10.000 |            |            | PQ         | B2B          |              |
| 3     | C1                   | 380.0      | 380.000    | 1.000      | 0.000      |    -65.546 |     15.633 |            |            |            |            | SLACK      | -            |              |
| 4     | C2                   | 380.0      | 378.295    | 0.996      | 3.109      |    116.000 |            |     50.000 |     12.000 |            |            | PQ         | B2B          |              |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

==========================================================================================================================================================================================================================
| Branch                    | Type   | Connection                | P [MW]     | Q [MVar]   | P [MW]     | Q [MVar]   | Pv [MW]    | Qv [MVar]  | Ctrl       | P_tgt      | TapPos    | Ctrl status            |
==========================================================================================================================================================================================================================
| B_ACL_380_1_2             | Line   | A1 -> A2                  | 162.751    | 32.010     | -160.000   | -10.000    | 2.751      | 22.010     | -          | -          | -         | -                      |
| B_ACL_380_3_4             | Line   | C1 -> C2                  | -65.546    | 15.633     | 66.000     | -12.000    | 0.454      | 3.633      | -          | -          | -         | -                      |
| HVDC_A2_C2                | Link   | A2 -> C2                  | 120.000    | 0.000      | -116.000   | 0.000      | 4.000      | 0.000      | setpoint   | 120.000    | -         | HVDC, not a branch     |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


--------------------------------------------------------------------------- HVDC Link Flows ---------------------------------------------------------------------------
| Nr    | Name           | From         | To           | Mode        | P_from [MW] | P_to [MW] | Loss [MW] | Q_from [MVar] | Q_to [MVar] | Rating  | Status           |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 1     | HVDC_A2_C2     | A2           | C2           | setpoint    | 120.000     | 116.000   | 4.000     | 0.000         | 0.000       | 150.0   | converged        |

Control
-------
Transformer controls: none

HVDC Pair Control Summary (back-to-back)
----------------------------------------
B2B_A2_C2 B2B (link A2 -> C2)
  transfer           : 120.000 MW (rating 150.0 MW)
  loss               : 4.000 MW
  from injection     : -120.000 MW / 0.000 MVAr
  to injection       : 116.000 MW / 0.000 MVAr
  converged          : true
  at_limit           : false
  status             : converged

Reading aid (Example 2.2): the HVDC pair reports inside the Control section of the classical result output, in the same aligned label/value layout as the transformer, machine, and TCSC summaries (printHvdcPairControllerSummary prints the same block standalone). The link itself has its own HVDC Link Flows table right after the link table: ordered transfer, delivered power, loss, and the controller status per link. And look at the LAST row of the branch table: the link appears there too, typed Link and marked "HVDC, not a branch", so the topology reads in one table; it looks like a branch (two buses, power moves), but no Y-bus element exists behind it, Q is zero, and its Pv column is the converter loss. The link now carries 120 MW instead of 80: the line A1 -> A2 supplies 40 MW more (area A's reference generates the export), while C1 -> C2 turns around and carries the received power away from the converter bus.

Why does the solver still report two islands? An AC voltage angle is only defined within one synchronous island, relative to that island's own reference. The link transfers power but no angle information (there is no branch, no admittance, no angle coupling between the areas), so each island keeps its own reference pinned at 0 degrees. The two-island report is the model telling you the areas are asynchronous; it would be wrong for it to disappear. The peek below makes that visible: both reference buses sit at exactly 0.0 deg, and comparing an A-side angle with a C-side angle carries no information, because each is measured against a different zero.

# bus_va_deg comes from the warm-up cell (shared helpers up top)
for (bus, role) in (("A1", "reference of island A"), ("A2", "converter, exports 120 MW"), ("C1", "reference of island C"), ("C2", "converter, receives 116 MW"))
  println(rpad(bus, 4), rpad(role, 27), ": Vm = ", round(get_bus_vm_pu(net6, bus); digits = 4), " pu, Va = ", round(bus_va_deg(net6, bus); digits = 3), " deg")
end
A1  reference of island A      : Vm = 1.0 pu, Va = 0.0 deg
A2  converter, exports 120 MW  : Vm = 0.9665 pu, Va = -7.551 deg
C1  reference of island C      : Vm = 1.0 pu, Va = 0.0 deg
C2  converter, receives 116 MW : Vm = 0.9955 pu, Va = 3.109 deg

Reading aid (Example 2.2): within island A the angle falls toward A2 (the converter bus imports 120 MW plus the local load from the reference), within island C it rises toward C2 (the converter bus feeds the island). Each gradient is meaningful only against its own 0-degree reference. The same controller attaches automatically on import when a MATPOWER case sets matpower_dcline_mode = paired_control or a CGMES delivery is loaded with hvdc_mode = paired_control. Theory: HVDC Back-to-Back.

HVDC as the island's source (grid-forming)

Can the converter itself BE the reference (slack) of island C? Not as one side of the paired controller: the pairing treats the transfer as a setpoint on both sides, while a slack's power is the outcome of its island's balance (load plus losses). One injection cannot be both at once, so addHvdcPairControl! refuses a reference bus by design.

The converse is a perfectly valid model though, called a grid-forming (Vf) converter: the receiving converter IS the island's source. It holds voltage and angle at its PCC, and its power output follows from whatever the island draws. Think of an offshore platform or an asynchronously supplied island grid.

A word on terms: "slack" is the solver's name for an island's reference node, and every island has exactly one, however it is modeled. The ideal slack of the basic tour's chapter 3 (Example 3.1 there), the external-grid SOURCE behind an impedance, and the grid-forming converter here are three MODELS of that one reference. The result output keeps them apart: SOURCE in the bus table (and Source: m in the header) is reserved for the external-grid feeder element, because there the reference voltage sits BEHIND an impedance. The grid-forming converter is an ideal reference directly at its PCC, so its row honestly reads SLACK; its role is marked in the Control column instead: B2B src for the grid-forming reference, B2B for a steered converter injection. Example 2.3: the grid-forming converter. Island C below has NO source of its own; its reference moves onto the converter bus C2 (compare the sketch with the setpoint variant of Example 2.1):

     island A                          island C
  A1 -------- A2  ===== DC link =====  C2 -------- C1
(slack)    load 40 MW            grid-forming    load 50 MW
           + sending             converter
           converter             (= island C reference)
function build_b2b_source(name::String; sending_mw::Float64 = 0.0)
  net = Net(name = name, baseMVA = 100.0)
  for b in ("A1", "A2", "C1", "C2")
    addBus!(net = net, busName = b, vn_kV = 380.0)
  end
  addPIModelACLine!(net = net, fromBus = "A1", toBus = "A2", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "C2", toBus = "C1", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
  addProsumer!(net = net, busName = "A1", type = "EXTERNALNETWORKINJECTION", referencePri = "A1", vm_pu = 1.0, va_deg = 0.0)
  addProsumer!(net = net, busName = "A2", type = "ENERGYCONSUMER", p = 40.0, q = 10.0)
  # island C: no classical slack. The receiving converter is the
  # grid-forming source, holding 1.0 pu / 0 deg at its PCC bus C2.
  addProsumer!(net = net, busName = "C2", type = "EXTERNALNETWORKINJECTION", referencePri = "C2", vm_pu = 1.0, va_deg = 0.0)
  addProsumer!(net = net, busName = "C1", type = "ENERGYCONSUMER", p = 50.0, q = 12.0)
  # sending-side converter; mirrored below once the island balance is known
  addProsumer!(net = net, busName = "A2", type = "GENERATOR", p = sending_mw, q = 0.0)
  return net
end

net7 = build_b2b_source("tour_b2b_source")
solve!(net7; islands_enabled = true)
p_island_c = get_branch_p_from_to_mw(net7, "C2", "C1")
println("grid-forming converter at C2 delivers ", round(p_island_c; digits = 3), " MW (island C load + line loss)")
for bus in ("C2", "C1")
  println("  ", bus, ": Vm = ", round(get_bus_vm_pu(net7, bus); digits = 4), " pu, Va = ", round(bus_va_deg(net7, bus); digits = 3), " deg")
end
AC island detection:
  islands: 2
  island 1: buses=2, branches=1, ref=1, status=has_ref
  island 2: buses=2, branches=1, ref=4, status=has_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
grid-forming converter at C2 delivers 50.273 MW (island C load + line loss)
  C2: Vm = 1.0 pu, Va = 0.0 deg
  C1: Vm = 0.9844 pu, Va = -2.259 deg

Reading aid (Example 2.3): the transfer is no longer a setpoint. Island C decides how much it draws (load plus line loss), the converter delivers exactly that, and the island's reference sits at the converter PCC: 1.0 pu and 0 degrees at C2, while the load bus C1 hangs below it. Example 2.4: mirroring the sending side. On the same corridor, the sending side must now mirror the island draw plus the converter loss:

net8 = build_b2b_source("tour_b2b_mirrored"; sending_mw = -(p_island_c + 4.0))
addHvdcLink!(net8; from_bus = "A2", to_bus = "C2")  ## Stage-0 record for the result tables
etime8, ite8 = solve!(net8; islands_enabled = true)
println("sending side A1 -> A2 carries ", round(get_branch_p_from_to_mw(net8, "A1", "A2"); digits = 3), " MW (40 MW local load + ", round(p_island_c + 4.0; digits = 3), " MW export + line loss)")
printACPFlowResults(net8, etime8, ite8, 1e-8)
AC island detection:
  islands: 2
  island 1: buses=2, branches=1, ref=1, status=has_ref
  island 2: buses=2, branches=1, ref=4, status=has_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
sending side A1 -> A2 carries 95.21 MW (40 MW local load + 54.273 MW export + line loss)
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :  25-Aug-26 16:19:23
Iterations     :         9
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.001320 s
Case           :tour_b2b_mirrored
Cooldown iters :         0
Q-hysteresis   :    0.0000 pu
Jacobian cond. : kappa1(J) = 1.42, attainable accuracy ~ 3.2e-16, well conditioned (tol 1.0e-8 reachable)
BaseMVA        :       100
Nodes          :         4 (PV: 0 PQ: 2 Slack: 2)
Grid connection: slack bus A1; slack bus C2
Branches       :         2
Links          :         0
HVDC links     :         1
Lines          :         2
Trafos         :         0
Generators     :         3
Loads          :         2
Shunts         :         0
Controllers    :         0 (Tap: 0, Q(U): 0, P(U): 0)
PV→PQ locks    :         0
PV→PQ events   :         0

total network power balance (Σ S_branch): P =      1.210 [MW], Q =      9.680 [MVar]
converter losses (HVDC): P =     54.273 [MW]

==========================================================================================================================================================================================================================
| Nr    | Bus                  | Vn [kV]    | V [kV]     | V [pu]     | phi [deg]  | Pg [MW]    | Qg [MVar]  | Pl [MW]    | Ql [MVar]  | Ps [MW]    | Qs [MVar]  | Type       | Control      | Tap Vm tgt   |
==========================================================================================================================================================================================================================
| 1     | A1                   | 380.0      | 380.000    | 1.000      | 0.000      |     95.210 |     17.497 |            |            |            |            | SLACK      | -            |              |
| 2     | A2                   | 380.0      | 372.139    | 0.979      | -4.358     |    -54.273 |            |     40.000 |     10.000 |            |            | PQ         | -            |              |
| 3     | C1                   | 380.0      | 374.069    | 0.984      | -2.259     |            |            |     50.000 |     12.000 |            |            | PQ         | -            |              |
| 4     | C2                   | 380.0      | 380.000    | 1.000      | 0.000      |     50.273 |     14.183 |            |            |            |            | SLACK      | -            |              |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

==========================================================================================================================================================================================================================
| Branch                    | Type   | Connection                | P [MW]     | Q [MVar]   | P [MW]     | Q [MVar]   | Pv [MW]    | Qv [MVar]  | Ctrl       | P_tgt      | TapPos    | Ctrl status            |
==========================================================================================================================================================================================================================
| B_ACL_380_1_2             | Line   | A1 -> A2                  | 95.210     | 17.497     | -94.273    | -10.000    | 0.937      | 7.497      | -          | -          | -         | -                      |
| B_ACL_380_4_3             | Line   | C2 -> C1                  | 50.273     | 14.183     | -50.000    | -12.000    | 0.273      | 2.183      | -          | -          | -         | -                      |
| HVDC_A2_C2                | Link   | A2 -> C2                  | 54.273     | 0.000      | -0.000     | 0.000      | 54.273     | 0.000      | -          | -          | -         | HVDC, not a branch     |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


--------------------------------------------------------------------------- HVDC Link Flows ---------------------------------------------------------------------------
| Nr    | Name           | From         | To           | Mode        | P_from [MW] | P_to [MW] | Loss [MW] | Q_from [MVar] | Q_to [MVar] | Rating  | Status           |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 1     | HVDC_A2_C2     | A2           | C2           | fixed       | 54.273      | 0.000     | 54.273    | 0.000         | 0.000       | -       | -                |

Control
-------
Transformer controls: none

Reading aid (Example 2.4): compare the SLACK rows with the setpoint variant (Examples 2.1 and 2.2). The references are now A1 and C2: the receiving converter itself is island C's reference, its Pg is the island draw, and C1 carries only load.

Example 2.5: the pairing refusal. And the refusal from above, demonstrated on the net of Example 2.4: pairing the grid-forming converter is rejected, because its injection is the island balance, not a setpoint:

try
  addHvdcPairControl!(net8; from_bus = "A2", to_bus = "C2", p_transfer_mw = 50.0)
catch err
  println(sprint(showerror, err))
end
HvdcPairControl: bus C2 is the reference (slack) of its island; its injection balances the island and cannot follow a transfer setpoint. For a grid-forming receiving converter use mode = :island_feed instead.

Example 2.6: island_feed mode. The pairing controller automates exactly this mirror on the corridor of Example 2.3: mode = :island_feed reads the island balance after each solve and keeps the sending side matched, with an honest at_limit once the island draw exceeds p_rating_mw. No transfer setpoint is given, the island decides:

net9 = build_b2b_source("tour_b2b_grid_forming")
addHvdcPairControl!(net9; from_bus = "A2", to_bus = "C2", mode = :island_feed, loss_mw = 4.0, p_rating_mw = 150.0)
result9 = run_control!(net9; controllers = collect_outer_controllers(net9), pf_config = PowerFlowConfig(method = :rectangular, max_iter = 25, tol = 1e-8), control_config = ControlConfig(max_outer_iterations = 8, trace = false))
calcNetLosses!(net9)
printACPFlowResults(net9, etime, result9.last_pf_iterations, 1e-8)
AC island detection:
  islands: 2
  island 1: buses=2, branches=1, ref=1, status=has_ref
  island 2: buses=2, branches=1, ref=4, status=has_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
AC island detection:
  islands: 2
  island 1: buses=2, branches=1, ref=1, status=has_ref
  island 2: buses=2, branches=1, ref=4, status=has_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :  25-Aug-26 16:19:23
Iterations     :         5
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.001417 s
Case           :tour_b2b_grid_forming
Cooldown iters :         0
Q-hysteresis   :    0.0000 pu
Jacobian cond. : kappa1(J) = 1.42, attainable accuracy ~ 3.2e-16, well conditioned (tol 1.0e-8 reachable)
BaseMVA        :       100
Nodes          :         4 (PV: 0 PQ: 2 Slack: 2)
Grid connection: slack bus A1; slack bus C2
Branches       :         2
Links          :         0
HVDC links     :         1
Lines          :         2
Trafos         :         0
Generators     :         3
Loads          :         2
Shunts         :         0
Controllers    :         1 (Tap: 0, Q(U): 0, P(U): 0, HVDC: 1)
PV→PQ locks    :         0
PV→PQ events   :         0

total network power balance (Σ S_branch): P =      1.210 [MW], Q =      9.680 [MVar]
converter losses (HVDC): P =      4.000 [MW]

==========================================================================================================================================================================================================================
| Nr    | Bus                  | Vn [kV]    | V [kV]     | V [pu]     | phi [deg]  | Pg [MW]    | Qg [MVar]  | Pl [MW]    | Ql [MVar]  | Ps [MW]    | Qs [MVar]  | Type       | Control      | Tap Vm tgt   |
==========================================================================================================================================================================================================================
| 1     | A1                   | 380.0      | 380.000    | 1.000      | 0.000      |     95.210 |     17.497 |            |            |            |            | SLACK      | -            |              |
| 2     | A2                   | 380.0      | 372.139    | 0.979      | -4.358     |    -54.273 |            |     40.000 |     10.000 |            |            | PQ         | B2B          |              |
| 3     | C1                   | 380.0      | 374.069    | 0.984      | -2.259     |            |            |     50.000 |     12.000 |            |            | PQ         | -            |              |
| 4     | C2                   | 380.0      | 380.000    | 1.000      | 0.000      |     50.273 |     14.183 |            |            |            |            | SLACK      | B2B src      |              |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

==========================================================================================================================================================================================================================
| Branch                    | Type   | Connection                | P [MW]     | Q [MVar]   | P [MW]     | Q [MVar]   | Pv [MW]    | Qv [MVar]  | Ctrl       | P_tgt      | TapPos    | Ctrl status            |
==========================================================================================================================================================================================================================
| B_ACL_380_1_2             | Line   | A1 -> A2                  | 95.210     | 17.497     | -94.273    | -10.000    | 0.937      | 7.497      | -          | -          | -         | -                      |
| B_ACL_380_4_3             | Line   | C2 -> C1                  | 50.273     | 14.183     | -50.000    | -12.000    | 0.273      | 2.183      | -          | -          | -         | -                      |
| B2B_A2_C2                 | Link   | A2 -> C2                  | 54.273     | 0.000      | -50.273    | 0.000      | 4.000      | 0.000      | island_fe… | 54.273     | -         | HVDC, not a branch     |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


--------------------------------------------------------------------------- HVDC Link Flows ---------------------------------------------------------------------------
| Nr    | Name           | From         | To           | Mode        | P_from [MW] | P_to [MW] | Loss [MW] | Q_from [MVar] | Q_to [MVar] | Rating  | Status           |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 1     | B2B_A2_C2      | A2           | C2           | island_feed | 54.273      | 50.273    | 4.000     | 0.000         | 0.000       | 150.0   | converged        |

Control
-------
Transformer controls: none

HVDC Pair Control Summary (back-to-back)
----------------------------------------
B2B_A2_C2 B2B (link A2 -> C2)
  mode               : island_feed (grid-forming to side)
  transfer           : 54.273 MW (mirrored from island draw) (rating 150.0 MW)
  loss               : 4.000 MW
  from injection     : -54.273 MW / 0.000 MVAr
  to injection       : 50.273 MW / 0.000 MVAr (island balance outcome)
  converged          : true
  at_limit           : false
  status             : converged

Reading aid (Example 2.6), and the direct comparison with the setpoint variant of Example 2.2. Both versions report Slack: 2 in the header (one reference per island, always), but WHERE the references sit is the whole difference:

  • Setpoint variant (Example 2.2): references at A1 and C1, each island brings its own source. Both converters are plain PQ injections steered by the controller (-120 / +116 MW in the bus table), and the transfer is an order the link follows. Look at C1's Pg: it is negative, island C's own slack absorbs the surplus the link pumps in.
  • Grid-forming variant (this table, Example 2.6): references at A1 and C2. The receiving converter itself is island C's SLACK row, marked B2B src in the Control column; its Pg column is the island balance outcome (load plus line loss, no setpoint anywhere), C1 carries only load, and the HVDC block in the Control section shows the transfer as "mirrored from island draw". The HVDC Link Flows table lists the same link with mode = island_feed and the mirrored transfer in its P_from column.

Try p_rating_mw = 40.0: the sending side pins at the rating with at_limit = true and converged = false. The island's reference still balances in the model (a power flow cannot show the collapse), so the honest flag is what marks the undeliverable draw.

Variant: grid-forming with droop (SOURCE model)

Example 2.7: grid-forming with droop. The ideal reference of Example 2.3 holds exactly 1.0 pu at the PCC no matter what the island draws. A real VSC has finite control stiffness. The external-grid element from the basic tour's chapter 3 (Example 3.2 there) models exactly that: the reference voltage sits BEHIND the impedance $Z_Q = U_n^2 / S_k''$, so the PCC voltage droops under load. Declaring the converter that way (same corridor as Example 2.3, the receiving converter now an external-grid feeder) finally makes the bus table say SOURCE, and the header counts Slack: 1 Source: 1:

function build_b2b_droop(name::String; sending_mw::Float64 = 0.0, sk_mva::Float64 = 800.0)
  net = Net(name = name, baseMVA = 100.0)
  for b in ("A1", "A2", "C1", "C2")
    addBus!(net = net, busName = b, vn_kV = 380.0)
  end
  addPIModelACLine!(net = net, fromBus = "A1", toBus = "A2", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "C2", toBus = "C1", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
  addProsumer!(net = net, busName = "A1", type = "EXTERNALNETWORKINJECTION", referencePri = "A1", vm_pu = 1.0, va_deg = 0.0)
  addProsumer!(net = net, busName = "A2", type = "ENERGYCONSUMER", p = 40.0, q = 10.0)
  addProsumer!(net = net, busName = "C1", type = "ENERGYCONSUMER", p = 50.0, q = 12.0)
  # the grid-forming converter as a non-ideal source: reference voltage
  # behind Z_Q, declared by its short-circuit power like a real feeder
  addExternalGrid!(net = net, busName = "C2", vm_pu = 1.0, sk_max_MVA = sk_mva, sk_min_MVA = sk_mva, rx_max = 0.1, internal_impedance = true)
  addProsumer!(net = net, busName = "A2", type = "GENERATOR", p = sending_mw, q = 0.0)
  return net
end

net10 = build_b2b_droop("tour_b2b_droop"; sending_mw = -(p_island_c + 4.0))
etime10, ite10 = solve!(net10; islands_enabled = true)
printACPFlowResults(net10, etime10, ite10, 1e-8)
AC island detection:
  islands: 2
  island 1: buses=2, branches=1, ref=1, status=has_ref
  island 2: buses=3, branches=2, ref=5, status=has_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :  25-Aug-26 16:19:23
Iterations     :        10
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.001190 s
Case           : tour_b2b_droop
Cooldown iters :         0
Q-hysteresis   :    0.0000 pu
Jacobian cond. : kappa1(J) = 16.4, attainable accuracy ~ 3.6e-15, well conditioned (tol 1.0e-8 reachable)
BaseMVA        :       100
Nodes          :         5 (PV: 0 PQ: 3 Slack: 1 Source: 1)
Grid connection: slack bus A1; external-grid source at C2 (Sk'' = 800.0 MVA, R/X = 0.1; internal slack: C2__extgrid_int)
Branches       :         3
Links          :         0
HVDC links     :         0
Lines          :         3
Trafos         :         0
Generators     :         3
Loads          :         2
Shunts         :         0
Controllers    :         0 (Tap: 0, Q(U): 0, P(U): 0)
PV→PQ locks    :         0
PV→PQ events   :         0

total network power balance (Σ S_branch): P =      1.585 [MW], Q =     13.394 [MVar]

==========================================================================================================================================================================================================================
| Nr    | Bus                  | Vn [kV]    | V [kV]     | V [pu]     | phi [deg]  | Pg [MW]    | Qg [MVar]  | Pl [MW]    | Ql [MVar]  | Ps [MW]    | Qs [MVar]  | Type       | Control      | Tap Vm tgt   |
==========================================================================================================================================================================================================================
| 1     | A1                   | 380.0      | 380.000    | 1.000      | 0.000      |     95.210 |     17.497 |            |            |            |            | SLACK      | -            |              |
| 2     | A2                   | 380.0      | 372.139    | 0.979      | -4.358     |    -54.273 |            |     40.000 |     10.000 |            |            | PQ         | -            |              |
| 3     | C1                   | 380.0      | 363.752    | 0.957      | -5.966     |            |            |     50.000 |     12.000 |            |            | PQ         | -            |              |
| 4     | C2                   | 380.0      | 369.869    | 0.973      | -3.580     |            |            |            |            |            |            | PQ         | -            |              |
| 5     | C2__extgrid_int      | 380.0      | 380.000    | 1.000      | 0.000      |     50.647 |     17.897 |            |            |            |            | SOURCE     | -            |              |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

==========================================================================================================================================================================================================================
| Branch                    | Type   | Connection                | P [MW]     | Q [MVar]   | P [MW]     | Q [MVar]   | Pv [MW]    | Qv [MVar]  | Ctrl       | P_tgt      | TapPos    | Ctrl status            |
==========================================================================================================================================================================================================================
| B_ACL_380_1_2             | Line   | A1 -> A2                  | 95.210     | 17.497     | -94.273    | -10.000    | 0.937      | 7.497      | -          | -          | -         | -                      |
| B_ACL_380_4_3             | Line   | C2 -> C1                  | 50.289     | 14.308     | -50.000    | -12.000    | 0.289      | 2.308      | -          | -          | -         | -                      |
| B_ACL_380_5_4             | Line   | C2__extgrid_int -> C2     | 50.647     | 17.897     | -50.289    | -14.308    | 0.359      | 3.589      | -          | -          | -         | -                      |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Control
-------
Transformer controls: none

Reading aid (Example 2.7): the header reads Slack: 1 Source: 1, the grid connection line names the source with its feeder data, and the hidden anchor bus behind the impedance is the SOURCE row. The PCC bus C2 is now a plain PQ bus whose voltage sags below 1.0 pu under the island load: that sag is the droop, and its size follows from the declared sk_max_MVA (stiffer converter = higher $S_k''$ = less droop). Pairing this source-model reference with the island_feed controller is a possible follow-up of the pairing controller.

Meshed operation: the two areas get an AC tie

So far the link was the ONLY connection. Now close an AC branch between A1 and C1: the two areas become one synchronous island, and one synchronous island carries exactly ONE angle reference. The link transfers power, the tie transfers the angle.

     one synchronous island
  A1 -------- A2  ===== DC link =====  C2 -------- C1
  |                                                 |
  +------------------- AC tie ---------------------+
function build_meshed(name::String; c1_model::Symbol)
  net = Net(name = name, baseMVA = 100.0)
  for b in ("A1", "A2", "C1", "C2")
    addBus!(net = net, busName = b, vn_kV = 380.0)
  end
  addPIModelACLine!(net = net, fromBus = "A1", toBus = "A2", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "C1", toBus = "C2", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = net, fromBus = "A1", toBus = "C1", r_pu = 0.02, x_pu = 0.16, b_pu = 0.0, status = 1)
  addProsumer!(net = net, busName = "A1", type = "EXTERNALNETWORKINJECTION", referencePri = "A1", vm_pu = 1.0, va_deg = 0.0)
  if c1_model === :reference
    # scene 1: island C keeps its old reference although the tie closed
    addProsumer!(net = net, busName = "C1", type = "EXTERNALNETWORKINJECTION", referencePri = "C1", vm_pu = 1.0, va_deg = 0.0)
  else
    # scenes 2+: demoted to a voltage-regulated generator (PV)
    addProsumer!(net = net, busName = "C1", type = "GENERATOR", p = 20.0, q = 0.0, vm_pu = 1.0, isRegulated = true)
  end
  addProsumer!(net = net, busName = "A2", type = "ENERGYCONSUMER", p = 40.0, q = 10.0)
  addProsumer!(net = net, busName = "C2", type = "ENERGYCONSUMER", p = 50.0, q = 12.0)
  addProsumer!(net = net, busName = "A2", type = "GENERATOR", p = -80.0, q = 0.0)  ## converter, exports
  addProsumer!(net = net, busName = "C2", type = "GENERATOR", p = 76.0, q = 0.0)   ## converter, receives
  return net
end
build_meshed (generic function with 1 method)

Example 2.8: two references in one island. Keeping BOTH old references fails fast, with the buses named. The solver never demotes a reference on its own; you decide which one survives:

netm = build_meshed("tour_meshed_two_refs"; c1_model = :reference)
try
  solve!(netm; islands_enabled = true)
catch err
  println(sprint(showerror, err))
end
AC island 1 has 2 angle references (A1, C1). One synchronous island can carry exactly one reference. Demote the others to PV (an ExternalNetworkInjection without referencePri, or a voltage-regulated generator), or remove the AC tie if the areas are meant to be asynchronous. For an HVDC link in mode = :island_feed the grid-forming converter cannot stay the reference once its island is synchronously tied.

Reading aid (Example 2.8): this is the same one-reference-per-island rule from the beginning of the chapter, now seen from the other side. Example 2.9: the meshed pair. Demote C1 to a voltage-regulated generator and the meshed net solves (same topology, diagram above); the pair keeps its setpoint and the tie carries the balance:

netm2 = build_meshed("tour_meshed"; c1_model = :pv)
addHvdcPairControl!(netm2; from_bus = "A2", to_bus = "C2", p_transfer_mw = 120.0, loss_mw = 4.0, p_rating_mw = 150.0)
resultm = run_control!(netm2; controllers = collect_outer_controllers(netm2), pf_config = PowerFlowConfig(method = :rectangular, max_iter = 25, tol = 1e-8), control_config = ControlConfig(max_outer_iterations = 8, trace = false))
calcNetLosses!(netm2)
printACPFlowResults(netm2, etime, resultm.last_pf_iterations, 1e-8)
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :  25-Aug-26 16:19:23
Iterations     :         4
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.001417 s
Case           :    tour_meshed
Cooldown iters :         0
Q-hysteresis   :    0.0000 pu
Jacobian cond. : kappa1(J) = 75.6, attainable accuracy ~ 1.7e-14, well conditioned (tol 1.0e-8 reachable)
BaseMVA        :       100
Nodes          :         4 (PV: 1 PQ: 2 Slack: 1)
Grid connection: slack bus A1
Branches       :         3
Links          :         0
HVDC links     :         1
Lines          :         3
Trafos         :         0
Generators     :         4
Loads          :         2
Shunts         :         0
Controllers    :         1 (Tap: 0, Q(U): 0, P(U): 0, HVDC: 1)
PV→PQ locks    :         0
PV→PQ events   :         0

total network power balance (Σ S_branch): P =      4.673 [MW], Q =     37.387 [MVar]
converter losses (HVDC): P =      4.000 [MW]

==========================================================================================================================================================================================================================
| Nr    | Bus                  | Vn [kV]    | V [kV]     | V [pu]     | phi [deg]  | Pg [MW]    | Qg [MVar]  | Pl [MW]    | Ql [MVar]  | Ps [MW]    | Qs [MVar]  | Type       | Control      | Tap Vm tgt   |
==========================================================================================================================================================================================================================
| 1     | A1                   | 380.0      | 380.000    | 1.000      | 0.000      |     78.673 |     48.484 |            |            |            |            | SLACK      | -            |              |
| 2     | A2                   | 380.0      | 367.269    | 0.966      | -7.551     |   -120.000 |            |     40.000 |     10.000 |            |            | PQ         | B2B          |              |
| 3     | C1                   | 380.0      | 380.000    | 1.000      | 7.922      |     20.000 |     10.903 |            |            |            |            | PV         | -            |              |
| 4     | C2                   | 380.0      | 378.295    | 0.996      | 11.031     |    116.000 |            |     50.000 |     12.000 |            |            | PQ         | B2B          |              |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

==========================================================================================================================================================================================================================
| Branch                    | Type   | Connection                | P [MW]     | Q [MVar]   | P [MW]     | Q [MVar]   | Pv [MW]    | Qv [MVar]  | Ctrl       | P_tgt      | TapPos    | Ctrl status            |
==========================================================================================================================================================================================================================
| B_ACL_380_1_2             | Line   | A1 -> A2                  | 162.751    | 32.010     | -160.000   | -10.000    | 2.751      | 22.010     | -          | -          | -         | -                      |
| B_ACL_380_3_4             | Line   | C1 -> C2                  | -65.546    | 15.633     | 66.000     | -12.000    | 0.454      | 3.633      | -          | -          | -         | -                      |
| B_ACL_380_1_3             | Line   | A1 -> C1                  | -84.078    | 16.474     | 85.546     | -4.729     | 1.468      | 11.745     | -          | -          | -         | -                      |
| B2B_A2_C2                 | Link   | A2 -> C2                  | 120.000    | 0.000      | -116.000   | 0.000      | 4.000      | 0.000      | setpoint   | 120.000    | -         | HVDC, not a branch     |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


--------------------------------------------------------------------------- HVDC Link Flows ---------------------------------------------------------------------------
| Nr    | Name           | From         | To           | Mode        | P_from [MW] | P_to [MW] | Loss [MW] | Q_from [MVar] | Q_to [MVar] | Rating  | Status           |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 1     | B2B_A2_C2      | A2           | C2           | setpoint    | 120.000     | 116.000   | 4.000     | 0.000         | 0.000       | 150.0   | converged        |

Control
-------
Transformer controls: none

HVDC Pair Control Summary (back-to-back)
----------------------------------------
B2B_A2_C2 B2B (link A2 -> C2)
  transfer           : 120.000 MW (rating 150.0 MW)
  loss               : 4.000 MW
  from injection     : -120.000 MW / 0.000 MVAr
  to injection       : 116.000 MW / 0.000 MVAr
  converged          : true
  at_limit           : false
  status             : converged

Reading aid (Example 2.9): ONE island, ONE SLACK row (A1), C1 is an ordinary PV generator now. The branch table shows the parallel paths side by side: three real AC branches plus the Link row marked "HVDC, not a branch". The HVDC Link Flows table still shows the ordered 120 MW with 4 MW loss, and the tie A1 -> C1 carries whatever the link over- or under-delivers relative to what area C draws. Retarget the pair and watch the exchange move between link and tie:

ctrlm = only(collect_outer_controllers(netm2))
for target in (120.0, 40.0)
  ctrlm.p_transfer_mw = target
  ctrlm.p_applied = false
  run_control!(netm2; controllers = collect_outer_controllers(netm2), pf_config = PowerFlowConfig(method = :rectangular, max_iter = 25, tol = 1e-8), control_config = ControlConfig(max_outer_iterations = 8, trace = false))
  calcNetLosses!(netm2)
  println("transfer ", target, " MW ordered: AC tie A1 -> C1 carries ", round(get_branch_p_from_to_mw(netm2, "A1", "C1"); digits = 3), " MW")
end
transfer 120.0 MW ordered: AC tie A1 -> C1 carries -84.078 MW
transfer 40.0 MW ordered: AC tie A1 -> C1 carries -5.958 MW

Reading aid (Example 2.9): the pair keeps its order exactly (that is what a setpoint means), so every retarget shows up one-to-one in the tie flow. And mode = :island_feed? A grid-forming converter inside a synchronous grid is a different device model and out of scope: with the tie closed the registration is rejected by the same one-reference rule, and a demoted reference afterwards makes the controller report invalid_topology instead of silently changing modes.

Chapter 3: state estimation

Example 3.1: WLS estimation on the ring. Close the loop: solve a reference power flow on the 7-bus ring of the basic tour's Example 1.1 (build_ring7, see the warm-up; diagram in the basic tour), derive a noisy synthetic measurement set from it, check observability, and let the WLS estimator reconstruct the state. The full narrative is the state-estimation notebook.

net_se = build_ring7("tour_se")
ite_pf, status_pf = runpf!(net_se, 40, 1e-10, 0)
status_pf == 0 || error("Power flow did not converge")

std = measurementStdDevs(vm = 1e-3, pinj = 1.0, qinj = 1.0, pflow = 0.7, qflow = 0.7)
setMeasurementsFromPF!(
  net_se;
  includeVm = true,
  includePinj = true,
  includeQinj = true,
  includePflow = true,
  includeQflow = true,
  noise = true,
  stddev = std,
  rng = MersenneTwister(42),
)

gobs = evaluate_global_observability(net_se; flatstart = true, jacEps = 1e-6)
println("observability: ", gobs.quality, " (", gobs.n_measurements, " measurements, ", gobs.n_states, " states)")

se = runse!(net_se; maxIte = 12, tol = 1e-6, flatstart = true, jacEps = 1e-6, updateNet = true)
println("SE converged: ", se.converged, " in ", se.iterations, " iterations")
println("objective J:  ", round(se.objectiveJ; digits = 2), " (dof ", se.dof, ", within 3σ: ", se.jWithin3Sigma, ")")
for (name, idx) in sort(collect(net_se.busDict); by = last)
  v = se.voltages[idx]
  println(rpad(name, 4), "  Vm = ", round(abs(v); digits = 4), " pu   Va = ", round(rad2deg(angle(v)); digits = 3), "°")
end
observability: good (57 measurements, 13 states)
SE converged: true in 3 iterations
objective J:  47.94 (dof 44, within 3σ: true)
B1    Vm = 1.0208 pu   Va = 0.0°
B2    Vm = 0.9947 pu   Va = -2.305°
B3    Vm = 0.9925 pu   Va = -1.983°
B4    Vm = 0.9791 pu   Va = -3.74°
B5    Vm = 0.9849 pu   Va = -3.28°
B6    Vm = 0.9889 pu   Va = -2.793°
B7    Vm = 1.0019 pu   Va = -1.766°

Reading aid (Example 3.1): with mild noise the estimate reproduces the reference state to a few 1e-3 pu, and $J$ lands near the degrees of freedom, the textbook health check for a WLS estimator.

Chapter 4: FACTS devices and their limits

FACTS devices use power electronics to control voltage and flow. The workshop has met two members already: the phase-shifting tap (basic tour, chapter 4's family) and the HVDC pair (chapter 2). This chapter is about the LIMIT characteristics, because that is where the devices actually differ. In range, every shunt compensator holds its voltage target the same way; at the limit:

  • a classical machine keeps its constant reactive box [Qmin, Qmax],
  • a STATCOM is current-limited, it delivers $Q = V \cdot S_{max}$ (LINEAR collapse under a sag),
  • an SVC is susceptance-limited, it delivers $Q = V^2 \cdot B$ (QUADRATIC collapse).

The ranking shows exactly under the depressed voltage the compensator was installed for. Example 4.1: three shunt devices at the same rating. We build one weak corridor (build_sag_corridor, reused by Examples 4.2 and 4.4) that sags to about 0.92 pu and give all three devices the SAME 10-MVAr rating at 1.0 pu:

 (slack, 1.0 pu)
   Slack -------- Mid -------- Load
             compensator    60 MW / 25 MVAr
             under test
facts_rating = 10.0
function build_sag_corridor(name::String; with_machine::Bool)
  cnet = Net(name = name, baseMVA = 100.0)
  for bus in ("Slack", "Mid", "Load")
    addBus!(net = cnet, busName = bus, vn_kV = 110.0)
  end
  addProsumer!(net = cnet, busName = "Slack", type = "EXTERNALNETWORKINJECTION", vm_pu = 1.0, va_deg = 0.0, referencePri = "Slack")
  addProsumer!(net = cnet, busName = "Load", type = "LOAD", p = 60.0, q = 25.0)
  with_machine && addProsumer!(net = cnet, busName = "Mid", type = "GENERATOR", p = 0.0, q = 0.0)
  addPIModelACLine!(net = cnet, fromBus = "Slack", toBus = "Mid", r_pu = 0.02, x_pu = 0.20, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = cnet, fromBus = "Mid", toBus = "Load", r_pu = 0.02, x_pu = 0.20, b_pu = 0.0, status = 1)
  ok, msg = validate!(net = cnet)
  ok || error("corridor net invalid: $msg")
  return cnet
end

# classical machine: constant reactive box, the outer loop parks at_limit
box_net = build_sag_corridor("tour_facts_box"; with_machine = true)
addMachineVoltageControl!(box_net; bus = "Mid", target_bus = "Load", target_vm_pu = 1.0, qmin_mvar = -facts_rating, qmax_mvar = facts_rating)
run_control!(box_net)
box_ctrl = only([c for c in box_net.machineControls if c isa MachineVoltageControl])

# STATCOM: the SAME controller with a converter rating instead of the box;
# the bound Q_lim = V * S_max is refreshed from the solved terminal
# voltage every outer iteration, so the delivered Q tracks the sag
st_net = build_sag_corridor("tour_facts_statcom"; with_machine = true)
addMachineVoltageControl!(st_net; bus = "Mid", target_bus = "Load", target_vm_pu = 1.0, s_max_mva = facts_rating)
run_control!(st_net)
st_ctrl = only([c for c in st_net.machineControls if c isa MachineVoltageControl])
v_st = get_bus_vm_pu(st_net, "Mid")

# SVC: continuous susceptance; at the clamp the Y-bus stamp makes the
# delivered Q follow V^2 all by itself
svc_net = build_sag_corridor("tour_facts_svc"; with_machine = false)
addShuntVoltageControl!(svc_net; bus = "Mid", target_vm_pu = 1.0, bs_min_mvar = -facts_rating, bs_max_mvar = facts_rating)
run_control!(svc_net)
svc_ctrl = only([c for c in svc_net.machineControls if c isa ShuntVoltageControl])
v_svc = get_bus_vm_pu(svc_net, "Mid")

println("all three at their capacitive limit, rated ", facts_rating, " MVAr at 1.0 pu:")
println("  machine box : Q = ", round(box_ctrl.q_mvar; digits = 2), " MVAr (constant)")
println("  STATCOM     : Q = ", round(st_ctrl.q_mvar; digits = 2), " MVAr = V*S_max at V = ", round(v_st; digits = 4), " pu (", round(100 * st_ctrl.q_mvar / facts_rating; digits = 1), " % of rating)")
println("  SVC         : Q = ", round(v_svc^2 * svc_ctrl.bs_mvar; digits = 2), " MVAr = V^2*B at V = ", round(v_svc; digits = 4), " pu (", round(100 * v_svc^2 * svc_ctrl.bs_mvar / facts_rating; digits = 1), " % of rating)")
all three at their capacitive limit, rated 10.0 MVAr at 1.0 pu:
  machine box : Q = 10.0 MVAr (constant)
  STATCOM     : Q = 9.17 MVAr = V*S_max at V = 0.9174 pu (91.7 % of rating)
  SVC         : Q = 8.38 MVAr = V^2*B at V = 0.9154 pu (83.8 % of rating)

Most voltage control in real grids is not an SVC but a mechanically SWITCHED bank (MSC/MSR): whole capacitor blocks go in or out, nothing in between. Example 4.2: a switched bank vs a continuous SVC. The same controller models the bank with step_mvar (issue #324), here on the sag corridor of Example 4.1. Two design rules make a switched bank usable in a solver loop: it moves in WHOLE blocks truncated toward the target (approaching from one side, never overshooting, so it cannot hunt between two adjacent steps), and when no whole block improves the voltage further it PARKS on the reached step, deliberately the last one BEFORE crossing the target (under-compensation instead of a possible overvoltage):

# the same corridor and target once with a continuous SVC, once as a bank
cont_net = build_sag_corridor("tour_facts_cont"; with_machine = false)
addShuntVoltageControl!(cont_net; bus = "Mid", target_vm_pu = 0.95, bs_min_mvar = -40.0, bs_max_mvar = 40.0)
run_control!(cont_net)
cont_ctrl = only([c for c in cont_net.machineControls if c isa ShuntVoltageControl])
println("continuous SVC: Bs = ", round(cont_ctrl.bs_mvar; digits = 2), " MVAr, Vm(Mid) = ", round(get_bus_vm_pu(cont_net, "Mid"); digits = 4), " pu, status = ", cont_ctrl.status)

msc_net = build_sag_corridor("tour_facts_msc"; with_machine = false)
addShuntVoltageControl!(msc_net; bus = "Mid", target_vm_pu = 0.95, bs_min_mvar = -40.0, bs_max_mvar = 40.0, step_mvar = 10.0)
msc_res = run_control!(msc_net)
msc_ctrl = only([c for c in msc_net.machineControls if c isa ShuntVoltageControl])
println("MSC bank (4 x 10 MVAr blocks): Bs = ", msc_ctrl.bs_mvar, " MVAr (block ", round(Int, msc_ctrl.bs_mvar / 10.0), " of 4), Vm(Mid) = ", round(get_bus_vm_pu(msc_net, "Mid"); digits = 4), " pu")
println("  status = ", msc_ctrl.status, " after ", msc_res.outer_iterations, " outer iterations (parked on the step grid, no hunting)")
continuous SVC: Bs = 24.71 MVAr, Vm(Mid) = 0.95 pu, status = converged
MSC bank (4 x 10 MVAr blocks): Bs = 20.0 MVAr (block 2 of 4), Vm(Mid) = 0.9387 pu
  status = parked after 3 outer iterations (parked on the step grid, no hunting)

Reading aid (Example 4.2): the continuous SVC settles wherever the secant sends it (about 24.7 MVAr here) and converges; the bank can only offer 20 or 30 and stops at 20, the last block before crossing the 0.95-pu target. status = :parked is the bank's third state next to converged and at_limit: the step resolution, not the rating, ended the movement.

The series side has the same split. A TCSC owns a FIXED reactance window and keeps it at any loading; an SSSC injects a series voltage, so its usable window $|x - x_{base}| \le V_{inj,max}/|I|$ SHRINKS with the branch current: it saturates exactly at high transfer. Example 4.3: TCSC vs SSSC on a two-corridor loop. Same loop (build_facts_loop), same 35-MW target, once per device:

 (slack)
   A ------ M1 ------ B     load 80 MW / 20 MVAr at B
   |                  |
   +------ M2 --------+     series device in the A-M2 leg
function build_facts_loop(name::String)
  lnet = Net(name = name, baseMVA = 100.0)
  for bus in ("A", "M1", "M2", "B")
    addBus!(net = lnet, busName = bus, vn_kV = 110.0)
  end
  addProsumer!(net = lnet, busName = "A", type = "EXTERNALNETWORKINJECTION", referencePri = "A", vm_pu = 1.0, va_deg = 0.0)
  addProsumer!(net = lnet, busName = "B", type = "ENERGYCONSUMER", p = 80.0, q = 20.0)
  addPIModelACLine!(net = lnet, fromBus = "A", toBus = "M1", r_pu = 0.01, x_pu = 0.10, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = lnet, fromBus = "M1", toBus = "B", r_pu = 0.01, x_pu = 0.10, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = lnet, fromBus = "A", toBus = "M2", r_pu = 0.02, x_pu = 0.20, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = lnet, fromBus = "M2", toBus = "B", r_pu = 0.02, x_pu = 0.20, b_pu = 0.0, status = 1)
  ok, msg = validate!(net = lnet)
  ok || error("loop net invalid: $msg")
  return lnet
end

tcsc_net = build_facts_loop("tour_facts_tcsc")
tcsc_ctrl = addSeriesReactanceControl!(tcsc_net; fromBus = "A", toBus = "M2", p_target_mw = 35.0, x_min_pu = 0.02, x_max_pu = 0.30)
run_control!(tcsc_net)
println("TCSC window 0.02..0.30 pu : P = ", round(tcsc_ctrl.achieved_p_mw; digits = 2), " MW at x = ", round(tcsc_ctrl.x_pu; digits = 4), " pu, converged = ", tcsc_ctrl.converged)

sssc_net = build_facts_loop("tour_facts_sssc")
sssc_ctrl = addSeriesReactanceControl!(sssc_net; fromBus = "A", toBus = "M2", p_target_mw = 35.0, v_inj_max_pu = 0.01)
run_control!(sssc_net)
println("SSSC V_inj,max 0.01 pu    : P = ", round(sssc_ctrl.achieved_p_mw; digits = 2), " MW at x = ", round(sssc_ctrl.x_pu; digits = 4), " pu, at_limit = ", sssc_ctrl.at_limit)
println("  live window [", round(sssc_ctrl.x_min_pu; digits = 4), ", ", round(sssc_ctrl.x_max_pu; digits = 4), "] pu around x_base ", sssc_ctrl.x_base_pu, ", injected voltage ", round(abs(sssc_ctrl.x_pu - sssc_ctrl.x_base_pu) * sssc_ctrl.i_pu; digits = 4), " pu of 0.01 available")
TCSC window 0.02..0.30 pu : P = 34.62 MW at x = 0.0709 pu, converged = true
SSSC V_inj,max 0.01 pu    : P = 28.61 MW at x = 0.1671 pu, at_limit = true
  live window [0.1671, 0.2329] pu around x_base 0.2, injected voltage 0.01 pu of 0.01 available

Reading aid (Example 4.3): the TCSC reaches the 35-MW target (x moves from 0.20 down to about 0.07 pu), the SSSC pins at its injectable voltage with the flow stuck near 28.6 MW: its whole window is $\pm 0.033$ pu at this loading. Both devices report the miss honestly as at_limit instead of pretending convergence.

Example 4.4: in range, with the generic element view. IN range, the converter devices are ordinary voltage/flow controllers: with a rating that never binds, the STATCOM settles into the same deadband as the classical constant-Q controller (back on the sag corridor of Example 4.1). All registered devices describe themselves through one generic view, controllableElements: element, device, actuator with its LIVE range (for the STATCOM that is $\pm V \cdot S_{max}$ of the last solved operating point, not the nameplate), target, and status:

ok_net = build_sag_corridor("tour_facts_inrange"; with_machine = true)
addMachineVoltageControl!(ok_net; bus = "Mid", target_bus = "Load", target_vm_pu = 0.95, s_max_mva = 200.0)
run_control!(ok_net)
for e in controllableElements(ok_net)
  println(e.device, " on ", e.element, ": ", e.actuator, " in [", round(e.actuator_min; digits = 2), ", ", round(e.actuator_max; digits = 2), "], status = ", e.status)
end
println("in range: Vm(Load) = ", round(get_bus_vm_pu(ok_net, "Load"); digits = 4), " pu (target 0.95)")
STATCOM (VSC) on machine@Mid: machine_q_mvar in [-204.47, 204.47], status = converged
in range: Vm(Load) = 0.9499 pu (target 0.95)

Example 4.5: the combined device, a UPFC in one call. A UPFC couples an SSSC (series converter, line flow) and a STATCOM (shunt converter, bus voltage) through one DC link. In the stationary model the series injection is in quadrature with the line current and exchanges no active power, so the DC link carries about zero and the pair decouples: addUpfcControl! registers exactly that pair as ONE named device. Back on the loop of Example 4.3 (diagram there), with the shunt converter's machine added at M2:

upfc_net = build_facts_loop("tour_facts_upfc")
addProsumer!(net = upfc_net, busName = "M2", type = "GENERATOR", p = 0.0, q = 0.0)
upfc = addUpfcControl!(upfc_net; fromBus = "A", toBus = "M2", shunt_bus = "M2", target_bus = "B", target_vm_pu = 0.99, p_target_mw = 35.0, v_inj_max_pu = 0.08, s_max_mva = 40.0)
run_control!(upfc_net)
println("UPFC ", upfc.name, ":")
println("  series: P(A->M2) = ", round(upfc.series.achieved_p_mw; digits = 2), " MW toward 35, converged = ", upfc.series.converged)
println("  shunt : Vm(B) = ", round(upfc.shunt.achieved_vm_pu; digits = 4), " pu toward 0.99, Q = ", round(upfc.shunt.q_mvar; digits = 2), " MVAr, at_limit = ", upfc.shunt.at_limit)
for e in controllableElements(upfc_net)
  println("  row: ", e.device, " (", e.actuator, "), at_limit = ", e.at_limit)
end
UPFC UPFC_A_M2:
  series: P(A->M2) = 34.84 MW toward 35, converged = true
  shunt : Vm(B) = 0.9717 pu toward 0.99, Q = 40.31 MVAr, at_limit = true
  row: UPFC shunt (VSC pair, stationary quadrature model) (machine_q_mvar), at_limit = true
  row: UPFC series (VSC pair, stationary quadrature model) (series_x_pu), at_limit = false

Reading aid (Example 4.5): one call, one composite name, but honestly TWO result rows: each converter keeps its own actuator and its own at_limit (here the series side reaches the 35-MW target while the 40-MVA shunt rating cannot lift bus B to 0.99 pu and parks at its limit). What the quadrature model does NOT have is series ACTIVE-power injection, the phase-shifter degree of freedom a real UPFC feeds through its DC link; independent P and Q steering of the line stays out of scope in Example 4.5.

Example 4.6: the FULL UPFC, independent P and Q on one line. Lifting the quadrature restriction lets the series converter inject a voltage of ARBITRARY phase. Its in-phase component (relative to the line current) now carries active power P_se, which flows through the DC link and is balanced by the shunt converter (P_sh = -P_se); that is the phase-shifter degree of freedom, and it lets the line hold INDEPENDENT P and Q targets. The full model needs the shunt at the SENDING bus of its line, so this uses a small meshed corridor (the parallel S->L path lets the flow be steered):

   S (slack) --- I ==[UPFC series]== J --- L (load)
                 |                            S ------------- L
           shunt converter at I               (parallel path)
function build_facts_mesh(name::String)
  m = Net(name = name, baseMVA = 100.0)
  for b in ("S", "I", "J", "L")
    addBus!(net = m, busName = b, vn_kV = 110.0)
  end
  addProsumer!(net = m, busName = "S", type = "EXTERNALNETWORKINJECTION", referencePri = "S", vm_pu = 1.0, va_deg = 0.0)
  addProsumer!(net = m, busName = "I", type = "GENERATOR", p = 0.0, q = 0.0)   # shunt converter
  addProsumer!(net = m, busName = "L", type = "ENERGYCONSUMER", p = 90.0, q = 30.0)
  addPIModelACLine!(net = m, fromBus = "S", toBus = "I", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = m, fromBus = "I", toBus = "J", r_pu = 0.02, x_pu = 0.18, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = m, fromBus = "J", toBus = "L", r_pu = 0.01, x_pu = 0.08, b_pu = 0.0, status = 1)
  addPIModelACLine!(net = m, fromBus = "S", toBus = "L", r_pu = 0.02, x_pu = 0.16, b_pu = 0.0, status = 1)
  ok, msg = validate!(net = m)
  ok || error("mesh net invalid: $msg")
  return m
end

full_net = build_facts_mesh("tour_facts_upfc_full")
full = addUpfcControl!(full_net; model = :full, fromBus = "I", toBus = "J", shunt_bus = "I",
                       p_target_mw = 40.0, q_target_mvar = 10.0, q_shunt_mvar = 0.0,
                       v_inj_max_pu = 0.30, s_max_mva = 120.0,
                       deadband_p_mw = 1e-2, deadband_q_mvar = 1e-2, max_outer_iters = 80)
run_control!(full_net; control_config = ControlConfig(max_outer_iterations = 80))
u = full.upfc
println("FULL UPFC on I->J:")
println("  line P = ", round(u.achieved_p_mw; digits = 2), " MW (target 40), Q = ", round(u.achieved_q_mvar; digits = 2), " MVAr (target 10) -- both, at once")
println("  series V_se = ", round(abs(u.v_se_pu); digits = 4), " pu at ", round(rad2deg(angle(u.v_se_pu)); digits = 1), " deg, P_se = ", round(u.p_se_mw; digits = 3), " MW")
println("  DC-link balance: P_se + P_sh = ", round(u.p_se_mw + u.p_sh_mw; digits = 4), " MW (residual ", round(u.dc_residual_mw; digits = 4), ")")
FULL UPFC on I->J:
  line P = 40.0 MW (target 40), Q = 9.99 MVAr (target 10) -- both, at once
  series V_se = 0.0552 pu at -90.0 deg, P_se = 0.631 MW
  DC-link balance: P_se + P_sh = 0.0 MW (residual 0.0004)

the classical result tables report the UPFC too: the "Controllers" line counts it and the "UPFC Control Summary" block shows its state

calcNetLosses!(full_net)
printACPFlowResults(full_net, 0.0, 1, 1e-8)
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :  25-Aug-26 16:19:25
Iterations     :         1
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.000000 s
Case           :tour_facts_upfc_full
Cooldown iters :         0
Q-hysteresis   :    0.0000 pu
Jacobian cond. : kappa1(J) = 17.5, attainable accuracy ~ 3.9e-15, well conditioned (tol 1.0e-8 reachable)
BaseMVA        :       100
Nodes          :         4 (PV: 0 PQ: 3 Slack: 1)
Grid connection: slack bus S
Branches       :         4
Links          :         0
HVDC links     :         0
Lines          :         4
Trafos         :         0
Generators     :         2
Loads          :         1
Shunts         :         0
Controllers    :         1 (Tap: 0, Q(U): 0, P(U): 0, UPFC: 1)
PV→PQ locks    :         0
PV→PQ events   :         0

total network power balance (Σ S_branch): P =      2.026 [MW], Q =      9.299 [MVar]

==========================================================================================================================================================================================================================
| Nr    | Bus                  | Vn [kV]    | V [kV]     | V [pu]     | phi [deg]  | Pg [MW]    | Qg [MVar]  | Pl [MW]    | Ql [MVar]  | Ps [MW]    | Qs [MVar]  | Type       | Control      | Tap Vm tgt   |
==========================================================================================================================================================================================================================
| 1     | S                    | 110.0      | 110.000    | 1.000      | 0.000      |     92.657 |     39.299 |            |            |            |            | SLACK      | -            |              |
| 2     | I                    | 110.0      | 108.601    | 0.987      | -1.829     |     -0.631 |            |            |            |            |            | PQ         | -            |              |
| 3     | J                    | 110.0      | 105.523    | 0.959      | -2.771     |            |            |            |            |            |            | PQ         | -            |              |
| 4     | L                    | 110.0      | 104.302    | 0.948      | -4.681     |            |            |     90.000 |     30.000 |            |            | PQ         | -            |              |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

==========================================================================================================================================================================================================================
| Branch                    | Type   | Connection                | P [MW]     | Q [MVar]   | P [MW]     | Q [MVar]   | Pv [MW]    | Qv [MVar]  | Ctrl       | P_tgt      | TapPos    | Ctrl status            |
==========================================================================================================================================================================================================================
| B_ACL_110_1_2             | Line   | S -> I                    | 40.813     | 11.428     | -40.633    | -9.991     | 0.180      | 1.437      | -          | -          | -         | -                      |
| B_ACL_110_2_3             | Line   | I -> J                    | 40.002     | 9.991      | -39.023    | -9.067     | 0.979      | 0.924      | UPFC       | 40.000     | -         | converged              |
| B_ACL_110_3_4             | Line   | J -> L                    | 39.023     | 9.067      | -38.849    | -7.672     | 0.174      | 1.395      | -          | -          | -         | -                      |
| B_ACL_110_1_4             | Line   | S -> L                    | 51.844     | 27.871     | -51.151    | -22.328    | 0.693      | 5.543      | -          | -          | -         | -                      |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Control
-------
Transformer controls: none

UPFC Control Summary (full, DC-link coupled)
-------------------------------------------
UPFC_I_J UPFC (line I -> J, shunt at I)
  line P target/achieved : 40.000 / 40.002 MW
  line Q target/achieved : 10.000 / 9.991 MVar
  series voltage V_se    : 0.0552 pu at -90.0 deg
  series/shunt active    : P_se 0.631 MW, P_sh -0.631 MW
  DC-link residual       : 0.0004 MW
  shunt reactive Q_sh    : 0.000 MVar (bound +-120.0)
  injected-voltage limit : 0.3000 pu
  converged / at_limit   : true / false
  status                 : converged

Forcing the series phase back to quadrature collapses onto Example 4.5's behaviour: the in-phase component vanishes, P_se drops to zero.

quad_net = build_facts_mesh("tour_facts_upfc_quad")
quad = addUpfcControl!(quad_net; model = :full, series_phase = :quadrature, fromBus = "I", toBus = "J",
                       shunt_bus = "I", p_target_mw = 40.0, q_target_mvar = 0.0, q_shunt_mvar = 0.0,
                       v_inj_max_pu = 0.30, s_max_mva = 120.0, deadband_p_mw = 1e-2, max_outer_iters = 80)
run_control!(quad_net; control_config = ControlConfig(max_outer_iterations = 80))
println("quadrature-forced: P_se = ", round(quad.upfc.p_se_mw; digits = 4), " MW (zero: no phase-shifter DOF)")
quadrature-forced: P_se = 0.0 MW (zero: no phase-shifter DOF)

Reading aid (Example 4.6): the number that makes independent P and Q possible is P_se, the active power the series converter pushes through the DC link (nonzero here, exactly zero when forced to quadrature). The line holds BOTH its P and its Q target at once, which the quadrature composite of Example 4.5 cannot. First-cut honesty: the shunt runs on a reactive setpoint (closed-loop shunt voltage regulation is a follow-up), and the model converges for feasible, moderate flow targets; the FACTS Devices page states the full limitation list.

The same devices are available declaratively, as named entries under control.controllers in a configuration file (types machine_voltage with s_max_mva, series_reactance with v_inj_max_pu, upfc with model: quadrature or model: full); see the Control Framework page. The full taxonomy (both UPFC models and their limitations) is on the FACTS Devices page; examples/others/exp_facts_limit_modes.jl runs these contrasts as a script, and the TCSC has its own notebook.

Chapter 5: N-1 contingency analysis

Would the network survive the loss of any single branch? An N-1 batch answers that by outaging every branch in turn and re-solving. The API does the bookkeeping that is easy to get wrong: every case works on its own copy of the SOLVED base case (warm start, the base net is never mutated), and failures are REPORTED per case, never thrown.

Example 5.1: an N-1 batch with a stranded spur. The 7-bus ring of the basic tour's Example 1.1 (diagram there) is too forgiving for a demo (every single outage leaves a connected path), so we hang a two-bus spur B8-B9 on a single line from B4:

 (ring of Example 1.1) B4 ------ B8 ------ B9
                                6 MW      4 MW

Losing that line strands the PAIR as an island with a live branch but no voltage reference, and the report has to say so instead of crashing. (A single stranded bus would not do: markIsolatedBuses! inside the batch marks buses without any in-service branch as isolated and solves the rest, so a one-bus stub converges cleanly.)

net_n1 = build_ring7("tour_n1")
addBus!(net = net_n1, busName = "B8", vn_kV = 110.0)
addBus!(net = net_n1, busName = "B9", vn_kV = 110.0)
addPIModelACLine!(net = net_n1, fromBus = "B4", toBus = "B8", r_pu = 0.02, x_pu = 0.10, b_pu = 0.0, status = 1)
addPIModelACLine!(net = net_n1, fromBus = "B8", toBus = "B9", r_pu = 0.02, x_pu = 0.10, b_pu = 0.0, status = 1)
addProsumer!(net = net_n1, busName = "B8", type = "LOAD", p = 6.0, q = 2.0)
addProsumer!(net = net_n1, busName = "B9", type = "LOAD", p = 4.0, q = 1.0)
ok_n1, msg_n1 = validate!(net = net_n1)
ok_n1 || error("N-1 net invalid: $msg_n1")

cases = generateN1Branches(net_n1)
println(length(cases), " single-branch outage cases")
results = runContingencies!(net_n1, cases; vm_min_pu = 0.95, vm_max_pu = 1.05)
printContingencyResults(results)
11 single-branch outage cases
AC island detection:
  islands: 2
  island 1: buses=7, branches=9, ref=1, status=has_ref
  island 2: buses=2, branches=1, ref=none, status=missing_ref
AC island diagnostic artifact: /tmp/ac_islands.csv
N-1 contingency results (11 case(s), 10 converged)
-------------------------------------------------------------------------------------------------------------------------------------------------------
case                         converged  iter start   Vmin[pu]  Vmax[pu]   loading[%]   overld   V-viol islands shed[MW]  weight severity  error
-------------------------------------------------------------------------------------------------------------------------------------------------------
B_ACL_110_4_8                NO            0 none           -         -            -        0        0       2     10.0    1.00        -  islanded: load-only, 10.0 MW load disconnected
B_ACL_110_1_2                yes           5 warm      0.8609    1.0200            -        0        7       1        -    1.00     0.00
B_ACL_110_2_3                yes           4 warm      0.9630    1.0200            -        0        0       1        -    1.00     0.00
B_ACL_110_3_4                yes           4 warm      0.9404    1.0200            -        0        3       1        -    1.00     0.00
B_ACL_110_4_5                yes           4 warm      0.9505    1.0200            -        0        0       1        -    1.00     0.00
B_ACL_110_5_6                yes           4 warm      0.9630    1.0200            -        0        0       1        -    1.00     0.00
B_ACL_110_6_7                yes           4 warm      0.9428    1.0200            -        0        2       1        -    1.00     0.00
B_ACL_110_7_1                yes           5 warm      0.9271    1.0200            -        0        7       1        -    1.00     0.00
B_ACL_110_2_5                yes           4 warm      0.9516    1.0200            -        0        0       1        -    1.00     0.00
B_ACL_110_3_6                yes           4 warm      0.9664    1.0200            -        0        0       1        -    1.00     0.00
B_ACL_110_8_9                yes           4 warm      0.9715    1.0200            -        0        0       1        -    1.00     0.00
-------------------------------------------------------------------------------------------------------------------------------------------------------

Reading aid (Example 5.1), three things to check in the table:

  • the B_ACL_110_4_8 outage (the B4-B8 spur line) reports islanded without reference: the B8-B9 pair loses its only feed and the case is a reported failure, not a crash;
  • the ring outages converge, and the tight 0.95-pu floor flags the voltage sags an outage causes (the V-viol column counts the buses outside the band per case; the B1-B2 outage sags the ring to 0.86 pu);
  • iterations stay small: every case starts from the solved base-case voltages (warm start), not from flat.

The result rows are plain data, so post-processing is ordinary Julia: rank the converged cases by their voltage sag and export everything as CSV for a report or a spreadsheet:

ranked = sort([r for r in results if r.converged]; by = r -> r.min_vm_pu)
println("worst three outages by post-outage Vmin:")
for r in ranked[1:3]
  println("  ", rpad(r.name, 16), " Vmin = ", round(r.min_vm_pu; digits = 4), " pu, ", length(r.voltage_violations), " bus(es) below the band")
end
csv_n1 = joinpath(mktempdir(), "n1_results.csv")
writeContingencyResultsCSV(csv_n1, results)
println("exported: ", csv_n1, " (", filesize(csv_n1), " bytes)")
worst three outages by post-outage Vmin:
  B_ACL_110_1_2    Vmin = 0.8609 pu, 7 bus(es) below the band
  B_ACL_110_7_1    Vmin = 0.9271 pu, 7 bus(es) below the band
  B_ACL_110_3_4    Vmin = 0.9404 pu, 3 bus(es) below the band
exported: /tmp/jl_x9yTNX/n1_results.csv (1021 bytes)

Two knobs worth knowing: retry_flat_start = true grants one bounded flat-start retry per failed case (the rescue ladder is deliberately NOT in this loop), and generateN1Branches(net; include_transformers = false) restricts the case list to AC lines. Parallel circuits are disambiguated as name#branchIdx, and imported MATPOWER FOR001 outage lists work as case sources too; see N-1 Contingency Analysis. On a real case this batch is exactly what the threads in the next chapter are for.

Part II: Beyond

Chapter 6: using your cores

Independent work items (island solves, short-circuit fault sweeps, N-1 contingency batches) fan out over Julia THREADS since 0.9.10, gated by runtime.parallel.* (default on). Threads are fixed at process start: julia --threads=auto uses all cores, and without the flag everything runs serially through the identical code path. First: how many do we have right now?

println("this session runs on ", Threads.nthreads(), " Julia thread(s)")
this session runs on 1 Julia thread(s)

Example 6.1: a fault sweep, serial vs parallel. The mechanics on a fault sweep: eight feeder-fed rings of 500 buses each, every bus a fault location, once serial and once parallel. The results must be IDENTICAL: parallelism only changes the wall clock, never a number.

 (grid k)
   Pk_B1 ---- Pk_B2 ---- ... ---- Pk_B500
     |                               |
     +-------------------------------+     k = 1..8 independent rings
net_par = Net(name = "tour_parallel", baseMVA = 100.0)
for k in 1:8
  bus = i -> "P$(k)_B$(i)"
  for i in 1:500
    addBus!(net = net_par, busName = bus(i), vn_kV = 110.0)
  end
  addExternalGrid!(net = net_par, busName = bus(1), vm_pu = 1.0, sk_max_MVA = 2000.0 + 100.0 * k, sk_min_MVA = 1500.0, rx_max = 0.1, internal_impedance = false)
  for i in 1:500
    addPIModelACLine!(net = net_par, fromBus = bus(i), toBus = bus(i == 500 ? 1 : i + 1), r_pu = 0.001, x_pu = 0.004, b_pu = 0.0, status = 1)
  end
end
validate!(net = net_par)

runShortCircuit!(net_par; case = :max, parallel_enabled = false)      ## warm both paths
runShortCircuit!(net_par; case = :max, parallel_min_work_items = 2)
t_ser = @elapsed sc_ser = runShortCircuit!(net_par; case = :max, parallel_enabled = false)
t_par = @elapsed sc_par = runShortCircuit!(net_par; case = :max, parallel_min_work_items = 2)
println("fault sweep over ", length(sc_ser.rows), " buses: serial ", round(t_ser * 1000; digits = 1), " ms, parallel ", round(t_par * 1000; digits = 1), " ms (", round(t_ser / t_par; digits = 2), "x)")
println("rows identical: ", isequal(sc_ser.rows, sc_par.rows))
[ Info: More than one slack bus defined in the network
fault sweep over 4000 buses: serial 71.0 ms, parallel 71.0 ms (1.0x)
rows identical: true

Reading aid (Example 6.1): on Colab's free tier you usually get 1-2 vCPUs, so the factor here stays modest (with one thread the parallel call falls back to the very same serial function). The real effect wants your local machine: julia --threads=auto --project=. examples/run_parallel_suite.jl runs the three dedicated demos, island solving (power_flow.islands.mode: solve_parallel), this fault sweep at 8000 buses, and a full N-1 contingency batch on case1354pegase (measured 71.7 s serial vs 17.6 s on 16 threads), each asserting serial/parallel identity. The N-1 batch itself is chapter 5 (Example 5.1); on a large case it is the prime customer of these threads.

Where to go next

The basic tour (Newcomer to Advanced) and the focused notebooks:

And the documentation for going further: