State Estimation and Measurements Reference
Public API
Sparlectra.Measurement — Type
MeasurementGeneric 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: Iffalse, measurement is ignored by estimator.busIdx: Bus index for bus measurements.branchIdx: Branch index for branch flow measurements.direction: Branch direction:fromor: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.
Sparlectra.MeasurementType — Type
@enum MeasurementTypeSupported 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).
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).
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.
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
branchNror a uniquefromBus/toBusbranch pair;directionselects the measured branch end (:from/:to). - Shunt-bay current (SE phase 2): identified by
busNamealone (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.
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.
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.
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!).
Sparlectra.addPinjMeasurement! — Method
addPinjMeasurement!(measurements; net, busName, value, sigma, active=true, id="")Append an active-power injection measurement identified by busName.
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!).
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).
Sparlectra.addQinjMeasurement! — Method
addQinjMeasurement!(measurements; net, busName, value, sigma, active=true, id="")Append a reactive-power injection measurement identified by busName.
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!).
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!).
Sparlectra.addVmMeasurement! — Method
addVmMeasurement!(measurements; net, busName, value, sigma, active=true, id="")Append a bus voltage-magnitude measurement identified by busName.
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
busIdxsis provided, those indices are used. - Else if
busNamesis 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.
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
VmMeasat the bus. The voltage entersQ = -B V^2squared, 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.
Sparlectra.findPassiveBuses — Method
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.
Sparlectra.generateMeasurementsFromPF — Method
generateMeasurementsFromPF(net; kwargs...) -> Vector{Measurement}Generate synthetic measurements from the current solved network state.
Keyword options:
includeVm,includePinj,includeQinj,includePflow,includeQflowincludeImag: add branch current-magnitude measurements in ampere for both ends (defaultfalse; auxiliary only, seeaddImagMeasurement!)includeShuntQ: add shunt-bay reactive-power measurements in MVar for every in-service shunt (defaultfalse; sign convention of theq_shuntresults)includeVa: add PMU voltage-angle measurements (degrees, defaultfalse)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 referencenoise: add Gaussian noise iftruestddev: dictionary fromMeasurementType => sigmarelativeSigma: interpret thestddevvalues 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 rowsigma = max(fraction * |value|, sigmaFloor[type]); the floor guards near-zero readings (a zero flow must not get weight infinity).VaMeasstays absolute in degrees even in relative mode (an angle passes through zero, a fraction of it is meaningless).sigmaFloor: the per-type floors forrelativeSigma(defaultmeasurementSigmaFloors())rng: random number generator
Sparlectra.measurementSigmaFloors — Method
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).
Sparlectra.measurementStdDevs — Method
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.
Sparlectra.readMeasurementsCSV! — Method
readMeasurementsCSV!(net; file, replace=true) -> NamedTupleRead 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.
Sparlectra.setMeasurementsFromPF! — Method
setMeasurementsFromPF!(net; kwargs...)Replace the network's measurements with a set generated from its solved power flow (see generateMeasurementsFromPF).
Sparlectra.writeMeasurementsCSV — Method
writeMeasurementsCSV(net; file) -> NamedTupleWrite net.measurements as a measurement CSV v1 file:
# sparlectra-measurements v1
type,bus,from_bus,to_bus,link_nr,direction,value,sigma,active,idOne 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.
Sparlectra.SEResult — Type
SEResultResult 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.
Sparlectra.evaluate_global_observability — Method
evaluate_global_observability(net, measurements; kwargs...) -> NamedTupleEvaluate global observability on active measurements using the finite-difference measurement Jacobian.
Includes global redundancy metrics
redundancy = r = m - nredundancy_ratio = ρ = m / ndof = ν = m - n(a COUNT difference: for an observable set it equalsm - 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):
- Structural:
detect_ac_islandson the contracted SE net. More than one synchronous island containing measured buses yieldsquality = :not_observablewith:structural_islandsinnotes, regardless of numeric rank (no measurement ties the islands' angle references together). - 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. Withtol = nothingthe rank tolerance therefore defaults tostate_estimation.rank_tol_factor * jacEps * sigma_max(factor default 10.0). An explicitly passedtolwins.
Sparlectra.evaluate_local_observability — Method
evaluate_local_observability(net, measurements, stateCols; kwargs...) -> NamedTupleEvaluate local observability on selected Jacobian columns (stateCols).
Procedure:
- Build global Jacobian
Hfrom currently active measurements. - 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.
- 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:
:goodmeans local states are observable with positive redundancy and no single critical measurement.:criticalmeans still observable but vulnerable to a single outage (or ν <= 0).:not_observablemeans 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.
Sparlectra.evaluate_local_observability_matrix — Method
evaluate_local_observability_matrix(H, stateCols; tol=nothing) -> NamedTupleEvaluate 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).
Sparlectra.evaluate_observability_matrix — Method
evaluate_observability_matrix(H; tol=nothing) -> NamedTupleEvaluate 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).
Sparlectra.measurement_jacobian — Method
measurement_jacobian(net; flatstart=true, jacEps=1e-6, pmuRefOffset=...) -> NamedTupleBuild 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 evaluationevaluate_global_observabilityuses.rows: one NamedTuple per active measurement,(index, type, location, sigma);indexis the position innet.measurements,locationnames 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 PMUVameasurements 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.
Sparlectra.numerical_observable — Method
numerical_observable(H; tol=nothing) -> BoolNumerical observability test on a Jacobian-like matrix H. Returns true when rank(H) == n (full column rank).
Sparlectra.numerical_row_redundant — Method
numerical_row_redundant(H, i; tol=nothing) -> BoolCheck if row i remains numerically redundant in H.
Sparlectra.print_se_diagnostics — Method
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
Sparlectra.print_se_view — Method
print_se_view(view; io=stdout, format=:plain)Pretty-print an se_view report (format = :plain or :markdown).
Sparlectra.readSEStateCSV! — Method
readSEStateCSV!(net; file) -> NamedTupleRead 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.
Sparlectra.runpf_from_se! — Function
runpf_from_se!(net, maxIte, tolerance=1e-8, verbose=0; mode=:se_state, kwargs...) -> NamedTupleRun 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 thepower_flow.start_mode.profile_source = state_estimationvalue.:se_snapshot: the PF additionally takes the NODAL BALANCES from the estimation: the bus load aggregates of the WORKING net are replaced bygen_static - inj_estfor the run and restored afterwards; the persistent model is never mutated (same protection philosophy asupdateShunts). With consistent PV setpoints the PF then converges in 0 or 1 iterations and the slack pickup stays below tolerance. Config valueprofile_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.
Sparlectra.runse! — Method
runse!(net; kwargs...) -> StateEstimationResultRun 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.
Sparlectra.runse_diagnostics — Method
runse_diagnostics(net, measurements; max_eliminations=3, kwargs...) -> NamedTupleExtended 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 initialvalidate_measurementsreporteliminations: one trace row per elimination(elimination, measurement_index, id, typ, normalized_residual_before, wii, skipped_unlocalizable, objective_before, objective_after).wiiis the localizability of the removed row andskipped_unlocalizablecounts 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_convergedfinal_diagnostics: the report after the last elimination (equal todiagnosticswhen nothing was eliminated)rerun: backward-compatible view of the FIRST elimination (deactivated_measurement_index,deactivated_measurement_id,diagnostics),nothingwhen 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.
Sparlectra.se_view — Method
se_view(net) -> NamedTupleStatic 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 SEshunts: one row per shunt released viasetShuntEstimation!(busIdx, busName, status)withstatus = :released(has an active direct bay measurement),:frozen_no_measurement, or:injection_mode_rejectedlinks:(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 representativeexcluded_measurements: the notes of the measurement remapping (link-referenced flow measurements, partial-cluster injections, collapsed branches, aggregations), reasons as inrunse!
See print_se_view for a formatted report.
Sparlectra.structural_observable — Method
structural_observable(H) -> BoolStructural observability test on a Jacobian-like matrix H. Returns true when the maximum bipartite matching size equals the number of state columns n.
Sparlectra.structural_row_redundant — Method
structural_row_redundant(H, i) -> BoolCheck if row i remains structurally redundant in H.
Sparlectra.summarize_se_diagnostics — Method
summarize_se_diagnostics(diag) -> NamedTupleCreate 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 bandfalse: either non-convergence or implausibly large objective
Sparlectra.validate_measurements — Method
validate_measurements(net, measurements; kwargs...) -> NamedTupleRun 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
wiiand alocalizableflag (wii > wiiThreshold, default 0.3, the established literature threshold) - suspicious measurement list (threshold-based)
- optional residual-correlation columns (
reportResidualCorrelation, default fromstate_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.
Sparlectra.writeSEStateCSV — Method
writeSEStateCSV(net; file) -> NamedTupleWrite 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_MVarOne 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.
Sparlectra.calcMachineTrafoTapFromSE — Method
calcMachineTrafoTapFromSE(net; trafo, v_machine_pu = nothing, p_mw = nothing, q_mvar = nothing) -> NamedTupleBack-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.
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).
Sparlectra.test_topology_hypotheses — Function
test_topology_hypotheses(net, measurements = net.measurements; candidates = :auto, max_candidates = 5, maxIte = ..., tol = ...) -> NamedTupleStage-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.
Sparlectra.validate_topology — Function
validate_topology(net, measurements = net.measurements; kwargs...) -> NamedTupleStage-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 belowk * sigmaat 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 thank * 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 thank * sqrt(sum sigma^2), for P and Q separately. Partially measured nodes and nodes inside closed-link clusters are skipped, never guessed.
Internals
Sparlectra.ZERO_INJECTION_SIGMA — Constant
ZERO_INJECTION_SIGMADefault 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.
Sparlectra._duplicate_measured_quantities — Method
_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.
Sparlectra._column_normalized — Method
_column_normalized(H) -> SparseMatrixCSCNormalize 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.
Sparlectra._suppression_normalized_residuals — Method
_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.
Sparlectra.TapStateMap — Type
TapStateMapState-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.
Sparlectra._cascade_tap — Method
_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.
Sparlectra._fixate_taps — Method
_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.
Sparlectra._is_machine_transformer — Method
_is_machine_transformer(net, k) -> BoolMachine (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.
Sparlectra._resolve_trafo_branch — Method
_resolve_trafo_branch(net, trafo) -> IntResolve 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).
Sparlectra._tap_apply_freezes — Method
_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.
Sparlectra._tap_cut_component — Method
_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).
Sparlectra._tap_guard_freezes — Method
_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.
Sparlectra._tap_r0_split — Method
_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.
Sparlectra._tap_step_limits — Method
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.