Skip to content

fix(position-estimation): large uncertainty for weight-dominated single-anchor solves (#3616)#3617

Merged
Yeraze merged 1 commit into
mainfrom
fix/3616-snr-weight-estimation
Jun 22, 2026
Merged

fix(position-estimation): large uncertainty for weight-dominated single-anchor solves (#3616)#3617
Yeraze merged 1 commit into
mainfrom
fix/3616-snr-weight-estimation

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Summary

Issue #3616 hypothesized that the SNR weighting in position estimation was inverted, collapsing weak-signal nodes onto the anchor. After reading positionEstimationService.ts end-to-end, the weighting is correct — the real bug is in the uncertainty calculation.

Root cause (investigation)

The weight model is 10^(snrDb/10) (observationWeight, lines 77-84):

  • -12 dB → 0.063, 0 dB → 1, +10 dB → 10. Higher SNR → higher weight → pulled toward that anchor. Not inverted.

The actual bug: solveNodePosition computed uncertainty as

if (nEff <= 1) uncertaintyKm = DEFAULT_SINGLE_ANCHOR_KM; // 5km
else uncertaintyKm = max(MIN, rmsKm / sqrt(nEff));

With skewed weights (one strong anchor + one far weak -12 dB anchor, the exact symptom), the weighted centroid collapses onto the strong anchor, the far/weak anchor barely contributes to the weighted RMS, and the Kish effective sample size nEff lands just above 1 (~1.01). That skips the 5 km single-anchor branch and the rmsKm/sqrt(nEff) formula reports a spuriously tight sub-km radius — a confident, wrong "inside the reporter's house" estimate.

So this is the uncertainty-calc bug (hypothesis b/c), not inverted weighting (hypothesis a).

Fix

Blend the radio-range default toward the statistical radius using nEff:

  • nEff = 1 (lone or weight-dominated anchor) → full 5 km radio-range default.
  • nEff >= 2 (balanced multi-anchor) → pure statistical estimate (unchanged).
  • Linear blend in between (confidence = clamp(nEff - 1, 0, 1)).

The weight model is left as-is (it is physically correct). No sign was flipped.

Before / after — issue's case (strong +10 dB anchor + weak −12 dB anchor ~4 km apart) | | latitude | nEff | uncertaintyKm |

|---|---|---|---|
| Before | 10.0002 (at anchor) | 1.013 | 0.31 km (falsely confident) |
| After | 10.0002 (at anchor) | 1.013 | 4.94 km (correctly unreliable) |

The estimate still collapses onto the dominant anchor (the max-likelihood point for an effectively-single observation), but now carries a large uncertainty radius flagging it as unreliable.

A truly lone −12 dB anchor still reports exactly 5 km. Balanced tight 4-anchor clusters stay confident (<1 km), unchanged.

Tests

Added to positionEstimationService.test.ts:

  • SNR→weight ordering: weight(-12) < weight(0) < weight(+10) with independently-computed values (0.063 / 1 / 10).
  • Lone −12 dB anchor → estimate at anchor, uncertaintyKm === 5.
  • Issue regression: strong + far weak anchor → uncertaintyKm > 4 (not a sub-km confident circle).
  • Balanced tight 4-anchor cluster → uncertaintyKm < 1 (no regression to confident estimates).

Verification

  • Full Vitest suite: success=true, 7099 passed, 0 failed, 2392 suites (JSON reporter).
  • tsc -p tsconfig.server.json --noEmit: no new errors (the only errors are 5 pre-existing unrelated ones in TelemetryChart.tsx).

Closes #3616

🤖 Generated with Claude Code

…le-anchor solves (#3616)

The SNR weight model (10^(snr/10)) is correct — higher SNR yields higher
weight, pulling the estimate toward that anchor — so the "inverted weighting"
hypothesis does not hold. The real bug was in the uncertainty calculation.

When one strong anchor dominates the SNR weights and the other anchor is weak
and far (e.g. +10 dB vs -12 dB, ~4 km apart), the weighted centroid collapses
onto the strong anchor and the far/weak anchor contributes almost nothing to the
weighted RMS. The Kish effective sample size (nEff) lands just above 1 (~1.01),
so the old `nEff <= 1` branch was skipped and the estimate reported a spuriously
tight sub-km radius — a confident, wrong "inside the reporter's house" estimate.

Fix: blend the radio-range default toward the statistical radius using nEff.
At nEff = 1 (a lone or weight-dominated anchor) uncertainty is the full 5 km
radio-range default; it reaches the pure statistical estimate only at nEff >= 2
(a balanced multi-anchor solve). The issue's case now reports ~4.94 km
(unreliable) instead of ~0.31 km. Balanced multi-anchor estimates are unchanged.

Closes #3616

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review — fix(position-estimation): large uncertainty for weight-dominated single-anchor solves (#3616)

  • Gather context
  • Read changed files
  • Analyze implementation
  • Post review feedback

Summary: Well-reasoned fix with clean implementation. The root-cause analysis in the PR description is accurate, the math is correct, and the tests are comprehensive. A few minor observations below.


positionEstimationService.ts

The core fix (lines 148–158) is mathematically sound.

const confidence = Math.min(1, Math.max(0, nEff - 1));
const uncertaintyKm = Math.max(
  MIN_UNCERTAINTY_KM,
  DEFAULT_SINGLE_ANCHOR_KM * (1 - confidence) + statisticalKm * confidence,
);

The linear blend from nEff = 1 (full radio-range default) to nEff ≥ 2 (pure statistical) is a principled and transparent choice. Using the Kish effective sample size is appropriate here.

One subtle edge case to consider: What happens when nEff is between 1 and 2 and statisticalKm > DEFAULT_SINGLE_ANCHOR_KM? For example, if two anchors are very far apart (say 50 km), the statistical estimate would be large, and with confidence = 0.5, the blend would compute:

5 * 0.5 + 50 * 0.5 = 27.5 km

This is lower than the pure statistical estimate (50 km), so the blend would actually under-report uncertainty in that transition zone for very-wide anchor sets. This is probably acceptable in practice (widely separated anchors likely still constrain the location reasonably), but it's worth noting as a design choice rather than a bug.

statisticalKm floor uses Math.max(nEff, 1) (line 148):

const statisticalKm = Math.max(MIN_UNCERTAINTY_KM, rmsKm / Math.sqrt(Math.max(nEff, 1)));

Since used.length > 0 and wSum > 0 are checked at line 131, nEff = wSum²/w²Sum will be ≥ 1 (by Cauchy-Schwarz) whenever there's at least one valid observation, so the Math.max(nEff, 1) guard is defensive but redundant. Not a problem.


positionEstimationService.test.ts

The new tests are well-structured and directly target the failure modes:

SNR ordering test (line 104–113): Good regression guard. The hardcoded expected values (0.0631, 1, 10) match 10^(snrDb/10) exactly and serve as an independent cross-check.

Lone anchor test (line 120–126): Correctly pins the exact value (toBe(5) for DEFAULT_SINGLE_ANCHOR_KM). Note that this test uses a specific SNR value (-12 dB), but the outcome (exactly 5 km) holds for any SNR on a lone anchor — the test could optionally add a second variant with snrDb: 10 to make that invariant explicit. Minor suggestion, not a blocker.

Regression case (line 133–143): The toBeCloseTo(10.0, 2) tolerance (±0.005 degrees) is slightly looser than the SNR bias test (line 91 uses implicit tolerance). Fine, since the real assertion is the uncertainty check. The uncertaintyKm > 4 threshold (line 142) is a reasonable fuzzy bound rather than pinning an exact value that could drift with formula tweaks.

Balanced cluster test (line 148–157): Good non-regression guard. uncertaintyKm < 1 for a cluster spread ≈ 0.005° × cos(lat) ≈ 0.55 km is plausible; the test validates that the fix doesn't inflate confidence for genuinely multi-anchor solves.

Missing test scenario: There's no test for exactly nEff == 2.0 (the crossover point where confidence = 1 and the uncertainty transitions to pure-statistical). This would confirm the boundary is clean. Again, minor — the existing tests bracket it well from both sides.


Code Quality

  • The implementation is clean, minimal (25 lines changed), and contained entirely within solveNodePosition.
  • Documentation comments are thorough and clearly explain the "why".
  • No raw SQL, no multi-source violations, no new dependencies introduced.
  • The old if (nEff <= 1) branch that could silently bypass the radio-range default is cleanly eliminated.

Verdict

Approve. The fix correctly addresses the root cause (uncertainty underestimation in weight-dominated cases), the blend formula is well-chosen, tests cover the key scenarios including the regression case from the issue. The minor points above are non-blocking suggestions.

@Yeraze Yeraze merged commit a6a318f into main Jun 22, 2026
19 checks passed
@Yeraze Yeraze deleted the fix/3616-snr-weight-estimation branch June 22, 2026 00:50
Yeraze added a commit that referenced this pull request Jun 22, 2026
Add a deep-dive page documenting how MeshMonitor computes a node's
position-estimate accuracy circle (uncertaintyKm), for publication on
meshmonitor.org:

- the observation weight model: time-decay (24h half-life) × SNR linear
  power (10^(SNR/10)), with an SNR→weight reference table
- the SNR/time weighted centroid (the estimated position)
- the accuracy radius: weighted RMS spread, Kish effective sample size,
  and the n_eff confidence blend toward the radio-range default (the
  #3616/#3617 refinement that prevents falsely-confident single-anchor
  circles)
- ASCII pipeline/geometry/blend diagrams and three worked examples
  (single anchor, strong+weak/far dominated, balanced multi-anchor)
- a formula reference and guidance on reading the circle / tuning the
  max-acceptable-accuracy cutoff

Links it from the existing Position Estimation page and adds it to the
Maps & Visualization sidebar. Docs build verified (no dead links).


Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: SNR weighting in position estimation may be inverted, collapsing estimates to anchor regardless of signal strength

1 participant