OPF result dictionary

solve_opf returns a plain Dict{String,Any} in SI units (V, A, W, var). The structure mirrors the input network dict: top-level keys match the network's component types, and within each component the result is keyed by the component id, then by terminal name (or winding label for transformers).

result = solve_opf(net)

result["termination_status"]   # "LOCALLY_SOLVED", "INFEASIBLE", …
result["objective"]            # objective value; units depend on solve mode
result["solve_time"]           # wall-clock time in seconds

result["bus"]["b1"]["1"]["vm"] # phase-1 voltage magnitude at bus b1 (V)
result["line"]["l1"]["1"]["cr_fr"]  # real part of current at from-terminal "1" (A)
result["generator"]["g1"]["1"]["pg"]  # active power on phase "1" (W)

Because the result is a plain dict, it serializes straight to JSON. write_result writes it to a file or IO, and read_result reads it back to an identical dict:

write_result(result, "result.json")
result2 = read_result("result.json")   # result2 == result

To turn the SolutionReport from profile_solution into a Markdown file, pass a path to render_solution: render_solution(report, "report.md").

Infeasible solutions

Candidate availability is distinct from termination. primal_status and result_count describe whether values were returned. A TIME_LIMIT or ITERATION_LIMIT candidate is retained, including an INFEASIBLE_POINT for diagnostics; a dual/infeasibility certificate is not a primal operating point. When no candidate exists, variable-derived results are NaN. Solver-reported feasible means a feasible/nearly-feasible primal status, not independent validation.

profile_solution reports solver_claimed_feasible, verification_status (checks_passed, failed, or indeterminate), missing paths, and unassessed dimensions. Its compatibility feasible flag additionally requires complete assessed data and no failing checks. None of these flags certifies all network physics or optimality. Bus checks derive magnitude and angle from vr/vi; inconsistent supplied vm is an error. The default bound/phasor consistency tolerance remains 0.2%, with the documented absolute floors.

Supplied transformer coil loading

profile_solution(net, result).results[:transformer_loading] exposes per-coil loading alongside aggregates in render_solution Markdown and in the existing execute_solution_verification JSON response. It reuses the existing E.SOL.THERMAL_VIOLATION and W.SOL.THERMAL_ACTIVE Findings from solution_check; the report does not introduce a second bound checker or change its tolerances.

Each coils row identifies transformer/subtype, constrained side, positional coil, bus and terminal pair, supplied s_VA and cap_VA, utilization s/cap, headroom cap-s, comparison status and Finding codes. Derived ratios or sums that overflow Float64 are null; an overflowing group sum is incomplete, preserving JSON serialization without inventing a finite total. A null second terminal denotes the ground reference. Caps are taken from result s_max, never inferred by dividing the case's nameplate. Missing or invalid values remain null with explicit field paths; a zero cap is retained, with null utilization and finite headroom. within_reported_cap describes only the supplied comparison.

groups contains sums of apparent-power magnitudes and caps for each constrained side/winding, with aggregate and worst-coil utilization. It does not sum opposite sides, and sum(|S_coil|) is not |sum(S_coil)|. Aggregate sums require every expected coil's finite S/cap; incomplete data never becomes zero loading. Worst utilization is null when any coil has an undefined zero-cap ratio. A 100 kVA three-coil bank can show 40% aggregate loading while one coil is at 120%; the per-coil row retains its negative headroom and violation Finding.

Native subtypeResult slots representedNative nameplate meaning (context only)
single_phasefr, one index per from-side winding pairTotal nameplate shared over coils
center_tapfr/1, HV coilWhole nameplate on the HV coil
wye_deltafr, wye phase positionsTotal shared over wye coils
delta_wyeto, wye phase positionsTotal shared over wye coils
single_phase_autotransformerfr/1, through-power windingWhole regulator nameplate
open_delta_regulatorfr/1, fr/2, oriented regulating pairsWhole per-regulator nameplate on each pair
n_windingw1…wN, phase-leg indicesEach winding's own s_max shared over its legs

