HVDC Converters (VSC & LCC)

To follow along, you can download this tutorial as a Julia script (.jl) or Jupyter notebook (.ipynb).

An HVDC link moves power between two AC buses through a DC stage: an AC/DC converter at each end, joined by a DC line. PowerFlows co-solves this DC stage together with the surrounding AC network, so an AC power flow with an HVDC branch converges to one consistent solution rather than being solved as two separate problems. PowerFlows represents two converter technologies:

  • VSC (voltage-source converter): self-commutated, so each terminal independently controls its own active power (or DC voltage) and reactive power (or AC voltage).
  • LCC (line-commutated converter): thyristor-based, so the only controls are each terminal's firing/extinction angle and transformer tap, and the converter always draws reactive power from the AC system it connects to.

This tutorial builds one 14-bus system for each technology and solves them with the same ACPowerFlow solver used elsewhere in these docs.

Loading the needed packages

using PowerSystemCaseBuilder
using PowerSystems
using PowerFlows

VSC case

Build the 14-bus test system with a single PowerSystems.TwoTerminalVSCLine replacing the AC line on the bus 2↔3 arc:

sys_vsc = build_system(
    PSITestSystems,
    "c_sys14_hvdc_vsc";
    force_build = true,
    add_forecasts = false,
)
System
Property Value
Name
Description
System Units Base SYSTEM_BASE
Base Power 100.0
Base Frequency 60.0
Num Components 70
Static Components
Type Count
ACBus 14
Arc 20
Line 15
PowerLoad 11
TapTransformer 3
ThermalStandard 5
Transformer2W 1
TwoTerminalVSCLine 1

Solve it with ACPowerFlow, the same convenience wrapper used for a plain AC-only system — no special solver settings are needed for a VSC line:

vsc_results = solve_power_flow(ACPowerFlow(), sys_vsc)
Dict{String, DataFrame} with 6 entries:
  "flow_results" => 19×10 DataFrame
 Row │ flow_name  bus_from  bus_to  P_from_to      Q_from_to   P_to_from       Q_to_from   P_losses      Q_losses    angle_difference
     │ String     Int64     Int64   Float64        Float64     Float64         Float64     Float64       Float64     Float64
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1-2               1       2  151.206        -19.0678    -147.217         25.3951     3.98818      6.32733          0.0836842
   2 │ 1-5               1       5   83.4989         5.1547     -80.1161         3.5094     3.38278      8.6641           0.169872
   3 │ 2-4               2       4   71.8485        -1.92205    -69.1016         6.65797    2.74697      4.73593          0.120062
   4 │ 2-5               2       5   53.3743         0.688583   -51.8851         0.185282   1.48914      0.873864         0.0861876
   5 │ 3-4               3       4  -54.2           22.4956      56.4817       -17.9808     2.28172      4.51474         -0.10602
   6 │ 4-5               4       5  -76.768         18.6542      77.5809       -16.0902     0.812856     2.564           -0.0338745
   7 │ 4-7               4       7   26.4901        -6.33111    -26.4901         7.77863    3.33067e-14  1.44753          0.0510156
   8 │ 4-9               4       9   15.0978         2.89982    -15.0978        -1.69565   -5.55112e-15  1.20417          0.0779145
   9 │ 5-6               5       6   46.8203        10.7953     -46.8203        -5.89344    2.22045e-14  4.90188          0.101394
  10 │ 6-11              6      11    8.87323        8.19859     -8.75215       -7.94503    0.12108      0.253556         0.00880368
  11 │ 6-12              6      12    8.12053        3.07436     -8.03959       -2.9059     0.0809395    0.168458         0.0150775
  12 │ 6-13              6      13   18.6266         9.62763    -18.3726        -9.12739    0.254015     0.500235         0.0159768
  13 │ 7-8               7       8   -5.30825e-13  -24.2014       5.30825e-13   25.1383     0.0          0.93692         -8.32667e-16
  14 │ 7-9               7       9   26.4901        16.4227     -26.4901       -15.4522     0.0          0.970468         0.0268988
  15 │ 9-10              9      10    3.80107       -0.219032    -3.79674        0.230521   0.00432521   0.0114895        0.003081
  16 │ 9-14              9      14    8.28684        0.766913    -8.20426       -0.591263   0.0825761    0.17565          0.0203397
  17 │ 10-11            10      11   -5.20324       -6.03052      5.25216        6.14503    0.0489183    0.114512        -0.0046723
  18 │ 12-13            12      13    1.93958        1.3059      -1.92869       -1.29605    0.010884     0.00984746       0.00089934
  19 │ 13-14            13      14    6.80122        4.62352     -6.69574       -4.40876    0.105481     0.214763         0.0147578
  "bus_results" => 14×9 DataFrame
 Row │ bus_number  Vm       θ           P_gen    P_load   P_net    Q_gen     Q_load   Q_net
     │ Int64       Float64  Float64     Float64  Float64  Float64  Float64   Float64  Float64
