# NOTES — MSA 2-piece affine gap vs 1-piece affine

## What was built
- `src/simulate.cpp`: simulates a random binary tree over N taxa, evolves a root DNA
  sequence with K2P substitutions (ts:tv≈2:1) and indels whose length is drawn from a
  mixture of two geometrics (mean 2 w.p. `1-pi_long`, mean 30 w.p. `pi_long`). Ground-truth
  alignment columns are tracked exactly via a global doubly-linked "master column list"
  that every lineage splices into on insertion — this gives an exact reference alignment,
  not an approximation.
- `src/msa.cpp`: progressive MSA — k-mer(k=4) cosine distance, UPGMA guide tree,
  profile-profile Gotoh DP. Gap cost is either 1-piece affine `g(l)=O+l*E` (3-state DP:
  M/Ix/Iy) or 2-piece affine `g(l)=min(O1+l*E1, O2+l*E2)` (5-state DP: M/Ix1/Iy1/Ix2/Iy2,
  combined via max-score = min-cost at every step). Reports wall-clock ms and peak DP byte
  allocation per merge.
- `experiment.py`: orchestrates simulate+msa, does the two dev-set grid searches (baseline
  O,E and then O2,E2 with O1,E1 fixed from baseline), evaluates SP / TC / SP-near-long-indel
  / runtime / peak bytes on the test set, writes `results.json` and the figure.

## A real bug I found and fixed (documented per the honesty rule)
First full run showed nonsensical progressive alignments: pairwise alignment of two
genuinely diverged leaf sequences (no profile involved at all) produced heavily
fragmented output with spurious single-column gaps every 2-4 residues, even though a gap
open cost of 8 should always beat a handful of -1 mismatches. Raising the gap-open cost
made it *worse*, which ruled out "params too permissive" and pointed at the scoring scale
itself: with match=+2/mismatch=-1 and branch lengths drawn from U(0.03,0.18) accumulated
along tree paths, sequence divergence between distant leaves got high enough that the
*expected* score of a random column (0.25·2 + 0.75·(-1) = -0.25) went negative — at that
point inserting small gaps to dodge locally bad windows is a genuine (if degenerate)
optimum of the DP, not a bug in the recursion. Fixed by (a) rescaling to match=+1/mismatch=-3
(closer to EDNAFULL-style discrimination) and (b) shortening branch lengths to
U(0.01,0.05), which brings pairwise divergence back into a realistic regime. Re-tested:
the DP then reconstructs the ground-truth alignment almost exactly on a 5-taxon smoke test.
This means the params in the falsifiable prediction interact with the score scale — the
absolute (O,E) grid values only make sense given this match/mismatch scale, which the task
spec did not fix, so I had to choose and validate one against ground truth before trusting
any downstream number.

## Gate B fix (re-run after first submission)
First submission's `sp_near_long_indel` had `None` in `per_seed` for pi_long=0.00 (all 3
seeds) and pi_long=0.05 (seed 2): those seeds' 4 test families happened to contain zero
true gaps of length>=10 (long indels are drawn with probability `pi_long`, so at
pi_long=0.00 they're rare, not literally impossible — this seed just didn't roll one).
Averaging an empty list produced `None`, which fails the "no non-numeric per_seed" gate.
Fixed by falling back that (seed, arm) value to the seed's whole-alignment `sp_score`
when no flank windows exist, with the fallback logged explicitly in `logs/run.log`
(`... falls back to sp_score=...`, 8 occurrences) and declared in `results.json.deviations`.
This metric was never the basis of the headline claim (that's plain `sp_score`), so the
fallback doesn't touch the refutation conclusion below — it only affects a secondary
mechanism-inspection metric. Headline numbers are bit-identical to the first run (all
computation is seeded/deterministic); only this one metric's representation changed.

## Deviations from the original plan (scope cut for the 45-min budget)
- `n_taxa`: 8 → 6, `root_len`: 800bp → 220bp — cuts DP cell count (~O(L²) per merge) by
  >10x without changing the qualitative question.
- families per seed: 10 → 4 (both dev and test) — the grid searches multiply this by 20
  combos twice per pi_long value, so this alone is a 2.5x runtime cut.
- Grid sizes kept exactly as specified (5×4=20 baseline combos, 5×4=20 proposed combos) —
  did not need to cut these, full run finished in 22.7s.
- `pi_long` sweep, 3 seeds, and the falsifiable-prediction structure kept exactly as specified.
- Given the reduced family count, per-seed SP variance is high (see `results.json`,
  std up to 0.09) — this affects how much weight the "3 seeds must agree in sign" check
  can carry; the conclusion below already accounts for that (signs disagree at every
  pi_long, which is itself informative, not just a power problem).

## Result
Full grid search + 3-seed test evaluation ran in 22.7s (`results.json.runtime_sec`).

| pi_long | baseline SP | proposed SP | gain (pp) | seed signs consistent |
|---|---|---|---|---|
| 0.00 | 0.4830 | 0.4840 | 0.09 | No |
| 0.05 | 0.4484 | 0.4533 | 0.49 | No |
| 0.20 | 0.4602 | 0.4670 | 0.68 | No |

**Prediction refuted.** The falsifiable prediction required ≥2pp gain with consistent
sign across all 3 seeds at pi_long=0.20; actual gain there is 0.68pp and the sign is
*not* consistent across seeds (one seed slightly favors the 1-piece baseline). The
gain does trend upward with `pi_long` (0.09 → 0.49 → 0.68pp), which is directionally
consistent with the mechanism the prediction was built on, but it is far too small and
noisy to survive greedy progressive propagation at this scale. Also note pi_long=0
did *not* show the baseline being significantly better either (0.09pp, well under the
1pp veto threshold for "gain must be ≤0.5pp" — technically fails that sub-condition too,
though it's the pi_long=0.20 failure that drives the overall `refuted` verdict).

`sp_near_long_indel` (the mechanism-targeted metric near true long-gap boundaries) and
`tc_score` are reported per-arm in `results.json` for further inspection; they show the
same qualitative pattern (small, noisy, sign-inconsistent gains) and are not the basis of
the headline claim.

## Honest read
Two plausible explanations, not adjudicated by this run:
1. The effect is real but small at this problem size (6 taxa, ~200bp, moderate
   divergence) and would need larger families/more seeds to resolve from noise — the
   reduced scope (4 families/seed instead of 10) may be under-powered.
2. Progressive (non-iterative) guide-tree propagation genuinely erases most of the
   theoretical benefit of a richer gap model, because early greedy merges lock in gap
   placements before the 2-piece model's flexibility can be exploited — consistent with
   the out-of-scope note in the plan that iterative refinement was excluded.
No result was adjusted, no seed or subset was cherry-picked, and this negative result is
reported as-is per `negative_result: true` in `results.json`.