Slot interpretation follows the native model's declared terminal maps and neutral resolution; n-winding currently recognizes a/b/c/n as its native reader does. Unsupported subtypes, malformed mappings and unrecognized supplied S/cap slots are listed in unassessed, with no inferred physical allocation. This covers apparent-power records, not a reinterpretation of conductor-current limits.

All rows are supplied-result evidence, not independent reconstruction or authentication of solver stamping or case-to-result provenance. Candidate state is explicit: supplied_feasible_candidate is a solver claim (including a feasible time-limit candidate), diagnostic_candidate retains failed/infeasible candidate values, and no_candidate cannot establish an operating point. The latter two have unassessed comparisons even if finite diagnostic values are supplied. Termination/primal status and result count are retained. These labels do not override whole-network verification_status or certify physical feasibility.

The report neither changes ratings nor retries solves. It is separate from W.OPS.XFMR_OVERLOADED, the existing 90% downstream nominal-load estimate. Qualified per-coil preflight demand/headroom estimates are documented in Qualified nominal transformer coil headroom and remain separate from supplied results. The independent voltage/current regression for the ideal bank is case-specific evidence; it does not validate every transformer configuration.

Top-level fields

KeyTypeDescription
termination_statusStringJuMP termination status (e.g. "LOCALLY_SOLVED", "INFEASIBLE", "TIME_LIMIT")
primal_statusStringSolver status of the primal candidate
result_countIntNumber of available solver results
feasibleBoolSolver-reported primal feasibility, independent of termination
objectiveFloat64For default solve_opf, cost rate ($/h). For solve_feasibility_opf, the squared-slack objective in solver working coordinates (plus its transformer tie-break). Custom objectives retain caller-defined units.
solve_timeFloat64Solver wall-clock time (s)
busDictPer-bus, per-terminal voltage results
lineDictPer-line, per-conductor current results
switchDictPer-switch, per-conductor current results
loadDictPer-load, per-phase current and absorbed power
generatorDictPer-generator, per-phase current and produced power
ibrDictPer-IBR, per-phase current and produced power
transformerDictPer-transformer, per-winding-side currents
voltage_sourceDictPer-source, per-phase slack current and imported power
initialisationDictPer-bus, per-terminal Ipopt start values (see below)
lossesDictNetwork-total active/reactive losses; per-element losses live under each line/transformer (see below)

bus — voltages

result["bus"][bus_id][terminal] => Dict

All voltages are phase-to-ground (i.e. referenced to the global ground at potential 0 V). The rectangular components vr/vi are the primary variables solved by the OPF; vm and va are derived for convenience.

FieldUnitDescription
vrVReal part of complex voltage
viVImaginary part of complex voltage
vmVVoltage magnitude: √(vr² + vi²)
varadVoltage angle: atan(vi, vr)

Grounded terminals (listed in perfectly_grounded_terminals or fixed by the voltage source neutral) are present in the result with vr = vi = vm = 0, va = 0.

The neutral terminal voltage is an explicit variable. In a balanced network it is close to zero; in an unbalanced network it reflects the neutral shift.

line — terminal currents

result["line"][line_id][terminal_name] => Dict

Conductors are keyed by terminal name taken from terminal_map_from of the input line. A line with terminal_map_from = ["1","2","n"] produces result keys "1", "2", "n".

The reported quantities are the total per-end currents: the series current plus that end's π-model shunt half-section, expressed as the current flowing out of the bus into the branch. This is exactly the quantity the thermal magnitude limit is enforced on (see Lines).

FieldUnitDescription
cr_frAReal part of total current leaving the from-bus into the line
ci_frAImaginary part of total current leaving the from-bus
cr_toAReal part of total current leaving the to-bus into the line
ci_toAImaginary part of total current leaving the to-bus
cm_frACurrent magnitude at from-end: √(cr_fr² + ci_fr²)
cm_toACurrent magnitude at to-end: √(cr_to² + ci_to²)

KCL sign convention: current is positive flowing out of the bus into the branch. Internally the OPF solves a single series-current variable per conductor with c_to = −c_fr; the shunt currents are linear functions of the bus voltages and are added here to form the totals.

