Workshop

This workshop guides you through the basic steps of using Sparlectra.jl to create, manipulate, and solve power system networks.

Note: addBus! creates electrical nodes. The operational PF bus type (Slack/PV/PQ) is derived from attached prosumers. The legacy busType input is still accepted for compatibility, but does not define PF behavior.

Loading data from a file

using Sparlectra
using Logging

global_logger(ConsoleLogger(stderr, Logging.Warn))

file = "caseXYZ.m"
path = "C:/Users/YourUsername/Documents"

printResultToFile = false
tol = 1e-6
ite = 10
verbose = 0   # 0: no output, 1: iteration norm, 2: + Y-Bus, 3: + Jacobian, 4: + Power Flow

net = run_acpflow(
    max_ite = ite,
    tol = tol,
    path = path,
    casefile = file,
    verbose = verbose,
    printResultToFile = printResultToFile,

Building and extending a network from scratch

Start by creating a new network object:

using Sparlectra

net = Net(name = "example_network", baseMVA = 100.0)

Add buses

addBus!(net = net, busName = "B1", vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)
addBus!(net = net, busName = "B2", vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)
addBus!(net = net, busName = "B3", vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)
addBus!(net = net, busName = "B4", vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)
addBus!(net = net, busName = "B5", vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)

Add AC lines and transformers

addACLine!(net = net, fromBus = "B1", toBus = "B2", length = 25.0, r = 0.2, x = 0.39)
addACLine!(net = net, fromBus = "B1", toBus = "B3", length = 25.0, r = 0.2, x = 0.39)
addPIModelACLine!(net = net, fromBus = "B3", toBus = "B4", r_pu = 0.05, x_pu = 0.2, b_pu = 0.01, status = 1)

add2WTrafo!(
    net = net,
    fromBus = "B2",
    toBus = "B4",
    sn_mva = 100.0,
    vk_percent = 10.0,
    vkr_percent = 0.5,
    pfe_kw = 20.0,
    i0_percent = 0.1,
)

addPIModelTrafo!(
    net = net,
    fromBus = "B4",
    toBus = "B5",
    r_pu = 0.01,
    x_pu = 0.1,
    b_pu = 0.0,
    status = 1,
    ratio = 1.05,
)

Add loads, generators, and shunts

addProsumer!(net = net, busName = "B1", type = "ENERGYCONSUMER", p = 1.0, q = 2.0)
addProsumer!(net = net, busName = "B2", type = "ENERGYCONSUMER", p = 1.0, q = 2.0)

addProsumer!(
    net = net,
    busName = "B5",
    type = "SYNCHRONMASCHINE",
    referencePri = "B5",
    vm_pu = 1.0,
    va_deg = 0.0,
)

addProsumer!(
    net = net,
    busName = "B1",
    type = "GENERATOR",
    p = 1.1,
    q = 2.0,
    vm_pu = 1.02,
)

addShunt!(net = net, busName = "B1", pShunt = 0.0, qShunt = 1.0)

Use links to model ideal busbar couplers or sectionalizers without adding impedance to the YBUS.

linkNr = addLink!(net = net, fromBus = "B1", toBus = "B2", status = 1)
setNetLinkStatus!(net = net, linkNr = linkNr, status = 0)  # 0=open, 1=closed

Closed links are treated as ideal couplers during runpf!. Buses connected by active links share voltage magnitude and angle in the internal power-flow model. After convergence, call calcLinkFlowsKCL! to allocate and report the link exchange on the original topology.

ite, erg = runpf!(net, 25)
if erg == 0
    calcNetLosses!(net)
    calcLinkFlowsKCL!(net)
end

For a complete scenario with open and closed links, see examples/using_links.jl and the detailed notes in links.md.

Validate and solve the network

Always validate your network after making significant modifications:

result, msg = validate!(net = net)
if !result
    @error "Network is invalid: \$msg"
end
tol = 1e-6
maxIte = 10

etime = @elapsed begin
    ite, erg = runpf!(net, maxIte, tol, 0)
end

if erg != 0
    @warn "Power flow did not converge"
else
    calcNetLosses!(net)
    printACPFlowResults(net, etime, ite, tol)
end

Creating a network from scratch and exporting it to a file

using Sparlectra
using Logging

global_logger(ConsoleLogger(stderr, Logging.Info))

tol = 1e-8
ite = 10
verbose = 0
writeCase = true
print_results = true

net = Net(name = "workshop_case5", baseMVA = 100.0)

addBus!(net = net, busName = "B1",    vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)
addBus!(net = net, busName = "B2",    vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)
addBus!(net = net, busName = "B3",    vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)
addBus!(net = net, busName = "B4",    vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)
addBus!(net = net, busName = "B5", vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)

addACLine!(net = net, fromBus = "B1", toBus = "B2", length = 25.0, r = 0.2, x = 0.39)
addACLine!(net = net, fromBus = "B1", toBus = "B3", length = 25.0, r = 0.2, x = 0.39)
addACLine!(net = net, fromBus = "B2", toBus = "B4", length = 25.0, r = 0.2, x = 0.39)
addACLine!(net = net, fromBus = "B3", toBus = "B4", length = 25.0, r = 0.2, x = 0.39)
addACLine!(net = net, fromBus = "B4", toBus = "B5", length = 25.0, r = 0.2, x = 0.39)

addProsumer!(net = net, busName = "B1", type = "ENERGYCONSUMER", p = 1.0, q = 2.0)
addProsumer!(net = net, busName = "B2", type = "ENERGYCONSUMER", p = 1.0, q = 2.0)
addProsumer!(net = net, busName = "B3", type = "ENERGYCONSUMER", p = 1.0, q = 2.0)

addProsumer!(net = net, busName = "B5", type = "SYNCHRONMASCHINE", referencePri = "B5", vm_pu = 1.0, va_deg = 0.0)
addProsumer!(net = net, busName = "B1", type = "GENERATOR", p = 1.1, q = 2.0)

result, msg = validate!(net = net)
if !result
    @warn msg
    return false
end

if writeCase
    path = "C:/Users/YourUsername/Documents"
    writeMatpowerCasefile(net, path)
end

maxIte = 10
tol = 1e-6

etime = @elapsed begin
    ite, erg = runpf!(net, maxIte, tol, verbose)
end

if erg != 0
    @warn "Power flow did not converge"
elseif print_results
    calcNetLosses!(net)
    printACPFlowResults(net, etime, ite, tol)
end

Loading a file and manipulating the network

using Sparlectra

file = "case5.m"
net = run_acpflow(casefile = file)

brVec = getNetBranchNumberVec(net = net, fromBus = "1", toBus = "2")
setNetBranchStatus!(net = net, branchNr = brVec[1], status = 0)

run_acpflow(net = net)

addBusShuntPower!(net = net, busName = "1", p = 0.0, q = 1.0)

filename = "_case5a.m"
jpath = joinpath(pwd(), "data", "mpower", filename)
writeMatpowerCasefile(net, jpath)

Update component parameters

brVec = getNetBranchNumberVec(net = net, fromBus = "B1", toBus = "B2")
updateBranchParameters!(
    net = net,
    branchNr = brVec[1],
    branch = BranchModel(
        r_pu = 0.02,
        x_pu = 0.2,
        b_pu = 0.01,
        g_pu = 0.0,
        ratio = 1.0,
        angle = 0.0,
        sn_MVA = 100.0,
    ),
)

addBusLoadPower!(net = net, busName = "B1", p = 2.0, q = 1.0)
addBusGenPower!(net = net, busName = "B5", p = 3.0, q = 1.5)
addBusShuntPower!(net = net, busName = "B2", p = 0.0, q = 1.0)

Remove or isolate network elements

The practical removal workflow belongs naturally in the workshop because it is usually part of iterative model editing:

  1. identify the element to remove,
  2. remove it with the dedicated helper,
  3. mark or clear isolated buses, and
  4. validate and solve again.
removeACLine!(net = net, fromBus = "1", toBus = "2")
removeShunt!(net = net, busName = "2")
removeProsumer!(net = net, busName = "3", type = "ENERGYCONSUMER")

markIsolatedBuses!(net = net, log = true)
clearIsolatedBuses!(net = net)

result, msg = validate!(net = net)
if !result
    error("Network validation failed: \$msg")
end

ite, status, etime = run_acpflow(net = net, show_results = false)

Notes on component removal

  • removeBus! is intentionally conservative: it only checks whether a bus could be removed. Because Net is immutable at the struct level, the helper acts as a guard instead of deleting the bus directly.
  • removeBranch!, removeACLine!, and removeTrafo! mutate the network and can create isolated buses as a side effect.
  • markIsolatedBuses! is useful for diagnostics, while clearIsolatedBuses! tries to remove buses that are now safe to delete.
  • For the exact signatures and generated API docs, see the Function Reference.

For a detailed explanation of link behavior, zero-impedance loops, and pseudoinverse-based flow allocation, see links.md.

using Sparlectra

net = Net(name = "workshop_links", baseMVA = 100.0)

addBus!(net = net, busName = "Bus1",    vn_kV = 110.0)
addBus!(net = net, busName = "Bus1a",    vn_kV = 110.0)
addBus!(net = net, busName = "Bus4",    vn_kV = 110.0)
addBus!(net = net, busName = "Bus5", vn_kV = 110.0)

addPIModelACLine!(net = net, fromBus = "Bus1",  toBus = "Bus4", r_pu = 0.010, x_pu = 0.080, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "Bus1a", toBus = "Bus4", r_pu = 0.009, x_pu = 0.070, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "Bus4",  toBus = "Bus5", r_pu = 0.006, x_pu = 0.050, b_pu = 0.0)

linkNr = addLink!(net = net, fromBus = "Bus1", toBus = "Bus1a", status = 1)

addProsumer!(net = net, busName = "Bus1",  type = "GENERATOR", p = 45.0, q = 0.0, vm_pu = 1.01)
addProsumer!(net = net, busName = "Bus5",  type = "EXTERNALNETWORKINJECTION", referencePri = "Bus5", vm_pu = 1.02, va_deg = 0.0)
addProsumer!(net = net, busName = "Bus1a", type = "LOAD", p = 30.0, q = 10.0)

ite, status, etime = run_acpflow(
    net = net,
    max_ite = 25,
    tol = 1e-8,
    show_results = false,
)

setNetLinkStatus!(net = net, linkNr = linkNr, status = 0)

ite2, status2, etime2 = run_acpflow(
    net = net,
    max_ite = 25,
    tol = 1e-8,
    show_results = false,
)

report = buildACPFlowReport(
    net;
    ct = etime2,
    ite = ite2,
    tol = 1e-8,
    converged = (status2 == 0),
    solver = :rectangular,
)

println(report)
println("Link rows in report: ", length(report.links))

printACPFlowResults(net, etime2, ite2, 1e-8)

Transformer Tap Control (OLTC / PST / combined)

This section gives a compact workflow for transformer tap control and points to the dedicated runnable examples.

1. Enable tap capability on a transformer branch

tbr = getNetBranch(net = net, fromBus = "Slack", toBus = "Mid")
tbr.has_ratio_tap = true
tbr.tap_min = 0.90
tbr.tap_max = 1.10
tbr.tap_step = 0.00625

For phase-shift control (PST), configure:

tbr.has_phase_tap = true
tbr.phase_min_deg = -15.0
tbr.phase_max_deg = 15.0
tbr.phase_step_deg = 1.0

2. Add controller(s)

addTapController!(net;
    trafo = string(tbr.branchIdx),
    mode = :voltage,                      # :branch_active_power or :voltage_and_branch_active_power
    target_bus = "Load",
    target_vm_pu = 1.01,
    control_ratio = true,
    control_phase = false,
    is_discrete = true,
)

For active-power control, use target_branch = ("FromBus", "ToBus") and p_target_mw. Reported achieved_p_mw is interpreted in exactly that configured direction (from -> to).

3. Run PF including tap-control post-processing

Prefer:

ite, status, etime = run_acpflow(
    net = net,
    show_results = false,
)

This includes post-processing (losses, branch flows, link flows), so controller targets and report values stay consistent.

4. Read classic and structured reports

printTapControllerSummary(stdout, net)
report = buildACPFlowReport(net; ct = etime, ite = ite, tol = 1e-8, converged = (status == 0), solver = :rectangular)

Use report.transformer_controls for machine-readable controller rows (DataFrame-compatible), including controller type, target/achieved values, tap/phase positions, limits, and status.

Transformer control with the generic outer loop

You can also inspect the generic control result directly:

run_acpflow(net = net, show_results = false)

result = latest_control_result(net)
result.status
result.outer_iterations
result.powerflow_solves
result.controllers
result.trace

Direct orchestration is also available:

result = run_control!(
    net;
    pf_config = powerflow_config(),
    control_config = control_config(),
)

5. Example programs

  • examples/example_transformer_tap.jl Minimal setup for OLTC / PST / combined controller behavior.
  • examples/example_transformer_phase_shift_control.jl Focused PST active-power target control example.
  • examples/tap_control_demo_grid.jl Lightweight demo that:
    • uses central configuration from examples/configuration.yaml (or SPARLECTRA_CONFIGURATION_YAML),
    • reads demo-specific setpoints from examples/tap_control_demo_grid.yaml,
    • runs through run_acpflow(net = net; config = ...),
    • inspects structured output from latest_control_result(net).

Copy and edit:

  • examples/tap_control_demo_grid.yaml.exampleexamples/tap_control_demo_grid.yaml

Optional classic view:

SPARLECTRA_TAP_DEMO_CLASSIC=1 julia --project=. examples/tap_control_demo_grid.jl

Running rectangular NR with Q-limits

This section shows how to use the rectangular Newton-Raphson solver with reactive power limits.

1. Prepare or load a network

Use an existing network or build one as shown above.

2. Define PV buses and Q-limits

Define a slack prosumer and a regulating generator (PV behavior), then set Q-limits.

addProsumer!(
    net = net,
    busName = "B5",
    type = "EXTERNALNETWORKINJECTION",
    referencePri = "B5",
    vm_pu = 1.0,
    va_deg = 0.0,
)

addProsumer!(
    net = net,
    busName = "B1",
    type = "SYNCHRONMACHINE",
    p = 10.0,
    q = 10.0,
    vm_pu = 1.03,
    isRegulated = true,
    va_deg = 0.0,
    qMax = 50.0,
    qMin = -50.0,
)

3. Validate the network

result, msg = validate!(net = net)
if !result
    @error "Network validation failed: \$msg"
    return
end

4. Run the solver

maxIte = 20
tol = 1e-8
verbose = 1
damp = 0.2

etime = @elapsed begin
    ite, status = runpf!(
        net,
        maxIte,
        tol,
        verbose;
        damp = damp,
    )
end

if status != 0
    @warn "Rectangular NR did not converge (status = \$status)"
    return
end

5. Distribute bus results to prosumers

distributeBusResults!(net)

If multiple generators are connected to the same bus and one of them is at its Q-limit, Sparlectra uses a simple “water-filling” style redistribution so that:

  • total bus P/Q stays consistent with the solved power flow,
  • individual generator Q stays within its limits,
  • remaining reactive power is redistributed among non-limited generators at that bus.

6. Print results

calcNetLosses!(net)
printACPFlowResults(net, etime, ite, tol)
printProsumerResults(net)
printQLimitLog(net)

State Estimation (SE)

For state estimation workflows, observability analysis, and measurement handling, see state_estimation.md.

using Sparlectra
using Random

# 1) Build a simple 7-bus network
net = Net(name = "workshop_se_7bus", baseMVA = 100.0)

addBus!(net = net, busName = "B1", vn_kV = 110.0, vm_pu = 1.02, va_deg = 0.0)
for i in 2:7
    addBus!(net = net, busName = "B\$(i)", vn_kV = 110.0, vm_pu = 1.0, va_deg = 0.0)
end

# Ring + cross-connections
addPIModelACLine!(net = net, fromBus = "B1", toBus = "B2", r_pu = 0.010, x_pu = 0.080, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B2", toBus = "B3", r_pu = 0.011, x_pu = 0.085, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B3", toBus = "B4", r_pu = 0.012, x_pu = 0.090, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B4", toBus = "B5", r_pu = 0.010, x_pu = 0.080, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B5", toBus = "B6", r_pu = 0.011, x_pu = 0.085, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B6", toBus = "B7", r_pu = 0.012, x_pu = 0.090, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B7", toBus = "B1", r_pu = 0.010, x_pu = 0.080, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B2", toBus = "B5", r_pu = 0.009, x_pu = 0.070, b_pu = 0.0)
addPIModelACLine!(net = net, fromBus = "B3", toBus = "B6", r_pu = 0.009, x_pu = 0.070, b_pu = 0.0)

# Source / generation / loads
addProsumer!(net = net, busName = "B1", type = "EXTERNALNETWORKINJECTION", referencePri = "B1", vm_pu = 1.02, va_deg = 0.0)
addProsumer!(net = net, busName = "B3", type = "GENERATOR", p = 60.0, q = 10.0)
addProsumer!(net = net, busName = "B2", type = "LOAD", p = 35.0, q = 10.0)
addProsumer!(net = net, busName = "B4", type = "LOAD", p = 45.0, q = 15.0)
addProsumer!(net = net, busName = "B5", type = "LOAD", p = 25.0, q = 8.0)
addProsumer!(net = net, busName = "B6", type = "LOAD", p = 30.0, q = 10.0)
addProsumer!(net = net, busName = "B7", type = "LOAD", p = 20.0, q = 6.0)

ok, msg = validate!(net = net)
ok || error("Validation failed: \$msg")

# 2) Solve reference power flow
ite_pf, status_pf = runpf!(net, 40, 1e-10, 0)
status_pf == 0 || error("Power flow did not converge")

# 3) Build synthetic measurements (with light noise)
std = measurementStdDevs(vm = 1e-3, pinj = 1.0, qinj = 1.0, pflow = 0.7, qflow = 0.7)
setMeasurementsFromPF!(
    net;
    includeVm = true,
    includePinj = true,
    includeQinj = true,
    includePflow = true,
    includeQflow = true,
    noise = true,
    stddev = std,
    rng = MersenneTwister(42),
)

# 4) Check observability
gobs = evaluate_global_observability(net; flatstart = true, jacEps = 1e-6)
println("Global observability quality: ", gobs.quality)
println("Measurements: ", gobs.n_measurements, ", states: ", gobs.n_states)

# 5) Run state estimation
se = runse!(
    net;
    maxIte = 12,
    tol = 1e-6,
    flatstart = true,
    jacEps = 1e-6,
    updateNet = true,
)

println("SE converged: ", se.converged, ", iterations: ", se.iterations)
println("Final objective J: ", se.objectiveJ)

# 6) Inspect the estimated network state
printBusResults(net)
printBranchResults(net)

Building measurement sets with helper functions

If you want to assemble measurements manually, you do not have to create Measurement(...) entries yourself. Sparlectra provides helper functions that work similarly to addBus! or addACLine! and resolve bus or branch references for you.

empty!(net.measurements)

addVmMeasurement!(net; busName = "B1", value = 1.02, sigma = 0.002)
addPinjMeasurement!(net; busName = "B2", value = -35.0, sigma = 1.0)
addQinjMeasurement!(net; busName = "B2", value = -10.0, sigma = 1.0)
addPflowMeasurement!(net; fromBus = "B1", toBus = "B2", value = 22.0, sigma = 0.8, direction = :from)
addQflowMeasurement!(net; branchNr = 1, value = 7.0, sigma = 0.8, direction = :to)

obs = evaluate_global_observability(net; flatstart = true, jacEps = 1e-6)
println("Manual measurement set quality: ", obs.quality)