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 PowerFlowsVSC 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"]| 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.4618The 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"]| 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.