State Estimation and Measurements Reference

Public API

Sparlectra.MeasurementType
Measurement

Generic state-estimation measurement model.

Fields:

  • typ: Measurement type.
  • value: Measured value (Vm in p.u., powers in MW/MVar, Va in degrees).
  • sigma: Standard deviation in measurement units.
  • weight: Weight used in WLS (1/sigma^2).
  • active: If false, measurement is ignored by estimator.
  • busIdx: Bus index for bus measurements.
  • branchIdx: Branch index for branch flow measurements.
  • direction: Branch direction :from or :to, otherwise :none.
  • linkIdx: Bus-link index for link flow measurements (SE phase 3). A link-referenced flow measurement never enters the WLS estimator: the link is not part of the Ybus, so it constrains only the post-SE flow allocation (calcLinkFlowsSE!).
  • id: Optional measurement identifier.
source
Sparlectra.MeasurementTypeType
@enum MeasurementType

Supported measurement types for the first WLS state-estimation implementation.

ImagMeas is a current-magnitude measurement in ampere, in two variants: the branch variant (branchIdx + direction) and the bus-referenced shunt-bay variant (busIdx only, SE phase 2) measuring the current of the shunt bay at that bus. Current magnitudes never carry observability: the observability checks exclude them, and the estimator gates them by iteration count and by the value >= 3 sigma rule (see runse!).

ShuntQMeas is the reactive power of a shunt bay in MVar (busIdx referenced, sign convention identical to the solved q_shunt results). It is the direct measurement that releases a shunt susceptance for estimation (SE phase 2, case A) and the carrier of the derived pseudo-measurements from the shunt back-calculation (case B, deriveShuntPseudoMeasurements!).

IaMeas is a PMU current-phasor ANGLE in degrees at a branch end (or the shunt bay, same variants as ImagMeas), referenced to the PMU time base: the common reference-angle offset alpha applies exactly as for VaMeas. A current angle is meaningless near zero current, so an IaMeas row is active only while the predicted current magnitude passes I >= 3 * sigma_I_ref (the paired ImagMeas sigma at the same end, else the config floor state_estimation.ia_current_floor_A); it shares the ImagMeas iteration gate and, like ImagMeas, never carries observability in this stage (a linear PMU-only estimation that would use current phasors for observability is future work).

source
Sparlectra.addCurrentPhasorMeasurement!Method
addCurrentPhasorMeasurement!(measurements; net, i_A, ia_deg, sigmaI=10.0, sigmaIa=0.05, direction=:from, branchNr=nothing, fromBus=nothing, toBus=nothing, busName=nothing, active=true, idPrefix="PMU")

Append a full PMU current phasor as the ImagMeas + IaMeas pair (mirror of addPmuPhasorMeasurement! for voltages). Returns (imag, ia).

source
Sparlectra.addIaMeasurement!Method
addIaMeasurement!(measurements; net, value, sigma, direction=:from, branchNr=nothing, fromBus=nothing, toBus=nothing, busName=nothing, active=true, id="")

Append a PMU current-phasor ANGLE measurement in degrees, PMU-referenced like VaMeas (the common reference-angle offset alpha applies). Same two location variants as addImagMeasurement! (branch end or shunt bay). The row is active in the solve only while the predicted current magnitude passes the floor gate (see IaMeas); like ImagMeas it never carries observability.

source
Sparlectra.addImagMeasurement!Method
addImagMeasurement!(measurements; net, value, sigma, direction=:from, branchNr=nothing, fromBus=nothing, toBus=nothing, busName=nothing, active=true, id="")

Append a current-magnitude measurement in ampere. Two variants:

  • Branch current: identified by branchNr or a unique fromBus/toBus branch pair; direction selects the measured branch end (:from/:to).
  • Shunt-bay current (SE phase 2): identified by busName alone (mutually exclusive with the branch keywords); measures the current drawn by the shunt at that bus. Errors when the bus carries no shunt.

Current magnitudes are auxiliary measurements: they must only supplement power measurements, never replace them. The observability checks exclude them, and the estimator activates them only from the configured iteration on (state_estimation.imag_activation_iteration) and only when value >= 3 sigma holds.

source
Sparlectra.addMeasurement!Method
addMeasurement!(measurements; typ, value, sigma, active=true, busIdx=nothing, branchIdx=nothing, direction=:none, id="")

Append a state-estimation measurement to measurements and return it.

source
Sparlectra.addMeasurementNoise!Method
addMeasurementNoise!(net::Net; stddev, relativeSigma = true, sigmaFloor, rng, sigmas_from_rows = true) -> Vector{Measurement}

Perturb the measurement values net already holds with Gaussian noise, in place, and return the perturbed rows.

This is the counterpart to generating a set: it needs no power flow and no truth state, because it works on the VALUES that are there. That matters for a set delivered with a case file, which is typically noise-free: an estimation on ideal values returns J = 0 by construction, and the only way to a realistic run used to be regenerating the whole set from a fresh solve.

Each row is drawn as value + sigma * randn(rng). sigmas_from_rows = true (the default) uses each row's OWN sigma, which is what the set already declares about its accuracy; with false the sigma comes from stddev (relative to the reading when relativeSigma, with the per-type floor). A row with a virtual sigma (a zero-injection constraint, sigma <= 1e-6) is never touched: it states a physical law, not a reading.

source
Sparlectra.addPflowMeasurement!Method
addPflowMeasurement!(measurements; net, value, sigma, direction=:from, branchNr=nothing, fromBus=nothing, toBus=nothing, linkNr=nothing, active=true, id="")

