Skip to content

fix(graph): propagation fill bit-parity to main (PROTEA pin)#21

Merged
frapercan merged 1 commit into
mainfrom
fix/propagation-to-main
Jun 25, 2026
Merged

fix(graph): propagation fill bit-parity to main (PROTEA pin)#21
frapercan merged 1 commit into
mainfrom
fix/propagation-to-main

Conversation

@frapercan

Copy link
Copy Markdown
Owner

Cherry-picks the propagation correctness fix (#20, bdbb103) onto main so PROTEA's git+main pin picks it up. main already has the CSR/sparse-DAG perf (#19); this adds ONLY the prop=fill correctness fix (sparse pushup overshot fill blockers; now bit-identical to dense + upstream, property test included). Metric-changing (tiny, ~0.002 Fmax) but strictly more correct.

…ges metrics, needs review) (#20)

## Summary (CORRECTNESS-CRITICAL: changes metric numbers , please
review)

The `CAFAEVAL_SPARSE` env switched not only the confusion-matrix kernel
but also the **propagation algorithm**. Under `prop="fill"` (PROTEA's
production default), the sparse path (`_propagate_sparse_pushup`) and
the dense serial sweep produced **different propagated prediction
matrices** on real DAG inputs, shifting Fmax:

| Setting | sparse (before) | dense (before) |
|, -|, -|, -|
| NK Fmax | 0.58859 | 0.58914 |
| LK Fmax | 0.61612 | 0.61391 |
| PK Fmax | 0.40195 | 0.40195 |

## Which impl was canonical

The **dense serial sweep is canonical**: it is bit-identical to upstream
cafaeval `prop="fill"` (BioComputingUP/CAFA-evaluator and
claradepaolis/CAFA-evaluator-PK, the fork's ancestors). Verified on 5
hand-worked DAGs plus a 200-trial randomized stress; the dense path
matched the upstream oracle in 100% of cases.

The **sparse pushup was the buggy one**. `fill` is a stepwise
leaves-to-roots recurrence where a non-zero intermediate term *blocks*
deeper descendants (it keeps its own value, and that value, not the
descendant's, flows up to the parent). The pushup scattered every input
non-zero straight into every ancestor and group-maxed, ignoring those
blockers and overshooting. Worked counter-example (chain
leaf->mid->root, leaf=1.0, mid=0.5, root=0):

- upstream / dense fill: root = **0.5** (mid blocks the leaf)
- sparse pushup: root = **1.0** (leaf scattered straight to root), WRONG

This matches the observed "sparse=1.0 where dense=0.86" divergence,
concentrated at high tau on biological_process multi-term
shared-ancestor rows.

## The fix (which impl was fixed)

The **sparse** path is fixed. `mode="fill"` is now routed to a new
`_propagate_sparse_fill` that reproduces the recurrence sparsely (walk
`ont.order`, per-row max over each term's children's *current* values,
never overwrite originally-non-zero cells). The fast scatter pushup is
retained for `mode="max"`, where it is proven correct. Minimal,
localized change to the propagation path only.

## After: bit-identical

- **Propagated prediction matrices: bit-identical** between sparse and
dense, `max|diff| = 0.000e+00` across BPO/CCO/MFO (including the BPO
rows where the bug lived).
- NK/LK/PK Fmax now identical between both paths and equal to the dense
(canonical) values:

| Setting | sparse (after) | dense (after) |
|, -|, -|, -|
| NK Fmax | 0.5891445 | 0.5891445 |
| LK Fmax | 0.6139093 | 0.6139093 |
| PK Fmax | 0.4019451 | 0.4019451 |

(Residual ~1e-16 / 1 ULP in some weighted count columns is float
reduction-order noise in the counting kernel, downstream of the
now-identical propagated matrices, below any metric significance.)

- Sparse stays ~5x faster than dense on the fill path (no perf
regression).

## Test added

`tests/test_propagation_fill_parity.py`: the worked blocked-chain
counter-example, shared-ancestor and high-tau BPO-shaped rows, plus a
200-trial randomized DAG stress, all asserting `sparse == dense ==
upstream-oracle` bit-for-bit. Closes the coverage gap left by the
existing self-parity test, which only exercises `prop="max"`.

## Local CI

ruff clean; mypy clean (py3.12 target, matching the CI runner); full
`tests/` + `tests/diff` (Phase B) green.

## Deploy note

PROTEA pins `cafaeval @ git+main`. A redeploy will pick this up once it
lands on `main`. I did NOT bump the pin.
@frapercan frapercan merged commit 80d705a into main Jun 25, 2026
6 checks passed
@frapercan frapercan deleted the fix/propagation-to-main branch June 25, 2026 04:19
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.

1 participant