─────┼─────────────────────────────────────────────────────────────────────────────────────────
   1 │          1  1.06      0.0        234.704      0.0  234.704  -13.913       0.0  -13.913
   2 │          2  1.045    -0.0836842   40.0       21.7   18.3     31.8617     12.7   19.1617
   3 │          3  1.01     -0.309766     0.0       94.2  -94.2     31.4956     19.0   12.4956
   4 │          4  1.01245  -0.203746     0.0       47.8  -47.8      0.0        -3.9    3.9
   5 │          5  1.01539  -0.169872     0.0        7.6   -7.6      0.0         1.6   -1.6
   6 │          6  1.07     -0.271266     0.0       11.2  -11.2     22.5071      7.5   15.0071
   7 │          7  1.04938  -0.254762     0.0        0.0    0.0      0.0         0.0    0.0
   8 │          8  1.09     -0.254762     0.0        0.0    0.0     25.1383      0.0   25.1383
   9 │          9  1.03253  -0.281661     0.0       29.5  -29.5      0.0        16.6  -16.6
  10 │         10  1.03155  -0.284742     0.0        9.0   -9.0      0.0         5.8   -5.8
  11 │         11  1.04692  -0.280069     0.0        3.5   -3.5      0.0         1.8   -1.8
  12 │         12  1.05344  -0.286343     0.0        6.1   -6.1      0.0         1.6   -1.6
  13 │         13  1.0469   -0.287243     0.0       13.5  -13.5      0.0         5.8   -5.8
  14 │         14  1.02053  -0.302        0.0       14.9  -14.9      0.0         5.0   -5.0
  "mtdc_line_results" => 0×5 DataFrame
 Row │ line_name  dc_bus_from  dc_bus_to  dc_current  P_losses
     │ String     Int64        Int64      Float64     Float64
─────┴─────────────────────────────────────────────────────────
  "vsc_results" => 1×11 DataFrame
 Row │ line_name  bus_from  bus_to  P_from_to  P_to_from  Q_from_to  Q_to_from  dc_current  Vdc_from  Vdc_to   P_losses
     │ String     Int64     Int64   Float64    Float64    Float64    Float64    Float64     Float64   Float64  Float64
─────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ VSC_2_3           2       3    40.2945      -40.0        5.0       10.0    0.383758      1.05  1.04232   0.29454
  "mtdc_results" => 0×8 DataFrame
 Row │ converter_name  ac_bus  dc_bus  P_dc     Q        Vac      Vdc      mode
     │ String          Int64   Int64   Float64  Float64  Float64  Float64  String
─────┴────────────────────────────────────────────────────────────────────────────
  "lcc_results" => 0×13 DataFrame
 Row │ line_name  bus_from  bus_to  rectifier_tap  inverter_tap  rectifier_delay_angle  inverter_extinction_angle  P_from_to  P_to_from  Q_from_to  Q_to_from  P_losses  Q_losses
     │ String     Int64     Int64   Float64        Float64       Float64                Float64                    Float64    Float64    Float64    Float64    Float64   Float64
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

The result Dict carries the usual AC keys ("bus_results", "flow_results") plus HVDC-specific tables. For a VSC line, the relevant one is "vsc_results":