Append an active-power flow measurement identified by branchNr or a unique fromBus/toBus branch pair, or, with linkNr, on a bus link (SE phase 3): positive from link.fromBus to link.toBus, direction = :from only. A link flow measurement never enters the WLS estimator; it is consumed by the post-SE flow allocation (calcLinkFlowsSE!).

source
Sparlectra.addPinjMeasurement!Method
addPinjMeasurement!(measurements; net, busName, value, sigma, active=true, id="")

Append an active-power injection measurement identified by busName.

source
Sparlectra.addPmuPhasorMeasurement!Method
addPmuPhasorMeasurement!(measurements; net, busName, vm_pu, va_deg, sigmaVm=0.002, sigmaVa=0.02, active=true, idPrefix="PMU")

Append a complete PMU voltage-phasor measurement — magnitude and angle — for one bus and return the pair (vmMeas, vaMeas).

The magnitude enters as an ordinary VmMeas (p.u.); PMU accuracy is expressed solely through the tight default sigmaVm. The angle enters as a VaMeas (degrees, referenced to the common PMU time base; see the pmu_ref_offset handling in runse!).

source
Sparlectra.addQflowMeasurement!Method
addQflowMeasurement!(measurements; net, value, sigma, direction=:from, branchNr=nothing, fromBus=nothing, toBus=nothing, linkNr=nothing, active=true, id="")

Append a reactive-power flow measurement identified by branchNr or a unique fromBus/toBus branch pair, or, with linkNr, on a bus link (SE phase 3, see addPflowMeasurement! for the link conventions).

source
Sparlectra.addQinjMeasurement!Method
addQinjMeasurement!(measurements; net, busName, value, sigma, active=true, id="")

Append a reactive-power injection measurement identified by busName.

source
Sparlectra.addShuntQMeasurement!Method
addShuntQMeasurement!(measurements; net, busName, value, sigma, active=true, id="")

Append a shunt-bay reactive-power measurement in MVar for the shunt at busName (sign convention identical to the solved q_shunt results). Errors when the bus carries no shunt. This is the direct measurement that releases a shunt susceptance for estimation (SE phase 2, case A; see setShuntEstimation!).

source
Sparlectra.addVaMeasurement!Method
addVaMeasurement!(measurements; net, busName, value, sigma, active=true, id="")

Append a bus voltage-angle measurement (PMU synchrophasor) identified by busName. value and sigma are in degrees, referenced to the common PMU time base (see the pmu_ref_offset handling in runse!).

source
Sparlectra.addVmMeasurement!Method
addVmMeasurement!(measurements; net, busName, value, sigma, active=true, id="")

Append a bus voltage-magnitude measurement identified by busName.

source
Sparlectra.addZeroInjectionMeasurements!Method
addZeroInjectionMeasurements!(measurements; net, sigma=ZERO_INJECTION_SIGMA, busNames=nothing, busIdxs=nothing, active=true, idPrefix="ZI") -> Vector{Measurement}

Append active- and reactive-power zero-injection pseudo-measurements for the selected buses and return the newly added measurements.

Selection rules:

  • If busIdxs is provided, those indices are used.
  • Else if busNames is provided, names are resolved to indices.
  • Else passive buses are detected automatically via findPassiveBuses(net).

These pseudo-measurements are the current way to encode equality constraints P_inj = 0 and Q_inj = 0 in the WLS estimator.

source
Sparlectra.deriveShuntPseudoMeasurements!Method
deriveShuntPseudoMeasurements!(net; sigmaFloor=nothing) -> Vector{Measurement}

Shunt back-calculation (SE phase 2, case B): a measurement-preprocessing step that runs BEFORE runse! and derives a ShuntQMeas pseudo-measurement (id prefix SHDERIV) for every in-service shunt whose bay has

  • an active shunt-bay current measurement (bus-referenced ImagMeas),
  • NO active direct ShuntQMeas (a direct measurement takes precedence, case A), and
  • an active VmMeas at the bus. The voltage enters Q = -B V^2 squared, so a measured voltage is a hard prerequisite; without one the shunt is skipped with a warning, never computed from nominal voltage.

Derivation per shunt: U_kV = vn_kV * vm_meas, S_MVA = sqrt(3) U_kV I_A / 1000, magnitude sqrt(max(S^2 - P^2, 0)) with the sign taken from the model susceptance (reactor versus capacitor, q_shunt convention).

Deliberate 0.10.0 restriction: no bay active-power measurement type exists, so the bay P is always taken as 0 and the propagated sigma is doubled to cover the neglected active part. For reactors and capacitor banks P is small against Q and the error is secondary; for filter circuits with a substantial active component the derived value is biased, use a direct ShuntQMeas (case A) there instead.

The sigma comes from first-order error propagation over sigma_I and sigma_Vm; sigmaFloor optionally sets a minimum sigma so the derived pseudo-measurement cannot outweigh real measurements. Returns the added measurements.

source
Sparlectra.findPassiveBusesMethod
findPassiveBuses(net; atol=1e-9, includeSlack=false) -> Vector{Int}

Return bus indices that have no generation, no load, and no shunt contribution within the given tolerance atol.

This is useful for state-estimation workflows where passive / transit buses are often modeled through zero-injection pseudo-measurements.

source
Sparlectra.generateMeasurementsFromPFMethod
generateMeasurementsFromPF(net; kwargs...) -> Vector{Measurement}

Generate synthetic measurements from the current solved network state.

