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 legacybusTypeinput 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)Add and operate bus links
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=closedClosed 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)
endFor 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"
endtol = 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)
endCreating 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)
endLoading 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:
- identify the element to remove,
- remove it with the dedicated helper,
- mark or clear isolated buses, and
- 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. BecauseNetis immutable at the struct level, the helper acts as a guard instead of deleting the bus directly.removeBranch!,removeACLine!, andremoveTrafo!mutate the network and can create isolated buses as a side effect.markIsolatedBuses!is useful for diagnostics, whileclearIsolatedBuses!tries to remove buses that are now safe to delete.- For the exact signatures and generated API docs, see the Function Reference.
Working with links (bus couplers / sectionalizers)
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.00625For 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.02. 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.traceDirect orchestration is also available:
result = run_control!(
net;
pf_config = powerflow_config(),
control_config = control_config(),
)5. Example programs
examples/example_transformer_tap.jlMinimal setup for OLTC / PST / combined controller behavior.examples/example_transformer_phase_shift_control.jlFocused PST active-power target control example.examples/tap_control_demo_grid.jlLightweight demo that:- uses central configuration from
examples/configuration.yaml(orSPARLECTRA_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).
- uses central configuration from
Copy and edit:
examples/tap_control_demo_grid.yaml.example→examples/tap_control_demo_grid.yaml
Optional classic view:
SPARLECTRA_TAP_DEMO_CLASSIC=1 julia --project=. examples/tap_control_demo_grid.jlRunning 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
end4. 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
end5. 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)