FACTS flow control: from the TCSC to the UPFC
Level: Expert, companion of the advanced tour's FACTS chapter. Covers the series-reactance controller (TCSC) and builds up to the full UPFC (unified power flow controller), including the DC-link-coupled model that steers a line's active and reactive flow independently.
Note: This workshop was created with AI assistance and is reviewed and curated by the maintainer; it is not a fully machine-generated text.
In a meshed AC network, power does not follow contracts, it follows impedance: parallel paths split the transfer in inverse proportion to their reactances. A TCSC (thyristor controlled series capacitor) exploits exactly that lever: a variable series reactance in one line steers how much flow that corridor carries. In this notebook you build a small loop network with Sparlectra.jl, watch the natural flow split, and then let the SeriesReactanceControl outer-loop controller move the split onto a target, including the honest failure mode when the target is out of reach.
Note: On Google Colab the install cell takes a minute or two 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 cell loads the package and defines what the examples share (the network builders of all examples), so any example can run on its own after it; a code cell that uses a name from another cell says so in a comment. Its last line, warmup(), runs every path the notebook exercises once (the power-flow solver, the outer control loop with a series-reactance controller, the result tables, both UPFC models), so nothing stalls mid-study; its code is in docs/lit/warmup/workshop_series_compensation.jl of the package. How long the warm-up takes depends on the machine: a Colab session is several times slower than a desktop; every later cell then runs without compile pauses.
using Sparlectra
# the network builders of the examples, defined here so that any example
# runs on its own after this cell:
# build_loop() two parallel corridors A to B (Examples 1 to 4)
# build_upfc_mesh() small mesh with a parallel S->L path (Example 5)
function build_loop()
net = Net(name = "tcsc_workshop", baseMVA = 100.0)
for b in ("A", "M1", "M2", "B")
addBus!(net = net, busName = b, vn_kV = 110.0)
end
addProsumer!(net = net, busName = "A", type = "EXTERNALNETWORKINJECTION", referencePri = "A", vm_pu = 1.0, va_deg = 0.0)
addProsumer!(net = net, busName = "B", type = "ENERGYCONSUMER", p = 80.0, q = 20.0)
addPIModelACLine!(net = net, fromBus = "A", toBus = "M1", r_pu = 0.01, x_pu = 0.10, b_pu = 0.0, status = 1)
addPIModelACLine!(net = net, fromBus = "M1", toBus = "B", r_pu = 0.01, x_pu = 0.10, b_pu = 0.0, status = 1)
addPIModelACLine!(net = net, fromBus = "A", toBus = "M2", r_pu = 0.02, x_pu = 0.20, b_pu = 0.0, status = 1)
addPIModelACLine!(net = net, fromBus = "M2", toBus = "B", r_pu = 0.02, x_pu = 0.20, b_pu = 0.0, status = 1)
ok, msg = validate!(net = net)
ok || error("Network validation failed: $msg")
return net
end
function build_upfc_mesh()
m = Net(name = "upfc_mesh", 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
# compile every path the chapters use once; the warm-up code is in
# docs/lit/warmup/workshop_series_compensation.jl of the installed package
include(pkgdir(Sparlectra, "docs", "lit", "warmup", "workshop_series_compensation.jl"))
warmup()warm-up: compiles the paths of this workshop once; how long it takes depends on the machine (a Colab session is several times slower than a desktop)
warm: power flow plus series-reactance control 0.74 s (first calls compile)
further paths : 5.04 s (control runs, result tables, both UPFC models); everything warmWhy a series reactance steers flow
Every branch enters the power flow through its admittance matrix (see the Branch Model page for the derivation):
\[Y_{br} = \begin{bmatrix} \frac{1}{\tau^2}\left(y_{ser} + \frac{y_{shunt}}{2}\right) & -y_{ser}\,\frac{1}{\tau e^{-j\phi}} \\ -y_{ser}\,\frac{1}{\tau e^{j\phi}} & y_{ser} + \frac{y_{shunt}}{2} \end{bmatrix}, \qquad y_{ser} = \frac{1}{R + jX},\]
with $N = 1$ for lines. The TCSC acts purely through $X$ inside $y_{ser}$: every accepted controller step changes one branch stamp and the outer loop re-stamps the Y-bus before the next solve.
For a lossless line the transfer relation
\[P_{12} = \frac{V_1 V_2}{X}\,\sin(\delta_1 - \delta_2)\]
says that a lower series reactance carries more power at a given angle difference. In a loop, flow redistributes between the parallel paths according to their reactance ratio, which is exactly what we are about to watch.
A loop network with two corridors
Example 1: the natural flow split. 80 MW travel from source A to sink B over two parallel corridors. The upper corridor (A to M2 to B) has twice the reactance of the lower one, so it naturally carries only one third of the transfer.
+---- M1 ----+ corridor 1: x = 0.10 per line (TCSC here)
| |
A ----+ +---- B (load 80 MW)
(slack) | |
+---- M2 ----+ corridor 2: x = 0.20 per linebuild_loop (this network) is defined in the warm-up cell at the top.
# uses: build_loop (warm-up cell)
net = build_loop()
run_sparlectra(net = net)
println("natural split: corridor 1 (A->M1) = ", round(get_branch_p_from_to_mw(net, "A", "M1"); digits = 2), " MW")
println(" corridor 2 (A->M2) = ", round(get_branch_p_from_to_mw(net, "A", "M2"); digits = 2), " MW")natural split: corridor 1 (A->M1) = 54.0 MW
corridor 2 (A->M2) = 27.0 MWReading aid (Example 1): the 2:1 reactance ratio produces the 2:1 flow split, independent of any thermal ratings: the low-reactance corridor attracts the flow.
Attach the TCSC and steer the split
Example 2: steering the split onto a target. addSeriesReactanceControl! registers the controller on the line from A to M2, continuing on the Example 1 network (diagram above). The target of 35 MW needs a visible reactance move: the outer loop measures the branch flow after each converged solve, steps x_pu via secant iteration (the first step is a bounded probe, because the sign of $dP/dX$ depends on the network), and stops inside the 0.5 MW default deadband.
# uses: net (Example 1)
@isdefined(net) || error("Run Example 1 first: it sets up net.")
ctrl = addSeriesReactanceControl!(net; fromBus = "A", toBus = "M2", p_target_mw = 35.0, x_min_pu = 0.02, x_max_pu = 0.30)
run_sparlectra(net = net)
println("steered: corridor 2 (A->M2) = ", round(get_branch_p_from_to_mw(net, "A", "M2"); digits = 2), " MW (target 35)")
println(" x_pu moved 0.20 -> ", round(ctrl.x_pu; digits = 4), ", status = ", ctrl.status)steered: corridor 2 (A->M2) = 34.62 MW (target 35)
x_pu moved 0.20 -> 0.0709, status = convergedThe controller row from the last control run (Example 2) and the generic controllable-element view carry the shared vocabulary (actuator, range, quantity, target) that all outer controllers report:
# uses: net (Example 1, Example 2)
@isdefined(net) || error("Run Example 1, Example 2 first: they set up net.")
cr = latest_control_result(net)
println("outer loop: status = ", cr.status, ", outer iterations = ", cr.outer_iterations, ", pf solves = ", cr.powerflow_solves)
for row in cr.controllers
println("controller: ", row.controller_name, " achieved ", round(row.achieved_p_mw; digits = 2), " MW of ", row.p_target_mw, " MW, x_pu = ", round(row.x_pu; digits = 4))
end
for e in controllableElements(net)
println("element: ", e.element, " | ", e.device, " | ", e.actuator, " in [", e.actuator_min, ", ", e.actuator_max, "] | ", e.quantity, " @ ", e.target)
endouter loop: status = converged, outer iterations = 4, pf solves = 4
controller: TCSC_A_M2 TCSC achieved 34.62 MW of 35.0 MW, x_pu = 0.0709
element: branch@A-M2 | TCSC (series compensation) | series_x_pu in [0.02, 0.3] | branch_active_power @ A->M2The same information lands in the classical result tables: the "Controllers" line counts the TCSC, and the controlled A->M2 line carries the Ctrl = TCSC marker with its P target and status (the moved reactance is in the Series Reactance Control Summary of the Control footer).
# uses: net (Example 1, Example 2)
@isdefined(net) || error("Run Example 1, Example 2 first: they set up net.")
calcNetLosses!(net)
printACPFlowResults(net, 0.0, 1, 1e-8)================================================================================
| SPARLECTRA Version 0.50.0 - AC Power Flow Results |
================================================================================
Date : 19-Oct-26 18:1:19
Iterations : 1
Control passes : 4 (Iterations above = last pass; 13 inner iterations in total)
Flatstart : No
Tolerance : 1.0e-08
Solver : NR
Total time : 0.000000 s
Case : tcsc_workshop
Cooldown iters : 1
Q-hysteresis : 0.0100 pu
Jacobian cond. : kappa1(J) = 9.39, attainable accuracy ~ 2.1e-15, well conditioned (tol 1.0e-8 reachable)
BaseMVA : 100
Nodes : 4 (PV: 0 PQ: 3 Slack: 1)
Grid connection: slack bus A
Branches : 4
Links : 0
HVDC links : 0
Lines : 4
Trafos : 0
Generators : 1
Loads : 1
Shunts : 0
Controllers : 1 (Tap: 0, Q(U): 0, P(U): 0, TCSC: 1)
PV→PQ locks : 0
PV→PQ events : 0
total network power balance (Σ S_branch): P = 1.020 [MW], Q = 8.491 [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 | A | 110.0 | 110.000 | 1.000 | 0.000 | 81.020 | 28.491 | | | | | SLACK | - | |
| 2 | M1 | 110.0 | 107.692 | 0.979 | -2.615 | | | | | | | PQ | - | |
| 3 | M2 | 110.0 | 108.398 | 0.985 | -1.298 | | | | | | | PQ | - | |
| 4 | B | 110.0 | 105.618 | 0.960 | -5.339 | | | 80.000 | 20.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 | A -> M1 | 46.403 | 17.361 | -46.157 | -14.906 | 0.245 | 2.455 | - | - | - | - |
| B_ACL_110_2_4 | Line | M1 -> B | 46.157 | 14.906 | -45.912 | -12.452 | 0.245 | 2.455 | - | - | - | - |
| B_ACL_110_1_3 | Line | A -> M2 | 34.617 | 11.131 | -34.353 | -10.193 | 0.264 | 0.938 | TCSC | 35.000 | - | converged |
| B_ACL_110_3_4 | Line | M2 -> B | 34.353 | 10.193 | -34.088 | -7.548 | 0.264 | 2.644 | - | - | - | - |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Control
-------
Transformer controls: none
Series Reactance Control Summary (TCSC)
---------------------------------------
TCSC_A_M2 TCSC (branch A -> M2)
target P : 35.000 MW
achieved P : 34.617 MW
series reactance : 0.07092 pu
reactance range : 0.02000 .. 0.30000 pu
deadband : 0.500 MW
converged : true
at_limit : false
status : converged
Q-V characteristic: no non-physical generator states.The honest limit
Example 3: the honest limit. Ask the same corridor for 70 MW, on a fresh copy of the Example 1 loop (diagram there), and the range [0.02, 0.30] is not enough: the reactance clamps at the capacitive end, the branch behaves as a fixed compensated line, and the controller reports at_limit instead of pretending convergence. The power flow itself stays valid.
# uses: build_loop (warm-up cell)
net2 = build_loop()
ctrl2 = addSeriesReactanceControl!(net2; fromBus = "A", toBus = "M2", p_target_mw = 70.0, x_min_pu = 0.02, x_max_pu = 0.30)
run_sparlectra(net = net2)
println("limited: corridor 2 (A->M2) = ", round(get_branch_p_from_to_mw(net2, "A", "M2"); digits = 2), " MW (target 70)")
println(" x_pu = ", round(ctrl2.x_pu; digits = 4), " (clamped), at_limit = ", ctrl2.at_limit, ", converged = ", ctrl2.converged)limited: corridor 2 (A->M2) = 38.93 MW (target 70)
x_pu = 0.02 (clamped), at_limit = true, converged = falseIn the classical result the honest limit is visible in the branch row: the A->M2 line's Ctrl status reads "atlimitnot_converged" instead of pretending convergence.
# uses: net2 (Example 3)
@isdefined(net2) || error("Run Example 3 first: it sets up net2.")
calcNetLosses!(net2)
printACPFlowResults(net2, 0.0, 1, 1e-8)================================================================================
| SPARLECTRA Version 0.50.0 - AC Power Flow Results |
================================================================================
Date : 19-Oct-26 18:1:21
Iterations : 1
Control passes : 3 (Iterations above = last pass; 13 inner iterations in total)
Flatstart : No
Tolerance : 1.0e-08
Solver : NR
Total time : 0.000000 s
Case : tcsc_workshop
Cooldown iters : 1
Q-hysteresis : 0.0100 pu
Jacobian cond. : kappa1(J) = 13.8, attainable accuracy ~ 3.1e-15, well conditioned (tol 1.0e-8 reachable)
BaseMVA : 100
Nodes : 4 (PV: 0 PQ: 3 Slack: 1)
Grid connection: slack bus A
Branches : 4
Links : 0
HVDC links : 0
Lines : 4
Trafos : 0
Generators : 1
Loads : 1
Shunts : 0
Controllers : 1 (Tap: 0, Q(U): 0, P(U): 0, TCSC: 1)
PV→PQ locks : 0
PV→PQ events : 0
total network power balance (Σ S_branch): P = 1.067 [MW], Q = 7.701 [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 | A | 110.0 | 110.000 | 1.000 | 0.000 | 81.067 | 27.701 | | | | | SLACK | - | |
| 2 | M1 | 110.0 | 107.845 | 0.980 | -2.369 | | | | | | | PQ | - | |
| 3 | M2 | 110.0 | 108.893 | 0.990 | -0.318 | | | | | | | PQ | - | |
| 4 | B | 110.0 | 105.882 | 0.963 | -4.830 | | | 80.000 | 20.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 | A -> M1 | 42.141 | 16.211 | -41.937 | -14.172 | 0.204 | 2.039 | - | - | - | - |
| B_ACL_110_2_4 | Line | M1 -> B | 41.937 | 14.172 | -41.733 | -12.133 | 0.204 | 2.039 | - | - | - | - |
| B_ACL_110_1_3 | Line | A -> M2 | 38.926 | 11.491 | -38.596 | -11.161 | 0.329 | 0.329 | TCSC | 70.000 | - | at_limit_not_converged |
| B_ACL_110_3_4 | Line | M2 -> B | 38.596 | 11.161 | -38.267 | -7.867 | 0.329 | 3.294 | - | - | - | - |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Control
-------
Transformer controls: none
Series Reactance Control Summary (TCSC)
---------------------------------------
TCSC_A_M2 TCSC (branch A -> M2)
target P : 70.000 MW
achieved P : 38.926 MW
series reactance : 0.02000 pu
reactance range : 0.02000 .. 0.30000 pu
deadband : 0.500 MW
converged : false
at_limit : true
status : at_limit
status detail : target not reached, reactance clamped at the range end (fixed compensated line)
Q-V characteristic: no non-physical generator states.From series reactance to the UPFC
The TCSC above steers flow by changing a series REACTANCE: its injected voltage is in quadrature with the line current, so it exchanges no active power. A UPFC (unified power flow controller) removes that restriction. It adds a SHUNT converter at one line end and couples the two converters through a DC link, so the SERIES converter may inject a voltage of ARBITRARY phase. The in-phase component now carries active power, balanced through the DC link by the shunt, and that is the extra degree of freedom: the line can hold INDEPENDENT active and reactive targets at once.
bus i (from) series converter bus j (to)
V_i o----+---[ + V_se - ]---[ line ]---o V_j
| controlled flow ->
[ shunt conv ] P_sh = -P_se (DC balance) + Q_sh
|
=== DC link === (couples the two converters)Example 4: the quadrature composite (SSSC + STATCOM). In the quadrature limit P_se = 0, the DC link idles, and the UPFC is exactly an SSSC on the branch plus a STATCOM at the bus. addUpfcControl! (default model = :quadrature) registers that pair as one device, on the loop of Example 1 (diagram above) with a machine added at M2 for the shunt converter:
# uses: build_loop (warm-up cell)
netu = build_loop()
addProsumer!(net = netu, busName = "M2", type = "GENERATOR", p = 0.0, q = 0.0)
upfc = addUpfcControl!(netu; 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!(netu)
println("quadrature UPFC: two converter rows for one device:")
for row in controllableElements(netu)
println(" ", rpad(row.device, 48), row.actuator, ", at_limit = ", row.at_limit)
endquadrature UPFC: two converter rows for one device:
UPFC shunt (VSC pair, stationary quadrature model)machine_q_mvar, at_limit = true
UPFC series (VSC pair, stationary quadrature model)series_x_pu, at_limit = falseThe classical result tables show the composite as its two sub-controllers: the series side under the TCSC/SSSC summary, the shunt side under the machine (STATCOM) summary, both counted on the "Controllers" line.
# uses: netu (Example 4)
@isdefined(netu) || error("Run Example 4 first: it sets up netu.")
calcNetLosses!(netu)
printACPFlowResults(netu, 0.0, 1, 1e-8)================================================================================
| SPARLECTRA Version 0.50.0 - AC Power Flow Results |
================================================================================
Date : 19-Oct-26 18:1:21
Iterations : 1
Control passes : 8 (Iterations above = last pass; 29 inner iterations in total)
Flatstart : No
Tolerance : 1.0e-08
Solver : NR
Total time : 0.000000 s
Case : tcsc_workshop
Cooldown iters : 1
Q-hysteresis : 0.0100 pu
Jacobian cond. : kappa1(J) = 9.13, attainable accuracy ~ 2.0e-15, well conditioned (tol 1.0e-8 reachable)
BaseMVA : 100
Nodes : 4 (PV: 0 PQ: 3 Slack: 1)
Grid connection: slack bus A
Branches : 4
Links : 0
HVDC links : 0
Lines : 4
Trafos : 0
Generators : 2
Loads : 1
Shunts : 0
Controllers : 2 (Tap: 0, Q(U): 0, P(U): 0, MachV: 1, TCSC: 1)
PV→PQ locks : 0
PV→PQ events : 0
total network power balance (Σ S_branch): P = 1.091 [MW], Q = 8.453 [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 | A | 110.0 | 110.000 | 1.000 | 0.000 | 81.091 | -11.852 | | | | | SLACK | - | |
| 2 | M1 | 110.0 | 108.327 | 0.985 | -2.625 | | | | | | | PQ | - | |
| 3 | M2 | 110.0 | 110.840 | 1.008 | -1.471 | | 40.305 | | | | | PQ | - | |
| 4 | B | 110.0 | 106.888 | 0.972 | -5.326 | | | 80.000 | 20.000 | | | PQ | STATCOM | |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
==========================================================================================================================================================================================================================
| 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 | A -> M1 | 46.256 | 11.619 | -46.028 | -9.345 | 0.227 | 2.275 | - | - | - | - |
| B_ACL_110_2_4 | Line | M1 -> B | 46.028 | 9.345 | -45.801 | -7.070 | 0.227 | 2.275 | - | - | - | - |
| B_ACL_110_1_3 | Line | A -> M2 | 34.835 | -23.471 | -34.482 | 24.544 | 0.353 | 1.073 | TCSC | 35.000 | - | converged |
| B_ACL_110_3_4 | Line | M2 -> B | 34.482 | 15.762 | -34.199 | -12.930 | 0.283 | 2.832 | - | - | - | - |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Control
-------
Transformer controls: none
Machine Voltage Control Summary
-------------------------------
UPFC_A_M2_shunt STATCOM (machine at M2 -> bus B)
target Vm : 0.9900 pu
achieved Vm : 0.9717 pu
reactive output : 40.305 MVAr
limit mode : STATCOM current limit, S_max = 40.000 MVA at 1.0 pu
live Q range : -40.305 .. 40.305 MVAr (at Vt = 1.0076 pu)
deadband : 0.0010 pu
converged : false
at_limit : true
status : at_limit
status detail : target not reached because a reactive limit was hit
Series Reactance Control Summary (TCSC)
---------------------------------------
UPFC_A_M2_series SSSC (branch A -> M2)
target P : 35.000 MW
achieved P : 34.835 MW
series reactance : 0.06080 pu
limit mode : SSSC injected-voltage limit, V_inj,max = 0.0800 pu
live x window : 0.00954 .. 0.39046 pu (x_base 0.20000, |I| = 0.4200 pu)
injected voltage : 0.0585 pu (of 0.0800 pu available)
deadband : 0.500 MW
converged : true
at_limit : false
status : converged
Q-V characteristic: no non-physical generator states.Reading aid (Example 4): one call, one composite name, but honestly TWO result rows, the series (SSSC) and the shunt (STATCOM), each with its own at_limit. This is the whole device in the quadrature limit; it steers ONE line quantity plus the shunt voltage, but not independent P and Q.
Example 5: the full model, independent P and Q. Lifting the quadrature restriction (model = :full) lets the series converter inject an arbitrary-phase voltage; the line then holds distinct P and Q targets, with the active part balanced across the DC link. The full model needs the shunt at the SENDING bus, so a small mesh (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)build_upfc_mesh (this mesh) is defined in the warm-up cell at the top.
# uses: build_upfc_mesh (warm-up cell)
netf = build_upfc_mesh()
full = addUpfcControl!(netf; 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!(netf; control_config = ControlConfig(max_outer_iterations = 80))
f = full.upfc
println("full UPFC on I->J:")
println(" line P = ", round(f.achieved_p_mw; digits = 2), " MW (target 40) and Q = ", round(f.achieved_q_mvar; digits = 2), " MVAr (target 10), both at once")
println(" series V_se = ", round(abs(f.v_se_pu); digits = 4), " pu, P_se = ", round(f.p_se_mw; digits = 3), " MW")
println(" DC-link balance P_se + P_sh = ", round(f.p_se_mw + f.p_sh_mw; digits = 4), " MW")full UPFC on I->J:
line P = 40.0 MW (target 40) and Q = 9.99 MVAr (target 10), both at once
series V_se = 0.0542 pu, P_se = -0.632 MW
DC-link balance P_se + P_sh = 0.0 MWThe classical result tables carry the whole picture: the "Controllers" line now counts the UPFC, and the "UPFC Control Summary" block reports the line P/Q targets vs achieved, the series voltage, and the DC-link residual.
# uses: netf (Example 5)
@isdefined(netf) || error("Run Example 5 first: it sets up netf.")
calcNetLosses!(netf)
printACPFlowResults(netf, 0.0, 1, 1e-8)================================================================================
| SPARLECTRA Version 0.50.0 - AC Power Flow Results |
================================================================================
Date : 19-Oct-26 18:1:21
Iterations : 1
Control passes : 32 (Iterations above = last pass; 91 inner iterations in total)
Flatstart : No
Tolerance : 1.0e-08
Solver : NR
Total time : 0.000000 s
Case : upfc_mesh
Cooldown iters : 1
Q-hysteresis : 0.0100 pu
Jacobian cond. : kappa1(J) = 17.3, attainable accuracy ~ 3.8e-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.018 [MW], Q = 9.263 [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 | 91.385 | 39.263 | | | | | SLACK | - | |
| 2 | I | 110.0 | 108.619 | 0.987 | -1.770 | 0.632 | | | | | | PQ | - | |
| 3 | J | 110.0 | 105.511 | 0.959 | -2.771 | | | | | | | PQ | - | |
| 4 | L | 110.0 | 104.294 | 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 | 39.537 | 11.345 | -39.368 | -9.991 | 0.169 | 1.354 | - | - | - | - |
| B_ACL_110_2_3 | Line | I -> J | 40.001 | 9.991 | -39.020 | -9.025 | 0.981 | 0.966 | UPFC | 40.000 | - | converged |
| B_ACL_110_3_4 | Line | J -> L | 39.020 | 9.025 | -38.845 | -7.630 | 0.174 | 1.395 | - | - | - | - |
| B_ACL_110_1_4 | Line | S -> L | 51.848 | 27.918 | -51.155 | -22.370 | 0.694 | 5.548 | - | - | - | - |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
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.001 MW
line Q target/achieved : 10.000 / 9.991 MVar
series voltage V_se : 0.0542 pu at -89.6 deg
series/shunt active : P_se -0.632 MW, P_sh 0.632 MW
DC-link residual : 0.0003 MW
shunt reactive Q_sh : 0.000 MVar (bound +-120.0)
injected-voltage limit : 0.3000 pu
converged / at_limit : true / false
status : converged
Q-V characteristic: no non-physical generator states.Forcing the series phase back to quadrature collapses P_se to zero, back to the Example 4 behaviour:
# uses: build_upfc_mesh (warm-up cell)
netfq = build_upfc_mesh()
fq = addUpfcControl!(netfq; 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!(netfq; control_config = ControlConfig(max_outer_iterations = 80))
println("quadrature-forced: P_se = ", round(fq.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 5): the number that unlocks independent P and Q is P_se, the active power the series converter pushes through the DC link (nonzero here, exactly zero when forced to quadrature). First-cut honesty: the shunt runs on a reactive setpoint (closed-loop shunt voltage is a follow-up), and the model converges for feasible, moderate targets. The full limitation list and the phasor picture are on the FACTS Devices page.
Where to go next
- FACTS Devices: the device taxonomy (STATCOM, SVC, SSSC, TCSC, both UPFC models), the limit-characteristic comparison, and the UPFC phasor picture.
- Series Compensation (TCSC): the theory page, compensation degree, device versus model, and the resonance guard.
- Control Framework: the outer loop all controllers share, and the uniform element view.
- Workshop tour: all workshop examples in one Colab session.