fix(AR-025): only evidence spends the evidence budget

The correlation discount is an effective-sample correction: with observations
correlated at rho, the n-th is worth n_eff(n+1) - n_eff(n) = 2/((n+1)(n+2))
at rho=0.5, so it decays quadratically and the total converges to 1/rho = 2.
That saturation is deliberate and stays — a long static shot must not
out-argue varied evidence purely by lasting longer.

What was not deliberate is that every scored face spent it. An observation at
p=0.02 contributes log(0.98) = -0.02 of belief, which is nothing, while
consuming the same increment as one at p=0.95. On SuperHero-2, track 3 carried
103 observations for an effective weight of 2.026 and a belief of 0.455
against a 0.881 threshold — with the 51 frames that *did* identify the actor
arriving when each was worth 0.0002. The budget had been spent by the frames
that recognised nobody.

It also made the answer depend on frame rate: deliver more frames, dilute the
budget with more non-matches, and a track that was owned stops being owned.
That is the defect AR-013 already had to fix once for reaping, still present
in accumulation. It is how this was found — SuperHero-2 identified Jeremy
before a KPN throughput fix and not after, from identical input, and bisect
put the change at the KPN bump in d98dc28 rather than anywhere in this repo.

The floor is 0.5, which is where the posterior stops favouring the hypothesis,
not a tuned threshold. Near-misses still count: the identity matcher
deliberately feeds every scored face rather than only accepted ones, and an
observation at 0.7 — under the matcher's 0.754 acceptance — still accumulates,
which the second test pins. Below 0.5 the observation argues *against*, which
noisy-OR cannot represent, so declining to spend a budget on it loses nothing.

Scored against the DVU knowledge-graph ground truth, all ten SuperHero scenes:

                 TP   FP   FN   precision   recall     F1
  before          9    0   15       1.000    0.375   0.545
  after          13    0   14       1.000    0.481   0.650

Four more true positives and no false positives — the recall gain is not
bought with precision. Scene 7's Isabelle and scene 10's Isabelle and Jeremy
are all in the ground truth. Apples-to-apples over scenes 1-9 (scene 10's
before-run timed out): TP 9 to 11, FN 15 to 13, FP 0 either way.

An earlier attempt at this gave each actor its own budget. It was a no-op:
measured on the same track, n_obs_for[best] equalled n_obs at 103, because
every observation votes for the best-matching actor. Reverted rather than
kept.

151/151.

TRACES: AR-025, AR-013 | SR-002
This commit is contained in:
2026-08-08 14:48:40 +02:00
parent 24d35cbde3
commit 7b73bf923a
2 changed files with 114 additions and 3 deletions
+67
View File
@@ -450,3 +450,70 @@ TEST_CASE("a track retired from association is still open to evidence",
REQUIRE(sink.claims.size() == 1);
CHECK(sink.claims[0].actor_idx == 7);
}
// ── AR-025 — non-matches must not spend an actor's evidence budget ───────────
TEST_CASE("frames that recognise nobody do not exhaust the budget",
"[registry][AR-025]") {
// The correlation discount is an effective-sample correction: with
// observations correlated at rho, the n-th is worth
// n_eff(n+1) - n_eff(n) = 2/((n+1)(n+2)) at rho=0.5, so it decays
// quadratically and the total converges to 1/rho = 2. That saturation is
// deliberate — a long static shot must not out-argue varied evidence by
// lasting longer.
//
// What was not deliberate is that every scored face spent it, including
// ones that matched nobody. An observation at p=0.02 contributes
// log(0.98) = -0.02 of belief — nothing — while consuming the same
// increment as one at p=0.95. Measured on SuperHero-2: 103 observations on
// one track, effective weight 2.026, belief 0.455 against a 0.881
// threshold, with the 51 frames that *did* identify the actor arriving
// when each was worth 0.0002. The identification was lost.
//
// It also made the answer depend on frame rate — deliver more frames,
// dilute the budget with more non-matches, and a track that was owned stops
// being owned — which is the defect AR-013 already had to fix once.
TrackRegistry reg(cfg(), disc());
Sink sink; sink.attach(reg);
int id;
{ auto f = reg.begin_frame(0.0); id = f.create(0.0, axis(0)); }
// A long run of frames that match nobody: the detector saw a face, the
// matcher could not place it. These are not evidence for actor 1.
for (int i = 0; i < 40; ++i) reg.observe(id, 1, 0.02f, axis(0));
// Then the actor is clearly recognised. Before this fix the budget was
// already spent and these could not move the belief.
for (int i = 0; i < 6; ++i) reg.observe(id, 1, 0.9f, axis(0));
reg.flush(1.0);
REQUIRE(sink.claims.size() == 1);
INFO("belief " << sink.claims[0].belief
<< " effective_obs " << sink.claims[0].effective_obs);
CHECK(sink.claims[0].actor_idx == 1);
CHECK(sink.claims[0].belief > 0.88f);
}
TEST_CASE("a near-miss is still evidence", "[registry][AR-025]") {
// The floor is at 0.5 — where the posterior stops favouring the hypothesis
// — not at the matcher's acceptance threshold. A run of near-misses for one
// actor is informative and must still accumulate, which is the property the
// identity matcher's comment relies on when it feeds every scored face
// rather than only the accepted ones.
TrackRegistry reg(cfg(), disc());
Sink sink; sink.attach(reg);
int id;
{ auto f = reg.begin_frame(0.0); id = f.create(0.0, axis(0)); }
// 0.7 is below the matcher's acceptance threshold (0.754 on the SuperHero
// gallery) and above the 0.5 floor: a frame that would not be reported as
// an identification, but is still evidence. Twelve of them accumulate to
// ~0.89, past the 0.881 ownership threshold.
for (int i = 0; i < 12; ++i) reg.observe(id, 3, 0.7f, axis(0));
reg.flush(1.0);
REQUIRE(sink.claims.size() == 1);
INFO("belief " << sink.claims[0].belief);
CHECK(sink.claims[0].actor_idx == 3); // owned on near-misses alone
}