Robust GM estimation: leverage down-weighting, Schweppe-Huber, high-breakdown LTS/LMS, and HTI¶
Status (verified 1.95.091): Partly implemented: Schweppe-Huber GM (
EstimationMethod.SchweppeGm), the projection-statistics routine (internal, not wired intoSchweppeGm) and hypothesis-testing identification (HypothesisTestingBadData,HtiBadDataEstimation, remove-only) exist. High-breakdown LTS and LMS, theLeverageSourceoption and leverage diagnostics in the result and export are not implemented. HTI correct-in-place exists since October 2026 (N061) asHtiAction.CorrectThenReverify, opt-in (estimate --hti --hti-correct); removal stays the default. Implemented in:src/GridSim.Core/Estimation/WlsStateEstimator.cs(EstimationMethod.SchweppeGm,EstimationOptions.HuberK),src/GridSim.Core/Estimation/WlsStep.cs(IWlsStep.NormalizedResiduals),src/GridSim.Core/Estimation/BadDataAnalysis.cs,src/GridSim.Core/Estimation/ProjectionStatistics.cs(ProjectionStatistics, internal),src/GridSim.Core/Estimation/HypothesisTestingBadData.cs(HypothesisTestingBadData.Identify,HtiIdentified),src/GridSim.Core/Estimation/HtiBadDataEstimation.cs(HtiBadDataEstimation,HtiEstimationResult,HtiRemoval,HtiStopReason). CLI:estimate --method schweppe-gm(aliasgm) andestimate --hti. Design-to-code name map:
Design name Implemented as Estimation/RobustGm.csthe SchweppeGmbranch of the IRLS loop inWlsStateEstimator.csIWlsStep.LeverageDiagonal,HatDiagonalnot implemented; leverage is taken from IWlsStep.NormalizedResidualsMeasurementResidual.Leverage,leveragefield ingridsim-estimate-export/1not implemented EstimationOptions.Leverage,LeverageSource,LeverageFallbackToHat,SchweppeCutoffnot implemented; ProjectionStatisticsexists but no option selects itEstimation/HypothesisTestingIdentification.cs(HypothesisTestingIdentification)HypothesisTestingBadData.cs(HypothesisTestingBadData.Identify) andHtiBadDataEstimation.csBadDataId,BadDataCorrection,HtiMaxSuspects,EstimationResult.Correctednot implemented. HtiAction { Remove, CorrectThenReverify }exists (N061) as a parameter ofHtiBadDataEstimation.Estimate, defaultRemove; corrections are reported inHtiEstimationResult.Corrected(HtiCorrection), not onEstimationResultEstimation/HighBreakdownDriver.cs,EstimationMethod.Lts/Lms,LtsSubsetCount,LtsCoverage,LtsSeednot implemented RobustGmLeverageTests.cstests/GridSim.Tests/SchweppeGmTests.cs,ProjectionStatisticsTests.csHtiTests.cstests/GridSim.Tests/HypothesisTestingBadDataTests.cs,R062EstimationWiringTests.csHighBreakdownTests.csnot present This is a design record. The current behaviour is documented in the technical manual chapter 6. State estimation.
Implementation status (October 2026)¶
| Item (section) | Status | Where it lives / what differs from this spec |
|---|---|---|
| Schweppe-Huber GM with hat-diagonal leverage (3.1, 4) | Shipped | EstimationMethod.SchweppeGm in WlsStateEstimator; leverage taken from IWlsStep.NormalizedResiduals (no separate LeverageDiagonal). EstimationOptions.HuberK exists. CLI: estimate --method schweppe-gm (alias gm), not schweppegm. |
| Projection statistics (3.2) | Code exists, not wired | Estimation/ProjectionStatistics.cs (internal). There is no LeverageSource option and SchweppeGm does not use it (R259). |
| HTI (6) | Shipped, remove-only | HypothesisTestingBadData.Identify (suspect set, joint error estimate, per-suspect test) and the loop HtiBadDataEstimation (R062), which replaces sequential LNR when asked. CLI: estimate --hti. Correct-in-place (HtiAction.CorrectThenReverify, N061): opt-in with --hti-correct, corrections in hti-corrections.csv; a row identified again after its correction is removed; corrections plus removals capped at MaxBadDataRemovals. Not built: the BadDataId option, the correlation-based suspect-set expansion, HtiMaxSuspects, and a Corrected list on EstimationResult. HTI is dense (refused above 2,000 states) and refuses the robust methods. |
| High-breakdown LTS / LMS (5) | NOT IMPLEMENTED | There is no Lts or Lms EstimationMethod, no HighBreakdownDriver, and no LtsSubsetCount / LtsCoverage / LtsSeed options. Everything in section 5, and the LTS parts of sections 7, 8, 10 and 11, is still only a proposal. |
| Leverage diagnostics (7.3, 7.5, 7.6) | Not implemented | No IWlsStep.LeverageDiagonal, no MeasurementResidual.Leverage, no leverage field in gridsim-estimate-export/1. |
Original specification follows. It contains equations, data-structure and file layout, the seams to extend, and the validation plan. No C# is written here.
Governing constraint: everything below is a measurement-only static filter (no Dynamics/
process model, no forward step, no clock read). Under UnderstandingRestrictions.md sec. 3 and
the review gate (RestrictionReview.Reviewed (src/GridSim.Core/Diagnostics/RestrictionReview.cs)), such estimators are filters, not predictors: they need no
entry and are unconditionally permitted. Section 9 discharges this obligation explicitly and
states the invariants the suite must assert. Read UnderstandingRestrictions.md and the review
log before implementing.
1. Purpose and scope¶
The estimator today (src/GridSim.Core/Estimation/WlsStateEstimator.cs) carries two robust
methods behind the IWlsStep seam - Huber and LAV, both IRLS outer loops that reweight from the
raw standardized residual r~_i = |z_i - h_i(x)| / sigma_i - and a sequential
largest-normalised-residual (LNR) bad-data loop (Estimation/BadDataAnalysis.cs). Both have a
known blind spot: a gross error sitting on a high-leverage measurement at a low-redundancy
bus. The error pulls the fitted state toward itself (leverage), which keeps its own raw residual
small (masking / smearing), so k / r~_i never triggers in the Huber loop and the LNR test never
promotes it to "worst". tools/crossval/se_residual_check.py --leverage already exposes exactly
this quantity (the hat-matrix diagonal K_ii) as the independent arbiter of criticality; this
design brings that leverage into the estimator's decision logic.
This document specifies four additions, in increasing order of assurance and cost:
- Leverage (hat-matrix diagonal) down-weighting inside the IRLS loop, so high-leverage rows are judged against a leverage-adjusted residual instead of the raw one (sec. 3, sec. 4).
- A Schweppe-Huber Generalized-M (GM) weight function that formalises (1) with a leverage-scaled Huber threshold (sec. 4).
- High-breakdown LTS / LMS methods as new
EstimationMethodoptions that reuse theIWlsStepseam for their elemental/subset solves, used as the 50%-breakdown initial fit that feeds the GM reweighting (sec. 5). - Hypothesis-Testing Identification (HTI) for multiple interacting bad data, replacing the single-shot sequential LNR loop with a joint suspect-set error estimate and per-suspect hypothesis test (sec. 6).
Every addition is a drop-in behind existing seams; the estimator loop's ownership boundary (start
point, damping, convergence, chi-square) is unchanged. Companion reference doc:
docs/technical/06-state-estimation.md.
2. The failure this fixes (worked mechanism)¶
Let H = dh/dx at the estimate, W = diag(1/sigma_i^2) the measurement weights, and
G = H^T W H the gain (all as formed in DenseWlsStep.FormNormalEquations). The hat (influence)
matrix is
and its diagonal K_ii in [0, 1] is the leverage of measurement i. The residual of a
converged WLS fit relates to the true measurement error e by
so the residual-covariance diagonal is Omega_ii = sigma_i^2 (1 - K_ii) and the normalised
residual already computed in BadDataAnalysis.NormalizedResiduals is
Key identity (the whole design leans on it). BadDataAnalysis already computes
hGh = h_i^T G^-1 h_i and Omega_ii = sigma_i^2 - hGh. Therefore the leverage is recoverable for
free from quantities already in hand:
This is bit-for-bit the quantity se_residual_check.py computes as
K[r] = hrow @ lu.solve(hrow) * W[r] - so the independent validator is already the arbiter (sec.
10).
Why plain Huber / LNR miss the high-leverage outlier. When K_ii -> 1 (a measurement that is
the near-sole observer of some state direction - a radial GSP's single injection meter), the fit
is dragged onto that measurement, so r_i (hence r~_i = |r_i|/sigma_i) stays small even when
the reading is grossly wrong. The Huber factor k / r~_i therefore stays at 1 (no down-weight) and
LNR's r~-driven ranking never selects it. The cure is to judge the measurement against
sqrt(1 - K_ii) - the leverage-adjusted residual r^N_i - which inflates precisely at high
leverage and defeats the masking. That single substitution is the core of the Schweppe-Huber GM
weight (sec. 4).
3. Two leverage models¶
The estimator needs a per-measurement leverage weight a_i in (0, 1], with a_i -> 1 for a
well-redundant measurement and a_i -> 0 for a leverage point. Two sources are offered, selected
by EstimationOptions.Leverage (sec. 7). They trade cost against masking-immunity.
3.1 Hat-matrix diagonal (LeverageSource.HatDiagonal) - cheap, reuses existing machinery¶
This is already computed by step.NormalizedResiduals (via Omega_ii); no new gain solve is
needed. It is the default. Caveat: K_ii is derived from the reweighted gain, so under a single
gross error it is only mildly corrupted, but under multiple interacting errors the hat diagonal
can itself be masked. For that regime use 3.2 or the LTS initialiser (sec. 5).
Boundary cases:
- A critical measurement has Omega_ii <= sigma_i^2 * 1e-8 (the CriticalFloor in
BadDataAnalysis), i.e. K_ii ~ 1, a_i ~ 0. A critical row is the sole observer of a state
direction; it cannot be down-weighted without losing observability. The reweight must clamp
its factor to 1 (leave it at its declared weight) and rely on HTI/constraints, never IRLS, to
vet it. Concretely: when NormalizedResiduals returns critical[i] == true (NaN normalised
residual), the GM factor for i is 1.
- sum_i K_ii = trace(K) = s (state size). This is a cheap, exact determinism/sanity invariant
the test suite asserts (sec. 10.4).
3.2 Projection statistics (LeverageSource.ProjectionStatistic) - masking-immune, dense-only¶
The hat diagonal measures leverage against the possibly-contaminated fit. Projection
statistics (Mili, Cheniae, Rousseeuw) measure how outlying each Jacobian row h_i (a point in
state space R^s) is among all rows, using a high-breakdown location/scale, so they are immune to
the masking that corrupts K_ii. Algorithm, computed once at the linearisation point from the
Jacobian pattern (no operating-point dependence beyond H(x)):
Points: p_i = h_i (row i of H), i = 1..m, in R^s
For each candidate direction u (unit vector):
project: t_j = p_j . u for all j
center: med = median_j(t_j)
scale: MAD = 1.4826 * median_j |t_j - med|
PS_i(u) = |t_i - med| / MAD
PS_i = max over directions u of PS_i(u)
Directions u are taken as the normalised vectors from each point to the coordinate-wise (L1)
median of the point cloud - the standard m-direction approximation - giving O(m^2 s) cost.
The leverage weight is the Schweppe cutoff form:
where nu is the count of nonzero entries of h_i (its structural column count from
MeasurementModel.StructuralColumns) and chi-square_{nu, p} uses the existing
Estimation/ChiSquare.cs quantile. A row with normal leverage has PS_i <= d, hence a_i = 1
(no penalty); a leverage point has PS_i >> d, hence a_i small.
Scaling doctrine (matches the codebase's dense/sparse split). O(m^2 s) PS does not scale to
gb-full. Projection statistics are therefore computed on the dense backend only (state size
<= SolverSelector dense threshold, the same gate Constrained/Feasible use). Above it,
ProjectionStatistic selection fails loud with NotSupportedException (the established
"dense only, fail loud rather than silently degrade" convention), or - if
EstimationOptions.LeverageFallbackToHat is set - silently falls back to HatDiagonal, whose PS
approximation the sparse Gilbert-Peierls factorisation already supports for free. Default: fail
loud.
4. The Schweppe-Huber GM weight function¶
4.1 Objective and weight¶
The Schweppe-Huber Generalized-M estimator minimises
with rho the Huber loss and a_i the leverage weight (sec. 3). r_Si is the
leverage-adjusted standardized (Schweppe) residual: the raw standardized residual divided by
the leverage weight. Differentiating and casting as IRLS (the same reduction the current loop
uses), the effective diagonal WLS weight on measurement i becomes
W_i,eff = q(r_Si) * w_i, w_i = 1/sigma_i^2, q(r) = psi(r)/r
Huber psi: q(r_Si) = 1 if |r_Si| <= k
q(r_Si) = k / |r_Si| if |r_Si| > k (k = 1.345 default)
This is a one-line change to the existing IRLS block. Compare the current reweight in
WlsStateEstimator.Estimate (the EstimationMethod.Huber arm):
current Huber: r~_i = |z_i - h_i| / sigma_i factor = r~_i <= k ? 1 : k/r~_i
new SchweppeGm: r_Si = |z_i - h_i| / (sigma_i * a_i) factor = r_Si <= k ? 1 : k/r_Si
When a_i = sqrt(1 - K_ii) (sec. 3.1), r_Si equals the normalised residual r^N_i exactly, so
the implementation can obtain r_Si directly from step.NormalizedResiduals(vm, va, residualPu)
- no new numerics - and apply the Huber q() to it. When a_i comes from projection
statistics (sec. 3.2), a_i is computed once (masking-immune) and held constant across IRLS
passes; r_Si is then r~_i / a_i.
4.2 Integration into the existing IRLS loop¶
The change is localized to the reweight block of WlsStateEstimator.Estimate (currently lines
~261-279) and the maxOuter / wEff gating (lines 214, 235, 259, 289):
- Extend the "is a robust IRLS method" predicate to include
SchweppeGmwhereverMethod is Huber or Lavappears (weights cloned intowEff,maxOuter = 10, chi-square against original sigmas, removal loop skipped by default - or handed to HTI, sec. 6). - In the reweight, for
SchweppeGm: - If
Leverage == HatDiagonal: callstep.NormalizedResiduals(vm, va, residualPu)(which the loop already computes for the removal path) to getnormalized[i]andcritical[i].r_Si = normalized[i]. Forcritical[i], setfactor = 1. - If
Leverage == ProjectionStatistic:r_Si = (|z_i - h_i| / sigma_i) / a_i, witha_ifrom the once-computed PS vector. factor = r_Si <= HuberK ? 1 : HuberK / r_Si, thenwEff[i] = w[i] * factor, and the existingmaxFactorChange < 1e-3convergence test on the outer loop is unchanged.- Expose the Huber
k(hardcodedhuberK = 1.345) asEstimationOptions.HuberKso Huber and SchweppeGm share it; default preserves today's value exactly.
Reduces-to invariants (tested, sec. 10.4):
- On clean data (all r_Si <= k), every factor == 1, so W_eff == W and SchweppeGm is
bit-for-bit WLS - it must reproduce the machine-precision recovery fixture
(NoiseScale = 0) exactly.
- With a_i == 1 forced (leverage disabled), SchweppeGm is bit-for-bit the existing Huber.
- SchweppeGm never removes a measurement; like Huber/LAV it down-weights smoothly and reports
chi-square against the original sigmas for comparability.
5. High-breakdown LTS / LMS as EstimationMethod options¶
M-estimators (Huber, LAV, SHGM) have a breakdown point of at most 1/(m) in the presence of
leverage points - a single bad leverage point can still break them if it is not caught. LTS
(Least Trimmed Squares) and LMS (Least Median of Squares) have a ~50% breakdown point and are
used as the robust initial fit that seeds the SHGM reweighting (the classical two-stage
high-breakdown SE pipeline: high-breakdown initialiser -> statistically-efficient GM cleanup).
5.1 Objectives¶
LMS: minimise median_i ( r_i^2 / sigma_i^2 )
LTS: minimise sum_{i=1..h} ( r^2 / sigma^2 )_(i) (h smallest squared standardized residuals)
where h = floor((m + s + 1) / 2) for maximum breakdown (tunable via
EstimationOptions.LtsCoverage, the trimming fraction). Both are combinatorial; the practical
driver is resampling plus concentration:
5.2 The resampling / C-step driver¶
LtsDriver(model, mm, opt):
best_obj = +inf; best_state = null
for trial in 1..LtsSubsetCount:
draw an OBSERVABLE elemental subset E of measurement rows (|E| = s), seeded RNG
solve the exactly-determined subset via an IWlsStep over the sub-set -> x_E
(LTS only) run C concentration steps:
repeat up to LtsCStepCap:
rank all m standardized residuals at x_E; keep the h smallest -> subset E'
re-solve WLS over E' via IWlsStep -> x_E'
stop when the h-subset stops changing
obj = LMS or LTS objective at the final subset state
if obj < best_obj: best_obj = obj; best_state = x_E'
scale_hat = 1.4826 * (1 + 5/(m - s)) * sqrt(best_obj) (robust residual scale)
return best_state, scale_hat
Key points:
- The subset solve reuses the IWlsStep seam: an elemental subset is an exactly-determined
observable measurement set, so DenseWlsStep/SparseWlsStep solve it unchanged. The driver is a
new sibling class (HighBreakdownDriver), not a new step backend - it drives the step, it does
not replace it.
- Observable elemental subsets only. A random s-row draw is usually unobservable; the draw
must be rejected and re-drawn (bounded retries) until step.GateObservability passes, or built
greedily to cover all state directions. This reuses the existing observability gate.
- LtsSubsetCount defaults to a fixed count (e.g. 200) sized for gb-spine; determinism requires a
seeded RNG and strictly model-order subset enumeration (sec. 10.4), mirroring
SyntheticMeasurements (seed default 20260713).
5.3 The two-stage pipeline (recommended default)¶
--method lts (or lms) runs the driver, then hands best_state as the warm start and
scale_hat as the residual scale to a SchweppeGm reweighting pass. The GM stage recovers
statistical efficiency (LTS/LMS alone are inefficient) while the LTS stage guarantees the GM starts
from an uncontaminated fit - so masking cannot corrupt the leverage weights. This is the highest-
assurance measurement-only configuration and the one to reach for on the weakest GB periods (PN-
fallback days, radial GSPs).
Scaling doctrine: the resampling driver is dense-first. For gb-full it is gated exactly like
Constrained/Feasible (fail loud above the dense threshold) until a sparse elemental-solve path
is proven; state that limit in the exception message, per house convention.
6. HTI: Hypothesis-Testing Identification for multiple interacting bad data¶
The sequential LNR loop in WlsStateEstimator removes one measurement per pass (the single largest
r^N), re-estimating between removals. It is correct for a single gross error and for
well-separated errors, but for multiple interacting (conforming) errors it can smear the
residuals so the largest r^N points at a good measurement - misidentification. HTI replaces
the single-shot ranking with a joint estimate of the errors over a suspect set, then a
per-suspect hypothesis test. It is offered as EstimationOptions.BadDataId = Hti and slots into the
post-convergence identification stage (replacing the LNR block at
WlsStateEstimator.Estimate ~lines 320-357).
6.1 Equations¶
At the converged estimate, using the linearised residual model r = S e, S = I - K,
Omega = S W^-1 = W^-1 - H G^-1 H^T:
- Suspect set.
Sset = { i : r^N_i > tau }(tau = HtiSuspectThreshold, default 3.0), optionally expanded to include any measurement strongly correlated with a suspect (|Omega_ij| / sqrt(Omega_ii Omega_jj) > rho_minfor some suspectj) so interacting partners are not left out. Cap|Sset| <= HtiMaxSuspects(fail-soft: if exceeded, fall back to LNR and report it). - Joint error estimate. Assume only suspects carry error (
e_j = 0forjnot inSset). Thenr_Sset = S_SS * e_Sset, whereS_SSis the principal|Sset| x |Sset|submatrix ofS. Solve
Both S_SS and Omega_SS are built from |Sset| columns of H G^-1 H^T, i.e. |Sset|
back-substitutions against the gain factorisation already formed by
NormalizedResiduals - cheap when |Sset| is small. (S_SS = I - (H G^-1 H^T W)_SS,
Omega_SS = (W^-1)_SS - (H G^-1 H^T)_SS.)
3. Per-suspect hypothesis test. For each i in Sset:
t_i > gamma (gamma = HtiTestThreshold, default 3.0, a standard-normal significance level)
declares measurement i bad. Measurements with t_i <= gamma are exonerated even
though their raw r^N exceeded tau - this is precisely the interacting-error case LNR gets
wrong.
6.2 Correction vs removal (low-redundancy advantage)¶
For each identified-bad suspect, HTI can either remove it (as LNR does) or correct it in
place: z_i <- z_i - e^_i, keeping the row. Correction preserves observability - decisive at low
redundancy, where removing a radial GSP's sole injection is exactly the runaway the prior-backfill
option (BackfillRemovedFromPrior) treats from the other side. Recommended policy:
HtiAction = CorrectThenReverify (default): correct all identified suspects, re-estimate, re-run
the chi-square test; if it now passes, done; if not, iterate (capped at MaxBadDataRemovals),
falling back to removal for any suspect whose correction fails to clear. HtiAction = Remove
reproduces removal semantics for callers that want excluded-not-corrected behaviour. Corrections
and removals are both recorded in EstimationResult.Removed / a new
EstimationResult.Corrected list (sec. 7) with the estimated error e^_i and its t_i.
6.3 Interaction with SHGM and constraints¶
- HTI runs after the (optionally SHGM-reweighted) fit converges, on the current weights.
- Critical measurements (
Omega_ii ~ 0) cannot enterSset(theirr^Nis NaN) - a network fact is never removable, same doctrine as today. virtual-zero-injectionconstraint rows andremoval-backfill-priorpseudos are excluded fromSset(they are not removable telemetry), matching the existing LNR guards.
7. Code seams and data structures¶
All names below fit the existing style (PascalCase enum members and options, <summary> on every
public member, internal for new numeric machinery).
7.1 EstimationMethod (extend Estimation/WlsStateEstimator.cs)¶
Add, after Feasible:
SchweppeGm- Schweppe-Huber Generalized-M: Huber IRLS on the leverage-adjusted standardized residualr_Si = r_i / (sigma_i * a_i), rejecting high-leverage bad data the plain Huber/LNR loop masks. CLI:--method schweppegm(the estimator lowercases the enum name viaToString().ToLowerInvariant(), see the--methoderror path).Lts- Least Trimmed Squares initial fit (resampling + C-steps) feeding aSchweppeGmcleanup pass; ~50% breakdown. Dense-only, fail-loud above threshold.Lms- Least Median of Squares initial fit (resampling), same pipeline asLtswith the median objective.
Each gets a full <summary> in the enum, matching the existing Huber/LAV prose.
7.2 EstimationOptions (extend the same record)¶
New init properties, each documented:
LeverageSource Leverage = LeverageSource.HatDiagonal // sec. 3
bool LeverageFallbackToHat = false // sec. 3.2 (else fail loud)
double HuberK = 1.345 // shared Huber/SHGM threshold
double SchweppeCutoff = 2.5 // 'd' scale for PS weights, sec. 3.2
BadDataIdentification BadDataId = BadDataIdentification.SequentialLnr // vs Hti, sec. 6
double HtiSuspectThreshold = 3.0 // tau
double HtiTestThreshold = 3.0 // gamma
int HtiMaxSuspects = 30 // cap; fall back to LNR beyond
HtiAction HtiAction = HtiAction.CorrectThenReverify
int LtsSubsetCount = 200 // resampling trials
double LtsCoverage = 0.5 // trimming fraction -> h
int LtsCStepCap = 20
int LtsSeed = 20260713 // determinism, matches SyntheticMeasurements
New supporting enums in Estimation/ (their own small file or beside EstimationMethod):
LeverageSource { None, HatDiagonal, ProjectionStatistic },
BadDataIdentification { SequentialLnr, Hti },
HtiAction { Remove, CorrectThenReverify }.
7.3 IWlsStep extension (Estimation/WlsStep.cs)¶
Add one method to the seam so leverage is a first-class product of the representation that owns the gain:
DenseWlsStep/ConstrainedDenseWlsStep/FeasibleWlsStep:K_ii = 1 - Omega_ii/sigma_i^2, reusing the exactBadDataAnalysisback-substitution path (form the gain, factor once, one solve per row) - it is theNormalizedResidualscomputation minus the residual division, so factor the shared code rather than duplicating it.SparseWlsStep: same, reusing the single Gilbert-Peierls factorisation it already builds forNormalizedResiduals(FactorPrepared+ per-rowSolve). No new O(s^3) cost.
Because HatDiagonal leverage is exactly 1 - Omega_ii/sigma_i^2, an implementation may instead
derive a_i straight from the existing NormalizedResiduals return (the loop already calls it),
and LeverageDiagonal becomes an optional convenience. Choose one and keep the dense/sparse math on
a single code path (the codebase's standing rule that "dense and sparse math cannot drift apart").
7.4 New files (all internal, namespace GridSim.Core.Estimation)¶
Estimation/RobustGm.cs- the Schweppe-Huber weight functionq(r_Si)and the leverage-weight assembly (HatDiagonalfromOmega,ProjectionStatisticfromProjectionStatistics). Pure functions; the IRLS loop calls in.Estimation/ProjectionStatistics.cs-PS_icomputation (sec. 3.2), dense-gated, ChiSquare cutoff. Cross-checked againstse_residual_check.pyleverage on small cases.Estimation/HighBreakdownDriver.cs- the LTS/LMS resampling + C-step driver (sec. 5), seeded RNG, observable-subset draw viastep.GateObservability, returns(state, robustScale).Estimation/HypothesisTestingIdentification.cs- HTI suspect set, joint error estimate, per- suspect test, correct/remove policy (sec. 6). ConsumesS_SS/Omega_SSbuilt from the gain factorisation the step exposes.
7.5 EstimationResult additions (Estimation/EstimationResult.cs)¶
MeasurementResidual: adddouble Leverage = double.NaN(theK_ii), a new trailing optional record parameter (back-compatible), populated when any leverage-aware method ran. Surfaces ingridsim-estimate-export/1alongside the residual for the validator to check.EstimationResult: addIReadOnlyList<BadDataCorrection> Corrected(init, default empty) for HTI in-place corrections, whereBadDataCorrection(Measurement, double EstimatedError, double TestStat, int Pass)mirrorsBadDataRemoval.Removedkeeps its meaning (excluded rows), so both the LNR and HTI paths report through typed lists.
7.6 Export / crossval surface¶
Extend gridsim-estimate-export/1 (additive, versioned) so each residual carries leverage
(K_ii) and the document carries the identification method and any HTI corrections. This lets
tools/crossval/se_residual_check.py --leverage diff the estimator's leverage against its own
independent recomputation - the arbiter already exists (sec. 10).
8. Namespace / file layout summary¶
src/GridSim.Core/Estimation/
WlsStateEstimator.cs (extend: enum members, options fields, IRLS reweight arm, HTI hookup)
WlsStep.cs (extend IWlsStep + 4 backends: LeverageDiagonal)
BadDataAnalysis.cs (factor out the K_ii / Omega core shared with LeverageDiagonal)
EstimationResult.cs (add MeasurementResidual.Leverage, BadDataCorrection, Corrected)
RobustGm.cs (new: Schweppe-Huber q(), leverage-weight assembly)
ProjectionStatistics.cs (new: PS_i, dense-gated)
HighBreakdownDriver.cs (new: LTS/LMS resampling + C-steps)
HypothesisTestingIdentification.cs (new: HTI)
docs/technical/
06-state-estimation.md (extend: new methods, leverage, HTI - the reference doc)
tools/crossval/
se_residual_check.py (already computes leverage K and omega; extend to diff exported K and
to validate an HTI-corrected export)
tests/GridSim.Tests/
RobustGmLeverageTests.cs, HighBreakdownTests.cs, HtiTests.cs, RestrictionInvariantTests.cs (add)
No public API beyond the Estimation namespace changes; the CLI estimate --method ... surface
extends by three names and the new options, all defaulted so existing runs are byte-for-byte
unchanged.
9. Restriction-invariant compliance¶
Every method here is a static, measurement-only filter. Per the review gate (RestrictionReview):
"Static, measurement-only estimators (WLS, Huber, LAV, constrained, feasible, QR, linear-PMU,
branch-current, EWLAV) are filters, not predictors - they need no entry and are unconditionally
permitted." SchweppeGm, Lts, Lms, and HTI join that list. Concretely, each is compliant
because:
- No
Dynamics/process model. None integrates a state-transition model; there is no predict step.RestrictionReview.AssertPermitted(method, usesDynamicsModel: false)returns immediately. These methods therefore get noRestrictionReviewEntryand no row inRestrictionReview.Reviewed- adding one would be wrong (that register is only forDynamics/->Estimation/fusion, invariant #5). - Filter, not predictor (no forward component). Leverage, PS, LTS resampling, and HTI all operate on a single measurement snapshot already resolved at the estimate. They read no clock and emit nothing stamped later than the newest input. The estimate is the filtered present/past, exactly as WLS/Huber today.
- Guard-limited inputs unchanged. Inputs still arrive through
MeasurementSet.Load, which appliesForwardInferenceGuard(>= 169 h old or replay). No new ingestion seam is introduced; the LTS driver's RNG draws from the existing measurement rows, not from any external or live feed. - No dynamics fusion smuggled in. The LTS warm start is a measurement subset fit, not a
process-model prediction; the SHGM cleanup consumes it as an initial
x, identical in kind to today'sWarmStartseeding. Nothing crosses theDynamics/->Estimation/seam.
Invariants the suite must assert (add to RestrictionInvariantTests):
- I1.
RestrictionReview.AssertPermitted("SchweppeGm"/"Lts"/"Lms", usesDynamicsModel: false)does not throw, and none of these names appears inRestrictionReview.Reviewed(they are not dynamics-fused, so they must not be registered). - I2. Running any new method on a set whose
TakenUtcis newer thanUtcNow - 169his rejected by the guard before estimation (reuse the existingForwardInferenceGuardseam test). - I3. No new-method output carries a timestamp later than its newest input (the general no-forward-inference output invariant).
10. Validation and test plan¶
The estimator's validation doctrine (see docs/technical/11-validation.md): machine-epsilon
cross-checks against an independent reference, and bit-for-bit determinism. Every item below
follows it.
10.1 The headline fixture: reject a high-leverage outlier LNR/Huber miss¶
Construct a small network (e.g. a 5-bus radial spur off a meshed core) with a low-redundancy
bus whose single injection meter has leverage K_ii near 1 (verify via se_residual_check.py
--leverage). Inject a gross error into that meter, sized so its raw residual stays below the
3-sigma LNR/Huber trigger because the fit follows it (masking). Assert:
- Plain
--method wls+ LNR: the bad meter is not removed (or the wrong meter is), chi-square passes or a good measurement is flagged - the documented failure. - Plain
--method huber:factorfor the bad meter stays ~1 (not down-weighted). --method schweppegm: the leverage-adjustedr_Si = r^N_iexceedsk,factor = k/r_Si << 1, the bad meter is down-weighted, and the recovered state matches the clean-data WLS state within a set tolerance. This is the must-pass demonstration of the whole design.- With
Leverage = ProjectionStatistic: same rejection, and additionally survives a second interacting error whereHatDiagonalleverage is itself masked (the masking-immunity claim).
10.2 HTI vs sequential LNR on interacting bad data¶
Two conforming errors on adjacent/parallel meters constructed so their residuals smear and the
largest r^N points at a good measurement (the classic LNR misidentification). Assert:
BadDataId = SequentialLnr: removes the good measurement first (misidentification), needing extra passes or leaving chi-square failing.BadDataId = Hti: both true errors land inSset,t_i > gammafor exactly the two bad meters andt_i <= gammafor the exonerated good one;CorrectThenReverifycorrects both and chi-square then passes. ReportEstimatedErrorvs the injected error within tolerance.
10.3 Independent-reference cross-checks (machine epsilon)¶
- Leverage honesty.
se_residual_check.py --leveragerecomputesK_iifrom an independently built Ybus / pi-model (no GridSim math) and diffs it against the exportedleverage; requiremax |K_indep - K_exported|below the validator tolerance. The script's criticality arbitration (K > 1 - 1e-6) already exists; extend it to diff the leverage field per residual. - SHGM reduces to WLS. On a clean synthetic set (
--synth,NoiseScale = 0),--method schweppegmmust produce a state bit-for-bit identical to--method wls(all factors 1), and the independent Gauss-Newton optimality step fromse_residual_check.pymust be negligible. - PS self-check. On small cases, cross-check
PS_iand the projection directions against a reference NumPy recomputation (an added--projstatmode in the crossval script, mirroring the existing--fdanalytic-vs-finite-difference self-check).
10.4 Determinism and structural invariants¶
- Trace identity.
sum_i K_ii == StateSizeto machine tolerance on every fixture (exact hat- trace property) - a cheap always-on assertion. - LTS/LMS determinism. Two runs with the same
LtsSeedproduce bit-identical subsets, objective, and state; subset enumeration walks measurements in strict model order; tie-breaks are deterministic (lowest index wins). Mirrors theSyntheticMeasurementsdeterminism doctrine. - HTI determinism. Suspect-set construction and the joint solve are deterministic (fixed ordering, deterministic factorisation); repeated runs are identical.
- Huber equivalence.
SchweppeGmwithLeverage = None(alla_i = 1) equals--method huberbit-for-bit - guards against accidental behaviour change to the existing method.
10.5 Cross-validation vs an external estimator (optional, where feasible)¶
Where pandapower.estimation (the existing crossval_estimation.py reference) offers a comparable
robust/GM path, cross-check the down-weighted final state on a shared case. Absent a matching GM
implementation, the independent optimality + leverage checks in 10.3 are the primary gate (they
need no second estimator, only an independent h/K recomputation).
11. Zero-warning build gate and implementation checklist¶
The build ships at 0 warnings; keep it there:
- Every new public member (
EstimationMethodvalues,EstimationOptionsproperties, enums,BadDataCorrection,MeasurementResidual.Leverage) carries an XML<summary>in the established prose style. - New numeric types are
internal sealed; nullable-reference discipline uses the= null!Begin-then-bind pattern already in the step backends. - No unused fields, no unreachable arms; the
switchoverEstimationMethodin the reweight and in the method-selection block stays exhaustive (the new members are handled or explicitly fall through to WLS behaviour). - Fail-loud, never silent-degrade:
ProjectionStatisticandLts/Lmsabove the dense threshold throwNotSupportedExceptionwith a message naming the limit (the--method constrainedpattern), unless the documented fallback flag is set. - Determinism: seeded RNG only, no
DateTime.Now, no unordered-collection iteration in any path that affects the result.
Implementation order (each step independently shippable and testable):
IWlsStep.LeverageDiagonal+MeasurementResidual.Leverage+ export/crossval diff (no method behaviour change yet; pure diagnostics, validates the leverage plumbing againstse_residual_check.py).SchweppeGmwithHatDiagonalleverage (the one-line IRLS substitution + reduces-to tests) - delivers the headline fixture (sec. 10.1).ProjectionStatistics+LeverageSource.ProjectionStatistic(masking-immunity).HypothesisTestingIdentification+BadDataId = Hti(sec. 10.2).HighBreakdownDriver+Lts/Lmstwo-stage pipeline (sec. 5).
12. Open questions / follow-ups¶
- Sparse projection statistics and sparse LTS. Both are dense-gated here. A sparse PS approximation (subsampled directions) and a sparse elemental solve would lift the gb-full limit; out of scope for the first landing, called out in the fail-loud messages.
- HTI suspect-set expansion policy. The correlation-based expansion (
rho_min) is a tunable; the initial default should be validated against a battery of multiple-interacting-error cases before it is trusted on GB history. - GM efficiency tuning.
HuberK = 1.345(95% Gaussian efficiency) andSchweppeCutoffare the standard starting points; a GB-specific tuning study (against the sigma model indocs/technical/06-state-estimation.mdsec. "GDA-side sigma model") is a later calibration task. - Interaction with the feasibility gate. SHGM/LTS states should also pass the Ring C physics
acceptance gate when combined with
Feasible; the composition of robust weighting with the KKT constraint backend is a follow-up (both currently sit behindIWlsStep, so it is a matter of wiring, not new math).
13. See also¶
UnderstandingRestrictions.md- the no-forward-inference rule (sec. 3: filter, not predictor).RestrictionReview.Reviewed(src/GridSim.Core/Diagnostics/RestrictionReview.cs) - the review register; measurement-only methods need no entry.docs/technical/06-state-estimation.md- the WLS estimator, IRLS Huber/LAV, LNR bad-data loop, observability, the GDA sigma model (the reference this design extends).src/GridSim.Core/Estimation/WlsStateEstimator.cs- the IRLS block (Huber/LAV) and the LNR removal loop this design modifies.src/GridSim.Core/Estimation/BadDataAnalysis.cs- theOmega_ii/ normalised-residual / criticality core that the leverageK_iiand HTI submatrices reuse.src/GridSim.Core/Estimation/WlsStep.cs- theIWlsStepseam the new methods extend.tools/crossval/se_residual_check.py- the independent leverage/criticality arbiter (--leverage).