Rectangular Power Flow Reference
Public API
Sparlectra.electricalIslandComponents — Method
electricalIslandComponents(net) -> Vector{Vector{Int}}Connected bus components of net counting both in-service branches and closed links as connections — the electrical view.
_active_ac_island_components deliberately walks branchVec only, because the solver runs on a net whose link clusters are already contracted. On a raw imported net that is misleading: parts joined by a closed switch appear as separate islands (a CGMES delivery with retained switches can show several "islands" that are one electrical island). Use this helper whenever islands are judged on a net that has not been link-merged — diagnostics, and the importer's slack decisions.
Sparlectra.electricalIslandOfBus — Method
Bus index → electrical island number (see electricalIslandComponents).
Sparlectra.mismatch_rectangular — Method
mismatch_rectangular(Ybus, V::Vector{ComplexF64}, S::Vector{ComplexF64}, bus_types::Vector{Symbol}, Vset::Vector{Float64}, slack_idx::Int) -> F::Vector{Float64}Compute the real-valued mismatch vector F(V) for the rectangular complex-state formulation with PQ and PV buses.
For each non-slack bus i:
if bustypes[i] == :PQ: ΔPi = Re(Scalc[i]) - Re(Sspec[i]) ΔQi = Im(Scalc[i]) - Im(S_spec[i])
if bustypes[i] == :PV: ΔPi = Re(Scalc[i]) - Re(Sspec[i]) ΔV_i = |V[i]| - Vset[i]
F is stacked as [ΔP2, ΔQ/ΔV2, ..., ΔPn, ΔQ/ΔVn] over all non-slack buses.
Sparlectra.UmfpackReuseNewtonContext — Type
UmfpackReuseNewtonContextReuse context for the UMFPACK backend (linear_solver = :umfpack_reuse): the symbolic analysis of the first lu factorization is reused via lu!(F, J) in subsequent iterations. Same multifrontal factorization as the default umfpack path, minus the repeated symbolic-analysis cost.
Sparlectra.solve_newton_factorized! — Method
solve_newton_factorized!(ctx, J, rhs; pattern_changed=false) -> xSolve J * x = rhs through the backend of ctx, reusing the symbolic analysis stored in the context across Newton iterations. A full analyze + factor runs when pattern_changed is set (PV↔PQ active-set switch), when no factorization exists yet, or when the structural fingerprint of J differs from the analyzed pattern; otherwise only the numeric refactorization runs. The structural guard is mandatory: silent pattern drift fed into a refactorization could produce wrong numbers instead of an exception.
Any backend failure (structure mismatch, bad pivots, singularity) resets the context and falls back to solve_sparse_system(J, rhs; context = :powerflow) so the existing UMFPACK/QR/SVD chain and the outer loop's :singular_newton_step handling keep working unchanged.
Sparlectra.classify_final_q_limits — Method
classify_final_q_limits(net, Sbus_pu, bus_types, qmin_pu, qmax_pu; q_hyst_pu, final_q_accept_pu, tol) -> NamedTupleThe one final Q-limit check every enforcement mode ends with: which PV buses lie over Qmax or under Qmin at the solved state, by how much, and what that means for the run. Judged by the SIZE of the overshoot, not by the number of buses: power_flow.qlimits.hysteresis_pu and power_flow.qlimits.final_q_accept_pu are the two thresholds, see _final_q_limit_class.
Up to 0.17.2 the active set counted every PV overshoot beyond the Newton tolerance as a remaining violation, while the classic modes stopped their outer loop at the hysteresis and ran no final check at all: the same operating point (a released machine 0.68 MVAr over Qmax inside a 1 MVAr hysteresis) was rejected by one mode and accepted by the other.
Sbus_pu are the bus injections of the solved state (V conj(Y V)), the machine output is the injection plus the bus load. Returns (rows, status, violations, bounded, within_hysteresis, max_dev_pu, buses): one row (bus, busI, side, q_pu, limit_pu, dev_pu, class) per PV bus beyond a limit by more than tol, sorted by the overshoot, status the worst class as a run status (:ok, :within_hysteresis, :bounded_q_limit_violation, :remaining_pv_q_limit_violations), buses the rows as busI:class joined by ; for the run metadata.
Sparlectra.final_q_check_line — Method
final_q_check_line(status, rows, baseMVA) -> StringThe final Q-limit check in one line, the same words in the text report, run.log, the run metadata and on the Web UI result page: the run status and one clause per row with bus, side, overshoot in pu and MVAr and class.
Sparlectra.runpf! — Function
runpf!(net, maxIte, tolerance=1e-6, verbose=0; method=:rectangular, kwargs...) -> (iterations, status)Unified AC power flow interface with the solver options as keywords. Active bus links are contracted before the solve, and the solved voltages are copied back to every bus of a link cluster. A network that splits into several AC islands is solved island by island only with islands_enabled = true; otherwise the call ends with an error.
Arguments:
net::Net: networkmaxIte::Int: maximum iterationstolerance::Float64: mismatch toleranceverbose::Int: verbosity levelmethod::Symbol: must be:rectangularautodamp::Bool: enable residual-based backtracking for rectangular Newton stepsautodamp_min::Float64: minimum automatic damping factor whenautodamp = truenewton_update::Symbol: how a Newton step is applied,:polar(default: magnitude times 1 + Re(dV/V), angle plus Im(dV/V); MATPOWER's update, keeps magnitudes in range on the large rotations of a flat start) or:rectangular(V + dV, the update up to 0.20.5)power_mode::Bool: keep the Ybus, the symbolic LU analysis and the work arrays on the net between solves, for repeated solves of one network (defaultfalse)linear_solver::Symbol: sparse linear-algebra backend for the Newton step,:umfpack_reuse(default: symbolic-analysis reuse across iterations vialu!) or:umfpack(full analysis every iteration, the historical path)qlimit_start_iter::Int: first Newton iteration where PV→PQ Q-limit switching may run in:iterationmodeqlimit_start_mode::Symbol::iteration,:auto, or:iteration_or_autostart criterion for PV→PQ switchingqlimit_auto_q_delta_pu::Float64: PV reactive-power request change threshold for automatic switching startislands_enabled::Bool: solve a network with several AC islands island by island (defaultfalse)auto_slack::Bool: promote a reference bus first when the network has none (defaultfalse)
Link flows are not computed here: calcLinkFlowsKCL! recovers them from the solved state afterwards.
Returns: (iterations::Int, status::Int) where status == 0 indicates convergence.
Sparlectra.runpf! — Method
runpf!(net; config=nothing, kwargs...) -> (iterations, status)Run the Newton-Raphson power flow with the solver options read from a configuration; the run-level values (flat start, Q-limit switching) are passed to the solver explicitly. Only the runtime keywords verbose, damp, pv_table_rows, validate_limits_after_pf, q_limit_violation_headroom, qlimit_lock_reason and performance_profile are accepted as kwargs; any other keyword is an ArgumentError, solver options belong in the configuration. The positional form runpf!(net, maxIte, ...) takes them as keywords instead.
Without an explicit config the net's own imported configuration is used (see _runpf_effective_config), and only if the net carries none does the globally active configuration apply. A full SparlectraConfig also forwards its runtime.parallel switches to the island fan-out.
Sparlectra.runpf_rectangular! — Function
runpf_rectangular!(net, maxIte, tolerance=1e-6, verbose=0; kwargs...) -> (iterations, status)Positional overload of runpf_rectangular! with the signature and solver keywords of the positional runpf!. It forwards to the keyword method on net as given: no link contraction, no island split, no auto_slack. runpf! calls it once per contracted network or island. status == 0 indicates convergence.
Sparlectra.runpf_rectangular! — Method
runpf_rectangular!(net; maxiter=20, tol=1e-8, damp=0.2, verbose=0)Run a complex-state Newton-Raphson power flow in rectangular coordinates on a Sparlectra network.
Arguments
net::Net: Network object containing bus, branch, and generation datamaxiter::Int=20: Maximum number of Newton-Raphson iterationstol::Float64=1e-8: Convergence tolerance for the LARGEST SINGLE bus mismatch (infinity norm over the stacked active/reactive residual vector; PV rows contribute their voltage residual in pu). Readable physically astol * baseMVA: 1e-8 pu equals 1 W at a 100 MVA basedamp::Float64=0.2: Damping factor for Newton step (0 < damp ≤ 1)verbose::Int=0: Verbosity level (0=quiet, 1=basic info, 2=detailed)
Returns
Tuple{Int, Int}: (iterationsused, errorcode)- errorcode: 0=converged, 1=maxiterations_reached
Details
This function implements a complete power flow solver using complex voltages in rectangular coordinates (Vr + jVi) as state variables, providing an alternative to conventional polar formulations.
Mathematical Foundation
- State vector: x = [Vr(non-slack); Vi(non-slack)] ∈ ℝ^(2(n-1))
- Complex power: S = V .* conj(Y * V) where Y is the bus admittance matrix
- PQ buses: ΔP = Re(Scalc - Sspec), ΔQ = Im(Scalc - Sspec)
- PV buses: ΔP = Re(Scalc - Sspec), ΔV = |V| - V_set
- Slack bus: Voltage held constant throughout iterations
Active Set Q-Limit Management
- PV→PQ switching: When reactive power demand violates generator Q-limits
- Optional PQ→PV re-enable: With hysteresis band and cooldown mechanisms
- Robust handling: Guards against inappropriate switching of non-generator buses
Algorithm Steps
- Initialization: Extract voltages, build Y-bus, classify bus types
- Power specification: Build S = P + jQ from network loads/generation
- Iterative solution: Newton-Raphson with mismatch function for PQ/PV constraints
- Q-limit enforcement: Active-set management during iterations
- Result update: Write final voltages and computed powers back to network
Network Integration
- Input: Uses
net.nodeVecfor bus data,net.baseMVAfor per-unit conversion - Output: Updates
node._vm_pu,node._va_deg,node._pƩGen,node._qƩGen - Q-limits: Integrates with
net.qLimitEventsandnet.qLimitLogfor tracking - Compatibility: Maintains same interface as
runpf!()for easy substitution
See Also
runpf_rectangular!(net, maxIte, tolerance, verbose; ...): compatibility overload matchingrunpf!()signaturemismatch_rectangular(): Core mismatch function for PQ/PV constraintsbuild_rectangular_jacobian_pq_pv(): Analytic Jacobian construction
Sparlectra.project_rectangular_start — Method
Selects a numerically robust rectangular voltage start vector for Newton-Raphson.
The function optionally evaluates multiple start candidates derived from the raw seed:
- sanitized raw start,
- DC-angle start,
- optional raw/DC blended starts,
- on a flat start, the ratio profile (PQ magnitudes scaled by the transformer ratios on the path from the reference).
Default behavior chooses the finite measured start candidate with the smallest residual 2-norm, and a candidate replaces the raw seed only when its 2-norm is at least 10 percent below the seed's (START_PROJECTION_MIN_IMPROVEMENT), so a solved state from the case file is not traded for a marginally different start (#465). The reported mismatch fields keep the max of the residual. Requested DC behavior is different: when requested_angle_mode == :dc, a finite and guarded DC-angle start is used as the requested baseline, and raw is used only if the DC candidate is invalid/non-finite or rejected by a start guard, or if the DC start's residual 2-norm is not at least 10 percent below the raw seed's (a solved state from the case file must not be replaced by a colder start; reason :raw_seed_closer_than_dc_start). The slack-bus complex voltage is preserved as reference in all generated candidates.
Arguments
Ybus: Bus admittance matrix (dense or sparse).Vraw::Vector{ComplexF64}: Raw complex voltage seed.S::Vector{ComplexF64}: Specified net complex injections in p.u.bus_types::Vector{Symbol}: Bus types (:Slack,:PV,:PQ, ...).Vset::Vector{Float64}: Voltage magnitude setpoints used for regulated buses.slack_idx::Int: Slack-bus index.
Keywords
enabled::Bool=false: Iffalse, returnsVrawunchanged.try_dc_start::Bool=true: Build/evaluate a DC-angle candidate.try_blend_scan::Bool=true: Evaluate blend candidates between raw and DC start.branch_guard::Bool=true: Enforce finite-voltage safety fallback to sanitized raw.measure_candidates::Bool=true: Compare candidates via mismatch metric.accept_unmeasured_dc_start::Bool=false: Allow DC candidate selection without mismatch evaluation.blend_lambdas=[0.25, 0.5, 0.75]: Blend weights for candidate generation.dc_angle_limit_deg::Float64=60.0: Absolute cap for non-slack relative DC angles.verbose::Int=0: Logging verbosity.performance_profile=nothing: Optional profiling dictionary for timing/diagnostics.ratio_profile_factors=Float64[]: per-bus factors from_ratio_profile_factors(the caller passes them only for a flat start withpower_flow.start_mode.ratio_profileon). Non-empty, the ratio-profile candidate (PQ magnitudes scaled by the factors, angles kept) is measured after the DC and blend candidates and wins under the same rule (smallest residual 2-norm, at least 10 percent below the raw seed); a requested DC angle start (requested_angle_mode = :dc) excludes it like the blends. Empty (the default), nothing is built and the selection is unchanged.dc_model=nothing: A_ProjectionDcModel(the net'sBbusandPbusinjfromassemble_dc_bbus, built by_projection_dc_model) so the DC candidate solves the same DC power flow asrundcpf!, phase-shifter injections included. Without it the candidate usesimag(Ybus)and the bus injections alone.
Returns
Vector{ComplexF64}: Selected projected start vector for the rectangular NR solve.
Internals
Sparlectra._bus_display_name — Method
Human-readable bus label: the imported original name if we kept one, else the component name.
Sparlectra._prepare_island_net — Method
Cut one island out of net as a standalone Net that the solver can run on unchanged. Every bus index is renumbered to 1..n_island, so all index-carrying containers have to be rebuilt consistently – missing one of them yields a net that looks fine and solves the wrong topology.
Sparlectra._validate_single_reference_per_island! — Method
One synchronous island carries exactly ONE angle reference. Two references in one island (typically two formerly asynchronous areas that got tied by a new AC branch while each kept its Slack, or a grid-forming HVDC converter whose island was synchronously tied) leave the angles overdetermined; the mismatch assembly would abort later with the generic unsupported-bus-type error. This shared validation fails early with an actionable message naming the island and its reference buses. Runs on the (link-merged) net of both solver paths, single- and multi-island, using the already-computed island report.
Sparlectra.detect_ac_islands — Method
detect_ac_islands(net; promote_generators = false) -> NamedTupleBuild the per-island report the solver, the diagnostics writer and the CSV artifact all share. Purely descriptive – it decides nothing, it only records what each island contains and which reference it would get (chosen_ref_bus, status, and in note the reason):
- an island with a Slack bus keeps it (
has_ref); - otherwise the bus of its best voltage-controlled unit (
promote_pv_ref); - otherwise, with
promote_generators(the solver always passes it), the bus of its best generating unit (promote_generator_ref); a static var compensator is never chosen here; - otherwise none (
missing_ref, the run is rejected).
"Best" is the stated reference priority first (ProSumer.referencePriority, CGMES semantics: the smallest positive value wins, 0 states none), then reference_candidate_rank; equal keys go to the smallest bus index.
Sparlectra._run_guarded_apslf_start — Method
_run_guarded_apslf_start(Ybus, Vraw, S, bus_types, Vset, slack_idx; enabled, order, baseMVA, verbose=0, performance_profile=nothing) -> (V, summary)Guarded start-value preconditioner ahead of the rectangular Newton-Raphson solve (power_flow.apslf_start), analogous to _run_guarded_current_iteration_start but sourcing the candidate profile from the AnalyticLoadFlow.jl-backed analytic power-series solver instead of a manual current-injection update.
APSLF is used purely as a start-value improver here, not a solver: nr_polish is always disabled (the downstream Newton-Raphson step performs the polish) and Q-limits are not considered (matching the scope of the existing current-iteration pre-solve). The candidate is only adopted when it strictly improves the rectangular mismatch relative to the incoming start profile; if AnalyticLoadFlow.jl is not loaded, apslf_solver raises its standard "not installed" error immediately (this is a hard configuration error, not a numerical guard rejection). Numerical failures of the APSLF solve itself (non-finite result, internal exception) are caught and treated as "not improved", restoring the incoming start values.
Sparlectra.build_complex_jacobian — Method
build_complex_jacobian(Ybus, V)Builds the 2n × 2n Wirtinger-type Jacobian blocks for the complex-state Newton–Raphson formulation.
Given: I = Ybus * V S = V .* conj.(I)
We construct the blocks: J11 = ∂S/∂V J12 = ∂S/∂V* J21 = ∂conj(S)/∂V J22 = ∂conj(S)/∂V*
Returns: J11, J12, J21, J22 (all full matrices, not Diagonal)
Sparlectra._branch_anomaly_diagnostics — Method
_branch_anomaly_diagnostics(net, bus_index; top_n=5) -> Vector{NamedTuple}Scan the in-service branches incident to bus_index (an internal bus index, matching net.nodeVec/V indexing) for modeling traits that commonly explain a persistently poor mismatch at that bus: zero impedance, an off-nominal transformer tap ratio, a large phase shift, or an unusually resistive (low X/R) branch. Each incident branch is scored by how many traits it triggers; rows are returned worst-first, capped at top_n (top_n < 0 returns all incident branches).
This is a passive read of already-imported network data — it does not modify net or influence solver behavior. It exists to turn "bus 42 has the worst mismatch" into "bus 42 has the worst mismatch, and its transformer branch #17 has a suspicious 0.62 tap ratio" for diagnose.log.
Sparlectra._capture_initial_residual_rows! — Method
_capture_initial_residual_rows!(profile, Ybus, V0, S, bus_types, Vset, slack_idx, net)Capture the full per-bus Newton mismatch at the start state V0 into profile[:initial_residual_rows], appending across island solves (each island subnet reports its buses under their original net-wide ids via busOrigIdxDict). Runs only when the caller set profile[:capture_initial_residual_rows] = true — the fixed-reference self-check / diagnostics path — so normal runs pay nothing. One entry per bus: original bus id and name, nominal voltage, start vm/va, the P residual, and the Q residual for PQ buses (NaN for PV and slack buses, whose second mismatch equation is a voltage setpoint, not reactive power), plus transformer-terminal and shunt counts for attribution grouping (transformer-conversion vs. shunt-conversion suspects).
Sparlectra.DistributedSlackState — Type
Solver-side distributed-slack state for one island solve.
alpha: per-bus participation,length(alpha) == n,sum(alpha) == 1(aggregated over the generators of a bus; zero for non-participants). Fixed for the whole solve — a PV→PQ switch during Q-limit handling does NOT remove a participant.lambda: the acceptedlambda_Pin p.u. (persists across the NR loop of onerunpf_rectangular!call, starting at 0.0).lambda_trial: the valuemismatch_rectangularactually reads. Step-search helpers (autodamp, trust region) evaluate trial statesV + a*δV; the matching trial lambda islambda + a*δlambda, set via_dslack_set_trial!before each trial mismatch evaluation.gen_table: generator-level rows for reporting (bus, prosumer index, raw weight, alpha share).
Sparlectra._distributed_slack_unit_shares — Method
_distributed_slack_unit_shares(net) -> (active::Bool, shares::Dict{Int,NTuple{2,Float64}})Distributed-slack participation of the last solve per unit: index in net.prosumpsVec to (alpha, dp_mw), read from the persisted participation table of the solver status. active is false when the last solve ran without the distributed slack or no solver status exists. A unit without an entry did not take part; its delivered output is its schedule.
Sparlectra._dslack_accept! — Method
Accept the staged trial: the last staged lambda_trial becomes the iterate's lambda.
Sparlectra._dslack_set_trial! — Method
Stage the trial lambda for a step of relative length a (see DistributedSlackState).
Sparlectra._island_participation_to_net — Method
_island_participation_to_net(rows, buses, prosumer_idx) -> VectorParticipation rows of one island solve in the numbering of the whole net: buses[k] is the net bus of island bus k, prosumer_idx[g] the net prosumer index of island unit g (see _island_prosumer_indices).
Sparlectra._merge_distributed_slack_diagnostics — Method
_merge_distributed_slack_diagnostics(status_build, performance_profile, net, dslack, Sbase, verbose) -> status_build#192: attach distributed-slack result metadata to the solver status NamedTuple (flat distributed_slack_* keys, following the wrong_branch_* naming of the existing metadata) and print a compact console summary at verbose > 0. The full participant table goes to the debug log only — the console stays at a few lines per the repository logging rule.
When respect_p_limits is set, each participant whose corrected output Pg + alpha_gen * lambda_P * Sbase leaves [minP, maxP] produces one WARN; the count is reported as distributed_slack_p_limit_violations. Stage 1 warns only — it does not clamp and re-solve.
Sparlectra.build_distributed_slack_state — Method
build_distributed_slack_state(net, bus_types;
p_mode, fallback, weights,
respect_p_limits, island_label) -> state | nothingDiscover participants and build the per-island alpha vector (#192).
Candidates are the generator-type prosumers at the island's REF or PV buses. Raw weights per mode: :pg_weighted scheduled Pg, :pmax_weighted maxP, :headroom_weighted max(maxP − Pg, 0), :imported ProSumer.participationFactor (MATPOWER APF / CGMES normalPF; the one mode in which a unit at a PQ bus takes part, when its factor is positive), :explicit the config table (bus name or bus index as key). Invalid candidates (missing data, weight ≤ 0, NaN/Inf) are dropped with a debug log and counted. Surviving weights are normalized to sum(alpha) = 1, aggregated per bus.
With no surviving participant, fallback = :error throws an ArgumentError naming island and mode; :ref_only returns nothing with a warning — the island then solves classically.
Sparlectra.AbstractNewtonSolverContext — Type
AbstractNewtonSolverContextCommon supertype of the factorization-reuse contexts for the rectangular Newton step (power_flow.linear_solver). A concrete context holds the reusable factorization, a fingerprint of the analyzed sparsity pattern (colptr/rowval copies), and diagnostic counters. Lifetime is one runpf_rectangular! invocation (= one island solve on the island path); a context must never be shared across islands or threads.
Sparlectra.RectangularJacobianAssembly — Type
RectangularJacobianAssemblyReusable buffers for the rectangular Jacobian build (issue #292): the COO triplet vectors, the retained SparseMatrixCSC, and a position map from emit order to nzval index. Under the structural (value-independent) sparsity pattern of the reuse backend, the emit order is identical in every iteration with an unchanged active set, so subsequent builds can overwrite nzval in place instead of allocating fresh triplets plus a new CSC. valid is cleared on PV↔PQ active-set changes (pattern changes) and by the runtime guard when a replay does not reproduce the recorded emit count.
Sparlectra.newton_linear_solver_context — Method
newton_linear_solver_context(linear_solver::Symbol)Map a validated power_flow.linear_solver value to a fresh per-solve context: :umfpack_reuse → UmfpackReuseNewtonContext, :umfpack → nothing (default direct solve_linear path).
Sparlectra._build_rectangular_final_status — Function
Build the final rectangular solver status payload.
Why
A stable status schema is required for reporting, tests, and UI consumers. This helper consolidates convergence flags, Q-limit statistics, and wrong-branch metrics into one normalized NamedTuple.
Returns
NamedTuple with:
final_reasonfinal_statusstatus(full diagnostics payload)
Sparlectra._final_q_limit_class — Method
_final_q_limit_class(dev_pu; tol, q_hyst_pu, final_q_accept_pu) -> SymbolThe class of one overshoot dev_pu (Q - Qmax or Qmin - Q, in pu) of a PV bus after the solve: :ok up to the Newton tolerance, :within_hysteresis up to the switching hysteresis (the switching logic tolerates that much on purpose, so the final check has to as well), :bounded up to final_q_accept_pu (accepted, with a warning), :violation beyond it (the run is not accepted). With both thresholds at zero the check is as strict as it was up to 0.17.2.
Sparlectra._finalize_rectangular_qlimit_summary — Method
Finalize Q-limit diagnostics after the numerical rectangular solve.
Why
Numerical NR convergence alone is not sufficient when PV Q-limits are active. This helper applies the post-solve acceptance policy for remaining PV violations:
- reject when violations remain and bounded acceptance is disabled/exceeded,
- accept with a dedicated reason when bounded acceptance is enabled.
Returns
NamedTuple with:
qlimit_summaryconvergedrejection_reason
Sparlectra._finalize_rectangular_wrong_branch_diagnostics — Method
Finalize wrong-branch diagnostics and map them to convergence policy.
Why
Wrong-branch detection is evaluated after a numerically converged state exists. This helper centralizes policy mapping (:off, :fail, :rescue) so callers receive a normalized result and consistent rejection reason.
Returns
NamedTuple with:
branch_qualityconvergedrejection_reasonwrong_branch_rescue_attemptedwrong_branch_rescue_reason
Sparlectra._store_and_print_rectangular_final_status! — Method
Store final rectangular solver status and optionally print convergence summary.
Why
Status persistence and user-visible summary should be emitted from one place to avoid divergence between stored diagnostics and console output.
Sparlectra.build_rectangular_jacobian_pq_pv — Method
build_rectangular_jacobian_pq_pv(
Ybus::SparseMatrixCSC{ComplexF64},
V,
bus_types,
Vset,
slack_idx;
)Sparlectra.build_rectangular_jacobian_pq_pv_sparse — Method
build_rectangular_jacobian_pq_pv_sparse(
Ybus::SparseMatrixCSC{ComplexF64},
V::Vector{ComplexF64},
bus_types::Vector{Symbol},
Vset::Vector{Float64},
slack_idx::Int;
) -> SparseMatrixCSC{Float64}Builds the analytic rectangular Jacobian corresponding to mismatch_rectangular using the sparsity pattern of Ybus.
State vector: x = [Vr(non-slack); Vi(non-slack)] ∈ ℝ^(2(n-1))
Residual F(V): - PQ buses: ΔPi, ΔQi - PV buses: ΔPi, ΔVi - Slack bus: no equations
Jacobian entries are derived from Si(V) = Vi * conj( (Ybus * V)_i )
Wirtinger-based identities: ∂S/∂V = diag(conj(I)) + diag(V) * conj(Ybus) ∂S/∂V* = diag(V) * conj(Ybus)
Chain rule to rectangular: ∂S/∂Vr = ∂S/∂V + ∂S/∂V* ∂S/∂Vi = j(∂S/∂V - ∂S/∂V*)
With ΔPi = Re(ΔSi), ΔQi = Im(ΔSi), ΔVi = |Vi| - Vset[i].
Returns: J :: SparseMatrixCSC{Float64} with size (2(n-1)) × (2(n-1)).
Sparlectra._active_link_representative_map — Method
Map every bus to the representative of its closed-link cluster.
Links (busbar couplers, retained CGMES switches) are impedance-less: their two buses are one electrical node. The solver cannot represent a zero-impedance branch – it would make the Ybus singular – so such clusters are contracted onto a single representative bus before the Ybus is built.
Returns a vector reps with reps[bus] = representative bus. Buses without a closed link map to themselves, so reps == 1:n means "nothing to merge".
Sparlectra._merged_pf_net — Method
Return a working net in which all closed-link clusters are contracted.
Yields (wnet, reps, has_merges). When nothing has to be merged the ORIGINAL net is returned unchanged (has_merges == false) – callers must therefore not assume they own a private copy, and must consult has_merges before syncing results back through reps.
The contraction moves every cluster's injections onto its representative and marks the remaining cluster members Isolated. Bus indices deliberately stay untouched, so the caller can map results back one-to-one via reps.
Sparlectra._rescue_config_variants — Method
Ordered rescue strategies for a non-converged AC solve. Each entry is a (name, config) pair derived from the failed run's configuration:
:alternate_start— toggle the flat-start flag: a stalled imported start often solves from flat, and a failed flat start may solve from the imported voltages.:autodamp— adaptive damping with the default floor (skipped when the failed run already used autodamp; the competing trust-region step control is disabled for this attempt).:dc_seed— flat voltage magnitudes with DC-projected start angles via the existing start-projection machinery.:settled_qlimits— the globalization package for large, Q-limit-noisy systems: Armijo merit line search, a low damping floor, and Q-limit switching held back until the reactive requests have settled (qlimits.start_mode = :auto). Measured on an 82000-bus case whose Q limits are not even binding at the solution: the shipped early switching produced 8445 PV/PQ flips and diverged, this combination converges in ~60 iterations with the limits fully enforced.
Sparlectra._rescue_kwargs_variants — Method
_rescue_kwargs_variants(kw, flatstart) -> Vector{Tuple{Symbol,NamedTuple}}The rescue ladder of _rescue_config_variants for callers that solve through the positional runpf!(net, maxIte, tol, verbose; kw...) form (the scenario engine's base case). Each entry is (name, merged) with merged the caller's own keywords kw plus the overrides of that strategy, so every setting the strategy does not name (distributed slack, auto_slack, the Q-limit settings, newton_update, power_mode, ...) stays exactly as the failed solve had it. flatstart is the flat-start flag the failed solve ran with (kw.opt_flatstart when given, else the net's own flag); the alternate start toggles it.
The strategy names, their order and the values they set are the ones of _rescue_config_variants; the reset values (trust region off, merit line search on with its defaults) are read from the configuration structs, so a changed default there reaches both ladders. Keep the two functions in step: a strategy added to one belongs in the other.
Sparlectra._runpf_effective_config — Method
_runpf_effective_config(net, config) -> Union{Nothing,PowerFlowConfig,SparlectraConfig}Which configuration a runpf!(net; ...) call actually solves under. Precedence:
- an explicitly passed
config, - the configuration the net was IMPORTED with (
Net._import_config), - the globally active configuration.
Step 2 exists since 0.10.1. Before it, a net imported under an explicit configuration was solved under the global one, silently: the Q-limit enforcement mode, the tolerance and even "Q limits off" never reached the solver, and every run looked plausible. The tell was identical residuals to the last digit across settings that cannot produce the same answer.
Sparlectra.RectangularTrustRegionCollapsed — Type
RectangularTrustRegionCollapsedThrown by choose_rectangular_trust_region_step when the trust-region radius falls below min_radius without ever accepting a step. Caught by the outer Newton-Raphson loop (rectangular_network_solver.jl) alongside the existing singular-Jacobian failure handling, and reported as non-convergence with reason :trust_region_collapsed.
Sparlectra.choose_rectangular_autodamp — Method
choose_rectangular_autodamp(Ybus, V, S, δx, F0; slack_idx, damp, autodamp_min, bus_types, Vset)Select a Newton step length for the rectangular power-flow solver by backtracking from damp toward autodamp_min. The first trial step that reduces the maximum absolute mismatch is accepted. If no trial reduces the mismatch, the smallest finite trial is returned so the solver can continue safely with a conservative step.
When merit_enabled = true (opt-in, requires autodamp = true), an Armijo sufficient-decrease criterion on the merit function f(x) = 1/2 ‖W F(x)‖² is tried first for each trial step, in the same backtracking order; see Merit-Function Line Search for the theoretical background. If active_set_changed = true (a PV/PQ switch happened this Newton iteration), the merit comparison is skipped for this call and the classic max-mismatch criterion is used instead, since the residual vector's entries change meaning across an active-set switch. If no trial satisfies Armijo, fallback_max_mismatch selects whether to fall back to the classic max-mismatch criterion (true) or directly to the conservative best-finite-trial fallback (false).
Returns (alpha, Vtrial, trial_mismatch). When merit_enabled = true, per-trial diagnostics (f_before, directional_derivative, tested/accepted λ, accept_reason) are pushed to merit_log if it is an AbstractVector.
Sparlectra.choose_rectangular_trust_region_step — Method
choose_rectangular_trust_region_step(Ybus, V, S, δx, F0, J; slack_idx, bus_types, Vset, radius_ref, min_radius, max_radius, eta_accept, shrink_factor, expand_factor, expand_threshold, step_mode, active_set_changed)Trust-region step control with two step-construction modes selected by step_mode:
:scaled(default): the full Newton stepδxis scaled down to the current radius when it exceeds it; direction unchanged. Byte-for-byte the original scaled-Newton-only implementation.:dogleg: blends the Newton direction with a steepest-descent (Cauchy) step along the dogleg path (see_rectangular_dogleg_step) so the solver can still make progress when the Newton direction degrades. See Trust-Region Step Control, "Dogleg step mode".
In both modes the trial is accepted when the ratio rho of actual to predicted merit-function reduction is at least eta_accept. Rejected trials shrink the radius and retry within the same Newton iteration (bounded by min_radius); the radius persists across iterations via radius_ref, which is mutated in place.
Reuses the exact merit function from the merit-function line search (_rectangular_merit_value, unweighted W = I) as m(x), matching m(x) = ‖F(x)‖² up to the constant 1/2 factor common to numerator and denominator of rho — no second merit computation is implemented. The dogleg gradient g = JᵀF uses the same W = I convention for consistency with m(x).
When step_mode = :dogleg and active_set_changed = true (a PV/PQ switch happened this Newton iteration), the pre-switch gradient/Cauchy point would be invalid, so — mirroring the merit-function line search's active_set_skip policy — the dogleg comparison is skipped for this call: a single scaled-Newton trial is taken at the current radius and accepted unconditionally, and the radius is left unchanged.
Throws RectangularTrustRegionCollapsed if the radius falls below min_radius without an accepted step. Returns the accepted trial voltage vector. Per-iteration diagnostics (accept_reason ∈ {:scaled, :dogleg_newton, :dogleg_interp, :dogleg_cauchy, :active_set_skip, :none}, where :none marks a collapse entry) are pushed to diagnostics if it is an AbstractVector.
Sparlectra.complex_newton_step_rectangular — Method
complex_newton_step_rectangular(
Ybus,
V,
S;
slack_idx,
damp,
bus_types,
Vset,
)Performs one Newton–Raphson step in rectangular coordinates using the analytic Jacobian that matches mismatch_rectangular.
- State: x = [Vr(non-slack); Vi(non-slack)]
- Residual: F(x) = mismatch_rectangular(...)
Sparlectra.POWER_MODE_LU_KLU_MAX_FLOPS — Constant
POWER_MODE_LU_KLU_MAX_FLOPSThe threshold of power_flow.power_mode_lu = auto: KLU's symbolic estimate of the factorization flops (klu_analyze, BTF plus AMD, no numeric work) of the first power-mode Jacobian of a network. Up to it power mode factorizes with KLU, above it with UMFPACK.
The value is fitted on measured Jacobians (structural Jacobian of the first solve, refactorization time of both backends): KLU was faster on case2869pegase (estimate 1.95e6), case9241pegase (1.29e7), mvlv29840 (1.76e6), caseACTIVSg2000 (1.13e7), case13659pegase (1.52e7) and the islands 2 and 3 of caseSyntheticUSA (2.71e7, 1.05e7); UMFPACK was faster on caseACTIVSg25k (1.26e8) and island 1 of caseSyntheticUSA (5.60e8). 5e7 lies between the two groups. The estimate does not see KLU's numeric pivoting, which is what makes KLU slow on the large cases (5762 off-diagonal pivots on case_SyntheticUSA island 1); the threshold uses the size of the problem as its stand-in.
Sparlectra.PowerModeCache — Type
PowerModeCacheWhat a power-mode solve keeps on the network (net._power_cache) for the next solve: the Ybus with the fingerprint of the data it was built from, the linear-solver context (PowerModeLuContext: symbolic analysis, factorization object, the Jacobian assembly buffers), the Newton iteration workspace, and the power_flow.power_mode_lu = auto decisions of the network (lu_memo, under lu_key; island nets of a solve record into the memo of their parent network). The Ybus is reused only when the fingerprint matches, the context re-analyses by itself when the Jacobian pattern differs (bus types, bus count), so a changed topology or a changed branch parameter never reuses stale state; the LU decision is kept across such changes (once per network). lu_source is where the LU choice of the last solve came from (:decided, :earlier_decision, :configured, :klu_not_loaded).
Sparlectra.PowerModeLuContext — Type
PowerModeLuContextThe linear-solver context of a power-mode solve: the reuse scheme of UmfpackReuseNewtonContext (one analysis per Jacobian pattern, a numeric refactorization per Newton step) with the backend chosen by power_flow.power_mode_lu. backend is :klu or :umfpack and says what fact is. With decision_pending set (auto, no decision for this network yet), the first full factorization decides (PowerModeLuDecision) and records the decision in memo under key. One context per network (per island net on the island path), never shared across tasks.
Sparlectra.PowerModeLuDecision — Type
PowerModeLuDecisionThe sparse LU power_flow.power_mode_lu = auto chose for a network (or one island of it): choice (:klu or :umfpack) and klu_est_flops, KLU's symbolic flop estimate of the Jacobian the choice was made on, compared with POWER_MODE_LU_KLU_MAX_FLOPS.
Sparlectra._power_mode_lu_select! — Method
_power_mode_lu_select!(cache, power_mode_lu) -> PowerModeLuContextSets up the linear-solver context of a power-mode solve for power_flow.power_mode_lu: :umfpack and :klu fix the backend (:klu without the loaded extension falls back to UMFPACK with a warning); :auto without the extension is UMFPACK; with it, the decision the memo holds for this network (cache.lu_key, else the whole network, key 0), or a pending decision at the first factorization when there is none. The context keeps its analysis while the backend stays; a change of backend drops the factorization, so the next Newton step factorizes afresh.
Sparlectra.power_mode_linear_solver_backend — Method
power_mode_linear_solver_backend() -> Symbol:klu when the KLU extension is loaded (using KLU in the session), so that power_flow.power_mode_lu can choose KLU; :umfpack_reuse otherwise, the only sparse LU a power-mode solve then has. Which backend a solve used is in its status (power_mode_lu_choice).
Sparlectra.reset_power_mode! — Method
reset_power_mode!(net)Drops the power-mode state of net (Ybus, factorization, workspace, the LU decisions). The next power-mode solve rebuilds it and decides the LU again. Not needed after a topology or parameter change (the fingerprint and the pattern guard cover that); for callers that want the memory back.
Sparlectra._lock_listed_pv_buses_as_pq! — Method
_lock_listed_pv_buses_as_pq!(net, bus_types, S, qmin_pu, qmax_pu, buses; verbose) -> Vector{Int}Run the PV buses listed in power_flow.qlimits.lock_pv_to_pq_buses (internal bus positions) as PQ from the start of the solve. Each listed bus that is PV in bus_types becomes PQ with its scheduled reactive generation (the sum of the generator qVal at the bus, a Q(U) unit that follows its characteristic at its Q at the held voltage, as in the bus Q band) clamped into the bus's [qmin, qmax], so the run cannot end with a reactive violation at a bus it was told not to regulate. Listed buses that are not PV (slack, PQ, already reclassified by the guard) and positions outside the network are left alone.
The conversion is logged at iteration 0 like every other setup-time reclassification (zero-range and narrow-range guard, warm active set): the side is the limit the scheduled value was clamped to, otherwise the sign of the run value (the guard's convention). The caller runs this BEFORE the PV origin mask is taken, so the PQ->PV release rule never releases a listed bus: the list is a lock, not a start state.
Returns the converted bus positions.
Sparlectra._start_guarded_buses_from_fixed_injection! — Method
_start_guarded_buses_from_fixed_injection!(V0, Ybus, S, buses; max_sweeps = 10, tol = 1e-10) -> IntStart voltage of the buses the Q-limit guard ran as PQ (buses): each one gets the voltage its own bus equation gives with the fixed injection S[bus] against its neighbours' start voltages, instead of the setpoint of a machine that cannot hold it. A machine whose reactive range is zero or narrower than min_q_range_pu is a fixed-Q injection, so its setpoint says nothing about the voltage the bus will settle at; starting the bus there leaves a reactive mismatch of the size the machine would need to hold the setpoint (measured on case_SyntheticUSA island 1: 145 pu on one zero-range 69 kV bus whose setpoint is 1.04 pu and whose bus voltage in the case file is 1.009 pu), and Newton did not converge from that start.
The equations of the guarded buses are solved together by Jacobi sweeps with every other bus held at its start value: per sweep, V[b] = (conj(S[b]) / conj(V[b]) - sum(Y[b,j] * V[j], j != b)) / Y[b,b] for each guarded bus, until the largest voltage change is below tol or max_sweeps sweeps have run. Only V0 entries of buses change. A bus with a zero diagonal or a non-finite update keeps its previous value.
Returns the number of sweeps run (0 when buses is empty).
Sparlectra._clamped_bus_physical_qgen! — Method
_clamped_bus_physical_qgen!(net, Sbus_MVA, q_consumed) -> IntWrite the physical generator Q (MVAr) to node._qƩGen of every clamped bus (PQ with an entry in net.qLimitEvents) that carries a Q(U) unit following its characteristic (see _reapply_qlimit_clamps!). The clamp record is the band value, with the follower at its Q at the held voltage; the clamped bus does not hold that voltage, so the follower runs at another Q. The physical value is the solved injection plus what the bus consumes at its solved voltage magnitude (q_consumed, MVAr per bus from _bus_consumption_MVA): loads (a Q(U) load at its characteristic) and voltage-dependent shunts. A clamped bus without such a unit keeps its clamp record, which is its physical value already.
Called after the solved voltages are written to the nodes. Returns the number of buses written.
Sparlectra._reapply_qlimit_clamps! — Method
_reapply_qlimit_clamps!(S, dQinj_dVm, net, bus_types, V) -> IntApply the Q-limit clamps again after buildControlledSVec rebuilt S for the current voltage V. With a voltage-dependent controller in the net the rectangular Newton loop rebuilds S every iteration from the units' schedules, and the rebuild dropped every clamp written before (active set make_pq!, the guard, lock_pv_to_pq_buses, the warm clamps): the regulating unit of a clamped bus was back at its scheduled Q.
A clamped bus is a bus that started the solve as PV, is PQ in bus_types and has an entry in net.qLimitEvents. Its clamp value is the generator Q every clamp site writes to node._qƩGen (pu times baseMVA), in the terms of the bus Q band: a Q(U) unit that follows its characteristic (see _buildQLimits!) is in it with its Q at the held voltage. The clamp stands for the other generator units of the bus; the followers keep their Q at V from the rebuild. Per clamped bus:
S_q = S_q(rebuild) - Q_other(V) + Q_clamp - Q_follow(Vheld)
with Q_other(V) the Q the rebuild gave the other generator units. Loads, voltage-dependent shunts and the P part stay as rebuilt. dQinj_dVm loses the derivative of the other units that carry a Q(U) controller, since their Q is fixed by the clamp.
Returns the number of clamped buses written.
Sparlectra.update_net_voltages_from_complex! — Method
update_net_voltages_from_complex!(net, V)Update the bus voltage magnitudes and angles in the network from the final complex voltages V (in per-unit).
Sparlectra._ProjectionDcModel — Type
_ProjectionDcModel(Bbus, Pbusinj)The DC model the start projection's DC candidate solves when the caller owns a Net: Bbus and Pbusinj exactly as assemble_dc_bbus returns them (series reactance over tap magnitude, phase shift as the injection vector), so the candidate is the same DC power flow rundcpf! solves. Without it the candidate falls back to imag(Ybus) and the bus injections alone (the unit-level callers without a net), which leaves the phase-shifter injections out: on case6495rte that start was wrong by 510 and 361 pu on two shifted branches and diverged where the rundcpf! seed converged.
Sparlectra._aux_bus_flat_start! — Method
_aux_bus_flat_start!(V0, net, slack_idx) -> IntFlat start of the auxiliary buses (three-winding star points and the other internal nodes an importer creates, see _is_aux_node): every in-service PQ auxiliary bus starts at the value that lets no current flow through its branch with the smallest series impedance |r + jx|, i.e. the flat value of the visible bus at the other end times the ratio step of _ratio_profile_adjacency (the same step the ratio-profile candidate multiplies along its path). The angle is the neighbour's (0 on a flat start). Visible buses keep their flat values, and branches to another auxiliary bus are not used. Applied on every flat start of the rectangular solver, with or without the start projection. Returns the number of buses set; a net without auxiliary PQ buses returns 0 before anything is built, so its start vector stays bit-identical.
Why it exists: a star point has no load and starts at 1.0 pu, but its stiffest winding has an off-nominal ratio. On the CGMES MiniGrid the two 110 kV star windings (r 0.00044 pu, ratio 0.96491) then carry 164 pu of circulating active power on a 9 MW case, and the polar Newton update diverges from that start.
Sparlectra._projection_dc_model — Method
_projection_dc_model(net::Net) -> Union{Nothing,_ProjectionDcModel}
_projection_dc_model(net::Net, bus_idx::Vector{Int}) -> Union{Nothing,_ProjectionDcModel}Assemble the projection's DC model from net through assemble_dc_bbus, the one DC assembly in the code base. Returns nothing when an in-service branch has x_pu == 0 (the DC model has no series susceptance for it and assemble_dc_bbus refuses the net); the projection then keeps the imag(Ybus) fallback and reports dc_model = :ybus_susceptance in its summary. The bus_idx variant maps the net-indexed model onto a compressed bus ordering (buildPfModel's active-bus vector, position k holds net node bus_idx[k]).
Sparlectra._ratio_profile_factors — Method
_ratio_profile_factors(net::Net, slack_idx::Int) -> Vector{Float64}Per-bus scale factors of the ratio-profile start candidate (power_flow.start_mode.ratio_profile): the product of the off-nominal winding ratios on the path from the reference bus slack_idx, found by a breadth-first search over the in-service branches (the path with the fewest branches; a line contributes 1, a transformer its LIVE ratio magnitude, abs(calcBranchRatio(br)), the same complex ratio the Ybus stamps, phase shift ignored because the candidate keeps the angles). The ratio sits on the from side (V_from / t faces the series impedance), so no current flows through an unloaded branch when |V_to| = |V_from| / |t|: stepping from the from bus to the to bus divides by |t|, the reverse step multiplies. Every bus of one voltage level therefore gets the same factor, and a flat start scaled by it carries no circulating current through the transformers. Buses the search does not reach (isolated buses, another island) keep the factor 1.0, i.e. their flat value.
Why it exists: a flat start puts the full ratio offset of every off-nominal transformer across its series impedance. On the CGMES MiniGrid the two star-equivalent 110 kV windings (r 0.00044 pu, x about 0, ratio 0.96491) then carry 164 pu of circulating active power on a 9 MW case, and the polar Newton update diverges from that start; scaled by the ratios the start mismatch drops to 0.09 pu and polar Newton converges in three steps.
Sparlectra._print_qlimit_active_set_summary — Method
_print_qlimit_active_set_summary(io::IO, status)Write a detailed Q-limit active-set convergence summary to an output stream.
Displays:
- Numerical NR convergence
- Final mismatch value
- Active-set convergence status
- Switching event counters (PV→PQ, active-set changes, re-enable events)
- Count of oscillating buses and guarded narrow-Q buses
- Final solver status symbol
Purpose
Provide a structured diagnostic report for Q-limit iterations when the rectangular solver has run. Used in verbose output and detailed solver convergence analysis. Helps users understand whether Q-limit switching was the limiting factor or whether the NR mismatch itself failed to converge.
Arguments
io::IO: Output stream (typicallystdoutor a log file)status: Status struct with fields for convergence flags, mismatch, event counts, and final status symbol.
Side effects
- Writes formatted table to
io.
Sparlectra._print_rectangular_convergence_summary — Method
_print_rectangular_convergence_summary(io::IO, status)Write a one-line convergence summary to an output stream.
Displays:
- Numerical convergence (NR mismatch) status
- Q-limit active-set convergence status
- Final overall convergence flag
- Symbolic rejection/stop reason
Purpose
Provide quick console/log summary of what halted the solver (convergence, max iter, singular Jacobian, wrong-branch, or Q-limit oscillation). Intended for solver report output, not for interactive diagnostics.
Arguments
io::IO: Output stream (typicallystdoutor a log file)status: Status struct with fieldsnumerical_converged,q_limit_active_set_ok,final_converged, andreason_text.
Side effects
- Writes formatted text to
io.
Sparlectra.rectangular_pf_status — Method
rectangular_pf_status(net::Net) -> AnyRetrieve the most recent rectangular power-flow solver status for a network.
If no solver has run on this network, returns nothing. Otherwise returns a status struct (type depends on solver implementation) containing convergence flags, iteration counts, and diagnosis data.
The status is stored as a field on the Net itself, so deepcopy(net) carries the status with the copy, and reading it is a plain field access (thread-safe for concurrent readers). It is solver state, not model data: serializers must skip it.
Arguments
net::Net: Network object to query.
Returns
- Status struct or
nothingif no status is available.
Sparlectra._bus_voltage_setpoints_from_prosumers — Method
Build per-bus voltage-magnitude setpoints for rectangular power-flow initialization.
Setpoints are resolved in explicit phases (mapping, regulator scan, fallback, fill) so timing/profiling can attribute costs to each step and diagnostics can explain where a bus setpoint came from.
Resolution precedence per bus:
- Voltage-regulating prosumer setpoint (
ps.vm_pu) if available.- Slack prosumer setpoints are considered only when
net.flatstart == true.
- Slack prosumer setpoints are considered only when
- Fallback to node voltage (
node._vm_pu) from imported/current network state. - Final fallback
1.0p.u. when node voltage is missing.
If multiple regulating prosumers define different setpoints for the same bus, the first encountered value is kept and a debug message is emitted.
Arguments
net::Net: Network model containing buses and prosumers.
Keywords
performance_profile=nothing: Optional profiling dictionary used by_perf_profile_time!for phase timing and counters.
Returns
Vector{Float64}: Voltage setpoint vector in solver bus order.
Sparlectra._check_wrong_branch_solution — Method
Convenience overload that accepts net as first positional argument.
Forwards to the keyword-based implementation to keep all decision logic in one place.
Sparlectra._check_wrong_branch_solution — Method
Evaluate a solved voltage state for wrong-branch indicators.
The check is intentionally conservative:
:failis reserved for invalid states (e.g. non-finite voltages),- suspicious but finite states are returned as
:warn, - final policy (
:warn,:fail, potential rescue path) is decided by caller logic.
When net is available, the angle heuristics (angle spread, branch angle) are evaluated only on the network's highest nominal voltage level; lower levels carry normal operating spread that would produce false SUSPECT verdicts. The voltage band (min_vm_pu, max_vm_pu) is judged on every energised bus of every level: a magnitude below 0.70 pu is a wrong branch on a 150 kV bus as much as on a 750 kV bus (case13659pegase ended with 130 buses of its 150 kV level at 0.33 pu while its three 750 kV buses were clean). The reported min_vm_pu/max_vm_pu/lowest_buses refer to all levels. Without a net every bus is in scope for every heuristic.
With a net, the angle across every in-service branch at the reference bus slack_idx is measured on every level (phase shift compensated) and returned as reference_branch_angle_deg with the branch in case bus numbers (reference_branch); above a positive max_reference_branch_angle_deg it raises reference_branch_angle_exceeded (the default 0 leaves the rule off for unit-level callers).
Returns
A normalized diagnostic NamedTuple created by _wrong_branch_result.
Sparlectra._circular_angle_spread_deg — Method
Compute circular angle spread (degrees) from a collection of angles.
Returns the minimal covering arc on the unit circle after wrap-to-180 normalization. Non-finite entries are ignored.
Sparlectra._wrap_to_180_deg — Method
Normalize an angle (degrees) to the interval (-180, 180].
Why
Wrong-branch diagnostics compare relative angles from different buses and branches. A canonical wrap prevents false spread inflation due to ±360° representation jumps.
Sparlectra._wrong_branch_result — Method
Build a normalized wrong-branch diagnostic result payload.
Why
A single NamedTuple shape keeps caller/reporting logic simple and stable, even when some diagnostics are not available (e.g., no branch checks).