Keyword options:

  • includeVm, includePinj, includeQinj, includePflow, includeQflow
  • includeImag: add branch current-magnitude measurements in ampere for both ends (default false; auxiliary only, see addImagMeasurement!)
  • includeShuntQ: add shunt-bay reactive-power measurements in MVar for every in-service shunt (default false; sign convention of the q_shunt results)
  • includeVa: add PMU voltage-angle measurements (degrees, default false)
  • vaBusIdxs: restrict Va measurements to these bus indices (default: all buses)
  • vaRefOffsetDeg: common angle offset added to all generated Va values, emulating a PMU time base that differs from the slack reference
  • noise: add Gaussian noise if true
  • stddev: dictionary from MeasurementType => sigma
  • relativeSigma: interpret the stddev values as FRACTIONS of the true measured value (0.01 = 1 percent of reading) instead of absolute units. This keeps one accuracy setting meaningful across voltage levels: 1 percent of a 400 MW flow and of a 4 MW flow both get a class-appropriate sigma, where an absolute 2 MW would be tight at 400 kV and absurd at 30 kV. Per row sigma = max(fraction * |value|, sigmaFloor[type]); the floor guards near-zero readings (a zero flow must not get weight infinity). VaMeas stays absolute in degrees even in relative mode (an angle passes through zero, a fraction of it is meaningless).
  • sigmaFloor: the per-type floors for relativeSigma (default measurementSigmaFloors())
  • rng: random number generator
source
Sparlectra.measurementSigmaFloorsMethod
measurementSigmaFloors(; kwargs...) -> Dict{MeasurementType,Float64}

Per-type sigma floors for generateMeasurementsFromPF(relativeSigma = true): a relative sigma is max(fraction * |value|, floor), so a near-zero reading never gets a near-zero sigma (weight 1/sigma^2 would explode). Units are the measurement units: pu (vm), MW/MVar (pinj, qinj, pflow, qflow, shuntq), ampere (imag), degrees (va, also the absolute sigma in relative mode).

The power floors model the RANGE term of a transducer class (error = percent of reading plus a range contribution) and double as a numerical guard: a floor that is too small turns every zero-injection bus into a near-constraint and visibly slows the flat-start Gauss-Newton (MiniGrid: 32 iterations at 0.01 MW versus 11 at the 0.05 MW default).

source
Sparlectra.measurementStdDevsMethod
measurementStdDevs(; vm=0.005, pinj=1.0, qinj=1.0, pflow=1.0, qflow=1.0, va=0.02, imag=10.0, shuntq=1.0)

Create default standard-deviation map for synthetic measurement generation. va is the PMU voltage-angle standard deviation in degrees; typical PMU accuracy is 0.01 to 0.05° (IEEE C37.118 TVE < 1 %). imag is the current-magnitude standard deviation in ampere; the 10 A default matches a class 1 instrument on a measuring range of a few hundred ampere (typical HV line current transformer). shuntq is the shunt-bay reactive-power standard deviation in MVar.

source
Sparlectra.readMeasurementsCSV!Method
readMeasurementsCSV!(net; file, replace=true) -> NamedTuple

Read a measurement CSV v1 file (see writeMeasurementsCSV) into net.measurements. The import is ATOMIC: the whole file is parsed and validated first, and any error (unknown version comment, unknown type name, malformed row, unresolvable bus/branch/link) aborts with a line-precise message (file:line: reason) leaving net.measurements untouched; there is never a partial import. replace = true (default) replaces the stored measurements, replace = false appends.

Returns (counts = Dict{MeasurementType,Int}, total, skipped); skipped counts empty and comment lines after the version line.

source
Sparlectra.setMeasurementsFromPF!Method
setMeasurementsFromPF!(net; kwargs...)

Replace the network's measurements with a set generated from its solved power flow (see generateMeasurementsFromPF).

source
Sparlectra.writeMeasurementsCSVMethod
writeMeasurementsCSV(net; file) -> NamedTuple

Write net.measurements as a measurement CSV v1 file:

# sparlectra-measurements v1
type,bus,from_bus,to_bus,link_nr,direction,value,sigma,active,id

One row per measurement; exactly one location group per row (bus for bus measurements including the shunt-bay ImagMeas, from_bus/to_bus plus branch_nr for branch measurements, link_nr for link flow measurements). Buses are written by NAME (matched whitespace-insensitively on read) or, with busReference = :mrid, by their preserved CGMES mRID (ENTSO-E UUID; name fallback where none is recorded; MATPOWER/DTF nets have no mRIDs and always use bus numbers/names). The reader resolves names, component ids/names, and mRIDs. Values use round-trip-exact Float64 formatting (decimal point, UTF-8), so readMeasurementsCSV! restores a bitwise-equal measurement vector. Returns (count = number of rows written,).

branch_nr carries the branch index and disambiguates parallel branches between the same bus pair (routine in CGMES nets); the reader verifies it against the named endpoints. Files written without the column (first-cut v1 header) are still read, with pair-based resolution that rejects parallels (same rule as addPflowMeasurement!). headerComments lines are written as # comments after the version line (free-form metadata, e.g. the transformer tap positions a generated set is based on); the reader skips them.

source
Sparlectra.SEResultType
SEResult

Result container for the first classical WLS state-estimation run.

vaRefOffsetDeg is the estimated common PMU reference-angle offset α in degrees (the slack-bus angle expressed in the PMU time base). It is nothing when no offset state was part of the estimation, i.e. when there are no active VaMeas measurements or pmu_ref_offset = :off.

