SWER case study: why optimisation matters
This case study follows one rural feeder — a Single-Wire Earth-Return (SWER) line — to show where optimisation earns its keep in distribution planning. Every code block runs when the docs are built, so the voltages below are real.
A Julia session with BMOPFTools plus JuMP and Ipopt (using Pkg; Pkg.add(["JuMP", "Ipopt"])), or julia --project=docs from a clone of the repository. Building this page runs six Ipopt solves (four power flows, two OPFs) — a few seconds on a laptop.
What SWER is, and why it is hard
A SWER line carries single-phase power on one conductor, using the earth itself as the return path. There is no second wire. A dedicated isolating transformer taps the three-phase medium-voltage network and feeds the SWER line; distribution transformers along the line step down to 230/240 V for customers, each earthed so the load current returns through the ground. By halving the conductor count and the pole hardware, SWER electrifies vast, sparsely populated areas at a fraction of the cost of a two- or three-wire line. It is the backbone of rural electrification in Australia, New Zealand, Brazil and southern Africa — Ergon Energy alone operates ~65,000 km of it in Queensland, at 12.7 kV (off 22 kV feeders) and 19.1 kV (off 33 kV).
The economy comes at a price: voltage regulation. A SWER line is long, lightly built and high-impedance, so it sits on a knife-edge between two opposite failure modes that the literature documents well:
- Peak demand → undervoltage. Rural load has grown far beyond the original design — air-conditioning, variable-speed-drive motors, irrigation pumps — and the voltage drop along a high-impedance single conductor with an earth return is severe (Sultan & Hawkins, Rural SWER networks — associated problems and cost-effective solutions, IJEPES 2011).
- Light load + rooftop PV → overvoltage. Modern rural customers export solar. Reverse power flow on the same high-impedance line pushes the far end above its limit (the North Jericho SWER study, Capacity improvements for rural SWER systems, IEEE). On real, very long SWER lines the light-load line-charging (Ferranti) rise adds to this; the fixture used below models no shunt capacitance, so the overvoltage demonstrated here is purely PV reverse flow.
Because voltage regulation is the capacity-limiting factor, the cheapest way to carry more load or host more PV is not a bigger conductor — it is better voltage control. The North Jericho study found that replacing fixed shunt reactors with controllable ones raised feeder capacity by ~85 %. That is the thread this case study pulls on: a single fixed setting cannot serve both failure modes, but an optimiser dispatching a controllable device per operating point can.
BMOPF models the earth return as a solidly-grounded bus neutral, so a SWER line is a single phase conductor whose linecode impedance carries the loop. The split-phase (centre-tapped) distribution transformer is modelled as a full coupled-coil 3-winding unit — the primitive admittance is reconstructed from the OpenDSS short-circuit set carried by PowerIO's BMOPF export, so its two legs track OpenDSS even under heavy, unbalanced loading. See the end-to-end tutorial for the general pipeline and Validating the OPF for the OpenDSS cross-checks.
1. Load and diagnose
The case ships with the package: a Queensland-style isolated-SWER feeder — a 22 kV three-phase source, a phase-to-phase isolating transformer to a 12.7 kV single-wire backbone, a single-ended distribution transformer (dx1 → lv_1) and a split-phase centre-tapped one (dx2 → lv_2).
using BMOPFTools
dss = joinpath(pkgdir(BMOPFTools), "test", "data", "SWER", "Master.dss")
net = from_dss(dss)
report = analyze(net)
println("SWER zones : ", report.results[:connectivity]["n_swer_zones"])
println("split-phase zones : ", report.results[:connectivity]["n_split_phase_zones"])
println("lv_1 nominal (V) : ",
round(report.results[:voltage_levels]["bus_voltage_map"]["lv_1"], digits=1)) Downloading artifact: powerio_capi
┌ Warning: from_dss: 11 parsing and conversion diagnostic record(s) in 5 class(es) (full list on net["_meta"]["powerio_warnings"]):
│ EMIT.BMOPF.FIELD_DROPPED: `units` has no place in the BMOPF schema; dropped from 3 elements
│ EMIT.BMOPF.FIELD_DROPPED: `units` has no place in the BMOPF schema; dropped from 3 elements
│ EMIT.BMOPF.FIELD_DROPPED: 4 occurrences, e.g. `kv` has no place in the BMOPF schema; dropped from 3 elements
│ EMIT.BMOPF.FIELD_DROPPED: 3 occurrences, e.g. voltage source source: `angle` has no place in the BMOPF schema; dropped from the output
│ EMIT.BMOPF.RETAINED_SOURCE_ONLY: 3 occurrences, e.g. transformer dx1: no_load_shunt have no single_phase slot in BMOPF schema 0.1.0; kept under extras.transformer
└ @ BMOPFTools ~/work/BMOPFTools.jl/BMOPFTools.jl/src/io/from_dss.jl:853
SWER zones : 2
split-phase zones : 1
lv_1 nominal (V) : 240.0analyze recognises the topology directly: the single-wire, transformer-isolated sections are flagged I.PROV.SWER_ZONE, and the centre-tap-fed section as split-phase.
for f in report.findings
f.code in ("I.PROV.SWER_ZONE", "I.PROV.SPLIT_PHASE_ZONE") &&
println(f.code, " — ", f.message)
endI.PROV.SWER_ZONE — Galvanic zone anchored at bus 'swer_0' is single-wire and transformer-isolated — a SWER (single-wire earth-return) section.
I.PROV.SPLIT_PHASE_ZONE — Galvanic zone anchored at bus 'lv_2' is split-phase (fed by center_tap transformer 'dx2'): legs ["a", "b"] are anti-phase about the centre-tap neutral.
I.PROV.SWER_ZONE — Galvanic zone anchored at bus 'lv_1' is single-wire and transformer-isolated — a SWER (single-wire earth-return) section.2. Stress it into a realistic feeder
The committed fixture is a deliberately small canonical case (it is also a unit-test network). Real SWER lines run tens of kilometres on a high-resistance conductor and carry an aggregated peak up to the isolating-transformer rating. We stretch the backbone to ~96 km and use a realistic 2.5 Ω/km conductor, and size the two distribution transformers to their customers (the canonical stubs ship at a token rating) — plain edits to the network dictionary, which doubles as a tour of the data model.
for (_, l) in net["line"]; l["length"] *= 8.0; end # 12 km → 96 km
net["linecode"]["swer"]["R_series_1_1"] = 2.5 / 1000 # Ω/m
# Right-size the transformers (the OPF enforces each nameplate `s_rating` as a
# per-winding apparent-power cap, so an undersized unit is flagged as overloaded
# rather than silently run past its rating). The distribution transformers are
# sized to the ~40 kW leg they each serve; the isolating transformer must carry
# the whole 80 kW peak PLUS the reactive support the compensator (below) pushes
# through it, so it is the larger 150 kVA unit.
for (_, sub) in net["transformer"], (id, x) in sub
id == "dx1" && (x["s_rating"] = 63_000.0) # single-ended distribution xfmr
id == "dx2" && (x["s_rating"] = 50_000.0) # split-phase distribution xfmr
id == "iso" && (x["s_rating"] = 150_000.0) # isolating xfmr (peak + reactive)
end
println("backbone length (km): ", sum(l["length"] for l in values(net["line"]))/1000)backbone length (km): 96.0The LV taps must stay inside the Australian AS IEC 60038:2022 supply-voltage range (230 V nominal, −10 %/+10 % → 207–253 V) — the same window that inverter standards such as AS/NZS 4777.2 key their responses off; we hold the 12.7 kV backbone to ±10 %.
function set_limits!(n)
for b in ("lv_1", "lv_2")
nph = count(t -> t != "n", n["bus"][b]["terminal_names"])
n["bus"][b]["vpn_min"] = fill(207.0, nph)
n["bus"][b]["vpn_max"] = fill(253.0, nph)
end
for b in ("swer_0", "swer_1", "swer_2")
n["bus"][b]["v_min"] = [11430.0]; n["bus"][b]["v_max"] = [13970.0]
end
n
endNext, the measurement helper and the operating points. vpn reads the phase-to-neutral magnitude out of a solved result — the quantity the limits bind. An operating point is just a scaled copy of the loads: the fixture ships 5 kW of canonical load, so scale(16.0) lifts it to an 80 kW aggregated peak — the whole of it crossing the 150 kVA isolating transformer (real SWER peaks are transformer-limited), while the two distribution transformers (63 and 50 kVA above) each carry their ~40 kW leg. scale(2.0) is a 10 kW light-load trough. The OPF enforces every transformer's nameplate as a hard per-winding apparent-power cap, so the dispatch we compute below respects the thermal limits as well as the voltage window — no operating point runs a transformer past its rating. That coupling is the point of the isolating transformer's headroom: it has to carry the peak and the reactive support the compensator injects to hold the far-end voltage.
using JuMP, Ipopt, Printf
const OPT = optimizer_with_attributes(Ipopt.Optimizer, "print_level" => 0)
# Phase-to-neutral voltage magnitude at bus `bid`, terminal `ph`, from a result.
vpn(res, bid, ph) = (b = res["bus"][bid];
abs((b[ph]["vr"] + im*b[ph]["vi"]) -
(haskey(b, "n") ? b["n"]["vr"] + im*b["n"]["vi"] : 0.0 + 0im)))
# Operating point = the fixture's loads scaled by K (deepcopy → scenarios stay independent).
scale(K) = (n = deepcopy(net); for (_, ld) in n["load"]
ld["p_nom"] = ld["p_nom"].*K; ld["q_nom"] = get(ld,"q_nom",[0.0]).*K; end; n)The overvoltage driver is rooftop PV at the lv_1 tap. 55 kW — roughly a dozen 5 kW rooftop systems behind one tap — pushes ~45 kW back up the line against the 10 kW trough. The IBR dict pins the unit at full, unity-power-factor output, so the power flow sees a fixed injection rather than a dispatchable device:
function add_pv!(n, kw)
n["ibr"] = get(n, "ibr", Dict{String,Any}())
n["ibr"]["pv"] = Dict{String,Any}(
"bus" => "lv_1", "terminal_map" => ["a","n"], "topology" => "SINGLE_PHASE",
"prime_mover" => "PV",
"s_max" => [kw*1100.0], # VA rating: 10 % headroom over P, typical sizing
"p_min" => [kw*1000.0], # p_min = p_max pins P at full output —
"p_max" => [kw*1000.0], # a fixed injection, not a decision variable
"q_min" => [0.0],
"q_max" => [0.0], # q pinned to 0: unity power factor
"p_avail" => [kw*1000.0]) # irradiance-limited available power (= P here)
return n
endFinally the two candidate devices. The fixed reactor is a constant inductive shunt (no decision variable), at 120 kVAr sized by trial so the light-load PV overvoltage comes back inside 253 V. The controllable compensator is an IBR with p = 0 and reactive power free in a band, which the OPF dispatches (STATCOM-like); its 180 kVAr band gives headroom in both directions so the voltage limits, not the device rating, are what bind. Both sit at the SWER end, swer_2 — the classic location for a SWER line reactor. Vpf solves a power flow; Vopf solves an OPF and returns the voltage plus the compensator dispatch, erroring readably if the solve fails.
add_reactor!(n, kvar) = (n["shunt"] = get(n, "shunt", Dict{String,Any}());
n["shunt"]["reac"] = Dict{String,Any}("bus"=>"swer_2", "terminal_map"=>["a"],
"B_1_1" => -kvar*1000.0 / 12700.0^2); n) # fixed inductive susceptance (S)
add_comp!(n, kvar) = (n["ibr"] = get(n, "ibr", Dict{String,Any}());
n["ibr"]["comp"] = Dict{String,Any}("bus"=>"swer_2", "terminal_map"=>["a","n"],
"topology"=>"SINGLE_PHASE", "s_max"=>[kvar*1000.0], "p_min"=>[0.0],
"p_max"=>[0.0], "q_min"=>[-kvar*1000.0], "q_max"=>[kvar*1000.0],
"p_avail"=>[0.0]); n)
Vpf(n) = vpn(solve_pf(n; optimizer=OPT, per_unit=true), "lv_1", "a")
function Vopf(n)
r = solve_opf(set_limits!(n); optimizer=OPT, per_unit=true)
st = r["termination_status"]
st in ("LOCALLY_SOLVED", "OPTIMAL") || error("OPF did not solve: status = ", st)
vpn(r, "lv_1", "a"), r["ibr"]["comp"]["a"]["qg"]/1000
end3. The two failure modes
A determined power flow (solve_pf) at each operating point shows the feeder fall out of the AS IEC 60038:2022 supply-voltage window at both ends of its operating range.
v_peak_nc = Vpf(scale(16.0))
v_pv_nc = Vpf(add_pv!(scale(2.0), 55.0))
println("PEAK demand lv_1 = ", round(v_peak_nc, digits=1),
" V (< 207 → UNDERVOLTAGE)")
println("LIGHT load + 55 kW PV lv_1 = ", round(v_pv_nc, digits=1),
" V (> 253 → OVERVOLTAGE)")PEAK demand lv_1 = 202.3 V (< 207 → UNDERVOLTAGE)
LIGHT load + 55 kW PV lv_1 = 255.2 V (> 253 → OVERVOLTAGE)4. A fixed reactor cannot serve both
A fixed shunt reactor sized to cure the light-load PV overvoltage absorbs reactive power all the time — so at peak demand it deepens the undervoltage. This is the exact mechanism the North Jericho study gives for why fixed reactors cap SWER capacity.
v_peak_fr = Vpf(add_reactor!(scale(16.0), 120.0))
v_pv_fr = Vpf(add_reactor!(add_pv!(scale(2.0), 55.0), 120.0))
println("PEAK + fixed 120 kVAr reactor lv_1 = ", round(v_peak_fr, digits=1), " V (WORSE)")
println("PV + fixed 120 kVAr reactor lv_1 = ", round(v_pv_fr, digits=1), " V (better)")PEAK + fixed 120 kVAr reactor lv_1 = 188.5 V (WORSE)
PV + fixed 120 kVAr reactor lv_1 = 236.3 V (better)5. One controllable compensator, optimally dispatched
Replace the fixed reactor with a controllable compensator and let solve_opf choose its reactive output subject to the voltage limits. The same device now injects vars at peak and absorbs vars under PV — the degree of freedom that a fixed setting lacks.
vp, qp = Vopf(add_comp!(scale(16.0), 180.0))
vl, ql = Vopf(add_comp!(add_pv!(scale(2.0), 55.0), 180.0))
println("PEAK + compensator lv_1 = ", round(vp, digits=1),
" V (OPF injects ", round(qp, digits=0), " kVAr)")
println("PV + compensator lv_1 = ", round(vl, digits=1),
" V (OPF absorbs ", round(ql, digits=0), " kVAr)")PEAK + compensator lv_1 = 219.4 V (OPF injects 119.0 kVAr)
PV + compensator lv_1 = 246.8 V (OPF absorbs -52.0 kVAr)Both operating points are now inside the AS IEC 60038:2022 window, served by one device, only because the OPF dispatches it per operating point.
Why optimisation matters
The table is computed from the results above — the same live numbers, side by side:
@printf("%-18s | %-14s | %-20s | %s\n",
"operating point", "no control", "fixed 120 kVAr", "compensator (OPF)")
println("-"^85)
@printf("%-18s | %6.1f V under | %6.1f V worse | %6.1f V ok (%+.0f kVAr)\n",
"peak demand", v_peak_nc, v_peak_fr, vp, qp)
@printf("%-18s | %6.1f V over | %6.1f V ok | %6.1f V ok (%+.0f kVAr)\n",
"light + 55 kW PV", v_pv_nc, v_pv_fr, vl, ql)operating point | no control | fixed 120 kVAr | compensator (OPF)
-------------------------------------------------------------------------------------
peak demand | 202.3 V under | 188.5 V worse | 219.4 V ok (+119 kVAr)
light + 55 kW PV | 255.2 V over | 236.3 V ok | 246.8 V ok (-52 kVAr)Voltage regulation is the SWER capacity limit, and no single static setting spans the operating range — the optimiser's per-operating-point reactive dispatch is what unlocks capacity (the controllable-reactor capacity gains of the North Jericho study). On a high R/X SWER feeder reactive support is a limited lever, which is itself worth seeing in the numbers; where it runs out, the same OPF machinery sizes and dispatches real-power DERs and smart-IBR Volt-var.
The DER placement tutorial sites and sizes DERs and shows how the binding constraint flips; the VVWO tutorial puts distributed smart-IBR Volt-var/Volt-watt control inside the OPF, so every rooftop PV does autonomously what the lumped compensator did here. The runnable version of this page is examples/swer_tutorial.jl.