Project

General

Profile

User Story #75 » 0004-feat-engine-detect-a-biased-inter-anchor-edge-by-gra.patch

knight8241, 08/07/2026 18:33

View differences:

common/src/commonMain/kotlin/com/aether/mofe/engine/MultiObserverFusionEngine.kt
return if (n == 0) Double.NaN else kotlin.math.sqrt(sumSq / n)
}
/**
* #63 — inter-anchor edges flagged BIASED by GRAPH RESIDUAL on the given [positions] and measured
* [distances]: a stable, lopsided link the rigid solve cannot reconcile with the rest of the graph
* (the capture4 cross-room bias the triangle-inequality guard and per-link health both miss).
* Returns each biased pair (canonical order) → its residual, EMPTY unless the graph is REDUNDANT
* (≥5 anchors / >3n−6 edges) — on an exactly-rigid graph a single bias is absorbed with ZERO
* residual and is undetectable (see [InterAnchorLinkReport.biasedEdges]). Adds a STABILITY gate on
* top of the pure detector: a bias is trusted only from a WELL-SAMPLED link (≥ strongMinSamples),
* never a transient bad read — reusing the same per-link sample accounting the STRONG/WEAK health
* uses. Single-sources detection for both [interAnchorLinkReport]'s flag and the refine down-weight.
*/
private fun biasedInterAnchorEdges(
positions: Map<DeviceId, Vector3D>,
distances: Map<Pair<DeviceId, DeviceId>, Double>,
): Map<Pair<DeviceId, DeviceId>, Double> {
val candidates = InterAnchorLinkReport.biasedEdges(positions, distances, interAnchorLinkThresholds)
if (candidates.isEmpty()) return candidates
return candidates.filterKeys { pair ->
val st = interAnchorLinkStats[pair] ?: interAnchorLinkStats[pair.second to pair.first]
(st?.count ?: 0) >= interAnchorLinkThresholds.strongMinSamples
}
}
/**
* #69b-ii — per-edge weights that down-weight the biased edges [biasedInterAnchorEdges] finds, for
* [AnchorConstellationSolver.refine]. GUARDRAIL: never push the solve under-determined. A rigid 3-D
* graph of n nodes needs ≥ 3n−6 full-weight edges; only the SURPLUS is down-weighted (most-biased
* first), so at least 3n−6 edges keep unit weight. Detection already requires a redundant graph
* (edges > 3n−6), so the budget is ≥ 1 for a lone biased edge. No flags (or the feature off) ⇒
* EMPTY map ⇒ the refine is byte-identical to the un-weighted solve.
*/
private fun biasedEdgeWeights(
positions: Map<DeviceId, Vector3D>,
distances: Map<Pair<DeviceId, DeviceId>, Double>,
): Map<Pair<DeviceId, DeviceId>, Double> {
if (!config.anchorConstellationDownWeightBiasedEnabled) return emptyMap()
val biased = biasedInterAnchorEdges(positions, distances)
if (biased.isEmpty()) return emptyMap()
val usable = distances.keys.filter { it.first in positions && it.second in positions }
val nodes = usable.flatMapTo(HashSet()) { listOf(it.first, it.second) }
val budget = usable.size - maxOf(3 * nodes.size - 6, 0) // full-weight edges to preserve = 3n−6
if (budget <= 0) return emptyMap()
val w = config.anchorConstellationBiasedEdgeWeight
return biased.entries.sortedByDescending { it.value }.take(budget).associate { it.key to w }
}
/** Reset the stability run so a fresh convergence must re-confirm before the frame can lock. */
private fun resetLockStability() { lastShapeSignature = null; stableSolveTicks = 0 }
......
val edges = correctedInterAnchorDistances().filterKeys { it.first in anchors && it.second in anchors }
if (edges.size < config.anchorConstellationMinEdges) return 0
// #69b-ii: down-weight any inter-anchor edge flagged BIASED by graph residual on the CURRENT
// (already-placed) constellation, so the good edges out-vote the bias in this refine instead of
// the frame warping to absorb it. EMPTY on a rigid/clean graph (or the feature off) ⇒ the
// solve is byte-identical to before. The guardrail in [biasedEdgeWeights] keeps ≥ 3n−6 full-
// weight edges so the graph never goes under-determined.
val edgeWeights = biasedEdgeWeights(anchors, edges)
val result = AnchorConstellationSolver.refine(
initial = anchors,
distances = edges,
rootId = rootId,
priorWeight = config.anchorConstellationPriorWeight,
weights = edgeWeights,
)
if (result.maxCorrectionMeters < config.anchorConstellationMinCorrectionMeters) return 0
......
sampleCount = st.count,
)
}
return InterAnchorLinkReport.build(ids, samples, nowMicros, interAnchorLinkThresholds)
// #63: mark stable, lopsided edges by graph residual on the current solved constellation. Root
// sits at the gauge origin; distances are rotation-invariant so the oriented frame positions
// serve directly. EMPTY on a rigid/clean graph ⇒ no biased flags ⇒ report is unchanged.
val rootId = frameManager.topology?.frame?.originDeviceId
val positions = buildMap {
if (rootId != null) put(rootId, Vector3D.ZERO)
frameManager.getAllReferencePoints().forEach { if (it.id != rootId) put(it.id, it.position) }
}
val corrected = correctedInterAnchorDistances()
.filterKeys { it.first in positions && it.second in positions }
val biasedPairs = biasedInterAnchorEdges(positions, corrected).keys
return InterAnchorLinkReport.build(ids, samples, nowMicros, interAnchorLinkThresholds, biasedPairs)
}
/**
common/src/commonMain/kotlin/com/aether/mofe/model/ConfigTypes.kt
/** Prior pull toward the current (AoA) positions; fixes the rotational gauge
* without resisting the distance-constrained shape. Small. */
val anchorConstellationPriorWeight: Double = 0.005,
/** #63/#69b-ii: down-weight an inter-anchor edge the graph-residual detector flags BIASED (a
* stable, lopsided link — see [InterAnchorLinkReport.biasedEdges]) in the leader's rigid-body
* refine, so the good edges out-vote the bias instead of the frame warping to absorb it. Only
* ever bites on a REDUNDANT graph (≥5 anchors / >3n−6 edges) with a dominant, well-sampled
* outlier, and never down-weights so many edges that the solve goes under-determined — so with
* no such edge it is a no-op (empty weight map ⇒ byte-identical solve). */
val anchorConstellationDownWeightBiasedEnabled: Boolean = true,
/** Weight applied to a down-weighted biased inter-anchor edge. ≪1 so the good edges dominate the
* fit, but non-zero so the edge is only SOFTENED, not deleted — the distance graph stays connected. */
val anchorConstellationBiasedEdgeWeight: Double = 0.01,
// ── Frame-lock TRUST gate (accuracy > speed) ───────────────────────────────
// The frame LOCK (frameEstablished) freezes the constellation to stop jitter; a
common/src/commonMain/kotlin/com/aether/mofe/model/InterAnchorLink.kt
package com.aether.mofe.model
import kotlin.math.abs
/**
* Health of one anchor↔anchor UWB link, for the calibration / mesh-health UI.
*
......
val strongMinSamples: Int = 3,
/** |measured − solved| at/below this (m) is a STRONG link; above it is WEAK. */
val strongResidualMeters: Double = 0.20,
/** #63 graph-residual BIAS detection ([InterAnchorLinkReport.biasedEdges]). An edge is flagged
* biased when its post-solve graph residual |‖p_a−p_b‖ − d| clears BOTH this absolute floor (m) … */
val biasResidualMeters: Double = 0.10,
/** … AND this multiple of the MEDIAN edge residual, so only a lopsided edge that stands out from
* the consensus is flagged — a uniform/global error (every edge equally off) flags nothing. On a
* minimally-redundant graph (5 anchors, one surplus edge) the solve spreads a lone bias the most,
* leaving the biased edge only ~2.4× the median, so the gate sits at 2× (the biased edge is still
* the sole one clearing both this AND the absolute floor). */
val biasDominanceRatio: Double = 2.0,
)
/** One classified anchor↔anchor link. [a] < [b] by device id, so a pair appears once. */
......
val ageMicros: Long?,
/** Measurements folded into this link. */
val sampleCount: Int,
/** #63: a stable, LOPSIDED link — it ranges fine (often [InterAnchorLinkStatus.STRONG]) yet carries
* a large, dominant post-solve GRAPH residual the rigid solve could not absorb, i.e. a directional
* ranging bias. Orthogonal to [status] (a biased edge is usually still "ranging well"). Only ever
* set on a REDUNDANT constellation (≥5 anchors / >3n−6 edges); on an exactly-rigid graph a bias is
* absorbed with zero residual and is undetectable this way. See [InterAnchorLinkReport.biasedEdges]. */
val biased: Boolean = false,
)
/**
......
val strong: Int,
val weak: Int,
val missing: Int,
/** Links flagged [InterAnchorLink.biased] by graph residual (#63). 0 unless [build] was given the
* solved positions to detect against, and the constellation is redundant enough to detect at all. */
val biased: Int = 0,
) {
/** A distance graph with a MISSING expected link is not rigid — the solve is blocked/degraded. */
val hasMissingLink: Boolean get() = missing > 0
/** At least one stable, lopsided inter-anchor link — a directional ranging bias that a redundant
* solve can down-weight (#69b-ii) but a rigid one silently absorbs into a warped frame. */
val hasBiasedLink: Boolean get() = biased > 0
companion object {
val EMPTY = InterAnchorLinkReport(emptyList(), 0, 0, 0, 0, 0)
......
* Pure classifier: for every unordered pair of [anchorIds], look up its accumulated
* [samples] and classify STRONG / WEAK / MISSING at [nowMicros]. Order-independent
* (pair key is normalized by device id). No engine or IO — unit-tested directly.
*
* [biasedPairs] (canonical order, from [biasedEdges]) marks the [InterAnchorLink.biased] flag
* on the matching links. Empty (the default) ⇒ every link biased=false and biased count 0 —
* byte-identical to the ranging-only classification.
*/
fun build(
anchorIds: List<DeviceId>,
samples: Map<Pair<DeviceId, DeviceId>, InterAnchorLinkSample>,
nowMicros: Long,
thresholds: InterAnchorLinkThresholds = InterAnchorLinkThresholds(),
biasedPairs: Set<Pair<DeviceId, DeviceId>> = emptySet(),
): InterAnchorLinkReport {
if (anchorIds.size < 2) return EMPTY
val ids = anchorIds.distinct()
......
var strong = 0
var weak = 0
var missing = 0
var biased = 0
for (i in ids.indices) {
for (j in i + 1 until ids.size) {
val a = ids[i]
......
InterAnchorLinkStatus.WEAK -> weak++
InterAnchorLinkStatus.MISSING -> missing++
}
// A MISSING link has no distance, so it can never be in biasedPairs; a biased flag
// therefore only ever lands on a link that is actually ranging.
val isBiased = key in biasedPairs
if (isBiased) biased++
links.add(
InterAnchorLink(
a = key.first,
......
residualMeters = s?.residualMeters,
ageMicros = age,
sampleCount = s?.sampleCount ?: 0,
biased = isBiased,
),
)
}
}
return InterAnchorLinkReport(links, ids.size, ids.size * (ids.size - 1) / 2, strong, weak, missing)
return InterAnchorLinkReport(
links, ids.size, ids.size * (ids.size - 1) / 2, strong, weak, missing, biased,
)
}
/**
* #63 — flag stable, lopsided inter-anchor edges by GRAPH RESIDUAL. Given the solved anchor
* [positions] (any rigid orientation — inter-anchor distances are rotation/translation
* invariant) and the measured inter-anchor [distances], return each biased edge (canonical
* order) mapped to its post-solve residual |‖p_a−p_b‖ − d|.
*
* The gap this closes: the triangle-inequality guard ([CrossAnchorFuser.applyGeometric-
* ConsistencyCheck]) catches only distances that break geometry outright, and the per-link
* STRONG/WEAK health catches a large per-sample residual — but a TIGHT, STABLE bias (a fixed
* offset on one cross-room link, the capture4 field failure) breaks neither: it ranges
* consistently and can satisfy the triangle inequality. It only shows up once the WHOLE graph
* is solved together and one edge cannot be reconciled with the rest.
*
* ── CRITICAL CORRECTNESS INVARIANT (honor + do not "optimise" away) ───────────────────────
* This works ONLY on a REDUNDANT (over-determined) graph. A rigid 3-D constellation of n
* nodes has 3n−6 internal DOF. With EXACTLY 3n−6 edges (n=4 ⇒ 6 edges = complete K4) the
* least-squares solve has just enough freedom to satisfy every edge, so a biased edge is
* absorbed by WARPING the frame and its post-solve residual is ZERO — invisible here. Only
* when edges > 3n−6 (n≥5, e.g. K5 = 10 edges > 9 DOF) does the bias have nowhere to hide and
* surface as a dominant residual. Below that this returns EMPTY, by design, not as a miss.
*
* A flagged edge must be both ABSOLUTELY large (≥ [InterAnchorLinkThresholds.biasResidualMeters])
* and DOMINANT over the median edge residual (≥ ratio×median), so a single lopsided link is
* caught while a uniform/global error (which no single down-weight could fix) flags nothing.
*/
fun biasedEdges(
positions: Map<DeviceId, Vector3D>,
distances: Map<Pair<DeviceId, DeviceId>, Double>,
thresholds: InterAnchorLinkThresholds = InterAnchorLinkThresholds(),
): Map<Pair<DeviceId, DeviceId>, Double> {
// Canonicalize; keep only edges whose BOTH endpoints have a solved position + a valid range.
val edges = HashMap<Pair<DeviceId, DeviceId>, Double>()
for ((pair, d) in distances) {
val (a, b) = pair
if (a == b || d <= 0.0 || !d.isFinite()) continue
if (a !in positions || b !in positions) continue
edges[if (a.value <= b.value) a to b else b to a] = d
}
// n = anchors actually in the ranging graph (not merely positioned) — that is what the
// 3n−6 rigidity DOF is measured against.
val nodes = edges.keys.flatMapTo(HashSet()) { listOf(it.first, it.second) }
val n = nodes.size
if (n < 5) return emptyMap() // < 5 ⇒ at best exactly-rigid ⇒ bias absorbed
val dof = 3 * n - 6
if (edges.size <= dof) return emptyMap() // not redundant ⇒ residual is zero ⇒ undetectable
val resid = edges.mapValues { (pair, d) ->
abs(positions.getValue(pair.first).distanceTo(positions.getValue(pair.second)) - d)
}
val median = resid.values.sorted().let { it[it.size / 2] }
val floor = thresholds.biasResidualMeters
val ratio = thresholds.biasDominanceRatio
return resid.filterValues { r -> r >= floor && r >= ratio * median }
}
}
}
common/src/commonTest/kotlin/com/aether/mofe/engine/BiasedInterAnchorEdgeTest.kt
package com.aether.mofe.engine
import com.aether.mofe.integration.MofeTestHarness
import com.aether.mofe.model.DeviceId
import com.aether.mofe.model.InterAnchorLinkReport
import com.aether.mofe.model.InterAnchorLinkSample
import com.aether.mofe.model.MofeConfig
import com.aether.mofe.model.Vector3D
import kotlin.math.abs
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* #63 (detect) + #69b-ii (down-weight) — flag a STABLE, LOPSIDED inter-anchor edge by GRAPH RESIDUAL,
* then down-weight it in the rigid-body solve so the good edges out-vote the bias.
*
* The gap these close: [CrossAnchorFuser.applyGeometricConsistencyCheck] catches only triangle-
* inequality breaks, and per-link STRONG/WEAK health catches a large per-sample residual — but a
* tight, stable cross-room bias (capture4: +0.57/+0.75 m on the long links) breaks neither and warps
* the frame. It only surfaces once the WHOLE graph is solved and one edge cannot be reconciled.
*
* ── CRITICAL CORRECTNESS FACT (the whole reason for the redundancy gate) ─────────────────────────
* On an EXACTLY-RIGID 4-anchor graph (6 edges = 6 DOF) the least-squares solve has just enough
* freedom to satisfy every edge, so the biased edge's post-solve residual is ZERO — the solve absorbs
* the bias by WARPING the frame and detection is impossible. Graph-residual detection therefore works
* ONLY on a REDUNDANT graph (≥5 anchors / >3n−6 edges). Both facts are asserted below.
*/
class BiasedInterAnchorEdgeTest {
private val a1 = DeviceId("A1"); private val a2 = DeviceId("A2")
private val a3 = DeviceId("A3"); private val a4 = DeviceId("A4"); private val a5 = DeviceId("A5")
private fun dist(a: Vector3D, b: Vector3D) = a.distanceTo(b)
private fun canon(a: DeviceId, b: DeviceId) = if (a.value <= b.value) a to b else b to a
/** Every unordered inter-anchor distance for [pts], with [biasPair] inflated by [bias] m. */
private fun distances(
pts: Map<DeviceId, Vector3D>, biasPair: Pair<DeviceId, DeviceId>, bias: Double,
): Map<Pair<DeviceId, DeviceId>, Double> {
val ids = pts.keys.toList()
val out = HashMap<Pair<DeviceId, DeviceId>, Double>()
for (i in ids.indices) for (j in i + 1 until ids.size) {
val (x, y) = ids[i] to ids[j]
val biased = (x to y) == biasPair || (y to x) == biasPair
out[x to y] = dist(pts.getValue(x), pts.getValue(y)) + if (biased) bias else 0.0
}
return out
}
private val truth5 = mapOf(
a1 to Vector3D(0.0, 0.0, 0.0), a2 to Vector3D(2.0, 0.0, 0.0),
a3 to Vector3D(0.0, 2.0, 0.0), a4 to Vector3D(1.0, 1.0, 0.6), a5 to Vector3D(2.0, 2.0, 0.3),
)
// ── Part A: DETECT ───────────────────────────────────────────────────────────────────────────
@Test
fun exactly_rigid_four_anchor_graph_absorbs_the_bias_so_nothing_is_flagged() {
// 4 anchors = 6 edges = 6 DOF: the solve satisfies every edge, so the +0.6 bias is absorbed
// into a warped frame with ZERO post-solve residual — undetectable by graph residual, BY DESIGN.
val truth4 = mapOf(
a1 to Vector3D(0.0, 0.0, 0.0), a2 to Vector3D(2.0, 0.0, 0.0),
a3 to Vector3D(0.0, 2.0, 0.0), a4 to Vector3D(1.0, 1.0, 0.6),
)
val biased = distances(truth4, a2 to a3, 0.6)
val solved = AnchorConstellationSolver.refine(truth4, biased, rootId = a1)
// The frame really did warp (A3 pushed off truth) — the bias went somewhere …
val a3err = solved.positions.getValue(a3).distanceTo(truth4.getValue(a3))
assertTrue(a3err > 0.15, "rigid graph should absorb the bias by warping (A3 err=${a3err} m)")
// … but NOT into any edge residual, so detection correctly finds nothing.
val flagged = InterAnchorLinkReport.biasedEdges(solved.positions, biased)
assertTrue(flagged.isEmpty(),
"an exactly-rigid graph must flag NO biased edge (residual is absorbed); got $flagged")
}
@Test
fun redundant_five_anchor_graph_flags_exactly_the_biased_edge() {
val biased = distances(truth5, a2 to a3, 0.6)
val solved = AnchorConstellationSolver.refine(truth5, biased, rootId = a1)
val flagged = InterAnchorLinkReport.biasedEdges(solved.positions, biased)
val residuals = biased.entries.joinToString { (p, d) ->
"${p.first.value}-${p.second.value}=" +
"${abs(solved.positions.getValue(p.first).distanceTo(solved.positions.getValue(p.second)) - d)}"
}
assertTrue(canon(a2, a3) in flagged,
"the redundant solve must flag the biased A2-A3 edge. residuals=[$residuals] flagged=$flagged")
assertEquals(setOf(canon(a2, a3)), flagged.keys,
"ONLY the biased edge should be flagged. residuals=[$residuals] flagged=$flagged")
}
// ── Part B: DOWN-WEIGHT (the required detect → down-weight → recover loop) ──────────────────────
@Test
fun detect_then_downweight_recovers_the_biased_anchor() {
val biased = distances(truth5, a2 to a3, 0.6)
// 1) UN-weighted solve: an over-determined graph still spreads a single bias into the frame.
val unweighted = AnchorConstellationSolver.refine(truth5, biased, rootId = a1)
val errUnweighted = unweighted.positions.getValue(a3).distanceTo(truth5.getValue(a3))
assertTrue(errUnweighted > 0.10, "the un-weighted solve spreads the bias into A3 (err=${errUnweighted} m)")
// 2) DETECT the biased edge from that solve (no ground truth used — pure graph residual).
val flagged = InterAnchorLinkReport.biasedEdges(unweighted.positions, biased)
assertTrue(flagged.isNotEmpty(), "detection must find the biased edge to drive the down-weight")
// 3) DOWN-WEIGHT the flagged edges and re-solve → the good edges recover A3.
val weights = flagged.keys.associateWith { 0.01 }
val weighted = AnchorConstellationSolver.refine(truth5, biased, rootId = a1, weights = weights)
val errWeighted = weighted.positions.getValue(a3).distanceTo(truth5.getValue(a3))
assertTrue(errWeighted < 0.05,
"detect + down-weight must recover A3 (err ${errWeighted} m vs un-weighted ${errUnweighted} m)")
}
// ── Part A wiring: the flag surfaces on InterAnchorLinkReport ───────────────────────────────────
@Test
fun build_surfaces_the_biased_flag_and_count_and_is_a_noop_when_empty() {
val ids = truth5.keys.toList()
val samples = buildMap {
val d = distances(truth5, a2 to a3, 0.6)
for ((pair, dm) in d) put(canon(pair.first, pair.second), InterAnchorLinkSample(dm, null, 0L, 5))
}
// Empty biasedPairs ⇒ unchanged report (no biased flags).
val plain = InterAnchorLinkReport.build(ids, samples, nowMicros = 0L)
assertEquals(0, plain.biased)
assertFalse(plain.hasBiasedLink)
assertTrue(plain.links.none { it.biased })
// With the flag set, exactly that link is marked and counted.
val flagged = InterAnchorLinkReport.build(ids, samples, nowMicros = 0L, biasedPairs = setOf(canon(a2, a3)))
assertEquals(1, flagged.biased)
assertTrue(flagged.hasBiasedLink)
val link = flagged.links.first { it.a == canon(a2, a3).first && it.b == canon(a2, a3).second }
assertTrue(link.biased, "the A2-A3 link must carry the biased flag")
assertEquals(1, flagged.links.count { it.biased })
}
// ── End-to-end through the real engine (report flag + refine down-weight wiring) ───────────────
private fun feedBiased(h: MofeTestHarness) {
val d = distances(truth5, a2 to a3, 0.6)
for ((pair, dm) in d) repeat(4) { h.engine.processInterAnchorRanging(pair.first, pair.second, dm) }
}
private fun refs(h: MofeTestHarness): Map<DeviceId, Vector3D> =
h.frameManager.getAllReferencePoints().associate { it.id to it.position }
private fun maxPairwiseError(got: Map<DeviceId, Vector3D>): Double {
val ids = truth5.keys.toList()
var maxErr = 0.0
for (i in ids.indices) for (j in i + 1 until ids.size) {
val g = got.getValue(ids[i]).distanceTo(got.getValue(ids[j]))
val w = truth5.getValue(ids[i]).distanceTo(truth5.getValue(ids[j]))
maxErr = maxOf(maxErr, abs(g - w))
}
return maxErr
}
private fun seedAtTruth(h: MofeTestHarness) {
h.engine.initializeAsRoot(a1)
h.engine.registerMeshNode(a2, truth5.getValue(a2))
h.engine.registerMeshNode(a3, truth5.getValue(a3))
h.engine.registerMeshNode(a4, truth5.getValue(a4))
h.engine.registerMeshNode(a5, truth5.getValue(a5))
}
@Test
fun engine_report_flags_the_biased_link_on_a_redundant_constellation() {
val h = MofeTestHarness(MofeConfig()).build()
seedAtTruth(h)
feedBiased(h)
val report = h.engine.interAnchorLinkReport()
assertTrue(report.hasBiasedLink, "the engine report must flag the stable cross-room bias")
val biasedLinks = report.links.filter { it.biased }.map { canon(it.a, it.b) }
assertTrue(canon(a2, a3) in biasedLinks, "the flagged link must be A2-A3; got $biasedLinks")
}
@Test
fun engine_downweight_prevents_the_bias_from_warping_the_frame() {
// Down-weight ON (default): refineAnchorConstellation holds the frame near truth.
val on = MofeTestHarness(MofeConfig()).build()
seedAtTruth(on); feedBiased(on)
on.engine.refineAnchorConstellation()
val errOn = maxPairwiseError(refs(on))
// Down-weight OFF: the same biased edge warps the constellation on refine.
val off = MofeTestHarness(MofeConfig(anchorConstellationDownWeightBiasedEnabled = false)).build()
seedAtTruth(off); feedBiased(off)
off.engine.refineAnchorConstellation()
val errOff = maxPairwiseError(refs(off))
assertTrue(errOff > 0.10, "without down-weighting the bias must warp the frame (maxErr=${errOff} m)")
assertTrue(errOn < errOff * 0.6,
"down-weighting must keep the frame closer to truth (on=${errOn} m vs off=${errOff} m)")
}
}
    (1-1/1)