shuntEstimates (SE phase 2, case A) carries one row per shunt released via setShuntEstimation!: (busIdx, busName, B_model, B_est, delta, frozen), susceptances in pu. Frozen rows (no direct measurement, or B column not observable) keep B_est = B_model. nothing when no shunt is released. Each estimated B consumes one degree of freedom (it counts as a state in ν = m − n).

robustRows (SE phase 4, robust = true) lists every measurement that left stage 0 of the two-stage R modification in the FINAL iteration: (measurement_index, id, stage, t, sigma_factor) with t = |z - h|/sigma (original sigma) and sigma_factor = sigma_mod/sigma. nothing when the robust mode is off. The modification changes only the solve weights; objectiveJ, the residuals, and every diagnostic statistic stay on the original sigmas.

source
Sparlectra.evaluate_global_observabilityMethod
evaluate_global_observability(net, measurements; kwargs...) -> NamedTuple

Evaluate global observability on active measurements using the finite-difference measurement Jacobian.

Includes global redundancy metrics

  • redundancy = r = m - n
  • redundancy_ratio = ρ = m / n
  • dof = ν = m - n (a COUNT difference: for an observable set it equals m - rank(H); for an unobservable set it can be negative and then reads as a shortfall, not a redundancy)

Quality classes:

  • :good: observable and no critical single measurement
  • :critical: observable, but at least one single critical measurement (or ν <= 0)
  • :not_observable: not observable

For a not-observable set the result additionally names the dark states in unobservable_state_columns (state columns touched by the null space of H); empty when observable. This is the rigorous per-state answer that the column-restricted local check cannot give (see evaluate_local_observability).

Current-magnitude measurements (ImagMeas) are excluded before the Jacobian is built: currents are auxiliary measurements that must never carry observability, so the verdict states whether the system is observable from the power and voltage measurements alone.

