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.

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.

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 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

Julia compiles each function on first use. This cell loads the package and warms the two paths this notebook exercises, the power-flow solver and the outer control loop with a series-reactance controller, on a tiny throwaway corridor, so the real study runs at full speed.

using Sparlectra

wnet = Net(name = "warmup", baseMVA = 100.0)
for b in ("A", "M", "B")
  addBus!(net = wnet, busName = b, vn_kV = 110.0)
end
addProsumer!(net = wnet, busName = "A", type = "EXTERNALNETWORKINJECTION", referencePri = "A", vm_pu = 1.0, va_deg = 0.0)
addProsumer!(net = wnet, busName = "B", type = "ENERGYCONSUMER", p = 10.0, q = 3.0)
addPIModelACLine!(net = wnet, fromBus = "A", toBus = "M", r_pu = 0.01, x_pu = 0.10, b_pu = 0.0, status = 1)
addPIModelACLine!(net = wnet, fromBus = "M", toBus = "B", r_pu = 0.01, x_pu = 0.10, b_pu = 0.0, status = 1)
addPIModelACLine!(net = wnet, fromBus = "A", toBus = "B", r_pu = 0.02, x_pu = 0.20, b_pu = 0.0, status = 1)
addSeriesReactanceControl!(wnet; fromBus = "A", toBus = "M", p_target_mw = 6.0, x_min_pu = 0.05, x_max_pu = 0.2)
t_ctrl = @elapsed run_control!(wnet; controllers = collect_outer_controllers(wnet), pf_config = PowerFlowConfig(method = :rectangular, max_iter = 15, tol = 1e-8), control_config = ControlConfig(max_outer_iterations = 4, trace = false))
println("warm: power flow plus series-reactance control ", round(t_ctrl; digits = 2), " s (first calls compile)")
warm: power flow plus series-reactance control 4.99 s (first calls compile)

Why 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 line
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

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 MW

Reading 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.

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 = converged

The 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:

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)
end
outer 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->M2

The 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).

calcNetLosses!(net)
printACPFlowResults(net, 0.0, 1, 1e-8)
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :  25-Aug-26 16:18:58
Iterations     :         1
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.000000 s
Case           :  tcsc_workshop
Cooldown iters :         0
Q-hysteresis   :    0.0000 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

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.

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 = false

In 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.

calcNetLosses!(net2)
printACPFlowResults(net2, 0.0, 1, 1e-8)
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :   25-Aug-26 16:19:1
Iterations     :         1
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.000000 s
Case           :  tcsc_workshop
Cooldown iters :         0
Q-hysteresis   :    0.0000 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)

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:

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)
end
quadrature 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 = false

The 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.

calcNetLosses!(netu)
printACPFlowResults(netu, 0.0, 1, 1e-8)
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :   25-Aug-26 16:19:2
Iterations     :         1
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.000000 s
Case           :  tcsc_workshop
Cooldown iters :         0
Q-hysteresis   :    0.0000 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

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)
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

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.0552 pu, P_se = 0.631 MW
  DC-link balance P_se + P_sh = 0.0 MW

The 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.

calcNetLosses!(netf)
printACPFlowResults(netf, 0.0, 1, 1e-8)
================================================================================
| SPARLECTRA Version 0.9.19     - AC Power Flow Results                        |
================================================================================
Date           :   25-Aug-26 16:19:2
Iterations     :         1
Flatstart      :        No
Tolerance      : 1.0e-08
Solver         :             NR
Total time     : 0.000000 s
Case           :      upfc_mesh
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 P_se to zero, back to the Example 4 behaviour:

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.