Skip to content

C++ search#210

Draft
ms609 wants to merge 909 commits into
mainfrom
cpp-search
Draft

C++ search#210
ms609 wants to merge 909 commits into
mainfrom
cpp-search

Conversation

@ms609

@ms609 ms609 commented Mar 19, 2026

Copy link
Copy Markdown
Owner
  • other optimizations + features

Manual testing underway; shiny app in particular has some usability issues.

@ms609
ms609 marked this pull request as draft March 25, 2026 14:21
ms609 added a commit that referenced this pull request Mar 28, 2026
ms609 added a commit that referenced this pull request May 18, 2026
In R CMD check, R runs as a non-interactive subprocess with captured
stdout. R_FlushConsole() calls fflush() on that pipe; when the buffer
fills the call blocks indefinitely, causing the 6 h GHA timeout seen
on every ubuntu runner for PR #210.

Gate the \r-overwrite progress line and the flush behind R_Interactive
(FALSE in batch/check contexts). Interactive sessions are unchanged.
At verbosity >= 2 in batch mode, emit plain \n-terminated lines so
diagnostic logs still carry progress detail without the flush risk.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ms609 added a commit that referenced this pull request May 18, 2026
In R CMD check, R runs as a non-interactive subprocess with captured
stdout. R_FlushConsole() calls fflush() on that pipe; when the buffer
fills the call blocks indefinitely, causing the 6 h GHA timeout seen
on every ubuntu runner for PR #210.

Gate the \r-overwrite progress line and the flush behind R_Interactive
(FALSE in batch/check contexts). Interactive sessions are unchanged.
At verbosity >= 2 in batch mode, emit plain \n-terminated lines so
diagnostic logs still carry progress detail without the flush risk.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…te (#245)

Closes the coverage gap that let the IW NNI rescore bug (3df9088) ship.
The existing IW NNI test (test-ts-spr-nni-opt.R) only asserted
result$score >= 0 and topology shape — it never cross-checked that the
returned score matched the IW score of the returned topology.  Pre-fix,
nni_search did new_score = best_score + integer_delta unconditionally,
so under IW the returned score was the accumulated wrong float and the
existing assertions still passed.

New file mirrors the EW pattern from "NNI: 15-tip random dataset"
(result$score == ts_score(rt, ds)) but under concavity=10, sweeps a few
concavities, and exercises multistate + NA cases.  Profile parsimony
isn't reached because ts_nni_search's Rcpp bridge doesn't forward
infoAmounts; the IW path through compute_weighted_score is the same
codepath the fix unified, so this catches both modes at the C++ level.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
ms609 and others added 22 commits June 1, 2026 06:25
)

* line endings

* feat(ls): least-squares distance scoring for the C++ search kernel

Add a least-squares (LS) tree-fitting capability to the optimised C++
search kernel, so the existing NNI/SPR rearrangement engine can find the
tree that best fits a target distance matrix under a least-squares
criterion -- not just minimise parsimony. Built for Lapointe & Cucumel's
(1997) average consensus procedure (a new sibling `Consensus` package),
where an averaged, generally non-additive patristic distance matrix must
be fit by a heuristic topology search.