vsc_results["vsc_results"]
line_name bus_from bus_to P_from_to P_to_from Q_from_to Q_to_from dc_current Vdc_from Vdc_to P_losses
String Int64 Int64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64
VSC_2_3 2 3 40.29453966932661 -40.0 5.0 10.0 0.3837575206602534 1.05 1.042324849586795 0.29453966932661

P_from_to is the active power (MW) the from-side AC bus delivers into the converter; Q_from_to/Q_to_from are the reactive power each AC terminal exchanges with its own bus; dc_current, Vdc_from, Vdc_to describe the state of the (here two-node) internal DC network; and P_losses is the combined DC-side loss (converter losses plus the DC line's own resistive drop).

The two converters play different roles. This system's from converter is configured with dc_control_from = VSCDCControlModes.DC_VOLTAGE: it holds the DC network at its scheduled voltage, acting as the DC-side slack. The to converter is configured with dc_control_to = VSCDCControlModes.DC_POWER: it is dispatched to a scheduled active-power transfer, and the from converter's power then adjusts to balance that transfer plus DC losses — which is exactly the relationship between P_from_to and P_losses above. Independently of this DC-side role, each converter also holds its own AC-side reactive-power setpoint (ac_control_from/ac_control_to = VSCACControlModes.AC_REACTIVE_POWER here), so Q_from_to and Q_to_from are unrelated numbers rather than mirror images of each other.

Bus voltages solve normally alongside the DC network:

vsc_results["bus_results"]
4 rows omitted
bus_number Vm θ P_gen P_load P_net Q_gen Q_load Q_net
Int64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64
1 1.06 0.0 234.70446489882693 0.0 234.70446489882693 -13.913021798342488 0.0 -13.913021798342488
2 1.045 -0.08368416344667391 40.0 21.7 18.3 31.86174384166173 12.7 19.161743841661732
3 1.01 -0.3097663750172548 0.0 94.19999999999999 -94.19999999999999 31.495575041799846 19.0 12.495575041799844
4 1.0124457670918174 -0.20374625536865054 0.0 47.8 -47.8 0.0 -3.9 3.9
5 1.0153921142961957 -0.1698717250002936 0.0 7.6 -7.6 0.0 1.6 -1.6
6 1.07 -0.271265739694749 0.0 11.200000000000001 -11.200000000000001 22.507111190315804 7.5 15.007111190315802
7 1.0493750708989091 -0.2547619007971649 0.0 0.0 0.0 0.0 0.0 0.0
8 1.09 -0.25476190079716404 0.0 0.0 0.0 25.13833327333852 0.0 25.13833327333852
9 1.0325320744216007 -0.28166072443970774 0.0 29.5 -29.5 0.0 16.6 -16.6
10 1.031545197148625 -0.2847417221263094 0.0 9.0 -9.0 0.0 5.800000000000001 -5.800000000000001

LCC case

Build the matching system with a single PowerSystems.TwoTerminalLCCLine on the same 2↔3 arc, and solve it the same way:

sys_lcc = build_system(
    PSITestSystems,
    "c_sys14_hvdc_lcc";
    force_build = true,
    add_forecasts = false,
)
lcc_results = solve_power_flow(ACPowerFlow(), sys_lcc)
Dict{String, DataFrame} with 6 entries:
  "flow_results" => 19×10 DataFrame
 Row │ flow_name  bus_from  bus_to  P_from_to      Q_from_to   P_to_from       Q_to_from   P_losses      Q_losses    angle_difference
     │ String     Int64     Int64   Float64        Float64     Float64         Float64     Float64       Float64     Float64
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ 1-2               1       2  152.969        -19.4851    -148.886         26.1021     4.08306      6.61703          0.0847029
   2 │ 1-5               1       5   81.1987         5.02285    -77.9991         2.88196    3.19961      7.90481          0.165001
   3 │ 2-4               2       4   67.2199        -1.34845    -64.8154         5.04322    2.40458      3.69477          0.111906
   4 │ 2-5               2       5   49.9663         1.126      -48.6595        -0.811242   1.30675      0.314762         0.0802976
   5 │ 3-4               3       4  -45.3912        17.6128      46.9638       -14.9088     1.5726       2.704           -0.0879452
   6 │ 4-5               4       5  -71.8724        16.967       72.5818       -14.7296     0.709339     2.23747         -0.0316087
   7 │ 4-7               4       7   26.7028        -6.1635     -26.7028         7.62713    5.55112e-15  1.46363          0.0513757
   8 │ 4-9               4       9   15.2212         2.96206    -15.2212        -1.73855    0.0          1.22351          0.0784713
   9 │ 5-6               5       6   46.4769        11.0586     -46.4769        -6.21864   -3.33067e-14  4.84001          0.100585
  10 │ 6-11              6      11    8.66495        8.19544     -8.54694       -7.94832    0.118007     0.247121         0.00843502
  11 │ 6-12              6      12    8.0932         3.07793     -8.01271       -2.91041    0.0804874    0.167517         0.0150112
  12 │ 6-13              6      13   18.5187         9.62347    -18.267         -9.12789    0.251654     0.495585         0.0158528
  13 │ 7-8               7       8   -1.32533e-13  -24.0052       1.32533e-13   24.9264     0.0          0.921191        -2.498e-16
  14 │ 7-9               7       9   26.7028        16.378      -26.7028       -15.3983     0.0          0.979671         0.0270956
  15 │ 9-10              9      10    4.00518       -0.224733    -4.00038        0.237477   0.00479765   0.0127445        0.00324229
  16 │ 9-14              9      14    8.41889        0.761642    -8.33376       -0.580558   0.0851305    0.181084         0.0206716
  17 │ 10-11            10      11   -4.99961       -6.03748      5.04696        6.14832    0.0473495    0.11084         -0.00430264
  18 │ 12-13            12      13    1.9127         1.31041     -1.902         -1.30073    0.010701     0.00968183       0.000841509
  19 │ 13-14            13      14    6.66901        4.6287      -6.56624       -4.41947    0.102764     0.209232         0.0143142
  "bus_results" => 14×9 DataFrame
 Row │ bus_number  Vm       θ           P_gen    P_load   P_net    Q_gen     Q_load   Q_net
     │ Int64       Float64  Float64     Float64  Float64  Float64  Float64   Float64  Float64
─────┼─────────────────────────────────────────────────────────────────────────────────────────
   1 │          1  1.06      0.0        234.168      0.0  234.168  -14.4622      0.0  -14.4622
   2 │          2  1.045    -0.0847029   40.0       21.7   18.3     46.7101     12.7   34.0101
   3 │          3  1.01     -0.284554     0.0       94.2  -94.2     54.9443     19.0   35.9443
   4 │          4  1.0131   -0.196609     0.0       47.8  -47.8      0.0        -3.9    3.9
   5 │          5  1.01603  -0.165001     0.0        7.6   -7.6      0.0         1.6   -1.6
   6 │          6  1.07     -0.265585     0.0       11.2  -11.2     22.1782      7.5   14.6782
   7 │          7  1.04972  -0.247985     0.0        0.0    0.0      0.0         0.0    0.0
   8 │          8  1.09     -0.247985     0.0        0.0    0.0     24.9264      0.0   24.9264
   9 │          9  1.03293  -0.275081     0.0       29.5  -29.5      0.0        16.6  -16.6
  10 │         10  1.03189  -0.278323     0.0        9.0   -9.0      0.0         5.8   -5.8
  11 │         11  1.04711  -0.27402      0.0        3.5   -3.5      0.0         1.8   -1.8
  12 │         12  1.05346  -0.280596     0.0        6.1   -6.1      0.0         1.6   -1.6
  13 │         13  1.04697  -0.281438     0.0       13.5  -13.5      0.0         5.8   -5.8
  14 │         14  1.0208   -0.295752     0.0       14.9  -14.9      0.0         5.0   -5.0
  "mtdc_line_results" => 0×5 DataFrame
 Row │ line_name  dc_bus_from  dc_bus_to  dc_current  P_losses
     │ String     Int64        Int64      Float64     Float64
─────┴─────────────────────────────────────────────────────────
  "vsc_results" => 0×11 DataFrame
 Row │ line_name  bus_from  bus_to  P_from_to  P_to_from  Q_from_to  Q_to_from  dc_current  Vdc_from  Vdc_to   P_losses
     │ String     Int64     Int64   Float64    Float64    Float64    Float64    Float64     Float64   Float64  Float64
─────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  "mtdc_results" => 0×8 DataFrame
 Row │ converter_name  ac_bus  dc_bus  P_dc     Q        Vac      Vdc      mode
     │ String          Int64   Int64   Float64  Float64  Float64  Float64  String
─────┴────────────────────────────────────────────────────────────────────────────
  "lcc_results" => 1×13 DataFrame
 Row │ line_name  bus_from  bus_to  rectifier_tap  inverter_tap  rectifier_delay_angle  inverter_extinction_angle  P_from_to  P_to_from  Q_from_to  Q_to_from  P_losses  Q_losses
     │ String     Int64     Int64   Float64        Float64       Float64                Float64                    Float64    Float64    Float64    Float64    Float64   Float64
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │ LCC_2_3           2       3        1.27379       1.35646                    0.0                   0.296706       50.0   -48.8088    8.13037    18.3315   1.19115   26.4618

The matching result table is "lcc_results":

lcc_results["lcc_results"]
line_name bus_from bus_to rectifier_tap inverter_tap rectifier_delay_angle inverter_extinction_angle P_from_to P_to_from Q_from_to Q_to_from P_losses Q_losses
String Int64 Int64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64
LCC_2_3 2 3 1.2737858888888838 1.356459216677401 0.0 0.29670597283903605 50.0 -48.808848170151535 8.130370146724644 18.331477764564745 1.1911518298484636 26.46184791128939

An LCC has no independent control over its terminal voltages or reactive power. The rectifier and inverter each have one control angle — rectifier_delay_angle (firing angle) and inverter_extinction_angle (extinction angle) — plus a transformer tap, rectifier_tap/ inverter_tap. PowerFlows keeps both thyristor angles at their minimum physical limits (rectifier_delay_angle_limits/ inverter_extinction_angle_limits on the component) and instead uses the tap ratios to hold the line's scheduled DC power transfer — here the line's transfer_setpoint of 50 MW appears directly as P_from_to.

Because thyristors can only be switched on, not off, on demand, an LCC always draws reactive power from the AC system at both ends to commutate — it cannot supply it. Q_from_to and Q_to_from are both positive (absorbed from the AC network), in contrast to the VSC case above, where each converter's reactive power is an independent setpoint that can be positive, negative, or zero.

lcc_results["bus_results"]
4 rows omitted
bus_number Vm θ P_gen P_load P_net Q_gen Q_load Q_net
Int64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64
1 1.06 0.0 234.16804797003087 0.0 234.16804797003087 -14.462180723202398 0.0 -14.462180723202398
2 1.045 -0.08470294455086101 40.0 21.7 18.3 46.710133847591706 12.7 34.0101338475917
3 1.01 -0.2845544567506656 0.0 94.19999999999999 -94.19999999999999 54.94425015772357 19.0 35.94425015772357
4 1.0131003362369728 -0.1966092908293052 0.0 47.8 -47.8 0.0 -3.9 3.9
5 1.0160263215193588 -0.1650005894215484 0.0 7.6 -7.6 0.0 1.6 -1.6
6 1.07 -0.26558518030657585 0.0 11.200000000000001 -11.200000000000001 22.17817853246846 7.5 14.678178532468461
7 1.049717526472408 -0.24798499322427772 0.0 0.0 0.0 0.0 0.0 0.0
8 1.09 -0.24798499322427747 0.0 0.0 0.0 24.92642490750469 0.0 24.92642490750469
9 1.032932594115382 -0.2750805471919239 0.0 29.5 -29.5 0.0 16.6 -16.6
10 1.0318884362688723 -0.2783228409812764 0.0 9.0 -9.0 0.0 5.800000000000001 -5.800000000000001

When each is modeled

Reach for VSC when a converter has independent control over active power, reactive power (or AC voltage), and DC voltage — the behavior of modern IGBT-based HVDC links. Reach for LCC to model classic thyristor-based HVDC, where the only controls are firing/extinction angles and transformer taps, and the converter is always a net reactive-power sink on the AC side. PowerFlows solves both inside the same AC power flow as the surrounding network, so no separate DC power-flow step is needed for either technology.