The verdict is a two-stage check (SE phase 4):

  1. Structural: detect_ac_islands on the contracted SE net. More than one synchronous island containing measured buses yields quality = :not_observable with :structural_islands in notes, regardless of numeric rank (no measurement ties the islands' angle references together).
  2. FD-aware numeric rank: the Jacobian is built by forward differences, so its error floor is O(jacEps), not machine epsilon; an eps-scale SVD tolerance would count FD noise as rank. With tol = nothing the rank tolerance therefore defaults to state_estimation.rank_tol_factor * jacEps * sigma_max (factor default 10.0). An explicitly passed tol wins.
source
Sparlectra.evaluate_local_observabilityMethod
evaluate_local_observability(net, measurements, stateCols; kwargs...) -> NamedTuple

Evaluate local observability on selected Jacobian columns (stateCols).

Procedure:

  1. Build global Jacobian H from currently active measurements.
  2. Keep only rows that have at least one nonzero entry in the selected columns. These rows correspond to measurements that are locally sensitive to the requested states.
  3. Evaluate observability/redundancy on the reduced matrix Hlocal.

Returned NamedTuple extends global metrics with:

  • rows: selected row indices (within global active-Jacobian row numbering)
  • stateCols: copied input state-column selection.

Interpretation:

  • :good means local states are observable with positive redundancy and no single critical measurement.
  • :critical means still observable but vulnerable to a single outage (or ν <= 0).
  • :not_observable means local states cannot be uniquely reconstructed.

Limitation: this column-restricted test is NECESSARY but not sufficient; a positive verdict can be wrong when the touching rows couple the selected states to neighbor states that are themselves undetermined. The rigorous per-state answer is unobservable_state_columns from evaluate_global_observability.

Like the global check, current-magnitude measurements (ImagMeas) are excluded before the Jacobian is built (currents must never carry observability), and with tol = nothing the rank tolerance is FD-aware (rank_tol_factor * jacEps * sigma_max of the local submatrix; see evaluate_global_observability). The structural island stage is deliberately global-only: a column selection inside ONE island is perfectly observable even when the net contains further measured islands.

source
Sparlectra.evaluate_local_observability_matrixMethod
evaluate_local_observability_matrix(H, stateCols; tol=nothing) -> NamedTuple

Evaluate local observability on a matrix H restricted to selected stateCols.

Limitation: this column-restricted submatrix test is NECESSARY but not sufficient; a positive verdict can be wrong when the touching rows couple the selected states to neighbor states that are themselves undetermined (the rigorous per-state answer is unobservable_state_columns from the global check).

source
Sparlectra.evaluate_observability_matrixMethod
evaluate_observability_matrix(H; tol=nothing) -> NamedTuple

Evaluate global observability and single-row criticality directly on a matrix H (without building a network model).

The result includes unobservable_state_columns::Vector{Int}: for a numerically NOT observable H, the state columns with a component above tolerance in any null-space basis vector, i.e. exactly the states no measurement pins down (their union partitions the network into observable islands). Empty for observable systems; computed only on the not-observable path (a dense null-space probe, fine at workshop and distribution-network sizes).

source
Sparlectra.measurement_jacobianMethod
measurement_jacobian(net; flatstart=true, jacEps=1e-6, pmuRefOffset=...) -> NamedTuple

Build the measurement Jacobian H of the ACTIVE measurements on net, labeled for humans: the matrix the observability checks and the WLS normal equations run on, with one described row per measurement and one described column per state.

Returns (H, rows, cols):

  • H::Matrix{Float64}: m x n finite-difference Jacobian at the flat (or stored) start state, the same evaluation evaluate_global_observability uses.
  • rows: one NamedTuple per active measurement, (index, type, location, sigma); index is the position in net.measurements, location names the bus (injections, voltages) or the oriented branch (flows).
  • cols: state-column labels in Jacobian order, "Va(bus)" for every non-slack bus, then "Vm(bus)" for every bus, plus "alpha" when PMU Va measurements activate the reference-offset state.

Errors when no active measurement exists. Intended for measurement-matrix reports and placement studies; see the state-estimation suite summary and the workshop's observability deep dive.

source
Sparlectra.numerical_observableMethod
numerical_observable(H; tol=nothing) -> Bool

Numerical observability test on a Jacobian-like matrix H. Returns true when rank(H) == n (full column rank).

source
Sparlectra.print_se_diagnosticsMethod
print_se_diagnostics(io, diag; topN=10)

Pretty-print diagnostics from validate_measurements or runse_diagnostics including:

  • explanation of global_consistency
  • tabular measurement ranking
  • BAD/OK marker per measurement
  • optional rerun comparison if present
source
Sparlectra.readSEStateCSV!Method
readSEStateCSV!(net; file) -> NamedTuple

Read an SE state CSV (see writeSEStateCSV) written for the SAME case, write the estimated voltages into net, and register the state so runpf_from_se! can start from it. Buses are matched by name; a bus missing from the file or an unknown bus name aborts with a line-precise error.

source
Sparlectra.runpf_from_se!Function
runpf_from_se!(net, maxIte, tolerance=1e-8, verbose=0; mode=:se_state, kwargs...) -> NamedTuple

Run a power flow that starts from the last state-estimation result of net (the SE-to-PF chain). Requires a preceding runse!(...; updateNet = true) on this net or a state restored via readSEStateCSV!; a clear error otherwise.

Modes:

  • :se_state (default): the PF starts from the ESTIMATED VOLTAGES; the model injections stay authoritative, so the measurement/model difference goes into the (possibly distributed) slack. This is the semantics of the power_flow.start_mode.profile_source = state_estimation value.
  • :se_snapshot: the PF additionally takes the NODAL BALANCES from the estimation: the bus load aggregates of the WORKING net are replaced by gen_static - inj_est for the run and restored afterwards; the persistent model is never mutated (same protection philosophy as updateShunts). With consistent PV setpoints the PF then converges in 0 or 1 iterations and the slack pickup stays below tolerance. Config value profile_source = se_snapshot.

kwargs are forwarded to runpf! (the flat start is forced off; the estimated voltages ARE the start). Returns (iterations, erg, converged, mode, slack_pickup_mw, slack_pickup_mvar); the slack pickup is the PF slack injection minus the estimated injection at the slack bus.

source
Sparlectra.runse!Method
runse!(net; kwargs...) -> StateEstimationResult

Run the weighted-least-squares state estimation on the network's measurements, island-wise, and write the estimated state back when updateNet is set. The keyword form reads its defaults from the active configuration.

source
Sparlectra.runse_diagnosticsMethod
runse_diagnostics(net, measurements; max_eliminations=3, kwargs...) -> NamedTuple

Extended diagnostics workflow around validate_measurements with sequential bad-data elimination (identification stage 1):

While the χ²-like 3σ band test fails, a suspicious measurement (|rn| >= normalizedThreshold) exists, and fewer than max_eliminations measurements have been removed, the top suspicious measurement is deactivated and the diagnostics rerun. Zero-injection pseudo-measurements (id prefix ZI) and near-exact measurements (sigma <= 1e-6) are never eliminated.

Returns (diagnostics, rerun, eliminations, stop_reason, final_diagnostics):

  • diagnostics: the initial validate_measurements report
  • eliminations: one trace row per elimination (elimination, measurement_index, id, typ, normalized_residual_before, wii, skipped_unlocalizable, objective_before, objective_after). wii is the localizability of the removed row and skipped_unlocalizable counts the suspects that were passed over in that round BECAUSE they are not localizable, so the trace shows why the largest normalized residual was not the one removed.
  • stop_reason: :consistent (band test passed), :no_suspicious_left, :no_localizable_suspect (suspects remain, but none of them is localizable: the data cannot say which row is wrong, which points at a measurement gap around those rows rather than at bad data), :max_eliminations, or :not_converged
  • final_diagnostics: the report after the last elimination (equal to diagnostics when nothing was eliminated)
  • rerun: backward-compatible view of the FIRST elimination (deactivated_measurement_index, deactivated_measurement_id, diagnostics), nothing when no elimination happened

Backward compatibility: deactivate_and_rerun = true is kept as an alias for max_eliminations = 1 (exactly one elimination pass, result also in rerun). Unlike the old single pass, the loop only eliminates while the band test fails; a suspicious measurement in an already consistent set is reported but not removed.

source
Sparlectra.se_viewMethod
se_view(net) -> NamedTuple

Static SE view of the network (SE phase 3): reports how the estimator will treat controllers, shunts, links, and measurements, WITHOUT mutating net.

Guarantee: runse! never invokes the outer control loop. Every controller is frozen at its current operating point; the state estimation is a snapshot. The concept transformation table applies: OLTC/PST/Schraegregler act as transformers with fixed taps, SVC/STATCOM and reactors/capacitor banks act as shunts (their susceptance is estimable per phase 2, case A), and series compensation acts as a fixed branch impedance.

Returned fields:

  • controllers: one row per registered outer controller (name, type, enabled), all frozen during SE
  • shunts: one row per shunt released via setShuntEstimation! (busIdx, busName, status) with status = :released (has an active direct bay measurement), :frozen_no_measurement, or :injection_mode_rejected
  • links: (closed_clusters, open_links); each closed cluster is (representative, members) in original bus indices, and the SE runs on the contracted net that fuses each cluster onto its representative
  • excluded_measurements: the notes of the measurement remapping (link-referenced flow measurements, partial-cluster injections, collapsed branches, aggregations), reasons as in runse!

See print_se_view for a formatted report.

source
Sparlectra.structural_observableMethod
structural_observable(H) -> Bool

Structural observability test on a Jacobian-like matrix H. Returns true when the maximum bipartite matching size equals the number of state columns n.

source
Sparlectra.summarize_se_diagnosticsMethod
summarize_se_diagnostics(diag) -> NamedTuple

Create a compact interpretation summary for a diagnostics object returned by validate_measurements or runse_diagnostics.

global_consistency is interpreted as:

  • true: SE converged and objective is inside χ²-like 3σ plausibility band
  • false: either non-convergence or implausibly large objective
source
Sparlectra.validate_measurementsMethod
validate_measurements(net, measurements; kwargs...) -> NamedTuple

Run state-estimation diagnostics on currently active measurements and return a machine-readable report with:

  • global bad-data consistency check (global_consistency)
  • χ²-like objective plausibility summary
  • largest-normalized-residual ranking with the residual-sensitivity diagonal wii and a localizable flag (wii > wiiThreshold, default 0.3, the established literature threshold)
  • suspicious measurement list (threshold-based)
  • optional residual-correlation columns (reportResidualCorrelation, default from state_estimation.report_residual_correlation): per row the maximum |k_ij| over all partners plus a warning above 1/sqrt(2) (the result 7, simple-redundant group)

Current-magnitude measurements below their 3 sigma value gate are excluded, matching the estimator's own activation rule, so the report describes the measurement set the estimator actually used. With active ImagMeas rows the residual-sensitivity and correlation figures become load-flow dependent they hold for the estimated operating point.

measurement_index in the ranking rows is the position in the caller's measurement vector, with one sentinel: 0 marks a LINKAGG cluster aggregate (SE phase 3) that has no single source row. Check >= 1 before indexing the measurement vector with it.

source
Sparlectra.writeSEStateCSVMethod
writeSEStateCSV(net; file) -> NamedTuple

Write the last SE start state of net (registered by runse!(...; updateNet = true) or readSEStateCSV!) as a CSV artifact:

# sparlectra-se-state v1
bus,vm_pu,va_deg,pinj_MW,qinj_MVar

One row per bus (bus by name). This is the persistence half of the SE -> PF chain: a later power-flow run (possibly in another process) restores the state with readSEStateCSV! and starts via runpf_from_se!. Errors when no SE state is registered for net.

source
Sparlectra.calcMachineTrafoTapFromSEMethod
calcMachineTrafoTapFromSE(net; trafo, v_machine_pu = nothing, p_mw = nothing, q_mvar = nothing) -> NamedTuple

Back-calculate the tap position of a MACHINE (generator step-up) transformer after a state estimation. Mutually exclusive with a tap release on the same transformer: a released tap is estimated, a machine tap is reconstructed here, never both.

A machine terminal is invisible to the estimator (the AVR sets its voltage, nothing measures it independently), so taking both the terminal voltage AND the terminal injection from the SE would circularly reproduce the model tap. The non-circular information is: the estimated NETWORK-side voltage (from runse!(...; updateNet = true)), the known machine terminal voltage magnitude (the AVR setpoint vm_pu of the generator prosumer, or v_machine_pu), the active-power dispatch (prosumer p schedule, or p_mw), and the machine reactive power. CAREFUL with Q: under AVR voltage control the machine's Q is NOT its schedule value; pass the MEASURED machine reactive power (q_mvar, the SCADA telemetry every unit has). The prosumer q value is only a fallback for uncontrolled machines. The function solves the transformer terminal equation for the machine angle (active-power balance) nested inside a scan over the tap fraction (reactive-power balance) and reports the continuous electrical step plus the nearest mechanical step on the tap_step fraction grid. Reporting only, no write-back.

Errors: transformer released for estimation, not a machine transformer, no registered SE result, or no machine voltage/injection available.

source
Sparlectra.setTapEstimation!Method
setTapEstimation!(net; trafo, mode = :ratio, alpha_deg = nothing, enabled = true)

Release a transformer's tap position(s) as state-estimation states (0.10.0). mode = :ratio estimates the longitudinal regulator r1, mode = :pst the angle regulator r2 along the FIXED nameplate direction alpha_deg (required, typically 30/60/90; never estimated), and mode = :both the two-regulator cascade. enabled = false clears the release. The r0 split (see _tap_r0_split) runs at release time as a consistency assert between the device metadata and the branch state. Returns (branch, mode, alpha_deg, r1_0, r2_0).

source
Sparlectra.test_topology_hypothesesFunction
test_topology_hypotheses(net, measurements = net.measurements; candidates = :auto, max_candidates = 5, maxIte = ..., tol = ...) -> NamedTuple

Stage-3 topology hypothesis test: for each candidate element the service-state is TOGGLED on a working copy of the net, the estimation is re-run, and the chi-square band verdict before versus after decides whether the hypothesis explains the data. The input net is never touched (bitwise, test-enforced); the result is a RANKED RECOMMENDATION LIST and nothing is ever switched automatically.

candidates = :auto collects the stage-2 suspected stations' switchable elements first, then the stage-1 status contradictions, capped at max_candidates (cost guard; per-candidate timing is reported). An explicit vector of (kind = :branch | :link, idx) tuples is also accepted. A hypothesis is :hypothesis_supported only when the toggled run lands inside the Wilson-Hilferty band while the original run failed it :high; several supported hypotheses are flagged ambiguous.

source
Sparlectra.validate_topologyFunction
validate_topology(net, measurements = net.measurements; kwargs...) -> NamedTuple

Stage-1 topology pre-checks: pure, linear in the measurement count, no state estimation involved. Returns (findings, summary, n_checked_branches, n_checked_links) where each finding is (stage = :precheck, kind, location, evidence, severity). ADVISORY only: nothing is blocked or mutated; the caller decides what to do with the findings. Out-of-service elements are exempt from the plausibility checks; only the status-contradiction check (:open_element_with_flow) looks at them, because a measured flow over an open element IS the topology error.

Checks (thresholds as sigma multiples, keywords mirror the state_estimation.topology_* configuration):

  • :open_element_with_flow (k_open, default 4.0): an OPEN branch or link carries an active flow/current measurement with |value| > k * sigma.
  • :closed_element_without_flow (k_dead, default 3.0): a CLOSED branch whose present flow/current measurements are all below k * sigma at both ends, while at least one terminal station carries other measured flows above the threshold (lower severity; a legitimately unloaded branch with equally quiet neighbours never fires).
  • :closed_link_voltage_mismatch (k_v, default 4.0): a closed link with voltage-magnitude measurements on both sides disagreeing by more than k * sqrt(sigma_a^2 + sigma_b^2).
  • :kcl_violation (k_kcl, default 4.0): a COMPLETELY measured node (injection measurement present, every closed attached branch carries a flow measurement at this end) whose balance misses by more than k * sqrt(sum sigma^2), for P and Q separately. Partially measured nodes and nodes inside closed-link clusters are skipped, never guessed.
source

Internals

Sparlectra.ZERO_INJECTION_SIGMAConstant
ZERO_INJECTION_SIGMA

Default sigma of a zero-injection pseudo-measurement.

A passive bus injects nothing, so the row is a CONSTRAINT rather than a reading, and the sigma only says how hard it is enforced. The former 1e-6 means a weight of 1e12, and on a large network that is not "tight", it is crushing: on a 25000-bus case with 27268 such rows the estimation did not converge in 30 iterations, reported an objective of 3.8e15 per degree of freedom and losses 2069 percent above the reference. The same set with 1e-3 converges in 7 iterations to J/dof 4.97 and losses within 0.02 percent.

1e-3 still binds the bus far tighter than any real transducer (the tightest real rows in that set carry 0.01), while leaving the normal equations solvable.

source
Sparlectra._duplicate_measured_quantitiesMethod
_duplicate_measured_quantities(ms) -> Dict{String,Int}

Count, per measurement type, how many entries repeat a quantity (same type, same bus/branch/link, same direction) that an earlier entry already covers. The first occurrence of each quantity is not counted, so an empty result means the set measures every quantity at most once.

Only ACTIVE rows count: a deactivated row contributes nothing to the objective, so it never competes with the reading it sits next to.

source
Sparlectra._column_normalizedMethod
_column_normalized(H) -> SparseMatrixCSC

Normalize every column of H to unit norm for the rank test.

Rank does not change under column scaling, but its NUMERICAL determination does. Unscaled, a voltage column (entries around 1) sits next to an angle column whose entries carry the branch admittances of a 765 kV line, so the columns span orders of magnitude; sigma_max then belongs to the largest scale and genuine small singular values fall below a relative cut. That read as a rank deficit on a 25000-bus network whose set was four times overdetermined.

Rows are deliberately NOT scaled by sigma. The rank is invariant under positive row scaling, so sigma carries no information for this question, and dividing by it is actively harmful: zero-injection pseudo-measurements carry sigma 1e-6 against 0.01 to 24 for real ones, so the division lifts those rows by six orders of magnitude, they take over sigma_max, and everything else drops below the cut. Measured on that network: raw deficit 191, with row scaling 12308, with column normalization alone 0.

A column of norm zero carries no measurement at all and stays zero, so it keeps producing the rank deficit it should.

source
Sparlectra._suppression_normalized_residualsMethod
_suppression_normalized_residuals(H, r, w) -> Union{Nothing,Vector{Float64}}

Normalized residuals rn = r_i / sqrt(Omega_ii) for one suppression round, through the same _residual_diagnostics the bad-data elimination uses, so both decisions read one scale (tasksebaddatav0100).

Returns nothing when no verdict is possible: no Jacobian was kept, or the diagnostics refuse this problem size (they error rather than allocate an unaffordable dense inverse). The caller then suppresses NOTHING. Falling back to the raw ratio |r|/sigma is deliberately not offered: it is the measure this task removed, and a suppression decided on the wrong scale is worse than none, because nothing downstream shows which scale was used.

Returns rn together with the residual sensitivity wii, because the caller needs both: rn says HOW far the row is off, wii says whether that verdict can be trusted for this row at all.

The wii guard of _residual_diagnostics carries over unchanged: a row whose Omega_ii sits at the numerical floor is not localizable and gets rn = 0, so it can never be suppressed on the strength of a large raw residual.

source
Sparlectra.TapStateMapType
TapStateMap

State-map of the released transformer taps inside one SE (island) run: per released trafo the snet branch position, mode, fixed direction alpha, the state column of each regulator (0 = frozen at its r0), the r0 values, and a dedicated scratch Branch whose (tap_ratio, phase_shift_deg) are set from the cascade before every prediction (directive: the four admittance terms always come from calcAdmittance on that scratch branch, never re-derived). col0 is the first tap state column, ncols the number of tap states.

source
Sparlectra._cascade_tapMethod
_cascade_tap(br, r1, r2, alpha_deg) -> (tap_ratio, phase_shift_deg)

Cascade tap model with FIXED regulator directions:

t(r1, r2) = ratio_base / ((1 + r1) * (1 + r2 * cis(deg2rad(alpha))))

built strictly from the established convention: one calcSkewAngleTap (:reciprocal_from_side) factor per regulator (r1 longitudinal, direction 0; r2 with the nameplate direction alpha), composed onto the neutral base (ratio, angle). No re-derived admittance terms anywhere: the caller evaluates calcAdmittance on a scratch branch carrying these two fields.

source
Sparlectra._fixate_tapsMethod
_fixate_taps(x, map, net) -> (rows, fixedMap, xFixed)

After the estimation the taps are always fixed to the nearest MECHANICAL step and one more run is solved in which the tap is no state any more. This helper computes, per released transformer, the continuous (electrical) and rounded (fixed) step of each regulator on its OWN grid (regulator 1: the ratio grid tap_step with the tap_min/tap_max range; regulator 2: the phase grid phase_step_deg with its range), out-of-range flags (the fixed step is clamped into the range), the r values of the fixed position, a TapStateMap with zero state columns (all regulators frozen at the fixed position), and the state vector without the tap columns.

source
Sparlectra._is_machine_transformerMethod
_is_machine_transformer(net, k) -> Bool

Machine (generator step-up) transformer detection for the tap-release guards: one terminal bus of branch k hangs on this branch alone (no other closed branch, no closed link) and carries a generator prosumer. Estimating such a tap is pointless: the generator terminal voltage behind it is set by the machine, not observed independently, so the tap state absorbs whatever the AVR does.

source
Sparlectra._resolve_trafo_branchMethod
_resolve_trafo_branch(net, trafo) -> Int

Resolve a transformer reference to its branch index: an Int branch index, or a String matching the branch component name, component id, or a numeric index. Errors when nothing matches or the branch is not a transformer (ratio == 0 marks a line).

source
Sparlectra._tap_apply_freezesMethod
_tap_apply_freezes(map, freeze1, freeze2, net, Ybus) -> Union{Nothing,TapStateMap}

Rebuild the tap state map after guard or observability freezes: frozen regulator columns are removed (the regulator stays at its r0), and a transformer with NO free column left leaves the map entirely and gets its model stamp restored in the Ybus (bitwise identical to an unreleased transformer). Returns nothing when nothing remains released. Column numbers are re-packed from map.col0.

source
Sparlectra._tap_cut_componentMethod
_tap_cut_component(net, k) -> Union{Nothing,Set{Int}}

Bridge test for the tap-release guards: connectivity of the two terminal buses of branch k WITHOUT the branch itself, over closed branches and closed links. Returns nothing when the terminals stay connected (the transformer sits in a loop), otherwise the set of buses in the component containing toBus (the side the transformer alone ties to the rest).

source
Sparlectra._tap_guard_freezesMethod
_tap_guard_freezes(net, map, activeMeas) -> (freeze1, freeze2, reasons)

Structural guards on the released taps BEFORE the numerical observability test: a transformer that is a BRIDGE (its removal cuts the net) whose cut-off side carries no active voltage-magnitude measurement makes the tap and the downstream voltage indistinguishable; both regulators of that transformer are frozen (:radial_no_voltage_pin). reasons[j] is :ok for untouched transformers.

source
Sparlectra._tap_r0_splitMethod
_tap_r0_split(br, mode, alpha_deg; tol = 1e-12) -> (r1_0, r2_0)

Initialize the regulator states from the CURRENT branch position: with t_base = calcComplexRatio(ratio, angle) and t_cur = calcBranchRatio(br) the cascade demands v = t_base / t_cur = (1 + r1)(1 + r2 cis(alpha)). r2_0 comes from the component along cis(alpha), r1_0 from the remaining real factor. For single-regulator modes the frozen component must vanish to tol; a violation means the device metadata contradicts the branch state and errors instead of being silently absorbed.

source
Sparlectra._tap_step_limitsMethod

Per-iteration STEP LIMIT of each released regulator state, in the unit of the state itself (r1 as a tap FRACTION, r2 as the additional-voltage amplitude or the regulating-vector ratio, matching _tap_r0_split).

The limit is derived from the changer's declared mechanical band: one iteration may move a regulator by at most a quarter of its full travel. For regulator 1 the multiplier band [tap_min, tap_max] maps through the reciprocal 1/(1+r1) onto [1/tap_max - 1, 1/tap_min - 1].

Why a step limit and NOT a hard band on the state: without any bound the Gauss-Newton step drives r1 toward -1, where the cascade t = t_base/((1+r1)(1+r2 e^{jalpha})) is singular and the stamped admittance blows up (measured 2026-09-06: one weakly determined 50 kV transformer ran to -329 electrical steps on a band of about -14 to +18 and took the whole estimation down). But clamping the STATE into the band was measured to be worse than no bound at all: five transformers that had converged stuck to the lower bound and diverged. The transient excursion is part of how a weakly determined regulator finds its position, so what must be bounded is the distance ONE iteration may travel, not the region the path may visit.

source