cm_fr ≠ cm_to when the linecode has a nonzero shunt admittance (π model), because the two ends draw different shunt currents. For purely series lines (no shunt) the totals reduce to the series current and cm_fr = cm_to.

Besides the per-conductor terminal keys, the line dict carries two reserved keys: "ground" — the device's net current into earth (cg_r, cg_i, cgm [A]), nonzero only with a phase-to-ground π-shunt — and "loss", the element's active/reactive losses (see losses).

switch — switch currents

result["switch"][switch_id][terminal_name] => Dict

Conductors are keyed by terminal name from terminal_map_from. Open switches have all currents fixed to zero; they appear in the result as cr = ci = cm = 0.

FieldUnitDescription
crAReal part of current (positive into bus at from-side)
ciAImaginary part of current
cmACurrent magnitude: √(cr² + ci²)

Switches are ideal (lossless and voltage-coupling): cr_from = -cr_to exactly. Only the from-side current is stored.

load — absorbed power

result["load"][load_id][phase_terminal] => Dict

Loads are keyed by phase terminal name (the neutral terminal, if present, carries no independent current variable and is absent from the result). For a WYE load with terminal_map = ["1","2","3","n"] the result has keys "1", "2", "3".

The power fields confirm that the constant-power constraints were satisfied at the solved operating point.

FieldUnitDescription
crdAReal part of phase current drawn by the load
cidAImaginary part of phase current drawn by the load
pdWActive power absorbed: Δvr·crd + Δvi·cid
qdvarReactive power absorbed: Δvi·crd − Δvr·cid

Δv = v_phase − v_neutral for WYE/SINGLEPHASE; `Δv = vpos − vnegacross the DELTA element (terminalkto terminalk mod n + 1). For a feasible solvepd ≈ pnom[k]andqd ≈ q_nom[k]`.

generator — produced power

result["generator"][gen_id][phase_terminal] => Dict

Same terminal indexing as load — phase terminals only, neutral absent.

FieldUnitDescription
crgAReal part of phase current injected by the generator
cigAImaginary part of phase current injected by the generator
pgWActive power produced: Δvr·crg + Δvi·cig
qgvarReactive power produced: Δvi·crg − Δvr·cig

When the optional s_max / i_max ratings are supplied the dispatch also respects the apparent-power circle pg² + qg² ≤ s_max² and the current-magnitude circle crg² + cig² ≤ i_max² (see Generators).

ibr — produced power

result["ibr"][ibr_id][phase_terminal] => Dict

IBRs are keyed by phase terminal name, following the IBR's topology:

  • FOUR_LEG — keyed by each phase terminal in terminal_map (the neutral, the last terminal, carries the summed return current and is absent).
  • THREE_LEG (delta) — keyed by the first terminal of each conductor pair (k, k mod n + 1); there is no neutral.
  • SINGLE_PHASE — a single key, the phase terminal terminal_map[1] (referenced against terminal_map[2]).
FieldUnitDescription
criAReal part of phase current injected by the IBR
ciiAImaginary part of phase current injected by the IBR
pgWActive power produced: Δvr·cri + Δvi·cii
qgvarReactive power produced: Δvi·cri − Δvr·cii

Δv is the topology-appropriate voltage difference: phase-to-neutral for FOUR_LEG, phase-to-reference for SINGLE_PHASE, and across the conductor pair for THREE_LEG. Sign convention matches the generator: positive pg/qg is power injected into the network. See IBRs for the constraint model (box q_min/q_max bounds, constant-power-factor coupling, the s_max apparent-power circle, and the optional i_max current-magnitude limit cri² + cii² ≤ i_max²).

transformer — winding currents

result["transformer"][xfmr_id]["fr"|"to"][k] => Dict

Transformer results use winding-side keys "fr" and "to", each mapping to a positional index string ("1", "2", ...) rather than a terminal name. The two winding sides may have different terminal maps (e.g. a wye_delta transformer has a neutral terminal on the wye side but not on the delta side), so a shared terminal-name key is not well-defined.

To map position back to terminal: position k corresponds to terminal_map_from[k] (from-side) or terminal_map_to[k] (to-side) in the input network.

Regulator subtypes index by winding, not terminal