- src/ts_ls.{h,cpp}: design matrix over the tree's unrooted branches
  (splits + pendant edges, 2n-3 columns; root's two edges merged), OLS
  (normal equations + Cholesky) and NNLS (Lawson-Hanson) solvers, RSS,
  and NNI/SPR LS hill-climbing reusing the TreeState move primitives on a
  topology-only tree (never touches the Fitch state arrays).
- ts_ls_fit / ts_ls_search Rcpp entry points (src/ts_rcpp.cpp, registered
  in TreeSearch-init.c).
- R/LeastSquares.R: LeastSquaresFit() (fixed topology) and
  LeastSquaresTree() (search from NULL/NJ, one tree, or a multiPhylo),
  returning unrooted trees with fitted branch lengths and an "RSS" attr.
  Supports OLS/NNLS and unit/Fitch-Margoliash/custom weighting.

Validated against phangorn::nnls.tree() and designTree() to machine
precision (tests + dev/ls_validate.R). The existing parsimony API is
unchanged. Singular (rank-deficient / zero-weight) fits fail gracefully
with a warning rather than crashing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* spell: add Cucumel, Lapointe, Margoliash to WORDLIST

Author names introduced in the LeastSquares documentation and bib entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
WideSample() now selects topologically spread trees by solving the Max-Min
Diversity Problem with the MaxMin package, dispatching by a quality tier
auto-chosen from length(trees):

- exact node-packing IP (highs) for very small sets,
- DropAdd-TS while the dense distance matrix is affordable,
- matrix-free anchored Gonzalez (on-demand distance-column oracle) beyond
  that, so the largest tree sets never materialise an N x N matrix.

- Imports: MaxMin (Remotes: ms609/MaxMin) instead of FurthestPoint; the
  GonzalezColumn / DropAddTS / ExactMaxMin calls dispatch to MaxMin.
- Add Porumbel (2011) and Sayyady & Fathi (2016) references.
- dev/smoke_40k.R: 40,000-tree matrix-free smoke test.
- test-WideSample: 16/16 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ms609 and others added 30 commits July 13, 2026 13:33
…ounded scan

Within each (has_inapp, weight) block group, order characters by descending
homoplasy (minority-state mass) as a tertiary sort key, so the bounded
indirect-length scorers reach `cutoff` in fewer blocks on rejected candidates.
Measured ~3% fewer blocks scanned on the EW reroot-x4 path (~82% of EW per-move
work).

No valid move is ever dropped: a candidate below cutoff is always fully scanned
and accepted regardless of block order; only rejected candidates bail early, and
only their discarded partial changes. The score is order-invariant (covered by
the existing "Scores are correct after block reordering" test). Because the
search is stochastic (RNG seeded from R), a faster bail can realign the stream
to a different-but-equally-valid path — reach is neutral-to-better across seeds,
not a regression.

Cost measure: `minority` = informative tips off the plurality state. Chosen over
`entropy` (log() risks ULP-level sort-key flips across platforms) and `min_steps`
(the offset-out floor; constant on multistate, =1 on all binary chars). Env knob
TS_CHAR_ORDER {none,min_steps,minority,entropy} allows retuning/disabling without
a rebuild. Full search/scoring suite green (0 failures / 1229 assertions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Durable deliverables from the TNT per-move kernel-gap investigation on
project5432 (the ~24x per-candidate gap):

- profiling/exactness-gate.md (+ .R harness): three-way empirical gate over
  6066 candidates. Exact directional edge_set = truth; union of TRUE finals is
  a sound lower bound; the DEPLOYED union (uppass_node final_) strictly
  OVER-counts (unsafe as a screen).
- red-team/union-of-finals-bound-proof.md: math-prover derivation that
  final(A)|final(D) subset E(D) for the deployed finals => upper bound, the
  reverse of the old in-code comment. Grounds the ts_fitch.cpp comment fix.
- profiling/l3b-footprint-482.md: L3b changed-view footprint collapses with N
  (mean fp_ref 0.18 at 482t) -> incremental edge-set FEASIBLE at scale;
  refutes the small-N "dead" verdict. Feasibility basis for lever #6.
- profiling/s7-fastpath-sizing.md: #7 sized to ~5% whole-search (not 10x); the
  64-bit Hamilton TNT is SSE2 (not scalar) -> the gap is fewer-words-per-move,
  not per-word arithmetic.
- plans/2026-07-14-lever7-scorer-monomorphization.md and
  plans/2026-07-14-lever6-incremental-edgeset-land.md: self-contained build
  specs (land #7 first).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The NOTE on fitch_indirect_length claimed the union final(A)|final(D) is a
SUPERSET of the true directional edge set and therefore UNDER-counts (a sound
lower bound). That is backwards for the finals the function actually reads:
tree.final_ is the simplified uppass_node up-pass (final subset prelim, never
unions the parent state in), so final(A)|final(D) subset edge_set[D] = MPR(D)
=> the union OVER-counts and is an UPPER bound, NOT a sound lower bound.

Documentation only; no behavioural change. The function stays a heuristic
ranker (temper) whose chosen move is re-scored exactly. Comment now warns it
must NOT be used as a pruning/rejection screen (it can discard improving moves)
and cites the proof at dev/red-team/union-of-finals-bound-proof.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lift the per-candidate criterion-flavour dispatch out of the plain-EW SPR
hot loop into a compile-time template (spr_scan_plain_ew<UseFlat,Wrong>),
selected once per weight-class at the scan site. The dead-in-plain-EW
branches (NA/IW/sector/constraint/collapsed/b2) compile away, and the
UseFlat=true instantiation uses the flat kernel (fitch_indirect_cached_flat)
— weight-blind, so UNSAFE as a global toggle but SAFE as an instantiation
because dispatch selects it only for all_weight_one data.

EXACT / byte-identical: default ON, kill-switch TS_EW_MONO_OFF. Gate passed
on Wortley2006/Zanol2014/Zhu2013 x>=2 seeds via ts_tbr_diagnostics AND
MaximizeParsimony (score + candidates_evaluated + per-pass identical), plus
a clean-HEAD binary cross-check and a positive control (split flat/cached
fire counters + a diverging wrong instantiation). The MaximizeParsimony gate
caught + fixed a real bug: collapsed_all_zero went stale across mid-search
compute_collapsed_flags recomputes; now refreshed at every recompute.

Per-candidate SPR: +12-19% on all_weight_one (flat kernel), +6% on weighted
(dead-branch strip). Whole-search ~2-5% unit-weight / ~1% weighted, exact.
Orthogonal to 5432 reach. See dev/profiling/s7-land.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…easure

Address review before hand-off:
- Compile-time-gate the TS_EW_MONO_WRONG corruption behind -DTS_EW_MONO_WRONG
  (like the removed TS_EW_MONO_ASSERT probe). A production binary now has NO
  env-var path to a corrupted scorer; verified TS_EW_MONO_WRONG=1 is INERT on
  the default build, and the positive-control divergence reproduces only on a
  flag-built binary. Closes the footgun.
- Widen the MaximizeParsimony byte-identity gate to 6 seeds x 3 datasets
  (18/18 PASS) confirming the collapsed_all_zero-refresh fix is not seed-luck.
- Add the faithful whole-search wall-clock (full MaximizeParsimony, nThreads=1,
  min-of-runs): Zhu2013 +1.3%, Zanol2014 +0.1% — lower than the single-descent
  proxy (+2.7%) because ratchet/sector/enum phases don't hit the flat SPR path.
  Reword the cross-dataset flat-vs-strip comparison. Final airtight preclean
  (CCACHE_DISABLE) build re-confirms byte-identity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eteness

Post-review doc polish: state the whole-search number as the one resolvable
unit-weight point (Zhu +1.3%) rather than firming a range; record that
collapsed_all_zero is the ONLY per-clip-mutable input to the ew_mono_plain
gate, so refresh-at-every-recompute closes the whole staleness class (not just
the Zhu2013/seed202 instance).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add spr_scan_plain_iw<Wrong> and a 3-way SPR dispatch (EW-flat / IW / general
loop) gated on a shared refreshed mono_plain predicate. IW has no flat kernel
(implied weights are per-pattern), so the win is the dead-branch strip only;
the scorer + accept are reproduced verbatim from the general loop's IW branch.

Byte-identical (default ON; TS_EW_MONO_OFF reverts both EW+IW):
- ts_tbr_diagnostics IW 6/6 + MaximizeParsimony/XPIWE 9/9 (score to full
  precision, candidates_evaluated, per-pass all identical); IW template fired
  100% of clips (iw fire counter); wrong IW instantiation diverges on the
  -DTS_EW_MONO_WRONG build. EW regression re-checked 6/6 after the refactor.
- Per-candidate IW SPR-specific +2.7-4.0% (strip only; the heavier IW
  double-accumulator scorer dilutes the fixed branch fraction below EW's ~5-6%).
  IW/XPIWE is the production default, so this is the common path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mpleteness)

Independent supervisor review found the lever-7 collapsed_all_zero gate could
go stale at ONE recompute site the "every recompute" fix missed: the legacy
physical-reroot path (TS_PHYS_REROOT=1, ~ts_tbr.cpp:2756) recomputed `collapsed`
without the refresh_collapsed_all_zero() that the default fast-reroot (2735) and
accepted-move (2671) sites carry. Latent (default phys_reroot=false never runs
it; NEW==BASE stayed byte-identical on Zhu2013 101/202 even under
TS_PHYS_REROOT=1), but it broke the pattern and could diverge where a physical
reroot flips a non-root-child zero-length branch into existence.

Add the same refresh at 2756. Re-verified byte-identical NEW-vs-BASE (via the
TS_EW_MONO_OFF kill-switch) on the default path (ts_tbr_diagnostics 6/6 +
MaximizeParsimony Zhu2013 101/202, cand identical) AND under TS_PHYS_REROOT=1
(1.08-1.22B candidates identical). s7-land.md updated to note the third site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Objective (i) always-find-best-score and (ii) be-as-fast-as-possible are
distinct problems on the current frontier:

- Mission A (cold-start basin capture): 5432's 3-step/0.15% gap is PURE
  cold-start basin capture (1943 is a stable TS optimum TS's TBR rebuilds from
  within ~8-12 TBR; cold search lands ~109 away). Lists every refuted lever, the
  one that moved the floor (reweighting kick 5->auto), and the open constructor
  angle. Existence-before-build.
- Mission B (8x kernel): the ~30x per-move gap; the live lever is the Goloboff
  1996 up-aware approximate screen. Gated on read-the-paper + vary-n_states
  discriminator + naming where TS is kernel-bound-and-losing, and a sound-bound
  soundness proof (the up-ignoring screen was proven unsound).

Self-contained briefs so each mission can run in its own fresh session off
auto-memory. #6/#7 (per-move micro-levers) landed; these target the real gaps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rift/sector opt-in + findings (#273)

* docs(profiling): settle union-of-finals exactness gate (empirical)

Fresh rooted union-of-finals is NOT an exact EW-Fitch insertion cost, under
any rooting or on binary data. Three-way empirical verdict over 6066
reconnection candidates vs TreeLength full-rescore ground truth:

- Per-child edge set E(D)=combine(prelim(D),up(D)): EXACT (6066/6066).
- Union of TRUE edge sets E(A)|E(D): sound LOWER bound (under-counts, never exact).
- Union of the DEPLOYED simplified final_ (fitch_indirect_length): OVER-counts
  (unsafe as a lower bound), because uppass_node's simplified rule never unions
  so final_ is a strict subset of prelim. Fresh finals => formula limitation,
  not staleness. The code comment claiming it under-counts is wrong for the
  deployed path.

=> "root like TNT -> cheap exact union-of-finals" refactor is a mirage.

Adds dev/profiling/exactness-gate.md (verdict) + exactness-gate.R (harness).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(drift,spr): opt-in exact directional EW scorer + scan-error census

The DRIFT phase (ts_drift.cpp) and standalone spr_search (ts_search.cpp) still
scored pure-EW reconnection candidates with the union-of-finals approximation
(fitch_indirect_length_bounded, reading the incrementally-maintained
tree.final_), never migrated to the exact directional edge set that main TBR
adopted in 2b299e4.

Severity (dev/profiling/drift-exactness-gate.md): no inexact score is ever
leaked (every accept is drift_full_rescore'd / full_rescore-verified), but on
the deployed incremental path the union error is TWO-SIDED (over- AND
under-counts up to ~100 steps, not the strict over-count the exactness gate
measured with fresh finals). argmin selection then suffers an optimizer's curse
(picks moves the scan is optimistic about), so drift admits moves outside its
AFD envelope (env_violation 52-62% on Zhu2013/Zanol2014) and spr_search
converges prematurely (union 810 vs exact 639).

Fix (opt-in, union stays default; NA/IW untouched, gate !has_na && !use_iw):
- TS_DRIFT_EXACT routes drift's SPR + reroot EW scans to
  fitch_indirect_length_cached over a per-clip compute_insertion_edge_sets,
  mirroring tbr_search.
- TS_SPR_EXACT does the same for spr_search.
- TS_DRIFT_SCANCHK: two-sided scan-error + decision census (select_flip,
  env_violation, applied-move predicted-vs-full_rescore; cf. TS_IW_SCANCHK).

Validation: exact path applied_mismatch == 0 across all applied moves incl.
reroots; default (flags-off) path byte-identical (drift/SPR/char-ordering tests
pass). Local matched-wall pilot: frontier CROSSOVER -- exact wins the auto/
wall-race regime on all 3, union wins the thorough/saturated regime on 2/3 ->
land opt-in, do not flip the default. spr_search is an unambiguous reach win.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(hamilton): drift-exactness matched-wall gate build + array scripts

Dedicated build (lib-driftexact, does not clobber the shared cpp-search lib)
fetching claude/happy-jennings-e7a165, and a 3-task array (one per dataset)
running dev/profiling/drift-exactness-gate-bench.R at 30 seeds x nCycles
{8,16,32,64,128,256}, union vs TS_DRIFT_EXACT, to resolve the matched-wall
plateau-vs-climb crossover the local pilot showed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(hamilton): resolve gate deps from shared lib (R_LIBS_USER)

lib-driftexact holds only the branch's TreeSearch; point R_LIBS_USER at the
shared TreeSearch/lib so Rcpp/TreeTools/ape resolve at build+run time while the
harness loads our TreeSearch via lib.loc=GATE_LIB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(drift): close matched-wall gate with 30-seed Hamilton result

Hamilton array (17870087, 30 seeds x nCycles {8..256}, 3 datasets) confirms the
8-seed pilot's matched-wall CROSSOVER with clean monotone gaps: exact wins the
auto/wall-race regime on all three (~2.7-3x faster, no time-to-optimum
regression), union wins the thorough/saturated regime on 2/3 (Zhu2013, Zanol2014
- exact plateaus, union over-diversifies deeper); Dikow2009 exact dominates every
budget. best-of-30 equal on all three (reliability effect, not reachability).

Verdict: LAND OPT-IN (TS_DRIFT_EXACT), do not flip the drift default. Adds the
per-dataset raw CSVs and the frontier reanalysis script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(drift): soften saturated-tail claim (nCycles-cap censoring)

The nc<=256 gate compared a censored exact tail (exact ~3x faster, so frozen at
its nc256 score with unspent wall) against union's fuller curve; exact was still
descending nc128->256, not plateaued. So "union wins thorough on 2/3" is not
established. auto/wall-race win stands. Extension (exact+union nc {512,1024} on
Zhu2013+Zanol2014) running to settle at matched wall. Opt-in landing unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(drift): resolve saturated tail — crossover is real (nc1024 check)

Extended exact+union to nc {512,1024} on Zhu2013/Zanol2014 (30 seeds) to remove
the nCycles-cap censoring. Exact does NOT plateau (Zhu 624.80->624.40) but
descends slower than union; at genuinely matched wall union stays ahead through
the saturated tail (Zhu floor 624.0 vs exact ~624.4; Zanol narrower ~0.13). So
the auto/thorough crossover is real, not an artifact: exact wins the wall-race
regime on all three, union wins the thorough regime on 2/3.

Verdict CLOSED: land TS_DRIFT_EXACT opt-in, do not flip the drift default. Adds
the extension CSVs and the combined-frontier reanalysis script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(bench): sector-godrift isolation gate harness + Hamilton array (route B)

Full "thorough" search with driftCycles=0 (main-tree drift off; verified gd=0 =>
0 drift calls, gd=25 => all-sectoral) isolates the in-sector godrift scorer.
Three arms none/union/exact, maxReplicates knob, matched-wall frontier. Array
adds mbank_X30754 (180t) as the larger/harder case where sector drift is most
active. Reuses lib-driftexact (no rebuild).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(spr): make exact directional the default EW scorer for spr_search

spr_search is a pure hill-climb with no exact-TBR phase to launder union's
over-count, so union converges catastrophically short (multistart best-of-25:
Zhu2013 650 vs exact 625; Zanol2014 1292 vs 1265 — systematic, not variance),
and exact is also faster per start. No crossover, unlike drift. Flip default to
exact; TS_SPR_UNION restores the old scorer (kill switch). Tests green
(SPR/spr-nni-opt/spr-state-restore/drift). Reach today = standalone
ts_spr_search (sprFirst OFF in presets), but this makes an SPR warmup viable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(bench): route-3 marginal-value experiment (drift vs ratchet at matched wall)

From a shared per-seed local optimum, compare where a wall-second is best spent:
incumbent ratchet vs exact-drift vs union-drift (control). If exact-drift's
anytime frontier beats ratchet's over some wall range and dominates union-drift,
drift earns a place in wall-limited recipes (route 3) -> worth re-tuning.
mbank_X30754 (180t) is the large/hard anchor. Reuses lib-driftexact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(bench): route-3 follow-ons — +replicate gap-closer + training-corpus recipe test

arm A (+replicate): is a wall-second better on exact-drift than on the project's
real incumbent lever, more RAS+TBR replicates? Same datasets as route3 for
frontier comparison.
arm B' (recipe): whole-search thorough on TRAINING-split MorphoBank (EW-recoded,
one key/task, sector drift off), arms none/union/exact main-tree drift, to see
if the drift scorer choice moves a wall-limited recipe. Both reuse lib-driftexact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(drift): route-3 recipe verdict — NO-GO (marginal win, ensemble-redundant)

Marginal value (from a shared local-opt): exact-drift is the best single
continuation, beating ratchet + union-drift + replicate. But the whole-search
recipe test on 8 training-split MorphoBank keys (EW, sector-drift off,
sequestered) shows main-tree drift is REDUNDANT in the multistart+ratchet+
sectorial+TBR ensemble at both saturated (none ties + faster) and tight-budget
ends (exact beats none only 1/4 keys). So the isolated win doesn't survive ->
no recipe surgery; exact-drift stays opt-in (TS_DRIFT_EXACT), D preset-refactor
not worth it. Adds companion note + raw CSVs + analysis. (spr_search exact
default stands as the real landed win from this line.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… Scheme 1)

Exact incremental replacement for the per-clip from-scratch
compute_insertion_edge_sets recompute, gated to the plain directional EW/IW
TBR regime (TS_L3B_INCREMENTAL, default OFF). Patch-from-full "Scheme 1":
maintain a pristine per-pass intact-tree base; per clip patch only the changed
frontier (worklist incremental up-pass, value-based attenuation stop), score,
then restore base over the changed-node list. Shares ts_fitch_combine with the
from-scratch path so patched words are byte-identical.

Chose Scheme 1 over the design doc's Euler "Config C": the 482t evidence that
reopened the lever (fp_ref) supports Scheme 1; the Euler descend-delta was
killed at small N and never re-measured; the clip loop already restores per
clip (Scheme-1-native) and keeps the shuffle => bit-identical C-vs-A. Advisor
concurred. Rationale + results in dev/profiling/l3b-land.md.

Correctness: per-clip oracle (edge_set == from-scratch, full array) mismatch=0
across random EW (64 configs), real multi-state/ambiguous panel + project5432
subsamples, EW+IW, single-rooting and do_reroot; C-vs-A bit-identical; tbr
completeness oracles clean. do_reroot supported (not a footprint penalty).

Wall (raw tbr_search, real project5432, bit-identical trajectory): wash at
<=75t, 1.10x@120t, 1.39x@240t, 1.36-1.38x@482t (both unrooted) — the predicted
large-N ceiling. Open: end-to-end MaximizeParsimony at shipped ratchet on
Hamilton (sector share is gated out, so end-to-end is diluted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full MaximizeParsimony C-vs-A on project5432 (EW): 1.01x wash at both 120t and
240t, despite raw tbr_search being 1.39x at 240t. Coverage confirmed (l3b fires
in driven+ratchet; sector ~30% gated out) and the per-clip oracle is clean in
the ratchet upweight/collapsed context. The raw-TBR win is diluted to nothing
end-to-end by the sector exclusion + per-pass base-recompute amortization across
ratchet's short passes. Verdict: exact/correct large-N raw-TBR lever, NOT a net
mission-wall lever as built -> land default-OFF; the unlock is incremental base
update after accept. Replaces the earlier ~1.2x end-to-end estimate with measured
numbers; marks the Hamilton 482t full-search run not-worth-running until the
base-update follow-on lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ran the cheap discriminator (MP 240t, TS_L3B_STATS+ORACLE): in-search fp_changed
= 0.244, essentially the clean 240t footprint (0.243) -> the per-clip patch win is
INTACT, refuting "in-search fp elevated -> win structurally small". So the wash is
sector-exclusion (predicts ~1.24x) + the per-pass O(n_node) base recompute, which
MP's short ratchet passes amortize poorly. Confirms base-incremental-update as the
plausible unlock; notes it is itself a correctness-critical kernel (TBR-accept, two
dirty paths + reroot, harder than the clip patch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… honest verdict

Replaces the per-pass full compute_insertion_edge_sets base recompute with an
O(footprint) in-place update after each accepted SPR move
(update_base_after_spr_move): the move decomposes into a remove-leg at nz + a
single-site insert-leg at above; nx gets a fresh up so A1-path up is recomputed
(not assumed invariant). Gated SPR-only (TBR-reroot/reroot/first-pass fall back
to full recompute); pass-top recompute skipped when l3b_base_current; buffers
synced over the changed set; kill switch TS_L3B_NOBASEINCR. Base oracle
(base == from-scratch after each SPR accept) clean across EW+IW tbr_oracle
(to 120t x18) and full ratchet MaximizeParsimony on real Zhu2013/Zanol2014;
first-version root-child seed bug caught by the oracle and fixed.

Measured (project5432, EW): base-incr removes the per-pass recompute, converting
plain-l3b's small MP regression (0.99x @240t) into a clean wash (1.00x), and
preserves raw tbr_search 1.39x@482t / 0.97x@37t. But it yields NO net MP win =>
the base recompute was NOT the bottleneck. The MP wash is structural: short
ratchet-recovery passes (few clips/pass) + sector ~30% excluded. Verdict
unchanged and now complete: exact large-N raw-TBR lever, not a mission-wall
lever; land default-OFF. STOP (advisor gate: fixed cost removed, still no win).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The L3b incremental edge-set patch is EXACT (bit-identical trajectory) and
1.36-1.38x faster on raw large-N TBR (dev/profiling/l3b-land.md); end-to-end
MaximizeParsimony is a clean wash (neutral, with the base-incremental follow-on).
Per objective (ii) be-as-fast-as-possible, take the free large-N win: default-ON
for n_tip>=150; small trees (where the raw-TBR win washes out) pay zero overhead.

Gate: l3b_active = !TS_L3B_OFF && (TS_L3B_INCREMENTAL || n_tip>=150) && use_directional && ...
  TS_L3B_OFF          kill-switch (full disable)
  TS_L3B_INCREMENTAL  force-on regardless of size (A/B + small-tree testing)

Verified on the merged+flipped build (CCACHE_DISABLE --preclean; header changed):
force-ON == force-OFF byte-identical on the panel (Zhu/Zanol x2 seeds:
score+n_evaluated+passes), l3b fires (patch_clips=3909), n_tip<150 default inert
(== pre-#6 #7 baseline). The n_tip>=150 exact path was validated at 482t by the
chip's per-clip oracle + C-vs-A.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The single-seed assertion (seed 1001) that zero-mode and upweight-mode
ratchet produce different search statistics coincidentally held equal on
macOS after d8c5999 (homoplasy character ordering) shifted the RNG-seeded
trajectory under libc++'s std::sort tie-break. On a 15x10 binary matrix the
two modes coincide on ~3-7% of seeds; which seeds coincide is platform
dependent, so a single fixed seed is fragile.

Assert instead that the modes diverge on >=1 of several seeds (early break),
so an incidental single-seed coincidence no longer fails while a genuine
collapse (upweight silently == zero) still does. The deterministic
regime-distinctness guarantee is already covered by test-ts-na-evcache.R.

Validated green on a clean committed-HEAD build (CCACHE_DISABLE=1 --preclean):
all 9 blocks pass, no crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…liverable

ratchetPerturbMaxMoves 5->auto/scaled corpus anytime A/B (Hamilton job
17872154, 25 training matrices x 3 arms, validation sequestered) => NO broad
wall benefit: 14/25 tie exactly; clean large/xlarge reach the optimum in the
same replicate (rep2hit ratio 1.000); narrow reach regression on a degenerate
4062t giant; marginal noisy +reach on one hard multi-basin matrix. Verdict:
keep the default at 5; the kick is an opt-in hard-tail lever, not a default.

Rewrite dev/plans/2026-07-11-...: retire the FALSIFIED clade-generation thesis
(restore238 shows TS's TBR rebuilds 1943 from within ~8-12 TBR moves), reframe
to cold-start basin capture, correct the numbers, fold in the kick result.

Adds kick_anytime.R (harness), kick_anytime_hamilton.sh (SLURM array),
kick_analyze.R (analysis), kick_anytime_FINDINGS.md (write-up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…from 1943)

Mission A angle #2 (push the reweighting kick as a basin-HOPPING schedule to
cross the last steps past the ~1945 TS floor on project5432).

Mechanism: src/ts_ratchet.cpp is a MONOTONE strict-descent ILS (accept-only-if-
strictly-better, else reset-to-best), so strength/schedule/multi-kick all stay
inside a monotone frame; the only escape lever is the acceptance frame
(accept-worse: drift, or reweighting with accept-worse between cycles).

Gate (buildless, dev/benchmarks/basin_hop_gate.R): ordered-char re-score by label
(self-check L(1943 floor)==1943) + TBRDist(exact=FALSE, hard lower bounds) to the
1943 floor. Every near-optimal TS tree from EVERY generation method (kick 1945 /
strongperturb / cold / random / largefirst / invweight, 1945-1958) is tbr_min >=97
TBR from 1943 (min 97, best kick only 2 score-steps above 1943). Corroborated by
basin_5432 (descent 0/5 by d~20) and geom_full (~109). No accept-worse schedule
walks ~100 rearrangements to a narrow distant basin, so the afd-sweep probe and the
accept-worse build were NOT run.

Scope: refutes route #2 only (escape the floor). Angle #1 (cold-start constructor)
and #3 (structured breadth) are untouched - they generate a start rather than walk
from a floor. Kick is not useless (CID 0.22 vs cold 0.38: a better LOCAL basin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gating [focus-test: ts-ratchet-opt]

R-CMD-check gates the macOS `full` job behind the ubuntu/windows `core` wave
(full needs core needs sense-check), so a flake elsewhere stops the macOS run
from ever building. focus-test runs ONE testthat file on ONE OS (default
macOS-latest) with no full R CMD check -- via the Actions tab (workflow_dispatch)
or a [focus-test] / [focus-test: <filter>] marker in a commit message.

This commit's own [focus-test: ts-ratchet-opt] marker triggers a macOS run of
the de-flaked ratchet test as a live check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auto-memory is now organised as campaign hubs indexed at the top of MEMORY.md; record the write-side rule (advance a campaign by updating its hub the same turn) on the always-loaded project-instructions channel so future agents follow it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o diameter build)

Zero-code precondition for a TNT-style dss (diameter-limited local sector) build.
The dss benefit (local sector -> cheaper escape) is driven by TIP-COUNT, which the
existing sectorMaxSize knob controls; rev5's missiongate sweep never varied it small
(held maxS>=0.65n). This probe sweeps small maxS {12,20,30} x ras=3 from the fixed
TNT T0, vs a large-sector anchor and ratchet, on the missiongate protocol.

Local pilot (Zanol s1, Zhu s1, n=3) + Zanol reach test (n=16 restarts, up to r=256):
- steps/Mcand APPEARS to favour small sectors (-2.4 vs ratchet -1.5) but this is a
  candidate-COUNT artifact: small-sector candidates cost ~2x ratchet's per wall
  (per-pick reduced-dataset build + 3x RAS overhead), so on the WALL axis (the true
  mission axis) ratchet dominates the hard target (-29 vs -24 steps/s, reaches deeper).
- REACH CEILING: small sectors PLATEAU at -12 on Zanol at ANY budget; ratchet reaches
  the -13 target. Whole-tree-barrier prior (rev2): a sector <= maxS tips cannot emit
  the split(s) for the -13 reorganization.

=> dss/diameter-limiting is strictly MORE local -> shallower reach -> cannot help.
No C++ build. Confirms + extends rev5 (sectorial Pareto-dominated by ratchet) to the
small-maxS regime it never measured. Hamilton n=15 confirmation scripts included.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-a source dir gone on Hamilton)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…losed

Hamilton array 17898332 (build 17898331, HEAD 023b038), 12 cells x n=15
(3 TNT-T0 seeds x 5 reps), corrected wall+reach aggregate.

VERDICT 0/4: no small-sector config reaches ratchet's median depth at <= its wall.
On the hard Zanol target ratchet dominates EVERY axis incl the biased steps/Mcand
(-1.90 vs best smallmax -1.78). The n=3 local-pilot "lead" (maxS=12 r=1 = -2.39
st/Mcand; a Zhu wall win) was noise + metric bias: at n=15 it falls to -1.19 and the
Zhu win vanishes (ratchet med -3 vs smallmax -1).

steps/Mcand is a BIASED axis here: small-sector candidates cost ~2x ratchet's per
wall (reduced-dataset build + 3x RAS overhead over fewer candidates), so it flatters
small sectors. Adjudicated on WALL + reach instead.

=> dss/diameter-limiting is strictly MORE local -> shallower/less-competitive ->
cannot help. No C++ build. Confirms + EXTENDS rev5 (sectorial Pareto-dominated by
ratchet) to the small-maxS regime rev5 never measured. Also refutes rev5's handoff
nuance (small maxS + ras=3 vs single ratchet start on Zhu/Giles): smallmax median
<= ratchet median on all cells. Full writeup: DSS_FINDINGS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… retested

The selectem_large anchor used auto picks (~3/round), under-provisioning rev5's
selectem (explicit pk 5-20): my Zhu selectem_large median -1 vs rev5's -6. So it is
NOT a faithful rev5 reproduction and cannot speak to rev5's handoff (large-band selectem
being lower-VARIANCE than a single ratchet start on Zhu/Giles — a variance question never
measured here). Retract "refutes rev5's handoff"; that stays OPEN as rev5 left it.

Core verdict UNAFFECTED: the smallmax arm used auto picks too, but for small bands that
gives MANY picks (~16/round at maxS=12) = over-provisioned, so its 0/4 loss vs ratchet
(esc_med-vs-ratchet-at-wall, same pooled seeds) is decisive. Also de-emphasised the
cross-seed-pooled reach flag as non-load-bearing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TS_PACK_LOCAL packs each 64-char block to its own applicable-state alphabet
rather than the global one, cutting Fitch planes scanned per candidate on
low-state morphological data (~1.1x small -> ~1.5x char-rich, climbing with
nChar). Score-EXACT per tree; now default-ON (set TS_PACK_LOCAL=0 to force off).

- ts_data.cpp/.h: default-ON flip; add CharBlock::plane_state[] mapping each
  packed plane back to its global applicable label (identity in the unpacked
  layout), populated in build_dataset by inverting loc[] through state_remap.
- ts_rcpp.cpp: ts_na_debug_char reconstruction consumes plane_state[] so packed
  output prints the global state label, not the plane index.
- test-ts-pack-local.R (new): score-exactness (TreeLength ON vs OFF) + a
  hand-built non-contiguous-alphabet fixture that exercises plane_state[] -- the
  only gate that does, since the scorer is index-agnostic.
- reeval pack_{gate,reach,mpt_gate}.R: OFF-arm switched to TS_PACK_LOCAL=0
  (unset now means ON).

Validation: pack_mpt_gate Sansom2010 EW/IW/NA byte-identical ON vs OFF (score +
ntree + candidates_evaluated, 3 seeds; NA reproduced the 66-tree MPT set); full
testthat default-ON 0 failures (11337 pass); corpus 18/18 reach-neutral.
ts_na_debug_char is package-internal (no exported surface changed) and
PlotCharacter is pure-R, so no user-visible reconstruction changes. The packed
ambiguous-set content differs (local vs global alphabet) -- correct, not a
regression: phantom states never appear in an MPR, proven by identical
score+candidates_evaluated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give the 4-wide flat reroot batch kernels (fitch_indirect_cached_flat_x4 /
indirect_iw_cached_flat_x4 + NA) the same next-row software prefetch the scalar
reroot path already has. The x4 batches issued 4 cold demand loads to scattered
vroot_cache rows per batch with no hint; the in-batch MLP can't hide the
batch-boundary head latency (cutoff early-break is a control dependency).

EW x4 prefetches kept_ei[ki..ki+3] (next batch). IW x4 tracks a parallel iw_pf
cursor into kept_ei -- valid because its skip predicate is identical to the
kept_ei construction (use_collapsed == !collapsed.empty(), ts_tbr.cpp:1532), so
kept_ei is the same candidate sequence in the same order. Purely additive and
flag-gated (opt-in TS_TBR_PREFETCH=1), so flag-OFF is byte-identical to prior
committed code by construction; a prefetch hint cannot change a score anyway.

Byte-identity validated: ts_tbr_diagnostics ON vs OFF on project970, EW + IW,
identical score + n_candidates_evaluated (38.9M/44.8M) + n_clips + final edge.
Local Windows/MinGW A/B flat (project970 60.0->60.2, project510 57.1->57.0, no
regression) -- expected, since the memory-latency win only surfaces in the EPYC
L3-pressure regime. Landed default-OFF as a validated building block per the
lever-6 precedent; the win/wash is settled on Hamilton EPYC raw-TBR + full
MaximizeParsimony wall before any default flip. See dev/profiling/kernel-prefetch-x4.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the +1 slack on the EW/NA integer bail cutoff (best_candidate - divided_length
+ 1 -> + 0) at the EW sites: spr_scan_plain_ew template, SPR general, reroot x4,
reroot scalar. The scorers bail on extra_steps >= cutoff; the +1 let an
equal-length, non-improving candidate accumulate its full extra before rejection
(scanning every block on the near-optimal plateau), whereas +0 bails it the moment
its running extra reaches the current minimum.

Byte-identical: acceptance is strict < everywhere, so a bailed candidate (extra >=
cutoff = best-divided) is rejected identically, a true improver never reaches the
tighter cutoff, and an equal candidate bails one block earlier but is still
rejected. The +1 was not protecting acceptEqual (decided post-scan on the recorded
best_candidate), and the drift path already ships slack 0. Gated by a cached
tbr_cutoff_slack() (default 0; TS_TBR_LOOSE_CUTOFF=1 reverts to the historical +1
for the A/B). Left the +1 on spr_scan_plain_iw (cutoff dead on the IW path -- IW
bounds on the float best_candidate), the IW-float cutoff, and the INT_MAX init.

Validated byte-identical (score + n_candidates_evaluated + n_clips + final edge)
tight vs loose on project970 EW + IW + Sansom2010 NA. Chosen by the post-packing
lever-hunt workflow with 3/3 adversarial verifiers confirming exactness + non-wash.
Local ns A/B is flat (blocks-scanned is hardware-independent, so representative):
the exact-equal-full-scan population is small in practice (dedup skips equivalent
rerootings; worse candidates already bail early). Landed default-on regardless --
byte-identical, guaranteed non-negative, the correct tighter bound; may pay in the
raw-TBR @482t plateau regime, to be checked on the EPYC A/B. See dev/profiling/kernel-cutoff-tighten.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ened it)

Whole-program -mavx2 is 8-9% faster per candidate than the package -msse2 build
(project970 60.6->55.2, project2668 34.7->31.9) because the target(avx2) reduce
helper is a non-inlined call boundary and packing (ns 2-4) made that a big fraction
of the block body. Pre-packing this was -1.2% (T-P5f); packing reopened it.

The portable source capture (target(avx2) scorer clone + cpu_has_avx2 dispatch) was
flat locally and reverted: cpu_has_avx2() is almost certainly false on this
MinGW/Windows box (__builtin_cpu_supports unreliable there), so the shipped Windows
binary likely runs SSE2 -- a separate portable win. On Linux/EPYC the runtime check
works, so the baseline already uses the AVX2 helpers and the clone (boundary kill)
is the right lever there. Resolution deferred to a Hamilton EPYC -msse2-vs-mavx2 A/B
(after the 5432 packbuild job frees the lib). See dev/profiling/kernel-avx2-reduce-gate.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0% end-to-end

EPYC A/B (job 17908514): the shipped -msse2 build pays a per-block AVX2-call
boundary (cpu_has_avx2 true on Linux, but the target(avx2) helper can't inline
into the -msse2 scorer). -mavx2 removes it: 1.67x/1.38x/1.26x per candidate on
project970/510/2668, scaling with block count. Windows masked this (cpu_has_avx2
false there -> inline SSE2 -> gap looked like 9%).

But end-to-end (job 17908602, fixed-seed maxReplicates=4, identical trajectory):
project970 1.01x, project510 1.10x. Amdahl: scoring is only ~3% (970) / ~34%
(510) of MaximizeParsimony wall; the rest is non-scoring machinery (ratchet/
sectorial/tree-ops). The per-candidate win does not propagate -- same dilution
that sank lever-6, and why packing (shrinks total_words pervasively) beat AVX2
(reduce compute only).

Banked as a -march=native Hamilton deployment build (dev/profiling/tsmarch_build.sh
-> /nobackup/pjjg18/tsmarch/lib): free ~1-10% for EPYC runs, machine-specific,
NOT for CRAN (breaks non-AVX2 CPUs; release keeps -msse2 + runtime dispatch). The
portable target(avx2) clone is not worth it (small, dataset-dependent end-to-end).
Confirms the mission: time-to-optimum is bounded by search machinery/stopping,
not the per-move kernel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t=off

Verified the -march=native deployment lib gives byte-identical EW+IW scores vs
-msse2 (project970, 8 trees, IW k=10, 12dp). Add -ffp-contract=off so byte-identity
is guaranteed (not just empirical): keeps the AVX2-inline win (boundary-kill, not
FMA) but forbids FMA fusion of the IW/profile float accumulators, which could
otherwise shift IW ties -> trajectory drift in a race. tsmarch/lib is thus a
transparent speedup, safe for IW-tie-sensitive race runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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