# 📦 WP-169 — `fix(assign): alignment weights survive zero-metric ties and low overlap`

> Engineering log · [Phase 15 — Audit remediation, v0.8.0](https://borda.github.io/lucid-YOLO/engineering/phase-15-audit-remediation/)
>
> 🔗 Depends on [WP-165b](https://borda.github.io/lucid-YOLO/work_packages/wp-165b/) · Needed by [WP-175](https://borda.github.io/lucid-YOLO/work_packages/wp-175/)
>
> ⬅️ [WP-168](https://borda.github.io/lucid-YOLO/work_packages/wp-168/) · [Roadmap](https://borda.github.io/lucid-YOLO/ROADMAP/) · [WP-170](https://borda.github.io/lucid-YOLO/work_packages/wp-170/) ➡️


## WP-169 — alignment weights survive zero-metric ties and small overlap

<a id="wp-169"></a>

**`topk` ranked the ties globally, not within each ground truth.** `_select_topk` ran `topk` over an alignment metric that is zero at non-candidates *and* zero at a candidate whose prediction misses — the state a freshly initialized head is in, since `decode_ltrb` applies no non-negativity and inverted distances decode to zero-IoU boxes. With a ground truth's whole row tied at zero, `topk` filled its `k` slots from the global tie order, which need not contain a single one of *that* ground truth's candidates, and the intersection with `candidate_mask` then returned **zero** positives for a ground truth that had four. Measured on a 4×4 stride-4 grid with one real ground truth whose candidates are `{10, 11, 14, 15}` — deliberately not the low indices the tie order favours — every one of `topk` ∈ {1, 3, 4, 6} returned 0 positives where `min(k, c)` was expected. Ranking over `align_metric.masked_fill(~candidate_mask, -1.0)` fixes it; `-1` is the sentinel `_resolve_conflicts` already uses and is safe because `t` is non-negative by construction. The intersection is kept rather than dropped: when `k > c` the surplus slots land on the `-1` fill and are removed there.

**An absolute floor under a sixth power.** The target normalization divided by `t_max + eps` with `eps = 1e-9`, but `t_max = s · u_max⁶` collapses as the sixth power of the overlap, so the floor is eventually larger than the quantity it guards. Parametrized over `u_max` ∈ {1.0, 0.5, 0.1, 0.01}, the best anchor's weight — which is `u_max` by definition, since `t_max` is its own row's maximum — came out `0.999999998`, `0.49999994`, `0.0998004` and `4.9975e-06`: correct at full overlap, **2001× too small** at IoU 0.01. A positive trains as background while `fg_mask` still calls it foreground. The denominator is now floored at `torch.finfo(dtype).tiny`, which only ever guards a true 0/0 — and there `align_pos` is zero too, so the product is zero rather than NaN.

**The repo's own frozen data was the strongest evidence.** `tests/ptl/prechange_detect_step.json` pinned, at initialization, `train/o2m_box` at `4.12e-09` and `train/o2m_l1` at `3.17e-08` — the one-to-many branch's entire localization objective, weighted into nothing, roughly 1.9e6× suppressed. That snapshot was written to detect objective drift across [WP-088](https://borda.github.io/lucid-YOLO/work_packages/wp-088/) and did that job; what it also did, unremarked for eight releases, was pin the defect. `tests/assign/test_rotated_assign.py` carried the same fingerprint: image 1 anchor 5 is its ground truth's best anchor, so its weight is exactly `u_max = 0.25`, and the snapshot stored `0.249998539686203`.

**Both frozen sets were re-captured, and the ruling is recorded here rather than in `ESCALATION.md`.** Nothing under `goldens/` moves — all 43 pass, and a zero-tolerance sweep of all 447 metrics across 48 files confirms every `assignment_cases.json` metric is bit-identical in the live copy and in all six frozen sets. Trigger 4 is "a change would require altering a frozen golden", and no change does, so no escalation entry was opened. The two `tests/` snapshots are a different question, decided deliberately: the pre-change loss values were re-captured by replaying the test's exact sequence under the same seeds and `set_num_threads(1)`, with seeds, config, gains and the 714 `state_dict_keys` unchanged and a `recapture_reason` field recorded in the file. The rotated weights were *derived* from the frozen ones by the closed-form factor rather than read off the code under test, and agree with the current output to ≤2 ULP.

**One test passed only because of the defect.** `test_rotated_candidacy_reaches_assigner` asserted the rotated call produced strictly *fewer* positives than the axis-aligned one. It passed — because the smaller rotated candidate set simply lost more slots to the global tie fill. With H-16 fixed both saturate at `min(k, c) = k`, so the count stopped discriminating while set membership still does; the assertion moved to `not torch.equal(rotated.fg_mask, axis_aligned.fg_mask)`, which still fails if `gt_rboxes` is dropped, since the two calls then become identical. Probed over 40 independent seeds: the new assertion holds 40/40 and the old one 0/40, both counts pinned at exactly 10 every time. The old assertion is not merely weakened — it is now never true.

**Attribution was measured, not argued.** Applying one fix at a time separates them cleanly: with H-16 reverted, `test_detect_step_reproduces_pre_change_snapshot` yields `24.32134246826172`, identical to all digits with both fixes, so H-16 contributes nothing to that scene; the three bit-identity failures in `test_rotated_assign.py` also survive H-16's reversion and move by exactly `(t_max + 1e-9) / t_max`, and only `test_rotated_candidacy_reaches_assigner` passes with H-16 reverted.

**What this row hands to [WP-167](https://borda.github.io/lucid-YOLO/work_packages/wp-167/).** H-17 changes the objective's weighting on the first step of every trained run, so `goldens/gpu/overfit_micro_*.json` and `goldens/gpu/shapes_regression_det.json` are expected to move. Those five files have no copies under `goldens/frozen/`, so recomputing them is an ordinary `FREEZE=1` re-freeze on an accelerator rather than a §7 question — but it must happen with 169 and 172 both landed, which is why it belongs to 167 and not here.

