Known issues and gotchas
Every item here was found by reading the source. They are documented rather than hidden because most of them fail silently — the model solves, returns a plausible number, and is wrong. Knowing about them up front is considerably cheaper than discovering them.
Critical
Calling AC_busbars_split second wipes the DC couples, and build_acdc_BuS_AC_DC then iterates an empty :dcswitch_couples, so constraint_exclusivity_dc_switch, constraint_ZIL_dc_switch, and constraint_BS_OTS_dcbranch are never posted. The model still solves; the answer is meaningless, and typically over-optimistic, because DC elements are free to connect to both halves of a busbar at once.
There is no error and no warning. If you ever reorder these calls, assert first:
@assert !isempty(data["switch_couples"])
@assert !isempty(data["dcswitch_couples"])Silent behavioural surprises
Quadratic generation costs are ignored
calc_gen_cost reads only the linear coefficient:
if length(g["cost"]) ≥ 2
JuMP.add_to_expression!(cost, g["cost"][end-1], _PM.var(pm, :pg, g_id))
endSo cost = [c₂, c₁, c₀] contributes only c₁ · Pg. Both c₂ and the constant c₀ are dropped. This is consistent with the reference paper, which zeroes quadratic terms, but if your case relies on them the objective will not be what you expect — and the objective of a BuS run will not be comparable to a PowerModelsACDC OPF on the same data, which does use the full cost curve.
Workaround: linearize your cost curves before use, or compare only against other runs from this package.
Switch ratings default to effectively infinite
Every generated switch gets psw = qsw = thermal_rating = 100.0 p.u. On a 100 MVA base that is 10 GVA — never binding on the bundled cases. If you intended switch ratings to constrain the solution, set them explicitly:
for (id, sw) in data_split["switch"]
sw["thermal_rating"] = 3.0
sw["psw"] = 3.0
sw["qsw"] = 3.0
endThe busbar-coupler penalty may suppress small savings
Each coupler carries cost = 1.0, charged as cost · (1 − z) when opened. That is deliberate — it stops the model splitting busbars for no gain — but on a case with a small absolute saving it can dominate. If splitting never occurs where you expect it, reduce the penalty on the ZIL switches and re-run.
AC_busbars_split assumes no pre-existing switches
It writes data["switch"]["1"], ["2"], … from scratch. A case that already contains switches will have them overwritten.
It also indexes data["switch"] without creating it, so if that key is absent — an unusual but possible state for a hand-built dictionary — you get a KeyError. Initialize it first:
haskey(data, "switch") || (data["switch"] = Dict{String,Any}())Some split functions mutate, some copy the original input data dictionary
| Function | Behaviour |
|---|---|
AC_busbars_split | copies |
DC_busbars_split | copies |
AC_busbar_split_AC_grid | mutates |
AC_busbars_split_ordered | mutates |
deepcopy before calling the mutating ones.
Binary results are floats
Even declared-binary variables come back as 0.9999999 or 3.2e-9. Always threshold:
is_open = sw["status"] < 0.01 # ✅
is_open = sw["status"] == 0 # ✗ will silently never fireStructural gaps
Pkg.test() still checks nothing
test/runtests.jl is a zero-byte file, so Pkg.test() passes trivially and gives no regression protection.
test/scripts/5_buses_test_case_BuS_test.jl now contains the full 5-bus busbar-splitting workflow, which is a useful worked reference, but it is a demo script rather than a test: it contains no @test assertions, nothing calls it from runtests.jl, and its data path (joinpath(@__DIR__, "data_sources", ...)) resolves to test/scripts/data_sources/ while the case files actually live in test/data_sources/.
Until that is wired up, validate changes against the published results in Formulations. The 5-bus AC-OPF objective of 194.139 $/h and the AC-BuS big-M result of 184.972 $/h are good canaries.
Big-M values are hardcoded
M_va = 2π, M_vm = 1.0, M_dc = 1.0, written inline in src/formdcgrid/acp.jl, lpac.jl, shared.jl, and dcp.jl. There is no way to configure them from the data or the call site. They are conservative, which supposedly weakens the LP relaxation and slows branch-and-bound. Tightening them is the highest-leverage performance change available, and requires editing the source. Indicator constraints seem not to speed up the formulation.
No N-1 constraints
The optimized topology is not checked against contingencies. Splitting a busbar reduces redundancy, so a topology that is optimal here may violate N-1. Listed as future work in the reference paper.
Performance troubleshooting
The MINLP will not converge
Expected behaviour above roughly 100 buses with all elements switchable. In order of effectiveness:
- Switch to
LPACCPowerModeland run an AC feasibility check afterwards. - Split one busbar rather than all of them.
- Restrict the switchable element set.
- Warm-start the ACP solve from an LPAC solution via
prepare_starting_value_dict. - Tighten the big-M constants.
Memory blow-up on large cases
Splitting all busbars in a 3120-bus network generates tens of thousands of switches. The count is Σ_b (2·n_b) + B. Split one busbar at a time.