For single_phase and single_phase_autotransformer the neutral is the return path, so positions index only the phase conductors. For open_delta_regulator the positions index the two regulators ("1", "2"), not the four bus terminals — the shared-phase straight-through wire current is internal and not reported. In these cases position k does not align with terminal_map[k].

FieldUnitDescription
crAReal part of winding current
ciAImaginary part of winding current
cmACurrent magnitude: √(cr² + ci²)
sVACoil apparent power |V_coil·I_coil*|, on the winding whose nameplate cap is active (from/HV side). Recorded from the cap's own auxiliaries, so it is exactly the quantity the constraint bounds.
s_maxVAThe per-winding nameplate cap applied to s (the per-phase share of s_rating). Present on the same winding as s.

s/s_max appear only on the coil the nameplate cap is stamped on (see current vs. apparent-power limits); the opposite side carries currents only. profile_solution compares s against s_max and reports E.SOL.THERMAL_VIOLATION / W.SOL.THERMAL_ACTIVE.

For ideal transformers the apparent power S = V·I* is conserved across windings (up to the ideal turns ratio). Series winding impedances (r_series_from, x_series_from, etc.) cause a small difference.

Alongside the "fr"/"to" winding keys, the transformer dict carries a "ground" entry (net current into earth: cg_r, cg_i, cgm [A]) and a "loss" entry (see losses).

When the transformer's tap is a free decision variable (see continuous tap optimization), the solved tap is also reported:

FieldUnitDescription
tap—Optimized dimensionless tap multiplier (single_phase, delta_wye, wye_delta)
tap_ratio—Optimized regulation ratio — scalar (single_phase_autotransformer) or [a₁, a₂] (open_delta_regulator)
tap_binding—true when the optimized tap sits at a tap_min/tap_max bound (regulation range exhausted); per-regulator vector for open_delta_regulator

These keys are present only when the tap was free (tap_min < tap_max); a fixed-tap transformer reports neither.

losses — active/reactive losses

Losses are computed exactly from the terminal-power identity S_loss = 1ᵀ S_from + 1ᵀ S_to, summed over every conductor the element drives (phases and neutral, both winding sides), using the per-device ledger of terminal-current injections built during model construction. Because the injected currents sum to zero internally, the result is independent of the ground voltage reference.

The top-level losses dict holds the network totals over all lines and transformers:

FieldUnitDescription
p_lossWTotal active power dissipated (≥ 0 for a passive network)
q_lossvarNet reactive absorption; negative when the network is net-capacitive (line charging, capacitor shunts)

The same identity is attached per element under each line and transformer as a "loss" sub-dict:

result["line"][line_id]["loss"]         => Dict
result["transformer"][xfmr_id]["loss"]  => Dict
FieldUnitDescription
p_lossWActive power dissipated by this element (≥ 0; a negative value is non-physical and is flagged by W.SOL.NEG_LOSS)
q_lossvarNet reactive absorption (negative = element is net-capacitive, e.g. line charging or transformer magnetizing)
s_throughVAThroughput scale `Σ

Switches are ideal (lossless) and carry no "loss" entry. The per-element sub-dicts are present only when the result was produced with the loss ledger (i.e. by solve_opf); they may be absent in results from other solvers.

voltage_source — slack current and grid injection

result["voltage_source"][source_id][phase_terminal] => Dict

The voltage source is the network's current slack: it fixes terminal voltages and injects the slack current that closes KCL at the source bus (see Voltage source as current slack). Results are keyed by phase terminal (neutral excluded — it carries the summed return current).

FieldUnitDescription
crAReal part of slack current injected at the phase terminal
ciAImaginary part of slack current
cmACurrent magnitude: √(cr² + ci²)
psWActive power imported into the network: Δvr·cr + Δvi·ci
qsvarReactive power imported: Δvi·cr − Δvr·ci

Positive ps/qs is power flowing into the network from the source. To obtain the total active power drawn from the grid, sum ps over all phases:

src    = result["voltage_source"]["source"]   # your source id
p_grid = sum(v["ps"] for v in values(src))

initialisation — Ipopt start values

result["initialisation"][bus_id][terminal] => Dict

The start values set on the vr/vi JuMP variables before optimize! is called. These are captured immediately after the warm-start setup and before the solver overwrites them. The section is always present (even for infeasible results), so it can be read without checking termination_status first.

FieldUnitDescription
vr_initVReal part of the start voltage
vi_initVImaginary part of the start voltage
vm_initVStart voltage magnitude: √(vr_init² + vi_init²)
va_initradStart voltage angle: atan(vi_init, vr_init)

Grounded terminals are recorded as all-zero regardless of the start-value setting, because they are fixed in the JuMP model and the solver ignores their start value.

Convergence diagnostics. profile_solution reads this section and emits warnings when the start values differ substantially from the solved values:

  • W.SOL.INIT_LEVEL_MISMATCH — a terminal where vm_init / vm_solved is outside [0.1, 10]. The most common cause is applying the source voltage magnitude to buses on a different voltage level (e.g. a 6.35 kV source voltage applied to a 230 V LV bus via flat initialization). The solver may still converge but the path is longer and local-minimum risk is higher.
  • W.SOL.INIT_LARGE_ERROR — a phase terminal where the init error exceeds 20 % of the solved voltage magnitude. The start was a poor approximation.
  • I.SOL.INIT_NEUTRAL_NONZERO — a neutral terminal with vm_init > 0. Neutral start values should be zero; a non-zero value indicates an initialization inconsistency.

To compare start and solved voltages manually:

init  = result["initialisation"]
buses = result["bus"]
for (bid, t_dict) in init
    for (t, ivals) in t_dict
        vm_s = buses[bid][t]["vm"]
        vm_i = ivals["vm_init"]
        vm_s > 0 && println("$bid.$t  ratio=$(round(vm_i/vm_s, digits=2))")
    end
end

Coordinate spaces and sign conventions

QuantitySpaceSign
Bus voltageRectangular (vr/vi), phase-to-ground—
Line currentRectangular, referenced to from-bus directionPositive leaving the bus into the line
Load currentRectangular, phase current onlyPositive flowing from bus into load
Generator currentRectangular, phase current onlyPositive flowing from generator into bus
Power (all)P = Re(V · I*), Q = Im(V · I*)Positive = absorbed (load) / produced (generator)

All phase-to-neutral voltage differences use the solved neutral voltage (explicit JuMP variable), not a forced-zero assumption. This is the correct reference for 4-wire networks where the neutral is not perfectly grounded.

Per-unit scaling

When solve_opf is called with per_unit=true the result is automatically converted back to SI before being returned. The caller always receives SI units regardless of the internal solver representation.

Interpreting optimization-profile screens

has_active_inequalities reports whether any assessed inequality binds; has_weakly_active_inequalities reports small multipliers at binding inequalities. The legacy is_opf and degenerate keys are aliases for those screens, not task classifications or proofs of non-uniqueness. Unavailable evidence is nothing. degrees_of_freedom remains the count n_variables - n_eq_constraints for compatibility; dimension_method explicitly labels it as a count proxy, not an equality-Jacobian rank computation.

strict_complementarity is a numerical screen in the solver's scaling and thresholds. Missing active duals or unassessed inequalities leave it unknown. It does not establish local uniqueness, second-order sufficiency or global optimality. The profile records its thresholds and coverage counts.

Zero ratings and numerical tolerance

A phase with s_max = 0 can return a tiny nonzero P/Q value after a numerical solve. Generator and IBR apparent-power checks use a relative tolerance of 1e-6 with an absolute floor of 1e-6 VA; excess above that tolerance remains an ERROR. The Finding records tolerance_VA and excess_VA. This is a stated verification tolerance, not an increase in the equipment rating. JSON numeric zeros are accepted whether decoded as integers or floating-point values.

Switch current magnitudes use cm in native results; line endpoint magnitudes use cm_fr and cm_to. Missing switch cm remains unassessed, while a supplied current exceeding its rating is checked normally.

For nominal-demand screening before a solve, see Qualified nominal transformer coil headroom in the analysis guide. analyze stores that estimate under preflight.transformer_coil_headroom; it is never substituted for this page's supplied-result coil loading.