diff --git a/.AGENTS/memory/architecture.md b/.AGENTS/memory/architecture.md new file mode 100644 index 000000000..af5c05991 --- /dev/null +++ b/.AGENTS/memory/architecture.md @@ -0,0 +1,198 @@ +# Architecture Reference + +Load this when: editing `src/ts_*.cpp`/`.h`, adding Rcpp exports, reading +the R-level API, or reviewing design decisions. + +--- + +## R-level API + +| Function | Engine | Purpose | +|----------|--------|---------| +| `MaximizeParsimony()` | C++ driven search | Primary search (EW, IW, profile, constraints) | +| `Morphy()` | R-loop + MorphyLib | Legacy search (custom stopping, per-iteration callbacks) | +| `MaximizeParsimony2()` | — | Deprecated alias for `MaximizeParsimony()` | +| `Resample()` | C++ | Jackknife/bootstrap resampling | +| `SuccessiveApproximations()` | C++ | Successive approximations weighting | +| `TreeLength()` | C++ `ts_fitch_score` | Score one or more trees | +| `FastCharacterLength()` | C++ `ts_char_steps` | Per-character step counts | +| `AdditionTree()` | C++ `ts_wagner_tree` | Wagner tree construction | +| `RandomTreeScore()` | C++ (phyDat) or MorphyLib (morphyPtr) | Score a random tree | +| `TaxonInfluence()` | C++ via `MaximizeParsimony()` | Per-taxon search | +| `SearchControl()` | — | Expert parameter constructor for `MaximizeParsimony()` | +| `ParsSim()` | Pure R | Simulate datasets under parsimony (EW/IW/profile) | + +`MaximizeParsimony()` has a backward-compatibility shim: passing old +Morphy-style parameters (`ratchIter`, `tbrIter`, etc.) triggers a deprecation +warning and delegates to `Morphy()`. Scheduled for removal in 2028. + +--- + +## C++ module map + +| Module | Header/Source | Purpose | +|--------|--------------|---------| +| Fitch scoring | `ts_fitch.h/.cpp` | Downpass, uppass, incremental, indirect | +| NA scoring | `ts_fitch_na.h` | Three-pass inapplicable algorithm (Brazeau et al. 2019) | +| NA incremental | `ts_fitch_na_incr.h` | Incremental NA-aware scoring for TBR/drift | +| SIMD | `ts_simd.h` | SSE2/NEON portability layer for bit-parallel ops | +| Data | `ts_data.h/.cpp` | `DataSet`, `CharBlock`, `build_dataset`, simplification | +| Tree | `ts_tree.h/.cpp` | `TreeState`, topology manipulation, `PreallocUndo` | +| Constraint | `ts_constraint.h/.cpp` | Topological constraint enforcement | +| TBR | `ts_tbr.h/.cpp` | TBR search (with sector_mask for CSS) | +| SPR/NNI | `ts_search.h/.cpp` | SPR and NNI search (standalone, not in driven pipeline) | +| Ratchet | `ts_ratchet.h/.cpp` | Perturbation (zero/upweight/mixed, adaptive) | +| Drift | `ts_drift.h/.cpp` | Accept suboptimal moves within AFD/RFD limits | +| Wagner | `ts_wagner.h/.cpp` | Greedy addition tree (incremental scoring, NA-aware) | +| Sectorial | `ts_sector.h/.cpp` | RSS (conflict-guided), XSS, CSS; from-above HTU | +| Fuse | `ts_fuse.h/.cpp` | Tree fusing (in-place exchange) | +| Pool | `ts_pool.h/.cpp` | Dedup, eviction, consensus hash, split frequency table | +| Splits | `ts_splits.h/.cpp` | Bipartition computation, comparison, `hash_single_split()` | +| Driven | `ts_driven.h/.cpp` | Multi-replicate orchestrator | +| Resample | `ts_resample.h/.cpp` | Jackknife, bootstrap, successive approximations | +| Parallel | `ts_parallel.h/.cpp` | `std::thread` inter-replicate parallelism | +| RNG | `ts_rng.h/.cpp` | Thread-safe RNG (`thread_local` dispatch) | +| Simplify | `ts_simplify.h/.cpp` | Character compression and uninformativeness checks | +| Collapsed | `ts_collapsed.h/.cpp` | Zero-length edge detection for clip skipping | +| NNI perturb | `ts_nni_perturb.h/.cpp` | Stochastic NNI-perturbation (IQ-TREE-style topology escape) | +| HSJ scoring | `ts_hsj.h/.cpp` | Hopkins & St. John hierarchy scoring | +| Sankoff | `ts_sankoff.h/.cpp` | Sankoff step-matrix scoring (x-transform) | +| Rcpp bridge | `ts_rcpp.cpp` | All Rcpp-exported functions | + +--- + +## Scoring modes + +`ScoringMode` enum in `ts_data.h`: `EW`, `IW`, `PROFILE`, `XFORM`. +- **EW**: standard Fitch parsimony +- **IW**: implied weights via `e/(k+e)` where `e = steps - min_steps` +- **PROFILE**: lookup in `info_amounts` table (structurally identical to IW pipeline) +- **XFORM**: Fitch(non-hierarchy) + Sankoff(recoded composite characters) + +Profile mode sets `ds.concavity = 1.0` (finite sentinel) so existing +`isfinite()` checks activate the weighted pipeline without code duplication. + +--- + +## Parallelism design + +- `std::thread` (not OpenMP) to avoid R memory allocator conflicts +- Per-thread: `DataSet` copy, `ConstraintData` copy, `std::mt19937` RNG +- Shared: `ThreadSafePool` (mutex-guarded), atomic stop flag +- Main thread: pre-generates seeds from R's RNG, polls + `R_CheckUserInterrupt()` and timeout every 200ms +- Worker threads make no R API calls — `ts_rng.h` provides `thread_local` + dispatch (null → R API for serial; set → thread-local for parallel) + +--- + +## Scoring notes + +- `.h` file changes (`ts_fitch_na.h`, `ts_fitch_na_incr.h`) may require + `touch src/ts_fitch.cpp` before rebuild if the build system doesn't track + header dependencies. +- Incremental scoring is a **screening heuristic** for candidate selection; + `full_rescore()` / `score_tree()` is always authoritative. +- See `.positai/expertise/fitch-scoring.md` for detailed invariants: + uppass correctness proof, NA staleness analysis, `upweight_mask` audit. + +--- + +## Constraint enforcement + +- `build_constraint()` reads R split matrix with **column-major** indexing: + `split_matrix[s + n_splits * t]`. +- Wagner uses LCA-based constraint mapping (`wagner_map_constraint_nodes`) + since splits aren't fully present during incremental construction. +- Wagner has a posthoc retry loop (up to 100 random addition orders) as a + safety net for edge cases. + +--- + +## Exported Rcpp functions + +All registered in `ts_rcpp.cpp` and `TreeSearch-init.c`. Run +`Rscript check_init.R` to verify consistency. + +| Function | Module | Purpose | +|----------|--------|---------| +| `ts_fitch_score` | ts_fitch | Score a tree | +| `ts_char_steps` | ts_rcpp | Per-pattern step counts (with simplification offsets) | +| `ts_na_debug_char` | ts_fitch_na | Per-node debug for a single pattern | +| `ts_na_char_steps` | ts_fitch_na | Per-pattern step counts (raw, no offsets) | +| `ts_debug_clip` | ts_fitch | Debug SPR clip/regraft | +| `ts_test_indirect` | ts_fitch | Debug indirect length | +| `ts_nni_search` | ts_search | NNI hill-climbing | +| `ts_spr_search` | ts_search | SPR hill-climbing | +| `ts_tbr_search` | ts_tbr | TBR with plateau exploration | +| `ts_ratchet_search` | ts_ratchet | Ratchet perturbation | +| `ts_drift_search` | ts_drift | Drift search | +| `ts_wagner_tree` | ts_wagner | Wagner tree (specified addition order) | +| `ts_random_wagner_tree` | ts_wagner | Wagner tree (random order) | +| `ts_compute_splits` | ts_splits | Bipartition splits from edge matrix | +| `ts_trees_equal` | ts_splits | Compare two trees | +| `ts_pool_test` | ts_pool | Pool deduplication test | +| `ts_tree_fuse` | ts_fuse | Fuse two trees | +| `ts_sector_diag` | ts_sector | Sectorial search diagnostics | +| `ts_rss_search` | ts_sector | Random Sectorial Search | +| `ts_xss_search` | ts_sector | Exclusive Sectorial Search | +| `ts_driven_search` | ts_driven | Full driven search | +| `ts_resample_search` | ts_resample | One jackknife/bootstrap replicate | +| `ts_successive_approx` | ts_resample | Successive approximations | +| `ts_parallel_resample` | ts_parallel | Batch resample with parallelism | +| `ts_bench_tbr_phases` | ts_rcpp | TBR phase timing diagnostic | +| `ts_hsj_score` | ts_hsj | HSJ hierarchy scoring | + +--- + +## Key design decisions + +1. **PreallocUndo** (`ts_tree.h`): Pre-allocated flat buffers for TBR/drift + undo stack. Uses `grow()` to dynamically expand when capacity exceeded + (NA uppass saves both internal nodes and tips). Initial capacity `3 * n_node`. + +2. **TBR symmetry breaking** (`ts_tbr.cpp`): FNV-1a hash deduplication of + `virtual_prelim` vectors to skip redundant rerooting evaluations. + +3. **Bounded indirect scoring**: All search modules use `_bounded` variants + that bail out when accumulated score exceeds best candidate. + +4. **Profile parsimony**: Reuses IW indirect pipeline unchanged; only delta + precomputation differs. `ds.concavity = 1.0` sentinel activates weighted + path. Max 2 informative states per character; inapplicable → ambiguous. + +5. **MPT enumeration**: Post-search TBR plateau walk from all pool seeds. + `tbr_search()` accepts optional `TreePool* collect_pool` parameter. + +6. **All-ambiguous phyDat guard**: `TreeLength()` and `MaximizeParsimony()` + check for `levels = NULL` / 0-column contrast matrix before calling C++. + +7. **From-above HTU for sectorial search** (`ts_sector.cpp`): + `compute_from_above_for_sector()` computes `from_above[sector_root]` — + the Fitch state-set the rest of the tree sends *down* to the sector + boundary, excluding the sector's own contribution. Used instead of + `final_[parent]` in `build_reduced_dataset()`. O(depth × total_words). + +8. **Split frequency table** (`ts_pool.h/.cpp`): `SplitFrequencyTable` maps + per-split FNV-1a hash → occurrence count across best-score pool trees. + Used by conflict-guided RSS to weight sector selection. The same FNV-1a + hash (`hash_single_split()` in `ts_splits.h`) is used by consensus + hashing and split frequency counting — must stay consistent. + +9. **Consensus-stability hash** (`ts_pool.cpp`): XOR of FNV-1a hashes of + splits present in ALL best-score trees. Updated after each replicate. + Hash collision false-matches are conservative (over-count stability). + +10. **Diversity-aware pool eviction** (`ts_pool.cpp`): When the pool is full + and a new tree ties the worst score, the entry most similar to the new + tree (most shared splits, counted via per-split FNV-1a hash set + membership) is evicted. Falls back to arbitrary worst entry when the + new tree is strictly better. + +11. **Cross-replicate consensus constraint tightening** (`ts_driven.cpp`): + When `consensus_constrain = true` and no user constraint is supplied, + after ≥5 replicates, unanimous pool splits are extracted and enforced + as topological constraints via `build_constraint_from_bitsets()`. The + TBR/SPR search then avoids breaking established consensus clades. + Constraints are cleared and rebuilt whenever the best score changes. + Sector/fuse operations do not enforce auto-constraints. diff --git a/.AGENTS/memory/benchmarking.md b/.AGENTS/memory/benchmarking.md new file mode 100644 index 000000000..78453c010 --- /dev/null +++ b/.AGENTS/memory/benchmarking.md @@ -0,0 +1,174 @@ +# Benchmarks and Profiling + +Load this when: running benchmarks, interpreting benchmark results, +doing VTune profiling, or selecting datasets for strategy validation. + +See also: `search-algorithms.md` (NNI, biased Wagner, outer cycles results), +`search_strategy.md` (presets, ratchet tuning). + +--- + +## VTune driver scripts — dry-run first + +**Always test a VTune driver script with plain `Rscript` before launching +VTune.** Software-sampling overhead can be 5–20×; if the bare script takes +30s, VTune may need 10 min. Target < 5s bare run for a lite driver. + +MaddisonSlatkin is exponential in tip count — even n=20 with k=3 can take +seconds per call. Use small n (≤15 for k=3, ≤12 for k=4, ≤9 for k=5) +and few iterations for VTune drivers. + +--- + +## MorphoBank external benchmark corpus + +The neotrans repo (`../neotrans/inst/matrices/`) contains ~800 MorphoBank +NEXUS matrices. Complement to the 14 bundled datasets and 1 large-tree dataset. + +**Catalogue:** `dev/benchmarks/mbank_catalogue.csv` (659 usable matrices +after ntax≥20 filter and dedup). Regenerate with +`Rscript dev/benchmarks/build_mbank_catalogue.R`. + +**Train/validation split:** Matrices whose MorphoBank project number is +divisible by 5 are **validation** (124 matrices, ~19%). All others are +**training** (535 matrices). The 7 `syab*` files are always training. + +**Dedup:** Multi-file projects with ≥95% character identity on shared taxa +(≥80% taxon overlap) are flagged `dedup_drop = TRUE`. 24 near-duplicates excluded. + +**IMPORTANT:** Validation results must **never** be used to guide strategy +tuning. They confirm generalization only. This is a one-way door. + +**Fixed 25-matrix training sample:** `MBANK_FIXED_SAMPLE` in +`bench_datasets.R` — 7 small, 7 medium, 7 large, 4 xlarge. Selected via +max-min distance on standardized features. **Do not modify.** Used by +`benchmark_mbank_sample()`. Fitch track only. + +**Fixed 20-matrix Brazeau-track sample:** `MBANK_BRAZEAU_SAMPLE` in +`bench_datasets.R` — 5 small, 6 medium, 6 large, 3 xlarge. Restricted to +training matrices with **pct_inapp ≥ 4%**. **Do not modify.** + +**Key functions** (in `dev/benchmarks/bench_datasets.R`): +- `load_mbank_catalogue()` — loads metadata CSV (excludes dedup by default) +- `load_mbank_sample(cat, n, seed, split)` — stratified random sample +- `load_mbank_datasets(cat, keys)` — load specific matrices by key +- `load_mbank_brazeau_sample(cat)` — fixed 20-matrix Brazeau sample +- `has_meaningful_inapp(cat, threshold)` — filter to pct_inapp ≥ threshold + +**Benchmark runners** (in `dev/benchmarks/bench_framework.R`): +- `benchmark_mbank_sample()` — fixed 25-matrix training sample (routine) +- `benchmark_mbank_sweep(split)` — full training or validation sweep +- `benchmark_mbank_validation()` — validation sweep with prominent warning + +**Benchmark tracks:** + +| Track | Scoring | Datasets | Purpose | +|-------|---------|----------|---------| +| **Fitch** | `fitch_mode()` | 14 bundled + `MBANK_FIXED_SAMPLE` | TNT comparison, core search quality | +| **Brazeau** | Default (Brazeau 2019) | `MBANK_BRAZEAU_SAMPLE` + bundled | NA-algorithm-specific strategy tuning | + +TNT comparisons are Fitch track only. + +**TNT comparison suite** lives in `../TS-TNT-bench/`. Key files: +- `dev/benchmarks/bench_tnt_compare.R` — runner (smoke/medium/full) +- `dev/benchmarks/tnt_comparison.qmd` — Quarto report +- Requires TNT 1.6 at `C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe` + +Benchmark scripts in `dev/benchmarks/`. Key files: +- `bench_regression.R` — CI regression test (score quality + timing bounds) +- `bench_framework.R` — Dataset × strategy × replicate grid +- `strategies.md` — Strategy space documentation + +--- + +## Benchmarking methodology notes + +**Metric:** When comparing strategies with different time costs (e.g. +NNI→TBR vs TBR), use **time-adjusted expected best** (TAEB) — the expected +minimum score from k = budget / time_per_rep independent replicates. Median +per-replicate score is adequate only when comparing parameter changes on a +fixed pipeline (same time-per-rep). Bootstrap estimation: sample k scores +with replacement, take the min, repeat 5000×, take the mean. + +**Brazeau vs EW scoring confound (T-265, 2026-03-26):** TreeSearch uses the +Brazeau et al. (2019) inapplicable algorithm by default, which penalizes +inapplicable-to-applicable transitions. TNT treats `-` as `?` (standard EW +Fitch). On 11 gap datasets, the apparent mean gap was +17.8 steps; the +actual EW-vs-EW gap is only +2.2 steps (5 datasets at 0 gap). **All TNT +comparisons MUST use `fitch_mode()` to convert inapplicable to missing** +for apples-to-apples scoring. `fitch_mode()` is defined in +`bench_intra_fuse.R` and `bench_t265_regression.R`. + +**`maxTime` confound (2026-03-23):** `maxTime` (legacy Morphy parameter) +silently delegates to the R-loop `Morphy()` engine. Use `maxSeconds` for +the C++ driven search, which is ~10× faster at 180 tips. + +**Early vs late search:** Early replicates are dominated by initial descent +quality (Wagner → local optimum); late replicates test ratchet/drift escape. +At ≤88 tips, 20s gives 10–40 replicates spanning both regimes. At 180 tips, +20s doesn't complete one replicate. + +--- + +## Phase distribution baselines + +**T-290b (2026-03-28, Brazeau-sample datasets, 30s, post-T-255 no-drift presets):** + +| Phase | Fitch/EW/default | Fitch/EW/thorough | Brazeau/EW/default | Brazeau/EW/thorough | +|-------|:---:|:---:|:---:|:---:| +| Ratchet | 76% | 65% | 74% | 63% | +| TBR | 8% | 5% | 7% | 4% | +| XSS | 6% | 7% | 5% | 6% | +| RSS | 3% | 10% | 3% | 10% | +| CSS | — | 7% | — | 7% | +| Wagner | 4% | 3% | 9% | 7% | +| Final TBR | 2% | 2% | 2% | 2% | + +*(Drift has been 0% in all presets since T-255.)* + +**Brazeau / Fitch per-phase cost ratios (T-290b, EW):** + +| Phase | default | thorough | +|-------|:-------:|:--------:| +| Wagner | **3.6×** | **3.9×** | +| Ratchet | 1.3× | 1.3× | +| RSS/CSS | 1.3× | 1.3× | +| TBR | 0.9× | 0.9× | + +Wagner is the outlier. All other phases are within 0.9–1.4× of Fitch cost. + +**wagnerStarts under Brazeau (T-290b/c, 2026-03-28):** +- *Multiple reps/budget*: wagnerStarts=1 and 3 equivalent; w3 marginally better. +- *~1 rep/budget* (60s at 86t/3660c): wagnerStarts=3 better by +564 steps. +- *0 reps/budget* (30s at 86t/3660c): wagnerStarts=1 **better** — Brazeau + Wagner is expensive (~4×), 3 starts consume budget. +Current presets correct: thorough (w3, gets ≥1 rep at 65–119t) ✓; large (w1) ✓. + +Per-candidate indirect scoring is at memory-throughput limit (~23 ns at 75 tips). + +--- + +## Ratchet tuning validation (2026-03-22) + +Full 14-dataset comparison, optimized vs original defaults (10s budget, 3 seeds). + +| Dataset | Tips | Original | Optimized | Delta | +|---------|:---:|:---:|:---:|:---:| +| Longrich2010 | 20 | 131 | 131 | 0 | +| Vinther2008 | 23 | 79 | 79 | 0 | +| Sansom2010 | 23 | 189 | 189 | 0 | +| DeAssis2011 | 33 | 64 | 64 | 0 | +| Aria2015 | 35 | 143 | 143 | 0 | +| Wortley2006 | 37 | 494 | 491 | +3 | +| Griswold1999 | 43 | 408 | 407 | +1 | +| Schulze2007 | 52 | 165 | 164 | +1 | +| Eklund2004 | 54 | 442 | 441 | +1 | +| Agnarsson2004 | 62 | 778 | 778 | 0 | +| Zanol2014 | 74 | 1338 | 1331 | +7 | +| Zhu2013 | 75 | 649 | 650 | −1 | +| Giles2015 | 78 | 720 | 716 | +4 | +| Dikow2009 | 88 | 1614 | 1614 | 0 | + +Zhu2013 marginal regression at 10s resolves at 20s (median 649→644). +At 20s with 5 seeds: Zhu2013 645/643, Giles2015 712/710, Dikow2009 +1611/1611 (all improvements). diff --git a/.AGENTS/memory/feature-inapplicable.md b/.AGENTS/memory/feature-inapplicable.md new file mode 100644 index 000000000..7c776b65c --- /dev/null +++ b/.AGENTS/memory/feature-inapplicable.md @@ -0,0 +1,96 @@ +# Alternative Inapplicable-Handling Algorithms + +Load this when: working on HSJ scoring, x-transform recoding, Sankoff engine, +`inapplicable=` parameter, or character hierarchy specification. + +Plan: `.positai/plans/2026-03-19-0643-alternative-inapplicable-handling-algorithms.md` + +Adding HSJ (Hopkins & St. John 2021) and step-matrix/x-transformation +(Goloboff et al. 2021) scoring as alternatives to the existing Brazeau +et al. (2019) three-pass algorithm. Both require an explicit character +hierarchy specification. + +--- + +## New files + +| File | Purpose | Status | +|------|---------|--------| +| `R/CharacterHierarchy.R` | `CharacterHierarchy` S3 class, `validate_hierarchy()`, `hierarchy_from_names()`, `hierarchy_chars()`, `hierarchy_controlling()`, `non_hierarchy_weights()` | Complete, 34 tests passing | +| `tests/testthat/test-CharacterHierarchy.R` | Unit tests for hierarchy specification + weight partitioning | Complete | +| `src/ts_hsj.h` | `HierarchyBlock` struct (with `absent_state`), `hsj_score()` declaration, `partition_weights()` | Complete | +| `src/ts_hsj.cpp` | `partition_weights()`, `fitch_label_char()` (with uppass), `score_hierarchy_block()`, `hsj_score()` | Complete (full-rescore only; not wired to search pipeline) | +| `src/ts_sankoff.h` | `SankoffChar`, `SankoffData` structs, `sankoff_score()`, `sankoff_score_char()`, `sankoff_uppass()` | Complete | +| `src/ts_sankoff.cpp` | Sankoff downpass, uppass, root forcing | Complete | +| `R/recode_hierarchy.R` | `recode_hierarchy()`: x-transformation recoding (Goloboff et al. 2021) | Complete, 49 tests | +| `tests/testthat/test-recode-hierarchy.R` | Unit tests for recode_hierarchy() | Complete | +| `inst/REFERENCES.bib` | Added `Goloboff2021` entry | Complete | + +--- + +## Modified files + +| File | Change | +|------|--------| +| `DESCRIPTION` | Added `CharacterHierarchy.R` to Collate field | +| `R/MaximizeParsimony.R` | Added `hierarchy`, `inapplicable`, `hsj_alpha` params with validation | +| `src/ts_data.h` | Added `inapp_state` field to `DataSet` (for HSJ) | +| `src/ts_data.cpp` | Populate `inapp_state` in `build_dataset()` | + +--- + +## Design decisions + +- `hierarchy` is a **separate argument** to `MaximizeParsimony()` (not a phyDat attribute) +- `inapplicable` and `hsj_alpha` are **top-level args** alongside `concavity` +- Default `hsj_alpha = 1.0` +- IW + hierarchy and Profile + hierarchy: **deferred** +- Constraint interaction: **ignored** for now +- Resampling: **hierarchical** — resample top-level chars; when a controlling primary is sampled, also resample within its block; recurse for nested hierarchies + +--- + +## Resampling with hierarchy (T-124) + +`Resample()` now accepts `hierarchy`, `inapplicable`, and `hsj_alpha` +parameters. When `inapplicable != "brazeau"`, resampling is hierarchy-aware: + +- **Resampling units**: each non-hierarchy character = 1 unit; each + top-level hierarchy block (primary + all dependents) = 1 atomic unit. +- **Jackknife**: retain `proportion` of units without replacement. +- **Bootstrap**: sample `n_units` units with replacement (blocks can be + duplicated). +- Per replicate: `.HierarchicalResampleWeights()` computes pattern weights + for non-hierarchy chars and per-block sample counts. `.ResampleHierarchy()` + calls `ts_driven_search` per replicate with filtered HSJ blocks or xform + chars. +- **No C++ changes**: reuses existing `ts_driven_search` HSJ/xform infrastructure. +- **Parallelism**: serial R loop over replicates (C++ inter-search parallelism + via `nThreads` still available within each replicate). + +--- + +## Key algorithm notes (HSJ) + +- Paper's Algorithm 1 initializes `a(l) = p(l) = 0` for all leaves. This is + incorrect for enforcing observed leaf states. Correct initialization: + leaf with primary absent → `a(l) = 0, p(l) = INF`; primary present → + `a(l) = INF, p(l) = 0`. Verified against hand-computed example. +- `score_hierarchy_block()` operates per hierarchy block. Non-hierarchy + characters use standard Fitch. Total = Fitch(non-hierarchy) + Σ HSJ(blocks). +- Secondary character labels at internal nodes from Fitch first-pass + (inapplicable treated as a separate state). +- HSJ is full-rescore only (no incremental variant). Performance mitigation: + candidate screening via Fitch, full HSJ only for promising candidates. + +--- + +## Phase 2 (step-matrix/x-transform) — Complete + +Sankoff engine (`ts_sankoff.h/.cpp`) implements downpass, uppass, root forcing. +R-level `recode_hierarchy()` combines primary + secondaries into composite +step-matrix character with asymmetric costs (gain:loss = n+1:1). Multistate +secondaries supported (state count = ∏k_i + 1). Nested hierarchies deferred. +Integration complete: `ScoringMode::XFORM` in `score_tree()` dispatches +Fitch(non-hierarchy) + Sankoff(recoded). `MaximizeParsimony()` accepts +`inapplicable = "xform"`. End-to-end search verified. diff --git a/.AGENTS/memory/r-package-conventions.md b/.AGENTS/memory/r-package-conventions.md new file mode 100644 index 000000000..cb6b272a1 --- /dev/null +++ b/.AGENTS/memory/r-package-conventions.md @@ -0,0 +1,53 @@ +# R Package Conventions + +Load this when: adding `.R` files, writing roxygen docs, updating vignettes, +or running pre-commit documentation checks. + +--- + +## R source file ordering + +`DESCRIPTION` has an explicit `Collate:` field. When adding a new `.R` file, +**update the Collate field** — otherwise R sources alphabetically, which can +break if one file's top-level code depends on a later file. + +--- + +## Documentation checks (mandatory) + +After any change to a function signature or roxygen block, run: + +```r +devtools::check_man() +``` + +After writing or updating documentation prose, also run: + +```r +spelling::spell_check_package() +``` + +Both should be clean before committing. `check_man` catches Rd parse errors, +cross-ref failures, `\usage` mismatches; `spell_check_package` catches typos +in `@description`/`@details`/`@param` text. + +References are added using Rdpack's `\insertCite{}`, with +`\insertAllCited{}` in the references section. + +--- + +## Algorithm vignette (mandatory updates) + +`vignettes/search-algorithm.Rmd` documents the search algorithm for +publication. **Any change that modifies search behaviour** — new heuristics, +parameter tuning, scoring methods, stopping criteria, pool management, or +rearrangement operators — **must be accompanied by an update to this vignette.** + +- Published techniques: add a short summary and `@Key` citation. +- Novel contributions: describe the algorithm in enough detail for a reader + to understand the design and rationale. Include empirical results where + available (e.g. benchmark deltas). +- New references: add `@article{Key, ...}` to `inst/REFERENCES.bib`. + +The vignette uses pandoc-style `@Key` citations (same as the other +vignettes), not Rdpack `\insertCite{}`. diff --git a/.AGENTS/memory/search-algorithms.md b/.AGENTS/memory/search-algorithms.md new file mode 100644 index 000000000..8aef2d4fe --- /dev/null +++ b/.AGENTS/memory/search-algorithms.md @@ -0,0 +1,149 @@ +# Search Algorithm Design Notes + +Load this when: researching NNI warmup, biased Wagner, outer cycles, +large-tree behaviour, or reviewing the search optimization history. + +See also: `search_strategy.md` (pipeline structure, strategy presets), +`benchmarking.md` (corpus, methodology, benchmark tables). + +--- + +## NNI in the driven pipeline + +`nni_search()` in `ts_search.cpp` is implemented. At ≤88 tips, NNI is +redundant — TBR subsumes it. At 180 tips, NNI becomes essential: TBR +evaluates O(n²) candidates per pass (millions of evaluations, many minutes +to converge from Wagner); NNI evaluates O(n) candidates (~1000× cheaper). + +**All presets set `nniFirst = TRUE`** (NNI warmup before TBR). Each Wagner +start is NNI-optimized before selection. SPR is counterproductive when NNI +is active — NNI→TBR outperforms NNI→SPR→TBR empirically. + +**Empirical comparison at 180 tips** (mbank_X30754, 3 seeds, EW): + +| Strategy | Median score | Median time | +|----------|:-----------:|:-----------:| +| TBR alone | 1427 | 13.6s | +| SPR→TBR | 1360 | 13.1s | +| **NNI→TBR** | **1326** | **6.8s** | +| NNI→SPR→TBR | 1369 | 8.8s | + +NNI→TBR wins on both score AND time (~2× faster, ~100 steps better). + +**Time-adjusted expected best (5 seeds, EW):** + +| Budget | 88t: TBR | 88t: NNI→SPR→TBR | 180t: TBR | 180t: NNI→SPR→TBR | +|--------|:--------:|:-----------------:|:---------:|:-----------------:| +| 20s | 1617 | 1619 (+2) | 1388 | 1278 (−110) | +| 60s | 1617 | 1619 (+2) | 1348 | 1253 (−95) | +| 120s | 1617 | 1619 (+2) | 1337 | 1247 (−90) | + +At ≤88 tips: NNI has a consistent but negligible 2-step penalty. At 180 +tips: NNI saves 90–110 steps. No reactive per-run switching needed — always-on +NNI warmup is optimal. + +--- + +## Stochastic NNI-perturbation (T-186) + +`ts_nni_perturb.h/.cpp` implements topology-space escape inspired by +IQ-TREE's `doRandomNNIs()`. Complementary to the weight-perturbation +ratchet: ratchet reshapes the objective function; NNI-perturbation directly +displaces the tree topology. + +**Algorithm:** Collect all internal NNI edges. For each edge (with probability +`perturb_fraction`, default 0.5), apply a random NNI swap — skip edges +adjacent to already-swapped edges. Track touched nodes in a hash set. +After all compatible swaps, rebuild postorder and full rescore, then TBR +to a new local optimum. Repeat for `n_cycles`. + +**Pipeline placement:** Between ratchet and drift. **Disabled by default +(`nniPerturbCycles = 0`)** and in all presets since T-274 (2026-03-27). + +**R API:** `SearchControl(nniPerturbCycles, nniPerturbFraction)`. + +**T-274 benchmark (2026-03-27):** 20 seeds, Zhu2013/Giles2015/Dikow2009 +(75–88t). NNI-perturb adds 59–69% per-replicate overhead with ≤0.1-step +expected-best benefit at all budgets — within bootstrap noise. Set +`nniPerturbCycles = 0` in thorough preset. Available via `SearchControl()` +for manual use. + +--- + +## Biased Wagner addition (T-188, 2026-03-23) + +`biased_wagner_tree()` (`ts_wagner.h/.cpp`) samples the taxon-addition order +from a softmax distribution weighted by informativeness score. + +Two criteria: +- **GOLOBOFF** (bias=1): `score[t]` = number of non-ambiguous characters for + taxon t. Ref: Goloboff 2014 (*Extended implied weighting*) §3.3. +- **ENTROPY** (bias=2): `score[t]` = Σ_c (n_states_c − |state set for t|). + +**R API:** `SearchControl(wagnerBias = 0L, wagnerBiasTemp = 0.3)`. +Applied only to the first of `wagnerStarts` starts; remaining starts use +random order for basin diversity. + +**Benchmark results** (2026-03-23, 14 standard + crico-174): +- Wagner→TBR gap reduction: ~80% at 174t (random: 1356 steps, Goloboff: 244) +- Score improvement after TBR convergence: ~22 steps at 174t; 1–2 steps at ≤88t +- Anomalous slight regression at 75–100t; T=0.3 stochastic is safer than T=0 + +--- + +## Outer search cycle loop (T-189, 2026-03-23) + +`outer_cycles` in `SearchParams` / `outerCycles` in `SearchControl()`. +Wraps steps 3–6 of `run_single_replicate()` in a configurable outer loop: +`[XSS+RSS+CSS → Ratchet → NNI-perturb → Drift → TBR] × N`. +Ratchet/NNI-perturb/drift cycles are divided evenly among N outer cycles. + +`outerCycles = 1` (default) is bit-for-bit identical to the previous +linear pipeline. `thorough` preset defaults to `outerCycles = 2`. + +Matches TNT's `xmult` interleaving (Goloboff 1999 §2.3): after each +ratchet/drift escape, a fresh XSS pass exploits the new topology. + +--- + +## Large-tree scaling issues (discovered 2026-03-23) + +The 180-taxon `mbank_X30754` dataset (425 chars, 374 informative patterns, +40% missing, 20% inapplicable) exposed: + +1. ~~**`maxTime` triggers Morphy delegation.**~~ **Fixed (T-184)**: + `maxTime` is now intercepted before the Morphy shim check and mapped + to `maxSeconds` with a deprecation warning. +2. **C++ TBR convergence at 180 tips takes ~13s** (Wagner ~2560 → local + optimum ~1420). NNI warmup (~1.5s) followed by TBR reduces this to + ~7s while finding better scores. T-178 filed. +3. **Strategy presets assume replicate time O(seconds).** At 180 tips, + a single replicate takes ~60-100s. Cycle counts need recalibration. + +**180-taxon baseline (C++ driven search, EW, single replicate):** +- Wagner (best of 3): ~2560 steps, 16ms +- NNI convergence: ~1600 steps, 1.5s +- TBR convergence: ~1330 steps, 7s (from NNI-optimal start) +- XSS: additional ~60 steps improvement, 5s +- Total single replicate: ~25s (before ratchet/drift) + +--- + +## Search optimization roadmap + +Items completed as of 2026-03-29. Numbered by original priority. + +1. ~~Consensus-guided sector targeting~~ — **Done**: RSS weighted by pool split conflict scores +2. ~~Diverse pool maintenance~~ — **Done**: evict most-similar entry on ties +3. ~~Cross-replicate constraint tightening~~ — **Done**: opt-in via `consensusConstrain = TRUE` +4. ~~Collapsed-tree clip skipping~~ — **Done**: zero-length edges skipped in TBR, SPR, drift. Skip rate 0% on standard morphological datasets (benefit expected on sparse/synthetic data). +5. ~~Collapsed-region regraft merging + pool dedup~~ — **Done**: boundary-only regraft evaluation; collapsed-topology pool dedup. +6. ~~Strategy preset tuning~~ — **Done**: `default` uses `wagnerStarts=3`, `sprFirst=TRUE`, `adaptiveLevel=TRUE`; `thorough` uses `sprFirst=TRUE`. +7. ~~Ratchet perturbation tuning~~ — **Done**: perturbation probability 4%→25%, perturbed TBR moves 20→5, ratchet cycles 5→10 (default), 10→20 (thorough). Drift cycles 2→4, AFD 5, RFD 0.15. Validated on 14 datasets. +8. ~~Biased Wagner addition~~ — **Done** (T-188): see above. +9. ~~Outer search cycle loop~~ — **Done** (T-189): see above. +10. ~~Drift MPT diversity experiment~~ — **Done** (T-254): drift provides zero score benefit, zero MPT enumeration benefit. Delays consensus stability. `driftCycles=0` in all presets (T-255). +11. ~~NNI-perturb cycle count at thorough-preset scale~~ — **Done** (T-274): see above. +12. ~~Size-weighted TBR clip ordering~~ — **Closed** (2026-03-29): Hypothesis FALSIFIED. Tip clips (~51% of all clips) account for only 22–38% of accepted moves (enrichment 0.43–0.76×). Medium-small clips (size 2..√n) are most productive. All three variants (INV_WEIGHT, TIPS_FIRST, BUCKET) favour tips — wrong direction. Diagnostic code preserved in `feature/weighted-clip-order` branch. +13. ~~XSS↔TBR cycling under IW~~ — **Closed** (2026-03-29): IW3 XSS improvement rate ~30% vs EW ~25%; below 2× threshold. Key finding: XSS cycling benefit scales with tree size, not scoring mode. At 180t: XSS adds 12–19% overhead, TAEB Δ = −6.8 to −9.8 EW steps at 30–120s. +14. ~~Targeted post-clip sector search~~ — **Closed** (2026-03-29): Hit rate ~35% but net HARMFUL — local sector refinement after each TBR move changes global trajectory, steering into worse basins. Validates existing design: XSS should run as a separate phase AFTER TBR convergence. diff --git a/.AGENTS/memory/search_strategy.md b/.AGENTS/memory/search_strategy.md new file mode 100644 index 000000000..6a8438cdb --- /dev/null +++ b/.AGENTS/memory/search_strategy.md @@ -0,0 +1,162 @@ +### Driven search pipeline per replicate + +1. Random Wagner tree → NNI warmup → TBR to local optimum +2. XSS sectorial search (if tree large enough) +3. RSS random sectorial search +4. CSS constrained sectorial search +5. Ratchet perturbation to escape local optima +5a. Post-ratchet XSS+RSS+CSS (if `postRatchetSectorial = TRUE`) +6. NNI-perturbation (topology-space escape, if `nniPerturbCycles > 0`) +7. Drift search (accept suboptimal moves) +8. PCSA perturbation (if `annealCycles > 0`) +9. Final TBR polish +10. Add to pool +11. Fuse against pool (every `fuse_interval` replicates) + +Steps 2–9 are wrapped in the `outerCycles` loop (default 1). + +Post-search: TBR plateau enumeration from all pool seeds to find MPTs. + +### Strategy presets (auto-selected by `NTip` and signal density) + +| Preset | Condition | Key settings | +|--------|-----------|-------------| +| sprint | ≤30 tips | 3 ratchet (4%), 0 drift, XSS only, NNI-first | +| default | 31–64 tips; or ≥65 tips with <100 char patterns | 12 ratchet (25%, 5 moves), 0 drift, XSS+RSS, Wagner×3, NNI-first, adaptive level | +| thorough | 65–119 tips with ≥100 char patterns | 20 ratchet (25%, 5 moves, adaptive), 0 NNI-perturb (T-274), 0 drift, XSS+RSS+CSS, Wagner×3, NNI-first, outerCycles=2 | +| large | ≥120 tips with ≥100 char patterns | 12 ratchet (25%, 5 moves, adaptive), 0 NNI-perturb, 0 drift, 1 SA cycle (T=20→0, 5 phases), XSS(3)+RSS(2)+CSS(1), Wagner×1 biased (Goloboff 2014), NNI-first, outerCycles=1, tbrMaxHits=1, sectorMaxSize=100, pruneReinsert=5 cycles NNI-polish (T-289f Stage 5: NNI polish fixes 0-rep failure at 206t; improves 131–180t) | + +**T-264 (2026-03-26):** `consensusStableReps` removed from all presets +(disabled, 0). The previous setting of 3 caused catastrophic early +termination — the search stopped after 3 replicates with unchanged +consensus, using only 7–20% of the time budget on most datasets. + +**Large preset design rationale (T-179, 2026-03-24):** At 180 tips, each TBR +convergence takes ~5–7s, making phases like NNI-perturbation (~5.5s/cycle) and +drift (~4s/cycle) extremely expensive. Systematic benchmarking on mbank_X30754 +(180t, 418p) showed that reducing cycle counts (12 ratchet, 4 drift, no NNI-perturb) +with outerCycles=1 and a single biased Wagner start outperforms the thorough +preset by 4–7 steps (median) at 30–60s budgets and ties at 120s, while +consistently completing more replicates. + +**T-289 Stage 4 (2026-03-28, EPYC 7702, 10 seeds, 5 datasets 131–206t):** +PR (c=5, d=5%, MISSING) vs baseline. 60s: mean Δ=+0.5 steps (neutral); +project3701 146t regresses −12 steps; syab07205 206t: 0 replicates complete +(per-rep cost ~60s, budget exceeded). 120s: mean Δ=−9.1 steps but driven +by project3701 (−37 steps); others ≤6 steps. Replicate ratio 0.82 at 60s, +0.68 at 120s. Decision: disable PR (TBR polish) — 0-rep failure at 206t/60s +is a showstopper. + +**T-289f Stage 5 (2026-03-29, EPYC 7702, 10 seeds, 5 datasets 131–206t):** +PR (c=5, NNI full-tree polish) vs pr_tbr (TBR polish, Stage 4 reference) vs +baseline. pr_tbr at 206t/60s: still 0 reps (confirmed). pr_nni fixes the +0-rep failure (2 reps at 206t/60s). Score deltas vs baseline: project4133 +(131t) ≈0; project3701 (146t) **−178 steps** at 60s, −128 at 120s; project804 +(173t) −9/−2; mbank_X30754 (180t) −4/−7; syab07205 (206t) +17.5 at 60s +(neutral at 120s). **Decision: enable pruneReinsertCycles=5, pruneReinsertNni=TRUE +in large preset.** Note G-006: NNI polish ignores ConstraintData — irrelevant +since large preset does not use topological constraints. + +**Post-T-206 Hamilton HPC baselines (2026-03-26, EPYC 7702, 5 seeds):** +30s median=1202 (range 1189–1214), 60s median=1190 (1190–1202), 120s +median=1185 (1171–1189). Per-replicate median 17.3s (cf. ~60s pre-T-206). +The 65–74 step improvement over pre-T-206 Intel baselines is primarily +from the outer cycle reset cap (maxOuterResets=0), not hardware. +Phase distribution: TBR 43.6%, Ratchet 32.2%, SA 7.4% (14% hit rate, +0.8 steps/s — least productive phase). T-248 benchmarked annealCycles +0/1/3: AC=1 (400ms/rep, 40% hit rate) is most cost-effective; AC=3 +(1370ms/rep, 21% hit rate) showed no significant score gain (p>0.5, +n=5 seeds). Large preset reduced to annealCycles=1. + +All presets set `nniFirst = TRUE` (NNI warmup before TBR) and +`sprFirst = FALSE` (SPR is counterproductive when NNI is active — +empirically NNI→TBR outperforms NNI→SPR→TBR). With `nniFirst`, each +Wagner start is NNI-optimized before selection (best of 3 NNI-local optima +rather than 3 raw Wagner scores). `default` also enables `adaptiveLevel = +TRUE` (scale ratchet/drift by hit rate); `thorough` omits it because high +base cycle counts already cover hard landscapes. + +**Ratchet perturbation tuning (2026-03-22)**: Systematic profiling across +all 14 benchmark datasets showed the previous 4% perturbation probability +was far too gentle. With 253 characters (Zhu2013), 4% zeroes only ~10 +characters — insufficient to reshape the landscape. Increasing to 25% +with fewer perturbed TBR moves (5 instead of auto=20) improves median +scores by 3–7 steps on hard datasets while completing fewer but more +productive replicates. 9/14 datasets improved, 4 unchanged, 1 marginal at +10s budget (resolves at 20s). The key insight: the perturbed-phase TBR +should be short (the landscape is warped, so extensive search on it is +wasteful), but the perturbation itself should be aggressive enough to +meaningfully displace the tree from its current basin of attraction. + +Signal-density gate: datasets with few character patterns (<100) have flat +parsimony landscapes where intensive search adds no benefit. + +### Adaptive sectorial search + +XSS and CSS use **adaptive early-exit**: after each round of sector searches ++ global TBR polish, if the overall best score did not improve, remaining +rounds are skipped. This avoids wasting ~7% of replicate time on datasets +where sectorial search is unproductive (e.g. Dikow2009). On productive +datasets (e.g. Zhu2013), the early exit never fires. + +### Conflict-guided RSS + +RSS uses **conflict-guided sector selection**: before each replicate's RSS +phase, `driven_search()` computes a `SplitFrequencyTable` from the pool's +best-score trees. Within `rss_search()`, each internal node's "conflict +score" is `1 − (fraction of pool trees containing that split)`. +Max-descendant conflict is propagated upward, and eligible sector roots +are sampled via `std::discrete_distribution` with weight `1 + 3 × conflict`. +Falls back to uniform selection when the pool has <2 best-score trees or +when conflict variation is negligible. + +### Consensus-stability stopping + +After each replicate, if `consensus_stable_reps > 0` (disabled in all +presets since T-264; available via `SearchControl(consensusStableReps=N)`), +the pool's strict consensus hash is compared to the previous replicate's. +If unchanged for `consensus_stable_reps` consecutive replicates, the +search terminates early. `compute_consensus_hash()` uses +XOR of per-split FNV-1a hashes for O(pool × splits) cost. + +### Adaptive search level + +When `adaptive_level = true`, ratchet and drift cycle counts are scaled +each replicate based on the cumulative hit rate: +- hit_rate > 0.7 → 0.5× (easy landscape) +- hit_rate > 0.4 → 0.75× +- hit_rate < 0.15 → 1.5× (hard landscape) +- else → 1.0× + +### TBR zero-length clip skipping + regraft merging (collapsed flags) + +`compute_collapsed_flags()` (`ts_collapsed.h/.cpp`) identifies edges where +clipping provably cannot improve score. Checks 5 conditions: (1) zero +standard-block cost at parent, (2) zero NA-block cost at parent, (3) prelim +preservation (`prelim[sibling] == prelim[parent]`), (4) down2 preservation +(NA), (5) subtree_actives preservation (NA). Works for EW, IW, Profile, +and NA-aware scoring. Integrated into TBR, SPR, and drift search. +Disabled during MPT enumeration (equal-score topologies may exist). +Recomputed after every accepted move. + +**Regraft merging** (Goloboff 1996): within a collapsed region (connected +set of nodes linked by zero-length edges), all regraft positions yield the +same full score. Only boundary edges (entering the region) are evaluated; +interior collapsed edges are skipped via `if (collapsed[below]) continue`. +TBR, SPR, and drift all use this. The `CollapsedRegions` struct exists in +the header but callers use `compute_collapsed_flags()` directly (the +`region_id` field is unused — only the boolean flag array matters). + +**Collapsed-topology pool dedup**: `compute_collapsed_splits()` in +`ts_splits.cpp` produces the split set excluding collapsed edges. Two +binary trees differing only in zero-length resolutions produce the same +collapsed split set → treated as duplicates by `TreePool::add_collapsed()`. +Both serial (`driven_search`) and parallel (`ThreadSafePool`) paths use +collapsed dedup. + +**Benchmark results** (2026-03-22, 4 standard datasets, 3 seeds each): +Skip rate = 0% on all datasets (Vinther2008 23t, Agnarsson2004 62t, +Zhu2013 75t, Dikow2009 88t). Near-optimal trees in these morphological +datasets have negligible zero-length edges. Overhead from flag computation +is negligible. Score equivalence confirmed (enabled vs disabled produce +identical best scores). Benefit expected on sparse/synthetic data. diff --git a/.AGENTS/memory/shiny_app.md b/.AGENTS/memory/shiny_app.md new file mode 100644 index 000000000..4808e4955 --- /dev/null +++ b/.AGENTS/memory/shiny_app.md @@ -0,0 +1 @@ +## Shiny app (`inst/Parsimony/`) Fully modularized from monolithic `app.R` into Shiny modules: - `global.R` library calls, constants, helpers, colours, citations, module UI instantiation - `ui.R` `fluidPage(...)` definition using module UI elements - `server.R` `AppState()` + module wiring + `ShowConfigs` observer + `onStop()` - `server/app_state.R` `AppState()` typed `reactiveValues()` constructor - `server/logging.R` session logging infrastructure - `server/mod_*.R` 7 Shiny modules (`NS()`/`moduleServer()`) **All server logic now lives in modules.** The old `events.R` has been dissolved; its `ShowConfigs` function and `plotFormat` observer are inlined in `server.R` (they operate on top-level DOM elements). **Modules:** - `mod_references.R` references panel (no state) - `mod_downloads.R` all 8 download handlers - `mod_data.R` data loading + tree management (9 returned reactives). Uses `cb_ref` forward-reference env for circular deps with consensus module. - `mod_clustering.R` clustering analysis + tree distances (5 returned reactives) - `mod_search.R` search engine, scoring, weighting. Owns ExtendedTask, search config modal, result accumulation. - `mod_treespace.R` tree space visualization + plot settings (14 returned reactives) - `mod_consensus.R` consensus plotting, character mapping, stability/rogue analysis, concordance, cluster consensus, main plot dispatch, plot logging (1327 lines). Returns `MainPlot`, `RCode`, `UpdateKeepNTipsRange`, `UpdateDroppedTaxaDisplay`, `UpdateOutgroupInput`. **Important:** Server source files are in `server/` NOT `R/`. Shiny 1.5+ auto-sources all `.R` files in an app's `R/` directory at startup (before any session exists), which crashes on references to `output`/`input`/`session`. Test suite: `NOT_CRAN=true` required for shinytest2 integration tests. Run from `inst/Parsimony/`: ```bash NOT_CRAN=true Rscript -e "testthat::test_dir('tests/testthat')" ``` `setup.R` loads `library(shinytest2)` for `AppDriver` availability. **Important:** Integration tests trigger `pkgbuild::compile_dll(debug=TRUE)` via `load_all()`. `src/TreeSearch-win.def` prevents linker failures from corrupted auto-generated `tmp.def` on Windows. Module tests: `test-mod-references.R` (4), `test-mod-data.R` (9), `test-mod-clustering.R` (12), `test-mod-treespace.R` (5), `test-mod-downloads.R` (11), `test-mod-search.R` (28), `test-mod-consensus.R` (9). Integration tests: `test-app-smoke.R` (3), `test-Distribution.R` (13), `test-SearchLog.R` (4), `test-ViewChars.R` (12). Total: 110 assertions. \ No newline at end of file diff --git a/.AGENTS/memory/testing.md b/.AGENTS/memory/testing.md new file mode 100644 index 000000000..515cfd1d6 --- /dev/null +++ b/.AGENTS/memory/testing.md @@ -0,0 +1,41 @@ +# Test File Conventions + +Load this when: adding or modifying `tests/testthat/test-ts-*.R` files, +choosing test tiers, or writing test helpers. + +--- + +## Conventions + +All `tests/testthat/test-ts-*.R` files must use `TreeSearch:::` to call +internal C++ bridge functions. Define short local wrappers for readability. + +Shared helpers are in `tests/testthat/helper-ts.R` (`make_ts_data()`, +`ts_score()`, `validate_result()`, `skip_extended()`). + +**Never use `%in%` on Splits objects in test files** — S3 dispatch fails +in the cloned namespace created by `test_check()`. Use `as.logical()` +matrix comparison instead. + +--- + +## Test tiering + +Every new `test-ts-*.R` file must be assigned to one of three tiers. +See `tests/testing-strategy.md` for the full rationale. + +| Tier | Guard | When it runs | Use for | +|------|-------|-------------|---------| +| 1 — CRAN | none | always (CRAN + CI + local) | Fast (< ~2 s) API and data-structure unit tests | +| 2 — CI | `skip_on_cran()` at **file level** (first executable line) | CI + local | C++ engine correctness, scoring, search algorithms | +| 3 — Extended | `skip_extended()` at **file level** | `TREESEARCH_EXTENDED_TESTS=true` only | Stress tests, benchmarks, timing measurements | + +**Default for new `test-ts-*` files: Tier 2.** Add `skip_on_cran()` as the +very first executable line (before any helpers or `test_that()` calls): + +```r +# Tier 2: skipped on CRAN; see tests/testing-strategy.md +skip_on_cran() +``` + +Use Tier 3 only for tests that take > ~10 s or are sensitive to machine load. diff --git a/.Rbuildignore b/.Rbuildignore index a0ff5612e..6e5860d20 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -11,17 +11,20 @@ ^inst/Parsimony/tests ^man-roxygen$ ^memcheck$ +^pdf$ ^pkgdown$ ^revdep$ ^split-support$ /^src\-/ CONTRIBUTING\.md +papers\.md README\.md cran\-comments\.md vignettes/\.RData -/^\.git/ -^.*\.o$ +^\.git ^.*\.dll$ +^.*\.o$ +^.*\.sh$ ^.*\.yml$ ^.*\.Rproj$ ^\.Rproj\.user$ @@ -32,3 +35,26 @@ vignettes/\.RData ^_pkgdown\.yml$ ^codemeta\.json$ ^LICENSE$ +^\.positai$ +^\.claude$ +^\.agent- +^\.AGENTS +^\.RESUME +^AGENTS\.md$ +^agent-.*$ +^check_init\.R$ +^coordination\.md$ +^to-do\.md$ +^completed-tasks\.md$ +^issues\.md$ +Makevars\.win\..*-bak$ +^.*\.Rcheck$ + +# Test artifacts +^test.*\.txt$ +^vtune +^dev$ + +# Agent note files +^remote-jobs\.md$ +^papers\.md$ diff --git a/.gitattributes b/.gitattributes index bdb0cabc8..f59250f93 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,3 +15,6 @@ *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain +*.R text eol=lf +*.cpp text eol=lf +*.h text eol=lf diff --git a/.github/workflows/ASan.yml b/.github/workflows/ASan.yml index 8f4c21296..69d55c18b 100644 --- a/.github/workflows/ASan.yml +++ b/.github/workflows/ASan.yml @@ -1,4 +1,5 @@ # Address Sanitizer: Replicate CRAN's gcc-ASAN 'Additional Test' +# Uses the r-hub gcc-asan container (R-devel built with ASAN/UBSAN). on: workflow_dispatch: push: @@ -26,7 +27,9 @@ name: gcc-ASAN jobs: mem-check: - runs-on: ubuntu-24.04 # Update RSPM when increasing + runs-on: ubuntu-latest + container: + image: ghcr.io/r-hub/containers/gcc-asan:latest name: AddressSanitizer ${{ matrix.config.test }} @@ -39,46 +42,14 @@ jobs: - {test: 'vignettes'} env: - R_REMOTES_NO_ERRORS_FROM_WARNINGS: true - _R_CHECK_FORCE_SUGGESTS_: false - RSPM: https://packagemanager.rstudio.com/cran/__linux__/noble/latest - USING_ASAN: true - STRINGI_DISABLE_PKG_CONFIG: true - BIOCONDUCTOR_USE_CONTAINER_REPOSITORY: FALSE # For stringi GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - ASAN_OPTIONS: verify_asan_link_order=0 steps: - - uses: actions/checkout@v5 - - - name: Initialize ASan configuration - run: | - export LD_PRELOAD=$(gcc -print-file-name=libasan.so) - - echo "PKG_CFLAGS = -g -O0 -fsanitize=address -fno-omit-frame-pointer" > src/Makevars - echo "PKG_CXXFLAGS = -g -O0 -fsanitize=address -fno-omit-frame-pointer" >> src/Makevars - - mkdir ~/.R - echo "LDFLAGS = -g -O0 -fsanitize=address -fno-omit-frame-pointer" >> ~/.R/Makevars - - - uses: r-lib/actions/setup-r@v2 + - uses: ms609/actions/asan@main with: - r-version: release # CRAN uses devel, but takes ages to load deps. - - - name: Set up R dependencies - uses: r-lib/actions/setup-r-dependencies@v2 - with: - dependencies: "'soft'" - needs: | - memcheck - - - name: Install package - run: | - cd .. - R CMD build --no-build-vignettes --no-manual --no-resave-data TreeSearch - R CMD INSTALL TreeSearch*.tar.gz - cd TreeSearch - - - name: ASAN - memcheck ${{ matrix.config.test }} - run: | - Rscript memcheck/${{ matrix.config.test }}.R + test: ${{ matrix.config.test }} + # Rogue hard-imports Rfast, a ~30-min source build under ASAN that + # adds no coverage to TreeSearch's own compiled code. All Rogue use + # (two vignettes + the Shiny consensus module) is requireNamespace- + # guarded and skips cleanly when absent. + exclude-packages: Rogue diff --git a/.github/workflows/R-CMD-check.yml b/.github/workflows/R-CMD-check.yml index de0a2f900..35049cfc3 100644 --- a/.github/workflows/R-CMD-check.yml +++ b/.github/workflows/R-CMD-check.yml @@ -20,9 +20,7 @@ on: - "**.R[dD]ata" - "**.Rpro*" pull_request: - branches: - - main - - master + branches: ["*"] paths-ignore: - "Meta**" - "memcheck**" @@ -67,7 +65,7 @@ jobs: steps: - name: Checkout git repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Temporarily bump package version run: | @@ -104,8 +102,14 @@ jobs: with: needs: | check + # highs >= 1.13 calls base R's `%||%` (added in R 4.4.0) while still + # declaring `R (>= 4.0.0)`, so it errors on our R 4.1 leg with + # "could not find function '%||%'". Pin the last good version there + # until upstream fixes its declared R dependency (already done on + # their dev branch). Remove once a fixed highs reaches CRAN. extra-packages: | phangorn=?ignore-before-r=4.1.0 + ${{ matrix.config.r == '4.1' && 'url::https://cran.r-project.org/src/contrib/Archive/highs/highs_1.12.0-3.tar.gz' || '' }} - name: Set up R dependencies (covr) uses: r-lib/actions/setup-r-dependencies@v2 diff --git a/.github/workflows/RcppDeepState.yml b/.github/workflows/RcppDeepState.yml index f815aaa7c..4849fb432 100644 --- a/.github/workflows/RcppDeepState.yml +++ b/.github/workflows/RcppDeepState.yml @@ -1,46 +1,46 @@ -on: - push: - branches: - - main - - master - - '**valgrind**' - paths: - - '.github/workflows/RcppDeepState.yml' - - 'src/**' - - 'inst/include/**' - - 'memcheck/**' - - 'tests/testthat/**.R' - - 'vignettes/**.Rmd' - pull_request: - branches: - - main - - master - paths: - - '.github/workflows/RcppDeepState.yml' - - 'src/**' - - 'inst/include/**' - - 'memcheck/**' - - 'tests/testthat/**.R' - - 'vignettes/**.Rmd' - -name: 'RcppDeepState analysis' -jobs: - RcppDeepState: - runs-on: ubuntu-latest - - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - - steps: - - uses: actions/checkout@v5 - - - uses: FabrizioSandri/RcppDeepState-action@main - with: - fail_ci_if_error: 'true' - additional_dependencies: libgsl-dev - location: '/' - seed: '-1' - max_seconds_per_function: '2' - max_inputs: '3' - comment: 'true' - verbose: 'true' +on: + push: + branches: + - main + - master + - '**valgrind**' + paths: + - '.github/workflows/RcppDeepState.yml' + - 'src/**' + - 'inst/include/**' + - 'memcheck/**' + - 'tests/testthat/**.R' + - 'vignettes/**.Rmd' + pull_request: + branches: + - main + - master + paths: + - '.github/workflows/RcppDeepState.yml' + - 'src/**' + - 'inst/include/**' + - 'memcheck/**' + - 'tests/testthat/**.R' + - 'vignettes/**.Rmd' + +name: 'RcppDeepState analysis' +jobs: + RcppDeepState: + runs-on: ubuntu-latest + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - uses: actions/checkout@v6 + + - uses: FabrizioSandri/RcppDeepState-action@main + with: + fail_ci_if_error: 'true' + additional_dependencies: libgsl-dev + location: '/' + seed: '-1' + max_seconds_per_function: '2' + max_inputs: '3' + comment: 'true' + verbose: 'true' diff --git a/.github/workflows/agent-benchmark.yml b/.github/workflows/agent-benchmark.yml index 49403ad87..5af88a3b7 100644 --- a/.github/workflows/agent-benchmark.yml +++ b/.github/workflows/agent-benchmark.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout git repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up R uses: r-lib/actions/setup-r@v2 @@ -70,7 +70,7 @@ jobs: - name: Upload results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: benchmark-results path: benchmark-results/ diff --git a/.github/workflows/agent-check.yml b/.github/workflows/agent-check.yml index 420ac7497..7e2f40e71 100644 --- a/.github/workflows/agent-check.yml +++ b/.github/workflows/agent-check.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout git repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up R uses: r-lib/actions/setup-r@v2 @@ -86,7 +86,7 @@ jobs: steps: - name: Checkout git repo - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up R uses: r-lib/actions/setup-r@v2 diff --git a/.github/workflows/extended-tests.yml b/.github/workflows/extended-tests.yml new file mode 100644 index 000000000..d090d5d69 --- /dev/null +++ b/.github/workflows/extended-tests.yml @@ -0,0 +1,65 @@ +# Extended test suite — Tier 3 stress / bench / timing tests. +# Runs weekly (Sundays, 3am) and on-demand. +# Sets TREESEARCH_EXTENDED_TESTS=true so skip_extended() guards are lifted. +# See tests/testing-strategy.md for full tiering documentation. + +on: + workflow_dispatch: + schedule: + - cron: '0 3 * * 0' # Sundays, 3am + +name: extended-tests + +jobs: + extended-tests: + runs-on: ubuntu-24.04 + + env: + NOT_CRAN: 'true' + TREESEARCH_EXTENDED_TESTS: 'true' + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + RSPM: "https://packagemanager.posit.co/cran/__linux__/noble/latest" + + steps: + - name: Checkout git repo + uses: actions/checkout@v6 + + - name: Set up R + uses: r-lib/actions/setup-r@v2 + with: + r-version: 'release' + + - name: Install apt packages + run: sudo apt-get install -y texlive-latex-base texlive-fonts-recommended + + - name: Set up R dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + needs: check + + - name: Build and install package + run: R CMD INSTALL . + shell: bash + + - name: Run extended test suite + run: | + Rscript -e " + library(testthat) + library(TreeSearch) + test_dir('tests/testthat', filter = 'ts-', + reporter = 'progress', stop_on_failure = TRUE) + " + shell: bash + + - name: Notify on failure + if: failure() && github.event_name == 'schedule' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: 186, + body: 'Extended tests workflow has failed: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' + }); diff --git a/.github/workflows/update-csl.yml b/.github/workflows/update-csl.yml index 36fb0725c..baac81b63 100644 --- a/.github/workflows/update-csl.yml +++ b/.github/workflows/update-csl.yml @@ -13,7 +13,7 @@ jobs: update-csl: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Download latest CSL file run: | diff --git a/.gitignore b/.gitignore index 9fe0f1395..ff4f6c7d6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ inst/ape inst/doc results-* revdep/ +.tnt-survey/ tests/testthat/_snaps/*.new.* vignettes/*.html vignettes/*.pdf @@ -57,3 +58,30 @@ test_output.txt /.tnt-bench /.vtune* /vtune* +dev/profiling/.vtune-lib-*/ +dev/profiling/.build-* +dev/profiling/.Makevars.symboled +/.claude + +# /profile skill — VTune results and profvis HTML are large and stale fast +dev/profiling/result_*/ +dev/profiling/drivers/*-profvis.html +dev/profiling/drivers/*-profvis_files/ +dev/profiling/*-lib/ +dev/profiling/*-lib-*/ + +# Dispatcher state and logs (ephemeral runtime files) +.dispatch/ +!.dispatch/logs/.gitkeep + +# T-300 NA benchmark artifacts +dev/profiling/.bench-libs/ +dev/profiling/t300_na_bench_*.rds + +# dev/benchmarks throwaway outputs (regenerable from the bench_*/diag_* scripts) +dev/benchmarks/*.csv +dev/benchmarks/.*.csv +dev/benchmarks/*.run +dev/benchmarks/*.png +dev/benchmarks/trace_*.txt +dev/benchmarks/*_raw.txt diff --git a/.positai/expertise/coordination.md b/.positai/expertise/coordination.md new file mode 100644 index 000000000..466f072d4 --- /dev/null +++ b/.positai/expertise/coordination.md @@ -0,0 +1,74 @@ +# Coordination Expertise — TreeSearch + +## Purpose + +Review the overall state of multi-agent work. Update `coordination.md`, +propose new tasks, resolve blockers. This is the "project manager" role. + +## Workflow + +1. **Read all agent files** (`agent-a.md` through `agent-f.md`): + - Who is working on what? + - Is anyone stuck or blocked? + - Has anyone finished a task without updating to-do.md? + +2. **Read `to-do.md`**: + - Are completed tasks moved to the Completed section? + - Are task statuses accurate? + - Are priorities still correct given current project state? + - Are there enough OPEN tasks to keep all agents busy? + - Adjust standing task priorities per the dynamic priority rule. + +3. **Read `coordination.md`**: + - Update the Agent Status table from agent files. + - Update Known Issues if any have been resolved. + - Add new Architecture Decisions if agents have made significant choices. + +4. **Read `AGENTS.md`** (bottom sections): + - Check for newly documented completed work. + - Verify that documentation matches what agents report. + +5. **Propose new tasks** if needed: + - If <6 OPEN specific tasks, look at `coordination.md` strategic + objectives and break the next one into concrete, assignable tasks. + - If agents have reported findings (from red-team or profiling), + ensure those are captured in to-do.md. + +6. **Update all files**: + - `coordination.md` — agent status, any new issues or decisions + - `to-do.md` — new tasks, priority adjustments, status corrections + - `agent-X.md` — mark your own task as complete + +## Task Creation Guidelines + +Good tasks are: +- **Specific**: "Profile ratchet inner loop for Zhu2013 dataset" not + "Investigate performance" +- **Scoped**: Completable by one agent in one session (~1-2 hours) +- **Independent**: Minimal overlap with other tasks (check Blocks column) +- **Testable**: Clear success criteria (tests pass, benchmark improves, etc.) + +When deriving tasks from strategic objectives: +- Break Phase 6 steps into individual tasks (T-001 through T-005 already done) +- For code quality work, group related TODOs into one task per file/module +- For documentation, one task per major section (vignettes, function docs, etc.) + +## Priority Guidelines + +| Priority | Criteria | +|----------|----------| +| P0 | Blocks multiple agents or causes incorrect results | +| P1 | Blocks the next strategic objective or is a correctness bug | +| P2 | Important but not blocking; performance improvements | +| P3 | Nice to have; cleanup; future-looking | + +## Cross-Agent Conflict Detection + +Watch for: +- Two agents modifying the same file (especially `ts_rcpp.cpp`, + `TreeSearch-init.c`, `R/RcppExports.R`) +- Incompatible parameter changes to the same Rcpp bridge function +- One agent's optimization breaking another's assumptions + +If conflicts are detected, flag them in `to-do.md` as P0 and note +which agents are affected. diff --git a/.positai/expertise/fitch-scoring.md b/.positai/expertise/fitch-scoring.md new file mode 100644 index 000000000..4f74afa9f --- /dev/null +++ b/.positai/expertise/fitch-scoring.md @@ -0,0 +1,136 @@ +# Fitch Scoring — Design Notes & Proven Invariants + +Reference for agents working on `ts_fitch.h/.cpp`, `ts_fitch_na.h`, +`ts_fitch_na_incr.h`, or the search modules that call them. + +## Incremental uppass correctness (standard Fitch) + +The incremental uppass (`fitch_incremental_uppass`) uses a dirty-flag +propagation scheme that does **not** explicitly revisit every node whose +prelim changed during the incremental downpass. Only nodes whose +*ancestor's final* changed are recomputed. + +This looks like it could miss updates when the downpass stops before +root (prelim stabilises at some intermediate node N). Nodes between +`clip_ancestor` and N have changed prelims but their ancestors' finals +are unchanged, so the dirty-flag scheme skips them. + +**This is provably correct for standard (non-NA) Fitch blocks.** + +### Proof sketch + +When the downpass stops at node N, `fitch(M_new, S) = fitch(M_old, S)` +where M is N's child on the downpass path and S is the sibling. + +**Case 1 — both intersection-type:** `M_old ∩ S = M_new ∩ S = P`. +Then N_final ⊆ P ⊆ M_old and N_final ⊆ P ⊆ M_new. So +`uppass(N_final, M_old) = N_final ∩ M_old = N_final` and likewise for +M_new. Finals are identical. + +**Case 2 — both union-type:** `M_old ∪ S = M_new ∪ S` with +`M_old ∩ S = ∅` and `M_new ∩ S = ∅`. Since the unions are equal and +both M sets are disjoint from S, `M_old = M_new`. No change. + +**Case 3 — mixed types:** Intersection equals union only if both +operands are identical and the set is trivial. Not reachable in +practice (would require empty state sets). + +The argument applies per-character (per bit position), so it holds +for packed 64-bit representations. + +### Consequence + +No code change needed. The dirty-flag scheme is an optimisation that +happens to be exact for standard Fitch, not just a heuristic. + +--- + +## NA uppass `children_app` staleness + +The NA-aware incremental uppass (`fitch_na_incremental_uppass`) has a +**theoretical staleness issue** that does NOT affect standard blocks. + +The NA uppass formula at internal nodes uses: + +```cpp +uint64_t children_app = 0; +for (int s = 1; s < k; ++s) + children_app |= (tree.prelim[left + s] | tree.prelim[right + s]); +``` + +This `children_app` can change even when the node's own prelim is +stable, because the NA downpass aggregates children differently (using +intersection/union/strip cases) from the raw OR of children's states. + +If the downpass stops at node N because N's NA-aware prelim didn't +change, but N's child M *did* change prelim, then `children_app` at N +is different from before. The dirty-flag scheme won't revisit N, so +N's `final_` for NA blocks may be stale. + +### Impact + +- `fitch_na_pass3_score()` uses `final_` for `ss_app` (applicability). + A stale `ss_app` can make `divided_length` slightly wrong. +- Indirect length calculations use `final_` for virtual-root + computation, so candidate scores can be slightly wrong. +- **Conservative**: `full_rescore()` always runs before accepting a + move, so final results are never affected. +- Same design class as the documented `extract_divided_steps` heuristic + (ts_tbr.cpp:39-41) which uses stale `local_cost` for NA blocks. + +### If this ever needs fixing + +Mark the entire rootward path from `clip_ancestor` as dirty: + +```cpp +int node = clip_ancestor; +while (node != root) { + dirty[node] = true; + node = tree.parent[node]; +} +``` + +This is O(depth) extra work per clip, acceptable for correctness. +Currently not worth doing because full_rescore is authoritative. + +--- + +## upweight_mask coverage + +During ratchet perturbation, `upweight_mask` doubles the contribution +of selected characters. Every function that computes EW step counts +must account for it. The pattern: + +```cpp +int ns = popcount64(needs_step); +if (blk.upweight_mask) ns += popcount64(needs_step & blk.upweight_mask); +extra_steps += blk.weight * ns; +``` + +**Sites that must have this** (all verified correct as of 2026-03-19): + +| Function | File | Status | +|----------|------|--------| +| `fitch_downpass` | ts_fitch.cpp | ✓ | +| `fitch_incremental_downpass` | ts_fitch.cpp | ✓ | +| `fitch_indirect_length` | ts_fitch.cpp | ✓ | +| `fitch_indirect_length_bounded` | ts_fitch.cpp | ✓ (fixed T-096) | +| `fitch_indirect_length_cached` | ts_fitch.cpp | ✓ (fixed T-096) | +| `fitch_na_indirect_length` | ts_fitch_na_incr.h | ✓ | +| `fitch_na_indirect_length_bounded` | ts_fitch_na_incr.h | ✓ | +| `fitch_na_indirect_length_cached` | ts_fitch_na_incr.h | ✓ | +| `fitch_na_score` Pass 1 (standard blocks) | ts_fitch_na.h | ✓ | +| `fitch_na_score` Pass 3 | ts_fitch_na.h | ✓ | +| `fitch_na_pass3_score` | ts_fitch_na_incr.h | ✓ | +| `fitch_na_incremental_downpass` (standard blocks) | ts_fitch_na_incr.h | ✓ | +| `nx_cost` in TBR | ts_tbr.cpp | ✓ (fixed T-096) | +| `nx_cost` in SPR | ts_search.cpp | ✓ (fixed T-096) | +| `nx_cost` in drift | ts_drift.cpp | ✓ (fixed T-096) | +| drift RFD computation | ts_drift.cpp | ✓ (fixed T-096) | + +**Does NOT need upweight_mask:** +- `extract_char_steps` / `extract_divided_steps` — these extract raw + per-pattern step counts for IW/profile scoring, which uses + `pattern_freq` doubling instead of `upweight_mask`. +- `fitch_downpass_node` (standalone) — callers handle weighting. +- IW indirect variants — weighting baked into `iw_delta`. diff --git a/.positai/expertise/profiling.md b/.positai/expertise/profiling.md new file mode 100644 index 000000000..879ec7505 --- /dev/null +++ b/.positai/expertise/profiling.md @@ -0,0 +1,506 @@ +# Profiling Expertise — TreeSearch + +## Purpose + +Profile the C++ search engine to identify bottlenecks. Produce specific, +actionable optimization tasks for `to-do.md`. + +## Tools + +### 1. Built-in Phase Timing (Quick) + +The driven search already has `std::chrono` phase timing at `verbosity >= 2`. +Use the R-level interface: + +```r +library(TreeSearch) +library(TreeTools) +dataset <- TreeSearch::inapplicable.datasets[["Vinther2008"]] +result <- MaximizeParsimony(dataset, maxReplicates = 3, verbosity = 2L) +``` + +This prints per-phase timing. For programmatic access, use the +`ts_bench_tbr_phases` diagnostic function (7 args, registered in +TreeSearch-init.c). + +### 2. std::chrono Micro-Benchmarks (Medium) + +For fine-grained timing of specific functions, add `steady_clock` timing +around the code path of interest. See `inst/benchmarks/bench_memory.R` +and `inst/benchmarks/bench_simd.R` for examples. + +Key metrics to measure: +- Per-candidate indirect scoring cost (ns) +- Clip+incremental phase time (μs per TBR pass) +- Full rescore time (μs) +- Snapshot save/restore time (μs) + +### 3. VTune (Thorough) + +For instruction-level hotspot analysis, use the `r-package-profiling` +skill (load via the skill tool). Key steps: + +1. Build with debug symbols: set `DLLFLAGS` via `MAKEFLAGS` env var +2. Run a representative workload under VTune +3. Analyze hotspots in the VTune GUI + +See `.positai/skills/r-package-profiling/references/` for detailed +VTune workflow on Windows. + +**Current version: VTune 2025.10** (updated 2026-03-19). Requires Ice Lake +or newer CPU (10th gen Intel Core / 3rd gen Xeon Scalable+). VS 2019 +integration and Eclipse integration are removed in 2025.x. Command-line +workflow (`vtune -collect hotspots`) is unchanged. + +### 4. R-Level Profiling + +For R overhead identification: + +```r +Rprof("profile.out") +result <- MaximizeParsimony(dataset, maxReplicates = 5) +Rprof(NULL) +summaryRprof("profile.out") +``` + +## Known Baselines + +### Latest run: 2026-03-27 by Agent A (round 6: post-T-261/T-262/T-263 phase distribution) + +See "Phase distribution: current thorough preset" section below for updated numbers. +The 2026-03-18 baselines used strategy='none' (TBR-only); the thorough preset +now dominates medium-scale search, making direct comparison impractical. + +### Previous run: 2026-03-18 16:00 by Agent A (v2.0.0, single-agent, quiet machine) + +Previous baselines (2026-03-17) were inflated ~30–40% by multi-agent machine +contention. Scores are identical. Timings below are authoritative. + +### End-to-end benchmarks (3-run medians, 5 reps, strategy='none', EW): + +| Dataset | Tips | Chars | Median (s) | Score | +|---------|------|-------|------------|-------| +| Vinther2008 | 23 | 57 | 0.390 | 79 | +| Agnarsson2004 | 62 | 242 | 1.860 | 778 | +| Zhu2013 | 75 | 253 | 2.720 | 655 | +| Dikow2009 | 88 | 220 | 3.860 | 1614 | + +### Per-phase breakdown (Zhu2013, 5 reps, two runs averaged): + +| Phase | % of time | Avg ms/rep | +|-------|-----------|------------| +| Wagner | <0.1% | <1 | +| TBR | 24–37% | 110–160 | +| XSS | 10% | 35–55 | +| RSS | 2% | 9–13 | +| Ratchet | 24–28% | 90–155 | +| Drift | 25–33% | 90–200 | +| Final TBR | 2% | 7–10 | + +Ratchet (24-28%) and drift (25-33%) dominate. TBR (24-37%) varies +substantially by run. XSS ~10%, RSS ~2%, both stable. + +### Wagner tree construction: Negligible (<0.1% of search time) + +| Dataset | Tips | µs/tree | % of replicate | +|---------|------|---------|----------------| +| Vinther2008 | 23 | 300 | <0.1% | +| Agnarsson2004 | 62 | 1000 | 0.3% | +| Zhu2013 | 75 | 600 | 0.1% | +| Dikow2009 | 88 | 1400 | 0.2% | + +Not a bottleneck at any dataset size. No optimization needed. + +### Parallel scaling (2 threads) + +| Dataset | Reps | 1T (s) | 2T (s) | Speedup | Efficiency | +|---------|------|--------|--------|---------|------------| +| Zhu2013 | 5 | 2.53 | 1.59 | 1.59× | 80% | +| Zhu2013 | 10 | 5.16 | 3.29 | 1.57× | 78% | +| Zhu2013 | 20 | 10.70 | 5.20 | 2.06× | 103%* | +| Zhu2013 | 40 | 18.63 | 11.35 | 1.64× | 82% | +| Dikow2009 | 10 | 7.76 | 5.11 | 1.52× | 76% | + +*Superlinear at 20 reps is stochastic noise (different search paths). + +**Finding:** Typical 2-thread efficiency is 78–82%. The old 1.24× measurement +was a multi-agent machine contention artifact. The implementation (dynamic +work-stealing via `atomic::fetch_add`, mutex-guarded pool) is sound. +Main loss is stochastic load imbalance between replicate times. + +### XSS/RSS effectiveness (5 reps per dataset) + +| Dataset | Tips | XSS hits | XSS avg Δ | XSS avg ms | RSS hits | RSS avg Δ | RSS avg ms | +|---------|------|----------|-----------|------------|----------|-----------|------------| +| Agnarsson2004 | 62 | 3/5 | 3.8 steps | 59 | 0/5 | 0 | 14 | +| Zhu2013 | 75 | 5/5 | 26.6 steps | 43 | 2/5 | 1.0 | 11 | +| Dikow2009 | 88 | 0/5 | 0 | 93 | 1/5 | 3.2 | 29 | + +**Finding:** XSS effectiveness is highly dataset-dependent — from zero +improvement (Dikow2009) to 27-step average improvement (Zhu2013). No obvious +predictor from simple nTip/nChar statistics. XSS cost is ~10% of replicate +time; acceptable when effective but wasted when not. + +RSS is marginal across all datasets (0–3 steps, 2% of time). One exception: +Dikow2009 where RSS found 16 steps while XSS found 0 — suggests they +explore different neighbourhoods. + +### Auto strategy (reference — unchanged from T-066/T-068 study) + +Threshold: ≥75 tips AND nChar < 100 triggers "thorough". Signal-density gate +prevents unnecessary thorough runs on character-rich datasets. + +### R overhead: <0.5% of wall time (confirmed via Rprof, unchanged) + +### Scaling exponent: ~2.82 (TBR pass time vs tips, unchanged) + +### Drift/ratchet cycle tuning (reference — unchanged from T-029 study) + +| Config | Med score | Min score | Med time | Speedup | +|--------|-----------|-----------|----------|---------| +| d5_r5 (default) | 656 | 648 | 5.7s | — | +| d2_r5 | 660 | 646 | 4.1s | 28% | +| d2_r2 | 662 | 656 | 3.8s | 33% | +| d0_r5 | 658 | 650 | 2.8s | 51% | +| d5_r0 | 662 | 660 | 4.8s | 16% | + +Lower score = better. Current defaults: d2_r5. + +### CSS effectiveness: Marginal (adds 2-6% time, no consistent improvement) +Disabled by default (cssRounds=0). + +### Latest EW regression check: 2026-03-19 by Agent A (v2.0.0, post T-115–T-124) + +All datasets pass regression benchmark. EW baselines updated with 7-run medians: + +| Dataset | Tips | Chars | Median (s) | Score (range) | Notes | +|---------|------|-------|------------|---------------|-------| +| Vinther2008 | 23 | 57 | 0.420 | 79 | stable | +| Agnarsson2004 | 62 | 242 | 1.790 | 778 | stable | +| Zhu2013 | 75 | 253 | 3.170 | 648–666 | high variance (2.5–7.6s range) | +| Dikow2009 | 88 | 220 | 4.900 | 1612–1614 | high variance (4.0–12.4s range) | + +Zhu2013/Dikow2009 appear slightly slower than 2026-03-18 baselines (~17–27%) but +within stochastic noise. Phase breakdown unchanged. No regression in C++ engine. +The recent DataSet changes (inapp_state field, HSJ/XFORM modes) have no measurable +effect on EW search paths. + +### HSJ and XFORM scoring baselines: 2026-03-19 by Agent A + +Synthetic hierarchical datasets (valid hierarchy structure: primary + secondary chars, +secondaries are inapplicable when primary absent). 3-run medians, 5 reps per run. + +| Config | Tips | Chars | Blocks | EW (s) | HSJ (s) | XFORM (s) | HSJ/EW | XFORM/EW | +|--------|------|-------|--------|--------|---------|-----------|--------|----------| +| small | 20 | 19 | 3 | 0.020 | 0.010 | 0.020 | 0.5× | 1.0× | +| medium | 40 | 50 | 5 | 0.170 | 0.100 | 0.280 | 0.6× | 1.6× | +| large | 60 | 82 | 8 | 0.610 | 0.360 | 1.330 | 0.6× | 2.2× | +| xlarge | 80 | 120 | 10 | 5.920 | 3.560 | 9.460 | 0.6× | 1.6× | + +**HSJ is faster than EW** (~0.6× at medium/large sizes) because: +1. Fitch candidate screening guards expensive full HSJ rescore — most candidates + are rejected by Fitch before HSJ is called. +2. Hierarchy datasets have a simpler parsimony landscape (secondaries add signal + only when primary is present), leading to faster search convergence. + +**XFORM is slower than EW** (~1.6–2.2× at medium/large sizes) due to Sankoff +cost per candidate. Phase breakdown (large config, 5 reps): + +| Phase | EW avg ms/rep | HSJ avg ms/rep | XFORM avg ms/rep | +|-------|---------------|----------------|------------------| +| TBR | 25 | 23 | 29 | +| XSS | 14 | 7 | 14 | +| RSS | 4 | 2 | 5 | +| Ratchet | 51 | 28 | 86 | +| Drift | 22 | 13 | 36 | +| Final TBR | 2 | 1 | 4 | +| **Total** | **117** | **74** | **174** | + +XFORM overhead concentrated in Ratchet (+69%) and Drift (+64%), which perform +more scoring iterations than TBR. XSS/RSS overhead is negligible. + +**Conclusion:** Both modes are acceptable. XFORM at ~1.7× overhead for real +workflows is reasonable given the algorithmic complexity (Sankoff vs Fitch). +No optimization tasks raised — XFORM at this cost is expected behavior. + +### Hierarchical resampling: 2026-03-19 by Agent A + +Medium config (40 tips, 50 chars, 5 blocks), jackknife, 20 reps: + +| Mode | 1 thread (s) | 2 threads (s) | Speedup | +|------|-------------|--------------|---------| +| Brazeau (C++ parallel) | 5.19 | 2.05 | 2.5× | +| HSJ hierarchical (serial R loop) | 1.76 | 1.64 | 1.1× | +| XFORM hierarchical (serial R loop) | measured via 10-rep: ~1.58 | — | — | + +**Finding 1 (positive):** HSJ/XFORM hierarchical resampling is faster than Brazeau +per-replicate because the block-level resampling units (35 vs 50 units) produce +simpler per-replicate datasets. No performance concern here. + +**Finding 2 (known limitation):** Hierarchical resampling uses a serial R loop +across replicates — `nThreads` only applies within each replicate's internal search. +Brazeau gets full 2.5× at 2 threads; HSJ/XFORM get only ~1.1×. For users running +50–100 jackknife replicates with large HSJ/XFORM datasets, wall time will be ~2× +longer than equivalent Brazeau. This is documented in AGENTS.md as a known future +optimization (C++-level inter-replicate parallelism for hierarchical resampling). +No new task filed — already on the roadmap. + +### Preset tuning benchmark: 2026-03-22 by Agent A + +Compared updated presets (wagnerStarts=3, sprFirst=TRUE, adaptiveLevel=TRUE +for default; wagnerStarts=3, sprFirst=TRUE for thorough) against old presets +(wagnerStarts=1, sprFirst=FALSE, adaptiveLevel=FALSE). 7-run medians via +`MaximizeParsimony()`, strategy=auto, 10 reps, 1 thread. + +| Dataset | Tips | Preset | Old time (s) | New time (s) | Δ time | Old score | New score | +|---------|------|--------|-------------|-------------|--------|-----------|-----------| +| Vinther2008 | 23 | sprint | 0.76 | 0.65 | –14% (noise) | 79 | 79 | +| Agnarsson2004 | 62 | default | 3.59 | 2.41 | **–33%** | 778 | 778 | +| Zhu2013 | 75 | thorough | 23.65 | 24.83 | +5% (noise) | 647 | 648 | +| Dikow2009 | 88 | thorough | 49.19 | 39.24 | **–20%** | 1611 | 1612 | + +**Findings:** +- `adaptiveLevel` in `default` preset: consensus-stability triggers early exit + on easy landscapes (Agnarsson2004), saving 33%. No score regression. +- `sprFirst + wagnerStarts=3` in `thorough`: 20% faster on Dikow2009 (better + starting tree reduces initial TBR descent). Neutral on Zhu2013. +- **Do not enable `adaptiveLevel` in `thorough`**: with 20 ratchet + 12 drift + base, 1.5× scaling creates 30 ratchet + 18 drift per hard replicate, + causing 3–4× slowdowns for only 2–3 step improvement (benchmarked separately). + +### 180-tip large-preset baselines: 2026-03-26 by Agent E (Hamilton HPC, EPYC 7702) + +Dataset: mbank_X30754 (180 taxa, 425 chars, 418 patterns, 40% missing, 20% inapplicable). +Strategy: auto → "large" preset. 5 seeds per budget, single-threaded. + +**Score quality by budget (median, 5 seeds):** + +| Budget | Median score | Range | Reps/seed | +|--------|:-----------:|:-----:|:---------:| +| 30s | 1202 | 1189–1214 | ~1.5 | +| 60s | 1190 | 1190–1202 | ~3 | +| 120s | 1185 | 1171–1189 | ~6 | + +Per-replicate time: median 17.3s (range 13.7–21.2s). MPT enumeration adds +0–2 steps beyond best single-replicate score. + +**Phase distribution (rep 1, 30s budget, 5-seed averages):** + +| Phase | % time | Mean ms | Steps/s | Hit rate | +|-------|:------:|--------:|:-------:|:--------:| +| TBR | 43.6% | 7313 | 91.4 | 5/5 (661 steps avg) | +| Ratchet | 32.2% | 5390 | 4.5 | 5/5 (26.6 steps avg) | +| SA (anneal) | 7.4% | 1241 | 0.8 | 7/50 (14%, 1.3 steps) | +| XSS | 5.4% | 897 | 13.8 | 4/5 | +| Wagner+NNI | 4.7% | 790 | — | starting point | +| RSS | 3.2% | 530 | 4.8 | 3/5 | +| CSS | 2.5% | 424 | 11.2 | 2/5 | +| Final TBR | 1.0% | 174 | 5.2 | 1/5 | + +**SA (simulated annealing) phase is the least productive:** 7.4% of time, +14% hit rate (7/50 reps improved by 1.3 steps on average). Efficiency = +0.8 steps/s, far below ratchet (4.5) or XSS (13.8). annealCycles=3, +annealPhases=5 may be overtuned. Reducing could save ~1.2s/rep → 1 extra +replicate per ~17s saved. + +**Comparison with earlier Intel desktop baselines (T-179, pre-T-206):** + +| Budget | Intel (pre-T-206) | EPYC (post-T-206) | Delta | +|--------|:-:|:-:|:-:| +| 30s | 1276 | 1202 | −74 | +| 60s | 1255 | 1190 | −65 | +| 120s | 1250 | 1185 | −65 | + +The 65–74 step gap is **primarily due to T-206** (outer cycle reset cap), +not hardware. T-206 was merged 2026-03-24 19:27; the Intel baselines were +recorded at 12:56 the same day (pre-T-206). Without the reset cap, each +replicate performed 3–5 pipeline cycles (~51–85s) vs ~17s with cap=0. +At 120s budget: ~2 replicates pre-T-206 vs ~6 post-T-206. Hardware +differences (Intel desktop vs EPYC 7702) are a secondary factor. + +### Phase distribution: current thorough preset (2026-03-27, Agent A, round 6) + +Dataset: Zhu2013 (75t, 253 chars). Strategy: auto → thorough. +3 reps, single-threaded, post-T-261+T-262+T-263. Total: 33.7 s = ~11.2 s/rep. + +| Phase | Calls | Total ms | Mean ms | % | +|-------|:-----:|:--------:|:-------:|:---:| +| Ratchet | 14 | 15617 | 1116 | 46.3% | +| NNI-perturb | 14 | 11565 | 826 | **34.3%** | +| RSS | 14 | 2488 | 178 | 7.4% | +| CSS | 14 | 1477 | 106 | 4.4% | +| XSS | 14 | 1079 | 77 | 3.2% | +| TBR (post-phase) | 14 | 622 | 44 | 1.8% | +| Initial TBR | 3 | 468 | 156 | 1.4% | +| wag+NNI | 2 | 427 | 214 | 1.3% | + +**Key findings vs 2026-03-18 baselines:** + +1. **TBR is no longer a bottleneck** (1.4% + 1.8% = 3.2%). T-261+T-262+T-263 + combined are working — TBR has become fast enough that other phases dominate. + Drift was 25–33% before T-255; its removal freed that budget to more ratchet. + +2. **NNI-perturb at 34.3% with poor efficiency:** + - Hit rate: 14% (2/14 calls improved score) + - Mean improvement when hit: 1 step + - Efficiency: 0.17 steps/s vs ratchet's ~4–8 steps/call at comparable cost + - Cost grows within a replicate (early calls ~300ms, late calls ~1300ms) + - This phase likely over-tuned for 75-tip datasets. Filed **T-274** (P2). + +3. **RSS at 7.4%** — higher than old 2% baseline. With conflict-guided RSS and + outerCycles/reset mechanism creating ~4.7 RSS calls per replicate at ~178ms each + (~837ms/rep). Old uniform RSS: ~11ms/rep. 16× overhead increase. Most of this + is the actual sector TBR cost (more calls × similar per-sector time), not conflict + computation overhead. The reset mechanism is the multiplier. + +4. **wag+NNI at 1.3%**: biased Wagner + 3 starts + NNI warmup adds ~214ms per + replicate start. Negligible at this scale; confirms T-246/NNI-warmup tuning is fine. + +## What to Profile + +Status key: ✅ resolved, ⚠ partially explored, ❌ not yet investigated + +1. ✅ **Drift + ratchet inner loops** (50–60% of C++ time combined). Both use + TBR internally. Per-candidate indirect evaluation at memory-throughput + limit (~23 ns at 75 tips per T-075). Cycle counts tuned (d2_r5). + **Drift threshold sensitivity (2026-03-18 Agent E):** AFD={1,3,5,8} × + RFD={0.05,0.1,0.2} on Zhu2013 (75 tips, 15 runs each): no significant + score difference between any config (Wilcoxon p=0.60–1.00). Permissive + thresholds (AFD=8, RFD=0.2) waste time; tight vs default indistinguishable. + On Dikow2009 (88 tips), d2 drift provides no benefit over ratchet alone + (p=0.54); d6 gives 2-step improvement (p=0.006) at 2× time cost. + **Conclusion:** Current defaults (AFD=3, RFD=0.1) are fine. Cycle count + matters more than threshold values. No optimization task raised. + +2. ✅ **Sectorial search effectiveness** (12% of time). XSS effectiveness is + dataset-dependent (0–27 steps). RSS is marginal (0–3 steps). No clear + predictor from simple dataset statistics. Could make XSS adaptive (skip + after N unproductive reps) but time savings would be <10%. + +3. ✅ **Wagner tree construction**: <0.1% of search time. Not a bottleneck. + +4. ✅ **R overhead**: <0.5% of wall time. Not a bottleneck. + +5. ✅ **Parallel scaling**: 78–82% efficiency at 2 threads. Implementation is + sound (dynamic work-stealing, low-contention pool). Main loss is stochastic + load imbalance. No obvious improvement without algorithmic changes. + +6. ✅ **IW scoring overhead** (2026-03-18 Agent E). Compared EW vs IW (k=10, + k=3) on three datasets (5 runs each, d2_r5, 5 reps, serial): + - Vinther2008 (23 tips): IW 64% *faster* (landscape converges quicker) + - Agnarsson2004 (62 tips): IW 26–39% slower + - Zhu2013 (75 tips): IW 40–57% slower + IW overhead scales with dataset size due to per-character weighted delta + computation in indirect scoring. No optimization opportunity — the delta + lookup is already O(n_blocks) per candidate, same as EW Fitch. + +7. ✅ **Fuse effectiveness** (2026-03-18 Agent E). Compared fuseInterval=0 vs + 3 on three datasets (8 runs each, 10 reps): + - Agnarsson2004: identical scores/time (pool deduplicates to 1 tree) + - Zhu2013: identical scores/time + - Dikow2009: negligible overhead (13.65s vs 13.78s with poolSuboptimal=5) + Fuse is cheap when pool is small, free when pool=1. Current default + (fuseInterval=3) is appropriate. No optimization task raised. + +## Comparing Search Strategies: Time-Adjusted Expected Best + +When comparing strategies that differ in per-replicate cost (e.g. NNI→TBR +vs TBR alone), the **median per-replicate score is the wrong metric**. +Multi-start search keeps the best tree across all replicates, so what +matters is the expected minimum from k independent draws, where +k = budget / time_per_replicate. + +A strategy with high variance but occasional excellent scores can dominate +a consistent-but-mediocre one — if it's fast enough to get more draws. + +**Bootstrap estimation:** +```r +expected_best <- function(scores, k, n_boot = 5000) { + mean(replicate(n_boot, min(sample(scores, k, replace = TRUE)))) +} + +# k = budget / median_time_per_rep for each strategy +k <- floor(budget / median_time) +exp_best <- expected_best(observed_scores, k) +``` + +Compare `exp_best` across strategies at fixed budget (e.g. 20s, 60s, 120s). +This naturally trades off per-replicate quality against replicate throughput. + +**When median IS acceptable:** comparing parameter changes on a fixed pipeline +(same time-per-rep), e.g. ratchet perturbation probability. All runs take +roughly the same time, so k is constant and the median is a reasonable proxy. + +See AGENTS.md "NNI in the driven pipeline" for the reference application of +this metric (NNI→TBR vs TBR at 88 and 180 tips). + +## Reporting Format + +For each finding, add to `to-do.md`: + +``` +| T-NNN | P2 | OPEN | — | [Profile] Brief description | X% of time. Potential Y% improvement via Z approach. | +``` + +Include the measurement methodology and baseline numbers so the implementer +can verify the improvement. + +8. ✅ **HSJ scoring overhead** (2026-03-19 Agent A). HSJ is ~0.6× EW wall time + (faster) on synthetic hierarchical data. Fitch screening gates full HSJ rescore + effectively. No optimization needed. + +9. ✅ **XFORM (Sankoff) scoring overhead** (2026-03-19 Agent A). XFORM is ~1.6–2.2× + EW wall time. Overhead concentrated in Ratchet (+69%) and Drift (+64%). This + is expected Sankoff vs Fitch arithmetic cost — no obvious optimization target. + +10. ✅ **Hierarchical resampling parallelism** (2026-03-19 Agent A). Serial R loop + means `nThreads` only applies within each replicate. Brazeau 2T = 2.5× speedup; + HSJ/XFORM hierarchical 2T = 1.1× only. Known limitation, future optimization + (C++-level inter-replicate parallelism for hierarchical resampling). + +11. ✅ **MaddisonSlatkin internal bottlenecks** (2026-03-19 Agent A, T-149). + VTune hotspot collection (software sampling, `-g -fno-omit-frame-pointer` + symbols build) on 57 calls at boundary cases: k=3/n=20–25, k=4/n=14–18, + k=5/n=9–12. Total ~23 s CPU time; 63% in `TreeSearch.dll`. + + **CPU time breakdown within TreeSearch.dll (14.1 s):** + + | Category | CPU (s) | % DLL | + |----------|---------|-------| + | `logB_cache::find` (k=3,4,5) | 2.72 | 19% | + | `SolverT::LogB` compute | 1.88 | 13% | + | `logPVec_cache::find` (k=3,4,5) | 1.91 | 14% | + | `SolverT::LogPVec` compute | 1.24 | 9% | + | `LogPVecKey::operator==` | 1.11 | 8% | + | `StateKeyT::operator==` | 1.01 | 7% | + | `expl`/`_expl_internal` (LogB LSE) | 0.91 | 6% | + | `logRD_cache::find` | 0.74 | 5% | + | `std::isfinite` (all sites) | 0.70 | 5% | + | `vector::~vector` (eviction) | 0.60 | 4% | + | `logconv` actual convolution | 0.20 | 1% | + + **Key findings:** + - `logconv` is only **1%** of DLL time — the Phase 2 vectorization worked + perfectly; the algorithm itself is no longer the bottleneck. + - **Hash map infrastructure dominates** (53% of DLL time): `unordered_map::find` + + key equality checks across the three caches (logB, logPVec, logRD). + Switching to a flat/open-addressing map would help but adds complexity. + - **`expl()` in `LSEAccumulator`** (6%) uses long-double arithmetic. Switching + to `double`/`exp()` would save ~0.7s at negligible precision cost. → **T-151** + - **`std::isfinite`** (5%) routes through `_fpclassify` on MinGW/Windows. + Replacing with `x != NEG_INF` saves the function-call overhead. → **T-152** + - `memcmp` in ucrtbase.dll (1.6 s / 7% of total) is the `StateKeyT::operator==` + fall-through when `cached_hash` and `cached_sum` both match — unavoidable + with the current key design. + + **Estimated combined T-151 + T-152 saving: ~1.4 s (6%) per cold-cache run.** + +## Build and Test (Reminder) + +Always use isolated library: +```bash +R CMD build --no-build-vignettes --no-manual . && R CMD INSTALL --library=.agent-X TreeSearch_*.tar.gz && rm -f TreeSearch_*.tar.gz +Rscript -e "library(TreeSearch, lib.loc='.agent-X'); testthat::test_dir('tests/testthat', filter='ts-')" +``` + +Max 2 CPU cores. Use `nThreads = 2L` at most in benchmarks. diff --git a/.positai/expertise/red-team.md b/.positai/expertise/red-team.md new file mode 100644 index 000000000..c2c514a99 --- /dev/null +++ b/.positai/expertise/red-team.md @@ -0,0 +1,16 @@ +# Red-Team Expertise — TreeSearch (MIGRATED — stub) + +> **This file has been retired.** The red-team records moved on 2026-06-16 to the +> structure mandated by the `/red-team` skill: +> +> | Was here | Now lives in | +> |----------|--------------| +> | Focus-area rotation table + `start_tier` | `dev/red-team/focus-areas.md` | +> | Append-only round log + `last_focus:` pointer | `dev/red-team/log.md` | +> | Open findings | `dev/red-team/findings.md` (mirror of `to-do.md ### Bugs`) | +> | Bug/perf patterns, fragile areas (durable wisdom) | `dev/expertise/red-team.md` | +> +> This stub is kept only because the global `/red-team` skill (`SKILL.md`) hard-codes +> `…/TreeSearch/.positai/expertise/red-team.md` as a template-mirror path. Do **not** +> add live rotation state here — update `dev/red-team/` instead. If the skill's +> reference path is ever updated to point at `dev/red-team/`, this file can be deleted. diff --git a/.positai/expertise/shiny-app.md b/.positai/expertise/shiny-app.md new file mode 100644 index 000000000..b91be89d4 --- /dev/null +++ b/.positai/expertise/shiny-app.md @@ -0,0 +1,424 @@ +# Shiny App Expertise — TreeSearch + +## Purpose + +This document provides best practices and troubleshooting guidance for developing and maintaining the TreeSearch Shiny interactive application (`inst/Parsimony/app.R`). The app provides a user-friendly interface for phylogenetic tree search with real-time feedback, logging, and publication-ready visualization. + +## App Architecture + +### High-level Structure + +``` +app.R (3683 lines) +├── UI (lines 264-471) +│ ├── Left sidebar (3-column) +│ │ ├── Data loading (file, package datasets) +│ │ ├── Search controls (configure, start, save log) +│ │ ├── Tree loading and sampling +│ │ └── Display configuration (format, outgroup, etc.) +│ └── Main panel (9-column) +│ ├── Plot area with dynamic sizing +│ ├── Plot controls (size, export, concordance, clustering) +│ └── Tree/space visualization panels (conditional display) +│ +├── Server (lines 506-3683) +│ ├── Logging infrastructure (Write, LogCode, LogComment, etc.) +│ ├── Data loading (UpdateData, Excel/TNT/PhyDat parsers) +│ ├── Tree management (UpdateAllTrees, UpdateActiveTrees, filtering) +│ ├── Search execution (StartSearch, MaximizeParsimony dispatch) +│ ├── Display rendering (consensus, clustering, tree space visualization) +│ ├── User interactions (observeEvent handlers, reactive computations) +│ └── Export functionality (Newick, Nexus, PDF, PNG, R script logging) +│ +└── Supporting Elements + ├── Palettes (56+ color schemes for taxa) + ├── References (formatted bibliography) + ├── Helper functions (Enquote, EnC, Icon, ErrorPlot) + └── Notification system (Notification function wrapping showNotification) +``` + +### Key Reactive Values (lines 508-517) + +- `r$dataFiles`, `r$excelFiles`, `r$treeFiles` — file counters for temp caching +- `r$dataset` — loaded phyDat object +- `r$allTrees`, `r$trees` — all vs. displayed tree subset +- `r$outgroup` — selected outgroup taxa for rooting +- `r$searchWithout` — taxa to exclude from search +- `r$sortTrees` — whether to reorder edges by clade size (for display) +- `r$plotLog`, `r$cmdLogFile` — logging outputs for export + +### Data Flow + +1. **Data load** → `UpdateData()` (line 797) + - Detects file type (Excel, TNT, PhyDat) + - Caches to temp directory + - Logs code for reproducibility + - Attempts to load trees from same file + +2. **Search** → `StartSearch()` (line 1566) + - Builds or uses existing starting tree + - Dispatches to `MaximizeParsimony()` (C++ engine) + - Logs search code with all parameters + - Updates tree display + +3. **Display** → Reactive plot rendering (lines 1731+) + - User selects plot format (individual trees, consensus, clustering, tree space) + - Conditional UI elements show/hide based on selection + - Plots render via R base graphics (not ggplot2) + +## Critical Functions by Purpose + +### Data Loading + +| Function | Lines | Role | +|----------|-------|------| +| `UpdateData()` | 797 | Main dispatcher; handles file/package sources | +| Excel parsing | 830-903 | readxl-based with skip/column controls | +| TNT/PhyDat parsing | 908-949 | Tries multiple formats; caches successfully read files | +| `CacheInput()` | 739 | Copies file to temp for reproducibility | +| Character extraction | 961 | Reads character names/notes for display | + +### Tree Management + +| Function | Lines | Role | +|----------|-------|------| +| `UpdateAllTrees()` | 1145 | Replace all trees; renumber tips consistently | +| `UpdateActiveTrees()` | 1086 | Thin to user-selected range and count | +| `UpdateTreeRange()` | 1067 | Sync range slider with data structures | +| `UpdateNTree()` | 1026 | Update tree count; validate against range | +| `FetchNTree()`, `FetchTreeRange()` | 1012, 1053 | Debounced reactive accessors | + +### Search & Scoring + +| Function | Lines | Role | +|----------|-------|------| +| `StartSearch()` | 1566 | Build starting tree, dispatch MaximizeParsimony, log code | +| `scores()` | 1344 | Cached TreeLength() call on active trees | +| `DisplayTreeScores()` | 1369 | Update results text; show score range and weighting | +| `concavity()` | 1550 | Parse IW exponent or profile mode from input | +| `weighting()` | 1332 | Map UI "on"/"off"/"prof" to concavity values | + +### Rogue Taxon Detection + +| Function | Lines | Role | +|----------|-------|------| +| `Rogues()` | 1775 | Cached Rogue::QuickRogue() call | +| `nNonRogues()` | 1834 | Rogue count at selected p-value | +| `KeptTips()`, `DroppedTips()` | 1949, 1973 | Filter tree tips by rogue analysis | +| `UpdateKeepNTipsRange()` | 1402 | Validate user input; sync with rogue count | + +### Visualization + +| Function | Lines | Role | +|----------|-------|------| +| `PlottedTree()` | 1731 | Consensus or individual tree, rooted/sorted | +| `concordance()` | 1862 | Calculate split support (multiple measures) | +| `LabelConcordance()` | 1876 | Annotate tree with support values | +| `ConsensusPlot()` | 1982 | Render consensus with rogue drop sequence | +| `TipCols()` | 1840 | Color tips by stability (Rogue::ColByStability) | + +### Logging & Export + +| Function | Lines | Role | +|----------|-------|------| +| `BeginLog()` | 590 | Initialize search log with system info | +| `LogCode()`, `LogComment()` | 692, 704 | Append to R script log | +| `Write()` | 524 | Append to temp log file with indentation | +| `StashTrees()` | 745 | Save trees to Nexus in temp for export | + +## Best Practices + +### 1. Reactive Programming Patterns + +**Use `reactive()` for derived values, `bindCache()` for expensive calls:** +```r +# Simple derived value +weighting <- reactive(switch(input$implied.weights, "on" = Inf, ...)) + +# Cached function (re-run only if dependencies change) +scores <- bindCache(reactive({ TreeLength(r$trees, ...) }), + r$treeHash, r$dataHash, concavity()) +``` + +**Avoid:** +- Direct `input$*` reads in observers (use reactive() wrapper) +- Computing the same expensive value multiple times +- Calling `reactive()` inside `observe()`/`observeEvent()` + +### 2. File Handling + +**Always cache input files to temp directory for reproducibility:** +```r +CacheInput("data", fileName) # Copies to tempdir() + DataFileName(counter) +LogCode(paste0("dataFile <- \"", LastFile("data"), "\"")) +``` + +**Supported formats (auto-detect by extension):** +- `.xlsx` / `.xls` — Excel (readxl + configurable skip/columns) +- `.nex` — Nexus (read.nexus) +- `.tre` / `.txt` — TNT or Newick (ReadTntTree or read.tree/read.nexus) +- Any phyDat-compatible text format (ReadAsPhyDat) + +### 3. Logging Code Reproducibility + +**Every significant user action must log equivalent R code:** +```r +LogCode(c( + "newTrees <- MaximizeParsimony(", + " dataset,", + " concavity = 10,", + " maxReplicates = 100", + ")" +)) +``` + +**Use `EnC()` to quote parameters safely:** +```r +# EnC(c("a", "b")) → "c(\"a\", \"b\")" +# EnC("profile") → "\"profile\"" +# EnC(10) → "10" +``` + +**Indentation via `LogIndent()` for nested scopes:** +```r +LogIndent(2) # Indent +2 spaces +LogCode("for (tree in trees) {") +LogIndent(2) +LogCode(" tree <- Consensus(tree, p = 0.5)") +LogIndent(-2) +LogCode("}") +LogIndent(-2) +``` + +### 4. Observing User Input + +**Use debounce for high-frequency inputs (sliders, text boxes):** +```r +PlottedChar <- debounce(reactive({ as.integer(input$plottedChar) }), aJiffy) +``` + +**Use `ignoreInit = TRUE` to skip initialization:** +```r +observeEvent(input$searchConfig, { ... }, ignoreInit = TRUE) +``` + +**Cache tree hashes to detect changes (avoid spurious recalculations):** +```r +observeEvent(r$dataset, { + r$dataHash <- rlang::hash(r$dataset) +}) +r$trees <- thinnedTrees +r$treeHash <- rlang::hash(r$trees) +``` + +### 5. Conditional UI & Show/Hide Elements + +**Use bslib-style id-based show/hide (not class-based):** +```r +# Define in UI with hidden(...) wrapper +hidden(tags$div(id = "displayConfig", ...)) + +# Toggle in server +show("displayConfig", anim = TRUE) # With fade-in animation +hide("displayConfig") # Fade-out +showElement("displayConfig") # JavaScript show() without animation +hideElement("displayConfig") +``` + +**Manage multiple related configs via `ShowConfigs()`:** +```r +observeEvent(input$plotFormat, { + ShowConfigs(switch(input$plotFormat, + "ind" = c("whichTree", "charChooser", "treePlotConfig"), + "cons" = c("consConfig", "branchLegend", "savePlottedTrees"), + "clus" = c("clusConfig", "clusLegend", "savePlottedTrees"), + "" # Default: hide all + )) +}) +``` + +### 6. Modal Dialogs for Configuration + +**Example: Search configuration modal (line 1220):** +```r +observeEvent(input$searchConfig, { + # Pre-populate with current values + updateSelectInput(session, "concavity", selected = input$concavity) + + showModal(modalDialog( + fluidPage(column(6, ...), column(6, ...)), + title = "Tree search settings", + footer = tagList( + modalButton("Close", icon = Icon("rectangle-xmark")), + actionButton("modalGo", "Start search", icon = Icon("magnifying-glass")) + ), + easyClose = TRUE + )) +}) + +observeEvent(input$modalGo, { + removeModal() + StartSearch() +}) +``` + +## Common Issues & Troubleshooting + +### Issue 1: File Upload Not Working + +**Symptom:** User selects file, nothing happens. + +**Checks:** +- File size < `shiny.maxRequestSize` (default 5MB; app sets 1GB at line 4) +- File extension recognized (Excel, TNT, Nexus, text) +- `readxl` installed for Excel files (auto-install at line 831) +- Check browser console for error messages +- If TNT format: tip labels must be inferrable (will try 4 caterpillar orderings) + +### Issue 2: Search Hangs or No Results + +**Symptom:** Click "Search", progress bar shows, but never completes. + +**Checks:** +- Dataset is valid phyDat (not NULL, has tips) +- Tree space not empty or trivial (≥4 tips recommended) +- Replicates/timeout reasonable (maxReplicates ≥ 1, timeout > search time) +- Check `maxSeconds` timeout — if 0, no timeout; if very small, search aborts early +- Parallel mode (nThreads > 1) is non-deterministic; may find different trees + +**Debugging:** +```r +# In console: +ds <- ReadAsPhyDat("data.nex") +attr(ds, "nr") # Check character count +length(ds) # Check taxon count +tree <- AdditionTree(ds) # Should complete quickly +``` + +### Issue 3: Trees Don't Display / Blank Plot + +**Symptom:** Plot area is empty; no error message. + +**Checks:** +- Trees loaded? (r$trees length > 0) +- Dataset loaded? (needed for consensus/character display) +- Display format selected? (default "cons" should show something) +- Outgroup valid? (must be in tree tips) +- Rogue-dropping valid? (can't drop all tips) + +**Debugging:** +```r +# In console: +length(app_env$r$trees) # Should be > 0 +app_env$AnyTrees() # Should be TRUE +app_env$Consensus(app_env$r$trees, p=1) # Should render +``` + +### Issue 4: Logging Code Mismatch + +**Symptom:** Exported R script doesn't reproduce results. + +**Checks:** +- File paths in log correct? (should use temp files like "dataFile-00.txt") +- Parameters logged correctly? (check `Enquote()` results) +- Library calls present? (BeginLog should include all imports) +- Character encoding OK? (use system-appropriate paths) + +**Prevention:** +- Always use `LogCode()` immediately after performing an action +- Test exported script manually in a fresh R session +- Check `tempdir()` for actual cached files + +### Issue 5: Rogue Analysis Crashes or Misses Taxa + +**Symptom:** `Rogues()` returns NULL, or taxa don't appear in drop sequence. + +**Checks:** +- Dataset properly loaded (not NULL) +- Trees properly loaded (at least 1 tree, tip labels match) +- `p` parameter reasonable (0.5 to 1.0; default 1.0 = strict majority rule) +- Run `Rogue::QuickRogue()` manually to test: + ```r + rogues <- Rogue::QuickRogue(r$trees, neverDrop = input$neverDrop, + fullSeq = TRUE, p = consP()) + ``` + +### Issue 6: Memory Leak or Slowdown Over Time + +**Symptom:** App slows down after many searches; process memory grows. + +**Checks:** +- File caching in `tempdir()` consuming space? (e.g., 1000 searches → 1000s of cached files) +- Large tree objects retained? (clear old results before new search) +- Image caches building up? (plots rendered reactively, may leak if observer not cleaned up) + +**Prevention:** +- Periodically clear `tempdir()` (not auto-cleared by default) +- Use `on.exit()` to clean up temporary objects: + ```r + observeEvent(input$clearCache, { + do.call(file.remove, list(dir(tempdir(), full.names=TRUE))) + Notification("Cache cleared", type="message") + }) + ``` + +## Integration with C++ Engine + +### Key Changes from Legacy Morphy + +**Old (MorphyLib):** +```r +# Had to delegate constraints/profile to Morphy() +MaximizeParsimony(dataset, constraint = cons, concavity = "profile") +→ fell back to R-loop Morphy() search +``` + +**New (C++ engine):** +```r +# C++ engine handles everything natively +MaximizeParsimony(dataset, constraint = cons, concavity = "profile", + strategy = "auto", nThreads = 2, verbosity = 1) +``` + +### Strategy Presets (line 1231) + +- **"auto"** — Auto-selects based on dataset size (sprint ≤30, default 31-60, thorough 61+) +- **"sprint"** — 3 ratchet cycles, no drift; minimal sectorial +- **"default"** — 5 ratchet, 2 drift; XSS+RSS+CSS +- **"thorough"** — 20 ratchet, 12 drift; intensive sectorial; adaptive ratchet + +### Weighting Mode (line 1224) + +- **"on"** (Implied) — IW with concavity exponent (k = 10^exponent) +- **"off"** (Equal) — EW (all characters weight 1) +- **"prof"** (Profile) — Profile parsimony (info-theoretic weighting) + +## Testing Checklist + +Before deploying app updates: + +- [ ] Data loads: Excel (with skip/columns), TNT, Nexus, generic text +- [ ] Search runs: EW, IW, profile; small (4 tips), medium (25), large (75+) +- [ ] Logging: exported R script runs in fresh session, reproduces trees +- [ ] Display: individual, consensus, clustering, tree space all render +- [ ] Rogue analysis: correctly identifies and drops unstable taxa +- [ ] Outgroup: rooting works; must be in tree and dataset +- [ ] Export: PDF, PNG, Newick, Nexus files valid +- [ ] Performance: 50+ searches don't slow app significantly +- [ ] Parallel: nThreads=2 works; results reasonable (non-deterministic) +- [ ] Edge cases: 3-tip tree, single-character dataset, all inapplicable, empty pool + +## Performance Tips + +1. **Limit active tree display** — reduce `whichTree` max range if >100 trees +2. **Cache tree hashes** — avoid re-scoring unchanged trees +3. **Use bounded indirect** — ensure TBR/drift/SPR use `_bounded` variants +4. **Debounce slider inputs** — high-frequency slider updates (default aJiffy ≈ 42ms) +5. **Profile big plots** — use `system.time({ ... })` for consensus/space rendering + +## References + +- **app.R**: Main application file (3683 lines) +- **Related packages**: shiny, shinyjs, bslib, TreeTools, TreeSearch, Rogue, TreeDist +- **C++ search**: MaximizeParsimony() documented in `R/MaximizeParsimony.R` +- **Logging infrastructure**: BeginLog, LogCode, Write functions (lines 590-715) diff --git a/.positai/expertise/tnt.md b/.positai/expertise/tnt.md new file mode 100644 index 000000000..d8c13015a --- /dev/null +++ b/.positai/expertise/tnt.md @@ -0,0 +1,87 @@ +# TNT (Tree analysis using New Technology) + +## Installation + +TNT is installed at `C:\Programs\Phylogeny\tnt\`. + +### Executables + +| Path | Version | Notes | +|------|---------|-------| +| `tnt/tnt.exe` | older | **Do not use.** | +| `tnt/TNT-bin/tnt.exe` | 1.6 | **Use this one.** Console/script mode. | +| `tnt/TNT-bin/wTNT.exe` | 1.6 | Windows GUI version. | + +Always use `C:\Programs\Phylogeny\tnt\TNT-bin\tnt.exe` (version 1.6). + +### Invocation + +**Never launch TNT without passing a script file.** TNT defaults to +interactive mode and will block waiting for keyboard input, hanging any +automated pipeline. + +**Correct pattern** — pass a `.run` script as a positional argument with +trailing semicolon: + +```bash +"C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe" "myscript.run;" +``` + +This launches TNT in PISH (batch) mode. It reads and executes the script, +then exits when it hits `quit;`. + +**Critical: script files must use `.run` extension.** TNT interprets `.tnt` +files as data files. If you pass a `.tnt` script, TNT will try to parse it +as data and fail with "Can't open .tnt". + +**Critical: script filenames must be purely alphabetic (no digits or +underscores).** TNT parses the filename as a command line — it splits on +digits and underscores, treating the first alphabetic token as a command. +`bench1.run` → command `bench`; `Vinther2008_EW.run` → command `vinther`. +Safe names: `tntbench.run`, `mytest.run`, `abc.run`. + +**Piping via stdin does NOT work reliably** — `echo "..." | tnt.exe` launches +interactive mode (shows ASCII banner) and may hang. + +**Encoding**: TNT stdout contains non-UTF8 progress bar characters. Use +`iconv(output, from = "", to = "UTF-8", sub = "")` to sanitize before +regex matching in R. + +### TNT script basics + +- Commands are terminated by `;` +- `mxram N;` — set memory (MB); must be first command +- `proc ;` — read data file (TNT `.tnt` or Nexus format) +- `xmult;` — heuristic search (new technology search) +- `xmult=hits N replic M;` — search with convergence/replicate limits +- `piwe = K;` — implied weights with concavity constant K +- `xpiwe = K;` — extended implied weights +- `rseed N;` — set random seed +- `timeout HH:MM:SS;` — set search time limit +- `best;` — report best score and tree count +- `length;` — print tree lengths +- `quit;` — exit TNT (essential for non-interactive use) + +### Data format + +TNT can read NEXUS (`.nex`) files and its own format (`.tnt`). +For NEXUS input, use `proc ;`. + +Export from R: `TreeTools::WriteTntCharacters(phyDat_obj, filepath)`. + +### Output parsing + +TNT stdout contains parseable lines: +- `"Best score: 78."` or `"Best score: 3.80000."` (IW) — best score +- `"N trees retained"` — number of trees found +- `"Best score hit N times."` — convergence hits +- `"Total rearrangements examined: N."` — total rearrangements + +### Score comparability with TreeSearch + +TNT standard Fitch treats inapplicable tokens as a regular character state +(column-based). TreeSearch uses Brazeau et al. (2019) three-pass algorithm. +For datasets with inapplicable characters, TNT EW scores will generally be +≤ TreeSearch EW scores. For IW, both use Goloboff's `e/(k+e)` formula. + +Example: Vinther2008 — TNT EW = 78, TreeSearch EW = 79. diff --git a/.positai/settings.json b/.positai/settings.json new file mode 100644 index 000000000..bbdb16049 --- /dev/null +++ b/.positai/settings.json @@ -0,0 +1,57 @@ +{ + "model": { + "id": "claude-sonnet-4-6", + "provider": "positai", + "thinkingEffort": "high" + }, + "permission": { + "edit": { + "*.md": "allow", + "*.h": "allow", + "*.cpp": "allow", + "*.R": "allow", + "*.c": "allow", + "*/NAMESPACE": "allow" + }, + "bash": { + "cd C:/Users/pjjg18/GitHub/TreeSearch": "allow", + "Rscript -e \"pkgbuild::compile_dll()\" 2>&1": "allow", + "grep *": "allow", + "head *": "allow", + "cd \"C:/Users/pjjg18/GitHub/TreeSearch\"": "allow", + "Rscript --vanilla -e \"pkgbuild::compile_dll(debug=FALSE)\" 2>&1": "allow", + "cd /c/Users/pjjg18/GitHub/TreeSearch": "allow", + "Rscript -e \"roxygen2::roxygenise(load_code = roxygen2::load_installed)\" 2>&1": "allow", + "tail *": "allow", + "Rscript -e \".libPaths(c('.agent-A', .libPaths())); roxygen2::roxygenise(load_code = roxygen2::load_installed)\" 2>&1": "allow", + "git *": "allow" + }, + "read": { + "*.cpp": "allow" + }, + "external_directory": { + "C:/Users/pjjg18/GitHub/TreeDist/*": "allow", + "C:/Users/pjjg18/GitHub/TreeDist/R/*": "allow", + "C:/Users/pjjg18/GitHub/TreeDist/src/*": "allow", + "C:/Users/pjjg18/GitHub/TreeDist/vignettes/*": "allow", + "C:/Users/pjjg18/.positai/skills/r-package-profiling/references/*": "allow", + "C:/Users/pjjg18/GitHub/TS-MadSlat/R/*": "allow", + "C:/Users/pjjg18/GitHub/TS-MadSlat/inst/benchmarks/*": "allow", + "C:/Users/pjjg18/.positai/skills/r-package-profiling/*": "allow", + "C:/Users/pjjg18/GitHub/TS-MadSlat/src/*": "allow", + "*": "allow" + }, + "skill": { + "r-package-profiling": "allow", + "hamilton-hpc": "allow" + }, + "webfetch": { + "https://repo.r-wasm.org/*": "allow", + "https://agentskills.io/*": "allow", + "https://platform.claude.com/*": "allow", + "https://github.com/*": "allow", + "https://raw.githubusercontent.com/*": "allow", + "https://cran.r-project.org/*": "allow" + } + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..943f3c6e0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,454 @@ +# TreeSearch Multi-Agent Development Notes + +Always check the contents of `.AGENTS` for memories and policies relevant to +the task you have been assigned. + +Update memory files with anything relevant you learn. But keep them lean. + +## Current phase: bug-fixing / pre-release (as of 2026-03-29) + +The project is in a **bug-fixing and stabilisation phase** with the goal of +shipping the package. Agents should: + +- Monitor `to-do.md` as usual for task selection. +- **Prioritise bug fixes, test failures, documentation issues, and R CMD check + problems** over new functionality. +- **Do not implement new features on `cpp-search` or `main`.** + Feature work is allowed only on dedicated `feature/` branches, and + only when the task is explicitly labelled as a feature and has been approved + for active development. +- When in doubt, prefer a conservative fix (minimal diff, no API changes) over + an ambitious refactor. + +This phase ends when a clean `R CMD check` (0 errors, 0 warnings) is confirmed +and the maintainer signals readiness to tag a release. + +--- + +## Validation workflow — GHA first (mandatory) + +**Use GitHub Actions for all validation:** R CMD check, full test suites, +and benchmarks. Local builds are for **targeted iteration only** (editing +code → building → running one or two specific test files to check your +change). Never run a full test suite or R CMD check locally. + +### GHA dispatch (primary validation path) + +Before dispatching, run `spelling::spell_check_package()` (or a targetted `spell_check_files()`). +GHA will fail on spelling errors. +If any "errors" can be avoided (e.g. by spelling out acronyms or wrapping in +\acronym{}; by hyphenating compound words), reword. Add false positives +to `inst/WORDLIST`. + +Once confirmed, dispatch GHA with: + +```bash +# Push your branch and dispatch checks +git push -u origin feature/ +cd .. +bash gha-dispatch.sh agent-check.yml feature/ + +# Poll for results +bash gha-poll.sh +``` + +Park the task while waiting and pick up another (see root `AGENTS.md` for +parking protocol). Do **not** block waiting for GHA results. + +### Local builds (targeted iteration only) + +Multiple agents share the same `src/` directory. In-place `R CMD INSTALL .` +compiles `.o` files and links the DLL directly in `src/`, causing races. + +**Always build via tarball** so compilation happens in an isolated temp +directory: + +```bash +SRC=$(pwd) && TMPBUILD=$(mktemp -d) && \ + rm -f src/*.o src/*.dll && \ + (cd "$TMPBUILD" && R CMD build --no-build-vignettes --no-manual --no-resave-data "$SRC") && \ + R CMD INSTALL --library=.agent- "$TMPBUILD"/TreeSearch_*.tar.gz && \ + rm -rf "$TMPBUILD" +``` + +Key points: +- `rm -f src/*.o src/*.dll` **must** precede every build — stale artifacts slow traversal and corrupt DLLs. +- Build into an agent-specific `$TMPBUILD` outside the source tree — avoids tarball collision when multiple agents build concurrently. +- `--no-resave-data` skips unnecessary `.rda` re-saving (not needed for dev installs). + +Run **targeted** tests only: +```bash +Rscript -e "library(TreeSearch, lib.loc='.agent-'); testthat::test_dir('tests/testthat', filter='test-ts-foo')" +``` + +**Never** use `R CMD INSTALL --library=.agent- .` (in-place build). + +**Never** install to the default library. On Windows, a loaded DLL locks +the file and blocks other agents. + +**Never** use `devtools::load_all()` or `pkgbuild::compile_dll()` — these +target a shared temp location and will conflict. + +**Never** run full test suites or R CMD check locally — use GHA. + +## Build failure recovery + +### Debug `.o` contamination + +`roxygen2::roxygenise()` (default mode) calls `pkgbuild::compile_dll(debug=TRUE)`, +which leaves debug `.o` files in `src/`. Subsequent `R CMD INSTALL` reuses them, +producing a DLL that crashes at runtime (exit code 127/139). + +**Fix:** `rm -f src/*.o src/*.dll` then rebuild. + +**Prevention:** Never use bare `roxygen2::roxygenise()`. To regenerate docs: +```bash +Rscript -e ".libPaths(c('.agent-', .libPaths())); roxygen2::roxygenise(load_code = roxygen2::load_installed)" +``` + +### DLL lock + +If `R CMD INSTALL` fails with "Access is denied", another R process has the +DLL loaded. Kill it or wait, then retry. + +### `TreeSearch-init.c` arg count mismatch + +After `Rcpp::compileAttributes()`, **always** run `Rscript check_init.R` to +verify arg counts match between `RcppExports.cpp` and `TreeSearch-init.c`. + +### Quick recovery + +```bash +SRC=$(pwd) && TMPBUILD=$(mktemp -d) && \ + rm -f src/*.o src/*.dll && \ + (cd "$TMPBUILD" && R CMD build --no-build-vignettes --no-manual --no-resave-data "$SRC") && \ + R CMD INSTALL --library=.agent- "$TMPBUILD"/TreeSearch_*.tar.gz && \ + rm -rf "$TMPBUILD" +Rscript check_init.R +``` + +## CPU limits — max 2 cores per agent + +Use `nThreads = 2L` at most in tests/benchmarks. Never `nThreads = 0L` +(auto-detect). Use `-j2` at most for make. + +## Shared files — coordination rules + +`src/ts_rcpp.cpp` and `src/TreeSearch-init.c` are modified by every agent. +**Append only** — add new entries at the end. Do not reformat or reorder. + +### `src/Makevars.win` + +**Never leave a `src/Makevars.win` in place.** Debug/PGO/UBSan flags cause +crashes or miscompilation. Delete after any profiling session. + +### `src/TreeSearch-win.def` + +**Keep this file.** It explicitly exports `R_init_TreeSearch` for Windows +DLL builds. Without it, the default `nm | sed` pipeline generates a +`tmp.def` that truncates long C++ mangled symbols, causing linker failures +or corrupt DLLs (especially under `pkgbuild::compile_dll(debug=TRUE)`). + +## Branch structure + +``` +main ← stable, taggable; receives only reviewed bug fixes + └─ cpp-search ← integration branch; all feature work merges here + ├─ feature/cid-consensus + ├─ feature/hsj-polish + └─ feature/ (one per major feature) +``` + +### Rules + +- **`main`**: bug fixes and release tags only. No experiments. +- **`cpp-search`**: integration target. **Agents must not merge directly to + `cpp-search`.** All code changes go through PRs reviewed by the human. + Coordination-only commits (agent logs, to-do.md updates) may be pushed + directly. +- **`feature/*`**: branch from `cpp-search`; contain **code changes only**. + Each feature branch is owned by a single agent at a time. + +### Coordination files live on `cpp-search` only + +`to-do.md`, `u.nnn`, `completed-tasks.md`, `coordination.md`, +and `AGENTS.md` are **never committed on feature branches**. When a dispatched +agent working on a feature branch needs to claim a task or update coordination +files, they commit those changes directly to `cpp-search` (coordination-only +commit), keeping the feature branch clean. + +To read coordination files while on a feature branch without switching: +```bash +git show cpp-search:to-do.md +git show cpp-search:coordination.md +``` + +### Shared files at merge time + +`src/ts_rcpp.cpp` and `src/TreeSearch-init.c` use the existing append-only +convention — merge conflicts resolve cleanly by keeping both appended blocks. +`DESCRIPTION` (Collate field) and `NAMESPACE` require a manual merge pass; +this is expected and should be done carefully at feature-merge time. + +### Feature branch lifecycle + +1. `git checkout cpp-search && git checkout -b feature/` + Optionally create a worktree: `git worktree add ../worktrees/TS- feature/` + **Never** switch the main `./TreeSearch` checkout away from `cpp-search` (or a + feature branch actively being worked). Worktrees must always live under `../worktrees/`. +2. Claim task on `cpp-search`'s `to-do.md` (coordination commit). +3. Do all code work on `feature/`. Use local targeted tests only + during iteration; use GHA for full validation. +4. When ready: push and dispatch GHA checks: + ```bash + git push -u origin feature/ + bash gha-dispatch.sh agent-check.yml feature/ + ``` +5. On GHA success, open a PR: + ```bash + gh pr create --base cpp-search --head feature/ \ + --title "T-nnn: " --body "Dispatched agent . ..." + ``` +6. Set `to-do.md` status to `PR #N ()`. Move on. +7. Human reviews and merges the PR. +8. After merge, clean up: + ```bash + git worktree remove ../worktrees/TS- # if worktree was used + git branch -d feature/ + git push origin --delete feature/ + ``` + +--- + +## Multi-agent workflow protocol + +> **Task IDs:** New tasks use `T-nnn` format. Existing `T-nnn`, `-nnn` +> IDs in `to-do.md`, `completed-tasks.md`, PRs, and git log are valid and need +> not be renamed. +> Before adding or removing rows in `to-do.md`, acquire the lock: +> `bash ../../todo-lock.sh . acquire` / `bash ../../todo-lock.sh . release`. + +### Dispatcher model + +Agents are launched by the dispatcher (`dispatch.sh`) and receive an ephemeral +ID of the form `d1`, `d2`, etc. The dispatcher: + +1. Reads `.dispatch/state.json` to determine which tasks are already in-flight. +2. Selects a task (via the Haiku ranker or an explicit task ID). +3. Mints a new agent ID and updates `to-do.md` to `ASSIGNED (d1)`. +4. Spawns a `claude -p` subprocess whose brief is loaded from + `dev/dispatch/agent-brief.md`. +5. Logs output to `.dispatch/logs/-.log`. + +**Agents do not edit `.dispatch/state.json` directly.** State is written only +by `dispatch.sh checkin`. + +#### Starting a session + +The user (or a parent dispatched session) calls: + +```bash +bash dispatch.sh allocate # e.g. 5%/5h, 2%/wk, 15m +bash dispatch.sh task [budget] # explicit task; budget optional +``` + +#### Session start protocol (every dispatched agent, before claiming work) + +1. **Resume check:** read your brief — if a resume action is recorded, execute + it now. +2. **Triage user reports** (`a.*` and `u.*` files) — see "User report intake" + below for the full claim protocol: + a. List all `a.[0-9]*` **and** `u.[0-9]*` files in the project root + (excluding any `*.claimed-*` files). + b. For each file, check its size first. **Skip files shorter than + 20 characters** (likely mid-edit — the human may still be typing). + Do not rename or touch these files; leave them for a later pass. + c. For files ≥20 characters, claim atomically: + `mv a.010 a.010.claimed-` (or `mv u.010 u.010.claimed-`). + If the rename fails, another agent claimed it — skip. + d. Create a `to-do.md` entry. **`a.*` files** → `### Shiny App`, tag + `[Shiny]`. **`u.*` files** → section matching content (search bug, + docs issue, etc.). Default priority P2; crash = P1, cosmetic = P3. + e. Delete the `.claimed-` file once the `to-do.md` entry is written. + f. Repeat for all files before moving on. **Do not start working a + task until all pending reports are triaged.** (An issue may be P0.) +3. **Check `remote-jobs.md`** for retrievable results. If a job is listed + as complete (or past its expected duration), retrieve and process the + results before claiming a new task. +4. If no untriaged issues or pending remote results, proceed with the + assigned task. + +> **Concurrency guard:** Atomic rename (`mv a.010 a.010.claimed-` or +> `mv u.001 u.001.claimed-`) ensures exactly one agent wins each file. +> NTFS rename is atomic; losers see "file not found" and skip. + +### Worktree tasks + +Tasks with status `WORKTREE (name)` are actively developed in a dedicated git +worktree under `C:/Users/pjjg18/GitHub/worktrees/` (e.g. +`../worktrees/TS-CID-cons`). **Do not claim or modify these tasks.** They are +reserved for the human developer working in that worktree. To mark a task as +in-flight on a worktree, set its status to `WORKTREE (name)` where *name* +matches the worktree directory basename. + +> **Worktree rule:** Worktrees must **always** be created under `../worktrees/` +> (i.e. `C:/Users/pjjg18/GitHub/worktrees/`). **Never** create a worktree +> directly inside `../` alongside the main checkout, and **never** switch the +> main `C:/Users/pjjg18/GitHub/TreeSearch` directory to a different branch using +> `git checkout` — it must remain on `cpp-search` (or the current feature branch +> being actively developed). Use a worktree instead. + +### During work + +- All work uses `.agent-/` as library directory (e.g. `.agent-d1/`). +- **All builds, tests, and benchmarks in bash subprocesses** — never in the + RStudio R session. +- **Use GHA for validation** (full test suites, R CMD check, benchmarks). + Local builds are for targeted iteration only (build + run 1–2 test files). + See "Validation workflow" section above. + +### On task completion + +1. **Delete** the task row from `to-do.md`. If the task was the last open + row in a section/group, delete the section header too. +2. **`completed-tasks.md` is decision-only — not an archive.** For a routine + fix, the commit/PR *is* the record; do **not** add a row. Add a row **only** + when the task closes without a routine fix — a **not-a-bug determination, a + superseded/ruled-out design, or a negative experimental result** whose + reasoning a future agent would otherwise re-investigate. When you do, append + one row to the matching section with the terminal decision + a pointer to the + write-up (e.g. `dev/benchmarks/*.md`). Keep it to a line or two; the detail + lives in the linked file, not the row. +3. Update `coordination.md` if strategic objectives are affected. +4. Run `bash dispatch.sh checkin --done`. + +### Parking (waiting for GHA / Hamilton / human review) + +When the dispatched agent must stop and wait for an external event: + +```bash +bash dispatch.sh checkin \ + --kind= \ + --ref= \ + --eta= \ + --resume="" +``` + +Then exit cleanly. The dispatcher's `reap` subcommand surfaces parked agents +once their ETA has passed. `to-do.md` status flips to `PARKED (, )`. + +### User report intake (`a.*` / `u.*`) + +The human files reports as individual files in the project root. +Each file contains a free-text description. The human's workflow is: +create file → write → save → never touch again. + +**Naming convention:** +- `a.###` — app (Shiny) bug. Always routes to `### Shiny App` in `to-do.md`. +- `u.###` — general user issue (search quality, docs, API, etc.). Route by content. + +**Agent responsibility:** Triage all pending files into `to-do.md` at +the start of every dispatched session (step 2 in Session start protocol above). + +**Claim protocol:** +```bash +# List unclaimed reports +ls a.[0-9]* u.[0-9]* 2>/dev/null | grep -v 'claimed' + +# Skip short files (< 20 chars) — don't rename, don't touch +wc -c < a.010 # check size first + +# Claim atomically (rename) +mv a.010 a.010.claimed-d1 + +# Read, triage into to-do.md, then delete +cat a.010.claimed-d1 +# ... create to-do.md entry ... +rm a.010.claimed-d1 +``` + +**Skip guard:** Files shorter than 20 characters are likely mid-edit. +Do **not** rename them — leave in place for a later pass. +(Renaming and renaming back triggers RStudio "file moved" dialogs.) + +**Shiny (`a.*`) fixes** are committed directly to `cpp-search` (bug +fixes in `inst/Parsimony/`, no feature branch needed). Use a temporary +worktree if changes span multiple files. + +### Standing tasks + +| ID | Type | Expertise file | +|----|------|---------------| +| S-RED | Red-team review | `dev/expertise/red-team.md` | +| S-PROF | Performance profiling | `dev/expertise/profiling.md` | +| S-COORD | Coordination review | `dev/expertise/coordination.md` | + +Priority: P3 when ≥6 OPEN tasks, P2 when 3–5, P1 when <3. + +### Key files + +| File | Purpose | +|------|---------| +| `a.###` | App (Shiny) bug reports → triage to `### Shiny App`, then delete | +| `u.###` | General user issue reports → triage to matching section, then delete | +| `to-do.md` | Task queue (active/open tasks only) | +| `remote-jobs.md` | Pending async jobs (Hamilton SLURM, long GHA) — check at session start | +| `completed-tasks.md` | Decision-only log: not-a-bug / superseded / negative-result closures. `grep` before reopening a closed task; don't archive routine fixes here | +| `coordination.md` | Strategic plan | +| `AGENTS.md` | Conventions + workflow reference | +| `.dispatch/state.json` | Live dispatcher state (active agents, check-ins, budget tally) | +| `dev/expertise/*.md` | Standing-task methodology references | +| `dev/dispatch/ranker.txt` | Haiku ranker prompt template used by `dispatch.sh` | +| `dev/dispatch/agent-brief.md` | Spawned-agent system prompt template used by `dispatch.sh` | + +--- + +## Mandatory checks + +Run these before committing whenever the trigger applies: + +| Trigger | Command | +|---------|---------| +| Function signature or roxygen block changed | `Rscript -e "devtools::check_man()"` | +| Documentation prose changed | `Rscript -e "spelling::spell_check_package()"` | +| `Rcpp::compileAttributes()` run | `Rscript check_init.R` (verifies `ts_rcpp.cpp` / `TreeSearch-init.c` arg counts) | +| Search behaviour changed (heuristics, scoring, stopping, pool) | Update `vignettes/search-algorithm.Rmd` | + +Full details: `.AGENTS/memory/r-package-conventions.md`. + +--- + +## MorphyLib deprecation status + +Migration plan in `inst/deprecation/morphy-migration.md`. + +**Already migrated to C++:** `MaximizeParsimony`, `AdditionTree`, `Resample`, +`SuccessiveApproximations`, `TreeLength`, `CharacterLength`, +`FastCharacterLength`, `RandomTreeScore`, `TaxonInfluence`. + +**Still using MorphyLib:** Legacy search functions (`Ratchet`, `Jackknife`, +`MorphyBootstrap`, `CustomSearch`), R-level tree rearrangement functions. +These are candidates for deprecation rather than migration. + +## Version and CRAN status + +- **Version**: 2.0.0 (major bump for new `MaximizeParsimony()` API) +- **R CMD check**: 0 ERRORs, 0 WARNINGs, 1 NOTE (R 4.5.2 internal bug) +- **Test suite**: ~9200 R-level + 1859 ts-* + 128 ParsSim + 37 MaddisonSlatkin + 49 recode-hierarchy pass + +--- + +## Technical reference + +Load the relevant `.AGENTS/memory/` file before starting work in that area: + +| Memory file | Load when... | +|-------------|--------------| +| `architecture.md` | Editing `src/ts_*.cpp`/`.h`, adding Rcpp exports, reviewing R-level API or key design decisions | +| `benchmarking.md` | Running benchmarks, doing VTune profiling, interpreting phase-distribution or Brazeau/Fitch results | +| `feature-inapplicable.md` | Working on HSJ, x-transform/Sankoff, `inapplicable=` parameter, or `CharacterHierarchy` | +| `r-package-conventions.md` | Adding `.R` files to `Collate:`, writing roxygen docs, updating vignettes | +| `search-algorithms.md` | Researching NNI warmup, biased Wagner, outer cycles, large-tree behaviour, or the search optimization history | +| `search_strategy.md` | Understanding the driven pipeline, strategy presets, adaptive search, collapsed-flag optimization | +| `shiny_app.md` | Working on `inst/Parsimony/`, Shiny modules, or app tests | +| `testing.md` | Adding or modifying `tests/testthat/test-ts-*.R`, choosing test tiers, writing helpers | diff --git a/DESCRIPTION b/DESCRIPTION index 710cfafb8..c532adbd1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: TreeSearch Title: Phylogenetic Analysis with Discrete Character Data -Version: 1.8.0.9001 +Version: 2.0.0 Authors@R: c( person( "Martin R.", 'Smith', @@ -18,15 +18,17 @@ License: GPL (>= 3) Copyright: Incorporates C/C++ code from Morphy Phylogenetic Library by Martin Brazeau (GPL3) -Description: Reconstruct phylogenetic trees from discrete data. +Description: Reconstruct phylogenetic trees from discrete data using a + high-performance C++ search engine with multi-replicate driven search. + Supports equal weights, implied weights (Goloboff, 1993) + and profile parsimony + (Faith and Trueman, 2001) . Inapplicable character states are handled using the algorithm of Brazeau, - Guillerme and Smith (2019) with the "Morphy" - library, under equal or implied step weights. + Guillerme and Smith (2019) . Contains a "shiny" user interface for interactive tree search and exploration of results, including character visualization, rogue taxon detection, tree space mapping, and cluster consensus trees (Smith 2022a, b) , . - Profile Parsimony (Faith and Trueman, 2001) , Successive Approximations (Farris, 1969) and custom optimality criteria are implemented. URL: https://ms609.github.io/TreeSearch/ (doc), @@ -36,49 +38,103 @@ Depends: R (>= 4.0) Imports: abind, ape (>= 5.6), - base64enc, cli (>= 3.0), - cluster, colorspace, - fastmap, fastmatch (>= 1.1.3), - fs, - future, - PlotTools, - promises, - protoclust, + graphics, + grDevices, Rcpp, Rdpack (>= 0.7), - Rogue (> 2.0.0), - shiny (>= 1.6.0), - shinyjs, stats, - stringi, TreeDist (>= 2.6.3), - TreeTools (>= 1.16), + TreeTools (>= 2.3.0.9002), + utils, Suggests: + cluster, + future, + highs, knitr, + MaxMin, phangorn (>= 2.2.1), + PlotTools, + promises, + protoclust, Quartet, readxl, rmarkdown, - shinytest, + Rogue (> 2.0.0), + shiny (>= 1.8.1), + shinyjs, + shinytest2, spelling, - testthat, + testthat (>= 3.0.0), vdiffr (>= 1.0.0), + zip, +Remotes: + ms609/MaxMin, Config/Needs/check: callr, pkgbuild, rcmdcheck, Config/Needs/coverage: covr, spelling -Config/Needs/memcheck: devtools +Config/Needs/memcheck: devtools, pkgdown, testthat Config/Needs/metadata: codemeta Config/Needs/revdeps: revdepcheck Config/Needs/website: curl, igraph, pkgdown, + remotes, + shinylive, Config/roxygen2/version: 8.0.0 +Config/testthat/edition: 3 +Collate: + 'AdditionTree.R' + 'Bootstrap.R' + 'CharacterHierarchy.R' + 'ClusterStrings.R' + 'Concordance.R' + 'Consistency.R' + 'CustomSearch.R' + 'IWScore.R' + 'ImposeConstraint.R' + 'Jackknife.R' + 'LeastSquares.R' + 'SearchControl.R' + 'MaximizeParsimony.R' + 'Morphy.R' + 'NNI.R' + 'PaintCharacters.R' + 'ParsSim.R' + 'PlotCharacter.R' + 'PolEscapa.R' + 'PresentContra.R' + 'QuartetResolution.R' + 'RandomTreeScore.R' + 'Ratchet.R' + 'RcppExports.R' + 'ts-driven-compat.R' + 'ReleaseQuestions.R' + 'recode_hierarchy.R' + 'SPR.R' + 'ScoreSpectrum.R' + 'Sectorial.R' + 'SuccessiveApproximations.R' + 'TBR.R' + 'TaxonInfluence.R' + 'TreeSearch_utilities.R' + 'WhenFirstHit.R' + 'WideSample.R' + 'data.R' + 'data_manipulation.R' + 'fractional-weights.R' + 'length_range.R' + 'mpl_morphy_objects.R' + 'mpl_morphyex.R' + 'pp_info_extra_step.r' + 'tree_length.R' + 'tree_rearrangement.R' + 'zzz.R' RdMacros: Rdpack LinkingTo: Rcpp, diff --git a/NAMESPACE b/NAMESPACE index e542e3959..c9333d938 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -18,12 +18,16 @@ S3method(TreeLength,list) S3method(TreeLength,multiPhylo) S3method(TreeLength,numeric) S3method(TreeLength,phylo) +S3method(print,CharacterHierarchy) +S3method(print,ScoreSpectrum) +S3method(print,SearchControl) S3method(summary,morphyPtr) export(.NonDuplicateRoot) export(.UniqueExceptHits) export(AdditionTree) export(C_MorphyLength) export(Carter1) +export(CharacterHierarchy) export(CharacterLength) export(ClusterStrings) export(ClusteringConcordance) @@ -42,16 +46,25 @@ export(Fitch) export(FitchSteps) export(GapHandler) export(GetMorphyLength) +export(HierarchyChars) +export(HierarchyControlling) +export(HierarchyFromNames) export(IWScore) export(IWTreeSearch) export(JackLabels) export(Jackknife) +export(LeastSquaresFit) +export(LeastSquaresTree) export(LengthAdded) export(Log2Carter1) export(LogCarter1) +export(MaddisonSlatkin) +export(MaddisonSlatkin_clear_cache) export(MaximizeParsimony) +export(MaximizeParsimony2) export(MaximumLength) export(MinimumLength) +export(Morphy) export(MorphyBootstrap) export(MorphyErrorCheck) export(MorphyLength) @@ -62,6 +75,8 @@ export(MultiRatchet) export(MutualClusteringConcordance) export(NNI) export(NNISwap) +export(PaintCharacters) +export(ParsSim) export(PhyDat2Morphy) export(PhylogeneticConcordance) export(PlotCharacter) @@ -79,6 +94,7 @@ export(RandomTreeScore) export(Ratchet) export(RatchetConsensus) export(RearrangeEdges) +export(RecodeHierarchy) export(Resample) export(RootedNNI) export(RootedNNISwap) @@ -90,6 +106,8 @@ export(SPR) export(SPRMoves) export(SPRSwap) export(SPRWarning) +export(ScoreSpectrum) +export(SearchControl) export(SetMorphyWeights) export(SharedPhylogeneticConcordance) export(SingleCharMorphy) @@ -106,11 +124,14 @@ export(TaxonInfluence) export(TreeLength) export(TreeSearch) export(UnloadMorphy) +export(ValidateHierarchy) export(WhenFirstHit) +export(WideSample) export(WithOneExtraStep) export(cNNI) export(cSPR) export(is.morphyPtr) +export(mc_fitch_scores) export(mpl_apply_tipdata) export(mpl_attach_rawdata) export(mpl_attach_symbols) @@ -134,10 +155,8 @@ export(mpl_set_parsim_t) export(mpl_translate_error) export(mpl_update_lower_root) export(mpl_update_tip) -importFrom(PlotTools,SpectrumLegend) importFrom(Rcpp,compileAttributes) importFrom(Rdpack,reprompt) -importFrom(Rogue,ColByStability) importFrom(TreeDist,ClusteringEntropy) importFrom(TreeDist,ClusteringInfo) importFrom(TreeDist,ClusteringInfoDistance) @@ -145,7 +164,6 @@ importFrom(TreeDist,Entropy) importFrom(TreeDist,MutualClusteringInfo) importFrom(TreeDist,SharedPhylogeneticInfo) importFrom(TreeDist,entropy_int) -importFrom(TreeTools,AddTipEverywhere) importFrom(TreeTools,AddUnconstrained) importFrom(TreeTools,CharacterInformation) importFrom(TreeTools,CladeSizes) @@ -160,6 +178,7 @@ importFrom(TreeTools,DropTip) importFrom(TreeTools,EdgeAncestry) importFrom(TreeTools,ImposeConstraint) importFrom(TreeTools,KeepTip) +importFrom(TreeTools,LnUnrooted) importFrom(TreeTools,Log2DoubleFactorial) importFrom(TreeTools,Log2Unrooted) importFrom(TreeTools,Log2UnrootedMult) @@ -173,6 +192,7 @@ importFrom(TreeTools,NTip) importFrom(TreeTools,NUnrooted) importFrom(TreeTools,NUnrootedMult) importFrom(TreeTools,NexusTokens) +importFrom(TreeTools,PaintTree) importFrom(TreeTools,PectinateTree) importFrom(TreeTools,PhyDatToMatrix) importFrom(TreeTools,PhyToString) @@ -185,6 +205,7 @@ importFrom(TreeTools,Renumber) importFrom(TreeTools,RenumberEdges) importFrom(TreeTools,RenumberTips) importFrom(TreeTools,RenumberTree) +importFrom(TreeTools,RootNode) importFrom(TreeTools,RootTree) importFrom(TreeTools,SampleOne) importFrom(TreeTools,SplitConflicts) @@ -204,7 +225,6 @@ importFrom(ape,plot.phylo) importFrom(ape,read.nexus) importFrom(ape,root) importFrom(ape,write.nexus) -importFrom(base64enc,base64encode) importFrom(cli,cli_alert) importFrom(cli,cli_alert_danger) importFrom(cli,cli_alert_info) @@ -214,31 +234,28 @@ importFrom(cli,cli_h1) importFrom(cli,cli_progress_bar) importFrom(cli,cli_progress_done) importFrom(cli,cli_progress_update) -importFrom(cluster,pam) -importFrom(cluster,silhouette) importFrom(colorspace,hex) importFrom(colorspace,max_chroma) importFrom(colorspace,polarLUV) -importFrom(fastmap,fastmap) importFrom(fastmatch,"%fin%") importFrom(fastmatch,fmatch) -importFrom(fs,path_sanitize) -importFrom(future,future) +importFrom(grDevices,col2rgb) +importFrom(grDevices,convertColor) +importFrom(grDevices,rgb) importFrom(graphics,abline) importFrom(graphics,image) importFrom(graphics,mtext) importFrom(graphics,par) -importFrom(promises,future_promise) -importFrom(protoclust,protoclust) -importFrom(shiny,runApp) -importFrom(shinyjs,useShinyjs) importFrom(stats,as.dist) +importFrom(stats,cophenetic) importFrom(stats,cutree) +importFrom(stats,dnorm) importFrom(stats,median) importFrom(stats,runif) +importFrom(stats,sd) importFrom(stats,setNames) importFrom(stats,weighted.mean) -importFrom(stringi,stri_paste) importFrom(utils,adist) importFrom(utils,combn) +importFrom(utils,head) useDynLib(TreeSearch, .registration = TRUE) diff --git a/NEWS.md b/NEWS.md index 73720b1a6..71966135b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,331 @@ +# To integrate into 2.0.0 notes + +- New opt-in `MaximizeParsimony(strategy = "intensive")` preset: `"thorough"` plus + extra Wagner starts for more starting-basin diversity. On difficult datasets it + finds shorter trees (a few steps) at neutral-to-lower search cost; never selected + by `strategy = "auto"`, so choose it explicitly. + +- `MaximizeParsimony()` results now carry a `candidates_evaluated` attribute: + the number of TBR/SPR-class rearrangements examined during a single-threaded + search (the analogue of TNT's "rearrangements examined"), for diagnosing + search efficiency. + +- New `SearchControl()` option `stallEscalateFactor` (default `1`, disabled): + when a driven search stalls, escalate ratchet perturbation strength for + subsequent replicates so the search adapts to a difficult dataset at runtime. + +- Faster driven search: per-clip allocation churn in the TBR kernel removed + (reusable scratch buffers and an open-addressed rerooting-dedup table), and + the debug-only topology validation no longer runs in release builds. + +- Further driven-search speedup: the exact directional insertion edge-set + computation now reuses caller-owned scratch and skips its per-clip zero-fill + (under a write-before-read invariant, debug-asserted), saving up to ~16% wall + on large datasets where the `O(n_node * words)` zero-fill dominated. Search + results are bit-identical (score and `candidates_evaluated` unchanged). + +- HSJ (Hopkins & St John 2021) scoring is now invariant to the + arbitrary ordering of a `phyDat` object's `levels`. Both the primary + absent/present term and the secondary-character dissimilarity term + previously depended on the internal token ordering, so the same dataset + could score differently under different (equivalent) `levels`. + +- Hierarchical-scoring helpers renamed to the package's `BigCamelCase` + convention (the snake_case names are removed): `recode_hierarchy()` -> + `RecodeHierarchy()`, `hierarchy_from_names()` -> `HierarchyFromNames()`, + `validate_hierarchy()` -> `ValidateHierarchy()`, `hierarchy_chars()` -> + `HierarchyChars()`, `hierarchy_controlling()` -> `HierarchyControlling()`. + The internal C++-bridge helpers (`build_tip_labels()`, + `hierarchy_to_blocks()`, `non_hierarchy_weights()`, `hsj_absent_state()`) + are now private and no longer exported. + +- `WideSample()` now dispatches to the appropriate Max-Min diversity (MMDP) + solver from the `MaxMin` package, choosing the tier automatically + from `length(trees)`. + +- New functions `LeastSquaresTree()` and `LeastSquaresFit()` search for, and + fit branch lengths to, the tree that best matches a target distance matrix + under a least-squares criterion, reusing the optimised C++ rearrangement + kernel (NNI + SPR). Ordinary (`method = "ols"`) and non-negative + (`method = "nnls"`) least squares are supported, with optional + Fitch-Margoliash (`weight = "fm"`) or custom weighting. + `LeastSquaresFit()` mirrors `phangorn::nnls.tree()` but runs in the native + kernel. + + +- `attr(dataset, "weight")` now accepts non-integer character weights. The + C++ scoring engine still stores `int` weights internally; fractional + inputs are rescaled to integer with a configurable precision (default + 0.001, controlled by `getOption("TreeSearch.fractional.scale", 1000L)`). + Previously, fractional weights were silently truncated at the Rcpp + boundary (e.g. `c(0.5, 1.7)` became `c(0L, 1L)`, dropping 50% / 41% of + the respective characters' contributions). Integer weights pass + through unchanged. `TreeLength()` and other scores are returned in + units of `steps * scale` when fractional weights are present; within- + run ranking is unaffected. + +- `LengthAdded()` removes a temporary warning guard that fired on datasets + triggering the T-302 `qmApp` scalar-unwrap fix; regression tests now cover + both the `qmApp` (T-302) and `qm` (commit e8b318c3) scalar-unwrap paths, + confirming all deltas are non-negative and match independent computation. + +# TreeSearch 2.0.0 + +## Breaking changes + +- Implied weighting now applies the missing-entries correction of + Goloboff (2014) by default (`extended_iw = TRUE`). Characters with + many missing entries receive a reduced effective concavity, compensating + for artificially low observed homoplasy. Set `extended_iw = FALSE` to + reproduce pre-2.0.0 behaviour. +- `MaximizeParsimony()` has an entirely new parameter interface. + The previous `MaximizeParsimony()` (R-loop search using MorphyLib) has been + renamed to `Morphy()`. + Code that passes Morphy-style parameters (e.g. `ratchIter`, `tbrIter`, + `maxHits`) to `MaximizeParsimony()` will be automatically forwarded to + `Morphy()` with a deprecation warning. + Update your code to call `Morphy()` directly, or adopt the new + `MaximizeParsimony()` parameters. + This compatibility shim will be removed in a future release. + +## C++ search engine + +`MaximizeParsimony()` is rewritten from the ground up with a native C++ search +engine, replacing the R-loop/MorphyLib backend for equal weights, implied +weights, and profile parsimony. Typical searches are an order of magnitude +faster; inapplicable character handling (Brazeau _et al._ 2019) is built in. + +### New features + +- `PaintCharacters()` colours each character in a morphological dataset by its + most concordant tree edges. + +- `ScoreSpectrum()`: Chao1-style landscape coverage estimator. Treats + distinct parsimony scores found across replicates as "species" and estimates + how thoroughly the parsimony landscape has been sampled (Good-Turing sample + coverage, Chao1 richness lower bound, unseen score-level fraction). The + Shiny app's confidence panel now displays the coverage estimate when + sufficient replicates have been completed. `MaximizeParsimony()` now + returns a `replicate_scores` attribute containing per-replicate local-optimum + scores for this purpose. + +- **Multi-replicate driven search** pipeline: random Wagner tree → TBR → + sectorial search (XSS, RSS, CSS) → ratchet → drift → tree fusing → + final TBR. +- **Parallel search** via `nThreads`: replicates run on independent threads + with a shared tree pool. +- **Timeout** via `maxSeconds`. +- **User-supplied starting tree**: when a `tree` argument is provided, the + first replicate begins from that topology; subsequent replicates use + random Wagner trees. +- **Adaptive strategy presets** via `strategy`: `"auto"` (default) selects + `"sprint"`, `"default"`, or `"thorough"` based on the number of tips. + Explicit parameters always override preset values. +- **Profile parsimony** runs natively in C++; no longer delegates to + `Morphy()`. +- **Topological constraints** enforced natively in C++ (including during + Wagner tree construction and sectorial search). +- **Per-phase timing** returned as a `timings` attribute on the result. +- **MPT enumeration**: after the main search converges, a TBR plateau walk + from each pool tree discovers additional most-parsimonious topologies on the + same and neighbouring score plateaus, up to `poolMaxSize`. + This addresses a common complaint that the previous implementation returned + only one tree when multiple MPTs exist. +- `LeastSquaresTree()` and `LeastSquaresFit()` search for, and + fit branch lengths to, the tree that best matches a target distance matrix + under a least-squares criterion, reusing the optimised C++ rearrangement + kernel (NNI + SPR). Ordinary (`method = "ols"`) and non-negative + (`method = "nnls"`) least squares are supported, with optional + Fitch-Margoliash (`weight = "fm"`) or custom weighting. This provides the + topology-search step of Lapointe & Cucumel's (1997) average consensus + procedure; `LeastSquaresFit()` mirrors `phangorn::nnls.tree()` but runs in + the native kernel. +- `PaintCharacters()` colours each character in a morphological + dataset by the hue of the tree edges it most concordantly supports, using + `ConcordanceTable()` MI weights averaged in CIELAB colour space. Pairs with + `TreeTools::PaintTree()` to visually map characters to clades. +- `attr(dataset, "weight")` now accepts non-integer character weights. The + C++ scoring engine still stores `int` weights internally; fractional + inputs are rescaled to integer with a configurable precision (default + 0.001, controlled by `getOption("TreeSearch.fractional.scale", 1000L)`). + Previously, fractional weights were silently truncated at the Rcpp + boundary (e.g. `c(0.5, 1.7)` became `c(0L, 1L)`, dropping 50% / 41% of + the respective characters' contributions). Integer weights pass + through unchanged. `TreeLength()` and other scores are returned in + units of `steps * scale` when fractional weights are present; within- + run ranking is unaffected. + +### New parameters for `MaximizeParsimony()` + +- `strategy` — `"auto"` (default), `"sprint"`, `"default"`, `"thorough"`, + or `"none"`. +- `nThreads` — number of parallel worker threads (default 1). +- `maxSeconds` — wall-clock timeout (0 = no limit). +- `sprFirst` — run SPR before TBR in each replicate. +- `ratchetPerturbMode`, `ratchetPerturbMaxMoves`, `ratchetAdaptive` — + configure ratchet perturbation (zero-weight, up-weight, mixed, adaptive). +- `driftCycles`, `driftAfdLimit`, `driftRfdLimit` — drift search parameters. +- `xssRounds`, `xssPartitions`, `rssRounds`, `cssRounds`, `cssPartitions`, + `sectorMinSize`, `sectorMaxSize` — sectorial search parameters. +- `fuseInterval`, `fuseAcceptEqual` — tree fusing parameters. +- `poolMaxSize`, `poolSuboptimal` — tree pool management. +- `tbrMaxHits`, `wagnerStarts`, `tabuSize`. +- `nniFirst` — NNI warmup pass before SPR/TBR in each replicate; at + ≥100 tips this substantially improves the Wagner starting-tree quality + at negligible cost for small datasets. +- `postRatchetSectorial` — run a second XSS+RSS+CSS pass after ratchet + perturbation; approximates TNT's interleaved sectorial pattern. + Enabled by default in the `"thorough"` preset. +- `outerCycles`, `maxOuterResets` — repeat the full + \[XSS/RSS/CSS → ratchet → NNI-perturbation → drift → TBR\] sequence + _n_ times per replicate; budget is divided evenly. Enabled in the + `"thorough"` preset (`outerCycles = 2`). +- `wagnerBias`, `wagnerBiasTemp` — bias taxon addition order during Wagner + tree construction toward taxa with more informative characters + (Goloboff 2014), substantially improving starting-tree quality at large + tip counts. +- `perturbStopFactor` — stop after `nTip × perturbStopFactor` consecutive + replicates that fail to improve the best score; provides 2–7× speedup on + converged searches at no score cost. +- `pruneReinsertCycles`, `pruneReinsertDrop`, `pruneReinsertSelection` — + taxon pruning-reinsertion perturbation: drop a fraction of leaves, let + the backbone re-optimise with TBR, then reinsert taxa greedily. + Complementary to the ratchet (which perturbs character weights). +- `nniPerturbCycles`, `nniPerturbFraction` — stochastic NNI-perturbation: + randomly apply NNI swaps to a fraction of internal branches and + reconverge, escaping local optima without altering character weights. +- `annealCycles`, `annealPhases`, `annealTStart`, `annealTEnd`, + `annealMovesPerPhase` — multi-cycle PCSA (simulated annealing + perturbation) phase. +- `adaptiveLevel` — dynamically scale ratchet and drift effort per + replicate based on the observed hit rate. +- `adaptiveStart` — Thompson-sampling bandit strategy for starting-tree + selection; adapts over replicates to which strategies yield best scores. +- `enumTimeFraction` — fraction of `maxSeconds` reserved for the MPT + plateau enumeration walk at the end of the search (default 10%). +- `intraFuse` — within-replicate tree fusing against pool donors after TBR + polish; approximates TNT's within-replicate fusing pattern. +- `ratchetTaper` — gradually reduce ratchet perturbation probability as + the pool stabilises, allowing finer local exploration late in the search. +- `consensusConstrain` — lock pool-consensus splits as topological + constraints for subsequent replicates. +- `consensusStableReps` — stop when the strict consensus is unchanged for + this many consecutive replicates (0 = disabled; set e.g. 3 to enable). +- `progressCallback` — R function called after each replicate (for custom + progress reporting). + +### Search output + +- **Convergence summary**: when `verbosity > 0` (the default), + `MaximizeParsimony()` now prints a one-line summary on exit reporting the + best score, number of replicates completed, replicates since last + improvement, number of distinct MPTs found, stop reason (time limit, + target hits, perturbation-stop, or user interrupt), and elapsed time. + The same information is available as named attributes on the returned + tree list. + +### Search optimizations + +- **Collapsed-edge clip skipping**: TBR, SPR, and drift search skip + clips at zero-length edges that provably cannot improve the score, + reducing unnecessary evaluations on sparse data. +- **Conflict-guided sectorial search**: random sectorial search targets + sectors around splits that conflict across pool trees. +- **Diversity-aware pool eviction**: when the tree pool is full, the most + topologically similar entry is evicted to maintain diversity. +- **Cross-replicate consensus constraint tightening**: opt-in via + `consensusConstrain = TRUE` in `SearchControl()`. +- **Consensus-stability early stopping**: when `consensusStableReps > 0` in + `SearchControl()`, search stops when the strict consensus of best-score + pool trees has been unchanged for that many consecutive replicates. + Disabled by default. + +### Batch resampling + +- `Resample()` gains `nReplicates` and `nThreads` parameters for batch and + parallel jackknife/bootstrap resampling via a single C++ call. +- `SuccessiveApproximations()` gains `concavity` and `constraint` parameters. + +## Profile parsimony: multi-state support + +- Profile parsimony now supports characters with up to 5 informative states + (previously limited to 2). Characters with 3--5 states use the recursive + algorithm of Maddison & Slatkin (1991). +- New C++ function `MaddisonSlatkin()` computes the number of labelled + histories for multi-state characters. + +## Data simulation + +- New function `ParsSim()` simulates morphological datasets under a parsimony + model (equal weights, implied weights, or profile parsimony). Each + character starts at minimum steps; extra steps are placed one at a time, + verified to increase the Fitch score by exactly 1. + +## Scoring + +- `TreeLength()` and `CharacterLength()` / `FastCharacterLength()` use the + C++ engine for all scoring modes (equal weights, implied weights, profile + parsimony). + +## Function rename + +- `TaxonInfluence()` now uses `MaximizeParsimony()` internally. +- `AdditionTree()` now uses the C++ Wagner tree engine, with native support + for implied weights, profile parsimony, and constraints. + +## Bug fixes + +- `LengthAdded()` no longer errors on datasets whose contrast matrix contains + zero-sum rows for tokens that are declared in the SYMBOLS list but not used + by any taxon in the character being scored (#294). + +- `LengthAdded()` no longer returns negative values when multiple rows of the + contrast matrix satisfy the fully-ambiguous applicable condition (e.g. + datasets with ~19 taxa and certain character structures); the first matching + row is now used consistently (#302). + +- Shiny: scoring error notification now shows the actual error message + (e.g. "Trees have different numbers of edges") rather than the generic + "Could not score all trees with dataset". +- Shiny: fix search requiring two clicks to start when trees have mixed + topologies (polytomous/binary). The "Search" shortcut button now appears + only after the modal is dismissed via its own Search button, so it is never + obscured by the modal backdrop. +- Fix output trees from `MaximizeParsimony()` having invalid preorder + numbering (affected `DropTip()`, distance calculations, and plotting). +- Fix `fuseInterval = 0` causing a crash (division by zero). +- Fix `is_uninformative()` misclassifying ambiguous characters as + uninformative. +- Fix `compute_fixed_steps()` undercount for all-ambiguous characters. +- Fix IW scoring with missing `min_steps` offset. +- Fix crash when dataset contains only ambiguous (`?`) tokens. + +## Custom search functions + +- `Ratchet()`, `MultiRatchet()`, `Jackknife()`, `MorphyBootstrap()`, and + `TreeSearch()` are no longer deprecated. These functions support pluggable + `TreeScorer` and `EdgeSwapper` functions for custom scoring strategies; + for standard parsimony, use `MaximizeParsimony()`. + +## App improvements (`EasyTrees()`) + +- **Async search**: the session remains responsive while a search is running. +- **Parallel search**: the search settings modal includes a thread count slider + (when multiple cores are available). +- **Tree accumulation**: repeated "Continue search" runs accumulate trees at + the same optimal score, with de-duplication by topology. +- **Search confidence**: after each search, the results pane shows the hit rate + and an estimate of the replicates needed for 95% confidence. +- **Search config modal** reorganized into labelled sections (step weighting, + parallelization, search intensity, results to keep). +- Fix `PlotCharacter()` crash on multifurcating consensus trees. +- Fix first search not appearing to update trees in memory. +- Clarified "Stop after best score found N times" slider label with help text. +- Dataset-adaptive timeout default (1–15 minutes based on dataset size). +- Internal modularization of the Shiny app into proper Shiny modules. + +## Other improvements + # TreeSearch 1.8.0.9001 (2026-04-23) - Reorder parameters in `Q[A]Col(quality, amount)`. diff --git a/R/AdditionTree.R b/R/AdditionTree.R index d6a63605b..595186bba 100644 --- a/R/AdditionTree.R +++ b/R/AdditionTree.R @@ -12,9 +12,7 @@ #' @template MRS #' @return `AdditionTree()` returns a tree of class `phylo`, rooted on #' `sequence[1]`. -#' @importFrom TreeTools AddUnconstrained AddTipEverywhere MatrixToPhyDat -#' PectinateTree -#' @importFrom cli cli_progress_bar cli_progress_update +#' @importFrom TreeTools PectinateTree Renumber #' @family tree generation functions #' @seealso #' @@ -26,113 +24,123 @@ #' [`TreeTools::ConstrainedNJ()`]( #' https://ms609.github.io/TreeTools/reference/ConstrainedNJ) #' @export -AdditionTree <- function (dataset, concavity = Inf, constraint, sequence) { - - # Initialize missing parameters +AdditionTree <- function(dataset, concavity = Inf, constraint, sequence) { + + if (!inherits(dataset, "phyDat")) { + stop("`dataset` must be a `phyDat` object") + } taxa <- names(dataset) + nTaxa <- length(taxa) + + if (nTaxa < 4L) { + return(PectinateTree(taxa)) + } + + # Build addition order if (missing(sequence)) { sequence <- taxa[[1]] } else if (is.numeric(sequence)) { + # Reject non-positive, fractional, out-of-range or duplicated indices before + # subsetting: R's `taxa[i]` would otherwise silently drop (`i <= 0`), + # truncate (fractional) or recycle, yielding a tree that ignores the + # requested order rather than erroring. + if (anyNA(sequence) || any(sequence != round(sequence)) || + any(sequence < 1L) || any(sequence > nTaxa) || + anyDuplicated(sequence)) { + stop("numeric `sequence` must be distinct whole-number indices ", + "between 1 and ", nTaxa, " (the number of taxa in `dataset`)") + } sequence <- taxa[sequence] } - - nTaxa <- length(taxa) - if (length(taxa) < 4) { - return(PectinateTree(taxa)) + if (anyNA(sequence) || !all(sequence %in% taxa)) { + stop("`sequence` must list only taxa present in `dataset` ", + "(by name, or by valid index)") + } + # A duplicated taxon poisons the C++ kernel's addition order: the repeated + # tip is inserted twice and a different tip is never added, so the returned + # tree silently contains one taxon twice and drops another (the numeric path + # already rejects duplicates; mirror that here for character `sequence`). + if (anyDuplicated(sequence)) { + stop("`sequence` must not list any taxon more than once") } - unlisted <- setdiff(taxa, sequence) - if (length(unlisted) > 0) { + if (length(unlisted) > 0L) { sequence <- c(sequence, sample(unlisted)) } - if (!missing(constraint)) { - constraint <- AddUnconstrained(constraint, taxa) - } - - # PrepareDataXXX attributes only valid for full dataset - attr(dataset, "info.amounts") <- NULL - attr(dataset, "min.length") <- NULL - attr(dataset, "informative") <- NULL - attr(dataset, "originalIndex") <- NULL - - # Starting tree, rooted on first element in sequence - tree <- PectinateTree(sequence[1:3]) - - cli_progress_bar("Addition tree", total = sum(2 * (4:nTaxa) - 5)) - for (addition in sequence[4:nTaxa]) { - candidates <- AddTipEverywhere(tree, addition) - nCands <- length(candidates) - - theseTaxa <- candidates[[1]][["tip.label"]] - theseData <- .Recompress(dataset[theseTaxa]) - if (is.finite(concavity)) { - theseData <- PrepareDataIW(theseData) - } else if (is.character(concavity)) { - theseData <- suppressMessages(PrepareDataProfile(theseData)) - } - - if (!missing(constraint)) { - if (!inherits(constraint, "phyDat")) { - if (is.numeric(constraint) && is.null(dim(constraint))) { - constraint <- t(constraint) - } - constraint <- MatrixToPhyDat(t(as.matrix(constraint))) - } - thisConstr <- constraint[theseTaxa] - if (.ConstraintConstrains(thisConstr)) { - # Constraint constrains theseTaxa - - morphyConstr <- PhyDat2Morphy(thisConstr) - # Calculate constraint minimum score - constraintLength <- sum(MinimumLength(thisConstr, compress = TRUE) * - attr(thisConstr, "weight")) - - .Forbidden <- function (edges) { - preorder_morphy(edges, morphyConstr) != constraintLength - } - - - candidates <- candidates[!vapply(lapply(candidates, `[[`, "edge"), - .Forbidden, logical(1))] - UnloadMorphy(morphyConstr) - } - } - - # Score remaining candidates - if (length(theseData)) { - scores <- TreeLength(candidates, theseData, concavity) - minScore <- which.min(scores) - nMin <- length(minScore) - if (nMin > 1) { - minScore <- minScore[sample.int(nMin, 1)] - } - tree <- candidates[[minScore]] - } else { - tree <- sample(candidates, 1)[[1]] + addition_order <- match(sequence, taxa) + + # Profile parsimony: simplify data and extract info_amounts + useProfile <- !missing(concavity) && identical(concavity, "profile") + profileArgs <- list() + if (useProfile) { + dataset <- PrepareDataProfile(dataset) + infoAmounts <- attr(dataset, "info.amounts") + if (!is.null(infoAmounts) && length(infoAmounts) > 0L) { + profileArgs$infoAmounts <- infoAmounts } - cli_progress_update(nCands) + concavity <- Inf + } + # NaN/NA slip past `is.finite() && <= 0` and would reach the kernel as a + # non-finite double, silently selecting equal weights; reject them explicitly. + if (!is.numeric(concavity) || length(concavity) != 1L || is.na(concavity)) { + stop("`concavity` must be a single number (or Inf for equal weights, ", + "or \"profile\" for profile parsimony).") + } + if (is.finite(concavity) && concavity <= 0) { + stop("`concavity` must be positive (or Inf for equal weights, ", + "or \"profile\" for profile parsimony).") } - tree + + # Extract data matrices + at <- attributes(dataset) + contrast <- at$contrast + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = nTaxa, byrow = TRUE) + weight <- .ScaleWeight(at$weight) + levels <- at$levels + + # Constraint + consArgs <- list() + if (!missing(constraint)) { + consArgs <- .PrepareConstraint(constraint, dataset) + } + + # Call C++ Wagner tree + searchArgs <- list( + contrast = contrast, + tip_data = tip_data, + weight = weight, + levels = levels, + addition_order = addition_order, + concavity = as.double(concavity) + ) + result <- do.call(ts_wagner_tree, c(searchArgs, consArgs, profileArgs)) + + # Reconstruct phylo from edge matrix + tree <- list( + edge = result$edge, + tip.label = taxa, + Nnode = nTaxa - 1L + ) + class(tree) <- "phylo" + Renumber(tree) } .ConstraintConstrains <- function(constraint) { + if (is.null(constraint) || length(constraint) == 0L) return(FALSE) if (length(constraint[[1]]) < 1) { FALSE } else { contrast <- attr(constraint, "contrast") - if (dim(contrast)[[2]] < 2) { + if (is.null(contrast) || dim(contrast)[[2]] < 2) { FALSE } else { cont <- `mode<-`(contrast, "logical") nLevel <- dim(contrast)[[1]] - # Could be > 2× more efficient using lower.tri exclude <- vapply(seq_len(nLevel), function(i) { colSums(apply(cont, 1, `&`, cont[i, ])) == 0 }, logical(nLevel)) - - # TODO Validate; passes existing tests, but these do not include all - # edge cases, e.g. 02 03 1 1 splits <- exclude * tabulate(unlist(constraint), nLevel) any(splits[lower.tri(splits)] > 1 & t(splits)[lower.tri(splits)] > 1) } @@ -141,5 +149,5 @@ AdditionTree <- function (dataset, concavity = Inf, constraint, sequence) { .Recompress <- function(dataset) { - MatrixToPhyDat(PhyDatToMatrix(dataset)) + TreeTools::MatrixToPhyDat(TreeTools::PhyDatToMatrix(dataset)) } diff --git a/R/CharacterHierarchy.R b/R/CharacterHierarchy.R new file mode 100644 index 000000000..ab708f5a4 --- /dev/null +++ b/R/CharacterHierarchy.R @@ -0,0 +1,539 @@ +#' Define character hierarchy for inapplicable data +#' +#' Specify the dependency structure between characters in a morphological +#' dataset that uses reductive coding. A "controlling primary" character +#' (typically presence/absence of a structure) determines whether its +#' associated "secondary" characters are applicable. Secondary characters +#' can in turn control tertiary characters, and so on. +#' +#' This hierarchy is required by the HSJ +#' \insertCite{Hopkins2021}{TreeSearch} and step-matrix +#' \insertCite{Goloboff2021}{TreeSearch} approaches to inapplicable +#' characters, and is passed to [`MaximizeParsimony()`] via the `hierarchy` +#' argument. +#' +#' @param ... Named arguments where each name is the index of a controlling +#' character (coerced to integer) and each value is an integer vector of +#' the character indices it controls. Use nested [`list()`]s for deeper +#' hierarchies (see Examples). +#' +#' @return An object of class `"CharacterHierarchy"`. +#' +#' @examples +#' # Simple: character 1 controls characters 2-5 +#' h <- CharacterHierarchy("1" = 2:5) +#' +#' # Multiple controlling primaries +#' h <- CharacterHierarchy("1" = 2:5, "6" = 7:8) +#' +#' # Nested: char 1 controls 2-5; char 3 further controls 9-10 +#' h <- CharacterHierarchy("1" = list(2, 3, 4, 5, "3" = 9:10)) +#' +#' @references +#' \insertAllCited{} +#' @family tree scoring +#' @seealso [MaximizeParsimony()], [HierarchyFromNames()] +#' @export +CharacterHierarchy <- function(...) { + args <- list(...) + if (length(args) == 0L) { + stop("At least one controlling character must be specified.") + } + tree <- .ParseHierarchyArgs(args) + structure(tree, class = "CharacterHierarchy") +} + +# Parse user args into a normalized tree structure. +# Returns a list of nodes, each: +# list(controlling = int, dependents = int[], children = list(, ...)) +# "children" are sub-hierarchies (controlling secondaries). +.ParseHierarchyArgs <- function(args) { + if (is.null(names(args)) || any(names(args) == "")) { + stop("Every element of `...` must be named with the controlling ", + "character index.") + } + controllingIndices <- suppressWarnings(as.integer(names(args))) + if (anyNA(controllingIndices)) { + stop("Controlling character names must be integer indices.") + } + + lapply(seq_along(args), function(i) { + ctrl <- controllingIndices[i] + val <- args[[i]] + .ParseOneBlock(ctrl, val) + }) +} + +# Parse a single controlling-character block. +# val can be: +# - integer vector: simple list of dependent character indices +# - list with mixed named/unnamed elements: unnamed = dependents, +# named = sub-hierarchies (controlling secondaries) +.ParseOneBlock <- function(ctrl, val) { + if (is.numeric(val) && is.null(names(val))) { + # Simple case: vector of dependent indices + return(list( + controlling = as.integer(ctrl), + dependents = as.integer(val), + children = list() + )) + } + if (is.list(val)) { + nms <- names(val) + if (is.null(nms)) nms <- rep("", length(val)) + dependents <- integer(0) + children <- list() + for (j in seq_along(val)) { + if (nms[j] == "") { + # Unnamed: a dependent character index + dependents <- c(dependents, as.integer(val[[j]])) + } else { + # Named: a sub-hierarchy + subCtrl <- suppressWarnings(as.integer(nms[j])) + if (is.na(subCtrl)) { + stop("Sub-hierarchy names must be integer character indices, got '", + nms[j], "'.") + } + # The sub-controlling character is also a dependent of this block + dependents <- c(dependents, subCtrl) + children <- c(children, list(.ParseOneBlock(subCtrl, val[[j]]))) + } + } + return(list( + controlling = as.integer(ctrl), + # A sub-controller may also be listed as an explicit dependent (e.g. + # `list(2, 3, 4, 5, "3" = 9:10)`); keep it once so ValidateHierarchy() + # does not flag it as appearing in multiple blocks. + dependents = unique(dependents), + children = children + )) + } + # Scalar + list( + controlling = as.integer(ctrl), + dependents = as.integer(val), + children = list() + ) +} + +#' @export +print.CharacterHierarchy <- function(x, ...) { + cat("CharacterHierarchy\n") + .PrintBlock <- function(node, indent = 1L) { + pad <- strrep(" ", indent) + leafDeps <- setdiff( + node$dependents, + vapply(node$children, `[[`, integer(1), "controlling") + ) + cat(sprintf("%sChar %d controls: {%s}\n", + pad, node$controlling, + paste(node$dependents, collapse = ", "))) + for (child in node$children) { + .PrintBlock(child, indent + 1L) + } + } + for (node in x) { + .PrintBlock(node) + } + invisible(x) +} + +#' Validate a CharacterHierarchy against a dataset +#' +#' Check that a [`CharacterHierarchy`] object is consistent with a +#' [`phyDat`][phangorn::phyDat] dataset: character indices exist, +#' controlling characters are binary (absent/present), secondaries are +#' coded inapplicable where expected, and no character appears in +#' multiple blocks. +#' +#' @param hierarchy A [`CharacterHierarchy`] object. +#' @param dataset A `phyDat` object. +#' +#' @return `hierarchy`, invisibly (called for side effects: stops with an +#' informative error if validation fails). +#' +#' @keywords internal +#' @importFrom utils head +#' @export +ValidateHierarchy <- function(hierarchy, dataset) { + if (!inherits(hierarchy, "CharacterHierarchy")) { + stop("`hierarchy` must be a CharacterHierarchy object.") + } + if (!inherits(dataset, "phyDat")) { + stop("`dataset` must be a phyDat object.") + } + + nChar <- length(attr(dataset, "index")) + allLevels <- attr(dataset, "allLevels") + levels <- attr(dataset, "levels") + contrast <- attr(dataset, "contrast") + + # Identify the inapplicable token + inappToken <- "-" + if (!inappToken %in% allLevels) { + stop("Dataset does not contain an inapplicable token ('-').") + } + + # Build the original character matrix + idx <- attr(dataset, "index") + origMat <- do.call(rbind, lapply(dataset, function(x) { + allLevels[x[idx]] + })) + + # Identify the "0" state (absence) in the controlling primary + absenceState <- "0" + + # Track all characters claimed by any block + + claimed <- integer(0) + + .ValidateBlock <- function(node, depth = 1L) { + ctrl <- node$controlling + deps <- node$dependents + + # Check indices exist + allIdx <- c(ctrl, deps) + bad <- allIdx[allIdx < 1L | allIdx > nChar] + if (length(bad) > 0L) { + stop(sprintf( + "Character index(es) %s out of range [1, %d].", + paste(bad, collapse = ", "), nChar + )) + } + + # Check no double-claiming + overlap <- intersect(allIdx, claimed) + if (length(overlap) > 0L) { + stop(sprintf( + "Character(s) %s appear in multiple hierarchy blocks.", + paste(overlap, collapse = ", ") + )) + } + claimed <<- c(claimed, allIdx) + + # Check controlling character is binary (has exactly states "0" and "1", + # possibly with inapplicable/missing) + ctrlVals <- unique(origMat[, ctrl]) + ctrlInformative <- setdiff(ctrlVals, c("?", "-")) + if (!all(ctrlInformative %in% c("0", "1"))) { + stop(sprintf( + paste0("Controlling character %d must be binary (states '0' and '1'),", + " but has states: %s."), + ctrl, paste(ctrlInformative, collapse = ", ") + )) + } + + # Check secondaries are "-" where controlling is "0" + absentTaxa <- which(origMat[, ctrl] == absenceState) + if (length(absentTaxa) > 0L) { + for (d in deps) { + depVals <- origMat[absentTaxa, d] + badTaxa <- which(!depVals %in% c("-", "?")) + if (length(badTaxa) > 0L) { + badNames <- rownames(origMat)[absentTaxa[badTaxa]] + stop(sprintf( + paste0("Secondary character %d has non-inapplicable values for ", + "taxa where controlling character %d is absent: %s."), + d, ctrl, paste(head(badNames, 5), collapse = ", ") + )) + } + } + } + + # Recurse into children + for (child in node$children) { + .ValidateBlock(child, depth + 1L) + } + } + + for (node in hierarchy) { + .ValidateBlock(node) + } + + invisible(hierarchy) +} + + +#' Construct a CharacterHierarchy from TNT-style character names +#' +#' Parse character names following the TNT convention where controlling +#' characters are named `sup_` and their dependent characters are +#' named `sub_[_suffix]`. Tags must match between a controlling +#' character and its dependents. Nested hierarchies are detected when a +#' `sub_` character is also a `sup_` for further characters. +#' +#' @param charNames Character vector of names, one per original character. +#' +#' @return A [`CharacterHierarchy`] object, or `NULL` if no hierarchy is +#' detected. +#' +#' @examples +#' names <- c("sup_tail", "sub_tail_colour", "sub_tail_shape", +#' "sup_wing", "sub_wing_venation", "eyes") +#' HierarchyFromNames(names) +#' +#' @family tree scoring +#' @seealso [CharacterHierarchy()] +#' @export +HierarchyFromNames <- function(charNames) { + if (!is.character(charNames) || length(charNames) == 0L) { + stop("`charNames` must be a non-empty character vector.") + } + + # Find sup_ and sub_ characters + supIdx <- grep("^sup_", charNames) + subIdx <- grep("^sub_", charNames) + + if (length(supIdx) == 0L) { + return(NULL) + } + + # Extract tags + supTags <- sub("^sup_", "", charNames[supIdx]) + subTagsFull <- sub("^sub_", "", charNames[subIdx]) + # The tag is the first component before any additional underscore-suffix + # e.g. "sub_tail_colour" → tag = "tail" + subTags <- sub("_.*", "", subTagsFull) + + # Build mapping: tag → controlling index, tag → dependent indices + tagToSup <- setNames(supIdx, supTags) + + # Group sub characters by tag + tagToSubs <- split(subIdx, subTags) + + # Check for sub_ characters referencing nonexistent sup_ tags + orphanTags <- setdiff(names(tagToSubs), supTags) + if (length(orphanTags) > 0L) { + warning(sprintf( + "sub_ characters reference tags with no corresponding sup_: %s", + paste(orphanTags, collapse = ", ") + )) + } + + # Detect nested hierarchies: a sub_ character that is also a sup_ + # Find sub_ chars that are also in supIdx + subAlsoSup <- intersect(subIdx, supIdx) + + # Build hierarchy + # First pass: create flat blocks for all sup_ tags + args <- list() + for (tag in supTags) { + ctrl <- tagToSup[[tag]] + subs <- tagToSubs[[tag]] + if (is.null(subs)) subs <- integer(0) + + # Check which subs are themselves controlling (nested hierarchy) + nestedSubs <- intersect(subs, supIdx) + flatSubs <- setdiff(subs, supIdx) + + if (length(nestedSubs) == 0L) { + # Simple block + args[[as.character(ctrl)]] <- as.integer(subs) + } else { + # Nested: build list with named sub-hierarchies + block <- as.list(as.integer(flatSubs)) + for (ns in nestedSubs) { + nsTag <- supTags[supIdx == ns] + nsSubs <- tagToSubs[[nsTag]] + if (is.null(nsSubs)) nsSubs <- integer(0) + block[[as.character(ns)]] <- as.integer(nsSubs) + } + args[[as.character(ctrl)]] <- block + } + } + + # Filter out sup_ chars whose index also appears in subIdx + # (they'll be included as children of their parent) + topLevelSup <- setdiff(supIdx, subIdx) + if (length(topLevelSup) == 0L) { + # All sup_ characters are also sub_ — circular or all nested. + # Fall back to treating all as top-level with a warning. + warning("All sup_ characters are also sub_ characters. ", + "Treating all as top-level.") + topLevelSup <- supIdx + } + topLevelCtrls <- as.character(topLevelSup) + args <- args[topLevelCtrls] + + do.call(CharacterHierarchy, args) +} + + +#' Extract all character indices from a hierarchy +#' +#' Returns all character indices (controlling + dependent) referenced by +#' a [`CharacterHierarchy`], useful for partitioning characters into +#' hierarchy vs. non-hierarchy sets. +#' +#' @param hierarchy A [`CharacterHierarchy`] object. +#' +#' @return An integer vector of character indices (unsorted, may contain +#' duplicates if the hierarchy is malformed). +#' +#' @keywords internal +#' @export +HierarchyChars <- function(hierarchy) { + .CollectIndices <- function(node) { + c(node$controlling, node$dependents, + unlist(lapply(node$children, .CollectIndices))) + } + unique(unlist(lapply(hierarchy, .CollectIndices))) +} + + +#' List top-level controlling characters +#' +#' @param hierarchy A [`CharacterHierarchy`] object. +#' @return Integer vector of top-level controlling character indices. +#' @keywords internal +#' @export +HierarchyControlling <- function(hierarchy) { + vapply(hierarchy, `[[`, integer(1), "controlling") +} + + +# Build the tip-labels matrix for HSJ scoring. +# +# Converts a phyDat dataset into an integer matrix of per-tip, per-character +# state labels (0-based) for the C++ HSJ scorer: length(dataset) rows (tips) by +# length(attr(dataset, "index")) columns (original characters). +.BuildTipLabels <- function(dataset) { + idx <- attr(dataset, "index") + nTip <- length(dataset) + nChar <- length(idx) + + # dataset is a list of integer vectors (pattern indices per tip) + # Expand via index to original characters, convert to 0-based + mat <- matrix(0L, nrow = nTip, ncol = nChar) + for (t in seq_len(nTip)) { + patternTokens <- dataset[[t]] # token indices for each pattern + mat[t, ] <- patternTokens[idx] - 1L # 0-based + } + mat +} + + +# Identify the primary "absent" state for HSJ scoring. +# +# Returns the 0-based token index of the controlling primary character's +# *absent* state, for the C++ HSJ scorer's `absent_state` argument. +# +# Under reductive coding (Hopkins & St John 2021) the primary codes a +# structure's presence/absence, conventionally "0" = absent, "1" = present. +# The index of "0" depends on the dataset's `levels` ordering (e.g. it is 1 for +# c("-", "0", "1") but 0 for c("0", "1")), so it must be computed rather than +# hard-coded. The inapplicable token "-" is also treated as absent by the +# scorer; if no "0" level exists, the index of "-" is returned. +.HSJAbsentState <- function(dataset) { + lv <- attr(dataset, "levels") + idx <- match("0", lv) + if (is.na(idx)) { + idx <- match("-", lv) + } + if (is.na(idx)) 0L else as.integer(idx - 1L) +} + + +# Convert a CharacterHierarchy into a flat list of hierarchy blocks for the C++ +# ts_hsj_score() bridge. Each block is a list with `primary` (0-based) and +# `secondaries` (0-based integer vector). +.HierarchyToBlocks <- function(hierarchy) { + .FlattenBlock <- function(node) { + block <- list( + primary = node$controlling - 1L, + secondaries = node$dependents - 1L + ) + childBlocks <- lapply(node$children, .FlattenBlock) + c(list(block), unlist(childBlocks, recursive = FALSE)) + } + unlist(lapply(hierarchy, .FlattenBlock), recursive = FALSE) +} + + +# Compute non-hierarchy pattern weights: given a phyDat dataset and a +# CharacterHierarchy, return the integer weight vector (same length as +# attr(dataset, "weight")) with hierarchy characters' contributions subtracted. +# Patterns appearing only in hierarchy characters end up with weight 0. +.NonHierarchyWeights <- function(dataset, hierarchy) { + w <- attr(dataset, "weight") + idx <- attr(dataset, "index") + hChars <- HierarchyChars(hierarchy) + + adjusted <- as.integer(w) + for (ci in hChars) { + if (ci < 1L || ci > length(idx)) next + pat <- idx[ci] + if (pat >= 1L && pat <= length(adjusted) && adjusted[pat] > 0L) { + adjusted[pat] <- adjusted[pat] - 1L + } + } + adjusted +} + + +# Generate resampled weights for hierarchical resampling. +# +# Instead of treating every character independently, groups characters into +# resampling units: each non-hierarchy character is one unit, and each +# top-level hierarchy block (primary + all dependents, recursively) is one +# unit. Jackknife or bootstrap operates on these units. +# +# Returns a list with: +# nonHierarchyWeights: pattern weights for Fitch scoring (non-hierarchy +# chars only, reflecting which free chars were sampled) +# blockCounts: integer vector (length = number of top-level blocks) +# giving how many times each block was sampled (0/1 for jackknife, +# 0+ for bootstrap) +.HierarchicalResampleWeights <- function(dataset, hierarchy, bootstrap, + proportion) { + idx <- attr(dataset, "index") + nPatterns <- length(attr(dataset, "weight")) + nChars <- length(idx) + + # Collect chars per top-level block (includes nested dependents) + .CollectAll <- function(node) { + c(node$controlling, node$dependents, + unlist(lapply(node$children, .CollectAll))) + } + nBlocks <- length(hierarchy) + blockChars <- lapply(hierarchy, function(node) unique(.CollectAll(node))) + hCharsSet <- unique(unlist(blockChars)) + + freeChars <- setdiff(seq_len(nChars), hCharsSet) + nFree <- length(freeChars) + nUnits <- nFree + nBlocks + + if (nUnits < 2L) { + # Degenerate: can't jackknife with < 2 units + return(list( + nonHierarchyWeights = .NonHierarchyWeights(dataset, hierarchy), + blockCounts = rep(1L, nBlocks) + )) + } + + if (bootstrap) { + sampled <- sample.int(nUnits, nUnits, replace = TRUE) + } else { + nKeep <- max(1L, ceiling(proportion * nUnits)) + nKeep <- min(nKeep, nUnits - 1L) + sampled <- sample.int(nUnits, nKeep, replace = FALSE) + } + + unitCounts <- tabulate(sampled, nbins = nUnits) + + # Non-hierarchy pattern weights from retained free chars + nhWeights <- integer(nPatterns) + for (i in seq_len(nFree)) { + if (unitCounts[i] > 0L) { + pat <- idx[freeChars[i]] + nhWeights[pat] <- nhWeights[pat] + unitCounts[i] + } + } + + blockCounts <- unitCounts[nFree + seq_len(nBlocks)] + + list( + nonHierarchyWeights = nhWeights, + blockCounts = blockCounts + ) +} diff --git a/R/ClusterStrings.R b/R/ClusterStrings.R index 1ee081e7d..4d4962099 100644 --- a/R/ClusterStrings.R +++ b/R/ClusterStrings.R @@ -18,8 +18,6 @@ #' paste0("AnotherCluster_", letters[1:6]))) #' @template MRS #' @importFrom utils adist -#' @importFrom cluster pam silhouette -#' @importFrom protoclust protoclust #' @importFrom stats as.dist cutree #' @family utility functions #' @export @@ -27,6 +25,14 @@ ClusterStrings <- function (x, maxCluster = 12) { if (maxCluster < 2L) { stop("`maxCluster` must be at least two.") } + if (!requireNamespace("cluster", quietly = TRUE)) { + stop("Package \"cluster\" is required for ClusterStrings().\n", # nocov + "Install it with: install.packages(\"cluster\")", call. = FALSE) # nocov + } + if (!requireNamespace("protoclust", quietly = TRUE)) { + stop("Package \"protoclust\" is required for ClusterStrings().\n", # nocov + "Install it with: install.packages(\"protoclust\")", call. = FALSE) # nocov + } if (length(unique(x)) < maxCluster) { nom <- unique(x) @@ -42,19 +48,19 @@ ClusterStrings <- function (x, maxCluster = 12) { kInc <- 1 / (nMethodsChecked * nK) pamClusters <- lapply(possibleClusters, function (k) { - pam(dists, k = k) + cluster::pam(dists, k = k) }) pamSils <- vapply(pamClusters, function (pamCluster) { - mean(silhouette(pamCluster)[, 3]) + mean(cluster::silhouette(pamCluster)[, 3]) }, double(1)) bestPam <- which.max(pamSils) pamSil <- pamSils[bestPam] pamCluster <- pamClusters[[bestPam]][["clustering"]] - hTree <- protoclust(as.dist(dists)) + hTree <- protoclust::protoclust(as.dist(dists)) hClusters <- lapply(possibleClusters, function (k) cutree(hTree, k = k)) hSils <- vapply(hClusters, function (hCluster) { - mean(silhouette(hCluster, dists)[, 3]) + mean(cluster::silhouette(hCluster, dists)[, 3]) }, double(1)) bestH <- which.max(hSils) hSil <- hSils[bestH] diff --git a/R/Concordance.R b/R/Concordance.R index 3fc2c2904..904f3becb 100644 --- a/R/Concordance.R +++ b/R/Concordance.R @@ -151,7 +151,7 @@ NULL #' @importFrom abind abind #' @importFrom stats setNames #' @importFrom TreeDist ClusteringEntropy Entropy entropy_int -#' MutualClusteringInfo +#' @importFrom TreeDist MutualClusteringInfo #' @importFrom TreeTools as.Splits MatchStrings Subsplit TipLabels #' @export ClusteringConcordance <- function( @@ -166,7 +166,7 @@ ClusteringConcordance <- function( return(NULL) } if (is.null(tree)) { - warning("Cannot calculate concordance without `dataset`.") + warning("Cannot calculate concordance without `tree`.") return(NULL) } @@ -259,7 +259,17 @@ ClusteringConcordance <- function( charInfo <- MutualClusteringInfo(tree, charSplits)[at[["index"]]] if (is.numeric(normalize)) { rTrees <- replicate(normalize, RandomTree(tree), simplify = FALSE) - randInfo <- MutualClusteringInfo(rTrees, charSplits)[, attr(dataset, "index")] + # Score each random tree against `charSplits` separately: characters with + # ambiguous tokens yield splits over different tip subsets, and the + # vectorised `MutualClusteringInfo(, )` + # path cannot reconcile a single label set across them ("Old and new + # labels must match"). Looping one tree at a time mirrors the working + # `charInfo` call above. + randInfo <- t(vapply( + rTrees, + function(rt) MutualClusteringInfo(rt, charSplits), + double(length(charSplits)) + ))[, attr(dataset, "index"), drop = FALSE] randMean <- colMeans(randInfo) var <- rowSums((t(randInfo) - randMean) ^ 2) / (normalize - 1) mcse <- sqrt(var / normalize) @@ -311,6 +321,16 @@ ClusteringConcordance <- function( mcseInfo[mcseInfo < sqrt(.Machine$double.eps)] <- 0 structure(ret, hMax = charMax, mcse = mcseInfo) } else { + # The characterwise return is deliberately NOT random-expectation + # normalized for logical `normalize`: `charInfo` is + # MutualClusteringInfo() against the whole tree, whereas the + # analytic `zero` baseline above is per-single-split expected MI, so + # subtracting it would mix incompatible quantities (and the + # entropy-weighted variant was abandoned -- see the note below the + # @return docs). Only the Monte-Carlo path (numeric `normalize`) + # offers a same-scale empirical baseline. So return charInfo scaled + # by its maximum (hBest-like), as shipped since the original + # implementation (#205). structure(charInfo / charMax, hMax = charMax) } }, { @@ -452,11 +472,20 @@ QALegend <- function(where = c(0.1, 0.3, 0.1, 0.3), n = 5, Col = QACol, #' If a vector (length > 1), each entry controls one side following the usual #' `par(mar)` order — `c(bottom, left, top, right)` — where a positive value #' enables that strip with the given width/height and `NA` or `0` suppresses it. -#' Currently only the bottom (entry 1) and left (entry 2) strips are -#' implemented; further entries are accepted but ignored. -#' The left strip is coloured by the characterwise concordance (weighted mean -#' across edges); the bottom strip by the edgewise concordance (weighted mean -#' across characters). One blank cell separates each strip from the main grid. +#' The left and right strips are coloured by the characterwise concordance +#' (weighted mean across edges); the bottom and top strips by the edgewise +#' concordance (weighted mean across characters). +#' One blank cell separates each strip from the main grid. +#' @param paintSize Integer scalar or vector. Adds a painted strip OUTSIDE any +#' `marginSize` strip, using hue from [TreeTools::PaintTree()] (edges) and the +#' [PaintCharacters()] algorithm (characters). A scalar `> 0` adds a right +#' strip (characters) and a top strip (edges), each `paintSize` cells wide/tall. +#' A length-4 vector follows `c(bottom, left, top, right)` like `marginSize`; +#' `NA` or `0` suppresses that side. One blank cell separates each paint strip +#' from the adjacent margin strip (or main grid if no margin exists on that side). +#' @param palette Palette specification passed to [TreeTools::PaintTree()]. +#' Either a character string (`"default"`, `"protanopia"`, `"tritanopia"`) or +#' a function `function(h, s)`. Ignored when `paintSize` is zero on all sides. #' @param \dots Arguments to `abline`, to control the appearance of vertical #' lines marking important edges. #' @returns `ConcordanceTable()` invisibly returns an named list containing: @@ -474,7 +503,8 @@ QALegend <- function(where = c(0.1, 0.3, 0.1, 0.3), n = 5, Col = QACol, #' #' # Plot tree and identify nodes #' library("TreeTools", quietly = TRUE) -#' plot(tree) +#' paint <- PaintTree(tree) +#' plot(tree, edge.col = paint$edgeCol, tip.col = paint$tipCol, edge.width = 2) #' nodeIndex <- as.integer(rownames(as.Splits(tree))) #' nodelabels(seq_along(nodeIndex), nodeIndex, adj = c(2, 1), #' frame = "none", bg = NULL) @@ -482,7 +512,7 @@ QALegend <- function(where = c(0.1, 0.3, 0.1, 0.3), n = 5, Col = QACol, #' #' # View information shared by characters and edges #' ConcordanceTable(tree, dataset, largeClade = 3, col = 2, lwd = 3, -#' marginSize = 1:4) +#' marginSize = c(0, 0, 1, 2), paintSize = c(1, 2, 0, 0)) #' axis(1) #' axis(2) #' @@ -490,7 +520,8 @@ QALegend <- function(where = c(0.1, 0.3, 0.1, 0.3), n = 5, Col = QACol, #' image(t(`mode<-`(PhyDatToMatrix(dataset), "numeric")), axes = FALSE, #' xlab = "Leaf", ylab = "Character") #' @importFrom graphics abline image mtext -#' @importFrom TreeTools CladeSizes NTip +#' @importFrom grDevices col2rgb convertColor rgb +#' @importFrom TreeTools CladeSizes NTip PaintTree #' @family split support functions #' @seealso #' - [SiteConcordance()]: compute underlying concordance values. @@ -498,7 +529,8 @@ QALegend <- function(where = c(0.1, 0.3, 0.1, 0.3), n = 5, Col = QACol, ConcordanceTable <- function(tree, dataset, Col = QACol, largeClade = 0, xlab = "Edge", ylab = "Character", normalize = TRUE, plot = TRUE, - marginSize = 0L, ...) { + marginSize = 0L, paintSize = 0L, + palette = "default", ...) { cc <- ClusteringConcordance(tree, dataset, return = "all", normalize = normalize) nodes <- seq_len(dim(cc)[[2]]) @@ -512,21 +544,47 @@ ConcordanceTable <- function(tree, dataset, Col = QACol, largeClade = 0, col <- matrix(Col(quality, amount), dim(amount)[[1]], dim(amount)[[2]]) - # Parse marginSize: scalar → both sides; vector → c(bottom, left, ...) + # Parse marginSize: scalar → bottom + left; vector → c(bottom, left, top, right) ms <- as.integer(marginSize) if (length(ms) == 1L) { ms_bottom <- if (!is.na(ms) && ms > 0L) ms else 0L ms_left <- ms_bottom + ms_top <- 0L + ms_right <- 0L } else { - ms_bottom <- ms[1L] # [] returns NA if length(ms) < 1 - if (is.na(ms_bottom) || ms_bottom < 0L) ms_bottom <- 0L - ms_left <- ms[2L] - if (is.na(ms_left) || ms_left < 0L) ms_left <- 0L + ms_bottom <- if (!is.na(ms[1L]) && ms[1L] > 0L) ms[1L] else 0L + ms_left <- if (length(ms) >= 2L && !is.na(ms[2L]) && ms[2L] > 0L) ms[2L] else 0L + ms_top <- if (length(ms) >= 3L && !is.na(ms[3L]) && ms[3L] > 0L) ms[3L] else 0L + ms_right <- if (length(ms) >= 4L && !is.na(ms[4L]) && ms[4L] > 0L) ms[4L] else 0L } - x_offset <- if (ms_left > 0L) ms_left + 1L else 0L - y_offset <- if (ms_bottom > 0L) ms_bottom + 1L else 0L - if (ms_left > 0L || ms_bottom > 0L) { + # Parse paintSize: scalar → top + right; vector → c(bottom, left, top, right) + ps <- as.integer(paintSize) + if (length(ps) == 1L) { + ps_top <- if (!is.na(ps) && ps > 0L) ps else 0L + ps_right <- ps_top + ps_bottom <- 0L + ps_left <- 0L + } else { + ps_bottom <- if (!is.na(ps[1L]) && ps[1L] > 0L) ps[1L] else 0L + ps_left <- if (length(ps) >= 2L && !is.na(ps[2L]) && ps[2L] > 0L) ps[2L] else 0L + ps_top <- if (length(ps) >= 3L && !is.na(ps[3L]) && ps[3L] > 0L) ps[3L] else 0L + ps_right <- if (length(ps) >= 4L && !is.na(ps[4L]) && ps[4L] > 0L) ps[4L] else 0L + } + + # Paint is outermost; its width is prepended/appended to the margin offset. + ps_x_offset <- if (ps_left > 0L) ps_left + 1L else 0L + ps_y_offset <- if (ps_bottom > 0L) ps_bottom + 1L else 0L + ps_x_suffix <- if (ps_right > 0L) ps_right + 1L else 0L + ps_y_suffix <- if (ps_top > 0L) ps_top + 1L else 0L + + x_offset <- ps_x_offset + if (ms_left > 0L) ms_left + 1L else 0L + y_offset <- ps_y_offset + if (ms_bottom > 0L) ms_bottom + 1L else 0L + x_suffix <- (if (ms_right > 0L) ms_right + 1L else 0L) + ps_x_suffix + y_suffix <- (if (ms_top > 0L) ms_top + 1L else 0L) + ps_y_suffix + + if (ms_left > 0L || ms_bottom > 0L || ms_top > 0L || ms_right > 0L || + ps_left > 0L || ps_bottom > 0L || ps_top > 0L || ps_right > 0L) { n_edges <- dim(cc)[[2]] n_chars <- dim(cc)[[3]] @@ -536,29 +594,75 @@ ConcordanceTable <- function(tree, dataset, Col = QACol, largeClade = 0, # `quality` already has NAs zeroed above # Extended layout (x = left→right, y = bottom→top): - # x: [char margin: 1..ms_left] [blank: ms_left+1] [grid: (x_offset+1)..(x_offset+n_edges)] - # y: [edge margin: 1..ms_bottom] [blank: ms_bottom+1] [grid: (y_offset+1)..(y_offset+n_chars)] - # (absent margin ↔ x_offset or y_offset = 0, so that portion of the range vanishes) - nx <- x_offset + n_edges - ny <- y_offset + n_chars + # x: [paint_left] [blank] [margin_left] [blank] [grid] [blank] [margin_right] [blank] [paint_right] + # y: [paint_bottom] [blank] [margin_bottom] [blank] [grid] [blank] [margin_top] [blank] [paint_top] + nx <- x_offset + n_edges + x_suffix + ny <- y_offset + n_chars + y_suffix ext_col <- matrix("#FFFFFF", nx, ny) xi <- (x_offset + 1L):(x_offset + n_edges) # x indices of main grid yi <- (y_offset + 1L):(y_offset + n_chars) # y indices of main grid ext_col[xi, yi] <- col - if (ms_left > 0L) { + if (ms_left > 0L || ms_right > 0L) { denom_c <- colSums(hBest_w) - char_conc <- ifelse(denom_c == 0, 0, colSums(quality * hBest_w) / denom_c) + char_conc <- pmax(-1, pmin(1, + ifelse(denom_c == 0, 0, colSums(quality * hBest_w) / denom_c))) charInfo <- cc["hChar", 1, ] * cc["n", 1, ] char_cols <- Col(char_conc, charInfo / max(charInfo)) - for (i in seq_len(ms_left)) ext_col[i, yi] <- char_cols + if (ms_left > 0L) { + for (i in seq_len(ms_left)) ext_col[ps_x_offset + i, yi] <- char_cols + } + if (ms_right > 0L) { + for (i in seq_len(ms_right)) ext_col[x_offset + n_edges + 1L + i, yi] <- char_cols + } } - if (ms_bottom > 0L) { + if (ms_bottom > 0L || ms_top > 0L) { denom_e <- rowSums(hBest_w) - edge_conc <- ifelse(denom_e == 0, 0, rowSums(quality * hBest_w) / denom_e) + edge_conc <- pmax(-1, pmin(1, + ifelse(denom_e == 0, 0, rowSums(quality * hBest_w) / denom_e))) edge_cols <- Col(edge_conc, rowMeans(cc["hSplit", , ])) - for (j in seq_len(ms_bottom)) ext_col[xi, j] <- edge_cols + if (ms_bottom > 0L) { + for (j in seq_len(ms_bottom)) ext_col[xi, ps_y_offset + j] <- edge_cols + } + if (ms_top > 0L) { + for (j in seq_len(ms_top)) ext_col[xi, y_offset + n_chars + 1L + j] <- edge_cols + } + } + + if (ps_left > 0L || ps_right > 0L || ps_top > 0L || ps_bottom > 0L) { + paint <- PaintTree(tree, palette) + ctNodes <- as.integer(rownames(info)) + edgeIdx <- match(ctNodes, tree[["edge"]][, 2L]) + edge_paint_cols <- paint$edgeCol[edgeIdx] + + if (ps_left > 0L || ps_right > 0L) { + # Per-character colours: Lab-weighted mean of edge paint colours, + # reusing `amount` (= relInfo) and `quality` already NA-zeroed above. + labMat <- matrix( + convertColor(t(col2rgb(edge_paint_cols)) / 255, from = "sRGB", to = "Lab"), + ncol = 3L + ) + wMat_p <- pmax(quality, 0) * amount + wSum_p <- colSums(wMat_p) + noInfo_p <- wSum_p == 0 + labAvg_p <- t(t(labMat) %*% wMat_p) / ifelse(noInfo_p, 1, wSum_p) + rgbAvg_p <- matrix( + pmax(0, pmin(1, convertColor(labAvg_p, from = "Lab", to = "sRGB"))), + ncol = 3L + ) + char_paint_cols <- rgb(rgbAvg_p[, 1L], rgbAvg_p[, 2L], rgbAvg_p[, 3L]) + char_paint_cols[noInfo_p] <- "#888888" + + if (ps_left > 0L) + for (i in seq_len(ps_left)) ext_col[i, yi] <- char_paint_cols + if (ps_right > 0L) + for (i in seq_len(ps_right)) ext_col[nx - ps_right + i, yi] <- char_paint_cols + } + if (ps_bottom > 0L) + for (j in seq_len(ps_bottom)) ext_col[xi, j] <- edge_paint_cols + if (ps_top > 0L) + for (j in seq_len(ps_top)) ext_col[xi, ny - ps_top + j] <- edge_paint_cols } image(seq_len(nx), seq_len(ny), @@ -686,19 +790,32 @@ QuartetConcordance <- function( warning("No overlap between tree labels and dataset.") return(NULL) } + dataset <- dataset[tipLabels, drop = FALSE] splits <- as.Splits(tree, dataset) logiSplits <- vapply(seq_along(splits), function (i) as.logical(splits[[i]]), logical(NTip(dataset))) - characters <- PhyDatToMatrix(dataset, ambigNA = TRUE) + contrast <- attr(dataset, "contrast") charLevels <- attr(dataset, "allLevels") - isAmbig <- rowSums(attr(dataset, "contrast")) > 1 + isInapp <- charLevels == "-" - nonGroupingLevels <- charLevels[isAmbig | isInapp] - characters[characters %in% nonGroupingLevels] <- NA - - charInt <- `mode<-`(characters, "integer") + isAmbig <- rowSums(contrast[, colnames(contrast) != "-"]) > 1 + isGrouping <- !isAmbig & !isInapp + + # For each grouping level, which column of the contrast matrix does it uniquely set? + groupingCols <- apply(contrast[isGrouping, , drop = FALSE] > 0, 1, which) + + levelToInt <- rep(NA_integer_, length(charLevels)) + levelToInt[isGrouping] <- as.integer(groupingCols) + + characters <- PhyDatToMatrix(dataset) + charInt <- array( + levelToInt[match(characters, charLevels)], + dim = dim(characters), + dimnames = dimnames(characters) + ) + raw_counts <- quartet_concordance(logiSplits, charInt) num <- raw_counts$concordant @@ -752,24 +869,22 @@ QuartetConcordance <- function( } } -#' @importFrom fastmap fastmap -.ExpectedMICache <- fastmap() +.ExpectedMICache <- new.env(hash = TRUE, parent = emptyenv()) # @param a must be a vector of length <= 2 # @param b may be longer -#' @importFrom base64enc base64encode .ExpectedMI <- function(a, b) { if (length(a) < 2 || length(b) < 2) { 0 } else { - key <- base64enc::base64encode(mi_key(a, b)) - if (.ExpectedMICache$has(key)) { - .ExpectedMICache$get(key) + key <- mi_key(a, b) + if (!is.null(.ExpectedMICache[[key]])) { + .ExpectedMICache[[key]] } else { ret <- expected_mi(a, b) # Cache: - .ExpectedMICache$set(key, ret) + .ExpectedMICache[[key]] <- ret # Return: ret } @@ -809,7 +924,7 @@ QuartetConcordance <- function( #' split's internal numbering. #' #' @importFrom TreeTools as.multiPhylo CladisticInfo CompatibleSplits -#' MatchStrings +#' @importFrom TreeTools MatchStrings #' @export PhylogeneticConcordance <- function(tree, dataset) { if (is.null(dataset)) { diff --git a/R/Consistency.R b/R/Consistency.R index 2aeb4adbc..e084370f6 100644 --- a/R/Consistency.R +++ b/R/Consistency.R @@ -109,8 +109,7 @@ Consistency <- function (dataset, tree, nRelabel = 0, compress = FALSE) { } -#' @importFrom fastmap fastmap -.CharLengthCache <- fastmap() +.CharLengthCache <- new.env(hash = TRUE, parent = emptyenv()) #' Expected length #' @@ -127,7 +126,6 @@ Consistency <- function (dataset, tree, nRelabel = 0, compress = FALSE) { #' #' @export #' @importFrom stats median -#' @importFrom stringi stri_paste #' @family tree scoring #' @template MRS ExpectedLength <- function(dataset, tree, nRelabel = 1000, compress = FALSE) { @@ -151,9 +149,9 @@ ExpectedLength <- function(dataset, tree, nRelabel = 1000, compress = FALSE) { }, integer(nLevels))) .LengthForChar <- function(x) { - key <- stri_paste(c(nRelabel, x), collapse = ",") - if (.CharLengthCache$has(key)) { - .CharLengthCache$get(key) + key <- paste(c(nRelabel, x), collapse = ",") + if (!is.null(.CharLengthCache[[key]])) { + .CharLengthCache[[key]] } else { patterns <- apply(unname(unique(t( as.data.frame(replicate(nRelabel, sample(rep(seq_along(x), x))))))), @@ -170,7 +168,7 @@ ExpectedLength <- function(dataset, tree, nRelabel = 1000, compress = FALSE) { contrast = rwContrast, class = "phyDat") ret <- median(FastCharacterLength(tree, phy)) - .CharLengthCache$set(key, ret) + .CharLengthCache[[key]] <- ret ret } } diff --git a/R/CustomSearch.R b/R/CustomSearch.R index 0e47c2150..eb16e198f 100644 --- a/R/CustomSearch.R +++ b/R/CustomSearch.R @@ -101,6 +101,8 @@ EdgeListSearch <- function (edgeList, dataset, #' #' Run standard search algorithms (\acronym{NNI}, \acronym{SPR} or \acronym{TBR}) #' to search for a more parsimonious tree. +#' For standard parsimony searches, [`MaximizeParsimony()`] is faster; +#' use `TreeSearch()` when you need a custom `TreeScorer` or `EdgeSwapper`. #' #' For detailed documentation of the "TreeSearch" package, including full #' instructions for loading phylogenetic data into R and initiating and @@ -177,7 +179,6 @@ TreeSearch <- function (tree, dataset, maxIter = 100L, maxHits = 20L, stopAtPeak = FALSE, stopAtPlateau = 0L, verbosity = 1L, ...) { - # initialize tree and data if (dim(tree[["edge"]])[1] != 2 * tree[["Nnode"]]) { stop("tree must be bifurcating; try rooting with ape::root") } diff --git a/R/Jackknife.R b/R/Jackknife.R index 78ab21b9e..fce0b7941 100644 --- a/R/Jackknife.R +++ b/R/Jackknife.R @@ -1,11 +1,8 @@ #' Jackknife resampling #' #' Resample trees using Jackknife resampling, i.e. removing a subset of -#' characters. -#' -#' The function assumes that `InitializeData()` will return a morphy object; -#' if this doesn't hold for you, post a [GitHub issue]( -#' https://github.com/ms609/TreeSearch/issues/new/) or e-mail the maintainer. +#' characters. For standard parsimony, [`Resample()`] is faster; use +#' `Jackknife()` when you need a custom `TreeScorer` or `EdgeSwapper`. #' #' @inheritParams Ratchet #' @param resampleFreq Double between 0 and 1 stating proportion of characters @@ -16,8 +13,8 @@ #' @template MRS #' @importFrom TreeTools RenumberEdges RenumberTips #' @seealso -#' - [`Resample()`]: Jackknife resampling for non-custom searches performed -#' using `MaximizeParsimony()`. +#' - [`Resample()`]: Jackknife and bootstrap resampling using the C++ search +#' engine. #' - [`JackLabels()`]: Label nodes of a tree with jackknife supports. #' @family split support functions #' @family custom search functions @@ -29,7 +26,6 @@ Jackknife <- function(tree, dataset, resampleFreq = 2 / 3, EdgeSwapper = TBRSwap, jackIter = 5000L, searchIter = 4000L, searchHits = 42L, verbosity = 1L, ...) { - # Initialize tree and data if (dim(tree[["edge"]])[1] != 2 * tree[["Nnode"]]) { stop("tree must be bifurcating; try rooting with ape::root") } diff --git a/R/LeastSquares.R b/R/LeastSquares.R new file mode 100644 index 000000000..ceb0d034e --- /dev/null +++ b/R/LeastSquares.R @@ -0,0 +1,243 @@ +# Least-squares distance tree fitting and search. +# +# A sibling to MaximizeParsimony() that optimises a least-squares fit to a +# target distance matrix instead of a parsimony score, using the same fast +# C++ rearrangement kernel (NNI + SPR). Built for Lapointe & Cucumel's (1997) +# average consensus procedure, where the averaged patristic distance matrix is +# generally non-additive and the best-fitting topology must be found +# heuristically. + +# Internal: coerce `dist` to a labelled symmetric matrix. +.LSMatrix <- function(dist) { + D <- as.matrix(dist) + if (nrow(D) != ncol(D)) { + stop("`dist` must be a square distance matrix or a `dist` object") + } + labs <- rownames(D) + if (is.null(labs)) labs <- colnames(D) + if (is.null(labs)) { + stop("`dist` must carry tip labels (row/column names)") + } + dimnames(D) <- list(labs, labs) + if (anyDuplicated(labs)) { + stop("`dist` has duplicated tip labels") + } + if (any(!is.finite(D))) { + stop("`dist` contains non-finite values (NA, NaN or Inf)") + } + if (!isSymmetric(unname(D))) { + stop("`dist` must be a symmetric distance matrix") + } + D +} + +# Internal: per-pair weight matrix, or NULL for unit weights. +# `weight`: NULL, "fm" (Fitch-Margoliash 1/D^2), or a numeric matrix. +.LSWeight <- function(weight, D) { + if (is.null(weight)) return(NULL) + if (is.character(weight)) { + weight <- match.arg(weight, c("fm", "none")) + if (weight == "none") return(NULL) + W <- matrix(0, nrow(D), ncol(D), dimnames = dimnames(D)) + nz <- D != 0 + W[nz] <- 1 / (D[nz]^2) + return(W) + } + W <- as.matrix(weight) + if (!identical(dim(W), dim(D))) { + stop("`weight` matrix must have the same dimensions as `dist`") + } + dimnames(W) <- dimnames(D) + W +} + +# Internal: prepare a starting tree for the C++ kernel — a rooted binary tree +# in canonical TreeSearch numbering (root = nTip + 1). Returns the prepared +# tree; align the distance matrix to its `tip.label` order before calling C++. +.LSPrepTree <- function(tree, labs) { + if (!inherits(tree, "phylo")) stop("starting tree must be a `phylo` object") + tree <- TreeTools::KeepTip(tree, labs) + # The kernel needs a *rooted* binary tree (n - 1 internal nodes, 2n - 2 + # edges). Neighbour-joining and unrooted inputs are binary but unrooted, so + # test for rootedness too; multi2di resolves any basal polytomy, rooting the + # tree, and is a no-op on a tree that is already rooted and binary. + if (!ape::is.binary(tree) || !ape::is.rooted(tree)) { + tree <- ape::multi2di(tree, random = FALSE) + } + tree <- TreeTools::Preorder(tree) + nTip <- length(tree[["tip.label"]]) + if (nrow(tree[["edge"]]) != 2L * nTip - 2L) { + stop("Could not coerce starting tree to a rooted binary form") # nocov + } + tree +} + +# Internal: build the returned tree from a rooted binary edge matrix and fitted +# branch lengths. Always constructs a *fresh* phylo (no inherited attributes) +# and strips any "order" attribute after unrooting: a stale order attribute +# (e.g. "preorder" from TreeTools::Preorder) makes ape's C routines, including +# cophenetic(), read the edge matrix in the wrong order and corrupt memory. +.LSFinalize <- function(edge, edgeLength, rss, tipLabels) { + nTip <- length(tipLabels) + out <- structure( + list(edge = edge, + edge.length = edgeLength, + Nnode = nTip - 1L, + tip.label = tipLabels), + class = "phylo" + ) + # phangorn convention: report the unrooted tree. The kernel returns the two + # root edges as (length, 0), so unrooting sums them to the true branch length. + out <- ape::unroot(out) + attr(out, "order") <- NULL + attr(out, "RSS") <- rss + out +} + +#' Fit branch lengths to a distance matrix on a fixed topology +#' +#' Fits branch lengths on a fixed tree topology that minimise the (optionally +#' weighted) least-squares discrepancy between the tree's patristic distances +#' and a target distance matrix, using the package's C++ kernel. This is the +#' fixed-topology counterpart of [`LeastSquaresTree()`], and the direct analogue +#' of [phangorn::nnls.tree()]. +#' +#' @param tree A bifurcating tree of class \code{\link[ape]{phylo}}. Edge +#' lengths, if any, are ignored and refitted. +#' @param dist A distance matrix (object of class \code{\link[stats]{dist}} or a +#' symmetric matrix with tip labels) over the tips of `tree`. +#' @param method Either `"nnls"` (non-negative least squares; branch lengths are +#' constrained to be \eqn{\ge 0}, matching [phangorn::nnls.tree()] and Lapointe +#' & Cucumel) or `"ols"` (ordinary least squares; faster, closed form, but may +#' return negative lengths). +#' @param weight Optional weighting of the residuals. `NULL` (default) gives +#' unweighted least squares; `"fm"` applies Fitch-Margoliash weights +#' \eqn{1 / D_{ij}^2}; a numeric matrix supplies custom per-pair weights. +#' +#' @return The input `tree`, returned **unrooted**, with `edge.length` set to the +#' fitted branch lengths and an attribute `"RSS"` giving the residual sum of +#' squares. +#' +#' @examples +#' tree <- ape::rtree(8) +#' D <- cophenetic(tree) +#' fit <- LeastSquaresFit(tree, D) +#' attr(fit, "RSS") # ~ 0: D is additive on this topology +#' +#' @seealso [`LeastSquaresTree()`] to search topologies; [phangorn::nnls.tree()]. +#' @template MRS +#' @family least-squares functions +#' @importFrom stats cophenetic +#' @export +LeastSquaresFit <- function(tree, dist, method = c("nnls", "ols"), + weight = NULL) { + method <- match.arg(method) + D <- .LSMatrix(dist) + if (nrow(D) < 3L) { + stop("Least-squares fit needs at least three tips") + } + W <- .LSWeight(weight, D) + prepped <- .LSPrepTree(tree, rownames(D)) + labs <- prepped[["tip.label"]] + Dord <- D[labs, labs, drop = FALSE] + Word <- if (is.null(W)) NULL else W[labs, labs, drop = FALSE] + methodCode <- if (method == "ols") 0L else 1L + + res <- ts_ls_fit(prepped[["edge"]], Dord, Word, methodCode) + if (!isTRUE(res[["ok"]])) { + warning("Least-squares solve was singular; results may be unreliable") + } + .LSFinalize(prepped[["edge"]], res[["edge_length"]], res[["rss"]], labs) +} + +#' Find the least-squares-optimal tree for a distance matrix +#' +#' Searches tree topologies for the one whose patristic distances best fit a +#' target distance matrix under a least-squares criterion, fitting branch +#' lengths on each candidate and minimising the residual sum of squares. The +#' heuristic uses the package's optimised C++ kernel, alternating \acronym{NNI} +#' and \acronym{SPR} rearrangements, exactly as the parsimony search does — but +#' driven by the least-squares score rather than tree length. +#' +#' This implements the topology-search step of Lapointe & Cucumel's (1997) +#' average consensus procedure, in which an averaged (and generally +#' non-additive) patristic distance matrix is fit by a Fitch-Margoliash +#' least-squares tree. +#' +#' @inheritParams LeastSquaresFit +#' @param dist A distance matrix (object of class \code{\link[stats]{dist}} or a +#' symmetric matrix with tip labels). +#' @param tree Optional starting point: a single \code{\link[ape]{phylo}} tree, +#' a list of trees (\code{multiPhylo}), or `NULL` (the default) to start from the +#' neighbour-joining tree of `dist`. When several trees are supplied the search +#' is run from each and the best-fitting result is returned. +#' @param maxHits Integer; during hill-climbing, the number of equally-scoring +#' rearrangements to accept before moving on (helps traverse plateaux). +#' @param spr Logical; if `TRUE` (default) interleave \acronym{SPR} sweeps with +#' \acronym{NNI}, otherwise use \acronym{NNI} only (faster, more local). +#' +#' @return The best-fitting tree found, returned **unrooted**, with fitted +#' `edge.length` and an attribute `"RSS"` giving its residual sum of squares. +#' +#' @examples +#' set.seed(1) +#' trueTree <- ape::rtree(10) +#' D <- cophenetic(trueTree) # additive: the generating tree fits exactly +#' found <- LeastSquaresTree(D) +#' attr(found, "RSS") # ~ 0 +#' +#' @seealso [`LeastSquaresFit()`] for fixed-topology fitting; +#' [`MaximizeParsimony()`] for the parsimony analogue. +#' @template MRS +#' @references \insertRef{LapointeCucumel1997}{TreeSearch} +#' @family least-squares functions +#' @importFrom stats cophenetic +#' @export +LeastSquaresTree <- function(dist, tree = NULL, method = c("nnls", "ols"), + weight = NULL, maxHits = 1L, spr = TRUE) { + method <- match.arg(method) + methodCode <- if (method == "ols") 0L else 1L + D <- .LSMatrix(dist) + W <- .LSWeight(weight, D) + labs <- rownames(D) + nTip <- length(labs) + if (nTip < 4L) { + stop("Least-squares tree search needs at least four tips") + } + + starts <- if (is.null(tree)) { + list(ape::nj(stats::as.dist(D))) + } else if (inherits(tree, "phylo")) { + list(tree) + } else { + # multiPhylo, possibly stored in compressed (.compressTipLabel) form where + # components carry no `tip.label`. Index with `[[`, whose multiPhylo method + # restores the shared labels; `as.list()` would bypass it and yield + # label-less trees. + lapply(seq_along(tree), function(i) tree[[i]]) + } + + best <- NULL + bestRSS <- Inf + for (start in starts) { + prepped <- .LSPrepTree(start, labs) + tl <- prepped[["tip.label"]] + Dord <- D[tl, tl, drop = FALSE] + Word <- if (is.null(W)) NULL else W[tl, tl, drop = FALSE] + + res <- ts_ls_search(prepped[["edge"]], Dord, Word, methodCode, + as.integer(maxHits), isTRUE(spr)) + # Keep the first result unconditionally so a singular fit (RSS = Inf, e.g. + # a weighting that leaves a branch unidentifiable) still yields a tree + # rather than NULL; better fits replace it. + if (is.null(best) || res[["rss"]] < bestRSS) { + bestRSS <- res[["rss"]] + best <- .LSFinalize(res[["edge"]], res[["edge_length"]], res[["rss"]], tl) + } + } + if (!is.finite(bestRSS)) { + warning("Least-squares fit was singular for every starting tree; ", + "branch lengths are unreliable. Check for zero weights/distances.") + } + best +} diff --git a/R/MaximizeParsimony.R b/R/MaximizeParsimony.R index ef6d237cc..163676b7f 100644 --- a/R/MaximizeParsimony.R +++ b/R/MaximizeParsimony.R @@ -1,128 +1,299 @@ +# Internal helper: count non-missing taxa per character pattern. +# Used by XPIWE (Goloboff 2014) to compute the extrapolation factor. +# @param dataset A phyDat object. +# @return Integer vector of length = number of unique patterns. +# @keywords internal +.ObsCount <- function(dataset) { + at <- attributes(dataset) + contrast <- at$contrast + levels <- at$levels + # "?" = all-1s contrast row. + is_missing <- apply(contrast, 1, function(row) all(row == 1)) + # "-" (inapplicable/gap) also counts as missing for XPIWE (Goloboff 2014). + # TNT counts both ? and - as missing, verified against TNT 1.6. + inapp_col <- match("-", levels) + if (!is.na(inapp_col)) { + is_inapp <- apply(contrast, 1, function(row) { + row[inapp_col] == 1 && sum(row) == 1 + }) + is_missing <- is_missing | is_inapp + } + # dataset is a list of integer vectors (token indices, 1-based) per taxon. + # tip_data: n_taxa x n_patterns matrix + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) + # Count non-missing taxa per pattern + vapply(seq_len(ncol(tip_data)), function(p) { + sum(!is_missing[tip_data[, p]]) + }, integer(1)) +} + +# Internal helper: prepare constraint data for C++ engine. +# Returns a named list of constraint arguments (empty list if no constraint). +# @param constraint A phyDat, phylo, or NULL. +# @param dataset A phyDat whose names define the tip ordering. +# @keywords internal +.PrepareConstraint <- function(constraint, dataset) { + if (is.null(constraint)) return(list()) + + if (inherits(constraint, "phylo")) { + constraint <- MatrixToPhyDat(t(as.matrix(constraint))) + } + if (!inherits(constraint, "phyDat")) { + constraint <- MatrixToPhyDat(constraint) + } + + # Match constraint taxa to dataset + consTaxa <- names(constraint) + treeTaxa <- names(dataset) + treeOnly <- setdiff(treeTaxa, consTaxa) + if (length(treeOnly)) { + constraint <- AddUnconstrained(constraint, treeOnly) + } + consOnly <- setdiff(consTaxa, treeTaxa) + if (length(consOnly)) { + warning("Ignoring taxa in constraint missing on tree: ", + paste0(consOnly, collapse = ", ")) + constraint <- constraint[-match(consOnly, consTaxa)] + } + constraint <- constraint[names(dataset)] + + consContrast <- attr(constraint, "contrast") + nConsStates <- ncol(consContrast) + if (nConsStates < 2L) return(list()) + + consMat <- matrix(unlist(constraint, use.names = FALSE), + nrow = length(constraint), byrow = TRUE) + consSplits <- matrix(0L, nrow = ncol(consMat), ncol = length(constraint)) + for (ch in seq_len(ncol(consMat))) { + for (tip in seq_len(length(constraint))) { + token <- consMat[tip, ch] + if (consContrast[token, nConsStates] == 1 && + consContrast[token, 1] == 0) { + consSplits[ch, tip] <- 1L + } + } + } + + keep <- apply(consSplits, 1, function(row) { + s <- sum(row) + s >= 1 && s < length(constraint) - 1 + }) + consSplits <- consSplits[keep, , drop = FALSE] + if (nrow(consSplits) == 0L) return(list()) + + consWeight <- attr(constraint, "weight") + consExpectedScore <- sum( + MinimumLength(constraint, compress = TRUE) * consWeight + ) + + consTipData <- matrix(unlist(constraint, use.names = FALSE), + nrow = length(constraint), byrow = TRUE) + + list( + consSplitMatrix = consSplits, + consContrast = consContrast, + consTipData = consTipData, + consWeight = as.integer(consWeight), + consLevels = attr(constraint, "levels"), + consExpectedScore = as.integer(consExpectedScore) + ) +} + +# Strategy presets for adaptive search (Phase 6E). +# Wrapped in a function to avoid load-order dependency on SearchControl(). +.StrategyPresets <- function() list( + sprint = SearchControl( + tbrMaxHits = 1L, ratchetCycles = 3L, ratchetPerturbProb = 0.04, + ratchetPerturbMode = 0L, ratchetAdaptive = FALSE, + driftCycles = 0L, xssRounds = 1L, xssPartitions = 4L, + rssRounds = 0L, cssRounds = 0L, cssPartitions = 4L, + sectorMinSize = 6L, sectorMaxSize = 50L, + fuseInterval = 5L, fuseAcceptEqual = FALSE, + tabuSize = 0L, wagnerStarts = 1L, + nniFirst = TRUE, sprFirst = FALSE + ), + default = SearchControl( + # ratchetCycles 12->6 (T-P5d, 2026-06-19): profiling found the ratchet + # over-provisioned -- halving cycles saved 20-38% wall on the mid-size EW + # benchmarks (Wills/Zanol/Zhu/Giles) at zero quality loss. Provisional; + # the planned dataset-property grid will confirm across sizes. + tbrMaxHits = 1L, ratchetCycles = 6L, ratchetPerturbProb = 0.25, + ratchetPerturbMode = 0L, ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = FALSE, + driftCycles = 0L, + xssRounds = 3L, xssPartitions = 4L, + rssRounds = 1L, cssRounds = 0L, cssPartitions = 4L, + sectorMinSize = 6L, sectorMaxSize = 50L, + fuseInterval = 3L, fuseAcceptEqual = FALSE, + tabuSize = 100L, wagnerStarts = 3L, + nniFirst = TRUE, sprFirst = FALSE, adaptiveLevel = TRUE, + maxOuterResets = 2L + ), + thorough = SearchControl( + tbrMaxHits = 3L, ratchetCycles = 20L, ratchetPerturbProb = 0.25, + ratchetPerturbMode = 2L, ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = TRUE, + nniPerturbCycles = 0L, # T-274: 69% overhead, zero time-adjusted benefit + driftCycles = 0L, + xssRounds = 5L, xssPartitions = 6L, + rssRounds = 3L, cssRounds = 2L, cssPartitions = 6L, + sectorMinSize = 6L, sectorMaxSize = 80L, + fuseInterval = 2L, fuseAcceptEqual = TRUE, + tabuSize = 200L, wagnerStarts = 3L, + nniFirst = TRUE, sprFirst = FALSE, + outerCycles = 2L, + maxOuterResets = 3L, + adaptiveStart = TRUE + ), + # Opt-in "intensive" preset: `thorough` plus extra Wagner starts for more + # starting-basin diversity. Never auto-selected (.AutoStrategy returns only + # sprint/default/thorough/large); the user opts in with strategy = "intensive". + # Phase-2 sweep (2026-06-16, 5 seeds, EW Fitch): wagnerStarts 3->5 improved the + # hardest datasets (Wortley2006 -3, Zhu2013 -2 toward the TNT optimum) at + # neutral-to-lower candidate cost, with a ~+1-step trade-off on a couple of + # others (Zanol2014, Giles2015) -- hence opt-in rather than a default change. + # NB rasStarts=3 (TNT-faithful per-sector restarts) was evaluated 2026-06-18: + # it closes the rss-ONLY gap (+7/+8 -> +1, wins time-matched) but is REDUNDANT + # in the full thorough pipeline (Zanol/Zhu reach the optimum at rasStarts=1, + # 60s) -- so NOT adopted. Revisit for larger datasets / shorter budgets where + # the full search can't reach the optimum (diag_thorough_rasstarts_tm.R + + # the Hamilton grid t29_thorough_rasstarts_hamilton.sh). + intensive = SearchControl( + tbrMaxHits = 3L, ratchetCycles = 20L, ratchetPerturbProb = 0.25, + ratchetPerturbMode = 2L, ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = TRUE, + nniPerturbCycles = 0L, + driftCycles = 0L, + xssRounds = 5L, xssPartitions = 6L, + rssRounds = 3L, cssRounds = 2L, cssPartitions = 6L, + sectorMinSize = 6L, sectorMaxSize = 80L, + fuseInterval = 2L, fuseAcceptEqual = TRUE, + tabuSize = 200L, wagnerStarts = 5L, + nniFirst = TRUE, sprFirst = FALSE, + outerCycles = 2L, + maxOuterResets = 3L, + adaptiveStart = TRUE + ), + # Large-tree preset (>=120 tips): at 180 tips each TBR convergence takes + # ~5-7s, so phase costs scale sharply. Key design decisions (T-179): + # - Fewer perturbation cycles: ratchet 12, drift 4 (vs thorough 20/12) + # - No NNI-perturbation: at ~5.5s/cycle, it dominates the budget; ratchet + # provides more diverse escapes per unit time at large-tree scale + # - Annealing (1 cycle) replaces drift: linear cooling T=20→0 over 5 + # phases uses stochastic TBR with Boltzmann acceptance — cheaper + # per-cycle than drift. 1 cycle (400ms) captures 40% hit rate at + # 180 tips; 3 cycles (1370ms) showed no significant score gain (T-248) + # - No outer-cycle interleaving: outerCycles=1 avoids re-running expensive + # XSS/RSS/CSS after ratchet (saves ~10s per repeated sectorial pass) + # - Single biased-Wagner start: saves ~2.6s vs 3 random starts; biased + # addition (Goloboff 2014) gives near-optimal Wagner at 180 tips + # - tbrMaxHits=1: faster TBR passes (fewer equal-score trees explored) + # - No adaptiveStart: with ~1 replicate per 60s budget, the bandit has + # no learning opportunity; adaptiveStart empirically regresses here + # - Larger sector sizes for proportional tree coverage + # - Prune-reinsert with NNI polish (T-289f Stage 5, 2026-03-29): 5 cycles, + # NNI full-tree polish (pruneReinsertNni=TRUE). TBR polish (Stage 4) was + # catastrophic at 206t/60s (0 reps). NNI polish (Stage 5, 5 datasets + # 131-206t, 10 seeds, 60s+120s) fixes the 0-rep failure and improves + # median scores at 131-180t (project3701 146t: -178 steps at 60s; + # project804 173t: -9 steps; mbank_X30754 180t: -4 steps at 60s/-7 at + # 120s). syab07205 (206t) shows +17.5 steps at 60s but neutral at 120s + # — acceptable given the gains at smaller sizes in range. See G-006 for + # a known limitation (NNI polish ignores ConstraintData; irrelevant here + # since the large preset does not use topological constraints). + # Validated on mbank_X30754 (180t, 418p), 5 seeds at 30/60/120s budgets: + # 60s: large median=1255 vs thorough 1259 (+4 steps better) + # 120s: large median=1250 vs thorough 1250 (tied, 2 reps vs 0-1) + # 30s: large median=1276 vs thorough 1283 (+7 steps better) + large = SearchControl( + tbrMaxHits = 1L, ratchetCycles = 12L, ratchetPerturbProb = 0.25, + ratchetPerturbMode = 2L, ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = TRUE, + nniPerturbCycles = 0L, + driftCycles = 0L, + annealCycles = 1L, annealPhases = 5L, annealTStart = 20, annealTEnd = 0, + xssRounds = 3L, xssPartitions = 6L, + rssRounds = 2L, cssRounds = 1L, cssPartitions = 6L, + sectorMinSize = 8L, sectorMaxSize = 100L, + fuseInterval = 3L, fuseAcceptEqual = TRUE, + tabuSize = 100L, wagnerStarts = 1L, + wagnerBias = 1L, wagnerBiasTemp = 0.3, + nniFirst = TRUE, sprFirst = FALSE, + outerCycles = 1L, + pruneReinsertCycles = 5L, pruneReinsertNni = TRUE, + consensusStableReps = 0L + ) +) + +# Select strategy preset based on dataset size and character count. +# @param nTip Integer number of taxa +# @param nChar Integer number of character patterns (unique columns) +# @return Character name of the strategy preset +# @details +# Empirically calibrated on 15 neotrans matrices (61-86 tips) + 4 +# inapplicable.phyData datasets. Key findings: +# - Datasets with few characters (< 100 patterns) have flat parsimony +# landscapes where extra search adds zero score improvement (0/6 benefited). +# - Datasets with >= 100 patterns and >= 65 taxa have structured landscapes +# where thorough search finds substantially better trees (7/9 benefited, +# median +14 steps, max +74 steps at 86 tips / 528 chars). +# - At 62 tips (Agnarsson2004, 242 patterns) thorough adds 0 steps; at 65 +# tips (project3617, 361 patterns) it adds 14 steps. +.AutoStrategy <- function(nTip, nChar) { + if (nTip <= 30L) return("sprint") + # Few characters -> flat landscape; thorough search is pointless + if (nChar < 100L) return("default") + # Large trees (>=120 tips): per-replicate cost is high; use scaled preset + # with NNI warmup and biased Wagner (empirically validated on 180-tip data). + if (nTip >= 120L) return("large") + # Enough characters to have a structured landscape; + # moderate-to-large datasets benefit from intensive search + if (nTip >= 65L) return("thorough") + "default" +} + #' Find most parsimonious trees -#' -#' Search for most parsimonious trees using the parsimony ratchet and -#' \acronym{TBR} rearrangements, treating inapplicable data as such using the -#' algorithm of \insertCite{Brazeau2019;textual}{TreeSearch}. -#' -#' Tree search will be conducted from a specified or automatically-generated -#' starting tree in order to find a tree with an optimal parsimony score, -#' under implied or equal weights, treating inapplicable characters as such -#' in order to avoid the artefacts of the standard Fitch algorithm -#' \insertCite{@see @Maddison1993; @Brazeau2019}{TreeSearch}. -#' Tree length is calculated using the MorphyLib C library -#' \insertCite{Brazeau2017}{TreeSearch}. -#' -#' Tree search commences with `ratchIter` iterations of the parsimony ratchet -#' \insertCite{Nixon1999}{TreeSearch}, which bootstraps the input dataset -#' in order to escape local optima. -#' A final round of tree bisection and reconnection (\acronym{TBR}) -#' is conducted to broaden the sampling of trees. -#' -#' This function can be called using the R command line / terminal, or through -#' the "shiny" graphical user interface app (type `EasyTrees()` to launch). -#' -#' The optimal strategy for tree search depends in part on how close to optimal -#' the starting tree is, the size of the search space (which increases -#' super-exponentially with the number of leaves), and the complexity of the -#' search space (e.g. the existence of multiple local optima). -#' -#' One possible approach is to employ four phases: -#' -#' 1. Rapid search for local optimum: tree score is typically easy to improve -#' early in a search, because the initial tree is often far from optimal. -#' When many moves are likely to be accepted, running several rounds of search -#' with a low value of `maxHits` and a high value of `tbrIter` allows many -#' trees to be evaluated quickly, hopefully moving quickly to a more promising -#' region of tree space. -#' -#' 2. Identification of local optimum: -#' Once close to a local optimum, a more extensive search -#' with a higher value of `maxHits` allows a region to be explored in more -#' detail. Setting a high value of `tbrIter` will search a local -#' neighbourhood more completely -#' -#' 3. Search for nearby peaks: -#' Ratchet iterations allow escape from local optima. -#' Setting `ratchIter` to a high value searches the wider neighbourhood more -#' extensively for other nearby peaks; `ratchEW = TRUE` accelerates these -#' exploratory searches. Ratchet iterations can be ineffective when `maxHits` -#' is too low for the search to escape its initial location. -#' -#' 4. Extensive search of final optimum. As with step 2, it may be valuable to -#' fully explore the optimum that is found after ratchet searches to be sure -#' that the locally optimal score has been obtained. Setting a high value of -#' `finalIter` performs a thorough search that can give confidence that further -#' searches would not find better (local) trees. -#' -#' A search is unlikely to have found a global optimum if: -#' -#' - Tree score continues to improve on the final iteration. If a local optimum -#' has not yet been reached, it is unlikely that a global optimum has -#' been reached. -#' Try increasing `maxHits`. -#' -#' - Successive ratchet iterations continue to improve tree scores. -#' If a recent ratchet iteration improved the score, rather than finding -#' a different region of tree space with the same optimal score, it is likely -#' that still better global optima remain to be found. Try increasing -#' `ratchIter` (more iterations give more chance for improvement) and -#' `maxHits` (to get closer to the local optimum after each ratchet iteration). -#' -#' - Optimal areas of tree space are only visited by a single ratchet iteration. -#' (See vignette: [Exploring tree space]( -#' https://ms609.github.io/TreeSearch/articles/tree-space.html).) -#' If some areas of tree space are only found by one ratchet iteration, there -#' may well be other, better areas that have not yet been visited. -#' Try increasing `ratchIter`. -#' -#' When continuing a tree search, it is usually best to start from an optimal -#' tree found during the previous iteration - there is no need to start from -#' scratch. -#' -#' A more time consuming way of checking that a global optimum has been reached -#' is to repeat a search with the same parameters multiple times, starting -#' from a different, entirely random tree each time. If all searches obtain the -#' same optimal tree score despite their different starting points, -#' this score is likely to correspond to the global optimum. -#' -#' For detailed documentation of the "TreeSearch" package, including full -#' instructions for loading phylogenetic data into R and initiating and -#' configuring tree search, see the -#' [package documentation](https://ms609.github.io/TreeSearch/). -#' -#' +#' +#' Performs a multi-replicate driven search for most-parsimonious trees, +#' combining random addition sequence (Wagner) starting trees, TBR +#' rearrangement, exclusive sectorial search (XSS), ratchet perturbation, +#' drift, and tree fusing -- all in compiled C++. +#' +#' The search pipeline follows the "new technology search" approach of +#' \insertCite{Goloboff1999;textual}{TreeSearch}, as implemented in TNT +#' \insertCite{Goloboff2016}{TreeSearch}. +#' Parsimony scoring uses the Fitch +#' \insertCite{Fitch1971}{TreeSearch} algorithm; inapplicable characters +#' are handled with the algorithm of +#' \insertCite{Brazeau2019;textual}{TreeSearch}. +#' Each replicate builds a random addition sequence (Wagner) tree +#' \insertCite{Kluge1969}{TreeSearch}, optimizes it with TBR, +#' applies sectorial search and the parsimony ratchet +#' \insertCite{Nixon1999}{TreeSearch} to escape local optima, then adds +#' the result to a pool of unique topologies. +#' Periodically, tree fusing recombines the best trees in the pool. +#' The search stops when the best score has been independently discovered +#' `targetHits` times, or `maxReplicates` replicates have been completed. +#' +#' Implied weighting is supported natively: set `concavity` to a numeric +#' value (e.g.\sspace{}10). +#' Profile parsimony (`concavity = "profile"`) is supported natively: +#' characters are simplified to binary (max 2 informative states), +#' inapplicable tokens are treated as ambiguous, and per-character +#' information profiles are used for scoring +#' \insertCite{Faith2001}{TreeSearch}. +#' #' @param dataset A phylogenetic data matrix of \pkg{phangorn} class #' \code{phyDat}, whose names correspond to the labels of any accompanying tree. -#' Perhaps load into R using \code{\link[TreeTools]{ReadAsPhyDat}()}. -#' Additive (ordered) characters can be handled using -#' \code{\link[TreeTools]{Decompose}()}. #' @param tree (optional) A bifurcating tree of class \code{\link[ape]{phylo}}, -#' containing only the tips listed in `dataset`, from which the search -#' should begin. -#' If unspecified, an [addition tree][AdditionTree()] will be generated from -#' `dataset`, respecting any supplied `constraint`. -#' Edge lengths are not supported and will be deleted. -#' @param ratchIter Numeric specifying number of iterations of the -#' parsimony ratchet \insertCite{Nixon1999}{TreeSearch} to conduct. -#' @param tbrIter Numeric specifying the maximum number of \acronym{TBR} -#' break points on a given tree to evaluate before terminating the search. -#' One "iteration" comprises selecting a branch to break, and evaluating -#' each possible reconnection point in turn until a new tree improves the -#' score. If a better score is found, then the counter is reset to zero, -#' and tree search continues from the improved tree. -#' @param startIter Numeric: an initial round of tree search with -#' `startIter` × `tbrIter` \acronym{TBR} break points is conducted in -#' order to locate a local optimum before beginning ratchet searches. -#' @param finalIter Numeric: a final round of tree search will evaluate -#' `finalIter` × `tbrIter` \acronym{TBR} break points, in order to -#' sample the final optimal neighbourhood more intensely. -#' @param maxHits Numeric specifying the maximum times that an optimal -#' parsimony score may be hit before concluding a ratchet iteration or final -#' search concluded. -#' @param maxTime Numeric: after `maxTime` minutes, stop tree search at the -#' next opportunity. -#' @param quickHits Numeric: iterations on subsampled datasets -#' will retain `quickHits` × `maxHits` trees with the best score. +#' or a `multiPhylo` (first tree used). +#' When supplied, the first replicate uses this topology as its starting +#' point (warm-start), skipping the random Wagner tree construction. +#' Subsequent replicates still begin from random Wagner trees. +#' This is useful for continuing a search from a previously found optimum. +#' If unspecified, all replicates start from random Wagner trees. +#' Edge lengths are not supported and will be deleted. #' @param concavity Determines the degree to which extra steps beyond the first #' are penalized. Specify a numeric value to use implied weighting #' \insertCite{Goloboff1993}{TreeSearch}; `concavity` specifies _k_ in @@ -131,893 +302,658 @@ #' \insertCite{Goloboff2018,Smith2019}{TreeSearch}. #' Better still explore the sensitivity of results under a range of #' concavity values, e.g. `k = 2 ^ (1:7)`. -#' Specify `Inf` to weight each additional step equally, -#' (which underperforms step weighting approaches -#' \insertCite{Goloboff2008,Goloboff2018,Goloboff2019,Smith2019}{TreeSearch}). -#' Specify `"profile"` to employ an approximation of profile parsimony +#' Specify `Inf` to weight each additional step equally. +#' Specify `"profile"` to employ profile parsimony #' \insertCite{Faith2001}{TreeSearch}. -#' @param ratchEW Logical specifying whether to use equal weighting during -#' ratchet iterations, improving search speed whilst still facilitating -#' escape from local optima. -#' @param tolerance Numeric specifying degree of suboptimality to tolerate -#' before rejecting a tree. The default, `sqrt(.Machine$double.eps)`, retains -#' trees that may be equally parsimonious but for rounding errors. -#' Setting to larger values will include trees suboptimal by up to `tolerance` -#' in search results, which may improve the accuracy of the consensus tree -#' (at the expense of resolution) \insertCite{Smith2019}{TreeSearch}. +#' @param extended_iw Logical: if `TRUE` (default) and `concavity` is finite, +#' apply the missing-entries correction of +#' \insertCite{Goloboff2014;textual}{TreeSearch}. +#' Characters with missing data receive a reduced effective concavity +#' _k_c_ = _k_ / _f_c_, making their weights drop off faster. +#' This compensates for the artificially low homoplasy of poorly sampled +#' characters. Set `FALSE` for legacy Goloboff (1993) behaviour. +#' Ignored when `concavity = Inf` (equal weights) or `"profile"`. +#' @param xpiwe_r Numeric in (0, 1]: proportion of observed homoplasy +#' expected in unobserved (missing) entries. Default 0.5 (following TNT). +#' Only used when `extended_iw = TRUE`. +#' @param xpiwe_max_f Numeric >= 1: maximum extrapolation factor. +#' Characters with very few observed entries are clamped so that the +#' extrapolation factor does not exceed this value. Default 5 (following +#' TNT). Only used when `extended_iw = TRUE`. +#' @param hierarchy A [`CharacterHierarchy`] object specifying which +#' characters are controlling primaries and which are their dependent +#' secondaries. Required when `inapplicable` is `"hsj"` or `"xform"`; +#' ignored when `inapplicable = "bgs"` (the default). +#' See [`CharacterHierarchy()`] for how to construct one, and +#' [`HierarchyFromNames()`] for automated construction from +#' TNT-style character names. +#' @param inapplicable Character: method for handling inapplicable characters. +#' Case-insensitive. +#' See `vignette("inapplicable", package = "TreeSearch")` for details. +#' \describe{ +#' \item{`"bgs"` (default)}{Three-pass algorithm of +#' \insertCite{Brazeau2019;textual}{TreeSearch}, inferring applicability +#' regions from the `"-"` token. No hierarchy required.} +#' \item{`"hsj"`}{Dissimilarity-metric scoring of +#' \insertCite{Hopkins2021;textual}{TreeSearch}. Requires a +#' `hierarchy`; controlled by `hsj_alpha`.} +#' \item{`"xform"`}{Step-matrix recoding approximating maximum homology +#' via x-transformations +#' \insertCite{Goloboff2021;textual}{TreeSearch}. Requires a +#' `hierarchy`.} +#' } +#' @param hsj_alpha Numeric in \[0, 1\]: scaling parameter for secondary- +#' character contributions under the HSJ method. 0 = secondaries ignored; +#' 1 (default) = secondaries contribute up to 1 per branch per hierarchy +#' block. Only used when `inapplicable = "hsj"`. #' @param constraint Either an object of class `phyDat`, in which case #' returned trees will be perfectly compatible with each character in #' `constraint`; or a tree of class `phylo`, all of whose nodes will occur #' in any output tree. -#' See \code{\link[TreeTools:ImposeConstraint]{ImposeConstraint()}} and -#' [vignette](https://ms609.github.io/TreeSearch/articles/tree-search.html) -#' for further examples. +#' Constraint searches are supported natively: all tree rearrangements +#' are filtered to respect the constraint topology. +#' @param strategy Character: named strategy preset controlling the search +#' heuristic parameters. Presets: +#' \describe{ +#' \item{`"auto"` (default)}{Selects automatically based on dataset size +#' and character count: +#' `"sprint"` for <=30 taxa; `"large"` for >=120 taxa with >=100 +#' character patterns; `"thorough"` for 65-119 taxa with >=100 +#' character patterns; `"default"` otherwise.} +#' \item{`"sprint"`}{Fast search: 3 ratchet cycles, no drift, minimal +#' sectorial. Good for small datasets or quick surveys.} +#' \item{`"default"`}{Balanced: 12 ratchet + sectorial + fusing.} +#' \item{`"thorough"`}{Intensive: 20 ratchet cycles, adaptive +#' perturbation, extra sectorial rounds, NNI perturbation, outer cycle +#' loop. Best for datasets with 65-119 tips and 100+ character patterns.} +#' \item{`"large"`}{Large-tree search (>=120 tips): reduced cycle +#' counts scaled for expensive per-replicate cost, no NNI +#' perturbation, single biased Wagner start (Goloboff 2014), larger +#' sector sizes, 1-cycle simulated annealing instead of drift +#' (linear cooling from T=20 to T=0 over 5 phases). Empirically matches +#' or exceeds `"thorough"` at 180 tips across all time budgets.} +#' \item{`"intensive"`}{Opt-in (never auto-selected): `"thorough"` plus extra +#' Wagner starts (5) for more starting-basin diversity. Improves the +#' hardest datasets by a few steps at neutral-to-lower candidate cost, with +#' an occasional ~+1-step trade-off elsewhere; choose it explicitly when +#' pushing for the shortest tree on a difficult matrix.} +#' \item{`"none"`}{Use only the explicitly supplied parameter values.} +#' } +#' Presets stop on `targetHits` and the `perturbStopFactor` no-improvement +#' rule; `consensusStableReps` (consensus-stability stopping) is off by default +#' and is not enabled by any preset. +#' Explicit `control` fields always override the preset; for example, +#' `strategy = "sprint", control = SearchControl(ratchetCycles = 10L)` uses +#' sprint defaults for everything except `ratchetCycles`. +#' @param maxReplicates Integer: maximum number of independent search +#' replicates (default: 96). +#' The default is a multiple of 48 (= LCM(12, 16)) so that replicates +#' divide evenly across common 12- or 16-core machines when running in +#' parallel. +#' For large or complex datasets a higher value improves the chance of +#' finding all MPTs. A rough minimum is +#' `max(10, ceiling(NTip * NChar / 5000))`, where `NChar = sum(weight)`. +#' A warning is issued when an explicit value falls below this threshold +#' for datasets with 30 or more taxa. +#' @param targetHits Integer: stop when the best score has been found +#' independently this many times (default: `max(10, NTip / 5)`). +#' @param maxSeconds Numeric: maximum wall-clock time in seconds for the +#' search. When reached, the current replicate finishes and the search +#' stops. `0` (default) means no time limit. +#' @param nThreads Integer: number of parallel threads for search replicates. +#' \describe{ +#' \item{`1` (default)}{Serial execution -- identical to previous behaviour.} +#' \item{`0`}{Auto-detect: use one fewer thread than the number of CPU +#' cores.} +#' \item{`> 1`}{Use the specified number of worker threads.} +#' } +#' In parallel mode, each replicate runs independently with a shared tree +#' pool. Results may vary across runs with the same `set.seed()` due to +#' thread scheduling nondeterminism. Use `nThreads = 1` for reproducible +#' results. #' @param verbosity Integer specifying level of messaging; higher values give -#' more detailed commentary on search progress. Set to `0` to run silently. -#' @param \dots Additional parameters to `MaximizeParsimony()`. -#' -#' @return `MaximizeParsimony()` returns a list of trees with class -#' `multiPhylo`. This lists all trees found during each search step that -#' are within `tolerance` of the optimal score, listed in the sequence that -#' they were first visited, and named according to the step in which they were -#' first found; it may contain more than `maxHits` elements. -#' Note that the default search parameters may need to be increased in order for -#' these trees to be the globally optimal trees; examine the messages printed -#' during tree search to evaluate whether the optimal score has stabilized. -#' -#' The return value has the attribute `firstHit`, a named integer vector listing -#' the number of optimal trees visited for the first time in each stage of -#' the tree search. Stages are named: -#' - `seed`: starting trees; -#' - `start`: Initial TBR search; -#' - `ratchN`: Ratchet iteration `N`; -#' - `final`: Final TBR search. -#' The first tree hit for the first time in ratchet iteration three is named -#' `ratch3_1`. -#' +#' more detail. Set to `0` to run silently. +#' @param progressCallback Optional function called with a single list +#' argument containing search progress information. +#' The list includes elements: `replicate`, `max_replicates`, +#' `best_score`, `hits_to_best`, `target_hits`, `pool_size`, +#' `phase` (character), `elapsed` (seconds), and `phase_score`. +#' When `NULL` (default) and `verbosity >= 1` in an interactive session, +#' a `cli` progress bar is created automatically. +#' Supply a custom function (e.g. using [shiny::setProgress()]) +#' to control progress display. +#' @param control A [`SearchControl`] object (or a named list) of low-level +#' search parameters. Most users can rely on the `strategy` presets and +#' ignore this argument; see [`SearchControl()`] for full documentation +#' of individual fields. +#' @param ... Backward compatibility: individual control parameters (e.g. +#' `ratchetCycles = 10L`) may still be passed as named arguments. +#' These override the corresponding `control` fields and the strategy +#' preset. +#' Legacy `Morphy()`-style parameters (e.g. `ratchIter`, `tbrIter`) are +#' detected and forwarded to [`Morphy()`] with a deprecation warning. +#' +#' @return A `multiPhylo` object containing the best tree(s) found, with +#' attributes: +#' \describe{ +#' \item{`score`}{Best parsimony score.} +#' \item{`replicates`}{Number of replicates completed.} +#' \item{`hits_to_best`}{Number of independent discoveries of the best +#' score.} +#' \item{`n_topologies`}{Number of distinct topologies in the pool at the +#' best score.} +#' \item{`last_improved_rep`}{1-based index of the replicate that last +#' improved the best score (0 if not tracked, e.g. parallel search).} +#' \item{`timed_out`}{Logical: `TRUE` if the search stopped because +#' `maxSeconds` was exceeded.} +#' \item{`consensus_stable`}{Logical: `TRUE` if the search stopped +#' because the strict consensus was unchanged for +#' `consensusStableReps` consecutive replicates.} +#' \item{`perturb_stop`}{Logical: `TRUE` if the search stopped because +#' `nTip * perturbStopFactor` consecutive replicates failed to improve +#' the best score (see [`SearchControl()`]).} +#' \item{`timings`}{Named numeric vector of cumulative wall-clock time +#' (in milliseconds) spent in each search phase across all replicates: +#' `wagner_ms`, `tbr_ms`, `xss_ms`, `rss_ms`, `css_ms`, `ratchet_ms`, +#' `drift_ms`, `final_tbr_ms`, `fuse_ms`.} +#' \item{`replicate_scores`}{Numeric vector of the best parsimony score +#' found by each completed replicate. Passed to [ScoreSpectrum()] for +#' Chao1-style landscape coverage estimation.} +#' \item{`candidates_evaluated`}{Number of TBR/SPR-class candidate +#' rearrangements evaluated across the whole search — the analogue of +#' TNT's "rearrangements examined", useful for comparing search +#' efficiency (candidates per unit of score improvement). Counted only +#' for single-threaded searches (`0` when `nThreads > 1`); excludes +#' NNI-warmup and simulated-annealing candidates.} +#' } +#' #' @examples -#' ## Only run examples in interactive R sessions -#' if (interactive()) { -#' # launch "shiny" point-and-click interface -#' EasyTrees() -#' -#' # Here too, use the "continue search" function to ensure that tree score -#' # has stabilized and a global optimum has been found -#' } -#' -#' -#' # Load data for analysis in R -#' library("TreeTools") #' data("inapplicable.phyData", package = "TreeSearch") -#' dataset <- inapplicable.phyData[["Asher2005"]] -#' -#' # A very quick run for demonstration purposes -#' trees <- MaximizeParsimony(dataset, ratchIter = 0, startIter = 0, -#' tbrIter = 1, maxHits = 4, maxTime = 1/100, -#' concavity = 10, verbosity = 4) -#' names(trees) -#' cons <- Consensus(trees) +#' dataset <- inapplicable.phyData[["Vinther2008"]] +#' result <- MaximizeParsimony(dataset, maxReplicates = 3L, targetHits = 2L) +#' result +#' attr(result, "score") #' -#' # In actual use, be sure to check that the score has converged on a global -#' # optimum, conducting additional iterations and runs as necessary. -#' -#' if (interactive()) { -#' # Jackknife resampling -#' nReplicates <- 10 -#' jackTrees <- replicate(nReplicates, -#' #c() ensures that each replicate returns a list of trees -#' c(Resample(dataset, trees, ratchIter = 0, tbrIter = 2, startIter = 1, -#' maxHits = 5, maxTime = 1 / 10, -#' concavity = 10, verbosity = 0)) -#' ) -#' -#' # In a serious analysis, more replicates would be conducted, and each -#' # search would undergo more iterations. -#' -#' # Now we must decide what to do with the multiple optimal trees from -#' # each replicate. -#' -#' # Set graphical parameters for plotting -#' oPar <- par(mar = rep(0, 4), cex = 0.9) -#' -#' # Take the strict consensus of all trees for each replicate -#' # (May underestimate support) -#' JackLabels(cons, lapply(jackTrees, ape::consensus)) -#' -#' # Take a single tree from each replicate (here, the first) -#' # Potentially problematic if chosen tree is not representative -#' JackLabels(cons, lapply(jackTrees, `[[`, 1)) -#' -#' # Count iteration as support if all most parsimonious trees support a split; -#' # as contradiction if all trees contradict it; don't include replicates where -#' # not all trees agree on the resolution of a split. -#' labels <- JackLabels(cons, jackTrees) -#' -#' # How many iterations were decisive for each node? -#' attr(labels, "decisive") -#' -#' # Show as proportion of decisive iterations -#' JackLabels(cons, jackTrees, showFrac = TRUE) -#' -#' # Restore graphical parameters -#' par(oPar) -#' } -#' -#' # Tree search with a constraint -#' constraint <- MatrixToPhyDat(c(a = 1, b = 1, c = 0, d = 0, e = 0, f = 0)) -#' characters <- MatrixToPhyDat(matrix( -#' c(0, 1, 1, 1, 0, 0, -#' 1, 1, 1, 0, 0, 0), ncol = 2, -#' dimnames = list(letters[1:6], NULL))) -#' MaximizeParsimony(characters, constraint = constraint, verbosity = 0) -#' #' @template MRS -#' -#' @importFrom cli cli_alert cli_alert_danger cli_alert_info cli_alert_success -#' cli_alert_warning cli_h1 -#' cli_progress_bar cli_progress_done cli_progress_update -#' @importFrom fastmatch fmatch -#' @importFrom stats runif -#' @importFrom TreeTools -#' AddUnconstrained -#' CharacterInformation -#' ConstrainedNJ -#' DropTip -#' ImposeConstraint -#' MakeTreeBinary -#' MatrixToPhyDat -#' NTip +#' @family tree scoring +#' @seealso [`Morphy()`] for fine-grained control over the R-level search loop. +#' [`Resample()`] for jackknife and bootstrap resampling. +#' [`SearchControl()`] for expert-level tuning of the search heuristics. #' @references #' \insertAllCited{} -#' @seealso -#' Tree search _via_ graphical user interface: [`EasyTrees()`] -#' +#' @importFrom TreeTools NTip RandomTree Renumber RenumberTips RootTree +#' @importFrom TreeTools MakeTreeBinary Preorder +#' @importFrom cli cli_alert_success cli_alert_info cli_alert_warning #' @encoding UTF-8 #' @export -MaximizeParsimony <- function(dataset, tree, - ratchIter = 7L, - tbrIter = 2L, - startIter = 2L, finalIter = 1L, - maxHits = NTip(dataset) * 1.8, - maxTime = 60, - quickHits = 1 / 3, - concavity = Inf, - ratchEW = TRUE, - tolerance = sqrt(.Machine[["double.eps"]]), - constraint, - verbosity = 3L) { - - ### User messaging functions ### - .Message <- function (level, ...) { - if (level < verbosity) { - cli_alert(paste0(...)) - } +MaximizeParsimony <- function( + dataset, + tree, + concavity = Inf, + extended_iw = TRUE, + xpiwe_r = 0.5, + xpiwe_max_f = 5, + hierarchy = NULL, + inapplicable = "bgs", + hsj_alpha = 1.0, + constraint, + strategy = "auto", + maxReplicates = 96L, + targetHits = NULL, + maxSeconds = 0, + nThreads = 1L, + verbosity = 1L, + progressCallback = NULL, + control = SearchControl(), + ... +) { + + # --- Input validation: check dataset first --- + if (is.null(dataset)) { + stop("`dataset` cannot be NULL.") } - .Heading <- function (text, ...) { - if (0 < verbosity) { - cli_h1(text) - if (length(list(...))) { - cli_alert(paste0(...)) - } - } + + # --- Set targetHits default if not provided --- + if (is.null(targetHits)) { + targetHits <- max(10L, as.integer(NTip(dataset) / 5)) } - .Info <- function (level, ...) { - if (level < verbosity) { - cli_alert_info(paste0(...)) + + # --- Backward compatibility: intercept maxTime → maxSeconds --- + dots <- list(...) + if ("maxTime" %in% names(dots)) { + if (missing(maxSeconds) || maxSeconds == 0) { + maxSeconds <- as.double(dots[["maxTime"]]) } + .Deprecated(msg = paste0( + "Use `maxSeconds` instead of `maxTime` in MaximizeParsimony().\n", + " `maxTime` was a Morphy()-style parameter; `maxSeconds` is the ", + "equivalent for the new C++ search engine." + )) + dots[["maxTime"]] <- NULL + } + + # --- Backward compatibility: detect Morphy()-style parameters --- + .morphyParams <- c("ratchIter", "tbrIter", "startIter", "finalIter", + "maxHits", "quickHits", "ratchEW", + "tolerance") + legacyHits <- intersect(names(dots), .morphyParams) + if (length(legacyHits)) { + .Deprecated( + "Morphy", + msg = paste0( + "Parameter", if (length(legacyHits) > 1L) "s", " ", + paste0(sQuote(legacyHits), collapse = ", "), + " belong", if (length(legacyHits) == 1L) "s", " to `Morphy()`,", + " not the new `MaximizeParsimony()`.\n", + " Delegating to `Morphy()`. ", + "Please update your code to call `Morphy()` directly ", + "or use the new MaximizeParsimony() parameters.\n", + " See ?Morphy and ?MaximizeParsimony for details." + ) + ) + morphyArgs <- dots + morphyArgs$dataset <- dataset + if (!missing(tree) && !is.null(tree)) morphyArgs$tree <- tree + if (!missing(concavity)) morphyArgs$concavity <- concavity + if (!missing(constraint)) morphyArgs$constraint <- constraint + if (!missing(verbosity)) morphyArgs$verbosity <- verbosity + return(do.call(Morphy, morphyArgs)) + } + + # --- Resolve control: merge control + ... overrides --- + # Coerce a plain list to SearchControl + if (!inherits(control, "SearchControl")) { + control <- do.call(SearchControl, control) } - .Success <- function (level, ...) { - if (level < verbosity) { - cli_alert_success(paste0(...)) + + # Named ... args that match SearchControl fields override `control` + controlFields <- names(SearchControl()) + controlDots <- dots[intersect(names(dots), controlFields)] + otherDots <- dots[setdiff(names(dots), controlFields)] + if (length(controlDots)) { + for (nm in names(controlDots)) { + control[[nm]] <- controlDots[[nm]] } } - - ### Tree score functions ### - .EWScore <- function (edge, morphyObj, ...) { - preorder_morphy(edge, morphyObj) - } - - .IWScore <- function (edge, morphyObjs, weight, charSeq, concavity, - minLength, target = Inf) { - morphy_iw(edge, morphyObjs, weight, minLength, charSeq, - concavity, target + epsilon) - } - - # Must have same order of parameters as .IWScore, even though minLength unused - .ProfileScore <- function (edge, morphyObjs, weight, charSeq, profiles, - minLength, target = Inf) { - morphy_profile(edge, morphyObjs, weight, charSeq, profiles, - target + epsilon) - } - - .Score <- function (edge) { - if (length(dim(edge)) == 3L) { - edge <- edge[, , 1] + if (length(otherDots)) { + warning("Unknown arguments ignored: ", + paste0(sQuote(names(otherDots)), collapse = ", ")) + } + + # --- Apply strategy preset --- + if (!is.null(strategy) && !identical(strategy, "none")) { + if (identical(strategy, "auto")) { + strategy <- .AutoStrategy(NTip(dataset), + sum(attr(dataset, "weight"))) } - if (profile) { - .ProfileScore(edge, morphyObjects, startWeights, charSeq, profiles) - } else if (iw) { - .IWScore(edge, morphyObjects, startWeights, charSeq, concavity, minLength) - } else { - preorder_morphy(edge, morphyObj) + preset <- .StrategyPresets()[[strategy]] + if (!is.null(preset)) { + # Determine which control fields the user explicitly set. + # Fields are "explicit" if: + # (a) passed via ... (already merged into control above), OR + # (b) control was explicitly supplied and differs from SearchControl() + defaults <- SearchControl() + explicit_via_dots <- names(controlDots) + explicit_via_control <- if ("control" %in% names(match.call())) { + # User passed control = SearchControl(...) — honour all fields in it + names(control) + } else { + character(0) + } + explicit <- union(explicit_via_dots, explicit_via_control) + + # Apply preset values for any field the user didn't explicitly set + for (nm in names(preset)) { + if (!(nm %in% explicit)) { + control[[nm]] <- preset[[nm]] + } + } + if (verbosity >= 1L) { + cli::cli_alert_info("Strategy: {.strong {strategy}}") + } + } else if (!identical(strategy, "auto")) { + warning("Unknown strategy '", strategy, "'; using default parameters.") } } - - ### Tree search functions ### - .TBRSearch <- function (Score, name, - edge, morphyObjs, weight, - tbrIter, maxHits, - minLength = NULL, charSeq = NULL, concavity = NULL) { - - iter <- 0L - nHits <- 1L - hold <- array(NA, dim = c(dim(edge), max(maxHits * 1.1, maxHits + 10L))) - maxHits <- ceiling(maxHits) - hold[, , 1] <- edge - bestScore <- Score(edge, morphyObjs, weight, charSeq, concavity, minLength) - bestPlusEps <- bestScore + epsilon - cli_progress_bar(name, total = maxHits, - auto_terminate = FALSE, - clear = verbosity < 3L, - format_done = paste0(" - TBR rearrangement at depth {iter}", - " found score {signif(bestScore)}", - " {nHits} time{?s}.")) - - while (iter < tbrIter) { - iter <- iter + 1L - brkOptions <- sample(3:(nTip * 2 - 2)) - .Message(4L, " New TBR iteration (depth ", iter, - ", score ", signif(bestScore), ")") - cli_progress_update(set = 0, total = length(brkOptions)) - - for (brk in brkOptions) { - cli_progress_update(1, status = paste0("D", iter, ", score ", - signif(bestScore), ", hit ", - nHits, ".")) - .Message(7L, " Break ", brk) - moves <- TBRMoves(edge, brk) - improvedScore <- FALSE - nMoves <- length(moves) - moveList <- sample.int(nMoves) - for (i in seq_along(moveList)) { - move <- moves[[moveList[i]]] - if (.Forbidden(move)) { - .Message(10L, " Skipping prohibited topology") - next - } - moveScore <- Score(move, morphyObjs, weight, charSeq, concavity, - minLength, bestPlusEps) - if (moveScore < bestPlusEps) { - edge <- move - if (moveScore < bestScore) { - improvedScore <- TRUE - iter <- 0L - bestScore <- moveScore - bestPlusEps <- bestScore + epsilon - nHits <- 1L - hold[, , 1] <- edge - .Message(5L, " New best score ", signif(bestScore), - " at break ", fmatch(brk, brkOptions), "/", length(brkOptions)) - break - } else { - .Message(6L, " Best score ", signif(bestScore), - " hit again (", nHits, "/", ceiling(maxHits), ")") - nHits <- nHits + 1L - hold[, , nHits] <- edge - if (nHits >= maxHits) break - } - } - # If an early iteration improves the score, a later iteration will - # probably improve it even more; we may as well keep working through - # the list instead of calculating a new one (which takes time) - if (improvedScore && runif(1) < (i / nMoves) ^ 2) break - } - if (nHits >= maxHits) break - pNextTbr <- (fmatch(brk, brkOptions) / length(brkOptions)) ^ 2 - if (improvedScore && runif(1) < pNextTbr) break + + # --- Progress callback: build default cli bar if needed --- + if (is.null(progressCallback) && verbosity >= 1L && interactive()) { + pb_env <- new.env(parent = environment()) + pb_env$id <- cli::cli_progress_bar( + total = as.integer(maxReplicates), + format = paste0( + "Rep {cli::pb_current}/{cli::pb_total}", + " | Best: {best}", + " | Hits: {hits}/{target}" + ), + .auto_close = FALSE, + .envir = pb_env + ) + pb_env$best <- "?" + pb_env$hits <- 0L + pb_env$target <- as.integer(targetHits) + progressCallback <- function(info) { + pb_env$best <- signif(info$best_score, 6) + pb_env$hits <- info$hits_to_best + pb_env$target <- info$target_hits + if (identical(info$phase, "done")) { + cli::cli_progress_done(id = pb_env$id, .envir = pb_env) + } else if (identical(info$phase, "replicate")) { + cli::cli_progress_update( + id = pb_env$id, set = info$replicate, .envir = pb_env + ) } - if (nHits >= maxHits) break } - cli_progress_done() - - # Return: - unique(hold[, , seq_len(nHits), drop = FALSE], MARGIN = 3L) - - } - - - .Search <- function (name = "TBR search", .edge = edge, .hits = searchHits, - .weight = startWeights, .forceEW = FALSE) { - if (length(dim(.edge)) == 3L) { - .edge <- .edge[, , 1] + on.exit( + tryCatch( + cli::cli_progress_done(id = pb_env$id, .envir = pb_env), + error = function(e) NULL + ), + add = TRUE + ) + } + + # --- Progress file callback (for Shiny background futures) --- + if (is.null(progressCallback)) { + progressFile <- Sys.getenv("TREESEARCH_PROGRESS_FILE", "") + if (nzchar(progressFile)) { + progressCallback <- function(info) { + if (identical(info$phase, "replicate")) { + tryCatch( + writeLines(paste(info$replicate, info$max_replicates, + signif(info$best_score, 8), info$hits_to_best, + info$target_hits), + progressFile), + error = function(e) NULL + ) + } + } } - .Message(4L, paste("<<< Begin:", name)) - on.exit(.Message(4L, paste(">>> Complete:", name))) - if (profile && isFALSE(.forceEW)) { - .TBRSearch(.ProfileScore, name, edge = .edge, morphyObjects, - tbrIter = searchIter, maxHits = .hits, - weight = .weight, minLength = minLength, charSeq = charSeq, - concavity = profiles) - - } else if (iw && isFALSE(.forceEW)) { - .TBRSearch(.IWScore, name, edge = .edge, morphyObjects, - tbrIter = searchIter, maxHits = .hits, - weight = .weight, minLength = minLength, charSeq = charSeq, - concavity = concavity) + } + + # --- Profile parsimony: prepare data --- + useProfile <- !missing(concavity) && identical(concavity, "profile") + if (useProfile) { + profileApprox <- if (!is.null(dots[["profile_approx"]])) { + dots[["profile_approx"]] } else { - .TBRSearch(.EWScore, name, edge = .edge, morphyObj, - tbrIter = searchIter, maxHits = .hits, - concavity = if(isTRUE(.forceEW)) Inf else concavity) + "auto" } + dataset <- PrepareDataProfile(dataset, approx = profileApprox) + concavity <- Inf # EW on the simplified binary data; profile scores via lookup } - - .Timeout <- function() { - if (Sys.time() > stopTime) { - .Info(1L, "Stopping search at ", .DateTime(), ": ", maxTime, - " minutes have elapsed.", - " Best score was ", signif(.Score(bestEdges[, , 1])), ".", - if (maxTime == 60) "\nIncrease `maxTime` for longer runs.") - return (TRUE) + + # --- Input validation --- + if (!inherits(dataset, "phyDat")) { + stop("`dataset` must be a phyDat object.") + } + + nTip <- length(dataset) + if (nTip < 4L) { + stop("Need at least 4 taxa for tree search.") + } + if (is.null(attr(dataset, "levels")) || ncol(attr(dataset, "contrast")) == 0L) { + stop("Dataset contains no informative character states.") + } + + # --- Validate inapplicable-handling parameters --- + inapplicable <- tolower(inapplicable) + if (inapplicable == "brazeau") inapplicable <- "bgs" + inapplicable <- match.arg(inapplicable, c("bgs", "hsj", "xform")) + if (inapplicable != "bgs") { + if (is.null(hierarchy)) { + stop("A `hierarchy` is required when inapplicable = \"", inapplicable, + "\". See ?CharacterHierarchy.") } - - FALSE - } - - .ReturnValue <- function(bestEdges) { - if (verbosity > 0L) { - cli_alert_success(paste0(.DateTime(), - ": Tree search terminated with score {.strong ", - "{signif(.Score(bestEdges[, , 1]))}}")) + if (!inherits(hierarchy, "CharacterHierarchy")) { + stop("`hierarchy` must be a CharacterHierarchy object.") } - firstHit <- attr(bestEdges, "firstHit") - structure(lapply(seq_len(dim(bestEdges)[3]), function (i) { - tr <- tree - tr[["edge"]] <- bestEdges[, , i] - if (any(is.na(outgroup))) { - tr - } else { - RootTree(tr, outgroup) - } - }), - firstHit = firstHit, - names = paste0(rep(names(firstHit), firstHit), "_", unlist(lapply(firstHit, seq_len))), - class = "multiPhylo") - } - - - # Define constants - epsilon <- tolerance - pNextTbr <- 0.33 - profile <- .UseProfile(concavity) - iw <- is.finite(concavity) - constrained <- !missing(constraint) - startTime <- Sys.time() - stopTime <- startTime + as.difftime(maxTime, units = "mins") - - # Initialize tree - startTrees <- NULL - if (missing(tree)) { - tree <- AdditionTree(dataset, constraint = constraint, - concavity = concavity) + ValidateHierarchy(hierarchy, dataset) + if (useProfile) { + stop("Profile parsimony is not currently supported with inapplicable = \"", + inapplicable, "\".") + } + if (is.finite(concavity)) { + stop("Implied weighting is not currently supported with inapplicable = \"", + inapplicable, "\".") + } + # xform validation is done; recoding happens below + } + if (!is.numeric(hsj_alpha) || length(hsj_alpha) != 1L || + hsj_alpha < 0 || hsj_alpha > 1) { + stop("`hsj_alpha` must be a single number in [0, 1].") + } + if (is.finite(concavity) && concavity <= 0) { + stop("`concavity` must be positive (or Inf for equal weights, ", + "or \"profile\" for profile parsimony).") + } + + # --- Starting tree --- + userTree <- !missing(tree) && !is.null(tree) + if (!userTree) { + tree <- TreeTools::RandomTree(nTip, root = TRUE) + tree[["tip.label"]] <- names(dataset) } else if (inherits(tree, "multiPhylo")) { - startTrees <- unique(tree) - sampledTree <- sample.int(length(tree), 1) - .Info(2L, paste0("Starting search from {.var tree[[", sampledTree, "]]}")) - tree <- tree[[sampledTree]] - } else if (inherits(tree, "phylo")) { - startTrees <- c(tree) - } - if (dim(tree[["edge"]])[1] != 2 * tree[["Nnode"]]) { - cli_alert_warning("`tree` is not bifurcating; collapsing polytomies at random") + tree <- tree[[1L]] + } + if (!inherits(tree, "phylo")) { + stop("`tree` must be of class 'phylo'.") + } + + # Make bifurcating if needed + if (dim(tree[["edge"]])[1] != 2L * tree[["Nnode"]]) { tree <- MakeTreeBinary(tree) - if (dim(tree[["edge"]])[1] != 2 * tree[["Nnode"]]) { - cli_alert_warning("Rooting `tree` on first leaf") - tree <- RootTree(tree, 1) + if (dim(tree[["edge"]])[1] != 2L * tree[["Nnode"]]) { + tree <- RootTree(tree, 1L) } - if (dim(tree[["edge"]])[1] != 2 * tree[["Nnode"]]) { + if (dim(tree[["edge"]])[1] != 2L * tree[["Nnode"]]) { stop("Could not make `tree` binary.") } } - - # Check tree labels matches dataset + + # --- Match tree tips to dataset --- leaves <- tree[["tip.label"]] taxa <- names(dataset) - treeOnly <- setdiff(leaves, taxa) - datOnly <- setdiff(taxa, leaves) + treeOnly <- setdiff(leaves, taxa) + datOnly <- setdiff(taxa, leaves) if (length(treeOnly)) { - cli_alert_warning(paste0("Ignoring taxa on tree missing in dataset:\n> ", - paste0(treeOnly, collapse = ", "))) - warning("Ignored taxa on tree missing in dataset:\n ", - paste0(treeOnly, collapse = ", ")) - tree <- DropTip(tree, treeOnly) - startTrees <- DropTip(startTrees, treeOnly) + warning("Dropping taxa on tree but not in dataset: ", + paste0(treeOnly, collapse = ", ")) + tree <- TreeTools::DropTip(tree, treeOnly) } if (length(datOnly)) { - cli_alert_warning(paste0("Ignoring taxa in dataset missing on tree:\n> ", - paste0(datOnly, collapse = ", "))) - warning("Ignored taxa in dataset missing on tree:\n> ", + warning("Dropping taxa in dataset but not on tree: ", paste0(datOnly, collapse = ", ")) - dataset <- dataset[-fmatch(datOnly, taxa)] - } - if (constrained) { - if (!inherits(constraint, "phyDat")) { - constraint <- MatrixToPhyDat(t(as.matrix(constraint))) - } - consTaxa <- TipLabels(constraint) - treeOnly <- setdiff(tree[["tip.label"]], consTaxa) - if (length(treeOnly)) { - constraint <- AddUnconstrained(constraint, treeOnly) - } - consOnly <- setdiff(consTaxa, tree[["tip.label"]]) - if (length(consOnly)) { - cli_alert_warning( - paste0("Ignoring taxa in constraint missing on tree:\n> ", - paste0(consOnly, collapse = ", "))) - warning("Ignored taxa in constraint missing on tree:\n ", - paste0(consOnly, collapse = ", ")) - constraint <- constraint[-fmatch(consOnly, consTaxa)] - } - constraint <- constraint[names(dataset)] + dataset <- dataset[-match(datOnly, taxa)] } - - + + # Reorder tips to match dataset, put in preorder tree <- Preorder(RenumberTips(tree, names(dataset))) - nTip <- NTip(tree) - edge <- tree[["edge"]] - - # Initialize constraints - if (constrained) { - morphyConstr <- PhyDat2Morphy(constraint) - on.exit(morphyConstr <- UnloadMorphy(morphyConstr), add = TRUE) - constraintWeight <- attr(constraint, "weight") - if (any(constraintWeight > 1)) { - cli_alert_warning("Some constraints are exact duplicates.") - } - # Calculate constraint minimum score - constraintLength <- sum(MinimumLength(constraint, compress = TRUE) * - constraintWeight) - - .Forbidden <- function (edges) { - preorder_morphy(edges, morphyConstr) != constraintLength - } - - # Check that starting tree is consistent with constraints - if (.Forbidden(edge)) { - cli_alert_warning("Modifying `tree` to match `constraint`...") - outgroup <- edge[ - DescendantEdges(parent = edge[, 1], child = edge[, 2])[1, ], - 2] - outgroup <- outgroup[outgroup <= nTip] - tree <- RootTree(ImposeConstraint(tree, constraint), outgroup) - # RootTree leaves `tree` in preorder - edge <- tree[["edge"]] - if (.Forbidden(edge)) { - stop("Could not reconcile starting tree with `constraint`. ", - "Are all constraints compatible?") - } - } - - cli_alert_success(paste0("Initialized ", length(constraintWeight), - " distinct constraints.")) - - } else { - .Forbidden <- function (edges) FALSE - } - - - if (edge[1, 2] > nTip) { - outgroup <- edge[ - DescendantEdges(parent = edge[, 1], child = edge[, 2])[1, ], - 2] - outgroup <- outgroup[outgroup <= nTip] - if (length(outgroup) > nTip / 2L) { - outgroup <- seq_len(nTip)[-outgroup] - } - tree <- RootTree(tree, 1) - edge <- tree[["edge"]] - } else { - outgroup <- NA - } - - # Initialize data - if (profile) { - dataset <- PrepareDataProfile(dataset) - originalLevels <- attr(dataset, "levels") - if ("-" %fin% originalLevels) { - #TODO Fixing this will require updating the counts table cleverly - # Or we could use approximate info amounts, e.g. by treating "-" as - # an extra token - cli_alert_info(paste0("Inapplicable tokens \"-\" treated as ambiguous ", - "\"?\" for profile parsimony")) - cont <- attr(dataset, "contrast") - cont[cont[, "-"] != 0, ] <- 1 - attr(dataset, "contrast") <- cont[, colnames(cont) != "-"] - attr(dataset, "levels") <- originalLevels[originalLevels != "-"] - } - profiles <- attr(dataset, "info.amounts") - } - - if ((!iw && !profile) || # Required for equal weights search - (isTRUE(ratchEW) && ratchIter > 0) # For EW ratchet searches - ) { - morphyObj <- PhyDat2Morphy(dataset) - on.exit(morphyObj <- UnloadMorphy(morphyObj), add = TRUE) - } - - if (iw || profile) { - at <- attributes(dataset) - characters <- PhyToString(dataset, ps = "", useIndex = FALSE, - byTaxon = FALSE, concatenate = FALSE) - startWeights <- at[["weight"]] - minLength <- MinimumLength(dataset, compress = TRUE) - morphyObjects <- lapply(characters, SingleCharMorphy) - on.exit(morphyObjects <- vapply(morphyObjects, UnloadMorphy, integer(1)), - add = TRUE) - - nLevel <- length(at[["level"]]) - nChar <- at[["nr"]] - nTip <- length(dataset) - cont <- at[["contrast"]] - if (is.null(colnames(cont))) colnames(cont) <- as.character(at[["levels"]]) - simpleCont <- ifelse(rowSums(cont) == 1, - apply(cont != 0, 1, function (x) colnames(cont)[x][1]), - "?") - - - unlisted <- unlist(dataset, use.names = FALSE) - tokenMatrix <- matrix(simpleCont[unlisted], nChar, nTip) - charInfo <- apply(tokenMatrix, 1, CharacterInformation) - needsInapp <- rowSums(tokenMatrix == "-") > 2 - inappSlowdown <- 3L # A guess - # Crude estimate of score added per unit processing time - rawPriority <- charInfo / ifelse(needsInapp, inappSlowdown, 1) - priority <- startWeights * rawPriority - informative <- needsInapp | charInfo > 0 - # Will work from end of sequence to start. - charSeq <- seq_along(charInfo)[informative][order(priority[informative])] - 1L - } else { - startWeights <- unlist(MorphyWeights(morphyObj)[1, ]) # exact == approx - } - - # Initialize variables and prepare search - - nHits <- 1L - tbrStart <- startIter > 0 - tbrEnd <- finalIter > 0 - if (is.null(startTrees)) { - bestEdges <- edge - dim(bestEdges) <- c(dim(bestEdges), 1) - bestScore <- .Score(edge) - } else { - starters <- RenumberTips(startTrees, names(dataset)) - startEdges <- vapply(lapply(starters, Preorder), - `[[`, startTrees[[1]][["edge"]], - "edge") - startScores <- apply(startEdges, 3, .Score) - bestScore <- min(startScores) - bestEdges <- startEdges[, , startScores == bestScore, drop = FALSE] - } - nStages <- sum(tbrStart, ratchIter, tbrEnd) - attr(bestEdges, "firstHit") <- c("seed" = dim(bestEdges)[3], - setNames(double(nStages), - c(if(tbrStart) "start", - if(ratchIter > 0) paste0("ratch", seq_len(ratchIter)), - if(tbrEnd) "final"))) - - .Heading(paste0("BEGIN TREE SEARCH (k = ", concavity, ")"), - "Initial score: {.strong {signif(bestScore)} }") - - - # Find a local optimum - - if (tbrStart) { - searchIter <- tbrIter * startIter - searchHits <- maxHits - - .Heading("Find local optimum", - " TBR depth ", as.integer(searchIter), - "; keeping max ", as.integer(searchHits), - " trees; k = ", concavity, ".") - initialScore <- bestScore - - newEdges <- .Search("TBR search 1") - - newBestScore <- .Score(newEdges) - scoreImproved <- newBestScore + epsilon < bestScore - bestEdges <- if (scoreImproved) { - .ReplaceResults(bestEdges, newEdges, 2) - } else { - .CombineResults(bestEdges, newEdges, 2) - } - if (.Timeout()) { - .Info(1L, .DateTime(), ": Timed out with score ", - signif(min(bestScore, newBestScore))) - return(.ReturnValue(bestEdges)) # nocov - } - edge <- bestEdges[, , 1L] - bestScore <- .Score(edge) - if (bestScore < initialScore) { - .Success(2L, "{.strong New best score: {signif(bestScore)} }") - } else { - .Info(1L, .DateTime(), ": Did not beat initial score: ", - "{signif(bestScore)}") - } + + # Ensure root's first child is a tip (for C++ engine compatibility) + if (tree[["edge"]][1L, 2L] > NTip(tree)) { + tree <- RootTree(tree, 1L) } - - searchIter <- tbrIter - searchHits <- maxHits * quickHits - bestPlusEps <- bestScore + epsilon - - - - # Use Parsimony Ratchet to escape local optimum - - if (ratchIter > 0L) { - - .Heading("Escape local optimum", "{ratchIter} ratchet iterations; ", - "TBR depth {ceiling(searchIter)}; ", - "max. {ceiling(searchHits)} hits; ", - "k = {concavity}.") - .Info(1L, "{ .DateTime()}: Score to beat: {.strong {signif(bestScore)}}") - - iter <- 0L - while (iter < ratchIter) { - iter <- iter + 1L - .Message(1L, "Ratchet iteration {iter} @ {(.Time())}", - "; score to beat: {.strong {signif(bestScore)} }") - verbosity <- verbosity - 1L - eachChar <- seq_along(startWeights) - deindexedChars <- rep.int(eachChar, startWeights) - resampling <- tabulate(sample(deindexedChars, replace = TRUE), - length(startWeights)) - if (!isTRUE(ratchEW) && (profile || iw)) { - priority <- resampling * rawPriority - sampled <- informative & resampling > 0 - ratchSeq <- seq_along(charInfo)[sampled][order(priority[sampled])] - 1L - ratchetTrees <- .Search("Bootstrapped search", .weight = resampling) - } else { - errors <- vapply(eachChar, function (i) - mpl_set_charac_weight(i, resampling[i], morphyObj), integer(1)) - if (any(errors)) { # nocov start - stop ("Error resampling morphy object: ", - mpl_translate_error(unique(errors[errors < 0L]))) - } - if (mpl_apply_tipdata(morphyObj) -> error) { - stop("Error applying tip data: ", mpl_translate_error(error)) - } # nocov end - - ratchetTrees <- if (ratchEW) { - .Search("EW Bootstrapped search", .forceEW = TRUE) - } else { - .Search("Bootstrapped search") - } - - errors <- vapply(eachChar, function (i) - mpl_set_charac_weight(i, startWeights[i], morphyObj), integer(1)) - if (any(errors)) stop ("Error resampling morphy object: ", - mpl_translate_error(unique(errors[errors < 0L]))) - if (mpl_apply_tipdata(morphyObj) -> error) { - stop("Error applying tip data: ", mpl_translate_error(error)) - } - } - - verbosity <- verbosity + 1L - ratchetStart <- ratchetTrees[, , sample.int(dim(ratchetTrees)[3], 1)] - ratchStartScore <- .Score(ratchetStart) - .Message(2L, "Obtained new starting tree @ {(.Time())}", - " with score: {signif(ratchStartScore)}") - - # nocov start - if (.Timeout()) { - if (ratchetScore + epsilon < bestScore) { - bestEdges <- .ReplaceResults(bestEdges, ratchetStart, - 1 + tbrStart + iter) - } - return(.ReturnValue(bestEdges)) - } - # nocov end - - ratchetImproved <- .Search("TBR search", .edge = ratchetStart, - .hits = maxHits) - ratchetScore <- .Score(ratchetImproved[, , 1]) - - if (ratchetScore < bestPlusEps) { - if (ratchetScore + epsilon < bestScore) { - .Success(2L, "{.strong New best score}: {signif(ratchetScore)}") - bestScore <- ratchetScore - bestPlusEps <- bestScore + epsilon - bestEdges <- .ReplaceResults(bestEdges, ratchetImproved, - 1 + tbrStart + iter) - edge <- ratchetImproved[, , sample.int(dim(ratchetImproved)[3], 1)] - } else { - .Info(3L, "Hit best score {.strong {signif(bestScore)}} again") - - edge <- ratchetImproved[, , sample.int(dim(ratchetImproved)[3], 1)] - bestEdges <- .CombineResults(bestEdges, ratchetImproved, - 1 + tbrStart + iter) - } - } else { - if (3L < verbosity) { - cli_alert_danger("Did not hit best score {signif(bestScore)}") - } - } - if (.Timeout()) { - return(.ReturnValue(bestEdges)) # nocov - } + + # --- Extract data matrices --- + at <- attributes(dataset) + contrast <- at$contrast + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) + weight <- .ScaleWeight(at$weight) + levels <- at$levels + + # --- Replicate count adequacy check --- + # Warn only when the user explicitly passed maxReplicates. + # Formula: max(10, ceiling(nTip * nChar / 5000)) where nChar = sum(weight). + # Derived from T-069 benchmarks: at 225 taxa / 748 chars a single rep takes + # ~40s and at least ~34 reps are needed to fill the tree pool reliably. + if (!missing(maxReplicates) && nTip >= 30L && verbosity > 0L) { + nChars <- sum(weight) + minReps <- pmax(10L, ceiling(nTip * nChars / 5000L)) + if (maxReplicates < minReps) { + warning( + "With ", nTip, " taxa and ", nChars, " characters, at least ", + minReps, " replicates are recommended for reliable results ", + "(you specified ", maxReplicates, "). ", + "Consider increasing `maxReplicates` or setting `maxSeconds` ", + "to allow more search time.", + call. = FALSE + ) } } - - # Branch breaking - if (tbrEnd) { - searchIter <- tbrIter * finalIter - searchHits <- maxHits - - .Heading("Sample local optimum", - "TBR depth {searchIter}; keeping {searchHits}", - " trees; k = {concavity}") - .Info(1L, .DateTime(), ": Score: ", signif(bestScore)) - finalEdges <- .Search("Final search") - newBestScore <- .Score(finalEdges[, , 1]) - improved <- newBestScore + epsilon < bestScore - bestEdges <- if (improved) { - .ReplaceResults(bestEdges, finalEdges, 1 + tbrStart + ratchIter + 1) - } else { - .CombineResults(bestEdges, finalEdges, 1 + tbrStart + ratchIter + 1) + + # --- Prepare constraint for C++ engine --- + consArgs <- .PrepareConstraint( + constraint = if (!missing(constraint)) constraint, + dataset = dataset + ) + if (length(consArgs) > 0L && verbosity > 0L) { + cli_alert_info("Constraint: {nrow(consArgs$consSplitMatrix)} split{?s}") + } + + # --- Profile parsimony: extract info_amounts --- + profileArgs <- list() + if (useProfile) { + infoAmounts <- attr(dataset, "info.amounts") + if (!is.null(infoAmounts) && length(infoAmounts) > 0L) { + profileArgs$infoAmounts <- infoAmounts } } - - # Return: - .ReturnValue(bestEdges) -} -#' Combine two edge matrices -#' -#' @param x,y 3D arrays, each slice containing an edge matrix from a tree -#' of class `phylo`. `x` should not contain duplicates. -#' @return A single 3D array containing each unique edge matrix from (`x` and) -#' `y`, with a `firstHit` attribute as documented in [`MaximizeParsimony()`]. -#' @template MRS -#' @keywords internal -.CombineResults <- function (x, y, stage) { - xDim <- dim(x) - if (length(xDim) == 2L) { - xDim <- c(xDim, 1L) - } - if (any(duplicated(x, MARGIN = 3L))) { - warning(".CombineResults(x) should not contain duplicates.") - } - - res <- unique(array(c(x, y), dim = xDim + c(0, 0, dim(y)[3])), MARGIN = 3L) - firstHit <- attr(x, "firstHit") - firstHit[stage] <- dim(res)[3] - xDim[3] - attr(res, "firstHit") <- firstHit - - # Return: - res -} + # --- HSJ: prepare hierarchy data for C++ --- + hsjArgs <- list() + useHSJ <- !is.null(hierarchy) && identical(inapplicable, "hsj") + if (useHSJ) { + hsjArgs$hierarchyBlocks <- .HierarchyToBlocks(hierarchy) + hsjArgs$hsjTipLabels <- .BuildTipLabels(dataset) + hsjArgs$hsjAlpha <- as.double(hsj_alpha) + # 0-based token index of the primary's "absent" state (depends on level + # ordering, so computed from the data rather than hard-coded). + hsjArgs$hsjAbsentState <- .HSJAbsentState(dataset) -#' @rdname dot-CombineResults -#' @param old old array of edge matrices with `firstHit` attribute. -#' @param new new array of edge matrices. -#' @param stage Integer specifying element of `firstHit` in which new hits -#' should be recorded. -#' @keywords internal -.ReplaceResults <- function (old, new, stage) { - hit <- attr(old, "firstHit") - hit[] <- 0 - hit[stage] <- dim(new)[3] - structure(new, "firstHit" = hit) -} + # Adjust weights: subtract hierarchy characters so Fitch scores non-hierarchy + adj_weight <- .NonHierarchyWeights(dataset, hierarchy) + weight <- as.integer(adj_weight) + } -.Time <- function() { - format(Sys.time(), "%H:%M:%S") -} + # --- Xform: recode hierarchy into step-matrix characters --- + xformArgs <- list() + useXform <- !is.null(hierarchy) && identical(inapplicable, "xform") + if (useXform) { + recoded <- RecodeHierarchy(dataset, hierarchy) + xformArgs$xformChars <- recoded$sankoff_chars -.DateTime <- function() { - format(Sys.time(), "%Y-%m-%d %T") -} + # Adjust weights: subtract hierarchy characters so Fitch scores non-hierarchy + adj_weight <- .NonHierarchyWeights(dataset, hierarchy) + weight <- as.integer(adj_weight) + } -#' @rdname MaximizeParsimony -#' -#' @param method Unambiguous abbreviation of `jackknife` or `bootstrap` -#' specifying how to resample characters. Note that jackknife is considered -#' to give more meaningful results. -#' -#' @param proportion Numeric between 0 and 1 specifying what proportion of -#' characters to retain under jackknife resampling. -#' -#' @section Resampling: -#' Note that bootstrap support is a measure of the amount of data supporting -#' a split, rather than the amount of confidence that should be afforded the -#' grouping. -#' "Bootstrap support of 100% is not enough, the tree must also be correct" -#' \insertCite{Phillips2004}{TreeSearch}. -#' See discussion in \insertCite{Egan2006;textual}{TreeSearch}; -#' \insertCite{Wagele2009;textual}{TreeSearch}; -#' \insertCite{Simmons2011}{TreeSearch}; -#' \insertCite{Kumar2012;textual}{TreeSearch}. -#' -#' For a discussion of suitable search parameters in resampling estimates, see -#' \insertCite{Muller2005;textual}{TreeSearch}. -#' The user should decide whether to start each resampling -#' from the optimal tree (which may be quicker, but result in overestimated -#' support values as searches get stuck in local optima close to the -#' optimal tree) or a random tree (which may take longer as more rearrangements -#' are necessary to find an optimal tree on each iteration). -#' -#' For other ways to estimate clade concordance, see [`SiteConcordance()`]. -#' -#' @return `Resample()` returns a `multiPhylo` object containing a list of -#' trees obtained by tree search using a resampled version of `dataset`. -#' @family split support functions -#' @encoding UTF-8 -#' @export -Resample <- function(dataset, tree, method = "jack", proportion = 2 / 3, - ratchIter = 1L, tbrIter = 8L, finalIter = 3L, - maxHits = 12L, concavity = Inf, - tolerance = sqrt(.Machine[["double.eps"]]), - constraint, verbosity = 2L, - ...) { - - if (!inherits(dataset, "phyDat")) { - stop("`dataset` must be of class `phyDat`.") - } - - index <- attr(dataset, "index") - kept <- switch(pmatch(tolower(method), c("jackknife", "bootstrap")), - { - nKept <- ceiling(proportion * length(index)) - if (nKept < 1L) { - stop("No characters retained. `proportion` must be positive.") - } - if (nKept == length(index)) { - stop("`proportion` too high; no characters deleted.") - } - sample(index, nKept) - }, { - sample(index, length(index), replace = TRUE) - }) - - if (is.null(kept)) { - stop("`method` must be either \"jackknife\" or \"bootstrap\".") - } - - attr(dataset, "index") <- kept - attr(dataset, "weight") <- vapply(seq_len(attr(dataset, "nr")), - function (x) sum(kept == x), - integer(1)) - - MaximizeParsimony(dataset, tree = tree, - ratchIter = ratchIter, tbrIter = tbrIter, - finalIter = finalIter, - maxHits = maxHits, - concavity = concavity, - tolerance = tolerance, constraint = constraint, - verbosity = verbosity, ...) -} + # --- IW: compute minimum step counts per character --- + if (is.finite(concavity)) { + minSteps <- as.integer(MinimumLength(dataset, compress = TRUE)) + } -#' Launch tree search graphical user interface -#' -#' @rdname MaximizeParsimony -#' @importFrom cluster pam silhouette -#' @importFrom future future -#' @importFrom PlotTools SpectrumLegend -#' @importFrom promises future_promise -#' @importFrom protoclust protoclust -#' @importFrom Rogue ColByStability -#' @importFrom shiny runApp -#' @importFrom shinyjs useShinyjs -#' @importFrom TreeDist ClusteringInfoDistance -#' @export -EasyTrees <- function () {#nocov start - shiny::runApp(system.file("Parsimony", package = "TreeSearch")) + # --- XPIWE: compute per-pattern observed-taxa counts --- + useXpiwe <- isTRUE(extended_iw) && is.finite(concavity) && !useProfile + if (useXpiwe) { + obsCount <- .ObsCount(dataset) + } + + # --- Run C++ driven search --- + # searchControl: the resolved SearchControl object (already type-coerced) + # runtimeConfig: session-level params not in SearchControl + runtimeConfig <- list( + maxReplicates = as.integer(maxReplicates), + targetHits = as.integer(targetHits), + maxSeconds = as.double(maxSeconds), + verbosity = as.integer(verbosity), + nThreads = as.integer(nThreads), + startEdge = if (userTree) tree[["edge"]] else NULL, + progressCallback = progressCallback + ) + + # scoringConfig: scoring method params + scoringConfig <- list( + min_steps = if (is.finite(concavity)) minSteps else integer(0), + concavity = as.double(concavity), + xpiwe = useXpiwe, + xpiwe_r = as.double(xpiwe_r), + xpiwe_max_f = as.double(xpiwe_max_f), + obs_count = if (useXpiwe) obsCount else integer(0), + infoAmounts = profileArgs$infoAmounts + ) + + # constraintConfig / hsjConfig / xformConfig: NULL when empty + constraintConfig <- if (length(consArgs) > 0L) consArgs + hsjConfig <- if (length(hsjArgs) > 0L) hsjArgs + xformConfig <- if (length(xformArgs) > 0L) xformArgs + + result <- ts_driven_search( + contrast, tip_data, weight, levels, + control, runtimeConfig, scoringConfig, + constraintConfig, hsjConfig, xformConfig + ) + + # --- Reconstruct phylo from edge matrices --- + treeTpl <- tree + treeTpl[["edge.length"]] <- NULL + resultTrees <- result$trees + if (length(resultTrees) == 0L) { + resultTrees <- list() + } + outTrees <- lapply(resultTrees, function(edgeMat) { + tr <- treeTpl + tr[["edge"]] <- edgeMat + # C++ edge order may differ from template; renumber to valid preorder + Renumber(tr) + }) + if (length(outTrees) == 0L) { + outTrees <- list(treeTpl) + } + + # --- Output --- + if (verbosity > 0L) { + total_s <- round(sum(unlist(result$timings), na.rm = TRUE) / 1000, 1) + stop_reason <- if (isTRUE(result$timed_out)) "timeout" + else if (isTRUE(result$consensus_stable)) "consensus stable" + else if (isTRUE(result$perturb_stop)) "perturbation limit" + else "replicate limit" + cli_alert_success(paste0( + "Search complete: score {.strong {signif(result$best_score, 7)}}, ", + "{result$replicates} replicate{?s} ", + "(last improved: #{result$last_improved_rep}), ", + "{result$hits_to_best} hit{?s} to best, ", + "{result$n_topologies} MPT{?s}, ", + "stop: {stop_reason}, {total_s}s" + )) + } + + structure( + outTrees, + score = result$best_score, + replicates = result$replicates, + hits_to_best = result$hits_to_best, + n_topologies = result$n_topologies, + last_improved_rep = result$last_improved_rep, + timed_out = isTRUE(result$timed_out), + consensus_stable = isTRUE(result$consensus_stable), + perturb_stop = isTRUE(result$perturb_stop), + timings = unlist(result$timings), + strategy_diagnostics = result$strategy_diagnostics, + replicate_scores = result$replicate_scores, + candidates_evaluated = result$candidates_evaluated, + class = "multiPhylo" + ) } + #' @rdname MaximizeParsimony +#' @usage MaximizeParsimony2(...) +#' @section Deprecated: +#' `MaximizeParsimony2()` is a deprecated alias for `MaximizeParsimony()`. #' @export -EasyTreesy <- EasyTrees -#nocov end - -.UseProfile <- function (concavity) { - pmatch(tolower(concavity), "profile", -1L) == 1L +MaximizeParsimony2 <- function(...) { + .Deprecated("MaximizeParsimony") + MaximizeParsimony(...) } diff --git a/R/Morphy.R b/R/Morphy.R new file mode 100644 index 000000000..14b5b2865 --- /dev/null +++ b/R/Morphy.R @@ -0,0 +1,1347 @@ +#' Tree search using MorphyLib scoring +#' +#' Search for most parsimonious trees using the parsimony ratchet and +#' \acronym{TBR} rearrangements, scoring with the MorphyLib C library +#' \insertCite{Brazeau2017}{TreeSearch}. +#' Supports equal weights, implied weights, and profile parsimony. +#' Treats inapplicable data using the algorithm of +#' \insertCite{Brazeau2019;textual}{TreeSearch}. +#' +#' For most users, [`MaximizeParsimony()`] provides a faster search using the +#' C++ engine, with native support for equal weights, implied weights, profile +#' parsimony, and topological constraints. +#' `Morphy()` is retained for users who need fine-grained control over the +#' R-level search loop (e.g.\sspace{}custom stopping criteria, per-iteration +#' callbacks, or direct access to MorphyLib scoring). +#' +#' Tree search commences with `ratchIter` iterations of the parsimony ratchet +#' \insertCite{Nixon1999}{TreeSearch}, which bootstraps the input dataset +#' in order to escape local optima. +#' A final round of tree bisection and reconnection (\acronym{TBR}) +#' is conducted to broaden the sampling of trees. +#' +#' This function can be called using the R command line / terminal, or through +#' the "shiny" graphical user interface app (type `EasyTrees()` to launch). +#' +#' The optimal strategy for tree search depends in part on how close to optimal +#' the starting tree is, the size of the search space (which increases +#' super-exponentially with the number of leaves), and the complexity of the +#' search space (e.g. the existence of multiple local optima). +#' +#' One possible approach is to employ four phases: +#' +#' 1. Rapid search for local optimum: tree score is typically easy to improve +#' early in a search, because the initial tree is often far from optimal. +#' When many moves are likely to be accepted, running several rounds of search +#' with a low value of `maxHits` and a high value of `tbrIter` allows many +#' trees to be evaluated quickly, hopefully moving quickly to a more promising +#' region of tree space. +#' +#' 2. Identification of local optimum: +#' Once close to a local optimum, a more extensive search +#' with a higher value of `maxHits` allows a region to be explored in more +#' detail. Setting a high value of `tbrIter` will search a local +#' neighbourhood more completely +#' +#' 3. Search for nearby peaks: +#' Ratchet iterations allow escape from local optima. +#' Setting `ratchIter` to a high value searches the wider neighbourhood more +#' extensively for other nearby peaks; `ratchEW = TRUE` accelerates these +#' exploratory searches. Ratchet iterations can be ineffective when `maxHits` +#' is too low for the search to escape its initial location. +#' +#' 4. Extensive search of final optimum. As with step 2, it may be valuable to +#' fully explore the optimum that is found after ratchet searches to be sure +#' that the locally optimal score has been obtained. Setting a high value of +#' `finalIter` performs a thorough search that can give confidence that further +#' searches would not find better (local) trees. +#' +#' A search is unlikely to have found a global optimum if: +#' +#' - Tree score continues to improve on the final iteration. If a local optimum +#' has not yet been reached, it is unlikely that a global optimum has +#' been reached. +#' Try increasing `maxHits`. +#' +#' - Successive ratchet iterations continue to improve tree scores. +#' If a recent ratchet iteration improved the score, rather than finding +#' a different region of tree space with the same optimal score, it is likely +#' that still better global optima remain to be found. Try increasing +#' `ratchIter` (more iterations give more chance for improvement) and +#' `maxHits` (to get closer to the local optimum after each ratchet iteration). +#' +#' - Optimal areas of tree space are only visited by a single ratchet iteration. +#' (See vignette: [Exploring tree space]( +#' https://ms609.github.io/TreeSearch/articles/tree-space.html).) +#' If some areas of tree space are only found by one ratchet iteration, there +#' may well be other, better areas that have not yet been visited. +#' Try increasing `ratchIter`. +#' +#' When continuing a tree search, it is usually best to start from an optimal +#' tree found during the previous iteration - there is no need to start from +#' scratch. +#' +#' A more time consuming way of checking that a global optimum has been reached +#' is to repeat a search with the same parameters multiple times, starting +#' from a different, entirely random tree each time. If all searches obtain the +#' same optimal tree score despite their different starting points, +#' this score is likely to correspond to the global optimum. +#' +#' For detailed documentation of the "TreeSearch" package, including full +#' instructions for loading phylogenetic data into R and initiating and +#' configuring tree search, see the +#' [package documentation](https://ms609.github.io/TreeSearch/). +#' +#' +#' @param dataset A phylogenetic data matrix of \pkg{phangorn} class +#' \code{phyDat}, whose names correspond to the labels of any accompanying tree. +#' Perhaps load into R using \code{\link[TreeTools]{ReadAsPhyDat}()}. +#' Additive (ordered) characters can be handled using +#' \code{\link[TreeTools]{Decompose}()}. +#' @param tree (optional) A bifurcating tree of class \code{\link[ape]{phylo}}, +#' containing only the tips listed in `dataset`, from which the search +#' should begin. +#' If unspecified, an [addition tree][AdditionTree()] will be generated from +#' `dataset`, respecting any supplied `constraint`. +#' Edge lengths are not supported and will be deleted. +#' @param ratchIter Numeric specifying number of iterations of the +#' parsimony ratchet \insertCite{Nixon1999}{TreeSearch} to conduct. +#' @param tbrIter Numeric specifying the maximum number of \acronym{TBR} +#' break points on a given tree to evaluate before terminating the search. +#' One "iteration" comprises selecting a branch to break, and evaluating +#' each possible reconnection point in turn until a new tree improves the +#' score. If a better score is found, then the counter is reset to zero, +#' and tree search continues from the improved tree. +#' @param startIter Numeric: an initial round of tree search with +#' `startIter` × `tbrIter` \acronym{TBR} break points is conducted in +#' order to locate a local optimum before beginning ratchet searches. +#' @param finalIter Numeric: a final round of tree search will evaluate +#' `finalIter` × `tbrIter` \acronym{TBR} break points, in order to +#' sample the final optimal neighbourhood more intensely. +#' @param maxHits Numeric specifying the maximum times that an optimal +#' parsimony score may be hit before concluding a ratchet iteration or final +#' search concluded. +#' @param maxTime Numeric: after `maxTime` minutes, stop tree search at the +#' next opportunity. +#' @param quickHits Numeric: iterations on subsampled datasets +#' will retain `quickHits` × `maxHits` trees with the best score. +#' @param concavity Determines the degree to which extra steps beyond the first +#' are penalized. Specify a numeric value to use implied weighting +#' \insertCite{Goloboff1993}{TreeSearch}; `concavity` specifies _k_ in +#' _k_ / _e_ + _k_. A value of 10 is recommended; +#' TNT sets a default of 3, but this is too low in some circumstances +#' \insertCite{Goloboff2018,Smith2019}{TreeSearch}. +#' Better still explore the sensitivity of results under a range of +#' concavity values, e.g. `k = 2 ^ (1:7)`. +#' Specify `Inf` to weight each additional step equally, +#' (which underperforms step weighting approaches +#' \insertCite{Goloboff2008,Goloboff2018,Goloboff2019,Smith2019}{TreeSearch}). +#' Specify `"profile"` to employ an approximation of profile parsimony +#' \insertCite{Faith2001}{TreeSearch}. +#' @param ratchEW Logical specifying whether to use equal weighting during +#' ratchet iterations, improving search speed whilst still facilitating +#' escape from local optima. +#' @param tolerance Numeric specifying degree of suboptimality to tolerate +#' before rejecting a tree. The default, `sqrt(.Machine$double.eps)`, retains +#' trees that may be equally parsimonious but for rounding errors. +#' Setting to larger values will include trees suboptimal by up to `tolerance` +#' in search results, which may improve the accuracy of the consensus tree +#' (at the expense of resolution) \insertCite{Smith2019}{TreeSearch}. +#' @param constraint Either an object of class `phyDat`, in which case +#' returned trees will be perfectly compatible with each character in +#' `constraint`; or a tree of class `phylo`, all of whose nodes will occur +#' in any output tree. +#' See \code{\link[TreeTools:ImposeConstraint]{ImposeConstraint()}} and +#' [vignette](https://ms609.github.io/TreeSearch/articles/tree-search.html) +#' for further examples. +#' @param verbosity Integer specifying level of messaging; higher values give +#' more detailed commentary on search progress. Set to `0` to run silently. +#' @param \dots Additional parameters to `Morphy()`. +#' +#' @return `Morphy()` returns a list of trees with class +#' `multiPhylo`. This lists all trees found during each search step that +#' are within `tolerance` of the optimal score, listed in the sequence that +#' they were first visited, and named according to the step in which they were +#' first found; it may contain more than `maxHits` elements. +#' Note that the default search parameters may need to be increased in order for +#' these trees to be the globally optimal trees; examine the messages printed +#' during tree search to evaluate whether the optimal score has stabilized. +#' +#' The return value has the attribute `firstHit`, a named integer vector listing +#' the number of optimal trees visited for the first time in each stage of +#' the tree search. Stages are named: +#' - `seed`: starting trees; +#' - `start`: Initial TBR search; +#' - `ratchN`: Ratchet iteration `N`; +#' - `final`: Final TBR search. +#' The first tree hit for the first time in ratchet iteration three is named +#' `ratch3_1`. +#' +#' @examples +#' ## Only run examples in interactive R sessions +#' if (interactive()) { +#' # launch "shiny" point-and-click interface +#' EasyTrees() +#' +#' # Here too, use the "continue search" function to ensure that tree score +#' # has stabilized and a global optimum has been found +#' } +#' +#' +#' # Load data for analysis in R +#' library("TreeTools") +#' data("inapplicable.phyData", package = "TreeSearch") +#' dataset <- inapplicable.phyData[["Asher2005"]] +#' +#' \donttest{ +#' # A very quick run for demonstration purposes +#' trees <- Morphy(dataset, ratchIter = 0, startIter = 0, +#' tbrIter = 1, maxHits = 4, maxTime = 1/100, +#' concavity = 10, verbosity = 4) +#' names(trees) +#' cons <- Consensus(trees) +#' } +#' +#' # In actual use, be sure to check that the score has converged on a global +#' # optimum, conducting additional iterations and runs as necessary. +#' +#' if (interactive()) { +#' # Jackknife resampling +#' nReplicates <- 10 +#' jackTrees <- replicate(nReplicates, +#' #c() ensures that each replicate returns a list of trees +#' c(Resample(dataset, trees, ratchIter = 0, tbrIter = 2, startIter = 1, +#' maxHits = 5, maxTime = 1 / 10, +#' concavity = 10, verbosity = 0)) +#' ) +#' +#' # In a serious analysis, more replicates would be conducted, and each +#' # search would undergo more iterations. +#' +#' # Now we must decide what to do with the multiple optimal trees from +#' # each replicate. +#' +#' # Set graphical parameters for plotting +#' oPar <- par(mar = rep(0, 4), cex = 0.9) +#' +#' # Take the strict consensus of all trees for each replicate +#' # (May underestimate support) +#' JackLabels(cons, lapply(jackTrees, ape::consensus)) +#' +#' # Take a single tree from each replicate (here, the first) +#' # Potentially problematic if chosen tree is not representative +#' JackLabels(cons, lapply(jackTrees, `[[`, 1)) +#' +#' # Count iteration as support if all most parsimonious trees support a split; +#' # as contradiction if all trees contradict it; don't include replicates where +#' # not all trees agree on the resolution of a split. +#' labels <- JackLabels(cons, jackTrees) +#' +#' # How many iterations were decisive for each node? +#' attr(labels, "decisive") +#' +#' # Show as proportion of decisive iterations +#' JackLabels(cons, jackTrees, showFrac = TRUE) +#' +#' # Restore graphical parameters +#' par(oPar) +#' } +#' +#' # Tree search with a constraint +#' constraint <- MatrixToPhyDat(c(a = 1, b = 1, c = 0, d = 0, e = 0, f = 0)) +#' characters <- MatrixToPhyDat(matrix( +#' c(0, 1, 1, 1, 0, 0, +#' 1, 1, 1, 0, 0, 0), ncol = 2, +#' dimnames = list(letters[1:6], NULL))) +#' Morphy(characters, constraint = constraint, verbosity = 0) +#' +#' @template MRS +#' +#' @importFrom cli cli_alert cli_alert_danger cli_alert_info cli_alert_success +#' @importFrom cli cli_alert_warning cli_h1 cli_progress_bar cli_progress_done +#' @importFrom cli cli_progress_update +#' @importFrom fastmatch fmatch +#' @importFrom stats runif +#' @importFrom TreeTools AddUnconstrained CharacterInformation ConstrainedNJ +#' @importFrom TreeTools DropTip ImposeConstraint MakeTreeBinary MatrixToPhyDat +#' @importFrom TreeTools NTip +#' @references +#' \insertAllCited{} +#' @seealso +#' [`MaximizeParsimony()`] for the faster C++ driven search engine +#' (recommended for most analyses). +#' +#' Tree search _via_ graphical user interface: [`EasyTrees()`] +#' +#' @encoding UTF-8 +#' @export +Morphy <- function(dataset, tree, + ratchIter = 7L, + tbrIter = 2L, + startIter = 2L, finalIter = 1L, + maxHits = NTip(dataset) * 1.8, + maxTime = 60, + quickHits = 1 / 3, + concavity = Inf, + ratchEW = TRUE, + tolerance = sqrt(.Machine[["double.eps"]]), + constraint, + verbosity = 3L) { + + ### User messaging functions ### + .Message <- function (level, ...) { + if (level < verbosity) { + cli_alert(paste0(...)) + } + } + .Heading <- function (text, ...) { + if (0 < verbosity) { + cli_h1(text) + if (length(list(...))) { + cli_alert(paste0(...)) + } + } + } + .Info <- function (level, ...) { + if (level < verbosity) { + cli_alert_info(paste0(...)) + } + } + .Success <- function (level, ...) { + if (level < verbosity) { + cli_alert_success(paste0(...)) + } + } + + ### Tree score functions ### + .EWScore <- function (edge, morphyObj, ...) { + preorder_morphy(edge, morphyObj) + } + + .IWScore <- function (edge, morphyObjs, weight, charSeq, concavity, + minLength, target = Inf) { + morphy_iw(edge, morphyObjs, weight, minLength, charSeq, + concavity, target + epsilon) + } + + # Must have same order of parameters as .IWScore, even though minLength unused + .ProfileScore <- function (edge, morphyObjs, weight, charSeq, profiles, + minLength, target = Inf) { + morphy_profile(edge, morphyObjs, weight, charSeq, profiles, + target + epsilon) + } + + .Score <- function (edge) { + if (length(dim(edge)) == 3L) { + edge <- edge[, , 1] + } + if (profile) { + .ProfileScore(edge, morphyObjects, startWeights, charSeq, profiles) + } else if (iw) { + .IWScore(edge, morphyObjects, startWeights, charSeq, concavity, minLength) + } else { + preorder_morphy(edge, morphyObj) + } + } + + ### Tree search functions ### + .TBRSearch <- function (Score, name, + edge, morphyObjs, weight, + tbrIter, maxHits, + minLength = NULL, charSeq = NULL, concavity = NULL) { + + iter <- 0L + nHits <- 1L + hold <- array(NA, dim = c(dim(edge), max(maxHits * 1.1, maxHits + 10L))) + maxHits <- ceiling(maxHits) + hold[, , 1] <- edge + bestScore <- Score(edge, morphyObjs, weight, charSeq, concavity, minLength) + bestPlusEps <- bestScore + epsilon + cli_progress_bar(name, total = maxHits, + auto_terminate = FALSE, + clear = verbosity < 3L, + format_done = paste0(" - TBR rearrangement at depth {iter}", + " found score {signif(bestScore)}", + " {nHits} time{?s}.")) + + while (iter < tbrIter) { + iter <- iter + 1L + brkOptions <- sample(3:(nTip * 2 - 2)) + .Message(4L, " New TBR iteration (depth ", iter, + ", score ", signif(bestScore), ")") + cli_progress_update(set = 0, total = length(brkOptions)) + + for (brk in brkOptions) { + cli_progress_update(1, status = paste0("D", iter, ", score ", + signif(bestScore), ", hit ", + nHits, ".")) + .Message(7L, " Break ", brk) + moves <- TBRMoves(edge, brk) + improvedScore <- FALSE + nMoves <- length(moves) + moveList <- sample.int(nMoves) + for (i in seq_along(moveList)) { + move <- moves[[moveList[i]]] + if (.Forbidden(move)) { + .Message(10L, " Skipping prohibited topology") + next + } + moveScore <- Score(move, morphyObjs, weight, charSeq, concavity, + minLength, bestPlusEps) + if (moveScore < bestPlusEps) { + edge <- move + if (moveScore < bestScore) { + improvedScore <- TRUE + iter <- 0L + bestScore <- moveScore + bestPlusEps <- bestScore + epsilon + nHits <- 1L + hold[, , 1] <- edge + .Message(5L, " New best score ", signif(bestScore), + " at break ", fmatch(brk, brkOptions), "/", length(brkOptions)) + break + } else { + .Message(6L, " Best score ", signif(bestScore), + " hit again (", nHits, "/", ceiling(maxHits), ")") + nHits <- nHits + 1L + hold[, , nHits] <- edge + if (nHits >= maxHits) break + } + } + # If an early iteration improves the score, a later iteration will + # probably improve it even more; we may as well keep working through + # the list instead of calculating a new one (which takes time) + if (improvedScore && runif(1) < (i / nMoves) ^ 2) break + } + if (nHits >= maxHits) break + pNextTbr <- (fmatch(brk, brkOptions) / length(brkOptions)) ^ 2 + if (improvedScore && runif(1) < pNextTbr) break + } + if (nHits >= maxHits) break + } + cli_progress_done() + + # Return: + unique(hold[, , seq_len(nHits), drop = FALSE], MARGIN = 3L) + + } + + + .Search <- function (name = "TBR search", .edge = edge, .hits = searchHits, + .weight = startWeights, .forceEW = FALSE) { + if (length(dim(.edge)) == 3L) { + .edge <- .edge[, , 1] + } + .Message(4L, paste("<<< Begin:", name)) + on.exit(.Message(4L, paste(">>> Complete:", name))) + if (profile && isFALSE(.forceEW)) { + .TBRSearch(.ProfileScore, name, edge = .edge, morphyObjects, + tbrIter = searchIter, maxHits = .hits, + weight = .weight, minLength = minLength, charSeq = charSeq, + concavity = profiles) + + } else if (iw && isFALSE(.forceEW)) { + .TBRSearch(.IWScore, name, edge = .edge, morphyObjects, + tbrIter = searchIter, maxHits = .hits, + weight = .weight, minLength = minLength, charSeq = charSeq, + concavity = concavity) + } else { + .TBRSearch(.EWScore, name, edge = .edge, morphyObj, + tbrIter = searchIter, maxHits = .hits, + concavity = if(isTRUE(.forceEW)) Inf else concavity) + } + } + + .Timeout <- function() { + if (Sys.time() > stopTime) { + .Info(1L, "Stopping search at ", .DateTime(), ": ", maxTime, + " minutes have elapsed.", + " Best score was ", signif(.Score(bestEdges[, , 1])), ".", + if (maxTime == 60) "\nIncrease `maxTime` for longer runs.") + return (TRUE) + } + + FALSE + } + + .ReturnValue <- function(bestEdges) { + if (verbosity > 0L) { + cli_alert_success(paste0(.DateTime(), + ": Tree search terminated with score {.strong ", + "{signif(.Score(bestEdges[, , 1]))}}")) + } + firstHit <- attr(bestEdges, "firstHit") + structure(lapply(seq_len(dim(bestEdges)[3]), function (i) { + tr <- tree + tr[["edge"]] <- bestEdges[, , i] + if (any(is.na(outgroup))) { + tr + } else { + RootTree(tr, outgroup) + } + }), + firstHit = firstHit, + names = paste0(rep(names(firstHit), firstHit), "_", unlist(lapply(firstHit, seq_len))), + class = "multiPhylo") + } + + + # Define constants + epsilon <- tolerance + pNextTbr <- 0.33 + profile <- .UseProfile(concavity) + iw <- is.finite(concavity) + if (iw && concavity <= 0) { + stop("`concavity` must be positive (or Inf for equal weights, ", + "or \"profile\" for profile parsimony).") + } + constrained <- !missing(constraint) + startTime <- Sys.time() + stopTime <- startTime + as.difftime(maxTime, units = "mins") + + # Initialize tree + startTrees <- NULL + if (missing(tree)) { + tree <- AdditionTree(dataset, constraint = constraint, + concavity = concavity) + } else if (inherits(tree, "multiPhylo")) { + startTrees <- unique(tree) + sampledTree <- sample.int(length(tree), 1) + .Info(2L, paste0("Starting search from {.var tree[[", sampledTree, "]]}")) + tree <- tree[[sampledTree]] + } else if (inherits(tree, "phylo")) { + startTrees <- c(tree) + } + if (dim(tree[["edge"]])[1] != 2 * tree[["Nnode"]]) { + cli_alert_warning("`tree` is not bifurcating; collapsing polytomies at random") + tree <- MakeTreeBinary(tree) + if (dim(tree[["edge"]])[1] != 2 * tree[["Nnode"]]) { + cli_alert_warning("Rooting `tree` on first leaf") + tree <- RootTree(tree, 1) + } + if (dim(tree[["edge"]])[1] != 2 * tree[["Nnode"]]) { + stop("Could not make `tree` binary.") + } + } + + # Check tree labels matches dataset + leaves <- tree[["tip.label"]] + taxa <- names(dataset) + treeOnly <- setdiff(leaves, taxa) + datOnly <- setdiff(taxa, leaves) + if (length(treeOnly)) { + cli_alert_warning(paste0("Ignoring taxa on tree missing in dataset:\n> ", + paste0(treeOnly, collapse = ", "))) + warning("Ignored taxa on tree missing in dataset:\n ", + paste0(treeOnly, collapse = ", ")) + tree <- DropTip(tree, treeOnly) + startTrees <- DropTip(startTrees, treeOnly) + } + if (length(datOnly)) { + cli_alert_warning(paste0("Ignoring taxa in dataset missing on tree:\n> ", + paste0(datOnly, collapse = ", "))) + warning("Ignored taxa in dataset missing on tree:\n> ", + paste0(datOnly, collapse = ", ")) + dataset <- dataset[-fmatch(datOnly, taxa)] + } + if (constrained) { + if (!inherits(constraint, "phyDat")) { + constraint <- MatrixToPhyDat(t(as.matrix(constraint))) + } + consTaxa <- TipLabels(constraint) + treeOnly <- setdiff(tree[["tip.label"]], consTaxa) + if (length(treeOnly)) { + constraint <- AddUnconstrained(constraint, treeOnly) + } + consOnly <- setdiff(consTaxa, tree[["tip.label"]]) + if (length(consOnly)) { + cli_alert_warning( + paste0("Ignoring taxa in constraint missing on tree:\n> ", + paste0(consOnly, collapse = ", "))) + warning("Ignored taxa in constraint missing on tree:\n ", + paste0(consOnly, collapse = ", ")) + constraint <- constraint[-fmatch(consOnly, consTaxa)] + } + constraint <- constraint[names(dataset)] + } + + + tree <- Preorder(RenumberTips(tree, names(dataset))) + nTip <- NTip(tree) + edge <- tree[["edge"]] + + # Initialize constraints + if (constrained) { + morphyConstr <- PhyDat2Morphy(constraint) + on.exit(morphyConstr <- UnloadMorphy(morphyConstr), add = TRUE) + constraintWeight <- attr(constraint, "weight") + if (any(constraintWeight > 1)) { + cli_alert_warning("Some constraints are exact duplicates.") + } + # Calculate constraint minimum score + constraintLength <- sum(MinimumLength(constraint, compress = TRUE) * + constraintWeight) + + .Forbidden <- function (edges) { + preorder_morphy(edges, morphyConstr) != constraintLength + } + + # Check that starting tree is consistent with constraints + if (.Forbidden(edge)) { + cli_alert_warning("Modifying `tree` to match `constraint`...") + outgroup <- edge[ + DescendantEdges(parent = edge[, 1], child = edge[, 2])[1, ], + 2] + outgroup <- outgroup[outgroup <= nTip] + tree <- RootTree(ImposeConstraint(tree, constraint), outgroup) + # RootTree leaves `tree` in preorder + edge <- tree[["edge"]] + if (.Forbidden(edge)) { + stop("Could not reconcile starting tree with `constraint`. ", + "Are all constraints compatible?") + } + } + + cli_alert_success(paste0("Initialized ", length(constraintWeight), + " distinct constraints.")) + + } else { + .Forbidden <- function (edges) FALSE + } + + + if (edge[1, 2] > nTip) { + outgroup <- edge[ + DescendantEdges(parent = edge[, 1], child = edge[, 2])[1, ], + 2] + outgroup <- outgroup[outgroup <= nTip] + if (length(outgroup) > nTip / 2L) { + outgroup <- seq_len(nTip)[-outgroup] + } + tree <- RootTree(tree, 1) + edge <- tree[["edge"]] + } else { + outgroup <- NA + } + + # Initialize data + if (profile) { + dataset <- PrepareDataProfile(dataset) + originalLevels <- attr(dataset, "levels") + if ("-" %fin% originalLevels) { + #TODO Fixing this will require updating the counts table cleverly + # Or we could use approximate info amounts, e.g. by treating "-" as + # an extra token + cli_alert_info(paste0("Inapplicable tokens \"-\" treated as ambiguous ", + "\"?\" for profile parsimony")) + cont <- attr(dataset, "contrast") + cont[cont[, "-"] != 0, ] <- 1 + attr(dataset, "contrast") <- cont[, colnames(cont) != "-"] + attr(dataset, "levels") <- originalLevels[originalLevels != "-"] + } + profiles <- attr(dataset, "info.amounts") + } + + if ((!iw && !profile) || # Required for equal weights search + (isTRUE(ratchEW) && ratchIter > 0) # For EW ratchet searches + ) { + morphyObj <- PhyDat2Morphy(dataset) + on.exit(morphyObj <- UnloadMorphy(morphyObj), add = TRUE) + } + + if (iw || profile) { + at <- attributes(dataset) + characters <- PhyToString(dataset, ps = "", useIndex = FALSE, + byTaxon = FALSE, concatenate = FALSE) + startWeights <- at[["weight"]] + minLength <- MinimumLength(dataset, compress = TRUE) + morphyObjects <- lapply(characters, SingleCharMorphy) + on.exit(morphyObjects <- vapply(morphyObjects, UnloadMorphy, integer(1)), + add = TRUE) + + nLevel <- length(at[["level"]]) + nChar <- at[["nr"]] + nTip <- length(dataset) + cont <- at[["contrast"]] + if (is.null(colnames(cont))) colnames(cont) <- as.character(at[["levels"]]) + simpleCont <- ifelse(rowSums(cont) == 1, + apply(cont != 0, 1, function (x) colnames(cont)[x][1]), + "?") + + + unlisted <- unlist(dataset, use.names = FALSE) + tokenMatrix <- matrix(simpleCont[unlisted], nChar, nTip) + charInfo <- apply(tokenMatrix, 1, CharacterInformation) + needsInapp <- rowSums(tokenMatrix == "-") > 2 + inappSlowdown <- 3L # A guess + # Crude estimate of score added per unit processing time + rawPriority <- charInfo / ifelse(needsInapp, inappSlowdown, 1) + priority <- startWeights * rawPriority + informative <- needsInapp | charInfo > 0 + # Will work from end of sequence to start. + charSeq <- seq_along(charInfo)[informative][order(priority[informative])] - 1L + } else { + startWeights <- unlist(MorphyWeights(morphyObj)[1, ]) # exact == approx + } + + # Initialize variables and prepare search + + nHits <- 1L + tbrStart <- startIter > 0 + tbrEnd <- finalIter > 0 + if (is.null(startTrees)) { + bestEdges <- edge + dim(bestEdges) <- c(dim(bestEdges), 1) + bestScore <- .Score(edge) + } else { + starters <- RenumberTips(startTrees, names(dataset)) + startEdges <- vapply(lapply(starters, Preorder), + `[[`, startTrees[[1]][["edge"]], + "edge") + startScores <- apply(startEdges, 3, .Score) + bestScore <- min(startScores) + bestEdges <- startEdges[, , startScores == bestScore, drop = FALSE] + } + nStages <- sum(tbrStart, ratchIter, tbrEnd) + attr(bestEdges, "firstHit") <- c("seed" = dim(bestEdges)[3], + setNames(double(nStages), + c(if(tbrStart) "start", + if(ratchIter > 0) paste0("ratch", seq_len(ratchIter)), + if(tbrEnd) "final"))) + + .Heading(paste0("BEGIN TREE SEARCH (k = ", concavity, ")"), + "Initial score: {.strong {signif(bestScore)} }") + + + # Find a local optimum + + if (tbrStart) { + searchIter <- tbrIter * startIter + searchHits <- maxHits + + .Heading("Find local optimum", + " TBR depth ", as.integer(searchIter), + "; keeping max ", as.integer(searchHits), + " trees; k = ", concavity, ".") + initialScore <- bestScore + + newEdges <- .Search("TBR search 1") + + newBestScore <- .Score(newEdges) + scoreImproved <- newBestScore + epsilon < bestScore + bestEdges <- if (scoreImproved) { + .ReplaceResults(bestEdges, newEdges, 2) + } else { + .CombineResults(bestEdges, newEdges, 2) + } + if (.Timeout()) { + .Info(1L, .DateTime(), ": Timed out with score ", + signif(min(bestScore, newBestScore))) + return(.ReturnValue(bestEdges)) # nocov + } + edge <- bestEdges[, , 1L] + bestScore <- .Score(edge) + if (bestScore < initialScore) { + .Success(2L, "{.strong New best score: {signif(bestScore)} }") + } else { + .Info(1L, .DateTime(), ": Did not beat initial score: ", + "{signif(bestScore)}") + } + } + + searchIter <- tbrIter + searchHits <- maxHits * quickHits + bestPlusEps <- bestScore + epsilon + + + + # Use Parsimony Ratchet to escape local optimum + + if (ratchIter > 0L) { + + .Heading("Escape local optimum", "{ratchIter} ratchet iterations; ", + "TBR depth {ceiling(searchIter)}; ", + "max. {ceiling(searchHits)} hits; ", + "k = {concavity}.") + .Info(1L, "{ .DateTime()}: Score to beat: {.strong {signif(bestScore)}}") + + iter <- 0L + while (iter < ratchIter) { + iter <- iter + 1L + .Message(1L, "Ratchet iteration {iter} @ {(.Time())}", + "; score to beat: {.strong {signif(bestScore)} }") + verbosity <- verbosity - 1L + eachChar <- seq_along(startWeights) + deindexedChars <- rep.int(eachChar, startWeights) + resampling <- tabulate(sample(deindexedChars, replace = TRUE), + length(startWeights)) + if (!isTRUE(ratchEW) && (profile || iw)) { + priority <- resampling * rawPriority + sampled <- informative & resampling > 0 + ratchSeq <- seq_along(charInfo)[sampled][order(priority[sampled])] - 1L + ratchetTrees <- .Search("Bootstrapped search", .weight = resampling) + } else { + errors <- vapply(eachChar, function (i) + mpl_set_charac_weight(i, resampling[i], morphyObj), integer(1)) + if (any(errors)) { # nocov start + stop ("Error resampling morphy object: ", + mpl_translate_error(unique(errors[errors < 0L]))) + } + if (mpl_apply_tipdata(morphyObj) -> error) { + stop("Error applying tip data: ", mpl_translate_error(error)) + } # nocov end + + ratchetTrees <- if (ratchEW) { + .Search("EW Bootstrapped search", .forceEW = TRUE) + } else { + .Search("Bootstrapped search") + } + + errors <- vapply(eachChar, function (i) + mpl_set_charac_weight(i, startWeights[i], morphyObj), integer(1)) + if (any(errors)) stop ("Error resampling morphy object: ", + mpl_translate_error(unique(errors[errors < 0L]))) + if (mpl_apply_tipdata(morphyObj) -> error) { + stop("Error applying tip data: ", mpl_translate_error(error)) + } + } + + verbosity <- verbosity + 1L + ratchetStart <- ratchetTrees[, , sample.int(dim(ratchetTrees)[3], 1)] + ratchStartScore <- .Score(ratchetStart) + .Message(2L, "Obtained new starting tree @ {(.Time())}", + " with score: {signif(ratchStartScore)}") + + # nocov start + if (.Timeout()) { + if (ratchetScore + epsilon < bestScore) { + bestEdges <- .ReplaceResults(bestEdges, ratchetStart, + 1 + tbrStart + iter) + } + return(.ReturnValue(bestEdges)) + } + # nocov end + + ratchetImproved <- .Search("TBR search", .edge = ratchetStart, + .hits = maxHits) + ratchetScore <- .Score(ratchetImproved[, , 1]) + + if (ratchetScore < bestPlusEps) { + if (ratchetScore + epsilon < bestScore) { + .Success(2L, "{.strong New best score}: {signif(ratchetScore)}") + bestScore <- ratchetScore + bestPlusEps <- bestScore + epsilon + bestEdges <- .ReplaceResults(bestEdges, ratchetImproved, + 1 + tbrStart + iter) + edge <- ratchetImproved[, , sample.int(dim(ratchetImproved)[3], 1)] + } else { + .Info(3L, "Hit best score {.strong {signif(bestScore)}} again") + + edge <- ratchetImproved[, , sample.int(dim(ratchetImproved)[3], 1)] + bestEdges <- .CombineResults(bestEdges, ratchetImproved, + 1 + tbrStart + iter) + } + } else { + if (3L < verbosity) { + cli_alert_danger("Did not hit best score {signif(bestScore)}") + } + } + if (.Timeout()) { + return(.ReturnValue(bestEdges)) # nocov + } + } + } + + # Branch breaking + if (tbrEnd) { + searchIter <- tbrIter * finalIter + searchHits <- maxHits + + .Heading("Sample local optimum", + "TBR depth {searchIter}; keeping {searchHits}", + " trees; k = {concavity}") + .Info(1L, .DateTime(), ": Score: ", signif(bestScore)) + finalEdges <- .Search("Final search") + newBestScore <- .Score(finalEdges[, , 1]) + improved <- newBestScore + epsilon < bestScore + bestEdges <- if (improved) { + .ReplaceResults(bestEdges, finalEdges, 1 + tbrStart + ratchIter + 1) + } else { + .CombineResults(bestEdges, finalEdges, 1 + tbrStart + ratchIter + 1) + } + } + + # Return: + .ReturnValue(bestEdges) +} + +#' Combine two edge matrices +#' +#' @param x,y 3D arrays, each slice containing an edge matrix from a tree +#' of class `phylo`. `x` should not contain duplicates. +#' @return A single 3D array containing each unique edge matrix from (`x` and) +#' `y`, with a `firstHit` attribute as documented in [`Morphy()`]. +#' @template MRS +#' @keywords internal +.CombineResults <- function (x, y, stage) { + xDim <- dim(x) + if (length(xDim) == 2L) { + xDim <- c(xDim, 1L) + } + if (any(duplicated(x, MARGIN = 3L))) { + warning(".CombineResults(x) should not contain duplicates.") + } + + res <- unique(array(c(x, y), dim = xDim + c(0, 0, dim(y)[3])), MARGIN = 3L) + firstHit <- attr(x, "firstHit") + firstHit[stage] <- dim(res)[3] - xDim[3] + attr(res, "firstHit") <- firstHit + + # Return: + res +} + +#' @rdname dot-CombineResults +#' @param old old array of edge matrices with `firstHit` attribute. +#' @param new new array of edge matrices. +#' @param stage Integer specifying element of `firstHit` in which new hits +#' should be recorded. +#' @keywords internal +.ReplaceResults <- function (old, new, stage) { + hit <- attr(old, "firstHit") + hit[] <- 0 + hit[stage] <- dim(new)[3] + structure(new, "firstHit" = hit) +} + +.Time <- function() { + format(Sys.time(), "%H:%M:%S") +} + +.DateTime <- function() { + format(Sys.time(), "%Y-%m-%d %T") +} + +# Hierarchy-aware resampling: generates hierarchical weights per replicate +# and calls ts_driven_search with HSJ/xform scoring. +# This is an internal helper called from Resample() when inapplicable != "bgs". +.ResampleHierarchy <- function(dataset, hierarchy, inapplicable, hsj_alpha, + method_idx, proportion, nReplicates, + contrast, tip_data, weight, levels, nTip, + concavity, ratchIter, tbrIter, + consArgs, profileArgs, tree) { + bootstrap <- (method_idx == 2L) + + # Prepare full HSJ args (before resampling) + hsjBase <- list() + if (identical(inapplicable, "hsj")) { + # Get flat blocks grouped by top-level block + .FlattenOneTop <- function(node) { + block <- list( + primary = node$controlling - 1L, + secondaries = node$dependents - 1L + ) + child_blocks <- lapply(node$children, .FlattenOneTop) + c(list(block), unlist(child_blocks, recursive = FALSE)) + } + hsjBase$blocks_per_top <- lapply(hierarchy, .FlattenOneTop) + hsjBase$hsjTipLabels <- .BuildTipLabels(dataset) + hsjBase$hsjAlpha <- as.double(hsj_alpha) + hsjBase$hsjAbsentState <- .HSJAbsentState(dataset) + } + + # Prepare full xform args (before resampling) + xformBase <- list() + if (identical(inapplicable, "xform")) { + recoded <- RecodeHierarchy(dataset, hierarchy) + xformBase$all_chars <- recoded$sankoff_chars + } + + # Driven search params for resampling context (light search per replicate) + resampleControl <- SearchControl( + tbrMaxHits = as.integer(max(tbrIter, 1L)), + ratchetCycles = as.integer(max(ratchIter, 3L)), + driftCycles = 0L, + xssRounds = 0L, + rssRounds = 0L, + cssRounds = 0L, + fuseInterval = 0L, + poolMaxSize = 1L, + poolSuboptimal = 0.0 + ) + resampleRuntime <- list( + maxReplicates = as.integer(max(ratchIter, 5L)), + targetHits = 2L, + maxSeconds = 0.0, + verbosity = 0L, + nThreads = 1L, + startEdge = NULL, + progressCallback = NULL + ) + resampleScoring <- list( + min_steps = integer(0), + concavity = as.double(concavity), + xpiwe = FALSE, + xpiwe_r = 0.5, + xpiwe_max_f = 5.0, + obs_count = integer(0), + infoAmounts = profileArgs$infoAmounts + ) + + trees <- vector("list", nReplicates) + for (r in seq_len(nReplicates)) { + resamp <- .HierarchicalResampleWeights( + dataset, hierarchy, bootstrap, proportion + ) + + # Build per-replicate hierarchy args based on retained blocks + repHsj <- list() + repXform <- list() + + if (identical(inapplicable, "hsj")) { + # Expand retained flat blocks (supports bootstrap: block sampled >1 time) + rep_blocks <- list() + for (bi in seq_along(resamp$blockCounts)) { + if (resamp$blockCounts[bi] > 0L) { + top_blocks <- hsjBase$blocks_per_top[[bi]] + for (k in seq_len(resamp$blockCounts[bi])) { + rep_blocks <- c(rep_blocks, top_blocks) + } + } + } + repHsj$hierarchyBlocks <- rep_blocks + repHsj$hsjTipLabels <- hsjBase$hsjTipLabels + repHsj$hsjAlpha <- hsjBase$hsjAlpha + repHsj$hsjAbsentState <- hsjBase$hsjAbsentState + } + + if (identical(inapplicable, "xform")) { + rep_xf <- list() + for (bi in seq_along(resamp$blockCounts)) { + if (resamp$blockCounts[bi] > 0L) { + for (k in seq_len(resamp$blockCounts[bi])) { + rep_xf <- c(rep_xf, list(xformBase$all_chars[[bi]])) + } + } + } + repXform$xformChars <- rep_xf + } + + # Call ts_driven_search with resampled weights + constraintCfg <- if (length(consArgs) > 0L) consArgs + hsjCfg <- if (length(repHsj) > 0L) repHsj + xformCfg <- if (length(repXform) > 0L) repXform + + result <- ts_driven_search( + contrast, tip_data, + as.integer(resamp$nonHierarchyWeights), levels, + resampleControl, resampleRuntime, resampleScoring, + constraintCfg, hsjCfg, xformCfg + ) + + # Extract best tree + if (result$pool_size > 0L && length(result$trees) > 0L) { + tr <- structure( + list(edge = result$trees[[1L]], + tip.label = names(dataset), + Nnode = nTip - 1L), + class = "phylo" + ) + attr(tr, "score") <- result$best_score + } else { + tr <- if (!is.null(tree) && inherits(tree, "phylo")) tree + else AdditionTree(dataset) + attr(tr, "score") <- result$best_score + } + trees[[r]] <- tr + } + + structure(trees, class = "multiPhylo") +} + + +#' @rdname Morphy +#' +#' @param method Unambiguous abbreviation of `jackknife` or `bootstrap` +#' specifying how to resample characters. Note that jackknife is considered +#' to give more meaningful results. +#' +#' @param proportion Numeric between 0 and 1 specifying what proportion of +#' characters to retain under jackknife resampling. +#' +#' @section Resampling: +#' Note that bootstrap support is a measure of the amount of data supporting +#' a split, rather than the amount of confidence that should be afforded the +#' grouping. +#' "Bootstrap support of 100% is not enough, the tree must also be correct" +#' \insertCite{Phillips2004}{TreeSearch}. +#' See discussion in \insertCite{Egan2006;textual}{TreeSearch}; +#' \insertCite{Wagele2009;textual}{TreeSearch}; +#' \insertCite{Simmons2011}{TreeSearch}; +#' \insertCite{Kumar2012;textual}{TreeSearch}. +#' +#' For a discussion of suitable search parameters in resampling estimates, see +#' \insertCite{Muller2005;textual}{TreeSearch}. +#' The user should decide whether to start each resampling +#' from the optimal tree (which may be quicker, but result in overestimated +#' support values as searches get stuck in local optima close to the +#' optimal tree) or a random tree (which may take longer as more rearrangements +#' are necessary to find an optimal tree on each iteration). +#' +#' For other ways to estimate clade concordance, see [`SiteConcordance()`]. +#' +#' @param nReplicates Integer specifying how many resample replicates to run. +#' Default `1L` runs a single replicate (original behaviour). +#' When `> 1`, all replicates are run in a single call, optionally in parallel. +#' @param nThreads Integer specifying the number of threads for parallel +#' resampling. Default `1L` runs serially. Use `0L` for auto-detect. +#' Only effective when `nReplicates > 1`. +#' @param hierarchy A [`CharacterHierarchy`] object specifying which characters +#' are controlled by which primary characters. Required when +#' `inapplicable` is `"hsj"` or `"xform"`. When provided, resampling +#' operates on "units" rather than individual characters: each non-hierarchy +#' character is one unit, and each top-level hierarchy block (primary + +#' all dependents) is one unit. See [`CharacterHierarchy()`]. +#' @param inapplicable Character string specifying the inapplicable-character +#' handling method: `"bgs"` (default), `"hsj"`, or `"xform"`. +#' Case-insensitive; `"brazeau"` is accepted as an alias for `"bgs"`. +#' See [`MaximizeParsimony()`] and `vignette("inapplicable")` for details. +#' @param hsj_alpha Numeric in \[0, 1\] controlling the weight of secondary +#' character variation in HSJ scoring. Default `1.0`. Only used when +#' `inapplicable = "hsj"`. +#' @param extended_iw Logical; if `TRUE` (default), use extended implied +#' weighting (XPIWE; \insertCite{Goloboff2014;textual}{TreeSearch}), +#' which adjusts per-character concavity for missing entries. +#' Ignored when `concavity = Inf` or `"profile"`. +#' @param xpiwe_r Numeric; proportion of homoplasy assumed in missing entries. +#' Default `0.5`. Only used when `extended_iw = TRUE`. +#' @param xpiwe_max_f Numeric; maximum extrapolation factor. +#' Default `5`. Only used when `extended_iw = TRUE`. +#' +#' @return `Resample()` returns a `multiPhylo` object containing one best tree +#' per resample replicate. +#' @family split support functions +#' @encoding UTF-8 +#' @export +Resample <- function(dataset, tree, method = "jack", proportion = 2 / 3, + ratchIter = 1L, tbrIter = 8L, finalIter = 3L, + maxHits = 12L, concavity = Inf, + tolerance = sqrt(.Machine[["double.eps"]]), + constraint, verbosity = 2L, + nReplicates = 1L, nThreads = 1L, + hierarchy = NULL, inapplicable = "bgs", + hsj_alpha = 1.0, + extended_iw = TRUE, + xpiwe_r = 0.5, + xpiwe_max_f = 5, + ...) { + + if (!inherits(dataset, "phyDat")) { + stop("`dataset` must be of class `phyDat`.") + } + + method_idx <- pmatch(tolower(method), c("jackknife", "bootstrap")) + if (is.na(method_idx)) { + stop("`method` must be either \"jackknife\" or \"bootstrap\".") + } + + nReplicates <- as.integer(max(nReplicates, 1L)) + nThreads <- as.integer(max(nThreads, 1L)) + + # Validate proportion for jackknife + index <- attr(dataset, "index") + if (method_idx == 1L) { + nKept <- ceiling(proportion * length(index)) + if (nKept < 1L) { + stop("No characters retained. `proportion` must be positive.") + } + if (nKept == length(index)) { + stop("`proportion` too high; no characters deleted.") + } + } + + # --- Validate inapplicable-handling parameters --- + inapplicable <- tolower(inapplicable) + if (inapplicable == "brazeau") inapplicable <- "bgs" + inapplicable <- match.arg(inapplicable, c("bgs", "hsj", "xform")) + if (inapplicable != "bgs") { + if (is.null(hierarchy)) { + stop("A `hierarchy` is required when inapplicable = \"", inapplicable, + "\". See ?CharacterHierarchy.") + } + if (!inherits(hierarchy, "CharacterHierarchy")) { + stop("`hierarchy` must be a CharacterHierarchy object.") + } + ValidateHierarchy(hierarchy, dataset) + } + if (!is.numeric(hsj_alpha) || length(hsj_alpha) != 1L || + hsj_alpha < 0 || hsj_alpha > 1) { + stop("`hsj_alpha` must be a single number in [0, 1].") + } + + # Profile parsimony: prepare data + useProfile <- identical(concavity, "profile") + if (useProfile) { + if (inapplicable != "bgs") { + stop("Profile parsimony is not currently supported with inapplicable = \"", + inapplicable, "\".") + } + dataset <- PrepareDataProfile(dataset) + concavity <- Inf + } + if (is.finite(concavity) && inapplicable != "bgs") { + stop("Implied weighting is not currently supported with inapplicable = \"", + inapplicable, "\".") + } + if (is.finite(concavity) && concavity <= 0) { + stop("`concavity` must be positive (or Inf for equal weights, ", + "or \"profile\" for profile parsimony).") + } + + # C++ engine path + at <- attributes(dataset) + contrast <- at$contrast + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) + weight <- .ScaleWeight(at$weight) + levels <- at$levels + nTip <- length(dataset) + + # Prepare constraint + consArgs <- .PrepareConstraint( + constraint = if (!missing(constraint)) constraint, + dataset = dataset + ) + + # Profile parsimony: extract info_amounts + profileArgs <- list() + if (useProfile) { + infoAmounts <- attr(dataset, "info.amounts") + if (!is.null(infoAmounts) && length(infoAmounts) > 0L) { + profileArgs$infoAmounts <- infoAmounts + } + } + + # --- Hierarchy-aware resampling path --- + # When inapplicable != "bgs", resample at the unit level (free chars + + # hierarchy blocks) and run driven_search per replicate with HSJ/xform + # scoring. + if (inapplicable != "bgs" && !is.null(hierarchy)) { + return(.ResampleHierarchy( + dataset = dataset, hierarchy = hierarchy, inapplicable = inapplicable, + hsj_alpha = hsj_alpha, method_idx = method_idx, proportion = proportion, + nReplicates = nReplicates, + contrast = contrast, tip_data = tip_data, weight = weight, + levels = levels, nTip = nTip, concavity = concavity, + ratchIter = ratchIter, tbrIter = tbrIter, + consArgs = consArgs, profileArgs = profileArgs, + tree = if (!missing(tree)) tree else NULL + )) + } + + # XPIWE: compute per-pattern observed-taxa counts + useXpiwe <- isTRUE(extended_iw) && is.finite(concavity) && !useProfile + if (useXpiwe) { + obsCount <- .ObsCount(dataset) + } + + searchArgs <- list( + contrast = contrast, + tip_data = tip_data, + weight = weight, + levels = levels, + bootstrap = (method_idx == 2L), + jackProportion = proportion, + maxReplicates = as.integer(max(ratchIter, 5L)), + targetHits = 2L, + tbrMaxHits = as.integer(max(tbrIter, 1L)), + ratchetCycles = as.integer(max(ratchIter, 3L)), + min_steps = if (is.finite(concavity)) + as.integer(MinimumLength(dataset, compress = TRUE)) else integer(0), + concavity = as.double(concavity), + xpiwe = useXpiwe, + xpiwe_r = as.double(xpiwe_r), + xpiwe_max_f = as.double(xpiwe_max_f), + obs_count = if (useXpiwe) obsCount else integer(0) + ) + + if (nReplicates > 1L) { + # Batch mode: run all replicates at once (optionally in parallel) + batchArgs <- c(searchArgs, + list(nReplicates = nReplicates, nThreads = nThreads), + consArgs, profileArgs) + result <- do.call(ts_parallel_resample, batchArgs) + + trees <- vector("list", nReplicates) + for (r in seq_len(nReplicates)) { + em <- result$edges[[r]] + if (nrow(em) == 0L) { + tr <- if (!missing(tree) && inherits(tree, "phylo")) tree + else AdditionTree(dataset) + } else { + tr <- structure( + list(edge = em, + tip.label = names(dataset), + Nnode = nTip - 1L), + class = "phylo" + ) + } + attr(tr, "score") <- result$scores[r] + trees[[r]] <- tr + } + return(structure(trees, class = "multiPhylo")) + } + + # Single-replicate path (original behavior) + result <- do.call(ts_resample_search, c(searchArgs, consArgs, profileArgs)) + + if (nrow(result$edge) == 0L) { + tr <- if (!missing(tree) && inherits(tree, "phylo")) tree + else AdditionTree(dataset) + attr(tr, "score") <- result$score + return(structure(list(tr), class = "multiPhylo")) + } + + tr <- structure( + list(edge = result$edge, + tip.label = names(dataset), + Nnode = nTip - 1L), + class = "phylo" + ) + attr(tr, "score") <- result$score + + structure(list(tr), class = "multiPhylo") +} + +#' Launch tree search graphical user interface +#' +#' Opens a "shiny" app for interactive parsimony tree search and results +#' exploration. +#' +#' @return Opens a Shiny application; does not return a value. +#' @seealso [`MaximizeParsimony()`], [`Morphy()`] +#' @importFrom TreeDist ClusteringInfoDistance +#' @export +EasyTrees <- function () {#nocov start + needed <- c("cluster", "future", "PlotTools", "promises", + "protoclust", "Rogue", "shiny", "shinyjs") + missing <- needed[!vapply(needed, requireNamespace, + logical(1L), quietly = TRUE)] + if (length(missing)) { + stop("EasyTrees() requires additional packages: ", + paste(missing, collapse = ", "), ".\n", + "Install with: install.packages(", + paste0("\"", missing, "\"", collapse = ", "), ")", + call. = FALSE) + } + shiny::runApp(system.file("Parsimony", package = "TreeSearch")) +} + +#' @rdname EasyTrees +#' @export +EasyTreesy <- EasyTrees +#nocov end + +.UseProfile <- function (concavity) { + pmatch(tolower(concavity), "profile", -1L) == 1L +} diff --git a/R/NNI.R b/R/NNI.R index a66d1e98e..3b526932d 100644 --- a/R/NNI.R +++ b/R/NNI.R @@ -27,9 +27,8 @@ #' @return Returns a tree with class \code{phylo} (if \code{returnAll = FALSE}) or #' a set of trees, with class \code{multiPhylo} (if \code{returnAll = TRUE}). #' -#' @references -#' The algorithm is summarized in -#' \insertRef{Felsenstein2004}{TreeSearch} +#' @references \insertCite{Felsenstein2004}{TreeSearch} +#' \insertAllCited{} #' #' #' @examples @@ -140,7 +139,6 @@ NNISwap <- function (parent, child, nTips = (length(parent) / 2L) + 1L, RenumberEdges(parent, child) } -## TODO use RenumberList #' Double NNI #' #' Returns the edge parameter of the two trees consistent with the speficied \acronym{NNI} rearrangement diff --git a/R/PaintCharacters.R b/R/PaintCharacters.R new file mode 100644 index 000000000..7c26e24cc --- /dev/null +++ b/R/PaintCharacters.R @@ -0,0 +1,108 @@ +#' Colour characters by tree concordance +#' +#' `PaintCharacters()` assigns a colour to each character in `dataset` by +#' computing a perceptually weighted mean of the colours assigned to tree edges +#' by [TreeTools::PaintTree()], using the mutual information between each +#' character and each edge as the weight. +#' +#' For each character, the colour is the weighted mean (in CIELAB space, which +#' is perceptually uniform) of the colours of all tree edges that the character +#' concordantly supports. The weight for each edge is the product of its +#' normalized mutual information (concordance quality) and its relative +#' information amount; discordant edges (quality \eqn{\le 0}) are excluded. +#' Characters with no concordant signal on the tree are coloured grey +#' (`"#888888"`). +#' +#' If the returned colours look desaturated ("murky"), try raising `threshold` +#' to exclude low-information edges, or inspect `ConcordanceTable()` directly to +#' understand the character–edge signal. +#' +#' @param dataset A `phyDat` object containing morphological character data, +#' whose `names` match the tip labels of `tree`. +#' @param tree A `phylo` object whose tip labels match `names(dataset)`. +#' @param threshold Numeric scalar; edges whose information value (the +#' `"hBest"` × `"n"` product from [ClusteringConcordance()]) is below this +#' threshold are excluded from the weighted average regardless of their +#' concordance. Default `0` retains all concordant edges. Raising the +#' threshold suppresses low-information edges that would otherwise dilute the +#' colour signal. +#' @param palette Palette specification passed to [TreeTools::PaintTree()]. +#' Either a character string (`"default"`, `"protanopia"`, `"tritanopia"`) +#' or a function `function(h, s)` mapping hue (0–360°) and saturation (0–1) +#' to hex colours. +#' +#' @return A character vector of hex colour strings, one entry per character in +#' `dataset`, named by character index. Grey (`"#888888"`) indicates +#' characters with no concordant signal on the tree. +#' +#' @examples +#' data("congreveLamsdellMatrices", package = "TreeSearch") +#' dataset <- congreveLamsdellMatrices[[1]][, 1:12] +#' tree <- referenceTree +#' library("TreeTools", quietly = TRUE) +#' +#' cols <- PaintCharacters(dataset, tree) +#' conc <- ConcordanceTable(tree, dataset) +#' # Plot the tree alongside to interpret the colours: +#' paint <- PaintTree(tree) +#' plot(tree, edge.color = paint$edgeCol, edge.width = 2) +#' +#' @seealso [TreeTools::PaintTree()], [ConcordanceTable()] +#' @family split support functions +#' @importFrom grDevices col2rgb convertColor rgb +#' @importFrom TreeTools PaintTree +#' @export +PaintCharacters <- function(dataset, tree, threshold = 0, + palette = "default") { + paint <- TreeTools::PaintTree(tree, palette) + cc <- ClusteringConcordance(tree, dataset, return = "all") + + # Replicate the ConcordanceTable extraction (without triggering its plot). + # matrix() guards against dimension collapse when nChar == 1 or nEdge == 1. + nEdge <- dim(cc)[[2L]] + nChar <- dim(cc)[[3L]] + info <- matrix(cc["hBest", , ] * cc["n", , ], nEdge, nChar, + dimnames = dimnames(cc)[2:3]) + relInfo <- info / max(info, na.rm = TRUE) + relInfo[is.na(relInfo)] <- 0 + quality <- matrix(cc["normalized", , ], nEdge, nChar) + relInfo[is.na(quality)] <- 0 + quality[is.na(quality)] <- 0 + + # Align PaintTree edge colours to ClusteringConcordance edge order. + # Row names are child node IDs (non-trivial splits only). + ctNodes <- as.integer(rownames(info)) + edgeIdx <- match(ctNodes, tree[["edge"]][, 2L]) + edgeCols <- paint$edgeCol[edgeIdx] + + # Convert edge colours to CIELAB (perceptually uniform; a*/b* are Cartesian + # so weighted averages avoid the circular-mean issue of hue). + labMat <- matrix( + convertColor(t(col2rgb(edgeCols)) / 255, from = "sRGB", to = "Lab"), + ncol = 3L + ) # nEdges × 3 + + # Weight matrix: concordant edges only, scaled by relative information. + wMat <- pmax(quality, 0) * relInfo # nEdges × nChars + wMat[info < threshold] <- 0 + + wSum <- colSums(wMat) # nChars + noInfo <- wSum == 0 + wSumSafe <- ifelse(noInfo, 1, wSum) + + # Weighted Lab mean: t(3×nEdges %*% nEdges×nChars) → nChars×3, then / wSum. + labAvg <- t(t(labMat) %*% wMat) / wSumSafe + + # Convert back to sRGB; clamp out-of-gamut values; encode as hex. + # matrix() guards against convertColor() dropping to a vector for 1 row. + rgbAvg <- matrix( + pmax(0, pmin(1, convertColor(labAvg, from = "Lab", to = "sRGB"))), + ncol = 3L + ) + charCols <- rgb(rgbAvg[, 1L], rgbAvg[, 2L], rgbAvg[, 3L]) + charCols[noInfo] <- "#888888" + names(charCols) <- colnames(info) + + # Return: + charCols +} diff --git a/R/ParsSim.R b/R/ParsSim.R new file mode 100644 index 000000000..564af77d2 --- /dev/null +++ b/R/ParsSim.R @@ -0,0 +1,633 @@ +#' Simulate a dataset under parsimony +#' +#' Generates a morphological dataset under a strict parsimony model. +#' Characters are initialized at their minimum step count, then extra steps +#' are allocated one at a time. Each added step must increase the Fitch +#' parsimony score of the character by exactly one -- no "masked" or +#' "overprinted" steps are permitted. +#' +#' Back-mutations (e.g. \eqn{0 \to 1 \to 0}{0 -> 1 -> 0}) are allowed +#' when they genuinely add to the parsimony score. +#' +#' When `concavity` is finite (implied weighting), characters that already +#' carry more homoplasy are more likely to receive additional extra steps, +#' mirroring the mathematical relationship described by the +#' \eqn{k / (k + e)}{k/(k+e)} fit function. +#' +#' @param tree A \code{\link[ape:read.tree]{phylo}} object. If non-binary, +#' resolved to binary with a warning. If unrooted, rooted internally at an +#' arbitrary node. If no edge lengths are present, uniform lengths are used. +#' @param nChar Integer vector: `nChar[1]` gives the number of 2-state +#' characters, `nChar[2]` the number of 3-state characters, and so on. +#' @param nExtraSteps Single integer: total extra steps distributed one at a +#' time across all characters. +#' @param concavity Implied weighting concavity constant. `Inf` (default) +#' gives equal weights (uniform character selection). A finite positive +#' number _k_ gives implied weighting, with selection probability +#' proportional to `(k + e) / k`. `"profile"` uses profile parsimony +#' weighting: selection probability is proportional to the inverse of the +#' phylogenetic information at the current step count, computed via +#' [StepInformation()] after initialization. +#' @param rootState Integer vector: initial state at the root node for each +#' character (default `0L`). If length 1, the same root state is used for +#' all characters. If length `sum(nChar)`, each character gets its own root +#' state. Each root state must be in `0:(k-1)` where _k_ is the number of +#' states for that character. +#' @param missing Controls which cells are replaced with the ambiguous token +#' `?`. Missing data is applied _after_ the complete simulation, so +#' attributes such as `extra_steps` and `saturated` reflect the underlying +#' complete dataset. Accepted forms: +#' \describe{ +#' \item{**Scalar** (0--1)}{Flat rate: each cell is independently missing +#' with this probability.} +#' \item{**List** with `taxon` and/or `character` components}{Per-taxon +#' and/or per-character rates. Each component is a numeric vector of +#' probabilities (0--1). `taxon` should be named (matching tip labels) +#' or length `n_tip`; `character` should be length `sum(nChar)`. Per-cell +#' probability is `1 - (1 - p_taxon) * (1 - p_char)`.} +#' \item{**Matrix** (n_tip x total_chars)}{Per-cell probabilities (0--1). +#' Rows are taxa (named to match tip labels, or in tip order); +#' columns are characters.} +#' } +#' Default `0` (no missing data). +#' +#' @return A `phyDat` object with characters ordered by number of states +#' (2-state first, then 3-state, and so on). Additional attributes: +#' \describe{ +#' \item{`saturated`}{Logical vector: can each character accept another +#' step? Computed at return for all characters.} +#' \item{`steps_exhausted`}{Logical vector: was each character discovered +#' to be saturated during the step-placement loop (i.e., selected for +#' an extra step but no legal edge found)?} +#' \item{`extra_steps`}{Integer vector: number of extra steps placed on +#' each character.} +#' } +#' +#' @examples +#' tree <- TreeTools::BalancedTree(8) +#' dataset <- ParsSim(tree, nChar = c(20L), nExtraSteps = 10L) +#' TreeLength(tree, dataset) +#' +#' # Implied weighting: steps concentrate on fewer characters +#' dataset_iw <- ParsSim(tree, nChar = c(40L), nExtraSteps = 30L, +#' concavity = 3) +#' attr(dataset_iw, "extra_steps") +#' +#' # Profile parsimony weighting +#' dataset_pp <- ParsSim(tree, nChar = c(20L), nExtraSteps = 15L, +#' concavity = "profile") +#' attr(dataset_pp, "extra_steps") +#' +#' # 20% missing data injected post-hoc +#' dataset_missing <- ParsSim(tree, nChar = c(20L), nExtraSteps = 10L, +#' missing = 0.2) +#' +#' # Per-taxon missing rates (fragmentary taxa) +#' dataset_taxon <- ParsSim(tree, nChar = c(20L), nExtraSteps = 10L, +#' missing = list(taxon = c(t1 = 0.8, t2 = 0.5))) +#' +#' @references \insertCite{Goloboff2018}{TreeSearch} +#' \insertAllCited{} +#' @importFrom TreeTools MakeTreeBinary MatrixToPhyDat Postorder RootNode +#' @importFrom TreeTools RootTree +#' @family tree scoring +#' @export +ParsSim <- function(tree, + nChar = c(100L), + nExtraSteps = 0L, + concavity = Inf, + rootState = 0L, + missing = 0) { + # --- Validate inputs ------------------------------------------------------- + if (!inherits(tree, "phylo")) { + stop("`tree` must be a phylo object") + } + nChar <- as.integer(nChar) + nExtraSteps <- as.integer(nExtraSteps) + if (any(nChar < 0L)) stop("`nChar` values must be non-negative") + total_chars <- sum(nChar) + if (total_chars == 0L) stop("`nChar` must specify at least one character") + if (length(nExtraSteps) != 1L || nExtraSteps < 0L) { + stop("`nExtraSteps` must be a single non-negative integer") + } + missing_spec <- .pars_sim_validate_missing(missing) + + use_profile <- identical(concavity, "profile") + if (!use_profile) { + # `Inf` is valid (equal weights); reject NaN, -Inf and finite non-positive + # values, which otherwise slip past the `is.finite()` test below and are + # silently treated as equal weights. + if (!is.numeric(concavity) || length(concavity) != 1L || is.nan(concavity) || + identical(concavity, -Inf) || (is.finite(concavity) && concavity <= 0)) { + stop("`concavity` must be a positive number (or Inf for equal weights, ", + "or \"profile\" for profile parsimony)") + } + } + use_iw <- !use_profile && is.finite(concavity) + + # --- Prepare tree ---------------------------------------------------------- + tree_info <- .pars_sim_prepare_tree(tree) + + # --- Determine state counts per character ---------------------------------- + n_states_vec <- rep(seq_along(nChar) + 1L, times = nChar) + + # --- Validate and expand rootState ------------------------------------------ + rootState <- as.integer(rootState) + if (length(rootState) == 1L) { + rootState <- rep(rootState, total_chars) + } else if (length(rootState) != total_chars) { + stop("`rootState` must have length 1 or sum(nChar) (= ", total_chars, ")") + } + bad <- which(rootState < 0L | rootState >= n_states_vec) + if (length(bad) > 0L) { + stop("`rootState[", bad[1], "]` = ", rootState[bad[1]], + " is out of range for a ", n_states_vec[bad[1]], "-state character", + " (must be 0 to ", n_states_vec[bad[1]] - 1L, ")") + } + + # --- Initialize characters ------------------------------------------------- + char_states <- vector("list", total_chars) + char_scores <- integer(total_chars) + for (i in seq_len(total_chars)) { + init <- .pars_sim_init_char(tree_info, n_states_vec[i], rootState[i]) + char_states[[i]] <- init$node_states + char_scores[i] <- init$score + } + + # --- Compute info profiles for profile mode -------------------------------- + info_profiles <- NULL + if (use_profile) { + n_tip <- tree_info$n_tip + info_profiles <- vector("list", total_chars) + for (i in seq_len(total_chars)) { + tip_states_i <- char_states[[i]][seq_len(n_tip)] + info_profiles[[i]] <- StepInformation(tip_states_i) + } + } + + # --- Extra step loop ------------------------------------------------------- + extra_steps <- integer(total_chars) + steps_exhausted <- logical(total_chars) + + if (nExtraSteps > 0L) { + steps_placed <- 0L + while (steps_placed < nExtraSteps) { + available <- which(!steps_exhausted) + if (length(available) == 0L) { + warning("All characters saturated after ", steps_placed, " of ", + nExtraSteps, " extra steps.") + break + } + + # Select character + char_idx <- .pars_sim_select_char(available, extra_steps, concavity, + use_iw, use_profile, char_scores, + info_profiles) + + # Find legal edges + legal <- .pars_sim_legal_edges(char_states[[char_idx]], tree_info, + char_scores[char_idx], + n_states_vec[char_idx]) + + if (is.null(legal)) { + steps_exhausted[char_idx] <- TRUE + next + } + + # Sample legal move weighted by edge length + move_idx <- .safe_sample_idx(nrow(legal), prob = legal$edge_length) + + # Apply transition + char_states[[char_idx]] <- .pars_sim_apply_transition( + char_states[[char_idx]], tree_info, + legal$edge_idx[move_idx], legal$target_state[move_idx] + ) + char_scores[char_idx] <- char_scores[char_idx] + 1L + extra_steps[char_idx] <- extra_steps[char_idx] + 1L + steps_placed <- steps_placed + 1L + + # In profile mode, mark exhausted when info drops to 0 + if (use_profile) { + profile <- info_profiles[[char_idx]] + step_name <- as.character(char_scores[char_idx]) + if (!(step_name %in% names(profile)) || + profile[step_name] <= 0) { + steps_exhausted[char_idx] <- TRUE + } + } + } + } + + # --- Build phyDat ---------------------------------------------------------- + n_tip <- tree_info$n_tip + tip_matrix <- vapply(char_states, function(ns) ns[seq_len(n_tip)], + integer(n_tip)) + rownames(tip_matrix) <- tree_info$tip_labels + + prob_matrix <- .pars_sim_build_missing_matrix( + missing_spec, n_tip, total_chars, tree_info$tip_labels + ) + + if (!is.null(prob_matrix)) { + char_matrix <- matrix(as.character(tip_matrix), nrow = n_tip, + dimnames = dimnames(tip_matrix)) + is_missing <- matrix(runif(n_tip * total_chars), nrow = n_tip) < prob_matrix + char_matrix[is_missing] <- "?" + result <- MatrixToPhyDat(char_matrix) + } else { + result <- MatrixToPhyDat(tip_matrix) + } + + # --- Calculate saturation for all characters -------------------------------- + saturated <- vapply(seq_len(total_chars), function(i) { + is.null(.pars_sim_legal_edges(char_states[[i]], tree_info, + char_scores[i], n_states_vec[i])) + }, logical(1)) + + attr(result, "saturated") <- saturated + attr(result, "steps_exhausted") <- steps_exhausted + attr(result, "extra_steps") <- extra_steps + + result +} + + +# --- Internal helpers -------------------------------------------------------- + +#' Prepare a tree for simulation +#' @return Named list: edge (postorder matrix), edge_length, n_tip, n_node, +#' root, children (list of child-node vectors), tip_labels. +#' @keywords internal +#' @noRd +.pars_sim_prepare_tree <- function(tree) { + if (!ape::is.rooted(tree)) { + tree <- RootTree(tree, tree[["tip.label"]][1]) + } + if (!ape::is.binary(tree)) { + warning("Resolving non-binary tree to binary.") + tree <- MakeTreeBinary(tree) + } + + tree <- Postorder(tree) + edge <- tree[["edge"]] + n_tip <- length(tree[["tip.label"]]) + n_node <- n_tip + tree[["Nnode"]] + + edge_length <- tree[["edge.length"]] + if (is.null(edge_length)) { + edge_length <- rep(1, nrow(edge)) + } + + children <- vector("list", n_node) + for (i in seq_len(n_node)) children[[i]] <- integer(0) + for (i in seq_len(nrow(edge))) { + p <- edge[i, 1] + children[[p]] <- c(children[[p]], edge[i, 2]) + } + + list( + edge = edge, + edge_length = edge_length, + n_tip = n_tip, + n_node = n_node, + root = RootNode(tree), + children = children, + tip_labels = tree[["tip.label"]] + ) +} + + +#' Fitch parsimony score for a single character +#' +#' Pure R Fitch downpass using bit-vector state sets. +#' @param tip_states Integer vector of states (0-indexed) for tips 1..n_tip. +#' @param tree_info List from `.pars_sim_prepare_tree()`. +#' @return Integer parsimony score. +#' @keywords internal +#' @noRd +.pars_sim_fitch_score <- function(tip_states, tree_info) { + n_tip <- tree_info$n_tip + n_node <- tree_info$n_node + edge <- tree_info$edge + + sets <- integer(n_node) + sets[seq_len(n_tip)] <- bitwShiftL(1L, tip_states[seq_len(n_tip)]) + + score <- 0L + for (i in seq_len(nrow(edge))) { + p <- edge[i, 1] + ch <- edge[i, 2] + if (sets[p] == 0L) { + sets[p] <- sets[ch] + } else { + inter <- bitwAnd(sets[p], sets[ch]) + if (inter > 0L) { + sets[p] <- inter + } else { + sets[p] <- bitwOr(sets[p], sets[ch]) + score <- score + 1L + } + } + } + score +} + + +#' Initialize a character with minimum steps +#' +#' Sets all nodes to `root_state`, then places `n_states - 1` transitions on +#' randomly selected edges (weighted by edge length) to introduce each state. +#' @return List: `node_states` (integer vector, length n_node), `score`. +#' @keywords internal +#' @noRd +.pars_sim_init_char <- function(tree_info, n_states, root_state) { + node_states <- rep(as.integer(root_state), tree_info$n_node) + edge <- tree_info$edge + + other_states <- setdiff(seq.int(0L, n_states - 1L), root_state) + for (new_state in other_states) { + # Edges where both endpoints share the same state + unmarked <- which(node_states[edge[, 1]] == node_states[edge[, 2]]) + weights <- tree_info$edge_length[unmarked] + idx <- unmarked[.safe_sample_idx(length(unmarked), prob = weights)] + + node_states <- .pars_sim_apply_transition(node_states, tree_info, idx, + new_state) + } + + list(node_states = node_states, score = n_states - 1L) +} + + +#' Find contiguous region of same-state nodes below a start node +#' +#' DFS from `start_node` following edges where parent and child share the +#' same state. +#' @return List: `region` (all node indices), `tips` (tip-only indices), +#' `boundary_states` (states of nodes just outside the region). +#' @keywords internal +#' @noRd +.pars_sim_find_region <- function(node_states, tree_info, start_node) { + children <- tree_info$children + n_tip <- tree_info$n_tip + state <- node_states[start_node] + + region <- integer(0) + tips <- integer(0) + boundary_states <- integer(0) + stack <- start_node + + while (length(stack) > 0L) { + node <- stack[length(stack)] + stack <- stack[-length(stack)] + region <- c(region, node) + if (node <= n_tip) { + tips <- c(tips, node) + } else { + for (ch in children[[node]]) { + if (node_states[ch] == state) { + stack <- c(stack, ch) + } else { + boundary_states <- c(boundary_states, node_states[ch]) + } + } + } + } + + list(region = region, tips = tips, boundary_states = boundary_states) +} + + +#' Find all legal (edge, target-state) moves for one character +#' +#' For each unmarked edge (endpoints share state), tries each possible +#' target state. Uses a boundary prefilter followed by Fitch verification. +#' @return Data frame with columns `edge_idx`, `target_state`, `edge_length`, +#' or NULL if no legal moves. +#' @keywords internal +#' @noRd +.pars_sim_legal_edges <- function(node_states, tree_info, current_score, + n_states) { + edge <- tree_info$edge + n_edge <- nrow(edge) + + edge_idx_out <- integer(0) + target_state_out <- integer(0) + edge_length_out <- numeric(0) + all_states <- seq.int(0L, n_states - 1L) + + for (i in seq_len(n_edge)) { + p <- edge[i, 1] + ch <- edge[i, 2] + + # Only consider unmarked edges + if (node_states[p] != node_states[ch]) next + + current_state <- node_states[ch] + info <- .pars_sim_find_region(node_states, tree_info, ch) + targets <- setdiff(all_states, current_state) + + for (t in targets) { + # Boundary prefilter: if a boundary child already has target state, + # the transition would eliminate an existing step → skip + if (t %in% info$boundary_states) next + + # Fitch verify + new_tip_states <- node_states[seq_len(tree_info$n_tip)] + new_tip_states[info$tips] <- t + new_score <- .pars_sim_fitch_score(new_tip_states, tree_info) + + if (new_score == current_score + 1L) { + edge_idx_out <- c(edge_idx_out, i) + target_state_out <- c(target_state_out, t) + edge_length_out <- c(edge_length_out, tree_info$edge_length[i]) + } + } + } + + if (length(edge_idx_out) == 0L) return(NULL) + + data.frame(edge_idx = edge_idx_out, + target_state = target_state_out, + edge_length = edge_length_out) +} + + +#' Apply a transition on an edge +#' +#' Changes the child node and its contiguous same-state region to +#' `new_state`. +#' @return Updated `node_states` vector. +#' @keywords internal +#' @noRd +.pars_sim_apply_transition <- function(node_states, tree_info, edge_idx, + new_state) { + child_node <- tree_info$edge[edge_idx, 2] + info <- .pars_sim_find_region(node_states, tree_info, child_node) + node_states[info$region] <- new_state + node_states +} + + +#' Select a character for the next extra step +#' @keywords internal +#' @noRd +.pars_sim_select_char <- function(available, extra_steps, concavity, use_iw, + use_profile = FALSE, char_scores = NULL, + info_profiles = NULL) { + if (length(available) == 1L) return(available) + + if (use_profile) { + # Weight ∝ 1 / info_amount at current step count + weights <- vapply(available, function(i) { + profile <- info_profiles[[i]] + step_name <- as.character(char_scores[i]) + if (step_name %in% names(profile)) { + info <- profile[step_name] + if (info > 0) return(1.0 / info) + } + 0 + }, double(1)) + # If all weights are 0, all available characters are info-saturated + if (all(weights == 0)) return(available[1L]) + available[sample.int(length(available), 1L, prob = weights)] + } else if (use_iw) { + weights <- (concavity + extra_steps[available]) / concavity + available[sample.int(length(available), 1L, prob = weights)] + } else { + available[sample.int(length(available), 1L)] + } +} + + +#' Sample a single index, safe for length-1 vectors +#' @keywords internal +#' @noRd +.safe_sample_idx <- function(n, prob = NULL) { + if (n == 1L) return(1L) + if (!is.null(prob)) { + # Edge lengths drive the weights; a tree with all-zero (or absent / + # undefined) branch lengths leaves every candidate edge with weight 0, + # for which sample.int() errors "too few positive probabilities". Treat + # such edges as equiprobable instead. + prob[is.na(prob)] <- 0 + if (!any(prob > 0)) { + prob <- NULL + } + } + sample.int(n, 1L, prob = prob) +} + + +#' Validate and parse the `missing` argument +#' +#' Returns a list with `type` ("none", "scalar", "list", "matrix") and +#' the parsed value. +#' @keywords internal +#' @noRd +.pars_sim_validate_missing <- function(missing) { + if (is.matrix(missing)) { + if (!is.numeric(missing)) stop("`missing` matrix must be numeric") + if (any(is.na(missing)) || any(missing < 0) || any(missing > 1)) { + stop("`missing` matrix values must be between 0 and 1") + } + return(list(type = "matrix", value = missing)) + } + + if (is.list(missing)) { + valid_names <- c("taxon", "character") + bad <- setdiff(names(missing), valid_names) + if (length(bad) > 0L) { + stop("`missing` list may only contain 'taxon' and/or 'character' ", + "components; found: ", paste(bad, collapse = ", ")) + } + if (length(missing) == 0L || + !any(valid_names %in% names(missing))) { + stop("`missing` list must contain at least one of 'taxon' or 'character'") + } + for (comp in valid_names) { + if (comp %in% names(missing)) { + v <- missing[[comp]] + if (!is.numeric(v) || any(is.na(v)) || any(v < 0) || any(v > 1)) { + stop("`missing$", comp, "` must be a numeric vector with ", + "values between 0 and 1") + } + } + } + return(list(type = "list", value = missing)) + } + + # Scalar case + missing <- as.double(missing) + if (length(missing) != 1L || is.na(missing) || missing < 0 || missing > 1) { + stop("`missing` must be a number between 0 and 1, a list, or a matrix") + } + if (missing == 0) return(list(type = "none")) + list(type = "scalar", value = missing) +} + + +#' Build a per-cell probability matrix from a missing specification +#' +#' @return A n_tip × total_chars matrix of probabilities, or NULL if no +#' missing data should be applied. +#' @keywords internal +#' @noRd +.pars_sim_build_missing_matrix <- function(spec, n_tip, total_chars, + tip_labels) { + if (spec$type == "none") return(NULL) + + if (spec$type == "scalar") { + return(matrix(spec$value, nrow = n_tip, ncol = total_chars)) + } + + if (spec$type == "matrix") { + mat <- spec$value + if (!is.null(rownames(mat))) { + # Reorder rows to match tip_labels + if (!all(tip_labels %in% rownames(mat))) { + stop("`missing` matrix row names must include all tip labels") + } + mat <- mat[tip_labels, , drop = FALSE] + } + if (nrow(mat) != n_tip || ncol(mat) != total_chars) { + stop("`missing` matrix must have ", n_tip, " rows (taxa) and ", + total_chars, " columns (characters)") + } + return(mat) + } + + # List case: combine taxon and character rates + miss <- spec$value + p_taxon <- rep(0, n_tip) + if ("taxon" %in% names(miss)) { + tv <- miss$taxon + if (!is.null(names(tv))) { + if (!all(names(tv) %in% tip_labels)) { + stop("Names in `missing$taxon` must be valid tip labels") + } + # Named: match to tip labels; unlisted taxa get 0 + p_taxon[match(names(tv), tip_labels)] <- tv + } else { + if (length(tv) != n_tip) { + stop("`missing$taxon` must be named or have length ", n_tip) + } + p_taxon <- tv + } + } + + p_char <- rep(0, total_chars) + if ("character" %in% names(miss)) { + cv <- miss$character + if (length(cv) != total_chars) { + stop("`missing$character` must have length ", total_chars) + } + p_char <- cv + } + + # p_cell = 1 - (1 - p_taxon) * (1 - p_char) + prob_mat <- 1 - outer(1 - p_taxon, 1 - p_char) + + if (all(prob_mat == 0)) return(NULL) + prob_mat +} diff --git a/R/PolEscapa.R b/R/PolEscapa.R index 8263e39fb..b7518e6d3 100644 --- a/R/PolEscapa.R +++ b/R/PolEscapa.R @@ -48,9 +48,11 @@ LengthAdded <- function(trees, char, concavity = Inf) { stop("`char` must comprise a single character; try char[, 1]") } cont <- attr(char, "contrast") - if (any(rowSums(cont) == 0)) { - stop("`char` contract matrix lacks levels for ", - paste(which(rowSums(cont) == 0), collapse = ", ")) + zeroRows <- which(rowSums(cont) == 0) + usedTokens <- unique(unlist(char, use.names = FALSE)) + if (any(zeroRows %in% usedTokens)) { + stop("`char` contrast matrix lacks levels for token(s) ", + paste(zeroRows[zeroRows %in% usedTokens], collapse = ", ")) } if (inherits(trees, "phylo")) { trees <- c(trees) @@ -69,8 +71,14 @@ LengthAdded <- function(trees, char, concavity = Inf) { char <- PrepareDataProfile(char) } - # Define ambiguous state, depending on applicability + # Define ambiguous state, depending on applicability. + # Take the first matching row when multiple rows are fully ambiguous, to + # avoid silently assigning a vector to `charQm[[leaf]]` (analogous to the + # T-302 fix for `qmApp`). qm <- which(rowSums(cont) == dim(cont)[2]) + if (length(qm) > 0L) { + qm <- qm[[1L]] + } if ("-" %in% colnames(cont)) { inapp <- as.logical(cont[, "-"]) app <- as.logical(rowSums(contApp)) @@ -83,6 +91,20 @@ LengthAdded <- function(trees, char, concavity = Inf) { if (length(qmApp) == 0) { attr(char, "contrast") <- rbind(cont, colnames(cont) != "-") qmApp <- 1 + nrow(cont) + } else { + qmApp <- qmApp[[1L]] + } + # If no fully ambiguous (`?`) token row exists, `qm` is empty; a leaf whose + # starting token is inapplicable would then be assigned `integer(0)` at + # `charQm[[leaf]] <- qm`, silently corrupting the phyDat (dropping an + # element). Append an all-ones row (every state, applicable + inapplicable) + # and point `qm` at it. Done after the `qmApp` fallback so the row indices + # of `cont`/`contApp`/`app`/`inapp` computed above remain consistent; this + # only adds an extra row to the contrast that `qm` references. + if (length(qm) == 0L) { + newContrast <- rbind(attr(char, "contrast"), rep(1, dim(cont)[2])) + attr(char, "contrast") <- newContrast + qm <- nrow(newContrast) } QMScore <- function(leaf) { @@ -98,14 +120,6 @@ LengthAdded <- function(trees, char, concavity = Inf) { } deltas <- start - .vapply(seq_along(char), QMScore, start) - # Temp: - if (any(deltas < 0)) { - warning("Unknown scoring issue may distort score of ", - paste(names(char)[apply(deltas < 0, 2, any)], collapse = ", "), - ". Please report bug to maintainer.") - } - # /Temp - delta <- setNames(colSums(deltas), names(char)) # Return: diff --git a/R/RandomTreeScore.R b/R/RandomTreeScore.R index 539637d6f..bc5dc8b31 100644 --- a/R/RandomTreeScore.R +++ b/R/RandomTreeScore.R @@ -1,9 +1,12 @@ -#' Parsimony score of random postorder tree +#' Parsimony score of random tree #' -#' @inheritParams MorphyTreeLength +#' Generates a random tree topology and returns its parsimony score under +#' equal weights. +#' +#' @param dataset A `phyDat` object (recommended) or a Morphy object created +#' with [`PhyDat2Morphy()`] (legacy; deprecated). #' -#' @return `RandomTreeScore()` returns the parsimony score of a random tree -#' for the given Morphy object. +#' @return `RandomTreeScore()` returns a numeric parsimony score. #' @examples #' tokens <- matrix(c( #' 0, "-", "-", 1, 1, 2, @@ -11,21 +14,24 @@ #' 0, "-", "-", 0, 0, 0), byrow = TRUE, nrow = 3L, #' dimnames = list(letters[1:3], NULL)) #' pd <- TreeTools::MatrixToPhyDat(tokens) -#' morphyObj <- PhyDat2Morphy(pd) -#' -#' RandomTreeScore(morphyObj) -#' -#' morphyObj <- UnloadMorphy(morphyObj) +#' RandomTreeScore(pd) +#' @importFrom TreeTools RandomTree #' @export -RandomTreeScore <- function (morphyObj) { - nTip <- mpl_get_numtaxa(morphyObj) +RandomTreeScore <- function(dataset) { + if (inherits(dataset, "morphyPtr")) { + nTip <- mpl_get_numtaxa(dataset) + if (nTip < 2) { + return(0L) + } + return(.Call(`RANDOM_TREE_SCORE`, as.integer(nTip), dataset)) + } + + nTip <- length(dataset) if (nTip < 2) { - # Return: - 0L - } else { - # Return: - .Call(`RANDOM_TREE_SCORE`, as.integer(nTip), morphyObj) + return(0) } + tree <- RandomTree(dataset, root = TRUE) + TreeLength(tree, dataset) } #' Random postorder tree diff --git a/R/Ratchet.R b/R/Ratchet.R index 63c1b2278..eeefa811c 100644 --- a/R/Ratchet.R +++ b/R/Ratchet.R @@ -2,6 +2,8 @@ #' #' `Ratchet()` uses the parsimony ratchet \insertCite{Nixon1999}{TreeSearch} #' to search for a more parsimonious tree using custom optimality criteria. +#' For standard parsimony searches, [`MaximizeParsimony()`] is faster; +#' use `Ratchet()` when you need a custom `TreeScorer` or `EdgeSwapper`. #' #' For usage pointers, see the #' [vignette](https://ms609.github.io/TreeSearch/articles/custom.html). @@ -43,7 +45,11 @@ #' @param suboptimal retain trees that are suboptimal by this score. #' Defaults to a small value that will counter rounding errors. #' -#' @return `Ratchet()` returns a tree modified by parsimony ratchet iterations. +#' @return When `returnAll = FALSE` (the default), `Ratchet()` returns a single +#' optimal `phylo` tree, with its parsimony score in attribute `"score"`. +#' When `returnAll = TRUE`, it returns a `multiPhylo` of the optimal (and +#' near-optimal, within `suboptimal`) trees encountered, each carrying its own +#' `"score"` attribute. #' #' @references #' \insertAllCited{} @@ -84,7 +90,6 @@ Ratchet <- function(tree, dataset, suboptimal = sqrt(.Machine[["double.eps"]]), ...) { epsilon <- sqrt(.Machine[["double.eps"]]) hits <- 0L - # initialize tree and data if (dim(tree[["edge"]])[1] != 2 * tree[["Nnode"]]) { stop("tree must be bifurcating; try rooting with ape::root") } @@ -210,14 +215,17 @@ Ratchet <- function(tree, dataset, # Return to lapply: x}) ret <- unique(forest) + class(ret) <- "multiPhylo" if (verbosity > 1L) { message(" - Removing duplicates leaves ", length(ret), " unique trees") } uniqueScores <- vapply(ret, attr, double(1), "score") } else if (length(forest) == 1) { - ret <- tree newEdge <- forest[[1]] - ret[["edge"]] <- cbind(newEdge[[1]], newEdge[[2]]) + onlyTree <- tree + onlyTree[["edge"]] <- cbind(newEdge[[1]], newEdge[[2]]) + attr(onlyTree, "score") <- newEdge[[3]] + ret <- structure(list(onlyTree), class = "multiPhylo") uniqueScores <- newEdge[[3]] } else { stop("\nNo trees!? Is suboptimal set to a sensible (positive) value?") diff --git a/R/RcppExports.R b/R/RcppExports.R index a2bc7abe5..f66d5f6a0 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -1,6 +1,25 @@ # Generated by using Rcpp::compileAttributes() -> do not edit by hand # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 +#' @rdname Carter1 +#' @examples +#' # Log-probability that a 3-state character (2 "0", 3 "1", 2 "2") needs +#' # exactly 2 steps on a random 7-leaf tree: +#' logp <- MaddisonSlatkin(2, c("0" = 2, "1" = 3, "01" = 0, "2" = 2)) +#' # Convert to an expected number of trees: +#' exp(logp) * TreeTools::NUnrooted(7) +#' +#' @export +MaddisonSlatkin <- function(steps, states) { + .Call(`_TreeSearch_MaddisonSlatkin`, steps, states) +} + +#' @export +#' @keywords internal +MaddisonSlatkin_clear_cache <- function() { + invisible(.Call(`_TreeSearch_MaddisonSlatkin_clear_cache`)) +} + expected_mi <- function(ni, nj) { .Call(`_TreeSearch_expected_mi`, ni, nj) } @@ -53,3 +72,159 @@ all_tbr <- function(edge, break_order) { .Call(`_TreeSearch_all_tbr`, edge, break_order) } +#' Monte Carlo Fitch scores for a single character +#' +#' Generates `n_mc` random trees and scores each with a Fitch parsimony +#' downpass for a single character defined by `state_counts`. +#' Tree generation and scoring are done entirely in C with no R object +#' allocation per tree, making this very fast (~0.01 ms per tree). +#' +#' @param state_counts Integer vector giving the number of tips in each +#' state. Length determines the number of states (k); sum determines +#' the number of tips (n). For example, `c(13, 13, 12)` defines a +#' 3-state character with 38 tips. +#' @param n_mc Number of random trees to generate and score. +#' @return Integer vector of length `n_mc` containing the Fitch parsimony +#' score (number of state changes) for each random tree. +#' @keywords internal +#' @export +mc_fitch_scores <- function(state_counts, n_mc) { + .Call(`_TreeSearch_mc_fitch_scores`, state_counts, n_mc) +} + +ts_fitch_score <- function(edge, contrast, tip_data, weight, levels, min_steps = integer(), concavity = -1.0, infoAmounts = NULL, xpiwe = FALSE, xpiwe_r = 0.5, xpiwe_max_f = 5.0, obs_count = integer()) { + .Call(`_TreeSearch_ts_fitch_score`, edge, contrast, tip_data, weight, levels, min_steps, concavity, infoAmounts, xpiwe, xpiwe_r, xpiwe_max_f, obs_count) +} + +ts_ls_fit <- function(edge, dist, weight = NULL, method = 1L) { + .Call(`_TreeSearch_ts_ls_fit`, edge, dist, weight, method) +} + +ts_ls_search <- function(edge, dist, weight = NULL, method = 1L, maxHits = 1L, doSpr = TRUE) { + .Call(`_TreeSearch_ts_ls_search`, edge, dist, weight, method, maxHits, doSpr) +} + +ts_na_debug_char <- function(edge, contrast, tip_data, weight, levels, target_pattern) { + .Call(`_TreeSearch_ts_na_debug_char`, edge, contrast, tip_data, weight, levels, target_pattern) +} + +ts_na_char_steps <- function(edge, contrast, tip_data, weight, levels) { + .Call(`_TreeSearch_ts_na_char_steps`, edge, contrast, tip_data, weight, levels) +} + +ts_char_steps <- function(edge, contrast, tip_data, weight, levels) { + .Call(`_TreeSearch_ts_char_steps`, edge, contrast, tip_data, weight, levels) +} + +ts_debug_clip <- function(edge, contrast, tip_data, weight, levels, clip_node_1based) { + .Call(`_TreeSearch_ts_debug_clip`, edge, contrast, tip_data, weight, levels, clip_node_1based) +} + +ts_test_indirect <- function(edge, contrast, tip_data, weight, levels, clip_node_1based, above_1based, below_1based) { + .Call(`_TreeSearch_ts_test_indirect`, edge, contrast, tip_data, weight, levels, clip_node_1based, above_1based, below_1based) +} + +ts_spr_search <- function(edge, contrast, tip_data, weight, levels, maxHits = 20L, min_steps = integer(), concavity = -1.0) { + .Call(`_TreeSearch_ts_spr_search`, edge, contrast, tip_data, weight, levels, maxHits, min_steps, concavity) +} + +ts_tbr_search <- function(edge, contrast, tip_data, weight, levels, maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L, min_steps = integer(), concavity = -1.0) { + .Call(`_TreeSearch_ts_tbr_search`, edge, contrast, tip_data, weight, levels, maxHits, acceptEqual, maxChanges, min_steps, concavity) +} + +ts_ratchet_search <- function(edge, contrast, tip_data, weight, levels, nCycles = 10L, perturbProb = 0.04, maxHits = 1L, min_steps = integer(), concavity = -1.0, perturbMode = 0L, perturbMaxMoves = 0L, adaptive = FALSE, targetEscapeRate = 0.3) { + .Call(`_TreeSearch_ts_ratchet_search`, edge, contrast, tip_data, weight, levels, nCycles, perturbProb, maxHits, min_steps, concavity, perturbMode, perturbMaxMoves, adaptive, targetEscapeRate) +} + +ts_drift_search <- function(edge, contrast, tip_data, weight, levels, nCycles = 10L, afdLimit = 3L, rfdLimit = 0.1, maxHits = 1L, min_steps = integer(), concavity = -1.0) { + .Call(`_TreeSearch_ts_drift_search`, edge, contrast, tip_data, weight, levels, nCycles, afdLimit, rfdLimit, maxHits, min_steps, concavity) +} + +ts_wagner_tree <- function(contrast, tip_data, weight, levels, addition_order = integer(), min_steps = integer(), concavity = -1.0, infoAmounts = NULL, consSplitMatrix = NULL, consContrast = NULL, consTipData = NULL, consWeight = NULL, consLevels = NULL, consExpectedScore = 0L) { + .Call(`_TreeSearch_ts_wagner_tree`, contrast, tip_data, weight, levels, addition_order, min_steps, concavity, infoAmounts, consSplitMatrix, consContrast, consTipData, consWeight, consLevels, consExpectedScore) +} + +ts_random_wagner_tree <- function(contrast, tip_data, weight, levels, min_steps = integer(), concavity = -1.0, infoAmounts = NULL, consSplitMatrix = NULL, consContrast = NULL, consTipData = NULL, consWeight = NULL, consLevels = NULL, consExpectedScore = 0L) { + .Call(`_TreeSearch_ts_random_wagner_tree`, contrast, tip_data, weight, levels, min_steps, concavity, infoAmounts, consSplitMatrix, consContrast, consTipData, consWeight, consLevels, consExpectedScore) +} + +ts_compute_splits <- function(edge, n_tip) { + .Call(`_TreeSearch_ts_compute_splits`, edge, n_tip) +} + +ts_trees_equal <- function(edge1, edge2, n_tip) { + .Call(`_TreeSearch_ts_trees_equal`, edge1, edge2, n_tip) +} + +ts_pool_test <- function(edges, scores, n_tip, max_size = 100L, suboptimal = 0.0) { + .Call(`_TreeSearch_ts_pool_test`, edges, scores, n_tip, max_size, suboptimal) +} + +ts_nni_search <- function(edge, contrast, tip_data, weight, levels, maxHits = 20L, min_steps = integer(), concavity = -1.0) { + .Call(`_TreeSearch_ts_nni_search`, edge, contrast, tip_data, weight, levels, maxHits, min_steps, concavity) +} + +ts_tree_fuse <- function(edge, contrast, tip_data, weight, levels, pool_edges, pool_scores, accept_equal = FALSE, max_rounds = 10L, min_steps = integer(), concavity = -1.0) { + .Call(`_TreeSearch_ts_tree_fuse`, edge, contrast, tip_data, weight, levels, pool_edges, pool_scores, accept_equal, max_rounds, min_steps, concavity) +} + +ts_sector_diag <- function(edge, contrast, tip_data, weight, levels, sector_root_1based) { + .Call(`_TreeSearch_ts_sector_diag`, edge, contrast, tip_data, weight, levels, sector_root_1based) +} + +ts_rss_search <- function(edge, contrast, tip_data, weight, levels, minSectorSize = 6L, maxSectorSize = 50L, acceptEqual = FALSE, rssPicks = 0L, ratchetCycles = 6L, maxHits = 1L, min_steps = integer(), concavity = -1.0) { + .Call(`_TreeSearch_ts_rss_search`, edge, contrast, tip_data, weight, levels, minSectorSize, maxSectorSize, acceptEqual, rssPicks, ratchetCycles, maxHits, min_steps, concavity) +} + +ts_xss_search <- function(edge, contrast, tip_data, weight, levels, nPartitions = 4L, xssRounds = 3L, acceptEqual = FALSE, ratchetCycles = 6L, maxHits = 1L, min_steps = integer(), concavity = -1.0) { + .Call(`_TreeSearch_ts_xss_search`, edge, contrast, tip_data, weight, levels, nPartitions, xssRounds, acceptEqual, ratchetCycles, maxHits, min_steps, concavity) +} + +ts_driven_search <- function(contrast, tip_data, weight, levels, searchControl, runtimeConfig, scoringConfig, constraintConfig = NULL, hsjConfig = NULL, xformConfig = NULL) { + .Call(`_TreeSearch_ts_driven_search`, contrast, tip_data, weight, levels, searchControl, runtimeConfig, scoringConfig, constraintConfig, hsjConfig, xformConfig) +} + +ts_resample_search <- function(contrast, tip_data, weight, levels, bootstrap = FALSE, jackProportion = 2.0 / 3.0, maxReplicates = 5L, targetHits = 2L, tbrMaxHits = 1L, ratchetCycles = 3L, ratchetPerturbProb = 0.04, driftCycles = 0L, min_steps = integer(), concavity = -1.0, consSplitMatrix = NULL, consContrast = NULL, consTipData = NULL, consWeight = NULL, consLevels = NULL, consExpectedScore = 0L, infoAmounts = NULL, xpiwe = FALSE, xpiwe_r = 0.5, xpiwe_max_f = 5.0, obs_count = integer()) { + .Call(`_TreeSearch_ts_resample_search`, contrast, tip_data, weight, levels, bootstrap, jackProportion, maxReplicates, targetHits, tbrMaxHits, ratchetCycles, ratchetPerturbProb, driftCycles, min_steps, concavity, consSplitMatrix, consContrast, consTipData, consWeight, consLevels, consExpectedScore, infoAmounts, xpiwe, xpiwe_r, xpiwe_max_f, obs_count) +} + +ts_parallel_resample <- function(contrast, tip_data, weight, levels, nReplicates = 1L, nThreads = 1L, bootstrap = FALSE, jackProportion = 2.0 / 3.0, maxReplicates = 5L, targetHits = 2L, tbrMaxHits = 1L, ratchetCycles = 3L, ratchetPerturbProb = 0.04, driftCycles = 0L, min_steps = integer(), concavity = -1.0, consSplitMatrix = NULL, consContrast = NULL, consTipData = NULL, consWeight = NULL, consLevels = NULL, consExpectedScore = 0L, infoAmounts = NULL, xpiwe = FALSE, xpiwe_r = 0.5, xpiwe_max_f = 5.0, obs_count = integer()) { + .Call(`_TreeSearch_ts_parallel_resample`, contrast, tip_data, weight, levels, nReplicates, nThreads, bootstrap, jackProportion, maxReplicates, targetHits, tbrMaxHits, ratchetCycles, ratchetPerturbProb, driftCycles, min_steps, concavity, consSplitMatrix, consContrast, consTipData, consWeight, consLevels, consExpectedScore, infoAmounts, xpiwe, xpiwe_r, xpiwe_max_f, obs_count) +} + +ts_successive_approx <- function(contrast, tip_data, weight, levels, saK = 3.0, maxSAIter = 20L, maxReplicates = 10L, targetHits = 3L, tbrMaxHits = 1L, ratchetCycles = 5L, ratchetPerturbProb = 0.04, driftCycles = 0L, min_steps = integer(), concavity = -1.0, consSplitMatrix = NULL, consContrast = NULL, consTipData = NULL, consWeight = NULL, consLevels = NULL, consExpectedScore = 0L, infoAmounts = NULL, xpiwe = FALSE, xpiwe_r = 0.5, xpiwe_max_f = 5.0, obs_count = integer()) { + .Call(`_TreeSearch_ts_successive_approx`, contrast, tip_data, weight, levels, saK, maxSAIter, maxReplicates, targetHits, tbrMaxHits, ratchetCycles, ratchetPerturbProb, driftCycles, min_steps, concavity, consSplitMatrix, consContrast, consTipData, consWeight, consLevels, consExpectedScore, infoAmounts, xpiwe, xpiwe_r, xpiwe_max_f, obs_count) +} + +ts_bench_tbr_phases <- function(edge, contrast, tip_data, weight, levels, min_steps = integer(), concavity = -1.0) { + .Call(`_TreeSearch_ts_bench_tbr_phases`, edge, contrast, tip_data, weight, levels, min_steps, concavity) +} + +ts_simplify_diag <- function(contrast, tip_data, weight, levels) { + .Call(`_TreeSearch_ts_simplify_diag`, contrast, tip_data, weight, levels) +} + +ts_hsj_score <- function(edge, contrast, tip_data, weight, levels, hierarchy_blocks_r, alpha, tip_labels_r, absent_state) { + .Call(`_TreeSearch_ts_hsj_score`, edge, contrast, tip_data, weight, levels, hierarchy_blocks_r, alpha, tip_labels_r, absent_state) +} + +ts_sankoff_test <- function(edge, n_states_r, cost_matrices_r, tip_states_r, forced_root_r) { + .Call(`_TreeSearch_ts_sankoff_test`, edge, n_states_r, cost_matrices_r, tip_states_r, forced_root_r) +} + +ts_wagner_bias_bench <- function(contrast, tip_data, weight, levels, min_steps, concavity, bias, temperature, n_reps, run_tbr) { + .Call(`_TreeSearch_ts_wagner_bias_bench`, contrast, tip_data, weight, levels, min_steps, concavity, bias, temperature, n_reps, run_tbr) +} + +ts_test_strategy_tracker <- function(seed, n_draws) { + .Call(`_TreeSearch_ts_test_strategy_tracker`, seed, n_draws) +} + +ts_tbr_diagnostics <- function(edge, contrast, tip_data, weight, levels, maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L, min_steps = integer(), concavity = -1.0, clipOrder = 0L, unrooted = TRUE) { + .Call(`_TreeSearch_ts_tbr_diagnostics`, edge, contrast, tip_data, weight, levels, maxHits, acceptEqual, maxChanges, min_steps, concavity, clipOrder, unrooted) +} + +ts_ev_cache_key_probe <- function(edge, contrast, tip_data, weight, levels, concavity = -1.0, zero_active = FALSE, set_upweight = FALSE, bump_pattern_freq = FALSE) { + .Call(`_TreeSearch_ts_ev_cache_key_probe`, edge, contrast, tip_data, weight, levels, concavity, zero_active, set_upweight, bump_pattern_freq) +} + diff --git a/R/SPR.R b/R/SPR.R index 0374b0520..ef58b82fd 100644 --- a/R/SPR.R +++ b/R/SPR.R @@ -72,8 +72,8 @@ SPRWarning <- function (parent, child, error) { #' @param mergeEdge the index of an edge on which to merge the broken edge. #' @return This function returns a tree in \code{phyDat} format that has undergone one \acronym{SPR} iteration. #' -#' @references The \acronym{SPR} algorithm is summarized in -#' \insertRef{Felsenstein2004}{TreeSearch} +#' @references \insertCite{Felsenstein2004}{TreeSearch} +#' \insertAllCited{} #' #' @author Martin R. Smith #' @@ -104,8 +104,7 @@ SPR <- function(tree, edgeToBreak = NULL, mergeEdge = NULL) { unique(unlist(lapply(which(notDuplicateRoot), AllSPR, parent = parent, child = child, nEdge = nEdge, notDuplicateRoot = notDuplicateRoot), - recursive = FALSE)) # TODO the fact that we need to use `unique` indicates that - # we're being inefficient here. + recursive = FALSE)) } else { newEdge <- SPRSwap(parent, edge[, 2], edgeToBreak = edgeToBreak, mergeEdge = mergeEdge) @@ -160,7 +159,6 @@ SPRMoves.matrix <- function (tree, edgeToBreak = integer(0)) { unique(.all_spr(tree, edgeToBreak)) } -## TODO Do edges need to be pre-ordered before coming here? #' @describeIn SPR faster version that takes and returns parent and child parameters #' @inheritParams RearrangeEdges #' @param nEdge (optional) integer specifying the number of edges of a tree of @@ -174,7 +172,6 @@ SPRSwap <- function (parent, child, nEdge = length(parent), nNode = nEdge / 2L, edgeToBreak = NULL, mergeEdge = NULL) { if (nEdge < 5) { - # TODO we need to re-root this tree... return(list(parent, child)) } @@ -364,7 +361,6 @@ RootedSPR <- function(tree, edgeToBreak = NULL, mergeEdge = NULL) { return (tree) } -## TODO Do edges need to be pre-ordered before coming here? #' @describeIn SPR faster version that takes and returns parent and child parameters #' @return a list containing two elements, corresponding in turn to the rearranged parent and child parameters #' @export @@ -382,8 +378,7 @@ RootedSPRSwap <- function (parent, child, nEdge = length(parent), nNode = nEdge notDuplicateRoot <- .NonDuplicateRoot(parent, child, nEdge) return(unique(unlist(lapply(which(breakable), AllSPR, parent=parent, child=child, nEdge=nEdge, notDuplicateRoot=notDuplicateRoot), - recursive=FALSE))) # TODO the fact that we need to use `unique` indicates that - # we're being inefficient here. + recursive=FALSE))) } rightSide <- DescendantEdges(edge = 1, parent, child, nEdge = nEdge) diff --git a/R/ScoreSpectrum.R b/R/ScoreSpectrum.R new file mode 100644 index 000000000..3d0e0ca4a --- /dev/null +++ b/R/ScoreSpectrum.R @@ -0,0 +1,184 @@ +#' Score-spectrum coverage estimate for parsimony search +#' +#' `ScoreSpectrum()` applies Chao1-style abundance-based richness estimation +#' \insertCite{Chao1984}{TreeSearch} to the distribution of per-replicate +#' parsimony scores returned by [MaximizeParsimony()]. Treating each distinct +#' score value as a "species" and the number of replicates that found it as its +#' "abundance", the estimator quantifies how thoroughly the search has explored +#' the parsimony landscape. +#' +#' The **sample coverage** (Good-Turing estimator) +#' \insertCite{Good1953,Chao2012}{TreeSearch} is: +#' \deqn{\hat{C} = 1 - f_1 / n} +#' where \eqn{f_1} is the number of score levels seen exactly once and \eqn{n} +#' is the total number of replicates. A coverage close to 1 indicates that +#' most of the accessible score landscape has been sampled; low coverage +#' suggests meaningful unexplored territory remains. +#' +#' The **Chao1 lower bound** on total score-level richness is: +#' \deqn{\hat{S} = S_{\mathrm{obs}} + \frac{f_1^2}{2 f_2}} +#' When \eqn{f_2 = 0} (no doubleton scores) the bias-corrected form +#' \eqn{f_1(f_1 - 1)/2} is used instead. +#' +#' @param trees A `multiPhylo` object returned by [MaximizeParsimony()], which +#' must carry a `replicate_scores` attribute. Alternatively, a numeric +#' vector of per-replicate scores. +#' @param tol Numeric tolerance for binning floating-point scores. Scores +#' that differ by less than `tol` are treated as equal. The default +#' (`1e-4`) is suitable for implied-weights and profile-parsimony scores; +#' use `0` for strict equality when working with equal-weights (integer) +#' scores. +#' +#' @return A list of class `"ScoreSpectrum"` with components: +#' \describe{ +#' \item{`n_replicates`}{Total completed replicates.} +#' \item{`observed_levels`}{Distinct score values observed (\eqn{S_\mathrm{obs}}).} +#' \item{`estimated_levels`}{Chao1 lower-bound estimate of total score +#' levels (\eqn{\hat{S}}).} +#' \item{`coverage`}{Good-Turing sample coverage (\eqn{\hat{C}}).} +#' \item{`unseen_fraction`}{Estimated fraction of score levels not yet +#' seen: \eqn{1 - S_\mathrm{obs}/\hat{S}}.} +#' \item{`best_score`}{The lowest (best) score found.} +#' \item{`best_score_reps`}{Number of replicates that reached the best +#' score.} +#' \item{`f`}{Named integer vector: \eqn{f_k} = number of score levels +#' seen exactly \eqn{k} times (frequency spectrum).} +#' \item{`replicate_scores`}{The raw per-replicate scores.} +#' } +#' +#' @references +#' \insertAllCited{} +#' +#' @examples +#' library("TreeTools", quietly = TRUE) +#' data("Lobo", package = "TreeTools") +#' result <- MaximizeParsimony(Lobo.phy, maxReplicates = 4L) +#' sp <- ScoreSpectrum(result) +#' print(sp) +#' +#' @family search utilities +#' @export +ScoreSpectrum <- function(trees, tol = 1e-4) { + # Accept either a multiPhylo with attribute or a raw numeric vector + if (inherits(trees, "multiPhylo")) { + scores <- attr(trees, "replicate_scores") + if (is.null(scores)) { + stop("`trees` has no `replicate_scores` attribute. ", + "Re-run MaximizeParsimony() with this version of TreeSearch.") + } + } else if (is.numeric(trees)) { + scores <- trees + } else { + stop("`trees` must be a `multiPhylo` from MaximizeParsimony() or a ", + "numeric vector of per-replicate scores.") + } + + scores <- scores[is.finite(scores)] + n <- length(scores) + + if (n < 2L) { + return(structure( + list( + n_replicates = n, + observed_levels = if (n == 0L) 0L else 1L, + estimated_levels = NA_real_, + coverage = NA_real_, + unseen_fraction = NA_real_, + best_score = if (n > 0L) min(scores) else NA_real_, + best_score_reps = if (n > 0L) sum(scores == min(scores)) else 0L, + f = integer(0L), + replicate_scores = scores + ), + class = "ScoreSpectrum" + )) + } + + # Bin scores to handle floating-point equality + if (tol > 0) { + scores_binned <- round(scores / tol) * tol + } else { + scores_binned <- scores + } + + # Frequency of each distinct score value (abundance vector) + abundance <- tabulate(factor(scores_binned)) + s_obs <- length(abundance) # distinct score levels observed + + # Frequency spectrum: f_k = number of score levels seen exactly k times + max_k <- max(abundance) + f_k <- tabulate(abundance, nbins = max_k) + f1 <- if (max_k >= 1L) f_k[1L] else 0L + f2 <- if (max_k >= 2L) f_k[2L] else 0L + + # Good-Turing sample coverage + coverage <- 1.0 - f1 / n + + # Chao1 lower-bound estimate of total richness + if (f2 > 0L) { + s_hat <- s_obs + f1^2 / (2 * f2) + } else if (f1 > 1L) { + # Bias-corrected form when no doubletons + s_hat <- s_obs + f1 * (f1 - 1L) / 2 + } else { + # All observed levels are well-represented + s_hat <- s_obs + } + + unseen_fraction <- if (s_hat > 0) 1 - s_obs / s_hat else 0 + + best_score <- min(scores_binned) + best_score_reps <- sum(scores_binned <= best_score + tol) + + # Trim trailing zeros from f_k for a compact spectrum + last_nonzero <- max(which(f_k > 0L), 0L) + f_k_trimmed <- f_k[seq_len(last_nonzero)] + names(f_k_trimmed) <- seq_len(last_nonzero) + + structure( + list( + n_replicates = n, + observed_levels = s_obs, + estimated_levels = s_hat, + coverage = coverage, + unseen_fraction = unseen_fraction, + best_score = best_score, + best_score_reps = best_score_reps, + f = f_k_trimmed, + replicate_scores = scores + ), + class = "ScoreSpectrum" + ) +} + +#' @export +print.ScoreSpectrum <- function(x, ...) { + if (is.na(x$coverage)) { + cat("ScoreSpectrum: insufficient replicates (n =", x$n_replicates, ")\n") + return(invisible(x)) + } + cat(sprintf( + "Score-spectrum coverage (n = %d replicates)\n", + x$n_replicates + )) + cat(sprintf( + " Best score: %.4g (%d replicates)\n", + x$best_score, x$best_score_reps + )) + cat(sprintf( + " Score levels seen: %d (est. total: %.1f)\n", + x$observed_levels, x$estimated_levels + )) + cat(sprintf( + " Landscape coverage: %.1f%%", + 100 * x$coverage + )) + if (x$unseen_fraction > 0.01) { + cat(sprintf(" (~%.0f%% of score levels unseen)", 100 * x$unseen_fraction)) + } + cat("\n") + if (length(x$f) > 0L) { + cat(" Frequency spectrum (f_k): ") + cat(paste0("f", names(x$f), "=", x$f, collapse = ", "), "\n") + } + invisible(x) +} diff --git a/R/SearchControl.R b/R/SearchControl.R new file mode 100644 index 000000000..829f70eba --- /dev/null +++ b/R/SearchControl.R @@ -0,0 +1,447 @@ +#' Expert search heuristic parameters +#' +#' Construct a list of low-level search parameters for +#' [`MaximizeParsimony()`]. Most users can ignore these and rely on the +#' `strategy` presets (`"sprint"`, `"default"`, `"thorough"`); `SearchControl` +#' is provided for expert tuning. +#' +#' The parameters correspond to heuristics described by +#' \insertCite{Goloboff1999;textual}{TreeSearch} +#' (sectorial search, tree drifting, tree fusing) and +#' \insertCite{Nixon1999;textual}{TreeSearch} +#' (parsimony ratchet), as implemented in TNT +#' \insertCite{Goloboff2016}{TreeSearch}. +#' +#' @param tbrMaxHits Integer; number of equally-scoring trees to accept +#' before stopping a TBR pass. +#' @param clipOrder Integer (experimental); clip-ordering strategy for TBR +#' search. Determines the order in which edges are tried as clip points. +#' 0 = random (default); 1 = inverse-weight (fewest descendant taxa first); +#' 2 = tips-first (terminal edges before internal); 3 = bucket ordering; +#' 4 = anti-tip (internal before terminal); 5 = large-first (most descendant +#' taxa first). On datasets with \eqn{\ge}65 tips, \code{clipOrder = 2L} +#' (tips-first) typically increases replicate throughput by 5--15\% by +#' evaluating higher-probability improvement candidates earlier. +#' @param nniFirst Logical; run an NNI pass before SPR/TBR in each replicate? +#' At small tree sizes (\eqn{\le}88 tips) overhead is negligible; at \eqn{\ge}100 tips +#' this significantly accelerates the initial descent from the Wagner tree. +#' @param sprFirst Logical; run an SPR pass before TBR in each replicate? +#' @param tabuSize Integer; tabu list size for TBR plateau exploration. +#' @param wagnerStarts Integer; random Wagner starting trees per replicate. +#' @param ratchetCycles Integer; number of ratchet perturbation cycles. +#' @param ratchetPerturbProb Numeric (0--1); probability of perturbing each +#' character. +#' @param ratchetPerturbMode Integer; 0 = zero-weight only, 1 = up-weight only, +#' 2 = mixed. +#' @param ratchetPerturbMaxMoves Integer; maximum TBR moves per perturbation +#' cycle (0 = automatic). +#' @param ratchetAdaptive Logical; adjust perturbation probability based on +#' within-replicate escape rate? +#' @param ratchetTaper Logical; taper ratchet perturbation probability across +#' replicates as the pool stabilizes? When `TRUE`, early replicates use +#' the full `ratchetPerturbProb`; later replicates (with high hit rates) +#' use a reduced probability for finer local exploration. The effective +#' probability is `ratchetPerturbProb * max(floor, 1 - strength * hitRate)` +#' where `hitRate` is the fraction of replicates that found the current +#' best score. Default `FALSE`. +#' @param stallEscalateFactor Numeric (>= 1); cross-replicate stall escalation. +#' When a driven search stalls -- no improvement for `ceiling(nTip / 10)` +#' consecutive replicates -- the ratchet perturbation probability is +#' multiplied by this factor for each further `ceiling(nTip / 10)` replicates +#' without improvement (capped at 0.5), and adaptive perturbation +#' (`ratchetAdaptive`) is engaged, until an improvement resets the strength to +#' its base value. This lets a search discover at runtime the perturbation +#' strength a difficult dataset needs, rather than relying on a fixed value. +#' The default `1` disables escalation, leaving search behaviour unchanged. +#' @param driftCycles Integer; number of drift search cycles. +#' @param driftAfdLimit Integer; maximum absolute fit difference (steps) for +#' accepting a suboptimal drift move. +#' @param driftRfdLimit Numeric; maximum relative fit difference for +#' accepting a suboptimal drift move. +#' @param xssRounds Integer; rounds of exclusive sectorial search. +#' @param xssPartitions Integer; number of partitions in XSS. +#' @param rssRounds Integer; rounds of random sectorial search. +#' @param cssRounds Integer; rounds of constrained (sector-restricted TBR) +#' sectorial search. +#' @param cssPartitions Integer; number of partitions in CSS. +#' @param sectorMinSize,sectorMaxSize Integer; minimum and maximum clade +#' sizes for sectorial search. +#' @param rasStarts Integer; random-addition restarts (RAS + TBR) per sector in +#' XSS/RSS. `1` (default) polishes the current sector with a single TBR pass; +#' `n > 1` rebuilds the sector from scratch `n` times and keeps the best, +#' following \insertCite{Goloboff1999;textual}{TreeSearch} RSS (TNT uses 3). +#' Lets the search escape sector-local optima that a single TBR cannot leave. +#' @param sectorAcceptEqual Logical; accept equal-score sector resolutions in +#' XSS/RSS (default `FALSE`). On flat (e.g. missing-data) landscapes this lets +#' the search traverse equally-parsimonious plateaus laterally rather than +#' reverting every non-improving sector move, following Goloboff (2014). +#' @param sectorMaxHits Integer; equal-length trees the internal sector TBR holds +#' while swapping a sector (default `1`). TNT holds many; higher values let the +#' sector search traverse equally-parsimonious plateaus (pairs with +#' `sectorAcceptEqual`). +#' @param sectorCollapseTarget Integer; when `> 0`, a selected sector clade larger +#' than this is **collapsed** into approximately this many composite terminals +#' (deep sub-clades replaced by their first-pass state sets), so the sector +#' search rearranges major sub-clades as a coarse skeleton rather than shuffling +#' tips within a contiguous clade -- the reduced-dataset construction of +#' \insertCite{Goloboff1999;textual}{TreeSearch}. `0` (default) keeps the full +#' fully-resolved clade. +#' @param postRatchetSectorial Logical; when `TRUE`, run XSS+RSS+CSS again +#' after ratchet perturbation using the same round counts. Approximates +#' TNT's interleaved sectorial pattern. Default: `FALSE`. +#' @param fuseInterval Integer; fuse pool trees every _n_ replicates. +#' @param fuseAcceptEqual Logical; accept equally-scoring fused trees? +#' @param intraFuse Logical; fuse the current tree against pool donors +#' within each replicate, after TBR polish. This approximates TNT's +#' within-replicate fusing pattern. Default: `FALSE`. +#' @param poolMaxSize Integer; maximum trees retained in the pool. +#' @param poolSuboptimal Numeric; retain trees that are this many steps +#' worse than the best tree. 0 (default) keeps only optimal trees. +#' @param consensusStableReps Integer; stop when the strict consensus of +#' best-score pool trees has been unchanged for this many consecutive +#' replicates. +#' 0 (default) disables this criterion; a typical value is 3--5. +#' When both `consensusStableReps` and `targetHits` are active, the search +#' stops when either criterion is met first. +#' @param perturbStopFactor Integer; stop when the number of consecutive +#' replicates that fail to improve the best score exceeds +#' `(targetHits / hits) * nTip * perturbStopFactor`, where `hits` is +#' the number of replicates that have independently found the best score +#' so far. This scales patience inversely with progress toward +#' `targetHits`: with few hits the threshold is large (more persistence); +#' as hits approach `targetHits` the threshold converges to the flat +#' `nTip * perturbStopFactor` limit. Before any hit has been found +#' (`hits == 0`) the criterion does not fire. +#' When `targetHits` is disabled (0), falls back to the flat +#' `nTip * perturbStopFactor` limit. +#' 0 disables this criterion entirely. +#' Default 2. +#' Inspired by IQ-TREE's unsuccessful-perturbation stopping rule +#' \insertCite{Nguyen2015}{TreeSearch}; adapted from per-perturbation to +#' per-replicate granularity. +#' @param adaptiveLevel Logical; dynamically scale ratchet and drift effort +#' based on the observed hit rate? When `TRUE`, easy landscapes +#' (high hit rate) trigger reduced effort per replicate, while hard +#' landscapes trigger increased effort. Default `FALSE`. +#' @param nniPerturbCycles Integer; number of stochastic NNI-perturbation +#' cycles per replicate. Each cycle randomly applies NNI swaps to a +#' fraction of internal branches, then runs TBR to find a new local +#' optimum. Complementary to the weight-perturbation ratchet: the ratchet +#' perturbs the objective function, while NNI-perturbation perturbs the +#' topology directly. +#' 0 (default) disables NNI perturbation. +#' Inspired by `doRandomNNIs()` in IQ-TREE +#' \insertCite{Nguyen2015}{TreeSearch}. +#' @param nniPerturbFraction Numeric (0--1); fraction of internal branches +#' to swap during each NNI-perturbation cycle. Default 0.5. +#' @param pruneReinsertCycles Integer; number of taxon pruning-reinsertion +#' perturbation cycles per replicate. Each cycle drops a fraction of leaves, +#' runs TBR on the reduced tree to let the backbone restructure, then +#' greedily reinserts the dropped taxa via Wagner addition and TBR-polishes +#' the full tree. Complementary to the ratchet (which perturbs character +#' weights) and NNI-perturbation (which perturbs the topology directly). +#' 0 (default) disables this perturbation. +#' @param pruneReinsertDrop Numeric (0--1); fraction of tips to drop per +#' cycle. Default 0.10 (10%). Always drops at least 3 tips and keeps +#' at least 4. +#' @param pruneReinsertSelection Integer; tip selection strategy for choosing +#' which tips to drop: +#' - `0` = random (default). +#' - `1` = instability-weighted: tips whose parent-edge split is rare across +#' pool trees are preferentially dropped. Requires \eqn{\ge}2 pool trees; +#' falls back to random otherwise. +#' - `2` = missing-data-weighted: tips with more ambiguous or inapplicable +#' characters are preferentially dropped. High-missingness taxa are +#' hardest to score correctly and most likely to be trapped in suboptimal +#' positions. +#' - `3` = combined: weight = instability × (1 + normalised missingness). +#' Targets taxa that are both unstably placed and data-poor. +#' @param pruneReinsertTbrMoves Integer; maximum number of TBR moves accepted +#' during the reduced-tree backbone optimisation phase of each +#' prune-reinsert cycle. 0 means run to convergence; the default of 5 +#' mirrors the ratchet design (short perturbation, many diverse cycles) +#' and substantially reduces per-cycle cost on datasets with inapplicable +#' characters (where Brazeau scoring dominates). Increase towards 0 if +#' you prefer thorough backbone optimisation over replicate throughput. +#' @param pruneReinsertFullMoves Integer; maximum TBR moves during the +#' full-tree polish after each prune-reinsert cycle. 0 (default) runs +#' to convergence. Has no effect when `pruneReinsertNni = TRUE`. +#' @param pruneReinsertNni Logical; if `TRUE`, use NNI (nearest-neighbour +#' interchange) instead of TBR for the full-tree polish step. NNI +#' converges roughly 5x faster than TBR at large tip counts (\eqn{\ge}120), +#' substantially reducing per-cycle cost while still reaching a local +#' optimum before the outer-loop TBR polish. Default `FALSE`. +#' @param consensusConstrain Logical; lock the strict consensus of pool +#' trees as topological constraints for subsequent replicates? When +#' `TRUE`, after enough replicates (\eqn{\ge}5), splits present in ALL +#' best-score pool trees are enforced as constraints, focusing search on +#' uncertain regions. Constraints are cleared whenever a new best score +#' is found. Only active when no user-supplied `constraint` is +#' present. Default `FALSE`. +#' @param wagnerBias Integer; criterion for biasing taxon addition order +#' during Wagner tree construction. 0 = random (default), +#' 1 = Goloboff (2014) non-ambiguous-character priority, +#' 2 = entropy-based state-specificity priority. Biased orders use +#' softmax-weighted sampling for diversity across replicates. +#' @param wagnerBiasTemp Numeric; softmax temperature controlling +#' selectivity of biased Wagner addition (default 0.3). Lower values +#' concentrate sampling on the highest-scoring taxa; higher values +#' approach uniform random. +#' @param outerCycles Integer; number of outer search cycles per replicate +#' (default 1). Each outer cycle runs the full +#' \[XSS/RSS/CSS → ratchet → NNI-perturbation → drift → TBR\] sequence, +#' with perturbation cycles divided evenly among outer iterations. +#' Matches the interleaved sectorial + ratchet pattern of TNT's `xmult` +#' \insertCite{Goloboff1999}{TreeSearch}. +#' @param maxOuterResets Integer; maximum number of improvement-triggered +#' resets of the outer cycle counter (default 0 = no resets, so +#' `outerCycles` is exact). When the search finds a new best score during +#' an outer cycle, the counter resets up to this many times, allowing +#' productive re-exploration. Set to \eqn{-1} for unlimited resets. +#' Strategy presets (`"default"`, `"thorough"`) set 2–3. +#' @param annealCycles Integer; number of simulated annealing perturbation +#' cycles (PCSA) per replicate. Each cycle perturbs the current best tree +#' via scheduled SA cooling, then reconverges with TBR. If the result +#' improves on the best, it becomes the new starting point. Effective at +#' escaping deep basins under equal-weights parsimony at \eqn{\ge}100 tips. +#' 0 (default) disables SA perturbation. +#' @param annealPhases Integer; number of temperature steps in the linear +#' cooling schedule per SA cycle (default 5). +#' @param annealTStart Numeric; initial Boltzmann temperature for SA cooling +#' schedule (default 20). Higher temperatures accept more suboptimal moves. +#' @param annealTEnd Numeric; final Boltzmann temperature (default 0 = +#' strict hill-climbing at end of each cycle). +#' @param annealMovesPerPhase Integer; stochastic TBR moves per temperature +#' step (default 0 = number of tips). +#' @param enumTimeFraction Numeric between 0 and 0.5; fraction of `maxSeconds` +#' reserved for MPT enumeration (TBR plateau walk to discover additional +#' equal-score topologies). The main search loop exits at +#' `maxSeconds * (1 - enumTimeFraction)`. Set to 0 to disable the reserve +#' (pre-v1.6 behaviour: enumeration skipped if the main loop times out). +#' Default: `0.1` (10%). +#' @param adaptiveStart Logical; use Thompson-sampling (bandit) strategy +#' selection for starting trees? When `TRUE`, each replicate draws its +#' starting strategy from a pool of options (random Wagner, biased Wagner, +#' random tree, pool ratchet, pool NNI-perturb), adapting to which +#' strategies yield the best scores. Default `FALSE`. +#' +#' @return A named list of class `"SearchControl"`. +#' +#' @examples +#' # Use defaults +#' SearchControl() +#' +#' # Light ratchet, no drift +#' SearchControl(ratchetCycles = 5L, ratchetPerturbProb = 0.04, +#' driftCycles = 0L) +#' +#' @family tree search functions +#' @seealso [`MaximizeParsimony()`] +#' @references +#' \insertAllCited{} +#' @export +SearchControl <- function( + # TBR + tbrMaxHits = 1L, + # TBR clip ordering strategy (experimental). + # 0L=RANDOM (default), 1L=INV_WEIGHT (w=1/(1+s)), 2L=TIPS_FIRST, + # 3L=BUCKET (tips/small/large), 4L=ANTI_TIP (non-tips first), + # 5L=LARGE_FIRST (large then small then tips) + clipOrder = 0L, + nniFirst = TRUE, + sprFirst = FALSE, + tabuSize = 100L, + wagnerStarts = 1L, + # Wagner biased addition (Goloboff 2014 §3.3) + # 0L = random (default), 1L = Goloboff non-ambiguous score, 2L = entropy + wagnerBias = 0L, + wagnerBiasTemp = 0.3, + # Outer search cycle count (Goloboff 1999 §2.3) + # Repeat [XSS → Ratchet → NNI-perturb → Drift → TBR] this many times. + # Cycles are divided evenly; default 1 = single pipeline pass. + outerCycles = 1L, + # Max improvement-triggered resets of the outer cycle counter. + # 0 = no resets (outerCycles is exact); -1 = unlimited. + # Strategy presets set 2-3 for productive re-exploration. + maxOuterResets = 0L, + # Ratchet + # Default 12->6 (T-P5d, 2026-06-19): the ratchet was over-provisioned; + # halving cycles saved 20-38% wall at zero quality loss on the mid-size EW + # benchmarks. The `large` preset keeps 12 (deliberate large-tree tradeoff, + # T-179) and is unaffected by this formal-default change. + ratchetCycles = 6L, + ratchetPerturbProb = 0.25, + ratchetPerturbMode = 0L, + ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = FALSE, + ratchetTaper = FALSE, + stallEscalateFactor = 1.0, + # NNI perturbation + nniPerturbCycles = 0L, + nniPerturbFraction = 0.5, + # Drift + driftCycles = 0L, + driftAfdLimit = 5L, + driftRfdLimit = 0.15, + # Sectorial + xssRounds = 3L, + xssPartitions = 4L, + rssRounds = 1L, + cssRounds = 0L, + cssPartitions = 4L, + sectorMinSize = 6L, + sectorMaxSize = 50L, + rasStarts = 1L, + sectorAcceptEqual = FALSE, + sectorMaxHits = 1L, + sectorCollapseTarget = 0L, + postRatchetSectorial = FALSE, + # Fuse / pool + fuseInterval = 3L, + fuseAcceptEqual = FALSE, + intraFuse = FALSE, + poolMaxSize = 100L, + poolSuboptimal = 0, + # Stopping criteria + consensusStableReps = 0L, + perturbStopFactor = 2L, + adaptiveLevel = FALSE, + consensusConstrain = FALSE, + # Taxon pruning-reinsertion (T-266) + pruneReinsertCycles = 0L, + pruneReinsertDrop = 0.10, + pruneReinsertSelection = 0L, + pruneReinsertTbrMoves = 5L, + pruneReinsertFullMoves = 0L, + pruneReinsertNni = FALSE, + # Simulated annealing perturbation (PCSA, T-207) + annealCycles = 0L, + annealPhases = 5L, + annealTStart = 20, + annealTEnd = 0, + annealMovesPerPhase = 0L, + # Adaptive starting-tree strategy (T-190) + # When TRUE, each replicate draws its starting strategy via Thompson + # sampling from {Wagner-random, Wagner-Goloboff, Wagner-entropy, + # random-tree, pool-ratchet, pool-NNI-perturb}. Overrides wagnerBias. + adaptiveStart = FALSE, + enumTimeFraction = 0.1 +) { + # Guard the count parameters whose non-positive values crash the C++ kernel: + # `xssPartitions`/`cssPartitions` divide the tip count in `xss_partition()` + # (integer division by zero -> SIGFPE), and `poolMaxSize` sizes the tree pool + # whose eviction branch reads `entries_[0]` once `size >= max_size` + # (an out-of-bounds read on an empty pool -> segfault). Each must be >= 1. + for (.p in c("xssPartitions", "cssPartitions", "poolMaxSize")) { + .v <- as.integer(get(.p)) + if (length(.v) != 1L || is.na(.v) || .v < 1L) { + stop("`", .p, "` must be a single positive integer") + } + } + # `stallEscalateFactor` multiplies the ratchet perturbation probability when a + # run stalls; a value < 1 would *shrink* perturbation on stalling (the wrong + # direction), and the C++ escalator treats exactly 1 as "off". + .se <- as.double(stallEscalateFactor) + if (length(.se) != 1L || is.na(.se) || .se < 1) { + stop("`stallEscalateFactor` must be a single number >= 1") + } + structure( + list( + tbrMaxHits = as.integer(tbrMaxHits), + clipOrder = as.integer(clipOrder), + nniFirst = as.logical(nniFirst), + sprFirst = as.logical(sprFirst), + tabuSize = as.integer(tabuSize), + wagnerStarts = as.integer(wagnerStarts), + wagnerBias = as.integer(wagnerBias), + wagnerBiasTemp = as.double(wagnerBiasTemp), + outerCycles = as.integer(outerCycles), + maxOuterResets = as.integer(maxOuterResets), + ratchetCycles = as.integer(ratchetCycles), + ratchetPerturbProb = as.double(ratchetPerturbProb), + ratchetPerturbMode = as.integer(ratchetPerturbMode), + ratchetPerturbMaxMoves = as.integer(ratchetPerturbMaxMoves), + ratchetAdaptive = as.logical(ratchetAdaptive), + ratchetTaper = as.logical(ratchetTaper), + stallEscalateFactor = as.double(stallEscalateFactor), + nniPerturbCycles = as.integer(nniPerturbCycles), + nniPerturbFraction = as.double(nniPerturbFraction), + driftCycles = as.integer(driftCycles), + driftAfdLimit = as.integer(driftAfdLimit), + driftRfdLimit = as.double(driftRfdLimit), + xssRounds = as.integer(xssRounds), + xssPartitions = as.integer(xssPartitions), + rssRounds = as.integer(rssRounds), + cssRounds = as.integer(cssRounds), + cssPartitions = as.integer(cssPartitions), + sectorMinSize = as.integer(sectorMinSize), + sectorMaxSize = as.integer(sectorMaxSize), + rasStarts = as.integer(rasStarts), + sectorAcceptEqual = as.logical(sectorAcceptEqual), + sectorMaxHits = as.integer(sectorMaxHits), + sectorCollapseTarget = as.integer(sectorCollapseTarget), + postRatchetSectorial = as.logical(postRatchetSectorial), + fuseInterval = as.integer(fuseInterval), + fuseAcceptEqual = as.logical(fuseAcceptEqual), + intraFuse = as.logical(intraFuse), + poolMaxSize = as.integer(poolMaxSize), + poolSuboptimal = as.double(poolSuboptimal), + consensusStableReps = as.integer(consensusStableReps), + perturbStopFactor = as.integer(perturbStopFactor), + adaptiveLevel = as.logical(adaptiveLevel), + consensusConstrain = as.logical(consensusConstrain), + pruneReinsertCycles = as.integer(pruneReinsertCycles), + pruneReinsertDrop = as.double(pruneReinsertDrop), + pruneReinsertSelection = as.integer(pruneReinsertSelection), + pruneReinsertTbrMoves = as.integer(pruneReinsertTbrMoves), + pruneReinsertFullMoves = as.integer(pruneReinsertFullMoves), + pruneReinsertNni = as.logical(pruneReinsertNni), + annealCycles = as.integer(annealCycles), + annealPhases = as.integer(annealPhases), + annealTStart = as.double(annealTStart), + annealTEnd = as.double(annealTEnd), + annealMovesPerPhase = as.integer(annealMovesPerPhase), + adaptiveStart = as.logical(adaptiveStart), + enumTimeFraction = as.double(enumTimeFraction) + ), + class = "SearchControl" + ) +} + +#' @export +print.SearchControl <- function(x, ...) { + groups <- list( + "TBR" = c("tbrMaxHits", "clipOrder", "nniFirst", "sprFirst", "tabuSize", + "wagnerStarts", "wagnerBias", "wagnerBiasTemp", "outerCycles", + "maxOuterResets"), + "Ratchet" = c("ratchetCycles", "ratchetPerturbProb", "ratchetPerturbMode", + "ratchetPerturbMaxMoves", "ratchetAdaptive", + "ratchetTaper", "stallEscalateFactor"), + "NNI Perturbation" = c("nniPerturbCycles", "nniPerturbFraction"), + "Drift" = c("driftCycles", "driftAfdLimit", "driftRfdLimit"), + "Prune-Reinsert" = c("pruneReinsertCycles", "pruneReinsertDrop", + "pruneReinsertSelection", "pruneReinsertTbrMoves", + "pruneReinsertFullMoves", "pruneReinsertNni"), + "Annealing" = c("annealCycles", "annealPhases", "annealTStart", + "annealTEnd", "annealMovesPerPhase"), + "Sectorial" = c("xssRounds", "xssPartitions", "rssRounds", + "cssRounds", "cssPartitions", + "sectorMinSize", "sectorMaxSize", "rasStarts", + "sectorAcceptEqual", "sectorMaxHits", "sectorCollapseTarget", + "postRatchetSectorial"), + "Fuse/Pool" = c("fuseInterval", "fuseAcceptEqual", "intraFuse", + "poolMaxSize", "poolSuboptimal"), + "Stopping" = c("consensusStableReps", "perturbStopFactor", + "adaptiveLevel", + "consensusConstrain", "adaptiveStart", + "enumTimeFraction") + ) + cat("SearchControl object\n") + for (gname in names(groups)) { + cat(sprintf(" %s:\n", gname)) + for (pname in groups[[gname]]) { + cat(sprintf(" %-25s = %s\n", pname, format(x[[pname]]))) + } + } + invisible(x) +} diff --git a/R/SuccessiveApproximations.R b/R/SuccessiveApproximations.R index 33f204932..d5a414645 100644 --- a/R/SuccessiveApproximations.R +++ b/R/SuccessiveApproximations.R @@ -8,11 +8,17 @@ #' @param outgroup if not NULL, taxa on which the tree should be rooted #' @param k Constant for successive approximations, see Farris 1969 p. 379 #' @param maxSuccIter maximum iterations of successive approximation -#' @param ratchetHits maximum hits for parsimony ratchet -#' @param searchHits maximum hits in tree search -#' @param searchIter maximum iterations in tree search -#' @param ratchetIter maximum iterations of parsimony ratchet -#' @param suboptimal retain trees that are this proportion less optimal than the optimal tree +#' @param ratchetHits Number of replicates. +#' Internally capped at 100 and passed to the C++ engine as `maxReplicates`. +#' @param searchHits Convergence criterion: stop after finding this many +#' trees with the best score. +#' Internally capped at 10 and passed to the C++ engine as `targetHits`. +#' @param searchIter Unused (retained for backward compatibility). +#' @param ratchetIter Controls ratchet intensity within each replicate. +#' Converted to `ratchetCycles` (approximately `ratchetIter / 500`, +#' capped at 10). +#' @param suboptimal Retain trees that are this proportion less optimal +#' than the optimal tree. #' #' @return `SuccessiveApproximations()` returns a list of class `multiPhylo` #' containing optimal (and slightly suboptimal, if suboptimal > 0) trees. @@ -27,49 +33,113 @@ SuccessiveApproximations <- function (tree, dataset, outgroup = NULL, k = 3, maxSuccIter = 20, ratchetHits = 100, searchHits = 50, searchIter = 500, ratchetIter = 5000, verbosity = 0, - suboptimal = 0.1) { - - if (k < 1) stop ("k should be at least 1, see Farris 1969 p.379") - attr(dataset, "sa.weights") <- rep.int(1, length(attr(dataset, "weight"))) - collectSuboptimal <- suboptimal > 0 - - max.node <- max(tree[["edge"]][, 1]) - n.tip <- length(tree[["tip.label"]]) - n.node <- max.node - n.tip - bests <- vector("list", maxSuccIter + 1L) - bestsConsensus <- vector("list", maxSuccIter + 1L) - best <- bests[[1]] <- bestsConsensus[[1]] <- root(tree, outgroup, resolve.root=TRUE) - for (i in seq_len(maxSuccIter) + 1L) { - if (verbosity > 0) message("\nSuccessive Approximations Iteration ", i - 1L) - attr(best, "score") <- NULL - if (suboptimal > 0) { - suboptimalSearch <- suboptimal * sum(attr(dataset, "sa.weights") * - attr(dataset, "weight")) - } - trees <- Ratchet(best, dataset, TreeScorer = SuccessiveWeights, - all = collectSuboptimal, - suboptimal = suboptimalSearch, - rearrangements = "NNI", - ratchetHits=ratchetHits, searchHits = searchHits, - searchIter = searchIter, ratchetIter = ratchetIter, - outgroup = outgroup, verbosity = verbosity - 1) - trees <- unique(trees) - bests[[i]] <- trees - suboptimality <- Suboptimality(trees) - bestsConsensus[[i]] <- consensus(trees[suboptimality == 0]) - if (all.equal(bestsConsensus[[i]], bestsConsensus[[i - 1]])) { - return(bests[2:i]) + suboptimal = 0.1, + concavity = Inf, + constraint = NULL, + extended_iw = TRUE, + xpiwe_r = 0.5, + xpiwe_max_f = 5) { + + if (k < 1) stop("k should be at least 1, see Farris 1969 p.379") + + if (!inherits(dataset, "phyDat")) { + stop("`dataset` must be of class `phyDat`.") + } + + # Profile parsimony: prepare data + useProfile <- identical(concavity, "profile") + if (useProfile) { + dataset <- PrepareDataProfile(dataset) + concavity <- Inf + } + if (is.finite(concavity) && concavity <= 0) { + stop("`concavity` must be positive (or Inf for equal weights, ", + "or \"profile\" for profile parsimony).") + } + + nTip <- length(dataset) + + # Extract data for C++ engine + at <- attributes(dataset) + contrast <- at$contrast + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = nTip, byrow = TRUE) + weight <- .ScaleWeight(at$weight) + levels <- at$levels + + # Prepare constraint + consArgs <- .PrepareConstraint(constraint = constraint, dataset = dataset) + + # Profile parsimony: extract info_amounts + profileArgs <- list() + if (useProfile) { + infoAmounts <- attr(dataset, "info.amounts") + if (!is.null(infoAmounts) && length(infoAmounts) > 0L) { + profileArgs$infoAmounts <- infoAmounts } - best <- trees[suboptimality == 0][[1]] - l.i <- CharacterLength(best, dataset, compress = TRUE) - p.i <- l.i / (n.node - 1) - w.i <- ((p.i)^-k) - 1 - attr(dataset, "sa.weights") <- w.i } - message("Stability not reached.") - - # Return: - structure(bests, class = "multiPhylo") + + # XPIWE: compute per-pattern observed-taxa counts + useXpiwe <- isTRUE(extended_iw) && is.finite(concavity) && !useProfile + if (useXpiwe) { + obsCount <- .ObsCount(dataset) + } + + searchArgs <- list( + contrast = contrast, + tip_data = tip_data, + weight = weight, + levels = levels, + saK = as.double(k), + maxSAIter = as.integer(maxSuccIter), + maxReplicates = as.integer(min(ratchetHits, 100L)), + targetHits = as.integer(min(searchHits, 10L)), + tbrMaxHits = 1L, + ratchetCycles = as.integer(min(ceiling(ratchetIter / 500), 10L)), + min_steps = if (is.finite(concavity)) + as.integer(MinimumLength(dataset, compress = TRUE)) else integer(0), + concavity = as.double(concavity), + xpiwe = useXpiwe, + xpiwe_r = as.double(xpiwe_r), + xpiwe_max_f = as.double(xpiwe_max_f), + obs_count = if (useXpiwe) obsCount else integer(0) + ) + result <- do.call(ts_successive_approx, c(searchArgs, consArgs, profileArgs)) + + if (result$converged && verbosity > 0) { + message("Successive approximations converged after ", + result$sa_iterations, " iteration(s).") + } else if (!result$converged) { + message("Stability not reached after ", result$sa_iterations, + " iteration(s).") + } + + # Reconstruct phylo from C++ edge matrix + if (nrow(result$edge) == 0L) { + tr <- if (!missing(tree) && inherits(tree, "phylo")) tree + else AdditionTree(dataset) + attr(tr, "score") <- result$score + } else { + tr <- structure( + list(edge = result$edge, + tip.label = names(dataset), + Nnode = nTip - 1L), + class = "phylo" + ) + attr(tr, "score") <- result$score + } + + if (!is.null(outgroup)) { + tr <- root(tr, outgroup, resolve.root = TRUE) + } + + structure( + list(tr), + score = result$score, + sa_iterations = result$sa_iterations, + converged = result$converged, + class = "multiPhylo" + ) } #' Tree suboptimality diff --git a/R/TBR.R b/R/TBR.R index 3c84dd624..d0bf968ef 100644 --- a/R/TBR.R +++ b/R/TBR.R @@ -39,8 +39,8 @@ TBRWarning <- function (parent, child, error) { #' #' @return `TBR()` returns a tree in \code{phyDat} format that has undergone one #' \acronym{TBR} iteration. -#' @references The \acronym{TBR} algorithm is summarized in -#' \insertRef{Felsenstein2004}{TreeSearch} +#' @references \insertCite{Felsenstein2004}{TreeSearch} +#' \insertAllCited{} #' #' @examples #' library("ape") @@ -102,7 +102,6 @@ TBRMoves.matrix <- function (tree, edgeToBreak = integer(0)) { unique(allMoves) } -## TODO Do edges need to be pre-ordered before coming here? #' @describeIn TBR faster version that takes and returns parent and child #' parameters #' @inheritParams TreeTools::NeworderPhylo @@ -117,7 +116,7 @@ TBRSwap <- function(parent, child, nEdge = length(parent), edgeToBreak = NULL, mergeEdges = NULL) { if (nEdge < 5) { - return (list(parent, child)) #TODO do we need to re-root this tree? + return (list(parent, child)) } # Pick an edge at random @@ -361,7 +360,6 @@ RootedTBRSwap <- function (parent, child, nEdge=length(parent), if (sum(subtreeEdges, -edgesCutAdrift) > 2) { break; # the edge itself, and somewheres else } - # TODO check that all expected selections are valid selectableEdges[edgeToBreak] <- FALSE ###Assert(any(selectableEdges)) edgeToBreak <- SampleOne(which(selectableEdges)) diff --git a/R/TaxonInfluence.R b/R/TaxonInfluence.R index 51397ddfc..dd73e00d9 100644 --- a/R/TaxonInfluence.R +++ b/R/TaxonInfluence.R @@ -73,17 +73,19 @@ #' #' @template MRS #' @examples -#' #' # Load data for analysis in R +#' # Load data for analysis in R #' library("TreeTools") #' data("congreveLamsdellMatrices", package = "TreeSearch") -#' +#' #' # Small dataset for demonstration purposes #' dataset <- congreveLamsdellMatrices[[42]][1:8, ] +#' +#' \donttest{ # The tree searches below take a few seconds to run #' bestTree <- MaximizeParsimony(dataset, verbosity = 0)[[1]] -#' +#' #' # Calculate tip influence -#' influence <- TaxonInfluence(dataset, ratchIt = 0, startIt = 0, verbos = 0) -#' +#' influence <- TaxonInfluence(dataset, maxReplicates = 2, verbosity = 0) +#' #' # Colour tip labels according to their influence #' upperBound <- 2 * TreeDist::ClusteringEntropy( #' PectinateTree(NTip(dataset) - 1)) @@ -94,19 +96,23 @@ #' include.lowest = TRUE #' ) #' palette <- hcl.colors(nBin, "inferno") -#' +#' #' plot(bestTree, tip.color = palette[bin]) -#' PlotTools::SpectrumLegend( -#' "bottomleft", -#' palette = palette, -#' title = "Tip influence / bits", -#' legend = signif(seq(upperBound, 0, length.out = 4), 3), -#' bty = "n" -#' ) +#' # SpectrumLegend() needs the PlotTools package (a Suggests) +#' if (requireNamespace("PlotTools", quietly = TRUE)) { +#' PlotTools::SpectrumLegend( +#' "bottomleft", +#' palette = palette, +#' title = "Tip influence / bits", +#' legend = signif(seq(upperBound, 0, length.out = 4), 3), +#' bty = "n" +#' ) +#' } +#' } #' @family tree scoring #' @importFrom ape read.nexus write.nexus #' @importFrom cli cli_alert_info cli_h1 -#' @importFrom fs path_sanitize + #' @importFrom stats weighted.mean #' @importFrom TreeDist ClusteringInfoDistance #' @encoding UTF-8 @@ -141,19 +147,15 @@ TaxonInfluence <- function( } } - startTree <- MakeTreeBinary(if (inherits(tree, "phylo")) { - tree - } else { - tree[[1]] - }) - if (!inherits(startTree, "phylo")) { + refTree <- if (inherits(tree, "phylo")) tree else tree[[1]] + if (!inherits(refTree, "phylo")) { stop("`tree` must be an object / list of objects of class \"phylo\"") } # Return: vapply(names(dataset), function(leaf) { - leafFile <- paste0(savePath, path_sanitize(leaf), ".nex") + leafFile <- paste0(savePath, gsub("[/\\\\:*?\"<>|[:cntrl:]]", "_", leaf), ".nex") result <- if (useCache && file.exists(leafFile)) { if (verbosity > 1) { @@ -171,7 +173,6 @@ TaxonInfluence <- function( } result <- unique(MaximizeParsimony( dataset = dataset[setdiff(names(dataset), leaf)], - tree = DropTip(startTree, leaf), verbosity = verbosity, ... )) diff --git a/R/WhenFirstHit.R b/R/WhenFirstHit.R index 5e0fbad36..048c8fff5 100644 --- a/R/WhenFirstHit.R +++ b/R/WhenFirstHit.R @@ -4,7 +4,7 @@ #' This information is read from the `firstHit` attribute if present. #' If not, trees are taken to be listed in the order in which they were found, #' and named according to the search iteration in which they were first hit - -#' the situation when trees found by [`MaximizeParsimony()`] are saved to file. +#' the situation when trees found by [`Morphy()`] are saved to file. #' #' @param trees A list of trees, or a `multiPhylo` object. #' @return `trees`, with a `firstHit` attribute listing the number of trees hit @@ -23,7 +23,7 @@ #' attr(WhenFirstHit(trees), "firstHit") #' @family utility functions #' @seealso -#' - [`MaximizeParsimony()`] +#' - [`Morphy()`] #' @export WhenFirstHit <- function(trees) { if (is.null(attr(trees, "firstHit"))) { diff --git a/R/WideSample.R b/R/WideSample.R new file mode 100644 index 000000000..e06adc779 --- /dev/null +++ b/R/WideSample.R @@ -0,0 +1,387 @@ +#' Select a topologically diverse subset of trees +#' +#' Selects `n` trees from a `multiPhylo` object that are as topologically +#' distinct from one another as possible, by solving the Max-Min Diversity +#' Problem (MMDP): maximize the *minimum* pairwise distance within the chosen +#' subset. This is useful when a search returns many most-parsimonious trees +#' and downstream analyses (consensus, tree-space visualization) need a +#' manageable but diverse subset. +#' +#' Uniform random subsampling of MPTs is misleading: the number of trees in a +#' region of tree space reflects the density of the parsimony landscape, not +#' the likelihood or support for that topology. A random draw over-represents +#' topologies that sit on broad plateaux and under-represents isolated optima. +#' `WideSample()` instead selects for topological *spread*, density-blind, by +#' dispatching to the appropriate Max-Min Diversity Problem solver from the +#' \pkg{MaxMin} package: +#' +# TODO replace {TreeSearch} refs with {MaxMin} once package on CRAN and +# imported, and remove refs from inst/REFERENCES.bib (DRY) +#' \describe{ +#' \item{`FarFirst()` (`effort = 1`)}{Greedy farthest-first selection +#' \insertCite{Gonzalez1985}{TreeSearch} from a peripheral seed. +#' Fast and matrix-free: the only feasible option for very large tree sets.} +#' \item{`DropAdd()` (`effort = 2`)}{Drop-add tabu search +#' \insertCite{Porumbel2011}{TreeSearch}: a ~99%-optimal heuristic that +#' terminates at a deterministic plateau. +#' Requires the full distance matrix.} +#' \item{`Grasp()` (`effort = 3`)}{GRASP with path relinking +#' \insertCite{@Resende2010}{TreeSearch}: attains the highest \eqn{T_k} of the package's +#' heuristics, at a cost that grows steeply with `n`. Requires the full +#' distance matrix. Draws on the session RNG, so the particular trees it +#' returns vary between runs unless you call [set.seed()] first (the +#' achieved diversity is essentially unaffected).} +#' \item{exact (`effort = 4`)}{Node-packing integer program +#' \insertCite{@Sayyady2016}{TreeSearch}: the proven optimum. The solver is now +#' sparse-matrix and heuristic warm-started, so it is practical up to a few +#' hundred trees; it needs the \pkg{highs} package. The optimal +#' *diversity* is deterministic, but when several subsets are tied-optimal +#' the particular trees returned can vary with the session RNG.} +#' } +#' +#' With `effort = NULL` (default) the tier is chosen automatically from +#' `length(trees)`: the exact solver for small sets (up to ~200 trees, when +#' \pkg{highs} is available), `DropAdd()` while the distance matrix is +#' affordable to build, and `FarFirst()` beyond that. +#' `Grasp()` (`effort = 3`) is never selected automatically, as its cost grows +#' steeply with `n`. A dense +#' distance matrix is roughly `8 * length(trees)^2` bytes (about 1.1 GB at +#' 12,000 trees, 12.8 GB at 40,000), so for the largest sets only the +#' matrix-free `FarFirst()` tier is reachable. +#' +#' Two size thresholds govern automatic selection; tune them for the host +#' machine with [options()] rather than per call: +#' \describe{ +#' \item{`WideSample.buildCeiling`}{Largest `length(trees)` for +#' which a dense distance matrix is built from a distance function (default +#' `12000`; ~1.1 GB). Beyond it only the matrix-free `FarFirst()` tier is +#' reachable from a function (a pre-computed matrix is always honoured).} +#' \item{`WideSample.exactCeiling`}{Largest `length(trees)` at +#' which automatic selection reaches the exact tier (default `200`).} +#' } +#' +#' @param trees A `multiPhylo` object, or a single `phylo` (coerced silently). +#' @param n Integer specifying number of trees to retain. +#' @param dist Either: +#' \itemize{ +#' \item A function giving pairwise distances (default: +#' [TreeDist::ClusteringInfoDistance()]). It must support the form +#' `dist(trees)` returning a `dist` object; for the largest tree sets it +#' is additionally called as `dist(trees[[i]], trees)` and must then +#' return a numeric vector of length `length(trees)` (the distances from +#' tree `i` to every tree). `ClusteringInfoDistance()` satisfies both. +#' \item A pre-computed `dist` object or square numeric matrix whose size +#' matches `length(trees)`. +#' } +#' @param effort Integer solver tier, or `NULL` (default) to choose +#' automatically by `length(trees)`. `1` = `FarFirst()` (fast, matrix-free), +#' `2` = `DropAdd()` (~99%-optimal, deterministic), `3` = `Grasp()` +#' (highest-quality heuristic, higher cost), `4` = exact optimum. +#' Setting `effort` 2, 3 or 4 with a distance function fails when a tree set +#' is too large to store the distance matrix in memory; pass a pre-computed +#' `dist` or use `effort = 1` for such sets. +#' @param maxSeconds Numeric: wall-clock budget, in seconds, for the +#' refinement (`effort = 2`, `3`) and exact (`effort = 4`) tiers. +#' Default `60`. +#' +#' @return A `multiPhylo` object of length `min(n, length(trees))` containing +#' a uniform sample of `trees`. +#' If `n == 1`, the single most central tree (the medoid) is returned. +#' Attributes of the input (e.g. `score`, `hits_to_best`) are preserved. +#' +#' @examples +#' library("TreeTools") +#' trees <- as.phylo(0:99, nTip = 8) +#' +#' # WideSample() needs the MaxMin package (Max-Min diversity solvers) +#' if (requireNamespace("MaxMin", quietly = TRUE)) { +#' +#' # Fast FarFirst subsample (deterministic, matrix-free) +#' sub10 <- WideSample(trees, 10, effort = 1) +#' length(sub10) # 10 +#' +#' \donttest{ +#' # Automatic tier selection (exact at this size when 'highs' is installed, +#' # otherwise the DropAdd heuristic) +#' auto10 <- WideSample(trees, 10) +#' +#' # Pre-computed distances +#' dists <- TreeDist::ClusteringInfoDistance(trees) +#' sub5 <- WideSample(trees, 5, dist = dists) +#' +#' # Highest-quality heuristic (Grasp); set a seed for a reproducible selection +#' set.seed(1) +#' sub8 <- WideSample(trees, 8, effort = 3) +#' +#' # Force the exact optimum on a small set (needs the 'highs' package) +#' if (requireNamespace("highs", quietly = TRUE)) { +#' sub4 <- WideSample(trees[1:20], 4, effort = 4) +#' } +#' } +#' +#' } +#' +#' @references +#' \insertRef{Gonzalez1985}{TreeSearch} +#' +#' \insertRef{Porumbel2011}{TreeSearch} +#' +#' \insertRef{Resende2010}{TreeSearch} +#' +#' \insertRef{Sayyady2016}{TreeSearch} +#' +#' @template MRS +#' @family tree scoring +#' @importFrom TreeDist ClusteringInfoDistance +#' @export +WideSample <- function( + trees, + n, + dist = TreeDist::ClusteringInfoDistance, + effort = NULL, + maxSeconds = 60 +) { + if (!requireNamespace("MaxMin", quietly = TRUE)) { + stop("`WideSample()` requires the 'MaxMin' package, which provides the ", + "Max-Min diversity solvers; install it from ", + "https://github.com/ms609/MaxMin", call. = FALSE) + } + # Build ceiling: largest N for which we materialize a dense N x N matrix from + # a distance function. ~1.1 GB at 12,000; as.matrix.dist overflows near + # 46,340 (the dist half-vector exceeds .Machine$integer.max). + buildCeiling <- getOption("WideSample.buildCeiling", 12000L) + # Exact ceiling: largest N at which auto-selection reaches the exact tier. + # MaxMin::ExactMaxMin() is now a sparse-matrix, heuristic-warm-started solver + # (~20x faster than the dense form), practical to a few hundred trees at the + # small `n` of interest; beyond that the node-packing IP wall bites (the + # MaxMin optimum sits near the diameter, where the threshold graph is + # near-complete). Kept conservative because the IP cost turns on `n` and + # instance structure, not on `length(trees)` alone. + exactCeiling <- getOption("WideSample.exactCeiling", 200L) + + if (inherits(trees, "phylo")) { + trees <- c(trees) + } else if (!inherits(trees, "multiPhylo")) { + stop("`trees` must be a multiPhylo object") + } + nTrees <- length(trees) + + n <- as.integer(n) + if (length(n) != 1L || is.na(n) || n < 0L) { + stop("`n` must be a single non-negative integer") + } + if (n >= nTrees) { + # Return: + return(trees) + } + if (n == 0L) { + # Return: + return(.SubsetMultiPhylo(trees, integer(0))) + } + + # --- classify `dist`: pre-computed matrix vs distance function ------------- + distIsMatrix <- inherits(dist, "dist") || + (is.matrix(dist) && is.numeric(dist)) + distIsFun <- is.function(dist) + if (!distIsMatrix && !distIsFun) { + stop("`dist` must be a function, a `dist` object, or a numeric matrix") + } + + dmat <- NULL + if (distIsMatrix) { + dmat <- as.matrix(dist) + if (nrow(dmat) != ncol(dmat)) { + stop("`dist` matrix must be square") + } + if (nrow(dmat) != nTrees) { + stop("`dist` has ", nrow(dmat), " rows but `trees` has ", nTrees, + " trees") + } + } + matrixAvailable <- !is.null(dmat) + + # A single tree has no pairwise distance to maximize; return the medoid (the + # most central tree) as the most representative single choice. Independent of + # `effort`/`maxSeconds`, so handled before they are validated. + if (n == 1L) { + # Return: + return(.SubsetMultiPhylo( + trees, .WideSampleMedoid(dist, trees, nTrees, dmat, buildCeiling) + )) + } + + if (!is.null(effort)) { + effort <- as.integer(effort) + if (length(effort) != 1L || is.na(effort) || !effort %in% 1:4) { + stop("`effort` must be NULL, 1, 2, 3, or 4") + } + } + + if (!is.numeric(maxSeconds) || length(maxSeconds) != 1L || + is.na(maxSeconds) || maxSeconds <= 0) { + stop("`maxSeconds` must be a single positive number (or Inf)") + } + + # --- select the solver tier on (matrix-available, N) ---------------------- + tier <- .SelectWideSampleTier(effort, matrixAvailable, nTrees, + buildCeiling, exactCeiling) + + # --- build the matrix when the chosen tier needs one (tiers 2-4) ---------- + # Tier 1 (FarFirst) stays matrix-free: it reads distances through a column + # oracle, so it never builds an N x N matrix it was not already handed. The + # matrix-bound tiers (DropAdd, Grasp, exact) have no oracle path. + if (tier > 1L && !matrixAvailable) { + # .SelectWideSampleTier guarantees nTrees <= buildCeiling here. + dmat <- as.matrix(dist(trees)) + matrixAvailable <- TRUE + } + + # --- dispatch ------------------------------------------------------------- + # switch() on an integer selects by position, so the cases MUST stay in tier + # order (1, 2, 3, 4); the backtick labels are cosmetic. + idx <- switch( + tier, + # Tier 1: FarFirst fed by a column oracle. Reading the oracle from a + # supplied matrix or from the on-demand tree callback feeds FarFirst the + # identical distances, so the selection does not depend on whether distances + # were pre-computed; the deterministic peripheral seed keeps it RNG-free. + `1` = { + colFn <- if (matrixAvailable) { + function(i) dmat[, i] + } else { + .WideSampleColumnOracle(dist, trees, nTrees) + } + MaxMin::FarFirst(n, colFn, N = nTrees) + }, + # Tier 2: DropAdd returns the bare (sorted) index vector; it runs to its + # deterministic plateau, with `maxSeconds` as a safety cap. + `2` = MaxMin::DropAdd(n, dmat, maxSeconds = maxSeconds), + # Tier 3: Grasp likewise returns the bare index vector (RNG-dependent). + `3` = MaxMin::Grasp(n, dmat, maxSeconds = maxSeconds), + # Tier 4: exact solver returns the bare (ascending) index vector, like the + # other tiers. + `4` = { + if (nTrees > exactCeiling) { + warning("Exact MMDP (effort = 4) on ", nTrees, + " trees may be very slow; consider effort = 2 (DropAdd) ", + "or 3 (Grasp), or a larger `maxSeconds`.", + immediate. = TRUE) + } + MaxMin::ExactMaxMin(k = n, dmat, maxSeconds = maxSeconds) + } + ) + + # FarFirst returns farthest-first (selection) order; sort to ascending tree + # order so the subset preserves the input ordering. A no-op for tiers 2-4, + # which already return ascending indices. + .SubsetMultiPhylo(trees, as.integer(idx)) +} + +#' Choose the `WideSample()` solver tier +#' +#' Keyed on whether a distance matrix is already available and on +#' `length(trees)`, never on N alone: a supplied matrix keeps the higher tiers +#' reachable past the build ceiling, whereas a distance function past the +#' ceiling cannot reach them (building the matrix would exhaust memory). The +#' exact tier is additionally gated on a (smaller) exact ceiling and on the +#' \pkg{highs} package being installed; `Grasp()` (`effort = 3`) is never +#' auto-selected. +#' @return Integer tier (1, 2, 3 or 4); errors when a forced effort is +#' unreachable. +#' @keywords internal +.SelectWideSampleTier <- function(effort, matrixAvailable, nTrees, ceiling, + exactCeiling = 200L, + highsAvailable = + requireNamespace("highs", quietly = TRUE)) { + if (is.null(effort)) { + if (nTrees <= exactCeiling && highsAvailable) { + # Return: + return(4L) # exact, when affordable and available + } + if (matrixAvailable || nTrees <= ceiling) { + # Return: + return(2L) # DropAdd (build matrix if needed) + } + # Return: + return(1L) # FarFirst, matrix-free + } + if (effort == 1L) { + # Return: + return(1L) + } + # effort 2 (DropAdd), 3 (Grasp) and 4 (exact) all need the full matrix. + if (matrixAvailable || nTrees <= ceiling) { + # Return: + return(effort) + } + stop("`effort = ", effort, "` needs a distance matrix, but ", nTrees, + " trees exceeds the build ceiling (", ceiling, ") and no pre-computed ", + "`dist` was supplied. Use `effort = 1` (FarFirst) for sets ", + "this large, or pass a pre-computed distance matrix.") +} + +#' The medoid tree, for the single-tree (`n == 1`) case +#' +#' Returns the index of the most central tree -- the medoid, minimizing summed +#' distance to all others. Uses the distance matrix when one is available or +#' affordable to build; when only a distance function is supplied for a set too +#' large to build a matrix, the central medoid is not affordable, so the +#' deterministic peripheral seed ([MaxMin::FarFirst()] with `k = 1`) is returned +#' as a matrix-free fallback. +#' @return Integer index (1-based) of the selected tree. +#' @keywords internal +.WideSampleMedoid <- function(dist, trees, nTrees, dmat, buildCeiling) { + if (is.null(dmat) && nTrees <= buildCeiling) { + dmat <- as.matrix(dist(trees)) + } + if (!is.null(dmat)) { + # Medoid: smallest summed distance to the rest (diagonal is zero, so it + # does not bias the sum). which.min breaks ties on the smallest index. + # Return: + which.min(rowSums(dmat)) + } else { + colFn <- .WideSampleColumnOracle(dist, trees, nTrees) + # Return: + as.integer(MaxMin::FarFirst(colFn, k = 1L, N = nTrees)) + } +} + +#' Build a column-oracle closure for the matrix-free `FarFirst()` path +#' +#' Returns a function of one 1-based index `i` giving the distances from tree +#' `i` to every tree, as required by the distance-column oracle path of +#' [MaxMin::FarFirst()]. Probes +#' the `(tree, trees)` calling form once up front and fails clearly if the +#' supplied `dist` function does not support it. +#' @keywords internal +.WideSampleColumnOracle <- function(dist, trees, nTrees) { + probe <- tryCatch( + dist(trees[[1L]], trees), + error = function(e) { + stop("`dist` must accept `dist(trees[[i]], trees)` for tree sets too ", + "large to build a full distance matrix; calling it raised: ", + conditionMessage(e), call. = FALSE) + } + ) + if (!is.numeric(probe) || length(probe) != nTrees) { + stop("`dist(trees[[i]], trees)` must return a numeric vector of length ", + nTrees, "; got ", + if (is.numeric(probe)) paste0("length ", length(probe)) + else class(probe)[[1L]], ".") + } + function(i) as.numeric(dist(trees[[i]], trees)) +} + +#' Subset a multiPhylo preserving attributes +#' @keywords internal +.SubsetMultiPhylo <- function(trees, idx) { + saved <- attributes(trees) + result <- trees[idx] + # Restore non-standard attributes (e.g. score, hits_to_best) + standard <- c("names", "class") + for (nm in setdiff(names(saved), standard)) { + attr(result, nm) <- saved[[nm]] + } + # Return: + result +} diff --git a/R/data.R b/R/data.R index 5ba9a21db..9a9670059 100644 --- a/R/data.R +++ b/R/data.R @@ -174,9 +174,8 @@ #' #' @format A single phylogenetic tree saved as an object of class \code{phylo} #' -#' @references -#' \insertRef{Congreve2016}{TreeSearch} -#' \insertRef{Congreve2016dd}{TreeSearch} +#' @references \insertCite{Congreve2016,Congreve2016dd}{TreeSearch} +#' \insertAllCited{} #' #' @examples #' data(referenceTree) diff --git a/R/data_manipulation.R b/R/data_manipulation.R index d3d4b8b4c..db291868e 100644 --- a/R/data_manipulation.R +++ b/R/data_manipulation.R @@ -1,18 +1,58 @@ +# Feasibility thresholds for MaddisonSlatkin exact computation. +# The split_count is the coefficient of x^floor(n/2) in the generating +# polynomial prod_i (1 + x + ... + x^{a_i}), capturing partition shape. +# Calibrated from worst-case (balanced) partition timing experiments +# using bitmask encoding (states at positions 2^(i-1)): +# k=3: n=27 (9,9,9) sc=75 0.97s safe; n=31 (11,10,10) sc=96 1.32s marginal +# k=4: n=13 (4,3,3,3) sc=50 0.36s safe; n=15 (4,4,4,3) sc=70 0.94s marginal +# k=5: n=9 (2,2,2,2,1) sc=35 0.22s safe; n=10 (2,2,2,2,2) sc=51 0.49s +.MS_SC_THRESHOLD <- c(Inf, Inf, 75L, 50L, 35L) + +.MSSplitCount <- function(state_counts) { + counts <- state_counts[state_counts > 0L] + if (!length(counts)) return(0L) + n <- sum(counts) + if (n <= 2L) return(1L) + target <- n %/% 2L + poly <- 1.0 + for (ci in counts) { + new_len <- min(length(poly) + ci, target + 1L) + new_poly <- numeric(new_len) + for (j in seq_len(new_len)) { + lo <- max(1L, j - ci) + hi <- min(j, length(poly)) + if (lo <= hi) new_poly[j] <- sum(poly[lo:hi]) + } + poly <- new_poly + } + if (target + 1L <= length(poly)) poly[target + 1L] else 0.0 +} + #' Prepare data for Profile Parsimony #' -#' Calculates profiles for each character in a dataset. Will also simplify -#' characters, with a warning, where they are too complex for the present -#' implementation of profile parsimony: +#' Calculates profiles for each character in a dataset. +#' Characters with 2 informative states (i.e. states present in more than one +#' taxon) use the exact formula of Carter _et al._ (1990). +#' Characters with 3 or more informative states use the recursive algorithm of +#' Maddison & Slatkin (1991), falling back to a Monte Carlo approximation for +#' large or complex characters. +#' +#' Characters are simplified where necessary, with a warning: #' - inapplicable tokens will be replaced with the ambiguous token #' (i.e. `-` \ifelse{html}{\out{→}}{\eqn{\rightarrow}{-->}} `?`); #' - Ambiguous tokens will be treated as fully ambiguous #' (i.e. `{02}` \ifelse{html}{\out{→}}{\eqn{\rightarrow}{-->}} `?`) -#' - Where more than two states are informative (i.e. unambiguously present in -#' more than one taxon), states beyond the two most informative will be -#' ignored. -#TODO can do something more complex like first two to one TS, second two to another #' #' @param dataset dataset of class \code{phyDat} +#' @param approx Character string controlling how profile information amounts +#' are computed for multi-state characters with many tips. +#' `"auto"` (default) uses the exact Maddison & Slatkin calculation when +#' feasible, falling back to a Monte Carlo approximation for large or +#' complex characters. +#' `"mc"` always uses the Monte Carlo approximation; +#' `"exact"` always uses the exact calculation (may be very slow). +#' @param n_mc Integer; number of Monte Carlo samples for the MC +#' approximation. Default 100 000. #' #' @return An object of class `phyDat`, with additional attributes. #' `PrepareDataProfile` adds the attributes: @@ -38,10 +78,11 @@ #' @author Martin R. Smith; written with reference to #' `phangorn:::prepareDataFitch()` #' @importFrom cli cli_alert cli_alert_warning +#' @importFrom fastmatch %fin% #' @family profile parsimony functions #' @encoding UTF-8 #' @export -PrepareDataProfile <- function (dataset) { +PrepareDataProfile <- function (dataset, approx = "auto", n_mc = 100000L) { if ("info.amounts" %fin% names(attributes(dataset))) { # Already prepared return(dataset) @@ -65,7 +106,11 @@ PrepareDataProfile <- function (dataset) { ambigs <- which(contSums > 1L & contSums < ncol(cont)) inappLevel <- which(colnames(cont) == "-") if (length(inappLevel) != 0L) { - cli_alert("Inapplicable tokens treated as ambiguous for profile parsimony") + # cli_inform() routes through message(), so callers can suppress it with + # suppressMessages() and tests can capture it; cli_alert() would print + # uncatchably to stdout. + cli::cli_inform(c("!" = + "Inapplicable tokens treated as ambiguous for profile parsimony")) inappLevel <- which(apply(unname(cont), 1, identical, as.double(colnames(cont) == "-"))) dataset[] <- lapply(dataset, function (i) { @@ -75,83 +120,67 @@ PrepareDataProfile <- function (dataset) { } if (length(ambigs) != 0L) { - # Message unnecessary until multiple informative states are supported - # message("Ambiguous tokens ", paste(at[["allLevels"]][ambigs], collapse = ", "), - # " converted to "?"") dataset[] <- lapply(dataset, function (i) { i[i %fin% ambigs] <- qmLevel i }) } + # Build pattern matrix: rows = patterns (unique characters), cols = tips + nPattern <- max(index) mataset <- matrix(unlist(dataset, recursive = FALSE, use.names = FALSE), - max(index)) + nPattern) + # Transpose to: rows = tips, cols = patterns (matching .RemoveExtraTokens) + mataset <- t(mataset) - .RemoveExtraTokens <- function (char, ambiguousTokens) { - unambig <- char[!char %fin% ambiguousTokens] - if (length(unambig) == 0) { - return(matrix(nrow = length(char), ncol = 0)) - } - split <- table(unambig) - ranking <- order(order(split, decreasing = TRUE)) - ignored <- ranking > 2L - if (any(split[ignored] > 1L)) { - warningMsg <- "Can handle max. 2 informative tokens. Dropping others." - if (interactive()) { - cli_alert_warning(warningMsg) # nocov - } else { - warning(warningMsg) - } - } - if (length(ambiguousTokens) == 0) { - stop("No ambiguous token available for replacement") + # --- Strip singletons --- + maxInformative <- 0L + + for (j in seq_len(ncol(mataset))) { + col <- mataset[, j] + nonAmbig <- col[col != qmLevel[1]] + if (length(nonAmbig) == 0L) next + + tab <- table(nonAmbig) + informative <- tab > 1L + nInf <- sum(informative) + + # Convert singletons to ambiguous + singletonTokens <- as.integer(names(tab[!informative])) + if (length(singletonTokens) > 0L) { + mataset[mataset[, j] %in% singletonTokens, j] <- qmLevel[1] } - tokens <- names(split) - most <- tokens[which.min(ranking)] - vapply(setdiff(names(split)[split > 1], most), function (kept) { - simplified <- char - simplified[!simplified %fin% c(most, kept)] <- ambiguousTokens[1] - simplified - }, char) + + maxInformative <- max(maxInformative, nInf) } + - decomposed <- lapply(seq_along(mataset[, 1]), function (i) - .RemoveExtraTokens(mataset[i, ], ambiguousTokens = qmLevel)) - nChar <- vapply(decomposed, dim, c(0, 0))[2, ] - if (sum(nChar) == 0) { - cli_alert("No informative characters in `dataset`.") + if (maxInformative < 2L) { + cli::cli_inform(c("!" = "No informative characters in `dataset`.")) + # Construct empty phyDat manually (avoids [.phyDat issues with 0 columns) + dataset[] <- lapply(dataset, function(x) integer(0)) attr(dataset, "info.amounts") <- double(0) - return(dataset[0]) + attr(dataset, "weight") <- integer(0) + attr(dataset, "nr") <- 0L + attr(dataset, "index") <- integer(0) + return(dataset) } - newIndex <- seq_len(sum(nChar)) - oldIndex <- rep.int(seq_along(nChar), nChar) - index <- unlist(lapply(index, function (i) { - newIndex[oldIndex == i] - })) - - mataset <- unname(do.call(cbind, decomposed)) - - NON_AMBIG <- 1:2 - AMBIG <- max(NON_AMBIG) + 1L - .Recompress <- function (char, ambiguousTokens) { - tokens <- unique(char) - nonAmbig <- setdiff(tokens, ambiguousTokens) - stopifnot(length(nonAmbig) == 2L) - #available <- setdiff(seq_along(c(nonAmbig, ambiguousTokens)), ambiguousTokens) - - cipher <- seq_len(max(tokens)) - cipher[nonAmbig] <- NON_AMBIG # available[seq_along(nonAmbig)] - cipher[ambiguousTokens] <- AMBIG + + # --- Recompress: normalize tokens to 1..k, AMBIG --- + AMBIG_TOKEN <- maxInformative + 1L + + for (j in seq_len(ncol(mataset))) { + col <- mataset[, j] + nonAmbig <- sort(unique(col[col != qmLevel[1]])) - # Return: - cipher[char] - } - if (length(mataset) == 0) { - cli_alert("No informative characters in `dataset`.") - attr(dataset, "info.amounts") <- double(0) - return(dataset[0]) + newCol <- rep(AMBIG_TOKEN, length(col)) + for (i in seq_along(nonAmbig)) { + newCol[col == nonAmbig[i]] <- i + } + mataset[, j] <- newCol } - mataset <- apply(mataset, 2, .Recompress, qmLevel) + + # --- Deduplicate patterns --- dupCols <- duplicated(t(mataset)) kept <- which(!dupCols) copies <- lapply(kept, function (i) { @@ -169,13 +198,10 @@ PrepareDataProfile <- function (dataset) { mataset <- mataset[, !dupCols, drop = FALSE] dataset[] <- lapply(seq_len(length(dataset)), function (i) mataset[i, ]) - - #TODO when require R4.1: replace with - # info <- apply(mataset, 1, StepInformation, - # ambiguousTokens = c(qmLevel, inappLevel), - # simplify = FALSE) + # --- Compute StepInformation per unique pattern --- info <- lapply(seq_along(mataset[1, ]), function (i) - StepInformation(mataset[, i], ambiguousTokens = AMBIG)) + StepInformation(mataset[, i], ambiguousTokens = AMBIG_TOKEN, + approx = approx, n_mc = n_mc)) maxSteps <- max(vapply(info, @@ -199,12 +225,17 @@ PrepareDataProfile <- function (dataset) { attr(dataset, "nr") <- length(weight) attr(dataset, "info.amounts") <- info attr(dataset, "informative") <- colSums(info) > 0 - lvls <- c("0", "1") + + # Dynamic contrast matrix: k states + ambiguous + k <- maxInformative + lvls <- as.character(seq_len(k)) + contMatrix <- rbind(diag(k), rep(1L, k)) + dimnames(contMatrix) <- list(NULL, lvls) + attr(dataset, "levels") <- lvls attr(dataset, "allLevels") <- c(lvls, "?") - attr(dataset, "contrast") <- matrix(c(1,0,1,0,1,1), length(lvls) + 1L, length(lvls), - dimnames = list(NULL, lvls)) - attr(dataset, "nc") <- length(lvls) + attr(dataset, "contrast") <- contMatrix + attr(dataset, "nc") <- as.integer(k) if (!any(attr(dataset, "bootstrap") == "info.amounts")) { attr(dataset, "bootstrap") <- c(attr(dataset, "bootstrap"), "info.amounts") diff --git a/R/fractional-weights.R b/R/fractional-weights.R new file mode 100644 index 000000000..f4f481c05 --- /dev/null +++ b/R/fractional-weights.R @@ -0,0 +1,66 @@ +# Fractional per-character weights. +# +# TreeSearch's C++ scoring engine stores per-pattern weights as `int`. +# Without intervention, a phyDat with `attr(dat, "weight") <- c(0.5, 1.7)` +# would silently truncate to `c(0L, 1L)` at the Rcpp boundary, dropping +# 50% of the first character's contribution and 41% of the second's. +# +# `.ScaleWeight()` converts a fractional weight vector to integer with a +# documented scale factor (default 2*2*3*3*5*7 = 1260, ~0.001 precision). +# Integerweights pass through unchanged so the function is a no-op for the +# common case. +# +# The TreeLength value returned by the scoring engine is then in units of +# (steps * scale), so users comparing across runs with fractional weights +# should divide by `getOption("TreeSearch.fractional.scale", 1260L)` +# (or rely on within-run ranking, which is unaffected). + +#' @keywords internal +.ScaleWeight <- function(weight) { + if (length(weight) == 0L) { + # Return: + return(integer(0L)) + } + # Reject values that would corrupt the integer weight passed to C++: a + # negative weight reaches the scorer as a negative `int` (undefined + # behaviour), and NA/NaN/Inf otherwise surface only as an opaque + # "missing value where TRUE/FALSE needed" from the overflow guard below. + if (any(!is.finite(weight)) || any(weight < 0)) { + stop("`weight` must contain only finite, non-negative values.", + call. = FALSE) + } + if (is.integer(weight)) { + # Return: + weight + } else if (all(weight == as.integer(weight))) { + # Already-integer values stored as double: cast and return without scaling. + # Return: + as.integer(weight) + } else { + scale <- as.integer(getOption("TreeSearch.fractional.scale", 1260L)) + if (scale < 1L) scale <- 1L + scaled <- as.integer(round(weight * scale)) + # Guard against under-rounded weights becoming zero: a weight of zero + # would silently drop the character. Floor at 1 unless the user + # genuinely supplied a zero weight (preserved as 0L). + keep <- weight > 0 + scaled[keep & scaled < 1L] <- 1L + # Guard: the C++ resampling routines expand each pattern `weight[p]` + # times into a flat index vector whose length is cast to `int`. If + # sum(weights) > .Machine$integer.max the cast overflows to a negative + # value and the subsequent array access is undefined behaviour (segfault). + total <- sum(as.double(scaled)) + if (total > .Machine$integer.max) { + stop( + "Total scaled weight (", + format(round(total), big.mark = ",", scientific = FALSE), + ") exceeds .Machine$integer.max.\n", + "Reduce options(\"TreeSearch.fractional.scale\") from ", scale, + " to a smaller value, or set integer weights directly.", + call. = FALSE + ) + } + # Return: + scaled + } +} diff --git a/R/length_range.R b/R/length_range.R index 66a7e8d0e..d7a87872c 100644 --- a/R/length_range.R +++ b/R/length_range.R @@ -162,11 +162,11 @@ MinimumSteps <- function(x) { } #' @rdname MinimumLength -#' @return `MaximumLength()` returns a vector of integers specifying the +#' @return `MaximumLength()` returns a vector of integers specifying the #' maximum number of steps that each character can attain in a parsimonious #' reconstruction on a tree. Inapplicable tokens are not yet supported. #' @export -MaximumLength <- function(x, compress = TRUE) { +MaximumLength <- function(x, compress = FALSE) { UseMethod("MaximumLength") } diff --git a/R/mpl_morphy_objects.R b/R/mpl_morphy_objects.R index 66dbf1aa5..d060a1559 100644 --- a/R/mpl_morphy_objects.R +++ b/R/mpl_morphy_objects.R @@ -225,10 +225,9 @@ MorphyErrorCheck <- function(action) { #' Score a tree: [`MorphyTreeLength()`] #' #' @family Morphy API functions -#' @importFrom stringi stri_paste #' @export SingleCharMorphy <- function (char, gap = "inapp") { - char <- stri_paste(c(char, ";"), collapse = "") + char <- paste0(char, ";") entries <- gregexpr("\\{[^\\{]+\\}|\\([^\\()]+\\)|[^;]", char) nTip <- length(entries[[1]]) morphyObj <- mpl_new_Morphy() diff --git a/R/pp_info_extra_step.r b/R/pp_info_extra_step.r index 39388f3b7..3354877f3 100644 --- a/R/pp_info_extra_step.r +++ b/R/pp_info_extra_step.r @@ -8,9 +8,39 @@ #' _e_ extra steps, where _e_ ranges from its minimum possible value #' (i.e. number of different tokens minus one) to its maximum. #' +#' For characters with 2 informative tokens, uses the exact formula of +#' Carter _et al._ (1990) via [LogCarter1()]. +#' For characters with 3 or more informative tokens, uses the recursive +#' algorithm of Maddison & Slatkin (1991) via [MaddisonSlatkin()], falling +#' back to a Monte Carlo approximation for large or complex characters. +#' +#' When the Maddison & Slatkin computation would be infeasible (exponential +#' in the number of tips for a given number of tokens), behaviour depends on +#' the `approx` argument. With `"auto"` (default), the exact solver is used +#' where feasible and the Monte Carlo approximation is used otherwise. +#' With `"mc"`, the Monte Carlo approximation is always used. +#' The MC approximation computes the exact +#' minimum-steps probability analytically, uses random trees for the +#' distribution body, and bridges the gap with a log-quadratic interpolation. +#' The exact feasibility threshold depends on the partition shape +#' (balanced partitions are harder); roughly, 3-state characters +#' beyond ~27 tips, 4-state beyond ~13 tips, and 5-state beyond +#' ~9 tips trigger the approximation. +#' With `"exact"`, the full Maddison & Slatkin recursion is forced regardless +#' of cost (may be very slow for large or complex characters). +#' #' @param char Vector of tokens listing states for the character in question. #' @param ambiguousTokens Vector specifying which tokens, if any, correspond to #' the ambiguous token (`?`). +#' @param approx Character string controlling the computation method: +#' `"auto"` (default) uses exact computation when feasible, falling back to +#' Monte Carlo for large or complex characters (see Details); +#' `"mc"` always uses the Monte Carlo approximation; +#' `"exact"` forces exact computation regardless of cost (may be very slow +#' for large or complex characters). +#' @param n_mc Integer. Number of random trees used by the MC approximation. +#' Larger values improve accuracy but increase computation time. +#' Default: 100 000. #' #' @return `StepInformation()` returns a numeric vector detailing the amount #' of phylogenetic information (in bits) associated with the character when @@ -24,11 +54,12 @@ #' StepInformation(character) #' @template MRS #' @importFrom fastmatch %fin% -#' @importFrom stats setNames -#' @importFrom TreeTools Log2Unrooted +#' @importFrom stats setNames dnorm sd +#' @importFrom TreeTools Log2Unrooted LnUnrooted NUnrooted NUnrootedMult #' @family profile parsimony functions #' @export -StepInformation <- function (char, ambiguousTokens = c("-", "?")) { +StepInformation <- function (char, ambiguousTokens = c("-", "?"), + approx = "auto", n_mc = 100000L) { NIL <- c("0" = 0) char <- char[!char %fin% ambiguousTokens] if (length(char) == 0) { @@ -48,31 +79,230 @@ StepInformation <- function (char, ambiguousTokens = c("-", "?")) { return(setNames(0, minSteps)) } - if (length(split) > 2L) { - warning("Ignored least informative tokens where more than two informative ", - "tokens present.") - ranked <- order(order(split, decreasing = TRUE)) - split <- split[ranked < 3] + k <- length(split) + nTips <- sum(split) + + # Exact MaddisonSlatkin is only instantiated for k <= 5; larger k always + # uses MC (bitmask Fitch in mc_fitch_scores supports up to 32 states). + # For k <= 5, use partition-aware split_count to decide feasibility. + infeasible <- k > 5L || (k >= 3L && + .MSSplitCount(split) > .MS_SC_THRESHOLD[k]) + + if (identical(approx, "mc") || + (infeasible && !identical(approx, "exact"))) { + return(.ApproxStepInformation(split, n_mc = n_mc, + nSingletons = nSingletons)) } - logProfile <- vapply(seq_len(split[2]), LogCarter1, double(1), - split[1], split[2]) - ret <- setNames(Log2Unrooted(sum(split[1:2])) - - (.LogCumSumExp(logProfile) / log(2)), - seq_len(split[2]) + sum(singletons)) + if (k == 2L) { + # Binary: use Carter (fast, exact) + logProfile <- vapply(seq_len(split[2]), LogCarter1, double(1), + split[1], split[2]) + # Convert log-count to log-probability + logP <- logProfile - LnUnrooted(nTips) + reducedMinSteps <- 1L + } else { + # Multi-state (3-5): use MaddisonSlatkin + nStates <- 2L^k - 1L + states <- integer(nStates) + for (i in seq_along(split)) { + states[2L^(i - 1L)] <- split[i] + } + reducedMinSteps <- k - 1L + maxSteps <- nTips - 1L + logP <- tryCatch( + MaddisonSlatkin(reducedMinSteps:maxSteps, states), + error = function(e) NULL + ) + if (is.null(logP) || anyNA(logP)) { + # Exact solver hit capacity limit or timed out; fall back to MC + return(.ApproxStepInformation(split, n_mc = n_mc, + nSingletons = nSingletons)) + } + } + + # Trim trailing -Inf entries (impossible step counts) + finite_idx <- which(is.finite(logP)) + if (length(finite_idx) == 0L) { + return(setNames(0, minSteps)) + } + logP <- logP[seq_len(max(finite_idx))] + + # Cumulative information: -log2(cumsum(P)) + ret <- -.LogCumSumExp(logP) / log(2) + + # Name with total step counts (reduced steps + singleton offset) + names(ret) <- seq.int(reducedMinSteps, + reducedMinSteps + length(ret) - 1L) + nSingletons + ret[ret < sqrt(.Machine[["double.eps"]])] <- 0 # Floating point error inevitable # Return: ret } +# MC approximation with log-quadratic tail interpolation. +# Returns a named IC vector matching the format of StepInformation(). +# +# @param split Integer vector of informative token frequencies (sorted +# decreasing, singletons removed). +# @param n_mc Integer. Number of Monte Carlo trees to score. +# @param nSingletons Integer. Number of singleton tokens (for step offset). +# @return Named numeric vector of IC (bits) by step count. +# @keywords internal +.ApproxStepInformation <- function(split, n_mc = 100000L, nSingletons = 0L) { + k <- length(split) + n <- sum(split) + s_min <- k - 1L + s_max <- n - 1L + + # 1. Exact P(s_min) via product-of-double-factorials formula O(k) + log_p_min <- log(NUnrootedMult(split)) - log(NUnrooted(n)) + + # 2. MC: generate and score random trees via compiled Fitch downpass. + # No R object allocation per tree; ~0.01 ms per tree. + mc_scores <- mc_fitch_scores(split, n_mc) + + mu_hat <- mean(mc_scores) + sd_hat <- sd(mc_scores) + + # 3. Tabulate MC histogram + mc_tab <- tabulate(mc_scores - s_min + 1L, nbins = s_max - s_min + 1L) + # mc_tab[i] = count at step s_min + i - 1 + + # 4. Find the MC body edge: lowest s with >= min_count hits + min_count <- 10L + body_bins <- which(mc_tab >= min_count) + + # 5. Build log-probability vector + steps <- s_min:s_max + log_p <- rep(-Inf, length(steps)) + log_p[1L] <- log_p_min # exact P(s_min) + + if (length(body_bins) >= 2L) { + s_lo_idx <- body_bins[1L] # index into mc_tab / log_p + s_lo <- s_min + s_lo_idx - 1L + + # Fill MC body: all bins from s_lo onward + for (i in s_lo_idx:length(mc_tab)) { + if (mc_tab[i] > 0L) { + log_p[i] <- log(mc_tab[i] / n_mc) + } else { + # Right tail: normal extrapolation (negligible IC contribution) + log_p[i] <- dnorm(s_min + i - 1L, mu_hat, sd_hat, log = TRUE) + } + } + + # 6. Log-quadratic interpolation for the gap (s_min, s_lo) + if (s_lo_idx > 2L) { + # Three anchor points: exact P(s_min), plus two lowest good MC bins + s_lo2_idx <- body_bins[2L] + x1 <- s_min + x2 <- s_lo + x3 <- s_min + s_lo2_idx - 1L + y1 <- log_p_min + y2 <- log_p[s_lo_idx] + y3 <- log_p[s_lo2_idx] + + # Solve a + b*x + c*x^2 = y for three points + qfit <- .FitLogQuadratic(x1, y1, x2, y2, x3, y3) + + # Sanity: c < 0 (concave) and monotonically increasing from s_min to s_lo + if (!is.null(qfit) && qfit[3L] < 0) { + gap_s <- seq.int(s_min + 1L, s_lo - 1L) + gap_lp <- qfit[1L] + qfit[2L] * gap_s + qfit[3L] * gap_s^2 + # Check monotonicity + if (all(diff(c(log_p_min, gap_lp, log_p[s_lo_idx])) > 0)) { + for (j in seq_along(gap_s)) { + log_p[gap_s[j] - s_min + 1L] <- gap_lp[j] + } + } else { + # Fallback: log-linear interpolation between anchor and body edge + log_p <- .FillLogLinear(log_p, log_p_min, s_lo_idx) + } + } else { + log_p <- .FillLogLinear(log_p, log_p_min, s_lo_idx) + } + } + # If s_lo_idx == 2, no gap to fill (MC body starts right next to s_min) + } else { + # MC body too sparse — fall back to normal extrapolation for everything + for (i in 2L:length(steps)) { + s <- steps[i] + cnt <- mc_tab[i] + log_p[i] <- if (cnt > 0L) { + log(cnt / n_mc) + } else { + dnorm(s, mu_hat, sd_hat, log = TRUE) + } + } + } + + # 7. Trim trailing negligible entries + finite_idx <- which(is.finite(log_p) & log_p > -700) + if (length(finite_idx) == 0L) { + return(setNames(0, s_min + nSingletons)) + } + log_p <- log_p[seq_len(max(finite_idx))] + steps <- steps[seq_len(max(finite_idx))] + + # 8. Cumulative IC + ret <- -.LogCumSumExp(log_p) / log(2) + names(ret) <- steps + nSingletons + ret[ret < sqrt(.Machine[["double.eps"]])] <- 0 + + ret +} + +# Fit log P(s) = a + b*s + c*s^2 through three points. +# Returns c(a, b, c) or NULL if the system is singular. +# @keywords internal +.FitLogQuadratic <- function(x1, y1, x2, y2, x3, y3) { + # Solve the 3x3 system via elimination + # Row 2 - Row 1, Row 3 - Row 1 + dx2 <- x2 - x1 + dx3 <- x3 - x1 + dy2 <- y2 - y1 + dy3 <- y3 - y1 + sx2 <- x2^2 - x1^2 + sx3 <- x3^2 - x1^2 + + det <- dx2 * sx3 - dx3 * sx2 + if (abs(det) < 1e-12) return(NULL) + + c_coef <- (dx2 * dy3 - dx3 * dy2) / det + b_coef <- (dy2 - c_coef * sx2) / dx2 + a_coef <- y1 - b_coef * x1 - c_coef * x1^2 + + c(a_coef, b_coef, c_coef) +} + +# Log-linear interpolation: fill gap indices 2..(s_lo_idx - 1) in log_p. +# log_p[1] must already be set to log_p_min; log_p[s_lo_idx] to the body edge. +# Returns the modified log_p vector. +# @keywords internal +.FillLogLinear <- function(log_p, log_p_min, s_lo_idx) { + s_lo_lp <- log_p[s_lo_idx] + gap_len <- s_lo_idx - 1L + slope <- (s_lo_lp - log_p_min) / gap_len + for (j in 2L:(s_lo_idx - 1L)) { + log_p[j] <- log_p_min + slope * (j - 1L) + } + log_p +} + # Adapted from https://rpubs.com/FJRubio/LSE +# Guard: when both x[k] and Lk[k-1] are -Inf, the difference is NaN +# (IEEE 754: -Inf - (-Inf) = NaN), propagating silently. Keep Lk[k] = -Inf. .LogCumSumExp <- function (x) { n <- length(x) Lk <- c(x[1], double(n - 1L)) for (k in 1L + seq_len(n - 1L)) { Lk[k] <- Lk[k - 1] - Lk[k] <- max(x[k], Lk[k]) + log1p(exp(-abs(x[k] - Lk[k]))) + if (is.finite(x[k]) || is.finite(Lk[k])) { + Lk[k] <- max(x[k], Lk[k]) + log1p(exp(-abs(x[k] - Lk[k]))) + } + # else both -Inf: Lk[k] stays -Inf (log(0 + 0) = -Inf, not NaN) } # Return: @@ -81,27 +311,44 @@ StepInformation <- function (char, ambiguousTokens = c("-", "?")) { #' Number of trees with _m_ steps #' -#' Calculate the number of trees in which Fitch parsimony will reconstruct -#' _m_ steps, where _a_ leaves are labelled with one state, and _b_ leaves are -#' labelled with a second state. +#' Calculate the number of unrooted binary trees on which Fitch parsimony +#' reconstructs exactly _m_ steps for a character. #' -#' Implementation of theorem 1 from \insertCite{Carter1990;textual}{TreeTools} +#' `Carter1()` (and its logarithmic variants `Log2Carter1()`, `LogCarter1()`) +#' implement theorem 1 of \insertCite{Carter1990;textual}{TreeTools} for +#' **binary** characters, where _a_ leaves bear one state and _b_ bear the +#' other. #' -#' @param m Number of steps. +#' `MaddisonSlatkin()` generalises this result to characters with multiple +#' states using the recursive approach of +#' \insertCite{Maddison1991;textual}{TreeSearch}. +#' It returns the **log-probability** (i.e. log of the fraction of unrooted +#' binary trees) for each requested step count. The exact solver supports +#' 2--5 character tokens; for characters with more tokens, use +#' [StepInformation()] with `approx = "mc"` or `approx = "auto"` (default), +#' which falls back to a Monte Carlo approximation automatically. +#' +#' @param m,steps Number of steps. #' @param a,b Number of leaves labelled `0` and `1`. +#' @param states Integer vector giving the number of leaves bearing each +#' possible combination of states, laid out in binary fashion. +#' Entry 1 = state `1` (binary `001`), entry 2 = state `2` (binary `010`), +#' entry 3 = ambiguous state `{1,2}` (binary `011`), and so on. +#' Only observed singleton states need non-zero counts; polymorphic entries +#' are typically zero. #' -#' @references +#' @return `Carter1()` returns the number of unrooted binary trees on which a +#' binary character with `a` leaves in one state and `b` in the other can be +#' reconstructed using exactly `m` steps. +#' `Log2Carter1()` and `LogCarter1()` return that count logged to base 2 and to +#' base \eqn{e}, respectively. +#' `MaddisonSlatkin()` returns a numeric vector giving, for each requested +#' `steps` count, the natural logarithm of the fraction of unrooted binary +#' trees on which the character requires that number of steps. +#' @references \insertCite{Steel1993,Steel1995,Steel1996}{TreeSearch} #' \insertAllCited{} -#' -#' See also: -#' -#' \insertRef{Steel1993}{TreeSearch} -#' -#' \insertRef{Steel1995}{TreeSearch} -#' -#' (\insertRef{Steel1996}{TreeSearch}) #' @importFrom TreeTools LogDoubleFactorial -#' @examples +#' @examples #' # The character `0 0 0 1 1 1` #' Carter1(1, 3, 3) # Exactly one step #' Carter1(2, 3, 3) # Two steps (one extra step) @@ -237,6 +484,9 @@ LogCarter1 <- function (m, a, b) { #' Number of trees with one extra step #' @param \dots Vector or series of integers specifying the number of leaves #' bearing each distinct non-ambiguous token. +#' @return `WithOneExtraStep()` returns the number of unrooted binary trees on +#' which a character with the specified token counts can be reconstructed using +#' exactly one step more than the minimum. #' @importFrom TreeTools NRooted NUnrooted #' @examples #' WithOneExtraStep(1, 2, 3) @@ -278,7 +528,6 @@ WithOneExtraStep <- function (...) { stop("Not implemented.") # nocov start - # TODO test splits <- 2 2 4 sum(vapply(seq_along(splits), function (omit) { backboneSplits <- splits[-omit] omitted.tips <- splits[omit] @@ -292,8 +541,6 @@ WithOneExtraStep <- function (...) { backbones, attachTwoRegions, sum( - # TODO would be quicker to calculate just first half; special case: - # omitted.tips %% 2 vapply(seq_len(omitted.tips - 1), function (first.group) { # For each way of splitsting up the omitted tips, e.g. 1|16, 2|15, 3|14, etc choose(omitted.tips, first.group) * @@ -310,3 +557,13 @@ WithOneExtraStep <- function (...) { # nocov end } } + +#' Clear `MaddisonSlatkin()` cache +#' +#' Releases the internal C++ cache used by `MaddisonSlatkin()`. +#' Needed only in testing or if memory pressure is a concern. +#' +#' @name MaddisonSlatkin_clear_cache +#' @keywords internal +#' @export +NULL diff --git a/R/recode_hierarchy.R b/R/recode_hierarchy.R new file mode 100644 index 000000000..9c76db858 --- /dev/null +++ b/R/recode_hierarchy.R @@ -0,0 +1,190 @@ +#' Recode hierarchical characters as step-matrix characters +#' +#' Implements the x-transformation recoding of +#' \insertCite{Goloboff2021;textual}{TreeSearch}. +#' Each hierarchy block (one controlling primary character plus \eqn{n} +#' secondary characters) is combined into a single step-matrix character +#' with \eqn{\prod k_i + 1} states and an asymmetric cost matrix. +#' +#' @details +#' ## State encoding +#' +#' State 0 represents "primary absent". +#' States \eqn{1 \ldots \prod k_i} represent all possible combinations of +#' secondary character states (where \eqn{k_i} is the number of informative +#' states of secondary character \eqn{i}). +#' +#' ## Cost matrix +#' +#' - **Absent → present (gain):** cost = \eqn{n + 1}, where \eqn{n} is the +#' number of secondary characters. +#' - **Present → absent (loss):** cost = 1. +#' - **Present → present:** Hamming distance (number of secondaries with +#' different states). +#' +#' @param dataset A [`phyDat`][phangorn::phyDat] object. +#' @param hierarchy A [`CharacterHierarchy`] object. +#' +#' @return A list with elements: +#' \describe{ +#' \item{`sankoff_chars`}{A list of per-block lists, each containing: +#' \describe{ +#' \item{`n_states`}{Integer, number of states (absent + present combos).} +#' \item{`cost_matrix`}{Numeric matrix (\code{n_states × n_states}), +#' row-major: \code{cost_matrix[from, to]}.} +#' \item{`tip_states`}{Integer vector (length \code{n_tip}, 0-based). +#' 0 = absent, 1..n_present = present combination, +#' -1 = fully ambiguous (all states possible), +#' -2 = present but unknown combination.} +#' \item{`forced_root_state`}{Integer: -1 (unconstrained).} +#' \item{`block_chars`}{Integer vector of original character indices +#' (1-based) belonging to this block.} +#' } +#' } +#' \item{`non_hierarchy_indices`}{Integer vector of original character +#' indices (1-based) not in any hierarchy block.} +#' } +#' +#' @references +#' \insertAllCited{} +#' @family tree scoring +#' @seealso [CharacterHierarchy()], [MaximizeParsimony()] +#' @keywords internal +#' @export +RecodeHierarchy <- function(dataset, hierarchy) { + ValidateHierarchy(hierarchy, dataset) + + idx <- attr(dataset, "index") + allLevels <- attr(dataset, "allLevels") + nChar <- length(idx) + nTip <- length(dataset) + + # Original character matrix (taxon × char), as token strings + origMat <- do.call(rbind, lapply(dataset, function(x) { + allLevels[x[idx]] + })) + + .RecodeBlock <- function(node) { + ctrl <- node$controlling + deps <- node$dependents + + if (length(node$children) > 0L) { + stop("Nested hierarchies not yet supported in RecodeHierarchy(). ", + "Block controlled by character ", ctrl, " has sub-hierarchies.") + } + + # Informative levels for each secondary (exclude "-" and "?") + secLevels <- lapply(deps, function(d) { + sort(setdiff(unique(origMat[, d]), c("-", "?"))) + }) + secNStates <- vapply(secLevels, length, integer(1)) + + nPresent <- prod(secNStates) + nStates <- nPresent + 1L + nSec <- length(deps) + + if (nStates > 32L) { + warning(sprintf( + paste0("Hierarchy block controlled by character %d produces %d states ", + "(> 32). Large state spaces may be slow."), + ctrl, nStates + )) + } + + # All present-state combinations (expand.grid: first dim varies fastest) + if (nSec > 0L) { + comboGrid <- as.matrix(expand.grid( + lapply(secLevels, seq_along) + )) + } else { + # No secondaries: 2 states (absent + one present) + comboGrid <- matrix(integer(0), nrow = 1L, ncol = 0L) + } + + # --- Cost matrix --- + gainCost <- nSec + 1L + cm <- matrix(0, nStates, nStates) + for (i in seq_len(nStates)) { + for (j in seq_len(nStates)) { + if (i == j) next + if (i == 1L) { + cm[i, j] <- gainCost # absent → present + } else if (j == 1L) { + cm[i, j] <- 1 # present → absent + } else { + # Hamming distance between present combinations + cm[i, j] <- sum(comboGrid[i - 1L, ] != comboGrid[j - 1L, ]) + } + } + } + + # --- Tip states --- + tipStates <- integer(nTip) + for (t in seq_len(nTip)) { + pri <- origMat[t, ctrl] + + if (pri == "?") { + tipStates[t] <- -1L # fully ambiguous + next + } + if (pri == "0" || pri == "-") { + tipStates[t] <- 0L # absent + next + } + # Primary present: encode secondary combination + if (nSec == 0L) { + tipStates[t] <- 1L # only present state + next + } + + secVals <- origMat[t, deps] + anyUnknown <- FALSE + levelIndices <- integer(nSec) + + for (s in seq_len(nSec)) { + if (secVals[s] %in% c("-", "?")) { + anyUnknown <- TRUE + break + } + mi <- match(secVals[s], secLevels[[s]]) + if (is.na(mi)) { + anyUnknown <- TRUE + break + } + levelIndices[s] <- mi + } + + if (anyUnknown) { + tipStates[t] <- -2L # present, unknown combination + next + } + + # Mixed-radix encoding (first dim varies fastest, matching expand.grid) + rowIdx <- 1L + multiplier <- 1L + for (s in seq_len(nSec)) { + rowIdx <- rowIdx + (levelIndices[s] - 1L) * multiplier + multiplier <- multiplier * secNStates[s] + } + tipStates[t] <- rowIdx # 1-based present state = Sankoff state index + } + + list( + n_states = nStates, + cost_matrix = cm, + tip_states = tipStates, + forced_root_state = -1L, + block_chars = c(ctrl, deps) + ) + } + + blocks <- lapply(hierarchy, .RecodeBlock) + + hChars <- HierarchyChars(hierarchy) + nonH <- setdiff(seq_len(nChar), hChars) + + list( + sankoff_chars = blocks, + non_hierarchy_indices = nonH + ) +} diff --git a/R/tree_length.R b/R/tree_length.R index 122770c43..343f67b5b 100644 --- a/R/tree_length.R +++ b/R/tree_length.R @@ -1,11 +1,14 @@ #' Calculate the parsimony score of a tree given a dataset #' -#' `TreeLength()` uses the Morphy library \insertCite{Brazeau2017}{TreeSearch} -#' to calculate a parsimony score for a tree, handling inapplicable data -#' according to the algorithm of \insertCite{Brazeau2019;textual}{TreeSearch}. +#' `TreeLength()` calculates a parsimony score for a tree. #' Trees may be scored using equal weights, implied weights #' \insertCite{Goloboff1993}{TreeSearch}, or profile parsimony #' \insertCite{Faith2001}{TreeSearch}. +#' Inapplicable characters are handled using the algorithm of +#' \insertCite{Brazeau2019;textual}{TreeSearch} by default, or +#' alternatively using the hierarchical scoring of +#' \insertCite{Hopkins2021;textual}{TreeSearch} when +#' `inapplicable = "hsj"` and a [`CharacterHierarchy`] is provided. #' #' @param tree A tree of class `phylo`, a list thereof (optionally of class #' `multiPhylo`), or an integer -- in which case `tree` random trees will be @@ -20,27 +23,46 @@ #' tree <- TreeTools::BalancedTree(inapplicable.phyData[[1]]) #' TreeLength(tree, inapplicable.phyData[[1]]) #' TreeLength(tree, inapplicable.phyData[[1]], concavity = 10) +#' \donttest{ # PrepareDataProfile() and random-tree scoring are slower: #' TreeLength(tree, inapplicable.phyData[[1]], concavity = "profile") #' TreeLength(5, inapplicable.phyData[[1]]) +#' +#' # HSJ scoring with a character hierarchy +#' dataset6 <- inapplicable.phyData[["Vinther2008"]] +#' hier <- CharacterHierarchy("1" = 2:3) +#' tree6 <- TreeTools::BalancedTree(dataset6) +#' TreeLength(tree6, dataset6, hierarchy = hier, inapplicable = "hsj") +#' } #' @seealso -#' - Conduct tree search using [`MaximizeParsimony()`] (command line), -#' [`EasyTrees()`] (graphical user interface), or [`TreeSearch()`] -#' (custom optimality criteria). +#' - Conduct tree search using [`MaximizeParsimony()`] (command line) or +#' [`EasyTrees()`] (graphical user interface). #' #' - See score for each character: [`CharacterLength()`]. #' @family tree scoring #' #' @references #' \insertAllCited{} -#' @author Martin R. Smith (using Morphy C library, by Martin Brazeau) +#' @author Martin R. Smith #' @importFrom fastmatch %fin% #' @importFrom TreeTools Renumber RenumberTips TreeIsRooted #' @export -TreeLength <- function(tree, dataset, concavity = Inf) UseMethod("TreeLength") +TreeLength <- function(tree, dataset, concavity = Inf, + extended_iw = TRUE, + xpiwe_r = 0.5, + xpiwe_max_f = 5, + hierarchy = NULL, inapplicable = "bgs", + hsj_alpha = 1.0) { + UseMethod("TreeLength") +} #' @rdname TreeLength #' @export -TreeLength.phylo <- function(tree, dataset, concavity = Inf) { +TreeLength.phylo <- function(tree, dataset, concavity = Inf, + extended_iw = TRUE, + xpiwe_r = 0.5, + xpiwe_max_f = 5, + hierarchy = NULL, inapplicable = "bgs", + hsj_alpha = 1.0) { tipLabels <- tree[["tip.label"]] if (!TreeIsRooted(tree)) { @@ -58,11 +80,48 @@ TreeLength.phylo <- function(tree, dataset, concavity = Inf) { paste(setdiff(tipLabels, names(dataset)), collapse = ", ")) } + if (is.null(attr(dataset, "levels")) || ncol(attr(dataset, "contrast")) == 0L) { + return(0L) + } + if (nTip < length(dataset)) { dataset <- .Recompress(dataset[tree[["tip.label"]]]) } - + + # --- Validate inapplicable-handling parameters --- + inapplicable <- tolower(inapplicable) + if (inapplicable == "brazeau") inapplicable <- "bgs" + inapplicable <- match.arg(inapplicable, c("bgs", "hsj", "xform")) + useHSJ <- !is.null(hierarchy) && identical(inapplicable, "hsj") + if (inapplicable != "bgs") { + if (is.null(hierarchy)) { + stop("A `hierarchy` is required when inapplicable = \"", inapplicable, + "\". See ?CharacterHierarchy.") + } + if (!inherits(hierarchy, "CharacterHierarchy")) { + stop("`hierarchy` must be a CharacterHierarchy object.") + } + ValidateHierarchy(hierarchy, dataset) + if (.UseProfile(concavity)) { + stop("Profile parsimony is not currently supported with inapplicable = \"", + inapplicable, "\".") + } + if (is.finite(concavity)) { + stop("Implied weighting is not currently supported with inapplicable = \"", + inapplicable, "\".") + } + } + useXform <- !is.null(hierarchy) && identical(inapplicable, "xform") + if (!is.numeric(hsj_alpha) || length(hsj_alpha) != 1L || + hsj_alpha < 0 || hsj_alpha > 1) { + stop("`hsj_alpha` must be a single number in [0, 1].") + } + if (is.finite(concavity)) { + if (concavity <= 0) { + stop("`concavity` must be positive (or Inf for equal weights, ", + "or \"profile\" for profile parsimony).") + } if (!("min.length" %fin% names(attributes(dataset)))) { dataset <- PrepareDataIW(dataset) } @@ -82,9 +141,22 @@ TreeLength.phylo <- function(tree, dataset, concavity = Inf) { " https://github.com/ms609/TreeSearch/issues/new\n\n", " See above for full tree: ", dput(tree)) } #nocov end - fit <- homoplasies / (homoplasies + concavity) + if (isTRUE(extended_iw)) { + obsCount <- .ObsCount(dataset) + nTaxa <- length(dataset) + # Goloboff (2014) Extension 3, verified against TNT 1.6: + # f = 1 + r * missing / obs (NOT r * total / obs) + f <- pmin(pmax(1 + xpiwe_r * (nTaxa - obsCount) / obsCount, 1), + xpiwe_max_f) + eff_k <- concavity / f + phi <- (1 + eff_k) / (1 + concavity) + } else { + eff_k <- concavity + phi <- 1 + } + fit <- homoplasies / (homoplasies + eff_k) # Return: - sum(fit * weight) + sum(fit * weight * phi) } else if (.UseProfile(concavity)) { dataset <- PrepareDataProfile(dataset) @@ -94,11 +166,42 @@ TreeLength.phylo <- function(tree, dataset, concavity = Inf) { # Return: sum(vapply(which(steps > 0), function(i) info[steps[i], i], double(1)) * attr(dataset, "weight")[steps > 0]) + } else if (useHSJ) { + tree <- RenumberTips(Renumber(tree), names(dataset)) + at <- attributes(dataset) + contrast <- at$contrast + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) + adj_weight <- .NonHierarchyWeights(dataset, hierarchy) + ts_hsj_score(tree[["edge"]], contrast, tip_data, + as.integer(adj_weight), at$levels, + .HierarchyToBlocks(hierarchy), + as.double(hsj_alpha), + .BuildTipLabels(dataset), + .HSJAbsentState(dataset)) + } else if (useXform) { + tree <- RenumberTips(Renumber(tree), names(dataset)) + at <- attributes(dataset) + contrast <- at$contrast + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) + adj_weight <- as.integer(.NonHierarchyWeights(dataset, hierarchy)) + recoded <- RecodeHierarchy(dataset, hierarchy) + xform <- .PrepareXformArgs(recoded, length(dataset)) + fitch_part <- ts_fitch_score(tree[["edge"]], contrast, tip_data, + adj_weight, at$levels) + res <- ts_sankoff_test(tree[["edge"]], xform$n_states, + xform$cost_matrices, xform$tip_states, + xform$forced_root) + fitch_part + res$score } else { tree <- RenumberTips(Renumber(tree), names(dataset)) - morphyObj <- PhyDat2Morphy(dataset) - on.exit(morphyObj <- UnloadMorphy(morphyObj)) - MorphyTreeLength(tree, morphyObj) + at <- attributes(dataset) + contrast <- at$contrast + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) + ts_fitch_score(tree[["edge"]], contrast, tip_data, + .ScaleWeight(at$weight), at$levels) } } @@ -106,19 +209,64 @@ TreeLength.phylo <- function(tree, dataset, concavity = Inf) { #' @rdname TreeLength #' @importFrom TreeTools RandomTree #' @export -#TODO could be cleverer still and allow TreeLength.edge -TreeLength.numeric <- function(tree, dataset, concavity = Inf) { +TreeLength.numeric <- function(tree, dataset, concavity = Inf, + extended_iw = TRUE, + xpiwe_r = 0.5, + xpiwe_max_f = 5, + hierarchy = NULL, inapplicable = "bgs", + hsj_alpha = 1.0) { TreeLength(lapply(!logical(tree), RandomTree, tips = dataset), - dataset = dataset, concavity = concavity) + dataset = dataset, concavity = concavity, + extended_iw = extended_iw, + xpiwe_r = xpiwe_r, xpiwe_max_f = xpiwe_max_f, + hierarchy = hierarchy, inapplicable = inapplicable, + hsj_alpha = hsj_alpha) } #' @rdname TreeLength #' @export -TreeLength.list <- function(tree, dataset, concavity = Inf) { - # Define constants +TreeLength.list <- function(tree, dataset, concavity = Inf, + extended_iw = TRUE, + xpiwe_r = 0.5, + xpiwe_max_f = 5, + hierarchy = NULL, inapplicable = "bgs", + hsj_alpha = 1.0) { iw <- is.finite(concavity) - profile <- .UseProfile(concavity) - + useProfile <- .UseProfile(concavity) + + # --- Validate inapplicable-handling parameters --- + inapplicable <- tolower(inapplicable) + if (inapplicable == "brazeau") inapplicable <- "bgs" + inapplicable <- match.arg(inapplicable, c("bgs", "hsj", "xform")) + useHSJ <- !is.null(hierarchy) && identical(inapplicable, "hsj") + if (inapplicable != "bgs") { + if (is.null(hierarchy)) { + stop("A `hierarchy` is required when inapplicable = \"", inapplicable, + "\". See ?CharacterHierarchy.") + } + if (!inherits(hierarchy, "CharacterHierarchy")) { + stop("`hierarchy` must be a CharacterHierarchy object.") + } + ValidateHierarchy(hierarchy, dataset) + if (useProfile) { + stop("Profile parsimony is not currently supported with inapplicable = \"", + inapplicable, "\".") + } + if (iw) { + stop("Implied weighting is not currently supported with inapplicable = \"", + inapplicable, "\".") + } + } + useXform <- !is.null(hierarchy) && identical(inapplicable, "xform") + if (!is.numeric(hsj_alpha) || length(hsj_alpha) != 1L || + hsj_alpha < 0 || hsj_alpha > 1) { + stop("`hsj_alpha` must be a single number in [0, 1].") + } + if (iw && concavity <= 0) { + stop("`concavity` must be positive (or Inf for equal weights, ", + "or \"profile\" for profile parsimony).") + } + nTip <- NTip(tree) if (length(unique(nTip)) > 1L) { stop("All trees must bear the same leaves.") @@ -127,66 +275,79 @@ TreeLength.list <- function(tree, dataset, concavity = Inf) { if (nTip < length(dataset)) { dataset <- .Recompress(dataset[TipLabels(tree[[1]])]) } - + tree[] <- RenumberTips(tree, dataset) - tree <- Preorder(tree) - tree[] <- lapply(tree, function(tr) { - if (TreeIsRooted(tr)) { - tr - } else { - warning("Unrooted tree rooted on tip 1.") - RootTree(tr, 1) - } - }) - + needRoot <- !vapply(tree, TreeIsRooted, logical(1L)) + if (any(needRoot)) warning("Unrooted tree rooted on tip 1.") + tree[] <- lapply(tree, function(tr) if (TreeIsRooted(tr)) tr else RootTree(tr, 1)) + nEdge <- unique(vapply(tree, function(tr) dim(tr[["edge"]])[1], integer(1))) if (length(nEdge) > 1L) { stop("Trees have different numbers of edges (", - paste0(nEdge, collapse = ", "), + paste0(nEdge, collapse = ", "), "); try collapsing polytomies?)") } - - edges <- vapply(tree, `[[`, tree[[1]][["edge"]], "edge") - - # Initialize data - if (profile) { - dataset <- PrepareDataProfile(dataset) - profiles <- attr(dataset, "info.amounts") + + if (is.null(attr(dataset, "levels")) || ncol(attr(dataset, "contrast")) == 0L) { + return(rep(0L, length(tree))) } - if (iw || profile) { - at <- attributes(dataset) - characters <- PhyToString(dataset, ps = "", useIndex = FALSE, - byTaxon = FALSE, concatenate = FALSE) - weight <- at[["weight"]] - informative <- at[["informative"]] - charSeq <- seq_along(characters) - 1L - - # Save time by dropping uninformative characters - if (!is.null(informative)) { - charSeq <- charSeq[informative] + + # Prepare dataset for C++ engine + if (useProfile) { + dataset <- PrepareDataProfile(dataset) + } else if (iw) { + if (!("min.length" %fin% names(attributes(dataset)))) { + dataset <- PrepareDataIW(dataset) } - morphyObjects <- lapply(characters, SingleCharMorphy) - on.exit(morphyObjects <- vapply(morphyObjects, UnloadMorphy, integer(1)), - add = TRUE) - } else { - morphyObj <- PhyDat2Morphy(dataset) - on.exit(morphyObj <- UnloadMorphy(morphyObj), add = TRUE) - weight <- unlist(MorphyWeights(morphyObj)[1, ]) # exact == approx } - - # Return: - if (iw) { - minLength <- at[["min.length"]] - if (is.null(minLength)) { - minLength <- attr(PrepareDataIW(dataset), "min.length") - } - apply(edges, 3, morphy_iw, morphyObjects, weight, minLength, charSeq, - concavity, Inf) - } else if (profile) { - apply(edges, 3, morphy_profile, morphyObjects, weight, charSeq, profiles, - Inf) + + at <- attributes(dataset) + contrast <- at$contrast + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) + weight <- .ScaleWeight(at$weight) + levels <- at$levels + + min_steps <- if (iw) as.integer(at[["min.length"]]) else integer(0) + concavity_val <- if (iw) concavity else Inf + infoAmounts <- if (useProfile) at$info.amounts else NULL + + # XPIWE: per-pattern observed-taxa counts + useXpiwe <- isTRUE(extended_iw) && iw && !useProfile + obsCount <- if (useXpiwe) .ObsCount(dataset) else integer(0) + + if (useHSJ) { + adj_weight <- as.integer(.NonHierarchyWeights(dataset, hierarchy)) + blocks <- .HierarchyToBlocks(hierarchy) + alpha <- as.double(hsj_alpha) + tip_labels <- .BuildTipLabels(dataset) + absent_state <- .HSJAbsentState(dataset) + vapply(tree, function(tr) { + ts_hsj_score(tr[["edge"]], contrast, tip_data, adj_weight, levels, + blocks, alpha, tip_labels, absent_state) + }, double(1)) + } else if (useXform) { + adj_weight <- as.integer(.NonHierarchyWeights(dataset, hierarchy)) + recoded <- RecodeHierarchy(dataset, hierarchy) + xform <- .PrepareXformArgs(recoded, length(dataset)) + vapply(tree, function(tr) { + fitch_part <- ts_fitch_score(tr[["edge"]], contrast, tip_data, + adj_weight, levels) + res <- ts_sankoff_test(tr[["edge"]], xform$n_states, + xform$cost_matrices, xform$tip_states, + xform$forced_root) + fitch_part + res$score + }, double(1)) } else { - apply(edges, 3, preorder_morphy, morphyObj) + vapply(tree, function(tr) { + ts_fitch_score(tr[["edge"]], contrast, tip_data, weight, levels, + min_steps = min_steps, concavity = concavity_val, + infoAmounts = infoAmounts, + xpiwe = useXpiwe, + xpiwe_r = as.double(xpiwe_r), + xpiwe_max_f = as.double(xpiwe_max_f), + obs_count = obsCount) + }, double(1)) } } @@ -196,7 +357,27 @@ TreeLength.list <- function(tree, dataset, concavity = Inf) { TreeLength.multiPhylo <- TreeLength.list #' @export -TreeLength.NULL <- function(tree, dataset, concavity = Inf) NULL +TreeLength.NULL <- function(tree, dataset, concavity = Inf, + extended_iw = TRUE, + xpiwe_r = 0.5, + xpiwe_max_f = 5, + hierarchy = NULL, inapplicable = "bgs", + hsj_alpha = 1.0) NULL + +# Pack RecodeHierarchy() output into the format ts_sankoff_test() expects. +.PrepareXformArgs <- function(recoded, n_tip) { + chars <- recoded$sankoff_chars + n_chars <- length(chars) + n_states <- as.integer(vapply(chars, function(ch) ch$n_states, numeric(1))) + forced_root <- as.integer(vapply(chars, function(ch) ch$forced_root_state, numeric(1))) + cost_matrices <- lapply(chars, function(ch) ch$cost_matrix) + tip_states <- matrix(0L, nrow = n_tip, ncol = n_chars) + for (i in seq_len(n_chars)) { + tip_states[, i] <- chars[[i]]$tip_states + } + list(n_states = n_states, cost_matrices = cost_matrices, + tip_states = tip_states, forced_root = forced_root) +} #' @rdname TreeLength #' @export @@ -308,43 +489,15 @@ FitchSteps <- function(tree, dataset) { #' @describeIn CharacterLength Do not perform checks. Use with care: may cause #' erroneous results or software crash if variables are in the incorrect format. -#' @importFrom fastmatch fmatch -#' @importFrom TreeTools Postorder FastCharacterLength <- function(tree, dataset) { - nTip <- NTip(tree) - levels <- attr(dataset, "levels") - morphyObj <- PhyDat2Morphy(dataset, weight = 0) - on.exit(morphyObj <- UnloadMorphy(morphyObj)) - - maxNode <- nTip + mpl_get_num_internal_nodes(morphyObj) - rootNode <- nTip + 1L - allNodes <- rootNode:maxNode - - edge <- Postorder(tree)[["edge"]] - parent <- edge[, 1] - child <- edge[, 2] - - parentOf <- parent[fmatch(seq_len(maxNode), child)] - parentOf[rootNode] <- rootNode # Root node's parent is a dummy node - leftChild <- child[length(parent) + 1L - fmatch(allNodes, rev(parent))] - rightChild <- child[fmatch(allNodes, parent)] - - if (nTip < 1L) { - # Run this test after we're sure that morphyObj is a morphyPtr, or lazy - # evaluation of nTaxa will cause a crash. - stop("Error: ", mpl_translate_error(nTip)) + at <- attributes(dataset) + if (is.null(at$levels) || ncol(at$contrast) == 0L) { + return(rep(0L, at$nr)) } - - vapply(seq_len(attr(dataset, "nr")), function(i) { - MorphyErrorCheck(mpl_set_charac_weight(i, 1, morphyObj)) - on.exit(MorphyErrorCheck(mpl_set_charac_weight(i, 0, morphyObj))) - MorphyErrorCheck(mpl_apply_tipdata(morphyObj)) - - # Return: - .Call(`MORPHYLENGTH`, as.integer(parentOf - 1L), - as.integer(leftChild - 1L), as.integer(rightChild - 1L), - morphyObj) - }, integer(1)) + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) + ts_char_steps(tree[["edge"]], at$contrast, tip_data, + .ScaleWeight(at$weight), at$levels) } #' Calculate parsimony score from Morphy object diff --git a/R/ts-driven-compat.R b/R/ts-driven-compat.R new file mode 100644 index 000000000..fea8e6127 --- /dev/null +++ b/R/ts-driven-compat.R @@ -0,0 +1,212 @@ +# Backward-compatible wrapper for ts_driven_search. +# +# Accepts the old flat-argument calling convention used by tests and +# packs them into the grouped lists expected by ts_driven_search(). +# Production code (MaximizeParsimony, .ResampleHierarchy) calls +# ts_driven_search() directly with pre-built grouped lists. +ts_driven_search <- function( + contrast, + tip_data, + weight, + levels, + # --- New grouped-list interface (used when calling with grouped args) --- + searchControl = NULL, + runtimeConfig = NULL, + scoringConfig = NULL, + constraintConfig = NULL, + hsjConfig = NULL, + xformConfig = NULL, + # --- Old flat-argument interface (used by tests) --- + maxReplicates = 100L, + targetHits = 10L, + tbrMaxHits = 1L, + ratchetCycles = 10L, + ratchetPerturbProb = 0.04, + ratchetPerturbMode = 0L, + ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = FALSE, + ratchetTaper = FALSE, + stallEscalateFactor = 1.0, + driftCycles = 6L, + driftAfdLimit = 3L, + driftRfdLimit = 0.1, + xssRounds = 3L, + xssPartitions = 4L, + rssRounds = 1L, + cssRounds = 1L, + cssPartitions = 4L, + sectorMinSize = 6L, + sectorMaxSize = 50L, + postRatchetSectorial = FALSE, + fuseInterval = 3L, + fuseAcceptEqual = FALSE, + poolMaxSize = 100L, + poolSuboptimal = 0.0, + maxSeconds = 0.0, + verbosity = 0L, + min_steps = integer(0), + concavity = -1.0, + consSplitMatrix = NULL, + consContrast = NULL, + consTipData = NULL, + consWeight = NULL, + consLevels = NULL, + consExpectedScore = 0L, + infoAmounts = NULL, + tabuSize = 100L, + wagnerStarts = 1L, + progressCallback = NULL, + nThreads = 1L, + startEdge = NULL, + sprFirst = FALSE, + nniFirst = TRUE, + hierarchyBlocks = NULL, + hsjTipLabels = NULL, + hsjAlpha = 1.0, + hsjAbsentState = 0L, + xformChars = NULL, + xpiwe = FALSE, + xpiwe_r = 0.5, + xpiwe_max_f = 5.0, + obs_count = integer(0), + consensusStableReps = 0L, + perturbStopFactor = 2L, + adaptiveLevel = FALSE, + consensusConstrain = FALSE, + nniPerturbCycles = 0L, + nniPerturbFraction = 0.5, + wagnerBias = 0L, + wagnerBiasTemp = 0.3, + outerCycles = 1L, + maxOuterResets = 0L, + adaptiveStart = FALSE, + enumTimeFraction = 0.1, + pruneReinsertCycles = 0L, + pruneReinsertDrop = 0.10, + pruneReinsertSelection = 0L, + annealConfig = NULL) +{ + # New-style call: grouped lists already provided + if (!is.null(searchControl)) { + return(.Call(`_TreeSearch_ts_driven_search`, + contrast, tip_data, weight, levels, + searchControl, runtimeConfig, scoringConfig, + constraintConfig, hsjConfig, xformConfig + )) + } + + # Old-style call: pack flat args into grouped lists + sc <- SearchControl( + tbrMaxHits = as.integer(tbrMaxHits), + nniFirst = as.logical(nniFirst), + sprFirst = as.logical(sprFirst), + tabuSize = as.integer(tabuSize), + wagnerStarts = as.integer(wagnerStarts), + wagnerBias = as.integer(wagnerBias), + wagnerBiasTemp = as.double(wagnerBiasTemp), + outerCycles = as.integer(outerCycles), + maxOuterResets = as.integer(maxOuterResets), + ratchetCycles = as.integer(ratchetCycles), + ratchetPerturbProb = as.double(ratchetPerturbProb), + ratchetPerturbMode = as.integer(ratchetPerturbMode), + ratchetPerturbMaxMoves = as.integer(ratchetPerturbMaxMoves), + ratchetAdaptive = as.logical(ratchetAdaptive), + ratchetTaper = as.logical(ratchetTaper), + stallEscalateFactor = as.double(stallEscalateFactor), + nniPerturbCycles = as.integer(nniPerturbCycles), + nniPerturbFraction = as.double(nniPerturbFraction), + driftCycles = as.integer(driftCycles), + driftAfdLimit = as.integer(driftAfdLimit), + driftRfdLimit = as.double(driftRfdLimit), + xssRounds = as.integer(xssRounds), + xssPartitions = as.integer(xssPartitions), + rssRounds = as.integer(rssRounds), + cssRounds = as.integer(cssRounds), + cssPartitions = as.integer(cssPartitions), + sectorMinSize = as.integer(sectorMinSize), + sectorMaxSize = as.integer(sectorMaxSize), + postRatchetSectorial = as.logical(postRatchetSectorial), + fuseInterval = as.integer(fuseInterval), + fuseAcceptEqual = as.logical(fuseAcceptEqual), + poolMaxSize = as.integer(poolMaxSize), + poolSuboptimal = as.double(poolSuboptimal), + consensusStableReps = as.integer(consensusStableReps), + perturbStopFactor = as.integer(perturbStopFactor), + adaptiveLevel = as.logical(adaptiveLevel), + consensusConstrain = as.logical(consensusConstrain), + pruneReinsertCycles = as.integer(pruneReinsertCycles), + pruneReinsertDrop = as.double(pruneReinsertDrop), + pruneReinsertSelection = as.integer(pruneReinsertSelection), + adaptiveStart = as.logical(adaptiveStart), + enumTimeFraction = as.double(enumTimeFraction) + ) + + # Anneal config: fold into SearchControl if provided + # Use if/is.null instead of %||% for R < 4.4 compatibility + .or <- function(x, default) if (is.null(x)) default else x + if (!is.null(annealConfig)) { + phases <- as.integer(.or(annealConfig$phases, 5L)) + # Backward compat: if phases > 0 but cycles not specified, default to 1 + sc$annealCycles <- as.integer(.or(annealConfig$cycles, + if (phases > 0L) 1L else 0L)) + sc$annealPhases <- phases + sc$annealTStart <- as.double(.or(annealConfig$tStart, 20)) + sc$annealTEnd <- as.double(.or(annealConfig$tEnd, 0)) + sc$annealMovesPerPhase <- as.integer(.or(annealConfig$movesPerPhase, 0L)) + } + + rt <- list( + maxReplicates = as.integer(maxReplicates), + targetHits = as.integer(targetHits), + maxSeconds = as.double(maxSeconds), + verbosity = as.integer(verbosity), + nThreads = as.integer(nThreads), + startEdge = startEdge, + progressCallback = progressCallback + ) + + scoring <- list( + min_steps = min_steps, + concavity = as.double(concavity), + xpiwe = as.logical(xpiwe), + xpiwe_r = as.double(xpiwe_r), + xpiwe_max_f = as.double(xpiwe_max_f), + obs_count = obs_count, + infoAmounts = infoAmounts + ) + + # Constraint config + cc <- NULL + if (!is.null(consSplitMatrix)) { + cc <- list( + consSplitMatrix = consSplitMatrix, + consContrast = consContrast, + consTipData = consTipData, + consWeight = consWeight, + consLevels = consLevels, + consExpectedScore = as.integer(consExpectedScore) + ) + } + + # HSJ config + hc <- NULL + if (!is.null(hierarchyBlocks)) { + hc <- list( + hierarchyBlocks = hierarchyBlocks, + hsjTipLabels = hsjTipLabels, + hsjAlpha = as.double(hsjAlpha), + hsjAbsentState = as.integer(hsjAbsentState) + ) + } + + # Xform config + xc <- NULL + if (!is.null(xformChars)) { + xc <- list(xformChars = xformChars) + } + + .Call(`_TreeSearch_ts_driven_search`, + contrast, tip_data, weight, levels, + sc, rt, scoring, cc, hc, xc + ) +} diff --git a/README.md b/README.md index 99de30875..0782dd51f 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,16 @@ visualization, (Smith 2022b), and cluster consensus trees. -Inapplicable character states are handled using the algorithm of Brazeau, -Guillerme and Smith (2019) using the "Morphy" C library (Brazeau _et al_. 2017). +Tree search uses a compiled C++ engine combining TBR rearrangement, the +parsimony ratchet, tree drifting, sectorial search, and tree fusing. +Inapplicable character states are handled using the algorithm of +Brazeau, Guillerme and Smith (2019). Implied weighting (Goloboff, 1993), -Profile Parsimony (Faith and Trueman, 2001) -and Successive Approximations (Farris, 1969) -are implemented; +Profile Parsimony (Faith and Trueman, 2001), +Successive Approximations (Farris, 1969), +and topological constraints are supported natively; [custom optimality criteria](https://ms609.github.io/TreeSearch/articles/custom.html) -and search approaches can also be defined. +can also be defined. # Installing in R @@ -72,6 +74,8 @@ type `choco install ffmpeg`; then restart your computer. Launch a graphical user interface by typing `TreeSearch::EasyTrees()` in the R console. For more control over search settings, see [`?MaximizeParsimony()`](https://ms609.github.io/TreeSearch/reference/MaximizeParsimony.html). +`MaximizeParsimony()` supports equal weights, implied weights, profile parsimony, and topological constraints natively in C++. +For fine-grained control over the R-level search loop, see [`?Morphy()`](https://ms609.github.io/TreeSearch/reference/Morphy.html). ![Flow charts listing common actions facilitated by TreeSearch](man/figures/Flow.svg) diff --git a/agent-e.md.tmp b/agent-e.md.tmp new file mode 100644 index 000000000..6efd9df0f --- /dev/null +++ b/agent-e.md.tmp @@ -0,0 +1,101 @@ +# Agent E — Progress Log + +## Current Task +- **Status:** PARKED — T-289 Stage 4 queued as SLURM 16621426 on Hamilton (~5h) + +### T-289 Stage 4 — multi-dataset PR validation — DISPATCHED (2026-03-28) + +Stage 3 confirmed: MISSING criterion (sel=2), c=5, d=5% gives mean −14.7 steps +vs baseline at 180t/60s (10 seeds). Applied to large preset. Stage 4 now tests +generalisation across 5 matrices (131–206t) at 60s and 120s. + +**Changes committed (b8b9f831):** +- `R/MaximizeParsimony.R`: large preset now includes + `pruneReinsertCycles=5, pruneReinsertDrop=0.05, pruneReinsertSelection=2` +- `AGENTS.md`: large preset table updated +- `dev/benchmarks/bench_pr_stage4_validation.R`: Stage 4 script (200 runs) +- `dev/benchmarks/t289e_stage4_hamilton.sh`: SLURM script + +**Stage 4 design:** +- 5 datasets: mbank_X30754 (180t), project4133 (131t), project3701 (146t), + project804 (173t), syab07205 (206t) +- 2 configs: baseline (no PR), pr_large (c=5, d=5%, MISSING) +- 2 budgets: 60s, 120s; 10 seeds; 200 total runs +- SLURM 16621426, ~5h wall time + +**Resume:** poll results when job completes, analyse per-dataset and +per-budget PR benefit. If consistent improvement, T-289 is done. +If any dataset regresses, investigate. + +### S-RED Area 4 — Parallelism & RNG — DONE (2026-03-27) + +Reviewed ts_rng.h/.cpp (110 lines) and ts_parallel.h/.cpp (732 lines). +ts_driven.cpp covered in E-003 (see below). + +**No bugs found.** Thread safety correct throughout. + +Observations (non-bugs): +- fuse_round holds pool mutex across entire tree_fuse() call (O(n) TBR + exchanges). Workers block for full fuse duration. Performance only. +- Multiple workers may trigger fuse_round at the same `replicates_done` + checkpoint due to relaxed read races. Redundant fuse (harmless). +- Lines 323-325 in main polling loop: empty if-block, dead code. +- Verbosity Rprintf acquires pool mutex via status(). If fuse_round holds + the lock, interrupt/timeout polling is delayed by fuse duration. +- ts_rng.h serial/parallel dispatch verified correct in all paths. + +### S-RED Focus 4 — ts_driven.cpp review — DONE (2026-03-27) + +Reviewed ts_driven.cpp (1054 lines) and ts_driven.h (322 lines) in full. +Focus areas per AGENTS.md: cross-replicate constraint tightening, outer +cycle loop, and features added since T-189. + +**Bugs fixed (committed to cpp-search):** + +1. **`unsuccessful_reps` not reset on fuse improvement** (`ts_driven.cpp` + line ~923). When inter-replicate fusing found a better score, the + perturb-stop counter was not cleared. Meanwhile `last_improved_rep` + *was* updated by fuse. This inconsistency could cause `perturb_stop` + to fire prematurely when fusing is still productive. Low severity + (factor defaults 0; limit = n_tips × factor is high when enabled), + but logically wrong. Fixed by adding `unsuccessful_reps = 0;` in the + fuse-improvement branch. + +2. **`DrivenResult::perturb_stop` flag missing** (`ts_driven.h`). + T-276 ("print convergence summary") explicitly lists perturb_stop as + a convergence indicator. Added the field and set it at the stopping + site in `driven_search()`. + +3. **Stale NNI-perturb comment** (step 4b, `ts_driven.cpp`). Opening + sentence said "Skip when constraints are active" but the code passes + `cd` through `nni_perturb_search()` and has been safe under constraints + for several tasks. Replaced with accurate one-liner. + +**Other observations (no fix needed):** + +- `consensus_constrain = true` with 0 unanimous splits calls + `extract_consensus_splits()` every replicate (performance, not + correctness). consensus_constrain defaults to false; low priority. +- `timed_out = true` is set for both timeout and user interrupt — no + distinction in DrivenResult. Acceptable for now; T-276 can note this + in summary text ("search interrupted/timed out"). +- `score_tree()` called at top of each outer cycle for improvement + comparison — minor overhead, by design. +- MPT enumeration uses user constraint `cd` only (not auto_cd) — by + design: enumeration should be unconstrained. +- Outer cycle reset logic correct; `score_before_cycle` / `score_after_cycle` + correctly bound the improvement check. +- Adaptive level, ratchet taper, consensus constraint tightening, and + adaptive start bandit logic all look correct. + +### Previous work +### ASAN vector OOB fix — DONE (2026-03-26) +- Root cause: total_words == 0 when all characters are parsimony-uninformative +- Fix: early returns in TBR, SPR, NNI, drift, ratchet, collapsed-flags +- Commit: 6505803f on cpp-search + +### S-COORD Round 27 — DONE +- Fixed R 4.1 `%||%` compat bug in `test-ts-anneal.R` (58fc2552) + +### T-265 — RESOLVED (scoring method confound) +### Previous: S-RED Focus 8, T-261+T-262, T-255, T-260 diff --git a/check_init.R b/check_init.R new file mode 100644 index 000000000..e3535ef72 --- /dev/null +++ b/check_init.R @@ -0,0 +1,59 @@ +# Compare arg counts between TreeSearch-init.c and RcppExports.cpp + +# Parse TreeSearch-init.c +init_lines <- readLines("src/TreeSearch-init.c") +init_pattern <- '[{]"(_TreeSearch_\\w+)".*,\\s*(\\d+)[}]' +init_matches <- regmatches(init_lines, regexec(init_pattern, init_lines)) +init_matches <- init_matches[lengths(init_matches) > 0] +init_df <- data.frame( + name = vapply(init_matches, `[`, "", 2), + init_args = as.integer(vapply(init_matches, `[`, "", 3)), + stringsAsFactors = FALSE +) + +# Parse RcppExports.cpp +export_lines <- readLines("src/RcppExports.cpp") +export_pattern <- "RcppExport SEXP (_TreeSearch_\\w+)[(]([^)]*)[)]" +export_matches <- regmatches(export_lines, regexec(export_pattern, export_lines)) +export_matches <- export_matches[lengths(export_matches) > 0] +export_df <- data.frame( + name = vapply(export_matches, `[`, "", 2), + export_args = vapply(export_matches, function(m) { + params <- trimws(m[3]) + if (nchar(params) == 0) return(0L) + length(strsplit(params, ",")[[1]]) + }, integer(1)), + stringsAsFactors = FALSE +) + +cat("init.c entries:", nrow(init_df), "\n") +cat("RcppExports.cpp entries:", nrow(export_df), "\n\n") + +# Merge and compare +merged <- merge(init_df, export_df, by = "name", all = TRUE) + +# Mismatches in shared entries +mis <- merged[!is.na(merged$init_args) & !is.na(merged$export_args) & + merged$init_args != merged$export_args, ] +if (nrow(mis) > 0) { + cat("ARG COUNT MISMATCHES:\n") + print(mis, row.names = FALSE) +} else { + cat("All shared entries: arg counts match.\n") +} +cat("\n") + +# In init.c but not RcppExports.cpp +manual <- merged[is.na(merged$export_args), ] +if (nrow(manual) > 0) { + cat("Manual entries (init.c only, not in RcppExports.cpp):", nrow(manual), "\n") + print(manual[, c("name", "init_args")], row.names = FALSE) +} +cat("\n") + +# In RcppExports.cpp but missing from init.c +missing_reg <- merged[is.na(merged$init_args), ] +if (nrow(missing_reg) > 0) { + cat("MISSING from init.c (in RcppExports.cpp but not registered):\n") + print(missing_reg[, c("name", "export_args")], row.names = FALSE) +} diff --git a/completed-tasks.md b/completed-tasks.md new file mode 100644 index 000000000..9a895bfe6 --- /dev/null +++ b/completed-tasks.md @@ -0,0 +1,68 @@ +# Closed Tasks — Decisions Worth Not Re-Litigating + +This is **not** a full archive of every completed task. Routine fixes live in +git history and merged PRs; do not duplicate them here. This file keeps only +the closures whose *reasoning* is not recoverable from a commit: **not-a-bug +determinations, superseded/ruled-out designs, and negative experimental +results** — the things an agent would otherwise waste budget re-investigating. + +**How to consult:** before investigating a recurring symptom or reopening a +closed `T-nnn`/`-nnn`, **`grep` this file** (by ID or keyword) for a +prior closure. Do **not** `Read` it whole. Old task IDs referenced here remain +valid (see AGENTS.md) and need not be renamed. + +**How to add a row:** only when a task closes *without a routine fix* — a +negative result, a "not a bug", or a superseded decision. One row, terminal +decision + a pointer to the write-up. Routine fixes get a one-line row in +`to-do.md`'s removal commit, not an entry here. + +--- + +## Not a bug / scoring-method confounds + +| ID | Topic | Decision | +|----|-------|----------| +| T-242 | Agnarsson2004 IW "~2% hit rate" | **Display bug only.** `ThreadSafePool::extract_into()` reset `hits_to_best` to distinct-topology count, not replicate hits. Search algorithm unaffected; real hit rate ~60–67%. Fixed `bc19667f2`; regression test in `test-ts-parallel.R`. | +| T-247 | XPIWE Vinther2008 score ≠ TNT | **Not a bug.** Discrepancy is entirely Brazeau three-pass vs standard-Fitch inapplicable handling. TreeSearch's tree (EW=79) is genuinely better under three-pass scoring. XPIWE uses `eff_k` in all paths — verified correct. | +| T-265 / T-249 / T-264 | Per-replicate "regression" vs TNT | **Scoring-method confound, not a regression.** T-249/T-264 compared Brazeau-scored TreeSearch to EW-scored TNT (apparent gap +17.8 steps; real EW-vs-EW gap +2.2, 5/11 datasets at 0). **Future TNT comparisons MUST use `fitch_mode()` for apples-to-apples.** | +| T-211 | Stale `final_` in temper candidate scoring | **Not worth fixing.** Conservative-only: stale `final_` biases Boltzmann screening but `temper_full_rescore` gates every accepted move. Fix cost (per-candidate rescore or full save/restore) exceeds negligible SA benefit. | + +## Superseded / ruled-out designs + +| ID | Topic | Decision | +|----|-------|----------| +| T-183 | Pool-seeded Wagner / consensus backbone | **Superseded** by `consensusConstrain` (ts_driven.cpp), which constrains the whole replicate pipeline, not just Wagner. Marginal starting-tree value given the NNI→TBR pipeline. | +| T-198–201 | Boltzmann parallel tempering | **Ruled out** by T-199: 0% cold↔warm swap acceptance across all datasets. PCSA component salvaged as T-207/PR #227. See pt-evaluation expertise note. | +| T-185 | IQ-TREE acceleration ideas | Stochastic NNI-perturbation worth trying (→T-186, implemented). **Batch NNI not worthwhile** — see batch-nni expertise note. | + +## Search-tuning experiments — settled, don't re-run + +Each row records a benchmark whose conclusion fixed a default or killed an idea. +Detailed data is in the named `dev/benchmarks/` write-up; re-running wastes +Hamilton/GHA budget unless the underlying kernel has changed. + +| ID | Experiment | Conclusion | +|----|-----------|------------| +| T-254 | Drift cycles (0 vs 2) | Drift gives **zero** score/MPT/diversity benefit, costs 10–22% of reps → `driftCycles=0` in default+thorough (T-255). `drift_mpt_analysis.md`. | +| T-256 | Sectorial intensity | Doubling/tripling xss+rss rounds → no score gain. Current `xss=3, rss=1` sufficient. | +| T-259 | Ratchet cycle count | Reducing 12→8/6/4 is mixed-to-worse; default **12 justified** (3-seed, directional). | +| T-274 | NNI-perturb cycles (thorough) | 59–69% overhead, ≤0.1-step benefit → `nniPerturbCycles=0` in thorough. `bench_t274_nni_perturb.R`. | +| T-248 | SA phase tuning (large) | `annealCycles=3` no significant gain over AC=1 (p>0.5) → **AC=1** in large preset (~6% faster). | +| F-029 (T-269) | Outer-cycle count | Higher `outerCycles` cuts replicate throughput with no score gain → **`outerCycles=2` optimal**. | +| PA-002 | XSS↔TBR cycling | Benefit scales with **tree size, not scoring mode**. ≤88t: pure overhead. 180t: −6.8 to −9.8 EW steps. No IW-specific treatment needed. `expt_tbr_xss_v2_results.rds`. | +| PA-003 | Targeted post-clip sector search | **NET HARMFUL** — local sector refinement after each TBR move steers into worse basins (+17 to +34 steps at 180t). Confirms XSS-as-a-separate-phase-after-TBR is correct. **Do not implement.** | +| PA-001 → F-030 | TBR clip ordering | PA-001's "tips-first falsified" was an **artifact** (clip_order reached only ~10% of TBR calls). F-030 with full propagation: `TIPS_FIRST` gives +8–13% throughput on 75–88t thorough preset. Default unchanged (`clipOrder=0`); PR #239. | +| T-289 / T-289f | Prune-reinsert polish (large) | TBR polish **catastrophic** at ≥206t (0 reps); NNI polish helps 131–180t → `pruneReinsertCycles=5, pruneReinsertNni=TRUE` in large preset, PR disabled elsewhere. `t289f_pr_nni_polish.csv`. | +| G-001 (T-290) | Brazeau-track phase profiling | Wagner is 3.6–5.2× costlier under Brazeau than Fitch; Fitch-tuned presets remain appropriate. `wagnerStarts=3` justified (better starting topology dominates when TBR convergence > budget). | +| F-006 (T-253) | Gap characterization | **ntax is the dominant predictor** of TNT gap (ρ≈0.63); nchar matters only >2000. `t253_gap_characterization.md`. | +| F-004 (T-252) | MorphoBank baseline | ≤35t converge at 30s; 66–135t still improving at 120s; project4284 (4062t) can't finish 1 replicate. CSVs in `dev/benchmarks/`. | +| T-251 | TNT trajectory analysis | Drift 30–170× less efficient than the next-worst phase; TNT spends ~67% in sectorial search vs TS's single pass. `tnt_trajectory_analysis.md`. | +| T-250 | TNT Fitch kernel disassembly | TNT is 32-bit scalar, no SIMD; TreeSearch has ~4× kernel throughput. TNT's convergence edge is **strategic, not implementation**. `tnt_disassembly_analysis.md`. | +| T-260 | VTune TBR overhead | Non-scoring overhead = 37.8% of TBR (StateSnapshot 14.6%, reset_states 9.1%) → T-261/262/263 (done). `vtune_tbr_analysis.md`. | + + diff --git a/coordination.md b/coordination.md new file mode 100644 index 000000000..4f73e3b2d --- /dev/null +++ b/coordination.md @@ -0,0 +1,751 @@ +# TreeSearch — Strategic Coordination + +## S-COORD Round 46 Summary (2026-03-29 07:40 BST, Agent E) + +**T-289f complete — Stage 5 NNI polish benchmark + large preset update:** +SLURM 16622483 completed (7h12m, EPYC 7702). 300 runs: 5 datasets (131–206t), +configs baseline/pr_nni/pr_tbr, 60s+120s, 10 seeds. +- pr_tbr (TBR polish): confirmed Stage 4 failure — syab07205 (206t) still 0 reps at 60s. +- pr_nni (NNI polish): fixes 0-rep failure; improves 131–180t: project3701 (146t) −178 + steps at 60s / −128 at 120s; project804 (173t) −9/−2; mbank_X30754 (180t) −4/−7. + syab07205 (206t) +17.5 at 60s, neutral at 120s — acceptable. +- Decision: **pruneReinsertCycles=5, pruneReinsertNni=TRUE enabled in large preset**. + commit 4a549eb4. Results: dev/benchmarks/t289f_pr_nni_polish.csv. AGENTS.md updated. + +**G-006 fixed — NNI constraint guard in prune_reinsert_search():** +One-line guard `if (params.nni_full && (!cd || !cd->active))` in ts_prune_reinsert.cpp. +When constraints active, falls through to TBR (which enforces them). Mirrors the +`nni_wagner` guard in ts_driven.cpp. Task deleted from to-do.md. + +**GHA 23703257153 in progress** on cpp-search (covers 4a549eb4 + G-006 fix). + +**PR status:** +- #213 (T-150, CID consensus): GHA PASS, awaiting human merge. +- #216 (T-204, native search): GHA PASS, awaiting human merge. +- #210 (cpp-search→main, DRAFT): Re-run 23702009435 in progress; previous failure was + Windows covr only (transient/infra — tests passed FAIL 0/PASS 11021). + +**Task queue:** Extremely sparse. Only standing tasks + T-280–288 (all WORKTREE/AltHom). +Standing tasks at **P1** (<3 open specific tasks). + +**Next:** S-RED (review alt-homology modules when T-280 merges, or review ts_search.cpp +and ts_nni_perturb.cpp which haven't been reviewed). S-PR to check PR status. + +## S-COORD Round 42 Summary (2026-03-28 16:10 GMT, Agent F) + +**T-269 complete — Fine-grained sectorial interleaving (30s, 4 datasets, outer_cycles 1/2/4/10/20):** +Higher outer_cycles uniformly reduces replicate throughput with no score benefit. +At outer_cycles=20: Dikow2009 gets 9 reps vs 54 at baseline; Zhu2013 gets 16 vs 88. +Scores are flat or marginally worse at high outer_cycles. The current outerCycles=2 in the +thorough preset is optimal; no preset change needed. + +**T-289 complete (E) — Stage 4 confirms disable-PR decision:** +5 datasets 131–206t, 10 seeds, 60s/120s. Key: syab07205 (206t) gets 0 PR reps at 60s +(per-rep cost ≈ 60s, budget exceeded). project3701 (146t) regresses 12 steps mean at 60s. +commit 746985243 disables pruneReinsertCycles in large preset. Available via SearchControl(). + +**F-027 WORDLIST fix — PASSED (GHA 23656560997).** Both 'config' and 'warmup' restored. + +**PR #210 (cpp-search→main):** codoc fix fdf25673 in place; R-CMD-check run 23688837232 +in progress. Previous pre-existing failures (Windows covr, R-devel rlang, ASAN TBB ODR) +are infra issues, not package check failures. + +**Open PRs:** #213 (T-150, GHA PASS), #216 (T-204, GHA PASS), #237 (T-279, GHA PASS). +All three await human merge. + +**Task queue:** T-245 (P3, TBR batching) is only open specific task. Standing tasks P1. +S-RED next: ts_mc_fitch.cpp, ts_tabu.h, ts_prune_reinsert.h (222 lines, unreviewed). + +## S-COORD Round 41 Summary (2026-03-28 14:35 GMT, Agent E) + +**Codoc fix — SearchControl.Rd (E-003):** +All R-CMD-check platforms failing on PR #210 since 2026-03-28 06:25 with +"Codoc mismatches from SearchControl.Rd". Root cause: commit 22f929cf +(`pruneReinsertTbrMoves` param, T-289) added the parameter to the function +and roxygen `@param` but the Rd file was not regenerated. Fix: manually added +`pruneReinsertTbrMoves = 5L` to `\usage` and its `\item` to `\arguments` in +`man/SearchControl.Rd`. Commit fdf25673. PR #210 CI re-triggered (run +23687279706, pending). Agent-check GHA 23687210711 also dispatched. + +**T-289 Stage 4 — Hamilton SLURM 16621426:** +Stage 3 confirmed MISSING criterion (sel=2, c=5, d=5%) gives −14.7 steps at +180t/60s. Large preset updated. Stage 4 validating across 5 matrices +(131–206t) at 60s/120s, 10 seeds, 200 runs. Submitted 2026-03-28 ~08:00 GMT, +~5h wall time. SSH unavailable — poll later. + +**F-027 WORDLIST fix (GHA 23656560997) — PASSED.** Resolved. + +**PR status:** +- #210 (cpp-search→main): CI re-running with codoc fix; was failing since 06:25. +- #213 (T-150, CID consensus): GHA 23650002703 PASS, awaiting merge. +- #216 (T-204, native search): GHA 23649607006 PASS, awaiting merge. +- #237 (T-279, drift constraint fix): GHA 23650290962 PASS, awaiting merge. + +**Task queue:** T-289 PARKED (Hamilton), T-269 PARKED (Hamilton), T-245/T-290/T-291 +OPEN. 3 open specific tasks → standing tasks P2 effective. + +**GHA 23687804562 results (PR #210, post-codoc-fix):** All 5 release platforms +PASS. Remaining failures are pre-existing infra issues: Windows covr path, R-devel +rlang DLL, ASAN RcppParallel TBB ODR. All in dep-install or coverage steps, not +"Check package". PR #210 ready for human review. + +**T-291 complete (E-004):** bench_framework.R benchmark_run() updated to new +ts_driven_search structured-list interface. commit f1ed5dfc. + +**Next:** Poll Hamilton for T-289 Stage 4 results when SSH is available. + +## S-COORD Round 39 Summary (2026-03-27 16:05 GMT) + +**GHA results confirmed (all PASS):** +- GHA 23653228247 (F-015: ratchet constraint staleness) — **PASSED** +- GHA 23653513217 (F-016: NNI-perturb constraint staleness) — **PASSED** +- GHA 23653782359 (F-018: prune-reinsert constraint staleness) — **PASSED** + +All constraint-staleness fixes now validated on both platforms. The full sweep +(TBR T-278, drift T-279, sector E-003, ratchet F-015, NNI-perturb F-016, +prune-reinsert F-018) is complete. All 6 constrained search modules now +consistently call `update_constraint(tree, *cd)` after any topology revert. + +**Hamilton SSH unavailable** — can't poll T-289 (SLURM 16607721) or T-269 +(SLURM 16607719/16607720). Jobs were submitted ~1.5h ago; T-289 ETA ~2.7h +from submission, so likely still running. Results will be in `t289_results/` +and `t269_results/` when complete. + +**PR status:** #213 (T-150), #216 (T-204), #237 (T-279) still awaiting human +merge. No new PRs needed (F-015/016/018 were direct cpp-search commits). + +**Task queue:** T-289 PARKED, T-269 PARKED, T-245 OPEN (only specific open +task). Standing tasks now **P1** (<3 open specific tasks). + +**Agent F next:** S-RED focus 23 (ts_fitch.cpp, 844+288 lines — core Fitch +scoring engine). + +## S-COORD Round 37 Summary (2026-03-27 15:15 GMT) + +**T-289 dispatched (F):** Prune-reinsert benchmark Stage 1 submitted to Hamilton +SLURM 16606222. 13 configs × 4-5 datasets × 5 seeds × 30s ≈ 325 runs, ETA ~2.7h. +Fixed Rscript invocation bug in t289_hamilton.sh: `Rscript -e "expr" file.R` does +NOT source file.R. Use `export R_LIBS_USER; Rscript file.R` (T-252 pattern). +Also committed bench_prune_reinsert.R which was untracked. commits 5b0c0ad5 + 03e981f8. + +Note: t265_hamilton.sh has the same Rscript bug but T-265 is complete. + +**F-015 / S-RED focus 16 — ts_ratchet.cpp (259+61 lines):** +Bug found and fixed directly to cpp-search (same pattern as E-003). +**Constraint staleness after best_tree revert:** in ratchet_search() non-escape +path, `update_constraint(tree, *cd)` was missing after copy_topology(best_tree) + +build_postorder + reset_states. Next cycle's perturbed TBR used stale DFS timestamps. +Same class as T-278/T-279/E-003. commit ae6a3528. GHA 23653228247 running. +All other invariants correct: save/restore state, FlatBlock sync (only active_mask +needed — FlatBlock has no upweight_mask field), perturb modes, adaptive tuning. + +**PR status:** #213 (T-150), #216 (T-204), #237 (T-279) all GHA-passed, awaiting +human merge. No change since round 36. + +**Task queue:** T-289 PARKED, T-245 OPEN (P3), T-269 OPEN (P3). Standing tasks P2 +(effective 3 open tasks counting T-289 parked). + +**Agent F next:** Park T-289 GHA (23653228247). Take T-269 (fine-grained sectorial +interleaving benchmark) — this can run locally on the Hamilton session. + + +Last updated: 2026-03-27 14:55 GMT (S-COORD round 35 by F) + +## S-COORD Round 35 Summary (2026-03-27 14:55 GMT) + +**T-253 complete (F):** Gap characterization by dataset features done. +ntax is dominant predictor of search difficulty (ρ≈0.63 in both T-265 fitch-mode gaps +and T-252 mbank convergence gaps). nchar matters only at extremes (>2000). pct_missing/ +pct_inapp weakly correlated but likely confounded with ntax. T-245 (TBR batching) +confirmed as highest-priority next step for ≥75-taxon regime. Results in +`dev/benchmarks/t253_gap_characterization.md`. commit d05638e5. + +**T-150 WORDLIST fix (F):** "Splitwise" was missing from inst/WORDLIST — the spell-check +test failure root cause. Added and re-dispatched as GHA 23648875258. Previous GHA +23648267378 failed on this (and only this) issue. + +**3 GHAs running:** +- 23648875258 (T-150, feature/cid-consensus, PR #213) +- 23648401936 (T-204, feature/native-search, PR #216) +- 23648703841 (S-RED fix, cpp-search: perturb_stop in parallel path) + +**Task queue:** 2 unblocked OPEN specific tasks (T-245, T-269) + E-002 (soft-blocked +on T-150/T-204 merge) + E-001 (ASSIGNED E). Standing tasks at **P2** (3–5 open). +Next priority: S-RED focus 6 (ts_tbr.cpp review) while GHAs run. + +**Agent F next:** S-RED focus 6. + +## S-COORD Round 34 Summary (2026-03-27 13:45 GMT) + +**T-277 (ScoreSpectrum, B):** Merged via PR #236 to cpp-search. Removed from to-do.md; added to completed-tasks.md. + +**T-276 (convergence summary, F):** DONE. GHA 23647640670 PASS. Removed from to-do.md. + +**S-RED focus 5 (ts_parallel.cpp, F):** Bug fixed — `result.perturb_stop` not initialized (UB) and not set in parallel path. commit 1a640b73. GHA 23648703841 running. + +**ASan.yml fix (E):** `pak::pak("r-lib/rlang")` approach broken — GitHub dev rlang 1.1.7.9000 also embeds `PREXPR` in `src/rlang/rlang-types.h`. New approach: patch CRAN source tarball with `#ifndef PREXPR / #define PREXPR(x) R_PromiseExpr(x) / #endif` shim before `R CMD INSTALL`. commit 05261c34. GHA 23648993981 dispatched to verify. + +**Agent C file stale:** agent-c.md still shows T-214 as PARKED, but T-214 was completed (GHA 23542642164 PASS, per completed-tasks.md). C should update agent-c.md on next assignment. + +**NEWS.md gap (E):** NEWS.md was last updated 2026-03-18. Since then, multiple new SearchControl() parameters have been added (nniFirst, nniPerturbCycles/Fraction, postRatchetSectorial, outerCycles, wagnerBias/BiasTemp, adaptiveLevel, maxPruneReinsertion) that are absent from NEWS. Verbosity convergence summary (T-276) also missing. Filed E-001 (P2). + +**Agent status:** +- A: IDLE. Can take T-245/T-269/E-002 or S-RED focus 6. +- B: IDLE (T-277 merged — B may not know yet). Can take T-245/T-269/E-002. +- C: IDLE (T-214 was done — file stale). Can take T-245/T-269/E-002. +- D: IDLE. Can take T-245/T-269/E-002. +- E: ASSIGNED E-001 (NEWS.md update). T-150/T-204 PRs parked waiting GHA (F). +- F: Parked on T-150 (GHA 23648875258) and T-204 (GHA 23648401936). ASSIGNED T-253. + +**Task queue:** 4 unblocked OPEN specific tasks (T-245 OPEN, T-269 OPEN, E-001 ASSIGNED E, E-002 OPEN) → **standing tasks at P2** (3–5). + +**Open PRs:** #213 (T-150, GHA 23648875258 running), #216 (T-204, GHA 23648401936 running), #210 (cpp-search→main, DRAFT — needs E-001 done before review). + + + +## S-COORD Round 32 Summary (2026-03-27 10:40 GMT) + +**T-268 (branch housekeeping, F):** Done. Pruned 11 stale local branches, updated AGENTS.md worktree table, triaged u.005 (interleaved sectorial rationale → T-269 notes). commit 838b14c1. + +**T-252 (Hamilton benchmarking, F):** Previous job 16598843 failed (httpuv/shiny not building in fresh lib). New `t252_v2.sh` uses `ts-bench/lib-baseline` for all deps. Job 16599543 submitted and running. + +**S-RED focus 2 (F):** T-263 snapshot hoisting VERIFIED CORRECT. T-235 SPR fix VERIFIED CORRECT. LATENT: `flat_blocks.active_mask` not synced by ratchet perturbation (zero call sites — safe now). T-273 filed as P3 preventive fix. + +**T-273 (NEW):** Fix `flat_blocks.active_mask` staleness during ratchet (P3). `FlatBlock` is populated at `build_dataset()` only; ratchet modifies `blocks[b].active_mask` but not `flat_blocks[b].active_mask`. Must be fixed before flat indirect functions are wired into the dispatch path. + +**Agent status:** +- A: IDLE (completed T-270, T-272, S-RED focus 1 today). Can take T-245/T-273/S-PROF. +- B: IDLE but T-204 PR #216 needs GHA fix (add roxygen2 docs for CleanNativeData/NativeBootstrap/NativeLength/PrepareNativeData; regenerate Rds). Should resume T-204. +- C: IDLE (T-214 done). Can take T-245/T-273/T-269. +- D: IDLE. Can take T-245/T-273/T-269. +- E: T-150 PARKED (InfoConsensus.Rd codoc fix needed in TS-CID-cons). Should resume T-150. +- F: T-252 PARKED (Hamilton 16599543). Available for more standing tasks. + +**Task queue:** 3 unblocked OPEN specific tasks (T-245, T-269, T-273) → **standing tasks at P2**. + +**Open PRs:** #213 (T-150, GHA failing — codoc fix), #216 (T-204, GHA failing — missing docs), #235 (T-266, PASSED, awaiting human merge), #210 (cpp-search→main). All others closed. + +## S-COORD Round 31 Summary (2026-03-27 09:20 GMT) + +**T-266 (prune-reinsert, A):** GHA 23636145497 PASSED. PR #235 opened to cpp-search. + +**T-150 (CID consensus, E):** GHA 23636944848 **FAILED** — codoc mismatch in `InfoConsensus.Rd`. Fix: `roxygen2::roxygenise(load_code=roxygen2::load_installed)` in TS-CID-cons worktree, commit, re-dispatch. + +**New tasks:** +- T-270 (P2): Algorithm vignette + AGENTS.md update for T-257 post-ratchet sectorial. Check if PR #234 already included it. +- T-272 (P3): Close stale PR #178 (concordance, Aug 2025, CONFLICTING DRAFT). + +**T-126 (Shiny hierarchy UI, D):** Referenced in AGENTS.md as "ASSIGNED D" but absent from to-do.md and completed-tasks.md. Likely deferred post-release. No action taken — flagged here for human awareness. + +**Task queue:** 3 unblocked OPEN specific tasks (T-245 P3, T-269 P3, T-270 P2). T-253 blocked by T-252. **Standing tasks at P2** (3–5 unblocked OPEN). + +**Open PRs (3 to cpp-search + 1 base PR):** #213 (T-150, GHA failing — codoc fix needed), #216 (T-204), #235 (T-266, GHA passed). #210 (cpp-search→main). #178 stale DRAFT — T-272 filed to close. + +## S-COORD Round 30 Summary (2026-03-27 08:50 GMT) + +**Three PRs merged to cpp-search overnight (2026-03-27):** +- PR #231 (T-263): StateSnapshot save hoisted to once per TBR pass (~14.6% TBR overhead eliminated) +- PR #233 (T-246): AVX2 runtime dispatch for Fitch SIMD (5–10% on multi-block datasets; SSE2 fallback) +- PR #234 (T-257): Post-ratchet sectorial search pass (`postRatchetSectorial` in SearchControl()) + +**T-267 (MaddisonSlatkin 5-state) FIXED by A.** Test now skips on budget timeout instead of failing with NA. + +**T-266 (taxon pruning-reinsertion, A):** GHA 23636145497 PASSED. PR #235 now open. + +**T-150 (CID consensus, E):** SPIC scoring added (commit 6636924c). GHA 23636944848 in progress. + +**Open PRs (3 to cpp-search + 1 base PR):** #213 (T-150, CID+SPIC, GHA pending), #216 (T-204, native-search), #235 (T-266, prune-reinsert, GHA passed). #210 (cpp-search→main) still open. #178 stale — recommend close. + +**Task queue:** 2 unblocked OPEN specific tasks (T-245 P3, T-269 P3) → **standing tasks at P1**. T-253 blocked by T-252 (F, in progress). T-268 (housekeeping) ASSIGNED F. + + +## S-COORD Round 29 Summary (2026-03-26 18:10 GMT) + +**T-242 (P1): CLOSED — not a bug.** The "2% hit rate" on Agnarsson2004 IW +was a parallel pool reporting bug: `ThreadSafePool::extract_into()` reset +`hits_to_best` to distinct topology count instead of actual replicate hits. +Fix already committed (`bc19667f2`, 92 commits ago). Score 50.1872 (XPIWE +k=10^0.75) is correct; actual hit rate ~60–67%. No P1 bugs remain. + +**T-257 GHA 23607823258: FAILED — doc mismatch only.** All 10934 tests pass +on both platforms. Windows failure is `SearchControl.Rd` codoc mismatch — +new `postRatchetSectorial` parameter needs roxygen regeneration. Agent F +should fix and re-dispatch. + +**Task queue:** 0 P1, 2 P2 (T-150 worktree, T-204 PR), 4 P3 (T-245 OPEN, +T-252 OPEN, T-253 blocked, T-257 PARKED). T-263 and T-246 on PRs. +2 unblocked OPEN specific tasks → **standing tasks at P1**. + +**PRs:** 5 open to cpp-search (all MERGEABLE: #213, #216, #231, #233 + #210 +cpp-search→main). #178 stale and CONFLICTING (Aug 2025 — recommend close). + +## S-COORD Round 27 Summary (2026-03-26 16:30 GMT) + +**CI fix pushed:** `%||%` operator in `test-ts-anneal.R` broke R 4.1 CI +(operator introduced in R 4.4). Replaced with `if/is.null` (58fc2552). +This was the root cause of ubuntu-24.04 (R 4.1) failures on runs +23601960123, 23601354741, and all queued runs. Windows R CMD check passes; +Windows covr failure is MaddisonSlatkin floating-point under instrumentation +(not actionable — main check clean). + +**GHA queue:** 9 queued + 4 in_progress runs on cpp-search (PR #210 +triggers). The fix commit will trigger a fresh set. Earlier queued runs +will still fail on R 4.1 but will be superseded. + +**Hamilton job 16597206** (T-265 EW-mode confirmation) — status unknown +(SSH unreachable from this session). Results expected in `t265_results/`. + +**Task queue:** 1 P1 (T-242, parked C — may be scoring confound like T-265), +1 P2 (T-263 PR #231), 3 P3 OPEN (T-245, T-252, T-257). Standing at P2 +(3 unblocked open). T-253 blocked by T-249+T-252 and needs rethinking +(gaps were mostly scoring confound artifacts). + +## S-COORD Round 26 Summary (2026-03-26 late afternoon) + +**T-265 (P1): RESOLVED — scoring method confound, not engine regression.** +The apparent +17.8-step gap between TreeSearch and TNT was almost entirely +due to comparing Brazeau-scored TreeSearch output to EW-scored TNT output. +TreeSearch uses Brazeau et al. (2019) inapplicable algorithm by default; +TNT treats `-` as `?`. When scoring is equalized (both EW), the actual +gap is only +2.2 steps (5/11 datasets at 0 gap, largest residual +7 at +15s budget). R2-equiv / R2-modern / auto-preset all find identical Brazeau +scores on Wilson2003 — no preset or engine regression. AGENTS.md updated +with mandatory `fitch_mode()` warning for future TNT comparisons. + +**T-264 (P0): Fully verified.** GHA passed both platforms. Scoring confound +resolved. Fix is correct. + +**T-249: Validated and closed.** Hamilton results confirmed; gaps were +scoring confound. Future comparisons must convert `-` → `?`. + +**Hamilton job 16597206** running: T-265 EW-mode benchmark (3 configs × +9 datasets × 5 seeds × 120s) for fuller confirmation. Results expected +in ~4-5 hours. + +**Task queue:** 1 P1 (T-242, parked C), 1 P2 (T-263 PR #231), +4 P3 OPEN (T-245, T-252, T-253, T-257). Standing at P2 (3-5 open). +T-253 needs rethinking given the scoring confound — the "gaps" it was +going to characterize are mostly artifacts. + +## S-COORD Round 28 Summary (2026-03-26 late afternoon, by F) + +**T-265 (scoring confound): CLOSED.** The apparent 5–54 step quality +regression vs TNT was a benchmarking methodology error: Brazeau +inapplicable scores were compared against TNT Fitch scores. Correct EW +(Fitch-mode) gaps are **0–7 steps** (mean 2.2) across 11 hard datasets at +120s, with 5 datasets optimal. T-265 moved to completed-tasks. T-264 and +T-249 also archived. Hamilton Phase 2a job (16597240) cancelled (low +cluster capacity + results would be uninformative given the confound). + +**Lesson:** Always compare like-for-like scoring. Brazeau three-pass +scoring produces inherently higher step counts than Fitch — this is by +design (it penalizes inapplicable placements), not a search failure. +`clean_inapplicable()` or `fitch_mode()` must be applied before comparing +against TNT. Added to Architecture Decisions. + +**R-4.1 compat fix:** `%||%` operator (R ≥ 4.4 only) replaced with local +`.or()` helper in `ts-driven-compat.R`. Committed to cpp-search (ad1dbde9). + +**AVX2 ASAN issue (PR #233):** `std::vector::operator[]` OOB assertion in +`ts-collapsed` tests under gcc ASAN. Agent E investigating. + +**Task queue:** 4 OPEN specific tasks (T-245, T-252, T-253, T-257). T-253 +unblocked from T-249 (complete); only blocked on T-252 now. Standing tasks +at P2. 6 open PRs (#233, #231, #216, #213, #210, #178). PR #178 remains +stale/CONFLICTING (Aug 2025) — recommend close. + +## S-COORD Round 25 Summary (2026-03-26 afternoon) + +**T-264 (P0): `consensusStableReps` catastrophic early termination FIXED.** +Root cause: presets set `consensusStableReps = 3`, stopping search after 3 +unchanged-consensus replicates. Most datasets used 7–20% of time budget. +Fix committed (23e9f57b) by F, removes from all presets (falls back to 0). +GHA 23600674681: ARM64 passed, Windows in progress. Hamilton verification +(8 worst datasets, 120s, 3 seeds) dispatched as job 16597096. + +**T-261+T-262 (eliminate-fill): MERGED** as PR #232. 8.6% TBR speedup. +S-RED focus 8 verified no scoring regressions (subtree_actives non-NA +positions safe: init to 0, never written, all reads NA-guarded). + +**T-255 (drift removal): COMPLETE.** GHA 23598220226 passed both platforms. + +**T-246 (AVX2): PR #233 opened** by F. MERGEABLE, CI in progress. + +**Task queue health:** 1 P0 (T-264, fix committed, GHA+Hamilton validating), +1 P1 (T-242, parked C), 1 P2 (T-263 PR #231), 3 P3 OPEN (T-245, T-252, T-257), +T-253 blocked by T-249+T-252. Standing tasks at P2 (3 open unblocked). +6 open PRs: #233, #231, #216, #213, #210, #178 (stale). + +**AGENTS.md updated** for T-264 (consensusStableReps disabled in presets). + +## S-COORD Round 22 Summary (2026-03-26 morning) + +**Drift elimination (T-254/T-255):** Drift search eliminated from default +and thorough presets. T-254 experiment (3 datasets × 3 seeds × 2 budgets) +confirmed zero benefit on score, MPT count, or topological diversity, with +10–22% replicate cost. `SearchControl()` default and all presets now +`driftCycles=0`. GHA 23590522833 in progress. + +**GHA fixes committed to cpp-search:** +- Spelling wordlist: added LCM, TREE's, speedup; removed 28 stale entries +- PrepareDataProfile/StepInformation codoc: `n_mc` 5000→100000 (stale Rd + from devtools::check_man() loading old installed version) +- test-ts-parallel.R:85 flaky timeout: Vinther→Agnarsson (fast ARM64 + completed 23-tip replicates before 1s timeout) + +**T-243 (hot-loop-opt):** GHA 23582386358 failed on the same parallel +timeout flake (ARM64). Fix is on cpp-search (371270b3); needs merge into +feature/hot-loop-opt via TS-HotLoop worktree. + +**Agent assignments:** E: T-255 parked (GHA), F: T-249/T-256/T-258/T-259. + +## S-COORD Round 20 Summary (2026-03-25 afternoon) + +**Major changes since round 15 (~9h ago):** +- 8 PRs merged to cpp-search (#211, #212, #214, #217, #218, #220, #221, #223, #225) +- T-214 (P1 constraint bug) fixed by C — was blocking multiple GHA runs +- PRs #215 (parallel-temper) and #222 (pt-eval) CLOSED without merge +- T-207/T-210 cherry-picked into new PR #227 (`feature/pcsa-phase`) +- ~40 Shiny bug fixes and features landed (T-219–T-243) +- S-RED focus 2–4 completed (found T-235 SPR stale state, T-243 consensus stability) + +**Stale entries cleaned from to-do.md:** +- T-214 (completed), T-212 (validated by T-214 fix), T-179 (completed), T-182 (PR merged) +- T-198–201 and T-196 marked STALE (PR #215 closed) +- T-207/T-210 updated to PR #227 + +**GHA status:** 4 parked Shiny tasks (T-232/T-240/T-239/T-241) had GHA failures from +pre-T-214 state. Run 23547582438 (current HEAD) queued; will validate all. T-242 +(IW regression, P1) GHA 23545987517 also queued. + +**Stale worktrees** (for human cleanup): +- TS-AdaptRatch (feature/adaptive-ratchet — PR #221 merged) +- TS-NNIcons (feature/nni-constraint-guard — PR #220 merged) +- TS-OuterCap (feature/outer-cap-t206 — PR #218 merged) +- TS-PT (feature/parallel-temper — PR #215 closed) +- TS-T211 (feature/stale-final-uppass — T-211 closed) +- TS-FixRandCons (feature/fix-random-tree-constraint — check if still needed) + +**Open PRs requiring review:** #216 (native-search), #213 (CID consensus). #226 (perturb-stop) and #227 (PCSA) merged. + +**Task queue health:** 1 OPEN specific task (T-183), 6 PR-pending, 4 Shiny PARKED +awaiting re-validation, 2 STALE (need decision). Standing tasks at P1. + +## Project State + +The C++ phylogenetic search engine is **v2.0.0** with a new +`MaximizeParsimony()` API, driven C++ search, and fully modularized Shiny app. + +**All planned development objectives are complete.** Two new feature tracks +(inapplicable-handling algorithms, Shiny UX) were added and are substantially +complete; only integration/polish tasks remain. + +Test suite health (full NOT_CRAN run, 2026-03-19 ~17:05): +- R-level: **~9835 pass, 0 fail** (1 stochastic ParsSim failure observed once, transient), 12 warn, 5 skip +- ts-* (C++ engine): 1676 pass, 0 fail (T-144 fix also resolved 3 ts-profile failures from human commit 5235d6e1) +- ParsSim: 128 pass +- MaddisonSlatkin: 37 pass (was 26 fail per E's S-RED round 6; fixed by T-144) +- Recode-hierarchy: 53 pass +- HSJ: 37 pass +- Sankoff: 24 pass +- Xform integration: 80 pass +- Shiny module tests: 88+ pass +- init.c: 45 entries (43 Rcpp + 2 manual), all arg counts match + +**CRAN REGRESSION T-144: FIXED** (Agent A, 2026-03-19). Added missing +binary-reduction warning to `PrepareDataProfile()`, fixed `dataset[0]` crash +in new TreeTools, updated test expectations. CRAN submission no longer blocked +on test failures. + +## Project State Update (2026-03-23) + +### Search optimization phase (2026-03-22–23) + +Systematic profiling of the driven search pipeline across all 14 benchmark +datasets (20–88 tips) led to committed improvements: + +1. **Ratchet perturbation tuning** (`f1ae7edb`): 4% → 25%, moves 20 → 5, + cycles 5 → 10. 9/14 datasets improved. +2. **Drift → ratchet reallocation** (`7ae01181`): driftCycles 4 → 2, + ratchetCycles 10 → 12. +3. **NNI warmup** (T-178): Always-on NNI before TBR. Each Wagner start + NNI-optimized. SPR auto-skipped when NNI active. +4. **NNI-perturbation** (T-186): New escape mechanism between ratchet and + drift. Random NNI swaps + TBR re-optimization. +5. **Biased Wagner** (T-188): Softmax-sampled taxon addition order. +6. **Outer cycle loop** (T-189): Interleave XSS/ratchet/drift. + +### XPIWE feature (2026-03-23) + +All 7 tasks (T-156–T-162) completed on feature/xpiwe branch by Agent G. +Extended implied weighting corrects for missing-entries bias in IW scoring. +Now the default in Shiny. Ready for merge. + +### Benchmark expansion (2026-03-23) + +- T-181: 180-taxon dataset (mbank_X30754) added as large-tree tier +- T-180: Warm-start benchmark infrastructure for isolating escape quality + +### Large-tree scaling (ongoing) + +The 180-taxon dataset exposed that `maxSeconds` doesn't fire mid-TBR (T-177, +P1, ASSIGNED Human+AI). NNI warmup (T-178) and strategy presets (T-179) are +planned but T-179 is blocked on T-177. + +## Current Strategic Objectives + +### Objective 1–4: COMPLETE +- Phase 6 adaptive strategy, code quality, documentation, CRAN readiness +- Version 2.0.0 (major bump for new API) + +### Objective 5: MorphyLib Migration — PARTIAL (not blocking CRAN) +- Tier 1+2 done (TreeLength, CharacterLength, RandomTreeScore, deprecation) +- Tier 3/4 (remove MorphyLib source): Far future + +### Objective 6: Shiny App Modularization — COMPLETE + +### Objective 7: Benchmark Expansion — COMPLETE + +### Objective 8: Shiny Bug Fixes — COMPLETE + +### Objective 9: NEWS.md — COMPLETE + +### Objective 10: Multi-state Profile Parsimony — COMPLETE +All tasks T-101 through T-107 done. MaddisonSlatkin for 3–5 state characters, +feasibility guard for exponential cases (binary fallback with warning), Shiny +app verified. Sun2018 (54 tips, multistate) completes in 2.4s. + +### Objective 11: Alternative Inapplicable-Handling Algorithms — SUBSTANTIALLY COMPLETE +Three scoring methods now functional end-to-end in `MaximizeParsimony()`: +- **Brazeau et al. (2019)**: Three-pass NA algorithm (pre-existing, default) +- **HSJ (Hopkins & St. John 2021)**: Dissimilarity metric with α parameter. + Full C++ implementation, uppass bug fixed, `TreeLength()` HSJ support added. +- **X-transformation (Goloboff et al. 2021)**: Step-matrix recoding via Sankoff. + `recode_hierarchy()` + C++ Sankoff engine, end-to-end search verified. +- **Hierarchical resampling (T-124)**: Done. `Resample()` hierarchy-aware. + +R-level API: `CharacterHierarchy()` class, `hierarchy_from_names()` auto-detect, +`recode_hierarchy()` for xform. Vignette `inapplicable.Rmd` documents all three. + +**Remaining Phase 3 task:** +- T-126 (ASSIGNED D): Shiny app hierarchy UI + method selector + +### Objective 12: Shiny Search UX — COMPLETE +- T-127–T-130, T-137–T-141, T-143: All Shiny UX tasks done +- T-163: Search confidence with binomial bound + diagnostics +- T-164: Pool stats wired to Shiny (topology count, trajectory) + +### Objective 13: Subsample MPTs — COMPLETE +- T-135 DONE: `WideSample()` maximin tree subsampling +- T-136 DONE: Wire WideSample into Shiny tree thinning + +### Objective 14: ParsSim — COMPLETE +`ParsSim()` simulates datasets under parsimony (EW/IW/profile). Supports +per-taxon/per-character missing rates, rootState vectors. 128 tests passing. + +## Agent Status + +No active dispatched agents. Live state: `.dispatch/state.json`. + +## Task Pipeline Health + +- **3 unblocked OPEN**: T-245 (P3), T-269 (P3), T-270 (P2) +- **1 blocked OPEN**: T-253 (P3, needs T-252) +- **1 PARKED (GHA FAILED)**: T-150 (E, codoc mismatch — fix and re-dispatch needed) +- **Tasks on open PRs**: T-150 (#213), T-204 (#216), T-266 (#235) +- **3 PRs to cpp-search**: #213 (GHA failing), #216, #235 (all awaiting review). #210 (cpp-search→main) open. #178 stale DRAFT (T-272 filed to close). +- Standing task effective priority: **P2** (3 unblocked OPEN specific tasks) + +### Observations (Round 15) + +1. **Heredoc artifact (`EOF 2>&1`) caused GHA failures across branches.** + Agent F's merge workflow leaked shell heredoc terminators into + `test-ts-constraint-small.R`. Fixed on cpp-search (3a34cbe1) and + feature/parallel-temper (c2250aa3). This caused T-212's GHA to fail + (re-dispatched as run 23528636505). + +2. **PR #215 compile errors from merge artifacts.** F's merge of cpp-search + into feature/parallel-temper duplicated `anneal_*` fields in DrivenParams + and left stale individual anneal params in MaximizeParsimony's searchArgs + (C++ only accepts annealConfig list). Fixed by A (c2250aa3). + +3. **Simplification of "all in [0,?]" characters in inapplicable datasets.** + The `has_inapp` bypass in `ts_simplify.cpp` was overly conservative: + `?` tokens (all bits set including inapp bit) triggered the bypass, + preventing simplification of genuinely uninformative characters. + Fixed (a48bfc4a); GHA pending. + +4. **T-208 (PR #219) is closed.** Fix was cherry-picked to cpp-search + directly. Removed from active task list. + +5. **T-211 closed (not worth fixing).** Conservative-only impact confirmed + by Agent C. + +### Observations (Round 14) + +1. **T-211 closed (not worth fixing).** Agent C confirmed the conservative-only + impact: stale `final_` affects Boltzmann screening probability only, + `temper_full_rescore` gates all accepted moves. Fix cost (per-candidate + full rescore or save/restore all final_ arrays) exceeds negligible benefit. + +2. **T-212 committed directly to cpp-search.** 7 tests (24 expectations) + covering RANDOM_TREE strategy with constraints, serial and parallel + (nThreads=2). GHA running. + +3. **T-213 (impose_constraint) in progress by Agent A.** New `impose_constraint()` + function repairs topology after constraint-violating moves (NNI perturbation, + fuse). 88 new tests. GHA running on `feature/impose-constraint`. + +4. **T-214 filed: constraint enforcement bug on ≥10-tip trees.** Found by + Agent C during T-212 development. TBR search violates constraint splits on + 10-tip trees (100% violation with 2 splits, sporadic with 1 split). Works + on 5–6 tips. Pre-existing, affects all strategies. T-213's `impose_constraint()` + may address this indirectly by repairing violations post-hoc. + +5. **S-PR done (Agent F).** Resolved merge conflicts on PRs #215, #213, #221. + PR #222 has substantive conflicts (two different SA designs) requiring human + judgment. PRs #216 and #211 are clean. + +6. **PR #219 removed from list.** The T-208 fix (WAGNER_RANDOM fallback for + constrained RANDOM_TREE) was cherry-picked to cpp-search directly + (commit `24427c9a`). The PR may have been closed. + +7. **PR backlog is 6.** Recommended merge order unchanged from round 13: + #215 → #216 → #211 → #213 → smaller PRs. + +### Objective 15: Large-Tree Scaling & Search Optimization + +Motivated by 180-taxon dataset testing. Goal: make `MaximizeParsimony()` +effective and responsive at 100–200+ tips. + +**Sub-goals:** +1. **Bug fix: mid-TBR timeout** (P1). Pass `check_timeout` into + `tbr_search()` and `spr_search()` so they can bail out mid-pass. + Without this, `maxTime` is ineffective for large trees. +2. **NNI warmup** (P1). Add `nni_search()` before SPR in driven pipeline, + gated on `n_tip > ~100`. Provides O(n)-cost initial descent. +3. **Large-tree strategy preset** (P2). For ≥120 tips: NNI→SPR→TBR + escalation, scaled ratchet/drift cycles, sector size tuning. +4. **Large-tree benchmark dataset** (P2). Add 180-taxon dataset to + `dev/benchmarks/`. Separate timing tier for large trees. +5. **Warm-start benchmark** (P2). Seed search with pre-computed local + optimum, measure ratchet escape effectiveness in isolation. +6. **Adaptive ratchet perturbation** (P3). Start aggressive (~40%), + taper by hit rate as pool quality stabilizes. +7. **Pool-seeded Wagner** (P3). Use pool consensus as backbone constraint + during Wagner construction. Concern: run independence. Mitigate by + only activating after N diverse pool trees. + +**Status:** T-178 (NNI warmup), T-186 (NNI-perturbation), T-188 (biased +Wagner), T-189 (outer cycle), T-180 (warm-start benchmark), T-181 (180t +dataset), T-184 (maxTime alias) all complete. T-177 (mid-TBR timeout) in +progress (Human+AI). T-179 (large-tree preset) blocked on T-177. T-182, +T-183, T-187 are P3 nice-to-haves. + +### Objective 16: Extended Implied Weighting (XPIWE) — COMPLETE +All 7 tasks (T-156–T-162) done on feature/xpiwe branch. PR #212 open. + +### Objective 17: Parallel Tempering — COMPLETE (on branch) +All 4 tasks (T-198–T-201, formerly T-190–T-193) implemented by Agent C on +`feature/parallel-temper`. Stochastic TBR, multi-chain framework, pipeline +integration, and benchmarking. No PR yet. + +## Known Issues + +1. **Ratchet `active_mask` not RAII-protected**: Low risk — DataSet rebuilt per R call. +2. **Wagner NA `subtree_actives` staleness**: Documented UB in incremental NA + scoring during Wagner construction. `score_tree()` at end gives correct result. +3. **Shinylive blockers**: See `dev/plans/2026-03-17-shinylive-plan.md`. +4. **Partial-tip constraint upstream bug**: `TreeTools::AddUnconstrained` crashes + on zero-character phyDat. Full-tip constraints work. +5. **XFORM rebuilds SankoffData per score_tree() call** (noted by Agent E in + S-RED focus 4). Optimization opportunity for future work. +6. **Stochastic ParsSim test**: 1 chi-squared test in ParsSim suite can fail + with unlucky random seed (~0.1% probability per run). Pre-existing; not actionable. +7. ~~**`maxTime` ineffective for large trees** (T-177)~~: **RESOLVED.** `check_timeout` + callback now threaded through `tbr_search`, `spr_search`, `nni_search`. Merged. +8. ~~**MPT enumeration blocked by timeout** (T-202)~~: **RESOLVED.** Two-phase + timeout reserves 10% of budget for MPT plateau walk. Merged via PR #217. + +## Architecture Decisions Log + +| Date | Decision | Rationale | +|------|----------|-----------| +| 2026-03-26 | TNT benchmarks must use Fitch-mode scoring (`clean_inapplicable()` or `fitch_mode()`) | Brazeau three-pass scores are inherently higher than Fitch; comparing across methods produces spurious gaps (T-265 confound) | +| 2026-03-16 | Inter-replicate parallelism via std::thread | Simplest; avoids R memory allocator conflicts | +| 2026-03-16 | thread_local RNG, not parameter-passing | Avoids changing ~15 function signatures | +| 2026-03-16 | Concavity sentinel -1.0 in Rcpp exports | Rcpp can't translate R_PosInf | +| 2026-03-16 | MaximizeParsimony() → C++ engine; Morphy() → legacy | Clean migration path | +| 2026-03-17 | Adaptive strategy: sprint ≤30, default/thorough by nTip×nChar | Benchmark data | +| 2026-03-17 | T-025 fix: bounds-check PreallocUndo capacity | Root cause of P0 crash | +| 2026-03-18 | Shiny modularization: modules return reactive lists | Reactives re-exported in server.R scope | +| 2026-03-18 | Forward-ref callbacks via env for data→search dependency | Data module needs DisplayTreeScores before search module defined | +| 2026-03-18 | Test tiering: 3 tiers (CRAN/CI/extended) | T-073: skip guards prevent slow tests on CRAN | +| 2026-03-18 | Strategy threshold: nTip≥65 AND nChar≥100 | T-068: signal-density gate was backwards | +| 2026-03-18 | Profiling: Wagner negligible, parallel ~80% eff | S-PROF: no new optimization tasks needed | +| 2026-03-18 | events.R dissolved → ShowConfigs inlined in server.R | Top-level DOM element show/hide belongs at top level | +| 2026-03-19 | Hierarchy as separate MP arg, not phyDat attribute | Enables reuse across HSJ/xform methods | +| 2026-03-19 | Mixed Fitch+Sankoff scoring in score_tree() | Non-hierarchy chars use Fitch; recoded chars use Sankoff | +| 2026-03-19 | HSJ full-rescore only (no incremental) | Screen candidates with Fitch, full HSJ for promising ones | +| 2026-03-19 | Multistate profile: binary fallback for infeasible MaddisonSlatkin | k=3/n>15, k=4/n>10, k=5/n>8 thresholds | +| 2026-03-22 | Ratchet perturbation 4%→25%, 5 moves, 10–12 cycles | Systematic sweep: 4% zeroes ~10/253 chars — too gentle | +| 2026-03-22 | Drift cycles 4→2, ratchet 10→12 | Drift contributes ~0 per-replicate improvement | +| 2026-03-23 | NNI essential at >100 tips; redundant at ≤88 | O(n) vs O(n²) per pass; first TBR pass >5min at 180t | +| 2026-03-23 | Biased Wagner: softmax sampling, first start only | Purely greedy = same tree every time; stochastic biasing keeps diversity | +| 2026-03-23 | Outer cycle loop default=1 (backward-compatible) | TNT xmult pattern; interleave XSS between perturbation phases | +| 2026-03-23 | XPIWE default in Shiny | Missing-entries bias correction; eff_k = concavity / f | +| 2026-03-23 | Search confidence: binomial bound (1-K/R)^R | Tighter than exp(-K); falls back when K==R | +| 2026-03-23 | Pool topology count via count_at_best() | Distinct topologies at best score, not pool_size (includes suboptimal) | +| 2026-03-24 | T-202: Two-phase timeout for MPT enumeration | Reserve 10% budget; `enumTimeFraction` tunable via SearchControl | +| 2026-03-24 | Adaptive fuse_accept_equal: hits≥2 && pool≥3 | Auto-enable equal-score fusing when score stable; avoids early-search waste | +| 2026-03-24 | Skip TBR cleanup for equal-score fuse exchanges | Both trees already TBR-optimal; full TBR pass rarely finds improvements | +| 2026-03-24 | Cap equal-score-only fuse rounds at 3 | Diminishing returns from lateral exchanges; `max_equal_rounds` tunable | +| 2026-03-25 | perturbStopFactor default=2 | Benchmarked on 10 datasets (23–213 tips): 2.4–6.9x speedup, 0 score loss. Complementary to targetHits on hard landscapes where hit rate is low | + +## Future: Search Convergence Diagnostics (post-v2.0.0) + +The current `exp(-K)` "miss probability" shown in the Shiny app is dataset- +independent and oversimplified. **T-163/T-164** implement a first improvement +(binomial bound + topology diversity + trajectory flags). + +Longer-term ideas for a later package version: + +1. **IQPNNI-style Weibull record-value stopping** (Vinh & von Haeseler 2004). + Model inter-improvement gaps within a replicate as Weibull; estimate + probability of further improvement. Dataset-adaptive by construction. + Needs adaptation from within-run iteration-level to TreeSearch's + multi-replicate framework. + +2. **Chao1-style score-spectrum estimator.** Treat distinct parsimony scores + found across replicates as "species"; use the singleton/doubleton ratio + (f1²/2f2) to estimate number of unseen score levels — including potentially + better ones. Requires collecting the full score distribution from search + (not currently returned). + +3. **Dataset difficulty prediction (Pythia-style).** ML-based prediction of + landscape ruggedness from dataset features (tip count, character count, + signal density). Would allow adaptive confidence messaging ("this dataset + is expected to be easy/hard"). Requires training data from empirical + benchmarks. + +## Completed Milestones + +| Phase | Description | Date | +|-------|-------------|------| +| 1A–6E | Feature-complete C++ engine | 2026-03-16–17 | +| — | Version 2.0.0, CRAN-ready | 2026-03-17 | +| — | Benchmark expansion (T-067–T-069) | 2026-03-18 | +| — | Test tiering (T-073) | 2026-03-18 | +| — | Shiny modularization complete (Phases 1–5) | 2026-03-18 | +| — | Shiny bug fixes complete (Obj 8) | 2026-03-18 | +| — | NEWS.md updated for v2.0.0 | 2026-03-18 | +| — | **All 9 original objectives COMPLETE** | 2026-03-18 | +| — | Multi-state profile parsimony (Obj 10) | 2026-03-19 | +| — | ParsSim dataset simulator (Obj 14) | 2026-03-19 | +| — | Inapplicable handling: HSJ + xform end-to-end (Obj 11) | 2026-03-19 | +| — | Subsample MPTs: WideSample + Shiny (Obj 13) | 2026-03-19 | +| — | Ratchet/drift tuning + polytomy-search merge | 2026-03-22 | +| — | NNI warmup, NNI-perturbation, biased Wagner, outer cycle | 2026-03-23 | +| — | XPIWE feature complete (Obj 16) | 2026-03-23 | +| — | Search confidence + pool stats wiring (Obj 12 done) | 2026-03-23 | +| — | Large-tree benchmark tier + warm-start infrastructure | 2026-03-23 | diff --git a/debug_ratchet.R b/debug_ratchet.R new file mode 100644 index 000000000..e6d285d5d --- /dev/null +++ b/debug_ratchet.R @@ -0,0 +1,22 @@ +devtools::load_all(quiet = TRUE) +library(TreeTools) + +# Replicate the failing test +true_tree <- ape::read.tree(text = "(((((1,2),3),4),5),6);") +dataset <- TreeTools::StringToPhyDat("110000 111000 111100", 1:6, byTaxon = FALSE) +start_tree <- TreeTools::RenumberTips(ape::read.tree( + text = "(((1, 6), 3), (2, (4, 5)));"), true_tree$tip.label) + +cat("Start tree score:", TreeLength(start_tree, dataset), "\n") +cat("True tree score:", TreeLength(true_tree, dataset), "\n") + +# Run Ratchet +set.seed(42) # For reproducibility +ratchetResult <- Ratchet(start_tree, dataset, + swappers = list(RootedTBRSwap, RootedSPRSwap, RootedNNISwap), + ratchIter = 3, searchHits = 5, verbosity = 2) + +ratchetScore <- attr(ratchetResult, "score") +cat("Ratchet score:", ratchetScore, "\n") +cat("Expected score:", TreeLength(true_tree, dataset), "\n") +cat("Test passes:", isTRUE(all.equal(TreeLength(true_tree, dataset), ratchetScore)), "\n") diff --git a/dev/benchmarks/ab_pr_bench.R b/dev/benchmarks/ab_pr_bench.R new file mode 100644 index 000000000..2ba5505aa --- /dev/null +++ b/dev/benchmarks/ab_pr_bench.R @@ -0,0 +1,79 @@ +#!/usr/bin/env Rscript +# A/B: original (converging reduced-TBR) vs optimised (5-move limit + code fixes) +# Both libraries must already be installed. +# orig_lib = .vtune-lib (tbr_max_moves=0, no build_postorder fix) +# opt_lib = .agent-Eopt (tbr_max_moves=5, build_postorder deferred) +# +# Usage: Rscript dev/benchmarks/ab_pr_bench.R + +orig_lib <- ".vtune-lib" +opt_lib <- ".agent-Eopt" +datasets <- c("Zhu2013", "Dikow2009") +seeds <- 1:5 +budget <- 20L + +run_one <- function(lib, label, ds_name, seed, budget) { + # Write temp script to avoid shell quoting issues + tmp <- tempfile(fileext = ".R") + writeLines(c( + sprintf('.libPaths(c("%s", .libPaths()))', lib), + 'library(TreeSearch)', + sprintf('ds <- inapplicable.phyData[["%s"]]', ds_name), + sprintf('set.seed(%d)', seed), + 't0 <- proc.time()', + sprintf( + 'res <- MaximizeParsimony(ds, maxSeconds=%dL, strategy="auto",', + budget), + ' pruneReinsertCycles=5L, pruneReinsertDrop=0.10,', + ' driftCycles=0L, nniPerturbCycles=0L, verbosity=0L, nThreads=1L)', + sprintf( + 'cat(sprintf("%s|%s|%d|%%g|%%d|%%.2f\\n",', + label, ds_name, seed), + ' attr(res,"score"), attr(res,"replicates"), (proc.time()-t0)[3])' + ), tmp) + out <- system2("Rscript", c("--no-save", tmp), + stdout = TRUE, stderr = FALSE) + unlink(tmp) + trimws(tail(out, 1)) +} + +results <- data.frame( + label = character(), dataset = character(), seed = integer(), + score = numeric(), reps = integer(), wall = numeric(), + stringsAsFactors = FALSE +) + +for (ds in datasets) { + cat(sprintf("\n=== %s ===\n", ds)) + for (s in seeds) { + for (cfg in list(list("orig", orig_lib), list("opt5", opt_lib))) { + line <- run_one(cfg[[2]], cfg[[1]], ds, s, budget) + cat(line, "\n") + parts <- strsplit(line, "\\|")[[1]] + if (length(parts) == 6) { + results <- rbind(results, data.frame( + label = parts[1], dataset = parts[2], seed = as.integer(parts[3]), + score = as.numeric(parts[4]), reps = as.integer(parts[5]), + wall = as.numeric(parts[6]), + stringsAsFactors = FALSE + )) + } + } + } +} + +cat("\n=== Median summary (5 seeds) ===\n") +agg <- aggregate(cbind(score, reps, wall) ~ label + dataset, results, median) +print(agg[order(agg$dataset, agg$label), ], row.names = FALSE) + +cat("\n=== Delta: opt5 vs orig ===\n") +for (ds in datasets) { + orig <- agg[agg$label == "orig" & agg$dataset == ds, ] + opt <- agg[agg$label == "opt5" & agg$dataset == ds, ] + cat(sprintf("%s: score %+.0f, reps %+.0f (%.0f%%), wall %+.2fs\n", + ds, + opt$score - orig$score, + opt$reps - orig$reps, + (opt$reps - orig$reps) / orig$reps * 100, + opt$wall - orig$wall)) +} diff --git a/dev/benchmarks/ab_pr_seq.R b/dev/benchmarks/ab_pr_seq.R new file mode 100644 index 000000000..86b055375 --- /dev/null +++ b/dev/benchmarks/ab_pr_seq.R @@ -0,0 +1,65 @@ +#!/usr/bin/env Rscript +# Sequential A/B: original vs optimised PR, run in subprocesses one at a time + +orig_lib <- "C:/Users/pjjg18/GitHub/TreeSearch-a/.vtune-lib" +opt_lib <- "C:/Users/pjjg18/GitHub/TreeSearch-a/.agent-Eopt" + +run_bench <- function(lib, label, ds_name, seed, budget = 20L) { + tmp <- tempfile(fileext = ".R") + writeLines(c( + sprintf('.libPaths(c("%s", .libPaths()))', gsub("\\\\", "/", lib)), + 'suppressPackageStartupMessages(library(TreeSearch))', + sprintf('ds <- inapplicable.phyData[["%s"]]', ds_name), + sprintf('set.seed(%dL)', seed), + 't0 <- proc.time()', + sprintf('res <- MaximizeParsimony(ds, maxSeconds = %dL,', budget), + ' strategy = "auto", pruneReinsertCycles = 5L,', + ' pruneReinsertDrop = 0.10, driftCycles = 0L,', + ' nniPerturbCycles = 0L, verbosity = 0L, nThreads = 1L)', + 'elapsed <- (proc.time() - t0)[[3]]', + 'cat(attr(res, "score"), attr(res, "replicates"), round(elapsed, 2))' + ), tmp) + out <- system2("Rscript", c("--no-save", tmp), stdout = TRUE, stderr = FALSE) + unlink(tmp) + vals <- as.numeric(strsplit(trimws(paste(out, collapse = " ")), "\\s+")[[1]]) + if (length(vals) >= 3) { + cat(sprintf(" %s | %s | seed=%d | score=%g reps=%d wall=%.2fs\n", + label, ds_name, seed, vals[1], vals[2], vals[3])) + return(data.frame(label=label, dataset=ds_name, seed=seed, + score=vals[1], reps=as.integer(vals[2]), wall=vals[3])) + } else { + cat(sprintf(" %s | %s | seed=%d | FAILED\n", label, ds_name, seed)) + return(NULL) + } +} + +results <- list() +for (ds in c("Zhu2013", "Dikow2009")) { + cat(sprintf("\n=== Dataset: %s ===\n", ds)) + for (s in 1:5) { + results[[length(results)+1]] <- run_bench(orig_lib, "orig(cvg)", ds, s) + Sys.sleep(0.5) # let DLL unload + results[[length(results)+1]] <- run_bench(opt_lib, "opt(5mv)", ds, s) + Sys.sleep(0.5) + } +} + +df <- do.call(rbind, Filter(Negate(is.null), results)) + +cat("\n=== Median over 5 seeds ===\n") +agg <- aggregate(cbind(score, reps, wall) ~ label + dataset, df, median) +for (ds in unique(df$dataset)) { + sub <- agg[agg$dataset == ds, ] + orig <- sub[sub$label == "orig(cvg)", ] + opt <- sub[sub$label == "opt(5mv)", ] + cat(sprintf("\n%s:\n", ds)) + cat(sprintf(" orig(cvg): score=%.0f reps=%d wall=%.2fs\n", + orig$score, orig$reps, orig$wall)) + cat(sprintf(" opt(5mv): score=%.0f reps=%d wall=%.2fs\n", + opt$score, opt$reps, opt$wall)) + cat(sprintf(" Delta: score=%+.0f reps=%+.0f (%+.0f%%) wall=%+.2fs\n", + opt$score - orig$score, + opt$reps - orig$reps, + (opt$reps - orig$reps) / orig$reps * 100, + opt$wall - orig$wall)) +} diff --git a/dev/benchmarks/accept_equal_true.csv b/dev/benchmarks/accept_equal_true.csv new file mode 100644 index 000000000..1d549a82b --- /dev/null +++ b/dev/benchmarks/accept_equal_true.csv @@ -0,0 +1,19 @@ +"dataset","seed","score","candidates" +"Wortley2006",1,485,41570896 +"Eklund2004",1,440,84684820 +"Zanol2014",1,1267,348763739 +"Zhu2013",1,626,341221205 +"Giles2015",1,672,417576399 +"Dikow2009",1,1606,372115534 +"Wortley2006",2,483,38616547 +"Eklund2004",2,440,93873455 +"Zanol2014",2,1264,358862388 +"Zhu2013",2,630,337517753 +"Giles2015",2,672,503259202 +"Dikow2009",2,1606,416466253 +"Wortley2006",3,485,33476553 +"Eklund2004",3,440,96750733 +"Zanol2014",3,1271,316853533 +"Zhu2013",3,629,340410843 +"Giles2015",3,671,491328798 +"Dikow2009",3,1606,405179884 diff --git a/dev/benchmarks/bench_beam.R b/dev/benchmarks/bench_beam.R new file mode 100644 index 000000000..8bb2edcea --- /dev/null +++ b/dev/benchmarks/bench_beam.R @@ -0,0 +1,68 @@ +# Beam sectorial vs the single-tree baseline, from the canonical T0. +# Tests whether running RSS over a RETAINED diverse buffer (beam) escapes the +# frozen T0 where single-tree sectorial plateaus (~1267 on Zanol). See +# dev/plans/2026-06-18-beam-sectorial.md. +# +# Budget is MATCHED: both arms run rssRounds x TS_RSS_PICKS = 30 x 20 = 600 +# sector searches. The ONLY differences in the beam arm: (a) each round starts +# from a beam-picked tree, not the cumulative single tree; (b) accept_equal is +# forced ON inside beam_sectorial (the diversity engine); (c) results written to +# a shared buffer. coll30_20 single-tree reached only ~-4 (1267) at this budget. +# +# Arms (TS_BMARMS env, space-sep; default "single beam"): +# single coll30_20 single-tree (TS_BEAM unset, accept_equal FALSE) -- baseline +# beam same params, TS_BEAM=1 (best-equal buffer, accept_equal forced ON) +# beamWide beam + TS_BEAM_SUBOPT band + TS_BEAM_PICKALL (Claim B, second test) +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", "C:/Users/pjjg18/GitHub/TS-selectem/.agent-selectem"), + winslash = "/")) + library(TreeTools) +}) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Zhu2013 Wortley2006 Giles2015")), "\\s+")[[1]] +arms <- strsplit(trimws(Sys.getenv("TS_BMARMS", "single beam")), "\\s+")[[1]] +ROUNDS <- as.integer(Sys.getenv("TS_RSSROUNDS", "30")) +PICKS <- Sys.getenv("TS_RSS_PICKS", "20") +SEEDS <- as.integer(strsplit(Sys.getenv("TS_SEEDS", "1 2 3"), "\\s+")[[1]]) +SUBOPT <- Sys.getenv("TS_BEAM_SUBOPT", "5") # band width for beamWide arm +target <- c(Zanol2014 = 1261, Wortley2006 = 480, Zhu2013 = 624, Giles2015 = 670) +t0dir <- "dev/benchmarks/t0" + +run_arm <- function(phy, t0, arm, seed) { + set.seed(seed) + Sys.setenv(TS_RSS_PICKS = PICKS) + if (arm == "single") { + Sys.unsetenv("TS_BEAM"); Sys.unsetenv("TS_BEAM_SUBOPT"); Sys.unsetenv("TS_BEAM_PICKALL") + } else if (arm == "beam") { + Sys.setenv(TS_BEAM = "1"); Sys.unsetenv("TS_BEAM_SUBOPT"); Sys.unsetenv("TS_BEAM_PICKALL") + } else if (arm == "beamWide") { + Sys.setenv(TS_BEAM = "1", TS_BEAM_SUBOPT = SUBOPT, TS_BEAM_PICKALL = "1") + } else if (arm == "beamMulti") { + # Full TNT analog: K diverse RAS+TBR seeds + wide buffer (retains them as + # best drops) + pick-all (re-solves each seed's distinct descent). + Sys.setenv(TS_BEAM = "1", TS_BEAM_SEEDS = Sys.getenv("TS_BEAM_SEEDS", "10"), + TS_BEAM_SUBOPT = SUBOPT, TS_BEAM_PICKALL = "1") + } + r <- suppressWarnings(MaximizeParsimony(phy, tree = t0, maxReplicates = 1L, nThreads = 1L, + maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, driftCycles = 0L, + xssRounds = 0L, cssRounds = 0L, rssRounds = ROUNDS, wagnerStarts = 1L, + fuseInterval = 9999L, sectorMinSize = 31L, sectorMaxSize = 99L, + rasStarts = 3L, sectorCollapseTarget = 30L, sectorAcceptEqual = FALSE)) + Sys.unsetenv("TS_BEAM"); Sys.unsetenv("TS_BEAM_SUBOPT"); Sys.unsetenv("TS_BEAM_PICKALL") + Sys.unsetenv("TS_RSS_PICKS") + min(as.double(attr(r, "score"))) +} + +for (nm in dsN) { + phy <- readRDS(file.path(t0dir, paste0(nm, ".phy.rds"))) + t0 <- ape::read.tree(file.path(t0dir, paste0(nm, ".tre"))) + t0len <- TreeLength(t0, phy); tgt <- target[[nm]] + cat(sprintf("\n==== %s | T0=%.0f target=%d (gap %+.0f) | budget %dx%s ====\n", + nm, t0len, tgt, tgt - t0len, ROUNDS, PICKS)) + for (an in arms) { + sc <- vapply(SEEDS, function(s) run_arm(phy, t0, an, s), double(1)) + best <- min(sc) + cat(sprintf(" %-9s seeds[%s] -> %s | best %.0f (%+.0f vs T0, %+.0f vs target)%s\n", + an, paste(SEEDS, collapse = ","), paste(format(sc), collapse = " "), + best, best - t0len, best - tgt, if (best <= tgt) " <== REACHED" else "")) + } +} diff --git a/dev/benchmarks/bench_cell.R b/dev/benchmarks/bench_cell.R new file mode 100644 index 000000000..4bdc634b2 --- /dev/null +++ b/dev/benchmarks/bench_cell.R @@ -0,0 +1,41 @@ +# Single-cell runner for SLURM job arrays (and local testing). +# +# Runs ONE (dataset x seed) cell of a panel and writes one partial CSV, so a +# panel can fan out across a SLURM --array (one task per cell). Replicate-bounded +# (deterministic candidates), nThreads=1. Merge partials with hamilton_merge.sh. +# +# Cell index: arg[1] or $SLURM_ARRAY_TASK_ID (0-based) into expand.grid(dataset, seed). +# Env: TS_LIB, TS_DATASETS, TS_SEEDS, TS_REPS, PARTIAL_DIR. +# Local test: Rscript dev/benchmarks/bench_cell.R 0 + +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-p0"), + winslash = "/")) + library(TreeTools) +}) +args <- commandArgs(trailingOnly = TRUE) +idx <- as.integer(if (length(args) >= 1L) args[[1]] else Sys.getenv("SLURM_ARRAY_TASK_ID", "0")) +reps <- as.integer(Sys.getenv("TS_REPS", "20")) +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3 4 5")), "\\s+")[[1]]) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Eklund2004 Zanol2014 Zhu2013 Giles2015 Dikow2009")), "\\s+")[[1]] +partdir <- Sys.getenv("PARTIAL_DIR", "dev/benchmarks/partials") + +grid <- expand.grid(dataset = dsN, seed = seeds, stringsAsFactors = FALSE) +if (idx < 0L || idx >= nrow(grid)) + stop(sprintf("cell index %d out of range [0, %d)", idx, nrow(grid))) +row <- grid[idx + 1L, ] + +data("inapplicable.phyData", package = "TreeSearch") +m <- PhyDatToMatrix(inapplicable.phyData[[row$dataset]], ambigNA = FALSE) +m[m == "-"] <- "?" +d <- MatrixToPhyDat(m) +set.seed(row$seed) +r <- suppressWarnings(MaximizeParsimony(d, maxReplicates = reps, targetHits = 999L, + maxSeconds = 0, nThreads = 1L, verbosity = 0L)) +out <- data.frame(dataset = row$dataset, seed = row$seed, score = attr(r, "score"), + candidates = attr(r, "candidates_evaluated"), stringsAsFactors = FALSE) +dir.create(partdir, showWarnings = FALSE, recursive = TRUE) +write.csv(out, file.path(partdir, sprintf("cell_%04d.csv", idx)), row.names = FALSE) +cat(sprintf("cell %d: %s seed %d -> score %g, candidates %s\n", + idx, row$dataset, row$seed, out$score, format(out$candidates, big.mark = ","))) diff --git a/dev/benchmarks/bench_clip_ordering.R b/dev/benchmarks/bench_clip_ordering.R new file mode 100644 index 000000000..daa376a64 --- /dev/null +++ b/dev/benchmarks/bench_clip_ordering.R @@ -0,0 +1,215 @@ +# bench_clip_ordering.R +# +# Benchmark comparison of TBR clip ordering strategies. +# +# Compares six clip_order variants: +# 0 = RANDOM (current default) +# 1 = INV_WEIGHT (w = 1/(1+s)) +# 2 = TIPS_FIRST (tips first, then rest; shuffled within) +# 3 = BUCKET (tips / small / large buckets; shuffled within) +# 4 = ANTI_TIP (non-tips first, then tips) +# 5 = LARGE_FIRST (large > √n first, then small, then tips) +# +# Metric: time-adjusted expected best (TAEB) score — the expected minimum +# score from k independent replicates where k = floor(budget / rep_time). +# Estimated via bootstrap resampling of per-replicate scores. +# +# Usage: Rscript dev/benchmarks/bench_clip_ordering.R [lib_path] [n_seeds] +# lib_path defaults to ".agent-wc" +# n_seeds defaults to 10 + +args <- commandArgs(trailingOnly = TRUE) +lib_path <- if (length(args) >= 1) args[1] else ".agent-wc" +n_seeds <- if (length(args) >= 2) as.integer(args[2]) else 10L + +library(TreeSearch, lib.loc = lib_path) + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +DATASETS <- c("Vinther2008", "Agnarsson2004", "Zhu2013", "Dikow2009") +BUDGETS <- c(30, 60) # seconds +SEEDS <- seq_len(n_seeds) * 1000L + 847L + +ORDERS <- c( + RANDOM = 0L, + INV_WEIGHT = 1L, + TIPS_FIRST = 2L, + BUCKET = 3L, + ANTI_TIP = 4L, + LARGE_FIRST= 5L +) + +# Expected-best bootstrap: given per-replicate scores and total wall time, +# estimate E[min score] at each time budget. +taeb <- function(scores, times_ms, budgets_s, n_boot = 2000L) { + stopifnot(length(scores) == length(times_ms)) + n <- length(scores) + if (n == 0) return(setNames(rep(NA_real_, length(budgets_s)), budgets_s)) + + # Mean time per replicate (use median to be robust to outliers) + med_time_s <- median(times_ms) / 1000 + if (med_time_s <= 0) med_time_s <- 1 + + vapply(budgets_s, function(budget) { + k <- max(1L, floor(budget / med_time_s)) + if (k >= n) { + # Can't bootstrap more than n replicates; just return min + return(min(scores)) + } + boot_mins <- replicate(n_boot, min(sample(scores, k, replace = TRUE))) + mean(boot_mins) + }, numeric(1L)) +} + +# --------------------------------------------------------------------------- +# Prepare datasets +# --------------------------------------------------------------------------- + +prepare <- function(name) { + ds <- TreeSearch::inapplicable.phyData[[name]] + at <- attributes(ds) + list( + name = name, + contrast = at$contrast, + tip_data = matrix(unlist(ds, use.names = FALSE), + nrow = length(ds), byrow = TRUE), + weight = at$weight, + levels = at$levels, + n_taxa = length(ds) + ) +} + +# --------------------------------------------------------------------------- +# Build a default SearchControl with preset based on n_tip +# --------------------------------------------------------------------------- + +make_sc <- function(n_tip, clip_order_int = 0L) { + # Mirror the "default" preset for datasets in 31-119 tip range, + # with only the clip_order changed. This gives a realistic context + # (same ratchet/XSS/RSS settings as normal use) for the comparison. + # + # NOTE: maxSeconds is set per run; runtimeConfig controls the budget. + # Here we only set SearchControl parameters. + SearchControl( + tbrMaxHits = 1L, + clipOrder = clip_order_int, + nniFirst = TRUE, + sprFirst = FALSE, + wagnerStarts = 1L, + wagnerBias = 0L, + outerCycles = 1L, + maxOuterResets = 0L, + ratchetCycles = 12L, + ratchetPerturbProb = 0.25, + ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = FALSE, + nniPerturbCycles = 0L, + driftCycles = 0L, + xssRounds = 3L, xssPartitions = 4L, + rssRounds = 1L, + cssRounds = 0L, + fuseInterval = 3L, + adaptiveLevel = TRUE, + consensusStableReps = 0L + ) +} + +make_runtime <- function(max_seconds) { + list(maxReplicates = 9999L, targetHits = 9999L, + maxSeconds = max_seconds, verbosity = 0L, nThreads = 1L) +} + +# --------------------------------------------------------------------------- +# Data collection +# --------------------------------------------------------------------------- + +cat(sprintf("Benchmark: %d datasets × %d ordering variants × %d seeds\n", + length(DATASETS), length(ORDERS), n_seeds)) +cat(sprintf("Budgets: %s seconds\n\n", paste(BUDGETS, collapse = ", "))) + +all_results <- list() + +for (dname in DATASETS) { + d <- prepare(dname) + + cat(sprintf("=== %s (n_tip=%d) ===\n", dname, d$n_taxa)) + + ds_results <- list() + + for (oname in names(ORDERS)) { + oint <- ORDERS[[oname]] + sc <- make_sc(d$n_taxa, oint) + + rep_scores <- numeric(n_seeds) + rep_times <- numeric(n_seeds) # ms per replicate + + for (i in seq_along(SEEDS)) { + set.seed(SEEDS[i]) + res <- TreeSearch:::ts_driven_search( + d$contrast, d$tip_data, d$weight, d$levels, + searchControl = sc, + runtimeConfig = make_runtime(max(BUDGETS)), + scoringConfig = list(min_steps = integer(), concavity = -1.0, + xpiwe = FALSE, xpiwe_r = 0.5, xpiwe_max_f = 5.0, + obs_count = integer(), infoAmounts = NULL) + ) + rep_scores[i] <- res$best_score + # Estimate per-replicate time from timings + t_total_ms <- sum(unlist(res$timings)) + n_reps <- max(1L, res$n_replicates) + rep_times[i] <- t_total_ms / n_reps + } + + taeb_vals <- taeb(rep_scores, rep_times, BUDGETS) + cat(sprintf(" %-12s: scores %s med_rep %.1fs TAEB@%ds=%.1f @%ds=%.1f\n", + oname, + paste(sprintf("%.0f", range(rep_scores)), collapse = "-"), + median(rep_times)/1000, + BUDGETS[1], taeb_vals[1], + BUDGETS[2], taeb_vals[2])) + + ds_results[[oname]] <- list( + order = oname, order_int = oint, + dataset = dname, n_tip = d$n_taxa, + scores = rep_scores, times_ms = rep_times, + taeb = taeb_vals + ) + } + + all_results[[dname]] <- ds_results + cat("\n") +} + +# --------------------------------------------------------------------------- +# Summary: Δ vs RANDOM baseline for each variant, averaged across datasets +# --------------------------------------------------------------------------- + +cat("=== Summary: TAEB Δ vs RANDOM (lower = better) ===\n\n") + +for (budget in BUDGETS) { + cat(sprintf("--- Budget: %ds ---\n", budget)) + cat(sprintf(" %-15s", "")) + for (dname in DATASETS) cat(sprintf(" %13s", dname)) + cat(sprintf(" %13s\n", "mean_delta")) + + for (oname in names(ORDERS)) { + if (oname == "RANDOM") next + cat(sprintf(" %-15s", oname)) + deltas <- numeric(length(DATASETS)) + for (j in seq_along(DATASETS)) { + dname <- DATASETS[j] + ref <- all_results[[dname]][["RANDOM"]]$taeb[[which(BUDGETS == budget)]] + this_val <- all_results[[dname]][[oname]]$taeb[[which(BUDGETS == budget)]] + delta <- this_val - ref # positive = worse (more steps) + deltas[j] <- delta + cat(sprintf(" %+13.2f", delta)) + } + cat(sprintf(" %+13.2f\n", mean(deltas))) + } + cat("\n") +} + +cat("Positive Δ = worse than RANDOM; negative Δ = better than RANDOM.\n") +cat("Done.\n") diff --git a/dev/benchmarks/bench_collapsed.R b/dev/benchmarks/bench_collapsed.R new file mode 100644 index 000000000..151751036 --- /dev/null +++ b/dev/benchmarks/bench_collapsed.R @@ -0,0 +1,129 @@ +#!/usr/bin/env Rscript +# Benchmark collapsed-tree optimization: skip counts, wall time, score equivalence +# +# Usage: Rscript dev/benchmarks/bench_collapsed.R +# +# Runs each dataset 3 times with fixed seeds and reports: +# - Skip counts (via ts_tbr_search on near-optimal tree) +# - Driven search wall time and score +# - Per-phase timing breakdown + +args <- commandArgs(trailingOnly = TRUE) +lib_path <- if (length(args) >= 1) args[1] else ".agent-a" + +library(TreeSearch, lib.loc = lib_path) +library(TreeTools) + +# --- Datasets --- +datasets <- c("Vinther2008", "Agnarsson2004", "Zhu2013", "Dikow2009") + +prepare <- function(name) { + ds <- TreeSearch::inapplicable.phyData[[name]] + at <- attributes(ds) + list( + contrast = at$contrast, + tip_data = matrix(unlist(ds, use.names = FALSE), + nrow = length(ds), byrow = TRUE), + weight = at$weight, + levels = at$levels, + n_taxa = length(ds) + ) +} + +# --- Part 1: Skip count measurement via TBR on near-optimal trees --- +cat("=== Part 1: Collapsed-flag skip counts (TBR) ===\n\n") + +for (nm in datasets) { + d <- prepare(nm) + n_tip <- d$n_taxa + n_internal <- n_tip - 1L + total_clips <- n_tip + n_internal - 1L # all nodes except root + + # Build a near-optimal tree via short driven search + set.seed(7391) + quick <- TreeSearch:::ts_driven_search( + d$contrast, d$tip_data, d$weight, d$levels, + maxReplicates = 3L, targetHits = 2L, + ratchetCycles = 3L, driftCycles = 1L, + xssRounds = 1L, xssPartitions = 3L, + rssRounds = 0L, cssRounds = 0L, + fuseInterval = 3L, maxSeconds = 30, + verbosity = 0L, nThreads = 1L + ) + + # Run TBR from that tree (converged = already at local optimum) + edge <- quick$trees[[1]] + tbr_res <- TreeSearch:::ts_tbr_search( + edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = 10L, acceptEqual = FALSE + ) + + pct_skip <- round(100 * tbr_res$n_zero_skipped / + (tbr_res$n_zero_skipped + tbr_res$n_evaluated), 1) + + cat(sprintf("%-15s tips=%d score=%.0f evaluated=%d skipped=%d skip%%=%.1f%%\n", + nm, n_tip, tbr_res$score, + tbr_res$n_evaluated, tbr_res$n_zero_skipped, pct_skip)) +} + +# --- Part 2: Driven search wall time & scores --- +cat("\n=== Part 2: Driven search (3 seeds × 4 datasets) ===\n\n") + +seeds <- c(2847L, 5192L, 8634L) +results <- list() + +for (nm in datasets) { + d <- prepare(nm) + + for (s in seeds) { + set.seed(s) + t0 <- proc.time() + res <- TreeSearch:::ts_driven_search( + d$contrast, d$tip_data, d$weight, d$levels, + maxReplicates = 10L, targetHits = 5L, + ratchetCycles = 5L, driftCycles = 2L, + xssRounds = 3L, xssPartitions = 4L, + rssRounds = 1L, cssRounds = 0L, + fuseInterval = 3L, maxSeconds = 60, + verbosity = 0L, nThreads = 1L + ) + elapsed <- (proc.time() - t0)[3] + + tim <- res$timings + row <- data.frame( + dataset = nm, + seed = s, + score = res$best_score, + reps = res$replicates, + pool = res$pool_size, + wall_s = round(elapsed, 2), + tbr_ms = round(tim[["tbr_ms"]], 0), + ratchet_ms = round(tim[["ratchet_ms"]], 0), + drift_ms = round(tim[["drift_ms"]], 0), + xss_ms = round(tim[["xss_ms"]], 0), + rss_ms = round(tim[["rss_ms"]], 0), + fuse_ms = round(tim[["fuse_ms"]], 0), + final_tbr_ms = round(tim[["final_tbr_ms"]], 0), + stringsAsFactors = FALSE + ) + results <- c(results, list(row)) + cat(sprintf(" %-15s seed=%d score=%.0f reps=%d wall=%.2fs\n", + nm, s, res$best_score, res$replicates, elapsed)) + } +} + +results_df <- do.call(rbind, results) + +cat("\n=== Summary by dataset ===\n\n") +for (nm in datasets) { + sub <- results_df[results_df$dataset == nm, ] + cat(sprintf("%-15s best=%.0f median_wall=%.2fs median_tbr_ms=%.0f median_ratchet_ms=%.0f median_drift_ms=%.0f\n", + nm, + min(sub$score), + median(sub$wall_s), + median(sub$tbr_ms), + median(sub$ratchet_ms), + median(sub$drift_ms))) +} + +cat("\nDone.\n") diff --git a/dev/benchmarks/bench_commensurate.R b/dev/benchmarks/bench_commensurate.R new file mode 100644 index 000000000..e30930b3c --- /dev/null +++ b/dev/benchmarks/bench_commensurate.R @@ -0,0 +1,54 @@ +# COMMENSURABILITY CHECK (advisor): is TNT's stdout sectorial score on the same +# scale as our TreeLength? The shared-start "gap" compared our TreeLength-scored +# tree to TNT's STDOUT number for a tree we never re-scored. Here we save TNT's +# actual best tree and score it ourselves. +# TreeLength(TNT_best) ~ TreeLength(T0) (e.g. both 1275) while TNT stdout says +# T0=1275 sect=1262 => SCORING OFFSET, same tree, NO search gap. +# TreeLength(TNT_best) genuinely < TreeLength(T0) => real improvement, real gap. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Wortley2006 Zhu2013 Giles2015")), "\\s+")[[1]] +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +tl_any <- function(tr, phy) { + if (is.null(tr)) return(NA_real_) + if (inherits(tr, "multiPhylo")) min(vapply(tr, TreeLength, double(1), phy)) else TreeLength(tr, phy) +} +# Unique per-process temp dir: a stale/locked data.tnt from a dead TNT orphan in +# a shared dir makes the next run fail silently (NA). Script stem MUST be purely +# alphabetic and not a TNT command -- TNT parses the filename as its command line +# (see dev/expertise/tnt.md). "c.run" => command `c` (ccode) => "Must read data +# before changing character settings"; "cmnstest.run" is safe. +wd <- file.path(tempdir(), paste0("commens", Sys.getpid())) +unlink(wd, recursive = TRUE); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", "rseed 1;", "taxname=;", + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", 8), "tsave *best.tre;", "save;", "tsave/;", "quit;") + writeLines(script, file.path(wd, "cmnstest.run")) + old <- setwd(wd) + out <- suppressWarnings(system2(TNT, args = "cmnstest.run;", stdout = TRUE, stderr = TRUE)) + setwd(old) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + if (!any(grepl("Best score", out))) { + cat(sprintf("==== %s: TNT produced NO score; raw output ====\n", nm)) + cat(head(out, 30), sep = "\n"); cat("\n") + } + s_tbr <- num(sub(".*Best score \\(TBR\\):\\s*([0-9.]+).*", "\\1", + grep("Best score \\(TBR\\):", out, value = TRUE)[1])) + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + s_sect <- if (length(s_sect)) s_sect[length(s_sect)] else NA + t0 <- tryCatch(ReadTntTree(file.path(wd, "t0.tre")), error = function(e) NULL) + best <- tryCatch(ReadTntTree(file.path(wd, "best.tre")), error = function(e) NULL) + cat(sprintf("%-11s | TNT stdout: T0=%.0f sect=%.0f | TreeLength: T0=%.0f best=%.0f\n", + nm, s_tbr, s_sect, tl_any(t0, phy), tl_any(best, phy))) +} diff --git a/dev/benchmarks/bench_css_oracle.R b/dev/benchmarks/bench_css_oracle.R new file mode 100644 index 000000000..4b64476b1 --- /dev/null +++ b/dev/benchmarks/bench_css_oracle.R @@ -0,0 +1,96 @@ +# Fidelity oracle (advisor): CSS runs sector-restricted TBR against the FULL +# dataset (ts_sector.cpp css_search) -> EXACT scoring, no HTU approximation. +# If CSS made to search hard STILL can't match TNT from an identical T0, the HTU +# fidelity hypothesis is exonerated and the gap is STRUCTURAL (sector shape / +# selection). If exact-scoring CSS closes it where HTU-based RSS could not, +# fidelity is the culprit. +# +# Shared-start design (as bench_ras_verify.R). Arms from TNT's own T0: +# rss = HTU-based RSS, K passes (the gap baseline) +# css2 = exact CSS, K rounds, 2 big sectors +# css3 = exact CSS, K rounds, 3 sectors +# gap = TS_sect - TNT_sect; lower = closer. Also reports Mcand (did CSS search?). +# +# Env: TS_LIB (default .agent-ratchet), TNT_EXE, TS_DATASETS, TS_SEEDS, TS_KPASS, OUT_CSV. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +K <- as.integer(Sys.getenv("TS_KPASS", "8")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Zanol2014 Zhu2013 Giles2015")), "\\s+")[[1]] +out_csv <- Sys.getenv("OUT_CSV", "dev/benchmarks/sect_css_oracle.csv") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), "cssoracle"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +run_tnt <- function(phy, seed, kpass) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", kpass), "quit;") + writeLines(script, file.path(wd, "ss.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "ss.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + t0 <- tryCatch(ReadTntTree(file.path(wd, "t0.tre")), error = function(e) NULL) + if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + list(t0 = t0, s_sect = if (length(s_sect)) s_sect[length(s_sect)] else NA) +} +run_rss <- function(d, tree, rounds) { + set.seed(1); nt <- length(d) + smin <- as.integer(round(nt * 0.35)); smax <- as.integer(round(nt * 0.65)) + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, + wagnerStarts = 1L, fuseInterval = 9999L, sectorMinSize = smin, sectorMaxSize = smax, + rssRounds = as.integer(rounds))) + list(score = as.double(attr(r, "score")), cand = as.double(attr(r, "candidates_evaluated"))) +} +run_css <- function(d, tree, rounds, part) { + set.seed(1) + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, rssRounds = 0L, + cssRounds = as.integer(rounds), cssPartitions = as.integer(part), + wagnerStarts = 1L, fuseInterval = 9999L)) + list(score = as.double(attr(r, "score")), cand = as.double(attr(r, "candidates_evaluated"))) +} + +rows <- list() +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + for (sd in seeds) { + tn <- run_tnt(phy, sd, K) + if (is.null(tn$t0)) { cat(sprintf("WARN %s s%d: no T0\n", nm, sd)); next } + rss <- run_rss(phy, tn$t0, K) + css2 <- run_css(phy, tn$t0, K, 2L) + css3 <- run_css(phy, tn$t0, K, 3L) + rows[[length(rows) + 1]] <- data.frame(dataset = nm, seed = sd, tnt = tn$s_sect, + rss = rss$score, css2 = css2$score, css3 = css3$score, + g_rss = rss$score - tn$s_sect, g_css2 = css2$score - tn$s_sect, + g_css3 = css3$score - tn$s_sect, + Mc_rss = round(rss$cand / 1e6, 1), Mc_css2 = round(css2$cand / 1e6, 1), + stringsAsFactors = FALSE) + cat(sprintf("%-11s s%d | TNT=%.0f | rss=%.0f css2=%.0f css3=%.0f | gaps %+.0f/%+.0f/%+.0f | Mc rss=%.1f css2=%.1f\n", + nm, sd, tn$s_sect, rss$score, css2$score, css3$score, + rss$score - tn$s_sect, css2$score - tn$s_sect, css3$score - tn$s_sect, + rss$cand / 1e6, css2$cand / 1e6)) + } +} +S <- do.call(rbind, rows) +cat("\n== medians (gap = TS_sect - TNT_sect from identical T0; lower = closer) ==\n") +agg <- do.call(rbind, lapply(split(S, S$dataset), function(d) data.frame( + dataset = d$dataset[1], TNT = median(d$tnt), + g_rss = median(d$g_rss), g_css2 = median(d$g_css2), g_css3 = median(d$g_css3), + Mc_rss = median(d$Mc_rss), Mc_css2 = median(d$Mc_css2)))) +print(agg, row.names = FALSE) +dir.create(dirname(out_csv), showWarnings = FALSE, recursive = TRUE) +write.csv(S, out_csv, row.names = FALSE) +cat(sprintf("\nWrote %s\n", out_csv)) diff --git a/dev/benchmarks/bench_datasets.R b/dev/benchmarks/bench_datasets.R new file mode 100644 index 000000000..8b9cfa880 --- /dev/null +++ b/dev/benchmarks/bench_datasets.R @@ -0,0 +1,411 @@ +# Benchmark dataset loading and scoring utilities +# +# Usage: +# source("dev/benchmarks/bench_datasets.R") +# datasets <- load_benchmark_datasets() +# run_benchmark_suite(maxSeconds = 30, replicates = 5) + +library(TreeSearch) +library(TreeTools) + +# The 14 standard benchmark datasets (<=88 tips), ordered by tip count +BENCHMARK_NAMES <- c( + "Longrich2010", # 20 tips, 3 states, 45% missing + "Vinther2008", # 23 tips, 4 states, 21% missing + "Sansom2010", # 23 tips, 4 states, 40% missing + "DeAssis2011", # 33 tips, 3 states, 21% inapp + "Aria2015", # 35 tips, 6 states, 13% missing + "Wortley2006", # 37 tips, 8 states, 31% missing + "Griswold1999", # 43 tips, 6 states, 6% missing + "Schulze2007", # 52 tips, 3 states, 17% inapp + "Eklund2004", # 54 tips, 6 states, 30% missing + "Agnarsson2004", # 62 tips, 7 states, 6% missing + "Zanol2014", # 74 tips, 9 states, 17% inapp + "Zhu2013", # 75 tips, 4 states, 43% missing + "Giles2015", # 78 tips, 4 states, 42% missing + "Dikow2009" # 88 tips, 9 states, 0.4% missing +) + +# Large-tree benchmark datasets (>= 100 tips). +# Loaded from dev/benchmarks/ rather than inapplicable.phyData. +LARGE_BENCHMARK_NAMES <- c( + "mbank_X30754" # 180 tips, 425 chars, 40% missing, 20% inapplicable +) + +#' Prepare raw data for C++ bridge from a phyDat object +#' @param dataset A phyDat object +#' @return List with contrast, tip_data, weight, levels +prepare_ts_data <- function(dataset) { + at <- attributes(dataset) + list( + contrast = at$contrast, + tip_data = matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE), + weight = at$weight, + levels = at$levels, + n_taxa = length(dataset) + ) +} + +#' Load all 14 standard benchmark datasets +#' @return Named list of prepared datasets (ready for C++ bridge) +load_benchmark_datasets <- function() { + datasets <- list() + for (nm in BENCHMARK_NAMES) { + ds <- TreeSearch::inapplicable.phyData[[nm]] + if (is.null(ds)) { + warning("Dataset ", nm, " not found in inapplicable.phyData") + next + } + datasets[[nm]] <- prepare_ts_data(ds) + } + datasets +} + +#' Load large-tree benchmark datasets from dev/benchmarks/ +#' @return Named list of prepared datasets (ready for C++ bridge) +load_large_benchmark_datasets <- function() { + bench_dir <- "dev/benchmarks" + datasets <- list() + for (nm in LARGE_BENCHMARK_NAMES) { + nex_path <- file.path(bench_dir, paste0(nm, ".nex")) + if (!file.exists(nex_path)) { + warning("Large dataset file not found: ", nex_path) + next + } + phyDat <- TreeTools::ReadAsPhyDat(nex_path) + datasets[[nm]] <- prepare_ts_data(phyDat) + } + datasets +} + +#' Load all benchmark datasets (standard + large) +#' @return Named list of prepared datasets +load_all_benchmark_datasets <- function() { + c(load_benchmark_datasets(), load_large_benchmark_datasets()) +} + +#' Characterize a benchmark dataset +#' @param ds Prepared dataset (from prepare_ts_data) +#' @return Data frame with one row of characteristics +characterize_dataset <- function(ds) { + n_taxa <- ds$n_taxa + n_patterns <- length(ds$weight) + n_chars <- sum(ds$weight) + lvls <- ds$levels + contrast <- ds$contrast + n_states <- ncol(contrast) + inapp_idx <- which(lvls == "-") + n_app_states <- n_states - length(inapp_idx) + + td <- ds$tip_data + total_cells <- n_taxa * n_patterns + + n_inapp <- 0L + n_missing <- 0L + has_inapp <- length(inapp_idx) > 0 + for (i in seq_len(nrow(contrast))) { + is_inapp <- has_inapp && contrast[i, inapp_idx] > 0.5 + cols_check <- setdiff(seq_len(n_states), inapp_idx) + is_all <- all(contrast[i, cols_check] > 0.5) + count <- sum(td == i) + if (is_inapp && !is_all) n_inapp <- n_inapp + count + if (is_all) n_missing <- n_missing + count + } + + data.frame( + n_taxa = n_taxa, + n_chars = n_chars, + n_patterns = n_patterns, + pct_inapp = round(100 * n_inapp / total_cells, 1), + n_app_states = n_app_states, + pct_missing = round(100 * n_missing / total_cells, 1) + ) +} + +#' Run a single benchmark: driven search on one dataset +#' @param name Dataset name (from BENCHMARK_NAMES) +#' @param maxSeconds Timeout in seconds +#' @param maxReplicates Maximum replicates +#' @param seed RNG seed +#' @param datasets Pre-loaded datasets (optional) +#' @return List with score, replicates, time, etc. +score_dataset <- function(name, maxSeconds = 10, maxReplicates = 20L, + seed = 42L, datasets = NULL) { + if (is.null(datasets)) { + ds <- prepare_ts_data(TreeSearch::inapplicable.phyData[[name]]) + } else { + ds <- datasets[[name]] + } + if (is.null(ds)) stop("Dataset '", name, "' not found") + + set.seed(seed) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = maxReplicates, + targetHits = 5L, + ratchetCycles = 5L, + xssRounds = 1L, + xssPartitions = 3L, + fuseInterval = 5L, + maxSeconds = maxSeconds, + verbosity = 0L + ) + elapsed <- (proc.time() - t0)[3] + + list( + dataset = name, + n_taxa = ds$n_taxa, + best_score = result$best_score, + replicates = result$replicates, + pool_size = result$pool_size, + hits_to_best = result$hits_to_best, + timed_out = result$timed_out, + elapsed = elapsed + ) +} + +#' Run the full benchmark suite +#' @param maxSeconds Timeout per dataset +#' @param replicates Number of independent runs per dataset +#' @param seed Base seed (incremented per replicate) +#' @return Data frame with results +run_benchmark_suite <- function(maxSeconds = 30, replicates = 3L, + seed = 42L) { + datasets <- load_benchmark_datasets() + results <- list() + + for (nm in names(datasets)) { + for (rep in seq_len(replicates)) { + cat(sprintf("[%s] rep %d/%d (timeout=%ds)...", + nm, rep, replicates, maxSeconds)) + res <- score_dataset(nm, maxSeconds = maxSeconds, + seed = seed + rep - 1L, + datasets = datasets) + cat(sprintf(" score=%.0f reps=%d time=%.1fs\n", + res$best_score, res$replicates, res$elapsed)) + res$replicate <- rep + results <- c(results, list(as.data.frame(res))) + } + } + + do.call(rbind, results) +} + +#' Summarize benchmark results +#' @param results Data frame from run_benchmark_suite +#' @return Summary data frame (best score, median time, etc.) +summarize_benchmark <- function(results) { + datasets <- unique(results$dataset) + summaries <- list() + + for (nm in datasets) { + sub <- results[results$dataset == nm, ] + summaries <- c(summaries, list(data.frame( + dataset = nm, + n_taxa = sub$n_taxa[1], + best_score = min(sub$best_score), + median_score = median(sub$best_score), + median_time = round(median(sub$elapsed), 2), + median_reps = median(sub$replicates), + stringsAsFactors = FALSE + ))) + } + + do.call(rbind, summaries) +} + +# =========================================================================== +# MorphoBank external benchmark datasets (neotrans corpus) +# =========================================================================== + +# Hard-coded path to the neotrans matrices directory. +# The neotrans repo is a sibling of the TreeSearch source tree under GitHub/. +# This is a git submodule, so the path is stable. +NEOTRANS_MATRICES_DIR <- local({ + # Try from TreeSearch source root (getwd() == TreeSearch-a/) + candidates <- c( + file.path(getwd(), "..", "neotrans", "inst", "matrices"), + # From dev/benchmarks/ (when sourcing directly) + file.path(getwd(), "..", "..", "neotrans", "inst", "matrices") + ) + for (d in candidates) { + d_norm <- normalizePath(d, mustWork = FALSE) + if (dir.exists(d_norm)) return(d_norm) + } + # Return the most likely path even if it doesn't exist yet + normalizePath(candidates[1], mustWork = FALSE) +}) + +# Minimum taxon count for benchmarking. Matrices below this size are +# trivially solved in milliseconds and contribute no useful signal. +MBANK_MIN_NTAX <- 20L + +# Fixed 25-matrix training sample, selected for diversity across size tiers. +# Chosen via max-min distance on standardized (ntax, nchar, pct_missing, +# pct_inapp) within each tier: 7 small, 7 medium, 7 large, 4 xlarge. +# Do not modify: results are only comparable when the same sample is used. +MBANK_FIXED_SAMPLE <- c( + # Small (20-30 taxa) + "project532", "project2346", "project2451", "project4501", + "project944", "project971_(1)", "project2762", + # Medium (31-60 taxa) + "project826", "project561", "project571", "project4146_(3)", + "project3688", "project4049", "project423", + # Large (61-120 taxa) + "project4286", "project4359", "project4397", "project2084_(1)", + "project2771", "project2184", "project3938", + # XLarge (121+ taxa) + "syab07201", "project4133", "project804", "project4284" +) + +#' Load the MorphoBank matrix catalogue +#' +#' Reads the pre-built catalogue CSV from dev/benchmarks/mbank_catalogue.csv. +#' Filters to usable matrices (parse_ok, ntax >= MBANK_MIN_NTAX) and +#' optionally excludes redundant multi-matrix duplicates. +#' +#' @param include_redundant If FALSE (default), exclude rows flagged +#' as redundant in the catalogue. +#' @return Data frame with one row per matrix. +load_mbank_catalogue <- function(include_redundant = FALSE) { + # Find the catalogue CSV + cat_candidates <- c( + file.path(getwd(), "dev", "benchmarks", "mbank_catalogue.csv"), + file.path(getwd(), "mbank_catalogue.csv") + ) + cat_path <- NULL + for (p in cat_candidates) { + if (file.exists(p)) { cat_path <- p; break } + } + if (is.null(cat_path)) { + stop("mbank_catalogue.csv not found. Run build_mbank_catalogue.R first.") + } + + cat <- read.csv(cat_path, stringsAsFactors = FALSE) + + # Filter to usable matrices + cat <- cat[cat$parse_ok & !is.na(cat$ntax) & cat$ntax >= MBANK_MIN_NTAX, ] + + # Exclude redundant multi-matrix duplicates (if column exists) + if (!include_redundant && "dedup_drop" %in% names(cat)) { + cat <- cat[!cat$dedup_drop, ] + } + + # Add tier classification + cat$tier <- cut(cat$ntax, + breaks = c(0, 30, 60, 120, Inf), + labels = c("small", "medium", "large", "xlarge")) + + rownames(cat) <- cat$key + cat +} + +#' Load MorphoBank datasets by key +#' +#' Reads .nex files from the neotrans matrices directory and prepares them +#' for the C++ bridge. +#' +#' @param catalogue Data frame from load_mbank_catalogue(). +#' @param keys Character vector of matrix keys to load. +#' @param verbose If TRUE, print progress. +#' @return Named list of prepared datasets. +load_mbank_datasets <- function(catalogue, keys, verbose = TRUE) { + if (!dir.exists(NEOTRANS_MATRICES_DIR)) { + stop("Neotrans matrices directory not found: ", NEOTRANS_MATRICES_DIR, + "\nIs the neotrans repo checked out?") + } + + datasets <- list() + for (k in keys) { + if (!k %in% catalogue$key) { + warning("Key '", k, "' not in catalogue; skipping.") + next + } + row <- catalogue[catalogue$key == k, ] + nex_path <- file.path(NEOTRANS_MATRICES_DIR, row$filename) + if (!file.exists(nex_path)) { + warning("File not found: ", nex_path, "; skipping.") + next + } + if (verbose) { + cat(sprintf(" Loading %s (%d taxa, %d chars)...\n", + k, row$ntax, row$nchar)) + } + tryCatch({ + pd <- suppressWarnings(TreeTools::ReadAsPhyDat(nex_path)) + datasets[[k]] <- prepare_ts_data(pd) + }, error = function(e) { + warning("Failed to load ", k, ": ", conditionMessage(e)) + }) + } + datasets +} + +#' Load a stratified sample of MorphoBank datasets +#' +#' Draws a reproducible stratified sample from the training or validation +#' split, with equal representation from each size tier. +#' +#' @param catalogue Data frame from load_mbank_catalogue(). +#' @param n Total number of matrices to sample (approximately). +#' @param seed RNG seed for reproducibility. +#' @param split "training" (default) or "validation". +#' @param tier Optional: restrict to a specific tier ("small", "medium", +#' "large", "xlarge"). +#' @param verbose If TRUE, print summary of what was loaded. +#' @return Named list of prepared datasets. +load_mbank_sample <- function(catalogue, n = 25L, seed = 7193L, + split = "training", tier = NULL, + verbose = TRUE) { + pool <- catalogue[catalogue$split == split, ] + if (!is.null(tier)) { + pool <- pool[pool$tier == tier, ] + } + if (nrow(pool) == 0) { + stop("No matrices in the ", split, " split", + if (!is.null(tier)) paste0(" (tier: ", tier, ")") else "") + } + + # Stratified sampling: allocate n proportionally across tiers + tier_counts <- table(pool$tier) + tier_counts <- tier_counts[tier_counts > 0] + n_per_tier <- round(n * tier_counts / sum(tier_counts)) + # Ensure at least 1 per tier if tier has matrices + n_per_tier <- pmax(n_per_tier, 1L) + + set.seed(seed) + selected <- character(0) + for (t in names(n_per_tier)) { + tier_pool <- pool[pool$tier == t, ] + k <- min(n_per_tier[t], nrow(tier_pool)) + selected <- c(selected, sample(tier_pool$key, k)) + } + + if (verbose) { + cat(sprintf("MorphoBank %s sample: %d matrices from %d tiers\n", + split, length(selected), length(n_per_tier))) + for (t in names(n_per_tier)) { + cat(sprintf(" %s: %d selected (of %d available)\n", + t, sum(pool$tier[pool$key %in% selected] == t), + sum(pool$tier == t))) + } + } + + load_mbank_datasets(catalogue, selected, verbose = verbose) +} + +#' Load all MorphoBank datasets for a given split +#' +#' @param catalogue Data frame from load_mbank_catalogue(). +#' @param split "training" or "validation". +#' @param verbose If TRUE, print progress. +#' @return Named list of prepared datasets. +load_mbank_split <- function(catalogue, split = "training", verbose = TRUE) { + pool <- catalogue[catalogue$split == split, ] + if (verbose) { + cat(sprintf("Loading all %d %s matrices...\n", nrow(pool), split)) + } + load_mbank_datasets(catalogue, pool$key, verbose = verbose) +} diff --git a/dev/benchmarks/bench_drift_mpt.R b/dev/benchmarks/bench_drift_mpt.R new file mode 100644 index 000000000..0aaddf307 --- /dev/null +++ b/dev/benchmarks/bench_drift_mpt.R @@ -0,0 +1,141 @@ +#!/usr/bin/env Rscript +# T-254: Drift MPT diversity experiment +# +# Compare pool size, MPT count, and topological diversity between +# driftCycles=0 and driftCycles=2 on the three gap datasets from T-251. +# +# Usage: +# Rscript dev/benchmarks/bench_drift_mpt.R + +library(TreeSearch, lib.loc = ".agent-E") +library(TreeTools) +library(TreeDist) + +DATASETS <- c("Wortley2006", "Zhu2013", "Geisler2001") +DRIFT_CONDITIONS <- c(0L, 2L) +SEEDS <- 1:3 +BUDGETS <- c(30, 120) + +# Use default preset parameters for everything except driftCycles. +# strategy = "none" bypasses auto-selection; explicit control overrides. +make_control <- function(drift_cycles) { + SearchControl( + tbrMaxHits = 1L, + nniFirst = TRUE, + sprFirst = FALSE, + tabuSize = 100L, + wagnerStarts = 3L, + outerCycles = 1L, + maxOuterResets = 2L, + ratchetCycles = 12L, + ratchetPerturbProb = 0.25, + ratchetPerturbMode = 0L, + ratchetPerturbMaxMoves = 5L, + adaptiveLevel = TRUE, + driftCycles = drift_cycles, + driftAfdLimit = 5L, + driftRfdLimit = 0.15, + xssRounds = 3L, + xssPartitions = 4L, + rssRounds = 1L, + cssRounds = 0L, + consensusStableReps = 3L, + fuseInterval = 3L, + fuseAcceptEqual = FALSE, + poolMaxSize = 100L, + enumTimeFraction = 0.1 + ) +} + +# Compute pairwise RF distances between trees, return summary stats +tree_diversity <- function(trees) { + n <- length(trees) + if (n < 2) return(list(mean_rf = NA, median_rf = NA, min_rf = NA, max_rf = NA)) + rf_mat <- as.matrix(RobinsonFoulds(trees)) + # Upper triangle only (exclude diagonal) + rf_vals <- rf_mat[upper.tri(rf_mat)] + list( + mean_rf = mean(rf_vals), + median_rf = median(rf_vals), + min_rf = min(rf_vals), + max_rf = max(rf_vals) + ) +} + +results <- list() +row_i <- 0L + +for (ds_name in DATASETS) { + ds <- inapplicable.phyData[[ds_name]] + n_tips <- length(ds) + cat(sprintf("\n=== %s (%d tips) ===\n", ds_name, n_tips)) + + for (budget in BUDGETS) { + for (drift in DRIFT_CONDITIONS) { + ctrl <- make_control(drift) + for (seed in SEEDS) { + row_i <- row_i + 1L + cat(sprintf(" budget=%ds drift=%d seed=%d ... ", budget, drift, seed)) + t0 <- proc.time() + + res <- MaximizeParsimony( + ds, + maxSeconds = budget, + strategy = "none", + control = ctrl, + verbosity = 0L, + nThread = 1L + ) + + wall_s <- as.double((proc.time() - t0)[3]) + best_score <- attr(res, "score") + n_trees <- length(res) + n_topo <- attr(res, "n_topologies") + n_reps <- attr(res, "replicates") + timings <- attr(res, "timings") + + # Topological diversity (RF distances) + div <- tree_diversity(res) + + cat(sprintf("score=%.0f trees=%d topo=%d reps=%d (%.1fs)\n", + best_score, n_trees, n_topo, n_reps, wall_s)) + + results[[row_i]] <- data.frame( + dataset = ds_name, + n_tips = n_tips, + budget_s = budget, + drift_cycles = drift, + seed = seed, + best_score = best_score, + n_trees = n_trees, + n_topologies = n_topo, + replicates = n_reps, + wall_s = round(wall_s, 2), + drift_ms = timings["drift_ms"], + total_ms = sum(timings), + drift_pct = round(100 * timings["drift_ms"] / sum(timings), 1), + mean_rf = div$mean_rf, + median_rf = div$median_rf, + min_rf = div$min_rf, + max_rf = div$max_rf, + stringsAsFactors = FALSE + ) + } + } + } +} + +df <- do.call(rbind, results) +rownames(df) <- NULL + +out_path <- "dev/benchmarks/results_drift_mpt.csv" +write.csv(df, out_path, row.names = FALSE) +cat(sprintf("\nResults written to %s\n", out_path)) + +# Quick summary table +cat("\n=== Summary by dataset × budget × drift ===\n") +agg <- aggregate( + cbind(best_score, n_trees, n_topologies, replicates, mean_rf) ~ dataset + budget_s + drift_cycles, + data = df, FUN = median +) +print(agg[order(agg$dataset, agg$budget_s, agg$drift_cycles), ]) diff --git a/dev/benchmarks/bench_framework.R b/dev/benchmarks/bench_framework.R new file mode 100644 index 000000000..23cba54b8 --- /dev/null +++ b/dev/benchmarks/bench_framework.R @@ -0,0 +1,597 @@ +# Phase 6D: Benchmarking framework +# +# Runs dataset x strategy x N replicates and records: +# - Best score found +# - Total wall-clock time +# - Time to best score (via progress callback) +# - Number of replicates to convergence +# - Per-phase timing breakdown +# +# When comparing strategies with DIFFERENT per-replicate cost (e.g. +# NNI→TBR vs TBR), use time-adjusted expected best — the expected +# minimum from k = budget / time_per_rep draws — not median score. +# See .positai/expertise/profiling.md for implementation and rationale. +# Median is fine when comparing parameter changes on a fixed pipeline +# (same time-per-rep). +# +# Usage: +# source("dev/benchmarks/bench_framework.R") +# results <- run_benchmark_grid() +# summary <- summarize_grid(results) + +library(TreeSearch) +library(TreeTools) + +source("dev/benchmarks/bench_datasets.R") + +# ---- Strategy presets (formalized from strategies.md, T-003) ---- + +STRATEGY_NAMES <- c("sprint", "default", "thorough", + "ratchet_heavy", "sectorial_heavy", "drift_heavy") +# Large-tree strategies (for use with LARGE_BENCHMARK_NAMES, >= 120 tips) +LARGE_STRATEGY_NAMES <- c("large", "thorough") + +get_strategy <- function(name = STRATEGY_NAMES) { + name <- match.arg(name) + strategies <- list( + sprint = list( + wagnerStarts = 1L, tbrMaxHits = 1L, tabuSize = 0L, + ratchetCycles = 3L, ratchetPerturbProb = 0.04, + ratchetPerturbMode = 0L, ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = FALSE, + driftCycles = 0L, driftAfdLimit = 3L, driftRfdLimit = 0.1, + xssRounds = 1L, xssPartitions = 4L, rssRounds = 0L, + cssRounds = 0L, cssPartitions = 4L, + sectorMinSize = 6L, sectorMaxSize = 50L, + fuseInterval = 5L, fuseAcceptEqual = FALSE + ), + default = list( + wagnerStarts = 3L, tbrMaxHits = 1L, tabuSize = 100L, + ratchetCycles = 12L, ratchetPerturbProb = 0.25, + ratchetPerturbMode = 0L, ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = FALSE, + driftCycles = 2L, driftAfdLimit = 5L, driftRfdLimit = 0.15, + xssRounds = 3L, xssPartitions = 4L, rssRounds = 1L, + cssRounds = 0L, cssPartitions = 4L, + sectorMinSize = 6L, sectorMaxSize = 50L, + fuseInterval = 3L, fuseAcceptEqual = FALSE, + sprFirst = TRUE, adaptiveLevel = TRUE, consensusStableReps = 3L + ), + thorough = list( + wagnerStarts = 3L, tbrMaxHits = 3L, tabuSize = 200L, + ratchetCycles = 20L, ratchetPerturbProb = 0.25, + ratchetPerturbMode = 2L, ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = TRUE, + driftCycles = 12L, driftAfdLimit = 5L, driftRfdLimit = 0.15, + xssRounds = 5L, xssPartitions = 6L, rssRounds = 3L, + cssRounds = 2L, cssPartitions = 6L, + sectorMinSize = 6L, sectorMaxSize = 80L, + fuseInterval = 2L, fuseAcceptEqual = TRUE + ), + ratchet_heavy = list( + wagnerStarts = 1L, tbrMaxHits = 1L, tabuSize = 100L, + ratchetCycles = 30L, ratchetPerturbProb = 0.30, + ratchetPerturbMode = 2L, ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = TRUE, + driftCycles = 2L, driftAfdLimit = 3L, driftRfdLimit = 0.1, + xssRounds = 1L, xssPartitions = 4L, rssRounds = 0L, + cssRounds = 0L, cssPartitions = 4L, + sectorMinSize = 6L, sectorMaxSize = 50L, + fuseInterval = 3L, fuseAcceptEqual = FALSE + ), + sectorial_heavy = list( + wagnerStarts = 1L, tbrMaxHits = 1L, tabuSize = 100L, + ratchetCycles = 5L, ratchetPerturbProb = 0.04, + ratchetPerturbMode = 0L, ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = FALSE, + driftCycles = 3L, driftAfdLimit = 3L, driftRfdLimit = 0.1, + xssRounds = 8L, xssPartitions = 6L, rssRounds = 4L, + cssRounds = 3L, cssPartitions = 6L, + sectorMinSize = 6L, sectorMaxSize = 80L, + fuseInterval = 2L, fuseAcceptEqual = TRUE + ), + drift_heavy = list( + wagnerStarts = 1L, tbrMaxHits = 1L, tabuSize = 100L, + ratchetCycles = 5L, ratchetPerturbProb = 0.04, + ratchetPerturbMode = 0L, ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = FALSE, + driftCycles = 20L, driftAfdLimit = 5L, driftRfdLimit = 0.2, + xssRounds = 2L, xssPartitions = 4L, rssRounds = 1L, + cssRounds = 0L, cssPartitions = 4L, + sectorMinSize = 6L, sectorMaxSize = 50L, + fuseInterval = 3L, fuseAcceptEqual = TRUE + ), + # Large-tree preset (>=120 tips): thorough + wagnerBias + larger sectors. + large = list( + wagnerStarts = 3L, tbrMaxHits = 3L, tabuSize = 200L, + ratchetCycles = 20L, ratchetPerturbProb = 0.25, + ratchetPerturbMode = 2L, ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = TRUE, + nniPerturbCycles = 5L, nniPerturbFraction = 0.5, + driftCycles = 12L, driftAfdLimit = 5L, driftRfdLimit = 0.15, + xssRounds = 5L, xssPartitions = 6L, rssRounds = 3L, + cssRounds = 2L, cssPartitions = 6L, + sectorMinSize = 8L, sectorMaxSize = 100L, + fuseInterval = 3L, fuseAcceptEqual = TRUE, + wagnerBias = 1L, wagnerBiasTemp = 0.3, + nniFirst = TRUE, sprFirst = FALSE, + outerCycles = 2L, consensusStableReps = 2L + ) + ) + strategies[[name]] +} + +# ---- Best-known EW scores (from datasets.md, T-002) ---- + +BEST_KNOWN_EW <- c( + Longrich2010 = 131, Vinther2008 = 79, Sansom2010 = 189, + DeAssis2011 = 64, Aria2015 = 145, Wortley2006 = 496, + Griswold1999 = 409, Schulze2007 = 167, Eklund2004 = 445, + Agnarsson2004 = 778, Zanol2014 = 1338, Zhu2013 = 649, + Giles2015 = 720, Dikow2009 = 1614 +) + +# Large-tree best-known EW scores. +# NA = not yet established; fill in after benchmarking. +BEST_KNOWN_LARGE_EW <- c( + mbank_X30754 = NA_real_ # 180 tips, 425 chars +) + +# ---- Core benchmark function ---- + +#' Run one driven search and record performance metrics. +#' +#' Calls ts_driven_search directly with the given strategy parameters. +#' Uses a progress callback to record the wall-clock time at which the +#' best score was first found ("time to best"). +#' +#' @param ds Prepared dataset (from prepare_ts_data). +#' @param strategy Named list of strategy parameters (from get_strategy). +#' @param maxReplicates Hard replicate cap. +#' @param targetHits Convergence criterion (hits to best score). +#' @param maxSeconds Wall-clock timeout (0 = no timeout). +#' @param seed RNG seed. +#' @return Named list with score, timing, and convergence metrics. +benchmark_run <- function(ds, strategy, + maxReplicates = 100L, + targetHits = NULL, + maxSeconds = 0, + seed = 42L) { + if (is.null(targetHits)) { + targetHits <- max(10L, ds$n_taxa %/% 5L) + } + + # Progress-callback state: track when best score first appeared + cb_env <- new.env(parent = emptyenv()) + cb_env$best <- Inf + cb_env$time_to_best <- NA_real_ + cb_env$trace <- list() + + progress_cb <- function(info) { + if (is.finite(info$best_score) && info$best_score < cb_env$best) { + cb_env$best <- info$best_score + cb_env$time_to_best <- info$elapsed + } + cb_env$trace[[length(cb_env$trace) + 1L]] <- list( + replicate = info$replicate, + elapsed = info$elapsed, + best_score = info$best_score, + hits = info$hits_to_best, + phase = info$phase + ) + } + + # Build structured args for ts_driven_search (new interface: three config lists). + # verbosity >= 1 required for the C++ engine to invoke the callback. + searchControl <- do.call(TreeSearch::SearchControl, strategy) + runtimeConfig <- list( + maxReplicates = as.integer(maxReplicates), + targetHits = as.integer(targetHits), + maxSeconds = as.double(maxSeconds), + verbosity = 1L, + nThreads = 1L, + startEdge = NULL, + progressCallback = progress_cb + ) + scoringConfig <- list( + concavity = -1.0, # sentinel for Inf (equal weights) + xpiwe = FALSE, + xpiwe_r = 0.0, + xpiwe_max_f = 1.0 + ) + + set.seed(seed) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + searchControl, runtimeConfig, scoringConfig + ) + wall_s <- as.double((proc.time() - t0)[3]) + + list( + best_score = result$best_score, + replicates = result$replicates, + hits_to_best = result$hits_to_best, + pool_size = result$pool_size, + timed_out = result$timed_out, + wall_s = wall_s, + time_to_best_s = cb_env$time_to_best, + timings = result$timings, + trace = cb_env$trace + ) +} + +# ---- Grid runner ---- + +#' Run the full dataset x strategy x replicate benchmark grid. +#' +#' @param dataset_names Character vector of dataset names. +#' @param strategy_names Character vector of strategy preset names. +#' @param replicates Number of independent runs per combination. +#' @param maxReplicates Replicate cap per run. +#' @param targetHits Convergence hits (NULL = auto). +#' @param maxSeconds Timeout per run (0 = no timeout). +#' @param base_seed Seed for first replicate; incremented per replicate. +#' @param datasets Pre-loaded named list of prepared datasets. If NULL +#' (default), loads all standard + large benchmark datasets. +#' @return A data.frame with one row per dataset x strategy x replicate. +run_benchmark_grid <- function( + dataset_names = BENCHMARK_NAMES, + strategy_names = STRATEGY_NAMES, + replicates = 5L, + maxReplicates = 100L, + targetHits = NULL, + maxSeconds = 30, + base_seed = 42L, + datasets = NULL +) { + if (is.null(datasets)) datasets <- load_all_benchmark_datasets() + n_combos <- length(dataset_names) * length(strategy_names) * replicates + cat(sprintf("Benchmark grid: %d datasets x %d strategies x %d reps = %d runs\n", + length(dataset_names), length(strategy_names), replicates, n_combos)) + + rows <- vector("list", n_combos) + idx <- 0L + + for (ds_name in dataset_names) { + ds <- datasets[[ds_name]] + if (is.null(ds)) { + warning("Skipping missing dataset: ", ds_name) + next + } + for (strat_name in strategy_names) { + strat <- get_strategy(strat_name) + for (rep in seq_len(replicates)) { + idx <- idx + 1L + seed <- base_seed + rep - 1L + + cat(sprintf("[%3d/%d] %s x %s rep %d ...", + idx, n_combos, ds_name, strat_name, rep)) + + res <- tryCatch( + benchmark_run(ds, strat, + maxReplicates = maxReplicates, + targetHits = targetHits, + maxSeconds = maxSeconds, + seed = seed), + error = function(e) { + cat(sprintf(" ERROR: %s\n", conditionMessage(e))) + NULL + } + ) + + if (is.null(res)) { + rows[[idx]] <- data.frame( + dataset = ds_name, strategy = strat_name, replicate = rep, + seed = seed, n_taxa = ds$n_taxa, + best_score = NA_real_, replicates = NA_integer_, + hits_to_best = NA_integer_, pool_size = NA_integer_, + timed_out = NA, wall_s = NA_real_, + time_to_best_s = NA_real_, + wagner_ms = NA_real_, tbr_ms = NA_real_, + xss_ms = NA_real_, rss_ms = NA_real_, css_ms = NA_real_, + ratchet_ms = NA_real_, drift_ms = NA_real_, + final_tbr_ms = NA_real_, fuse_ms = NA_real_, + stringsAsFactors = FALSE + ) + next + } + + cat(sprintf(" score=%.0f wall=%.1fs ttb=%.1fs reps=%d\n", + res$best_score, res$wall_s, + if (is.na(res$time_to_best_s)) -1 else res$time_to_best_s, + res$replicates)) + + rows[[idx]] <- data.frame( + dataset = ds_name, + strategy = strat_name, + replicate = rep, + seed = seed, + n_taxa = ds$n_taxa, + best_score = res$best_score, + replicates = res$replicates, + hits_to_best = res$hits_to_best, + pool_size = res$pool_size, + timed_out = res$timed_out, + wall_s = res$wall_s, + time_to_best_s = res$time_to_best_s, + wagner_ms = res$timings[["wagner_ms"]], + tbr_ms = res$timings[["tbr_ms"]], + xss_ms = res$timings[["xss_ms"]], + rss_ms = res$timings[["rss_ms"]], + css_ms = res$timings[["css_ms"]], + ratchet_ms = res$timings[["ratchet_ms"]], + drift_ms = res$timings[["drift_ms"]], + final_tbr_ms = res$timings[["final_tbr_ms"]], + fuse_ms = res$timings[["fuse_ms"]], + stringsAsFactors = FALSE + ) + } + } + } + + do.call(rbind, rows[seq_len(idx)]) +} + +# ---- Summarization ---- + +#' Summarize benchmark grid results per dataset x strategy. +#' +#' Computes: best score, median score, convergence rate (fraction that +#' hit targetHits before timeout), median wall time, median time-to-best, +#' and per-phase time medians. +#' +#' @param results Data frame from run_benchmark_grid. +#' @param best_known Named numeric vector of best-known EW scores. +#' @return Data frame with one row per dataset x strategy. +summarize_grid <- function(results, + best_known = c(BEST_KNOWN_EW, BEST_KNOWN_LARGE_EW)) { + combos <- unique(results[, c("dataset", "strategy")]) + out <- vector("list", nrow(combos)) + + for (i in seq_len(nrow(combos))) { + ds_name <- combos$dataset[i] + st_name <- combos$strategy[i] + sub <- results[results$dataset == ds_name & results$strategy == st_name, ] + sub <- sub[!is.na(sub$best_score), , drop = FALSE] + + if (nrow(sub) == 0) next + + bk <- if (ds_name %in% names(best_known)) best_known[[ds_name]] else NA_real_ + + # How many runs found the best-known score? + found_optimal <- if (is.na(bk)) NA_real_ else mean(sub$best_score <= bk) + + total_phase_ms <- sub$wagner_ms + sub$tbr_ms + sub$xss_ms + sub$rss_ms + + sub$css_ms + sub$ratchet_ms + sub$drift_ms + sub$final_tbr_ms + + sub$fuse_ms + + out[[i]] <- data.frame( + dataset = ds_name, + strategy = st_name, + n_taxa = sub$n_taxa[1], + n_runs = nrow(sub), + best_score = min(sub$best_score), + median_score = median(sub$best_score), + best_known = if (is.na(bk)) NA_real_ else bk, + pct_found_optimal = round(100 * found_optimal, 1), + converge_rate = round(100 * mean(!sub$timed_out), 1), + median_wall_s = round(median(sub$wall_s), 3), + median_ttb_s = round(median(sub$time_to_best_s, na.rm = TRUE), 3), + median_reps = median(sub$replicates), + median_hits = median(sub$hits_to_best), + # Phase fraction (median % of total C++ time) + pct_wagner = round(100 * median(sub$wagner_ms / total_phase_ms, + na.rm = TRUE), 1), + pct_tbr = round(100 * median(sub$tbr_ms / total_phase_ms, + na.rm = TRUE), 1), + pct_xss = round(100 * median(sub$xss_ms / total_phase_ms, + na.rm = TRUE), 1), + pct_rss = round(100 * median(sub$rss_ms / total_phase_ms, + na.rm = TRUE), 1), + pct_css = round(100 * median(sub$css_ms / total_phase_ms, + na.rm = TRUE), 1), + pct_ratchet = round(100 * median(sub$ratchet_ms / total_phase_ms, + na.rm = TRUE), 1), + pct_drift = round(100 * median(sub$drift_ms / total_phase_ms, + na.rm = TRUE), 1), + pct_fuse = round(100 * median(sub$fuse_ms / total_phase_ms, + na.rm = TRUE), 1), + stringsAsFactors = FALSE + ) + } + + do.call(rbind, out[!vapply(out, is.null, logical(1))]) +} + +# ---- Persistence helpers ---- + +#' Save benchmark results to CSV. +save_results <- function(results, + file = sprintf("dev/benchmarks/results_%s.csv", + format(Sys.time(), "%Y%m%d_%H%M"))) { + write.csv(results, file, row.names = FALSE) + cat("Results saved to", file, "\n") + invisible(file) +} + +#' Load benchmark results from CSV. +load_results <- function(file) { + read.csv(file, stringsAsFactors = FALSE) +} + +# ---- Quick-start convenience wrappers ---- + +#' Run a small smoke test: 2 datasets x 2 strategies x 2 reps, 5s timeout. +benchmark_smoke <- function() { + run_benchmark_grid( + dataset_names = c("Vinther2008", "Agnarsson2004"), + strategy_names = c("sprint", "default"), + replicates = 2L, + maxReplicates = 20L, + maxSeconds = 5, + base_seed = 42L + ) +} + +#' Run the full production benchmark (all 14 datasets x 6 strategies). +#' +#' Warning: this takes a long time. At 30s timeout per run with 5 reps: +#' 14 x 6 x 5 = 420 runs x 30s = ~3.5 hours worst case. +benchmark_full <- function(maxSeconds = 30, replicates = 5L) { + run_benchmark_grid( + maxReplicates = 100L, + maxSeconds = maxSeconds, + replicates = replicates, + base_seed = 42L + ) +} + +#' Run benchmark grid on large-tree datasets. +#' +#' Uses longer timeouts and fewer replicates than the standard benchmark, +#' since each replicate at 180+ tips takes minutes rather than seconds. +#' +#' @param strategy_names Strategies to test (default: "default" and "thorough"). +#' @param replicates Independent runs per combination. +#' @param maxReplicates Replicate cap per search (low: most info comes from +#' a single replicate at this scale). +#' @param maxSeconds Timeout per run (default 120s). +#' @param base_seed RNG seed. +#' @return Data frame matching run_benchmark_grid output format. +benchmark_large <- function( + strategy_names = c("default", "thorough"), + replicates = 3L, + maxReplicates = 10L, + maxSeconds = 120, + base_seed = 42L +) { + large_ds <- load_large_benchmark_datasets() + if (length(large_ds) == 0L) stop("No large benchmark datasets found") + run_benchmark_grid( + dataset_names = names(large_ds), + strategy_names = strategy_names, + replicates = replicates, + maxReplicates = maxReplicates, + targetHits = 3L, + maxSeconds = maxSeconds, + base_seed = base_seed + ) +} + +# =========================================================================== +# MorphoBank external benchmark suite +# =========================================================================== +# +# Uses the neotrans MorphoBank corpus (~700 matrices) with a deterministic +# train/validation split: project numbers divisible by 5 are validation. +# See .positai/plans/2026-03-24-0551-*.md for rationale. +# +# IMPORTANT: Validation results must NEVER be used to guide strategy tuning. +# They are a one-way check to confirm that improvements generalize. + +#' Run the MorphoBank fixed training sample benchmark. +#' +#' Runs the fixed 25-matrix training sample (MBANK_FIXED_SAMPLE) through +#' the benchmark grid. Use custom keys to override the fixed sample. +#' +#' @param keys Character vector of matrix keys (default: MBANK_FIXED_SAMPLE). +#' @param strategy_names Strategies to test. +#' @param replicates Independent runs per combination. +#' @param maxSeconds Timeout per run. +#' @param base_seed Base RNG seed. +#' @return Data frame matching run_benchmark_grid output format, with +#' an additional `source` column. +benchmark_mbank_sample <- function( + keys = MBANK_FIXED_SAMPLE, + strategy_names = c("default"), + replicates = 3L, + maxSeconds = 10, + base_seed = 42L +) { + cat_df <- load_mbank_catalogue() + datasets <- load_mbank_datasets(cat_df, keys = keys) + if (length(datasets) == 0L) stop("No MorphoBank training datasets loaded") + + results <- run_benchmark_grid( + dataset_names = names(datasets), + strategy_names = strategy_names, + replicates = replicates, + maxReplicates = 50L, + maxSeconds = maxSeconds, + base_seed = base_seed, + datasets = datasets + ) + results$source <- "mbank_train" + results +} + +#' Run benchmark on all MorphoBank matrices in a given split. +#' +#' WARNING: Running all ~550 training matrices takes a very long time. +#' Use benchmark_mbank_sample() for routine work. +#' +#' @param split "training" or "validation". +#' @param strategy_names Strategies to test. +#' @param replicates Independent runs per combination. +#' @param maxSeconds Timeout per run. +#' @param base_seed Base RNG seed. +#' @return Data frame matching run_benchmark_grid output format. +benchmark_mbank_sweep <- function( + split = "training", + strategy_names = c("default"), + replicates = 1L, + maxSeconds = 10, + base_seed = 42L +) { + cat_df <- load_mbank_catalogue() + datasets <- load_mbank_split(cat_df, split = split) + if (length(datasets) == 0L) { + stop("No MorphoBank ", split, " datasets loaded") + } + + results <- run_benchmark_grid( + dataset_names = names(datasets), + strategy_names = strategy_names, + replicates = replicates, + maxReplicates = 50L, + maxSeconds = maxSeconds, + base_seed = base_seed, + datasets = datasets + ) + results$source <- paste0("mbank_", split) + results +} + +#' Run the MorphoBank VALIDATION benchmark. +#' +#' This is a ONE-WAY DOOR: validation results confirm that strategy +#' improvements generalize, but must not be used to guide further tuning. +#' A prominent warning is printed. +#' +#' @param strategy_names Strategies to test. +#' @param replicates Independent runs per combination. +#' @param maxSeconds Timeout per run. +#' @param base_seed Base RNG seed. +#' @return Data frame matching run_benchmark_grid output format. +benchmark_mbank_validation <- function( + strategy_names = c("default"), + replicates = 1L, + maxSeconds = 10, + base_seed = 42L +) { + message(paste(rep("=", 70), collapse = "")) + message(" VALIDATION DATA") + message(" Do NOT use these results to guide strategy tuning.") + message(" This is a one-way check to confirm generalization.") + message(paste(rep("=", 70), collapse = "")) + Sys.sleep(2) + + benchmark_mbank_sweep( + split = "validation", + strategy_names = strategy_names, + replicates = replicates, + maxSeconds = maxSeconds, + base_seed = base_seed + ) +} diff --git a/dev/benchmarks/bench_grid_run.R b/dev/benchmarks/bench_grid_run.R new file mode 100644 index 000000000..560fbf300 --- /dev/null +++ b/dev/benchmarks/bench_grid_run.R @@ -0,0 +1,152 @@ +# Focused benchmark grid: no callback (workaround for segfault in progress_cb). +# Collects per-phase timings, wall-clock time, scores, convergence stats. + +library(TreeSearch, lib.loc = if (dir.exists(".agent-a")) ".agent-a" else .libPaths()) +library(TreeTools) + +source("dev/benchmarks/bench_datasets.R") +source("dev/benchmarks/bench_framework.R") + +# Simplified benchmark_run without callback +benchmark_run_nocb <- function(ds, strategy, + maxReplicates = 100L, + targetHits = NULL, + maxSeconds = 0, + seed = 42L) { + if (is.null(targetHits)) { + targetHits <- max(10L, ds$n_taxa %/% 5L) + } + + args <- c( + list( + contrast = ds$contrast, + tip_data = ds$tip_data, + weight = ds$weight, + levels = ds$levels, + maxReplicates = as.integer(maxReplicates), + targetHits = as.integer(targetHits), + maxSeconds = as.double(maxSeconds), + verbosity = 0L + ), + strategy + ) + + set.seed(seed) + t0 <- proc.time() + result <- do.call(TreeSearch:::ts_driven_search, args) + wall_s <- as.double((proc.time() - t0)[3]) + + list( + best_score = result$best_score, + replicates = result$replicates, + hits_to_best = result$hits_to_best, + pool_size = result$pool_size, + timed_out = result$timed_out, + wall_s = wall_s, + timings = result$timings + ) +} + +# Representative subset: small, medium, large datasets +GRID_DATASETS <- c( + "Longrich2010", # 20 tips + "Vinther2008", # 23 tips + "Aria2015", # 35 tips + "Griswold1999", # 43 tips + "Agnarsson2004", # 62 tips + "Zhu2013", # 75 tips + "Giles2015", # 78 tips + "Dikow2009" # 88 tips +) + +run_grid <- function(dataset_names = GRID_DATASETS, + strategy_names = STRATEGY_NAMES, + replicates = 3L, + maxReplicates = 100L, + maxSeconds = 20, + base_seed = 7142L) { + datasets <- load_benchmark_datasets() + n_combos <- length(dataset_names) * length(strategy_names) * replicates + cat(sprintf("Grid: %d datasets x %d strategies x %d reps = %d runs\n", + length(dataset_names), length(strategy_names), replicates, n_combos)) + + rows <- vector("list", n_combos) + idx <- 0L + + for (ds_name in dataset_names) { + ds <- datasets[[ds_name]] + if (is.null(ds)) { + warning("Skipping missing dataset: ", ds_name) + next + } + for (strat_name in strategy_names) { + strat <- get_strategy(strat_name) + for (rep in seq_len(replicates)) { + idx <- idx + 1L + seed <- base_seed + (idx - 1L) * 7L + + cat(sprintf("[%3d/%d] %-15s x %-16s rep %d ...", + idx, n_combos, ds_name, strat_name, rep)) + + res <- tryCatch( + benchmark_run_nocb(ds, strat, + maxReplicates = maxReplicates, + targetHits = max(10L, ds$n_taxa %/% 5L), + maxSeconds = maxSeconds, + seed = seed), + error = function(e) { + cat(sprintf(" ERROR: %s\n", conditionMessage(e))) + NULL + } + ) + + if (is.null(res)) { + rows[[idx]] <- data.frame( + dataset = ds_name, strategy = strat_name, replicate = rep, + seed = seed, n_taxa = ds$n_taxa, + best_score = NA_real_, replicates = NA_integer_, + hits_to_best = NA_integer_, pool_size = NA_integer_, + timed_out = NA, wall_s = NA_real_, + wagner_ms = NA_real_, tbr_ms = NA_real_, + xss_ms = NA_real_, rss_ms = NA_real_, css_ms = NA_real_, + ratchet_ms = NA_real_, drift_ms = NA_real_, + final_tbr_ms = NA_real_, fuse_ms = NA_real_, + stringsAsFactors = FALSE + ) + next + } + + cat(sprintf(" score=%.0f wall=%.1fs reps=%d %s\n", + res$best_score, res$wall_s, res$replicates, + if (res$timed_out) "[TIMEOUT]" else "")) + + rows[[idx]] <- data.frame( + dataset = ds_name, strategy = strat_name, replicate = rep, + seed = seed, n_taxa = ds$n_taxa, + best_score = res$best_score, replicates = res$replicates, + hits_to_best = res$hits_to_best, pool_size = res$pool_size, + timed_out = res$timed_out, wall_s = res$wall_s, + wagner_ms = res$timings[["wagner_ms"]], + tbr_ms = res$timings[["tbr_ms"]], + xss_ms = res$timings[["xss_ms"]], + rss_ms = res$timings[["rss_ms"]], + css_ms = res$timings[["css_ms"]], + ratchet_ms = res$timings[["ratchet_ms"]], + drift_ms = res$timings[["drift_ms"]], + final_tbr_ms = res$timings[["final_tbr_ms"]], + fuse_ms = res$timings[["fuse_ms"]], + stringsAsFactors = FALSE + ) + } + } + } + + do.call(rbind, rows[seq_len(idx)]) +} + +# Main +cat("Starting benchmark grid...\n\n") +results <- run_grid() +outfile <- "dev/benchmarks/results_grid.csv" +write.csv(results, outfile, row.names = FALSE) +cat(sprintf("\nResults saved to %s (%d rows)\n", outfile, nrow(results))) diff --git a/dev/benchmarks/bench_intra_fuse.R b/dev/benchmarks/bench_intra_fuse.R new file mode 100644 index 000000000..d873205b9 --- /dev/null +++ b/dev/benchmarks/bench_intra_fuse.R @@ -0,0 +1,172 @@ +#!/usr/bin/env Rscript +# T-258: Intra-replicate fusing experiment +# +# Compares baseline vs intraFuse=TRUE on gap datasets to measure +# score quality and replicate throughput effects. +# +# DESIGNED FOR HAMILTON HPC. Do not run locally. +# +# Usage: +# Rscript bench_intra_fuse.R [timeout_s] [output_dir] + +library(TreeSearch) +library(TreeTools) + +args <- commandArgs(trailingOnly = TRUE) +timeout_s <- if (length(args) >= 1) as.integer(args[1]) else 30L +output_dir <- if (length(args) >= 2) args[2] else "." + +cat("=== T-258: Intra-Replicate Fusing Experiment ===\n") +cat(sprintf("Timeout: %ds\n", timeout_s)) +cat(sprintf("TreeSearch version: %s\n", packageVersion("TreeSearch"))) +cat(sprintf("Started: %s\n\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) + +# ---- Datasets ---- +gap_names <- c("Conrad2008", "Geisler2001", "Wortley2006", + "Zanol2014", "Zhu2013") + +fitch_mode <- function(dataset) { + contrast <- attr(dataset, "contrast") + levels <- attr(dataset, "levels") + inapp_col <- match("-", levels) + if (is.na(inapp_col)) return(dataset) + for (i in seq_len(nrow(contrast))) { + if (contrast[i, inapp_col] == 1 && sum(contrast[i, ]) == 1) { + contrast[i, ] <- 1 + } + } + attr(dataset, "contrast") <- contrast + dataset +} + +datasets <- lapply( + setNames(gap_names, gap_names), + function(nm) fitch_mode(inapplicable.phyData[[nm]]) +) + +# ---- Configurations ---- +configs <- list( + baseline = list(label = "baseline", desc = "default preset, no intra-fuse"), + intra_fuse = list(label = "intra_fuse", desc = "default preset + intraFuse=TRUE", + intraFuse = TRUE) +) + +seeds <- c(1L, 2L, 3L, 4L, 5L) # 5 seeds for better signal +total_runs <- length(configs) * length(datasets) * length(seeds) +cat(sprintf("Configs: %d, Datasets: %d, Seeds: %d -> %d total runs\n\n", + length(configs), length(datasets), length(seeds), total_runs)) + +# ---- TNT reference scores ---- +tnt_best <- c( + Conrad2008 = 1725, Geisler2001 = 1293, Wortley2006 = 479, + Zanol2014 = 1261, Zhu2013 = 624 +) + +# ---- Run experiments ---- +results <- data.frame( + dataset = character(), n_tips = integer(), n_chars = integer(), + config = character(), seed = integer(), timeout_s = integer(), + score = numeric(), n_trees = integer(), replicates = integer(), + hits = integer(), wall_s = numeric(), + tnt_best = numeric(), gap = numeric(), + stringsAsFactors = FALSE +) + +run_idx <- 0L +for (cfg_name in names(configs)) { + cfg <- configs[[cfg_name]] + cat(sprintf("--- Config: %s (%s) ---\n", cfg$label, cfg$desc)) + + for (ds_name in gap_names) { + ds <- datasets[[ds_name]] + ntip <- NTip(ds) + nchar <- sum(attr(ds, "weight")) + + for (seed in seeds) { + run_idx <- run_idx + 1L + cat(sprintf("[%d/%d] %s / %s / seed=%d ... ", + run_idx, total_runs, cfg$label, ds_name, seed)) + + set.seed(seed) + + call_args <- list( + dataset = ds, + concavity = Inf, + maxReplicates = 96L, + targetHits = 5L, + maxSeconds = as.double(timeout_s), + strategy = "auto", + verbosity = 0L, + nThreads = 1L + ) + override_names <- setdiff(names(cfg), c("label", "desc")) + for (nm in override_names) { + call_args[[nm]] <- cfg[[nm]] + } + + t0 <- proc.time() + result <- tryCatch( + do.call(MaximizeParsimony, call_args), + error = function(e) { + warning("Error: ", ds_name, "/", cfg$label, ": ", conditionMessage(e)) + structure(list(), class = "multiPhylo", + score = NA_real_, pool_size = NA_integer_, + replicates = NA_integer_, hits_to_best = NA_integer_) + } + ) + wall_s <- as.double((proc.time() - t0)[3]) + + sc <- attr(result, "score") + tnt_ref <- tnt_best[ds_name] + gap <- if (!is.na(sc)) sc - tnt_ref else NA_real_ + + cat(sprintf("score=%s (gap=%s) in %.1fs (%d reps)\n", + if (is.na(sc)) "NA" else format(sc, nsmall = 0), + if (is.na(gap)) "NA" else sprintf("%+d", gap), + wall_s, + if (is.na(attr(result, "replicates"))) 0L + else attr(result, "replicates"))) + + results <- rbind(results, data.frame( + dataset = ds_name, n_tips = ntip, n_chars = nchar, + config = cfg$label, seed = seed, timeout_s = timeout_s, + score = sc, n_trees = length(result), + replicates = if (is.na(attr(result, "replicates"))) NA_integer_ + else attr(result, "replicates"), + hits = if (is.na(attr(result, "hits_to_best"))) NA_integer_ + else attr(result, "hits_to_best"), + wall_s = wall_s, + tnt_best = tnt_ref, gap = gap, + stringsAsFactors = FALSE + )) + } + } + cat("\n") +} + +# ---- Write results ---- +outfile <- file.path(output_dir, + sprintf("t258_intra_fuse_%ds_%s.csv", + timeout_s, + format(Sys.time(), "%Y%m%d_%H%M"))) +write.csv(results, outfile, row.names = FALSE) +cat(sprintf("\nResults saved to: %s\n", outfile)) + +# ---- Summary ---- +cat("\n=== Summary: Median score by config x dataset ===\n\n") +for (ds_name in gap_names) { + cat(sprintf(" %s (TNT best: %d)\n", ds_name, tnt_best[ds_name])) + for (cfg_name in names(configs)) { + cfg <- configs[[cfg_name]] + sub <- results[results$dataset == ds_name & results$config == cfg$label, ] + med_score <- median(sub$score, na.rm = TRUE) + med_gap <- median(sub$gap, na.rm = TRUE) + best_score <- min(sub$score, na.rm = TRUE) + med_reps <- median(sub$replicates, na.rm = TRUE) + cat(sprintf(" %-15s median=%7.1f (gap %+5.1f) best=%7.1f reps=%.0f\n", + cfg$label, med_score, med_gap, best_score, med_reps)) + } + cat("\n") +} + +cat(sprintf("\nFinished: %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) diff --git a/dev/benchmarks/bench_iterate.R b/dev/benchmarks/bench_iterate.R new file mode 100644 index 000000000..fc2bb16ae --- /dev/null +++ b/dev/benchmarks/bench_iterate.R @@ -0,0 +1,61 @@ +# ITERATE tier — the pre-commit lever GATE, ~1-2 min, run POOL-DRAINED. +# +# Gap panel at a FIXED REPLICATE COUNT (NOT maxSeconds), nThreads=1, a few seeds. +# Fixed-replicate stopping is the only condition that makes candidates_evaluated +# machine-load-independent today (a true candidate-budget stop is the planned +# C++ refinement; see dev/plans). Reports per-dataset median candidates + +# median/best score, and a gap-to-TNT column from headtohead_phase0.csv targets. +# +# This is the signal a lever must move: a candidate-efficiency win shows as LOWER +# median candidates at equal-or-better score. ~0.7% seed spread on candidates +# (vs the +/-2-4 step score lottery), so 2-3 seeds resolve a real change. +# +# Env: TS_LIB, TS_DATASETS, TS_SEEDS (1 2 3), TS_REPS (20), OUT_CSV. + +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-p0"), + winslash = "/")) + library(TreeTools) +}) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Eklund2004 Zanol2014 Zhu2013 Giles2015 Dikow2009")), "\\s+")[[1]] +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +reps <- as.integer(Sys.getenv("TS_REPS", "20")) +out <- Sys.getenv("OUT_CSV", "dev/benchmarks/iterate_latest.csv") +# TNT-best targets (apples-to-apples Fitch) from headtohead_phase0.csv. +tnt <- c(Wortley2006 = 479, Eklund2004 = 440, Zanol2014 = 1261, + Zhu2013 = 624, Giles2015 = 670, Dikow2009 = 1606) + +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } + +t0 <- Sys.time() +rows <- list() +for (nm in dsN) { + d <- fitch(inapplicable.phyData[[nm]]) + for (sd in seeds) { + set.seed(sd) + r <- suppressWarnings(MaximizeParsimony(d, maxReplicates = reps, targetHits = 999L, + maxSeconds = 0, nThreads = 1L, verbosity = 0L)) + rows[[length(rows) + 1]] <- data.frame( + dataset = nm, seed = sd, score = attr(r, "score"), + candidates = attr(r, "candidates_evaluated"), stringsAsFactors = FALSE) + } +} +res <- do.call(rbind, rows) +agg <- do.call(rbind, lapply(split(res, res$dataset), function(d) { + nm <- d$dataset[1] + data.frame(dataset = nm, tips = length(inapplicable.phyData[[nm]]), + score_best = min(d$score), score_med = median(d$score), + gap = median(d$score) - (if (nm %in% names(tnt)) tnt[[nm]] else NA), + cand_med = median(d$candidates), + cand_spread_pct = round(100 * (max(d$candidates) - min(d$candidates)) / + median(d$candidates), 2), + stringsAsFactors = FALSE) +})) +agg <- agg[order(-agg$gap), ] +cat(sprintf("ITERATE | panel x %d seeds | %d reps | %.0fs\n", length(seeds), reps, + as.double(difftime(Sys.time(), t0, units = "secs")))) +print(agg, row.names = FALSE) +write.csv(res, out, row.names = FALSE) +cat("rows ->", out, "\n") diff --git a/dev/benchmarks/bench_large_preset.R b/dev/benchmarks/bench_large_preset.R new file mode 100644 index 000000000..93115cb58 --- /dev/null +++ b/dev/benchmarks/bench_large_preset.R @@ -0,0 +1,115 @@ +# bench_large_preset.R +# +# Validates the T-179 "large" strategy preset against "thorough" on the +# 180-taxon mbank_X30754 dataset. +# +# Run from package root: +# Rscript dev/benchmarks/bench_large_preset.R +# +# Results saved to dev/benchmarks/results_large_preset.csv + +.libPaths(c(".agent-X", .libPaths())) +library(TreeSearch) +library(TreeTools) + +SRC <- getwd() +source(file.path(SRC, "dev/benchmarks/bench_datasets.R")) +# Pull updated presets from source (no rebuild needed for pure-R changes) +source(file.path(SRC, "R/SearchControl.R")) +source(file.path(SRC, "R/MaximizeParsimony.R")) + +BUDGET_S <- 60 # 60s per run — allows ~1 replicate at 180 tips +SEEDS <- c(1031L, 2847L, 7193L, 4561L, 8822L) +OUT_FILE <- file.path(SRC, "dev/benchmarks/results_large_preset.csv") + +cat("TreeSearch version:", as.character(packageVersion("TreeSearch")), "\n") +cat(sprintf("Budget: %ds | Seeds: %d\n\n", BUDGET_S, length(SEEDS))) + +# Load 180-taxon dataset +large_ds_list <- load_large_benchmark_datasets() +ds_180 <- large_ds_list[["mbank_X30754"]] +if (is.null(ds_180)) stop("mbank_X30754 not found") +cat(sprintf("Dataset: mbank_X30754 | %d taxa | %d patterns\n\n", + ds_180$n_taxa, length(ds_180$weight))) + +# Use R-level SearchControl presets (sourced above) +presets <- .StrategyPresets() +conditions <- list( + large = unclass(presets[["large"]]), + thorough = unclass(presets[["thorough"]]) +) +conditions <- lapply(conditions, function(x) { attr(x, "class") <- NULL; x }) + +total_runs <- length(conditions) * length(SEEDS) +cat(sprintf("Total runs: %d conditions x %d seeds = %d\n\n", + length(conditions), length(SEEDS), total_runs)) + +rows <- list() +idx <- 0L + +for (cond_name in names(conditions)) { + strat <- conditions[[cond_name]] + for (seed in SEEDS) { + idx <- idx + 1L + cat(sprintf("[%d/%d] %-10s | seed %d ... ", + idx, total_runs, cond_name, seed)) + flush.console() + + t_start <- proc.time() + set.seed(seed) + result <- tryCatch( + do.call(TreeSearch:::ts_driven_search, + c(list(contrast = ds_180$contrast, + tip_data = ds_180$tip_data, + weight = ds_180$weight, + levels = ds_180$levels, + maxReplicates = 500L, + targetHits = max(10L, ds_180$n_taxa %/% 5L), + maxSeconds = as.double(BUDGET_S), + verbosity = 0L), + strat)), + error = function(e) { cat("ERROR:", conditionMessage(e), "\n"); NULL } + ) + wall_s <- as.double((proc.time() - t_start)[3]) + + if (is.null(result)) next + + cat(sprintf("score=%.0f reps=%d wall=%.1fs\n", + result$best_score, result$replicates, wall_s)) + + rows[[idx]] <- data.frame( + condition = cond_name, seed = seed, + best_score = result$best_score, + replicates = result$replicates, + hits_to_best = result$hits_to_best, + wall_s = wall_s, + stringsAsFactors = FALSE + ) + } +} + +results_df <- do.call(rbind, rows) +write.csv(results_df, OUT_FILE, row.names = FALSE) +cat("\nResults written to:", OUT_FILE, "\n") + +# Summary +cat("\n===== large vs thorough on mbank_X30754 (180 tips, 60s budget) =====\n") +cat(sprintf("%-12s %8s %8s %8s %8s\n", + "Condition", "Min", "Median", "Max", "Med.reps")) +for (cond in names(conditions)) { + r <- results_df[results_df$condition == cond & !is.na(results_df$best_score), ] + cat(sprintf("%-12s %8.0f %8.0f %8.0f %8.0f\n", + cond, min(r$best_score), median(r$best_score), + max(r$best_score), median(r$replicates))) +} + +# Per-seed comparison +cat("\nPer-seed comparison (large - thorough, negative = large better):\n") +for (s in SEEDS) { + lrg <- results_df$best_score[results_df$condition == "large" & results_df$seed == s] + thr <- results_df$best_score[results_df$condition == "thorough" & results_df$seed == s] + if (length(lrg) == 1 && length(thr) == 1) { + cat(sprintf(" seed %d: large=%4.0f thorough=%4.0f delta=%+.0f\n", + s, lrg, thr, lrg - thr)) + } +} diff --git a/dev/benchmarks/bench_memory.R b/dev/benchmarks/bench_memory.R new file mode 100644 index 000000000..91d19df61 --- /dev/null +++ b/dev/benchmarks/bench_memory.R @@ -0,0 +1,168 @@ +# Phase 3D: Memory layout profiling +# +# Measures TBR phase breakdown and scaling across tree sizes. +# Run with: source("dev/benchmarks/bench_memory.R") + +library(TreeSearch) +library(TreeTools) + +# --- Helper: prepare dataset args for Rcpp call --- +prep_ds <- function(dataset) { + at <- attributes(dataset) + contrast <- at$contrast + storage.mode(contrast) <- "double" + # phyDat stores data as list of integer vectors (one per taxon) + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) + storage.mode(tip_data) <- "integer" + weight <- at$weight + levels <- at$levels + + # min_steps from contrast matrix + min_steps <- apply(contrast, 2, function(x) sum(x > 0)) - 1L + min_steps <- pmax(min_steps, 0L) + + list(contrast = contrast, tip_data = tip_data, weight = weight, + levels = levels, min_steps = min_steps) +} + +# --- Helper: get random tree edge matrix for n tips --- +make_tree_edge <- function(dataset) { + tree <- RandomTree(names(dataset), root = TRUE) + tree$edge +} + +# --- Helper: generate synthetic dataset --- +make_synthetic <- function(n_tips, n_chars = 200, na_prob = 0.1) { + tree <- RandomTree(n_tips, root = TRUE) + mat <- matrix( + sample(c("0", "1", "-"), n_tips * n_chars, replace = TRUE, + prob = c((1 - na_prob) / 2, (1 - na_prob) / 2, na_prob)), + n_tips, n_chars, + dimnames = list(tree$tip.label, NULL) + ) + MatrixToPhyDat(mat) +} + +# --- Benchmark one dataset --- +bench_one <- function(dataset, label, n_reps = 3) { + ds_args <- prep_ds(dataset) + edge <- make_tree_edge(dataset) + + results <- vector("list", n_reps) + for (i in seq_len(n_reps)) { + edge <- make_tree_edge(dataset) # different random tree each rep + results[[i]] <- TreeSearch:::ts_bench_tbr_phases( + edge, ds_args$contrast, ds_args$tip_data, + ds_args$weight, ds_args$levels, + ds_args$min_steps + ) + } + + # Average across reps + avg <- function(field) mean(vapply(results, `[[`, numeric(1), field)) + + data.frame( + label = label, + n_tips = results[[1]]$n_tips, + n_node = results[[1]]$n_node, + n_blocks = results[[1]]$n_blocks, + total_words = results[[1]]$total_words, + total_chars = results[[1]]$total_chars, + has_na = results[[1]]$has_na, + score = avg("score"), + n_clips = avg("n_clips"), + n_candidates = avg("n_candidates"), + # Timing (microseconds) + full_rescore_us = avg("time_full_rescore_us"), + clip_incr_us = avg("time_clip_incr_us"), + indirect_us = avg("time_indirect_us"), + unclip_us = avg("time_unclip_us"), + snap_save_us = avg("time_snapshot_save_us"), + snap_restore_us = avg("time_snapshot_restore_us"), + snap_bytes = avg("snapshot_bytes"), + stringsAsFactors = FALSE + ) +} + +# --- Run benchmarks --- +cat("=== Phase 3D Memory Layout Profiling ===\n\n") + +set.seed(7382) + +# Empirical datasets +cat("Benchmarking empirical datasets...\n") +data("inapplicable.phyData", package = "TreeSearch") + +empirical_results <- list() +for (name in c("Vinther2008", "Agnarsson2004")) { + cat(" ", name, "...\n") + empirical_results[[name]] <- bench_one( + inapplicable.phyData[[name]], name, n_reps = 3 + ) +} + +# Synthetic datasets of increasing size +cat("Benchmarking synthetic datasets...\n") +sizes <- c(20, 50, 100, 200) +synthetic_results <- list() +for (n in sizes) { + label <- paste0("synth_", n) + cat(" ", label, "...\n") + ds <- make_synthetic(n, n_chars = 200, na_prob = 0.1) + synthetic_results[[label]] <- bench_one(ds, label, n_reps = 3) +} + +# Combine results +all_results <- do.call(rbind, c(empirical_results, synthetic_results)) + +# --- Display --- +cat("\n=== Results ===\n\n") +print(all_results[, c("label", "n_tips", "n_blocks", "total_words", + "n_clips", "n_candidates")]) + +cat("\n=== Timing breakdown (microseconds, total across all clips) ===\n\n") +timing_cols <- c("label", "n_tips", "full_rescore_us", "clip_incr_us", + "indirect_us", "unclip_us", "snap_save_us", "snap_restore_us") +print(all_results[, timing_cols], digits = 3) + +# Compute fractions +cat("\n=== Time fractions (clip+incr / indirect / unclip) ===\n\n") +total_pass <- all_results$clip_incr_us + all_results$indirect_us + + all_results$unclip_us +fracs <- data.frame( + label = all_results$label, + n_tips = all_results$n_tips, + pct_clip_incr = round(100 * all_results$clip_incr_us / total_pass, 1), + pct_indirect = round(100 * all_results$indirect_us / total_pass, 1), + pct_unclip = round(100 * all_results$unclip_us / total_pass, 1), + snap_save_per_op_us = round(all_results$snap_save_us, 1), + snap_restore_per_op_us = round(all_results$snap_restore_us, 1), + snap_KB = round(all_results$snap_bytes / 1024, 1) +) +print(fracs) + +# Per-candidate timing +cat("\n=== Per-candidate indirect timing ===\n\n") +per_cand <- data.frame( + label = all_results$label, + n_tips = all_results$n_tips, + n_candidates = round(all_results$n_candidates), + indirect_us_total = round(all_results$indirect_us), + ns_per_candidate = round(1000 * all_results$indirect_us / + all_results$n_candidates, 1) +) +print(per_cand) + +# Scaling analysis +cat("\n=== Scaling analysis (synthetic datasets) ===\n\n") +synth <- all_results[grepl("synth", all_results$label), ] +if (nrow(synth) >= 3) { + fit <- lm(log(indirect_us) ~ log(n_tips), data = synth) + cat("Indirect time scaling exponent:", round(coef(fit)[2], 2), + "(expected ~2 for O(n^2))\n") + fit2 <- lm(log(n_candidates) ~ log(n_tips), data = synth) + cat("Candidate count scaling exponent:", round(coef(fit2)[2], 2), "\n") +} + +cat("\nDone.\n") diff --git a/dev/benchmarks/bench_nni_survey.R b/dev/benchmarks/bench_nni_survey.R new file mode 100644 index 000000000..14350b77f --- /dev/null +++ b/dev/benchmarks/bench_nni_survey.R @@ -0,0 +1,184 @@ +# NNI survey: measure batch-NNI feasibility +# +# For each dataset, builds Wagner trees and surveys all NNI candidates to +# count how many moves improve the score. This measures the theoretical +# payoff of batch/simultaneous NNI at different search stages. +# +# Usage: Rscript dev/benchmarks/bench_nni_survey.R + +args <- commandArgs(trailingOnly = TRUE) +lib_path <- if (length(args) >= 1) args[1] else stop("Usage: Rscript bench_nni_survey.R ") +.libPaths(c(lib_path, .libPaths())) + +pkg_name <- basename(lib_path) +agent_letter <- sub(".*-", "", pkg_name) +renamed <- paste0("TreeSearch.", agent_letter) +library(renamed, character.only = TRUE) +if (is.null(.Internal(getRegisteredNamespace("TreeSearch")))) + .Internal(registerNamespace("TreeSearch", asNamespace(renamed))) + +library(TreeTools) + +prepare_ts_data <- function(dataset) { + at <- attributes(dataset) + list( + contrast = at$contrast, + tip_data = matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE), + weight = at$weight, + levels = at$levels, + n_taxa = length(dataset) + ) +} + +build_wagner <- function(ds, seed) { + set.seed(seed) + TreeSearch:::ts_wagner_tree(ds$contrast, ds$tip_data, ds$weight, ds$levels) +} + +run_survey <- function(edge_mat, ds) { + TreeSearch:::ts_nni_survey( + edge_mat, ds$contrast, ds$tip_data, ds$weight, ds$levels + ) +} + +run_nni <- function(edge_mat, ds, maxHits = 20L) { + TreeSearch:::ts_nni_search( + edge_mat, ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxHits = maxHits + ) +} + +analyze_survey <- function(survey) { + deltas <- survey$delta + n_candidates <- length(deltas) + n_improving <- sum(deltas < 0) + n_equal <- sum(deltas == 0) + + edge_ids <- survey$edge + best_per_edge <- tapply(deltas, edge_ids, min) + n_edges_improving <- sum(best_per_edge < 0) + + total_improvement <- -sum(deltas[deltas < 0]) + best_improvement <- if (n_improving > 0) -min(deltas) else 0L + + data.frame( + base_score = survey$base_score, + n_edges = survey$n_edges, + n_candidates = n_candidates, + n_improving = n_improving, + n_equal = n_equal, + n_edges_improving = n_edges_improving, + total_improvement = total_improvement, + best_single_improvement = best_improvement, + pct_edges_improving = round(100 * n_edges_improving / survey$n_edges, 1) + ) +} + +# All standard Fitch datasets (no inapplicable-dominant ones) +DATASETS <- c( + "Vinther2008", # 23 tips + "Griswold1999", # 43 tips + "Eklund2004", # 54 tips + "Agnarsson2004", # 62 tips + "Zhu2013", # 75 tips + "Giles2015", # 78 tips + "Dikow2009" # 88 tips +) + +SEEDS <- c(1742L, 5281L, 8093L, 3647L, 9210L) + +cat("=== NNI Survey: Batch-NNI Feasibility ===\n") +cat("Date:", format(Sys.time(), "%Y-%m-%d %H:%M"), "\n\n") + +all_wagner <- list() +all_converged <- list() + +for (nm in DATASETS) { + ds_raw <- TreeSearch::inapplicable.phyData[[nm]] + if (is.null(ds_raw)) { cat("SKIP:", nm, "\n"); next } + ds <- prepare_ts_data(ds_raw) + n_tips <- ds$n_taxa + + cat(sprintf("\n--- %s (%d tips, %d edges) ---\n", nm, n_tips, n_tips - 2L)) + + for (seed in SEEDS) { + # Stage 1: Wagner tree + wagner <- build_wagner(ds, seed) + survey_w <- run_survey(wagner$edge, ds) + info_w <- analyze_survey(survey_w) + info_w$dataset <- nm + info_w$n_tips <- n_tips + info_w$seed <- seed + info_w$stage <- "wagner" + + cat(sprintf(" seed=%d Wagner: score=%d, %d/%d edges improving (total delta=%d, best=%d)\n", + seed, as.integer(info_w$base_score), + info_w$n_edges_improving, info_w$n_edges, + info_w$total_improvement, info_w$best_single_improvement)) + + all_wagner <- c(all_wagner, list(info_w)) + + # Stage 2: After NNI convergence (maxHits=20, full plateau search) + nni_result <- run_nni(wagner$edge, ds, maxHits = 20L) + survey_c <- run_survey(nni_result$edge, ds) + info_c <- analyze_survey(survey_c) + info_c$dataset <- nm + info_c$n_tips <- n_tips + info_c$seed <- seed + info_c$stage <- "nni_converged" + info_c$nni_moves <- nni_result$n_moves + info_c$nni_iterations <- nni_result$n_iterations + + cat(sprintf(" NNI converged: score=%d (%d moves, %d iter), %d improving edges\n", + as.integer(info_c$base_score), + nni_result$n_moves, nni_result$n_iterations, + info_c$n_edges_improving)) + + all_converged <- c(all_converged, list(info_c)) + } +} + +wagner_df <- do.call(rbind, all_wagner) +converged_df <- do.call(rbind, all_converged) + +cat("\n\n========================================\n") +cat("=== SUMMARY: Wagner Tree Surveys ===\n") +cat("========================================\n\n") + +for (nm in unique(wagner_df$dataset)) { + sub <- wagner_df[wagner_df$dataset == nm, ] + csub <- converged_df[converged_df$dataset == nm, ] + cat(sprintf("%s (%d tips, %d NNI edges):\n", nm, sub$n_tips[1], sub$n_edges[1])) + cat(sprintf(" Wagner scores: %d-%d (median %d)\n", + min(as.integer(sub$base_score)), + max(as.integer(sub$base_score)), + as.integer(median(sub$base_score)))) + cat(sprintf(" Improving edges: %d-%d (median %.0f, %.0f%% of edges)\n", + min(sub$n_edges_improving), max(sub$n_edges_improving), + median(sub$n_edges_improving), + median(sub$pct_edges_improving))) + cat(sprintf(" Total delta: %d-%d steps (median %d)\n", + min(sub$total_improvement), max(sub$total_improvement), + as.integer(median(sub$total_improvement)))) + cat(sprintf(" Best single move: %d-%d steps\n", + min(sub$best_single_improvement), + max(sub$best_single_improvement))) + cat(sprintf(" NNI-converged: score %d-%d (%d-%d moves)\n\n", + min(as.integer(csub$base_score)), + max(as.integer(csub$base_score)), + min(csub$nni_moves), max(csub$nni_moves))) +} + +cat("\n=== Key Finding: Batch Size (improving edges on Wagner trees) ===\n") +cat(sprintf("%-15s %5s %10s %10s %10s %10s\n", + "Dataset", "Tips", "Med.Batch", "Max.Batch", "%Edges", "Med.Delta")) +for (nm in unique(wagner_df$dataset)) { + sub <- wagner_df[wagner_df$dataset == nm, ] + cat(sprintf("%-15s %5d %10.0f %10d %9.0f%% %10d\n", + nm, sub$n_tips[1], + median(sub$n_edges_improving), + max(sub$n_edges_improving), + median(sub$pct_edges_improving), + as.integer(median(sub$total_improvement)))) +} diff --git a/dev/benchmarks/bench_outer_cycles.R b/dev/benchmarks/bench_outer_cycles.R new file mode 100644 index 000000000..ac0f56da2 --- /dev/null +++ b/dev/benchmarks/bench_outer_cycles.R @@ -0,0 +1,163 @@ +# bench_outer_cycles.R +# +# Compares thorough preset with outerCycles=1 vs outerCycles=2 across all 14 +# standard benchmark datasets. Uses 3 seeds x 20s time budget per condition. +# +# Run from package root via: +# Rscript dev/benchmarks/bench_outer_cycles.R +# +# Results saved to dev/benchmarks/results_outer_cycles.csv + +.libPaths(c(".agent-X", .libPaths())) +library(TreeSearch) +library(TreeTools) + +SRC <- getwd() +source(file.path(SRC, "dev/benchmarks/bench_datasets.R")) +source(file.path(SRC, "dev/benchmarks/bench_framework.R")) + +BUDGET_S <- 20 +SEEDS <- c(1031L, 2847L, 7193L) +OUT_FILE <- file.path(SRC, "dev/benchmarks/results_outer_cycles.csv") + +cat("TreeSearch version:", as.character(packageVersion("TreeSearch")), "\n") +cat(sprintf("Budget: %ds | Seeds: %d\n", BUDGET_S, length(SEEDS))) + +# Build thorough strategy base (matches get_strategy("thorough") in bench_framework.R) +thorough_base <- list( + wagnerStarts = 3L, + tbrMaxHits = 3L, + tabuSize = 200L, + ratchetCycles = 20L, + ratchetPerturbProb = 0.25, + ratchetPerturbMode = 2L, + ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = TRUE, + driftCycles = 12L, + driftAfdLimit = 5L, + driftRfdLimit = 0.15, + xssRounds = 5L, + xssPartitions = 6L, + rssRounds = 3L, + cssRounds = 2L, + cssPartitions = 6L, + sectorMinSize = 6L, + sectorMaxSize = 80L, + fuseInterval = 2L, + fuseAcceptEqual = TRUE, + nniFirst = TRUE, + sprFirst = FALSE, + consensusStableReps = 3L +) + +conditions <- list( + thorough_1 = c(thorough_base, list(outerCycles = 1L)), + thorough_2 = c(thorough_base, list(outerCycles = 2L)) +) + +datasets <- load_benchmark_datasets() +cat("Datasets loaded:", length(datasets), "\n\n") + +total_runs <- length(BENCHMARK_NAMES) * length(conditions) * length(SEEDS) +cat(sprintf("Total runs: %d x %d conditions x %d seeds = %d\n\n", + length(BENCHMARK_NAMES), length(conditions), length(SEEDS), total_runs)) + +rows <- list() +idx <- 0L + +for (ds_name in BENCHMARK_NAMES) { + ds <- datasets[[ds_name]] + if (is.null(ds)) { warning("Skipping ", ds_name); next } + + for (cond_name in names(conditions)) { + strat <- conditions[[cond_name]] + + for (seed in SEEDS) { + idx <- idx + 1L + cat(sprintf("[%3d/%d] %-14s | %-12s | seed %d ... ", + idx, total_runs, ds_name, cond_name, seed)) + flush.console() + + t_start <- proc.time() + set.seed(seed) + result <- tryCatch( + do.call(TreeSearch:::ts_driven_search, + c(list(contrast = ds$contrast, + tip_data = ds$tip_data, + weight = ds$weight, + levels = ds$levels, + maxReplicates = 200L, + targetHits = max(10L, ds$n_taxa %/% 5L), + maxSeconds = as.double(BUDGET_S), + verbosity = 0L), + strat)), + error = function(e) { + cat("ERROR:", conditionMessage(e), "\n"); NULL + } + ) + wall_s <- as.double((proc.time() - t_start)[3]) + + if (is.null(result)) { + rows[[idx]] <- data.frame( + dataset = ds_name, condition = cond_name, seed = seed, + n_taxa = ds$n_taxa, best_score = NA_real_, + replicates = NA_integer_, hits_to_best = NA_integer_, + wall_s = wall_s, stringsAsFactors = FALSE + ) + next + } + + cat(sprintf("score=%.0f reps=%d wall=%.1fs\n", + result$best_score, result$replicates, wall_s)) + + rows[[idx]] <- data.frame( + dataset = ds_name, + condition = cond_name, + seed = seed, + n_taxa = ds$n_taxa, + best_score = result$best_score, + replicates = result$replicates, + hits_to_best = result$hits_to_best, + wall_s = wall_s, + stringsAsFactors = FALSE + ) + } + } +} + +results_df <- do.call(rbind, rows) +write.csv(results_df, OUT_FILE, row.names = FALSE) +cat("\nResults written to:", OUT_FILE, "\n") + +# Quick summary +library(dplyr) +summary_tbl <- results_df |> + filter(!is.na(best_score)) |> + group_by(dataset, n_taxa, condition) |> + summarise(median_score = median(best_score), + median_reps = median(replicates), + .groups = "drop") |> + tidyr::pivot_wider(names_from = condition, + values_from = c(median_score, median_reps)) |> + mutate(delta = median_score_thorough_2 - median_score_thorough_1) |> + arrange(n_taxa) + +cat("\n===== outerCycles=2 vs outerCycles=1 (lower score = better) =====\n") +cat(sprintf("%-16s %5s %8s %8s %6s %5s %5s\n", + "Dataset", "Tips", "OC1_score", "OC2_score", "Delta", + "OC1_reps", "OC2_reps")) +cat(strrep("-", 68), "\n") +for (i in seq_len(nrow(summary_tbl))) { + r <- summary_tbl[i, ] + cat(sprintf("%-16s %5d %8.0f %8.0f %+6.1f %5.0f %5.0f\n", + r$dataset, r$n_taxa, + r$median_score_thorough_1, r$median_score_thorough_2, + r$delta, + r$median_reps_thorough_1, r$median_reps_thorough_2)) +} +improved <- sum(summary_tbl$delta < -0.5, na.rm = TRUE) +unchanged <- sum(abs(summary_tbl$delta) <= 0.5, na.rm = TRUE) +worse <- sum(summary_tbl$delta > 0.5, na.rm = TRUE) +cat(strrep("-", 68), "\n") +cat(sprintf("Improved: %d Unchanged: %d Worse: %d\n", + improved, unchanged, worse)) diff --git a/dev/benchmarks/bench_p2_levers.R b/dev/benchmarks/bench_p2_levers.R new file mode 100644 index 000000000..16e1a764f --- /dev/null +++ b/dev/benchmarks/bench_p2_levers.R @@ -0,0 +1,104 @@ +# Phase 2 lever sweep — does cutting/rebalancing ratchet help the gap panel? +# +# Phase 1 found ratchet owns 63-83% of wall-clock (sectorial only 7-23%), the +# opposite of TNT. This tests ratchet/sectorial rebalancing via `auto` preset + +# `...` overrides (no rebuild). FIXED reps, parallel pool (replicate-bounded -> +# deterministic candidates; safe in the pool). +# +# CAVEAT: fixed-reps varies BOTH candidates and score per config, so it shows +# trade-offs, not a clean iso-candidate comparison (that needs the planned +# max_candidates C++ stop). Read: a config that holds score with FEWER candidates +# => ratchet over-invested; a config that improves score => quality win. +# +# Env: TS_LIB, TS_DATASETS, TS_SEEDS, TS_REPS, TS_HEADROOM, OUT_CSV. + +suppressMessages(library(parallel)) +LIB <- normalizePath(Sys.getenv("TS_LIB", ".agent-p0"), winslash = "/") +WD <- normalizePath(".", winslash = "/") +reps <- as.integer(Sys.getenv("TS_REPS", "20")) +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2")), "\\s+")[[1]]) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Eklund2004 Zanol2014 Zhu2013 Giles2015 Dikow2009")), "\\s+")[[1]] +headroom <- as.integer(Sys.getenv("TS_HEADROOM", "2")) + +# Config sets, selectable via TS_SWEEP. Each value is a list of `...` overrides +# applied on top of strategy="auto". Round 1 (ratchet/sectorial) and round 2 +# (fusing/ordering/starts) both gave no win over baseline — see +# dev/plans/2026-06-16-closing-the-tnt-gap.md Phase 2. +all_configs <- list( + ratchet = list( + baseline = list(), + ratchet6 = list(ratchetCycles = 6L), + ratchet3 = list(ratchetCycles = 3L), + adaptiveOff = list(adaptiveLevel = FALSE), + sectorHeavy = list(xssRounds = 6L, rssRounds = 2L), + rebalance = list(ratchetCycles = 6L, xssRounds = 6L, rssRounds = 2L) + ), + fuse = list( + baseline = list(), + intraFuse = list(intraFuse = TRUE), + fuseFreq = list(fuseInterval = 1L), + fuseEqual = list(intraFuse = TRUE, fuseAcceptEqual = TRUE), + clipTips = list(clipOrder = 2L), + wagner5 = list(wagnerStarts = 5L) + ), + optin = list( + baseline = list(), + intraFuse = list(intraFuse = TRUE), + wagner5 = list(wagnerStarts = 5L), + combo = list(intraFuse = TRUE, wagnerStarts = 5L) + ), + # Phase 3 probe: rebalance budget from ratchet toward EXACT sectorial (CSS), + # which avoids the approximate XSS/RSS miss-and-revert waste. Tests whether + # the cheapest exact phase, given more budget, carries more of the search. + rebalance = list( + baseline = list(), + css4 = list(cssRounds = 4L), + ratchetDown = list(ratchetCycles = 8L), + rebalA = list(ratchetCycles = 12L, cssRounds = 4L), + rebalB = list(ratchetCycles = 8L, cssRounds = 4L, cssPartitions = 6L) + ) +) +configs <- all_configs[[Sys.getenv("TS_SWEEP", "ratchet")]] +if (is.null(configs)) stop("unknown TS_SWEEP") + +jobs <- expand.grid(cfg = names(configs), dataset = dsN, seed = seeds, + stringsAsFactors = FALSE) +conc <- min(max(1L, parallel::detectCores(logical = TRUE) - headroom), nrow(jobs)) +cat(sprintf("P2 levers | %d jobs (%d cfg x %d ds x %d seeds) | conc=%d | %d reps\n", + nrow(jobs), length(configs), length(dsN), length(seeds), conc, reps)) + +t0 <- Sys.time() +cl <- makePSOCKcluster(conc) +on.exit(stopCluster(cl)) +clusterExport(cl, c("LIB", "WD", "reps", "jobs", "configs"), envir = environment()) +invisible(clusterEvalQ(cl, { + setwd(WD); Sys.setenv(OMP_NUM_THREADS = "1", OPENBLAS_NUM_THREADS = "1") + suppressMessages({ library(TreeSearch, lib.loc = LIB); library(TreeTools) }) + data("inapplicable.phyData", package = "TreeSearch") + fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +})) +rows <- parLapplyLB(cl, seq_len(nrow(jobs)), function(i) { + cfg <- jobs$cfg[i]; nm <- jobs$dataset[i]; sd <- jobs$seed[i] + d <- fitch(inapplicable.phyData[[nm]]); set.seed(sd) + base <- list(d, strategy = "auto", maxReplicates = reps, targetHits = 999L, + maxSeconds = 0, nThreads = 1L, verbosity = 0L) + r <- suppressWarnings(do.call(MaximizeParsimony, c(base, configs[[cfg]]))) + data.frame(cfg = cfg, dataset = nm, seed = sd, score = attr(r, "score"), + candidates = attr(r, "candidates_evaluated"), stringsAsFactors = FALSE) +}) +res <- do.call(rbind, rows) +wall <- as.double(difftime(Sys.time(), t0, units = "secs")) +agg <- aggregate(cbind(score, candidates) ~ cfg + dataset, res, median) +cat(sprintf("done in %.0fs\n", wall)) +for (nm in dsN) { + d <- agg[agg$dataset == nm, ] + d <- d[order(d$score, d$candidates), ] + b <- d[d$cfg == "baseline", ] + d$dScore <- d$score - b$score + d$dCand_pct <- round(100 * (d$candidates / b$candidates - 1)) + cat(sprintf("\n== %s (baseline %g @ %sM) ==\n", nm, b$score, + format(round(b$candidates / 1e6), big.mark = ","))) + print(d[, c("cfg", "score", "dScore", "dCand_pct")], row.names = FALSE) +} +write.csv(res, Sys.getenv("OUT_CSV", "dev/benchmarks/p2_levers.csv"), row.names = FALSE) diff --git a/dev/benchmarks/bench_parallel.R b/dev/benchmarks/bench_parallel.R new file mode 100644 index 000000000..a841e7faf --- /dev/null +++ b/dev/benchmarks/bench_parallel.R @@ -0,0 +1,60 @@ +# PARALLEL BATCH runner — run a (dataset x seed) panel across a local PSOCK pool. +# +# For BATCH panels ONLY (e.g. an iterate-style panel over many seeds, or a +# preset sweep). NOT for a single authoritative candidate/timing measurement — +# oversubscription perturbs wall-clock and, under any wall-clock-bounded stop, +# the candidate count too. Each worker is single-threaded (nThreads=1, OMP=1) +# and REPLICATE-bounded, so candidates_evaluated stays valid per run. +# +# 8 physical cores, memory-bandwidth-bound Fitch -> realistic ~5-7x, not 16x. +# Set TS_HEADROOM high (>=4) while another panel/process is live. +# +# Env: TS_LIB, TS_DATASETS, TS_SEEDS, TS_REPS, TS_STRATEGY (auto), TS_HEADROOM, OUT_CSV. + +suppressMessages(library(parallel)) +LIB <- normalizePath(Sys.getenv("TS_LIB", ".agent-p0"), winslash = "/") +WD <- normalizePath(".", winslash = "/") +reps <- as.integer(Sys.getenv("TS_REPS", "20")) +strat <- Sys.getenv("TS_STRATEGY", "auto") +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3 4 5")), "\\s+")[[1]]) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Eklund2004 Zanol2014 Zhu2013 Giles2015 Dikow2009")), "\\s+")[[1]] +out <- Sys.getenv("OUT_CSV", "dev/benchmarks/parallel_latest.csv") +headroom <- as.integer(Sys.getenv("TS_HEADROOM", "2")) +conc <- max(1L, parallel::detectCores(logical = TRUE) - headroom) + +jobs <- expand.grid(dataset = dsN, seed = seeds, stringsAsFactors = FALSE) +conc <- min(conc, nrow(jobs)) +cat(sprintf("PARALLEL | %d jobs | conc=%d (cores=%d, headroom=%d) | %d reps | strategy=%s\n", + nrow(jobs), conc, parallel::detectCores(logical = TRUE), headroom, reps, strat)) + +t0 <- Sys.time() +cl <- makePSOCKcluster(conc) +on.exit(stopCluster(cl)) +clusterExport(cl, c("LIB", "WD", "reps", "strat", "jobs"), envir = environment()) +invisible(clusterEvalQ(cl, { + setwd(WD) # PSOCK workers do NOT inherit CWD + Sys.setenv(OMP_NUM_THREADS = "1", OPENBLAS_NUM_THREADS = "1") + suppressMessages({ library(TreeSearch, lib.loc = LIB); library(TreeTools) }) + data("inapplicable.phyData", package = "TreeSearch") + fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +})) +rows <- parLapplyLB(cl, seq_len(nrow(jobs)), function(i) { + nm <- jobs$dataset[i]; sd <- jobs$seed[i] + d <- fitch(inapplicable.phyData[[nm]]); set.seed(sd) + r <- suppressWarnings(MaximizeParsimony(d, maxReplicates = reps, targetHits = 999L, + maxSeconds = 0, nThreads = 1L, strategy = strat, + verbosity = 0L)) + data.frame(dataset = nm, seed = sd, score = attr(r, "score"), + candidates = attr(r, "candidates_evaluated"), stringsAsFactors = FALSE) +}) +res <- do.call(rbind, rows) +wall <- as.double(difftime(Sys.time(), t0, units = "secs")) +agg <- do.call(rbind, lapply(split(res, res$dataset), function(d) + data.frame(dataset = d$dataset[1], score_best = min(d$score), + score_med = median(d$score), cand_med = median(d$candidates), + stringsAsFactors = FALSE))) +cat(sprintf("done in %.0fs (%d jobs at conc=%d)\n", wall, nrow(jobs), conc)) +print(agg[order(agg$dataset), ], row.names = FALSE) +write.csv(res, out, row.names = FALSE) +cat("rows ->", out, "\n") diff --git a/dev/benchmarks/bench_perturb_stop.R b/dev/benchmarks/bench_perturb_stop.R new file mode 100644 index 000000000..696a308b7 --- /dev/null +++ b/dev/benchmarks/bench_perturb_stop.R @@ -0,0 +1,197 @@ +#!/usr/bin/env Rscript +# Benchmark: perturbStopFactor effectiveness across dataset sizes +# +# Compares search convergence with different perturbStopFactor settings. +# For each dataset, runs MaximizeParsimony with: +# - Baseline (perturbStopFactor = 0, i.e. disabled) +# - perturbStopFactor = 2 +# - perturbStopFactor = 5 +# +# Measures: elapsed time, best score, replicates completed. + +.libPaths(c( + "C:/Users/pjjg18/GitHub/.builds/TreeSearch-Z", + .libPaths() +)) +library(TreeSearch.Z) +if (is.null(.Internal(getRegisteredNamespace("TreeSearch")))) + .Internal(registerNamespace("TreeSearch", asNamespace("TreeSearch.Z"))) +library(TreeTools) + +# Select datasets across the size spectrum. +# Use the inst/datasets (inapplicable.phyData) for small/medium, +# plus morphobank datasets from neotrans for large/XL. +neotrans_dir <- system.file("matrices", package = "neotrans") + +load_dataset <- function(name, source = "inapplicable") { + if (source == "inapplicable") { + return(TreeSearch::inapplicable.phyData[[name]]) + } else { + path <- file.path(neotrans_dir, paste0(name, ".nex")) + return(suppressWarnings(TreeTools::ReadAsPhyDat(path))) + } +} + +# Dataset selection: cover small (20-40), medium (41-80), large (81-150), +# XL (150+). Focus on medium-to-XL where the feature is most relevant. +datasets_spec <- list( + # Small — expect quick convergence, perturb-stop shouldn't matter + list(name = "Vinther2008", source = "inapplicable", ntip = 23), + list(name = "Aria2015", source = "inapplicable", ntip = 35), + + # Medium — starts to get interesting + list(name = "Griswold1999", source = "inapplicable", ntip = 43), + list(name = "Eklund2004", source = "inapplicable", ntip = 54), + + # Medium-large — key range + list(name = "Agnarsson2004", source = "inapplicable", ntip = 62), + list(name = "Zhu2013", source = "inapplicable", ntip = 75), + list(name = "Dikow2009", source = "inapplicable", ntip = 88), + + # Large — from morphobank/neotrans + list(name = "project2086", source = "neotrans", ntip = 91), + list(name = "project2769", source = "neotrans", ntip = 102), + list(name = "project1013", source = "neotrans", ntip = 112), + list(name = "project2286", source = "neotrans", ntip = 134), + + # XL + list(name = "project1024", source = "neotrans", ntip = 163), + list(name = "project2477", source = "neotrans", ntip = 213) +) + +# perturbStopFactor values to test (0 = disabled = baseline) +psf_values <- c(0L, 2L, 5L) + +# Per-dataset time budget: scale with tip count +# Small: 15s, Medium: 30s, Large: 60s, XL: 90s +time_budget <- function(ntip) { + if (ntip <= 40) 15 + else if (ntip <= 80) 30 + else if (ntip <= 150) 60 + else 90 +} + +# Number of reps per condition +n_reps <- 2L + +set.seed(7418) + +results <- data.frame( + dataset = character(), + ntip = integer(), + nchar = integer(), + psf = integer(), + rep = integer(), + elapsed_s = numeric(), + best_score = numeric(), + n_replicates = integer(), + stringsAsFactors = FALSE +) + +cat("=== Perturbation-Stop Benchmark ===\n") +cat(sprintf("Datasets: %d, PSF values: %s, Reps: %d\n", + length(datasets_spec), + paste(psf_values, collapse = "/"), + n_reps)) + +for (ds_spec in datasets_spec) { + cat(sprintf("\n--- %s (%d tips) ---\n", ds_spec$name, ds_spec$ntip)) + + dataset <- tryCatch( + load_dataset(ds_spec$name, ds_spec$source), + error = function(e) { + cat(" SKIP: ", conditionMessage(e), "\n") + NULL + } + ) + if (is.null(dataset)) next + + actual_ntip <- length(dataset) + actual_nchar <- sum(attr(dataset, "weight")) + budget <- time_budget(actual_ntip) + + cat(sprintf(" Actual: %d tips, %d chars, budget: %ds\n", + actual_ntip, actual_nchar, budget)) + + for (psf in psf_values) { + for (r in seq_len(n_reps)) { + seed <- sample.int(10000, 1) + set.seed(seed) + + ctrl <- SearchControl(perturbStopFactor = psf) + + t0 <- proc.time()["elapsed"] + res <- tryCatch( + MaximizeParsimony( + dataset, + control = ctrl, + maxSeconds = budget, + maxReplicates = 500L, + targetHits = max(10L, as.integer(actual_ntip / 5)), + verbosity = 0L, + nThreads = 2L + ), + error = function(e) { + cat(sprintf(" ERROR (psf=%d, rep=%d): %s\n", + psf, r, conditionMessage(e))) + NULL + } + ) + elapsed <- proc.time()["elapsed"] - t0 + + if (!is.null(res)) { + best <- attr(res, "score") + if (is.null(best)) best <- TreeLength(res[[1]], dataset) + n_reps_done <- attr(res, "replicates") + if (is.null(n_reps_done)) n_reps_done <- NA_integer_ + + results <- rbind(results, data.frame( + dataset = ds_spec$name, + ntip = actual_ntip, + nchar = actual_nchar, + psf = psf, + rep = r, + elapsed_s = round(elapsed, 2), + best_score = best, + n_replicates = n_reps_done, + stringsAsFactors = FALSE + )) + + cat(sprintf(" psf=%d rep=%d: %.1fs, score=%.1f, reps=%s\n", + psf, r, elapsed, + best, + ifelse(is.na(n_reps_done), "?", as.character(n_reps_done)))) + } + } + } +} + +cat("\n\n=== Summary ===\n") + +# Aggregate by dataset x psf +agg <- aggregate( + cbind(elapsed_s, best_score) ~ dataset + ntip + nchar + psf, + data = results, + FUN = mean +) +agg <- agg[order(agg$ntip, agg$psf), ] + +# Reshape for comparison +baseline <- agg[agg$psf == 0, c("dataset", "ntip", "nchar", + "elapsed_s", "best_score")] +names(baseline)[4:5] <- c("time_base", "score_base") + +for (p in psf_values[psf_values > 0]) { + psf_rows <- agg[agg$psf == p, c("dataset", "elapsed_s", "best_score")] + names(psf_rows)[2:3] <- paste0(c("time_psf", "score_psf"), p) + baseline <- merge(baseline, psf_rows, by = "dataset", all.x = TRUE) +} + +baseline <- baseline[order(baseline$ntip), ] +cat("\n") +print(baseline, row.names = FALSE) + +# Save +out_path <- "dev/benchmarks/results_perturb_stop.csv" +write.csv(results, out_path, row.names = FALSE) +cat(sprintf("\nRaw results saved to: %s\n", out_path)) diff --git a/dev/benchmarks/bench_perturb_stop2.R b/dev/benchmarks/bench_perturb_stop2.R new file mode 100644 index 000000000..6d8ed7aaf --- /dev/null +++ b/dev/benchmarks/bench_perturb_stop2.R @@ -0,0 +1,172 @@ +#!/usr/bin/env Rscript +# Benchmark v2: perturb-stop with generous time, replicate-limited +# +# Goal: see if perturbStopFactor can terminate searches early +# (before maxReplicates) and whether the scores it finds are equivalent. +# +# Key change from v1: use maxSeconds = 600 (generous) so +# replicate-based criteria can fire. Cap maxReplicates = 200. + +.libPaths(c( + "C:/Users/pjjg18/GitHub/.builds/TreeSearch-Z", + .libPaths() +)) +library(TreeSearch.Z) +if (is.null(.Internal(getRegisteredNamespace("TreeSearch")))) + .Internal(registerNamespace("TreeSearch", asNamespace("TreeSearch.Z"))) +library(TreeTools) + +neotrans_dir <- system.file("matrices", package = "neotrans") + +load_dataset <- function(name, source = "inapplicable") { + if (source == "inapplicable") { + return(TreeSearch::inapplicable.phyData[[name]]) + } else { + path <- file.path(neotrans_dir, paste0(name, ".nex")) + return(suppressWarnings(TreeTools::ReadAsPhyDat(path))) + } +} + +# Focus on medium-to-large datasets where convergence behavior matters. +# Include small ones as controls. +datasets_spec <- list( + # Small — should converge very quickly regardless + list(name = "Vinther2008", source = "inapplicable", ntip = 23), + list(name = "Aria2015", source = "inapplicable", ntip = 35), + + # Medium — may or may not converge + list(name = "Griswold1999", source = "inapplicable", ntip = 43), + list(name = "Eklund2004", source = "inapplicable", ntip = 54), + list(name = "Agnarsson2004", source = "inapplicable", ntip = 62), + list(name = "Zhu2013", source = "inapplicable", ntip = 75), + list(name = "Dikow2009", source = "inapplicable", ntip = 88), + + # Large — from morphobank/neotrans + list(name = "project2086", source = "neotrans", ntip = 91), + list(name = "project2769", source = "neotrans", ntip = 102), + list(name = "project1013", source = "neotrans", ntip = 112) +) + +psf_values <- c(0L, 2L, 5L) + +# Per-dataset: generous time, moderate maxReplicates to let +# convergence criteria fire. The question is whether PSF terminates +# before targetHits, and at what score. +max_reps_by_size <- function(ntip) { + # Enough replicates that targetHits should fire for easy datasets, + # but hard datasets won't hit targetHits within budget. + if (ntip <= 40) 100L + else if (ntip <= 80) 150L + else 200L +} + +max_seconds_by_size <- function(ntip) { + # Generous: 5x what the first benchmark showed was needed + if (ntip <= 40) 30 + else if (ntip <= 80) 120 + else 300 +} + +n_reps <- 2L +set.seed(4193) + +results <- data.frame( + dataset = character(), ntip = integer(), nchar = integer(), + psf = integer(), rep = integer(), elapsed_s = numeric(), + best_score = numeric(), n_replicates = integer(), + stringsAsFactors = FALSE +) + +cat("=== Perturbation-Stop Benchmark v2 ===\n") +cat("Focus: do stopping criteria fire before time limit?\n\n") + +for (ds_spec in datasets_spec) { + cat(sprintf("\n--- %s (%d tips) ---\n", ds_spec$name, ds_spec$ntip)) + dataset <- tryCatch(load_dataset(ds_spec$name, ds_spec$source), + error = function(e) { cat("SKIP:", conditionMessage(e), "\n"); NULL }) + if (is.null(dataset)) next + + actual_ntip <- length(dataset) + actual_nchar <- sum(attr(dataset, "weight")) + max_reps <- max_reps_by_size(actual_ntip) + max_secs <- max_seconds_by_size(actual_ntip) + target_hits <- max(10L, as.integer(actual_ntip / 5)) + + cat(sprintf(" %d tips, %d chars | maxReps=%d, maxSec=%d, targetHits=%d\n", + actual_ntip, actual_nchar, max_reps, max_secs, target_hits)) + + for (psf in psf_values) { + for (r in seq_len(n_reps)) { + seed <- sample.int(10000, 1) + set.seed(seed) + + ctrl <- SearchControl(perturbStopFactor = psf) + + t0 <- proc.time()["elapsed"] + res <- tryCatch( + MaximizeParsimony( + dataset, + control = ctrl, + maxSeconds = max_secs, + maxReplicates = max_reps, + targetHits = target_hits, + verbosity = 0L, + nThreads = 2L + ), + error = function(e) { + cat(sprintf(" ERROR (psf=%d, rep=%d): %s\n", psf, r, conditionMessage(e))) + NULL + } + ) + elapsed <- proc.time()["elapsed"] - t0 + + if (!is.null(res)) { + best <- attr(res, "score") + if (is.null(best)) best <- TreeLength(res[[1]], dataset) + n_reps_done <- attr(res, "replicates") + if (is.null(n_reps_done)) n_reps_done <- NA_integer_ + + # Determine which criterion likely fired + stop_reason <- "?" + if (!is.na(n_reps_done)) { + if (n_reps_done >= max_reps) stop_reason <- "maxReps" + else if (elapsed >= max_secs * 0.95) stop_reason <- "time" + else stop_reason <- "converged" + } + + results <- rbind(results, data.frame( + dataset = ds_spec$name, ntip = actual_ntip, nchar = actual_nchar, + psf = psf, rep = r, elapsed_s = round(elapsed, 2), + best_score = best, n_replicates = n_reps_done, + stringsAsFactors = FALSE + )) + + cat(sprintf(" psf=%d rep=%d: %.1fs, score=%.0f, reps=%s [%s]\n", + psf, r, elapsed, best, + ifelse(is.na(n_reps_done), "?", as.character(n_reps_done)), + stop_reason)) + } + } + } +} + +cat("\n\n=== Summary Table ===\n") +agg <- aggregate( + cbind(elapsed_s, best_score, n_replicates) ~ dataset + ntip + nchar + psf, + data = results, FUN = mean +) +agg <- agg[order(agg$ntip, agg$psf), ] + +# Print nicely +for (ds in unique(agg$dataset)) { + rows <- agg[agg$dataset == ds, ] + cat(sprintf("\n%s (%d tips, %d chars):\n", ds, rows$ntip[1], rows$nchar[1])) + for (i in seq_len(nrow(rows))) { + cat(sprintf(" psf=%d: %.1fs, score=%.1f, reps=%.0f\n", + rows$psf[i], rows$elapsed_s[i], + rows$best_score[i], rows$n_replicates[i])) + } +} + +write.csv(results, "dev/benchmarks/results_perturb_stop_v2.csv", row.names = FALSE) +cat("\nSaved to dev/benchmarks/results_perturb_stop_v2.csv\n") diff --git a/dev/benchmarks/bench_perturb_stop3.R b/dev/benchmarks/bench_perturb_stop3.R new file mode 100644 index 000000000..66fdbe246 --- /dev/null +++ b/dev/benchmarks/bench_perturb_stop3.R @@ -0,0 +1,109 @@ +#!/usr/bin/env Rscript +# Benchmark v3: isolate PSF by disabling targetHits +# +# With targetHits effectively disabled (set to 999), the only +# stopping criteria are: maxReplicates, maxSeconds, or PSF. +# This shows whether PSF would ever fire as a pure convergence signal. + +.libPaths(c( + "C:/Users/pjjg18/GitHub/.builds/TreeSearch-Z", + .libPaths() +)) +library(TreeSearch.Z) +if (is.null(.Internal(getRegisteredNamespace("TreeSearch")))) + .Internal(registerNamespace("TreeSearch", asNamespace("TreeSearch.Z"))) +library(TreeTools) + +neotrans_dir <- system.file("matrices", package = "neotrans") +load_dataset <- function(name, source = "inapplicable") { + if (source == "inapplicable") TreeSearch::inapplicable.phyData[[name]] + else suppressWarnings(TreeTools::ReadAsPhyDat( + file.path(neotrans_dir, paste0(name, ".nex")))) +} + +# Focus on medium datasets where per-rep cost is low enough to get many reps +datasets_spec <- list( + list(name = "Griswold1999", source = "inapplicable", ntip = 43), + list(name = "Eklund2004", source = "inapplicable", ntip = 54), + list(name = "Agnarsson2004", source = "inapplicable", ntip = 62), + list(name = "Zhu2013", source = "inapplicable", ntip = 75), + list(name = "Dikow2009", source = "inapplicable", ntip = 88) +) + +psf_values <- c(0L, 2L, 5L) +n_reps <- 3L +set.seed(8321) + +cat("=== PSF Isolation Test ===\n") +cat("targetHits=999 (disabled), maxReplicates=500, maxSeconds=300\n\n") + +results <- list() + +for (ds_spec in datasets_spec) { + dataset <- load_dataset(ds_spec$name, ds_spec$source) + if (is.null(dataset)) next + ntip <- length(dataset) + nchar <- sum(attr(dataset, "weight")) + + cat(sprintf("\n--- %s (%d tips, %d chars) ---\n", ds_spec$name, ntip, nchar)) + cat(sprintf(" PSF limits: psf=2 → %d reps, psf=5 → %d reps\n", + ntip * 2L, ntip * 5L)) + + for (psf in psf_values) { + for (r in seq_len(n_reps)) { + set.seed(sample.int(10000, 1)) + ctrl <- SearchControl(perturbStopFactor = psf) + + t0 <- proc.time()["elapsed"] + res <- tryCatch( + MaximizeParsimony( + dataset, control = ctrl, + maxSeconds = 300, maxReplicates = 500L, + targetHits = 999L, + verbosity = 0L, nThreads = 2L + ), + error = function(e) { cat(" ERR:", conditionMessage(e), "\n"); NULL } + ) + elapsed <- proc.time()["elapsed"] - t0 + + if (!is.null(res)) { + best <- attr(res, "score") + if (is.null(best)) best <- TreeLength(res[[1]], dataset) + nrep <- attr(res, "replicates") + if (is.null(nrep)) nrep <- NA_integer_ + + stop_reason <- if (!is.na(nrep) && nrep >= 500) "maxReps" + else if (elapsed >= 285) "time" + else "PSF/converged" + + results[[length(results) + 1]] <- data.frame( + dataset = ds_spec$name, ntip = ntip, nchar = nchar, + psf = psf, rep = r, elapsed_s = round(elapsed, 1), + best_score = best, n_replicates = nrep, stop = stop_reason, + stringsAsFactors = FALSE + ) + + cat(sprintf(" psf=%d rep=%d: %.0fs, score=%.0f, reps=%s [%s]\n", + psf, r, elapsed, best, + ifelse(is.na(nrep), "?", as.character(nrep)), + stop_reason)) + } + } + } +} + +results_df <- do.call(rbind, results) +cat("\n\n=== Did PSF ever fire? ===\n") +psf_fired <- results_df[results_df$psf > 0 & results_df$stop == "PSF/converged", ] +if (nrow(psf_fired) > 0) { + cat("YES — PSF fired in these cases:\n") + print(psf_fired, row.names = FALSE) +} else { + cat("NO — PSF never fired. All runs ended by maxReplicates or time.\n") +} + +cat("\n=== Full results ===\n") +print(results_df[order(results_df$ntip, results_df$psf, results_df$rep), ], + row.names = FALSE) + +write.csv(results_df, "dev/benchmarks/results_perturb_stop_v3.csv", row.names = FALSE) diff --git a/dev/benchmarks/bench_phase_yield.R b/dev/benchmarks/bench_phase_yield.R new file mode 100644 index 000000000..44cb46fbf --- /dev/null +++ b/dev/benchmarks/bench_phase_yield.R @@ -0,0 +1,86 @@ +# Phase-yield diagnosis (Phase 1) — where does TreeSearch spend its search? +# +# Uses existing instrumentation (no new build needed): +# * attr(result, "timings") per-phase cumulative wall-clock (ms) +# * attr(result, "candidates_evaluated") total TBR/SPR candidates (Phase 0a) +# * attr(result, "last_improved_rep") replicate that last improved the best +# +# Localises the candidates-per-improvement gap to a phase BEFORE building any +# Phase 2 lever: which phase eats the wall-clock, and does the search keep +# improving late (effort well spent) or plateau early (effort wasted)? +# +# Sectorial = xss + rss + css. apples-to-apples Fitch (-> "?"), nThreads = 1. +# +# Env: TS_LIB (.agent-p0), TS_DATASETS, TS_SEEDS, TS_SECONDS (budget), OUT_CSV + +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-p0"), + winslash = "/")) + library(TreeTools) +}) + +secs <- as.double(Sys.getenv("TS_SECONDS", "30")) +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +out_csv <- Sys.getenv("OUT_CSV", "dev/benchmarks/phase_yield_latest.csv") +dsNames <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Eklund2004 Zanol2014 Zhu2013 Giles2015 Dikow2009")), + "\\s+")[[1]] + +data("inapplicable.phyData", package = "TreeSearch") +fitch_convert <- function(phy) { + m <- PhyDatToMatrix(phy, ambigNA = FALSE) + m[m == "-"] <- "?" + MatrixToPhyDat(m) +} + +rows <- list() +for (nm in dsNames) { + fitch <- fitch_convert(inapplicable.phyData[[nm]]) + for (sd in seeds) { + set.seed(sd) + r <- suppressWarnings(MaximizeParsimony( + fitch, maxReplicates = 9999L, maxSeconds = secs, nThreads = 1L, + strategy = "auto", verbosity = 0L)) + tm <- attr(r, "timings") + g <- function(k) if (is.null(tm[[k]]) || is.na(tm[[k]])) 0 else tm[[k]] + sect <- g("xss_ms") + g("rss_ms") + g("css_ms") + total_ms <- sum(unlist(tm), na.rm = TRUE) + reps <- attr(r, "replicates") + rows[[length(rows) + 1]] <- data.frame( + dataset = nm, tips = length(fitch), seed = sd, + score = attr(r, "score"), + cand = attr(r, "candidates_evaluated"), + reps = reps, + last_improved = attr(r, "last_improved_rep"), + # fraction of replicates AFTER the last improvement (= wasted effort) + late_frac = round(1 - attr(r, "last_improved_rep") / max(reps, 1), 2), + pct_wagner = round(100 * g("wagner_ms") / total_ms), + pct_initial_tbr = round(100 * g("tbr_ms") / total_ms), + pct_sector = round(100 * sect / total_ms), + pct_ratchet = round(100 * g("ratchet_ms") / total_ms), + pct_final_tbr = round(100 * g("final_tbr_ms") / total_ms), + pct_fuse = round(100 * g("fuse_ms") / total_ms), + stringsAsFactors = FALSE) + } +} +res <- do.call(rbind, rows) + +agg <- do.call(rbind, lapply(split(res, res$dataset), function(d) { + data.frame( + dataset = d$dataset[1], tips = d$tips[1], + score_med = median(d$score), cand_med = median(d$cand), + reps_med = median(d$reps), late_frac_med = median(d$late_frac), + sector = median(d$pct_sector), ratchet = median(d$pct_ratchet), + final_tbr = median(d$pct_final_tbr), init_tbr = median(d$pct_initial_tbr), + fuse = median(d$pct_fuse), wagner = median(d$pct_wagner), + stringsAsFactors = FALSE) +})) +agg <- agg[order(-agg$cand_med), ] +cat(sprintf("Phase-yield | %d datasets | seeds {%s} | %gs | nThreads=1\n", + length(dsNames), paste(seeds, collapse = ","), secs)) +cat("(phase columns = %% of wall-clock; late_frac = fraction of reps after last improvement)\n") +cat(strrep("-", 96), "\n") +print(agg, row.names = FALSE) +dir.create(dirname(out_csv), showWarnings = FALSE, recursive = TRUE) +write.csv(res, out_csv, row.names = FALSE) +cat(sprintf("\nPer-run rows written to %s\n", out_csv)) diff --git a/dev/benchmarks/bench_pr_stage2_mbank.R b/dev/benchmarks/bench_pr_stage2_mbank.R new file mode 100644 index 000000000..1178653d7 --- /dev/null +++ b/dev/benchmarks/bench_pr_stage2_mbank.R @@ -0,0 +1,212 @@ +#!/usr/bin/env Rscript +# T-289c: Prune-reinsert Stage 2 — mbank_X30754 (180t) only, Brazeau scoring +# +# DESIGNED FOR HAMILTON HPC. Do not run locally. +# +# Stage 1 (13 configs × 5 datasets × 5 seeds × 30s) showed: +# - ≤88t: PR is net-negative (replicate cost >> score gain). No further testing. +# - 180t: Real signal. Best configs by mean delta vs baseline: +# pr_c3_d10: −8.0 (4/5 seeds), pr_c5_d10: −6.6 (5/5 seeds, most consistent) +# pr_c5_d05: −6.8 (4/5), pr_c3_d05: −4.8 (3/5) +# pr_c1_d10: −2.8 (3/5) — weak but cheap +# d≥20% with c≥3 rarely completes a replicate in 30s. +# +# Stage 2 goals: +# 1. Confirm signal at 60s (≥2 completed replicates per seed). +# 2. Narrow to best cycle/drop combination. +# 3. Test selection=1 (greedy insertion) for top-2 configs. +# +# Configs tested (8 + baseline = 9 total): +# baseline, pr_c1_d10, +# pr_c3_d05, pr_c3_d10, pr_c3_d10_sel1, +# pr_c5_d05, pr_c5_d10, pr_c5_d10_sel1 +# +# Grid: 9 configs × 1 dataset × 10 seeds × 60s ≈ 90 min wall time. +# +# Usage: +# Rscript bench_pr_stage2_mbank.R [timeout_s] [output_dir] +# timeout_s: search budget in seconds. Default: 60 +# output_dir: where to write CSV. Default: "." +# +# Output: t289c_stage2_{timeout}s.csv + +library(TreeSearch) +library(TreeTools) + +args <- commandArgs(trailingOnly = TRUE) +timeout_s <- if (length(args) >= 1) as.integer(args[1]) else 60L +output_dir <- if (length(args) >= 2) args[2] else "." + +cat("=== T-289c: Prune-Reinsert Stage 2 (mbank, Brazeau) ===\n") +cat(sprintf("Timeout: %ds | TreeSearch %s\n", timeout_s, + packageVersion("TreeSearch"))) +cat(sprintf("Output: %s\n", output_dir)) +cat(sprintf("Started: %s\n\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) + +# ---- Load 180-tip dataset ---- +mbank_path <- Sys.glob("/nobackup/*/TreeSearch-a/dev/benchmarks/mbank_X30754.nex") +if (length(mbank_path) == 0) { + mbank_path <- file.path(dirname(dirname(dirname(getwd()))), + "TreeSearch-a", "dev", "benchmarks", "mbank_X30754.nex") +} +if (length(mbank_path) > 0) mbank_path <- mbank_path[1] +if (!file.exists(mbank_path)) stop("mbank_X30754.nex not found") +cat("Loading:", mbank_path, "\n") +ds <- ReadAsPhyDat(mbank_path) +cat(sprintf(" %d taxa, %d patterns\n\n", length(ds), sum(attr(ds, "weight")))) + +seeds <- 1:10 + +# ---- Config grid ---- +# +# Stage 1 top performers (all random selection, pr_selection=0): +# pr_c3_d10: mean delta −8.0, 4/5 seeds improved +# pr_c5_d10: mean delta −6.6, 5/5 seeds improved ← most consistent +# pr_c5_d05: mean delta −6.8, 4/5 +# pr_c3_d05: mean delta −4.8, 3/5 +# pr_c1_d10: mean delta −2.8, 3/5 — cheap reference +# +# Also test selection=1 (greedy insertion) for the top-2 configs. +configs <- list( + baseline = list( + label = "baseline", + desc = "No prune-reinsert (auto preset)", + pr_cycles = 0L, pr_drop = 0.0, pr_selection = 0L + ), + pr_c1_d10 = list( + label = "pr_c1_d10", + desc = "PR 1 cycle, 10% drop, random", + pr_cycles = 1L, pr_drop = 0.10, pr_selection = 0L + ), + pr_c3_d05 = list( + label = "pr_c3_d05", + desc = "PR 3 cycles, 5% drop, random", + pr_cycles = 3L, pr_drop = 0.05, pr_selection = 0L + ), + pr_c3_d10 = list( + label = "pr_c3_d10", + desc = "PR 3 cycles, 10% drop, random", + pr_cycles = 3L, pr_drop = 0.10, pr_selection = 0L + ), + pr_c3_d10_sel1 = list( + label = "pr_c3_d10_sel1", + desc = "PR 3 cycles, 10% drop, greedy insertion", + pr_cycles = 3L, pr_drop = 0.10, pr_selection = 1L + ), + pr_c5_d05 = list( + label = "pr_c5_d05", + desc = "PR 5 cycles, 5% drop, random", + pr_cycles = 5L, pr_drop = 0.05, pr_selection = 0L + ), + pr_c5_d10 = list( + label = "pr_c5_d10", + desc = "PR 5 cycles, 10% drop, random", + pr_cycles = 5L, pr_drop = 0.10, pr_selection = 0L + ), + pr_c5_d10_sel1 = list( + label = "pr_c5_d10_sel1", + desc = "PR 5 cycles, 10% drop, greedy insertion", + pr_cycles = 5L, pr_drop = 0.10, pr_selection = 1L + ) +) + +total_runs <- length(configs) * length(seeds) +cat(sprintf("Configs: %d, Seeds: %d -> %d total runs\n\n", + length(configs), length(seeds), total_runs)) + +# ---- Run experiments ---- +results <- data.frame( + dataset = character(), n_tips = integer(), n_patterns = integer(), + config = character(), seed = integer(), timeout_s = integer(), + score = numeric(), n_trees = integer(), replicates = integer(), + hits = integer(), wall_s = numeric(), + pr_cycles = integer(), pr_drop = numeric(), pr_selection = integer(), + stringsAsFactors = FALSE +) + +ntip <- length(ds) +npat <- sum(attr(ds, "weight")) +run_idx <- 0L + +for (cfg_name in names(configs)) { + cfg <- configs[[cfg_name]] + cat(sprintf("\n--- %s: %s ---\n", cfg$label, cfg$desc)) + + for (s in seeds) { + run_idx <- run_idx + 1L + cat(sprintf(" [%d/%d] seed=%d ... ", run_idx, total_runs, s)) + + set.seed(s) + t0 <- proc.time() + + tryCatch({ + if (cfg$pr_cycles == 0L) { + res <- MaximizeParsimony( + ds, + maxSeconds = timeout_s, + strategy = "auto", + consensusStableReps = 0L, + nniPerturbCycles = 0L, + driftCycles = 0L, + verbosity = 0L, + nThreads = 1L + ) + } else { + res <- MaximizeParsimony( + ds, + maxSeconds = timeout_s, + strategy = "auto", + pruneReinsertCycles = cfg$pr_cycles, + pruneReinsertDrop = cfg$pr_drop, + pruneReinsertSelection = cfg$pr_selection, + consensusStableReps = 0L, + nniPerturbCycles = 0L, + driftCycles = 0L, + verbosity = 0L, + nThreads = 1L + ) + } + + elapsed <- (proc.time() - t0)[3] + best_score <- attr(res, "score") + n_trees <- length(res) + reps <- attr(res, "replicates") + hits <- attr(res, "hits") + + cat(sprintf("score=%g, reps=%d, %.1fs\n", best_score, reps, elapsed)) + + results <- rbind(results, data.frame( + dataset = "mbank_X30754", n_tips = ntip, n_patterns = npat, + config = cfg$label, seed = s, timeout_s = timeout_s, + score = best_score, n_trees = n_trees, replicates = reps, + hits = hits, wall_s = elapsed, + pr_cycles = cfg$pr_cycles, pr_drop = cfg$pr_drop, + pr_selection = cfg$pr_selection, + stringsAsFactors = FALSE + )) + }, error = function(e) { + cat(sprintf("ERROR: %s\n", conditionMessage(e))) + }) + } + + # Save after each config (crash recovery) + outfile <- file.path(output_dir, + sprintf("t289c_stage2_%ds.csv", timeout_s)) + write.csv(results, outfile, row.names = FALSE) +} + +# ---- Save final ---- +outfile <- file.path(output_dir, sprintf("t289c_stage2_%ds.csv", timeout_s)) +write.csv(results, outfile, row.names = FALSE) +cat(sprintf("\n=== Results written to %s (%d rows) ===\n", + outfile, nrow(results))) + +# ---- Quick summary ---- +cat("\n--- Mean scores by config ---\n") +agg <- aggregate(score ~ config, data = results, FUN = mean) +bl <- agg$score[agg$config == "baseline"] +agg$delta <- round(agg$score - bl, 2) +agg <- agg[order(agg$delta), ] +print(agg, row.names = FALSE) + +cat(sprintf("\nCompleted: %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) diff --git a/dev/benchmarks/bench_pr_stage3_mbank.R b/dev/benchmarks/bench_pr_stage3_mbank.R new file mode 100644 index 000000000..9eff3c5f6 --- /dev/null +++ b/dev/benchmarks/bench_pr_stage3_mbank.R @@ -0,0 +1,192 @@ +#!/usr/bin/env Rscript +# T-289d: Prune-reinsert Stage 3 — new drop criteria (MISSING, COMBINED) +# +# DESIGNED FOR HAMILTON HPC. Do not run locally. +# +# Stage 2 (9 configs x 10 seeds x 60s, mbank_X30754) established: +# - All PR configs improve over baseline at 180t. +# - Instability-weighted dropping (sel=1) beats random (sel=0) by 1.8–3.3 steps. +# - pr_c5_d05 (−12.3 steps, 3.0 reps) best cost-quality ratio at sel=0. +# - pr_c5_d10_sel1 (−14.1 steps, 2.2 reps) best overall. +# - Gap: pr_c5_d05_sel1 not tested. +# +# Stage 3 goals: +# 1. Fill gap: pr_c5_d05_sel1 (instability-weighted at cheapest good config). +# 2. Benchmark new criteria: MISSING (sel=2), COMBINED (sel=3) at d05 and d10. +# 3. Reference repeats: baseline + pr_c5_d05_sel0 + pr_c5_d10_sel1 for +# within-run comparability (avoids cross-run seed variance). +# +# Grid: 8 configs × 1 dataset × 10 seeds × 60s ≈ 87 min wall time. +# +# Drop criteria (pruneReinsertSelection): +# 0 = RANDOM uniform random +# 1 = INSTABILITY weighted by positional instability in pool +# 2 = MISSING weighted by ambiguous/inapplicable character count +# 3 = COMBINED instability × (1 + normalised missingness) +# +# Usage: +# Rscript bench_pr_stage3_mbank.R [timeout_s] [output_dir] + +library(TreeSearch) +library(TreeTools) + +args <- commandArgs(trailingOnly = TRUE) +timeout_s <- if (length(args) >= 1) as.integer(args[1]) else 60L +output_dir <- if (length(args) >= 2) args[2] else "." + +cat("=== T-289d: Prune-Reinsert Stage 3 (new criteria, mbank, Brazeau) ===\n") +cat(sprintf("Timeout: %ds | TreeSearch %s\n", timeout_s, + packageVersion("TreeSearch"))) +cat(sprintf("Output: %s\n", output_dir)) +cat(sprintf("Started: %s\n\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) + +# ---- Load 180-tip dataset ---- +mbank_path <- Sys.glob("/nobackup/*/TreeSearch-a/dev/benchmarks/mbank_X30754.nex") +if (length(mbank_path) == 0) { + mbank_path <- file.path(dirname(dirname(dirname(getwd()))), + "TreeSearch-a", "dev", "benchmarks", "mbank_X30754.nex") +} +if (length(mbank_path) > 0) mbank_path <- mbank_path[1] +if (!file.exists(mbank_path)) stop("mbank_X30754.nex not found") +cat("Loading:", mbank_path, "\n") +ds <- ReadAsPhyDat(mbank_path) +cat(sprintf(" %d taxa, %d patterns\n\n", length(ds), sum(attr(ds, "weight")))) + +seeds <- 1:10 + +# ---- Config grid ---- +# +# Notation: pr_c{cycles}_d{drop%}_sel{selection} +# References from Stage 2 included for within-run comparability. +configs <- list( + baseline = list( + label = "baseline", desc = "No prune-reinsert", + pr_cycles = 0L, pr_drop = 0.0, pr_selection = 0L + ), + # --- d=5%, c=5: cheapest good config from Stage 2 --- + pr_c5_d05_sel0 = list( + label = "pr_c5_d05_sel0", desc = "c5 d5% random (Stage2 ref)", + pr_cycles = 5L, pr_drop = 0.05, pr_selection = 0L + ), + pr_c5_d05_sel1 = list( + label = "pr_c5_d05_sel1", desc = "c5 d5% instability (gap)", + pr_cycles = 5L, pr_drop = 0.05, pr_selection = 1L + ), + pr_c5_d05_sel2 = list( + label = "pr_c5_d05_sel2", desc = "c5 d5% missing (new)", + pr_cycles = 5L, pr_drop = 0.05, pr_selection = 2L + ), + pr_c5_d05_sel3 = list( + label = "pr_c5_d05_sel3", desc = "c5 d5% combined (new)", + pr_cycles = 5L, pr_drop = 0.05, pr_selection = 3L + ), + # --- d=10%, c=5: Stage 2 overall winner config --- + pr_c5_d10_sel1 = list( + label = "pr_c5_d10_sel1", desc = "c5 d10% instability (Stage2 ref)", + pr_cycles = 5L, pr_drop = 0.10, pr_selection = 1L + ), + pr_c5_d10_sel2 = list( + label = "pr_c5_d10_sel2", desc = "c5 d10% missing (new)", + pr_cycles = 5L, pr_drop = 0.10, pr_selection = 2L + ), + pr_c5_d10_sel3 = list( + label = "pr_c5_d10_sel3", desc = "c5 d10% combined (new)", + pr_cycles = 5L, pr_drop = 0.10, pr_selection = 3L + ) +) + +total_runs <- length(configs) * length(seeds) +cat(sprintf("Configs: %d, Seeds: %d -> %d total runs\n\n", + length(configs), length(seeds), total_runs)) + +# ---- Run experiments ---- +results <- data.frame( + dataset = character(), n_tips = integer(), n_patterns = integer(), + config = character(), seed = integer(), timeout_s = integer(), + score = numeric(), n_trees = integer(), replicates = integer(), + hits = integer(), wall_s = numeric(), + pr_cycles = integer(), pr_drop = numeric(), pr_selection = integer(), + stringsAsFactors = FALSE +) + +ntip <- length(ds) +npat <- sum(attr(ds, "weight")) +run_idx <- 0L +outfile <- file.path(output_dir, sprintf("t289d_stage3_%ds.csv", timeout_s)) + +for (cfg_name in names(configs)) { + cfg <- configs[[cfg_name]] + cat(sprintf("\n--- %s: %s ---\n", cfg$label, cfg$desc)) + + for (s in seeds) { + run_idx <- run_idx + 1L + cat(sprintf(" [%d/%d] seed=%d ... ", run_idx, total_runs, s)) + + set.seed(s) + t0 <- proc.time() + + tryCatch({ + if (cfg$pr_cycles == 0L) { + res <- MaximizeParsimony( + ds, + maxSeconds = timeout_s, + strategy = "auto", + consensusStableReps = 0L, + nniPerturbCycles = 0L, + driftCycles = 0L, + verbosity = 0L, + nThreads = 1L + ) + } else { + res <- MaximizeParsimony( + ds, + maxSeconds = timeout_s, + strategy = "auto", + pruneReinsertCycles = cfg$pr_cycles, + pruneReinsertDrop = cfg$pr_drop, + pruneReinsertSelection = cfg$pr_selection, + consensusStableReps = 0L, + nniPerturbCycles = 0L, + driftCycles = 0L, + verbosity = 0L, + nThreads = 1L + ) + } + + elapsed <- (proc.time() - t0)[3] + best_score <- attr(res, "score") + reps <- attr(res, "replicates") + hits <- attr(res, "hits") + + cat(sprintf("score=%g, reps=%d, %.1fs\n", best_score, reps, elapsed)) + + results <- rbind(results, data.frame( + dataset = "mbank_X30754", n_tips = ntip, n_patterns = npat, + config = cfg$label, seed = s, timeout_s = timeout_s, + score = best_score, n_trees = length(res), replicates = reps, + hits = hits, wall_s = elapsed, + pr_cycles = cfg$pr_cycles, pr_drop = cfg$pr_drop, + pr_selection = cfg$pr_selection, + stringsAsFactors = FALSE + )) + }, error = function(e) { + cat(sprintf("ERROR: %s\n", conditionMessage(e))) + }) + } + + # Save after each config (crash recovery) + write.csv(results, outfile, row.names = FALSE) +} + +write.csv(results, outfile, row.names = FALSE) +cat(sprintf("\n=== Results written to %s (%d rows) ===\n", outfile, nrow(results))) + +# ---- Quick summary ---- +cat("\n--- Mean delta vs baseline ---\n") +bl_mean <- mean(results$score[results$config == "baseline"]) +agg <- aggregate(score ~ config + pr_selection, data = results, FUN = mean) +agg$delta <- round(agg$score - bl_mean, 2) +agg <- agg[order(agg$delta), ] +print(agg, row.names = FALSE) + +cat(sprintf("\nCompleted: %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) diff --git a/dev/benchmarks/bench_pr_stage4_validation.R b/dev/benchmarks/bench_pr_stage4_validation.R new file mode 100644 index 000000000..6199f03c2 --- /dev/null +++ b/dev/benchmarks/bench_pr_stage4_validation.R @@ -0,0 +1,161 @@ +# bench_pr_stage4_validation.R +# +# T-289: Prune-reinsert Stage 4 — multi-dataset validation at large-tree scale +# +# DESIGNED FOR HAMILTON HPC. Do not run locally. +# +# Stage 3 (mbank_X30754, 180t, 10 seeds, 60s) confirmed: +# MISSING criterion (sel=2) best or tied at d=5% and d=10%: +# pr_c5_d05_sel2: mean delta -14.7 (SE 5.9, 3.0 reps) +# PR enabled in large preset: c=5, d=5%, sel=MISSING +# +# Stage 4 goals: +# 1. Verify PR benefit generalises across 5 independent large matrices +# spanning 131-206 tips. +# 2. Check whether benefit persists or baseline catches up at 120s budget. +# +# Datasets (all training-split MorphoBank): +# mbank_X30754: 180t, 425p, 20% inapp (anchor — Stage 2/3 calibration) +# project4133: 131t, 349p, 6% inapp +# project3701: 146t, 324p, 15% inapp +# project804: 173t, 569p, 31% inapp +# syab07205: 206t, 748p, 4% inapp +# +# Configs (2): +# baseline: large preset, pruneReinsertCycles = 0 (no PR) +# pr_large: large preset, pruneReinsertCycles = 5, drop = 0.05, sel = MISSING (2) +# +# Grid: 5 datasets × 2 configs × 2 budgets × 10 seeds = 200 runs +# Expected wall time: ~5-6h on a single Hamilton node. +# +# Usage: +# Rscript bench_pr_stage4_validation.R [output_dir] +# output_dir: where to write CSV. Default: "." +# +# Output: t289e_stage4_validation.csv + +suppressPackageStartupMessages({ + library(TreeSearch) + library(TreeTools) +}) + +args <- commandArgs(trailingOnly = TRUE) +output_dir <- if (length(args) >= 1) args[1] else "." + +cat("=== T-289e: Prune-Reinsert Stage 4 Validation ===\n") +cat(sprintf("TreeSearch %s\n", packageVersion("TreeSearch"))) +cat(sprintf("Output: %s\n", output_dir)) +cat(sprintf("Started: %s\n\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) + +# ---- Dataset definitions ---- +neotrans_dir <- Sys.glob("/nobackup/*/neotrans/inst/matrices") +if (length(neotrans_dir) == 0) { + # Fallback: sibling of TreeSearch-a + neotrans_dir <- file.path(dirname(dirname(dirname(getwd()))), + "neotrans", "inst", "matrices") +} +neotrans_dir <- neotrans_dir[1] +if (!dir.exists(neotrans_dir)) stop("neotrans matrices directory not found: ", neotrans_dir) + +mbank_path <- Sys.glob("/nobackup/*/TreeSearch-a/dev/benchmarks/mbank_X30754.nex") +if (length(mbank_path) == 0) { + mbank_path <- file.path(dirname(dirname(dirname(getwd()))), + "TreeSearch-a", "dev", "benchmarks", "mbank_X30754.nex") +} +mbank_path <- mbank_path[1] +if (!file.exists(mbank_path)) stop("mbank_X30754.nex not found") + +dataset_defs <- list( + list(key = "mbank_X30754", path = mbank_path), + list(key = "project4133", path = file.path(neotrans_dir, "project4133.nex")), + list(key = "project3701", path = file.path(neotrans_dir, "project3701.nex")), + list(key = "project804", path = file.path(neotrans_dir, "project804.nex")), + list(key = "syab07205", path = file.path(neotrans_dir, "syab07205.nex")) +) + +# ---- Config grid ---- +sc_baseline <- SearchControl(pruneReinsertCycles = 0L) +sc_pr_large <- SearchControl( + pruneReinsertCycles = 5L, + pruneReinsertDrop = 0.05, + pruneReinsertSelection = 2L +) + +configs <- list( + baseline = sc_baseline, + pr_large = sc_pr_large +) + +budgets <- c(60L, 120L) +seeds <- 1:10 + +# ---- Output ---- +out_file <- file.path(output_dir, "t289e_stage4_validation.csv") +out_cols <- c("dataset","n_tips","n_patterns","config","seed","timeout_s", + "score","n_trees","replicates","hits","wall_s", + "pr_cycles","pr_drop","pr_selection") +write(paste(shQuote(out_cols), collapse = ","), out_file) + +total_runs <- length(dataset_defs) * length(configs) * length(budgets) * length(seeds) +cat(sprintf("Total runs: %d\n\n", total_runs)) +run_i <- 0L + +for (ddef in dataset_defs) { + cat(sprintf("--- Loading: %s ---\n", ddef$key)) + ds <- tryCatch(ReadAsPhyDat(ddef$path), error = function(e) { + cat(sprintf(" ERROR loading %s: %s\n", ddef$key, e$message)) + NULL + }) + if (is.null(ds)) next + n_tips <- length(ds) + n_patterns <- sum(attr(ds, "weight")) + cat(sprintf(" %d taxa, %d patterns\n\n", n_tips, n_patterns)) + + for (budget in budgets) { + for (cfg_name in names(configs)) { + sc <- configs[[cfg_name]] + for (seed in seeds) { + run_i <- run_i + 1L + cat(sprintf("[%d/%d] %s | %s | budget=%ds | seed=%d ... ", + run_i, total_runs, ddef$key, cfg_name, budget, seed)) + t0 <- proc.time()[["elapsed"]] + + res <- tryCatch( + MaximizeParsimony( + dataset = ds, + maxSeconds = budget, + nThreads = 2L, + seed = seed, + verbosity = 0L, + control = sc + ), + error = function(e) { + cat(sprintf("ERROR: %s\n", e$message)) + NULL + } + ) + + wall_s <- proc.time()[["elapsed"]] - t0 + if (is.null(res)) next + + score <- attr(res, "score") + n_trees <- length(res) + replicates <- attr(res, "replicates") + hits <- attr(res, "hits") + + pr_cycles <- if (!is.null(sc$pruneReinsertCycles)) sc$pruneReinsertCycles else 0L + pr_drop <- if (!is.null(sc$pruneReinsertDrop)) sc$pruneReinsertDrop else 0.05 + pr_sel <- if (!is.null(sc$pruneReinsertSelection)) sc$pruneReinsertSelection else 0L + + row <- sprintf('%s,%d,%d,%s,%d,%d,%g,%d,%d,%d,%.3f,%d,%.2f,%d', + shQuote(ddef$key), n_tips, n_patterns, shQuote(cfg_name), + seed, budget, score, n_trees, replicates, hits, wall_s, + pr_cycles, pr_drop, pr_sel) + write(row, out_file, append = TRUE) + cat(sprintf("score=%g reps=%d wall=%.1fs\n", score, replicates, wall_s)) + } + } + } +} + +cat(sprintf("\nDone. %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) diff --git a/dev/benchmarks/bench_pr_stage5_nni.R b/dev/benchmarks/bench_pr_stage5_nni.R new file mode 100644 index 000000000..cecccf520 --- /dev/null +++ b/dev/benchmarks/bench_pr_stage5_nni.R @@ -0,0 +1,163 @@ +# bench_pr_stage5_nni.R +# +# T-289f: Prune-reinsert Stage 5 — NNI full-tree polish cost reduction +# +# DESIGNED FOR HAMILTON HPC. Do not run locally. +# +# Stage 4 conclusion: PR (TBR full polish) is disqualified for the large preset +# at 60s budget because per-cycle cost is too high (~60s at 206 tips, leaving +# 0 replicates). Stage 5 asks whether NNI full-tree polish (pruneReinsertNni=TRUE, +# ~5x cheaper at large n) restores PR's value. +# +# Hypothesis: PR's benefit comes from topological displacement, not from the +# quality of post-reinsert local search. NNI reaches a local optimum sufficient +# to identify improvements; outer-loop TBR then polishes to full convergence. +# +# Three configs: +# baseline: large preset, pruneReinsertCycles=0 (no PR) +# pr_nni: large preset, c=5, d=5%, MISSING, NNI=TRUE (new cheap option) +# pr_tbr: large preset, c=5, d=5%, MISSING, NNI=FALSE (Stage 4 reference) +# +# Same 5 datasets as Stage 4 (131-206 tips, training-split MorphoBank). +# +# Grid: 5 datasets x 3 configs x 2 budgets x 10 seeds = 300 runs +# Expected wall time: ~4-6h (pr_nni ~5x faster than pr_tbr). +# +# Usage: +# Rscript bench_pr_stage5_nni.R [output_dir] +# output_dir: where to write CSV. Default: "." +# +# Output: t289f_pr_nni_polish.csv + +suppressPackageStartupMessages({ + library(TreeSearch) + library(TreeTools) +}) + +args <- commandArgs(trailingOnly = TRUE) +output_dir <- if (length(args) >= 1) args[1] else "." + +cat("=== T-289f: Prune-Reinsert Stage 5 — NNI Polish ===\n") +cat(sprintf("TreeSearch %s\n", packageVersion("TreeSearch"))) +cat(sprintf("Output: %s\n", output_dir)) +cat(sprintf("Started: %s\n\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) + +# ---- Dataset definitions ---- +neotrans_dir <- Sys.glob("/nobackup/*/neotrans/inst/matrices") +if (length(neotrans_dir) == 0) { + neotrans_dir <- file.path(dirname(dirname(dirname(getwd()))), + "neotrans", "inst", "matrices") +} +neotrans_dir <- neotrans_dir[1] +if (!dir.exists(neotrans_dir)) stop("neotrans matrices directory not found: ", neotrans_dir) + +mbank_path <- Sys.glob("/nobackup/*/TreeSearch-a/dev/benchmarks/mbank_X30754.nex") +if (length(mbank_path) == 0) { + mbank_path <- file.path(dirname(dirname(dirname(getwd()))), + "TreeSearch-a", "dev", "benchmarks", "mbank_X30754.nex") +} +mbank_path <- mbank_path[1] +if (!file.exists(mbank_path)) stop("mbank_X30754.nex not found") + +dataset_defs <- list( + list(key = "mbank_X30754", path = mbank_path), + list(key = "project4133", path = file.path(neotrans_dir, "project4133.nex")), + list(key = "project3701", path = file.path(neotrans_dir, "project3701.nex")), + list(key = "project804", path = file.path(neotrans_dir, "project804.nex")), + list(key = "syab07205", path = file.path(neotrans_dir, "syab07205.nex")) +) + +# ---- Config grid ---- +sc_baseline <- SearchControl( + pruneReinsertCycles = 0L +) +sc_pr_nni <- SearchControl( + pruneReinsertCycles = 5L, + pruneReinsertDrop = 0.05, + pruneReinsertSelection = 2L, # MISSING + pruneReinsertNni = TRUE # new: NNI polish instead of TBR +) +sc_pr_tbr <- SearchControl( + pruneReinsertCycles = 5L, + pruneReinsertDrop = 0.05, + pruneReinsertSelection = 2L, # MISSING + pruneReinsertNni = FALSE # Stage 4 reference: TBR full convergence +) + +configs <- list( + baseline = sc_baseline, + pr_nni = sc_pr_nni, + pr_tbr = sc_pr_tbr +) + +budgets <- c(60L, 120L) +seeds <- 1:10 + +# ---- Output ---- +out_file <- file.path(output_dir, "t289f_pr_nni_polish.csv") +out_cols <- c("dataset", "n_tips", "n_patterns", "config", "seed", "timeout_s", + "score", "n_trees", "replicates", "hits", "wall_s", + "pr_cycles", "pr_nni") +write(paste(shQuote(out_cols), collapse = ","), out_file) + +total_runs <- length(dataset_defs) * length(configs) * length(budgets) * length(seeds) +cat(sprintf("Total runs: %d\n\n", total_runs)) +run_i <- 0L + +for (ddef in dataset_defs) { + cat(sprintf("--- Loading: %s ---\n", ddef$key)) + ds <- tryCatch(ReadAsPhyDat(ddef$path), error = function(e) { + cat(sprintf(" ERROR loading %s: %s\n", ddef$key, e$message)) + NULL + }) + if (is.null(ds)) next + n_tips <- length(ds) + n_patterns <- sum(attr(ds, "weight")) + cat(sprintf(" %d taxa, %d patterns\n\n", n_tips, n_patterns)) + + for (budget in budgets) { + for (cfg_name in names(configs)) { + sc <- configs[[cfg_name]] + for (seed in seeds) { + run_i <- run_i + 1L + cat(sprintf("[%d/%d] %s | %s | budget=%ds | seed=%d ... ", + run_i, total_runs, ddef$key, cfg_name, budget, seed)) + t0 <- proc.time()[["elapsed"]] + + res <- tryCatch( + MaximizeParsimony( + dataset = ds, + maxSeconds = budget, + nThreads = 2L, + seed = seed, + verbosity = 0L, + control = sc + ), + error = function(e) { + cat(sprintf("ERROR: %s\n", e$message)) + NULL + } + ) + + wall_s <- proc.time()[["elapsed"]] - t0 + if (is.null(res)) next + + score <- attr(res, "score") + n_trees <- length(res) + replicates <- attr(res, "replicates") + hits <- attr(res, "hits") + pr_cycles <- if (!is.null(sc$pruneReinsertCycles)) sc$pruneReinsertCycles else 0L + pr_nni_val <- if (!is.null(sc$pruneReinsertNni)) as.integer(sc$pruneReinsertNni) else 0L + + row <- sprintf('%s,%d,%d,%s,%d,%d,%g,%d,%d,%d,%.3f,%d,%d', + shQuote(ddef$key), n_tips, n_patterns, shQuote(cfg_name), + seed, budget, score, n_trees, replicates, hits, wall_s, + pr_cycles, pr_nni_val) + write(row, out_file, append = TRUE) + cat(sprintf("score=%g reps=%d wall=%.1fs\n", score, replicates, wall_s)) + } + } + } +} + +cat(sprintf("\nDone. %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) diff --git a/dev/benchmarks/bench_profile_round2.R b/dev/benchmarks/bench_profile_round2.R new file mode 100644 index 000000000..f2570cea0 --- /dev/null +++ b/dev/benchmarks/bench_profile_round2.R @@ -0,0 +1,181 @@ +# Profiling round 2: Fresh baselines and detailed phase analysis +# Agent F, S-PROF, 2026-03-17 +# +# Run via: Rscript -e "library(TreeSearch, lib.loc='.agent-f'); source('dev/benchmarks/bench_profile_round2.R')" + +library(TreeSearch, lib.loc = ".agent-f") +library(TreeTools) + +# Representative datasets spanning the size range +DATASETS <- c("Vinther2008", "Agnarsson2004", "Zhu2013", "Dikow2009") + +prepare <- function(name) { + ds <- TreeSearch::inapplicable.phyData[[name]] + at <- attributes(ds) + list( + contrast = at$contrast, + tip_data = matrix(unlist(ds, use.names = FALSE), + nrow = length(ds), byrow = TRUE), + weight = at$weight, + levels = at$levels, + n_taxa = length(ds) + ) +} + +# ---- Section 1: End-to-end with timings attribute ---- + +cat("=== Section 1: End-to-end with per-phase timings ===\n\n") + +for (nm in DATASETS) { + ds <- prepare(nm) + cat(sprintf("--- %s (%d tips) ---\n", nm, ds$n_taxa)) + + # 3 runs, take medians + timings_list <- list() + wall_times <- numeric(3) + + for (run in 1:3) { + set.seed(7300 + run) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 5L, + targetHits = 3L, + ratchetCycles = 5L, + driftCycles = 5L, + xssRounds = 1L, + rssRounds = 1L, + cssRounds = 1L, + cssPartitions = 3L, + xssPartitions = 3L, + fuseInterval = 5L, + maxSeconds = 60, + verbosity = 0L, + nThreads = 1L + ) + elapsed <- (proc.time() - t0)[3] + wall_times[run] <- elapsed + timings_list[[run]] <- result$timings + cat(sprintf(" Run %d: %.3fs wall, score=%.0f, reps=%d\n", + run, elapsed, result$best_score, result$replicates)) + } + + # Median wall time + med_wall <- median(wall_times) + # Median per-phase (element-wise) + med_timings <- sapply(names(timings_list[[1]]), function(ph) { + median(sapply(timings_list, function(t) t[[ph]])) + }) + cpp_total <- sum(med_timings) + r_overhead <- med_wall * 1000 - cpp_total + + cat(sprintf("\n Median wall: %.3fs\n", med_wall)) + cat(" Per-phase (median ms):\n") + for (ph in names(med_timings)) { + pct <- if (cpp_total > 0) 100 * med_timings[[ph]] / cpp_total else 0 + cat(sprintf(" %-12s %8.1f ms (%4.1f%%)\n", ph, med_timings[[ph]], pct)) + } + cat(sprintf(" %-12s %8.1f ms (C++ total)\n", "TOTAL", cpp_total)) + cat(sprintf(" %-12s %8.1f ms (R overhead: %.1f%% of wall)\n\n", + "R overhead", r_overhead, 100 * r_overhead / (med_wall * 1000))) +} + +# ---- Section 2: IW comparison ---- + +cat("=== Section 2: IW mode comparison ===\n\n") + +for (nm in c("Vinther2008", "Zhu2013")) { + ds <- prepare(nm) + cat(sprintf("--- %s (%d tips, IW k=10) ---\n", nm, ds$n_taxa)) + + wall_times <- numeric(3) + timings_list <- list() + + for (run in 1:3) { + set.seed(7300 + run) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + concavity = 10.0, + maxReplicates = 5L, + targetHits = 3L, + ratchetCycles = 5L, + driftCycles = 5L, + xssRounds = 1L, + rssRounds = 1L, + cssRounds = 1L, + cssPartitions = 3L, + xssPartitions = 3L, + fuseInterval = 5L, + maxSeconds = 60, + verbosity = 0L, + nThreads = 1L + ) + elapsed <- (proc.time() - t0)[3] + wall_times[run] <- elapsed + timings_list[[run]] <- result$timings + cat(sprintf(" Run %d: %.3fs wall, score=%.2f, reps=%d\n", + run, elapsed, result$best_score, result$replicates)) + } + + med_wall <- median(wall_times) + med_timings <- sapply(names(timings_list[[1]]), function(ph) { + median(sapply(timings_list, function(t) t[[ph]])) + }) + cpp_total <- sum(med_timings) + + cat(sprintf("\n Median wall: %.3fs\n", med_wall)) + cat(" Per-phase (median ms):\n") + for (ph in names(med_timings)) { + pct <- if (cpp_total > 0) 100 * med_timings[[ph]] / cpp_total else 0 + cat(sprintf(" %-12s %8.1f ms (%4.1f%%)\n", ph, med_timings[[ph]], pct)) + } + cat(sprintf(" %-12s %8.1f ms (C++ total)\n\n", "TOTAL", cpp_total)) +} + +# ---- Section 3: Scaling test ---- + +cat("=== Section 3: Scaling — single TBR pass timing ===\n\n") + +for (nm in DATASETS) { + ds <- prepare(nm) + cat(sprintf("--- %s (%d tips) ---\n", nm, ds$n_taxa)) + + # Single replicate, no sectorial/ratchet/drift — just Wagner+TBR + wall_times <- numeric(3) + timings_list <- list() + + for (run in 1:3) { + set.seed(7300 + run) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 1L, + targetHits = 1L, + ratchetCycles = 0L, + driftCycles = 0L, + xssRounds = 0L, + rssRounds = 0L, + cssRounds = 0L, + fuseInterval = 0L, + maxSeconds = 60, + verbosity = 0L, + nThreads = 1L + ) + elapsed <- (proc.time() - t0)[3] + wall_times[run] <- elapsed + timings_list[[run]] <- result$timings + } + + med_wall <- median(wall_times) + med_timings <- sapply(names(timings_list[[1]]), function(ph) { + median(sapply(timings_list, function(t) t[[ph]])) + }) + + cat(sprintf(" Wagner: %6.1f ms\n", med_timings[["wagner"]])) + cat(sprintf(" TBR: %6.1f ms\n", med_timings[["tbr"]])) + cat(sprintf(" Wall: %6.1f ms\n", med_wall * 1000)) + cat(sprintf(" R ovhd: %6.1f ms\n\n", med_wall * 1000 - sum(med_timings))) +} + +cat("=== Profiling complete ===\n") diff --git a/dev/benchmarks/bench_profile_round2b.R b/dev/benchmarks/bench_profile_round2b.R new file mode 100644 index 000000000..97891e4fc --- /dev/null +++ b/dev/benchmarks/bench_profile_round2b.R @@ -0,0 +1,203 @@ +# Profiling round 2b: Drift/ratchet deep dive + scaling +# Agent F, S-PROF, 2026-03-17 + +library(TreeSearch, lib.loc = ".agent-f") +library(TreeTools) + +prepare <- function(name) { + ds <- TreeSearch::inapplicable.phyData[[name]] + at <- attributes(ds) + list( + contrast = at$contrast, + tip_data = matrix(unlist(ds, use.names = FALSE), + nrow = length(ds), byrow = TRUE), + weight = at$weight, + levels = at$levels, + n_taxa = length(ds) + ) +} + +# ---- Section 3: Drift cycle count sensitivity ---- + +cat("=== Section 3: Drift cycle count sensitivity ===\n\n") + +# How does drift time scale with cycle count? +# The question: are we doing too many drift cycles for the benefit? + +for (nm in c("Zhu2013", "Dikow2009")) { + ds <- prepare(nm) + cat(sprintf("--- %s (%d tips) ---\n", nm, ds$n_taxa)) + cat(sprintf(" %-8s %8s %8s %8s %8s\n", "dCycles", "drift_ms", "total_ms", "score", "reps")) + + for (dc in c(0L, 1L, 2L, 3L, 5L, 10L)) { + scores <- numeric(3) + drift_ms <- numeric(3) + total_ms <- numeric(3) + reps <- numeric(3) + + for (run in 1:3) { + set.seed(7300 + run) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 3L, + targetHits = 3L, + ratchetCycles = 5L, + driftCycles = dc, + xssRounds = 1L, + rssRounds = 1L, + cssRounds = 1L, + cssPartitions = 3L, + xssPartitions = 3L, + fuseInterval = 5L, + maxSeconds = 120, + verbosity = 0L, + nThreads = 1L + ) + elapsed <- (proc.time() - t0)[3] + scores[run] <- result$best_score + total_ms[run] <- elapsed * 1000 + drift_ms[run] <- result$timings[["drift_ms"]] + reps[run] <- result$replicates + } + + cat(sprintf(" %-8d %8.0f %8.0f %8.0f %8.0f\n", + dc, median(drift_ms), median(total_ms), + median(scores), median(reps))) + } + cat("\n") +} + +# ---- Section 4: Ratchet cycle count sensitivity ---- + +cat("=== Section 4: Ratchet cycle count sensitivity ===\n\n") + +for (nm in c("Zhu2013", "Dikow2009")) { + ds <- prepare(nm) + cat(sprintf("--- %s (%d tips) ---\n", nm, ds$n_taxa)) + cat(sprintf(" %-8s %8s %8s %8s %8s\n", "rCycles", "ratch_ms", "total_ms", "score", "reps")) + + for (rc in c(0L, 1L, 2L, 3L, 5L, 10L)) { + scores <- numeric(3) + ratch_ms <- numeric(3) + total_ms <- numeric(3) + reps <- numeric(3) + + for (run in 1:3) { + set.seed(7300 + run) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 3L, + targetHits = 3L, + ratchetCycles = rc, + driftCycles = 5L, + xssRounds = 1L, + rssRounds = 1L, + cssRounds = 1L, + cssPartitions = 3L, + xssPartitions = 3L, + fuseInterval = 5L, + maxSeconds = 120, + verbosity = 0L, + nThreads = 1L + ) + elapsed <- (proc.time() - t0)[3] + scores[run] <- result$best_score + total_ms[run] <- elapsed * 1000 + ratch_ms[run] <- result$timings[["ratchet_ms"]] + reps[run] <- result$replicates + } + + cat(sprintf(" %-8d %8.0f %8.0f %8.0f %8.0f\n", + rc, median(ratch_ms), median(total_ms), + median(scores), median(reps))) + } + cat("\n") +} + +# ---- Section 5: CSS effectiveness ---- + +cat("=== Section 5: CSS vs no CSS ===\n\n") + +for (nm in c("Zhu2013", "Dikow2009")) { + ds <- prepare(nm) + cat(sprintf("--- %s (%d tips) ---\n", nm, ds$n_taxa)) + + for (css in c(0L, 1L, 2L)) { + scores <- numeric(3) + css_ms <- numeric(3) + total_ms <- numeric(3) + + for (run in 1:3) { + set.seed(7300 + run) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 3L, + targetHits = 3L, + ratchetCycles = 5L, + driftCycles = 5L, + xssRounds = 1L, + rssRounds = 1L, + cssRounds = css, + cssPartitions = 3L, + xssPartitions = 3L, + fuseInterval = 5L, + maxSeconds = 120, + verbosity = 0L, + nThreads = 1L + ) + elapsed <- (proc.time() - t0)[3] + scores[run] <- result$best_score + css_ms[run] <- result$timings[["css_ms"]] + total_ms[run] <- elapsed * 1000 + } + + cat(sprintf(" cssRounds=%d: css_ms=%6.0f total_ms=%6.0f score=%6.0f\n", + css, median(css_ms), median(total_ms), median(scores))) + } + cat("\n") +} + +# ---- Section 6: Wagner + TBR-only (no perturbation) ---- + +cat("=== Section 6: Wagner + TBR only (scaling) ===\n\n") + +DATASETS <- c("Vinther2008", "Agnarsson2004", "Zhu2013", "Dikow2009") + +for (nm in DATASETS) { + ds <- prepare(nm) + wall_times <- numeric(5) + tbr_ms <- numeric(5) + wagner_ms <- numeric(5) + + for (run in 1:5) { + set.seed(7300 + run) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 1L, + targetHits = 1L, + ratchetCycles = 0L, + driftCycles = 0L, + xssRounds = 0L, + rssRounds = 0L, + cssRounds = 0L, + fuseInterval = 0L, + maxSeconds = 60, + verbosity = 0L, + nThreads = 1L + ) + elapsed <- (proc.time() - t0)[3] + wall_times[run] <- elapsed + tbr_ms[run] <- result$timings[["tbr_ms"]] + wagner_ms[run] <- result$timings[["wagner_ms"]] + } + + cat(sprintf(" %s (%2d tips): Wagner=%5.1f ms, TBR=%7.1f ms, Wall=%7.1f ms\n", + nm, ds$n_taxa, + median(wagner_ms), median(tbr_ms), median(wall_times) * 1000)) +} + +cat("\n=== Profiling complete ===\n") diff --git a/dev/benchmarks/bench_profile_round2c.R b/dev/benchmarks/bench_profile_round2c.R new file mode 100644 index 000000000..8ad786920 --- /dev/null +++ b/dev/benchmarks/bench_profile_round2c.R @@ -0,0 +1,179 @@ +# Profiling round 2c: Parallel scaling + quality impact of reduced cycles +# Agent F, S-PROF, 2026-03-17 + +library(TreeSearch, lib.loc = ".agent-f") +library(TreeTools) + +prepare <- function(name) { + ds <- TreeSearch::inapplicable.phyData[[name]] + at <- attributes(ds) + list( + contrast = at$contrast, + tip_data = matrix(unlist(ds, use.names = FALSE), + nrow = length(ds), byrow = TRUE), + weight = at$weight, + levels = at$levels, + n_taxa = length(ds) + ) +} + +# ---- Section 7: Quality impact with more statistical power ---- + +cat("=== Section 7: Drift/ratchet tuning — quality impact (10 seeds) ===\n\n") + +run_config <- function(ds, drift, ratchet, seed) { + set.seed(seed) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 5L, + targetHits = 3L, + ratchetCycles = ratchet, + driftCycles = drift, + xssRounds = 1L, + rssRounds = 1L, + cssRounds = 1L, + cssPartitions = 3L, + xssPartitions = 3L, + fuseInterval = 5L, + maxSeconds = 120, + verbosity = 0L, + nThreads = 1L + ) + elapsed <- (proc.time() - t0)[3] + c(score = unname(result$best_score), time = unname(elapsed), reps = unname(result$replicates)) +} + +configs <- list( + "d5_r5" = c(drift = 5, ratchet = 5), # current default + "d2_r2" = c(drift = 2, ratchet = 2), # reduced + "d2_r5" = c(drift = 2, ratchet = 5), # drift only reduced + "d5_r2" = c(drift = 5, ratchet = 2), # ratchet only reduced + "d0_r5" = c(drift = 0, ratchet = 5), # no drift + "d5_r0" = c(drift = 5, ratchet = 0) # no ratchet +) + +seeds <- 7301:7310 + +for (nm in c("Zhu2013", "Dikow2009")) { + ds <- prepare(nm) + cat(sprintf("--- %s (%d tips, 10 seeds) ---\n", nm, ds$n_taxa)) + cat(sprintf(" %-8s %8s %8s %8s %8s %8s\n", + "config", "med_scr", "mean_scr", "min_scr", "med_time", "mean_t")) + + for (cfg_name in names(configs)) { + cfg <- configs[[cfg_name]] + sc <- numeric(length(seeds)) + tm <- numeric(length(seeds)) + for (i in seq_along(seeds)) { + r <- run_config(ds, cfg[["drift"]], cfg[["ratchet"]], seeds[i]) + sc[i] <- r[["score"]] + tm[i] <- r[["time"]] + } + + cat(sprintf(" %-8s %8.0f %8.1f %8.0f %8.1f %8.1f\n", + cfg_name, median(sc), mean(sc), min(sc), + median(tm), mean(tm))) + } + cat("\n") +} + +# ---- Section 8: Parallel scaling ---- + +cat("=== Section 8: Parallel scaling ===\n\n") + +for (nm in c("Zhu2013")) { + ds <- prepare(nm) + cat(sprintf("--- %s (%d tips) ---\n", nm, ds$n_taxa)) + cat(sprintf(" %-10s %8s %8s %8s\n", "nThreads", "time_ms", "score", "reps")) + + for (nt in c(1L, 2L)) { + times <- numeric(3) + scores <- numeric(3) + reps <- numeric(3) + + for (run in 1:3) { + set.seed(7300 + run) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 5L, + targetHits = 5L, + ratchetCycles = 5L, + driftCycles = 5L, + xssRounds = 1L, + rssRounds = 1L, + cssRounds = 1L, + cssPartitions = 3L, + xssPartitions = 3L, + fuseInterval = 5L, + maxSeconds = 120, + verbosity = 0L, + nThreads = nt + ) + elapsed <- (proc.time() - t0)[3] + times[run] <- elapsed + scores[run] <- result$best_score + reps[run] <- result$replicates + } + + cat(sprintf(" %-10d %8.0f %8.0f %8.0f\n", + nt, median(times) * 1000, median(scores), median(reps))) + } + cat("\n") +} + +# ---- Section 9: Per-replicate cost breakdown ---- + +cat("=== Section 9: Per-replicate cost (ms/rep) ===\n\n") + +DATASETS <- c("Vinther2008", "Agnarsson2004", "Zhu2013", "Dikow2009") +cat(sprintf(" %-15s %4s %8s %8s %8s %8s %8s %8s %8s\n", + "dataset", "tips", "wagner", "tbr", "sect", "ratch", "drift", "fTBR", "TOTAL")) + +for (nm in DATASETS) { + ds <- prepare(nm) + + all_timings <- list() + all_reps <- numeric(3) + + for (run in 1:3) { + set.seed(7300 + run) + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 5L, + targetHits = 3L, + ratchetCycles = 5L, + driftCycles = 5L, + xssRounds = 1L, + rssRounds = 1L, + cssRounds = 1L, + cssPartitions = 3L, + xssPartitions = 3L, + fuseInterval = 5L, + maxSeconds = 120, + verbosity = 0L, + nThreads = 1L + ) + all_timings[[run]] <- result$timings + all_reps[run] <- result$replicates + } + + med_reps <- median(all_reps) + med_t <- sapply(names(all_timings[[1]]), function(ph) { + median(sapply(all_timings, function(t) t[[ph]])) + }) + + # Per-rep average + pr <- med_t / med_reps + sect <- pr[["xss_ms"]] + pr[["rss_ms"]] + pr[["css_ms"]] + total <- sum(pr) + + cat(sprintf(" %-15s %4d %8.1f %8.1f %8.1f %8.1f %8.1f %8.1f %8.1f\n", + nm, ds$n_taxa, + pr[["wagner_ms"]], pr[["tbr_ms"]], sect, + pr[["ratchet_ms"]], pr[["drift_ms"]], + pr[["final_tbr_ms"]], total)) +} + +cat("\n=== Profiling complete ===\n") diff --git a/dev/benchmarks/bench_prune_reinsert.R b/dev/benchmarks/bench_prune_reinsert.R new file mode 100644 index 000000000..a88a83d90 --- /dev/null +++ b/dev/benchmarks/bench_prune_reinsert.R @@ -0,0 +1,297 @@ +#!/usr/bin/env Rscript +# T-289: Prune-reinsert perturbation benchmark +# +# DESIGNED FOR HAMILTON HPC. Do not run locally (hours of wall time). +# +# Evaluates taxon prune-reinsert (T-266) as perturbation strategy: +# - Does it improve scores vs baseline (no prune-reinsert)? +# - Optimal cycle count (1, 3, 5)? +# - Optimal drop fraction (0.05, 0.10, 0.20, 0.30)? +# - RANDOM vs INSTABILITY tip selection? +# - Complement or substitute for ratchet? +# - Scaling with dataset size (37t → 180t)? +# +# Design: Two-stage grid. +# Stage 1 ("sweep"): coarse grid on 5 datasets, 5 seeds, 30s budget. +# Identifies best cycle count and drop fraction. +# Stage 2 ("confirm"): best configs + baseline on 5 datasets, +# 5 seeds, 30s + 60s budgets. Also tests INSTABILITY selection +# and ratchet-replacement. +# +# Usage: +# Rscript bench_prune_reinsert.R [stage] [timeout_s] [output_dir] +# stage: 1 (sweep) or 2 (confirm). Default: 1 +# timeout_s: search budget in seconds. Default: 30 +# output_dir: where to write CSV results. Default: "." +# +# Output: t289_stage{1,2}_{timeout}s.csv + +library(TreeSearch) +library(TreeTools) + +args <- commandArgs(trailingOnly = TRUE) +stage <- if (length(args) >= 1) as.integer(args[1]) else 1L +timeout_s <- if (length(args) >= 2) as.integer(args[2]) else 30L +output_dir <- if (length(args) >= 3) args[3] else "." + +cat("=== T-289: Prune-Reinsert Benchmark ===\n") +cat(sprintf("Stage: %d, Timeout: %ds\n", stage, timeout_s)) +cat(sprintf("TreeSearch version: %s\n", packageVersion("TreeSearch"))) +cat(sprintf("Output dir: %s\n", output_dir)) +cat(sprintf("Started: %s\n\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) + +# ---- Datasets ---- +# 5 datasets spanning tip-count range, chosen for enough landscape +# difficulty that perturbation quality matters. +bench_names <- c( + "Wortley2006", # 37 tips — small, gap dataset + "Agnarsson2004", # 62 tips — medium + "Zhu2013", # 75 tips — hard, high missing + "Dikow2009" # 88 tips — largest standard +) + +# Convert inapplicable to missing for EW Fitch scoring (match TNT) +fitch_mode <- function(dataset) { + contrast <- attr(dataset, "contrast") + levels <- attr(dataset, "levels") + inapp_col <- match("-", levels) + if (is.na(inapp_col)) return(dataset) + for (i in seq_len(nrow(contrast))) { + if (contrast[i, inapp_col] == 1 && sum(contrast[i, ]) == 1) { + contrast[i, ] <- 1 + } + } + attr(dataset, "contrast") <- contrast + dataset +} + +datasets <- lapply( + setNames(bench_names, bench_names), + function(nm) fitch_mode(inapplicable.phyData[[nm]]) +) + +# Also load 180-tip dataset if available +mbank_path <- file.path(dirname(dirname(getwd())), + "TreeSearch-a", "dev", "benchmarks", + "mbank_X30754.nex") +if (!file.exists(mbank_path)) { + mbank_path <- Sys.glob("/nobackup/*/TreeSearch-a/dev/benchmarks/mbank_X30754.nex") + if (length(mbank_path) > 0) mbank_path <- mbank_path[1] +} +if (length(mbank_path) == 1 && file.exists(mbank_path)) { + cat("Loading 180-tip dataset from:", mbank_path, "\n") + mbank <- fitch_mode(ReadAsPhyDat(mbank_path)) + datasets[["mbank_X30754"]] <- mbank + bench_names <- c(bench_names, "mbank_X30754") +} else { + cat("180-tip dataset not found; skipping mbank_X30754\n") +} + +# TNT reference scores (EW Fitch mode) +tnt_best <- c( + Wortley2006 = 479, Agnarsson2004 = 718, + Zhu2013 = 624, Dikow2009 = 1603, + mbank_X30754 = 1164 +) + +seeds <- 1:5 + +# ---- Baseline control (current auto preset, no prune-reinsert) ---- +make_baseline <- function() { + # No prune-reinsert; everything else at default + SearchControl( + pruneReinsertCycles = 0L, + consensusStableReps = 0L, + nniPerturbCycles = 0L, + driftCycles = 0L + ) +} + +# ---- Build config grid ---- +build_configs <- function(stage) { + cfgs <- list() + + # Baseline: no prune-reinsert + cfgs[["baseline"]] <- list( + label = "baseline", + desc = "No prune-reinsert (auto preset)", + control = NULL # use strategy = "auto" + ) + + if (stage == 1L) { + # Stage 1: sweep cycles × drop_fraction, RANDOM selection only + for (cyc in c(1L, 3L, 5L)) { + for (drop in c(0.05, 0.10, 0.20, 0.30)) { + tag <- sprintf("pr_c%d_d%02d", cyc, round(drop * 100)) + cfgs[[tag]] <- list( + label = tag, + desc = sprintf("PR: %d cycles, %.0f%% drop, random", + cyc, drop * 100), + pr_cycles = cyc, + pr_drop = drop, + pr_selection = 0L # RANDOM + ) + } + } + } else { + # Stage 2: best configs from stage 1 + INSTABILITY + ratchet-replacement + # (Placeholder — human fills in best configs after stage 1 analysis) + # Default stage 2 tests a reasonable spread: + for (cyc in c(1L, 3L)) { + for (drop in c(0.10, 0.20)) { + for (sel in c(0L, 1L)) { + sel_tag <- if (sel == 0L) "rand" else "inst" + tag <- sprintf("pr_c%d_d%02d_%s", cyc, round(drop * 100), sel_tag) + cfgs[[tag]] <- list( + label = tag, + desc = sprintf("PR: %d cycles, %.0f%% drop, %s", + cyc, drop * 100, sel_tag), + pr_cycles = cyc, + pr_drop = drop, + pr_selection = sel + ) + } + } + } + + # Ratchet-replacement: prune-reinsert WITH reduced ratchet + for (cyc in c(3L, 5L)) { + tag <- sprintf("pr_c%d_d10_noratch", cyc) + cfgs[[tag]] <- list( + label = tag, + desc = sprintf("PR: %d cycles, 10%% drop, ratchet halved", cyc), + pr_cycles = cyc, + pr_drop = 0.10, + pr_selection = 0L, + ratchet_override = TRUE # signal to halve ratchet cycles + ) + } + } + + cfgs +} + +configs <- build_configs(stage) +total_runs <- length(configs) * length(datasets) * length(seeds) +cat(sprintf("Configs: %d, Datasets: %d, Seeds: %d -> %d total runs\n\n", + length(configs), length(datasets), length(seeds), total_runs)) + +# ---- Run experiments ---- +results <- data.frame( + dataset = character(), n_tips = integer(), n_patterns = integer(), + config = character(), seed = integer(), timeout_s = integer(), + score = numeric(), n_trees = integer(), replicates = integer(), + hits = integer(), wall_s = numeric(), + pr_cycles = integer(), pr_drop = numeric(), pr_selection = integer(), + ratchet_halved = logical(), + tnt_best = numeric(), gap = numeric(), + stringsAsFactors = FALSE +) + +run_idx <- 0L +for (cfg_name in names(configs)) { + cfg <- configs[[cfg_name]] + cat(sprintf("\n--- Config: %s (%s) ---\n", cfg$label, cfg$desc)) + + for (ds_name in bench_names) { + ds <- datasets[[ds_name]] + ntip <- length(ds) + npat <- sum(attr(ds, "weight")) + + for (s in seeds) { + run_idx <- run_idx + 1L + cat(sprintf(" [%d/%d] %s / %s / seed=%d ... ", + run_idx, total_runs, ds_name, cfg$label, s)) + + set.seed(s) + t0 <- proc.time() + + tryCatch({ + if (cfg_name == "baseline") { + res <- MaximizeParsimony( + ds, + maxSeconds = timeout_s, + strategy = "auto", + consensusStableReps = 0L, + nniPerturbCycles = 0L, + driftCycles = 0L, + verbosity = 0L, + nThreads = 1L + ) + } else { + # Pass prune-reinsert params as ... args so the auto preset still + # governs everything else (ratchetCycles, wagnerStarts, etc.). + # Using control = SearchControl(...) marks ALL fields as explicit, + # which discards the preset — see MaximizeParsimony.R lines 532-543. + extra_args <- list( + ds, + maxSeconds = timeout_s, + strategy = "auto", + pruneReinsertCycles = cfg$pr_cycles, + pruneReinsertDrop = cfg$pr_drop, + pruneReinsertSelection = cfg$pr_selection, + consensusStableReps = 0L, + nniPerturbCycles = 0L, + driftCycles = 0L, + verbosity = 0L, + nThreads = 1L + ) + + # Ratchet-replacement mode: halve ratchet cycles + if (isTRUE(cfg$ratchet_override)) { + extra_args$ratchetCycles <- 6L # halved from preset default 12 + } + + res <- do.call(MaximizeParsimony, extra_args) + } + + elapsed <- (proc.time() - t0)[3] + best_score <- attr(res, "score") + n_trees <- length(res) + reps <- attr(res, "replicates") + hits <- attr(res, "hits") + tnt_ref <- tnt_best[ds_name] + gap <- if (!is.na(tnt_ref)) best_score - tnt_ref else NA_real_ + + cat(sprintf("score=%g, gap=%s, reps=%d, %.1fs\n", + best_score, + if (is.na(gap)) "?" else sprintf("%+d", gap), + reps, elapsed)) + + results <- rbind(results, data.frame( + dataset = ds_name, n_tips = ntip, n_patterns = npat, + config = cfg$label, seed = s, timeout_s = timeout_s, + score = best_score, n_trees = n_trees, replicates = reps, + hits = hits, wall_s = elapsed, + pr_cycles = if (is.null(cfg$pr_cycles)) 0L else cfg$pr_cycles, + pr_drop = if (is.null(cfg$pr_drop)) 0 else cfg$pr_drop, + pr_selection = if (is.null(cfg$pr_selection)) NA_integer_ + else cfg$pr_selection, + ratchet_halved = isTRUE(cfg$ratchet_override), + tnt_best = tnt_ref, gap = gap, + stringsAsFactors = FALSE + )) + }, error = function(e) { + cat(sprintf("ERROR: %s\n", conditionMessage(e))) + }) + } + } +} + +# ---- Save results ---- +outfile <- file.path( + output_dir, + sprintf("t289_stage%d_%ds.csv", stage, timeout_s) +) +write.csv(results, outfile, row.names = FALSE) +cat(sprintf("\n=== Results written to %s (%d rows) ===\n", + outfile, nrow(results))) + +# ---- Quick summary ---- +cat("\n--- Median scores by config × dataset ---\n") +agg <- aggregate(score ~ config + dataset, data = results, FUN = median) +agg_wide <- reshape(agg, direction = "wide", idvar = "config", + timevar = "dataset", v.names = "score") +print(agg_wide, row.names = FALSE) + +cat(sprintf("\nCompleted: %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) diff --git a/dev/benchmarks/bench_prune_reinsert_brazeau.R b/dev/benchmarks/bench_prune_reinsert_brazeau.R new file mode 100644 index 000000000..de365312c --- /dev/null +++ b/dev/benchmarks/bench_prune_reinsert_brazeau.R @@ -0,0 +1,250 @@ +#!/usr/bin/env Rscript +# T-289b: Prune-reinsert perturbation benchmark — Brazeau (default) scoring +# +# DESIGNED FOR HAMILTON HPC. Do not run locally (hours of wall time). +# +# Parallel companion to bench_prune_reinsert.R (Fitch/EW mode). +# Uses TreeSearch's default Brazeau et al. (2019) inapplicable algorithm, +# which is what package users actually experience. +# +# Comparison with TNT absolute scores is NOT valid here (different algorithms). +# Comparisons are: PR config vs baseline, both in Brazeau mode. +# +# Design: same two-stage grid as the Fitch companion. +# Stage 1 ("sweep"): coarse grid on 5 datasets, 5 seeds, 30s budget. +# Stage 2 ("confirm"): best configs from Stage 1, 30s + 60s budgets. +# +# Usage: +# Rscript bench_prune_reinsert_brazeau.R [stage] [timeout_s] [output_dir] +# stage: 1 (sweep) or 2 (confirm). Default: 1 +# timeout_s: search budget in seconds. Default: 30 +# output_dir: where to write CSV results. Default: "." +# +# Output: t289b_stage{1,2}_{timeout}s.csv ("b" = Brazeau mode) + +library(TreeSearch) +library(TreeTools) + +args <- commandArgs(trailingOnly = TRUE) +stage <- if (length(args) >= 1) as.integer(args[1]) else 1L +timeout_s <- if (length(args) >= 2) as.integer(args[2]) else 30L +output_dir <- if (length(args) >= 3) args[3] else "." + +cat("=== T-289b: Prune-Reinsert Benchmark (Brazeau scoring) ===\n") +cat(sprintf("Stage: %d, Timeout: %ds\n", stage, timeout_s)) +cat(sprintf("TreeSearch version: %s\n", packageVersion("TreeSearch"))) +cat(sprintf("Output dir: %s\n", output_dir)) +cat(sprintf("Started: %s\n\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) + +# ---- Datasets (NO fitch_mode conversion; default Brazeau scoring) ---- +bench_names <- c( + "Wortley2006", # 37 tips — small, gap dataset + "Agnarsson2004", # 62 tips — medium + "Zhu2013", # 75 tips — hard, high missing + "Dikow2009" # 88 tips — largest standard +) + +datasets <- lapply( + setNames(bench_names, bench_names), + function(nm) inapplicable.phyData[[nm]] +) + +# Also load 180-tip dataset if available +mbank_path <- file.path(dirname(dirname(getwd())), + "TreeSearch-a", "dev", "benchmarks", + "mbank_X30754.nex") +if (!file.exists(mbank_path)) { + mbank_path <- Sys.glob("/nobackup/*/TreeSearch-a/dev/benchmarks/mbank_X30754.nex") + if (length(mbank_path) > 0) mbank_path <- mbank_path[1] +} +if (length(mbank_path) == 1 && file.exists(mbank_path)) { + cat("Loading 180-tip dataset from:", mbank_path, "\n") + datasets[["mbank_X30754"]] <- ReadAsPhyDat(mbank_path) + bench_names <- c(bench_names, "mbank_X30754") +} else { + cat("180-tip dataset not found; skipping mbank_X30754\n") +} + +seeds <- 1:5 + +# ---- Build config grid (identical to Fitch companion) ---- +build_configs <- function(stage) { + cfgs <- list() + + cfgs[["baseline"]] <- list( + label = "baseline", + desc = "No prune-reinsert (auto preset)", + control = NULL + ) + + if (stage == 1L) { + for (cyc in c(1L, 3L, 5L)) { + for (drop in c(0.05, 0.10, 0.20, 0.30)) { + tag <- sprintf("pr_c%d_d%02d", cyc, round(drop * 100)) + cfgs[[tag]] <- list( + label = tag, + desc = sprintf("PR: %d cycles, %.0f%% drop, random", + cyc, drop * 100), + pr_cycles = cyc, + pr_drop = drop, + pr_selection = 0L + ) + } + } + } else { + for (cyc in c(1L, 3L)) { + for (drop in c(0.10, 0.20)) { + for (sel in c(0L, 1L)) { + sel_tag <- if (sel == 0L) "rand" else "inst" + tag <- sprintf("pr_c%d_d%02d_%s", cyc, round(drop * 100), sel_tag) + cfgs[[tag]] <- list( + label = tag, + desc = sprintf("PR: %d cycles, %.0f%% drop, %s", + cyc, drop * 100, sel_tag), + pr_cycles = cyc, + pr_drop = drop, + pr_selection = sel + ) + } + } + } + + for (cyc in c(3L, 5L)) { + tag <- sprintf("pr_c%d_d10_noratch", cyc) + cfgs[[tag]] <- list( + label = tag, + desc = sprintf("PR: %d cycles, 10%% drop, ratchet halved", cyc), + pr_cycles = cyc, + pr_drop = 0.10, + pr_selection = 0L, + ratchet_override = TRUE + ) + } + } + + cfgs +} + +configs <- build_configs(stage) +total_runs <- length(configs) * length(datasets) * length(seeds) +cat(sprintf("Configs: %d, Datasets: %d, Seeds: %d -> %d total runs\n\n", + length(configs), length(datasets), length(seeds), total_runs)) + +# ---- Run experiments ---- +results <- data.frame( + dataset = character(), n_tips = integer(), n_patterns = integer(), + config = character(), seed = integer(), timeout_s = integer(), + score = numeric(), n_trees = integer(), replicates = integer(), + hits = integer(), wall_s = numeric(), + pr_cycles = integer(), pr_drop = numeric(), pr_selection = integer(), + ratchet_halved = logical(), + stringsAsFactors = FALSE +) + +run_idx <- 0L +for (cfg_name in names(configs)) { + cfg <- configs[[cfg_name]] + cat(sprintf("\n--- Config: %s (%s) ---\n", cfg$label, cfg$desc)) + + for (ds_name in bench_names) { + ds <- datasets[[ds_name]] + ntip <- length(ds) + npat <- sum(attr(ds, "weight")) + + for (s in seeds) { + run_idx <- run_idx + 1L + cat(sprintf(" [%d/%d] %s / %s / seed=%d ... ", + run_idx, total_runs, ds_name, cfg$label, s)) + + set.seed(s) + t0 <- proc.time() + + tryCatch({ + if (cfg_name == "baseline") { + res <- MaximizeParsimony( + ds, + maxSeconds = timeout_s, + strategy = "auto", + consensusStableReps = 0L, + nniPerturbCycles = 0L, + driftCycles = 0L, + verbosity = 0L, + nThreads = 1L + ) + } else { + extra_args <- list( + ds, + maxSeconds = timeout_s, + strategy = "auto", + pruneReinsertCycles = cfg$pr_cycles, + pruneReinsertDrop = cfg$pr_drop, + pruneReinsertSelection = cfg$pr_selection, + consensusStableReps = 0L, + nniPerturbCycles = 0L, + driftCycles = 0L, + verbosity = 0L, + nThreads = 1L + ) + + if (isTRUE(cfg$ratchet_override)) { + extra_args$ratchetCycles <- 6L + } + + res <- do.call(MaximizeParsimony, extra_args) + } + + elapsed <- (proc.time() - t0)[3] + best_score <- attr(res, "score") + n_trees <- length(res) + reps <- attr(res, "replicates") + hits <- attr(res, "hits") + + cat(sprintf("score=%g, reps=%d, %.1fs\n", best_score, reps, elapsed)) + + results <- rbind(results, data.frame( + dataset = ds_name, n_tips = ntip, n_patterns = npat, + config = cfg$label, seed = s, timeout_s = timeout_s, + score = best_score, n_trees = n_trees, replicates = reps, + hits = hits, wall_s = elapsed, + pr_cycles = if (is.null(cfg$pr_cycles)) 0L else cfg$pr_cycles, + pr_drop = if (is.null(cfg$pr_drop)) 0 else cfg$pr_drop, + pr_selection = if (is.null(cfg$pr_selection)) NA_integer_ + else cfg$pr_selection, + ratchet_halved = isTRUE(cfg$ratchet_override), + stringsAsFactors = FALSE + )) + }, error = function(e) { + cat(sprintf("ERROR: %s\n", conditionMessage(e))) + }) + } + } +} + +# ---- Save results ---- +outfile <- file.path( + output_dir, + sprintf("t289b_stage%d_%ds.csv", stage, timeout_s) +) +write.csv(results, outfile, row.names = FALSE) +cat(sprintf("\n=== Results written to %s (%d rows) ===\n", + outfile, nrow(results))) + +# ---- Quick summary ---- +cat("\n--- Median scores by config × dataset ---\n") +agg <- aggregate(score ~ config + dataset, data = results, FUN = median) +agg_wide <- reshape(agg, direction = "wide", idvar = "config", + timevar = "dataset", v.names = "score") +print(agg_wide, row.names = FALSE) + +# ---- Delta vs baseline ---- +cat("\n--- Median delta vs baseline (negative = improvement) ---\n") +bl <- agg[agg$config == "baseline", c("dataset", "score")] +names(bl)[2] <- "baseline_score" +agg2 <- merge(agg[agg$config != "baseline", ], bl, by = "dataset") +agg2$delta <- agg2$score - agg2$baseline_score +delta_wide <- reshape(agg2[, c("config", "dataset", "delta")], + direction = "wide", idvar = "config", + timevar = "dataset", v.names = "delta") +print(delta_wide, row.names = FALSE) + +cat(sprintf("\nCompleted: %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) diff --git a/dev/benchmarks/bench_quartet_concordance.R b/dev/benchmarks/bench_quartet_concordance.R new file mode 100644 index 000000000..05176fdde --- /dev/null +++ b/dev/benchmarks/bench_quartet_concordance.R @@ -0,0 +1,49 @@ +# Benchmark and profile quartet_concordance +# T-298: Profile flat array vs NumericMatrix, int vs double +# +# Usage: Rscript dev/benchmarks/bench_quartet_concordance.R +# Or interactively for profvis output. +# +# Install from source first (tarball build per AGENTS.md). + +library(TreeSearch) + +set.seed(42) + +make_inputs <- function(n_taxa, n_splits, n_chars, n_states = 4) { + splits <- matrix(sample(c(TRUE, FALSE), n_taxa * n_splits, replace = TRUE), + nrow = n_taxa, ncol = n_splits) + # IntegerMatrix with NAs (~5% missing) + chars <- matrix(sample(c(0:(n_states - 1), NA_integer_), + n_taxa * n_chars, replace = TRUE, prob = c(rep(0.95/n_states, n_states), 0.05)), + nrow = n_taxa, ncol = n_chars) + list(splits = splits, chars = chars) +} + +sizes <- list( + small = list(n_taxa = 25, n_splits = 23, n_chars = 50), + medium = list(n_taxa = 100, n_splits = 98, n_chars = 200), + large = list(n_taxa = 300, n_splits = 298, n_chars = 400) +) + +cat("=== Timing quartet_concordance ===\n") +for (nm in names(sizes)) { + sz <- sizes[[nm]] + inp <- make_inputs(sz$n_taxa, sz$n_splits, sz$n_chars) + t <- system.time( + for (i in seq_len(10)) TreeSearch:::quartet_concordance(inp$splits, inp$chars) + ) + cat(sprintf("%-8s (%3d taxa, %3d splits, %3d chars): %.3fs / call\n", + nm, sz$n_taxa, sz$n_splits, sz$n_chars, t[["elapsed"]] / 10)) +} + +# --- profvis profile of the medium case --- +if (requireNamespace("profvis", quietly = TRUE)) { + inp <- make_inputs(100, 98, 200) + p <- profvis::profvis({ + for (i in seq_len(50)) TreeSearch:::quartet_concordance(inp$splits, inp$chars) + }) + print(p) +} else { + message("Install profvis for flame graph: install.packages('profvis')") +} diff --git a/dev/benchmarks/bench_ras_verify.R b/dev/benchmarks/bench_ras_verify.R new file mode 100644 index 000000000..aa869bd51 --- /dev/null +++ b/dev/benchmarks/bench_ras_verify.R @@ -0,0 +1,83 @@ +# Verify (with the now-plumbed rasStarts knob, clean .agent-ratchet build): +# does sector RE-SOLVE close the SHARED-START sectorial gap to TNT? +# +# Same design as bench_sectorial_shared.R: TNT builds ONE RAS+TBR tree T0 (hold 1), +# runs sectsch=rss from it; we read T0 and run OUR sectorial from the SAME T0 with +# ratchet/drift OFF, at rasStarts = 1 (polish) and rasStarts = 3 (re-solve). Scores +# are bitness-independent so local 32-bit TNT is valid. gap = TS_sect - TNT_sect. +# re-solve gap ~ polish gap => re-solve is NOT the missing piece (fidelity is) +# re-solve gap << polish gap => re-solve closes it +# +# Env: TS_LIB (default .agent-ratchet), TNT_EXE, TS_DATASETS, TS_SEEDS, TS_KPASS, OUT_CSV. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +K <- as.integer(Sys.getenv("TS_KPASS", "8")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Zanol2014 Zhu2013 Giles2015")), "\\s+")[[1]] +out_csv <- Sys.getenv("OUT_CSV", "dev/benchmarks/ras_verify.csv") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), "rasverify"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +run_tnt <- function(phy, seed, kpass) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", kpass), "quit;") + writeLines(script, file.path(wd, "ss.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "ss.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + t0 <- tryCatch(ReadTntTree(file.path(wd, "t0.tre")), error = function(e) NULL) + if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + list(t0 = t0, s_sect = if (length(s_sect)) s_sect[length(s_sect)] else NA) +} +run_ts <- function(d, tree, rss, ras) { + set.seed(1) + nt <- length(d) + smin <- as.integer(round(nt * 0.35)); smax <- as.integer(round(nt * 0.65)) + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, + wagnerStarts = 1L, fuseInterval = 9999L, sectorMinSize = smin, sectorMaxSize = smax, + rssRounds = as.integer(rss), rasStarts = as.integer(ras))) + list(score = as.double(attr(r, "score")), cand = as.double(attr(r, "candidates_evaluated"))) +} + +rows <- list() +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + for (sd in seeds) { + tn <- run_tnt(phy, sd, K) + if (is.null(tn$t0)) { cat(sprintf("WARN %s s%d: no T0\n", nm, sd)); next } + start <- TreeLength(tn$t0, phy) + a <- run_ts(phy, tn$t0, K, 1L) # polish (rasStarts = 1) + b <- run_ts(phy, tn$t0, K, 3L) # re-solve (rasStarts = 3) + rows[[length(rows) + 1]] <- data.frame(dataset = nm, seed = sd, start = start, + tnt_sect = tn$s_sect, ts_polish = a$score, ts_resolve = b$score, + gap_polish = a$score - tn$s_sect, gap_resolve = b$score - tn$s_sect, + Mcand_polish = round(a$cand / 1e6, 2), Mcand_resolve = round(b$cand / 1e6, 2), + stringsAsFactors = FALSE) + cat(sprintf("%-11s s%d | start=%.0f TNT=%.0f | polish=%.0f(g%+.0f) resolve=%.0f(g%+.0f) | Mcand %.1f->%.1f\n", + nm, sd, start, tn$s_sect, a$score, a$score - tn$s_sect, + b$score, b$score - tn$s_sect, a$cand / 1e6, b$cand / 1e6)) + } +} +S <- do.call(rbind, rows) +cat("\n== medians (gap = TS_sect - TNT_sect, from identical T0) ==\n") +agg <- do.call(rbind, lapply(split(S, S$dataset), function(d) data.frame( + dataset = d$dataset[1], TNT = median(d$tnt_sect), + polish = median(d$ts_polish), resolve = median(d$ts_resolve), + gap_polish = median(d$gap_polish), gap_resolve = median(d$gap_resolve)))) +print(agg, row.names = FALSE) +dir.create(dirname(out_csv), showWarnings = FALSE, recursive = TRUE) +write.csv(S, out_csv, row.names = FALSE) +cat(sprintf("\nWrote %s\n", out_csv)) diff --git a/dev/benchmarks/bench_regression.R b/dev/benchmarks/bench_regression.R new file mode 100644 index 000000000..93bf803bc --- /dev/null +++ b/dev/benchmarks/bench_regression.R @@ -0,0 +1,216 @@ +#!/usr/bin/env Rscript +# Performance regression benchmark for TreeSearch C++ engine. +# Run after every significant code change to catch quality or speed regressions. +# +# Usage: +# Rscript dev/benchmarks/bench_regression.R [lib_path] +# Rscript dev/benchmarks/bench_regression.R --datasets=Vinther2008,Zhu2013 --budget=30 +# Rscript dev/benchmarks/bench_regression.R --datasets=all --budget=20 --output=results.csv +# +# Arguments (positional, legacy): +# lib_path Library path for TreeSearch (default: auto-detect) +# +# Arguments (named): +# --lib=PATH Library path (overrides positional) +# --datasets=NAMES Comma-separated dataset names, or "all" (default: core 3) +# --budget=SECS Per-dataset time budget in seconds (default: 30) +# --output=FILE Write CSV results to FILE (in addition to stdout) +# --threads=N Number of threads (default: 1) +# +# Each benchmark runs in its own subprocess to isolate any crashes. +# +# Asserts: +# 1. Score quality: each dataset must reach its max allowed score. +# 2. Timing: no dataset should take more than 3x its reference time. +# +# Exit code 0 = pass, 1 = regression detected. + +# --- Parse arguments --- +args <- commandArgs(trailingOnly = TRUE) + +named_args <- list() +positional_args <- character(0) +for (arg in args) { + if (grepl("^--", arg)) { + parts <- strsplit(sub("^--", "", arg), "=", fixed = TRUE)[[1]] + named_args[[parts[1]]] <- if (length(parts) > 1) parts[2] else "true" + } else { + positional_args <- c(positional_args, arg) + } +} + +`%||%` <- function(a, b) if (is.null(a)) b else a + +lib_path <- named_args[["lib"]] %||% + (if (length(positional_args)) positional_args[1] else NULL) +budget <- as.numeric(named_args[["budget"]] %||% "30") +output_file <- named_args[["output"]] +n_threads <- as.integer(named_args[["threads"]] %||% "1") +dataset_arg <- named_args[["datasets"]] + +# --- Reference data --- +# Max scores are ~1-2% above optimal to allow for stochastic variation. +# ref_time_s is the expected time at budget=30s with 1 thread. +all_benchmarks <- list( + Vinther2008 = list(n_tip = 23, max_score = 80, ref_time_s = 1.0), + Agnarsson2004 = list(n_tip = 62, max_score = 785, ref_time_s = 5.0), + Zhu2013 = list(n_tip = 75, max_score = 662, ref_time_s = 8.0), + Longrich2010 = list(n_tip = 20, max_score = 132, ref_time_s = 0.5), + Sansom2010 = list(n_tip = 23, max_score = 190, ref_time_s = 0.8), + DeAssis2011 = list(n_tip = 33, max_score = 66, ref_time_s = 1.0), + Aria2015 = list(n_tip = 35, max_score = 145, ref_time_s = 1.5), + Wortley2006 = list(n_tip = 37, max_score = 500, ref_time_s = 2.0), + Griswold1999 = list(n_tip = 43, max_score = 415, ref_time_s = 3.0), + Schulze2007 = list(n_tip = 52, max_score = 168, ref_time_s = 4.0), + Eklund2004 = list(n_tip = 54, max_score = 450, ref_time_s = 4.0), + Zanol2014 = list(n_tip = 74, max_score = 1345, ref_time_s = 7.0), + Giles2015 = list(n_tip = 78, max_score = 725, ref_time_s = 7.0), + Dikow2009 = list(n_tip = 88, max_score = 1625, ref_time_s = 10.0) +) + +# Select datasets +default_names <- c("Vinther2008", "Agnarsson2004", "Zhu2013") +if (is.null(dataset_arg) || dataset_arg == "") { + selected_names <- default_names +} else if (tolower(dataset_arg) == "all") { + selected_names <- names(all_benchmarks) +} else { + selected_names <- trimws(strsplit(dataset_arg, ",")[[1]]) + unknown <- setdiff(selected_names, names(all_benchmarks)) + if (length(unknown)) { + stop("Unknown datasets: ", paste(unknown, collapse = ", "), + "\nAvailable: ", paste(names(all_benchmarks), collapse = ", ")) + } +} + +benchmarks <- all_benchmarks[selected_names] + +# Resolve library path +if (is.null(lib_path)) { + candidates <- c(Sys.glob(".agent-*"), Sys.glob(".builds/TreeSearch-*")) + if (length(candidates)) { + lib_path <- candidates[1] + cat("Auto-detected library:", lib_path, "\n") + } else { + lib_path <- .libPaths()[1] + } +} + +cat("=== TreeSearch Performance Regression Benchmark ===\n") +cat(sprintf(" Library: %s\n", lib_path)) +cat(sprintf(" Datasets: %s\n", paste(selected_names, collapse = ", "))) +cat(sprintf(" Budget: %ds per dataset\n", budget)) +cat(sprintf(" Threads: %d\n\n", n_threads)) + +n_pass <- 0L +n_fail <- 0L +results <- list() + +for (nm in names(benchmarks)) { + bm <- benchmarks[[nm]] + cat(sprintf("--- %s (%d tips) ---\n", nm, bm$n_tip)) + + script <- sprintf(' + library(TreeSearch, lib.loc = "%s") + library(TreeTools) + ds <- TreeSearch::inapplicable.phyData[["%s"]] + at <- attributes(ds) + contrast <- at$contrast + tip_data <- matrix(unlist(ds, use.names = FALSE), nrow = length(ds), byrow = TRUE) + weight <- at$weight + levels <- at$levels + set.seed(4217) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + contrast, tip_data, weight, levels, + maxReplicates = 100L, targetHits = 3L, + ratchetCycles = 12L, driftCycles = 2L, + xssRounds = 3L, xssPartitions = 4L, + rssRounds = 1L, cssRounds = 0L, + fuseInterval = 3L, maxSeconds = %d, + verbosity = 0L, nThreads = %dL + ) + elapsed <- (proc.time() - t0)[3] + cat(result$best_score, elapsed, result$replicates, sep = " ") + ', lib_path, nm, budget, n_threads) + + tf <- tempfile(fileext = ".R") + writeLines(script, tf) + timeout <- max(budget * 3, 60) + output <- tryCatch( + system2("Rscript", tf, stdout = TRUE, stderr = FALSE, timeout = timeout), + error = function(e) paste("ERROR:", conditionMessage(e)) + ) + unlink(tf) + + if (length(output) == 0 || startsWith(output[length(output)], "ERROR")) { + cat(" CRASHED or timed out\n") + n_fail <- n_fail + 1L + results[[nm]] <- data.frame( + dataset = nm, n_tip = bm$n_tip, + score = NA, elapsed = NA, replicates = NA, status = "CRASH", + stringsAsFactors = FALSE + ) + next + } + + vals <- strsplit(trimws(output[length(output)]), "\s+")[[1]] + if (length(vals) < 2) { + cat(" Unexpected output:", output[length(output)], "\n") + n_fail <- n_fail + 1L + results[[nm]] <- data.frame( + dataset = nm, n_tip = bm$n_tip, + score = NA, elapsed = NA, replicates = NA, status = "ERROR", + stringsAsFactors = FALSE + ) + next + } + + score <- as.numeric(vals[1]) + elapsed <- as.numeric(vals[2]) + reps <- if (length(vals) >= 3) as.integer(vals[3]) else NA_integer_ + + score_ok <- score <= bm$max_score + time_limit <- bm$ref_time_s * 3 * (budget / 30) + time_ok <- elapsed <= time_limit + + status <- if (score_ok && time_ok) "PASS" else "FAIL" + cat(sprintf(" Score: %.0f (max: %d) %s\n", + score, bm$max_score, + if (score_ok) "OK" else "REGRESSION")) + cat(sprintf(" Time: %.2fs (limit: %.1fs) %s\n", + elapsed, time_limit, + if (time_ok) "OK" else "REGRESSION")) + if (!is.na(reps)) cat(sprintf(" Reps: %d\n", reps)) + cat(sprintf(" Result: %s\n\n", status)) + + if (status == "PASS") n_pass <- n_pass + 1L + else n_fail <- n_fail + 1L + + results[[nm]] <- data.frame( + dataset = nm, n_tip = bm$n_tip, + score = score, elapsed = elapsed, replicates = reps, status = status, + stringsAsFactors = FALSE + ) +} + +cat(sprintf("=== Summary: %d PASS, %d FAIL ===\n", n_pass, n_fail)) + +# Write CSV output if requested +if (!is.null(output_file)) { + df <- do.call(rbind, results) + df$budget_s <- budget + df$threads <- n_threads + df$timestamp <- format(Sys.time(), "%Y-%m-%dT%H:%M:%S") + dir.create(dirname(output_file), showWarnings = FALSE, recursive = TRUE) + write.csv(df, output_file, row.names = FALSE) + cat(sprintf("Results written to %s\n", output_file)) +} + +if (n_fail > 0L) { + cat("\nREGRESSIONS DETECTED.\n") + quit(status = 1L) +} else { + cat("\nAll benchmarks passed.\n") + quit(status = 0L) +} diff --git a/dev/benchmarks/bench_score_micro.R b/dev/benchmarks/bench_score_micro.R new file mode 100644 index 000000000..2f11e2a58 --- /dev/null +++ b/dev/benchmarks/bench_score_micro.R @@ -0,0 +1,62 @@ +# Micro-benchmark: just Fitch scoring, no search +# Usage: Rscript dev/benchmarks/bench_score_micro.R +args <- commandArgs(trailingOnly = TRUE) +lib_path <- if (length(args) >= 1) args[1] else ".agent-pgo" + +library(TreeSearch, lib.loc = lib_path) +library(TreeTools) + +data("inapplicable.phyData") + +prep_ds <- function(dataset) { + at <- attributes(dataset) + contrast <- at$contrast + storage.mode(contrast) <- "double" + tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) + storage.mode(tip_data) <- "integer" + weight <- at$weight + levels <- at$levels + min_steps <- apply(contrast, 2, function(x) sum(x > 0)) - 1L + min_steps <- pmax(min_steps, 0L) + list(contrast = contrast, tip_data = tip_data, weight = weight, + levels = levels, min_steps = min_steps) +} + +for (nm in c("Agnarsson2004", "Dikow2009")) { + ds <- inapplicable.phyData[[nm]] + ds_args <- prep_ds(ds) + + set.seed(7294) + tree <- RandomTree(names(ds), root = TRUE) + edge <- tree$edge + + # Time many scoring calls + n_iter <- 500L + t0 <- system.time({ + for (i in seq_len(n_iter)) { + TreeSearch:::ts_fitch_score( + edge, ds_args$contrast, ds_args$tip_data, + ds_args$weight, ds_args$levels, ds_args$min_steps + ) + } + }) + cat(nm, ": ", n_iter, " scores in ", t0["elapsed"], "s (", + round(t0["elapsed"] / n_iter * 1000, 2), " ms/score)\n", sep = "") +} + +# TBR phase breakdown +for (nm in c("Agnarsson2004", "Dikow2009")) { + ds <- inapplicable.phyData[[nm]] + ds_args <- prep_ds(ds) + + set.seed(7294) + edge <- RandomTree(names(ds), root = TRUE)$edge + + r <- TreeSearch:::ts_bench_tbr_phases( + edge, ds_args$contrast, ds_args$tip_data, + ds_args$weight, ds_args$levels, ds_args$min_steps + ) + cat(nm, " TBR: indirect=", r$time_indirect_us, "us, clip_incr=", + r$time_clip_incr_us, "us, total_candidates=", r$n_candidates, "\n", sep = "") +} diff --git a/dev/benchmarks/bench_sect_2x2.R b/dev/benchmarks/bench_sect_2x2.R new file mode 100644 index 000000000..e1ee952da --- /dev/null +++ b/dev/benchmarks/bench_sect_2x2.R @@ -0,0 +1,82 @@ +# 2x2 shared-start probe: rasStarts {1,3} x sectorAcceptEqual {F,T}. +# +# Tests the advisor's hypothesis: re-solve looked inert because every lateral +# sector move was reverted (revert-unless-strictly-better). accept_equal is the +# minimal relaxation (Goloboff 2014 plateau traversal). Same shared-start design +# as bench_ras_verify.R: our sectorial runs from TNT's OWN T0 (ratchet/drift off). +# gap = TS_sect - TNT_sect, both from identical T0. Lower = closer to TNT. +# +# Env: TS_LIB (default .agent-ratchet), TNT_EXE, TS_DATASETS, TS_SEEDS, TS_KPASS, OUT_CSV. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +K <- as.integer(Sys.getenv("TS_KPASS", "8")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Zanol2014 Zhu2013 Giles2015")), "\\s+")[[1]] +out_csv <- Sys.getenv("OUT_CSV", "dev/benchmarks/sect_2x2.csv") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), "sect2x2"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +run_tnt <- function(phy, seed, kpass) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", kpass), "quit;") + writeLines(script, file.path(wd, "ss.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "ss.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + t0 <- tryCatch(ReadTntTree(file.path(wd, "t0.tre")), error = function(e) NULL) + if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + list(t0 = t0, s_sect = if (length(s_sect)) s_sect[length(s_sect)] else NA) +} +run_ts <- function(d, tree, rss, ras, aeq) { + set.seed(1) + nt <- length(d) + smin <- as.integer(round(nt * 0.35)); smax <- as.integer(round(nt * 0.65)) + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, + wagnerStarts = 1L, fuseInterval = 9999L, sectorMinSize = smin, sectorMaxSize = smax, + rssRounds = as.integer(rss), rasStarts = as.integer(ras), sectorAcceptEqual = aeq)) + as.double(attr(r, "score")) +} + +rows <- list() +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + for (sd in seeds) { + tn <- run_tnt(phy, sd, K) + if (is.null(tn$t0)) { cat(sprintf("WARN %s s%d: no T0\n", nm, sd)); next } + start <- TreeLength(tn$t0, phy) + base <- run_ts(phy, tn$t0, K, 1L, FALSE) # polish, strict + aeqo <- run_ts(phy, tn$t0, K, 1L, TRUE) # polish + accept_equal + ras <- run_ts(phy, tn$t0, K, 3L, FALSE) # re-solve, strict + both <- run_ts(phy, tn$t0, K, 3L, TRUE) # re-solve + accept_equal + rows[[length(rows) + 1]] <- data.frame(dataset = nm, seed = sd, start = start, + tnt = tn$s_sect, base = base, aeq = aeqo, ras = ras, both = both, + g_base = base - tn$s_sect, g_aeq = aeqo - tn$s_sect, + g_ras = ras - tn$s_sect, g_both = both - tn$s_sect, stringsAsFactors = FALSE) + cat(sprintf("%-11s s%d | TNT=%.0f | base=%.0f aeq=%.0f ras=%.0f both=%.0f | gaps %+.0f/%+.0f/%+.0f/%+.0f\n", + nm, sd, tn$s_sect, base, aeqo, ras, both, + base - tn$s_sect, aeqo - tn$s_sect, ras - tn$s_sect, both - tn$s_sect)) + } +} +S <- do.call(rbind, rows) +cat("\n== medians (gap = TS_sect - TNT_sect from identical T0; lower = closer to TNT) ==\n") +agg <- do.call(rbind, lapply(split(S, S$dataset), function(d) data.frame( + dataset = d$dataset[1], TNT = median(d$tnt), + g_base = median(d$g_base), g_aeq = median(d$g_aeq), + g_ras = median(d$g_ras), g_both = median(d$g_both)))) +print(agg, row.names = FALSE) +dir.create(dirname(out_csv), showWarnings = FALSE, recursive = TRUE) +write.csv(S, out_csv, row.names = FALSE) +cat(sprintf("\nWrote %s\n", out_csv)) diff --git a/dev/benchmarks/bench_sect_collapse.R b/dev/benchmarks/bench_sect_collapse.R new file mode 100644 index 000000000..9d8147bf3 --- /dev/null +++ b/dev/benchmarks/bench_sect_collapse.R @@ -0,0 +1,83 @@ +# Does COLLAPSING sub-clades into composite terminals (Goloboff 1999's reduced +# dataset) close the shared-start sectorial gap? sectorCollapseTarget>0 prunes a +# big selected clade to ~target composite first-pass terminals, so the sector +# search rearranges the coarse skeleton of major sub-clades instead of shuffling +# tips within a contiguous clade. Pairs with rasStarts (RAS-rebuild the skeleton +# = TNT's sectsch). Shared-start design; gap = TS_sect - TNT_sect, lower = closer. +# +# Env: TS_LIB (default .agent-ratchet), TNT_EXE, TS_DATASETS, TS_SEEDS, TS_KPASS, +# TS_COLLAPSE (target terminals, default 10), OUT_CSV. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +K <- as.integer(Sys.getenv("TS_KPASS", "8")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Zanol2014 Zhu2013 Giles2015")), "\\s+")[[1]] +CT <- as.integer(Sys.getenv("TS_COLLAPSE", "10")) +out_csv <- Sys.getenv("OUT_CSV", "dev/benchmarks/sect_collapse.csv") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), "sectcoll"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +run_tnt <- function(phy, seed, kpass) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", kpass), "quit;") + writeLines(script, file.path(wd, "ss.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "ss.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + t0 <- tryCatch(ReadTntTree(file.path(wd, "t0.tre")), error = function(e) NULL) + if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + list(t0 = t0, s_sect = if (length(s_sect)) s_sect[length(s_sect)] else NA) +} +run_ts <- function(d, tree, rss, ras, collapse) { + set.seed(1); nt <- length(d) + smin <- as.integer(round(nt * 0.35)); smax <- as.integer(round(nt * 0.65)) + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, + wagnerStarts = 1L, fuseInterval = 9999L, sectorMinSize = smin, sectorMaxSize = smax, + rssRounds = as.integer(rss), rasStarts = as.integer(ras), + sectorCollapseTarget = as.integer(collapse))) + list(score = as.double(attr(r, "score")), cand = as.double(attr(r, "candidates_evaluated"))) +} + +rows <- list() +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + for (sd in seeds) { + tn <- run_tnt(phy, sd, K) + if (is.null(tn$t0)) { cat(sprintf("WARN %s s%d: no T0\n", nm, sd)); next } + base <- run_ts(phy, tn$t0, K, 1L, 0L) # no collapse (gap baseline) + coll <- run_ts(phy, tn$t0, K, 1L, CT) # collapse + TBR + collras <- run_ts(phy, tn$t0, K, 3L, CT) # collapse + RAS-rebuild (= TNT) + rows[[length(rows) + 1]] <- data.frame(dataset = nm, seed = sd, tnt = tn$s_sect, + base = base$score, coll = coll$score, collras = collras$score, + g_base = base$score - tn$s_sect, g_coll = coll$score - tn$s_sect, + g_collras = collras$score - tn$s_sect, + Mc_base = round(base$cand / 1e6, 1), Mc_collras = round(collras$cand / 1e6, 1), + stringsAsFactors = FALSE) + cat(sprintf("%-11s s%d | TNT=%.0f | base=%.0f coll=%.0f collras=%.0f | gaps %+.0f/%+.0f/%+.0f | Mc %.1f->%.1f\n", + nm, sd, tn$s_sect, base$score, coll$score, collras$score, + base$score - tn$s_sect, coll$score - tn$s_sect, collras$score - tn$s_sect, + base$cand / 1e6, collras$cand / 1e6)) + } +} +S <- do.call(rbind, rows) +cat(sprintf("\n== medians (gap = TS_sect - TNT_sect from identical T0; collapse target=%d) ==\n", CT)) +agg <- do.call(rbind, lapply(split(S, S$dataset), function(d) data.frame( + dataset = d$dataset[1], TNT = median(d$tnt), + g_base = median(d$g_base), g_coll = median(d$g_coll), g_collras = median(d$g_collras)))) +print(agg, row.names = FALSE) +dir.create(dirname(out_csv), showWarnings = FALSE, recursive = TRUE) +write.csv(S, out_csv, row.names = FALSE) +cat(sprintf("\nWrote %s\n", out_csv)) diff --git a/dev/benchmarks/bench_sect_plateau.R b/dev/benchmarks/bench_sect_plateau.R new file mode 100644 index 000000000..57fcc6b4f --- /dev/null +++ b/dev/benchmarks/bench_sect_plateau.R @@ -0,0 +1,85 @@ +# Faithful plateau test (advisor): does letting the INTERNAL sector TBR hold many +# equal-length trees (sectorMaxHits) + keep laterals (sectorAcceptEqual) close the +# shared-start sectorial gap to TNT? TNT holds many trees while swapping a sector; +# we hold one (internal_max_hits = 1). rasStarts = 1 to isolate (re-solve is null). +# +# Engage-check: mh=20 must inflate candidates vs mh=1, else the knob isn't reaching +# the internal TBR. gap = TS_sect - TNT_sect from identical T0; lower = closer. +# STOP RULE: if 'plat' does not beat 'base', stop pulling levers -> instrument transfer. +# +# Env: TS_LIB (default .agent-ratchet), TNT_EXE, TS_DATASETS, TS_SEEDS, TS_KPASS, OUT_CSV. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +K <- as.integer(Sys.getenv("TS_KPASS", "8")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Zanol2014 Zhu2013 Giles2015")), "\\s+")[[1]] +out_csv <- Sys.getenv("OUT_CSV", "dev/benchmarks/sect_plateau.csv") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), "sectplat"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +run_tnt <- function(phy, seed, kpass) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", kpass), "quit;") + writeLines(script, file.path(wd, "ss.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "ss.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + t0 <- tryCatch(ReadTntTree(file.path(wd, "t0.tre")), error = function(e) NULL) + if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + list(t0 = t0, s_sect = if (length(s_sect)) s_sect[length(s_sect)] else NA) +} +run_ts <- function(d, tree, rss, ras, aeq, mh) { + set.seed(1) + nt <- length(d) + smin <- as.integer(round(nt * 0.35)); smax <- as.integer(round(nt * 0.65)) + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, + wagnerStarts = 1L, fuseInterval = 9999L, sectorMinSize = smin, sectorMaxSize = smax, + rssRounds = as.integer(rss), rasStarts = as.integer(ras), + sectorAcceptEqual = aeq, sectorMaxHits = as.integer(mh))) + list(score = as.double(attr(r, "score")), cand = as.double(attr(r, "candidates_evaluated"))) +} + +rows <- list() +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + for (sd in seeds) { + tn <- run_tnt(phy, sd, K) + if (is.null(tn$t0)) { cat(sprintf("WARN %s s%d: no T0\n", nm, sd)); next } + base <- run_ts(phy, tn$t0, K, 1L, FALSE, 1L) # current behaviour + mh20 <- run_ts(phy, tn$t0, K, 1L, FALSE, 20L) # +hold (max_hits alone) + plat <- run_ts(phy, tn$t0, K, 1L, TRUE, 20L) # +hold +accept_equal (faithful plateau) + rows[[length(rows) + 1]] <- data.frame(dataset = nm, seed = sd, + tnt = tn$s_sect, base = base$score, mh20 = mh20$score, plat = plat$score, + g_base = base$score - tn$s_sect, g_mh20 = mh20$score - tn$s_sect, + g_plat = plat$score - tn$s_sect, + Mc_base = round(base$cand / 1e6, 1), Mc_plat = round(plat$cand / 1e6, 1), + stringsAsFactors = FALSE) + cat(sprintf("%-11s s%d | TNT=%.0f | base=%.0f mh20=%.0f plat=%.0f | gaps %+.0f/%+.0f/%+.0f | Mcand %.1f->%.1f\n", + nm, sd, tn$s_sect, base$score, mh20$score, plat$score, + base$score - tn$s_sect, mh20$score - tn$s_sect, plat$score - tn$s_sect, + base$cand / 1e6, plat$cand / 1e6)) + } +} +S <- do.call(rbind, rows) +cat("\n== medians (gap = TS_sect - TNT_sect from identical T0; lower = closer) ==\n") +agg <- do.call(rbind, lapply(split(S, S$dataset), function(d) data.frame( + dataset = d$dataset[1], TNT = median(d$tnt), + g_base = median(d$g_base), g_mh20 = median(d$g_mh20), g_plat = median(d$g_plat), + Mc_base = median(d$Mc_base), Mc_plat = median(d$Mc_plat)))) +print(agg, row.names = FALSE) +dir.create(dirname(out_csv), showWarnings = FALSE, recursive = TRUE) +write.csv(S, out_csv, row.names = FALSE) +cat(sprintf("\nWrote %s\n", out_csv)) diff --git a/dev/benchmarks/bench_sectorial_shared.R b/dev/benchmarks/bench_sectorial_shared.R new file mode 100644 index 000000000..0d99b4212 --- /dev/null +++ b/dev/benchmarks/bench_sectorial_shared.R @@ -0,0 +1,94 @@ +# Probe: SHARED-START sectorial quality — TNT vs TreeSearch from an IDENTICAL tree. +# +# Removes the starting-tree confound (the 2-31 step gap in bench_sectorial_yield.R) +# to isolate sectorial QUALITY. TNT builds ONE RAS+TBR tree T0 (hold 1), saves it +# parenthetically (tsave *), and runs sectsch=rss from it; we read T0 via +# ReadTntTree and run OUR sectorial from the SAME T0 (ratchet/drift OFF). Scores & +# rearrangement counts are bitness-independent, so the local 32-bit TNT is valid. +# +# sect_gap = TS_sect - TNT_sect, both from the same T0: +# ~0 => our sectorial is quality-competitive (gap was the starting tree) +# >0 => our sectorial is genuinely weaker (justifies the multi-start rewrite) +# +# Env: TS_LIB, TNT_EXE, TS_DATASETS, TS_SEEDS, TS_KPASS, OUT_CSV. + +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", + "dev/profiling/.vtune-lib-20260617081344"), winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +K <- as.integer(Sys.getenv("TS_KPASS", "8")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Zanol2014 Zhu2013 Giles2015")), "\\s+")[[1]] +out_csv <- Sys.getenv("OUT_CSV", "dev/benchmarks/sectorial_shared.csv") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), "sectshared"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +run_tnt <- function(phy, seed, kpass) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", kpass), "quit;") + writeLines(script, file.path(wd, "sharedstart.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "sharedstart.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + s_tbr <- num(sub(".*Best score \\(TBR\\):\\s*([0-9.]+).*", "\\1", + grep("Best score \\(TBR\\):", out, value = TRUE)[1])) + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + rearr <- num(sub(".*examined:\\s*([0-9,]+).*", "\\1", + grep("Total rearrangements examined:", out, value = TRUE))) + t0 <- tryCatch(ReadTntTree(file.path(wd, "t0.tre")), error = function(e) NULL) + if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + list(t0 = t0, s_tbr = s_tbr, + s_sect = if (length(s_sect)) s_sect[length(s_sect)] else NA, + sect_rearr = if (length(rearr) >= 2) rearr[length(rearr)] - rearr[1] else NA) +} +run_ts <- function(d, tree, rss) { + set.seed(1) + nt <- length(d) + smin <- as.integer(Sys.getenv("TS_SECTMIN", as.character(round(nt * 0.35)))) + smax <- as.integer(Sys.getenv("TS_SECTMAX", as.character(round(nt * 0.65)))) + use_css <- Sys.getenv("TS_USE_CSS", "0") == "1" + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, + cssRounds = if (use_css) as.integer(rss) else 0L, + wagnerStarts = 1L, fuseInterval = 9999L, + sectorMinSize = smin, sectorMaxSize = smax, + rssRounds = if (use_css) 0L else as.integer(rss))) + list(score = as.double(attr(r, "score")), cand = as.double(attr(r, "candidates_evaluated"))) +} + +rows <- list() +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + for (sd in seeds) { + tn <- run_tnt(phy, sd, K) + if (is.null(tn$t0)) { cat(sprintf("WARN %s seed %d: no T0\n", nm, sd)); next } + start <- TreeLength(tn$t0, phy) + a <- run_ts(phy, tn$t0, 0L) # TS TBR-only from T0 (control) + b <- run_ts(phy, tn$t0, K) # TS TBR + sectorial from T0 + rows[[length(rows) + 1]] <- data.frame(dataset = nm, seed = sd, start = start, + tnt_sect = tn$s_sect, ts_tbr = a$score, ts_sect = b$score, + sect_gap = b$score - tn$s_sect, + tnt_Mrearr = round(tn$sect_rearr / 1e6, 2), + ts_Mcand = round((b$cand - a$cand) / 1e6, 2), stringsAsFactors = FALSE) + } +} +S <- do.call(rbind, rows) +cat("\n== Shared-start sectorial quality (TNT vs TS from identical T0) ==\n") +cat(sprintf("K=%d sectorial passes | seeds {%s}\n\n", K, paste(seeds, collapse = ","))) +agg <- do.call(rbind, lapply(split(S, S$dataset), function(d) data.frame( + dataset = d$dataset[1], start = median(d$start), TNT_sect = median(d$tnt_sect), + TS_tbr = median(d$ts_tbr), TS_sect = median(d$ts_sect), + sect_gap = median(d$sect_gap), stringsAsFactors = FALSE))) +print(agg, row.names = FALSE) +dir.create(dirname(out_csv), showWarnings = FALSE, recursive = TRUE) +write.csv(S, out_csv, row.names = FALSE) +cat(sprintf("\nWrote %s\n", out_csv)) diff --git a/dev/benchmarks/bench_sectorial_yield.R b/dev/benchmarks/bench_sectorial_yield.R new file mode 100644 index 000000000..09e58658a --- /dev/null +++ b/dev/benchmarks/bench_sectorial_yield.R @@ -0,0 +1,120 @@ +# Probe 1: sectorial-search YIELD — TNT vs TreeSearch (measure the prize). +# +# Question: from a TBR-local-optimum start, how many steps does sectorial search +# buy, and at what rearrangement cost, in each engine? This sizes the prize for +# rewriting our sectorial (TNT constructs ~n/2 sectors + 3 RAS+TBR multi-start; +# we select [6,80] clades + single TBR — see dev/benchmarks/tnt_sector_defaults.csv). +# +# TNT: `mult=replic 1` (RAS+TBR) -> K x `sectsch=rss`. Parses the per-phase +# cumulative "Total rearrangements examined" + per-phase best score. +# TreeSearch: ONE replicate, ratchet/drift OFF, Wagner(1)+TBR; compare TBR-only +# (rssRounds=0) vs TBR+RSS (rssRounds=K) on score + candidates_evaluated. +# +# Both rearrangement counts are bitness-independent, so the local 32-bit TNT is +# valid here (only wall-clock would need Hamilton). NB the two counters tally +# slightly different events (TNT counts all rearrangements incl. within-RAS; ours +# counts TBR/SPR candidates) so ABSOLUTE counts are only indicative — the +# steps-closed comparison is the clean signal. +# +# Env: TS_LIB, TNT_EXE, TS_DATASETS, TS_SEEDS, TS_KPASS, OUT_CSV. + +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", + "dev/profiling/.vtune-lib-20260617071429"), winslash = "/")) + library(TreeTools) +}) +TNT_EXE <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +K <- as.integer(Sys.getenv("TS_KPASS", "5")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Zanol2014 Zhu2013 Giles2015")), "\\s+")[[1]] +out_csv <- Sys.getenv("OUT_CSV", "dev/benchmarks/sectorial_yield.csv") + +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) + +tnt_work <- file.path(tempdir(), "sectyield") +dir.create(tnt_work, showWarnings = FALSE, recursive = TRUE) + +run_tnt_traj <- function(phy, seed, kpass) { + datafile <- file.path(tnt_work, "datafile.tnt") + runfile <- file.path(tnt_work, "styield.run") + WriteTntCharacters(phy, datafile) + script <- c("mxram 1024;", sprintf("proc %s;", basename(datafile)), + "hold 10000;", sprintf("rseed %d;", seed), + "mult=replic 1;", rep("sectsch=rss;", kpass), "quit;") + writeLines(script, runfile) + old <- setwd(tnt_work); on.exit(setwd(old)) + out <- tryCatch(system2(TNT_EXE, args = paste0(basename(runfile), ";"), + stdout = TRUE, stderr = TRUE), error = function(e) character(0)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + rearr <- num(sub(".*examined:\\s*([0-9,]+).*", "\\1", + grep("Total rearrangements examined:", out, value = TRUE))) + s_tbr <- num(sub(".*Best score \\(TBR\\):\\s*([0-9.]+).*", "\\1", + grep("Best score \\(TBR\\):", out, value = TRUE)[1])) + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + scores <- c(s_tbr, s_sect) + n <- min(length(scores), length(rearr)) + if (n < 1) return(data.frame(phase=integer(0), score=double(0), cum_rearr=double(0))) + data.frame(phase = 0:(n - 1), score = scores[1:n], cum_rearr = rearr[1:n]) +} + +run_ts <- function(phy, seed, kpass, do_sect) { + set.seed(seed) + r <- suppressWarnings(MaximizeParsimony( + phy, maxReplicates = 1L, nThreads = 1L, strategy = "auto", maxSeconds = 0, + verbosity = 0L, ratchetCycles = 0L, driftCycles = 0L, + xssRounds = 0L, cssRounds = 0L, wagnerStarts = 1L, fuseInterval = 9999L, + rssRounds = if (do_sect) kpass else 0L)) + list(score = as.double(attr(r, "score")), + cand = as.double(attr(r, "candidates_evaluated"))) +} + +traj_all <- list(); summ <- list() +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + for (sd in seeds) { + tj <- run_tnt_traj(phy, sd, K); tj$dataset <- nm; tj$seed <- sd + traj_all[[length(traj_all) + 1]] <- tj + a <- run_ts(phy, sd, K, FALSE); b <- run_ts(phy, sd, K, TRUE) + tnt_tbr <- if (nrow(tj)) tj$score[1] else NA + tnt_sect <- if (nrow(tj)) tj$score[nrow(tj)] else NA + tnt_r0 <- if (nrow(tj)) tj$cum_rearr[1] else NA + tnt_rF <- if (nrow(tj)) tj$cum_rearr[nrow(tj)] else NA + summ[[length(summ) + 1]] <- data.frame( + dataset = nm, seed = sd, + tnt_tbr = tnt_tbr, tnt_sect = tnt_sect, tnt_steps = tnt_tbr - tnt_sect, + tnt_sect_Mrearr = round((tnt_rF - tnt_r0) / 1e6, 2), + ts_tbr = a$score, ts_sect = b$score, ts_steps = a$score - b$score, + ts_sect_Mcand = round((b$cand - a$cand) / 1e6, 2), + stringsAsFactors = FALSE) + } +} +traj <- do.call(rbind, traj_all); S <- do.call(rbind, summ) + +cat("\n===== Sectorial yield: TNT vs TreeSearch (from TBR-local-opt) =====\n") +cat(sprintf("K=%d sectorial passes | seeds {%s}\n\n", K, paste(seeds, collapse = ","))) +agg <- do.call(rbind, lapply(split(S, S$dataset), function(d) data.frame( + dataset = d$dataset[1], + TNT_tbr = median(d$tnt_tbr), TNT_sect = median(d$tnt_sect), + TNT_steps = median(d$tnt_steps), TNT_Mrearr = median(d$tnt_sect_Mrearr), + TS_tbr = median(d$ts_tbr), TS_sect = median(d$ts_sect), + TS_steps = median(d$ts_steps), TS_Mcand = median(d$ts_sect_Mcand), + stringsAsFactors = FALSE))) +print(agg, row.names = FALSE) + +cat("\n--- TNT sectorial score trajectory (median score by phase) ---\n") +for (nm in dsN) { + t <- traj[traj$dataset == nm, ] + if (!nrow(t)) next + ph <- sort(unique(t$phase)) + med <- sapply(ph, function(p) median(t$score[t$phase == p])) + cat(sprintf(" %-12s %s\n", nm, paste(sprintf("%g", med), collapse = " -> "))) +} + +dir.create(dirname(out_csv), showWarnings = FALSE, recursive = TRUE) +write.csv(S, out_csv, row.names = FALSE) +write.csv(traj, sub("\\.csv$", "_traj.csv", out_csv), row.names = FALSE) +cat(sprintf("\nWrote %s (+ _traj.csv)\n", out_csv)) diff --git a/dev/benchmarks/bench_simd.R b/dev/benchmarks/bench_simd.R new file mode 100644 index 000000000..2ec3f03f9 --- /dev/null +++ b/dev/benchmarks/bench_simd.R @@ -0,0 +1,128 @@ +# Phase 3E SIMD benchmark: measure TBR search performance. +# +# This benchmark compares SIMD-enabled TBR performance across dataset sizes. +# Since SIMD is compiled in (no runtime toggle), we measure absolute timings +# and per-candidate costs to verify the Phase 3D profiling baseline is met +# or improved. +# +# Usage: Rscript dev/benchmarks/bench_simd.R + +library(TreeSearch) +library(TreeTools) + +cat("Phase 3E SIMD Benchmark\n") +cat("=======================\n\n") + +# Helper: run TBR search and measure time +bench_tbr <- function(dataset, n_reps = 3, label = "") { + ds <- list( + contrast = attr(dataset, "contrast"), + tip_data = t(vapply(dataset, I, dataset[[1]])), + weight = attr(dataset, "weight"), + levels = attr(dataset, "levels") + ) + n_tip <- length(dataset) + tree <- Preorder(PectinateTree(dataset)) + + times <- vapply(seq_len(n_reps), function(i) { + set.seed(4200 + i) + t0 <- proc.time() + TreeSearch:::ts_tbr_search( + tree$edge, ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxHits = 5L + ) + elapsed <- (proc.time() - t0)[["elapsed"]] + elapsed + }, numeric(1)) + + med <- median(times) + cat(sprintf(" %-30s tips=%3d median=%.3fs (%.3f, %.3f, %.3f)\n", + label, n_tip, med, times[1], times[2], times[3])) + data.frame(label = label, n_tip = n_tip, median_s = med, + stringsAsFactors = FALSE) +} + +# Helper: run driven search and measure time +bench_driven <- function(dataset, n_reps = 3, label = "") { + n_tip <- length(dataset) + ds <- list( + contrast = attr(dataset, "contrast"), + tip_data = t(vapply(dataset, I, dataset[[1]])), + weight = attr(dataset, "weight"), + levels = attr(dataset, "levels") + ) + + times <- vapply(seq_len(n_reps), function(i) { + set.seed(4200 + i) + t0 <- proc.time() + TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 2L, targetHits = 2L, + ratchetCycles = 2L, driftCycles = 0L, + xssPartitions = 2L, rssRounds = 0L, + cssRounds = 0L, cssPartitions = 2L, + fuseInterval = 0L, poolMaxSize = 2L, + maxSeconds = 30, verbosity = 0L + ) + elapsed <- (proc.time() - t0)[["elapsed"]] + elapsed + }, numeric(1)) + + med <- median(times) + cat(sprintf(" %-30s tips=%3d median=%.3fs (%.3f, %.3f, %.3f)\n", + label, n_tip, med, times[1], times[2], times[3])) + data.frame(label = label, n_tip = n_tip, median_s = med, + stringsAsFactors = FALSE) +} + +# ---- TBR benchmarks ---- +cat("TBR search (5 hits to best):\n") +results_tbr <- list() + +for (ds_name in c("Vinther2008", "Agnarsson2004", "Wills2012", + "Aria2015", "Zhu2013")) { + dataset <- inapplicable.phyData[[ds_name]] + results_tbr[[ds_name]] <- bench_tbr(dataset, label = ds_name) +} + +# DNA dataset +suppressWarnings(data("Laurasiatherian", package = "phangorn")) +results_tbr[["Laurasiatherian"]] <- bench_tbr(Laurasiatherian, + label = "Laurasiatherian (DNA)") + +cat("\nDriven search (2 replicates, 30s timeout):\n") +results_driven <- list() + +for (ds_name in c("Vinther2008", "Agnarsson2004", "Zhu2013")) { + dataset <- inapplicable.phyData[[ds_name]] + results_driven[[ds_name]] <- bench_driven(dataset, label = ds_name) +} + +# Phase benchmark diagnostic (if available) +cat("\nTBR phase timing (Phase 3D diagnostic):\n") +for (ds_name in c("Vinther2008", "Zhu2013")) { + dataset <- inapplicable.phyData[[ds_name]] + ds <- list( + contrast = attr(dataset, "contrast"), + tip_data = t(vapply(dataset, I, dataset[[1]])), + weight = attr(dataset, "weight"), + levels = attr(dataset, "levels") + ) + tree <- Preorder(PectinateTree(dataset)) + set.seed(7777) + ph <- tryCatch( + TreeSearch:::ts_bench_tbr_phases( + tree$edge, ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxHits = 3L + ), + error = function(e) NULL + ) + if (!is.null(ph)) { + cat(sprintf(" %s: clip=%.1fms indirect=%.1fms verify=%.1fms total=%.1fms\n", + ds_name, + ph$clip_us / 1000, ph$indirect_us / 1000, + ph$verify_us / 1000, ph$total_us / 1000)) + } +} + +cat("\nBenchmark complete.\n") diff --git a/dev/benchmarks/bench_smoke.R b/dev/benchmarks/bench_smoke.R new file mode 100644 index 000000000..44d4dc215 --- /dev/null +++ b/dev/benchmarks/bench_smoke.R @@ -0,0 +1,53 @@ +# SMOKE tier — breakage tripwire, ~seconds, run on every edit (POOL DRAINED). +# +# One R process, a few tiny datasets, REPLICATE-bounded (maxSeconds=0) so the +# candidate count is deterministic for a fixed seed (NOT wall-clock-bounded — +# that would make candidates machine-load-sensitive; see the critic note in +# dev/plans/2026-06-16-closing-the-tnt-gap.md). Green = "not broken / no +# candidate blow-up". This is a TRIPWIRE, never a ship gate: tiny datasets do +# not exercise sectorial search, so a real gap-lever can regress while smoke is +# green. Ship decisions use the iterate tier (bench_iterate.R). +# +# Env: TS_LIB (.agent-p0), TS_DATASETS, TS_REPS (4). SMOKE_WRITE_BASELINE=1 to +# (re)write dev/benchmarks/smoke_baseline.csv. Exit 1 on regression. + +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-p0"), + winslash = "/")) + library(TreeTools) +}) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Longrich2010 Vinther2008 DeAssis2011")), "\\s+")[[1]] +reps <- as.integer(Sys.getenv("TS_REPS", "4")) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } + +t0 <- Sys.time() +res <- do.call(rbind, lapply(dsN, function(nm) { + d <- fitch(inapplicable.phyData[[nm]]); set.seed(1) + r <- suppressWarnings(MaximizeParsimony(d, maxReplicates = reps, targetHits = 999L, + maxSeconds = 0, nThreads = 1L, verbosity = 0L)) + data.frame(dataset = nm, score = attr(r, "score"), + candidates = attr(r, "candidates_evaluated"), stringsAsFactors = FALSE) +})) +cat(sprintf("SMOKE | %d datasets | %d reps | %.1fs\n", length(dsN), reps, + as.double(difftime(Sys.time(), t0, units = "secs")))) +print(res, row.names = FALSE) + +base_f <- "dev/benchmarks/smoke_baseline.csv" +if (file.exists(base_f) && !nzchar(Sys.getenv("SMOKE_WRITE_BASELINE"))) { + base <- read.csv(base_f) + m <- merge(res, base, by = "dataset", suffixes = c("", ".base")) + m$score_delta <- m$score - m$score.base + m$cand_pct <- round(100 * (m$candidates / m$candidates.base - 1), 2) + bad <- m[m$score_delta != 0 | abs(m$cand_pct) > 5, ] + if (nrow(bad)) { + cat("\nSMOKE FAIL (score changed or candidates moved >5%):\n") + print(bad[, c("dataset", "score", "score.base", "cand_pct")], row.names = FALSE) + quit(status = 1L) + } + cat("SMOKE OK (score unchanged; candidates within +/-5% of baseline)\n") +} else { + write.csv(res, base_f, row.names = FALSE) + cat("Wrote smoke baseline:", base_f, "\n") +} diff --git a/dev/benchmarks/bench_stress_large.R b/dev/benchmarks/bench_stress_large.R new file mode 100644 index 000000000..47308da31 --- /dev/null +++ b/dev/benchmarks/bench_stress_large.R @@ -0,0 +1,268 @@ +# T-069: Stress test at 150–225 taxa +# Agent F, 2026-03-18 +# +# Three large neotrans matrices: project175 (165t), project3763 (205t), syab07204 (225t) +# Goals: +# 1. Per-phase timing and phase distribution at large size +# 2. TBR pass micro-benchmark via ts_bench_tbr_phases +# 3. Pool behaviour (pool_size, replicates, fuse events) +# 4. Scaling exponent for indirect scoring vs smaller datasets +# +# Run via: +# Rscript --vanilla -e "library(TreeSearch, lib.loc='.agent-f'); source('dev/benchmarks/bench_stress_large.R')" + +library(TreeSearch, lib.loc = ".agent-f") +library(TreeTools) + +NEOTRANS_DIR <- "../neotrans/inst/matrices" +MATRICES <- c("project175.nex", "project3763.nex", "syab07204.nex") + +# ---- Helpers ---------------------------------------------------------------- + +load_nex <- function(file) { + path <- file.path(NEOTRANS_DIR, file) + ReadAsPhyDat(path) +} + +prep_ds <- function(phyDat) { + at <- attributes(phyDat) + contrast <- at$contrast + storage.mode(contrast) <- "double" + tip_data <- matrix(unlist(phyDat, use.names = FALSE), + nrow = length(phyDat), byrow = TRUE) + storage.mode(tip_data) <- "integer" + weight <- at$weight + levels <- at$levels + # min_steps: number of non-zero contrast entries minus 1, clamped to 0 + min_steps <- pmax(apply(contrast, 2, function(x) sum(x > 0)) - 1L, 0L) + list(contrast = contrast, tip_data = tip_data, weight = weight, + levels = levels, min_steps = min_steps, + n_taxa = length(phyDat), n_chars = ncol(tip_data)) +} + +# ---- Section 1: Load matrices and summarise --------------------------------- + +cat("=== T-069 Large-Matrix Stress Test ===\n\n") +cat("=== Section 1: Dataset summary ===\n\n") + +datasets <- list() +for (f in MATRICES) { + cat(" Loading", f, "...\n") + pd <- load_nex(f) + ds <- prep_ds(pd) + datasets[[f]] <- ds + inappl_pct <- if (!is.null(attributes(pd)$levels) && + "-" %in% attributes(pd)$levels) { + round(100 * mean(unlist(pd) == which(attributes(pd)$levels == "-")), 1) + } else 0 + cat(sprintf(" %s: %d taxa, %d chars, inapplicable_pct=%.1f%%\n", + f, ds$n_taxa, ds$n_chars, inappl_pct)) +} + +# ---- Section 2: TBR pass micro-benchmark ------------------------------------ + +cat("\n=== Section 2: TBR pass micro-benchmark (ts_bench_tbr_phases) ===\n\n") + +tbr_results <- list() +for (f in MATRICES) { + ds <- datasets[[f]] + cat(sprintf(" %s (%d tips)...\n", f, ds$n_taxa)) + + reps_raw <- vector("list", 3) + for (i in 1:3) { + set.seed(4100 + i) + tree <- RandomTree(ds$n_taxa, root = TRUE) + reps_raw[[i]] <- TreeSearch:::ts_bench_tbr_phases( + tree$edge, + ds$contrast, ds$tip_data, ds$weight, ds$levels, + ds$min_steps + ) + } + + avg <- function(field) mean(vapply(reps_raw, `[[`, numeric(1), field)) + row <- data.frame( + file = f, + n_tips = reps_raw[[1]]$n_tips, + n_blocks = reps_raw[[1]]$n_blocks, + total_words = reps_raw[[1]]$total_words, + has_na = reps_raw[[1]]$has_na, + n_clips = avg("n_clips"), + n_candidates = avg("n_candidates"), + full_rescore_us = avg("time_full_rescore_us"), + clip_incr_us = avg("time_clip_incr_us"), + indirect_us = avg("time_indirect_us"), + unclip_us = avg("time_unclip_us"), + snap_save_us = avg("time_snapshot_save_us"), + snap_restore_us = avg("time_snapshot_restore_us"), + snap_bytes = avg("snapshot_bytes"), + stringsAsFactors = FALSE + ) + tbr_results[[f]] <- row + cat(sprintf(" clips=%.0f cands=%.0f indirect=%.0fms snap=%.1fKB\n", + row$n_clips, row$n_candidates, + row$indirect_us / 1000, row$snap_bytes / 1024)) +} + +tbr_df <- do.call(rbind, tbr_results) +rownames(tbr_df) <- NULL + +cat("\nTBR phase timing (μs, per pass):\n") +print(tbr_df[, c("file", "n_tips", "n_blocks", "full_rescore_us", + "clip_incr_us", "indirect_us", "unclip_us", + "snap_save_us", "snap_restore_us")], digits = 4) + +cat("\nPer-candidate indirect timing (ns):\n") +ns_cand <- round(1000 * tbr_df$indirect_us / tbr_df$n_candidates, 1) +print(data.frame(file = tbr_df$file, n_tips = tbr_df$n_tips, + n_candidates = round(tbr_df$n_candidates), + indirect_total_ms = round(tbr_df$indirect_us / 1000, 1), + ns_per_candidate = ns_cand)) + +# ---- Section 3: Scaling vs smaller datasets -------------------------------- +# +# Pull synthetic-series data from bench_memory.R baselines if available, +# otherwise run a quick synthetic series here. + +cat("\n=== Section 3: Scaling analysis ===\n\n") + +# Quick synthetic series: 20, 50, 100, 200, + new 225 point from tbr_df +make_synthetic <- function(n_tips, n_chars = 200, na_prob = 0.1) { + tree <- RandomTree(n_tips, root = TRUE) + mat <- matrix( + sample(c("0", "1", "-"), n_tips * n_chars, replace = TRUE, + prob = c((1 - na_prob) / 2, (1 - na_prob) / 2, na_prob)), + n_tips, n_chars, + dimnames = list(tree$tip.label, NULL) + ) + MatrixToPhyDat(mat) +} + +bench_tbr_one <- function(n_tips, n_chars = 200, na_prob = 0.1, seed = 4200) { + set.seed(seed) + pd <- make_synthetic(n_tips, n_chars, na_prob) + ds <- prep_ds(pd) + set.seed(seed + 1) + tree <- RandomTree(n_tips, root = TRUE) + r <- TreeSearch:::ts_bench_tbr_phases( + tree$edge, + ds$contrast, ds$tip_data, ds$weight, ds$levels, + ds$min_steps + ) + data.frame( + n_tips = n_tips, + n_candidates = r$n_candidates, + indirect_us = r$time_indirect_us, + clip_incr_us = r$time_clip_incr_us + ) +} + +synth_sizes <- c(20, 50, 100, 150, 200, 225) +cat(" Synthetic scaling series:", paste(synth_sizes, collapse = ", "), "tips...\n") +synth_rows <- lapply(synth_sizes, function(n) { + cat(" n =", n, "\n") + bench_tbr_one(n) +}) +synth_df <- do.call(rbind, synth_rows) +print(synth_df) + +# Fit scaling exponents +if (nrow(synth_df) >= 4) { + fit_indirect <- lm(log(indirect_us) ~ log(n_tips), data = synth_df) + fit_candidates <- lm(log(n_candidates) ~ log(n_tips), data = synth_df) + fit_clip <- lm(log(clip_incr_us) ~ log(n_tips), data = synth_df) + cat(sprintf("\nScaling exponents (log-log fit):\n")) + cat(sprintf(" indirect_us ~ n^%.2f (expected ~2.0)\n", coef(fit_indirect)[2])) + cat(sprintf(" n_candidates ~ n^%.2f (expected ~2.0)\n", coef(fit_candidates)[2])) + cat(sprintf(" clip_incr_us ~ n^%.2f\n", coef(fit_clip)[2])) +} + +# ---- Section 4: Full driven search (default params, 2 seeds) --------------- + +cat("\n=== Section 4: Full driven search at default params ===\n\n") +cat(" (maxReplicates=2, nThreads=2, default strategy)\n\n") + +driven_results <- list() +for (f in MATRICES) { + ds <- datasets[[f]] + cat(sprintf("--- %s (%d tips, %d chars) ---\n", f, ds$n_taxa, ds$n_chars)) + + # Auto-select strategy: replicate what MaximizeParsimony() does + # For large matrices, thorough if nChar < 100 AND nTip >= 65 + nTip <- ds$n_taxa + nChar <- ds$n_chars + use_thorough <- (nTip >= 65) && (nChar < 100) + if (use_thorough) { + ratchet <- 20L; drift <- 12L; xss <- 1L; rss <- 1L; css <- 0L + strat_name <- "thorough" + } else { + ratchet <- 5L; drift <- 2L; xss <- 1L; rss <- 1L; css <- 0L + strat_name <- "default" + } + cat(sprintf(" Strategy: %s (ratchet=%d, drift=%d)\n", strat_name, ratchet, drift)) + + run_list <- list() + for (seed_i in 1:2) { + set.seed(4300 + seed_i) + t0 <- proc.time() + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 2L, + targetHits = 1L, + ratchetCycles = ratchet, + driftCycles = drift, + xssRounds = xss, + rssRounds = rss, + cssRounds = css, + cssPartitions = 3L, + xssPartitions = 3L, + fuseInterval = 5L, + maxSeconds = 300, + verbosity = 0L, + nThreads = 2L + ) + elapsed <- (proc.time() - t0)[3] + run_list[[seed_i]] <- list(result = result, elapsed = elapsed) + cat(sprintf(" seed %d: %.2fs score=%.1f reps=%d pool=%d\n", + seed_i, elapsed, result$best_score, + result$replicates, result$pool_size)) + } + + # Per-phase breakdown from first run + r1 <- run_list[[1]]$result + if (!is.null(r1$timings)) { + timings <- r1$timings + cpp_total <- sum(unlist(timings)) + cat(sprintf(" Per-phase breakdown (seed 1):\n")) + for (ph in names(timings)) { + pct <- if (cpp_total > 0) 100 * timings[[ph]] / cpp_total else 0 + cat(sprintf(" %-12s %7.0f ms (%4.1f%%)\n", ph, timings[[ph]], pct)) + } + cat(sprintf(" %-12s %7.0f ms (C++ total)\n", "TOTAL", cpp_total)) + } + + driven_results[[f]] <- list( + file = f, + n_tips = ds$n_taxa, + n_chars = ds$n_chars, + strategy = strat_name, + score1 = run_list[[1]]$result$best_score, + score2 = run_list[[2]]$result$best_score, + time1 = run_list[[1]]$elapsed, + time2 = run_list[[2]]$elapsed, + pool1 = run_list[[1]]$result$pool_size, + reps1 = run_list[[1]]$result$replicates + ) + cat("\n") +} + +cat("=== Summary table ===\n\n") +summary_df <- do.call(rbind, lapply(driven_results, as.data.frame)) +rownames(summary_df) <- NULL +print(summary_df) + +# Save results +out_path <- "dev/benchmarks/stress_large_results.csv" +write.csv(summary_df, out_path, row.names = FALSE) +cat("\nResults written to", out_path, "\n") + +cat("\n=== T-069 complete ===\n") diff --git a/dev/benchmarks/bench_subprocess.R b/dev/benchmarks/bench_subprocess.R new file mode 100644 index 000000000..14a6ef216 --- /dev/null +++ b/dev/benchmarks/bench_subprocess.R @@ -0,0 +1,124 @@ +# Subprocess-isolated benchmark: each run in its own Rscript process. +# Workaround for T-025 (ratchet-triggered optimization-dependent UB that +# causes segfaults on consecutive ts_driven_search calls). + +library(TreeSearch) +library(TreeTools) + +source("dev/benchmarks/bench_datasets.R") +source("dev/benchmarks/bench_framework.R") + +GRID_DATASETS <- c( + "Longrich2010", # 20 tips + "Vinther2008", # 23 tips + "Aria2015", # 35 tips + "Griswold1999", # 43 tips + "Agnarsson2004", # 62 tips + "Zhu2013", # 75 tips + "Giles2015", # 78 tips + "Dikow2009" # 88 tips +) + +# One benchmark in a subprocess; returns CSV line or NA on crash +run_one_subprocess <- function(ds_name, strat_name, seed, maxSeconds = 20L, + maxReplicates = 100L) { + script <- sprintf(' +library(TreeSearch, lib.loc = if (dir.exists(".agent-a")) ".agent-a" else .libPaths()) +library(TreeTools) +source("dev/benchmarks/bench_datasets.R") +source("dev/benchmarks/bench_framework.R") +ds <- prepare_ts_data(TreeSearch::inapplicable.phyData[["%s"]]) +strat <- get_strategy("%s") +targetHits <- max(10L, ds$n_taxa %%/%% 5L) +args <- c( + list(contrast = ds$contrast, tip_data = ds$tip_data, + weight = ds$weight, levels = ds$levels, + maxReplicates = %dL, targetHits = targetHits, + maxSeconds = %d, verbosity = 0L), + strat) +set.seed(%dL) +t0 <- proc.time() +result <- do.call(TreeSearch:::ts_driven_search, args) +wall <- as.double((proc.time() - t0)[3]) +cat(result$best_score, result$replicates, result$hits_to_best, + result$pool_size, as.integer(result$timed_out), wall, + result$timings[["wagner_ms"]], result$timings[["tbr_ms"]], + result$timings[["xss_ms"]], result$timings[["rss_ms"]], + result$timings[["css_ms"]], result$timings[["ratchet_ms"]], + result$timings[["drift_ms"]], result$timings[["final_tbr_ms"]], + result$timings[["fuse_ms"]], sep = ",") +', ds_name, strat_name, maxReplicates, maxSeconds, seed) + + tf <- tempfile(fileext = ".R") + writeLines(script, tf) + on.exit(unlink(tf)) + + out <- tryCatch( + system2("Rscript", c("--no-save", tf), + stdout = TRUE, stderr = FALSE, timeout = maxSeconds + 30L), + error = function(e) NA_character_ + ) + + if (length(out) == 0 || is.na(out[1])) return(NULL) + vals <- as.numeric(strsplit(out[length(out)], ",")[[1]]) + if (length(vals) != 15) return(NULL) + + data.frame( + dataset = ds_name, strategy = strat_name, seed = seed, + n_taxa = length(TreeSearch::inapplicable.phyData[[ds_name]]), + best_score = vals[1], replicates = vals[2], hits_to_best = vals[3], + pool_size = vals[4], timed_out = as.logical(vals[5]), + wall_s = vals[6], + wagner_ms = vals[7], tbr_ms = vals[8], xss_ms = vals[9], + rss_ms = vals[10], css_ms = vals[11], ratchet_ms = vals[12], + drift_ms = vals[13], final_tbr_ms = vals[14], fuse_ms = vals[15], + stringsAsFactors = FALSE + ) +} + +# Run grid using subprocess isolation +run_grid_safe <- function(dataset_names = GRID_DATASETS, + strategy_names = STRATEGY_NAMES, + replicates = 3L, + maxSeconds = 20L, + base_seed = 7142L) { + n_combos <- length(dataset_names) * length(strategy_names) * replicates + cat(sprintf("Grid: %d datasets x %d strategies x %d reps = %d runs (subprocess)\n", + length(dataset_names), length(strategy_names), replicates, n_combos)) + + rows <- vector("list", n_combos) + idx <- 0L + + for (ds_name in dataset_names) { + for (strat_name in strategy_names) { + for (rep in seq_len(replicates)) { + idx <- idx + 1L + seed <- base_seed + (idx - 1L) * 7L + + cat(sprintf("[%3d/%d] %-15s x %-16s rep %d ... ", + idx, n_combos, ds_name, strat_name, rep)) + + res <- run_one_subprocess(ds_name, strat_name, seed, + maxSeconds = maxSeconds) + if (is.null(res)) { + cat("CRASH/ERROR\n") + next + } + cat(sprintf("score=%.0f wall=%.1fs reps=%d %s\n", + res$best_score, res$wall_s, res$replicates, + if (res$timed_out) "[TIMEOUT]" else "")) + rows[[idx]] <- res + } + } + } + + result <- do.call(rbind, rows[!vapply(rows, is.null, logical(1))]) + outfile <- "dev/benchmarks/results_grid.csv" + write.csv(result, outfile, row.names = FALSE) + cat(sprintf("\nResults saved to %s (%d rows)\n", outfile, nrow(result))) + invisible(result) +} + +# Main +cat("Starting subprocess-isolated benchmark grid...\n\n") +results <- run_grid_safe() diff --git a/dev/benchmarks/bench_t252_mbank_training.R b/dev/benchmarks/bench_t252_mbank_training.R new file mode 100644 index 000000000..8f39a0b10 --- /dev/null +++ b/dev/benchmarks/bench_t252_mbank_training.R @@ -0,0 +1,129 @@ +#!/usr/bin/env Rscript +# T-252: MorphoBank training-set baseline benchmark +# +# Runs the fixed 25-matrix training sample at 30s, 60s, and 120s budgets +# with the "default" strategy, 5 seeds per combination. +# Total: 25 matrices x 3 budgets x 5 seeds = 375 runs. +# Estimated wall time: ~4–5 hours (most runs hit timeout). +# +# Usage: +# Rscript bench_t252_mbank_training.R +# +# Requires: TreeSearch (installed), neotrans corpus in ../neotrans/ + +args <- commandArgs(trailingOnly = TRUE) +outdir <- if (length(args) >= 1) args[1] else "." + +# Find the repo root (this script lives in dev/benchmarks/) +# When run from repo root (cd $REPO; Rscript dev/benchmarks/...), getwd() is it. +repo_root <- getwd() +if (!file.exists(file.path(repo_root, "DESCRIPTION"))) { + # Try relative to script location + script_dir <- tryCatch( + dirname(normalizePath(sys.frame(1)$ofile)), + error = function(e) getwd() + ) + repo_root <- normalizePath(file.path(script_dir, "..", ".."), + mustWork = FALSE) +} +setwd(repo_root) + +cat("=== T-252: MorphoBank Training-Set Benchmark ===\n") +cat("Repo root:", repo_root, "\n") +cat("Output dir:", outdir, "\n") +cat("Started:", format(Sys.time(), "%Y-%m-%d %H:%M:%S"), "\n\n") + +library(TreeSearch) +library(TreeTools) + +source("dev/benchmarks/bench_datasets.R") +source("dev/benchmarks/bench_framework.R") + +# ---- Configuration ---- +BUDGETS <- c(30, 60, 120) # seconds +N_SEEDS <- 5L +BASE_SEED <- 3847L +STRATEGY <- "default" + +# ---- Load training matrices ---- +cat("Loading MorphoBank catalogue...\n") +catalogue <- load_mbank_catalogue() +cat(sprintf("Catalogue: %d usable matrices\n", nrow(catalogue))) + +cat(sprintf("Loading %d fixed training matrices...\n", + length(MBANK_FIXED_SAMPLE))) +datasets <- load_mbank_datasets(catalogue, keys = MBANK_FIXED_SAMPLE) +cat(sprintf("Successfully loaded: %d matrices\n\n", length(datasets))) + +if (length(datasets) == 0) { + stop("No datasets loaded. Is the neotrans repo available?") +} + +# ---- Characterize datasets ---- +cat("Dataset characteristics:\n") +chars <- do.call(rbind, lapply(names(datasets), function(nm) { + ch <- characterize_dataset(datasets[[nm]]) + ch$key <- nm + ch +})) +chars <- chars[order(chars$n_taxa), ] +print(chars[, c("key", "n_taxa", "n_chars", "n_patterns", + "pct_missing", "pct_inapp", "n_app_states")]) +cat("\n") + +# ---- Run benchmarks ---- +strat <- get_strategy(STRATEGY) +all_results <- list() + +for (budget in BUDGETS) { + cat(sprintf("\n========== Budget: %ds ==========\n", budget)) + + results <- run_benchmark_grid( + dataset_names = names(datasets), + strategy_names = STRATEGY, + replicates = N_SEEDS, + maxReplicates = 100L, + maxSeconds = budget, + base_seed = BASE_SEED, + datasets = datasets + ) + results$budget_s <- budget + results$source <- "mbank_training" + + # Save intermediate results per budget + outfile <- file.path( + outdir, + sprintf("t252_mbank_%ds_%s.csv", budget, + format(Sys.time(), "%Y%m%d_%H%M")) + ) + write.csv(results, outfile, row.names = FALSE) + cat(sprintf("Saved %d rows to %s\n", nrow(results), outfile)) + + all_results[[as.character(budget)]] <- results +} + +# ---- Combine and save final results ---- +final <- do.call(rbind, all_results) +final_file <- file.path(outdir, + sprintf("t252_mbank_all_%s.csv", + format(Sys.time(), "%Y%m%d_%H%M"))) +write.csv(final, final_file, row.names = FALSE) +cat(sprintf("\n=== Final results: %d rows saved to %s ===\n", + nrow(final), final_file)) + +# ---- Summary statistics ---- +cat("\n=== Summary by budget ===\n") +for (budget in BUDGETS) { + sub <- final[final$budget_s == budget, ] + cat(sprintf("\n--- %ds budget (%d runs) ---\n", budget, nrow(sub))) + cat(sprintf(" Median score: %.1f\n", median(sub$best_score, na.rm = TRUE))) + cat(sprintf(" Timed out: %d/%d (%.0f%%)\n", + sum(sub$timed_out, na.rm = TRUE), nrow(sub), + 100 * mean(sub$timed_out, na.rm = TRUE))) + cat(sprintf(" Median replicates: %.0f\n", + median(sub$replicates, na.rm = TRUE))) + cat(sprintf(" Median wall time: %.1fs\n", + median(sub$wall_s, na.rm = TRUE))) +} + +cat("\n=== Completed:", format(Sys.time(), "%Y-%m-%d %H:%M:%S"), "===\n") diff --git a/dev/benchmarks/bench_t265_regression.R b/dev/benchmarks/bench_t265_regression.R new file mode 100644 index 000000000..f17ff7b89 --- /dev/null +++ b/dev/benchmarks/bench_t265_regression.R @@ -0,0 +1,289 @@ +#!/usr/bin/env Rscript +# T-265: Per-replicate search quality regression diagnosis +# +# DESIGNED FOR HAMILTON HPC. Do not run locally. +# +# Tests whether the quality regression is in preset params vs engine code +# by comparing 3 configurations on the datasets with largest TNT gaps: +# +# r2_equiv — Minimal pipeline matching R2 structure: 12 ratchet (4%, +# auto moves), 2 drift, no sectorial, 1 Wagner, no tabu, +# no NNI warmup. Tests what R2 actually ran. +# r2_modern — R2 structure + modern ratchet tuning: 12 ratchet (25%, +# 5 moves), 0 drift, 1 Wagner, no sectorial, no tabu, +# NNI warmup ON. Tests whether modern ratchet params help +# with a minimal pipeline. +# auto_preset — Current auto-selected preset (default or thorough). +# Tests whether added complexity helps or hurts. +# +# If r2_equiv or r2_modern produce better scores -> preset complexity is +# the problem. If all configs show the same regression -> engine code issue. +# +# Usage: +# Rscript bench_t265_regression.R [timeout_s] [output_dir] +# +# Default: 120s budget, output to current directory. + +library(TreeSearch) +library(TreeTools) + +args <- commandArgs(trailingOnly = TRUE) +timeout_s <- if (length(args) >= 1) as.integer(args[1]) else 120L +output_dir <- if (length(args) >= 2) args[2] else "." + +cat("=== T-265: Per-Replicate Quality Regression Diagnosis ===\n") +cat(sprintf("Timeout: %ds\n", timeout_s)) +cat(sprintf("TreeSearch version: %s\n", packageVersion("TreeSearch"))) +cat(sprintf("Output dir: %s\n", output_dir)) +cat(sprintf("Started: %s\n\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) + +# ---- Datasets ---- +# 8 datasets with largest persistent TNT gaps, plus Wilson2003 from T-265 +gap_names <- c( + "Wortley2006", "Eklund2004", "Wilson2003", "Conrad2008", + "Geisler2001", "Zanol2014", "Zhu2013", "Giles2015", "Dikow2009" +) + +# Convert inapplicable to missing for EW Fitch scoring (match TNT) +fitch_mode <- function(dataset) { + contrast <- attr(dataset, "contrast") + levels <- attr(dataset, "levels") + inapp_col <- match("-", levels) + if (is.na(inapp_col)) return(dataset) + for (i in seq_len(nrow(contrast))) { + if (contrast[i, inapp_col] == 1 && sum(contrast[i, ]) == 1) { + contrast[i, ] <- 1 + } + } + attr(dataset, "contrast") <- contrast + dataset +} + +datasets <- lapply( + setNames(gap_names, gap_names), + function(nm) fitch_mode(inapplicable.phyData[[nm]]) +) + +# ---- Configurations ---- +configs <- list( + r2_equiv = list( + label = "r2_equiv", + desc = "R2 pipeline: 12 ratchet (4%), 2 drift, no sectorial, no tabu", + control = SearchControl( + ratchetCycles = 12L, + ratchetPerturbProb = 0.04, + ratchetPerturbMode = 0L, + ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = FALSE, + driftCycles = 2L, + driftAfdLimit = 5L, + driftRfdLimit = 0.15, + xssRounds = 0L, rssRounds = 0L, cssRounds = 0L, + wagnerStarts = 1L, + tabuSize = 0L, + nniFirst = FALSE, sprFirst = FALSE, + perturbStopFactor = 0L, + adaptiveLevel = FALSE, + maxOuterResets = 0L, + outerCycles = 1L, + fuseInterval = 5L, + fuseAcceptEqual = FALSE, + poolMaxSize = 100L, + consensusStableReps = 0L, + nniPerturbCycles = 0L, + annealCycles = 0L, + adaptiveStart = FALSE, + enumTimeFraction = 0.1 + ) + ), + r2_modern = list( + label = "r2_modern", + desc = "R2 structure + modern ratchet (25%, 5 moves), NNI warmup, no drift", + control = SearchControl( + ratchetCycles = 12L, + ratchetPerturbProb = 0.25, + ratchetPerturbMode = 0L, + ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = FALSE, + driftCycles = 0L, + xssRounds = 0L, rssRounds = 0L, cssRounds = 0L, + wagnerStarts = 1L, + tabuSize = 0L, + nniFirst = TRUE, sprFirst = FALSE, + perturbStopFactor = 0L, + adaptiveLevel = FALSE, + maxOuterResets = 0L, + outerCycles = 1L, + fuseInterval = 5L, + fuseAcceptEqual = FALSE, + poolMaxSize = 100L, + consensusStableReps = 0L, + nniPerturbCycles = 0L, + annealCycles = 0L, + adaptiveStart = FALSE, + enumTimeFraction = 0.1 + ) + ), + auto_preset = list( + label = "auto_preset", + desc = "Current auto-selected preset (default or thorough)" + # No control override — uses strategy = "auto" + ) +) + +seeds <- 1:5 +total_runs <- length(configs) * length(datasets) * length(seeds) +cat(sprintf("Configs: %d, Datasets: %d, Seeds: %d -> %d total runs\n", + length(configs), length(datasets), length(seeds), total_runs)) + +# TNT reference scores (from bench_intra_fuse.R and T-265 notes) +tnt_best <- c( + Wortley2006 = 479, Eklund2004 = 438, Wilson2003 = 860, + Conrad2008 = 1725, Geisler2001 = 1293, + Zanol2014 = 1261, Zhu2013 = 624, + Giles2015 = 670, Dikow2009 = 1603 +) + +# ---- Run experiments ---- +results <- data.frame( + dataset = character(), n_tips = integer(), n_patterns = integer(), + auto_strategy = character(), + config = character(), seed = integer(), timeout_s = integer(), + score = numeric(), n_trees = integer(), replicates = integer(), + hits = integer(), wall_s = numeric(), + tnt_best = numeric(), gap = numeric(), + stringsAsFactors = FALSE +) + +run_idx <- 0L +for (cfg_name in names(configs)) { + cfg <- configs[[cfg_name]] + cat(sprintf("\n--- Config: %s (%s) ---\n", cfg$label, cfg$desc)) + + for (ds_name in gap_names) { + ds <- datasets[[ds_name]] + ntip <- length(ds) + npat <- sum(attr(ds, "weight")) + auto_strat <- if (ntip <= 30) "sprint" + else if (npat < 100) "default" + else if (ntip >= 120) "large" + else if (ntip >= 65) "thorough" + else "default" + + for (s in seeds) { + run_idx <- run_idx + 1L + cat(sprintf(" [%d/%d] %s / %s / seed=%d ... ", + run_idx, total_runs, ds_name, cfg$label, s)) + + set.seed(s) + t0 <- proc.time() + + tryCatch({ + if (cfg_name == "auto_preset") { + res <- MaximizeParsimony( + ds, + maxSeconds = timeout_s, + strategy = "auto", + verbosity = 0L, + nThreads = 1L + ) + } else { + res <- MaximizeParsimony( + ds, + maxSeconds = timeout_s, + strategy = "none", + control = cfg$control, + verbosity = 0L, + nThreads = 1L + ) + } + + elapsed <- (proc.time() - t0)[3] + best_score <- attr(res, "score") + n_trees <- length(res) + reps <- attr(res, "replicates") + hits <- attr(res, "hits") + tnt_ref <- tnt_best[ds_name] + gap <- if (!is.na(tnt_ref)) best_score - tnt_ref else NA_real_ + + cat(sprintf("score=%g, gap=%s, reps=%d, %.1fs\n", + best_score, + if (is.na(gap)) "?" else sprintf("%+d", gap), + reps, elapsed)) + + results <- rbind(results, data.frame( + dataset = ds_name, n_tips = ntip, n_patterns = npat, + auto_strategy = auto_strat, + config = cfg$label, seed = s, timeout_s = timeout_s, + score = best_score, n_trees = n_trees, replicates = reps, + hits = hits, wall_s = elapsed, + tnt_best = tnt_ref, gap = gap, + stringsAsFactors = FALSE + )) + }, error = function(e) { + elapsed <- (proc.time() - t0)[3] + cat(sprintf("ERROR: %s (%.1fs)\n", conditionMessage(e), elapsed)) + results <<- rbind(results, data.frame( + dataset = ds_name, n_tips = ntip, n_patterns = npat, + auto_strategy = auto_strat, + config = cfg$label, seed = s, timeout_s = timeout_s, + score = NA_real_, n_trees = NA_integer_, replicates = NA_integer_, + hits = NA_integer_, wall_s = elapsed, + tnt_best = tnt_best[ds_name], gap = NA_real_, + stringsAsFactors = FALSE + )) + }) + } + } +} + +# ---- Save results ---- +out_file <- file.path(output_dir, + sprintf("t265_results_%ds.csv", timeout_s)) +write.csv(results, out_file, row.names = FALSE) +cat(sprintf("\nResults saved to: %s\n", out_file)) + +# ---- Summary ---- +cat("\n=== Summary by config × dataset (median score, median gap) ===\n\n") +for (ds_name in gap_names) { + sub <- results[results$dataset == ds_name, ] + if (nrow(sub) == 0) next + cat(sprintf(" %s (%dt, %dp, auto=%s, TNT=%s):\n", + ds_name, sub$n_tips[1], sub$n_patterns[1], + sub$auto_strategy[1], + if (is.na(tnt_best[ds_name])) "?" else tnt_best[ds_name])) + for (cfg_name in names(configs)) { + cfg_sub <- sub[sub$config == configs[[cfg_name]]$label, ] + if (nrow(cfg_sub) == 0) next + med_score <- median(cfg_sub$score, na.rm = TRUE) + med_gap <- median(cfg_sub$gap, na.rm = TRUE) + min_score <- min(cfg_sub$score, na.rm = TRUE) + max_score <- max(cfg_sub$score, na.rm = TRUE) + med_reps <- median(cfg_sub$replicates, na.rm = TRUE) + unique_scores <- length(unique(na.omit(cfg_sub$score))) + cat(sprintf(" %-14s median=%7.0f (range %g-%g), gap=%+.0f, reps=%.0f, unique_scores=%d\n", + configs[[cfg_name]]$label, med_score, min_score, max_score, + med_gap, med_reps, unique_scores)) + } +} + +# ---- Per-replicate convergence check ---- +cat("\n=== Score diversity across seeds (do all seeds find the same score?) ===\n\n") +for (ds_name in gap_names) { + sub <- results[results$dataset == ds_name, ] + if (nrow(sub) == 0) next + cat(sprintf(" %s:\n", ds_name)) + for (cfg_name in names(configs)) { + cfg_sub <- sub[sub$config == configs[[cfg_name]]$label, ] + if (nrow(cfg_sub) == 0) next + scores <- na.omit(cfg_sub$score) + if (length(scores) == 0) next + n_unique <- length(unique(scores)) + cat(sprintf(" %-14s scores: %s (%d unique)\n", + configs[[cfg_name]]$label, + paste(scores, collapse = ", "), + n_unique)) + } +} + +cat(sprintf("\nCompleted: %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) diff --git a/dev/benchmarks/bench_t269_interleaving.R b/dev/benchmarks/bench_t269_interleaving.R new file mode 100644 index 000000000..90081885e --- /dev/null +++ b/dev/benchmarks/bench_t269_interleaving.R @@ -0,0 +1,201 @@ +#!/usr/bin/env Rscript +# T-269: Fine-grained sectorial interleaving benchmark +# +# DESIGNED FOR HAMILTON HPC. Do not run locally (hours of wall time). +# +# Tests whether fine-grained interleaving of sectorial search with ratchet +# perturbation improves score quality. The key question: does performing +# one sectorial pass per ratchet cycle (outerCycles = ratchetCycles) help +# compared to the current thorough preset (outerCycles = 2)? +# +# Design: +# - Thorough preset as base (ratchetCycles=20, XSS+RSS+CSS, outerCycles=2) +# - Vary outerCycles ∈ {1, 2, 4, 10, 20} while holding ratchetCycles=20 +# - 4 standard gap datasets (37–88 tips), 5 seeds, 30s + 60s budgets +# - EW scoring throughout (inapplicable → missing via fitch_mode) +# +# outerCycles=1: all 20 ratchet cycles in one block, then 1 sectorial pass +# outerCycles=2: 2 × 10 ratchet + 2 sectorial passes (current thorough) +# outerCycles=4: 4 × 5 ratchet + 4 sectorial passes +# outerCycles=10: 10 × 2 ratchet + 10 sectorial passes +# outerCycles=20: 20 × 1 ratchet + 20 sectorial passes (TNT pattern) +# +# Usage: +# Rscript bench_t269_interleaving.R [timeout_s] [output_dir] +# timeout_s: search budget in seconds. Default: 30 +# output_dir: where to write CSV results. Default: "." +# +# Output: t269_interleaving_{timeout}s.csv + +library(TreeSearch) +library(TreeTools) + +args <- commandArgs(trailingOnly = TRUE) +timeout_s <- if (length(args) >= 1) as.integer(args[1]) else 30L +output_dir <- if (length(args) >= 2) args[2] else "." + +cat("=== T-269: Fine-Grained Sectorial Interleaving Benchmark ===\n") +cat(sprintf("Timeout: %ds\n", timeout_s)) +cat(sprintf("TreeSearch version: %s\n", packageVersion("TreeSearch"))) +cat(sprintf("Output dir: %s\n", output_dir)) +cat(sprintf("Started: %s\n\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) + +# ---- Datasets ---- +# 4 standard datasets with persistent TNT gaps — range 37–88 tips. +# inapplicable converted to missing for EW Fitch (match TNT). +fitch_mode <- function(dataset) { + contrast <- attr(dataset, "contrast") + levels <- attr(dataset, "levels") + inapp_col <- match("-", levels) + if (is.na(inapp_col)) return(dataset) + for (i in seq_len(nrow(contrast))) { + if (contrast[i, inapp_col] == 1 && sum(contrast[i, ]) == 1) { + contrast[i, ] <- 1 + } + } + attr(dataset, "contrast") <- contrast + dataset +} + +bench_names <- c("Wortley2006", "Agnarsson2004", "Zhu2013", "Dikow2009") +datasets <- lapply( + setNames(bench_names, bench_names), + function(nm) fitch_mode(inapplicable.phyData[[nm]]) +) + +# TNT reference scores (EW Fitch mode, from T-265) +tnt_best <- c( + Wortley2006 = 479, Agnarsson2004 = 718, + Zhu2013 = 624, Dikow2009 = 1603 +) + +seeds <- 1:5 + +# ---- Configs ---- +# Fixed thorough-preset parameters (ratchetCycles=20, no drift, no NNI-perturb) +# outerCycles varies: 1, 2, 4, 10, 20. +outer_cycles_grid <- c(1L, 2L, 4L, 10L, 20L) + +build_control <- function(outer_cycles) { + SearchControl( + # Thorough preset base + ratchetCycles = 20L, + ratchetPerturbProb = 0.25, + ratchetPerturbMode = 2L, + ratchetPerturbMaxMoves = 5L, + ratchetAdaptive = FALSE, # off for cleaner comparison + # Vary this: + outerCycles = outer_cycles, + # Sectorial + xssRounds = 5L, + rssRounds = 5L, + cssRounds = 2L, + # No drift/NNI-perturb + driftCycles = 0L, + nniPerturbCycles = 0L, + # Other thorough settings + wagnerStarts = 3L, + nniFirst = TRUE, + consensusStableReps = 0L + ) +} + +configs <- setNames( + lapply(outer_cycles_grid, build_control), + sprintf("outer_%02d", outer_cycles_grid) +) + +total_runs <- length(configs) * length(datasets) * length(seeds) +cat(sprintf("Configs: %d (outerCycles: %s), Datasets: %d, Seeds: %d -> %d total runs\n\n", + length(configs), + paste(outer_cycles_grid, collapse = "/"), + length(datasets), length(seeds), total_runs)) + +# ---- Run experiments ---- +results <- data.frame( + dataset = character(), n_tips = integer(), n_patterns = integer(), + outer_cycles = integer(), seed = integer(), timeout_s = integer(), + score = numeric(), n_trees = integer(), replicates = integer(), + wall_s = numeric(), tnt_best = numeric(), gap = numeric(), + stringsAsFactors = FALSE +) + +run_idx <- 0L +for (cfg_name in names(configs)) { + ctrl <- configs[[cfg_name]] + oc <- ctrl$outerCycles + cat(sprintf("\n--- outerCycles = %d ---\n", oc)) + + for (ds_name in bench_names) { + ds <- datasets[[ds_name]] + ntip <- length(ds) + npat <- sum(attr(ds, "weight")) + + for (s in seeds) { + run_idx <- run_idx + 1L + cat(sprintf(" [%d/%d] %s / oc=%d / seed=%d ... ", + run_idx, total_runs, ds_name, oc, s)) + + set.seed(s) + t0 <- proc.time() + + tryCatch({ + res <- MaximizeParsimony( + ds, + maxSeconds = timeout_s, + control = ctrl, + verbosity = 0L, + nThreads = 1L + ) + + elapsed <- (proc.time() - t0)[3] + best_score <- attr(res, "score") + n_trees <- length(res) + reps <- attr(res, "replicates") + tnt_ref <- tnt_best[ds_name] + gap <- if (!is.na(tnt_ref)) best_score - tnt_ref else NA_real_ + + cat(sprintf("score=%g, gap=%s, reps=%d, %.1fs\n", + best_score, + if (is.na(gap)) "?" else sprintf("%+d", gap), + reps, elapsed)) + + results <- rbind(results, data.frame( + dataset = ds_name, n_tips = ntip, n_patterns = npat, + outer_cycles = oc, seed = s, timeout_s = timeout_s, + score = best_score, n_trees = n_trees, replicates = reps, + wall_s = elapsed, + tnt_best = tnt_ref, gap = gap, + stringsAsFactors = FALSE + )) + }, error = function(e) { + cat(sprintf("ERROR: %s\n", conditionMessage(e))) + }) + } + } +} + +# ---- Save results ---- +outfile <- file.path( + output_dir, + sprintf("t269_interleaving_%ds.csv", timeout_s) +) +write.csv(results, outfile, row.names = FALSE) +cat(sprintf("\n=== Results written to %s (%d rows) ===\n", + outfile, nrow(results))) + +# ---- Quick summary ---- +cat("\n--- Median gap by outerCycles × dataset ---\n") +agg <- aggregate(gap ~ outer_cycles + dataset, data = results, FUN = median, + na.rm = TRUE) +agg_wide <- reshape(agg, direction = "wide", idvar = "outer_cycles", + timevar = "dataset", v.names = "gap") +names(agg_wide) <- sub("gap\\.", "", names(agg_wide)) +print(agg_wide[order(agg_wide$outer_cycles), ], row.names = FALSE) + +cat("\n--- Median gap by outerCycles (pooled) ---\n") +agg2 <- aggregate(gap ~ outer_cycles, data = results, FUN = median, + na.rm = TRUE) +print(agg2[order(agg2$outer_cycles), ], row.names = FALSE) + +cat(sprintf("\nCompleted: %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z"))) diff --git a/dev/benchmarks/bench_t274_nni_perturb.R b/dev/benchmarks/bench_t274_nni_perturb.R new file mode 100644 index 000000000..6f90bcfeb --- /dev/null +++ b/dev/benchmarks/bench_t274_nni_perturb.R @@ -0,0 +1,180 @@ +# bench_t274_nni_perturb.R +# +# T-274: Benchmark nniPerturbCycles=0 vs 5 at thorough-preset scale. +# +# S-PROF round 6 found NNI-perturb = 34.3% of Zhu2013 (75t) thorough-preset +# search time with only 14% hit rate and ~1-step mean improvement. +# This benchmark tests whether removing NNI-perturb improves time-adjusted +# expected best score at 30s and 60s budgets on 65–88 tip datasets. +# +# METHODOLOGY: Per-replicate sampling. +# - maxReplicates=1 per run, many seeds → per-replicate score distribution +# - time_per_rep estimated from wall time +# - expected_best(scores, k=floor(budget/median_time)) at 30s/60s +# +# Usage: +# Rscript dev/benchmarks/bench_t274_nni_perturb.R [lib_path] +# Default lib_path = .agent-F +# +# Results: dev/benchmarks/results_t274_nni_perturb.csv +# Run time: ~12-18 min (3 datasets x 2 conditions x 20 seeds) + +args <- commandArgs(trailingOnly = TRUE) +lib_path <- if (length(args) >= 1) args[[1L]] else ".agent-F" +.libPaths(c(lib_path, .libPaths())) +library(TreeSearch) +library(TreeTools) + +cat("TreeSearch version:", as.character(packageVersion("TreeSearch")), "\n") +cat("Date:", format(Sys.time(), "%Y-%m-%d %H:%M"), "\n\n") + +# ------------------------------------------------------------ +# Configuration +# ------------------------------------------------------------ +DATASETS <- c("Zhu2013", "Giles2015", "Dikow2009") # 75, 78, 88 tips +BUDGETS_S <- c(30, 60) +N_SEEDS <- 20L +NNI_CONDITIONS <- c(0L, 5L) +OUT_FILE <- "dev/benchmarks/results_t274_nni_perturb.csv" + +# Seeds — fixed for reproducibility +set.seed(4718) +seeds <- sample.int(99999L, N_SEEDS) + +# ------------------------------------------------------------ +# expected_best: bootstrap estimate of expected minimum from k draws +# ------------------------------------------------------------ +expected_best <- function(scores, k, n_boot = 5000L) { + mean(replicate(n_boot, min(sample(scores, k, replace = TRUE)))) +} + +# ------------------------------------------------------------ +# Per-replicate runs +# ------------------------------------------------------------ +total_runs <- length(DATASETS) * length(NNI_CONDITIONS) * N_SEEDS +cat(sprintf("Total runs: %d datasets x %d conditions x %d seeds = %d\n\n", + length(DATASETS), length(NNI_CONDITIONS), N_SEEDS, total_runs)) + +rows <- list() +idx <- 0L + +for (ds_name in DATASETS) { + dataset <- TreeSearch::inapplicable.phyData[[ds_name]] + if (is.null(dataset)) { + warning("Dataset not found: ", ds_name) + next + } + n_taxa <- length(dataset) + n_char <- sum(attr(dataset, "weight")) + + cat(sprintf("=== %s (%dt, %dc) ===\n", ds_name, n_taxa, n_char)) + + for (nni_cycles in NNI_CONDITIONS) { + cond_label <- if (nni_cycles == 0L) "nni=0" else sprintf("nni=%d", nni_cycles) + + for (seed in seeds) { + idx <- idx + 1L + cat(sprintf("[%3d/%d] %-12s | %-6s | seed %5d ... ", + idx, total_runs, ds_name, cond_label, seed)) + flush.console() + + set.seed(seed) + t0 <- proc.time()[[3L]] + result <- tryCatch( + # Pass nniPerturbCycles via ... so it overrides the thorough preset + # for just that parameter, leaving all other thorough params intact. + MaximizeParsimony(dataset, + strategy = "thorough", + nniPerturbCycles = as.integer(nni_cycles), + maxReplicates = 1L, + nThreads = 1L, + verbosity = 0L), + error = function(e) { + cat("ERROR:", conditionMessage(e), "\n") + NULL + } + ) + wall_s <- proc.time()[[3L]] - t0 + + if (is.null(result)) { + rows[[idx]] <- data.frame( + dataset = ds_name, n_taxa = n_taxa, nni_cycles = nni_cycles, + seed = seed, best_score = NA_real_, wall_s = NA_real_, + stringsAsFactors = FALSE + ) + next + } + + best_score <- min(attr(result, "score"), na.rm = TRUE) + cat(sprintf("score=%.0f wall=%.1fs\n", best_score, wall_s)) + + rows[[idx]] <- data.frame( + dataset = ds_name, + n_taxa = n_taxa, + nni_cycles = nni_cycles, + seed = seed, + best_score = best_score, + wall_s = wall_s, + stringsAsFactors = FALSE + ) + } + cat("\n") + } +} + +results_df <- do.call(rbind, rows) +write.csv(results_df, OUT_FILE, row.names = FALSE) +cat("\nResults written to:", OUT_FILE, "\n\n") + +# ------------------------------------------------------------ +# Analysis: Time-adjusted expected best +# ------------------------------------------------------------ +cat("===== Time-adjusted expected best (lower score = better) =====\n\n") + +for (ds_name in DATASETS) { + sub <- results_df[results_df$dataset == ds_name & !is.na(results_df$best_score), ] + cat(sprintf("--- %s ---\n", ds_name)) + + for (budget in BUDGETS_S) { + cat(sprintf(" Budget = %ds:\n", budget)) + for (nni in NNI_CONDITIONS) { + d <- sub[sub$nni_cycles == nni, ] + if (nrow(d) < 5L) { cat(sprintf(" nni=%d: insufficient data\n", nni)); next } + med_time <- median(d$wall_s, na.rm = TRUE) + k <- max(1L, floor(budget / med_time)) + eb <- expected_best(d$best_score, k) + cat(sprintf(" nni=%d: median_time=%.1fs, k=%d reps, expected_best=%.1f (n=%d)\n", + nni, med_time, k, eb, nrow(d))) + } + } + cat("\n") +} + +# Summary table: delta (nni=0 - nni=5) at each budget +cat("===== Expected-best delta (nni=0 vs nni=5, negative = nni=0 better) =====\n") +cat(sprintf("%-14s %8s %8s %8s %8s\n", + "Dataset", "30s_nni0", "30s_nni5", "60s_nni0", "60s_nni5")) +cat(strrep("-", 56), "\n") + +for (ds_name in DATASETS) { + sub <- results_df[results_df$dataset == ds_name & !is.na(results_df$best_score), ] + row_vals <- c(ds_name) + + for (budget in BUDGETS_S) { + for (nni in NNI_CONDITIONS) { + d <- sub[sub$nni_cycles == nni, ] + if (nrow(d) < 5L) { row_vals <- c(row_vals, "N/A"); next } + med_time <- median(d$wall_s, na.rm = TRUE) + k <- max(1L, floor(budget / med_time)) + eb <- expected_best(d$best_score, k) + row_vals <- c(row_vals, sprintf("%.1f", eb)) + } + } + cat(sprintf("%-14s %8s %8s %8s %8s\n", + row_vals[[1L]], row_vals[[2L]], row_vals[[3L]], + row_vals[[4L]], row_vals[[5L]])) +} + +cat("\n") +cat("Interpretation: Positive delta = nni=0 is better (removes overhead).\n") +cat("Negative delta = nni=5 is better (perturbation value exceeds overhead).\n") diff --git a/dev/benchmarks/bench_tbr_reach.R b/dev/benchmarks/bench_tbr_reach.R new file mode 100644 index 000000000..1722f4dd7 --- /dev/null +++ b/dev/benchmarks/bench_tbr_reach.R @@ -0,0 +1,72 @@ +# Is TNT's sectorial improvement even TBR-reachable from T0? Five sector mechanisms +# are null; exact-CSS showed T0 is TBR-optimal for us at max_hits=1. This isolates +# whether a THOROUGH global TBR (hold many equal trees -> traverse plateaus) from +# the identical T0 reaches TNT's sectorial score. ratchet/drift/sectors all OFF. +# tbr50 reaches TNT => improvement was a global plateau our max_hits=1 TBR missed +# tbr50 ~ start => not TBR-reachable; needs rebuild (and our RAS is failing) +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +K <- as.integer(Sys.getenv("TS_KPASS", "8")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Wortley2006 Zanol2014")), "\\s+")[[1]] +out_csv <- Sys.getenv("OUT_CSV", "dev/benchmarks/tbr_reach.csv") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), "tbrreach"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +run_tnt <- function(phy, seed, kpass) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", kpass), "quit;") + writeLines(script, file.path(wd, "ss.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "ss.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + t0 <- tryCatch(ReadTntTree(file.path(wd, "t0.tre")), error = function(e) NULL) + if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + list(t0 = t0, s_sect = if (length(s_sect)) s_sect[length(s_sect)] else NA) +} +run_tbr <- function(d, tree, maxhits) { + set.seed(1) + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, rssRounds = 0L, + cssRounds = 0L, wagnerStarts = 1L, fuseInterval = 9999L, + tbrMaxHits = as.integer(maxhits))) + as.double(attr(r, "score")) +} + +rows <- list() +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + for (sd in seeds) { + tn <- run_tnt(phy, sd, K) + if (is.null(tn$t0)) { cat(sprintf("WARN %s s%d: no T0\n", nm, sd)); next } + start <- TreeLength(tn$t0, phy) + t1 <- run_tbr(phy, tn$t0, 1L) + t50 <- run_tbr(phy, tn$t0, 50L) + rows[[length(rows) + 1]] <- data.frame(dataset = nm, seed = sd, start = start, + tnt = tn$s_sect, tbr1 = t1, tbr50 = t50, + g_tbr1 = t1 - tn$s_sect, g_tbr50 = t50 - tn$s_sect, stringsAsFactors = FALSE) + cat(sprintf("%-11s s%d | start=%.0f TNT=%.0f | tbr1=%.0f tbr50=%.0f | g_tbr50=%+.0f\n", + nm, sd, start, tn$s_sect, t1, t50, t50 - tn$s_sect)) + } +} +S <- do.call(rbind, rows) +cat("\n== medians (gap = TS_TBR - TNT_sect from identical T0) ==\n") +agg <- do.call(rbind, lapply(split(S, S$dataset), function(d) data.frame( + dataset = d$dataset[1], start = median(d$start), TNT = median(d$tnt), + tbr1 = median(d$tbr1), tbr50 = median(d$tbr50), + g_tbr50 = median(d$g_tbr50)))) +print(agg, row.names = FALSE) +dir.create(dirname(out_csv), showWarnings = FALSE, recursive = TRUE) +write.csv(S, out_csv, row.names = FALSE) +cat(sprintf("\nWrote %s\n", out_csv)) diff --git a/dev/benchmarks/bench_tnt_headtohead.R b/dev/benchmarks/bench_tnt_headtohead.R new file mode 100644 index 000000000..99c13f8e1 --- /dev/null +++ b/dev/benchmarks/bench_tnt_headtohead.R @@ -0,0 +1,153 @@ +# TreeSearch vs TNT head-to-head — Phase 0 baseline harness. +# +# Establishes the authoritative, apples-to-apples picture against TNT 1.6: +# * gap A (scoring method): TreeSearch Brazeau three-pass on RAW inapplicable +# data vs TNT column-Fitch — NOT a search gap, shown for context. +# * gap B (search quality): TreeSearch Fitch (-> "?") vs TNT, same objective. +# * candidates-per-improvement: TreeSearch `candidates_evaluated` (the new +# instrumentation) vs TNT "Total rearrangements examined". Both quantities +# are bitness-independent, so the LOCAL 32-bit TNT gives a valid comparison +# of search efficiency; only wall-clock ratio needs 64-bit TNT (Hamilton). +# +# Modes (TS_MODE): +# "converge" (default) — each engine runs to its natural convergence (capped +# by TS_SECONDS as a safety timeout). Compares best score + candidate +# counts. This is the candidates-per-improvement baseline. +# "budget" — both engines run to a fixed wall-clock (TS_SECONDS). +# For the wall-clock ratio; only meaningful with a fair (64-bit) TNT. +# +# Env vars (all optional): +# TS_LIB library path for the instrumented TreeSearch build (.agent-p0) +# TNT_EXE path to tnt.exe (default: local 32-bit 1.6) +# TS_DATASETS space-separated dataset names from inapplicable.phyData +# TS_SEEDS space-separated integer seeds +# TS_SECONDS safety timeout (converge) or budget (budget mode), seconds +# TS_MODE "converge" | "budget" +# TNT_REPLIC TNT xmult replicates (default 50) +# TNT_HITS TNT xmult target hits (default 10) +# OUT_CSV output CSV path (default dev/benchmarks/headtohead_latest.csv) + +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-p0"), + winslash = "/")) + library(TreeTools) +}) + +TNT_EXE <- Sys.getenv("TNT_EXE", + "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +mode <- Sys.getenv("TS_MODE", "converge") +secs <- as.double(Sys.getenv("TS_SECONDS", "60")) +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +replic <- as.integer(Sys.getenv("TNT_REPLIC", "50")) +hits <- as.integer(Sys.getenv("TNT_HITS", "10")) +out_csv <- Sys.getenv("OUT_CSV", "dev/benchmarks/headtohead_latest.csv") +dsNames <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Eklund2004 Zanol2014 Zhu2013 Giles2015 Dikow2009")), + "\\s+")[[1]] + +data("inapplicable.phyData", package = "TreeSearch") + +# --- TNT helpers ---------------------------------------------------------- +# TNT parses the script-name ARG as a command line (splits on digits/_), so the +# script basename must be purely alphabetic; data files read by `proc` are fine. +tnt_work <- file.path(tempdir(), "tntwork") +dir.create(tnt_work, showWarnings = FALSE, recursive = TRUE) + +run_tnt <- function(phy, seed, timeout_s) { + datafile <- file.path(tnt_work, "datafile.tnt") + runfile <- file.path(tnt_work, "htnt.run") + WriteTntCharacters(phy, datafile) + to <- sprintf("%02d:%02d:%02d", timeout_s %/% 3600, + (timeout_s %% 3600) %/% 60, timeout_s %% 60) + script <- paste( + "mxram 1024;", + sprintf("proc %s;", basename(datafile)), + "hold 10000;", + sprintf("rseed %d;", seed), + sprintf("timeout %s;", to), + sprintf("xmult=hits %d replic %d;", hits, replic), + "best;", "quit;", sep = "\n") + writeLines(script, runfile) + old <- setwd(tnt_work); on.exit(setwd(old)) + t0 <- Sys.time() + out <- tryCatch( + system2(TNT_EXE, args = paste0(basename(runfile), ";"), + stdout = TRUE, stderr = TRUE), + error = function(e) character(0)) + wall <- as.double(difftime(Sys.time(), t0, units = "secs")) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + txt <- paste(out, collapse = "\n") + score <- suppressWarnings(as.double( + sub(".*Best score:\\s*([0-9.]+).*", "\\1", + grep("Best score:", out, value = TRUE)[1]))) + rearr <- suppressWarnings(as.double(gsub(",", "", + sub(".*Total rearrangements examined:\\s*([0-9,]+).*", "\\1", + grep("Total rearrangements examined:", out, value = TRUE)[1])))) + list(score = score, rearr = rearr, wall = wall) +} + +# --- TreeSearch helper ---------------------------------------------------- +fitch_convert <- function(phy) { + m <- PhyDatToMatrix(phy, ambigNA = FALSE) + m[m == "-"] <- "?" + MatrixToPhyDat(m) +} + +run_ts <- function(phy, seed, timeout_s) { + set.seed(seed) + maxRep <- if (mode == "budget") 9999L else max(replic, 50L) + t0 <- Sys.time() + r <- suppressWarnings(MaximizeParsimony( + phy, maxReplicates = maxRep, nThreads = 1L, strategy = "auto", + maxSeconds = if (mode == "budget") timeout_s else timeout_s, + verbosity = 0L)) + wall <- as.double(difftime(Sys.time(), t0, units = "secs")) + list(score = attr(r, "score"), + cand = attr(r, "candidates_evaluated"), + reps = attr(r, "replicates"), + wall = wall) +} + +# --- Run panel ------------------------------------------------------------ +cat(sprintf("Head-to-head | mode=%s | %d datasets | seeds {%s} | cap %gs\n TNT: %s\n", + mode, length(dsNames), paste(seeds, collapse = ","), secs, TNT_EXE)) +cat(strrep("-", 92), "\n") + +rows <- list() +for (nm in dsNames) { + raw <- inapplicable.phyData[[nm]] + fitch <- fitch_convert(raw) + for (sd in seeds) { + ts_f <- run_ts(fitch, sd, secs) + ts_r <- run_ts(raw, sd, secs) # gap A: Brazeau three-pass + tnt <- run_tnt(fitch, sd, secs) + rows[[length(rows) + 1]] <- data.frame( + dataset = nm, tips = length(raw), seed = sd, + ts_fitch = ts_f$score, ts_raw = ts_r$score, tnt = tnt$score, + gapB = ts_f$score - tnt$score, + ts_cand = ts_f$cand, tnt_rearr = tnt$rearr, + cand_ratio = round(ts_f$cand / tnt$rearr, 2), + ts_wall = round(ts_f$wall, 1), tnt_wall = round(tnt$wall, 1), + ts_reps = ts_f$reps, stringsAsFactors = FALSE) + } +} +res <- do.call(rbind, rows) + +# --- Per-dataset summary -------------------------------------------------- +agg <- do.call(rbind, lapply(split(res, res$dataset), function(d) { + data.frame( + dataset = d$dataset[1], tips = d$tips[1], + ts_fitch_best = min(d$ts_fitch), ts_fitch_med = median(d$ts_fitch), + tnt_best = min(d$tnt), gapB_med = median(d$gapB), + ts_raw_med = median(d$ts_raw), # gap A context + ts_cand_med = median(d$ts_cand), tnt_rearr_med = median(d$tnt_rearr), + cand_ratio_med = median(d$cand_ratio), + ts_wall_med = median(d$ts_wall), tnt_wall_med = median(d$tnt_wall), + stringsAsFactors = FALSE) +})) +agg <- agg[order(-agg$gapB_med), ] + +print(agg, row.names = FALSE) +dir.create(dirname(out_csv), showWarnings = FALSE, recursive = TRUE) +write.csv(res, out_csv, row.names = FALSE) +cat(sprintf("\nPer-run rows written to %s\n", out_csv)) diff --git a/dev/benchmarks/bench_tnt_settings.R b/dev/benchmarks/bench_tnt_settings.R new file mode 100644 index 000000000..14b4f4139 --- /dev/null +++ b/dev/benchmarks/bench_tnt_settings.R @@ -0,0 +1,511 @@ +# bench_tnt_settings.R +# +# TNT 1.6 Settings Survey: time-to-best-score across search configurations. +# +# METRIC: time-to-target (TTT) — wall-clock seconds for TNT to first reach +# the best known score B for each dataset. Censored (NA) when B not reached +# within TIMEOUT_S. This is TNT-vs-TNT, so relative wall-clock is valid. +# +# MACHINE METADATA (embedded at write time): +# Hostname : DW-CZC429715G +# CPU : 12th Gen Intel(R) Core(TM) i7-12700 +# RAM : 15.7 GB +# TNT : C:/Programs/Phylogeny/tnt/tnt.exe (v1.6, 32-bit) +# Date : 2026-06-17 +# +# USAGE: +# source("dev/benchmarks/bench_tnt_settings.R") +# tnt_settings_validate() # Zhu2013 x sect+fuse x seed=1 (smoke test) +# results <- tnt_settings_full() +# write.csv(results, "dev/benchmarks/tnt_settings_survey.csv", row.names=FALSE) +# +# ENV OVERRIDES: +# TNT_EXE path to tnt.exe (default: C:/Programs/Phylogeny/tnt/tnt.exe) +# TNT_TIMEOUT per-run timeout in seconds (default: 120) +# TNT_SEEDS seeds per (config,dataset) (default: 5) +# TNT_B_TIMEOUT Phase-1 per-seed timeout in seconds (default: 300) +# TNT_B_SEEDS Phase-1 seeds to find B (default: 10) +# TNT_DATASETS comma-separated dataset names (default: 6 gap sets) +# +# REQUIRES: TreeSearch, TreeTools (with PhyDatToMatrix, MatrixToPhyDat, +# WriteTntCharacters, inapplicable.phyData) + +library(TreeSearch) +library(TreeTools) + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +TNT_EXE <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/tnt.exe") +TIMEOUT_S <- as.integer(Sys.getenv("TNT_TIMEOUT", "120")) +N_SEEDS <- as.integer(Sys.getenv("TNT_SEEDS", "5")) +B_TIMEOUT <- as.integer(Sys.getenv("TNT_B_TIMEOUT", "300")) +B_SEEDS <- as.integer(Sys.getenv("TNT_B_SEEDS", "10")) +STAGING <- ".tnt-survey" + +MACHINE <- list( + hostname = "DW-CZC429715G", + cpu = "12th Gen Intel(R) Core(TM) i7-12700", + ram_gb = 15.7 +) + +GAP_DATASETS <- c("Wortley2006", "Eklund2004", "Zanol2014", + "Zhu2013", "Giles2015", "Dikow2009") + +ALL_DATASETS <- c( + "Longrich2010", "Vinther2008", "Sansom2010", "DeAssis2011", + "Aria2015", "Wortley2006", "Griswold1999", "Schulze2007", + "Eklund2004", "Agnarsson2004", "Zanol2014", "Zhu2013", + "Giles2015", "Dikow2009" +) + +# xmult options per config. +# type="single" -> xmult = giveupscore B replic 9999; +# type="level" -> xmult = level N giveupscore B replic 9999; +# type="default" -> xmult = giveupscore B replic 9999; +# TNT 1.6 quirk: `fuse` inside `xmult =` prompts interactively for a count. +# Workaround: omit `fuse`/`nofuse` for configs that WANT fusing (TNT default +# has fuse=1), and use `nofuse` only where fusing must be disabled. +# The default also has drift=5; use `nodrift` to disable it. +CONFIGS <- list( + "sect-only" = list(type = "single", opts = "rss css xss nofuse noratchet nodrift"), + "sect+fuse" = list(type = "single", opts = "rss css xss noratchet nodrift"), + "sect+ratchet" = list(type = "single", opts = "rss css xss ratchet 10 nodrift"), + "sect+drift" = list(type = "single", opts = "rss css xss drift 10 noratchet"), + "all" = list(type = "single", opts = "rss css xss ratchet 10 drift 10"), + "ratchet-only" = list(type = "single", opts = "norss nocss noxss ratchet 10 nofuse nodrift"), + "level0" = list(type = "level", level = 0L), + "level1" = list(type = "level", level = 1L), + "level2" = list(type = "level", level = 2L), + "level3" = list(type = "level", level = 3L), + "level4" = list(type = "level", level = 4L), + "level5" = list(type = "level", level = 5L), + "level10" = list(type = "level", level = 10L), + "default" = list(type = "default") +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +check_tnt <- function() { + if (!file.exists(TNT_EXE)) + stop("TNT not found at ", TNT_EXE, + ". Set env TNT_EXE to the correct path.") +} + +#' Fitch-mode phyDat: replace inapplicable "-" tokens with "?" (missing) +to_fitch <- function(phy) { + m <- PhyDatToMatrix(phy, ambigNA = FALSE) + m[m == "-"] <- "?" + MatrixToPhyDat(m) +} + +#' Export one dataset to /.tnt (Fitch mode). +#' Dispatches to export_nexus_dataset() for MorphoBank scaling sets. +#' Returns list(ntip, nchar) +export_dataset <- function(name, dir = STAGING) { + if (name %in% names(SCALE_DATASETS)) return(export_nexus_dataset(name, dir)) + phy <- inapplicable.phyData[[name]] + if (is.null(phy)) stop("Dataset not found: ", name) + d <- to_fitch(phy) + dir.create(dir, showWarnings = FALSE, recursive = TRUE) + WriteTntCharacters(d, file.path(dir, paste0(name, ".tnt"))) + list(ntip = length(d), nchar = sum(attr(d, "weight"))) +} + +#' Build xmult run-line for a given config and target B. +#' Uses giveupscore B with hits 5 replic 100 (verified: hits<=5 works with +#' giveupscore; hits>=9999 silently disables it in TNT 1.6). +#' TTT is extracted from per-replicate output (BestScore column), which gives +#' exact timing regardless of whether giveupscore or timeout triggered the stop. +xmult_run_line <- function(cfg, B) { + suffix <- sprintf("giveupscore %g hits 5 replic 100", B) + switch(cfg$type, + single = sprintf("xmult = %s %s;", cfg$opts, suffix), + level = sprintf("xmult = level %d %s;", cfg$level, suffix), + default = sprintf("xmult = %s;", suffix), + stop("Unknown config type: ", cfg$type) + ) +} + +#' Parse per-replicate output to find time of first BestScore <= B. +#' Returns list(ttb_s = seconds or NA, first_rep = integer or NA) +parse_ttt <- function(txt, B) { + lines <- strsplit(txt, "\n", fixed = TRUE)[[1L]] + # Per-replicate lines look like: + # "57 TBR 2 624 624 0:00:08 358,825,811" + # Columns: rep algor tree score best_score time rearrangs + # Interrupted lines may have "------" for score/best fields — skip those. + pat <- "^\\s*(\\d+)\\s+\\S+\\s+\\d+\\s+(\\d+(?:\\.\\d+)?)\\s+(\\d+(?:\\.\\d+)?)\\s+(\\d+:\\d{2}:\\d{2})" + for (ln in lines) { + m <- regmatches(ln, regexec(pat, ln, perl = TRUE))[[1L]] + if (length(m) < 5L) next + best_sc <- suppressWarnings(as.numeric(m[4L])) # m[1]=full, m[2]=rep, m[3]=score, m[4]=best, m[5]=time + if (is.na(best_sc) || best_sc > B + 1e-6) next + tp <- as.integer(strsplit(m[5L], ":")[[1L]]) + ttb_s <- tp[1L] * 3600L + tp[2L] * 60L + tp[3L] + return(list(ttb_s = as.double(ttb_s), first_rep = as.integer(m[2L]))) + } + list(ttb_s = NA_real_, first_rep = NA_integer_) +} + +#' Write a survey script and return its path (filename: tntsurvey.run) +write_survey_script <- function(data_file, cfg, B, seed, + timeout_s = TIMEOUT_S, + dir = STAGING) { + if (!is.finite(B)) stop("B must be finite; Phase 1 did not find a valid score.") + hh <- timeout_s %/% 3600 + mm <- (timeout_s %% 3600) %/% 60 + ss <- timeout_s %% 60 + lines <- c( + "mxram 1500;", + sprintf("proc %s;", data_file), + sprintf("rseed %d;", seed), + "hold 1000;", + sprintf("timeout %d:%02d:%02d;", hh, mm, ss), + xmult_run_line(cfg, B), + "best;", + "quit;" + ) + path <- file.path(dir, "tntsurvey.run") + writeLines(lines, path) + normalizePath(path, winslash = "/") +} + +#' Write a Phase-1 script (find best score, no giveupscore target) +#' Uses TNT defaults (fuse=1, drift=5, rss+css) — no `fuse` keyword to avoid +#' the interactive prompt; hits 5 replic 10 gives 5 convergence confirmations. +write_phase1_script <- function(data_file, seed, + timeout_s = B_TIMEOUT, + dir = STAGING) { + hh <- timeout_s %/% 3600 + mm <- (timeout_s %% 3600) %/% 60 + ss <- timeout_s %% 60 + lines <- c( + "mxram 1500;", + sprintf("proc %s;", data_file), + sprintf("rseed %d;", seed), + "hold 1000;", + sprintf("timeout %d:%02d:%02d;", hh, mm, ss), + "xmult = hits 5 replic 10;", + "best;", + "quit;" + ) + path <- file.path(dir, "tntphaseone.run") + writeLines(lines, path) + normalizePath(path, winslash = "/") +} + +#' Run a TNT script from dir; return list(score, rearr, wall_s, raw) +run_tnt <- function(script_path, dir = STAGING, hard_timeout_s = NULL) { + check_tnt() + if (is.null(hard_timeout_s)) hard_timeout_s <- TIMEOUT_S + 60L + + old_wd <- setwd(normalizePath(dir)) + on.exit(setwd(old_wd), add = TRUE) + + t0 <- Sys.time() + raw <- tryCatch( + withCallingHandlers( + system2(TNT_EXE, + args = paste0(basename(script_path), ";"), + stdout = TRUE, stderr = TRUE, + timeout = hard_timeout_s), + warning = function(w) invokeRestart("muffleWarning") + ), + error = function(e) character(0) + ) + wall_s <- as.double(difftime(Sys.time(), t0, units = "secs")) + + txt <- paste(iconv(raw, from = "", to = "UTF-8", sub = ""), collapse = "\n") + + score <- NA_real_ + m_sc <- regmatches(txt, regexpr( + "Best score(?:\\s+\\(TBR\\))?:\\s+[0-9]+\\.?[0-9]*", txt, perl = TRUE)) + if (length(m_sc) == 1L) + score <- as.numeric(sub(".*:\\s+", "", m_sc)) + + rearr <- NA_real_ + m_rr <- regmatches(txt, regexpr( + "Total rearrangements examined:\\s+[0-9,]+", txt, perl = TRUE)) + if (length(m_rr) == 1L) + rearr <- as.numeric(gsub("[^0-9]", "", sub(".*:\\s+", "", m_rr))) + + list(score = score, rearr = rearr, wall_s = wall_s, raw = txt) +} + +# --------------------------------------------------------------------------- +# Phase 1: establish B per dataset +# --------------------------------------------------------------------------- + +#' Find best achievable score B for each dataset using the thorough config. +#' Runs b_seeds seeds, each up to b_timeout_s seconds. +#' Returns named numeric vector: dataset -> B. +establish_B <- function(dataset_names = GAP_DATASETS, + b_seeds = seq_len(B_SEEDS), + b_timeout = B_TIMEOUT) { + dir.create(STAGING, showWarnings = FALSE, recursive = TRUE) + B_map <- setNames(rep(Inf, length(dataset_names)), dataset_names) + + for (nm in dataset_names) { + info <- export_dataset(nm) + cat(sprintf("Phase1 %s (%dt %dc):", nm, info$ntip, info$nchar)) + for (s in b_seeds) { + script <- write_phase1_script(paste0(nm, ".tnt"), s, + timeout_s = b_timeout) + res <- run_tnt(script, hard_timeout_s = b_timeout + 60L) + if (!is.na(res$score)) B_map[[nm]] <- min(B_map[[nm]], res$score) + cat(sprintf(" %g(%.0fs)", res$score, res$wall_s)) + } + cat(sprintf(" => B=%g\n", B_map[[nm]])) + } + B_map +} + +# --------------------------------------------------------------------------- +# Phase 2: TTT per (config, dataset, seed) +# --------------------------------------------------------------------------- + +#' Run the full settings survey and return a data frame. +#' If outfile is set, each row is appended to CSV immediately (crash recovery). +run_survey <- function(dataset_names = GAP_DATASETS, + B_map, + configs = CONFIGS, + seeds = seq_len(N_SEEDS), + timeout_s = TIMEOUT_S, + outfile = NULL) { + dir.create(STAGING, showWarnings = FALSE, recursive = TRUE) + + total <- length(configs) * length(dataset_names) * length(seeds) + idx <- 0L + rows <- vector("list", total) + wrote_header <- FALSE + + for (cfg_nm in names(configs)) { + cfg <- configs[[cfg_nm]] + for (nm in dataset_names) { + B <- B_map[[nm]] + info <- export_dataset(nm) + data_file <- paste0(nm, ".tnt") + + for (s in seeds) { + idx <- idx + 1L + cat(sprintf("[%d/%d] %-14s %-12s seed=%d B=%-6g ", + idx, total, cfg_nm, nm, s, B)) + + script <- write_survey_script(data_file, cfg, B, s, + timeout_s = timeout_s) + res <- run_tnt(script, hard_timeout_s = timeout_s + 60L) + + # TTT determination: + # 1. Primary: parse per-replicate "BestScore" column (exact, 1s resolution) + # 2. Fallback: process wall_s when giveupscore fired mid-rep ("------") + # and per-replicate TTT is NA or 0 but final_score <= B + reached_B_final <- isTRUE(!is.na(res$score) && res$score <= B + 1e-6) + ttt <- parse_ttt(res$raw, B) + if (!is.na(ttt$ttb_s) && ttt$ttb_s > 0) { + reached <- TRUE + ttb <- ttt$ttb_s + } else if (reached_B_final) { + # giveupscore triggered mid-rep; use process wall_s (accurate for <1s runs) + reached <- TRUE + ttb <- round(res$wall_s, 3) + } else { + reached <- FALSE + ttb <- NA_real_ + } + + cat(sprintf("score=%-6s reached=%-5s ttt=%.1fs\n", + if (is.na(res$score)) "NA" else as.character(res$score), + reached, if (reached) ttb else res$wall_s)) + + rows[[idx]] <- data.frame( + machine = MACHINE$hostname, + cpu = MACHINE$cpu, + ram_gb = MACHINE$ram_gb, + config = cfg_nm, + dataset = nm, + ntip = info$ntip, + seed = s, + B = B, + reached_B = reached, + wall_s = ttb, # NA = censored (did not reach B) + final_score = res$score, + rearr = res$rearr, + stringsAsFactors = FALSE + ) + + if (!is.null(outfile)) { + write.table(rows[[idx]], outfile, + append = wrote_header, sep = ",", + row.names = FALSE, col.names = !wrote_header, + quote = TRUE) + wrote_header <- TRUE + } + } + } + } + do.call(rbind, rows) +} + +# --------------------------------------------------------------------------- +# Convenience entry points +# --------------------------------------------------------------------------- + +#' Smoke test: Zhu2013 x sect+fuse x seed=1 +#' Prints script, raw TNT output, and parsed result. +tnt_settings_validate <- function() { + check_tnt() + dir.create(STAGING, showWarnings = FALSE, recursive = TRUE) + + nm <- "Zhu2013" + cfg <- CONFIGS[["sect+fuse"]] + cat("=== Validation run: Zhu2013 / sect+fuse / seed=1 ===\n\n") + + cat("--- Phase 1: finding B (3 seeds x 60s) ---\n") + info <- export_dataset(nm) + cat(sprintf("Dataset: %d tips, %d chars\n", info$ntip, info$nchar)) + + best <- Inf + for (s in 1:3) { + sc <- write_phase1_script(paste0(nm, ".tnt"), s, timeout_s = 60L) + r <- run_tnt(sc, hard_timeout_s = 90L) + cat(sprintf(" seed=%d score=%g wall=%.1fs\n", s, r$score, r$wall_s)) + if (!is.na(r$score)) best <- min(best, r$score) + } + cat(sprintf(" => B = %g\n\n", best)) + + cat("--- Phase 2: giveupscore test ---\n") + sc2 <- write_survey_script(paste0(nm, ".tnt"), cfg, best, seed = 1L, + timeout_s = 60L) + cat("Script contents:\n") + cat(readLines(file.path(STAGING, "tntsurvey.run")), sep = "\n") + cat("\n\n") + + r2 <- run_tnt(sc2, hard_timeout_s = 90L) + ttt <- parse_ttt(r2$raw, best) + reached_final <- isTRUE(!is.na(r2$score) && r2$score <= best + 1e-6) + if (!is.na(ttt$ttb_s) && ttt$ttb_s > 0) { + reached <- TRUE; ttb_show <- ttt$ttb_s + } else if (reached_final) { + reached <- TRUE; ttb_show <- r2$wall_s + } else { + reached <- FALSE; ttb_show <- NA_real_ + } + cat(sprintf("Result: final_score=%s reached_B=%s proc_wall=%.1fs TTT=%s\n", + if (is.na(r2$score)) "NA" else r2$score, + reached, r2$wall_s, + if (reached) sprintf("%.1fs", ttb_show) else "CENSORED")) + cat("\nRaw TNT output:\n") + cat(r2$raw) + invisible(list(res = r2, ttt = ttt)) +} + +#' Full survey: 6 gap datasets, all configs, 5 seeds. +#' Set TNT_DATASETS env var (comma-separated) to override dataset list. +tnt_settings_full <- function(datasets = NULL, + b_timeout = B_TIMEOUT, + b_seeds = seq_len(B_SEEDS), + run_timeout = TIMEOUT_S, + run_seeds = seq_len(N_SEEDS)) { + check_tnt() + + if (is.null(datasets)) { + env_ds <- Sys.getenv("TNT_DATASETS", "") + datasets <- if (nchar(env_ds) > 0) + trimws(strsplit(env_ds, ",")[[1]]) + else + GAP_DATASETS + } + + cat("=== TNT 1.6 Settings Survey ===\n") + cat(sprintf("Machine : %s\n", MACHINE$hostname)) + cat(sprintf("CPU : %s\n", MACHINE$cpu)) + cat(sprintf("RAM : %.1f GB\n", MACHINE$ram_gb)) + cat(sprintf("TNT : %s\n", TNT_EXE)) + cat(sprintf("Date : %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S"))) + cat(sprintf("Datasets: %s\n", paste(datasets, collapse = ", "))) + cat(sprintf("Configs : %d\n", length(CONFIGS))) + cat(sprintf("Seeds : %d\n", length(run_seeds))) + cat(sprintf("Timeout : %ds per run\n\n", run_timeout)) + + B_map <- establish_B(datasets, b_seeds, b_timeout) + cat("\nB values:\n") + print(B_map) + cat("\n") + + outfile <- file.path("dev/benchmarks", "tnt_settings_survey.csv") + results <- run_survey(datasets, B_map, CONFIGS, run_seeds, run_timeout, + outfile = outfile) + cat(sprintf("\nResults written incrementally to %s\n", outfile)) + results +} + +# --------------------------------------------------------------------------- +# Scaling survey: larger MorphoBank datasets (above n=90 sector inflection) +# --------------------------------------------------------------------------- + +NEOTRANS_DIR <- normalizePath("../neotrans/inst/projects", winslash = "/", + mustWork = FALSE) + +SCALE_DATASETS <- list( + "project691" = list(path = file.path(NEOTRANS_DIR, "project691.nex")), + "project4230" = list(path = file.path(NEOTRANS_DIR, "project4230.nex")), + "project4103" = list(path = file.path(NEOTRANS_DIR, "project4103.nex")), + "project3763" = list(path = file.path(NEOTRANS_DIR, "project3763.nex")) +) + +#' Export a MorphoBank NEXUS dataset to /.tnt (Fitch mode). +#' Parenthesised polymorphisms (e.g. "(0,1)") are recoded as "?" before reading. +export_nexus_dataset <- function(name, dir = STAGING) { + info <- SCALE_DATASETS[[name]] + if (is.null(info)) stop("Unknown scaling dataset: ", name) + if (!requireNamespace("phangorn", quietly = TRUE)) + stop("phangorn required for NEXUS reading: install.packages('phangorn')") + lines <- readLines(info$path) + lines <- gsub("\\([0-9,]+\\)", "?", lines, perl = TRUE) + tmp <- tempfile(fileext = ".nex") + on.exit(unlink(tmp), add = TRUE) + writeLines(lines, tmp) + phy <- phangorn::read.phyDat(tmp, format = "nexus", type = "STANDARD") + d <- to_fitch(phy) + dir.create(dir, showWarnings = FALSE, recursive = TRUE) + WriteTntCharacters(d, file.path(dir, paste0(name, ".tnt"))) + list(ntip = length(d), nchar = sum(attr(d, "weight"))) +} + +#' Scaling survey: 4 MorphoBank datasets (103-205 taxa), all 14 configs, 3 seeds. +#' Uses longer timeouts than the gap-dataset survey (300s/run, 600s Phase-1). +tnt_scaling_full <- function(datasets = names(SCALE_DATASETS), + b_timeout = 600L, + b_seeds = seq_len(5L), + run_timeout = 300L, + run_seeds = seq_len(3L)) { + check_tnt() + if (!requireNamespace("phangorn", quietly = TRUE)) + stop("phangorn required: install.packages('phangorn')") + + cat("=== TNT 1.6 Scaling Survey ===\n") + cat(sprintf("Machine : %s\n", MACHINE$hostname)) + cat(sprintf("CPU : %s\n", MACHINE$cpu)) + cat(sprintf("RAM : %.1f GB\n", MACHINE$ram_gb)) + cat(sprintf("TNT : %s\n", TNT_EXE)) + cat(sprintf("Date : %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S"))) + cat(sprintf("Datasets: %s\n", paste(datasets, collapse = ", "))) + cat(sprintf("Configs : %d\n", length(CONFIGS))) + cat(sprintf("Seeds : %d\n", length(run_seeds))) + cat(sprintf("Timeout : %ds per run\n\n", run_timeout)) + + B_map <- establish_B(datasets, b_seeds, b_timeout) + cat("\nB values:\n") + print(B_map) + cat("\n") + + outfile <- file.path("dev/benchmarks", "tnt_scaling_survey.csv") + results <- run_survey(datasets, B_map, CONFIGS, run_seeds, run_timeout, + outfile = outfile) + cat(sprintf("\nResults written incrementally to %s\n", outfile)) + results +} diff --git a/dev/benchmarks/bench_trajectory.R b/dev/benchmarks/bench_trajectory.R new file mode 100644 index 000000000..bb7fef113 --- /dev/null +++ b/dev/benchmarks/bench_trajectory.R @@ -0,0 +1,416 @@ +#!/usr/bin/env Rscript +# T-251: TNT vs TreeSearch trajectory comparison +# +# Captures per-replicate search trajectories from both engines on the +# datasets where TNT has the largest score advantage. Focuses on: +# - Score vs wall-clock time +# - Rearrangements per improvement (TNT) vs phase cost per improvement (TS) +# - Escape effectiveness (delta from ratchet/drift/sectorial) +# +# Usage: +# source("dev/benchmarks/bench_trajectory.R") +# results <- trajectory_compare() # all 3 gap datasets, 30s +# results <- trajectory_compare_quick() # Wortley2006 only, 10s + +library(TreeSearch) +library(TreeTools) +library(dplyr) + +TNT_EXE <- "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe" +STAGING_DIR <- ".tnt-bench" +dir.create(STAGING_DIR, showWarnings = FALSE, recursive = TRUE) + +# Datasets with largest persistent gaps (from T-249): +# Geisler2001 +21, Zhu2013 +8, Wortley2006 +7, Conrad2008 +5, Zanol2014 +4 +GAP_DATASETS <- c("Geisler2001", "Zhu2013", "Wortley2006") + +# ---- Data preparation ---- + +prepare_dataset <- function(name) { + ds <- inapplicable.phyData[[name]] + # Convert inapplicable to missing to match TNT's default Fitch scoring + mat <- PhyDatToMatrix(ds) + mat[mat == "-"] <- "?" + ds_clean <- MatrixToPhyDat(mat) + + # Export for TNT + tnt_path <- file.path(STAGING_DIR, paste0(name, ".tnt")) + WriteTntCharacters(ds_clean, filepath = tnt_path) + + # Prepare for TreeSearch C++ bridge + at <- attributes(ds_clean) + list( + name = name, + phyDat = ds_clean, + contrast = at$contrast, + tip_data = matrix(unlist(ds_clean, use.names = FALSE), + nrow = length(ds_clean), byrow = TRUE), + weight = at$weight, + levels = at$levels, + n_taxa = length(ds_clean), + n_chars = sum(at$weight), + tnt_file = paste0(name, ".tnt") + ) +} + +# ---- TNT trajectory capture ---- + +run_tnt_trajectory <- function(data_file, timeout_s = 30, seed = 1, + hits = 10L, reps = 100L) { + commands <- c( + "mxram 1024;", + sprintf("proc %s;", data_file), + "hold 10000;", + sprintf("rseed %d;", seed), + sprintf("timeout %d:%02d:%02d;", + timeout_s %/% 3600, (timeout_s %% 3600) %/% 60, timeout_s %% 60), + sprintf("xmult=hits %d replic %d;", hits, reps), + "best;", + "quit;" + ) + + script_path <- file.path(STAGING_DIR, "tntbench.run") + writeLines(commands, script_path) + + old_wd <- setwd(STAGING_DIR) + on.exit(setwd(old_wd), add = TRUE) + + t0 <- proc.time() + output <- withCallingHandlers( + system2(TNT_EXE, args = "tntbench.run;", + stdout = TRUE, stderr = TRUE, timeout = timeout_s + 60), + warning = function(w) invokeRestart("muffleWarning") + ) + wall_s <- as.double((proc.time() - t0)[3]) + + output <- iconv(output, from = "", to = "UTF-8", sub = "") + parse_tnt_trajectory(output, wall_s) +} + +parse_tnt_trajectory <- function(output, wall_s) { + out_text <- paste(output, collapse = "\n") + + # TNT uses \r for progress bars — split on \r to get individual lines + raw_text <- paste(output, collapse = "\n") + all_lines <- unlist(strsplit(raw_text, "[\r\n]+")) + all_lines <- trimws(all_lines) + + # Parse per-replicate lines: + # "1 SECT 6 1301 1301 0:00:01 22,678,443" + # "5 FUSE 20 ------ ------ 0:00:04 100,410,686" + # Score and Best Score fields can be "------" + rep_pattern <- "(\\d+)\\s+(SECT|FUSE|RATCH|DRIFT|CSS|RAT|RAS|SPR|TBR|FUS)\\s+(\\d+)\\s+(-{2,}|\\d+)\\s+(-{2,}|\\d+)\\s+(\\d+:\\d+:\\d+)\\s+([0-9,]+)" + + reps <- list() + for (line in all_lines) { + m <- regmatches(line, gregexpr(rep_pattern, line, perl = TRUE))[[1]] + for (match in m) { + parts <- regmatches(match, regexec(rep_pattern, match, perl = TRUE))[[1]] + if (length(parts) >= 8) { + time_parts <- as.integer(strsplit(parts[7], ":")[[1]]) + secs <- time_parts[1] * 3600 + time_parts[2] * 60 + time_parts[3] + reps[[length(reps) + 1]] <- data.frame( + replicate = as.integer(parts[2]), + algorithm = parts[3], + trees = as.integer(parts[4]), + score = if (grepl("-", parts[5])) NA_integer_ else as.integer(parts[5]), + best_score = if (grepl("-", parts[6])) NA_integer_ else as.integer(parts[6]), + time_s = secs, + rearrangements = as.numeric(gsub(",", "", parts[8])), + stringsAsFactors = FALSE + ) + } + } + } + + # Parse totals (use raw_text which includes all \r-separated content) + total_rearr <- NA_real_ + m <- regmatches(raw_text, regexpr("Total rearrangements examined:\\s+([0-9,]+)", raw_text)) + if (length(m) == 1) { + total_rearr <- as.numeric(gsub("[^0-9]", "", sub("Total rearrangements examined:\\s+", "", m))) + } + + best_score <- NA_real_ + m <- regmatches(raw_text, regexpr("Best score:\\s+([0-9.]+)", raw_text)) + if (length(m) == 1) best_score <- as.numeric(sub("Best score:\\s+", "", m)) + + list( + trajectory = if (length(reps) > 0) do.call(rbind, reps) else NULL, + total_rearrangements = total_rearr, + best_score = best_score, + wall_s = wall_s, + raw_output = output + ) +} + +# ---- TreeSearch trajectory capture ---- + +run_ts_trajectory <- function(ds, timeout_s = 30, seed = 1, + hits = 10L, reps = 100L) { + # Capture verbosity=2 output by redirecting Rprintf + set.seed(seed) + + # Use a text connection to capture the C++ Rprintf output + log_file <- tempfile(fileext = ".txt") + t0 <- proc.time() + + # Capture C++ Rprintf output via output diversion + log_con <- file(log_file, open = "wt") + sink(log_con, type = "output") + + result <- tryCatch( + TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = as.integer(reps), + targetHits = as.integer(hits), + maxSeconds = as.double(timeout_s), + verbosity = 2L, + nThreads = 1L, + # Match current default strategy + ratchetCycles = 12L, + ratchetPerturbProb = 0.25, + driftCycles = 2L, + nniFirst = TRUE, + outerCycles = 1L, + maxOuterResets = 2L, + adaptiveLevel = TRUE + ), + finally = { + sink(type = "output") + close(log_con) + } + ) + wall_s <- as.double((proc.time() - t0)[3]) + + log_lines <- readLines(log_file, warn = FALSE) + unlink(log_file) + + parse_ts_trajectory(log_lines, result, wall_s) +} + +parse_ts_trajectory <- function(log_lines, result, wall_s) { + # Parse per-replicate, per-phase data from verbosity=2 output + # Format: " Phase score: NNNN [NNN ms total]" + # Replicate headers: "Replicate N/M" or "Replicate N/M (best: N, pool: N, hits: N)" + + phases <- list() + current_rep <- 0L + cumulative_ms <- 0 + + for (line in log_lines) { + # Replicate header + rep_match <- regmatches(line, regexec("Replicate (\\d+)/(\\d+)", line))[[1]] + if (length(rep_match) >= 2) { + current_rep <- as.integer(rep_match[2]) + next + } + + # Phase line: " Phase score: NNNN [NNN ms]" or " Phase score: NNNN [NNN ms total]" + phase_match <- regmatches( + line, + regexec("^\\s+(\\S+)\\s+.*score:\\s+(\\d+)\\s+\\[(\\d+\\.?\\d*)\\s+ms", line) + )[[1]] + if (length(phase_match) >= 4) { + phase_name <- sub("_.*", "", phase_match[2]) + score <- as.integer(phase_match[3]) + ms <- as.numeric(phase_match[4]) + + phases[[length(phases) + 1]] <- data.frame( + replicate = current_rep, + phase = phase_name, + score = score, + phase_ms = ms, + stringsAsFactors = FALSE + ) + next + } + + # Wagner line: " wag_rand+NNI tree score: NNNN [NNN ms]" + wag_match <- regmatches( + line, + regexec("^\\s+wag.*score:\\s+(\\d+)\\s+\\[(\\d+\\.?\\d*)\\s+ms", line) + )[[1]] + if (length(wag_match) >= 3) { + phases[[length(phases) + 1]] <- data.frame( + replicate = current_rep, + phase = "Wagner", + score = as.integer(wag_match[2]), + phase_ms = as.numeric(wag_match[3]), + stringsAsFactors = FALSE + ) + next + } + + # Outer cycle reset line + reset_match <- regmatches( + line, + regexec("Outer cycle improved.*\\((\\d+) -> (\\d+)\\)", line) + )[[1]] + if (length(reset_match) >= 3) { + phases[[length(phases) + 1]] <- data.frame( + replicate = current_rep, + phase = "Reset", + score = as.integer(reset_match[3]), + phase_ms = 0, + stringsAsFactors = FALSE + ) + } + } + + trajectory <- if (length(phases) > 0) do.call(rbind, phases) else NULL + + list( + trajectory = trajectory, + best_score = result$best_score, + replicates = result$replicates, + hits = result$hits_to_best, + wall_s = wall_s, + timings = result$timings, + log_lines = log_lines + ) +} + +# ---- Main comparison ---- + +trajectory_compare <- function(datasets = GAP_DATASETS, + timeout_s = 30, seeds = 1:3) { + results <- list() + + for (nm in datasets) { + cat(sprintf("\n=== %s ===\n", nm)) + ds <- prepare_dataset(nm) + cat(sprintf(" %d taxa, %d chars\n", ds$n_taxa, ds$n_chars)) + + for (seed in seeds) { + cat(sprintf(" Seed %d: ", seed)) + key <- paste0(nm, "_s", seed) + + # TNT + cat("TNT... ") + tnt <- run_tnt_trajectory(ds$tnt_file, timeout_s = timeout_s, + seed = seed, hits = 10L, reps = 100L) + cat(sprintf("%.0f (%.1fs, %.0fM rearr) | ", tnt$best_score, + tnt$wall_s, tnt$total_rearrangements / 1e6)) + + # TreeSearch + cat("TS... ") + ts <- run_ts_trajectory(ds, timeout_s = timeout_s, + seed = seed, hits = 10L, reps = 100L) + cat(sprintf("%.0f (%.1fs, %d reps)\n", ts$best_score, + ts$wall_s, ts$replicates)) + + results[[key]] <- list( + dataset = nm, seed = seed, n_taxa = ds$n_taxa, n_chars = ds$n_chars, + tnt = tnt, ts = ts + ) + } + } + + results +} + +trajectory_compare_quick <- function() { + trajectory_compare(datasets = "Wortley2006", timeout_s = 10, seeds = 1:2) +} + +# ---- Analysis helpers ---- + +summarize_trajectories <- function(results) { + rows <- list() + for (key in names(results)) { + r <- results[[key]] + tnt <- r$tnt + ts <- r$ts + + # TNT trajectory summary + tnt_traj <- tnt$trajectory + tnt_n_reps <- if (!is.null(tnt_traj)) max(tnt_traj$replicate) else NA + tnt_rearr_per_s <- if (!is.na(tnt$total_rearrangements) && tnt$wall_s > 0) { + round(tnt$total_rearrangements / tnt$wall_s / 1e6, 1) + } else NA + + # TreeSearch trajectory summary + ts_traj <- ts$trajectory + ts_n_phases <- if (!is.null(ts_traj)) nrow(ts_traj) else NA + + # Phase cost breakdown (ms) + tm <- unlist(ts$timings) + total_ms <- sum(tm) + ratchet_pct <- round(100 * tm["ratchet_ms"] / total_ms, 1) + tbr_pct <- round(100 * tm["tbr_ms"] / total_ms, 1) + drift_pct <- round(100 * tm["drift_ms"] / total_ms, 1) + xss_pct <- round(100 * tm["xss_ms"] / total_ms, 1) + css_pct <- round(100 * tm["css_ms"] / total_ms, 1) + + rows[[key]] <- data.frame( + dataset = r$dataset, seed = r$seed, + n_taxa = r$n_taxa, n_chars = r$n_chars, + tnt_score = tnt$best_score, + tnt_wall_s = round(tnt$wall_s, 2), + tnt_reps = tnt_n_reps, + tnt_rearr_M = round(tnt$total_rearrangements / 1e6, 1), + tnt_rearr_per_s_M = tnt_rearr_per_s, + ts_score = ts$best_score, + ts_wall_s = round(ts$wall_s, 2), + ts_reps = ts$replicates, + gap = ts$best_score - tnt$best_score, + ratchet_pct = ratchet_pct, tbr_pct = tbr_pct, + drift_pct = drift_pct, xss_pct = xss_pct, css_pct = css_pct, + stringsAsFactors = FALSE + ) + } + do.call(rbind, rows) +} + +# Extract per-replicate best score trajectory from TreeSearch log +ts_replicate_trajectory <- function(ts_result) { + traj <- ts_result$trajectory + if (is.null(traj)) return(NULL) + + # Get final score per replicate (last phase entry per replicate) + library(dplyr) + traj |> + group_by(replicate) |> + summarise( + rep_score = last(score), + total_phase_ms = sum(phase_ms), + n_phases = n(), + n_resets = sum(phase == "Reset"), + .groups = "drop" + ) |> + mutate( + best_so_far = cummin(rep_score), + improved = rep_score < lag(best_so_far, default = Inf) + ) +} + +# Compare escape effectiveness: how often does each perturbation phase +# actually improve the score? +ts_phase_effectiveness <- function(ts_result) { + traj <- ts_result$trajectory + if (is.null(traj)) return(NULL) + + # For each replicate, track score before and after each phase + traj |> + group_by(replicate) |> + mutate( + prev_score = lag(score, default = first(score)), + delta = prev_score - score, # positive = improvement + improved = delta > 0 + ) |> + ungroup() |> + filter(phase != "Wagner", phase != "Reset") |> + group_by(phase) |> + summarise( + n = n(), + n_improved = sum(improved), + hit_rate = round(mean(improved), 3), + mean_delta = round(mean(delta[improved]), 1), + total_ms = sum(phase_ms), + ms_per_improvement = if (sum(improved) > 0) { + round(sum(phase_ms) / sum(improved)) + } else NA_real_, + .groups = "drop" + ) |> + arrange(desc(hit_rate)) +} diff --git a/dev/benchmarks/bench_wagner.R b/dev/benchmarks/bench_wagner.R new file mode 100644 index 000000000..e42b71238 --- /dev/null +++ b/dev/benchmarks/bench_wagner.R @@ -0,0 +1,159 @@ +# ===================================================================== +# Axis A: is TreeSearch's RAS Wagner consistently DIFFERENT from TNT's? +# Compares the DISTRIBUTION (score + diversity + treespace occupancy) of +# random-addition-sequence Wagner trees (NO branch swapping) between: +# TSrand - TreeSearch ts_random_wagner_tree (deterministic first-found +# tie-break; randomness only from the addition order) +# TNTdet - TNT mult=wagner ras, default rseed] (deterministic insertion) +# TNTrand - TNT mult=wagner ras, rseed[ (random insertion = random +# tie-break among equal-best positions: diversity source we lack) +# +# A worse Wagner SCORE is not bad per se (the lead's point: a bad score is +# useful if randomness reaches a basin we'd never find post-TBR). So we report +# score AND diversity AND cross-set occupancy, and only call the methods +# "consistently different" if TSrand systematically departs from TNTdet. +# +# Env: DS (dataset, default Zanol2014), K (trees per arm, default 60), +# ARMS (comma list; default all), MDS (1 to save an MDS png). +# ===================================================================== +suppressMessages({ + library(TreeSearch, lib.loc = "C:/Users/pjjg18/GitHub/TS-selectem/.agent-selectem") + library(TreeTools) + library(TreeDist) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +nm <- Sys.getenv("DS", "Zanol2014") +K <- as.integer(Sys.getenv("K", "60")) +seed0 <- 1L + +phy <- readRDS(sprintf("dev/benchmarks/t0/%s.phy.rds", nm)) +taxa <- names(phy); n <- length(taxa) +cat(sprintf("== Wagner Axis A | %s | n=%d tips | K=%d trees/arm ==\n", nm, n, K)) + +# --- matrices for the C++ Wagner kernel (mirror AdditionTree.R) ---------- +at <- attributes(phy) +contrast <- at$contrast +tipData <- matrix(unlist(phy, use.names = FALSE), nrow = n, byrow = TRUE) +weight <- TreeSearch:::.ScaleWeight(at$weight) +levels <- at$levels + +.EdgeToPhylo <- function(edge) { + tr <- structure(list(edge = edge, tip.label = taxa, Nnode = n - 1L), + class = "phylo") + Renumber(tr) +} + +# --- TS random RAS Wagner: K trees, distinct seeds ----------------------- +TSrandTrees <- function(k) { + trees <- vector("list", k) + for (i in seq_len(k)) { + set.seed(seed0 + i) + res <- TreeSearch:::ts_random_wagner_tree(contrast, tipData, weight, levels) + trees[[i]] <- .EdgeToPhylo(res$edge) + } + structure(trees, class = "multiPhylo") +} + +# --- TNT RAS Wagner (no swap): K trees ---------------------------------- +TNTWagnerTrees <- function(k, randInsert) { + tag <- if (randInsert) "R" else "D" + wd <- file.path(tempdir(), paste0("wag", Sys.getpid(), nm, tag)) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c( + "mxram 1024;", + "proc data.tnt;", + sprintf("hold %d;", k + 50L), + sprintf("rseed %d;", seed0), + if (randInsert) "rseed[;" else "rseed];", + sprintf("mult = wagner replic %d keepall;", k), + "tsave *wag.tre;", "save;", "tsave/;", + "length;", + "quit;") + writeLines(script, file.path(wd, "wagrun.run")) + old <- setwd(wd) + out <- suppressWarnings(system2(TNT, args = "wagrun.run;", + stdout = TRUE, stderr = TRUE)) + setwd(old) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + trees <- ReadTntTree(file.path(wd, "wag.tre")) + if (!inherits(trees, "multiPhylo")) trees <- structure(list(trees), class = "multiPhylo") + attr(trees, "tntout") <- out + trees +} + +# --- score every tree with the SAME scorer (TreeLength) ------------------ +Scores <- function(trees) vapply(trees, TreeLength, double(1), phy) + +# --- run arms ------------------------------------------------------------ +armsWanted <- strsplit(Sys.getenv("ARMS", "TSrand,TNTdet,TNTrand"), ",")[[1]] +arms <- list() +if ("TSrand" %in% armsWanted) arms$TSrand <- TSrandTrees(K) +if ("TNTdet" %in% armsWanted) arms$TNTdet <- TNTWagnerTrees(K, randInsert = FALSE) +if ("TNTrand" %in% armsWanted) arms$TNTrand <- TNTWagnerTrees(K, randInsert = TRUE) + +# --- score distributions ------------------------------------------------- +cat("\n-- score distribution (TreeLength) --\n") +sc <- lapply(arms, Scores) +for (a in names(arms)) { + s <- sc[[a]] + cat(sprintf(" %-8s n=%3d mean=%.1f sd=%.1f min=%.0f max=%.0f distinct.topol=%d\n", + a, length(s), mean(s), sd(s), min(s), max(s), + length(unique(arms[[a]])))) +} + +# --- within-arm diversity (mean pairwise ClusteringInfoDist) ------------- +cat("\n-- within-arm diversity (mean pairwise ClusteringInfoDist, normalized) --\n") +divOf <- function(trees) { + d <- ClusteringInfoDist(trees, normalize = TRUE) + c(meanPair = mean(d), medNN = median(apply(as.matrix(d) + diag(Inf, length(trees)), 1, min))) +} +for (a in names(arms)) { + dv <- divOf(arms[[a]]) + cat(sprintf(" %-8s meanPairwise=%.4f medianNN=%.4f\n", a, dv["meanPair"], dv["medNN"])) +} + +# --- score-distribution tests: TSrand vs TNTdet -------------------------- +if (all(c("TSrand", "TNTdet") %in% names(arms))) { + cat("\n-- TSrand vs TNTdet score-distribution tests --\n") + w <- suppressWarnings(wilcox.test(sc$TSrand, sc$TNTdet)) + k2 <- suppressWarnings(ks.test(sc$TSrand, sc$TNTdet)) + cat(sprintf(" Mann-Whitney p=%.4g | KS D=%.3f p=%.4g | meanDiff(TSrand-TNTdet)=%+.1f\n", + w$p.value, k2$statistic, k2$p.value, mean(sc$TSrand) - mean(sc$TNTdet))) +} + +# --- cross-set occupancy: TSrand vs TNTdet (within-vs-cross NN) ---------- +if (all(c("TSrand", "TNTdet") %in% names(arms))) { + cat("\n-- treespace occupancy: TSrand vs TNTdet (ClusteringInfoDist) --\n") + pooled <- structure(c(arms$TSrand, arms$TNTdet), class = "multiPhylo") + D <- as.matrix(ClusteringInfoDist(pooled, normalize = TRUE)) + k1 <- length(arms$TSrand); idxA <- seq_len(k1); idxB <- k1 + seq_len(length(arms$TNTdet)) + nnIn <- function(rows, cols) { m <- D[rows, cols, drop = FALSE]; diag(m[, match(rows, cols), drop = FALSE]) <- Inf; + apply(m, 1, function(r) min(r[is.finite(r)])) } + # within-set NN (exclude self) and cross-set NN + withinA <- sapply(idxA, function(i) min(D[i, setdiff(idxA, i)])) + withinB <- sapply(idxB, function(i) min(D[i, setdiff(idxB, i)])) + crossAB <- sapply(idxA, function(i) min(D[i, idxB])) + crossBA <- sapply(idxB, function(i) min(D[i, idxA])) + cat(sprintf(" within TSrand NN median=%.4f | within TNTdet NN median=%.4f\n", + median(withinA), median(withinB))) + cat(sprintf(" cross TSrand->TNTdet NN median=%.4f | cross TNTdet->TSrand NN median=%.4f\n", + median(crossAB), median(crossBA))) + cat(" (cross approx within => same region; cross >> within => different regions)\n") + if (nzchar(Sys.getenv("MDS"))) { + pts <- cmdscale(as.dist(D), k = 2) + png(sprintf("dev/benchmarks/wagner_mds_%s.png", nm), width = 800, height = 800) + plot(pts, col = c(rep("red", k1), rep("blue", length(arms$TNTdet))), pch = 19, + main = sprintf("%s Wagner treespace (red=TSrand blue=TNTdet)", nm), + xlab = "MDS1", ylab = "MDS2") + dev.off() + cat(sprintf(" MDS saved: dev/benchmarks/wagner_mds_%s.png\n", nm)) + } +} + +# smoke aid: show the TNT length report tail so we can eyeball no-swap behaviour +if (!is.null(arms$TNTdet)) { + to <- attr(arms$TNTdet, "tntout") + cat("\n-- TNTdet tail (verify no-swap: lengths vary & are suboptimal) --\n") + cat(tail(to, 6), sep = "\n"); cat("\n") +} diff --git a/dev/benchmarks/bench_warmstart.R b/dev/benchmarks/bench_warmstart.R new file mode 100644 index 000000000..2cb3ccda4 --- /dev/null +++ b/dev/benchmarks/bench_warmstart.R @@ -0,0 +1,220 @@ +# Warm-start benchmark: measure ratchet/drift escape effectiveness +# +# Seeds search with a pre-computed TBR-optimal tree to isolate +# perturbation quality from initial descent quality. +# +# Usage: +# source("dev/benchmarks/bench_framework.R") +# source("dev/benchmarks/bench_warmstart.R") +# ws <- warmstart_benchmark("Agnarsson2004", replicates = 20) +# warmstart_summary(ws) + +library(TreeSearch) +library(TreeTools) + +source("dev/benchmarks/bench_datasets.R") + +#' Compute a TBR-optimal tree via a short sprint search. +#' +#' Runs a fast search (sprint strategy, 1 replicate) to produce a local +#' optimum. This tree serves as the warm-start seed for escape benchmarks. +#' +#' @param ds Prepared dataset (from prepare_ts_data). +#' @param seed RNG seed for the sprint search. +#' @return Named list with `edge` (edge matrix), `score` (optimum score), +#' and `tree` (phylo object) for inspection. +compute_warmstart_tree <- function(ds, seed = 7381L) { + set.seed(seed) + result <- TreeSearch:::ts_driven_search( + ds$contrast, ds$tip_data, ds$weight, ds$levels, + maxReplicates = 1L, + targetHits = 1L, + ratchetCycles = 0L, + driftCycles = 0L, + xssRounds = 0L, + rssRounds = 0L, + cssRounds = 0L, + nniPerturbCycles = 0L, + maxSeconds = 0, + verbosity = 0L + ) + if (result$pool_size == 0) stop("Sprint search produced no trees") + + edge_mat <- result$trees[[1]] + list( + edge = edge_mat, + score = result$best_score + ) +} + +#' Run a single warm-started replicate. +#' +#' Passes the pre-computed tree via `startEdge`, runs 1 replicate with +#' the given strategy. Since the starting tree is already TBR-optimal, +#' the initial TBR phase converges immediately; only ratchet/drift/XSS +#' perturbations can improve the score. +#' +#' @param ds Prepared dataset. +#' @param start_edge Edge matrix from compute_warmstart_tree(). +#' @param strategy Named list of strategy params (from get_strategy). +#' @param seed RNG seed for this replicate. +#' @param maxSeconds Timeout. +#' @return Named list with metrics. +warmstart_run <- function(ds, start_edge, strategy, + seed = 42L, maxSeconds = 30) { + + # Track when the score improves from the warm-start baseline + cb_env <- new.env(parent = emptyenv()) + cb_env$best <- Inf + cb_env$time_to_improvement <- NA_real_ + cb_env$trace <- list() + + progress_cb <- function(info) { + if (is.finite(info$best_score) && info$best_score < cb_env$best) { + cb_env$best <- info$best_score + cb_env$time_to_improvement <- info$elapsed + } + cb_env$trace[[length(cb_env$trace) + 1L]] <- list( + replicate = info$replicate, + elapsed = info$elapsed, + best_score = info$best_score, + phase = info$phase + ) + } + + args <- c( + list( + contrast = ds$contrast, + tip_data = ds$tip_data, + weight = ds$weight, + levels = ds$levels, + maxReplicates = 1L, + targetHits = 1L, + maxSeconds = as.double(maxSeconds), + verbosity = 1L, + startEdge = start_edge, + progressCallback = progress_cb + ), + strategy + ) + + set.seed(seed) + t0 <- proc.time() + result <- do.call(TreeSearch:::ts_driven_search, args) + wall_s <- as.double((proc.time() - t0)[3]) + + list( + best_score = result$best_score, + wall_s = wall_s, + time_to_improvement_s = cb_env$time_to_improvement, + timed_out = result$timed_out, + timings = result$timings, + trace = cb_env$trace + ) +} + +#' Run warm-start escape benchmark for one dataset. +#' +#' First computes a TBR-local-optimum via sprint, then runs multiple +#' warm-started replicates with varying seeds and strategies. +#' +#' @param ds_name Dataset name (from BENCHMARK_NAMES or LARGE_BENCHMARK_NAMES). +#' @param strategy_names Strategies to test. +#' @param replicates Independent warm-started runs per strategy. +#' @param maxSeconds Timeout per run. +#' @param warmstart_seed Seed for the initial sprint search. +#' @param base_seed Base seed for warm-started replicates. +#' @return Data frame with one row per strategy x replicate. +warmstart_benchmark <- function( + ds_name, + strategy_names = c("default", "thorough"), + replicates = 10L, + maxSeconds = 30, + warmstart_seed = 7381L, + base_seed = 42L +) { + all_ds <- load_all_benchmark_datasets() + ds <- all_ds[[ds_name]] + if (is.null(ds)) stop("Dataset '", ds_name, "' not found") + + cat(sprintf("Computing warm-start tree for %s (%d tips)...\n", + ds_name, ds$n_taxa)) + ws <- compute_warmstart_tree(ds, seed = warmstart_seed) + cat(sprintf("Warm-start score: %.5g\n\n", ws$score)) + + rows <- list() + for (strat_name in strategy_names) { + strat <- get_strategy(strat_name) + for (rep in seq_len(replicates)) { + seed <- base_seed + rep - 1L + cat(sprintf("[%s rep %d/%d] ...", strat_name, rep, replicates)) + + res <- tryCatch( + warmstart_run(ds, ws$edge, strat, seed = seed, + maxSeconds = maxSeconds), + error = function(e) { + cat(sprintf(" ERROR: %s\n", conditionMessage(e))) + NULL + } + ) + + if (is.null(res)) { + rows <- c(rows, list(data.frame( + dataset = ds_name, n_taxa = ds$n_taxa, + strategy = strat_name, replicate = rep, seed = seed, + warmstart_score = ws$score, + best_score = NA_real_, improvement = NA_real_, + wall_s = NA_real_, time_to_improvement_s = NA_real_, + timed_out = NA, + stringsAsFactors = FALSE + ))) + next + } + + improvement <- ws$score - res$best_score + cat(sprintf(" score=%.5g improvement=%.5g time=%.1fs\n", + res$best_score, improvement, res$wall_s)) + + rows <- c(rows, list(data.frame( + dataset = ds_name, n_taxa = ds$n_taxa, + strategy = strat_name, replicate = rep, seed = seed, + warmstart_score = ws$score, + best_score = res$best_score, + improvement = improvement, + wall_s = res$wall_s, + time_to_improvement_s = res$time_to_improvement_s, + timed_out = res$timed_out, + stringsAsFactors = FALSE + ))) + } + } + + do.call(rbind, rows) +} + +#' Summarize warm-start benchmark results. +#' +#' @param results Data frame from warmstart_benchmark. +#' @return Summary per strategy: median improvement, escape rate, timing. +warmstart_summary <- function(results) { + strats <- unique(results$strategy) + summaries <- list() + for (st in strats) { + sub <- results[results$strategy == st & !is.na(results$best_score), ] + if (nrow(sub) == 0) next + escaped <- sub$improvement > 0 + summaries <- c(summaries, list(data.frame( + strategy = st, + n_runs = nrow(sub), + warmstart_score = sub$warmstart_score[1], + best_found = min(sub$best_score), + median_score = median(sub$best_score), + median_improvement = median(sub$improvement), + escape_rate = round(100 * mean(escaped), 1), + median_wall_s = round(median(sub$wall_s), 2), + median_tti_s = round(median(sub$time_to_improvement_s, na.rm = TRUE), 2), + stringsAsFactors = FALSE + ))) + } + do.call(rbind, summaries) +} diff --git a/dev/benchmarks/benchmark_mp2.R b/dev/benchmarks/benchmark_mp2.R new file mode 100644 index 000000000..7cd7c0ef0 --- /dev/null +++ b/dev/benchmarks/benchmark_mp2.R @@ -0,0 +1,83 @@ +# Benchmark: MaximizeParsimony2 (C++ driven search) vs MaximizeParsimony (R loop) +# +# Compares wall-clock time and best score found on a selection of datasets +# from inapplicable.phyData, using equal-weight Fitch parsimony throughout. + +library(TreeSearch) +library(TreeTools) + +data("inapplicable.phyData") + +#' Convert inapplicable tokens to fully ambiguous for pure Fitch EW scoring +#' @param ds A phyDat object +#' @return The modified phyDat with "-" treated as "?" +strip_inapp <- function(ds) { + cont <- attr(ds, "contrast") + lvls <- attr(ds, "levels") + dash_col <- which(lvls == "-") + if (length(dash_col) == 0L) return(ds) + # Tokens that code for "-": make them fully ambiguous over applicable states + has_dash <- cont[, dash_col] == 1 + app_cols <- setdiff(seq_len(ncol(cont)), dash_col) + cont[has_dash, app_cols] <- 1 + # Drop the "-" state column + cont <- cont[, -dash_col, drop = FALSE] + attr(ds, "contrast") <- cont + attr(ds, "levels") <- lvls[-dash_col] + ds +} + +bench_datasets <- c( + "Vinther2008", # 23 tips, 50 chars + "Asher2005", # 23 tips, 125 chars + "Wortley2006", # 37 tips, 105 chars + "Wills2012", # 55 tips, 87 chars + "Agnarsson2004", # 62 tips, 225 chars + "Dikow2009" # 88 tips, 204 chars +) + +results <- data.frame( + dataset = character(), tips = integer(), patterns = integer(), + mp2_score = numeric(), mp1_score = numeric(), score_diff = numeric(), + mp2_time = numeric(), mp1_time = numeric(), speedup = numeric(), + stringsAsFactors = FALSE +) + +for (nm in bench_datasets) { + ds <- strip_inapp(inapplicable.phyData[[nm]]) + n_tip <- length(ds) + n_pat <- attr(ds, "nr") + cat("\n---", nm, "(", n_tip, "tips,", n_pat, "pat) ---\n") + + # --- MaximizeParsimony2 (C++ driven search) --- + set.seed(6218) + t2 <- system.time({ + r2 <- MaximizeParsimony2(ds, verbosity = 0L) + }) + s2 <- TreeLength(r2[[1]], ds) + + # --- MaximizeParsimony (R loop) --- + set.seed(6218) + t1 <- system.time({ + r1 <- MaximizeParsimony(ds, ratchIter = 7L, tbrIter = 2L, + maxHits = n_tip * 1.8, maxTime = 5, + verbosity = 0L) + }) + s1 <- TreeLength(r1[[1]], ds) + + cat(" MP2:", s2, sprintf("(%.2fs, %d reps)", t2["elapsed"], + attr(r2, "replicates")), + " MP1:", s1, sprintf("(%.2fs)", t1["elapsed"]), + " diff:", s2 - s1, "\n") + + results <- rbind(results, data.frame( + dataset = nm, tips = n_tip, patterns = n_pat, + mp2_score = s2, mp1_score = s1, score_diff = s2 - s1, + mp2_time = t2["elapsed"], mp1_time = t1["elapsed"], + speedup = t1["elapsed"] / t2["elapsed"], + stringsAsFactors = FALSE + )) +} + +cat("\n\n=== SUMMARY ===\n") +print(results, row.names = FALSE) diff --git a/dev/benchmarks/build_mbank_catalogue.R b/dev/benchmarks/build_mbank_catalogue.R new file mode 100644 index 000000000..84cf450e5 --- /dev/null +++ b/dev/benchmarks/build_mbank_catalogue.R @@ -0,0 +1,301 @@ +#!/usr/bin/env Rscript +# Build a catalogue of MorphoBank matrices from the neotrans corpus. +# +# Scans neotrans/inst/matrices/*.nex, attempts to parse each as phyDat, +# and records metadata (ntax, nchar, patterns, missing%, inapplicable%). +# +# Output: dev/benchmarks/mbank_catalogue.csv +# +# Run from the TreeSearch source root: +# Rscript dev/benchmarks/build_mbank_catalogue.R +# +# Or from dev/benchmarks/: +# Rscript build_mbank_catalogue.R + +library(TreeTools) + +# --- Path resolution --- +find_neotrans_dir <- function() { + candidates <- c( + file.path(getwd(), "..", "neotrans", "inst", "matrices"), + file.path(getwd(), "..", "..", "neotrans", "inst", "matrices"), + file.path(dirname(getwd()), "neotrans", "inst", "matrices") + ) + for (d in candidates) { + d <- normalizePath(d, mustWork = FALSE) + if (dir.exists(d)) return(d) + } + stop("Cannot find neotrans/inst/matrices/. ", + "Run from TreeSearch source root or dev/benchmarks/.") +} + +find_output_dir <- function() { + candidates <- c( + file.path(getwd(), "inst", "benchmarks"), + getwd() + ) + for (d in candidates) { + if (file.exists(file.path(d, "bench_datasets.R"))) return(d) + } + # Fall back to dev/benchmarks if it exists + d <- file.path(getwd(), "inst", "benchmarks") + if (dir.exists(d)) return(d) + stop("Cannot find dev/benchmarks/ directory.") +} + +neotrans_dir <- find_neotrans_dir() +output_dir <- find_output_dir() + +cat("Neotrans matrices dir:", neotrans_dir, "\n") +cat("Output dir:", output_dir, "\n") + +# --- Find all .nex files --- +nex_files <- list.files(neotrans_dir, pattern = "\\.nex$", + full.names = TRUE, recursive = FALSE) +cat("Found", length(nex_files), ".nex files\n") + +# --- Parse each file and collect metadata --- +characterize_phyDat <- function(dataset) { + at <- attributes(dataset) + contrast <- at$contrast + lvls <- at$levels + n_taxa <- length(dataset) + n_patterns <- length(at$weight) + n_chars <- sum(at$weight) + n_states <- ncol(contrast) + + inapp_idx <- which(lvls == "-") + n_app_states <- n_states - length(inapp_idx) + + td <- matrix(unlist(dataset, use.names = FALSE), + nrow = n_taxa, byrow = TRUE) + total_cells <- n_taxa * n_patterns + + n_inapp <- 0L + n_missing <- 0L + has_inapp <- length(inapp_idx) > 0 + for (i in seq_len(nrow(contrast))) { + is_inapp <- has_inapp && contrast[i, inapp_idx] > 0.5 + cols_check <- setdiff(seq_len(n_states), inapp_idx) + is_all <- length(cols_check) > 0 && all(contrast[i, cols_check] > 0.5) + count <- sum(td == i) + if (is_inapp && !is_all) n_inapp <- n_inapp + count + if (is_all) n_missing <- n_missing + count + } + + list( + ntax = n_taxa, + nchar = n_chars, + n_patterns = n_patterns, + n_states = n_app_states, + pct_missing = round(100 * n_missing / total_cells, 1), + pct_inapp = round(100 * n_inapp / total_cells, 1) + ) +} + +results <- vector("list", length(nex_files)) + +for (i in seq_along(nex_files)) { + f <- nex_files[i] + bname <- basename(f) + + # Extract project ID and matrix index + if (grepl("^project", bname, ignore.case = TRUE)) { + proj_num <- as.integer(sub("^project(\\d+).*", "\\1", bname, + ignore.case = TRUE)) + # Multi-matrix index: "project1037 (2).nex" -> 2 + if (grepl("\\(\\d+\\)", bname)) { + mat_idx <- as.integer(sub(".*\\((\\d+)\\).*", "\\1", bname)) + } else { + mat_idx <- NA_integer_ + } + source_type <- "morphobank" + } else if (grepl("^syab", bname, ignore.case = TRUE)) { + proj_num <- NA_integer_ + mat_idx <- NA_integer_ + source_type <- "syab" + } else { + proj_num <- NA_integer_ + mat_idx <- NA_integer_ + source_type <- "other" + } + + # Unique key for this matrix + key <- sub("\\.nex$", "", bname, ignore.case = TRUE) + key <- gsub(" ", "_", key) + + # Assign split + if (!is.na(proj_num) && proj_num %% 5 == 0) { + split <- "validation" + } else { + split <- "training" + } + + # Try to parse + row <- list( + key = key, + filename = bname, + project_id = proj_num, + matrix_idx = mat_idx, + source_type = source_type, + split = split, + ntax = NA_integer_, + nchar = NA_integer_, + n_patterns = NA_integer_, + n_states = NA_integer_, + pct_missing = NA_real_, + pct_inapp = NA_real_, + parse_ok = FALSE, + error_message = "" + ) + + tryCatch({ + pd <- ReadAsPhyDat(f) + chars <- characterize_phyDat(pd) + row$ntax <- chars$ntax + row$nchar <- chars$nchar + row$n_patterns <- chars$n_patterns + row$n_states <- chars$n_states + row$pct_missing <- chars$pct_missing + row$pct_inapp <- chars$pct_inapp + row$parse_ok <- TRUE + }, error = function(e) { + row$error_message <<- conditionMessage(e) + }, warning = function(w) { + # Warnings during parsing are common (e.g. "Duplicate taxon names") + # Try to continue + tryCatch({ + pd <- suppressWarnings(ReadAsPhyDat(f)) + chars <- characterize_phyDat(pd) + row$ntax <<- chars$ntax + row$nchar <<- chars$nchar + row$n_patterns <<- chars$n_patterns + row$n_states <<- chars$n_states + row$pct_missing <<- chars$pct_missing + row$pct_inapp <<- chars$pct_inapp + row$parse_ok <<- TRUE + row$error_message <<- paste("WARNING:", conditionMessage(w)) + }, error = function(e2) { + row$error_message <<- paste("WARNING:", conditionMessage(w), + "; ERROR:", conditionMessage(e2)) + }) + }) + + results[[i]] <- as.data.frame(row, stringsAsFactors = FALSE) + + if (i %% 50 == 0 || i == length(nex_files)) { + cat(sprintf(" [%d/%d] %s\n", i, length(nex_files), bname)) + } +} + +catalogue <- do.call(rbind, results) + +# --- Dedup: flag near-duplicate multi-file matrices --- +# Multi-file projects (e.g. "project1037 (1).nex", "project1037 (2).nex") often +# contain the same character data with minor taxon-sampling differences. We flag +# redundant copies so the benchmark loader can exclude them by default. +# +# Method: for each project with multiple usable files, load all matrices, +# compute pairwise character identity on shared taxa, and greedily keep the +# largest (most taxa) representative from each cluster of >=95% identical pairs. + +usable_mask <- catalogue$parse_ok & !is.na(catalogue$ntax) & catalogue$ntax >= 20 +catalogue$dedup_drop <- FALSE + +usable_multi <- catalogue[usable_mask & !is.na(catalogue$matrix_idx), ] +if (nrow(usable_multi) > 0) { + usable_multi$project <- sub("_\\(\\d+\\)$", "", usable_multi$key) + proj_counts <- table(usable_multi$project) + multi_projects <- names(proj_counts[proj_counts >= 2]) + + cat(sprintf("\nDedup: checking %d multi-file projects (%d matrices)...\n", + length(multi_projects), + sum(usable_multi$project %in% multi_projects))) + + drop_keys <- character(0) + + for (proj in multi_projects) { + rows <- usable_multi[usable_multi$project == proj, ] + keys <- rows$key + mats <- list() + for (j in seq_len(nrow(rows))) { + fpath <- file.path(neotrans_dir, rows$filename[j]) + tryCatch({ + mats[[rows$key[j]]] <- suppressWarnings(ReadAsPhyDat(fpath)) + }, error = function(e) NULL) + } + if (length(mats) < 2) next + + # Build pairwise character-identity matrix + mk <- names(mats) + identity_mat <- matrix(NA_real_, length(mk), length(mk), + dimnames = list(mk, mk)) + for (a in seq_len(length(mk) - 1)) { + for (b in (a + 1):length(mk)) { + taxa_a <- names(mats[[mk[a]]]) + taxa_b <- names(mats[[mk[b]]]) + common <- intersect(taxa_a, taxa_b) + # Require >=80% taxon overlap with the smaller matrix + if (length(common) < 0.8 * min(length(taxa_a), length(taxa_b))) next + mat_a <- as.matrix(mats[[mk[a]]])[common, , drop = FALSE] + mat_b <- as.matrix(mats[[mk[b]]])[common, , drop = FALSE] + if (ncol(mat_a) != ncol(mat_b)) next + identity_mat[mk[a], mk[b]] <- mean(mat_a == mat_b, na.rm = TRUE) + identity_mat[mk[b], mk[a]] <- identity_mat[mk[a], mk[b]] + } + } + + # Greedy dedup: sort by ntax desc, keep first, drop near-dups + sorted_keys <- rows$key[order(-rows$ntax, -rows$nchar)] + kept <- character(0) + for (k in sorted_keys) { + is_dup <- FALSE + for (kk in kept) { + ci <- identity_mat[k, kk] + if (!is.na(ci) && ci >= 0.95) { is_dup <- TRUE; break } + } + if (is_dup) drop_keys <- c(drop_keys, k) + else kept <- c(kept, k) + } + } + + catalogue$dedup_drop[catalogue$key %in% drop_keys] <- TRUE + cat(sprintf("Dedup: flagged %d near-duplicate matrices for exclusion.\n", + length(drop_keys))) +} + +# --- Summary --- +cat("\n=== Catalogue Summary ===\n") +cat("Total files scanned:", nrow(catalogue), "\n") +cat("Parse OK:", sum(catalogue$parse_ok), "\n") +cat("Parse failed:", sum(!catalogue$parse_ok), "\n") +cat("\nAfter ntax >= 20 filter:\n") +usable <- catalogue$parse_ok & !is.na(catalogue$ntax) & catalogue$ntax >= 20 +cat(" Usable (before dedup):", sum(usable), "\n") +cat(" Dedup dropped:", sum(usable & catalogue$dedup_drop), "\n") +usable_dedup <- usable & !catalogue$dedup_drop +cat(" Usable (after dedup):", sum(usable_dedup), "\n") +cat(" Training:", sum(usable_dedup & catalogue$split == "training"), "\n") +cat(" Validation:", sum(usable_dedup & catalogue$split == "validation"), "\n") + +cat("\nSize tiers (after dedup):\n") +usable_cat <- catalogue[usable_dedup, ] +usable_cat$tier <- cut(usable_cat$ntax, + breaks = c(0, 30, 60, 120, Inf), + labels = c("Small(20-30)", "Medium(31-60)", + "Large(61-120)", "XLarge(121+)")) +print(table(usable_cat$split, usable_cat$tier)) + +cat("\nParse failures:\n") +if (any(!catalogue$parse_ok)) { + fails <- catalogue[!catalogue$parse_ok, c("key", "error_message")] + for (j in seq_len(nrow(fails))) { + cat(sprintf(" %s: %s\n", fails$key[j], + substr(fails$error_message[j], 1, 80))) + } +} + +# --- Save --- +out_path <- file.path(output_dir, "mbank_catalogue.csv") +write.csv(catalogue, out_path, row.names = FALSE) +cat("\nCatalogue written to:", out_path, "\n") diff --git a/dev/benchmarks/build_t0_trees.R b/dev/benchmarks/build_t0_trees.R new file mode 100644 index 000000000..d64af034e --- /dev/null +++ b/dev/benchmarks/build_t0_trees.R @@ -0,0 +1,35 @@ +# Build the SHARED start trees (TNT mult, rseed 1, replic 1) for each gap dataset +# and save as Newick, so the heavy ceiling/validation sweep (local or Hamilton) is +# decoupled from TNT (Hamilton has no TNT). Also writes the Fitch matrices as +# TNT/phyDat-independent RDS so the sweep needs only TreeSearch + the .tre + .rds. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband2"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +dsN <- c("Zanol2014", "Wortley2006", "Zhu2013", "Giles2015") +outdir <- "dev/benchmarks/t0"; dir.create(outdir, showWarnings = FALSE, recursive = TRUE) + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + wd <- file.path(tempdir(), paste0("t0b", Sys.getpid(), nm)) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + writeLines(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1000;", + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", "quit;"), + file.path(wd, "t0build.run")) + old <- setwd(wd) + invisible(suppressWarnings(system2(TNT, args = "t0build.run;", stdout = TRUE, stderr = TRUE))) + setwd(old) + t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + t0len <- TreeLength(t0, phy) + # Persist tree (Newick) + Fitch phyDat (RDS) so the sweep needs no TNT. + ape::write.tree(t0, file.path(outdir, paste0(nm, ".tre"))) + saveRDS(phy, file.path(outdir, paste0(nm, ".phy.rds"))) + cat(sprintf("%-12s n=%d T0 len=%.0f -> %s.tre + .phy.rds\n", + nm, NTip(phy), t0len, nm)) +} +cat("\nSaved T0 trees + Fitch matrices to", outdir, "\n") diff --git a/dev/benchmarks/datasets.md b/dev/benchmarks/datasets.md new file mode 100644 index 000000000..f9fb34706 --- /dev/null +++ b/dev/benchmarks/datasets.md @@ -0,0 +1,112 @@ +# Benchmark Dataset Suite + +Selected from the 30 `inapplicable.phyData` datasets bundled with TreeSearch. +Criteria: cover small → large tip counts, varying inapplicable proportions, +varying state counts, and varying matrix densities (% missing data). + +## Dataset Selection + +| # | Dataset | Tips | Chars | Patterns | %Inapp | States | %Missing | Category | +|---|---------|------|-------|----------|--------|--------|----------|----------| +| 1 | Longrich2010 | 20 | 93 | 80 | 4.2 | 3 | 45.3 | Small, high missing | +| 2 | Vinther2008 | 23 | 57 | 50 | 6.1 | 4 | 21.0 | Small, moderate | +| 3 | Sansom2010 | 23 | 109 | 97 | 6.1 | 4 | 40.0 | Small, high missing | +| 4 | DeAssis2011 | 33 | 50 | 36 | 21.4 | 3 | 0.2 | Medium-small, high inapp | +| 5 | Aria2015 | 35 | 50 | 50 | 6.7 | 6 | 12.7 | Medium-small, multi-state | +| 6 | Wortley2006 | 37 | 105 | 105 | 2.7 | 8 | 31.4 | Medium, many states | +| 7 | Griswold1999 | 43 | 137 | 118 | 6.2 | 6 | 5.6 | Medium, dense matrix | +| 8 | Schulze2007 | 52 | 58 | 57 | 16.7 | 3 | 2.4 | Medium, high inapp, dense | +| 9 | Eklund2004 | 54 | 131 | 131 | 7.8 | 6 | 29.8 | Medium, moderate | +| 10 | Agnarsson2004 | 62 | 242 | 225 | 6.9 | 7 | 6.1 | Large, many chars, dense | +| 11 | Zanol2014 | 74 | 213 | 210 | 16.8 | 9 | 11.9 | Large, high inapp, many states | +| 12 | Zhu2013 | 75 | 253 | 253 | 12.4 | 4 | 42.6 | Large, high missing | +| 13 | Giles2015 | 78 | 236 | 236 | 11.8 | 4 | 41.5 | Large, high missing+inapp | +| 14 | Dikow2009 | 88 | 220 | 204 | 1.2 | 9 | 0.4 | Largest, dense, many states | + +## Selection Rationale + +- **Size range**: 20 → 88 tips (5× range). Covers small (exhaustive-feasible) + through large (heuristic-only). +- **Inapplicable variation**: 1.2% (Dikow) → 21.4% (DeAssis). Tests the + NA three-pass scoring path under varying load. +- **State count variation**: 3–9 applicable states. Affects `total_words` + (state word count per block) and thus inner-loop iteration count. +- **Missing data variation**: 0.2% (DeAssis) → 45.3% (Longrich). High missing + data creates more ambiguous tokens, affecting scoring and simplification. +- **Dense vs sparse**: DeAssis (0.2% missing) and Dikow (0.4% missing) are + nearly complete matrices; Longrich (45.3%) and Zhu (42.6%) are sparse. + +## Best-Known EW Scores + +Scores from the C++ driven search engine (5 replicates, 5s timeout per +dataset, `set.seed(42)`). These are the standard Fitch parsimony scores +(not inapplicable-aware). Published tree scores from `inapplicable.trees` +are generally higher because they may not be optimized for standard Fitch. + +| Dataset | C++ Best | Published Tree | Notes | +|---------|----------|---------------|-------| +| Longrich2010 | 131 | 167 | | +| Vinther2008 | 79 | 93 | | +| Sansom2010 | 189 | — | | +| DeAssis2011 | 64 | 89 | | +| Aria2015 | 145 | 185 | | +| Wortley2006 | 496 | 518 | | +| Griswold1999 | 409 | 511 | | +| Schulze2007 | 167 | 212 | | +| Eklund2004 | 445 | 496 | | +| Agnarsson2004 | 778 | 1035 | | +| Zanol2014 | 1338 | 1802 | | +| Zhu2013 | 649 | 810 | | +| Giles2015 | 720 | 1005 | | +| Dikow2009 | 1614 | 1646 | | + +Note: C++ scores are lower than published because (a) the published trees +were optimized for a different scoring method (inapplicable-aware), and +(b) our driven search may find better trees. These scores were obtained +with `set.seed(42)`, 10s timeout, 10 replicates. Use `bench_datasets.R` +with longer search times for authoritative best-known scores. + +## Large-Tree Benchmark Datasets + +Separate tier for datasets >= 100 tips, loaded from `dev/benchmarks/`. +These have fundamentally different search dynamics: single TBR convergence +takes seconds to minutes, replicates take minutes rather than sub-second. + +| # | Dataset | Tips | Chars | Patterns | %Missing | %Inapp | Source | +|---|---------|------|-------|----------|----------|--------|--------| +| L1 | mbank_X30754 | 180 | 425 | 418 | 40% | 20.5% | MorphoBank P30754 | + +### mbank_X30754 + +MorphoBank project X30754 (downloaded 2025-06-16). 180 taxa, 425 characters +with ~40% missing data and ~20% inapplicable entries. This is a realistic +large morphological matrix that exposes scaling issues in the search engine: +NNI warmup is essential, single TBR convergence takes ~13s, and the standard +strategy presets (calibrated for ≤88 tips) are poorly suited. + +Best-known EW score: TBD (to be established after systematic benchmarking). + +## Usage + +```r +source("dev/benchmarks/bench_datasets.R") + +# Load standard benchmark datasets (14 datasets, ≤88 tips) +datasets <- load_benchmark_datasets() + +# Load large-tree benchmark datasets (≥100 tips) +large <- load_large_benchmark_datasets() + +# Load all (standard + large) +all_ds <- load_all_benchmark_datasets() + +# Score a single dataset +score_dataset("Vinther2008", maxSeconds = 10) + +# Run standard benchmark suite +run_benchmark_suite(maxSeconds = 30, replicates = 5) + +# Run large-tree benchmark (from bench_framework.R) +# source("dev/benchmarks/bench_framework.R") +# benchmark_large(maxSeconds = 120) +``` diff --git a/dev/benchmarks/define_target.R b/dev/benchmarks/define_target.R new file mode 100644 index 000000000..bc09873d6 --- /dev/null +++ b/dev/benchmarks/define_target.R @@ -0,0 +1,56 @@ +# DEFINE THE TARGET (advisor's blocking point): from the CANONICAL hold-1000 T0, +# where does TNT's ratchet/drift-OFF RSS sectorial land? "Did TreeSearch match" is +# undefined until this number exists for the SAME start tree the TS arms will use. +# +# Recipe = ratchet-off sectorial: plain `mult=replic 1` (one RAS+TBR start, NO +# ratchet/drift/fuse) -> T0; then repeated `sectsch=rss;` (each = one full RSS round, +# escape-doc-verified). hold 1000 selects the canonical 1271 basin (subchip: hold 1 +# -> 1275, hold 1000 -> 1271 for Zanol). Saves each T0 to dev/benchmarks/t0/.tre +# (Newick) + the Fitch phyDat (.phy.rds) so the TreeSearch arms share the identical start. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband2"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) suppressWarnings(as.double(gsub(",", "", x))) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Wortley2006 Zhu2013 Giles2015")), "\\s+")[[1]] +NROUND <- as.integer(Sys.getenv("TS_NROUND", "10")) +outdir <- "dev/benchmarks/t0"; dir.create(outdir, showWarnings = FALSE, recursive = TRUE) + +bestLen <- function(tr, phy) { + if (inherits(tr, "multiPhylo")) min(vapply(tr, TreeLength, double(1), phy)) else TreeLength(tr, phy) +} + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]); n <- NTip(phy) + wd <- file.path(tempdir(), paste0("tgt", Sys.getpid(), nm)) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + writeLines(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1000;", + "mult=replic 1;", + "tsave *t0.tre;", "save;", "tsave/;", # canonical T0 (1271 basin) + rep("sectsch=rss;", NROUND), # ratchet-off RSS rounds + "tsave *final.tre;", "save;", "tsave/;", # post-sectorial + "quit;"), + file.path(wd, "deftgt.run")) + old <- setwd(wd) + out <- suppressWarnings(system2(TNT, args = "deftgt.run;", stdout = TRUE, stderr = TRUE)) + setwd(old) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + + t0 <- ReadTntTree(file.path(wd, "t0.tre")) + fin <- ReadTntTree(file.path(wd, "final.tre")) + t0len <- bestLen(t0, phy) + finlen <- bestLen(fin, phy) + # Persist canonical T0 + matrix for the shared-start TreeSearch arms + t0one <- if (inherits(t0, "multiPhylo")) t0[[1]] else t0 + ape::write.tree(t0one, file.path(outdir, paste0(nm, ".tre"))) + saveRDS(phy, file.path(outdir, paste0(nm, ".phy.rds"))) + + cat(sprintf("%-12s n=%3d | T0=%.0f -> TNT ratchet-off sectorial(%d rounds) = %.0f (escape %+.0f)\n", + nm, n, t0len, NROUND, finlen, finlen - t0len)) +} +cat(sprintf("\nSaved canonical T0 trees + matrices to %s/\n", outdir)) diff --git a/dev/benchmarks/diag_accept_gate_trace.R b/dev/benchmarks/diag_accept_gate_trace.R new file mode 100644 index 000000000..5feed86b7 --- /dev/null +++ b/dev/benchmarks/diag_accept_gate_trace.R @@ -0,0 +1,57 @@ +# Discriminating trace for the TNT-audit acceptance-gate question: +# Does a sector ever improve on the REDUCED score (red_best < red_cur) while +# the FULL tree does NOT improve (full_new >= full_best)? +# +# * If NEVER under EW-Fitch (- -> ?): reduced-improving <=> full-improving, +# so the from-above HTU scoring is EXACT and the strict full-tree accept +# gate is a NULL divergence from Goloboff 1999 (accept-on-reduced). +# * If it HAPPENS under native NA (Brazeau, keep -): scoring is inexact and +# the gate bites — but that is not the audited EW case. +# +# Uses the TS_SECT_DEBUG=1 trace already compiled into rss_search +# (ts_sector.cpp:1081). We force the sectorial path by giving a tree large +# enough for sectors and running with rss only. +# +# Env: TS_LIB (.agent-audit), TS_DS (dataset), TS_MODE (fitch|native), TS_SEED + +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-audit"), + winslash = "/")) + library(TreeTools) +}) + +ds_name <- Sys.getenv("TS_DS", "Zanol2014") +mode <- Sys.getenv("TS_MODE", "fitch") +seed <- as.integer(Sys.getenv("TS_SEED", "1")) + +data("inapplicable.phyData", package = "TreeSearch") +phy <- inapplicable.phyData[[ds_name]] +if (mode == "fitch") { + m <- PhyDatToMatrix(phy, ambigNA = FALSE) + m[m == "-"] <- "?" + phy <- MatrixToPhyDat(m) +} + +# Force sectorial to engage and run RSS specifically; disable ratchet/drift so +# the only score-changing phase whose trace we read is the sector accept path. +# rasStarts default 1; we test BOTH the polish (1) and rebuild (3) here. +ras <- as.integer(Sys.getenv("TS_RAS", "3")) +ctl <- SearchControl( + ratchetCycles = 0L, driftCycles = 0L, nniPerturbCycles = 0L, + pruneReinsertCycles = 0L, annealCycles = 0L, + xssRounds = 0L, cssRounds = 0L, + rssRounds = 3L, rasStarts = ras, + sectorMinSize = 6L, sectorMaxSize = 50L, + wagnerStarts = 1L, fuseInterval = 0L, intraFuse = FALSE, + maxOuterResets = 0L, outerCycles = 1L +) + +Sys.setenv(TS_SECT_DEBUG = "1") +set.seed(seed) +cat(sprintf("=== %s | mode=%s | seed=%d | rasStarts=%d ===\n", + ds_name, mode, seed, ras)) +# verbosity 0 so only the C-level TS_SECT_DEBUG REprintf lines appear on stderr +invisible(suppressWarnings(MaximizeParsimony( + phy, maxReplicates = 1L, maxSeconds = 30, nThreads = 1L, + strategy = "default", control = ctl, verbosity = 0L))) +cat("=== done ===\n") diff --git a/dev/benchmarks/diag_cid_wortley.R b/dev/benchmarks/diag_cid_wortley.R new file mode 100644 index 000000000..80a64ea3f --- /dev/null +++ b/dev/benchmarks/diag_cid_wortley.R @@ -0,0 +1,44 @@ +# CID measurement (owed): how far is TS's Wortley optimum from TNT's 479 tree, +# by TreeDist::ClusteringInfoDist(normalize=TRUE) -- robust, unlike the RF=54 I +# reported (RF inflates when one tip moves far). Low CID => basins are actually +# close (RF was misleading); high CID => genuine whole-tree difference. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) + library(TreeDist) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +phy <- fitch(inapplicable.phyData[["Wortley2006"]]) + +# TNT 479 tree (full xmult) +wd <- file.path(tempdir(), paste0("cid", Sys.getpid())) +unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) +WriteTntCharacters(phy, file.path(wd, "data.tnt")) +writeLines(c("mxram 1024;", "proc data.tnt;", "hold 10000;", "rseed 1;", + "xmult=hits 10 replic 50;", "best;", "tsave *t479.tre;", "save;", + "tsave/;", "quit;"), file.path(wd, "cidtest.run")) +old <- setwd(wd) +invisible(suppressWarnings(system2(TNT, args = "cidtest.run;", stdout = TRUE, stderr = TRUE))) +setwd(old) +T479 <- ReadTntTree(file.path(wd, "t479.tre")); if (inherits(T479, "multiPhylo")) T479 <- T479[[1]] + +# TS optimum (intensive, best of 3 short seeds) +best <- NULL; bestlen <- Inf +for (s in 1:3) { + set.seed(s) + r <- suppressWarnings(MaximizeParsimony(phy, strategy = "intensive", + maxReplicates = 9999L, maxSeconds = 20, nThreads = 1L, verbosity = 0L)) + l <- min(as.double(attr(r, "score"))) + tr <- if (inherits(r, "multiPhylo")) r[[1]] else r + if (l < bestlen) { bestlen <- l; best <- tr } +} + +T479 <- KeepTip(T479, best$tip.label) +cid <- TreeDist::ClusteringInfoDist(T479, best, normalize = TRUE) +rf <- TreeDist::RobinsonFoulds(T479, best, normalize = TRUE) +cat(sprintf("TNT T479 len=%.0f | TS-best len=%.0f\n", TreeLength(T479, phy), bestlen)) +cat(sprintf("ClusteringInfoDist(normalize=TRUE) = %.3f [0=identical, 1=maximally different]\n", cid)) +cat(sprintf("RobinsonFoulds(normalize=TRUE) = %.3f (for contrast)\n", rf)) diff --git a/dev/benchmarks/diag_clip_ordering.R b/dev/benchmarks/diag_clip_ordering.R new file mode 100644 index 000000000..1a3722b19 --- /dev/null +++ b/dev/benchmarks/diag_clip_ordering.R @@ -0,0 +1,286 @@ +# diag_clip_ordering.R +# +# Diagnostic script for the size-weighted TBR clip ordering experiment. +# +# Purpose: Characterise baseline (random) TBR clip ordering behaviour to test +# whether the small-clip-first hypothesis holds empirically. +# +# For each dataset and seed, builds a random Wagner starting tree, runs +# ts_tbr_diagnostics() to convergence, and accumulates per-pass records. +# Produces three summary tables: +# +# 1. Accepted clip size breakdown by bucket (tips / small / large). +# Key question: are tip clips over-represented in accepted moves +# relative to their uniform expectation? +# +# 2. Clips tried before acceptance (productive passes). +# Key question: is n_clips_tried typically large enough that a +# small-first ordering could meaningfully reduce it? +# +# 3. Evaluation budget split: productive vs null passes. +# Key question: what fraction of TBR work is "wasted" in null passes? +# +# Usage: Rscript dev/benchmarks/diag_clip_ordering.R [lib_path] +# lib_path defaults to ".agent-wc" + +args <- commandArgs(trailingOnly = TRUE) +lib_path <- if (length(args) >= 1) args[1] else ".agent-wc" + +library(TreeSearch, lib.loc = lib_path) + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +DATASETS <- c("Vinther2008", "Agnarsson2004", "Zhu2013", "Dikow2009") +SEEDS <- c(1847L, 2956L, 3712L, 4519L, 5823L, 6401L, 7238L, 8145L, 9032L, 9871L) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +prepare <- function(name) { + ds <- TreeSearch::inapplicable.phyData[[name]] + at <- attributes(ds) + list( + name = name, + contrast = at$contrast, + tip_data = matrix(unlist(ds, use.names = FALSE), + nrow = length(ds), byrow = TRUE), + weight = at$weight, + levels = at$levels, + n_taxa = length(ds) + ) +} + +# Bucket label for a clip of subtree size s given n_tip. +# Tip: s == 1 +# Small: 2 <= s <= floor(sqrt(n_tip)) +# Large: s > floor(sqrt(n_tip)) +clip_bucket <- function(s, n_tip) { + sq <- floor(sqrt(n_tip)) + ifelse(s == 1, "tip", + ifelse(s <= sq, "small", "large")) +} + +# Expected fraction of clips in each bucket for a binary rooted tree with +# n_tip leaves. Total clips = 2*(n_tip-1). +# Tip clips (s==1) : exactly n_tip +# Non-tip clips : n_tip - 2 +# Among non-tip, sizes 2..n_tip-1. Approximate uniform distribution: +# small (2..floor(sqrt)) : floor(sqrt)-1 sizes +# large (floor(sqrt)+1..n_tip-1): n_tip-1-floor(sqrt) sizes +# This is approximate (not all sizes appear equally often), but adequate +# for comparison against observed acceptance fractions. +expected_bucket_fracs <- function(n_tip) { + n_clips <- 2L * (n_tip - 1L) + sq <- floor(sqrt(n_tip)) + n_tip_c <- n_tip # tip clips + n_nontip <- n_tip - 2L # non-tip clips + n_small_c <- sq - 1L # sizes 2..sq (approximate, may be 0) + n_large_c <- n_nontip - n_small_c + list( + tip = n_tip_c / n_clips, + small = max(0, n_small_c) / n_clips, + large = max(0, n_large_c) / n_clips + ) +} + +# --------------------------------------------------------------------------- +# Data collection +# --------------------------------------------------------------------------- + +cat("Collecting TBR pass diagnostics (", length(SEEDS), "seeds per dataset)...\n\n", + sep = "") + +all_records <- list() + +for (dname in DATASETS) { + d <- prepare(dname) + n_tip <- d$n_taxa + sq <- floor(sqrt(n_tip)) + exp <- expected_bucket_fracs(n_tip) + + cat(sprintf("Dataset: %-15s n_tip=%d sqrt_n=%d total_clips=%d\n", + dname, n_tip, sq, 2L*(n_tip-1L))) + + ds_records <- vector("list", length(SEEDS)) + + for (i in seq_along(SEEDS)) { + set.seed(SEEDS[i]) + + # Random Wagner starting tree + wag <- TreeSearch:::ts_random_wagner_tree( + d$contrast, d$tip_data, d$weight, d$levels + ) + + # TBR to convergence with per-pass diagnostics (default clip_order = RANDOM) + res <- TreeSearch:::ts_tbr_diagnostics( + wag$edge, d$contrast, d$tip_data, d$weight, d$levels + ) + + passes <- res$passes + passes$dataset <- dname + passes$seed <- SEEDS[i] + passes$n_tip <- n_tip + passes$n_clips <- 2L * (n_tip - 1L) + passes$final_score <- res$score + passes$bucket <- clip_bucket(passes$accepted_clip_size, n_tip) + # bucket is only meaningful for productive passes; set NA for null passes + passes$bucket[!passes$productive] <- NA_character_ + + ds_records[[i]] <- passes + } + + all_records[[dname]] <- do.call(rbind, ds_records) + recs <- all_records[[dname]] + prod <- recs[recs$productive, ] + null <- recs[!recs$productive, ] + + cat(sprintf(" Passes: %d productive=%d (%.0f%%) null=%d (%.0f%%)\n", + nrow(recs), nrow(prod), 100*nrow(prod)/nrow(recs), + nrow(null), 100*nrow(null)/nrow(recs))) + + if (nrow(prod) > 0) { + tip_obs <- mean(prod$accepted_clip_size == 1) + tip_exp <- exp$tip + enrich <- tip_obs / tip_exp + cat(sprintf(" Tip-clip acceptance: observed=%.0f%% expected=%.0f%% enrichment=%.2fx\n", + 100*tip_obs, 100*tip_exp, enrich)) + cat(sprintf(" Clips tried before accept: median=%d mean=%.1f (out of %d clips)\n", + median(prod$n_clips_tried), mean(prod$n_clips_tried), 2L*(n_tip-1L))) + cat(sprintf(" Final score range: %.0f – %.0f\n", + min(recs$final_score), max(recs$final_score))) + } + cat("\n") +} + +combined <- do.call(rbind, all_records) +prod_all <- combined[combined$productive, ] + +# --------------------------------------------------------------------------- +# Table 1: Accepted clip size bucket breakdown +# --------------------------------------------------------------------------- + +cat("=== Table 1: Accepted clip size breakdown (productive passes only) ===\n\n") + +fmt_pct <- function(x) sprintf("%.1f%%", 100 * x) + +bucket_tbl <- do.call(rbind, lapply(DATASETS, function(dname) { + p <- prod_all[prod_all$dataset == dname, ] + n_tip <- p$n_tip[1] + exp <- expected_bucket_fracs(n_tip) + tot <- nrow(p) + + tip_obs <- mean(p$accepted_clip_size == 1) + small_obs <- mean(p$accepted_clip_size > 1 & + p$accepted_clip_size <= floor(sqrt(n_tip))) + large_obs <- mean(p$accepted_clip_size > floor(sqrt(n_tip))) + + data.frame( + dataset = dname, + n_tip = n_tip, + n_prod_passes = tot, + tip_obs = fmt_pct(tip_obs), + tip_exp = fmt_pct(exp$tip), + tip_enrichment = round(tip_obs / exp$tip, 2), + small_obs = fmt_pct(small_obs), + small_exp = fmt_pct(exp$small), + large_obs = fmt_pct(large_obs), + large_exp = fmt_pct(exp$large) + ) +})) + +print(bucket_tbl, row.names = FALSE) + +# --------------------------------------------------------------------------- +# Table 2: Clips tried before acceptance +# --------------------------------------------------------------------------- + +cat("\n=== Table 2: Clips tried in productive passes ===\n") +cat("(n_clips_tried includes the accepted clip itself; 1 = first clip accepted)\n\n") + +tried_tbl <- do.call(rbind, lapply(DATASETS, function(dname) { + p <- prod_all[prod_all$dataset == dname, ] + n_clips <- p$n_clips[1] + tried <- p$n_clips_tried + + data.frame( + dataset = dname, + n_clips = n_clips, + n_prod_passes = nrow(p), + pct_first_clip = fmt_pct(mean(tried == 1)), + pct_within_5 = fmt_pct(mean(tried <= 5)), + pct_within_10pct = fmt_pct(mean(tried <= 0.1 * n_clips)), + median_tried = median(tried), + mean_tried = round(mean(tried), 1), + median_position = round(median(tried) / n_clips, 2) + ) +})) + +print(tried_tbl, row.names = FALSE) + +# --------------------------------------------------------------------------- +# Table 3: Evaluation budget — productive vs null passes +# --------------------------------------------------------------------------- + +cat("\n=== Table 3: Evaluation budget by pass type ===\n\n") + +eval_tbl <- do.call(rbind, lapply(DATASETS, function(dname) { + d <- combined[combined$dataset == dname, ] + prod <- d[d$productive, ] + null <- d[!d$productive, ] + tot <- sum(d$n_candidates_evaluated) + + data.frame( + dataset = dname, + n_prod_passes = nrow(prod), + n_null_passes = nrow(null), + pct_evals_prod = fmt_pct(sum(prod$n_candidates_evaluated) / tot), + pct_evals_null = fmt_pct(sum(null$n_candidates_evaluated) / tot), + med_evals_prod = if (nrow(prod) > 0) median(prod$n_candidates_evaluated) else NA_real_, + med_evals_null = if (nrow(null) > 0) median(null$n_candidates_evaluated) else NA_real_ + ) +})) + +print(eval_tbl, row.names = FALSE) + +# --------------------------------------------------------------------------- +# Hypothesis assessment +# --------------------------------------------------------------------------- + +cat("\n=== Hypothesis assessment ===\n") +cat("H: small clips (s=1) are over-represented in accepted moves,\n") +cat(" AND n_clips_tried is large enough that ordering would help.\n\n") + +for (dname in DATASETS) { + p <- prod_all[prod_all$dataset == dname, ] + n_clips <- p$n_clips[1] + n_tip <- p$n_tip[1] + enrich <- (mean(p$accepted_clip_size == 1)) / + (expected_bucket_fracs(n_tip)$tip) + med_pos <- median(p$n_clips_tried) / n_clips # fraction of clips needed + + # Potential saving if tips-first: E[position of accepted tip clip in random + # order] - E[position in tips-first order]. Very roughly: + # random E[pos] ≈ n_clips/2; tips-first E[pos] ≈ n_tip/2. + # saving_fraction ≈ (n_clips/2 - n_tip/2) / n_clips = (1 - n_tip/n_clips)/2 ≈ 0.25 + # But only beneficial if tip clips ARE more commonly accepted (enrich > 1). + + verdict <- if (enrich >= 2.0 && med_pos >= 0.25) { + "STRONGLY SUPPORTS ordering (high enrichment + late acceptance)" + } else if (enrich >= 1.5 && med_pos >= 0.15) { + "SUPPORTS ordering (moderate enrichment + moderate position)" + } else if (enrich >= 1.5) { + "PARTIAL (enrichment, but acceptance mostly in first few clips)" + } else if (enrich < 0.8) { + "CONTRADICTS hypothesis (large clips accepted more often)" + } else { + "NEUTRAL (no consistent tip-clip enrichment)" + } + + cat(sprintf(" %-15s: tip enrichment=%.2fx median_pos=%.2f -> %s\n", + dname, enrich, med_pos, verdict)) +} + +cat("\nDone.\n") diff --git a/dev/benchmarks/diag_collapse_sect.R b/dev/benchmarks/diag_collapse_sect.R new file mode 100644 index 000000000..295269173 --- /dev/null +++ b/dev/benchmarks/diag_collapse_sect.R @@ -0,0 +1,89 @@ +# (c)-MECHANISM TEST (Goloboff sectsch escape), ZERO C++ risk -- all knobs R-exposed. +# Subagent (dev/plans/2026-06-17-sectsch-escape-mechanism.md) overturned D1 (frozen +# HTU; d1_confirm.out shows 0 confirms) and pins the escape on TNT's selectem GEOMETRY: +# LARGE (~n/2) sectors whose deep sub-clades are COLLAPSED into composite terminals, so +# RAS+TBR reshuffles whole sub-clades across the backbone -- large-radius full-tree moves +# our small-clade ras1 sectorial never proposes. All this is reachable from R: +# sectorMinSize/Max (force large) + sectorCollapseTarget (>0 collapse) + rasStarts(=3 +# RAS rebuild) + sectorAcceptEqual (the +1 bridge). Walk-up-from-random selection is +# the ONE piece NOT R-reachable (we pick existing in-band clades, not walk-up clades). +# +# LADDER (isolates each factor), SHARED start (TNT mult T0), 2 seeds, all-else-off: +# base defaults (min6 max50 ras1 coll0 eq F) -- current behaviour +# bigNoColl min31 max99 ras3 coll0 eq F -- large sector, NO collapse +# coll30 min31 max99 ras3 coll30 eq F -- + collapse to ~30 skeleton +# coll30eq min31 max99 ras3 coll30 eq T -- + accept-equal bridge +# collapse FIRES iff a picked clade > collapse_target; min31 > coll30 => every eligible +# pick collapses, so DIVERSITY (eligible clades in [31,99]) > 0 PROVES collapse fires +# (advisor's firing check, no rebuild). Low diversity (1-2) + null => walk-up is the +# missing piece (implement next), NOT "(c) refuted" (pre-committed interpretation). +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband2"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) suppressWarnings(as.double(gsub(",", "", x))) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Wortley2006 Zhu2013 Giles2015")), "\\s+")[[1]] +target <- c(Wortley2006 = 479, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) +ROUNDS <- as.integer(Sys.getenv("TS_RSSROUNDS", "15")) +SEEDS <- as.integer(strsplit(Sys.getenv("TS_SEEDS", "1 2"), "\\s+")[[1]]) + +# rooted clade sizes (proxy for the C++ eligible set: subtree_size per internal node) +cladeSizes <- function(tree) { + nTip <- length(tree$tip.label) + po <- Postorder(tree)$edge + cnt <- integer(max(po)) + cnt[seq_len(nTip)] <- 1L + for (i in seq_len(nrow(po))) cnt[po[i, 1]] <- cnt[po[i, 1]] + cnt[po[i, 2]] + cnt[(nTip + 1):length(cnt)] +} + +# config = list(min, max, ras, coll, eq) +cfgs <- list( + base = list(6L, 50L, 1L, 0L, FALSE), + bigNoColl = list(31L, 99L, 3L, 0L, FALSE), + coll30 = list(31L, 99L, 3L, 30L, FALSE), + coll30eq = list(31L, 99L, 3L, 30L, TRUE) +) + +get_t0 <- function(phy, wd) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + writeLines(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1000;", + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", "quit;"), + file.path(wd, "cstest.run")) + old <- setwd(wd); on.exit(setwd(old)) + invisible(suppressWarnings(system2(TNT, args = "cstest.run;", stdout = TRUE, stderr = TRUE))) + t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + t0 +} +run_cfg <- function(phy, t0, cfg, seed) { + set.seed(seed) + r <- suppressWarnings(MaximizeParsimony(phy, tree = t0, maxReplicates = 1L, nThreads = 1L, + maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, driftCycles = 0L, + xssRounds = 0L, cssRounds = 0L, rssRounds = ROUNDS, wagnerStarts = 1L, + fuseInterval = 9999L, + sectorMinSize = cfg[[1]], sectorMaxSize = cfg[[2]], rasStarts = cfg[[3]], + sectorCollapseTarget = cfg[[4]], sectorAcceptEqual = cfg[[5]])) + min(as.double(attr(r, "score"))) +} + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]); n <- NTip(phy); tgt <- target[[nm]] + wd <- file.path(tempdir(), paste0("cs", Sys.getpid(), nm)) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + t0 <- get_t0(phy, wd); t0len <- TreeLength(t0, phy) + cs <- cladeSizes(t0); inband <- sum(cs >= 31 & cs <= 99) + cat(sprintf("\n==== %s (%dt) | T0=%.0f target=%d | eligible clades in [31,99]: %d (>30 total: %d) ====\n", + nm, n, t0len, tgt, inband, sum(cs > 30))) + if (t0len < tgt - 0.5) cat(" [!] T0 already below target -- mapping/score sanity FAIL; skip\n") + for (cn in names(cfgs)) { + sc <- vapply(SEEDS, function(s) run_cfg(phy, t0, cfgs[[cn]], s), double(1)) + best <- min(sc) + cat(sprintf(" %-10s seeds[%s] -> %s | best %.0f (%+.0f vs T0, %+.0f vs target)%s\n", + cn, paste(SEEDS, collapse = ","), paste(format(sc), collapse = " "), + best, best - t0len, best - tgt, if (best <= tgt) " <== REACHED" else "")) + } +} diff --git a/dev/benchmarks/diag_convergence_ab.R b/dev/benchmarks/diag_convergence_ab.R new file mode 100644 index 000000000..accda35c0 --- /dev/null +++ b/dev/benchmarks/diag_convergence_ab.R @@ -0,0 +1,107 @@ +# A/B: xmult-style convergence stop vs the full thorough run. +# Lead lever = consensus_stable_reps (existing param, zero new code): it stops +# when the strict consensus of best-score trees is unchanged for K replicates. +# It is INHERENTLY safe against late score improvements: a new best score +# rebuilds the best-set, changing the consensus hash and resetting the counter, +# so the stability count cannot accumulate while improvements are still arriving. +# +# Verification is on the DELIVERABLE, not just the score: +# - score must equal the full-run MPT score (no quality loss); +# - strict-consensus fidelity vs the full run via ClusteringInfoDist (~0); +# - MPT count retained (early stop must not return a threadbare MPT set). +# +# Also free-simulates a pure score-plateau stop over a K sweep from the full +# run's replicate_scores, to show the (larger, dataset-dependent) K a blunt +# plateau stop would need — contrasting with consensus-stable's safety. +# +# Env: TS_LIB (default .agent-stop), NSEED (default 3). +.libPaths(c(Sys.getenv("TS_LIB", ".agent-stop"), .libPaths())) +suppressMessages({ library(TreeSearch); library(TreeTools); library(TreeDist) }) + +nseed <- as.integer(Sys.getenv("NSEED", "3")) +datasets <- c("Wortley2006", "Zanol2014", "Zhu2013", "Giles2015") +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) +csK <- c(3L, 5L) # consensus_stable_reps arms +data("inapplicable.phyData", package = "TreeSearch") + +# Strict consensus as a single phylo (handles the 1-tree case). +.strict <- function(trees) { + if (inherits(trees, "phylo")) return(trees) + if (length(trees) == 1L) return(trees[[1]]) + ape::consensus(trees, p = 1) +} +# Smallest score-plateau K that would NOT degrade the final score, simulated +# from the running-min trajectory: walk reps, reset stall on each new min; +# the run stops at the first rep where stall == K. Returns the min K for which +# the achieved (running-min-at-stop) score equals the full final score. +.minSafePlateauK <- function(scores) { + finalBest <- min(scores) + runMin <- cummin(scores) + for (K in seq_len(length(scores))) { + stall <- 0L; stoppedAt <- length(scores) + for (i in seq_along(scores)) { + if (i > 1L && runMin[i] < runMin[i - 1L]) stall <- 0L else stall <- stall + 1L + if (stall >= K) { stoppedAt <- i; break } + } + if (runMin[stoppedAt] <= finalBest) return(K) + } + length(scores) +} + +runOne <- function(phy, seed, csReps) { + set.seed(seed) + t <- system.time( + r <- suppressWarnings(MaximizeParsimony(phy, strategy = "thorough", + maxSeconds = 600, nThreads = 1L, verbosity = 0L, + consensusStableReps = csReps))) + list(trees = r, score = min(as.double(attr(r, "score"))), + reps = attr(r, "replicates"), + lastImp = attr(r, "last_improved_rep"), + repScores = attr(r, "replicate_scores"), + consensusStop = isTRUE(attr(r, "consensus_stable")), + nMPT = length(r), wall = as.double(t["elapsed"])) +} + +rows <- list() +for (nm in datasets) { + m <- PhyDatToMatrix(inapplicable.phyData[[nm]], ambigNA = FALSE); m[m == "-"] <- "?" + phy <- MatrixToPhyDat(m) + for (s in seq_len(nseed)) { + ref <- runOne(phy, s, 0L) # full run = reference + refCons <- .strict(ref$trees) + safeK <- .minSafePlateauK(ref$repScores) + for (K in csK) { + arm <- runOne(phy, s, K) + armCons <- .strict(arm$trees) + cid <- tryCatch( + as.double(ClusteringInfoDist(refCons, armCons, normalize = TRUE)), + error = function(e) NA_real_) + rows[[length(rows) + 1L]] <- data.frame( + dataset = nm, seed = s, csReps = K, + refScore = ref$score, armScore = arm$score, + scoreLoss = arm$score - ref$score, + refReps = ref$reps, armReps = arm$reps, + refWall = round(ref$wall, 1), armWall = round(arm$wall, 1), + wallFrac = round(arm$wall / ref$wall, 2), + refMPT = ref$nMPT, armMPT = arm$nMPT, + consCID = round(cid, 4), stoppedOnConsensus = arm$consensusStop, + plateauSafeK = safeK) + cat(sprintf(paste0("%-12s s%d cs%d: score %.0f vs %.0f (loss %+.0f) | ", + "reps %d->%d wall %.1f->%.1fs (%.0f%%) | ", + "MPT %d->%d consCID %.3f | plateauSafeK=%d %s\n"), + nm, s, K, ref$score, arm$score, arm$score - ref$score, + ref$reps, arm$reps, ref$wall, arm$wall, 100 * arm$wall / ref$wall, + ref$nMPT, arm$nMPT, ifelse(is.na(cid), -1, cid), safeK, + ifelse(arm$consensusStop, "[cons-stop]", "[other-stop]"))) + } + } +} +df <- do.call(rbind, rows) +write.csv(df, file.path(Sys.getenv("OUTDIR", "dev/benchmarks"), + "convergence_ab.csv"), row.names = FALSE) +cat("\n=== median by dataset x csReps ===\n") +agg <- aggregate(cbind(scoreLoss, armReps, wallFrac, armMPT, consCID, plateauSafeK) ~ + dataset + csReps, df, median) +print(agg[order(agg$dataset, agg$csReps), ], row.names = FALSE) +cat("\nGo if: scoreLoss==0 everywhere, consCID~0, armMPT not collapsed, wallFrac<<1.\n") +cat("plateauSafeK shows the (larger) K a blunt score-plateau would need.\n") diff --git a/dev/benchmarks/diag_convergence_enum.R b/dev/benchmarks/diag_convergence_enum.R new file mode 100644 index 000000000..9f63373f9 --- /dev/null +++ b/dev/benchmarks/diag_convergence_enum.R @@ -0,0 +1,84 @@ +# Does cheap MPT enumeration recover consensus completeness when the convergence +# stop is ON? Enumeration (TBR plateau walk in finish:) is SKIPPED while the +# pool is at its cap (100), so the early-stop's clustered 100-tree pool stays +# over-resolved. Test: give the pool room (poolMaxSize up) so enumeration runs +# and injects within-island diversity; measure whether the strict-consensus node +# count collapses back toward the full run's, and at what wall cost. +# +# Reference for CID = leave-one-out union of the FULL-run MPTs (unbiased). +# "Truth" is the MOST collapsed consensus (fewest internal nodes); lower armNode +# = closer to truth = better. Win if cs6+enum: score 0-loss, armNode <= fullNode +# (or close), wall still << full. Else it's a genuine speed/consensus tradeoff. +# +# Env: TS_LIB (default .agent-stop), NSEED (default 3), POOL (default 400). +.libPaths(c(Sys.getenv("TS_LIB", ".agent-stop"), .libPaths())) +suppressMessages({ library(TreeSearch); library(TreeTools); library(TreeDist) }) + +nseed <- as.integer(Sys.getenv("NSEED", "3")) +pool <- as.integer(Sys.getenv("POOL", "400")) +datasets <- c("Zanol2014", "Zhu2013") # the two over-resolved cases +data("inapplicable.phyData", package = "TreeSearch") + +.phy <- function(nm) { + m <- PhyDatToMatrix(inapplicable.phyData[[nm]], ambigNA = FALSE); m[m == "-"] <- "?" + MatrixToPhyDat(m) +} +.strict <- function(trees) { + if (inherits(trees, "phylo")) return(trees) + if (length(trees) == 1L) return(trees[[1]]) + ape::consensus(trees, p = 1) +} + +rows <- list() +for (nm in datasets) { + phy <- .phy(nm) + fullTrees <- vector("list", nseed); armList <- vector("list", nseed) + fullWall <- armWall <- numeric(nseed); fullSc <- armSc <- numeric(nseed) + for (s in seq_len(nseed)) { + set.seed(s) + tf <- system.time(rf <- suppressWarnings(MaximizeParsimony(phy, + strategy = "thorough", maxSeconds = 600, nThreads = 1L, + verbosity = 0L))) # full, pool 100 + set.seed(s) + ta <- system.time(ra <- suppressWarnings(MaximizeParsimony(phy, + strategy = "thorough", maxSeconds = 600, nThreads = 1L, + verbosity = 0L, consensusStableReps = 6L, + poolMaxSize = pool, enumTimeFraction = 0.3))) # stop + enum room + fullTrees[[s]] <- rf; armList[[s]] <- ra + fullWall[s] <- tf["elapsed"]; armWall[s] <- ta["elapsed"] + fullSc[s] <- min(as.double(attr(rf, "score"))) + armSc[s] <- min(as.double(attr(ra, "score"))) + } + for (s in seq_len(nseed)) { + others <- setdiff(seq_len(nseed), s) + refPool <- do.call(c, lapply(others, function(j) { + tj <- fullTrees[[j]]; if (inherits(tj, "phylo")) list(tj) else tj })) + class(refPool) <- "multiPhylo" + refCons <- .strict(refPool) + fullCons <- .strict(fullTrees[[s]]); armCons <- .strict(armList[[s]]) + cidFull <- as.double(ClusteringInfoDist(fullCons, refCons, normalize = TRUE)) + cidArm <- as.double(ClusteringInfoDist(armCons, refCons, normalize = TRUE)) + rows[[length(rows) + 1L]] <- data.frame( + dataset = nm, seed = s, scoreLoss = armSc[s] - fullSc[s], + fullWall = round(fullWall[s], 1), armWall = round(armWall[s], 1), + wallFrac = round(armWall[s] / fullWall[s], 2), + fullMPT = length(fullTrees[[s]]), armMPT = length(armList[[s]]), + fullNode = fullCons$Nnode, armNode = armCons$Nnode, + nodeDelta = armCons$Nnode - fullCons$Nnode, + cidFull2ref = round(cidFull, 4), cidArm2ref = round(cidArm, 4)) + cat(sprintf(paste0("%-12s s%d: loss %+.0f | wall %.0f%% (%.1f->%.1fs) | ", + "MPT %d->%d | nodes full=%d arm=%d (%+d) | cid full=%.3f arm=%.3f\n"), + nm, s, armSc[s] - fullSc[s], 100 * armWall[s] / fullWall[s], + fullWall[s], armWall[s], length(fullTrees[[s]]), length(armList[[s]]), + fullCons$Nnode, armCons$Nnode, armCons$Nnode - fullCons$Nnode, + cidFull, cidArm)) + } +} +df <- do.call(rbind, rows) +write.csv(df, file.path(Sys.getenv("OUTDIR", "dev/benchmarks"), + "convergence_enum.csv"), row.names = FALSE) +cat("\n=== median by dataset (cs6 + poolMaxSize=", pool, " + enumFrac 0.3) ===\n", sep = "") +agg <- aggregate(cbind(scoreLoss, wallFrac, armMPT, nodeDelta, cidArm2ref, cidFull2ref) ~ + dataset, df, median) +print(agg, row.names = FALSE) +cat("\nRecovery WIN if nodeDelta ~<=0 and wallFrac << 1. Else: genuine tradeoff -> ask user.\n") diff --git a/dev/benchmarks/diag_convergence_fidelity.R b/dev/benchmarks/diag_convergence_fidelity.R new file mode 100644 index 000000000..4d06391ae --- /dev/null +++ b/dev/benchmarks/diag_convergence_fidelity.R @@ -0,0 +1,114 @@ +# Consensus-fidelity gate for the xmult-style convergence stop (consensusStableReps). +# Decides whether to DEFAULT the stop, using an UNBIASED reference: +# +# (1) Leave-one-out union consensus: ref_s = strict consensus of the union of +# the OTHER seeds' full-run MPTs (so a full run is never scored against a +# set it belongs to). Ship-clear if mean consCID(cs->ref) <= mean(full->ref): +# early-stopping then costs nothing the seed lottery wasn't already costing. +# (2) Resolution direction: internal-node count of each consensus. If the +# early-stop consensus is MORE resolved than the full-run consensus it +# overstates support (harmful); equal-or-less is conservative (harmless). +# (3) Zhu stress: extra seeds at the ship K, score-loss must stay 0 (Zhu is the +# high-plateauSafeK case where cs3 already lost +1). +# +# Scope: `thorough` only (all tuning was on thorough). +# Env: TS_LIB (default .agent-stop), NSEED (default 3), SHIPK (default 6), +# ZHU_EXTRA (default 9 extra Zhu seeds for the stress test). +.libPaths(c(Sys.getenv("TS_LIB", ".agent-stop"), .libPaths())) +suppressMessages({ library(TreeSearch); library(TreeTools); library(TreeDist) }) + +nseed <- as.integer(Sys.getenv("NSEED", "3")) +shipK <- as.integer(Sys.getenv("SHIPK", "6")) +zhuExtra<- as.integer(Sys.getenv("ZHU_EXTRA", "9")) +armsK <- c(5L, shipK) # cs5 (continuity) + ship candidate +datasets <- c("Wortley2006", "Zanol2014", "Zhu2013", "Giles2015") +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) +data("inapplicable.phyData", package = "TreeSearch") + +.phy <- function(nm) { + m <- PhyDatToMatrix(inapplicable.phyData[[nm]], ambigNA = FALSE); m[m == "-"] <- "?" + MatrixToPhyDat(m) +} +.strict <- function(trees) { + if (inherits(trees, "phylo")) return(trees) + if (length(trees) == 1L) return(trees[[1]]) + ape::consensus(trees, p = 1) +} +.run <- function(phy, seed, csReps) { + set.seed(seed) + t <- system.time( + r <- suppressWarnings(MaximizeParsimony(phy, strategy = "thorough", + maxSeconds = 600, nThreads = 1L, verbosity = 0L, + consensusStableReps = csReps))) + attr(r, "wall") <- as.double(t["elapsed"]); r +} + +# --- Phases 1+2: fidelity + resolution on 4 datasets x nseed ------------------- +fid <- list() +for (nm in datasets) { + phy <- .phy(nm) + fullTrees <- vector("list", nseed); fullScore <- numeric(nseed) + armTrees <- list() # [[K]][[seed]] + for (K in armsK) armTrees[[as.character(K)]] <- vector("list", nseed) + for (s in seq_len(nseed)) { + rf <- .run(phy, s, 0L) + fullTrees[[s]] <- rf; fullScore[s] <- min(as.double(attr(rf, "score"))) + for (K in armsK) armTrees[[as.character(K)]][[s]] <- .run(phy, s, K) + } + # Leave-one-out union reference + comparisons + for (s in seq_len(nseed)) { + others <- setdiff(seq_len(nseed), s) + pool <- do.call(c, lapply(others, function(j) { + tj <- fullTrees[[j]]; if (inherits(tj, "phylo")) list(tj) else tj + })) + class(pool) <- "multiPhylo" + refCons <- .strict(pool) + refNode <- refCons$Nnode + fullCons <- .strict(fullTrees[[s]]) + cidFull <- as.double(ClusteringInfoDist(fullCons, refCons, normalize = TRUE)) + for (K in armsK) { + ar <- armTrees[[as.character(K)]][[s]] + arCons <- .strict(ar) + cidArm <- as.double(ClusteringInfoDist(arCons, refCons, normalize = TRUE)) + fid[[length(fid) + 1L]] <- data.frame( + dataset = nm, seed = s, csReps = K, + fullScore = fullScore[s], armScore = min(as.double(attr(ar, "score"))), + scoreLoss = min(as.double(attr(ar, "score"))) - fullScore[s], + fullWall = round(attr(fullTrees[[s]], "wall"), 1), + armWall = round(attr(ar, "wall"), 1), + wallFrac = round(attr(ar, "wall") / attr(fullTrees[[s]], "wall"), 2), + cidFull2ref = round(cidFull, 4), cidArm2ref = round(cidArm, 4), + refNode = refNode, fullNode = fullCons$Nnode, armNode = arCons$Nnode, + # >0 => arm MORE resolved than full (overstates support = harmful) + nodeDelta = arCons$Nnode - fullCons$Nnode) + cat(sprintf(paste0("%-12s s%d cs%d: loss %+.0f | wall %.0f%% | ", + "cid(full->ref)=%.3f cid(cs->ref)=%.3f | nodes full=%d cs=%d (%+d)\n"), + nm, s, K, min(as.double(attr(ar,"score"))) - fullScore[s], + 100 * attr(ar,"wall")/attr(fullTrees[[s]],"wall"), + cidFull, cidArm, fullCons$Nnode, arCons$Nnode, + arCons$Nnode - fullCons$Nnode)) + } + } +} +fdf <- do.call(rbind, fid) +write.csv(fdf, file.path(Sys.getenv("OUTDIR", "dev/benchmarks"), + "convergence_fidelity.csv"), row.names = FALSE) +cat("\n=== fidelity median by dataset x csReps ===\n") +agg <- aggregate(cbind(scoreLoss, wallFrac, cidFull2ref, cidArm2ref, nodeDelta) ~ + dataset + csReps, fdf, median) +print(agg[order(agg$dataset, agg$csReps), ], row.names = FALSE) + +# --- Phase 3: Zhu stress at ship K (score-loss only) --------------------------- +cat(sprintf("\n=== Zhu stress: seeds %d..%d at cs%d ===\n", nseed + 1L, + nseed + zhuExtra, shipK)) +phyZ <- .phy("Zhu2013"); zloss <- integer(0) +for (s in (nseed + 1L):(nseed + zhuExtra)) { + rz <- .run(phyZ, s, shipK) + sc <- min(as.double(attr(rz, "score"))) + zloss <- c(zloss, sc - target[["Zhu2013"]]) + cat(sprintf("Zhu s%d cs%d: score %.0f (%+.0f) | reps %d wall %.1fs\n", + s, shipK, sc, sc - target[["Zhu2013"]], + attr(rz, "replicates"), attr(rz, "wall"))) +} +cat(sprintf("\nZhu stress at cs%d: max loss over %d extra seeds = %+d (want 0)\n", + shipK, zhuExtra, max(zloss))) diff --git a/dev/benchmarks/diag_convergence_tail.R b/dev/benchmarks/diag_convergence_tail.R new file mode 100644 index 000000000..f5fe6b700 --- /dev/null +++ b/dev/benchmarks/diag_convergence_tail.R @@ -0,0 +1,71 @@ +# Convergence-tail diagnostic: is there a recoverable wall-clock tail between +# time-to-first-MPT and the stop replicate? TS self-terminates on targetHits +# (~nTip/5 rediscoveries); TNT xmult stops the moment the score stops improving. +# Because best_score is monotonic, `last_improved_rep` IS time-to-first-MPT. +# The gap (replicates - last_improved_rep) is the tail an xmult-style stop cuts. +# +# Also records the MAX gap between consecutive score improvements on the path to +# the optimum (from replicate_scores), which sets the floor for any plateau-K. +# +# Env: TS_LIB (default .agent-stop), NSEED (default 3). +.libPaths(c(Sys.getenv("TS_LIB", ".agent-stop"), .libPaths())) +suppressMessages({ library(TreeSearch); library(TreeTools) }) + +nseed <- as.integer(Sys.getenv("NSEED", "3")) +datasets <- c("Wortley2006", "Zanol2014", "Zhu2013", "Giles2015") +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) + +data("inapplicable.phyData", package = "TreeSearch") + +# Running-minimum of the per-replicate best score, and the replicate at which +# each new minimum (improvement) first appeared; returns the max gap between +# consecutive improvements (patience a plateau-stop must survive). +.maxImproveGap <- function(scores) { + if (length(scores) < 2L) return(0L) + runMin <- cummin(scores) + improveReps <- which(c(TRUE, diff(runMin) < 0)) # reps that lowered the min + if (length(improveReps) < 2L) return(0L) + max(diff(improveReps)) +} + +rows <- list() +for (nm in datasets) { + m <- PhyDatToMatrix(inapplicable.phyData[[nm]], ambigNA = FALSE) + m[m == "-"] <- "?" + phy <- MatrixToPhyDat(m) + nTip <- length(phy) + for (s in seq_len(nseed)) { + set.seed(s) + t <- system.time( + r <- suppressWarnings(MaximizeParsimony(phy, strategy = "thorough", + maxSeconds = 600, nThreads = 1L, verbosity = 0L))) + reps <- attr(r, "replicates") + lastImp <- attr(r, "last_improved_rep") + repScores <- attr(r, "replicate_scores") + stopReason <- if (isTRUE(attr(r, "consensus_stable"))) "consensus" + else if (isTRUE(attr(r, "perturb_stop"))) "perturb" + else "targetHits/max" + rows[[length(rows) + 1L]] <- data.frame( + dataset = nm, nTip = nTip, target = target[[nm]], seed = s, + score = min(as.double(attr(r, "score"))), + reps = reps, lastImproveRep = lastImp, + tailReps = reps - lastImp, # recoverable replicates + maxImproveGap = .maxImproveGap(repScores), # floor for plateau-K + stopReason = stopReason, + wall_s = round(as.double(t["elapsed"]), 1)) + cat(sprintf("%-12s s%d: score %.0f (%+.0f) | reps %d, last-improve %d, tail %d, maxGap %d | %s | %.1fs\n", + nm, s, min(as.double(attr(r, "score"))), + min(as.double(attr(r, "score"))) - target[[nm]], + reps, lastImp, reps - lastImp, .maxImproveGap(repScores), + stopReason, t["elapsed"])) + } +} +df <- do.call(rbind, rows) +outdir <- Sys.getenv("OUTDIR", "dev/benchmarks") +write.csv(df, file.path(outdir, "convergence_tail.csv"), row.names = FALSE) +cat("\n=== median by dataset ===\n") +agg <- aggregate(cbind(reps, lastImproveRep, tailReps, maxImproveGap, wall_s) ~ dataset, + df, median) +print(agg, row.names = FALSE) +cat("\nIf tailReps >> 0 and maxImproveGap is small, an xmult-style plateau stop ", + "recovers (tailReps/reps) of the wall.\n", sep = "") diff --git a/dev/benchmarks/diag_d1_freehtu.R b/dev/benchmarks/diag_d1_freehtu.R new file mode 100644 index 000000000..496d96d29 --- /dev/null +++ b/dev/benchmarks/diag_d1_freehtu.R @@ -0,0 +1,46 @@ +# D1 ORACLE (audit D1) — scoring-only, NO reinsertion, ZERO topology-risk. +# From an identical TNT `mult` T0 (global-TBR optimum), run our rss sectorial and +# let the C++ TS_FREE_HTU_PROBE diagnostic (ts_sector.cpp search_sector) compare, +# per sector: the HTU-ANCHORED frozen reduced score vs an UNCONSTRAINED reduced +# search where the HTU floats as an ordinary (S+1)th leaf (free re-resolve x +# re-attach). Since reduced = full - const (const invariant to HTU attachment), +# free < frozen on ANY sector PROVES a strictly shorter FULL tree exists that our +# anchored sectorial cannot reach -> D1 confirmed (the escape lever). If no sector +# shows free < frozen, D1 is refuted for the EW case. +# advisor: the escape only shows via the from-scratch RAS rebuild w/ free HTU, +# so rasStarts>=3 and the free probe does its own Wagner+TBR (R=3). +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +Sys.setenv(TS_FREE_HTU_PROBE = "1") +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Wortley2006")), "\\s+")[[1]] + +get_t0 <- function(phy, seed = 1) { + wd <- file.path(tempdir(), paste0("d1t0", Sys.getpid())) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + writeLines(c("mxram 1024;", "proc data.tnt;", "hold 100;", sprintf("rseed %d;", seed), + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", "quit;"), + file.path(wd, "dttest.run")) + old <- setwd(wd); on.exit(setwd(old)) + invisible(suppressWarnings(system2(TNT, args = "dttest.run;", stdout = TRUE, stderr = TRUE))) + t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + t0 +} + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + t0 <- get_t0(phy) + cat(sprintf("\n==== %s | T0 len=%.0f | D1 warm-revert probe (rss-only, rasStarts=1) ====\n", + nm, TreeLength(t0, phy))) + set.seed(1) + invisible(suppressWarnings(MaximizeParsimony(phy, tree = t0, maxReplicates = 1L, + nThreads = 1L, maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, driftCycles = 0L, + xssRounds = 0L, cssRounds = 0L, rssRounds = 1L, rasStarts = 20L, wagnerStarts = 1L, + sectorMinSize = 30L, sectorMaxSize = 45L, fuseInterval = 9999L))) +} diff --git a/dev/benchmarks/diag_drift.R b/dev/benchmarks/diag_drift.R new file mode 100644 index 000000000..e192e495f --- /dev/null +++ b/dev/benchmarks/diag_drift.R @@ -0,0 +1,45 @@ +# DRIFT LEG: TNT's sectsch plateaus at the TS ceiling on Wortley (480); the final +# 480->479 is xmult's DRIFT (+fuse), which every TreeSearch preset disables +# (driftCycles=0, audit D3). Does enabling TNT-faithful drift let TS reach the +# xmult target? Wortley 479 (sectsch-null, drift-only) is the clean case; Zanol +# 1261 is mixed (sectorial + drift). nThreads=1, best-of-N over seeds. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +secs <- as.numeric(Sys.getenv("TS_SECONDS", "60")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Wortley2006")), "\\s+")[[1]] +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3 4 5")), "\\s+")[[1]]) +target <- c(Wortley2006 = 479, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) + +run <- function(phy, seed, extra) { + set.seed(seed) + args <- c(list(dataset = phy, maxReplicates = 9999L, maxSeconds = secs, + nThreads = 1L, verbosity = 0L), extra) + r <- tryCatch(suppressWarnings(do.call(MaximizeParsimony, args)), + error = function(e) { message("ERR: ", conditionMessage(e)); NULL }) + if (is.null(r)) return(NA_real_) + min(as.double(attr(r, "score"))) +} + +arms <- list( + intensive = list(strategy = "intensive"), # drift=0 baseline + drift30 = list(strategy = "intensive", driftCycles = 30L), + drift30_fuse = list(strategy = "intensive", driftCycles = 30L, intraFuse = TRUE) +) +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + tgt <- target[[nm]] + cat(sprintf("\n==== %s (%d tips) | xmult target=%d | %gs x %d seeds ====\n", + nm, NTip(phy), tgt, secs, length(seeds))) + for (an in names(arms)) { + sc <- vapply(seeds, function(s) run(phy, s, arms[[an]]), numeric(1)) + best <- suppressWarnings(min(sc, na.rm = TRUE)) + cat(sprintf(" %-14s best=%-5.0f median=%-6.1f all={%s} gap=%+.0f%s\n", + an, best, median(sc, na.rm = TRUE), paste(sprintf("%.0f", sc), collapse = ","), + best - tgt, if (is.finite(best) && best <= tgt) " <== REACHED" else "")) + } +} diff --git a/dev/benchmarks/diag_e2e_gate.R b/dev/benchmarks/diag_e2e_gate.R new file mode 100644 index 000000000..dcf7048e8 --- /dev/null +++ b/dev/benchmarks/diag_e2e_gate.R @@ -0,0 +1,44 @@ +# THE HONEST GATE (advisor): does the plateau fix help END-TO-END, time-matched? +# The oracle (sectorial-from-T0) is null because the 482 basin is across an uphill +# barrier accept-equal can't cross. But the full pipeline has ratchet/drift for +# uphill moves; the question is whether ADDING plateau sector exploration +# (rasStarts>1 + sectorAcceptEqual) helps the full search reach a better score in +# the SAME wall-clock. Time-matched (same maxSeconds) so a win isn't just churn. +# ON < OFF at matched time => real end-to-end improvement (ship-worthy) +# ON ~ OFF (or worse) => plateau is not the closer; gap needs uphill/other +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014")), "\\s+")[[1]] +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) +secs <- as.numeric(Sys.getenv("TS_SECONDS", "120")) +target <- c(Wortley2006 = 482, Zanol2014 = 1262, Zhu2013 = 627, Giles2015 = 671) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +run <- function(d, seed, plateau) { + set.seed(seed) + args <- list(dataset = d, maxSeconds = secs, verbosity = 0L) + if (plateau) { args$rasStarts <- 3L; args$sectorAcceptEqual <- TRUE } + r <- suppressWarnings(do.call(MaximizeParsimony, args)) + min(as.double(attr(r, "score"))) +} +rows <- list() +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + for (sd in seeds) { + off <- run(phy, sd, FALSE) + on <- run(phy, sd, TRUE) + rows[[length(rows) + 1]] <- data.frame(dataset = nm, seed = sd, off = off, on = on, + d = on - off, stringsAsFactors = FALSE) + cat(sprintf("%-11s s%d | OFF=%.0f ON=%.0f | d=%+.0f | TNT=%s\n", + nm, sd, off, on, on - off, target[[nm]])) + } +} +S <- do.call(rbind, rows) +cat("\n== medians (time-matched, full search) ==\n") +agg <- do.call(rbind, lapply(split(S, S$dataset), function(d) data.frame( + dataset = d$dataset[1], OFF = median(d$off), ON = median(d$on), + TNT = target[[d$dataset[1]]]))) +print(agg, row.names = FALSE) diff --git a/dev/benchmarks/diag_eq_bug.R b/dev/benchmarks/diag_eq_bug.R new file mode 100644 index 000000000..c07d409fa --- /dev/null +++ b/dev/benchmarks/diag_eq_bug.R @@ -0,0 +1,59 @@ +# IS sectorAcceptEqual=TRUE BUGGY? Decisive no-op check. +# Static read (ts_sector.cpp): the equal branch (1181-1187) KEEPS the equal-score +# topology (no revert), so accept_equal is structurally LIVE -- BUT unlike the strict +# branch (1147-1160) it does NOT recompute subtree_size/eligible, so selection goes +# STALE across a plateau walk (latent defect, flagged separately). +# Empirical no-op test: from a near-optimal TNT T0, run rss (rasStarts=3, large +# sectors) with accept_equal F vs T and compare the OUTPUT TOPOLOGY (CID): +# EQUAL-keep>0 (TS_SECT_DEBUG) AND CID(treeF,treeT)>0 -> LIVE (plateau-walks; equal +# final score just means no downhill exit) -> NOT a no-op bug. +# EQUAL-keep==0 -> equal moves never PROPOSED (search_sector tie path dead) -> BUG. +# EQUAL-keep>0 but CID==0 -> kept-but-identical reinsert -> BUG. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband2"), + winslash = "/")) + library(TreeTools) + library(TreeDist) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +nm <- Sys.getenv("TS_DS", "Zanol2014") +SMIN <- as.integer(Sys.getenv("TS_SMIN", "31")) +SMAX <- as.integer(Sys.getenv("TS_SMAX", "99")) +MAXHITS <- as.integer(Sys.getenv("TS_MAXHITS", "1")) +ROUNDS <- as.integer(Sys.getenv("TS_ROUNDS", "5")) +phy <- fitch(inapplicable.phyData[[nm]]) + +wd <- file.path(tempdir(), paste0("eqb", Sys.getpid())) +unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) +WriteTntCharacters(phy, file.path(wd, "data.tnt")) +writeLines(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1000;", + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", "quit;"), + file.path(wd, "eqbtest.run")) +old <- setwd(wd); invisible(suppressWarnings(system2(TNT, args = "eqbtest.run;", stdout = TRUE, stderr = TRUE))); setwd(old) +t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] +t0len <- TreeLength(t0, phy) +cat(sprintf("==== %s | T0 len=%.0f (rasStarts=3, sectors[%d,%d], rssRounds=5) ====\n", nm, t0len, SMIN, SMAX)) + +Sys.setenv(TS_SECT_DEBUG = "1") # streams STRICT / EQUAL-keep / WORSE-revert to stderr +run1 <- function(eq) { + set.seed(1) + suppressWarnings(MaximizeParsimony(phy, tree = t0, maxReplicates = 1L, nThreads = 1L, + maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, driftCycles = 0L, + xssRounds = 0L, cssRounds = 0L, rssRounds = ROUNDS, wagnerStarts = 1L, fuseInterval = 9999L, + sectorMinSize = SMIN, sectorMaxSize = SMAX, rasStarts = 3L, sectorMaxHits = MAXHITS, + sectorCollapseTarget = 0L, sectorAcceptEqual = eq)) +} +cat("\n--- run F (accept_equal=FALSE) ---\n"); treeF <- run1(FALSE) +cat("\n--- run T (accept_equal=TRUE) ---\n"); treeT <- run1(TRUE) +sF <- min(as.double(attr(treeF, "score"))); sT <- min(as.double(attr(treeT, "score"))) +if (inherits(treeF, "multiPhylo")) treeF <- treeF[[1]] +if (inherits(treeT, "multiPhylo")) treeT <- treeT[[1]] +cid <- function(a, b) as.double(ClusteringInfoDist(a, b, normalize = TRUE)) +cat(sprintf("\n accept_equal=F: score=%.0f CID(T0,F)=%.3f\n", sF, cid(t0, treeF))) +cat(sprintf(" accept_equal=T: score=%.0f CID(T0,T)=%.3f\n", sT, cid(t0, treeT))) +dFT <- cid(treeF, treeT) +cat(sprintf(" CID(treeF, treeT) = %.3f %s\n", dFT, + if (dFT > 1e-6) "<- topology DIFFERS (accept_equal LIVE)" else + "<- IDENTICAL (accept_equal had NO topological effect)")) diff --git a/dev/benchmarks/diag_escape_wortley.R b/dev/benchmarks/diag_escape_wortley.R new file mode 100644 index 000000000..969de50e1 --- /dev/null +++ b/dev/benchmarks/diag_escape_wortley.R @@ -0,0 +1,92 @@ +# WORTLEY 480->479 ESCAPE PROBE — the simplest reproducible instance of the gap. +# TS stalls at 480 (15/15 runs, even intensive+fuse, 60s); TNT xmult reaches 479. +# Characterise the barrier on the SMALLEST case: +# (a) commensurate: does TreeLength score TNT's 479 tree as 479? +# (b) are 479 and TS's 480 BOTH TS-TBR local optima? (if 479 drops below under +# our TBR, 479 isn't optimal; if both hold, they are separate basins) +# (c) how far apart are the two optima? (RF split distance) +# (d) can TS's TBR reach 479 if STARTED adjacent to it? (perturb T479 by 1 NNI, +# re-search: returns to 479 => findable basin; falls to 480 => leaks away) +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +phy <- fitch(inapplicable.phyData[["Wortley2006"]]) + +# RF (symmetric split difference); RobinsonFoulds not exported in this TreeTools. +rf <- function(a, b) { + b <- KeepTip(b, a$tip.label) + cl <- function(t) { + pp <- ape::prop.part(t); labs <- attr(pp, "labels") + s <- vapply(pp, function(ix) { + x <- labs[ix]; if (length(x) > length(labs) / 2) x <- setdiff(labs, x) + paste(sort(x), collapse = ",") + }, character(1)) + s[vapply(strsplit(s, ","), length, 1L) >= 2L] + } + sa <- cl(a); sb <- cl(b) + length(setdiff(sa, sb)) + length(setdiff(sb, sa)) +} + +# Pure-TBR polish from a given tree (ratchet/drift/sectorial OFF). +polish <- function(tree) { + set.seed(1) + r <- suppressWarnings(MaximizeParsimony(phy, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, rssRounds = 0L, + cssRounds = 0L, wagnerStarts = 1L, fuseInterval = 9999L)) + min(as.double(attr(r, "score"))) +} + +# 1. TNT -> 479 tree +wd <- file.path(tempdir(), paste0("esc", Sys.getpid())) +unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) +WriteTntCharacters(phy, file.path(wd, "data.tnt")) +writeLines(c("mxram 1024;", "proc data.tnt;", "hold 10000;", "rseed 1;", + "xmult=hits 10 replic 50;", "best;", "tsave *t479.tre;", "save;", + "tsave/;", "quit;"), file.path(wd, "esctest.run")) +old <- setwd(wd) +out <- suppressWarnings(system2(TNT, args = "esctest.run;", stdout = TRUE, stderr = TRUE)) +setwd(old) +out <- iconv(out, from = "", to = "UTF-8", sub = "") +tnt_score <- num(sub(".*Best score:\\s*([0-9.]+).*", "\\1", grep("Best score:", out, value = TRUE)[1])) +T479 <- ReadTntTree(file.path(wd, "t479.tre")); if (inherits(T479, "multiPhylo")) T479 <- T479[[1]] +len479 <- TreeLength(T479, phy) +cat(sprintf("(a) TNT Best score=%.0f | TreeLength(T479)=%.0f [commensurate: %s]\n", + tnt_score, len479, if (isTRUE(tnt_score == len479)) "YES" else "NO!")) + +# 2. TS from scratch -> best (intensive, 3 seeds) +best_ts <- NULL; best_len <- Inf +for (s in 1:3) { + set.seed(s) + r <- suppressWarnings(MaximizeParsimony(phy, strategy = "intensive", + maxReplicates = 9999L, maxSeconds = 30, nThreads = 1L, verbosity = 0L)) + l <- min(as.double(attr(r, "score"))) + tr <- if (inherits(r, "multiPhylo")) r[[1]] else r + if (l < best_len) { best_len <- l; best_ts <- tr } +} +cat(sprintf(" TS-from-scratch best=%.0f\n", best_len)) + +# 3/(b) local-optimum check +p479 <- polish(T479); p480 <- polish(best_ts) +cat(sprintf("(b) TBR-polish T479 -> %.0f (479 %s TBR-optimal) | TBR-polish TS-best -> %.0f (%.0f %s TBR-optimal)\n", + p479, if (p479 >= len479) "IS" else "NOT", p480, best_len, + if (p480 >= best_len) "IS" else "NOT")) + +# 4/(c) basin distance +cat(sprintf("(c) RF(T479, TS-best) = %d splits (max %d)\n", rf(T479, best_ts), NTip(phy) - 3L)) + +# 5/(d) start TS adjacent to T479: does it find 479? +set.seed(7) +adj <- TBRMoves <- NULL +adj <- tryCatch(TreeTools::Postorder(ape::rNNI(T479, moves = 1L)), error = function(e) T479) +r_adj <- suppressWarnings(MaximizeParsimony(phy, tree = adj, strategy = "intensive", + maxReplicates = 50L, maxSeconds = 30, nThreads = 1L, verbosity = 0L)) +cat(sprintf("(d) TS from (T479 + 1 NNI) -> %.0f (%s recover 479)\n", + min(as.double(attr(r_adj, "score"))), + if (min(as.double(attr(r_adj, "score"))) <= len479) "DID" else "did NOT")) diff --git a/dev/benchmarks/diag_full_reach.R b/dev/benchmarks/diag_full_reach.R new file mode 100644 index 000000000..5dc7a4386 --- /dev/null +++ b/dev/benchmarks/diag_full_reach.R @@ -0,0 +1,28 @@ +# PREMISE RE-CONFIRM (advisor): does TreeSearch's FULL default search ever reach +# TNT's sectorial score? The harness lied about the levers; sanity-check it didn't +# also flatter the headline gap. If full search reaches the target, sectorial-from-T0 +# is one weak link others cover (a wall-clock problem). If it plateaus above, the +# sectorial gap is the genuine missing piece. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Wortley2006 Zanol2014")), "\\s+")[[1]] +secs <- as.numeric(Sys.getenv("TS_SECONDS", "120")) +target <- c(Wortley2006 = 482, Zanol2014 = 1262, Zhu2013 = 627, Giles2015 = 671) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + set.seed(1) + t0 <- proc.time() + r <- suppressWarnings(MaximizeParsimony(phy, maxSeconds = secs, verbosity = 0L)) + el <- (proc.time() - t0)["elapsed"] + best <- min(as.double(attr(r, "score"))) + tg <- target[[nm]] + cat(sprintf("%-11s | full default best=%.0f | TNT target=%s | %s | %.0fs ntrees=%d\n", + nm, best, ifelse(is.null(tg), "?", tg), + ifelse(!is.null(tg) && best <= tg, "REACHED", "ABOVE"), el, + length(r))) +} diff --git a/dev/benchmarks/diag_gap_panel_postfix.R b/dev/benchmarks/diag_gap_panel_postfix.R new file mode 100644 index 000000000..07faf9303 --- /dev/null +++ b/dev/benchmarks/diag_gap_panel_postfix.R @@ -0,0 +1,35 @@ +# Gap-panel re-measurement AFTER the Wagner + TBR-vroot + build_ras_sector +# directional-cost fixes. The 2026-06-16 plan concluded the EW-Fitch score gap +# was at a "landscape/escape-bound floor" (+1.5..+3.5 on the hard datasets) with +# a "competitive per-candidate" kernel -- but that predated finding the +# union-of-finals cost bug. This re-runs the full `thorough` pipeline at a fixed +# budget on the hard panel to test whether the bug fix closed the score gap. +# Env: TS_LIB (default .agent-sectfix), TS_SECONDS, TS_SEEDS, TS_DATASETS. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-sectfix"), + winslash = "/")) + library(TreeTools) +}) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +# Known EW-Fitch MPT / TNT targets (apples-to-apples, -> ?). +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Wortley2006 Zanol2014 Zhu2013 Giles2015")), "\\s+")[[1]] +secs <- as.integer(Sys.getenv("TS_SECONDS", "60")) +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3")), "\\s+")[[1]]) + +cat(sprintf("Gap panel | thorough | %ds | seeds {%s} | lib %s\n", + secs, paste(seeds, collapse=","), Sys.getenv("TS_LIB", ".agent-sectfix"))) +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]); tgt <- target[[nm]] + sc <- vapply(seeds, function(s) { + set.seed(s) + r <- suppressWarnings(MaximizeParsimony(phy, strategy = "thorough", + maxSeconds = secs, nThreads = 1L, verbosity = 0L)) + min(as.double(attr(r, "score"))) + }, double(1)) + cat(sprintf(" %-12s target=%4d scores {%s} min %+.0f median %+.0f\n", + nm, tgt, paste(sprintf("%.0f", sc), collapse=","), + min(sc) - tgt, median(sc) - tgt)) +} diff --git a/dev/benchmarks/diag_locate.R b/dev/benchmarks/diag_locate.R new file mode 100644 index 000000000..afc629a39 --- /dev/null +++ b/dev/benchmarks/diag_locate.R @@ -0,0 +1,59 @@ +# LOCALISE THE MISSING MOVE (advisor rung 4): where do T0 (487) and TNT's best +# (482) differ on Wortley? Prune to the symmetric split-difference. If the 5 steps +# live in ONE small sub-clade, a single correct sector should recover it (=> our +# selection or reduction misses it). If spread across many clades, it's the +# ITERATION of accept-equal resolves that matters. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +nm <- Sys.getenv("TS_DATASET", "Wortley2006") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), paste0("locate", Sys.getpid())) +unlink(wd, recursive = TRUE); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +phy <- fitch(inapplicable.phyData[[nm]]) +WriteTntCharacters(phy, file.path(wd, "data.tnt")) +script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", "rseed 1;", "taxname=;", + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", 8), "tsave *best.tre;", "save;", "tsave/;", "quit;") +writeLines(script, file.path(wd, "loctest.run")) +old <- setwd(wd) +out <- suppressWarnings(system2(TNT, args = "loctest.run;", stdout = TRUE, stderr = TRUE)) +setwd(old) +t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] +best <- ReadTntTree(file.path(wd, "best.tre")); if (inherits(best, "multiPhylo")) best <- best[[1]] +labs <- TipLabels(t0) +best <- KeepTip(best, labs) +cat(sprintf("%s: TreeLength T0=%.0f best=%.0f (diff %+.0f)\n", + nm, TreeLength(t0, phy), TreeLength(best, phy), + TreeLength(best, phy) - TreeLength(t0, phy))) + +# Clades (bipartitions) of each tree as canonical tip-sets, via ape::prop.part. +clades <- function(tr) { + pp <- ape::prop.part(tr); lab <- attr(pp, "labels") + lapply(pp, function(ix) sort(lab[ix])) +} +small <- function(s) if (length(s) <= length(labs) / 2) s else sort(setdiff(labs, s)) +key <- function(s) paste(small(s), collapse = "|") +c0 <- clades(t0); cb <- clades(best) +k0 <- vapply(c0, key, ""); kb <- vapply(cb, key, "") +gained <- cb[!(kb %in% k0)]; lost <- c0[!(k0 %in% kb)] +cat(sprintf("RF = %d (%d clades gained, %d lost)\n", + length(gained) + length(lost), length(gained), length(lost))) +cat("\n-- small side of each GAINED clade (the rearrangement TNT made that we lack) --\n") +involved <- character(0) +for (g in gained) { sm <- small(g); involved <- union(involved, sm) + cat(sprintf(" [%2d tips] %s\n", length(sm), paste(sm, collapse = ", "))) } +cat(sprintf("\nUNION of tips in gained clades: %d of %d total\n", length(involved), length(labs))) +# smallest clade of T0 that CONTAINS all involved tips (the sector that must be picked) +contain_sz <- vapply(c0, function(s) { sd <- small(s) + if (all(involved %in% sd)) length(sd) else .Machine$integer.max }, integer(1)) +cat(sprintf("Smallest T0 clade containing all moved tips: %d tips (sector must cover this)\n", + min(contain_sz))) +cat("\nT0 newick:\n"); cat(ape::write.tree(ape::ladderize(t0)), "\n") +cat("\nbest newick:\n"); cat(ape::write.tree(ape::ladderize(best)), "\n") diff --git a/dev/benchmarks/diag_nhalf_sectors.R b/dev/benchmarks/diag_nhalf_sectors.R new file mode 100644 index 000000000..9ce79aad6 --- /dev/null +++ b/dev/benchmarks/diag_nhalf_sectors.R @@ -0,0 +1,48 @@ +# CHEAP-WIN TEST: does ANCHORED rss at ~n/2 sector size + RAS restarts reach the +# sectsch target from T0 -- no floating/reinsert, pure selection+rasStarts tuning? +# ~n/2 is where free560). If anchored alone closes most of the gap -> cheap win +# (no kernel surgery); residual -> floating reinsert. Shared start (TNT mult T0). +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Wortley2006 Zhu2013 Giles2015")), "\\s+")[[1]] +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) + +get_t0 <- function(phy) { + wd <- file.path(tempdir(), paste0("nh", Sys.getpid())) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + writeLines(c("mxram 1024;", "proc data.tnt;", "hold 100;", "rseed 1;", + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", "quit;"), + file.path(wd, "nhtest.run")) + old <- setwd(wd); on.exit(setwd(old)) + invisible(suppressWarnings(system2(TNT, args = "nhtest.run;", stdout = TRUE, stderr = TRUE))) + t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + t0 +} +rss_from <- function(phy, t0, ras, lo, hi) { + set.seed(1) + r <- suppressWarnings(MaximizeParsimony(phy, tree = t0, maxReplicates = 1L, nThreads = 1L, + maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, + cssRounds = 0L, rssRounds = 8L, rasStarts = as.integer(ras), wagnerStarts = 1L, + sectorMinSize = as.integer(lo), sectorMaxSize = as.integer(hi), fuseInterval = 9999L)) + min(as.double(attr(r, "score"))) +} +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]); n <- NTip(phy) + lo <- max(6L, as.integer(round(n * 0.35))); hi <- as.integer(round(n * 0.55)) + t0 <- get_t0(phy); t0len <- TreeLength(t0, phy); tgt <- target[[nm]] + cat(sprintf("\n==== %s (%dt, ~n/2 sector %d-%d) | T0=%.0f target=%d ====\n", + nm, n, lo, hi, t0len, tgt)) + for (ras in c(3L, 10L, 20L)) { + sc <- rss_from(phy, t0, ras, lo, hi) + cat(sprintf(" anchored rss rasStarts=%-2d -> %.0f (%+.0f vs T0, %+.0f vs target)%s\n", + ras, sc, sc - t0len, sc - tgt, if (sc <= tgt) " <== REACHED" else "")) + } +} diff --git a/dev/benchmarks/diag_plateau.R b/dev/benchmarks/diag_plateau.R new file mode 100644 index 000000000..e42e7b2cf --- /dev/null +++ b/dev/benchmarks/diag_plateau.R @@ -0,0 +1,59 @@ +# PLATEAU EXPERIMENT (oracle, mechanism check -- NOT the gate). Does accepting +# equal-length RAS-rebuild alternatives in the sector search (sectorAcceptEqual + +# rasStarts>1) let our strict-descent sectorial escape the TBR-local optimum T0? +# Per advisor: Wortley-from-T0 dropping ANY amount below 487 = positive direction +# (Wortley already ties end-to-end). The honest gate is end-to-end Zanol < 1265, +# tested separately. SCORE is the signal (candidates_evaluated is untrustworthy). +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Wortley2006")), "\\s+")[[1]] +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), paste0("plateau", Sys.getpid())) +unlink(wd, recursive = TRUE); dir.create(wd, showWarnings = FALSE, recursive = TRUE) +get_t0 <- function(phy, seed = 1) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", 8), "quit;") + writeLines(script, file.path(wd, "plttest.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "plttest.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + list(t0 = t0, tnt = if (length(s_sect)) s_sect[length(s_sect)] else NA) +} +run <- function(d, tree, ras, ae) { + set.seed(1) + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, + wagnerStarts = 1L, fuseInterval = 9999L, rssRounds = 8L, + rasStarts = as.integer(ras), sectorAcceptEqual = ae)) + as.double(attr(r, "score")) +} +arms <- list( + c(1, FALSE), # base: default polish, strict descent (control) + c(1, TRUE), # ae only, no rebuild -> change is inert (expect == base) + c(3, FALSE), # rebuild, discard equal (old behaviour, expect == base) + c(3, TRUE), # rebuild + keep equal alternative <-- THE TEST + c(10, TRUE)) # more rebuild starts -> more plateau escape routes +lab <- c("ras1/ae0", "ras1/ae1", "ras3/ae0", "ras3/ae1", "ras10/ae1") +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + tn <- get_t0(phy) + start <- TreeLength(tn$t0, phy) + cat(sprintf("\n==== %s | T0=%.0f TNT_sect=%.0f ====\n", nm, start, tn$tnt)) + for (i in seq_along(arms)) { + sc <- run(phy, tn$t0, arms[[i]][1], as.logical(arms[[i]][2])) + cat(sprintf(" %-9s score=%.0f (%+.0f vs T0)%s\n", lab[i], sc, sc - start, + ifelse(sc < start, " <-- ESCAPED", ""))) + } +} diff --git a/dev/benchmarks/diag_quality_ceiling.R b/dev/benchmarks/diag_quality_ceiling.R new file mode 100644 index 000000000..59260e202 --- /dev/null +++ b/dev/benchmarks/diag_quality_ceiling.R @@ -0,0 +1,56 @@ +# QUALITY CEILING: can TreeSearch reach TNT full-xmult's score at all? +# +# headtohead_phase0.csv used strategy="auto" (=thorough for 65-119 tips, +# default for <65) and TS finished 1-4 steps WORSE than TNT xmult: +# Wortley 483 vs 479 | Zanol 1264 vs 1261 | Zhu 626 vs 624 | Giles 671 vs 670 +# But "auto" never tries the strongest preset. This asks: given the STRONGEST +# TS config (thorough -> intensive -> intensive+intraFuse) and generous time, +# does TS REACH the TNT target, or is it a hard ceiling? +# REACHED => gap is preset/tuning/speed (recommend intensive; tune auto). +# CEILING => real search-power deficit (need a better component/algorithm). +# nThreads=1 to stay comparable to the headtohead baseline that produced 483/1264. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } + +secs <- as.numeric(Sys.getenv("TS_SECONDS", "60")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Wortley2006")), "\\s+")[[1]] +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2 3 4 5")), "\\s+")[[1]]) +target <- c(Wortley2006 = 479, Eklund2004 = 440, Zanol2014 = 1261, + Zhu2013 = 624, Giles2015 = 670, Dikow2009 = 1606) + +run_arm <- function(phy, seed, secs, extra) { + set.seed(seed) + args <- c(list(dataset = phy, maxReplicates = 9999L, maxSeconds = secs, + nThreads = 1L, verbosity = 0L), extra) + r <- tryCatch(suppressWarnings(do.call(MaximizeParsimony, args)), + error = function(e) { message(" ARM ERROR: ", conditionMessage(e)); NULL }) + if (is.null(r)) return(NA_real_) + min(as.double(attr(r, "score"))) +} + +arms <- list( + intensive = list(strategy = "intensive"), + plateau = list(strategy = "intensive", rasStarts = 3L, sectorAcceptEqual = TRUE), + plateauFuse = list(strategy = "intensive", rasStarts = 3L, sectorAcceptEqual = TRUE, + intraFuse = TRUE) +) + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + tgt <- target[[nm]] + cat(sprintf("\n==== %s (%d tips) | TNT xmult target=%d | %gs x %d seeds, nThreads=1 ====\n", + nm, NTip(phy), tgt, secs, length(seeds))) + for (an in names(arms)) { + sc <- vapply(seeds, function(s) run_arm(phy, s, secs, arms[[an]]), numeric(1)) + best <- suppressWarnings(min(sc, na.rm = TRUE)) + cat(sprintf(" %-14s best=%-5.0f median=%-6.1f all={%s} gap_best=%+.0f%s\n", + an, best, median(sc, na.rm = TRUE), + paste(sprintf("%.0f", sc), collapse = ","), + best - tgt, if (is.finite(best) && best <= tgt) " <== REACHED" else "")) + } +} diff --git a/dev/benchmarks/diag_reinsert_scan.R b/dev/benchmarks/diag_reinsert_scan.R new file mode 100644 index 000000000..a94c5b61f --- /dev/null +++ b/dev/benchmarks/diag_reinsert_scan.R @@ -0,0 +1,50 @@ +# Per-edge exactness probe: clip one tip from a fixed tree, and for every +# reattachment edge compare the union-of-finals formula cost and the directional +# intersect-else-union cost against the TRUE full-rescore cost. +suppressMessages({ + library(TreeSearch, lib.loc = "C:/Users/pjjg18/GitHub/TS-selectem/.agent-selectem") + library(TreeTools) +}) +nm <- Sys.getenv("DS", "Zanol2014") +phy <- readRDS(sprintf("dev/benchmarks/t0/%s.phy.rds", nm)) +n <- length(phy) + +set.seed(11) +tr <- AdditionTree(phy) # any valid full tree +phyO <- phy[tr$tip.label] # data in the tree's tip order +at <- attributes(phyO) +contrast <- at$contrast +tipData <- matrix(unlist(phyO, use.names = FALSE), nrow = n, byrow = TRUE) +weight <- TreeSearch:::.ScaleWeight(at$weight); levels <- at$levels + +# --- small partial tree (12 taxa): is dir exact when sets are ambiguous? --- +{ + small <- names(phy)[1:12] + phyS <- phy[small]; nS <- length(small) + trS <- AdditionTree(phyS) + phySO <- phyS[trS$tip.label]; atS <- attributes(phySO) + tdS <- matrix(unlist(phySO, use.names = FALSE), nrow = nS, byrow = TRUE) + rS <- TreeSearch:::ts_reinsert_scan(trS$edge, atS$contrast, tdS, + TreeSearch:::.ScaleWeight(atS$weight), atS$levels, 4L) + cat(sprintf("-- SMALL 12-taxon tree, clip tip 4: union==actual %d/%d, dir==actual %d/%d --\n", + sum(rS$union_extra == rS$actual_extra), length(rS$actual_extra), + sum(rS$dir_extra == rS$actual_extra), length(rS$actual_extra))) +} + +for (clip in c(3L, 12L, 40L)) { + r <- TreeSearch:::ts_reinsert_scan(tr$edge, contrast, tipData, weight, levels, clip) + un <- r$union_extra; di <- r$dir_extra; ac <- r$actual_extra + cat(sprintf("\n-- clip tip %d (%s), main_score=%d, %d edges --\n", + clip, tr$tip.label[clip], r$main_score, length(ac))) + cat(sprintf(" union==actual: %d/%d | dir==actual: %d/%d\n", + sum(un == ac), length(ac), sum(di == ac), length(ac))) + cat(sprintf(" min actual=%d | union picks edge w/ actual=%d | dir picks edge w/ actual=%d\n", + min(ac), ac[which.min(un)], ac[which.min(di)])) + # show a few rows where they disagree with truth + bad <- which(un != ac | di != ac) + if (length(bad)) { + show <- head(bad, 6) + for (i in show) cat(sprintf(" edge(%d,%d): union=%d dir=%d actual=%d\n", + r$above[i], r$below[i], un[i], di[i], ac[i])) + } +} diff --git a/dev/benchmarks/diag_rss_rasstarts.R b/dev/benchmarks/diag_rss_rasstarts.R new file mode 100644 index 000000000..b591fd1cf --- /dev/null +++ b/dev/benchmarks/diag_rss_rasstarts.R @@ -0,0 +1,46 @@ +# SECTORIAL LEG, cheap precursor (D2): does our FROZEN rss from TNT's T0 reach the +# sectsch target if we just raise rasStarts (TNT does R=3 + r=3)? rss-only, from +# the identical TNT mult T0, bounded work (rssRounds fixed), nThreads=1. +# reaches target -> D2 (cheap, no kernel surgery) +# stuck at ~T0 -> frozen rebuild is null; D1 (floating HTU) is the real lever +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014")), "\\s+")[[1]] +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) + +get_t0 <- function(phy, seed = 1) { + wd <- file.path(tempdir(), paste0("rrt0", Sys.getpid())) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + writeLines(c("mxram 1024;", "proc data.tnt;", "hold 100;", sprintf("rseed %d;", seed), + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", "quit;"), + file.path(wd, "rttest.run")) + old <- setwd(wd); on.exit(setwd(old)) + invisible(suppressWarnings(system2(TNT, args = "rttest.run;", stdout = TRUE, stderr = TRUE))) + t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + t0 +} +rss_from <- function(phy, t0, ras) { + set.seed(1) + r <- suppressWarnings(MaximizeParsimony(phy, tree = t0, maxReplicates = 1L, + nThreads = 1L, maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, + driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, rssRounds = 8L, + rasStarts = as.integer(ras), wagnerStarts = 1L, fuseInterval = 9999L)) + min(as.double(attr(r, "score"))) +} +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + t0 <- get_t0(phy); t0len <- TreeLength(t0, phy); tgt <- target[[nm]] + cat(sprintf("\n==== %s | TNT mult T0=%.0f | sectsch target=%d ====\n", nm, t0len, tgt)) + for (ras in c(1L, 3L, 6L)) { + sc <- rss_from(phy, t0, ras) + cat(sprintf(" rss rasStarts=%d -> %.0f (%+.0f vs T0, %+.0f vs target)%s\n", + ras, sc, sc - t0len, sc - tgt, if (sc <= tgt) " <== REACHED" else "")) + } +} diff --git a/dev/benchmarks/diag_sect_engage.R b/dev/benchmarks/diag_sect_engage.R new file mode 100644 index 000000000..f7bdd6f6b --- /dev/null +++ b/dev/benchmarks/diag_sect_engage.R @@ -0,0 +1,54 @@ +# ENGAGEMENT TEST (advisor steps 1-2): is our sectorial search actually executing, +# and does the rssRounds flag engage? The harness has lied twice today (c.run bug; +# acceptequal==greedy), so the "five nulls" are suspect. From the identical TNT T0 +# (a TBR-local optimum), run with sectorial OFF (rssRounds=0) vs ON (rssRounds=8), +# all else fixed, and compare BOTH score and candidates_evaluated. +# dCand > 0 => sectorial is doing work (executing) +# dCand ~ 0 => sectorial is NOT running (gated off / dead wiring) -- that's the bug +# dScore < 0 => sectorial escapes the local optimum (runs AND helps) +# dScore = 0 => executes but finds nothing (quality/selection/reduction bug) +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Wortley2006 Zanol2014")), "\\s+")[[1]] +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), paste0("engage", Sys.getpid())) +unlink(wd, recursive = TRUE); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +get_t0 <- function(phy, seed = 1) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", 8), "quit;") + writeLines(script, file.path(wd, "engtest.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "engtest.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + list(t0 = t0, tnt = if (length(s_sect)) s_sect[length(s_sect)] else NA) +} +run <- function(d, tree, rss) { + set.seed(1) + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, + wagnerStarts = 1L, fuseInterval = 9999L, rssRounds = as.integer(rss))) + list(score = as.double(attr(r, "score")), cand = as.double(attr(r, "candidates_evaluated"))) +} +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + tn <- get_t0(phy) + start <- TreeLength(tn$t0, phy) + a0 <- run(phy, tn$t0, 0L) + aK <- run(phy, tn$t0, 8L) + cat(sprintf("%-11s | start=%.0f TNT_sect=%.0f | rss0: score=%.0f cand=%.0f | rss8: score=%.0f cand=%.0f | dScore=%+.0f dCand=%+.0f\n", + nm, start, tn$tnt, a0$score, a0$cand, aK$score, aK$cand, + aK$score - a0$score, aK$cand - a0$cand)) +} diff --git a/dev/benchmarks/diag_sect_levers.R b/dev/benchmarks/diag_sect_levers.R new file mode 100644 index 000000000..417f914f1 --- /dev/null +++ b/dev/benchmarks/diag_sect_levers.R @@ -0,0 +1,70 @@ +# LEVER VERIFICATION (advisor steps 2-3): re-run every sector lever on the now- +# trustworthy harness, reporting BOTH score and candidates_evaluated so we can see +# which flags actually ENGAGE (dCand != 0) vs which are dead wiring (dCand ~ 0). +# Shared start = identical TNT T0 (a TBR-local optimum). Leading hypothesis: our +# default POLISHES the sector (TBR, already stuck); Goloboff's RSS REBUILDS it +# (RAS+TBR, rasStarts>1). If rebuild engages AND drops the score toward TNT, that +# is the missing mechanism. If a lever changes nothing observable, it was never +# tested -- fix the wiring. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-ratchet"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Wortley2006")), "\\s+")[[1]] +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), paste0("levers", Sys.getpid())) +unlink(wd, recursive = TRUE); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +get_t0 <- function(phy, seed = 1) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", 8), "quit;") + writeLines(script, file.path(wd, "levtest.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "levtest.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + s_sect <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + list(t0 = t0, tnt = if (length(s_sect)) s_sect[length(s_sect)] else NA) +} +run <- function(d, tree, rss, ras, ae, mh, ct) { + set.seed(1) + r <- suppressWarnings(MaximizeParsimony(d, tree = tree, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, + wagnerStarts = 1L, fuseInterval = 9999L, rssRounds = as.integer(rss), + rasStarts = as.integer(ras), sectorAcceptEqual = ae, + sectorMaxHits = as.integer(mh), sectorCollapseTarget = as.integer(ct))) + list(score = as.double(attr(r, "score")), cand = as.double(attr(r, "candidates_evaluated"))) +} +levers <- list( + base = list(rss = 8, ras = 1, ae = FALSE, mh = 1, ct = 0), + ras3 = list(rss = 8, ras = 3, ae = FALSE, mh = 1, ct = 0), + ras10 = list(rss = 8, ras = 10, ae = FALSE, mh = 1, ct = 0), + ae = list(rss = 8, ras = 1, ae = TRUE, mh = 1, ct = 0), + mh20 = list(rss = 8, ras = 1, ae = FALSE, mh = 20, ct = 0), + ct10 = list(rss = 8, ras = 1, ae = FALSE, mh = 1, ct = 10), + all = list(rss = 8, ras = 10, ae = TRUE, mh = 20, ct = 10)) + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + tn <- get_t0(phy) + start <- TreeLength(tn$t0, phy) + cat(sprintf("\n==== %s | start(T0)=%.0f TNT_sect=%.0f (gap to beat = %+.0f) ====\n", + nm, start, tn$tnt, tn$tnt - start)) + b <- run(phy, tn$t0, 8, 1, FALSE, 1, 0) + cat(sprintf(" %-7s score=%.0f cand=%.0f\n", "base", b$score, b$cand)) + for (lv in names(levers)[-1]) { + p <- levers[[lv]] + r <- run(phy, tn$t0, p$rss, p$ras, p$ae, p$mh, p$ct) + cat(sprintf(" %-7s score=%.0f cand=%.0f | dScore=%+.0f dCand=%+.0f %s\n", + lv, r$score, r$cand, r$score - b$score, r$cand - b$cand, + ifelse(abs(r$cand - b$cand) < 1, "<-- DEAD (no engage)", ""))) + } +} diff --git a/dev/benchmarks/diag_sector_shape.R b/dev/benchmarks/diag_sector_shape.R new file mode 100644 index 000000000..2e3bb8b76 --- /dev/null +++ b/dev/benchmarks/diag_sector_shape.R @@ -0,0 +1,129 @@ +# Diagnostic: characterise the FIRST score-improving TNT sectorial move, to +# decide WHICH fix our sectorial needs (advisor's 4-way discrimination): +# - non-clade band -> full multi-stub reduced-dataset rewrite +# - clade OUTSIDE size band -> just widen sector selection (trivial) +# - clade in-band, ATTACHMENT-only change -> b=1 floating-HTU (deferred piece) +# - clade in-band, INTERNAL change -> fix RAS diversity/acceptance +# +# Method (advisor): isolate ONE operation. Run TNT `mult` -> T0, then K single +# `sectsch=rss` passes, saving the tree after EACH pass. Diff the FIRST pass that +# drops the score (A = pre, B = post). K=8 cumulative would look non-clade. +# +# Env: TS_LIB, TNT_EXE, TS_DATASETS, TS_SEEDS, TS_KPASS. + +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-p0"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 3")), "\\s+")[[1]]) +K <- as.integer(Sys.getenv("TS_KPASS", "8")) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Wortley2006 Zhu2013")), + "\\s+")[[1]] +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) as.double(gsub(",", "", x)) +wd <- file.path(tempdir(), "sectshape"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +# Canonical key per split: sorted tip labels of the side not containing tip[1] +# (size tie broken lexicographically). RF == 0 <=> equal key sets. +split_keys <- function(tree) { + sp <- as.Splits(tree) + m <- as.logical(sp) # n_split x n_tip (TRUE = in split) + if (is.null(dim(m))) m <- matrix(m, nrow = 1) + labs <- TipLabels(tree) + apply(m, 1, function(r) { + a <- sort(labs[r]); b <- sort(labs[!r]) + side <- if (length(a) != length(b)) { + if (length(a) < length(b)) a else b + } else if (paste(a, collapse = ",") < paste(b, collapse = ",")) a else b + paste(side, collapse = "|") + }) +} +same_tree <- function(a, b) setequal(split_keys(a), split_keys(b)) + +# Descendant tip labels of every internal node (the rooted clades of `tree`). +clade_tipsets <- function(tree) { + nt <- NTip(tree); labs <- TipLabels(tree) + desc <- phangorn::Descendants(tree, (nt + 1):(nt + tree$Nnode), type = "tips") + lapply(desc, function(ix) labs[ix]) +} + +run_tnt_perpass <- function(phy, seed, kpass) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + saves <- character(0) + for (i in seq_len(kpass)) + saves <- c(saves, "sectsch=rss;", sprintf("tsave *p%d.tre;", i), "save;", "tsave/;") + script <- c("mxram 1024;", "proc data.tnt;", "hold 1;", sprintf("rseed %d;", seed), + "taxname=;", "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + saves, "quit;") + writeLines(script, file.path(wd, "sharedstart.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "sharedstart.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + pass_scores <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + rd <- function(f) { t <- tryCatch(ReadTntTree(file.path(wd, f)), error = function(e) NULL) + if (inherits(t, "multiPhylo")) t[[1]] else t } + list(t0 = rd("t0.tre"), + trees = lapply(seq_len(kpass), function(i) rd(sprintf("p%d.tre", i))), + pass_scores = pass_scores) +} + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + n <- length(phy); smin <- round(n * 0.35); smax <- round(n * 0.65) + for (sd in seeds) { + cat(sprintf("\n========== %s seed %d (n=%d, eligible clade band [%d,%d]) ==========\n", + nm, sd, n, smin, smax)) + r <- run_tnt_perpass(phy, sd, K) + if (is.null(r$t0)) { cat(" no T0\n"); next } + s0 <- TreeLength(r$t0, phy) + scores <- c(s0, r$pass_scores) + cat(sprintf(" scores by pass: %s\n", paste(scores, collapse = " -> "))) + # First pass that strictly drops the score + imp <- which(diff(scores) < 0) + if (!length(imp)) { cat(" no improving pass (sectorial found nothing)\n"); next } + i <- imp[1] # 1-based pass index in r$trees + A <- if (i == 1) r$t0 else r$trees[[i - 1]] + B <- r$trees[[i]] + if (is.null(A) || is.null(B)) { cat(" missing tree for pass ", i, "\n"); next } + dropA <- scores[i] - scores[i + 1] + cat(sprintf(" FIRST improving pass = %d : %g -> %g (drop %g)\n", + i, scores[i], scores[i + 1], dropA)) + # RF (count of differing splits) + kA <- split_keys(A); kB <- split_keys(B) + rf <- length(setdiff(kA, kB)) + length(setdiff(kB, kA)) + cat(sprintf(" RF(A,B) = %d differing splits\n", rf)) + if (same_tree(A, B)) { cat(" (trees identical - score drop without topology change?!)\n"); next } + # Single-SPR moved-set test: smallest clade C of A whose removal makes A,B match + csets <- clade_tipsets(A) + csets <- csets[order(lengths(csets))] + moved <- NULL + for (C in csets) { + if (length(C) < 2 || length(C) > n - 2) next + Am <- tryCatch(KeepTip(A, setdiff(TipLabels(A), C)), error = function(e) NULL) + Bm <- tryCatch(KeepTip(B, setdiff(TipLabels(B), C)), error = function(e) NULL) + if (!is.null(Am) && !is.null(Bm) && same_tree(Am, Bm)) { moved <- C; break } + } + if (is.null(moved)) { + cat(" NOT a single clade-SPR: no clade-removal makes A==B.\n") + cat(" => either a non-clade band, a TBR (re-rooted regraft), or multiple moves.\n") + cat(sprintf(" lost splits (in A, |smaller side|): %s\n", + paste(sort(sapply(setdiff(kA, kB), function(k) length(strsplit(k,"\\|")[[1]]))), collapse=","))) + cat(sprintf(" gained splits (in B, |smaller side|): %s\n", + paste(sort(sapply(setdiff(kB, kA), function(k) length(strsplit(k,"\\|")[[1]]))), collapse=","))) + } else { + sz <- length(moved); inband <- sz >= smin && sz <= smax + # Attachment-only vs internal change: compare induced topology on moved set + indA <- KeepTip(A, moved); indB <- KeepTip(B, moved) + attach_only <- if (sz >= 4) same_tree(indA, indB) else TRUE + cat(sprintf(" SINGLE clade-SPR. moved clade size = %d (in-band [%d,%d]? %s)\n", + sz, smin, smax, inband)) + cat(sprintf(" moved-clade internal topology: %s\n", + if (attach_only) "UNCHANGED => ATTACHMENT/ROOTING-only (b=1 floating-HTU fix)" + else "CHANGED => internal rearrangement (RAS diversity/acceptance)")) + } + } +} diff --git a/dev/benchmarks/diag_sectras_dirfix.R b/dev/benchmarks/diag_sectras_dirfix.R new file mode 100644 index 000000000..5fadc2415 --- /dev/null +++ b/dev/benchmarks/diag_sectras_dirfix.R @@ -0,0 +1,40 @@ +# Before/after for the build_ras_sector directional-edge fix (task #27). +# build_ras_sector (the sector-internal RAS rebuild) fires only when rasStarts>=2, +# so the fix is inert at the default rasStarts=1 (which must therefore be IDENTICAL +# between libs -- a built-in sanity check) and shows only at rasStarts>=2. +# +# rss-only from a shared in-R Wagner start (same on both libs: the build_ras_sector +# change does not touch wagner_tree), seeds aggregated by MaximizeParsimony's own +# multi-start. Run twice: +# TS_LIB=.agent-wagsect Rscript ... > before +# TS_LIB=.agent-sectfix Rscript ... > after +# Directional (.agent-sectfix) should be EQUAL-OR-BETTER at rasStarts>=2. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-sectfix"), + winslash = "/")) + library(TreeTools) +}) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +phy <- fitch(inapplicable.phyData[["Zanol2014"]]) +target <- 1261 + +# Shared deterministic Wagner start (identical across libs). +set.seed(7); t0 <- AdditionTree(phy, sequence = sample(seq_along(phy))) +t0len <- TreeLength(t0, phy) +cat(sprintf("lib=%s | Zanol2014 | Wagner T0=%.0f | target=%d\n", + Sys.getenv("TS_LIB", ".agent-sectfix"), t0len, target)) + +rss_from <- function(ras) { + set.seed(1) + r <- suppressWarnings(MaximizeParsimony(phy, tree = t0, maxReplicates = 1L, + nThreads = 1L, maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, + driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, rssRounds = 8L, + rasStarts = as.integer(ras), wagnerStarts = 1L, fuseInterval = 9999L)) + min(as.double(attr(r, "score"))) +} +for (ras in c(1L, 3L, 6L)) { + sc <- rss_from(ras) + cat(sprintf(" rss rasStarts=%d -> %.0f (%+.0f vs T0, %+.0f vs target)\n", + ras, sc, sc - t0len, sc - target)) +} diff --git a/dev/benchmarks/diag_sectras_sweep.R b/dev/benchmarks/diag_sectras_sweep.R new file mode 100644 index 000000000..99213dc54 --- /dev/null +++ b/dev/benchmarks/diag_sectras_sweep.R @@ -0,0 +1,37 @@ +# How far does raising rasStarts close the sectorial gap, and at what cost? +# rss-only from a shared in-R Wagner T0, per dataset, rasStarts in {1,3,6}, with +# wall-clock. Scores are bitness-independent (vs the hardcoded TNT targets); +# relative timing shows the rasStarts cost multiplier. Informs whether a +# TNT-faithful rasStarts (TNT uses 3) should be the sectorial default/preset. +# Env: TS_LIB (default .agent-sectfix), TS_DATASETS. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-sectfix"), + winslash = "/")) + library(TreeTools) +}) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Zhu2013 Wortley2006")), "\\s+")[[1]] + +rss_from <- function(phy, t0, ras) { + set.seed(1) + t <- system.time(r <- suppressWarnings(MaximizeParsimony(phy, tree = t0, + maxReplicates = 1L, nThreads = 1L, maxSeconds = 0, verbosity = 0L, + ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, + rssRounds = 8L, rasStarts = as.integer(ras), wagnerStarts = 1L, + fuseInterval = 9999L))) + list(score = min(as.double(attr(r, "score"))), secs = as.double(t["elapsed"])) +} + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + set.seed(7); t0 <- AdditionTree(phy, sequence = sample(seq_along(phy))) + t0len <- TreeLength(t0, phy); tgt <- target[[nm]] + cat(sprintf("\n==== %s | Wagner T0=%.0f | target=%d ====\n", nm, t0len, tgt)) + for (ras in c(1L, 3L, 6L)) { + o <- rss_from(phy, t0, ras) + cat(sprintf(" rasStarts=%d -> %.0f (%+.0f vs target) [%.1fs]\n", + ras, o$score, o$score - tgt, o$secs)) + } +} diff --git a/dev/benchmarks/diag_sectras_timematched.R b/dev/benchmarks/diag_sectras_timematched.R new file mode 100644 index 000000000..b4e59d276 --- /dev/null +++ b/dev/benchmarks/diag_sectras_timematched.R @@ -0,0 +1,39 @@ +# TIME-MATCHED rss: rasStarts=1 (many shallow rounds) vs 3 (fewer deeper rounds) +# under an IDENTICAL wall-clock budget. The unbounded sweep (diag_sectras_sweep.R) +# showed ras=3 reaches +1 vs ras=1's +7/+8 when rss runs to completion -- but ras=3 +# costs ~3-5x/sector, so the real question for a preset change is whether it still +# wins when TIME is the constraint. rssRounds set high so maxSeconds is the bound. +# Local wall-clock is only indicative (Hamilton is authoritative); the score +# comparison at matched budget is the signal. Env: TS_LIB, TS_SECONDS, TS_SEEDS. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-sectfix"), + winslash = "/")) + library(TreeTools) +}) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +target <- c(Zanol2014 = 1261, Zhu2013 = 624) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Zhu2013")), "\\s+")[[1]] +secs <- as.integer(Sys.getenv("TS_SECONDS", "30")) +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2")), "\\s+")[[1]]) + +rss_timed <- function(phy, t0, ras, seed) { + set.seed(seed) + r <- suppressWarnings(MaximizeParsimony(phy, tree = t0, maxReplicates = 1L, + nThreads = 1L, maxSeconds = secs, verbosity = 0L, ratchetCycles = 0L, + driftCycles = 0L, xssRounds = 0L, cssRounds = 0L, rssRounds = 50L, + rasStarts = as.integer(ras), wagnerStarts = 1L, fuseInterval = 9999L)) + min(as.double(attr(r, "score"))) +} +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + set.seed(7); t0 <- AdditionTree(phy, sequence = sample(seq_along(phy))) + tgt <- target[[nm]] + cat(sprintf("\n==== %s | target=%d | budget=%ds ====\n", nm, tgt, secs)) + for (ras in c(1L, 3L)) { + sc <- vapply(seeds, function(s) rss_timed(phy, t0, ras, s), double(1)) + cat(sprintf(" rasStarts=%d -> scores {%s} median %+.0f vs target\n", + ras, paste(sprintf("%.0f", sc), collapse = ","), + median(sc) - tgt)) + } +} diff --git a/dev/benchmarks/diag_sectsch_sweep.R b/dev/benchmarks/diag_sectsch_sweep.R new file mode 100644 index 000000000..6127590a9 --- /dev/null +++ b/dev/benchmarks/diag_sectsch_sweep.R @@ -0,0 +1,60 @@ +# SECTSCH LEVER SWEEP (parallel exploration of the OTHER sectsch differences). +# Shared-start: from TNT's mult T0, run our sectorial-ONLY (ratchet/drift OFF) under +# each lever config; report score reached vs the sectsch target. Score-based + +# bounded (rssRounds fixed) => robust to CPU contention, deterministic. Isolates +# which param-exposed sectsch lever (size, RAS restarts, accept-equal, max-hits) +# moves us toward TNT's sectsch endpoint, before any kernel work. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Wortley2006 Zhu2013 Giles2015")), "\\s+")[[1]] +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) # TNT sectsch endpoints + +get_t0 <- function(phy, seed = 1) { + wd <- file.path(tempdir(), paste0("swt0", Sys.getpid(), substr(deparse(substitute(phy)),1,3))) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + writeLines(c("mxram 1024;", "proc data.tnt;", "hold 100;", sprintf("rseed %d;", seed), + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", "quit;"), + file.path(wd, "swtest.run")) + old <- setwd(wd); on.exit(setwd(old)) + invisible(suppressWarnings(system2(TNT, args = "swtest.run;", stdout = TRUE, stderr = TRUE))) + t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + t0 +} +run_sect <- function(phy, t0, cfg) { + set.seed(1) + base <- list(dataset = phy, tree = t0, maxReplicates = 1L, nThreads = 1L, + maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, driftCycles = 0L, + xssRounds = 0L, cssRounds = 0L, rssRounds = 8L, wagnerStarts = 1L, + fuseInterval = 9999L) + args <- modifyList(base, cfg) + r <- tryCatch(suppressWarnings(do.call(MaximizeParsimony, args)), + error = function(e) { message("ERR ", conditionMessage(e)); NULL }) + if (is.null(r)) return(NA_real_) + min(as.double(attr(r, "score"))) +} +cfgs <- list( + baseline = list(), + ras3 = list(rasStarts = 3L), + ras6 = list(rasStarts = 6L), + bigSectors = list(sectorMinSize = 30L, sectorMaxSize = 45L), + acceptEq = list(sectorAcceptEqual = TRUE, sectorMaxHits = 10L), + tntFaithful = list(rasStarts = 3L, sectorMinSize = 30L, sectorMaxSize = 45L, + sectorAcceptEqual = TRUE, sectorMaxHits = 10L) +) +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + t0 <- get_t0(phy); t0len <- TreeLength(t0, phy); tgt <- target[[nm]] + cat(sprintf("\n==== %s | TNT mult T0=%.0f | sectsch target=%d ====\n", nm, t0len, tgt)) + for (cn in names(cfgs)) { + sc <- run_sect(phy, t0, cfgs[[cn]]) + cat(sprintf(" %-12s -> %.0f (%+.0f vs T0, %+.0f vs target)%s\n", + cn, sc, sc - t0len, sc - tgt, if (is.finite(sc) && sc <= tgt) " <== REACHED" else "")) + } +} diff --git a/dev/benchmarks/diag_shared_start_truth.R b/dev/benchmarks/diag_shared_start_truth.R new file mode 100644 index 000000000..ccce78287 --- /dev/null +++ b/dev/benchmarks/diag_shared_start_truth.R @@ -0,0 +1,51 @@ +# DISPOSITIVE shared-start test (advisor): does TNT sectsch reach the target from +# the EXACT T0 our sectorial uses? Prior "TNT sectsch -> 1261" came from sectsch +# running on TNT's in-memory mult tree, NEVER verified == the t0.tre we fed our +# sectorial -> possible apples-to-oranges. Here ONE TNT run: mult builds A, saves +# A to t0.tre, runs sectsch FROM A; our sectorial reads the SAME t0.tre. Both +# share A by construction. MAPPING CHECK: TreeLength(read t0.tre) must be sane +# (~ the mult score); garbage => ReadTntTree permuted taxa, result invalid. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) suppressWarnings(as.double(gsub(",", "", x))) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Wortley2006 Zhu2013 Giles2015")), "\\s+")[[1]] +target <- c(Wortley2006 = 479, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + wd <- file.path(tempdir(), paste0("sst", Sys.getpid(), nm)) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + writeLines(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1000;", + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + rep("sectsch=rss;", 8), "quit;"), file.path(wd, "ssttest.run")) + old <- setwd(wd) + out <- suppressWarnings(system2(TNT, args = "ssttest.run;", stdout = TRUE, stderr = TRUE)) + setwd(old) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + sect_vals <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + t0 <- ReadTntTree(file.path(wd, "t0.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] + A_len <- TreeLength(t0, phy) + set.seed(1) + r <- suppressWarnings(MaximizeParsimony(phy, tree = t0, maxReplicates = 1L, nThreads = 1L, + maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, driftCycles = 0L, xssRounds = 0L, + cssRounds = 0L, rssRounds = 8L, rasStarts = 3L, wagnerStarts = 1L, fuseInterval = 9999L)) + ours <- min(as.double(attr(r, "score"))) + tnt_sect <- if (length(sect_vals)) min(sect_vals, na.rm = TRUE) else NA + cat(sprintf("\n==== %s | target=%d ====\n", nm, target[[nm]])) + cat(sprintf(" TNT mult T0 (our ruler) A_len = %.0f [mapping sane? expect a real MP-ish score]\n", A_len)) + cat(sprintf(" TNT sectsch FROM A -> %s (escape %+.0f vs A)\n", + format(tnt_sect), if (is.finite(tnt_sect)) tnt_sect - A_len else NA)) + cat(sprintf(" OUR sectorial FROM A -> %.0f (escape %+.0f vs A)\n", ours, ours - A_len)) + cat(sprintf(" VERDICT: %s\n", + if (is.finite(tnt_sect) && tnt_sect < A_len - 0.5) + "TNT sectsch ESCAPES shared A -> real sectorial gap, trace mechanism" + else "TNT sectsch does NOT escape shared A -> 1261 was a different basin; hunt dissolves")) +} diff --git a/dev/benchmarks/diag_tbr_falseconv_check.R b/dev/benchmarks/diag_tbr_falseconv_check.R new file mode 100644 index 000000000..3f00f548a --- /dev/null +++ b/dev/benchmarks/diag_tbr_falseconv_check.R @@ -0,0 +1,53 @@ +# Is our default TBR's "convergence" genuine, on the SHIPPING cpp-search build +# (post directional-vroot fix, commit 2b299e4b)? We run TBR to convergence via +# ts_tbr_diagnostics, then enumerate the FULL unrooted canonical-TBR neighbourhood +# of the result with the SEPARATE, unoptimised enumerator TBRMoves (-> all_tbr in +# rearrange.cpp, a different code path). >0 improving neighbour => the kernel +# falsely declared convergence (the competent-chaum move-incompleteness finding). +# +# Result (Zanol2014, 2026-06-18): GOOD Wagner starts -> genuine optima (0 +# improving); POOR random starts -> strand at 1272 with only 1-9 improving (vs the +# chip's PRE-fix 40+), i.e. the vroot scoring fix recovered most of the gap and the +# residual move-skip bug is small. See dev/plans/2026-06-18-wagner-insertion-cost-bug.md. +# +# Env: TS_LIB (default .agent-wagsect). +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-wagsect"), + winslash = "/")) + library(TreeTools) +}) + +data("inapplicable.phyData", package = "TreeSearch") +fitchify <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +phy <- fitchify(inapplicable.phyData[["Zanol2014"]]) +at <- attributes(phy) +d <- list(phy = phy, contrast = at$contrast, + tip_data = matrix(unlist(phy, use.names = FALSE), nrow = length(phy), byrow = TRUE), + weight = at$weight, levels = at$levels, nTip = length(phy)) +norm <- function(tr) Preorder(RenumberTips(tr, names(d$phy))) + +# Run our default (rooted, optimised) TBR to convergence from a warm start. +tsTbr <- function(start, seed) { + set.seed(seed) + res <- TreeSearch:::ts_tbr_diagnostics(norm(start)[["edge"]], d$contrast, d$tip_data, + d$weight, d$levels, maxHits = 1L, acceptEqual = FALSE) + structure(list(edge = res$edge, Nnode = d$nTip - 1L, tip.label = names(d$phy)), + class = "phylo") +} + +probe <- function(label, start, seed) { + tr <- tsTbr(start, seed) + baseLen <- TreeLength(tr, d$phy) + ls <- vapply(TBRMoves(norm(tr)), TreeLength, double(1), d$phy) # full unrooted-TBR neighbourhood + cat(sprintf("%-12s start=%4.0f | TS-TBR converged=%.0f | enum %d nb, best=%.0f, %d IMPROVING\n", + label, TreeLength(norm(start), d$phy), baseLen, length(ls), min(ls), + sum(ls < baseLen - 0.5))) +} + +cat("--- GOOD starts (RAS Wagner, post-fix) ---\n") +for (s in 1:3) { set.seed(s); probe(sprintf("wagner s%d", s), + AdditionTree(d$phy, sequence = sample(seq_along(d$phy))), s) } + +cat("--- POOR starts (random topology, like the chip's) ---\n") +for (s in 1:3) { set.seed(1000 + s); probe(sprintf("random s%d", s), + RandomTree(d$phy, root = TRUE), s) } diff --git a/dev/benchmarks/diag_thorough_rasstarts_tm.R b/dev/benchmarks/diag_thorough_rasstarts_tm.R new file mode 100644 index 000000000..4c006f6a2 --- /dev/null +++ b/dev/benchmarks/diag_thorough_rasstarts_tm.R @@ -0,0 +1,35 @@ +# Full-search time-matched gate for rasStarts=3 in the AUTO-SELECTED `thorough` +# preset (task #29). Unlike the rss-only tests, this runs the WHOLE thorough +# pipeline (ratchet/drift/xss/css/rss/fuse interleaved) under a fixed wall-clock +# budget, varying ONLY rasStarts (explicit arg overrides the preset field; all +# other thorough fields unchanged). This is the decision test before flipping +# thorough's default. Local wall-clock is INDICATIVE; Hamilton is authoritative. +# Env: TS_LIB, TS_SECONDS, TS_SEEDS, TS_DATASETS. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-sectfix"), + winslash = "/")) + library(TreeTools) +}) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +target <- c(Zanol2014 = 1261, Zhu2013 = 624) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Zhu2013")), "\\s+")[[1]] +secs <- as.integer(Sys.getenv("TS_SECONDS", "60")) +seeds <- as.integer(strsplit(trimws(Sys.getenv("TS_SEEDS", "1 2")), "\\s+")[[1]]) + +run <- function(phy, ras, seed) { + set.seed(seed) + r <- suppressWarnings(MaximizeParsimony(phy, strategy = "thorough", + rasStarts = as.integer(ras), maxSeconds = secs, nThreads = 1L, + verbosity = 0L)) + min(as.double(attr(r, "score"))) +} +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]); tgt <- target[[nm]] + cat(sprintf("\n==== %s | target=%d | thorough | budget=%ds ====\n", nm, tgt, secs)) + for (ras in c(1L, 3L)) { + sc <- vapply(seeds, function(s) run(phy, ras, s), double(1)) + cat(sprintf(" rasStarts=%d -> {%s} median %+.0f vs target\n", + ras, paste(sprintf("%.0f", sc), collapse = ","), median(sc) - tgt)) + } +} diff --git a/dev/benchmarks/diag_tiebreak.R b/dev/benchmarks/diag_tiebreak.R new file mode 100644 index 000000000..d51116abc --- /dev/null +++ b/dev/benchmarks/diag_tiebreak.R @@ -0,0 +1,34 @@ +# Is exact-cost greedy stepwise addition highly sensitive to TIE-BREAKING? +# Brute-force exact greedy with first-min ('<') vs last-min ('<=') tie-break, +# same addition orders. If they swing by ~100+, the kernel's 1482-vs-1305 gap +# is tie-break (kernel correct, just a poor deterministic tie-break), and the +# real lever is a good/random tie-break (cf. TNT rseed[). +suppressMessages({ library(TreeSearch); library(TreeTools) }) +nm <- Sys.getenv("DS", "Zanol2014") +phy <- readRDS(sprintf("dev/benchmarks/t0/%s.phy.rds", nm)) +taxa <- names(phy); n <- length(taxa) + +Brute <- function(ord, lastMin = FALSE) { + tr <- PectinateTree(ord[1:3]) + for (k in 4:n) { + tip <- ord[k]; phySub <- phy[ord[1:k]] + nNodeNow <- 2L * (k - 1L) - 1L; best <- NULL; bestLen <- Inf + for (w in 0:nNodeNow) { + cand <- tryCatch(AddTip(tr, where = w, label = tip), error = function(e) NULL) + if (is.null(cand)) next + L <- TreeLength(cand, phySub) + take <- if (lastMin) (L <= bestLen) else (L < bestLen) + if (take) { bestLen <- L; best <- cand } + } + tr <- best + } + TreeLength(tr, phy) +} + +cat(sprintf("== %s | brute greedy tie-break sensitivity ==\n", nm)) +for (s in 1:6) { + set.seed(3000 + s); ord <- sample(taxa) + cat(sprintf(" seed %d: firstMin=%.0f lastMin=%.0f diff=%+.0f\n", + s, Brute(ord, FALSE), Brute(ord, TRUE), + Brute(ord, TRUE) - Brute(ord, FALSE))) +} diff --git a/dev/benchmarks/diag_tnt_help.R b/dev/benchmarks/diag_tnt_help.R new file mode 100644 index 000000000..b7275fa31 --- /dev/null +++ b/dev/benchmarks/diag_tnt_help.R @@ -0,0 +1,20 @@ +# Dump TNT help for the commands relevant to Wagner-only (no-swap) RAS and +# fixed-addition-sequence, plus bbreak/mult, to verify exact syntax before +# spending the K=200 TNT batch. Uses the define_target.R system2 pattern. +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +wd <- file.path(tempdir(), paste0("tnthelp", Sys.getpid())) +unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) +writeLines(c( + "mxram 1024;", + "help mult;", + "help rseed;", + "help bbreak;", + "help randtrees;", + "help hold;", + "quit;"), + file.path(wd, "helpdump.run")) +old <- setwd(wd) +out <- suppressWarnings(system2(TNT, args = "helpdump.run;", stdout = TRUE, stderr = TRUE)) +setwd(old) +out <- iconv(out, from = "", to = "UTF-8", sub = "") +cat(out, sep = "\n") diff --git a/dev/benchmarks/diag_tnt_noglobal_probe.R b/dev/benchmarks/diag_tnt_noglobal_probe.R new file mode 100644 index 000000000..ef9909fc3 --- /dev/null +++ b/dev/benchmarks/diag_tnt_noglobal_probe.R @@ -0,0 +1,49 @@ +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), winslash = "/")) + library(TreeTools) +}) +TNT <- "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe" +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) suppressWarnings(as.double(gsub(",", "", x))) +dsN <- c("Wortley2006", "Zanol2014", "Zhu2013", "Giles2015") +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) +cfgs <- list( + default = character(0), + noglobal = "sectsch: noglobal;", + equals = "sectsch: equals;", + global1 = "sectsch: global 1;", + recurse2 = "sectsch: recurse 2;" +) +rx_best <- "Sectorial search \\(RSS\\), best score:" +rx_tbr <- "Best score \\(TBR\\):" +run_cfg <- function(wd, setlines) { + is_rec <- any(grepl("recurse", setlines)) + pre <- if (is_rec) setlines else character(0) + post <- if (is_rec) character(0) else setlines + writeLines(c("mxram 1024;", pre, "proc data.tnt;", "rseed 1;", "hold 1;", + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", + post, rep("sectsch=rss;", 8), "quit;"), + file.path(wd, "optest.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "optest.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + vl <- grep(rx_best, out, value = TRUE) + v <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", vl)) + tl <- grep(rx_tbr, out, value = TRUE) + t0 <- num(sub(".*\\(TBR\\):\\s*([0-9.]+).*", "\\1", tl[1])) + list(t0 = t0, best = if (length(v)) min(v, na.rm = TRUE) else NA_real_) +} +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + wd <- file.path(tempdir(), paste0("ng", Sys.getpid(), nm)) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + cat(sprintf("\n==== %s | target(sectsch)=%d ====\n", nm, target[[nm]])) + for (cn in names(cfgs)) { + r <- run_cfg(wd, cfgs[[cn]]) + d <- if (is.finite(r$best) && is.finite(r$t0)) r$best - r$t0 else NA_real_ + cat(sprintf(" %-9s T0=%s sectsch_best=%s (%+.0f vs T0)\n", + cn, format(r$t0), format(r$best), d)) + } +} diff --git a/dev/benchmarks/diag_tnt_sect_escape.R b/dev/benchmarks/diag_tnt_sect_escape.R new file mode 100644 index 000000000..075cebf72 --- /dev/null +++ b/dev/benchmarks/diag_tnt_sect_escape.R @@ -0,0 +1,39 @@ +# FOUNDATIONAL CHECK for the D1 hunt: does TNT's sectorial (sectsch=rss) actually +# ESCAPE its own mult T0? The audit's whole premise is "TNT RSS improves T0 by +# +3..+11; ours improves 0". If TNT sectsch does NOT beat its mult T0, the +# sectorial-escape story is a misattribution and D1 is moot. TNT-only. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) suppressWarnings(as.double(gsub(",", "", x))) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Wortley2006 Zanol2014")), "\\s+")[[1]] + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + wd <- file.path(tempdir(), paste0("se", Sys.getpid(), nm)) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + writeLines(c("mxram 1024;", "proc data.tnt;", "hold 1000;", "rseed 1;", + "mult=replic 1;", "best;", # T0 (mult) best score + rep("sectsch=rss;", 8), "best;", # post-sectorial best score + "quit;"), file.path(wd, "setest.run")) + old <- setwd(wd) + out <- suppressWarnings(system2(TNT, args = "setest.run;", stdout = TRUE, stderr = TRUE)) + setwd(old) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + best_lines <- grep("Best score:", out, value = TRUE) + best_vals <- num(sub(".*Best score:\\s*([0-9.]+).*", "\\1", best_lines)) + sect_lines <- grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE) + sect_vals <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", sect_lines)) + t0 <- if (length(best_vals)) best_vals[1] else NA + final <- if (length(best_vals)) min(best_vals, na.rm = TRUE) else NA + cat(sprintf("%-11s | mult T0=%s | final=%s | escape=%s | sectsch progression: %s\n", + nm, format(t0), format(final), + if (is.finite(t0) && is.finite(final)) sprintf("%+.0f", final - t0) else "NA", + paste(format(sect_vals), collapse=" "))) +} diff --git a/dev/benchmarks/diag_tnt_sectsch_options.R b/dev/benchmarks/diag_tnt_sectsch_options.R new file mode 100644 index 000000000..e5e39305e --- /dev/null +++ b/dev/benchmarks/diag_tnt_sectsch_options.R @@ -0,0 +1,47 @@ +# TNT sectsch OPTION TRACE: from the shared T0 (mult, rseed 1), run sectsch=rss +# with escape-relevant knobs toggled, to isolate WHICH drives TNT's -10 escape. +# noglobal -> if escape dies, the GLOBAL-TBR cadence is the mechanism +# equals -> if escape grows/changes, LATERAL acceptance matters +# global 1 -> max global-TBR cadence +# TNT-only, deterministic T0 across runs (same rseed/mult). +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) suppressWarnings(as.double(gsub(",", "", x))) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014")), "\\s+")[[1]] + +# each config = the "sectsch: ;" line(s) before the runs ("" = defaults) +cfgs <- list( + default = character(0), + noglobal = "sectsch: noglobal;", + equals = "sectsch: equals;", + global1 = "sectsch: global 1;", + eq_global1= c("sectsch: equals;", "sectsch: global 1;") +) +run_cfg <- function(phy, wd, setlines) { + writeLines(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1000;", + "mult=replic 1;", setlines, rep("sectsch=rss;", 8), "quit;"), + file.path(wd, "optest.run")) + old <- setwd(wd); on.exit(setwd(old)) + out <- suppressWarnings(system2(TNT, args = "optest.run;", stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + v <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) + if (length(v)) min(v, na.rm = TRUE) else NA_real_ +} +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]) + wd <- file.path(tempdir(), paste0("opt", Sys.getpid(), nm)) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + cat(sprintf("\n==== %s (T0 ~ TNT mult) | sectsch option trace ====\n", nm)) + for (cn in names(cfgs)) { + sc <- run_cfg(phy, wd, cfgs[[cn]]) + cat(sprintf(" %-11s sectsch best = %s\n", cn, format(sc))) + } +} diff --git a/dev/benchmarks/diag_tnt_seq_accum.R b/dev/benchmarks/diag_tnt_seq_accum.R new file mode 100644 index 000000000..c5571b9f8 --- /dev/null +++ b/dev/benchmarks/diag_tnt_seq_accum.R @@ -0,0 +1,24 @@ +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), winslash = "/")) + library(TreeTools) +}) +TNT <- "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe" +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) suppressWarnings(as.double(gsub(",", "", x))) +nm <- Sys.getenv("DS", "Zanol2014") +phy <- fitch(inapplicable.phyData[[nm]]) +wd <- file.path(tempdir(), paste0("seq", Sys.getpid())) +unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) +WriteTntCharacters(phy, file.path(wd, "data.tnt")) +writeLines(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1;", + "mult=replic 1;", rep("sectsch=rss;", 8), "quit;"), + file.path(wd, "seq.run")) +old <- setwd(wd) +out <- suppressWarnings(system2(TNT, args = "seq.run;", stdout = TRUE, stderr = TRUE)) +setwd(old) +out <- iconv(out, from = "", to = "UTF-8", sub = "") +cat(sprintf("==== %s | per-pass RSS trace ====\n", nm)) +# Show every line that reports a score or replacements, in order +keep <- grep("Best score|RSS|eplac|earrang|ector", out, ignore.case = TRUE, value = TRUE) +cat(paste0(" ", trimws(keep)), sep = "\n") diff --git a/dev/benchmarks/diag_treespace_pool.R b/dev/benchmarks/diag_treespace_pool.R new file mode 100644 index 000000000..f62dae78c --- /dev/null +++ b/dev/benchmarks/diag_treespace_pool.R @@ -0,0 +1,125 @@ +# Decisive tree-space sampling comparison, isolating THREE confounded effects on +# the strict-consensus resolution (internal-node count; fewer = more conservative +# = more thoroughly sampled plateau): +# (A) pool-cap under-sampling : TS full pool 100 vs TS full pool 10000 +# (B) early-stop island deficit: TS full vs TS cs6 (at the SAME pool) +# (C) the benchmark : TNT xmult=level 10, hold 10000 +# +# Tree-space sampling is bitness-independent, so local 32-bit TNT is valid. +# Reference = strict consensus of the UNION of all methods' MPTs (per dataset); +# each method's consensus node count + ClusteringInfoDist-to-union reported. +# +# Verdict hinges on where TNT sits: +# - TNT ~ TS-full-pool100 (both over-resolved) => TNT is ALSO island-limited in +# fast mode; our early stop is no worse than the engine we match => SHIP stop. +# - TNT ~ TS-full-pool10000 (well collapsed) << TS-cs6 => TNT samples better; +# keep the full/large-pool path for conservative consensus (stop = opt-in). +# - TS-full-pool100 >> TS-full-pool10000 => our DEFAULT pool of 100 under-samples +# vs TNT regardless of the stop => raise poolMaxSize (separate, important fix). +# +# Env: TS_LIB (default .agent-stop), NSEED (default 3), +# TNT_EXE (local 32-bit), BIGPOOL (default 10000), MAXSEC (default 300). +.libPaths(c(Sys.getenv("TS_LIB", ".agent-stop"), .libPaths())) +suppressMessages({ library(TreeSearch); library(TreeTools); library(TreeDist) }) + +nseed <- as.integer(Sys.getenv("NSEED", "3")) +bigPool <- as.integer(Sys.getenv("BIGPOOL", "10000")) +maxSec <- as.integer(Sys.getenv("MAXSEC", "300")) +TNT_EXE <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +datasets <- c("Zanol2014", "Zhu2013", "Wortley2006", "Giles2015") +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) +data("inapplicable.phyData", package = "TreeSearch") +wd <- file.path(tempdir(), "tspool"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +.phy <- function(nm) { + m <- PhyDatToMatrix(inapplicable.phyData[[nm]], ambigNA = FALSE); m[m == "-"] <- "?" + MatrixToPhyDat(m) +} +.strict <- function(tr) { + if (is.null(tr) || length(tr) == 0L) return(NULL) + if (inherits(tr, "phylo")) return(tr) + if (length(tr) == 1L) return(tr[[1]]) + ape::consensus(tr, p = 1) +} +.asMP <- function(tr) { if (is.null(tr)) list() else if (inherits(tr, "phylo")) list(tr) else unclass(tr) } + +runTNT <- function(phy, seed) { + datafile <- file.path(wd, "d.tnt"); out <- file.path(wd, "o.tre") + if (file.exists(out)) file.remove(out) + WriteTntCharacters(phy, datafile) + cmds <- c("mxram 1024;", sprintf("proc %s;", basename(datafile)), "hold 10000;", + sprintf("rseed %d;", seed), "xmult=level 10;", "best;", + "tsave *o.tre;", "save;", "tsave/;", "quit;") + old <- setwd(wd); on.exit(setwd(old)) + t <- system.time(system2(TNT_EXE, input = cmds, stdout = FALSE, stderr = FALSE)) + tr <- tryCatch(ReadTntTree("o.tre"), error = function(e) NULL) + list(tr = tr, wall = as.double(t["elapsed"])) +} +runTS <- function(phy, seed, csReps, poolSize) { + set.seed(seed) + t <- system.time(r <- suppressWarnings(MaximizeParsimony(phy, strategy = "thorough", + maxSeconds = maxSec, nThreads = 1L, verbosity = 0L, + consensusStableReps = csReps, poolMaxSize = poolSize))) + list(tr = r, wall = as.double(t["elapsed"])) +} + +methods <- list( + TNT = function(phy, s) { x <- runTNT(phy, s); list(mp = .asMP(x$tr), wall = x$wall) }, + TSf_p100 = function(phy, s) { x <- runTS(phy, s, 0L, 100L); list(mp = .asMP(x$tr), wall = x$wall) }, + TSf_pBig = function(phy, s) { x <- runTS(phy, s, 0L, bigPool); list(mp = .asMP(x$tr), wall = x$wall) }, + TScs6_pBig = function(phy, s) { x <- runTS(phy, s, 6L, bigPool); list(mp = .asMP(x$tr), wall = x$wall) } +) + +allMP <- list(); rows <- list() +for (nm in datasets) { + phy <- .phy(nm); tgt <- target[[nm]] + for (s in seq_len(nseed)) { + rec <- list(dataset = nm, seed = s, target = tgt) + for (mn in names(methods)) { + res <- methods[[mn]](phy, s) + mp <- res$mp; allMP[[mn]][[nm]][[s]] <- mp + sc <- if (length(mp)) min(vapply(mp, function(t) TreeLength(t, phy), 0)) else NA_real_ + cons <- .strict(mp) + rec[[paste0(mn, "_sc")]] <- round(sc) + rec[[paste0(mn, "_n")]] <- length(mp) + rec[[paste0(mn, "_node")]] <- if (is.null(cons)) NA_integer_ else cons$Nnode + rec[[paste0(mn, "_wall")]] <- round(res$wall, 1) + } + rows[[length(rows) + 1L]] <- as.data.frame(rec) + cat(sprintf("%-12s s%d | nodes: TNT=%s TSf100=%s TSfBig=%s cs6Big=%s | n: %s/%s/%s/%s | sc TNT=%s\n", + nm, s, rec$TNT_node, rec$TSf_p100_node, rec$TSf_pBig_node, rec$TScs6_pBig_node, + rec$TNT_n, rec$TSf_p100_n, rec$TSf_pBig_n, rec$TScs6_pBig_n, rec$TNT_sc)) + } +} +df <- do.call(rbind, rows) +write.csv(df, file.path(Sys.getenv("OUTDIR","dev/benchmarks"), "treespace_pool.csv"), row.names = FALSE) + +# Union reference per dataset + per-method CID +cidRows <- list() +for (nm in datasets) { + uni <- do.call(c, lapply(names(methods), function(mn) + do.call(c, lapply(seq_len(nseed), function(s) allMP[[mn]][[nm]][[s]])))) + uni <- uni[!vapply(uni, is.null, TRUE)]; class(uni) <- "multiPhylo" + refCons <- .strict(uni) + for (mn in names(methods)) { + ma <- do.call(c, lapply(seq_len(nseed), function(s) allMP[[mn]][[nm]][[s]])) + ma <- ma[!vapply(ma, is.null, TRUE)]; class(ma) <- "multiPhylo" + mc <- .strict(ma) + cid <- tryCatch(as.double(ClusteringInfoDist(mc, refCons, normalize = TRUE)), + error = function(e) NA_real_) + cidRows[[length(cidRows)+1L]] <- data.frame(dataset = nm, method = mn, + node = mc$Nnode, refNode = refCons$Nnode, cid2union = round(cid, 4)) + } +} +cdf <- do.call(rbind, cidRows) +write.csv(cdf, file.path(Sys.getenv("OUTDIR","dev/benchmarks"), "treespace_pool_cid.csv"), row.names = FALSE) + +cat("\n=== median consensus internal nodes (lower = more conservative sampling) ===\n") +print(aggregate(cbind(TNT_node, TSf_p100_node, TSf_pBig_node, TScs6_pBig_node) ~ dataset, + df, median), row.names = FALSE) +cat("\n=== median MPTs retained ===\n") +print(aggregate(cbind(TNT_n, TSf_p100_n, TSf_pBig_n, TScs6_pBig_n) ~ dataset, df, median), row.names = FALSE) +cat("\n=== median wall (s) ===\n") +print(aggregate(cbind(TNT_wall, TSf_p100_wall, TSf_pBig_wall, TScs6_pBig_wall) ~ dataset, df, median), row.names = FALSE) +cat("\n=== pooled consensus vs union-of-all-methods reference ===\n") +print(cdf[order(cdf$dataset, cdf$method), ], row.names = FALSE) diff --git a/dev/benchmarks/diag_treespace_sampling.R b/dev/benchmarks/diag_treespace_sampling.R new file mode 100644 index 000000000..5a628d4fb --- /dev/null +++ b/dev/benchmarks/diag_treespace_sampling.R @@ -0,0 +1,131 @@ +# How well does each engine SAMPLE tree space (not just "what's the best score")? +# Reframes the consensus-fidelity question: our early-stop consensus is more +# resolved than our OWN exhaustive full run -- but the engine we are matching is +# TNT, whose xmult also self-terminates. So the fair benchmark is TNT's +# sampling, not our gold-plated full run. +# +# Tree-space sampling is BITNESS-INDEPENDENT (only wall-clock needs Hamilton), so +# local 32-bit TNT is valid here. For each dataset x seed we collect the MPT set +# from three methods and compare the STRICT-CONSENSUS RESOLUTION (internal-node +# count) -- fewer nodes = more conservative = more thoroughly sampled plateau: +# TNT : xmult=level 10, hold 10000, best (representative thorough user) +# TSf : TreeSearch thorough, full run (current default; no early stop) +# TScs : TreeSearch thorough + consensusStableReps=6 (the proposed early stop) +# +# A "best available" reference consensus is built from the UNION of all three +# methods' MPTs (all seeds); each method's distance to it (ClusteringInfoDist) +# and its node count are reported. Verdict logic in the trailer. +# +# Env: TS_LIB (default .agent-stop), NSEED (default 3), +# TNT_EXE (default local 32-bit 1.6). +.libPaths(c(Sys.getenv("TS_LIB", ".agent-stop"), .libPaths())) +suppressMessages({ library(TreeSearch); library(TreeTools); library(TreeDist) }) + +nseed <- as.integer(Sys.getenv("NSEED", "3")) +TNT_EXE <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +datasets <- c("Wortley2006", "Zanol2014", "Zhu2013", "Giles2015") +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) +data("inapplicable.phyData", package = "TreeSearch") +wd <- file.path(tempdir(), "tntsamp"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) + +.phy <- function(nm) { + m <- PhyDatToMatrix(inapplicable.phyData[[nm]], ambigNA = FALSE); m[m == "-"] <- "?" + MatrixToPhyDat(m) +} +.strict <- function(trees) { + if (is.null(trees) || length(trees) == 0L) return(NULL) + if (inherits(trees, "phylo")) return(trees) + if (length(trees) == 1L) return(trees[[1]]) + ape::consensus(trees, p = 1) +} +.asMP <- function(tr) { # normalise to multiPhylo list of phylo + if (is.null(tr)) return(list()) + if (inherits(tr, "phylo")) return(list(tr)) + unclass(tr) +} + +# --- TNT: xmult=level 10, retain MPTs, save all trees, read back -------------- +runTNT <- function(phy, seed) { + datafile <- file.path(wd, "d.tnt"); out <- file.path(wd, "tntout.tre") + if (file.exists(out)) file.remove(out) + WriteTntCharacters(phy, datafile) + cmds <- c("mxram 1024;", sprintf("proc %s;", basename(datafile)), + "hold 10000;", sprintf("rseed %d;", seed), + "xmult=level 10;", "best;", + "tsave *tntout.tre;", "save;", "tsave/;", "quit;") + old <- setwd(wd); on.exit(setwd(old)) + system2(TNT_EXE, input = cmds, stdout = FALSE, stderr = FALSE) + tr <- tryCatch(ReadTntTree("tntout.tre"), error = function(e) NULL) + tr +} + +allMPT <- list() # method -> dataset -> seed -> multiPhylo (for union ref) +rows <- list() +for (nm in datasets) { + phy <- .phy(nm); tgt <- target[[nm]] + for (s in seq_len(nseed)) { + # TNT + tnt <- runTNT(phy, s) + tntMP <- .asMP(tnt) + tntSc <- if (length(tntMP)) min(vapply(tntMP, function(t) TreeLength(t, phy), 0)) else NA + # TreeSearch full + set.seed(s) + tsf <- suppressWarnings(MaximizeParsimony(phy, strategy = "thorough", + maxSeconds = 600, nThreads = 1L, verbosity = 0L)) + # TreeSearch + early stop + set.seed(s) + tscs <- suppressWarnings(MaximizeParsimony(phy, strategy = "thorough", + maxSeconds = 600, nThreads = 1L, verbosity = 0L, + consensusStableReps = 6L)) + allMPT[["TNT"]][[nm]][[s]] <- tntMP + allMPT[["TSf"]][[nm]][[s]] <- .asMP(tsf) + allMPT[["TScs"]][[nm]][[s]] <- .asMP(tscs) + rows[[length(rows) + 1L]] <- data.frame( + dataset = nm, seed = s, target = tgt, + tntScore = round(tntSc), tntMPT = length(tntMP), + tntNode = { c <- .strict(tnt); if (is.null(c)) NA else c$Nnode }, + tsfScore = min(as.double(attr(tsf, "score"))), tsfMPT = length(tsf), + tsfNode = .strict(tsf)$Nnode, + tscsScore = min(as.double(attr(tscs, "score"))), tscsMPT = length(tscs), + tscsNode = .strict(tscs)$Nnode) + cat(sprintf(paste0("%-12s s%d: TNT %.0f (n=%d, nodes=%s) | ", + "TSfull %.0f (n=%d, nodes=%d) | TScs6 %.0f (n=%d, nodes=%d)\n"), + nm, s, tntSc, length(tntMP), + { c <- .strict(tnt); if (is.null(c)) "NA" else c$Nnode }, + min(as.double(attr(tsf,"score"))), length(tsf), .strict(tsf)$Nnode, + min(as.double(attr(tscs,"score"))), length(tscs), .strict(tscs)$Nnode)) + } +} +df <- do.call(rbind, rows) + +# --- Union reference per dataset + CID of each method's per-dataset consensus -- +cidRows <- list() +for (nm in datasets) { + uni <- do.call(c, lapply(c("TNT", "TSf", "TScs"), function(meth) + do.call(c, lapply(seq_len(nseed), function(s) allMPT[[meth]][[nm]][[s]])))) + uni <- uni[!vapply(uni, is.null, TRUE)] + class(uni) <- "multiPhylo" + refCons <- .strict(uni); refNode <- refCons$Nnode + for (meth in c("TNT", "TSf", "TScs")) { + methAll <- do.call(c, lapply(seq_len(nseed), function(s) allMPT[[meth]][[nm]][[s]])) + methAll <- methAll[!vapply(methAll, is.null, TRUE)]; class(methAll) <- "multiPhylo" + mc <- .strict(methAll) + cid <- tryCatch(as.double(ClusteringInfoDist(mc, refCons, normalize = TRUE)), + error = function(e) NA_real_) + cidRows[[length(cidRows) + 1L]] <- data.frame( + dataset = nm, method = meth, node = mc$Nnode, refNode = refNode, + cid2union = round(cid, 4)) + } +} +cdf <- do.call(rbind, cidRows) +write.csv(df, file.path(Sys.getenv("OUTDIR", "dev/benchmarks"), "treespace_sampling.csv"), row.names = FALSE) +write.csv(cdf, file.path(Sys.getenv("OUTDIR", "dev/benchmarks"), "treespace_cid.csv"), row.names = FALSE) + +cat("\n=== resolution (median internal nodes; lower = more conservative sampling) ===\n") +print(aggregate(cbind(tntNode, tsfNode, tscsNode) ~ dataset, df, median), row.names = FALSE) +cat("\n=== pooled-by-dataset consensus vs union-of-all-methods reference ===\n") +print(cdf[order(cdf$dataset, cdf$method), ], row.names = FALSE) +cat("\nVERDICT: if tscsNode is between TNT and TSfull (i.e. TScs <= TNT), our early\n", + "stop samples tree space at least as conservatively as TNT -> no regression vs\n", + "the engine we are matching -> ship the stop. If TNT ~ TSfull << TScs, TNT truly\n", + "samples better and the full run is worth keeping as the non-early-stop path.\n", sep = "") diff --git a/dev/benchmarks/diag_wagner_bias_scores.R b/dev/benchmarks/diag_wagner_bias_scores.R new file mode 100644 index 000000000..f463dc048 --- /dev/null +++ b/dev/benchmarks/diag_wagner_bias_scores.R @@ -0,0 +1,28 @@ +# Does the insertion-cost bug also hit the BIASED Wagner variants (the +# production "default" strategy uses wagnerBias=1 Goloboff, temp 0.3)? +# Bias only changes the addition ORDER, not the cost formula, so we expect +# all three to be ~+350 over optimum if the formula is the culprit. +suppressMessages({ + library(TreeSearch, lib.loc = "C:/Users/pjjg18/GitHub/TS-selectem/.agent-selectem") + library(TreeTools) +}) +nm <- Sys.getenv("DS", "Zanol2014") +phy <- readRDS(sprintf("dev/benchmarks/t0/%s.phy.rds", nm)) +n <- length(phy) +at <- attributes(phy); contrast <- at$contrast +tipData <- matrix(unlist(phy, use.names = FALSE), nrow = n, byrow = TRUE) +weight <- TreeSearch:::.ScaleWeight(at$weight); levels <- at$levels + +set.seed(1) +biasName <- c("RANDOM", "GOLOBOFF(default)", "ENTROPY") +cat(sprintf("== %s | Wagner score by bias (n_reps=12, no TBR) ==\n", nm)) +cat("(exact-insertion RAS ~1300; optimum ~1261)\n") +for (b in 0:2) { + temp <- if (b == 0) 1.0 else 0.3 + res <- TreeSearch:::ts_wagner_bias_bench( + contrast, tipData, weight, levels, integer(0), -1.0, + b, temp, 12L, FALSE) + s <- res$wagner_score + cat(sprintf(" %-18s mean=%.0f sd=%.0f min=%.0f max=%.0f\n", + biasName[b + 1], mean(s), sd(s), min(s), max(s))) +} diff --git a/dev/benchmarks/diag_wagner_exact.R b/dev/benchmarks/diag_wagner_exact.R new file mode 100644 index 000000000..73b650c8f --- /dev/null +++ b/dev/benchmarks/diag_wagner_exact.R @@ -0,0 +1,53 @@ +# Decisive test: does an EXACT-insertion RAS Wagner (try every edge, full +# TreeLength, pick the true argmin) reach TNT-like Wagner quality (~1300)? +# If yes, our fast insertion-cost formula (fitch_indirect_length: +# Y = final(A) | final(D)) is the bug behind the +30% Wagner deficit. +suppressMessages({ + library(TreeSearch, lib.loc = "C:/Users/pjjg18/GitHub/TS-selectem/.agent-selectem") + library(TreeTools) +}) +nm <- Sys.getenv("DS", "Zanol2014") +phy <- readRDS(sprintf("dev/benchmarks/t0/%s.phy.rds", nm)) +taxa <- names(phy); n <- length(taxa) +NSEED <- as.integer(Sys.getenv("NSEED", "3")) + +ExactWagner <- function(seed) { + set.seed(seed) + ord <- sample(taxa) + tr <- PectinateTree(ord[1:3]) # unique 3-tip topology + for (k in 4:n) { + tip <- ord[k] + phySub <- phy[ord[1:k]] # data on tips so far + nNodeNow <- 2L * (k - 1L) - 1L # nodes in current (k-1)-tip tree + best <- NULL; bestLen <- Inf + for (w in 0:nNodeNow) { # 0 = above root + cand <- tryCatch(AddTip(tr, where = w, label = tip), + error = function(e) NULL) + if (is.null(cand)) next + L <- TreeLength(cand, phySub) + if (L < bestLen) { bestLen <- L; best <- cand } + } + tr <- best + } + TreeLength(tr, phy) +} + +# Reference: our fast-formula kernel Wagner, same seeds +at <- attributes(phy); contrast <- at$contrast +tipData <- matrix(unlist(phy, use.names = FALSE), nrow = n, byrow = TRUE) +weight <- TreeSearch:::.ScaleWeight(at$weight); levels <- at$levels +FastWagner <- function(seed) { + set.seed(seed) + TreeSearch:::ts_random_wagner_tree(contrast, tipData, weight, levels)$score +} + +cat(sprintf("== %s | n=%d | exact-insertion vs fast-formula RAS Wagner ==\n", nm, n)) +cat("(TNT no-swap RAS Wagner ~1283-1325; optimum ~1261)\n\n") +for (s in seq_len(NSEED)) { + t0 <- proc.time()["elapsed"] + ex <- ExactWagner(s) + el <- proc.time()["elapsed"] - t0 + fa <- FastWagner(s) + cat(sprintf(" seed %d: exact=%.0f fast=%.0f gap(fast-exact)=%+.0f [%.0fs]\n", + s, ex, fa, fa - ex, el)) +} diff --git a/dev/benchmarks/diag_wagner_union.R b/dev/benchmarks/diag_wagner_union.R new file mode 100644 index 000000000..28ab4a545 --- /dev/null +++ b/dev/benchmarks/diag_wagner_union.R @@ -0,0 +1,42 @@ +# Isolate "union formula wrong" vs "incremental final_ stale": run the +# directional kernel with TS_WAGNER_UNION (production union-of-finals formula +# + FULL fitch_score each step) vs my directional combine, vs brute-force +# oracle, all on the SAME addition order. +suppressMessages({ + library(TreeSearch, lib.loc = "C:/Users/pjjg18/GitHub/TS-selectem/.agent-selectem") + library(TreeTools) +}) +nm <- Sys.getenv("DS", "Zanol2014") +phy <- readRDS(sprintf("dev/benchmarks/t0/%s.phy.rds", nm)) +taxa <- names(phy); n <- length(taxa) +at <- attributes(phy); contrast <- at$contrast +tipData <- matrix(unlist(phy, use.names = FALSE), nrow = n, byrow = TRUE) +weight <- TreeSearch:::.ScaleWeight(at$weight); levels <- at$levels + +Dir <- function(ord) { + TreeSearch:::ts_wagner_tree_dir(contrast, tipData, weight, levels, + addition_order = as.integer(match(ord, taxa)))$score +} +Brute <- function(ord) { + tr <- PectinateTree(ord[1:3]) + for (k in 4:n) { + tip <- ord[k]; phySub <- phy[ord[1:k]] + nNodeNow <- 2L * (k - 1L) - 1L; best <- NULL; bestLen <- Inf + for (w in 0:nNodeNow) { + cand <- tryCatch(AddTip(tr, where = w, label = tip), error = function(e) NULL) + if (is.null(cand)) next + L <- TreeLength(cand, phySub); if (L < bestLen) { bestLen <- L; best <- cand } + } + tr <- best + } + TreeLength(tr, phy) +} + +cat(sprintf("== %s | union(full final_) vs combine vs brute, same order ==\n", nm)) +for (s in 1:6) { + set.seed(2000 + s); ord <- sample(taxa) + Sys.setenv(TS_WAGNER_UNION = "1"); u <- Dir(ord) + Sys.unsetenv("TS_WAGNER_UNION"); d <- Dir(ord) + b <- Brute(ord) + cat(sprintf(" seed %d: union=%.0f combine=%.0f brute=%.0f\n", s, u, d, b)) +} diff --git a/dev/benchmarks/diag_wagner_validate.R b/dev/benchmarks/diag_wagner_validate.R new file mode 100644 index 000000000..30c51f59e --- /dev/null +++ b/dev/benchmarks/diag_wagner_validate.R @@ -0,0 +1,75 @@ +# Validate the directional edge-set Wagner kernel (ts_wagner_tree_dir) against +# the exact-insertion oracle (brute-force full-rescore argmin). Strict gate: +# (1) score DISTRIBUTION matches the oracle on >=2 datasets; +# (2) on the SAME addition order, directional == brute-force (modulo early +# tie-break divergence). A consistently-higher directional score would +# indicate a close-but-wrong formula. +suppressMessages({ + library(TreeSearch, lib.loc = "C:/Users/pjjg18/GitHub/TS-selectem/.agent-selectem") + library(TreeTools) +}) +datasets <- strsplit(Sys.getenv("DS", "Zanol2014 Zhu2013"), "\\s+")[[1]] + +Mats <- function(phy) { + n <- length(phy); at <- attributes(phy) + list(n = n, contrast = at$contrast, + tipData = matrix(unlist(phy, use.names = FALSE), nrow = n, byrow = TRUE), + weight = TreeSearch:::.ScaleWeight(at$weight), levels = at$levels) +} + +# Brute-force exact-insertion Wagner for an EXPLICIT addition order. +BruteWagner <- function(phy, ord) { + taxa <- names(phy); n <- length(taxa) + tr <- PectinateTree(ord[1:3]) + for (k in 4:n) { + tip <- ord[k]; phySub <- phy[ord[1:k]] + nNodeNow <- 2L * (k - 1L) - 1L + best <- NULL; bestLen <- Inf + for (w in 0:nNodeNow) { + cand <- tryCatch(AddTip(tr, where = w, label = tip), error = function(e) NULL) + if (is.null(cand)) next + L <- TreeLength(cand, phySub) + if (L < bestLen) { bestLen <- L; best <- cand } + } + tr <- best + } + TreeLength(tr, phy) +} + +for (nm in datasets) { + phy <- readRDS(sprintf("dev/benchmarks/t0/%s.phy.rds", nm)) + taxa <- names(phy); M <- Mats(phy) + cat(sprintf("\n================ %s (n=%d) ================\n", nm, M$n)) + + Dir <- function(ord = NULL, seed = NULL) { + if (!is.null(seed)) set.seed(seed) + ao <- if (is.null(ord)) integer(0) else as.integer(match(ord, taxa)) + TreeSearch:::ts_wagner_tree_dir(M$contrast, M$tipData, M$weight, M$levels, + addition_order = ao)$score + } + Buggy <- function(seed) { + set.seed(seed) + TreeSearch:::ts_random_wagner_tree(M$contrast, M$tipData, M$weight, M$levels)$score + } + + # (1) distribution: directional random vs buggy random + dirRand <- vapply(1:12, function(s) Dir(seed = s), double(1)) + buggy <- vapply(1:12, function(s) Buggy(s), double(1)) + cat(sprintf(" directional(random) mean=%.1f sd=%.1f min=%.0f max=%.0f\n", + mean(dirRand), sd(dirRand), min(dirRand), max(dirRand))) + cat(sprintf(" buggy union-formula mean=%.1f sd=%.1f min=%.0f max=%.0f\n", + mean(buggy), sd(buggy), min(buggy), max(buggy))) + + # oracle reference (3 brute-force trees) + oracle <- vapply(1:3, function(s) { set.seed(s); BruteWagner(phy, sample(taxa)) }, double(1)) + cat(sprintf(" oracle brute-force : %s\n", paste(round(oracle), collapse = " "))) + + # (2) same-order: directional vs brute-force on identical addition orders + cat(" -- same-order (directional vs brute-force) --\n") + for (s in 1:6) { + set.seed(1000 + s); ord <- sample(taxa) + d <- Dir(ord = ord); b <- BruteWagner(phy, ord) + cat(sprintf(" seed %d: directional=%.0f brute=%.0f diff=%+.0f%s\n", + s, d, b, d - b, if (d > b) " <-- dir WORSE" else "")) + } +} diff --git a/dev/benchmarks/diag_wagner_verify.R b/dev/benchmarks/diag_wagner_verify.R new file mode 100644 index 000000000..bbb6e1672 --- /dev/null +++ b/dev/benchmarks/diag_wagner_verify.R @@ -0,0 +1,44 @@ +# Make-or-break: is the +356 TS Wagner deficit real, or a reconstruction +# artifact? Compare the C++ kernel's OWN score to TreeLength of the +# reconstructed tree, and triangulate with the tested AdditionTree() path. +suppressMessages({ + library(TreeSearch, lib.loc = "C:/Users/pjjg18/GitHub/TS-selectem/.agent-selectem") + library(TreeTools) +}) +nm <- Sys.getenv("DS", "Zanol2014") +phy <- readRDS(sprintf("dev/benchmarks/t0/%s.phy.rds", nm)) +taxa <- names(phy); n <- length(taxa) +at <- attributes(phy) +contrast <- at$contrast +tipData <- matrix(unlist(phy, use.names = FALSE), nrow = n, byrow = TRUE) +weight <- TreeSearch:::.ScaleWeight(at$weight) +levels <- at$levels + +.EdgeToPhylo <- function(edge) { + tr <- structure(list(edge = edge, tip.label = taxa, Nnode = n - 1L), class = "phylo") + Renumber(tr) +} + +cat("== kernel score vs TreeLength(reconstruction) ==\n") +for (i in 1:5) { + set.seed(i) + res <- TreeSearch:::ts_random_wagner_tree(contrast, tipData, weight, levels) + tr <- .EdgeToPhylo(res$edge) + tl <- TreeLength(tr, phy) + cat(sprintf(" seed %d: kernel=%.0f TreeLength=%.0f %s\n", + i, res$score, tl, + if (abs(res$score - tl) < 0.5) "MATCH" else "*** MISMATCH ***")) +} + +cat("\n== AdditionTree() (tested path) with random sequences ==\n") +for (i in 1:5) { + set.seed(100 + i) + seq_i <- sample(taxa) + tr <- AdditionTree(phy, sequence = seq_i) + cat(sprintf(" seed %d: AdditionTree TreeLength=%.0f\n", i, TreeLength(tr, phy))) +} + +cat("\n== sanity: a purely random topology score (upper reference) ==\n") +set.seed(7) +rt <- RandomTree(phy, root = taxa[1]) +cat(sprintf(" RandomTree TreeLength=%.0f\n", TreeLength(rt, phy))) diff --git a/dev/benchmarks/drift_mpt_analysis.md b/dev/benchmarks/drift_mpt_analysis.md new file mode 100644 index 000000000..1299ec6fc --- /dev/null +++ b/dev/benchmarks/drift_mpt_analysis.md @@ -0,0 +1,99 @@ +# T-254: Drift MPT Diversity Experiment + +## Question + +Drift search consumes 15–19% of wall time but contributes <1% of score +improvement (T-251). Before reducing it, we need to check whether drift +helps **MPT enumeration** — finding topologically distinct optimal trees +that the post-search TBR plateau walk uses as seeds. + +## Design + +- **Datasets**: Wortley2006 (37t), Zhu2013 (75t), Geisler2001 (68t) +- **Conditions**: `driftCycles=0` vs `driftCycles=2` (default preset value) +- **Seeds**: 1, 2, 3 +- **Budgets**: 30s (primary, equal-budget), 120s (with consensus stopping) +- **Other params**: All match `default` preset (ratchet 12 cycles, 25% + perturbation, XSS 3 rounds, etc.) +- **Metrics**: best score, pool tree count, n_topologies, replicates + completed, mean pairwise Robinson-Foulds distance + +### Equal-budget design + +The primary comparison uses `consensusStableReps=0` to disable +consensus-stability early stopping. This ensures both conditions use the +full 30s budget, avoiding the confound that no-drift converges to consensus +stability faster (fewer replicates needed to stabilize the strict consensus). + +## Results (30s, equal budget) + +| Dataset | Drift | Med score | Med trees | Med reps | Med RF | Drift % | +|-------------|:-----:|:---------:|:---------:|:--------:|:------:|:-------:| +| Geisler2001 | 0 | 1295 | 100 | 27 | 7.3 | 0 | +| Geisler2001 | 2 | 1295 | 100 | 25 | 7.4 | 18 | +| Wortley2006 | 0 | 482 | 4 | 75 | 17.3 | 0 | +| Wortley2006 | 2 | 482 | 2 | 62 | 10.0 | 15 | +| Zhu2013 | 0 | 638 | 100 | 26 | 11.6 | 0 | +| Zhu2013 | 2 | 638 | 100 | 19 | 10.2 | 17 | + +### Replicate cost + +| Dataset | Reps (d=0) | Reps (d=2) | Loss | +|-------------|:----------:|:----------:|:----:| +| Geisler2001 | 27 | 24 | 10% | +| Wortley2006 | 76 | 61 | 20% | +| Zhu2013 | 25 | 20 | 22% | + +### Key findings + +1. **Score quality**: Identical. Both conditions find the same best score + on all datasets at all seeds. + +2. **MPT count**: On Wortley2006, no-drift consistently finds 4 MPTs + (all 3 seeds) while drift finds 1–3 (median 2). On larger datasets, + both fill the 100-tree pool. Drift does NOT help MPT enumeration. + +3. **Topological diversity**: Mean pairwise RF distances are essentially + identical on Geisler2001 (7.3 vs 7.4 out of max 132). On Zhu2013, + no-drift shows slightly higher RF (11.6 vs 10.2 out of max 146). + On Wortley2006, no-drift has higher RF (17.3 vs 10.0 out of max 70). + **Drift does not improve topological diversity.** + +4. **Replicate throughput**: No-drift completes 10–22% more replicates + in the same wall time. Each independent replicate starts from a random + Wagner tree, providing more diverse initial basins than drift's local + perturbation within a single basin. + +5. **Consensus stability confound**: With consensus stopping enabled + (120s budget), no-drift reaches consensus stability 2–3× faster and + stops early. Drift prevents early stabilization (by perturbing into + slightly different topologies) but the extra time produces no better + scores or more MPTs. This means drift actively delays convergence + without adding value. + +## Conclusion + +**Drift can be safely eliminated from the default preset.** It provides: +- Zero score benefit (confirmed both here and in T-251) +- Zero MPT enumeration benefit (fewer MPTs on Wortley2006) +- Zero topological diversity benefit +- Negative throughput impact (10–22% fewer replicates) + +The time saved should be reallocated to additional replicates (which +provide genuinely independent basin sampling via random Wagner starts). + +## Recommendation for T-255 + +- **default**: `driftCycles = 0` (was 2) +- **sprint**: already 0 (no change) +- **thorough**: reduce from 12 to 0 or 1. The thorough preset has many + other escape mechanisms (NNI-perturbation, adaptive ratchet, outer + cycles) that make drift redundant. +- **large**: already 0 (no change) + +## Scripts and data + +- `dev/benchmarks/bench_drift_mpt.R` — full experiment script +- `dev/benchmarks/results_drift_mpt_30s.csv` — 30s with consensus stopping +- `dev/benchmarks/results_drift_mpt_120s.csv` — 120s with consensus stopping +- `dev/benchmarks/results_drift_mpt_30s_nostop.csv` — 30s equal-budget (primary) diff --git a/dev/benchmarks/gate_abandon.R b/dev/benchmarks/gate_abandon.R new file mode 100644 index 000000000..d5424b9d4 --- /dev/null +++ b/dev/benchmarks/gate_abandon.R @@ -0,0 +1,58 @@ +# BIT-IDENTITY GATE for the early-abandonment seed change (ts_tbr.cpp:900). +# Runs a fixed, deterministic search (set.seed + fixed replicates + non-binding +# timeout, nThreads=1) across EW / NA / IW x strict / accept_equal, capturing +# score + MPT count + a topology checksum. Run against the baseline lib then +# the edited lib; the change is behaviour-preserving iff every row matches. +# TS_LIB=.agent-aband Rscript dev/benchmarks/gate_abandon.R # baseline +# TS_LIB=.agent-aband2 Rscript dev/benchmarks/gate_abandon.R # edited +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), + winslash = "/")) + library(TreeTools) +}) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } + +lib <- Sys.getenv("TS_LIB", ".agent-aband") + +# Deterministic topology checksum (no external pkg; identical trees -> identical). +tdig <- function(r) { + tr <- if (inherits(r, "multiPhylo")) r else structure(list(r), class = "multiPhylo") + nw <- vapply(tr, function(t) paste(ape::write.tree(t)), character(1)) + s <- paste(sort(nw), collapse = "|") + sprintf("%d:%d", length(tr), sum(as.integer(charToRaw(s)))) +} + +# config: dataset, scoremode (ew/na/iw), extra args (incl. accept_equal route) +cfgs <- list( + list(id = "Zanol_ew_strict", ds = "Zanol2014", mode = "ew", extra = list(strategy = "thorough")), + list(id = "Zanol_ew_acceq", ds = "Zanol2014", mode = "ew", extra = list(strategy = "thorough", sectorAcceptEqual = TRUE, rssRounds = 4L)), + list(id = "Zanol_na_strict", ds = "Zanol2014", mode = "na", extra = list(strategy = "thorough")), + list(id = "Wortley_ew_strict", ds = "Wortley2006", mode = "ew", extra = list(strategy = "thorough")), + list(id = "Wortley_iw_k3", ds = "Wortley2006", mode = "iw", extra = list(strategy = "thorough", concavity = 3)) +) +seeds <- c(1L, 2L) + +rows <- list() +for (cf in cfgs) { + raw <- inapplicable.phyData[[cf$ds]] + phy <- if (cf$mode == "na") raw else fitch(raw) + for (sd in seeds) { + set.seed(sd) + args <- c(list(dataset = phy, maxReplicates = 1L, maxSeconds = 600, + nThreads = 1L, verbosity = 0L), cf$extra) + t0 <- Sys.time() + r <- suppressWarnings(do.call(MaximizeParsimony, args)) + wall <- as.double(difftime(Sys.time(), t0, units = "secs")) + rows[[length(rows) + 1]] <- data.frame( + lib = basename(lib), config = cf$id, seed = sd, + score = min(as.double(attr(r, "score"))), dig = tdig(r), + wall = round(wall, 2), stringsAsFactors = FALSE) + cat(sprintf("%-18s s%d | score=%.0f dig=%s wall=%.1fs\n", + cf$id, sd, min(as.double(attr(r, "score"))), tdig(r), wall)) + } +} +S <- do.call(rbind, rows) +out <- sprintf("dev/benchmarks/gate_abandon_%s.csv", basename(lib)) +write.csv(S, out, row.names = FALSE) +cat(sprintf("\nWrote %s (sum wall %.1fs)\n", out, sum(S$wall))) diff --git a/dev/benchmarks/hamilton_build_once.sh b/dev/benchmarks/hamilton_build_once.sh new file mode 100644 index 000000000..4aeea230b --- /dev/null +++ b/dev/benchmarks/hamilton_build_once.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Build/install TreeSearch ONCE into a shared read-only library, so a job-array +# panel never recompiles. Submit first; chain the array on afterok of this job. +# DISPATCH-UNTESTED template (cell logic validated locally) — smoke in test.q first. +#SBATCH --job-name=ts-build-once +#SBATCH -p shared +#SBATCH -n 4 +#SBATCH --mem=8G +#SBATCH --time=0:45:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/build_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/build_%j.err + +module load r/4.5.1 +module load gcc/14.2 + +LIB=/nobackup/$USER/TreeSearch/lib +REPO=/nobackup/$USER/TreeSearch-a +mkdir -p "$LIB" /nobackup/$USER/TreeSearch/logs + +cd "$REPO" || { echo "FATAL: no $REPO"; exit 1; } +git fetch origin cpp-search && (git checkout cpp-search && git pull --ff-only origin cpp-search \ + || git reset --hard origin/cpp-search) +echo "Git HEAD: $(git log --oneline -1)" + +rm -f src/*.o src/*.so +R CMD build --no-build-vignettes --no-manual --no-resave-data . +R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz +rc=$? +rm -f TreeSearch_*.tar.gz +echo "INSTALL exit: $rc; version: $(R_LIBS_USER=$LIB Rscript -e 'cat(as.character(packageVersion("TreeSearch")))' 2>/dev/null)" +exit $rc diff --git a/dev/benchmarks/hamilton_merge.sh b/dev/benchmarks/hamilton_merge.sh new file mode 100644 index 000000000..125bf8a99 --- /dev/null +++ b/dev/benchmarks/hamilton_merge.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Merge per-cell partial CSVs from the array into one panel CSV. +# Submit with --dependency=afterany: (afterany so partial failures +# still merge what succeeded). Validate row count == expected grid size. +# DISPATCH-UNTESTED template. +#SBATCH --job-name=ts-merge +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=2G +#SBATCH --time=0:10:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/merge_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/merge_%j.err + +module load r/4.5.1 +P=/nobackup/$USER/TreeSearch/panel_partials +O=/nobackup/$USER/TreeSearch/panel_results +mkdir -p "$O" +Rscript -e "f<-list.files('$P',pattern='cell_.*csv\$',full.names=TRUE); \ + d<-do.call(rbind,lapply(f,read.csv)); \ + write.csv(d,file.path('$O','panel.csv'),row.names=FALSE); \ + cat(sprintf('%d rows from %d cells -> %s/panel.csv\n', nrow(d), length(f), '$O'))" diff --git a/dev/benchmarks/hamilton_panel_array.sh b/dev/benchmarks/hamilton_panel_array.sh new file mode 100644 index 000000000..4d16fc281 --- /dev/null +++ b/dev/benchmarks/hamilton_panel_array.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Job-array panel: one task per (dataset x seed) cell, consuming the shared $LIB +# from hamilton_build_once.sh (submit with --dependency=afterok:). +# Tune --array upper bound to (n_datasets * n_seeds - 1); %N caps concurrency. +# DISPATCH-UNTESTED template — bench_cell.R is validated locally. +#SBATCH --job-name=ts-panel +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=4G +#SBATCH --time=0:30:00 +#SBATCH --array=0-29%32 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/panel_%A_%a.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/panel_%A_%a.err + +module load r/4.5.1 +module load gcc/14.2 +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +LIB=/nobackup/$USER/TreeSearch/lib +REPO=/nobackup/$USER/TreeSearch-a +export R_LIBS_USER=$LIB +export TS_LIB=$LIB +export PARTIAL_DIR=/nobackup/$USER/TreeSearch/panel_partials +export TS_REPS=20 +export TS_SEEDS="1 2 3 4 5" +export TS_DATASETS="Wortley2006 Eklund2004 Zanol2014 Zhu2013 Giles2015 Dikow2009" + +cd "$REPO" || exit 1 +Rscript dev/benchmarks/bench_cell.R "$SLURM_ARRAY_TASK_ID" diff --git a/dev/benchmarks/hamilton_thorough_rasstarts.R b/dev/benchmarks/hamilton_thorough_rasstarts.R new file mode 100644 index 000000000..eb5460810 --- /dev/null +++ b/dev/benchmarks/hamilton_thorough_rasstarts.R @@ -0,0 +1,55 @@ +# Hamilton driver (task #29): full-search time-matched gate for rasStarts=3 in the +# AUTO-SELECTED `thorough` preset. Runs the WHOLE thorough pipeline at matched +# wall-clock, varying ONLY rasStarts (explicit arg overrides the preset field). +# Authoritative wall-clock (vs the indicative local run diag_thorough_rasstarts_tm.R). +# +# Grid: datasets x rasStarts{1,3} x budgets{60,120}s x seeds{1..NSEED}. +# Decision: adopt rasStarts=3 in thorough iff it improves (or matches at lower +# variance) the median score at matched budget across datasets, without hurting +# replicate throughput enough to regress on any. +# +# Env: TS_LIB (installed pkg), OUTDIR, NSEED (default 10), BUDGETS, TS_DATASETS. +suppressMessages({ + library(TreeSearch, lib.loc = Sys.getenv("TS_LIB", .libPaths()[1])) + library(TreeTools) +}) +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", + "Zanol2014 Zhu2013 Wortley2006 Giles2015")), "\\s+")[[1]] +budgets <- as.integer(strsplit(trimws(Sys.getenv("BUDGETS", "60 120")), "\\s+")[[1]]) +nseed <- as.integer(Sys.getenv("NSEED", "10")) +outdir <- Sys.getenv("OUTDIR", "dev/benchmarks") +out_csv <- file.path(outdir, "thorough_rasstarts.csv") + +rows <- list() +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]); tgt <- target[[nm]]; nt <- length(phy) + for (secs in budgets) for (ras in c(1L, 3L)) for (s in seq_len(nseed)) { + set.seed(s) + t <- system.time(r <- suppressWarnings(MaximizeParsimony(phy, + strategy = "thorough", rasStarts = ras, maxSeconds = secs, + nThreads = 1L, verbosity = 0L))) + sc <- min(as.double(attr(r, "score"))) + nrep <- length(as.double(attr(r, "score"))) + rows[[length(rows) + 1L]] <- data.frame(dataset = nm, nTip = nt, + target = tgt, budget = secs, rasStarts = ras, seed = s, + score = sc, over = sc - tgt, n_trees = nrep, + elapsed = round(as.double(t["elapsed"]), 1)) + cat(sprintf("%-12s b=%3d ras=%d s=%2d -> %.0f (%+.0f) [%.0fs]\n", + nm, secs, ras, s, sc, sc - tgt, as.double(t["elapsed"]))) + } +} +df <- do.call(rbind, rows) +write.csv(df, out_csv, row.names = FALSE) + +# Per (dataset,budget): median over vs target, by rasStarts. +cat("\n=== median (score - target) by dataset x budget x rasStarts ===\n") +agg <- aggregate(over ~ dataset + budget + rasStarts, df, median) +w <- reshape(agg, idvar = c("dataset", "budget"), timevar = "rasStarts", + direction = "wide") +names(w) <- sub("over\\.", "ras", names(w)) +w$delta <- w$ras3 - w$ras1 # negative = ras3 better +print(w, row.names = FALSE) +cat(sprintf("\nWrote %s\n", out_csv)) diff --git a/dev/benchmarks/hamilton_timing.R b/dev/benchmarks/hamilton_timing.R new file mode 100644 index 000000000..11aa207a2 --- /dev/null +++ b/dev/benchmarks/hamilton_timing.R @@ -0,0 +1,69 @@ +# Hamilton wall-clock comparison: TreeSearch vs TNT 1.6 (64-bit), ONE dataset. +# Representative of what a real (not highly sophisticated) user runs in each engine, +# timed on identical 64-bit hardware. Scores re-computed in R via TreeLength +# (bitness-independent, authoritative); wall-clock is the comparison of interest. +# TNT results are static — cache once. +# +# Env: TS_LIB, TS_DATASET, TNT_EXE, OUTDIR, NSEED (default 3). +# Requires LD_LIBRARY_PATH=/TNT-bin and TERM=xterm in the job env. +.libPaths(c(Sys.getenv("TS_LIB", .libPaths()[1]), .libPaths())) +suppressMessages({ + library(TreeSearch) + library(TreeTools) +}) +nm <- Sys.getenv("TS_DATASET", "Zanol2014") +TNT <- Sys.getenv("TNT_EXE") +nseed <- as.integer(Sys.getenv("NSEED", "3")) +outdir<- Sys.getenv("OUTDIR", ".") +target <- c(Wortley2006 = 480, Zanol2014 = 1261, Zhu2013 = 624, Giles2015 = 670) + +data("inapplicable.phyData", package = "TreeSearch") +m <- PhyDatToMatrix(inapplicable.phyData[[nm]], ambigNA = FALSE); m[m == "-"] <- "?" +phy <- MatrixToPhyDat(m); tgt <- target[[nm]] +wd <- file.path(tempdir(), "tnt"); dir.create(wd, showWarnings = FALSE, recursive = TRUE) +WriteTntCharacters(phy, file.path(wd, "data.tnt")) + +# --- TreeSearch: realistic preset runs, timed to completion --- +run_ts <- function(strat, seed) { + set.seed(seed) + t <- system.time(r <- suppressWarnings(MaximizeParsimony(phy, strategy = strat, + maxSeconds = 600, nThreads = 1L, verbosity = 0L))) + c(score = min(as.double(attr(r, "score"))), wall = as.double(t["elapsed"])) +} +# --- TNT: representative user configs (verified headless), timed; re-score tree --- +run_tnt <- function(cfg, seed) { + out <- file.path(wd, "out.tre") + if (file.exists(out)) file.remove(out) + cmds <- c("mxram 2048;", "proc data.tnt;", sprintf("rseed %d;", seed), + paste0(cfg, ";"), "tsave *out.tre;", "save;", "tsave/;", "quit;") + old <- setwd(wd); on.exit(setwd(old)) + t <- system.time(system2(TNT, input = cmds, stdout = FALSE, stderr = FALSE)) + tr <- tryCatch(ReadTntTree("out.tre"), error = function(e) NULL) + if (inherits(tr, "multiPhylo")) tr <- tr[[1]] + sc <- if (is.null(tr)) NA_real_ else TreeLength(tr, phy) + c(score = sc, wall = as.double(t["elapsed"])) +} + +configs <- list( + list(engine = "TreeSearch", config = "default", fn = function(s) run_ts("default", s)), + list(engine = "TreeSearch", config = "thorough", fn = function(s) run_ts("thorough", s)), + list(engine = "TNT", config = "mult-basic", fn = function(s) run_tnt("mult=replic 10", s)), + list(engine = "TNT", config = "xmult-default",fn = function(s) run_tnt("xmult", s)), + list(engine = "TNT", config = "xmult-level10",fn = function(s) run_tnt("xmult=level 10", s)) +) + +rows <- list() +for (cf in configs) for (s in seq_len(nseed)) { + v <- cf$fn(s) + rows[[length(rows) + 1L]] <- data.frame(dataset = nm, target = tgt, + engine = cf$engine, config = cf$config, seed = s, + score = unname(v["score"]), over = unname(v["score"]) - tgt, + wall_s = round(unname(v["wall"]), 1)) + cat(sprintf("%-12s %-13s s%d -> %.0f (%+.0f) [%.1fs]\n", + cf$engine, cf$config, s, v["score"], v["score"] - tgt, v["wall"])) +} +df <- do.call(rbind, rows) +write.csv(df, file.path(outdir, paste0("timing_", nm, ".csv")), row.names = FALSE) +cat("\n=== median by engine/config ===\n") +agg <- aggregate(cbind(over, wall_s) ~ engine + config, df, median) +print(agg[order(agg$wall_s), ], row.names = FALSE) diff --git a/dev/benchmarks/headtohead_phase0.csv b/dev/benchmarks/headtohead_phase0.csv new file mode 100644 index 000000000..a17e30b59 --- /dev/null +++ b/dev/benchmarks/headtohead_phase0.csv @@ -0,0 +1,13 @@ +"dataset","tips","seed","ts_fitch","ts_raw","tnt","gapB","ts_cand","tnt_rearr","cand_ratio","ts_wall","tnt_wall","ts_reps" +"Wortley2006",37,1,483,482,479,4,107285740,57400345,1.87,4,3,50 +"Wortley2006",37,2,481,482,479,2,108105147,56879782,1.9,3.9,3,50 +"Eklund2004",54,1,440,440,440,0,245801050,143840946,1.71,7.7,3.1,50 +"Eklund2004",54,2,440,440,440,0,247734038,144919658,1.71,7.8,3.1,50 +"Zanol2014",74,1,1265,1315,1261,4,820718775,572823050,1.43,32.8,16.6,50 +"Zanol2014",74,2,1264,1314,1261,3,858055396,702694020,1.22,33.8,20.3,50 +"Zhu2013",75,1,626,638,624,2,845311334,590742615,1.43,21.4,13.4,50 +"Zhu2013",75,2,627,638,624,3,839045939,578021783,1.45,21.8,13.1,50 +"Giles2015",78,1,671,710,670,1,1031771313,580830408,1.78,25.1,13.3,50 +"Giles2015",78,2,672,710,670,2,1052547883,582101679,1.81,25.7,13.5,50 +"Dikow2009",88,1,1606,1611,1606,0,1041956865,787952594,1.32,37.1,17.1,50 +"Dikow2009",88,2,1606,1611,1606,0,1047938649,733737873,1.43,36.8,15.9,50 diff --git a/dev/benchmarks/intensive_panel.csv b/dev/benchmarks/intensive_panel.csv new file mode 100644 index 000000000..f739a37e2 --- /dev/null +++ b/dev/benchmarks/intensive_panel.csv @@ -0,0 +1,19 @@ +"dataset","seed","score","candidates" +"Wortley2006",1,484,52874094 +"Eklund2004",1,440,135562059 +"Zanol2014",1,1266,315143279 +"Zhu2013",1,627,354091783 +"Giles2015",1,672,415431625 +"Dikow2009",1,1606,380165923 +"Wortley2006",2,482,49323481 +"Eklund2004",2,440,128671456 +"Zanol2014",2,1265,333147540 +"Zhu2013",2,627,370791265 +"Giles2015",2,672,400656946 +"Dikow2009",2,1606,484714081 +"Wortley2006",3,481,51587098 +"Eklund2004",3,440,121285996 +"Zanol2014",3,1263,350071241 +"Zhu2013",3,628,361358957 +"Giles2015",3,673,395935002 +"Dikow2009",3,1607,391590566 diff --git a/dev/benchmarks/iterate_baseline_auto.csv b/dev/benchmarks/iterate_baseline_auto.csv new file mode 100644 index 000000000..7cd611f03 --- /dev/null +++ b/dev/benchmarks/iterate_baseline_auto.csv @@ -0,0 +1,19 @@ +"dataset","seed","score","candidates" +"Wortley2006",1,485,41570896 +"Eklund2004",1,440,84684820 +"Zanol2014",1,1263,421365335 +"Zhu2013",1,626,370707289 +"Giles2015",1,671,406039950 +"Dikow2009",1,1606,372115534 +"Wortley2006",2,483,38616547 +"Eklund2004",2,440,93873455 +"Zanol2014",2,1264,359832065 +"Zhu2013",2,630,337323906 +"Giles2015",2,672,524808796 +"Dikow2009",2,1606,416466253 +"Wortley2006",3,485,33476553 +"Eklund2004",3,440,96750733 +"Zanol2014",3,1268,360431126 +"Zhu2013",3,629,343784935 +"Giles2015",3,671,491328798 +"Dikow2009",3,1606,405179884 diff --git a/dev/benchmarks/mbank_X30754.nex b/dev/benchmarks/mbank_X30754.nex new file mode 100644 index 000000000..4e6507490 --- /dev/null +++ b/dev/benchmarks/mbank_X30754.nex @@ -0,0 +1,5240 @@ +#NEXUS + + [ File output by Morphobank v3.0 (http://www.morphobank.org); 2025-06-16 11.36.14 ] + + BEGIN TAXA; + DIMENSIONS NTAX=180; + TAXLABELS + 'Orstenoloricus shergoldii' + 'Gastrotricha' + 'Lineus' + 'Solenogastres' + 'Nereis' + 'Ancalagon minor' + 'Fieldia lanceolata' + 'Scolecofurca rara' + 'Markuelia lauriei' + 'Shergoldana australiensis' + 'Xinliscolex intermedius' + 'Shanscolex decorus' + 'Qinscolex spinosus' + 'Zhongpingscolex qinensis' + 'Eokinorhynchus rarus' + 'Eopriapulites sphinx' + 'Eolorica deadwoodensis' + 'Nanaloricus mysticus' + 'Armorloricus elegans' + 'Spinoloricus turbatio' + 'Rugiloricus carolinensis' + 'Pliciloricus corvus' + 'Urnaloricus ibenae' + 'Wataloricus japonicus' + 'Tenuiloricus shirayamai' + 'Patuloricus tangaroa' + 'Scaberiloricus samba' + 'Franciscideres kalenesos' + 'Antygomonas paulae' + 'Campyloderes cf vanhoeffeni' + 'Centroderes spinosus' + 'Echinoderes dujardinii' + 'Zelinkaderes klepali' + 'Cateria gerlachi' + 'Dracoderes abei' + 'Paracentrophyes anurus' + 'Pycnophyes zelinkaei' + 'Chordodes' + 'Nectonema' + 'Euchromadora' + 'Odontophora' + 'Kinonchulus' + 'Anatonchus' + 'Acanthopriapulus horridus' + 'Halicryptus spinulosus' + 'Maccabeus' + 'Meiopriapulus fijiensis' + 'Priapulopsis bicaudatus' + 'Priapulus caudatus' + 'Tubiluchus lemburgi' + 'Tubiluchus vanuatensis' + 'Euperipatoides' + 'Plicatoperipatus' + 'Ooperipatellus' + 'Archechiniscus bahamensis' + 'Batillipes pennaki' + 'Batillipes phreaticus' + 'Coronarctus yurupari' + 'Coronarctus laubieri' + 'Dipodarctus susannae' + 'Wingstrandarctus unsculptus' + 'Neoarctus primigenius' + 'Neostygarctus oceanopolis' + 'Renaudarctus fossorius' + 'Mesostygarctus spiralis' + 'Parastygarctus renaudae' + 'Raiarctus jesperi' + 'Styraconyx nanoqsunguak' + 'Actinarctus neretinus' + 'Isoechiniscoides sifae' + 'Neoechiniscoides aski' + 'Oreella chugachii' + 'Echiniscus testudo' + 'Multipseudechiniscus raneyi' + 'Testechiniscus spitsbergensis' + 'Pseudechiniscus suillus' + 'Cornechiniscus imperfectus' + 'Milnesium berladnicorum' + 'Milnesium swolenski' + 'Milnesium tardigradum' + 'Austeruseus faeroensis' + 'Mesocrista revelata' + 'Hypsibius dujardini' + 'Beron leggi' + 'Calohypsibius ornatus' + 'Fractonotus verrucosus' + 'Cryoconicus kaczmareki' + 'Haplomacrobiotus utahensis' + 'Doryphoribius dawkinsi' + 'Paradoryphoribius chronocaribbeus' + 'Halobiotus crispae' + 'Macrobiotus paulinae' + 'Dactylobiotus ovimutans' + 'Richtersius coronifer' + 'Sicyophorus rarus' + 'Sirilorica carlsbergi' + 'Acosmia' + 'Eximipriapulus globocaudata' + 'Laojieella thecata' + 'Ottoia prolifica' + 'Ottoia tricuspida' + 'Paratubiluchus bicaudatus' + 'Priapulites konecniorum' + 'Selkirkia columbia' + 'Paraselkirkia sinica' + 'Xiaoheiqingella peculiaris' + 'Xystoscolex boreogyrus' + 'Chalazoscolex pharkus' + 'Louisella pedunculata' + 'Corynetis brevis' + 'GUANDUSCOLEX minor' + 'MAOTIANSHANIA cylindrica' + 'PALAEOSCOLEX piscatorum' + 'SCHISTOSCOLEX umbilicatus' + 'SCATHASCOLEX minor' + 'WRONASCOLEX antiquus' + 'WRONASCOLEX iacoborum' + 'YUNNANOSCOLEX magnus' + 'MAFANGSCOLEX yunnanensis' + 'Cricocosmia n. sp.' + 'CRICOCOSMIA jinningensis' + 'TABELLISCOLEX hexagonus' + 'Tylotites petiolaris' + 'Xenusion' + 'Hadranax' + 'Aysheaia' + 'Siberion' + 'Onychodictyon ferox' + 'Diania' + 'Paucipodia' + 'Cardiodictyon' + 'Microdictyon' + 'Onychodictyon gracilis' + 'Thanahita distos' + 'Orstenotubulus' + 'Tritonychus phanerosarkus' + 'Carbotubulus' + 'Hallucigenia sparsa' + 'Hallucigenia fortis' + 'Hallucigenia hongmeia' + 'Facivermis yunnanicus' + 'Luolishania' + 'Ovatiovermis cribratus' + 'Collinsium' + 'Collinsovermis monstruosus' + 'Emu Bay Collins monster' + 'Acinocricus' + 'Antennacanthopodia' + 'Helenodora' + 'Tertiapatus dominicanus' + 'Siberian Orsten tardigrade' + 'Youti yuanshi' + 'Megadictyon' + 'Jianshanopodia' + 'Cucumericrus' + 'Kerygmachela' + 'Pambdelurion' + 'Omnidens qiongqii' + 'Parapeytoia' + 'Kylinxia' + 'Isoxys' + 'Stanleycaris' + 'Opabinia' + 'Utaurora' + 'Caryosyntrips camurus' + 'Amplectobelua symbrachiata' + 'Anomalocaris canadensis' + 'Cambroraster falcatus' + 'Hurdia victoria' + 'Cf. Peytoia' + 'Peytoia nathorsti' + 'Aegirocassis benmoulai' + 'Lyrarapax unguispinus' + 'Schinderhannes' + 'Chengjiangocaris' + 'Fuxianhuia' + 'Leanchoilia' + 'Alalcomenaeus' + 'Misszhouia longicaudata' + 'Kuamaia lata' + ; + ENDBLOCK; + + BEGIN CHARACTERS; + DIMENSIONS NCHAR=425; + FORMAT DATATYPE=STANDARD GAP=- MISSING=? SYMBOLS="0123456789A"; + CHARLABELS + [1] 'General organization: Voluminous primary body cavity' + [2] 'General organization: Aspect ratio of body length to (maximum) trunk width in adult' + [3] 'General organization: Clear differentiation of dorsal and ventral trunk' + [4] 'General organization: Paired appendages' + [5] 'General organization: Anus position' + [6] 'General organization: Mouth opening position' + [7] 'General organization: Mouth orientation' + [8] 'Introvert: Distinct introvert' + [9] 'Introvert: Triangular proboscis' + [10] 'Introvert: Invaginable' + [11] 'Introvert: Extent of invagination' + [12] 'Introvert: Two rings of introvert retractors attach through the collar-shaped brain' + [13] 'Introvert: Trichoscalids' + [14] 'Introvert: Trichoscalids: Nature of separation between trichoscalids and Zone I armature' + [15] 'Introvert: Trichoscalids: Number per ring' + [16] 'Introvert: Trichoscalids: Number of rings' + [17] 'Introvert: Trichoscalids: Basal plates' + [18] 'Introvert: Trichoscalids: Articulation' + [19] 'Introvert: Trichoscalids: Morphology' + [20] 'Introvert: Trichoscalids: Doubled' + [21] 'Introvert: Zone I armature' + [22] 'Introvert: Elements that comprise first three circlets define number of longitudinal rows of elements on the introvert' + [23] 'Introvert: Zone I armature: Direction' + [24] 'Introvert: Zone I armature: Number of circlets' + [25] 'Introvert: Zone I armature: Elements in two superposed series' + [26] 'Introvert: Zone I armature: Arranged in rows' + [27] 'Introvert: Zone I armature: Row orientation' + [28] 'Introvert: Zone I armature: Extent' + [29] 'Introvert: Zone I armature: Cuticularized' + [30] 'Introvert: Zone I armature: Solid elements' + [31] 'Introvert: Zone I armature: Elongate elements' + [32] 'Introvert: Zone I armature: Element curvature' + [33] 'Introvert: Zone I armature: Bifurcating elements' + [34] 'Introvert: Zone I armature: Elements are dentate' + [35] 'Introvert: Zone I armature: Elements comprise articulated units' + [36] 'Introvert: Zone I armature: Elements bear setules' + [37] 'Introvert: Zone I armature: Telescopic elements' + [38] 'Introvert: Zone I armature: Hooded elements' + [39] 'Introvert: Zone I armature: Intrinsic musculature' + [40] 'Introvert: Symmetry: Pentaradial' + [41] 'Introvert: Symmetry: Twentyfive-fold' + [42] 'Introvert: Symmetry: Hexaraidal' + [43] 'Pharynx: Large dorsal tooth' + [44] 'Pharynx: Pre-oral chamber' + [45] 'Pharynx: Annulations' + [46] 'Pharynx: Eversion' + [47] 'Pharynx: Eversion: Permanent' + [48] 'Pharynx: Eversion: Introvert or pharynx employed in locomotion' + [49] 'Pharynx: Eversion: Zone III eversible' + [50] 'Pharynx: Eversion: Size when everted' + [51] 'Pharynx: Eversion: Zone III fully inversible' + [52] 'Pharynx: Symmetry: Pharyngeal lumina symmetry' + [53] 'Pharynx: Zone II armature' + [54] 'Pharynx: Zone II armature: Contact area' + [55] 'Pharynx: Zone II armature: Disposition' + [56] 'Pharynx: Zone II armature: Differentiated elements' + [57] 'Pharynx: Zone II armature: Differentiated elements: Number of enlarged plates' + [58] 'Pharynx: Zone II armature: Furrowed folds' + [59] 'Pharynx: Zone II armature: Nodes on outer face' + [60] 'Pharynx: Zone II armature: Nodes on inner face' + [61] 'Pharynx: Zone II armature: Element constitution' + [62] 'Pharynx: Zone II armature: Elements in proximal circlet' + [63] 'Pharynx: Zone II armature: Aspect ratio' + [64] 'Pharynx: Zone II armature: Multiple cusps' + [65] 'Pharynx: Zone II armature: Spinose projections from inner face' + [66] 'Pharynx: Zone II armature: Spinose projections from inner face: Number' + [67] 'Pharynx: Zone II armature: Proximal circlet fused to introvert' + [68] 'Pharynx: Zone III wider than Zone II' + [69] 'Pharynx: Proximal region: Unarmed region between Zone II and Zone III' + [70] 'Pharynx: Proximal region: Cuticular reinforcement' + [71] 'Pharynx: Proximal region: Oral ridges' + [72] 'Pharynx: Proximal region: Oral ridges: Number' + [73] 'Pharynx: Proximal region: Oral ridges: Furcae' + [74] 'Pharynx: Proximal region: Oral ridges: Differentiated series' + [75] 'Pharynx: Proximal region: Fenestrae' + [76] 'Pharynx: Zone III armature' + [77] 'Pharynx: Zone III armature: Complexity' + [78] 'Pharynx: Zone III armature: Retained to adulthood' + [79] 'Pharynx: Zone III armature: Composition' + [80] 'Pharynx: Zone III armature: Disposition' + [81] 'Pharynx: Zone III armature: Radial extent' + [82] 'Pharynx: Zone III armature: Number of circlets' + [83] 'Pharynx: Zone III armature: Number of pentagonal circlets in proximal region' + [84] 'Pharynx: Zone III armature: Proximal circlet: Number of elements' + [85] 'Pharynx: Zone III armature: Proximal circlet: Which multiple of five' + [86] 'Pharynx: Zone III armature: Proximal circlet: Dorsal element reduced' + [87] 'Pharynx: Zone III armature: Proximal circlet: Alternating size' + [88] 'Pharynx: Zone III armature: Proximal circlet: Prominent central spine in elements' + [89] 'Pharynx: Zone III armature: Proximal circlet: Prominent central spine: Recurved (hooked)' + [90] 'Pharynx: Zone III armature: Proximal circlet: Additional robust spines (multispinose) or pectinate fringe on elements' + [91] 'Pharynx: Zone III armature: Proximal circlet: Elements comprise articulated units' + [92] 'Pharynx: Zone III armature: Proximal circlet: Massively reduced' + [93] 'Pharynx: Zone III armature: Proximal circlet: Morphologically differentiated' + [94] 'Pharynx: Zone III armature: Ring fold' + [95] 'Pharynx: Zone III armature: Middle circlets of Zone III armature reduced' + [96] 'Pharynx: Zone III armature: Middle circlets: Element morphology' + [97] 'Pharynx: Zone III armature: Distal circlets: Morphologically distinct' + [98] 'Pharynx: Zone III armature: Distal circlets: Element morphology' + [99] 'Pharynx: Zone III armature: Distal circlets: Trend of element size' + [100] 'Pharynx: Zone III armature: Intrinsic muscles of outer oral styles' + [101] 'Pharynx: Zone III armature: Placoids' + [102] 'Pharynx: Zone III armature: Placoids: Type' + [103] 'Pharynx: Zone III armature: Microplacoid' + [104] 'Pharynx: Zone III armature: Reinforcement of pharynx cuticle' + [105] 'Pharynx: Buccal tube: Apophysis for the insertion of the stylet muscle' + [106] 'Pharynx: Buccal tube: Apophysis: Type' + [107] 'Pharynx: Terminal bulb' + [108] 'Neck: Forms segment-like ring' + [109] 'Neck: Encircled by ring of cuticular plates' + [110] 'Neck: Cuticular neck plates: Form closing mechanism when adult head retracted into trunk' + [111] 'Neck: Cuticular neck plates: Closing apparatus: Symmetry' + [112] 'Neck: Cuticular neck plates: Number' + [113] 'Neck: Cuticular neck plates: Distal margin shape' + [114] 'Neck: Cuticular neck plates: Attachment to first trunk segment' + [115] 'Head region: Amphids' + [116] 'Head region: Amphids: Fovea shape' + [117] 'Head region: Anterodorsal lobe' + [118] 'Head region: Anterior region covered by sclerites' + [119] 'Head region: Head shield (cephalic shield) formed by fused cephalic segments' + [120] 'Head region: Dorsal isolated sclerite: Position' + [121] 'Head region: Dorsal isolated sclerite: Shape' + [122] 'Head region: Dorsal isolated sclerite: Reticulate ornament' + [123] 'Head region: Degree of attachment of dorsal isolated sclerite on head' + [124] 'Head region: Isolated lateral sclerites, forming tripartite carapace' + [125] 'Head region: Isolated lateral sclerites: Shape' + [126] 'Head region: Ventral isolated sclerite' + [127] 'Head region: Anterior trunk flexure in coronal plane' + [128] 'Head region: Swelling of anteriormost trunk ' + [129] 'Head region: Paired anterior projections' + [130] 'Head region: Paired anterior projections: Incorporated into lips' + [131] 'Head region: Paired anterior projection: Sensory field' + [132] 'Head region: Paired anterior projections: Position of Cirri A' + [133] 'Head region: Club or dome-shaped chemosensory organ' + [134] 'Ocular structures' + [135] 'Ocular structures: Number' + [136] 'Ocular structures: Compound eyes' + [137] 'Ocular structures: Compound eyes: Attachment' + [138] 'Ocular structures: Compound eyes: Posterior displacement' + [139] 'Cephalic/anterior appendages: Protocerebral appendage pair: Sclerotization' + [140] 'Cephalic/anterior appendages: Protocerebral appendage pair: Arthrodial membranes' + [141] 'Cephalic/anterior appendages: Pre-ocular (protocerebral) limb pair: Structurally differentiated from trunk appendages' + [142] 'Cephalic/anterior appendages: Protocerebral appendage pair: Podomeres' + [143] 'Cephalic/anterior appendages: Protocerebral appendages: Podomeres: Differentiation' + [144] 'Cephalic/anterior appendages: Protocerebral appendages: Podomeres: Distal taper' + [145] 'Cephalic/anterior appendages: Protocerebral appendage pair: Position' + [146] 'Cephalic/anterior appendages: Protocerebral appendage pair: Posterior shift' + [147] 'Cephalic/anterior appendages: Protocerebral appendages: Directly adjacent to one another' + [148] 'Cephalic/anterior appendages: Protocerebral appendages: Basal adjacency' + [149] 'Cephalic/anterior appendages: Protocerebral appendages: Mechanical fusion' + [150] 'Cephalic/anterior appendages: Protocerebral appendage pair: Loss of claws' + [151] 'Cephalic/anterior appendages: Protocerebral appendage pair: Ventral spine series' + [152] 'Cephalic/anterior appendages: Protocerebral appendages: Ventral spine/spinules: Number' + [153] 'Cephalic/anterior appendages: Protocerebral appendages: Ventral spine/spinules: Height' + [154] 'Cephalic/anterior appendages: Protocerebral appendages: Ventral spine/spinules: Accessory spines' + [155] 'Cephalic/anterior appendages: Protocerebral appendages: Ventral spine/spinules: Accessory spine distribution' + [156] 'Cephalic/anterior appendages: Protocerebral appendages: Ventral spine/spinules: Alternation' + [157] 'Cephalic/anterior appendages: Protocerebral appendages: Ventral spine/spinules: Width' + [158] 'Cephalic/anterior appendages: Protocerebral appendages: Ventral spine/spinules: Base to tip thickness' + [159] 'Cephalic/anterior appendages: Protocerebral appendages: Ventral spine/spinules: Tip orientation' + [160] 'Cephalic/anterior appendages: Protocerebral spine series: Lateral spine series' + [161] 'Cephalic/anterior appendages: Protocerebral appendage pair: Multifurcate distal termination' + [162] 'Cephalic/anterior appendages: Protocerebral appendages: Kink' + [163] 'Cephalic/anterior appendages: Protocerebral appendages: Pincer' + [164] 'Cephalic/anterior appendages: Protocerebral appendages: Outer spines' + [165] 'Cephalic/anterior appendages: Protocerebral appendages: Accessory gnathal spines' + [166] 'Cephalic/anterior appendages: Post-ocular (post-protocerebral) appendages: Arthrodial membranes' + [167] 'Cephalic/anterior appendages: Nature of post-ocular lobopodous inner branch' + [168] 'Cephalic/anterior appendages: Deutocerebral limb pair structurally differentiated from trunk appendages' + [169] 'Cephalic/anterior appendages: Nature of sclerotized first post-ocular (deutocerebral) appendage' + [170] 'Cephalic/anterior appendages: Nature of lobopodous first post-ocular (deutocerebral) appendage' + [171] 'Cephalic/anterior appendages: Inner blade of deutocerebral jaw with diastema' + [172] 'Cephalic/anterior appendages: Nature of lobopodous second post-ocular (tritocerebral) appendage' + [173] 'Cephalic/anterior appendages: Nature of arthropodized second post-ocular (tritocerebral) appendage' + [174] 'Trunk region: Annulations' + [175] 'Trunk region: Annulations: Organization' + [176] 'Trunk region: Annulations: Annulations become indistinct in undifferentiated anterior trunk' + [177] 'Trunk region: Annulations: Branching of annular rings' + [178] 'Trunk region: Epidermal segmentation' + [179] 'Trunk region: Dorsal integument sclerotized to form sternal plates' + [180] 'Trunk region: Sternal plates: Connected by arthrodial membranes' + [181] 'Trunk region: Sternal plates: First sternite: Anterior margin with lateral projections' + [182] 'Trunk region: Sternal plates: First sternite: Anterior margin with medial notch' + [183] 'Trunk region: Sternal plates: First sternite: Posterior ventral spine' + [184] 'Trunk region: Sternal plates: Second segment is a single ring' + [185] 'Trunk region: Sternal plates: Differentiation in third and fourth segment' + [186] 'Trunk region: Sternal plates: Present in trunk segments 7+' + [187] 'Trunk region: Sternal plates: Posterior sternite differentiated' + [188] 'Trunk region: Sternal plates: Posterior sternite: Dorsal extension of margins' + [189] 'Trunk region: Sternal plates: Posterior sternite: Dorsal extension of margins: Extended into spinose process' + [190] 'Trunk region: Sternal plates: Posterior sternite: Lateral terminal spines' + [191] 'Trunk region: Sternal plates: Posterior sternite: Lateral accessory spines' + [192] 'Trunk region: Sternal plates: Posterior sternite: Medial spine' + [193] 'Trunk region: Sternal plates: Posterior sternite: Medial spine: Muscles' + [194] 'Trunk region: Sternal plates: Posterior sternite: Lateroventral notches in margins' + [195] 'Trunk region: Sternal plates: Setae' + [196] 'Trunk region: Sternal plates: Scales' + [197] 'Trunk region: Sternal plates: Secondary fringe' + [198] 'Trunk region: Serially repeated mid-gut glands' + [199] 'Trunk region: Narrowing posteriad' + [200] 'Trunk region: Differentiated anterior trunk' + [201] 'Trunk region: Middle of trunk bears single pair of elongated lateral cuspidate spines' + [202] 'Trunk region: Flosculi or sensory spots' + [203] 'Trunk region: Sensory spots: Flosculi' + [204] 'Trunk region: Sensory spots: Flosculi: Petals' + [205] 'Trunk region: Sensory spots: Flosculi: Petals: Number' + [206] 'Trunk region: Papillae on trunk annulations' + [207] 'Trunk region: Epidermal papillae in two ventral rows' + [208] 'Trunk region: Lorica' + [209] 'Trunk region: Lorica: Retained to adulthood' + [210] 'Trunk region: Lorica: Cuticle thickened in dorsal and ventral plicae' + [211] 'Trunk region: Lorica: Series of lorical plates' + [212] 'Trunk region: Lorica: Number of plates per series' + [213] 'Trunk region: Lorica: Differentiated dorsal and ventral plates' + [214] 'Epidermal sclerites: Present on adult trunk' + [215] 'Epidermal sclerites: Comprise a stack of nested elements' + [216] 'Epidermal sclerites: Integumentary trunk sclerites' + [217] 'Epidermal sclerites: Trunk sclerites: Heavily phosphatized' + [218] 'Epidermal sclerites: Trunk sclerites: Shape' + [219] 'Epidermal sclerites: Trunk sclerites: Nodes' + [220] 'Epidermal sclerites: Trunk sclerites: Nodes: Number of rings' + [221] 'Epidermal sclerites: Trunk sclerites: Nodes: Number in central ring is constant' + [222] 'Epidermal sclerites: Trunk sclerites: Nodes: Number in central ring' + [223] 'Epidermal sclerites: Trunk sclerites: Nodes: Exact number of nodes in central ring (if three to six)' + [224] 'Epidermal sclerites: Trunk sclerites: Differentiated anterior region' + [225] 'Epidermal sclerites: Trunk sclerites: Distribution: Complete rings' + [226] 'Epidermal sclerites: Trunk sclerites: Distribution' + [227] 'Epidermal sclerites: Trunk sclerites: Rows: Sclerite fields per annulation' + [228] 'Epidermal sclerites: Trunk sclerites: Rows: Distribution of sclerites within fields' + [229] 'Epidermal sclerites: Trunk sclerites: Distribution: Row arrangement' + [230] 'Epidermal sclerites: Trunk sclerites: Microplates present in addition to plates' + [231] 'Epidermal sclerites: Trunk sclerites: Tessellation' + [232] 'Epidermal sclerites: Sparse specialized sclerites' + [233] 'Epidermal sclerites: Sparse specialized sclerites: Trunk tubuli' + [234] 'Epidermal sclerites: Sparse specialized sclerites: Tumuli (small sclerites)' + [235] 'Epidermal sclerites: Sparse specialized sclerites: Tumuli: Radial supporting buttresses' + [236] 'Epidermal sclerites: Enlarged sclerites' + [237] 'Epidermal sclerites: Enlarged sclerites: Regular distribution' + [238] 'Epidermal sclerites: Enlarged sclerites: Transverse bands: Maximum elements per band' + [239] 'Epidermal sclerites: Enlarged sclerites: Transverse bands: Frequency' + [240] 'Epidermal sclerites: Enlarged sclerites: Transverse bands: Spacing' + [241] 'Epidermal sclerites: Enlarged sclerites: Transverse bands: Intersegmental dorsal plates' + [242] 'Epidermal sclerites: Enlarged sclerites: Transverse bands: Consistent size' + [243] 'Epidermal sclerites: Enlarged sclerites: Transverse bands: Pseudosegmental dorsal plates' + [244] 'Epidermal sclerites: Enlarged sclerites: Proportions' + [245] 'Epidermal sclerites: Enlarged sclerites: Acute distal termination' + [246] 'Epidermal sclerites: Enlarged sclerites: Acute distal termination: Curvature' + [247] 'Epidermal sclerites: Enlarged sclerites: Shape of distal margins' + [248] 'Epidermal sclerites: Enlarged sclerites: Degree of sclerotization' + [249] 'Epidermal sclerites: Enlarged sclerites: Lateral flanges' + [250] 'Epidermal sclerites: Enlarged sclerites: Ornament' + [251] 'Epidermal sclerites: Enlarged sclerites: Ornament: Bosses at net junctions' + [252] 'Trunk appendages: Sclerotization' + [253] 'Trunk appendages: Longitudinal (gill-like) wrinkling on distal part of (outer branch) flaps-v2' + [254] 'Trunk appendages: Trunk exites' + [255] 'Trunk appendages: Trunk exites: Form' + [256] 'Trunk appendages: Trunk exites: Fused with endopod to form biramous appendage' + [257] 'Trunk appendages: Dorsal flaps' + [258] 'Trunk appendages: Antero-posteriorly compressed protopodite with gnathobasic endites in post-deutocerebral appendage pair' + [259] 'Trunk appendages: Exite distribution' + [260] 'Trunk appendages: Shape of lobopodous appendages' + [261] 'Trunk appendages: Secondary structures on non-sclerotized (lobopodous) limbs' + [262] 'Trunk appendages: Nature of secondary structure' + [263] 'Trunk appendages: Type of secondary structure' + [264] 'Trunk appendages: Length of spines on secondary structure' + [265] 'Trunk appendages: Papillae on non-sclerotized (lobopodous) limbs' + [266] 'Trunk appendages: Finger-like elements in distal tip of limbs' + [267] 'Trunk appendages: Papillae with terminal spine' + [268] 'Trunk appendages: Discs ' + [269] 'Trunk appendages: Claws on trunk limbs' + [270] 'Trunk appendages: Claws: Shape of base' + [271] 'Trunk appendages: Claws: Position' + [272] 'Trunk appendages: Claws: Multiple branches' + [273] 'Trunk appendages: Claws: Multiple branches: Type' + [274] 'Trunk appendages: Claws: Multiple branches: Type: Symmetry of fused claws' + [275] 'Trunk appendages: Claws: Multiple branches: Primary branch' + [276] 'Trunk appendages: Claws: Multiple branches: Branch angle' + [277] 'Claws: Multiple branches: Anterior claws: Connection between primary and secondary branch' + [278] 'Claws: Multiple branches: Anterior claws: Symmetry of primary and secondary branches with respect to median plane of leg' + [279] 'Claws: Multiple branches: Anterior claws: External claw primary branch connection to basal section' + [280] 'Claws: Multiple branches: Anterior claws: Angular insertion of external claw secondary branch to basal section' + [281] 'Claws: Multiple branches: Anterior claws: Basal section subdivided into stem/peduncle and distal section' + [282] 'Claws: Multiple branches: Anterior claws: Base extension' + [283] 'Claws: Multiple branches: Anterior claws: Base extension: Type' + [284] 'Claws: Multiple branches: Posterior claws: Connection between primary and secondary branch' + [285] 'Claws: Multiple branches: Posterior claws: Symmetry of primary and secondary branches with respect to leg median plane' + [286] 'Claws: Multiple branches: Posterior claws: Primary branch connection to basal section' + [287] 'Claws: Multiple branches: Posterior claws: Angular insertion of secondary branch to basal section' + [288] 'Claws: Multiple branches: Posterior claws: Basal section subdivided into basal and distal sections' + [289] 'Claws: Multiple branches: Posterior claws: Base extension' + [290] 'Claws: Multiple branches: Posterior claws: Base extension: Type' + [291] 'Trunk appendages: Maximum number of claws on walking limbs' + [292] 'Trunk appendages: Number of claws varies between appendages' + [293] 'Trunk appendages: Nature of claws on each trunk limb' + [294] 'Trunk appendages: Differentiated distal foot in lobopodous trunk limbs' + [295] 'Trunk appendages: Telescopic lobopodous limbs' + [296] 'Trunk appendages: External branch expressed as lateral flaps (body extends laterally into imbricated, unsclerotized flaps)-v2' + [297] 'Trunk appendages: Strengthening rays in lateral flaps' + [298] 'Trunk appendages: Posterior tapering of lateral flaps' + [299] 'Trunk appendages: Anteriormost limb pair hypertrophied' + [300] 'Trunk appendages: Anterior limbs reduced' + [301] 'Trunk appendages: Lobopodous limbs differentiated into two batches of multiple anterior/long and posterior/short limbs' + [302] 'Trunk appendages: Number of limbs on differentiated anterior trunk' + [303] 'Trunk appendages: Nature of lobopodous limbs on differentiated anterior trunk' + [304] 'Trunk appendages: Appendages comprise 15 or more podomeres' + [305] 'Trunk appendages: Leg plate' + [306] 'Posterior termination: Limbless posterior extension of the lobopodous trunk' + [307] 'Posterior termination: Posterior trunk divided into appendages' + [308] 'Posterior termination: Posterior tagma composed of three paired lateral flaps' + [309] 'Posterior termination: Direction of claws on posteriormost appendage pair' + [310] 'Posterior termination: Posterior trunk appendages: Structural differentiation' + [311] 'Posterior termination: Posterior trunk appendages: Structural differentiation: Nature' + [312] 'Posterior termination: Posterior trunk appendages: Tail: Nature' + [313] 'Posterior termination: Posterior trunk appendages: Tail: Shape' + [314] 'Posterior termination: Posterior trunk with localised bulbous widening' + [315] 'Posterior termination: Caudal appendage' + [316] 'Posterior termination: Caudal appendage: Eversible' + [317] 'Posterior termination: Caudal appendage: Length' + [318] 'Posterior termination: Caudal appendages: Divided' + [319] 'Posterior termination: Caudal appendage: Single' + [320] 'Posterior termination: Caudal appendage: Position' + [321] 'Posterior termination: Caudal appendage: Surface' + [322] 'Posterior termination: Spinneret' + [323] 'Posterior termination: Posterior projections (i.e. spines or hooks)' + [324] 'Posterior termination: Posterior projections: Sclerotization' + [325] 'Posterior termination: Posterior projections: Basal diameter >20% trunk diameter' + [326] 'Posterior termination: Posterior projections: Number' + [327] 'Posterior termination: Posterior projections: Arrangement' + [328] 'Posterior termination: Posterior ring papillae' + [329] 'Posterior termination: Posterior abdomen greatly extensible' + [330] 'Posterior termination: Posterior warts' + [331] 'Posterior termination: Posterior wart size' + [332] 'Musculature: Skeletal musculature' + [333] 'Musculature: Longitudinal peripheral musculature' + [334] 'Musculature: Ventromedian longitudinal muscle' + [335] 'Musculature: Longitudinal muscle attachment points' + [336] 'Musculature: Longitudinal muscle attachment points: Position on tegumental plate' + [337] 'Musculature: Circular peripheral musculature' + [338] 'Musculature: Circular musculature inside longitudinal musculature' + [339] 'Musculature: Loss of dorsoventral muscles in segment 1' + [340] 'Musculature: Box-truss' + [341] 'Musculature: Heart' + [342] 'Musculature: Pharynx protractor muscles' + [343] 'Neuroanatomy: Nerve cord location' + [344] 'Neuroanatomy: Ventral nerve cord: Paired' + [345] 'Neuroanatomy: Paired ventral nerve cords: Symmetry' + [346] 'Neuroanatomy: Paired ventral nerve cords: Merge caudally' + [347] 'Neuroanatomy: Paired ventral nerve cords: Paired ganglia' + [348] 'Neuroanatomy: Paired ventral nerve cords: Position' + [349] 'Neuroanatomy: Paired ventral nerve cords: Medial interpedal commissures' + [350] 'Neuroanatomy: VNC with morphologically discrete condensed hemiganglia connected by medial commissures' + [351] 'Neuroanatomy: Regularly spaced peripheral nerves along the entire length of the nerve cord' + [352] 'Neuroanatomy: Nerve cord has orthogonal organization' + [353] 'Neuroanatomy: Orthogonal nerve cord: Complete ring commissures' + [354] 'Neuroanatomy: Segmental leg nerves shifted anteriorly relative to appendages' + [355] 'Neuroanatomy: Segmental leg nerves paired' + [356] 'Neuroanatomy: Stomatogastric ganglion' + [357] 'Neuroanatomy: Circumpharyngeal brain' + [358] 'Neuroanatomy: Circumpharyngeal brain: Subpharyngeal main region with weak suprapharyngeal commissure' + [359] 'Neuroanatomy: Dorsal condensed brain' + [360] 'Neuroanatomy: Dorsal condensed brain: Neuromeres' + [361] 'Neuroanatomy: Mouth innervation relative to brain neuromeres' + [362] 'Neuroanatomy: Dorsal nerve cord' + [363] 'Neuroanatomy: Dorsal nerve cord: Paired' + [364] 'Neuroanatomy: Brain neuropil sandwiched by perikarya' + [365] 'Neuroanatomy: Apical brain composed of perikarya' + [366] 'Neuroanatomy: Tooth ganglia connected by diagonal nerve net' + [367] 'Organ systems: Perigenital area: Cloaca' + [368] 'Organ systems: Perigenital area: Cloaca in both sexes' + [369] 'Organ systems: Perigenital area: Urogenital system attached to the body wall by a ligament' + [370] 'Organ systems: Perigenital area: Seminal receptacle: External ' + [371] 'Organ systems: Perigenital area: Perigenital setae' + [372] 'Organ systems: Perigenital area: Clavulae' + [373] 'Organ systems: Perigenital area: Clavulae: Stalk length' + [374] 'Organ systems: Perigenital area: Clavulae: Distal bulb' + [375] 'Organ systems: Perigenital area: Bullulae' + [376] 'Organ systems: Expanded anterior gut' + [377] 'Organ systems: Polythyridium' + [378] 'Organ systems: Protonephridia' + [379] 'Organ systems: Protonephridia: Integrated into the gonad' + [380] 'Organ systems: Protonephridia: Compound filter, built by two or more terminal cells' + [381] 'Organ systems: Protonephridia: Sieve plates' + [382] 'Organ systems: Protonephridia: Terminal cells with circumciliary microvilli' + [383] 'Organ systems: Tube' + [384] 'Cellular structure: Flagellate spermatozoa' + [385] 'Cellular structure: Primary constituent of cuticle' + [386] 'Cellular structure: Layer of cuticle containing abundant chitin' + [387] 'Cellular structure: Middle layer of cuticle has distinct composition' + [388] 'Cellular structure: Nucleation of "peritoneal" membrane' + [389] 'Cellular structure: Pillar-like structure in the epicuticle' + [390] 'Cellular structure: Tanycytes' + [391] 'Cellular structure: Cross-wise fibres in cuticle' + [392] 'Cellular structure: Large helical fibres in cuticle' + [393] 'Cellular structure: Egg ornamentation' + [394] 'Larval morphology: Developmental mode' + [395] 'Larval morphology: Cuticle dorso-ventrally flattened with six accordion-like lateral plates' + [396] 'Larval morphology: Neck crenulated like an accordion' + [397] 'Larval morphology: Larvae/juveniles with long pharynx retractor muscles' + [398] 'Larval morphology: Body divided into proboscis + abdomen' + [399] 'Larval morphology: Diaphragm separates larval thorax from abdomen' + [400] 'Larval morphology: Pair of spines at anterior of larval abdomen' + [401] 'Larval morphology: Caudal spines or appendages at posterior of larval abdomen' + [402] 'Larval morphology: Buccal canal morphology' + [403] 'Larval morphology: Large mesenchyme cells' + [404] 'Larval morphology: Higgins larva' + [405] 'Higgins larva: Head-trunk dimensions' + [406] 'Higgins larva: Thorax-abdomen length dimensions' + [407] 'Higgins larva: Thorax ornamentation' + [408] 'Higgins larva: Thorax wrinkles: Nature' + [409] 'Higgins larva: Lorica composition' + [410] 'Higgins larva: Closing plates on ventral side of thorax' + [411] 'Higgins larva: Head and thorax separated by collar region' + [412] 'Higgins larva: Mouth cone with oral teeth' + [413] 'Higgins larva: Inner armature' + [414] 'Higgins larva: Clavoscalids with distal units forming lobe with a hook' + [415] 'Higgins larva: Number of Row 2 scalids' + [416] 'Higgins larva: Row 2 scalids: Small, pincher-shaped claw' + [417] 'Higgins larva: Bifurcated scalids in penultimate row' + [418] 'Higgins larva: Alternating trifurcated and kite-shaped scalids in posteriormost row' + [419] 'Higgins larva: Anteroventral setae' + [420] 'Higgins larva: Anterolateral setae' + [421] 'Higgins larva: Short ventral tube-like setae' + [422] 'Higgins larva: Toe' + [423] 'Higgins larva: Toe: Shape' + [424] 'Higgins larva: Toe: Mucrones' + [425] 'Higgins larva: Toe: Ball and socket articulation' + ; + STATELABELS + 1 + 'absent' + 'present' + , + 2 + '[Transformational character]' + '<10' + '10-20' + '>20' + , + 3 + 'trunk cylindrical and undifferentiated on dorsoventral axis' + 'dorsal and/or ventral surface recognizable by shape, armature, or location of appendages' + , + 4 + 'absent' + 'present' + , + 5 + 'terminal' + 'subterminal' + 'in abdomen' + , + 6 + 'terminal' + 'ventral' + , + 7 + '[Transformational character: Inapplicable if mouth is not ventral]' + 'anterior' + 'ventral' + 'posterior' + , + 8 + 'absent' + 'present' + , + 9 + 'absent' + 'present' + , + 10 + 'introvert not present or not invaginable' + 'introvert invaginable' + , + 11 + '[Transformational character: Inapplicable if introvert not eversible]' + 'invaginable to part of Zone I or equivalent' + 'completely invaginable into the trunk (i.e. to the base of Zone I)' + , + 12 + 'absent' + 'present' + , + 13 + 'absent' + 'present' + , + 14 + '[Transformational character: Inapplicable if tricoscalids absent]' + 'constriction (as in loriciferans)' + 'insertion of muscles (as in kinorhynchs)' + , + 15 + '[Transformational character: Inapplicable if tricoscalids absent]' + 'six' + 'seven' + 'nine' + 'fourteen' + 'fifteen' + , + 16 + '[Transformational character: Inapplicable if trichoscalids absent]' + 'one' + 'two' + , + 17 + 'absent; trichoscalids attach directly to introvert' + 'trichoscalid plate present' + , + 18 + 'not articulated' + 'articulated' + , + 19 + '[Transformational character: Inapplicable if trichoscalids absent]' + 'simple, unornamented' + 'serrated' + 'with pectinate fringe' + , + 20 + 'absent' + 'present' + , + 21 + 'unarmed' + 'armed (whether in larva or adult)' + , + 22 + '[Transformational character: Inapplicable if Zone I lacks longitudinal rows of sclerites]' + 'no' + 'yes' + , + 23 + '[Transformational character: Inapplicable if introvert unarmoured or not eversible]' + 'concave surface directed anteriad when introvert is everted (or equivalent)' + 'concave surface directed posteriad when introvert is everted' + , + 24 + '[Transformational character: Inapplicable if no Zone I armature]' + 'single circlet' + 'multiple circlets' + , + 25 + '[Transformational character: Inapplicable if Zone I unarmed]' + 'elements as a single series, whether or not morphology differs' + 'elements organized into two or more transverse bands or series, possibly with different element morphologies within each series, but the sequence of morphologies being comparable between subsequent series' + , + 26 + '[Transformational character: Inapplicable if Zone I unarmed]' + 'not in rows' + 'in prominent rows (excepting transverse rows)' + , + 27 + '[Transformational character: Inapplicable if not in rows]' + 'discrete parallel longitudinal rows' + 'rows aligned diagonal to the anterior-posterior axis of the animal, possibly producing a quincunx' + , + 28 + '[Transformational character: Inapplicable if Zone I armature absent]' + 'continuous to end of introvert / Zone II elements' + 'gap between armature and end of introvert' + , + 29 + '[Transformational character: Inapplicable if Zone I unarmed]' + 'papillae only' + 'cuticularized spines, hooks or scalids' + , + 30 + '[Transformational character: Inapplicable if Zone I lacks sclerotized armature]' + 'elements hollow' + 'elements solid' + , + 31 + '[Transformational character: Inapplicable if Zone I unarmed]' + 'elements not elongate' + 'extreme elongation: elements more than 20 times longer than wide' + , + 32 + '[Transformational character: Inapplicable if Zone I unarmed]' + 'dead straight' + 'spinose/conical' + 'curved or hooked' + , + 33 + 'elements do not bifurcate' + 'bifurcating elements' + , + 34 + 'edentate' + 'dentate' + 'pectinate' + , + 35 + 'lacking articulation' + 'articulated joints' + , + 36 + 'setules absent' + 'setules present' + , + 37 + 'not telescopic' + 'telescopic' + , + 38 + 'elements lack hood' + 'elements with hood' + , + 39 + 'absent' + 'present' + , + 40 + 'not a multiple of five' + 'a multiple of five' + , + 41 + 'not a multiple of 25' + 'a multiple of 25' + , + 42 + 'not a multiple of six' + 'a multiple of six' + , + 43 + 'absent' + 'present' + , + 44 + 'absent' + 'present' + , + 45 + 'absent' + 'present' + , + 46 + 'pharynx (mouth cone) permanently inverted' + 'pharynx eversible' + , + 47 + '[Transformational character: Inapplicable if pharynx not eversible]' + 'pharynx eversible and invaginable' + 'pharynx permanently everted' + , + 48 + '[Transformational character: Inapplicable if neither pharynx nor introvert eversible]' + 'neither introvert nor pharynx involved in locomotion' + 'introvert or pharynx involved in locomotion' + , + 49 + '[Transformational character: Inapplicable if pharynx not eversible]' + 'complete' + 'incomplete (but beyond proximal teeth only)' + 'restricted (only as far as proximal teeth)' + , + 50 + '[Transformational character: Inapplicable if pharynx not eversible]' + 'diminutive (<2% of animal length)' + 'very large (>30% of animal length)' + , + 51 + '[Transformational character]' + 'invaginable' + 'distal region permanently everted' + 'proximal region forms non-invertible mouth cone' + , + 52 + 'round' + 'triradiate' + , + 53 + 'absent' + 'circumpharyngeal structures present' + , + 54 + '[Transformational character: Inapplicable if Zone II unarmed]' + 'small contact area (e.g. coronal spines)' + 'large contact area (e.g. Parapeytoia)' + , + 55 + '[Transformational character: Ambiguous if Zone II unarmed]' + 'continuous ring' + 'opposed bilateral series' + , + 56 + '[Transformational character: Inapplicable if circumoral structures, if present, are neither scalids nor plates]' + 'undifferentiated' + 'differentiated (e.g. Radiodonta – three or four enlarged plates)' + , + 57 + '[Transformational character: Inapplicable if no differentiated elements]' + '3 enlarged plates' + '4 enlarged plates' + , + 58 + 'absent' + 'present' + , + 59 + 'absent' + 'present' + , + 60 + 'absent' + 'present' + , + 61 + '[Transformational character: Inapplicable if radial circumpharyngeal structures absent]' + 'labile papillae or lamellae' + 'cuticularized scalids or plates' + , + 62 + '[Transformational character: Inapplicable if Zone II lacks armature]' + 'four' + 'six' + 'seven' + 'eight' + 'nine' + 'ten' + 'many' + , + 63 + '[Transformational character: Inapplicable if Zone II lacks armature]' + 'less than four times longer than wide' + 'elongate spines; at least ten times longer than wide' + , + 64 + '[Transformational character: Inapplicable if Zone II unarmed]' + 'monocuspate elements' + 'polycuspate elements' + , + 65 + 'absent' + 'present' + , + 66 + '[Transformational character: Inapplicable if spinose projections absent]' + 'proximal surface with single projection' + 'proximal surface with multiple spines' + , + 67 + 'unfused' + 'fused to introvert' + , + 68 + 'not substantially (i.e. less than 2×) wider' + 'substantially (at least 2×) wider' + , + 69 + '[transformational character]' + 'teeth gap; pharyngeal teeth not directly adjacent ' + 'no teeth gap; pharyngeal teeth directly adjacent' + , + 70 + 'absent' + 'present' + , + 71 + 'absent' + 'present' + , + 72 + '[Transformational character: Inapplicable if oral ridges absent]' + 'six' + 'eight' + , + 73 + 'absent' + 'present' + , + 74 + 'undifferentiated' + 'differentiated' + , + 75 + 'absent' + 'present' + , + 76 + 'unarmed' + 'armed (whether in larvae or adults)' + , + 77 + '[Transformational character]' + 'no elaboration of tooth point; spinose/acicular' + 'each tooth has multiple cusps, perhaps expressed as denticles or serrations' + , + 78 + '[Transformational character: Inapplicable if Zone III unarmed]' + 'lost at metamorphosis, or primarily absent' + 'retained to adulthood' + , + 79 + '[Transformational character: Inapplicable if Zone III unarmed]' + 'composed exclusively of cuticle' + 'outer covering of cuticle with central cavity' + , + 80 + '[Transformational character: Ambiguous if Zone III unarmed]' + 'radial rings or whorls' + 'haphazard distribution around full circumference of pharynx' + 'bilaterally opposed series' + , + 81 + '[Transformational character: Inapplicable if Zone III unarmed]' + 'occupying most of circumference of pharynx, perhaps with modest gap between series' + 'few longitudinal rows or series with large gap between' + , + 82 + '[Transformational character: Inapplicable if Zone III lacks armature]' + 'one' + 'strictly four' + 'four to six' + 'strictly six' + 'many' + , + 83 + '[Transformational character: Inapplicable if Zone III does not follow this configuration]' + 'five' + 'six' + 'seven' + 'eight' + , + 84 + '[Transformational character: Inapplicable if none present, or Zone III does not follow this configuration]' + 'four' + 'multiple of five' + 'multiple of six' + 'multiple of eight' + , + 85 + '[Transformational character: Inapplicable if number of elements in proximal circlet is not a multiple of five]' + 'five' + 'ten' + , + 86 + 'not reduced' + 'reduced' + , + 87 + '[Transformational character: Inapplicable if Zone III unarmed]' + 'uniform size' + 'alternate elements large then small' + , + 88 + 'elements lack prominent central spine' + 'elements with prominent central spine' + , + 89 + '[Transformational character: Inapplicable if proximal circlet not morphologically differentiated; ambiguous if reduced]' + 'straight' + 'strongly recurved (hooked)' + 'appendicules' + , + 90 + 'absent' + 'present' + , + 91 + 'not articulated' + 'articulated' + , + 92 + 'not reduced' + 'reduced' + , + 93 + 'armature not differentiated' + 'armature of proximal circlet (or few proximal circlets) is morphologically differentiated from rest of Zone III armature' + , + 94 + 'absent' + 'present' + , + 95 + '[Transformational character: Inapplicable if Zone III lacks armature, or only has 1-4 circlets]' + 'not reduced' + 'reduced' + , + 96 + '[Transformational character: Inapplicable if middle circlets absent; ambiguous if reduced]' + 'papillae or simple cone (no spine, wider than tall)' + 'single spine' + 'multiple spines' + 'pectinate' + , + 97 + '[Transformational character: Inapplicable if Zone III unarmoured]' + 'distal circlets not differentiated, or only differentiated in size or aspect ratio' + 'teeth in distal armature field morphologically distinct from teeth in other circlets' + , + 98 + '[Transformational character: Inapplicable if distal circlets not morphologically differentiated; ambiguous if reduced]' + 'papillae (no spine, wider and longer than tall)' + 'single spine' + 'multiple spines' + 'pectinate' + 'wide lamella or plate' + 'chain-like elements' + , + 99 + '[Transformational character: Inapplicable if Zone III unarmed or insufficient distal circlets to assess]' + 'approximately equal' + 'decreasing distally (distalmost elements less than half the size of proximal)' + , + 100 + 'absent' + 'present' + , + 101 + 'absent' + 'present' + , + 102 + '[Transformational character: Inapplicable if placoids absent]' + 'single undivided macroplacoid' + 'divided macroplacoids' + , + 103 + 'absent' + 'present' + , + 104 + 'absent' + 'present' + , + 105 + 'absent' + 'present' + , + 106 + '[Transformational character: Inapplicable if no apophysis for the insertion of the stylet muscle]' + 'hook shaped' + 'ventral ridge' + 'ridge shaped' + , + 107 + 'absent' + 'present' + , + 108 + 'no segment-like ring' + 'neck forms segment-like ring' + , + 109 + 'absent' + 'present (placids or lips)' + , + 110 + 'absent' + 'present' + , + 111 + '[Transformational character: Inapplicable closing apparatus absent]' + 'radial' + 'bilateral' + , + 112 + '[Transformational character: Inapplicable if cuticular neck plates absent]' + 'six' + 'seven' + 'nine' + 'twelve' + 'fourteen' + 'sixteen' + , + 113 + '[Transformational character: Inapplicable if cuticular neck plates absent]' + 'straight' + 'rectangular with straight margin and angular corners' + 'tripartite' + 'spikes present on anterior margin of plate' + , + 114 + '[Transformational character: Inapplicable if cuticular neck plates absent]' + 'fused with first trunk segment' + 'articulated' + , + 115 + 'absent' + 'present' + , + 116 + '[Transformational character: Inapplicable if amphids absent]' + 'round' + 'slit-like' + , + 117 + 'absent' + 'present' + , + 118 + 'absent' + 'present' + , + 119 + '[transformational character]' + 'absent' + 'present' + , + 120 + '[transformational]' + 'dorsal' + 'anterior' + , + 121 + '[transformational]' + 'oval/rounded' + 'elongate' + , + 122 + 'absent' + 'present' + , + 123 + '[transformational character]' + 'broad attachment to cephalic region' + 'narrow attachment to anterior edge of cephalic region' + , + 124 + 'absent' + 'present' + , + 125 + '[transformational character]' + 'subcircular' + 'elongate' + , + 126 + 'absent' + 'present' + , + 127 + 'orientation of mouth is fixed relative to main trunk' + 'flexible anterior trunk allowing mouth''s dorsal-ventral orientation to be independent of main trunk axis' + , + 128 + 'anteriormost trunk contiguous with posterior trunk; no swollen ‘head’' + 'anteriormost trunk elliptical, substantially wider than adjacent trunk' + , + 129 + 'absent' + 'present' + , + 130 + 'Frontal filaments not incorporated into lip papillae' + 'Incorporated into lip papillae' + , + 131 + 'no sensory field' + 'sensory field present' + , + 132 + '[Transformational character: Inapplicable if Cirri A absent]' + 'Mid-head' + 'Posterior part of the head' + 'First trunk segment' + , + 133 + 'absent' + 'present' + , + 134 + 'absent' + 'present' + , + 135 + '[Transformational character: Inapplicable if occular structures absent]' + 'two' + 'four' + , + 136 + 'absent' + 'present' + , + 137 + 'eye stalks absent' + 'eye stalks present' + , + 138 + '[transformational character]' + 'approximately dorsal to mouth' + 'significantly posterior of mouth' + , + 139 + 'not sclerotized' + 'sclerotized' + , + 140 + 'absent' + 'present' + , + 141 + 'pre-ocular limb pair absent or not differentiated from other limbs' + 'distinct pre-ocular limb pair' + , + 142 + 'absent' + 'present' + , + 143 + 'no material differentiation of podomeres' + 'strong differentiation of proximal from distal podomeres' + , + 144 + '[transformational character]' + 'Distal podomeres approximately uniform size' + 'Distal podomere diameter strongly reducing distally' + , + 145 + '[transformational character]' + 'lateral' + 'ventral' + 'within mouth cavity' + , + 146 + 'frontal appendages not shifted posteriorly' + 'frontal appendages shifted posteriorly' + , + 147 + '[transformational character]' + 'pre-ocular appendages not directly adjacent' + 'pre-ocular appendages adjacent to one another, with or without physical fusion' + , + 148 + '[transformational character]' + 'basally adjacent' + 'bases separated by physical gap' + , + 149 + '[transformational character]' + 'pre-ocular appendages adjacent but not mechanically fused' + 'pre-ocular appendages are mechanically fused to form a single element' + , + 150 + 'no loss of claws on differentiated protocerebral appendage' + 'differentiated protocerebral appendage claws lost' + , + 151 + 'absent' + 'present' + , + 152 + '[Transformational character: Inapplicable if ventral spine series absent]' + 'one row' + 'two rows' + 'more than two rows' + , + 153 + '[Transformational character: Inapplicable if ventral spine series absent]' + 'comparable size to shaft' + 'significantly larger than shaft' + , + 154 + 'absent' + 'present' + , + 155 + '[Transformational character: Inapplicable if accessory spines absent]' + 'accessory spines originate near base of main spine' + 'accessory spines regularly spaced along main spine' + , + 156 + 'no alternation in length' + 'alternation in length from each spine to the next' + , + 157 + '[Transformational character: Inapplicable if ventral spine series absent]' + 'comparable width of spine to podomere width' + 'spine width significantly narrower' + , + 158 + '[Transformational character: Inapplicable if spine series absent]' + 'no increase (e.g., Anomalocaris)' + 'increase (e.g., Hurdia)' + , + 159 + '[transformational character]' + 'spine series point to other appendage' + 'spine series point outwards' + , + 160 + 'absent' + 'present' + , + 161 + 'absent' + 'present' + , + 162 + 'absent' + 'present' + , + 163 + 'absent' + 'present' + , + 164 + 'absent' + 'present' + , + 165 + 'absent' + 'present' + , + 166 + 'arthrodial membranes absent' + 'arthrodial membranes present' + , + 167 + '[transformational character]' + 'cylindrical/subconical appendage' + 'laterally expanded swimming flap' + , + 168 + 'undifferentiated, or differentiated in size only' + 'structurally differentiated' + , + 169 + '[transformational character]' + 'antenniform with distinct podomeres' + 'short great-appendage' + , + 170 + '[transformational character]' + 'ambulatory' + 'sensorial' + 'masticatory, with sclerotized jaw' + , + 171 + 'absent' + 'present' + , + 172 + 'undifferentiated' + 'specialized papilla' + , + 173 + '[transformational character]' + 'ambulatory limb with distinct podomeres' + 'specialized post-antennal appendage' + , + 174 + 'absent' + 'present' + , + 175 + '[transformational character]' + 'homonomous' + 'heteronomous' + , + 176 + 'annulations continue unaltered for full length of anterior trunk' + 'annulations becoming indistinct anteriad' + , + 177 + '[Transformational character: Inapplicable if annular rings absent]' + 'unbranched' + 'branched' + , + 178 + 'absent' + 'present' + , + 179 + 'absent' + 'present' + , + 180 + 'absent' + 'present' + , + 181 + 'projections absent' + 'angular projections on anterolateral corners of first sternites' + , + 182 + '[Transformational character: Inapplicable if sternites absent]' + 'straight' + 'medially incised' + , + 183 + 'absent' + 'spinose midventral process' + , + 184 + '[Transformational character: Inapplicable if sternal plates absent]' + 'second segment an undivided ring' + 'second segment divided into sternites and tergites' + , + 185 + 'as in segments 7+' + 'differentiated' + , + 186 + '[Transformational character]' + 'one tergal plate with midventral articulation' + 'one tergal and two sternal plates' + , + 187 + 'as in segments 7+' + 'differentiated' + , + 188 + 'absent' + 'present' + , + 189 + 'not extended' + 'spinose process extending well beyond posterior segment margin' + , + 190 + 'absent' + 'lateral terminal spines present' + , + 191 + 'absent' + 'lateral terminal accessory spines present' + , + 192 + 'absent' + 'midterminal spine present' + , + 193 + 'absent' + 'present' + , + 194 + 'entire' + 'deep lateroventral notches, with or without spines' + , + 195 + 'setae absent on sternal plates' + 'setae on sternal plates' + , + 196 + 'absent' + 'present' + , + 197 + 'absent' + 'present' + , + 198 + 'absent' + 'reniform, submillimetric lamellar' + , + 199 + '[transformational character]' + 'broadly uniform trunk width' + 'substantial posteriad trend to narrower trunk' + , + 200 + 'trunk of uniform construction' + 'anterior trunk differentiated from posterior trunk by abrupt change in thickness, armature and appendage construction' + , + 201 + 'absent' + 'present at some point during ontogeny' + , + 202 + 'absent' + 'present' + , + 203 + '[Transformational character: Inapplicable if flosculi absent]' + 'flosculi, including N-flosculi and P-flosculi' + 'sensory spots' + , + 204 + 'no petals' + 'petals' + , + 205 + '[Transformational character: Inapplicable if petals absent]' + 'variable' + 'invariably eight' + , + 206 + '[transformational character]' + 'absent' + 'present' + , + 207 + 'absent' + 'two transverse rows of accentuated papillae present' + , + 208 + 'absent' + 'ring of cuticular elements post-introvert (i.e. girdling neck / cervical region) present at any point in ontogeny' + , + 209 + 'absent' + 'present' + , + 210 + 'absent' + 'present' + , + 211 + '[Transformational character: Inapplicable if lorica absent at all stages in ontogeny]' + 'no plates; lorica comprises plicae' + 'one series of plates or plicae' + 'two series (cf. Sirilorica)' + 'four series (cf. Shergoldana)' + , + 212 + '[Transformational character: Inapplicable if lorical plates absent]' + 'six' + 'seven' + 'eight' + 'ten' + 'twenty' + , + 213 + 'plates equant' + 'dorsal and ventral plates enlarged' + , + 214 + 'absent' + 'present' + , + 215 + 'absent' + 'present' + , + 216 + 'absent' + 'present' + , + 217 + 'no more than a trace of phosphorous' + 'principally phosphatic in composition' + , + 218 + '[Transformational character: Inapplicable if plates absent]' + 'essentially circular' + 'elongated parallel to body axis' + 'acutely pointed, extended perpendicular to body axis' + , + 219 + 'absent' + 'present' + , + 220 + '[Transformational character]' + 'single node' + 'single ring' + 'two rings' + , + 221 + '[Transformational character]' + 'variable within an individual' + 'constant number' + , + 222 + '[Transformational character]' + 'single central node' + 'three to six' + 'eight to ten' + , + 223 + '[Transformational character: Inapplicable if not three to six nodes]' + 'three' + 'four' + 'five' + , + 224 + 'no differentiated anterior region' + 'anterior trunk with differentiated spinose sclerites' + , + 225 + '[Transformational character: Inapplicable if trunk sclerites not arranged in transverse series]' + 'complete rings' + 'transverse rows of limited extent that do not surround trunk' + , + 226 + '[Transformational character: Inapplicable if integumentary trunk sclerites absent]' + 'irregularly disposed' + 'in transverse fields (''rows'')' + 'in longitudinal fields (''columns'')' + , + 227 + '[Transformational character: Inapplicable if plates disordered]' + 'sclerites distributed irregularly within each annulation' + 'single primary field (or row) of sclerites / large plates on each annulation' + 'two separate primary fields of large plates on each annulation, one on each margin' + , + 228 + '[Transformational character; inapplicable if plates disordered]' + 'single series of sclerites' + 'sclerites occur in pairs along each field' + 'three rows of sclerites within each field' + 'four rows of sclerites within each field' + , + 229 + '[Transformational character: Inapplicable if integumental trunk sclerites not arranged in rows]' + 'linear; each transverse row identical to last' + 'alternate transverse rows offset, so sclerites produce quincunx' + 'no exact correspondence between sclerites of one row to the next' + , + 230 + 'no differentiated class of smaller platelets' + 'large plates and smaller platelets' + , + 231 + '[Transformational character; inapplicable if platelets absent]' + 'gaps between trunk sclerites and platelets' + 'tessellate to cover entire surface of organism' + , + 232 + 'absent' + 'present' + , + 233 + 'absent' + 'present' + , + 234 + 'no separate class of diminutive sclerites' + 'standard trunk sclerites accompanied by smaller sclerites (or tumuli)' + , + 235 + 'absent' + 'radial buttresses, giving stellate appearance' + , + 236 + 'absent' + 'present' + , + 237 + '[Transformational character: Inapplicable if enlarged sclerites absent]' + 'irregular distribution' + 'arranged in regular configuration' + , + 238 + '[Transformational character: Inapplicable if transverse bands not present]' + 'one' + 'two' + 'three' + 'four' + 'five' + 'six' + 'seven' + 'fourteen' + '20 to 25' + , + 239 + '[Transformational character: Inapplicable if not regularly spaced]' + 'Occur on every annulation' + 'Occur at lower frequency' + , + 240 + '[Transformational character: Inapplicable if irregular distribution]' + 'regular' + 'variable' + , + 241 + 'absent' + 'present' + , + 242 + '[Transformational character; Inapplicable if not multiple transverse bands of sclerites]' + 'each group of dorsal elements of equivalent size' + 'size of dorsal elements varies between groups' + , + 243 + 'absent' + 'present' + , + 244 + '[Transformational character: Inapplicable if enlarged sclerites absent]' + 'wider than tall (e.g. nodes or plates)' + 'taller than wide (e.g. spines)' + , + 245 + 'absent' + 'present' + , + 246 + '[Transformational character: Inapplicable if epidermal evaginations absent or lack an acute distal terminus]' + 'absent' + 'present' + , + 247 + '[Transformational character: Inapplicable if enlarged sclerites absent]' + 'round' + 'straight' + 'rectangular with straight margin and angular corners' + 'spikes present on anterior margin of plate' + , + 248 + '[Transformational character: Inapplicable if enlarged sclerites absent]' + 'weak' + 'substantial' + , + 249 + 'absent' + 'present' + , + 250 + '[Transformational character: Inapplicable if enlarged sclerites absent]' + 'unornamented' + 'honeycomb surface ornament (cf. Nanaloricus)' + 'regular perforations (cf. Tabelliscolex)' + 'net-like holes (cf. Microdictyon)' + 'scaly' + 'tufted' + , + 251 + 'absent' + 'present' + , + 252 + 'not sclerotized' + 'sclerotized' + , + 253 + 'absent' + 'present' + , + 254 + 'absent' + 'present' + , + 255 + '[Transformational character: Inapplicable if trunk exites absent]' + 'lateral lobes' + 'setal blades' + 'simple oval paddle with marginal spines' + 'bipartite shaft with lamellar setae' + , + 256 + 'not fused' + 'fused' + , + 257 + 'absent' + 'present' + , + 258 + 'absent' + 'present' + , + 259 + '[transformational character]' + 'confined laterally' + 'present dorsally' + , + 260 + '[Transformational character]' + 'cylindrical (e.g. Hallucigenia sparsa)' + 'conical; significantly tapered (e.g. Aysheaia)' + , + 261 + 'absent' + 'present' + , + 262 + '[transformational character]' + 'spines/setae' + 'appendicules' + , + 263 + '[transformational character]' + 'arranged in rows' + 'one or two spines' + , + 264 + '[transformational character]' + 'short/equant' + 'needle-like' + , + 265 + 'absent' + 'present' + , + 266 + 'absent' + 'present' + , + 267 + 'spine absent' + 'spine present' + , + 268 + 'absent' + 'present' + , + 269 + 'absent' + 'present' + , + 270 + '[Transformational character: Inapplicable if claws absent]' + 'no enlarged base (e.g. Paucipodia''s claws)' + 'enlarged base (e.g. Onychophora claws)' + , + 271 + '[Transformational character]' + 'terminal' + 'sub-terminal' + , + 272 + 'absent' + 'present' + , + 273 + '[Transformational character: Inapplicable if branched claws absent]' + 'seperated' + 'fused' + , + 274 + '[transformational]' + 'Aysymmetrical (2121)' + 'Symmetrical (2112)' + , + 275 + '[Transformational character: Inapplicable if branched claws absent]' + 'rigid' + 'flexible' + , + 276 + '[transformational]' + 'Right-angled' + 'Curved' + , + 277 + '[Transformational character: Inapplicable if claws unbranched]' + 'not connected' + 'connected' + , + 278 + '[Transformational character: Inapplicable if claws unbranched]' + 'symmetrical' + 'asymmetrical' + , + 279 + 'direct' + 'with a flexible connection' + , + 280 + '[Transformational character: Inapplicable if claws unbranched]' + 'not perpendicular' + 'perpendicular' + , + 281 + 'undivided' + 'divided' + , + 282 + 'absent' + 'present' + , + 283 + '[Transformational character: Inapplicable if claws unbranched or base not extended]' + 'basal thickening' + 'pseudolunules' + 'lunules' + , + 284 + '[Transformational character: Inapplicable if claws unbranched]' + 'not connected' + 'connected' + , + 285 + '[Transformational character: Inapplicable if claws unbranched]' + 'symmetrical' + 'asymmetrical' + , + 286 + 'direct' + 'with a flexible connection' + , + 287 + '[Transformational character: Inapplicable if claws unbranched]' + 'not perpendicular' + 'perpendicular' + , + 288 + 'undivided' + 'divided' + , + 289 + 'absent' + 'present' + , + 290 + '[Transformational character: Inapplicable if base extension absent]' + 'basal thickening' + 'pseudolunules' + 'lunules' + , + 291 + '[transformational character]' + 'one' + 'two' + 'three' + 'four' + 'six' + 'seven' + , + 292 + '[transformational character]' + 'equal number of claws on all claw-bearing appendages' + 'variable number of claws' + , + 293 + '[transformational character]' + 'claws on single limb all identical' + 'claws on single limb differentiated' + , + 294 + 'absent' + 'present' + , + 295 + 'absent' + 'present' + , + 296 + 'absent' + 'present' + , + 297 + 'absent' + 'present' + , + 298 + '[transformational character]' + 'absent' + 'even body outline' + 'present' + 'pronounced decrease in lobe width posteriad' + , + 299 + 'first pair of trunk limbs comparable in size to subsequent pairs' + 'first pair of trunk limbs hypertrophied' + , + 300 + 'no reduction of anterior limbs' + 'anterior limbs reduced in size or absent' + , + 301 + 'absent' + 'present' + , + 302 + '[transformational character]' + 'two' + 'three' + 'five' + 'six' + , + 303 + '[transformational character]' + 'slender, simple' + 'cirrate' + , + 304 + '[transformational character]' + 'Fewer than 15 podomeres' + '15 or more podomeres' + , + 305 + 'absent' + 'present' + , + 306 + 'absent' + 'present: tubular portion of the body extends beyond the last observable appendage pair' + , + 307 + 'absent' + 'present; tubular portion of the body extends beyond the last observable appendage pair' + , + 308 + 'absent' + 'present' + , + 309 + '[transformational character]' + 'same direction as claws on other appendages' + 'rotated anteriad' + , + 310 + 'undifferentiated' + 'differentiated' + , + 311 + '[Transformational character: Inapplicable if posteriormost appendages not differentiated]' + 'appendicular tail' + 'partially fused/reduced walking legs' + , + 312 + '[Transformational character: Inapplicable if posterior trunk appendages do not form a differentiated tail]' + 'tail rami' + 'tail flaps' + , + 313 + '[Transformational character: Inapplicable if posterior trunk appendages do not form a differentiated tail]' + 'blade-like' + 'paddle-like' + 'elongate filament or spine' + , + 314 + 'absent' + 'present' + , + 315 + 'absent' + 'present' + , + 316 + 'not eversible' + 'eversible' + , + 317 + '[Transformational character: Inapplicable if caudal appendage absent]' + 'shorter than body' + 'longer than body' + , + 318 + '[Transformational character: Inapplicable if caudal appendage absent]' + 'undivided' + 'pseudo-segmented' + , + 319 + '[Transformational character: Inapplicable if caudal appendage absent]' + 'single' + 'bicaudal' + , + 320 + '[Transformational character: Inapplicable if caudal appendage absent]' + 'terminal' + 'dorso-medial' + , + 321 + '[Transformational character: Inapplicable if caudal appendage absent]' + 'smooth' + 'vesiculate' + 'bearing large warts' + , + 322 + 'absent' + 'present' + , + 323 + 'absent' + 'present' + , + 324 + '[Transformational character: Inapplicable if posterior projections absent]' + 'non-sclerotized tubulae' + 'sclerotized sclerites or setae' + , + 325 + '[Transformational character: Inapplicable if posterior projections absent]' + 'smaller' + 'larger' + , + 326 + '[Transformational character: Inapplicable if posterior projections absent]' + 'two' + 'three' + 'four' + 'six' + 'eight' + , + 327 + '[Transformational character: Inapplicable if single pair or no posterior projections]' + 'irregular' + 'bilateral arc' + 'radial ring' + , + 328 + 'absent' + 'present' + , + 329 + 'absent' + 'present' + , + 330 + 'absent' + 'present' + , + 331 + 'small' + 'large' + , + 332 + 'peripheral longitudinal and circular muscle' + 'metamerically arranged skeletal muscle' + , + 333 + 'absent' + 'present' + , + 334 + 'absent' + 'present' + , + 335 + '[Transformational character; Inapplicable if longitudinal musculature absent]' + 'anterior and posterior of trunk only' + 'successive attachment points along the body' + 'attached laterally, to chords of the epidermis' + , + 336 + '[Transformational character: Inapplicable if tegumental plates lacking]' + 'pachycycli at anterior segment margins' + 'anterior or central part of tegumental plates' + , + 337 + 'absent' + 'present' + , + 338 + '[Transformational character; Inapplicable if circular or longitudinal musculature absent]' + 'circular muscles inside longitudinal' + 'longitudinal muscles inside circular' + , + 339 + 'not reduced' + 'reduced in segment 1 only' + , + 340 + 'absent' + 'present' + , + 341 + 'absent' + 'present' + , + 342 + 'absent' + 'present' + , + 343 + '[Transformational character]' + 'intraepithelial' + 'basiepithelial' + , + 344 + 'unpaired' + 'paired' + , + 345 + 'no differentiation of nerve cords' + 'paired nerve cords differentiated in size or extent' + , + 346 + 'no fusion of nerve cords: unpaired or paired for full length' + 'merge caudally' + , + 347 + 'absent' + 'present' + , + 348 + '[Transformational character: Inapplicable if nerve cord unpaired]' + 'ventral (Alalcomenaeus, Fuxianhuia, Tardigrada)' + 'lateralized (Onychophora)' + , + 349 + 'medial interpedal commissures absent' + 'medial interpedal commissures present' + , + 350 + 'hemiganglia absent' + 'morphologically discrete condensed hemiganglia connected by medial commissures' + , + 351 + 'absent, or not occurring regularly along entire length of nerve cord' + 'present along entire length of nerve cord' + , + 352 + 'not orthogonally organized' + 'orthogonally organized' + , + 353 + 'ring commissures incomplete or absent' + 'complete ring commissures' + , + 354 + '[Transformational character: Inapplicable if leg nerves absent]' + 'not shifted anteriorly' + 'shifted anteriorly' + , + 355 + '[Transformational character: Inapplicable if leg nerves absent]' + 'unpaired' + 'paired' + , + 356 + 'absent' + 'present' + , + 357 + 'absent' + 'present' + , + 358 + 'absent' + 'present' + , + 359 + 'absent' + 'present' + , + 360 + '[Transformational character: Inapplicable if dorsal condensed brain absent]' + 'one' + 'two' + 'three' + , + 361 + 'protocerebral innervation, or innervated by circumoral nerve ring' + 'deutocerebral innervation' + 'innervation from multiple neuromeres' + 'tritocerebral innervation' + , + 362 + 'absent' + 'present' + , + 363 + '[Transformational character: Inapplicable if dorsal nerve cord absent]' + 'unpaired' + 'paired' + , + 364 + 'equal distribution of perikarya' + 'brain consisting of perikarya-neuropil-perikarya' + , + 365 + '[Transformational character: Inapplicable if brain not of cycloneuralian pattern]' + 'apical perikarya lost' + 'apical perikarya retained' + , + 366 + 'absent' + 'present' + , + 367 + 'absent' + 'present' + , + 368 + 'absent' + 'present' + , + 369 + 'no' + 'yes' + , + 370 + 'absent' + 'present' + , + 371 + 'absent' + 'present (whether or not reduced)' + , + 372 + 'absent' + 'present' + , + 373 + 'short' + 'long' + , + 374 + 'distal bulb absent' + 'distal bulb present' + , + 375 + 'absent' + 'present' + , + 376 + 'anterior gut similar diameter to mid gut' + 'expanded anterior gut' + , + 377 + 'absent' + 'present' + , + 378 + 'fused with first trunk segment' + 'articulated' + , + 379 + 'not integrated into the gonad' + 'integrated into, or flow into, the gonad' + , + 380 + 'absent' + 'present' + , + 381 + 'absent' + 'present' + , + 382 + 'circumciliary microvilli absent' + 'circumciliary microvilli present' + , + 383 + 'absent' + 'tube composed of plant debris' + 'tube comprised of chitin' + , + 384 + 'spermatozoa lack flagellum' + 'spermatozoa with flagellum' + , + 385 + 'alpha-chitin' + 'collagen' + , + 386 + 'exocuticle (middle cuticle layer)' + 'endocuticle (lowermost cuticle layer)' + , + 387 + 'composition not distinct' + 'distinct composition' + , + 388 + 'membrane without nuclei or simply with ameobocytes in association with the surface' + 'membrane containing scattered nuclei' + , + 389 + 'absent' + 'present' + , + 390 + 'absent' + 'present' + , + 391 + 'absent' + 'present' + , + 392 + 'absent' + 'present' + , + 393 + 'unornamented; smooth' + 'ornamented' + , + 394 + 'direct' + 'biphasic (or multiphasic)' + , + 395 + 'absent' + 'present' + , + 396 + '[Transformational character: Inapplicable if direct development, or larva without defined neck]' + 'larval neck smooth' + 'larval neck crenulated' + , + 397 + 'absent' + 'present' + , + 398 + 'division not evident' + 'body divided' + , + 399 + 'absent' + 'present' + , + 400 + 'absent' + 'present' + , + 401 + 'absent' + 'present' + , + 402 + '[Transformational character: Inapplicable if no buccal canal]' + 'short, linear or sacculose' + 'elongate, curving' + , + 403 + 'not present in both sexes' + 'present in both sexes' + , + 404 + 'absent' + 'present' + , + 405 + '[Transformational character; inapplicable if no Higgins larva]' + 'trunk wider than head' + 'trunk and head same width' + 'head wider than trunk' + , + 406 + '[Transformational character]' + 'thorax shorter than abdomen' + 'thorax longer than addomen' + , + 407 + '[Transformational character]' + 'plates' + 'wrinkles' + , + 408 + '[Transformational character: Inapplicable if thorax not wrinkled]' + 'irregular wrinkles' + 'zigzag wrinkles' + , + 409 + '[Transformational character: Inapplicable if no Higgins larva]' + 'plates' + 'plicae' + , + 410 + 'absent' + 'present' + , + 411 + 'absent' + 'present' + , + 412 + 'absent' + 'present' + , + 413 + 'absent' + 'present' + , + 414 + 'absent' + 'present' + , + 415 + 'row missing' + 'six or seven scalids' + 'ten or more scalids' + , + 416 + 'absent' + 'present' + , + 417 + 'absent' + 'present' + , + 418 + 'absent' + 'present' + , + 419 + '[Transformational character]' + 'tripartite locomotory setae' + 'single unit, eventually branched' + , + 420 + 'absent' + 'present' + , + 421 + 'absent' + 'present' + , + 422 + 'absent' + 'present' + , + 423 + '[Transformational character: Inapplicable if toe absent]' + 'spinous' + 'elongate with abrupt tapering' + 'stout' + , + 424 + 'absent' + 'present' + , + 425 + 'absent' + 'present' + + ; + MATRIX + 'Orstenoloricus shergoldii' ??0000-????????????????????????????????????????????????????????????????????????????????????????????????????000----??00---0-0-000000-?0-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-00000000000?210????101?02501000------0-----0-10000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---0????????????????????????????????????????????????????????????????????????????????02?1?10??1?12210?????????000-00 + 'Gastrotricha' 01?00??1?0--0--?00-00-----------0000000???0????---?10----000----0-0--??????0---------0-----000-----00-00??0?00----0-00---0-0-00????-?1?00-00000--0---00--0-----0000000-0--00-0-0-00-0-0-0-000000000000???0-0-?0000--?0000------0-----0-00000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----0??1??000-????-1?0??0?1?00?????????0????12010?00?00-00?010???010-0??000?00??0?????0-----000-0-000-000-00 + 'Lineus' ???0????????????????????????----00??000???0?????????0----000----0-0?-??????0---------0-----000-----00-00????00----0-00---0-0-00????-?1?00-00000--0---00--0-----0000000-0--00-0-0-00-0-0-0-000000000000????????0000--?0000------0-----0-00000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----0?????????????-??0??0????????????????????????????0?????0?????01???????????????????0-----000-0-000-000-00 + 'Solenogastres' ??10????????????????????????----00??000???0?????????0----000----0-0?-??????0---------0-----000-----00-00????00----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-00-0-0-0-000000000000?????????000--?1010------?-1---0-00000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----0?????????????-??0??0????????????????????????????0?????0?????01???????????????????0-----000-0-000-000-00 + 'Nereis' ?31100-0???-0--?00-00-----------0000000???0?????-???0----000----0-0--??????0---------0-----000-----00-00??0?00----0-00---0-0-00????-?1?00-00??????????0--0-----?0????0?0?????11??10-0-0-0-000000000000???0-0-?0000--?0000------0-----0-00000----0-0-0---0-0?00-0?0??0---?0??0--0------0-00---0-00----??-0???0?????00-0???000-----0??1??0000????-??0?0??10001????????0?1-?0-0?-??0?00-0??010???01?????????10??0?????0-----000-0-000-000-00 + 'Ancalagon minor' ?20000-1?1??0--?00-01?2212212?120000000???0??11?3?1?0----000----0-00??????01?2???1-???11200?002-1-10???0??0000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000??0?????0?0????1010------?-1---0-?000??-????0-?-?-?-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----?00-????-??0????????????????????????????????0-0??0?????0?????????????????????????????????????????? + 'Fieldia lanceolata' ?30000-1012?0--?00-01-?212222?1?0000000???00?0-?--??1111-0??27110-00???????1?????1-???????0????????0????0-?000----0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-11??00-0-0-0-000000000000110?????0000--?1010------?12--20-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----01211-?00-????-???????????????????????????????????0-0??0?????0?????????????????????????????????????????? + 'Scolecofurca rara' ???000-1????0--?00-01?221??12?120?0?000???00?1??????11??????2?21??00???????1?2??15-????????????????0??????1000----0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-000000000000??0?????0000--?1?10------?12--?0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---00?0-0---????????0?????????????-???????????????????????????????????????0??????0?????????????????????????????????????????? + 'Markuelia lauriei' ?20000-1?12?0--?00-01-221--121120000000110001?1?????1??1-???27?1??0???????????2???????????0??0?????0???0???000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-00000000000??000-0-?0000--??0?0?0----?-1---0-??00??-????0-?---?-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---?00-----01224??00-????-???????????????????????????????????0-0??0????????????????001-0000-0?????????????????????? + 'Shergoldana australiensis' ?10000-1????????????1?12??????120000000001?????????????????????????????????????????????????????????0???????000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110200-0-0-0-00000000000??100-0-?01?04??1?10------?12--10-0??01241???011222010000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----012(1 2)1-000-????-???????????????????????????????????0-00???????0???????????02?1?01???????????????????????? + 'Xinliscolex intermedius' 111000-?????0--?00-0?????????????????????????11??(1 2)1????????????????????????11?2?15???????????0121-100-000-0???????0-00---0-0-000?????0-00-00000--0---00--0-----0000000-0--00-11?200-0-0-0-000000000000??0????10000--?1100------0-----0-?00012(2 3)2202011212010000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----00??????-???????????????????????????????????0-00???????0?????????????????????????????????????????? + 'Shanscolex decorus' ???0?0-1?1??0--?00-01?2211-?21120000000001?????????????????????????????????????????????????????????0???????000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-00000000000??100-0-?0??????101?30----0122130-00001112???011112?10000-000--0---00000--0------0-00---0-00----0000-000---00?0-0---????????0?????????????-???????????????????????????????????0-00?????????????????????????????????????????????????? + 'Qinscolex spinosus' 1??0?0-1?1??0--?00-01?2211-12112000000000000?11???10??????????????????????011?2115????11?00?00121-?00-000-0000----0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-11?100-0-0-0-00000000000??000-0-10000--?111?30----?122130-?0001?(1 2)2?0?011212010000-000--0---00000--0------0-00---0-00----0000-000---00?0-0---????????0?????????????-???????????????????????????????????0-00?????????????????????????????????????????????????? + 'Zhongpingscolex qinensis' ?11000-1?1??0--?00-01??211-?21?20000000????????????????????????????????????????????????????????????0???????000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-00000000000??000-0-?0??????101?30----0122130-00001122???010-12?10000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-????-???????????????????????????????????0-00???????0?????????????????????????????????????????? + 'Eokinorhynchus rarus' ?11000-1?1??0--?00-0112212212?110000000???0??11?(1 2)1??0----000----0-00110-000112211(2 5 3 4)-4-01110000012???00-000-0000----0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-0000000000002100-0-10000--?101??0----????????10001222202011212010000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----0122320000????-???????0000-00?????????????????????0-00???????0?????????????????????????????????????????? + 'Eopriapulites sphinx' 110000-10???0--?00-01?2212112112000000000100?11?????1111-00027??0-002?????0112211(2 5 3)-3-01110000012???0???????000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110200-0-0-0-00000000000?2000-0-10000--?1000------0----?0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-????-???????0000-00?????????????????????0-00???????0?????????????????????????????????????????? + 'Eolorica deadwoodensis' ?10000-1?0-?1???????1?22????21210011000?????????????1??1-???2211??????????????????????????0??0?????0???????000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-?10?00-0-0-0-00000000000???0?????01102501000------0----10-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----0??????00-????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Nanaloricus mysticus' 010000-1?0-1115110211122121121210011000001001111(2 3)?310----000----0-0011121?01?????????????????02-1-?0??????1000----0-00---0-0-00?000-00-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000210110-?01102111000------0----10-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----01211-000-0???-1?00-0?1000?????????100-012120?01?00-00?011???010110?100?10211111211111-11000010001011100 + 'Armorloricus elegans' ?10000-1?12111???0211?22121121????????????001111(2 3)?310----000----0-0?11121?0??????????????????02-1-?0??????1000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-00000000000021??????01102111000------0----?0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----01211-?00-0???-1?00?0??0????????????0?????????????????????????????????????????????1111-11000000001011100 + 'Spinoloricus turbatio' ?1?000-1?1211????0211?22121121?????????????0?111(2 3)?310----000----0-0?1112010?12?112-3-0111000102-26100-000-1000----0-00---0-0-000000-0?????00000--0---00--0-----0000000-0--00-110100-0-0-0-000000000000210111?101102311?00------0----?0-??????????????????20000-000--0---00000--0------0-00---0-00----0000-000---00?0-0---????????0?????????????-??00?0?????????????????????????????????????????????????????????????1111-11000010001011100 + 'Rugiloricus carolinensis' ?1?000-1?1211151102011221211212????????????00111(2 3)?31????????????????1???0??????????????????????????0??????????????0-??????????00000-0?????00000--0---00--0-----0000000-0--00-?????0-0-0-0-00000000000?2100-0-?01101-00000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---00?0-0---????????0???????10????-??00?0?????????????????????????????????????????????????????????????1111-20010000002101300 + 'Pliciloricus corvus' 010000-1?121115110211122121121210011000001000111(2 3)2310----000----0-0011??0?01122112-1-0111000102-26200-000-1000----0-00---0-0-000???-00-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000210110-?011?1-00000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----01211-00100???-1?00-0?1000?????????100-0??????01?00-00?011???010110?100?10211111??1111-20111020102101201 + 'Urnaloricus ibenae' ???????1??????????????????????????????????????????????????????????????????????????????????????????????????????????0-???????????????????????????????????????????????????????????????????????????????????????????1?01-0???0????????????????????????????????????????????????????????????????????????????????????????????????????????0???????????????????????????????????????????????????????????????????????102?1?111?1121-20111020102101111 + 'Wataloricus japonicus' 010000-1?121115110201122121121210000000101000111(2 3)2310----000----0-001112000112211?-??????????02-22?00-000-1000----0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-0-0-00-0-0-0-000000000000210????101102-?0000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----01211-000?0???-??00-??????????????????????????????0??00??????0??????????102?1?112?1122120001120112101200 + 'Tenuiloricus shirayamai' ?1??00-???????????????????????????????????????????????????????????????????????????????????????????????????????????0-???????????????????????????????????????????????????????????????????????????????????????????1?0-?????0????????????????????????????????????????????????????????????????????????????????????????????????????????0???????????????????????????????????????????????????????????????????????102?1?11??1322220000121002001110 + 'Patuloricus tangaroa' ?1?000-1?121????????1?22121??1?????????????0?111(2 3)?31????????????????1?????????????????????????????????????????????0-??????????????????????00000--0---00--0-----0000000-0--00-??????????????????????????????????????????00------0----?0-????????????????????000-000--0---00000--0------0-00---0-00----0000-000---00?0-0---????????0?????????????-???0???????????????????????????????????????????????????????????????1322220000121112101200 + 'Scaberiloricus samba' ?1?000-1?121????????1?22121??1?????????????0?111(2 3)?31????????????????1?????????????????????????????????????????????0-??????????????????????00000--0---00--0-----0000000-0--00-??????????????????????????????????????????00------0----?0-????????????????????000-000--0---00000--0------0-00---0-00----0000-000---00?0-0---????????0?????????????-???0???????????????????????????????????????????????????????????????1222220011120112101200 + 'Franciscideres kalenesos' 021000-1???10--?00-0112211-?21210000000100000112313?1111-00026210-00100-0001122112-22111101010121-100-000-01001---0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-0-0-111010101100110010000200110--0000--?1000------0-----0-10000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-100220-0001?10??1??000--0100-012120?00000-000010???010010??00?001-0000-00-----000-0-000-000-00 + 'Antygomonas paulae' 011000-1?121124?0???112211-?21210?????11000??11231301??1-???2??1??00??0-0001?2???2-???1???1??011??-1??????001126120-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-11102020200011111011020?110-?0000--?1000------0-----0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-100210-1101?101?1??000--0100-012120?00?00-000010???010010??00?001-0000-00-----000-0-000-000-00 + 'Campyloderes cf vanhoeffeni' 011000-1?121124?1030112211-?2121021000?1000??11231301??1-???2??1??00??0-0001?2???2-???1???1??011??-???????001115120-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-111010202000111?00110200110-??000--?1000------0-----0-?0??0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-100210-?101?10??1??000--0100-012120?00?00-000010???010010??00?001-0000-00-----000-0-000-000-00 + 'Centroderes spinosus' 011000-1???1124?0???112211-?21210?????11000??1123130???1-??????1??00??0-000???????-???????1??0?????1??????001116120-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-111011202000111?0011020?110-?0000--?1000------0-----0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-100210-?101?10??1??000--0100-012120?00?00-000010???010010??00?001-0000-00-----000-0-000-000-00 + 'Echinoderes dujardinii' 011000-1?12112111?30112211-?212100100001000??11231300----000----0-00??0-0001?22??2-22111111010111--???????001116120-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-111010102000110000010200110-?0000--?1000------0-----0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-100210-1101210111??000--0100-012120?00?00-000010?1?010010??00?001-0000-00-----000-0-000-000-00 + 'Zelinkaderes klepali' 021000-10121124?0010112211-12121020000110000011231301111-00026210-00100-0001122112-22111111010141--10-000-001016210-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-11101010100011111000020?110-?0000--?1000------0-----0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-100220-1101?101?1??000--0100-012120?00?00-000010???010010??00?001-0000-00-----000-0-000-000-00 + 'Cateria gerlachi' 021000-10??112410030112211-?21211?????01000??11231301111-00026210-001?0-0001122112-22111(1 2)01010121-?00-000-001014120-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-0-0-11101021100011101000020?110-?0000--?1000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-100220-0001?10??1??000--0100-012120?00?00-000010???010010??00?001-0000-00-----000-0-000-000-00 + 'Dracoderes abei' 011000-1?121123?0030112211-?212100100011000??112313?1??2????2??1??00??0-0001?2???2-???1???11?011??-???????001123120-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-111010202000100000010200110-?0000--?1000------0-----0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-100210-?001?10??1??000--0100-012120?00?00-000010???010010??00?001-0000-00-----000-0-000-000-00 + 'Paracentrophyes anurus' 011000-1?121124?0???112211-?212100000001000??112313?0----000----0-00??0-0001?22???-22111111110????1???????001122120-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-111110202110101?01100200110-??000--?1000------0-----0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-100210-?001?10??1??000--0100-012120?00?00-0000?????010010??00?001-0000-00-----000-0-000-000-00 + 'Pycnophyes zelinkaei' 011000-1???1124?0???112211-?212100000001000??1123131???1-??????1??00??0-0001?22???-???????0??0????1???????001121120-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-111110202010100001110200110-?0000--?1000------0-----0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-100210-0001?10111??000--0100-012120?00?00-0000101?0010010?100?001-0000-00-----000-0-000-000-00 + 'Chordodes' 130020-111200--?00-01?221??12?12100000000100?1111?210----000----0-0-100-0001112325?00011100000121--00-010-0000----0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-0-0-00-0-0-0-000000000000100120-?0000--?1010------?-1---0-10100----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-0????0-000021010-00??????110-011120110?00-00?00000-00110???11?10101101?10-----000-0-000-000-00 + 'Nectonema' 130000-1?1200--?00-01-22?--?211?0000000???0??1111?2?0----000----0-0-?00-0001?1????????????0??0?????00-010-0000----0-00---0-0-00?000-?0-00-00000--0---00--0-----0000000-0--00-0-0-00-0-0-0-0000000000001000-0-?0000--?0000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-0???-0-000021010-00??????110-011120-10?00-00?00000-00110???11?1????1??210-----000-0-000-000-00 + 'Euchromadora' 130010-10??0??12001?0-----------000000000110?0-1--1?0----000----0-00100-0001122112---?111000101122-00-000-0000----1200---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-0000000000002000-0-10000--??000------0----?0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----10----000?0103?0-?000?????????00-?010???????01????0???0????????1???0??????????????0-----000-0-000-000-00 + 'Odontophora' 130010-10??0??12001?11222--221120(0 1)00000001?000-1--1?0----000----0-0?-00-00?0---------0-----000-----?0-000-00?0----1100---0-0-000000-?0-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-00000000000?2000-0-10000--?1000------0----?0-10000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----10----000?0103?0-0?00????????000?-?1?????????????????000????????????0?????????????0-----000-0-000-000-00 + 'Kinonchulus' 130010-1012?1?12011?112212112?12000000000110?0-2--111111-00027210-00200-000112?113---010-0000011--?00-000-0000----1200---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-0000000000002000-0-10000--?0000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-0103-0-0000?1110-00000--0100-011120????????????????01???????????????????0-----000-0-000-000-00 + 'Anatonchus' 130000-100-01?12001?0-----------00000000011010-1--110----000----0-00-00-0000---------0-----000-----00-010-0000----1100---0-0-000???-00-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-0000000000002000-0-10000--?0000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----00----000-0103-0-000011110-00000--0100-011120100?00-00000000-00111?00(0 1)1?10100000??0-----000-0-000-000-00 + 'Acanthopriapulus horridus' 110020-101210--?00-01222121121110000000110000112?210??????????????0?100-0001222115?21011(1 2)00??02-24100-000-0000----0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-000000000000100101?20?0????1010------?-1---0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---0101212200----100-01??-120001?0000-00???--0100-012121?01??0-00?011???010??10??0???????????0-----000-0-000-000-00 + 'Halicryptus spinulosus' 110020-1?1210--?00-012221211211201000001100??1123(1 2)101??1-???1711??00?00-0001?22??5421020-00010131-20???0??0000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000?000-0-?01012311010------?-1---0-?000??81???0-?-2-?20000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----01211-10100???-1?0?01?0000-00??????100-012121?01?10-00?011???010111??00?11111001100-----000-0-000-000-00 + 'Maccabeus' 110000-1?1210--?00-012121211211201000001100??1123(1 2)101??1-???24?2??00?00-0001?22??5321020-00010132210???0??0000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000?011111?0000--?0000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----0121?3010-0???-1?0?01?0000-00??????100-012121?01?10-00?011???110??0???0?11111000100-----000-0-000-000-00 + 'Meiopriapulus fijiensis' 110000-1?1210--?00-011221211211202000101100??1122(1 2)101??1-???2411??00?00-0001?22??5-4-010-00000141-10???0??0000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-00-0-0-0-000000000000?101112?0000--?1010------?-1---0-?11???-????0-?---?-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----0121?3000-0???-1?0?01?0000-00??????100-012111?01?10-00?111???010??0?110?001-0000-00-----000-0-000-000-00 + 'Priapulopsis bicaudatus' 110020-1?1210--?00-012122211211100001001100??11232101??1-???2411??01?00-0001?22??5121011110010131-20???0??0000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000?0?0-0-?01012311010------?-1---0-?011??-????0-?---?20000-000--0---00000--0------0-00---0-00----0000-000---0000-0---010112-200----100-0???-1?0?01?0000-00??????100-012121?01?10-00?011???010??1?????11111?01??0-----000-0-000-000-00 + 'Priapulus caudatus' 110020-1?1210--?00-012222211211100000001100??11232101??1-???2411??01?00-0001?22??5321010-00000131-20???0??0000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000?010-0-?01012311010------?-1---0-?000??81???0-?-2-?20000-000--0---00000--0------0-00---0-00----0000-000---0000-0---0101212200----10110???-1?0?0110000-00??????100-012121?01?10-00?011???010011??00?11111001100-----000-0-000-000-00 + 'Tubiluchus lemburgi' 110020-101210--?00-0122212112112020000011000?1122(1 2)1011?1-00024111100100-0001222?15?21010-000101424100-00--0000----0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-000000000000110111220000--?1010------??3--?0-?1110----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---0102111100----000?0101-12000110000-00111--0100-012111001011011?111???010??001?0?10111101100-----000-0-000-000-00 + 'Tubiluchus vanuatensis' 110020-1?1210--?00-012221211211202000001100??1121(1 2)101??1-???1711??00?00-0001?22??5-???1???0??0142410???0??0000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000??01111?0100250?0?0------???--?0-?111??91???0-?-2-?10000-000--0---00000--0------0-00---0-00----0000-000---0000-0---0101111100----000-0???-1?0?0110000-00??????100-012111?01?11????111???010??0?1?0?10111101100-----000-0-000-000-00 + 'Euperipatoides' 12110120?0-00--?00-00-----------0000000???01?0-1--110----000----0-0?-00-0000---------0-----000-----00-00--0000----0-00---0-0-000110?01100-00100-101--10--0-----000000011-301-11?200-0-0-0-0000000000001000-0-20000--?1110------??2--30-10000----0-0-0---0-0000-000-20---10101210----??????????????21110-0-000---010010---000-----00----000?0111-110010?1000210111120101220-0?0100000?00?0000??01011?0?00?00100000?00-----000-0-000-000-00 + 'Plicatoperipatus' 12?1?12????00--?00-00-----------0000000???01?0-1--1?0----000----0-0?-00-0000---------0-----000-----00-00--??00----0-00---0-0-000110?01100-00100-101--10--0-----000000011-311-11?200-0-0-0-00000000000010?????2?000--?1110??????????????1???0----0-0-0---0-00?0-000-20---10101210----??????????????21110?0-00?---01?010---????????0?????????0111-110010?10?021011112010122?????1??0??????????????????0??????????????0-----000-0-000-000-00 + 'Ooperipatellus' 12?1?12????00--?00-00-----------0000000???01?0-1--1?0----000----0-0?-00-00?0---------0-----000-----00-00--??00----0-00---0-0-000110?01100-00100-101--10--0-----000000011-311-11?200-0-0-0-00000000000010?????2?000--?1110??????????????1???0----0-0-0---0-00?0-000-20---10101210----??????????????21110?0-00?---01?010---????????0?????????0111-110010?10?021011112010122?????1??0??????????????????0??????????????0-----000-0-000-000-00 + 'Archechiniscus bahamensis' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-?????0??0?0-000-2112?0?001210----??????????????41200?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Batillipes pennaki' 111100-11???0--?00-00-----------0000000???00?0-1--1?111(1 2)-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-????????0?0???????????????????????-?-?????0??0?0-000-2112???01??1????????????????????1?01?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??0??????????????????1??????????????0-----000-0-000-000-00 + 'Batillipes phreaticus' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-????????0?0???????????????????????-?-?????0??0?0-000-2112?0??1??1????????????????????1?00?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??0??????????????????1??????????????0-----000-0-000-000-00 + 'Coronarctus yurupari' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-?????0??0?0-000-2112?0?001210----??????????????41100?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Coronarctus laubieri' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-?????0??0?0-000-2112?0?001210----??????????????41101?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Dipodarctus susannae' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-?????0??0?0-000-2112?0?001210----??????????????41201?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Wingstrandarctus unsculptus' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-?????1??0?0-000-2112?0?001210----??????????????41101?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Neoarctus primigenius' 111101?11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100311100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????0?-?????1??0?0-000-2112?0?001220----??????????????31101?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Neostygarctus oceanopolis' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????1?-?????1??0?0-000-2112?0?001110----??????????????41101?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Renaudarctus fossorius' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????1?-?????1??0?0-000-2112?0?001210----??????????????41101?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Mesostygarctus spiralis' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????1?-?????1??0?0-000-2112?0?001210----??????????????41101?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Parastygarctus renaudae' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????1?-?????1??0?0-000-2112?0?001210----??????????????41201?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Raiarctus jesperi' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-?????0??0?0-000-2112?0?001210----??????????????41101?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Styraconyx nanoqsunguak' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-?????0??0?0-000-2112?0?001210----??????????????41101?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Actinarctus neretinus' 111100-111?00--?00-00-----------000000000100?0-1--111111-0??2211?10-?00-00?1?21?1??--010-0000?--1--00-000-0000----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-0000000000001000-0--??00-??1000------0----?0-1???12???-?-10--2020000-000-2112?01001210----??????????????41101-0-000---0010212--000-----00----000-1012-0-01-0?100111111022?101100-0-00?0100-00?0000?-01011?1?00?00100000-00-----000-0-000-000-00 + 'Isoechiniscoides sifae' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-??0??0?0-000-2112?0?001210----??????????????51101?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??0??????????????????1??????????????0-----000-0-000-000-00 + 'Neoechiniscoides aski' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-??0??0?0-000-2112?0?001210----??????????????61101?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??0??????????????????1??????????????0-----000-0-000-000-00 + 'Oreella chugachii' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-00---0-0-000100111100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-?????0??0?0-000-2112?0?001210----??????????????41201?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??1??????????????????1??????????????0-----000-0-000-000-00 + 'Echiniscus testudo' 111100-11??00--?00-00-----------0000000???00?0-1--111111-0???????1??-00-0000---------0-----000-----00-000-??00----0-01---0-0-000100211100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????1?0?????0??0?0-000-2112?0?001210----??????????????41201?0-00?---10?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??0??????????????????1??????????????0-----000-0-000-000-00 + 'Multipseudechiniscus raneyi' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-01---0-0-000100211100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????1?1?????0??0?0-000-2112?0?001210----??????????????41201?0-00?---10?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??0??????????????????1??????????????0-----000-0-000-000-00 + 'Testechiniscus spitsbergensis' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-01---0-0-000100211100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????1?0?????0??0?0-000-2112?0?001210----??????????????41101?0-00?---10?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??0??????????????????1??????????????0-----000-0-000-000-00 + 'Pseudechiniscus suillus' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-01---0-0-000100211100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????1?1?????0??0?0-000-2112?0?001210----??????????????41201?0-00?---10?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??0??????????????????1??????????????0-----000-0-000-000-00 + 'Cornechiniscus imperfectus' 111100-11???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????00-000-??00----0-01---0-0-000100211100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????1?1?????0??0?0-000-2112?0?001210----??????????????41101?0-00?---10?0212--????????0?????????1012-0-01-0?10?111111022?10110?????0??0??????????????????1??????????????0-----000-0-000-000-00 + 'Milnesium berladnicorum' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0?0?????1???00-00?????????????????????????00-000-??00----0-00---0-0-0001011(0 1)1100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?0011111---??????????????41200?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0??????????????0-----000-0-000-000-00 + 'Milnesium swolenski' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0?0????0-???00-00?????????????????????????0???0????00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-????????0?0-000-20---0?001?111---??????????????41?00?0-00?---00?0212--????????0?????????????-??0??0?10?1??1??????10110???????????????????????????0??????????????0-----000-0-000-000-00 + 'Milnesium tardigradum' 111100-10??00--?00-00-----------000000000100?0-1--111111-00021110-00-00-0010---------0-----000-----00-000-??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?0011111---11?--1111---1141200?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0???0??????????0-----000-0-000-000-00 + 'Austeruseus faeroensis' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-00126110-00100-0001121?12---0---00001--25-01??013??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-?????0??0?0-000-20---0?0012112111??????????????21200?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0??????????????0-----000-0-000-000-00 + 'Mesocrista revelata' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????01??011??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?0012112121??????????????21200?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0??????????????0-----000-0-000-000-00 + 'Hypsibius dujardini' 111100-10??00--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????0120011??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-?0-?20??0?0-000-20---0?00121121212211012221101221200?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0???0??????????0-----000-0-000-000-00 + 'Beron leggi' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0??????0-???00-00?????????????????????????????0????00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-10?-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-????????0?0-000-20---0?001?11????22110??221100-21?00?0-00?---00?0212--????????0?????????????-??0????10?1??1??????10110???????????????????????????0??????????????0-----000-0-000-000-00 + 'Calohypsibius ornatus' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????0120011??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?0012112121220100-220100-21100?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0???0??????????0-----000-0-000-000-00 + 'Fractonotus verrucosus' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????01??013??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?0012112112??????????????21100?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0??????????????0-----000-0-000-000-00 + 'Cryoconicus kaczmareki' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????01??011??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?0012112121??????????????21200?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0??????????????0-----000-0-000-000-00 + 'Haplomacrobiotus utahensis' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????01??013??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?0012112112??????????????21200?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0??????????????0-----000-0-000-000-00 + 'Doryphoribius dawkinsi' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????0120013??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?0012112112220200-220200-21100?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0???0??????????0-----000-0-000-000-00 + 'Paradoryphoribius chronocaribbeus' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0???????1???00-00?????????????????????????0?100????00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-????????0?0-000-20---0?001?112112220200-220200-2??00?0-00?---00?0212--????????0?????????????-??0????10?1??1??????10110???????????????????????????0??????????????0-----000-0-000-000-00 + 'Halobiotus crispae' 111100-101??0--?00-00-----------000000000100?0-1--111111-0??2211?10-?00-00?112111??--010-0000?--1--01210130000----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-0000000000001000-0--?000--?1000------0----?0-1???0----0-0-0---0-0000-000-20---0100121121122202012220201221100-0-000---0010212--000-----00----000-1012-0-01-0?1001111110221101100-0-01?0000-00?0000?-01011?0?00000100000-00-----000-0-000-000-00 + 'Macrobiotus paulinae' 111100-1011?0--?00-00-----------000000000100?0-1--1?1111-00126110-00100-0001121312---010-00001--25-01210120000----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?00121122112101113210111321100?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0???1??????????0-----000-0-000-000-00 + 'Dactylobiotus ovimutans' 111100-1011?0--?00-00-----------0000000???00?0-1--1?1111-00126110-00100-00?1121315---010-00000--25-01??012??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?0012112211??????????????21100?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0??????????????0-----000-0-000-000-00 + 'Richtersius coronifer' 111100-10???0--?00-00-----------0000000???00?0-1--1?1111-0012?110-00200-0001121?15---010-00000--1--0120012??00----0-00---0-0-000101101100-00100-301--00--0-----?0000?010-100-0-0-00-0-0-0-00000000000010?????-???????10?0??????????????1????????-?-10-?20??0?0-000-20---0?00121122112101113210111321100?0-00?---00?0212--????????0?????????1012-0-01-0?10?111111022110110?????1??0??????????????????0???1??????????0-----000-0-000-000-00 + 'Sicyophorus rarus' ?10000-1011?0--?00-0122212112?120?0?00??0?00?112(1 2)(1 2)??1111-0??27210-002?????01?22115????1???0?001?1-1?0-0?0-?000----0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-??0?00-0-0-0-000000000000110?????01102501?00------0----?0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---?00-----00----?00-????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Sirilorica carlsbergi' ?10000-1????0--?00-0???????????????????0010?????????1??1-???2212???0?????????1-??----010-00000--1--???????0000----0-00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-(0 1)-0-00-0-0-0-000000000000??0?????01103201?00------0----?0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----0??????00-????-???????????????????????????????????0-0????????0??????????101???00???????????????????????? + 'Acosmia' ?200?0-1?0-?0--?00-0112111-?2113000000????00?0-1--110----000----0-0-1??????112212?---0-0-0??-0??????0-010-0000----0-00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-111100-0-0-0-0000000000001000-0-?0000--?101010----1-1---0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?0----000-????-??0????0000-00?????????????????????0-00?0?????0?????????????????????????????????????????? + 'Eximipriapulus globocaudata' ?10000-10???0--?00-01?2211-12?12000000????00?1123-1?1111-00027210-001?????01?2?115????1???00001(1 2 3)????0-000-0000----??00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-110200-0-0-0-0000000000001?00-0-10000--?101030----012(1 2)-30-1???0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---100-----?0----000-????-???????????????????????????????????0-0??0?????0?????????????????????????????????????????? + 'Laojieella thecata' ?20000-1?12?0--?00-01?2212212?11000000????0??11?1?1?0----000----0-00??????01?2???5-???????0??01???1????0??0000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000??0?????0000--??000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---110112-1?0----?00-????-???????????????????????????????????0-0??0?????0?????????????????????????????????????????? + 'Ottoia prolifica' ?10000-1?12?0--?00-01?2212212112010000????0??11?1(1 2)1?1??1-?002711??01??0-0001?22??5-???11110?10132410???0??1000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000?00?????0000--??000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---01111111?1215-?00-0???-??0????0000-00????????0-0?????????00-00?0?????0?????????????????????????????????????????? + 'Ottoia tricuspida' ?10000-1?12?0--?00-01?2212212112010000????0??11?1(1 2)1?1??1-?002711??01??0-0001?22??5-???11110?10132410???0??1000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000?00?????0000--??000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---01111111?1215-?00-0???-??0????0000-00????????????????????00-00?0?????0?????????????????????????????????????????? + 'Paratubiluchus bicaudatus' ?10000-1????0--?00-012221211??????????????0??11??21?0----000----0-00???????1?2????????????0??01????????0???000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-0-0-00-0-0-0-000000000000?10?????0000--??000------0-----0-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---01?112-1?0----?00-????-????????????????????????????????????-0????????0?????????????????????????????????????????? + 'Priapulites konecniorum' ?100?0-1?1??0--?00-012?2?2112?12000000????00?11??(1 2)1?1??????????????????????????????????????????????????????000----??00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-000000000000100?????0000--???00------0----?0-????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---01?11??1?0----?00-????-???????????????????????????????????????0??????0?????????????????????????????????????????? + 'Selkirkia columbia' ?(1 2)0000-1?12?0--?00-01?2212212111010000????0??11?1(1 2)1?1??1-???2711??00??????01?22??5-???10-10?10131-1????0??0000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-?1??00-0-0-0-000000000000??0?????0?0????101030----?121-30-??????-????0-?-?-?-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---0?0-1---?0----?00-0???-???????????????????????????????????0-0??0?????2?????????????????????????????????????????? + 'Paraselkirkia sinica' ?(1 2)0000-1????0--?00-01?22122121110?0000????0??11?1?1?1??1-???2?11??00??????01?22??5-???1???0??0????1????0??0000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-?1??00-0-0-0-000000000000?10???????0????101030----?121-30-??????-????0-?-?-?-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---0?0-1---?0----?00-0???-???????????????????????????????????0-0??0?????2?????????????????????????????????????????? + 'Xiaoheiqingella peculiaris' ?20000-1????0--?00-0122212112?11000000?1100??11???1?1??1-???2411??00???????1?22???????????0???1????????0???000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000?00???????0??????00------0----?0-??????-????0-?-?-?-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---11?112-1?0----?00-????-???????0000-00?????????????????????0-0??0?????0?????????????????????????????????????????? + 'Xystoscolex boreogyrus' ?(1 2)00?0-1????0--?00-01?22122?2?11000000????0??11?1???1??1-???2????????0????01?2???????????????0?????????????000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-11??00-0-0-0-00000000000??00???????0????1?1?1?????012?????????0----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---00?0-0---000-----???????00-0???-1??????????????????????????????????0-0????????0?????????????????????????????????????????? + 'Chalazoscolex pharkus' ?21000-1?1??0--?00-01?22????2?11000000????0?????(1 2)????????????????????0?????1?2?11?????1????????????????0???000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000?00?????0?0????1?1?1?????11221312?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---00?0-0---01?1111????????00-0???-1??????????????????????????????????0-0????????0?????????????????????????????????????????? + 'Louisella pedunculata' ?21000-1?12?0--?00-01?2212211?110?0???????0??11?1(1 2)1?1??1-?002721??01?0????01?2???5-???10-00?001(2 3 4)??1????0??1000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000?10?????1?0????101030----0122130-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---01111111?0----?00-0???-??????????????????????????????????00-00?0?????0?????????????????????????????????????????? + 'Corynetis brevis' ?(1 2)0000-1012?0--?00-00-----------0000000???0??1111(1 2)1?1111-00027210-0010????0112?215---010-00000111-1?0-000-0000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110?00-0-0-0-000000000000?00?????0?0????101?11311-012212???0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---01?11111?0----?00-0???-???????????????????????????????????0-00?0?????0?????????????????????????????????????????? + 'GUANDUSCOLEX minor' ?20000-10???0--?00-01??21?????????00??????00011?1(1 2)1????????????????0100-0001?2?115-???1?????001???1?0-000-0000----??00---0-0-000???-00-00-00000--0---00--0-----0000000-0--00-11?100-0-0-0-00000000000?100????10?0????1?1111312-01223212?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?1211-?00-0???-???????????????????????????????????0-0??0?????0?????????????????????????????????????????? + 'MAOTIANSHANIA cylindrica' ?30000-1????0--?00-01??2122???????????????0??11?1?1????????????????0?0????01?2????????10-0??00????????????0000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-11??00-0-0-0-000000000000?00?????0?0????1011112222012--???00000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?1221-?00-0???-???????????????????????????????????0-00?0?????0?????????????????????????????????????????? + 'PALAEOSCOLEX piscatorum' ?30000-??????????????????????????????????????????????????????????????0????0?????????????????????????????????????????00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-11??00-0-0-0-00000000000???0?????0?00-??101121213-0123131200000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0010-0---000-----???????00-0???-???????????????????????????????????0-00?0?????0?1?????11????????????????????????????????? + 'SCHISTOSCOLEX umbilicatus' ??0000-??????????????????????????????????????????????????????????????0??????????????????????????????????????????????00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-11??00-0-0-0-00000000000???00-0-?0??????101111212-01231?1200000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0010-0---000-----?11233?00-0???-???????????????????????????????????0-00???????0?1???????????????????????????????????????? + 'SCATHASCOLEX minor' ?3?000-1????0--?00-01-211--22?12000000????0??1???(1 2)1?1111-?002711??00?0????01?2????-???10-00?0013???????0???000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-000000000000100?????0?00-??1011112?2?01232?0-00000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?12(1 2)32000-0???-???????????????????????????????????0-00???????0?????????????????????????????????????????? + 'WRONASCOLEX antiquus' ?30000-1????0--?00-01-?11--?2?1?????00???????1???(1 2)???????????????????0?????????????????????????????????????000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-11??00-0-0-0-00000000000??00?????0?0????101111212-0122131200000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?12232?00-0???-???????????????????????????????????0-00???????0?????????????????????????????????????????? + 'WRONASCOLEX iacoborum' ?300?0-??????????????????????????????????????????????????????????????0?????????????????????????????????????000----??00---0-0-00????-?0-00-00000--0---00--0-----0000000-0--00-11??00-0-0-0-00000000000???0?????0?0????10111122230123131?00000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---00?0-0---0?????????????????0???-???????????????????????????????????0-00???????0?????????????????????????????????????????? + 'YUNNANOSCOLEX magnus' ?(1 2)100?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????00000--0---00--0-----0000000-0--00-11?100-0-0-0-000000000000??0????10?0????1?1111212-0122320-?0000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?1211-?00-0???-???????????????????????????????????0-00???????0?????????????????????????????????????????? + 'MAFANGSCOLEX yunnanensis' ?31000-101??0--?00-01122122121120?0000?00100?11?1(1 2)??1111-00027110-0010????01222115-22010-100001(3 4)2(3 4)1?0-0?0-1000----??00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-110100-0-0-0-000000000000100????10000--?1111111---012--(1 3)1210000----0-0-0---0-0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?1221-000-0???-???????0000-00?????????????????????0-00?0?????0?????????????????????????????????????????? + 'Cricocosmia n. sp.' ?31000-10???0--?00-01?221??12?12000000????00?11?1(1 2)101111-00027(1 2)10-0010??0?0122?115???????????0131-1?0-00(- 0)-?000----??00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-11?100-0-0-0-0000000000001(0 1)0????10?00-??11111122220121-10-10001222101011112010000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?1221-?00?0???-???????????????????????????????????0?00?0?????0?????????????????????????????????????????? + 'CRICOCOSMIA jinningensis' ?31000-10???0--?00-01?2212212112000000????00011?1(1 2)101111-00027(1 2)10-00?00-000122?115?3-01??????0131-1?0-000-1000----??00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-111100-0-0-0-000000000000110????10?00-??1??1??????012??1??10001221101011112030000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?1221-?00-0???-???????????????????????????????????0-00?0?????0?????????????????????????????????????????? + 'TABELLISCOLEX hexagonus' ?31000-10???0--?00-01?2212?12?12000000????00011?1(1 2)100----000----0-00100-0001?2?115?????0-?00?01?2(2 3 4)1?0-000-?000----??00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-111100-0-0-0-000000000000110????10?0????1?11112?220121-???10001222101010-12030000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?1221-?00-0???-???????????????????????????????????0-00?0?????0?????????????????????????????????????????? + 'Tylotites petiolaris' ?(2 3)(0 1)000-1?1??0--?00-01??212212?12000000??0000011?1(1 2)1????????????????0100-0001?2?115???????????01(2 3)???????00-1000----??00---0-0-000000-00-00-00000--0---00--0-----0000000-0--00-111100-0-0-0-000000000000110?????0?0????1?????????012??3???????(- 2)(- 8)?(- 1)(- 0)(- 1)0(- 2)?(- 2)(- 1)(- 2)0(- 1)0000-000--0---00000--0------0-00---0-00----0000-000---0000-0---000-----?1221-?00-0???-???????????????????????????????????0-0??0?????0?????????????????????????????????????????? + 'Xenusion' ?(2 3)11??????????????????????????????????????0????????????1-???-????????0??????????????????????????????0-0????000----??00---0-0-000????00-00-00000-1?1--?0--0-----000000010-100-110100-0-0-0-00000000000?100????10000--?1??0??????????????????1222101011?12010000-000-2121-10000--0------0-00---0-00----00-0-000---0100-0---000-----?0----????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Hadranax' ?(2 3)11?????????????????????????????????????????????????????????????????0??????????????????????????????????????00----???????????????????????????????????????????????????01?-???-12?100-0-0-0-00000000000?1?0????20000--?1?103??????12??30-???0124210(1 2)010-(1 2)?0-0?00-000-20---10?00--0------0-00---0-00----00-0-??0---0??0-????0?????????????????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Aysheaia' ?21100-1011?1?11001?0-----------000000000100?111?11?1111-00012(1 2)10-00?0????????????????????????????????????0000----??00---0-0-000000-00-00-00100-101--10--0-----110000010-100-110100-0-0-0-000000000000100????20000--?101030----02221?0-1???0----0-0-0---0-0000-000-2112100001220----??????????????61100-0-000---0010212--000-----?0----?00-0???-???????????????????????????????????0-0010?????0?????????????????????????????????????????? + 'Siberion' ?(1 2)11?0-???-?????????0-----------0000000???00?0-?--1?1????0001????1???0??????????????????????????????0-0?--??00----??00---0-0-000????00-00-00100-102-1?????????11?0000010-100-110100-0-0-0-00000000000?10?????2?000--?1??0????????????0-???00----0-0-0---0-0000-000-?????0?00??????????????????????????0-0-000---0??0?????000-----??????????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Onychodictyon ferox' ?(1 2)1100-101??0--?00-00-----------0000000---00?0-1--1????--000----?-0-?0????????????????????0?????????0-0???0000----??00---0-0-000100?01100-00100-101--112-0-0--?10000?010-100-120100-0-0-0-000000000000100????21?0????1?1030----0?221?0-1?00122?1?10111?2041000-000-?121-0000121???????????????????21200-0-000---0010212--000-----?0----?0??????-???????????????????????????????????0-0010?????0?????????????????????????????????????????? + 'Diania' ?(2 3)11?0-?????0--?00-00-----------0000000???0??0-1--1??????????????????0?????????????????????????????????????000----??00---0-0-0??000-00-00-00000-101--00--0-----000000010-100-121100-0-0-0-0000000000001?0?????0000--?1?1030----01221(2 3)0-1000121?101010-11010000-000-10---00001110----??????????????-1100-0-000---010010---000-----?0----000-????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Paucipodia' ??11?0-0?0-?????????0-----------0000000???0??0-?--1????1-???-????????0?????????????????????????????????0--??00----??00---0-0-000000-0??00-00000-101--10--0-----?00000010-100-110100-0-0-0-00000000000010?????10000--?1?00------0-----0-10000----0-0-0---0-0000-000-10---000011?0----??????????????(1 2)2100-0-0?0---0100?0---0?????????????????????-???????????????????????????????????????0??????0?????????????????????????????????????????? + 'Cardiodictyon' 1311?0-?0???0--?00-00-----------0000000???0?00-(- 1)--1????(- 1)-??????(- 1)???0?00-000?????????????????????????0-00--?000----??01111010-001000-01100-00?00-101--10--0-----000000010-200-121100-0-0-0-000000000000110????10000--?1?00------0-----0-100012{1 2}2101010-(1 2)20(2 3 4)?000-000-10---00001110----??????????????21100-0-01121-010010---000-----?0----000?????-???????????????????????????????????????0??????0?????????????????????????????????????????? + 'Microdictyon' ?21100-000-?0--?00-00-----------0000000---0??0----1????1-???---1???-?0???????????----010-0?00?--1--?0-00--0?00----??00---0-0-010000-0??00-00000-101--10--0-----?00000010-100-121100-0-0-0-000000000000100????10000--?1?00------0-----0-10001-22101010--2041000-000-10---00001110----??????????????2??00-0-000---010010---000-----?0----?00-????-???????????????????????????????????0-0?00?????0?????????????????????????????????????????? + 'Onychodictyon gracilis' ??11?0-?????????????0-----------0000000???00?0-?--1????1-???-????????0??????????????????????????????0-0?????00----??0????0?0-0???00?0????????????????????????????????01?-?00-1?0100-0-0-0-0000000000001??????2??0????1?103???????221?0-1??0122?101011??20(2 3 4 5 6)1?00-000-2????00001210----??????????????2??00-0-000??-0100?12--000-----??????????????-???????????????????????????????????????0??????0?????????????????????????????????????????? + 'Thanahita distos' 12?1????????????????0-----------0000000???0???????1?????????????????????????????????????????????????0-0?--??00----??????????????????0????????????????????????????????01??????0-0-00-0-0-0-00000000000011?????-???????1?00------0----?0-1???1?2?2010(1 2)0-?106???0-000-10---00001110----??????????????22100?0-01??1-01?010---??????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Orstenotubulus' ??11??????????????????????????????????????????????1?????????????????????????????????????????????????????--??00----??????????????????0????????????????????????????????01?-???-12?200-0-0-0-00000000000?1??????2??0????1?00------0----?0-1???122???202????0?0?00-000-10---1?1???????????????????????????0-0-??0??-0??0?????0?????????????????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Tritonychus phanerosarkus' ???1????????????????0-----------0000000???????????1?????????????????????????????????????????????????0-0?--??????????????????????????0????????????????????????????????01?-???-1??200-0-0-0-00000000000????????2???????1?00------0----?0-1??????????0??????????0-000-10---101?121???????????????????3?100?0-??????0??????????????????????????01??-???0????????????????????????????????????????????????????????????????????????????????????? + 'Carbotubulus' ?(1 2)?1????????????????0-----------0000000???0???????1?????????????????????????????????????????????????0-0?????00----??????????????????0????????????????????????????????01?-???-0-0-00-0-0-0-00000000000?11?????-?????????00------0----?0-???????????0??????????0-000-10---0?0???????????????????????????0?0-?????-00?0???--??????????????????0???-????????????????????????????????????????????????????????????????????????????????????????? + 'Hallucigenia sparsa' 131100-0?0-?0--?00-00-----------0000000---0??0----1?1111-0002721?10010?????112??2?????10-00?0012??1?0-00--0000----??00---0-0-011000-01100-00000-101--?0--0-----000000010-{1 2}00-0-0-00-0-0-0-0000000000001100-0--0000--?1100------0-----0-1000122?1010212-2050000-000-10---00001110----??????????????22100-0-01021-00?010---000-----?0----?00-????-???????????????????????????????????0-0?00?????0?????????????????????????????????????????? + 'Hallucigenia fortis' ?211?0-0?0-?????????0-----------0000000??????0-?--1????1-???-????????0??????????????????????????????0-0?--??00----??01111010-011000-01100-00000-101--?0--0-----000000010-{1 2}00-121?00-0-0-0-00000000000011?????10000--?1?00------0-----0-1000122?1010212-20(1 2 3 4 5 6)0000-000-10---000?1?10----??????????????2??00-0-01011-0?0010---0?????????????????????-???????????????????????????????????????0??????0?????????????????????????????????????????? + 'Hallucigenia hongmeia' ??11????????????????0-----------0000000???????????1??????????????????0??????????????????????????????????--??00----??????????????????0????-???????????????????????????01?-??0-12??00-0-0-0-00000000000?1??????10000--?1?00------0-----0-1000122?1020212-20(2 4)0?00-000-10---00001110----??????????????11-00-0-??0??-0?00?0---0?????????????????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Facivermis yunnanicus' ?2?1?0-?????????????0-----------0000000??????0-?--1??????????????????0??????????????????????????????0-0?--??00----??00---0-0-0?0?00?01100-00000-101--?0--0-----?0000?010-{1 2}00-12?100-0-0-0-00000000000011?????2?000--?1?10??????0122130-1???0----0-0-0---0-00?0-000-1111210001?10----??????????????11-00?0-00?32-0110?0---??????????????????????-???????????????????????????????????????0????????????????????????????????????????????????? + 'Luolishania' ?211?0-?????????????0-----------0000000???0??0-?--1????1-???-????????0??????????????????????????????0-00--??00----??011??0-0-0?1100?01100-00000-101--10--0-----?0000?010-{1 2}00-121100-0-0-0-00000000000011?????2??0????1??0????????????0-1??0123?2?20212?20?0000-000-1111210001210----??????????????11-00-0-00132-0?0010---0?????????????????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Ovatiovermis cribratus' ?2?1?0-?0???????????0-----------0000000???0??0-?--1?????????????????10?????112???5---010-00000111-1?0-00--0000----??00---0-0-010?00?01100-00000-101--?0--0-----?0000?010-{1 2}00-12?100-0-0-0-00000000000011?????1???????1100------0----?0-1???1?2?2??021??20??0?0-000-1111200001210----??????????????11100?0-00?42-00?010---??????????????????????-???????????????????????????????????????0????????????????????????????????????????????????? + 'Collinsium' ?2?1?0-?????????????0-----------0000000???0???????1??????????????????0??????????????????????????????0-00--??00----??011??0-0-010100?00-00-00000-101--10--0-----?0000?010-{1 2}00-12?100-0-0-0-00000000000011?????2???????1100------0----?0-10001?5?2?20212?20(2 4)?0?0-000-?111200001210----??????????????11-00?0-00?42-011010---??????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Collinsovermis monstruosus' ?2?1????????0--?00-00-----------0000000???0??0-?--1??????????????????0??????????????????????????????0-00--??00----??011??0-0-0??100?0??00-00000-101--10--0-----??0000010-{1 2}00-12?100-0-0-0-00000000000011?????????????1100------0----?0-1???1?3?1?20212?20??0?0-000-21112???01210----??????????????11?00?0-00?42-0??0?0---??????????????????????-???????????????????????????????????????0????????????????????????????????????????????????? + 'Emu Bay Collins monster' ??11????????0--?00-00-----------0000000???????????1?????????????????????????????????????????????????0-0?--??00----??011?????????????0?????00?????????????????????????01?-{1 2}00-12?100-0-0-0-00000000000?11????????0????1?00------0----?0-1???123???20212?20?0?00-000-?111200001210----??????????????11-00?0-00132-0??0?????0?????????????????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Acinocricus' ???1????????0--?00-00-----------0000000???????????1?????????????????????????????????????????????????????????00----???????????????????????????????????????????????????010-{1 2}00-12?100-0-0-0-00000000000011?????????????111030----0122130-????1?6?1?20212?2?????0-000-?1112????????????????????????????????0-00?32-???0???????????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Antennacanthopodia' 1211?1??????0--?00-0??????????????????????0??0-(- 1)--1????????????????????????????????????????????????????????000----??00---0-0-000??0-01100-00100-101--10--0-----000000011-?00-????00-0-0-0-000000000000100?????0000--?1?1030----01221?0-1???0----0-0-0---0-0000-000-10---?0101?10----??????????????11-00(- 0)0-000---0110?1113000-----?0----????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Helenodora' ??11????????0--?00-00-----------0000000?????????????????????????????????????????????????????????????0-0?--??00----??00---0-0-0?0????0?????00100-101--??-?0-0-???0000?01?-??1-1???00-0-0-0-0000000000001??????2?000--?1?10????????????0-1??00----0-0-0---0-0000-000-20---10?01?10----??????????????211?0?0-0?0??-0??0?????0?????????????????????-??????????????????????????????????????????????0?????1???????????????????????????????????? + 'Tertiapatus dominicanus' ?2?1?12?????????????0-----------0000000???01??????1?????????????????????????????????????????????????0-0?--??00----??00---0-0-000110?01100-00100-101--?0--0-----?0000?011-??1-11??00-0-0-0-00000000000?10?????2?000--????0??????????????????0----0-0-0---0-00?0-000-?0---?0?0??????????????????????????0?0-00?---01?0?0---??????????????????0???-????????????????????????????????????????????????????0???????????????????????????????????? + 'Siberian Orsten tardigrade' ?111?0-?0??????????????????????????????????0????????1??1-0???????1??????????????????????????????????????????00----??00---0-0-000????0??????0???-?????????????????????010-??0-0-0-00-0-0-0-00000000000?10?????-??0????1?00------0----?0-1?????-?---0?0-??0?0000-000-20---000012111?12??????????????21200?0-00?---0010?12--0?????????????????????-??????????????????????????????????????????????0?????1???????????????????????????????????? + 'Youti yuanshi' 1?11?12????0??????????????????????????????00?0-?--1?????????????????-0?????0---------0-----000-----?0-0?--??00----??10---0-0-00010??011????01???(1 2)0???????????????????01?-(1 2)00-????00-0-0-0-000000000001?0???????000--???????????????????????0----0-0-0---0-00???000?20---0000????----????????????????????0-00?---???????????????????????????????-????1??????1????????10110??????????????0????????????????????????????????????????????????? + 'Megadictyon' 1111??????????????????????????????????????0??0-?--1?1???????????????2?????012???1????????????0??????0-0?--??00----??00---0-0-0?0100-0??00-00100-101--01??0-0--?100000010-100-11?100-0-0-0-00000000000110?????10000--?0000------0-----0-?0000----0-0-0---0-0000-000-?121-0000?21??????????????????????00-0-000---00?0?????0?????????????????????-???????????????????????????????????????1??????0?????????????????????????????????????????? + 'Jianshanopodia' ?111?0-?????0--?00-00-----------0000000---00?0-?--1?1?1?????2721??0020????0122??1?????10-0??0014????0-0?--0?00----??00---0-0-000???-0??00-00100-10????11?0-0--?100000010-100-12?100-0-0-0-00000000000110?????10000--?0000------0-----0-?0000----0-0-0---0-0000-000-2121-0000??1??????????????????????00-0-000---0010-11220?????????????????????-???????????????????????????????????????1??????0?????????????????????????????????????????? + 'Cucumericrus' ??11????????????????????????????????????????????????????????????????????????????????????????????????????????00----?????????????????-???????1?????????????????????????1(1 2)????????????????????????????????????????????????????????????????0??????????0????????1?110111?0---?0??0--0------0-00---0-00----00?1???????0???-??????????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Kerygmachela' ?211?11?????0--?00-00-----------0000000---00?0----1?1????000?--1?10-?????????????-?--010-00000--1--?0-0?--0?00----??10---0-0-000100-?1110100100-101--11--0-0---110000010-100-120100-0-0-0-000000000001100????20000--?1000------0-----0-0000124-1??010--10-00111010120---00000--0------0-00---0-00----00101000---0010-1113000-----?0----000-01?1-1???1??10?????????????110??????????0-0??0?????0?????????????????????????????????????????? + 'Pambdelurion' ?111?12?????????????1?211??22?12010000????00?0----1?1?21-000271?11002?????0122?313---0111??000131--?0-00--??00----???0---0-0-000100-????0?00100-2022111--0-0---1?0000010-100-1???00-0-0-0-00000000000110???????000--?0000------0-----0-00000----0-0-0---0-00111010120---00000--0------0-00---0-00----00101000---0???-????0?????????????????1111-???0??????????????????????????????????????????0?????????????????????????????????????????? + 'Omnidens qiongqii' ??11????????0--?00-01-211??2?112010000????00?0-1--??1221-000271?12002?????01222313---?11110000131--?0-00--?000----????????????????????????00100-(1 2)???(- 1)?0--0-----000000???-?0?-????????????????????????????????????????????------???--?0-???????????0????????????????????????????????????????????????????????????????????????????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Parapeytoia' ??11??????????????????????????????????????00?0-?--??12?2(2 1)000????12??????????????????????????????????????????00----????????????00??????????111????????1???????????????11???0?????????????????????????????????????????????0??????????????0????????-?0?????0??1?110111-?????0??0--0------0-00---0-00----0????????-10???-??????????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Kylinxia' ?111?13?????????????0-----------0000000???00?0-?--1?????????????????????????????????????????????????0-00--??00----??1112101?-?00?00-0121111111012122111211112120000001-0-100-????110??????????????????2??????-??????????0??????????????0???????---0?0-??0??1?1100?1-0---00000--0------0-00---0-00----00???01?--100?1-1122??????????????????????-???????????????????????????????????????0????????????????????????????????????????????????? + 'Isoxys' ?111?13?????????????0-----------0000000???00?0-?--1??????????????????0??????????????????????????????0-00--??00----??112???????00???-01?1111111??212211???????????????1-???00-?????0-0-0-0-00000000000??0???????????????00------0----?0-0????????-?0????????1?12???1-0---00000--0------0-00---0-00----00????????10???-??????????????????????????-???????????????????????????????????????0????????????????????????????????????????????????? + 'Stanleycaris' 1111?13?????????????0-----------0000000???00?0-?--1?12121000????12??????????????????????????????????0-0?--??00----??11121010-000100-011111111112202211112121221100011020-100-0-0-10-0-0-0-00000000000120?????-?000--?0000------0-----0-00000----0-0-0---0-00?120002-0---00000--0------0-00---0-00----00?1200?--?00?1-111???????????????????????-???????10?1???????????1????????????????0????????????????????????????????????????????????? + 'Opabinia' 1111?130?0-?????????0-----------0000000???00?0----1?12?1-0002???11???????????-??????????????????????0-00--??00----??11111010-000?00-0121110011??202111???0-?--1?0000?010-100-1?0?10-0-0-0-00000000000110???????000--?1000------0-----0-000012--1-?010--10-000110102?0---00000--0------0-00---0-00----00101000---01?1-11120?????????????????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Utaurora' ??11?13?????????????0-----------0000000????0?0-?--1?????????????????????????????????????????????????0-0?--??00----???????0-0-?00???-01?111??1???20211?????????????0??0????0???????0-0-0-0-00000000000?10????????????????0??????????????0????????-?0?????0??0?110102?????00000--0------0-00---0-00----00?0100?--?0??1-1111??????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Caryosyntrips camurus' ??11????????????????0-----------0000000???????????1?????????????????????????????????????????????????????????00----????????????????????????1?110?2021111??0-?--1100001?????0?????????????????????????????????????????????0??????????????0??????????0????????????????????????????????????????????????????????????????????????????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Amplectobelua symbrachiata' ?111?12?????????????0-----------0000000???00?0-?--1?1??22110?????(1 2)??????????????????????????????????0-00--??00----??111?10111?0????-01111?1111112?2211121111212011110020-100-?????0-0-0-0-00000000000?20?????-?000--??000------0----?0-0???0----0-0-0---0-00?11001?-0---00000--0------0-00---0-00----00?1201?--?00?1-1111??????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Anomalocaris canadensis' ?111?120?0-?????????0-----------0000000???00?0----1?1212111027(1 2)1120??????????1??????????????????????0-00--??00----??11111011110??00-011111111101202211121111212010010020-100-0-0-00-0-0-0-00000000000120?????-?000--??000------0----?0-????0----0-0-0---0-000110002-?---0000?--0----??????????????---00112010--?0011-11110?????????????????1???-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Cambroraster falcatus' ?111?11?????????????0-----------0000000???00?0-?--1?12122000?????2??2?????012???1???????????????????0-0?--??00----??11112111200??00-01111211111220?2?11121201?1010010020-100-0-0-00-0-0-0-00000000000?20?????-?000--??000------0----?0-0???0----0-0-0---0-00?110002-0---00000--0------0-00---0-00----00??101?--?00?0-1121??????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Hurdia victoria' 1111?1?0?0-?0--?00-00-----------0000000???00?0----1?1212200027(1 2)112002?????0122?113???010-1000013---?0-00--?000----??11112111200??00-01111211111220???1112120121010010020-100-0-0-?0-0-0-0-00000000000??0?????-?000--??000------0----?0-0???0----0-0-0---0-000110?02-0---000?0--0------0-00---0-00----001?1010--?0010-11220?????????????????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Cf. Peytoia' ??11????????????????0-----------0000000?????????????????????????????????????????????????????????????????????00----????????????????????????1111??2021111?2120121100011?????0????????????????????????0????????????????????0?????????????????????????0????????????????????????????????????????????????????????????????????????????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Peytoia nathorsti' ?111?120?0-?????????0-----------0000000???00?0-?--1?1212200027(1 2)1120??????????1???????????????0??????0-0?--??00----??11112011210??00-011112111111202?11112120121010010020-100-0-0-00-0-0-0-00000000000120?????-?000--??000------0----?0-0???0----0-0-0---0-000110?02-0---00000--0------0-00---0-00----00112010--?0010-0---0?????????????????????-??????????????????????????????????????????????0?????????????????????????????????????????? + 'Aegirocassis benmoulai' ?111????????????????0-----------0000000???00??????1???1?????????????????????????????????????????????????--??00----??11112011??0????-??????11110?20???11121201?100?000020-100-0-0-10-0-0-0-00000000000??0?????-?000--??000------0----?0-0???0----0-0-0---0-00?110102-0---00000--0------0-00---0-00----00?120??--?00?0-0---??????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Lyrarapax unguispinus' ?111?12?????????????0-----------0000000???00?0-?--1?12?2201?????12???????????????????????????0??????0-0?--??00----??11111011??0??00-?111111111?1202211111111212001110020-100-0-0-10-0-0-0-00000000000120?????-?000--??000------0----?0-0???0----0-0-0---0-00?110002-0---00000--0------0-00---0-00----00?1211?--?00?1-111???????????????????1???-???????10??1?1??????0?110???????????????????????????????????????????????????????????????? + 'Schinderhannes' ?111?1??????????????0-----------0000000???00?0-?--1?1?11-0???????????0??????????????????????????????0-0?--??00----?????????1??0????-0111111?11??202?111(1 2)2120??21??0??0??-100-??????????????????????0????????????????????0???????????????????????-?0????????0?????0?-??????????????????????????????????0???1??--?00?0-1111??????????????????????-????????????????????????????????????????????????????????????????????????????????????????? + 'Chengjiangocaris' ?111?130?0-?????????0-----------0000000???00?0----1?0----000----0-0?-0?????0---------0-----000-----?0-00--??00----??1112102???0????-01111110100-212?210--0-----?0?0??1-11-0?2??0?110????????0??????0?0?-?????-??0?????000------0----?0-0????1-?---0---?-0-010121001-0---?0?00--0------0-00---0-00----0000-0?0--20--0-11220?????????????????????-???????10?11??1????????????????????0-00???????0?????????????????????????????????????????? + 'Fuxianhuia' ?111?130?0-0????????0-----------0000000???00?0----1?0----000----0-0?-0?????0---------0-----000-----?0-00--??00----??1112102???0?100-01111110100-212??10--0-----?0?0??1-11-0?2??0?110????????0??????0?0?-?????-??0?????000------0----?0-0????1-?---0---?-0-010121001-0---?0?00--0------0-00---0-00----0?00-0?0--20--0-112?0?????????????????10??-0-??1??10????1????????131??????????0-000??????0?????????????????????????????????????????? + 'Leanchoilia' ?111?130?0-?????????0-----------0000000???00?0----1?0----000----0-0?-0?????0---------0-----000-----?0-00--??00----??1122102??10????-012111101???212??10--0-----?0?0??1-1??0?1??0?11?????????0??????0?1?-?????-??0?????000------0----?0-0????1-?---0---?-0-010121011-0---?0?01110----??????????????{1 3}1?0000-0?0--10--0-0---0?????????????????????-???????????????????1???????????????0-000??????0?????0???????????????????????????????????? + 'Alalcomenaeus' ?111?130?0-?????????0-----------0000000???00?0----1?0----000----0-0?-0?????0---------0-----000-----?0-00--??00----??1122102???0????-012111??1???212??1???????-????0??1-1??0?1??0?11?????????0??????0?1?-?????-??0?????000------0----?0-0????1-?---0---?-0-010121011-0---?0?01110----??????????????11-0?00-0?0--10--0-0---0?????????????????????-???????10?11010--??1??131??????????0-000??????0?????0???????????????????????????????????? + 'Misszhouia longicaudata' ?111?130?0-?????????0-----------0000000???00?0----1?0----000----0-0?-0?????0---------0-----000-----?0-00--??00----??1122102???0????-00-00-10100-212?210--0-----?0?0??1-11-0?1??0?111????????0??????0?1?-?????-??0?????000------0----?0-0????1-?---0---?-0-010131011-0---?0?01110----??????????????11-0000-0?0--10--0-0---0?????????????????1012-0-????????????????????????????0??0?0-000??????0?????????????????????????????????????????? + 'Kuamaia lata' ?111?130?0-?????????0-----------0000000???00?0----1?0----000----0-0?-0?????0---------0-----000-----?0-00--??00----??11221?2???0????-01111110100-212?210--0-----?0?0??1-11-0?1??0?111????????0??????0?1?-?????-??0?????000------0----?0-0????1-?---0---?-0-010131011-0---?0?01110----??????????????3120?00-0?0--10--0-0---0?????????????????1012-0-????????????????????????????0??0?0-00???????0?????????????????????????????????????????? + ; + ENDBLOCK; + + BEGIN NOTES; + [Taxon comments] + TEXT TAXON=9 TEXT='@Dong2010^n'; + TEXT TAXON=11 TEXT='@Zhang2022'; + TEXT TAXON=12 TEXT='@Liu2019'; + TEXT TAXON=13 TEXT='@Liu2019'; + TEXT TAXON=14 TEXT='@Shao2020'; + TEXT TAXON=15 TEXT='@Zhang2015'; + TEXT TAXON=16 TEXT='@Liu2014; @Shao2016; @Shao2020; @Wang2025'; + TEXT TAXON=22 TEXT='@Gad2005za'; + TEXT TAXON=23 TEXT='Adult Urnaloricus have not been found and may not exist [@Sørensen2025]'; + TEXT TAXON=24 TEXT='@Fujimoto2020mb'; + TEXT TAXON=25 TEXT='@Neves2014ode'; + TEXT TAXON=28 TEXT='@Rucci2020z'; + TEXT TAXON=34 TEXT='@Neuhaus2015z'; + TEXT TAXON=39 TEXT='@Kulikov1998rjn'; + TEXT TAXON=40 TEXT='@Inglis1969bbmnh - detailed line drawings of pharyngeal armature^n@Venekey2019z - Schematic of head; taxonomic diagnosis^n@Kulikov1998rjn - detailed description and illustration of E. robusta'; + TEXT TAXON=41 TEXT='@Leduc2016n'; + TEXT TAXON=42 TEXT='Kinonchulus Riemann, 1972^n^n= Pseudonchulus Altherr, 1972 syn. n.^n^n^n^nsee Holovachov et al., 2008'; + TEXT TAXON=95 TEXT='A senior synonym of Palaeopriapulites parvus [@Smith2015]'; + TEXT TAXON=128 TEXT='@Ou2012; @Liu2008'; + TEXT TAXON=170 TEXT='USNM 57490'; + TEXT TAXON=172 TEXT='Taxon name corrected using international commission on zoological nomenclature from A. benmoulae to A. benmoulai by Van Roy et al., 2015.^n^nTaxon name corrected using international commission on zoological nomenclature from A. benmoulae to A. benmoulai by Van Roy et al., 2015.'; + + [Character comments] + TEXT CHARACTER=1 TEXT='@Wills2012 (character 94) consider this to denote a priapulan synapomorphy. However, large primary body cavities occur in many ecdysozoan phyla.^n^nThe body cavity of both priapulids and nematomorphs represents a cleft in the extracellular matrix, and is thus defined as a primary body cavity, in contrast to a coelom (which would be lined with epithelia) [@SchmidtRhaesa2013].^n^nIn onychophorans a reduced coelom surrounds the gonads and protonephridia, but the perivisceral cavity is a primary body cavity, or strictly a mixocoel (resulting from the fusion of the primary body cavity with coelomic tissue during embryogenesis) [@Mayer2004az]. Tardigrades likewise exhibit a large primary body cavity; the tardigrade coelom is restricted to the gonads [@Dewel1998ar]. ^n^nThe body cavities of kinorhynchs and loriciferans are reduced [@SchmidtRhaesa2013]. ^n^n'; + TEXT CHARACTER=2 TEXT='WTS25. Taxa within 25% of the borderline between tokens are coded ambiguous for either token.^nDimensions for palaeoscolecids from (García-Bellido et al. 2013a). Eopriapulites follows (Shao et al. 2016). Xystoscolex measured from photographs (Conway Morris and Peel 2010) at close to 10; scored ambiguous (0, 1). Selkirkia around 7–10, depending on how much of tube the body occupies; scored as ambiguous (0, 1). Paraselkirkia , measured from photographs (Hou et al. 2017)almost exactly 10'; + TEXT CHARACTER=3 TEXT='This character distinguishes essentially cylindrical worms such as Palaeoscolex from taxa with clearly defined dorsal and ventral surfaces, whether by the presence of appendages (such as Louisella and lobopodians) or plates (such as Cricocosmia and Tabelliscolex) or by the differential expression of spinose armature (such as Tylotites). ^n^nThis character addresses fundamental asymmetry in the trunk organization, and thus overlooks differentiation that is restricted to the proboscis or the posterior trunk, such as the location of the anus or presence of tail hooks or caudal appendages; and diminutive landmarks such as specifically-positioned setae that do not affect the overall trunk morphology.^n'; + TEXT CHARACTER=4 TEXT='Character 1 in @Smith2015 and @Yang2015.'; + TEXT CHARACTER=5 TEXT='WTS43.^nTerminal in Maccabeus (Por and Bromley 1974)^nTerminal in Onychophora and Tardigrada; not clear why coded as in abdomen in Wills et al. 2012^nSubterminal in many nematodes, which have a caudal filament or spinneret glands posterior of the anus.'; + TEXT CHARACTER=6 TEXT='This character identifies the hypothesized evolutionary event of a movement of the mouth position. In Euarthropods the mouth is in a ventral position. In certain lobopodians, including Collinsium, hallucigeniids and Luolishania, the terminal mouth can superficially appear ventral due to the flexure of the neck [@Ma2009; @Smith2015; @Yang2015].^n^nThis character captures the transformation envisaged by characters 23 and 24 in Yang et al. (2015): both these characters appear to code for the same event of movement of mouth position, leading to a rotation in the head area, with appendages moving to an anterior position relative to the mouth.^n'; + TEXT CHARACTER=7 TEXT='When the mouth is anterior and terminal, mouth orientation is fixed as it can only face anteriorly. However, if the mouth is in a ventral position, then it can face anteriorly [following the interpretation of Kerygmachela by @Park2018], ventrally (anomalocaridids) or posteriorly (crown euarthropods, opabiniids). This character is only applicable when mouth is ventral.^n^nAdapted from character 11 in @Zhang2016 [SC: 7; Y: 23]^n'; + TEXT CHARACTER=8 TEXT='The anterior trunk of Aysheaia and Onychodictyon ferox is differentiated into a stout ‘proboscis’, distinct from the trunk by virtue of its shape and its lack of annulations (Ou et al. 2012). This ‘proboscis’ is considered homologous to the cycloneuralian worm introvert (=armature Zone I). This region is reduced in taxa such as Hallucigenia (where it has become part of the buccal cavity) and Anomalocaris (where it has been reduced and is no longer evident). We consider the oral region of tardigrades as a potential homologue of the introvert, noting the similarity of oral papillae with similar features in Aysheaia. @Kihm2023 draw attention to the similarity in form and function of tardigrade oral papillae and buccal sclerites in priapulan larvae; and there is indeed a compelling resemblance with the introvert-borne buccal papillae of e.g. Halicryptus [@Merriman1981].^n^nIn nematodes [@SchmidtRhaesa2014] and priapulans [@SchmidtRhaesa2012], only the midgut has an endodermal origin; the foregut (including the pharynx) and hindgut are ectodermal and hence covered in cuticle. The pseudointestine of Nematomorphs is endodermal and hence homologous to the intestine [@SchmidtRhaesa2012].^n^nThe ''head seam'' marks the anterior limit of the nematode trunk [@Kulikov1998rjon].^nThe nematode mouth comprises a cheilostome and pharyngostome (which together form the buccal cavity, or stoma sensu lato), pharynx (oesophagus), and pharyngo-intestinal junction (cardia) [@SchmidtRhaesa2014]. As the mouth opening (including the lips) and cheilostome are covered with body exocuticle [@Kulikov1998rjon] and occasionally bear cuticular projections [@SchmidtRhaesa2014] (historically termed odontia [@Inglis1966plsl]), we treat these as equivalent to the introvert. The wedge ring, which delimits this region [@DeLey1995], corresponds to Zone II. The subsequent elements of the foregut bear pharyngeal cuticle [@SchmidtRhaesa2014] and thus correspond to Zone III [@ConwayMorris1977]. As such, the gymnostome (proximal pharyngostome) corresponds to the unarmoured region between Zones II and III, whereas the stegosome (distal pharyngostome), which often bears denticles or teeth [@SchmidtRhaesa2014] historically termed onchia [@Inglis1966plsl], corresponds to the armoured pharynx. The six (inner) labial sensilla are somatic; in apomorphic taxa they head up a series of sensillae that continues along the trunk. They thus do not form part of the introvert, even if they secondarily migrate onto the lips in some cases [@SchdmitRhaesa2014]. The six outer labial / inner cephalic sensilla [terminology differs; see @Meldal2004] and the four (outer) cephalic sensilla, which primitively occur upon the lips, have a distinct developmental origin [@SchdmitRhaesa2014] and are thus not homologous with the trunk sensilla.^n^nIn nematomorphs, the adult intestine is reduced; it includes an anterior region that bears teeth and rods, perhaps vestiges of larval armature, followed by a cuticular pharynx (=oesophagus) [@SchmidtRhaesa2012]. We treat the spines, point backwards when the proboscis is everted [@SchmidtRhaesa2012], as introvert hooks. There are three rings of six hooks; the ventral hook on the outer ring (the first to be everted) is deeply cleft, giving the false appearance of a second hook [@SchmidtRhaesa2012].^n^n^nThe mouth cone of kinorhynchs occupies an equivalent position to that of loriciferans, but the cones in the two lineages exhibit distinct muscular, sensory and nervous configurations; they seem to have evolved independently [@Nebelsick1993]. The mouth cone ''represents the beginning of the alimentary canal'' [@SchmidtRhaesa2012] and may be regarded as equivalent to the priapulan foregut, in a permanently everted configuration [@Nebelsick1993]^n^nThe kinorhynch mouth cone exhibits (typically four) rings of stylets; the outer stylets sit posterior of the boundary between the mouth cone and the buccal epithelium [@Nebelsick1993]; they do not represent Zone II armature, contra @ConwayMorris1977. Their status as articulated spinose outgrowths makes it unlikely that they correspond to the cuticular thickenings of loriciferan oral ridges. Inner oral styles [@Nebelsick1993; @Neuhaus2002icb] occur on the buccal epithelium; unusually, their tips are directed toward the mouth opening. The styles are interpreted as Zone III armature; their position anterior to the pharyngeal bulb (cf. loriciferan placoids) suggests that they denote medial rather than distal rings.^n^nThe primary spinoscalids surround the base of the mouth cone, and thus correspond in position to Zone II elements. Their interpretation as distinct from the Zone I elements of the introvert is supported by their distinct structure, and their different spatial position: the primary spinoscalids define the boundaries between zones of introvert teeth [@SchmidtRhaesa2012; @Herranz2016za; @Neuhaus2002icb].^n^nIn loriciferans [@Neves2016za], the ring(s) of trichoscalids are considered homologous with those of kinorhynchs, and thus a feature of the neck rather than the introvert (Zone I) armature. The armature of the introvert (spinoscalids and clavoscalids) is considered to correspond to Zone I.^n^nThough taxa may display various reinforcements of the proximal mouth cone, none exhibits distinct sclerites; Zone II must be identified as unarmed. The oral ridges (sometimes termed oral stylets [@Neves2016za]) are thickenings of the mouth cone rather than sclerites. ^n^nThe oral stylets and (in Higgins larvae) oral teeth are distal scleritozations of the mouth cone that surround the mouth opening [@Gad2005za]. The longitudinal stylets are internal within the mouth cone, but are associated with a closing apparatus [@Gad2005za; @Sorensen2022za]; they are ''enwrapped by an apical ring at the tip of the mouth cone'' [@Sorensen2022za]. These are the clearest candidates for Zone II elements. ^n^nA weakly sclerotized pharyngeal tube connects the mouth opening to the pharyngeal bulb [@Gad2005za]. In Pliciloricus [@Gad2005za], the buccal tube bears three symmetrical ''bracelets'' of ''prepharyngeal'' armature, which supports the buccal tube [@Neves2013fz]. These are external to the buccal channel and are thus part of the mouth cone structure rather than scalids of the pharynx itself.^n^nThe inner surface of the pharyngeal bulb bears transverse rows (three in Pliciloricus [@Gad2005za]; five in Armorloricus [@Kristensen2004cbm]; multiple in larvae, including shira larvae, of Patuloricus [@Sorensen2022za]) of placoids, which surely correspond to Zone III.^n'; + TEXT CHARACTER=9 TEXT='Many heterotardigrades have a triangular proboscis, the proboscis being the combination of introvert and pharynx.^n^nCharacter 62 in @Kihm2023.'; + TEXT CHARACTER=10 TEXT='After transformation series 1 in @Wills2012. The introvert is the region of the trunk that corresponds to the Zone I armature zone in the scheme of @ConwayMorris1977. (Zones II and III are on the pharynx, which is often termed the ‘mouth cone’ in the priapulid and kinorhynch literature.)^nTaxa without such a region, or where the region is extremely short (as in Scathascolex and Wronascolex), do not have an invaginable introvert. This character cannot readily be applied to taxa with a non-terminal mouth.^nThe introvert of Eokinorhynchus is partly inverted in some specimens, fully everted in others; the maximum extent of its invagination is unknown (Zhang et al. 2015).^nCoded as ambiguous in Sirilorica (Peel et al. 2013); introvert never seen invaginated but sample size insufficient to determine whether this was not biologically possible.^nAn introvert is not present in adult Chordodidae (Poinar Jr. and Doelman 1974).^nAmbiguous in Xiaoheiqingella as the introvert is not retracted in any specimen (Han et al. 2004; Huang et al. 2004b; Han and Hu 2006; HU et al. 2017) ^nSeemingly invaginable to some extent in Chalazoscolex; unclear in Xystoscolex (Conway Morris and Peel 2010)?^nInvaginable in tardigrades (Guidetti et al. 2013b)^nThe loriciferan introvert can be telescopically retracted inside the lorica, but not inverted (Kristensen 1983), so these are coded as not invaginable.^nPartially inversible in Aysheaia (Whittington 1978)'; + TEXT CHARACTER=11 TEXT='After transformation series 1 in Wills et al. (2012). It is not clear how Wills established that the introverts of Louisella and Selkirkia/Paraselkirkia could not be fully retracted; as such these taxa are left ambiguous.^nAmbiguous in Scathascolex (Smith 2015)'; + TEXT CHARACTER=12 TEXT='Proposed by Nielsen (2001, 2012) as a synapomorphy of kinorhynchs, loriciferans and extant priapulids.'; + TEXT CHARACTER=13 TEXT='Trichoscalids are scalids that occur posterior to the last spinoscalid ring of the introvert in kinorhynchs, distinguished from other scalids morphologically, by their ‘hairy’ appearance, and positionally, by the gap between them and the Zone I scalids (Neves et al. 2016). They are listed as features of the neck as trichoscalid plates, where present, are connected to the placids (Sørensen et al. 2015). Wills et al. (2012) considered these as a separate ring of the Zone I armature (see their transformation series 11), but they are here treated separately.^nThe fringed tips of sclerites in Eolorica (Harvey and Butterfield 2017) are taken to identify the presence of trichoscalids.^n^nKinorhynch trichoscalids are innervated by longitudinal introvert nerves that extend from the forebrain, with one nerve corresponding to each trichoscalid (and the subsequent introvert scalids) [@Nebelsick1993z]. There is an analogy here with the two rings of six mechanoreceptory labial papillae of the nematode introvert, which are also innervated by six longitudinal nerves that seem to emanate from a cluster of cells immediately anterior of the circumoral brain [@SchmidtRhaesa2016]. Labial papillae occur in two rings, rather than the single ring of trichoscalids typical of kinorhynchs and loriciferans; but some kinorhynchs exhibit a second ring of accessory trichoscalids [@Herranz2013za]. By extension, we also consider the labial papillae of Aysheaia as possible homologues.^n^n^nCoded ambiguous in Sicyophorus, as there is a hint of spine-like structures at the base of the introvert of (fig. 3a Maas et al. 2007c) that could conceivably represent tricoscalids.^n^nThe row of backward pointing spines in Markuelia (Haug and Maas 2009) occupy an equivalent position and are coded as homologous.'; + TEXT CHARACTER=14 TEXT='After transformation series 11 in Wills et al. (2012).'; + TEXT CHARACTER=15 TEXT='Following character 5 of (Sørensen et al. 2015). Loriciferans (Neves et al. 2016) have fifteen trichoscalids (seven of which are sometimes ‘double’, interpreted as basally bifurcating as they attach to the same trichoscalid plate).'; + TEXT CHARACTER=16 TEXT='One ring in most kinorhynchs and loriciferans; two rings in nematodes. The four cephalic sclerites typically at the level of the amphids have a different symmetry and presumably innervation, so are not considered as homologues.'; + TEXT CHARACTER=17 TEXT='Trichoscalid plates are large plates to which trichoscalids attach. They are always present in lorificerans (Neves et al. 2016), and occur in certain kinorhynchs, where they connect at their posterior margin to the placids; see character 6 of (Sørensen et al. 2015)'; + TEXT CHARACTER=18 TEXT='Characters 5-6 in @Meldal2004. Trichoscalids (= labial papillae) comprise multiple articulated units in certain nematodes'; + TEXT CHARACTER=20 TEXT='Certain loriciferans exhibit doubled trichoscalids that share a common base [e.g. @Gad2005ode]'; + TEXT CHARACTER=21 TEXT='After transformation series 5 in Wills et al. (2012).^nAmbiguous in Tabelliscolex due to low preservational fidelity (Han et al. 2003b).^n^nAmbiguous in Shergoldana and Orstenoloricus because these taxa are presumed to represent larvae, meaning that the adult situation is unknown.^nCoded as ambiguous in Palaeoscolex piscatorum (Whittard 1953; Conway Morris 1997); not clear on what basis Wills et al. (2012) coded introvert features.^nThe specimens of Cricocosmia figured in (Hou et al. 2017) clearly shows that there is a single circlet of Zone I armature.^nMultiple circlets are evident in Tylotites (Han et al. 2007c)^nDetailed references: Nanaloricus (Kristensen et al. 2007); Pliciloricus, (Heiner and Kristensen 2005); Echinoderes (Sørensen and Pardos 2008; Herranz et al. 2014); Paracentrophyes, (Sørensen et al. 2010); Campyloderes, (Neuhaus and Sørensen 2013); CEntroderes, (Neuhaus et al. 2014); Zelinkaderes (Sørensen et al. 2007; Altenburger et al. 2015)^n^n(~) inapplicable; introvert absent or armature not comparable to priapulid proboscis zones^nAfter transformation series 5 in Wills et al. (2012).^nAmbiguous in Tabelliscolex due to low preservational fidelity (Han et al. 2003b).^nAbsent in Corynetis (Hu et al. 2012).^nAmbiguous in Shergoldana and Orstenoloricus because these taxa are presumed to represent larvae, meaning that the adult situation is unknown.^nCoded as ambiguous in Palaeoscolex piscatorum (Whittard 1953; Conway Morris 1997); not clear on what basis Wills et al. (2012) coded introvert features.^nThe specimens of Cricocosmia figured in (Hou et al. 2017) clearly shows that there is a single circlet of Zone I armature.^nMultiple circlets are evident in Tylotites (Han et al. 2007c)^nDetailed references: Nanaloricus (Kristensen et al. 2007); Pliciloricus, (Heiner and Kristensen 2005); Echinoderes (Sørensen and Pardos 2008; Herranz et al. 2014); Paracentrophyes, (Sørensen et al. 2010); Campyloderes, (Neuhaus and Sørensen 2013); CEntroderes, (Neuhaus et al. 2014); Zelinkaderes (Sørensen et al. 2007; Altenburger et al. 2015)'; + TEXT CHARACTER=22 TEXT='Although the first three circlets of Meiopriapulus contain 25 sclerites, these do not define longitudinal rows of sclerites or a 25-fold symmetry of the introvert (Adrianov and Malakhov 2001)^nThe circlets of Maccabeus contain 25 elements apiece; the first circlet is interpreted as representing an amalgamation of the first three circlets (Adrianov and Malakhov 2001)^nNot so in Kinorhynchs (Herranz et al. 2013), or Kinonchulus (where it is the first one row that defines the symmetry) (Liu et al. 2014)'; + TEXT CHARACTER=23 TEXT='Scalids and pharyngeal teeth have been distinguished based on their orientation on the pharynx (Nielsen 2001 p. 332)^nCoded ambiguous in Tylotites as descriptions are ambiguous on this point (Han et al. 2003a, 2007c)^nDirected anteriad in Maccabeus (Por and Bromley 1974)'; + TEXT CHARACTER=24 TEXT='Nematomorphs have two (Nectonema?) or three (Gordiida) circlets (Schmidt-Rhaesa 1996). Kinonchulus has around seven. Shergoldana possibly has more than one.'; + TEXT CHARACTER=25 TEXT='After transformation series 10 in Wills et al. (2012).'; + TEXT CHARACTER=26 TEXT='Zone I sclerites arranged ‘radially’, in longitudinal or diagonal lines; see transformation series 6 in Wills et al. (2012). The nature of the radial arrangement – pentaradial or hexaradial – is not independent of the number of longitudinal armature rows (trans. ser. 84) and is thus not coded separately here. A radial arrangement is not apparent in Eokinorhynchus (Zhang et al. 2015); this does not seem to represent preservation and is taken as authentic.^nNot in rows in Nematomorphs: they have a 6-6-7 arrangement (Schmidt-Rhaesa 1996)^nThe symmetric arrangement of kinorhynchs (Herranz et al. 2013) and loriciferans (Kristensen et al. 2007) does not qualify as neat rows; the character aims to capture the regimented organization of priapulids. The armature of Ottoia and Selkirkia is in prominent diagonal rows, producing a quincunx arrangement (Smith et al. 2015). Inapplicable in Markuelia as the three rings preserved seem to correspond to those that define the prominent rows in priapulids (Dong et al. 2010).'; + TEXT CHARACTER=27 TEXT='After transformation series 6 in Wills et al. (2012). Diagonal in Eokinorhynchus (Zhang et al. 2015). Parallel in Nanaloricus, Pliciloricus (Neves et al. 2016). The regular arrangement of scalids in Eolorica (Harvey and Butterfield 2017) is suggestive of a row-wise arrangement, though the orientation of such rows cannot be determined.^nDiagonal in Laojieella (Han et al. 2006)^nAmbiguous in Eopiapulites; although sclerites occur in more-or-less transverse rows, helical ridges suggest that the underlying organization may be spiral (Liu et al. 2014)'; + TEXT CHARACTER=29 TEXT='This and the following transformation series attempt to extract the full phylogenetic information implicit in transformation series 8 of Wills et al. (2012).^nThe spines of Scolecofurca appear to have originally been cuticularized, based on images taken by Jean-Bernard Caron (Caron 2011). These depict simple posterior-directed spines, though finer subsidiary morphology is possible.'; + TEXT CHARACTER=30 TEXT='The solid sclerites of nematomorphs differ from the structures borne by scalidophorans (Schmidt-Rhaesa 1996). The construction of Zone II and Zone III armature elements is typically the same as that of Zone I elements, so this character statement stands for elements of all three zones.'; + TEXT CHARACTER=31 TEXT='Maccabeus has long and short sclerites in Zone I (Por and Bromley 1974) so is coded as ambiguous.'; + TEXT CHARACTER=32 TEXT='Coded as spinose in loriciferans (Neves et al. 2016)^nLimited information is available from Nectonema (Schmidt-Rhaesa 1996)'; + TEXT CHARACTER=33 TEXT='In certain kinorhynch genera (here, Cateria), primary spinoscalids bifurcate at their base, giving the appearance that their number is twice its true figure (Sørensen et al. 2015).'; + TEXT CHARACTER=34 TEXT='Dentate elements bear secondary denticles; pectinate elements bear a fine comb-like fringe'; + TEXT CHARACTER=35 TEXT='Spinoscalids and clavoscalids of many loriciferans, including Eolorica, bear articulated joints (Neves et al. 2016; Harvey and Butterfield 2017)^nSclerites of Dracoderes are also articulated (Sørensen et al. 2012b)'; + TEXT CHARACTER=36 TEXT='Spinoscalids of many loriciferans, including Eolorica, bear small subsidiary setules (Neves et al. 2016; Harvey and Butterfield 2017)'; + TEXT CHARACTER=37 TEXT='The sclerites of Priapulopsis are telescopic (Storch et al. 1995).'; + TEXT CHARACTER=38 TEXT='The Zone I sclerites of Meiopriapulus bear a pectinate hood (Morse 1981)'; + TEXT CHARACTER=39 TEXT='The primary spinoscalids of certain kinorhynchs have intrinsic muscles [@Herranz2021z]'; + TEXT CHARACTER=40 TEXT='Modified from transformation series 9 in Wills et al. (2012). In order to capture homology due to a radial arrangement, the armature number is formulated a number of transformation series, each corresponding to a common factor and thus a potential homology of symmetry. Priapulids, with 25 rows, also exhibit pentaradial symmetry. (A taxon could conceivably exhibit 30-fold symmetry, which would have both pentaradial and hexaradial symmetry.) In priapulids, the symmetry of the pharynx is defined by the number of elements that comprise the first three circlets and, hence, defining the number of longitudinal rows of elements on the introvert.^nTaxa with 25 scalid rows: Recent Priapulidae, Halicryptidae, Tubiluchidae and Maccabeus (Adrianov and Malakhov 2001), Xiaoheiqingella, Yunnanpriapulus (Huang et al. 2004b), Markuelia (Dong et al. 2010)^nThe first three rows contain 8 + 9 + 8 = 25 scalids in Meiopriapulus even if they do not define the symmetry (Adrianov and Malakhov 2001)^nEximipriapulus: “More than 30” (Ma et al. 2014b)^nfewer than 25 – c. 10 on each side – in Sicyophorus and Palaeopriapulites (Maas et al. 2007c)^n6+6+7=19 in nematomorphs (Conway Morris 1977a) – but does this reflect an underlying 6-fold symmetry (see below)?^nShergoldana’s armature comprises a ring of cushion-like folds, each bearing a single tooth. Each fold is associated with two round humps, and a further round hump occurs between each pair of folds (Maas et al. 2007a). This arrangement suggests a six-fold symmetry.^nThe armature of kinorhynchs is arranged in a pentaradial fashion (Sørensen et al. 2008)^nA six-fold symmetry is observed in Chordodes, Shergoldana (Maas et al. 2007a), larval nematodes (despite lack of Zone I armature), and loriciferans (Sørensen et al. 2008). In Halicryptus, the hatching larva has seven-fold symmetry, becoming eightfold in the Higgins larva (Storch and Higgins 1991; Janssen et al. 2009)^nThe six oral papillae of Aysheaia (Whittington 1978) and Tardigrada (Urban 2013) are taken to indicate a 6-fold pharyngeal symmetry, reflected by the six oral plates of Actinarctus (Boesgaard and Kristensen 2001) and Halobiotus (Biserova and Kuznetsova 2012). The six denticles of Sirilorica (Peel 2010; Peel et al. 2013)are interpreted in the same way'; + TEXT CHARACTER=41 TEXT='Treated as a neomorphic character, contingent on five-fold symmetry, analogous to an ordered character (not five-fold / five-fold / twentyfive-fold) reflecting progressively higher degrees of organization.'; + TEXT CHARACTER=43 TEXT='The dorsal stylet (large dorsal tooth of Kinonchulus) arises outside the pharynx, as revealed during moulting and by the possession of its own set of musculature (p 191 Bird and Bird 1991); it is not considered part of the pharyngeal armature. Its dorsal position indicates that it is not homologous with the (ventral) tardigrade stylet.'; + TEXT CHARACTER=44 TEXT='We define a pre-oral (‘buccal’) chamber as a region enclosing the mouth and formed by the outgrowth of surrounding body tissue – as observed in modern onychophorans [@Martin2014]. The character is coded as absent where the oral region is clearly displayed externally, as in Tardigrada, euarthropods from Kerygmachela crownwards (where the position of the mouth is marked by the expression of an ‘oral cone’), Jianshanopodia [@Vannier2014], Aysheaia [@Whittington1978], and Siberion [@Dzik2011]. It is coded as ambiguous in other taxa, as the location of the original mouth is unclear.^n^nCharacter 8 in @Smith2015.'; + TEXT CHARACTER=45 TEXT='The buccal tube of Nanaloricidae, part of the pharynx, bears annulations; these are absent in Pliciloricidae @Neves2016za]'; + TEXT CHARACTER=46 TEXT='After transformation series 4 in Wills et al. (2012).^nScathascolex and Wronascolex are coded as ambiguous as available material is insufficient to determine the invagibility of the pharynx.^nA pharynx, permanently inverted, is present in Chordodes; it is degenerate in adults and there is pharyngeal armature is not recorded (though it exists in larval stages) (Bolek et al. 2010).^nEversible in Xystoscolex; and seemingly Chalazoscolex (Conway Morris and Peel 2010)^nAn everted pharynx can be observed in some specimens of Aysheaia (e.g. USNM 58655; Whittington 1978)^n^n---^nThe pharynx of priapulans and palaeoscolecids can be everted [@ConwayMorris1977], whereas the panarthropod foregut is permanently inverted. No lobopodians exhibit complete eversion of the pharynx, even if some taxa display a certain degree of flexibility: as perhaps evident in the proboscis of Onychodictyon ferox [@Ou2012] or the presumed suction-feeding mouthparts of anomalocaridids [@Daley2012]. Taxa whose mouth region is unknown or is known from a limited sample size are coded ambiguous to reflect the possibility that eversion was possible but not displayed in the available material.^n^nCharacter 12 in @Smith2015.'; + TEXT CHARACTER=47 TEXT='After transformation series 4 in Wills et al. (2012).^nScathascolex and Wronascolex are coded as ambiguous as available material is insufficient to determine the invagibility of the pharynx. Scolecofurca is viewed as having its narrow pharynx in a minimally everted position.^nLoriciferans are coded as permanently everted as the mouth cone remains everted even when the introvert is retracted (Neves et al. 2013)'; + TEXT CHARACTER=48 TEXT='Cf. WTS85.^nCoded as absent in Corynetis following the interpretation of (Huang et al. 2004a). Movement of pharynx is interpreted as having a role in locomotion in Kinonchulus (Riemann 1972), but it is not clear whether this employs peristalsis.. Both Kinorhyncha (Neuhaus and Higgins 2002) and Loricifera employ their introvert in locomotion (Sørensen et al. 2008), though seemingly through the use movement of individual scalids rather than peristaltic contraction of the entire introvert.'; + TEXT CHARACTER=49 TEXT='WTS22^nCoded ambiguous in Scathascolex as there are insufficient specimens to determine whether the pharynx is preserved in its fully everted position. Coded as complete or incomplete in Eokinorhynchus as a specimen is preserved with eversion beyond proximal teeth (Zhang et al. 2015)'; + TEXT CHARACTER=50 TEXT='Cf. WTS83. ^nThe large size of the pharynx underlies the proposed ''Megintroverta'' clade uniting Acanthopriapulus with Priapulus and Priapulopsis [@Lemburg1999; @SchmidtRhaesa2022za]. This formulation attempts to avoid attaching undue significance to subtle variations in introvert length. It is phrased as ‘foregut’ rather than ‘introvert’ to recognize the proposed homology between the panarthropod foregut and the cycloneuralian introvert. Scored as ambiguous (0, 1) where neither category applies.'; + TEXT CHARACTER=51 TEXT='The distal pharynx of nematomorphs can be retracted into the body, but not inverted [@SchmidtRhaesa2012]. The mouth cone of kinorhynchs and loriciferans corresponds to the anterior pharynx; it can be withdrawn, but not inverted.'; + TEXT CHARACTER=52 TEXT='WTS86. A triradiate introvert is a putative synapomorphy of Loricifera, Priapulida + Kinorhyncha (Yamasaki et al. 2015). The pharynx is also triradiate in most nematodes (Altun and Hall 2017), including Kinonchulus (Riemann 1972) and in some larval onychophorans (Schmidt-Rhaesa et al. 1998)^nTriradiate in Pycnophyes and Kinorhynchus, but round in Cyclorhagida (Neuhaus and Higgins 2002)'; + TEXT CHARACTER=53 TEXT='See transformation series 12 in Wills et al. (2012).^n^nZone II is considered to represent the base of the pharynx and the position of the stomodeum. As such, the proximal circlet of Zone II sclerites represent circumpharyngeal structures, which are coded as homologous with the radial mouthparts of Hallucigenia [@Smith2015] and the circumoral apparatus of anomalocaridids [@Smith2015; @Li2024].^n^nThe grasping denticles of Sirilorica (Peel 2010; Peel et al. 2013) are interpreted as circumpharyngeal.^n^nThe six (?) peri-oral structures of Eolorica (Harvey and Butterfield 2017) are interpreted as robust oral ridges, preserved where the accompanying mouth cone has decayed.^nCoded as ambiguous in Tylotites (Han et al. 2003a, 2007c) as it is possible that the distinct and forward-oriented ring of introvert hooks corresponds to the spines at the base of Zone II.^nA single ring of elongate spines appears to gird the base of Zone II in Cricocosmia (Hou and Bergström 1994; Han et al. 2007b)^nThe buccal papillae of Halicryptus are assumed to correspond to the Zone II elements; these are not sclerotized and are irregularly distributed (Merriman 1981; Storch et al. 1990; Adrianov and Malakhov 2001). A similar condition occurs in Tubiluchus (Calloway 1975)^nThe ‘double tentacles’ of Maccabeus are described as surrounding the mouth, but the eight trigger spines sit directly on the circumoral nerve ring (Por and Bromley 1974). On this basis, the latter are homologized with the Zone II elements; the tentacles are considered to represent modified Zone I spines. ^nScolecofurca appears to have elongate Zone II elements visible at its anterior margin (Caron 2011)^nCoded ambiguous for Fieldia; just a hint of some form of structure around the base of the everted Zone II (Pers Obs of ROM 93-1678A)^nNot described in Guanduscolex (Hu et al. 2008), though it is possible that these have been overlooked on account of preservation (cf. the faint preservation in Mafangscolex mannus).^nXystoscolex has prominent ridge-like features at the boundary of the introvert and the pharynx (Conway Morris and Peel 2010); these presumably correspond to Zone II armature, though their detailed morphology remains ambiguous^n^n---^nTODO Review in Aysheaia and Siberion whether the oral papillae might correspond to Zone I^n^n---^nThe mouth of many ecdysozoans is surrounded by radially-arranged cuticular or sclerotized structures. In priapulans and other cycloneuralians these are typically conical spines, arranged centripetally when the pharynx is contracted and centrifugally when the pharynx is everted [@ConwayMorris1977]. In basal panarthropods such as Aysheaia, Kerygmachela, Hallucigenia and Jianshanopodia, the structures are regular non-sclerotized lamellae, aciculae or plates [@Whittington1978; @Budd1998trse; @Vannier2014; @Smith2015]; among anomalocaridids the plates are sclerotized and differentiated [@Daley2012; @Daley2013p; @Daley2013jsp]. (Although the three or four prominent plates in the anomalocaridid oral cone are differentiated to give rise to bilateral symmetry [@Daley2012], the underlying radial arrangement of these plates is clear: some plates straddle the midline, and if rotated by 90° the smaller plates are equivalent to their counterparts.) Tardigrades bear circumoral lamellae [@Budd2001za; @Guidetti2012; @Mayer2013po]. Other lobopodians are coded as ambiguous; euarthropods and onychophorans are coded as absent [discussed in @Smith2015, char. 9].^n^nCharacter 9 in @Smith2015; character 25 in @Yang2015.'; + TEXT CHARACTER=54 TEXT='This character differentiates circumoral structures with a small contact area with the body (e.g. coronal spines of priapulomorph worms) from the plate-like circumoral structures that have a large surface area in contact with the body - found commonly in stem euarthropods (e.g. Parapeytoia, Hurdia)'; + TEXT CHARACTER=55 TEXT='Zone II sclerites form a ring around the mouth of e.g. priapulans, but are paired bilateral series in Omnidens. As the underlying symmetry mechanism is assumed to be conserved even in the absence of armature, taxa without Zone II sclerites are coded ambiguous.'; + TEXT CHARACTER=56 TEXT='This character distinguishes the simple organization of the mouth apparatus in Hallucigenia [@Smith2015] and Pambdelurion [@Budd1998ar; @Vinther2016] from the more complex mouthparts of anomalocaridids [@Daley2009; @Daley2012; @Daley2014]. We score Megadictyon, Schinderhannes and Jianshanopodia as uncertain to reflect their mouthparts'' poor preservation [@Liu2006; @Liu2007az; @Kuhl2009]. The character is treated as neomorphic to reflect the increasing gradient of complexity reflected by differentiation.^n^nCharacter 10 in @Smith2015.'; + TEXT CHARACTER=59 TEXT='Node-bearing plates are considered by @Liu2018nsr to represent a synapomorphy of Anomalocarididae + Amplectobeluidae. @Kihm2023 interpret the first band of teeth in the tardigrade oral cavity as homologous to these nodes. Because they occur on the inner rather than the outer surface of the plates, we prefer to ascribe such nodes to a separate transformation series.'; + TEXT CHARACTER=60 TEXT='@Dewel2006 notes that the first band of teeth in the tardigrade oral cavity occur on the circumoral plates, rather than forming part of the buccal tube.^n@Kihm2023 interpret the teeth as homologous with the nodes on anomalocaridid + amplectobeluid circumoral plates, which we consider unlikely as these occur on the outer face of the plates, whereas tardigrade elements occur on the inner face.'; + TEXT CHARACTER=61 TEXT='Panarthropods express a considerable diversity of circumoral structures, which represent a symplesiomorphic feature of Ecdysozoa as a whole (e.g. Edgecombe 2009). Various lobopodians bear oral papillae/lamellae (e.g. Aysheaia (Whittington 1978); Kerygmachela (Budd 1993, 1998a); Opabinia (Whittington 1975)); a similar feature occurs in the oral cone of Tardigrada (Dewel and Eibye-Jacobsen 2006; Guidetti et al. 2012). Pambdelurion (Budd 1998b) and anomalocaridids (e.g. Daley et al. 2009; Daley and Edgecombe 2014) exhibit radially arranged plates that together form a mouth apparatus (Daley and Bergström 2012). We code the nature of the circumoral structures in Megadictyon and Jianshanopodia (Liu et al. 2006, 2007; Vannier et al. 2014) as uncertain; in the former case, the type material does not unequivocally exhibit a plate-like nature; in the latter, the documentation of the plates is inconclusive. The transformation series is scored as inapplicable in Onychophora because the bilaterally symmetrical lip papillae are demonstrably not homologous with the radially symmetrical structures of other taxa (Eriksson and Budd 2000; Martin and Mayer 2014)'; + TEXT CHARACTER=62 TEXT='After transformation series 12 in Wills et al. (2012).^nThe single circlet of large denticles in Sirilorica (Peel et al. 2013) is interpreted as the proximal circlet of Zone II. There are at least six denticles; a seventh may be obscured by incomplete preservation (Peel et al. 2013). Nanaloricidae, and most species of Pliciloricus, bear eight oral ridges (though ranging from six to twelve in Pliciloricidae) (Neves et al. 2016). The six peri-oral sclerites preserved in Eolorica (Harvey and Butterfield 2017) are taken to represent the full complement. Six in Actinarctus (Boesgaard and Kristensen 2001), Halobiotus (Biserova and Kuznetsova 2012), Aysheaia. Twelve liplets corresponding to six lobes in Anatonchus (Borgonie et al. 1995), so coded as homologous to six as this is the underlying organization. c. Eighteen in Eopriapulites (Liu et al. 2014)'; + TEXT CHARACTER=63 TEXT='After transformation series 13 in Wills et al. (2012).^nMaccabeus is coded ambiguous; the spines have a length:width ratio of 4:1 if the width is measured at its maximum in the basal region, but 12:1 if measured at the base of the elongate projection (Por and Bromley 1974).'; + TEXT CHARACTER=64 TEXT='The spines of Sirilorica seem to have multiple cusps (Peel et al. 2013), as do the trigger spines of Maccabeus (Por and Bromley 1974).^nShergoldana is coded as ambiguous as it is not clear whether the ‘cushion-like folds’ (Maas et al. 2007a) are part of the spine or represent soft tissue.'; + TEXT CHARACTER=65 TEXT='The inner face of plates of e.g. Omnidens bear inward-directed spines.'; + TEXT CHARACTER=66 TEXT='The inner surface of the circumoral plates of Anomalocaris, Peytoia and Hurdia bear multiple spinose projections [@Daley2012; @Daley2013jsp], but the equivalent face of plates in priapulans and Hallucigenia sparsa lacks projections [@Smith2015].^n^nCharacter 16 in @Zhang2016.'; + TEXT CHARACTER=67 TEXT='Where present, the oral ridges of loriciferans (Neves et al. 2016) are interpreted as Zone II sclerites fused to the introvert. In other taxa, only the proximal part of the sclerites is attached to the trunk. ^nIn certain kinorhynchs (here, Campyloderes) the outer oral stylets (= Zone III sclerites) lie flat against the introvert, to which they are fused (Sørensen et al. 2015).'; + TEXT CHARACTER=68 TEXT='WTS20^nSlightly narrower in Corynetis (Huang et al. 2004a; Hu et al. 2012)^nNot substantially wider in Cricocosmia (Hou and Bergström 1994)^nNot wider in Halicryptus (Merriman 1981; Shirley and Storch 1999; Adrianov and Malakhov 2001)'; + TEXT CHARACTER=69 TEXT='Pharyngeal teeth in Cricocosmia do not extend to the circumoral spines of the introvert [e.g. @Cong2017]. Similarly in Hallucigenia there is an absence of pharyngeal teeth in proximity to the circumoral structures [@Smith2015]. In tardigrades [e.g. @Hansen2002], Hurdia [@Daley2009] and Cambroraster [@Moysiuk2019], the pharyngeal teeth are directly adjacent to the radially arranged circumoral structures.'; + TEXT CHARACTER=70 TEXT='The proximal pharynx of Spinoloricus forms a short pleated ring [@Heiner2007; @Neves2016za]. A similar reinforced region occurs in Armorloricus, referred to as ''basal plate row of mouth cone base'' [@Kristensen2004cbm].'; + TEXT CHARACTER=71 TEXT='The loriciferan mouth cone bears typically six to twelve external (outer) oral folds or ridges, which are thickenings of the cuticle [@Neves2016za]'; + TEXT CHARACTER=72 TEXT='Number of oral ridges present in adults'; + TEXT CHARACTER=73 TEXT='The oral ridge bifurcates in Nanaloricidae [@Neves2016za]'; + TEXT CHARACTER=74 TEXT='In certain taxa, alternating ridges are differentiated into two lengths'; + TEXT CHARACTER=75 TEXT='In certain tardigrades, "the mouth ring appears to be demarcated from the buccal tube by a zone of attenuated or fenestrated cuticle" [@Dewel2006, fig. 11]'; + TEXT CHARACTER=76 TEXT='The Zone III armature trans. series has been reformulated from Wills et al. (2012, trans. ser. 15–19) to better capture possible homologies between similar structures, and to avoid treating different character states as homologous.^n^nMany tardigrades exhibit three rows of oral teeth in their buccal apparatus [@Michalczyk2003]. We follow @Kihm2023 in treating the first row of teeth at nodes of the circumoral plates, even if their equivalence with potential homologues in anomalocaridids is difficult to demonstrate conclusively. ^n@Kihm2023 contend that the second and third rows of oral teeth cannot correspond to Zone III elements in other ecdysozoans, because the buccal tube is not part of the pharynx. Whereas the buccopharyngeal apparatus is clearly divided into a buccal tube and a pharyngeal bulb, both components of the apparatus derive from the stomodeum [@Massa2024], and are thus together homologous with the pharynx of other ecdysozoan worms. Treating the circumoral lamellae as equivalent to Zone II teeth implicitly accepts the homology between these features. We thus treat the buccal tube as a modification of the component of the proximal region of Zone III, which corresponds to the proximal pharynx in other ecdysozoan worms; and the tardigrade pharyngeal bulb as equivalent to the distal region of Zone III.^n^n^nAfter transformation series 3 in Wills et al. (2012). Priapulids’ Zone III sclerites, and the microspines (proventricular acanthae) of the arthropod foregut, are included here. The presence of comparable features in the foregut of Jianshanopodia (Vannier et al. 2014) demonstrates that comparable features can be observed in Cambrian lobopodians given suitable preservation; lobopodians are thus coded as ambiguous for this transformation series. The features are absent in Onychophora (Elzinga 1998); electron-dense thickenings in the tardigrade pharynx (Dewel and Clark 1973b) are tentatively considered to represent armature that has been reduced in size due to the miniaturization of the tardigrade body.^nCorresponds to the inner rows of teeth in Hurdia (Transformation series 9 in Daley et al. (2009). ^nOral stylets are occasionally present (three, four or six) in Pliciloricus (Neves et al. 2016); eight are present in Nanaloricus mysticus (Kristensen 1983).^nPresence of armature in Priapulites (Schram 1973) indicated by robust preservation of pharyngeal region.^nThe adult condition is used, except where elements are present in larvae only.^nSimple spinose elements within the oral circlet of Eopriapulites (Liu et al. 2014) are interpreted as elements of Zone III.^n^nIn nematomorphs, the proboscis contains cuticular spines (=hooks), interpreted as introvert elements); and a ''stylet'' [@SchmidtRhaesa2012]. The stylet comprises three dense cuticular rods; the surface contains series of individual cuticularized teeth [@SchmidtRhaesa2012] which we treat as Zone III elements. The opening in the centre of the stylet corresponds to a gland duct; @SchmidtRhaesa2012 consider it unlikely to correspond to a mouth. As no other mouth position is identified, and we interpret this as the likely position of the mouth.^n^n---^nIn many taxa the pharynx is lined with cuticular sclerites or "pharyngeal teeth". Priapulans have an eversible pharynx lined with pharyngeal teeth [@vanderLand1970; @ConwayMorris1977]. Hallucigenia sparsa has a structurally differentiated (narrower) pharynx lined with acicular teeth [@Smith2015]. Jianshanopodia bears a series of pharyngeal teeth with multiple cusps [@Liu2006; @Vannier2014]. This fossil, perhaps alongside Omnidens [@Hou2006], resembles a longitudinally extended Hurdia mouthpart; the inner rows of teeth in Hurdia are correspondingly interpreted as pharyngeal teeth [@Daley2009], 2013a). Sclerotized teeth have also been reported in the foregut of Paucipodia [@Hou2004], although the nature and distribution of the teeth is not clear from the fossil material. Onychophorans bear a differentiated pharynx with an oesophageal constriction, but this is unornamented [@Elzinga1998]. ^n^nMost tardigrade taxa exhibit two to five rows of teeth (= mucrones) caudally to their circumoral lamellae [@Pilato1972; @Schuster1980; @Hansen2002; @Dastych2003; @Guidetti2012]; some have a further row of sclerotized transverse ridges (= baffles). Following @Khim2023 we consider these oral teeth to be a separate innovation, and thus not homologous to pharyngeal teeth. Coded as ambiguous in all other taxa due to inadequate preservation.^n^nCharacter 13 in @Smith2015.'; + TEXT CHARACTER=77 TEXT='Priapulans'' pharyngeal teeth exhibit a range of morphologies but always bear multiple cusps [@vanderLand1970; @Smith2015p]. Hallucigenia sparsa has acicular teeth that come to a single point [@Smith2015]. The teeth of Hurdia and Jianshanopodia have multiple cusps [@Daley2009; @Daley2013jsp; @Vannier2014]; tardigrade teeth do not [@Pilato1972; @Hansen2002; @Schuster1980; @Dastych2003].^n^nCharacter 14 in @Smith2015.'; + TEXT CHARACTER=78 TEXT='The situation in Zone III is assumed also to apply to Zone I armature, thus this character statement stands for both.'; + TEXT CHARACTER=79 TEXT='After transformation series 3 in Wills et al. (2012).'; + TEXT CHARACTER=80 TEXT='Zone III sclerites form rings or whorls around the pharynx in e.g. priapulans, but are paired bilateral series in Omnidens [@Li2024] and some tardigrades [@Michalczyk2003]. They surround the pharynx in a haphazard arrangement in taxa such as Corynetis [@Hu2012]. As the underlying symmetry mechanism is assumed to be conserved even in the absence of armature, taxa without Zone III sclerites are coded ambiguous.'; + TEXT CHARACTER=81 TEXT='In contrast to the uniform distribution of sclerites in priapulans and total-group euarthropods, the pharyngeal teeth in Hallucigenia sparsa seem to occupy one or two longitudinal rows and do not cover the entire surface of the pharynx [@Smith2015].^n^nCharacter 15 in @Smith2015.'; + TEXT CHARACTER=82 TEXT='After transformation series 14 in Wills et al. (2012). ^nThe oral stylets of loriciferans, where present, occur in a single circlet (Neves et al. 2016).^nTwo bands of teeth occur in tardigrades, plus Band I, interpreted herein as representing nodes of the circumoral lamellae (after @Kihm2023). Band II often contains irregularly distributed elements.^n^nAnatonchus (Nematoda) has four circlets (Borgonie et al. 1995), in addition to three teeth (coded as distal elements)^nMaotianshania has around 12 circlets (Hou and Bergström 1994)^nEokinorhynchus has at least two circlets; the full number is unknown as the pharynx may not be fully everted (Zhang et al. 2015).^nKinorhynchs have four circlets within Zone III: one ring of outer oral stylets, two rings of inner oral styles, and the helioscalids (Sørensen and Pardos 2008)^nOttoia has c. 40 (Smith et al. 2015)^nCorynetis has c. 60, if the armature is arranged in circlets rather than irregularly (Hu et al. 2012)^nHalluicgenia is coded as ?; its sclerites are not strictly arranged in circlets, and occur in a large number of rows (Smith and Caron 2015).^nHalycryptus has multiple circlets, with the number increasing during growth (Adrianov and Malakhov 2001)^nMany circlets in Maccabeus (Por and Bromley 1974)^nc. 50 in Priapulus (Adrianov and Malakhov 2001)^nAround 12 clear circlets in Tubiluchus, with further more distally (Kirsteuer 1976)^nAt least thirteen in Priapulopsis (van der Land 1970)^nVariable numbers reported in Meiopriapulus, possible changing with ontogeny (Sørensen et al. 2012a)^nc. 20 in Laojieella (Han et al. 2007a)^nSeemingly a single circlet in Ancalagon and Fieldia (Conway Morris 1977a) (and Pers. Obs)'; + TEXT CHARACTER=83 TEXT='A distinct pentaradial symmetry is evident “only in the [first] eight circlets” in Halicryptus (Adrianov and Malakhov 2001)^nSeven (including the proximal circlet) in Maccabeus (Por and Bromley 1974)^nWTS19'; + TEXT CHARACTER=84 TEXT='WTS18^nEight in Meiopriapulus (Sørensen et al. 2012a)^nFive in Tubiluchus lemburgi (Schmidt-Rhaesa et al. 2013), reflected by the five basal papillae (which represent a distinct circlet from the remainder of the pharyngeal teeth).^nSix in Namnaloricus, Pliciloricus (Neves et al. 2016), Kinochulus ^nMaotianshania has approximately 15 (Hou and Bergström 1994); coded as ?^nCoded ambiguous in Cricocosmia (Hou and Bergström 1994)?^nSixteen in Eokinorhynchus (Zhang et al. 2015)^nOttoia approx.. 24–30; probably not a constant number between specimens (Smith et al. 2015); coded ambiguous^nThe dorsal outer oral stylet in Kinorhyncha is understood to be secondarily reduced; the decaradial arrangement of the elements being clearly evident (Nebelsick 1993); these taxa are thus coded as ten.^nTwelve reported in Kinonchulus, though six drawn; possibly six are the Zone II ‘hooks’.^nAnatonchus > 40 (Borgonie et al. 1995), coded ambiguous ^nFive in Halicryptus (Adrianov and Malakhov 2001) and Priapulopsis (van der Land 1970)^nc. Sixteen in Fieldia? (pers. obs)'; + TEXT CHARACTER=85 TEXT='The proximal circlet in kinorhynchs has an obvious relationship to the subsequent circlets. This character distinguishes this arrangement, whilst still recognizing the underlying five-fold symmetry shared with other taxa.'; + TEXT CHARACTER=87 TEXT='Following Sørensen et al. 2015 character 1 (Sørensen et al. 2015), the outer oral stylets of neocentrophyid and dracoderid kinorhynchs alternate between prominent and less well-developed sizes; see for example Paracentrophyes (Sørensen et al. 2010)'; + TEXT CHARACTER=88 TEXT='This and the following transformation series have been modified from trans. ser. 15 in Wills et al. 2012 to better capture possible homologies between sclerite morphologies.'; + TEXT CHARACTER=90 TEXT='Pectinate projections can occur from the fringe of a central cone (cf. trans. ser. 15, state 4 in Wills et al. 2012) or can occur along the margin of a scalid that lacks a central spine (cf. trans. ser. 15, state 6 in Wills et al. 2012). There is a gradation from a pectinate fringe (cf. Ottoia) via a multispinose situation (cf. Selkirkia Type A) through multiple spines (cf. Pripaulopsis); as such, all of these are coded in a single character statement.^nA basal fringe is present in the outer oral stylets of certain kinorhynchs – Paracentrophyes (Sørensen et al. 2010), Dracoderes (Sørensen et al. 2012b)'; + TEXT CHARACTER=91 TEXT='The outer oral stylets of many kinorhynchs (though not certain Pycnophyidae) consist of two to three rigid articulating units (Sørensen et al. 2015)'; + TEXT CHARACTER=92 TEXT='Reduction relates to size; this transformation series therefore applies whether or not the *morphology* of the proximal circlet is differentiated'; + TEXT CHARACTER=93 TEXT='After transformation series 18 in Wills et al. (2012).^nDifferentiated circlets are depicted in the drawn reconstruction of Corynetis (Huang et al. 2004a), but not formally described or depicted; this taxon is coded as ambiguous.^nDifferentiated in Halicryptus, Priapulopsis (Conway Morris 1977a).^nScored as inapplicable in loriciferans (Neves et al. 2016), whose single circlet cannot be meaningfully coded as ‘proximal’, ‘medial’ or ‘distal’.^nScored as differentiated in Ancalagon and Fieldia as the medial Zone III armature is massively reduced or absent in these taxa (Conway Morris 1977a)^nThe circlet of five sclerotized trabeculae in Maccabeus (Por and Bromley 1974) are interpreted as a reduced circlet of Zone III teeth.^nIn Priapulopsis bicaudatus, the first ring of teeth feature a reduced central spine (van der Land 1970)^nDifferentiated in Ottoia and Selkirkia (Type A teeth) (Conway Morris 1977a; Smith et al. 2015)'; + TEXT CHARACTER=94 TEXT='A raised band lies proximal to bands II and III of the tardigrade oral cavity armature [@Michalczyk2003]'; + TEXT CHARACTER=95 TEXT='Fieldia and Ancalagon only possess a single ring of Zone III teeth (Conway Morris 1977a), which I consider to represent a differentiated proximal circlet; the middle circlets are perhaps reduced or indistinct in the fossil material. The situation in loriciferans is taken to be the same: the Zone III armature comprises a single ring (typically) of oral ridges (i.e. a proximal circlet) and a single ring (where present) of oral stylets (a single distal circlet) (Neves et al. 2016).^nKinorhynchs have three rings of simple spinose styles (Sørensen and Pardos 2008; Herranz et al. 2014)'; + TEXT CHARACTER=96 TEXT='Multiple spines in Ottoia and Selkirkia (Smith et al. 2015). Simple spines in Eokinorhynchus (Zhang et al. 2015)^nSingle spines with pectinate fringe in Antygomonas (Bauer-Nebelsick 1996) and Centroderes (Neuhaus et al. 2014) coded as single spines; not clear that pectinate fringe is always clear enough to be unambiguously observed in other taxa.'; + TEXT CHARACTER=97 TEXT='In taxa such as Ottoia, the distal teeth in Zone III are morphologically and constitutionally distinct from the more proximal Zone III teeth (Smith et al. 2015). Scored as present if sclerites in the distal region of Zone III are morphologically distinct from those in the medial region, which are typically more robustly cuticularized. Sclerites that arm the upstanding eversible ‘mouth cone’ of priapulids are not included as part of the Zone III armature. Ambiguous in Louisella (Conway Morris 1977a). ^nDifferentiated ‘curved, scimitar-shaped’ teeth at entrance to stomach in Maccabeus (Por and Bromley 1974)'; + TEXT CHARACTER=98 TEXT='Pectinate in Ottoia (Smith et al. 2015).^nMorphologically distinct, though still pectinate, in Tubiluchus (Kirsteuer and Ruetzler 1973)'; + TEXT CHARACTER=99 TEXT='WTS23. If the proximal ring of elements is morphologically distinct, they are not included in this consideration.^nThe Zone III elements in Ottoia and Selkirkia do not change in size, just in angle of preservation (Smith et al. 2015).^nNo change in size is evident in Scathascolex.'; + TEXT CHARACTER=100 TEXT='Certain kinorhynchs exhibit small muscles that allow each outer style to be moved individually [@Herranz2021z]'; + TEXT CHARACTER=101 TEXT='Placoids are thickenings of the pharyngeal cuticle related to the attachment of the buccal tube.^n^nCharacter 64 from @Shi2021, 76 from @Khim2023, 32 in @Mapalo2024cb.'; + TEXT CHARACTER=102 TEXT='Character 33 in @Mapalo2024cb'; + TEXT CHARACTER=103 TEXT='Character 35 in @Mapalo2024cb. Present in certain tardigrades.'; + TEXT CHARACTER=104 TEXT='The ''stylets'' of nematomorphs comprise thickenings of the pharyngeal cuticle (and are thus not obvious homologues with structures termed ''stylets'' in other taxa)'; + TEXT CHARACTER=105 TEXT='In parachelan tardigrades, the anterior part of the buccal tube has hooks or ridges for the insertion of stylet musculature. ^n^nCharacter 74 from @Khim2023, 28 from @Mapalo2024cb.'; + TEXT CHARACTER=106 TEXT='Character 75 from @Khim2023; characters 20 and 30 in @Mapalo2024cb'; + TEXT CHARACTER=107 TEXT='WTS21.^nThe fully everted pharynx of Ottoia, Sirilorica and Louisella expresses a marked increase in width; this bulb-like feature is armed in Louisella (Peel et al. 2013)(Conway Morris 1977a).^nCoded ambiguous in Scathascolex as there are insufficient specimens to determine whether the pharynx is preserved in its fully everted position.^nCoded ambiguous in Cricocosmia (Hou and Bergström 1994); the material on which the reconstruction of (Han et al. 2007b) is based is not figured.^nCoded present in nematodes as pharynx (though not eversible) bears bulbs (Altun and Hall 2017).^nCoded ambiguous in Paratubiluchus as the ‘bulb’ may represent gut contents (Han et al. 2004)'; + TEXT CHARACTER=108 TEXT='A feature of certain kinorhynchs, e.g. Franciscideres; see character 7 in @Sorensen2015'; + TEXT CHARACTER=109 TEXT='Character 8 in @Sorensen2015.^nPlacids are a ring of plates in the neck region of most kinorhynchs, posterior to the introvert. Coded ambiguous in Sicyophorus, as there is a hint of spine-like structures at the base of the introvert (fig. 3a Maas et al. 2007c) that could conceivably correspond to placids or lips'; + TEXT CHARACTER=110 TEXT='In most cases, placids form a closing mechanism when the head is retracted into the trunk; see character 8 in (Sørensen et al. 2015). Nematode ‘lips’ also serve to close the front of the trunk (Borgonie et al. 1995; Altun and Hall 2017)'; + TEXT CHARACTER=111 TEXT='In certain kinorhynch taxa, the arrangement of placids incorporates gaps that give rise to a bilaterally symmetric character; see character 13 in (Sørensen et al. 2015).'; + TEXT CHARACTER=112 TEXT='See character 9 in (Sørensen et al. 2015).^nSix in Franciscideres; seven in Paracentrophyes; nine in Dracoderes; fourteen in Campyloderes; sixteen in Antygomonas, Centroderes , Echinoderes, Zelinkaderes (Sørensen et al. 2015).'; + TEXT CHARACTER=113 TEXT='See character 11 in (Sørensen et al. 2015).^nSirilorica and Nanaloricus bear spikes on the anterior margins of their loricae (Peel et al. 2013; Neves et al. 2016); Pliciloricus and Eolorica do not (Neves et al. 2016; Harvey and Butterfield 2017)'; + TEXT CHARACTER=114 TEXT='See character 12 in (Sørensen et al. 2015).'; + TEXT CHARACTER=115 TEXT='See characters 14-17 in @Meldal2004. Amphids are lateral sensory organs in nematodes, typically comprising a round or slit-like opening and an inner pocket.'; + TEXT CHARACTER=116 TEXT='The opening of the amphids may be round or slit-like'; + TEXT CHARACTER=117 TEXT='The head of Kerygmachela has a dorsal protruding lobe that contains neural tissue [@Park2018], presumed homologous to the projection of YKLP 12387. This is distinct from the "swelling" of the anterior trunk in certain hallucigeniid lobopodians, which gives rise to a bulbous "head" region. In higher euarthropods, the anterior lobe may be covered by a dorsal sclerite.'; + TEXT CHARACTER=118 TEXT='Numerous lobopodians have been considered to have cephalic sclerites [see @Ma2014jsp, char. 37], but in some cases this interpretation requires revision or confirmation through new material. Following @Liu2014ppp, we score this character as absent in Hallucigenia fortis [contra @Hou1995zjls], Onychodictyon ferox [contra @Ou2012] and Cardiodictyon [see @Hou1995zjls]. It is coded as ambiguous in Onychodictyon gracilis [@Liu2008app] and Hallucigenia hongmeia [@Steiner2012], as well as Luolishania [following @Smith2014]. Fossil taxa with an incomplete anterior region are coded as uncertain.^n^nCharacter 2 in @Smith2015 and @Yang2015.^n^n---^nNumerous lobopodians have been considered to have cephalic sclerites (Ma et al. 2014a), but in some cases this interpretation requires revision or confirmation through new material. Following recent data presented by Liu and Dunlop (2014), we score this transformation series as absent in Hallucigenia fortis (contra Hou and Bergström 1995), Onychodictyon ferox (contra Ou et al. 2012) and Cardiodictyon (see Hou and Bergström 1995). We code it as uncertain where the anterior region is ambiguously preserved, as in Onychodictyon gracilis (Liu et al. 2008) and Hallucigenia hongmeia (Steiner et al. 2012). An uncertain coding is also applied to Luolishania, as their apparent presence is only documented by a single specimen (Ma et al. 2009) whose ‘sclerites’ worryingly resemble features in other lobopodians whose original interpretation as sclerites has since been overthrown. Taxa with an incomplete anterior region are coded as uncertain.'; + TEXT CHARACTER=119 TEXT='We score this character as absent for fuxianhuiids, because the cephalic shield is not derived from fused segments [@Chen1995s; @Waloszek2005; @Bergstrom2008; @Yang2013], and in anomalocaridids, because the carapace-like structure on the head seems not to cover multiple cephalic segments [e.g. @Daley2009; @Daley2014].^n^nCharacter 3 in @Smith2015 and @Yang2015.^n'; + TEXT CHARACTER=120 TEXT='This character represents the hypothetical change in position of the anterior sclerite of the upper-stem euarthropods and the dorsal head sclerite of the anomalocaridids that are associated with protocerebral structures [see @Budd2021].'; + TEXT CHARACTER=121 TEXT='Character adapted from 59 in @VanRoy2015. Character 5 in @Yang2015.'; + TEXT CHARACTER=122 TEXT='The head sclerites of certain hurdiids exhibit a conspicuous reticulate ornamentation.^n^nCharacter 26 in @Moysiuk2019.^n'; + TEXT CHARACTER=123 TEXT='The dorsal sclerite is attached broadly in Radiodonta [@Daley2009; @Daley2012; @Cong2014; @Daley2014; @VanRoy2015], whereas the euarthropod anterior sclerite is only narrowly attached to the anterior end of the body in upper-stem and crown-group euarthropods [@Edgecombe1999; @Budd2008; @Yang2013; @Ortega2015].^n^nCharacter 6 in @Yang2015.^n'; + TEXT CHARACTER=124 TEXT='This character refers to the lateral "P" elements that typify the anterior scleritome of hurdiid radiodontans [@Daley2009; @Daley2012; @VanRoy2015].^n^nCharacter 7 in @Yang2015.'; + TEXT CHARACTER=125 TEXT='Character 30 in @Moysiuk2019. The lateral sclerites of certain hurdiids are elongate, whereas those of anomalocaridids are more circular in aspect and shape.'; + TEXT CHARACTER=126 TEXT='Character formulated from possible homology of ventral sclerites in §7 of @Budd2021.'; + TEXT CHARACTER=127 TEXT='The terminal mouths of Hallucigenia sparsa [@Smith2015], H. fortis [@Liu2014ppp], Collinsium [@Yang2015], Microdictyon and Cardiodictyon [@Chen1995bnmns; @Liu2014ppp] are consistently oriented ventrally, perpendicular to the main trunk axis; the anteriormost trunk (or, colloquially, ‘head’) can be manoeuvred independently of the main trunk. In other taxa (e.g. priapulans), the orientation of the mouth is fixed relative to the main trunk.^n^nCharacter 5 in @Smith2015.'; + TEXT CHARACTER=128 TEXT='Certain lobopodians (Cardiodictyon, Hallucigenia fortis, Luolishania) have a differentiated anteriormost trunk that forms a wide ellipse or "head" [@Liu2014ppp]. In Hallucigenia sparsa, the "head" is denoted by a slight increase in the width of the anteriormost trunk, which is most prominent in smaller specimens [@Smith2015]. In other taxa (Aysheaia, Onychodictyon ferox, Megadictyon, Jianshanopodia, Ilyodes, Collinsium), the anteriormost trunk is not differentiated in this way [@Thompson1980; @Ou2012; @Vannier2014; @Yang2015]. Coded as ambiguous in euarthropods, where the "trunk" has been replaced by sclerotized segments. ^n^nCharacter 6 in @Smith2015'; + TEXT CHARACTER=129 TEXT='@Budd2021 argues that anterior projections in certain lobopodians and tardigrades are homologous to euarthropod frontal filaments. We additionally interpret the anterior projections of Pambdelurion and Megadictyon as potential homologues.^n^nPotential homologues to the frontal filaments – the frontal processes, which migrate to become the anteriormost pair of lip papillae in adults – are present in crown group Onychophora [@Ortega2016asd]. On this view, we interpret the dorsal, apparently non-appendicular, antenniform appendages of Collinsovermis and Luolishania as potential homologues to the frontal filaments.^n^nAdapted from character 95 in @Yang2016.^n'; + TEXT CHARACTER=130 TEXT='Potential homologues to the frontal filaments – the frontal processes – migrate to become the anteriormost pair of lip papillae in adult Onychophora [@Ortega2016asd]. As the migration is a derived state, this character is treated as neomorphic.'; + TEXT CHARACTER=131 TEXT='Eutardigrades have a sensory field in the same region where cirri A occurs in heterotardigrades, which is considered a remnant of cirri A. A reduced cirrus A/sensory field is difficult to establish in fossils, we consider this reduction to be an synapomorphy of heterotardigrades, as such, fossil lobopodians are coded as absent, until further evidence is provided. Since the reduction (sensory field) is the derived state, we consider this character neomorphic.^n^nAdapted from character 15 in @Khim2023.'; + TEXT CHARACTER=132 TEXT='Cirri A in most athrotardigrades are on the head segment, and on the posterior part of the head in echiniscoideans. Neoarctus has cirri A on the first trunk segment. We conservatively code lobopodians with filamentous structures as uncertain as the homology to tardigrade Cirri A are not clear.^n^nAdapted from Character 16 from @Khim2023.'; + TEXT CHARACTER=133 TEXT='This character refers to the suite of cirri and clavae characteristic of heterotardigrades. ^n^nAdapted from @Khim2023 character 17.'; + TEXT CHARACTER=134 TEXT='@Daley2009 (char. 10), @Ma2014jsp (chars. 25, 27) and @Lan2021 implicitly treat compound eyes and ocelli as homologous structures. We uphold the case for deep homology between these organs. Modified ocelli can resemble a single ommatidium of a compound eye [@Land2012, pp. 125-126] and compound eyes can be de-differentiated into an ocelli during metamorphosis [@Bitsch2005, §3.1]. This implies a deep homology in fossils of ocelli and compound eyes despite notable differences in certain aspects, such as visual pigments, in ocelli and compound eyes in extant euarthropods [@Henze2012]. Paleontological support for this homology is reviewed by @Schoenemann2023. ^n^nAdapted from characters: 16 and 18 in @Smith2015; 29 and 31 in @Yang2015.'; + TEXT CHARACTER=135 TEXT='Number of discrete visual units, whether compound eyes or ocelli. Despite differences in visual pigmentation and innervation, we hypothesize that all visual units – whether compound or singular – share a deep homology.'; + TEXT CHARACTER=136 TEXT='Treated as a separate organ from ocelli; see parent character for discussion.^n^nAdapted from characters: 16 and 18 in @Smith2015; 29 and 31 in @Yang2015.^n^nTreated as a separate organ from ocelli; see parent character for discussion.^n^nAdapted from characters: 16 and 18 in @Smith2015; 29 and 31 in @Yang2015.'; + TEXT CHARACTER=137 TEXT='Treated as neomorphic as a stalk represents an additional morphological structure.^n^nCharacter 26 in @Ma2014jsp; character 17 in @Smith2015; and character 30 in @Yang2015. Character 4 in @Smith2015 and @Yang2016 is redundant to this character, so is not included in the present matrix.'; + TEXT CHARACTER=138 TEXT='After character 15 in @Moysiuk2019.^nThe eyes of certain hurdiid radiodonts are dislocated to an extremely posterior location.'; + TEXT CHARACTER=139 TEXT='This new character reflects the hypothesis that sclerotization originated in the protocerebral (preocular) appendages, before being co-opted in trunk appendages. Modified from @Yang2015 character "Cephalic/anterior appendages: Protocerebral limb pair sclerotized"(character 9; also character 21 in @Smith2015).^n^nWe code this character as present in any taxon with sclerotized pre-ocular (protocerebral) limbs, including the podomeres in anomalocaridid "great appendages" [@Daley2014] and the hypostome that covers the euarthropod labrum [e.g. @Edgecombe1999; @Yang2013]. We score this character as uncertain in taxa where the presence of a hypostome is suggested, but not verified (e.g. Alalcomenaeus). The sclerotized stylets and stylet supports of tardigrades are likely modified claws [@Mobjerg2018], hence no appendage sclerotization (or arthrodial membranes) are present.^nThe character is treated as neomorphic, as sclerotization represents a novel increase in the complexity of the appendage.^n'; + TEXT CHARACTER=140 TEXT='May be present only if protocerebral limbs are sclerotized.^n^nThis transformation series distinguishes the arthropodized ‘great appendages’ of anomalocaridids (Daley and Edgecombe 2014) from the hypostome of Euarthropoda (e.g. Edgecombe and Ramsköld 1999; Yang et al. 2013) and the stylet of Tardigrada (e.g. Halberg et al. 2009), both of which are sclerotized but lack soft arthrodial membranes.'; + TEXT CHARACTER=141 TEXT='In most panarthropods, the first pair of limbs is pre-ocular (at least developmentally), is associated with the protocerebral segment, and is structurally differentiated from other limb pairs. In hallucigeniids, however, the first limb pair is not structurally differentiated from its neighbour; moreover, the great distance between the head and the first limb pair in Hallucigenia sparsa [@Smith2015] argues against a pre-ocular or indeed cerebral identity of these appendages. Whether or not the first appendage pair truly corresponds to the pre-ocular appendage of other groups, the absence of a differentiated pre-ocular appendage characterizes a number of armoured lobopodians: Xenusion [@Dzik1989], Diania [@Ma2014jsp], Microdictyon [@Chen1995bnmns], Paucipodia [@Chen1995trse; @Hou2004], H. fortis [@Ramskold1998], and H. sparsa [@Smith2015]. A distinct structure is evident in onychophorans, Antennacanthopodia and Ilyodes (antennae); tardigrades (the stylet apparatus); anomalocaridids (great appendages) [@Cong2014]; Opabinia (proboscis) [@Dhungana2021]; and euarthropods and basal panarthropods (homologues of the labrum) [@Budd2021]. We differ from previous studies in homologizing the antenniform appendages of luolishaniids with frontal filaments, rather than appendage, reflecting their dorsal position and lack of obvious parallels with the differentiated trunk appendages. Coded as ambiguous in taxa where the head is not preserved (including Carbotubulus).^n^nCharacter 20 in @Smith2015.'; + TEXT CHARACTER=142 TEXT='This neomorphic character distinguishes the arthropodized "great appendages" of radiodontans [@Daley2014] from the hypostome of Euarthropoda [e.g. @Edgecombe1999].^nThe sclerotized stylets and stylet supports of tardigrades are likely modified claws [@Mobjerg2018]. No podomeres are present.^n^nAdapted from character 22 in @Smith2015 and character 10 in @Yang2015.'; + TEXT CHARACTER=143 TEXT='Character 33 in @Moysiuk2019. The segments of the first appendage pair are uniform in form (homonomous) along the length of the limb in Anomalocaris, whereas in Hurdia the segments of the distal and proximal sections are strongly distinct.^n^nThe peduncle and outer spines are not considered in this character. The character is treated as neomorphic, as differentiation is seen to reflect a greater degree of developmental and morphological specialization.'; + TEXT CHARACTER=144 TEXT='The distalmost podomeres of Caryosyntrips, Hurdia are differentiated and strongly reduce distally, resulting in "inward flexure" of these podomeres.^n^nTreated as applicable even when podomeres are homonomous, as differentiation in size need not depend on differentiation of podomere morphology. Inapplicable in taxa that lack sclerotized protocerebral appendages.^n^nCharacter 35 in @Moysiuk2019.^n'; + TEXT CHARACTER=145 TEXT='We score this character as ventral in Euarthropoda given that the reduced protocerebral appendage pair, transformed into the labrum, occupies a ventral position in association with the mouth [e.g. @Scholtz2006]. As the forward-facing stylet apparatus of tardigrades is internalized into the mouth cone [@Halberg2009], the position of the stylets are not independent of the mouth position, therefore we code this as an alternative character state. ^n^nCharacter 26 in @Smith2015 and character 16 in @Yang2015.'; + TEXT CHARACTER=146 TEXT='This character reflects the migration of the frontal appendages from an ancestrally anterior position, as in lobopodians (e.g. Kerygmachela, Jianshanopodia, Pambdelurion, Siberion), to a more posterior (e.g. Megacheirans and Leanchoiliids) and ultimately ventral position, as in the euarthropod labrum [@Budd2021].^n^nAs the direction of evolution is well attested by developmental data, we treat this character as neomorphic.'; + TEXT CHARACTER=147 TEXT='Modified from character 16 in @Ma2014jsp to reflect the posited homology between the anterior appendages of lobopodians and the euarthropod labrum [cf. @Eriksson2000; @Budd2002]: specifically, the euarthropod labrum is coded as a fused pair of appendages [@Scholtz2006; @Liu2009; @Liu2010; @Posnien2009]. The stylet apparatus of Tardigrada is not coded as fused, as each stylet within the buccal tube remains independent despite significant modification [@Dewel2006; @Halberg2009; @Guidetti2012].^n^nCharacter 27 in @Smith2015 and 17 in @Yang2015.'; + TEXT CHARACTER=148 TEXT='In Opabinia, Caryosyntrips and cf. Peytoia [@Moysiuk2021], the protocerebral appendages are adjacent to the other, without a gap; in radiodonts such as Anomalocaris canadensis, the protocerebral appendages are separated by a gap [e.g. @Daley2014, fig. 1]. The situation is unclear in many hurdiids due to limited preservation of appendage bases. The adjacency of bases is a prerequisite for the physical mechanical fusion of the protocerebral appendages.'; + TEXT CHARACTER=149 TEXT='In Kerygmachela, Pambdelurion and Siberion, the appendages have migrated into an adjacent position but are not mechanically connected [@Budd1993; @Budd1998ar; @Budd1998trse; @Dzik2011]; this also seems to be the case in radiodontans [@Daley2009; @Daley2014]. In euarthropods, the appendages exhibit a degree of fusion.^n^nCharacter 28 in @Smith2015, cf. character 17 in @Yang2015.^n'; + TEXT CHARACTER=150 TEXT='This neomorphic character represents the loss of claws on the (differentiated) protocerebral appendage as compared to the (undifferentiated) trunk appendages. By definition, taxa with undifferentiated protocerebral appendages have not undergone loss of claws on those appendages. Taxa without claws are coded as ambiguous as we cannot tell if a claw suppression mechanism acts silently in the protocerebral appendages; in other words, the gain or loss of claws on the trunk represents a separate neomorphic event and is thus independent of this character.^n'; + TEXT CHARACTER=151 TEXT='This neomorphic character refers to the spines/spinules present in the most anterior appendage pair of anomalocaridids [@Daley2009; @Daley2014], gilled lobopodians [Kerygmachela, see @Budd1993, @Budd1998trse; Pambdelurion, see @Budd1998ar; Opabinia, see @Budd1996] and certain lobopodians [e.g. Aysheaia, see @Whittington1978; Jianshanopodia, see @Liu2006; Megadictyon, see @Liu2007az; Onychodictyon ferox, see @Ou2012].^n^nWe treat the presence of lateral and ventral spine series as different characters. Certain taxa (e.g. Stanleycaris, cf. Peytoia) have both series present, whereas other taxa (e.g., Caryosyntrips) have only lateral spine series (see @Moysiuk2021). We extend this homology scheme to stem-euarthropods with lateral spine series such as Kerygmachela, Pambdelurion and Opabinia following @Dhungana2021. Lateral spine series (referred to as "gnathal" spines in @Moysiuk2021) in sclerotized appendages are often characterized by small asymmetric accessory spines that originate near the base of the main lateral spine [@Moysiuk2021].^n^nVentral spine series (endites) characterize most radiodonts (with the notable exception of Caryosyntrips). These ventral spines have regularly spaced accessory spines along their length in Hurdiids. Anomalocaris and Lyrarapax symmetric accessory spines originating at the base of the main ventral spines. The similarity of Kylinxia"s "dorsal" spine series to Anomalocaris indicates possible homology [@Zeng2020], and rotation of the protocerebral appendages. See @Guo2019 for an overview of radiodont appendage morphology. ^n^nCharacters 42 of @Zhang2016 is redundant under this formulation, so has been removed from our matrix. Adapted from character 30 in @Smith2015 and 19 in @Yang2015.^n'; + TEXT CHARACTER=152 TEXT='This character pertains to the rows of ventral spines (endites). Amplectobeluidae and Anomalocarididae have two rows, Hurdiidae have one row [@Guo2019].^n^nPrevious formulation inspired by char. 31 in @Smith2015 and char. 20 in @Yang2015. These matrices did not separate the lateral from ventral spine series (see discussion in character description above: Protocerebral appendage pair: Spine series).^n'; + TEXT CHARACTER=153 TEXT='Hurdiids typically have very long main spines (endites) compared to the thickness of the shaft of the appendage. We treat the ventral spine series as distinct from lateral spine series [following @Dhungana2021], and limit this character to ventral spines. ^n^nAdapted from @Zeng2020 character 191; @Aria2019 character 90.'; + TEXT CHARACTER=154 TEXT='Accessory spines to the main ventral spines of the protocerebral spine series of many radiodonts.'; + TEXT CHARACTER=155 TEXT='Hurdiids have accessory spines arranged in a regular series along the main spine, whereas e.g. Anomalocaris canadensis has accessory spines originating near the base of the main spine, giving a multifurcate appearance.^n'; + TEXT CHARACTER=156 TEXT='Character 44 in @Vinther2014 and 41 in @Moysiuk2019. The endites of certain anomalocaridid appendages alternate in length from podomere to podomere. Treated as neomorphic as alternation represents additional complexity in developmental control.'; + TEXT CHARACTER=157 TEXT='Spine series can be comparable in width to the base of the podomere/annulation, or be significantly narrower.^n^nCharacter adapted from char. 192 in @Zeng2020; char. 108 in @Aria2019^n'; + TEXT CHARACTER=158 TEXT='The large ventral endites of radiodonts can increase in size from base to tip e.g., Hurdia, Peytoia, Stanleycaris. Treated as transformational. See table 1 in @Guo2019, and figure 2 in @Pates2019.'; + TEXT CHARACTER=159 TEXT='The orientation of spine series are independent of the position of the spine series. In Hurdiids, for example, the main enditic spines are in a ventral position, but spines curve such that the distal tips face the other appendage. In gilled lobopodians and Caryosyntrips, spine series point towards the other appendage. In Anomalocaris, the ventral spine series do not face the other appendage, but are straight and point outwards (ventrally).'; + TEXT CHARACTER=160 TEXT='@Moysiuk2021 suggest that the laterally located gnathal spine series in e.g. Caryosyntrips is independent of the ventral enditic spine series observed in many radiodonts. We treat the lateral spine series of e.g. Aysheaia and gilled lobopodians as equivalent.'; + TEXT CHARACTER=161 TEXT='This neomorphic character describes the multifurcate termination observed in the protocerebral appendages of dinocaridids [@Budd1996; @Daley2009; @Daley2010; @Budd2012; @Daley2014] and certain lobopodians -- such as Aysheaia [@Whittington1978], Megadictyon [@Liu2007az] and Kerygmachela [@Budd1993; @Budd1998trse] -- but absent in Onychodictyon ferox [@Ou2012].^nCoded as inapplicable in tardigrades due to the extremely modification of the pre-ocular appendage into a stylet apparatus, which poses challenges to the identification of homologues of appendicular features.^n^nCharacter 33 in @Smith2015 and 22 in @Yang2015.^n'; + TEXT CHARACTER=162 TEXT='In Amplectobelua, Lyrarapax and Anomalocaris saron, the distal appendage kinks outwards at a high angle relative to the appendage peduncle (shaft).^n^nCharacter 36 in @Moysiuk2019, following character 27 in @Vinther2014.^n'; + TEXT CHARACTER=163 TEXT='Character 35 in @Vinther2014 and 40 in @Moysiuk2019. In Amplectobelua and Lyrarapax, a proximal endite projects forwards to oppose the distal endites, forming a "pincer" or "claw".'; + TEXT CHARACTER=164 TEXT='Most radiodonts have outer spine series in addition to inner spine series on the protocerebral appendage [@Moysiuk2019] also referred to as "dorsal" spines [e.g. @Zeng2020], typically the spines in this series are larger distal-ward. This spine series appears to be independent of the medial/ventral spine series [@Moysiuk2019]^n'; + TEXT CHARACTER=165 TEXT='Adapted from char. 55 in @Moysiuk2021, who note that auxiliary spines are present on the lateral spines/gnathites of Stanleycaris and cf. Peytoia.^n'; + TEXT CHARACTER=166 TEXT='Character atomized from previous formulation to reflect complexity in "arthropodization" of the post-ocular appendages. ^n^nAdapted from character 19 in @Smith2015 and 8 in @Yang2015.'; + TEXT CHARACTER=167 TEXT='The cylindrical ambulacral lobopodous leg characteristic of lobopodians is also found in Opabinia [@Budd1996; @Budd2012], Kerygmachela [@Budd1993; @Budd1998trse], Pambdelurion [@Budd1998ar] and Aegirocassis [@VanRoy2015]. Coding for radiodontans follows @VanRoy2015.^n^nCharacter 23 from @Smith2015 and 11 in @Yang2015.'; + TEXT CHARACTER=168 TEXT='There are various taxa in which the deutocerebral appendage pair is morphologically differentiated from the rest of the trunk appendages [see references in @Liu2014ppp]. For example, Antennacanthopodia has a second set of antenna-like limbs that are morphologically distinct from the walking legs [@Ou2011]. The first pair of legs in Tardigrada is serially homologous with the deutocerebral segment of Euarthropoda [@Mayer2013po], and thus is not structurally different from the rest of the trunk appendages. The deutocerebral jaws of Onychophora are significantly modified relative to the rest of the appendages in the body [@Eriksson2010; @Oliveira2013]. In Euarthropoda, this morphological differentiation is generally expressed in the presence of an antenniform [e.g. @Edgecombe1999; @Ma2012n; @Yang2013] or raptorial [@Chen2004; @Haug2012p; @Tanaka2013] deutocerebral appendage. The second leg pair of hallucishaniid taxa are not differentiated from their neighbours [@Ramskold1998] and are therefore coded as undifferentiated; the trunk limbs are instead divided into two morphological zones.^n^nCharacter 24 in @Smith2015 and 14 in @Yang2015.'; + TEXT CHARACTER=169 TEXT='This character, adapted from char. 25 from @Smith2015 and char. 12 from @Yang2015, has been re-formulated into two separate characters on the basis that the arthropodization of the first post-ocular appendage is not independent from the arthropodization of the subsequent trunk appendages. This formulation makes it unnecessary to distinguish taxa with differentiated deutocerebral appendages (e.g., char. 24 @Smith2015).^n'; + TEXT CHARACTER=170 TEXT='The first post-ocular limb is not observable in Tertiapatus or Ilyodes [@Poinar2000; @Haug2012cb], and is thus scored as ambiguous. It is difficult to evaluate the role of the slender appendages of Hallucigenia [@Ramskold1998; @Smith2015] and the cirrate post-ocular appendages of Luolishania, Collinsium, Acinocricus and the Collins monsters [@Ma2009; @Garcia2013; @Yang2015; @Caron2020]; as such, these are coded as ambiguous for states "ambulatory" and "sensorial".^n^nCharacter 25 from @Smith2015 and 12 from @Yang2015.^n'; + TEXT CHARACTER=171 TEXT='See character 13 in @Yang2015.^nPresent in Peripatidae [@Oliveira2013], but absent in Euperipatoides [@Smith2014].'; + TEXT CHARACTER=172 TEXT='As with character relating to the nature of the deutocerebral appendages, this character is coded as a separate character in taxa with lobopodous and with arthropodized appendages. Ilyodes [@Haug2012cb], Tertiapatus [@Poinar2000] and extant onychophorans are interpreted as bearing paired oral papillae.^n^nAdapted from character 15 in @Yang2015.'; + TEXT CHARACTER=173 TEXT='The tritocerebral appendages of fuxianhuiids are reduced for a sweep-feeding function [@Yang2013].^n^nAdapted from character 15 in @Yang2015.'; + TEXT CHARACTER=174 TEXT='Annulations are repeated superficial integument rings.^n^nCharacter 26 in @Daley2009, 37 in @Smith2015 and 36 in @Yang2015. WTS27.^n^n^nAnnulations are repeated superficial integument rings. Coded as present in Eokinorhynchus, reflecting ring-like nature of epidermal ‘segments’ (Zhang et al. 2015). Present in Fieldia, reflected by transverse arrangement in spines in certain specimens (e.g. USNM57715, see (Caron 2011)) The cuticle of Anatonchus bears hints of fine annulations on its tip (Peneva et al. 1999; Choudhary et al. 2009); the cuticle of Kinonchulus is ‘delicately annulated’ (Riemann 1972). Coded ambiguous in Selkirkia and Paraselkirkia as the trunk is concealed by the tube, and ambiguous in Palaeopriapulites and Sicyophorus as the ‘trunk’ is putatively concealed by a lorica (Hou et al. 2017).^n^nTaxa in which annulations are present on the appendages but not the trunk are coded ambiguous.'; + TEXT CHARACTER=175 TEXT='This character distinguishes between annulation patterns that are uniform along the length of the trunk (homonomous) from those which display serially repeated differentiated fields (heteronomous), usually associated with the location of limbs.^n^nCharacter: 29 in @Liu2011; 27 in @Daley2009; 40 in @Smith2015 and 38 in @Yang2015. WTS27.'; + TEXT CHARACTER=176 TEXT='The bulbous heads of Hallucigenia fortis, Microdictyon, Cardiodictyon and Luolishania lack annulations [@Chen1995bnmns; @Ma2009; @Ma2012asd; @Liu2014ppp]. In contrast, annulations continue to the tip of the head in Paucipodia, Onychodictyon gracilis, and Diania (whichever end of Diania is interpreted as anterior) [@Chen1995trse; @Hou2004; @Liu2008app; @Ma2014jsp].^n^nIn contrast to character 39 in @Smith2015, we do not interpret the introvert of priapulans or lobopodians as part of the trunk.'; + TEXT CHARACTER=177 TEXT='Unbranched in Aysheaia, Siberion, Onychodictyon, Diania, Xenusion, Paucipodia, Microdictyon, Luolishania, the Collins Monsters, Acinocricus, Jianshanopodia, Hadranax and Kerygmachela [@Whittington1978; @Caron2020; @ConwayMorris1988; @Dzik1989; @Chen1995bnmns; @Budd1998p; @Hou2004; @Liu2006; @Liu2008app; @Ma2009; @Ma2014jsp; @Dzik2011; @Ou2012; @Garcia2013; @Yang2015]; branched in Orstenotubulus, onychophorans (i.e. anastomosing plicae) and the Orsten-type lobopodian segment [@Maas2007csb; @Oliveira2014]; ambiguous in Megadictyon, Antennacanthopodia and Tertiapatus [@Poinar2000; @Liu2007az; @Ou2011].^n^nCharacter 51 in @Zhang2016.'; + TEXT CHARACTER=178 TEXT='Epidermal segmentation is a distinguishing feature of Euarthropoda [@Budd2001za; @Edgecombe2009]. Although the body of Onychophora and Tardigrada is metamerically organized, both at the level of segment polarity gene expression [@Gabriel2007; @Eriksson2009] and musculature [e.g. @Halberg2009; @Marchioro2013], this pattern is not expressed on the epidermis: we thus score it as absent in these phyla. Epidermal segmentation is not evident in most radiodontans [e.g. @Daley2014], which we score absent. Kinorhynchs and annelids also exhibit a segmented epidermis; though this presumably has an independent derivation from the segmentation of arthropods, the lack of a clear morphological basis for discrimination means separate character states cannot be assigned to these phyla.^n^nCharacter 25 in @Daley2009, 34 in @Smith2015 and 32 in @Yang2015.'; + TEXT CHARACTER=179 TEXT='The development of sclerotized tergal plates connected by arthrodial membranes is distinctive of body arthrodization, and thus exclusive to Euarthropoda [@Edgecombe1999; @Haug2012p; @Yang2013]. Given the morphological similarity between arthropod tergites and the articulated tergal and sternal plates of kinorhynchs [e.g. @Sorensen2008, @SchmidtRhaesa2012], we treat the latter using the same transformation series, though noting that the structures are almost certainly not homologous. Plates in kinorhynchs arise through progressive sclerotization of flexible cuticle through ontogeny [@SchmidtRhaesa2012], and are thus not considered to be equivalent to epidermal sclerites.^n^nAlthough some heterotardigrades possess dorsal plates (e.g. Nelson 2002; Marchioro et al. 2013; Persson et al. 2014), these are not connected by arthrodial membranes and thus score the heterotardigrade terminal Actinarctus as absent for this transformation series.^n^nCharacter 35 in @Smith2015, 33 in @Yang2015.'; + TEXT CHARACTER=180 TEXT='Sternites – ventral sclerotized plates – are a key feature of most Euarthropoda, and are well documented in Artiopoda [e.g. @Whittington1993; @Edgecombe1999; @Ortega2012]. Sternites are notably absent in Fuxianhuiida [@Chen1995s; @Waloszek2005; @Bergstrom2008; @Yang2013], even though these taxa have a sclerotized dorsal exoskeleton. We code sternites as uncertain in leanchoiliids. Given the morphological similarity between arthropod sternites and the articulated sternal plates of kinorhynchs (e.g. Sørensen 2008), the latter are also scored as present.^n^nCharacter 36 in @Smith2015 and 34 in @Yang2015.'; + TEXT CHARACTER=181 TEXT='Present in Pycnophyidae and Neocentrophyidae (Kinorhyncha); see character 14 in (Sørensen et al. 2015)'; + TEXT CHARACTER=182 TEXT='See character 17 in @Sorensen2015'; + TEXT CHARACTER=183 TEXT='See character 23 in @Sorensen2015'; + TEXT CHARACTER=184 TEXT='See character 19 in @Sorensen2015'; + TEXT CHARACTER=185 TEXT='Modified from character 20 in @Sorensen2015; comparison with other plates avoids over-weighting the presence/absence of two sternal plates'; + TEXT CHARACTER=186 TEXT='See character 21 in @Sorensen2015'; + TEXT CHARACTER=187 TEXT='Modified from character 22 in @Sorensen2015; comparison with other plates avoids over-weighting the presence/absence of two sternal plates'; + TEXT CHARACTER=188 TEXT='See character 25 in @Sorensen2015'; + TEXT CHARACTER=189 TEXT='See character 26 in @Sorensen2015'; + TEXT CHARACTER=190 TEXT='See character 40 in @Sorensen2015^nMost kinorhynchs (though not, for example, Kinorhynchus, Neocentrophyes) exhibit prominent lateroterminal spines (Sørensen and Pardos 2008), clearly distinguished from palaeoscolecid/priapulid posterior hooks by their position and morphology.'; + TEXT CHARACTER=191 TEXT='Certain kinorhynchs (Antygomonas, Franciscideres, Cateria, Campyloderes, Centroderes, Echinoderes, Zelnkaderes) exhibit an secondary spine alongside their lateroterminal spine (Higgins 1968; Sørensen and Pardos 2008; Dal Zotto et al. 2013; Neuhaus and Sørensen 2013; Neuhaus et al. 2014; Altenburger et al. 2015; Landers and Sørensen 2016). Others (Pyconophyes, Dracoderes, Paracentrophyes) do not (Sørensen et al. 2010, 2012b; Herranz et al. 2014; Sánchez et al. 2016). See character 38 in (Sørensen et al. 2015).'; + TEXT CHARACTER=192 TEXT='See character 41 in @Sorensen2015; modified to present in Paracentrophyes (Sørensen et al. 2010)^nFollowing the coding of Sorensen where this contradicts the data from (Sørensen and Pardos 2008; Dal Zotto et al. 2013)'; + TEXT CHARACTER=194 TEXT='See character 24 in @Sorensen2015. Treated as neomorphic.'; + TEXT CHARACTER=195 TEXT='See character 27 in @Sorensen2015'; + TEXT CHARACTER=196 TEXT='Scales [@Neuhaus2013za; @SchmidtRhaesa2012] are cuticular, short, triangular to shingle-like processes or projections of the sternal plates, often found in the central region; they give the plates a ''bristled'' appearance.'; + TEXT CHARACTER=197 TEXT='A secondary fringe is a line of small cuticular processes (usually triangular^nscales) at anterior margin of segmental plates [@SchmidtRhaesa2013]. More than one may be present. '; + TEXT CHARACTER=198 TEXT='The nature of the mid-gut glands of Megadictyon, Jianshanopodia, Pambdelurion and Opabinia is elucidated by [@Vannier2014]. Midgut glands were biologically, rather than taphonomically, absent in Ilyodes [@Haug2012cb], Hallucigenia sparsa [@Smith2015], Lyrarapax [@Cong2014], Acinocricus [@ConwayMorris1988] and Collinsium [@Yang2015].^n^nCharacter 42 in @Ma2014jsp; 16 in @Daley2009; 53 in @Smith2015 and 52 in @Yang2015.^n^n^n---^nCoded as uncertain in Antennacanthopodia (Ou et al. 2011) because the dark infilling of the type material may represent decayed internal organs. The nature of the mid-gut glands of Megadictyon, Jianshanopodia, Pambdelurion and Opabinia is elucidated by Vannier et al. (2014).'; + TEXT CHARACTER=199 TEXT='Lobopodians have a relatively cylindrical trunk with a uniform width, whereas the trunk of anomalocaridids narrows markedly towards the posterior.^n^nCharacter 65 in @Moysiuk2019.^n'; + TEXT CHARACTER=200 TEXT='This character reflects the pronounced differentiation of the posterior and anterior trunk – not just the trunk appendages – in certain lobopodians. In Hallucigenia sparsa, the region of the trunk anterior of the third appendage pair is narrower, lacks dorsal armature, and expresses differentiated appendages [@Smith2015]. The short constricted region anterior of the first spine pair in H. fortis is associated with two differentiated appendage pairs [@Ramskold1998] and apparently corresponds with the ‘neck’ of H. sparsa. In luolishaniids, the anterior body bears elongate limbs with accentuated armature [@Ma2009; @Garcia2013]. The portion of the trunk in Carbotubulus corresponding to the first two or three leg pairs is substantially narrower than the posterior trunk and its associated appendages are narrower and less prominent than the posterior appendages, indicating trunk differentiation [@Haug2012cb]. Although the width of the trunk narrows gradually towards the front of Paucipodia, this tapering is gradual and does not correspond to the differentiation of the anterior trunk [@Chen1995trse; @Hou2004]. Coded ambiguous in Orstenotubulus, Hallucigenia hongmeia, and Ilyodes due to incomplete preservation [@Thompson1980; @Maas2007csb; @Steiner2012].^n^nCharacter 54 in @Smith2015 and 72 in @Yang2015.^n^n^n---^nAfter transformation series 54 in Smith & Caron (2015). The differentiation observed in lobopodians (see below) is also reflected in the organisation of Louisella and Tylotites (Conway Morris 1977a; Han et al. 2007c; Zhang et al. 2015), where the anterior trunk has a different annulation pattern to the posterior portion, with an abrupt change separating the two regions. The “neck” of Eokinorhynchus is not consistently distinguishable from the introvert, and is considered to represent part of that structure. The same is arguably true in Halicryptus higginsi, though not in H. spinulosus. The anterior annulations of H. higginsi are much more closely spaced and bear denser setae than the posterior annulations (Shirley and Storch 1999), but in the absence of a sharp distinction between the regions this character is scored as ambiguous. Coded absent in Cricocosmia (Hou et al. 2017); the diminution of annulations anteriad is reflected in a separate transformation series, and there is no clear morphological division of an anterior portion of the trunk.^nThis transformation series reflects the pronounced differentiation of the posterior and anterior trunk – not just the trunk appendages – in certain lobopodians. In Hallucigenia sparsa, the region of the trunk anterior of the third appendage pair is narrower, lacks dorsal armature, and expresses differentiated appendages (this study). The short constricted region anterior of the first spine pair in H. fortis is associated with two differentiated appendage pairs (Ramsköld and Chen 1998) and apparently corresponds with the ‘neck’ of H. sparsa. In luolishaniids, the anterior body bears elongate limbs with accentuated armature (Ma et al. 2009; García-Bellido et al. 2013). The portion of the trunk in Carbotubulus corresponding to the first two or three leg pairs is substantially narrower than the posterior trunk and its associated appendages are narrower and less prominent than the posterior appendages, indicating trunk differentiation (Haug et al. 2012c). Although the width of the trunk narrows gradually towards the front of Paucipodia, this tapering is gradual and does not correspond to the differentiation of the anterior trunk (Chen et al. 1995a; Hou et al. 2004). Coded ambiguous in Orstenotubulus, Hallucigenia hongmeia, and Ilyodes due to incomplete preservation (Thompson and Jones 1980; Maas et al. 2007; Steiner et al. 2012).^nCoded as absent in loriciferans (Neves et al. 2016), Sirilorica (Peel et al. 2013).^nThe anterior 5–8% of the trunk of Meiopriapulus bears trunk scalids and lacks the wrinkles, tubercles and other structures of the posterior trunk (Sørensen et al. 2012a)^nTubiluchus lemburgi has a distinctive anterior trunk marked by a change in diameter and surface ornament (Schmidt-Rhaesa et al. 2013)^nCoded as present in Paratubiluchus (Han et al. 2004).^nPresent in Paraselkirkia, where armature becomes enhanced (Hou et al. 2017); not evident in Selkirkia, even USNM 57624 in which the trunk is well extended; but coded ambiguous as posterior trunk unknown.^nCoded as present in Eximipriapulus (Ma et al. 2014b), though with the caveat that the differential appearance of the neck might conceivably be attributed to preservational factors^nCoded absent in Markuelia; the unannulated region in e.g. Dong et al. fig. 10D seems to correspond to the introvert, as suggested by the presence of a single row of spines (cf. trichoscalids, anterior head setae of nematodes); it is not demarked from the rest of the trunk by a change of thickness etc.'; + TEXT CHARACTER=201 TEXT='A proposed synapomorphy of Scalidophora, though absent in loriciferans, and not really codable as present in priapulids (Sørensen et al. 2008) – thus alternatively proposed as a synapomorphy of kinorhynchs and loriciferans (Neuhaus and Higgins 2002)^nThis said, the spines in kinorhynchs (e.g. Zelinkaderes, Neuhaus and Higgins 2002) are not restricted to the mid-trunk – more occur more posteriorly ^n## For a more careful description see Schmidt-Rhaesa 1997⁄98; Lemburg 1999; Neuhaus and Higgins 2002^nUnambiguously absent in Halicryptus and Tubiluchus Higgins larvae (Higgins and Storch 1989; Storch and Higgins 1991; Higgins et al. 1993), but present in Pripaulus (van der Land 1970) and early larvae of Maccabeus (Por and Bromley 1974)'; + TEXT CHARACTER=202 TEXT='WTS36^nFlosculi have been proposed as a synapomorphy of Scalidophora (Lemburg 1995; Nielsen 2012). They are raised, flower-like structures with a central cilium [@SchmidtRhaesa2015].^nAmong loricifera, present only in Nanaloricus and Pliciloricus (Neves and Kristensen 2014).^nCoded ambiguous in Chordodes (Bolek et al. 2010) as it is unclear whether any of its areoles might be considered equivalent to flosculi.^nFlosculi are really tiny (Storch and Alberti 1985) and the chances of picking them out from cuticular ridges in Burgess Shale-type fossils are slim – such taxa are coded ambiguous accordingly. Coded absent in Schistoscolex as the preservation is of sufficient fidelity, and the posterior region of is preserved (Duan et al. 2012)^n^nSensory spots are flat-lying regions of the cuticle, surrounding a cilliated pore, covered in small projections [@SchmidtRhaesa2015]'; + TEXT CHARACTER=203 TEXT='Flosculi in Maccabeus (Por and Bromley 1974), Meiopriapulus and Tubiluchus (Sørensen et al. 2012a), kinorhynchs and loriciferans [[Nematomorpha, Priapulida, Kinorhyncha, Loricifera edited by Andreas Schmidt-Rhaesa]]'; + TEXT CHARACTER=204 TEXT='Flosculi in priapulids have petal-like structrues (typically eight) (Wills et al. 2012); in loriciferans they do not bear clear petals (Neves et al. 2016)'; + TEXT CHARACTER=205 TEXT='WTS36'; + TEXT CHARACTER=206 TEXT='Treated as transformational as it is not clear whether the absence of papillae on limbs represents a differentiation of the limbs (and the introduction of a separate developmental regime to pattern them independently from the trunk)^n^nCharacter 41 in @Ma2014jsp; character 50 in @Smith2015 and 51 in @Yang2015.^n'; + TEXT CHARACTER=207 TEXT='Louisella and Onychodictyon ferox bear transverse rows of ventral papillae (Conway Morris 1977a; Ou et al. 2012)'; + TEXT CHARACTER=208 TEXT='A lorica is inferred to be present in a larval stage of any taxon in which it is present in an adult, even if certain taxa also exhibit post-hatching, pre-loricate larval stages [@Janssen2009].^n^nThe placids of kinorhynchs and the lorical ring of loriciferans and larval priapulids form cuticular plates that surround the neck region of the respective organisms (Wennberg et al. 2009; Peel et al. 2013; Sørensen et al. 2015); whilst placids conceivably represent reduced lorical plates, they are not considered homologous and are treated as two separate transformation series. Coded absent in Shergoldana as the plates do not form clear rings and do not clearly girdle the neck (Maas et al. 2007a).^nCoded as present in Corynetis as a ring of robust plates seems to occur at the anterior margin of specimens with retracted introverts, though there is no indication that these could form a closing apparatus, their size being too large (Hu et al. 2012).^nCoded as ambiguous in Acanthopriapulus as larval stages are unknown (van der Land 1970; Higgins and Storch 1991)^nCf. WTS61.'; + TEXT CHARACTER=209 TEXT='Cf. WTS24.^n^nPlates not retained in Tubiluchus; no reference available to support retention in T. vanuatensis (Kirsteuer and Ruetzler 1973; Calloway 1975; Kirsteuer 1976)^nThere is a possibility that the ‘theca’ of Laojieella (Han et al. 2006) is homologous with loriciferan plates (cf. Priapulus Higgins larvae with a prominent dorsal and ventral plate, and Sirilorica with prominent regions of the trunk anterior and posterior of its lorica), but this cannot be substantiated, so Laojieella is coded as ambiguous.^nThe lorica of Sicyophorus is considered to represent the adult form due to the size of the organisms (Hou et al. 2017). I consider Palaeopriapulites to have a lorical too; a distinct anterior margin is evident in some specimens (Hou et al. 2017).?'; + TEXT CHARACTER=210 TEXT='Cf. WTS62^nPresent in Halicryptus (Storch and Higgins 1991)^nAbsent in Sicyophorus; may be present in Palaeopriapulites (coded ambiguous) (Maas et al. 2007c; Hou et al. 2017)'; + TEXT CHARACTER=211 TEXT='Coded as ambiguous in macrofossils that do not preserve loricae, as the early developmental stages are unknown.^n'; + TEXT CHARACTER=212 TEXT='Number of lorical plates in a single ring, when multiple series are present'; + TEXT CHARACTER=213 TEXT='In certain priapulans the dorsal and ventral plates are substantially larger than the slender lateral plates'; + TEXT CHARACTER=214 TEXT='Many ecdysozoans bear cuticular sclerites on their trunk (i.e. posterior of the neck or proboscis). We recognize three broad categories of sclerites: (i) integumentary trunk sclerites: densely arranged sclerites that cover the trunk; (ii) sparse specialized sclerites: sparsely arranged sclerites specialized for a specific purpose (e.g. sensory sclerites, claws); (iii) enlarged dorsal sclerites: often paired, reinforced or sculptured, and with a presumed defensive function. These elements are likely homologous as sclerites, yet each category may be controlled by a distinct genetic toolkit. The broad character of ''epidermal sclerites'' is therefore present in most taxa in this matrix, and is coded ambiguous in many fossil taxa given the often diminutive scale of sensory sclerites. Secondary characters, each neomorphic, record the existence of sclerites in each of the three categories.^n^nWe include the setae, tubes, spines and processes of Kinorhyncha and Loricifera as sclerites. Lorical plates seem to form through the thickening of cuticle and are not treated as sclerites.^n^n^n----^n^nTransformation series 41 in Ma et al. (Ma et al. 2014a) and 30 (and cf. 29) in Wills (2012). We code Orstenotubulus as uncertain as its papillae are not clearly observed throughout the trunk region (Maas et al. 2007b).^nSpine-like ornament of Tylotites (Han et al. 2007c)^nSpines in Louisella (Smith 2015),^nRings of papillae in certain lobopodians (e.g. Aysheaia, onychophorans)^nPresent in Eokinorhynchus (Zhang et al. 2015).^nOccur, seemingly in rings, in Markuelia (Haug and Maas 2009; Dong et al. 2010)^nAbsent in Cricocosmia and Tabelliscolex (Han et al. 2007b).^nDetails of Mafangscolex given by (Liu et al. 2016)^nDetail of Maotianshania mentioned in (Hu et al. 2012)^nAmbiguous in Shergoldana as adult state unknown.^nAmbiguous in Antennacanthopoda (Ou et al. 2011) as preservational quality insufficient to discern,^nAreoles in Chordodes [@Bolek2010] are treated as epidermal plates.^nRound non-mineralized plates adorn the posterior trunk of Ancalagon (pers. obs.)^nSpines adorn the surface of Fieldia (ROM 93-1678; @ConwayMorris1977) ^nSmall spines (setae) occur on Halicryptus and Priapulopsis (van der Land 1970). Somatic setae occur irregularly on Kinonchulus (Riemann 1972) and in other onchulids (Olovachov et al. 2008)^nAbsent in Maccabeus (Por and Bromley 1974)^nRobustly-topped spines in Aysheaia (Whittington 1978)^nSpines are present at least in the anterior trunk of Selkirkia and Paraselkirkia (termed ‘Zone C of the proboscis’ by @ConwayMorris1977); this was variably emergent from the tube (see Caron 2011)^nNot reported in Kerygmachela (Budd 1998a)'; + TEXT CHARACTER=215 TEXT='This transformation series is coded as present in any taxon where sclerites comprise stacked constituent elements at all stages of growth (as in Hallucigenia sparsa and Euperipatoides, see main text), not just during ecdysis (as in Onychodictyon, see Topper et al. 2013). Where sclerites are not preserved in sufficient detail to assess their construction, this transformation series is coded as ambiguous.'; + TEXT CHARACTER=216 TEXT='This character describes sclerites that are broadly distributed across much of the trunk integument.^n^nIn taxa such as Hallucigenia, trunk sclerites are absent, leaving only the enlarged sclerites (dorsal spines and claws).^n^nThe tergal plates of kinorhynchs derive through thickening of the trunk cuticle, hence these do not represent trunk sclerites.^n^n---^n^nPhosphatized Hadimopanella-like plates characterize palaeoscolecids sensu lato (Harvey et al. 2010).^n^nPlates in Louisella have the same properties as non-mineralized cuticular structures.^nNematomorph areoles are cuticular, not mineralized (Bolek et al. 2010)^nSpines are heavily chitinised in Acanthopriapulus (van der Land 1970)^nSpines of Corynetis are not obviously mineralized (Huang et al. 2004a)^n'; + TEXT CHARACTER=217 TEXT='Palaeoscolecid plates are routinely preserved in three dimensions as phosphate. Traces of phosphorous, as occur in e.g. Hallucigenia spines in the Burgess Shale, are not taken to denote a heavy degree of original phosphatization, so taxa where a phosphatic composition is not robustly attested are coded as lacking heavy phosphatization.'; + TEXT CHARACTER=218 TEXT='Circular in Scathascolex, Wronascolex spp.^nElongated parallel to body axis in Palaeoscolex piscatorum^nEssentially circular in Chordodes (Bolek et al. 2010)^ncf. WTS30'; + TEXT CHARACTER=219 TEXT='Nodes are raised lumps, arranged in a series parallel to the plate margin^nBlackberry areoles in Chordodes have a similar construction, even if the nodes are irregularly distributed (Bolek et al. 2010) – but these areoles are perhaps better considered as equivalent to platelets, by comparison with priapulid tumuli.^nSchistoscolex has four nodes in an irregular ring (Müller and Hinz-Schallreuter 1993)'; + TEXT CHARACTER=221 TEXT='Palaeoscolex piscatorum has eight to ten nodes on its plates (Conway Morris 1997)^nScathascolex sometimes has five, perhaps sometimes has four as well?^nWronascolex antiquus has four to six ^nWronascolex iacoborum has five, always'; + TEXT CHARACTER=224 TEXT='In certain taxa the anterior and posterior trunk exhibit prominently distinct sclerite morphology, even if the trunk itself may not be differentiated'; + TEXT CHARACTER=226 TEXT='This character refers to integumentary trunk sclerites. Enlarged sclerites often exhibit a distinct distribution (as in Eokinorhynchus); if only enlarged sclerites are present (as in Hallucigenia, treating claws and spines as enlarged trunk sclerites), this character is inapplicable.^n^nPlates of Corynetis form clear transverse rows (Huang et al. 2004a)^nThose of Tubiluchus lemburgi form longitudinal rows that occasionally arise or pinch out (Schmidt-Rhaesa et al. 2013)^nTaxa with a differentiated fore-trunk (e.g. Eximipriapulus, Meiopriapulus) often show a more regular arrangement in their ‘neck’; the arrangement in the trunk (which is typically irregular) is what is coded here.^nSome ordering is apparent in Selkirkia, where the rows are clearly diagonal/quincuncial^nIn ventrolateral, bilaterally paired groups of one or more elements'; + TEXT CHARACTER=227 TEXT='Wronascolex antiquus has a single row of plates on each annulation. Scathascolex minor has a row of plates on each margin of each annulation; within each row, sclerites are longitudinally paired. I have interpreted this as two primary fields per annulation, each comprising two rows of sclerites. cf. WTS30^nAmbiguous (at best) in Louisella (Conway Morris 1977a, 1997; Smith 2015)^nProminently single in Tylotites (Han et al. 2007c)^nSeemingly single in Chalazoscolex (Conway Morris and Peel 2010)'; + TEXT CHARACTER=228 TEXT='This character primarily has in mind the regimented distribution of plates within each plate field of palaeoscolecid worms.'; + TEXT CHARACTER=229 TEXT='The plates of Corynetis form a quincuncial arrangement, a consequence of each subsequent transverse row being offset relative to the previous (Huang et al. 2004a).'; + TEXT CHARACTER=230 TEXT='Microplates are smaller than plates and platelets and are expressed as a patterning of the cuticle. [tbc]^nAmbiguous in Louisella and Tylotites as plates are not strongly preserved; preservational quality is inadequate to assess the presence of microplates'; + TEXT CHARACTER=232 TEXT='This character captures the differentiation of individual sclerites to specialized roles, including sensory and locomotory sclerites. The specific role is not specified, reflecting the fact that sclerites may serve multiple roles (for example, many priapulan scalids are sensory structures used in locomotion) and the possibility that a the primary role of a structure may vary depending on context. Moreover, the function of a sclerite is difficult to infer from fossil material.'; + TEXT CHARACTER=233 TEXT='WTS34^nTubuli are distinctive tube-like projections arising from the trunk in certain priapulids (at loricate and adult stages) [@SchmidttRhaesa2013] (e.g. Janssen et al. 2009). In Tubilucus, these are adhesive organs with a bulbous base and a stiff tapering tube (Todaro and Shirley 2003).'; + TEXT CHARACTER=234 TEXT='WTS33^nTumuli are small papillae: round-topped cuticular wart-like structures [@SchmidtRhaesa2012]. In Tubiluchus they are supported at their periphery by cuticular ridges, giving them a star-shaped aspect (Todaro and Shirley 2003).^nThis character is applied inclusively to incorporate any case where small sclerites occur alongside regular sclerites, with an equivalent distribution and ornamentation.^n^nVariation in plate size in Chordodes is neither systematic nor substantive (Bolek et al. 2010); this taxon is coded as having plates of a single size.'; + TEXT CHARACTER=235 TEXT='Priapulid tumuli have a distinctively star-shaped appearance (Schmidt-Rhaesa et al. 2013)'; + TEXT CHARACTER=236 TEXT='Taxa such as Eokinorhynchus exhibit two size classes of sclerites: small sclerites borne on individual annulations, which typically cover much of the trunk; and individual sclerites that are prominently larger. These larger sclerites often include a prominent spine. There is a continuity in morphology between these spines and the dorsolateral specializations in cricocsmiids, in Microdictyon and Onychodictyon, and in hallucishaniids. We therefore consider these sclerites as potential homologues. In Shergoldana the enlarged sclerites form tessellating plates that encircle the trunk, corresponding to the position of plicae in loriciferan Higgins larvae [see e.g. @Neves2019] – which in some cases also exhibit a broad base and a pointed apical projection.^n^nThe nodes, plates and spines of lobopodian taxa (TS32) represent epidermal evaginations; the paired sclerotized dorsal plates of Actinarctus (Heterotardigrada) are also interpreted as epidermal evaginations (e.g. Nelson 2002; Marchioro et al. 2013; Persson et al. 2014). The paired pits that serve as muscle attachment sites in Halobiotus (Eutardigrada) are not treated as homologous (Halberg et al. 2009; Marchioro et al. 2013). We code Paucipodia, Diania and Aysheaia as uncertain; their preservation is insufficient to establish whether the paired specializations are node-like evaginations or pit-like depressions (Chen et al. 1995a; Liu and Dunlop 2014; Ma et al. 2014a).^nShergoldana bears three rings of four epidermal evaginations (Maas et al. 2007a); we follow the model of Dzik and Krumbiegel (Dzik and Krumbiegel 1989) and code these in the same fashion as the trunk developments of certain palaeoscolecids (e.g. Cricocosmia) and lobopodians (e.g. Microdictyon).^n^n^n---^nThis character refers to the differentiated epidermal regions found on the dorsal side of most lobopodians. The epidermal specialization is usually conspicuous, as in the paired nodes of Xenusion [@Dzik1989], Hadranax [@Budd1998p] and Kerygmachela [@Budd1993; @Budd1998trse]; the sclerotized plates of Onychodictyon [@Zhang2007; @Ou2012]; and the spines of Hallucigenia [@Ramskold1992; @Hou1995zjls; @Steiner2012], luolishaniids [@Ma2009; @Yang2015] and Orstenotubulus [@Maas2007csb]. The transformation is also coded as present in the modern tardigrades, denoting the paired pit-like structures associated with each pair of legs. These have been described as sites for muscular attachment in the visceral side of the body wall [e.g. @Halberg2009; @Marchioro2013]; the epidermal specializations of lobopodians have also been interpreted as muscle attachment sites [@Budd2001ed; @Zhang2007].^n^n---^nCharacters 41-42 in @Smith2015 and 39-40 in @Yang2015.^n^nMODIFIED in regards to how tardigrades are treated: plates, but not depressions, are included here, returning to the formulation of e.g. @Nelson2002; @Marchioro2013; @Persson2014.^nThe epidermal depressions of Halobiotus (Eutardigrada), represented by the paired pits that serve as muscle attachment sites [@Halberg2009; @Marchioro2013], are therefore not included.'; + TEXT CHARACTER=237 TEXT='Given the possibility that lobopodian sclerites derived from the plates of palaeoscolecid worms (Dzik and Krumbiegel 1989), we have reformulated this transformation series from (Smith and Ortega-Hernández 2014) to encapsulate the ‘two longitudinal rows’ of sclerites envisioned by trans. ser. 31 in Wills et al. (2012). We still code these as present in tardigrades to represent the possible homology of their epidermal depressions with the epidermal evaginations of other lobopodians (Smith and Ortega-Hernández 2014). Aysheaia is coded as absent as its ‘plates’ (reported by Liu and Dunlop 2014) seem to represent the impressions of the opposite pair of legs (see Whittington 1978). Eokinorhynchus is coded as present as its spines are regularly paired; the seemingly ventral position of the first pair may represent relocation late in development, or deformation of the specimen during preservation. Chalazoscolex is coded present, with the “two to three” individual sclerites occupying the width of each segment (Conway Morris and Peel 2010) assumed to reflect sclerites of the dorsal zone. Loriciferans are scored as present, as their plicae form regular rings of plates around their lorical region [@Neves2016].'; + TEXT CHARACTER=238 TEXT='Cf. WTS32.^n^nWe score Cardiodictyon as having two epidermal specializations (token 1), following suggestions that the apparently single dorsal sclerite is formed by the fusion of a pair of elements (Liu and Dunlop 2014).^n^nIn Loricifera, this character denotes the number of plicae in each ring of the lorica.^n^nWe score Cardiodictyon as having two epidermal specializations, following suggestions that the apparently single dorsal sclerite is formed by the fusion of a pair of elements [@Liu2014ppp]. The plates of Cricocosmia occur in pairs [@Han2007app]. Collinsium bears five primary spines [@Yang2015]; Acinocricus bears seven [@ConwayMorris1988]. Tardigrades are coded as ambiguous in view of the complex integration of their dorsal plates.^n^nCharacter 49 in @Smith2015 and 47 in @Yang2015.^n'; + TEXT CHARACTER=239 TEXT='Enlarged sclerites may occur on every annulation (as in Cricocosmia jinningensis) or less frequently.^n^nThe plates of the lorica occur on every annulation of the lorica zone [@Neves2016].'; + TEXT CHARACTER=240 TEXT='In most lobopodian taxa, the epidermal specializations exhibit a regular spacing, even if the spacing of appendages varies along the body [@Smith2015]. Both Collinsium and Luolishania, by contrast, exhibit an extended spacing between spines in the medial portion of the trunk [@Ma2009; @Yang2015].^n^nCharacter 50 in @Yang2015.'; + TEXT CHARACTER=241 TEXT='Some heterotardigrades dorsally have plates between segmental plates. ^n^nCharacter 112 in @Khim2023. '; + TEXT CHARACTER=242 TEXT='In most armoured lobopodians, each group of dorsal spines or plates exhibits a similar size [e.g. @Smith2015]. In Collinsium, Hallucigenia hongmeia, Luolishania, Acinocricus and the Emu Bay Collins Monster, the size of spines varies between each group [@ConwayMorris1988; @Liu2007az; @Ma2009; @Steiner2012; @Garcia2013].^n^nCharacter 49 in @Yang2015.^n'; + TEXT CHARACTER=243 TEXT='Some echiniscoidean tardigrades have a a dorsal segmental plate at the last trunk segment, with an additional plate which does not match to the trunk segment. ^n^nCharacter 111 in @Khim2023.'; + TEXT CHARACTER=244 TEXT='Lobopodians’ epidermal evaginations fall into two geometric categories: flat nodes or plates (token 1) and tall spines (token 2). Although the distal portions of the evaginations of Orstenotubulus are not preserved (Maas et al. 2007b), we infer a spine-like habit from the proportions of the spine stubs.^n^nCharacter 43 in @Smith2015 and 41 in @Yang2015.'; + TEXT CHARACTER=245 TEXT='This character refers solely to the shape of the trunk evaginations’ apices. It is independent from the evaginations’ proportions, as demonstrated by Onychodictyon ferox, where sclerites are wider than tall (i.e. plates) but display an acute distal termination [@Zhang2007; @Ou2012; @Topper2013].^n^nCharacter 44 in @Smith2015 and 42 in @Yang2015.'; + TEXT CHARACTER=246 TEXT='The spines of Hallucigenia fortis (Hou and Bergström 1995), H. hongmeia (Steiner et al. 2012), Luolishania (Ma et al. 2009) and the Emu Bay ‘Collins Monster’ (García-Bellido et al. 2013b) are distinctively curved, whereas those of H. sparsa (Conway Morris 1977b) and Onychodictyon ferox (Topper et al. 2013) are essentially straight.'; + TEXT CHARACTER=247 TEXT='See character 11 in (Sørensen et al. 2015).^nSirilorica and Nanaloricus bear spikes on the anterior margins of their loricae (Peel et al. 2013; Neves et al. 2016); Pliciloricus and Eolorica do not (Neves et al. 2016; Harvey and Butterfield 2017)'; + TEXT CHARACTER=248 TEXT='The epidermal evaginations of Cricocosmia and "armoured" lobopodians are substantially sclerotized [@Hou1995zjls; @Han2007app; @Steiner2012; @Caron2013], in contrast to those of Xenusion [@Dzik1989], Hadranax [@Budd1998p], Diania [@Ma2014jsp] and Kerygmachela [@Budd1993, @Budd1998trse].^n^nCharacter 46 in @Smith2015 and 44 in @Yang2015.^n^n---^n^nThe epidermal evaginations of ‘armoured’ lobopodians are substantially sclerotized (Hou and Bergström 1995; Steiner et al. 2012; Caron et al. 2013), in contrast to those of Xenusion (Dzik and Krumbiegel 1989), Hadranax (Budd and Peel 1998) and Kerygmachela (Budd 1993, 1998a).^nThe robust preservation and narrow spinose projections of the evaginations of Shergoldana (Maas et al. 2007a) suggest primary sclerotization.'; + TEXT CHARACTER=249 TEXT='The epidermal specializations of athrotardigrades such as Wingstrandarctus and Raiarctus exhibit have a cuticular expansion.^nWe code this as neomorphic.^n^nAdapted from character 109 in @Khim2023. '; + TEXT CHARACTER=250 TEXT='We code this character as uncertain in taxa that are not well enough preserved for the ornament to be apparent. Hallucigenia sparsa has a scaly ornament [@Caron2013] whereas H. hongmeia and Collinsium bear a net-like pattern [@Steiner2012; @Yang2015] shared with Onychodictyon, Microdictyon and Cricocosmia [@Han2007app; @Topper2013]; Cardiodictyon specimens show a comparable ornament [@Liu2014ppp fig. 4f]. The ornament of Cricocosmia and Tabelliscolex has been compared to Microdictyon, but this is in fact quite distinct, with a much more regular pattern of equally sized performations [@Shi2022]^n^nCharacter 47 in @Smith2015, 45 in @Yang2015, 5 in @Sorensen2023.'; + TEXT CHARACTER=251 TEXT='Microdictyon and Onychodictyon plates exhibit mushroom-like bosses at the junction of the net-like pattern.'; + TEXT CHARACTER=252 TEXT='Arthropodization is thought to happen first in pre-ocular appendages, then co-opted to the rest of the appendages [@Chipman2019]. This character reflects this hypothesised event. Sclerotization is thought to occur simultaneously in all trunk appendages (as they have been co-opted from the pre-ocular appendages), therefore we code this in one character. This is treated as a neomorphic character as the trunk appendages" co-option of sclerotization from pre-ocular appendages requires additional genetic control.^n'; + TEXT CHARACTER=253 TEXT='(~) inapplicable: lateral flaps (trans. ser. 55) not present^nTransformation series 38 in Daley et al. (2009).'; + TEXT CHARACTER=254 TEXT='Treated as neomorphic.^n^nTo summarise @Daley2009 and @VanRoy2015:^nExopods, the outer branch of a true biramous limb, are unique to Mandibulata. The outer appendage branch of chelicerates and many stem-group euarthropods is interpreted as an exite, a lateral flap which is not homologous to the mandibulate exopod [see also @Bruce2020]. ^n@Daley2009 treated this flap as homologous with the lateral flaps of anomalocaridids and gilled lobopodians, which often bear dorsal lanceolate blades (= setal blades). ^n@VanRoy2015 considered the setal blades themselves to represent the exite, homologizing the wrinkling on Kerygmachela and Pambdelurion flaps, the setal blades of Opabinia and anomalocaridids, and the exites of upper stem euarthropods. Dorsal flaps are therefore not necessarily present in addition to the setal blades; indeed @VanRoy2015 code them as absent in Amplectobeluids and Anomalocaris (as well as euarthropods).^n^nAdapted from character 31 in @Daley2009. Character 55 in @Smith2015 and 53 in @Yang2015.^n'; + TEXT CHARACTER=255 TEXT='The description of Aegirocassis @VanRoy2015 clarifies the relationship of the dorsal lanceolate (setal) blades in gilled lobopodians and radiodontans, and establishes their homology with setae borne on the outer appendage branches (i.e. exites) of upper-stem Euarthropoda.^n^nAdapted from characters 51, 56 and 68 from @Smith2015 and 54 in @Yang2015.'; + TEXT CHARACTER=256 TEXT='The dorsal flaps of anomalocaridids and gilled lobopodians are considered as homologous with euarthropod exites.^nSee character 57 in @VanRoy2015. Treated as neomorphic.^n^nCharacter 57 in @Smith2015 and @Yang2015.'; + TEXT CHARACTER=257 TEXT='@VanRoy2015 consider the setal blades to represent exites. Dorsal flaps are not always present in addition to the setal blades: the (ventral) flaps of amplectobeluids and Anomalocaris correspond to the euarthropod endopod. Dorsal flaps are considered to represent an elaboration of the setal blades, and thus treated as a neomorphic character.^n^nModified from character 21 in @VanRoy2015. Character 67 in @Smith2015 and 55 in @Yang2015.'; + TEXT CHARACTER=258 TEXT='Gnathobasic appendages are absent in fuxianhuiids [@Chen1995s; @Waloszek2005; @Bergstrom2008; @Yang2013] but present in Artiopoda [@Edgecombe1999; @Ortega2013] and megacheirans [@Chen2004; @Haug2012bmceb; @Haug2012p].^n^nCharacter 8 of @Ma2014jsp; 35 in @Daley2009; 58 in @Smith2015 and @Yang2015.'; + TEXT CHARACTER=259 TEXT='Character 51 of @VanRoy2015, reflecting the continuation of setal blades in certain dinocaridids across the dorsal surface.^n^nCharacter 56 in @Yang2015.'; + TEXT CHARACTER=260 TEXT='Some lobopodians have cylindrical appendages (e.g. Microdictyon, Hallucigenia) whereas others have more conical or tapered lobopods.^nInapplicable when lobopodous limbs are absent.^n'; + TEXT CHARACTER=261 TEXT='Only structures that are distinct from trunk sclerites are considered here.^n^nModified from character 9 in @Ma2014jsp. 59 in @Smith2015 and Yang2015.^n'; + TEXT CHARACTER=262 TEXT='Spines and setae taper to sharp point, whereas appendicules have a uniform length and a flattened terminus.^n^nCharacter 60 in @Smith2015 and @Yang2015.'; + TEXT CHARACTER=263 TEXT='In Luolishaniids the secondary structures are arranged in rows, whereas in Ayesheaia, there is only one or two on trunk limbs.'; + TEXT CHARACTER=264 TEXT='Luolishaniids have long setiform spines [@Caron2020; @ConwayMorris1988; @Ma2009; @Garcia2013; @Yang2015], which contrast with the short, more equant spines of Diania and Aysheaia [@Whittington1978; @Ma2014jsp].^n^nCharacter 61 in @Yang2015.'; + TEXT CHARACTER=265 TEXT='In contrast to appendicules and spines, papillae are short projections associated with the annulations. The preservation of papillae in Ilyodes indicates that the absence of papillae in Carbotubulus is not taphonomic [@Haug2012cb]. Ambiguous in euarthropods as sclerotization is considered to overprint and obscure any papillae that may have been present.^n^nCharacter 10 in @Ma2014jsp; 61 in @Smith2015 and 62 in @Yang2015.'; + TEXT CHARACTER=266 TEXT='The finger-like projections in the legs of tardigrades can bear sets of terminal claws or sucking discs [@Schuster1980; @Nelson2002].^n^nCharacter 62 in @Smith2015 and 63 in @Yang2015.'; + TEXT CHARACTER=267 TEXT='A cuticularized spine is borne by the papillae of the partial Orsten-type lobopodian and crown-group onychophorans.^n^nCharacter 77 in @Zhang2016.^n^nA cuticularized spine is borne by the papillae of the partial Orsten-type lobopodian and crown-group onychophorans.^n^nCharacter 77 in @Zhang2016.'; + TEXT CHARACTER=268 TEXT='Arthrotardigrade Batillipes has discs on the tip of its limbs. Coded as neomorphic. ^n^nModified from character 50 from @Khim2023.'; + TEXT CHARACTER=269 TEXT='From character 63 in @Smith2015 and 64 in @Yang2015. This character is contingent on the presence of specialized trunk sclerites.'; + TEXT CHARACTER=270 TEXT='The outer edge of e.g. onychophoran claws have a similar curvature along its length, whereas the inner edge has a distinct inflection/step in curvature along its length, forming an enlarged attachment base.'; + TEXT CHARACTER=271 TEXT='Whilst many lobopodians have terminal claws, Aysheaia"s claws are sub-terminal; lobopods extend beyond the claws [@Whittington1978].^nInapplicable when terminal or sub-terminal claws absent.'; + TEXT CHARACTER=272 TEXT='Present in Eutardigrada [@Schuster1980; @Nelson2002; @Halberg2009] and the Siberian Orsten-type tardigrade [@Maas2001]. Absent in heterotardigrades and Palaeozoic lobopodians, which express simple concavo-convex claws.^n^nCharacter 64 in @Smith2015 and 65 in @Yang2015. Similar to character 52 in @Khim2023.^n^n'; + TEXT CHARACTER=273 TEXT='In apochelans, the primary and secondary branches are seperate, whereas in parachelans they are fused. ^n^nCharacter 53 from @Khim2023.'; + TEXT CHARACTER=274 TEXT='Where 1 represents the primary branch, and 2 is the secondary branch. If claw is 2121, the sequence of claws on a limb is external claw secondary (2), external claw primary (1), internal claw secondary (2), internal claw primary (1). ^n^nCharacter 54 in @Khim2023.'; + TEXT CHARACTER=275 TEXT='Character 55 of @Kihm2023'; + TEXT CHARACTER=276 TEXT='Character 56 of @Kihm2023'; + TEXT CHARACTER=277 TEXT='@Mapalo2024cb, character 4'; + TEXT CHARACTER=278 TEXT='@Mapalo2024cb, character 5'; + TEXT CHARACTER=279 TEXT='@Mapalo2024cb, character 8'; + TEXT CHARACTER=280 TEXT='@Mapalo2024cb, character 10'; + TEXT CHARACTER=281 TEXT='@Mapalo2024cb, characters 11 and 12 merged'; + TEXT CHARACTER=282 TEXT='@Mapalo2024cb, character 13'; + TEXT CHARACTER=283 TEXT='@Mapalo2024cb, character 14'; + TEXT CHARACTER=284 TEXT='@Mapalo2024cb, character 15'; + TEXT CHARACTER=285 TEXT='@Mapalo2024cb, character 16'; + TEXT CHARACTER=286 TEXT='@Mapalo2024cb, character 19'; + TEXT CHARACTER=287 TEXT='@Mapalo2024cb, character 21'; + TEXT CHARACTER=288 TEXT='@Mapalo2024cb, characters 22 and 23 merged'; + TEXT CHARACTER=289 TEXT='@Mapalo2024cb, character 24'; + TEXT CHARACTER=290 TEXT='@Mapalo2024cb, character 25'; + TEXT CHARACTER=291 TEXT='The differentiated anterior appendages of hallucishaniids do not bear unambiguous claws: structures interpreted as such (e.g. in Ovatiovermis, Luolishania) are not morphologically or compositionally distinct from co-occurring setae/spinules. As such, only the walking trunk limbs are considered.^n^nCharacter 65 in @Smith2015 and 66 in @Yang2015.'; + TEXT CHARACTER=292 TEXT='In many lobopodians, posterior trunk appendages bear fewer claws than anterior appendages.'; + TEXT CHARACTER=293 TEXT='All seven claws in Aysheaia are identical [@Whittington1978]. Euperipatoides claws are identical on trunk limbs, although the jaw elements are differentiated [@Smith2014]. Paucipodia claws are not visibly differentiated [@Hou2004]; neither are those of Hallucigenia sparsa [@Smith2015]. Onychodictyon ferox has a large and a small claw [@Steiner2012].^n^nCharacter 66 in @Smith2015; 6 and 17 in @Mapalo2024cb.'; + TEXT CHARACTER=294 TEXT='A movable foot is present in the Onychophoran crown group, but not in Tertiapatus [@Poinar2000].^n^nCharacter 67 in @Yang2015.^n'; + TEXT CHARACTER=295 TEXT='Certain heterotardigrades have partitioned, retractable limbs. We code this character as neomorphic. ^n^nCharacter 42 from @Kihm2023'; + TEXT CHARACTER=296 TEXT='Transformation series 31 in Ma et al. (Ma et al. 2014a); trans. ser. 36 in Daley et al. (2009). The definition has been slightly modified reflect the presence of two pairs of lateral flaps in Anomalocaridida (Van Roy et al. 2013).'; + TEXT CHARACTER=297 TEXT='Character 37 in @Daley2009; 69 in @Smith2015 and @Yang2015.^n'; + TEXT CHARACTER=298 TEXT='Character 40 in @Daley2009: "Posterior tapering of the width of the lateral lobes is pronounced in Anomalocaris and Laggania, while other lateral lobe-bearing taxa, including Hurdia, have a more even body outline."^n^nCharacter 70 in @Smith2015 and @Yang2015.^n'; + TEXT CHARACTER=299 TEXT='The first pair of body flaps (posterior of segments lacking flaps) are enlarged into "paddles" in Schinderhannes and Lyrarapax [@Kuhl2009; @Cong2014; @Cong2016]. Because the body flaps of these radiodontans are homologous with endopods and lobopods [@VanRoy2015], this character has been generalized from @Yang2016 in order to apply to all appendage-bearing taxa.^n^nCharacter 68 in @Yang2015.^n'; + TEXT CHARACTER=300 TEXT='In Lyrarapax, Hurdia, Peytoia and Anomalocaris, the flaps of the anterior region are reduced [@Daley2009; @Cong2014; @Daley2014], whereas in Opabinia, Kerygmachela and Pambdelurion, the equivalent flaps remain expressed [@Whittington1975; @Budd1998ar; @Budd1998trse]. The preservation of Aegirocassis and Schinderhannes in inadequate to resolve this feature.^nBecause the ventral body flaps of the radiodonts are homologous with endopods and lobopods [@VanRoy2015], this character has been generalized from @Yang2015 and @Yang2016 in order to apply to all limb-bearing taxa.^n^nCharacter 71 in @Yang2015.'; + TEXT CHARACTER=301 TEXT='Transformation series 38 in Ma et al. (Ma et al. 2014a).'; + TEXT CHARACTER=302 TEXT='Hallucigenia fortis has two pairs of elongate limbs [@Ma2012asd]; Hallucigenia sparsa has three [@Smith2015]; Luolishania, Facivermis, Acinocricus and the Emu Bay Collins Monster have five [@Ramskold1998; @Ma2009; @Garcia2013; @Howard2020]; Collinsium, Ovatiovermis and Collinsovermis bear six [@Caron2020; @Yang2015; @Caron2017].^n^nCharacter 73 in @Yang2015.'; + TEXT CHARACTER=303 TEXT='The anterior limbs of Hallucigenia sparsa are simple and lack cirri; the anterior limbs of luolishaniids bear multiple cirri. The trunk is not differentiated into distinct anterior and posterior components in any other taxon.^n^nCharacter 71 in @Yang2015.^n'; + TEXT CHARACTER=304 TEXT='The endopods of certain taxa in the euarthropod stem-group, such as fuxianhuiids, bear 15 or more podomeres and are considered "multipodomerous" [@Chen1995s; @Waloszek2005; @Bergstrom2008; @Yang2013].^n^nCharacter 72 in @Smith2015 and 74 in @Yang2015.^n'; + TEXT CHARACTER=305 TEXT='Some echiniscoideans have a small sclerotized plate on the last pair of limbs.^nWe code this character as neomorphic. ^n^nCharacter 118 from @Khim2023.'; + TEXT CHARACTER=306 TEXT='This character has been modified by that of previous analyses [e.g. character 34 in @Ma2014jsp] to reflect the fact that, in extant onychophorans, the posterior extension of the lobopodous trunk (i.e. anal cone) corresponds to a segment that has lost its appendage pair, as evinced by the prevalence of nephridia in this region [@Mayer2005]. As it is not possible to determine whether the posterior extension of the trunk in Palaeozoic lobopodians arises through the loss of the last appendage pair (as in Onychophora) or as an elongation of the trunk, we code this character as present in all taxa where the trunk extends posteriad of the last observable pair of limbs. Coded ambiguous where trunk appendages are absent.^n^nCharacter 73 in @Smith2015 and 75 in @Yang2015.'; + TEXT CHARACTER=307 TEXT='i.e. terminal limbs of lobopodians; lumps of palaeoscolecids. Distinguish from caudal appendages.^n^n^nThis transformation series has been modified by that of previous analyses (Ma et al. 2014a) to reflect the fact that, in extant Onychophorans, the posterior extension of the lobopodous trunk (i.e. anal cone) corresponds to a segment that has lost its appendage pair, as evinced by the prevalence of nephridia in this region (Mayer and Koch 2005). As it is not possible to determine whether the posterior extension of the trunk in Palaeozoic lobopodians arises through the loss of the last appendage pair (as in Onychophora) or as an elongation of the trunk, we code this transformation series as present in all taxa where the trunk extends posteriad of the last observable pair of limbs. We code this transformation series as absent in Kerygmachela (Budd 1993, 1998a), Jianshanopodia (Liu et al. 2006) and Anomalocaris (Daley and Edgecombe 2014) as their tails likely represent modified appendages (see transformation series 63 and 64). There is possible, but inconclusive, evidence for a small posterior extension in Opabinia (Whittington 1975; Budd 1996; Budd and Daley 2012), which is thus coded as uncertain. Siberion is scored as uncertain as it is difficult to distinguish the possible body termination from a posterior leg or pair of legs (Dzik 2011). Hallucigenia sparsa is also coded as uncertain; the posterior part of its body is poorly known (Ramsköld 1992). It is present in other species of Hallucigenia (e.g. Hou and Bergström 1995).'; + TEXT CHARACTER=308 TEXT='Character 42 in @Daley2009, 74 in @Smith2015 and 76 in @Yang2015.'; + TEXT CHARACTER=309 TEXT='The last pair of legs are rotated anteriad in tardigrades [e.g. @Marchioro2013], Aysheaia [@Whittington1978] and O. ferox [@Ou2012], but not in O. gracilis, Cardiodictyon, Hallucigenia fortis or Microdictyon [@Hou1995zjls].^n^nCharacter 78 in @Smith2015 and 80 in @Yang2015.'; + TEXT CHARACTER=310 TEXT='Character 75 in @Smith2015 and 77 in @Yang2015. See also character 35 in @Ma2014jsp.'; + TEXT CHARACTER=311 TEXT='In fuxianhuiids, the posteriormost appendage pair is modified into a tail fan or tail flukes [e.g. @Chen1995s; @Yang2013]; a similar condition is also observed in Opabinia [@Whittington1975; @Budd1996; @Budd2012], Anomalocaris [@Daley2014] and Hurdia [@Daley2009]. Partial fusion of the last pair of legs occurs in Aysheaia [@Whittington1978], Onychodictyon gracilis [@Liu2008app], O. ferox [@Ou2012] and Tardigrada [e.g. @Halberg2009; @Marchioro2013]; in these taxa, this characteristic is expressed as an incipient fusion of the medioproximal bases of the posteriormost appendage pair. ^n^nCharacter 76 in @Smith2015 and 78 in @Yang2015.'; + TEXT CHARACTER=312 TEXT='As noted by @Pates2022, many euathropods have caudal rami. The rami of Kergmachela may represent fused rami. ^n^nThis character distinguishes the long tail rami of Kerygmachela [@Budd1993; @Budd1998trse] from the flaps observed in Jianshanopodia [@Liu2006], anomalocaridids [@Daley2009; @Daley2014], and fuxianhuiids [e.g. @Yang2013].^n^nCharacter 77 in @Smith2015 and 79 in @Yang2015.'; + TEXT CHARACTER=313 TEXT='Opabiniids [@Budd2012; @Pates2022] and Anomalocaridids [@Daley2009] have differentiated posterior appendages that form a tail fan. Fuxianhuiids have similar modified appendicular tail flukes [e.g. @Yang2013]. Opabinia regalis has a paddle-like, more symmetric morphology to its tail appendages, whereas Anomalocaris and Utaurora appendages are more asymmetric, with a sharp anterior edge forming a blade-like morphology [@Pates2022].^nCoded as inapplicable when posterior tagma tail flaps are absent.^n^nCharacter 106 from @Pates2022.'; + TEXT CHARACTER=314 TEXT='Observed in Laojieella, Eximipriapulus, Xiaoheiqingella^nIncludes the lorical region of Palaeopriapulites and Sicyophorus. (Hou et al. 2017)'; + TEXT CHARACTER=315 TEXT='Cf. WTS45.^nBy comparison with Corynetis (Huang et al. 2004a; Hu et al. 2012), the posterior trunk region of Louisella (Conway Morris 1977a) is coded as a caudal appendage.^nThe posterior lobes of nematomorphs are not considered to represent separate appendages or organs, so caudal appendages are coded as absent in this taxon.^nJust a hint of some form of caudal appendage in Fieldia (ROM 93-1509)^nCoded ambiguous in Selkirkia as the posterior trunk is not known; the posterior of the tube was apparently open.'; + TEXT CHARACTER=316 TEXT='WTS41.^nGiven the difficulty of distinguishing the ‘bursa’ in fossil worms such as Ottoia and Louisella (Conway Morris 1977a) from a caudal appendage, or indeed of clearly defining a distinction, a ‘bursa’ is coded as a caudal appendage; this transformation series refers to the eversibility of this appendage.^nThe identity of the posterior extension in Chalazascolex as a bursa is speculative (Conway Morris and Peel 2010); this taxon is coded ambiguous.'; + TEXT CHARACTER=317 TEXT='Cf. WTS45. The caudal appendage of Tubiluchus lemburgi is distinctly longer than the body'; + TEXT CHARACTER=318 TEXT='WTS46.'; + TEXT CHARACTER=319 TEXT='Cf. WTS47.'; + TEXT CHARACTER=320 TEXT='Cf WTS47.'; + TEXT CHARACTER=321 TEXT='WTS48.'; + TEXT CHARACTER=322 TEXT='See characters 176-129 in @Meldal2004. Caudal glands open through a spinneret to secrete an adhesive that free-living nematodes use to attach to a substrate.'; + TEXT CHARACTER=323 TEXT='Cf. WTS39.^n^nThis character considers posterior spines, setae, and tubulae, but not posterior bifurcations of the trunk (as in adult nematomorphs), which are treated separately.^n^nThe ''toes'' of loriciferans are spines, used for locomotion and adhesion; they are reduced in adults [@Neves2016]. ^nLoriciferans bear posterior spines, interpreted as sensory setae (Neves and Kristensen 2014)^n^nPosterior projections in kinorhynchs [@Sorensen2008] that correspond to projections on other segments are not treated as posterior projections for the purposes of this character. Likewise, tergal extensions are extensions of the tergal plate [@Herranz2014], rather than distinct projections.'; + TEXT CHARACTER=324 TEXT='Cf. WTS44.'; + TEXT CHARACTER=325 TEXT='Cf. WTS49.^nCoded as ambiguous in Scathascolex and Eokinorhynchus as the tail hooks’ basal diameter is close to 20% of the trunk diameter; the preservation of the fossils makes it difficult to determine the exact diameter of the hooks.'; + TEXT CHARACTER=326 TEXT='Scathascolex and Eokinorhynchus have four hooks.^nWronascolex antiquus is scored as having four hooks; the hooks are occluded by adpression in the specimens figured in (García-Bellido et al. 2013a) but seem to occur in two pairs.^nMaccabeus has 40–65 hooks (Por and Bromley 1974)^nMeiopriapulus has 32–38 in a single ring (Sørensen et al. 2012a)^nSix in Markuelia (Dong et al. 2010)'; + TEXT CHARACTER=327 TEXT='Cf WTS39.^nSchistoscolex has four projections, two bilateral pairs; they encircle the entire posterior surface of the organism and are thus coded as being a radial ring.^nThe pairs in Eokinorhynchus form an open arc (Zhang et al. 2015)^nThe condition in Markuelia is unclear (Dong et al. 2010)^nThe condition in Acanthopriapulus is taken to be irregular (van der Land 1970)'; + TEXT CHARACTER=328 TEXT='WTS40. Ring papillae are small peg-like structures tipped with a seta; they occur on the annulus/annuli closes to the anus. They grade into abdominal setae, and are easily missed except with SEM analysis of living priapulids (Merriman 1981) and are thus coded ambiguous in fossil taxa except the exquisitely preserved Schistoscolex, where they are demonstrably absent. Corynetis and Xiaoheiqingella, coded as present in Wills et al. 2012, do not obviously express a ring of papillae that are distinct from abdominal spines.'; + TEXT CHARACTER=329 TEXT='WTS42.'; + TEXT CHARACTER=330 TEXT='Cf. WTS50.'; + TEXT CHARACTER=331 TEXT='Cf. WTS50.'; + TEXT CHARACTER=332 TEXT='@Budd2001za proposes the distribution of musculature as a key phylogenetic character. The musculature of tardigrades, Pambdelurion, Anomalocaris and more derived euarthropods is metamerically arranged and runs through the body cavity, whereas muscles in cycloneuralians, onychophorans and Kerygmachela are seemingly dominated by longitudinal and circular structures [@Carnevali1979; @Hoyle1980; @Budd1998l, @Budd2001za].^n^nCharacter 52 in @Smith2015.'; + TEXT CHARACTER=333 TEXT='Longitudinal muscles may exist in the peripheral region [see @Zhang2016] in addition to circular and/or metameric musculature. Present in priapulans and onychophorans [@Carnevali1979; @Hoyle1980]; absent in tardigrades and euarthropods [@Halberg2009], and presumed absent in Fuxianhuia.^n^nCharacter 113 in @Zhang2016.^n^nIn nematodes, longitudinal somatic musculature lies directly underneath the epidermis [@SchmidtRhaesa2014]'; + TEXT CHARACTER=334 TEXT='Observed in Pambdelurion, tardigrades and onychophorans [@Young2017].'; + TEXT CHARACTER=335 TEXT='In tardigrades, longitudinal muscles attach at successive points along the body; on onychophorans and gilled lobopodians, they attach only at the anterior and posterior end of the trunk [@Young2017]. Inapplicable if longitudinal muscles are absent.^n^nIn nematodes, the muscles attach at their edges to lateral, dorsal and ventral chords that protrude inwards from the epidermis [@SchmidtRhaesa2014]'; + TEXT CHARACTER=336 TEXT='In most kinorhynchs, longitudinal muscles attach to the pachycycli situated at the anterior segment margins; in ''aberrant'' kinorhynchs, they attach more posteriorly to the anteriormost part, or the central part, of each tegumental plate. [Paraphrased from @Herranz2021z]'; + TEXT CHARACTER=337 TEXT='WTS87.^n^nPresent in priapulans and onychophorans [@Carnevali1979; @Hoyle1980]; absent in tardigrades and euarthropods [@Halberg2009], and presumed absent in Fuxianhuia. ^nNanaloricus bears circular muscles around the neck (Neves et al. 2013)^nCircular muscles are reduced in adult Nematoids (Sørensen et al. 2008)^n^nCharacter 114 in @Zhang2016.'; + TEXT CHARACTER=338 TEXT='Longitudinal muscles occur inside circular muscles in priapulans and onychophorans [@Carnevali1979; @Hoyle1980].^n^nCharacter 115 in @Zhang2016.'; + TEXT CHARACTER=339 TEXT='Dorsoventral muscles are absent in segment 1 of certain kinorhynchs [@Herranz2021z]. Treated as a neomorphic character denoting the specialization of segment 1, hence coded as absent in taxa without segmented dorsoventral musculature.'; + TEXT CHARACTER=340 TEXT='Metamerically arranged dorsoventral and oblique muscles connecting the lateral and ventral muscle groups are present in tardigrades and euarthropods, resulting in a "box-truss trunk musculature system" [@Young2017]'; + TEXT CHARACTER=341 TEXT='Character(s) 84 in @Smith2015, 86 and 81 in @Yang2015.^n^nMa et al. (Ma et al. 2014a) described a dorsal heart in Fuxianhuia; all other fossil taxa are scored as ambiguous. Budd (2001b) discussed the difficulty of interpreting the absence of a circulatory system in Tardigrada as ancestral or derived, given that a circulatory system is unnecessary in a miniaturized organism; he concluded that the most methodologically sound way to address this issue in a cladistic context is to score the character as inapplicable.'; + TEXT CHARACTER=342 TEXT='Pharynx protractor muscles connect the base of the mouth cone to the posterior end of the pharynx in priapulans and kinorhynchs [@Neuhaus2002icb; @Altenburger2016ed]'; + TEXT CHARACTER=343 TEXT='The nervous system of priapulids is intraepithelial; neurites are basiepithelial in nematomorphs'; + TEXT CHARACTER=344 TEXT='WTS52.^n^n“Living priapulids possess unpaired ventral nerve cords, whereas gastrotrichs, onychophorans and loriciferans possess ventral nerve cords that are paired throughout their length, and the ventral nerve cords of nematomorphs and nematodes divide at points along their length [@SchmidtRhaesa1997]; the situation in kinorhynchs is unresolved (paired according to Kristensen and Higgins, 1991; unpaired according to Neuhaus 1994). The condition in Ottoia is common to extant priapulids (Conway Morris, 1977).”^nSee also (Martín-Durán et al. 2016)^nIn kinorhynchs there are seven to twelve nerve cords; the ventral nerve cord is unpaired in Echinoderes (Neuhaus and Higgins 2002)^n^nThis neomorphic character codes the transformation from a single ventral nerve cord (e.g., priapulans) to a pair (e.g., extant panarthropods, Chengjiangocaris). ^n^nTreated as paired in nematomorphs, a paired configuration can be observed in e.g. Paragordius [@SchmidtRhaesa2014], and its vestiges can be observed throughout the phylum [@SchmidtRhaesa1997]^n^nCharacter 85 in @Yang2016.'; + TEXT CHARACTER=345 TEXT='In many nematodes, the central cord exhibits a primary right branch and a subsidiary left branch, which may merge back into the primary cord terminally [@SchmidtRhaesa2014]. In other taxa, paired cords are equivalent in size [@SchmidtRhaesa2012].^n'; + TEXT CHARACTER=346 TEXT='WTS53.^nIn many nematodes, the central cord exhibits a primary right branch and a subsidiary left branch, which may merge back into the primary cord terminally [@SchmidtRhaesa2014]. The paired cords of certain nematomorphs also merge caudally [@SchmidtRhaesa2012]. Coded present also in cases where the nematomorph nerve cord is fully merged.^n'; + TEXT CHARACTER=347 TEXT='Character 2 in @Tanaka2013, 79 in @Smith2015 and 81 in @Yang2015.^n^nTardigrada and Euarthropoda have a ganglionated ventral nerve cord [@Schulze2014], in contrast to the ladder-like ventral nerve cord in Onychophora [@Mayer2013bmceb]. Priapulida have an unpaired nerve cord associated with a net-like system of neural connectives [@Storch1991; @Rothe2010].^n'; + TEXT CHARACTER=348 TEXT='Character 1 in @Tanaka2013; revised by @Yang2016 to apply only to paired nerve cords. This character distinguishes the organization of the ventral nerve cord in Onychophora [e.g. @Mayer2013bmceb] from that in other phyla.^n^nCharacter 83 in @Smith2015, 85 in @Yang2015 and 87 in @Yang2016.^n^n---^nTransformation series 1 in Tanaka et al. (2013). This transformation series distinguishes the organization of the ventral nerve cord in Onychophora (e.g. Mayer et al. 2013a) from that in other phyla.'; + TEXT CHARACTER=349 TEXT='Present in Onychophora and Tardigrada; absent in Euarthropoda, including Alalcomenaeus. Ambiguous in Lyrarapax and Chengjiangocaris. See @Yang2016 for further discussion.^n^nCharacter 88 in @Yang2016.'; + TEXT CHARACTER=350 TEXT='Neural concentrations (ganglia) along the ventral nerve cord give a "rope ladder-like" appearance in tardigrades and euarthropods, in contrast to a ladder-like VNC, found in onychophorans [@Yang2016]. The presence of transverse commissures likely are fundamentally linked neurological features [@Yang2016].^n^nCharacter 86 in @Yang2016.^n'; + TEXT CHARACTER=351 TEXT='Present in Priapulida, Onychophora, Tardigrada and Chengjiangocaris; absent in Euarthropoda and Alalcomenaeus. See @Yang2016 for further discussion.^n^nCharacter 93 in @Yang2016.^n'; + TEXT CHARACTER=352 TEXT='Orthogonal organization of several ring-like commissures and peripheral nerves that intersect longitudinal dorsal and lateral nerve strands to form a reticulate pattern. Present in Priapulida, Onychophora and Tardigrada. Uncertain in Chengjiangocaris; absent in Alalcomenaeus and crown Euarthropoda. See @Yang2016 for further discussion. Contra @Yang2016, we score this character as inapplicable in taxa where the regularly spaced peripheral nerves that constitute the transverse component of the orthogonal organization are not present.^n^nCharacter 89 in @Yang2016.'; + TEXT CHARACTER=353 TEXT='Complete in Priapulida and Onychophora; incomplete in Tardigrada. Inapplicable in Euarthropoda. See @Yang2016 for further discussion.^n^nCharacter 90 in @Yang2016.'; + TEXT CHARACTER=354 TEXT='Anteriorly displaced in Tardigrada and Euarthropoda; not in Onychophora. Ambiguous in fossil taxa. See @Yang2016 for further discussion.^n^nCharacter 91 in @Yang2016.^n'; + TEXT CHARACTER=355 TEXT='Two nerves innervate each leg in Onychophora and Eutardigrada, but a single nerve innervates each Euarthropod leg. The configuration is ambiguous in fossil material. See @Yang2016 for further discussion.^n^nCharacter 92 in @Yang2016.^n'; + TEXT CHARACTER=356 TEXT='Present in Eutardigrada and Euarthropoda; uncertain in Heterotardigrada; absent in Onychophora and Priapulida. See @Yang2016 for further discussion.^n^nCharacter 64 in @Yang2016.'; + TEXT CHARACTER=357 TEXT='WTS55.^n^nProposed as a synapomorphy of Cycloneuralia (Nielsen 2012), though also present in Panarthropoda; Euperipatoides has been scored as present based on the homology of the supraoesophageal ganglion with the circumpharyngeal brain, as argued by @Eriksson2000. Present in Nematomorpha despite the absence of a pharynx [@Henne2017; @SchmidtRhaesa2012]^n^nCircumpharyngeal nerve rings are found in the nematode brain [@White1997; @Henne2017] and the anterior nervous systems of extant tardigrades [@Mayer2013bmceb; @Smith2017]. They are likely precursors of the dorsal condensed brain [@Smith2024]'; + TEXT CHARACTER=358 TEXT='A synapomorphy of Nematomorpha [@SchmidtRhaesa1996; @SchmidtRhaesa1997]'; + TEXT CHARACTER=359 TEXT='Whereas typical cycloneuralians have a circumoesophageal nerve ring [e.g. @Storch1991; @Telford2008; @Edgecombe2009; @Rothe2010], Panarthropoda is characterized by dorsal condensed brain neuromeres [@Eriksson2003; @Mittmann2003; @Harzsch2005asd; @Mayer2010; @Mayer2013po]. A dorsal condensed brain has been described in Fuxianhuia [@Ma2012n] and Alalcomenaeus [@Tanaka2013].^n^nCharacter 80 in @Smith2015 and 82 in @Yang2015.^n'; + TEXT CHARACTER=360 TEXT='Number of neuromeres integrated into the dorsal condensed brain. See the introductory statements for char. 81 in @Smith2015 and char. 83 in @Yang2015.^n'; + TEXT CHARACTER=361 TEXT='Recent fossil data suggest a likely deutocerebral innervation for the mouth in Fuxianhuia and Alalcomenaeus based on the position of the oesophageal foramen relative to the brain [@Ma2012n; @Tanaka2013], which is congruent with the organization found in phylogenetically basal extant euarthropods such as Chelicerata and Myriapoda [@Mittmann2003; @Harzsch2005asd; @Scholtz2005; @Scholtz2006]. Tritocerebral innervation is observed in Pancrustacea, but not among the taxa included in this study.^n^nThe circumoral nerve ring is treated as homologous with the protocerebrum, per @Smith2024.^n^nCharacter 82 in @Smith2015 and 84 in @Yang2015.^n'; + TEXT CHARACTER=362 TEXT='Absent in panarthropods (Martin et al. 2017)'; + TEXT CHARACTER=363 TEXT='Cf. WTS54.^nUnpaired dorsal nerve cords are seen as a synapomorphy of Nematoida (Sørensen et al. 2008)'; + TEXT CHARACTER=364 TEXT='WTS56. The cycloneuralian brain comprises three distinct regions: an anterior aggregation of somata from neurons (perikarya), followed by a central neuropil, followed posteriorly by a further region of perikarya (Rothe and Schmidt-Rhaesa 2010). This has been proposed as a synapomorphy of the cycloneuralians (Lemburg 1999): as the brains of panarthropods are arranged differently (Martin et al. 2017). However, the perikarya has an equal distribution in nematomorpha (Schmidt-Rhaesa 1997a)'; + TEXT CHARACTER=365 TEXT='Modified from Wills et al. (2012) character statement 57, which is understood to refer to a modification of the cycloneuralian brain in Tubiluchus and Meiopriapulus; the revised character is thus scored inapplicable in taxa without a cycloneuralian brain arrangement. This avoids the difficulty in deciding which bit of the onychophoran brain, which contains abundant perikarya (Martin et al. 2017), is ‘apical’.'; + TEXT CHARACTER=366 TEXT='WTS58. Lemburg (1999) recognizes the presence of this character as a synapomorphy of (extant) Priapulida'; + TEXT CHARACTER=367 TEXT='Eutardigrades have a cloaca (combined opening of gonopore and anus). ^n^nCharacter 91 in @Khim2023.'; + TEXT CHARACTER=368 TEXT='WTS66.^nThe presence of a cloaca in both sexes is seen as a synapomorphy of Nematoida (Sørensen et al. 2008)'; + TEXT CHARACTER=369 TEXT='WTS68.'; + TEXT CHARACTER=370 TEXT='Some heterotardigrades the duct of the seminal receptacle (sperm storage pocket) extends to the external part of the body.^n^nAdapted from character 92 @Khim2023.'; + TEXT CHARACTER=371 TEXT='Cf. WTS72.^nPerigenital setae comprising a ventral shaft and distal spine occur close to the urogenital pores in the anterior trunk of certain priapulans [@Land1970]'; + TEXT CHARACTER=372 TEXT='‘Mushroom shaped’ structures present in the genital region of Tubiluchus (Priapulida). WTS37'; + TEXT CHARACTER=373 TEXT='The clavula of Tubiluchus lemburgi has a short stalk and moderately sized distal bulb (Schmidt-Rhaesa et al. 2013).^nT. corallicola, T. australiensis have a short-stalked clavula (Van Der Land 1982; Schmidt-Rhaesa et al. 2013)^nT. remanei has a long-stalked clavula (Van Der Land 1982)'; + TEXT CHARACTER=374 TEXT='The clavula of Tubiluchus lemburgi has a short stalk and moderately sized distal bulb (Schmidt-Rhaesa et al. 2013).^nThe clavulae of T. remanei and T. corallicola are club-shaped with a distal bulb (van der Land 1982)^n^nFrom Schmidt-Rhaesa et al. 2013:^nIn T. remanei (see van der Land, 1982),^nthere is a row of perigenital setae of varying shape and size and^nvery long stalked clavula on each side next to the cloacal opening.^nInT. corallicola(seevan der Land, 1970), the urogenital pore and^nthe anus are very small and almost invisible. Close to each pore is a^nclavula, posterior are two large setae and anterior is a row of small^nperigenital setae. This row leads to a broad ventral region, in which^nnormal setae, large perigenital setae and tubuli are present; most^nprominent is a group of large “normal” setae anterior to the row^nof small perigenital setae. InT. remanei(see van der Land, 1982),^nthere is a row of perigenital setae of varying shape and size and^nvery long stalked clavula on each side next to the cloacal opening.^nA comb-like series of cuticular ridges is described, but not figured.^nTubiluchus australensis (see van der Land, 1985) has a clavula with a^nlarge spherical distal end and a short stalk. Additionally, only a row^nof eight perigenital setae of varying shape and size is present on^neach side of the animal. Whereas the urogenital opening is almost^ninvisible in all previous species, it is quite large and funnel-shaped^ninT. philippinensis (see van der Land, 1985). With a length of about^n25µm the clavulae are very large, and their distal ends are clubshaped. Some setae are present close to each clavula, but the most^nconspicuous structures are a dense group of small perigenital setae^nanterior to each urogenital opening. InT. troglodytes(seeTodaro^nand Shirley, 2003), there are circular cuticular ridges, which in total^nhave the form of an “8”. Eight to 10 setae are present along the ridge^non each side, and a clavula and two setae are present in the anterior^nregion anterolateral of the ridges on each side. Anterior of these^nstructures is a dense group of up to 70 setae. The genital structures^nofT. arcticusandT. vanuatuensiscould not be included here (they^nare, e.g. not mentioned in the English summary of the species in^nAdrianov and Malakhov, 1996).'; + TEXT CHARACTER=375 TEXT='Bullulae are small hemispherical elevations present in the genital region of certain priapulids, including Tubiluchus lemburgi (Schmidt-Rhaesa et al. 2013).^nPresent in T. corallicola (Van Der Land 1982)'; + TEXT CHARACTER=376 TEXT='Onychodictyon ferox''s gut expands anteriad forming a cone shape [see @Vannier2017], whilst some other lobopodian guts do not expand significantly in the anterior region, with the anterior end of a gut with a similar diameter to the mid-gut. We code the taxa with an eversible pharynx as ambiguous as it is unclear how these should be coded. ^n^n^nThis replaces the invariant character 17 from @Zhang2016, "Pharynx differentiated from midgut" [SC: 11].^n'; + TEXT CHARACTER=377 TEXT='WTS51.^nThe polythyridium is a muscular component of the gut surrounding the entrance to the intestine, adorned with circlets of cuticular plates (valvulae) [@Rothe2010]. It is interpreted as an autapomorphy of Tubiluchidae [@Kirsteuer1970].'; + TEXT CHARACTER=378 TEXT='Cf. WTS67.^nThe reduction of protonephridia is seen as a possible nematoid synapomorphy (Sørensen et al. 2008)'; + TEXT CHARACTER=379 TEXT='Cf. WTS67.'; + TEXT CHARACTER=380 TEXT='A possible synapomorphy of Scalidophora (Sørensen et al. 2008)^nPresent in Pycnophyes (Neuhaus 1988)^nCheck Neuhaus 1994, Ultrastructure of alimentary canal and body cavity, ground pattern, and phylogenetic relationships of the Kinorhyncha , Microfauna marina for Zelinkaderes details.'; + TEXT CHARACTER=381 TEXT='Homologous to cuticularized tubes of Pycnophyes (and Kinorhyncha) (Neuhaus 1988). In species of Echinoderidae, the protonephridial openings form two fairly conspicuous sieve plates, and due to their distinct appearance in LM as well as SEM, they are often reported in systematic and taxonomic studies. However, in non-echinoderid species there are no sieve plates and the nephridial pores are much more inconspicuous.'; + TEXT CHARACTER=382 TEXT='The absence of such microvilli is a possible synapomorphy of Priapulida + Kinorhyncha (Neuhaus and Higgins 2002)'; + TEXT CHARACTER=383 TEXT='WTS38. These states are retained in a single transformation series as states 1 and 2 are mutually exclusive but are unlikely to be homologous.'; + TEXT CHARACTER=384 TEXT='WTS69.^nThe reduction of the flagellum is seen as a possible synapomorphy of Nematoids (Sørensen et al. 2008), though a flagelliform tail is found in Kinonchulus (Riemann 1972). A flagellum has been reported in Gordius, but this is probably a misinterpretation (Schmidt-Rhaesa 1997b) , so Chordodes is coded as lacking a flagellum.'; + TEXT CHARACTER=385 TEXT='WTS73.^nSee (Bereiter-Hahn et al. 1984). Nematodes and Nematomorphs have principally replaced chitin with collagen as the principle component of their cuticle, though vestiges of chitin remain (Nielsen 2012). Coded as present in Palaeoscolex as chambers in the cuticle are believed to correspond to collagen fibres (Kraft and Mergl 1989)'; + TEXT CHARACTER=386 TEXT='Cf. WTS74.'; + TEXT CHARACTER=387 TEXT='WTS76.'; + TEXT CHARACTER=388 TEXT='WTS88'; + TEXT CHARACTER=389 TEXT='Present in heterotardigrades. Some eutardigrades also show a pillar-like structure in their epicuticle. Character 4 from @Khim2023. '; + TEXT CHARACTER=390 TEXT='Special types of glial/epidermal cells with characteristic bundles of tonofilaments, interpreted as a scalidophoran synapomorphy (though absent in certain Echinoderes species) (Nebelsick 1993). Coded, following the references in Nebelsick, as present in Tubiluchus, Meiopriapulus, Pycnophyes and Loricifera, ambiguous in Echinoderes dujardinii, and absent in Nematoda.'; + TEXT CHARACTER=391 TEXT='WTS78.'; + TEXT CHARACTER=392 TEXT='Cf. WTS79.^n^nTo add^nIn the tardigrade Echiniscus viridis, the central cuticle comprises:^n- An outer portion of alternating dense antd transparent layuers, with a much denser band proximally^n- Within these, a region made up of hexagons (looking striated in transverse section), with a complex dense outer layer and a less dense inner one^n- Proximal to that, an electron transparent zone containing dense rods^n- Within that, and innermost, transversely oriented fibres^n- The structure of the ventral cuticle is […] virtually identical to that described by Wright and Hope (1968) for the cuticle of the marine nematode, Acanthonchus (duplicatus Wieser, 1959 and quite similar to that described by Inglis (1964) and Watson (1965) for cuticles of certain other nematodes, including Elichromadora sp. Moreover, the striated layer found in the cuticle of E. viridi.s appears to be a nearly universal characteristic of nematode cuticles (cf. I,ee 1966, Wisse and Daems 1968 – (Crowe et al. 1970)'; + TEXT CHARACTER=393 TEXT='Character 36 in @Mapalo2024cb'; + TEXT CHARACTER=394 TEXT='WTS60. Biphasic encompasses the multiple phases of priapulid larvae (e.g. Wennberg et al. 2009), also documented in Sirilorica (Peel et al. 2013)^nThe nematode larva is morphologically similar to the adult, but lacks reproductive functions.^nLarvae of Priapulopsis are poorly known but are understood to be similar to Priapulus (van der Land 1970), and are coded equivalently herein.'; + TEXT CHARACTER=395 TEXT='Cf. WTS62'; + TEXT CHARACTER=396 TEXT='Crenulated in the Higgins larva of loriciferans (Neves et al. 2016)^nNot in Halicryptus (Storch and Higgins 1991; Janssen et al. 2009)'; + TEXT CHARACTER=397 TEXT='WTS63. Lemburg (1999) recognised the presence of this character as a synapomorphy of (extant) Priapulida. However, it has since been demonstrated that the larvae of nematomorphs also possess six pharyngeal retractor muscles (Kristensen 2003; Müller et al. 2004). Long pharynx retractor muscles are also present in loriciferans (at least within the Nanaloricidae) (Neves et al. 2013).'; + TEXT CHARACTER=398 TEXT='WTS65.^nA proposed synapomorphy of scalidophora, but also present in nematomorphs ^nPresent in Priapulid caudatus, Tubiluchis corallicola; absent in Kinorhyncha, Loricifera ; probably absent in Nectonema larvae yet present in adults (Schmidt-Rhaesa 1997a)'; + TEXT CHARACTER=399 TEXT='Proposed as a synapomorphy between nematomorph larvae and loriciferan adults [@Kristensen1983], but not seemingly ^nNot reported in Halicryptus, Maccabeus or Priapulus (van der Land 1970; Por and Bromley 1974; Storch and Higgins 1991), but present in Tubiluchus (Higgins and Storch 1989)'; + TEXT CHARACTER=400 TEXT='(~) inapplicable: proboscis and abdomen undivided^nThe larvae of Orstenoloricus (Maas and Waloszek 2009) possess a pair of spines at the anterior of the trunk. These may correspond to the anteroventral setae of Tenuiloricus (Neves and Kristensen 2014). Similar spines are present in Nanaloricidae and Pliciloricidae (Neves et al. 2016)^nThe tubuli present in the Halicryptus hatching larva are not paired, and disappear in the Higgins larval stage; paired spines are coded as absent in this taxon (Storch and Higgins 1991; Janssen et al. 2009) similar structures present in Tubiluchus (Kirsteuer 1976)'; + TEXT CHARACTER=401 TEXT='(~) inapplicable^nPresent in the Higgins larva of many loriciferans (Neves et al. 2016), Shergoldana (Maas et al. 2007a), ^nCoded as absent in Orstenoloricus (Maas and Waloszek 2009) as most specimens unambiguously lack them; only a single specimen has putative structures that are not unequivocally spines or appendages.^nPosterior protuberances occur at the posterior of the Halicryptus lorica (van der Land 1970); these probably ought to be coded in a separate transformation series but are included here for now. Similar features (‘tubuli’) are present in Priapulus (Higgins et al. 1993)'; + TEXT CHARACTER=402 TEXT='A similarity between the Nectonema and Nanaloricus larvae (Kristensen 1983)^nSac-like guts with single ‘fold’ in larvae of e.g. Tubiluchus (Higgins and Storch 1989)'; + TEXT CHARACTER=403 TEXT='Large mesenchyme cells in the larva are a similarity between nematomorph and loriciferan larvae (Kristensen 1983)'; + TEXT CHARACTER=404 TEXT='The Higgins larva is a component of the loriciferan lifecycle with a distinctive morphology'; + TEXT CHARACTER=405 TEXT='@Sorensen2023, character 1'; + TEXT CHARACTER=406 TEXT='@Sorensen2023, character 2'; + TEXT CHARACTER=407 TEXT='After @Sorensen2023, character 3; nature of wrinkles set as additional character'; + TEXT CHARACTER=408 TEXT='After character 3 in @Sorensen2023'; + TEXT CHARACTER=409 TEXT='@Sorensen2023, character 4'; + TEXT CHARACTER=410 TEXT='@Sorensen2023, character 6'; + TEXT CHARACTER=411 TEXT='@Sorensen2023, character 7'; + TEXT CHARACTER=412 TEXT='@Sorensen2023, character 8'; + TEXT CHARACTER=413 TEXT='@Sorensen2023, character 9'; + TEXT CHARACTER=414 TEXT='@Sorensen2023, character 12'; + TEXT CHARACTER=415 TEXT='@Sorensen2023, character 13'; + TEXT CHARACTER=416 TEXT='@Sorensen2023, character 14'; + TEXT CHARACTER=417 TEXT='@Sorensen2023, character 15'; + TEXT CHARACTER=418 TEXT='@Sorensen2023, character 17'; + TEXT CHARACTER=419 TEXT='@Sorensen2023, character 18'; + TEXT CHARACTER=420 TEXT='@Sorensen2023, character 19'; + TEXT CHARACTER=421 TEXT='@Sorensen2023, character 21'; + TEXT CHARACTER=423 TEXT='@Sorensen2023, character 22: modified to make the presence of mucrones a separate character'; + TEXT CHARACTER=424 TEXT='Modified from @Sorensen2023, character 22'; + TEXT CHARACTER=425 TEXT='@Sorensen2023, character 24'; + + [Attribute comments] + TEXT CHARACTER= 1 TAXON=11 TEXT='Indicated by CT sections [@Zhang2022]'; + TEXT CHARACTER= 1 TAXON=13 TEXT='Large internal spaces [@Liu2019]'; + TEXT CHARACTER= 1 TAXON=16 TEXT='Seemingly present [@Shao2016]'; + TEXT CHARACTER= 1 TAXON=38 TEXT='Exhibits a large body cavity under certain conditions (possibly reproductive maturity?), even if some specimens lack one entirely [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 1 TAXON=39 TEXT='Exhibits a large body cavity under certain conditions (possibly reproductive maturity?), even if some specimens lack one entirely [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 1 TAXON=42 TEXT='Nematodes exhibit a primary body cavity that surrounds the organs and occupies much of the trunk [@SchmidtRhaesa2014, §1.7 and fig. 1.12]'; + TEXT CHARACTER= 1 TAXON=43 TEXT='Nematodes exhibit a primary body cavity that surrounds the organs and occupies much of the trunk [@SchmidtRhaesa2014, §1.7 and fig. 1.12]'; + TEXT CHARACTER= 1 TAXON=47 TEXT='Meiopriapulus is the only priapulan to exhibit a coelom: a small coelom surrounds the foregut [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 1 TAXON=52 TEXT='Large mixocoel is apparently homologous with the primary body cavity, though it fuses with the coelom during development [@Mayer2004az]'; + TEXT CHARACTER= 1 TAXON=53 TEXT='Large mixocoel is apparently homologous with the primary body cavity, though it fuses with the coelom during development [@Mayer2004az]'; + TEXT CHARACTER= 1 TAXON=54 TEXT='Large mixocoel is apparently homologous with the primary body cavity, though it fuses with the coelom during development [@Mayer2004az]'; + TEXT CHARACTER= 1 TAXON=131 TEXT='Coded as present, following @Smith2024'; + TEXT CHARACTER= 1 TAXON=134 TEXT='The ambiguous internal structure is interpreted as a primary body cavity, following @Smith2024'; + TEXT CHARACTER= 1 TAXON=138 TEXT='We interpret the dark stain within the cuticle as corresponding to the primary body cavity, after @Smith2024'; + TEXT CHARACTER= 1 TAXON=148 TEXT='We interpret the dark stain within the cuticle as corresponding to the primary body cavity, after @Smith2024'; + TEXT CHARACTER= 1 TAXON=152 TEXT='Large perivisceral cavity [@Smith2024]'; + TEXT CHARACTER= 1 TAXON=153 TEXT='Coded as present, following @Smith2024'; + TEXT CHARACTER= 1 TAXON=162 TEXT='Tonguelettes are interpreted as extensions of the primary body cavity [@Smith2024]'; + TEXT CHARACTER= 1 TAXON=163 TEXT='We interpret the dark stain within the cuticle as corresponding to the primary body cavity, after @Smith2024'; + TEXT CHARACTER= 1 TAXON=169 TEXT='Tonguelettes are interpreted as extensions of the primary body cavity [@Smith2024]'; + TEXT CHARACTER= 2 TAXON=7 TEXT='The preserved section of the incomplete NMNH198597 is 20 times longer than wide [@ConwayMorris1977]'; + TEXT CHARACTER= 2 TAXON=11 TEXT='The anterior is somewhat incomplete, but the layout of the gut demonstrates a short body [@Zhang2022]'; + TEXT CHARACTER= 2 TAXON=14 TEXT='Four [@Shao2020]'; + TEXT CHARACTER= 2 TAXON=16 TEXT='Estimated to range between 6 and 10 [@Shao2016]'; + TEXT CHARACTER= 2 TAXON=28 TEXT='Above 10:1, and notably longer than in other kinorhynchs -- interpreted as an adaptation to interstitial habitats [@Herranz2021z]'; + TEXT CHARACTER= 2 TAXON=33 TEXT='Above 10:1, and notably longer than in other kinorhynchs -- interpreted as an adaptation to interstitial habitats [@Herranz2021z]'; + TEXT CHARACTER= 2 TAXON=34 TEXT='Above 10:1, and notably longer than in other kinorhynchs -- interpreted as an adaptation to interstitial habitats [@Herranz2021z]'; + TEXT CHARACTER= 2 TAXON=97 TEXT='Incomplete specimens close to ten times longer than wide. Listed dimensions are 8 mm width and up to 100 mm length [@Howard2020].'; + TEXT CHARACTER= 2 TAXON=111 TEXT='~16 measured from @Hu2008, though the dimensions given in the text give a ratio closer to 12-13.'; + TEXT CHARACTER= 2 TAXON=119 TEXT='30-50 times longer than wide [@Yang2020]'; + TEXT CHARACTER= 2 TAXON=123 TEXT='"The ratio of width to length is ca. 1/20" [@Han2007pr]'; + TEXT CHARACTER= 2 TAXON=124 TEXT='Preserved component >10× longer than wide'; + TEXT CHARACTER= 2 TAXON=125 TEXT='At least 10× longer than wide [@Budd1998p]'; + TEXT CHARACTER= 2 TAXON=129 TEXT='Close to 20, measured from YKLP11313 [@Ma2014]'; + TEXT CHARACTER= 2 TAXON=131 TEXT='>20 [@Strausfeld2022]'; + TEXT CHARACTER= 2 TAXON=137 TEXT='~10 [@Haug2012]'; + TEXT CHARACTER= 2 TAXON=148 TEXT='~12-14 [@Ou2011]'; + TEXT CHARACTER= 3 TAXON=10 TEXT='Absent as not clear that plate distribution follows dorsal-ventral axis [@Maas2007].'; + TEXT CHARACTER= 3 TAXON=11 TEXT='Consistent orientation of expanded plates [@Zhang2022]'; + TEXT CHARACTER= 3 TAXON=12 TEXT='Uncertain; not enough of the trunk is preserved to determine whether sclerites indicate a dorsoventral polarity; @Liu2019 do not articulate their basis for identifying the dorsoventral orientation'; + TEXT CHARACTER= 3 TAXON=13 TEXT='Uncertain; not enough of the trunk is preserved to determine whether sclerites indicate a dorsoventral polarity'; + TEXT CHARACTER= 3 TAXON=14 TEXT='[@Shao2020]'; + TEXT CHARACTER= 3 TAXON=16 TEXT='Location of nerve cord [@Wang2025] not considered to differentiate trunk'; + TEXT CHARACTER= 3 TAXON=45 TEXT='Present: both Halicryptus species bear a ventral grove [@Shirley1999]'; + TEXT CHARACTER= 3 TAXON=108 TEXT='Present, reflected by the three lateral zones [@ConwayMorris2010]'; + TEXT CHARACTER= 3 TAXON=118 TEXT='Ventral trunk bears enlarged plates, termed protuberances [@Hu2012]'; + TEXT CHARACTER= 3 TAXON=119 TEXT='Ventral surface distinguished by presence of sclerites [@Han2007; @Shi2022; @ThisStudy]'; + TEXT CHARACTER= 3 TAXON=123 TEXT='Dorsal spines longer than ventral spines [@Han2007pr] – but no prominent differentiation of trunk, so coded ambiguous.'; + TEXT CHARACTER= 3 TAXON=126 TEXT='Appendages, and dorsal extent of sclerites [@Whittington1975]'; + TEXT CHARACTER= 4 TAXON=120 TEXT='Ventral projections are treated as potential homologues to paired appendages [@Dhungana2023]'; + TEXT CHARACTER= 5 TAXON=39 TEXT='Intestine terminates at the posterior end of the larva [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 5 TAXON=42 TEXT='Anus not terminal (Riemann 1972)'; + TEXT CHARACTER= 5 TAXON=103 TEXT='Although coded as abdominal by @Wills2012, it is not clear that this can be supported based on described fossil material [@Schram1973; @ConwayMorris1977]'; + TEXT CHARACTER= 6 TAXON=73 TEXT='The mouth appears to be at a terminal position, but due to the curvature of the trunk region, it faces anterio-ventrally. Since this character codes for a change in the position of the mouth, which is not observed, we code as terminal. '; + TEXT CHARACTER= 6 TAXON=74 TEXT='The mouth appears to be at a terminal position, but due to the curvature of the trunk region, it faces anterio-ventrally. Since this character codes for a change in the position of the mouth, which is not observed, we code as terminal. '; + TEXT CHARACTER= 6 TAXON=75 TEXT='The mouth appears to be at a terminal position, but due to the curvature of the trunk region, it faces anterio-ventrally. Since this character codes for a change in the position of the mouth, which is not observed, we code as terminal. '; + TEXT CHARACTER= 6 TAXON=76 TEXT='The mouth appears to be at a terminal position, but due to the curvature of the trunk region, it faces anterio-ventrally. Since this character codes for a change in the position of the mouth, which is not observed, we code as terminal. '; + TEXT CHARACTER= 6 TAXON=77 TEXT='The mouth appears to be at a terminal position, but due to the curvature of the trunk region, it faces anterio-ventrally. Since this character codes for a change in the position of the mouth, which is not observed, we code as terminal. '; + TEXT CHARACTER= 6 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 6 TAXON=126 TEXT='Terminal [@Whittington1978]'; + TEXT CHARACTER= 6 TAXON=128 TEXT='Following @Ou2012'; + TEXT CHARACTER= 6 TAXON=131 TEXT='Terminal mouth [@Strausfeld2022]'; + TEXT CHARACTER= 6 TAXON=141 TEXT='Terminal [@Howard2020]'; + TEXT CHARACTER= 6 TAXON=148 TEXT='A terminal mouth is incompatible with the extent of the preserved body cavity [@Ou2011]'; + TEXT CHARACTER= 6 TAXON=153 TEXT='@Liu2007az suggest a ventral location, although this could be due to compaction, therefore we code this as uncertain.'; + TEXT CHARACTER= 6 TAXON=156 TEXT='@Park2018 interpret a ventral position, contra @Budd1993; @Budd1998trse'; + TEXT CHARACTER= 6 TAXON=157 TEXT='The mouth opening is ventrally oriented in Pambdelurion [@Budd1998ar].'; + TEXT CHARACTER= 6 TAXON=166 TEXT='Ventral [@Cong2017]'; + TEXT CHARACTER= 7 TAXON=2 TEXT='The mouth of Gastrotrich is anterior, in some species terminal, in others is sub-terminal'; + TEXT CHARACTER= 7 TAXON=62 TEXT='Unclear if ventrally or anteriorly facing based on @Grimaldi1992 drawings.'; + TEXT CHARACTER= 7 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 7 TAXON=148 TEXT='Impossible to determine as obscured by head [@Ou2011]'; + TEXT CHARACTER= 7 TAXON=153 TEXT='Unclear mouth position, however mouth clearly not rotated to posteriad [@Liu2007az]'; + TEXT CHARACTER= 7 TAXON=156 TEXT='@Park2018 interpret the mouth has moved to a ventral position, yet faces anteriad, therefore has not been rotated to point posteriad.'; + TEXT CHARACTER= 7 TAXON=168 TEXT='Coded as anterior: figure 2J from @Moysiuk2019 shows that the mouth faces anteriorly, in contrast to the ventral facing mouth of e.g., Anomalocaris [see figures 5 and 8 from @Daley2014]. Therefore we code as anterior.'; + TEXT CHARACTER= 7 TAXON=169 TEXT='Hurdia specimens are often markedly disarticulated, and therefore difficult to code orientation of circumoral elements [@Daley2013jsp]. We conservatively code Hurdia ambiguous as there are no specimens in a ventral position to determine mouth orientation [supplementary data in @Daley2009]'; + TEXT CHARACTER= 7 TAXON=171 TEXT='Coded as ventral, per the two specimens in figure 3 of @Budd2021'; + TEXT CHARACTER= 7 TAXON=173 TEXT='@Cong2014 interpret a ventral-facing mouth'; + TEXT CHARACTER= 7 TAXON=174 TEXT='We code as ambiguous, as the mouthpart orientation is not clear from the fossil evidence given in figure 1F,H of @Kuhl2009'; + TEXT CHARACTER= 8 TAXON=7 TEXT='Contra @ConwayMorris1977, we interpret the anterior trunk as a differentiated anterior trunk; there is a gradual gradation between the anterior and posterior trunk, rather than a clear delineation. We interpret the narrow, seemingly unarmed (or perhaps lightly armed? Preservation does not allow the preclusion of diminutive armature) region between the trunk and the single ring of spines as the introvert, with the spines therefore corresponding to Zone II circumoral spines.'; + TEXT CHARACTER= 8 TAXON=10 TEXT='Denoted by ring of cusion-like folds'; + TEXT CHARACTER= 8 TAXON=11 TEXT='Anteriormost trunk missing [@Zhang2022]'; + TEXT CHARACTER= 8 TAXON=97 TEXT='The ''anterior proboscis'', which is ornamented with conical papillae [@Howard2020] as a differentiated anterior trunk. The circumoral elements are interpreted as denoting the Zone I armature. No Zone II armature is evident.'; + TEXT CHARACTER= 8 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 8 TAXON=119 TEXT='Following Yang et al., 2020'; + TEXT CHARACTER= 8 TAXON=122 TEXT='Present [@Shi2022]'; + TEXT CHARACTER= 8 TAXON=129 TEXT='The narrow end of the complete specimen YKLP11314 [@Ma2014] is not dissimilar from the introvert of O. ferox.^nWe interpret ELEL-SJ102058 [@Ou2018] to be folded back upon itself, accounting for the juxtaposition of its appendages on two adjacent layers within the sediment. On this view, the ''head'' of this specimen represents a cross-section through the trunk as it folds out of the plane of the specimen.'; + TEXT CHARACTER= 8 TAXON=131 TEXT='Uncertain; a small round disk seems to extend beyond the anterior head in @Strausfeld2022 fig. 3D, with a similar (denticulated?) circular structure in @Liu2014 fig. 4D. The identity of this structure requires further investigation.'; + TEXT CHARACTER= 8 TAXON=148 TEXT='Existence of a ventral structure cannot be ruled out [@Ou2011]'; + TEXT CHARACTER= 8 TAXON=156 TEXT='Coded ambiguous, reflecting possibility that Pambdelurion auxiliary plates correspond to introvert scalids [@Kihm2023]'; + TEXT CHARACTER= 8 TAXON=157 TEXT='Coded ambiguous, reflecting possibility that Pambdelurion auxiliary plates correspond to introvert scalids [@Kihm2023]'; + TEXT CHARACTER= 8 TAXON=158 TEXT='Coded ambiguous, reflecting possibility that Pambdelurion auxiliary plates correspond to introvert scalids [@Kihm2023]'; + TEXT CHARACTER= 9 TAXON=38 TEXT='Created by the lateral extension [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 9 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 9 TAXON=131 TEXT='Seemingly round [@Strausfeld2022]'; + TEXT CHARACTER= 10 TAXON=16 TEXT='Probably not invaginable, based on preservation of type material [@Liu2014]; but difficult to demonstrate conclusively'; + TEXT CHARACTER= 10 TAXON=19 TEXT='The anterior end can be inverted into the lorica [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 10 TAXON=20 TEXT='The anterior end can be inverted into the lorica [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 10 TAXON=21 TEXT='The anterior end can be inverted into the lorica [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 10 TAXON=22 TEXT='The anterior end can be inverted into the lorica [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 10 TAXON=23 TEXT='The anterior end can be inverted into the lorica [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 10 TAXON=24 TEXT='The anterior end can be inverted into the lorica [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 10 TAXON=26 TEXT='The anterior end can be inverted into the lorica [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 10 TAXON=27 TEXT='The anterior end can be inverted into the lorica [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 10 TAXON=33 TEXT='Can be fully retracted [@Neuhaus2002icb]'; + TEXT CHARACTER= 10 TAXON=38 TEXT='Invaginable [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 10 TAXON=97 TEXT='Same width as trunk and never invaginated; interpreted as not invaginable [@Howard2020]'; + TEXT CHARACTER= 10 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 11 TAXON=7 TEXT='Invaginable (see NMNH83939)'; + TEXT CHARACTER= 11 TAXON=39 TEXT='See e.h. @Kakui2021'; + TEXT CHARACTER= 11 TAXON=95 TEXT='Always dumbbell shaped (?) [@Maas2007ppp]'; + TEXT CHARACTER= 11 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 12 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 13 TAXON=9 TEXT='Absent [@Dong2010]'; + TEXT CHARACTER= 13 TAXON=17 TEXT='Seemingly present [@Harvey2017nee], but difficult to establish number or morphology; elements in SEM perhaps bear subtle hints of serration, but are not unequivocally trichoscalids.'; + TEXT CHARACTER= 13 TAXON=24 TEXT='15 single trichoscalids [@Fujimoto2020mb]'; + TEXT CHARACTER= 13 TAXON=28 TEXT='Absent [@Rucci2020z; @Herranz2021z]'; + TEXT CHARACTER= 13 TAXON=32 TEXT='Introvert stylets are innervated from ten longitudinal introvert nerves that extend from the ventrally open forebrain, which comprises ten lobes of perikarya [@Nebelsick1993z]'; + TEXT CHARACTER= 13 TAXON=42 TEXT='Two rings of articulate labial papillae [@Riemann1972] '; + TEXT CHARACTER= 13 TAXON=43 TEXT='"Labial region offset by a constriction. Papillae prominent." [@Peneva1999n]'; + TEXT CHARACTER= 13 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 13 TAXON=126 TEXT='The oral papillae are treated as potential homologues, by comparison with nematodes'; + TEXT CHARACTER= 14 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 15 TAXON=21 TEXT='Fifteen [@Gad2005mbr]'; + TEXT CHARACTER= 15 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 17 TAXON=24 TEXT='Basal plates present on neck alongside alternate trichoscalids, though absent on thorax [@Fujimoto2020mb]'; + TEXT CHARACTER= 17 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 18 TAXON=19 TEXT='@Heiner2004hmr'; + TEXT CHARACTER= 18 TAXON=20 TEXT='@Heiner2007hmr'; + TEXT CHARACTER= 18 TAXON=22 TEXT='@Gad2005ode'; + TEXT CHARACTER= 19 TAXON=21 TEXT='Serrated [@Heiner2008sb]'; + TEXT CHARACTER= 19 TAXON=24 TEXT='[@Fujimoto2020mb]'; + TEXT CHARACTER= 19 TAXON=30 TEXT='Fringed [@Neuhaus2012za]'; + TEXT CHARACTER= 19 TAXON=33 TEXT='Simple spines [@Herranz2016za]'; + TEXT CHARACTER= 19 TAXON=34 TEXT='Covered with long hairs [@Neuhaus2015z]'; + TEXT CHARACTER= 19 TAXON=35 TEXT='Seemingly fringed [@Sorensen2012mbrm fig. 5b]'; + TEXT CHARACTER= 20 TAXON=18 TEXT='Seven double and eight single [@Kristensen2007ib]'; + TEXT CHARACTER= 20 TAXON=19 TEXT='Eight single, seven double [@Heiner2004hmr]'; + TEXT CHARACTER= 20 TAXON=20 TEXT='Eight single, seven double [@Heiner2007hmr]'; + TEXT CHARACTER= 20 TAXON=21 TEXT='Single [@Heiner2008sb]'; + TEXT CHARACTER= 20 TAXON=22 TEXT='Eight single, seven double [@Gad2005ode]'; + TEXT CHARACTER= 20 TAXON=24 TEXT='Single [@Fujimoto2020mb]'; + TEXT CHARACTER= 21 TAXON=1 TEXT='By comparison with the Higgins larva [e.g. Sorensen2023ode], it is possible that an introvert existed anterior to the preserved lorica and neck, but is not preserved due to distinct preservation [@Maas2009] or involusion in the available material.'; + TEXT CHARACTER= 21 TAXON=7 TEXT='Four rows of diminutive scalids at base of armature (ROM 93-1678), with gap before circumoral scalids (see e.g. NMNH198597, 198605)'; + TEXT CHARACTER= 21 TAXON=9 TEXT='@Dong2010'; + TEXT CHARACTER= 21 TAXON=40 TEXT='Lobe-like outgrowths of the stoma with small denticles on their edges [@Kulikov1998rjn] are treated as elements of the pharyngostome, per @Venekey2019z and @Inglis1969bbmnh; hence there is no introvert armature'; + TEXT CHARACTER= 21 TAXON=41 TEXT='Six odontia in anterior portion of buccal cavity (i.e. cheilostome), with accessory structures in between [@Leduc2016n]'; + TEXT CHARACTER= 21 TAXON=42 TEXT='A lip papilla occurs immediately adjacent to the articulated head seta; ahead of this is a double row of sclerotized spines [@Riemann1972].'; + TEXT CHARACTER= 21 TAXON=43 TEXT='Unarmed [@Borgonie1995]'; + TEXT CHARACTER= 21 TAXON=97 TEXT='A ring of diminutive conical elements (''oral spines'') surrounds the larger plates (RCCBYU10233; YKLP 11410) [@Howard2020]'; + TEXT CHARACTER= 21 TAXON=110 TEXT='Corynetis seems to have an unarmoured introvert leading to a ring of elongate circumoral spines [@Huang2004; @Hu2012; @Chen2012]'; + TEXT CHARACTER= 21 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 21 TAXON=121 TEXT='Specimens with well-preserved introverts and pharynxes are figured by @Maas2007ppp, @Hou1994; @Vannier2017'; + TEXT CHARACTER= 21 TAXON=131 TEXT='If the short oral projections visible in Fig. 4D are not taphonomic artefacts, they are most likely to correspond to Zone II elements.'; + TEXT CHARACTER= 21 TAXON=157 TEXT='We score the ovate plates as equivalent to Zone I armature, following @Kihm2023'; + TEXT CHARACTER= 21 TAXON=158 TEXT='We score the ovate plates as equivalent to Zone I armature, following @Kihm2023'; + TEXT CHARACTER= 22 TAXON=9 TEXT='Presumably yes; the three circlets comprise 8+8+9 sclerites [@Dong2010]'; + TEXT CHARACTER= 22 TAXON=16 TEXT='Ambiguous: Defined by first two rows. 12 rows of 9 scalids each offset to produce 18 rows [@Shao2020]'; + TEXT CHARACTER= 22 TAXON=38 TEXT='Only three circlets present^n'; + TEXT CHARACTER= 22 TAXON=44 TEXT='25 rows less regimented than in other priapulans [@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 22 TAXON=97 TEXT='Single ring of elements. (See discussion of introvert for identity of zonal elements.)'; + TEXT CHARACTER= 22 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 22 TAXON=119 TEXT='Defined by first circlet only [@Yang2020]'; + TEXT CHARACTER= 23 TAXON=40 TEXT='Not figured in sufficient detail [@Kulikov1998rjn]'; + TEXT CHARACTER= 23 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 23 TAXON=157 TEXT='Seemingly directed as in Omnidens [@Vinther2016]'; + TEXT CHARACTER= 23 TAXON=158 TEXT='Apices directed away from mouth, thus posteriad [@Li2024]'; + TEXT CHARACTER= 24 TAXON=9 TEXT='Three [@Dong2010]'; + TEXT CHARACTER= 24 TAXON=39 TEXT='Two in adults [@Poinar2001]'; + TEXT CHARACTER= 24 TAXON=41 TEXT='Odontia and accessory buccal structures [@Leduc2016n] treated as separate circlets in close proximity'; + TEXT CHARACTER= 24 TAXON=97 TEXT='Seemingly a single circlet of oral teeth [@Howard2020]'; + TEXT CHARACTER= 24 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 24 TAXON=120 TEXT='Five circlets [@ThisStudy]'; + TEXT CHARACTER= 24 TAXON=121 TEXT='Multiple circlets most obvious in @Vannier2017'; + TEXT CHARACTER= 25 TAXON=103 TEXT='Reported as two transverse bands by @Wills1998, without evidence; this is not evident in figured material [@Schram1973; @ConwayMorris1977], so is scored as ambiguous'; + TEXT CHARACTER= 25 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 26 TAXON=38 TEXT='Only three rows; hence conservatively coded as ambiguous'; + TEXT CHARACTER= 26 TAXON=98 TEXT='Chaotically scattered [@Ma2014; @Yang2021]'; + TEXT CHARACTER= 26 TAXON=103 TEXT='Prominent rows [@ConwayMorris1977]'; + TEXT CHARACTER= 26 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 26 TAXON=120 TEXT='Parallel longitudinal rows [@ThisStudy]'; + TEXT CHARACTER= 26 TAXON=121 TEXT='Forming quincunx, possibly with gap between anterior and posterior region [@Vannier2017]'; + TEXT CHARACTER= 27 TAXON=16 TEXT='Parallel rows [@Liu2014]'; + TEXT CHARACTER= 27 TAXON=18 TEXT='Parallel rows [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 27 TAXON=38 TEXT='Only three rows; hence conservatively coded as ambiguous'; + TEXT CHARACTER= 27 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 27 TAXON=122 TEXT='Seemingly longitudinal [@Shi2022], but difficult to be certain '; + TEXT CHARACTER= 27 TAXON=123 TEXT='Qincunx [@Han2007pr]'; + TEXT CHARACTER= 28 TAXON=8 TEXT='GSC 45331'; + TEXT CHARACTER= 28 TAXON=42 TEXT='Continuous to the base of the ''pricks'' [@Reiman1972], which we interpret as Zone II elements'; + TEXT CHARACTER= 28 TAXON=97 TEXT='Position of Zone II unclear.'; + TEXT CHARACTER= 28 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 29 TAXON=16 TEXT='Hollow cuticular spines [@Liu2014]'; + TEXT CHARACTER= 29 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 30 TAXON=38 TEXT='Stated as solid, but likely hollow as in Nectonema.'; + TEXT CHARACTER= 30 TAXON=39 TEXT='Hollow [@SchmidtRhaesa1996]'; + TEXT CHARACTER= 30 TAXON=41 TEXT='Central cavity evident in odontia [@Leduc2016n]'; + TEXT CHARACTER= 30 TAXON=97 TEXT='Preservation suggests hollow'; + TEXT CHARACTER= 30 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 30 TAXON=121 TEXT='Preservation suggests a central cavity [@Vanner2017]'; + TEXT CHARACTER= 31 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 32 TAXON=7 TEXT='Too short to evaluate'; + TEXT CHARACTER= 32 TAXON=8 TEXT='Simple cones (GSC 45331)'; + TEXT CHARACTER= 32 TAXON=24 TEXT='No innate curvature evident, though flexible [@Fujimoto2020mb]'; + TEXT CHARACTER= 32 TAXON=97 TEXT='Slightly curved posteriad [@Howard2020]'; + TEXT CHARACTER= 32 TAXON=103 TEXT='"Apparently simple cones" [@ConwayMorris1977]'; + TEXT CHARACTER= 32 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 33 TAXON=28 TEXT='No evidence of bifurcation [@Rucci2020z]'; + TEXT CHARACTER= 33 TAXON=38 TEXT='Bifurcation of single ventral sclerite'; + TEXT CHARACTER= 33 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 34 TAXON=8 TEXT='Cannot rule out presence of fine denticles'; + TEXT CHARACTER= 34 TAXON=33 TEXT='Basal component with pectinate fringe [@BauerNebelsick1995]'; + TEXT CHARACTER= 34 TAXON=41 TEXT='Accessory elements bear knobs [@Leduc2016n]'; + TEXT CHARACTER= 34 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 35 TAXON=33 TEXT='Bipartite, with a broad base and an elongate tip, but not obviously articulated [@BauerNebelsick1995]'; + TEXT CHARACTER= 35 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 36 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 37 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 38 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 39 TAXON=28 TEXT='Absent [@Herranz2021z]'; + TEXT CHARACTER= 39 TAXON=29 TEXT='Presence in genera reported by @Herranz2021z'; + TEXT CHARACTER= 39 TAXON=30 TEXT='No data available [@Herranz2021z]'; + TEXT CHARACTER= 39 TAXON=31 TEXT='Presence in genera reported by @Herranz2021z'; + TEXT CHARACTER= 39 TAXON=32 TEXT='Absent [@Herranz2021z]'; + TEXT CHARACTER= 39 TAXON=33 TEXT='Presence in genera reported by @Herranz2021z'; + TEXT CHARACTER= 39 TAXON=34 TEXT='Absent [@Herranz2021z]'; + TEXT CHARACTER= 39 TAXON=35 TEXT='Present [@Herranz2021z]'; + TEXT CHARACTER= 39 TAXON=36 TEXT='Absent [@Herranz2021z]'; + TEXT CHARACTER= 39 TAXON=37 TEXT='Absent; within Allomalorhagida, intrinsic muscles in the primary spinoscalids are only present in Dracoderes [@Herranz2021z]'; + TEXT CHARACTER= 40 TAXON=12 TEXT='Alternating rows of 12 sclerites [@Liu2019]'; + TEXT CHARACTER= 40 TAXON=13 TEXT='11 introvert rows; see media [@ThisStudy]'; + TEXT CHARACTER= 40 TAXON=16 TEXT='18 rows [@Shao2016]'; + TEXT CHARACTER= 40 TAXON=24 TEXT='Thirty elements per row [@Fujimoto2020mb]'; + TEXT CHARACTER= 40 TAXON=92 TEXT='Ten buccal lamellae [@Michalczyk2003], but these do not necessarily correspond to the symmetry of the introvert.'; + TEXT CHARACTER= 40 TAXON=103 TEXT='At least twenty, and possibly twenty five, rows – but exact number uncertain [@ConwayMorris1977]'; + TEXT CHARACTER= 40 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 40 TAXON=123 TEXT='Likely pentaradial: four or five scalids in each ring, offset to produce comprises 8-10 longitudinal rows [@Han2007]'; + TEXT CHARACTER= 41 TAXON=9 TEXT='8+8+9 [@Dong2010]'; + TEXT CHARACTER= 41 TAXON=95 TEXT='Ten rows visible [@Maas2007ppp]; total could conceivably be 18, 19, or 20.'; + TEXT CHARACTER= 41 TAXON=111 TEXT='Possibly around 25 elements [@Hu2008], but preservation to poor to confirm'; + TEXT CHARACTER= 41 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 42 TAXON=12 TEXT='Alternating rows of 12 sclerites [@Liu2019]'; + TEXT CHARACTER= 42 TAXON=16 TEXT='18 [@Shao2016]'; + TEXT CHARACTER= 42 TAXON=38 TEXT='Six teeth per row'; + TEXT CHARACTER= 42 TAXON=41 TEXT='6 + 6'; + TEXT CHARACTER= 42 TAXON=80 TEXT='Six, defined by oral papillae [@Dewel2006]'; + TEXT CHARACTER= 42 TAXON=91 TEXT='@Kristensen1982'; + TEXT CHARACTER= 42 TAXON=92 TEXT='Six buccal lamellae [@Kihm2023]'; + TEXT CHARACTER= 42 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 42 TAXON=123 TEXT='Likely pentaradial: four or five scalids in each ring, offset to produce comprises 8-10 longitudinal rows [@Han2007pr]'; + TEXT CHARACTER= 43 TAXON=40 TEXT='Large solid onchium, usually ''bent about its mid-length'' [@Inglis1969bbmnh]'; + TEXT CHARACTER= 43 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 44 TAXON=38 TEXT='The "buccal cavity" corresponds to the inverted introvert, rather than a separate chamber [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 44 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 44 TAXON=131 TEXT='Possibly represented by the ''buccal tube'' of @Strausfeld2022'; + TEXT CHARACTER= 44 TAXON=138 TEXT='Ambiguous: the apparently internal position of the circumoral plates could denote post mortem retraction of the pharyngeal apparatus, as observed in tardigrades [@Khim2023].^n'; + TEXT CHARACTER= 45 TAXON=24 TEXT='No annulations evident [@Fujimoto2020mb]'; + TEXT CHARACTER= 45 TAXON=41 TEXT='Not evident in light micrographs [@Leduc2016n]'; + TEXT CHARACTER= 45 TAXON=43 TEXT='Annulations below bifurcated lobes in buccal cavity [@Borgonie1995fan]'; + TEXT CHARACTER= 45 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 46 TAXON=7 TEXT='Never preserved everted [@ConwayMorris1977]; consistent position, extent and shape in NMNH198605, NMNH198597, ROM93-1678'; + TEXT CHARACTER= 46 TAXON=8 TEXT='Partly everted in GSC 45331'; + TEXT CHARACTER= 46 TAXON=11 TEXT='Inferred from bulb-like shape [@Zhang2022]'; + TEXT CHARACTER= 46 TAXON=38 TEXT='The stylet is treated as an eversible pharynx'; + TEXT CHARACTER= 46 TAXON=39 TEXT='The stylet is treated as an eversible pharynx'; + TEXT CHARACTER= 46 TAXON=97 TEXT='No indication of eversibility [@Howard2020]'; + TEXT CHARACTER= 46 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 46 TAXON=126 TEXT='NMNH83942a exhibits a small pyrimidal extension of the pharynx; NMNH57655 displays an everted pharynx, narrower than the introvert and conceivably tipped with triangular teeth, evident in reflected light but obscured by a dark stain in polarized light [@Whittington1975, figs 10–11]. Further investigation is necessary to establish the nature of this structure.'; + TEXT CHARACTER= 46 TAXON=156 TEXT='Anterior position in certain specimens is attributed to post-mortem processes [@Park2018]'; + TEXT CHARACTER= 46 TAXON=157 TEXT='Oral cone eversible, but pharynx is not [@Vinther2016]'; + TEXT CHARACTER= 47 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 48 TAXON=28 TEXT='The kinorhynch introvert is a locomotory and sensory organ [@Herranz2021z]'; + TEXT CHARACTER= 48 TAXON=29 TEXT='The kinorhynch introvert is a locomotory and sensory organ [@Herranz2021z]'; + TEXT CHARACTER= 48 TAXON=30 TEXT='The kinorhynch introvert is a locomotory and sensory organ [@Herranz2021z]'; + TEXT CHARACTER= 48 TAXON=31 TEXT='The kinorhynch introvert is a locomotory and sensory organ [@Herranz2021z]'; + TEXT CHARACTER= 48 TAXON=32 TEXT='The kinorhynch introvert is a locomotory and sensory organ [@Herranz2021z]'; + TEXT CHARACTER= 48 TAXON=33 TEXT='The kinorhynch introvert is a locomotory and sensory organ [@Herranz2021z]'; + TEXT CHARACTER= 48 TAXON=34 TEXT='The kinorhynch introvert is a locomotory and sensory organ [@Herranz2021z]'; + TEXT CHARACTER= 48 TAXON=35 TEXT='The kinorhynch introvert is a locomotory and sensory organ [@Herranz2021z]'; + TEXT CHARACTER= 48 TAXON=36 TEXT='The kinorhynch introvert is a locomotory and sensory organ [@Herranz2021z]'; + TEXT CHARACTER= 48 TAXON=37 TEXT='The kinorhynch introvert is a locomotory and sensory organ [@Herranz2021z]'; + TEXT CHARACTER= 48 TAXON=42 TEXT='Employed in locomotion [@Reiman1972]'; + TEXT CHARACTER= 48 TAXON=95 TEXT='Interpreted as locomotory introvert [@Maas2007ppp]'; + TEXT CHARACTER= 48 TAXON=98 TEXT='Introvert interpreted as locomotory [@Ma2014]'; + TEXT CHARACTER= 48 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 48 TAXON=129 TEXT='Locomotion presumably employed the appendages'; + TEXT CHARACTER= 49 TAXON=44 TEXT='Fully everted pharynx not observed in the six available specimens [@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 49 TAXON=95 TEXT='Substantial eversion evident [@Maas2007ppp]'; + TEXT CHARACTER= 49 TAXON=98 TEXT='Not everted beyond proximal teeth in any known specimen [@Ma2014; @Yang2021]'; + TEXT CHARACTER= 49 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 50 TAXON=22 TEXT='Elongate mouth cone possibly an apomorphy of this species [@Higgins1986scz]'; + TEXT CHARACTER= 50 TAXON=24 TEXT='Elongate mouth tube [@Fujimoto2020mb]'; + TEXT CHARACTER= 50 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 50 TAXON=121 TEXT='Neither'; + TEXT CHARACTER= 51 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 52 TAXON=33 TEXT='Round [@Neuhaus2002icb]'; + TEXT CHARACTER= 52 TAXON=38 TEXT='Triradial oesophagus [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 52 TAXON=69 TEXT='Triradiate in A. doryphorus [@EibyeJacobsen2001jzser]'; + TEXT CHARACTER= 52 TAXON=73 TEXT='Triradiate in E. viridissimus [@EibyeJacobsen2001jzser]'; + TEXT CHARACTER= 52 TAXON=80 TEXT='Triradiate [@EibyeJacobsen2001jzser]'; + TEXT CHARACTER= 52 TAXON=91 TEXT='Triradiate [@EibyeJacobsen2001jzser]'; + TEXT CHARACTER= 52 TAXON=97 TEXT='Three prominent robust elements'; + TEXT CHARACTER= 52 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 53 TAXON=8 TEXT='Circumpharyngeal spines evident in GSC 45331, left and right of partly everted pharynx'; + TEXT CHARACTER= 53 TAXON=11 TEXT='Associated region not preserved [@Zhang2022]'; + TEXT CHARACTER= 53 TAXON=15 TEXT='The ‘wrinkles’ at the base of the Eokinorhynchus pharynx [@Zhang2015, fig. 1f] seem to be cuticular structures rather than spines lying flat against the pharynx surface'; + TEXT CHARACTER= 53 TAXON=20 TEXT='The base of the mouth cone is marked by a ring of pleats that intriguingly resemble circumoral plates [@Heiner2007hmr], but in fact represent ridges of the cuticle [@Neves2016za].'; + TEXT CHARACTER= 53 TAXON=28 TEXT='Spinose processes (cf. those in Cateria?) occur just inside the primary spinoscalids [@Rucci2020z]'; + TEXT CHARACTER= 53 TAXON=34 TEXT='Some specimens of Cateria exhibit a ring of cuticular spines anterior to the primary spinoscalids; these spines are sometimes joined by a sheet of cuticle, becoming distinct only distally [@Neuhaus2015z, fig. 6A, 10E, 13G]. These elements are indistinct and poorly known; if they represent Zone II elements this may prompt the primary spinoscalids to be reconsidered as elements of Zone I. We treat the primary spinoscalids as Zone II elements here, leaving the nature of the cuticular spine-sheet open.'; + TEXT CHARACTER= 53 TAXON=42 TEXT='The twelve pricks [@Reiman1972] are interpreted as Zone II elements based on their position and morphology'; + TEXT CHARACTER= 53 TAXON=44 TEXT='Not obvious in SEM or µCT images [@SchmidtRhaesa2022za], but absence difficult to determine'; + TEXT CHARACTER= 53 TAXON=94 TEXT='The velum is considered to represent fused lamellae [@Guidetti2012]'; + TEXT CHARACTER= 53 TAXON=95 TEXT='Spines [@Maas2007ppp, fig 7a]'; + TEXT CHARACTER= 53 TAXON=98 TEXT='Elongate spines [@Yang2021]'; + TEXT CHARACTER= 53 TAXON=103 TEXT='Peribuccal collar preserved [@ConwayMorris1977]'; + TEXT CHARACTER= 53 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 53 TAXON=121 TEXT='Faint ring of elongate circumoral spines [@Maas2007ppp; @Vannier2017]'; + TEXT CHARACTER= 53 TAXON=122 TEXT='Seemingly absent. The "collar spines" of @Shi2022 are taphonomic features reflecting flaking of the cuticle; they do not have a consistent shape and do not recur around the pharynx. The margins of the pharynx are smooth, and thus prominently unarmed. The possible presence of coronal spines is harder to discount with certainty, but we see no candidates; spines if present must be diminutive. We score as absent.'; + TEXT CHARACTER= 53 TAXON=123 TEXT='Anterior scalids are distinct from others on the introvert and can point anteriad or posteriad [@Han2007pr], and are likely coronal spines – but better material is required for confident designation.'; + TEXT CHARACTER= 53 TAXON=126 TEXT='Mouth surrounded by six slim papillae [@Whittington1975]'; + TEXT CHARACTER= 53 TAXON=129 TEXT='Armature not preserved, but impossible to rule out absence, particularly given the elusive nature of equivalent structures in e.g. Hallucigenia [@Smith2015].'; + TEXT CHARACTER= 53 TAXON=131 TEXT='Potentially represented by the radial structures that surround the mouth in @Liu2014, fig. 4D'; + TEXT CHARACTER= 53 TAXON=132 TEXT='The cuticular ring reported in the head of Microdictyon [@Liu2014ppp] requires detailed study before its interpretation can be considered secure. '; + TEXT CHARACTER= 53 TAXON=141 TEXT='Preservation inadequate to evaluate [@Howard2020]'; + TEXT CHARACTER= 53 TAXON=143 TEXT='Detailed arrangement of tooth-like structures compatible with arrangement in Hallucigenia [@Smith2015], but inadequately preserved to evaluate [@Caron2017]. Coded as ambiguous.'; + TEXT CHARACTER= 53 TAXON=148 TEXT='Antennacanthopodia [@Ou2011] is coded as ambiguous as there is no direct evidence for the location of the mouth.'; + TEXT CHARACTER= 53 TAXON=152 TEXT='Uncertain, as oral surface is incompletely preserved [@Smith2023n], and it is possible that such structures would become more prominent in adults'; + TEXT CHARACTER= 53 TAXON=153 TEXT='Present [@Vannier2014, supplementary figure 6]'; + TEXT CHARACTER= 53 TAXON=163 TEXT='Radial structures around the mouth drawn by @Whittington1975 are interpreted by @Dhungana2021 as circumoral plates.'; + TEXT CHARACTER= 53 TAXON=166 TEXT='Smooth and tuberculate plates are interpreted as elements of an Anomalocaris-like oral cone [@Cong2017]'; + TEXT CHARACTER= 53 TAXON=173 TEXT='Not described in original reports [@Cong2014; @Cong2016; @Cong2017], but documented in a juvenile by @Liu2018nsr, who propose that the absence in larger specimens is taphonomic.'; + TEXT CHARACTER= 54 TAXON=7 TEXT='Erect triangular spines [@ConwayMorris1977]'; + TEXT CHARACTER= 54 TAXON=20 TEXT='We interpret the plicae as erect spines, as in cases they seem to .'; + TEXT CHARACTER= 54 TAXON=55 TEXT='We code tardigrades as having a small contact area as the peribuccal lamellae are only basally attached to the body [e.g. @Guidetti2013, figure 3B]'; + TEXT CHARACTER= 54 TAXON=97 TEXT='Semi-erect plates [@Howard2020, RCCBYU 10233]'; + TEXT CHARACTER= 54 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 54 TAXON=126 TEXT='Erect [@Whittington1975]'; + TEXT CHARACTER= 54 TAXON=153 TEXT='Due to the limited material we code this as ambiguous'; + TEXT CHARACTER= 54 TAXON=157 TEXT='Ambiguous. Although @Vinther2016 (e.g. Figure 3D) reconstruct only the basal parts of the ''triangular plates'' as in contact with the body, comparison with Omnidens suggests a more complete attachment.'; + TEXT CHARACTER= 54 TAXON=158 TEXT='The flat surfaces of the plate are interpreted as in contact with the body, with the inner spines protruding'; + TEXT CHARACTER= 55 TAXON=8 TEXT='Presumably continuous ring but only evident at sides (GSC 45331)'; + TEXT CHARACTER= 55 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 55 TAXON=154 TEXT='Traditionally interpreted as radial. @Li2024 suggest the possibility of a biserial configuration, but @Vannier2014 clearly show both Zone II and Zone III elements occurring along the midline of the specimen, seemingly corroborating a radial configuration.'; + TEXT CHARACTER= 55 TAXON=157 TEXT='@Li2024'; + TEXT CHARACTER= 55 TAXON=159 TEXT='Traditionally interpreted as radial, but plausibly bilateral [@Li2024]'; + TEXT CHARACTER= 55 TAXON=166 TEXT='Traditionally interpreted as radial, but plausibly bilateral [@Li2024]'; + TEXT CHARACTER= 55 TAXON=173 TEXT='Traditionally interpreted as radial, but plausibly bilateral [@Li2024]'; + TEXT CHARACTER= 56 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 56 TAXON=158 TEXT='Some size differences but no prominent differentiation [@Li2024]'; + TEXT CHARACTER= 56 TAXON=159 TEXT='Differentiated, following @Budd2021'; + TEXT CHARACTER= 56 TAXON=163 TEXT='Undifferentiated elongate plates [@Dhungana2021]'; + TEXT CHARACTER= 56 TAXON=166 TEXT='Three or possibly four tuberculate plates [@Cong2017]'; + TEXT CHARACTER= 56 TAXON=168 TEXT='@Moysiuk2019'; + TEXT CHARACTER= 56 TAXON=173 TEXT='Four enlarged plates [@Liu2018nsr]'; + TEXT CHARACTER= 56 TAXON=174 TEXT='Figure 1H of @Kuhl2009 has no indication that any of the plates are enlarged, therefore we code Schinderhannes as having undifferentiated circumoral sclerites. The reconstruction of @Kuhl2009 implicitly implies a lack of differentiation'; + TEXT CHARACTER= 57 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 58 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 59 TAXON=92 TEXT='On inner face only [@Michalczyk2003]'; + TEXT CHARACTER= 59 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 59 TAXON=166 TEXT='Tubercles [@Cong2017]'; + TEXT CHARACTER= 59 TAXON=168 TEXT='Not evident or interpreted as present by @Moysiuk2019'; + TEXT CHARACTER= 59 TAXON=173 TEXT='Present [@Liu2018nsr]'; + TEXT CHARACTER= 60 TAXON=78 TEXT='Only present in parachelan species [@Guidetti2012]'; + TEXT CHARACTER= 60 TAXON=79 TEXT='Only present in parachelan species [@Guidetti2012]'; + TEXT CHARACTER= 60 TAXON=80 TEXT='Absent [@Dewel2006]'; + TEXT CHARACTER= 60 TAXON=81 TEXT='In Bertolanius volubilis (Eohypsibiidae) [@Guidetti2015]'; + TEXT CHARACTER= 60 TAXON=92 TEXT='On inner face only [@Michalczyk2003]'; + TEXT CHARACTER= 60 TAXON=93 TEXT='The ''anterior tooth row'' of @Kihm20203, fig. 1F'; + TEXT CHARACTER= 60 TAXON=94 TEXT='Corresponding to anterior band of buccal armature [@Guidetti2012]'; + TEXT CHARACTER= 60 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 61 TAXON=97 TEXT='Strong three-dimensional relief [@Howard2020] implies robust original construction '; + TEXT CHARACTER= 61 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 62 TAXON=16 TEXT='''Possibly'' twelve [@Shao2020; @Liu2014]'; + TEXT CHARACTER= 62 TAXON=42 TEXT='Six pairs [@Reiman1972]'; + TEXT CHARACTER= 62 TAXON=80 TEXT='Four [@Dewel2006]'; + TEXT CHARACTER= 62 TAXON=81 TEXT='In Bertolanius volubilis (Eohypsibiidae) [@Guidetti2015]'; + TEXT CHARACTER= 62 TAXON=92 TEXT='Ten [@Guidetti2012]'; + TEXT CHARACTER= 62 TAXON=93 TEXT='Ten [@Guidetti2012]'; + TEXT CHARACTER= 62 TAXON=94 TEXT='Single fused element? [@Guidetti2012]'; + TEXT CHARACTER= 62 TAXON=97 TEXT='Three visible in lateral view, indicating six in original circlet'; + TEXT CHARACTER= 62 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 63 TAXON=8 TEXT='Elongate spines (GSC 45331)'; + TEXT CHARACTER= 63 TAXON=16 TEXT='Only bases preserved [@Liu2014]'; + TEXT CHARACTER= 63 TAXON=95 TEXT='Just visible in @Maas2007ppp, figs 3b, 7a'; + TEXT CHARACTER= 63 TAXON=98 TEXT='Elongate [@Yang2021]'; + TEXT CHARACTER= 63 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 63 TAXON=126 TEXT='Around five times longer than wide [@Whittington1975]'; + TEXT CHARACTER= 64 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 65 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 65 TAXON=163 TEXT='Single projection [@Dhungana2021]'; + TEXT CHARACTER= 66 TAXON=33 TEXT='Secondary setae and pectinate projections [@BauerNebelsick1995]'; + TEXT CHARACTER= 66 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 66 TAXON=158 TEXT='Accessory spines on certain plates [@Li2024]'; + TEXT CHARACTER= 66 TAXON=163 TEXT='Single projection [@Dhungana2021]'; + TEXT CHARACTER= 66 TAXON=166 TEXT='@Cong2017 identify two spinose projections on one tuberculate plate (fig. 8e), but only one is unambiguously evident. We thus code this character as ambiguous.'; + TEXT CHARACTER= 66 TAXON=168 TEXT='Multiple spines [@Moysiuk2019]'; + TEXT CHARACTER= 66 TAXON=173 TEXT='Multiple spines [@Liu2018nsr]'; + TEXT CHARACTER= 67 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 68 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 68 TAXON=122 TEXT='Narrower [@Shi2022]'; + TEXT CHARACTER= 69 TAXON=15 TEXT='Short gap of wrinkled cuticle [@Zhang2015, fig. 1]'; + TEXT CHARACTER= 69 TAXON=16 TEXT='Negligible gap [@Liu2014]'; + TEXT CHARACTER= 69 TAXON=38 TEXT='Unarmed region present [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 69 TAXON=44 TEXT='Seemingly a gap based on µCT data [@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 69 TAXON=50 TEXT='Gap [@Schmidt2017, fig 2A]'; + TEXT CHARACTER= 69 TAXON=92 TEXT='Gap [@Michalczyk2003]'; + TEXT CHARACTER= 69 TAXON=93 TEXT='Gap, best seen towards bottom of @Kihm20203, fig. 1F'; + TEXT CHARACTER= 69 TAXON=94 TEXT='Without prominent gap [@Guidetti2012]'; + TEXT CHARACTER= 69 TAXON=95 TEXT='No gap [@Maas2007ppp]'; + TEXT CHARACTER= 69 TAXON=98 TEXT='Gap [@Ma2014, fig 5.4]'; + TEXT CHARACTER= 69 TAXON=111 TEXT='Apparent teeth gap [@Hu2008]'; + TEXT CHARACTER= 69 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 69 TAXON=120 TEXT='Prominent gap [@ThisStudy]'; + TEXT CHARACTER= 69 TAXON=121 TEXT='Gap minimal, if it exists at all [@Maas2007ppp; @Vannier2017]'; + TEXT CHARACTER= 69 TAXON=122 TEXT='Prominent teeth gap (see notes on Zone II sclerites)'; + TEXT CHARACTER= 69 TAXON=123 TEXT='Unarmed ''collar'' [@Han2007pr]'; + TEXT CHARACTER= 69 TAXON=154 TEXT='No gap [@Liu2006]'; + TEXT CHARACTER= 69 TAXON=157 TEXT='Directly adjacent [@Vinther2016]'; + TEXT CHARACTER= 69 TAXON=158 TEXT='No separation [@Li2024; @Li2025]'; + TEXT CHARACTER= 70 TAXON=15 TEXT='The wrinkles [@Zhang2015, fig. 1] are not dissimilar to the pleats of certain loriciferans, so are interpreted as denoting cuticular reinforcement'; + TEXT CHARACTER= 70 TAXON=18 TEXT='Cuticularized bars occur on the proximal mouth cone, preceding each oral furca [@Neves2021po].'; + TEXT CHARACTER= 70 TAXON=22 TEXT='The flexible cuticle of the base of the mouth cone is divided into eight plates [@Gad2005za]'; + TEXT CHARACTER= 70 TAXON=24 TEXT='Well-developed ruff: "a cuticular ring with fibres arising from eight points" [@Fujimoto2020mb]'; + TEXT CHARACTER= 70 TAXON=34 TEXT='Conceivably represented by the cuticular sheath [@Neuhaus2015z], whose ridges are akin to those observed in the basal ring of loriciferans.'; + TEXT CHARACTER= 70 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 71 TAXON=18 TEXT='Eight in Nanaloricidae [@Neves2016za]'; + TEXT CHARACTER= 71 TAXON=19 TEXT='Eight in Nanaloricidae [@Neves2016za]'; + TEXT CHARACTER= 71 TAXON=20 TEXT='Eight in Nanaloricidae [@Neves2016za]'; + TEXT CHARACTER= 71 TAXON=24 TEXT='Eight [@Fujimoto2020mb]'; + TEXT CHARACTER= 71 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 71 TAXON=120 TEXT='Cuticular folds akin to oral ridges [@ThisStudy] are likely taphonomic. Coded ambiguous.'; + TEXT CHARACTER= 72 TAXON=18 TEXT='Eight in Nanaloricidae [@Neves2016za]'; + TEXT CHARACTER= 72 TAXON=19 TEXT='Eight in Nanaloricidae [@Neves2016za]'; + TEXT CHARACTER= 72 TAXON=20 TEXT='Eight [@Heiner2007hmr]'; + TEXT CHARACTER= 72 TAXON=24 TEXT='Eight [@Fujimoto2020mb]'; + TEXT CHARACTER= 72 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 73 TAXON=18 TEXT='Present in Nanaloricidae only [@Neves2016za]'; + TEXT CHARACTER= 73 TAXON=19 TEXT='Present in Nanaloricidae only [@Neves2016za]'; + TEXT CHARACTER= 73 TAXON=20 TEXT='No sclerotized furcae [@Heiner2007hmr]'; + TEXT CHARACTER= 73 TAXON=21 TEXT='Present in Nanaloricidae only [@Neves2016za]'; + TEXT CHARACTER= 73 TAXON=22 TEXT='Present in Nanaloricidae only [@Neves2016za]'; + TEXT CHARACTER= 73 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 74 TAXON=20 TEXT='Two different lengths [@Heiner2007hmr]'; + TEXT CHARACTER= 74 TAXON=24 TEXT='Anterior tips of ridges are not attached to mouth cone [@Fujimoto2020mb]'; + TEXT CHARACTER= 74 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 75 TAXON=80 TEXT='Fenestrated cuticle [@EibyeJacobsen2001za, fig. 12; @Dewel2006, fig. 11]'; + TEXT CHARACTER= 75 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 76 TAXON=7 TEXT='No armature visible [@ConwayMorris1977], perhaps due to non-eversion of pharynx'; + TEXT CHARACTER= 76 TAXON=8 TEXT='Carbon-rich preservation in everted component of phayrnx in GSC 45331 suggests armature; inverted component seemingly armed too, but difficult to determine with confidence'; + TEXT CHARACTER= 76 TAXON=11 TEXT='Present on inverted pharynx [@Zhang2022]^n'; + TEXT CHARACTER= 76 TAXON=22 TEXT='The distal mouth cone contains four oral stylets; three rows of placoids adorn the pharyngeal bulb [@Gad2005za]'; + TEXT CHARACTER= 76 TAXON=24 TEXT='Internal armature present in adults; presence of stylets equivocal [@Fujimoto2020mb]'; + TEXT CHARACTER= 76 TAXON=41 TEXT='Unarmed [@Keppner1988tams; @Leduc2016n]'; + TEXT CHARACTER= 76 TAXON=42 TEXT='Not visible in drawings of @Reiman1972, but present in close relative Onchulus [@Swart1993]'; + TEXT CHARACTER= 76 TAXON=43 TEXT='Coded as unarmed. The teeth and denticles within the buccal cavity [@Borgonie1995] are outgrowths of three plates, one of which corresponds to the dorsal tooth; hence these seem not to represent equivalents of the Zone III pharyngeal teeth.'; + TEXT CHARACTER= 76 TAXON=73 TEXT='@Dewel2006'; + TEXT CHARACTER= 76 TAXON=80 TEXT='@Dewel2006'; + TEXT CHARACTER= 76 TAXON=93 TEXT='Posterior band of small teeth [@Guidetti2012]'; + TEXT CHARACTER= 76 TAXON=94 TEXT='Posterior band of small teeth [@Guidetti2012]'; + TEXT CHARACTER= 76 TAXON=103 TEXT='No armature preserved [@ConwayMorris1977]'; + TEXT CHARACTER= 76 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 76 TAXON=126 TEXT='Not displayed, but preservation of figured material [@Whittington1975] inadequate to exclude internal pharyngeal structures'; + TEXT CHARACTER= 76 TAXON=129 TEXT='Impossible to rule out the presence of pharyngeal teeth based on available material'; + TEXT CHARACTER= 76 TAXON=131 TEXT='Not possible to rule out the presence of hallucigeniid-like aciculae based on available material'; + TEXT CHARACTER= 76 TAXON=143 TEXT='Present [@Caron2017]'; + TEXT CHARACTER= 76 TAXON=157 TEXT='Present [@Vinther2016]'; + TEXT CHARACTER= 76 TAXON=166 TEXT='Preservation insufficient to evaluate [@Cong2017]'; + TEXT CHARACTER= 76 TAXON=168 TEXT='Present [@Moysiuk2019]'; + TEXT CHARACTER= 77 TAXON=40 TEXT='Most denticles are expressed as individual cusps expressed on outgrowths of the pharynx [@Kulikov1998rjn]'; + TEXT CHARACTER= 77 TAXON=44 TEXT='Multiple cusps certainly in distal teeth, if possibly not in proximal teeth [@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 77 TAXON=110 TEXT='Seemingly simple scalids [@Hu2012]'; + TEXT CHARACTER= 77 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 77 TAXON=119 TEXT='Seemingly multicuspate [@Yang2020], cf. Selkirkia'; + TEXT CHARACTER= 77 TAXON=143 TEXT='Short spines [@Caron2017]'; + TEXT CHARACTER= 77 TAXON=153 TEXT='From @Vannier2014 supplementary figure 6c, the pharyngeal teeth appear multicupsate, although only few are preserved well.'; + TEXT CHARACTER= 77 TAXON=157 TEXT='Multiple cusps inferred based on similarity to Omnidens [@Vinther2016]'; + TEXT CHARACTER= 77 TAXON=168 TEXT='Multiple cusps [@Moysiuk2019]'; + TEXT CHARACTER= 78 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 79 TAXON=15 TEXT='Hollow elements Short gap of wrinkled cuticle [@Zhang2015, fig. 1]'; + TEXT CHARACTER= 79 TAXON=38 TEXT='Small cavity present in Paragordius [@Jochmann2007]'; + TEXT CHARACTER= 79 TAXON=97 TEXT='Seeminhly hollow [@Howard2022, fig. 1c]'; + TEXT CHARACTER= 79 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 79 TAXON=143 TEXT='No evidence of void, but preservation consistent with central cavity [@Caron2017]'; + TEXT CHARACTER= 79 TAXON=158 TEXT='Cavity seems likely based on sediment-like infilling [@Li2024]'; + TEXT CHARACTER= 80 TAXON=38 TEXT='Two bilateral series, with diminutive third on lateral extension'; + TEXT CHARACTER= 80 TAXON=40 TEXT='Four approximate series [@Inglis1999bbmnh]'; + TEXT CHARACTER= 80 TAXON=91 TEXT='@Kristensen1982'; + TEXT CHARACTER= 80 TAXON=92 TEXT='Strong bilateral symmetry, particularly in row III, with a gap between bilateral series [@Michalczyk2003]'; + TEXT CHARACTER= 80 TAXON=93 TEXT='Not prominent in ''Row II'', but clearly present in ''Row III''; @Kihm20203, fig. 1F'; + TEXT CHARACTER= 80 TAXON=97 TEXT='Triradial disposition [@Howard2020]'; + TEXT CHARACTER= 80 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 80 TAXON=143 TEXT='Disordered [@Caron2017]'; + TEXT CHARACTER= 81 TAXON=97 TEXT='Three series'; + TEXT CHARACTER= 81 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 81 TAXON=143 TEXT='Distribution unclear; possibly uniform around pharynx [@Caron2017], but coded as ambiguous'; + TEXT CHARACTER= 81 TAXON=157 TEXT='Uniformly distributed [@Vinther2016]'; + TEXT CHARACTER= 81 TAXON=168 TEXT='Four series [@Moysiuk2019]'; + TEXT CHARACTER= 82 TAXON=15 TEXT='At least two [@Zhang2015]'; + TEXT CHARACTER= 82 TAXON=16 TEXT='More than one'; + TEXT CHARACTER= 82 TAXON=20 TEXT='Six oral stylets [@Neves2016zab]'; + TEXT CHARACTER= 82 TAXON=22 TEXT='Oral stylets + three rows of placoids in bulb [@Gad2005za]'; + TEXT CHARACTER= 82 TAXON=40 TEXT='Four circlets identified [@Inglis1969bbmnh]'; + TEXT CHARACTER= 82 TAXON=81 TEXT='Three in Band II, plus transverse crests (= Band III), in Bertolanius volubilis (Eohypsibiidae) [@Guidetti2015]'; + TEXT CHARACTER= 82 TAXON=92 TEXT='Three circlets (?) in Band II; one in Band III [@Michalczyk2003, fig 57a]'; + TEXT CHARACTER= 82 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 82 TAXON=158 TEXT='State ''four to six'' to denote limited number of rows, potentially variable based on specimen size, in contrast to strict number of four observed in other taxa.'; + TEXT CHARACTER= 83 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 84 TAXON=16 TEXT='Eighteen [@Liu2014]'; + TEXT CHARACTER= 84 TAXON=20 TEXT='Six oral stylets in S. neuhausi [@Neves2016zab]'; + TEXT CHARACTER= 84 TAXON=24 TEXT='Six in Higgins larva; undetermined in adults [@Fujimoto2020mb]'; + TEXT CHARACTER= 84 TAXON=44 TEXT='Five [@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 84 TAXON=110 TEXT='Haphazard distribution [@Hu2012]'; + TEXT CHARACTER= 84 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 84 TAXON=119 TEXT='Seemingly 10 [@Yang2020]'; + TEXT CHARACTER= 84 TAXON=121 TEXT='Each circlet in CWM360 [@Maas2007ppp fig. 5B] and ) ELI-000-1402 [@Vannier2017, fig. 3d] contains six visible elements for a total of twelve.'; + TEXT CHARACTER= 85 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 85 TAXON=119 TEXT='Five visible on upper surface, for a total of ten [@Yang2020, fig. 2h]'; + TEXT CHARACTER= 86 TAXON=33 TEXT='Dorsal style reduced [@BauerNebelsick1995]'; + TEXT CHARACTER= 86 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 87 TAXON=108 TEXT='Inferred from seemingly quincunxial distribution [@ConwayMorris2010]'; + TEXT CHARACTER= 87 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 88 TAXON=15 TEXT='Single spine [@Zhang2015]'; + TEXT CHARACTER= 88 TAXON=38 TEXT='@Bolek2010; @Szmygiel2014'; + TEXT CHARACTER= 88 TAXON=44 TEXT='Cuspidate teeth [@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 88 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 88 TAXON=119 TEXT='Broad triangle, probably with secondary elements, but no prominent central spine [@Yang2020]'; + TEXT CHARACTER= 88 TAXON=122 TEXT='Seemingly a denticulate triangular arch, resembling Selkirkia teeth [@Smith2015]'; + TEXT CHARACTER= 88 TAXON=169 TEXT='Multiple cusps [@Daley2013]'; + TEXT CHARACTER= 89 TAXON=34 TEXT='Somewhat recurved [@Neuhaus2015z, fig. 13C]'; + TEXT CHARACTER= 89 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 90 TAXON=16 TEXT='Grooved, but not seemingly producing additional spines [@Liu2014]'; + TEXT CHARACTER= 90 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 90 TAXON=143 TEXT='Simple rods or spines [@Caron2017]'; + TEXT CHARACTER= 91 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 92 TAXON=33 TEXT='Large [@BauerNebelsick1995]'; + TEXT CHARACTER= 92 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 93 TAXON=40 TEXT='Two large denticles, without smaller denticles [@Inglis1969bbmnh]'; + TEXT CHARACTER= 93 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 94 TAXON=81 TEXT='Modest ring fold in Bertolanius volubilis (Eohypsibiidae) [@Guidetti2015]'; + TEXT CHARACTER= 94 TAXON=93 TEXT='Not evident [@Kihm20203, fig. 1F]'; + TEXT CHARACTER= 94 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 95 TAXON=22 TEXT='The extended gap between the oral stylets and the placoids is interpreted as denoting the absence of the middle circlets.'; + TEXT CHARACTER= 95 TAXON=44 TEXT='We score the apparent gap between the two regions of teeth [@SchmidtRhaesa2022za] as denoting the reduction of the middle circlets'; + TEXT CHARACTER= 95 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 96 TAXON=28 TEXT='Elongate spine [@Rucci2020z]'; + TEXT CHARACTER= 96 TAXON=33 TEXT='With pectinate fringe [@BauerNebelsick1995]'; + TEXT CHARACTER= 96 TAXON=110 TEXT='Seemingly simple [@Hu2012]'; + TEXT CHARACTER= 96 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 96 TAXON=121 TEXT='Prominent central spine with additional lateral elements indicated by footprint in @Vannier2017, fig. 3d'; + TEXT CHARACTER= 97 TAXON=18 TEXT='No placoids in '; + TEXT CHARACTER= 97 TAXON=40 TEXT='larger and basally fused [@Inglis1969bbmnh]'; + TEXT CHARACTER= 97 TAXON=81 TEXT='In Bertolanius volubilis (Eohypsibiidae) [@Guidetti2015]'; + TEXT CHARACTER= 97 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 97 TAXON=120 TEXT='No indication of variability in morphology [@ThisStudy]'; + TEXT CHARACTER= 97 TAXON=121 TEXT='Possibly larger, but no clear differentiated field'; + TEXT CHARACTER= 97 TAXON=122 TEXT='Distal teeth more conical and elongate [@Shi2022, fig. 3a]'; + TEXT CHARACTER= 97 TAXON=123 TEXT='Distal region likely not exposed in available material [@Han2007pr]'; + TEXT CHARACTER= 98 TAXON=24 TEXT='Seemingly acicular [@Fujimoto2020mb]'; + TEXT CHARACTER= 98 TAXON=81 TEXT='In Bertolanius volubilis (Eohypsibiidae) [@Guidetti2015]'; + TEXT CHARACTER= 98 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 99 TAXON=22 TEXT='Placoids of first row larger than subsequent rows [@Gad2005za]'; + TEXT CHARACTER= 99 TAXON=42 TEXT='Insufficient circlets (in Onchulus) to discriminate a dorsal region'; + TEXT CHARACTER= 99 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 100 TAXON=28 TEXT='Absent [@Herranz2021z]'; + TEXT CHARACTER= 100 TAXON=29 TEXT='Present [@Herranz2021z]'; + TEXT CHARACTER= 100 TAXON=31 TEXT='Present [@Herranz2021z]'; + TEXT CHARACTER= 100 TAXON=33 TEXT='Present [@Herranz2021z]'; + TEXT CHARACTER= 100 TAXON=34 TEXT='Absent [@Herranz2021z]'; + TEXT CHARACTER= 101 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 102 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 102 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 102 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 102 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 102 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 102 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 102 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 103 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 103 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 103 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 103 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 103 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 103 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 103 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 103 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 103 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 104 TAXON=40 TEXT='Not prominently reinforced [@Kulikov1998rjn]'; + TEXT CHARACTER= 104 TAXON=43 TEXT='The three plates in the buccal cavity [@Borgone1995] are treated as possible developments of reinforced pharyngeal cuticle'; + TEXT CHARACTER= 104 TAXON=97 TEXT='Three reinforced ridges [@Howard2020]'; + TEXT CHARACTER= 104 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 105 TAXON=89 TEXT='After @Kihm2023, noting that @Mapalo2024cb score D. macrodon as lacking a dorsal apophysis'; + TEXT CHARACTER= 105 TAXON=92 TEXT='After @Kihm2023, noting that @Mapalo2024cb score M. hufelandi as lacking a dorsal apophysis'; + TEXT CHARACTER= 105 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 106 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 107 TAXON=8 TEXT='Seemingly present in inverted pharynx of GSC 45331'; + TEXT CHARACTER= 107 TAXON=18 TEXT='A large pharyngeal bulb characterizes the Nanaloricidae [@Kristensen2004cbm]'; + TEXT CHARACTER= 107 TAXON=19 TEXT='A large pharyngeal bulb characterizes the Nanaloricidae [@Kristensen2004cbm]'; + TEXT CHARACTER= 107 TAXON=20 TEXT='Present [@Neves2016za]'; + TEXT CHARACTER= 107 TAXON=22 TEXT='A small pharyngeal bulb occurs within the mouth cone [@Gad2005az]'; + TEXT CHARACTER= 107 TAXON=43 TEXT='Absent [@Borgonie1995]'; + TEXT CHARACTER= 107 TAXON=95 TEXT='No clear evidence for terminal bulb [@Maas2007ppp]'; + TEXT CHARACTER= 107 TAXON=108 TEXT='No obvious evidence pertaining to the presence of this structure [@ConwayMorris2010]'; + TEXT CHARACTER= 107 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 107 TAXON=121 TEXT='Visible in CWM360 [@Maas2007ppp fig. 5b]'; + TEXT CHARACTER= 107 TAXON=131 TEXT='Depends on interpretation of pharyngeal bulb [@Strausfeld2022]'; + TEXT CHARACTER= 108 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 109 TAXON=28 TEXT='Cylindrical neck without placids [@Herranz2021z]'; + TEXT CHARACTER= 109 TAXON=34 TEXT='Cateria gerlachi does, contra @Sorensen2015, exhibit a neck with 12 placids [@Neuhaus2015z]; a neck and closing apparatus is absent in C. styx [@Herranz2019]'; + TEXT CHARACTER= 109 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 110 TAXON=33 TEXT='As the spinoscalids are too long to be fully withdrawn, the neck does not function as a closing apparatus [@Herranz2021z]'; + TEXT CHARACTER= 110 TAXON=34 TEXT='Cateria gerlachi does, contra @Sorensen2015, exhibit a neck with 12 placids [@Neuhaus2015z]; a neck and closing apparatus is absent in C. styx [@Herranz2019]. Nonetheless, as the spinoscalids are too long to be fully withdrawn, the neck does not function as a closing apparatus [@Herranz2021z]'; + TEXT CHARACTER= 110 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 111 TAXON=28 TEXT='Radial closing apparatus [@Herranz2021z]'; + TEXT CHARACTER= 111 TAXON=33 TEXT='Radial [@Herranz2021z]'; + TEXT CHARACTER= 111 TAXON=34 TEXT='Slight bilateral symmetry produced by narrow dorsal placid [@Neuhaus2015z], but considered radial [@Herranz2021z]'; + TEXT CHARACTER= 111 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 112 TAXON=34 TEXT='Twelve [@Neuhaus2015z]'; + TEXT CHARACTER= 112 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 113 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 114 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 116 TAXON=40 TEXT='Slit-like [@Kulikov1998rjn]'; + TEXT CHARACTER= 116 TAXON=41 TEXT='Round [@Leduc2016n]'; + TEXT CHARACTER= 116 TAXON=42 TEXT='Broad, pocket-like [@Riemann1972]'; + TEXT CHARACTER= 116 TAXON=43 TEXT='Amphids ''cup-shaped'' [@Peneva1999n]'; + TEXT CHARACTER= 117 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 117 TAXON=152 TEXT='Present [@Smith2023n]'; + TEXT CHARACTER= 117 TAXON=156 TEXT='Present [@Park2018]'; + TEXT CHARACTER= 117 TAXON=157 TEXT='Coded ambiguous as the character of the anterior protrusion between the appendages of Pambdelurion [e.g. @Vinther2016, fig. 1] is uncertain: this may be a manifestation of the oral apparatus, or may be a Kerygmachela-like lobe, as perhaps suggested by the anterior-directed filaments [@Vinther2016, fig. 3], which conceivably correspond to dorsal cirri.'; + TEXT CHARACTER= 117 TAXON=160 TEXT='We interpret an anterior lobe as underlying the medial sclerite of Kylinxia [@Dhungana2021]'; + TEXT CHARACTER= 117 TAXON=173 TEXT='Covered by dorsal sclerite'; + TEXT CHARACTER= 118 TAXON=69 TEXT='Coded as absent as a single dorsal sclerite covers the entire body; this structure does not seem to correspond directly to the anterior sclerites of other taxa [@Boesgaard2001]'; + TEXT CHARACTER= 118 TAXON=73 TEXT='We code this as '; + TEXT CHARACTER= 118 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 118 TAXON=131 TEXT='The rigid, ridged ''carapace'' [@Strausfeld2022] seemingly denotes a sclerotization of the dorsal head region; its margin displays relief and a consistent shape across specimens [@Liu2014]'; + TEXT CHARACTER= 118 TAXON=133 TEXT='@Liu2008app, in fig 2A4-5, suggest that the anterior region is sclerotized, although preservation shows irregular margins therefore more specimens are needed to confirm presence.'; + TEXT CHARACTER= 118 TAXON=139 TEXT='The head region of H. fortis displays a similar shape, medial ridge and doublure to that of Cardiodictyon; it is notably darker (= more heavily sclerotized?) in some specimens [e.g. ELI-JS0013; @Liu2014ppp]'; + TEXT CHARACTER= 118 TAXON=141 TEXT='Head sclerite absent [@Howard2020]'; + TEXT CHARACTER= 118 TAXON=143 TEXT='Absent [@Caron2017]'; + TEXT CHARACTER= 118 TAXON=152 TEXT='No evidence of incipient sclerotization [@Smith2023n]'; + TEXT CHARACTER= 118 TAXON=158 TEXT='Likely, but not certain [@Li2024]'; + TEXT CHARACTER= 118 TAXON=163 TEXT='Present [@Dhungana2021]'; + TEXT CHARACTER= 118 TAXON=166 TEXT='Central oval head shield present [@Cong2017]'; + TEXT CHARACTER= 119 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 120 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 120 TAXON=162 TEXT='Prominently anterior [@Moysiuk2022]'; + TEXT CHARACTER= 120 TAXON=163 TEXT='Dorsal sclerite present [@Dhungana2021]'; + TEXT CHARACTER= 121 TAXON=73 TEXT='We do not code for the presence of the dorsal sclerite in certain heterotardigrades (contra @Khim2023, as those cephalic sclerites are always present when trunk sclerites are present, and therefore unlikely to be homologous to the euarthropod dorsal/anterior sclerite'; + TEXT CHARACTER= 121 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 121 TAXON=166 TEXT='Oval [@Cong2017]'; + TEXT CHARACTER= 122 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 122 TAXON=166 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 122 TAXON=167 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 122 TAXON=168 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 122 TAXON=169 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 122 TAXON=171 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 122 TAXON=172 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 122 TAXON=173 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 122 TAXON=174 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 123 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 124 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 124 TAXON=157 TEXT='No indication of lateral sclerites in @Young2017 or @Budd1998ar'; + TEXT CHARACTER= 124 TAXON=166 TEXT='Prominent ovoid structures adjacent to the frontal appendage are interpreted as P-elements, connected by a rod [@Cong2017]'; + TEXT CHARACTER= 124 TAXON=167 TEXT='Present [@Moysiuk2019]'; + TEXT CHARACTER= 124 TAXON=171 TEXT='Present [@Moysiuk2019]'; + TEXT CHARACTER= 124 TAXON=173 TEXT='Present [@Moysiuk2019]'; + TEXT CHARACTER= 124 TAXON=174 TEXT='@Moysiuk2019 interpret the ventrolateral plate-like elements as lateral sclerites'; + TEXT CHARACTER= 125 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 125 TAXON=166 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 125 TAXON=167 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 125 TAXON=168 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 125 TAXON=169 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 125 TAXON=171 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 125 TAXON=172 TEXT='The dorsal sclerite of Aegirocassis [@VanRoy2015] is neither resembles the cub-circular Anomalocaris-type sclerite nor as elongate as e.g., Hurdia. Therefore we code this character as ambiguous.'; + TEXT CHARACTER= 125 TAXON=173 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 125 TAXON=174 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 126 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 126 TAXON=166 TEXT='Following @Budd2021'; + TEXT CHARACTER= 126 TAXON=167 TEXT='Following @Budd2021'; + TEXT CHARACTER= 126 TAXON=168 TEXT='No intermediate plate between p-elements observed [@Moysiuk2019]'; + TEXT CHARACTER= 126 TAXON=169 TEXT='Following @Budd2021'; + TEXT CHARACTER= 126 TAXON=171 TEXT='Following @Budd2021'; + TEXT CHARACTER= 126 TAXON=177 TEXT='Following @Budd2021'; + TEXT CHARACTER= 127 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 127 TAXON=143 TEXT='Interpreted as flexible [@Caron2017]'; + TEXT CHARACTER= 128 TAXON=95 TEXT='Expanded introvert, giving dumbbell shaped appearance [@Maas2007ppp], is not treated as equivalent to the condition described in lobopodians.'; + TEXT CHARACTER= 128 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 128 TAXON=129 TEXT='The ''head'' of @Ou2018 is interpreted as a cross-section through the folded trunk; see comments on introvert.'; + TEXT CHARACTER= 128 TAXON=141 TEXT='No swelling [@Howard2020]'; + TEXT CHARACTER= 129 TAXON=55 TEXT='Coded as present based on innervation data that suggests that heterotardigrade anterior cephalic structures are homologous to sensory fields in eutardigrades [@Gross2021]'; + TEXT CHARACTER= 129 TAXON=78 TEXT='Coded as present based on innervation data that suggests that heterotardigrade anterior cephalic structures are homologous to sensory fields in eutardigrades [@Gross2021]'; + TEXT CHARACTER= 129 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 129 TAXON=124 TEXT='Inadequately preserved for confident scoring'; + TEXT CHARACTER= 129 TAXON=126 TEXT='There are no obvious equivalents to the anterior paired projections [@Whittington1975]'; + TEXT CHARACTER= 129 TAXON=127 TEXT='Inadequately preserved for confident scoring'; + TEXT CHARACTER= 129 TAXON=128 TEXT='Unannulated, narrow antenniform structures [@Liu2008app, fig. 3D] are interpreted as potential homologues.'; + TEXT CHARACTER= 129 TAXON=129 TEXT='No obvious candidates present in the complete specimen [@Ma2014]'; + TEXT CHARACTER= 129 TAXON=131 TEXT='By analogy with Hallucigenia and Microdictyon, the anterior appendages [@Strausfeld2022] are all treated as trunk appendages; this is consistent with their uniform position and shape'; + TEXT CHARACTER= 129 TAXON=133 TEXT='Ambiguous: although described as absent by @Liu2008app, we do not consider the limited available material sufficient to definitively rule out the presence of these features.'; + TEXT CHARACTER= 129 TAXON=141 TEXT='Ambiguous [@Howard2020]'; + TEXT CHARACTER= 129 TAXON=143 TEXT='Figure 1H from @Caron2017 shows a possible anterior projection. More detailed head anatomy is needed to be certain of this feature.'; + TEXT CHARACTER= 129 TAXON=145 TEXT='Present [@Caron2020]'; + TEXT CHARACTER= 129 TAXON=148 TEXT='The possibility that one set of antenniform appendages corresponds to enlarged frontal filaments is enticing but difficult to test.'; + TEXT CHARACTER= 129 TAXON=152 TEXT='Dorsal filaments treated as potential homologues [@Smith2023n]'; + TEXT CHARACTER= 129 TAXON=153 TEXT='See Figure 1e from @Vannier2014'; + TEXT CHARACTER= 129 TAXON=154 TEXT='Inadequately preserved for confident scoring'; + TEXT CHARACTER= 129 TAXON=156 TEXT='Interpreted as present by @Ortega2016asd. See rostral spines in supplementary figure 8 from @Park2018.'; + TEXT CHARACTER= 129 TAXON=157 TEXT='Interpreted as present by @Ortega2016asd'; + TEXT CHARACTER= 129 TAXON=160 TEXT='Difficult to demonstrate absence based on available material [@Zeng2020]'; + TEXT CHARACTER= 129 TAXON=162 TEXT='We consider the structures interpreted as "filament-like anterior nerves" [@Moysiuk2022, e.g. fig. 3a] as potential homologues of the frontal filaments '; + TEXT CHARACTER= 129 TAXON=167 TEXT='Coded ambiguous; although the head is known from many articulated specimens [@Daley2014], the disposition of large sclerotized head elements leaves the absence of cirri difficult to conclusively demonstrate.'; + TEXT CHARACTER= 129 TAXON=168 TEXT='Ambiguous; head obscured by carapaces [@Moysiuk2019]'; + TEXT CHARACTER= 129 TAXON=169 TEXT='Ambiguous; head obscured by carapaces [@Daley2013jsp]'; + TEXT CHARACTER= 129 TAXON=171 TEXT='Ambiguities in head region [@Budd2021] mean the absence of these features cannot be determined with confidence'; + TEXT CHARACTER= 129 TAXON=173 TEXT='Considered ambiguous due to position of head sclerite and difficulty in interpreting head outline in available material [@Cong2014; @Cong2016; @Liu2018nsr]'; + TEXT CHARACTER= 129 TAXON=175 TEXT='Coded ambiguous, as anterior region comprises sclerotized segments.'; + TEXT CHARACTER= 129 TAXON=176 TEXT='Frontal filaments present [@Budd2021]'; + TEXT CHARACTER= 129 TAXON=177 TEXT='Considered ambiguous in megacheirans by @Ortega2016asd'; + TEXT CHARACTER= 129 TAXON=178 TEXT='Considered ambiguous in megacheirans by @Ortega2016asd'; + TEXT CHARACTER= 129 TAXON=179 TEXT='Coded ambiguous, as anterior region comprises sclerotized segments.'; + TEXT CHARACTER= 129 TAXON=180 TEXT='Coded ambiguous, as anterior region comprises sclerotized segments.'; + TEXT CHARACTER= 130 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 131 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 132 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 133 TAXON=1 TEXT='Preservation and larval status inadequate to establish'; + TEXT CHARACTER= 133 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 134 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 134 TAXON=128 TEXT='Present [cf. @Ou2012]'; + TEXT CHARACTER= 134 TAXON=131 TEXT='Bears a single pair of eyespots [@Liu2014ppp]'; + TEXT CHARACTER= 134 TAXON=132 TEXT='Coded ambiguous. A dark structure occurs in a location equivalent to the ocellus of Hallucigenia sparsa in ELRC 30060 [@Chen1995bnmns, pl. 6 fig. 2]; reexamination of fossil material is necessary before the absence of ocelli can be categorically confirmed.'; + TEXT CHARACTER= 134 TAXON=138 TEXT='Ocelli [@Smith2015]'; + TEXT CHARACTER= 134 TAXON=139 TEXT='We follow @Liu2014ppp in recognizing a single pair of eyespots. The various carbonaceous regions and pigmented patches [@Ma2012asd] likely represent a degraded but originally continuous carbon film.'; + TEXT CHARACTER= 134 TAXON=141 TEXT='Pair of simple ocellus-like eyes [@Howard2020]'; + TEXT CHARACTER= 134 TAXON=142 TEXT='Pit-type eyes [per @Smith2015, char. 18]'; + TEXT CHARACTER= 134 TAXON=143 TEXT='Sessile ocellus-type eyes [@Caron2017]'; + TEXT CHARACTER= 134 TAXON=156 TEXT='Compound, following @Park2018'; + TEXT CHARACTER= 134 TAXON=157 TEXT='Coded ambiguous: the dorsal surface of Pambdelurion is poorly known [@Budd1998ar]'; + TEXT CHARACTER= 134 TAXON=166 TEXT='Stalked eyes present [@Cong2017]'; + TEXT CHARACTER= 134 TAXON=168 TEXT='@Moysiuk2019'; + TEXT CHARACTER= 134 TAXON=179 TEXT='Reduced [@Mayers2019]'; + TEXT CHARACTER= 135 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 135 TAXON=138 TEXT='Two [@Smith2015]'; + TEXT CHARACTER= 135 TAXON=139 TEXT='We follow @Liu2014ppp in recognizing a single pair of eyespots. In our view the various carbonaceous regions and pigmented patches [@Ma2012asd] likely represent a degraded but originally continuous carbon film.'; + TEXT CHARACTER= 135 TAXON=141 TEXT='Pair of simple ocellus-like eyes [@Howard2020]'; + TEXT CHARACTER= 135 TAXON=160 TEXT='Four [@Dhungana2021]'; + TEXT CHARACTER= 135 TAXON=161 TEXT='A large pair of compound eyes is present [@Schoenemann2011; @Fu2011], but we consider it possible that a small pair of medial ocelli, if present, would be impossible to recognize in the preserved fossil material, so we conservatively code this taxon as ambiguous.'; + TEXT CHARACTER= 135 TAXON=162 TEXT='Two. We consider the "third eye" of @Moysiuk2022 to correspond to nervous tissue in an anterior lobe.'; + TEXT CHARACTER= 135 TAXON=163 TEXT='Four [@Dhungana2021]'; + TEXT CHARACTER= 135 TAXON=164 TEXT='Ambiguous. Possible eyes interpreted by @Pates2022'; + TEXT CHARACTER= 135 TAXON=166 TEXT='Structures interpreted as eyes are not, so this remains ambiguous [@Cong2017]'; + TEXT CHARACTER= 135 TAXON=175 TEXT='Two. Only two eyes have been described [@Yang2013]; we have been unable to substantiate the view of @Lan2021 that fuxianhuiids exhibit medial ocelli in addition to their lateral compound eyes.'; + TEXT CHARACTER= 135 TAXON=176 TEXT='Though @Lan2021 contend that fuxianhuiids exhibit medial ocelli in addition to their lateral compound eyes, @Ma2012n interpret putative medial eyes as lateral extensions of the rostrum.'; + TEXT CHARACTER= 135 TAXON=177 TEXT='Sideward pair and forward pair [@Lan2021]'; + TEXT CHARACTER= 135 TAXON=178 TEXT='Sideward pair and forward pair [@Lan2021]'; + TEXT CHARACTER= 135 TAXON=179 TEXT='Eyes are secondarily lost in Misszhouia and other naraoiids [@Mayers2019]'; + TEXT CHARACTER= 136 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 136 TAXON=156 TEXT='Present, following @Park2018'; + TEXT CHARACTER= 136 TAXON=157 TEXT='Coded ambiguous: the dorsal surface of Pambdelurion is poorly known [@Budd1998ar]'; + TEXT CHARACTER= 136 TAXON=166 TEXT='Stalked eyes presumed compound [@Cong2017]'; + TEXT CHARACTER= 136 TAXON=168 TEXT='@Moysiuk2019'; + TEXT CHARACTER= 136 TAXON=179 TEXT='Eyes are secondarily lost in Misszhouia and other naraoiids [@Mayers2019]'; + TEXT CHARACTER= 137 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 137 TAXON=138 TEXT='Sessile [@Smith2015]'; + TEXT CHARACTER= 137 TAXON=156 TEXT='Sessile, following @Park2018'; + TEXT CHARACTER= 137 TAXON=157 TEXT='Whether or not eyes are present, available specimens clearly demonstrate the absence of an eye stalk [@Budd1998ar; @Young2017]'; + TEXT CHARACTER= 137 TAXON=179 TEXT='Eyes are secondarily lost in Misszhouia and other naraoiids [@Mayers2019]'; + TEXT CHARACTER= 138 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 138 TAXON=156 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=163 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=167 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=168 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=169 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=171 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=173 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=174 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=175 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=176 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=177 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=178 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 138 TAXON=179 TEXT='Eyes are secondarily lost in Misszhouia and other naraoiids [@Mayers2019]'; + TEXT CHARACTER= 138 TAXON=180 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 139 TAXON=55 TEXT='The sclerotized stylets and stylet supports of tardigrades are likely modified claws [see @Mobjerg2018], since the appendages have been reduced, we code this character as ambiguously. '; + TEXT CHARACTER= 139 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 139 TAXON=126 TEXT='The spines of Aysheaia are not likely to be sclerotized given they preserve similarly the trunk cuticle, and are not enriched in carbon (darker) like the claws. '; + TEXT CHARACTER= 139 TAXON=151 TEXT='The protocerebral appendage pair (assuming its modification to a stylet, as in modern tardigrades) cannot be directly observed.'; + TEXT CHARACTER= 139 TAXON=152 TEXT='Probably not sclerotized [@Smith2023n] - but coded conservatively'; + TEXT CHARACTER= 139 TAXON=163 TEXT='Opabinia''s protocerebral appendages are more robust than fully lobopodous appendages, and may have a single terminal sclerotized segment. We code as uncertain to allow for the possibility that this kind of hardened tip of the appendages are a precursor to sclerotized appendages of radiosdonts. '; + TEXT CHARACTER= 139 TAXON=178 TEXT='The presence of a hypostome is suggested, but not verified'; + TEXT CHARACTER= 140 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 140 TAXON=168 TEXT='Following @DeVivo2021'; + TEXT CHARACTER= 140 TAXON=169 TEXT='Following @DeVivo2021'; + TEXT CHARACTER= 140 TAXON=171 TEXT='Following @DeVivo2021'; + TEXT CHARACTER= 140 TAXON=175 TEXT='Absent, presumably secondarily, in the reduced labrum'; + TEXT CHARACTER= 140 TAXON=176 TEXT='Absent, presumably secondarily, in the reduced labrum'; + TEXT CHARACTER= 140 TAXON=177 TEXT='Absent, presumably secondarily, in the reduced labrum'; + TEXT CHARACTER= 140 TAXON=179 TEXT='Absent, presumably secondarily, in the reduced labrum'; + TEXT CHARACTER= 140 TAXON=180 TEXT='Absent, presumably secondarily, in the reduced labrum'; + TEXT CHARACTER= 141 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 141 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 141 TAXON=129 TEXT='No evidence of appendage differentiation [@Ma2014]; the candidate appendages presented by @Ou2018 are interpreted to be folds associated with flexure of the trunk.'; + TEXT CHARACTER= 141 TAXON=131 TEXT='Coded as ambiguous, as the detailed morphology of the head is unclear'; + TEXT CHARACTER= 141 TAXON=141 TEXT='Not differentiated [@Howard2020]'; + TEXT CHARACTER= 141 TAXON=142 TEXT='We interpret the antenniform structures [@Ma2009] as possible homologues to the frontal filaments rather than appendages. '; + TEXT CHARACTER= 141 TAXON=143 TEXT='Not evident [@Caron2017]'; + TEXT CHARACTER= 141 TAXON=144 TEXT='We code the anterior antennae-like structures [@Yang2015] as possible homologous of the frontal filaments. Hence the first pair of limbs are coded as undifferentiated.'; + TEXT CHARACTER= 141 TAXON=145 TEXT='The first pair of appendages are not differentiated [@Caron2020]. We code the anterior antennae-like structures as possible homologous of the frontal filaments. '; + TEXT CHARACTER= 141 TAXON=148 TEXT='The two anterior appendages may correspond to (i) the protocerebral trunk appendage plus an enlarged anterior filament; or (ii) the protocerebral and deuterocerebral trunk appendages. Under either interpretation, the protocerebral limb pair is distinct from the trunk appendages.'; + TEXT CHARACTER= 141 TAXON=158 TEXT='No evidence of trunk appendages, indicating distinct form (lobopodous?) if present [@Li2024]'; + TEXT CHARACTER= 142 TAXON=55 TEXT='The sclerotized stylets and stylet supports of tardigrades are likely modified claws [see @Mobjerg2018], since the appendages have been reduced, we code this character as ambiguously. '; + TEXT CHARACTER= 142 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 142 TAXON=152 TEXT='Conservatively coded as ambiguous to reflect possibility of later development of podomeres'; + TEXT CHARACTER= 142 TAXON=163 TEXT='We interpret the claws of Opabinia’s protocerebral appendage as podomerous [see @Whittington1975, figs 75 and 79], and thus the protocerebral appendage as sclerotized'; + TEXT CHARACTER= 143 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 143 TAXON=152 TEXT='Conservatively coded as ambiguous to reflect possibility of later development of podomeres'; + TEXT CHARACTER= 143 TAXON=163 TEXT='The basal podomeres are poorly preserved [@Dhungana2021] hence we code as ambiguous. '; + TEXT CHARACTER= 143 TAXON=165 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 143 TAXON=166 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 143 TAXON=167 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 143 TAXON=168 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 143 TAXON=169 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 143 TAXON=171 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 143 TAXON=172 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 143 TAXON=173 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 143 TAXON=174 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 144 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 144 TAXON=152 TEXT='Uncertain as adult morphology unknown; observed tapering [@Smith2023n] may be a developmental phenomenon.'; + TEXT CHARACTER= 144 TAXON=163 TEXT='We interpret the claws of Opabinia''s protocerebral appendage as podomerous [see @Whittington1975, figs 75, 79]. The distal three podomeres are differentiated, and could be homologous to the differentiation of distal podomeres of certain hurdiids, however, given that hurdiid distal podomeres taper in diameter, and Opabinia''s terminal podomere is the largest, we code as uncertain for this character.'; + TEXT CHARACTER= 144 TAXON=166 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 144 TAXON=167 TEXT='No significant change.'; + TEXT CHARACTER= 144 TAXON=168 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 144 TAXON=169 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 144 TAXON=171 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 144 TAXON=172 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 144 TAXON=173 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 144 TAXON=174 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 145 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 145 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 145 TAXON=149 TEXT='The first pair of appendages in Ilyodes are lateral [@Thompson1980; @Haug2012cb]'; + TEXT CHARACTER= 145 TAXON=152 TEXT='Ventrolateral – adult position uncertain'; + TEXT CHARACTER= 145 TAXON=157 TEXT='Ventral [@Budd1998ar]'; + TEXT CHARACTER= 146 TAXON=55 TEXT='As the mouth is terminal, and the appendages have been assumed to be incorporated into the mouth, we code that the frontal appendages have not shifted posteriorly.'; + TEXT CHARACTER= 146 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 147 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 147 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 147 TAXON=149 TEXT='The first pair of appendages in Ilyodes are not directly adjacent [@Thompson1980; @Haug2012cb]'; + TEXT CHARACTER= 147 TAXON=152 TEXT='Coded as uncertain as protocerebral appendages are known to migrate during development [@Budd2021]'; + TEXT CHARACTER= 147 TAXON=153 TEXT='The first pair of appendages in Megadictyon are not directly adjacent [@Liu2007az]^n'; + TEXT CHARACTER= 147 TAXON=154 TEXT='Jianshanopodia is coded uncertain due to unclear preservation [@Liu2006; @Liu2007az]'; + TEXT CHARACTER= 147 TAXON=156 TEXT='Not directly adjacent, but separated by anterior lobe [@Park2018]'; + TEXT CHARACTER= 147 TAXON=166 TEXT='Adjacent in better-articulated material, and thus presumably in life [@Cong2017]'; + TEXT CHARACTER= 147 TAXON=169 TEXT='Coded as ambiguous, as the well-developed dorsal cephalic plate in Hurdia and Aegirocassis obscures the base of the appendages [@Daley2009; @VanRoy2015].'; + TEXT CHARACTER= 147 TAXON=172 TEXT='Coded as ambiguous, as the well-developed dorsal cephalic plate in Hurdia and Aegirocassis obscures the base of the appendages [@Daley2009; @VanRoy2015].'; + TEXT CHARACTER= 148 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 148 TAXON=152 TEXT='Coded as uncertain as protocerebral appendages are known to migrate during development [@Budd2021]'; + TEXT CHARACTER= 148 TAXON=168 TEXT='Figure 2J in @Moysiuk2019 shows a prominent gap between appendages'; + TEXT CHARACTER= 149 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 149 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 149 TAXON=152 TEXT='Coded as uncertain as protocerebral appendages are known to migrate during development [@Budd2021]'; + TEXT CHARACTER= 150 TAXON=55 TEXT='In tardigrades, the presence of stylet glands, responsible for the moulting and production of stylet and stylet supports are likely transformed claw glands [@Mobjerg2018]. As such stylets and stylet supports are interpreted as modified claws [@Halberg2009; @Nielsen2001]. This homology is supported by the presence of microtubules in the epidermal cell attachments of exclusively the retractor muscles of claws and stylets in tardigrades [@Halberg2009, J. of Morphology].'; + TEXT CHARACTER= 150 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 150 TAXON=124 TEXT='Although protocerebral appendage is undifferentiated, claws are not preserved [@Dzik1989] and as such this cell is coded as ambiguous.'; + TEXT CHARACTER= 150 TAXON=126 TEXT='No claws on differentiated protocerebral appendages [see @Whittington1978]'; + TEXT CHARACTER= 150 TAXON=127 TEXT='Potential claws not preserved, therefore ambiguous [@Dzik2011]'; + TEXT CHARACTER= 150 TAXON=138 TEXT='Claws are absent in multiple anterior appendages, therefore coded as ambiguous (although posterior appendages are clawed).'; + TEXT CHARACTER= 150 TAXON=141 TEXT='Claws absent in multiple anterior appendages [@Howard2020], therefore coded as ambiguous (although posterior appendages are clawed) '; + TEXT CHARACTER= 150 TAXON=153 TEXT='@Liu2007az suggest claws present on differentiated protocerebral appendages of Megadictyon; these are figured by @Vannier2014'; + TEXT CHARACTER= 150 TAXON=154 TEXT='Not evident from incompletely preserved available material [@Liu2006; @Vannier2014]'; + TEXT CHARACTER= 150 TAXON=156 TEXT='Claws absent on protocerebral appendages [@Park2018, supplementary figure 3]'; + TEXT CHARACTER= 150 TAXON=157 TEXT='Following @Vinther2016 we code terminal claws on protocerebral appendages to be absent. The terminal structures are not well differentiated from the pointed outgrowths along the inner edge of the appendages and no terminal claw can be readily distinguished (see @Vinther2016, fig. 1; contra @Vannier2014).'; + TEXT CHARACTER= 150 TAXON=158 TEXT='Unclear whether spines are modified claws or separate elaborations, hence coded ambiguous, though presumably lost per Pambdelurion'; + TEXT CHARACTER= 150 TAXON=163 TEXT='Opabinia''s protocerebral spines are not homologous to lobopodian-style claws.'; + TEXT CHARACTER= 151 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 151 TAXON=128 TEXT='Spine series present on differentiated protocerebral appendage, therefore coded as present.'; + TEXT CHARACTER= 151 TAXON=149 TEXT='Coded as absent [@Haug2012cb]'; + TEXT CHARACTER= 151 TAXON=152 TEXT='Coded ambiguous as protocerebral appendages appear to be in an early developmental stage [@Smith2023n]; adult morphology is uncertain.'; + TEXT CHARACTER= 151 TAXON=156 TEXT='Present [@Budd1993; @Budd1998trse]'; + TEXT CHARACTER= 151 TAXON=157 TEXT='Present [@Budd1998ar]'; + TEXT CHARACTER= 151 TAXON=158 TEXT='Absent [@Li2024]'; + TEXT CHARACTER= 151 TAXON=163 TEXT='We code this as uncertain, as the present material on Opabinia''s frontal appendages does not allow for a clear assessment if lateral spines are present on the frontal appendages. '; + TEXT CHARACTER= 152 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 152 TAXON=152 TEXT='Coded ambiguous as protocerebral appendages appear to be in an early developmental stage [@Smith2023n]; adult morphology is uncertain.'; + TEXT CHARACTER= 152 TAXON=154 TEXT='@Li2024'; + TEXT CHARACTER= 152 TAXON=156 TEXT='@Li2024'; + TEXT CHARACTER= 152 TAXON=157 TEXT='@Li2024'; + TEXT CHARACTER= 152 TAXON=160 TEXT='Following @Zeng2020'; + TEXT CHARACTER= 152 TAXON=166 TEXT='Paired ventral endites are present on podomeres 2-9 only [@Daley2010]'; + TEXT CHARACTER= 152 TAXON=169 TEXT='Hurdiidae have one row [@Guo2019]'; + TEXT CHARACTER= 152 TAXON=171 TEXT='Hurdiidae have one row [@Guo2019]'; + TEXT CHARACTER= 152 TAXON=174 TEXT='Though previously coded and reconstructed as having two rows, we code to allow the possibility that Schinderhannes may only have one row [only one row clear in @Kuhl2009, Supplementary Fig S1A].'; + TEXT CHARACTER= 153 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 154 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 154 TAXON=152 TEXT='Coded ambiguous as protocerebral appendages appear to be in an early developmental stage [@Smith2023n]; adult morphology is uncertain.'; + TEXT CHARACTER= 155 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 155 TAXON=159 TEXT='The serrated margins of the main spines of Parapeytoia [@Hou1995gff] have been compared to megacheiran appendages (see @Budd2021). We conservatively code this character as ambiguous as the potential homology to accessory endite spines in radiodonts is unclear.'; + TEXT CHARACTER= 156 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 156 TAXON=153 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=154 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=156 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=157 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=160 TEXT='Alternating [@Zeng2020]'; + TEXT CHARACTER= 156 TAXON=166 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=167 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=168 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=169 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=171 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=172 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=173 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 156 TAXON=174 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 157 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 157 TAXON=160 TEXT='Narrower than podomere: Kylinxia is closer to the condition in anomalocaridids than in hurdiids [@Zeng2020]'; + TEXT CHARACTER= 157 TAXON=165 TEXT='As lateral spine (gnathal) series are not homologous to the ventral spine series of radiodonts [@Moysiuk2021], we code taxa with lateral spine series only as inapplicable.'; + TEXT CHARACTER= 158 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 158 TAXON=126 TEXT='Uniform length [@Whittington1975]'; + TEXT CHARACTER= 158 TAXON=166 TEXT='No increase'; + TEXT CHARACTER= 158 TAXON=167 TEXT='No increase'; + TEXT CHARACTER= 159 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 159 TAXON=152 TEXT='Coded ambiguous as protocerebral appendages appear to be in an early developmental stage [@Smith2023n]; adult morphology is uncertain.'; + TEXT CHARACTER= 159 TAXON=174 TEXT='Schinerhannes possibly has straight endites [@Moysiuk2019], although this is difficult to ascertain from the original material [@Kuhl2009], hence conservatively we code ambiguously.'; + TEXT CHARACTER= 160 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 160 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 160 TAXON=128 TEXT='Two, one on each side of the appendage.'; + TEXT CHARACTER= 160 TAXON=152 TEXT='Coded ambiguous as protocerebral appendages appear to be in an early developmental stage [@Smith2023n]; adult morphology is uncertain.'; + TEXT CHARACTER= 160 TAXON=163 TEXT='Uncertain if spine series are present.'; + TEXT CHARACTER= 160 TAXON=174 TEXT='Present [following @Moysiuk2021]'; + TEXT CHARACTER= 161 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 161 TAXON=129 TEXT='Terminal spines are spines, rather than modifications of the appendages [@Ma2014]'; + TEXT CHARACTER= 161 TAXON=152 TEXT='Coded ambiguous as protocerebral appendages appear to be in an early developmental stage [@Smith2023n]; adult morphology is uncertain.'; + TEXT CHARACTER= 161 TAXON=153 TEXT='Megadictyon protocerebral appendages end in a single claw [e.g. @Vannier2014], therefore have a single rather than multifurcate termination.'; + TEXT CHARACTER= 161 TAXON=157 TEXT='@Vannier2014 suggest Pambdelurion''s protocerebral appendage terminates in a single claw, however, this "claw" could be a taphonomic artefact. We code as uncertain. '; + TEXT CHARACTER= 161 TAXON=159 TEXT='The affinity of the anterior appendages of Parapeytoia is unclear therefore we code this character ambiguously, although there is o indication that distalmost podomere is multifurcate [e.g. @Hou1995gff, fig. 12]'; + TEXT CHARACTER= 161 TAXON=163 TEXT='We interpret the claws of Opabinia''s protocerebral appendage as podomerous [see @Whittington1975, figs 75, 79]. The distalmost podomere terminates in a single point [e.g. @Whittington1975, fig. 79], therefore we code the multifurcate termination as absent.'; + TEXT CHARACTER= 161 TAXON=170 TEXT='@Moysiuk2021 interpret the tip of the appendages to have outer spine series ("os" in their figure 6F) with a single terminal stub without a multifurcate termination'; + TEXT CHARACTER= 161 TAXON=172 TEXT='"Terminal podomere stout, with pointed tip." [@VanRoy2015]'; + TEXT CHARACTER= 161 TAXON=173 TEXT='@Liu2018nsr shows that the Lyrarapax appendage terminates in a distal claw, and does not have a multifurcate distal termination.'; + TEXT CHARACTER= 162 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 162 TAXON=152 TEXT='Coded ambiguous as protocerebral appendages appear to be in an early developmental stage [@Smith2023n]; adult morphology is uncertain.'; + TEXT CHARACTER= 162 TAXON=167 TEXT='Unkinked [@Daley2014], though kink present in A. saron. ^nOriginally coded as kinked by @Vinther2014; updated to not kinked by @Moysiuk2019'; + TEXT CHARACTER= 163 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 163 TAXON=152 TEXT='Coded ambiguous as protocerebral appendages appear to be in an early developmental stage [@Smith2023n]; adult morphology is uncertain.'; + TEXT CHARACTER= 163 TAXON=159 TEXT='The affinity of the frontal appendages of Parapeytoia is unclear, hence we code this character as ambiguous although the ''pincer'' of Parapeytoia is formed by distal endite with opposing curvature [@Hou1995gff], rather than the proximal endite (such as in Lyrarapax). '; + TEXT CHARACTER= 163 TAXON=166 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 163 TAXON=167 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 163 TAXON=168 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 163 TAXON=169 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 163 TAXON=171 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 163 TAXON=172 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 163 TAXON=173 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 163 TAXON=174 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 164 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 164 TAXON=152 TEXT='Coded ambiguous as protocerebral appendages appear to be in an early developmental stage [@Smith2023n]; adult morphology is uncertain.'; + TEXT CHARACTER= 164 TAXON=172 TEXT='Following @Moysiuk2021'; + TEXT CHARACTER= 165 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 165 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 165 TAXON=152 TEXT='Coded ambiguous as protocerebral appendages appear to be in an early developmental stage [@Smith2023n]; adult morphology is uncertain.'; + TEXT CHARACTER= 166 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 166 TAXON=129 TEXT='Although @Liu2011 described the appendages of the lobopodian Diania as having an arthropodized organization, a recent revision of this taxon [@Ma2014jsp] concluded that the podomere-like structures on the legs represent taphonomic features on lobopodous appendages.'; + TEXT CHARACTER= 166 TAXON=166 TEXT='Flaps are not sclerotized [@Chen1994]'; + TEXT CHARACTER= 166 TAXON=174 TEXT='Schinderhannes [@Kuhl2009] is coded as having lobopodous post-protocerebral appendages based on the presence of a pair of enlarged lateral body flaps resembling those of Lyrarapax [@Cong2014].^n'; + TEXT CHARACTER= 167 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 167 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 167 TAXON=174 TEXT='Schinderhannes is coded uncertain in view of its ambiguous morphology [@Kuhl2009; @Ortega2016br]'; + TEXT CHARACTER= 168 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 168 TAXON=131 TEXT='The second appendage pair of Cardiodictyon does not seem to be differentiated [@Liu2008app]'; + TEXT CHARACTER= 168 TAXON=137 TEXT='Because the head of Carbotubulus is not preserved [@Haug2012cb], the identity of the limbs is unclear and this character is coded as ambiguous.'; + TEXT CHARACTER= 168 TAXON=148 TEXT='Coded ambiguous to reflect uncertainty as to whether the two anterior appendage pairs represent (i) the protocerebral appendage and a dorsal projection; (ii) the protocerebral and deutocerebral appendages'; + TEXT CHARACTER= 168 TAXON=152 TEXT='Preservation insufficient to evaluate potential differentiation in adult; and appendages may be in an early developmental stage, with differentiation occurring late in development [@Smith2023n].'; + TEXT CHARACTER= 168 TAXON=166 TEXT='The first three flaps are reduced, but the deutocerebral appendage is not morphologically distinct [@Cong2017]. The gnathobase-like structures [@Cong2017] are captured in a separate character.'; + TEXT CHARACTER= 168 TAXON=167 TEXT='@Daley2014 reported the presence of a smaller set of flaps in proximity with the putative head region of Anomalocaris canadensis; given that this differentiation is expressed in size, rather than structural identity, we score the deutocerebral limbs as undifferentiated in Anomalocaris.'; + TEXT CHARACTER= 168 TAXON=174 TEXT='The nature of the second appendage in Schinderhannes is unclear due to poor preservation [@Kuhl2009].'; + TEXT CHARACTER= 169 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 170 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 170 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 170 TAXON=131 TEXT='Short and without claws, indicating non-ambulatory function [@Strausfeld2022]'; + TEXT CHARACTER= 170 TAXON=148 TEXT='The ‘second antenna’ of Antennacanthopodia [@Ou2011] is interpreted as a sensorial appendage.'; + TEXT CHARACTER= 170 TAXON=174 TEXT='Schinderhannes is scored as having an ambulatory limb based on the structure of the enlarged body flap, which is the first observable pot-ocular appendage [@Kuhl2009].'; + TEXT CHARACTER= 171 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 172 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 172 TAXON=175 TEXT='Inapplicable in upper stem euarthropods with arthropodized second appendage; coded with ambiguous token as character is neomorphic.'; + TEXT CHARACTER= 172 TAXON=176 TEXT='Inapplicable in upper stem euarthropods with arthropodized second appendage; coded with ambiguous token as character is neomorphic.'; + TEXT CHARACTER= 172 TAXON=177 TEXT='Inapplicable in upper stem euarthropods with arthropodized second appendage; coded with ambiguous token as character is neomorphic.'; + TEXT CHARACTER= 172 TAXON=178 TEXT='Inapplicable in upper stem euarthropods with arthropodized second appendage; coded with ambiguous token as character is neomorphic.'; + TEXT CHARACTER= 172 TAXON=179 TEXT='Inapplicable in upper stem euarthropods with arthropodized second appendage; coded with ambiguous token as character is neomorphic.'; + TEXT CHARACTER= 172 TAXON=180 TEXT='Inapplicable in upper stem euarthropods with arthropodized second appendage; coded with ambiguous token as character is neomorphic.'; + TEXT CHARACTER= 173 TAXON=118 TEXT='Anterior region not preserved [@Hu2012]'; + TEXT CHARACTER= 174 TAXON=1 TEXT='Neck with 9-11 annular folds [@Maas2009]'; + TEXT CHARACTER= 174 TAXON=7 TEXT='Present, and implied by distribution of trunk spines, particularly in posterior trunk, though the trunk cuticle is often indistinct'; + TEXT CHARACTER= 174 TAXON=10 TEXT='Present, if imperfectly, in thorax'; + TEXT CHARACTER= 174 TAXON=11 TEXT='Annuli present, but weakly developed and in places seem to pinch out [@Zhang2022]'; + TEXT CHARACTER= 174 TAXON=17 TEXT='Presumed present in thorax of Higgins larva, but not possible to establish'; + TEXT CHARACTER= 174 TAXON=18 TEXT='Prominent in thorax of Higgins larva [@Neves2019]'; + TEXT CHARACTER= 174 TAXON=19 TEXT='Prominent in thorax of Higgins larva [@Neves2019]'; + TEXT CHARACTER= 174 TAXON=20 TEXT='Degree of annulation on neck [@Heiner2007hmr]'; + TEXT CHARACTER= 174 TAXON=22 TEXT='Prominent in thorax of Higgins larva [@Neves2019]'; + TEXT CHARACTER= 174 TAXON=23 TEXT='Prominent in thorax [@Neves2019]'; + TEXT CHARACTER= 174 TAXON=24 TEXT='No annulations evident in adult or larva [@Fujimoto2020mb]'; + TEXT CHARACTER= 174 TAXON=25 TEXT='Prominent throughout Shira larval trunk [@Neves2014ode]'; + TEXT CHARACTER= 174 TAXON=28 TEXT='Absent; segmentation instead'; + TEXT CHARACTER= 174 TAXON=38 TEXT='Annulations in larval trunk only [@Bolek2013]'; + TEXT CHARACTER= 174 TAXON=95 TEXT='Preservation insufficient to evaluate [@Maas2007ppp]'; + TEXT CHARACTER= 174 TAXON=96 TEXT='Inferred as present based on distribution of denticles, even if not evident from preservation of cuticle'; + TEXT CHARACTER= 174 TAXON=137 TEXT='A taphonomic absence can be discounted because annulations are preserved in co-occurring specimens of Ilyodes [@Haug2012cb]^n'; + TEXT CHARACTER= 174 TAXON=138 TEXT='Absent [@Smith2015]'; + TEXT CHARACTER= 174 TAXON=141 TEXT='Annulated trunk and limbs [@Howard2020]'; + TEXT CHARACTER= 174 TAXON=143 TEXT='Fine epidermal annuli between limb pairs [@Caron2017]'; + TEXT CHARACTER= 174 TAXON=148 TEXT='Present on limbs; it is unclear whether the trunk was annulated, due to effaced preservation [@Ou2011].'; + TEXT CHARACTER= 174 TAXON=152 TEXT='Coded ambiguous, as larval stages may lack evidence of annulations that are present in adults [e.g. in Onychophora; @Walker2004]'; + TEXT CHARACTER= 174 TAXON=155 TEXT='Lobopodous limb appears annulated [@Hou1995gff]'; + TEXT CHARACTER= 174 TAXON=157 TEXT='Present on limbs; it is unclear whether the trunk was annulated, due to effaced preservation [@Budd1998ar].'; + TEXT CHARACTER= 174 TAXON=175 TEXT='Coded ambiguous as sclerotization of trunk assumed to overprint evidence of annulation'; + TEXT CHARACTER= 174 TAXON=176 TEXT='Coded ambiguous as sclerotization of trunk assumed to overprint evidence of annulation'; + TEXT CHARACTER= 174 TAXON=177 TEXT='Coded ambiguous as sclerotization of trunk assumed to overprint evidence of annulation'; + TEXT CHARACTER= 174 TAXON=178 TEXT='Coded ambiguous as sclerotization of trunk assumed to overprint evidence of annulation'; + TEXT CHARACTER= 174 TAXON=179 TEXT='Coded ambiguous as sclerotization of trunk assumed to overprint evidence of annulation'; + TEXT CHARACTER= 174 TAXON=180 TEXT='Coded ambiguous as sclerotization of trunk assumed to overprint evidence of annulation'; + TEXT CHARACTER= 175 TAXON=14 TEXT='Annulations uneven in size, but not systematically differentiated [@Shao2020]'; + TEXT CHARACTER= 175 TAXON=124 TEXT='Homonomous annulation, despite presence of appendages [@Dzik1989; @Jaeger2010]'; + TEXT CHARACTER= 175 TAXON=153 TEXT='Annulations in Megadictyon appear regular [@Liu2007az], so this taxon is coded as homonomous.'; + TEXT CHARACTER= 175 TAXON=156 TEXT='Jianshanopodia exhibits regions of narrower annulations between appendages [@Liu2006], so is coded as heteronomous.'; + TEXT CHARACTER= 175 TAXON=157 TEXT='We code Pambdelurion as uncertain, as the trunk is not adequately preserved to make a confident assignation [@Budd1998ar; @Young2017]'; + TEXT CHARACTER= 176 TAXON=15 TEXT='Coded absent in Eokinorhynchus as the ‘neck’ region is considered part of the introvert [@Zhang2015]'; + TEXT CHARACTER= 176 TAXON=45 TEXT='Coded as continuing to front in Halicryptus. A deep groove separates the region that is adorned with Zone I armature, but this area seems to bear faint annulations (that are not associated with the armature) [@Shirley1999].'; + TEXT CHARACTER= 176 TAXON=49 TEXT='Continuing to the front [@Hammond1970]'; + TEXT CHARACTER= 176 TAXON=97 TEXT='Indistinct in anterior trunk (see notes on Introvert for delineation of trunk and introvert) [@Howard2020]'; + TEXT CHARACTER= 176 TAXON=102 TEXT='Coded ambiguous in Paratubiluchus [@Han2004] as annulations are not clearly enough preserved to evaluate their distribution in the neck area.^n'; + TEXT CHARACTER= 176 TAXON=111 TEXT='Coded ambiguous in Guanduscolex [@Hu2008] as the apparent absence of anterior annulations may be preservational.'; + TEXT CHARACTER= 176 TAXON=119 TEXT='Consistent annulation to base of introvert [@Yang2020]'; + TEXT CHARACTER= 176 TAXON=122 TEXT='As with Cricocosmia, the anterior trunk is less prominently annulated and lacks prominent dorsal sclerites [@Shi2022]'; + TEXT CHARACTER= 176 TAXON=123 TEXT='Anterior region with indistinct annulations and reduction of dorsal armature'; + TEXT CHARACTER= 176 TAXON=126 TEXT='The introvert is not treated as part of the trunk'; + TEXT CHARACTER= 176 TAXON=128 TEXT='The introvert is not treated as part of the trunk'; + TEXT CHARACTER= 176 TAXON=129 TEXT='Indistinct near narrow end of trunk [@Ma2014]'; + TEXT CHARACTER= 176 TAXON=153 TEXT='Annulations are not clearly preserved in the anterior region [@Liu2006; @Liu2007], making this character difficult to score with confidence.'; + TEXT CHARACTER= 176 TAXON=154 TEXT='Annulations are not clearly preserved in the anterior region [@Liu2006; @Liu2007], making this character difficult to score with confidence.'; + TEXT CHARACTER= 176 TAXON=156 TEXT='Annulations in the pharynx of Kerygmachela continue to the terminal mouth [@Budd1998trse]; given the position of the prominent annulated appendages, it seems likely that the head also expressed external annulations.'; + TEXT CHARACTER= 177 TAXON=10 TEXT='Branching present [@Maas2007]'; + TEXT CHARACTER= 177 TAXON=11 TEXT='Branching and pinching out evident [@Zhang2022]'; + TEXT CHARACTER= 177 TAXON=13 TEXT='First ten annulae unbranched [@Liu2018]'; + TEXT CHARACTER= 177 TAXON=15 TEXT='Strictly unbranched [@Zhang2015]'; + TEXT CHARACTER= 177 TAXON=16 TEXT='Apparent branching [@Shao2020]'; + TEXT CHARACTER= 177 TAXON=98 TEXT='Apparent branching / overlapping [@Ma2014, fig. 3.3]'; + TEXT CHARACTER= 177 TAXON=141 TEXT='No branching observed [@Howard2020]'; + TEXT CHARACTER= 177 TAXON=143 TEXT='No evidence of branching [@Caron2017]'; + TEXT CHARACTER= 177 TAXON=153 TEXT='No indication of branching in @Ramskold1998, fig 3.8C '; + TEXT CHARACTER= 178 TAXON=15 TEXT='The repeated elements of Eokinorhynchus are coded as annulations with serially iterated sclerites; this taxon is not coded as segmented.'; + TEXT CHARACTER= 178 TAXON=162 TEXT='@Moysiuk2022 observe segmental boundaries (arguably implying arthrodization) in the dorsal trunk cuticle, though these are not apparent on the ventral surface; this recalls the ventrally flexible configuration of Opabinia.'; + TEXT CHARACTER= 178 TAXON=163 TEXT='Coded as present since has discrete body segments separated by furrows [@Budd1996; @Zhang2007; @Budd2012]'; + TEXT CHARACTER= 178 TAXON=169 TEXT='The single complete specimen does not conclusively establish the presence or absence of epidermal segmentation [@Daley2009]'; + TEXT CHARACTER= 178 TAXON=172 TEXT='Interpreted as present by @Moysiuk2022'; + TEXT CHARACTER= 178 TAXON=173 TEXT='Interpreted as present by Moysiuk & Caron (2022)'; + TEXT CHARACTER= 178 TAXON=174 TEXT='Although interpreted as present by @Moysiuk2022, we do not consider the single available specimen [@Kuhl2009] to definitively establish the presence or absence of epidermal segmentation'; + TEXT CHARACTER= 179 TAXON=69 TEXT='Although some heterotardigrades possess dorsal plates [e.g. @Nelson2002; @Marchioro2013], these are not connected by arthrodial membranes. We thus score Actinarctus as absent for this character.'; + TEXT CHARACTER= 179 TAXON=129 TEXT='The dorsal oval elements [@Liu2011; @Ma2014] are interpreted as modified trunk sclerites'; + TEXT CHARACTER= 179 TAXON=160 TEXT='Following @Zeng2020'; + TEXT CHARACTER= 179 TAXON=161 TEXT='Trunk not arthrodized [@Zhang2023]'; + TEXT CHARACTER= 180 TAXON=160 TEXT='No arthrodial membranes [@Zeng2020]'; + TEXT CHARACTER= 180 TAXON=161 TEXT='Trunk not arthrodized [@Zhang2023]'; + TEXT CHARACTER= 180 TAXON=176 TEXT='Absent'; + TEXT CHARACTER= 192 TAXON=28 TEXT='Reported as present by @DalZotto2013, but considered absent by @Herranz2021z, who note instead the presence of a long mid-dorsal spine on segment 11, not associated with musculature'; + TEXT CHARACTER= 193 TAXON=28 TEXT='Absent [@Herranz2021z]'; + TEXT CHARACTER= 193 TAXON=29 TEXT='Present [@Herranz2021z]'; + TEXT CHARACTER= 193 TAXON=33 TEXT='Present [@Herranz2021z]'; + TEXT CHARACTER= 193 TAXON=34 TEXT='Absent [@Herranz2021z]'; + TEXT CHARACTER= 196 TAXON=28 TEXT='Absent [@DalZotto2013sb]'; + TEXT CHARACTER= 196 TAXON=29 TEXT='Present in Antygomonas, Campyloderes, Centroderes, Dracoderes, Echinoderes, Meristoderes, Semnoderes, Sphenoderes, Tubulideres, Kinorhynchus and Pycnophyes [@SchmidtRheasa2013]'; + TEXT CHARACTER= 196 TAXON=30 TEXT='Present in Antygomonas, Campyloderes, Centroderes, Dracoderes, Echinoderes, Meristoderes, Semnoderes, Sphenoderes, Tubulideres, Kinorhynchus and Pycnophyes [@SchmidtRheasa2013]'; + TEXT CHARACTER= 196 TAXON=31 TEXT='Present in Antygomonas, Campyloderes, Centroderes, Dracoderes, Echinoderes, Meristoderes, Semnoderes, Sphenoderes, Tubulideres, Kinorhynchus and Pycnophyes [@SchmidtRheasa2013]'; + TEXT CHARACTER= 196 TAXON=32 TEXT='Present in Antygomonas, Campyloderes, Centroderes, Dracoderes, Echinoderes, Meristoderes, Semnoderes, Sphenoderes, Tubulideres, Kinorhynchus and Pycnophyes [@SchmidtRheasa2013]'; + TEXT CHARACTER= 196 TAXON=33 TEXT='Present in Antygomonas, Campyloderes, Centroderes, Dracoderes, Echinoderes, Meristoderes, Semnoderes, Sphenoderes, Tubulideres, Kinorhynchus and Pycnophyes [@SchmidtRheasa2013]'; + TEXT CHARACTER= 196 TAXON=34 TEXT='Present in Antygomonas, Campyloderes, Centroderes, Dracoderes, Echinoderes, Meristoderes, Semnoderes, Sphenoderes, Tubulideres, Kinorhynchus and Pycnophyes [@SchmidtRheasa2013]'; + TEXT CHARACTER= 196 TAXON=35 TEXT='Present in Antygomonas, Campyloderes, Centroderes, Dracoderes, Echinoderes, Meristoderes, Semnoderes, Sphenoderes, Tubulideres, Kinorhynchus and Pycnophyes [@SchmidtRheasa2013]'; + TEXT CHARACTER= 196 TAXON=36 TEXT='Present in Antygomonas, Campyloderes, Centroderes, Dracoderes, Echinoderes, Meristoderes, Semnoderes, Sphenoderes, Tubulideres, Kinorhynchus and Pycnophyes [@SchmidtRheasa2013]'; + TEXT CHARACTER= 196 TAXON=37 TEXT='Present in Antygomonas, Campyloderes, Centroderes, Dracoderes, Echinoderes, Meristoderes, Semnoderes, Sphenoderes, Tubulideres, Kinorhynchus and Pycnophyes [@SchmidtRheasa2013]'; + TEXT CHARACTER= 197 TAXON=28 TEXT='Absent [@DalZotto2013sb]'; + TEXT CHARACTER= 198 TAXON=141 TEXT='Absent [@Howard2020]'; + TEXT CHARACTER= 198 TAXON=148 TEXT='Not evident, despite some preservation of internal tissue [@Ou2011]'; + TEXT CHARACTER= 198 TAXON=152 TEXT='Present [@Smith2023n]'; + TEXT CHARACTER= 198 TAXON=167 TEXT='Present [@Briggs1984; @Daley2014]'; + TEXT CHARACTER= 198 TAXON=173 TEXT='Present in L. trilobus [@Cong2016]; reported absence in L. unguispinus [@Cong2014] attributed to non-preservation.'; + TEXT CHARACTER= 199 TAXON=16 TEXT='Posterior narrowing [@Shao2020]'; + TEXT CHARACTER= 199 TAXON=42 TEXT='Uniform for most of length, before narrowing to caudal filament that comprises a third of the body [@Reiman1972]'; + TEXT CHARACTER= 199 TAXON=43 TEXT='Narrow post-anal caudal extension of the trunk'; + TEXT CHARACTER= 199 TAXON=152 TEXT='Unknown whether narrowing [@Smith2023n] is developmental or would be retained to adulthood.'; + TEXT CHARACTER= 199 TAXON=156 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 199 TAXON=157 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 199 TAXON=160 TEXT='@Zeng2020 supplementary info clarifies narrowing trend posteriad'; + TEXT CHARACTER= 199 TAXON=163 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 199 TAXON=166 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 199 TAXON=167 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 199 TAXON=168 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 199 TAXON=169 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 199 TAXON=171 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 199 TAXON=172 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 199 TAXON=173 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 199 TAXON=174 TEXT='Following @Moysiuk2019'; + TEXT CHARACTER= 200 TAXON=7 TEXT='Primarily manifested in differentiation of armature type, distribution and density [@ConwayMorris1977]. See notes on introvert for interpretation of the anterior trunk vs. introvert.'; + TEXT CHARACTER= 200 TAXON=10 TEXT='Thorax differentiated from plate-bearing region and anal field'; + TEXT CHARACTER= 200 TAXON=12 TEXT='Anterior four annulations are narrower and exhibit distinct sclerite shape [@Liu2019]'; + TEXT CHARACTER= 200 TAXON=14 TEXT='Some variation in annular expression between first five and subsequent annulations [@Shao2020], but not prominent enough to denote a distinct subdivision of the trunk'; + TEXT CHARACTER= 200 TAXON=15 TEXT='Differentiated ''neck'' region [@Zhang2015] resembles anterior trunk of Acosmia [@Howard2020]'; + TEXT CHARACTER= 200 TAXON=16 TEXT='Not differentiated beyond introvert [@Zhao2016]'; + TEXT CHARACTER= 200 TAXON=17 TEXT='Unclear in adult but likely prominent in larva'; + TEXT CHARACTER= 200 TAXON=95 TEXT='Dumbbell shape hints at anterior differentiation [@Maas2007ppp]'; + TEXT CHARACTER= 200 TAXON=96 TEXT='Unclear in adult but likely prominent in larva'; + TEXT CHARACTER= 200 TAXON=97 TEXT='Differentiated only by armature and diminished annulations [@Howard2020], both of which are acknowledged in separate characters. '; + TEXT CHARACTER= 200 TAXON=98 TEXT='Ambiguous: Smooth anterior trunk in YKLP 11333 [@Ma2014] but the introvert of this specimen is not obviously equivalent to that in Eximipriapulus and no other specimen shows this differentiation so clearly. '; + TEXT CHARACTER= 200 TAXON=120 TEXT='Plausible differentiation, by analogy with C. jinningensis [@ThisStudy]'; + TEXT CHARACTER= 200 TAXON=121 TEXT='Present: the anterior trunk of many specimens lacks plates, and annulations are more closely spaced or absent [@Hou1994; @Maas2007ppp; @Vannier2017]'; + TEXT CHARACTER= 200 TAXON=122 TEXT='Anterior region with diminished annulation and absence of dorsal plates'; + TEXT CHARACTER= 200 TAXON=129 TEXT='Ambiguous; depends on whether the narrow end [@Ma2014] is interpreted as corresponding to an introvert.'; + TEXT CHARACTER= 200 TAXON=131 TEXT='Three differentiated appendages associated with sclerotized region [@Strausfeld2022]'; + TEXT CHARACTER= 200 TAXON=134 TEXT='Change in appendage construction, and possibly thickness [@Siveter2018]'; + TEXT CHARACTER= 200 TAXON=141 TEXT='Differentiated: posterior trunk lacks appendages [@Howard2020]'; + TEXT CHARACTER= 200 TAXON=143 TEXT='Short posterior trunk comprising three appendage pairs [@Caron2017]'; + TEXT CHARACTER= 202 TAXON=1 TEXT='May not be evident if epicuticle is not preserved [@Maas2009]'; + TEXT CHARACTER= 202 TAXON=21 TEXT='Absent [@Heiner2008sb]'; + TEXT CHARACTER= 202 TAXON=38 TEXT='Crowned areoles [@Bolek2013] have some resemblance to sensory spots in other taxa'; + TEXT CHARACTER= 202 TAXON=44 TEXT='Sensory structures ringed with tube-like elements, and ''ring papillae'' [@SchmidtRhaesa2022za], both recall these sensory structures'; + TEXT CHARACTER= 204 TAXON=44 TEXT='Petal-like configuration [@SchmidtRhaesa2022za, fig. 6c]'; + TEXT CHARACTER= 206 TAXON=8 TEXT='Unclear, but cuticular elements present'; + TEXT CHARACTER= 206 TAXON=44 TEXT='Present [@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 206 TAXON=103 TEXT='''may have borne surface ornamentation'' [@ConwayMorris1977]'; + TEXT CHARACTER= 206 TAXON=124 TEXT='No indication of papillae on annulations [@Dzik1989; @Jaeger2010]'; + TEXT CHARACTER= 206 TAXON=129 TEXT='Ambiguous, through trunk spines are present [@Ou2018].'; + TEXT CHARACTER= 206 TAXON=135 TEXT='Present [@Maas2007csb]'; + TEXT CHARACTER= 206 TAXON=141 TEXT='Present [@Howard2020]'; + TEXT CHARACTER= 206 TAXON=148 TEXT='Coded as ambiguous in Antennacanthopodia [@Ou2011] as its trunk annulations are not clearly apparent.'; + TEXT CHARACTER= 207 TAXON=127 TEXT='Possibly represented by the row of ''tubercles'' [@Dzik2011]'; + TEXT CHARACTER= 208 TAXON=10 TEXT='The tessellating plates [@Maas2007] satisfy the morphological criteria for inclusion as a lorica'; + TEXT CHARACTER= 208 TAXON=25 TEXT='Present [@Neves2014ode]'; + TEXT CHARACTER= 209 TAXON=95 TEXT='Specimens reach consistent large size [@Maas2007ppp] so are assumed adult'; + TEXT CHARACTER= 211 TAXON=20 TEXT='One series, interspersed with intercalar plicae [@Heiner2007hmr]'; + TEXT CHARACTER= 211 TAXON=21 TEXT='30–60 plicae [@SchmidtRhasea2013]'; + TEXT CHARACTER= 211 TAXON=95 TEXT='Single series [@Maas2007ppp]'; + TEXT CHARACTER= 212 TAXON=17 TEXT='Twenty [@Harvey2017]'; + TEXT CHARACTER= 212 TAXON=18 TEXT='Six in adults [@SchmidtRhasea2013]^n22 plicae in N. mysticus Higgins larva; 20-25 in genus [@Neves2016]'; + TEXT CHARACTER= 212 TAXON=19 TEXT='Six [@SchmidtRhasea2013]'; + TEXT CHARACTER= 212 TAXON=20 TEXT='Eight [@SchmidtRhasea2013]'; + TEXT CHARACTER= 212 TAXON=21 TEXT='30 to 60 longitudinal folds [@Fujimoto2020mb]'; + TEXT CHARACTER= 212 TAXON=22 TEXT='Twenty plicae in Higgins larva of P. orphanus, P. gracilis [@Neves2016]; 22 or 24 in other species^n^nTwenty-two plicae in adults [generalizes @SchmidtRhasea2013]'; + TEXT CHARACTER= 212 TAXON=23 TEXT='Thirty plicae'; + TEXT CHARACTER= 212 TAXON=24 TEXT='About 46 longitudinal folds [@Fujimoto2020mb]'; + TEXT CHARACTER= 212 TAXON=45 TEXT='Large dorsal and ventral with six slender accordion-like lateral plates [@Storch1991jm]'; + TEXT CHARACTER= 212 TAXON=48 TEXT='Large dorsal and ventral plates plus six slender lateral plates [@SchmidtRhaesa2023za]'; + TEXT CHARACTER= 212 TAXON=49 TEXT='Eight in first (and second?) lorica larva [@Wennberg2009ib]'; + TEXT CHARACTER= 212 TAXON=51 TEXT='Twenty [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 212 TAXON=95 TEXT='~20; ten on visible surface [@Maas20087ppp]'; + TEXT CHARACTER= 212 TAXON=96 TEXT='Seven [@Peel2013]'; + TEXT CHARACTER= 213 TAXON=20 TEXT='Differentiated and somewhat enlarged'; + TEXT CHARACTER= 213 TAXON=49 TEXT='Somewhat distinct [@Wennberg2009ib, fig. 6B]'; + TEXT CHARACTER= 214 TAXON=8 TEXT='Triangular/conical elements evident on each annulation in GSC 45331'; + TEXT CHARACTER= 214 TAXON=10 TEXT='Papillae extending into spines or setae [@Maas2007]'; + TEXT CHARACTER= 214 TAXON=16 TEXT='The ''pits'' [@Shao2020] are interpreted as the (broken?) bases of sclerites'; + TEXT CHARACTER= 214 TAXON=21 TEXT='Occasional setae, but no lorical plates or obvious sclerites [e.g. @Heiner2006sb]'; + TEXT CHARACTER= 214 TAXON=22 TEXT='Cuticle is folded but lacks sclerotized fields [@Gad2005za]'; + TEXT CHARACTER= 214 TAXON=24 TEXT='No sclerites posterior of the trichoscalids, borne on the neck.'; + TEXT CHARACTER= 214 TAXON=41 TEXT='Copulatory spicules and other setae [@Keppner1988tams]'; + TEXT CHARACTER= 214 TAXON=42 TEXT='Absent in Onchulus [@Swart1993] (Description of two new species of the genera Onchulus and Limonchulus from Southern Africa (Nematoda: Enoplida, Onchulinae))'; + TEXT CHARACTER= 214 TAXON=44 TEXT='Prominent on caudal appendage [@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 214 TAXON=96 TEXT='Evident elements occur on the posterior introvert [@Peel2013], but the status of the trunk is uncertain; sensory setae are possibly present but not preserved'; + TEXT CHARACTER= 214 TAXON=106 TEXT='Annulations are observed, but sclerites are not [@Hu2017]; the preservation is inadequate to evaluate the possible presence of specialized sclerites'; + TEXT CHARACTER= 214 TAXON=107 TEXT='Xystoscolex clearly displays bands of plates, and sclerites on its extensive introvert [@ConwayMorris2010]; though there is no direct evidence, it is hard to rule out the possibility that diminutive sclerites occur along the trunk'; + TEXT CHARACTER= 214 TAXON=111 TEXT='No evidence of sclerites among plates [@Hu2012], but material inadequately known to exclude the possibility of e.g. ventral spines'; + TEXT CHARACTER= 214 TAXON=112 TEXT='Not reported or visible; only plates present [@Hou1994]'; + TEXT CHARACTER= 214 TAXON=114 TEXT='''Ruptures'' in cuticle [@Duan2012] conceivably denote sensory structures, but no evidence of robust sclerites in phosphatized specimens'; + TEXT CHARACTER= 214 TAXON=116 TEXT='No sclerties preserved despite high fidelity preservation of plates [@GarciaBellido2013]'; + TEXT CHARACTER= 214 TAXON=117 TEXT='No sclerties preserved despite high fidelity preservation of plates [@GarciaBellido2013]'; + TEXT CHARACTER= 214 TAXON=118 TEXT='No evidence of sclerites among plates [@Hu2012], but material inadequately known to exclude the possibility of e.g. ventral spines'; + TEXT CHARACTER= 214 TAXON=119 TEXT='Paired spines present [@Shi2022]'; + TEXT CHARACTER= 214 TAXON=121 TEXT='Paired dorsal sclerites [@Shi2022]'; + TEXT CHARACTER= 214 TAXON=126 TEXT='@Whittington1975 describes rows of seven ''tubercles'' with a triangular lateral profile and which bore an apical spine. They exhibit slight relief and are carbonized. We homologize these with trunk sclerites.'; + TEXT CHARACTER= 214 TAXON=127 TEXT='Difficult to interpret the single longitudinal series of ''tubercles'' [@Dzik2011], which could correspond to a row of ventral papillae (cg. Onychodictyon)'; + TEXT CHARACTER= 214 TAXON=130 TEXT='Claws present (but no other sclerites)'; + TEXT CHARACTER= 214 TAXON=147 TEXT='The smaller spines between the enlarged spines [@ConwayMorris1988] are treated as ''standard'' trunk sclerites that have been incorporated into the sclerotized rings, rather than a separate character as in @Yang2015 (character 48).^n'; + TEXT CHARACTER= 214 TAXON=158 TEXT='Presence of nodes plausible but impossible to establish'; + TEXT CHARACTER= 215 TAXON=11 TEXT='Seemingly present in enlarged sclerites [@ThisStudy]'; + TEXT CHARACTER= 215 TAXON=12 TEXT='Hollow, without internal elements [@Liu2019]'; + TEXT CHARACTER= 215 TAXON=14 TEXT='Hollow sclerites; suggestion of laminar construction in enlarged sclerites on annulus 9 [@Shao2020] is presumed taphonomic'; + TEXT CHARACTER= 215 TAXON=95 TEXT='Not obviously apparent [@Maas2007ppp], but quality of preservation insufficient to determine with confidence'; + TEXT CHARACTER= 215 TAXON=119 TEXT='Stacked elements [@ThisStudy]'; + TEXT CHARACTER= 215 TAXON=120 TEXT='Evident on trunk sclerites and tail spines [@ThisStudy]'; + TEXT CHARACTER= 215 TAXON=121 TEXT='Not reported [@Shi2022], presumably reflecting inadequate preservation'; + TEXT CHARACTER= 215 TAXON=126 TEXT='Aysheaia claws do not have stacked elements [@Smith2014].'; + TEXT CHARACTER= 215 TAXON=128 TEXT='Some form of internal structure evidence in @Vannier2007 fig. 5, but unclear whether this corresponds to nested elements.'; + TEXT CHARACTER= 215 TAXON=129 TEXT='Ambiguous: some elements hint at a stacked construction [@Ma2014], but it is not possible to account for preservation in the published figures.'; + TEXT CHARACTER= 215 TAXON=130 TEXT='Long, slender claws make structure difficult to determine; a plausible hint of an outer element exists in one specimen [@Vannier2017, fig. 5e] but this interpretation is at best ambiguous.'; + TEXT CHARACTER= 215 TAXON=138 TEXT='Present [@Caron2013; @Smith2014]'; + TEXT CHARACTER= 215 TAXON=141 TEXT='Spines not described in adequate detail to evaluate presence of stacked elements [@Howard2020]'; + TEXT CHARACTER= 215 TAXON=143 TEXT='Claws comprise stacked elements [@Caron2017]'; + TEXT CHARACTER= 215 TAXON=144 TEXT='Yes, in dorsal spines [@Yang2015]'; + TEXT CHARACTER= 215 TAXON=145 TEXT='Present; see discussion in @Caron2020'; + TEXT CHARACTER= 215 TAXON=147 TEXT='Stacked elements present in spines [@Caron2020]'; + TEXT CHARACTER= 216 TAXON=4 TEXT='Sclerites are often mineralized, but are considered homologous to lophotrochozoan chaetae and thus not treated as homologous here'; + TEXT CHARACTER= 216 TAXON=11 TEXT='Enlarged and specialized sclerites only [@Zhang2022]'; + TEXT CHARACTER= 216 TAXON=16 TEXT='Specialized elements only [@Liu2014; @Shao2020]'; + TEXT CHARACTER= 216 TAXON=97 TEXT='The anterior papillae have a conical shape; the disc-like posterior papillae are likely equivalent, perhaps with a lower profile [@Howard2020]. There is no evidence of mineralization. All papillae are therefore treated as papillae rather than plates.'; + TEXT CHARACTER= 216 TAXON=106 TEXT='Annulations are observed, but sclerites are not [@Hu2017]'; + TEXT CHARACTER= 216 TAXON=107 TEXT='More or less circular; no microstructure visible [@ConwayMorris2010]'; + TEXT CHARACTER= 216 TAXON=122 TEXT='Present, if difficult to discern from e.g. @Shi2022, fig. 4b'; + TEXT CHARACTER= 216 TAXON=124 TEXT='At least some annulae have a pustulose appearance suggestive of sclerite presence [@Dzik1989]'; + TEXT CHARACTER= 216 TAXON=125 TEXT='The angular nature of the papillae on annulations [@Budd1998p] suggests their identification as sclerotized elements'; + TEXT CHARACTER= 216 TAXON=126 TEXT='We treat the sclerites as non-enlarged given their diminutive size; the subtle nature of their preservation argues against the robust structure that often characterizes enlarged sclerites.'; + TEXT CHARACTER= 216 TAXON=127 TEXT='Difficult to interpret the single longitudinal series of ''tubercles'' [@Dzik2011], which conceivably correspond to trunk sclerites'; + TEXT CHARACTER= 216 TAXON=128 TEXT='"Finger-like papillae" [@Ou2012] have a spine-shaped outline and likely correspond to trunk sclerites in other taxa'; + TEXT CHARACTER= 216 TAXON=133 TEXT='Probably represented by ''tubercles'' [@Liu2008]'; + TEXT CHARACTER= 216 TAXON=134 TEXT='Seemingly absent '; + TEXT CHARACTER= 216 TAXON=136 TEXT='Specialized elements only [@Zhang2016bl]'; + TEXT CHARACTER= 216 TAXON=142 TEXT='Insufficiently preserved to evaluate [@Ma2009; @Ma2012]'; + TEXT CHARACTER= 216 TAXON=143 TEXT='Absent, except for spines on anterior appendages [@Caron2017].'; + TEXT CHARACTER= 216 TAXON=144 TEXT='Scored as absent: the papillae and hair-like setae are sparsely distributed [@Yang2015] and so coded as specialized.'; + TEXT CHARACTER= 216 TAXON=145 TEXT='Annulae seemingly without sclerites, which may nonetheless be present on anterior appendages [@Caron2020]'; + TEXT CHARACTER= 216 TAXON=147 TEXT='Spinose sclerites present on annulae [@Caron2020, fig. 6b]'; + TEXT CHARACTER= 216 TAXON=148 TEXT='Small conical spines evident in rings [@Ou2011]'; + TEXT CHARACTER= 217 TAXON=107 TEXT='No clear evidence of phosphatization but difficult to evaluate from preservational mode'; + TEXT CHARACTER= 217 TAXON=110 TEXT='Prominent three-dimensional relief and dark colouration [@huang] hints at an originally phosphatic composition '; + TEXT CHARACTER= 217 TAXON=123 TEXT='Though not reported, the surface exhibits a plate-like texture [@Han2007pr, fig. 1.8] and in regions seems to preserve with relief [@Han2007pr, fig. 1.7], hinting at the possible presence of trunk sclerites'; + TEXT CHARACTER= 218 TAXON=98 TEXT='Subtriangular elements [@Ma2014jp]'; + TEXT CHARACTER= 218 TAXON=104 TEXT='Elongate [@Smith2015p]'; + TEXT CHARACTER= 218 TAXON=105 TEXT='Triangular [@Yang2021]'; + TEXT CHARACTER= 218 TAXON=109 TEXT='Triangular projections'; + TEXT CHARACTER= 218 TAXON=110 TEXT='Reconstructed as triangular, but preserved sclerites are flat discs [@Huang2004, fig. 3c]'; + TEXT CHARACTER= 218 TAXON=125 TEXT='Seemingly conical [@Budd1998p]'; + TEXT CHARACTER= 218 TAXON=126 TEXT='Triangular [@Whittington1978]'; + TEXT CHARACTER= 218 TAXON=133 TEXT='Seemingly triangular in profile [see pair in top left corner of @Liu2008, fig. 2A6]'; + TEXT CHARACTER= 219 TAXON=107 TEXT='Potential nodes on sclerites (sets of four) in @ConwayMorris2010, fig. 5B, but these are not mentioned in text and inadequately figured to support a decisive scoring.'; + TEXT CHARACTER= 219 TAXON=108 TEXT='Some surface texture [@ConwayMorris2010]; unclear whether this corresponds to nodes.'; + TEXT CHARACTER= 219 TAXON=110 TEXT='Five to ten nodes around a central node [@Huang2004, fig. 1c]'; + TEXT CHARACTER= 219 TAXON=119 TEXT='Plates with single node [@Yang2020]'; + TEXT CHARACTER= 219 TAXON=122 TEXT='Four nodes in single ring, where this can be determined from @Shi2022, fig. 4'; + TEXT CHARACTER= 220 TAXON=114 TEXT='Prominent single central boss with three to four nodes [@Duan2012]'; + TEXT CHARACTER= 220 TAXON=118 TEXT='Single ring of four to six nodes [@Hu2012]'; + TEXT CHARACTER= 220 TAXON=119 TEXT='Single node [@Yang2020]'; + TEXT CHARACTER= 221 TAXON=122 TEXT='No cases that obviously don''t have four, but images in @Shi2022 insufficient to determine with confidence.'; + TEXT CHARACTER= 223 TAXON=121 TEXT='Four nodes [@ThisStudy]'; + TEXT CHARACTER= 224 TAXON=98 TEXT='Possible distinction of posterior band [@Ma2014jp] is not considered equivalent, if this is indeed not a taphonomic feature.'; + TEXT CHARACTER= 224 TAXON=104 TEXT='Posterior not visible'; + TEXT CHARACTER= 224 TAXON=105 TEXT='Posterior not visible'; + TEXT CHARACTER= 224 TAXON=108 TEXT='Spinose anterior region [@ConwayMorris2010] interpreted as anterior trunk rather than introvert, by comparison with Acosmia [@Howard2020]'; + TEXT CHARACTER= 224 TAXON=125 TEXT='Anterior missing'; + TEXT CHARACTER= 225 TAXON=126 TEXT='Seemingly restricted to dorsal surface [@Whittington1978]'; + TEXT CHARACTER= 225 TAXON=128 TEXT='Ventral disposition unknown'; + TEXT CHARACTER= 225 TAXON=129 TEXT='Seemingly complete; certainly spanning the width of the body [@Ou2018]'; + TEXT CHARACTER= 225 TAXON=148 TEXT='Completely encircling appendages [@Ou2011]'; + TEXT CHARACTER= 226 TAXON=7 TEXT='Regular series in quincunx'; + TEXT CHARACTER= 226 TAXON=11 TEXT='Tubules and enlarged plates'; + TEXT CHARACTER= 226 TAXON=98 TEXT='Transverse fields in at least the posterior region of the trunk [@Ma2014jp]'; + TEXT CHARACTER= 226 TAXON=107 TEXT='Transverse rows evident in posterior trunk [@ConwayMorris2010, fig. 5B]'; + TEXT CHARACTER= 226 TAXON=122 TEXT='Following @Shi2022, under the interpretation presented by @ThisStudy'; + TEXT CHARACTER= 226 TAXON=126 TEXT='Regular transverse rows [@Whittington1978]'; + TEXT CHARACTER= 226 TAXON=141 TEXT='Along annular rings [@Howard2020cb]'; + TEXT CHARACTER= 226 TAXON=148 TEXT='On appendages only [@Ou2011]'; + TEXT CHARACTER= 227 TAXON=111 TEXT='One field comprising three rows of plates per annulation [@Hu2008]'; + TEXT CHARACTER= 227 TAXON=122 TEXT='Irregular [@Shi2022, fig. 4]'; + TEXT CHARACTER= 228 TAXON=111 TEXT='One field comprising three rows of plates per annulation [@Hu2008]'; + TEXT CHARACTER= 229 TAXON=12 TEXT='Disorderly [@Liu2019]'; + TEXT CHARACTER= 229 TAXON=14 TEXT='Number of sclerites increases in line with trunk circumference [@Shao2020]'; + TEXT CHARACTER= 229 TAXON=15 TEXT='We interpret the flat subrectuangular elements as an expression of cuticular structure, rather than distinct sclerites. Distinct sclerites display an inexact correspondence between subsequent rows in the type material [@Zhang2015], and in other material ascribed to the genus [@Wang2025, fig. 2F]'; + TEXT CHARACTER= 229 TAXON=108 TEXT='No evidence of correspondence [@ConwayMorris2010]'; + TEXT CHARACTER= 229 TAXON=109 TEXT='No evidence of correspondence [@Smith2015]'; + TEXT CHARACTER= 229 TAXON=110 TEXT='Prominent quincunx [@Huang2004]'; + TEXT CHARACTER= 229 TAXON=123 TEXT='Alignment similar between rings but number of spines not consistent, so not forming rows along the trunk [@Han2007]'; + TEXT CHARACTER= 229 TAXON=126 TEXT='Difficult to evaluate'; + TEXT CHARACTER= 230 TAXON=108 TEXT='Seemingly represented by polygonal texture [e.g. @ConwayMorris2010 fig, 6d]'; + TEXT CHARACTER= 230 TAXON=110 TEXT='Seemingly absent but SEM required to verify'; + TEXT CHARACTER= 230 TAXON=111 TEXT='Platelets considered absent [@Hu2008], but do seem to be evident (subtly) in figures; we attribute their diminished prominence to the manner of preservation.'; + TEXT CHARACTER= 230 TAXON=117 TEXT='Smaller plates irregularly dispersed [@GarciaBellido2013]'; + TEXT CHARACTER= 230 TAXON=118 TEXT='Platelets not preserved, in contrast to co-occurring Wudingscolex [@Hu2012]. Larter plates (''protruberences'') are present [@Hu2012].'; + TEXT CHARACTER= 230 TAXON=119 TEXT='PLatelets present [@Yang2020]'; + TEXT CHARACTER= 232 TAXON=1 TEXT='Paired spines at anterior of lorica, plus pair at posterior in larger ?semaphront [@Maas2009aap]'; + TEXT CHARACTER= 232 TAXON=15 TEXT='''Small spines'' [@Zhang2015]'; + TEXT CHARACTER= 232 TAXON=28 TEXT='Various setae and tubes [@Rucci2020z]'; + TEXT CHARACTER= 232 TAXON=38 TEXT='''Thorns'' [@Bolek2013]'; + TEXT CHARACTER= 232 TAXON=95 TEXT='Not obviously apparent [@Maas2007ppp], but quality of preservation insufficient to determine with confidence'; + TEXT CHARACTER= 232 TAXON=97 TEXT='Ambiguous: not reported, but preservation does not exclude the presence of diminutive elements'; + TEXT CHARACTER= 232 TAXON=98 TEXT='Interpreted as present based on mid-trunk sclerites with setal traces [@Ma2014jp]'; + TEXT CHARACTER= 232 TAXON=135 TEXT='Specialized spines borne on papillae [@Maas2007csb]'; + TEXT CHARACTER= 232 TAXON=136 TEXT='Individual sclerites present on trunk [@Zhang2016]'; + TEXT CHARACTER= 232 TAXON=177 TEXT='Euarthropod claws are interpreted as specializations of the appendage sclerotization rather than homologues of epidermal sclerites'; + TEXT CHARACTER= 232 TAXON=178 TEXT='Euarthropod claws are interpreted as specializations of the appendage sclerotization rather than homologues of epidermal sclerites'; + TEXT CHARACTER= 232 TAXON=179 TEXT='Euarthropod claws are interpreted as specializations of the appendage sclerotization rather than homologues of epidermal sclerites'; + TEXT CHARACTER= 232 TAXON=180 TEXT='Euarthropod claws are interpreted as specializations of the appendage sclerotization rather than homologues of epidermal sclerites'; + TEXT CHARACTER= 233 TAXON=95 TEXT='Not obviously apparent [@Maas2007ppp], but quality of preservation insufficient to determine with confidence'; + TEXT CHARACTER= 234 TAXON=10 TEXT='Small structures could be setae or papillae [@Maas2007]'; + TEXT CHARACTER= 234 TAXON=95 TEXT='Not obviously apparent [@Maas2007ppp], but quality of preservation insufficient to determine with confidence'; + TEXT CHARACTER= 234 TAXON=117 TEXT='The isolated small sclerites [@GarciaBellido2013] are treated as microplates rather than sclerites.'; + TEXT CHARACTER= 236 TAXON=15 TEXT='Occurring in irregularly spaced bilateral pairs [@Zhang2015]'; + TEXT CHARACTER= 236 TAXON=25 TEXT='The lorica field does not comprise enlarged plicae [@Neves2014]'; + TEXT CHARACTER= 236 TAXON=121 TEXT='Present [@Han2007app; @Steiner2012]'; + TEXT CHARACTER= 236 TAXON=123 TEXT='Given the possible presence of palaeoscolecid-like plates between the spine rows [@Han2007pr, fig. 1.8], it is possible that the spines are best interpreted as enlarged sclerites. We code these as ambiguous pending further information on Tylotites.'; + TEXT CHARACTER= 236 TAXON=126 TEXT='Paucipodia [@Chen1995trse] and Aysheaia [@Liu2014ppp, fig. 1] have been reported to bear subtle sub-circular specializations, but these putative structures in fact represent flattened appendages [@Hou2004; @Yang2015].'; + TEXT CHARACTER= 236 TAXON=127 TEXT='Impressions of the dorsal and ventral surfaces are interpreted as evident on the single specimen; neither surface shows evidence of epidermal specializations [@Dzik2011]'; + TEXT CHARACTER= 236 TAXON=129 TEXT='Coded as present based on the shield-like specializations associated with each leg pair [@Ma2014jsp, fig. 2].'; + TEXT CHARACTER= 236 TAXON=130 TEXT='Paucipodia [@Chen1995trse] and Aysheaia [@Liu2014ppp, fig. 1] have been reported to bear subtle sub-circular specializations, but these putative structures in fact represent flattened appendages [@Hou2004; @Yang2015].'; + TEXT CHARACTER= 236 TAXON=137 TEXT='Ambiguous, as the dorsal surface is not visible in the available material [@Haug2012cb]'; + TEXT CHARACTER= 236 TAXON=141 TEXT='Not evident [@Howard2020]'; + TEXT CHARACTER= 236 TAXON=143 TEXT='We interpret the ''gut diverculata'' described by @Caron2017 to be dorsal epidermal evaginations. As @Caron2017 point out, these features are located above limb pairs. Their additional file 1 shows these features are pointed dorsally and rounded ventrally, and exhibit more consistent shape. They also overprint the annulations in their additional file 4 panel c, consistent with being an external feature - and where they do so they exhibit a well-defined gut margin. These features have a paired appearance in Additional file 2 panel b.^n^nFurthermore, elemental mapping in their fig 1C shows no hint of a gut characterization in the posteriormost element (see also panel D, E in their additional file 6). Instead, they are associated with elevated concentrations of carbon, as are the claws; see the carbon distribution in additional file 6 panel A. They extend beyond the body wall (additional figure 9), indicating an external feature.'; + TEXT CHARACTER= 236 TAXON=157 TEXT='Not evident in well-preserved specimens of @Budd1998ar or @Young2017.'; + TEXT CHARACTER= 237 TAXON=11 TEXT='Single medial row (albeit with seemingly irregular spacing) [@Zhang2022]'; + TEXT CHARACTER= 237 TAXON=13 TEXT='Only a single large sclerite is known; the opposite side of the trunk is missing [@Liu2019]'; + TEXT CHARACTER= 237 TAXON=15 TEXT='Large sclerites occurs in pairs, with front of large sclerites aligned with annulations 1, 5, 10, 15, 19; pair two consistently more ventral than other pairs [@Zhang2015]'; + TEXT CHARACTER= 238 TAXON=11 TEXT='The ''caudal'' sclerites seem to occur on the dorsal surface (as defined by the central columns of enlarged sclerites) and do not obviously surround the anus [@Zhang2022]. They are thus treated as belonging to dorsal bands of sclerites. They seem to be slightly offset from the central sclerites; hence this state is coded ambiguous to denote two sclerites if the lateral sclerites form separate bands, or three sclerites per row if the sclerites are lateral to the medial sclerite in a single band.'; + TEXT CHARACTER= 238 TAXON=13 TEXT='Only a single large sclerite is known; as the opposite side of the trunk is missing, it is possible that a second sclerite is present [@Liu2019]'; + TEXT CHARACTER= 238 TAXON=91 TEXT='Halobiotus (Eutardigrada) has paired epidermal specialisations (depressions), represented by pits that serve as muscle attachment sites [@Halberg2009; @Marchioro2013]'; + TEXT CHARACTER= 238 TAXON=131 TEXT='Single element, potentially representing two fused elements [@Strausfeld2022]'; + TEXT CHARACTER= 238 TAXON=134 TEXT='Two papillae reported per leg pair, with additional in between leg pairs [@Siveter2018]'; + TEXT CHARACTER= 238 TAXON=145 TEXT='Three [@Caron2020]'; + TEXT CHARACTER= 239 TAXON=14 TEXT='Symmetrical pairs of enlarged sclerites on annulae 7 and 9; single medial element on annulus 12 [@Shao2020]'; + TEXT CHARACTER= 239 TAXON=15 TEXT='Every three to five annulations'; + TEXT CHARACTER= 239 TAXON=121 TEXT='Every annulation [@Shi2022]'; + TEXT CHARACTER= 239 TAXON=122 TEXT='On every other annulation [@Shi2022]'; + TEXT CHARACTER= 240 TAXON=11 TEXT='First sclerite widely separated from later bands [@Zhang2022]'; + TEXT CHARACTER= 240 TAXON=120 TEXT='Sub-regular [@ThisStudy]'; + TEXT CHARACTER= 240 TAXON=122 TEXT='Alternate annulations [@Shi2022]'; + TEXT CHARACTER= 242 TAXON=11 TEXT='Differing [@Zhang2022]'; + TEXT CHARACTER= 242 TAXON=125 TEXT='Not all dorsal specialisations present, as trunk incomplete in @Budd1998p, therefore coded as ambiguous (applicable).'; + TEXT CHARACTER= 242 TAXON=135 TEXT='Orstenotubulus has prominent spines and buttresses above some leg pairs, but these are profoundly diminished above others [@Maas2007csb].'; + TEXT CHARACTER= 244 TAXON=11 TEXT='Wider than tall (at least anteriorly) [@Zhang2022]'; + TEXT CHARACTER= 244 TAXON=134 TEXT='Equant [@Siveter2018], so coded ambiguous'; + TEXT CHARACTER= 245 TAXON=13 TEXT='Truncated but clearly evident originally [@Liu2019]'; + TEXT CHARACTER= 245 TAXON=121 TEXT='Present [@Shi2022]'; + TEXT CHARACTER= 245 TAXON=124 TEXT='Dorsal spine [@Jaeger2010]'; + TEXT CHARACTER= 245 TAXON=125 TEXT='Seemingly absent [@Budd1998p]; no central ''pore'' as in Xenusion [@Jaeger2010]'; + TEXT CHARACTER= 245 TAXON=131 TEXT='Interpreted as having a pointed apex [@Hou1991; @Liu2014], but whilst certain aspects of the spine have an angular silhouette, there is no distinct pointed apex [@Strausfeld2022]'; + TEXT CHARACTER= 245 TAXON=135 TEXT='Ambiguously preserved [@Maas2007]'; + TEXT CHARACTER= 246 TAXON=10 TEXT='Gently curved posteriad [@Maas2007]'; + TEXT CHARACTER= 246 TAXON=11 TEXT='Modest curvature [@Zhang2022]'; + TEXT CHARACTER= 246 TAXON=12 TEXT='Limited curvature, if any [@Liu2019]'; + TEXT CHARACTER= 246 TAXON=120 TEXT='Absent in Cricocosmia n. sp. [@ThisStudy]'; + TEXT CHARACTER= 246 TAXON=121 TEXT='Simple cones [@Shi2022]'; + TEXT CHARACTER= 246 TAXON=128 TEXT='Figs 1B.1, 2C in @Liu2008app show O. ferox with a straight distal termination in epidermal evagination.'; + TEXT CHARACTER= 246 TAXON=133 TEXT='@Liu2008app figures 3a, b depict a curved morphology; however, as fossil photographs do not convincingly demonstrate this interpretation, we code as ambiguous.'; + TEXT CHARACTER= 246 TAXON=138 TEXT='The spines of Hallucigenia sparsa are gently curved [@Smith2014; @Smith2015].'; + TEXT CHARACTER= 247 TAXON=17 TEXT='Anterior margin of lorica plate straight'; + TEXT CHARACTER= 247 TAXON=18 TEXT='Lorica plates rectangular with straight margin and round corners'; + TEXT CHARACTER= 247 TAXON=19 TEXT='Spike present on anterior margin of lorical plate'; + TEXT CHARACTER= 247 TAXON=95 TEXT='Spikes on anterior lorical plate [@Maas2007ppp]'; + TEXT CHARACTER= 247 TAXON=125 TEXT='Not angular [@Budd1998p]'; + TEXT CHARACTER= 248 TAXON=124 TEXT='Substantial relief, with spines'; + TEXT CHARACTER= 248 TAXON=134 TEXT='Not obviously sclerotized [@Siveter2018]'; + TEXT CHARACTER= 250 TAXON=10 TEXT='Spines/setae but no ornament [@Maas2007]'; + TEXT CHARACTER= 250 TAXON=18 TEXT='Honeycomb ornament on lorical plates [@Neves2016, fig. 17]'; + TEXT CHARACTER= 250 TAXON=19 TEXT='Honeycomb pattern in Higgins larva lorica plates [@Neves2016za, fig. 17] but unornamented in adult [@Neves2016za]'; + TEXT CHARACTER= 250 TAXON=69 TEXT='Actinarctus sclerites exhibit a polygonal ornament, but the indentations do not penetrate the sclerites [@Marchioro2013].'; + TEXT CHARACTER= 250 TAXON=95 TEXT='Seemingly unornamented [@Maas2007ppp]'; + TEXT CHARACTER= 250 TAXON=128 TEXT='@Liu2008app, fig. 2B, shows a net like texture of sclerite ornaments for O. ferox, similar to those described by @Topper2013 in Onychodictyon sp. plates'; + TEXT CHARACTER= 250 TAXON=131 TEXT='Regular polygonal pattern [@Liu2014], but undetermined whether these polygons penetrate the sclerites or whether their distribution corresponds to Microdictyon / Onychodictyon plates.'; + TEXT CHARACTER= 250 TAXON=133 TEXT='Coded ambiguous as the texture is difficult to discern from the figures of @Liu2008app'; + TEXT CHARACTER= 250 TAXON=134 TEXT='Thanahita exhibits a distinct tuft-like morphology [@Siveter2018]'; + TEXT CHARACTER= 250 TAXON=139 TEXT='Unclear from @Hou1995zjls, but clearly not inapplicable.'; + TEXT CHARACTER= 250 TAXON=140 TEXT='A honeycomb-like pattern that seems to be a surface ornament, but conceivably forms net-like holes [@Steiner2012]'; + TEXT CHARACTER= 250 TAXON=144 TEXT='"The dorsolateral spines of Collinsium have a distinctive punctate-like ornamentation similar to that of H. hongmeia" [@Yang2015]; hence coded per that taxon.^n'; + TEXT CHARACTER= 251 TAXON=69 TEXT='Absent [@Marchioro2013]'; + TEXT CHARACTER= 251 TAXON=121 TEXT='Absent [@Shi2022]'; + TEXT CHARACTER= 251 TAXON=128 TEXT='Likely evident; not obvious in articulated material [@Steiner2012, fig. 8], consistent with the diminutive stature of the feature in isolated plates [@Topper2013]'; + TEXT CHARACTER= 251 TAXON=133 TEXT='Presumed present based on presence in disarticulated Onychodictyon sp. [@Topper2013]'; + TEXT CHARACTER= 251 TAXON=138 TEXT='Whilst it is conceivable that the spinose ornament on H. fortis spines corresponds to bosses of an originally net-like sclerite, we do not consider there to be sufficient evidence to treat these as homologous here.'; + TEXT CHARACTER= 251 TAXON=140 TEXT='Seemingly evident as carbon-enriched spots in elemental maps [@Steiner2012, fig. 7H]'; + TEXT CHARACTER= 254 TAXON=152 TEXT='Coded ambiguous: not present at larval stage [@Smith2023n], but dorsal extensions of the haemolymph system are plausible precursors of a feature that may be added in an adult stage with different metabolic requirements.'; + TEXT CHARACTER= 257 TAXON=152 TEXT='Coded as absent, reflecting the absence of any indication of flaps, despite expression of appendages [@Smith2023n] – though it remains possible that these structures were not expressed until a later instar.'; + TEXT CHARACTER= 257 TAXON=156 TEXT='Setal blades are expressed as wrinkles on the dorsal flaps of gilled lobopodians [@VanRoy2015]'; + TEXT CHARACTER= 257 TAXON=157 TEXT='Setal blades are expressed as wrinkles on the dorsal flaps of gilled lobopodians [@VanRoy2015]'; + TEXT CHARACTER= 257 TAXON=166 TEXT='Coded as absent by @VanRoy2015'; + TEXT CHARACTER= 257 TAXON=168 TEXT='Only a single series of lateral flaps is reconstructed [@Moysiuk2019]'; + TEXT CHARACTER= 257 TAXON=169 TEXT='Coded ambiguous: the presence of dorsal and ventral flaps is tentatively interpreted by @VanRoy2015, though @Moysiuk2019 consider them absent'; + TEXT CHARACTER= 257 TAXON=171 TEXT='@VanRoy2015 identify "clear evidence" of two sets of flaps, though @Moysiuk2019 consider the evidence equivocal. We thus take the conservative position of coding this taxon ambiguous.'; + TEXT CHARACTER= 258 TAXON=166 TEXT='Coded as present to reflect proposed homology of gnathobasic endites with those of euarthropods [@Cong2017]'; + TEXT CHARACTER= 259 TAXON=168 TEXT='Crossing the body [@Moysiuk2019]'; + TEXT CHARACTER= 260 TAXON=127 TEXT='Appendages not completely preserved [@Dzik2011], so coded ambiguous'; + TEXT CHARACTER= 260 TAXON=128 TEXT='Neither entirely slender and cylindrical or conical'; + TEXT CHARACTER= 260 TAXON=129 TEXT='Minimal tapering [@Ou2018]'; + TEXT CHARACTER= 260 TAXON=130 TEXT='Figure 5a in @Vannier2017 establishes that lobopods, when oriented parallel to bedding, are cylindrical.'; + TEXT CHARACTER= 260 TAXON=144 TEXT='Ambiguous [@Yang2015]'; + TEXT CHARACTER= 260 TAXON=153 TEXT='Preservation inadequate to distinguish [@Liu2007az]'; + TEXT CHARACTER= 261 TAXON=127 TEXT='Coded as uncertain because its limbs are poorly preserved [@Dzik2011]. '; + TEXT CHARACTER= 261 TAXON=129 TEXT='Spines are treated as equivalent to t'; + TEXT CHARACTER= 261 TAXON=133 TEXT='We code as O. gracilis as uncertain as its longitudinal series of dot-like structures [@Liu2008csb fig. 2A6] could indicate an organization of appendicules similar to those of O. ferox [see @Ou2012, fig. 2a]. '; + TEXT CHARACTER= 261 TAXON=141 TEXT='Double series of Luolishania-like spines [@Howard2020]'; + TEXT CHARACTER= 261 TAXON=148 TEXT='Not distinct from possible trunk sclerites'; + TEXT CHARACTER= 261 TAXON=167 TEXT='Anomalocaris is treated as uncertain [@VanRoy2015]. '; + TEXT CHARACTER= 261 TAXON=169 TEXT='Absent [@VanRoy2015]'; + TEXT CHARACTER= 261 TAXON=171 TEXT='Absent [@VanRoy2015]'; + TEXT CHARACTER= 262 TAXON=55 TEXT='Heterotardigrades have a spine-like sensory organ on the trunk limbs. See character 36 in @Khim2023. '; + TEXT CHARACTER= 262 TAXON=143 TEXT='Two series of spines, arranged in chevrons [@Caron2017]'; + TEXT CHARACTER= 265 TAXON=124 TEXT='Appendage-parallel banding present [@Jaeger2010]'; + TEXT CHARACTER= 265 TAXON=126 TEXT='A small number of possible cases [@Whittington1975], but not convincingly demonstrated.'; + TEXT CHARACTER= 265 TAXON=141 TEXT='Seemingly present [@Howard2020]'; + TEXT CHARACTER= 265 TAXON=143 TEXT='Absent [@Caron2017]'; + TEXT CHARACTER= 265 TAXON=148 TEXT='Unclear whether spines borne on papillae'; + TEXT CHARACTER= 267 TAXON=125 TEXT='Ambiguous; sclerotized elements may account for the angular termination of the papillae'; + TEXT CHARACTER= 269 TAXON=120 TEXT='Coded as ambiguous: the potential homology between the pair of terminal hooks of Cricocosmia and the similarly-shaped claws on trunk appendages [@Steiner2012] is difficult to evaluate.'; + TEXT CHARACTER= 269 TAXON=124 TEXT='Seemingly absent [@Dzik1989; @Jaeger2010]'; + TEXT CHARACTER= 269 TAXON=126 TEXT='The lobopod claws of Aysheaia are sub-terminal; the lobopods extend beyond the claws [@Whittington1978]'; + TEXT CHARACTER= 269 TAXON=129 TEXT='The appendages terminate in sclerites of equivalent construction to those that adorn the rest of the appendage [@Liu2011; @Ma2014jsp; @Ou2018]. Because claws are likely homologous with trunk sclerites, we code this transformation series as present to reflect the possible homology with claws of other taxa.'; + TEXT CHARACTER= 269 TAXON=130 TEXT='Simple elongate claws [@Vannier2017]'; + TEXT CHARACTER= 269 TAXON=134 TEXT='Present [@Siveter2018]'; + TEXT CHARACTER= 269 TAXON=138 TEXT='Hallucigenia sparsa is coded with two claws as this is the state on most trunk limbs, even if a second claw is not evident on the posteriormost appendages [@Smith2015].'; + TEXT CHARACTER= 269 TAXON=141 TEXT='The "trunk spines" [@Howard2020] are interpreted as corresponding to terminal claws on vestigial trunk limbs'; + TEXT CHARACTER= 269 TAXON=142 TEXT='Coded as present (one claw) as this represents the state of its typical trunk limbs. Spinose elements on its anterior limbs do not exhibit a claw-like morphology and may represent cirri rather than claws.'; + TEXT CHARACTER= 269 TAXON=148 TEXT='The sclerotized ''pads'' [@Ou2011] are positionally and compositionally equivalent to claws in other taxa'; + TEXT CHARACTER= 269 TAXON=152 TEXT='As claws are not evident until a rather late stage of onychophoran development [@Walker2004], we cannot be confident that their absence in YKLP 12387 [@Smith2023n] reflects the adult condition.'; + TEXT CHARACTER= 269 TAXON=153 TEXT='Jianshanopodia [@Liu2006] and Megadictyon [@Liu2007az] are also coded as uncertain as the preservation of the type material does not allow the presence or absence of terminal claws to be confirmed. '; + TEXT CHARACTER= 269 TAXON=154 TEXT='Jianshanopodia [@Liu2006] and Megadictyon [@Liu2007az] are also coded as uncertain as the preservation of the type material does not allow the presence or absence of terminal claws to be confirmed. '; + TEXT CHARACTER= 269 TAXON=163 TEXT='Absent, following @Budd2012'; + TEXT CHARACTER= 269 TAXON=167 TEXT='Coded ambiguous, as there is no definitive information on the presence of lobopodous limbs or a second set of flaps [@VanRoy2015]. '; + TEXT CHARACTER= 269 TAXON=177 TEXT='Leanchoilia is coded as ambiguous for one or three claws to reflect the conflicting interpretations of @Garcia2007 and @Haug2012bmceb.'; + TEXT CHARACTER= 270 TAXON=91 TEXT='Eutardigrades have a two-branched claw with differing morphologies, however, the base of most claws appear enlarged [including Halobiotidae, Doryphoribiidae, Eohysibiidae, Rhichtersiidae; see @Gasiorek2019] hence we code this as present for this taxon.'; + TEXT CHARACTER= 270 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 270 TAXON=126 TEXT='Enlarged base; figured in supplementary material of @Smith2014'; + TEXT CHARACTER= 270 TAXON=130 TEXT='Paucipodia''s claws do not have an enlarged base [@Vannier2017]'; + TEXT CHARACTER= 270 TAXON=131 TEXT='No enlarged base [@Ramskold1998]'; + TEXT CHARACTER= 270 TAXON=133 TEXT='The claws of Onychodictyon gracilis appear to have an enlarged base [see @Liu2008app, fig 2A6], although few other claws have been described.'; + TEXT CHARACTER= 270 TAXON=138 TEXT='Hallucigenia''s claws do not have an enlarged base, with similar curvature through the length of the claw [@Smith2014].'; + TEXT CHARACTER= 270 TAXON=142 TEXT='Enlarged base in the claws of posterior lobopods [@Ma2009, figure 10]'; + TEXT CHARACTER= 270 TAXON=146 TEXT='Enlarged base [@Garcia2013]'; + TEXT CHARACTER= 270 TAXON=177 TEXT='No enlarged base [@Garcia2007]'; + TEXT CHARACTER= 270 TAXON=178 TEXT='see @Briggs1999'; + TEXT CHARACTER= 271 TAXON=62 TEXT='Neoarctus has sub-terminal claws. See @Fontoura2017.'; + TEXT CHARACTER= 271 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 271 TAXON=130 TEXT='@Vannier2017 (fig. 5a, 5b) indicate Paucipodia''s claws are sub-terminal; however, there is a possibility that this is taphonomic [cf. @Murdock2014], as the musculature attached to the claws may have shrunk relative to the cuticle, giving the false impression of sub-terminal claws. As previous studies describe the claws as terminal [@Hou2004], we code claw position as ambiguous.'; + TEXT CHARACTER= 272 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 277 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 277 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 277 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 277 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 277 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 277 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 277 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 277 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 277 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 278 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 278 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 278 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 278 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 278 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 278 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 278 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 278 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 278 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 279 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 279 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 279 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 279 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 279 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 279 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 279 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 279 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 279 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 280 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 280 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 280 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 280 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 280 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 280 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 280 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 280 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 280 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 281 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 281 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 281 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 281 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 281 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 281 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 281 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 281 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 281 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 282 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 282 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 282 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 282 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 282 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 282 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 282 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 282 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 282 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 283 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 283 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 283 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 283 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 283 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 283 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 283 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 283 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 283 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 284 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 284 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 284 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 284 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 284 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 284 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 284 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 284 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 284 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 285 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 285 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 285 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 285 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 285 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 285 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 285 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 285 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 285 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 286 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 286 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 286 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 286 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 286 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 286 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 286 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 286 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 286 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 287 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 287 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 287 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 287 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 287 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 287 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 287 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 287 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 287 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 288 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 288 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 288 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 288 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 288 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 288 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 288 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 288 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 288 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 289 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 289 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 289 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 289 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 289 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 289 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 289 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 289 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 289 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 290 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 290 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 290 TAXON=84 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 290 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 290 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 290 TAXON=90 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 290 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 290 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 290 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 291 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 291 TAXON=128 TEXT='A pair of claws occurs on each appendage [@Liu2008app]'; + TEXT CHARACTER= 291 TAXON=129 TEXT='Inapplicable as claws not yet differentiated from appendage sclerites'; + TEXT CHARACTER= 291 TAXON=130 TEXT='@Hou2004 report that each lobopod carries two claws, whereas @Vannier2017 only observe a single claw on the two complete and exceptionally well preserved appendages of ELI-JS0001A. We interpret this discrepancy as representing variation in claw number between appendages.'; + TEXT CHARACTER= 291 TAXON=131 TEXT='Cardiodictyon unambiguously has two claws on each leg [@Ramskold1998]'; + TEXT CHARACTER= 291 TAXON=132 TEXT='Two claws. Though @Hou1995zjls observe a single claw, they leave open the possibility of a second; @Liu2008app interpret the presence of two claws.'; + TEXT CHARACTER= 291 TAXON=133 TEXT='A pair of claws is evident in one appendage [@Liu2008app], and taken to represent the typical number.'; + TEXT CHARACTER= 291 TAXON=134 TEXT='One on some appendages, two on others [@Siveter2018]'; + TEXT CHARACTER= 291 TAXON=138 TEXT='Two claws on anterior trunk appendages, one on posterior [@Smith2015]'; + TEXT CHARACTER= 291 TAXON=140 TEXT='Single claw [@Steiner2012]'; + TEXT CHARACTER= 291 TAXON=142 TEXT='Only one claw is observed on the unmodified (i.e. posterior) trunk limbs [@Ma2009]. Spinose elements on anterior limbs do not exhibit a claw-like morphology and may represent cirri rather than claws.'; + TEXT CHARACTER= 291 TAXON=143 TEXT='The two "claws" on anterior limbs have the same shape and elemental composition as cirri [@Caron2017], so are not treated as homologous with claws. The posterior appendages each bear a single claw [@Caron2017]'; + TEXT CHARACTER= 291 TAXON=144 TEXT='No claws on anterior appendages; single claw on each posterior appendage [@Yang2015]'; + TEXT CHARACTER= 291 TAXON=146 TEXT='Single claw present on posterior appendages; claws are not apparent on cirrate anterior appendages [@Garcia2013]'; + TEXT CHARACTER= 291 TAXON=177 TEXT='Coded as ambiguous (one or three claws) to reflect the conflicting interpretations of @Garcia2007 and @Haug2012bmceb'; + TEXT CHARACTER= 292 TAXON=120 TEXT='Equal number [@Dhungana2023]'; + TEXT CHARACTER= 292 TAXON=128 TEXT='A pair of claws occurs on each appendage [@Liu2008app]'; + TEXT CHARACTER= 292 TAXON=130 TEXT='@Hou2004 report that each lobopod carries two claws, whereas @Vannier2017 only observe a single claw on the two complete and exceptionally well preserved appendages of ELI-JS0001A. We interpret this discrepancy as representing variation in claw number between appendages.'; + TEXT CHARACTER= 292 TAXON=131 TEXT='Cardiodictyon unambiguously has two claws on each leg [@Ramskold1998]'; + TEXT CHARACTER= 292 TAXON=133 TEXT='Only one appendage is adequately preserved to identify claws [@Liu2008app]'; + TEXT CHARACTER= 292 TAXON=134 TEXT='One on some appendages, two on others [@Siveter2018]'; + TEXT CHARACTER= 292 TAXON=138 TEXT='Two claws on anterior trunk appendages, one on posterior [@Smith2015]'; + TEXT CHARACTER= 292 TAXON=143 TEXT='Two claws on anterior limbs, one on posterior [@Caron2017]'; + TEXT CHARACTER= 292 TAXON=146 TEXT='Single claw presumed on all posterior appendages'; + TEXT CHARACTER= 293 TAXON=52 TEXT='Euperipatoides claws are identical on trunk limbs, although the jaw elements are differentiated [@Smith2014].'; + TEXT CHARACTER= 293 TAXON=85 TEXT='Treated as similar, after @Mapalo2024cb [contra @Kihm2023]'; + TEXT CHARACTER= 293 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 293 TAXON=126 TEXT='All seven claws are identical [@Whittington1978].'; + TEXT CHARACTER= 293 TAXON=128 TEXT='Onychodictyon ferox has a large and a small claw [@Steiner2012, fig. 8] '; + TEXT CHARACTER= 293 TAXON=130 TEXT='Not visibly differentiated [@Hou2004]'; + TEXT CHARACTER= 293 TAXON=138 TEXT='Not visibly differentiated [@Smith2015]'; + TEXT CHARACTER= 294 TAXON=152 TEXT='Ambiguous as distal foot does not arise in Onychophora until a late stage in development [@Walker2004]'; + TEXT CHARACTER= 297 TAXON=166 TEXT='"Oblique veins" @Chen1994 interpreted as strengthening rays.'; + TEXT CHARACTER= 297 TAXON=169 TEXT='Treated as ambiguous by @Moysiuk2019'; + TEXT CHARACTER= 297 TAXON=173 TEXT='Present in L. trilobus [@Cong2016]; possibly reflected by striations in L. unguispinus?'; + TEXT CHARACTER= 298 TAXON=168 TEXT='Relatively even [@Moysiuk2019]'; + TEXT CHARACTER= 300 TAXON=131 TEXT='The first three limbs are diminutive [@Strausfeld2022]'; + TEXT CHARACTER= 300 TAXON=137 TEXT='Single anterior pair reduced in size [@Haug2012cb]; as it is unclear whether this represents a homologous reduction, we code as ambiguous'; + TEXT CHARACTER= 300 TAXON=168 TEXT='First three flaps reduced [@Moysiuk2019]'; + TEXT CHARACTER= 306 TAXON=120 TEXT='Present [@Dhungana2023]'; + TEXT CHARACTER= 306 TAXON=127 TEXT='Siberion is scored as uncertain as it is difficult to distinguish the possible body termination from a posterior leg or pair of legs [@Dzik2011].'; + TEXT CHARACTER= 306 TAXON=132 TEXT='Present [@Chen1995bnmns]'; + TEXT CHARACTER= 306 TAXON=134 TEXT='Conical extension present [@Siveter2018]'; + TEXT CHARACTER= 306 TAXON=137 TEXT='Absent [@Haug2012cb]'; + TEXT CHARACTER= 306 TAXON=138 TEXT='Absent [@Smith2015]'; + TEXT CHARACTER= 306 TAXON=139 TEXT='H. fortis and H. hongmeia are coded as ambiguous, as the preservation is insufficiently clear to determine whether possible "posterior extensions" correspond to the trunk or to legs [@Hou1995zjls; @Steiner2012; @Liu2014ppp]'; + TEXT CHARACTER= 306 TAXON=140 TEXT='H. fortis and H. hongmeia are coded as ambiguous, as the preservation is insufficiently clear to determine whether possible "posterior extensions" correspond to the trunk or to legs [@Hou1995zjls; @Steiner2012; @Liu2014ppp]'; + TEXT CHARACTER= 306 TAXON=141 TEXT='Pear-shaped posterior bulge [@Howard2020]'; + TEXT CHARACTER= 306 TAXON=142 TEXT='Although Luolishania is described as bearing a protruding posterior termination, this is not unambiguously evident in specimens or camera lucida images; this taxon is thus coded as ambiguous [@Liu2008csb; @Ma2009].'; + TEXT CHARACTER= 306 TAXON=143 TEXT='Absent [@Caron2017]'; + TEXT CHARACTER= 306 TAXON=152 TEXT='The posterior appendages are incompletely formed at this developmental stage [@Smith2023n], so it is impossible to evaluate their condition in mature individuals.'; + TEXT CHARACTER= 306 TAXON=154 TEXT='We code this character as absent in Kerygmachela [@Budd1993; @Budd1998trse], Jianshanopodia [@Liu2006] and Anomalocaris [@Daley2014] as their tails likely represent modified appendages.'; + TEXT CHARACTER= 306 TAXON=156 TEXT='We code this character as absent in Kerygmachela [@Budd1993; @Budd1998trse], Jianshanopodia [@Liu2006] and Anomalocaris [@Daley2014] as their tails likely represent modified appendages.'; + TEXT CHARACTER= 306 TAXON=157 TEXT='We score Pambdelurion as uncertain because its posterior trunk is poorly known [@Budd1998ar].'; + TEXT CHARACTER= 306 TAXON=163 TEXT='The trunk of Opabinia extends further than the lobopodous limbs [@DhunganaForthcoming].'; + TEXT CHARACTER= 306 TAXON=167 TEXT='We code this character as absent in Kerygmachela [@Budd1993; @Budd1998trse], Jianshanopodia [@Liu2006] and Anomalocaris [@Daley2014] as their tails likely represent modified appendages.'; + TEXT CHARACTER= 308 TAXON=157 TEXT='We score Pambdelurion as uncertain because its posterior trunk is poorly known [@Budd1998ar].'; + TEXT CHARACTER= 308 TAXON=166 TEXT='Amplectobelua "resembles Anomalocaris in the number of lateral flaps, the flap venation, tail fan, and long furcae" [@Chen1994]'; + TEXT CHARACTER= 309 TAXON=120 TEXT='Following @Dhungana2023'; + TEXT CHARACTER= 309 TAXON=133 TEXT='Uncertain [@Liu2008app]'; + TEXT CHARACTER= 309 TAXON=138 TEXT='The claws of Hallucigenia sparsa seem to be oriented in the same direction on all appendage pairs [@Smith2015].'; + TEXT CHARACTER= 309 TAXON=143 TEXT='Claw direction on posteriormost pair (appendage 9) matches that of adjacent appendages (7 and 8) [@Caron2017]. @Caron2017 assert that the posteriormost two or three claws of Hallucigenia and Collinsium are directed in a different direction to those of other trunk limbs, citing references that do not obviously support this assertion.'; + TEXT CHARACTER= 309 TAXON=151 TEXT='Uncertain [@Maas2007csb]'; + TEXT CHARACTER= 309 TAXON=157 TEXT='We score Pambdelurion as uncertain because its posterior trunk is poorly known [@Budd1998ar].'; + TEXT CHARACTER= 310 TAXON=52 TEXT='Onychophora are scored as undifferentiated, as the posteriormost appendages are appendages are lost, not structurally differentiated [@Mayer2005].'; + TEXT CHARACTER= 310 TAXON=53 TEXT='Onychophora are scored as undifferentiated, as the posteriormost appendages are appendages are lost, not structurally differentiated [@Mayer2005].'; + TEXT CHARACTER= 310 TAXON=54 TEXT='Onychophora are scored as undifferentiated, as the posteriormost appendages are appendages are lost, not structurally differentiated [@Mayer2005].'; + TEXT CHARACTER= 310 TAXON=120 TEXT='Undifferentiated [@Dhungana2023]'; + TEXT CHARACTER= 310 TAXON=148 TEXT='The posterior filaments [@Ou2011] are treated as modified appendages, by analogy with Kerygmachela'; + TEXT CHARACTER= 310 TAXON=149 TEXT='The preservation is inadequate to evaluate this feature.'; + TEXT CHARACTER= 310 TAXON=154 TEXT='We score Jianshanopodia [@Liu2006] as present because the lateral extensions of the tail fan likely correspond to a modified pair of appendages. '; + TEXT CHARACTER= 310 TAXON=157 TEXT='We score Pambdelurion as uncertain because its posterior trunk is poorly known [@Budd1998ar].'; + TEXT CHARACTER= 310 TAXON=166 TEXT='Amplectobelua "resembles Anomalocaris in the number of lateral flaps, the flap venation, tail fan, and long furcae" [@Chen1994]'; + TEXT CHARACTER= 310 TAXON=168 TEXT='Several pairs of lobes incorporated into tail fan [@Moysiuk2019]'; + TEXT CHARACTER= 310 TAXON=169 TEXT='Hurdia and Schinderhannes bear a single flap-like appendage on the posterior end [@Daley2009; @Kuhl2009].'; + TEXT CHARACTER= 310 TAXON=174 TEXT='Hurdia and Schinderhannes bear a single flap-like appendage on the posterior end [@Daley2009; @Kuhl2009].'; + TEXT CHARACTER= 311 TAXON=151 TEXT='The Siberian Orsten tardigrade is scored as having a reduced posteriormost appendage pair based on the vestigial rudiment present on its posteroventral body region [@Maas2001].'; + TEXT CHARACTER= 311 TAXON=154 TEXT='The last appendage pair of Jianshanopodia is modified into a set of lateral flaps, which form a tail fan together with the flattened terminal portion of the body [@Liu2006]'; + TEXT CHARACTER= 311 TAXON=156 TEXT='The paired tail rami of Kerygmachela [@Budd1993; @Budd1998trse] likely represent modified appendages. '; + TEXT CHARACTER= 311 TAXON=157 TEXT='We score Pambdelurion as uncertain because its posterior trunk is poorly known [@Budd1998ar].'; + TEXT CHARACTER= 311 TAXON=166 TEXT='Amplectobelua "resembles Anomalocaris in the number of lateral flaps, the flap venation, tail fan, and long furcae" [@Chen1994]'; + TEXT CHARACTER= 312 TAXON=157 TEXT='We score Pambdelurion as uncertain because its posterior trunk is poorly known [@Budd1998ar].'; + TEXT CHARACTER= 312 TAXON=163 TEXT='Rami [@Pates2022]'; + TEXT CHARACTER= 312 TAXON=164 TEXT='Tail fan composed of seven pairs of elongate blades, and a pair of caudal rami [@Pates2022]'; + TEXT CHARACTER= 312 TAXON=166 TEXT='Following @Pates2021'; + TEXT CHARACTER= 312 TAXON=173 TEXT='Following @Pates2021'; + TEXT CHARACTER= 312 TAXON=174 TEXT='Following @Pates2021'; + TEXT CHARACTER= 313 TAXON=175 TEXT='Tail flukes appear more paddle-like than blade like [@Yang2013, supplementary figure 4b]'; + TEXT CHARACTER= 314 TAXON=95 TEXT='Dumbbell shape [@Maas2007ppp] indicates presence of lorica, and is not obviously equivalent to other posterior bulbs; coded ambiguous in order to be conservative.^n'; + TEXT CHARACTER= 314 TAXON=98 TEXT='Bulbous posterior trunk [@Ma2014]'; + TEXT CHARACTER= 315 TAXON=108 TEXT='A 6 mm terminal extension beyond the segmented body is compared to the bursa of Ottoia [@ConwayMorris2010]'; + TEXT CHARACTER= 319 TAXON=103 TEXT='Most likely single, but specimens are indecisive [@ConwayMorris1977]'; + TEXT CHARACTER= 320 TAXON=103 TEXT='Scored as dorso-medial by @Wills2012, but unclear how this can be determined from available material [@Schram1973; @ConwayMorris1977]'; + TEXT CHARACTER= 321 TAXON=103 TEXT='Considered smooth by @ConwayMorris1977'; + TEXT CHARACTER= 322 TAXON=40 TEXT='Present [@Kulikov1998rjn]'; + TEXT CHARACTER= 322 TAXON=41 TEXT='Present [@Luduc2016n]'; + TEXT CHARACTER= 323 TAXON=1 TEXT='Ambiguous: absent in smaller form, but present in larger form, a possible semaphront [@Maas2009aap]; may also be present in unknown adult.'; + TEXT CHARACTER= 323 TAXON=7 TEXT='Pair? of extended straight spines in NMNH198604'; + TEXT CHARACTER= 323 TAXON=10 TEXT='The single pair of ventroterminal outgrowths extending into posterior spines or setae are conceivably homologous with loriciferan ''toes''.'; + TEXT CHARACTER= 323 TAXON=11 TEXT='The lateral spines are treated as dorsal; they are not constrained to the posteriormost end of the organism, but occur on the two posterior ''segments'' [@Zhang2022]'; + TEXT CHARACTER= 323 TAXON=13 TEXT='Specimen incomplete'; + TEXT CHARACTER= 323 TAXON=14 TEXT='Absent, presuming that tongue-like structure denotes end of body [@Shao2020]'; + TEXT CHARACTER= 323 TAXON=15 TEXT='Eokinorhynchus has two pairs of caudal spines, distinguishing them from the series of lateral spines on the dorsal trunk (Zhang et al. 2015).^n'; + TEXT CHARACTER= 323 TAXON=16 TEXT='No posterior structures present in Eopriapulites (Shao et al. 2016)'; + TEXT CHARACTER= 323 TAXON=38 TEXT='Scored as absent, despite presence in larvae [e.g. @Marek2010], to ensure consistent coding with fossil taxa'; + TEXT CHARACTER= 323 TAXON=44 TEXT='Acanthopriapulus is covered in a profusion of hooks [@Land1970]; tail hooks are not distinguished from other trunk hooks, so the character is scored as ambiguous.^n'; + TEXT CHARACTER= 323 TAXON=45 TEXT='Two present in Halicryptus [@Shirley1999]^n'; + TEXT CHARACTER= 323 TAXON=49 TEXT='Tail hooks are absent in Priapulus; it is possible that the posterior warts correspond to these structures, but I was unable to find any literature that documented their distribution.'; + TEXT CHARACTER= 323 TAXON=96 TEXT='Preservation inadequate to determine whether vestigial features may be present [@Peel2010]'; + TEXT CHARACTER= 323 TAXON=113 TEXT='We know of no specimens of Palaeoscolex piscatorum that document the posterior end; it’s not clear how @Wills2012 coded hooks as present.'; + TEXT CHARACTER= 325 TAXON=10 TEXT='Approximately 20%'; + TEXT CHARACTER= 325 TAXON=111 TEXT='Not clearly figured or described, but sketch indicates small size [@Hu2008]'; + TEXT CHARACTER= 325 TAXON=118 TEXT='Narrow but elongated [@Hu2012]'; + TEXT CHARACTER= 328 TAXON=16 TEXT='Seemingly absent [@Shao2016]'; + TEXT CHARACTER= 328 TAXON=44 TEXT='Present [@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 330 TAXON=21 TEXT='Four small warts [e.g. @Gad2005mbr]'; + TEXT CHARACTER= 330 TAXON=22 TEXT='Six posterior warts [@Gad2005za]'; + TEXT CHARACTER= 330 TAXON=44 TEXT='Absent [@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 332 TAXON=137 TEXT='A longitudinal arrangement of musculature is suggested by the longitudinal wrinkling [@Haug2012cb]'; + TEXT CHARACTER= 332 TAXON=167 TEXT='Present [@Daley2014]'; + TEXT CHARACTER= 332 TAXON=173 TEXT='Present [@Cong2014]'; + TEXT CHARACTER= 332 TAXON=176 TEXT='Coded as present in Fuxianhuia based on a probable fuxianhuiid with muscle tissue from Kaili [@Zhu2004]'; + TEXT CHARACTER= 332 TAXON=179 TEXT='The metameric distribution of musculature in artiopodans is inferred by comparison with Campanamuta [@Budd2011].'; + TEXT CHARACTER= 332 TAXON=180 TEXT='The metameric distribution of musculature in artiopodans is inferred by comparison with Campanamuta [@Budd2011]'; + TEXT CHARACTER= 333 TAXON=28 TEXT='One pair of bundles of ventral and dorsal longitudinal muscles extending between the pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 333 TAXON=29 TEXT='One pair of bundles of ventral and dorsal longitudinal muscles extending between the pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 333 TAXON=30 TEXT='One pair of bundles of ventral and dorsal longitudinal muscles extending between the pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 333 TAXON=31 TEXT='One pair of bundles of ventral and dorsal longitudinal muscles extending between the pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 333 TAXON=32 TEXT='One pair of bundles of ventral and dorsal longitudinal muscles extending between the pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 333 TAXON=33 TEXT='One pair of bundles of ventral and dorsal longitudinal muscles extending between the pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 333 TAXON=34 TEXT='One pair of bundles of ventral and dorsal longitudinal muscles extending between the pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 333 TAXON=35 TEXT='One pair of bundles of ventral and dorsal longitudinal muscles extending between the pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 333 TAXON=36 TEXT='One pair of bundles of ventral and dorsal longitudinal muscles extending between the pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 333 TAXON=37 TEXT='One pair of bundles of ventral and dorsal longitudinal muscles extending between the pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 333 TAXON=156 TEXT='Well-developed longitudinal muscles "appear to sheath the entire body" [@Young2017]'; + TEXT CHARACTER= 333 TAXON=157 TEXT='Pambdelurion exhibits longitudinal peripheral musculature [@Budd1998l; @Young2017].'; + TEXT CHARACTER= 333 TAXON=179 TEXT='An axial distribution of longitudinal muscle is inferred in artiopodans by comparison with Campanamuta [@Young2017].'; + TEXT CHARACTER= 334 TAXON=50 TEXT='Priapulans exhibit undifferentiated longitudinal muscle bands [@Young2017]'; + TEXT CHARACTER= 334 TAXON=157 TEXT='Present [@Young2017]'; + TEXT CHARACTER= 334 TAXON=179 TEXT='Inferred in artiopodans by comparison with Kiisortoqia [@Young2017]'; + TEXT CHARACTER= 334 TAXON=180 TEXT='Inferred in artiopodans by comparison with Kiisortoqia [@Young2017]'; + TEXT CHARACTER= 335 TAXON=28 TEXT='Attaching to pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 335 TAXON=29 TEXT='Attaching to pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 335 TAXON=30 TEXT='Attaching to pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 335 TAXON=31 TEXT='Attaching to pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 335 TAXON=32 TEXT='Attaching to pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 335 TAXON=33 TEXT='Attaching to pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 335 TAXON=34 TEXT='Attaching to pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 335 TAXON=35 TEXT='Attaching to pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 335 TAXON=36 TEXT='Attaching to pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 335 TAXON=37 TEXT='Attaching to pachycycli of subsequent segments [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 335 TAXON=179 TEXT='The successive attachment points in artiopodans are inferred by comparison with Campanamuta [@Young2017]'; + TEXT CHARACTER= 335 TAXON=180 TEXT='The successive attachment points in artiopodans are inferred by comparison with Campanamuta [@Young2017]'; + TEXT CHARACTER= 337 TAXON=18 TEXT='Musculature of adult described by @Neves2013^n'; + TEXT CHARACTER= 337 TAXON=19 TEXT='Musculature of Higgins larva described by @Neves2013'; + TEXT CHARACTER= 337 TAXON=28 TEXT='Circular muscles in certain places (bases of scalid rings 6 and 7; connecting placids) but not in integument [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 337 TAXON=29 TEXT='Circular muscles in certain places (bases of scalid rings 6 and 7; connecting placids) but not in integument [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 337 TAXON=30 TEXT='Circular muscles in certain places (bases of scalid rings 6 and 7; connecting placids) but not in integument [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 337 TAXON=31 TEXT='Circular muscles in certain places (bases of scalid rings 6 and 7; connecting placids) but not in integument [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 337 TAXON=32 TEXT='Circular muscles in certain places (bases of scalid rings 6 and 7; connecting placids) but not in integument [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 337 TAXON=33 TEXT='Circular muscles in certain places (bases of scalid rings 6 and 7; connecting placids) but not in integument [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 337 TAXON=34 TEXT='Circular muscles in certain places (bases of scalid rings 6 and 7; connecting placids) but not in integument [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 337 TAXON=35 TEXT='Circular muscles in certain places (bases of scalid rings 6 and 7; connecting placids) but not in integument [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 337 TAXON=36 TEXT='Circular muscles in certain places (bases of scalid rings 6 and 7; connecting placids) but not in integument [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 337 TAXON=37 TEXT='Circular muscles in certain places (bases of scalid rings 6 and 7; connecting placids) but not in integument [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 337 TAXON=156 TEXT='Circular muscle reported by multiple studies [@Budd1993; @Budd1998trse; @Young2017]'; + TEXT CHARACTER= 337 TAXON=157 TEXT='Pambdelurion exhibits longitudinal peripheral musculature; the presence of circular muscle is equivocal [@Budd1998l; @Young2017].'; + TEXT CHARACTER= 337 TAXON=179 TEXT='The absence of circular muscle in artiopodans is inferred by comparison with Campanamuta, in which no circular musculature is evident despite preservation of individual myofibrils [@Young2017]'; + TEXT CHARACTER= 337 TAXON=180 TEXT='The absence of circular muscle in artiopodans is inferred by comparison with Campanamuta, in which no circular musculature is evident despite preservation of individual myofibrils [@Young2017]'; + TEXT CHARACTER= 338 TAXON=44 TEXT='[@SchmidtRhaesa2022za]'; + TEXT CHARACTER= 339 TAXON=28 TEXT='Muscles present in all segments [@Herranz2021z]'; + TEXT CHARACTER= 339 TAXON=29 TEXT='Reduced in segment 1 [@Herranz2021z]'; + TEXT CHARACTER= 339 TAXON=32 TEXT='Reduced in segment 1 [@Herranz2021z]'; + TEXT CHARACTER= 339 TAXON=33 TEXT='Reduced in segment 1 [@Herranz2021z]'; + TEXT CHARACTER= 339 TAXON=34 TEXT='Muscles present in all segments [@Herranz2021z]'; + TEXT CHARACTER= 339 TAXON=37 TEXT='Muscles present in all segments [@Herranz2021z]'; + TEXT CHARACTER= 340 TAXON=29 TEXT='Oblique muscles present in Cyclorhagida only [@SchmidtRhaesa2013]; could broadly be said to mirror the box-truss system observed in Tactopoda '; + TEXT CHARACTER= 340 TAXON=30 TEXT='Oblique muscles present in Cyclorhagida only [@SchmidtRhaesa2013]; could broadly be said to mirror the box-truss system observed in Tactopoda '; + TEXT CHARACTER= 340 TAXON=31 TEXT='Oblique muscles present in Cyclorhagida only [@SchmidtRhaesa2013]; could broadly be said to mirror the box-truss system observed in Tactopoda '; + TEXT CHARACTER= 340 TAXON=32 TEXT='Oblique muscles present in Cyclorhagida only [@SchmidtRhaesa2013]; could broadly be said to mirror the box-truss system observed in Tactopoda '; + TEXT CHARACTER= 340 TAXON=33 TEXT='Oblique muscles present in Cyclorhagida only [@SchmidtRhaesa2013]; could broadly be said to mirror the box-truss system observed in Tactopoda '; + TEXT CHARACTER= 340 TAXON=136 TEXT='Oblique musculature, but no dorsoventral [@Zhang2016]'; + TEXT CHARACTER= 340 TAXON=156 TEXT='Oblique muscles are evident in the anterior, but there is no good evidence of dorsoventral muscles [@Young2017].'; + TEXT CHARACTER= 340 TAXON=157 TEXT='Dorsoventral muscles not reported; extent of oblique muscles disputed [@Budd1998l; @Young2017], and orientation does not match that of box-truss.'; + TEXT CHARACTER= 341 TAXON=156 TEXT='We suggest that the pericardial region represents the musculature of the heart.'; + TEXT CHARACTER= 341 TAXON=176 TEXT='Present [@Ma2014nc]'; + TEXT CHARACTER= 342 TAXON=5 TEXT='Code with care - an unreliable internet source attests to their presence^n'; + TEXT CHARACTER= 343 TAXON=32 TEXT='Basiepithelial mouth cone nerves, stomatogastric nerves, and circumoral brain, the latter situated between the first scalid ring and the base of the mouth cone [@Nebelsick1993z]'; + TEXT CHARACTER= 343 TAXON=38 TEXT='Basiepithelial neurites in the epidermis [@SchmidtRhaesa2014]'; + TEXT CHARACTER= 343 TAXON=39 TEXT='Basiepithelial neurites in the epidermis [@SchmidtRhaesa2014]'; + TEXT CHARACTER= 343 TAXON=43 TEXT='Enclosed within a basal lamina shared with the epidermis [@SchmidtRhaesa2014]'; + TEXT CHARACTER= 343 TAXON=49 TEXT='Intraepithelial [@Rothe2010]'; + TEXT CHARACTER= 343 TAXON=50 TEXT='Intraepithelial in T. troglodytes [@Rothe2010]'; + TEXT CHARACTER= 343 TAXON=51 TEXT='Intraepithelial in T. troglodytes [@Rothe2010]'; + TEXT CHARACTER= 344 TAXON=15 TEXT='Unpaired [@Wang2025]'; + TEXT CHARACTER= 344 TAXON=16 TEXT='Unpaired [@Wang2025]'; + TEXT CHARACTER= 344 TAXON=28 TEXT='The ventral nerve cord originates from the forebrain as two distinct strands, which fuse to one cord in certain taxa [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 344 TAXON=29 TEXT='The ventral nerve cord originates from the forebrain as two distinct strands, which fuse to one cord in certain taxa [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 344 TAXON=30 TEXT='The ventral nerve cord originates from the forebrain as two distinct strands, which fuse to one cord in certain taxa [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 344 TAXON=31 TEXT='The ventral nerve cord originates from the forebrain as two distinct strands, which fuse to one cord in certain taxa [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 344 TAXON=32 TEXT='The ventral nerve cord originates from the forebrain as two distinct strands, which fuse to one cord in certain taxa [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 344 TAXON=33 TEXT='The ventral nerve cord originates from the forebrain as two distinct strands, which fuse to one cord in certain taxa [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 344 TAXON=34 TEXT='The ventral nerve cord originates from the forebrain as two distinct strands, which fuse to one cord in certain taxa [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 344 TAXON=35 TEXT='The ventral nerve cord originates from the forebrain as two distinct strands, which fuse to one cord in certain taxa [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 344 TAXON=36 TEXT='The ventral nerve cord originates from the forebrain as two distinct strands, which fuse to one cord in certain taxa [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 344 TAXON=37 TEXT='The ventral nerve cord originates from the forebrain as two distinct strands, which fuse to one cord in certain taxa [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 344 TAXON=38 TEXT='Unpaired in Chordodes [@Son2009] and Gordius [@SchmidtRhaesa1996], though paired in Paragordius [@SchmidtRhaesa2014]; nematomorph ventral nerve cords retain vestiges of a paired origin [@SchmidtRhaesa1997]'; + TEXT CHARACTER= 344 TAXON=39 TEXT='Though visibly unpaired [@SchmidtRhaesa1996], nematomorph ventral nerve cords retain vestiges of a paired origin [@SchmidtRhaesa1997]'; + TEXT CHARACTER= 344 TAXON=42 TEXT='Paired, unequal [@SchmidtRhaesa2014]'; + TEXT CHARACTER= 344 TAXON=43 TEXT='Paired, unequal [@SchmidtRhaesa2014]'; + TEXT CHARACTER= 344 TAXON=50 TEXT='Unpaired see @Yang2016. '; + TEXT CHARACTER= 344 TAXON=52 TEXT='Paired [@Yang2016]'; + TEXT CHARACTER= 344 TAXON=53 TEXT='Paired [@Yang2016]'; + TEXT CHARACTER= 344 TAXON=54 TEXT='Paired [@Yang2016]'; + TEXT CHARACTER= 344 TAXON=55 TEXT='Paired [@Yang2016]'; + TEXT CHARACTER= 344 TAXON=97 TEXT='@Wang2025'; + TEXT CHARACTER= 344 TAXON=106 TEXT='@Wang2025'; + TEXT CHARACTER= 344 TAXON=119 TEXT='@Wang2025'; + TEXT CHARACTER= 344 TAXON=130 TEXT='Ambiguous. @Hou2004 report the presence of a ventral nerve cord, although it is not possible to discern if it is paired or not [@Yang2016]'; + TEXT CHARACTER= 344 TAXON=156 TEXT='Tentatively interpreted as paired, unfused [@Park2018]'; + TEXT CHARACTER= 344 TAXON=173 TEXT='Paired. Two descending tracts on the anterior trunk region [@Cong2014]'; + TEXT CHARACTER= 344 TAXON=175 TEXT='Paired [@Yang2016]'; + TEXT CHARACTER= 344 TAXON=178 TEXT='Paired [@Tanaka2013]'; + TEXT CHARACTER= 346 TAXON=29 TEXT='Fuse to one cord after leaving forebrain [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 346 TAXON=32 TEXT='Fuse to one cord after leaving forebrain [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 346 TAXON=33 TEXT='Fuse to one cord after leaving forebrain [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 346 TAXON=37 TEXT='One chord reported in some Pycnopyhes species [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 347 TAXON=15 TEXT='Absent [@Wang2025]'; + TEXT CHARACTER= 347 TAXON=32 TEXT='Paired ganglia [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 347 TAXON=37 TEXT='Paired ganglia [@SchmidtRhaesa2013]'; + TEXT CHARACTER= 347 TAXON=42 TEXT='Single ventral nerve cord terminates in single terminal ganglion [@SchmidtRhaesa2014]'; + TEXT CHARACTER= 347 TAXON=130 TEXT='@Hou2004 (figs 2f, 4f) reported faint paired structures adjacent to the gut of Paucipodia, which were interpreted as potential nerve ganglia. We nevertheless code Paucipodia as ambiguous: the structures cannot be observed in the figured material, and are described as "faintly preserved with a pink colour" in contrast to the conspicuously dark colouration of unambiguous nervous tissue in Chengjiang-type fossils [see @Ma2012n; @Tanaka2013; @Yang2013].'; + TEXT CHARACTER= 347 TAXON=156 TEXT='We code this ambiguously as @Park2018 only implicitly reconstruct paired ganglia (in their figure 4); the ''nerve cords'' referred to in the text could represent the circumpharyngeal connectives that lead to the ventral nerve cord.'; + TEXT CHARACTER= 347 TAXON=173 TEXT='Ambiguous [@Cong2014]'; + TEXT CHARACTER= 347 TAXON=175 TEXT='Recent data on the neurological organization of stem-euarthropods indicate that paired ganglia are present in Chengjiangocaris [@Yang2013] and Alalcomenaeus [@Tanaka2013].'; + TEXT CHARACTER= 347 TAXON=178 TEXT='Recent data on the neurological organization of stem-euarthropods indicate that paired ganglia are present in Chengjiangocaris [@Yang2013] and Alalcomenaeus [@Tanaka2013]'; + TEXT CHARACTER= 348 TAXON=152 TEXT='The single medial sinus contrasts with the two lateral perineural sinuses of onychophorans [@Jahn2023]'; + TEXT CHARACTER= 348 TAXON=156 TEXT='Hints of a paired nerve cord in the anterior of Kerygmachela [@Park2018] are insufficient to establish their lateralization, though the positioning seems to correspond to that of Lyrarapax [@Cong2014]'; + TEXT CHARACTER= 348 TAXON=173 TEXT='Medial (Cong et al. 2014)'; + TEXT CHARACTER= 356 TAXON=177 TEXT='Coded present by proxy as unambiguously present in crown-Euarthropoda [see @Budd2021].'; + TEXT CHARACTER= 356 TAXON=178 TEXT='Coded present by proxy as unambiguously present in crown-Euarthropoda [see @Budd2021].'; + TEXT CHARACTER= 357 TAXON=52 TEXT='Following @Martin2022, who argue that the circumpharyngeal connective represents the last vestiges of the circumoral nerve ring.'; + TEXT CHARACTER= 357 TAXON=53 TEXT='Following @Martin2022, who argue that the circumpharyngeal connective represents the last vestiges of the circumoral nerve ring.'; + TEXT CHARACTER= 357 TAXON=54 TEXT='Following @Martin2022, who argue that the circumpharyngeal connective represents the last vestiges of the circumoral nerve ring.'; + TEXT CHARACTER= 357 TAXON=156 TEXT='The ''nerve cords'' interpreted by @Park2018 could represent circumoral connectives (interpreted by @Martin2022 as homologous to the circumoral nerve ring).'; + TEXT CHARACTER= 357 TAXON=173 TEXT='Not interpreted as present [@Cong2014; @Park2018]'; + TEXT CHARACTER= 359 TAXON=42 TEXT='Ventral nerve ring without condensation [@SchmidtRhaesa2014]'; + TEXT CHARACTER= 359 TAXON=156 TEXT='Present [@Park2018]'; + TEXT CHARACTER= 360 TAXON=156 TEXT='The brain is protocerebral [@Park2018]'; + TEXT CHARACTER= 361 TAXON=52 TEXT='Onychophora are coded as innervated from multiple neuromeres to reflect their complex neurological organization: although the jaws have a deutocerebral segmental affinity and innervation, the lip papillae that delineate the oral opening are formed as epidermal derivatives of the three anteriormost body segments, and thus receive nervous terminals from the protocerebrum, deutocerebrum and part of the ventral nerve cord [@Eriksson2000; @Martin2014].'; + TEXT CHARACTER= 361 TAXON=53 TEXT='Onychophora are coded as innervated from multiple neuromeres to reflect their complex neurological organization: although the jaws have a deutocerebral segmental affinity and innervation, the lip papillae that delineate the oral opening are formed as epidermal derivatives of the three anteriormost body segments, and thus receive nervous terminals from the protocerebrum, deutocerebrum and part of the ventral nerve cord [@Eriksson2000; @Martin2014].'; + TEXT CHARACTER= 361 TAXON=54 TEXT='Onychophora are coded as innervated from multiple neuromeres to reflect their complex neurological organization: although the jaws have a deutocerebral segmental affinity and innervation, the lip papillae that delineate the oral opening are formed as epidermal derivatives of the three anteriormost body segments, and thus receive nervous terminals from the protocerebrum, deutocerebrum and part of the ventral nerve cord [@Eriksson2000; @Martin2014].'; + TEXT CHARACTER= 361 TAXON=55 TEXT='The tardigrade mouth cone is innervated from the protocerebrum [@Mayer2013po].'; + TEXT CHARACTER= 361 TAXON=173 TEXT='Lyrarapax has protocerebral mouth innervation [@Cong2014].'; + TEXT CHARACTER= 367 TAXON=38 TEXT='Single combined body opening'; + TEXT CHARACTER= 367 TAXON=39 TEXT='Inapplicable: intestine is incomplete and ends blindly [@SchmidtRhaesa2012]'; + TEXT CHARACTER= 367 TAXON=43 TEXT='Present in males; separate vulva and anus in females [@SchmidtRhaesa2024]'; + TEXT CHARACTER= 367 TAXON=179 TEXT='Absent by proxy for Euarthropoda crown.'; + TEXT CHARACTER= 367 TAXON=180 TEXT='Absent by proxy for Euarthropoda crown'; + TEXT CHARACTER= 368 TAXON=43 TEXT='Present in males; separate vulva and anus in females [@SchmidtRhaesa2024]'; + TEXT CHARACTER= 370 TAXON=179 TEXT='Absent by proxy for Euarthropod crown.'; + TEXT CHARACTER= 370 TAXON=180 TEXT='Absent by proxy for Euarthropod crown.'; + TEXT CHARACTER= 376 TAXON=126 TEXT='Anterior gut expanded [@Whittington1978, e.g. fig 43]'; + TEXT CHARACTER= 376 TAXON=130 TEXT='No indication of gut widening, whichever end is anterior [@Hou2004 / @Vannier2017]'; + TEXT CHARACTER= 376 TAXON=179 TEXT='@Chen1997'; + TEXT CHARACTER= 383 TAXON=11 TEXT='The associated Conotheca fragment is oppositely directed and hence not a dwelling tube of the organism [@Zhang2022]'; + TEXT CHARACTER= 384 TAXON=42 TEXT='Flagelliform tail present [@Reimann1972]'; + TEXT CHARACTER= 389 TAXON=177 TEXT='Coded present by proxy as absent in crown-Euarthropoda (see @Khim2023). If this matrix is to be used to investigate euarthropod relationships in future, this coding should be adjusted accordingly.'; + TEXT CHARACTER= 389 TAXON=178 TEXT='Coded present by proxy as absent in crown-Euarthropoda (see @Khim2023). If this matrix is to be used to investigate euarthropod relationships in future, this coding should be adjusted accordingly.'; + TEXT CHARACTER= 393 TAXON=80 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 393 TAXON=83 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 393 TAXON=85 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 393 TAXON=89 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 393 TAXON=91 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 393 TAXON=92 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 393 TAXON=94 TEXT='After @Mapalo2024cb'; + TEXT CHARACTER= 398 TAXON=24 TEXT='Wrinkled thorax, abdominal lorica [@Fujimoto2020mb]'; + TEXT CHARACTER= 400 TAXON=24 TEXT='Pair of anteroventral setae present, plus an anterolateral pair [@Fujimoto2020mb]'; + TEXT CHARACTER= 401 TAXON=24 TEXT='Posterodorsal and posterolateral setae present [@Fujimoto2020mb]'; + TEXT CHARACTER= 404 TAXON=1 TEXT='In view of the morphological arrangement, treated as a likely homologue of the Higgins larva.'; + TEXT CHARACTER= 405 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 405 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 405 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 405 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 405 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 405 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 405 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 405 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 405 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 405 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 406 TAXON=1 TEXT='The thorax (crenulated region) is shorter than the abdomen (loricate region) in most specimens [@Maas2009aap], become equant in the larger specimen.'; + TEXT CHARACTER= 406 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 406 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 406 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 406 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 406 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 406 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 406 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 406 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 406 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 406 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 407 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 407 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 407 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 407 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 407 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 407 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 407 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 407 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 407 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 407 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 409 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 409 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 409 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 409 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 409 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 409 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 409 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 409 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 409 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 409 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 410 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 410 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 410 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 410 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 410 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 410 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 410 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 410 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 410 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 410 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 411 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 411 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 411 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 411 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 411 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 411 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 411 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 411 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 411 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 411 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 412 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 412 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 412 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 412 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 412 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 412 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 412 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 412 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 412 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 412 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 413 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 413 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 413 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 413 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 413 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 413 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 413 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 413 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 413 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 413 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 414 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 414 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 414 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 414 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 414 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 414 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 414 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 414 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 414 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 414 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 415 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 415 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 415 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 415 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 415 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 415 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 415 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 415 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 415 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 415 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 416 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 416 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 416 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 416 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 416 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 416 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 416 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 416 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 416 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 416 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 417 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 417 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 417 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 417 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 417 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 417 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 417 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 417 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 417 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 417 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 418 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 418 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 418 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 418 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 418 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 418 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 418 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 418 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 418 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 418 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 419 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 419 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 419 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 419 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 419 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 419 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 419 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 419 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 419 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 419 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 420 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 420 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 420 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 420 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 420 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 420 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 420 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 420 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 420 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 420 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 421 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 421 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 421 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 421 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 421 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 421 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 421 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 421 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 421 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 421 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 422 TAXON=1 TEXT='Not evident [@Maas2009app]'; + TEXT CHARACTER= 422 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 422 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 422 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 422 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 422 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 422 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 422 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 422 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 422 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 422 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 423 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 423 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 423 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 423 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 423 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 423 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 423 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 423 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 423 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 423 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 424 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 424 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 424 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 424 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 424 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 424 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 424 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 424 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 424 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 424 TAXON=27 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 425 TAXON=18 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 425 TAXON=19 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 425 TAXON=20 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 425 TAXON=21 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 425 TAXON=22 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 425 TAXON=23 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 425 TAXON=24 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 425 TAXON=25 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 425 TAXON=26 TEXT='Following @Sorensen2023'; + TEXT CHARACTER= 425 TAXON=27 TEXT='Following @Sorensen2023'; + ENDBLOCK; + BEGIN ASSUMPTIONS; + TYPESET * UNTITLED = unord: 1 - 425; + ENDBLOCK; + \ No newline at end of file diff --git a/dev/benchmarks/mbank_catalogue.csv b/dev/benchmarks/mbank_catalogue.csv new file mode 100644 index 000000000..9c82a3983 --- /dev/null +++ b/dev/benchmarks/mbank_catalogue.csv @@ -0,0 +1,805 @@ +"key","filename","project_id","matrix_idx","source_type","split","ntax","nchar","n_patterns","n_states","pct_missing","pct_inapp","parse_ok","error_message","dedup_drop" +"project1013","project1013.nex",1013,NA,"morphobank","training",112,174,172,9,23.3,0,TRUE,"",FALSE +"project1020","project1020.nex",1020,NA,"morphobank","validation",28,110,108,10,15.1,8.1,TRUE,"",FALSE +"project1024","project1024.nex",1024,NA,"morphobank","training",163,156,151,5,7.2,1,TRUE,"",FALSE +"project1035","project1035.nex",1035,NA,"morphobank","validation",58,185,183,6,17.4,2.9,TRUE,"",FALSE +"project1037_(1)","project1037 (1).nex",1037,1,"morphobank","training",62,71,71,4,13.7,12,TRUE,"",FALSE +"project1037_(2)","project1037 (2).nex",1037,2,"morphobank","training",62,69,69,4,14,12.4,TRUE,"",TRUE +"project1037_(3)","project1037 (3).nex",1037,3,"morphobank","training",64,69,69,4,14.7,12.4,TRUE,"",FALSE +"project104","project104.nex",104,NA,"morphobank","training",29,207,202,6,25,3.1,TRUE,"",FALSE +"project1045","project1045.nex",1045,NA,"morphobank","validation",13,37,35,4,8.4,0,TRUE,"",FALSE +"project1046","project1046.nex",1046,NA,"morphobank","training",34,291,290,5,36,0,TRUE,"",FALSE +"project1049","project1049.nex",1049,NA,"morphobank","training",41,145,133,2,3.2,0,TRUE,"",FALSE +"project1066","project1066.nex",1066,NA,"morphobank","training",32,92,78,4,5.9,4.2,TRUE,"",FALSE +"project1070","project1070.nex",1070,NA,"morphobank","validation",72,426,426,6,23.3,5.7,TRUE,"",FALSE +"project1076","project1076.nex",1076,NA,"morphobank","training",22,70,70,4,29.7,0,TRUE,"",FALSE +"project108","project108.nex",108,NA,"morphobank","training",29,207,202,6,25,3.1,TRUE,"",FALSE +"project1088","project1088.nex",1088,NA,"morphobank","training",11,44,38,2,25.4,3.8,TRUE,"",FALSE +"project1097","project1097.nex",1097,NA,"morphobank","training",66,1,1,5,1.5,0,TRUE,"",FALSE +"project1102","project1102.nex",1102,NA,"morphobank","training",61,143,143,4,27.7,5.8,TRUE,"WARNING: Could not parse character states; does each end with a ' or ;?.",FALSE +"project1104","project1104.nex",1104,NA,"morphobank","training",60,127,123,4,3,2.8,TRUE,"",FALSE +"project1105","project1105.nex",1105,NA,"morphobank","validation",22,57,57,4,15,0.7,TRUE,"",FALSE +"project1109","project1109.nex",1109,NA,"morphobank","training",11,65,56,3,26.9,2.1,TRUE,"",FALSE +"project1113","project1113.nex",1113,NA,"morphobank","training",38,42,40,4,1.9,5.1,TRUE,"",FALSE +"project1115","project1115.nex",1115,NA,"morphobank","validation",25,51,50,5,13.9,1.2,TRUE,"",FALSE +"project1118","project1118.nex",1118,NA,"morphobank","training",37,98,98,3,31.5,4.6,TRUE,"",FALSE +"project1119","project1119.nex",1119,NA,"morphobank","training",73,408,408,6,46.5,0,TRUE,"",FALSE +"project1120","project1120.nex",1120,NA,"morphobank","validation",33,85,84,4,27.4,0.3,TRUE,"",FALSE +"project1122","project1122.nex",1122,NA,"morphobank","training",32,63,58,5,10.9,22.1,TRUE,"",FALSE +"project1126","project1126.nex",1126,NA,"morphobank","training",132,560,560,5,59.8,0,TRUE,"",FALSE +"project1135","project1135.nex",1135,NA,"morphobank","validation",29,127,124,5,40,0,TRUE,"",FALSE +"project1138","project1138.nex",1138,NA,"morphobank","training",56,72,70,3,13.1,0.6,TRUE,"",FALSE +"project1144","project1144.nex",1144,NA,"morphobank","training",10,32,28,3,21.8,0,TRUE,"",FALSE +"project1150","project1150.nex",1150,NA,"morphobank","validation",62,111,111,7,40.7,7.6,TRUE,"",FALSE +"project1151","project1151.nex",1151,NA,"morphobank","training",12,16,16,3,6.2,0,TRUE,"",FALSE +"project1157","project1157.nex",1157,NA,"morphobank","training",110,205,156,3,0.1,2.8,TRUE,"",FALSE +"project1166","project1166.nex",1166,NA,"morphobank","training",71,141,140,10,23.4,3.7,TRUE,"",FALSE +"project1187","project1187.nex",1187,NA,"morphobank","training",32,81,81,3,20.5,2.9,TRUE,"",FALSE +"project1189","project1189.nex",1189,NA,"morphobank","training",26,72,72,4,14,5.3,TRUE,"",FALSE +"project1192","project1192.nex",1192,NA,"morphobank","training",80,51,51,6,35.2,18.4,TRUE,"",FALSE +"project1194","project1194.nex",1194,NA,"morphobank","training",49,175,172,7,37.5,0,TRUE,"",FALSE +"project1197","project1197.nex",1197,NA,"morphobank","training",28,83,66,5,7.1,8.2,TRUE,"",FALSE +"project1207","project1207.nex",1207,NA,"morphobank","training",53,208,207,7,23.7,1.6,TRUE,"",FALSE +"project1209","project1209.nex",1209,NA,"morphobank","training",46,125,91,5,0,0,TRUE,"WARNING: Could not parse character states; does each end with a ' or ;?.",FALSE +"project1210","project1210.nex",1210,NA,"morphobank","validation",86,36,17,3,4,0,TRUE,"",FALSE +"project1213","project1213.nex",1213,NA,"morphobank","training",29,139,136,5,41,0,TRUE,"",FALSE +"project1214","project1214.nex",1214,NA,"morphobank","training",12,42,40,4,6.9,5.2,TRUE,"",FALSE +"project1220_(1)","project1220 (1).nex",1220,1,"morphobank","validation",4,24,20,5,8.8,1.2,TRUE,"",FALSE +"project1220_(2)","project1220 (2).nex",1220,2,"morphobank","validation",5,61,45,4,5.3,5.3,TRUE,"",FALSE +"project1221","project1221.nex",1221,NA,"morphobank","training",150,252,251,8,31.9,15.3,TRUE,"",FALSE +"project1223","project1223.nex",1223,NA,"morphobank","training",30,78,77,4,27.1,0.9,TRUE,"",FALSE +"project1228","project1228.nex",1228,NA,"morphobank","training",18,20,19,2,20.2,0,TRUE,"",FALSE +"project1271","project1271.nex",1271,NA,"morphobank","training",25,33,32,25,24,0,TRUE,"",FALSE +"project1278","project1278.nex",1278,NA,"morphobank","training",23,60,60,8,38.8,6.2,TRUE,"",FALSE +"project157","project157.nex",157,NA,"morphobank","training",69,408,408,6,40.2,4.6,TRUE,"",FALSE +"project161","project161.nex",161,NA,"morphobank","training",21,173,165,4,28.5,0,TRUE,"",FALSE +"project171","project171.nex",171,NA,"morphobank","training",68,228,222,5,14.6,3.2,TRUE,"",FALSE +"project175","project175.nex",175,NA,"morphobank","validation",165,71,71,6,12.9,0,TRUE,"",FALSE +"project181","project181.nex",181,NA,"morphobank","training",24,119,116,6,29.7,0,TRUE,"",FALSE +"project182","project182.nex",182,NA,"morphobank","training",50,115,108,6,46.9,0,TRUE,"",FALSE +"project194","project194.nex",194,NA,"morphobank","training",72,207,163,8,13.9,0,TRUE,"",FALSE +"project198","project198.nex",198,NA,"morphobank","training",83,412,412,5,37,3.3,TRUE,"",FALSE +"project199","project199.nex",199,NA,"morphobank","training",NA,NA,NA,NA,NA,NA,FALSE,"WARNING: Missing character state definition for: 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173 ; ERROR: missing value where TRUE/FALSE needed",FALSE +"project200","project200.nex",200,NA,"morphobank","validation",40,123,121,4,28.6,0,TRUE,"",FALSE +"project205","project205.nex",205,NA,"morphobank","validation",41,315,315,11,37.7,0.1,TRUE,"",FALSE +"project2084_(1)","project2084 (1).nex",2084,1,"morphobank","training",86,3660,3601,10,20.9,24.9,TRUE,"",FALSE +"project2084_(2)","project2084 (2).nex",2084,2,"morphobank","training",68,146,146,4,28.5,5.3,TRUE,"",FALSE +"project2086","project2086.nex",2086,NA,"morphobank","training",91,453,453,8,45.3,15.1,TRUE,"",FALSE +"project2099_(1)","project2099 (1).nex",2099,1,"morphobank","training",114,555,555,7,57.3,2.3,TRUE,"",FALSE +"project2099_(2)","project2099 (2).nex",2099,2,"morphobank","training",114,555,555,7,57.4,2.3,TRUE,"",FALSE +"project2106","project2106.nex",2106,NA,"morphobank","training",62,90,90,4,32.3,2,TRUE,"",FALSE +"project2116","project2116.nex",2116,NA,"morphobank","training",74,158,158,10,27.7,6,TRUE,"",FALSE +"project2124","project2124.nex",2124,NA,"morphobank","training",81,477,477,5,65.2,0,TRUE,"",FALSE +"project2131","project2131.nex",2131,NA,"morphobank","training",32,55,54,6,36.5,7.9,TRUE,"",FALSE +"project2144","project2144.nex",2144,NA,"morphobank","training",109,124,123,4,48.9,2.7,TRUE,"",FALSE +"project2151","project2151.nex",2151,NA,"morphobank","training",55,56,56,5,8.5,2.2,TRUE,"",FALSE +"project216","project216.nex",216,NA,"morphobank","training",51,129,105,8,2.3,1.8,TRUE,"",FALSE +"project2167","project2167.nex",2167,NA,"morphobank","training",81,421,419,10,47.5,2.3,TRUE,"",FALSE +"project2183","project2183.nex",2183,NA,"morphobank","training",318,535,533,5,22.5,0,TRUE,"",FALSE +"project2184","project2184.nex",2184,NA,"morphobank","training",114,205,168,3,1.7,2.5,TRUE,"",FALSE +"project2189","project2189.nex",2189,NA,"morphobank","training",73,777,743,9,39.4,0,TRUE,"",FALSE +"project2191","project2191.nex",2191,NA,"morphobank","training",105,216,215,8,23.6,12.8,TRUE,"",FALSE +"project2193","project2193.nex",2193,NA,"morphobank","training",38,364,363,5,33.3,1.5,TRUE,"",FALSE +"project2194_(1)","project2194 (1).nex",2194,1,"morphobank","training",16,57,50,3,32.4,0.6,TRUE,"",FALSE +"project2194_(2)","project2194 (2).nex",2194,2,"morphobank","training",16,1,1,3,6.2,0,TRUE,"",FALSE +"project2196","project2196.nex",2196,NA,"morphobank","training",99,339,339,9,34.9,6.8,TRUE,"",FALSE +"project2197","project2197.nex",2197,NA,"morphobank","training",61,178,178,5,19.1,2.3,TRUE,"",FALSE +"project2209","project2209.nex",2209,NA,"morphobank","training",1,6,5,4,0,0,TRUE,"WARNING: Could not parse character states; does each end with a ' or ;?.",FALSE +"project2215","project2215.nex",2215,NA,"morphobank","validation",15,64,60,3,27.7,3.4,TRUE,"",FALSE +"project2216","project2216.nex",2216,NA,"morphobank","training",61,232,227,7,29,18.4,TRUE,"",FALSE +"project2218","project2218.nex",2218,NA,"morphobank","training",54,164,164,4,16.7,1.7,TRUE,"",FALSE +"project2219","project2219.nex",2219,NA,"morphobank","training",17,35,33,8,42.4,0.7,TRUE,"",FALSE +"project222","project222.nex",222,NA,"morphobank","training",63,28,27,9,3.5,0,TRUE,"",FALSE +"project2220_(1)","project2220 (1).nex",2220,1,"morphobank","validation",92,17,17,6,6,0.5,TRUE,"",FALSE +"project2220_(2)","project2220 (2).nex",2220,2,"morphobank","validation",267,17,17,6,41.4,0.6,TRUE,"",FALSE +"project2225","project2225.nex",2225,NA,"morphobank","validation",3,272,37,4,31.5,6.3,TRUE,"",FALSE +"project2238","project2238.nex",2238,NA,"morphobank","training",1,272,7,5,14.3,14.3,TRUE,"",FALSE +"project2244","project2244.nex",2244,NA,"morphobank","training",54,194,173,9,11,23.6,TRUE,"",FALSE +"project2261","project2261.nex",2261,NA,"morphobank","training",42,83,82,5,26.1,0,TRUE,"",FALSE +"project2285","project2285.nex",2285,NA,"morphobank","validation",19,73,71,6,18.7,1,TRUE,"",FALSE +"project2286","project2286.nex",2286,NA,"morphobank","training",134,232,194,3,1.4,1.8,TRUE,"",FALSE +"project2289","project2289.nex",2289,NA,"morphobank","training",73,220,211,3,15.3,0.9,TRUE,"",FALSE +"project2291","project2291.nex",2291,NA,"morphobank","training",79,132,130,7,53.9,0,TRUE,"",FALSE +"project2292","project2292.nex",2292,NA,"morphobank","training",114,497,493,6,47.4,0,TRUE,"",FALSE +"project2320","project2320.nex",2320,NA,"morphobank","validation",66,382,382,6,48.8,0,TRUE,"",FALSE +"project2329","project2329.nex",2329,NA,"morphobank","training",51,65,65,4,20.5,2.8,TRUE,"",FALSE +"project2331","project2331.nex",2331,NA,"morphobank","training",94,272,271,5,39,4.3,TRUE,"",FALSE +"project2332","project2332.nex",2332,NA,"morphobank","training",49,71,70,8,1.5,0,TRUE,"",FALSE +"project2334","project2334.nex",2334,NA,"morphobank","training",12,85,82,8,8.7,1.1,TRUE,"",FALSE +"project2335","project2335.nex",2335,NA,"morphobank","validation",45,60,60,3,17.9,1.3,TRUE,"",FALSE +"project2340_(1)","project2340 (1).nex",2340,1,"morphobank","validation",40,43,43,9,5.1,3.5,TRUE,"",FALSE +"project2340_(2)","project2340 (2).nex",2340,2,"morphobank","validation",40,46,46,8,5.1,4.3,TRUE,"",FALSE +"project2340_(3)","project2340 (3).nex",2340,3,"morphobank","validation",31,46,44,6,6.6,4.3,TRUE,"",TRUE +"project2341","project2341.nex",2341,NA,"morphobank","training",64,47,43,5,1.6,0,TRUE,"",FALSE +"project2342","project2342.nex",2342,NA,"morphobank","training",30,234,229,6,43.3,0,TRUE,"",FALSE +"project2346","project2346.nex",2346,NA,"morphobank","training",23,144,141,4,18,28.5,TRUE,"",FALSE +"project2348","project2348.nex",2348,NA,"morphobank","training",39,93,92,8,15.2,2.4,TRUE,"",FALSE +"project2349","project2349.nex",2349,NA,"morphobank","training",26,66,64,4,7.8,3.9,TRUE,"",FALSE +"project2359","project2359.nex",2359,NA,"morphobank","training",42,111,111,7,3.3,26.3,TRUE,"",FALSE +"project2368","project2368.nex",2368,NA,"morphobank","training",62,351,350,8,60.3,0.2,TRUE,"",FALSE +"project2384","project2384.nex",2384,NA,"morphobank","training",150,226,226,10,37.6,7.9,TRUE,"",FALSE +"project2387","project2387.nex",2387,NA,"morphobank","training",28,22,22,4,14.6,0.2,TRUE,"",FALSE +"project2399","project2399.nex",2399,NA,"morphobank","training",111,439,439,6,55.2,0,TRUE,"",FALSE +"project240","project240.nex",240,NA,"morphobank","validation",21,245,230,7,20.8,2,TRUE,"",FALSE +"project2403","project2403.nex",2403,NA,"morphobank","training",23,66,66,6,8,0.8,TRUE,"",FALSE +"project2405","project2405.nex",2405,NA,"morphobank","validation",5,197,62,4,0.3,0,TRUE,"",FALSE +"project2406_(1)","project2406 (1).nex",2406,1,"morphobank","training",6,65,49,3,0,0,TRUE,"",FALSE +"project2406_(2)","project2406 (2).nex",2406,2,"morphobank","training",6,65,52,3,0,0,TRUE,"",FALSE +"project2406_(3)","project2406 (3).nex",2406,3,"morphobank","training",6,65,58,3,0,0,TRUE,"",FALSE +"project2409","project2409.nex",2409,NA,"morphobank","training",19,14,13,3,0.8,11.3,TRUE,"",FALSE +"project2411","project2411.nex",2411,NA,"morphobank","training",69,75,75,5,9.6,10.5,TRUE,"",FALSE +"project2416","project2416.nex",2416,NA,"morphobank","training",102,600,600,6,56,3.3,TRUE,"",FALSE +"project2436_(1)","project2436 (1).nex",2436,1,"morphobank","training",41,273,271,6,24.9,1.9,TRUE,"",FALSE +"project2436_(2)","project2436 (2).nex",2436,2,"morphobank","training",41,273,271,6,25.3,1.9,TRUE,"",TRUE +"project2439","project2439.nex",2439,NA,"morphobank","training",32,101,101,5,35.9,0,TRUE,"",FALSE +"project2442","project2442.nex",2442,NA,"morphobank","training",53,206,204,10,26.3,11.7,TRUE,"",FALSE +"project2448","project2448.nex",2448,NA,"morphobank","training",13,13,13,7,0,4.7,TRUE,"",FALSE +"project2449","project2449.nex",2449,NA,"morphobank","training",176,463,292,8,58.1,0,TRUE,"",FALSE +"project2450","project2450.nex",2450,NA,"morphobank","validation",24,391,378,6,48.7,0,TRUE,"",FALSE +"project2451","project2451.nex",2451,NA,"morphobank","training",24,380,367,6,54.5,0,TRUE,"",FALSE +"project2452","project2452.nex",2452,NA,"morphobank","training",94,272,271,5,38.3,4.4,TRUE,"",FALSE +"project246","project246.nex",246,NA,"morphobank","training",35,204,199,6,27.6,2.6,TRUE,"",FALSE +"project2463","project2463.nex",2463,NA,"morphobank","training",20,5,5,6,2,0,TRUE,"",FALSE +"project2473","project2473.nex",2473,NA,"morphobank","training",8,24,23,3,18.5,0,TRUE,"",FALSE +"project2477","project2477.nex",2477,NA,"morphobank","training",213,387,386,4,4.6,4,TRUE,"",FALSE +"project2482_(1)","project2482 (1).nex",2482,1,"morphobank","training",15,78,59,3,15.6,0.9,TRUE,"",FALSE +"project2482_(2)","project2482 (2).nex",2482,2,"morphobank","training",15,78,59,3,15.6,0.9,TRUE,"",FALSE +"project2490","project2490.nex",2490,NA,"morphobank","validation",13,8,8,4,1,0,TRUE,"",FALSE +"project2495","project2495.nex",2495,NA,"morphobank","validation",20,75,75,4,32.3,1.2,TRUE,"",FALSE +"project2501_(1)","project2501 (1).nex",2501,1,"morphobank","training",57,97,96,5,34.6,0,TRUE,"",FALSE +"project2501_(2)","project2501 (2).nex",2501,2,"morphobank","training",57,97,96,5,34.6,0,TRUE,"",TRUE +"project2506","project2506.nex",2506,NA,"morphobank","training",25,30,30,8,5.9,0.5,TRUE,"",FALSE +"project2525","project2525.nex",2525,NA,"morphobank","validation",134,44,43,8,6,0,TRUE,"",FALSE +"project2527","project2527.nex",2527,NA,"morphobank","training",32,247,247,4,29.4,3.8,TRUE,"",FALSE +"project2532","project2532.nex",2532,NA,"morphobank","training",133,561,561,5,60.1,0,TRUE,"",FALSE +"project2533","project2533.nex",2533,NA,"morphobank","training",8,63,36,6,6.9,4.5,TRUE,"",FALSE +"project2537_(1)","project2537 (1).nex",2537,1,"morphobank","training",58,77,77,8,9.4,24.7,TRUE,"",FALSE +"project2537_(2)","project2537 (2).nex",2537,2,"morphobank","training",48,63,63,8,9.5,15.9,TRUE,"",FALSE +"project2544","project2544.nex",2544,NA,"morphobank","training",8,12,10,2,0,0,TRUE,"",FALSE +"project2545","project2545.nex",2545,NA,"morphobank","validation",13,50,39,3,43,0,TRUE,"",FALSE +"project2546","project2546.nex",2546,NA,"morphobank","training",14,75,67,3,39.7,0,TRUE,"",FALSE +"project2547","project2547.nex",2547,NA,"morphobank","training",69,119,118,6,34.3,0,TRUE,"",FALSE +"project2551","project2551.nex",2551,NA,"morphobank","training",42,131,131,6,35.7,0.8,TRUE,"WARNING: Could not parse character states; does each end with a ' or ;?.",FALSE +"project2553","project2553.nex",2553,NA,"morphobank","training",37,145,145,4,35.3,7,TRUE,"",FALSE +"project2554","project2554.nex",2554,NA,"morphobank","training",43,282,281,6,25.6,4.1,TRUE,"",FALSE +"project2576","project2576.nex",2576,NA,"morphobank","training",83,125,115,5,16.2,0,TRUE,"",FALSE +"project2577","project2577.nex",2577,NA,"morphobank","training",6,20,17,4,0,0,TRUE,"",FALSE +"project2579","project2579.nex",2579,NA,"morphobank","training",31,78,77,4,27.9,0.8,TRUE,"",FALSE +"project2600","project2600.nex",2600,NA,"morphobank","validation",42,58,58,8,4.6,5.9,TRUE,"",FALSE +"project2604","project2604.nex",2604,NA,"morphobank","training",43,307,306,5,45.9,1.5,TRUE,"",FALSE +"project2606","project2606.nex",2606,NA,"morphobank","training",153,256,255,8,31.6,15.3,TRUE,"",FALSE +"project2607_(1)","project2607 (1).nex",2607,1,"morphobank","training",72,321,318,7,29,2.7,TRUE,"",TRUE +"project2607_(2)","project2607 (2).nex",2607,2,"morphobank","training",74,321,318,7,30.4,2.7,TRUE,"",FALSE +"project2610","project2610.nex",2610,NA,"morphobank","validation",17,53,51,5,6.2,0,TRUE,"WARNING: Could not parse character states; does each end with a ' or ;?.",FALSE +"project2615","project2615.nex",2615,NA,"morphobank","validation",18,41,40,6,28.8,3.6,TRUE,"",FALSE +"project262","project262.nex",262,NA,"morphobank","training",61,111,111,7,40.4,7.7,TRUE,"",FALSE +"project2621","project2621.nex",2621,NA,"morphobank","training",48,32,32,6,17,0,TRUE,"",FALSE +"project2626","project2626.nex",2626,NA,"morphobank","training",97,568,568,7,53.7,2.6,TRUE,"",FALSE +"project2627","project2627.nex",2627,NA,"morphobank","training",72,169,168,5,37.4,2.3,TRUE,"",FALSE +"project264","project264.nex",264,NA,"morphobank","training",63,150,146,7,20.2,9,TRUE,"",FALSE +"project2648","project2648.nex",2648,NA,"morphobank","training",95,272,271,5,39.1,4.3,TRUE,"",FALSE +"project265","project265.nex",265,NA,"morphobank","validation",30,208,203,6,22.9,2.4,TRUE,"",FALSE +"project2650","project2650.nex",2650,NA,"morphobank","validation",32,101,101,5,35.9,0,TRUE,"",FALSE +"project2653","project2653.nex",2653,NA,"morphobank","training",12,16,13,3,9,0,TRUE,"",FALSE +"project2655","project2655.nex",2655,NA,"morphobank","validation",38,272,270,5,26.4,4.4,TRUE,"",FALSE +"project2657","project2657.nex",2657,NA,"morphobank","training",20,22,22,6,21.8,0,TRUE,"",FALSE +"project266","project266.nex",266,NA,"morphobank","training",17,209,187,7,19,1.1,TRUE,"",FALSE +"project2668","project2668.nex",2668,NA,"morphobank","training",196,1227,1140,6,49.1,3.2,TRUE,"",FALSE +"project2669","project2669.nex",2669,NA,"morphobank","training",96,270,270,7,52.5,0,TRUE,"",FALSE +"project2691","project2691.nex",2691,NA,"morphobank","training",17,41,38,5,8.4,4.8,TRUE,"",FALSE +"project2694","project2694.nex",2694,NA,"morphobank","training",31,31,30,6,3.8,1.2,TRUE,"",FALSE +"project2702","project2702.nex",2702,NA,"morphobank","training",29,40,40,7,4.7,0,TRUE,"",FALSE +"project2707","project2707.nex",2707,NA,"morphobank","training",64,35,35,12,23,0,TRUE,"",FALSE +"project2713","project2713.nex",2713,NA,"morphobank","training",14,16,16,3,23.7,0,TRUE,"",FALSE +"project2722","project2722.nex",2722,NA,"morphobank","training",385,520,519,4,30.9,3.1,TRUE,"",FALSE +"project2723","project2723.nex",2723,NA,"morphobank","training",56,97,95,6,12,1.6,TRUE,"",FALSE +"project2726","project2726.nex",2726,NA,"morphobank","training",24,71,54,5,6,0,TRUE,"",FALSE +"project2749","project2749.nex",2749,NA,"morphobank","training",64,163,163,4,42.1,0,TRUE,"",FALSE +"project2762","project2762.nex",2762,NA,"morphobank","training",29,187,177,5,34.8,17.1,TRUE,"",FALSE +"project2769","project2769.nex",2769,NA,"morphobank","training",102,219,218,5,10.9,3.5,TRUE,"",FALSE +"project277","project277.nex",277,NA,"morphobank","training",12,40,39,4,9.2,0,TRUE,"",FALSE +"project2770","project2770.nex",2770,NA,"morphobank","validation",55,307,307,4,37,2.7,TRUE,"",FALSE +"project2771","project2771.nex",2771,NA,"morphobank","training",94,124,123,8,1,30,TRUE,"",FALSE +"project2776","project2776.nex",2776,NA,"morphobank","training",96,270,270,7,52.5,0,TRUE,"",FALSE +"project2781","project2781.nex",2781,NA,"morphobank","training",58,202,199,6,40,0,TRUE,"",FALSE +"project2788_(1)","project2788 (1).nex",2788,1,"morphobank","training",33,106,106,3,40.4,0,TRUE,"",TRUE +"project2788_(2)","project2788 (2).nex",2788,2,"morphobank","training",34,106,106,3,40.1,0,TRUE,"",FALSE +"project2789","project2789.nex",2789,NA,"morphobank","training",72,75,75,4,13.7,7.1,TRUE,"",FALSE +"project2792","project2792.nex",2792,NA,"morphobank","training",93,230,219,10,19.8,13.7,TRUE,"",FALSE +"project2794","project2794.nex",2794,NA,"morphobank","training",113,170,170,5,39.5,2.2,TRUE,"",FALSE +"project2798__Ungulate_dental","project2798__Ungulate_dental.nex",2798,NA,"morphobank","training",76,92,91,4,20.3,0,TRUE,"",FALSE +"project2798_Gheerbrant_et_al._(2016)","project2798_Gheerbrant et al. (2016).nex",2798,2016,"morphobank","training",28,184,182,6,25.3,0,TRUE,"",FALSE +"project2798_Muizon_et_al._(2015)","project2798_Muizon et al. (2015).nex",2798,2015,"morphobank","training",73,426,426,6,29.9,0,TRUE,"",FALSE +"project2798_Tabuce_et_al._(2011)","project2798_Tabuce et al. (2011).nex",2798,2011,"morphobank","training",38,65,64,5,16.9,0,TRUE,"",FALSE +"project2799","project2799.nex",2799,NA,"morphobank","training",64,401,400,6,51.6,0,TRUE,"",FALSE +"project2800","project2800.nex",2800,NA,"morphobank","validation",54,225,217,5,48.7,9.7,TRUE,"",FALSE +"project2804","project2804.nex",2804,NA,"morphobank","training",86,76,74,4,5.3,9.8,TRUE,"",FALSE +"project2806_(1)","project2806 (1).nex",2806,1,"morphobank","training",37,6,6,10,27.9,0,TRUE,"",FALSE +"project2806_(2)","project2806 (2).nex",2806,2,"morphobank","training",37,165,162,7,5,20,TRUE,"",FALSE +"project2816","project2816.nex",2816,NA,"morphobank","training",57,323,323,4,37.9,2.7,TRUE,"",FALSE +"project291","project291.nex",291,NA,"morphobank","training",17,395,386,6,20.6,0,TRUE,"",FALSE +"project295","project295.nex",295,NA,"morphobank","validation",31,145,141,5,19.2,2.1,TRUE,"",FALSE +"project299","project299.nex",299,NA,"morphobank","training",30,144,129,9,7,4.6,TRUE,"",FALSE +"project3151","project3151.nex",3151,NA,"morphobank","training",20,107,107,6,18.8,0,TRUE,"",FALSE +"project3154","project3154.nex",3154,NA,"morphobank","training",33,209,193,6,35.6,4.2,TRUE,"",FALSE +"project316","project316.nex",316,NA,"morphobank","training",69,408,408,6,40.2,4.6,TRUE,"",FALSE +"project3165","project3165.nex",3165,NA,"morphobank","validation",28,59,59,5,41.2,0,TRUE,"",FALSE +"project3167","project3167.nex",3167,NA,"morphobank","training",13,47,46,11,35.1,0,TRUE,"",FALSE +"project3168","project3168.nex",3168,NA,"morphobank","training",90,415,415,6,45,1.3,TRUE,"",FALSE +"project3172","project3172.nex",3172,NA,"morphobank","training",43,227,223,4,49.4,0,TRUE,"",FALSE +"project3173","project3173.nex",3173,NA,"morphobank","training",95,419,419,5,43.9,0.2,TRUE,"",FALSE +"project3184","project3184.nex",3184,NA,"morphobank","training",39,52,52,5,16.9,2.6,TRUE,"",FALSE +"project3187","project3187.nex",3187,NA,"morphobank","training",25,101,84,4,14.8,0,TRUE,"",FALSE +"project3188","project3188.nex",3188,NA,"morphobank","training",30,77,76,4,27.8,0.8,TRUE,"",FALSE +"project3189","project3189.nex",3189,NA,"morphobank","training",47,211,206,7,23,14.4,TRUE,"",FALSE +"project3199","project3199.nex",3199,NA,"morphobank","training",88,168,138,3,0,1,TRUE,"",FALSE +"project3200","project3200.nex",3200,NA,"morphobank","validation",138,113,111,5,4.3,5.7,TRUE,"",FALSE +"project3203","project3203.nex",3203,NA,"morphobank","training",61,337,337,4,37.4,2.6,TRUE,"",FALSE +"project321","project321.nex",321,NA,"morphobank","training",81,661,656,6,38.6,0,TRUE,"",FALSE +"project3210","project3210.nex",3210,NA,"morphobank","validation",37,70,69,6,22.3,1,TRUE,"",FALSE +"project3211","project3211.nex",3211,NA,"morphobank","training",50,192,191,15,31.4,4.2,TRUE,"",FALSE +"project3212","project3212.nex",3212,NA,"morphobank","training",146,10,10,9,0.5,0,TRUE,"",FALSE +"project3216","project3216.nex",3216,NA,"morphobank","training",19,98,88,6,16.1,0.5,TRUE,"",FALSE +"project3234","project3234.nex",3234,NA,"morphobank","training",13,45,45,4,16.6,0.5,TRUE,"",FALSE +"project3239","project3239.nex",3239,NA,"morphobank","training",49,18,18,2,5.8,22.6,TRUE,"",FALSE +"project3244","project3244.nex",3244,NA,"morphobank","training",26,34,30,4,7.6,5.4,TRUE,"",FALSE +"project3249","project3249.nex",3249,NA,"morphobank","training",89,413,413,5,42.2,0,TRUE,"",FALSE +"project3253","project3253.nex",3253,NA,"morphobank","training",125,394,393,7,49,1.8,TRUE,"",FALSE +"project3260","project3260.nex",3260,NA,"morphobank","validation",18,74,74,8,26.9,3.4,TRUE,"",FALSE +"project3264","project3264.nex",3264,NA,"morphobank","training",66,303,301,9,41.4,1.6,TRUE,"",FALSE +"project3267","project3267.nex",3267,NA,"morphobank","training",68,355,352,5,61.3,0.3,TRUE,"",FALSE +"project3285","project3285.nex",3285,NA,"morphobank","validation",391,520,519,4,29.2,3.1,TRUE,"",FALSE +"project3287_Cassidulidae_complete","project3287_Cassidulidae_complete.nex",3287,NA,"morphobank","training",66,98,97,7,8.5,0.3,TRUE,"",FALSE +"project3287_Cassidulidae_without_partial_uncertainties","project3287_Cassidulidae_without partial uncertainties.nex",3287,NA,"morphobank","training",66,98,97,7,9.1,0.3,TRUE,"",FALSE +"project3293","project3293.nex",3293,NA,"morphobank","training",32,111,111,8,27.3,0,TRUE,"",FALSE +"project332","project332.nex",332,NA,"morphobank","training",22,107,105,3,25.7,0.3,TRUE,"",FALSE +"project3335","project3335.nex",3335,NA,"morphobank","validation",13,36,35,5,9.5,0,TRUE,"",FALSE +"project3345","project3345.nex",3345,NA,"morphobank","validation",44,77,70,8,1.5,11.2,TRUE,"",FALSE +"project3351","project3351.nex",3351,NA,"morphobank","training",34,143,138,7,23,0,TRUE,"",FALSE +"project3354_(1)","project3354 (1).nex",3354,1,"morphobank","training",78,18,18,8,11.8,10.3,TRUE,"",FALSE +"project3354_(2)","project3354 (2).nex",3354,2,"morphobank","training",78,121,120,8,25,4.6,TRUE,"",FALSE +"project3380","project3380.nex",3380,NA,"morphobank","validation",33,121,120,6,45.8,0.4,TRUE,"",FALSE +"project3381","project3381.nex",3381,NA,"morphobank","training",34,93,92,4,23.7,0.8,TRUE,"",FALSE +"project3384","project3384.nex",3384,NA,"morphobank","training",45,352,352,5,39.3,0,TRUE,"",FALSE +"project3385_(1)","project3385 (1).nex",3385,1,"morphobank","validation",96,555,454,5,55.8,21.3,TRUE,"",FALSE +"project3385_(2)","project3385 (2).nex",3385,2,"morphobank","validation",55,634,563,5,47,29,TRUE,"",FALSE +"project3392_(1)","project3392 (1).nex",3392,1,"morphobank","training",47,132,131,7,2.2,10.5,TRUE,"",TRUE +"project3392_(2)","project3392 (2).nex",3392,2,"morphobank","training",49,132,132,7,2.8,10.3,TRUE,"",FALSE +"project3392","project3392.nex",3392,NA,"morphobank","training",47,132,131,7,2.2,10.5,TRUE,"",FALSE +"project3400","project3400.nex",3400,NA,"morphobank","validation",24,38,36,5,11.6,0,TRUE,"WARNING: Could not parse character states; does each end with a ' or ;?.",FALSE +"project3405","project3405.nex",3405,NA,"morphobank","validation",100,324,321,3,8.4,13.6,TRUE,"",FALSE +"project3408","project3408.nex",3408,NA,"morphobank","training",19,30,24,4,19.5,0,TRUE,"",FALSE +"project3411","project3411.nex",3411,NA,"morphobank","training",84,530,528,8,39.4,16.9,TRUE,"",FALSE +"project3419","project3419.nex",3419,NA,"morphobank","training",40,368,343,7,5.4,0,TRUE,"",FALSE +"project3422","project3422.nex",3422,NA,"morphobank","training",110,278,277,6,41.2,4.4,TRUE,"",FALSE +"project3436","project3436.nex",3436,NA,"morphobank","training",99,245,245,7,31.3,10,TRUE,"",FALSE +"project3437","project3437.nex",3437,NA,"morphobank","training",64,89,89,7,17.4,19.1,TRUE,"",FALSE +"project3445","project3445.nex",3445,NA,"morphobank","validation",30,34,33,4,10.1,4.5,TRUE,"",FALSE +"project3448","project3448.nex",3448,NA,"morphobank","training",135,81,79,6,1.3,2.7,TRUE,"",FALSE +"project3456","project3456.nex",3456,NA,"morphobank","training",36,102,101,4,15.5,1.7,TRUE,"",FALSE +"project3466","project3466.nex",3466,NA,"morphobank","training",20,170,149,5,29.3,1.6,TRUE,"",FALSE +"project3470","project3470.nex",3470,NA,"morphobank","validation",55,303,299,9,35,1.7,TRUE,"",FALSE +"project3477","project3477.nex",3477,NA,"morphobank","training",21,109,109,6,16.5,0,TRUE,"",FALSE +"project3480","project3480.nex",3480,NA,"morphobank","validation",42,202,202,4,38.3,2.6,TRUE,"",FALSE +"project3489","project3489.nex",3489,NA,"morphobank","training",50,115,115,5,39.5,0.2,TRUE,"",FALSE +"project3497","project3497.nex",3497,NA,"morphobank","training",160,259,258,8,33,15.3,TRUE,"",FALSE +"project3501","project3501.nex",3501,NA,"morphobank","training",102,270,270,7,53.5,0,TRUE,"",FALSE +"project3508","project3508.nex",3508,NA,"morphobank","training",39,294,292,5,42.4,0,TRUE,"",FALSE +"project3509","project3509.nex",3509,NA,"morphobank","training",15,51,42,4,12.9,0.3,TRUE,"",FALSE +"project3512_(1)","project3512 (1).nex",3512,1,"morphobank","training",75,45,45,16,3.1,3.3,TRUE,"",FALSE +"project3512_(2)","project3512 (2).nex",3512,2,"morphobank","training",72,54,54,20,2,4.5,TRUE,"",FALSE +"project3512_(3)","project3512 (3).nex",3512,3,"morphobank","training",63,77,76,22,8,3.6,TRUE,"",FALSE +"project352_(1)","project352 (1).nex",352,1,"morphobank","training",61,88,88,5,2.7,1.2,TRUE,"",FALSE +"project352_(2)","project352 (2).nex",352,2,"morphobank","training",59,27,27,4,17.3,4,TRUE,"",FALSE +"project352_(3)","project352 (3).nex",352,3,"morphobank","training",59,19,19,5,1.8,0,TRUE,"",FALSE +"project3520","project3520.nex",3520,NA,"morphobank","validation",102,324,324,10,28.2,6.3,TRUE,"",FALSE +"project3521","project3521.nex",3521,NA,"morphobank","training",93,156,156,4,47.7,0,TRUE,"",FALSE +"project3533","project3533.nex",3533,NA,"morphobank","training",42,85,84,2,18.9,4.4,TRUE,"",FALSE +"project3538","project3538.nex",3538,NA,"morphobank","training",99,138,121,8,4.3,9.7,TRUE,"",FALSE +"project3541_(1)","project3541 (1).nex",3541,1,"morphobank","training",24,74,74,4,26.3,0.3,TRUE,"",FALSE +"project3541_(2)","project3541 (2).nex",3541,2,"morphobank","training",22,74,74,4,28,0.3,TRUE,"",TRUE +"project3544","project3544.nex",3544,NA,"morphobank","training",34,120,115,4,27.1,0,TRUE,"",FALSE +"project3558","project3558.nex",3558,NA,"morphobank","training",86,59,59,5,19,10.2,TRUE,"",FALSE +"project3561","project3561.nex",3561,NA,"morphobank","training",36,110,109,5,4,3.2,TRUE,"",FALSE +"project3569","project3569.nex",3569,NA,"morphobank","training",43,97,96,5,24.2,0,TRUE,"",FALSE +"project3575","project3575.nex",3575,NA,"morphobank","validation",21,37,35,3,24.1,0,TRUE,"",FALSE +"project3581","project3581.nex",3581,NA,"morphobank","training",56,63,61,4,16.5,18,TRUE,"",FALSE +"project3587","project3587.nex",3587,NA,"morphobank","training",106,194,193,7,29.8,7.1,TRUE,"",FALSE +"project3592","project3592.nex",3592,NA,"morphobank","training",41,99,95,6,3.8,1.7,TRUE,"",FALSE +"project3597","project3597.nex",3597,NA,"morphobank","training",62,2,2,2,43.5,0,TRUE,"",FALSE +"project3599","project3599.nex",3599,NA,"morphobank","training",54,128,121,5,2.7,3.7,TRUE,"",FALSE +"project360","project360.nex",360,NA,"morphobank","validation",38,34,34,4,5.4,0,TRUE,"",FALSE +"project3601","project3601.nex",3601,NA,"morphobank","training",24,52,51,4,27.7,0,TRUE,"",FALSE +"project3602","project3602.nex",3602,NA,"morphobank","training",105,197,196,7,30,7,TRUE,"",FALSE +"project3603","project3603.nex",3603,NA,"morphobank","training",70,14,14,5,1.7,0.9,TRUE,"",FALSE +"project3613","project3613.nex",3613,NA,"morphobank","training",49,63,62,4,10.5,17.2,TRUE,"",FALSE +"project3617","project3617.nex",3617,NA,"morphobank","training",65,361,361,7,32.4,3.9,TRUE,"",FALSE +"project3619","project3619.nex",3619,NA,"morphobank","training",21,57,52,6,1.8,0.4,TRUE,"",FALSE +"project3621","project3621.nex",3621,NA,"morphobank","training",62,245,245,5,52.4,0,TRUE,"",FALSE +"project3625","project3625.nex",3625,NA,"morphobank","validation",27,57,55,4,1.6,0,TRUE,"",FALSE +"project3626","project3626.nex",3626,NA,"morphobank","training",22,57,57,7,18.6,0,TRUE,"",FALSE +"project3627","project3627.nex",3627,NA,"morphobank","training",22,74,74,5,28.6,0.3,TRUE,"",FALSE +"project3637","project3637.nex",3637,NA,"morphobank","training",86,530,528,8,39.1,17.1,TRUE,"",FALSE +"project3646","project3646.nex",3646,NA,"morphobank","training",69,202,193,6,0.9,9.1,TRUE,"",FALSE +"project365","project365.nex",365,NA,"morphobank","validation",22,75,75,6,23.2,2,TRUE,"",FALSE +"project3655","project3655.nex",3655,NA,"morphobank","validation",45,77,72,5,28.9,4.7,TRUE,"",FALSE +"project3656","project3656.nex",3656,NA,"morphobank","training",61,339,339,4,37.1,2.7,TRUE,"",FALSE +"project3664","project3664.nex",3664,NA,"morphobank","training",20,26,25,6,5.4,0,TRUE,"",FALSE +"project3665","project3665.nex",3665,NA,"morphobank","validation",39,297,297,5,15.3,3,TRUE,"",FALSE +"project367","project367.nex",367,NA,"morphobank","training",51,216,215,4,38.3,0,TRUE,"",FALSE +"project3670","project3670.nex",3670,NA,"morphobank","validation",62,120,115,8,10,9.1,TRUE,"",FALSE +"project3672_(1)","project3672 (1).nex",3672,1,"morphobank","training",11,54,35,6,7.8,0,TRUE,"",FALSE +"project3672_(2)","project3672 (2).nex",3672,2,"morphobank","training",11,54,35,6,8.8,0,TRUE,"",FALSE +"project3677","project3677.nex",3677,NA,"morphobank","training",50,98,97,4,19.4,0,TRUE,"",FALSE +"project3684","project3684.nex",3684,NA,"morphobank","training",36,248,244,6,33.9,1.5,TRUE,"",FALSE +"project3685","project3685.nex",3685,NA,"morphobank","validation",37,257,257,6,49.4,0,TRUE,"",FALSE +"project3687","project3687.nex",3687,NA,"morphobank","training",43,252,252,6,54.3,0,TRUE,"",FALSE +"project3688","project3688.nex",3688,NA,"morphobank","training",60,245,245,7,57.3,0,TRUE,"",FALSE +"project3695","project3695.nex",3695,NA,"morphobank","validation",40,45,44,4,8.6,0.2,TRUE,"",FALSE +"project3696","project3696.nex",3696,NA,"morphobank","training",22,22,21,4,5.8,0.2,TRUE,"",FALSE +"project3698","project3698.nex",3698,NA,"morphobank","training",20,62,52,4,14,0,TRUE,"",FALSE +"project3701","project3701.nex",3701,NA,"morphobank","training",146,324,324,10,30.9,15.1,TRUE,"",FALSE +"project3705","project3705.nex",3705,NA,"morphobank","validation",27,193,185,7,7.7,2.7,TRUE,"",FALSE +"project3707","project3707.nex",3707,NA,"morphobank","training",151,131,121,10,0.6,25.9,TRUE,"",FALSE +"project3708","project3708.nex",3708,NA,"morphobank","training",69,254,254,8,38.9,0,TRUE,"",FALSE +"project3709","project3709.nex",3709,NA,"morphobank","training",42,65,39,2,0,0,TRUE,"",FALSE +"project3710","project3710.nex",3710,NA,"morphobank","validation",115,65,39,2,0,0,TRUE,"",FALSE +"project3711","project3711.nex",3711,NA,"morphobank","training",79,132,130,4,3.6,13.7,TRUE,"",FALSE +"project3725","project3725.nex",3725,NA,"morphobank","validation",90,189,187,6,50.4,2.3,TRUE,"",FALSE +"project3726","project3726.nex",3726,NA,"morphobank","training",76,146,146,4,29.7,4.8,TRUE,"",FALSE +"project3730","project3730.nex",3730,NA,"morphobank","validation",21,107,107,6,17.8,0,TRUE,"",FALSE +"project3733","project3733.nex",3733,NA,"morphobank","training",157,853,841,8,65.5,0,TRUE,"",FALSE +"project3740","project3740.nex",3740,NA,"morphobank","validation",66,39,39,5,2.7,24.1,TRUE,"",FALSE +"project3741","project3741.nex",3741,NA,"morphobank","training",86,110,107,6,4.3,14.7,TRUE,"",FALSE +"project3742","project3742.nex",3742,NA,"morphobank","training",9,16,13,4,9.4,0.9,TRUE,"",FALSE +"project3755","project3755.nex",3755,NA,"morphobank","validation",46,201,201,5,8.4,4.4,TRUE,"",FALSE +"project3756","project3756.nex",3756,NA,"morphobank","training",34,69,69,4,36.3,0.6,TRUE,"",FALSE +"project3757","project3757.nex",3757,NA,"morphobank","training",59,61,60,7,6,3.6,TRUE,"",FALSE +"project3760_(1)","project3760 (1).nex",3760,1,"morphobank","validation",130,509,506,7,49.7,0,TRUE,"",FALSE +"project3760_(2)","project3760 (2).nex",3760,2,"morphobank","validation",130,509,506,12,49.7,0,TRUE,"",TRUE +"project3763","project3763.nex",3763,NA,"morphobank","training",205,105,103,6,10.8,8.5,TRUE,"",FALSE +"project3766","project3766.nex",3766,NA,"morphobank","training",89,286,283,8,31.7,1.5,TRUE,"",FALSE +"project3768","project3768.nex",3768,NA,"morphobank","training",79,214,213,7,29.6,1.6,TRUE,"",FALSE +"project3769","project3769.nex",3769,NA,"morphobank","training",76,123,120,6,0.3,1.8,TRUE,"",FALSE +"project3773","project3773.nex",3773,NA,"morphobank","training",194,823,812,6,59.4,4.3,TRUE,"",FALSE +"project3782","project3782.nex",3782,NA,"morphobank","training",83,163,163,6,33.9,0,TRUE,"",FALSE +"project3785","project3785.nex",3785,NA,"morphobank","validation",21,27,26,4,29.9,3.3,TRUE,"",FALSE +"project3794","project3794.nex",3794,NA,"morphobank","training",24,65,65,8,18.9,3.8,TRUE,"",FALSE +"project380","project380.nex",380,NA,"morphobank","validation",17,164,152,5,17.8,1,TRUE,"",FALSE +"project3804","project3804.nex",3804,NA,"morphobank","training",54,117,113,7,11.6,19.3,TRUE,"",FALSE +"project3806","project3806.nex",3806,NA,"morphobank","training",202,746,746,7,72.3,1.2,TRUE,"",FALSE +"project3807","project3807.nex",3807,NA,"morphobank","training",96,83,81,10,7.8,5.6,TRUE,"",FALSE +"project3812","project3812.nex",3812,NA,"morphobank","training",98,568,568,7,55.9,0,TRUE,"",FALSE +"project3818","project3818.nex",3818,NA,"morphobank","training",49,206,206,4,41.2,2.7,TRUE,"",FALSE +"project3825","project3825.nex",3825,NA,"morphobank","validation",136,37,18,10,12.2,43.4,TRUE,"",FALSE +"project383","project383.nex",383,NA,"morphobank","training",27,84,84,3,10.9,1.6,TRUE,"",FALSE +"project3831","project3831.nex",3831,NA,"morphobank","training",46,134,128,4,27.1,0,TRUE,"",FALSE +"project3832","project3832.nex",3832,NA,"morphobank","training",10,27,17,3,2.4,0.6,TRUE,"",FALSE +"project3833","project3833.nex",3833,NA,"morphobank","training",48,69,68,5,3.2,1.7,TRUE,"",FALSE +"project3854","project3854.nex",3854,NA,"morphobank","training",89,188,186,9,50.4,2.3,TRUE,"",FALSE +"project386","project386.nex",386,NA,"morphobank","training",10,21,19,3,3.7,0,TRUE,"",FALSE +"project3868","project3868.nex",3868,NA,"morphobank","training",20,42,34,3,10.6,1.9,TRUE,"",FALSE +"project3874","project3874.nex",3874,NA,"morphobank","training",54,125,125,6,16.5,45.5,TRUE,"",FALSE +"project3887_(1)","project3887 (1).nex",3887,1,"morphobank","training",55,275,272,8,43.6,14.2,TRUE,"",FALSE +"project3887_(2)","project3887 (2).nex",3887,2,"morphobank","training",196,823,823,6,58.8,4.2,TRUE,"",FALSE +"project3894","project3894.nex",3894,NA,"morphobank","training",58,148,128,8,4.6,0,TRUE,"",FALSE +"project3896","project3896.nex",3896,NA,"morphobank","training",72,207,201,4,2.5,8.6,TRUE,"",FALSE +"project3898","project3898.nex",3898,NA,"morphobank","training",85,143,143,8,11.9,0,TRUE,"",FALSE +"project3906","project3906.nex",3906,NA,"morphobank","training",54,58,58,8,18.6,5.3,TRUE,"",FALSE +"project3908","project3908.nex",3908,NA,"morphobank","training",51,364,364,5,40.2,1.3,TRUE,"",FALSE +"project3910","project3910.nex",3910,NA,"morphobank","validation",135,28,28,2,13,0,TRUE,"",FALSE +"project3914","project3914.nex",3914,NA,"morphobank","training",13,86,76,5,15.9,0,TRUE,"",FALSE +"project3916","project3916.nex",3916,NA,"morphobank","training",25,140,70,5,2.1,0.7,TRUE,"",FALSE +"project3927","project3927.nex",3927,NA,"morphobank","training",63,154,154,7,39.6,15.4,TRUE,"",FALSE +"project3929","project3929.nex",3929,NA,"morphobank","training",40,130,122,5,14,0.8,TRUE,"",FALSE +"project3930","project3930.nex",3930,NA,"morphobank","validation",32,84,84,5,12.1,6,TRUE,"",FALSE +"project3931","project3931.nex",3931,NA,"morphobank","training",115,287,287,4,49.9,2.3,TRUE,"",FALSE +"project3932","project3932.nex",3932,NA,"morphobank","training",72,170,169,5,37.1,2.3,TRUE,"",FALSE +"project3933","project3933.nex",3933,NA,"morphobank","training",21,43,42,4,44.2,0.2,TRUE,"",FALSE +"project3934","project3934.nex",3934,NA,"morphobank","training",85,418,418,5,42.6,0,TRUE,"",FALSE +"project3935","project3935.nex",3935,NA,"morphobank","validation",10,36,30,5,27.7,0,TRUE,"",FALSE +"project3936","project3936.nex",3936,NA,"morphobank","training",42,170,166,5,29.8,2,TRUE,"",FALSE +"project3938","project3938.nex",3938,NA,"morphobank","training",119,677,677,6,52.6,4.3,TRUE,"",FALSE +"project3939","project3939.nex",3939,NA,"morphobank","training",32,57,57,7,35.4,0,TRUE,"",FALSE +"project3941","project3941.nex",3941,NA,"morphobank","training",80,600,600,6,45.1,4.2,TRUE,"",FALSE +"project3942","project3942.nex",3942,NA,"morphobank","training",33,102,94,4,16.4,3.9,TRUE,"",FALSE +"project3943","project3943.nex",3943,NA,"morphobank","training",121,551,548,7,52.6,0,TRUE,"",FALSE +"project3951_(1)","project3951 (1).nex",3951,1,"morphobank","training",41,107,106,7,34.3,1.3,TRUE,"",FALSE +"project3951_(2)","project3951 (2).nex",3951,2,"morphobank","training",1,1,1,0,0,0,TRUE,"",FALSE +"project3951_(3)","project3951 (3).nex",3951,3,"morphobank","training",1,1,1,0,0,0,TRUE,"",FALSE +"project3955","project3955.nex",3955,NA,"morphobank","validation",76,395,394,8,60.5,0.5,TRUE,"",FALSE +"project3958","project3958.nex",3958,NA,"morphobank","training",79,284,268,8,42.3,8.4,TRUE,"",FALSE +"project3964","project3964.nex",3964,NA,"morphobank","training",79,419,419,6,52.3,1.6,TRUE,"",FALSE +"project3970","project3970.nex",3970,NA,"morphobank","validation",68,339,339,4,39.5,2.4,TRUE,"",FALSE +"project3978","project3978.nex",3978,NA,"morphobank","training",58,164,164,4,18.1,1.6,TRUE,"",FALSE +"project3989","project3989.nex",3989,NA,"morphobank","training",25,181,170,6,13.9,6.8,TRUE,"",FALSE +"project4010_(1)","project4010 (1).nex",4010,1,"morphobank","validation",28,112,112,3,2.5,0,TRUE,"",TRUE +"project4010_(2)","project4010 (2).nex",4010,2,"morphobank","validation",40,112,112,3,28.2,0,TRUE,"",FALSE +"project4010_(3)","project4010 (3).nex",4010,3,"morphobank","validation",28,112,112,3,3.2,0,TRUE,"",TRUE +"project402","project402.nex",402,NA,"morphobank","training",32,80,80,6,0.2,18,TRUE,"",FALSE +"project4034","project4034.nex",4034,NA,"morphobank","training",37,218,214,3,63.5,0.1,TRUE,"",FALSE +"project4044","project4044.nex",4044,NA,"morphobank","training",30,93,83,2,8.7,4.5,TRUE,"",FALSE +"project4049","project4049.nex",4049,NA,"morphobank","training",60,721,719,5,22.2,0,TRUE,"",FALSE +"project4056","project4056.nex",4056,NA,"morphobank","training",16,568,560,11,37.2,4.2,TRUE,"",FALSE +"project4066","project4066.nex",4066,NA,"morphobank","training",26,27,26,5,4.7,1.5,TRUE,"",FALSE +"project407","project407.nex",407,NA,"morphobank","training",23,25,25,4,6.1,4.2,TRUE,"",FALSE +"project4074","project4074.nex",4074,NA,"morphobank","training",12,26,24,3,12.8,0.7,TRUE,"",FALSE +"project4077","project4077.nex",4077,NA,"morphobank","training",52,101,100,9,9.9,0,TRUE,"",FALSE +"project4078","project4078.nex",4078,NA,"morphobank","training",80,192,190,5,17.3,22.4,TRUE,"",FALSE +"project408","project408.nex",408,NA,"morphobank","training",27,77,75,3,7.7,0,TRUE,"",FALSE +"project4085","project4085.nex",4085,NA,"morphobank","validation",164,716,716,7,58.2,4,TRUE,"",FALSE +"project4087","project4087.nex",4087,NA,"morphobank","training",27,71,60,5,9,0,TRUE,"",FALSE +"project4091","project4091.nex",4091,NA,"morphobank","training",7,25,20,3,22.1,2.9,TRUE,"",FALSE +"project4095","project4095.nex",4095,NA,"morphobank","validation",21,26,26,4,11,0.2,TRUE,"",FALSE +"project4103","project4103.nex",4103,NA,"morphobank","training",144,159,152,6,1.3,6.1,TRUE,"",FALSE +"project4104","project4104.nex",4104,NA,"morphobank","training",64,92,88,5,52.9,1.1,TRUE,"",FALSE +"project4111","project4111.nex",4111,NA,"morphobank","training",74,102,100,5,0,7.1,TRUE,"",FALSE +"project4112","project4112.nex",4112,NA,"morphobank","training",30,100,92,5,19.1,20.6,TRUE,"",FALSE +"project4119","project4119.nex",4119,NA,"morphobank","training",32,69,66,4,41.6,1.7,TRUE,"",FALSE +"project4123_(1)","project4123 (1).nex",4123,1,"morphobank","training",39,187,187,3,32.1,5.8,TRUE,"",FALSE +"project4123_(2)","project4123 (2).nex",4123,2,"morphobank","training",39,173,173,4,33.7,5.3,TRUE,"",FALSE +"project4125","project4125.nex",4125,NA,"morphobank","validation",59,156,155,5,51.3,0,TRUE,"",FALSE +"project4126","project4126.nex",4126,NA,"morphobank","training",30,106,101,3,32,0,TRUE,"",FALSE +"project4133","project4133.nex",4133,NA,"morphobank","training",131,349,349,5,31.3,6,TRUE,"",FALSE +"project4135","project4135.nex",4135,NA,"morphobank","validation",29,78,77,4,20.9,0.5,TRUE,"",FALSE +"project4138","project4138.nex",4138,NA,"morphobank","training",131,45,45,3,20.2,0,TRUE,"",FALSE +"project4146_(1)","project4146 (1).nex",4146,1,"morphobank","training",57,129,129,6,17.1,46,TRUE,"",FALSE +"project4146_(2)","project4146 (2).nex",4146,2,"morphobank","training",56,129,129,6,16.5,46.2,TRUE,"",TRUE +"project4146_(3)","project4146 (3).nex",4146,3,"morphobank","training",59,130,130,7,18.1,45.6,TRUE,"",FALSE +"project4146_(4)","project4146 (4).nex",4146,4,"morphobank","training",59,130,130,7,18.4,45.6,TRUE,"",TRUE +"project4146_(5)","project4146 (5).nex",4146,5,"morphobank","training",56,131,130,6,16.3,46.1,TRUE,"",FALSE +"project4146_(6)","project4146 (6).nex",4146,6,"morphobank","training",56,130,130,6,16.2,46.1,TRUE,"",FALSE +"project4146_(7)","project4146 (7).nex",4146,7,"morphobank","training",56,130,129,6,16.5,46.3,TRUE,"",TRUE +"project4146_(8)","project4146 (8).nex",4146,8,"morphobank","training",56,129,129,6,16.5,46.2,TRUE,"",TRUE +"project4147","project4147.nex",4147,NA,"morphobank","training",71,153,150,7,36.5,14.4,TRUE,"",FALSE +"project4149","project4149.nex",4149,NA,"morphobank","training",40,178,178,4,17.4,1.1,TRUE,"",FALSE +"project4163","project4163.nex",4163,NA,"morphobank","training",33,72,72,4,44.7,0,TRUE,"",FALSE +"project4166","project4166.nex",4166,NA,"morphobank","training",63,355,355,4,23.4,5.6,TRUE,"",FALSE +"project4168","project4168.nex",4168,NA,"morphobank","training",43,46,45,5,6.8,0,TRUE,"",FALSE +"project4169","project4169.nex",4169,NA,"morphobank","training",34,88,88,4,45.9,0.3,TRUE,"",FALSE +"project417","project417.nex",417,NA,"morphobank","training",12,39,37,4,20.7,1.4,TRUE,"",FALSE +"project4171","project4171.nex",4171,NA,"morphobank","training",13,39,38,4,31.6,1.8,TRUE,"",FALSE +"project4173","project4173.nex",4173,NA,"morphobank","training",81,155,136,8,0.3,13.2,TRUE,"",FALSE +"project4174","project4174.nex",4174,NA,"morphobank","training",13,30,22,4,7.3,0,TRUE,"",FALSE +"project4176","project4176.nex",4176,NA,"morphobank","training",148,22,22,5,20.6,0,TRUE,"",FALSE +"project4181","project4181.nex",4181,NA,"morphobank","training",54,219,219,4,17.1,5.8,TRUE,"",FALSE +"project4182","project4182.nex",4182,NA,"morphobank","training",22,29,29,4,9.1,6.1,TRUE,"",FALSE +"project4183","project4183.nex",4183,NA,"morphobank","training",36,20,14,4,0,7.5,TRUE,"",FALSE +"project4184","project4184.nex",4184,NA,"morphobank","training",106,435,435,9,61.4,0.9,TRUE,"",FALSE +"project4185","project4185.nex",4185,NA,"morphobank","validation",41,88,85,4,49.8,1.1,TRUE,"",FALSE +"project4186","project4186.nex",4186,NA,"morphobank","training",48,33,33,8,4,0,TRUE,"",FALSE +"project4187","project4187.nex",4187,NA,"morphobank","training",10,7,7,3,1.4,1.4,TRUE,"",FALSE +"project4190","project4190.nex",4190,NA,"morphobank","validation",50,89,82,3,2.9,0,TRUE,"",FALSE +"project4192_(1)","project4192 (1).nex",4192,1,"morphobank","training",42,104,101,5,16.1,3.7,TRUE,"",FALSE +"project4192_(2)","project4192 (2).nex",4192,2,"morphobank","training",42,104,101,5,16.1,3.7,TRUE,"",TRUE +"project4204","project4204.nex",4204,NA,"morphobank","training",163,37,37,2,3.7,0.8,TRUE,"",FALSE +"project4210","project4210.nex",4210,NA,"morphobank","validation",43,235,234,3,65.9,0,TRUE,"",FALSE +"project4220","project4220.nex",4220,NA,"morphobank","validation",47,48,45,4,3.4,9.6,TRUE,"",FALSE +"project423","project423.nex",423,NA,"morphobank","training",60,253,219,5,12.2,15.4,TRUE,"",FALSE +"project4230","project4230.nex",4230,NA,"morphobank","validation",125,302,302,4,29.4,8,TRUE,"",FALSE +"project4235","project4235.nex",4235,NA,"morphobank","validation",13,93,90,3,16.3,0,TRUE,"",FALSE +"project4255","project4255.nex",4255,NA,"morphobank","validation",24,106,106,5,28.3,0,TRUE,"",FALSE +"project4263","project4263.nex",4263,NA,"morphobank","training",4,35,7,3,7.1,0,TRUE,"",FALSE +"project4264_(1)","project4264 (1).nex",4264,1,"morphobank","training",112,441,441,6,66.2,0.3,TRUE,"",FALSE +"project4264_(2)","project4264 (2).nex",4264,2,"morphobank","training",104,394,394,10,61.5,1.1,TRUE,"",FALSE +"project4265","project4265.nex",4265,NA,"morphobank","validation",13,82,72,4,13.5,1,TRUE,"",FALSE +"project427","project427.nex",427,NA,"morphobank","training",223,364,364,10,41.6,3.9,TRUE,"",FALSE +"project4271_Modified_Herrera_et_al._(2021)","project4271_Modified Herrera et al. (2021).nex",4271,2021,"morphobank","training",169,519,519,7,52.3,3.6,TRUE,"",FALSE +"project4271_Modified_Wilberg_et_al._2019","project4271_Modified Wilberg et al. 2019.nex",4271,NA,"morphobank","training",105,410,410,6,40.7,4.1,TRUE,"",FALSE +"project4278","project4278.nex",4278,NA,"morphobank","training",78,214,213,7,31.1,0,TRUE,"",FALSE +"project4281","project4281.nex",4281,NA,"morphobank","training",61,146,145,8,43.8,3.8,TRUE,"",FALSE +"project4284","project4284.nex",4284,NA,"morphobank","training",4062,27,27,5,82.9,2.6,TRUE,"",FALSE +"project4285","project4285.nex",4285,NA,"morphobank","validation",81,155,136,8,0.3,13.2,TRUE,"",FALSE +"project4286","project4286.nex",4286,NA,"morphobank","training",63,135,135,7,18.7,46.7,TRUE,"",FALSE +"project4288","project4288.nex",4288,NA,"morphobank","training",14,37,35,4,15.7,0,TRUE,"",FALSE +"project429","project429.nex",429,NA,"morphobank","training",36,65,49,4,0.5,11.4,TRUE,"",FALSE +"project4291_(1)","project4291 (1).nex",4291,1,"morphobank","training",63,246,246,4,40.6,1.5,TRUE,"",TRUE +"project4291_(2)","project4291 (2).nex",4291,2,"morphobank","training",66,246,246,4,43.8,1.5,TRUE,"",FALSE +"project4291_(3)","project4291 (3).nex",4291,3,"morphobank","training",78,246,246,4,51,1.2,TRUE,"",FALSE +"project4291","project4291.nex",4291,NA,"morphobank","training",78,246,246,4,51,1.2,TRUE,"",FALSE +"project4299_(1)","project4299 (1).nex",4299,1,"morphobank","training",15,34,31,4,19.8,0,TRUE,"",FALSE +"project4299_(2)","project4299 (2).nex",4299,2,"morphobank","training",16,34,33,4,22.9,0,TRUE,"",FALSE +"project4299_(3)","project4299 (3).nex",4299,3,"morphobank","training",18,33,32,4,20.3,0,TRUE,"",FALSE +"project4299_(4)","project4299 (4).nex",4299,4,"morphobank","training",24,33,33,4,28.4,0,TRUE,"",FALSE +"project4300","project4300.nex",4300,NA,"morphobank","validation",158,717,717,7,57.9,3.9,TRUE,"",FALSE +"project4304","project4304.nex",4304,NA,"morphobank","training",29,91,91,5,19.6,0,TRUE,"",FALSE +"project4305","project4305.nex",4305,NA,"morphobank","validation",36,65,62,4,10.2,0.3,TRUE,"",FALSE +"project4306","project4306.nex",4306,NA,"morphobank","training",73,244,233,8,41.1,10.2,TRUE,"",FALSE +"project4307_(1)","project4307 (1).nex",4307,1,"morphobank","training",71,246,237,7,41.2,9.6,TRUE,"",TRUE +"project4307_(2)","project4307 (2).nex",4307,2,"morphobank","training",72,246,237,7,41.7,9.6,TRUE,"",FALSE +"project4308","project4308.nex",4308,NA,"morphobank","training",27,68,65,6,22.6,0,TRUE,"",FALSE +"project4309","project4309.nex",4309,NA,"morphobank","training",16,68,65,6,11.8,0,TRUE,"",FALSE +"project431","project431.nex",431,NA,"morphobank","training",64,141,141,4,30,5.7,TRUE,"",FALSE +"project4310","project4310.nex",4310,NA,"morphobank","validation",48,46,40,5,19,2.7,TRUE,"",FALSE +"project4311","project4311.nex",4311,NA,"morphobank","training",4,35,7,3,7.1,0,TRUE,"",FALSE +"project4313","project4313.nex",4313,NA,"morphobank","training",41,125,124,6,27.7,0,TRUE,"",FALSE +"project4315","project4315.nex",4315,NA,"morphobank","validation",55,74,74,8,27,2.7,TRUE,"",FALSE +"project4317","project4317.nex",4317,NA,"morphobank","training",98,284,275,8,50.7,5.8,TRUE,"",FALSE +"project4318","project4318.nex",4318,NA,"morphobank","training",15,27,23,4,24.1,0,TRUE,"",FALSE +"project4319","project4319.nex",4319,NA,"morphobank","training",65,52,46,4,4.3,0,TRUE,"",FALSE +"project4326","project4326.nex",4326,NA,"morphobank","training",25,57,47,3,8.2,0,TRUE,"",FALSE +"project4327","project4327.nex",4327,NA,"morphobank","training",197,823,823,6,58.3,4.3,TRUE,"",FALSE +"project4328","project4328.nex",4328,NA,"morphobank","training",27,60,57,4,23.7,0,TRUE,"",FALSE +"project4329","project4329.nex",4329,NA,"morphobank","training",47,4,4,4,0,0,TRUE,"",FALSE +"project4332","project4332.nex",4332,NA,"morphobank","training",47,359,359,5,41.4,0,TRUE,"",FALSE +"project4333","project4333.nex",4333,NA,"morphobank","training",58,223,222,4,38.6,0,TRUE,"",FALSE +"project4335","project4335.nex",4335,NA,"morphobank","validation",49,359,359,5,40.8,0,TRUE,"",FALSE +"project4340","project4340.nex",4340,NA,"morphobank","validation",78,214,213,7,31.1,0,TRUE,"",FALSE +"project4348","project4348.nex",4348,NA,"morphobank","training",87,142,141,4,20.8,6.3,TRUE,"",FALSE +"project4356","project4356.nex",4356,NA,"morphobank","training",18,43,42,5,10.6,1.7,TRUE,"",FALSE +"project4358","project4358.nex",4358,NA,"morphobank","training",104,140,134,5,5.6,0.3,TRUE,"",FALSE +"project4359","project4359.nex",4359,NA,"morphobank","training",71,245,146,7,83.8,3.1,TRUE,"",FALSE +"project4363","project4363.nex",4363,NA,"morphobank","training",36,76,71,5,3.1,1.8,TRUE,"",FALSE +"project4364","project4364.nex",4364,NA,"morphobank","training",21,40,40,4,10.4,0.6,TRUE,"",FALSE +"project4372","project4372.nex",4372,NA,"morphobank","training",25,57,57,7,22.7,0,TRUE,"",FALSE +"project4376","project4376.nex",4376,NA,"morphobank","training",17,31,22,3,14.4,0,TRUE,"",FALSE +"project4377","project4377.nex",4377,NA,"morphobank","training",160,182,153,4,0.7,5.6,TRUE,"",FALSE +"project4390","project4390.nex",4390,NA,"morphobank","validation",27,109,108,6,15.4,0,TRUE,"",FALSE +"project4392","project4392.nex",4392,NA,"morphobank","training",55,265,261,6,50.6,1.2,TRUE,"",FALSE +"project4396","project4396.nex",4396,NA,"morphobank","training",19,48,42,4,23.2,0,TRUE,"",FALSE +"project4397","project4397.nex",4397,NA,"morphobank","training",75,223,222,4,32.3,4.6,TRUE,"",FALSE +"project44","project44.nex",44,NA,"morphobank","training",27,46,45,7,7.2,0,TRUE,"",FALSE +"project4400","project4400.nex",4400,NA,"morphobank","validation",99,419,419,5,44.8,0.2,TRUE,"",FALSE +"project4405","project4405.nex",4405,NA,"morphobank","validation",74,215,214,4,33.9,4.5,TRUE,"",FALSE +"project4406","project4406.nex",4406,NA,"morphobank","training",42,115,108,4,8.9,23.1,TRUE,"",FALSE +"project441","project441.nex",441,NA,"morphobank","training",61,231,227,6,10.1,10.4,TRUE,"",FALSE +"project4411","project4411.nex",4411,NA,"morphobank","training",121,443,443,6,56.5,0,TRUE,"",FALSE +"project4415","project4415.nex",4415,NA,"morphobank","validation",28,87,87,4,30.5,2.3,TRUE,"",FALSE +"project4416","project4416.nex",4416,NA,"morphobank","training",7,9,9,2,9.5,0,TRUE,"",FALSE +"project4417","project4417.nex",4417,NA,"morphobank","training",24,63,63,7,6.6,0,TRUE,"",FALSE +"project4420","project4420.nex",4420,NA,"morphobank","validation",68,61,60,5,4.6,9.9,TRUE,"",FALSE +"project4421","project4421.nex",4421,NA,"morphobank","training",34,86,77,4,21.8,6.5,TRUE,"",FALSE +"project4422","project4422.nex",4422,NA,"morphobank","training",67,93,93,7,18.3,19.7,TRUE,"",FALSE +"project4430_(1)","project4430 (1).nex",4430,1,"morphobank","validation",121,176,169,8,3.9,5,TRUE,"",FALSE +"project4431","project4431.nex",4431,NA,"morphobank","training",40,123,122,6,25.7,0,TRUE,"",FALSE +"project4434","project4434.nex",4434,NA,"morphobank","training",110,130,130,7,11.1,12.2,TRUE,"",FALSE +"project4445","project4445.nex",4445,NA,"morphobank","validation",104,268,268,6,44.7,1.6,TRUE,"",FALSE +"project4446_(1)","project4446 (1).nex",4446,1,"morphobank","training",199,1773,1742,2,79.9,0,TRUE,"",FALSE +"project4446_(2)","project4446 (2).nex",4446,2,"morphobank","training",153,860,859,8,65.6,0,TRUE,"",FALSE +"project4449","project4449.nex",4449,NA,"morphobank","training",105,268,267,6,44.3,1.8,TRUE,"",FALSE +"project4458","project4458.nex",4458,NA,"morphobank","training",25,81,81,4,25,0.1,TRUE,"",FALSE +"project4460","project4460.nex",4460,NA,"morphobank","validation",61,167,167,6,43.5,8.6,TRUE,"",FALSE +"project4461","project4461.nex",4461,NA,"morphobank","training",44,95,95,5,34.5,0,TRUE,"",FALSE +"project4467","project4467.nex",4467,NA,"morphobank","training",47,48,45,4,3.4,9.6,TRUE,"",FALSE +"project4469","project4469.nex",4469,NA,"morphobank","training",110,287,287,4,46.5,2.8,TRUE,"",FALSE +"project4473","project4473.nex",4473,NA,"morphobank","training",37,87,87,5,17.5,0,TRUE,"",FALSE +"project449","project449.nex",449,NA,"morphobank","training",24,43,43,4,25.9,0,TRUE,"",FALSE +"project4495","project4495.nex",4495,NA,"morphobank","validation",22,61,61,4,27.2,2.5,TRUE,"",FALSE +"project4496","project4496.nex",4496,NA,"morphobank","training",28,83,83,4,33.3,0,TRUE,"",FALSE +"project4499","project4499.nex",4499,NA,"morphobank","training",66,96,91,6,12.3,2,TRUE,"",FALSE +"project45","project45.nex",45,NA,"morphobank","validation",40,65,58,4,23.9,2.9,TRUE,"",FALSE +"project450","project450.nex",450,NA,"morphobank","validation",40,14,14,4,0.5,0,TRUE,"",FALSE +"project4501","project4501.nex",4501,NA,"morphobank","training",24,42,41,6,4.1,13.1,TRUE,"",FALSE +"project4516","project4516.nex",4516,NA,"morphobank","training",70,41,41,6,0.6,0,TRUE,"",FALSE +"project4517","project4517.nex",4517,NA,"morphobank","training",99,285,282,8,33.8,1.4,TRUE,"",FALSE +"project4531","project4531.nex",4531,NA,"morphobank","training",71,256,252,8,40.8,0,TRUE,"",FALSE +"project4532_(1)","project4532 (1).nex",4532,1,"morphobank","training",33,74,72,4,21.1,12.1,TRUE,"",FALSE +"project4532_(2)","project4532 (2).nex",4532,2,"morphobank","training",33,72,71,4,21.3,11.2,TRUE,"",FALSE +"project4532_(3)","project4532 (3).nex",4532,3,"morphobank","training",32,74,72,4,19.7,12.5,TRUE,"",TRUE +"project4532_(4)","project4532 (4).nex",4532,4,"morphobank","training",33,138,101,4,19.1,18.1,TRUE,"",FALSE +"project4532_(5)","project4532 (5).nex",4532,5,"morphobank","training",33,74,72,6,21.3,12,TRUE,"",TRUE +"project4532_(6)","project4532 (6).nex",4532,6,"morphobank","training",33,138,101,4,19.1,18.1,TRUE,"",TRUE +"project4533","project4533.nex",4533,NA,"morphobank","training",50,95,93,9,11.8,6.6,TRUE,"",FALSE +"project4542","project4542.nex",4542,NA,"morphobank","training",20,27,27,3,31.7,0,TRUE,"",FALSE +"project4545","project4545.nex",4545,NA,"morphobank","validation",26,31,31,3,16.6,0,TRUE,"",FALSE +"project4550","project4550.nex",4550,NA,"morphobank","validation",230,889,889,8,60.2,4.1,TRUE,"",FALSE +"project4553","project4553.nex",4553,NA,"morphobank","training",72,244,244,8,38.3,0,TRUE,"",FALSE +"project456","project456.nex",456,NA,"morphobank","training",148,146,144,18,16.1,21.3,TRUE,"",FALSE +"project4580","project4580.nex",4580,NA,"morphobank","validation",109,676,676,6,49,4.6,TRUE,"",FALSE +"project4581","project4581.nex",4581,NA,"morphobank","training",72,323,323,5,51,2.8,TRUE,"",FALSE +"project4596","project4596.nex",4596,NA,"morphobank","training",98,35,35,5,23.4,7.1,TRUE,"",FALSE +"project4598","project4598.nex",4598,NA,"morphobank","training",74,103,70,2,2.3,1.4,TRUE,"",FALSE +"project46","project46.nex",46,NA,"morphobank","training",80,368,315,7,33.5,0,TRUE,"",FALSE +"project4614","project4614.nex",4614,NA,"morphobank","training",112,287,287,4,46.6,2.9,TRUE,"",FALSE +"project4620","project4620.nex",4620,NA,"morphobank","validation",19,37,35,3,21.4,0,TRUE,"",FALSE +"project4622","project4622.nex",4622,NA,"morphobank","training",11,16,11,3,3.3,0,TRUE,"",FALSE +"project4624","project4624.nex",4624,NA,"morphobank","training",76,510,510,8,38.1,2.7,TRUE,"",FALSE +"project4626","project4626.nex",4626,NA,"morphobank","training",63,33,16,10,66.3,9,TRUE,"",FALSE +"project463","project463.nex",463,NA,"morphobank","training",60,227,227,4,21.3,5.7,TRUE,"",FALSE +"project4634","project4634.nex",4634,NA,"morphobank","training",41,92,90,6,33.5,2.6,TRUE,"",FALSE +"project4637","project4637.nex",4637,NA,"morphobank","training",106,90,90,8,43.3,0.8,TRUE,"",FALSE +"project4649","project4649.nex",4649,NA,"morphobank","training",82,127,119,6,18,0.7,TRUE,"",FALSE +"project466_(1)","project466 (1).nex",466,1,"morphobank","training",7,151,118,8,6.9,0,TRUE,"",FALSE +"project466_(2)","project466 (2).nex",466,2,"morphobank","training",7,151,119,9,7.8,0,TRUE,"",FALSE +"project466_(3)","project466 (3).nex",466,3,"morphobank","training",7,151,120,10,6.4,0,TRUE,"",FALSE +"project466_(4)","project466 (4).nex",466,4,"morphobank","training",7,151,118,8,5.6,0,TRUE,"",FALSE +"project466_(5)","project466 (5).nex",466,5,"morphobank","training",7,151,113,9,6.2,0,TRUE,"",FALSE +"project466_(6)","project466 (6).nex",466,6,"morphobank","training",7,151,122,10,5.3,0,TRUE,"",FALSE +"project4661","project4661.nex",4661,NA,"morphobank","training",101,230,228,8,58.3,7.7,TRUE,"",FALSE +"project4671","project4671.nex",4671,NA,"morphobank","training",62,83,83,6,24,0,TRUE,"",FALSE +"project4672","project4672.nex",4672,NA,"morphobank","training",22,27,27,3,23.1,3,TRUE,"",FALSE +"project4675","project4675.nex",4675,NA,"morphobank","validation",48,105,105,4,37.5,0.3,TRUE,"",FALSE +"project4680","project4680.nex",4680,NA,"morphobank","validation",80,180,179,8,47.5,8.5,TRUE,"",FALSE +"project470","project470.nex",470,NA,"morphobank","validation",14,48,47,4,6.2,0,TRUE,"",FALSE +"project4712","project4712.nex",4712,NA,"morphobank","training",27,110,107,4,24,0,TRUE,"",FALSE +"project4747","project4747.nex",4747,NA,"morphobank","training",25,15,15,4,12,0,TRUE,"",FALSE +"project4761","project4761.nex",4761,NA,"morphobank","training",58,370,369,6,29.7,4.5,TRUE,"",FALSE +"project4789","project4789.nex",4789,NA,"morphobank","training",13,12,10,4,5.4,0,TRUE,"",FALSE +"project4790","project4790.nex",4790,NA,"morphobank","validation",16,32,32,4,12.7,0,TRUE,"",FALSE +"project48","project48.nex",48,NA,"morphobank","training",80,690,658,6,29.3,9.3,TRUE,"",FALSE +"project4817","project4817.nex",4817,NA,"morphobank","training",101,267,264,7,14.9,38.8,TRUE,"",FALSE +"project482","project482.nex",482,NA,"morphobank","training",44,69,69,4,19.6,0.9,TRUE,"",FALSE +"project484","project484.nex",484,NA,"morphobank","training",20,50,50,4,22.2,0,TRUE,"",FALSE +"project485","project485.nex",485,NA,"morphobank","validation",82,413,413,5,37.8,3.3,TRUE,"",FALSE +"project4867","project4867.nex",4867,NA,"morphobank","training",60,138,138,3,40.3,9.3,TRUE,"",FALSE +"project488","project488.nex",488,NA,"morphobank","training",38,75,75,10,23,0,TRUE,"",FALSE +"project489","project489.nex",489,NA,"morphobank","training",46,243,243,8,13.2,39.4,TRUE,"",FALSE +"project4910","project4910.nex",4910,NA,"morphobank","validation",26,160,156,12,32.6,0,TRUE,"",FALSE +"project493","project493.nex",493,NA,"morphobank","training",35,290,289,5,36.3,0,TRUE,"",FALSE +"project495","project495.nex",495,NA,"morphobank","validation",19,66,66,3,13,0,TRUE,"",FALSE +"project496","project496.nex",496,NA,"morphobank","training",74,408,408,6,47.3,0,TRUE,"",FALSE +"project497.1","project497.1.nex",497,NA,"morphobank","training",NA,NA,NA,NA,NA,NA,FALSE,"WARNING: no non-missing arguments to max; returning -Inf ; ERROR: values must be type 'integer', + but FUN(X[[1]]) result is type 'double'",FALSE +"project497.2","project497.2.nex",497,NA,"morphobank","training",NA,NA,NA,NA,NA,NA,FALSE,"WARNING: no non-missing arguments to max; returning -Inf ; ERROR: values must be type 'integer', + but FUN(X[[1]]) result is type 'double'",FALSE +"project506","project506.nex",506,NA,"morphobank","training",30,137,133,5,12.3,0,TRUE,"",FALSE +"project5099","project5099.nex",5099,NA,"morphobank","training",53,15,15,4,2.4,1.5,TRUE,"",FALSE +"project510","project510.nex",510,NA,"morphobank","validation",188,2954,2857,12,22.1,0,TRUE,"",FALSE +"project5186","project5186.nex",5186,NA,"morphobank","training",43,41,40,4,0.9,0,TRUE,"",FALSE +"project5201","project5201.nex",5201,NA,"morphobank","training",86,71,71,5,14.2,27.6,TRUE,"",FALSE +"project5228","project5228.nex",5228,NA,"morphobank","training",59,146,126,8,0.8,0,TRUE,"",FALSE +"project5230","project5230.nex",5230,NA,"morphobank","validation",71,40,40,6,0.4,3.8,TRUE,"",FALSE +"project5255","project5255.nex",5255,NA,"morphobank","validation",13,9,9,3,6,0,TRUE,"",FALSE +"project5268","project5268.nex",5268,NA,"morphobank","training",30,46,45,3,13.6,0,TRUE,"",FALSE +"project528","project528.nex",528,NA,"morphobank","training",44,99,98,4,17.3,0,TRUE,"",FALSE +"project529","project529.nex",529,NA,"morphobank","training",27,107,106,5,19.9,0,TRUE,"",FALSE +"project530_(1)","project530 (1).nex",530,1,"morphobank","validation",20,39,38,4,6.6,0,TRUE,"",FALSE +"project530_(2)","project530 (2).nex",530,2,"morphobank","validation",23,90,89,5,18.9,0,TRUE,"",FALSE +"project532","project532.nex",532,NA,"morphobank","training",21,674,427,9,15.9,2,TRUE,"",FALSE +"project5327","project5327.nex",5327,NA,"morphobank","training",55,135,133,7,24.7,4.5,TRUE,"",FALSE +"project537","project537.nex",537,NA,"morphobank","training",30,58,58,3,25.9,5.1,TRUE,"",FALSE +"project538","project538.nex",538,NA,"morphobank","training",11,19,19,4,10.5,0,TRUE,"",FALSE +"project539","project539.nex",539,NA,"morphobank","training",22,51,50,5,7.9,1.4,TRUE,"",FALSE +"project540","project540.nex",540,NA,"morphobank","validation",55,114,113,6,15.9,12.4,TRUE,"",FALSE +"project541","project541.nex",541,NA,"morphobank","training",33,71,71,3,24.6,4,TRUE,"",FALSE +"project542","project542.nex",542,NA,"morphobank","training",24,43,43,4,13.4,3.4,TRUE,"",FALSE +"project549","project549.nex",549,NA,"morphobank","training",84,395,384,9,28.2,23.4,TRUE,"",FALSE +"project553","project553.nex",553,NA,"morphobank","training",NA,NA,NA,NA,NA,NA,FALSE,"WARNING: no non-missing arguments to max; returning -Inf ; ERROR: values must be type 'integer', + but FUN(X[[1]]) result is type 'double'",FALSE +"project561","project561.nex",561,NA,"morphobank","training",34,356,329,6,5,9.6,TRUE,"",FALSE +"project563","project563.nex",563,NA,"morphobank","training",82,50,49,6,19.9,4.3,TRUE,"",FALSE +"project567","project567.nex",567,NA,"morphobank","training",24,86,84,5,11.9,0,TRUE,"",FALSE +"project568","project568.nex",568,NA,"morphobank","training",45,81,80,10,18.4,3.5,TRUE,"",FALSE +"project569","project569.nex",569,NA,"morphobank","training",22,60,58,5,11.5,0.9,TRUE,"",FALSE +"project571","project571.nex",571,NA,"morphobank","training",42,125,125,5,16.8,4.2,TRUE,"",FALSE +"project574","project574.nex",574,NA,"morphobank","training",19,97,97,14,27,0.2,TRUE,"",FALSE +"project578","project578.nex",578,NA,"morphobank","training",23,166,163,5,25.5,2.7,TRUE,"",FALSE +"project581","project581.nex",581,NA,"morphobank","training",85,301,301,5,39.3,0,TRUE,"",FALSE +"project586","project586.nex",586,NA,"morphobank","training",36,80,80,3,26.4,5.6,TRUE,"",FALSE +"project589","project589.nex",589,NA,"morphobank","training",69,135,124,8,2.2,18.5,TRUE,"",FALSE +"project599","project599.nex",599,NA,"morphobank","training",18,60,51,5,0,0.8,TRUE,"",FALSE +"project600","project600.nex",600,NA,"morphobank","validation",21,60,51,5,0,0.7,TRUE,"",FALSE +"project608","project608.nex",608,NA,"morphobank","training",97,313,259,10,52.7,0,TRUE,"",FALSE +"project610","project610.nex",610,NA,"morphobank","validation",47,69,66,9,8.6,1.4,TRUE,"",FALSE +"project611","project611.nex",611,NA,"morphobank","training",23,66,65,4,28.2,0,TRUE,"",FALSE +"project618","project618.nex",618,NA,"morphobank","training",17,42,42,10,2.9,26.9,TRUE,"",FALSE +"project619","project619.nex",619,NA,"morphobank","training",41,89,78,7,3.9,9,TRUE,"",FALSE +"project622","project622.nex",622,NA,"morphobank","training",29,65,54,7,4.1,2.1,TRUE,"",FALSE +"project623","project623.nex",623,NA,"morphobank","training",37,84,73,7,3.9,7.2,TRUE,"",FALSE +"project624","project624.nex",624,NA,"morphobank","training",34,80,69,7,3.8,7.6,TRUE,"",FALSE +"project625","project625.nex",625,NA,"morphobank","validation",106,258,236,8,18.7,15.3,TRUE,"",FALSE +"project628","project628.nex",628,NA,"morphobank","training",15,50,50,3,31.1,0,TRUE,"",FALSE +"project631","project631.nex",631,NA,"morphobank","training",44,253,155,4,14.7,12.4,TRUE,"",FALSE +"project632_(1)","project632 (1).nex",632,1,"morphobank","training",42,34,32,8,15.7,0,TRUE,"",FALSE +"project632_(2)","project632 (2).nex",632,2,"morphobank","training",52,54,54,5,7.9,2.4,TRUE,"",FALSE +"project633","project633.nex",633,NA,"morphobank","training",12,41,30,3,1.7,0,TRUE,"",FALSE +"project635","project635.nex",635,NA,"morphobank","validation",19,20,15,3,0,0,TRUE,"",FALSE +"project638","project638.nex",638,NA,"morphobank","training",71,115,102,5,0.4,3.7,TRUE,"",FALSE +"project640","project640.nex",640,NA,"morphobank","validation",27,53,52,3,1.4,1.6,TRUE,"",FALSE +"project641","project641.nex",641,NA,"morphobank","training",31,95,81,5,0.8,3,TRUE,"",FALSE +"project643","project643.nex",643,NA,"morphobank","training",11,28,23,4,2.8,0,TRUE,"",FALSE +"project647","project647.nex",647,NA,"morphobank","training",15,56,49,4,1.1,3.1,TRUE,"",FALSE +"project648","project648.nex",648,NA,"morphobank","training",21,19,17,5,0.6,0.3,TRUE,"",FALSE +"project652","project652.nex",652,NA,"morphobank","training",56,224,224,4,49.9,0,TRUE,"",FALSE +"project657","project657.nex",657,NA,"morphobank","training",54,99,95,5,10.9,0,TRUE,"",FALSE +"project660","project660.nex",660,NA,"morphobank","validation",117,477,477,8,56,2.9,TRUE,"",FALSE +"project667","project667.nex",667,NA,"morphobank","training",65,259,254,4,41.1,3.2,TRUE,"",FALSE +"project674","project674.nex",674,NA,"morphobank","training",18,54,54,4,20.1,0.5,TRUE,"",FALSE +"project675","project675.nex",675,NA,"morphobank","validation",16,52,52,4,0.1,2.9,TRUE,"",FALSE +"project676","project676.nex",676,NA,"morphobank","training",27,59,57,4,19.9,1.8,TRUE,"",FALSE +"project681","project681.nex",681,NA,"morphobank","training",22,50,40,4,2.3,0,TRUE,"",FALSE +"project682","project682.nex",682,NA,"morphobank","training",94,78,78,4,28.7,0,TRUE,"",FALSE +"project683","project683.nex",683,NA,"morphobank","training",19,71,69,5,25.3,1.7,TRUE,"",FALSE +"project684","project684.nex",684,NA,"morphobank","training",52,303,298,9,33.1,1.8,TRUE,"",FALSE +"project687","project687.nex",687,NA,"morphobank","training",90,272,271,5,37.8,4.4,TRUE,"",FALSE +"project689_(1)","project689 (1).nex",689,1,"morphobank","training",76,183,173,8,37.9,11.9,TRUE,"",TRUE +"project689_(2)","project689 (2).nex",689,2,"morphobank","training",109,183,173,8,37.4,12.3,TRUE,"",FALSE +"project691","project691.nex",691,NA,"morphobank","training",103,446,443,6,43.4,0,TRUE,"",FALSE +"project692","project692.nex",692,NA,"morphobank","training",71,408,408,6,40.7,4.5,TRUE,"",FALSE +"project694","project694.nex",694,NA,"morphobank","training",46,286,286,9,17.5,3.6,TRUE,"",FALSE +"project696","project696.nex",696,NA,"morphobank","training",34,35,35,7,0,4.5,TRUE,"",FALSE +"project699","project699.nex",699,NA,"morphobank","training",47,175,170,7,37.6,0,TRUE,"",FALSE +"project701","project701.nex",701,NA,"morphobank","training",35,12,12,4,1,0,TRUE,"",FALSE +"project706","project706.nex",706,NA,"morphobank","training",9,114,85,4,8.5,2.6,TRUE,"",FALSE +"project709","project709.nex",709,NA,"morphobank","training",31,38,38,4,20.1,6.3,TRUE,"",FALSE +"project713","project713.nex",713,NA,"morphobank","training",32,334,333,7,43.1,0.3,TRUE,"",FALSE +"project715","project715.nex",715,NA,"morphobank","validation",23,68,68,5,10.4,0,TRUE,"",FALSE +"project717","project717.nex",717,NA,"morphobank","training",29,101,100,5,30.4,0,TRUE,"",FALSE +"project721","project721.nex",721,NA,"morphobank","training",19,68,68,4,29.3,0,TRUE,"",FALSE +"project723","project723.nex",723,NA,"morphobank","training",22,72,65,5,34.3,0.1,TRUE,"",FALSE +"project724","project724.nex",724,NA,"morphobank","training",37,114,114,8,27.9,8.9,TRUE,"",FALSE +"project727","project727.nex",727,NA,"morphobank","training",15,56,52,3,14.6,0,TRUE,"",FALSE +"project728","project728.nex",728,NA,"morphobank","training",59,98,97,5,11.6,0,TRUE,"",FALSE +"project730","project730.nex",730,NA,"morphobank","validation",27,77,75,3,12.1,1.9,TRUE,"",FALSE +"project735","project735.nex",735,NA,"morphobank","validation",37,90,89,5,17,0,TRUE,"",FALSE +"project739","project739.nex",739,NA,"morphobank","training",38,261,258,6,23.7,2.1,TRUE,"",FALSE +"project740","project740.nex",740,NA,"morphobank","validation",89,78,78,6,0.3,0.1,TRUE,"",FALSE +"project741","project741.nex",741,NA,"morphobank","training",27,206,199,3,58.6,0.1,TRUE,"",FALSE +"project742","project742.nex",742,NA,"morphobank","training",46,71,70,6,0.2,0.2,TRUE,"",FALSE +"project743","project743.nex",743,NA,"morphobank","training",23,43,43,18,23.5,0.7,TRUE,"",FALSE +"project746","project746.nex",746,NA,"morphobank","training",77,348,348,5,58.2,0,TRUE,"",FALSE +"project748","project748.nex",748,NA,"morphobank","training",60,138,138,3,40.3,9.3,TRUE,"",FALSE +"project749","project749.nex",749,NA,"morphobank","training",25,53,53,12,5.3,1.9,TRUE,"",FALSE +"project750","project750.nex",750,NA,"morphobank","validation",34,240,240,5,21.8,0,TRUE,"",FALSE +"project751","project751.nex",751,NA,"morphobank","training",52,193,192,5,15.3,1.9,TRUE,"",FALSE +"project758","project758.nex",758,NA,"morphobank","training",28,74,71,5,11.7,0.9,TRUE,"",FALSE +"project776","project776.nex",776,NA,"morphobank","training",69,232,231,5,35.6,5.2,TRUE,"",FALSE +"project779","project779.nex",779,NA,"morphobank","training",51,118,117,9,24.9,0,TRUE,"",FALSE +"project780_(1)","project780 (1).nex",780,1,"morphobank","validation",63,104,103,11,24.1,4.5,TRUE,"",TRUE +"project780_(2)","project780 (2).nex",780,2,"morphobank","validation",66,104,101,8,27.1,4.4,TRUE,"",FALSE +"project784","project784.nex",784,NA,"morphobank","training",188,2,2,9,0,5.6,TRUE,"",FALSE +"project790","project790.nex",790,NA,"morphobank","validation",108,210,208,20,16.8,16.5,TRUE,"",FALSE +"project793","project793.nex",793,NA,"morphobank","training",51,253,179,5,18.2,11.7,TRUE,"",FALSE +"project794","project794.nex",794,NA,"morphobank","training",47,213,204,9,5.6,10.1,TRUE,"",FALSE +"project798","project798.nex",798,NA,"morphobank","training",73,282,278,8,24.4,1.7,TRUE,"",FALSE +"project802","project802.nex",802,NA,"morphobank","training",26,73,71,4,39.9,0,TRUE,"",FALSE +"project804","project804.nex",804,NA,"morphobank","training",173,589,569,10,32.8,30.9,TRUE,"",FALSE +"project805","project805.nex",805,NA,"morphobank","validation",7,16,10,2,10,5.7,TRUE,"",FALSE +"project806","project806.nex",806,NA,"morphobank","training",58,82,82,8,14.3,16.1,TRUE,"",FALSE +"project809","project809.nex",809,NA,"morphobank","training",41,90,81,4,3.9,2.6,TRUE,"",FALSE +"project810","project810.nex",810,NA,"morphobank","validation",16,40,33,9,10,0,TRUE,"",FALSE +"project811","project811.nex",811,NA,"morphobank","training",64,97,89,17,16.1,0,TRUE,"",FALSE +"project816","project816.nex",816,NA,"morphobank","training",23,35,34,5,4.2,3.2,TRUE,"",FALSE +"project825","project825.nex",825,NA,"morphobank","validation",33,131,129,6,21.6,0.4,TRUE,"",FALSE +"project826","project826.nex",826,NA,"morphobank","training",33,218,213,3,61.7,0.1,TRUE,"",FALSE +"project831","project831.nex",831,NA,"morphobank","training",21,49,41,6,12.8,2.4,TRUE,"",FALSE +"project833","project833.nex",833,NA,"morphobank","training",36,6,6,3,0,0,TRUE,"",FALSE +"project84","project84.nex",84,NA,"morphobank","training",14,39,38,4,20.5,3.9,TRUE,"",FALSE +"project847","project847.nex",847,NA,"morphobank","training",38,126,123,6,11.2,15.7,TRUE,"",FALSE +"project849","project849.nex",849,NA,"morphobank","training",22,47,46,5,17.6,0,TRUE,"",FALSE +"project854","project854.nex",854,NA,"morphobank","training",33,201,200,4,41.4,3,TRUE,"",FALSE +"project858_(1)","project858 (1).nex",858,1,"morphobank","training",30,115,115,4,10.8,0.6,TRUE,"",FALSE +"project858_(2)","project858 (2).nex",858,2,"morphobank","training",56,58,57,5,22,1.5,TRUE,"",FALSE +"project861","project861.nex",861,NA,"morphobank","training",141,32,32,4,0.7,0,TRUE,"",FALSE +"project869","project869.nex",869,NA,"morphobank","training",47,175,170,7,37.8,0,TRUE,"WARNING: Could not parse character states; does each end with a ' or ;?.",FALSE +"project870","project870.nex",870,NA,"morphobank","validation",37,74,73,5,23.1,0,TRUE,"",FALSE +"project871","project871.nex",871,NA,"morphobank","training",28,111,102,7,22.6,0,TRUE,"",FALSE +"project876","project876.nex",876,NA,"morphobank","training",44,137,132,5,36.3,0,TRUE,"",FALSE +"project896","project896.nex",896,NA,"morphobank","training",27,22,22,4,6.9,4.7,TRUE,"",FALSE +"project906","project906.nex",906,NA,"morphobank","training",24,177,164,6,8.9,7,TRUE,"",FALSE +"project908","project908.nex",908,NA,"morphobank","training",30,177,174,6,16.6,6.3,TRUE,"",FALSE +"project912","project912.nex",912,NA,"morphobank","training",173,74,74,9,19,3.7,TRUE,"",FALSE +"project922","project922.nex",922,NA,"morphobank","training",40,94,86,7,14.7,5.7,TRUE,"",FALSE +"project923","project923.nex",923,NA,"morphobank","training",28,46,1,0,0,0,TRUE,"",FALSE +"project929_(1)","project929 (1).nex",929,1,"morphobank","training",38,258,256,6,23.3,1.7,TRUE,"",FALSE +"project929_(2)","project929 (2).nex",929,2,"morphobank","training",38,258,256,6,22.9,1.7,TRUE,"",TRUE +"project931","project931.nex",931,NA,"morphobank","training",13,23,22,3,9.8,0,TRUE,"",FALSE +"project936","project936.nex",936,NA,"morphobank","training",23,33,33,7,2.1,4,TRUE,"",FALSE +"project937","project937.nex",937,NA,"morphobank","training",30,83,81,5,24.1,0,TRUE,"",FALSE +"project938","project938.nex",938,NA,"morphobank","training",35,83,81,5,18.4,0.5,TRUE,"",FALSE +"project944","project944.nex",944,NA,"morphobank","training",25,72,72,4,17.2,0.9,TRUE,"",FALSE +"project945","project945.nex",945,NA,"morphobank","validation",64,102,99,5,5,6.9,TRUE,"",FALSE +"project947","project947.nex",947,NA,"morphobank","training",80,220,220,7,28.6,0,TRUE,"",FALSE +"project950","project950.nex",950,NA,"morphobank","validation",12,9,9,3,3.7,1.9,TRUE,"",FALSE +"project954","project954.nex",954,NA,"morphobank","training",83,75,75,5,15.4,1.7,TRUE,"",FALSE +"project955","project955.nex",955,NA,"morphobank","validation",26,66,66,3,37.6,0,TRUE,"",FALSE +"project960","project960.nex",960,NA,"morphobank","validation",21,37,37,4,20.6,0,TRUE,"",FALSE +"project961","project961.nex",961,NA,"morphobank","training",24,33,28,3,5.4,0,TRUE,"",FALSE +"project964","project964.nex",964,NA,"morphobank","training",24,98,90,4,29.1,8,TRUE,"",FALSE +"project970","project970.nex",970,NA,"morphobank","validation",157,1844,1346,6,52.3,2.8,TRUE,"",FALSE +"project971_(1)","project971 (1).nex",971,1,"morphobank","training",26,101,73,6,53.3,0.5,TRUE,"",FALSE +"project971_(2)","project971 (2).nex",971,2,"morphobank","training",26,47,38,5,43.7,0.9,TRUE,"",FALSE +"project977_(1)","project977 (1).nex",977,1,"morphobank","training",14,234,212,6,38.4,0,TRUE,"",FALSE +"project977_(2)","project977 (2).nex",977,2,"morphobank","training",14,234,212,6,38.4,0,TRUE,"",FALSE +"project979","project979.nex",979,NA,"morphobank","training",119,477,419,8,58.2,3.2,TRUE,"",FALSE +"project984","project984.nex",984,NA,"morphobank","training",28,205,203,3,55.6,0.1,TRUE,"",FALSE +"project987","project987.nex",987,NA,"morphobank","training",108,122,117,7,21.5,6.5,TRUE,"",FALSE +"project996","project996.nex",996,NA,"morphobank","training",53,70,70,5,39.9,0,TRUE,"",FALSE +"project997","project997.nex",997,NA,"morphobank","training",66,80,76,4,0.9,3,TRUE,"",FALSE +"syab07200","syab07200.nex",NA,NA,"syab","training",39,297,297,5,15.3,3,TRUE,"",FALSE +"syab07201","syab07201.nex",NA,NA,"syab","training",125,2954,2813,10,28.3,0,TRUE,"",FALSE +"syab07202","syab07202.nex",NA,NA,"syab","training",111,360,359,7,45.1,2,TRUE,"",FALSE +"syab07203","syab07203.nex",NA,NA,"syab","training",50,196,191,5,5.1,4.6,TRUE,"",FALSE +"syab07204","syab07204.nex",NA,NA,"syab","training",225,748,748,2,53,4.5,TRUE,"",FALSE +"syab07205","syab07205.nex",NA,NA,"syab","training",206,748,748,2,52.4,4.1,TRUE,"",FALSE +"syab07206","syab07206.nex",NA,NA,"syab","training",117,538,535,6,52,0,TRUE,"",FALSE diff --git a/dev/benchmarks/memory_profile_results.md b/dev/benchmarks/memory_profile_results.md new file mode 100644 index 000000000..100338b5f --- /dev/null +++ b/dev/benchmarks/memory_profile_results.md @@ -0,0 +1,189 @@ +# Phase 3D: Memory Layout Profiling Results + +Date: 2026-03-16 +Platform: Windows, R 4.5.2, GCC 14.2.0 +CPU: Intel (L1 32 KB, L2 256 KB typical) + +## 1. Baseline Measurements + +### TBR pass phase breakdown + +All timings in microseconds (μs), averaged over 3 random trees per dataset. + +| Dataset | Tips | Blocks | Words | Clips | Candidates | Clip+Incr (μs) | Indirect (μs) | Unclip (μs) | +|---------|------|--------|-------|-------|------------|-----------------|----------------|-------------| +| Vinther2008 | 23 | 6 | 28 | 38 | 3,585 | 789 | 286 | 268 | +| Agnarsson2004 | 62 | 8 | 59 | 112 | 56,501 | 2,948 | 5,175 | 856 | +| synth_20 | 20 | 4 | 11 | 34 | 2,535 | 271 | 65 | 93 | +| synth_50 | 50 | 4 | 12 | 91 | 32,776 | 1,021 | 989 | 314 | +| synth_100 | 100 | 4 | 12 | 190 | 237,536 | 3,880 | 7,999 | 1,013 | +| synth_200 | 200 | 4 | 12 | 377 | 1,090,533 | 11,238 | 35,930 | 2,695 | + +### Time fraction breakdown + +| Dataset | Tips | % Clip+Incr | % Indirect | % Unclip | +|---------|------|-------------|------------|----------| +| synth_20 | 20 | 63.2 | 15.1 | 21.7 | +| synth_50 | 50 | 43.9 | 42.6 | 13.5 | +| synth_100 | 100 | 30.1 | 62.0 | 7.9 | +| synth_200 | 200 | 22.5 | 72.1 | 5.4 | + +**Conclusion:** Indirect scoring dominates at scale (72% at 200 tips). The clip+incremental +phase dominates at small scales because the incremental downpass is O(depth) ≈ O(n) for +small trees (depth ≈ n), while indirect evaluation is O(n²). + +### Per-candidate indirect timing + +| Dataset | Tips | total_words | Candidates | ns/candidate | +|---------|------|-------------|------------|--------------| +| Vinther2008 | 23 | 28 | 3,585 | 79.9 | +| Agnarsson2004 | 62 | 59 | 56,501 | 91.6 | +| synth_20 | 20 | 11 | 2,535 | 25.6 | +| synth_50 | 50 | 12 | 32,776 | 30.2 | +| synth_100 | 100 | 12 | 237,536 | 33.7 | +| synth_200 | 200 | 12 | 1,090,533 | 32.9 | + +**Conclusion:** Per-candidate cost is stable across tree sizes (~33 ns for `total_words=12`), +confirming that cache effects are not increasing per-candidate cost. The cost scales linearly +with `total_words` (28 words → 80 ns, 59 words → 92 ns). + +### Scaling analysis + +- Indirect time scaling exponent: **2.78** (vs expected 2.0 for O(n²)) +- Candidate count scaling exponent: **2.66** +- The super-quadratic scaling is primarily from candidate count growth (2.66), + not from per-candidate cost degradation (stable at ~33 ns). +- The extra 0.12 exponent may come from TBR rerooting generating O(k) sub-edges + per clip, where k is subtree size. + +### Snapshot overhead + +| Tips | Save (μs) | Restore (μs) | Size (KB) | +|------|-----------|---------------|-----------| +| 20 | 0.3 | 0.3 | 14.6 | +| 50 | 1.1 | 1.1 | 40.2 | +| 100 | 2.5 | 2.3 | 80.8 | +| 200 | 5.4 | 5.0 | 162.1 | + +**Conclusion:** Snapshot save/restore is negligible — 5 μs per operation at 200 tips, +compared to 36 ms for indirect evaluation. StateSnapshot optimization (Step 6) is not +worth pursuing. + +## 2. Steps Investigated and Decisions + +### Step 3: Postorder node renumbering — SKIPPED + +Analysis of node-ID strides during postorder traversal (50-tip tree): +- Mean stride: 34.6 node IDs (~52 cache lines at `total_words=12`) +- Max stride: 93 node IDs (~140 cache lines) + +However, the downpass is **not the hot path** — it's only 22% of time at 200 tips. The +state arrays fit comfortably in L2 (prelim for 200 tips = 37 KB; total state data ≈ 162 +KB). Since the bottleneck is indirect scoring (which uses vroot_cache with linear access), +postorder renumbering would not improve the hot path. + +**Decision:** Not implemented. Cost/benefit ratio unfavorable. + +### Step 4: Binary-character specialization — SKIPPED + +Block `n_states` values for typical datasets: +- Vinther2008: 4, 4, 5, 5, 5, 5 (total_words=28) +- Agnarsson2004: 7, 7, 7, 7, 7, 8, 8, 8 (total_words=59) +- synth_200 (binary+NA): 3, 3, 3, 3 (total_words=12) + +`n_states` per block is determined by the **total number of applicable states in the +contrast matrix**, not by individual character state coverage. All standard blocks share +the same `n_states`. Binary characters contribute to blocks with the full `n_states` +because `state_remap` assigns globally consecutive indices. + +**Decision:** Per-block unrolling for binary characters is not possible with the current +block structure. Changing this would require per-block state counts, which is a deep +architectural change. Not worth it for Phase 3D. + +Verified: all inner loops correctly iterate `blk.n_states` (not `total_words`). No bug. + +### Step 5: Block-major layout — SKIPPED + +The vroot_cache (Phase 2B) already provides linear access for the indirect scoring hot +path. Per-candidate cost is stable across tree sizes, confirming no cache pressure issue. +State arrays for 200 tips fit in L2 (162 KB total). + +For morphological data (the target use case), `total_words` is small (12-59) and trees +rarely exceed 500 tips. Block-major layout would add complexity without measurable benefit. + +**Decision:** Not implemented. Experiment not justified by profiling data. + +### Step 6: StateSnapshot reduction — SKIPPED + +Snapshot overhead is <0.01% of TBR pass time at scale. Not worth optimizing. + +## 3. Optimizations Applied + +### Postorder save/restore in TBR (ts_tbr.cpp) + +After `spr_unclip()`, the tree topology is identical to before `spr_clip()`, so the +postorder traversal is the same. Previously, `build_postorder()` (O(n) DFS with vector +allocations) was called to reconstruct it. Now the pre-clip postorder is saved and +restored via `assign()` (O(n) memcpy, no allocation). + +Similarly, after `state_snap.restore()` on rejection, the postorder is already restored +by the snapshot's memcpy. The redundant `build_postorder()` calls were removed. + +**Changes:** +- Save `tree.postorder` before `spr_clip()`, restore after `spr_unclip()` +- Remove 2 redundant `build_postorder()` calls after `state_snap.restore()` + +**Impact:** Eliminates ~377 `build_postorder()` calls per TBR pass at 200 tips. Each call +saves O(n) DFS traversal plus 2 vector allocations. Estimated savings: 1-3% of the +unclip phase. The benefit is modest because unclip is only 5% of total TBR pass time; +the real bottleneck (indirect scoring at 72%) is addressed by Phase 3E (SIMD). + +## 4. Implications for Future Phases + +### Phase 3E (SIMD) — highest priority + +The profiling clearly shows that the **indirect scoring inner loop** is the primary target +for optimization. At 200 tips, it consumes 72% of TBR pass time. The inner loop is: + +```cpp +for (int b = 0; b < ds.n_blocks; ++b) { + uint64_t any_hit = 0; + for (int s = 0; s < blk.n_states; ++s) { + any_hit |= (clip_prelim[offset+s] & vroot[offset+s]); + } + uint64_t needs_step = ~any_hit & blk.active_mask; + extra_steps += blk.weight * popcount64(needs_step); +} +``` + +This is a textbook SIMD target: independent AND/OR operations over contiguous uint64_t +arrays. SSE2 can process 2 words per instruction, AVX2 can process 4. With `n_states` +typically 3-8 per block, even 2× throughput from SSE2 would be significant. + +### Algorithmic improvements + +The candidate count scaling exponent (2.66 > 2.0) suggests that TBR rerooting generates +more candidates than pure SPR. Reducing the candidate set (e.g., tighter bounds on which +rerootings to try) could reduce the constant factor. + +## 5. Files Created/Modified + +### Created: +- `dev/benchmarks/bench_memory.R` — profiling harness +- `dev/benchmarks/memory_profile_results.md` — this file +- `tests/testthat/test-ts-memory-layout.R` — 32 regression tests + +### Modified: +- `src/ts_rcpp.cpp` — added `ts_bench_tbr_phases` diagnostic (append only), added + `#include ` and `#include ` +- `src/TreeSearch-init.c` — registered `ts_bench_tbr_phases` (7 args) +- `src/ts_tbr.cpp` — postorder save/restore optimization (3 changes) +- `R/RcppExports.R` — regenerated via `Rcpp::compileAttributes()` +- `src/RcppExports.cpp` — regenerated + +### Test status: +- memory-layout: 32/32 passing +- driven: 53/53 passing +- tbr-bench: 26/26 passing +- fuse: 16/16 passing (1 skip) +- sector: 32/32 passing diff --git a/dev/benchmarks/nblocks_cost_bench.csv b/dev/benchmarks/nblocks_cost_bench.csv new file mode 100644 index 000000000..95fc7e0f2 --- /dev/null +++ b/dev/benchmarks/nblocks_cost_bench.csv @@ -0,0 +1,46 @@ +"file","ntax","nchar","n_blocks","total_words","seed","n_candidates","ns_per_cand","time_indirect_us","time_clip_us","time_rescore_us" +"project2144.nex",109,123,3,16,1,282145,17.8029027627638,5023,2168,25 +"project2144.nex",109,123,3,16,2,322536,17.5205248406379,5651,2286,21 +"project2144.nex",109,123,3,16,3,250546,18.1204249918179,4540,2431,22 +"project2144.nex",109,123,3,16,4,332963,17.5214663491139,5834,2251,22 +"project2144.nex",109,123,3,16,5,283110,17.8905725689661,5065,2408,22 +"project987.nex",108,114,4,30,1,269474,23.8316126973289,6422,2409,30 +"project987.nex",108,114,4,30,2,314216,23.436107645696,7364,2298,29 +"project987.nex",108,114,4,30,3,245212,25.6961323263136,6301,2772,29 +"project987.nex",108,114,4,30,4,315884,23.7840473085057,7513,2570,29 +"project987.nex",108,114,4,30,5,276910,24.275035209996,6722,2830,29 +"project2191.nex",105,215,5,46,1,190882,33.4761790006391,6390,3983,55 +"project2191.nex",105,215,5,46,2,290576,32.098315070756,9327,4224,54 +"project2191.nex",105,215,5,46,3,228263,33.5534011206372,7659,4121,54 +"project2191.nex",105,215,5,46,4,249097,31.7587124694396,7911,4068,54 +"project2191.nex",105,215,5,46,5,262366,32.035400928474,8405,4013,53 +"project3422.nex",110,277,6,42,1,288184,34.3010021375232,9885,4499,56 +"project3422.nex",110,277,6,42,2,329304,33.528289969147,11041,5005,55 +"project3422.nex",110,277,6,42,3,265616,34.534817179688,9173,5471,55 +"project3422.nex",110,277,6,42,4,368991,33.2447132856899,12267,4762,55 +"project3422.nex",110,277,6,42,5,290950,34.3289224952741,9988,4668,56 +"project4264 (1).nex",112,441,7,50,1,372418,39.0179851672046,14531,6786,68 +"project4264 (1).nex",112,441,7,50,2,344072,39.2475993396731,13504,6548,66 +"project4264 (1).nex",112,441,7,50,3,278578,40.8216011314605,11372,6177,66 +"project4264 (1).nex",112,441,7,50,4,402180,38.3808245064399,15436,6373,66 +"project4264 (1).nex",112,441,7,50,5,307630,39.492247180054,12149,6334,64 +"project1157.nex",110,138,8,26,1,288184,36.1158148960386,10408,2032,29 +"project1157.nex",110,138,8,26,2,329304,35.9272890702816,11831,2073,28 +"project1157.nex",110,138,8,26,3,265616,36.4134690681284,9672,2014,28 +"project1157.nex",110,138,8,26,4,368991,35.7271586569862,13183,2126,28 +"project1157.nex",110,138,8,26,5,290950,36.1780374634817,10526,1993,28 +"project691.nex",103,443,9,64,1,164942,57.1716118393132,9430,5012,77 +"project691.nex",103,443,9,64,2,271032,51.2411818530653,13888,5584,78 +"project691.nex",103,443,9,64,3,228027,53.3621018563591,12168,5739,78 +"project691.nex",103,443,9,64,4,223032,53.2210624484379,11870,5366,78 +"project691.nex",103,443,9,64,5,245470,52.7559375891148,12950,5665,77 +"project625.nex",106,236,10,86,1,205087,61.076518745703,12526,6110,87 +"project625.nex",106,236,10,86,2,298504,56.8836598504543,16980,6861,87 +"project625.nex",106,236,10,86,3,232494,61.0639414350478,14197,7257,87 +"project625.nex",106,236,10,86,4,269753,60.3255570837025,16273,6858,88 +"project625.nex",106,236,10,86,5,267310,60.117466611799,16070,5887,88 +"project2292.nex",114,493,11,76,1,385477,59.7960449002145,23050,8509,100 +"project2292.nex",114,493,11,76,2,365468,59.7945647772172,21853,7772,104 +"project2292.nex",114,493,11,76,3,302948,61.7366676789416,18703,7130,100 +"project2292.nex",114,493,11,76,4,381935,61.9398588765104,23657,8153,100 +"project2292.nex",114,493,11,76,5,319570,62.7843664924743,20064,7902,103 diff --git a/dev/benchmarks/nblocks_cost_findings.md b/dev/benchmarks/nblocks_cost_findings.md new file mode 100644 index 000000000..ac8345e31 --- /dev/null +++ b/dev/benchmarks/nblocks_cost_findings.md @@ -0,0 +1,80 @@ +# Per-Candidate Cost vs Number of Character Blocks + +**Task:** T-075 +**Date:** 2026-03-18 +**Agent:** A + +## Setup + +- 9 neotrans matrices selected from the 100–130 tip range +- All have inapplicable characters (NA-aware scoring) +- 5 random tree seeds per matrix +- Measured via `ts_bench_tbr_phases()` (one full TBR clip–evaluate–unclip pass) + +## Key finding + +Per-candidate indirect scoring cost is **linear** in both `n_blocks` and +`total_words`, with no significant nonlinearity (quadratic term p = 0.41). + +### Model: `ns_per_cand ~ n_blocks + total_words` + +| Term | Coefficient | SE | Interpretation | +|------|------------|-----|----------------| +| intercept | 2.4 ns | 0.7 | Base overhead per candidate | +| n_blocks | 3.3 ns | 0.2 | Per-block overhead (loop, function call) | +| total_words | 0.29 ns | 0.02 | Per-word cost (bit-parallel ops) | + +R² = 0.990 (45 observations from 9 datasets × 5 seeds) + +### Predicted cost at range extremes + +| n_blocks | total_words | Predicted ns/candidate | Observed mean | +|----------|-------------|----------------------|---------------| +| 3 | 16 | 17.1 | 17.8 | +| 11 | 76 | 61.2 | 61.2 | + +Ratio: 3.6× cost increase from simplest to most complex dataset. + +### Standalone models + +- `n_blocks` alone: R² = 0.931, slope ≈ 5.4 ns/block +- `total_words` alone: R² = 0.885, slope ≈ 0.62 ns/word + +## Practical implications + +1. **No threshold effect**: Cost scales linearly — there's no critical + n_blocks value after which performance degrades sharply. + +2. **Block overhead dominates**: At typical total_words (30–80), the per-block + overhead (3.3 ns × n_blocks) contributes more than per-word cost + (0.29 ns × total_words) for datasets with many state-count groups. + +3. **Optimisation opportunity**: Merging blocks with adjacent state counts + (e.g., 5-state and 6-state characters into a single padded block) could + reduce n_blocks by 2–4, saving ~7–13 ns/candidate. At 300k candidates + per clip, this would save ~2–4 ms per clip pass, or ~100–200 ms across + a full TBR sweep with 50 clips. Meaningful for large datasets but not + critical — this is a low-priority micro-optimisation. + +4. **For strategy selection**: n_blocks can be computed cheaply at dataset + load time. Datasets with n_blocks ≥ 10 will have ~3× higher per-candidate + cost than datasets with n_blocks ≤ 4, which affects expected search + duration. This could inform time estimates in the Shiny app. + +## Data + +Raw results: `nblocks_cost_bench.csv` (45 rows: 9 datasets × 5 seeds) + +### Datasets used + +| File | n_tips | n_char | n_blocks | total_words | Mean ns/cand | +|------|--------|--------|----------|-------------|-------------| +| project2144.nex | 109 | 123 | 3 | 16 | 17.8 | +| project987.nex | 108 | 114 | 4 | 30 | 24.2 | +| project2191.nex | 105 | 215 | 5 | 46 | 32.6 | +| project3422.nex | 110 | 277 | 6 | 42 | 34.0 | +| project4264 (1).nex | 112 | 441 | 7 | 50 | 39.4 | +| project1157.nex | 110 | 138 | 8 | 26 | 36.1 | +| project691.nex | 103 | 443 | 9 | 64 | 53.6 | +| project625.nex | 106 | 236 | 10 | 86 | 59.9 | +| project2292.nex | 114 | 493 | 11 | 76 | 61.2 | diff --git a/dev/benchmarks/neotrans_baselines.csv b/dev/benchmarks/neotrans_baselines.csv new file mode 100644 index 000000000..85bff8a87 --- /dev/null +++ b/dev/benchmarks/neotrans_baselines.csv @@ -0,0 +1,11 @@ +"file","ntax","nchar","nlevels","inapplicable","reps","score","time_s","status" +"project265.nex",30,203,7,TRUE,5,690,0.75,"OK" +"project463.nex",60,227,5,TRUE,5,1193,4.15,"OK" +"project692.nex",71,408,7,TRUE,5,2469,9.59,"OK" +"project3199.nex",88,138,4,TRUE,5,424,3.52,"OK" +"syab07206.nex",117,535,7,TRUE,3,2788,33.07,"OK" +"syab07201.nex",125,2813,10,FALSE,3,15528,89.36,"OK" +"project3200.nex",138,111,6,TRUE,3,818,19.13,"OK" +"project175.nex",165,71,6,FALSE,2,426,2.88999999999999,"OK" +"project3763.nex",205,103,7,TRUE,2,1503,38.34,"OK" +"syab07204.nex",225,748,3,TRUE,2,11960,156.89,"OK" diff --git a/dev/benchmarks/neotrans_strategy_comparison.csv b/dev/benchmarks/neotrans_strategy_comparison.csv new file mode 100644 index 000000000..45eed92b1 --- /dev/null +++ b/dev/benchmarks/neotrans_strategy_comparison.csv @@ -0,0 +1,16 @@ +"file","ntax","nchar","ratio","default_med","default_min","default_time","thorough_med","thorough_min","thorough_time","improvement","slowdown" +"project4626.nex",63,16,0.253968253968254,35,35,1.35,35,34,4.45,0,3.2962962962963 +"project3437.nex",64,89,1.390625,278,277,1.58,276,275,7.04000000000001,2,4.45569620253165 +"project3617.nex",65,361,5.55384615384615,2899,2885,9.63,2885,2856,24.17,14,2.50986500519211 +"project4420.nex",68,60,0.882352941176471,188,188,1.75,189,187,4.90000000000003,-1,2.80000000000002 +"project3970.nex",68,339,4.98529411764706,1345,1324,7.5,1312,1304,22.83,33,3.044 +"project4147.nex",71,150,2.11267605633803,532,525,6.5,530,527,18.6600000000001,2,2.87076923076924 +"project3896.nex",72,201,2.79166666666667,868,867,8.29999999999995,869,867,29.55,-1,3.56024096385544 +"project4553.nex",72,244,3.38888888888889,1035,1025,3.38999999999999,1017,1002,8.24000000000001,18,2.43067846607671 +"project4306.nex",73,233,3.19178082191781,655,649,12.9200000000001,645,640,35.98,10,2.78482972136221 +"project689 (1).nex",76,173,2.27631578947368,505,504,12.3099999999999,501,496,31.4400000000001,4,2.55402112103982 +"project563.nex",82,49,0.597560975609756,156,154,2.22000000000003,156,154,7.26999999999998,0,3.27477477477473 +"project549.nex",84,384,4.57142857142857,910,906,20.8999999999999,903,901,61.5599999999999,7,2.94545454545456 +"project1210.nex",86,17,0.197674418604651,45,45,0.440000000000055,45,45,1.48000000000002,0,3.36363636363599 +"project3558.nex",86,59,0.686046511627907,198,195,1.68999999999983,196,194,5.45000000000005,2,3.22485207100627 +"project3637.nex",86,528,6.13953488372093,2560,2544,24.1200000000001,2486,2460,90.2999999999997,74,3.74378109452733 diff --git a/dev/benchmarks/p2_fuse_5seed.csv b/dev/benchmarks/p2_fuse_5seed.csv new file mode 100644 index 000000000..994f68a4a --- /dev/null +++ b/dev/benchmarks/p2_fuse_5seed.csv @@ -0,0 +1,181 @@ +"cfg","dataset","seed","score","candidates" +"baseline","Wortley2006",1,485,41570896 +"intraFuse","Wortley2006",1,483,34926496 +"fuseFreq","Wortley2006",1,485,41570896 +"fuseEqual","Wortley2006",1,484,34904687 +"clipTips","Wortley2006",1,480,29283741 +"wagner5","Wortley2006",1,482,36391655 +"baseline","Eklund2004",1,440,84684820 +"intraFuse","Eklund2004",1,440,62711794 +"fuseFreq","Eklund2004",1,440,84684820 +"fuseEqual","Eklund2004",1,440,99023337 +"clipTips","Eklund2004",1,441,62481678 +"wagner5","Eklund2004",1,440,83919950 +"baseline","Zanol2014",1,1263,421365335 +"intraFuse","Zanol2014",1,1266,377043499 +"fuseFreq","Zanol2014",1,1264,329628125 +"fuseEqual","Zanol2014",1,1266,377043499 +"clipTips","Zanol2014",1,1269,243764334 +"wagner5","Zanol2014",1,1266,315143279 +"baseline","Zhu2013",1,626,370707289 +"intraFuse","Zhu2013",1,626,448049016 +"fuseFreq","Zhu2013",1,626,360954055 +"fuseEqual","Zhu2013",1,626,448049016 +"clipTips","Zhu2013",1,630,258483218 +"wagner5","Zhu2013",1,627,354091783 +"baseline","Giles2015",1,671,406039950 +"intraFuse","Giles2015",1,673,417685203 +"fuseFreq","Giles2015",1,673,381323258 +"fuseEqual","Giles2015",1,673,417685203 +"clipTips","Giles2015",1,673,337343021 +"wagner5","Giles2015",1,672,415431625 +"baseline","Dikow2009",1,1606,372115534 +"intraFuse","Dikow2009",1,1606,389730451 +"fuseFreq","Dikow2009",1,1607,408820411 +"fuseEqual","Dikow2009",1,1606,389730451 +"clipTips","Dikow2009",1,1606,268659050 +"wagner5","Dikow2009",1,1606,380165923 +"baseline","Wortley2006",2,483,38616547 +"intraFuse","Wortley2006",2,481,39997998 +"fuseFreq","Wortley2006",2,483,38616547 +"fuseEqual","Wortley2006",2,482,37004210 +"clipTips","Wortley2006",2,484,29267044 +"wagner5","Wortley2006",2,482,34109790 +"baseline","Eklund2004",2,440,93873455 +"intraFuse","Eklund2004",2,440,97017787 +"fuseFreq","Eklund2004",2,440,93873455 +"fuseEqual","Eklund2004",2,440,96581338 +"clipTips","Eklund2004",2,440,67327848 +"wagner5","Eklund2004",2,440,76157545 +"baseline","Zanol2014",2,1264,359832065 +"intraFuse","Zanol2014",2,1264,373678403 +"fuseFreq","Zanol2014",2,1268,346590188 +"fuseEqual","Zanol2014",2,1264,373678403 +"clipTips","Zanol2014",2,1263,231269957 +"wagner5","Zanol2014",2,1265,333147540 +"baseline","Zhu2013",2,630,337323906 +"intraFuse","Zhu2013",2,625,412134281 +"fuseFreq","Zhu2013",2,630,337323906 +"fuseEqual","Zhu2013",2,625,412134281 +"clipTips","Zhu2013",2,630,274710579 +"wagner5","Zhu2013",2,627,370791265 +"baseline","Giles2015",2,672,524808796 +"intraFuse","Giles2015",2,672,462130641 +"fuseFreq","Giles2015",2,672,524808796 +"fuseEqual","Giles2015",2,672,462130641 +"clipTips","Giles2015",2,673,321587085 +"wagner5","Giles2015",2,672,400656946 +"baseline","Dikow2009",2,1606,416466253 +"intraFuse","Dikow2009",2,1607,404678679 +"fuseFreq","Dikow2009",2,1607,430703624 +"fuseEqual","Dikow2009",2,1607,404678679 +"clipTips","Dikow2009",2,1607,293691476 +"wagner5","Dikow2009",2,1606,484714081 +"baseline","Wortley2006",3,485,33476553 +"intraFuse","Wortley2006",3,484,36681570 +"fuseFreq","Wortley2006",3,485,33476553 +"fuseEqual","Wortley2006",3,483,35194598 +"clipTips","Wortley2006",3,485,29670090 +"wagner5","Wortley2006",3,482,36077788 +"baseline","Eklund2004",3,440,96750733 +"intraFuse","Eklund2004",3,440,75374358 +"fuseFreq","Eklund2004",3,440,96750733 +"fuseEqual","Eklund2004",3,441,76805216 +"clipTips","Eklund2004",3,440,76303880 +"wagner5","Eklund2004",3,440,103474510 +"baseline","Zanol2014",3,1268,360431126 +"intraFuse","Zanol2014",3,1266,345818077 +"fuseFreq","Zanol2014",3,1269,355248581 +"fuseEqual","Zanol2014",3,1266,345818077 +"clipTips","Zanol2014",3,1266,243432313 +"wagner5","Zanol2014",3,1263,350071241 +"baseline","Zhu2013",3,629,343784935 +"intraFuse","Zhu2013",3,631,360896187 +"fuseFreq","Zhu2013",3,629,343784935 +"fuseEqual","Zhu2013",3,631,360896187 +"clipTips","Zhu2013",3,629,248064232 +"wagner5","Zhu2013",3,628,361358957 +"baseline","Giles2015",3,671,491328798 +"intraFuse","Giles2015",3,672,454561957 +"fuseFreq","Giles2015",3,671,481408370 +"fuseEqual","Giles2015",3,672,454561957 +"clipTips","Giles2015",3,673,314981615 +"wagner5","Giles2015",3,673,395935002 +"baseline","Dikow2009",3,1606,405179884 +"intraFuse","Dikow2009",3,1606,397189857 +"fuseFreq","Dikow2009",3,1606,422797739 +"fuseEqual","Dikow2009",3,1606,397189857 +"clipTips","Dikow2009",3,1606,308476287 +"wagner5","Dikow2009",3,1607,391590566 +"baseline","Wortley2006",4,485,38426065 +"intraFuse","Wortley2006",4,481,30472365 +"fuseFreq","Wortley2006",4,485,38426065 +"fuseEqual","Wortley2006",4,483,36868403 +"clipTips","Wortley2006",4,483,30883075 +"wagner5","Wortley2006",4,482,35180379 +"baseline","Eklund2004",4,440,98341281 +"intraFuse","Eklund2004",4,440,71570971 +"fuseFreq","Eklund2004",4,440,98341281 +"fuseEqual","Eklund2004",4,440,110869985 +"clipTips","Eklund2004",4,440,69045725 +"wagner5","Eklund2004",4,440,87640868 +"baseline","Zanol2014",4,1263,316517081 +"intraFuse","Zanol2014",4,1264,377844943 +"fuseFreq","Zanol2014",4,1263,316517081 +"fuseEqual","Zanol2014",4,1264,377844943 +"clipTips","Zanol2014",4,1268,244560721 +"wagner5","Zanol2014",4,1265,357703869 +"baseline","Zhu2013",4,629,324227265 +"intraFuse","Zhu2013",4,625,414559322 +"fuseFreq","Zhu2013",4,629,324227265 +"fuseEqual","Zhu2013",4,625,414559322 +"clipTips","Zhu2013",4,629,289028193 +"wagner5","Zhu2013",4,630,355091485 +"baseline","Giles2015",4,672,403869623 +"intraFuse","Giles2015",4,673,435902753 +"fuseFreq","Giles2015",4,672,415207689 +"fuseEqual","Giles2015",4,673,435902753 +"clipTips","Giles2015",4,672,309300895 +"wagner5","Giles2015",4,673,432228869 +"baseline","Dikow2009",4,1607,422492579 +"intraFuse","Dikow2009",4,1607,426949097 +"fuseFreq","Dikow2009",4,1606,405140432 +"fuseEqual","Dikow2009",4,1607,426949097 +"clipTips","Dikow2009",4,1607,301065276 +"wagner5","Dikow2009",4,1606,466105135 +"baseline","Wortley2006",5,484,40373785 +"intraFuse","Wortley2006",5,483,38384374 +"fuseFreq","Wortley2006",5,484,40373785 +"fuseEqual","Wortley2006",5,483,40000547 +"clipTips","Wortley2006",5,484,30198429 +"wagner5","Wortley2006",5,485,35882431 +"baseline","Eklund2004",5,440,98300507 +"intraFuse","Eklund2004",5,440,101327689 +"fuseFreq","Eklund2004",5,440,98300507 +"fuseEqual","Eklund2004",5,440,110559153 +"clipTips","Eklund2004",5,441,75053941 +"wagner5","Eklund2004",5,440,90015638 +"baseline","Zanol2014",5,1268,363290368 +"intraFuse","Zanol2014",5,1265,348431906 +"fuseFreq","Zanol2014",5,1268,368618798 +"fuseEqual","Zanol2014",5,1265,348431906 +"clipTips","Zanol2014",5,1266,290725402 +"wagner5","Zanol2014",5,1263,356850240 +"baseline","Zhu2013",5,630,337214291 +"intraFuse","Zhu2013",5,628,410764690 +"fuseFreq","Zhu2013",5,630,337214291 +"fuseEqual","Zhu2013",5,628,410764690 +"clipTips","Zhu2013",5,628,317981234 +"wagner5","Zhu2013",5,627,339293684 +"baseline","Giles2015",5,671,403065132 +"intraFuse","Giles2015",5,672,517734593 +"fuseFreq","Giles2015",5,671,403065132 +"fuseEqual","Giles2015",5,672,517734593 +"clipTips","Giles2015",5,673,309645059 +"wagner5","Giles2015",5,672,416814012 +"baseline","Dikow2009",5,1607,374722935 +"intraFuse","Dikow2009",5,1606,391344618 +"fuseFreq","Dikow2009",5,1606,433099599 +"fuseEqual","Dikow2009",5,1606,391344618 +"clipTips","Dikow2009",5,1607,288919635 +"wagner5","Dikow2009",5,1606,365810668 diff --git a/dev/benchmarks/p2_levers.csv b/dev/benchmarks/p2_levers.csv new file mode 100644 index 000000000..5d80d231f --- /dev/null +++ b/dev/benchmarks/p2_levers.csv @@ -0,0 +1,73 @@ +"cfg","dataset","seed","score","candidates" +"baseline","Wortley2006",1,485,41570896 +"ratchet6","Wortley2006",1,486,19975746 +"ratchet3","Wortley2006",1,485,13479564 +"adaptiveOff","Wortley2006",1,487,31453521 +"sectorHeavy","Wortley2006",1,483,38270628 +"rebalance","Wortley2006",1,488,21064184 +"baseline","Eklund2004",1,440,84684820 +"ratchet6","Eklund2004",1,440,56678412 +"ratchet3","Eklund2004",1,440,35751581 +"adaptiveOff","Eklund2004",1,440,82219483 +"sectorHeavy","Eklund2004",1,441,88927352 +"rebalance","Eklund2004",1,440,61270761 +"baseline","Zanol2014",1,1263,421365335 +"ratchet6","Zanol2014",1,1264,206728185 +"ratchet3","Zanol2014",1,1266,165791888 +"adaptiveOff","Zanol2014",1,1263,421365335 +"sectorHeavy","Zanol2014",1,1266,313672366 +"rebalance","Zanol2014",1,1274,171041457 +"baseline","Zhu2013",1,626,370707289 +"ratchet6","Zhu2013",1,633,250933255 +"ratchet3","Zhu2013",1,629,169274556 +"adaptiveOff","Zhu2013",1,626,370707289 +"sectorHeavy","Zhu2013",1,630,340156814 +"rebalance","Zhu2013",1,628,168122887 +"baseline","Giles2015",1,671,406039950 +"ratchet6","Giles2015",1,671,241953889 +"ratchet3","Giles2015",1,673,189274561 +"adaptiveOff","Giles2015",1,671,406039950 +"sectorHeavy","Giles2015",1,672,408086240 +"rebalance","Giles2015",1,673,209593063 +"baseline","Dikow2009",1,1606,372115534 +"ratchet6","Dikow2009",1,1607,245488190 +"ratchet3","Dikow2009",1,1607,187210920 +"adaptiveOff","Dikow2009",1,1606,372115534 +"sectorHeavy","Dikow2009",1,1606,386228598 +"rebalance","Dikow2009",1,1607,193670801 +"baseline","Wortley2006",2,483,38616547 +"ratchet6","Wortley2006",2,483,22533186 +"ratchet3","Wortley2006",2,487,13471858 +"adaptiveOff","Wortley2006",2,483,31055830 +"sectorHeavy","Wortley2006",2,485,34418491 +"rebalance","Wortley2006",2,485,24804440 +"baseline","Eklund2004",2,440,93873455 +"ratchet6","Eklund2004",2,440,64437145 +"ratchet3","Eklund2004",2,440,41532400 +"adaptiveOff","Eklund2004",2,440,82095238 +"sectorHeavy","Eklund2004",2,440,99196910 +"rebalance","Eklund2004",2,440,70202811 +"baseline","Zanol2014",2,1264,359832065 +"ratchet6","Zanol2014",2,1268,176898725 +"ratchet3","Zanol2014",2,1266,152633282 +"adaptiveOff","Zanol2014",2,1264,359832065 +"sectorHeavy","Zanol2014",2,1263,331430090 +"rebalance","Zanol2014",2,1266,170035973 +"baseline","Zhu2013",2,630,337323906 +"ratchet6","Zhu2013",2,627,189821548 +"ratchet3","Zhu2013",2,630,150595662 +"adaptiveOff","Zhu2013",2,630,337323906 +"sectorHeavy","Zhu2013",2,628,404606325 +"rebalance","Zhu2013",2,629,178830081 +"baseline","Giles2015",2,672,524808796 +"ratchet6","Giles2015",2,673,225002086 +"ratchet3","Giles2015",2,673,182993464 +"adaptiveOff","Giles2015",2,672,524808796 +"sectorHeavy","Giles2015",2,672,417627395 +"rebalance","Giles2015",2,673,202582066 +"baseline","Dikow2009",2,1606,416466253 +"ratchet6","Dikow2009",2,1607,234993072 +"ratchet3","Dikow2009",2,1607,204536032 +"adaptiveOff","Dikow2009",2,1606,416466253 +"sectorHeavy","Dikow2009",2,1606,419143811 +"rebalance","Dikow2009",2,1606,239313695 diff --git a/dev/benchmarks/p2_levers_fuse.csv b/dev/benchmarks/p2_levers_fuse.csv new file mode 100644 index 000000000..24f13c81b --- /dev/null +++ b/dev/benchmarks/p2_levers_fuse.csv @@ -0,0 +1,73 @@ +"cfg","dataset","seed","score","candidates" +"baseline","Wortley2006",1,485,41570896 +"intraFuse","Wortley2006",1,483,34926496 +"fuseFreq","Wortley2006",1,485,41570896 +"fuseEqual","Wortley2006",1,484,34904687 +"clipTips","Wortley2006",1,480,29283741 +"wagner5","Wortley2006",1,482,36391655 +"baseline","Eklund2004",1,440,84684820 +"intraFuse","Eklund2004",1,440,62711794 +"fuseFreq","Eklund2004",1,440,84684820 +"fuseEqual","Eklund2004",1,440,99023337 +"clipTips","Eklund2004",1,441,62481678 +"wagner5","Eklund2004",1,440,83919950 +"baseline","Zanol2014",1,1263,421365335 +"intraFuse","Zanol2014",1,1266,377043499 +"fuseFreq","Zanol2014",1,1264,329628125 +"fuseEqual","Zanol2014",1,1266,377043499 +"clipTips","Zanol2014",1,1269,243764334 +"wagner5","Zanol2014",1,1266,315143279 +"baseline","Zhu2013",1,626,370707289 +"intraFuse","Zhu2013",1,626,448049016 +"fuseFreq","Zhu2013",1,626,360954055 +"fuseEqual","Zhu2013",1,626,448049016 +"clipTips","Zhu2013",1,630,258483218 +"wagner5","Zhu2013",1,627,354091783 +"baseline","Giles2015",1,671,406039950 +"intraFuse","Giles2015",1,673,417685203 +"fuseFreq","Giles2015",1,673,381323258 +"fuseEqual","Giles2015",1,673,417685203 +"clipTips","Giles2015",1,673,337343021 +"wagner5","Giles2015",1,672,415431625 +"baseline","Dikow2009",1,1606,372115534 +"intraFuse","Dikow2009",1,1606,389730451 +"fuseFreq","Dikow2009",1,1607,408820411 +"fuseEqual","Dikow2009",1,1606,389730451 +"clipTips","Dikow2009",1,1606,268659050 +"wagner5","Dikow2009",1,1606,380165923 +"baseline","Wortley2006",2,483,38616547 +"intraFuse","Wortley2006",2,481,39997998 +"fuseFreq","Wortley2006",2,483,38616547 +"fuseEqual","Wortley2006",2,482,37004210 +"clipTips","Wortley2006",2,484,29267044 +"wagner5","Wortley2006",2,482,34109790 +"baseline","Eklund2004",2,440,93873455 +"intraFuse","Eklund2004",2,440,97017787 +"fuseFreq","Eklund2004",2,440,93873455 +"fuseEqual","Eklund2004",2,440,96581338 +"clipTips","Eklund2004",2,440,67327848 +"wagner5","Eklund2004",2,440,76157545 +"baseline","Zanol2014",2,1264,359832065 +"intraFuse","Zanol2014",2,1264,373678403 +"fuseFreq","Zanol2014",2,1268,346590188 +"fuseEqual","Zanol2014",2,1264,373678403 +"clipTips","Zanol2014",2,1263,231269957 +"wagner5","Zanol2014",2,1265,333147540 +"baseline","Zhu2013",2,630,337323906 +"intraFuse","Zhu2013",2,625,412134281 +"fuseFreq","Zhu2013",2,630,337323906 +"fuseEqual","Zhu2013",2,625,412134281 +"clipTips","Zhu2013",2,630,274710579 +"wagner5","Zhu2013",2,627,370791265 +"baseline","Giles2015",2,672,524808796 +"intraFuse","Giles2015",2,672,462130641 +"fuseFreq","Giles2015",2,672,524808796 +"fuseEqual","Giles2015",2,672,462130641 +"clipTips","Giles2015",2,673,321587085 +"wagner5","Giles2015",2,672,400656946 +"baseline","Dikow2009",2,1606,416466253 +"intraFuse","Dikow2009",2,1607,404678679 +"fuseFreq","Dikow2009",2,1607,430703624 +"fuseEqual","Dikow2009",2,1607,404678679 +"clipTips","Dikow2009",2,1607,293691476 +"wagner5","Dikow2009",2,1606,484714081 diff --git a/dev/benchmarks/p2_optin_5seed.csv b/dev/benchmarks/p2_optin_5seed.csv new file mode 100644 index 000000000..1a3ab0451 --- /dev/null +++ b/dev/benchmarks/p2_optin_5seed.csv @@ -0,0 +1,121 @@ +"cfg","dataset","seed","score","candidates" +"baseline","Wortley2006",1,485,41570896 +"intraFuse","Wortley2006",1,483,34926496 +"wagner5","Wortley2006",1,482,36391655 +"combo","Wortley2006",1,484,35433866 +"baseline","Eklund2004",1,440,84684820 +"intraFuse","Eklund2004",1,440,62711794 +"wagner5","Eklund2004",1,440,83919950 +"combo","Eklund2004",1,440,74873467 +"baseline","Zanol2014",1,1263,421365335 +"intraFuse","Zanol2014",1,1266,377043499 +"wagner5","Zanol2014",1,1266,315143279 +"combo","Zanol2014",1,1267,345249538 +"baseline","Zhu2013",1,626,370707289 +"intraFuse","Zhu2013",1,626,448049016 +"wagner5","Zhu2013",1,627,354091783 +"combo","Zhu2013",1,626,395022844 +"baseline","Giles2015",1,671,406039950 +"intraFuse","Giles2015",1,673,417685203 +"wagner5","Giles2015",1,672,415431625 +"combo","Giles2015",1,672,454421337 +"baseline","Dikow2009",1,1606,372115534 +"intraFuse","Dikow2009",1,1606,389730451 +"wagner5","Dikow2009",1,1606,380165923 +"combo","Dikow2009",1,1606,414772739 +"baseline","Wortley2006",2,483,38616547 +"intraFuse","Wortley2006",2,481,39997998 +"wagner5","Wortley2006",2,482,34109790 +"combo","Wortley2006",2,485,30666317 +"baseline","Eklund2004",2,440,93873455 +"intraFuse","Eklund2004",2,440,97017787 +"wagner5","Eklund2004",2,440,76157545 +"combo","Eklund2004",2,440,68206736 +"baseline","Zanol2014",2,1264,359832065 +"intraFuse","Zanol2014",2,1264,373678403 +"wagner5","Zanol2014",2,1265,333147540 +"combo","Zanol2014",2,1266,416033805 +"baseline","Zhu2013",2,630,337323906 +"intraFuse","Zhu2013",2,625,412134281 +"wagner5","Zhu2013",2,627,370791265 +"combo","Zhu2013",2,626,406287034 +"baseline","Giles2015",2,672,524808796 +"intraFuse","Giles2015",2,672,462130641 +"wagner5","Giles2015",2,672,400656946 +"combo","Giles2015",2,672,428602652 +"baseline","Dikow2009",2,1606,416466253 +"intraFuse","Dikow2009",2,1607,404678679 +"wagner5","Dikow2009",2,1606,484714081 +"combo","Dikow2009",2,1606,395491199 +"baseline","Wortley2006",3,485,33476553 +"intraFuse","Wortley2006",3,484,36681570 +"wagner5","Wortley2006",3,482,36077788 +"combo","Wortley2006",3,481,27895958 +"baseline","Eklund2004",3,440,96750733 +"intraFuse","Eklund2004",3,440,75374358 +"wagner5","Eklund2004",3,440,103474510 +"combo","Eklund2004",3,440,64094326 +"baseline","Zanol2014",3,1268,360431126 +"intraFuse","Zanol2014",3,1266,345818077 +"wagner5","Zanol2014",3,1263,350071241 +"combo","Zanol2014",3,1266,346466168 +"baseline","Zhu2013",3,629,343784935 +"intraFuse","Zhu2013",3,631,360896187 +"wagner5","Zhu2013",3,628,361358957 +"combo","Zhu2013",3,630,409215203 +"baseline","Giles2015",3,671,491328798 +"intraFuse","Giles2015",3,672,454561957 +"wagner5","Giles2015",3,673,395935002 +"combo","Giles2015",3,670,510589043 +"baseline","Dikow2009",3,1606,405179884 +"intraFuse","Dikow2009",3,1606,397189857 +"wagner5","Dikow2009",3,1607,391590566 +"combo","Dikow2009",3,1606,412056345 +"baseline","Wortley2006",4,485,38426065 +"intraFuse","Wortley2006",4,481,30472365 +"wagner5","Wortley2006",4,482,35180379 +"combo","Wortley2006",4,483,35143796 +"baseline","Eklund2004",4,440,98341281 +"intraFuse","Eklund2004",4,440,71570971 +"wagner5","Eklund2004",4,440,87640868 +"combo","Eklund2004",4,440,86105163 +"baseline","Zanol2014",4,1263,316517081 +"intraFuse","Zanol2014",4,1264,377844943 +"wagner5","Zanol2014",4,1265,357703869 +"combo","Zanol2014",4,1269,360536735 +"baseline","Zhu2013",4,629,324227265 +"intraFuse","Zhu2013",4,625,414559322 +"wagner5","Zhu2013",4,630,355091485 +"combo","Zhu2013",4,626,386960563 +"baseline","Giles2015",4,672,403869623 +"intraFuse","Giles2015",4,673,435902753 +"wagner5","Giles2015",4,673,432228869 +"combo","Giles2015",4,671,449452204 +"baseline","Dikow2009",4,1607,422492579 +"intraFuse","Dikow2009",4,1607,426949097 +"wagner5","Dikow2009",4,1606,466105135 +"combo","Dikow2009",4,1606,365401824 +"baseline","Wortley2006",5,484,40373785 +"intraFuse","Wortley2006",5,483,38384374 +"wagner5","Wortley2006",5,485,35882431 +"combo","Wortley2006",5,483,36293254 +"baseline","Eklund2004",5,440,98300507 +"intraFuse","Eklund2004",5,440,101327689 +"wagner5","Eklund2004",5,440,90015638 +"combo","Eklund2004",5,440,66823562 +"baseline","Zanol2014",5,1268,363290368 +"intraFuse","Zanol2014",5,1265,348431906 +"wagner5","Zanol2014",5,1263,356850240 +"combo","Zanol2014",5,1265,378791607 +"baseline","Zhu2013",5,630,337214291 +"intraFuse","Zhu2013",5,628,410764690 +"wagner5","Zhu2013",5,627,339293684 +"combo","Zhu2013",5,628,405573710 +"baseline","Giles2015",5,671,403065132 +"intraFuse","Giles2015",5,672,517734593 +"wagner5","Giles2015",5,672,416814012 +"combo","Giles2015",5,672,427674323 +"baseline","Dikow2009",5,1607,374722935 +"intraFuse","Dikow2009",5,1606,391344618 +"wagner5","Dikow2009",5,1606,365810668 +"combo","Dikow2009",5,1607,419058655 diff --git a/dev/benchmarks/p3_rebalance.csv b/dev/benchmarks/p3_rebalance.csv new file mode 100644 index 000000000..28e20d866 --- /dev/null +++ b/dev/benchmarks/p3_rebalance.csv @@ -0,0 +1,91 @@ +"cfg","dataset","seed","score","candidates" +"baseline","Wortley2006",1,485,41570896 +"css4","Wortley2006",1,483,41101737 +"ratchetDown","Wortley2006",1,483,27321827 +"rebalA","Wortley2006",1,483,41101737 +"rebalB","Wortley2006",1,486,24776083 +"baseline","Eklund2004",1,440,84684820 +"css4","Eklund2004",1,440,85400101 +"ratchetDown","Eklund2004",1,440,67860361 +"rebalA","Eklund2004",1,440,85400101 +"rebalB","Eklund2004",1,440,75853736 +"baseline","Zanol2014",1,1263,421365335 +"css4","Zanol2014",1,1263,406685064 +"ratchetDown","Zanol2014",1,1268,210318021 +"rebalA","Zanol2014",1,1268,257327835 +"rebalB","Zanol2014",1,1268,212157573 +"baseline","Zhu2013",1,626,370707289 +"css4","Zhu2013",1,626,338006278 +"ratchetDown","Zhu2013",1,633,200493534 +"rebalA","Zhu2013",1,627,265031779 +"rebalB","Zhu2013",1,632,213068105 +"baseline","Giles2015",1,671,406039950 +"css4","Giles2015",1,673,393953714 +"ratchetDown","Giles2015",1,673,238646302 +"rebalA","Giles2015",1,672,285578158 +"rebalB","Giles2015",1,673,232648139 +"baseline","Dikow2009",1,1606,372115534 +"css4","Dikow2009",1,1606,372115534 +"ratchetDown","Dikow2009",1,1606,255617307 +"rebalA","Dikow2009",1,1607,284558522 +"rebalB","Dikow2009",1,1606,255617307 +"baseline","Wortley2006",2,483,38616547 +"css4","Wortley2006",2,484,34469661 +"ratchetDown","Wortley2006",2,482,27259629 +"rebalA","Wortley2006",2,484,34469661 +"rebalB","Wortley2006",2,484,29833732 +"baseline","Eklund2004",2,440,93873455 +"css4","Eklund2004",2,440,87480234 +"ratchetDown","Eklund2004",2,440,71578715 +"rebalA","Eklund2004",2,440,87480234 +"rebalB","Eklund2004",2,440,70115708 +"baseline","Zanol2014",2,1264,359832065 +"css4","Zanol2014",2,1265,343270450 +"ratchetDown","Zanol2014",2,1271,222903937 +"rebalA","Zanol2014",2,1270,254731430 +"rebalB","Zanol2014",2,1271,222903937 +"baseline","Zhu2013",2,630,337323906 +"css4","Zhu2013",2,628,340526861 +"ratchetDown","Zhu2013",2,627,208355115 +"rebalA","Zhu2013",2,631,261715372 +"rebalB","Zhu2013",2,627,233521790 +"baseline","Giles2015",2,672,524808796 +"css4","Giles2015",2,672,493523388 +"ratchetDown","Giles2015",2,673,232868396 +"rebalA","Giles2015",2,672,301968860 +"rebalB","Giles2015",2,673,355160905 +"baseline","Dikow2009",2,1606,416466253 +"css4","Dikow2009",2,1606,416466253 +"ratchetDown","Dikow2009",2,1607,244395945 +"rebalA","Dikow2009",2,1606,350378943 +"rebalB","Dikow2009",2,1607,244395945 +"baseline","Wortley2006",3,485,33476553 +"css4","Wortley2006",3,484,43456099 +"ratchetDown","Wortley2006",3,487,27466789 +"rebalA","Wortley2006",3,484,43456099 +"rebalB","Wortley2006",3,482,29982070 +"baseline","Eklund2004",3,440,96750733 +"css4","Eklund2004",3,440,90385516 +"ratchetDown","Eklund2004",3,440,65822843 +"rebalA","Eklund2004",3,440,90385516 +"rebalB","Eklund2004",3,441,61735036 +"baseline","Zanol2014",3,1268,360431126 +"css4","Zanol2014",3,1268,348193623 +"ratchetDown","Zanol2014",3,1266,197260144 +"rebalA","Zanol2014",3,1263,271892668 +"rebalB","Zanol2014",3,1266,197260144 +"baseline","Zhu2013",3,629,343784935 +"css4","Zhu2013",3,627,349874362 +"ratchetDown","Zhu2013",3,629,213015739 +"rebalA","Zhu2013",3,627,267620332 +"rebalB","Zhu2013",3,629,210064134 +"baseline","Giles2015",3,671,491328798 +"css4","Giles2015",3,671,491328798 +"ratchetDown","Giles2015",3,674,251197510 +"rebalA","Giles2015",3,672,294968604 +"rebalB","Giles2015",3,674,251197510 +"baseline","Dikow2009",3,1606,405179884 +"css4","Dikow2009",3,1606,405179884 +"ratchetDown","Dikow2009",3,1606,241619535 +"rebalA","Dikow2009",3,1606,278235263 +"rebalB","Dikow2009",3,1606,241619535 diff --git a/dev/benchmarks/pgo_recipe.md b/dev/benchmarks/pgo_recipe.md new file mode 100644 index 000000000..c055d8d65 --- /dev/null +++ b/dev/benchmarks/pgo_recipe.md @@ -0,0 +1,118 @@ +# PGO (Profile-Guided Optimization) Build Recipe + +## Overview + +PGO lets GCC optimize branch prediction, function layout, and inlining +decisions based on actual runtime behavior. Requires two compilation +passes: one instrumented build to gather profile data, then a second +build that uses that data for optimization. + +## Results (2026-03-16, GCC 13 / rtools45, Windows x86_64) + +| Benchmark | Baseline (s) | PGO (s) | Speedup | +|-----------|-------------|---------|---------| +| Vinther EW (23 tips) | 0.240 | 0.240 | 0% | +| Vinther IW (23 tips) | 0.170 | 0.190 | -12% | +| Zhu EW (75 tips) | 4.010 | 3.790 | 5% | +| Zhu IW (75 tips) | 5.340 | 4.990 | 7% | +| Agnarsson EW (62 tips) | 2.200 | 2.080 | 5% | + +PGO provides a modest ~5-7% speedup on medium-sized datasets where the +C++ hot path dominates. On small datasets, R overhead and startup time +swamp any C++ improvement. Scores are identical (correctness verified: +53/53 driven search tests pass). + +## Build Steps + +All steps run from the package root directory. + +### Step 1: Baseline build (no PGO) + +Ensure no `src/Makevars.win` exists: + +```bash +rm -f src/Makevars.win src/*.o src/*.dll +R CMD INSTALL --library=.agent-pgo . +``` + +### Step 2: Instrumented build + +Create `src/Makevars.win`: + +```makefile +PROFILE_DIR = C:/Users/pjjg18/GitHub/TreeSearch/.pgo-data +PKG_CXXFLAGS = -fprofile-generate=$(PROFILE_DIR) +PKG_CFLAGS = -fprofile-generate=$(PROFILE_DIR) +PKG_LIBS = -fprofile-generate +``` + +Build and install: + +```bash +rm -rf .pgo-data && mkdir .pgo-data +rm -f src/*.o src/*.dll +R CMD INSTALL --library=.agent-pgo-gen . +``` + +### Step 3: Training workload + +Load the instrumented build and exercise all major code paths: + +```r +library(TreeSearch, lib.loc = ".agent-pgo-gen") +data(inapplicable.phyData, package = "TreeSearch") + +# EW + IW on small and medium datasets +MaximizeParsimony(inapplicable.phyData[["Vinther2008"]], + maxReplicates = 5L, targetHits = 3L, verbosity = 0L) +MaximizeParsimony(inapplicable.phyData[["Vinther2008"]], concavity = 10, + maxReplicates = 5L, targetHits = 3L, verbosity = 0L) +MaximizeParsimony(inapplicable.phyData[["Zhu2013"]], + maxReplicates = 3L, targetHits = 2L, verbosity = 0L) +MaximizeParsimony(inapplicable.phyData[["Zhu2013"]], concavity = 10, + maxReplicates = 3L, targetHits = 2L, verbosity = 0L) +MaximizeParsimony(inapplicable.phyData[["Agnarsson2004"]], + maxReplicates = 3L, targetHits = 2L, verbosity = 0L) +``` + +The `.gcda` files appear under `.pgo-data/C~/Users/.../src/`. + +### Step 4: PGO-use build + +Replace `src/Makevars.win`: + +```makefile +PROFILE_DIR = C:/Users/pjjg18/GitHub/TreeSearch/.pgo-data +PKG_CXXFLAGS = -fprofile-use=$(PROFILE_DIR) -fprofile-correction +PKG_CFLAGS = -fprofile-use=$(PROFILE_DIR) -fprofile-correction +PKG_LIBS = -fprofile-use +``` + +Build (**note: takes 3-5 minutes**, much longer than normal): + +```bash +rm -f src/*.o src/*.dll +R CMD INSTALL --library=.agent-pgo-use . +``` + +### Step 5: Clean up + +**Always remove `src/Makevars.win` after PGO builds** — leaving PGO +flags in place will cause segfaults (instrumented build) or broken +builds (PGO-use without matching `.gcda` files): + +```bash +rm -f src/Makevars.win src/*.o src/*.dll +``` + +## Notes + +- `-fprofile-correction` is needed because some source files may have + changed since profile generation. It tells GCC to accept mismatched + profiles gracefully rather than erroring. +- The `.pgo-data/` directory contains machine-specific binary data. + Do not commit to version control. +- PGO-use compilation is 2-5× slower than normal. Allow 5 minutes for + a full rebuild (30+ source files). +- GCC on Windows (rtools45) nests `.gcda` files under a path encoding + like `C~/Users/...`. This is expected behavior. diff --git a/dev/benchmarks/phase_yield_phase0.csv b/dev/benchmarks/phase_yield_phase0.csv new file mode 100644 index 000000000..6ca4828fb --- /dev/null +++ b/dev/benchmarks/phase_yield_phase0.csv @@ -0,0 +1,19 @@ +"dataset","tips","seed","score","cand","reps","last_improved","late_frac","pct_wagner","pct_initial_tbr","pct_sector","pct_ratchet","pct_final_tbr","pct_fuse" +"Wortley2006",37,1,481,709081694,326,147,0.55,6,2,7,83,2,0 +"Wortley2006",37,2,481,679934093,305,27,0.91,6,2,7,83,2,0 +"Wortley2006",37,3,482,387329129,182,34,0.81,6,2,7,83,2,0 +"Eklund2004",54,1,440,458323192,90,2,0.98,11,5,7,76,2,0 +"Eklund2004",54,2,440,504053508,97,1,0.99,11,4,7,76,2,0 +"Eklund2004",54,3,440,479554050,93,4,0.96,11,4,7,76,2,0 +"Zanol2014",74,1,1265,679219501,41,16,0.61,5,5,22,66,2,0 +"Zanol2014",74,2,1264,735739258,40,10,0.75,5,5,22,66,2,0 +"Zanol2014",74,3,1264,689791719,38,31,0.18,5,4,23,66,2,0 +"Zhu2013",75,1,626,1052992859,61,10,0.84,8,3,24,63,2,0 +"Zhu2013",75,2,627,1047657194,61,34,0.44,8,4,23,61,2,1 +"Zhu2013",75,3,626,1061817495,60,45,0.25,8,3,23,63,2,0 +"Giles2015",78,1,671,1114974502,54,16,0.7,6,4,21,65,2,1 +"Giles2015",78,2,672,1098705888,52,3,0.94,6,4,21,66,2,1 +"Giles2015",78,3,671,1161713038,52,5,0.9,6,4,20,66,2,2 +"Dikow2009",88,1,1606,787819804,38,8,0.79,4,3,21,68,2,1 +"Dikow2009",88,2,1606,769476786,36,4,0.89,4,3,21,68,2,2 +"Dikow2009",88,3,1606,801650507,38,21,0.45,5,3,22,66,2,2 diff --git a/dev/benchmarks/probe_hold.R b/dev/benchmarks/probe_hold.R new file mode 100644 index 000000000..3f432e7ef --- /dev/null +++ b/dev/benchmarks/probe_hold.R @@ -0,0 +1,32 @@ +# RESOLVE the hold-1000-vs-10-trees conflation (user catch). +# `hold 1000` = buffer CAP (1000); the "10 trees" is the emergent count mult deposits. +# Decisive question for a beam design: does TNT's escape need only the ~10 SEED trees, or +# a buffer that keeps GROWING during sectorial? Isolate by building the SAME 10-tree 1271 +# set, then running sectsch under different buffer caps (>=10 so the seed set is intact; +# cap=10 forbids growth, cap=1000 allows it). +suppressMessages({ library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband2"), winslash = "/")); library(TreeTools) }) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m=="-"] <- "?"; MatrixToPhyDat(m) } +nm <- Sys.getenv("TS_DS", "Zanol2014"); phy <- fitch(inapplicable.phyData[[nm]]) +wd <- file.path(tempdir(), paste0("ph", Sys.getpid())); unlink(wd, recursive = TRUE) +dir.create(wd, recursive = TRUE, showWarnings = FALSE); WriteTntCharacters(phy, file.path(wd, "data.tnt")) +bestLen <- function(f) { tr <- ReadTntTree(f); if (inherits(tr,"multiPhylo")) min(vapply(tr,TreeLength,double(1),phy)) else TreeLength(tr,phy) } +ntree <- function(f) { tr <- ReadTntTree(f); if (inherits(tr,"multiPhylo")) length(tr) else 1L } + +# 1. Build the canonical 10-tree 1271 set (hold 1000 mult), save it. +writeLines(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1000;","mult=replic 1;", + "tsave *set10.tre;","save;","tsave/;","quit;"), file.path(wd,"build.run")) +old <- setwd(wd); invisible(suppressWarnings(system2(TNT, "build.run;", stdout=TRUE, stderr=TRUE))); setwd(old) +cat(sprintf("==== %s | seed set: %d trees @ len %.0f ====\n", nm, ntree(file.path(wd,"set10.tre")), bestLen(file.path(wd,"set10.tre")))) + +# 2. Re-load the SAME 10-tree set, run sectsch under varying buffer cap. +for (cap in c(10L, 12L, 25L, 1000L)) { + writeLines(c("mxram 1024;","proc data.tnt;","rseed 1;", + sprintf("hold %d;", cap), "proc set10.tre;", # load seed set under this cap + rep("sectsch=rss;", 10), "tsave *out.tre;","save;","tsave/;","quit;"), + file.path(wd,"sect.run")) + old <- setwd(wd); invisible(suppressWarnings(system2(TNT, "sect.run;", stdout=TRUE, stderr=TRUE))); setwd(old) + cat(sprintf(" hold %4d -> land %.0f (final buffer %d trees)\n", + cap, bestLen(file.path(wd,"out.tre")), ntree(file.path(wd,"out.tre")))) +} diff --git a/dev/benchmarks/ratchet_race.R b/dev/benchmarks/ratchet_race.R new file mode 100644 index 000000000..4e310a30b --- /dev/null +++ b/dev/benchmarks/ratchet_race.R @@ -0,0 +1,122 @@ +# ratchet_race.R — #39 gate-2 ratchet isolation race: TS ratchet vs TNT ratchet +# from an IDENTICAL Wagner start. Answers "does TNT reach the optimum in fewer +# reweight cycles?" Unit = rearrangements (TS total_tbr_moves <-> TNT "Total +# rearrangements examined"); score-parity = validity gate; seed distributions. +# Both optimise the same Fitch objective (inapplicable -> '?'). +# +# Env (tbr_shared_start_lib.R reads TS_LIB / TNT_EXE / T0_DIR): SHARED_LIB (path +# to tbr_shared_start_lib.R), RACE_DATASETS, RACE_SEEDS, RACE_WAGSEED, +# RAT_ITER, RACE_OUT. +source(Sys.getenv("SHARED_LIB", "dev/benchmarks/tbr_shared_start_lib.R")) + +dsN <- strsplit(trimws(Sys.getenv("RACE_DATASETS", + "Wortley2006 Giles2015 Zhu2013 Zanol2014")), "\\s+")[[1]] +seeds <- as.integer(strsplit(trimws(Sys.getenv("RACE_SEEDS", "1 2 3 4 5")), "\\s+")[[1]]) +wagSeed <- as.integer(Sys.getenv("RACE_WAGSEED", "11")) +nIter <- as.integer(Sys.getenv("RAT_ITER", "30")) + +# As GrepNum but takes the LAST match (the ratchet's rearrangement line, after +# any tread/check bookkeeping). +GrepNumLast <- function(out, pat) { + hit <- grep(pat, out, value = TRUE) + if (!length(hit)) return(NA_real_) + suppressWarnings(as.numeric(gsub(",", "", sub(pat, "\\1", hit[length(hit)])))) +} + +# TNT ratchet from a tread'd start: `ratchet=iter N` (pinned syntax). Reports +# best score (R-scored from saved trees), total rearrangements examined, wall. +TntRatchet <- function(d, startTree, seed, nIter, hold = 1000) { + script <- c("mxram 1024;", "taxname=;", "proc data.tnt;", + paste0("rseed ", seed, ";"), + paste0("hold ", hold, ";"), + paste0("tread ", ToTntTree(startTree), ";"), + paste0("ratchet=iter ", nIter, ";"), + "tsave *out.tre;", "save;", "tsave/;", "quit;") + wd <- file.path(tempdir(), paste0("tntrat", Sys.getpid())) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(d$phy, file.path(wd, "data.tnt")) + old <- setwd(wd); on.exit(setwd(old), add = TRUE) + .t0 <- Sys.time() + # STDIN pipe (not runfile-arg): headless 64-bit TNT on Hamilton launches the + # curses UI when handed a runfile arg and yields no parseable stdout. + out <- suppressWarnings(system2(TNT_EXE, input = script, stdout = TRUE, stderr = TRUE)) + .wall <- as.double(difftime(Sys.time(), .t0, units = "secs")) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + rearr <- GrepNumLast(out, ".*Total rearrangements examined:\\s*([0-9,]+).*") + trees <- tryCatch(ReadTntTree(file.path(wd, "out.tre")), error = function(e) NULL) + finalR <- if (is.null(trees)) NA_real_ else { + if (inherits(trees, "multiPhylo")) min(vapply(trees, TreeLength, double(1), d$phy)) + else TreeLength(trees, d$phy) + } + data.frame(engine = "TNT", seed = seed, final_len = finalR, + rearrangements = rearr, wall = .wall, stringsAsFactors = FALSE) +} + +# TS ratchet from the same start: ts_ratchet_search with production-like params +# (perturbProb 0.25, perturbMaxMoves 5, maxHits 1). +TsRatchet <- function(d, startTree, seed, nIter) { + edge <- PhyloToKernelEdge(startTree, d) + set.seed(seed) + .t0 <- Sys.time() + res <- TreeSearch:::ts_ratchet_search( + edge, d$contrast, d$tip_data, d$weight, d$levels, + nCycles = nIter, perturbProb = 0.25, maxHits = 1L, + perturbMode = 0L, perturbMaxMoves = 5L) + .wall <- as.double(difftime(Sys.time(), .t0, units = "secs")) + resTree <- structure(list(edge = res$edge, Nnode = d$nTip - 1L, + tip.label = names(d$phy)), class = "phylo") + data.frame(engine = "TS", seed = seed, + final_len = TreeLength(resTree, d$phy), + rearrangements = res$total_tbr_moves, wall = .wall, + stringsAsFactors = FALSE) +} + +cat(sprintf("RATCHET RACE | lib=%s | iter=%d | datasets {%s} | seeds {%s}\n", + Sys.getenv("TS_LIB"), nIter, paste(dsN, collapse = ","), + paste(seeds, collapse = ","))) + +allRows <- list() +for (nm in dsN) { + d <- prepareDataset(nm) + set.seed(wagSeed) + wag <- TreeSearch:::ts_random_wagner_tree(d$contrast, d$tip_data, d$weight, d$levels) + wagTree <- Preorder(RenumberTips(structure(list(edge = wag$edge, Nnode = d$nTip - 1L, + tip.label = names(d$phy)), class = "phylo"), names(d$phy))) + startLen <- TreeLength(wagTree, d$phy) + cat(sprintf("\n==== %s (%dt) Wagner(seed %d) start_len=%.0f ====\n", + nm, d$nTip, wagSeed, startLen)) + rows <- list() + for (s in seeds) { + rows[[length(rows) + 1]] <- cbind(dataset = nm, tips = d$nTip, + start_len = startLen, TntRatchet(d, wagTree, s, nIter)) + rows[[length(rows) + 1]] <- cbind(dataset = nm, tips = d$nTip, + start_len = startLen, TsRatchet(d, wagTree, s, nIter)) + } + dd <- do.call(rbind, rows) + allRows[[nm]] <- dd + print(dd[, c("engine", "seed", "final_len", "rearrangements", "wall")], row.names = FALSE) +} +res <- do.call(rbind, allRows) + +cat(sprintf("\n=== PER-DATASET MEDIAN (ratchet from shared Wagner start, %d iters) ===\n", nIter)) +agg <- do.call(rbind, lapply(split(res, res$dataset), function(z) { + tnt <- z[z$engine == "TNT", ]; ts <- z[z$engine == "TS", ] + data.frame(dataset = z$dataset[1], tips = z$tips[1], start_len = z$start_len[1], + ts_final = median(ts$final_len), tnt_final = median(tnt$final_len), + ts_rearr = round(median(ts$rearrangements)), tnt_rearr = round(median(tnt$rearrangements)), + rearr_ratio = round(median(ts$rearrangements) / median(tnt$rearrangements), 2), + ts_wall = round(median(ts$wall), 2), tnt_wall = round(median(tnt$wall), 2), + wall_ratio = round(median(ts$wall) / median(tnt$wall), 2), + stringsAsFactors = FALSE) +})) +agg <- agg[order(agg$tips), ] +print(agg, row.names = FALSE) +cat("\n--- READ ---\n") +cat("PRIMARY metrics = score@fixed-iters (ts_final vs tnt_final) + wall (64-bit authoritative).\n") +cat("UNIT CAVEAT: TS rearrangements = total_tbr_moves (APPLIED moves) is NOT commensurable with TNT\n") +cat("'Total rearrangements examined' (EXAMINED candidates) -> rearr_ratio is NOT an efficiency ratio.\n") +cat("A clean ratchet-efficiency race needs TS to expose examined-candidates (RatchetResult lacks it).\n") +cat("So this is a COARSE score@budget + wall probe (advisor: order-of-magnitude only).\n") +outCsv <- Sys.getenv("RACE_OUT", "ratchet_race.csv") +write.csv(res, outCsv, row.names = FALSE) +cat(sprintf("\nrows -> %s\n", outCsv)) diff --git a/dev/benchmarks/results_analysis.md b/dev/benchmarks/results_analysis.md new file mode 100644 index 000000000..6f7ab28be --- /dev/null +++ b/dev/benchmarks/results_analysis.md @@ -0,0 +1,52 @@ +# Benchmark Results Analysis (Agent A, T-005) + +## Dataset + +8 datasets × 6 strategies × 3 reps = 144 planned runs. +55/144 succeeded (38%) due to T-025 optimization-dependent UB segfault. +Aria2015 (35 tips) and Dikow2009 (88 tips) had highest crash rates. + +## Key Findings + +### 1. All strategies find optimal on small datasets (≤43 tips) +- Longrich (20 tips), Vinther (23 tips), Griswold (43 tips): 100% optimal +- Strategy choice doesn't matter much for small datasets + +### 2. Thorough and ratchet_heavy win on large datasets +- Zhu2013 (75 tips): `thorough` found best-known (649), `sprint` failed (652) +- Giles2015 (78 tips): `ratchet_heavy` found best (714), others 716-720 +- Dikow2009 (88 tips): `ratchet_heavy` and `drift_heavy` both found 1612 (vs best-known 1614) + +### 3. Sprint is fastest but loses quality at scale +- Sprint uses 3 ratchet cycles, no drift, minimal sectorial +- At ≤43 tips: optimal quality, 2-10× faster wall time +- At 75+ tips: fails to find optimal within 20s timeout + +### 4. Phase time distribution depends strongly on strategy +| Strategy | TBR | Ratchet | Drift | Sectorial | Fuse | +|----------|-----|---------|-------|-----------|------| +| sprint | 43% | 42% | 0% | 9% | 1% | +| default | 11% | 37% | 39% | 11% | 0% | +| ratchet_heavy | 6% | 87% | 5% | 1% | 0% | +| sectorial_heavy | 13% | 20% | 21% | 38% | 7% | +| drift_heavy | 7% | 12% | 74% | 4% | 3% | + +### 5. Replicates-to-convergence varies by strategy +- Sprint: 16-43 reps (many cheap reps) +- Thorough: 6-10 reps (few expensive reps) +- At 20s timeout, sprint completes 35-100 reps; thorough completes 6-10 + +## Recommendations for Adaptive Strategy + +1. **Size-based switching**: Use sprint for ≤30 tips, default for 30-60, + thorough or ratchet_heavy for 60+. +2. **Phase timing feedback**: If ratchet/drift phases dominate but scores + aren't improving, switch to more replicates with lighter per-replicate effort. +3. **Time budget**: With short timeouts, sprint covers more replicates. + With longer timeouts, thorough explores deeper per replicate. + +## Limitations + +- Only 38% of runs succeeded due to T-025 bug +- 20s timeout limits large-dataset exploration +- No IW or profile parsimony benchmarks (EW only) diff --git a/dev/benchmarks/results_drift_mpt_120s.csv b/dev/benchmarks/results_drift_mpt_120s.csv new file mode 100644 index 000000000..45b2d6b19 --- /dev/null +++ b/dev/benchmarks/results_drift_mpt_120s.csv @@ -0,0 +1,19 @@ +"dataset","n_tips","budget_s","drift_cycles","seed","best_score","n_trees","n_topologies","replicates","wall_s","drift_ms","total_ms","drift_pct","mean_rf","median_rf" +"Wortley2006",37,120,0,1,483,3,3,19,5.27,0,5258.8291,0,24,34 +"Wortley2006",37,120,0,2,482,1,1,22,7.56,0,7402.2598,0,NA,NA +"Wortley2006",37,120,0,3,484,74,74,15,4.84,0,3893.9118,0,21.7778600518327,24 +"Wortley2006",37,120,2,1,484,6,6,6,1.72,200.3704,1611.3042,12.4,4.4,4 +"Wortley2006",37,120,2,2,483,5,5,11,3.73,417.7614,3086.4603,13.5,9.2,14 +"Wortley2006",37,120,2,3,483,6,6,10,2.69,359.331,2666.7322,13.5,22.9333333333333,22 +"Zhu2013",75,120,0,1,638,100,100,20,24.86,0,24391.2249,0,9.42545454545455,10 +"Zhu2013",75,120,0,2,638,100,100,14,20.3,0,19788.4594,0,8.82141414141414,8 +"Zhu2013",75,120,0,3,639,100,100,8,8.38,0,8068.8115,0,8.38747474747475,8 +"Zhu2013",75,120,2,1,638,80,80,26,40.85,6750.1543,37658.7576,17.9,13.8329113924051,12 +"Zhu2013",75,120,2,2,638,100,100,26,32.33,5607.5592,32091.1807,17.5,9.45212121212121,8 +"Zhu2013",75,120,2,3,638,44,44,6,6.88,671.2499,4907.1112,13.7,16.8964059196617,6 +"Geisler2001",68,120,0,1,1298,100,100,9,6.54,0,6259.6277,0,9.44323232323232,8 +"Geisler2001",68,120,0,2,1295,100,100,19,17.65,0,17364.6849,0,8.51434343434343,8 +"Geisler2001",68,120,0,3,1296,100,100,16,14.7,0,14335.6088,0,9.81737373737374,8 +"Geisler2001",68,120,2,1,1296,100,100,8,7.83,1242.2633,7675.4627,16.2,8.44525252525253,8 +"Geisler2001",68,120,2,2,1295,100,100,22,29,5418.9427,28501.9577,19,8.02020202020202,8 +"Geisler2001",68,120,2,3,1297,100,100,10,9.32,1448.9778,9118.779,15.9,8.85777777777778,8 diff --git a/dev/benchmarks/results_drift_mpt_30s.csv b/dev/benchmarks/results_drift_mpt_30s.csv new file mode 100644 index 000000000..5dd8bcbbc --- /dev/null +++ b/dev/benchmarks/results_drift_mpt_30s.csv @@ -0,0 +1,19 @@ +"dataset","n_tips","budget_s","drift_cycles","seed","best_score","n_trees","n_topologies","replicates","wall_s","drift_ms","total_ms","drift_pct","mean_rf","median_rf" +"Wortley2006",37,30,0,1,483,3,3,12,2.33,0,2312.2469,0,25.3333333333333,30 +"Wortley2006",37,30,0,2,484,46,46,14,3.59,0,3293.46,0,21.5478260869565,24 +"Wortley2006",37,30,0,3,482,2,2,52,14.47,0,14469.3116,0,10,10 +"Wortley2006",37,30,2,1,484,5,5,19,5.24,763.1592,5231.0175,14.6,16.6,24 +"Wortley2006",37,30,2,2,482,3,3,47,15.44,2364.2745,15437.9649,15.3,8,10 +"Wortley2006",37,30,2,3,485,72,72,7,2.3,138.8317,1218.1715,11.4,12.5915492957746,8 +"Zhu2013",75,30,0,1,638,100,100,25,27.41,0,27001.8771,0,10.3668686868687,10 +"Zhu2013",75,30,0,2,639,100,100,17,27,0,26340.9309,0,11.9260606060606,10 +"Zhu2013",75,30,0,3,638,100,100,10,16.76,0,15910.4713,0,8.12848484848485,8 +"Zhu2013",75,30,2,1,639,24,24,11,28.41,4420.0451,27002.2213,16.4,6.55072463768116,6 +"Zhu2013",75,30,2,2,639,100,100,15,24.68,3575.011,24043.2459,14.9,10.3781818181818,6 +"Zhu2013",75,30,2,3,639,100,100,13,16.3,2397.1034,15842.008,15.1,10.2448484848485,8 +"Geisler2001",68,30,0,1,1295,100,100,26,26.64,0,26346.3173,0,7.65252525252525,8 +"Geisler2001",68,30,0,2,1295,100,100,13,10.97,0,10790.435,0,7.30060606060606,8 +"Geisler2001",68,30,0,3,1297,100,100,17,14.51,0,14270.2228,0,8.12929292929293,8 +"Geisler2001",68,30,2,1,1295,100,100,22,27.3,5069.8475,27009.3889,18.8,6.39555555555556,6 +"Geisler2001",68,30,2,2,1295,100,100,19,27.5,4803.201,27008.6251,17.8,7.92040404040404,8 +"Geisler2001",68,30,2,3,1295,100,100,16,27.29,5096.0644,26999.3252,18.9,6.85131313131313,6 diff --git a/dev/benchmarks/results_drift_mpt_30s_nostop.csv b/dev/benchmarks/results_drift_mpt_30s_nostop.csv new file mode 100644 index 000000000..d4c5886d0 --- /dev/null +++ b/dev/benchmarks/results_drift_mpt_30s_nostop.csv @@ -0,0 +1,19 @@ +"dataset","n_tips","budget_s","drift_cycles","seed","best_score","n_trees","n_topologies","replicates","wall_s","drift_ms","total_ms","drift_pct","mean_rf","median_rf" +"Wortley2006",37,30,0,1,482,4,4,74,27.03,0,26999.6914,0,18.3333333333333,19 +"Wortley2006",37,30,0,2,482,4,4,75,25.41,0,25392.1515,0,17.3333333333333,24 +"Wortley2006",37,30,0,3,482,4,4,79,27.01,0,26996.0762,0,17.3333333333333,24 +"Wortley2006",37,30,2,1,482,2,2,58,27.02,4054.7815,26998.224,15,2,2 +"Wortley2006",37,30,2,2,482,1,1,63,27,4105.1979,26999.9565,15.2,NA,NA +"Wortley2006",37,30,2,3,482,3,3,62,27,4293.3852,26999.6641,15.9,18,26 +"Zhu2013",75,30,0,1,638,100,100,26,27.2,0,26997.0477,0,26.9260606060606,37 +"Zhu2013",75,30,0,2,639,100,100,29,27.32,0,27003.2961,0,11.6056565656566,8 +"Zhu2013",75,30,0,3,638,47,47,21,30,0,26991.8496,0,5.80203515263645,6 +"Zhu2013",75,30,2,1,639,100,100,19,27.38,4862.418,27004.7242,18,10.1882828282828,8 +"Zhu2013",75,30,2,2,638,100,100,21,27.6,4386.1555,27003.6628,16.2,16.8638383838384,8 +"Zhu2013",75,30,2,3,638,100,100,19,27.36,4630.2351,27003.3931,17.1,8.0210101010101,8 +"Geisler2001",68,30,0,1,1295,100,100,27,27.5,0,27008.9305,0,8.39919191919192,8 +"Geisler2001",68,30,0,2,1295,100,100,26,27.52,0,27004.1687,0,7.28525252525252,8 +"Geisler2001",68,30,0,3,1295,100,100,28,27.4,0,27000.9511,0,6.9179797979798,6 +"Geisler2001",68,30,2,1,1295,100,100,23,27.24,5153.3365,27003.0785,19.1,7.29131313131313,8 +"Geisler2001",68,30,2,2,1295,100,100,25,27.44,4788.7047,27000.6298,17.7,7.45292929292929,8 +"Geisler2001",68,30,2,3,1295,100,100,25,27.28,4788.3551,27001.2777,17.7,7.41454545454545,8 diff --git a/dev/benchmarks/results_grid.csv b/dev/benchmarks/results_grid.csv new file mode 100644 index 000000000..5feb5baf9 --- /dev/null +++ b/dev/benchmarks/results_grid.csv @@ -0,0 +1,56 @@ +"dataset","strategy","seed","n_taxa","best_score","replicates","hits_to_best","pool_size","timed_out","wall_s","wagner_ms","tbr_ms","xss_ms","rss_ms","css_ms","ratchet_ms","drift_ms","final_tbr_ms","fuse_ms" +"Longrich2010","sprint",7156,20,131,16,10,10,FALSE,0.35,5.5612,168.9216,33.5792,0,0,136.0137,0,17.6695,4.3639 +"Longrich2010","default",7177,20,131,12,10,9,FALSE,0.79,3.5726,97.9913,73.731,19.9502,35.3677,281.7302,248.3973,11.462,4.9795 +"Longrich2010","thorough",7191,20,131,10,10,10,FALSE,1.43,10.8893,83.8347,107.9262,53.7111,69.9601,595.777,425.8947,14.7263,60.7966 +"Longrich2010","ratchet_heavy",7212,20,131,10,10,10,FALSE,0.87,2.9456,80.6327,16.5024,0,0,705.3218,54.506,10.1509,3.8626 +"Longrich2010","sectorial_heavy",7226,20,131,14,10,9,FALSE,0.92,4.3215,117.3891,207.5194,74.8896,95.5932,178.7793,162.2722,14.6907,68.3265 +"Longrich2010","sectorial_heavy",7240,20,131,12,10,9,FALSE,0.84,3.7353,112.879,216.303,65.1713,81.3997,134.3745,157.5637,12.6482,61.7056 +"Longrich2010","drift_heavy",7261,20,131,10,10,10,FALSE,1.22,3.6682,125.7203,53.9082,17.2822,0,162.2324,788.4821,14.5726,42.4944 +"Vinther2008","sprint",7282,23,79,43,10,9,FALSE,0.96,13.4144,305.9908,101.5532,0,0,469.8212,0,56.8494,12.0851 +"Vinther2008","default",7303,23,79,15,10,8,FALSE,1.29,4.4285,147.0399,120.6356,26.9947,49.2964,511.2222,386.3871,22.1471,9.2218 +"Vinther2008","ratchet_heavy",7338,23,79,13,10,9,FALSE,1.75,4.183,79.8243,29.7423,0,0,1518.565,93.1282,18.5196,7.6951 +"Vinther2008","ratchet_heavy",7345,23,79,12,10,6,FALSE,1.58,4.3809,88.3588,25.7994,0,0,1318.619,116.2221,23.6332,5.2536 +"Vinther2008","drift_heavy",7373,23,79,15,10,8,FALSE,1.89,4.4506,155.6572,68.4049,31.5383,0,287.7199,1229.335,23.0958,96.2598 +"Griswold1999","sprint",7527,43,407,100,1,1,FALSE,8.95,86.3985,3142.07,898.5605,0,0,4334.641,0,472.8122,17.423 +"Griswold1999","sprint",7534,43,407,100,1,1,FALSE,8.33,72.3996,3019.879,829.1007,0,0,3972.065,0,419.8558,5.3561 +"Griswold1999","default",7541,43,407,60,5,4,TRUE,20.01,51.3871,2028.548,1608.473,517.8287,646.6741,7913.14,6882.725,304.1913,54.717 +"Griswold1999","default",7555,43,407,52,4,3,TRUE,20.14,48.1219,1989.576,1628.201,551.7708,647.1327,8067.253,6908.469,285.1488,23.3511 +"Griswold1999","thorough",7569,43,407,26,7,4,TRUE,20.28,69.5974,883.0762,1322.144,665.1719,620.8039,9653.046,6315.852,128.0589,608.7557 +"Griswold1999","sectorial_heavy",7604,43,409,33,10,10,FALSE,10.99,25.9571,1050.931,2538.971,959.7211,1151.893,2304.769,2019.774,160.1463,778.1662 +"Griswold1999","sectorial_heavy",7611,43,407,59,4,2,TRUE,20.03,50.699,1937.855,4619.779,1815.873,2144.137,4311.689,3853.577,296.9927,1001.677 +"Griswold1999","sectorial_heavy",7618,43,408,54,5,5,TRUE,20,53.296,2058.464,4475.212,1792.645,2162.046,4179.014,3912.437,295.7644,1082.892 +"Griswold1999","drift_heavy",7632,43,407,29,1,1,TRUE,20.31,29.243,1345.69,789.8366,342.0521,0,2829.767,14532.42,180.9324,260.5181 +"Griswold1999","drift_heavy",7639,43,407,35,2,2,TRUE,20.4,28.5549,1216.641,692.5993,348.2278,0,2660.212,14800.87,181.3329,468.1162 +"Agnarsson2004","sprint",7646,62,778,25,12,4,FALSE,7.64,38.8319,3306.102,664.5492,0,0,3240.233,0,355.2857,36.4514 +"Agnarsson2004","sprint",7653,62,778,16,12,4,FALSE,4.84,24.0395,1993.6,434.3031,0,0,2112.969,0,248.6172,23.9967 +"Agnarsson2004","sprint",7660,62,778,28,12,4,FALSE,9.03,46.9422,4055.725,707.0973,0,0,3766.75,0,412.585,43.3126 +"Agnarsson2004","default",7681,62,778,13,12,4,FALSE,14.88,20.086,1636.263,1010.528,253.8344,416.1035,5540.763,5752.729,200.2798,46.9966 +"Agnarsson2004","thorough",7695,62,778,6,7,3,TRUE,20.52,38.019,978.6282,1304.621,495.5814,712.8048,9477.131,6990.946,106.7311,409.6798 +"Agnarsson2004","ratchet_heavy",7709,62,778,7,8,3,TRUE,20.57,17.9828,1367.128,284.0427,0,0,17716.98,1049.291,110.6061,16.2788 +"Agnarsson2004","ratchet_heavy",7716,62,778,8,9,4,TRUE,20.4,15.0823,1281.816,243.2763,0,0,17582.67,1088.114,167.5827,17.3994 +"Agnarsson2004","ratchet_heavy",7723,62,778,8,9,3,TRUE,20.52,13.2234,1090.884,264.887,0,0,18161.67,851.4318,118.6747,12.781 +"Agnarsson2004","sectorial_heavy",7730,62,778,14,12,4,FALSE,16.49,20.8428,2065.099,3273.523,1150.482,1610.231,3425.814,3582.109,234.0559,1118.618 +"Agnarsson2004","sectorial_heavy",7744,62,778,13,12,4,FALSE,15.71,21.851,2042.292,3228.474,1304.259,1763.248,2981.633,3185.584,208.5059,979.7699 +"Agnarsson2004","drift_heavy",7758,62,778,9,10,3,TRUE,20.03,14.879,1375.563,591.4157,212.5581,0,2459.886,14734.41,135.1524,506.9411 +"Zhu2013","sprint",7772,75,651,39,1,1,TRUE,20.02,72.9111,13737.71,1183.313,0,0,4580.738,0,440.0807,0 +"Zhu2013","sprint",7779,75,653,40,1,1,TRUE,20.06,75.0919,13781.53,1404.004,0,0,4354.869,0,446.4723,0 +"Zhu2013","default",7800,75,648,26,0,1,TRUE,20.28,68.5746,9550.869,1557.155,383.2678,647.4293,3937.54,3835.627,272.6035,38.4683 +"Zhu2013","default",7807,75,652,27,1,1,TRUE,20,43.4461,9116.992,1808.787,423.0211,706.6058,3813.093,3831.995,263.5211,0 +"Zhu2013","thorough",7821,75,644,10,1,1,TRUE,20.29,60.7229,3234.679,1154.143,452.1932,679.1188,6656.512,7966.357,97.8088,0 +"Zhu2013","ratchet_heavy",7835,75,647,10,2,2,TRUE,20.1,21.6947,4091.166,338.5121,0,0,13942.89,1542.329,162.059,0 +"Zhu2013","sectorial_heavy",7863,75,646,18,1,1,TRUE,20.34,30.4038,6684.249,3763.609,1155.983,1669.957,2394.022,4458.786,188.8624,0 +"Zhu2013","sectorial_heavy",7870,75,654,14,1,1,TRUE,20.13,28.2669,6638.929,3353.36,1181.068,1522.333,2687.835,4516.656,191.1578,0 +"Giles2015","sprint",7898,78,716,31,0,1,TRUE,20.02,67.1701,13977.79,905.5851,0,0,4606.327,0,431.2986,22.5306 +"Giles2015","sprint",7912,78,720,35,1,1,TRUE,20.08,66.9398,14428.12,932.5643,0,0,4150.108,0,463.9039,26.9601 +"Giles2015","default",7919,78,717,20,0,1,TRUE,20.1,40.6125,9100.392,1475.09,348.6732,593.0079,4175.914,4029.259,263.322,80.5659 +"Giles2015","default",7926,78,724,21,1,1,TRUE,20.03,38.5791,8895.455,1497.378,372.198,614.7384,4047.391,4313.701,255.574,0 +"Giles2015","default",7933,78,719,22,1,1,TRUE,20.19,39.7737,9231.238,1406.215,337.7801,601.8482,4118.125,4177.309,277.7193,0 +"Giles2015","thorough",7940,78,718,8,1,1,TRUE,20.47,41.1324,3145.117,865.2061,495.5709,516.7613,6827.516,8496.909,76.0479,0 +"Giles2015","ratchet_heavy",7975,78,714,11,2,2,TRUE,21.1,17.4333,3992.947,333.8161,0,0,14876.64,1729.617,113.2207,28.6116 +"Giles2015","sectorial_heavy",7982,78,719,17,1,1,TRUE,20.09,26.083,6699.72,3134.947,1041.038,1372.883,2912.329,4542.178,170.6497,199.7884 +"Giles2015","sectorial_heavy",7989,78,716,14,1,1,TRUE,20.19,30.4455,6425.667,3180.292,1006.807,1690.058,3088.124,4532.7,221.133,0 +"Giles2015","sectorial_heavy",7996,78,720,18,1,1,TRUE,20.34,41.4848,6531.305,2608.641,956.5212,1391.038,3147.44,5209.824,196.5575,274.792 +"Giles2015","drift_heavy",8017,78,716,7,1,1,TRUE,21.13,13.0351,3371.816,429.5716,149.9113,0,1706.396,15358.03,85.6689,0 +"Dikow2009","ratchet_heavy",8101,88,1612,3,1,1,TRUE,20.29,8.1673,1104.885,161.8035,0,0,17875.2,1046.379,78.6071,0 +"Dikow2009","sectorial_heavy",8108,88,1614,9,2,2,TRUE,20.16,26.449,3291.174,4484.637,1531.966,2196.717,3844.513,4295.55,233.0998,251.5741 +"Dikow2009","drift_heavy",8136,88,1612,4,1,1,TRUE,21.61,13.7694,1500.282,559.3438,242.1143,0,2671.889,16521.6,106.8854,0 diff --git a/dev/benchmarks/results_large_preset.csv b/dev/benchmarks/results_large_preset.csv new file mode 100644 index 000000000..7e8e1fbca --- /dev/null +++ b/dev/benchmarks/results_large_preset.csv @@ -0,0 +1,20 @@ +"condition","seed","best_score","replicates","budget_s","notes" +"large_v2",2847,1271,1,60,"T-179: lean_c design (ratch12,drift4,nniP0,outer1)" +"large_v2",7193,1255,1,60,"T-179: lean_c design" +"large_v2",4561,1237,1,60,"T-179: lean_c design" +"large_v2",1031,1219,1,60,"T-179: lean_c design (round 3 partial)" +"thorough",2847,1263,0,60,"source_large baseline (ratch20,drift12,nniP5,outer2,as=T)" +"thorough",7193,1247,0,60,"source_large baseline" +"thorough",4561,1257,0,60,"source_large baseline" +"large_v2",2847,1250,2,120,"lean_c at 120s budget" +"large_v2",7193,1243,2,120,"lean_c at 120s budget" +"large_v2",4561,1253,2,120,"lean_c at 120s budget" +"thorough",2847,1250,1,120,"thorough at 120s budget" +"thorough",7193,1233,0,120,"thorough at 120s budget" +"thorough",4561,1252,1,120,"thorough at 120s budget" +"large_v2",2847,1276,0,30,"lean_c at 30s budget" +"large_v2",7193,1274,0,30,"lean_c at 30s budget" +"large_v2",4561,1292,0,30,"lean_c at 30s budget" +"thorough",2847,1283,0,30,"thorough at 30s budget" +"thorough",7193,1277,0,30,"thorough at 30s budget" +"thorough",4561,1316,0,30,"thorough at 30s budget" diff --git a/dev/benchmarks/results_outer_cycles.csv b/dev/benchmarks/results_outer_cycles.csv new file mode 100644 index 000000000..0a584db5e --- /dev/null +++ b/dev/benchmarks/results_outer_cycles.csv @@ -0,0 +1,85 @@ +"dataset","condition","seed","n_taxa","best_score","replicates","hits_to_best","wall_s" +"Longrich2010","thorough_1",1031,20,131,6,6,1.14 +"Longrich2010","thorough_1",2847,20,131,8,8,1.43 +"Longrich2010","thorough_1",7193,20,131,8,8,1.01 +"Longrich2010","thorough_2",1031,20,131,6,6,0.91 +"Longrich2010","thorough_2",2847,20,131,7,7,1.25 +"Longrich2010","thorough_2",7193,20,131,8,8,1.15 +"Vinther2008","thorough_1",1031,23,79,7,5,1.72 +"Vinther2008","thorough_1",2847,23,79,8,7,2.11 +"Vinther2008","thorough_1",7193,23,79,5,5,1.46 +"Vinther2008","thorough_2",1031,23,79,5,5,1.48 +"Vinther2008","thorough_2",2847,23,79,6,4,1.7 +"Vinther2008","thorough_2",7193,23,79,5,5,1.44 +"Sansom2010","thorough_1",1031,23,189,10,7,2.05 +"Sansom2010","thorough_1",2847,23,189,10,9,1.84 +"Sansom2010","thorough_1",7193,23,189,7,4,1.33 +"Sansom2010","thorough_2",1031,23,189,11,7,2.3 +"Sansom2010","thorough_2",2847,23,189,12,9,2.62 +"Sansom2010","thorough_2",7193,23,189,14,9,3.11 +"DeAssis2011","thorough_1",1031,33,64,5,5,1.14 +"DeAssis2011","thorough_1",2847,33,64,8,8,1.47 +"DeAssis2011","thorough_1",7193,33,64,7,7,1.27 +"DeAssis2011","thorough_2",1031,33,64,6,6,1.17 +"DeAssis2011","thorough_2",2847,33,64,7,7,1.37 +"DeAssis2011","thorough_2",7193,33,64,5,5,1.04 +"Aria2015","thorough_1",1031,35,143,12,2,2.39 +"Aria2015","thorough_1",2847,35,143,9,2,2.5 +"Aria2015","thorough_1",7193,35,143,16,3,3.67 +"Aria2015","thorough_2",1031,35,143,8,4,2.73 +"Aria2015","thorough_2",2847,35,143,11,2,3.81 +"Aria2015","thorough_2",7193,35,143,19,4,3.8 +"Wortley2006","thorough_1",1031,37,490,46,2,19.19 +"Wortley2006","thorough_1",2847,37,490,17,2,7.7 +"Wortley2006","thorough_1",7193,37,487,43,1,20.02 +"Wortley2006","thorough_2",1031,37,490,6,2,3.48 +"Wortley2006","thorough_2",2847,37,488,12,1,6.86 +"Wortley2006","thorough_2",7193,37,487,37,1,20 +"Griswold1999","thorough_1",1031,43,407,23,5,18.72 +"Griswold1999","thorough_1",2847,43,407,9,3,6.44 +"Griswold1999","thorough_1",7193,43,407,21,4,13.83 +"Griswold1999","thorough_2",1031,43,407,10,2,7.62 +"Griswold1999","thorough_2",2847,43,407,11,2,8.30000000000001 +"Griswold1999","thorough_2",7193,43,407,14,3,10.22 +"Schulze2007","thorough_1",1031,52,164,10,2,3.85999999999999 +"Schulze2007","thorough_1",2847,52,164,15,4,5.31 +"Schulze2007","thorough_1",7193,52,164,12,2,4.05000000000001 +"Schulze2007","thorough_2",1031,52,164,12,3,4.78 +"Schulze2007","thorough_2",2847,52,164,27,2,11.98 +"Schulze2007","thorough_2",7193,52,164,16,2,7.05000000000001 +"Eklund2004","thorough_1",1031,54,441,18,3,18.39 +"Eklund2004","thorough_1",2847,54,440,9,2,10.26 +"Eklund2004","thorough_1",7193,54,441,12,5,15.88 +"Eklund2004","thorough_2",1031,54,441,9,3,12.64 +"Eklund2004","thorough_2",2847,54,441,16,1,20.02 +"Eklund2004","thorough_2",7193,54,441,7,2,10.35 +"Agnarsson2004","thorough_1",1031,62,778,7,7,18.6 +"Agnarsson2004","thorough_1",2847,62,778,7,7,17.06 +"Agnarsson2004","thorough_1",7193,62,778,6,6,16.92 +"Agnarsson2004","thorough_2",1031,62,778,6,7,20 +"Agnarsson2004","thorough_2",2847,62,778,5,5,15.8 +"Agnarsson2004","thorough_2",7193,62,778,6,6,16.81 +"Zanol2014","thorough_1",1031,74,1322,5,1,20.02 +"Zanol2014","thorough_1",2847,74,1326,5,1,20 +"Zanol2014","thorough_1",7193,74,1324,4,1,20 +"Zanol2014","thorough_2",1031,74,1321,5,1,20.03 +"Zanol2014","thorough_2",2847,74,1325,5,1,20.02 +"Zanol2014","thorough_2",7193,74,1322,5,1,20 +"Zhu2013","thorough_1",1031,75,641,8,1,20.01 +"Zhu2013","thorough_1",2847,75,642,7,1,20.02 +"Zhu2013","thorough_1",7193,75,645,7,1,20 +"Zhu2013","thorough_2",1031,75,643,7,2,20.01 +"Zhu2013","thorough_2",2847,75,643,7,1,20 +"Zhu2013","thorough_2",7193,75,646,6,1,20 +"Giles2015","thorough_1",1031,78,714,6,1,20.0200000000001 +"Giles2015","thorough_1",2847,78,713,6,2,20 +"Giles2015","thorough_1",7193,78,714,7,1,20 +"Giles2015","thorough_2",1031,78,712,6,2,20.01 +"Giles2015","thorough_2",2847,78,713,6,1,20.02 +"Giles2015","thorough_2",7193,78,717,5,2,20.02 +"Dikow2009","thorough_1",1031,88,1611,4,1,20.01 +"Dikow2009","thorough_1",2847,88,1611,3,1,20.03 +"Dikow2009","thorough_1",7193,88,1611,3,1,20 +"Dikow2009","thorough_2",1031,88,1615,3,1,20.0500000000001 +"Dikow2009","thorough_2",2847,88,1614,4,1,20.0899999999999 +"Dikow2009","thorough_2",7193,88,1612,4,2,20.0200000000001 diff --git a/dev/benchmarks/results_perturb_stop.csv b/dev/benchmarks/results_perturb_stop.csv new file mode 100644 index 000000000..8347e1e45 --- /dev/null +++ b/dev/benchmarks/results_perturb_stop.csv @@ -0,0 +1,79 @@ +"dataset","ntip","nchar","psf","rep","elapsed_s","best_score","n_replicates" +"Vinther2008",23,57,0,1,0.71,79,13 +"Vinther2008",23,57,0,2,0.78,79,16 +"Vinther2008",23,57,2,1,0.78,79,17 +"Vinther2008",23,57,2,2,0.77,79,12 +"Vinther2008",23,57,5,1,0.71,79,11 +"Vinther2008",23,57,5,2,0.79,79,11 +"Aria2015",35,50,0,1,1.48,143,37 +"Aria2015",35,50,0,2,1.72,143,46 +"Aria2015",35,50,2,1,1.69,143,42 +"Aria2015",35,50,2,2,1.33,143,36 +"Aria2015",35,50,5,1,1.27,143,35 +"Aria2015",35,50,5,2,1.73,143,49 +"Griswold1999",43,137,0,1,6.89,407,52 +"Griswold1999",43,137,0,2,7.07,407,51 +"Griswold1999",43,137,2,1,7.77,407,58 +"Griswold1999",43,137,2,2,6.7,407,54 +"Griswold1999",43,137,5,1,8.61,407,65 +"Griswold1999",43,137,5,2,7.96,407,58 +"Eklund2004",54,131,0,1,4.67,440,35 +"Eklund2004",54,131,0,2,6.09,440,49 +"Eklund2004",54,131,2,1,8.38,440,67 +"Eklund2004",54,131,2,2,5.54,440,43 +"Eklund2004",54,131,5,1,7.3,440,62 +"Eklund2004",54,131,5,2,4.27,440,35 +"Agnarsson2004",62,242,0,1,4.82,778,12 +"Agnarsson2004",62,242,0,2,5.58,778,12 +"Agnarsson2004",62,242,2,1,5.64,778,13 +"Agnarsson2004",62,242,2,2,5.47,778,14 +"Agnarsson2004",62,242,5,1,4.89,778,12 +"Agnarsson2004",62,242,5,2,4.41,778,12 +"Zhu2013",75,253,0,1,27.42,638,98 +"Zhu2013",75,253,0,2,27.33,638,92 +"Zhu2013",75,253,2,1,27.56,638,88 +"Zhu2013",75,253,2,2,27.44,638,100 +"Zhu2013",75,253,5,1,27.5,639,90 +"Zhu2013",75,253,5,2,27.33,639,98 +"Dikow2009",88,220,0,1,56.91,1611,69 +"Dikow2009",88,220,0,2,55.64,1612,58 +"Dikow2009",88,220,2,1,57.44,1611,57 +"Dikow2009",88,220,2,2,56.84,1611,62 +"Dikow2009",88,220,5,1,60.02,1611,76 +"Dikow2009",88,220,5,2,54.86,1611,91 +"project2086",91,453,0,1,54.98,2076,34 +"project2086",91,453,0,2,54.86,2078,30 +"project2086",91,453,2,1,55.41,2077,27 +"project2086",91,453,2,2,54.63,2077,26 +"project2086",91,453,5,1,55.48,2078,27 +"project2086",91,453,5,2,54.78,2079,29 +"project2769",102,219,0,1,55.51,2048,58 +"project2769",102,219,0,2,59.42,2051,49 +"project2769",102,219,2,1,55.54,2051,53 +"project2769",102,219,2,2,54.3,2050,58 +"project2769",102,219,5,1,54.89,2051,61 +"project2769",102,219,5,2,54.42,2052,67 +"project1013",112,174,0,1,55.94,1866,41 +"project1013",112,174,0,2,55.03,1865,40 +"project1013",112,174,2,1,55.14,1866,52 +"project1013",112,174,2,2,54.77,1867,47 +"project1013",112,174,5,1,54.95,1868,40 +"project1013",112,174,5,2,54.58,1868,46 +"project2286",134,232,0,1,54.36,620,35 +"project2286",134,232,0,2,54.51,621,35 +"project2286",134,232,2,1,54.46,621,34 +"project2286",134,232,2,2,54.72,622,36 +"project2286",134,232,5,1,54.5,620,38 +"project2286",134,232,5,2,54.39,620,35 +"project1024",163,156,0,1,81.36,637,79 +"project1024",163,156,0,2,81.19,637,82 +"project1024",163,156,2,1,81.37,637,100 +"project1024",163,156,2,2,81.35,637,89 +"project1024",163,156,5,1,81.33,637,89 +"project1024",163,156,5,2,81.56,637,84 +"project2477",213,387,0,1,89.62,2822,10 +"project2477",213,387,0,2,83.93,2823,8 +"project2477",213,387,2,1,85.44,2818,10 +"project2477",213,387,2,2,90.1,2827,8 +"project2477",213,387,5,1,89.82,2819,7 +"project2477",213,387,5,2,90.1,2839,7 diff --git a/dev/benchmarks/results_perturb_stop_v2.csv b/dev/benchmarks/results_perturb_stop_v2.csv new file mode 100644 index 000000000..20d7e2a63 --- /dev/null +++ b/dev/benchmarks/results_perturb_stop_v2.csv @@ -0,0 +1,61 @@ +"dataset","ntip","nchar","psf","rep","elapsed_s","best_score","n_replicates" +"Vinther2008",23,57,0,1,1.24,79,12 +"Vinther2008",23,57,0,2,1.37,79,11 +"Vinther2008",23,57,2,1,1.55,79,12 +"Vinther2008",23,57,2,2,1.2,79,12 +"Vinther2008",23,57,5,1,1.23,79,13 +"Vinther2008",23,57,5,2,1.43,79,16 +"Aria2015",35,50,0,1,2.31,143,37 +"Aria2015",35,50,0,2,1.75,143,35 +"Aria2015",35,50,2,1,4.19,143,75 +"Aria2015",35,50,2,2,2.53,143,46 +"Aria2015",35,50,5,1,2.06,143,43 +"Aria2015",35,50,5,2,4.02,143,77 +"Griswold1999",43,137,0,1,9.37,407,50 +"Griswold1999",43,137,0,2,6.33,407,32 +"Griswold1999",43,137,2,1,16.7,407,91 +"Griswold1999",43,137,2,2,11.46,407,60 +"Griswold1999",43,137,5,1,6.75,407,33 +"Griswold1999",43,137,5,2,19.52,407,94 +"Eklund2004",54,131,0,1,11.76,440,63 +"Eklund2004",54,131,0,2,8.18,440,48 +"Eklund2004",54,131,2,1,8.67,440,51 +"Eklund2004",54,131,2,2,8.2,440,50 +"Eklund2004",54,131,5,1,12.7,440,73 +"Eklund2004",54,131,5,2,5.88,440,34 +"Agnarsson2004",62,242,0,1,6.19,778,12 +"Agnarsson2004",62,242,0,2,6.69,778,13 +"Agnarsson2004",62,242,2,1,6.02,778,12 +"Agnarsson2004",62,242,2,2,6.07,778,12 +"Agnarsson2004",62,242,5,1,6.93,778,13 +"Agnarsson2004",62,242,5,2,6.14,778,12 +"Zhu2013",75,253,0,1,58.28,638,150 +"Zhu2013",75,253,0,2,58.97,638,150 +"Zhu2013",75,253,2,1,54.87,638,150 +"Zhu2013",75,253,2,2,49.67,638,150 +"Zhu2013",75,253,5,1,54,638,150 +"Zhu2013",75,253,5,2,52.19,638,150 +"Dikow2009",88,220,0,1,170.29,1611,200 +"Dikow2009",88,220,0,2,164.58,1611,200 +"Dikow2009",88,220,2,1,154.11,1611,200 +"Dikow2009",88,220,2,2,118.19,1611,178 +"Dikow2009",88,220,5,1,127.05,1611,200 +"Dikow2009",88,220,5,2,106.56,1611,200 +"project2086",91,453,0,1,221.56,2076,200 +"project2086",91,453,0,2,206.41,2077,200 +"project2086",91,453,2,1,202.11,2076,200 +"project2086",91,453,2,2,228.21,2075,200 +"project2086",91,453,5,1,241.21,2076,200 +"project2086",91,453,5,2,196.7,2076,200 +"project2769",102,219,0,1,104.66,2049,200 +"project2769",102,219,0,2,108.75,2049,200 +"project2769",102,219,2,1,118.24,2048,200 +"project2769",102,219,2,2,123.31,2048,200 +"project2769",102,219,5,1,165.88,2048,200 +"project2769",102,219,5,2,170.17,2050,200 +"project1013",112,174,0,1,253.52,1865,200 +"project1013",112,174,0,2,201.42,1864,200 +"project1013",112,174,2,1,180.31,1864,200 +"project1013",112,174,2,2,147.59,1867,200 +"project1013",112,174,5,1,151.11,1863,200 +"project1013",112,174,5,2,167.5,1863,200 diff --git a/dev/benchmarks/results_perturb_stop_v3.csv b/dev/benchmarks/results_perturb_stop_v3.csv new file mode 100644 index 000000000..9be19235e --- /dev/null +++ b/dev/benchmarks/results_perturb_stop_v3.csv @@ -0,0 +1,46 @@ +"dataset","ntip","nchar","psf","rep","elapsed_s","best_score","n_replicates","stop" +"Griswold1999",43,137,0,1,52.4,407,500,"maxReps" +"Griswold1999",43,137,0,2,57.8,407,500,"maxReps" +"Griswold1999",43,137,0,3,45.3,407,500,"maxReps" +"Griswold1999",43,137,2,1,7.2,407,89,"PSF/converged" +"Griswold1999",43,137,2,2,7.7,407,92,"PSF/converged" +"Griswold1999",43,137,2,3,7.7,407,93,"PSF/converged" +"Griswold1999",43,137,5,1,19.5,407,217,"PSF/converged" +"Griswold1999",43,137,5,2,24.5,407,230,"PSF/converged" +"Griswold1999",43,137,5,3,22.3,407,216,"PSF/converged" +"Eklund2004",54,131,0,1,45.1,440,500,"maxReps" +"Eklund2004",54,131,0,2,40.7,440,500,"maxReps" +"Eklund2004",54,131,0,3,40.2,440,500,"maxReps" +"Eklund2004",54,131,2,1,9.2,440,118,"PSF/converged" +"Eklund2004",54,131,2,2,8.7,440,117,"PSF/converged" +"Eklund2004",54,131,2,3,8.4,440,110,"PSF/converged" +"Eklund2004",54,131,5,1,21.6,440,274,"PSF/converged" +"Eklund2004",54,131,5,2,22.2,440,277,"PSF/converged" +"Eklund2004",54,131,5,3,22.4,440,288,"PSF/converged" +"Agnarsson2004",62,242,0,1,120.3,778,500,"maxReps" +"Agnarsson2004",62,242,0,2,122.2,778,500,"maxReps" +"Agnarsson2004",62,242,0,3,119.3,778,500,"maxReps" +"Agnarsson2004",62,242,2,1,27.8,778,125,"PSF/converged" +"Agnarsson2004",62,242,2,2,28.6,778,126,"PSF/converged" +"Agnarsson2004",62,242,2,3,30,778,126,"PSF/converged" +"Agnarsson2004",62,242,5,1,69.9,778,313,"PSF/converged" +"Agnarsson2004",62,242,5,2,69.6,778,313,"PSF/converged" +"Agnarsson2004",62,242,5,3,70.2,778,311,"PSF/converged" +"Zhu2013",75,253,0,1,94,638,500,"maxReps" +"Zhu2013",75,253,0,2,87,638,500,"maxReps" +"Zhu2013",75,253,0,3,86.7,638,500,"maxReps" +"Zhu2013",75,253,2,1,29.5,638,168,"PSF/converged" +"Zhu2013",75,253,2,2,35.6,638,194,"PSF/converged" +"Zhu2013",75,253,2,3,47,638,274,"PSF/converged" +"Zhu2013",75,253,5,1,69,638,394,"PSF/converged" +"Zhu2013",75,253,5,2,87.2,638,480,"PSF/converged" +"Zhu2013",75,253,5,3,77.4,638,411,"PSF/converged" +"Dikow2009",88,220,0,1,254.4,1611,500,"maxReps" +"Dikow2009",88,220,0,2,271.1,1611,483,"PSF/converged" +"Dikow2009",88,220,0,3,250.4,1611,500,"maxReps" +"Dikow2009",88,220,2,1,110,1611,181,"PSF/converged" +"Dikow2009",88,220,2,2,92.4,1611,184,"PSF/converged" +"Dikow2009",88,220,2,3,84.6,1611,180,"PSF/converged" +"Dikow2009",88,220,5,1,227.3,1611,455,"PSF/converged" +"Dikow2009",88,220,5,2,248.1,1611,442,"PSF/converged" +"Dikow2009",88,220,5,3,271.2,1611,394,"PSF/converged" diff --git a/dev/benchmarks/results_t274_nni_perturb.csv b/dev/benchmarks/results_t274_nni_perturb.csv new file mode 100644 index 000000000..b5a124045 --- /dev/null +++ b/dev/benchmarks/results_t274_nni_perturb.csv @@ -0,0 +1,121 @@ +"dataset","n_taxa","nni_cycles","seed","best_score","wall_s" +"Zhu2013",75,0,69788,645,2.06 +"Zhu2013",75,0,8923,638,10.82 +"Zhu2013",75,0,79376,640,2.42 +"Zhu2013",75,0,16815,643,2.8 +"Zhu2013",75,0,19686,639,2.9 +"Zhu2013",75,0,63005,642,4.28 +"Zhu2013",75,0,84922,640,2.58 +"Zhu2013",75,0,43596,640,3.17 +"Zhu2013",75,0,40810,644,1.78 +"Zhu2013",75,0,24478,641,2.1 +"Zhu2013",75,0,26571,638,3.06 +"Zhu2013",75,0,69494,639,2.53 +"Zhu2013",75,0,91340,639,2.24 +"Zhu2013",75,0,50693,640,1.45 +"Zhu2013",75,0,23811,645,1.67 +"Zhu2013",75,0,75529,640,2.05 +"Zhu2013",75,0,11851,644,1.55 +"Zhu2013",75,0,34949,638,2.04 +"Zhu2013",75,0,65380,639,2.44 +"Zhu2013",75,0,73338,641,1.75 +"Zhu2013",75,5,69788,645,2.19 +"Zhu2013",75,5,8923,638,8.53999999999999 +"Zhu2013",75,5,79376,640,3.61 +"Zhu2013",75,5,16815,643,3.93000000000001 +"Zhu2013",75,5,19686,641,3.92 +"Zhu2013",75,5,63005,642,3.89999999999999 +"Zhu2013",75,5,84922,638,3.22000000000001 +"Zhu2013",75,5,43596,640,4.14 +"Zhu2013",75,5,40810,641,2.73999999999999 +"Zhu2013",75,5,24478,639,4.36 +"Zhu2013",75,5,26571,638,7.09 +"Zhu2013",75,5,69494,640,3.92 +"Zhu2013",75,5,91340,638,3.86 +"Zhu2013",75,5,50693,638,4.34 +"Zhu2013",75,5,23811,645,2.91 +"Zhu2013",75,5,75529,639,4.33 +"Zhu2013",75,5,11851,644,2.19 +"Zhu2013",75,5,34949,640,2.99999999999999 +"Zhu2013",75,5,65380,640,2.98000000000002 +"Zhu2013",75,5,73338,641,2.01999999999998 +"Giles2015",78,0,69788,714,2.09 +"Giles2015",78,0,8923,711,2.05000000000001 +"Giles2015",78,0,79376,710,2.85999999999999 +"Giles2015",78,0,16815,712,1.98000000000002 +"Giles2015",78,0,19686,712,2.63 +"Giles2015",78,0,63005,710,1.97 +"Giles2015",78,0,84922,711,2.40000000000001 +"Giles2015",78,0,43596,710,2.16999999999999 +"Giles2015",78,0,40810,713,2.84999999999999 +"Giles2015",78,0,24478,713,1.94 +"Giles2015",78,0,26571,711,2.14000000000001 +"Giles2015",78,0,69494,712,1.88999999999999 +"Giles2015",78,0,91340,710,2.92000000000002 +"Giles2015",78,0,50693,712,1.91 +"Giles2015",78,0,23811,711,2.84999999999999 +"Giles2015",78,0,75529,712,3.08000000000001 +"Giles2015",78,0,11851,715,3.48999999999998 +"Giles2015",78,0,34949,713,2.04000000000002 +"Giles2015",78,0,65380,712,3.23999999999998 +"Giles2015",78,0,73338,712,2.26000000000002 +"Giles2015",78,5,69788,711,3.53999999999999 +"Giles2015",78,5,8923,711,3.59999999999999 +"Giles2015",78,5,79376,711,3.88 +"Giles2015",78,5,16815,712,2.95000000000002 +"Giles2015",78,5,19686,711,3.63999999999999 +"Giles2015",78,5,63005,710,2.75 +"Giles2015",78,5,84922,711,3.5 +"Giles2015",78,5,43596,710,2.5 +"Giles2015",78,5,40810,712,6.44 +"Giles2015",78,5,24478,710,4.63 +"Giles2015",78,5,26571,711,2.54000000000002 +"Giles2015",78,5,69494,714,3.06999999999999 +"Giles2015",78,5,91340,711,4.09999999999999 +"Giles2015",78,5,50693,712,2.69 +"Giles2015",78,5,23811,710,3.88 +"Giles2015",78,5,75529,713,3.06 +"Giles2015",78,5,11851,712,4.27000000000001 +"Giles2015",78,5,34949,711,3.41999999999999 +"Giles2015",78,5,65380,712,2.92000000000002 +"Giles2015",78,5,73338,710,2.86000000000001 +"Dikow2009",88,0,69788,1612,5.43999999999997 +"Dikow2009",88,0,8923,1615,4.20000000000005 +"Dikow2009",88,0,79376,1621,3.94 +"Dikow2009",88,0,16815,1620,4.15999999999997 +"Dikow2009",88,0,19686,1616,3.25999999999999 +"Dikow2009",88,0,63005,1616,3.36000000000001 +"Dikow2009",88,0,84922,1614,3.05000000000001 +"Dikow2009",88,0,43596,1611,5.62 +"Dikow2009",88,0,40810,1615,5.38 +"Dikow2009",88,0,24478,1611,7.53000000000003 +"Dikow2009",88,0,26571,1615,10.79 +"Dikow2009",88,0,69494,1611,4.05000000000001 +"Dikow2009",88,0,91340,1616,12.66 +"Dikow2009",88,0,50693,1617,3.16999999999996 +"Dikow2009",88,0,23811,1612,5.58000000000004 +"Dikow2009",88,0,75529,1611,4.35999999999996 +"Dikow2009",88,0,11851,1614,8.98000000000002 +"Dikow2009",88,0,34949,1613,3.86000000000001 +"Dikow2009",88,0,65380,1613,4.27999999999997 +"Dikow2009",88,0,73338,1613,3.77000000000004 +"Dikow2009",88,5,69788,1612,7.13999999999999 +"Dikow2009",88,5,8923,1612,7.13999999999999 +"Dikow2009",88,5,79376,1611,7.87 +"Dikow2009",88,5,16815,1611,7.22000000000003 +"Dikow2009",88,5,19686,1615,8.38 +"Dikow2009",88,5,63005,1611,8.92000000000002 +"Dikow2009",88,5,84922,1614,4.88 +"Dikow2009",88,5,43596,1614,5.52999999999997 +"Dikow2009",88,5,40810,1615,5.86000000000001 +"Dikow2009",88,5,24478,1611,10.22 +"Dikow2009",88,5,26571,1613,7.58000000000004 +"Dikow2009",88,5,69494,1611,6 +"Dikow2009",88,5,91340,1617,3.94999999999999 +"Dikow2009",88,5,50693,1612,4.17000000000002 +"Dikow2009",88,5,23811,1611,8.89999999999998 +"Dikow2009",88,5,75529,1611,8.75 +"Dikow2009",88,5,11851,1612,6.81999999999999 +"Dikow2009",88,5,34949,1612,3.65000000000003 +"Dikow2009",88,5,65380,1615,6.15999999999997 +"Dikow2009",88,5,73338,1613,3.97000000000003 diff --git a/dev/benchmarks/run_iw_tests.R b/dev/benchmarks/run_iw_tests.R new file mode 100644 index 000000000..266fb4f35 --- /dev/null +++ b/dev/benchmarks/run_iw_tests.R @@ -0,0 +1,25 @@ +# Run the IW-specific testthat files against a chosen lib (TS_LIB, default +# .agent-tbr), loading helper-*.R the way testthat does so the tests actually +# execute (test_file alone does not auto-source helpers). +lib <- normalizePath(Sys.getenv("TS_LIB", ".agent-tbr"), winslash = "/") +library(TreeSearch, lib.loc = lib) +library(testthat) +helpers <- list.files("tests/testthat", pattern = "^helper", full.names = TRUE) +for (h in helpers) sys.source(h, envir = globalenv()) + +files <- c("test-ts-iw.R", "test-iw-scoring.R", "test-ts-nni-iw-rescore.R", + "test-ts-xpiwe.R", "test-ts-iw-profile-red10.R") +tp <- 0; tf <- 0; te <- 0; ts <- 0 +for (f in files) { + p <- file.path("tests/testthat", f) + if (!file.exists(p)) { cat(sprintf("-- %-32s MISSING\n", f)); next } + r <- tryCatch(as.data.frame(test_file(p, reporter = "silent")), + error = function(e) { cat("FILE ERROR:", conditionMessage(e), "\n"); NULL }) + if (is.null(r)) next + pa <- sum(r$passed); fl <- sum(r$failed) + er <- sum(as.integer(r$error), na.rm = TRUE) + sk <- if ("skipped" %in% names(r)) sum(as.integer(r$skipped), na.rm = TRUE) else 0L + tp <- tp + pa; tf <- tf + fl; te <- te + er; ts <- ts + sk + cat(sprintf("-- %-32s passed=%d failed=%d error=%d skipped=%d\n", f, pa, fl, er, sk)) +} +cat(sprintf("\nIW TESTS TOTAL: passed=%d failed=%d error=%d skipped=%d\n", tp, tf, te, ts)) diff --git a/dev/benchmarks/run_tbr_tests.R b/dev/benchmarks/run_tbr_tests.R new file mode 100644 index 000000000..c28f477dd --- /dev/null +++ b/dev/benchmarks/run_tbr_tests.R @@ -0,0 +1,22 @@ +# Regression check: default path must be intact after the directional-vroot +# merge + the opt-in unrooted reroot mechanism (which defaults off). +suppressMessages({ + library(testthat) + library(TreeSearch, lib.loc = ".agent-tbr") +}) +files <- list.files("tests/testthat", pattern = "^test-", full.names = TRUE) +keep <- grepl("ts-tbr|ts-sector|ts-driven|ts-ratchet|ts-drift|ts-spr|SPR|wagner|MaximizeParsimony|SearchControl", + files, ignore.case = TRUE) +files <- files[keep] +fail <- 0L +for (f in files) { + cat("---", basename(f), "---\n") + r <- tryCatch(as.data.frame(test_file(f, reporter = "silent")), + error = function(e) { cat("ERROR:", conditionMessage(e), "\n"); NULL }) + if (!is.null(r)) { + nf <- sum(r$failed); ne <- sum(r$error %in% TRUE) + cat(sprintf(" passed=%d failed=%d error=%d\n", sum(r$passed), nf, ne)) + fail <- fail + nf + ne + } else fail <- fail + 1L +} +cat(sprintf("\nTOTAL failures/errors: %d\n", fail)) diff --git a/dev/benchmarks/run_tnt_scaling.R b/dev/benchmarks/run_tnt_scaling.R new file mode 100644 index 000000000..50c01e5db --- /dev/null +++ b/dev/benchmarks/run_tnt_scaling.R @@ -0,0 +1,27 @@ +setwd("C:/Users/pjjg18/GitHub/TreeSearch") +source("dev/benchmarks/bench_tnt_settings.R") + +# Quick smoke-test: project691 x sect+fuse x seed=1 +cat("--- Smoke test: project691 / sect+fuse / seed=1 ---\n") +info <- export_nexus_dataset("project691") +cat(sprintf("Exported: %dt %dc\n", info$ntip, info$nchar)) + +sc1 <- write_phase1_script("project691.tnt", seed = 1L, timeout_s = 60L) +r1 <- run_tnt(sc1, hard_timeout_s = 90L) +cat(sprintf("Phase1 seed=1: score=%g wall=%.1fs\n", r1$score, r1$wall_s)) + +if (!is.na(r1$score)) { + B <- r1$score + cfg <- CONFIGS[["sect+fuse"]] + sc2 <- write_survey_script("project691.tnt", cfg, B, seed = 1L, timeout_s = 60L) + r2 <- run_tnt(sc2, hard_timeout_s = 90L) + ttt <- parse_ttt(r2$raw, B) + reached <- isTRUE(!is.na(r2$score) && r2$score <= B + 1e-6) + ttb <- if (!is.na(ttt$ttb_s) && ttt$ttb_s > 0) ttt$ttb_s else r2$wall_s + cat(sprintf("Phase2 sect+fuse: score=%g reached=%s TTT=%.1fs\n", + r2$score, reached, ttb)) +} + +cat("\n--- Launching full scaling survey ---\n") +results <- tnt_scaling_full() +message("Scaling survey complete. ", nrow(results), " rows.") diff --git a/dev/benchmarks/run_tnt_survey.R b/dev/benchmarks/run_tnt_survey.R new file mode 100644 index 000000000..efe1e7160 --- /dev/null +++ b/dev/benchmarks/run_tnt_survey.R @@ -0,0 +1,4 @@ +setwd("C:/Users/pjjg18/GitHub/TreeSearch") +source("dev/benchmarks/bench_tnt_settings.R") +results <- tnt_settings_full() +message("Survey complete. ", nrow(results), " rows collected.") diff --git a/dev/benchmarks/smoke_baseline.csv b/dev/benchmarks/smoke_baseline.csv new file mode 100644 index 000000000..fe1b7047b --- /dev/null +++ b/dev/benchmarks/smoke_baseline.csv @@ -0,0 +1,4 @@ +"dataset","score","candidates" +"Longrich2010",131,289805 +"Vinther2008",78,1740544 +"DeAssis2011",64,729100 diff --git a/dev/benchmarks/strategies.md b/dev/benchmarks/strategies.md new file mode 100644 index 000000000..1a41780a9 --- /dev/null +++ b/dev/benchmarks/strategies.md @@ -0,0 +1,497 @@ +# Driven Search Strategy Space + +Last updated: 2026-03-17 + +This document defines all tunable parameters of the C++ driven search +engine (`MaximizeParsimony()`) and proposes named strategy presets for +benchmarking (Phase 6D) and adaptive search (Phase 6F). + +## Pipeline Overview + +Each replicate executes this fixed phase sequence: + +``` +Wagner → TBR → XSS → RSS → CSS → Ratchet → Drift → Final TBR +``` + +Phases may be skipped by setting their cycle/round counts to 0. +Sectorial phases (XSS, RSS, CSS) only run when the tree has +≥ 2 × `sectorMinSize` tips. + +Between replicates, the pool collects the best tree(s) and tree +fusing may run (every `fuseInterval` replicates). + +--- + +## Parameter Categories + +### A. Strategy Parameters (per-replicate search behavior) + +These control how each replicate explores tree space. They are the +primary targets for strategy tuning in Phase 6D. + +#### A1. Wagner Start + +| R parameter | C++ field | Default | Description | +|-------------|-----------|---------|-------------| +| `wagnerStarts` | `wagner_starts` | 1 | Random Wagner trees built per replicate; best-scoring one used as TBR starting point. Higher values improve starting topology at low cost for small datasets. | + +#### A2. TBR + +| R parameter | C++ field | Default | Description | +|-------------|-----------|---------|-------------| +| `tbrMaxHits` | `tbr_max_hits` | 1 | Equal-score hits before TBR declares convergence. Higher values explore the plateau more thoroughly. | +| `tabuSize` | `tabu_size` | 100 | Tabu list capacity for TBR. Prevents revisiting recently-explored topologies on plateaus. 0 = disabled. | + +#### A3. Ratchet + +| R parameter | C++ field | Default | Description | +|-------------|-----------|---------|-------------| +| `ratchetCycles` | `ratchet_cycles` | 10 | Perturbation-then-search cycles per replicate. Primary knob for ratchet intensity. 0 = skip ratchet. | +| `ratchetPerturbProb` | `ratchet_perturb_prob` | 0.04 | Per-character probability of perturbation. Higher = more disruptive. | +| `ratchetPerturbMode` | `ratchet_perturb_mode` | 0 | 0 = zero (silence characters), 1 = upweight (double weight), 2 = mixed (zero some, double others). | +| `ratchetPerturbMaxMoves` | `ratchet_perturb_max_moves` | 0 (auto) | Max TBR moves during perturbation phase. 0 = `max(20, min(200, n_tip/8))`. | +| `ratchetAdaptive` | `ratchet_adaptive` | FALSE | Auto-tune `perturbProb` to target a ~30% escape rate. | + +#### A4. Drift + +| R parameter | C++ field | Default | Description | +|-------------|-----------|---------|-------------| +| `driftCycles` | `drift_cycles` | 6 | Suboptimal-exploration cycles per replicate. 0 = skip drift. | +| `driftAfdLimit` | `drift_afd_limit` | 3 | Max absolute fit difference (steps) for accepting suboptimal moves. | +| `driftRfdLimit` | `drift_rfd_limit` | 0.1 | Max relative fit difference for accepting suboptimal moves. | + +#### A5. Sectorial Search + +| R parameter | C++ field | Default | Description | +|-------------|-----------|---------|-------------| +| `xssRounds` | `xss_rounds` | 3 | Exclusive Sectorial Search (systematic partition) rounds. 0 = skip XSS. | +| `xssPartitions` | `xss_partitions` | 4 | Number of non-overlapping sectors per XSS round. | +| `rssRounds` | `rss_rounds` | 1 | Random Sectorial Search rounds after XSS. 0 = skip RSS. | +| `cssRounds` | `css_rounds` | 1 | Constrained Sectorial Search (full-tree exact scoring) rounds. 0 = skip CSS. | +| `cssPartitions` | `css_partitions` | 4 | Partitions for CSS. | +| `sectorMinSize` | `sector_min_size` | 6 | Minimum sector clade size (tips). | +| `sectorMaxSize` | `sector_max_size` | 50 | Maximum sector clade size (tips). | + +#### A6. Tree Fusing + +| R parameter | C++ field | Default | Description | +|-------------|-----------|---------|-------------| +| `fuseInterval` | `fuse_interval` | 3 | Fuse best tree against pool every N replicates. | +| `fuseAcceptEqual` | `fuse_accept_equal` | FALSE | Accept equal-score fusions (increases pool diversity). | + +### B. Convergence Parameters (when to stop) + +These control total search effort across replicates. Independent of +per-replicate strategy — benchmarking should generally fix these. + +| R parameter | C++ field | Default | Description | +|-------------|-----------|---------|-------------| +| `maxReplicates` | `max_replicates` | 100 | Hard cap on replicates. | +| `targetHits` | `target_hits` | `max(10, n_tip/5)` | Stop after this many independent hits to the best score. | + +### C. Pool Parameters + +| R parameter | C++ field | Default | Description | +|-------------|-----------|---------|-------------| +| `poolMaxSize` | `pool_max_size` | 100 | Maximum trees retained in the pool. | +| `poolSuboptimal` | `pool_suboptimal` | 0.0 | Score tolerance for retaining suboptimal trees. | + +### D. Infrastructure Parameters (not strategy-relevant) + +| R parameter | C++ field | Default | Description | +|-------------|-----------|---------|-------------| +| `concavity` | — | Inf | Scoring mode: Inf = EW, finite = IW, "profile" = profile parsimony. | +| `nThreads` | — | 1 | Worker threads. | +| `verbosity` | `verbosity` | 1 | 0 = silent, 1 = per-replicate, 2 = per-phase. | +| `progressCallback` | — | NULL (auto) | Custom progress reporting function. | +| `constraint` | — | (none) | Topology constraint (splits). | +| — | `max_seconds` | 0 | Timeout in seconds (available in C++ bridge, not exposed in R-level `MaximizeParsimony`). | + +### E. Not Yet Implemented (noted in production plan) + +| Parameter | Description | Status | +|-----------|-------------|--------| +| SPR vs TBR phase choice | Use SPR first, escalate to TBR only where SPR plateaus | Not implemented (T-012) | +| NNI pre-pass | Quick NNI before TBR | Not implemented | + +--- + +## Strategy Vector + +For Phase 6D benchmarking, the **strategy vector** consists of the 20 +Category A parameters. Each preset specifies values for all 20. + +--- + +## Named Strategy Presets + +### 1. `sprint` + +Minimal effort for fast interactive exploration. Skips expensive phases. +Suitable as a quick-look default or for very small datasets where a +single TBR pass is often sufficient. + +``` +wagnerStarts = 1 +tbrMaxHits = 1 +tabuSize = 0 +ratchetCycles = 3 +ratchetPerturbProb = 0.04 +ratchetPerturbMode = 0 +ratchetPerturbMaxMoves = 0 +ratchetAdaptive = FALSE +driftCycles = 0 # skip drift +driftAfdLimit = 3 +driftRfdLimit = 0.1 +xssRounds = 1 +xssPartitions = 4 +rssRounds = 0 # skip RSS +cssRounds = 0 # skip CSS +cssPartitions = 4 +sectorMinSize = 6 +sectorMaxSize = 50 +fuseInterval = 5 +fuseAcceptEqual = FALSE +``` + +**Rationale**: 3 ratchet cycles (vs 10) provides some escape from local +optima without large time cost. No drift (most expensive phase per cycle). +Minimal sectorial (1 XSS round, no RSS/CSS). No tabu (saves memory and +TBR overhead for quick passes). + +### 2. `default` + +Current production defaults. Balanced for general use. + +``` +wagnerStarts = 1 +tbrMaxHits = 1 +tabuSize = 100 +ratchetCycles = 5 +ratchetPerturbProb = 0.04 +ratchetPerturbMode = 0 +ratchetPerturbMaxMoves = 0 +ratchetAdaptive = FALSE +driftCycles = 2 +driftAfdLimit = 3 +driftRfdLimit = 0.1 +xssRounds = 3 +xssPartitions = 4 +rssRounds = 1 +cssRounds = 0 +cssPartitions = 4 +sectorMinSize = 6 +sectorMaxSize = 50 +fuseInterval = 3 +fuseAcceptEqual = FALSE +``` + +### 3. `thorough` + +More exhaustive exploration. More cycles of everything, adaptive ratchet, +multiple Wagner starts, wider plateau exploration. + +``` +wagnerStarts = 3 +tbrMaxHits = 3 +tabuSize = 200 +ratchetCycles = 20 +ratchetPerturbProb = 0.04 +ratchetPerturbMode = 2 # mixed +ratchetPerturbMaxMoves = 0 +ratchetAdaptive = TRUE +driftCycles = 12 +driftAfdLimit = 5 +driftRfdLimit = 0.15 +xssRounds = 5 +xssPartitions = 6 +rssRounds = 3 +cssRounds = 2 +cssPartitions = 6 +sectorMinSize = 6 +sectorMaxSize = 80 +fuseInterval = 2 +fuseAcceptEqual = TRUE +``` + +**Rationale**: Doubles most cycle counts. Adaptive ratchet tunes perturbation +intensity automatically. Mixed perturbation mode (zero + upweight) provides +more diverse perturbation landscapes. More Wagner starts improve starting +point quality. Higher `tbrMaxHits` + `tabuSize` explore plateaus better. +`fuseAcceptEqual` increases pool diversity for fusing. + +### 4. `ratchet_heavy` + +Emphasize ratchet perturbation for escaping deep local optima. Useful +when the fitness landscape has many local optima separated by large +barriers (common in large datasets with many inapplicable characters). + +``` +wagnerStarts = 1 +tbrMaxHits = 1 +tabuSize = 100 +ratchetCycles = 30 +ratchetPerturbProb = 0.08 +ratchetPerturbMode = 2 # mixed +ratchetPerturbMaxMoves = 0 +ratchetAdaptive = TRUE +driftCycles = 2 # reduced +driftAfdLimit = 3 +driftRfdLimit = 0.1 +xssRounds = 1 # reduced +xssPartitions = 4 +rssRounds = 0 # skip +cssRounds = 0 # skip +cssPartitions = 4 +sectorMinSize = 6 +sectorMaxSize = 50 +fuseInterval = 3 +fuseAcceptEqual = FALSE +``` + +**Rationale**: 3× ratchet cycles, 2× perturbation probability, adaptive +tuning + mixed mode. Drift and sectorial reduced to leave time budget +for ratchet. Most time goes to perturbation-escape cycles. + +### 5. `sectorial_heavy` + +Emphasize sectorial search for large trees where full-tree TBR is +expensive. Decompose the problem into cheaper subproblems. + +``` +wagnerStarts = 1 +tbrMaxHits = 1 +tabuSize = 100 +ratchetCycles = 5 # reduced +ratchetPerturbProb = 0.04 +ratchetPerturbMode = 0 +ratchetPerturbMaxMoves = 0 +ratchetAdaptive = FALSE +driftCycles = 3 # reduced +driftAfdLimit = 3 +driftRfdLimit = 0.1 +xssRounds = 8 # increased +xssPartitions = 6 # more partitions +rssRounds = 4 # increased +cssRounds = 3 # increased +cssPartitions = 6 +sectorMinSize = 6 +sectorMaxSize = 80 # larger sectors +fuseInterval = 2 +fuseAcceptEqual = TRUE +``` + +**Rationale**: Heavy sectorial search (XSS + RSS + CSS) with more +partitions and larger max sector size. Ratchet and drift reduced. +For large trees (60+ tips), sectorial search per-step cost is lower +than full-tree TBR, so more sectorial rounds may yield better +time-to-optimal. + +### 6. `drift_heavy` + +Emphasize tree drifting for exploring the near-optimal landscape. +Useful when the fitness landscape has broad plateaus or many +near-optimal trees. + +``` +wagnerStarts = 1 +tbrMaxHits = 1 +tabuSize = 100 +ratchetCycles = 5 # reduced +ratchetPerturbProb = 0.04 +ratchetPerturbMode = 0 +ratchetPerturbMaxMoves = 0 +ratchetAdaptive = FALSE +driftCycles = 20 # increased +driftAfdLimit = 5 # wider +driftRfdLimit = 0.2 # wider +xssRounds = 2 # reduced +xssPartitions = 4 +rssRounds = 1 +cssRounds = 0 # skip +cssPartitions = 4 +sectorMinSize = 6 +sectorMaxSize = 50 +fuseInterval = 3 +fuseAcceptEqual = TRUE +``` + +**Rationale**: 3× drift cycles with relaxed acceptance criteria +(AFD 5, RFD 0.2) allow the search to wander farther from local +optima via incremental suboptimal moves. Ratchet and sectorial +reduced. `fuseAcceptEqual` helps propagate diverse drifted topologies. + +--- + +## Preset Summary Table + +| Preset | Wagner | TBR hits | Ratchet | Drift | XSS | RSS | CSS | Fuse int | +|--------|--------|----------|---------|-------|-----|-----|-----|----------| +| sprint | 1 | 1 | 3 cyc | off | 1 rnd | off | off | 5 | +| default | 1 | 1 | 10 cyc | 6 cyc | 3 rnd | 1 rnd | 1 rnd | 3 | +| thorough | 3 | 3 | 20 cyc adaptive | 12 cyc | 5 rnd | 3 rnd | 2 rnd | 2 | +| ratchet_heavy | 1 | 1 | 30 cyc adaptive | 2 cyc | 1 rnd | off | off | 3 | +| sectorial_heavy | 1 | 1 | 5 cyc | 3 cyc | 8 rnd | 4 rnd | 3 rnd | 2 | +| drift_heavy | 1 | 1 | 5 cyc | 20 cyc | 2 rnd | 1 rnd | off | 3 | + +--- + +## Usage in Benchmarking (Phase 6D) + +The benchmarking framework should: + +1. Fix convergence parameters (`maxReplicates`, `targetHits`) identically + across presets to make wall-clock comparisons fair. +2. For each benchmark dataset × preset combination, measure: + - Time to find the best-known score (from `datasets.md`) + - Total time for convergence or timeout + - Number of replicates to convergence + - Phase-level timing breakdown (from `timings` attribute) +3. The results matrix (datasets × presets → metrics) feeds Phase 6E + (predictive model) and Phase 6F (adaptive search). + +## Usage in Adaptive Search (Phase 6F) + +The warmup-then-switch approach: +1. Run 2–3 replicates with `default` preset while collecting phase timings. +2. Compute dataset features + phase yield metrics (e.g., "ratchet improved + score in 80% of cycles" → ratchet-heavy might help). +3. Select the best preset for remaining replicates. + +Alternatively, online adaptation could smoothly interpolate between presets +based on per-phase improvement rates. + +--- + +## R Helper Function + +The `dev/benchmarks/bench_datasets.R` benchmark utility can use a +`get_strategy(name)` helper. Example: + +```r +get_strategy <- function(name = c("sprint", "default", "thorough", + "ratchet_heavy", "sectorial_heavy", + "drift_heavy")) { + name <- match.arg(name) + strategies <- list( + sprint = list( + wagnerStarts = 1L, tbrMaxHits = 1L, tabuSize = 0L, + ratchetCycles = 3L, ratchetPerturbProb = 0.04, + ratchetPerturbMode = 0L, ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = FALSE, + driftCycles = 0L, driftAfdLimit = 3L, driftRfdLimit = 0.1, + xssRounds = 1L, xssPartitions = 4L, rssRounds = 0L, + cssRounds = 0L, cssPartitions = 4L, + sectorMinSize = 6L, sectorMaxSize = 50L, + fuseInterval = 5L, fuseAcceptEqual = FALSE + ), + default = list( + wagnerStarts = 1L, tbrMaxHits = 1L, tabuSize = 100L, + ratchetCycles = 5L, ratchetPerturbProb = 0.04, + ratchetPerturbMode = 0L, ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = FALSE, + driftCycles = 2L, driftAfdLimit = 3L, driftRfdLimit = 0.1, + xssRounds = 3L, xssPartitions = 4L, rssRounds = 1L, + cssRounds = 0L, cssPartitions = 4L, + sectorMinSize = 6L, sectorMaxSize = 50L, + fuseInterval = 3L, fuseAcceptEqual = FALSE + ), + thorough = list( + wagnerStarts = 3L, tbrMaxHits = 3L, tabuSize = 200L, + ratchetCycles = 20L, ratchetPerturbProb = 0.04, + ratchetPerturbMode = 2L, ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = TRUE, + driftCycles = 12L, driftAfdLimit = 5L, driftRfdLimit = 0.15, + xssRounds = 5L, xssPartitions = 6L, rssRounds = 3L, + cssRounds = 2L, cssPartitions = 6L, + sectorMinSize = 6L, sectorMaxSize = 80L, + fuseInterval = 2L, fuseAcceptEqual = TRUE + ), + ratchet_heavy = list( + wagnerStarts = 1L, tbrMaxHits = 1L, tabuSize = 100L, + ratchetCycles = 30L, ratchetPerturbProb = 0.08, + ratchetPerturbMode = 2L, ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = TRUE, + driftCycles = 2L, driftAfdLimit = 3L, driftRfdLimit = 0.1, + xssRounds = 1L, xssPartitions = 4L, rssRounds = 0L, + cssRounds = 0L, cssPartitions = 4L, + sectorMinSize = 6L, sectorMaxSize = 50L, + fuseInterval = 3L, fuseAcceptEqual = FALSE + ), + sectorial_heavy = list( + wagnerStarts = 1L, tbrMaxHits = 1L, tabuSize = 100L, + ratchetCycles = 5L, ratchetPerturbProb = 0.04, + ratchetPerturbMode = 0L, ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = FALSE, + driftCycles = 3L, driftAfdLimit = 3L, driftRfdLimit = 0.1, + xssRounds = 8L, xssPartitions = 6L, rssRounds = 4L, + cssRounds = 3L, cssPartitions = 6L, + sectorMinSize = 6L, sectorMaxSize = 80L, + fuseInterval = 2L, fuseAcceptEqual = TRUE + ), + drift_heavy = list( + wagnerStarts = 1L, tbrMaxHits = 1L, tabuSize = 100L, + ratchetCycles = 5L, ratchetPerturbProb = 0.04, + ratchetPerturbMode = 0L, ratchetPerturbMaxMoves = 0L, + ratchetAdaptive = FALSE, + driftCycles = 20L, driftAfdLimit = 5L, driftRfdLimit = 0.2, + xssRounds = 2L, xssPartitions = 4L, rssRounds = 1L, + cssRounds = 0L, cssPartitions = 4L, + sectorMinSize = 6L, sectorMaxSize = 50L, + fuseInterval = 3L, fuseAcceptEqual = TRUE + ) + ) + strategies[[name]] +} +``` + +This helper will be formalized in the benchmarking framework (T-004). + +--- + +## External Benchmark Datasets (MorphoBank corpus) + +### Train/validation split + +The `neotrans/inst/matrices/` directory contains ~800 MorphoBank phylogenetic +matrices. These supplement the 14 bundled datasets for broader, less +overfitting-prone benchmarking. + +**Split rule:** A matrix belongs to the **validation** set if its MorphoBank +project number is divisible by 5 (i.e., `project_id %% 5 == 0`); all others +are **training**. The 7 `syab*` files (non-MorphoBank) are always training. + +After filtering (ntax ≥ 20, parse OK, dedup): 535 training, 124 validation. + +**Usage rules:** +- **Training** matrices may be used freely during development and tuning. +- **Validation** matrices are a **one-way door**: run once to confirm that + improvements generalize. Results must **never** inform strategy tuning. +- If validation is ever used for tuning, the split is compromised and must + be rebuilt with a new rule. + +### Dedup + +Multi-file projects (same MorphoBank project, separate `.nex` files) often +contain the same character matrix with minor taxon-sampling variations. These +are flagged as `dedup_drop = TRUE` in the catalogue. The dedup uses pairwise +character identity ≥ 95% on shared taxa (requiring ≥ 80% taxon overlap), +keeping the largest matrix per redundancy cluster. + +24 near-duplicates are excluded, leaving 659 usable matrices. + +### Fixed 25-matrix training sample + +For routine benchmarking, a fixed sample of 25 matrices is used +(`MBANK_FIXED_SAMPLE` in `bench_datasets.R`). Selected via max-min distance +on standardized (ntax, nchar, pct_missing, pct_inapp) within each tier: + +| Tier | Count | Keys | +|------|-------|------| +| Small (20–30) | 7 | project532, project2346, project2451, project4501, project944, project971_(1), project2762 | +| Medium (31–60) | 7 | project826, project561, project571, project4146_(3), project3688, project4049, project423 | +| Large (61–120) | 7 | project4286, project4359, project4397, project2084_(1), project2771, project2184, project3938 | +| XLarge (121+) | 4 | syab07201, project4133, project804, project4284 | + +**Do not modify this list.** Benchmark comparisons require the same sample. diff --git a/dev/benchmarks/stress_large_findings.md b/dev/benchmarks/stress_large_findings.md new file mode 100644 index 000000000..23d5dcdb0 --- /dev/null +++ b/dev/benchmarks/stress_large_findings.md @@ -0,0 +1,83 @@ +# T-069 Stress Test Findings — 150–225 taxa +Agent F, 2026-03-18 + +## Datasets + +| File | Taxa | Chars | NA blocks | Inapplicable | +|------|------|-------|-----------|--------------| +| project175.nex | 165 | 71 | 2 | 0% | +| project3763.nex | 205 | 103 | 3 | 50.1% | +| syab07204.nex | 225 | 748 | 12 | 25.1% | + +## Key Findings + +### 1. Scaling exponents (synthetic series, n=20–225) + +| Metric | Exponent | Expected | +|--------|----------|---------| +| `n_candidates` | **n^2.86** | O(n^2) = 2.0 | +| `indirect_us` | **n^2.73** | — | +| `clip_incr_us` | **n^1.50** | — | + +Candidate count scales slightly super-quadratically (larger pruned subtrees give more valid regraft positions). Indirect scoring tracks candidates closely. Clip/incremental is sub-linear relative to candidates — incremental state amortises well. + +Both exponents are consistent with the existing AGENTS.md note (~n^2.8 TBR cost). + +### 2. NA block count drives per-candidate cost + +| Dataset | n_tips | n_blocks | ns/candidate | +|---------|--------|----------|--------------| +| project175 | 165 | 2 | 12.6 ns | +| project3763 | 205 | 3 | 19.2 ns | +| syab07204 | 225 | 12 | **57.5 ns** | + +syab07204's 12 NA character blocks cause ~4.6× higher per-candidate cost than the 2-block case, and 3× higher than 3-block. The NA three-pass scoring cost is proportional to n_blocks, not just n_tips. This is a real bottleneck for large, character-rich matrices with many inapplicable characters. + +The existing baseline in AGENTS.md (`~23 ns at 75 tips`) was measured on small inapplicable.phyData sets. Large real matrices with many NA blocks can be 2–3× slower per candidate. + +### 3. TBR fraction surpasses ratchet+drift at 200+ taxa + +| Dataset | TBR% | Ratchet% | Drift% | +|---------|------|----------|--------| +| project175 (165t, thorough) | 17% | 38% | 42% | +| project3763 (205t, default) | **57%** | 13% | 28% | +| syab07204 (225t, default) | **49%** | 13% | 27% | + +At ≤100 taxa, ratchet+drift dominate (~65–70%). At 200+ taxa, TBR itself becomes the largest single cost (49–57%). This crossover happens around 150–175 taxa. The phase distribution shift is driven by the super-quadratic TBR cost overwhelming the approximately-linear perturbation overhead. + +### 4. Pool collapse at large n with many characters + +syab07204 (225t, 748 chars) produced pool sizes of **8 and 2** from 2 replicates (2 reps each, nThreads=2). In contrast, project3763 (205t, 103 chars) filled the 100-tree pool even from 2 reps. + +The near-empty pool for syab07204 means: +- Tree fusing has almost no material to work with +- MPT enumeration from the pool will be from very few seeds +- Users may get poor solutions without many more replicates + +This is expected behaviour (each TBR pass takes ~150ms, so a 2-rep run completes very few TBR iterations), but it highlights that **recommended replicates should scale with taxa × chars**. At 225t / 748 chars, users need 10–20+ replicates for reliable results. + +### 5. Score variability at large n + +| Dataset | Score seed1 | Score seed2 | Δ | +|---------|------------|------------|---| +| project175 | 419 | 424 | 5 (1.2%) | +| project3763 | 1643 | 1513 | 130 (7.9%) | +| syab07204 | 11785 | 11933 | 148 (1.3%) | + +project3763 shows high variability (7.9%) despite only 205 taxa — likely because the 50% inapplicable data creates a very complex landscape. High inapplicable fractions interact with the NA three-pass to create many near-equal plateau trees. + +### 6. Memory (snapshot bytes per TBR pass) + +| Dataset | Snapshot KB | +|---------|------------| +| project175 (165t, 2 blocks) | 66.8 KB | +| project3763 (205t, 3 blocks) | 290.8 KB | +| syab07204 (225t, 12 blocks) | **547.2 KB** | + +Snapshot memory is manageable (well under 1 MB per pass), but the 547 KB for syab07204 means that with nThreads=2 each thread carries ~1 MB of snapshot state. Not a memory problem, but cache pressure contributes to the elevated per-candidate cost. + +## Suggested Follow-up Tasks + +- **T-073 (potential)**: Benchmark per-candidate cost as a function of `n_blocks` (hold n_tips fixed). Determine whether there's a block-count threshold beyond which a different NA scoring strategy would help. +- **T-074 (potential)**: Auto-scale `maxReplicates` recommendation in `SearchControl()` based on n_tips × n_chars × n_blocks. +- Revisit `thorough` strategy for large char-dense matrices: at 225t/748 chars, the ratchet+drift overhead is proportionally small (40%), so increasing ratchet/drift cycles is cheap relative to per-pass TBR cost. diff --git a/dev/benchmarks/stress_large_results.csv b/dev/benchmarks/stress_large_results.csv new file mode 100644 index 000000000..cbfee5f20 --- /dev/null +++ b/dev/benchmarks/stress_large_results.csv @@ -0,0 +1,4 @@ +"file","n_tips","n_chars","strategy","score1","score2","time1","time2","pool1","reps1" +"project175.nex",165,71,"thorough",419,424,1.86,1.85,100,1 +"project3763.nex",205,103,"default",1643,1513,14.58,17.88,100,1 +"syab07204.nex",225,748,"default",11785,11933,41.83,35.32,8,1 diff --git a/dev/benchmarks/t0/Giles2015.phy.rds b/dev/benchmarks/t0/Giles2015.phy.rds new file mode 100644 index 000000000..fb23043c8 Binary files /dev/null and b/dev/benchmarks/t0/Giles2015.phy.rds differ diff --git a/dev/benchmarks/t0/Giles2015.tre b/dev/benchmarks/t0/Giles2015.tre new file mode 100644 index 000000000..b155a9b20 --- /dev/null +++ b/dev/benchmarks/t0/Giles2015.tre @@ -0,0 +1 @@ +((((((Pterichthyodes,Bothriolepis),Parayunnanolepis),Yunnanolepis),((((Macropetalichthys,Lunaspis),((Rhamphodopsis,Campbellodus),Austroptyctodus)),(((Jagorina,Gemuendina),(((((Eurycaraspis,Cowralepis),((Incisoscutum,Coccosteus),Buchanosteus)),Dicksonosteus),Kujdanowiaspis),(((((((((((((Porolepis,Glyptolepis),(Gogonasus,Eusthenopteron)),Styloichthys),Onychodus),Psarolepis),Guiyu),((((Moythomasia,Kentuckia),Mimipiscis),Howqualepis),Cheirolepis)),Ligulalepis),Dialipina),Ramirosuarezia),Janusiscus),(((((Parexus,Brachyacanthus),Climatius),Ptomacanthus),V_waynensis),((((((Obtusacanthus,Lupopsyrus),Gyracanthides),(((((Tamiobatis,Orthacanthus),(((Onychoselache,Hamiltonichthys),Tristychius),(((Debeerius,Chondrenchelys),Helodus),((Cobelodus,Akmonistion),Cladoselache)))),Cladodoides),Doliodus),Pucapampella)),((((Gladiobranchus,Diplacanthus),(((Poracanthodes,Ischnacanthus),((((Promesacanthus,Mesacanthus),Cassidiceps),((Homalacanthus,Acanthodes),Cheiracanthus)),Euthacanthus)),Latviacanthus)),Culmacanthus),Tetanopsyrus)),Kathemacanthus),Brochoadmones))),Entelognathus))),Romundina)),Brindabellaspis)),Osteostraci),Galeaspida); diff --git a/dev/benchmarks/t0/Wortley2006.phy.rds b/dev/benchmarks/t0/Wortley2006.phy.rds new file mode 100644 index 000000000..c67a810dd Binary files /dev/null and b/dev/benchmarks/t0/Wortley2006.phy.rds differ diff --git a/dev/benchmarks/t0/Wortley2006.tre b/dev/benchmarks/t0/Wortley2006.tre new file mode 100644 index 000000000..487a0e94f --- /dev/null +++ b/dev/benchmarks/t0/Wortley2006.tre @@ -0,0 +1 @@ +((((((((Thomandersia_laurifolia,Thomandersia_hensii),Synapsis),Schlegelia),Scrophularia),Halleria),((((Proboscidea,Martynia),Paulownia),((((((Veronica,Ligustrum),((Nicotiana,Borago),Gentiana)),Retzia),Callicarpa),Bartsia),((((Sesamum,Ceratotheca),((Kigelia,Catalpa),Jacaranda)),Petrea),(((Streptocarpus,Nematanthus),(((Hemimeris,Calceolaria),Jovellana),Elytraria)),(((Verbena,Lamium),Stachytarpheta),(Thunbergia,Barleria)))))),Lindenbergia)),Mimulus),Angelonia); diff --git a/dev/benchmarks/t0/Zanol2014.phy.rds b/dev/benchmarks/t0/Zanol2014.phy.rds new file mode 100644 index 000000000..af56dca91 Binary files /dev/null and b/dev/benchmarks/t0/Zanol2014.phy.rds differ diff --git a/dev/benchmarks/t0/Zanol2014.tre b/dev/benchmarks/t0/Zanol2014.tre new file mode 100644 index 000000000..1eeb7f23b --- /dev/null +++ b/dev/benchmarks/t0/Zanol2014.tre @@ -0,0 +1 @@ +((((((Lysidice_unicornis,Lysidice_ninetta),(Lysidice_sp2,Lysidice_collaris)),Lysidice_sp1),(((Nicidion_cincta,Nicidion_cariboea),(((Nicidion_mutilata,Nicidion_amoureuxi),(((((Palola_viridis,Palola_sp_A7Pohnpei142),(((Palola_sp_B7,Palola_sp_B1),(Palola_sp_A9Kosrae161,Palola_sp_A3)),((Palola_sp_B5,Palola_siciliensis),Palola_sp_A1))),(((Euniphysa_tridontesa,Euniphysa_aculeata),Eunice_impexa),(Eunice_sp,Eunice_filamentosa))),(((((((((((Leodice_harassii,Leodice_antarctica),(((Leodice_limosa,Leodice_americana),Leodice_rubra),Leodice_miurai)),Leodice_antennata),Leodice_lucei),Leodice_antillensis),Leodice_valens),Leodice_torquata),(Leodice_marcusi,Eunice_fucata)),Leodice_thomasiana),Eunice_norvegica),((Eunice_cf_violacemaculata,Eunice_aphroditois),Eunice_roussaei))),(((Nidicion_notata,Nicidion_angeli),Nicidion_hentscheli),((((((((Marphysa_novahollandiae,Marphysa_mossambica),Marphysa_sanguinea),Marphysa_viridis),(Marphysa_californica,Marphysa_brevitentaculata)),(Marphysa_disjuncta,Marphysa_bellii)),((((Onuphis_iridescens,Onuphis_elegans),((((Paradiopatra_quadricuspis,Hyalinoecia_sp),Mooreonuphis_pallidula),Onuphis_eremita),Diopatra_dentata)),Diopatra_ornata),((((Glycera_dibranchiata,Dorvillea_erucaeformis),Dorvillea_sociabilis),((Lumbrineris_latreille,Lumbrineris_inflata),(Oenone_fulgida,Arabella_semimaculata))),Paramphinome_jeffreysii))),Marphysa_fallax),Marphysa_regalis)))),Nicidion_mikeli)),Nicidion_insularis)),Fauchaldius_cyrtauloni),Aciculomarphysa_comes); diff --git a/dev/benchmarks/t0/Zhu2013.phy.rds b/dev/benchmarks/t0/Zhu2013.phy.rds new file mode 100644 index 000000000..85cb664ce Binary files /dev/null and b/dev/benchmarks/t0/Zhu2013.phy.rds differ diff --git a/dev/benchmarks/t0/Zhu2013.tre b/dev/benchmarks/t0/Zhu2013.tre new file mode 100644 index 000000000..b43b651b5 --- /dev/null +++ b/dev/benchmarks/t0/Zhu2013.tre @@ -0,0 +1 @@ +(((((Pterichthyodes,Bothriolepis),Parayunnanolepis),((Macropetalichthys,Brindabellaspis),(((((Coccosteus,Buchanosteus),Dicksonosteus),Cowralepis),((Rhamphodopsis,Austroptyctodus),Campbellodus)),((((((((((Osorioichthys,Ligulalepis),((Moythomasia,Mimipiscis),Howqualepis)),Cheirolepis),Dialipina),Meemannia),(((((((Gogonasus,Eusthenopteron),Osteolepis),Kenichthys),(((Youngolepis,Diabolepis),Powichthys),Porolepis)),Styloichthys),(((Psarolepis,Achoania),Guiyu),Onychodus)),Miguashaia)),Lophosteus),Entelognathus),(((((Rhadinacanthus,Gladiobranchus),Diplacanthus),Tetanopsyrus),(((((((Parexus,Brachyacanthus),Climatius),Ptomacanthus),Vernicomacanthus),Brochoadmones),Kathemacanthus),(((Lupopsyrus,Obtusacanthus),((Debeerius,Chondrenchelys),(((Onychoselache,Hamiltonichthys),Tristychius),((Pucapampella,Cladoselache),((((Orthacanthus,Cladodoides),Tamiobatis),Doliodus),(Cobelodus,Akmonistion)))))),(((((Promesacanthus,Mesacanthus),(((Poracanthodes,Ischnacanthus),Euthacanthus),Cassidiceps)),Cheiracanthus),Homalacanthus),Acanthodes)))),Culmacanthus)),Sigaspis)))),Osteostraci),Galeaspida); diff --git a/dev/benchmarks/t249_round3_120s_20260326_1439.csv b/dev/benchmarks/t249_round3_120s_20260326_1439.csv new file mode 100644 index 000000000..5b3649307 --- /dev/null +++ b/dev/benchmarks/t249_round3_120s_20260326_1439.csv @@ -0,0 +1,49 @@ +"dataset","n_tips","n_chars","timeout_s","seed","score","n_trees","replicates","hits","wall_s","tnt_best","gap" +"Wortley2006",37,105,120,1,484,49,13,2,24.7,479,5 +"Wortley2006",37,105,120,2,482,1,87,2,48.8,479,3 +"Wortley2006",37,105,120,3,484,15,11,4,5.8,479,5 +"Eklund2004",54,131,120,1,440,100,11,5,9.1,440,0 +"Eklund2004",54,131,120,2,440,100,10,4,15.3,440,0 +"Eklund2004",54,131,120,3,440,100,12,4,6.8,440,0 +"Rougier2012",58,314,120,1,1149,90,8,6,14.1,1147,2 +"Rougier2012",58,314,120,2,1149,78,7,7,10.4,1147,2 +"Rougier2012",58,314,120,3,1149,90,6,3,10.2,1147,2 +"Shultz2007",59,195,120,1,434,100,11,9,8.8,431,3 +"Shultz2007",59,195,120,2,434,100,7,7,7.8,431,3 +"Shultz2007",59,195,120,3,434,100,9,8,9,431,3 +"Wilson2003",61,161,120,1,879,48,14,8,19.5,860,19 +"Wilson2003",61,161,120,2,879,24,13,5,20.5,860,19 +"Wilson2003",61,161,120,3,879,36,10,4,6.4,860,19 +"OMeara2014",63,315,120,1,1215,15,11,2,14.6,1208,7 +"OMeara2014",63,315,120,2,1215,13,13,2,19.5,1208,7 +"OMeara2014",63,315,120,3,1215,9,13,3,22.8,1208,7 +"Wetterer2000",63,145,120,1,559,84,8,2,7.4,549,10 +"Wetterer2000",63,145,120,2,559,100,19,4,18.6,549,10 +"Wetterer2000",63,145,120,3,559,95,13,5,11.8,549,10 +"Conrad2008",64,360,120,1,1761,100,16,4,22.1,1725,36 +"Conrad2008",64,360,120,2,1761,100,20,9,22.9,1725,36 +"Conrad2008",64,360,120,3,1761,100,16,6,28.3,1725,36 +"Capa2011",67,124,120,1,385,100,10,10,15.1,381,4 +"Capa2011",67,124,120,2,385,100,9,8,13.9,381,4 +"Capa2011",67,124,120,3,385,100,12,11,24.7,381,4 +"Geisler2001",68,185,120,1,1295,100,27,2,60.6,1293,2 +"Geisler2001",68,185,120,2,1295,100,12,3,22.9,1293,2 +"Geisler2001",68,185,120,3,1295,100,8,2,37.1,1293,2 +"Liljeblad2008",68,299,120,1,2868,1,17,1,108.2,2840,28 +"Liljeblad2008",68,299,120,2,2869,2,18,2,101.6,2840,29 +"Liljeblad2008",68,299,120,3,2868,2,14,2,76.5,2840,28 +"Zanol2014",74,210,120,1,1315,12,14,1,108.9,1261,54 +"Zanol2014",74,210,120,2,1314,4,17,2,109.1,1261,53 +"Zanol2014",74,210,120,3,1319,15,7,1,110.3,1261,58 +"Zhu2013",75,253,120,1,638,100,14,4,82.6,624,14 +"Zhu2013",75,253,120,2,638,100,28,2,109.5,624,14 +"Zhu2013",75,253,120,3,639,100,6,2,12.8,624,15 +"Aguado2009",76,102,120,1,578,100,39,3,75.6,575,3 +"Aguado2009",76,102,120,2,580,100,10,4,20.8,575,5 +"Aguado2009",76,102,120,3,578,100,20,2,38.8,575,3 +"Giles2015",78,236,120,1,711,100,7,5,15.6,670,41 +"Giles2015",78,236,120,2,711,100,6,2,12.8,670,41 +"Giles2015",78,236,120,3,710,100,18,4,40.8,670,40 +"Dikow2009",88,204,120,1,1611,60,5,2,54.3,1606,5 +"Dikow2009",88,204,120,2,1611,62,10,5,67.3,1606,5 +"Dikow2009",88,204,120,3,1611,16,12,1,109.9,1606,5 diff --git a/dev/benchmarks/t249_round3_30s_20260326_1408.csv b/dev/benchmarks/t249_round3_30s_20260326_1408.csv new file mode 100644 index 000000000..5dce74797 --- /dev/null +++ b/dev/benchmarks/t249_round3_30s_20260326_1408.csv @@ -0,0 +1,49 @@ +"dataset","n_tips","n_chars","timeout_s","seed","score","n_trees","replicates","hits","wall_s","tnt_best","gap" +"Wortley2006",37,105,30,1,484,49,13,2,3.1,479,5 +"Wortley2006",37,105,30,2,482,1,87,2,21.6,479,3 +"Wortley2006",37,105,30,3,484,15,11,4,1.8,479,5 +"Eklund2004",54,131,30,1,440,100,11,5,2.9,440,0 +"Eklund2004",54,131,30,2,440,100,10,4,2.9,440,0 +"Eklund2004",54,131,30,3,440,100,12,4,3.2,440,0 +"Rougier2012",58,314,30,1,1149,90,8,6,7.4,1147,2 +"Rougier2012",58,314,30,2,1149,78,7,7,6.8,1147,2 +"Rougier2012",58,314,30,3,1149,90,6,3,23.6,1147,2 +"Shultz2007",59,195,30,1,434,100,11,9,18.2,431,3 +"Shultz2007",59,195,30,2,434,100,7,7,12,431,3 +"Shultz2007",59,195,30,3,434,100,9,8,5.4,431,3 +"Wilson2003",61,161,30,1,879,48,14,8,23.5,860,19 +"Wilson2003",61,161,30,2,879,12,6,1,28.9,860,19 +"Wilson2003",61,161,30,3,879,36,8,4,30.1,860,19 +"OMeara2014",63,315,30,1,1221,1,5,1,27.1,1208,13 +"OMeara2014",63,315,30,2,1216,33,4,1,30.1,1208,8 +"OMeara2014",63,315,30,3,1215,2,3,1,27.1,1208,7 +"Wetterer2000",63,145,30,1,559,84,6,2,30.1,549,10 +"Wetterer2000",63,145,30,2,559,100,18,4,29.2,549,10 +"Wetterer2000",63,145,30,3,559,64,7,4,30.3,549,10 +"Conrad2008",64,360,30,1,1762,10,4,3,29,1725,37 +"Conrad2008",64,360,30,2,1761,72,4,1,30.2,1725,36 +"Conrad2008",64,360,30,3,1761,100,4,1,28.5,1725,36 +"Capa2011",67,124,30,1,385,100,3,4,28.3,381,4 +"Capa2011",67,124,30,2,385,100,3,3,27.7,381,4 +"Capa2011",67,124,30,3,385,100,1,2,28.5,381,4 +"Geisler2001",68,185,30,1,1300,100,1,2,28.8,1293,7 +"Geisler2001",68,185,30,2,1295,100,7,2,28,1293,2 +"Geisler2001",68,185,30,3,1295,100,8,2,17.4,1293,2 +"Liljeblad2008",68,299,30,1,2871,1,2,1,27.1,2840,31 +"Liljeblad2008",68,299,30,2,2873,3,1,1,27.4,2840,33 +"Liljeblad2008",68,299,30,3,2872,9,1,1,28.4,2840,32 +"Zanol2014",74,210,30,1,1316,100,1,1,28.4,1261,55 +"Zanol2014",74,210,30,2,1319,100,1,1,28,1261,58 +"Zanol2014",74,210,30,3,1319,15,1,1,29.8,1261,58 +"Zhu2013",75,253,30,1,638,100,5,1,29.8,624,14 +"Zhu2013",75,253,30,2,638,27,3,1,30.1,624,14 +"Zhu2013",75,253,30,3,639,100,2,1,28,624,15 +"Aguado2009",76,102,30,1,579,100,3,1,28.1,575,4 +"Aguado2009",76,102,30,2,580,100,5,3,27.3,575,5 +"Aguado2009",76,102,30,3,578,100,13,1,27.9,575,3 +"Giles2015",78,236,30,1,711,100,7,5,22.3,670,41 +"Giles2015",78,236,30,2,711,100,6,2,14.6,670,41 +"Giles2015",78,236,30,3,711,100,3,1,30.2,670,41 +"Dikow2009",88,204,30,1,1611,30,0,1,30.1,1606,5 +"Dikow2009",88,204,30,2,1611,16,0,1,30.2,1606,5 +"Dikow2009",88,204,30,3,1613,32,0,1,30.1,1606,7 diff --git a/dev/benchmarks/t252_hamilton.sh b/dev/benchmarks/t252_hamilton.sh new file mode 100644 index 000000000..2413bc62f --- /dev/null +++ b/dev/benchmarks/t252_hamilton.sh @@ -0,0 +1,70 @@ +#!/bin/bash +#SBATCH --job-name=t252-mbank +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=8G +#SBATCH --time=8:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t252_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t252_%j.err + +# T-252: MorphoBank training-set baseline benchmark +# 25 matrices x 3 budgets (30/60/120s) x 5 seeds = 375 runs +# Estimated: ~5 hours + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t252_results + +mkdir -p "$LIB" +mkdir -p "$OUTDIR" +mkdir -p /nobackup/$USER/TreeSearch/logs + +echo "=== T-252 MorphoBank Training-Set Benchmark ===" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "" + +# Build and install from latest cpp-search +cd "$REPO" || exit 1 +git pull --ff-only origin cpp-search 2>/dev/null || true +echo "Git HEAD: $(git log --oneline -1)" +echo "" + +rm -f src/*.o src/*.so +R CMD build --no-build-vignettes --no-manual --no-resave-data . +R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz +rc=$? +echo "Install exit code: $rc" +rm -f TreeSearch_*.tar.gz + +if [ $rc -ne 0 ]; then + echo "FATAL: install failed" + exit 1 +fi + +# Verify neotrans corpus is available +NEOTRANS=/nobackup/$USER/neotrans/inst/matrices +if [ ! -d "$NEOTRANS" ]; then + echo "FATAL: neotrans matrices not found at $NEOTRANS" + echo "Clone with: cd /nobackup/$USER && git clone " + exit 1 +fi +echo "Neotrans matrices: $(ls $NEOTRANS | wc -l) files" +echo "" + +# Run benchmark +cd "$REPO" +export R_LIBS_USER="$LIB" +Rscript dev/benchmarks/bench_t252_mbank_training.R "$OUTDIR" 2>&1 + +echo "" +echo "Completed: $(date)" +echo "Results in: $OUTDIR" +ls -la "$OUTDIR"/t252_*.csv 2>/dev/null diff --git a/dev/benchmarks/t252_mbank_120s_20260327_1317.csv b/dev/benchmarks/t252_mbank_120s_20260327_1317.csv new file mode 100644 index 000000000..b6c1174a7 --- /dev/null +++ b/dev/benchmarks/t252_mbank_120s_20260327_1317.csv @@ -0,0 +1,126 @@ +"dataset","strategy","replicate","seed","n_taxa","best_score","replicates","hits_to_best","pool_size","timed_out","wall_s","time_to_best_s","wagner_ms","tbr_ms","xss_ms","rss_ms","css_ms","ratchet_ms","drift_ms","final_tbr_ms","fuse_ms","budget_s","source" +"project532","default",1,3847,21,1139,7,5,4,FALSE,0.597999999999956,0.174283273,148.503215,14.327887,24.690415,17.636236,0,303.184831,62.553178,12.901541,2.343453,120,"mbank_training" +"project532","default",2,3848,21,1139,6,4,3,FALSE,0.597999999999956,0.123916778,121.97986,33.485211,22.381566,18.933991,0,320.406746,55.284499,12.856154,1.757961,120,"mbank_training" +"project532","default",3,3849,21,1139,6,5,4,FALSE,0.579999999999927,0.139597412,132.649526,22.214793,26.952043,15.646982,0,300.59517,53.942751,12.362817,1.812183,120,"mbank_training" +"project532","default",4,3850,21,1139,7,3,3,FALSE,0.750999999999294,0.197873706,141.871555,42.623983,26.681093,20.747777,0,423.874721,68.928898,15.491427,0.949228,120,"mbank_training" +"project532","default",5,3851,21,1139,6,3,2,FALSE,0.592999999999847,0.129039236,125.361359,30.279261,26.760433,19.136244,0,319.94664,49.821137,13.127436,2.038109,120,"mbank_training" +"project2346","default",1,3847,23,316,18,2,4,FALSE,0.829999999999927,0.608430951,71.029184,35.473094,32.825955,19.374163,0,519.261742,131.533754,13.02882,0.765322,120,"mbank_training" +"project2346","default",2,3848,23,318,8,2,20,FALSE,0.304000000000087,0.117708173,33.520126,9.281411,16.761981,7.827445,0,158.257356,36.152855,5.366508,0.311627,120,"mbank_training" +"project2346","default",3,3849,23,320,12,2,4,FALSE,0.46599999999944,0.124930457,48.132288,18.466171,27.800715,12.278035,0,276.894882,67.959639,8.181343,0.703515,120,"mbank_training" +"project2346","default",4,3850,23,317,17,2,13,FALSE,0.795000000000073,0.344254344,68.922419,28.551914,29.73234,17.731987,0,483.753854,123.08423,12.051296,0.341163,120,"mbank_training" +"project2346","default",5,3851,23,314,78,1,4,FALSE,3.67699999999968,1.435090677,304.810463,132.69613,141.883327,73.51865,0,2356.842515,605.479236,51.597378,1.447145,120,"mbank_training" +"project2451","default",1,3847,24,735,12,2,4,FALSE,0.427999999999884,0.221843663,66.205828,13.396343,18.763832,10.832563,0,227.105905,79.846971,6.955762,0.330172,120,"mbank_training" +"project2451","default",2,3848,24,732,67,1,1,FALSE,3.22999999999956,0.823948679,346.979458,82.764533,119.244198,52.943228,0,1902.570635,683.326202,38.894812,0,120,"mbank_training" +"project2451","default",3,3849,24,731,7,2,6,FALSE,0.244999999999891,0.092966703,41.354672,8.912597,12.506295,4.7361,0,123.690091,42.28259,4.282265,0.193916,120,"mbank_training" +"project2451","default",4,3850,24,731,74,1,3,FALSE,3.45600000000013,1.132576511,383.106166,87.869532,126.007845,56.212429,0,2044.100132,703.889984,42.021767,1.663113,120,"mbank_training" +"project2451","default",5,3851,24,730,56,1,2,FALSE,2.67200000000048,0.277758048,280.567088,85.41411,100.316807,46.314025,0,1563.836515,557.562805,33.588465,0,120,"mbank_training" +"project4501","default",1,3847,24,118,11,4,63,FALSE,0.449999999999818,0.049543826,20.627493,11.198614,11.815055,7.776755,0,138.45961,22.195465,5.140892,14.184838,120,"mbank_training" +"project4501","default",2,3848,24,118,8,6,93,FALSE,0.360999999999876,0.02131701,15.809006,7.847642,11.572414,6.5989,0,69.68908,14.638501,3.76466,0.836356,120,"mbank_training" +"project4501","default",3,3849,24,118,11,8,63,FALSE,0.387000000000626,0.025869516,17.988182,13.159864,11.846536,7.174633,0,103.145235,18.32269,5.53868,1.248622,120,"mbank_training" +"project4501","default",4,3850,24,118,11,6,63,FALSE,0.428999999999178,0.05354915,18.767008,14.497546,12.386349,6.770313,0,134.033431,20.862917,5.271509,0.836095,120,"mbank_training" +"project4501","default",5,3851,24,118,9,4,93,FALSE,0.377000000000407,0.018355794,15.849192,12.337627,7.080306,5.553369,0,101.60287,17.539517,3.805357,0.62106,120,"mbank_training" +"project944","default",1,3847,25,128,7,7,60,FALSE,0.349999999999454,0.018372965,13.749598,8.665922,6.577338,4.388015,0,50.700024,9.316056,2.852453,0.884296,120,"mbank_training" +"project944","default",2,3848,25,128,6,6,60,FALSE,0.305999999999585,0.026536602,16.762202,6.242929,6.315995,4.568325,0,59.400532,10.678432,3.202963,1.232052,120,"mbank_training" +"project944","default",3,3849,25,128,6,6,60,FALSE,0.353000000000065,0.023519519,12.339231,7.414637,6.222741,4.003219,0,49.831448,10.604764,2.731975,1.047022,120,"mbank_training" +"project944","default",4,3850,25,128,8,8,60,FALSE,0.373000000000502,0.018053484,16.38878,8.637668,6.467942,5.269536,0,61.244825,13.507091,3.300045,0.928819,120,"mbank_training" +"project944","default",5,3851,25,128,9,9,60,FALSE,0.289999999999964,0.019552678,18.095653,12.151427,7.457316,5.676282,0,68.236955,11.678938,3.661214,1.465672,120,"mbank_training" +"project971_(1)","default",1,3847,26,157,7,5,100,FALSE,0.199999999999818,0.034288362,19.540965,8.624554,13.504326,6.719836,0,112.82168,17.875589,5.259305,0.877813,120,"mbank_training" +"project971_(1)","default",2,3848,26,157,7,3,100,FALSE,0.216000000000349,0.03481252,16.035452,7.674916,8.319572,7.100072,0,124.029589,18.743483,5.235702,0.826979,120,"mbank_training" +"project971_(1)","default",3,3849,26,157,14,9,100,FALSE,0.42200000000048,0.07753632,34.000391,20.956551,31.22286,14.428183,0,240.754111,33.668735,10.389131,1.483253,120,"mbank_training" +"project971_(1)","default",4,3850,26,157,9,5,100,FALSE,0.268000000000029,0.034529356,20.521152,11.128321,12.392041,9.051989,0,167.722094,26.988611,6.584964,0.792242,120,"mbank_training" +"project971_(1)","default",5,3851,26,157,8,5,100,FALSE,0.269000000000233,0.033144458,22.034311,10.560942,19.018461,9.771033,0,142.472617,25.789647,7.242129,1.522097,120,"mbank_training" +"project2762","default",1,3847,29,259,9,5,100,FALSE,0.552000000000589,0.076698963,102.552619,31.991234,32.017848,18.777667,0,255.924423,60.232298,12.068471,3.722681,120,"mbank_training" +"project2762","default",2,3848,29,259,9,5,100,FALSE,0.559000000000196,0.124296864,110.021915,23.549355,22.25614,22.257053,0,265.42186,58.979949,11.944967,3.162605,120,"mbank_training" +"project2762","default",3,3849,29,259,14,7,100,FALSE,0.896999999999935,0.271071243,182.034881,44.101888,34.36301,33.517622,0,439.83955,83.552751,18.319235,3.683558,120,"mbank_training" +"project2762","default",4,3850,29,259,7,2,100,FALSE,0.552000000000589,0.072439469,103.33374,22.591772,28.818399,19.640743,0,260.223182,55.45098,10.463058,0.79617,120,"mbank_training" +"project2762","default",5,3851,29,259,9,2,100,FALSE,0.640000000000327,0.196973069,111.81434,30.079626,29.628034,21.70403,0,335.326449,74.873774,12.244683,1.187246,120,"mbank_training" +"project826","default",1,3847,33,431,10,5,100,FALSE,0.731000000000677,0.076276196,121.741229,108.597777,43.75939,21.104991,0,309.097116,60.566575,12.983497,2.531488,120,"mbank_training" +"project826","default",2,3848,33,431,14,8,100,FALSE,0.932999999999993,0.072963527,190.042457,114.346752,47.944522,34.721809,0,421.781289,70.243541,16.782441,3.219924,120,"mbank_training" +"project826","default",3,3849,33,431,9,5,100,FALSE,0.604999999999563,0.14725746,118.219918,80.220355,40.605209,16.564079,0,268.776122,41.222762,11.147686,2.454232,120,"mbank_training" +"project826","default",4,3850,33,431,13,7,100,FALSE,0.806999999999789,0.069525721,173.788642,93.184784,51.796127,23.401488,0,341.117535,62.517173,15.345382,4.454339,120,"mbank_training" +"project826","default",5,3851,33,431,10,7,100,FALSE,0.626999999999498,0.132048574,127.710431,85.490192,32.522076,19.169466,0,269.264299,52.301932,12.045337,2.735471,120,"mbank_training" +"project561","default",1,3847,34,1169,5,4,2,FALSE,0.766999999999825,0.200921647,104.203848,28.18669,31.8137,34.553072,0,434.189226,104.452797,18.555571,1.495537,120,"mbank_training" +"project561","default",2,3848,34,1169,10,8,2,FALSE,1.57900000000063,0.235814397,184.091378,57.688625,102.624785,76.210602,0,897.399635,209.365029,40.577297,1.872447,120,"mbank_training" +"project561","default",3,3849,34,1169,6,4,2,FALSE,1.03800000000047,0.215172027,112.760827,32.396947,72.031643,37.331976,0,617.902089,130.587572,22.017981,3.256114,120,"mbank_training" +"project561","default",4,3850,34,1169,7,6,2,FALSE,1.09799999999996,0.255119559,141.429342,58.494966,51.773246,51.428364,0,604.051173,149.422167,28.95167,1.973197,120,"mbank_training" +"project561","default",5,3851,34,1169,13,10,2,FALSE,1.8779999999997,0.227611827,254.913113,90.500066,104.908815,86.761706,0,1057.262797,223.664192,49.85961,0,120,"mbank_training" +"project571","default",1,3847,42,634,8,3,12,FALSE,0.894999999999527,0.199396634,135.515565,66.212952,42.147855,27.872039,0,445.262381,81.952351,16.597809,1.538257,120,"mbank_training" +"project571","default",2,3848,42,635,7,6,28,FALSE,0.715000000000146,0.099789584,112.405676,51.24409,31.78723,22.885687,0,258.887033,54.158859,11.817098,2.125303,120,"mbank_training" +"project571","default",3,3849,42,634,14,5,12,FALSE,1.57200000000012,0.225831785,228.209175,86.461079,80.530839,48.792823,0,858.462695,170.886235,26.904718,3.584772,120,"mbank_training" +"project571","default",4,3850,42,634,6,3,12,FALSE,0.603000000000065,0.137255932,117.591152,49.656388,31.269116,20.712042,0,260.278734,57.636379,11.03258,2.350998,120,"mbank_training" +"project571","default",5,3851,42,634,7,4,12,FALSE,0.618999999999687,0.103445409,115.114959,37.63192,24.581629,23.032833,0,293.901454,58.725278,11.897841,2.274955,120,"mbank_training" +"project4146_(3)","default",1,3847,59,260,88,2,100,FALSE,34.116,1.413533364,2671.985035,1807.940198,1630.073094,728.712218,0,19646.876166,7051.046702,440.119674,2.154328,120,"mbank_training" +"project4146_(3)","default",2,3848,59,262,16,2,100,FALSE,4.8080000000009,3.259231522,437.061873,332.329626,260.812709,136.426056,0,2658.735654,754.976978,76.161495,3.929552,120,"mbank_training" +"project4146_(3)","default",3,3849,59,263,11,2,100,FALSE,3.04700000000048,1.879511765,306.395457,247.90622,145.666599,77.723276,0,1638.1709,496.971015,51.236215,1.810841,120,"mbank_training" +"project4146_(3)","default",4,3850,59,261,43,1,100,FALSE,15.3180000000002,2.374788278,1320.469164,894.874744,714.758367,346.257565,0,8599.051059,3133.391372,210.801877,10.461404,120,"mbank_training" +"project4146_(3)","default",5,3851,59,261,31,2,100,FALSE,12.2269999999999,0.741778749,1014.974118,655.035662,685.970374,273.578485,0,6815.550667,2422.397126,162.815185,2.274815,120,"mbank_training" +"project3688","default",1,3847,60,854,17,2,100,FALSE,5.02500000000055,5.021771877,510.37382,427.643124,259.063646,112.165339,0,2383.360876,1192.373307,62.203885,1.168781,120,"mbank_training" +"project3688","default",2,3848,60,852,56,2,100,FALSE,17.5849999999991,17.581586221,1508.551866,1422.340587,842.489003,302.981617,0,8563.456329,4566.251877,183.361074,8.76499,120,"mbank_training" +"project3688","default",3,3849,60,850,86,2,100,FALSE,23.5720000000001,7.411819695,2115.873042,1577.791567,1209.467271,451.31191,0,11671.087764,6083.894379,257.34136,1.490678,120,"mbank_training" +"project3688","default",4,3850,60,845,100,0,100,FALSE,28.1569999999992,24.162367597,2261.042949,2157.45028,1376.283966,483.92103,0,14079.414317,7325.004318,307.627242,8.029794,120,"mbank_training" +"project3688","default",5,3851,60,851,100,2,100,FALSE,31.8050000000003,31.802672613,2618.332145,2409.378973,1494.269097,570.378077,0,15695.913627,8333.697,340.037484,0,120,"mbank_training" +"project4049","default",1,3847,60,5237,58,2,69,FALSE,80.0329999999994,32.052243607,16809.550981,5256.228341,2110.483353,1030.435606,0,35258.662297,11422.279982,653.04131,4.22619,120,"mbank_training" +"project4049","default",2,3848,60,5241,14,2,100,FALSE,16.0689999999995,7.870476592,4173.924444,1414.553628,544.295207,261.063861,0,6683.463902,1911.022872,169.10444,3.227579,120,"mbank_training" +"project4049","default",3,3849,60,5237,86,0,69,TRUE,120.012,29.14747189,24915.557783,8555.080917,2826.307074,1585.849986,0,52725.984647,16420.480107,960.753632,19.357873,120,"mbank_training" +"project4049","default",4,3850,60,5237,82,1,67,TRUE,113.106000000001,87.538582491,24380.17746,8460.16691,2744.356645,1587.127748,0,53779.48615,16087.417783,947.49759,0,120,"mbank_training" +"project4049","default",5,3851,60,5238,48,2,100,FALSE,61.625,42.237513028,14343.858343,4557.954165,1591.975844,975.424709,0,29679.922169,9072.33144,551.917905,6.537494,120,"mbank_training" +"project423","default",1,3847,60,495,8,4,100,FALSE,3.40499999999975,0.526153467,566.898634,190.75694,215.289551,136.966522,0,1626.799883,386.053694,70.778868,9.020881,120,"mbank_training" +"project423","default",2,3848,60,495,10,4,100,FALSE,3.90000000000055,0.411584624,588.627592,245.185435,238.855369,144.338626,0,1998.711399,474.040828,90.219949,16.501401,120,"mbank_training" +"project423","default",3,3849,60,495,9,4,100,FALSE,3.39900000000034,0.46492677,604.06468,177.265199,213.227558,110.902656,0,1578.009632,412.734359,77.942664,17.989694,120,"mbank_training" +"project423","default",4,3850,60,495,9,3,100,FALSE,3.30899999999929,0.925018402,492.497982,155.53686,151.105736,102.833498,0,1678.795371,457.312226,70.475237,10.101998,120,"mbank_training" +"project423","default",5,3851,60,495,7,3,100,FALSE,2.92799999999988,0.693696482,413.570945,112.361907,210.636516,84.170433,0,1356.68924,368.077685,66.544864,10.714993,120,"mbank_training" +"project4286","default",1,3847,63,283,29,1,100,FALSE,11.7159999999994,4.100318091,1046.296071,768.137171,567.789875,282.419437,0,6540.196924,2232.430133,165.583054,11.269518,120,"mbank_training" +"project4286","default",2,3848,63,286,17,1,100,FALSE,6.14800000000014,2.731416157,577.453715,449.078541,267.206664,154.448919,0,3413.660776,1114.497786,88.825288,12.203679,120,"mbank_training" +"project4286","default",3,3849,63,282,100,0,100,FALSE,44.9789999999994,5.96331056,3683.094082,2517.868007,1813.204677,950.494329,0,25889.045977,9131.184162,567.907615,10.793273,120,"mbank_training" +"project4286","default",4,3850,63,282,36,2,100,FALSE,15.5619999999999,7.152473633,1275.286219,836.323187,605.933541,328.329446,0,8875.831635,3180.293214,206.313307,4.975995,120,"mbank_training" +"project4286","default",5,3851,63,281,100,0,100,FALSE,46.5769999999993,35.041412631,3699.824056,2689.559526,2089.605703,946.042036,0,26985.775764,9107.103506,618.261001,11.396668,120,"mbank_training" +"project4359","default",1,3847,71,183,44,14,100,FALSE,41.9520000000002,3.203920721,8767.766222,2006.351704,1513.433996,1382.957629,0,23498.199682,3646.497845,799.066168,169.565444,120,"mbank_training" +"project4359","default",2,3848,71,183,30,14,100,FALSE,21.7539999999999,0.915047912,5225.794822,1438.006894,960.455597,825.817399,0,10896.586175,1663.655235,533.446058,88.870922,120,"mbank_training" +"project4359","default",3,3849,71,183,48,14,100,FALSE,40.0810000000001,4.272051181,8738.055745,2383.24364,1527.197327,1300.041735,0,21769.124441,3284.361575,822.207967,151.216495,120,"mbank_training" +"project4359","default",4,3850,71,183,39,14,100,FALSE,30.3309999999992,4.925332616,7050.165321,1939.795687,1343.31804,1102.898121,0,15901.077905,2095.50652,655.182761,120.679789,120,"mbank_training" +"project4359","default",5,3851,71,184,5,4,100,FALSE,3.22699999999986,0.863578676,733.995855,246.411799,130.749929,134.986509,0,1477.365187,265.119975,95.768599,7.753395,120,"mbank_training" +"project4397","default",1,3847,75,1645,100,1,80,FALSE,92.9229999999998,14.423068514,13204.176944,13809.390556,2675.089632,1261.189782,0,43065.316764,14214.117253,874.400732,0,120,"mbank_training" +"project4397","default",2,3848,75,1647,44,2,72,FALSE,40.518,40.516149152,5462.808026,6069.197401,1228.633206,540.398184,0,17364.29475,5740.716472,370.847129,5.18697,120,"mbank_training" +"project4397","default",3,3849,75,1649,22,2,100,FALSE,18.2539999999999,18.252467134,2903.384257,3034.807995,547.422862,272.134946,0,8130.583929,2706.994697,187.14634,2.482476,120,"mbank_training" +"project4397","default",4,3850,75,1646,100,0,100,FALSE,89.067,62.749311293,12699.873477,14195.273694,2819.594899,1371.308273,0,42354.846242,13338.153642,863.920671,23.747802,120,"mbank_training" +"project4397","default",5,3851,75,1646,100,1,43,FALSE,88.2970000000005,8.63749485,12756.82406,14581.059802,2551.350852,1244.568156,0,41346.437513,13615.086309,876.091028,0,120,"mbank_training" +"project2084_(1)","default",1,3847,86,28962,3,1,1,TRUE,108.528,39.357658916,23627.712028,24029.377952,5464.093052,1094.520444,0,35104.689634,17833.210932,1040.792512,0,120,"mbank_training" +"project2084_(1)","default",2,3848,86,28206,2,1,2,TRUE,110.605,43.882252328,21477.758523,22676.801355,8385.164939,1728.20278,0,33082.151982,20161.210155,644.290514,0,120,"mbank_training" +"project2084_(1)","default",3,3849,86,28303,3,1,1,TRUE,108.306,42.637740632,30964.641428,23462.698032,5351.663578,1468.335058,0,32030.349028,13873.619593,887.377121,0,120,"mbank_training" +"project2084_(1)","default",4,3850,86,28724,3,0,1,TRUE,108.268,79.500306167,21209.336022,16545.544527,6915.74316,1698.567909,0,40609.952976,18209.011495,999.977684,1818.540659,120,"mbank_training" +"project2084_(1)","default",5,3851,86,29024,4,1,1,TRUE,108.461,78.301270838,25734.337567,17627.057703,7778.349417,1409.668754,0,38256.493533,16373.841466,1007.174092,0,120,"mbank_training" +"project2771","default",1,3847,94,1042,65,1,16,TRUE,109.469999999999,90.147688908,7955.523496,3364.064349,5118.545353,1685.906249,0,59102.753807,29649.557642,1129.086726,4.686277,120,"mbank_training" +"project2771","default",2,3848,94,1049,65,1,10,TRUE,109.496,109.495686787,8276.423813,3896.479947,5070.025056,1554.401001,0,60209.181891,27813.96109,1150.255409,25.301502,120,"mbank_training" +"project2771","default",3,3849,94,1055,65,1,10,TRUE,108.414,108.413357524,7932.479499,4160.669826,6047.767008,1677.958011,0,59065.792055,27961.877695,1152.576054,0,120,"mbank_training" +"project2771","default",4,3850,94,1046,66,0,6,TRUE,108.206,20.66770794,8170.151603,3690.397114,5343.117597,1847.35924,0,60061.381446,27735.459754,1123.077726,38.827043,120,"mbank_training" +"project2771","default",5,3851,94,1059,65,1,1,TRUE,108.043000000001,44.486758246,8124.186883,3750.441696,4893.377873,1690.985489,0,59411.915119,29030.299076,1117.712092,0,120,"mbank_training" +"project2184","default",1,3847,114,565,13,1,100,FALSE,16.5100000000002,10.270284347,1901.604448,4187.802544,481.297502,267.875047,0,7162.991061,1958.33887,236.763264,35.048523,120,"mbank_training" +"project2184","default",2,3848,114,564,8,2,100,FALSE,9.17799999999988,4.400413194,1068.136565,2525.129151,342.888574,182.385329,0,3728.285364,981.185821,141.535775,6.532746,120,"mbank_training" +"project2184","default",3,3849,114,565,18,2,100,FALSE,23.433,14.00867502,2492.973307,5244.857721,716.101013,419.309974,0,10828.214684,3285.003272,326.780788,18.981366,120,"mbank_training" +"project2184","default",4,3850,114,563,73,0,100,TRUE,108.129000000001,15.334493816,9759.896311,23116.913797,2974.664553,1610.739116,0,53197.148769,15938.358971,1339.551383,64.034474,120,"mbank_training" +"project2184","default",5,3851,114,564,18,2,100,FALSE,24.5619999999999,1.094052073,2382.013724,5658.68694,772.047049,388.245695,0,11377.309218,3381.536146,324.950291,12.937261,120,"mbank_training" +"project3938","default",1,3847,119,3417,9,2,100,FALSE,44.5619999999999,19.075341196,11413.369643,15029.814616,2202.384628,586.820894,0,10863.89477,3556.837676,388.669586,26.65143,120,"mbank_training" +"project3938","default",2,3848,119,3408,18,1,100,TRUE,108.633,20.195418378,26861.257588,33098.946527,4015.335393,1157.836335,0,29753.222391,12283.646294,828.075363,0,120,"mbank_training" +"project3938","default",3,3849,119,3413,9,2,100,FALSE,46.6349999999993,25.918489915,12043.938351,14090.206009,1790.431927,539.632353,0,12164.741083,4564.794154,398.036473,187.663721,120,"mbank_training" +"project3938","default",4,3850,119,3408,18,0,100,TRUE,108.391,83.647681499,24710.571406,34061.871319,4085.051645,1121.98776,0,30625.896483,12543.803153,802.23311,62.598762,120,"mbank_training" +"project3938","default",5,3851,119,3405,18,1,100,TRUE,108.735000000001,108.725656538,27374.72291,33415.734207,2665.163714,1469.101328,0,31037.667179,11162.01504,797.550904,62.214628,120,"mbank_training" +"syab07201","default",1,3847,125,14933,12,1,3,TRUE,108.728,12.592821236,13570.480664,8180.130151,3577.144371,1630.401141,0,51213.95676,28665.633721,1232.812355,0,120,"mbank_training" +"syab07201","default",2,3848,125,14931,12,1,1,TRUE,108.170999999999,89.087282798,13846.124014,8503.522116,3033.045252,1743.023891,0,56010.682657,23629.967986,1254.266463,0,120,"mbank_training" +"syab07201","default",3,3849,125,14932,12,1,4,TRUE,109.077,82.250482396,14298.112856,7593.533046,3447.583924,1667.188132,0,53409.384596,26372.219385,1269.450238,0,120,"mbank_training" +"syab07201","default",4,3850,125,14948,13,1,3,TRUE,108.708,58.209723111,14448.190969,8478.906994,3301.745774,1760.348587,0,52382.540065,26330.066606,1350.91075,0,120,"mbank_training" +"syab07201","default",5,3851,125,14926,9,1,2,TRUE,108.34,38.305426931,13591.55147,10687.585468,2838.502225,2031.87457,0,52607.030956,24885.943975,1424.417762,0,120,"mbank_training" +"project4133","default",1,3847,131,2371,28,1,100,TRUE,109.487999999999,109.485362629,18339.628663,29653.665094,3505.219405,1074.854075,0,37083.883069,17530.824584,823.678609,0,120,"mbank_training" +"project4133","default",2,3848,131,2379,28,1,100,TRUE,109.438,12.828640628,18298.553113,32054.720869,3214.405102,1234.216956,0,35374.579163,17020.561662,822.438309,0,120,"mbank_training" +"project4133","default",3,3849,131,2378,28,1,100,TRUE,109.021000000001,16.15741946,19005.412995,29148.022626,4032.380029,1146.585333,0,37006.380772,16787.657354,879.708429,0,120,"mbank_training" +"project4133","default",4,3850,131,2372,25,1,100,TRUE,109.190000000001,109.18586107,18321.807637,32304.089003,3145.022262,1071.913018,0,36400.932524,15981.659703,761.298839,0,120,"mbank_training" +"project4133","default",5,3851,131,2376,27,1,100,TRUE,108.601000000001,108.597659452,19342.257079,29311.053978,3138.549354,1310.424968,0,37076.326113,17006.773101,825.497051,0,120,"mbank_training" +"project804","default",1,3847,173,1361,5,1,100,TRUE,119.700000000001,119.687429526,18892.753258,22370.05734,6848.462855,1344.700911,0,38968.418347,18451.002179,1120.532486,0,120,"mbank_training" +"project804","default",2,3848,173,1361,6,1,77,TRUE,120.030999999999,120.019861675,19661.854592,29304.260402,6104.284431,1238.988987,0,35207.71569,15866.336486,1093.321497,0,120,"mbank_training" +"project804","default",3,3849,173,1374,6,1,37,TRUE,120.075000000001,120.066968555,20123.993779,21883.447515,4358.387938,1306.7305,0,35922.962296,22978.080261,1861.523875,0,120,"mbank_training" +"project804","default",4,3850,173,1363,5,1,100,TRUE,115.026,115.013226373,19002.039202,30904.950987,10687.222416,1399.585322,0,33172.285515,12033.102396,860.626358,0,120,"mbank_training" +"project804","default",5,3851,173,1363,7,1,100,TRUE,115.235999999999,115.223057538,22282.004866,20105.230448,6611.483638,1417.110018,0,39375.434188,17047.971526,1247.914574,0,120,"mbank_training" +"project4284","default",1,3847,4062,1072,0,1,100,TRUE,349.493,349.46794991,103451.107481,13308.954479,0,0,0,0,0,0,0,120,"mbank_training" +"project4284","default",2,3848,4062,1322,0,1,100,TRUE,462.067999999999,461.989119379,101258.802999,13033.694143,0,0,0,0,0,0,0,120,"mbank_training" +"project4284","default",3,3849,4062,1193,0,1,1,TRUE,120.931999999999,120.925018555,108451.106187,12471.801843,0,0,0,0,0,0,0,120,"mbank_training" +"project4284","default",4,3850,4062,1040,0,1,100,TRUE,333.196,333.186980399,95453.13475,13018.854749,0,0,0,0,0,0,0,120,"mbank_training" +"project4284","default",5,3851,4062,1220,0,1,100,TRUE,279.598,279.591312384,98085.089315,17653.911914,0,0,0,0,0,0,0,120,"mbank_training" diff --git a/dev/benchmarks/t252_mbank_30s_20260327_1044.csv b/dev/benchmarks/t252_mbank_30s_20260327_1044.csv new file mode 100644 index 000000000..b05455aac --- /dev/null +++ b/dev/benchmarks/t252_mbank_30s_20260327_1044.csv @@ -0,0 +1,126 @@ +"dataset","strategy","replicate","seed","n_taxa","best_score","replicates","hits_to_best","pool_size","timed_out","wall_s","time_to_best_s","wagner_ms","tbr_ms","xss_ms","rss_ms","css_ms","ratchet_ms","drift_ms","final_tbr_ms","fuse_ms","budget_s","source" +"project532","default",1,3847,21,1139,7,5,4,FALSE,0.542,0.159055272,135.619212,13.04573,22.272809,15.919137,0,276.407229,56.168931,11.423204,2.049818,30,"mbank_training" +"project532","default",2,3848,21,1139,6,4,3,FALSE,0.454999999999999,0.094952955,92.239999,25.374239,16.837307,14.426449,0,245.246002,42.116067,9.629668,1.382472,30,"mbank_training" +"project532","default",3,3849,21,1139,6,5,4,FALSE,0.475000000000001,0.115830358,107.415156,18.13517,22.390323,12.792332,0,248.020245,43.648871,9.998143,1.511787,30,"mbank_training" +"project532","default",4,3850,21,1139,7,3,3,FALSE,0.583,0.158395851,110.868678,33.604432,20.556298,16.136819,0,327.756102,53.823888,12.069131,0.705788,30,"mbank_training" +"project532","default",5,3851,21,1139,6,3,2,FALSE,0.443000000000001,0.095074113,94.240283,22.942882,19.845399,14.211134,0,237.886468,36.744354,9.808866,1.523488,30,"mbank_training" +"project2346","default",1,3847,23,316,18,2,4,FALSE,0.706,0.483245432,59.526585,29.639617,27.097784,15.987328,0,445.189137,111.983257,10.955062,0.615469,30,"mbank_training" +"project2346","default",2,3848,23,318,8,2,20,FALSE,0.272,0.107900119,30.632818,8.388472,14.761471,6.777368,0,142.381512,32.831868,4.816267,0.208482,30,"mbank_training" +"project2346","default",3,3849,23,320,12,2,4,FALSE,0.401999999999999,0.107059556,42.565554,15.985884,22.632968,10.643807,0,238.422067,59.348087,7.143847,0.571676,30,"mbank_training" +"project2346","default",4,3850,23,317,17,2,13,FALSE,0.695,0.302030974,61.210236,24.433465,24.273697,15.448662,0,427.301713,107.544701,10.352768,0.259899,30,"mbank_training" +"project2346","default",5,3851,23,314,78,1,4,FALSE,3.36,1.270754501,279.259769,120.36549,125.623277,66.597026,0,2160.387218,553.280991,47.067397,1.0782,30,"mbank_training" +"project2451","default",1,3847,24,735,12,2,4,FALSE,0.391,0.201359523,57.55009,12.458124,17.440524,10.013999,0,208.719441,73.761881,6.301613,0.352573,30,"mbank_training" +"project2451","default",2,3848,24,732,67,1,1,FALSE,2.958,0.754287847,321.511476,75.820549,109.558619,48.42972,0,1739.621241,624.549235,35.606072,0,30,"mbank_training" +"project2451","default",3,3849,24,731,7,2,6,FALSE,0.221999999999998,0.083935985,38.627521,8.011942,11.150373,4.288323,0,111.132705,37.786174,3.844417,0.195688,30,"mbank_training" +"project2451","default",4,3850,24,731,74,1,3,FALSE,3.226,1.072942617,382.702166,78.858534,117.675578,52.337654,0,1893.503165,653.858183,39.018316,1.668751,30,"mbank_training" +"project2451","default",5,3851,24,730,56,1,2,FALSE,2.475,0.259627507,272.084943,78.715156,92.241695,42.774133,0,1441.742098,513.345106,30.965669,0,30,"mbank_training" +"project4501","default",1,3847,24,118,11,4,63,FALSE,0.402000000000001,0.045659888,19.456467,10.321312,11.249257,7.273704,0,127.024403,20.262034,4.693674,0.592876,30,"mbank_training" +"project4501","default",2,3848,24,118,8,6,93,FALSE,0.331000000000003,0.019307548,14.526607,7.223871,10.667763,6.112406,0,63.857366,13.417429,3.468459,0.840874,30,"mbank_training" +"project4501","default",3,3849,24,118,11,8,63,FALSE,0.353999999999999,0.024017324,16.544606,12.054514,10.874954,6.541294,0,94.229752,16.766104,5.012416,1.208605,30,"mbank_training" +"project4501","default",4,3850,24,118,11,6,63,FALSE,0.399999999999999,0.049086008,17.238983,13.324074,11.375175,6.189715,0,122.860359,19.102503,4.840804,0.788614,30,"mbank_training" +"project4501","default",5,3851,24,118,9,4,93,FALSE,0.378,0.018508023,15.81334,12.376079,7.116948,5.60964,0,102.823873,17.718845,3.840052,0.661435,30,"mbank_training" +"project944","default",1,3847,25,128,7,7,60,FALSE,0.306999999999999,0.018572714,13.799929,8.682096,6.631403,4.331374,0,50.309772,9.260855,2.764333,0.884165,30,"mbank_training" +"project944","default",2,3848,25,128,6,6,60,FALSE,0.233000000000001,0.019885466,12.479662,4.603999,4.628432,3.361849,0,44.265201,7.877521,2.368148,0.873353,30,"mbank_training" +"project944","default",3,3849,25,128,6,6,60,FALSE,0.314,0.020729454,10.93727,6.439242,5.535302,3.476875,0,43.362833,9.229105,2.367437,0.930562,30,"mbank_training" +"project944","default",4,3850,25,128,8,8,60,FALSE,0.356999999999999,0.018798128,16.221829,8.35613,6.25217,5.100312,0,59.507827,13.044878,3.160491,0.944067,30,"mbank_training" +"project944","default",5,3851,25,128,9,9,60,FALSE,0.276999999999997,0.018958742,17.363908,11.685671,7.165347,5.454358,0,65.433255,11.16655,3.531729,1.479684,30,"mbank_training" +"project971_(1)","default",1,3847,26,157,7,5,100,FALSE,0.193000000000001,0.032616012,18.605586,8.255522,13.045921,6.466022,0,108.291193,17.142443,5.032654,0.93516,30,"mbank_training" +"project971_(1)","default",2,3848,26,157,7,3,100,FALSE,0.209,0.033034109,15.300445,7.423335,8.066606,6.895672,0,120.329215,17.930359,5.086765,0.809634,30,"mbank_training" +"project971_(1)","default",3,3849,26,157,14,9,100,FALSE,0.411000000000001,0.073843174,32.755924,20.344981,29.768159,14.029603,0,232.580428,32.34325,10.062332,1.52913,30,"mbank_training" +"project971_(1)","default",4,3850,26,157,9,5,100,FALSE,0.266000000000002,0.034971856,20.398181,11.072927,12.306986,9.028718,0,166.869904,26.803601,6.538127,0.79671,30,"mbank_training" +"project971_(1)","default",5,3851,26,157,8,5,100,FALSE,0.220000000000002,0.030588355,18.153386,8.61647,15.762383,8.048763,0,118.243181,20.804155,5.864089,1.237399,30,"mbank_training" +"project2762","default",1,3847,29,259,9,5,100,FALSE,0.477,0.064657482,89.005679,28.625208,27.015199,16.271522,0,222.30784,53.258133,10.549338,2.931178,30,"mbank_training" +"project2762","default",2,3848,29,259,9,5,100,FALSE,0.500999999999998,0.107338091,97.623433,21.280192,19.892007,19.848566,0,237.460345,52.994316,10.82002,2.92123,30,"mbank_training" +"project2762","default",3,3849,29,259,14,7,100,FALSE,0.824000000000002,0.248402003,167.013281,40.401138,31.5735,30.962567,0,404.788011,76.685877,16.82874,3.46784,30,"mbank_training" +"project2762","default",4,3850,29,259,7,2,100,FALSE,0.442,0.060617737,82.572548,17.849112,22.881476,15.476914,0,210.443998,44.501366,8.239273,0.595401,30,"mbank_training" +"project2762","default",5,3851,29,259,9,2,100,FALSE,0.549999999999997,0.156776252,96.810243,25.176484,24.499914,18.026146,0,289.442735,65.42589,10.713507,1.121812,30,"mbank_training" +"project826","default",1,3847,33,431,10,5,100,FALSE,0.585000000000001,0.058192461,97.322235,84.950343,36.014361,16.87607,0,243.540672,48.527945,10.402926,2.289268,30,"mbank_training" +"project826","default",2,3848,33,431,14,8,100,FALSE,0.815000000000001,0.064265745,164.329903,100.342802,40.22171,29.986892,0,362.511776,60.62095,14.329798,2.752712,30,"mbank_training" +"project826","default",3,3849,33,431,9,5,100,FALSE,0.506,0.112018993,97.964377,66.801407,33.044869,13.567502,0,224.709019,34.377508,9.366282,2.14059,30,"mbank_training" +"project826","default",4,3850,33,431,13,7,100,FALSE,0.698,0.062873383,154.074276,81.270968,45.052406,19.77621,0,295.134169,54.289724,13.291651,3.70243,30,"mbank_training" +"project826","default",5,3851,33,431,10,7,100,FALSE,0.533000000000001,0.1020999,107.928141,72.157363,27.216947,15.95665,0,228.789413,45.677109,10.315738,2.329386,30,"mbank_training" +"project561","default",1,3847,34,1169,5,4,2,FALSE,0.659999999999997,0.181347639,92.846159,25.059065,28.207322,29.981203,0,370.359995,88.552929,15.979782,1.446704,30,"mbank_training" +"project561","default",2,3848,34,1169,10,8,2,FALSE,1.232,0.18098681,142.576079,44.374205,78.560658,59.124055,0,702.572509,163.908009,31.657059,1.392722,30,"mbank_training" +"project561","default",3,3849,34,1169,6,4,2,FALSE,0.892000000000003,0.193806133,98.658642,28.550289,63.174951,32.545117,0,527.190542,111.328013,20.332498,2.808147,30,"mbank_training" +"project561","default",4,3850,34,1169,7,6,2,FALSE,0.850000000000001,0.196609952,112.865658,45.375572,39.933657,39.492137,0,466.18721,115.350405,22.395089,1.381331,30,"mbank_training" +"project561","default",5,3851,34,1169,13,10,2,FALSE,1.577,0.176815928,214.233348,75.676926,87.389599,73.393768,0,887.255078,189.841499,42.006711,0,30,"mbank_training" +"project571","default",1,3847,42,634,8,3,12,FALSE,0.694000000000003,0.164095501,106.218011,51.333049,32.312008,21.394067,0,342.296416,63.619378,12.651639,1.096264,30,"mbank_training" +"project571","default",2,3848,42,635,7,6,28,FALSE,0.639000000000003,0.088679535,100.785177,45.952629,28.371058,20.286232,0,230.646768,48.366994,10.52306,1.873838,30,"mbank_training" +"project571","default",3,3849,42,634,14,5,12,FALSE,1.271,0.205375594,186.209453,70.512624,64.876695,39.745794,0,696.746744,136.711756,21.663235,2.899548,30,"mbank_training" +"project571","default",4,3850,42,634,6,3,12,FALSE,0.503999999999998,0.106289547,97.153147,40.469948,25.720071,17.059675,0,215.716522,48.979746,9.500026,2.09783,30,"mbank_training" +"project571","default",5,3851,42,634,7,4,12,FALSE,0.562000000000005,0.094291991,104.624002,34.374362,22.712617,21.010924,0,265.769841,52.724237,10.747413,2.061411,30,"mbank_training" +"project4146_(3)","default",1,3847,59,260,79,1,100,TRUE,27.184,1.159421069,2161.951899,1431.436231,1369.737579,584.938795,0,15612.088541,5495.045514,342.372656,0,30,"mbank_training" +"project4146_(3)","default",2,3848,59,262,16,2,100,FALSE,4.43300000000001,3.006056794,398.978693,305.709476,242.038821,126.156641,0,2449.890491,699.98229,70.565506,3.637498,30,"mbank_training" +"project4146_(3)","default",3,3849,59,263,11,2,100,FALSE,2.911,1.75921419,292.432282,236.70639,138.23963,74.826225,0,1569.189041,477.95843,49.093865,1.822251,30,"mbank_training" +"project4146_(3)","default",4,3850,59,261,43,1,100,FALSE,13.781,2.134428645,1195.778958,805.816844,652.279548,310.548343,0,7750.977861,2798.563687,188.875727,9.304356,30,"mbank_training" +"project4146_(3)","default",5,3851,59,261,31,2,100,FALSE,10.048,0.622684871,834.240637,543.209237,558.784365,222.447677,0,5626.288589,1986.738636,134.143195,1.677448,30,"mbank_training" +"project3688","default",1,3847,60,854,17,2,100,FALSE,4.04899999999999,4.046715439,408.411651,338.435934,212.952407,88.730993,0,1919.553444,962.191821,49.845781,0.793184,30,"mbank_training" +"project3688","default",2,3848,60,852,56,2,100,FALSE,15.176,15.173541808,1302.515289,1226.872642,710.476017,259.35943,0,7396.027813,3937.753164,158.084343,7.912235,30,"mbank_training" +"project3688","default",3,3849,60,850,86,2,100,FALSE,22.625,7.183552468,2072.997198,1511.265434,1160.574252,431.875451,0,11185.916283,5821.436102,245.879774,1.441333,30,"mbank_training" +"project3688","default",4,3850,60,845,100,0,100,FALSE,26.996,23.186208158,2348.914977,2049.502682,1311.093182,459.592175,0,13408.741898,6965.596352,294.003592,7.587966,30,"mbank_training" +"project3688","default",5,3851,60,846,99,2,11,TRUE,27.29,27.28811545,2406.499645,2060.393789,1264.02288,485.95709,0,13418.549013,7075.465949,287.115493,0,30,"mbank_training" +"project4049","default",1,3847,60,5240,26,1,100,TRUE,27.317,5.573148428,6703.751431,2176.123735,825.960021,396.113765,0,12768.91305,3880.674858,253.963364,0,30,"mbank_training" +"project4049","default",2,3848,60,5241,14,2,100,FALSE,13.655,7.027287931,3626.086034,1198.572419,456.608051,220.963331,0,5704.360758,1589.957546,142.553627,3.045504,30,"mbank_training" +"project4049","default",3,3849,60,5237,25,0,69,TRUE,30.007,24.853505039,6743.164871,2194.709548,693.107501,428.940065,0,12666.958798,4006.547911,248.048225,20.524267,30,"mbank_training" +"project4049","default",4,3850,60,5238,25,1,42,TRUE,30.004,0.932033283,6776.515792,2319.610345,684.456542,434.411469,0,12776.817935,3766.735394,249.499943,0,30,"mbank_training" +"project4049","default",5,3851,60,5239,25,1,100,TRUE,27.486,3.676459796,6617.98495,2189.03596,703.300419,449.992373,0,12985.569232,3805.079674,248.978194,0,30,"mbank_training" +"project423","default",1,3847,60,495,8,4,100,FALSE,2.75400000000002,0.419124173,456.884432,154.343943,179.912009,110.096176,0,1290.272654,315.113496,59.03124,8.115354,30,"mbank_training" +"project423","default",2,3848,60,495,10,4,100,FALSE,3.13900000000001,0.370053092,496.594593,197.924011,185.722131,115.789679,0,1590.611775,385.967567,74.139425,14.305423,30,"mbank_training" +"project423","default",3,3849,60,495,9,4,100,FALSE,2.88499999999999,0.385641831,500.870846,145.446436,177.377975,93.716956,0,1323.701525,355.745644,66.297395,15.357013,30,"mbank_training" +"project423","default",4,3850,60,495,9,3,100,FALSE,3.17099999999999,0.913398284,477.038761,151.875211,145.583234,99.231705,0,1607.287212,441.466933,67.048899,9.279487,30,"mbank_training" +"project423","default",5,3851,60,495,7,3,100,FALSE,2.30000000000001,0.587509882,327.409848,89.159324,165.845635,66.60812,0,1078.061213,286.696336,50.646708,8.055533,30,"mbank_training" +"project4286","default",1,3847,63,283,29,1,100,FALSE,10.679,3.568361219,949.758854,695.981939,507.727922,255.192069,0,5968.563535,2042.300499,151.240761,10.412181,30,"mbank_training" +"project4286","default",2,3848,63,286,17,1,100,FALSE,5.93299999999999,2.493855935,547.902595,427.93154,253.562543,148.533339,0,3300.116545,1089.177478,84.825461,12.413171,30,"mbank_training" +"project4286","default",3,3849,63,282,69,0,100,TRUE,27.162,5.203139408,2360.989445,1512.558401,1088.458169,571.79875,0,15666.679256,5442.170076,346.111242,8.470814,30,"mbank_training" +"project4286","default",4,3850,63,282,36,2,100,FALSE,13.267,6.209710639,1092.563694,721.114448,524.582188,278.114164,0,7555.478801,2692.809987,176.91012,4.096113,30,"mbank_training" +"project4286","default",5,3851,63,283,70,1,100,TRUE,27.169,1.557344311,2287.207347,1582.845371,1211.075903,547.191397,0,15688.016025,5332.93518,347.612348,0,30,"mbank_training" +"project4359","default",1,3847,71,183,41,13,100,TRUE,27.097,2.261897091,5771.353119,1305.011338,1006.712715,912.447879,0,15026.53261,2340.667863,522.209948,117.847876,30,"mbank_training" +"project4359","default",2,3848,71,183,30,14,100,FALSE,16.129,0.665570597,3928.068751,1053.953565,706.859134,611.186316,0,8062.325089,1227.655533,389.882129,64.610764,30,"mbank_training" +"project4359","default",3,3849,71,183,43,13,100,TRUE,27.09,3.114505389,6132.139298,1549.203645,1017.246691,869.207429,0,14564.715459,2203.509034,547.569478,96.465078,30,"mbank_training" +"project4359","default",4,3850,71,183,39,14,100,FALSE,23.331,3.776490508,5602.272217,1470.537157,1043.605592,841.264513,0,12109.540516,1584.418474,494.61431,93.977932,30,"mbank_training" +"project4359","default",5,3851,71,184,5,4,100,FALSE,2.51499999999999,0.625239003,581.307694,181.271259,99.713473,106.22802,0,1151.351682,219.65755,60.431613,5.864753,30,"mbank_training" +"project4397","default",1,3847,75,1645,39,1,84,TRUE,29.898,11.652682336,4059.293835,4611.243157,859.16792,378.941198,0,12614.467637,4211.561709,269.027083,0,30,"mbank_training" +"project4397","default",2,3848,75,1648,40,1,11,TRUE,27.222,16.275268683,4083.297886,4499.993494,922.733936,399.403983,0,12638.777186,4181.619232,271.886856,1.900378,30,"mbank_training" +"project4397","default",3,3849,75,1649,22,2,100,FALSE,14.671,14.669488113,2326.940955,2439.062281,438.000839,215.523963,0,6522.444608,2178.359279,147.225135,1.934383,30,"mbank_training" +"project4397","default",4,3850,75,1648,40,1,100,TRUE,27.3430000000001,10.737418532,4263.486964,4441.437048,998.363541,456.623285,0,12534.551377,4024.735858,269.57219,10.386157,30,"mbank_training" +"project4397","default",5,3851,75,1646,39,1,42,TRUE,28.025,6.965588083,4076.471754,4722.078731,843.218866,405.213474,0,12542.741242,4122.793808,285.67242,0,30,"mbank_training" +"project2084_(1)","default",1,3847,86,28962,0,1,1,TRUE,27.2950000000001,27.284871037,6087.152752,7601.569896,1703.323901,292.649511,0,10154.252001,1190.248423,0,0,30,"mbank_training" +"project2084_(1)","default",2,3848,86,28206,0,1,2,TRUE,27.893,27.88159824,6449.215962,9863.875036,2809.440548,508.882523,0,7554.658108,0,0,0,30,"mbank_training" +"project2084_(1)","default",3,3849,86,28303,0,1,1,TRUE,27.3520000000001,27.341604691,5532.892173,4659.700576,2729.695298,369.901689,0,10917.922654,2879.327597,0,0,30,"mbank_training" +"project2084_(1)","default",4,3850,86,29263,0,1,1,TRUE,27.337,27.32739407,5136.496018,4624.353351,2476.729143,305.33289,0,12907.034261,1626.271363,0,0,30,"mbank_training" +"project2084_(1)","default",5,3851,86,29028,0,1,1,TRUE,27.277,27.265355976,6452.248698,4808.151115,2003.218686,324.815519,0,10753.100827,2544.773109,129.969162,0,30,"mbank_training" +"project2771","default",1,3847,94,1061,18,1,2,TRUE,27.628,27.627539078,2381.315256,1179.557903,1473.537564,476.722015,0,14814.327106,6359.277975,317.095031,4.795324,30,"mbank_training" +"project2771","default",2,3848,94,1051,18,1,2,TRUE,27.056,4.598189444,2329.759595,1058.876302,1523.15815,437.623557,0,14578.550452,6707.850826,347.023291,25.508291,30,"mbank_training" +"project2771","default",3,3849,94,1055,18,1,25,TRUE,28.596,28.595855406,2208.668083,1237.955741,1885.302937,461.560814,0,14342.634134,6721.423525,318.360851,0,30,"mbank_training" +"project2771","default",4,3850,94,1046,18,0,6,TRUE,27.212,20.807814846,2244.376253,996.635736,1387.986264,635.028035,0,14958.245088,6429.43522,307.637888,39.061989,30,"mbank_training" +"project2771","default",5,3851,94,1061,18,1,83,TRUE,30.005,30.00408473,2175.970571,1277.59992,1288.181195,517.295524,0,14521.506037,6925.085047,312.43618,0,30,"mbank_training" +"project2184","default",1,3847,114,565,13,1,100,FALSE,16.5749999999999,10.308158943,1901.553612,4214.328218,485.080766,269.267477,0,7183.460842,1969.620003,238.019628,35.13861,30,"mbank_training" +"project2184","default",2,3848,114,564,8,2,100,FALSE,9.25199999999995,4.431088219,1068.876064,2542.898349,345.815564,183.719232,0,3766.096167,990.361813,142.478229,6.554553,30,"mbank_training" +"project2184","default",3,3849,114,565,18,2,100,FALSE,23.623,14.129842318,2496.19712,5294.070284,723.003063,423.243246,0,10922.706838,3311.749457,329.429608,19.212075,30,"mbank_training" +"project2184","default",4,3850,114,563,19,0,100,TRUE,27.1590000000001,15.437353718,2783.61853,6176.676207,832.030467,454.22667,0,12537.971451,3609.087835,353.928239,64.443171,30,"mbank_training" +"project2184","default",5,3851,114,564,18,2,100,FALSE,24.5440000000001,1.10036677,2383.790706,5703.843109,776.865951,391.596843,0,11368.827702,3408.154639,327.715989,13.345019,30,"mbank_training" +"project3938","default",1,3847,119,3416,5,1,100,TRUE,28.0219999999999,28.003850298,7657.059517,10286.657825,1092.717385,263.907318,0,5528.088936,1944.685173,217.01893,0,30,"mbank_training" +"project3938","default",2,3848,119,3408,5,1,100,TRUE,27.7280000000001,20.346678436,8009.709849,9193.549534,847.319192,396.677981,0,5963.455621,2388.873139,226.921023,0,30,"mbank_training" +"project3938","default",3,3849,119,3413,5,1,100,TRUE,27.654,25.92062548,7597.568873,8397.953192,1192.278244,267.524903,0,6370.474937,2821.517517,219.073277,160.334703,30,"mbank_training" +"project3938","default",4,3850,119,3418,5,1,100,TRUE,27.4750000000001,9.50999064,7772.234407,9643.990176,1475.966887,248.789132,0,5542.434776,2106.959977,212.515308,0,30,"mbank_training" +"project3938","default",5,3851,119,3422,5,1,100,TRUE,27.732,5.309066635,8970.183635,8855.738138,862.2458,303.566881,0,5511.693151,2297.473368,216.724037,0,30,"mbank_training" +"syab07201","default",1,3847,125,14933,3,1,3,TRUE,27.6960000000001,12.634024402,4209.831881,3971.282564,792.06249,485.70106,0,11643.179767,5614.675847,313.862167,0,30,"mbank_training" +"syab07201","default",2,3848,125,15033,4,1,1,TRUE,27.1510000000001,6.377969028,5490.40643,1351.931126,1062.927057,575.611519,0,11885.543952,6220.76002,417.368365,0,30,"mbank_training" +"syab07201","default",3,3849,125,14953,4,1,1,TRUE,27.2079999999999,19.582339775,4913.616435,2616.01053,1404.911755,502.94652,0,11247.894925,5943.98444,427.252739,0,30,"mbank_training" +"syab07201","default",4,3850,125,15017,4,1,1,TRUE,27.154,7.581652703,5078.897263,4206.671582,732.611115,532.396846,0,12075.781743,3956.822031,428.170321,0,30,"mbank_training" +"syab07201","default",5,3851,125,14926,4,1,2,TRUE,27.3429999999998,26.157566299,4773.575904,2310.009118,810.267198,599.034403,0,12631.506286,5520.778049,411.987076,0,30,"mbank_training" +"project4133","default",1,3847,131,2386,7,1,100,TRUE,27.915,15.584865867,5481.31431,8081.440573,1366.990872,340.315942,0,8473.379962,3062.261999,200.053899,0,30,"mbank_training" +"project4133","default",2,3848,131,2375,7,1,100,TRUE,29.7460000000001,29.74417613,5614.65215,9794.68452,805.954015,307.043522,0,7098.080922,3172.412796,211.646549,0,30,"mbank_training" +"project4133","default",3,3849,131,2377,8,1,100,TRUE,29.912,29.909055691,5418.41755,8613.7662,1026.893078,312.755544,0,8189.025034,3218.032499,236.792295,0,30,"mbank_training" +"project4133","default",4,3850,131,2374,7,1,100,TRUE,28.2819999999999,28.278871211,5472.009347,9335.30634,1118.641229,294.130262,0,7872.79282,2702.84137,207.013293,0,30,"mbank_training" +"project4133","default",5,3851,131,2385,8,1,100,TRUE,27.2849999999999,15.627101633,5979.695623,8990.18192,1047.259017,274.950811,0,7223.982727,3261.511037,237.368799,0,30,"mbank_training" +"project804","default",1,3847,173,1375,1,1,3,TRUE,27.7629999999999,17.248265978,5083.03061,7686.516173,873.982178,424.487131,0,10184.06465,2628.645841,184.614359,0,30,"mbank_training" +"project804","default",2,3848,173,1370,1,1,3,TRUE,30.0450000000001,30.041864633,4723.645469,9349.265054,812.927877,420.088117,0,8947.386937,2593.918773,183.881763,0,30,"mbank_training" +"project804","default",3,3849,173,1373,1,1,12,TRUE,30.056,30.051604437,6251.779509,6250.564102,1728.904108,435.905993,0,9042.788334,3203.429055,272.356204,0,30,"mbank_training" +"project804","default",4,3850,173,1387,1,1,100,TRUE,30.0989999999999,18.068643362,7075.131855,6976.566617,2091.847773,385.917478,0,7139.858341,3216.192954,174.730846,0,30,"mbank_training" +"project804","default",5,3851,173,1372,1,1,99,TRUE,30.1019999999999,15.872184522,5765.983551,5838.745921,1365.292695,412.16547,0,9596.695161,3903.152447,177.915998,0,30,"mbank_training" +"project4284","default",1,3847,4062,1268,0,1,1,TRUE,42.9269999999999,42.89428889,27450.181719,15441.009559,0,0,0,0,0,0,0,30,"mbank_training" +"project4284","default",2,3848,4062,1411,0,1,1,TRUE,40.9389999999999,40.934490269,27438.224699,13493.849822,0,0,0,0,0,0,0,30,"mbank_training" +"project4284","default",3,3849,4062,1193,0,1,1,TRUE,39.7939999999999,39.789130405,27403.434015,12382.574941,0,0,0,0,0,0,0,30,"mbank_training" +"project4284","default",4,3850,4062,1107,0,1,1,TRUE,40.596,40.592123674,27251.044937,13338.608947,0,0,0,0,0,0,0,30,"mbank_training" +"project4284","default",5,3851,4062,1360,0,1,1,TRUE,42.2569999999998,42.252403973,27459.165573,14790.786965,0,0,0,0,0,0,0,30,"mbank_training" diff --git a/dev/benchmarks/t252_mbank_60s_20260327_1135.csv b/dev/benchmarks/t252_mbank_60s_20260327_1135.csv new file mode 100644 index 000000000..da622ea80 --- /dev/null +++ b/dev/benchmarks/t252_mbank_60s_20260327_1135.csv @@ -0,0 +1,126 @@ +"dataset","strategy","replicate","seed","n_taxa","best_score","replicates","hits_to_best","pool_size","timed_out","wall_s","time_to_best_s","wagner_ms","tbr_ms","xss_ms","rss_ms","css_ms","ratchet_ms","drift_ms","final_tbr_ms","fuse_ms","budget_s","source" +"project532","default",1,3847,21,1139,7,5,4,FALSE,0.531999999999925,0.155157045,132.94095,12.896659,22.605166,15.817748,0,270.798128,54.609435,11.237556,2.086117,60,"mbank_training" +"project532","default",2,3848,21,1139,6,4,3,FALSE,0.446999999999889,0.093571746,91.262179,25.037604,16.547562,14.29948,0,240.200325,41.112086,9.448097,1.32286,60,"mbank_training" +"project532","default",3,3849,21,1139,6,5,4,FALSE,0.451999999999998,0.107800963,103.294437,17.516446,21.010614,12.231377,0,234.851555,42.029894,9.646431,1.373085,60,"mbank_training" +"project532","default",4,3850,21,1139,7,3,3,FALSE,0.560000000000173,0.151373582,105.851452,32.081296,19.716629,15.418977,0,315.724865,51.357515,11.456677,0.675031,60,"mbank_training" +"project532","default",5,3851,21,1139,6,3,2,FALSE,0.424999999999955,0.09220405,90.138613,21.852509,18.915639,13.605615,0,229.299615,35.483317,9.40638,1.447665,60,"mbank_training" +"project2346","default",1,3847,23,316,18,2,4,FALSE,0.674999999999955,0.462164759,56.807774,28.543874,26.025564,15.40238,0,425.377913,107.322736,10.523358,0.588648,60,"mbank_training" +"project2346","default",2,3848,23,318,8,2,20,FALSE,0.261999999999944,0.104624604,29.655097,8.09496,14.298628,6.572934,0,136.799606,31.645155,4.640075,0.204004,60,"mbank_training" +"project2346","default",3,3849,23,320,12,2,4,FALSE,0.388999999999896,0.104599907,41.047939,15.545544,21.653463,10.344817,0,231.190393,57.622497,6.943413,0.455207,60,"mbank_training" +"project2346","default",4,3850,23,317,17,2,13,FALSE,0.672000000000025,0.2921479,59.06303,23.600541,23.237525,14.67714,0,413.671557,104.475003,10.015255,0.229012,60,"mbank_training" +"project2346","default",5,3851,23,314,78,1,4,FALSE,3.25399999999991,1.237738072,270.848388,116.721824,120.649286,64.301568,0,2091.267241,537.485683,45.54476,1.042463,60,"mbank_training" +"project2451","default",1,3847,24,735,12,2,4,FALSE,0.380000000000109,0.195803825,58.437761,11.924619,16.795548,9.706935,0,201.186919,70.898262,6.095423,0.310965,60,"mbank_training" +"project2451","default",2,3848,24,732,67,1,1,FALSE,2.86299999999983,0.739887091,309.339849,73.561174,106.35613,47.104094,0,1682.872919,606.382824,34.560083,0,60,"mbank_training" +"project2451","default",3,3849,24,731,7,2,6,FALSE,0.213999999999942,0.08147899,36.285649,7.782122,10.81444,4.176962,0,107.266687,36.934762,3.742436,0.176081,60,"mbank_training" +"project2451","default",4,3850,24,731,74,1,3,FALSE,3.10799999999995,1.03302151,354.13203,76.234593,112.742415,50.471643,0,1835.061126,633.554558,37.608168,1.54608,60,"mbank_training" +"project2451","default",5,3851,24,730,56,1,2,FALSE,2.38400000000001,0.248315494,251.682816,76.48362,89.400481,41.476819,0,1392.188884,499.804545,30.093098,0,60,"mbank_training" +"project4501","default",1,3847,24,118,11,4,63,FALSE,0.388000000000147,0.044144614,18.206382,9.968826,10.619233,6.948279,0,123.442226,19.781661,4.620336,0.453224,60,"mbank_training" +"project4501","default",2,3848,24,118,8,6,93,FALSE,0.31899999999996,0.01872961,14.026555,6.975631,10.285114,5.881491,0,61.758493,12.974875,3.354795,0.776,60,"mbank_training" +"project4501","default",3,3849,24,118,11,8,63,FALSE,0.342000000000098,0.022986874,15.960925,11.566373,10.484639,6.290182,0,90.623341,16.135268,4.844278,1.13743,60,"mbank_training" +"project4501","default",4,3850,24,118,11,6,63,FALSE,0.385999999999967,0.047210546,16.637923,12.774526,11.017282,5.998301,0,118.138331,18.401079,4.671276,0.787301,60,"mbank_training" +"project4501","default",5,3851,24,118,9,4,93,FALSE,0.366999999999962,0.018027006,15.285145,12.046139,6.928463,5.445069,0,99.51878,17.209297,3.704213,0.634866,60,"mbank_training" +"project944","default",1,3847,25,128,7,7,60,FALSE,0.294999999999845,0.018088743,13.30662,8.38728,6.301262,4.208281,0,48.776002,9.005236,2.673461,0.830533,60,"mbank_training" +"project944","default",2,3848,25,128,6,6,60,FALSE,0.224999999999909,0.019102592,12.06331,4.438717,4.448804,3.289853,0,42.937782,7.673997,2.300673,0.821297,60,"mbank_training" +"project944","default",3,3849,25,128,6,6,60,FALSE,0.300999999999931,0.020199055,10.4648,6.232242,5.257136,3.33605,0,41.69849,8.874975,2.280816,0.85007,60,"mbank_training" +"project944","default",4,3850,25,128,8,8,60,FALSE,0.346000000000004,0.017898024,15.590799,8.054094,6.008188,4.86531,0,57.379981,12.570734,3.056505,0.871962,60,"mbank_training" +"project944","default",5,3851,25,128,9,9,60,FALSE,0.271999999999935,0.018105484,16.873965,11.336702,6.987282,5.283617,0,63.617765,10.896273,3.429023,1.41757,60,"mbank_training" +"project971_(1)","default",1,3847,26,157,7,5,100,FALSE,0.188000000000102,0.0321425,18.231222,8.017143,12.649642,6.286026,0,106.37865,16.725717,4.923929,0.854858,60,"mbank_training" +"project971_(1)","default",2,3848,26,157,7,3,100,FALSE,0.202999999999975,0.032296189,14.838464,7.174025,7.827495,6.616547,0,116.535101,17.333633,4.903698,0.776372,60,"mbank_training" +"project971_(1)","default",3,3849,26,157,14,9,100,FALSE,0.396999999999935,0.071620829,31.668768,19.663817,28.913962,13.593501,0,225.426793,31.191852,9.765443,1.439651,60,"mbank_training" +"project971_(1)","default",4,3850,26,157,9,5,100,FALSE,0.256000000000085,0.033776476,19.610728,10.628288,11.828968,8.692054,0,160.535044,25.838523,6.279481,0.742718,60,"mbank_training" +"project971_(1)","default",5,3851,26,157,8,5,100,FALSE,0.212999999999965,0.029701174,17.588101,8.384524,15.25026,7.746113,0,114.389611,20.183938,5.732391,1.160304,60,"mbank_training" +"project2762","default",1,3847,29,259,9,5,100,FALSE,0.456000000000131,0.061945926,85.191185,27.493606,26.010027,15.582262,0,213.117243,51.055173,10.07161,2.744155,60,"mbank_training" +"project2762","default",2,3848,29,259,9,5,100,FALSE,0.478000000000065,0.102143639,93.133407,20.179709,18.970153,19.004044,0,226.906961,50.594025,10.297777,2.769936,60,"mbank_training" +"project2762","default",3,3849,29,259,14,7,100,FALSE,0.793999999999869,0.238375902,161.098384,38.927341,30.230702,29.649027,0,389.337059,73.892207,16.263747,3.270378,60,"mbank_training" +"project2762","default",4,3850,29,259,7,2,100,FALSE,0.428000000000111,0.058983519,80.459811,17.423549,22.272229,15.008232,0,203.642406,42.868811,8.013146,0.555866,60,"mbank_training" +"project2762","default",5,3851,29,259,9,2,100,FALSE,0.535000000000082,0.151931487,94.256053,24.5204,23.889673,17.526596,0,281.778727,63.415303,10.419676,1.102776,60,"mbank_training" +"project826","default",1,3847,33,431,10,5,100,FALSE,0.562999999999874,0.056729766,94.04372,82.409339,32.56211,15.907697,0,235.778602,47.145061,10.064618,1.889146,60,"mbank_training" +"project826","default",2,3848,33,431,14,8,100,FALSE,0.773000000000138,0.06226196,158.0213,96.919785,38.896136,28.937695,0,349.497322,58.555463,13.803087,2.599543,60,"mbank_training" +"project826","default",3,3849,33,431,9,5,100,FALSE,0.48700000000008,0.107898042,94.132256,64.66845,31.710938,13.12036,0,216.448743,33.174753,9.034518,1.991478,60,"mbank_training" +"project826","default",4,3850,33,431,13,7,100,FALSE,0.675999999999931,0.06100803,148.574968,78.863774,43.65249,19.242854,0,286.236231,52.802974,12.941453,3.579557,60,"mbank_training" +"project826","default",5,3851,33,431,10,7,100,FALSE,0.51299999999992,0.098033702,103.843431,69.345303,26.127607,15.397387,0,220.027508,44.046429,9.939384,2.271867,60,"mbank_training" +"project561","default",1,3847,34,1169,5,4,2,FALSE,0.641999999999825,0.17492852,90.052497,24.187263,27.040114,28.734684,0,360.911661,86.894653,15.497404,1.369959,60,"mbank_training" +"project561","default",2,3848,34,1169,10,8,2,FALSE,1.2030000000002,0.177173586,138.669793,43.757383,76.882535,58.167253,0,685.538141,159.881894,30.825721,1.339312,60,"mbank_training" +"project561","default",3,3849,34,1169,6,4,2,FALSE,0.869999999999891,0.18790584,95.455449,27.965055,61.085397,31.719011,0,515.495499,109.62762,18.470351,2.695755,60,"mbank_training" +"project561","default",4,3850,34,1169,7,6,2,FALSE,0.826999999999998,0.192727156,109.19865,44.217522,38.954784,38.522289,0,453.311567,111.987842,21.865571,1.357856,60,"mbank_training" +"project561","default",5,3851,34,1169,13,10,2,FALSE,1.52800000000002,0.170371331,207.568654,73.128851,84.761045,71.265761,0,857.731735,184.962638,40.678768,0,60,"mbank_training" +"project571","default",1,3847,42,634,8,3,12,FALSE,0.671000000000049,0.157445137,102.114585,49.798648,31.506792,20.846217,0,331.649651,60.904718,12.152725,1.196162,60,"mbank_training" +"project571","default",2,3848,42,635,7,6,28,FALSE,0.619000000000142,0.086190797,97.06063,44.233453,27.674077,19.839528,0,223.541076,46.893869,10.235498,1.847647,60,"mbank_training" +"project571","default",3,3849,42,634,14,5,12,FALSE,1.22199999999998,0.196988458,179.846047,68.135679,62.309499,38.144598,0,669.29153,131.646387,20.805877,2.624811,60,"mbank_training" +"project571","default",4,3850,42,634,6,3,12,FALSE,0.484000000000151,0.102017813,93.340107,39.119923,24.874958,16.486808,0,207.705063,47.070943,9.135167,1.997872,60,"mbank_training" +"project571","default",5,3851,42,634,7,4,12,FALSE,0.546000000000049,0.091017033,101.386263,33.300991,21.79463,20.3546,0,258.555851,51.713282,10.502081,1.974336,60,"mbank_training" +"project4146_(3)","default",1,3847,59,260,88,2,100,FALSE,29.146,1.121571562,2298.172541,1541.883571,1388.448093,619.279036,0,16782.220998,6022.513703,373.627406,1.907832,60,"mbank_training" +"project4146_(3)","default",2,3848,59,262,16,2,100,FALSE,4.29300000000012,2.907800167,388.581579,296.941575,232.639383,121.909569,0,2373.682509,675.650357,68.198961,3.556906,60,"mbank_training" +"project4146_(3)","default",3,3849,59,263,11,2,100,FALSE,2.81899999999996,1.70537764,283.697704,229.458175,134.279144,72.100403,0,1519.530647,460.846893,47.369725,1.771184,60,"mbank_training" +"project4146_(3)","default",4,3850,59,261,43,1,100,FALSE,13.5260000000001,2.079365236,1173.373873,791.026839,639.12085,305.072936,0,7612.742871,2745.110794,185.134828,9.093941,60,"mbank_training" +"project4146_(3)","default",5,3851,59,261,31,2,100,FALSE,9.84099999999989,0.609325093,815.511969,534.029647,547.343539,218.158431,0,5510.730906,1946.230582,130.943191,1.776644,60,"mbank_training" +"project3688","default",1,3847,60,854,17,2,100,FALSE,3.92099999999982,3.919504781,385.501468,328.321857,206.991666,85.831002,0,1864.174767,936.303222,48.589011,0.777434,60,"mbank_training" +"project3688","default",2,3848,60,852,56,2,100,FALSE,14.6719999999998,14.670244516,1209.847499,1190.050694,689.647573,251.904609,0,7175.943495,3823.25799,153.520014,7.791968,60,"mbank_training" +"project3688","default",3,3849,60,850,86,2,100,FALSE,21.9349999999999,6.939731752,1927.898247,1467.977175,1126.604546,419.843857,0,10905.788095,5657.655153,238.271027,1.38597,60,"mbank_training" +"project3688","default",4,3850,60,845,100,0,100,FALSE,27.221,23.330009764,2294.310236,1989.385736,1275.489694,446.473982,0,13802.071859,6750.57814,283.637525,15.922755,60,"mbank_training" +"project3688","default",5,3851,60,851,100,2,100,FALSE,26.5239999999999,26.46343803,2229.919796,2008.345071,1232.32596,472.192149,0,13016.601459,6917.000715,281.497787,0,60,"mbank_training" +"project4049","default",1,3847,60,5237,50,1,24,TRUE,55.635,28.849031526,12812.355924,4022.935411,1534.967378,770.142303,0,26041.833005,8340.113622,482.876614,0,60,"mbank_training" +"project4049","default",2,3848,60,5241,14,2,100,FALSE,13.231,6.809624175,3518.238252,1155.261025,441.629723,213.68929,0,5531.891441,1538.969853,138.060614,2.902625,60,"mbank_training" +"project4049","default",3,3849,60,5237,51,0,69,TRUE,60.0050000000001,24.444532187,12896.301176,4401.928909,1452.051462,798.980615,0,25780.83288,8172.16566,480.915105,19.044584,60,"mbank_training" +"project4049","default",4,3850,60,5238,50,1,42,TRUE,57.2310000000002,0.927573556,12594.197084,4495.215782,1233.283096,798.486808,0,26471.88747,7921.979066,483.190351,0,60,"mbank_training" +"project4049","default",5,3851,60,5238,48,2,100,FALSE,51.5299999999997,35.234272169,12285.229797,3817.922871,1290.084708,807.233125,0,24705.508017,7500.001315,463.980469,5.626637,60,"mbank_training" +"project423","default",1,3847,60,495,8,4,100,FALSE,2.64699999999993,0.400347063,441.869488,149.306496,171.026613,105.804704,0,1238.875356,303.385543,56.319473,7.638977,60,"mbank_training" +"project423","default",2,3848,60,495,10,4,100,FALSE,3.02799999999979,0.356720675,484.076736,189.899313,179.44867,111.645103,0,1532.396363,369.926824,71.14467,13.962615,60,"mbank_training" +"project423","default",3,3849,60,495,9,4,100,FALSE,2.79700000000003,0.373323881,487.248487,141.088071,171.733752,90.220876,0,1283.917822,344.518464,64.250429,14.971223,60,"mbank_training" +"project423","default",4,3850,60,495,9,3,100,FALSE,3.04700000000003,0.878920563,461.192317,144.824947,139.270067,95.323255,0,1543.655094,421.459133,64.434844,9.116608,60,"mbank_training" +"project423","default",5,3851,60,495,7,3,100,FALSE,2.23999999999978,0.566558355,320.509298,86.205063,162.281775,64.508463,0,1050.605463,277.976441,49.275418,7.849402,60,"mbank_training" +"project4286","default",1,3847,63,283,29,1,100,FALSE,11.241,3.589487481,989.545223,727.212385,542.731052,270.822659,0,6261.587648,2167.214292,158.508772,11.424284,60,"mbank_training" +"project4286","default",2,3848,63,286,17,1,100,FALSE,6.8130000000001,2.866909116,617.645772,485.539661,292.257845,172.845279,0,3823.49682,1229.65814,98.085378,13.692145,60,"mbank_training" +"project4286","default",3,3849,63,282,100,0,100,FALSE,42.7640000000001,6.010016617,3526.899076,2417.887392,1721.874089,909.951811,0,24570.01672,8672.056574,543.419217,9.878051,60,"mbank_training" +"project4286","default",4,3850,63,282,36,2,100,FALSE,14.7190000000001,6.805195528,1212.868133,790.710567,583.564096,308.686255,0,8340.113943,2979.273262,196.729748,5.521758,60,"mbank_training" +"project4286","default",5,3851,63,281,100,0,100,FALSE,43.7359999999999,33.033736523,3495.468488,2532.104488,2037.564548,902.984646,0,25233.885802,8661.81827,553.655857,9.87596,60,"mbank_training" +"project4359","default",1,3847,71,183,44,14,100,FALSE,32.0450000000001,2.284122122,6608.926961,1508.127227,1179.962765,1060.524953,0,18053.52329,2758.388991,624.412424,148.430653,60,"mbank_training" +"project4359","default",2,3848,71,183,30,14,100,FALSE,17.991,0.669618659,4264.572667,1193.658269,796.706378,688.173414,0,9061.52797,1384.455348,438.30725,72.17638,60,"mbank_training" +"project4359","default",3,3849,71,183,48,14,100,FALSE,34.951,3.286649126,7525.892458,2044.580377,1323.267198,1124.803309,0,19080.732472,2870.561837,717.551942,135.330823,60,"mbank_training" +"project4359","default",4,3850,71,183,39,14,100,FALSE,24.9720000000002,4.017413013,5956.408073,1609.563323,1125.355011,908.142088,0,12957.920369,1690.329368,530.747424,99.477889,60,"mbank_training" +"project4359","default",5,3851,71,184,5,4,100,FALSE,2.68499999999995,0.670274178,618.15515,193.851391,105.988098,112.845919,0,1234.846153,232.937716,64.465129,6.273569,60,"mbank_training" +"project4397","default",1,3847,75,1645,68,1,100,TRUE,56.2779999999998,12.170124323,7991.80297,8621.763032,1577.508321,731.455933,0,25971.726563,8575.839645,531.199288,0,60,"mbank_training" +"project4397","default",2,3848,75,1647,44,2,72,FALSE,37.058,37.056632143,4958.379407,5528.590199,1114.292194,481.745149,0,15757.734049,5206.777362,340.585754,4.460428,60,"mbank_training" +"project4397","default",3,3849,75,1649,22,2,100,FALSE,16.4540000000002,16.451864336,2598.100076,2728.514627,483.544724,241.513852,0,7331.81783,2463.322895,165.851744,2.134348,60,"mbank_training" +"project4397","default",4,3850,75,1647,69,1,100,TRUE,54.3110000000001,34.441545572,7951.753605,9080.744802,1793.795614,869.29426,0,25555.851696,8210.116374,524.851391,11.542171,60,"mbank_training" +"project4397","default",5,3851,75,1646,68,1,29,TRUE,54.7840000000001,7.434980652,7951.811871,8898.509066,1817.529788,778.936639,0,25542.609159,8458.239025,548.439589,0,60,"mbank_training" +"project2084_(1)","default",1,3847,86,28962,1,1,1,TRUE,54.4740000000002,36.027487568,14979.095008,14999.501691,2975.967362,696.11225,0,15425.894194,4695.655548,410.523317,0,60,"mbank_training" +"project2084_(1)","default",2,3848,86,28206,1,1,2,TRUE,56.5880000000002,43.132102468,13181.638643,16595.40308,5997.758415,594.90442,0,11696.330144,7423.078548,256.345525,0,60,"mbank_training" +"project2084_(1)","default",3,3849,86,28303,1,1,1,TRUE,54.8099999999999,33.587438404,13343.930947,12374.806132,4238.386208,917.861064,0,19324.586019,3811.831915,333.559326,0,60,"mbank_training" +"project2084_(1)","default",4,3850,86,29022,1,1,2,TRUE,54.9989999999998,34.283512361,11256.392787,12465.419556,6071.672261,804.994572,0,19199.613915,3854.822219,513.142007,0,60,"mbank_training" +"project2084_(1)","default",5,3851,86,29028,1,1,1,TRUE,54.3580000000002,32.795953363,13459.063726,7689.930694,7774.009206,647.933228,0,20775.360569,3475.924906,262.921936,0,60,"mbank_training" +"project2771","default",1,3847,94,1061,30,1,7,TRUE,54.5080000000003,54.50748361,4055.693235,2007.917633,2703.941653,867.315002,0,29991.013706,13797.074177,578.259028,5.170318,60,"mbank_training" +"project2771","default",2,3848,94,1051,30,1,2,TRUE,54.0459999999998,4.789783894,4271.827506,1819.564166,2938.795187,815.396822,0,30148.284471,13355.075623,621.537151,27.556894,60,"mbank_training" +"project2771","default",3,3849,94,1054,30,1,7,TRUE,56.1459999999997,56.145150433,4154.5174,2329.563524,3529.689896,867.569369,0,29018.037154,13612.762975,617.564404,0,60,"mbank_training" +"project2771","default",4,3850,94,1046,30,0,6,TRUE,54.2339999999999,23.20915686,4222.126455,2096.845122,2913.642794,1085.219592,0,29918.720473,13139.276886,579.73558,51.066233,60,"mbank_training" +"project2771","default",5,3851,94,1059,29,1,1,TRUE,54.0189999999998,50.911111792,3962.747493,2194.650357,2428.450395,882.999859,0,29559.448131,14387.393034,580.596246,0,60,"mbank_training" +"project2184","default",1,3847,114,565,13,1,100,FALSE,18.6109999999999,11.402577243,2131.156542,4725.997082,530.837764,293.965445,0,8043.019541,2221.036326,273.326637,44.186869,60,"mbank_training" +"project2184","default",2,3848,114,564,8,2,100,FALSE,11.154,5.345453234,1274.511283,3095.947372,411.255146,218.503153,0,4509.715032,1206.564447,175.035752,7.404348,60,"mbank_training" +"project2184","default",3,3849,114,565,18,2,100,FALSE,26.3409999999999,16.141419283,2771.799272,6012.279972,808.007389,471.532338,0,12121.375686,3661.194424,370.384895,20.829929,60,"mbank_training" +"project2184","default",4,3850,114,563,33,0,100,TRUE,54.2220000000002,16.851939064,4867.676228,11886.042139,1552.892566,850.359378,0,26578.906698,7519.570371,675.624246,70.321069,60,"mbank_training" +"project2184","default",5,3851,114,564,18,2,100,FALSE,28.5320000000002,1.17403699,2732.189783,6746.666943,896.459513,454.621275,0,13185.943663,3950.044113,370.900718,16.416084,60,"mbank_training" +"project3938","default",1,3847,119,3417,9,2,100,FALSE,51.2469999999998,21.493213813,13097.014635,17240.880122,2577.814748,685.203831,0,12611.715252,4025.969422,457.683269,30.401117,60,"mbank_training" +"project3938","default",2,3848,119,3408,8,1,100,TRUE,54.9359999999997,22.446101436,14280.167284,17485.494785,2503.527778,693.394689,0,13934.462712,4716.448387,413.765958,0,60,"mbank_training" +"project3938","default",3,3849,119,3413,9,2,100,FALSE,53.6620000000003,29.438755385,13646.266789,16273.772787,1957.851645,607.939928,0,14071.340355,5282.540219,453.269932,204.081741,60,"mbank_training" +"project3938","default",4,3850,119,3409,9,1,100,TRUE,54.4050000000002,32.279967283,14393.190366,16570.649742,3077.911077,580.782538,0,14041.999764,4926.950212,444.543922,0,60,"mbank_training" +"project3938","default",5,3851,119,3410,9,0,100,TRUE,55.8039999999996,54.032769174,15041.883919,18403.466552,1289.59538,761.434192,0,13465.906008,4532.486313,469.045411,66.780611,60,"mbank_training" +"syab07201","default",1,3847,125,14933,6,1,3,TRUE,54.5649999999996,13.463024632,8283.224612,5422.107356,2155.406468,975.963841,0,23496.45208,12990.29354,720.755871,0,60,"mbank_training" +"syab07201","default",2,3848,125,15033,6,1,1,TRUE,54.1619999999998,6.879759994,8631.661473,4556.603853,1981.947232,1083.600626,0,24835.027048,12258.982609,667.987072,0,60,"mbank_training" +"syab07201","default",3,3849,125,14953,6,1,1,TRUE,54.3450000000003,23.402911157,8807.088314,4685.148042,2305.728683,1070.541614,0,25444.06322,11129.258295,704.90096,0,60,"mbank_training" +"syab07201","default",4,3850,125,15017,6,1,2,TRUE,54.4230000000002,9.162811337,8130.311725,5679.011914,2400.763562,1039.644438,0,25123.454571,10902.237259,753.185253,0,60,"mbank_training" +"syab07201","default",5,3851,125,14926,6,1,2,TRUE,54.5190000000002,31.986010941,7983.21979,6412.495985,1702.238379,1285.062244,0,25779.764873,10200.97239,729.078123,0,60,"mbank_training" +"project4133","default",1,3847,131,2373,12,1,100,TRUE,55.0460000000003,50.763268092,9777.261701,16367.982627,2105.14162,590.469161,0,17587.748365,7183.47726,410.978192,0,60,"mbank_training" +"project4133","default",2,3848,131,2378,12,1,100,TRUE,56.9690000000001,56.966936068,10115.779173,17903.407013,1533.860302,741.545257,0,15572.04862,7743.139666,419.157453,0,60,"mbank_training" +"project4133","default",3,3849,131,2377,12,1,100,TRUE,54.7849999999999,54.782788502,10360.897605,15017.756517,2720.598684,644.129521,0,17636.426464,7206.39643,420.041532,0,60,"mbank_training" +"project4133","default",4,3850,131,2371,13,1,100,TRUE,56.2750000000001,56.271602815,10099.90732,17124.399453,1715.449085,535.285948,0,17256.680013,6864.383411,406.018243,0,60,"mbank_training" +"project4133","default",5,3851,131,2376,12,1,100,TRUE,55.3900000000003,55.386705398,10123.53186,16744.885155,2063.074556,809.123932,0,17128.859433,6715.284436,414.796312,0,60,"mbank_training" +"project804","default",1,3847,173,1375,3,1,47,TRUE,60.0190000000002,21.23623549,11228.226484,12303.850331,2430.559834,830.477995,0,17909.169516,8717.022799,708.078116,0,60,"mbank_training" +"project804","default",2,3848,173,1368,3,1,100,TRUE,60.1500000000005,60.080988944,10396.680621,17198.177502,1861.84538,753.302697,0,16601.247605,6676.313676,571.262477,0,60,"mbank_training" +"project804","default",3,3849,173,1373,2,1,86,TRUE,60.0889999999999,60.066671998,9195.650252,12590.645613,3033.072112,755.479002,0,20511.079276,7269.364064,711.281004,0,60,"mbank_training" +"project804","default",4,3850,173,1387,3,1,100,TRUE,60.0789999999997,22.113473518,12998.737773,12270.630953,4290.703251,689.04819,0,15290.848349,7909.329051,669.759235,0,60,"mbank_training" +"project804","default",5,3851,173,1370,3,1,7,TRUE,60.0300000000007,60.022688033,13480.410099,11992.159228,3052.961199,727.439951,0,16306.47525,8248.843774,634.589284,0,60,"mbank_training" +"project4284","default",1,3847,4062,1268,0,1,1,TRUE,75.3469999999998,75.339956447,54305.100757,21032.114597,0,0,0,0,0,0,0,60,"mbank_training" +"project4284","default",2,3848,4062,1407,0,1,1,TRUE,72.5429999999997,72.536462982,54305.789977,18227.270056,0,0,0,0,0,0,0,60,"mbank_training" +"project4284","default",3,3849,4062,1193,0,1,1,TRUE,70.0289999999995,70.021119683,54540.924025,15477.382811,0,0,0,0,0,0,0,60,"mbank_training" +"project4284","default",4,3850,4062,1107,0,1,1,TRUE,70.415,70.408575744,54275.210143,16129.911666,0,0,0,0,0,0,0,60,"mbank_training" +"project4284","default",5,3851,4062,1355,0,1,1,TRUE,74.0500000000002,74.042071236,54236.199289,19803.278572,0,0,0,0,0,0,0,60,"mbank_training" diff --git a/dev/benchmarks/t252_mbank_all_20260327_1317.csv b/dev/benchmarks/t252_mbank_all_20260327_1317.csv new file mode 100644 index 000000000..5f5cc6c46 --- /dev/null +++ b/dev/benchmarks/t252_mbank_all_20260327_1317.csv @@ -0,0 +1,376 @@ +"dataset","strategy","replicate","seed","n_taxa","best_score","replicates","hits_to_best","pool_size","timed_out","wall_s","time_to_best_s","wagner_ms","tbr_ms","xss_ms","rss_ms","css_ms","ratchet_ms","drift_ms","final_tbr_ms","fuse_ms","budget_s","source" +"project532","default",1,3847,21,1139,7,5,4,FALSE,0.542,0.159055272,135.619212,13.04573,22.272809,15.919137,0,276.407229,56.168931,11.423204,2.049818,30,"mbank_training" +"project532","default",2,3848,21,1139,6,4,3,FALSE,0.454999999999999,0.094952955,92.239999,25.374239,16.837307,14.426449,0,245.246002,42.116067,9.629668,1.382472,30,"mbank_training" +"project532","default",3,3849,21,1139,6,5,4,FALSE,0.475000000000001,0.115830358,107.415156,18.13517,22.390323,12.792332,0,248.020245,43.648871,9.998143,1.511787,30,"mbank_training" +"project532","default",4,3850,21,1139,7,3,3,FALSE,0.583,0.158395851,110.868678,33.604432,20.556298,16.136819,0,327.756102,53.823888,12.069131,0.705788,30,"mbank_training" +"project532","default",5,3851,21,1139,6,3,2,FALSE,0.443000000000001,0.095074113,94.240283,22.942882,19.845399,14.211134,0,237.886468,36.744354,9.808866,1.523488,30,"mbank_training" +"project2346","default",1,3847,23,316,18,2,4,FALSE,0.706,0.483245432,59.526585,29.639617,27.097784,15.987328,0,445.189137,111.983257,10.955062,0.615469,30,"mbank_training" +"project2346","default",2,3848,23,318,8,2,20,FALSE,0.272,0.107900119,30.632818,8.388472,14.761471,6.777368,0,142.381512,32.831868,4.816267,0.208482,30,"mbank_training" +"project2346","default",3,3849,23,320,12,2,4,FALSE,0.401999999999999,0.107059556,42.565554,15.985884,22.632968,10.643807,0,238.422067,59.348087,7.143847,0.571676,30,"mbank_training" +"project2346","default",4,3850,23,317,17,2,13,FALSE,0.695,0.302030974,61.210236,24.433465,24.273697,15.448662,0,427.301713,107.544701,10.352768,0.259899,30,"mbank_training" +"project2346","default",5,3851,23,314,78,1,4,FALSE,3.36,1.270754501,279.259769,120.36549,125.623277,66.597026,0,2160.387218,553.280991,47.067397,1.0782,30,"mbank_training" +"project2451","default",1,3847,24,735,12,2,4,FALSE,0.391,0.201359523,57.55009,12.458124,17.440524,10.013999,0,208.719441,73.761881,6.301613,0.352573,30,"mbank_training" +"project2451","default",2,3848,24,732,67,1,1,FALSE,2.958,0.754287847,321.511476,75.820549,109.558619,48.42972,0,1739.621241,624.549235,35.606072,0,30,"mbank_training" +"project2451","default",3,3849,24,731,7,2,6,FALSE,0.221999999999998,0.083935985,38.627521,8.011942,11.150373,4.288323,0,111.132705,37.786174,3.844417,0.195688,30,"mbank_training" +"project2451","default",4,3850,24,731,74,1,3,FALSE,3.226,1.072942617,382.702166,78.858534,117.675578,52.337654,0,1893.503165,653.858183,39.018316,1.668751,30,"mbank_training" +"project2451","default",5,3851,24,730,56,1,2,FALSE,2.475,0.259627507,272.084943,78.715156,92.241695,42.774133,0,1441.742098,513.345106,30.965669,0,30,"mbank_training" +"project4501","default",1,3847,24,118,11,4,63,FALSE,0.402000000000001,0.045659888,19.456467,10.321312,11.249257,7.273704,0,127.024403,20.262034,4.693674,0.592876,30,"mbank_training" +"project4501","default",2,3848,24,118,8,6,93,FALSE,0.331000000000003,0.019307548,14.526607,7.223871,10.667763,6.112406,0,63.857366,13.417429,3.468459,0.840874,30,"mbank_training" +"project4501","default",3,3849,24,118,11,8,63,FALSE,0.353999999999999,0.024017324,16.544606,12.054514,10.874954,6.541294,0,94.229752,16.766104,5.012416,1.208605,30,"mbank_training" +"project4501","default",4,3850,24,118,11,6,63,FALSE,0.399999999999999,0.049086008,17.238983,13.324074,11.375175,6.189715,0,122.860359,19.102503,4.840804,0.788614,30,"mbank_training" +"project4501","default",5,3851,24,118,9,4,93,FALSE,0.378,0.018508023,15.81334,12.376079,7.116948,5.60964,0,102.823873,17.718845,3.840052,0.661435,30,"mbank_training" +"project944","default",1,3847,25,128,7,7,60,FALSE,0.306999999999999,0.018572714,13.799929,8.682096,6.631403,4.331374,0,50.309772,9.260855,2.764333,0.884165,30,"mbank_training" +"project944","default",2,3848,25,128,6,6,60,FALSE,0.233000000000001,0.019885466,12.479662,4.603999,4.628432,3.361849,0,44.265201,7.877521,2.368148,0.873353,30,"mbank_training" +"project944","default",3,3849,25,128,6,6,60,FALSE,0.314,0.020729454,10.93727,6.439242,5.535302,3.476875,0,43.362833,9.229105,2.367437,0.930562,30,"mbank_training" +"project944","default",4,3850,25,128,8,8,60,FALSE,0.356999999999999,0.018798128,16.221829,8.35613,6.25217,5.100312,0,59.507827,13.044878,3.160491,0.944067,30,"mbank_training" +"project944","default",5,3851,25,128,9,9,60,FALSE,0.276999999999997,0.018958742,17.363908,11.685671,7.165347,5.454358,0,65.433255,11.16655,3.531729,1.479684,30,"mbank_training" +"project971_(1)","default",1,3847,26,157,7,5,100,FALSE,0.193000000000001,0.032616012,18.605586,8.255522,13.045921,6.466022,0,108.291193,17.142443,5.032654,0.93516,30,"mbank_training" +"project971_(1)","default",2,3848,26,157,7,3,100,FALSE,0.209,0.033034109,15.300445,7.423335,8.066606,6.895672,0,120.329215,17.930359,5.086765,0.809634,30,"mbank_training" +"project971_(1)","default",3,3849,26,157,14,9,100,FALSE,0.411000000000001,0.073843174,32.755924,20.344981,29.768159,14.029603,0,232.580428,32.34325,10.062332,1.52913,30,"mbank_training" +"project971_(1)","default",4,3850,26,157,9,5,100,FALSE,0.266000000000002,0.034971856,20.398181,11.072927,12.306986,9.028718,0,166.869904,26.803601,6.538127,0.79671,30,"mbank_training" +"project971_(1)","default",5,3851,26,157,8,5,100,FALSE,0.220000000000002,0.030588355,18.153386,8.61647,15.762383,8.048763,0,118.243181,20.804155,5.864089,1.237399,30,"mbank_training" +"project2762","default",1,3847,29,259,9,5,100,FALSE,0.477,0.064657482,89.005679,28.625208,27.015199,16.271522,0,222.30784,53.258133,10.549338,2.931178,30,"mbank_training" +"project2762","default",2,3848,29,259,9,5,100,FALSE,0.500999999999998,0.107338091,97.623433,21.280192,19.892007,19.848566,0,237.460345,52.994316,10.82002,2.92123,30,"mbank_training" +"project2762","default",3,3849,29,259,14,7,100,FALSE,0.824000000000002,0.248402003,167.013281,40.401138,31.5735,30.962567,0,404.788011,76.685877,16.82874,3.46784,30,"mbank_training" +"project2762","default",4,3850,29,259,7,2,100,FALSE,0.442,0.060617737,82.572548,17.849112,22.881476,15.476914,0,210.443998,44.501366,8.239273,0.595401,30,"mbank_training" +"project2762","default",5,3851,29,259,9,2,100,FALSE,0.549999999999997,0.156776252,96.810243,25.176484,24.499914,18.026146,0,289.442735,65.42589,10.713507,1.121812,30,"mbank_training" +"project826","default",1,3847,33,431,10,5,100,FALSE,0.585000000000001,0.058192461,97.322235,84.950343,36.014361,16.87607,0,243.540672,48.527945,10.402926,2.289268,30,"mbank_training" +"project826","default",2,3848,33,431,14,8,100,FALSE,0.815000000000001,0.064265745,164.329903,100.342802,40.22171,29.986892,0,362.511776,60.62095,14.329798,2.752712,30,"mbank_training" +"project826","default",3,3849,33,431,9,5,100,FALSE,0.506,0.112018993,97.964377,66.801407,33.044869,13.567502,0,224.709019,34.377508,9.366282,2.14059,30,"mbank_training" +"project826","default",4,3850,33,431,13,7,100,FALSE,0.698,0.062873383,154.074276,81.270968,45.052406,19.77621,0,295.134169,54.289724,13.291651,3.70243,30,"mbank_training" +"project826","default",5,3851,33,431,10,7,100,FALSE,0.533000000000001,0.1020999,107.928141,72.157363,27.216947,15.95665,0,228.789413,45.677109,10.315738,2.329386,30,"mbank_training" +"project561","default",1,3847,34,1169,5,4,2,FALSE,0.659999999999997,0.181347639,92.846159,25.059065,28.207322,29.981203,0,370.359995,88.552929,15.979782,1.446704,30,"mbank_training" +"project561","default",2,3848,34,1169,10,8,2,FALSE,1.232,0.18098681,142.576079,44.374205,78.560658,59.124055,0,702.572509,163.908009,31.657059,1.392722,30,"mbank_training" +"project561","default",3,3849,34,1169,6,4,2,FALSE,0.892000000000003,0.193806133,98.658642,28.550289,63.174951,32.545117,0,527.190542,111.328013,20.332498,2.808147,30,"mbank_training" +"project561","default",4,3850,34,1169,7,6,2,FALSE,0.850000000000001,0.196609952,112.865658,45.375572,39.933657,39.492137,0,466.18721,115.350405,22.395089,1.381331,30,"mbank_training" +"project561","default",5,3851,34,1169,13,10,2,FALSE,1.577,0.176815928,214.233348,75.676926,87.389599,73.393768,0,887.255078,189.841499,42.006711,0,30,"mbank_training" +"project571","default",1,3847,42,634,8,3,12,FALSE,0.694000000000003,0.164095501,106.218011,51.333049,32.312008,21.394067,0,342.296416,63.619378,12.651639,1.096264,30,"mbank_training" +"project571","default",2,3848,42,635,7,6,28,FALSE,0.639000000000003,0.088679535,100.785177,45.952629,28.371058,20.286232,0,230.646768,48.366994,10.52306,1.873838,30,"mbank_training" +"project571","default",3,3849,42,634,14,5,12,FALSE,1.271,0.205375594,186.209453,70.512624,64.876695,39.745794,0,696.746744,136.711756,21.663235,2.899548,30,"mbank_training" +"project571","default",4,3850,42,634,6,3,12,FALSE,0.503999999999998,0.106289547,97.153147,40.469948,25.720071,17.059675,0,215.716522,48.979746,9.500026,2.09783,30,"mbank_training" +"project571","default",5,3851,42,634,7,4,12,FALSE,0.562000000000005,0.094291991,104.624002,34.374362,22.712617,21.010924,0,265.769841,52.724237,10.747413,2.061411,30,"mbank_training" +"project4146_(3)","default",1,3847,59,260,79,1,100,TRUE,27.184,1.159421069,2161.951899,1431.436231,1369.737579,584.938795,0,15612.088541,5495.045514,342.372656,0,30,"mbank_training" +"project4146_(3)","default",2,3848,59,262,16,2,100,FALSE,4.43300000000001,3.006056794,398.978693,305.709476,242.038821,126.156641,0,2449.890491,699.98229,70.565506,3.637498,30,"mbank_training" +"project4146_(3)","default",3,3849,59,263,11,2,100,FALSE,2.911,1.75921419,292.432282,236.70639,138.23963,74.826225,0,1569.189041,477.95843,49.093865,1.822251,30,"mbank_training" +"project4146_(3)","default",4,3850,59,261,43,1,100,FALSE,13.781,2.134428645,1195.778958,805.816844,652.279548,310.548343,0,7750.977861,2798.563687,188.875727,9.304356,30,"mbank_training" +"project4146_(3)","default",5,3851,59,261,31,2,100,FALSE,10.048,0.622684871,834.240637,543.209237,558.784365,222.447677,0,5626.288589,1986.738636,134.143195,1.677448,30,"mbank_training" +"project3688","default",1,3847,60,854,17,2,100,FALSE,4.04899999999999,4.046715439,408.411651,338.435934,212.952407,88.730993,0,1919.553444,962.191821,49.845781,0.793184,30,"mbank_training" +"project3688","default",2,3848,60,852,56,2,100,FALSE,15.176,15.173541808,1302.515289,1226.872642,710.476017,259.35943,0,7396.027813,3937.753164,158.084343,7.912235,30,"mbank_training" +"project3688","default",3,3849,60,850,86,2,100,FALSE,22.625,7.183552468,2072.997198,1511.265434,1160.574252,431.875451,0,11185.916283,5821.436102,245.879774,1.441333,30,"mbank_training" +"project3688","default",4,3850,60,845,100,0,100,FALSE,26.996,23.186208158,2348.914977,2049.502682,1311.093182,459.592175,0,13408.741898,6965.596352,294.003592,7.587966,30,"mbank_training" +"project3688","default",5,3851,60,846,99,2,11,TRUE,27.29,27.28811545,2406.499645,2060.393789,1264.02288,485.95709,0,13418.549013,7075.465949,287.115493,0,30,"mbank_training" +"project4049","default",1,3847,60,5240,26,1,100,TRUE,27.317,5.573148428,6703.751431,2176.123735,825.960021,396.113765,0,12768.91305,3880.674858,253.963364,0,30,"mbank_training" +"project4049","default",2,3848,60,5241,14,2,100,FALSE,13.655,7.027287931,3626.086034,1198.572419,456.608051,220.963331,0,5704.360758,1589.957546,142.553627,3.045504,30,"mbank_training" +"project4049","default",3,3849,60,5237,25,0,69,TRUE,30.007,24.853505039,6743.164871,2194.709548,693.107501,428.940065,0,12666.958798,4006.547911,248.048225,20.524267,30,"mbank_training" +"project4049","default",4,3850,60,5238,25,1,42,TRUE,30.004,0.932033283,6776.515792,2319.610345,684.456542,434.411469,0,12776.817935,3766.735394,249.499943,0,30,"mbank_training" +"project4049","default",5,3851,60,5239,25,1,100,TRUE,27.486,3.676459796,6617.98495,2189.03596,703.300419,449.992373,0,12985.569232,3805.079674,248.978194,0,30,"mbank_training" +"project423","default",1,3847,60,495,8,4,100,FALSE,2.75400000000002,0.419124173,456.884432,154.343943,179.912009,110.096176,0,1290.272654,315.113496,59.03124,8.115354,30,"mbank_training" +"project423","default",2,3848,60,495,10,4,100,FALSE,3.13900000000001,0.370053092,496.594593,197.924011,185.722131,115.789679,0,1590.611775,385.967567,74.139425,14.305423,30,"mbank_training" +"project423","default",3,3849,60,495,9,4,100,FALSE,2.88499999999999,0.385641831,500.870846,145.446436,177.377975,93.716956,0,1323.701525,355.745644,66.297395,15.357013,30,"mbank_training" +"project423","default",4,3850,60,495,9,3,100,FALSE,3.17099999999999,0.913398284,477.038761,151.875211,145.583234,99.231705,0,1607.287212,441.466933,67.048899,9.279487,30,"mbank_training" +"project423","default",5,3851,60,495,7,3,100,FALSE,2.30000000000001,0.587509882,327.409848,89.159324,165.845635,66.60812,0,1078.061213,286.696336,50.646708,8.055533,30,"mbank_training" +"project4286","default",1,3847,63,283,29,1,100,FALSE,10.679,3.568361219,949.758854,695.981939,507.727922,255.192069,0,5968.563535,2042.300499,151.240761,10.412181,30,"mbank_training" +"project4286","default",2,3848,63,286,17,1,100,FALSE,5.93299999999999,2.493855935,547.902595,427.93154,253.562543,148.533339,0,3300.116545,1089.177478,84.825461,12.413171,30,"mbank_training" +"project4286","default",3,3849,63,282,69,0,100,TRUE,27.162,5.203139408,2360.989445,1512.558401,1088.458169,571.79875,0,15666.679256,5442.170076,346.111242,8.470814,30,"mbank_training" +"project4286","default",4,3850,63,282,36,2,100,FALSE,13.267,6.209710639,1092.563694,721.114448,524.582188,278.114164,0,7555.478801,2692.809987,176.91012,4.096113,30,"mbank_training" +"project4286","default",5,3851,63,283,70,1,100,TRUE,27.169,1.557344311,2287.207347,1582.845371,1211.075903,547.191397,0,15688.016025,5332.93518,347.612348,0,30,"mbank_training" +"project4359","default",1,3847,71,183,41,13,100,TRUE,27.097,2.261897091,5771.353119,1305.011338,1006.712715,912.447879,0,15026.53261,2340.667863,522.209948,117.847876,30,"mbank_training" +"project4359","default",2,3848,71,183,30,14,100,FALSE,16.129,0.665570597,3928.068751,1053.953565,706.859134,611.186316,0,8062.325089,1227.655533,389.882129,64.610764,30,"mbank_training" +"project4359","default",3,3849,71,183,43,13,100,TRUE,27.09,3.114505389,6132.139298,1549.203645,1017.246691,869.207429,0,14564.715459,2203.509034,547.569478,96.465078,30,"mbank_training" +"project4359","default",4,3850,71,183,39,14,100,FALSE,23.331,3.776490508,5602.272217,1470.537157,1043.605592,841.264513,0,12109.540516,1584.418474,494.61431,93.977932,30,"mbank_training" +"project4359","default",5,3851,71,184,5,4,100,FALSE,2.51499999999999,0.625239003,581.307694,181.271259,99.713473,106.22802,0,1151.351682,219.65755,60.431613,5.864753,30,"mbank_training" +"project4397","default",1,3847,75,1645,39,1,84,TRUE,29.898,11.652682336,4059.293835,4611.243157,859.16792,378.941198,0,12614.467637,4211.561709,269.027083,0,30,"mbank_training" +"project4397","default",2,3848,75,1648,40,1,11,TRUE,27.222,16.275268683,4083.297886,4499.993494,922.733936,399.403983,0,12638.777186,4181.619232,271.886856,1.900378,30,"mbank_training" +"project4397","default",3,3849,75,1649,22,2,100,FALSE,14.671,14.669488113,2326.940955,2439.062281,438.000839,215.523963,0,6522.444608,2178.359279,147.225135,1.934383,30,"mbank_training" +"project4397","default",4,3850,75,1648,40,1,100,TRUE,27.3430000000001,10.737418532,4263.486964,4441.437048,998.363541,456.623285,0,12534.551377,4024.735858,269.57219,10.386157,30,"mbank_training" +"project4397","default",5,3851,75,1646,39,1,42,TRUE,28.025,6.965588083,4076.471754,4722.078731,843.218866,405.213474,0,12542.741242,4122.793808,285.67242,0,30,"mbank_training" +"project2084_(1)","default",1,3847,86,28962,0,1,1,TRUE,27.2950000000001,27.284871037,6087.152752,7601.569896,1703.323901,292.649511,0,10154.252001,1190.248423,0,0,30,"mbank_training" +"project2084_(1)","default",2,3848,86,28206,0,1,2,TRUE,27.893,27.88159824,6449.215962,9863.875036,2809.440548,508.882523,0,7554.658108,0,0,0,30,"mbank_training" +"project2084_(1)","default",3,3849,86,28303,0,1,1,TRUE,27.3520000000001,27.341604691,5532.892173,4659.700576,2729.695298,369.901689,0,10917.922654,2879.327597,0,0,30,"mbank_training" +"project2084_(1)","default",4,3850,86,29263,0,1,1,TRUE,27.337,27.32739407,5136.496018,4624.353351,2476.729143,305.33289,0,12907.034261,1626.271363,0,0,30,"mbank_training" +"project2084_(1)","default",5,3851,86,29028,0,1,1,TRUE,27.277,27.265355976,6452.248698,4808.151115,2003.218686,324.815519,0,10753.100827,2544.773109,129.969162,0,30,"mbank_training" +"project2771","default",1,3847,94,1061,18,1,2,TRUE,27.628,27.627539078,2381.315256,1179.557903,1473.537564,476.722015,0,14814.327106,6359.277975,317.095031,4.795324,30,"mbank_training" +"project2771","default",2,3848,94,1051,18,1,2,TRUE,27.056,4.598189444,2329.759595,1058.876302,1523.15815,437.623557,0,14578.550452,6707.850826,347.023291,25.508291,30,"mbank_training" +"project2771","default",3,3849,94,1055,18,1,25,TRUE,28.596,28.595855406,2208.668083,1237.955741,1885.302937,461.560814,0,14342.634134,6721.423525,318.360851,0,30,"mbank_training" +"project2771","default",4,3850,94,1046,18,0,6,TRUE,27.212,20.807814846,2244.376253,996.635736,1387.986264,635.028035,0,14958.245088,6429.43522,307.637888,39.061989,30,"mbank_training" +"project2771","default",5,3851,94,1061,18,1,83,TRUE,30.005,30.00408473,2175.970571,1277.59992,1288.181195,517.295524,0,14521.506037,6925.085047,312.43618,0,30,"mbank_training" +"project2184","default",1,3847,114,565,13,1,100,FALSE,16.5749999999999,10.308158943,1901.553612,4214.328218,485.080766,269.267477,0,7183.460842,1969.620003,238.019628,35.13861,30,"mbank_training" +"project2184","default",2,3848,114,564,8,2,100,FALSE,9.25199999999995,4.431088219,1068.876064,2542.898349,345.815564,183.719232,0,3766.096167,990.361813,142.478229,6.554553,30,"mbank_training" +"project2184","default",3,3849,114,565,18,2,100,FALSE,23.623,14.129842318,2496.19712,5294.070284,723.003063,423.243246,0,10922.706838,3311.749457,329.429608,19.212075,30,"mbank_training" +"project2184","default",4,3850,114,563,19,0,100,TRUE,27.1590000000001,15.437353718,2783.61853,6176.676207,832.030467,454.22667,0,12537.971451,3609.087835,353.928239,64.443171,30,"mbank_training" +"project2184","default",5,3851,114,564,18,2,100,FALSE,24.5440000000001,1.10036677,2383.790706,5703.843109,776.865951,391.596843,0,11368.827702,3408.154639,327.715989,13.345019,30,"mbank_training" +"project3938","default",1,3847,119,3416,5,1,100,TRUE,28.0219999999999,28.003850298,7657.059517,10286.657825,1092.717385,263.907318,0,5528.088936,1944.685173,217.01893,0,30,"mbank_training" +"project3938","default",2,3848,119,3408,5,1,100,TRUE,27.7280000000001,20.346678436,8009.709849,9193.549534,847.319192,396.677981,0,5963.455621,2388.873139,226.921023,0,30,"mbank_training" +"project3938","default",3,3849,119,3413,5,1,100,TRUE,27.654,25.92062548,7597.568873,8397.953192,1192.278244,267.524903,0,6370.474937,2821.517517,219.073277,160.334703,30,"mbank_training" +"project3938","default",4,3850,119,3418,5,1,100,TRUE,27.4750000000001,9.50999064,7772.234407,9643.990176,1475.966887,248.789132,0,5542.434776,2106.959977,212.515308,0,30,"mbank_training" +"project3938","default",5,3851,119,3422,5,1,100,TRUE,27.732,5.309066635,8970.183635,8855.738138,862.2458,303.566881,0,5511.693151,2297.473368,216.724037,0,30,"mbank_training" +"syab07201","default",1,3847,125,14933,3,1,3,TRUE,27.6960000000001,12.634024402,4209.831881,3971.282564,792.06249,485.70106,0,11643.179767,5614.675847,313.862167,0,30,"mbank_training" +"syab07201","default",2,3848,125,15033,4,1,1,TRUE,27.1510000000001,6.377969028,5490.40643,1351.931126,1062.927057,575.611519,0,11885.543952,6220.76002,417.368365,0,30,"mbank_training" +"syab07201","default",3,3849,125,14953,4,1,1,TRUE,27.2079999999999,19.582339775,4913.616435,2616.01053,1404.911755,502.94652,0,11247.894925,5943.98444,427.252739,0,30,"mbank_training" +"syab07201","default",4,3850,125,15017,4,1,1,TRUE,27.154,7.581652703,5078.897263,4206.671582,732.611115,532.396846,0,12075.781743,3956.822031,428.170321,0,30,"mbank_training" +"syab07201","default",5,3851,125,14926,4,1,2,TRUE,27.3429999999998,26.157566299,4773.575904,2310.009118,810.267198,599.034403,0,12631.506286,5520.778049,411.987076,0,30,"mbank_training" +"project4133","default",1,3847,131,2386,7,1,100,TRUE,27.915,15.584865867,5481.31431,8081.440573,1366.990872,340.315942,0,8473.379962,3062.261999,200.053899,0,30,"mbank_training" +"project4133","default",2,3848,131,2375,7,1,100,TRUE,29.7460000000001,29.74417613,5614.65215,9794.68452,805.954015,307.043522,0,7098.080922,3172.412796,211.646549,0,30,"mbank_training" +"project4133","default",3,3849,131,2377,8,1,100,TRUE,29.912,29.909055691,5418.41755,8613.7662,1026.893078,312.755544,0,8189.025034,3218.032499,236.792295,0,30,"mbank_training" +"project4133","default",4,3850,131,2374,7,1,100,TRUE,28.2819999999999,28.278871211,5472.009347,9335.30634,1118.641229,294.130262,0,7872.79282,2702.84137,207.013293,0,30,"mbank_training" +"project4133","default",5,3851,131,2385,8,1,100,TRUE,27.2849999999999,15.627101633,5979.695623,8990.18192,1047.259017,274.950811,0,7223.982727,3261.511037,237.368799,0,30,"mbank_training" +"project804","default",1,3847,173,1375,1,1,3,TRUE,27.7629999999999,17.248265978,5083.03061,7686.516173,873.982178,424.487131,0,10184.06465,2628.645841,184.614359,0,30,"mbank_training" +"project804","default",2,3848,173,1370,1,1,3,TRUE,30.0450000000001,30.041864633,4723.645469,9349.265054,812.927877,420.088117,0,8947.386937,2593.918773,183.881763,0,30,"mbank_training" +"project804","default",3,3849,173,1373,1,1,12,TRUE,30.056,30.051604437,6251.779509,6250.564102,1728.904108,435.905993,0,9042.788334,3203.429055,272.356204,0,30,"mbank_training" +"project804","default",4,3850,173,1387,1,1,100,TRUE,30.0989999999999,18.068643362,7075.131855,6976.566617,2091.847773,385.917478,0,7139.858341,3216.192954,174.730846,0,30,"mbank_training" +"project804","default",5,3851,173,1372,1,1,99,TRUE,30.1019999999999,15.872184522,5765.983551,5838.745921,1365.292695,412.16547,0,9596.695161,3903.152447,177.915998,0,30,"mbank_training" +"project4284","default",1,3847,4062,1268,0,1,1,TRUE,42.9269999999999,42.89428889,27450.181719,15441.009559,0,0,0,0,0,0,0,30,"mbank_training" +"project4284","default",2,3848,4062,1411,0,1,1,TRUE,40.9389999999999,40.934490269,27438.224699,13493.849822,0,0,0,0,0,0,0,30,"mbank_training" +"project4284","default",3,3849,4062,1193,0,1,1,TRUE,39.7939999999999,39.789130405,27403.434015,12382.574941,0,0,0,0,0,0,0,30,"mbank_training" +"project4284","default",4,3850,4062,1107,0,1,1,TRUE,40.596,40.592123674,27251.044937,13338.608947,0,0,0,0,0,0,0,30,"mbank_training" +"project4284","default",5,3851,4062,1360,0,1,1,TRUE,42.2569999999998,42.252403973,27459.165573,14790.786965,0,0,0,0,0,0,0,30,"mbank_training" +"project532","default",1,3847,21,1139,7,5,4,FALSE,0.531999999999925,0.155157045,132.94095,12.896659,22.605166,15.817748,0,270.798128,54.609435,11.237556,2.086117,60,"mbank_training" +"project532","default",2,3848,21,1139,6,4,3,FALSE,0.446999999999889,0.093571746,91.262179,25.037604,16.547562,14.29948,0,240.200325,41.112086,9.448097,1.32286,60,"mbank_training" +"project532","default",3,3849,21,1139,6,5,4,FALSE,0.451999999999998,0.107800963,103.294437,17.516446,21.010614,12.231377,0,234.851555,42.029894,9.646431,1.373085,60,"mbank_training" +"project532","default",4,3850,21,1139,7,3,3,FALSE,0.560000000000173,0.151373582,105.851452,32.081296,19.716629,15.418977,0,315.724865,51.357515,11.456677,0.675031,60,"mbank_training" +"project532","default",5,3851,21,1139,6,3,2,FALSE,0.424999999999955,0.09220405,90.138613,21.852509,18.915639,13.605615,0,229.299615,35.483317,9.40638,1.447665,60,"mbank_training" +"project2346","default",1,3847,23,316,18,2,4,FALSE,0.674999999999955,0.462164759,56.807774,28.543874,26.025564,15.40238,0,425.377913,107.322736,10.523358,0.588648,60,"mbank_training" +"project2346","default",2,3848,23,318,8,2,20,FALSE,0.261999999999944,0.104624604,29.655097,8.09496,14.298628,6.572934,0,136.799606,31.645155,4.640075,0.204004,60,"mbank_training" +"project2346","default",3,3849,23,320,12,2,4,FALSE,0.388999999999896,0.104599907,41.047939,15.545544,21.653463,10.344817,0,231.190393,57.622497,6.943413,0.455207,60,"mbank_training" +"project2346","default",4,3850,23,317,17,2,13,FALSE,0.672000000000025,0.2921479,59.06303,23.600541,23.237525,14.67714,0,413.671557,104.475003,10.015255,0.229012,60,"mbank_training" +"project2346","default",5,3851,23,314,78,1,4,FALSE,3.25399999999991,1.237738072,270.848388,116.721824,120.649286,64.301568,0,2091.267241,537.485683,45.54476,1.042463,60,"mbank_training" +"project2451","default",1,3847,24,735,12,2,4,FALSE,0.380000000000109,0.195803825,58.437761,11.924619,16.795548,9.706935,0,201.186919,70.898262,6.095423,0.310965,60,"mbank_training" +"project2451","default",2,3848,24,732,67,1,1,FALSE,2.86299999999983,0.739887091,309.339849,73.561174,106.35613,47.104094,0,1682.872919,606.382824,34.560083,0,60,"mbank_training" +"project2451","default",3,3849,24,731,7,2,6,FALSE,0.213999999999942,0.08147899,36.285649,7.782122,10.81444,4.176962,0,107.266687,36.934762,3.742436,0.176081,60,"mbank_training" +"project2451","default",4,3850,24,731,74,1,3,FALSE,3.10799999999995,1.03302151,354.13203,76.234593,112.742415,50.471643,0,1835.061126,633.554558,37.608168,1.54608,60,"mbank_training" +"project2451","default",5,3851,24,730,56,1,2,FALSE,2.38400000000001,0.248315494,251.682816,76.48362,89.400481,41.476819,0,1392.188884,499.804545,30.093098,0,60,"mbank_training" +"project4501","default",1,3847,24,118,11,4,63,FALSE,0.388000000000147,0.044144614,18.206382,9.968826,10.619233,6.948279,0,123.442226,19.781661,4.620336,0.453224,60,"mbank_training" +"project4501","default",2,3848,24,118,8,6,93,FALSE,0.31899999999996,0.01872961,14.026555,6.975631,10.285114,5.881491,0,61.758493,12.974875,3.354795,0.776,60,"mbank_training" +"project4501","default",3,3849,24,118,11,8,63,FALSE,0.342000000000098,0.022986874,15.960925,11.566373,10.484639,6.290182,0,90.623341,16.135268,4.844278,1.13743,60,"mbank_training" +"project4501","default",4,3850,24,118,11,6,63,FALSE,0.385999999999967,0.047210546,16.637923,12.774526,11.017282,5.998301,0,118.138331,18.401079,4.671276,0.787301,60,"mbank_training" +"project4501","default",5,3851,24,118,9,4,93,FALSE,0.366999999999962,0.018027006,15.285145,12.046139,6.928463,5.445069,0,99.51878,17.209297,3.704213,0.634866,60,"mbank_training" +"project944","default",1,3847,25,128,7,7,60,FALSE,0.294999999999845,0.018088743,13.30662,8.38728,6.301262,4.208281,0,48.776002,9.005236,2.673461,0.830533,60,"mbank_training" +"project944","default",2,3848,25,128,6,6,60,FALSE,0.224999999999909,0.019102592,12.06331,4.438717,4.448804,3.289853,0,42.937782,7.673997,2.300673,0.821297,60,"mbank_training" +"project944","default",3,3849,25,128,6,6,60,FALSE,0.300999999999931,0.020199055,10.4648,6.232242,5.257136,3.33605,0,41.69849,8.874975,2.280816,0.85007,60,"mbank_training" +"project944","default",4,3850,25,128,8,8,60,FALSE,0.346000000000004,0.017898024,15.590799,8.054094,6.008188,4.86531,0,57.379981,12.570734,3.056505,0.871962,60,"mbank_training" +"project944","default",5,3851,25,128,9,9,60,FALSE,0.271999999999935,0.018105484,16.873965,11.336702,6.987282,5.283617,0,63.617765,10.896273,3.429023,1.41757,60,"mbank_training" +"project971_(1)","default",1,3847,26,157,7,5,100,FALSE,0.188000000000102,0.0321425,18.231222,8.017143,12.649642,6.286026,0,106.37865,16.725717,4.923929,0.854858,60,"mbank_training" +"project971_(1)","default",2,3848,26,157,7,3,100,FALSE,0.202999999999975,0.032296189,14.838464,7.174025,7.827495,6.616547,0,116.535101,17.333633,4.903698,0.776372,60,"mbank_training" +"project971_(1)","default",3,3849,26,157,14,9,100,FALSE,0.396999999999935,0.071620829,31.668768,19.663817,28.913962,13.593501,0,225.426793,31.191852,9.765443,1.439651,60,"mbank_training" +"project971_(1)","default",4,3850,26,157,9,5,100,FALSE,0.256000000000085,0.033776476,19.610728,10.628288,11.828968,8.692054,0,160.535044,25.838523,6.279481,0.742718,60,"mbank_training" +"project971_(1)","default",5,3851,26,157,8,5,100,FALSE,0.212999999999965,0.029701174,17.588101,8.384524,15.25026,7.746113,0,114.389611,20.183938,5.732391,1.160304,60,"mbank_training" +"project2762","default",1,3847,29,259,9,5,100,FALSE,0.456000000000131,0.061945926,85.191185,27.493606,26.010027,15.582262,0,213.117243,51.055173,10.07161,2.744155,60,"mbank_training" +"project2762","default",2,3848,29,259,9,5,100,FALSE,0.478000000000065,0.102143639,93.133407,20.179709,18.970153,19.004044,0,226.906961,50.594025,10.297777,2.769936,60,"mbank_training" +"project2762","default",3,3849,29,259,14,7,100,FALSE,0.793999999999869,0.238375902,161.098384,38.927341,30.230702,29.649027,0,389.337059,73.892207,16.263747,3.270378,60,"mbank_training" +"project2762","default",4,3850,29,259,7,2,100,FALSE,0.428000000000111,0.058983519,80.459811,17.423549,22.272229,15.008232,0,203.642406,42.868811,8.013146,0.555866,60,"mbank_training" +"project2762","default",5,3851,29,259,9,2,100,FALSE,0.535000000000082,0.151931487,94.256053,24.5204,23.889673,17.526596,0,281.778727,63.415303,10.419676,1.102776,60,"mbank_training" +"project826","default",1,3847,33,431,10,5,100,FALSE,0.562999999999874,0.056729766,94.04372,82.409339,32.56211,15.907697,0,235.778602,47.145061,10.064618,1.889146,60,"mbank_training" +"project826","default",2,3848,33,431,14,8,100,FALSE,0.773000000000138,0.06226196,158.0213,96.919785,38.896136,28.937695,0,349.497322,58.555463,13.803087,2.599543,60,"mbank_training" +"project826","default",3,3849,33,431,9,5,100,FALSE,0.48700000000008,0.107898042,94.132256,64.66845,31.710938,13.12036,0,216.448743,33.174753,9.034518,1.991478,60,"mbank_training" +"project826","default",4,3850,33,431,13,7,100,FALSE,0.675999999999931,0.06100803,148.574968,78.863774,43.65249,19.242854,0,286.236231,52.802974,12.941453,3.579557,60,"mbank_training" +"project826","default",5,3851,33,431,10,7,100,FALSE,0.51299999999992,0.098033702,103.843431,69.345303,26.127607,15.397387,0,220.027508,44.046429,9.939384,2.271867,60,"mbank_training" +"project561","default",1,3847,34,1169,5,4,2,FALSE,0.641999999999825,0.17492852,90.052497,24.187263,27.040114,28.734684,0,360.911661,86.894653,15.497404,1.369959,60,"mbank_training" +"project561","default",2,3848,34,1169,10,8,2,FALSE,1.2030000000002,0.177173586,138.669793,43.757383,76.882535,58.167253,0,685.538141,159.881894,30.825721,1.339312,60,"mbank_training" +"project561","default",3,3849,34,1169,6,4,2,FALSE,0.869999999999891,0.18790584,95.455449,27.965055,61.085397,31.719011,0,515.495499,109.62762,18.470351,2.695755,60,"mbank_training" +"project561","default",4,3850,34,1169,7,6,2,FALSE,0.826999999999998,0.192727156,109.19865,44.217522,38.954784,38.522289,0,453.311567,111.987842,21.865571,1.357856,60,"mbank_training" +"project561","default",5,3851,34,1169,13,10,2,FALSE,1.52800000000002,0.170371331,207.568654,73.128851,84.761045,71.265761,0,857.731735,184.962638,40.678768,0,60,"mbank_training" +"project571","default",1,3847,42,634,8,3,12,FALSE,0.671000000000049,0.157445137,102.114585,49.798648,31.506792,20.846217,0,331.649651,60.904718,12.152725,1.196162,60,"mbank_training" +"project571","default",2,3848,42,635,7,6,28,FALSE,0.619000000000142,0.086190797,97.06063,44.233453,27.674077,19.839528,0,223.541076,46.893869,10.235498,1.847647,60,"mbank_training" +"project571","default",3,3849,42,634,14,5,12,FALSE,1.22199999999998,0.196988458,179.846047,68.135679,62.309499,38.144598,0,669.29153,131.646387,20.805877,2.624811,60,"mbank_training" +"project571","default",4,3850,42,634,6,3,12,FALSE,0.484000000000151,0.102017813,93.340107,39.119923,24.874958,16.486808,0,207.705063,47.070943,9.135167,1.997872,60,"mbank_training" +"project571","default",5,3851,42,634,7,4,12,FALSE,0.546000000000049,0.091017033,101.386263,33.300991,21.79463,20.3546,0,258.555851,51.713282,10.502081,1.974336,60,"mbank_training" +"project4146_(3)","default",1,3847,59,260,88,2,100,FALSE,29.146,1.121571562,2298.172541,1541.883571,1388.448093,619.279036,0,16782.220998,6022.513703,373.627406,1.907832,60,"mbank_training" +"project4146_(3)","default",2,3848,59,262,16,2,100,FALSE,4.29300000000012,2.907800167,388.581579,296.941575,232.639383,121.909569,0,2373.682509,675.650357,68.198961,3.556906,60,"mbank_training" +"project4146_(3)","default",3,3849,59,263,11,2,100,FALSE,2.81899999999996,1.70537764,283.697704,229.458175,134.279144,72.100403,0,1519.530647,460.846893,47.369725,1.771184,60,"mbank_training" +"project4146_(3)","default",4,3850,59,261,43,1,100,FALSE,13.5260000000001,2.079365236,1173.373873,791.026839,639.12085,305.072936,0,7612.742871,2745.110794,185.134828,9.093941,60,"mbank_training" +"project4146_(3)","default",5,3851,59,261,31,2,100,FALSE,9.84099999999989,0.609325093,815.511969,534.029647,547.343539,218.158431,0,5510.730906,1946.230582,130.943191,1.776644,60,"mbank_training" +"project3688","default",1,3847,60,854,17,2,100,FALSE,3.92099999999982,3.919504781,385.501468,328.321857,206.991666,85.831002,0,1864.174767,936.303222,48.589011,0.777434,60,"mbank_training" +"project3688","default",2,3848,60,852,56,2,100,FALSE,14.6719999999998,14.670244516,1209.847499,1190.050694,689.647573,251.904609,0,7175.943495,3823.25799,153.520014,7.791968,60,"mbank_training" +"project3688","default",3,3849,60,850,86,2,100,FALSE,21.9349999999999,6.939731752,1927.898247,1467.977175,1126.604546,419.843857,0,10905.788095,5657.655153,238.271027,1.38597,60,"mbank_training" +"project3688","default",4,3850,60,845,100,0,100,FALSE,27.221,23.330009764,2294.310236,1989.385736,1275.489694,446.473982,0,13802.071859,6750.57814,283.637525,15.922755,60,"mbank_training" +"project3688","default",5,3851,60,851,100,2,100,FALSE,26.5239999999999,26.46343803,2229.919796,2008.345071,1232.32596,472.192149,0,13016.601459,6917.000715,281.497787,0,60,"mbank_training" +"project4049","default",1,3847,60,5237,50,1,24,TRUE,55.635,28.849031526,12812.355924,4022.935411,1534.967378,770.142303,0,26041.833005,8340.113622,482.876614,0,60,"mbank_training" +"project4049","default",2,3848,60,5241,14,2,100,FALSE,13.231,6.809624175,3518.238252,1155.261025,441.629723,213.68929,0,5531.891441,1538.969853,138.060614,2.902625,60,"mbank_training" +"project4049","default",3,3849,60,5237,51,0,69,TRUE,60.0050000000001,24.444532187,12896.301176,4401.928909,1452.051462,798.980615,0,25780.83288,8172.16566,480.915105,19.044584,60,"mbank_training" +"project4049","default",4,3850,60,5238,50,1,42,TRUE,57.2310000000002,0.927573556,12594.197084,4495.215782,1233.283096,798.486808,0,26471.88747,7921.979066,483.190351,0,60,"mbank_training" +"project4049","default",5,3851,60,5238,48,2,100,FALSE,51.5299999999997,35.234272169,12285.229797,3817.922871,1290.084708,807.233125,0,24705.508017,7500.001315,463.980469,5.626637,60,"mbank_training" +"project423","default",1,3847,60,495,8,4,100,FALSE,2.64699999999993,0.400347063,441.869488,149.306496,171.026613,105.804704,0,1238.875356,303.385543,56.319473,7.638977,60,"mbank_training" +"project423","default",2,3848,60,495,10,4,100,FALSE,3.02799999999979,0.356720675,484.076736,189.899313,179.44867,111.645103,0,1532.396363,369.926824,71.14467,13.962615,60,"mbank_training" +"project423","default",3,3849,60,495,9,4,100,FALSE,2.79700000000003,0.373323881,487.248487,141.088071,171.733752,90.220876,0,1283.917822,344.518464,64.250429,14.971223,60,"mbank_training" +"project423","default",4,3850,60,495,9,3,100,FALSE,3.04700000000003,0.878920563,461.192317,144.824947,139.270067,95.323255,0,1543.655094,421.459133,64.434844,9.116608,60,"mbank_training" +"project423","default",5,3851,60,495,7,3,100,FALSE,2.23999999999978,0.566558355,320.509298,86.205063,162.281775,64.508463,0,1050.605463,277.976441,49.275418,7.849402,60,"mbank_training" +"project4286","default",1,3847,63,283,29,1,100,FALSE,11.241,3.589487481,989.545223,727.212385,542.731052,270.822659,0,6261.587648,2167.214292,158.508772,11.424284,60,"mbank_training" +"project4286","default",2,3848,63,286,17,1,100,FALSE,6.8130000000001,2.866909116,617.645772,485.539661,292.257845,172.845279,0,3823.49682,1229.65814,98.085378,13.692145,60,"mbank_training" +"project4286","default",3,3849,63,282,100,0,100,FALSE,42.7640000000001,6.010016617,3526.899076,2417.887392,1721.874089,909.951811,0,24570.01672,8672.056574,543.419217,9.878051,60,"mbank_training" +"project4286","default",4,3850,63,282,36,2,100,FALSE,14.7190000000001,6.805195528,1212.868133,790.710567,583.564096,308.686255,0,8340.113943,2979.273262,196.729748,5.521758,60,"mbank_training" +"project4286","default",5,3851,63,281,100,0,100,FALSE,43.7359999999999,33.033736523,3495.468488,2532.104488,2037.564548,902.984646,0,25233.885802,8661.81827,553.655857,9.87596,60,"mbank_training" +"project4359","default",1,3847,71,183,44,14,100,FALSE,32.0450000000001,2.284122122,6608.926961,1508.127227,1179.962765,1060.524953,0,18053.52329,2758.388991,624.412424,148.430653,60,"mbank_training" +"project4359","default",2,3848,71,183,30,14,100,FALSE,17.991,0.669618659,4264.572667,1193.658269,796.706378,688.173414,0,9061.52797,1384.455348,438.30725,72.17638,60,"mbank_training" +"project4359","default",3,3849,71,183,48,14,100,FALSE,34.951,3.286649126,7525.892458,2044.580377,1323.267198,1124.803309,0,19080.732472,2870.561837,717.551942,135.330823,60,"mbank_training" +"project4359","default",4,3850,71,183,39,14,100,FALSE,24.9720000000002,4.017413013,5956.408073,1609.563323,1125.355011,908.142088,0,12957.920369,1690.329368,530.747424,99.477889,60,"mbank_training" +"project4359","default",5,3851,71,184,5,4,100,FALSE,2.68499999999995,0.670274178,618.15515,193.851391,105.988098,112.845919,0,1234.846153,232.937716,64.465129,6.273569,60,"mbank_training" +"project4397","default",1,3847,75,1645,68,1,100,TRUE,56.2779999999998,12.170124323,7991.80297,8621.763032,1577.508321,731.455933,0,25971.726563,8575.839645,531.199288,0,60,"mbank_training" +"project4397","default",2,3848,75,1647,44,2,72,FALSE,37.058,37.056632143,4958.379407,5528.590199,1114.292194,481.745149,0,15757.734049,5206.777362,340.585754,4.460428,60,"mbank_training" +"project4397","default",3,3849,75,1649,22,2,100,FALSE,16.4540000000002,16.451864336,2598.100076,2728.514627,483.544724,241.513852,0,7331.81783,2463.322895,165.851744,2.134348,60,"mbank_training" +"project4397","default",4,3850,75,1647,69,1,100,TRUE,54.3110000000001,34.441545572,7951.753605,9080.744802,1793.795614,869.29426,0,25555.851696,8210.116374,524.851391,11.542171,60,"mbank_training" +"project4397","default",5,3851,75,1646,68,1,29,TRUE,54.7840000000001,7.434980652,7951.811871,8898.509066,1817.529788,778.936639,0,25542.609159,8458.239025,548.439589,0,60,"mbank_training" +"project2084_(1)","default",1,3847,86,28962,1,1,1,TRUE,54.4740000000002,36.027487568,14979.095008,14999.501691,2975.967362,696.11225,0,15425.894194,4695.655548,410.523317,0,60,"mbank_training" +"project2084_(1)","default",2,3848,86,28206,1,1,2,TRUE,56.5880000000002,43.132102468,13181.638643,16595.40308,5997.758415,594.90442,0,11696.330144,7423.078548,256.345525,0,60,"mbank_training" +"project2084_(1)","default",3,3849,86,28303,1,1,1,TRUE,54.8099999999999,33.587438404,13343.930947,12374.806132,4238.386208,917.861064,0,19324.586019,3811.831915,333.559326,0,60,"mbank_training" +"project2084_(1)","default",4,3850,86,29022,1,1,2,TRUE,54.9989999999998,34.283512361,11256.392787,12465.419556,6071.672261,804.994572,0,19199.613915,3854.822219,513.142007,0,60,"mbank_training" +"project2084_(1)","default",5,3851,86,29028,1,1,1,TRUE,54.3580000000002,32.795953363,13459.063726,7689.930694,7774.009206,647.933228,0,20775.360569,3475.924906,262.921936,0,60,"mbank_training" +"project2771","default",1,3847,94,1061,30,1,7,TRUE,54.5080000000003,54.50748361,4055.693235,2007.917633,2703.941653,867.315002,0,29991.013706,13797.074177,578.259028,5.170318,60,"mbank_training" +"project2771","default",2,3848,94,1051,30,1,2,TRUE,54.0459999999998,4.789783894,4271.827506,1819.564166,2938.795187,815.396822,0,30148.284471,13355.075623,621.537151,27.556894,60,"mbank_training" +"project2771","default",3,3849,94,1054,30,1,7,TRUE,56.1459999999997,56.145150433,4154.5174,2329.563524,3529.689896,867.569369,0,29018.037154,13612.762975,617.564404,0,60,"mbank_training" +"project2771","default",4,3850,94,1046,30,0,6,TRUE,54.2339999999999,23.20915686,4222.126455,2096.845122,2913.642794,1085.219592,0,29918.720473,13139.276886,579.73558,51.066233,60,"mbank_training" +"project2771","default",5,3851,94,1059,29,1,1,TRUE,54.0189999999998,50.911111792,3962.747493,2194.650357,2428.450395,882.999859,0,29559.448131,14387.393034,580.596246,0,60,"mbank_training" +"project2184","default",1,3847,114,565,13,1,100,FALSE,18.6109999999999,11.402577243,2131.156542,4725.997082,530.837764,293.965445,0,8043.019541,2221.036326,273.326637,44.186869,60,"mbank_training" +"project2184","default",2,3848,114,564,8,2,100,FALSE,11.154,5.345453234,1274.511283,3095.947372,411.255146,218.503153,0,4509.715032,1206.564447,175.035752,7.404348,60,"mbank_training" +"project2184","default",3,3849,114,565,18,2,100,FALSE,26.3409999999999,16.141419283,2771.799272,6012.279972,808.007389,471.532338,0,12121.375686,3661.194424,370.384895,20.829929,60,"mbank_training" +"project2184","default",4,3850,114,563,33,0,100,TRUE,54.2220000000002,16.851939064,4867.676228,11886.042139,1552.892566,850.359378,0,26578.906698,7519.570371,675.624246,70.321069,60,"mbank_training" +"project2184","default",5,3851,114,564,18,2,100,FALSE,28.5320000000002,1.17403699,2732.189783,6746.666943,896.459513,454.621275,0,13185.943663,3950.044113,370.900718,16.416084,60,"mbank_training" +"project3938","default",1,3847,119,3417,9,2,100,FALSE,51.2469999999998,21.493213813,13097.014635,17240.880122,2577.814748,685.203831,0,12611.715252,4025.969422,457.683269,30.401117,60,"mbank_training" +"project3938","default",2,3848,119,3408,8,1,100,TRUE,54.9359999999997,22.446101436,14280.167284,17485.494785,2503.527778,693.394689,0,13934.462712,4716.448387,413.765958,0,60,"mbank_training" +"project3938","default",3,3849,119,3413,9,2,100,FALSE,53.6620000000003,29.438755385,13646.266789,16273.772787,1957.851645,607.939928,0,14071.340355,5282.540219,453.269932,204.081741,60,"mbank_training" +"project3938","default",4,3850,119,3409,9,1,100,TRUE,54.4050000000002,32.279967283,14393.190366,16570.649742,3077.911077,580.782538,0,14041.999764,4926.950212,444.543922,0,60,"mbank_training" +"project3938","default",5,3851,119,3410,9,0,100,TRUE,55.8039999999996,54.032769174,15041.883919,18403.466552,1289.59538,761.434192,0,13465.906008,4532.486313,469.045411,66.780611,60,"mbank_training" +"syab07201","default",1,3847,125,14933,6,1,3,TRUE,54.5649999999996,13.463024632,8283.224612,5422.107356,2155.406468,975.963841,0,23496.45208,12990.29354,720.755871,0,60,"mbank_training" +"syab07201","default",2,3848,125,15033,6,1,1,TRUE,54.1619999999998,6.879759994,8631.661473,4556.603853,1981.947232,1083.600626,0,24835.027048,12258.982609,667.987072,0,60,"mbank_training" +"syab07201","default",3,3849,125,14953,6,1,1,TRUE,54.3450000000003,23.402911157,8807.088314,4685.148042,2305.728683,1070.541614,0,25444.06322,11129.258295,704.90096,0,60,"mbank_training" +"syab07201","default",4,3850,125,15017,6,1,2,TRUE,54.4230000000002,9.162811337,8130.311725,5679.011914,2400.763562,1039.644438,0,25123.454571,10902.237259,753.185253,0,60,"mbank_training" +"syab07201","default",5,3851,125,14926,6,1,2,TRUE,54.5190000000002,31.986010941,7983.21979,6412.495985,1702.238379,1285.062244,0,25779.764873,10200.97239,729.078123,0,60,"mbank_training" +"project4133","default",1,3847,131,2373,12,1,100,TRUE,55.0460000000003,50.763268092,9777.261701,16367.982627,2105.14162,590.469161,0,17587.748365,7183.47726,410.978192,0,60,"mbank_training" +"project4133","default",2,3848,131,2378,12,1,100,TRUE,56.9690000000001,56.966936068,10115.779173,17903.407013,1533.860302,741.545257,0,15572.04862,7743.139666,419.157453,0,60,"mbank_training" +"project4133","default",3,3849,131,2377,12,1,100,TRUE,54.7849999999999,54.782788502,10360.897605,15017.756517,2720.598684,644.129521,0,17636.426464,7206.39643,420.041532,0,60,"mbank_training" +"project4133","default",4,3850,131,2371,13,1,100,TRUE,56.2750000000001,56.271602815,10099.90732,17124.399453,1715.449085,535.285948,0,17256.680013,6864.383411,406.018243,0,60,"mbank_training" +"project4133","default",5,3851,131,2376,12,1,100,TRUE,55.3900000000003,55.386705398,10123.53186,16744.885155,2063.074556,809.123932,0,17128.859433,6715.284436,414.796312,0,60,"mbank_training" +"project804","default",1,3847,173,1375,3,1,47,TRUE,60.0190000000002,21.23623549,11228.226484,12303.850331,2430.559834,830.477995,0,17909.169516,8717.022799,708.078116,0,60,"mbank_training" +"project804","default",2,3848,173,1368,3,1,100,TRUE,60.1500000000005,60.080988944,10396.680621,17198.177502,1861.84538,753.302697,0,16601.247605,6676.313676,571.262477,0,60,"mbank_training" +"project804","default",3,3849,173,1373,2,1,86,TRUE,60.0889999999999,60.066671998,9195.650252,12590.645613,3033.072112,755.479002,0,20511.079276,7269.364064,711.281004,0,60,"mbank_training" +"project804","default",4,3850,173,1387,3,1,100,TRUE,60.0789999999997,22.113473518,12998.737773,12270.630953,4290.703251,689.04819,0,15290.848349,7909.329051,669.759235,0,60,"mbank_training" +"project804","default",5,3851,173,1370,3,1,7,TRUE,60.0300000000007,60.022688033,13480.410099,11992.159228,3052.961199,727.439951,0,16306.47525,8248.843774,634.589284,0,60,"mbank_training" +"project4284","default",1,3847,4062,1268,0,1,1,TRUE,75.3469999999998,75.339956447,54305.100757,21032.114597,0,0,0,0,0,0,0,60,"mbank_training" +"project4284","default",2,3848,4062,1407,0,1,1,TRUE,72.5429999999997,72.536462982,54305.789977,18227.270056,0,0,0,0,0,0,0,60,"mbank_training" +"project4284","default",3,3849,4062,1193,0,1,1,TRUE,70.0289999999995,70.021119683,54540.924025,15477.382811,0,0,0,0,0,0,0,60,"mbank_training" +"project4284","default",4,3850,4062,1107,0,1,1,TRUE,70.415,70.408575744,54275.210143,16129.911666,0,0,0,0,0,0,0,60,"mbank_training" +"project4284","default",5,3851,4062,1355,0,1,1,TRUE,74.0500000000002,74.042071236,54236.199289,19803.278572,0,0,0,0,0,0,0,60,"mbank_training" +"project532","default",1,3847,21,1139,7,5,4,FALSE,0.597999999999956,0.174283273,148.503215,14.327887,24.690415,17.636236,0,303.184831,62.553178,12.901541,2.343453,120,"mbank_training" +"project532","default",2,3848,21,1139,6,4,3,FALSE,0.597999999999956,0.123916778,121.97986,33.485211,22.381566,18.933991,0,320.406746,55.284499,12.856154,1.757961,120,"mbank_training" +"project532","default",3,3849,21,1139,6,5,4,FALSE,0.579999999999927,0.139597412,132.649526,22.214793,26.952043,15.646982,0,300.59517,53.942751,12.362817,1.812183,120,"mbank_training" +"project532","default",4,3850,21,1139,7,3,3,FALSE,0.750999999999294,0.197873706,141.871555,42.623983,26.681093,20.747777,0,423.874721,68.928898,15.491427,0.949228,120,"mbank_training" +"project532","default",5,3851,21,1139,6,3,2,FALSE,0.592999999999847,0.129039236,125.361359,30.279261,26.760433,19.136244,0,319.94664,49.821137,13.127436,2.038109,120,"mbank_training" +"project2346","default",1,3847,23,316,18,2,4,FALSE,0.829999999999927,0.608430951,71.029184,35.473094,32.825955,19.374163,0,519.261742,131.533754,13.02882,0.765322,120,"mbank_training" +"project2346","default",2,3848,23,318,8,2,20,FALSE,0.304000000000087,0.117708173,33.520126,9.281411,16.761981,7.827445,0,158.257356,36.152855,5.366508,0.311627,120,"mbank_training" +"project2346","default",3,3849,23,320,12,2,4,FALSE,0.46599999999944,0.124930457,48.132288,18.466171,27.800715,12.278035,0,276.894882,67.959639,8.181343,0.703515,120,"mbank_training" +"project2346","default",4,3850,23,317,17,2,13,FALSE,0.795000000000073,0.344254344,68.922419,28.551914,29.73234,17.731987,0,483.753854,123.08423,12.051296,0.341163,120,"mbank_training" +"project2346","default",5,3851,23,314,78,1,4,FALSE,3.67699999999968,1.435090677,304.810463,132.69613,141.883327,73.51865,0,2356.842515,605.479236,51.597378,1.447145,120,"mbank_training" +"project2451","default",1,3847,24,735,12,2,4,FALSE,0.427999999999884,0.221843663,66.205828,13.396343,18.763832,10.832563,0,227.105905,79.846971,6.955762,0.330172,120,"mbank_training" +"project2451","default",2,3848,24,732,67,1,1,FALSE,3.22999999999956,0.823948679,346.979458,82.764533,119.244198,52.943228,0,1902.570635,683.326202,38.894812,0,120,"mbank_training" +"project2451","default",3,3849,24,731,7,2,6,FALSE,0.244999999999891,0.092966703,41.354672,8.912597,12.506295,4.7361,0,123.690091,42.28259,4.282265,0.193916,120,"mbank_training" +"project2451","default",4,3850,24,731,74,1,3,FALSE,3.45600000000013,1.132576511,383.106166,87.869532,126.007845,56.212429,0,2044.100132,703.889984,42.021767,1.663113,120,"mbank_training" +"project2451","default",5,3851,24,730,56,1,2,FALSE,2.67200000000048,0.277758048,280.567088,85.41411,100.316807,46.314025,0,1563.836515,557.562805,33.588465,0,120,"mbank_training" +"project4501","default",1,3847,24,118,11,4,63,FALSE,0.449999999999818,0.049543826,20.627493,11.198614,11.815055,7.776755,0,138.45961,22.195465,5.140892,14.184838,120,"mbank_training" +"project4501","default",2,3848,24,118,8,6,93,FALSE,0.360999999999876,0.02131701,15.809006,7.847642,11.572414,6.5989,0,69.68908,14.638501,3.76466,0.836356,120,"mbank_training" +"project4501","default",3,3849,24,118,11,8,63,FALSE,0.387000000000626,0.025869516,17.988182,13.159864,11.846536,7.174633,0,103.145235,18.32269,5.53868,1.248622,120,"mbank_training" +"project4501","default",4,3850,24,118,11,6,63,FALSE,0.428999999999178,0.05354915,18.767008,14.497546,12.386349,6.770313,0,134.033431,20.862917,5.271509,0.836095,120,"mbank_training" +"project4501","default",5,3851,24,118,9,4,93,FALSE,0.377000000000407,0.018355794,15.849192,12.337627,7.080306,5.553369,0,101.60287,17.539517,3.805357,0.62106,120,"mbank_training" +"project944","default",1,3847,25,128,7,7,60,FALSE,0.349999999999454,0.018372965,13.749598,8.665922,6.577338,4.388015,0,50.700024,9.316056,2.852453,0.884296,120,"mbank_training" +"project944","default",2,3848,25,128,6,6,60,FALSE,0.305999999999585,0.026536602,16.762202,6.242929,6.315995,4.568325,0,59.400532,10.678432,3.202963,1.232052,120,"mbank_training" +"project944","default",3,3849,25,128,6,6,60,FALSE,0.353000000000065,0.023519519,12.339231,7.414637,6.222741,4.003219,0,49.831448,10.604764,2.731975,1.047022,120,"mbank_training" +"project944","default",4,3850,25,128,8,8,60,FALSE,0.373000000000502,0.018053484,16.38878,8.637668,6.467942,5.269536,0,61.244825,13.507091,3.300045,0.928819,120,"mbank_training" +"project944","default",5,3851,25,128,9,9,60,FALSE,0.289999999999964,0.019552678,18.095653,12.151427,7.457316,5.676282,0,68.236955,11.678938,3.661214,1.465672,120,"mbank_training" +"project971_(1)","default",1,3847,26,157,7,5,100,FALSE,0.199999999999818,0.034288362,19.540965,8.624554,13.504326,6.719836,0,112.82168,17.875589,5.259305,0.877813,120,"mbank_training" +"project971_(1)","default",2,3848,26,157,7,3,100,FALSE,0.216000000000349,0.03481252,16.035452,7.674916,8.319572,7.100072,0,124.029589,18.743483,5.235702,0.826979,120,"mbank_training" +"project971_(1)","default",3,3849,26,157,14,9,100,FALSE,0.42200000000048,0.07753632,34.000391,20.956551,31.22286,14.428183,0,240.754111,33.668735,10.389131,1.483253,120,"mbank_training" +"project971_(1)","default",4,3850,26,157,9,5,100,FALSE,0.268000000000029,0.034529356,20.521152,11.128321,12.392041,9.051989,0,167.722094,26.988611,6.584964,0.792242,120,"mbank_training" +"project971_(1)","default",5,3851,26,157,8,5,100,FALSE,0.269000000000233,0.033144458,22.034311,10.560942,19.018461,9.771033,0,142.472617,25.789647,7.242129,1.522097,120,"mbank_training" +"project2762","default",1,3847,29,259,9,5,100,FALSE,0.552000000000589,0.076698963,102.552619,31.991234,32.017848,18.777667,0,255.924423,60.232298,12.068471,3.722681,120,"mbank_training" +"project2762","default",2,3848,29,259,9,5,100,FALSE,0.559000000000196,0.124296864,110.021915,23.549355,22.25614,22.257053,0,265.42186,58.979949,11.944967,3.162605,120,"mbank_training" +"project2762","default",3,3849,29,259,14,7,100,FALSE,0.896999999999935,0.271071243,182.034881,44.101888,34.36301,33.517622,0,439.83955,83.552751,18.319235,3.683558,120,"mbank_training" +"project2762","default",4,3850,29,259,7,2,100,FALSE,0.552000000000589,0.072439469,103.33374,22.591772,28.818399,19.640743,0,260.223182,55.45098,10.463058,0.79617,120,"mbank_training" +"project2762","default",5,3851,29,259,9,2,100,FALSE,0.640000000000327,0.196973069,111.81434,30.079626,29.628034,21.70403,0,335.326449,74.873774,12.244683,1.187246,120,"mbank_training" +"project826","default",1,3847,33,431,10,5,100,FALSE,0.731000000000677,0.076276196,121.741229,108.597777,43.75939,21.104991,0,309.097116,60.566575,12.983497,2.531488,120,"mbank_training" +"project826","default",2,3848,33,431,14,8,100,FALSE,0.932999999999993,0.072963527,190.042457,114.346752,47.944522,34.721809,0,421.781289,70.243541,16.782441,3.219924,120,"mbank_training" +"project826","default",3,3849,33,431,9,5,100,FALSE,0.604999999999563,0.14725746,118.219918,80.220355,40.605209,16.564079,0,268.776122,41.222762,11.147686,2.454232,120,"mbank_training" +"project826","default",4,3850,33,431,13,7,100,FALSE,0.806999999999789,0.069525721,173.788642,93.184784,51.796127,23.401488,0,341.117535,62.517173,15.345382,4.454339,120,"mbank_training" +"project826","default",5,3851,33,431,10,7,100,FALSE,0.626999999999498,0.132048574,127.710431,85.490192,32.522076,19.169466,0,269.264299,52.301932,12.045337,2.735471,120,"mbank_training" +"project561","default",1,3847,34,1169,5,4,2,FALSE,0.766999999999825,0.200921647,104.203848,28.18669,31.8137,34.553072,0,434.189226,104.452797,18.555571,1.495537,120,"mbank_training" +"project561","default",2,3848,34,1169,10,8,2,FALSE,1.57900000000063,0.235814397,184.091378,57.688625,102.624785,76.210602,0,897.399635,209.365029,40.577297,1.872447,120,"mbank_training" +"project561","default",3,3849,34,1169,6,4,2,FALSE,1.03800000000047,0.215172027,112.760827,32.396947,72.031643,37.331976,0,617.902089,130.587572,22.017981,3.256114,120,"mbank_training" +"project561","default",4,3850,34,1169,7,6,2,FALSE,1.09799999999996,0.255119559,141.429342,58.494966,51.773246,51.428364,0,604.051173,149.422167,28.95167,1.973197,120,"mbank_training" +"project561","default",5,3851,34,1169,13,10,2,FALSE,1.8779999999997,0.227611827,254.913113,90.500066,104.908815,86.761706,0,1057.262797,223.664192,49.85961,0,120,"mbank_training" +"project571","default",1,3847,42,634,8,3,12,FALSE,0.894999999999527,0.199396634,135.515565,66.212952,42.147855,27.872039,0,445.262381,81.952351,16.597809,1.538257,120,"mbank_training" +"project571","default",2,3848,42,635,7,6,28,FALSE,0.715000000000146,0.099789584,112.405676,51.24409,31.78723,22.885687,0,258.887033,54.158859,11.817098,2.125303,120,"mbank_training" +"project571","default",3,3849,42,634,14,5,12,FALSE,1.57200000000012,0.225831785,228.209175,86.461079,80.530839,48.792823,0,858.462695,170.886235,26.904718,3.584772,120,"mbank_training" +"project571","default",4,3850,42,634,6,3,12,FALSE,0.603000000000065,0.137255932,117.591152,49.656388,31.269116,20.712042,0,260.278734,57.636379,11.03258,2.350998,120,"mbank_training" +"project571","default",5,3851,42,634,7,4,12,FALSE,0.618999999999687,0.103445409,115.114959,37.63192,24.581629,23.032833,0,293.901454,58.725278,11.897841,2.274955,120,"mbank_training" +"project4146_(3)","default",1,3847,59,260,88,2,100,FALSE,34.116,1.413533364,2671.985035,1807.940198,1630.073094,728.712218,0,19646.876166,7051.046702,440.119674,2.154328,120,"mbank_training" +"project4146_(3)","default",2,3848,59,262,16,2,100,FALSE,4.8080000000009,3.259231522,437.061873,332.329626,260.812709,136.426056,0,2658.735654,754.976978,76.161495,3.929552,120,"mbank_training" +"project4146_(3)","default",3,3849,59,263,11,2,100,FALSE,3.04700000000048,1.879511765,306.395457,247.90622,145.666599,77.723276,0,1638.1709,496.971015,51.236215,1.810841,120,"mbank_training" +"project4146_(3)","default",4,3850,59,261,43,1,100,FALSE,15.3180000000002,2.374788278,1320.469164,894.874744,714.758367,346.257565,0,8599.051059,3133.391372,210.801877,10.461404,120,"mbank_training" +"project4146_(3)","default",5,3851,59,261,31,2,100,FALSE,12.2269999999999,0.741778749,1014.974118,655.035662,685.970374,273.578485,0,6815.550667,2422.397126,162.815185,2.274815,120,"mbank_training" +"project3688","default",1,3847,60,854,17,2,100,FALSE,5.02500000000055,5.021771877,510.37382,427.643124,259.063646,112.165339,0,2383.360876,1192.373307,62.203885,1.168781,120,"mbank_training" +"project3688","default",2,3848,60,852,56,2,100,FALSE,17.5849999999991,17.581586221,1508.551866,1422.340587,842.489003,302.981617,0,8563.456329,4566.251877,183.361074,8.76499,120,"mbank_training" +"project3688","default",3,3849,60,850,86,2,100,FALSE,23.5720000000001,7.411819695,2115.873042,1577.791567,1209.467271,451.31191,0,11671.087764,6083.894379,257.34136,1.490678,120,"mbank_training" +"project3688","default",4,3850,60,845,100,0,100,FALSE,28.1569999999992,24.162367597,2261.042949,2157.45028,1376.283966,483.92103,0,14079.414317,7325.004318,307.627242,8.029794,120,"mbank_training" +"project3688","default",5,3851,60,851,100,2,100,FALSE,31.8050000000003,31.802672613,2618.332145,2409.378973,1494.269097,570.378077,0,15695.913627,8333.697,340.037484,0,120,"mbank_training" +"project4049","default",1,3847,60,5237,58,2,69,FALSE,80.0329999999994,32.052243607,16809.550981,5256.228341,2110.483353,1030.435606,0,35258.662297,11422.279982,653.04131,4.22619,120,"mbank_training" +"project4049","default",2,3848,60,5241,14,2,100,FALSE,16.0689999999995,7.870476592,4173.924444,1414.553628,544.295207,261.063861,0,6683.463902,1911.022872,169.10444,3.227579,120,"mbank_training" +"project4049","default",3,3849,60,5237,86,0,69,TRUE,120.012,29.14747189,24915.557783,8555.080917,2826.307074,1585.849986,0,52725.984647,16420.480107,960.753632,19.357873,120,"mbank_training" +"project4049","default",4,3850,60,5237,82,1,67,TRUE,113.106000000001,87.538582491,24380.17746,8460.16691,2744.356645,1587.127748,0,53779.48615,16087.417783,947.49759,0,120,"mbank_training" +"project4049","default",5,3851,60,5238,48,2,100,FALSE,61.625,42.237513028,14343.858343,4557.954165,1591.975844,975.424709,0,29679.922169,9072.33144,551.917905,6.537494,120,"mbank_training" +"project423","default",1,3847,60,495,8,4,100,FALSE,3.40499999999975,0.526153467,566.898634,190.75694,215.289551,136.966522,0,1626.799883,386.053694,70.778868,9.020881,120,"mbank_training" +"project423","default",2,3848,60,495,10,4,100,FALSE,3.90000000000055,0.411584624,588.627592,245.185435,238.855369,144.338626,0,1998.711399,474.040828,90.219949,16.501401,120,"mbank_training" +"project423","default",3,3849,60,495,9,4,100,FALSE,3.39900000000034,0.46492677,604.06468,177.265199,213.227558,110.902656,0,1578.009632,412.734359,77.942664,17.989694,120,"mbank_training" +"project423","default",4,3850,60,495,9,3,100,FALSE,3.30899999999929,0.925018402,492.497982,155.53686,151.105736,102.833498,0,1678.795371,457.312226,70.475237,10.101998,120,"mbank_training" +"project423","default",5,3851,60,495,7,3,100,FALSE,2.92799999999988,0.693696482,413.570945,112.361907,210.636516,84.170433,0,1356.68924,368.077685,66.544864,10.714993,120,"mbank_training" +"project4286","default",1,3847,63,283,29,1,100,FALSE,11.7159999999994,4.100318091,1046.296071,768.137171,567.789875,282.419437,0,6540.196924,2232.430133,165.583054,11.269518,120,"mbank_training" +"project4286","default",2,3848,63,286,17,1,100,FALSE,6.14800000000014,2.731416157,577.453715,449.078541,267.206664,154.448919,0,3413.660776,1114.497786,88.825288,12.203679,120,"mbank_training" +"project4286","default",3,3849,63,282,100,0,100,FALSE,44.9789999999994,5.96331056,3683.094082,2517.868007,1813.204677,950.494329,0,25889.045977,9131.184162,567.907615,10.793273,120,"mbank_training" +"project4286","default",4,3850,63,282,36,2,100,FALSE,15.5619999999999,7.152473633,1275.286219,836.323187,605.933541,328.329446,0,8875.831635,3180.293214,206.313307,4.975995,120,"mbank_training" +"project4286","default",5,3851,63,281,100,0,100,FALSE,46.5769999999993,35.041412631,3699.824056,2689.559526,2089.605703,946.042036,0,26985.775764,9107.103506,618.261001,11.396668,120,"mbank_training" +"project4359","default",1,3847,71,183,44,14,100,FALSE,41.9520000000002,3.203920721,8767.766222,2006.351704,1513.433996,1382.957629,0,23498.199682,3646.497845,799.066168,169.565444,120,"mbank_training" +"project4359","default",2,3848,71,183,30,14,100,FALSE,21.7539999999999,0.915047912,5225.794822,1438.006894,960.455597,825.817399,0,10896.586175,1663.655235,533.446058,88.870922,120,"mbank_training" +"project4359","default",3,3849,71,183,48,14,100,FALSE,40.0810000000001,4.272051181,8738.055745,2383.24364,1527.197327,1300.041735,0,21769.124441,3284.361575,822.207967,151.216495,120,"mbank_training" +"project4359","default",4,3850,71,183,39,14,100,FALSE,30.3309999999992,4.925332616,7050.165321,1939.795687,1343.31804,1102.898121,0,15901.077905,2095.50652,655.182761,120.679789,120,"mbank_training" +"project4359","default",5,3851,71,184,5,4,100,FALSE,3.22699999999986,0.863578676,733.995855,246.411799,130.749929,134.986509,0,1477.365187,265.119975,95.768599,7.753395,120,"mbank_training" +"project4397","default",1,3847,75,1645,100,1,80,FALSE,92.9229999999998,14.423068514,13204.176944,13809.390556,2675.089632,1261.189782,0,43065.316764,14214.117253,874.400732,0,120,"mbank_training" +"project4397","default",2,3848,75,1647,44,2,72,FALSE,40.518,40.516149152,5462.808026,6069.197401,1228.633206,540.398184,0,17364.29475,5740.716472,370.847129,5.18697,120,"mbank_training" +"project4397","default",3,3849,75,1649,22,2,100,FALSE,18.2539999999999,18.252467134,2903.384257,3034.807995,547.422862,272.134946,0,8130.583929,2706.994697,187.14634,2.482476,120,"mbank_training" +"project4397","default",4,3850,75,1646,100,0,100,FALSE,89.067,62.749311293,12699.873477,14195.273694,2819.594899,1371.308273,0,42354.846242,13338.153642,863.920671,23.747802,120,"mbank_training" +"project4397","default",5,3851,75,1646,100,1,43,FALSE,88.2970000000005,8.63749485,12756.82406,14581.059802,2551.350852,1244.568156,0,41346.437513,13615.086309,876.091028,0,120,"mbank_training" +"project2084_(1)","default",1,3847,86,28962,3,1,1,TRUE,108.528,39.357658916,23627.712028,24029.377952,5464.093052,1094.520444,0,35104.689634,17833.210932,1040.792512,0,120,"mbank_training" +"project2084_(1)","default",2,3848,86,28206,2,1,2,TRUE,110.605,43.882252328,21477.758523,22676.801355,8385.164939,1728.20278,0,33082.151982,20161.210155,644.290514,0,120,"mbank_training" +"project2084_(1)","default",3,3849,86,28303,3,1,1,TRUE,108.306,42.637740632,30964.641428,23462.698032,5351.663578,1468.335058,0,32030.349028,13873.619593,887.377121,0,120,"mbank_training" +"project2084_(1)","default",4,3850,86,28724,3,0,1,TRUE,108.268,79.500306167,21209.336022,16545.544527,6915.74316,1698.567909,0,40609.952976,18209.011495,999.977684,1818.540659,120,"mbank_training" +"project2084_(1)","default",5,3851,86,29024,4,1,1,TRUE,108.461,78.301270838,25734.337567,17627.057703,7778.349417,1409.668754,0,38256.493533,16373.841466,1007.174092,0,120,"mbank_training" +"project2771","default",1,3847,94,1042,65,1,16,TRUE,109.469999999999,90.147688908,7955.523496,3364.064349,5118.545353,1685.906249,0,59102.753807,29649.557642,1129.086726,4.686277,120,"mbank_training" +"project2771","default",2,3848,94,1049,65,1,10,TRUE,109.496,109.495686787,8276.423813,3896.479947,5070.025056,1554.401001,0,60209.181891,27813.96109,1150.255409,25.301502,120,"mbank_training" +"project2771","default",3,3849,94,1055,65,1,10,TRUE,108.414,108.413357524,7932.479499,4160.669826,6047.767008,1677.958011,0,59065.792055,27961.877695,1152.576054,0,120,"mbank_training" +"project2771","default",4,3850,94,1046,66,0,6,TRUE,108.206,20.66770794,8170.151603,3690.397114,5343.117597,1847.35924,0,60061.381446,27735.459754,1123.077726,38.827043,120,"mbank_training" +"project2771","default",5,3851,94,1059,65,1,1,TRUE,108.043000000001,44.486758246,8124.186883,3750.441696,4893.377873,1690.985489,0,59411.915119,29030.299076,1117.712092,0,120,"mbank_training" +"project2184","default",1,3847,114,565,13,1,100,FALSE,16.5100000000002,10.270284347,1901.604448,4187.802544,481.297502,267.875047,0,7162.991061,1958.33887,236.763264,35.048523,120,"mbank_training" +"project2184","default",2,3848,114,564,8,2,100,FALSE,9.17799999999988,4.400413194,1068.136565,2525.129151,342.888574,182.385329,0,3728.285364,981.185821,141.535775,6.532746,120,"mbank_training" +"project2184","default",3,3849,114,565,18,2,100,FALSE,23.433,14.00867502,2492.973307,5244.857721,716.101013,419.309974,0,10828.214684,3285.003272,326.780788,18.981366,120,"mbank_training" +"project2184","default",4,3850,114,563,73,0,100,TRUE,108.129000000001,15.334493816,9759.896311,23116.913797,2974.664553,1610.739116,0,53197.148769,15938.358971,1339.551383,64.034474,120,"mbank_training" +"project2184","default",5,3851,114,564,18,2,100,FALSE,24.5619999999999,1.094052073,2382.013724,5658.68694,772.047049,388.245695,0,11377.309218,3381.536146,324.950291,12.937261,120,"mbank_training" +"project3938","default",1,3847,119,3417,9,2,100,FALSE,44.5619999999999,19.075341196,11413.369643,15029.814616,2202.384628,586.820894,0,10863.89477,3556.837676,388.669586,26.65143,120,"mbank_training" +"project3938","default",2,3848,119,3408,18,1,100,TRUE,108.633,20.195418378,26861.257588,33098.946527,4015.335393,1157.836335,0,29753.222391,12283.646294,828.075363,0,120,"mbank_training" +"project3938","default",3,3849,119,3413,9,2,100,FALSE,46.6349999999993,25.918489915,12043.938351,14090.206009,1790.431927,539.632353,0,12164.741083,4564.794154,398.036473,187.663721,120,"mbank_training" +"project3938","default",4,3850,119,3408,18,0,100,TRUE,108.391,83.647681499,24710.571406,34061.871319,4085.051645,1121.98776,0,30625.896483,12543.803153,802.23311,62.598762,120,"mbank_training" +"project3938","default",5,3851,119,3405,18,1,100,TRUE,108.735000000001,108.725656538,27374.72291,33415.734207,2665.163714,1469.101328,0,31037.667179,11162.01504,797.550904,62.214628,120,"mbank_training" +"syab07201","default",1,3847,125,14933,12,1,3,TRUE,108.728,12.592821236,13570.480664,8180.130151,3577.144371,1630.401141,0,51213.95676,28665.633721,1232.812355,0,120,"mbank_training" +"syab07201","default",2,3848,125,14931,12,1,1,TRUE,108.170999999999,89.087282798,13846.124014,8503.522116,3033.045252,1743.023891,0,56010.682657,23629.967986,1254.266463,0,120,"mbank_training" +"syab07201","default",3,3849,125,14932,12,1,4,TRUE,109.077,82.250482396,14298.112856,7593.533046,3447.583924,1667.188132,0,53409.384596,26372.219385,1269.450238,0,120,"mbank_training" +"syab07201","default",4,3850,125,14948,13,1,3,TRUE,108.708,58.209723111,14448.190969,8478.906994,3301.745774,1760.348587,0,52382.540065,26330.066606,1350.91075,0,120,"mbank_training" +"syab07201","default",5,3851,125,14926,9,1,2,TRUE,108.34,38.305426931,13591.55147,10687.585468,2838.502225,2031.87457,0,52607.030956,24885.943975,1424.417762,0,120,"mbank_training" +"project4133","default",1,3847,131,2371,28,1,100,TRUE,109.487999999999,109.485362629,18339.628663,29653.665094,3505.219405,1074.854075,0,37083.883069,17530.824584,823.678609,0,120,"mbank_training" +"project4133","default",2,3848,131,2379,28,1,100,TRUE,109.438,12.828640628,18298.553113,32054.720869,3214.405102,1234.216956,0,35374.579163,17020.561662,822.438309,0,120,"mbank_training" +"project4133","default",3,3849,131,2378,28,1,100,TRUE,109.021000000001,16.15741946,19005.412995,29148.022626,4032.380029,1146.585333,0,37006.380772,16787.657354,879.708429,0,120,"mbank_training" +"project4133","default",4,3850,131,2372,25,1,100,TRUE,109.190000000001,109.18586107,18321.807637,32304.089003,3145.022262,1071.913018,0,36400.932524,15981.659703,761.298839,0,120,"mbank_training" +"project4133","default",5,3851,131,2376,27,1,100,TRUE,108.601000000001,108.597659452,19342.257079,29311.053978,3138.549354,1310.424968,0,37076.326113,17006.773101,825.497051,0,120,"mbank_training" +"project804","default",1,3847,173,1361,5,1,100,TRUE,119.700000000001,119.687429526,18892.753258,22370.05734,6848.462855,1344.700911,0,38968.418347,18451.002179,1120.532486,0,120,"mbank_training" +"project804","default",2,3848,173,1361,6,1,77,TRUE,120.030999999999,120.019861675,19661.854592,29304.260402,6104.284431,1238.988987,0,35207.71569,15866.336486,1093.321497,0,120,"mbank_training" +"project804","default",3,3849,173,1374,6,1,37,TRUE,120.075000000001,120.066968555,20123.993779,21883.447515,4358.387938,1306.7305,0,35922.962296,22978.080261,1861.523875,0,120,"mbank_training" +"project804","default",4,3850,173,1363,5,1,100,TRUE,115.026,115.013226373,19002.039202,30904.950987,10687.222416,1399.585322,0,33172.285515,12033.102396,860.626358,0,120,"mbank_training" +"project804","default",5,3851,173,1363,7,1,100,TRUE,115.235999999999,115.223057538,22282.004866,20105.230448,6611.483638,1417.110018,0,39375.434188,17047.971526,1247.914574,0,120,"mbank_training" +"project4284","default",1,3847,4062,1072,0,1,100,TRUE,349.493,349.46794991,103451.107481,13308.954479,0,0,0,0,0,0,0,120,"mbank_training" +"project4284","default",2,3848,4062,1322,0,1,100,TRUE,462.067999999999,461.989119379,101258.802999,13033.694143,0,0,0,0,0,0,0,120,"mbank_training" +"project4284","default",3,3849,4062,1193,0,1,1,TRUE,120.931999999999,120.925018555,108451.106187,12471.801843,0,0,0,0,0,0,0,120,"mbank_training" +"project4284","default",4,3850,4062,1040,0,1,100,TRUE,333.196,333.186980399,95453.13475,13018.854749,0,0,0,0,0,0,0,120,"mbank_training" +"project4284","default",5,3851,4062,1220,0,1,100,TRUE,279.598,279.591312384,98085.089315,17653.911914,0,0,0,0,0,0,0,120,"mbank_training" diff --git a/dev/benchmarks/t252_setup_and_run.sh b/dev/benchmarks/t252_setup_and_run.sh new file mode 100644 index 000000000..e5fa0866b --- /dev/null +++ b/dev/benchmarks/t252_setup_and_run.sh @@ -0,0 +1,97 @@ +#!/bin/bash +#SBATCH --job-name=t252-mbank +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=8G +#SBATCH --time=10:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t252_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t252_%j.err + +# T-252: MorphoBank training-set baseline benchmark +# Phase 1: Install dependencies +# Phase 2: Install TreeSearch +# Phase 3: Run 25 matrices x 3 budgets x 5 seeds = 375 runs + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t252_results + +mkdir -p "$LIB" "$OUTDIR" /nobackup/$USER/TreeSearch/logs + +echo "=== T-252 MorphoBank Training-Set Benchmark ===" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "" + +# Phase 1: Install R dependencies +echo "=== Phase 1: Installing R dependencies ===" +export R_LIBS_USER="$LIB" +Rscript -e " + .libPaths(c('$LIB', .libPaths())) + needed <- c('Rcpp', 'ape', 'TreeTools', 'TreeDist', 'Rdpack', + 'cli', 'fastmatch', 'abind', 'colorspace') + missing <- needed[!vapply(needed, requireNamespace, logical(1), quietly = TRUE)] + if (length(missing) > 0) { + cat('Installing:', paste(missing, collapse = ', '), '\n') + install.packages(missing, lib = '$LIB', + repos = 'https://cloud.r-project.org', Ncpus = 1) + } else { + cat('All dependencies already installed\n') + } + # Verify + ok <- vapply(needed, requireNamespace, logical(1), quietly = TRUE) + if (!all(ok)) { + stop('Still missing: ', paste(needed[!ok], collapse = ', ')) + } + cat('All', length(needed), 'dependencies OK\n') +" 2>&1 +rc=$? +if [ $rc -ne 0 ]; then + echo "FATAL: dependency installation failed" + exit 1 +fi + +# Phase 2: Install TreeSearch +echo "" +echo "=== Phase 2: Installing TreeSearch ===" +cd "$REPO" || exit 1 +git pull --ff-only origin cpp-search 2>/dev/null || true +echo "Git HEAD: $(git log --oneline -1)" + +rm -f src/*.o src/*.so +R CMD build --no-build-vignettes --no-manual --no-resave-data . +R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz +rc=$? +echo "Install exit code: $rc" +rm -f TreeSearch_*.tar.gz + +if [ $rc -ne 0 ]; then + echo "FATAL: TreeSearch install failed" + exit 1 +fi + +# Verify neotrans +NEOTRANS=/nobackup/$USER/neotrans/inst/matrices +if [ ! -d "$NEOTRANS" ] || [ "$(ls $NEOTRANS | wc -l)" -eq 0 ]; then + echo "FATAL: neotrans matrices not found or empty at $NEOTRANS" + exit 1 +fi +echo "Neotrans matrices: $(ls $NEOTRANS | wc -l) files" + +# Phase 3: Run benchmark +echo "" +echo "=== Phase 3: Running benchmark ===" +cd "$REPO" +Rscript dev/benchmarks/bench_t252_mbank_training.R "$OUTDIR" 2>&1 + +echo "" +echo "=== Completed: $(date) ===" +echo "Results in: $OUTDIR" +ls -la "$OUTDIR"/t252_*.csv 2>/dev/null diff --git a/dev/benchmarks/t252_v2.sh b/dev/benchmarks/t252_v2.sh new file mode 100644 index 000000000..b9f3ca489 --- /dev/null +++ b/dev/benchmarks/t252_v2.sh @@ -0,0 +1,89 @@ +#!/bin/bash +#SBATCH --job-name=t252-mbank +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=8G +#SBATCH --time=8:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t252_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t252_%j.err + +# T-252: MorphoBank training-set baseline benchmark (v2 — fixed lib paths) +# 25 matrices x 3 budgets (30/60/120s) x 5 seeds = 375 runs (~5 hours) +# +# Uses ts-bench/lib-baseline for all deps (TreeDist, TreeTools, etc.), +# installs only the fresh TreeSearch build into TreeSearch/lib-t252. + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +FRESH_LIB=/nobackup/$USER/TreeSearch/lib-t252 +DEP_LIB=/nobackup/$USER/ts-bench/lib-baseline +OUTDIR=/nobackup/$USER/TreeSearch/t252_results + +mkdir -p "$FRESH_LIB" "$OUTDIR" /nobackup/$USER/TreeSearch/logs + +echo "=== T-252 MorphoBank Training-Set Benchmark v2 ===" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "Fresh lib: $FRESH_LIB" +echo "Dep lib: $DEP_LIB" +echo "" + +# Phase 1: Build and install TreeSearch (deps resolved from DEP_LIB) +echo "=== Building TreeSearch from cpp-search ===" +cd "$REPO" || exit 1 +git pull --ff-only origin cpp-search 2>/dev/null || true +echo "Git HEAD: $(git log --oneline -1)" + +rm -f src/*.o src/*.so +TMPBUILD=$(mktemp -d) +(cd "$TMPBUILD" && R CMD build --no-build-vignettes --no-manual --no-resave-data "$REPO") + +# Install using both libs so R can find TreeSearch's Imports during install +export R_LIBS="$FRESH_LIB:$DEP_LIB" +R CMD INSTALL --library="$FRESH_LIB" "$TMPBUILD"/TreeSearch_*.tar.gz +rc=$? +rm -rf "$TMPBUILD" +echo "Install exit code: $rc" + +if [ $rc -ne 0 ]; then + echo "FATAL: TreeSearch install failed" + exit 1 +fi + +# Verify the install loaded correctly +Rscript -e " + .libPaths(c('$FRESH_LIB', '$DEP_LIB', .libPaths())) + library(TreeSearch) + cat('TreeSearch version:', as.character(packageVersion('TreeSearch')), '\n') +" +rc=$? +if [ $rc -ne 0 ]; then + echo "FATAL: TreeSearch failed to load" + exit 1 +fi + +# Phase 2: Verify neotrans corpus +NEOTRANS=/nobackup/$USER/neotrans/inst/matrices +if [ ! -d "$NEOTRANS" ] || [ "$(ls $NEOTRANS | wc -l)" -eq 0 ]; then + echo "FATAL: neotrans matrices not found at $NEOTRANS" + exit 1 +fi +echo "Neotrans matrices: $(ls $NEOTRANS | wc -l) files" + +# Phase 3: Run benchmark +echo "" +echo "=== Running benchmark ===" +cd "$REPO" +export R_LIBS="$FRESH_LIB:$DEP_LIB" +Rscript dev/benchmarks/bench_t252_mbank_training.R "$OUTDIR" 2>&1 + +echo "" +echo "=== Completed: $(date) ===" +echo "Results in: $OUTDIR" +ls -la "$OUTDIR"/t252_*.csv 2>/dev/null diff --git a/dev/benchmarks/t253_conv_gap_mbank.csv b/dev/benchmarks/t253_conv_gap_mbank.csv new file mode 100644 index 000000000..6392d9c22 --- /dev/null +++ b/dev/benchmarks/t253_conv_gap_mbank.csv @@ -0,0 +1,26 @@ +dataset,s_30,s_120,conv_gap,ntax,nchar,n_patterns,pct_missing,pct_inapp +project2084_(1),28962,28724,238,86,3660,3601,20.9,24.9 +project2184,564,564,0,114,205,168,1.7,2.5 +project2346,317,317,0,23,144,141,18,28.5 +project2451,731,731,0,24,380,367,54.5,0 +project2762,259,259,0,29,187,177,34.8,17.1 +project2771,1055,1049,6,94,124,123,1,30 +project3688,850,851,-1,60,245,245,57.3,0 +project3938,3416,3408,8,119,677,677,52.6,4.3 +project4049,5239,5237,2,60,721,719,22.2,0 +project4133,2377,2376,1,131,349,349,31.3,6 +project4146_(3),261,261,0,59,130,130,18.1,45.6 +project423,495,495,0,60,253,219,12.2,15.4 +project4284,1268,1193,75,4062,27,27,82.9,2.6 +project4286,283,282,1,63,135,135,18.7,46.7 +project4359,183,183,0,71,245,146,83.8,3.1 +project4397,1648,1646,2,75,223,222,32.3,4.6 +project4501,118,118,0,24,42,41,4.1,13.1 +project532,1139,1139,0,21,674,427,15.9,2 +project561,1169,1169,0,34,356,329,5,9.6 +project571,634,634,0,42,125,125,16.8,4.2 +project804,1373,1363,10,173,589,569,32.8,30.9 +project826,431,431,0,33,218,213,61.7,0.1 +project944,128,128,0,25,72,72,17.2,0.9 +project971_(1),157,157,0,26,101,73,53.3,0.5 +syab07201,14953,14932,21,125,2954,2813,28.3,0 diff --git a/dev/benchmarks/t253_gap_characterization.md b/dev/benchmarks/t253_gap_characterization.md new file mode 100644 index 000000000..c1ed84fbb --- /dev/null +++ b/dev/benchmarks/t253_gap_characterization.md @@ -0,0 +1,110 @@ +# T-253: Gap Characterization by Dataset Features + +**Date:** 2026-03-27 +**Agent:** F +**Data sources:** +- `t265_results/t265_phase1_20260326_1617.csv` — 8 named datasets, fitch_mode EW, 120s (TNT vs TreeSearch, apples-to-apples) +- `t252_mbank_*` CSVs — 25 MorphoBank training matrices, TreeSearch 30/60/120s (convergence proxy) + +--- + +## Summary + +**ntax is the primary predictor of search difficulty** in both analyses (Spearman ρ ≈ 0.63). +At ≤60 taxa with modest character counts, TreeSearch converges fully at 30s. +Difficulty increases steadily above ~75 taxa and becomes acute above ~120 taxa. + +Character count (nchar) matters only at extremes (e.g. 3660 chars, 2954 chars); +pct_missing and pct_inapp show moderate individual correlations (ρ = 0.49–0.55 in T-265) +but inconsistent signal in the MorphoBank sample — small samples mean these +correlations are unreliable beyond the ntax signal. + +--- + +## TNT comparison gaps (T-265, fitch_mode, 120s, 8 datasets) + +These are the only reliable apples-to-apples gaps (Fitch TreeSearch vs TNT Fitch). + +| Dataset | ntax | nchar | pct_missing | pct_inapp | median_gap | +|---------|-----:|------:|:-----------:|:---------:|:----------:| +| Zanol2014 | 74 | 213 | 11.7% | 16.6% | **3** | +| Zhu2013 | 75 | 253 | 42.6% | 12.4% | **3** | +| Conrad2008 | 64 | 363 | 23.4% | 5.1% | 2 | +| Giles2015 | 78 | 236 | 41.5% | 11.8% | 2 | +| OMeara2014 | 63 | 317 | 43.4% | 5.4% | 2 | +| Liljeblad2008 | 68 | 308 | 5.2% | 5.6% | 0 | +| Wetterer2000 | 63 | 150 | 21.2% | 7.7% | 0 | +| Wilson2003 | 61 | 165 | 7.7% | 8.6% | 0 | + +Spearman correlations with `median_gap`: + +| Feature | ρ | +|---------|:-:| +| ntax | 0.63 | +| pct_missing | 0.55 | +| pct_inapp | 0.49 | +| nchar | 0.28 | +| n_patterns | 0.28 | + +**Note:** n=8 is too small for reliable multivariate analysis. The pct_missing/pct_inapp +signals may be confounded with ntax (larger datasets often have more missing data). + +--- + +## Convergence gaps (T-252, MorphoBank 25 matrices, 30s → 120s improvement) + +Most matrices converge fully at 30s (gap=0). Non-zero gap datasets: + +| Dataset | ntax | nchar | pct_miss | pct_inapp | conv_gap | +|---------|-----:|------:|:--------:|:---------:|:--------:| +| project2068 | 86 | **3660** | 20.9% | 24.9% | 238 | +| project4284 | **4062** | 27 | 82.9% | 0% | 75 | +| syab072 | 125 | 2954 | 28.3% | ? | 21 | +| project804 | 173 | 589 | 32.8% | ? | 10 | +| project3938 | 119 | 677 | 52.6% | 4.3% | 8 | +| project2771 | 94 | 124 | 1.0% | 30.0% | 6 | +| (others) | ≤131 | ≤721 | | | ≤2 | + +Spearman correlations with `conv_gap` (n=23, excluding 2 extreme outliers): + +| Feature | ρ | +|---------|:-:| +| ntax | **0.64** | +| n_patterns | 0.34 | +| pct_inapp | 0.36 | +| nchar | 0.30 | +| pct_missing | −0.04 | + +--- + +## Key findings + +1. **ntax is the dominant difficulty predictor** (consistent ρ ≈ 0.63 across two + independent datasets/metrics). The hard wall is around 75–130 taxa under the + current strategy presets. + +2. **nchar matters only at extremes.** project2068 (86t, 3660c) has the largest + absolute convergence gap despite modest ntax — the 3660-character search space + is simply too large per-replicate. syab072 (125t, 2954c) similarly. + +3. **Missing data and inapplicable characters** show moderate correlations in T-265 + but not in T-252. This likely reflects a confound with ntax (larger datasets often + have more missing data in MorphoBank matrices), not an independent effect. + +4. **Most datasets are already covered** (≤60 taxa, ≤700 chars): 19 of 25 MorphoBank + training matrices and all datasets ≤60 taxa converge at 30s. TreeSearch's + CRAN benchmark suite (14 datasets, ≤88 taxa) is well-covered. + +--- + +## Strategic implications for T-253 + +| Priority | Action | Targets | +|----------|--------|---------| +| High | **T-245: TBR batching** — reduce per-candidate evaluation cost | ≥75 taxa (nchar moderate) | +| High | **NNI escalation** (already in presets via `nniFirst=TRUE`) | ≥75 taxa | +| Medium | **Character batching / lazy scoring** for high-nchar datasets | ≥1000 chars | +| Low | Missing/inapplicable tuning | Not independently predictive | + +The clearest opportunity is the ≥75-taxon regime. T-245 (TBR candidate batching, +estimated ~13% gain) is the highest-value next step for search quality at scale. diff --git a/dev/benchmarks/t253_gap_features_t265.csv b/dev/benchmarks/t253_gap_features_t265.csv new file mode 100644 index 000000000..232220fd4 --- /dev/null +++ b/dev/benchmarks/t253_gap_features_t265.csv @@ -0,0 +1,9 @@ +dataset,median_gap,n_taxa,n_chars,n_patterns,n_levels,pct_missing,pct_inapp +Conrad2008,2,64,363,360,7,0.23390151515151514,0.050576790633608815 +Giles2015,2,78,236,236,3,0.41536288570186874,0.11777488048674489 +Liljeblad2008,0,68,308,299,7,0.05213903743315508,0.05576776165011459 +OMeara2014,2,63,317,315,5,0.4338290521255821,0.05437884933153072 +Wetterer2000,0,63,150,145,6,0.21206349206349207,0.07661375661375662 +Wilson2003,0,61,165,161,5,0.07660208643815201,0.08614008941877795 +Zanol2014,3,74,213,210,8,0.11737089201877934,0.16565156706001777 +Zhu2013,3,75,253,253,3,0.42582345191040843,0.12442687747035573 diff --git a/dev/benchmarks/t264_verify_20260326_1526.csv b/dev/benchmarks/t264_verify_20260326_1526.csv new file mode 100644 index 000000000..af16d8263 --- /dev/null +++ b/dev/benchmarks/t264_verify_20260326_1526.csv @@ -0,0 +1,25 @@ +"dataset","seed","score","replicates","wall_s","tnt_best","gap" +"Wilson2003",1,879,21,16.1,860,19 +"Wilson2003",2,879,29,25.7,860,19 +"Wilson2003",3,879,32,30.4,860,19 +"Wetterer2000",1,559,30,14.5,549,10 +"Wetterer2000",2,559,57,26.1,549,10 +"Wetterer2000",3,559,33,20.3,549,10 +"Conrad2008",1,1761,35,89.4,1725,36 +"Conrad2008",2,1761,29,63.6,1725,36 +"Conrad2008",3,1761,30,30.8,1725,36 +"Giles2015",1,710,40,109.1,670,40 +"Giles2015",2,710,51,108.5,670,40 +"Giles2015",3,710,26,108.7,670,40 +"Zanol2014",1,1315,12,109.5,1261,54 +"Zanol2014",2,1314,8,108.5,1261,53 +"Zanol2014",3,1319,6,110,1261,58 +"Liljeblad2008",1,2869,10,108.1,2840,29 +"Liljeblad2008",2,2869,10,108.1,2840,29 +"Liljeblad2008",3,2868,18,108.1,2840,28 +"Zhu2013",1,638,26,109.1,624,14 +"Zhu2013",2,638,20,109.2,624,14 +"Zhu2013",3,639,17,108.4,624,15 +"OMeara2014",1,1215,33,113.3,1208,7 +"OMeara2014",2,1215,28,110.1,1208,7 +"OMeara2014",3,1215,20,115.8,1208,7 diff --git a/dev/benchmarks/t265_hamilton.sh b/dev/benchmarks/t265_hamilton.sh new file mode 100644 index 000000000..84b393004 --- /dev/null +++ b/dev/benchmarks/t265_hamilton.sh @@ -0,0 +1,56 @@ +#!/bin/bash +#SBATCH --job-name=t265-regression +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=8G +#SBATCH --time=6:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t265_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t265_%j.err + +# T-265: Per-replicate quality regression diagnosis +# 3 configs x 9 datasets x 5 seeds x 120s = ~135 runs x ~120s = ~4.5 hours + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t265_results + +mkdir -p "$LIB" +mkdir -p "$OUTDIR" + +echo "=== T-265 Hamilton job ===" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "" + +# Build and install from latest cpp-search +cd "$REPO" || exit 1 +echo "Git HEAD: $(git log --oneline -1)" + +rm -f src/*.o src/*.so +R CMD build --no-build-vignettes --no-manual --no-resave-data . +R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz +rc=$? +echo "Install exit code: $rc" +rm -f TreeSearch_*.tar.gz + +if [ $rc -ne 0 ]; then + echo "FATAL: install failed" + exit 1 +fi + +# Run benchmark +cd "$OUTDIR" +Rscript -e ".libPaths(c('$LIB', .libPaths()))" \ + "$REPO/dev/benchmarks/bench_t265_regression.R" 120 "$OUTDIR" + +echo "" +echo "Completed: $(date)" +echo "Results in: $OUTDIR" +ls -la "$OUTDIR"/t265_*.csv 2>/dev/null diff --git a/dev/benchmarks/t269_hamilton.sh b/dev/benchmarks/t269_hamilton.sh new file mode 100644 index 000000000..ea779df78 --- /dev/null +++ b/dev/benchmarks/t269_hamilton.sh @@ -0,0 +1,82 @@ +#!/bin/bash +#SBATCH --job-name=t269-interleave +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=4G +#SBATCH --time=4:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t269_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t269_%j.err + +# T-269: Fine-grained sectorial interleaving benchmark +# +# 5 configs × 4 datasets × 5 seeds × {30s, 60s} = 200 runs × ~45s avg ≈ 2.5h +# +# Usage: +# sbatch t269_hamilton.sh # 30s budget +# sbatch t269_hamilton.sh 60 # 60s budget + +TIMEOUT=${1:-30} + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t269_results + +mkdir -p "$LIB" +mkdir -p "$OUTDIR" +mkdir -p /nobackup/$USER/TreeSearch/logs + +echo "=== T-269 Hamilton job ===" +echo "Timeout: ${TIMEOUT}s" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "" + +# Install CRAN dependencies into local lib (if missing) +echo "Checking/installing CRAN dependencies..." +Rscript --no-save -e " + lib <- '$LIB' + .libPaths(c(lib, .libPaths())) + pkgs <- c('ape', 'cli', 'inapplicable', 'phangorn', 'Rdpack', 'TreeDist', 'TreeTools') + need <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)] + if (length(need) > 0) { + message('Installing: ', paste(need, collapse = ', ')) + install.packages(need, lib = lib, repos = 'https://cloud.r-project.org', quiet = TRUE) + } else { + message('All dependencies present.') + } +" + +# Build and install from latest cpp-search +cd "$REPO" || exit 1 +git fetch origin cpp-search +git pull --ff-only origin cpp-search || git reset --hard origin/cpp-search +echo "Git HEAD: $(git log --oneline -1)" + +rm -f src/*.o src/*.so +R CMD build --no-build-vignettes --no-manual --no-resave-data . +R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz +rc=$? +echo "Install exit code: $rc" +rm -f TreeSearch_*.tar.gz + +if [ $rc -ne 0 ]; then + echo "FATAL: install failed" + exit 1 +fi + +# Run benchmark +cd "$OUTDIR" +export R_LIBS_USER="$LIB" +Rscript "$REPO/dev/benchmarks/bench_t269_interleaving.R" "$TIMEOUT" "$OUTDIR" + +echo "" +echo "Completed: $(date)" +echo "Results in: $OUTDIR" +ls -la "$OUTDIR"/t269_*.csv 2>/dev/null diff --git a/dev/benchmarks/t289_hamilton.sh b/dev/benchmarks/t289_hamilton.sh new file mode 100644 index 000000000..454869306 --- /dev/null +++ b/dev/benchmarks/t289_hamilton.sh @@ -0,0 +1,86 @@ +#!/bin/bash +#SBATCH --job-name=t289-prune-ri +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=4G +#SBATCH --time=8:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t289_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t289_%j.err + +# T-289: Prune-reinsert perturbation benchmark +# +# Stage 1: 13 configs × 5 datasets × 5 seeds × 30s ≈ 325 runs × ~30s ≈ 2.7h +# Stage 2: ~10 configs × 5 datasets × 5 seeds × {30s,60s} ≈ 500 runs × ~45s ≈ 6.3h +# +# Usage: +# sbatch t289_hamilton.sh # runs stage 1 (30s) +# sbatch t289_hamilton.sh 2 30 # stage 2, 30s budget +# sbatch t289_hamilton.sh 2 60 # stage 2, 60s budget + +STAGE=${1:-1} +TIMEOUT=${2:-30} + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t289_results +export R_LIBS="$LIB:${R_LIBS}" + +mkdir -p "$LIB" +mkdir -p "$OUTDIR" +mkdir -p /nobackup/$USER/TreeSearch/logs + +echo "=== T-289 Hamilton job ===" +echo "Stage: $STAGE, Timeout: ${TIMEOUT}s" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "" + +# Install CRAN dependencies into local lib (if missing) +echo "Checking/installing CRAN dependencies..." +Rscript --no-save -e " + lib <- '$LIB' + .libPaths(c(lib, .libPaths())) + pkgs <- c('abind', 'ape', 'cli', 'colorspace', 'fastmatch', 'Rdpack', 'TreeDist', 'TreeTools') + need <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)] + if (length(need) > 0) { + message('Installing: ', paste(need, collapse = ', ')) + install.packages(need, lib = lib, repos = 'https://cloud.r-project.org', + dependencies = NA, quiet = TRUE) # NA = Imports+Depends only + } else { + message('All dependencies present.') + } +" + +# Build and install from latest cpp-search +cd "$REPO" || exit 1 +git fetch origin cpp-search +git pull --ff-only origin cpp-search || git reset --hard origin/cpp-search +echo "Git HEAD: $(git log --oneline -1)" + +rm -f src/*.o src/*.so +R CMD build --no-build-vignettes --no-manual --no-resave-data . +R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz +rc=$? +echo "Install exit code: $rc" +rm -f TreeSearch_*.tar.gz + +if [ $rc -ne 0 ]; then + echo "FATAL: install failed" + exit 1 +fi + +# Run benchmark +cd "$OUTDIR" +Rscript "$REPO/dev/benchmarks/bench_prune_reinsert.R" "$STAGE" "$TIMEOUT" "$OUTDIR" + +echo "" +echo "Completed: $(date)" +echo "Results in: $OUTDIR" +ls -la "$OUTDIR"/t289_*.csv 2>/dev/null diff --git a/dev/benchmarks/t289b_brazeau_hamilton.sh b/dev/benchmarks/t289b_brazeau_hamilton.sh new file mode 100644 index 000000000..065a25a26 --- /dev/null +++ b/dev/benchmarks/t289b_brazeau_hamilton.sh @@ -0,0 +1,103 @@ +#!/bin/bash +#SBATCH --job-name=t289b-brazeau +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=4G +#SBATCH --time=8:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t289b_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t289b_%j.err + +# T-289b: Prune-reinsert benchmark — Brazeau (default) scoring +# +# Parallel companion to t289_hamilton.sh (Fitch/EW mode). +# Uses TreeSearch's default Brazeau et al. (2019) inapplicable scoring. +# Shares the same build artifact as the Fitch job — no rebuild needed +# if t289_hamilton.sh has already installed TreeSearch in $LIB. +# +# Stage 1: 13 configs x 5 datasets x 5 seeds x 30s ≈ 325 runs ≈ 2.7h +# Stage 2: ~10 configs x 5 datasets x 5 seeds x {30s,60s} ≈ 500 runs ≈ 6.3h +# +# Usage: +# sbatch t289b_brazeau_hamilton.sh # stage 1, 30s +# sbatch t289b_brazeau_hamilton.sh 2 30 # stage 2, 30s +# sbatch t289b_brazeau_hamilton.sh 2 60 # stage 2, 60s + +STAGE=${1:-1} +TIMEOUT=${2:-30} + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t289b_results +export R_LIBS="$LIB:${R_LIBS}" + +mkdir -p "$LIB" +mkdir -p "$OUTDIR" +mkdir -p /nobackup/$USER/TreeSearch/logs + +echo "=== T-289b Hamilton job (Brazeau scoring) ===" +echo "Stage: $STAGE, Timeout: ${TIMEOUT}s" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "" + +# Install CRAN dependencies into local lib (if missing) +echo "Checking/installing CRAN dependencies..." +Rscript --no-save -e " + lib <- '$LIB' + .libPaths(c(lib, .libPaths())) + pkgs <- c('abind', 'ape', 'cli', 'colorspace', 'fastmatch', 'Rdpack', 'TreeDist', 'TreeTools') + need <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)] + if (length(need) > 0) { + message('Installing: ', paste(need, collapse = ', ')) + install.packages(need, lib = lib, repos = 'https://cloud.r-project.org', + dependencies = NA, quiet = TRUE) + } else { + message('All dependencies present.') + } +" + +# Build and install from latest cpp-search +# (Skip rebuild if TreeSearch is already installed and up to date; +# rebuild if the Fitch job hasn't run yet or if HEAD has moved.) +cd "$REPO" || exit 1 +git fetch origin cpp-search +git pull --ff-only origin cpp-search || git reset --hard origin/cpp-search +echo "Git HEAD: $(git log --oneline -1)" + +INSTALLED_VER=$(Rscript --no-save -e \ + ".libPaths(c('$LIB', .libPaths())); cat(as.character(packageVersion('TreeSearch')))" \ + 2>/dev/null || echo "none") +REPO_VER=$(grep '^Version:' DESCRIPTION | awk '{print $2}') +echo "Installed: $INSTALLED_VER Repo: $REPO_VER" + +if [ "$INSTALLED_VER" != "$REPO_VER" ]; then + echo "Rebuilding TreeSearch..." + rm -f src/*.o src/*.so + R CMD build --no-build-vignettes --no-manual --no-resave-data . + R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz + rc=$? + echo "Install exit code: $rc" + rm -f TreeSearch_*.tar.gz + if [ $rc -ne 0 ]; then + echo "FATAL: install failed" + exit 1 + fi +else + echo "TreeSearch already up to date; skipping rebuild." +fi + +# Run benchmark +cd "$OUTDIR" +Rscript "$REPO/dev/benchmarks/bench_prune_reinsert_brazeau.R" "$STAGE" "$TIMEOUT" "$OUTDIR" + +echo "" +echo "Completed: $(date)" +echo "Results in: $OUTDIR" +ls -la "$OUTDIR"/t289b_*.csv 2>/dev/null diff --git a/dev/benchmarks/t289c_stage2_hamilton.sh b/dev/benchmarks/t289c_stage2_hamilton.sh new file mode 100644 index 000000000..400af6e3b --- /dev/null +++ b/dev/benchmarks/t289c_stage2_hamilton.sh @@ -0,0 +1,96 @@ +#!/bin/bash +#SBATCH --job-name=t289c-pr-s2 +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=6G +#SBATCH --time=3:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t289c_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t289c_%j.err + +# T-289c: Prune-reinsert Stage 2 — mbank_X30754 only, 60s budget +# +# Stage 1 (5 datasets × 13 configs × 5 seeds × 30s) verdict: +# - ≤88t: PR net-negative (replicate cost >> score gain). Not tested here. +# - 180t: Real signal. pr_c5_d10 most consistent (5/5 seeds, mean −6.6 steps). +# +# Stage 2 grid: 9 configs × 1 dataset × 10 seeds = 90 runs × ~65s ≈ 98 min. +# SBATCH --time=3:00:00 provides comfortable margin. +# +# Usage: +# sbatch t289c_stage2_hamilton.sh [timeout_s] +# Default timeout: 60s + +TIMEOUT=${1:-60} + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t289c_results +export R_LIBS="$LIB:${R_LIBS}" + +mkdir -p "$LIB" "$OUTDIR" /nobackup/$USER/TreeSearch/logs + +echo "=== T-289c Hamilton job (PR Stage 2) ===" +echo "Timeout: ${TIMEOUT}s" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "" + +# Install CRAN dependencies if missing +echo "Checking CRAN dependencies..." +Rscript --no-save -e " + lib <- '$LIB' + .libPaths(c(lib, .libPaths())) + pkgs <- c('abind', 'ape', 'cli', 'colorspace', 'fastmatch', 'Rdpack', 'TreeDist', 'TreeTools') + need <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)] + if (length(need) > 0) { + message('Installing: ', paste(need, collapse = ', ')) + install.packages(need, lib = lib, repos = 'https://cloud.r-project.org', + dependencies = NA, quiet = TRUE) + } else { + message('All dependencies present.') + } +" + +# Build and install TreeSearch from cpp-search +cd "$REPO" || exit 1 +git fetch origin cpp-search +git pull --ff-only origin cpp-search || git reset --hard origin/cpp-search +echo "Git HEAD: $(git log --oneline -1)" + +INSTALLED_VER=$(Rscript --no-save -e \ + ".libPaths(c('$LIB', .libPaths())); cat(as.character(packageVersion('TreeSearch')))" \ + 2>/dev/null || echo "none") +REPO_VER=$(grep '^Version:' DESCRIPTION | awk '{print $2}') +echo "Installed: $INSTALLED_VER Repo: $REPO_VER" + +if [ "$INSTALLED_VER" != "$REPO_VER" ]; then + echo "Rebuilding TreeSearch..." + rm -f src/*.o src/*.so + R CMD build --no-build-vignettes --no-manual --no-resave-data . + R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz + rc=$? + rm -f TreeSearch_*.tar.gz + echo "Install exit code: $rc" + if [ $rc -ne 0 ]; then + echo "FATAL: install failed" + exit 1 + fi +else + echo "TreeSearch already up to date; skipping rebuild." +fi + +# Run benchmark +cd "$OUTDIR" +Rscript "$REPO/dev/benchmarks/bench_pr_stage2_mbank.R" "$TIMEOUT" "$OUTDIR" + +echo "" +echo "Completed: $(date)" +echo "Results in: $OUTDIR" +ls -lh "$OUTDIR"/t289c_*.csv 2>/dev/null diff --git a/dev/benchmarks/t289d_stage3_hamilton.sh b/dev/benchmarks/t289d_stage3_hamilton.sh new file mode 100644 index 000000000..e605f62a7 --- /dev/null +++ b/dev/benchmarks/t289d_stage3_hamilton.sh @@ -0,0 +1,81 @@ +#!/bin/bash +#SBATCH --job-name=t289d-pr-s3 +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=6G +#SBATCH --time=3:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t289d_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t289d_%j.err + +# T-289d: Prune-reinsert Stage 3 — new drop criteria (MISSING, COMBINED) +# +# Requires TreeSearch >= commit 1ce5e12e (feat: MISSING+COMBINED criteria). +# +# Grid: 8 configs × 1 dataset × 10 seeds × 60s ≈ 87 min. +# SBATCH --time=3:00:00 provides comfortable margin. +# +# Usage: +# sbatch t289d_stage3_hamilton.sh [timeout_s] +# Default: 60s + +TIMEOUT=${1:-60} + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t289d_results +export R_LIBS="$LIB:${R_LIBS}" + +mkdir -p "$LIB" "$OUTDIR" /nobackup/$USER/TreeSearch/logs + +echo "=== T-289d Hamilton job (PR Stage 3 — new criteria) ===" +echo "Timeout: ${TIMEOUT}s" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "" + +# Install CRAN dependencies if missing +Rscript --no-save -e " + lib <- '$LIB' + .libPaths(c(lib, .libPaths())) + pkgs <- c('abind', 'ape', 'cli', 'colorspace', 'fastmatch', 'Rdpack', 'TreeDist', 'TreeTools') + need <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)] + if (length(need) > 0) { + message('Installing: ', paste(need, collapse = ', ')) + install.packages(need, lib = lib, repos = 'https://cloud.r-project.org', + dependencies = NA, quiet = TRUE) + } else { message('All dependencies present.') } +" + +# Always rebuild — Stage 3 requires the new MISSING/COMBINED criteria +# (commit 1ce5e12e on cpp-search). +cd "$REPO" || exit 1 +git fetch origin cpp-search +git pull --ff-only origin cpp-search || git reset --hard origin/cpp-search +echo "Git HEAD: $(git log --oneline -1)" + +echo "Rebuilding TreeSearch (new criteria require recompile)..." +rm -f src/*.o src/*.so src/*.dll +R CMD build --no-build-vignettes --no-manual --no-resave-data . +R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz +rc=$? +rm -f TreeSearch_*.tar.gz +echo "Install exit code: $rc" +if [ $rc -ne 0 ]; then + echo "FATAL: install failed" + exit 1 +fi + +# Run benchmark +cd "$OUTDIR" +Rscript "$REPO/dev/benchmarks/bench_pr_stage3_mbank.R" "$TIMEOUT" "$OUTDIR" + +echo "" +echo "Completed: $(date)" +ls -lh "$OUTDIR"/t289d_*.csv 2>/dev/null diff --git a/dev/benchmarks/t289e_stage4_hamilton.sh b/dev/benchmarks/t289e_stage4_hamilton.sh new file mode 100644 index 000000000..2d7a89a83 --- /dev/null +++ b/dev/benchmarks/t289e_stage4_hamilton.sh @@ -0,0 +1,74 @@ +#!/bin/bash +#SBATCH --job-name=t289e-pr-s4 +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=8G +#SBATCH --time=8:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t289e_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t289e_%j.err + +# T-289e: Prune-reinsert Stage 4 — multi-dataset validation +# +# Validates that PR (c=5, d=5%, MISSING) benefit generalises across 5 large-tree +# matrices (131-206 tips) and persists at 120s budget. +# +# Grid: 5 datasets × 2 configs × 2 budgets × 10 seeds = 200 runs +# Expected wall time: ~5h; 8h limit provides comfortable margin. + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t289e_results +export R_LIBS="$LIB:${R_LIBS}" + +mkdir -p "$LIB" "$OUTDIR" /nobackup/$USER/TreeSearch/logs + +echo "=== T-289e Hamilton job (PR Stage 4 — multi-dataset validation) ===" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "" + +# Install CRAN dependencies if missing +Rscript --no-save -e " + lib <- '$LIB' + .libPaths(c(lib, .libPaths())) + pkgs <- c('abind', 'ape', 'cli', 'colorspace', 'fastmatch', 'Rdpack', 'TreeDist', 'TreeTools') + need <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)] + if (length(need) > 0) { + message('Installing: ', paste(need, collapse = ', ')) + install.packages(need, lib = lib, repos = 'https://cloud.r-project.org', + dependencies = NA, quiet = TRUE) + } else { message('All dependencies present.') } +" + +# Rebuild — Stage 4 runs the large preset with PR (commit in cpp-search) +cd "$REPO" || exit 1 +git fetch origin cpp-search +git pull --ff-only origin cpp-search || git reset --hard origin/cpp-search +echo "Git HEAD: $(git log --oneline -1)" + +echo "Rebuilding TreeSearch..." +rm -f src/*.o src/*.so src/*.dll +R CMD build --no-build-vignettes --no-manual --no-resave-data . +R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz +rc=$? +rm -f TreeSearch_*.tar.gz +echo "Install exit code: $rc" +if [ $rc -ne 0 ]; then + echo "FATAL: install failed" + exit 1 +fi + +# Run benchmark +cd "$OUTDIR" +Rscript "$REPO/dev/benchmarks/bench_pr_stage4_validation.R" "$OUTDIR" + +echo "" +echo "Completed: $(date)" +ls -lh "$OUTDIR"/t289e_*.csv 2>/dev/null diff --git a/dev/benchmarks/t289f_pr_nni_polish.csv b/dev/benchmarks/t289f_pr_nni_polish.csv new file mode 100644 index 000000000..141a7e828 --- /dev/null +++ b/dev/benchmarks/t289f_pr_nni_polish.csv @@ -0,0 +1,593 @@ +'dataset','n_tips','n_patterns','config','seed','timeout_s','score','n_trees','replicates','hits','wall_s','pr_cycles','pr_nni' +'mbank_X30754',180,425,'baseline',9,60,1197,100,4,1,56.715,0,0 +'mbank_X30754',180,425,'baseline',1,60,1177,100,2,1,59.092,0,0 +'mbank_X30754',180,425,'baseline',10,60,1176,100,3,1,55.062,0,0 +'mbank_X30754',180,425,'baseline',2,60,1190,100,2,1,54.989,0,0 +'mbank_X30754',180,425,'pr_nni',1,60,1180,100,2,1,55.082,5,1 +'mbank_X30754',180,425,'baseline',3,60,1222,100,2,1,54.731,0,0 +'mbank_X30754',180,425,'pr_nni',2,60,1179,100,2,1,57.167,5,1 +'mbank_X30754',180,425,'baseline',4,60,1177,100,2,1,57.861,0,0 +'mbank_X30754',180,425,'pr_nni',3,60,1179,100,2,1,55.015,5,1 +'mbank_X30754',180,425,'baseline',5,60,1194,100,3,1,55.508,0,0 +'mbank_X30754',180,425,'pr_nni',4,60,1199,100,2,1,55.457,5,1 +'mbank_X30754',180,425,'baseline',6,60,1197,100,3,1,55.868,0,0 +'mbank_X30754',180,425,'pr_nni',5,60,1179,100,2,1,56.435,5,1 +'mbank_X30754',180,425,'baseline',7,60,1185,100,2,1,56.900,0,0 +'mbank_X30754',180,425,'pr_nni',6,60,1217,100,2,1,59.724,5,1 +'mbank_X30754',180,425,'baseline',8,60,1189,100,2,1,54.543,0,0 +'mbank_X30754',180,425,'pr_nni',7,60,1176,100,2,1,55.750,5,1 +'mbank_X30754',180,425,'baseline',9,60,1204,100,2,1,55.588,0,0 +'mbank_X30754',180,425,'pr_nni',8,60,1180,100,2,1,55.757,5,1 +'mbank_X30754',180,425,'baseline',10,60,1192,100,2,1,55.061,0,0 +'mbank_X30754',180,425,'pr_nni',9,60,1204,100,2,2,55.467,5,1 +'mbank_X30754',180,425,'pr_nni',1,60,1185,100,2,1,55.101,5,1 +'mbank_X30754',180,425,'pr_nni',10,60,1188,100,2,1,54.681,5,1 +'mbank_X30754',180,425,'pr_nni',2,60,1176,100,2,1,55.528,5,1 +'mbank_X30754',180,425,'pr_tbr',1,60,1193,100,2,1,56.211,5,0 +'mbank_X30754',180,425,'pr_nni',3,60,1183,100,2,1,55.575,5,1 +'mbank_X30754',180,425,'pr_tbr',2,60,1198,100,2,1,55.143,5,0 +'mbank_X30754',180,425,'pr_nni',4,60,1191,100,2,1,56.851,5,1 +'mbank_X30754',180,425,'pr_tbr',3,60,1196,100,2,1,54.840,5,0 +'mbank_X30754',180,425,'pr_nni',5,60,1192,100,2,1,58.001,5,1 +'mbank_X30754',180,425,'pr_tbr',4,60,1209,100,2,1,54.656,5,0 +'mbank_X30754',180,425,'pr_nni',6,60,1191,100,2,1,56.559,5,1 +'mbank_X30754',180,425,'pr_tbr',5,60,1186,100,2,1,54.891,5,0 +'mbank_X30754',180,425,'pr_nni',7,60,1186,100,2,1,57.042,5,1 +'mbank_X30754',180,425,'pr_tbr',6,60,1180,100,2,1,55.273,5,0 +'mbank_X30754',180,425,'pr_nni',8,60,1189,100,2,1,55.129,5,1 +'mbank_X30754',180,425,'pr_tbr',7,60,1203,100,2,1,55.910,5,0 +'mbank_X30754',180,425,'pr_nni',9,60,1220,100,2,1,58.170,5,1 +'mbank_X30754',180,425,'pr_tbr',8,60,1195,100,2,1,55.233,5,0 +'mbank_X30754',180,425,'pr_nni',10,60,1202,100,2,1,56.174,5,1 +'mbank_X30754',180,425,'pr_tbr',9,60,1183,100,2,1,55.821,5,0 +'mbank_X30754',180,425,'pr_tbr',1,60,1185,100,2,1,56.862,5,0 +'mbank_X30754',180,425,'pr_tbr',10,60,1196,100,2,2,55.204,5,0 +'mbank_X30754',180,425,'pr_tbr',2,60,1192,100,2,1,54.971,5,0 +'mbank_X30754',180,425,'pr_tbr',3,60,1176,100,2,1,57.297,5,0 +'mbank_X30754',180,425,'baseline',1,120,1185,100,7,1,111.575,0,0 +'mbank_X30754',180,425,'pr_tbr',4,60,1184,100,2,1,54.973,5,0 +'mbank_X30754',180,425,'pr_tbr',5,60,1209,100,2,1,58.387,5,0 +'mbank_X30754',180,425,'baseline',2,120,1184,100,7,1,109.196,0,0 +'mbank_X30754',180,425,'pr_tbr',6,60,1185,100,2,1,55.455,5,0 +'mbank_X30754',180,425,'pr_tbr',7,60,1194,100,2,1,55.388,5,0 +'mbank_X30754',180,425,'baseline',3,120,1178,100,8,1,108.885,0,0 +'mbank_X30754',180,425,'pr_tbr',8,60,1179,100,2,1,55.061,5,0 +'mbank_X30754',180,425,'pr_tbr',9,60,1193,100,2,1,54.979,5,0 +'mbank_X30754',180,425,'baseline',4,120,1193,100,8,1,109.054,0,0 +'mbank_X30754',180,425,'pr_tbr',10,60,1192,100,2,1,57.003,5,0 +'mbank_X30754',180,425,'baseline',5,120,1190,100,8,1,108.889,0,0 +'mbank_X30754',180,425,'baseline',1,120,1178,100,7,1,109.109,0,0 +'mbank_X30754',180,425,'baseline',6,120,1182,100,8,1,109.471,0,0 +'mbank_X30754',180,425,'baseline',2,120,1184,100,6,1,109.635,0,0 +'mbank_X30754',180,425,'baseline',7,120,1183,100,8,1,118.883,0,0 +'mbank_X30754',180,425,'baseline',3,120,1167,100,6,1,108.920,0,0 +'mbank_X30754',180,425,'baseline',8,120,1172,100,8,1,110.093,0,0 +'mbank_X30754',180,425,'baseline',4,120,1179,100,7,1,110.132,0,0 +'mbank_X30754',180,425,'baseline',9,120,1180,100,7,1,111.729,0,0 +'mbank_X30754',180,425,'baseline',5,120,1186,100,7,1,109.305,0,0 +'mbank_X30754',180,425,'baseline',10,120,1187,100,8,1,108.743,0,0 +'mbank_X30754',180,425,'baseline',6,120,1183,100,6,1,108.823,0,0 +'mbank_X30754',180,425,'pr_nni',1,120,1179,100,6,1,109.762,5,1 +'mbank_X30754',180,425,'baseline',7,120,1165,100,6,1,109.404,0,0 +'mbank_X30754',180,425,'pr_nni',2,120,1186,100,6,1,108.679,5,1 +'mbank_X30754',180,425,'baseline',8,120,1183,100,7,1,112.141,0,0 +'mbank_X30754',180,425,'pr_nni',3,120,1171,100,6,1,111.128,5,1 +'mbank_X30754',180,425,'baseline',9,120,1182,100,6,1,110.592,0,0 +'mbank_X30754',180,425,'pr_nni',4,120,1171,100,6,1,109.038,5,1 +'mbank_X30754',180,425,'baseline',10,120,1198,100,6,1,110.612,0,0 +'mbank_X30754',180,425,'pr_nni',5,120,1168,100,6,1,109.992,5,1 +'mbank_X30754',180,425,'pr_nni',1,120,1191,100,6,1,109.929,5,1 +'mbank_X30754',180,425,'pr_nni',6,120,1172,100,6,1,109.218,5,1 +'mbank_X30754',180,425,'pr_nni',2,120,1166,100,6,1,109.651,5,1 +'mbank_X30754',180,425,'pr_nni',7,120,1176,100,6,1,109.912,5,1 +'mbank_X30754',180,425,'pr_nni',3,120,1170,100,6,1,109.166,5,1 +'mbank_X30754',180,425,'pr_nni',8,120,1177,100,6,1,109.341,5,1 +'mbank_X30754',180,425,'pr_nni',4,120,1199,100,6,1,108.951,5,1 +'mbank_X30754',180,425,'pr_nni',9,120,1179,100,6,1,109.004,5,1 +'mbank_X30754',180,425,'pr_nni',5,120,1184,100,6,1,108.633,5,1 +'mbank_X30754',180,425,'pr_nni',10,120,1165,100,6,1,109.002,5,1 +'mbank_X30754',180,425,'pr_nni',6,120,1172,100,6,1,108.740,5,1 +'mbank_X30754',180,425,'pr_tbr',1,120,1173,100,6,1,108.584,5,0 +'mbank_X30754',180,425,'pr_nni',7,120,1181,100,6,0,109.348,5,1 +'mbank_X30754',180,425,'pr_tbr',2,120,1184,100,6,1,109.429,5,0 +'mbank_X30754',180,425,'pr_nni',8,120,1169,100,6,1,109.950,5,1 +'mbank_X30754',180,425,'pr_tbr',3,120,1172,100,6,1,111.199,5,0 +'mbank_X30754',180,425,'pr_nni',9,120,1176,100,6,1,109.544,5,1 +'mbank_X30754',180,425,'pr_tbr',4,120,1187,100,6,1,109.747,5,0 +'mbank_X30754',180,425,'pr_nni',10,120,1184,100,6,1,110.134,5,1 +'mbank_X30754',180,425,'pr_tbr',5,120,1175,100,6,1,109.611,5,0 +'mbank_X30754',180,425,'pr_tbr',1,120,1182,100,5,1,109.597,5,0 +'mbank_X30754',180,425,'pr_tbr',6,120,1191,100,6,1,108.677,5,0 +'mbank_X30754',180,425,'pr_tbr',2,120,1183,100,6,1,109.602,5,0 +'mbank_X30754',180,425,'pr_tbr',7,120,1169,100,6,1,109.658,5,0 +'mbank_X30754',180,425,'pr_tbr',3,120,1179,100,6,1,110.182,5,0 +'mbank_X30754',180,425,'pr_tbr',8,120,1178,100,6,1,110.202,5,0 +'mbank_X30754',180,425,'pr_tbr',4,120,1180,100,6,1,109.706,5,0 +'mbank_X30754',180,425,'pr_tbr',9,120,1188,100,5,1,108.700,5,0 +'mbank_X30754',180,425,'pr_tbr',5,120,1182,100,5,1,109.927,5,0 +'mbank_X30754',180,425,'pr_tbr',10,120,1178,100,6,1,114.074,5,0 +'mbank_X30754',180,425,'pr_tbr',6,120,1173,100,5,1,108.743,5,0 +'project4133',131,349,'baseline',1,60,2380,100,19,1,56.519,0,0 +'project4133',131,349,'baseline',2,60,2382,100,20,1,56.966,0,0 +'mbank_X30754',180,425,'pr_tbr',7,120,1172,100,6,1,110.728,5,0 +'project4133',131,349,'baseline',3,60,2381,100,19,1,54.533,0,0 +'project4133',131,349,'baseline',4,60,2384,100,20,1,55.538,0,0 +'mbank_X30754',180,425,'pr_tbr',8,120,1178,100,6,1,109.630,5,0 +'project4133',131,349,'baseline',5,60,2382,100,20,1,55.160,0,0 +'project4133',131,349,'baseline',6,60,2387,100,20,1,54.761,0,0 +'mbank_X30754',180,425,'pr_tbr',9,120,1175,100,6,1,109.415,5,0 +'project4133',131,349,'baseline',7,60,2379,100,20,1,54.772,0,0 +'project4133',131,349,'baseline',8,60,2382,100,18,1,55.407,0,0 +'mbank_X30754',180,425,'pr_tbr',10,120,1186,100,6,1,108.966,5,0 +'project4133',131,349,'baseline',9,60,2382,100,20,1,54.695,0,0 +'project4133',131,349,'baseline',1,60,2376,100,19,1,55.222,0,0 +'project4133',131,349,'baseline',10,60,2375,100,19,1,55.297,0,0 +'project4133',131,349,'baseline',2,60,2376,100,18,0,55.204,0,0 +'project4133',131,349,'pr_nni',1,60,2380,100,16,0,54.902,5,1 +'project4133',131,349,'baseline',3,60,2377,100,20,1,55.130,0,0 +'project4133',131,349,'pr_nni',2,60,2377,100,16,1,54.472,5,1 +'project4133',131,349,'baseline',4,60,2383,100,20,1,55.378,0,0 +'project4133',131,349,'pr_nni',3,60,2379,100,17,1,55.110,5,1 +'project4133',131,349,'baseline',5,60,2381,100,19,0,54.706,0,0 +'project4133',131,349,'pr_nni',4,60,2371,100,16,1,54.680,5,1 +'project4133',131,349,'baseline',6,60,2373,100,19,1,55.544,0,0 +'project4133',131,349,'pr_nni',5,60,2381,100,16,1,55.020,5,1 +'project4133',131,349,'baseline',7,60,2379,100,18,1,55.174,0,0 +'project4133',131,349,'pr_nni',6,60,2378,100,16,1,55.187,5,1 +'project4133',131,349,'baseline',8,60,2386,100,19,1,55.515,0,0 +'project4133',131,349,'pr_nni',7,60,2381,100,16,1,54.865,5,1 +'project4133',131,349,'baseline',9,60,2386,100,19,2,54.507,0,0 +'project4133',131,349,'pr_nni',8,60,2376,100,16,1,54.993,5,1 +'project4133',131,349,'baseline',10,60,2379,100,19,1,55.416,0,0 +'project4133',131,349,'pr_nni',9,60,2383,100,16,1,56.042,5,1 +'project4133',131,349,'pr_nni',1,60,2377,100,16,1,54.876,5,1 +'project4133',131,349,'pr_nni',10,60,2386,100,16,2,55.174,5,1 +'project4133',131,349,'pr_nni',2,60,2380,100,16,1,54.711,5,1 +'project4133',131,349,'pr_tbr',1,60,2382,100,14,1,55.313,5,0 +'project4133',131,349,'pr_nni',3,60,2386,100,16,1,54.472,5,1 +'project4133',131,349,'pr_tbr',2,60,2380,100,14,1,55.553,5,0 +'project4133',131,349,'pr_nni',4,60,2381,100,16,1,55.697,5,1 +'project4133',131,349,'pr_tbr',3,60,2377,100,14,1,55.433,5,0 +'project4133',131,349,'pr_nni',5,60,2378,100,16,1,55.456,5,1 +'project4133',131,349,'pr_tbr',4,60,2382,100,14,0,55.032,5,0 +'project4133',131,349,'pr_nni',6,60,2382,100,16,1,55.048,5,1 +'project4133',131,349,'pr_tbr',5,60,2374,100,14,1,56.293,5,0 +'project4133',131,349,'pr_nni',7,60,2380,100,16,1,54.921,5,1 +'project4133',131,349,'pr_tbr',6,60,2372,100,14,1,54.974,5,0 +'project4133',131,349,'pr_nni',8,60,2379,100,16,1,56.451,5,1 +'project4133',131,349,'pr_tbr',7,60,2374,100,14,0,55.044,5,0 +'project4133',131,349,'pr_nni',9,60,2378,100,16,1,57.662,5,1 +'project4133',131,349,'pr_tbr',8,60,2376,100,14,1,55.855,5,0 +'project4133',131,349,'pr_nni',10,60,2381,100,16,1,55.670,5,1 +'project4133',131,349,'pr_tbr',9,60,2376,100,14,1,54.633,5,0 +'project4133',131,349,'pr_tbr',1,60,2377,100,15,1,55.159,5,0 +'project4133',131,349,'pr_tbr',10,60,2381,100,14,1,55.251,5,0 +'project4133',131,349,'pr_tbr',2,60,2370,100,15,0,56.899,5,0 +'project4133',131,349,'pr_tbr',3,60,2373,100,14,1,57.368,5,0 +'project4133',131,349,'baseline',1,120,2375,100,37,1,108.586,0,0 +'project4133',131,349,'pr_tbr',4,60,2378,100,14,1,55.260,5,0 +'project4133',131,349,'pr_tbr',5,60,2374,100,14,1,54.918,5,0 +'project4133',131,349,'baseline',2,120,2377,100,39,1,109.877,0,0 +'project4133',131,349,'pr_tbr',6,60,2370,100,14,0,55.524,5,0 +'project4133',131,349,'pr_tbr',7,60,2377,100,14,1,55.338,5,0 +'project4133',131,349,'baseline',3,120,2378,100,38,1,108.823,0,0 +'project4133',131,349,'pr_tbr',8,60,2385,100,14,1,54.774,5,0 +'project4133',131,349,'pr_tbr',9,60,2375,100,14,1,54.685,5,0 +'project4133',131,349,'baseline',4,120,2370,100,38,1,109.582,0,0 +'project4133',131,349,'pr_tbr',10,60,2379,100,14,1,55.873,5,0 +'project4133',131,349,'baseline',5,120,2377,100,39,1,108.986,0,0 +'project4133',131,349,'baseline',1,120,2382,100,40,0,108.747,0,0 +'project4133',131,349,'baseline',6,120,2377,100,38,1,109.615,0,0 +'project4133',131,349,'baseline',2,120,2378,100,40,1,109.510,0,0 +'project4133',131,349,'baseline',7,120,2379,100,39,1,109.977,0,0 +'project4133',131,349,'baseline',3,120,2378,100,39,1,108.720,0,0 +'project4133',131,349,'baseline',8,120,2373,100,38,1,110.431,0,0 +'project4133',131,349,'baseline',4,120,2372,100,40,0,108.750,0,0 +'project4133',131,349,'baseline',9,120,2380,100,40,1,109.730,0,0 +'project4133',131,349,'baseline',5,120,2381,100,39,0,108.775,0,0 +'project4133',131,349,'baseline',10,120,2374,100,40,1,109.588,0,0 +'project4133',131,349,'baseline',6,120,2378,100,40,1,109.225,0,0 +'project4133',131,349,'pr_nni',1,120,2373,100,34,0,111.930,5,1 +'project4133',131,349,'baseline',7,120,2382,100,39,1,108.839,0,0 +'project4133',131,349,'pr_nni',2,120,2376,100,34,1,108.882,5,1 +'project4133',131,349,'baseline',8,120,2366,100,40,1,109.575,0,0 +'project4133',131,349,'pr_nni',3,120,2378,100,34,1,109.824,5,1 +'project4133',131,349,'baseline',9,120,2376,100,40,0,108.894,0,0 +'project4133',131,349,'pr_nni',4,120,2377,100,34,1,109.120,5,1 +'project4133',131,349,'baseline',10,120,2370,100,40,1,109.005,0,0 +'project4133',131,349,'pr_nni',5,120,2378,100,33,1,109.037,5,1 +'project4133',131,349,'pr_nni',1,120,2372,100,34,1,108.830,5,1 +'project4133',131,349,'pr_nni',6,120,2379,100,32,1,108.544,5,1 +'project4133',131,349,'pr_nni',2,120,2375,100,34,1,109.696,5,1 +'project4133',131,349,'pr_nni',7,120,2375,100,33,1,110.982,5,1 +'project4133',131,349,'pr_nni',3,120,2373,100,33,1,111.071,5,1 +'project4133',131,349,'pr_nni',8,120,2375,100,34,1,109.485,5,1 +'project4133',131,349,'pr_nni',4,120,2371,100,33,1,108.501,5,1 +'project4133',131,349,'pr_nni',9,120,2379,100,34,1,108.758,5,1 +'project4133',131,349,'pr_nni',5,120,2380,100,34,0,108.912,5,1 +'project4133',131,349,'pr_nni',10,120,2377,100,33,1,108.391,5,1 +'project4133',131,349,'pr_nni',6,120,2380,100,34,1,109.191,5,1 +'project4133',131,349,'pr_tbr',1,120,2376,100,30,1,109.663,5,0 +'project4133',131,349,'pr_nni',7,120,2379,100,34,1,109.609,5,1 +'project4133',131,349,'pr_tbr',2,120,2371,100,30,1,108.952,5,0 +'project4133',131,349,'pr_nni',8,120,2378,100,34,1,111.150,5,1 +'project4133',131,349,'pr_tbr',3,120,2378,100,29,1,108.725,5,0 +'project4133',131,349,'pr_nni',9,120,2380,100,34,2,108.717,5,1 +'project4133',131,349,'pr_tbr',4,120,2381,100,30,1,109.789,5,0 +'project4133',131,349,'pr_nni',10,120,2379,100,34,1,109.038,5,1 +'project4133',131,349,'pr_tbr',5,120,2375,100,30,1,109.177,5,0 +'project4133',131,349,'pr_tbr',1,120,2373,100,29,1,109.232,5,0 +'project4133',131,349,'pr_tbr',6,120,2371,100,28,1,114.340,5,0 +'project4133',131,349,'pr_tbr',2,120,2375,100,29,1,109.025,5,0 +'project4133',131,349,'pr_tbr',7,120,2379,100,29,1,108.769,5,0 +'project4133',131,349,'pr_tbr',3,120,2376,100,30,1,108.938,5,0 +'project4133',131,349,'pr_tbr',8,120,2375,100,29,1,111.501,5,0 +'project4133',131,349,'pr_tbr',4,120,2377,100,30,1,109.013,5,0 +'project4133',131,349,'pr_tbr',9,120,2375,100,30,1,109.654,5,0 +'project4133',131,349,'pr_tbr',5,120,2376,100,30,1,110.094,5,0 +'project4133',131,349,'pr_tbr',10,120,2378,100,30,1,109.259,5,0 +'project4133',131,349,'pr_tbr',6,120,2372,100,31,1,109.013,5,0 +'project3701',146,324,'baseline',1,60,3936,1,7,1,59.506,0,0 +'project3701',146,324,'baseline',2,60,4236,2,8,1,58.009,0,0 +'project4133',131,349,'pr_tbr',7,120,2374,100,30,1,108.595,5,0 +'project3701',146,324,'baseline',3,60,4274,10,8,1,60.040,0,0 +'project4133',131,349,'pr_tbr',8,120,2378,100,30,1,108.926,5,0 +'project3701',146,324,'baseline',4,60,4023,75,8,1,60.018,0,0 +'project3701',146,324,'baseline',5,60,4185,4,7,1,57.522,0,0 +'project4133',131,349,'pr_tbr',9,120,2376,100,29,1,108.921,5,0 +'project3701',146,324,'baseline',6,60,4164,100,8,1,60.026,0,0 +'project3701',146,324,'baseline',7,60,4172,25,8,1,60.046,0,0 +'project4133',131,349,'pr_tbr',10,120,2376,100,30,1,109.691,5,0 +'project3701',146,324,'baseline',8,60,4028,100,8,1,58.648,0,0 +'project3701',146,324,'baseline',1,60,4183,33,8,1,60.043,0,0 +'project3701',146,324,'baseline',9,60,4135,6,7,1,57.190,0,0 +'project3701',146,324,'baseline',2,60,4111,38,8,1,60.025,0,0 +'project3701',146,324,'baseline',10,60,4291,1,8,1,54.254,0,0 +'project3701',146,324,'baseline',3,60,4102,15,8,1,60.010,0,0 +'project3701',146,324,'pr_nni',1,60,4043,2,6,1,57.124,5,1 +'project3701',146,324,'baseline',4,60,4148,4,8,1,57.610,0,0 +'project3701',146,324,'pr_nni',2,60,4005,2,6,1,56.982,5,1 +'project3701',146,324,'baseline',5,60,4105,1,8,1,54.562,0,0 +'project3701',146,324,'pr_nni',3,60,4044,1,6,1,54.141,5,1 +'project3701',146,324,'baseline',6,60,4224,4,8,1,58.400,0,0 +'project3701',146,324,'pr_nni',4,60,3987,2,6,1,59.605,5,1 +'project3701',146,324,'baseline',7,60,4169,2,8,1,54.240,0,0 +'project3701',146,324,'pr_nni',5,60,4067,2,5,1,55.037,5,1 +'project3701',146,324,'baseline',8,60,4086,1,8,1,54.655,0,0 +'project3701',146,324,'pr_nni',6,60,4034,100,6,1,56.858,5,1 +'project3701',146,324,'baseline',9,60,4255,2,8,1,56.195,0,0 +'project3701',146,324,'pr_nni',7,60,3933,1,6,1,54.738,5,1 +'project3701',146,324,'baseline',10,60,4170,2,7,1,54.824,0,0 +'project3701',146,324,'pr_nni',8,60,3953,2,6,1,56.303,5,1 +'project3701',146,324,'pr_nni',1,60,4021,100,6,1,59.274,5,1 +'project3701',146,324,'pr_nni',9,60,4034,100,6,1,55.486,5,1 +'project3701',146,324,'pr_nni',2,60,3991,1,6,1,58.778,5,1 +'project3701',146,324,'pr_nni',10,60,4000,47,6,1,60.034,5,1 +'project3701',146,324,'pr_nni',3,60,3957,49,6,1,60.017,5,1 +'project3701',146,324,'pr_tbr',1,60,4304,1,5,1,55.593,5,0 +'project3701',146,324,'pr_nni',4,60,3953,73,6,1,60.020,5,1 +'project3701',146,324,'pr_tbr',2,60,4079,2,6,1,58.223,5,0 +'project3701',146,324,'pr_tbr',3,60,4161,2,6,1,54.635,5,0 +'project3701',146,324,'pr_nni',5,60,3947,100,6,1,60.019,5,1 +'project3701',146,324,'pr_nni',6,60,3883,2,6,1,55.778,5,1 +'project3701',146,324,'pr_tbr',4,60,4182,2,6,1,60.040,5,0 +'project3701',146,324,'pr_nni',7,60,4039,2,6,1,57.403,5,1 +'project3701',146,324,'pr_tbr',5,60,4106,21,6,1,60.021,5,0 +'project3701',146,324,'pr_tbr',6,60,4198,4,6,1,54.833,5,0 +'project3701',146,324,'pr_nni',8,60,3985,42,6,1,60.050,5,1 +'project3701',146,324,'pr_tbr',7,60,4218,3,5,1,55.935,5,0 +'project3701',146,324,'pr_nni',9,60,3934,2,6,1,55.977,5,1 +'project3701',146,324,'pr_tbr',8,60,4159,1,6,1,55.356,5,0 +'project3701',146,324,'pr_nni',10,60,3932,83,6,1,60.020,5,1 +'project3701',146,324,'pr_tbr',9,60,4116,2,6,1,60.033,5,0 +'project3701',146,324,'pr_tbr',1,60,4152,49,6,1,60.035,5,0 +'project3701',146,324,'pr_tbr',10,60,4138,2,6,1,56.370,5,0 +'project3701',146,324,'pr_tbr',2,60,4085,1,6,1,59.708,5,0 +'project3701',146,324,'pr_tbr',3,60,4264,1,6,1,54.272,5,0 +'project3701',146,324,'baseline',1,120,4064,80,16,1,120.051,0,0 +'project3701',146,324,'pr_tbr',4,60,4123,98,6,1,60.032,5,0 +'project3701',146,324,'pr_tbr',5,60,4183,4,6,1,56.191,5,0 +'project3701',146,324,'pr_tbr',6,60,4176,1,6,1,54.199,5,0 +'project3701',146,324,'baseline',2,120,3998,48,16,1,120.048,0,0 +'project3701',146,324,'pr_tbr',7,60,4282,4,6,1,56.526,5,0 +'project3701',146,324,'pr_tbr',8,60,4199,100,6,1,57.092,5,0 +'project3701',146,324,'baseline',3,120,4121,100,16,1,115.286,0,0 +'project3701',146,324,'pr_tbr',9,60,4014,100,6,1,60.035,5,0 +'project3701',146,324,'baseline',4,120,4107,1,16,1,108.661,0,0 +'project3701',146,324,'pr_tbr',10,60,4116,52,6,1,60.045,5,0 +'project3701',146,324,'baseline',5,120,4024,5,16,1,117.648,0,0 +'project3701',146,324,'baseline',1,120,4056,100,15,1,115.870,0,0 +'project3701',146,324,'baseline',2,120,4054,1,17,1,109.320,0,0 +'project3701',146,324,'baseline',6,120,4133,100,16,1,114.469,0,0 +'project3701',146,324,'baseline',7,120,4119,1,17,1,108.415,0,0 +'project3701',146,324,'baseline',3,120,3997,1,16,1,114.274,0,0 +'project3701',146,324,'baseline',8,120,4080,1,17,1,110.831,0,0 +'project3701',146,324,'baseline',4,120,3976,100,17,1,111.987,0,0 +'project3701',146,324,'baseline',9,120,4158,2,17,1,113.085,0,0 +'project3701',146,324,'baseline',5,120,4137,1,16,1,112.144,0,0 +'project3701',146,324,'baseline',10,120,4080,2,15,1,110.105,0,0 +'project3701',146,324,'baseline',6,120,4137,100,16,1,110.400,0,0 +'project3701',146,324,'pr_nni',1,120,3939,1,12,1,109.166,5,1 +'project3701',146,324,'baseline',7,120,4132,13,17,1,120.013,0,0 +'project3701',146,324,'pr_nni',2,120,4014,100,12,1,113.200,5,1 +'project3701',146,324,'baseline',8,120,4053,100,16,1,116.639,0,0 +'project3701',146,324,'pr_nni',3,120,4074,1,12,1,108.249,5,1 +'project3701',146,324,'baseline',9,120,4062,100,14,1,114.849,0,0 +'project3701',146,324,'pr_nni',4,120,4000,88,12,1,120.035,5,1 +'project3701',146,324,'baseline',10,120,4123,100,15,1,114.626,0,0 +'project3701',146,324,'pr_nni',5,120,3905,100,12,1,114.257,5,1 +'project3701',146,324,'pr_nni',1,120,3955,1,11,1,108.643,5,1 +'project3701',146,324,'pr_nni',6,120,3981,6,12,1,118.738,5,1 +'project3701',146,324,'pr_nni',2,120,3903,2,11,1,110.838,5,1 +'project3701',146,324,'pr_nni',3,120,3884,4,11,1,112.425,5,1 +'project3701',146,324,'pr_nni',7,120,4019,100,12,1,119.768,5,1 +'project3701',146,324,'pr_nni',8,120,3965,100,12,1,112.421,5,1 +'project3701',146,324,'pr_nni',4,120,3987,2,12,1,115.265,5,1 +'project3701',146,324,'pr_nni',5,120,3997,6,12,2,117.877,5,1 +'project3701',146,324,'pr_nni',9,120,3888,94,12,1,120.075,5,1 +'project3701',146,324,'pr_nni',10,120,4050,1,12,2,110.971,5,1 +'project3701',146,324,'pr_nni',6,120,3862,2,11,1,114.865,5,1 +'project3701',146,324,'pr_nni',7,120,3924,100,12,1,112.974,5,1 +'project3701',146,324,'pr_tbr',1,120,4066,100,12,1,116.072,5,0 +'project3701',146,324,'pr_tbr',2,120,4270,1,12,1,108.180,5,0 +'project3701',146,324,'pr_nni',8,120,3949,100,11,1,119.000,5,1 +'project3701',146,324,'pr_tbr',3,120,4142,6,12,1,113.382,5,0 +'project3701',146,324,'pr_nni',9,120,3948,8,12,1,115.132,5,1 +'project3701',146,324,'pr_tbr',4,120,4204,1,11,1,110.056,5,0 +'project3701',146,324,'pr_nni',10,120,3906,100,12,1,113.346,5,1 +'project3701',146,324,'pr_tbr',5,120,4123,1,12,1,116.092,5,0 +'project3701',146,324,'pr_tbr',1,120,4149,2,13,1,108.639,5,0 +'project3701',146,324,'pr_tbr',6,120,4037,100,12,1,114.350,5,0 +'project3701',146,324,'pr_tbr',2,120,4124,1,12,1,108.376,5,0 +'project3701',146,324,'pr_tbr',7,120,4155,86,13,1,120.051,5,0 +'project3701',146,324,'pr_tbr',3,120,4048,42,12,1,120.045,5,0 +'project3701',146,324,'pr_tbr',4,120,4187,1,13,1,108.138,5,0 +'project3701',146,324,'pr_tbr',8,120,4019,6,10,1,119.104,5,0 +'project3701',146,324,'pr_tbr',5,120,4129,4,13,1,111.829,5,0 +'project3701',146,324,'pr_tbr',9,120,4008,100,11,1,118.236,5,0 +'project3701',146,324,'pr_tbr',6,120,4141,100,12,1,115.253,5,0 +'project3701',146,324,'pr_tbr',10,120,4082,3,12,1,113.797,5,0 +'project804',173,589,'baseline',1,60,1379,89,4,1,60.144,0,0 +'project3701',146,324,'pr_tbr',7,120,4222,1,12,1,108.299,5,0 +'project804',173,589,'baseline',2,60,1370,100,3,1,56.737,0,0 +'project804',173,589,'baseline',3,60,1365,100,2,1,60.138,0,0 +'project3701',146,324,'pr_tbr',8,120,4059,100,11,1,119.711,5,0 +'project804',173,589,'baseline',4,60,1380,100,3,1,60.042,0,0 +'project804',173,589,'baseline',5,60,1370,100,3,1,60.130,0,0 +'project3701',146,324,'pr_tbr',9,120,4146,1,10,1,108.835,5,0 +'project804',173,589,'baseline',6,60,1380,9,4,1,60.036,0,0 +'project804',173,589,'baseline',7,60,1390,100,3,1,60.055,0,0 +'project3701',146,324,'pr_tbr',10,120,4069,2,11,1,114.383,5,0 +'project804',173,589,'baseline',8,60,1385,100,3,1,60.104,0,0 +'project804',173,589,'baseline',1,60,1375,100,2,1,59.142,0,0 +'project804',173,589,'baseline',9,60,1372,100,2,1,57.940,0,0 +'project804',173,589,'baseline',2,60,1381,100,2,1,60.131,0,0 +'project804',173,589,'baseline',10,60,1360,64,4,1,60.045,0,0 +'project804',173,589,'baseline',3,60,1378,3,2,1,60.073,0,0 +'project804',173,589,'pr_nni',1,60,1360,100,2,1,59.051,5,1 +'project804',173,589,'baseline',4,60,1377,75,2,1,60.057,0,0 +'project804',173,589,'pr_nni',2,60,1371,100,2,1,60.111,5,1 +'project804',173,589,'baseline',5,60,1381,100,2,1,60.086,0,0 +'project804',173,589,'pr_nni',3,60,1366,19,2,1,60.114,5,1 +'project804',173,589,'baseline',6,60,1389,39,2,1,60.017,0,0 +'project804',173,589,'pr_nni',4,60,1373,100,2,1,58.283,5,1 +'project804',173,589,'baseline',7,60,1373,7,2,1,60.087,0,0 +'project804',173,589,'pr_nni',5,60,1363,96,2,1,60.112,5,1 +'project804',173,589,'baseline',8,60,1377,100,2,1,59.280,0,0 +'project804',173,589,'pr_nni',6,60,1362,100,2,1,57.957,5,1 +'project804',173,589,'baseline',9,60,1366,40,2,1,60.101,0,0 +'project804',173,589,'pr_nni',7,60,1376,42,2,1,60.055,5,1 +'project804',173,589,'baseline',10,60,1378,14,2,1,60.055,0,0 +'project804',173,589,'pr_nni',8,60,1365,61,2,1,60.109,5,1 +'project804',173,589,'pr_nni',1,60,1385,100,2,1,60.090,5,1 +'project804',173,589,'pr_nni',9,60,1367,15,2,1,60.069,5,1 +'project804',173,589,'pr_nni',2,60,1365,100,2,2,60.066,5,1 +'project804',173,589,'pr_nni',10,60,1382,100,2,1,59.691,5,1 +'project804',173,589,'pr_nni',3,60,1376,82,2,1,60.047,5,1 +'project804',173,589,'pr_tbr',1,60,1381,29,2,1,60.055,5,0 +'project804',173,589,'pr_nni',4,60,1369,100,2,1,60.091,5,1 +'project804',173,589,'pr_tbr',2,60,1385,100,2,1,58.501,5,0 +'project804',173,589,'pr_nni',5,60,1379,100,2,1,60.057,5,1 +'project804',173,589,'pr_tbr',3,60,1372,100,2,1,59.974,5,0 +'project804',173,589,'pr_nni',6,60,1361,5,2,1,56.079,5,1 +'project804',173,589,'pr_tbr',4,60,1369,97,2,1,60.136,5,0 +'project804',173,589,'pr_nni',7,60,1360,100,2,1,59.682,5,1 +'project804',173,589,'pr_tbr',5,60,1367,98,2,1,60.028,5,0 +'project804',173,589,'pr_nni',8,60,1374,100,2,1,60.045,5,1 +'project804',173,589,'pr_tbr',6,60,1377,59,2,1,60.033,5,0 +'project804',173,589,'pr_nni',9,60,1368,32,2,2,60.047,5,1 +'project804',173,589,'pr_tbr',7,60,1372,77,2,1,60.131,5,0 +'project804',173,589,'pr_nni',10,60,1373,13,2,1,60.034,5,1 +'project804',173,589,'pr_tbr',8,60,1375,100,2,1,60.103,5,0 +'project804',173,589,'pr_tbr',1,60,1382,100,2,1,58.947,5,0 +'project804',173,589,'pr_tbr',9,60,1387,35,2,1,60.074,5,0 +'project804',173,589,'pr_tbr',2,60,1366,25,2,1,60.061,5,0 +'project804',173,589,'pr_tbr',10,60,1393,100,2,1,57.554,5,0 +'project804',173,589,'pr_tbr',3,60,1390,91,2,1,60.112,5,0 +'project804',173,589,'pr_tbr',4,60,1372,100,2,1,60.169,5,0 +'project804',173,589,'baseline',1,120,1367,100,7,1,120.095,0,0 +'project804',173,589,'pr_tbr',5,60,1379,16,2,1,60.096,5,0 +'project804',173,589,'pr_tbr',6,60,1374,11,2,1,60.024,5,0 +'project804',173,589,'baseline',2,120,1369,100,7,2,118.125,0,0 +'project804',173,589,'pr_tbr',7,60,1379,100,2,1,59.602,5,0 +'project804',173,589,'pr_tbr',8,60,1369,88,2,1,60.085,5,0 +'project804',173,589,'baseline',3,120,1367,100,6,1,109.514,0,0 +'project804',173,589,'pr_tbr',9,60,1375,59,2,1,60.067,5,0 +'project804',173,589,'pr_tbr',10,60,1377,100,2,1,60.054,5,0 +'project804',173,589,'baseline',4,120,1367,100,6,1,120.167,0,0 +'project804',173,589,'baseline',1,120,1361,58,6,1,120.099,0,0 +'project804',173,589,'baseline',5,120,1378,100,8,1,120.193,0,0 +'project804',173,589,'baseline',2,120,1361,20,6,1,120.092,0,0 +'project804',173,589,'baseline',6,120,1371,100,6,1,117.192,0,0 +'project804',173,589,'baseline',3,120,1377,100,6,1,120.043,0,0 +'project804',173,589,'baseline',7,120,1369,100,7,1,114.480,0,0 +'project804',173,589,'baseline',4,120,1361,100,6,1,117.798,0,0 +'project804',173,589,'baseline',8,120,1370,100,6,0,111.198,0,0 +'project804',173,589,'baseline',5,120,1365,100,6,1,117.842,0,0 +'project804',173,589,'baseline',9,120,1370,45,7,1,120.029,0,0 +'project804',173,589,'baseline',6,120,1373,100,6,1,110.251,0,0 +'project804',173,589,'baseline',10,120,1365,100,8,1,118.772,0,0 +'project804',173,589,'baseline',7,120,1366,66,6,1,120.061,0,0 +'project804',173,589,'pr_nni',1,120,1364,100,6,1,115.659,5,1 +'project804',173,589,'baseline',8,120,1366,100,6,1,117.724,0,0 +'project804',173,589,'pr_nni',2,120,1367,100,6,1,116.814,5,1 +'project804',173,589,'baseline',9,120,1366,100,6,1,120.078,0,0 +'project804',173,589,'pr_nni',3,120,1367,100,6,1,118.761,5,1 +'project804',173,589,'baseline',10,120,1371,100,6,1,115.588,0,0 +'project804',173,589,'pr_nni',4,120,1355,100,6,1,114.508,5,1 +'project804',173,589,'pr_nni',1,120,1367,100,4,1,120.148,5,1 +'project804',173,589,'pr_nni',5,120,1361,100,6,1,114.829,5,1 +'project804',173,589,'pr_nni',2,120,1376,100,5,1,120.122,5,1 +'project804',173,589,'pr_nni',6,120,1358,100,6,1,114.471,5,1 +'project804',173,589,'pr_nni',7,120,1361,100,6,1,114.447,5,1 +'project804',173,589,'pr_nni',3,120,1373,100,5,1,118.995,5,1 +'project804',173,589,'pr_nni',4,120,1368,100,6,1,111.709,5,1 +'project804',173,589,'pr_nni',8,120,1362,94,6,1,120.092,5,1 +'project804',173,589,'pr_nni',5,120,1362,35,4,1,120.094,5,1 +'project804',173,589,'pr_nni',9,120,1366,82,6,1,120.106,5,1 +'project804',173,589,'pr_nni',6,120,1366,96,5,1,120.133,5,1 +'project804',173,589,'pr_nni',10,120,1365,68,6,1,120.078,5,1 +'project804',173,589,'pr_nni',7,120,1365,100,5,1,117.875,5,1 +'project804',173,589,'pr_tbr',1,120,1374,100,5,1,117.393,5,0 +'project804',173,589,'pr_nni',8,120,1365,100,6,1,120.133,5,1 +'project804',173,589,'pr_tbr',2,120,1364,68,5,1,120.095,5,0 +'project804',173,589,'pr_nni',9,120,1357,100,6,1,114.293,5,1 +'project804',173,589,'pr_tbr',3,120,1368,100,6,1,118.277,5,0 +'project804',173,589,'pr_nni',10,120,1375,100,5,1,120.075,5,1 +'project804',173,589,'pr_tbr',4,120,1374,100,6,0,116.211,5,0 +'project804',173,589,'pr_tbr',1,120,1367,100,4,1,114.706,5,0 +'project804',173,589,'pr_tbr',5,120,1376,4,5,1,120.015,5,0 +'project804',173,589,'pr_tbr',2,120,1366,100,4,2,120.081,5,0 +'project804',173,589,'pr_tbr',6,120,1379,100,6,1,117.838,5,0 +'project804',173,589,'pr_tbr',3,120,1371,100,4,1,112.028,5,0 +'project804',173,589,'pr_tbr',7,120,1373,100,6,1,116.842,5,0 +'project804',173,589,'pr_tbr',4,120,1379,100,4,1,115.752,5,0 +'project804',173,589,'pr_tbr',8,120,1373,100,5,1,116.783,5,0 +'project804',173,589,'pr_tbr',5,120,1367,100,4,1,115.504,5,0 +'project804',173,589,'pr_tbr',9,120,1366,100,6,1,117.697,5,0 +'project804',173,589,'pr_tbr',6,120,1374,56,4,1,120.104,5,0 +'project804',173,589,'pr_tbr',10,120,1374,89,5,1,120.036,5,0 +'syab07205',206,748,'baseline',1,60,10399,29,2,1,60.057,0,0 +'project804',173,589,'pr_tbr',7,120,1363,32,4,1,120.022,5,0 +'syab07205',206,748,'baseline',2,60,10407,60,2,1,60.066,0,0 +'syab07205',206,748,'baseline',3,60,10413,23,2,1,60.070,0,0 +'project804',173,589,'pr_tbr',8,120,1366,100,4,1,119.935,5,0 +'syab07205',206,748,'baseline',4,60,10413,25,2,1,60.083,0,0 +'syab07205',206,748,'baseline',5,60,10475,76,2,1,60.054,0,0 +'project804',173,589,'pr_tbr',9,120,1362,100,4,1,118.924,5,0 +'syab07205',206,748,'baseline',6,60,10414,32,2,1,60.042,0,0 +'syab07205',206,748,'baseline',7,60,10456,6,2,1,58.700,0,0 +'project804',173,589,'pr_tbr',10,120,1376,100,4,1,114.016,5,0 +'syab07205',206,748,'baseline',8,60,10539,10,2,1,60.046,0,0 +'syab07205',206,748,'baseline',1,60,10382,9,2,1,57.329,0,0 +'syab07205',206,748,'baseline',9,60,10596,63,2,1,60.030,0,0 +'syab07205',206,748,'baseline',2,60,10516,100,2,1,60.053,0,0 +'syab07205',206,748,'baseline',10,60,10510,64,2,1,60.075,0,0 +'syab07205',206,748,'baseline',3,60,10428,71,2,1,60.024,0,0 +'syab07205',206,748,'pr_nni',1,60,10445,1,2,1,60.052,5,1 +'syab07205',206,748,'baseline',4,60,10464,57,2,1,60.020,0,0 +'syab07205',206,748,'pr_nni',2,60,10461,16,2,1,60.039,5,1 +'syab07205',206,748,'baseline',5,60,10422,34,2,1,60.018,0,0 +'syab07205',206,748,'pr_nni',3,60,10505,3,1,1,58.640,5,1 +'syab07205',206,748,'baseline',6,60,10433,64,2,1,60.022,0,0 +'syab07205',206,748,'pr_nni',4,60,10440,27,2,1,60.029,5,1 +'syab07205',206,748,'baseline',7,60,10389,8,2,1,59.710,0,0 +'syab07205',206,748,'pr_nni',5,60,10430,7,2,1,60.020,5,1 +'syab07205',206,748,'baseline',8,60,10572,100,2,1,60.073,0,0 +'syab07205',206,748,'pr_nni',6,60,10465,65,2,1,60.030,5,1 +'syab07205',206,748,'baseline',9,60,10480,4,2,1,56.434,0,0 +'syab07205',206,748,'pr_nni',7,60,10435,100,2,1,56.943,5,1 +'syab07205',206,748,'baseline',10,60,10496,86,2,1,60.100,0,0 +'syab07205',206,748,'pr_nni',8,60,10451,56,2,1,60.026,5,1 +'syab07205',206,748,'pr_nni',1,60,10463,100,2,1,60.042,5,1 +'syab07205',206,748,'pr_nni',9,60,10529,99,2,1,60.085,5,1 +'syab07205',206,748,'pr_nni',2,60,10465,18,2,1,60.018,5,1 +'syab07205',206,748,'pr_nni',10,60,10562,16,2,1,60.029,5,1 +'syab07205',206,748,'pr_nni',3,60,10444,100,1,1,60.116,5,1 +'syab07205',206,748,'pr_tbr',1,60,-1,1,0,0,54.057,5,0 +'syab07205',206,748,'pr_nni',4,60,10424,63,1,1,60.083,5,1 +'syab07205',206,748,'pr_tbr',2,60,-1,1,0,0,54.123,5,0 +'syab07205',206,748,'pr_nni',5,60,10384,42,2,1,60.083,5,1 +'syab07205',206,748,'pr_tbr',3,60,-1,1,0,0,54.103,5,0 +'syab07205',206,748,'pr_nni',6,60,10482,7,1,1,60.014,5,1 +'syab07205',206,748,'pr_tbr',4,60,-1,1,0,0,54.024,5,0 +'syab07205',206,748,'pr_tbr',5,60,-1,1,0,0,54.025,5,0 +'syab07205',206,748,'pr_nni',7,60,10421,31,2,1,60.036,5,1 +'syab07205',206,748,'pr_tbr',6,60,-1,1,0,0,54.031,5,0 +'syab07205',206,748,'pr_nni',8,60,10522,31,2,1,60.087,5,1 +'syab07205',206,748,'pr_tbr',7,60,-1,1,0,0,54.027,5,0 +'syab07205',206,748,'pr_nni',9,60,10473,36,1,1,60.022,5,1 +'syab07205',206,748,'pr_tbr',8,60,-1,1,0,0,54.026,5,0 +'syab07205',206,748,'pr_nni',10,60,10527,41,2,1,60.071,5,1 +'syab07205',206,748,'pr_tbr',9,60,-1,1,0,0,54.074,5,0 +'syab07205',206,748,'pr_tbr',1,60,-1,1,0,0,54.195,5,0 +'syab07205',206,748,'pr_tbr',10,60,-1,1,0,0,54.050,5,0 +'syab07205',206,748,'pr_tbr',2,60,-1,1,0,0,54.020,5,0 +'syab07205',206,748,'pr_tbr',3,60,-1,1,0,0,54.167,5,0 +'syab07205',206,748,'baseline',1,120,10442,100,4,1,112.614,0,0 +'syab07205',206,748,'pr_tbr',4,60,-1,1,0,0,54.049,5,0 +'syab07205',206,748,'pr_tbr',5,60,-1,1,0,0,54.209,5,0 +'syab07205',206,748,'baseline',2,120,10373,87,4,1,120.081,0,0 +'syab07205',206,748,'pr_tbr',6,60,-1,1,0,0,54.166,5,0 +'syab07205',206,748,'pr_tbr',7,60,-1,1,0,0,54.200,5,0 +'syab07205',206,748,'pr_tbr',8,60,-1,1,0,0,54.200,5,0 +'syab07205',206,748,'baseline',3,120,10490,100,4,1,119.513,0,0 +'syab07205',206,748,'pr_tbr',9,60,-1,1,0,0,54.161,5,0 +'syab07205',206,748,'pr_tbr',10,60,-1,1,0,0,54.161,5,0 +'syab07205',206,748,'baseline',4,120,10371,34,4,1,120.069,0,0 +'syab07205',206,748,'baseline',1,120,10425,100,4,1,112.412,0,0 +'syab07205',206,748,'baseline',5,120,10390,20,4,1,120.031,0,0 +'syab07205',206,748,'baseline',2,120,10432,100,4,1,111.530,0,0 +'syab07205',206,748,'baseline',6,120,10313,12,4,1,120.059,0,0 +'syab07205',206,748,'baseline',3,120,10363,8,4,1,119.719,0,0 +'syab07205',206,748,'baseline',7,120,10425,100,4,1,115.191,0,0 +'syab07205',206,748,'baseline',4,120,10499,16,4,1,116.035,0,0 +'syab07205',206,748,'baseline',8,120,10484,100,4,1,120.082,0,0 +'syab07205',206,748,'baseline',5,120,10389,54,4,1,120.042,0,0 +'syab07205',206,748,'baseline',9,120,10427,5,4,1,117.743,0,0 +'syab07205',206,748,'baseline',6,120,10436,56,4,1,120.074,0,0 +'syab07205',206,748,'baseline',10,120,10453,100,4,1,120.102,0,0 +'syab07205',206,748,'baseline',7,120,10435,100,4,1,115.716,0,0 +'syab07205',206,748,'pr_nni',1,120,10467,7,4,1,119.064,5,1 +'syab07205',206,748,'baseline',8,120,10373,14,4,1,115.910,0,0 +'syab07205',206,748,'pr_nni',2,120,10451,55,4,1,120.024,5,1 +'syab07205',206,748,'baseline',9,120,10426,100,4,1,120.017,0,0 +'syab07205',206,748,'pr_nni',3,120,10493,100,4,1,119.570,5,1 +'syab07205',206,748,'baseline',10,120,10403,82,4,1,120.059,0,0 +'syab07205',206,748,'pr_nni',4,120,10429,100,4,1,116.792,5,1 +'syab07205',206,748,'pr_nni',1,120,10407,100,4,1,115.317,5,1 +'syab07205',206,748,'pr_nni',5,120,10429,3,4,1,120.014,5,1 +'syab07205',206,748,'pr_nni',2,120,10374,100,4,1,117.950,5,1 +'syab07205',206,748,'pr_nni',6,120,10457,100,4,1,116.620,5,1 +'syab07205',206,748,'pr_nni',3,120,10477,52,3,1,120.035,5,1 +'syab07205',206,748,'pr_nni',7,120,10397,100,4,1,113.963,5,1 +'syab07205',206,748,'pr_nni',4,120,10448,8,3,1,120.060,5,1 +'syab07205',206,748,'pr_nni',8,120,10359,3,4,1,116.396,5,1 +'syab07205',206,748,'pr_nni',5,120,10459,8,3,1,116.031,5,1 +'syab07205',206,748,'pr_nni',9,120,10475,37,4,1,120.054,5,1 +'syab07205',206,748,'pr_nni',6,120,10421,100,4,1,120.119,5,1 +'syab07205',206,748,'pr_nni',10,120,10382,2,4,1,120.080,5,1 +'syab07205',206,748,'pr_nni',7,120,10435,25,4,1,120.061,5,1 +'syab07205',206,748,'pr_tbr',1,120,10452,4,2,1,115.760,5,0 +'syab07205',206,748,'pr_nni',8,120,10404,100,4,1,116.387,5,1 +'syab07205',206,748,'pr_tbr',2,120,10361,24,2,1,120.019,5,0 +'syab07205',206,748,'pr_nni',9,120,10382,30,4,1,120.083,5,1 +'syab07205',206,748,'pr_tbr',3,120,10431,19,2,1,120.062,5,0 +'syab07205',206,748,'pr_nni',10,120,10401,48,4,1,120.018,5,1 +'syab07205',206,748,'pr_tbr',4,120,10480,100,2,1,113.969,5,0 +'syab07205',206,748,'pr_tbr',1,120,10316,12,2,1,120.013,5,0 +'syab07205',206,748,'pr_tbr',5,120,10434,100,2,1,113.233,5,0 +'syab07205',206,748,'pr_tbr',2,120,10431,74,2,1,120.095,5,0 +'syab07205',206,748,'pr_tbr',6,120,10372,13,2,1,120.058,5,0 +'syab07205',206,748,'pr_tbr',3,120,10402,100,2,1,119.026,5,0 +'syab07205',206,748,'pr_tbr',7,120,10440,4,2,1,113.381,5,0 +'syab07205',206,748,'pr_tbr',4,120,10359,4,2,1,112.226,5,0 +'syab07205',206,748,'pr_tbr',8,120,10398,7,2,1,115.180,5,0 +'syab07205',206,748,'pr_tbr',5,120,10373,12,2,1,120.021,5,0 +'syab07205',206,748,'pr_tbr',9,120,10443,56,2,1,120.092,5,0 +'syab07205',206,748,'pr_tbr',6,120,10484,8,2,1,118.688,5,0 +'syab07205',206,748,'pr_tbr',10,120,10438,9,2,1,112.202,5,0 +'syab07205',206,748,'pr_tbr',7,120,10420,100,2,1,115.420,5,0 +'syab07205',206,748,'pr_tbr',8,120,10386,16,2,1,120.074,5,0 +'syab07205',206,748,'pr_tbr',9,120,10440,16,2,1,120.073,5,0 +'syab07205',206,748,'pr_tbr',10,120,10440,15,2,1,120.013,5,0 diff --git a/dev/benchmarks/t289f_stage5_hamilton.sh b/dev/benchmarks/t289f_stage5_hamilton.sh new file mode 100644 index 000000000..548cc808d --- /dev/null +++ b/dev/benchmarks/t289f_stage5_hamilton.sh @@ -0,0 +1,76 @@ +#!/bin/bash +#SBATCH --job-name=t289f-pr-nni +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=8G +#SBATCH --time=8:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t289f_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t289f_%j.err + +# T-289f: Prune-reinsert Stage 5 — NNI full-tree polish cost reduction +# +# Compares: baseline (no PR) vs pr_nni (NNI polish) vs pr_tbr (TBR polish) +# on the same 5 large-tree datasets as Stage 4 (131-206 tips). +# +# Builds from feature/tbr-batch (contains pruneReinsertNni parameter). +# +# Grid: 5 datasets x 3 configs x 2 budgets x 10 seeds = 300 runs +# Expected wall time: ~4-6h; 8h limit provides comfortable margin. + +module load r/4.5.1 +module load gcc/14.2 + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-a +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t289f_results +export R_LIBS="$LIB:${R_LIBS}" + +mkdir -p "$LIB" "$OUTDIR" /nobackup/$USER/TreeSearch/logs + +echo "=== T-289f Hamilton job (PR Stage 5 — NNI Polish) ===" +echo "Job ID: $SLURM_JOB_ID" +echo "Node: $(hostname)" +echo "Started: $(date)" +echo "" + +# Install CRAN dependencies if missing +Rscript --no-save -e " + lib <- '$LIB' + .libPaths(c(lib, .libPaths())) + pkgs <- c('abind', 'ape', 'cli', 'colorspace', 'fastmatch', 'Rdpack', 'TreeDist', 'TreeTools') + need <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)] + if (length(need) > 0) { + message('Installing: ', paste(need, collapse = ', ')) + install.packages(need, lib = lib, repos = 'https://cloud.r-project.org', + dependencies = NA, quiet = TRUE) + } else { message('All dependencies present.') } +" + +# Rebuild from cpp-search (pruneReinsertNni merged via PR #238) +cd "$REPO" || exit 1 +git fetch origin cpp-search +git reset --hard origin/cpp-search +echo "Git HEAD: $(git log --oneline -1)" + +echo "Rebuilding TreeSearch..." +rm -f src/*.o src/*.so src/*.dll +R CMD build --no-build-vignettes --no-manual --no-resave-data . +R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz +rc=$? +rm -f TreeSearch_*.tar.gz +echo "Install exit code: $rc" +if [ $rc -ne 0 ]; then + echo "FATAL: install failed" + exit 1 +fi + +# Run benchmark +cd "$OUTDIR" +Rscript "$REPO/dev/benchmarks/bench_pr_stage5_nni.R" "$OUTDIR" + +echo "" +echo "Completed: $(date)" +ls -lh "$OUTDIR"/t289f_*.csv 2>/dev/null diff --git a/dev/benchmarks/t29_thorough_rasstarts_hamilton.sh b/dev/benchmarks/t29_thorough_rasstarts_hamilton.sh new file mode 100644 index 000000000..4a5bab00d --- /dev/null +++ b/dev/benchmarks/t29_thorough_rasstarts_hamilton.sh @@ -0,0 +1,62 @@ +#!/bin/bash +#SBATCH --job-name=t29-thorough-ras +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=8G +#SBATCH --time=8:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/t29_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/t29_%j.err + +# Task #29: full-search time-matched gate for rasStarts=3 in the AUTO-SELECTED +# `thorough` preset. Local indicative + rss-only time-matched both favour +# rasStarts=3 (Zanol/Zhu, 5-8 steps); this confirms on authoritative wall-clock +# over the full thorough pipeline before flipping thorough's default (intensive +# already adopted it, commit e69765f3). +# +# Grid: 4 datasets x rasStarts{1,3} x budgets{60,120}s x 10 seeds = 160 runs. +# Expected ~4h; 8h limit gives margin. + +module load r/4.5.1 +module load gcc/14.2 +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 + +REPO=/nobackup/$USER/TreeSearch-t29 +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/t29_results +export R_LIBS="$LIB:${R_LIBS}" +mkdir -p "$LIB" "$OUTDIR" /nobackup/$USER/TreeSearch/logs + +echo "=== Task #29: thorough rasStarts=1 vs 3 (full-search, time-matched) ===" +echo "Job ID: $SLURM_JOB_ID | Node: $(hostname) | Started: $(date)" + +Rscript --no-save -e " + lib <- '$LIB'; .libPaths(c(lib, .libPaths())) + pkgs <- c('abind','ape','cli','colorspace','fastmatch','Rdpack','TreeDist','TreeTools') + need <- pkgs[!vapply(pkgs, requireNamespace, logical(1), quietly = TRUE)] + if (length(need)) install.packages(need, lib = lib, + repos = 'https://cloud.r-project.org', dependencies = NA, quiet = TRUE) +" + +# Build from origin/cpp-search (carries the rasStarts lever + build_ras_sector fix +# + this driver). Clone if absent. +if [ ! -d "$REPO/.git" ]; then + git clone https://github.com/ms609/TreeSearch.git "$REPO" +fi +cd "$REPO" || exit 1 +git fetch origin cpp-search && git reset --hard origin/cpp-search +echo "Git HEAD: $(git log --oneline -1)" + +echo "Rebuilding TreeSearch..." +rm -f src/*.o src/*.so src/*.dll +R CMD build --no-build-vignettes --no-manual --no-resave-data . +R CMD INSTALL --library="$LIB" TreeSearch_*.tar.gz; rc=$? +rm -f TreeSearch_*.tar.gz +if [ $rc -ne 0 ]; then echo "FATAL: install failed"; exit 1; fi + +cd "$OUTDIR" +TS_LIB="$LIB" OUTDIR="$OUTDIR" NSEED=10 BUDGETS="60 120" \ + Rscript "$REPO/dev/benchmarks/hamilton_thorough_rasstarts.R" + +echo "Completed: $(date)" +ls -lh "$OUTDIR"/thorough_rasstarts.csv 2>/dev/null diff --git a/dev/benchmarks/tbr_collapsed_test.R b/dev/benchmarks/tbr_collapsed_test.R new file mode 100644 index 000000000..1200f19ad --- /dev/null +++ b/dev/benchmarks/tbr_collapsed_test.R @@ -0,0 +1,74 @@ +# tbr_collapsed_test.R -- single-variable test (advisor, 2026-06-18). +# +# Confirms collapsed-edge pruning is the residual NEIGHBOURHOOD cause: it drops +# strict-improving TBR moves because its "provably cannot improve" proof is the +# SPR one and is unsound for TBR fragment-rerooting. +# +# TBRParams::unrooted=TRUE currently DISABLES collapsed pruning (only). The +# all-tips reroot emulation supplies the rooting coverage, so this isolates +# collapsed as the one variable. SUCCESS CRITERION: the resulting optimum's +# canonical-TBR neighbourhood has 0 improving neighbours (enumerator-clean). +source("dev/benchmarks/tbr_shared_start_lib.R") +d <- prepareDataset("Zanol2014") +norm <- function(tr) Preorder(RenumberTips(tr, names(d$phy))) + +# TsTbr with collapsed pruning OFF (unrooted=TRUE). +TsTbrU <- function(startTree, seed, acceptEqual = FALSE, maxHits = 1L) { + edge <- PhyloToKernelEdge(startTree, d); set.seed(seed) + res <- TreeSearch:::ts_tbr_diagnostics( + edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = maxHits, acceptEqual = acceptEqual, maxChanges = 0L, + unrooted = TRUE) + tr <- structure(list(edge = res$edge, Nnode = d$nTip - 1L, + tip.label = names(d$phy)), class = "phylo") + list(tree = norm(tr), len = TreeLength(tr, d$phy)) +} + +# All-tips reroot-invariant TBR, collapsed OFF. +RootInvariantU <- function(startTree, seed, rerootTips = names(d$phy)) { + cur <- TsTbrU(startTree, seed); best <- cur$tree; bestLen <- cur$len + repeat { + improved <- FALSE + for (tp in rerootTips) { + rr <- norm(ape::root(best, outgroup = tp, resolve.root = TRUE)) + r <- TsTbrU(rr, seed) + if (r$len < bestLen) { bestLen <- r$len; best <- r$tree; improved <- TRUE } + } + if (!improved) break + } + list(len = bestLen, tree = best) +} + +probe0 <- function(tree, label, baseLen) { + nb <- TBRMoves(norm(tree)); ls <- vapply(nb, TreeLength, double(1), d$phy) + best <- min(ls); nBetter <- sum(ls < baseLen - 0.5) + cat(sprintf(" [%s] enumerator: %d neighbours, best=%.0f, %d improving %s\n", + label, length(nb), best, nBetter, + if (nBetter == 0) "=> CLEAN (0 improving)" else "=> still incomplete")) + invisible(list(best = best, nBetter = nBetter)) +} + +cat("=== Collapsed-pruning isolation test (Zanol2014) ===\n\n") + +# (A) Direct: feed the OLD collapsed-ON optimum (1284) into collapsed-OFF descent. +optC <- norm(ape::read.tree("dev/benchmarks/tbr_results/ts_reroot_invariant_opt.tre")) +cat(sprintf("(A) collapsed-ON optimum = %.0f\n", TreeLength(optC, d$phy))) +a1 <- TsTbrU(optC, seed = 1) +cat(sprintf(" -> collapsed-OFF strict descent (same rooting): %.0f -> %.0f\n", + TreeLength(optC, d$phy), a1$len)) +aRI <- RootInvariantU(optC, seed = 1) +cat(sprintf(" -> collapsed-OFF all-tips reroot-invariant: %.0f -> %.0f\n", + TreeLength(optC, d$phy), aRI$len)) +probe0(aRI$tree, "A all-tips opt", aRI$len) + +# (B) From scratch: random seed1 start, collapsed-OFF all-tips reroot-invariant. +st <- norm({ set.seed(1001); RandomTree(d$phy, root = TRUE) }) +cat(sprintf("\n(B) random start = %.0f\n", TreeLength(st, d$phy))) +bRI <- RootInvariantU(st, seed = 1) +cat(sprintf(" -> collapsed-OFF all-tips reroot-invariant: %.0f\n", bRI$len)) +probe0(bRI$tree, "B all-tips opt", bRI$len) +ape::write.tree(bRI$tree, "dev/benchmarks/tbr_results/ts_collapsedoff_opt.tre") + +cat("\nReading: optimum dropping below 1284 AND probe 0-improving => collapsed pruning\n", + "was the residual neighbourhood cause (SPR-sound, TBR-unsound). Remaining gap to\n", + "TNT's 1264 (if any) is then basin/path, not move-set.\n", sep = "") diff --git a/dev/benchmarks/tbr_crossfeed.R b/dev/benchmarks/tbr_crossfeed.R new file mode 100644 index 000000000..94f2930ca --- /dev/null +++ b/dev/benchmarks/tbr_crossfeed.R @@ -0,0 +1,38 @@ +# tbr_crossfeed.R -- the discriminating 2x2: feed each engine's local optimum +# into the other. Distinguishes "TS reaches a worse basin" (escape/path +# problem) from "TS terminates before a true TBR local optimum" +# (neighbourhood incompleteness / premature stop). +source("dev/benchmarks/tbr_shared_start_lib.R") +d <- prepareDataset("Zanol2014") + +set.seed(11) +wag <- TreeSearch:::ts_random_wagner_tree(d$contrast, d$tip_data, d$weight, d$levels) +wagTree <- Preorder(RenumberTips(structure(list(edge=wag$edge, Nnode=d$nTip-1L, + tip.label=names(d$phy)), class="phylo"), names(d$phy))) +cat("Shared start = 1478\n\n") + +# Each engine to its own local optimum from 1478. +tsRun <- TsTbr(d, wagTree, seed = 2, acceptEqual = FALSE) +tsOpt <- tsRun$tree +cat("TS strict descent -> ", tsRun$row$final_len, " (TS local optimum)\n") + +tntRow <- TntTbr(d, wagTree, seed = 2, mulpars = FALSE, hold = 1, randclip = TRUE) +tntOpt <- attr(tntRow, "tree") +cat("TNT bbreak nomulpars-> ", tntRow$final_len, " (TNT local optimum)\n\n") + +# (1) TS local optimum -> TNT bbreak. Does TNT improve a TS-converged tree? +cat("=== (1) Feed TS local optimum (", tsRun$row$final_len, ") into TNT bbreak ===\n", sep="") +for (rc in c(FALSE, TRUE)) { + r <- TntTbr(d, tsOpt, seed = 2, mulpars = FALSE, hold = 1, randclip = rc) + cat(sprintf(" TNT nomulpars randclip=%-5s : %s -> %s\n", rc, r$start_len, r$final_len)) +} +rb <- TntTbr(d, tsOpt, seed = 2, mulpars = TRUE, hold = 1000, randclip = TRUE) +cat(sprintf(" TNT mulpars hold1000 : %s -> %s\n\n", rb$start_len, rb$final_len)) + +# (2) TNT local optimum -> TS strict bbreak. Holds (escape) or wanders (bug)? +cat("=== (2) Feed TNT local optimum (", tntRow$final_len, ") into TS strict TBR ===\n", sep="") +r2 <- TsTbr(d, tntOpt, seed = 2, acceptEqual = FALSE) +cat(sprintf(" TS strict descent : %s -> %s (converged=%s)\n", + r2$row$start_len, r2$row$final_len, r2$converged)) +r2e <- TsTbr(d, tntOpt, seed = 2, acceptEqual = TRUE, maxHits = 50L) +cat(sprintf(" TS plateau (mh50) : %s -> %s\n", r2e$row$start_len, r2e$row$final_len)) diff --git a/dev/benchmarks/tbr_gate.R b/dev/benchmarks/tbr_gate.R new file mode 100644 index 000000000..94239d50e --- /dev/null +++ b/dev/benchmarks/tbr_gate.R @@ -0,0 +1,13 @@ +# tbr_gate.R -- pre-flight gate #1: TS->TNT tree round-trip length identity. +source("dev/benchmarks/tbr_shared_start_lib.R") + +d <- prepareDataset("Zanol2014") +t0 <- ape::read.tree(file.path(T0_DIR, "Zanol2014.tre")) +cat("TreeLength(T0) =", TreeLength(t0, d$phy), "\n\n") + +script <- c("mxram 1024;", "taxname=;", "proc data.tnt;", + paste0("tread ", ToTntTree(t0), ";"), + "length;", "quit;") +out <- RunTnt(d$phy, script, tag = "gate") +cat("--- RAW TNT OUTPUT ---\n") +cat(out, sep = "\n") diff --git a/dev/benchmarks/tbr_grid.R b/dev/benchmarks/tbr_grid.R new file mode 100644 index 000000000..c64c3f5a9 --- /dev/null +++ b/dev/benchmarks/tbr_grid.R @@ -0,0 +1,102 @@ +# tbr_grid.R -- the deliverable ensemble grid. +# +# For each dataset, build a SHARED ladder of start trees spanning a range of +# qualities, write each to Newick ONCE, then feed the IDENTICAL Newick into +# BOTH engines and run TBR-to-convergence across several seeds, in two modes: +# Mode A strict single-tree TBR (TS acceptEqual=F ; TNT nomulpars hold 1) +# Mode B buffer / plateau (TS acceptEqual=T ; TNT mulpars hold 1000) +# Results are paired by start tree. Writes a tidy CSV + prints summary tables. +# +# Usage: Rscript dev/benchmarks/tbr_grid.R [datasets...] (default Zanol Zhu) +source("dev/benchmarks/tbr_shared_start_lib.R") + +args <- commandArgs(trailingOnly = TRUE) +DSETS <- if (length(args)) args else c("Zanol2014", "Zhu2013") +SEEDS <- 1:6 +OUTDIR <- "dev/benchmarks/tbr_results" +dir.create(OUTDIR, showWarnings = FALSE, recursive = TRUE) + +asPhylo <- function(edge, d) + structure(list(edge = edge, Nnode = d$nTip - 1L, + tip.label = names(d$phy)), class = "phylo") + +# Build a deterministic quality ladder of start trees for dataset `d`. +# Returns a named list of phylo trees (tips renumbered to d$phy order). +buildStartLadder <- function(d) { + norm <- function(tr) Preorder(RenumberTips(tr, names(d$phy))) + ladder <- list() + # 2 random topologies (poorest) + for (i in 1:2) { + set.seed(1000 + i) + ladder[[paste0("random", i)]] <- norm(RandomTree(d$phy, root = TRUE)) + } + # 2 RAS Wagner trees (poor) + for (i in 1:2) { + set.seed(2000 + i) + w <- TreeSearch:::ts_random_wagner_tree(d$contrast, d$tip_data, d$weight, d$levels) + ladder[[paste0("wagner", i)]] <- norm(asPhylo(w$edge, d)) + } + # 1 partially-TBR-optimised tree (medium): a Wagner pushed ~15 accepted moves + set.seed(3001) + w <- TreeSearch:::ts_random_wagner_tree(d$contrast, d$tip_data, d$weight, d$levels) + partial <- TsTbr(d, norm(asPhylo(w$edge, d)), seed = 3001, + acceptEqual = FALSE, maxChanges = 15L)$tree + ladder[["partial"]] <- norm(partial) + # near-optimal anchor: canonical TNT T0 (both engines should hold it) + ladder[["t0anchor"]] <- norm(ape::read.tree(file.path(T0_DIR, paste0(d$name, ".tre")))) + ladder +} + +allRows <- list() +for (dn in DSETS) { + d <- prepareDataset(dn) + cat(sprintf("\n=== %s (n=%d) ===\n", dn, d$nTip)) + ladder <- buildStartLadder(d) + # Persist the shared starts as a single multi-Newick file (inspectable). + starts <- structure(ladder, class = "multiPhylo") + ape::write.tree(starts, file.path(OUTDIR, paste0(dn, "_starts.nwk"))) + cat("start lengths:", + paste(sprintf("%s=%.0f", names(ladder), + vapply(ladder, TreeLength, double(1), d$phy)), collapse = " "), "\n") + + for (sname in names(ladder)) { + st <- ladder[[sname]] + for (s in SEEDS) { + runs <- list( + A_strict = list(TntTbr(d, st, seed = s, mulpars = FALSE, hold = 1), + TsTbr(d, st, seed = s, acceptEqual = FALSE)$row), + B_buffer = list(TntTbr(d, st, seed = s, mulpars = TRUE, hold = 1000), + TsTbr(d, st, seed = s, acceptEqual = TRUE, maxHits = 50L)$row)) + for (md in names(runs)) for (rr in runs[[md]]) { + rr$dataset <- dn; rr$start <- sname; rr$mode <- md + allRows[[length(allRows) + 1]] <- rr + } + } + cat(".") + } + cat(" done\n") +} + +res <- do.call(rbind, lapply(allRows, function(r) r[, c( + "dataset","start","mode","engine","seed", + "start_len","final_len","rearrangements")])) +res$improvement <- res$start_len - res$final_len +csv <- file.path(OUTDIR, paste0("tbr_grid_", paste(DSETS, collapse = "_"), ".csv")) +write.csv(res, csv, row.names = FALSE) +cat("\nWrote", nrow(res), "rows to", csv, "\n") + +# ---- Summary: per dataset x start x mode x engine -> final_len distribution ---- +cat("\n=== ENSEMBLE SUMMARY (final length over", length(SEEDS), "seeds) ===\n") +fmt <- function(v) sprintf("min=%.0f med=%.0f max=%.0f", + min(v), stats::median(v), max(v)) +for (dn in unique(res$dataset)) for (md in unique(res$mode)) { + cat(sprintf("\n-- %s mode %s --\n", dn, md)) + sub <- res[res$dataset == dn & res$mode == md, ] + for (sn in unique(sub$start)) { + ss <- sub[sub$start == sn, ] + sl <- ss$start_len[1] + tnt <- ss$final_len[ss$engine == "TNT"]; ts <- ss$final_len[ss$engine == "TS"] + cat(sprintf(" %-9s start=%-4.0f TNT[%s] TS[%s] gap(medianTS-medianTNT)=%+.0f\n", + sn, sl, fmt(tnt), fmt(ts), stats::median(ts) - stats::median(tnt))) + } +} diff --git a/dev/benchmarks/tbr_iw_directvsphys.R b/dev/benchmarks/tbr_iw_directvsphys.R new file mode 100644 index 000000000..a2f034e9b --- /dev/null +++ b/dev/benchmarks/tbr_iw_directvsphys.R @@ -0,0 +1,55 @@ +# tbr_iw_directvsphys.R -- for ONE failing (nTip, idx), compare the direct +# unrooted path vs the physical-reroot path (TS_PHYS_REROOT=1, exact multi- +# rooting) on the SAME start. If physical reaches a lower IW than direct, the +# residual is a DIRECT-PATH gap (single-rooting enumeration or incremental +# scoring); if both miss the oracle's improver, it is a deeper/plateau issue. +# +# Usage: Rscript dev/benchmarks/tbr_iw_directvsphys.R [nTip] [idx] [concavity] +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-tbr"), + winslash = "/")) + library(TreeTools) +}) +args <- commandArgs(trailingOnly = TRUE) +nTip <- if (length(args) >= 1) as.integer(args[[1]]) else 16L +idx <- if (length(args) >= 2) as.integer(args[[2]]) else 19L +conc <- if (length(args) >= 3) as.numeric(args[[3]]) else 10 +nChar <- 60L; nState <- 3L + +randomData <- function(seed) { + set.seed(seed) + tips <- paste0("t", seq_len(nTip)) + m <- matrix(sample(0:(nState - 1L), nTip * nChar, replace = TRUE), + nrow = nTip, dimnames = list(tips, NULL)) + phy <- phangorn::phyDat(m, type = "USER", levels = as.character(0:(nState - 1L))) + at <- attributes(phy) + list(phy = phy, contrast = at$contrast, + tip_data = matrix(unlist(phy, use.names = FALSE), nrow = length(phy), byrow = TRUE), + weight = at$weight, levels = at$levels, nTip = length(phy), labels = names(phy)) +} +scoreTree <- function(tree, d) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + TreeSearch:::ts_fitch_score(edge, d$contrast, d$tip_data, d$weight, d$levels, concavity = conc) +} +kernelTbr <- function(tree, d, phys) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + if (phys) Sys.setenv(TS_PHYS_REROOT = "1") else Sys.unsetenv("TS_PHYS_REROOT") + res <- TreeSearch:::ts_tbr_diagnostics( + edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L, concavity = conc, unrooted = TRUE) + Sys.unsetenv("TS_PHYS_REROOT") + list(tree = structure(list(edge = res$edge, Nnode = d$nTip - 1L, tip.label = d$labels), + class = "phylo"), score = res$score) +} + +d <- randomData(1000L + idx) +set.seed(7000L + idx); start <- RandomTree(d$phy, root = TRUE) +set.seed(idx); rD <- kernelTbr(start, d, FALSE) +set.seed(idx); rP <- kernelTbr(start, d, TRUE) +cat(sprintf("=== direct vs physical, nTip=%d tree#%d conc=%g ===\n", nTip, idx, conc)) +cat(sprintf("DIRECT : reported=%.5f ts_fitch=%.5f\n", rD$score, scoreTree(rD$tree, d))) +cat(sprintf("PHYSICAL : reported=%.5f ts_fitch=%.5f\n", rP$score, scoreTree(rP$tree, d))) +dlo <- scoreTree(rD$tree, d); plo <- scoreTree(rP$tree, d) +cat(sprintf("=> %s\n", if (plo < dlo - 1e-6) "PHYSICAL reaches lower => DIRECT-PATH gap (single-rooting)" + else if (dlo < plo - 1e-6) "DIRECT lower (physical incomplete?!)" + else "EQUAL => both miss it (deeper / plateau / common enumeration gap)")) diff --git a/dev/benchmarks/tbr_iw_multistart.R b/dev/benchmarks/tbr_iw_multistart.R new file mode 100644 index 000000000..8c0e95493 --- /dev/null +++ b/dev/benchmarks/tbr_iw_multistart.R @@ -0,0 +1,42 @@ +# tbr_iw_multistart.R -- multi-start IW search quality on one dataset. Runs the +# kernel IW TBR (unrooted) from many random starts and reports the BEST IW score +# reached, so single-start basin shifts (expected from the exact-scoring fix) +# wash out. Use to check the fix did not degrade achievable IW quality. +# +# Usage: Rscript dev/benchmarks/tbr_iw_multistart.R [nTip] [idx] [concavity] [nStart] +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-tbr"), + winslash = "/")) + library(TreeTools) +}) +a <- commandArgs(trailingOnly = TRUE) +nTip <- if (length(a) >= 1) as.integer(a[[1]]) else 16L +idx <- if (length(a) >= 2) as.integer(a[[2]]) else 19L +conc <- if (length(a) >= 3) as.numeric(a[[3]]) else 10 +nStart <- if (length(a) >= 4) as.integer(a[[4]]) else 40L +nChar <- 60L; nState <- 3L + +set.seed(1000L + idx) +tips <- paste0("t", seq_len(nTip)) +m <- matrix(sample(0:(nState-1L), nTip*nChar, replace=TRUE), nrow=nTip, dimnames=list(tips,NULL)) +phy <- phangorn::phyDat(m, type="USER", levels=as.character(0:(nState-1L))) +at <- attributes(phy) +d <- list(contrast=at$contrast, + tip_data=matrix(unlist(phy,use.names=FALSE), nrow=length(phy), byrow=TRUE), + weight=at$weight, levels=at$levels, labels=names(phy), phy=phy) + +kbest <- function(seedBase) { + best <- Inf + for (s in seq_len(nStart)) { + set.seed(seedBase + s); st <- RandomTree(d$phy, root=TRUE) + edge <- Preorder(RenumberTips(st, d$labels))[["edge"]] + set.seed(s) + res <- TreeSearch:::ts_tbr_diagnostics(edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits=1L, acceptEqual=FALSE, maxChanges=0L, concavity=conc, unrooted=TRUE) + if (res$score < best) best <- res$score + } + best +} +b <- kbest(50000L) +cat(sprintf("nTip=%d #%d conc=%g %d random starts => BEST IW = %.5f\n", + nTip, idx, conc, nStart, b)) diff --git a/dev/benchmarks/tbr_iw_residual.R b/dev/benchmarks/tbr_iw_residual.R new file mode 100644 index 000000000..7c7e33aff --- /dev/null +++ b/dev/benchmarks/tbr_iw_residual.R @@ -0,0 +1,106 @@ +# tbr_iw_residual.R -- characterise an IW unrooted-TBR residual miss. +# For a given (nTip, treeIndex), reproduce the kernel's converged tree, then +# split the improving neighbourhood into SPR-reachable vs TBR-only, and report +# the broken bipartition of the single best improving move (vs the converged +# tree) so we can tell if it is the ROOT edge or a non-root edge, and whether +# the deficit is at the SPR (clip+graft) level or the TBR (reroot) level. +# +# Usage: Rscript dev/benchmarks/tbr_iw_residual.R [nTip] [treeIndex] [concavity] +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-tbr"), + winslash = "/")) + library(TreeTools) +}) +args <- commandArgs(trailingOnly = TRUE) +nTip <- if (length(args) >= 1) as.integer(args[[1]]) else 16L +idx <- if (length(args) >= 2) as.integer(args[[2]]) else 19L +conc <- if (length(args) >= 3) as.numeric(args[[3]]) else 10 +nChar <- 60L; nState <- 3L; eps <- 1e-6 + +randomData <- function(seed) { + set.seed(seed) + tips <- paste0("t", seq_len(nTip)) + m <- matrix(sample(0:(nState - 1L), nTip * nChar, replace = TRUE), + nrow = nTip, dimnames = list(tips, NULL)) + phy <- phangorn::phyDat(m, type = "USER", levels = as.character(0:(nState - 1L))) + at <- attributes(phy) + list(phy = phy, contrast = at$contrast, + tip_data = matrix(unlist(phy, use.names = FALSE), nrow = length(phy), byrow = TRUE), + weight = at$weight, levels = at$levels, nTip = length(phy), labels = names(phy)) +} +scoreTree <- function(tree, d) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + TreeSearch:::ts_fitch_score(edge, d$contrast, d$tip_data, d$weight, d$levels, concavity = conc) +} +kernelTbr <- function(tree, d) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + res <- TreeSearch:::ts_tbr_diagnostics( + edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L, concavity = conc, unrooted = TRUE) + structure(list(edge = res$edge, Nnode = d$nTip - 1L, tip.label = d$labels), class = "phylo") +} +# Bipartitions (as sorted tip-sets of the smaller side) of an unrooted tree, +# computed manually from the edge matrix (robust to TreeTools S3 export quirks). +bips <- function(tree) { + tree <- Preorder(tree) + e <- tree$edge; nTip <- length(tree$tip.label); N <- nTip + tree$Nnode + desc <- vector("list", N) + for (i in seq_len(nTip)) desc[[i]] <- i + for (k in rev(seq_len(nrow(e)))) { # reverse preorder => children first + p <- e[k, 1]; ch <- e[k, 2] + desc[[p]] <- c(desc[[p]], desc[[ch]]) + } + tips <- tree$tip.label; out <- character(0) + for (k in seq_len(nrow(e))) { + ch <- e[k, 2] + if (ch <= nTip) next # leaf edge = trivial split + a <- sort(unique(desc[[ch]])) + if (length(a) < 2 || length(a) > nTip - 2) next + side <- tips[a]; other <- tips[setdiff(seq_len(nTip), a)] + s <- if (length(side) <= length(other)) side else other + out <- c(out, paste(sort(s), collapse = ",")) + } + sort(unique(out)) +} + +d <- randomData(1000L + idx) +set.seed(7000L + idx); start <- RandomTree(d$phy, root = TRUE) +set.seed(idx) +conv <- kernelTbr(start, d) +base <- scoreTree(conv, d) +cat(sprintf("=== IW residual: nTip=%d tree#%d concavity=%g ===\n", nTip, idx, conc)) +cat(sprintf("converged IW = %.5f\n", base)) + +# Enumerate neighbours at two rootings; keep best SPR and best TBR separately. +sprScores <- c(); tbrTrees <- list(); tbrScores <- c() +for (rt in d$labels[1:2]) { + rr <- Preorder(RootTree(conv, rt)) + sm <- SPRMoves(rr); tm <- TBRMoves(rr) + sprScores <- c(sprScores, vapply(sm, scoreTree, double(1), d = d)) + ts <- vapply(tm, scoreTree, double(1), d = d) + tbrScores <- c(tbrScores, ts); tbrTrees <- c(tbrTrees, tm) +} +bestSpr <- if (length(sprScores)) min(sprScores) else Inf +bestTbr <- if (length(tbrScores)) min(tbrScores) else Inf +cat(sprintf("best SPR neighbour = %.5f (improves: %s)\n", bestSpr, bestSpr < base - eps)) +cat(sprintf("best TBR neighbour = %.5f (improves: %s)\n", bestTbr, bestTbr < base - eps)) +cat(sprintf("=> deficit level: %s\n", + if (bestSpr < base - eps) "SPR (basic clip+graft miss)" else + if (bestTbr < base - eps) "TBR-reroot only" else "none (oracle artifact?)")) + +# Characterise the single best improving move: which bipartition(s) does it +# add/remove vs the converged tree? Is the changed edge incident to the root? +if (bestTbr < base - eps) { + wi <- which.min(tbrScores); bestT <- Preorder(tbrTrees[[wi]]) + cb <- bips(conv); tb <- bips(bestT) + added <- setdiff(tb, cb); removed <- setdiff(cb, tb) + cat(sprintf("\nbest improving tree IW = %.5f (delta %.5f)\n", tbrScores[wi], base - tbrScores[wi])) + cat(sprintf("bipartitions changed: %d removed, %d added\n", length(removed), length(added))) + cat("REMOVED (present in converged, gone in improver):\n") + for (s in removed) cat(" -", s, "\n") + cat("ADDED (new in improver):\n") + for (s in added) cat(" +", s, "\n") + # Re-feed the improver to the kernel: does it climb further (real basin)? + reconv <- scoreTree(kernelTbr(bestT, d), d) + cat(sprintf("\nkernel re-run from improver converges to IW = %.5f\n", reconv)) +} diff --git a/dev/benchmarks/tbr_missing_move_characterise.R b/dev/benchmarks/tbr_missing_move_characterise.R new file mode 100644 index 000000000..5df091af2 --- /dev/null +++ b/dev/benchmarks/tbr_missing_move_characterise.R @@ -0,0 +1,58 @@ +# tbr_missing_move_characterise.R -- runs only if the neighbourhood probe shows +# the kernel is incomplete (canonical TBR improves the TS reroot-invariant +# optimum). Pins WHICH kernel pruning drops the improving move, no rebuild. +# +# Suspects in ts_tbr.cpp: +# * L812 smaller-subtree skip -> improving move clips the LARGER side only. +# * L817/L919 collapsed pruning -> improving move touches a ZERO-LENGTH edge. +# * indirect-scoring cutoff / vp-dedup -> neither of the above. +source("dev/benchmarks/tbr_shared_start_lib.R") +d <- prepareDataset("Zanol2014") +norm <- function(tr) Preorder(RenumberTips(tr, names(d$phy))) + +optFile <- "dev/benchmarks/tbr_results/ts_reroot_invariant_opt.tre" +nbFile <- "dev/benchmarks/tbr_results/ts_opt_best_neighbour.tre" +stopifnot(file.exists(optFile), file.exists(nbFile)) +opt <- norm(ape::read.tree(optFile)) +nb <- norm(ape::read.tree(nbFile)) +optLen <- TreeLength(opt, d$phy); nbLen <- TreeLength(nb, d$phy) +cat(sprintf("TS optimum = %.0f ; best canonical-TBR neighbour = %.0f (improve %.0f)\n\n", + optLen, nbLen, optLen - nbLen)) + +# --- Zero-length edges of the optimum (the collapsed-pruning suspects) --- +# An internal edge is "zero length" if collapsing it (merging child into parent) +# leaves the parsimony length unchanged. +ed <- opt[["edge"]]; nTip <- d$nTip +internalChildEdges <- which(ed[, 2] > nTip) # edges whose child is internal +zeroLen <- 0L +for (e in internalChildEdges) { + collapsed <- opt + collapsed$edge.length <- NULL + collapsed <- ape::di2multi( # collapse just this edge via a tiny length vector + { t2 <- opt; t2$edge.length <- rep(1, nrow(ed)); t2$edge.length[e] <- 0; t2 }, + tol = 0.5) + if (abs(TreeLength(collapsed, d$phy) - optLen) < 0.5) zeroLen <- zeroLen + 1L +} +cat(sprintf("zero-length internal edges in the optimum: %d / %d\n", + zeroLen, length(internalChildEdges))) +cat(if (zeroLen == 0) + " => collapsed-edge pruning is INACTIVE here; cause is cutoff/dedup, not collapsed.\n" + else + " => collapsed-edge pruning is a LIVE suspect (zero-length edges present).\n") + +# --- Move magnitude: splits that differ between optimum and best neighbour --- +spOpt <- TreeTools::as.Splits(opt) +spNb <- TreeTools::as.Splits(nb, tipLabels = TipLabels(opt)) +# Count splits in one but not the other (RF-style raw difference). +inOpt <- apply(as.logical(spOpt), 1, function(r) paste(as.integer(r), collapse = "")) +inNb <- apply(as.logical(spNb), 1, function(r) paste(as.integer(r), collapse = "")) +# Normalise complement (a split and its complement are the same bipartition). +canon <- function(s) { v <- as.integer(strsplit(s, "")[[1]]) + if (v[1] == 1) paste(1L - v, collapse = "") else s } +inOptC <- vapply(inOpt, canon, ""); inNbC <- vapply(inNb, canon, "") +nDiff <- length(setdiff(inOptC, inNbC)) +cat(sprintf("\nsplits in optimum absent from best neighbour: %d (TBR move magnitude)\n", nDiff)) + +cat("\nReading:\n", + " - zero-length edges present + small splits-diff => collapsed pruning the likely culprit.\n", + " - no zero-length edges => indirect-cutoff / vp-dedup drops the move.\n", sep = "") diff --git a/dev/benchmarks/tbr_neighbourhood_probe.R b/dev/benchmarks/tbr_neighbourhood_probe.R new file mode 100644 index 000000000..ce2588d4a --- /dev/null +++ b/dev/benchmarks/tbr_neighbourhood_probe.R @@ -0,0 +1,84 @@ +# tbr_neighbourhood_probe.R -- fork-settler (advisor, 2026-06-18). +# +# The reroot-invariant cross-feed showed TNT nomulpars improves the all-tips +# reroot-invariant TS optimum (1284 -> 1270). Two possible causes: +# (i) TS's KERNEL TBR is incomplete: its optimisations (L812 smaller-subtree +# skip, collapsed-edge pruning, indirect-scoring cutoff) prune real moves, +# so it declares convergence with an improving canonical-TBR move present. +# (ii) TNT's "TBR" exceeds textbook single-tree TBR (collapse / re-resolution). +# +# Discriminator: enumerate the FULL unrooted-TBR neighbourhood of the TS optimum +# with TreeSearch's SEPARATE, UNOPTIMISED R/Rcpp enumerator (TBR(tree, -1) from +# rearrange.cpp -- a different code path from ts_tbr.cpp). If it finds an +# improving neighbour, the kernel missed a canonical TBR move => cause (i). +source("dev/benchmarks/tbr_shared_start_lib.R") + +d <- prepareDataset("Zanol2014") +norm <- function(tr) Preorder(RenumberTips(tr, names(d$phy))) +asPhylo <- function(edge) structure(list(edge = edge, Nnode = d$nTip - 1L, + tip.label = names(d$phy)), class = "phylo") + +RootInvariantTbr <- function(startTree, seed, rerootTips = names(d$phy)) { + cur <- TsTbr(d, startTree, seed = seed, acceptEqual = FALSE) + best <- cur$tree; bestLen <- cur$row$final_len + repeat { + improved <- FALSE + for (tp in rerootTips) { + rr <- norm(ape::root(best, outgroup = tp, resolve.root = TRUE)) + r <- TsTbr(d, rr, seed = seed, acceptEqual = FALSE) + if (r$row$final_len < bestLen) { bestLen <- r$row$final_len; best <- r$tree; improved <- TRUE } + } + if (!improved) break + } + list(len = bestLen, tree = norm(best)) +} + +# Reconstruct (and cache) the random-seed1 reroot-invariant optimum (~1284). +cacheFile <- "dev/benchmarks/tbr_results/ts_reroot_invariant_opt.tre" +if (file.exists(cacheFile)) { + tsOpt <- norm(ape::read.tree(cacheFile)); tsLen <- TreeLength(tsOpt, d$phy) + cat(sprintf("Loaded cached TS optimum: len=%.0f\n", tsLen)) +} else { + st <- norm({ set.seed(1001); RandomTree(d$phy, root = TRUE) }) + ri <- RootInvariantTbr(st, seed = 1) + tsOpt <- ri$tree; tsLen <- ri$len + ape::write.tree(tsOpt, cacheFile) + cat(sprintf("Computed TS reroot-invariant optimum: len=%.0f (cached)\n", tsLen)) +} + +# Probe a tree's full unrooted-TBR neighbourhood with the R/Rcpp enumerator +# (TBRMoves -> all_tbr in rearrange.cpp -- a different code path from +# ts_tbr.cpp). Scores every neighbour; reports the best + how many improve, +# and saves the best improving neighbour for move characterisation. +probeNeighbourhood <- function(tree, label, baseLen, saveBest = NULL) { + cat(sprintf("\n== %s (len=%.0f) ==\n", label, baseLen)) + nb <- TBRMoves(norm(tree)) + n <- length(nb) + ls <- vapply(nb, TreeLength, double(1), d$phy) + best <- min(ls); bestIdx <- which.min(ls); nBetter <- sum(ls < baseLen - 0.5) + cat(sprintf(" enumerated+scored %d TBR neighbours : best = %.0f (%d improving)\n", + n, best, nBetter)) + if (best < baseLen - 0.5) { + cat(sprintf(" => KERNEL INCOMPLETE: canonical TBR improves it by %.0f (%.0f -> %.0f).\n", + baseLen - best, baseLen, best)) + if (!is.null(saveBest)) { + ape::write.tree(norm(nb[[bestIdx]]), saveBest) + cat(" best improving neighbour saved ->", saveBest, "\n") + } + } else { + cat(" => no single canonical-TBR move improves it (true TBR optimum by the enumerator).\n") + } + invisible(list(best = best, nBetter = nBetter, n = n)) +} + +# (1) The TS optimum the kernel converged on (all-tips). Does canonical TBR improve it? +probeNeighbourhood(tsOpt, "TS reroot-invariant optimum", tsLen, + saveBest = "dev/benchmarks/tbr_results/ts_opt_best_neighbour.tre") + +# (2) Sanity: TNT's own optimum -- the enumerator should agree it's TBR-optimal +# (TS kernel already holds it). Recompute a TNT optimum to probe. +tntRow <- TntTbr(d, norm({ set.seed(1001); RandomTree(d$phy, root = TRUE) }), + seed = 1, mulpars = FALSE, hold = 1) +tntOpt <- attr(tntRow, "tree") +if (!is.null(tntOpt)) + probeNeighbourhood(tntOpt, "TNT own optimum", tntRow$final_len) diff --git a/dev/benchmarks/tbr_oracle.R b/dev/benchmarks/tbr_oracle.R new file mode 100644 index 000000000..ad27092e9 --- /dev/null +++ b/dev/benchmarks/tbr_oracle.R @@ -0,0 +1,126 @@ +# tbr_oracle.R -- small-tree DIFFERENTIAL ORACLE for kernel TBR completeness. +# +# "Everything correct" (project lead, 2026-06-18) made checkable: run the +# IN-KERNEL tbr_search (the path the default actually uses -- single rooting, no +# R reroot scaffold) to convergence, then assert the package's own UNOPTIMISED +# enumerator (TBRMoves/SPRMoves -> all_tbr/all_spr in rearrange.cpp) finds NO +# strictly-improving neighbour. Any failure prints a concrete small tree + the +# exact missing move -- debuggable in seconds, and a permanent regression test. +# +# Scope: tests the cpp-search kernel (ts_tbr_diagnostics -> tbr_search), which is +# what MaximizeParsimony / driven / ratchet / sector / fuse all route through. +# The old CustomSearch/TreeSearch path (RootedTBRSwap + Morphy) is separate. +# +# Usage: Rscript dev/benchmarks/tbr_oracle.R [nTrees=200] [nTip=12] [unrooted=0] +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-tbr"), + winslash = "/")) + library(TreeTools) +}) + +args <- commandArgs(trailingOnly = TRUE) +nTrees <- if (length(args) >= 1) as.integer(args[[1]]) else 200L +nTip <- if (length(args) >= 2) as.integer(args[[2]]) else 12L +unrooted <- if (length(args) >= 3) as.logical(as.integer(args[[3]])) else FALSE +emul <- if (length(args) >= 4) as.logical(as.integer(args[[4]])) else FALSE +nChar <- 60L +nState <- 3L # states 0,1,2 (+ '?') + +# Build a random USER-type phyDat on `nTip` tips, `nChar` characters. +randomData <- function(seed) { + set.seed(seed) + tips <- paste0("t", seq_len(nTip)) + m <- matrix(sample(0:(nState - 1L), nTip * nChar, replace = TRUE), + nrow = nTip, dimnames = list(tips, NULL)) + phy <- phangorn::phyDat(m, type = "USER", levels = as.character(0:(nState - 1L))) + at <- attributes(phy) + list(phy = phy, contrast = at$contrast, + tip_data = matrix(unlist(phy, use.names = FALSE), nrow = length(phy), byrow = TRUE), + weight = at$weight, levels = at$levels, nTip = length(phy), + labels = names(phy)) +} + +# Run the in-kernel TBR (single rooting -- as the default runs) to convergence. +# `unrooted=TRUE` requires the move-fix prototype build; on the clean post-fix +# cpp-search build (no `unrooted` arg) we call the default signature. +kernelTbr <- function(tree, d, unrooted) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + res <- if (isTRUE(unrooted)) + TreeSearch:::ts_tbr_diagnostics( + edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L, unrooted = TRUE) + else + TreeSearch:::ts_tbr_diagnostics( + edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L) + structure(list(edge = res$edge, Nnode = d$nTip - 1L, tip.label = d$labels), + class = "phylo") +} + +# All-tips reroot emulation: converge, then reroot at each tip and re-converge, +# adopting improvements, until a full tip sweep yields nothing. Approximates a +# true in-kernel unrooted/root-edge mechanism using the single-rooting kernel. +# Isolates the ROOT-EDGE residual from any move-generation residual. +kernelTbrEmul <- function(tree, d, unrooted) { + best <- kernelTbr(tree, d, unrooted); bestLen <- TreeLength(best, d$phy) + repeat { + improved <- FALSE + for (tp in d$labels) { + rr <- Preorder(RootTree(best, tp)) + r <- kernelTbr(rr, d, unrooted); rl <- TreeLength(r, d$phy) + if (rl < bestLen - 0.5) { bestLen <- rl; best <- r; improved <- TRUE } + } + if (!improved) break + } + best +} + +# Full unrooted-TBR cleanliness check: all_tbr at TWO distinct rootings (tip1 & +# tip2) covers every break edge (each rooting only omits its own root-edge = +# that tip's pendant); plus all_spr for good measure. Returns the best +# improving neighbour length and tree, or NULL if clean. +bestImproving <- function(tree, d) { + base <- TreeLength(tree, d$phy) + cand <- list() + for (rt in d$labels[1:2]) { + rr <- Preorder(RootTree(tree, rt)) + cand <- c(cand, TBRMoves(rr), SPRMoves(rr)) + } + if (!length(cand)) return(NULL) + ls <- vapply(cand, TreeLength, double(1), d$phy) + if (min(ls) < base - 0.5) + list(len = min(ls), tree = cand[[which.min(ls)]], base = base) + else NULL +} + +cat(sprintf("=== TBR completeness oracle: %d trees, %d tips, unrooted=%s ===\n", + nTrees, nTip, unrooted)) +fails <- 0L; firstFail <- NULL +for (i in seq_len(nTrees)) { + d <- randomData(1000L + i) + set.seed(7000L + i) + start <- RandomTree(d$phy, root = TRUE) + set.seed(i) # kernel RNG (clip order) + conv <- if (emul) kernelTbrEmul(start, d, unrooted) else kernelTbr(start, d, unrooted) + convLen <- TreeLength(conv, d$phy) + imp <- bestImproving(conv, d) + if (!is.null(imp)) { + fails <- fails + 1L + if (is.null(firstFail)) + firstFail <- list(i = i, conv = conv, convLen = convLen, imp = imp, + start = start, d = d) + } +} +cat(sprintf("\nRESULT: %d / %d converged trees had an improving canonical neighbour (FAILURES)\n", + fails, nTrees)) +if (fails == 0L) { + cat("=> KERNEL TBR IS COMPLETE on this sample (0 missing moves).\n") +} else { + ff <- firstFail + cat(sprintf("=> INCOMPLETE. First failure: tree #%d, kernel converged %.0f, canonical finds %.0f (miss %.0f)\n", + ff$i, ff$convLen, ff$imp$len, ff$convLen - ff$imp$len)) + cat(" converged Newick:", ape::write.tree(Preorder(RootTree(ff$conv, ff$d$labels[1]))), "\n") + cat(" improving Newick:", ape::write.tree(Preorder(RootTree(ff$imp$tree, ff$d$labels[1]))), "\n") + saveRDS(ff, "dev/benchmarks/tbr_results/oracle_first_fail.rds") + cat(" (saved first failure -> dev/benchmarks/tbr_results/oracle_first_fail.rds)\n") +} diff --git a/dev/benchmarks/tbr_oracle_iw.R b/dev/benchmarks/tbr_oracle_iw.R new file mode 100644 index 000000000..b51e924c0 --- /dev/null +++ b/dev/benchmarks/tbr_oracle_iw.R @@ -0,0 +1,84 @@ +# tbr_oracle_iw.R -- differential completeness oracle that scores with the +# KERNEL's own scorer (ts_fitch_score, with concavity), so it is valid for IW +# (and EW) without any TreeLength scoring-match. Trajectory-independent: run the +# in-kernel direct unrooted TBR to convergence, then assert no TBR/SPR neighbour +# (TreeTools enumerators, two rootings) scores strictly lower by ts_fitch_score. +# +# Usage: Rscript dev/benchmarks/tbr_oracle_iw.R [nTrees] [nTip] [concavity] +# concavity < 0 => EW; finite > 0 => IW +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-tbr"), + winslash = "/")) + library(TreeTools) +}) +args <- commandArgs(trailingOnly = TRUE) +nTrees <- if (length(args) >= 1) as.integer(args[[1]]) else 60L +nTip <- if (length(args) >= 2) as.integer(args[[2]]) else 12L +conc <- if (length(args) >= 3) as.numeric(args[[3]]) else -1 +unroot <- if (length(args) >= 4) as.logical(as.integer(args[[4]])) else TRUE +nChar <- 60L; nState <- 3L +eps <- 1e-6 + +randomData <- function(seed) { + set.seed(seed) + tips <- paste0("t", seq_len(nTip)) + m <- matrix(sample(0:(nState - 1L), nTip * nChar, replace = TRUE), + nrow = nTip, dimnames = list(tips, NULL)) + phy <- phangorn::phyDat(m, type = "USER", levels = as.character(0:(nState - 1L))) + at <- attributes(phy) + list(phy = phy, contrast = at$contrast, + tip_data = matrix(unlist(phy, use.names = FALSE), nrow = length(phy), byrow = TRUE), + weight = at$weight, levels = at$levels, nTip = length(phy), labels = names(phy)) +} + +# Kernel-matched score of any tree (EW or IW, by concavity). +scoreTree <- function(tree, d) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + TreeSearch:::ts_fitch_score(edge, d$contrast, d$tip_data, d$weight, d$levels, + concavity = conc) +} + +kernelTbr <- function(tree, d) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + res <- TreeSearch:::ts_tbr_diagnostics( + edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L, + concavity = conc, unrooted = unroot) + structure(list(edge = res$edge, Nnode = d$nTip - 1L, tip.label = d$labels), + class = "phylo") +} + +# Best improving neighbour by ts_fitch_score; all_tbr/all_spr at two rootings +# cover every break edge. Returns improving score, or NULL if clean. +bestImproving <- function(tree, d) { + base <- scoreTree(tree, d) + cand <- list() + for (rt in d$labels[1:2]) { + rr <- Preorder(RootTree(tree, rt)) + cand <- c(cand, TBRMoves(rr), SPRMoves(rr)) + } + if (!length(cand)) return(NULL) + ls <- vapply(cand, scoreTree, double(1), d = d) + if (min(ls) < base - eps) list(len = min(ls), base = base) else NULL +} + +cat(sprintf("=== IW/EW differential oracle (ts_fitch_score): %d trees, %d tips, concavity=%s (%s) ===\n", + nTrees, nTip, conc, if (conc < 0) "EW" else "IW")) +fails <- 0L; firstFail <- NULL +for (i in seq_len(nTrees)) { + d <- randomData(1000L + i) + set.seed(7000L + i); start <- RandomTree(d$phy, root = TRUE) + set.seed(i) + conv <- kernelTbr(start, d) + imp <- bestImproving(conv, d) + if (!is.null(imp)) { + fails <- fails + 1L + if (is.null(firstFail)) firstFail <- list(i = i, conv = scoreTree(conv, d), imp = imp) + } +} +cat(sprintf("\nRESULT: %d / %d converged trees had an improving neighbour (FAILURES)\n", fails, nTrees)) +if (fails == 0L) cat("=> DIRECT unrooted TBR is COMPLETE on this sample (kernel-scored).\n") else { + ff <- firstFail + cat(sprintf("=> INCOMPLETE. First: tree #%d, converged %.4f, neighbour %.4f (miss %.4f)\n", + ff$i, ff$conv, ff$imp$len, ff$conv - ff$imp$len)) +} diff --git a/dev/benchmarks/tbr_oracle_na.R b/dev/benchmarks/tbr_oracle_na.R new file mode 100644 index 000000000..41f92c77b --- /dev/null +++ b/dev/benchmarks/tbr_oracle_na.R @@ -0,0 +1,55 @@ +# tbr_oracle_na.R -- GROUND-TRUTH completeness oracle on a REAL (inapplicable) +# dataset, scored by the kernel's own ts_fitch_score (NA 3-pass, with concavity). +# For each of several random starts, run the kernel TBR to convergence (direct +# OR physical), then assert no TBR/SPR neighbour (TreeTools enumerators, two +# rootings) scores strictly lower. This does NOT assume physical is complete -- +# it is the independent oracle that decides whether EITHER path reaches true +# unrooted-TBR optima for NA. +# +# Usage: Rscript dev/benchmarks/tbr_oracle_na.R [dataset] [concavity] [phys 0/1] [nStart] +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-tbr"), winslash = "/")) + library(TreeTools) +}) +a <- commandArgs(trailingOnly = TRUE) +dsn <- if (length(a) >= 1) a[[1]] else "Aria2015" +conc <- if (length(a) >= 2) as.numeric(a[[2]]) else -1 +phys <- if (length(a) >= 3) as.logical(as.integer(a[[3]])) else FALSE +nStart <- if (length(a) >= 4) as.integer(a[[4]]) else 12L +eps <- 1e-6 +data("inapplicable.phyData", package = "TreeSearch") +dataset <- inapplicable.phyData[[dsn]] +at <- attributes(dataset) +d <- list(contrast = at$contrast, + tip_data = matrix(unlist(dataset, use.names = FALSE), nrow = length(dataset), byrow = TRUE), + weight = at$weight, levels = at$levels, labels = names(dataset)) +scoreTree <- function(tree) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + TreeSearch:::ts_fitch_score(edge, d$contrast, d$tip_data, d$weight, d$levels, concavity = conc) +} +kernelTbr <- function(tree) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + if (phys) Sys.setenv(TS_PHYS_REROOT = "1") else Sys.unsetenv("TS_PHYS_REROOT") + res <- TreeSearch:::ts_tbr_diagnostics(edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L, concavity = conc, unrooted = TRUE) + Sys.unsetenv("TS_PHYS_REROOT") + structure(list(edge = res$edge, Nnode = length(d$labels) - 1L, tip.label = d$labels), class = "phylo") +} +bestImproving <- function(tree) { + base <- scoreTree(tree); cand <- list() + for (rt in d$labels[1:2]) { rr <- Preorder(RootTree(tree, rt)); cand <- c(cand, TBRMoves(rr), SPRMoves(rr)) } + ls <- vapply(cand, scoreTree, double(1)) + if (min(ls) < base - eps) list(len = min(ls), base = base) else NULL +} +cat(sprintf("=== NA oracle: %s conc=%s path=%s, %d starts ===\n", dsn, conc, if (phys) "PHYSICAL" else "DIRECT", nStart)) +fails <- 0L; ff <- NULL +for (i in seq_len(nStart)) { + set.seed(7000L + i); start <- RandomTree(dataset, root = TRUE) + set.seed(i); conv <- kernelTbr(start) + imp <- bestImproving(conv) + if (!is.null(imp)) { fails <- fails + 1L; if (is.null(ff)) ff <- list(i = i, conv = scoreTree(conv), imp = imp) } +} +cat(sprintf("RESULT: %d / %d converged trees had an improving neighbour (FAILURES)\n", fails, nStart)) +if (fails == 0L) cat("=> COMPLETE on this sample (kernel-scored).\n") else + cat(sprintf("=> INCOMPLETE. First: start #%d, converged %.4f, neighbour %.4f (miss %.4f)\n", + ff$i, ff$conv, ff$imp$len, ff$conv - ff$imp$len)) diff --git a/dev/benchmarks/tbr_oracle_na_small.R b/dev/benchmarks/tbr_oracle_na_small.R new file mode 100644 index 000000000..5d5c79cb0 --- /dev/null +++ b/dev/benchmarks/tbr_oracle_na_small.R @@ -0,0 +1,79 @@ +# Fast small-tree NA completeness oracle. Random synthetic data WITH +# inapplicables (so the kernel takes the has_na convergence path), small trees so +# the brute-force bestImproving() is cheap -> many starts in seconds. For each +# start: run the in-kernel TBR (ts_tbr_diagnostics) to convergence, then assert +# TreeTools' own TBR+SPR enumerators (two rootings) find no strictly-improving +# neighbour under the kernel's own NA scorer. Complements tbr_oracle_na.R (real +# 74/88-tip data, slow) with a fast, high-N regression signal for the root-edge +# completeness fix. +# +# Usage: Rscript dev/benchmarks/tbr_oracle_na_small.R [nStart=100] [nTip=12] [nChar=40] +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-tbr"), + winslash = "/")) + library(TreeTools) +}) +a <- commandArgs(trailingOnly = TRUE) +nStart <- if (length(a) >= 1) as.integer(a[[1]]) else 100L +nTip <- if (length(a) >= 2) as.integer(a[[2]]) else 12L +nChar <- if (length(a) >= 3) as.integer(a[[3]]) else 40L +eps <- 1e-6 + +# Random data over states {-,0,1,2}; "-" (~25%) is inapplicable -> has_na path. +randomNaData <- function(seed) { + set.seed(seed) + tips <- paste0("t", seq_len(nTip)) + toks <- c("-", "0", "1", "2") + m <- matrix(sample(toks, nTip * nChar, replace = TRUE, prob = c(.25, .25, .25, .25)), + nrow = nTip, dimnames = list(tips, NULL)) + # Guard: drop all-inapplicable / constant columns MatrixToPhyDat may choke on. + keep <- apply(m, 2, function(col) length(unique(col[col != "-"])) >= 1) + m <- m[, keep, drop = FALSE] + MatrixToPhyDat(m) +} + +scoreTree <- function(tree, d) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + TreeSearch:::ts_fitch_score(edge, d$contrast, d$tip_data, d$weight, d$levels, + concavity = -1) +} +kernelTbr <- function(tree, d) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + res <- TreeSearch:::ts_tbr_diagnostics(edge, d$contrast, d$tip_data, d$weight, + d$levels, maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L, + concavity = -1, unrooted = TRUE) + structure(list(edge = res$edge, Nnode = length(d$labels) - 1L, + tip.label = d$labels), class = "phylo") +} +bestImproving <- function(tree, d) { + base <- scoreTree(tree, d); cand <- list() + for (rt in d$labels[1:2]) { + rr <- Preorder(RootTree(tree, rt)); cand <- c(cand, TBRMoves(rr), SPRMoves(rr)) + } + ls <- vapply(cand, scoreTree, double(1), d = d) + if (min(ls) < base - eps) list(len = min(ls), base = base) else NULL +} + +cat(sprintf("=== small NA oracle: %d starts, %d tips, %d chars ===\n", + nStart, nTip, nChar)) +fails <- 0L; ff <- NULL +for (i in seq_len(nStart)) { + dataset <- randomNaData(20000L + i) + at <- attributes(dataset) + d <- list(contrast = at$contrast, + tip_data = matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE), + weight = at$weight, levels = at$levels, labels = names(dataset)) + set.seed(7000L + i); start <- RandomTree(dataset, root = TRUE) + set.seed(i); conv <- kernelTbr(start, d) + imp <- bestImproving(conv, d) + if (!is.null(imp)) { + fails <- fails + 1L + if (is.null(ff)) ff <- list(i = i, conv = scoreTree(conv, d), imp = imp) + } +} +cat(sprintf("RESULT: %d / %d converged trees had an improving neighbour\n", + fails, nStart)) +if (fails == 0L) cat("=> COMPLETE on this sample.\n") else + cat(sprintf("=> INCOMPLETE. First: start #%d, converged %.4f, neighbour %.4f (miss %.4f)\n", + ff$i, ff$conv, ff$imp$len, ff$conv - ff$imp$len)) diff --git a/dev/benchmarks/tbr_pilot.R b/dev/benchmarks/tbr_pilot.R new file mode 100644 index 000000000..8aedb7a91 --- /dev/null +++ b/dev/benchmarks/tbr_pilot.R @@ -0,0 +1,41 @@ +# tbr_pilot.R -- pre-flight gates #1 (length identity) + #2 (seed sensitivity, +# first/best-improving characterization) on ONE Zanol start tree, both modes. +source("dev/benchmarks/tbr_shared_start_lib.R") + +d <- prepareDataset("Zanol2014") + +# A deliberately POOR start so TBR has room to climb. +set.seed(11) +wag <- TreeSearch:::ts_random_wagner_tree(d$contrast, d$tip_data, d$weight, d$levels) +wagTree <- structure(list(edge = wag$edge, Nnode = d$nTip - 1L, + tip.label = names(d$phy)), class = "phylo") +wagTree <- Preorder(RenumberTips(wagTree, names(d$phy))) +cat("Start TreeLength(Wagner seed11) =", TreeLength(wagTree, d$phy), "\n\n") + +seeds <- c(1, 2, 3) + +cat("=== MODE A: strict descent (TS acceptEqual=F ; TNT nomulpars hold 1) ===\n") +rows <- list() +for (s in seeds) { + rows[[length(rows)+1]] <- TntTbr(d, wagTree, seed = s, mulpars = FALSE, hold = 1) + rows[[length(rows)+1]] <- TsTbr(d, wagTree, seed = s, acceptEqual = FALSE)$row +} +modeA <- do.call(rbind, rows) +print(modeA, row.names = FALSE) + +cat("\n=== MODE B: buffer/plateau (TS acceptEqual=T ; TNT mulpars hold 1000) ===\n") +rows <- list() +for (s in seeds) { + rows[[length(rows)+1]] <- TntTbr(d, wagTree, seed = s, mulpars = TRUE, hold = 1000) + rows[[length(rows)+1]] <- TsTbr(d, wagTree, seed = s, acceptEqual = TRUE, + maxHits = 5L)$row +} +modeB <- do.call(rbind, rows) +print(modeB, row.names = FALSE) + +cat("\n--- GATE CHECKS ---\n") +cat("TNT start_len (R) vs start_len (TNT stdout):\n") +print(unique(modeA[, c("start_len", "start_len_tnt")])) +cat("TS final_len (R) vs final_len (kernel res$score) should match exactly:\n") +tsRows <- rbind(modeA[modeA$engine=="TS",], modeB[modeB$engine=="TS",]) +print(tsRows[, c("seed","final_len","final_len_tnt")]) diff --git a/dev/benchmarks/tbr_reroot_crossfeed.R b/dev/benchmarks/tbr_reroot_crossfeed.R new file mode 100644 index 000000000..61bea6d40 --- /dev/null +++ b/dev/benchmarks/tbr_reroot_crossfeed.R @@ -0,0 +1,88 @@ +# tbr_reroot_crossfeed.R -- THE gating experiment (advisor, 2026-06-18). +# +# tbr_crossfeed.R only ever fed TNT the ROOTED 1302 optimum, which just +# re-proves root-dependence. The decisive, never-run test is the +# REROOT-INVARIANT cross-feed: +# +# (1) Take the all-tips reroot-invariant TS optimum (~1284 -- which SHOULD +# be a complete unrooted-TBR local optimum, since every edge's large +# side holds some tip so all-tip rooting overcomes the L812 skip). +# Feed it into TNT bbreak. +# TNT IMPROVES it => residual is NEIGHBOURHOOD: TNT reaches moves our +# "complete" unrooted TBR doesn't (emulation not +# actually complete, or bbreak does more than +# single-tree TBR). Fix THAT before building. +# TNT HOLDS it => residual is BASIN/PATH: 1284 and 1264 are both +# valid unrooted optima; TNT just navigates better. +# The lever is clip-ordering / restarts, NOT the +# move set. Build still banks the root-dependence +# half, but won't reach TNT. +# (2) Reciprocal: feed TNT's own ~1264 optimum into RootInvariantTbr. +# Holds => confirms 1264 is a unrooted-TBR optimum too (basin/path). +source("dev/benchmarks/tbr_shared_start_lib.R") + +d <- prepareDataset("Zanol2014") +norm <- function(tr) Preorder(RenumberTips(tr, names(d$phy))) +asPhylo <- function(edge) structure(list(edge = edge, Nnode = d$nTip - 1L, + tip.label = names(d$phy)), class = "phylo") + +# All-tips reroot-invariant TBR to convergence. Returns BOTH length and tree. +# (tbr_reroot_recovery.R's version returned only the length.) +RootInvariantTbr <- function(startTree, seed, acceptEqual = FALSE, maxHits = 1L, + rerootTips = names(d$phy)) { + cur <- TsTbr(d, startTree, seed = seed, acceptEqual = acceptEqual, maxHits = maxHits) + best <- cur$tree; bestLen <- cur$row$final_len + repeat { + improved <- FALSE + for (tp in rerootTips) { + rr <- norm(ape::root(best, outgroup = tp, resolve.root = TRUE)) + r <- TsTbr(d, rr, seed = seed, acceptEqual = acceptEqual, maxHits = maxHits) + if (r$row$final_len < bestLen) { + bestLen <- r$row$final_len; best <- r$tree; improved <- TRUE + } + } + if (!improved) break + } + list(len = bestLen, tree = norm(best)) +} + +starts <- list( + wagner = { set.seed(2001) + w <- TreeSearch:::ts_random_wagner_tree(d$contrast, d$tip_data, + d$weight, d$levels) + norm(asPhylo(w$edge)) }, + random = norm({ set.seed(1001); RandomTree(d$phy, root = TRUE) })) + +cat("=== Reroot-invariant cross-feed (Zanol2014, TNT target ~1261) ===\n\n") +for (sn in names(starts)) { + st <- starts[[sn]] + for (s in 1:2) { + cat(sprintf("[%s seed=%d] start_len=%.0f\n", sn, s, TreeLength(st, d$phy))) + + # (1) TS reroot-invariant optimum, then feed into TNT. + ri <- RootInvariantTbr(st, seed = s, acceptEqual = FALSE) + tnt_nomp <- TntTbr(d, ri$tree, seed = s, mulpars = FALSE, hold = 1)$final_len + tnt_mp <- TntTbr(d, ri$tree, seed = s, mulpars = TRUE, hold = 1000)$final_len + cat(sprintf(" TS reroot-invariant opt = %.0f\n", ri$len)) + cat(sprintf(" -> TNT bbreak nomulpars : %.0f -> %.0f (%s)\n", + ri$len, tnt_nomp, + if (tnt_nomp < ri$len - 0.5) "NEIGHBOURHOOD: TNT improves it" + else "holds")) + cat(sprintf(" -> TNT bbreak mulpars1000: %.0f -> %.0f\n", ri$len, tnt_mp)) + + # (2) TNT's own optimum from the same start, fed back into reroot-invariant TS. + tntRow <- TntTbr(d, st, seed = s, mulpars = FALSE, hold = 1) + tntOpt <- attr(tntRow, "tree") + if (!is.null(tntOpt)) { + ri2 <- RootInvariantTbr(norm(tntOpt), seed = s, acceptEqual = FALSE) + cat(sprintf(" TNT own opt = %.0f -> reroot-invariant TS : %.0f -> %.0f (%s)\n", + tntRow$final_len, tntRow$final_len, ri2$len, + if (ri2$len < tntRow$final_len - 0.5) "TS improves TNT" + else "TS HOLDS TNT (1264-class is a unrooted-TBR optimum)")) + } + cat("\n") + } +} +cat("Reading: TNT improving the reroot-invariant opt => neighbourhood gap (fix first).\n", + "TNT holding it while its own search still reaches lower => basin/path (clip order).\n", + sep = "") diff --git a/dev/benchmarks/tbr_reroot_recovery.R b/dev/benchmarks/tbr_reroot_recovery.R new file mode 100644 index 000000000..c949c8d50 --- /dev/null +++ b/dev/benchmarks/tbr_reroot_recovery.R @@ -0,0 +1,57 @@ +# tbr_reroot_recovery.R -- Step-2 confirmation + calibration. +# +# If TS's deficit is a root-dependent (rooted) TBR neighbourhood, then emulating +# root-invariance with the EXISTING rooted kernel (TBR -> reroot-sweep -> TBR, +# looped to convergence over ALL tips) should recover a large part of the gap to +# TNT. It does -- ~half -- but STALLS +15-36 above TNT, and plateau-crossing +# does not close the residual. So root-dependence is a proven MAJOR contributor, +# not (yet) shown to be the whole cause. See dev/plans/2026-06-18-tbr-shared-start.md. +source("dev/benchmarks/tbr_shared_start_lib.R") + +d <- prepareDataset("Zanol2014") +norm <- function(tr) Preorder(RenumberTips(tr, names(d$phy))) +asPhylo <- function(edge) structure(list(edge = edge, Nnode = d$nTip - 1L, + tip.label = names(d$phy)), class = "phylo") + +# Root-invariant TBR emulation: run rooted TBR; then try re-rooting at each tip +# in `rerootTips` and re-running; adopt any rooting that improves; repeat until a +# full reroot sweep yields no improvement. Uses only the shipping rooted kernel. +RootInvariantTbr <- function(startTree, seed, acceptEqual = FALSE, maxHits = 1L, + rerootTips = names(d$phy)) { + cur <- TsTbr(d, startTree, seed = seed, acceptEqual = acceptEqual, maxHits = maxHits) + best <- cur$tree; bestLen <- cur$row$final_len + repeat { + improved <- FALSE + for (tp in rerootTips) { + rr <- norm(ape::root(best, outgroup = tp, resolve.root = TRUE)) + r <- TsTbr(d, rr, seed = seed, acceptEqual = acceptEqual, maxHits = maxHits) + if (r$row$final_len < bestLen) { + bestLen <- r$row$final_len; best <- r$tree; improved <- TRUE + } + } + if (!improved) break + } + bestLen +} + +set.seed(2001) +w <- TreeSearch:::ts_random_wagner_tree(d$contrast, d$tip_data, d$weight, d$levels) +starts <- list(wagner = norm(asPhylo(w$edge)), + random = norm({set.seed(1001); RandomTree(d$phy, root = TRUE)})) + +cat("Full 74-tip reroot-invariant TBR to convergence (Zanol):\n") +cat(sprintf("%-7s %-5s %-10s %-12s %-12s %-6s\n", + "start", "seed", "TS_rooted", "reroot_strict", "reroot_plateau", "TNT")) +for (sn in names(starts)) { + st <- starts[[sn]] + for (s in 1:2) { + rooted <- TsTbr(d, st, seed = s, acceptEqual = FALSE)$row$final_len + riStr <- RootInvariantTbr(st, seed = s, acceptEqual = FALSE) + riPlat <- RootInvariantTbr(st, seed = s, acceptEqual = TRUE, maxHits = 50L) + tnt <- TntTbr(d, st, seed = s, mulpars = FALSE, hold = 1)$final_len + cat(sprintf("%-7s %-5d %-10.0f %-12.0f %-12.0f %-6.0f\n", + sn, s, rooted, riStr, riPlat, tnt)) + } +} +cat("\nReading: reroot-invariance recovers ~half the gap but stalls +15-36 above\n", + "TNT; plateau-crossing does not close the residual.\n", sep = "") diff --git a/dev/benchmarks/tbr_shared_start_lib.R b/dev/benchmarks/tbr_shared_start_lib.R new file mode 100644 index 000000000..91ca0234c --- /dev/null +++ b/dev/benchmarks/tbr_shared_start_lib.R @@ -0,0 +1,154 @@ +# tbr_shared_start_lib.R +# +# Shared helpers for the isolated-TBR head-to-head between TreeSearch and +# TNT 1.6 from IDENTICAL starting trees. Loaded by the pilot and the full +# grid driver. See dev/plans/2026-06-18-tbr-shared-start.md for the design. +# +# Both engines optimise the SAME Fitch objective because the matrices have +# inapplicable tokens replaced by '?'. Lengths are therefore directly +# comparable (TreeLength vs TNT `length`). + +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-tbr"), + winslash = "/")) + library(TreeTools) +}) + +TNT_EXE <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +T0_DIR <- Sys.getenv("T0_DIR", "C:/Users/pjjg18/GitHub/TreeSearch/dev/benchmarks/t0") + +# --------------------------------------------------------------------------- +# Dataset preparation: phyDat -> the array bundle ts_tbr_diagnostics wants. +# --------------------------------------------------------------------------- +prepareDataset <- function(name) { + phy <- readRDS(file.path(T0_DIR, paste0(name, ".phy.rds"))) + at <- attributes(phy) + list( + name = name, + phy = phy, + contrast = at$contrast, + tip_data = matrix(unlist(phy, use.names = FALSE), + nrow = length(phy), byrow = TRUE), + weight = at$weight, + levels = at$levels, + nTip = length(phy) + ) +} + +# --------------------------------------------------------------------------- +# TNT helpers +# --------------------------------------------------------------------------- + +# ape Newick -> TNT parenthetical (space-separated, no branch lengths, +# no trailing semicolon). +ToTntTree <- function(tr) { + nw <- ape::write.tree(tr) + nw <- gsub(";", "", nw, fixed = TRUE) # drop trailing ';' + nw <- gsub(",", " ", nw, fixed = TRUE) # commas -> spaces + nw +} + +# Run a TNT script (character vector of lines) in a fresh temp dir that +# already contains a data.tnt for `phy`. Returns sanitised stdout lines. +# `files` is a named list of extra files to write into the working dir +# (name = filename, value = character vector of lines). +RunTnt <- function(phy, scriptLines, tag = "tnt", files = list()) { + wd <- file.path(tempdir(), paste0(tag, Sys.getpid())) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + for (nm in names(files)) writeLines(files[[nm]], file.path(wd, nm)) + writeLines(scriptLines, file.path(wd, "swapper.run")) + old <- setwd(wd) + on.exit(setwd(old), add = TRUE) + out <- suppressWarnings(system2(TNT_EXE, args = "swapper.run;", + stdout = TRUE, stderr = TRUE)) + iconv(out, from = "", to = "UTF-8", sub = "") +} + +# Pull a single number from the first line matching `pat` (with one capture +# group), stripping thousands separators. +GrepNum <- function(out, pat) { + hit <- grep(pat, out, value = TRUE) + if (!length(hit)) return(NA_real_) + suppressWarnings(as.numeric(gsub(",", "", sub(pat, "\\1", hit[1])))) +} + +# Run TNT TBR (bbreak) from `startTree`, save result, read it back, score in +# R with TreeLength. Returns a one-row data.frame. +# mulpars/hold : equal-tree buffer controls (Mode A: FALSE/1; Mode B: TRUE/1000) +# randclip : randomise clip order using rseed (the stochasticity knob) +TntTbr <- function(d, startTree, seed, mulpars, hold, randclip = TRUE) { + swap <- paste0("bbreak = tbr ", + if (randclip) "randclip " else "norandclip ", + if (mulpars) "mulpars" else "nomulpars", ";") + script <- c("mxram 1024;", "taxname=;", "proc data.tnt;", + paste0("rseed ", seed, ";"), + paste0("hold ", hold, ";"), + paste0("tread ", ToTntTree(startTree), ";"), + swap, + "tsave *out.tre;", "save;", "tsave/;", + "quit;") + wd <- file.path(tempdir(), paste0("tnttbr", Sys.getpid())) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(d$phy, file.path(wd, "data.tnt")) + writeLines(script, file.path(wd, "swapper.run")) + old <- setwd(wd); on.exit(setwd(old), add = TRUE) + out <- suppressWarnings(system2(TNT_EXE, args = "swapper.run;", + stdout = TRUE, stderr = TRUE)) + out <- iconv(out, from = "", to = "UTF-8", sub = "") + + startScore <- GrepNum(out, ".*Start swapping from .* \\(score ([0-9]+)\\).*") + bestStdout <- GrepNum(out, ".*Best score \\(TBR\\):\\s*([0-9]+).*") + rearr <- GrepNum(out, ".*Total rearrangements examined:\\s*([0-9,]+).*") + # Authoritative final score: read saved tree(s), score in R (identical engine) + trees <- tryCatch(ReadTntTree(file.path(wd, "out.tre")), error = function(e) NULL) + finalR <- if (is.null(trees)) NA_real_ else { + if (inherits(trees, "multiPhylo")) + min(vapply(trees, TreeLength, double(1), d$phy)) else TreeLength(trees, d$phy) + } + nTrees <- if (is.null(trees)) NA_integer_ else + if (inherits(trees, "multiPhylo")) length(trees) else 1L + bestTree <- if (is.null(trees)) NULL else if (inherits(trees, "multiPhylo")) { + trees[[which.min(vapply(trees, TreeLength, double(1), d$phy))]] + } else trees + row <- data.frame(engine = "TNT", seed = seed, mulpars = mulpars, hold = hold, + start_len = TreeLength(startTree, d$phy), + start_len_tnt = startScore, final_len = finalR, + final_len_tnt = bestStdout, n_trees = nTrees, + rearrangements = rearr, stringsAsFactors = FALSE) + attr(row, "tree") <- bestTree + row +} + +# --------------------------------------------------------------------------- +# TreeSearch helpers +# --------------------------------------------------------------------------- + +# phylo -> kernel edge matrix (standard ape numbering, tips matching d$phy). +PhyloToKernelEdge <- function(tree, d) { + tree <- RenumberTips(tree, names(d$phy)) + tree <- Preorder(tree) + tree[["edge"]] +} + +# Run TreeSearch TBR to convergence from `startTree`. acceptEqual=FALSE is +# strict descent (Mode A); acceptEqual=TRUE plateau-walks the single tree +# (Mode B analogue). Returns a one-row data.frame plus the pass trajectory. +TsTbr <- function(d, startTree, seed, acceptEqual, maxHits = 1L, maxChanges = 0L) { + edge <- PhyloToKernelEdge(startTree, d) + set.seed(seed) + res <- TreeSearch:::ts_tbr_diagnostics( + edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = maxHits, acceptEqual = acceptEqual, maxChanges = maxChanges) + resTree <- structure(list(edge = res$edge, Nnode = d$nTip - 1L, + tip.label = names(d$phy)), class = "phylo") + finalR <- TreeLength(resTree, d$phy) + row <- data.frame(engine = "TS", seed = seed, mulpars = NA, hold = NA, + start_len = TreeLength(startTree, d$phy), + start_len_tnt = NA_real_, final_len = finalR, + final_len_tnt = res$score, n_trees = 1L, + rearrangements = res$n_evaluated, stringsAsFactors = FALSE) + attr(row, "tree") <- resTree + list(row = row, tree = resTree, + passes = res$passes, n_accepted = res$n_accepted, converged = res$converged) +} diff --git a/dev/benchmarks/tbr_unrooted_scorecmp.R b/dev/benchmarks/tbr_unrooted_scorecmp.R new file mode 100644 index 000000000..74e1bb8b2 --- /dev/null +++ b/dev/benchmarks/tbr_unrooted_scorecmp.R @@ -0,0 +1,62 @@ +# tbr_unrooted_scorecmp.R -- validate the DIRECT unrooted root-edge path against +# the PHYSICAL-REROOT reference (TS_PHYS_REROOT=1), by comparing the kernel's own +# converged score (result.best_score) on identical starts. Physical reroot is +# complete by construction (tries all rootings, exact per-scorer scoring), so +# direct == phys on every tree => the direct path is equally complete. Uses the +# kernel-native score, so NO TreeLength scoring-match is required (avoids the +# apples-to-oranges trap for IW/NA). +# +# Usage: Rscript dev/benchmarks/tbr_unrooted_scorecmp.R [nTrees] [nTip] [concavity] +# concavity < 0 => equal weights (EW); finite >0 => implied weights (IW) +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-tbr"), + winslash = "/")) + library(TreeTools) +}) +args <- commandArgs(trailingOnly = TRUE) +nTrees <- if (length(args) >= 1) as.integer(args[[1]]) else 60L +nTip <- if (length(args) >= 2) as.integer(args[[2]]) else 12L +conc <- if (length(args) >= 3) as.numeric(args[[3]]) else -1 +nChar <- 60L; nState <- 3L + +randomData <- function(seed) { + set.seed(seed) + tips <- paste0("t", seq_len(nTip)) + m <- matrix(sample(0:(nState - 1L), nTip * nChar, replace = TRUE), + nrow = nTip, dimnames = list(tips, NULL)) + phy <- phangorn::phyDat(m, type = "USER", levels = as.character(0:(nState - 1L))) + at <- attributes(phy) + list(phy = phy, contrast = at$contrast, + tip_data = matrix(unlist(phy, use.names = FALSE), nrow = length(phy), byrow = TRUE), + weight = at$weight, levels = at$levels, nTip = length(phy), labels = names(phy)) +} + +runK <- function(tree, d, seed, phys) { + edge <- Preorder(RenumberTips(tree, d$labels))[["edge"]] + if (phys) Sys.setenv(TS_PHYS_REROOT = "1") else Sys.unsetenv("TS_PHYS_REROOT") + set.seed(seed) + res <- TreeSearch:::ts_tbr_diagnostics( + edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L, + concavity = conc, unrooted = TRUE) + Sys.unsetenv("TS_PHYS_REROOT") + res$score +} + +cat(sprintf("=== direct vs physical-reroot score-cmp: %d trees, %d tips, concavity=%s (%s) ===\n", + nTrees, nTip, conc, if (conc < 0) "EW" else "IW")) +mism <- 0L; worseDirect <- 0L +for (i in seq_len(nTrees)) { + d <- randomData(1000L + i) + set.seed(7000L + i); start <- RandomTree(d$phy, root = TRUE) + sD <- runK(start, d, i, FALSE) + sP <- runK(start, d, i, TRUE) + if (abs(sD - sP) > 1e-6) { + mism <- mism + 1L + if (sD > sP + 1e-6) worseDirect <- worseDirect + 1L + cat(sprintf(" tree %d: direct=%.4f phys=%.4f (%s)\n", i, sD, sP, + if (sD > sP) "DIRECT WORSE (incomplete)" else "direct better")) + } +} +cat(sprintf("\nMISMATCHES: %d / %d (direct strictly worse: %d)\n", mism, nTrees, worseDirect)) +if (mism == 0L) cat("=> direct path reaches the SAME optimum as physical reroot on all trees.\n") diff --git a/dev/benchmarks/tbr_unrooted_validate.R b/dev/benchmarks/tbr_unrooted_validate.R new file mode 100644 index 000000000..abd062d84 --- /dev/null +++ b/dev/benchmarks/tbr_unrooted_validate.R @@ -0,0 +1,69 @@ +# tbr_unrooted_validate.R -- quality + PERF of the in-kernel unrooted TBR +# (TBRParams::unrooted, reroot-at-convergence) on real data (Zanol2014). +# +# Correctness (0 canonical-improving) is proven separately by the small-tree +# oracle (tbr_oracle.R, unrooted=1 emul=0). This script measures, per start: +# - final length rooted (default) vs unrooted (in-kernel reroot) +# - wall-clock time rooted vs unrooted => the per-tbr_search perf cost +# from both POOR (random) and GOOD (RAS-Wagner) starts. Context: TNT reaches +# ~1262-1264; closing to TRUE unrooted-TBR optima (this fix) is expected to land +# ~1265-1272 -- the residual to TNT is basin/escape, a SEPARATE mechanism. +source("dev/benchmarks/tbr_shared_start_lib.R") +d <- prepareDataset("Zanol2014") +norm <- function(tr) Preorder(RenumberTips(tr, names(d$phy))) +asPhylo <- function(edge) structure(list(edge = edge, Nnode = d$nTip - 1L, + tip.label = names(d$phy)), class = "phylo") + +runKernel <- function(tree, seed, unrooted) { + edge <- PhyloToKernelEdge(tree, d) + set.seed(seed) + t <- system.time( + res <- TreeSearch:::ts_tbr_diagnostics( + edge, d$contrast, d$tip_data, d$weight, d$levels, + maxHits = 1L, acceptEqual = FALSE, maxChanges = 0L, unrooted = unrooted)) + tr <- norm(asPhylo(res$edge)) + list(tree = tr, len = TreeLength(tr, d$phy), sec = as.double(t["elapsed"])) +} + +# Is `tree` canonical-unrooted-TBR clean? all_tbr at two rootings (covers all +# break edges). Expensive (~2x100k neighbours); call sparingly. +isClean <- function(tree) { + base <- TreeLength(tree, d$phy) + best <- base + for (rt in names(d$phy)[1:2]) { + nb <- TBRMoves(norm(RootTree(tree, rt))) + best <- min(best, min(vapply(nb, TreeLength, double(1), d$phy))) + } + best >= base - 0.5 +} + +mkStart <- function(kind, seed) { + if (kind == "random") norm({ set.seed(1000 + seed); RandomTree(d$phy, root = TRUE) }) + else { set.seed(2000 + seed) + w <- TreeSearch:::ts_random_wagner_tree(d$contrast, d$tip_data, d$weight, d$levels) + norm(asPhylo(w$edge)) } +} + +cat("=== In-kernel unrooted TBR: quality + perf (Zanol2014, n=74; TNT ~1262-1264) ===\n") +cat(sprintf("%-7s %-4s %-7s | %-8s %-7s | %-8s %-7s %-6s | %-5s\n", + "start","seed","startL","rootedL","sec","unrootL","sec","clean","x")) +rows <- list() +for (kind in c("random","wagner")) for (s in 1:3) { + st <- mkStart(kind, s); sl <- TreeLength(st, d$phy) + r <- runKernel(st, s, FALSE) + u <- runKernel(st, s, TRUE) + # Cleanliness on full 74-tip is ~330s/call; correctness is proven broadly by + # the small-tree oracle, so confirm on real 74-tip data for ONE start only. + clean <- if (kind == "random" && s == 1) isClean(u$tree) else NA + cat(sprintf("%-7s %-4d %-7.0f | %-8.0f %-7.2f | %-8.0f %-7.2f %-6s | %-5.1f\n", + kind, s, sl, r$len, r$sec, u$len, u$sec, clean, u$sec / r$sec)) + rows[[length(rows)+1]] <- data.frame(kind, seed=s, startL=sl, + rootedL=r$len, rootedSec=r$sec, unrootL=u$len, unrootSec=u$sec, + clean=clean, ratio=u$sec/r$sec) +} +res <- do.call(rbind, rows) +cat(sprintf("\nMEDIAN: rooted=%.0f unrooted=%.0f (gain %.0f) median time x%.1f\n", + median(res$rootedL), median(res$unrootL), + median(res$rootedL - res$unrootL), median(res$ratio))) +cat(sprintf("unrooted results canonical-TBR-clean: %d/%d\n", sum(res$clean), nrow(res))) +write.csv(res, "dev/benchmarks/tbr_results/tbr_unrooted_validate.csv", row.names = FALSE) diff --git a/dev/benchmarks/tbr_verify.R b/dev/benchmarks/tbr_verify.R new file mode 100644 index 000000000..26e6947fc --- /dev/null +++ b/dev/benchmarks/tbr_verify.R @@ -0,0 +1,38 @@ +# tbr_verify.R -- sanity checks on the striking pilot result. +source("dev/benchmarks/tbr_shared_start_lib.R") +d <- prepareDataset("Zanol2014") +set.seed(11) +wag <- TreeSearch:::ts_random_wagner_tree(d$contrast, d$tip_data, d$weight, d$levels) +wagTree <- Preorder(RenumberTips(structure(list(edge = wag$edge, Nnode = d$nTip-1L, + tip.label = names(d$phy)), class = "phylo"), names(d$phy))) +cat("start =", TreeLength(wagTree, d$phy), "\n\n") + +# (1) Does TS TBR converge, and what does the per-pass trajectory look like? +for (ae in c(FALSE, TRUE)) { + r <- TsTbr(d, wagTree, seed = 2, acceptEqual = ae, maxHits = if (ae) 5L else 1L) + p <- r$passes + cat(sprintf("TS acceptEqual=%-5s final=%.0f converged=%s n_accepted=%d n_passes=%d\n", + ae, r$row$final_len, r$converged, r$n_accepted, nrow(p))) + cat(" productive passes:", sum(p$productive), " null passes:", sum(!p$productive), "\n") +} + +# (2) TNT determinism: norandclip same seed twice (should be identical); +# randclip different seeds (should differ). +cat("\n--- TNT norandclip x2 (determinism) ---\n") +a1 <- TntTbr(d, wagTree, seed=1, mulpars=FALSE, hold=1, randclip=FALSE) +a2 <- TntTbr(d, wagTree, seed=1, mulpars=FALSE, hold=1, randclip=FALSE) +cat("norandclip seed1 run1:", a1$final_len, " run2:", a2$final_len, "\n") +b1 <- TntTbr(d, wagTree, seed=1, mulpars=FALSE, hold=1, randclip=TRUE) +b2 <- TntTbr(d, wagTree, seed=2, mulpars=FALSE, hold=1, randclip=TRUE) +cat("randclip seed1:", b1$final_len, " seed2:", b2$final_len, "\n") + +# (3) Sanity: TNT bbreak from the OPTIMAL T0 (1271) must NOT do RAS (stay <=1271). +cat("\n--- TNT bbreak from T0=1271 (must not re-randomise) ---\n") +t0 <- ape::read.tree(file.path(T0_DIR, "Zanol2014.tre")) +c1 <- TntTbr(d, t0, seed=1, mulpars=FALSE, hold=1, randclip=TRUE) +cat("T0 start:", c1$start_len, " final:", c1$final_len, "\n") + +# (4) TS from T0=1271 strict descent (should stay near 1271). +cat("\n--- TS bbreak from T0=1271 ---\n") +t0r <- TsTbr(d, t0, seed=1, acceptEqual=FALSE) +cat("T0 start: 1271 TS final:", t0r$row$final_len, "\n") diff --git a/dev/benchmarks/test_diverse_starts.R b/dev/benchmarks/test_diverse_starts.R new file mode 100644 index 000000000..b847166df --- /dev/null +++ b/dev/benchmarks/test_diverse_starts.R @@ -0,0 +1,65 @@ +# CHIP FINDING TEST: TNT escapes via sectorial over a DIVERSE SET of equal-optimal +# trees, not single-tree polish. TreeSearch's rss_search is single-tree-per-replicate +# and MaximizeParsimony(tree=multiPhylo) keeps only tree[[1]] -> it CANNOT operate over +# a set. Here we test the weaker "independent lanes" route the chip measured (~1/15 reach +# 1261): run our best single-tree sectorial (large-clade [31,99] coll30, 20 picks x 30 +# rounds, ratchet off) from EACH of TNT's diverse hold-1000 trees, best-of. If lanes from +# diverse starts reach the target where the single canonical T0 stalls at the 1267-class +# plateau, start-diversity is confirmed as the lever on our side too. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband2"), + winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m=="-"] <- "?"; MatrixToPhyDat(m) } +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014")), "\\s+")[[1]] +ROUNDS <- as.integer(Sys.getenv("TS_RSSROUNDS", "30")) +SEEDS <- as.integer(strsplit(Sys.getenv("TS_SEEDS", "1 2 3"), "\\s+")[[1]]) +target <- c(Zanol2014 = 1261, Wortley2006 = 480, Zhu2013 = 624, Giles2015 = 670) + +# Diverse equal-optimal set from TNT hold-1000 mult (the trees TNT runs sectorial over). +diverse_set <- function(phy, wd) { + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + writeLines(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1000;", + "mult=replic 1;", "tsave *set.tre;", "save;", "tsave/;", "quit;"), + file.path(wd, "setbuild.run")) + old <- setwd(wd); on.exit(setwd(old)) + invisible(suppressWarnings(system2(TNT, args = "setbuild.run;", stdout = TRUE, stderr = TRUE))) + ts <- ReadTntTree(file.path(wd, "set.tre")) + if (!inherits(ts, "multiPhylo")) ts <- structure(list(ts), class = "multiPhylo") + ts +} + +lane <- function(phy, t, seed) { + set.seed(seed) + Sys.setenv(TS_RSS_PICKS = "20") + r <- suppressWarnings(MaximizeParsimony(phy, tree = t, maxReplicates = 1L, nThreads = 1L, + maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, driftCycles = 0L, + xssRounds = 0L, cssRounds = 0L, rssRounds = ROUNDS, wagnerStarts = 1L, + fuseInterval = 9999L, sectorMinSize = 31L, sectorMaxSize = 99L, + rasStarts = 3L, sectorCollapseTarget = 30L, sectorAcceptEqual = FALSE)) + Sys.unsetenv("TS_RSS_PICKS") + min(as.double(attr(r, "score"))) +} + +for (nm in dsN) { + phy <- fitch(inapplicable.phyData[[nm]]); tgt <- target[[nm]] + wd <- file.path(tempdir(), paste0("ds", Sys.getpid(), nm)) + unlink(wd, recursive = TRUE); dir.create(wd, recursive = TRUE, showWarnings = FALSE) + ts <- diverse_set(phy, wd) + lens <- vapply(ts, TreeLength, double(1), phy) + nset <- length(ts) + cat(sprintf("\n==== %s | TNT diverse set: %d trees, lengths %.0f-%.0f | target=%d ====\n", + nm, nset, min(lens), max(lens), tgt)) + allsc <- c() + for (i in seq_len(nset)) { + sc <- vapply(SEEDS, function(s) lane(phy, ts[[i]], s), double(1)) + allsc <- c(allsc, sc) + cat(sprintf(" tree %2d (len %.0f): %s\n", i, lens[i], paste(format(sc), collapse = " "))) + } + nhit <- sum(allsc <= tgt + 1e-6) + cat(sprintf(" >>> %d lanes; best %.0f (target %d); reached target: %d/%d lanes\n", + length(allsc), min(allsc), tgt, nhit, length(allsc))) +} diff --git a/dev/benchmarks/timing_hamilton.sh b/dev/benchmarks/timing_hamilton.sh new file mode 100644 index 000000000..835409680 --- /dev/null +++ b/dev/benchmarks/timing_hamilton.sh @@ -0,0 +1,31 @@ +#!/bin/bash +#SBATCH -p shared +#SBATCH -n 1 +#SBATCH --mem=8G +#SBATCH --time=2:00:00 +#SBATCH --output=/nobackup/%u/TreeSearch/logs/timing_%x_%j.out +#SBATCH --error=/nobackup/%u/TreeSearch/logs/timing_%x_%j.err + +# Per-dataset TreeSearch-vs-TNT wall-clock timing (run-only: reuses the +# pre-built lib + staged 64-bit TNT). One dataset per job (TS_DATASET via +# --export) so results land independently. TNT is static -> cache the CSV. +module load r/4.5.1 gcc/14.2 +export OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 +export LD_LIBRARY_PATH=/nobackup/$USER/TreeSearch/tnt/TNT-bin:$LD_LIBRARY_PATH +export TERM=xterm +export TNT_EXE=/nobackup/$USER/TreeSearch/tnt/TNT-bin/tnt + +LIB=/nobackup/$USER/TreeSearch/lib +OUTDIR=/nobackup/$USER/TreeSearch/timing_results +HARNESS=/nobackup/$USER/TreeSearch/scripts/hamilton_timing.R +mkdir -p "$OUTDIR" /nobackup/$USER/TreeSearch/logs + +echo "=== Timing: ${TS_DATASET} | $(date) | node $(hostname) ===" +echo "TreeSearch: $(Rscript -e ".libPaths(c(\"$LIB\",.libPaths())); cat(as.character(packageVersion(\"TreeSearch\")))" 2>/dev/null)" +echo "TNT: $TNT_EXE" + +TS_LIB="$LIB" TS_DATASET="$TS_DATASET" OUTDIR="$OUTDIR" NSEED="${NSEED:-3}" \ + Rscript "$HARNESS" + +echo "Completed: $(date)" +ls -lh "$OUTDIR/timing_${TS_DATASET}.csv" 2>/dev/null diff --git a/dev/benchmarks/timing_results/timing_Giles2015.csv b/dev/benchmarks/timing_results/timing_Giles2015.csv new file mode 100644 index 000000000..957e2f3c9 --- /dev/null +++ b/dev/benchmarks/timing_results/timing_Giles2015.csv @@ -0,0 +1,16 @@ +"dataset","target","engine","config","seed","score","over","wall_s" +"Giles2015",670,"TreeSearch","default",1,670,0,10 +"Giles2015",670,"TreeSearch","default",2,670,0,7.8 +"Giles2015",670,"TreeSearch","default",3,670,0,9.1 +"Giles2015",670,"TreeSearch","thorough",1,670,0,12.9 +"Giles2015",670,"TreeSearch","thorough",2,670,0,17 +"Giles2015",670,"TreeSearch","thorough",3,670,0,16.3 +"Giles2015",670,"TNT","mult-basic",1,670,0,0.8 +"Giles2015",670,"TNT","mult-basic",2,670,0,0.5 +"Giles2015",670,"TNT","mult-basic",3,670,0,0.4 +"Giles2015",670,"TNT","xmult-default",1,670,0,0.2 +"Giles2015",670,"TNT","xmult-default",2,670,0,0.2 +"Giles2015",670,"TNT","xmult-default",3,670,0,0.2 +"Giles2015",670,"TNT","xmult-level10",1,670,0,3.3 +"Giles2015",670,"TNT","xmult-level10",2,670,0,3.1 +"Giles2015",670,"TNT","xmult-level10",3,670,0,3.2 diff --git a/dev/benchmarks/timing_results/timing_Wortley2006.csv b/dev/benchmarks/timing_results/timing_Wortley2006.csv new file mode 100644 index 000000000..703749d92 --- /dev/null +++ b/dev/benchmarks/timing_results/timing_Wortley2006.csv @@ -0,0 +1,16 @@ +"dataset","target","engine","config","seed","score","over","wall_s" +"Wortley2006",480,"TreeSearch","default",1,479,-1,2.4 +"Wortley2006",480,"TreeSearch","default",2,479,-1,2.5 +"Wortley2006",480,"TreeSearch","default",3,479,-1,1.8 +"Wortley2006",480,"TreeSearch","thorough",1,479,-1,3.5 +"Wortley2006",480,"TreeSearch","thorough",2,479,-1,2.7 +"Wortley2006",480,"TreeSearch","thorough",3,479,-1,2.7 +"Wortley2006",480,"TNT","mult-basic",1,479,-1,0.2 +"Wortley2006",480,"TNT","mult-basic",2,479,-1,0.2 +"Wortley2006",480,"TNT","mult-basic",3,479,-1,0.2 +"Wortley2006",480,"TNT","xmult-default",1,482,2,0.1 +"Wortley2006",480,"TNT","xmult-default",2,481,1,0.1 +"Wortley2006",480,"TNT","xmult-default",3,480,0,0.1 +"Wortley2006",480,"TNT","xmult-level10",1,479,-1,1.1 +"Wortley2006",480,"TNT","xmult-level10",2,479,-1,1 +"Wortley2006",480,"TNT","xmult-level10",3,479,-1,1.1 diff --git a/dev/benchmarks/timing_results/timing_Zanol2014.csv b/dev/benchmarks/timing_results/timing_Zanol2014.csv new file mode 100644 index 000000000..c5d4d5159 --- /dev/null +++ b/dev/benchmarks/timing_results/timing_Zanol2014.csv @@ -0,0 +1,16 @@ +"dataset","target","engine","config","seed","score","over","wall_s" +"Zanol2014",1261,"TreeSearch","default",1,1261,0,68.5 +"Zanol2014",1261,"TreeSearch","default",2,1261,0,61 +"Zanol2014",1261,"TreeSearch","default",3,1261,0,58.1 +"Zanol2014",1261,"TreeSearch","thorough",1,1261,0,81.3 +"Zanol2014",1261,"TreeSearch","thorough",2,1262,1,31.5 +"Zanol2014",1261,"TreeSearch","thorough",3,1261,0,94.1 +"Zanol2014",1261,"TNT","mult-basic",1,1262,1,0.5 +"Zanol2014",1261,"TNT","mult-basic",2,1262,1,0.5 +"Zanol2014",1261,"TNT","mult-basic",3,1262,1,0.5 +"Zanol2014",1261,"TNT","xmult-default",1,1261,0,0.3 +"Zanol2014",1261,"TNT","xmult-default",2,1262,1,0.2 +"Zanol2014",1261,"TNT","xmult-default",3,1262,1,0.3 +"Zanol2014",1261,"TNT","xmult-level10",1,NA,NA,2 +"Zanol2014",1261,"TNT","xmult-level10",2,1261,0,3.8 +"Zanol2014",1261,"TNT","xmult-level10",3,1261,0,3.9 diff --git a/dev/benchmarks/timing_results/timing_Zhu2013.csv b/dev/benchmarks/timing_results/timing_Zhu2013.csv new file mode 100644 index 000000000..2346fe7e7 --- /dev/null +++ b/dev/benchmarks/timing_results/timing_Zhu2013.csv @@ -0,0 +1,16 @@ +"dataset","target","engine","config","seed","score","over","wall_s" +"Zhu2013",624,"TreeSearch","default",1,624,0,35.1 +"Zhu2013",624,"TreeSearch","default",2,624,0,45.5 +"Zhu2013",624,"TreeSearch","default",3,624,0,44.5 +"Zhu2013",624,"TreeSearch","thorough",1,624,0,65.7 +"Zhu2013",624,"TreeSearch","thorough",2,624,0,42.7 +"Zhu2013",624,"TreeSearch","thorough",3,624,0,57 +"Zhu2013",624,"TNT","mult-basic",1,625,1,0.4 +"Zhu2013",624,"TNT","mult-basic",2,626,2,0.4 +"Zhu2013",624,"TNT","mult-basic",3,624,0,0.5 +"Zhu2013",624,"TNT","xmult-default",1,624,0,0.2 +"Zhu2013",624,"TNT","xmult-default",2,624,0,0.2 +"Zhu2013",624,"TNT","xmult-default",3,624,0,0.2 +"Zhu2013",624,"TNT","xmult-level10",1,624,0,3.2 +"Zhu2013",624,"TNT","xmult-level10",2,624,0,3 +"Zhu2013",624,"TNT","xmult-level10",3,624,0,3.1 diff --git a/dev/benchmarks/tnt_bare/Zanol2014.phy.rds b/dev/benchmarks/tnt_bare/Zanol2014.phy.rds new file mode 100644 index 000000000..af56dca91 Binary files /dev/null and b/dev/benchmarks/tnt_bare/Zanol2014.phy.rds differ diff --git a/dev/benchmarks/tnt_bare/Zanol2014.t0.tre b/dev/benchmarks/tnt_bare/Zanol2014.t0.tre new file mode 100644 index 000000000..945b1bf0b --- /dev/null +++ b/dev/benchmarks/tnt_bare/Zanol2014.t0.tre @@ -0,0 +1,12 @@ +tread 'tree(s) from TNT, for data in C:\Users\pjjg18\AppData\Local\Temp\RtmpgT9Cz7\t027568Zanol2014\data.tnt' +(0 (16 ((55 ((56 ((((46 (43 (((73 (((1 59 )(32 33 ))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))(((12 (6 7 ))(11 (29 ((9 26 )(30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 )))))))))))(((8 13 )(10 (14 15 )))(((64 (63 69 ))((65 67 )(68 70 )))(66 71 )))))(50 57 )))(52 53 )))(36 ((34 37 )(35 38 ))))))* +(0 (16 ((55 (((56 (((46 (43 (((73 (((1 59 )(32 33 ))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))(((12 (6 7 ))(11 (29 ((9 26 )(30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 )))))))))))(((8 13 )(10 (14 15 )))(((64 (63 69 ))((65 67 )(68 70 )))(66 71 ))))))(50 57 ))(52 53 )))(36 ((34 37 )(35 38 ))))))* +(0 (16 ((55 ((56 ((((46 (43 (((73 (((1 59 )(32 33 ))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))(((12 (6 7 ))(11 (29 ((9 26 )(30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 )))))))))))(((8 13 )(10 (14 15 )))(71 (66 ((64 (63 69 ))((65 67 )(68 70 ))))))))(50 57 )))(52 53 )))(36 ((34 37 )(35 38 ))))))* +(0 (16 ((55 ((56 ((((46 (43 (((73 (((1 59 )(32 33 ))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))(((12 (6 7 ))(11 (29 ((9 26 )(30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 )))))))))))(((8 13 )(10 (14 15 )))(71 (66 (((64 (63 69 ))(68 70 ))(65 67 )))))))(50 57 )))(52 53 )))(36 ((34 37 )(35 38 ))))))* +(0 (16 ((55 ((56 ((((46 (43 (((73 (((1 59 )(32 33 ))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))(((12 (6 7 ))(11 (29 ((9 26 )(30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 )))))))))))(((8 13 )(10 (14 15 )))((((64 (63 69 ))(68 70 ))(65 67 ))(66 71 )))))(50 57 )))(52 53 )))(36 ((34 37 )(35 38 ))))))* +(0 (16 ((55 ((56 ((((46 (43 (((73 (((1 59 )(32 33 ))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))(((12 (6 7 ))(11 (29 ((9 26 )(30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 )))))))))))(((8 13 )(10 (14 15 )))(((64 (63 69 ))(67 (65 (68 70 ))))(66 71 )))))(50 57 )))(52 53 )))(36 ((34 37 )(35 38 ))))))* +(0 (16 ((55 ((56 ((((46 (43 (((73 (((1 59 )(32 33 ))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))(((12 (6 7 ))(11 (29 ((9 26 )(30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 )))))))))))(((8 13 )(10 (14 15 )))((((63 69 )(64 (68 70 )))(65 67 ))(66 71 )))))(50 57 )))(52 53 )))(36 ((34 37 )(35 38 ))))))* +(0 (16 ((55 ((56 ((((46 (43 (((73 (((1 59 )(32 33 ))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))(((12 (6 7 ))(11 (29 (9 (26 (30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 ))))))))))))(((8 13 )(10 (14 15 )))(((64 (63 69 ))((65 67 )(68 70 )))(66 71 )))))(50 57 )))(52 53 )))(36 ((34 37 )(35 38 ))))))* +(0 (16 ((55 ((56 ((((46 (43 (((73 (((1 59 )(32 33 ))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))((11 ((12 (6 7 ))(29 ((9 26 )(30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 )))))))))))(((8 13 )(10 (14 15 )))(((64 (63 69 ))((65 67 )(68 70 )))(66 71 )))))(50 57 )))(52 53 )))(36 ((34 37 )(35 38 ))))))* +(0 (16 ((55 ((56 ((((46 (43 (((73 ((33 (32 (1 59 )))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))(((12 (6 7 ))(11 (29 ((9 26 )(30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 )))))))))))(((8 13 )(10 (14 15 )))(((64 (63 69 ))((65 67 )(68 70 )))(66 71 )))))(50 57 )))(52 53 )))(36 ((34 37 )(35 38 )))))); +proc-; diff --git a/dev/benchmarks/tnt_bare/Zanol2014.t0single.tre b/dev/benchmarks/tnt_bare/Zanol2014.t0single.tre new file mode 100644 index 000000000..956cf8fa8 --- /dev/null +++ b/dev/benchmarks/tnt_bare/Zanol2014.t0single.tre @@ -0,0 +1,3 @@ +tread 'single T0 = tree1 of best set' +(0 (16 ((55 ((56 ((((46 (43 (((73 (((1 59 )(32 33 ))(5 (4 17 ))))(3 ((2 (61 (49 (18 72 ))))(60 62 ))))((39 42 )((40 41 )(48 (47 (44 45 ))))))))(54 (51 58 )))(((12 (6 7 ))(11 (29 ((9 26 )(30 (31 (22 (25 (21 ((27 (28 (19 24 )))(20 23 )))))))))))(((8 13 )(10 (14 15 )))(((64 (63 69 ))((65 67 )(68 70 )))(66 71 )))))(50 57 )))(52 53 )))(36 ((34 37 )(35 38 )))))); +proc-; diff --git a/dev/benchmarks/tnt_bare/Zanol2014.tnt b/dev/benchmarks/tnt_bare/Zanol2014.tnt new file mode 100644 index 000000000..d26f38c40 --- /dev/null +++ b/dev/benchmarks/tnt_bare/Zanol2014.tnt @@ -0,0 +1,79 @@ + +xread 'Dataset written by `TreeTools::WriteTntCharacters()`' +213 74 +Aciculomarphysa_comes 0 0 1 1 0 0 1 ? ? ? ? ? ? ? ? ? 1 1 1 1 0 1 ? ? 1 0 1 0 1 ? ? ? 1 0 2 1 0 ? 1 1 0 1 1 2 ? 1 0 ? ? ? ? 1 ? ? ? 0 1 0 ? ? ? 0 ? ? ? ? ? ? 1 0 1 0 ? ? 0 1 ? 4 0 0 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 ? ? ? ? ? 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? 0 1 1 1 1 1 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? +Arabella_semimaculata 0 0 0 1 0 0 0 0 ? 0 ? ? ? 0 0 ? ? ? 0 0 ? 0 ? ? 0 ? 0 ? 0 ? ? ? 0 ? 0 1 0 ? 1 1 0 0 ? 1 ? 0 ? 2 1 0 4 0 4 ? 0 0 1 0 1 1 2 0 ? ? ? ? ? 0 0 ? 0 ? ? ? 0 1 ? ? ? ? ? ? 0 0 0 0 ? ? ? ? ? ? ? ? 2 2 2 2 1 1 1 1 0 0 0 1 1 0 ? 1 1 ? ? ? ? 0 0 ? ? 1 0 0 0 ? 0 2 2 2 2 1 1 1 0 1 1 ? 1 1 1 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 0 0 ? ? ? ? ? ? ? ? 0 +Diopatra_dentata 2 0 ? ? 0 0 1 1 0 0 ? ? ? 0 0 ? 0 3 1 1 0 0 ? ? 1 1 1 0 0 ? ? ? 1 1 1 0 1 ? 1 1 0 1 1 2 2 1 0 2 1 0 1 1 1 ? 1 0 1 0 1 1 0 1 2 ? 0 1 0 2 1 0 1 0 0 0 0 1 ? 1 0 ? 0 ? ? ? ? ? ? 0 ? ? 4 5 0 ? 0 0 0 ? 1 1 1 ? 3 2 2 ? 0 1 0 1 1 ? ? 2 ? 0 0 ? ? 0 0 0 ? 0 0 ? 2 2 ? 0 1 0 1 1 1 1 1 1 ? 0 1 1 ? 0 0 ? ? 0 0 ? ? 2 2 ? 0 ? 0 0 ? 2 2 ? ? 0 0 ? ? ? ? 1 0 0 ? 1 0 ? ? ? ? ? 0 0 ? ? 0 1 1 ? 1 ? 1 ? 1 1 ? 1 ? ? ? ? ? ? 1 0 1 1 ? ? ? ? 2 ? 1 +Diopatra_ornata 2 0 0 0 0 0 1 1 0 0 ? ? ? 0 0 ? 0 2 1 1 0 1 0 ? 1 1 1 0 1 0 ? ? 1 1 1 0 1 ? 1 1 0 1 1 2 2 1 0 2 1 0 1 1 1 ? 1 0 1 0 1 1 0 1 2 ? 0 1 0 1 1 0 1 0 0 0 0 1 ? 1 0 0 0 1 0 1 1 1 ? 1 ? ? 4 5 0 0 0 0 0 0 1 1 1 0 3 2 2 ? 0 1 0 1 1 ? 0 2 2 0 0 ? ? 0 0 0 0 0 0 2 2 2 2 0 0 0 1 1 1 0 0 0 1 0 1 1 1 0 0 0 ? 1 1 1 ? 1 1 2 0 ? 0 0 ? 2 2 2 ? 0 0 0 ? ? ? 1 0 0 0 1 0 ? ? ? ? ? 0 0 ? ? 0 1 1 1 1 1 1 1 ? 1 1 1 ? ? 1 ? ? ? 1 0 6 1 ? ? ? ? ? ? 1 +Dorvillea_erucaeformis 0 ? ? ? 0 0 0 0 ? 0 ? ? ? 1 ? ? ? ? 0 1 2 0 ? ? 0 ? 1 1 0 ? ? ? 0 ? ? 1 0 ? 1 1 1 0 ? 0 ? ? ? ? ? ? ? ? ? ? ? ? 1 0 1 0 ? 0 ? ? ? ? ? 2 1 0 0 ? ? ? 0 1 ? ? ? ? ? ? ? ? ? ? ? ? ? ? 5 6 ? ? 1 1 ? ? 1 1 ? ? 4 3 ? ? 1 0 0 1 1 ? ? ? ? 0 0 ? ? 1 1 ? ? ? ? ? 0 ? ? 0 0 ? 0 0 ? ? ? ? ? 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 ? ? 1 1 1 0 0 ? ? ? 1 1 ? 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 ? 0 2 ? ? ? ? ? ? 1 +Dorvillea_sociabilis 3 3 3 1 0 0 0 0 ? 0 ? ? ? 1 ? ? ? ? 0 1 1 1 1 0 0 ? 1 0 1 1 0 ? 0 ? 0 1 0 ? 1 1 1 0 ? 0 ? ? ? ? ? ? ? ? ? ? ? ? 1 0 1 0 ? 0 ? ? ? ? ? 1 1 0 0 ? ? ? 0 1 ? ? ? ? ? ? 0 1 0 1 ? ? ? ? 1 0 0 1 2 2 2 0 2 2 2 0 ? ? ? ? 1 0 0 1 1 ? ? ? ? 0 0 ? ? ? ? ? ? ? ? 0 ? ? ? 0 0 0 1 1 ? 2 2 ? ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 1 1 0 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 1 1 2 2 2 3 ? ? ? ? 1 +Eunice_aphroditois 0 3 3 1 ? 0 1 1 1 1 2 2 0 0 1 1 1 3 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 0 1 0 1 1 1 1 0 1 1 0 1 1 1 0 1 1 0 1 0 1 0 0 0 0 0 0 ? 1 3 ? 0 0 0 0 1 ? 0 0 1 4 5 6 0 0 0 0 0 2 0 0 0 ? ? ? ? 0 0 0 1 1 1 1 1 2 1 1 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 2 1 1 1 0 0 0 0 1 ? 2 1 0 0 0 0 ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 0 1 ? 1 ? ? ? ? ? 1 ? ? 0 ? ? 1 1 1 0 4 6 5 ? ? ? ? 1 +Eunice_cf_violacemaculata 0 0 3 1 0 0 1 1 1 1 2 2 0 0 1 1 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 0 1 0 1 1 1 1 0 1 1 0 1 1 1 0 1 1 0 1 0 1 0 0 0 0 0 3 ? 1 1 0 2 0 0 0 1 ? 1 1 1 4 0 0 1 0 0 0 ? 2 2 0 0 ? ? ? ? 0 0 0 1 1 1 0 2 2 1 1 0 1 1 1 1 1 ? ? 0 ? 0 2 0 0 0 1 1 1 0 0 1 1 1 1 1 0 0 0 ? 0 0 0 ? 1 1 1 ? 0 0 0 0 1 1 1 ? 1 1 1 ? ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 1 1 1 1 1 ? 0 0 1 ? 0 ? 1 1 ? 1 1 0 6 3 2 2 0 2 2 1 +Eunice_filamentosa 1 0 3 1 0 0 1 1 1 1 0 2 1 0 1 0 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 0 1 0 0 1 1 1 0 1 1 0 1 1 1 2 2 1 0 1 0 1 0 0 0 0 0 4 0 1 0 0 2 0 1 1 1 ? 1 1 0 4 4 5 0 0 0 0 2 2 2 0 0 ? ? ? ? 0 0 0 1 1 1 0 2 2 1 0 0 1 1 1 0 0 ? ? ? 1 1 2 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 ? 0 0 0 ? 2 2 1 ? 0 0 0 0 1 1 1 ? 1 1 1 ? ? ? ? 1 1 1 1 1 1 1 3 3 0 0 ? 0 0 0 0 1 1 1 1 1 1 1 ? 0 0 1 0 1 ? 1 0 ? 1 1 3 6 8 2 1 1 1 0 1 +Eunice_fucata 0 3 3 1 0 0 1 1 1 1 1 2 1 0 1 1 0 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 0 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 2 1 1 1 0 1 1 0 1 1 1 0 1 0 0 1 0 1 0 0 1 0 0 6 0 1 1 0 2 0 0 1 1 ? 1 ? ? 1 0 6 0 2 0 0 2 2 2 2 2 ? ? ? ? 0 0 0 1 1 1 1 1 1 0 0 ? ? 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 2 2 1 2 0 0 0 0 1 1 1 1 1 1 1 1 ? ? ? 1 1 1 1 1 1 1 1 1 ? 0 ? 0 0 0 0 0 1 1 1 ? 0 1 ? ? 0 1 ? 0 0 2 2 ? 1 1 0 5 0 2 2 2 2 2 1 +Eunice_impexa 3 3 ? ? ? 0 1 1 1 1 0 0 1 0 1 0 1 2 1 1 0 1 0 ? 1 ? 1 0 1 0 ? ? 1 ? 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 0 1 0 0 1 1 1 0 1 1 0 1 ? 0 ? 2 ? 0 1 0 1 0 0 0 0 0 ? ? 1 ? 0 ? ? ? ? ? ? 1 0 ? 4 5 0 ? 0 0 0 ? 0 0 0 ? ? ? ? ? 0 0 0 1 1 0 0 1 ? 1 1 0 1 1 1 1 ? 1 1 0 0 0 ? 0 0 0 1 1 1 1 1 1 ? 1 1 1 ? 0 0 ? ? 0 0 ? 2 2 1 ? 0 0 0 0 1 1 1 ? 1 1 0 ? ? ? ? 1 1 1 ? 1 1 1 3 3 0 ? ? 0 0 0 0 0 1 ? 1 ? ? ? ? ? ? 1 ? 0 ? ? 0 ? 1 1 0 6 3 ? 2 2 ? ? 1 +Eunice_norvegica 0 3 ? 1 0 0 1 1 1 1 2 2 1 0 1 1 0 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 0 1 0 1 1 1 1 0 1 1 0 1 1 1 1 1 1 0 1 0 1 0 0 0 0 0 ? 0 1 ? 0 ? ? ? ? ? ? 1 1 ? 0 4 5 ? 0 0 0 ? 0 0 0 ? ? ? ? ? 0 0 0 1 1 1 1 1 ? 0 0 ? ? 1 1 1 ? 0 0 0 0 0 ? 0 0 0 1 1 1 1 1 1 ? 1 1 1 ? 0 0 ? ? 0 0 ? 2 2 2 ? 0 ? 0 0 ? 1 1 ? ? 1 1 ? ? ? ? 1 1 1 ? 1 1 1 0 0 0 ? ? 0 0 0 0 0 1 ? 1 ? 1 ? ? 0 ? 1 ? 0 ? 1 1 ? 1 1 0 3 8 ? 2 1 1 ? 1 +Eunice_roussaei 0 0 0 1 0 0 1 1 1 1 2 2 0 0 1 1 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 0 1 0 1 1 1 1 0 1 1 0 1 1 1 0 ? 1 0 1 0 1 0 0 0 0 0 4 1 1 1 0 ? 0 0 1 1 ? 1 1 ? 0 0 0 0 0 0 0 2 1 2 2 2 1 ? ? ? 0 0 0 1 1 1 2 1 ? 1 1 0 1 1 1 1 1 0 0 ? 0 0 ? 0 0 0 1 1 1 1 0 1 1 1 1 1 1 0 0 0 0 0 0 0 2 1 1 1 0 ? 0 0 1 1 1 1 0 0 0 0 ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 1 1 1 ? ? ? ? ? 1 ? 0 1 1 1 1 1 1 6 6 8 4 ? ? ? ? 1 +Eunice_sp 3 0 0 1 0 0 1 1 1 1 0 1 1 0 1 0 1 1 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 0 1 0 0 1 1 1 0 1 1 0 1 1 1 2 2 1 0 1 0 1 0 0 0 0 0 6 0 1 0 0 2 0 1 1 1 ? ? 1 0 4 0 5 4 0 0 0 0 2 0 0 0 ? ? ? ? 0 0 0 1 1 1 0 2 2 1 1 0 1 1 1 0 0 ? ? 1 1 1 2 0 0 0 1 1 1 1 1 1 0 1 1 1 1 0 0 0 ? 0 1 1 ? 1 1 1 0 ? 0 0 ? 1 1 1 ? 1 1 1 ? ? ? 1 1 1 1 1 1 1 3 3 0 0 ? 0 0 0 0 1 1 1 1 1 1 1 0 0 0 1 0 0 0 1 1 1 1 1 6 6 3 4 ? 2 ? ? 1 +Euniphysa_aculeata 1 0 0 1 1 0 1 1 1 1 3 3 1 0 1 1 0 2 1 1 0 0 ? ? 1 0 1 0 0 ? ? ? 1 0 2 1 1 0 1 1 0 1 1 2 1 1 0 1 1 0 5 1 5 ? 0 1 1 0 1 1 0 1 1 0 ? 2 1 0 1 0 1 0 0 0 0 0 4 3 1 0 0 ? 0 1 1 1 ? 1 1 1 4 5 0 0 0 0 0 0 0 0 0 0 ? ? ? ? 0 0 0 1 1 0 0 2 2 1 0 1 1 0 0 0 0 0 ? 0 0 ? 2 0 0 0 1 1 1 1 0 1 1 0 0 1 0 ? 0 ? ? ? 0 ? ? ? 1 ? 0 ? ? 0 ? ? 1 ? ? ? 1 ? ? ? ? 1 1 1 1 0 ? 1 ? 2 2 2 ? 3 0 0 0 1 1 1 1 1 1 1 ? 0 1 1 0 0 0 1 1 ? 1 1 1 6 4 0 1 1 1 2 1 +Euniphysa_tridontesa 1 0 ? 1 1 0 1 1 1 1 3 3 1 0 1 1 1 2 1 1 0 0 ? ? 1 0 1 0 0 ? ? ? 1 0 2 1 1 1 1 1 0 1 1 2 1 0 ? 0 1 0 5 1 5 ? 0 1 1 0 1 1 0 1 1 0 ? 1 ? 0 1 0 1 0 0 0 0 0 ? 2 1 ? 0 ? ? ? ? ? ? 1 1 ? 4 4 5 ? 2 0 0 ? 1 0 0 ? 1 ? ? ? 0 0 0 1 1 0 2 2 ? 1 0 1 1 1 0 0 ? 0 ? 0 1 0 ? 1 0 0 1 1 1 1 0 0 ? 0 0 1 ? 0 0 ? ? 0 0 ? ? 1 1 ? 0 ? 0 0 ? ? 1 ? ? 1 0 ? ? ? ? 0 1 1 ? ? ? 1 ? 2 ? ? ? ? 0 0 0 1 1 ? 1 ? 1 ? 1 1 ? 1 0 0 ? ? 0 ? 1 1 7 6 4 ? 2 1 1 ? 1 +Fauchaldius_cyrtauloni 0 0 0 0 0 0 1 1 1 0 ? 0 0 0 1 ? 1 2 1 1 1 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 0 1 1 0 1 1 2 1 1 0 0 1 0 ? ? ? ? ? ? 1 0 1 ? ? 0 ? ? ? ? ? 0 1 0 0 ? ? ? 0 1 ? 2 0 4 0 0 0 0 0 1 ? ? ? ? 3 3 4 3 2 2 2 ? 2 2 2 ? ? ? ? ? ? 0 0 0 0 ? ? 2 ? 0 0 ? ? ? ? ? ? ? ? ? ? 0 ? 0 0 0 0 0 0 ? ? ? ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? 1 1 1 1 1 1 1 1 0 0 0 0 ? ? ? ? ? ? 1 1 0 7 4 3 0 ? ? ? ? +Glycera_dibranchiata 0 0 0 1 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? 0 0 ? 0 ? ? 0 ? 1 ? 1 0 ? ? 0 ? ? 2 0 ? 0 0 ? 0 ? ? ? ? ? ? ? ? ? ? 0 ? ? ? 0 ? ? ? ? 1 0 ? ? 0 1 2 1 1 0 ? ? ? 0 1 ? ? ? ? ? ? 0 0 1 1 ? 1 1 1 ? ? ? ? 1 1 1 1 0 1 1 0 0 0 0 ? ? ? ? 1 1 ? ? ? ? 0 0 ? ? 0 0 0 ? ? ? 0 0 0 ? 0 0 0 0 0 0 ? ? ? ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 ? 1 ? ? ? ? ? ? ? 2 2 1 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 1 1 5 0 5 1 ? ? ? ? 6 +Hyalinoecia_sp 0 ? ? ? 0 0 1 1 0 0 ? ? ? 0 0 ? 1 3 1 1 0 1 0 ? 1 1 1 0 1 0 ? ? 1 1 1 0 0 ? 1 1 0 1 1 2 2 1 0 2 1 0 1 1 0 3 1 0 1 0 1 1 0 ? ? ? ? 2 ? 2 1 0 ? ? ? ? 0 1 ? 2 0 ? ? ? ? ? ? ? ? 0 ? ? 4 0 ? ? 0 0 ? ? 1 1 ? ? 3 2 ? ? 0 1 0 1 1 ? 2 ? ? 0 0 ? ? 0 1 ? ? 0 0 2 2 ? ? 1 1 ? 1 1 ? 1 1 ? ? 0 1 ? ? 0 ? ? ? 1 ? ? ? 2 ? ? 0 ? 0 ? ? 1 ? ? ? 1 ? ? ? ? ? 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? 0 1 ? ? ? ? ? ? 1 ? ? 1 0 ? ? ? ? ? 1 ? 1 1 ? ? ? ? ? ? 0 +Leodice_americana 0 0 0 1 0 0 1 1 1 1 1 0 0 0 1 1 0 2 1 1 0 1 0 ? 1 0 1 0 1 1 1 0 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 1 1 1 ? 1 1 1 0 1 1 0 1 1 1 0 0 0 0 1 0 1 0 1 ? 0 1 ? 3 0 1 0 0 1 1 1 1 ? 0 ? ? 4 0 5 0 0 0 0 0 2 2 0 0 ? ? ? ? 0 0 0 1 1 0 0 1 2 1 1 0 0 1 1 1 1 0 0 0 ? 0 2 0 0 0 1 1 1 1 1 0 1 1 1 1 0 0 0 ? 0 0 0 ? ? 2 2 ? 0 ? 0 0 1 1 1 ? ? 0 1 ? ? ? ? 1 1 1 1 1 1 1 0 0 ? ? ? 0 0 0 0 0 1 1 2 2 1 1 ? 1 1 1 ? 0 0 ? ? ? 1 1 6 3 1 4 2 2 1 ? 1 +Leodice_antarctica 3 3 3 1 0 0 1 1 1 1 1 0 1 0 1 1 0 2 1 1 0 1 0 ? 1 0 1 0 1 1 1 0 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 1 1 0 2 1 ? 1 0 1 1 0 1 1 1 ? 0 0 0 1 0 1 0 0 0 0 1 ? ? 0 1 0 0 ? 1 1 1 ? 0 0 ? 1 0 0 0 0 0 0 2 0 2 0 2 ? ? ? ? 0 0 0 1 1 1 1 1 ? 0 0 ? ? 1 1 1 1 0 0 ? ? 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 ? 1 1 0 2 2 2 2 0 ? 0 0 1 1 1 1 1 0 1 1 ? ? ? 1 1 1 1 1 1 1 1 1 0 0 ? 0 0 0 0 0 1 1 1 1 1 ? ? 1 ? 1 ? 1 0 ? ? ? 1 1 2 3 4 2 2 1 ? 2 1 +Leodice_antennata 3 3 3 1 0 0 1 1 1 1 1 2 1 0 1 1 0 2 1 1 0 1 1 1 1 0 1 0 1 1 1 1 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 1 1 0 2 1 0 1 0 1 1 0 1 1 1 1 1 1 0 1 0 1 0 0 0 0 1 ? 0 0 1 0 0 1 1 1 1 ? 1 0 1 1 0 0 1 0 0 0 2 2 2 0 2 ? ? ? ? 0 0 0 1 1 1 1 1 1 0 0 ? ? 1 1 1 1 0 0 0 0 ? ? 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 2 2 2 2 0 0 0 0 1 1 1 1 1 0 0 0 ? ? ? 1 1 1 1 1 1 1 0 0 0 ? ? 0 0 0 0 0 1 1 2 2 ? 1 ? ? 0 1 ? 1 1 ? ? ? 1 1 0 3 2 0 0 0 0 0 1 +Leodice_antillensis 0 0 3 1 0 0 1 1 1 1 1 2 0 0 1 1 0 2 1 1 0 1 1 0 1 0 1 0 1 1 1 0 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 1 1 0 2 1 1 1 0 1 1 0 1 1 1 2 1 0 0 1 0 1 0 0 0 0 1 ? 1 0 1 0 0 1 0 1 1 ? 1 ? ? 0 4 5 0 0 0 0 0 0 0 0 0 ? ? ? ? 0 0 0 1 1 1 1 1 1 0 0 ? ? 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 2 2 1 1 0 0 0 0 1 1 1 1 1 1 1 1 ? ? ? 1 1 1 1 1 1 1 1 1 0 0 ? 0 0 0 0 0 1 1 1 1 1 ? ? 0 ? 1 ? 1 1 ? ? ? 1 1 3 3 8 4 ? 1 ? ? 1 +Leodice_harassii 3 0 0 1 0 0 1 1 1 1 1 2 1 0 1 1 0 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 1 1 0 2 1 1 1 0 1 1 0 1 1 1 0 0 1 0 1 0 1 0 0 0 0 1 ? ? 0 1 0 0 1 0 ? ? ? 0 0 ? 0 0 0 0 0 0 0 0 2 2 0 0 ? ? ? ? 0 0 0 1 1 1 1 1 1 1 1 0 0 1 1 1 1 0 0 0 ? 0 ? 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 2 2 2 2 0 ? 0 0 1 1 1 1 1 1 1 0 ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 1 1 1 1 1 ? 1 1 1 ? 0 0 ? ? ? 1 1 6 3 8 2 2 2 2 ? 1 +Leodice_limosa 1 1 ? ? 0 0 1 1 1 1 1 1 1 0 1 1 0 2 1 1 0 1 1 0 1 0 1 0 1 1 1 0 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 1 1 1 ? 1 0 1 0 1 1 0 1 1 1 2 0 0 0 1 0 1 0 0 0 0 1 ? 0 0 ? 0 ? 1 ? ? ? ? 0 0 ? 0 4 5 ? 0 0 0 ? 2 2 0 ? ? ? ? ? 0 0 0 1 1 0 0 1 ? 1 1 0 0 1 1 1 ? 0 0 ? 2 0 ? 0 0 0 1 ? 1 1 ? 1 ? ? ? 1 ? ? 0 ? ? ? ? ? ? ? ? ? 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 ? ? ? 1 ? 0 0 ? ? ? 0 0 0 0 1 ? 2 ? ? ? ? ? ? 1 ? 0 ? ? ? ? 1 1 4 3 8 ? 1 ? 2 ? 1 +Leodice_lucei 3 0 0 1 0 0 1 1 1 1 1 2 1 0 1 1 1 2 1 1 0 1 1 1 1 0 1 0 1 1 1 1 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 1 1 0 2 1 0 1 0 1 1 0 1 1 1 0 1 1 0 1 0 1 0 0 0 0 1 ? ? 0 1 0 0 1 1 1 1 ? 1 1 ? 1 0 0 0 0 0 0 0 2 0 0 0 ? ? ? ? 0 0 0 1 1 1 0 ? 2 0 0 ? ? 1 1 1 1 0 0 0 0 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 2 2 2 1 0 0 0 0 ? 1 1 1 ? 1 ? 0 ? ? ? 1 1 1 1 1 1 1 1 1 0 0 ? 0 0 0 0 0 1 1 2 2 1 1 ? 1 1 1 ? 1 1 ? ? ? 1 1 0 3 8 2 2 2 2 2 1 +Leodice_marcusi 0 0 3 1 0 0 1 1 1 1 1 0 1 0 1 1 0 2 1 1 0 1 1 1 1 0 1 0 1 1 1 1 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 0 1 0 2 1 1 1 0 1 1 0 1 ? 0 ? 1 1 0 1 0 1 0 0 0 0 0 1 0 1 1 0 2 1 0 1 1 ? 1 1 ? 1 0 0 1 0 0 0 2 2 2 2 2 ? ? ? ? 0 0 0 1 1 1 1 1 1 0 0 ? ? 1 1 1 1 0 0 ? 0 0 2 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 ? 0 0 1 ? 2 2 2 0 ? 0 0 ? 1 1 1 ? ? 1 1 ? ? ? 1 1 1 1 1 1 1 1 1 0 0 ? 0 0 0 0 0 1 1 1 1 1 1 ? 0 0 1 ? 0 0 ? ? 2 1 1 0 3 8 2 2 2 ? ? 1 +Leodice_miurai 0 3 3 1 0 0 1 1 1 1 1 2 0 0 1 1 0 2 1 1 0 1 1 1 1 0 1 0 1 1 1 1 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 1 1 1 ? 1 0 1 0 1 1 0 1 1 1 ? 1 0 0 1 0 1 0 0 0 0 1 ? ? 0 1 0 0 1 0 1 1 ? 0 ? ? 1 0 1 0 0 0 0 0 2 0 0 0 ? ? ? ? 0 0 0 1 1 0 1 ? 1 0 0 ? ? 1 1 1 1 0 0 ? 1 0 ? 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 ? 0 ? ? ? 2 ? 2 ? 0 ? ? ? 1 ? 1 ? ? ? 1 ? ? ? ? 1 1 1 1 1 1 1 0 0 ? 1 ? 0 0 0 0 0 1 1 2 2 ? ? ? ? ? 1 ? 1 ? ? ? ? 1 1 2 3 2 2 2 2 2 2 1 +Leodice_rubra 3 0 0 1 0 0 1 1 1 1 1 0 1 0 1 1 1 2 1 1 0 1 1 1 1 0 1 0 1 1 1 1 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 1 1 0 2 1 0 1 0 1 1 0 1 1 1 0 1 1 0 1 0 1 0 0 0 0 1 ? ? 0 1 0 0 1 1 1 1 ? 0 1 1 4 0 0 0 0 2 2 2 2 1 2 2 ? 1 ? ? 0 0 0 1 1 0 0 1 1 0 0 ? ? 1 1 1 1 0 0 2 ? 1 ? 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 ? 0 1 0 2 2 2 2 0 ? 0 0 1 1 1 1 1 1 1 1 ? ? ? 1 1 1 1 1 1 1 1 0 0 1 ? 0 0 0 0 0 1 1 2 2 1 1 ? 1 1 1 ? 0 0 ? ? ? 1 1 6 3 8 4 0 ? ? ? 1 +Leodice_thomasiana 0 0 0 1 0 0 1 1 1 1 1 0 1 0 1 0 0 2 1 1 0 1 1 0 1 0 1 0 1 1 1 1 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 0 1 0 2 1 1 1 0 1 1 0 1 1 1 2 0 1 0 1 0 1 0 0 0 0 0 1 0 1 1 0 1 0 0 1 1 ? 1 1 ? 0 0 0 1 0 0 0 0 0 2 0 0 ? ? ? ? 0 0 0 1 1 1 1 1 2 0 0 ? ? 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 ? 0 0 0 ? 2 1 1 ? 0 0 0 0 1 1 1 ? 0 ? ? ? ? ? ? 1 1 1 1 1 1 1 1 1 0 0 ? 0 0 0 0 0 1 1 1 1 1 1 ? 0 0 1 ? 0 0 ? 2 2 1 1 0 3 3 2 2 1 2 ? 1 +Leodice_torquata 0 0 3 1 0 0 1 1 1 1 1 2 1 0 1 1 0 2 1 1 0 1 1 0 1 0 1 0 1 1 1 0 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 0 1 0 2 1 1 1 0 1 1 0 1 1 1 2 0 1 0 1 0 1 0 0 0 0 0 1 0 1 1 0 ? ? 1 ? 1 ? 1 1 ? 0 0 0 1 2 0 0 2 2 0 0 2 ? ? ? ? 0 0 0 1 1 1 1 1 2 0 0 ? ? 1 1 1 1 0 0 ? 0 0 0 0 0 0 1 1 1 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 2 2 2 0 0 0 0 1 1 1 1 0 1 1 0 ? ? ? 1 1 1 1 1 1 1 1 1 0 0 ? 0 0 0 0 0 1 1 1 1 1 1 ? 0 0 1 ? 0 0 1 1 0 1 1 3 3 0 0 ? 2 2 2 1 +Leodice_valens 0 0 ? 1 0 0 1 1 1 1 1 1 1 0 1 1 0 2 1 1 0 1 1 0 1 0 1 0 1 1 1 0 1 0 2 1 1 1 1 1 0 1 1 2 1 1 0 1 1 0 1 1 0 2 1 1 1 0 1 1 0 1 1 1 ? 0 0 0 1 0 1 0 0 0 0 1 ? 1 0 1 0 0 ? 1 ? 1 ? 1 ? ? 0 0 0 1 0 0 0 0 2 0 0 0 ? ? ? ? 0 0 0 1 1 1 1 1 1 0 0 ? ? 1 1 1 1 0 0 0 ? 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 2 2 2 2 0 0 0 0 1 1 1 1 0 0 0 0 ? ? ? 1 1 1 1 1 1 1 1 1 0 0 ? 0 0 0 0 0 1 1 1 1 1 1 ? 0 0 1 ? 0 0 ? ? ? 1 1 0 3 3 0 0 0 0 0 1 +Lumbrineris_inflata 0 0 0 1 0 0 0 0 ? 0 ? ? ? 0 0 ? ? ? 0 0 ? 0 ? ? 0 ? 0 ? 0 ? ? ? 0 ? 2 1 0 ? 1 1 0 1 0 2 0 1 1 1 1 0 3 0 3 ? 1 0 1 0 0 1 0 0 ? ? ? ? ? 1 0 ? 0 ? ? ? 0 1 ? ? ? ? ? ? 0 1 0 0 ? ? ? ? ? ? ? ? 1 0 1 1 1 1 1 1 0 0 0 0 1 0 0 1 1 ? ? ? ? 0 0 ? ? 0 0 0 0 0 0 2 2 ? ? 1 1 0 1 1 0 1 1 ? ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 0 0 1 0 0 ? ? ? ? 2 1 1 ? 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 0 0 ? ? ? ? ? ? ? ? 0 +Lumbrineris_latreille 0 0 0 1 0 0 0 0 ? 0 ? ? ? 0 0 ? ? ? 0 0 ? 0 ? ? 0 ? 0 ? 0 ? ? ? 0 ? 2 1 0 ? 1 1 0 1 0 2 0 1 1 1 1 0 3 0 3 ? 1 0 1 0 0 1 0 0 ? ? ? ? ? 2 0 ? 0 ? ? ? 0 1 ? ? ? ? ? ? ? 1 0 0 ? ? ? ? ? ? ? ? 1 1 1 0 1 1 1 1 1 0 2 2 1 0 0 1 1 ? ? ? ? 0 0 ? ? 1 0 0 0 ? 0 0 0 0 0 1 0 0 1 1 0 0 1 ? ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 0 0 0 1 0 ? ? ? ? ? 2 1 ? ? 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 0 0 ? ? ? ? 0 0 0 0 0 +Lysidice_collaris 0 0 ? ? 0 0 1 1 1 1 0 0 1 0 1 0 ? ? 1 1 1 1 0 ? 1 0 0 ? 0 ? ? ? 0 ? 2 1 0 ? 1 1 0 1 1 2 0 1 0 1 0 0 0 1 0 1 1 0 1 2 1 1 2 0 ? ? ? ? ? 0 1 0 1 0 0 0 0 0 ? 0 0 ? ? ? ? ? ? ? ? ? ? ? 0 0 4 ? 0 0 0 ? 2 2 0 ? ? ? ? ? 0 0 0 1 1 0 0 2 ? 0 0 ? ? 0 0 0 ? ? ? 0 0 0 ? 0 0 0 1 1 1 1 1 0 ? 0 1 1 ? 0 0 ? ? 1 1 ? ? 1 1 ? 0 ? 0 ? ? ? 1 ? ? ? 1 ? ? ? ? 1 1 1 ? 1 1 1 0 0 0 ? ? 0 0 0 0 1 1 ? 1 ? 1 ? ? 1 ? 1 0 0 ? ? ? ? 1 1 0 3 3 ? ? 1 1 ? 0 +Lysidice_ninetta 0 3 ? ? 0 0 1 1 1 0 ? 0 0 0 1 0 ? ? 1 1 1 1 0 ? 1 0 0 ? 0 ? ? ? 0 ? 2 1 0 ? 1 1 0 1 1 2 0 1 0 1 0 0 0 1 0 1 1 0 1 2 1 1 2 0 ? ? ? ? ? 1 1 0 1 0 0 0 0 0 ? 0 0 0 0 ? ? ? ? ? ? ? ? ? 0 0 4 ? 0 0 0 ? 1 2 0 ? 1 ? ? ? 0 0 0 1 1 2 2 2 ? 0 0 ? ? 1 0 0 ? ? ? 0 0 0 ? 0 0 0 1 1 1 1 1 1 ? 1 1 1 ? 0 0 ? 1 1 1 ? 2 2 1 ? 0 ? 0 0 1 1 2 ? 0 0 ? ? ? ? ? 1 1 1 ? 1 1 1 0 0 0 ? ? 0 0 0 0 1 1 ? 1 ? 1 ? ? 1 ? 1 0 1 ? ? 1 ? 1 1 0 3 4 ? 2 2 0 ? 0 +Lysidice_sp1 1 ? ? ? ? 0 1 1 1 1 0 0 2 0 1 0 ? ? 1 1 0 1 0 ? 1 0 0 ? 0 ? ? ? 0 ? 2 1 0 ? 1 1 0 1 1 2 0 1 0 1 0 0 0 1 0 2 1 0 1 2 1 1 2 ? ? ? ? ? ? 0 1 0 1 0 0 0 0 0 ? ? 0 ? ? ? ? ? ? ? ? ? ? ? 1 1 ? ? 0 0 ? ? 1 0 ? ? 1 ? ? ? 0 0 0 1 1 ? 2 ? ? 0 0 ? ? 1 0 ? ? ? ? ? 0 ? ? 0 0 ? 1 1 ? 1 1 ? ? 0 1 ? ? 0 ? ? ? 1 ? ? ? 1 ? ? 0 ? 0 ? ? 1 ? ? ? 1 ? ? ? ? ? 1 1 ? ? 1 1 1 0 0 ? ? ? 0 0 ? 0 1 ? ? ? ? ? ? 1 ? ? 1 0 ? ? ? ? ? 1 ? 0 3 ? ? ? 0 ? ? 0 +Lysidice_sp2 3 ? 3 1 0 0 1 1 1 1 0 0 1 0 1 0 ? ? 1 1 1 1 0 ? 1 0 0 ? 0 ? ? ? 0 ? 2 1 0 ? 1 1 0 1 1 2 0 1 0 1 0 0 0 1 0 1 1 0 1 2 1 1 2 0 ? ? ? ? ? 0 1 0 1 0 0 0 0 0 4 0 1 0 0 2 0 1 1 1 ? ? ? ? 0 0 0 0 2 0 0 0 1 1 0 0 1 1 ? ? 0 0 0 1 1 0 0 2 2 0 0 ? ? 1 1 0 0 ? ? 0 0 0 0 0 0 0 1 1 1 1 0 1 ? 1 1 1 ? 0 0 ? 1 1 1 ? 2 1 1 ? 0 0 0 0 1 1 1 ? 1 1 1 ? ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 ? 1 1 0 3 3 3 ? ? 2 ? 0 +Lysidice_unicornis 0 0 0 1 0 0 ? 0 ? 0 ? ? 0 0 1 0 ? ? 1 0 1 1 0 ? 1 ? 0 ? 0 ? ? ? 0 ? 2 1 0 ? 1 1 0 1 1 2 0 1 0 0 0 0 0 1 0 1 1 0 1 2 1 1 2 0 ? ? ? ? ? 1 1 0 1 0 0 0 0 0 7 0 0 0 0 0 0 1 0 1 ? ? ? ? 0 0 4 3 0 0 0 0 1 2 0 2 1 ? ? ? 0 0 0 1 1 1 0 2 2 0 0 ? ? 1 0 0 0 ? ? 0 0 0 0 0 0 0 1 1 1 1 0 0 1 0 0 1 0 ? 0 0 ? ? 1 1 ? ? 1 ? 0 ? ? 0 ? ? 0 0 ? ? 0 0 ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 1 1 1 1 1 1 1 ? 1 1 1 1 1 1 ? ? ? 1 1 4 3 1 3 2 2 0 2 0 +Marphysa_bellii 2 0 ? ? 0 0 1 1 1 0 ? 3 1 0 1 0 1 2 1 1 0 1 0 ? 1 ? 1 0 1 0 ? ? 1 ? 2 1 0 ? 1 1 0 1 1 2 0 1 0 1 1 0 1 1 0 3 1 0 1 0 1 1 0 1 1 1 1 2 0 0 1 0 1 0 1 ? 0 1 ? ? 1 1 0 ? ? ? ? ? ? 0 ? ? 4 0 5 ? 0 0 0 ? 1 1 0 ? 0 0 ? ? 0 0 0 1 1 1 2 2 ? 0 0 ? ? 1 0 0 ? 0 1 0 0 0 ? 0 0 0 1 1 1 0 0 0 ? 1 1 1 ? 0 0 ? 0 0 0 ? 1 0 1 ? 1 ? ? ? 0 0 0 ? 0 0 0 ? ? ? ? 1 1 1 ? 1 1 1 ? ? 0 ? ? 0 0 0 0 0 1 ? 1 ? 1 ? ? 0 ? 1 ? 0 ? 1 1 ? 1 1 3 4 4 ? ? 0 1 ? 1 +Marphysa_brevitentaculata 2 2 2 0 1 0 1 1 1 1 0 3 1 0 1 1 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 0 ? 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 3 1 0 1 0 1 1 0 1 1 1 2 2 1 0 1 0 1 1 0 0 0 0 0 0 1 2 ? 0 0 1 1 1 ? 1 0 ? 4 0 1 0 0 0 0 0 1 0 0 0 1 ? ? ? 0 0 0 1 1 1 0 2 2 0 0 ? ? 1 0 0 0 1 0 0 0 0 0 0 0 0 1 1 1 0 0 1 1 1 1 1 1 0 ? 1 ? 1 1 ? 1 1 0 ? 0 ? ? 0 ? 0 0 ? ? 1 1 ? 0 1 0 1 1 1 1 1 1 1 2 2 2 ? ? 0 0 0 0 0 1 1 0 0 0 0 ? ? ? ? ? 0 1 1 1 ? 1 1 0 3 7 3 ? 2 1 ? 1 +Marphysa_californica 0 2 2 0 0 0 1 1 1 1 0 3 1 0 1 0 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 0 ? 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 3 1 0 1 0 1 1 0 1 ? 1 2 2 1 0 1 0 1 0 0 0 0 0 5 0 1 1 0 0 0 1 1 1 ? ? 0 ? 4 0 0 0 0 0 0 0 1 1 0 0 0 1 ? ? 0 0 0 1 1 0 0 2 2 0 0 ? ? 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 1 1 1 1 1 1 0 ? 1 0 1 1 ? 1 1 0 ? 0 0 0 0 0 0 0 ? ? 1 1 ? 0 0 1 1 1 1 1 1 1 1 2 2 2 2 ? 0 0 0 0 0 1 1 1 1 1 ? ? 0 ? 1 ? 0 0 1 1 ? 1 1 0 3 3 0 ? ? 2 ? 1 +Marphysa_disjuncta 0 0 ? 0 0 0 1 1 1 0 ? 3 1 0 1 0 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 0 ? 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 2 1 0 1 0 1 1 0 1 1 1 1 2 0 0 1 0 1 0 0 0 0 0 ? 0 1 ? 0 ? ? ? ? ? ? 0 ? ? 4 0 5 ? 0 0 0 ? 1 1 0 ? 0 0 ? ? 0 0 0 1 1 0 0 2 ? 0 0 ? ? 0 0 1 ? 1 1 0 0 0 ? 0 0 0 1 1 1 0 0 0 ? 0 1 1 ? 0 0 ? ? 0 0 ? ? 1 1 ? 1 ? ? ? 0 0 0 ? ? 0 0 ? ? ? ? 1 1 1 ? 1 1 1 2 2 ? ? ? 0 0 0 0 0 1 ? ? ? 1 ? ? 1 ? 1 ? 0 ? 1 1 ? 1 1 1 6 1 ? 1 1 1 ? 1 +Marphysa_fallax 0 0 0 1 0 0 1 1 1 0 ? 0 0 0 1 0 0 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 0 ? 1 1 0 1 1 2 1 1 0 1 1 0 0 1 ? ? 1 0 1 0 1 1 0 1 0 ? ? ? 0 0 1 0 1 0 0 0 0 1 ? 0 0 1 0 1 0 1 ? ? ? 1 1 ? 0 0 0 ? 0 0 0 ? 1 1 2 ? 0 0 ? ? 0 0 0 1 1 ? ? 2 ? 0 0 ? ? 1 1 0 ? ? ? ? 0 0 ? 0 0 0 1 1 1 0 0 1 ? 1 1 1 ? 0 0 ? 1 1 1 ? 1 1 1 ? 0 0 0 0 0 0 0 ? 0 0 1 ? ? ? ? 1 1 1 ? 1 1 1 ? ? 0 ? ? 0 0 0 0 0 1 ? 1 ? ? ? ? ? ? 1 ? 1 ? ? ? ? 1 ? 0 3 ? ? 2 2 ? ? 0 +Marphysa_mossambica 1 2 2 0 1 0 1 1 1 1 0 3 1 0 1 1 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 0 ? 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 2 1 0 1 0 1 1 0 1 1 1 2 2 1 0 1 0 1 0 0 0 0 0 4 0 0 1 1 0 0 1 0 1 ? 1 1 1 4 5 0 3 0 0 0 0 1 0 0 2 1 ? ? ? 0 0 0 1 1 0 3 3 0 0 0 ? ? 0 0 0 0 1 1 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 1 1 0 1 1 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0 ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? 0 0 1 1 1 1 1 ? ? 0 ? ? ? 0 ? ? ? ? 1 1 0 4 4 0 0 0 ? ? 1 +Marphysa_novahollandiae 0 2 ? ? 0 0 1 1 1 1 0 3 1 0 1 ? 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 0 ? 1 1 0 1 1 2 1 1 0 0 1 0 ? 1 ? ? 1 0 1 0 1 ? ? 1 1 1 2 2 ? 0 1 0 1 0 ? ? 0 0 ? 0 0 ? ? ? ? ? ? ? ? ? 0 ? 4 5 6 ? 0 0 ? ? 2 ? ? ? 2 ? ? ? ? 0 0 1 1 0 0 0 ? 0 0 ? ? 0 0 0 ? 1 ? 0 0 0 ? 1 1 ? 1 1 1 ? ? ? ? ? ? 1 ? ? 1 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 0 ? 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? 0 0 1 ? 1 ? 1 ? ? 0 ? ? ? 0 ? ? ? ? 1 1 ? ? ? ? ? ? ? ? 1 +Marphysa_regalis 0 2 0 ? 0 0 1 1 1 1 0 0 1 0 1 1 0 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 0 ? 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 3 1 0 1 0 1 1 0 1 1 1 2 2 ? 0 1 0 1 1 0 0 0 1 ? 0 0 1 0 1 0 ? 1 1 ? ? 1 ? 0 0 1 1 0 0 0 0 1 1 0 0 0 1 ? ? 0 0 0 1 1 0 0 2 2 0 0 ? ? 1 1 0 0 0 0 ? 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 0 1 1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 1 0 0 0 0 ? ? ? ? ? 0 0 ? ? ? 1 1 0 3 7 5 2 ? ? ? 1 +Marphysa_sanguinea 0 2 2 0 ? 0 1 1 1 1 3 3 0 0 1 1 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 0 ? 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 2 1 0 1 0 1 1 0 1 1 1 2 2 1 0 1 0 1 0 1 ? 0 0 3 0 1 1 0 0 0 1 0 1 ? 0 0 ? 4 0 0 0 0 0 0 0 1 2 0 2 0 ? ? ? 0 0 0 1 1 0 0 2 0 0 0 ? ? 0 0 0 0 1 1 ? 0 0 0 0 0 0 1 1 1 0 0 0 1 0 1 1 1 0 ? 0 ? 1 1 1 ? 1 0 2 0 ? 0 0 ? 0 0 0 ? 1 1 0 0 1 ? 1 1 1 1 1 1 1 2 2 2 2 ? 0 0 0 0 0 1 1 1 1 1 ? ? 0 ? 1 ? 0 ? 1 1 1 1 1 0 3 3 5 0 0 0 2 1 +Marphysa_viridis 0 2 ? 0 0 0 1 1 1 1 3 3 2 0 1 0 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 ? 2 1 0 ? 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 2 1 0 1 0 1 1 0 1 1 1 2 2 1 0 1 0 1 1 0 0 0 0 4 0 0 1 0 0 0 1 0 1 ? 0 0 ? 0 0 0 3 0 0 0 0 1 2 0 0 0 ? ? ? 0 0 0 1 1 0 2 1 2 0 0 ? ? 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 1 0 1 1 0 0 ? ? ? 1 1 ? ? 1 0 ? 0 ? 0 0 ? 0 0 ? ? 0 1 ? 0 1 ? 1 1 1 1 1 1 1 2 2 2 ? ? 0 0 0 0 0 1 0 1 ? ? 0 ? ? ? ? ? 0 ? ? ? ? 1 1 0 3 7 3 2 1 2 2 1 +Mooreonuphis_pallidula 2 0 0 1 0 0 1 1 0 0 ? ? ? 0 0 ? 1 3 1 1 0 1 0 ? 1 1 1 0 1 0 ? ? 1 1 1 0 1 ? 1 1 0 1 1 2 2 1 0 2 1 0 1 1 1 ? ? 0 1 0 1 1 0 1 0 ? ? 1 1 2 1 0 1 0 0 0 0 1 ? 1 0 0 0 0 0 1 ? 1 ? 0 0 ? 4 0 0 0 0 0 0 0 1 1 0 0 3 2 ? ? 0 1 0 1 1 ? ? 2 2 0 0 ? ? 0 0 0 0 0 0 2 2 2 2 0 0 0 1 1 1 1 1 1 1 0 0 1 0 ? 0 ? ? ? 1 ? ? ? ? ? 0 ? ? 1 ? ? 1 ? ? ? 1 ? ? ? ? 1 0 0 1 1 0 ? ? ? ? 0 1 0 ? ? 0 1 1 1 1 1 1 ? ? 0 ? 1 ? ? 0 ? ? ? 1 0 6 1 ? ? ? ? ? ? 1 +Nicidion_amoureuxi 1 3 3 0 0 0 1 1 1 1 0 0 1 0 1 1 1 2 1 1 1 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 0 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 2 1 0 1 0 1 1 0 1 0 ? ? 1 0 0 1 0 1 0 0 0 0 0 2 1 1 1 0 2 0 1 1 1 ? 0 1 ? 1 0 0 0 0 0 0 0 1 1 0 0 1 1 ? ? 0 0 0 1 1 ? 0 2 2 0 0 ? ? ? 1 0 0 ? 0 ? 0 0 ? 0 0 0 1 1 1 1 0 1 ? 1 1 1 ? 0 0 ? 1 ? 1 ? 2 1 1 ? 0 0 ? 0 1 1 1 ? 1 1 0 ? ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 1 1 1 1 ? ? 0 ? 1 ? 0 0 2 ? ? 1 1 3 3 4 3 2 ? 0 ? 1 +Nicidion_angeli 0 0 ? 1 0 0 1 1 1 1 0 2 1 0 1 1 1 2 1 1 0 1 0 ? 1 ? 1 0 1 0 ? ? 1 2 2 1 0 ? 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 2 1 0 1 0 1 1 0 1 0 ? ? 2 1 0 1 0 1 0 0 0 0 0 4 0 1 ? 0 2 0 1 ? ? ? 0 0 ? 1 0 1 ? 0 0 0 ? 1 1 0 ? 0 1 ? ? 0 0 0 1 1 1 0 2 ? 0 0 ? ? 1 0 0 ? 0 0 0 ? 0 ? 0 0 0 1 1 1 1 0 1 ? 1 1 1 ? 0 0 ? 0 0 1 ? 2 1 1 ? 0 0 0 0 1 1 1 ? 1 1 1 ? ? ? ? 1 1 1 ? 1 1 1 0 0 0 ? ? 0 0 0 0 0 1 ? 1 ? 1 ? ? 1 ? 1 ? 0 ? 0 0 ? 1 1 0 3 4 ? 2 1 2 ? 1 +Nicidion_cariboea 0 0 0 ? 0 0 1 1 1 1 0 2 0 0 1 1 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 0 1 1 0 1 1 2 ? 1 0 1 1 0 0 1 0 2 1 0 1 0 1 1 0 0 ? ? ? ? ? 0 1 0 1 0 0 0 0 0 6 0 1 1 0 2 0 0 0 1 ? ? ? ? 0 0 3 3 0 0 0 0 2 2 0 0 ? ? ? ? 0 0 0 1 1 2 2 2 2 0 0 ? ? 0 0 0 0 ? ? 0 ? 0 0 0 0 0 1 1 1 0 0 1 1 1 1 1 1 0 0 0 0 1 1 1 0 0 1 1 0 0 0 0 1 1 1 1 1 1 1 0 ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 1 1 1 1 1 ? 0 0 1 ? 1 1 2 2 ? 1 1 0 4 4 3 2 ? 0 ? 1 +Nicidion_cincta ? 0 ? ? 0 0 1 1 1 1 0 2 ? ? ? ? 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0 ? ? ? ? ? 0 1 0 1 0 0 0 0 0 ? 0 1 ? ? ? ? ? ? ? ? ? ? ? 0 0 ? ? 0 0 ? ? 1 0 ? ? ? ? ? ? 0 0 0 1 1 0 2 ? ? 0 0 ? ? 0 0 ? ? ? ? 0 0 ? ? 0 0 ? 1 1 ? ? ? ? ? 1 1 ? ? 0 ? ? 0 0 ? ? ? ? ? ? 0 0 0 ? 1 1 ? ? ? ? ? ? ? ? ? 1 1 ? ? 1 1 1 0 0 ? ? ? 0 0 ? 0 ? 1 ? 1 ? 1 ? ? 1 ? 1 ? ? ? 2 ? ? 1 1 0 4 ? ? ? ? ? ? ? +Nicidion_hentscheli 0 0 0 1 0 0 1 1 1 1 0 0 1 0 1 1 1 2 1 1 0 1 0 ? 1 ? 1 0 1 0 ? ? 1 ? 2 1 0 ? 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 1 1 0 1 0 1 1 0 1 ? 0 ? 2 1 0 1 0 1 0 0 0 0 0 6 0 1 1 0 2 0 1 0 1 ? 0 0 1 0 0 0 1 2 0 0 0 2 2 0 0 ? ? ? ? 0 0 0 1 1 2 2 2 2 0 0 ? ? 1 0 0 0 0 ? 0 0 0 0 0 0 0 1 1 1 0 0 1 1 1 1 1 0 0 0 ? 0 1 1 ? 1 0 1 ? 0 0 0 0 0 1 1 ? 1 1 1 ? ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 0 1 ? 1 0 ? 0 ? 1 ? 1 ? 0 0 ? 1 1 0 3 0 3 1 1 1 0 1 +Nicidion_insularis 0 0 0 ? 0 0 1 1 1 1 0 0 0 0 1 0 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 2 1 0 0 1 0 0 ? 0 2 1 0 1 0 1 1 0 0 ? ? ? ? ? 0 1 0 1 0 1 ? 0 0 ? 0 1 1 0 ? 0 0 1 1 ? ? ? ? 0 0 4 0 0 0 0 0 2 2 0 0 ? ? ? ? 0 0 0 1 1 0 0 2 2 0 0 ? ? 1 0 0 0 ? ? ? 0 0 0 0 0 0 1 1 1 1 0 1 1 1 ? 1 1 ? 0 0 ? ? 0 1 2 ? 1 1 0 ? ? 0 ? ? 1 1 ? ? 1 1 ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 1 1 1 1 ? ? 1 ? 1 ? 0 0 2 ? 0 1 1 0 3 4 3 ? ? ? ? 1 +Nicidion_mikeli 0 0 0 1 0 0 1 1 1 1 0 2 1 0 1 1 1 2 1 1 0 1 0 ? 1 2 1 0 1 0 ? ? 1 2 2 1 1 0 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 1 1 0 1 0 1 1 0 1 0 ? ? 2 1 0 1 0 1 0 0 0 0 0 ? 0 1 1 0 ? 0 1 0 1 ? ? 0 ? 0 0 0 0 0 0 0 0 2 2 0 0 ? ? ? ? 0 0 0 1 1 1 1 2 2 0 0 ? ? 1 1 0 0 0 0 0 0 1 2 0 0 0 1 1 1 1 0 1 1 1 1 1 1 0 0 0 1 1 1 1 2 1 1 ? 0 0 0 0 0 0 1 1 1 1 1 0 ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 1 1 1 ? ? ? ? ? 1 ? 0 0 1 1 ? 1 1 3 3 3 1 1 1 0 0 1 +Nicidion_mutilata 0 0 0 1 0 0 1 1 1 1 0 2 0 0 1 0 1 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 0 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 1 1 0 1 0 1 1 0 1 1 1 2 1 1 0 1 0 1 0 0 0 0 0 2 1 1 0 0 2 0 0 0 1 ? 1 1 ? 1 0 0 4 2 0 0 2 1 2 0 2 2 ? ? ? 0 0 0 1 1 0 0 2 2 0 0 ? ? 1 1 0 0 0 ? 0 0 0 0 0 0 0 1 1 1 1 0 1 1 1 1 1 1 0 0 0 ? 1 1 1 ? 1 1 1 0 ? 0 0 ? 1 1 1 ? ? 0 1 ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 1 1 1 ? ? ? ? ? 1 ? 0 0 0 ? 0 1 1 0 3 4 3 1 1 1 0 1 +Nidicion_notata 0 1 1 1 0 0 1 1 1 1 0 2 1 0 1 0 1 2 1 1 0 1 0 ? 1 2 1 0 1 0 ? ? 1 2 2 1 1 1 1 1 0 1 1 2 1 1 0 0 1 0 0 1 0 1 1 0 1 0 1 1 0 1 0 ? ? 2 1 0 1 0 1 0 0 0 0 0 6 0 1 1 0 2 0 0 1 1 ? ? 1 ? 1 0 1 1 0 0 0 0 0 0 0 0 ? ? ? ? 0 0 0 1 1 2 1 2 2 0 0 ? ? 1 1 0 0 ? 0 ? ? ? 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 ? 1 1 1 1 1 1 0 0 ? 0 1 1 1 1 1 1 1 1 ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 1 1 1 1 1 ? ? 1 ? 1 ? 0 0 0 0 ? 1 1 0 4 3 2 2 2 2 ? 1 +Oenone_fulgida 0 0 0 1 0 0 0 0 ? 0 ? ? ? 0 0 ? ? ? 1 1 2 0 ? ? 0 ? 0 ? 0 ? ? ? 0 ? 0 0 0 ? 1 1 0 0 ? 1 ? 0 ? 2 1 0 4 0 4 ? 0 0 1 0 1 1 2 0 ? ? ? ? ? 1 1 0 0 ? ? ? 0 1 ? 1 0 1 0 0 0 1 0 0 ? ? ? ? 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 1 0 0 1 1 ? ? ? ? 0 0 ? ? 0 0 0 0 ? ? 0 0 0 0 1 1 1 1 1 1 2 2 2 2 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? 0 0 1 1 ? ? 1 1 ? 1 1 1 ? 0 0 ? ? ? 0 0 ? ? ? ? 0 0 0 0 1 +Onuphis_elegans 2 0 ? ? 0 0 1 1 0 0 ? ? ? 0 0 ? 1 3 1 1 0 0 ? ? 1 1 1 0 0 ? ? ? 1 1 1 0 1 ? 1 1 0 1 1 2 2 1 0 1 1 0 1 1 1 ? 1 0 1 0 1 1 0 1 0 ? ? 3 ? 1 1 0 1 0 ? ? 0 1 ? 1 0 0 ? ? ? ? ? ? 0 2 3 ? 4 5 5 ? 0 1 2 ? 1 1 1 ? 3 2 2 ? 0 1 ? 1 1 ? ? ? ? 0 0 ? ? 1 0 0 ? 0 0 ? 2 2 ? 0 0 0 1 1 1 0 0 0 ? 1 1 1 ? 0 0 ? 0 0 1 ? 2 2 2 ? 0 0 0 0 2 2 2 ? 0 0 1 ? ? ? ? 1 0 0 ? 1 0 ? ? ? ? ? 1 0 ? ? 0 1 1 ? 1 ? 1 ? 1 1 ? 1 ? ? ? ? ? ? 1 0 6 1 ? ? ? ? 1 ? 1 +Onuphis_eremita 2 ? ? ? 0 0 1 1 0 0 ? ? ? 0 0 ? 1 3 1 1 0 0 ? ? 1 1 1 0 1 1 1 ? 1 1 1 0 0 ? 1 1 0 1 1 2 0 1 0 2 1 0 1 1 1 ? 1 0 1 0 1 1 0 1 0 ? ? 1 ? 1 1 0 1 0 ? ? 0 1 ? ? 0 0 ? ? ? ? ? ? ? 1 1 ? 4 5 0 ? 0 0 0 ? 1 1 1 ? 3 2 2 ? 0 1 1 1 1 ? ? 2 ? 0 0 ? ? 1 0 1 ? 0 0 2 2 2 ? 0 0 0 1 1 1 1 1 1 ? 0 1 1 ? 0 0 ? ? 0 1 ? ? 2 2 ? 0 ? 1 1 ? ? 2 ? ? 0 1 ? ? ? ? 1 0 0 ? 1 0 ? ? ? ? ? ? 0 ? ? 0 1 1 ? 1 ? 1 ? 1 1 ? 1 1 1 ? ? ? ? 1 0 6 1 ? ? 2 2 0 ? ? +Onuphis_iridescens 2 ? ? ? 0 0 1 1 0 0 ? ? ? 0 0 ? 0 3 1 1 0 0 ? ? 1 1 1 0 0 ? ? ? 1 1 1 0 1 ? 1 1 0 1 1 2 2 1 0 2 1 0 1 1 1 ? 1 0 1 0 1 1 0 1 0 ? ? 3 ? 1 1 0 ? ? ? ? 0 1 ? ? 0 ? ? ? ? ? ? ? 1 0 ? ? 4 0 ? ? 0 0 ? ? 1 1 ? ? 3 2 ? ? 0 1 0 1 1 ? 1 ? ? 0 0 ? ? 0 0 ? ? 0 0 2 2 ? ? 0 0 ? 1 1 ? 1 0 ? ? 0 1 ? ? 0 ? ? ? ? ? ? ? 1 ? ? 0 ? 1 ? ? 2 ? ? ? 1 ? ? ? ? ? 1 0 ? ? 1 0 ? ? ? ? ? 1 0 ? ? 0 1 ? ? ? ? ? ? 1 ? ? 1 ? ? ? ? ? ? 1 ? 6 1 ? ? ? ? ? ? 1 +Palola_siciliensis 3 3 ? ? 1 0 1 1 1 1 3 0 ? 0 1 0 2 0 1 1 0 1 0 ? 1 ? 1 0 1 0 ? ? 1 ? 2 1 1 1 1 1 0 1 1 2 0 1 0 1 0 1 2 1 2 ? 1 1 1 1 1 1 1 1 0 ? ? 2 ? 0 1 0 1 0 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? 1 ? 0 0 0 ? 0 0 0 ? 0 0 0 ? ? ? ? ? 0 0 0 1 1 1 2 2 ? 1 0 0 1 1 0 0 ? ? 0 0 0 0 ? 0 0 0 1 1 1 1 1 0 ? 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 ? 1 1 1 0 0 0 ? ? 0 0 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 0 6 7 ? ? 2 2 ? 0 +Palola_sp_A1 3 0 ? 1 0 0 1 1 1 1 0 2 2 0 1 0 2 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 0 1 0 1 0 1 2 1 2 ? 1 1 1 1 1 1 1 1 0 ? ? 2 2 0 1 0 1 0 0 0 0 0 4 ? ? ? ? ? 0 1 1 1 ? ? 1 ? 0 0 4 0 0 0 0 0 2 0 0 0 ? ? ? ? 0 0 0 1 1 1 2 2 2 0 0 ? ? 1 1 0 0 ? ? 0 0 0 0 0 0 0 1 1 1 1 0 0 ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 1 1 0 6 1 1 ? 2 ? ? 0 +Palola_sp_A3 3 0 ? ? 0 0 1 1 1 1 0 2 2 0 1 ? 2 1 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 0 1 0 1 0 1 2 1 2 ? 1 1 1 1 1 1 1 1 0 ? ? 2 ? 0 1 0 1 0 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? 1 ? 0 1 4 ? 0 0 0 ? 2 0 0 ? ? ? ? ? 0 0 0 1 1 1 1 2 ? 0 0 ? ? 1 1 0 ? ? ? 0 0 0 ? 0 0 0 1 1 1 1 0 0 ? 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 ? 1 1 1 0 0 0 ? ? 0 0 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 6 3 4 ? 0 ? ? ? 0 +Palola_sp_A7Pohnpei142 3 3 3 1 1 0 1 1 1 1 0 2 0 0 1 0 2 1 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 0 1 0 1 0 1 2 1 2 ? 1 1 1 1 1 1 1 1 0 ? ? 2 2 0 1 0 1 0 0 0 0 0 4 ? ? ? ? ? 0 ? ? ? ? ? 0 ? 0 0 0 3 0 0 0 0 2 2 0 0 ? ? ? ? 0 0 0 1 1 0 1 2 2 0 0 ? ? 1 1 0 0 ? ? ? 0 0 0 0 0 0 1 1 1 1 1 1 ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 ? 1 1 1 0 0 0 ? ? 0 0 0 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 1 1 2 6 2 2 2 1 1 ? 0 +Palola_sp_A9Kosrae161 3 ? ? ? ? 0 1 1 1 1 0 2 2 0 1 0 2 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 0 1 0 1 0 1 2 1 2 ? 1 1 1 1 1 1 1 ? ? ? ? ? ? 0 1 0 1 ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0 0 ? ? 0 0 ? ? 2 0 ? ? ? ? ? ? 0 0 0 1 1 1 1 ? ? 0 0 ? ? 1 1 ? ? ? ? 0 0 ? ? 0 0 ? 1 1 ? 1 0 ? ? 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 ? ? 1 1 1 ? 0 ? ? ? 0 0 ? 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 ? 0 3 ? ? 2 ? ? ? 0 +Palola_sp_B1 3 0 0 1 1 0 1 1 1 1 0 2 2 0 1 0 2 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 0 1 0 1 0 1 2 1 2 ? 1 1 1 1 1 1 1 1 0 ? ? 2 2 0 1 0 1 0 0 0 0 0 0 ? ? ? ? ? 0 1 0 1 ? ? 1 ? 0 1 1 1 2 0 0 0 2 0 0 0 ? ? ? ? 0 0 0 1 1 1 1 2 2 0 0 ? ? 1 1 1 0 0 ? 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 1 1 0 6 2 2 2 2 2 ? 0 +Palola_sp_B5 3 ? 0 1 1 0 1 1 1 1 0 ? ? 0 1 0 2 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 ? 1 1 1 1 1 0 1 1 2 0 1 0 1 0 1 2 1 2 ? 1 1 1 1 1 1 1 1 0 ? ? 2 2 0 1 0 1 0 0 0 0 0 4 ? ? ? ? ? 0 1 1 1 ? ? 1 ? 1 0 4 0 0 0 0 0 2 0 0 0 ? ? ? ? 0 0 0 1 1 1 2 2 2 0 0 ? ? 1 0 0 0 ? ? 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 1 1 0 6 1 2 2 ? 2 ? 0 +Palola_sp_B7 3 0 3 1 0 0 1 1 1 1 0 2 2 0 1 0 2 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 0 1 0 1 0 1 2 1 2 ? 1 1 1 1 1 1 1 1 0 ? ? 2 2 0 1 0 1 0 0 0 0 0 4 ? ? ? ? ? 0 1 ? 1 ? ? 1 ? 0 1 4 3 2 0 0 0 1 2 0 0 1 ? ? ? 0 0 0 1 1 ? 2 2 2 0 0 ? 1 1 1 0 0 ? ? 2 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 1 1 1 1 0 0 0 0 ? 0 0 0 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 1 1 0 6 2 2 ? 2 ? ? 0 +Palola_viridis 3 0 ? ? ? 0 1 1 1 1 0 2 0 0 1 0 2 2 1 1 0 1 0 ? 1 0 1 0 1 0 ? ? 1 0 2 1 1 1 1 1 0 1 1 2 0 1 0 1 0 1 2 1 2 ? 1 1 1 1 1 1 1 1 0 ? ? 2 2 0 1 0 1 0 0 0 0 0 ? ? ? ? ? ? 0 ? ? ? ? ? 1 ? 1 0 0 ? 0 0 0 ? 2 2 0 ? ? ? ? ? 0 0 0 1 1 2 0 1 ? 0 0 ? ? 0 1 1 ? ? ? 0 0 0 ? 0 0 0 1 1 1 1 1 1 ? 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 1 ? 1 1 1 0 0 0 ? ? 0 0 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 1 2 6 4 ? 1 1 1 ? 0 +Paradiopatra_quadricuspis 0 ? ? ? 0 0 1 1 0 0 ? ? ? 0 0 ? 2 3 1 1 0 1 0 ? 1 1 1 0 1 0 ? ? 1 1 1 0 1 ? 1 1 0 1 1 2 2 1 0 2 1 0 1 1 1 ? 1 0 1 0 1 1 0 1 1 0 ? 1 ? 0 1 0 ? ? ? ? 0 1 ? 1 0 0 0 ? ? ? ? ? ? 2 ? ? 4 4 ? ? 0 0 ? ? 1 1 ? ? 3 2 ? ? 0 1 0 1 1 ? 1 ? ? 0 0 ? ? 0 1 ? ? 0 1 2 2 ? ? 0 0 ? 1 1 ? 1 1 ? ? 0 1 ? ? 0 ? ? ? 1 ? ? ? 2 ? ? 0 ? ? ? ? 0 ? ? ? ? ? ? ? ? ? 1 0 ? ? 1 0 ? ? ? ? ? ? 0 ? ? 0 1 ? ? ? ? ? ? 1 ? ? 1 ? ? ? ? ? ? 1 ? 6 1 ? ? ? 2 ? ? 1 +Paramphinome_jeffreysii 0 0 0 1 0 1 0 ? ? 0 ? ? ? ? ? ? 3 4 1 1 0 1 0 ? 0 ? 1 0 1 0 ? ? 0 ? ? 2 0 ? 0 0 ? 0 ? ? ? ? ? ? ? ? ? ? 0 ? ? ? 0 ? ? ? ? 1 3 ? 2 1 0 1 1 1 0 ? ? ? 1 1 ? ? ? ? ? ? 0 ? ? ? ? 1 ? ? ? ? ? ? 0 0 0 0 1 1 1 1 2 2 2 2 0 0 0 1 1 ? 0 2 ? 0 0 ? ? 0 0 0 ? ? ? ? 3 3 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0 0 0 0 ? ? ? ? ? ? ? ? ? ? ? 0 0 0 0 ? ? ? 0 ? ? ? ? ? ? ? ? ? ? 0 0 ? ? ? ? ? ? ? ? 7 +; + diff --git a/dev/benchmarks/tnt_bare/barebones.R b/dev/benchmarks/tnt_bare/barebones.R new file mode 100644 index 000000000..5a1f2830c --- /dev/null +++ b/dev/benchmarks/tnt_bare/barebones.R @@ -0,0 +1,56 @@ +# BARE-BONES sectsch from a fixed single-tree T0, read fresh (NO mult/TBR before sectsch). +# Dumps raw TNT output so we can see every reported score / accepted move. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +bare <- "dev/benchmarks/tnt_bare" +nm <- Sys.getenv("DS", "Zanol2014") +phy <- readRDS(file.path(bare, paste0(nm, ".phy.rds"))) + +wd <- file.path(tempdir(), paste0("bb", Sys.getpid(), nm)); unlink(wd, recursive = TRUE) +dir.create(wd, recursive = TRUE, showWarnings = FALSE) +WriteTntCharacters(phy, file.path(wd, "data.tnt")) + +# ---- Step 1: build a SINGLE-tree T0 (mult replic 1, rseed 1, hold 1), save to t0.tre ---- +writeLines(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1;", + "mult = replic 1;", "tsave *tee.tre;", "save;", "tsave/;", "quit;"), + file.path(wd, "maketee.run")) +old <- setwd(wd) +suppressWarnings(system2(TNT, args = "maketee.run;", stdout = TRUE, stderr = TRUE)) +setwd(old) +t0 <- ReadTntTree(file.path(wd, "tee.tre")); if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] +cat(sprintf("T0 single-tree score = %.0f (tips=%d)\n", TreeLength(t0, phy), length(t0$tip.label))) +file.copy(file.path(wd, "tee.tre"), file.path(bare, paste0(nm, ".t0single.tre")), overwrite = TRUE) + +# ---- Step 2: FRESH session: load matrix, read T0, run ONE stripped sectsch round ---- +script <- Sys.getenv("SCRIPT_FILE", "") +if (!nzchar(script)) { + script <- file.path(wd, "barerun.run") + writeLines(c( + "mxram 1024;", + "report+;", # verbose progress + "proc data.tnt;", + "rseed 1;", + "hold 1000;", + "proc tee.tre;", # read the fixed T0 (no search yet) + "sect: ;", # show CURRENT (default) sectsch settings + # strip the obvious bells: no global TBR, strict acceptance, no fusing, no drift + "sectsch: noglobal noequals nofuse godrift 9999 ;", + "sect: ;", # show stripped settings + "sectsch = rss ;", # ONE bare sectorial round + "score ;", + "quit;"), script) +} else { + file.copy(script, file.path(wd, basename(script)), overwrite = TRUE) + script <- file.path(wd, basename(script)) +} +old <- setwd(wd) +out <- suppressWarnings(system2(TNT, args = paste0(basename(script), ";"), stdout = TRUE, stderr = TRUE)) +setwd(old) +out <- iconv(out, from = "", to = "UTF-8", sub = "") +cat("==== RAW TNT OUTPUT (filtered to informative lines) ====\n") +keep <- grep("score|RSS|ector|eplac|earrang|settings|size|global|equal|drift|fuse|RAS|TBR", + out, ignore.case = TRUE, value = TRUE) +cat(paste0(trimws(keep)), sep = "\n") diff --git a/dev/benchmarks/tnt_bare/confirm.R b/dev/benchmarks/tnt_bare/confirm.R new file mode 100644 index 000000000..f025164d1 --- /dev/null +++ b/dev/benchmarks/tnt_bare/confirm.R @@ -0,0 +1,43 @@ +# Cross-dataset confirmation of the single-strict-plateaus / set-strict-escapes pattern. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } +num <- function(x) suppressWarnings(as.double(gsub(",", "", x))) +target <- c(Zanol2014 = 1261, Wortley2006 = 479, Giles2015 = 670, Zhu2013 = 624) +SEEDS <- 1:4; RDS <- 30 + +for (nm in strsplit(Sys.getenv("DSETS", "Zanol2014 Wortley2006 Giles2015"), "\\s+")[[1]]) { + phy <- fitch(inapplicable.phyData[[nm]]) + wd <- file.path(tempdir(), paste0("cf", Sys.getpid(), nm)); unlink(wd, recursive = TRUE) + dir.create(wd, recursive = TRUE, showWarnings = FALSE) + WriteTntCharacters(phy, file.path(wd, "data.tnt")) + run_tnt <- function(lines) { writeLines(lines, file.path(wd, "runme.run")) + old <- setwd(wd); o <- suppressWarnings(system2(TNT, "runme.run;", stdout = TRUE, stderr = TRUE)) + setwd(old); iconv(o, from = "", to = "UTF-8", sub = "") } + best <- function(lines) min(num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", run_tnt(lines), value = TRUE)))) + # Build T0 SET (hold 1000) and a single T0 file + run_tnt(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1000;","mult=replic 1;", + "tsave *set.tre;","save;","tsave/;","quit;")) + L <- readLines(file.path(wd, "set.tre")) + writeLines(c(L[1], paste0(sub("[*]$","",L[2]),";"), "proc-;"), file.path(wd, "tee.tre")) + start_n <- length(grep("[*]", L)) + 1L + ss <- sapply(SEEDS, function(s) best(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc tee.tre;","sectsch: noglobal noequals;", rep("sectsch=rss;",RDS),"quit;"))) + se <- sapply(SEEDS, function(s) best(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc tee.tre;","sectsch: equals;", rep("sectsch=rss;",RDS),"quit;"))) + set <- sapply(SEEDS, function(s) best(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc set.tre;","sectsch: noglobal noequals;", rep("sectsch=rss;",RDS),"quit;"))) + setd <- sapply(SEEDS, function(s) best(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc set.tre;", rep("sectsch=rss;",RDS),"quit;"))) # DEFAULT (noequals) on set + f <- function(v) sprintf("med=%g [%g-%g]", median(v), min(v), max(v)) + cat(sprintf("\n==== %s (target %d, %d-tree start set) ====\n", nm, target[[nm]], start_n)) + cat(sprintf(" SINGLE-T0 strict : %s\n", f(ss))) + cat(sprintf(" SINGLE-T0 equals : %s\n", f(se))) + cat(sprintf(" SET strict : %s\n", f(set))) + cat(sprintf(" SET default(TNT) : %s\n", f(setd))) +} diff --git a/dev/benchmarks/tnt_bare/driver1.R b/dev/benchmarks/tnt_bare/driver1.R new file mode 100644 index 000000000..c28181815 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driver1.R @@ -0,0 +1,17 @@ +source("dev/benchmarks/tnt_bare/harness.R") + +# Sanity: read T0 fresh and report TNT's own score for it. +chk <- run_tnt(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1000;", + "proc tee.tre;", "score;", "quit;")) +cat("---- start-tree score lines (raw) ----\n") +cat(paste0(" ", trimws(grep("score|Tree|length|1271|1275", chk, ignore.case = TRUE, + value = TRUE))), sep = "\n") + +cat("\n\n======== EXPERIMENT BATCH 1 ========\n") +# Bare-bones: strip global TBR, equal-acceptance, fuse, drift +bare <- run_config("noglobal noequals nofuse godrift 9999", rounds = 12, label = "BARE") +print_config(bare) + +# TNT default (no settings changed) for reference +def <- run_config("", rounds = 12, label = "DEFAULT") +print_config(def) diff --git a/dev/benchmarks/tnt_bare/driver2.R b/dev/benchmarks/tnt_bare/driver2.R new file mode 100644 index 000000000..f99d11e30 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driver2.R @@ -0,0 +1,41 @@ +source("dev/benchmarks/tnt_bare/harness.R") +# Parse helper: pull all RSS best scores + final TreeLength from a raw TNT run that +# writes finalt.tre. +rss_bests <- function(out) num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) +score_final <- function() { # MIN TreeLength over ALL saved trees (best in memory) + ff <- file.path(wd, "finalt.tre"); if (!file.exists(ff)) return(NA) + tr <- tryCatch(ReadTntTree(ff), error = function(e) NULL); if (is.null(tr)) return(NA) + if (!inherits(tr, "multiPhylo")) tr <- structure(list(tr), class = "multiPhylo") + tryCatch(min(vapply(tr, function(x) TreeLength(x, phy), numeric(1))), error = function(e) NA) +} +runblk <- function(lines) { out <- run_tnt(c(lines, "tsave *finalt.tre;","save;","tsave/;","quit;")) + list(bests = rss_bests(out), TL = score_final()) } +file.copy(file.path(bare, paste0(nm, ".t0.tre")), file.path(wd, "set.tre"), overwrite = TRUE) # 10-tree 1271 set + +cat("==== A: SINGLE 1271 tree, various knobs (hold 1000) ====\n") +for (cfg in c("", "noglobal noequals", "equals", "global 1", "equals global 1")) { + r <- runblk(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1000;","proc tee.tre;", + if (nzchar(cfg)) sprintf("sectsch: %s;", cfg) else character(0), + rep("sectsch=rss;", 12))) + cat(sprintf(" [%-22s] rounds: %s | final TL=%s\n", cfg, + paste(r$bests, collapse=" "), format(r$TL))) +} + +cat("\n==== B: 10-tree 1271 SET start (hold 1000) ====\n") +for (cfg in c("", "noglobal noequals", "equals")) { + r <- runblk(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1000;","proc set.tre;", + if (nzchar(cfg)) sprintf("sectsch: %s;", cfg) else character(0), + rep("sectsch=rss;", 12))) + cat(sprintf(" [%-22s] rounds: %s | final TL=%s\n", cfg, + paste(r$bests, collapse=" "), format(r$TL))) +} + +cat("\n==== C: in-memory hold-1 mult (=1275) then sectsch (reproduce prior seq_accum) ====\n") +for (cfg in c("", "noglobal noequals", "equals")) { + r <- runblk(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1;","mult=replic 1;", + if (nzchar(cfg)) sprintf("sectsch: %s;", cfg) else character(0), + rep("sectsch=rss;", 12))) + cat(sprintf(" [%-22s] rounds: %s | final TL=%s\n", cfg, + paste(r$bests, collapse=" "), format(r$TL))) +} diff --git a/dev/benchmarks/tnt_bare/driver3.R b/dev/benchmarks/tnt_bare/driver3.R new file mode 100644 index 000000000..525ff6764 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driver3.R @@ -0,0 +1,31 @@ +source("dev/benchmarks/tnt_bare/harness.R") +file.copy(file.path(bare, paste0(nm, ".t0.tre")), file.path(wd, "set.tre"), overwrite = TRUE) +runblk <- function(lines) { out <- run_tnt(c(lines, "tsave *finalt.tre;","save;","tsave/;","quit;")) + list(bests = rss_bests(out), TL = score_final(), n = n_trees()) } + +cat("==== CHECK 1: TNT's ACTUAL DEFAULT pipeline (noequals throughout) ====\n") +cat(" proc; rseed 1; hold H; mult=replic 1; then sectsch=rss to plateau\n") +for (h in c(1, 1000)) { + r <- runblk(c("mxram 1024;","proc data.tnt;","rseed 1;",sprintf("hold %d;",h), + "mult=replic 1;", rep("sectsch=rss;",16))) + cat(sprintf(" hold=%-4d : best=%s ntrees=%d rounds: %s\n", h, format(min(r$bests)), + r$n, paste(r$bests, collapse=" "))) +} + +cat("\n==== CHECK 2: variety accumulation, single T0 (A): trees-in-memory after k rounds ====\n") +for (cfg in c("noglobal noequals", "equals")) { + cat(sprintf(" -- %s --\n", cfg)) + for (k in c(1,2,4,8,12)) { + r <- runblk(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1000;","proc tee.tre;", + sprintf("sectsch: %s;",cfg), rep("sectsch=rss;",k))) + cat(sprintf(" k=%2d : best=%s ntrees=%d\n", k, format(min(r$bests)), r$n)) + } +} + +cat("\n==== CHECK 3: SET + STRICT — is it fusing or multi-tree sectorial? ====\n") +for (cfg in c("noglobal noequals", "noglobal noequals nofuse", "noglobal noequals tree 0")) { + r <- runblk(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1000;","proc set.tre;", + sprintf("sectsch: %s;",cfg), rep("sectsch=rss;",12))) + cat(sprintf(" [%-26s] best=%s ntrees=%d rounds: %s\n", cfg, format(min(r$bests)), + r$n, paste(r$bests, collapse=" "))) +} diff --git a/dev/benchmarks/tnt_bare/driver4.R b/dev/benchmarks/tnt_bare/driver4.R new file mode 100644 index 000000000..3c9684b51 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driver4.R @@ -0,0 +1,38 @@ +source("dev/benchmarks/tnt_bare/harness.R") +runblk <- function(lines) { out <- run_tnt(c(lines, "tsave *finalt.tre;","save;","tsave/;","quit;")) + list(bests = rss_bests(out), TL = score_final(), n = n_trees(), + err = grep("rror|nvalid|nrecogni", out, value = TRUE)) } + +# ---- Build a 10-IDENTICAL-copies set (tree 0 x10) to control for tree COUNT vs DIVERSITY ---- +L <- readLines(file.path(bare, paste0(nm, ".t0.tre"))) +tree0 <- sub("[*]$", "", L[2]) +ident <- c(L[1], paste0(rep(paste0(tree0, "*"), 9), collapse = "\n"), paste0(tree0, ";"), "proc-;") +writeLines(ident, file.path(wd, "ident.tre")) +file.copy(file.path(bare, paste0(nm, ".t0.tre")), file.path(wd, "set.tre"), overwrite = TRUE) + +cat("==== VARIETY CONTROL: 10 IDENTICAL copies vs 10 DIFFERENT trees (strict sectsch) ====\n") +for (src in c("ident.tre", "set.tre")) { + r <- runblk(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1000;",sprintf("proc %s;",src), + "sectsch: noglobal noequals;", rep("sectsch=rss;",12))) + cat(sprintf(" [%-10s] best=%s ntrees(end)=%d rounds: %s\n", src, format(min(r$bests)), + r$n, paste(r$bests, collapse=" "))) +} + +cat("\n==== `tree 0` behaviour check (did it error / restrict?) ====\n") +r <- runblk(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1000;","proc set.tre;", + "sectsch: noglobal noequals tree 0;", rep("sectsch=rss;",3))) +cat(sprintf(" errors: %s\n", if (length(r$err)) paste(unique(trimws(r$err)),collapse=" | ") else "")) + +cat("\n==== SEED ROBUSTNESS ====\n") +cat("-- Canonical default pipeline (mult + sectsch, noequals), hold 1 vs 1000 --\n") +for (h in c(1, 1000)) for (s in 1:3) { + r <- runblk(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s),sprintf("hold %d;",h), + "mult=replic 1;", rep("sectsch=rss;",16))) + cat(sprintf(" hold=%-4d seed=%d : best=%s ntrees=%d\n", h, s, format(min(r$bests)), r$n)) +} +cat("-- Single T0 (fixed seed-1 tree), vary sectsch rseed --\n") +for (cfg in c("noglobal noequals", "equals")) for (s in 1:3) { + r <- runblk(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s),"hold 1000;","proc tee.tre;", + sprintf("sectsch: %s;",cfg), rep("sectsch=rss;",12))) + cat(sprintf(" [%-18s] sectsch-seed=%d : best=%s ntrees=%d\n", cfg, s, format(min(r$bests)), r$n)) +} diff --git a/dev/benchmarks/tnt_bare/driver5.R b/dev/benchmarks/tnt_bare/driver5.R new file mode 100644 index 000000000..d2fb3ac03 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driver5.R @@ -0,0 +1,24 @@ +source("dev/benchmarks/tnt_bare/harness.R") +file.copy(file.path(bare, paste0(nm, ".t0.tre")), file.path(wd, "set.tre"), overwrite = TRUE) +runbest <- function(lines) { out <- run_tnt(c(lines,"tsave *finalt.tre;","save;","tsave/;","quit;")) + min(rss_bests(out)) } +summ <- function(v) sprintf("min=%g median=%g max=%g {%s}", min(v), median(v), max(v), paste(v,collapse=",")) +SEEDS <- 1:6 + +cat("==== Q1: plateau distribution over seeds (30 rounds) ====\n") +# Single fixed T0, vary sectsch seed +for (cfg in c("noglobal noequals", "equals")) { + v <- sapply(SEEDS, function(s) runbest(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc tee.tre;",sprintf("sectsch: %s;",cfg), rep("sectsch=rss;",30)))) + cat(sprintf(" SINGLE-T0 [%-18s] %s\n", cfg, summ(v))) +} +# Fixed diverse SET (seed-1), vary sectsch seed, strict +v <- sapply(SEEDS, function(s) runbest(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc set.tre;","sectsch: noglobal noequals;", rep("sectsch=rss;",30)))) +cat(sprintf(" SET(10div) [%-18s] %s\n", "noglobal noequals", v |> summ())) + +cat("\n==== Q2: DIVERSITY vs EFFORT — single-T0 strict with 10x rounds (300) ====\n") +cat(" (if effort alone reached 1261, single-strict@300 ~ set-strict@30)\n") +v <- sapply(1:4, function(s) runbest(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc tee.tre;","sectsch: noglobal noequals;", rep("sectsch=rss;",300)))) +cat(sprintf(" SINGLE-T0 strict @300 rounds, seeds 1-4: %s\n", summ(v))) diff --git a/dev/benchmarks/tnt_bare/driver6.R b/dev/benchmarks/tnt_bare/driver6.R new file mode 100644 index 000000000..17a23ef42 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driver6.R @@ -0,0 +1,22 @@ +source("dev/benchmarks/tnt_bare/harness.R") +# Lucky-tree control: does ANY single tree of the 10-tree 1271 set reach target SOLO under +# strict sectsch? If not, the set's escape needs cross-set variety, not one lucky member. +L <- readLines(file.path(bare, paste0(nm, ".t0.tre"))) +trees <- grep("^\\(", L, value = TRUE) # the 10 newick lines (some end '*', last ';') +trees <- sub("[*;]$", "", trees) +runbest <- function(lines) { out <- run_tnt(c(lines,"quit;")); min(rss_bests(out)) } + +cat(sprintf("==== %s: each of %d set trees, SOLO single-tree strict sectsch (seeds 1-3, 30 rounds) ====\n", + nm, length(trees))) +solo <- numeric(length(trees)) +for (i in seq_along(trees)) { + writeLines(c("tread 'solo'", paste0(trees[i], ";"), "proc-;"), file.path(wd, "solo.tre")) + v <- sapply(1:3, function(s) runbest(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc solo.tre;","sectsch: noglobal noequals;", rep("sectsch=rss;",30)))) + solo[i] <- min(v) + cat(sprintf(" tree %2d solo strict best (over seeds): %g {%s}\n", i, min(v), paste(v,collapse=","))) +} +cat(sprintf("\n BEST any-single-tree-solo = %g | SET-strict reaches 1261/target\n", min(solo))) +cat(sprintf(" => %s\n", if (min(solo) > 1261) + "no single member reaches target solo: cross-set variety IS the mechanism" + else "a single member reaches target solo: 'lucky tree', re-examine")) diff --git a/dev/benchmarks/tnt_bare/driver7.R b/dev/benchmarks/tnt_bare/driver7.R new file mode 100644 index 000000000..67f40c14e --- /dev/null +++ b/dev/benchmarks/tnt_bare/driver7.R @@ -0,0 +1,29 @@ +source("dev/benchmarks/tnt_bare/harness.R") +# Independent-parallel ("10 random starts, take best") vs SHARED-buffer (the real set run). +# Same seed (1), same rounds (30), strict noequals throughout. If set < min(independent solos), +# the 10 tracks are NOT independent -- they combine through TNT's single shared tree buffer. +L <- readLines(file.path(bare, paste0(nm, ".t0.tre"))) +trees <- sub("[*;]$", "", grep("^\\(", L, value = TRUE)) +best <- function(lines) min(rss_bests(run_tnt(c(lines, "quit;")))) +strict <- function(src, h = 1000) c("mxram 1024;","proc data.tnt;","rseed 1;", + sprintf("hold %d;", h), sprintf("proc %s;", src), + "sectsch: noglobal noequals;", rep("sectsch=rss;", 30)) + +# (1) each of the 10 trees SOLO, seed 1, 30 rounds -> independent-parallel baseline +solo <- numeric(length(trees)) +for (i in seq_along(trees)) { + writeLines(c("tread 'solo'", paste0(trees[i], ";"), "proc-;"), file.path(wd, "solo.tre")) + solo[i] <- best(strict("solo.tre")) +} +cat(sprintf("INDEPENDENT (10 solos, seed1, 30 rnds): each = {%s}\n", paste(solo, collapse=","))) +cat(sprintf(" -> best-of-10 independent = %g\n", min(solo))) + +# (2) the 10-tree SET in one shared buffer, seed 1, 30 rounds +file.copy(file.path(bare, paste0(nm, ".t0.tre")), file.path(wd, "set.tre"), overwrite = TRUE) +set_best <- best(strict("set.tre")) +cat(sprintf("SHARED (10-tree set, seed1, 30 rnds) = %g\n", set_best)) + +cat(sprintf("\nVERDICT: %s (independent-best %g vs shared %g)\n", + if (set_best < min(solo)) "SHARED beats best-independent -> tracks COMBINE, not just parallel" + else "shared == best-independent -> consistent with mere parallel restarts", + min(solo), set_best)) diff --git a/dev/benchmarks/tnt_bare/driver8.R b/dev/benchmarks/tnt_bare/driver8.R new file mode 100644 index 000000000..c57f556d6 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driver8.R @@ -0,0 +1,24 @@ +source("dev/benchmarks/tnt_bare/harness.R") +# Trace the BUFFER across rounds: how many trees, and the spread of their lengths. +# Distinguishes (a) 10 slots independently descending vs (b) buffer collapsing to best & +# re-seeding. Strict noequals, seed 1. +file.copy(file.path(bare, paste0(nm, ".t0.tre")), file.path(wd, "set.tre"), overwrite = TRUE) +L <- readLines(file.path(bare, paste0(nm, ".t0.tre"))) +writeLines(c("tread 'solo'", paste0(sub("[*;]$","",grep("^\\(",L,value=TRUE))[1],";"), "proc-;"), + file.path(wd, "solo.tre")) + +trace_buffer <- function(src, ks = c(0,1,2,4,8,16,30)) { + for (k in ks) { + out <- run_tnt(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1000;", + sprintf("proc %s;", src), + "sectsch: noglobal noequals;", + if (k > 0) rep("sectsch=rss;", k) else character(0), + "tsave *ft.tre;","save;","tsave/;","quit;")) + tr <- read_trees(file.path(wd, "ft.tre")) + sc <- if (is.null(tr)) NA else vapply(tr, function(x) TreeLength(x, phy), numeric(1)) + cat(sprintf(" k=%2d : ntrees=%2d lengths: min=%g max=%g distinct={%s}\n", + k, length(sc), min(sc), max(sc), paste(sort(unique(sc)), collapse=","))) + } +} +cat("==== SET (10 diverse trees) buffer trace ====\n"); trace_buffer("set.tre") +cat("\n==== SOLO (1 tree) buffer trace ====\n"); trace_buffer("solo.tre") diff --git a/dev/benchmarks/tnt_bare/driver9.R b/dev/benchmarks/tnt_bare/driver9.R new file mode 100644 index 000000000..efd211002 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driver9.R @@ -0,0 +1,32 @@ +source("dev/benchmarks/tnt_bare/harness.R") +# Is the set just "restart from 10 diverse trees, take best" (no sharing), or does the shared +# population reach something independent restarts cannot? Give each tree its OWN seeds (proper +# independent-restart baseline) and pour on restarts; compare best to the set's 1261. +L <- readLines(file.path(bare, paste0(nm, ".t0.tre"))) +trees <- sub("[*;]$", "", grep("^\\(", L, value = TRUE)) +best <- function(lines) min(rss_bests(run_tnt(c(lines, "quit;")))) +solo_best <- function(i, s) { writeLines(c("tread 'solo'", paste0(trees[i], ";"), "proc-;"), + file.path(wd, "solo.tre")) + best(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s),"hold 1000;","proc solo.tre;", + "sectsch: noglobal noequals;", rep("sectsch=rss;",30))) } + +SEEDS <- 1:5 +M <- outer(seq_along(trees), SEEDS, Vectorize(function(i,s) solo_best(i,s))) +rownames(M) <- paste0("tree", seq_along(trees)); colnames(M) <- paste0("s", SEEDS) +cat("Per-tree solo strict best (rows=tree, cols=seed):\n") +print(M) +diag_seed <- sapply(seq_along(trees), function(i) M[i, ((i-1) %% length(SEEDS))+1]) +cat(sprintf("\n'10 independent restarts (tree i, its own seed)' best = %g\n", min(diag_seed))) +cat(sprintf("BEST over ALL %d independent solo runs = %g\n", length(M), min(M))) +cat(sprintf("Number of independent runs that reached <=1261 = %d / %d\n", + sum(M <= 1261), length(M))) + +# the shared set, matched seeds +file.copy(file.path(bare, paste0(nm, ".t0.tre")), file.path(wd, "set.tre"), overwrite = TRUE) +set_best <- sapply(SEEDS, function(s) best(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc set.tre;","sectsch: noglobal noequals;", rep("sectsch=rss;",30)))) +cat(sprintf("\nSHARED 10-tree set best over seeds 1-5 = %g {%s}\n", + min(set_best), paste(set_best, collapse=","))) +cat(sprintf("\nVERDICT: %s\n", if (min(M) <= min(set_best)) + "independent restarts MATCH the set -> it's 'restart from diverse trees, take best', NOT sharing" + else "set BEATS all independent restarts -> genuine population synergy / sharing")) diff --git a/dev/benchmarks/tnt_bare/driverA.R b/dev/benchmarks/tnt_bare/driverA.R new file mode 100644 index 000000000..53fa2bc04 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driverA.R @@ -0,0 +1,30 @@ +source("dev/benchmarks/tnt_bare/harness.R") +L <- readLines(file.path(bare, paste0(nm, ".t0.tre"))) +trees <- sub("[*;]$", "", grep("^\\(", L, value = TRUE)) +best <- function(lines) min(rss_bests(run_tnt(c(lines, "quit;")))) +strict_rounds <- function(src, s, R = 30) c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;", sprintf("proc %s;", src), "sectsch: noglobal noequals;", rep("sectsch=rss;", R)) + +# ---- TEST 1: can ABUNDANT independent restarts reach 1261? (is it 'just restarts'?) ---- +# 10 trees x 20 seeds = 200 independent single-tree strict runs. +NS <- 20 +allbest <- 9999; hit <- 0; n <- 0 +for (i in seq_along(trees)) { + writeLines(c("tread 'solo'", paste0(trees[i], ";"), "proc-;"), file.path(wd, "solo.tre")) + for (s in 1:NS) { b <- best(strict_rounds("solo.tre", s)); allbest <- min(allbest, b) + hit <- hit + (b <= 1261); n <- n + 1 } +} +cat(sprintf("TEST1 %d independent solo restarts: best=%g, #reaching<=1261 = %d/%d\n", n, allbest, hit, n)) + +# ---- TEST 2: isolate COUPLING from diversity. Same single tree, 10 copies. ---- +# (a) 10 copies in ONE shared buffer (strict) vs (b) 10 independent restarts of that tree. +ti <- 1 # tree1 is frozen solo at seed1 +writeLines(c("tread 'solo'", paste0(trees[ti], ";"), "proc-;"), file.path(wd, "solo.tre")) +ident <- c(L[1], paste0(rep(paste0(trees[ti], "*"), 9), collapse = "\n"), paste0(trees[ti], ";"), "proc-;") +writeLines(ident, file.path(wd, "ident.tre")) +for (s in 1:5) { + shared <- best(strict_rounds("ident.tre", s)) # 10 identical copies, shared buffer + indep <- min(sapply(1:10, function(ss) best(strict_rounds("solo.tre", (s-1)*10+ss)))) # 10 separate restarts + cat(sprintf("TEST2 seed-block %d : 10-copies-shared-buffer=%g vs 10-separate-restarts=%g\n", + s, shared, indep)) +} diff --git a/dev/benchmarks/tnt_bare/driverB.R b/dev/benchmarks/tnt_bare/driverB.R new file mode 100644 index 000000000..9c6777b2d --- /dev/null +++ b/dev/benchmarks/tnt_bare/driverB.R @@ -0,0 +1,29 @@ +source("dev/benchmarks/tnt_bare/harness.R") +# DECISIVE paired test of the user's model: "10 lanes, sectorial within each, pick best." +# For each replicate s: SET (10 trees together, 30 rds) vs 10-INDEPENDENT (same 10 trees, each +# solo at a distinct seed, take min). Equal compute (both = 10 trees x 30 rds). Many replicates. +L <- readLines(file.path(bare, paste0(nm, ".t0.tre"))) +trees <- sub("[*;]$", "", grep("^\\(", L, value = TRUE)) +file.copy(file.path(bare, paste0(nm, ".t0.tre")), file.path(wd, "set.tre"), overwrite = TRUE) +best <- function(lines) min(rss_bests(run_tnt(c(lines, "quit;")))) +strict <- function(src, s) c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s),"hold 1000;", + sprintf("proc %s;", src), "sectsch: noglobal noequals;", rep("sectsch=rss;", 30)) + +REPS <- 15 +set_min <- indep_min <- numeric(REPS) +for (s in 1:REPS) { + set_min[s] <- best(strict("set.tre", s)) + # 10 independent lanes: tree i at its own distinct seed; take the best (min) lane + solo <- numeric(length(trees)) + for (i in seq_along(trees)) { + writeLines(c("tread 'solo'", paste0(trees[i], ";"), "proc-;"), file.path(wd, "solo.tre")) + solo[i] <- best(strict("solo.tre", (s-1)*length(trees) + i)) + } + indep_min[s] <- min(solo) + cat(sprintf(" rep %2d : SET=%g 10-INDEP-min=%g %s\n", s, set_min[s], indep_min[s], + if (set_min[s] < indep_min[s]) "set indep_min[s]) "set>indep" else "tie")) +} +cat(sprintf("\nSET reached 1261 in %d/%d reps (median %g)\n", sum(set_min<=1261), REPS, median(set_min))) +cat(sprintf("10-INDEP reached 1261 in %d/%d reps (median %g)\n", sum(indep_min<=1261), REPS, median(indep_min))) +cat(sprintf("paired: setindep %d\n", + sum(set_minindep_min))) diff --git a/dev/benchmarks/tnt_bare/driverC.R b/dev/benchmarks/tnt_bare/driverC.R new file mode 100644 index 000000000..0053c0759 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driverC.R @@ -0,0 +1,24 @@ +source("dev/benchmarks/tnt_bare/harness.R") +# Mechanism test: is the set's advantage just LARGER SECTORS (shared size-increase counter +# advances faster with 10 trees), not info transfer? Force a SINGLE tree to use big sectors. +L <- readLines(file.path(bare, paste0(nm, ".t0.tre"))) +file.copy(file.path(bare, paste0(nm, ".t0.tre")), file.path(wd, "set.tre"), overwrite = TRUE) +best <- function(lines) min(rss_bests(run_tnt(c(lines, "quit;")))) +SEEDS <- 1:6 +hit <- function(v) sprintf("1261 in %d/%d, med=%g [%g-%g]", sum(v<=1261), length(v), median(v), min(v), max(v)) + +cat("== SINGLE tree, strict, forced sector size minsize=maxsize=K (30 rounds) ==\n") +for (K in c(37, 45, 55, 65, 70)) { + v <- sapply(SEEDS, function(s) best(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc tee.tre;", + sprintf("sectsch: noglobal noequals minsize %d maxsize %d;", K, K), + rep("sectsch=rss;",30)))) + cat(sprintf(" size=%2d : %s\n", K, hit(v))) +} +cat("\n== references ==\n") +v <- sapply(SEEDS, function(s) best(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc tee.tre;","sectsch: noglobal noequals;", rep("sectsch=rss;",30)))) +cat(sprintf(" SINGLE default size : %s\n", hit(v))) +v <- sapply(SEEDS, function(s) best(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc set.tre;","sectsch: noglobal noequals;", rep("sectsch=rss;",30)))) +cat(sprintf(" SET (10 trees) : %s\n", hit(v))) diff --git a/dev/benchmarks/tnt_bare/driverD.R b/dev/benchmarks/tnt_bare/driverD.R new file mode 100644 index 000000000..0451a8a27 --- /dev/null +++ b/dev/benchmarks/tnt_bare/driverD.R @@ -0,0 +1,23 @@ +source("dev/benchmarks/tnt_bare/harness.R") +file.copy(file.path(bare, paste0(nm, ".t0.tre")), file.path(wd, "set.tre"), overwrite = TRUE) +best <- function(lines) min(rss_bests(run_tnt(c(lines, "quit;")))) +hit <- function(v) sprintf("1261 in %d/%d, med=%g [%g-%g]", sum(v<=1261), length(v), median(v), min(v), max(v)) + +# (1) Does TNT's sector SIZE escalate across sectsch=rss commands? Dump settings between rounds. +cat("==== TNT size schedule: settings between successive sectsch=rss rounds (single tree) ====\n") +out <- run_tnt(c("mxram 1024;","proc data.tnt;","rseed 1;","hold 1000;","proc tee.tre;", + "sect:;", "sectsch=rss;", "sect:;", "sectsch=rss;", "sect:;", "sectsch=rss;", + "sect:;", "quit;")) +size_lines <- grep("size|sectors of|selections", out, ignore.case = TRUE, value = TRUE) +cat(paste0(" ", trimws(size_lines)), sep = "\n") + +# (2) Does the escalating schedule matter, or is fixed n/2 enough? SET, fixed vs default size. +cat("\n==== SET (10 trees): fixed sector size vs default(escalating) schedule, seeds 1-6 ====\n") +SEEDS <- 1:6 +for (cfg in c("minsize 37 maxsize 37", "minsize 37 maxsize 37 increase 0", "")) { + v <- sapply(SEEDS, function(s) best(c("mxram 1024;","proc data.tnt;",sprintf("rseed %d;",s), + "hold 1000;","proc set.tre;", + paste0("sectsch: noglobal noequals", if (nzchar(cfg)) paste0(" ", cfg) else ""), ";", + rep("sectsch=rss;",30)))) + cat(sprintf(" [%-32s] %s\n", if (nzchar(cfg)) cfg else "default(escalating)", hit(v))) +} diff --git a/dev/benchmarks/tnt_bare/harness.R b/dev/benchmarks/tnt_bare/harness.R new file mode 100644 index 000000000..9c2dbdbd1 --- /dev/null +++ b/dev/benchmarks/tnt_bare/harness.R @@ -0,0 +1,75 @@ +# Reusable TNT sectsch harness. +# Reads a FIXED single-tree T0 fresh (no mult/TBR before sectsch), +# applies a sectsch config, runs N rounds of `sectsch=rss;`, capturing the +# running best score after each round + the final score. TNT score is +# authoritative; the final tree is also re-scored with TreeLength as a +# mapping sanity check. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +bare <- "dev/benchmarks/tnt_bare" +nm <- Sys.getenv("DS", "Zanol2014") +phy <- readRDS(file.path(bare, paste0(nm, ".phy.rds"))) +t0file <- file.path(bare, paste0(nm, ".t0single.tre")) +num <- function(x) suppressWarnings(as.double(gsub(",", "", x))) + +# One reusable working dir per process +wd <- file.path(tempdir(), paste0("hn", Sys.getpid(), nm)); unlink(wd, recursive = TRUE) +dir.create(wd, recursive = TRUE, showWarnings = FALSE) +WriteTntCharacters(phy, file.path(wd, "data.tnt")) +file.copy(t0file, file.path(wd, "tee.tre"), overwrite = TRUE) + +rss_bests <- function(out) num(sub(".*best score:\\s*([0-9.]+).*", "\\1", + grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE))) +read_trees <- function(ff) { + if (!file.exists(ff)) return(NULL) + tr <- tryCatch(ReadTntTree(ff), error = function(e) NULL); if (is.null(tr)) return(NULL) + if (!inherits(tr, "multiPhylo")) tr <- structure(list(tr), class = "multiPhylo"); tr +} +score_final <- function(ff = file.path(wd, "finalt.tre")) { # MIN TreeLength over saved trees + tr <- read_trees(ff); if (is.null(tr)) return(NA) + tryCatch(min(vapply(tr, function(x) TreeLength(x, phy), numeric(1))), error = function(e) NA) +} +n_trees <- function(ff = file.path(wd, "finalt.tre")) { tr <- read_trees(ff); if (is.null(tr)) NA else length(tr) } + +run_tnt <- function(lines) { + rf <- file.path(wd, "runme.run") + writeLines(lines, rf) + old <- setwd(wd) + out <- suppressWarnings(system2(TNT, args = "runme.run;", stdout = TRUE, stderr = TRUE)) + setwd(old) + iconv(out, from = "", to = "UTF-8", sub = "") +} + +# Run a config: setting_line is the `sectsch: ...;` options (may be ""), rounds = #sectsch=rss +run_config <- function(setting_line, rounds = 8, seed = 1, hold = 1000, label = "") { + pre <- c("mxram 1024;", "proc data.tnt;", sprintf("rseed %d;", seed), + sprintf("hold %d;", hold), "proc tee.tre;") + if (nzchar(setting_line)) pre <- c(pre, sprintf("sectsch: %s;", setting_line)) + body <- as.vector(rbind(rep("sectsch = rss;", rounds), + rep("tplot/;", 0))) # placeholder, removed below + body <- rep("sectsch = rss;", rounds) + lines <- c(pre, "score;", body, "score;", "tsave *finalt.tre;", "save;", "tsave/;", "quit;") + out <- run_tnt(lines) + # Parse every "best score:" from RSS, plus start/end "score;" outputs + best_lines <- grep("Sectorial search \\(RSS\\), best score:", out, value = TRUE) + bests <- num(sub(".*best score:\\s*([0-9.]+).*", "\\1", best_lines)) + # final tree score via TreeLength (mapping check) + tl <- NA + ff <- file.path(wd, "finalt.tre") + if (file.exists(ff)) { + tr <- tryCatch(ReadTntTree(ff), error = function(e) NULL) + if (!is.null(tr)) { if (inherits(tr, "multiPhylo")) tr <- tr[[1]] + tl <- tryCatch(min(TreeLength(tr, phy)), error = function(e) NA) } + } + list(label = label, setting = setting_line, rounds = rounds, seed = seed, hold = hold, + per_round = bests, final_tnt = if (length(bests)) min(bests) else NA, final_TL = tl) +} + +print_config <- function(r) { + cat(sprintf("\n[%s] hold=%d seed=%d '%s'\n", r$label, r$hold, r$seed, r$setting)) + cat(sprintf(" per-round best: %s\n", paste(r$per_round, collapse = " "))) + cat(sprintf(" FINAL TNT=%s TreeLength=%s\n", format(r$final_tnt), format(r$final_TL))) +} diff --git a/dev/benchmarks/tnt_bare/make_single.R b/dev/benchmarks/tnt_bare/make_single.R new file mode 100644 index 000000000..d9e4514f1 --- /dev/null +++ b/dev/benchmarks/tnt_bare/make_single.R @@ -0,0 +1,16 @@ +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), winslash = "/")) + library(TreeTools) +}) +bare <- "dev/benchmarks/tnt_bare" +nm <- Sys.getenv("DS", "Zanol2014") +phy <- readRDS(file.path(bare, paste0(nm, ".phy.rds"))) +L <- readLines(file.path(bare, paste0(nm, ".t0.tre"))) # the 1271 set (hold 1000) +first <- sub("[*]$", "", L[2]) # first tree, drop trailing '*' +writeLines(c("tread 'single T0 = tree1 of best set'", paste0(first, ";"), "proc-;"), + file.path(bare, paste0(nm, ".t0single.tre"))) +t <- ReadTntTree(file.path(bare, paste0(nm, ".t0single.tre"))) +if (inherits(t, "multiPhylo")) t <- t[[1]] +t <- RootTree(t, t$tip.label[1]) +cat(sprintf("%s single-tree T0 score (TreeLength) = %.0f tips=%d\n", + nm, TreeLength(t, phy), length(t$tip.label))) diff --git a/dev/benchmarks/tnt_bare/setup.R b/dev/benchmarks/tnt_bare/setup.R new file mode 100644 index 000000000..3222c1098 --- /dev/null +++ b/dev/benchmarks/tnt_bare/setup.R @@ -0,0 +1,32 @@ +# Setup: generate Zanol Fitch matrix + RDS, build fixed T0 (TNT mult replic 1, rseed 1). +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband"), winslash = "/")) + library(TreeTools) +}) +TNT <- Sys.getenv("TNT_EXE", "C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe") +data("inapplicable.phyData", package = "TreeSearch") +fitch <- function(p) { m <- PhyDatToMatrix(p, ambigNA = FALSE); m[m == "-"] <- "?"; MatrixToPhyDat(m) } + +bare <- "dev/benchmarks/tnt_bare" +nm <- Sys.getenv("DS", "Zanol2014") +phy <- fitch(inapplicable.phyData[[nm]]) +WriteTntCharacters(phy, file.path(bare, paste0(nm, ".tnt"))) +saveRDS(phy, file.path(bare, paste0(nm, ".phy.rds"))) +cat(sprintf("%s: %d tips, %d chars\n", nm, length(phy), attr(phy, "nr"))) + +# Build a fixed T0 with a single mult replicate (rseed 1). This is our fixture. +wd <- file.path(tempdir(), paste0("t0", Sys.getpid(), nm)); unlink(wd, recursive = TRUE) +dir.create(wd, recursive = TRUE, showWarnings = FALSE) +WriteTntCharacters(phy, file.path(wd, "data.tnt")) +writeLines(c("mxram 1024;", "proc data.tnt;", "rseed 1;", "hold 1000;", + "mult=replic 1;", "tsave *t0.tre;", "save;", "tsave/;", "quit;"), + file.path(wd, "buildtee.run")) +old <- setwd(wd) +out <- suppressWarnings(system2(TNT, args = "buildtee.run;", stdout = TRUE, stderr = TRUE)) +setwd(old) +file.copy(file.path(wd, "t0.tre"), file.path(bare, paste0(nm, ".t0.tre")), overwrite = TRUE) +t0 <- ReadTntTree(file.path(bare, paste0(nm, ".t0.tre"))) +if (inherits(t0, "multiPhylo")) t0 <- t0[[1]] +cat(sprintf("T0 (mult replic 1, rseed 1) score = %.0f tips=%d\n", TreeLength(t0, phy), length(t0$tip.label))) +out <- iconv(out, from = "", to = "UTF-8", sub = "") +cat("TNT mult lines:\n"); cat(paste0(" ", grep("score", out, ignore.case = TRUE, value = TRUE)), sep = "\n") diff --git a/dev/benchmarks/tnt_bench_new_ts.csv b/dev/benchmarks/tnt_bench_new_ts.csv new file mode 100644 index 000000000..fd7260e9c --- /dev/null +++ b/dev/benchmarks/tnt_bench_new_ts.csv @@ -0,0 +1,43 @@ +"dataset","n_taxa","n_chars","seed","timeout_s","ts_score","ts_trees","ts_wall_s","ts_reps","ts_hits" +"Longrich2010",20,93,1,10,131,100,0.11,11,3 +"Longrich2010",20,93,2,10,131,90,0.41,8,2 +"Longrich2010",20,93,3,10,131,100,0.09,8,2 +"Vinther2008",23,57,1,10,78,66,0.34,7,3 +"Vinther2008",23,57,2,10,78,66,0.33,19,4 +"Vinther2008",23,57,3,10,78,66,0.36,13,4 +"Sansom2010",23,109,1,10,188,1,0.25,20,1 +"Sansom2010",23,109,2,10,188,1,0.23,11,2 +"Sansom2010",23,109,3,10,189,31,0.22,11,3 +"DeAssis2011",33,50,1,10,64,100,0.14,5,5 +"DeAssis2011",33,50,2,10,64,100,0.13,5,5 +"DeAssis2011",33,50,3,10,64,100,0.12,5,5 +"Aria2015",35,50,1,10,142,100,0.41,10,4 +"Aria2015",35,50,2,10,142,100,0.65,14,5 +"Aria2015",35,50,3,10,142,100,0.31,8,5 +"Wortley2006",37,105,1,10,488,3,2.72,20,1 +"Wortley2006",37,105,2,10,487,2,2.49,20,1 +"Wortley2006",37,105,3,10,486,1,2.59,20,1 +"Griswold1999",43,137,1,10,394,13,0.81,6,2 +"Griswold1999",43,137,2,10,394,20,1.11,8,5 +"Griswold1999",43,137,3,10,394,8,1.46,10,2 +"Schulze2007",52,58,1,10,155,100,1.17,11,4 +"Schulze2007",52,58,2,10,155,100,0.7,9,5 +"Schulze2007",52,58,3,10,155,100,0.75,8,5 +"Eklund2004",54,131,1,10,440,100,6.39,20,1 +"Eklund2004",54,131,2,10,441,100,5.69,20,2 +"Eklund2004",54,131,3,10,441,100,3.08,10,1 +"Agnarsson2004",62,242,1,10,765,1,1.29,5,5 +"Agnarsson2004",62,242,2,10,765,1,1.24,5,5 +"Agnarsson2004",62,242,3,10,765,1,1.28,5,5 +"Zanol2014",74,213,1,10,1271,1,10,4,1 +"Zanol2014",74,213,2,10,1272,1,10,4,1 +"Zanol2014",74,213,3,10,1266,1,10,4,1 +"Zhu2013",75,253,1,10,636,1,10,4,1 +"Zhu2013",75,253,2,10,635,1,10,5,1 +"Zhu2013",75,253,3,10,631,1,10.01,5,1 +"Giles2015",78,236,1,10,676,1,10,6,1 +"Giles2015",78,236,2,10,675,1,10.02,6,1 +"Giles2015",78,236,3,10,674,1,10,5,1 +"Dikow2009",88,220,1,10,1606,3,10.01,4,3 +"Dikow2009",88,220,2,10,1606,1,10.02,3,1 +"Dikow2009",88,220,3,10,1606,1,10,4,1 diff --git a/dev/benchmarks/tnt_defaults.txt b/dev/benchmarks/tnt_defaults.txt new file mode 100644 index 000000000..199d6cf54 --- /dev/null +++ b/dev/benchmarks/tnt_defaults.txt @@ -0,0 +1,72 @@ + +PISH (Phylogenetic Inference SHell) + +Reading from C:\Users\pjjg18\GitHub\TreeSearch\dev\benchmarks\dumpdefaults.run +Running C:\Users\pjjg18\GitHub\TreeSearch\dev\benchmarks\dumpdefaults.run with + + +Sectorial search settings: + * Using separate matrix-buffer for sectors + * Recursion (user-defined searches) disabled + * Random sector selections + - Min. size 0, max. size 0 + - Max. selections for size S is M = ( T/S * 100 ) / ( 100 - 50 ) + - Increasing size in 100% when M selections made + * Sectors of size below 75 analyzed with 3 RAS+TBR + (and extra 3 starts if the first 3 produce score differences). + Not fusing starting trees for small sectors. + * Doing global TBR every 10 substitutions in small sectors, + and every 10 substitutions in large sectors. + * Not accepting equally good subtrees + + +Extra search settings: + * Using 4 replications as starting point for each hit + * Each replication initially autoconstrained (previous and wagner) + * Each replication with constraint and random sectorial searches, + with no ratchet, with drifting (5 iters.), no hybridization, and + fusing (1 rounds) + * Finding best score 1 times (=hits) + * Not consensing trees during search + * Multiplying trees by fusing after hitting best score + * Saving no more than 1 trees per replication + + +Ratchet settings: + * 50 iterations + * 40 substitutions (no more than 40 tree-rearrangements + accepted in perturbation phase) + * equally weighted cycle: yes + * Probability of up-weighting: 4 + * Probability of down-weighting: 4 + * Autoconstrained cycles: 0 + * Stopping when 99% of perturbation phase completed + + +Settings for tree-drifting: + * 30 iterations + * 60 substitutions (no more than 60 tree-rearrangements + accepted in perturbation phase) + * Max. absolute fit diff.: 1 + * Max. relative fit diff.: 0.20 + * Rejection factor for suboptimal trees: 3.00 + * Autoconstrained cycles: 0 + * Stopping when 99% of perturbation phase completed + + +Tree-fusing settings: + * Not accepting exchanges of equal score + * Using 5 rounds of fusing + * Starting from best tree + * Keeping all the trees + * Accepting all exchanges that improve initial score (not repeating) + * Swapping trees with TBR after fusion + +Tree-hybridization settings: + * 1 rounds of 1000 hybridizations each + * Replacing original tree(s) if hybrids are better + * If initial tree set increased to contain 15 times more + trees than input, retain best 1/15 trees + +Genetic algorithm in effect: tree-fusing + diff --git a/dev/benchmarks/tnt_disassembly_analysis.md b/dev/benchmarks/tnt_disassembly_analysis.md new file mode 100644 index 000000000..96d520da9 --- /dev/null +++ b/dev/benchmarks/tnt_disassembly_analysis.md @@ -0,0 +1,166 @@ +# TNT vs TreeSearch: Fitch Kernel Disassembly Comparison (T-250) + +Date: 2026-03-26 + +## Scope limitation + +**This analysis covers the native Windows TNT binary only.** The TNT +download page explicitly labels the Windows build as "[32 bits]". The +Mac, Linux, and Cygwin builds are compiled as 64-bit (Goloboff & Morales +2023). The 64-bit builds likely use wider registers and may include SIMD +or hardware `popcnt` — the "~4× throughput advantage" conclusion below +does **not** generalize to 64-bit TNT. Hamilton HPC benchmarks (T-249) +will run against the 64-bit Linux TNT and may show a very different +implementation-level gap. + +## TNT Binary Profile (Windows, 32-bit) + +- **File:** `C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe` (3.1 MB) +- **Format:** PE32 (32-bit i386), stripped (no symbols) +- **SIMD:** None. Zero xmm/ymm register references, zero popcnt instructions. +- **Code section:** `AUTO` — 2.4 MB, ~721K disassembly lines + +## TreeSearch DLL Profile + +- **File:** `.agent-E/TreeSearch/libs/x64/TreeSearch.dll` (1.8 MB) +- **Format:** PE32+ (64-bit x86-64), stripped +- **SIMD:** SSE2 (128-bit). 1281 integer SIMD ops (pand/por/pxor/pcmpeq), + 16472 xmm register references (includes scalar double FP). Zero ymm (no AVX2). +- **Popcount:** Software Hamming weight (0x5555.../0x3333.../0x0f0f... shift-mask + pattern). No hardware `popcnt` instruction. + +## Comparison Table + +| Feature | TNT | TreeSearch | +|---------|-----|------------| +| Architecture | 32-bit i386 | 64-bit x86-64 | +| Word size | 32-bit | 64-bit | +| SIMD for Fitch | None | SSE2 (128-bit `pand`/`por`) | +| Popcount | 64KB lookup table (two 16-bit halves) | Software Hamming weight (shift+mask) | +| Hardware `popcnt` | No | No | +| AVX2 | No | No | +| Bits/inner-loop iteration | 32 | 128 (2 × uint64 via `movdqu`/`pand`/`por`) | + +## TNT Fitch Kernel (0x420c04) + +The main scoring loop at 0x420c04–0x420c77 is a single-pass design: + +``` +loop: + dec counter; cmp -1; je exit // iterate over character words + mov 0x4(%esi),%eax // load left child state (32 bits) + mov 0x4(%ebx),%ecx // load right child state + add $0x4,%ebx; add $0x4,%esi // advance pointers (stride 4 = 32 bits) + not %eax; and %ecx,%eax // ~left & right = "extra states in right" + je skip // if zero, no extra states + mov %ecx,%edx; xor %eax,%edx // right XOR extra = intersection + push %eax; call 0x5c9f30 // popcount(extra) via 64KB LUT + mov %edx,(%ebx) // store intersection + sub %eax,(%edx) // adjust score counter +skip: + [symmetric check: ~result & left] + jmp loop +``` + +The popcount function at 0x5c9f30 splits a 32-bit value into two 16-bit halves +and uses a 64KB lookup table at 0x718dbd: + +``` +mov 0x8(%ebp),%edx // arg +mov %edx,%eax +and $0xffff,%eax // low 16 bits +shr $0x10,%edx // high 16 bits +mov 0x718dbd(%eax),%al // table[low] +add 0x718dbd(%edx),%al // + table[high] +movsbl %al,%eax +ret +``` + +**Key characteristics:** +- Processes one 32-bit word per iteration +- NOT+AND pattern (computes "extra states" directly) rather than AND then check-zero +- Includes a symmetric second check (right-to-left and left-to-right in the same loop body) +- Function call for popcount (not inlined) +- Branch per character word (`je skip`) + +## TreeSearch Fitch Kernel + +### Indirect scoring (TBR inner loop — 72% of wall time at 180 tips) + +`any_hit_reduce3()` in `ts_simd.h` is the critical inner function: + +```cpp +v128 acc = zero128(); +for (; s + 2 <= n_states; s += 2) { + v128 vc = loadu128(&clip[s]); // 128-bit load (2 × uint64) + v128 va = loadu128(&a[s]); + v128 vb = loadu128(&b[s]); + acc = or128(acc, and128(vc, or128(va, vb))); // clip & (a | b) +} +``` + +Compiled to: +``` +movdqu (%r8,%rax),%xmm0 // load 128 bits from clip +movdqu (%r9,%rax),%xmm2 // load 128 bits from a|b (pre-computed or inline) +add $0x10,%rax // stride 16 = 128 bits +pand %xmm2,%xmm0 // 128-bit AND +por %xmm0,%xmm1 // OR accumulate +cmp %rax,%rdx +jne loop +``` + +**Key characteristics:** +- Processes 128 bits per iteration (2 × uint64) +- SSE2 `pand`/`por` for bit operations +- Branchless within the character loop (no per-word branching) +- `popcount64()` on the result mask (software Hamming weight) + +### Downpass (`fitch_downpass_node`) + +Two-pass design: +1. **Pass 1:** `any_hit_reduce()` — tight SSE2 `pand`+`por` loop to determine + which characters have intersection (single 64-bit mask) +2. **Pass 2:** Broadcast mask + SSE2 select — no per-character branching + +## Implications + +### TNT's speed advantage is NOT implementation-level (Windows 32-bit) + +On Windows, TreeSearch has a **~4× raw Fitch throughput advantage** (128-bit +SSE2 vs 32-bit scalar). Yet TNT converges 3–5× faster on the same datasets. +This means — at minimum on Windows: + +1. **TNT's advantage is purely strategic** — fewer candidates evaluated, + more effective heuristics, or both. +2. **T-246 (AVX2)** would double TreeSearch's throughput from 128→256 bits + (and could add hardware `popcnt`). This is still worthwhile for absolute + speed, but it won't close the strategic gap with TNT. +3. **T-251 (trajectory analysis) is the higher-priority investigation** — + understanding *how many* candidates TNT evaluates per score improvement + will reveal whether the gap is in candidate pruning, search ordering, + or phase composition. + +### Minor optimization opportunities + +- **Hardware `popcnt`:** Neither program uses it. Adding `-mpopcnt` to + TreeSearch's compile flags (or runtime dispatch) would replace the + ~10-instruction software Hamming weight with a single `popcntq`. This + affects step counting after each `any_hit_reduce`, not the inner loop + itself, but could save ~5–10% of scoring time. +- **TNT's popcount is worse:** The 64KB LUT + function call overhead is + significantly more expensive than TreeSearch's inlined shift-mask. + This further confirms TNT's advantage is strategic. + +### What to investigate next + +The round 2 data shows TNT completing 50+ trees in 7–27s while TreeSearch +takes 45–110s for similar scores. If TreeSearch's per-candidate scoring is +faster, TNT must be evaluating far fewer candidates to achieve the same +result — either through better candidate pruning (e.g., more aggressive +clip skipping, smarter regraft ordering) or through phases that escape +local optima more efficiently (more effective ratchet/drift parameters). + +T-249 (rerun comparison) and T-251 (trajectory analysis) should focus on +comparing **total candidates evaluated** and **score improvement per candidate** +rather than wall-clock timing. diff --git a/dev/benchmarks/tnt_help.txt b/dev/benchmarks/tnt_help.txt new file mode 100644 index 000000000..11d4222ec --- /dev/null +++ b/dev/benchmarks/tnt_help.txt @@ -0,0 +1,431 @@ + +PISH (Phylogenetic Inference SHell) + +Reading from C:\Users\pjjg18\GitHub\TreeSearch\dev\benchmarks\helpdump.run +Running C:\Users\pjjg18\GitHub\TreeSearch\dev\benchmarks\helpdump.run with argu + + +XMULT + Run multiple replications, using sectorial searches, drifting, ratchet + and fusing combined. Options are: + hits N produce N hits to best length and stop + replications N for each hit, search initially with N replications + [no]targetscore N search until score N found (only with fusing) + [no]update do not update targetscore + [no]giveupscore N give up search as soon as score N found + [no]rss use random sectorial searches (settings with + sectsch:options) + [no]css use constraint sectorial searches (settings with + sectsch:options) + [no]xss use exclusive sectorial searches (settings with + sectsch:options) + [no]fuse use fusing (settings with tfuse:options) + [no]gfuse N every N hits, fuse all trees (=score check) + [no]dumpfuse if fusing fails to produce trees of target score, + dump the trees produced by fusing (may prevent + "clogging" of subsequent fuses by keeping only the + most distinct trees) + [no]rebuild N use N cycles of rebuilding (other settings with + "rebuild: options;"). + [no]drift N use N cycles of drifting (settings with drif:options) + [no]ratchet N use N cycles of ratchet (settings with rat:options) + hold N for ratchet, drifting, or rebuilding, save up to N + trees per initial replication (default = 1). + [no]autoconst N use consensus of previous run and initial stages of + current as constraint for initial stages. 1=previous + and wagner, 2=previous and SPR, 3=previous and TBR. + The first replication of a new hit is never + constrained (i.e. hits are totally independent). + Note that level=2 defaults to 1 when "skipspr" + is on. + [no]xmix after unsuccesful fusing, start a new set of + replications as autoconstrained (otherwise, don't) + [no]prvmix if trees existed in memory before running xmult, use + last one to autoconstrain first xmult replication + [no]consense N consense untill consensus is stabilized N times + conbase N base hits to check for consensus stabilizations + (larger numbers make more reliable estimations) + confactor N factor to increase number of hits to check consensus + stabilization (10-100, larger numbers: more reliable) + conmax N maximum new hits to recheck consensus (default=12) + [no]keepall keep trees from all replications. This has a differen + meaning when "hits" is 1 (=default) and when "hits" >1 + When "hits" = 1, it is trees from each of the RAS + TB + SS or DFT or RAT, in addition to the trees resulting f + fusing those. When "hits" > 1, then it means the tree + resulting from fusing the initial starting trees for e + of starting points. Thus, to find N trees, each resul + from S starting points (RAS+TBR+etc) and fusing, use + "xmult = hit N noupdate rep S keepall". + [no]retouch N before trying new replications, repeat sectorial + search and ratchet/drifting + level N set level of search (0-10). Use 0-2 for easy data + sets, 2-5 for medium, above 5 for difficult. If N + followed by a number T, set level for T taxa; otherwis + set level for currently active taxa. + chklevel N check search parameters during run, every N hits. + The parameters are increased or decreased, starting + from user settings. If N is preceded by +F (between + 0 and 2), the entire level is changed by F. If N is + preceded by -, user settings are starting point + [no]multiply after hitting target score, find additional trees by + fusing suboptimal with optimal trees (default = yes). + [no]verbose produce verbose reports + [no]hybrid use hybridization; this can be used jointly with + tree-fusing. Number of hybridizations, rounds, sample + size, and unsuccesful rounds to stop, are set with + the "tfuse" command. When combined with "picktype", + a number x F can follow "hybrid" --this is the factor + to multiply the number of replications if "hybrid" is + chosen instead of "fuse". Fusing normally needs fewer + trees as input (it is for more structured data sets), + when "pick" is used, genetic algorithm is determined a + run time, thus user cant't predict ahead of time which + genetic algorithm will be used. As example, with "pic + hybrid x4 repl 5", if fusing is picked, then it will u + (initially, at least) 5 replications for fusing; if + hybridization is picked, then it will use 20 replicati + [no]picktype N use either fusing or hybridization, choosing with + threshold N (see under "tfuse"). This uses only one + type of genetic algorithm; if you want both to be used + just set them both. If using "verbose", then choice i + indicated on screen. + [no]hfuse N every N hits to minimum length, hybridize all resultin + trees as an extra check for optimality. + ras,cas,ias, select type of addition sequence for the Wagner trees + sas,fas not specified, the one last used with "mult" is used). + Options are set with "xmult:options;" or "xmult=options;" (using + ":" only settings are changed; "=" runs as well). With "xmult:;" + current settings are displayed. If consensing, / followed by a taxon list + removes the specified taxa from the consensus (this must follow all the + other options). If css, rss, and xss are specified css is done first, + then rss, and xss last. Rebuilding, ratchet, and drifting (in that order) + always follow sectorial searches. Global hybridization and/or tree-fusing + are done last (although they may be done as part of rebuilding, ratchet, + or drifting). + +SECTSCH + Do sectorial-search, starting from pre-existing trees. Options are: + For determining choice of sectors: + rss do random sector selections + css do constraint-based selections + [no]xss N+R do N exclusive (i.e. non-overlapping) sector selections, + covering all tree, and analyze each; repeat process R + times or rounds (after the last one, do global TBR). + If R is followed by -G, then global TBR is done every + G rounds (and after last one). Using B-E instead of N + division starts at B and ends at E (increasing or + decreasing, depending on whether E>B or B>E) + dss N D select nodes around node N, up to D (=diameter) branches + away from N, and analyze sector. This requires specificatio + of a tree; it never swaps at the end (as in noglobal) + [no]xeven for exclusive sectors, [don't] use sectors of as even + a size as possible (uneven sectors make it more likely + to find better trees if using several rounds + minsize N minimum size for random selections + maxsize N maximum size for random selections + minfork N minimum fork for constraint-based selections + maxfork N maximum fork for constraint-based selections + increase N factor to increase size if enough selections of current + size completed. New size is S = S + ( ( S * N ) / 100 ) + selfact N factor to determine (under random selections) maximum number + of selections of size S, for T (active) taxa. Maximum + number, M, is determined as M = ( T * 100 ) / ( N * S ). + Alternatively, using "selfact = X Y Z" uses X for the + first selection, Y for the second, and so on (up to 30 + values can be defined) + moveon N if N selections fail to produce a better score, move on + rounds N for constraint-based selections, cycle N times over groups + For determining analysis: + global N for smaller selections, do global TBR every N replacements + dglobal N same, for larger selections (i.e. under drift and combined) + noglobal never swap globally. + [no]equals accept equally good subtrees + [no]fuse N when analyzing small sector (below drift size), keep + all trees and fuse (N rounds). Note: for sectors above + drift size, the autofuse option of drift applies. + godrift N sector size above which tree-drifting (not RAS+TBR) is used + drift N for drifted sectors, use N cycles of drift + gocomb N for sector of size N or more, use combined analyses + (RAS+drift+fuse). If N is smaller than the size to + use drift, drift is not done. Number C of drift cycles + for each start is determined with drift C, number F of + fuses is determined with fuse F. + starts N for sectors below minsize, number of randaddseqs plus TBR + combstarts N for sectors above size for combined analyses, use N starts + (if first N yield same score, stop, else do N more starts) + findscore N stop drifting on tree if score N found + [no]keepall keep only the best trees [don't] + General options: + [no]safesank for sankoff characters, use strict checking (=default) + to identify uninformative characters for reduced data set + (looser checking may produce small speedups, but may miss + better trees for complex transformation costs). + slack N make N percent extra memory for searches (prevents + memory errors during runs + [no]xbuf if memory is available, use independent matrix-buffer + for analysis of sectors (=faster updates, significant + time saved for small sectors in large data sets). + recurse N allow sectorial searches to recurse up to N levels + tree N select sectors for tree N (instead of all trees) + track allow tracking nodes between big tree and reduced + tree (valid only for sectorial searches with user + instructions). This is to be used in combination + with macro expressions "nodtosect", "biginsect" + chkroot for XSS searches only, make sure the base of tree (which + often may be unselected) is included as well; this may + actually use one more selection than requested. + and "bignotsect". + Options are set with "sectsch:options;" or "sectsch=options;" (using + ":" only settings are changed; "=" runs as well). With "sect:;" + current settings are displayed. + It is also possible to determine specifically how to search for + each sector generated, including the commands to search within square + brackets (including sectsch itself, possibly with user instructions as well + Maximum level of recursion has to be determined with "sectsch: recurse N;" + before reading data set. When using user instructions for each sector, + any settings changed for analyzing a subproblem will remain changed after + concluding analysis of the sector (the only exception to this is settings + for sectsch itself). + +RATCHET + Ratchet, from trees in memory. Options are: + iter N number of iterations + [no]equal periodic rounds with original weights [not] + numsubs N number of replacements (i.e. accepted tree + rearrangements) to do in perturbation phase + upfactor N probability of upweighting a character + downfact N same, for downweighting + [no]autoconst N number of auto-constrained cycles + [no]giveup N percentage of full swap to complete during perturbation + findscore N if score N or better found, stop + [no]fuse NxR every N iterations, do R rounds of fusing to the N + trees + [no]dumpfuse if fusing fails to produce a better tree, [don't] + dump all the suboptimal trees + [no]tradrat [don't] run the original ratchet (i.e. noequal, + during perturbation swap to completion and don't + accept equally good rearrangements). + Options are set with "ratchet:[options];" or "ratchet=[options];" + (first case changes settings only, second case runs as well). With + "ratchet:;" current settings are displayed + +DRIFT + Do tree-drifting, from trees in memory. Options are: + iterations number of cycles (=iterations) to do + numsubs N number of replacements (i.e. accepted tree + rearrangements) to do in perturbation phase + xfactor N larger values make acceptance of suboptimal + trees less likely + [no]autoconst N number of constrained cycles + [no]giveup N max. percentage of full swap to do in perturbation + phase. This is an int, so 99 means don't give up + fitdiff max. difference in absolute fit + rfitdiff max. difference in relative fit + findscore N stop drifting when score N hit + [no]equals alternate perturbed and unperturbed drift cycles + (note: for landmark data, unperturbed cycles are + never done). + [no]fuse NxR every N iterations, do R rounds of fusing to the + N trees + [no]dumpfuse if fusing fails to produce a better tree, [don't] + dump all the suboptimal trees + flat N run the first N iterations using the relative + fit difference defined with rflat + rflat N max. difference in relative fit difference + for initial iterations + flatnumsubs N number of replacements to do for initial iterations + [no]pert never accept suboptimal rearrangements (i.e. "drift" + only wanders around in the island; this does force + unperturbed cycles in the case of landmark data). + Options set with "drift:[options];" or "drift=[options];" (first + case changes settings only, second case runs). Using "drift:;" current + settings are displayed + +TFUSE + /S C T; create a new tree from trees S ("source") and T ("target") + inserting clade C of tree S into equivalent position of tree T + (clade C must be present in both trees; trees must be complete + and binary) + N combine set of trees N, and add resulting trees to existing set of + trees. + Options (possibly preceded by "no" and defaults in parentheses): + For tree-fusing: + [no]equals accept exchanges of equal score (don't) + [no]beststart use best tree to start (use it) + [no]choose choose only those exchanges that improve best score + found so far (don't) + [no]repeat for every individual fuse, re-fuse trees until + no exchanges improve it + [no]swap after exchanging clades, do TBR swap (swap) + minfork N if node is less than an N-polytomy in consensus of + both trees, skip exchanges (3) + rounds N use N rounds (5) + [no]keepall keep all trees found instead of best only (all) + [no]xroot N for each fuse, try N different (random) rootings + (N=0 is the default; it uses only outgroup as root) + For tree-hybridization: + [no]hybrid N*R/S instead of tree-fusing, use hybridization (as in + "hybrid" command). Hybridize randomly chosen pairs + of trees, N times, for each round R. Every round uses + the best S trees from previous round to continue hybri- + dizing. Defaults: N 1000, R 1, S 50 (S = 0 uses as man + trees as initially input, when doing several rounds. + Works better than fusing for very unstructured data set + (e.g. random). + [no]autostop N if N successive rounds of hybridization fail to improve + score, stop (default = 3). + [no]replace if hybridizing two trees produces a better tree, then + replace source tree(s) with the better tree(s) + (default = yes). + [no]clog N If initial tree set increased to contain N times more + trees than the initial input, retain best 1/N trees + (default = 15). + For selecting type of genetic algorithm: + [no]picktype N calculate a score for the expected outcome of fusing; + this score counts the proportion of groups that could + be exchanged between different pairs of trees in the + input trees (when few or no groups can be exchanged, + as is often the case for random data sets, tree-fusing + produces very poor results). If the score so calculated + is above N, use fusing, if below, use hybridization. + The default is "nopicktype"; if "picktype" specified + without a number, it uses the default threshold (1.5) + Large thresholds preferentially choose hybridization, + and viceversa. + Options are set with "tfuse:options;" or "tfuse=options;" (using + ":" only settings are changed; "=" runs as well). With "tfuse:;" + current settings are displayed. + +RSEED + N set random seed as N ( 0 = time ; default = 1 ) + +N increase random seed by N + *; set a new random seed, at random + [; in wagner trees, randomize insertion sequence + ]; in wagner trees, try insertions for new taxa from + top to bottom or from bottom up (=default) + > in wagner trees, also randomize outgroup. This + cannot be done when there are constraints or + asymmetric Sankoff characters (randomization is + skipped). Note that some "xmult" options use + internal constraints (and then skip randomization) + < in wagner trees, outgroup is always the first taxon + placed in the tree (=default) + :N; in multiple randomizations, instead of making sure + that each new seed is different from the ones used + before, increase the seed by N. This may save time + in very extensive randomizations (where checking + previous seeds takes time). When N=0, checks previous + seeds (this the default). + ! use quick approximation for randomization (faster) + - use careful randomizations (slower, more random; default) + +MULT + do N random addition sequences, followed by rearrangements. + Options are: + wagner no branch-swapping + spr use SPR branch swapping + tbr use TBR branch swapping + [no]keepall keep the trees from all replications + replic N do N replications + hold N save up to N trees per replication (only if swapping) + [no]ratchet do ratchet as well (settings with "ratchet" command) + [no]drift do drift as well (settings with "drift" command) + [no]wclus N after adding N taxa to the wagner tree, start using + node clusters (of size defined with "bbreak:clus SIZE"). + Useful only for very large data sets (several K-taxa). + outfreq N frequency for which reports are produced during branch + swapping (default is every N=10 clips, but for large data + sets this implies reports take too long to be produced). + ras use randomized addition sequences for Wagner trees + [default, works best for most data sets] + cas N use closest-addition sequence for Wagner trees (ties in + the addition sequence broken randomly), looking ahead up + N taxa (no N = all taxa). Tends to work best on data sets + with a lot of incongruence. + fas N as previous one, but using furthest addition sequence + ias N as previous one, but select first those taxa which make + the largest number of characters informative (N.B. only + additive/nonadditive characters considered for this; the + other character types have no influence on the sequence). + Tends to work best on data sets with many missing entries + and non-overlapping blocks of data. + sas N as previous one, but select first those taxa with the + largest difference in score for best/worst locations (N.B. + all character types are considered). Works best for the + same data sets as the previous one ("ias"), but it is + more thorough and slower + Usage: "mult:options;" changes settings only; "mult=options;" runs + as well. Entering "mult:;" current settings are reported. Setting + the type of addition sequence also determines the sequence to be used + in other commands (e.g. "xmult", "pfijo"). The insertion sequence + is always random for "cas", "sas", and "fas"; it can be changed (see + under "rseed") to be random or non-random for "ras" and "ias" + +BBREAK + Perform branch-swapping, using pre-existing trees as starting point. + Use "bbreak=options;" (change settings and run) or "bbreak:options;" + (change settings, don't run). This swaps according to current settings + of suboptimal, constraints, and collapsing. + + Basic options are: + tbr use TBR + spr use SPR + [no]fillonly swap until tree-buffer is filled, and then stop. + [no]mulpars save multiple trees. + + Fine-tuning options are: + [no]safe The "safe" option uses a slower (but safer) method for + updating buffers when finding a better tree under TBR + (default is "nosafe"). + [no]skipspr skips the SPR phase on a single tree when doing multiple + RAS+TBR saving several trees per replication; "skipspr" + is useful in conjunction with "nosafe", but not so much + with "safe" because then the initial portion of TBR + (when better trees are being found often) gets slowed down + and SPR doesn't. The "nosafe" option only makes a + difference for very large data sets; note that "skipspr" + modifies the behaviour of both "mult" and "xmult". + [no]int N There are two options, "int 1" and "int 2" (both options + identify most relevant characters for a series of swaps, + option 2 also reorders characters to try to save time). + These options useful only for large matrices (>10,000) with + large numbers of characters; otherwise they tend to produce + slower swapping. + [no]randclip randomize clipping sequence (with current random seed). + [no]preproc with "preproc" the program tries to identify and effect + first the clippings that would improve the tree the most; + this has effect only on "mult" searches with hold=1 (and + only when skipping the SPR phase), and TBR swapping from + existing trees with mulpars off. This may save a little + time in the initial stages of the search for very large + data sets, although the end gain is small. + clusters N use node-clusters of N nodes. As the data set becomes + larger, clusters of more nodes produce faster TBR-swapping. + When using clusters also for wagner trees, the same size as + defined here is used. + [no]strat for landmarks only; use a "stratified" error margin (i.e. + begin low, increase as swapping advances, as set with "lmark + errmarg") for the first tree swapped, final error for the + last one. Otherwise, use the final error margin from the + the beginning of the swapping (intended for trees that are + already optimal or near-optimal). + + Included for comparability with PAUP*: + [no]limit N when doing TBR, only use destinations and rerooting no more + than N nodes away from the original. Using a narrow limit + in large trees makes it look at only a tiny fraction of the + rearrangements, thus speeding up the search, but also making + it much less likely to find the optimal tree. Using a large + limit increases the chances of finding the optimal trees, but + when using this, shortcuts used in the absence of a limit are + not applicable, with the result that swapping with large + limits (probably above 1/4 to 1/3 of the taxa) produces a + slower TBR than swapping with no limit at all. Thus, the use + of this option is discouraged, except to make comparisons wit + other software using this option. N.B.: the use of limits is + compatible with constraints, but when using constraints with + limited TBR, only the rearrangements effectively done are + counted (in contrast to the default, unlimited TBR, which + counts rearrangements violating constraints as done and + rejected). diff --git a/dev/benchmarks/tnt_scaling_survey.csv b/dev/benchmarks/tnt_scaling_survey.csv new file mode 100644 index 000000000..f833736d0 --- /dev/null +++ b/dev/benchmarks/tnt_scaling_survey.csv @@ -0,0 +1,169 @@ +"machine","cpu","ram_gb","config","dataset","ntip","seed","B","reached_B","wall_s","final_score","rearr" +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project691",103,1,2169,TRUE,0.477,2169,12627650 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project691",103,2,2169,TRUE,0.46,2169,8826036 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project691",103,3,2169,TRUE,0.901,2169,24926688 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project4230",125,1,1149,TRUE,1.776,1149,124229870 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project4230",125,2,1149,TRUE,1.775,1149,126549808 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project4230",125,3,1149,TRUE,1.126,1149,71677962 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project4103",144,1,671,TRUE,0.266,671,17349780 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project4103",144,2,671,TRUE,0.253,671,14710764 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project4103",144,3,671,TRUE,0.452,671,37119133 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project3763",205,1,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project3763",205,2,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","project3763",205,3,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project691",103,1,2169,TRUE,0.499,2169,13069149 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project691",103,2,2169,TRUE,0.385,2169,8826036 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project691",103,3,2169,TRUE,0.932,2169,26898434 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project4230",125,1,1149,TRUE,1,1149,130914017 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project4230",125,2,1149,TRUE,1.911,1149,132570848 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project4230",125,3,1149,TRUE,1,1149,76573797 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project4103",144,1,671,TRUE,0.25,671,19225426 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project4103",144,2,671,TRUE,0.244,671,16475354 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project4103",144,3,671,TRUE,0.374,671,42287006 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project3763",205,1,1292,TRUE,300.413,1292,11677371763 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project3763",205,2,1292,TRUE,300.445,1290,11627120961 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","project3763",205,3,1292,TRUE,300.688,1291,10884122294 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project691",103,1,2169,TRUE,0.807,2169,30218026 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project691",103,2,2169,TRUE,0.473,2169,8826036 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project691",103,3,2169,TRUE,1.128,2169,37826838 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project4230",125,1,1149,TRUE,1.463,1149,123675532 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project4230",125,2,1149,TRUE,1.363,1149,114872567 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project4230",125,3,1149,TRUE,1.477,1149,119248549 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project4103",144,1,671,TRUE,0.26,671,27766365 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project4103",144,2,671,TRUE,0.29,671,21857412 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project4103",144,3,671,TRUE,0.579,671,78982974 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project3763",205,1,1292,TRUE,180.6,1290,15926766872 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project3763",205,2,1292,TRUE,86.599,1291,25109993773 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","project3763",205,3,1292,TRUE,62.328,1290,18239796901 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project691",103,1,2169,TRUE,1.005,2169,36744456 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project691",103,2,2169,TRUE,0.466,2169,8826036 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project691",103,3,2169,TRUE,1.236,2169,43291638 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project4230",125,1,1149,TRUE,1.104,1149,85580064 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project4230",125,2,1149,TRUE,1.107,1149,81863363 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project4230",125,3,1149,TRUE,1.691,1149,162503147 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project4103",144,1,671,TRUE,0.333,671,26826118 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project4103",144,2,671,TRUE,0.349,671,21692685 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project4103",144,3,671,TRUE,0.665,671,101274119 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project3763",205,1,1292,TRUE,53.603,1292,16006881830 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project3763",205,2,1292,TRUE,57.516,1291,17199467389 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","project3763",205,3,1292,TRUE,99.714,1291,29914821459 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project691",103,1,2169,TRUE,0.984,2169,38635944 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project691",103,2,2169,TRUE,0.463,2169,8826036 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project691",103,3,2169,TRUE,1.113,2169,38721494 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project4230",125,1,1149,TRUE,1.842,1149,167708026 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project4230",125,2,1149,TRUE,1.435,1149,131051863 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project4230",125,3,1149,TRUE,2.082,1149,184278682 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project4103",144,1,671,TRUE,0.458,671,50417375 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project4103",144,2,671,TRUE,0.347,671,27239267 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project4103",144,3,671,TRUE,0.564,671,73261357 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project3763",205,1,1292,TRUE,82.561,1291,27098909671 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project3763",205,2,1292,TRUE,112.753,1290,37237301058 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","project3763",205,3,1292,TRUE,296.918,1291,22174132929 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project691",103,1,2169,TRUE,3.31,2169,180223666 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project691",103,2,2169,TRUE,0.474,2169,20368962 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project691",103,3,2169,TRUE,1.22,2169,57710356 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project4230",125,1,1149,TRUE,5.06,1149,610549065 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project4230",125,2,1149,TRUE,4.975,1149,609221321 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project4230",125,3,1149,TRUE,3.547,1149,416609453 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project4103",144,1,671,TRUE,0.349,671,56457659 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project4103",144,2,671,TRUE,0.47,671,106480973 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project4103",144,3,671,TRUE,0.721,671,151869427 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project3763",205,1,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project3763",205,2,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","project3763",205,3,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project691",103,1,2169,TRUE,1.114,2169,36014272 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project691",103,2,2169,TRUE,1.254,2169,35737402 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project691",103,3,2169,TRUE,0.691,2169,21105766 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project4230",125,1,1149,TRUE,1.768,1149,139263828 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project4230",125,2,1149,TRUE,2.147,1149,141452602 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project4230",125,3,1149,TRUE,2.472,1149,144674354 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project4103",144,1,671,TRUE,0.476,671,35977993 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project4103",144,2,671,TRUE,0.489,671,41820897 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project4103",144,3,671,TRUE,0.498,671,32042708 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project3763",205,1,1292,FALSE,NA,1294,8432761029 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project3763",205,2,1292,FALSE,NA,1293,8414784244 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","project3763",205,3,1292,FALSE,NA,1294,8492647191 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project691",103,1,2169,TRUE,1.766,2169,54069861 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project691",103,2,2169,TRUE,0.801,2169,25335105 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project691",103,3,2169,TRUE,0.601,2169,16028361 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project4230",125,1,1149,TRUE,1.881,1149,131896942 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project4230",125,2,1149,TRUE,1.022,1149,66995791 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project4230",125,3,1149,TRUE,1.566,1149,102843225 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project4103",144,1,671,TRUE,0.363,671,16441581 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project4103",144,2,671,TRUE,0.24,671,15820112 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project4103",144,3,671,TRUE,0.334,671,32229949 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project3763",205,1,1292,FALSE,NA,1294,9500478866 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project3763",205,2,1292,FALSE,NA,1296,9486402946 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","project3763",205,3,1292,FALSE,NA,1294,9511491686 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project691",103,1,2169,TRUE,0.585,2169,13027975 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project691",103,2,2169,TRUE,0.701,2169,15883373 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project691",103,3,2169,TRUE,0.904,2169,26769813 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project4230",125,1,1149,TRUE,1.9,1149,127931916 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project4230",125,2,1149,TRUE,1.444,1149,100789434 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project4230",125,3,1149,TRUE,1.449,1149,96662240 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project4103",144,1,671,TRUE,0.362,671,36998237 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project4103",144,2,671,TRUE,0.368,671,16760149 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project4103",144,3,671,TRUE,0.367,671,24372307 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project3763",205,1,1292,FALSE,NA,1294,10990441900 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project3763",205,2,1292,FALSE,NA,1294,11011639865 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","project3763",205,3,1292,FALSE,NA,1294,10920589758 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project691",103,1,2169,TRUE,0.806,2169,26184635 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project691",103,2,2169,TRUE,0.815,2169,23248111 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project691",103,3,2169,TRUE,0.385,2169,9886020 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project4230",125,1,1149,TRUE,2.481,1149,192499135 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project4230",125,2,1149,TRUE,1.466,1149,113281498 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project4230",125,3,1149,TRUE,1.585,1149,129642210 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project4103",144,1,671,TRUE,0.35,671,32275417 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project4103",144,2,671,TRUE,0.25,671,19151188 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project4103",144,3,671,TRUE,0.365,671,36301121 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project3763",205,1,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project3763",205,2,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","project3763",205,3,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project691",103,1,2169,TRUE,0.723,2169,16876373 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project691",103,2,2169,TRUE,1.361,2169,43957186 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project691",103,3,2169,TRUE,0.707,2169,16719248 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project4230",125,1,1149,TRUE,1.69,1149,118764199 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project4230",125,2,1149,TRUE,0.911,1149,61946684 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project4230",125,3,1149,TRUE,1.921,1149,136340835 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project4103",144,1,671,TRUE,0.274,671,11516916 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project4103",144,2,671,TRUE,0.353,671,23719709 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project4103",144,3,671,TRUE,0.261,671,27631717 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project3763",205,1,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project3763",205,2,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","project3763",205,3,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project691",103,1,2169,TRUE,0.792,2169,24576059 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project691",103,2,2169,TRUE,0.683,2169,16297623 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project691",103,3,2169,TRUE,0.597,2169,16974951 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project4230",125,1,1149,TRUE,1.247,1149,91100804 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project4230",125,2,1149,TRUE,1.588,1149,108375177 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project4230",125,3,1149,TRUE,2.544,1149,189507928 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project4103",144,1,671,TRUE,0.463,671,41590753 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project4103",144,2,671,TRUE,0.38,671,40890926 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project4103",144,3,671,TRUE,0.614,671,59099872 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project3763",205,1,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project3763",205,2,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","project3763",205,3,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project691",103,1,2169,TRUE,1.026,2169,25527937 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project691",103,2,2169,TRUE,1,2169,30619852 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project691",103,3,2169,TRUE,0.935,2169,24011816 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project4230",125,1,1149,TRUE,1.69,1149,126426211 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project4230",125,2,1149,TRUE,1.665,1149,126251356 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project4230",125,3,1149,TRUE,1.239,1149,93590881 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project4103",144,1,671,TRUE,0.462,671,51118771 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project4103",144,2,671,TRUE,0.454,671,53224565 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project4103",144,3,671,TRUE,0.774,671,83769832 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project3763",205,1,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project3763",205,2,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","project3763",205,3,1292,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project691",103,1,2169,TRUE,0.474,2169,13027975 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project691",103,2,2169,TRUE,0.703,2169,15883373 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project691",103,3,2169,TRUE,1.036,2169,26769813 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project4230",125,1,1149,TRUE,1.895,1149,131896942 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project4230",125,2,1149,TRUE,0.923,1149,66995791 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project4230",125,3,1149,TRUE,1.556,1149,102843225 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project4103",144,1,671,TRUE,0.276,671,25518243 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project4103",144,2,671,TRUE,0.377,671,22050772 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project4103",144,3,671,TRUE,0.367,671,29026768 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project3763",205,1,1292,FALSE,NA,1296,9986160065 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project3763",205,2,1292,TRUE,300.472,1291,15923534597 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","project3763",205,3,1292,FALSE,NA,1296,10001200301 diff --git a/dev/benchmarks/tnt_scaling_survey.md b/dev/benchmarks/tnt_scaling_survey.md new file mode 100644 index 000000000..4fc1d1798 --- /dev/null +++ b/dev/benchmarks/tnt_scaling_survey.md @@ -0,0 +1,156 @@ +# TNT 1.6 Scaling Survey — Time-to-Target (TTT), MorphoBank datasets + +**Companion to:** `tnt_settings_survey.md` (6 gap datasets, 37–88 taxa) +**Datasets:** 4 MorphoBank projects, 103–205 taxa (above the n=90 sector-size inflection) +**Configs:** same 14 as the gap-dataset survey +**Seeds:** 3 per (config, dataset); **timeout:** 300 s/run +**Phase-1:** 5 seeds × 600 s to establish B + +**Machine:** DW-CZC429715G · 12th Gen Intel Core i7-12700 · 15.7 GB RAM +**TNT:** C:/Programs/Phylogeny/tnt/tnt.exe (v1.6, 32-bit) +**Date:** 2026-06-17 +**Script:** `dev/benchmarks/bench_tnt_settings.R` → `tnt_scaling_full()` +**Data:** `dev/benchmarks/tnt_scaling_survey.csv` + +--- + +## Dataset reference + +| Dataset | Tips | Chars | B (Phase-1) | Note | +|------------|-----:|------:|------------:|:-------------------------------| +| project691 | 103 | 529 | 2169 | just above n=90 inflection | +| project4230| 125 | 307 | 1149 | | +| project4103| 144 | 169 | 671 | | +| project3763| 205 | 109 | 1292 | Phase-2 found 1290 → B tight | + +B from Phase-1 (5 seeds × 600 s). For project3763 several Phase-2 configs +found 1290–1291, so the Phase-1 B is slightly conservative; all comparisons +treat ≤ 1292 as "reached". + +--- + +## Main results: median TTT (seconds, 3 seeds) + +`NA` = all 3 seeds censored (never reached B within 300 s). + +| Config | 103t (p691) | 125t (p4230) | 144t (p4103) | 205t (p3763) | +|:-------------|------------:|-------------:|-------------:|-------------:| +| sect-only | 0.48 | 1.77 | 0.27 | **NA** | +| sect+fuse | 0.50 | 1.00 | 0.25 | 300.4 | +| sect+ratchet | 0.81 | 1.46 | 0.29 | **86.6**| +| sect+drift | 1.00 | 1.11 | 0.35 | **57.5**| +| all | 0.98 | 1.84 | 0.46 | 112.8 | +| ratchet-only | 1.22 | 4.97 | 0.47 | **NA** | +| level0 | 1.11 | 2.15 | 0.49 | **NA** | +| level1 | 0.80 | 1.57 | 0.33 | **NA** | +| level2 | 0.70 | 1.45 | 0.37 | **NA** | +| level3 | 0.81 | 1.58 | 0.35 | **NA** | +| level4 | 0.72 | 1.69 | 0.27 | **NA** | +| level5 | 0.68 | 1.59 | 0.46 | **NA** | +| level10 | 1.00 | 1.67 | 0.46 | **NA** | +| default | 0.70 | 1.56 | 0.37 | 300.5 | + +Censored totals: project691 0/42, project4230 0/42, project4103 0/42, +project3763 **29/42**. + +--- + +## The 205-taxon failure — three distinct modes + +At 205 taxa, TNT's sector size is pinned at 45 (the n=90–450 plateau). With +only ~4–5 sectors tiling the tree, the within-sector RAS restarts cover a +small fraction of tree space. Without global perturbation, the search stalls. + +Three distinct failure modes are present; "censored at 300 s" ≠ "incapable": + +| Config at 205t | Actual TTT (s) | Median score | Seeds reached B | Mode | +|:---------------|---------------:|-------------:|----------------:|:-----| +| sect+ratchet | 62–181| 1290 | 3/3 | ✓ | +| sect+drift | 54–100| 1291 | 3/3 | ✓ | +| all | 83–297| 1291 | 3/3 | ✓ | +| sect+fuse | ~300 | 1291 | 3/3 | ✓ barely | +| default | 179–301| 1296 | 1/3 | converged short | +| level0 | 101–121| 1294 | 0/3 | converged short | +| level1 | 96–154| 1294 | 0/3 | converged short | +| level2 | 203–212| 1294 | 0/3 | converged short | +| sect-only | ~300 | NA | 0/3 | timeout+parse¹ | +| ratchet-only | ~300 | NA | 0/3 | timeout+parse¹ | +| level3 | 20–227 | NA | 0/3 | unstable² | +| level4/5/10 | 0.3–25 | NA | 0/3 | crash/OOM² | + +¹ Ran to full 300 s internal timeout; "Best score:" output not parsed by our + regex when the TNT `timeout` command fires mid-replicate. Actual score unknown. + +² Erratic termination times (0.3–227 s) with no parseable score suggest TNT + crashes or runs out of the 1500 MB mxram allocation at high `level N` settings + (XSS on a 205t matrix likely requires more memory). + +**Level 0/1/2** terminated *early* (95–212 s) because `hits 5` was satisfied — +they converged to a local optimum 1–4 steps from B and stopped. More seeds or +restarts would likely find B eventually; they are stuck, not hopeless. + +sect+ratchet finds 1290 (better than Phase-1 B=1292), confirming the Phase-1 +search was not exhaustive enough for this matrix. + +--- + +## Scaling verdict + +### At ≤ 144 taxa + +All configs reach B quickly (median < 5 s). The ordering broadly matches the +gap-dataset survey: sect+fuse and sect-only are fastest, ratchet-only is +slowest. Ratchet and drift add overhead with little benefit at this scale. + +### At 205 taxa — the picture inverts + +Pure sectorial (sect-only, all level variants, ratchet-only) **fail +completely**. Perturbation is no longer optional — it is load-bearing: + +- **sect+drift wins** (57 s) — drift perturbation rescues stalled sectorial +- **sect+ratchet** (87 s) — ratchet also rescues, slightly slower +- **ratchet-only fails** — perturbation alone without sectorial is insufficient +- **level configs fail** — TNT's own level-based heuristic cannot find B + +This reversal happens somewhere between 144 t and 205 t. The 29/42 censor +rate on project3763 (a 109-char matrix, not unusually complex) suggests the +transition is taxon-driven rather than character-driven. + +--- + +## Combined verdict (37–205 taxa) + +| Config | ≤ 88t rank | 103–144t rank | 205t outcome | +|:-------------|:----------:|:-------------:|:-------------| +| sect+ratchet | **1st** | middle | ✓ reaches B | +| sect+drift | 4th | middle | ✓ fastest | +| all | 5th | slowest | ✓ reaches B | +| sect+fuse | 11th | fastest | barely (300s)| +| sect-only | 11th | fastest | ✗ fails | +| level3 | **2nd** | middle | ✗ fails | +| ratchet-only | 10th | slowest | ✗ fails | +| default | 13th | middle | barely (300s)| + +**sect+ratchet is the only config that is fast at small scale AND reliable at +large scale.** This makes it the unambiguous emulation target. + +--- + +## Implications for TreeSearch emulation + +1. **Ratchet is mandatory, not optional** — at 205t, no-perturbation configs + all fail. The ratchet loop must be part of the core search, not an + add-on. + +2. **Sectorial without perturbation stalls beyond ~150t** — even `sect-only` + with TNT's native 3-RAS-per-sector fails. The sector resampling alone is + insufficient once the tree is large enough that sectors cover little of + the tree each pass. + +3. **Level configs scale poorly** — TNT's own `level N` parameter does not + help beyond 144t in our tests. This is consistent with level being a + within-pass intensity setting, not a global perturbation mechanism. + +4. **Priority fix for `search_sector` ([src/ts_sector.cpp:501](../../src/ts_sector.cpp)):** + add k RAS+TBR restarts per sector (as per the shared-start finding), AND + add a ratchet outer loop. Both are needed for large-matrix performance. diff --git a/dev/benchmarks/tnt_settings_survey.csv b/dev/benchmarks/tnt_settings_survey.csv new file mode 100644 index 000000000..88140617e --- /dev/null +++ b/dev/benchmarks/tnt_settings_survey.csv @@ -0,0 +1,421 @@ +"machine","cpu","ram_gb","config","dataset","ntip","seed","B","reached_B","wall_s","final_score","rearr" +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Wortley2006",37,1,479,TRUE,0.561,479,7791396 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Wortley2006",37,2,479,TRUE,1.014,479,14259655 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Wortley2006",37,3,479,TRUE,0.448,479,4239884 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Wortley2006",37,4,479,TRUE,0.878,479,12755353 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Wortley2006",37,5,479,TRUE,0.457,479,4855817 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Eklund2004",54,1,440,TRUE,0.246,440,2930128 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Eklund2004",54,2,440,TRUE,0.259,440,2313299 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Eklund2004",54,3,440,TRUE,0.276,440,1662699 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Eklund2004",54,4,440,TRUE,0.261,440,2178329 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Eklund2004",54,5,440,TRUE,0.231,440,2008400 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Zanol2014",74,1,1261,TRUE,2.646,1261,81633997 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Zanol2014",74,2,1261,TRUE,1.552,1261,43528205 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Zanol2014",74,3,1261,TRUE,3.541,1261,108230949 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Zanol2014",74,4,1261,TRUE,2.528,1261,80705319 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Zanol2014",74,5,1261,TRUE,2.875,1261,87252123 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Zhu2013",75,1,624,TRUE,0.677,624,22752250 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Zhu2013",75,2,624,TRUE,1.45,624,56167425 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Zhu2013",75,3,624,TRUE,1.103,624,38313873 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Zhu2013",75,4,624,TRUE,1.231,624,42717595 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Zhu2013",75,5,624,TRUE,0.887,624,33068535 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Giles2015",78,1,670,TRUE,0.341,670,9492023 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Giles2015",78,2,670,TRUE,0.45,670,12537199 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Giles2015",78,3,670,TRUE,0.358,670,5970017 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Giles2015",78,4,670,TRUE,0.448,670,11870359 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Giles2015",78,5,670,TRUE,0.463,670,10682741 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Dikow2009",88,1,1606,TRUE,1.55,1606,59548877 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Dikow2009",88,2,1606,TRUE,5.271,1606,215403664 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Dikow2009",88,3,1606,TRUE,3.3,1606,133172299 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Dikow2009",88,4,1606,TRUE,2.862,1606,109926627 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect-only","Dikow2009",88,5,1606,TRUE,4.281,1606,170801377 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Wortley2006",37,1,479,TRUE,0.553,479,7954440 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Wortley2006",37,2,479,TRUE,1.016,479,14433508 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Wortley2006",37,3,479,TRUE,0.472,479,4327734 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Wortley2006",37,4,479,TRUE,0.908,479,12967643 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Wortley2006",37,5,479,TRUE,0.456,479,4967424 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Eklund2004",54,1,440,TRUE,0.258,440,3354245 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Eklund2004",54,2,440,TRUE,0.254,440,2599788 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Eklund2004",54,3,440,TRUE,0.267,440,1942516 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Eklund2004",54,4,440,TRUE,0.246,440,2462010 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Eklund2004",54,5,440,TRUE,0.266,440,2428151 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Zanol2014",74,1,1261,TRUE,2.638,1261,83202203 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Zanol2014",74,2,1261,TRUE,1.56,1261,45030856 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Zanol2014",74,3,1261,TRUE,3.531,1261,109695363 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Zanol2014",74,4,1261,TRUE,2.646,1261,82248275 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Zanol2014",74,5,1261,TRUE,2.855,1261,88751072 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Zhu2013",75,1,624,TRUE,0.664,624,23853166 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Zhu2013",75,2,624,TRUE,1.473,624,57910929 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Zhu2013",75,3,624,TRUE,1.129,624,39917351 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Zhu2013",75,4,624,TRUE,1,624,44485875 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Zhu2013",75,5,624,TRUE,0.897,624,34923828 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Giles2015",78,1,670,TRUE,0.347,670,10584943 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Giles2015",78,2,670,TRUE,0.447,670,13630955 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Giles2015",78,3,670,TRUE,0.346,670,6336094 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Giles2015",78,4,670,TRUE,0.45,670,12942011 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Giles2015",78,5,670,TRUE,0.465,670,12128535 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Dikow2009",88,1,1606,TRUE,1.537,1606,61707979 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Dikow2009",88,2,1606,TRUE,5.276,1606,217650928 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Dikow2009",88,3,1606,TRUE,3.314,1606,135442035 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Dikow2009",88,4,1606,TRUE,2.226,1606,88843622 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+fuse","Dikow2009",88,5,1606,TRUE,4.086,1606,172550812 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Wortley2006",37,1,479,TRUE,0.239,479,1364202 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Wortley2006",37,2,479,TRUE,0.563,479,7734111 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Wortley2006",37,3,479,TRUE,0.352,479,2936703 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Wortley2006",37,4,479,TRUE,0.453,479,7109194 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Wortley2006",37,5,479,TRUE,0.467,479,5594484 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Eklund2004",54,1,440,TRUE,0.238,440,2544334 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Eklund2004",54,2,440,TRUE,0.161,440,2989500 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Eklund2004",54,3,440,TRUE,0.268,440,1629317 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Eklund2004",54,4,440,TRUE,0.26,440,1405239 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Eklund2004",54,5,440,TRUE,0.238,440,2029062 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Zanol2014",74,1,1261,TRUE,0.885,1261,29176111 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Zanol2014",74,2,1261,TRUE,2.214,1261,83561394 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Zanol2014",74,3,1261,TRUE,1.559,1261,58121545 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Zanol2014",74,4,1261,TRUE,0.668,1261,22383538 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Zanol2014",74,5,1261,TRUE,1.208,1261,40820711 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Zhu2013",75,1,624,TRUE,0.565,624,29369457 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Zhu2013",75,2,624,TRUE,0.556,624,20654074 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Zhu2013",75,3,624,TRUE,1.236,624,59780105 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Zhu2013",75,4,624,TRUE,0.994,624,44765440 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Zhu2013",75,5,624,TRUE,1.455,624,63492543 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Giles2015",78,1,670,TRUE,0.331,670,8345951 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Giles2015",78,2,670,TRUE,0.335,670,9095606 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Giles2015",78,3,670,TRUE,0.341,670,9573780 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Giles2015",78,4,670,TRUE,0.345,670,7964536 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Giles2015",78,5,670,TRUE,0.445,670,10660943 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Dikow2009",88,1,1606,TRUE,1.77,1606,81430784 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Dikow2009",88,2,1606,TRUE,2.201,1606,113076709 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Dikow2009",88,3,1606,TRUE,1.76,1606,81568851 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Dikow2009",88,4,1606,TRUE,2.846,1606,146662440 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+ratchet","Dikow2009",88,5,1606,TRUE,2.431,1606,125766176 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Wortley2006",37,1,479,TRUE,0.33,479,2876776 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Wortley2006",37,2,479,TRUE,0.344,479,3427179 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Wortley2006",37,3,479,TRUE,0.343,479,4840153 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Wortley2006",37,4,479,TRUE,0.339,479,3440517 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Wortley2006",37,5,479,TRUE,0.361,479,3167210 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Eklund2004",54,1,440,TRUE,0.263,440,1523693 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Eklund2004",54,2,440,TRUE,0.169,440,1785744 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Eklund2004",54,3,440,TRUE,0.244,440,2892681 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Eklund2004",54,4,440,TRUE,0.257,440,1405239 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Eklund2004",54,5,440,TRUE,0.256,440,2071778 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Zanol2014",74,1,1261,TRUE,1.562,1261,56735554 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Zanol2014",74,2,1261,TRUE,2.869,1261,117358221 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Zanol2014",74,3,1261,TRUE,1.667,1261,63720423 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Zanol2014",74,4,1261,TRUE,1.119,1261,42014210 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Zanol2014",74,5,1261,TRUE,1.113,1261,44177970 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Zhu2013",75,1,624,TRUE,0.793,624,34312843 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Zhu2013",75,2,624,TRUE,1.015,624,46263380 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Zhu2013",75,3,624,TRUE,0.998,624,45564698 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Zhu2013",75,4,624,TRUE,1.325,624,62316027 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Zhu2013",75,5,624,TRUE,1.219,624,61947576 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Giles2015",78,1,670,TRUE,0.235,670,8779323 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Giles2015",78,2,670,TRUE,0.361,670,7326272 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Giles2015",78,3,670,TRUE,0.369,670,9097288 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Giles2015",78,4,670,TRUE,0.461,670,12450350 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Giles2015",78,5,670,TRUE,0.341,670,10959151 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Dikow2009",88,1,1606,TRUE,2.658,1606,138344240 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Dikow2009",88,2,1606,TRUE,3.54,1606,189615472 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Dikow2009",88,3,1606,TRUE,3.539,1606,195419862 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Dikow2009",88,4,1606,TRUE,1.877,1606,96382357 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"sect+drift","Dikow2009",88,5,1606,TRUE,2.528,1606,139141297 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Wortley2006",37,1,479,TRUE,0.242,479,1364202 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Wortley2006",37,2,479,TRUE,0.346,479,1893379 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Wortley2006",37,3,479,TRUE,0.228,479,2444100 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Wortley2006",37,4,479,TRUE,0.352,479,3616917 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Wortley2006",37,5,479,TRUE,0.245,479,3472237 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Eklund2004",54,1,440,TRUE,0.247,440,2166951 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Eklund2004",54,2,440,TRUE,0.244,440,2787601 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Eklund2004",54,3,440,TRUE,0.262,440,1629317 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Eklund2004",54,4,440,TRUE,0.256,440,1405239 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Eklund2004",54,5,440,TRUE,0.241,440,2029062 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Zanol2014",74,1,1261,TRUE,0.906,1261,31072372 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Zanol2014",74,2,1261,TRUE,0.787,1261,25620087 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Zanol2014",74,3,1261,TRUE,0.888,1261,33995954 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Zanol2014",74,4,1261,TRUE,2.429,1261,107550898 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Zanol2014",74,5,1261,TRUE,0.905,1261,27430839 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Zhu2013",75,1,624,TRUE,1.984,624,104236142 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Zhu2013",75,2,624,TRUE,1.454,624,65643067 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Zhu2013",75,3,624,TRUE,1.879,624,97143631 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Zhu2013",75,4,624,TRUE,1.23,624,59470570 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Zhu2013",75,5,624,TRUE,1.118,624,55909055 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Giles2015",78,1,670,TRUE,0.356,670,8345951 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Giles2015",78,2,670,TRUE,0.333,670,8230119 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Giles2015",78,3,670,TRUE,0.354,670,7235943 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Giles2015",78,4,670,TRUE,0.35,670,7964536 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Giles2015",78,5,670,TRUE,0.469,670,14030274 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Dikow2009",88,1,1606,TRUE,2.215,1606,116076872 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Dikow2009",88,2,1606,TRUE,4.294,1606,242568096 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Dikow2009",88,3,1606,TRUE,4.732,1606,273996976 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Dikow2009",88,4,1606,TRUE,2.778,1606,157581767 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"all","Dikow2009",88,5,1606,TRUE,2.001,1606,112110426 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Wortley2006",37,1,479,TRUE,0.245,479,1370988 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Wortley2006",37,2,479,TRUE,0.338,479,4316150 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Wortley2006",37,3,479,TRUE,0.142,479,1590132 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Wortley2006",37,4,479,TRUE,0.349,479,3075026 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Wortley2006",37,5,479,TRUE,0.256,479,1965910 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Eklund2004",54,1,440,TRUE,0.266,440,1084154 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Eklund2004",54,2,440,TRUE,0.267,440,1194329 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Eklund2004",54,3,440,TRUE,0.269,440,1163079 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Eklund2004",54,4,440,TRUE,0.262,440,2675962 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Eklund2004",54,5,440,TRUE,0.264,440,1886857 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Zanol2014",74,1,1261,TRUE,1.442,1261,64957918 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Zanol2014",74,2,1261,TRUE,1.649,1261,81678838 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Zanol2014",74,3,1261,TRUE,1.337,1261,56621652 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Zanol2014",74,4,1261,TRUE,1.108,1261,53382443 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Zanol2014",74,5,1261,TRUE,1.136,1261,49728866 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Zhu2013",75,1,624,TRUE,1.34,624,67703456 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Zhu2013",75,2,624,TRUE,2.747,624,150134500 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Zhu2013",75,3,624,TRUE,1,624,117417168 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Zhu2013",75,4,624,TRUE,1.444,624,78650251 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Zhu2013",75,5,624,TRUE,1.221,624,62721751 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Giles2015",78,1,670,TRUE,0.342,670,6581867 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Giles2015",78,2,670,TRUE,0.236,670,7077374 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Giles2015",78,3,670,TRUE,0.47,670,15258050 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Giles2015",78,4,670,TRUE,0.359,670,9432250 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Giles2015",78,5,670,TRUE,0.355,670,11029848 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Dikow2009",88,1,1606,TRUE,3.205,1606,208839446 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Dikow2009",88,2,1606,TRUE,1.895,1606,119009625 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Dikow2009",88,3,1606,TRUE,4.515,1606,308201284 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Dikow2009",88,4,1606,TRUE,6.272,1606,431391651 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"ratchet-only","Dikow2009",88,5,1606,TRUE,4.716,1606,325240363 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Wortley2006",37,1,479,TRUE,0.577,479,7195731 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Wortley2006",37,2,479,TRUE,0.561,479,9273541 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Wortley2006",37,3,479,TRUE,0.907,479,14276863 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Wortley2006",37,4,479,TRUE,0.58,479,7230553 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Wortley2006",37,5,479,TRUE,0.562,479,9319616 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Eklund2004",54,1,440,TRUE,0.267,440,2208561 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Eklund2004",54,2,440,TRUE,0.275,440,1443821 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Eklund2004",54,3,440,TRUE,0.253,440,2447091 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Eklund2004",54,4,440,TRUE,0.244,440,2708061 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Eklund2004",54,5,440,TRUE,0.26,440,2802201 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Zanol2014",74,1,1261,TRUE,4.849,1261,164287516 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Zanol2014",74,2,1261,TRUE,3.659,1261,121985817 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Zanol2014",74,3,1261,TRUE,3.198,1261,111499937 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Zanol2014",74,4,1261,TRUE,2.643,1261,91266391 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Zanol2014",74,5,1261,TRUE,3.743,1261,132868983 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Zhu2013",75,1,624,TRUE,0.665,624,23479343 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Zhu2013",75,2,624,TRUE,0.79,624,26182344 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Zhu2013",75,3,624,TRUE,1.459,624,63933499 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Zhu2013",75,4,624,TRUE,1.321,624,55916541 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Zhu2013",75,5,624,TRUE,0.664,624,26646237 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Giles2015",78,1,670,TRUE,0.243,670,4428085 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Giles2015",78,2,670,TRUE,0.561,670,16098895 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Giles2015",78,3,670,TRUE,0.342,670,9933845 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Giles2015",78,4,670,TRUE,0.368,670,7008170 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Giles2015",78,5,670,TRUE,0.465,670,14156396 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Dikow2009",88,1,1606,TRUE,1.115,1606,45420758 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Dikow2009",88,2,1606,TRUE,3.63,1606,172430893 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Dikow2009",88,3,1606,TRUE,2.872,1606,131300581 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Dikow2009",88,4,1606,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level0","Dikow2009",88,5,1606,TRUE,2.215,1606,99399173 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Wortley2006",37,1,479,TRUE,0.548,479,6936526 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Wortley2006",37,2,479,TRUE,0.998,479,17332484 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Wortley2006",37,3,479,TRUE,0.791,479,10156760 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Wortley2006",37,4,479,TRUE,1.001,479,16937422 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Wortley2006",37,5,479,TRUE,0.568,479,7350790 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Eklund2004",54,1,440,TRUE,0.247,440,3280232 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Eklund2004",54,2,440,TRUE,0.269,440,2544303 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Eklund2004",54,3,440,TRUE,0.245,440,2731042 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Eklund2004",54,4,440,TRUE,0.246,440,3406320 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Eklund2004",54,5,440,TRUE,0.261,440,2135357 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Zanol2014",74,1,1261,TRUE,7.367,1261,251981303 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Zanol2014",74,2,1261,TRUE,2.967,1261,90811947 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Zanol2014",74,3,1261,TRUE,3.96,1261,120453020 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Zanol2014",74,4,1261,TRUE,2.637,1261,81474702 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Zanol2014",74,5,1261,TRUE,4.726,1261,151469824 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Zhu2013",75,1,624,TRUE,1.005,624,34528024 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Zhu2013",75,2,624,TRUE,1.536,624,57832542 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Zhu2013",75,3,624,TRUE,1.979,624,80133975 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Zhu2013",75,4,624,TRUE,0.99,624,36031779 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Zhu2013",75,5,624,TRUE,0.885,624,34342396 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Giles2015",78,1,670,TRUE,0.439,670,11053289 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Giles2015",78,2,670,TRUE,0.577,670,16767164 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Giles2015",78,3,670,TRUE,0.45,670,13472711 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Giles2015",78,4,670,TRUE,0.461,670,10941280 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Giles2015",78,5,670,TRUE,0.46,670,18148200 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Dikow2009",88,1,1606,TRUE,1.536,1606,60951196 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Dikow2009",88,2,1606,TRUE,6.95,1606,289451018 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Dikow2009",88,3,1606,TRUE,3.609,1606,129890414 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Dikow2009",88,4,1606,TRUE,3.297,1606,138057822 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level1","Dikow2009",88,5,1606,TRUE,1.321,1606,48517862 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Wortley2006",37,1,479,TRUE,0.77,479,11591422 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Wortley2006",37,2,479,TRUE,0.558,479,7684115 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Wortley2006",37,3,479,TRUE,0.552,479,5935330 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Wortley2006",37,4,479,TRUE,0.675,479,10248475 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Wortley2006",37,5,479,TRUE,0.564,479,8068903 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Eklund2004",54,1,440,TRUE,0.266,440,1550056 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Eklund2004",54,2,440,TRUE,0.255,440,3067533 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Eklund2004",54,3,440,TRUE,0.261,440,3204114 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Eklund2004",54,4,440,TRUE,0.229,440,2975839 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Eklund2004",54,5,440,TRUE,0.243,440,2190921 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Zanol2014",74,1,1261,TRUE,5.145,1261,167736331 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Zanol2014",74,2,1261,TRUE,3.083,1261,99001365 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Zanol2014",74,3,1261,TRUE,2.629,1261,82033999 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Zanol2014",74,4,1261,TRUE,3.382,1261,91071608 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Zanol2014",74,5,1261,TRUE,5.799,1261,182468990 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Zhu2013",75,1,624,TRUE,0.889,624,31443161 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Zhu2013",75,2,624,TRUE,0.774,624,25467786 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Zhu2013",75,3,624,TRUE,1.102,624,42457436 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Zhu2013",75,4,624,TRUE,0.68,624,21333745 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Zhu2013",75,5,624,TRUE,0.893,624,32472079 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Giles2015",78,1,670,TRUE,0.37,670,8339063 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Giles2015",78,2,670,TRUE,0.557,670,18157216 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Giles2015",78,3,670,TRUE,0.44,670,11876233 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Giles2015",78,4,670,TRUE,0.551,670,17851207 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Giles2015",78,5,670,TRUE,0.665,670,23621027 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Dikow2009",88,1,1606,TRUE,2.314,1606,96262240 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Dikow2009",88,2,1606,TRUE,1.988,1606,84027794 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Dikow2009",88,3,1606,TRUE,4.817,1606,202784164 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Dikow2009",88,4,1606,TRUE,3.083,1606,124514028 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level2","Dikow2009",88,5,1606,TRUE,2.514,1606,106032046 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Wortley2006",37,1,479,TRUE,0.34,479,4465245 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Wortley2006",37,2,479,TRUE,0.452,479,5395396 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Wortley2006",37,3,479,TRUE,0.243,479,3757989 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Wortley2006",37,4,479,TRUE,0.345,479,2443788 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Wortley2006",37,5,479,TRUE,0.354,479,4696636 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Eklund2004",54,1,440,TRUE,0.239,440,1482697 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Eklund2004",54,2,440,TRUE,0.25,440,1873939 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Eklund2004",54,3,440,TRUE,0.249,440,1907323 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Eklund2004",54,4,440,TRUE,0.246,440,2600965 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Eklund2004",54,5,440,TRUE,0.252,440,2282285 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Zanol2014",74,1,1261,TRUE,3.637,1261,130255347 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Zanol2014",74,2,1261,TRUE,1.646,1261,58182611 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Zanol2014",74,3,1261,TRUE,1.117,1261,33662957 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Zanol2014",74,4,1261,TRUE,1.676,1261,56841533 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Zanol2014",74,5,1261,TRUE,1.756,1261,59796147 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Zhu2013",75,1,624,TRUE,1.002,624,39783890 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Zhu2013",75,2,624,TRUE,1.016,624,34322153 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Zhu2013",75,3,624,TRUE,1.126,624,41807293 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Zhu2013",75,4,624,TRUE,1.009,624,35008581 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Zhu2013",75,5,624,TRUE,0.683,624,21464470 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Giles2015",78,1,670,TRUE,0.447,670,13199580 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Giles2015",78,2,670,TRUE,0.449,670,9761449 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Giles2015",78,3,670,TRUE,0.336,670,7367019 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Giles2015",78,4,670,TRUE,0.446,670,9933171 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Giles2015",78,5,670,TRUE,0.444,670,12628407 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Dikow2009",88,1,1606,TRUE,1.975,1606,86381334 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Dikow2009",88,2,1606,TRUE,1.868,1606,86311213 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Dikow2009",88,3,1606,TRUE,2.206,1606,95553150 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Dikow2009",88,4,1606,TRUE,0.69,1606,21476822 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level3","Dikow2009",88,5,1606,TRUE,2.753,1606,126858140 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Wortley2006",37,1,479,TRUE,0.36,479,3033734 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Wortley2006",37,2,479,TRUE,0.356,479,2988535 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Wortley2006",37,3,479,TRUE,0.563,479,7528575 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Wortley2006",37,4,479,TRUE,0.349,479,2551294 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Wortley2006",37,5,479,TRUE,0.335,479,3710954 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Eklund2004",54,1,440,TRUE,0.24,440,1627229 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Eklund2004",54,2,440,TRUE,0.254,440,2648753 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Eklund2004",54,3,440,TRUE,0.265,440,1914716 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Eklund2004",54,4,440,TRUE,0.247,440,1879841 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Eklund2004",54,5,440,TRUE,0.26,440,2090062 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Zanol2014",74,1,1261,TRUE,3.511,1261,122059383 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Zanol2014",74,2,1261,TRUE,1.879,1261,63203148 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Zanol2014",74,3,1261,TRUE,2.538,1261,88093257 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Zanol2014",74,4,1261,TRUE,1.77,1261,57414232 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Zanol2014",74,5,1261,TRUE,1.565,1261,52288087 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Zhu2013",75,1,624,TRUE,0.988,624,39473956 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Zhu2013",75,2,624,TRUE,0.669,624,19920359 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Zhu2013",75,3,624,TRUE,0.995,624,38842409 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Zhu2013",75,4,624,TRUE,0.672,624,20069404 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Zhu2013",75,5,624,TRUE,0.779,624,26628726 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Giles2015",78,1,670,TRUE,0.456,670,13582023 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Giles2015",78,2,670,TRUE,0.445,670,10845209 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Giles2015",78,3,670,TRUE,0.556,670,7804617 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Giles2015",78,4,670,TRUE,1.045,670,12986093 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Giles2015",78,5,670,TRUE,1.378,670,11412910 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Dikow2009",88,1,1606,TRUE,2.416,1606,112641454 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Dikow2009",88,2,1606,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Dikow2009",88,3,1606,TRUE,1,1606,47644756 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Dikow2009",88,4,1606,TRUE,2.979,1606,138525232 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level4","Dikow2009",88,5,1606,TRUE,3.183,1606,140188769 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Wortley2006",37,1,479,TRUE,0.462,479,4462989 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Wortley2006",37,2,479,TRUE,0.56,479,6978794 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Wortley2006",37,3,479,TRUE,0.454,479,5026482 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Wortley2006",37,4,479,TRUE,0.357,479,3292768 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Wortley2006",37,5,479,TRUE,0.465,479,5103994 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Eklund2004",54,1,440,TRUE,0.244,440,2109224 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Eklund2004",54,2,440,TRUE,0.247,440,2378248 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Eklund2004",54,3,440,TRUE,0.245,440,2291521 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Eklund2004",54,4,440,TRUE,0.246,440,2320750 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Eklund2004",54,5,440,TRUE,0.25,440,1913318 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Zanol2014",74,1,1261,TRUE,4.087,1261,142386613 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Zanol2014",74,2,1261,TRUE,4.062,1261,144744179 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Zanol2014",74,3,1261,TRUE,1.876,1261,62005241 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Zanol2014",74,4,1261,TRUE,1.978,1261,67659800 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Zanol2014",74,5,1261,TRUE,2.204,1261,79358709 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Zhu2013",75,1,624,TRUE,1.015,624,38388766 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Zhu2013",75,2,624,TRUE,0.907,624,29967079 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Zhu2013",75,3,624,TRUE,0.784,624,25487240 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Zhu2013",75,4,624,TRUE,0.791,624,26132810 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Zhu2013",75,5,624,TRUE,0.797,624,27826671 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Giles2015",78,1,670,TRUE,0.437,670,12210244 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Giles2015",78,2,670,TRUE,0.574,670,16038129 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Giles2015",78,3,670,TRUE,0.456,670,11943278 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Giles2015",78,4,670,TRUE,0.47,670,12008785 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Giles2015",78,5,670,TRUE,0.462,670,9999852 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Dikow2009",88,1,1606,TRUE,2.223,1606,93765239 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Dikow2009",88,2,1606,FALSE,NA,NA,NA +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Dikow2009",88,3,1606,TRUE,2.099,1606,84751862 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Dikow2009",88,4,1606,TRUE,3.287,1606,147604703 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level5","Dikow2009",88,5,1606,TRUE,1.988,1606,84811781 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Wortley2006",37,1,479,TRUE,0.334,479,3399632 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Wortley2006",37,2,479,TRUE,0.357,479,2443522 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Wortley2006",37,3,479,TRUE,0.354,479,2311973 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Wortley2006",37,4,479,TRUE,0.357,479,3761951 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Wortley2006",37,5,479,TRUE,0.35,479,4482199 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Eklund2004",54,1,440,TRUE,0.245,440,2675792 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Eklund2004",54,2,440,TRUE,0.354,440,3490059 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Eklund2004",54,3,440,TRUE,0.237,440,3609224 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Eklund2004",54,4,440,TRUE,0.232,440,3552452 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Eklund2004",54,5,440,TRUE,0.247,440,2865489 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Zanol2014",74,1,1261,TRUE,1.44,1261,56087823 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Zanol2014",74,2,1261,TRUE,3.984,1261,169352163 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Zanol2014",74,3,1261,TRUE,1.55,1261,57969638 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Zanol2014",74,4,1261,TRUE,2.853,1261,120072545 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Zanol2014",74,5,1261,TRUE,2.319,1261,95691536 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Zhu2013",75,1,624,TRUE,0.897,624,32285987 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Zhu2013",75,2,624,TRUE,0.77,624,28862449 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Zhu2013",75,3,624,TRUE,1.875,624,88046814 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Zhu2013",75,4,624,TRUE,0.782,624,29346257 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Zhu2013",75,5,624,TRUE,1.123,624,45322187 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Giles2015",78,1,670,TRUE,0.659,670,20756649 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Giles2015",78,2,670,TRUE,0.559,670,15526833 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Giles2015",78,3,670,TRUE,0.443,670,12772547 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Giles2015",78,4,670,TRUE,0.582,670,14511613 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Giles2015",78,5,670,TRUE,0.557,670,14950145 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Dikow2009",88,1,1606,TRUE,4.584,1606,253022759 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Dikow2009",88,2,1606,TRUE,3.507,1606,195629465 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Dikow2009",88,3,1606,TRUE,4.179,1606,238585409 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Dikow2009",88,4,1606,TRUE,3.095,1606,171758635 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"level10","Dikow2009",88,5,1606,TRUE,3.624,1606,188824708 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Wortley2006",37,1,479,TRUE,0.577,479,6936526 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Wortley2006",37,2,479,TRUE,0.994,479,17332484 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Wortley2006",37,3,479,TRUE,0.667,479,10156760 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Wortley2006",37,4,479,TRUE,1.007,479,16937422 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Wortley2006",37,5,479,TRUE,0.568,479,7350790 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Eklund2004",54,1,440,TRUE,0.245,440,3280232 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Eklund2004",54,2,440,TRUE,0.248,440,2544303 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Eklund2004",54,3,440,TRUE,0.248,440,2731042 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Eklund2004",54,4,440,TRUE,0.241,440,3406320 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Eklund2004",54,5,440,TRUE,0.255,440,2135357 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Zanol2014",74,1,1261,TRUE,7.238,1261,251981303 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Zanol2014",74,2,1261,TRUE,2.648,1261,90811947 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Zanol2014",74,3,1261,TRUE,3.519,1261,120453020 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Zanol2014",74,4,1261,TRUE,2.422,1261,81474702 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Zanol2014",74,5,1261,TRUE,4.398,1261,151469824 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Zhu2013",75,1,624,TRUE,0.898,624,34528024 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Zhu2013",75,2,624,TRUE,1.44,624,57832542 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Zhu2013",75,3,624,TRUE,1.878,624,80133975 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Zhu2013",75,4,624,TRUE,1.023,624,36031779 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Zhu2013",75,5,624,TRUE,0.904,624,34342396 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Giles2015",78,1,670,TRUE,0.452,670,11053289 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Giles2015",78,2,670,TRUE,0.567,670,16767164 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Giles2015",78,3,670,TRUE,0.454,670,13472711 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Giles2015",78,4,670,TRUE,0.467,670,10941280 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Giles2015",78,5,670,TRUE,0.566,670,18148200 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Dikow2009",88,1,1606,TRUE,1.447,1606,60951196 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Dikow2009",88,2,1606,TRUE,6.256,1606,289451018 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Dikow2009",88,3,1606,TRUE,2.966,1606,129890414 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Dikow2009",88,4,1606,TRUE,3.077,1606,138057822 +"DW-CZC429715G","12th Gen Intel(R) Core(TM) i7-12700",15.7,"default","Dikow2009",88,5,1606,TRUE,1.215,1606,48517862 diff --git a/dev/benchmarks/tnt_settings_survey.md b/dev/benchmarks/tnt_settings_survey.md new file mode 100644 index 000000000..c6ff95a11 --- /dev/null +++ b/dev/benchmarks/tnt_settings_survey.md @@ -0,0 +1,145 @@ +# TNT 1.6 Settings Survey — Time-to-Target (TTT) + +**Metric:** wall-clock seconds to first reach the best known score B for each dataset +**Censored:** runs that never reached B within 120 s are marked NA (3 of 420 runs) + +**Machine:** DW-CZC429715G · 12th Gen Intel Core i7-12700 · 15.7 GB RAM +**TNT:** C:/Programs/Phylogeny/tnt/tnt.exe (v1.6, 32-bit) +**Date:** 2026-06-17 +**Script:** `dev/benchmarks/bench_tnt_settings.R` +**Data:** `dev/benchmarks/tnt_settings_survey.csv` + +--- + +## Dataset reference + +| Dataset | Tips | B (best score) | +|-------------|-----:|---------------:| +| Wortley2006 | 37 | 479 | +| Eklund2004 | 54 | 440 | +| Zanol2014 | 74 | 1 261 | +| Zhu2013 | 75 | 624 | +| Giles2015 | 78 | 670 | +| Dikow2009 | 88 | 1 606 | + +All datasets run in **Fitch mode** (inapplicable `-` tokens replaced with `?`). +B was established by 10 TNT seeds (Phase 1) with 300 s per seed. + +--- + +## Config definitions + +| Config | xmult options | +|--------------|------------------------------------------------| +| sect-only | rss css xss nofuse noratchet nodrift | +| sect+fuse | rss css xss noratchet nodrift *(fuse=1 default)* | +| sect+ratchet | rss css xss ratchet 10 nodrift | +| sect+drift | rss css xss drift 10 noratchet | +| all | rss css xss ratchet 10 drift 10 | +| ratchet-only | norss nocss noxss ratchet 10 nofuse nodrift | +| level 0–10 | xmult = level N giveupscore B hits 5 replic 100 | +| default | xmult = giveupscore B hits 5 replic 100 | + +All survey runs used `giveupscore B hits 5 replic 100`. +TNT 1.6 quirk: `fuse` keyword inside `xmult =` triggers an interactive prompt; configs +wanting fuse simply omit `nofuse` (TNT default is fuse=1). + +--- + +## Main results: median TTT (seconds, 5 seeds) + +Configs ranked by median across all six datasets. + +| Config | Wortley | Eklund | Zanol | Zhu | Giles | Dikow | **Median** | +|:-------------|--------:|-------:|------:|-----:|------:|------:|-----------:| +| sect+ratchet | 0.45 | 0.24 | 1.21 | 0.99 | 0.34 | 2.20 | **0.56** | +| level3 | 0.34 | 0.25 | 1.68 | 1.01 | 0.45 | 1.98 | **0.57** | +| level5 | 0.46 | 0.25 | 2.20 | 0.80 | 0.46 | 2.16 | **0.57** | +| sect+drift | 0.34 | 0.26 | 1.56 | 1.01 | 0.36 | 2.66 | **0.63** | +| all | 0.24 | 0.25 | 0.90 | 1.45 | 0.35 | 2.78 | **0.63** | +| level0 | 0.58 | 0.26 | 3.66 | 0.79 | 0.37 | 2.54 | **0.66** | +| level4 | 0.36 | 0.25 | 1.88 | 0.78 | 0.56 | 2.70 | **0.67** | +| level10 | 0.35 | 0.24 | 2.32 | 0.90 | 0.56 | 3.62 | **0.72** | +| level2 | 0.56 | 0.26 | 3.38 | 0.89 | 0.55 | 2.51 | **0.73** | +| ratchet-only | 0.26 | 0.27 | 1.34 | 1.34 | 0.36 | 4.52 | **0.74** | +| sect-only | 0.56 | 0.26 | 2.65 | 1.10 | 0.45 | 3.30 | **0.88** | +| sect+fuse | 0.55 | 0.26 | 2.65 | 1.00 | 0.45 | 3.31 | **0.90** | +| default | 0.67 | 0.25 | 3.52 | 1.02 | 0.47 | 2.97 | **0.95** | +| level1 | 0.79 | 0.25 | 3.96 | 1.00 | 0.46 | 3.30 | **0.99** | + +Cells are median of 5 seeds. 3 censored runs (level0/4/5 × Dikow2009, one seed each) +excluded from medians. + +--- + +## Fastest config per dataset + +| Dataset | Tips | Fastest config | Median TTT | +|-------------|-----:|:---------------|------------| +| Wortley2006 | 37 | all | 0.245 s | +| Eklund2004 | 54 | sect+ratchet | 0.238 s | +| Zanol2014 | 74 | all | 0.905 s | +| Zhu2013 | 75 | level4 | 0.779 s | +| Giles2015 | 78 | sect+ratchet | 0.341 s | +| Dikow2009 | 88 | level3 | 1.975 s | + +--- + +## Ratchet / drift verdict + +Starting from the same sectorial baseline (rss+css+xss): + +| Config | Median TTT | vs sect-only | +|:-------------|------------|:-------------| +| sect-only | 0.883 s | baseline | +| sect+fuse | 0.903 s | −2% (noise) | +| sect+ratchet | 0.559 s | **−37%** | +| sect+drift | 0.627 s | **−29%** | +| all | 0.628 s | **−29%** | +| ratchet-only | 0.735 s | −17% | + +**Adding ratchet or drift both accelerate convergence substantially.** +Ratchet alone (+37%) outperforms drift alone (+29%) on average. +Combining them (`all`) does not compound further — it matches drift alone. +Fuse contributes nothing beyond what sectorial alone achieves. +Ratchet-without-sectors works but is slower than either sectorial variant. + +--- + +## Level series + +The `level N` controls TNT's new-technology search effort. The response is non-monotonic: + +| Level | Median TTT | Notes | +|:--------|------------|:----------------------------------| +| level0 | 0.664 s | Low effort; slow on Zanol | +| level1 | 0.994 s | **Worst in level series** | +| level2 | 0.725 s | | +| level3 | 0.568 s | **Best in level series** | +| level4 | 0.672 s | | +| level5 | 0.574 s | 2nd best | +| level10 | 0.715 s | | +| default | 0.949 s | No level specified; 2nd worst | + +Levels 3 and 5 are the sweet spot. `default` (TNT's xmult without a level flag) performs +poorly — equivalent to level1 behaviour in this size range. + +--- + +## Summary for emulation + +1. **Best single config overall:** `sect+ratchet` (rss+css+xss, ratchet 10, no drift) — + 37% faster than plain sectorial, 40% faster than TNT default. + +2. **Best for larger taxa (≥85t):** `level3` wins on Dikow2009 (88t); further testing on + larger matrices needed to see if this scales. + +3. **Fuse is inert** at these matrix sizes — neither helps nor hurts. + +4. **Ratchet > drift**, but both help; combining both does not compound. + +5. **Do NOT emulate TNT default** (`xmult;` with no extra options) — it ranks 13th of 14. + +6. **Priority lever for TreeSearch emulation:** our sectorial search already implements + rss+css+xss; adding a ratchet perturbation loop (10 iterations) is the single change + most likely to close the remaining score gap observed in bench_sectorial_shared.R. diff --git a/dev/benchmarks/tnt_trajectory_analysis.md b/dev/benchmarks/tnt_trajectory_analysis.md new file mode 100644 index 000000000..8ce0526a0 --- /dev/null +++ b/dev/benchmarks/tnt_trajectory_analysis.md @@ -0,0 +1,226 @@ +# T-251: TNT vs TreeSearch Trajectory Analysis + +Date: 2026-03-26 + +## Executive Summary + +TreeSearch's score gap with TNT (3–21 steps on gap datasets) arises from two +compounding factors: + +1. **Per-evaluation overhead**: TNT evaluates 1.5–3.6× more rearrangements + per second than TreeSearch, despite TreeSearch having wider SIMD (SSE2 + 128-bit vs TNT's 32-bit scalar on Windows). The overhead is in data + structure manipulation, not the Fitch kernel. + +2. **Phase allocation**: TreeSearch spends 16–23% of wall time on drift, + which has extremely poor return (405–1498 ms per step gained). TNT's + `xmult` is dominated by sectorial search, which is far more cost-effective. + +## Methodology + +Three datasets with the largest persistent score gaps (from T-249) were +compared at 30-second budgets, 3 seeds each, EW scoring, inapplicable +tokens treated as missing: + +| Dataset | Tips | Chars | Gap (TS − TNT) | +|---------|:----:|:-----:|:---:| +| Geisler2001 | 68 | 186 | 5–9 | +| Zhu2013 | 75 | 253 | 4–6 | +| Wortley2006 | 37 | 105 | 3–4 | + +TNT: console-mode Windows 32-bit (v1.6, 2026-02-20), `xmult=hits 10 +replic 100`. TreeSearch: cpp-search HEAD, `ts_driven_search()` with +default strategy parameters, `verbosity=2`. + +**Caveat:** TNT on Windows is 32-bit; Hamilton benchmarks will use the +64-bit Linux build which may have different throughput characteristics. +The per-evaluation throughput ratios below may not hold on Linux. + +## Per-Evaluation Throughput + +TNT's total rearrangements are reported directly. TreeSearch's +per-evaluation rate was measured via `ts_tbr_search()` on a single Wagner +→ TBR convergence. + +| Dataset | TNT M evals/s | TS M evals/s | TNT/TS ratio | +|---------|:---:|:---:|:---:| +| Geisler2001 (68t) | 16.5 | 10.9 | 1.5× | +| Zhu2013 (75t) | 27.9 | 13.9 | 2.0× | +| Wortley2006 (37t) | 12.2 | 3.4 | 3.6× | + +The gap is larger at smaller tree sizes, where the Fitch kernel is a +smaller fraction of per-evaluation cost and overhead dominates. + +T-250 showed TreeSearch's Fitch kernel processes 128 bits per SIMD +iteration vs TNT's 32 bits — a ~4× raw throughput advantage. Yet TNT +evaluates more total rearrangements per second. This means TreeSearch's +**per-evaluation overhead** (undo stack management, data structure +traversal, incremental scoring setup) exceeds TNT's by 6–14×, completely +negating the SIMD advantage. + +## Total Rearrangements (30s budget) + +| Dataset | TNT total evals | TS est. total evals | TNT/TS ratio | +|---------|:---:|:---:|:---:| +| Geisler2001 | 499M | ~210M (est.) | ~2.4× | +| Zhu2013 | 796M | ~280M (est.) | ~2.8× | +| Wortley2006 | 104M | ~54M (est.) | ~1.9× | + +TS estimates based on TBR throughput × phase time allocation. TNT +examines roughly twice as many candidates in the same wall time. + +## Phase Cost Efficiency + +TreeSearch phase efficiency = ms of wall time per step of score +improvement. Lower is better. Averaged over 3 seeds per dataset. + +### Geisler2001 (68 taxa) + +| Phase | Time (ms) | Steps gained | ms/step | % of time | +|-------|:---------:|:---:|:---:|:---:| +| TBR | 1773 | 2397 | 0.8 | 3% | +| CSS | 1408 | 154 | 9.1 | 3% | +| RSS | 863 | 49 | 18 | 2% | +| XSS | 1502 | 97 | 20 | 3% | +| Ratchet | 34616 | 1070 | 34 | 63% | +| **Drift** | **11843** | **8** | **1498** | **22%** | + +### Zhu2013 (75 taxa) + +| Phase | Time (ms) | Steps gained | ms/step | % of time | +|-------|:---------:|:---:|:---:|:---:| +| TBR | 1574 | 3321 | 0.5 | 3% | +| XSS | 2028 | 367 | 5.7 | 4% | +| CSS | 1372 | 107 | 14 | 3% | +| RSS | 833 | 46 | 18 | 2% | +| Ratchet | 33710 | 765 | 44 | 62% | +| **Drift** | **12695** | **10** | **1270** | **23%** | + +### Wortley2006 (37 taxa) + +| Phase | Time (ms) | Steps gained | ms/step | % of time | +|-------|:---------:|:---:|:---:|:---:| +| TBR | 1100 | 2655 | 0.4 | 2% | +| XSS | 1652 | 376 | 4.5 | 3% | +| CSS | 1332 | 226 | 6.1 | 3% | +| RSS | 883 | 83 | 11 | 2% | +| Ratchet | 35945 | 2058 | 18 | 72% | +| **Drift** | **7989** | **22** | **405** | **16%** | + +**Pattern:** Drift is 30–170× less efficient than the next-worst phase +(ratchet) across all three datasets. + +## TNT's Search Structure + +TNT's `xmult` trajectory reveals a fundamentally different phase +composition from TreeSearch's pipeline: + +**Geisler2001 (30s, seed 1):** TNT reports 30 sub-replicate results +across 7 replicates. Algorithm breakdown: +- SECT (sectorial search): ~20 entries +- TBR: ~8 entries +- FUSE: ~2 entries + +TNT hits score 1293 within replicate 0 (3 seconds, 56M rearrangements) +via TBR following sectorial search. Subsequent replicates hover around +1293–1303, with sectorial search and fusing maintaining the best score. + +TreeSearch hits 1298 as its best single-replicate score (replicate 10, +after ~14s of cumulative search time). No replicate reaches 1293. + +**Key structural differences:** + +1. **TNT does extensive sectorial search within each replicate.** Each TNT + replicate includes multiple rounds of sectorial search + TBR before + moving to the next Wagner start. TreeSearch does one pass of + XSS+RSS+CSS per outer cycle. + +2. **TNT's replicates are longer and more productive.** TNT completes ~7 + replicates in 30s on Geisler2001 (~4.3s each), with each replicate + including intensive sectorial + TBR + fuse. TreeSearch completes 14–19 + replicates (~1.5–2s each), but each is shallower. + +3. **TNT fuses frequently within the search.** The FUSE entries in TNT's + trajectory show tree fusing as an integrated part of the search cycle, + not a separate post-search step. + +## Per-Replicate Score Quality + +Median per-replicate score (the typical quality of a single search from +a random Wagner start): + +| Dataset | TNT median rep | TS median rep | TNT advantage | +|---------|:---:|:---:|:---:| +| Geisler2001 | ~1297 | 1313 | 16 steps | +| Zhu2013 | ~626 | 636 | 10 steps | +| Wortley2006 | ~487 | 488 | 1 step | + +TNT achieves better per-replicate scores, which means its intra-replicate +search (sectorial + TBR) is more thorough. + +## TreeSearch Per-Replicate Trajectory + +**Geisler2001 (seed 1):** 15 replicates +- Rep 1: 1349 (Wagner 1678 → TBR → Ratchet → Drift) +- Rep 2: 1308 (improvement) +- Rep 5: 1304 +- Rep 10: 1298 (best found) +- Rep 15: 1327 (no improvement in last 5 reps) + +Score improves from 1349 → 1298 over 15 replicates (51 steps). TNT +improves from ~1298 → 1293 within a single replicate. + +## Recommendations + +### High priority: Eliminate or drastically reduce drift + +Drift consumes 16–23% of search time but contributes <1% of score +improvement. At 405–1498 ms per step gained, it is 30–170× less +efficient than the next-worst phase. + +**Proposed change:** Set `driftCycles = 0` in the default preset. +Reallocate the saved time to additional ratchet cycles or sectorial +search rounds. The `thorough` preset (with many more base cycles) could +retain 1–2 drift cycles as a diversity mechanism. + +Expected impact: ~20% wall-time savings with negligible score loss. +Equivalent to adding ~4 more replicates per 30s budget. + +### Medium priority: Increase sectorial search intensity + +TNT's dominance of sectorial search (SECT appears in ~67% of trajectory +entries) suggests TreeSearch's single-pass XSS+RSS+CSS is insufficient. +Currently sectorial search takes only 6–10% of wall time but has +respectable efficiency (5–20 ms/step). + +**Proposed change:** Increase sectorial search rounds. Options: +- Double `xssRounds` and `rssRounds` within each outer cycle +- Add a second sectorial search pass after ratchet (currently + sectorial → ratchet → drift → TBR; change to + sectorial → ratchet → sectorial → TBR) +- Increase `sectorMaxSize` to capture more of the tree in each sector + +### Medium priority: Reduce per-evaluation overhead + +The 1.5–3.6× per-evaluation throughput gap means every search phase is +penalized. Likely targets: +- Undo stack management in TBR (PreallocUndo grow/shrink) +- Incremental scoring setup cost (even when not finding improvements) +- Collapsed-flag recomputation (O(n) per move, even when 0% collapsed) + +This is a deeper engineering effort (T-245/T-246 overlap) but has the +broadest impact since it accelerates every phase. + +### Low priority: Ratchet tuning + +Ratchet is the most time-consuming phase (62–72%) and mid-tier in +efficiency. The current 12 cycles at 25% perturbation may be too many; +diminishing returns likely set in after 6–8 cycles. The adaptive level +mechanism already scales this down when hit rates are high, but the +base count could be reduced for the default preset. + +## Data Files + +- `bench_trajectory.R` — comparison script +- `trajectory_results.rds` — raw results (3 datasets × 3 seeds) +- `tnt_trajectory_analysis.md` — this document diff --git a/dev/benchmarks/trajectory_results.rds b/dev/benchmarks/trajectory_results.rds new file mode 100644 index 000000000..b6c6b2815 Binary files /dev/null and b/dev/benchmarks/trajectory_results.rds differ diff --git a/dev/benchmarks/ts_arms.R b/dev/benchmarks/ts_arms.R new file mode 100644 index 000000000..20339a09b --- /dev/null +++ b/dev/benchmarks/ts_arms.R @@ -0,0 +1,83 @@ +# TreeSearch shared-start arms vs the TNT ratchet-off target (define_target.R). +# Starts from the IDENTICAL canonical T0 (dev/benchmarks/t0/.tre), ratchet/drift/ +# fuse OFF, rss-only. Verifies TreeLength(T0)==expected before searching. +# +# Arms (TS_ARMS env, space-sep; default "base coll30"): +# base defaults [6,50] ras1 coll0 -- current behaviour +# coll30 [31,99] ras3 break-big collapse->30 units -- the established null +# freezeDet [31,99] ras3 freeze-big (cap15 thr8) DET -- H2: large movable units +# freezeRand [31,99] ras3 freeze-big (cap15 thr8) RANDOM -- H1: per-pass diversity +# freezeRand20 freezeRand + rss_picks=20 -- + pick count +# Freeze arms route through build_reduced_dataset_freeze (TS_FREEZE_COLLAPSE); +# byte-identical to current code when unset. Point TS_LIB at the freeze build. +suppressMessages({ + library(TreeSearch, lib.loc = normalizePath(Sys.getenv("TS_LIB", ".agent-aband2"), + winslash = "/")) + library(TreeTools) +}) +data("inapplicable.phyData", package = "TreeSearch") +dsN <- strsplit(trimws(Sys.getenv("TS_DATASETS", "Zanol2014 Wortley2006 Zhu2013 Giles2015")), "\\s+")[[1]] +arms <- strsplit(trimws(Sys.getenv("TS_ARMS", "base coll30")), "\\s+")[[1]] +ROUNDS <- as.integer(Sys.getenv("TS_RSSROUNDS", "15")) +SEEDS <- as.integer(strsplit(Sys.getenv("TS_SEEDS", "1 2 3"), "\\s+")[[1]]) +target <- c(Zanol2014 = 1261, Wortley2006 = 480, Zhu2013 = 624, Giles2015 = 670) +t0dir <- "dev/benchmarks/t0" + +# arm = list(min, max, ras, coll, eq, freeze, rand, cap, thresh, picks) +cfg <- list( + base = list(6L, 50L, 1L, 0L, FALSE, 0L, 0L, 0L, 0L, 0L), + coll30 = list(31L, 99L, 3L, 30L, FALSE, 0L, 0L, 0L, 0L, 0L), + base20 = list(6L, 50L, 1L, 0L, FALSE, 0L, 0L, 0L, 0L, 20L), # budget-matched null + coll30_20 = list(31L, 99L, 3L, 30L, FALSE, 0L, 0L, 0L, 0L, 20L), # budget-matched null + largeOnly = list(31L, 99L, 1L, 0L, FALSE, 0L, 0L, 0L, 0L, 20L), # large band, ras1, NO collapse + largeRas3 = list(31L, 99L, 3L, 0L, FALSE, 0L, 0L, 0L, 0L, 20L), # large band, ras3, NO collapse + freezeDet = list(31L, 99L, 3L, 0L, FALSE, 1L, 0L, 15L, 8L, 0L), + freezeRand = list(31L, 99L, 3L, 0L, FALSE, 1L, 1L, 15L, 8L, 0L), + freezeRand20 = list(31L, 99L, 3L, 0L, FALSE, 1L, 1L, 15L, 8L, 20L), + freezeHT = list(31L, 99L, 3L, 0L, FALSE, 1L, 1L, 33L, 28L, 20L), # high thr, overshoot + freezeHT2 = list(31L, 99L, 3L, 0L, FALSE, 1L, 1L, 40L, 30L, 20L), + freezeHTdet = list(31L, 99L, 3L, 0L, FALSE, 1L, 0L, 33L, 28L, 20L), # H1/H2 ablation: DET + # n-scaled (negative field = percent of NTip): min .42n max .99n cap .45n thr .38n + freezeScaled = list(-42L, -99L, 3L, 0L, FALSE, 1L, 1L, -45L, -38L, 20L), + freezeScalDet= list(-42L, -99L, 3L, 0L, FALSE, 1L, 0L, -45L, -38L, 20L) +) + +run_arm <- function(phy, t0, a, seed) { + set.seed(seed) + n <- NTip(phy) + res <- function(v) if (v < 0L) as.integer(round(n * (-v) / 100)) else v # neg = pct of n + a[[1]] <- res(a[[1]]); a[[2]] <- res(a[[2]]); a[[8]] <- res(a[[8]]); a[[9]] <- res(a[[9]]) + if (a[[6]] > 0) { + Sys.setenv(TS_FREEZE_COLLAPSE = "1", + TS_FREEZE_CAP = as.character(a[[8]]), + TS_FREEZE_THRESH = as.character(a[[9]])) + if (a[[7]] > 0) Sys.setenv(TS_FREEZE_RANDOM = "1") else Sys.unsetenv("TS_FREEZE_RANDOM") + } else { + Sys.unsetenv("TS_FREEZE_COLLAPSE"); Sys.unsetenv("TS_FREEZE_RANDOM") + Sys.unsetenv("TS_FREEZE_CAP"); Sys.unsetenv("TS_FREEZE_THRESH") + } + if (a[[10]] > 0) Sys.setenv(TS_RSS_PICKS = as.character(a[[10]])) else Sys.unsetenv("TS_RSS_PICKS") + r <- suppressWarnings(MaximizeParsimony(phy, tree = t0, maxReplicates = 1L, nThreads = 1L, + maxSeconds = 0, verbosity = 0L, ratchetCycles = 0L, driftCycles = 0L, + xssRounds = 0L, cssRounds = 0L, rssRounds = ROUNDS, wagnerStarts = 1L, + fuseInterval = 9999L, sectorMinSize = a[[1]], sectorMaxSize = a[[2]], + rasStarts = a[[3]], sectorCollapseTarget = a[[4]], sectorAcceptEqual = a[[5]])) + Sys.unsetenv("TS_FREEZE_COLLAPSE"); Sys.unsetenv("TS_FREEZE_RANDOM") + Sys.unsetenv("TS_FREEZE_CAP"); Sys.unsetenv("TS_FREEZE_THRESH"); Sys.unsetenv("TS_RSS_PICKS") + min(as.double(attr(r, "score"))) +} + +for (nm in dsN) { + phy <- readRDS(file.path(t0dir, paste0(nm, ".phy.rds"))) + t0 <- ape::read.tree(file.path(t0dir, paste0(nm, ".tre"))) + t0len <- TreeLength(t0, phy); tgt <- target[[nm]] + cat(sprintf("\n==== %s | T0=%.0f target=%d (gap %+.0f) ====\n", nm, t0len, tgt, tgt - t0len)) + for (an in arms) { + a <- cfg[[an]] + sc <- vapply(SEEDS, function(s) run_arm(phy, t0, a, s), double(1)) + best <- min(sc) + cat(sprintf(" %-12s seeds[%s] -> %s | best %.0f (%+.0f vs T0, %+.0f vs target)%s\n", + an, paste(SEEDS, collapse = ","), paste(format(sc), collapse = " "), + best, best - t0len, best - tgt, if (best <= tgt) " <== REACHED" else "")) + } +} diff --git a/dev/benchmarks/vtune_pr_driver.R b/dev/benchmarks/vtune_pr_driver.R new file mode 100644 index 000000000..7f5d8a369 --- /dev/null +++ b/dev/benchmarks/vtune_pr_driver.R @@ -0,0 +1,49 @@ +#!/usr/bin/env Rscript +# VTune driver: prune-reinsert hotspot profiling +# +# Exercises prune_reinsert_search heavily on Zhu2013 (75t) and Dikow2009 (88t). +# Target: ~30-60s of CPU time in the PR hot path. +# +# Usage: +# Rscript dev/benchmarks/vtune_pr_driver.R +# vtune -collect hotspots -result-dir vtune-pr-out -- Rscript dev/benchmarks/vtune_pr_driver.R + +.libPaths(c(".vtune-lib", .libPaths())) +library(TreeSearch) +library(TreeTools) + +cat("TreeSearch:", as.character(packageVersion("TreeSearch")), "\n") +cat("Dataset: Zhu2013 (75t) + Dikow2009 (88t)\n\n") + +# Use both datasets for a more representative profile +datasets <- list( + Zhu2013 = inapplicable.phyData[["Zhu2013"]], + Dikow2009 = inapplicable.phyData[["Dikow2009"]] +) + +t0 <- proc.time() + +for (ds_name in names(datasets)) { + ds <- datasets[[ds_name]] + cat(sprintf("Running %s ...\n", ds_name)) + + # Maximise PR time share: high cycle count, no ratchet/drift/NNI-perturb, + # enough time for ~100+ replicates worth of PR work. + set.seed(7531) + MaximizeParsimony( + ds, + maxSeconds = 40L, + strategy = "auto", + pruneReinsertCycles = 5L, + pruneReinsertDrop = 0.10, + driftCycles = 0L, + nniPerturbCycles = 0L, + verbosity = 0L, + nThreads = 1L + ) + + elapsed <- (proc.time() - t0)[3] + cat(sprintf(" done (%.1fs elapsed)\n", elapsed)) +} + +cat(sprintf("\nTotal: %.1fs\n", (proc.time() - t0)[3])) diff --git a/dev/benchmarks/vtune_tbr_analysis.md b/dev/benchmarks/vtune_tbr_analysis.md new file mode 100644 index 000000000..f6d2c68b5 --- /dev/null +++ b/dev/benchmarks/vtune_tbr_analysis.md @@ -0,0 +1,149 @@ +# T-260: VTune TBR Per-Evaluation Overhead Analysis + +**Date:** 2026-03-26 +**Agent:** E +**CPU:** Intel Core i7-10700 @ 2.90 GHz (Comet Lake, 10th gen) +**Sampling:** User-mode software sampling (VTune 2025.10) +**Dataset:** Dikow2009 (88 tips, EW parsimony) +**Workload:** 50 random starts × (Wagner → NNI → 20 TBR passes) = 1000 TBR passes +**Total CPU time:** 30.96s (of which TreeSearch.dll = 23.71s = 76.6%) + +## Module breakdown + +| Module | CPU Time | % | +|--------|:--------:|:-:| +| TreeSearch.dll | 23.71s | 76.6% | +| ucrtbase.dll | 6.00s | 19.4% | +| R.dll | 1.10s | 3.6% | +| Other | 0.15s | 0.5% | + +## Top hotspots (TreeSearch.dll + attributed ucrtbase) + +### By logical category + +| Category | Time | % of total | Key functions | +|----------|:----:|:----------:|---------------| +| **Full NA-aware scoring** | 9.03s | 29.2% | `fitch_na_score` (includes NNI path: 3.62s) | +| **StateSnapshot save/restore** | 4.53s | 14.6% | `save` 2.15s, `restore` 1.97s, `restore_prealloc_undo` 0.18s (memcpy in ucrtbase) | +| **Incremental scoring** | 2.28s | 7.4% | `fitch_na_indirect_length_cached` 1.02s, `fitch_na_pass3_score` 0.89s, `fitch_na_indirect_length_bounded` 0.37s | +| **Tip state reloading** | 1.62s | 5.2% | `load_tip_states` (called from `reset_states` → `full_rescore`) | +| **SIMD bit ops** | ~2.0s | 6.5% | `any_hit_reduce` 1.60s, `or_reduce` 0.21s, `any_hit_reduce3` 0.31s | +| **Buffer zeroing** | ~1.20s | 3.9% | `std::fill` in `reset_states()` — zeroes prelim, final_, down2, subtree_actives, local_cost | +| **TBR orchestration** | ~1.9s | 6.1% | `tbr_search` 1.06s, `precompute_vroot_cache` 0.46s, `fitch_join_states` 0.13s, `collect_main_edges` 0.11s, `validate_topology` 0.07s, `fast_hash` 0.06s | +| **Data setup** | ~0.9s | 2.9% | `count_state_occurrences` 0.64s, `simplify_patterns` 0.12s, `build_dataset` 0.12s | +| **Memory management** | ~0.8s | 2.6% | `malloc_base` 0.77s | +| **popcount** | ~0.43s | 1.4% | `popcount64` (multiple sites) | +| **Hash set destructor** | 0.14s | 0.4% | `unordered_set::~unordered_set` (TBR tabu set) | + +### TBR-only breakdown (excluding NNI scoring) + +Subtracting the NNI path (3.62s fitch_na_score + proportional overhead), the +TBR-specific budget is approximately: + +| TBR phase | Time | % of TBR | +|-----------|:----:|:--------:| +| Full rescore scoring (`fitch_na_score`) | 5.41s | 28% | +| StateSnapshot save/restore | 4.53s | 23% | +| Incremental candidate screening | 2.28s | 12% | +| Buffer zeroing (`std::fill` in `reset_states`) | ~1.20s | 6% | +| Tip reloading (`load_tip_states`) | 1.60s | 8% | +| TBR orchestration | ~1.9s | 10% | +| SIMD / popcount / other | ~2.5s | 13% | +| **Total TBR** | **~19.4s** | **100%** | + +## Key finding: `full_rescore` overhead + +Every TBR candidate that passes incremental screening triggers: + +1. `state_snap.save()` — memcpy ~190 KB (5 arrays × n_node × total_words) +2. `apply_tbr_move()` — modifies topology + states +3. `full_rescore()` = `reset_states()` + `score_tree()` + - `reset_states()`: 5× `std::fill(0)` + `load_tip_states()` + - `score_tree()`: `fitch_na_score()` (full 3-pass) +4. If rejected: `state_snap.restore()` — memcpy ~190 KB back + +**The non-scoring overhead of a single candidate evaluation +(save + zero + load_tips + restore) totals 7.35s = 37.8% of TBR time.** + +The snapshot mechanism itself (save+restore = 4.53s) is an optimization +over the alternative (re-running `full_rescore` after rejection). But the +`reset_states()` step — zeroing all arrays before the downpass overwrites +them — is likely unnecessary since the Fitch downpass will recompute all +internal node values from tips up. + +## Top 3 actionable hotspots + +### 1. StateSnapshot save/restore — 14.6% (4.53s) + +**What:** Full-array memcpy of prelim, final_, down2, subtree_actives, +local_cost, and postorder before each candidate evaluation. Restore copies +everything back when the move is rejected. + +**Why it's expensive:** At 88 tips: n_node=175, total_words≈30 → each +state array is ~42 KB. With 5 arrays + cost array + postorder, each +save/restore copies ~190 KB. At 180 tips, this doubles. + +**Potential fixes:** +- **Selective save/restore**: Only save nodes affected by the TBR move + (the clip subtree path + regraft path to root). Requires tracking dirty + nodes in `apply_tbr_move()`. +- **Copy-on-write / versioned arrays**: Use generation counters instead + of bulk copy. +- **Eliminate the need**: If `full_rescore()` is made cheaper (see #2), + the restore path could simply re-run scoring instead of restoring from + snapshot. + +### 2. `reset_states()` (zero + reload tips) — 9.1% (2.82s) + +**What:** `full_rescore()` calls `reset_states()` which zeroes all 5 state +arrays then copies tip data back from the dataset. This runs before every +`score_tree()`. + +**Why it may be unnecessary:** The Fitch downpass computes every internal +node's `prelim` from its children's values (bottom-up), overwriting whatever +was there. The uppass similarly overwrites `final_`. The zeroing is only +needed if the scoring algorithm reads uninitialized memory — but if the +postorder traversal visits every internal node, it never does. + +**Potential fix:** Replace `reset_states()` with just `load_tip_states()`. +Verify that the NA-aware passes (down2, subtree_actives) also fully +overwrite internal nodes during their traversals. If they do, save 3.9% +immediately (the std::fill cost) and reduce tip loading to only the +arrays that aren't fully recomputed. + +### 3. `fitch_na_score` as authoritative rescore — 29.2% (9.03s) + +**What:** The full 3-pass NA-aware Fitch algorithm is called for every +candidate that passes incremental screening. This is the authoritative +score used to accept/reject moves. + +**Why it dominates:** It's the core algorithm — this is expected. But +it's called much more often than strictly necessary because incremental +scoring is only a screening heuristic. + +**Potential fixes:** +- **Improve incremental accuracy**: If incremental scoring matched + full-rescore more closely, fewer candidates would need full evaluation. + Currently ~every clip with a viable candidate triggers full_rescore. +- **Deferred full rescore**: Accept based on incremental score, batch + full rescores periodically (risk: score drift). +- **This is also addressed indirectly by fixes #1 and #2**: reducing + the per-evaluation overhead means each full_rescore call is cheaper. + +## Estimated impact of fixes + +| Fix | Savings | Effort | +|-----|:-------:|:------:| +| Eliminate `std::fill` in `reset_states` | ~3.9% (~1.2s) | Low — verify NA invariants, remove 5 fill calls | +| Selective StateSnapshot (save/restore only dirty nodes) | ~10–12% (~3–4s) | Medium — track dirty set in apply_tbr_move | +| Reduce `load_tip_states` scope (only reload modified arrays) | ~2–3% (~0.6–0.9s) | Low — check which tip arrays are read by scoring | +| **Combined** | **~16–19%** | — | + +## Raw VTune data + +Results stored in `vtune-tbr-out/` (gitignored). Regenerate with: +```bash +"C:/Program Files (x86)/Intel/oneAPI/vtune/latest/bin64/vtune.exe" \ + -collect hotspots -result-dir vtune-tbr-out \ + -- Rscript dev/vtune-tbr-driver.R +``` diff --git a/dev/briefings/briefing-multistate-profile.md b/dev/briefings/briefing-multistate-profile.md new file mode 100644 index 000000000..71141492b --- /dev/null +++ b/dev/briefings/briefing-multistate-profile.md @@ -0,0 +1,354 @@ +# Briefing: Extending Profile Parsimony to >2 States + +## Status: T-101 DONE, T-102–T-107 OPEN + +## Goal +Extend profile parsimony scoring from 2-state characters to multi-state (3+). + +## What exists already + +### Current 2-state implementation (on main branch) +- `R/pp_info_extra_step.r`: `StepInformation()` — computes information content + per character for all possible step counts. Uses `LogCarter1()` for 2 states. + Lines 51-56 explicitly warn and drop states beyond 2 informative tokens. +- `R/data_manipulation.R`: `PrepareDataProfile()` — decomposes multi-state + characters into pairs (keeping top-2 informative states), compresses to + binary, builds `info.amounts` matrix. Lines 89-115: `.RemoveExtraTokens()` + keeps only 2 most informative states; line 138 asserts exactly 2 non-ambig. + Lines 196-201 hardcode `levels = c("0", "1")` and a 3×2 contrast matrix. +- `R/MaximizeParsimony.R`: Lines 424-428 call `PrepareDataProfile()`, then + lines 528-533 extract `info.amounts` attribute and pass to C++. +- C++ engine: `ts_data.cpp` copies `info_amounts` table; `ts_fitch.cpp` + looks up `info_amounts[(step-1) + info_max_steps * pattern]` for each + pattern. The C++ scoring pipeline is generic — it handles multi-state Fitch + natively. Only the R-level data prep is restricted to 2 states. + +### Prior multi-state work (on `concordance-FitchInfo` branch, NOT on main) +- `src/MaddisonSlatkin.cpp`: Full C++ implementation of the Maddison & Slatkin + (1991) recursive algorithm for counting trees with exactly s steps for a + multi-state unordered character. Supports up to 5 states. +- `R/FitchInfo.R`: Uses `MaddisonSlatkin()` for concordance scoring with + multi-state characters. Not profile parsimony weighting, but the + mathematical core is exactly what's needed. +- Key commits: `ab5f80be` "Support 2-5 states", `23963c07` "Embed MadSlat to FI", + `9336c066` "FitchInfo" (latest on concordance-FitchInfo). +- The FitchInfo code already converts MaddisonSlatkin output to cumulative + information content (bits), which is the same transformation profile + parsimony needs. + +### Key mathematical insight +The existing `MaddisonSlatkin()` computes exactly what `StepInformation()` +needs: `log P(s steps | n_0, n_1, ..., n_k leaves)` for each possible step +count s, averaged over all unrooted binary trees. This is the multi-state +generalization of Carter et al. (1990)'s theorem 1. + +Profile parsimony's information content = `log2(N_total_trees) - log2(cumsum(N_trees_with_≤s_steps))` +where `N_trees_with_exactly_s_steps = exp(MaddisonSlatkin(s, states)) * N_total_trees`. + +So `MaddisonSlatkin()` output feeds directly into `StepInformation()`. + +## Architecture: what needs to change + +### Layer 1: Mathematics (already done on branch) +`MaddisonSlatkin()` computes `log(fraction of trees with exactly s steps)`. +This is the multi-state analog of `LogCarter1()`. + +### Layer 2: `StepInformation()` (R/pp_info_extra_step.r) +Currently calls `LogCarter1()` for 2 states, rejects >2. +Needs: dispatch to `MaddisonSlatkin()` when >2 informative states. +The transformation from log-probabilities to information content is identical. + +### Layer 3: `PrepareDataProfile()` (R/data_manipulation.R) +Currently decomposes to pairs and hardcodes binary contrast matrix. +Needs: pass multi-state characters through directly (no decomposition). +Must build a proper contrast matrix for k states + ambiguous token. +The `info.amounts` matrix dimensions change (more rows = more possible steps). + +### Layer 4: C++ engine +**No changes needed.** The `info_amounts` lookup table is already generic — +indexed by `(step, pattern)`. The Fitch scoring engine already handles +multi-state characters. We just need to feed it the right contrast matrix +and info_amounts. + +## Literature + +| Reference | Role | +|-----------|------| +| Carter et al. (1990) | Exact formula for 2-state trees — current basis | +| Steel (1993) | Distribution theory for bicolored trees | +| Steel & Charleston (1995) | Properties of parsimoniously colored trees | +| Steel, Goldstein & Waterman (1996) | CLT for parsimony length | +| Maddison & Slatkin (1991) | Recursive algorithm for multi-state — the key | +| Faith & Trueman (2001) | Original profile parsimony justification | + +No known closed-form generalization of Carter for >2 states exists. +Maddison & Slatkin's recursive algorithm is the standard approach. + +## Performance concerns +- MaddisonSlatkin is exponential in number of states (2^k bitmask states) +- Current C++ implementation handles up to 5 states +- For 2 states: `LogCarter1()` is O(1) per step count +- For 3-5 states: memoized recursion, feasible for typical morphological data +- For >5 states: may need approximation or capping +- `info.amounts` computation is a one-time precomputation cost (not in search loop) + +## Risks +1. MaddisonSlatkin.cpp is on a different branch — needs careful merge/cherry-pick +2. May need to handle the interaction with character simplification (currently + characters with many states get collapsed) +3. Performance for characters with many taxa AND many states +4. Need to handle edge cases: all-ambiguous, singleton states, etc. +5. Test coverage: existing profile parsimony tests assume binary data + +--- + +## T-106: Approximation for >5 State Profile Parsimony — Research Analysis + +### 1. Scaling of exact MaddisonSlatkin + +Benchmarked on Windows, R 4.5.2, single-threaded. All timings are for +computing the full step-count range (s_min to n-1). + +| k (tokens) | n (tips) | tips/state | Time | +|:-----------:|:--------:|:----------:|-----:| +| 2 | 4 | 2 | <1 ms | +| 2 | 10 | 5 | 10 ms | +| 3 | 9 | 3 | 100 ms | +| 3 | 15 | 5 | 3.7 s | +| 4 | 8 | 2 | 320 ms | +| 4 | 12 | 3 | 12.6 s | +| 4 | 20 | 5 | timeout (>30 s) | +| 5 | 10 | 2 | timeout (>30 s) | + +**Root cause:** The recursion partitions n tips into two subtrees in +all valid ways, for each of 2^k−1 root states, for each step count s +in 0..n−k. The memoization table grows as +O(#unique_leaf_configs × max_steps × #states), and the number of +unique leaf configurations grows combinatorially with n and k. + +**Conclusion:** Exact computation is infeasible for k≥5 with n≥15, or +k≥6 at any practical n. The current code's `k ≤ 5` limit is well-placed. + +### 2. Approximation approaches evaluated + +#### (a) Plain Monte Carlo + +Sample N random unrooted binary trees, score each with Fitch, tally the +step-count distribution. + +**Test:** k=6, n=30, split=(8,7,5,4,3,3), N=10,000 random trees. +Rate: ~1,700 trees/second. Distribution: observed range 13–22, peak at 19. + +**Problem:** The exact P(s_min=5) = exp(−38.6) ≈ 1.7×10⁻¹⁷, while the +smallest observable MC probability at N=10⁴ is 10⁻⁴. The "gap" between +the minimum step count and the MC-observable range spans 8 step counts and +13 orders of magnitude. Even at N=10⁶, the gap persists. + +**Verdict:** Cannot estimate the information-rich left tail. Only useful +for the body/right tail of the distribution. + +#### (b) Normal (CLT) approximation + +Steel, Goldstein & Waterman (1996) proved asymptotic normality of +parsimony length for binary characters. Multi-state CLT should hold by +similar arguments (sum of nearly independent subtree contributions). + +**Test:** Fitted normal(μ=18.9, σ=1.5) from MC data, extrapolated to s_min. + +| Metric | Normal | Exact | +|--------|-------:|------:| +| log P(s_min=5) | −44.3 | −38.6 | +| IC(s_min) bits | 62.1 | ~55.7 | + +**The normal overestimates IC at the minimum by ~6 bits** (the true +distribution has heavier left tails than Gaussian). However, this error +is at step counts that never occur on real trees during a search. + +In the MC-observable range (13–22 steps), the normal approximation agrees +well with empirical data. This is the range that actually affects search +decisions. + +**Verdict:** Accurate in the practical range. Left-tail error is +large but irrelevant for search quality. + +#### (c) Hybrid: exact anchor + MC body + +The key insight enabling this approach: + +> **P(s_min) has an exact O(k) formula for any k:** +> `P(s_min) = NUnrootedMult(split) / NUnrooted(n)` +> +> This uses the product-of-double-factorials counting formula for labeled +> trees consistent with k non-overlapping groups, and requires no recursion. + +The hybrid approach: +1. Exact P(s_min) via `NUnrootedMult` (instant for any k) +2. MC sample of N=50,000 random trees → empirical distribution for the body +3. Normal fit to MC data → parametric extrapolation for the sub-MC left tail +4. Blend: use exact at s_min, normal extrapolation for s_min+1 to MC left + edge, empirical distribution for MC-observable range + +**Verdict:** This is the recommended approach (see §3 below). + +#### (d) "Keep top 5" (current fallback) + +The existing `StepInformation()` already handles k>5 by keeping the 5 most +frequent tokens and dropping the rest. This discards real information +(the dropped tokens contribute genuine parsimony signal) but is safe. + +For characters where the dropped tokens each have only 2–3 leaves, the +information loss is modest. For characters with 6+ well-represented tokens, +the loss is significant but hard to quantify without exact values. + +#### Other approaches considered + +- **Importance sampling** (bias toward low-step trees): Could solve the + left-tail problem but requires a carefully designed proposal distribution. + Engineering effort disproportionate to the niche use case. +- **WithOneExtraStep() extension** to k>2: Currently unimplemented. The + combinatorics are substantially harder for k>2 (multiple ways to place + the extra step among k groups). Could provide exact P(s_min+1) but would + not solve the general left-tail problem. +- **Extending MaddisonSlatkin to k=6:** Structural changes to support + 2^6−1=63 states are modest (add `StateKeyT<6>` template), but the + computational blowup still makes it infeasible for n>10–12. + +### 3. Recommendation + +**Primary approach: MC-calibrated normal approximation with exact anchor** + +This approach requires minimal new code, has well-understood error +properties, and covers the only practical use case (characters with 6+ +states in morphological datasets). + +**Why this is sufficient:** Profile parsimony's search engine only compares +info_amounts values at step counts that actually occur on candidate trees. +For a k=6 character on 30 tips, candidate trees typically score 13–22 steps +(based on MC data). The information content in this range is well-estimated +by the normal approximation calibrated to MC samples. The extreme left +tail (5–12 steps) has enormous IC values that serve as theoretical upper +bounds but never affect search decisions, because no reasonable tree +achieves those step counts. + +**Performance:** The MC sampling adds ~30 seconds per character at +N=50,000 trees (for n=30 tips). This is a one-time precomputation cost. +For datasets with few >5-state characters, this is acceptable. For +datasets with many such characters, the MC could be parallelized or the +sample size reduced. + +**Accuracy:** In the practical range (within ~3σ of the MC mean), the +normal approximation's IC values match empirical estimates to within +~0.1 bits. This is smaller than the character's own noise and does not +materially affect search quality. + +**Fallback:** If MC is too slow or the user needs a quick result, retain +the existing "keep top 5" heuristic as an option. + +### 4. Prototype R code + +```r +#' Approximate StepInformation for >5 state characters +#' +#' Uses exact P(min_steps) + MC-calibrated normal approximation. +#' +#' @param split Integer vector of token frequencies (sorted decreasing, +#' singletons removed). +#' @param n_mc Number of Monte Carlo trees to sample (default 50000). +#' @return Named numeric vector of information content (bits) per step count. +#' @keywords internal +.ApproxStepInformation <- function(split, n_mc = 50000L) { + k <- length(split) + n <- sum(split) + s_min <- k - 1L + s_max <- n - 1L + + # 1. Exact P(minimum steps) — works for any k + log_p_min <- log(NUnrootedMult(split)) - log(NUnrooted(n)) + + # 2. Monte Carlo: sample random trees, tally step counts + labels <- paste0("t", seq_len(n)) + char_vec <- rep(seq_along(split) - 1L, split) + names(char_vec) <- labels + dat <- TreeTools::MatrixToPhyDat( + matrix(char_vec, ncol = 1, dimnames = list(labels, "c1")) + ) + mc_scores <- vapply( + seq_len(n_mc), + function(i) RandomTreeScore(dat), + double(1) + ) + + # 3. Fit normal to MC data + mu_hat <- mean(mc_scores) + sd_hat <- sd(mc_scores) + + # 4. Build log-probability vector for all step counts + steps <- s_min:s_max + n_steps <- length(steps) + log_p <- numeric(n_steps) + + for (i in seq_along(steps)) { + s <- steps[i] + if (s == s_min) { + # Exact value + log_p[i] <- log_p_min + } else { + # MC estimate (with continuity correction) + mc_count <- sum(mc_scores == s) + if (mc_count > 0) { + # Direct empirical estimate + log_p[i] <- log(mc_count / n_mc) + } else { + # Normal extrapolation for unobserved step counts + log_p[i] <- dnorm(s, mu_hat, sd_hat, log = TRUE) + } + } + } + + # 5. Cumulative IC + ret <- -.LogCumSumExp(log_p) / log(2) + ret[ret < sqrt(.Machine[["double.eps"]])] <- 0 + names(ret) <- steps + + ret +} +``` + +### 5. Implementation plan + +| Step | Description | Effort | +|------|-------------|--------| +| 1 | Add `.ApproxStepInformation()` to `R/pp_info_extra_step.r` | 1 hr | +| 2 | Modify `StepInformation()`: dispatch to `.Approx...` when k>5 (instead of current top-5 truncation) | 30 min | +| 3 | Add `approx` parameter to `StepInformation()` with options `"exact"` (current), `"mc"` (new), `"auto"` (default: exact for k≤5, MC for k>5) | 30 min | +| 4 | Tests: verify MC approximation agrees with exact for k=3 within ~10% relative IC at practical step counts | 1 hr | +| 5 | Documentation: update `StepInformation()` docs to describe approximation | 30 min | + +Total: ~3.5 hours. No C++ changes needed. + +### 6. Comparison with existing "keep top 5" approach + +| Criterion | Keep top 5 | MC approximation | +|-----------|-----------|------------------| +| Speed | Instant (delegates to exact) | ~30s per character | +| Accuracy at practical range | Unknown (drops signal) | ~0.1 bit error | +| Left tail | Exact for reduced char | Exact P(min) + normal extrapolation | +| Handles any k | Yes (truncates) | Yes | +| New code | 0 lines | ~60 lines R | +| C++ changes | None | None | + +For datasets where >5-state characters are rare (typical morphology), the +MC overhead is negligible relative to the search time. For datasets with +many such characters, the top-5 fallback remains available. + +### 7. Future improvements (deferred) + +- **Exact P(s_min + 1):** Extending `WithOneExtraStep()` to k>2 would + give a second exact anchor point, improving the left-tail interpolation. + The combinatorics are non-trivial but tractable. +- **Importance sampling:** For characters where the search regularly reaches + near-minimum step counts (small n, few states per token), importance + sampling could improve accuracy. Not worth implementing unless a specific + dataset demonstrates the need. +- **Cached MC tables:** For common state-frequency patterns, pre-computed + MC tables could eliminate the per-character sampling cost. diff --git a/dev/briefings/briefing-progressive-results.md b/dev/briefings/briefing-progressive-results.md new file mode 100644 index 000000000..04fe67dac --- /dev/null +++ b/dev/briefings/briefing-progressive-results.md @@ -0,0 +1,200 @@ +# Briefing: Progressive Search Result Display + +**Task:** T-129 +**Author:** Agent A +**Date:** 2026-03-19 + +--- + +## Summary + +**Recommendation: implement progress file polling using the existing C++ callback infrastructure.** + +This approach reuses the cancel-file pattern already in place, requires minimal new +code on both the C++ and Shiny sides, and gives users real-time per-replicate feedback +without architectural changes or streaming intermediates. + +Do **not** stream partial tree results to the UI mid-search. The benefits are marginal +and the implementation cost is high (see below). + +--- + +## Context + +Searches are invoked via `ExtendedTask` wrapping `future::future({MaximizeParsimony(...)})`. +The future runs in a separate R process — no reactive communication until it resolves. +The only currently-visible progress signal is a static "Searching…" notification and +a frozen output panel. + +For short searches (<10 s), this is not a problem. For long searches (large datasets, +many replicates, long timeouts), users have no feedback that the search is alive or +making progress. + +--- + +## What Already Exists + +### C++ side: `progress_callback` (ts_driven.h / ts_rcpp.cpp) + +`DrivenParams::progress_callback` is an `std::function` +already called after every phase and every replicate. The `ProgressInfo` struct carries: + +``` +replicate — current replicate (1-based) +max_replicates — configured max +best_score — pool best so far +hits_to_best — independent discoveries of best +target_hits — convergence target +pool_size — trees currently in pool +phase — "replicate", "done", "tbr", "ratchet", etc. +elapsed_seconds — wall time since search start +phase_score — score after this phase +``` + +The Rcpp bridge (`ts_rcpp.cpp` lines 1360–1375) already accepts an optional R function +and wraps it into this callback. **The infrastructure is complete — it just isn't used +by MaximizeParsimony() yet.** + +### Shiny side: file-polling pattern (mod_search.R profile prep) + +`profilePrepTask` already uses: +1. `tempfile()` progress path passed to the background task +2. `invalidateLater(500)` observer polling the file every 500ms +3. Notification update on each poll + +This is exactly the right pattern for search progress too. + +--- + +## Recommended Approach: Progress File Polling + +### How it works + +1. Before invoking `searchTask`, create a `progressPath` temp file. +2. In the background future, after `MaximizeParsimony()` sets `TREESEARCH_CANCEL_FILE`, + also set a `TREESEARCH_PROGRESS_FILE` environment variable. +3. In `MaximizeParsimony()` (R level), if `TREESEARCH_PROGRESS_FILE` is set, pass an R + function as `progressCallback` to `ts_driven_search()`. This callback writes + a single line to the file after each replicate: `{rep} {max_rep} {best_score} {hits}`. +4. The main Shiny process polls `progressPath` every 500ms. On each poll, update the + notification text. + +### What the user sees + +Currently: +> `Searching (50 runs, k=6, 2 threads)…` + +With progress polling: +> `Searching… Rep 15/50 | Best: 42 | 3 hits` + +Or with elapsed time: +> `Searching… Rep 15/50 | Best: 42 | 3 hits | 8.2s elapsed` + +When targetHits is reached before maxReplicates, this naturally shows convergence: +> `Searching… Rep 23/50 | Best: 42 | 5/5 hits ✔ (wrapping up…)` + +### Implementation path + +**R package changes (MaximizeParsimony.R):** +- Check `Sys.getenv("TREESEARCH_PROGRESS_FILE")` before calling ts_driven_search +- If set, construct a `progressCallback` function that `writeLines()` to the file + on `phase == "replicate"` events only (skip phase-level noise) +- ~20 lines R + +**Shiny changes (mod_search.R):** +- Add `progressPath <- tempfile()` and pass it to the future alongside `cancelPath` +- Set `Sys.setenv(TREESEARCH_PROGRESS_FILE = progressPath)` in the future alongside + the cancel env var +- Add `invalidateLater(500)` observer (mirroring the profile prep observer) that + reads and parses the progress file +- On read, update the `r$searchNotification` message text +- ~30 lines R, no new UI elements + +**C++ changes:** None required. The existing `progress_callback` / `progressCallback` +infrastructure handles everything. + +**Total estimated effort:** ~2–3 hours. + +--- + +## What NOT to Build: Partial Tree Streaming + +A common wish is to "show best trees so far" during a search. This sounds appealing +but has significant problems: + +### The pool is not intermediate-result-safe + +The internal `TreePool` accumulates trees across replicates. At any mid-search point, +the pool contains a **subset of replicates' local optima** — not the final MPT set. +Trees in the pool at rep 15/50 may be suboptimal relative to the final result; the +tree topology at the "current best score" may not even survive MPT enumeration. + +Displaying these trees as search results would be misleading: users might interpret +them as MPTs, save them, or make decisions based on incomplete evidence. + +### R-level chunking doesn't help + +Splitting the search into multiple short `MaximizeParsimony()` calls (each returning +a partial result) is tempting but: +- The pool and search state don't persist across calls (each call starts fresh) +- The quality/time tradeoff from very short searches is poor (no ratchet convergence) +- This is exactly what the "Continue search" button already provides at the user level + +If users want intermediate trees, "Continue search" with small `maxReplicates` already +achieves this. + +### The right display is convergence status + +What users actually need to know mid-search is not *which trees* are in the pool, but: +- Is the search still running? (alive check) +- Is the best score improving? (convergence progress) +- How many hits to the best score so far? (convergence confidence) + +All three are available from `ProgressInfo` at the replicate level with no new C++ +work. + +--- + +## Secondary Improvement: Elapsed Timer (Trivially Easy) + +Even without the C++ callback, a simple elapsed-time counter can be added with zero +package changes: + +```r +# In mod_search.R, near the searchInProgress observer: +observe({ + req(r$searchInProgress) + invalidateLater(1000) # fire every second + elapsed <- as.integer(difftime(Sys.time(), r$searchStartTime, units = "secs")) + # Update notification text: "Searching… (42s elapsed)" +}) +``` + +This costs ~10 lines and prevents "is the app frozen?" uncertainty. However, it +provides no information about progress — a 5-minute search with a frozen score +gives the user no convergence signal. The file-polling approach is clearly superior. + +--- + +## Decision Matrix + +| Approach | Effort | Value | Verdict | +|----------|--------|-------|---------| +| Elapsed timer only | ~10 lines | Low — no convergence info | Not worth it alone | +| **Progress file polling** | **~50 lines** | **High — reps + score + hits** | **✅ Recommended** | +| Partial tree streaming | ~200+ lines + arch changes | Low — misleads user | ✗ Do not build | +| R-level chunking | ~150+ lines + pool state | Medium — duplicates "Continue" | ✗ Redundant | + +--- + +## Concrete Task Proposal + +File as **T-141** (P3): + +> **Shiny: Per-replicate search progress display** +> Use existing `progress_callback` / `TREESEARCH_PROGRESS_FILE` env var pattern +> (mirrors cancel file + profile prep). MaximizeParsimony() writes rep/score/hits +> to file on each replicate. mod_search.R polls every 500ms during search. +> Result: notification updates from static "Searching…" to live "Rep 15/50 | Best: 42 | 3 hits". +> No C++ changes needed. +> Estimate: ~2–3 hours. diff --git a/dev/build-fast.R b/dev/build-fast.R new file mode 100644 index 000000000..112d11d20 --- /dev/null +++ b/dev/build-fast.R @@ -0,0 +1,37 @@ +#!/usr/bin/env Rscript +# Fast dev build for C++-only iteration. +# +# Incremental -O2 compile (only changed translation units, via ccache + parallel +# make from ~/.R/Makevars.win), then HOT-SWAP the freshly built DLL into a target +# install library so benchmarks/tests pick it up WITHOUT a full R CMD INSTALL. +# +# Use this for C++-only edits. For changes to R/, roxygen, or [[Rcpp::export]] +# SIGNATURES, do a full `R CMD INSTALL` (compileAttributes is run here to catch +# export changes, but a signature change still needs the R wrapper reinstalled). +# +# debug = FALSE => -O2, so timing / candidate-throughput / profiling stay valid. +# (Use compile_dll(debug=TRUE) -> -O0 ONLY for logic/correctness loops, never timing.) +# +# Usage: Rscript dev/build-fast.R [target_lib=.agent-p0] +# Then run benchmarks against that lib (lib.loc / TS_LIB = target_lib). +# Do NOT have an R session holding the target DLL open (Windows file lock). + +args <- commandArgs(trailingOnly = TRUE) +lib <- if (length(args) >= 1L) args[[1]] else ".agent-p0" + +t0 <- Sys.time() +Rcpp::compileAttributes(".") # guards the stale-RcppExports trap; no-op if unchanged +pkgbuild::compile_dll(".", debug = FALSE) # incremental, -O2; recompiles only changed TUs +build_s <- as.double(difftime(Sys.time(), t0, units = "secs")) + +dll <- Sys.glob(file.path("src", "*.dll")) +dst <- file.path(lib, "TreeSearch", "libs", "x64", "TreeSearch.dll") +if (length(dll) == 1L && dir.exists(dirname(dst))) { + ok <- file.copy(dll, dst, overwrite = TRUE) + cat(sprintf("Hot-swapped %s -> %s [%s]\n", dll, dst, + if (ok) "ok" else "FAILED (DLL locked? close R sessions using this lib)")) +} else { + cat(sprintf("No hot-swap: dll matches=%d, target dir exists=%s. Run a full install into '%s' first.\n", + length(dll), dir.exists(dirname(dst)), lib)) +} +cat(sprintf("build-fast: %.1fs\n", build_s)) diff --git a/dev/dispatch/agent-brief.md b/dev/dispatch/agent-brief.md new file mode 100644 index 000000000..6cdd10472 --- /dev/null +++ b/dev/dispatch/agent-brief.md @@ -0,0 +1,84 @@ +# Dispatcher Agent Brief + +You are agent **{{AGENT_ID}}**, assigned to task **{{TASK_ID}}**: `{{TASK_ROW}}` + +## Budget & Model + +- **Budget**: {{BUDGET_MINUTES}} minutes (stay within this slice; if work won't fit, do a sub-step and check in) +- **Model assigned**: {{MODEL}} +- **Effort level**: {{EFFORT}} +- **Resume action** (if parked): {{RESUME_HINT}} + +## Workflow + +1. **Startup intake** (before claiming work): + - Triage any new user reports (`a.*` and `u.*` files in project root) + - Check `remote-jobs.md` for pending async results + - See AGENTS.md for full protocols + +2. **Read conventions**: See `AGENTS.md` for: + - Build/test/branch rules (GHA-first validation, tarball builds, `.agent-{{AGENT_ID}}/` isolation) + - Shared-file coordination (append-only for `ts_rcpp.cpp`, `TreeSearch-init.c`) + - Feature branch lifecycle and mandatory pre-commit checks + - Multi-agent workflow (worktree reserved tasks, user-report claim protocol) + +3. **Worktree rule**: If you need a worktree, create it under `../worktrees/TS-`. + **Never** switch the main `C:/Users/pjjg18/GitHub/TreeSearch` checkout to a + different branch — it must stay on `cpp-search` (or the current feature branch). + +4. **Build isolation**: Use `.agent-{{AGENT_ID}}/` as the install library + ```bash + SRC=$(pwd) && TMPBUILD=$(mktemp -d) && \ + rm -f src/*.o src/*.dll && \ + (cd "$TMPBUILD" && R CMD build --no-build-vignettes --no-manual --no-resave-data "$SRC") && \ + R CMD INSTALL --library=.agent-{{AGENT_ID}} "$TMPBUILD"/TreeSearch_*.tar.gz && \ + rm -rf "$TMPBUILD" + ``` + + **Fast C++-only iteration** (single session; not for final validation): instead of the + full tarball install above, use `Rscript dev/build-fast.R .agent-{{AGENT_ID}}` — incremental + `-O2` compile (ccache + `-j8`) that hot-swaps the DLL into the lib (~3s vs ~90s). A full + install is still required for R / roxygen / `[[Rcpp::export]]`-signature changes and any + commit/CI validation. Measurement tiers + rules in `dev/expertise/fast-iteration.md`. + +5. **Validation via GHA** (never run full test suites or R CMD check locally): + - Push your branch: `git push -u origin feature/` + - Dispatch checks: `bash gha-dispatch.sh agent-check.yml feature/` + - Poll results: `bash gha-poll.sh ` (from another agent slice; don't block) + +6. **Exit protocol**: + + **When blocking on external wait** (GHA, Hamilton, human review): + ```bash + bash dispatch.sh checkin {{AGENT_ID}} \ + --kind= \ + --ref= \ + --eta= \ + --resume="" + ``` + Exit cleanly. The dispatcher will park this task and resume when the ETA passes. + + **When complete**: + - Update `to-do.md` (delete task row; create new sections if needed) + - Add a row to `completed-tasks.md` **only** if this closed without a routine + fix (not-a-bug / superseded / negative result). Routine fixes are recorded + by the commit/PR — do not duplicate them there. + - Call: + ```bash + bash dispatch.sh checkin {{AGENT_ID}} --done + ``` + - The dispatcher will mark the agent slot as free. + +## Budget discipline + +If the work won't fit in {{BUDGET_MINUTES}} minutes: +1. Do a **meaningful sub-step** (fix one bug, implement one small feature, resolve one blocker) +2. Check in with a resume action: `bash dispatch.sh checkin {{AGENT_ID}} --kind=other --eta= --resume=""` +3. Exit cleanly rather than blowing the budget + +## Tools + +- `.AGENTS/memory/` — technical references (architecture, testing, benchmarking, conventions) +- `todo-lock.sh` — lock protocol for coordinating `to-do.md` changes +- `gha-dispatch.sh` / `gha-poll.sh` — GitHub Actions integration +- Claude Code skills — use `skill(skill: "hamilton-hpc")` for Hamilton SLURM, `skill(skill: "r-package-profiling")` for profiling diff --git a/dev/dispatch/mining-notes.md b/dev/dispatch/mining-notes.md new file mode 100644 index 000000000..56e32c71d --- /dev/null +++ b/dev/dispatch/mining-notes.md @@ -0,0 +1,91 @@ +# Mining Notes: Legacy PositAI Artifacts + +## Surviving facts from agent-*.md + +### In-progress / Parked Tasks + +- **agent-c.md: T-214 PARKED on GHA 23536512228** — Multi-split constraint enforcement bug during TBR search. Root cause identified: `classify_clip_constraints()` marks clips as UNCONSTRAINED incorrectly when constraint tips and extras straddle attachment edge. Two-part fix implemented (post-hoc `map_constraint_nodes()` + FORBIDDEN clip zone). Added test-ts-constraint-multi.R (806 assertions). Needs GHA result. + +- **agent-e.md: T-289f PARKED — GHA 23690338955 (feature/tbr-batch); Hamilton down** — Prune-reinsert PR NNI polish cost reduction. Stage 5 submitted as SLURM 16622224. Root cause of Stage 4 failure: full TBR convergence after each PR cycle (~7s per 5 cycles). New SearchControl() params added: `pruneReinsertNni` (NNI vs TBR polish) and `pruneReinsertFullMoves` (limit full-tree TBR). Stage 5 results indicate pr_nni wins 7/10 conditions; benefit dataset-dependent, reverses at >=206t. Feature not enabled in large preset, available via SearchControl(). + +### Critical Findings + +- **agent-a.md: TS-PruneRI directory orphaned** — After T-266 completion and branch deletion, local git metadata removed but directory remains (manual cleanup needed). + +- **agent-a.md: T-204 fix complexity** — GHA 23641482723 failed due to T-204's `.Deprecated()` addition to `PhyDat2Morphy`/`UnloadMorphy` causing warnings in examples (PhyDat2Morphy.Rd, MorphyWeights.Rd, GapHandler.Rd, SingleCharMorphy.Rd, Morphy.R constraint example). Fixed via WORDLIST updates and `\donttest{}`/`suppressWarnings()` wrappers. + +- **agent-a.md: S-RED focus 10 bug fixed** — precompute_profile_delta had old_cost=0 when s>info_max_steps. Fixed in commit 7cff7870 (15 tests pass). + +- **agent-a.md: PR #213 (cid-consensus) aborted** — GHA conflict: ts_tbr.cpp between CID and T-263 snapshot. Needs E/human review. + +- **agent-d.md: S-RED focus 4 — consensus stability bug in parallel path** — Idle polls incorrectly increment unchanged counter → premature termination. Identified and fixed. + +- **agent-g.md: G-006 filed** — nni_search in ts_prune_reinsert.h/.cpp lacks ConstraintData* parameter (found during S-RED Focus 30-31). + +### Completed & Merged + +- **agent-a.md: T-266 (PR #235)** — Taxon pruning-reinsertion perturbation strategy. Commit afbf531f. Phase distribution: Ratchet 46.3%, NNI-perturb 34.3%, RSS 7.4%, CSS 4.4%, XSS 3.2%, TBR 3.2%. T-274 filed for benchmarking nniPerturbCycles=0 vs 5. + +- **agent-a.md: T-270 (vignette docs)** — Completed; updated vignettes/search-algorithm.Rmd (new pipeline step 5a, post-ratchet sectorial subsection). Commit d8f3c769. + +- **agent-b.md: T-277 (PR #236 open)** — ScoreSpectrum() Chao1 landscape coverage estimator. Awaiting human review/merge. + +- **agent-b.md: T-275, T-230, T-235, T-226 completed** — Prune-reinsert EW guard, replicate-count warning gate, full_rescore after rejected SPR regraft, remove "Trees in sequence" option. + +- **agent-f.md: F-030 (PR #239, merged)** — TBR clip-ordering Phase 2. Feature/weighted-clip-order deleted; worktree TS-WeightClip pending manual deletion. + +- **agent-f.md: T-245 (PR #238, merged)** — TBR 4-wide candidate batching. + +- **agent-g.md: T-289f Stage 5 complete** — Prune-Reinsert NNI vs TBR Polish benchmark (SLURM 16622421, 7h). Five large-tree datasets (131-206t), 20 seeds, EW scoring. pr_nni wins 7/10 conditions. Not enabled in large preset (benefit dataset-dependent). Strategies.md updated. + +- **agent-g.md: T-290c** — wagnerStarts=1 vs 3 under Brazeau scoring (2 datasets, 86-91t). Preset assignments confirmed correct. + +## Notes from .positai/ + +### Expertise files copied to dev/expertise/ + +All 6 expertise files copied: +- **coordination.md** — (copy of existing coordination.md reference; kept for legacy context) +- **fitch-scoring.md** — Technical reference on Fitch scoring implementation +- **profiling.md** — R package profiling techniques and tools +- **red-team.md** — Code review and correctness verification checklists +- **shiny-app.md** — Shiny app architecture and development notes +- **tnt.md** — TNT algorithm comparison and benchmarking notes + +### Plan files copied to dev/plans/ + +- **2026-03-22-1348-full-polytomy-search-for-treesearch-c-engine.md** — In-depth design for polytomy-search (collapsed-edge optimization). Approach B chosen (binary internals + collapsed-edge flags, ~16–24 agent-days estimated vs Approach A ~9-13 weeks). No C++ changes needed beyond Phase 1–10 (regions, TBR/SPR/drift, pool dedup, ratchet, sectorial, Wagner, testing, benchmarking). TNT benchmark re-run planned to validate score parity. + +### Briefing files reviewed + +- **briefing-multistate-profile.md** — T-101 done; T-102–T-107 open. Extends profile parsimony from 2 to multi-state (3+). Reuses MaddisonSlatkin() from concordance-FitchInfo branch for multi-state information content. Recommends MC-calibrated normal approximation for >5 states with exact anchor at s_min. ~3.5-hour implementation effort estimated. + - **Decision: KEPT.** Contains non-derivable mathematical theory and prototype R code for multi-state profile parsimony. Survival value: guides T-102–T-107 task execution and performance tuning. + +- **briefing-progressive-results.md** — T-129. Recommends progress-file polling using existing C++ callback infrastructure (no C++ changes needed; TREESEARCH_PROGRESS_FILE env var). Mirrors cancel-file pattern. ~2–3 hours estimated. Max-rep/best-score/hits display during search. + - **Decision: KEPT.** Contains implementation guidance and correctness rationale (why NOT to stream partial trees mid-search). Survival value: prevents re-analysis of the rejected alternatives (partial-tree streaming, R-level chunking). + +### .positai/settings.json reviewed + +**Content:** PositAI-era Sonnet 4.6 model config + permission allowlist (edit *.md/*.h/*.cpp/*.R, bash commands, git, Hamilton-HPC + r-package-profiling skills, TreeDist/TS-MadSlat external dirs). + +**Decision:** NOT copied to dev/. Current `.claude/settings.json` supersedes this entirely (Claude Code replaces PositAI). The model ID, thinking effort, and skill references are no longer applicable (Claude Code doesn't use PositAI providers). Permission allowlist is project-specific but `.claude/settings.json` will be maintained as the canonical config. + +### .positai/skills/ directory noted + +- **hamilton-hpc/SKILL.md** — Hamilton HPC integration skill. Deferred to separate Claude Code skill setup (not copied to dev/). These become `.claude/commands/` or Claude Code integrations separately. + +--- + +## Summary of archival decisions + +| Category | Files | Action | Justification | +|----------|-------|--------|----------------| +| **expertise** | 6 files | → dev/expertise/ | Still load-bearing technical references; decoupled from PositAI | +| **plans** | 1 file | → dev/plans/ | Polytomy-search plan (16–24 agent-days) needs full context; referenced in to-do | +| **briefings** | 2 files | → dev/briefings/ | Contain non-derivable theory + implementation guidance for open tasks | +| **settings.json** | — | Discard | Superseded by `.claude/settings.json`; PositAI config no longer applicable | +| **skills/** | 1 file | Note only | Hamilton-HPC → Claude Code skill (separate setup); not duplicated | + +--- + +## Word count: 732 words (this document) diff --git a/dev/dispatch/ranker.txt b/dev/dispatch/ranker.txt new file mode 100644 index 000000000..c10261575 --- /dev/null +++ b/dev/dispatch/ranker.txt @@ -0,0 +1,43 @@ +You are a task ranker for a TreeSearch multi-agent dispatcher. Your role is to pick exactly one task to work on, given constraints on model choice and effort. + +## Input + +You receive: +- {{TODO_ROWS}}: Filtered task table (OPEN, unblocked tasks only; no PARKED, PR, or WORKTREE rows) +- {{IN_FLIGHT}}: Set of task IDs currently assigned to other agents (avoid these) +- {{BUDGET_MINUTES}}: Time budget for this slice (e.g. 15 min, 90 min) +- {{HINTS}}: Per-task hints from to-do.md Notes column (e.g. `[m:haiku e:low]` means use Haiku model, low effort) + +## Task + +1. **Pick exactly one OPEN, unblocked task** from {{TODO_ROWS}} that: + - Is NOT in {{IN_FLIGHT}} + - Has estimated work that fits comfortably in {{BUDGET_MINUTES}} minutes +2. **Respect per-task hints** if present: + - `[m:haiku]`, `[m:sonnet]`, `[m:opus]` override your model heuristic + - `[e:low]`, `[e:medium]`, `[e:high]` override your effort heuristic +3. **Model selection** (if no hint): + - **Haiku**: housekeeping, docs, triage, spelling fixes, small refactors + - **Sonnet**: normal coding, bug fixes, test work + - **Opus**: hard architecture, red-team work, complex refactors +4. **Effort** (if no hint): + - **low**: <15 min (trivial fixes, docs, triage) + - **medium**: 15–60 min (standard feature, bug fix) + - **high**: 60+ min (deep refactor, complex feature) + +## Output + +Return ONLY valid JSON (no prose, no markdown fence): + +```json +{ + "task_id": "T-XXX", + "model": "claude-haiku-4-5", + "effort": "low", + "rationale": "Brief explanation of choice", + "est_minutes": 12 +} +``` + +Valid models: `claude-haiku-4-5`, `claude-sonnet-4-6`, `claude-opus-4-7` +Valid efforts: `low`, `medium`, `high` diff --git a/dev/expertise/coordination.md b/dev/expertise/coordination.md new file mode 100644 index 000000000..466f072d4 --- /dev/null +++ b/dev/expertise/coordination.md @@ -0,0 +1,74 @@ +# Coordination Expertise — TreeSearch + +## Purpose + +Review the overall state of multi-agent work. Update `coordination.md`, +propose new tasks, resolve blockers. This is the "project manager" role. + +## Workflow + +1. **Read all agent files** (`agent-a.md` through `agent-f.md`): + - Who is working on what? + - Is anyone stuck or blocked? + - Has anyone finished a task without updating to-do.md? + +2. **Read `to-do.md`**: + - Are completed tasks moved to the Completed section? + - Are task statuses accurate? + - Are priorities still correct given current project state? + - Are there enough OPEN tasks to keep all agents busy? + - Adjust standing task priorities per the dynamic priority rule. + +3. **Read `coordination.md`**: + - Update the Agent Status table from agent files. + - Update Known Issues if any have been resolved. + - Add new Architecture Decisions if agents have made significant choices. + +4. **Read `AGENTS.md`** (bottom sections): + - Check for newly documented completed work. + - Verify that documentation matches what agents report. + +5. **Propose new tasks** if needed: + - If <6 OPEN specific tasks, look at `coordination.md` strategic + objectives and break the next one into concrete, assignable tasks. + - If agents have reported findings (from red-team or profiling), + ensure those are captured in to-do.md. + +6. **Update all files**: + - `coordination.md` — agent status, any new issues or decisions + - `to-do.md` — new tasks, priority adjustments, status corrections + - `agent-X.md` — mark your own task as complete + +## Task Creation Guidelines + +Good tasks are: +- **Specific**: "Profile ratchet inner loop for Zhu2013 dataset" not + "Investigate performance" +- **Scoped**: Completable by one agent in one session (~1-2 hours) +- **Independent**: Minimal overlap with other tasks (check Blocks column) +- **Testable**: Clear success criteria (tests pass, benchmark improves, etc.) + +When deriving tasks from strategic objectives: +- Break Phase 6 steps into individual tasks (T-001 through T-005 already done) +- For code quality work, group related TODOs into one task per file/module +- For documentation, one task per major section (vignettes, function docs, etc.) + +## Priority Guidelines + +| Priority | Criteria | +|----------|----------| +| P0 | Blocks multiple agents or causes incorrect results | +| P1 | Blocks the next strategic objective or is a correctness bug | +| P2 | Important but not blocking; performance improvements | +| P3 | Nice to have; cleanup; future-looking | + +## Cross-Agent Conflict Detection + +Watch for: +- Two agents modifying the same file (especially `ts_rcpp.cpp`, + `TreeSearch-init.c`, `R/RcppExports.R`) +- Incompatible parameter changes to the same Rcpp bridge function +- One agent's optimization breaking another's assumptions + +If conflicts are detected, flag them in `to-do.md` as P0 and note +which agents are affected. diff --git a/dev/expertise/fast-iteration.md b/dev/expertise/fast-iteration.md new file mode 100644 index 000000000..f3f22f8f3 --- /dev/null +++ b/dev/expertise/fast-iteration.md @@ -0,0 +1,113 @@ +# Fast iteration for profiling/benchmark work + +How to keep the edit→measure loop in seconds–minutes instead of 10+ minutes. +Validated 2026-06-16 (Windows / Rtools45 / R-devel, i7-10700 8C/16T). Numbers below +are measured on this box. + +## TL;DR loop + +| Step | Command | Stop condition | Wall-clock | +|---|---|---|---| +| Build (C++ edit) | `Rscript dev/build-fast.R [lib]` | — | **~3s** (1 TU) / 0.5s no-op | +| Build (header edit) | `rm -f src/*.o && Rscript dev/build-fast.R [lib]` | — | **~19s** (ccache+`-j8`) | +| Smoke (every edit) | `Rscript dev/benchmarks/bench_smoke.R` | `maxReplicates` | **~0.2s**, exit 1 on regress | +| Iterate gate (pre-commit) | `Rscript dev/benchmarks/bench_iterate.R` | `maxReplicates` | ~1–2 min | +| Batch panel | `Rscript dev/benchmarks/bench_parallel.R` | `maxReplicates` | ~5–7× serial | +| Hamilton panel | `hamilton_*` job-array chain (below) | `maxReplicates` | ~10–15 min incl. queue | + +Two iron rules (the reason this works): +1. **Replicate-bounded, never `maxSeconds`, for any candidate/score signal.** + `candidates_evaluated` is only deterministic when the run stops on replicate count + (or target hits). Under `maxSeconds`, replicates-completed (hence candidates) depend on + machine load — non-reproducible. Verified: serial vs parallel give *identical* candidate + counts when replicate-bounded. +2. **Measure with the pool drained.** Any candidate/timing number must run alone + (8 physical cores, memory-bandwidth-bound Fitch). The process pool / SLURM array is for + *batch* panels only, never for the single authoritative measurement. + +## One-time machine setup (global, reversible) + +Already applied on this box. To reproduce elsewhere: + +1. **ccache** (object cache). `ccache --max-size=20G` (3GB thrashes — it was 99.96% full). +2. **`~/.R/Makevars.win`** — wires ccache + parallel make into *every* build while + preserving `-O2`. On Windows this file SHADOWS `~/.R/Makevars` (no merge), so the `-O2` + flags are reproduced in it; do not drop them or timings silently become invalid: + ```make + MAKEFLAGS = -j8 + CCACHE = ccache + CC = $(CCACHE) gcc + CXX = $(CCACHE) g++ + CXX11 = $(CCACHE) g++ + CXX14 = $(CCACHE) g++ + CXX17 = $(CCACHE) g++ + CXXFLAGS = -g -O2 -Wall -mfpmath=sse -msse2 -mstackrealign + PKG_CXXFLAGS = + ``` + Revert by deleting the file. + +## Build: `dev/build-fast.R` + +`compileAttributes()` (guards the stale-`RcppExports` trap) + `pkgbuild::compile_dll(debug=FALSE)` +(incremental, **-O2** — recompiles only changed TUs) + hot-swap the DLL into the install lib +(`.agent-p0` by default) so benchmarks pick it up without a full `R CMD INSTALL`. + +- **C++-only edits:** `Rscript dev/build-fast.R`. ~3s for one `.cpp`. +- **Header edits:** R's Makefile has no header-dependency tracking (a `.h` change recompiles + nothing), so `rm -f src/*.o` first, then build-fast — ccache + `-j8` makes the forced full + rebuild ~19s, not ~90s. +- **R / roxygen / `[[Rcpp::export]]` signature changes:** need a full `R CMD INSTALL` + (build-fast only rebuilds C++). For a release/CI build, full install + tests. +- **-O0 fast builds** (`compile_dll(debug=TRUE)`, ~2–4× faster compile) are for + *correctness/logic* loops only — tag them to a throwaway lib and NEVER benchmark them. + Candidate *counts* are opt-invariant (safe at -O0); *timing/throughput/profiles* are not. + +Do not have an R session holding the target DLL open during a hot-swap (Windows file lock). + +## Measurement tiers + +- **`bench_smoke.R`** — breakage tripwire. One R process, 3 tiny datasets, `maxReplicates=4`, + seed 1. Compares score (must be unchanged) + candidates (±5%) vs `smoke_baseline.csv`; + exit 1 on regress. NOT a ship gate — tiny datasets don't exercise sectorial search. + Re-baseline with `SMOKE_WRITE_BASELINE=1`. +- **`bench_iterate.R`** — the real lever gate. Gap panel, fixed `maxReplicates`, 2–3 seeds, + median candidates + score + gap-to-TNT. A win = lower median candidates at equal/better + score. ~0.7% seed spread on candidates (vs the ±2–4 step score lottery) → 2–3 seeds suffice. +- **`bench_parallel.R`** — PSOCK pool batch runner (`conc = cores − TS_HEADROOM`, OMP=1 per + worker). For batch panels/sweeps only. Raise `TS_HEADROOM` while anything else runs. +- **`bench_tnt_headtohead.R`** — full TNT comparison (validate tier); `bench_phase_yield.R` — + per-phase wall-clock shares. + +## Hamilton job-array (parallel validation panels) + +Convert the serial in-R panel loop to one SLURM task per `(dataset × seed)` cell. Chain: +build once → array → merge. + +```bash +# from a Hamilton login node (scripts scp'd to /nobackup/$USER/TreeSearch/scripts, CRLF-stripped) +bid=$(sbatch --parsable hamilton_build_once.sh) # compile ONCE into shared $LIB +aid=$(sbatch --parsable --dependency=afterok:$bid hamilton_panel_array.sh) # one cell per array task +sbatch --dependency=afterany:$aid hamilton_merge.sh # rbind partials -> panel.csv +``` + +- `bench_cell.R` runs one cell from `$SLURM_ARRAY_TASK_ID` (replicate-bounded), writing one + partial CSV; locally testable: `Rscript dev/benchmarks/bench_cell.R 0`. +- Array tasks NEVER build (the `afterok` build job populates a read-only `$LIB`). +- Collapses a ~4.5h serial panel to ~10–15 min incl. queue (not the optimistic 3–5 min: + count SLURM queue + ~2–5s NFS R-startup per cell + the build/merge jobs). +- **Blocker for the authoritative wall-clock head-to-head:** there is NO 64-bit Linux TNT on + Hamilton (no `module load tnt`, none under `/nobackup`). Candidate-count comparison is + bitness-independent (valid on local 32-bit TNT); only the wall-clock ratio needs a 64-bit + TNT staged under `/nobackup` and fed to `bench_tnt_headtohead.R` via `TNT_EXE`. The + Hamilton `*_hamilton.sh` SLURM templates here are **dispatch-untested** (cell logic is + validated locally); smoke them in `test.q` (5-min partition) before a full run. + +## Planned refinement: a true candidate-budget stop + +The cleanest iterate signal is "score at a fixed candidate budget" / "candidates to reach a +target score", which removes even the ~0.7% per-seed replicate-spend variance. Needs a small +C++ change: a `max_candidates` guard (and optional `target_score` early-exit) checked at the +replicate boundary in `src/ts_driven.cpp` (alongside the timeout at ~`:627`, the timed-out +set at ~`:897`/`:1043`, and the target-hits check at ~`:1006`), plumbed through +`runtimeConfig` → `ts_rcpp.cpp` → `SearchControl`. Until then, fixed `maxReplicates` is the +deterministic signal. diff --git a/dev/expertise/fitch-scoring.md b/dev/expertise/fitch-scoring.md new file mode 100644 index 000000000..4f74afa9f --- /dev/null +++ b/dev/expertise/fitch-scoring.md @@ -0,0 +1,136 @@ +# Fitch Scoring — Design Notes & Proven Invariants + +Reference for agents working on `ts_fitch.h/.cpp`, `ts_fitch_na.h`, +`ts_fitch_na_incr.h`, or the search modules that call them. + +## Incremental uppass correctness (standard Fitch) + +The incremental uppass (`fitch_incremental_uppass`) uses a dirty-flag +propagation scheme that does **not** explicitly revisit every node whose +prelim changed during the incremental downpass. Only nodes whose +*ancestor's final* changed are recomputed. + +This looks like it could miss updates when the downpass stops before +root (prelim stabilises at some intermediate node N). Nodes between +`clip_ancestor` and N have changed prelims but their ancestors' finals +are unchanged, so the dirty-flag scheme skips them. + +**This is provably correct for standard (non-NA) Fitch blocks.** + +### Proof sketch + +When the downpass stops at node N, `fitch(M_new, S) = fitch(M_old, S)` +where M is N's child on the downpass path and S is the sibling. + +**Case 1 — both intersection-type:** `M_old ∩ S = M_new ∩ S = P`. +Then N_final ⊆ P ⊆ M_old and N_final ⊆ P ⊆ M_new. So +`uppass(N_final, M_old) = N_final ∩ M_old = N_final` and likewise for +M_new. Finals are identical. + +**Case 2 — both union-type:** `M_old ∪ S = M_new ∪ S` with +`M_old ∩ S = ∅` and `M_new ∩ S = ∅`. Since the unions are equal and +both M sets are disjoint from S, `M_old = M_new`. No change. + +**Case 3 — mixed types:** Intersection equals union only if both +operands are identical and the set is trivial. Not reachable in +practice (would require empty state sets). + +The argument applies per-character (per bit position), so it holds +for packed 64-bit representations. + +### Consequence + +No code change needed. The dirty-flag scheme is an optimisation that +happens to be exact for standard Fitch, not just a heuristic. + +--- + +## NA uppass `children_app` staleness + +The NA-aware incremental uppass (`fitch_na_incremental_uppass`) has a +**theoretical staleness issue** that does NOT affect standard blocks. + +The NA uppass formula at internal nodes uses: + +```cpp +uint64_t children_app = 0; +for (int s = 1; s < k; ++s) + children_app |= (tree.prelim[left + s] | tree.prelim[right + s]); +``` + +This `children_app` can change even when the node's own prelim is +stable, because the NA downpass aggregates children differently (using +intersection/union/strip cases) from the raw OR of children's states. + +If the downpass stops at node N because N's NA-aware prelim didn't +change, but N's child M *did* change prelim, then `children_app` at N +is different from before. The dirty-flag scheme won't revisit N, so +N's `final_` for NA blocks may be stale. + +### Impact + +- `fitch_na_pass3_score()` uses `final_` for `ss_app` (applicability). + A stale `ss_app` can make `divided_length` slightly wrong. +- Indirect length calculations use `final_` for virtual-root + computation, so candidate scores can be slightly wrong. +- **Conservative**: `full_rescore()` always runs before accepting a + move, so final results are never affected. +- Same design class as the documented `extract_divided_steps` heuristic + (ts_tbr.cpp:39-41) which uses stale `local_cost` for NA blocks. + +### If this ever needs fixing + +Mark the entire rootward path from `clip_ancestor` as dirty: + +```cpp +int node = clip_ancestor; +while (node != root) { + dirty[node] = true; + node = tree.parent[node]; +} +``` + +This is O(depth) extra work per clip, acceptable for correctness. +Currently not worth doing because full_rescore is authoritative. + +--- + +## upweight_mask coverage + +During ratchet perturbation, `upweight_mask` doubles the contribution +of selected characters. Every function that computes EW step counts +must account for it. The pattern: + +```cpp +int ns = popcount64(needs_step); +if (blk.upweight_mask) ns += popcount64(needs_step & blk.upweight_mask); +extra_steps += blk.weight * ns; +``` + +**Sites that must have this** (all verified correct as of 2026-03-19): + +| Function | File | Status | +|----------|------|--------| +| `fitch_downpass` | ts_fitch.cpp | ✓ | +| `fitch_incremental_downpass` | ts_fitch.cpp | ✓ | +| `fitch_indirect_length` | ts_fitch.cpp | ✓ | +| `fitch_indirect_length_bounded` | ts_fitch.cpp | ✓ (fixed T-096) | +| `fitch_indirect_length_cached` | ts_fitch.cpp | ✓ (fixed T-096) | +| `fitch_na_indirect_length` | ts_fitch_na_incr.h | ✓ | +| `fitch_na_indirect_length_bounded` | ts_fitch_na_incr.h | ✓ | +| `fitch_na_indirect_length_cached` | ts_fitch_na_incr.h | ✓ | +| `fitch_na_score` Pass 1 (standard blocks) | ts_fitch_na.h | ✓ | +| `fitch_na_score` Pass 3 | ts_fitch_na.h | ✓ | +| `fitch_na_pass3_score` | ts_fitch_na_incr.h | ✓ | +| `fitch_na_incremental_downpass` (standard blocks) | ts_fitch_na_incr.h | ✓ | +| `nx_cost` in TBR | ts_tbr.cpp | ✓ (fixed T-096) | +| `nx_cost` in SPR | ts_search.cpp | ✓ (fixed T-096) | +| `nx_cost` in drift | ts_drift.cpp | ✓ (fixed T-096) | +| drift RFD computation | ts_drift.cpp | ✓ (fixed T-096) | + +**Does NOT need upweight_mask:** +- `extract_char_steps` / `extract_divided_steps` — these extract raw + per-pattern step counts for IW/profile scoring, which uses + `pattern_freq` doubling instead of `upweight_mask`. +- `fitch_downpass_node` (standalone) — callers handle weighting. +- IW indirect variants — weighting baked into `iw_delta`. diff --git a/dev/expertise/profiling.md b/dev/expertise/profiling.md new file mode 100644 index 000000000..7846cc6d0 --- /dev/null +++ b/dev/expertise/profiling.md @@ -0,0 +1,586 @@ +# Profiling Expertise — TreeSearch + +## Purpose + +Profile the C++ search engine to identify bottlenecks. Produce specific, +actionable optimization tasks for `to-do.md`. + +## Tools + +### 1. Built-in Phase Timing (Quick) + +The driven search already has `std::chrono` phase timing at `verbosity >= 2`. +Use the R-level interface: + +```r +library(TreeSearch) +library(TreeTools) +dataset <- TreeSearch::inapplicable.datasets[["Vinther2008"]] +result <- MaximizeParsimony(dataset, maxReplicates = 3, verbosity = 2L) +``` + +This prints per-phase timing. For programmatic access, use the +`ts_bench_tbr_phases` diagnostic function (7 args, registered in +TreeSearch-init.c). + +### 2. std::chrono Micro-Benchmarks (Medium) + +For fine-grained timing of specific functions, add `steady_clock` timing +around the code path of interest. See `inst/benchmarks/bench_memory.R` +and `inst/benchmarks/bench_simd.R` for examples. + +Key metrics to measure: +- Per-candidate indirect scoring cost (ns) +- Clip+incremental phase time (μs per TBR pass) +- Full rescore time (μs) +- Snapshot save/restore time (μs) + +### 3. VTune (Thorough) + +For instruction-level hotspot analysis, use the `r-package-profiling` +skill (load via the skill tool). Key steps: + +1. Build with debug symbols: set `DLLFLAGS` via `MAKEFLAGS` env var +2. Run a representative workload under VTune +3. Analyze hotspots in the VTune GUI + +See `.positai/skills/r-package-profiling/references/` for detailed +VTune workflow on Windows. + +**Current version: VTune 2025.10** (updated 2026-03-19). Requires Ice Lake +or newer CPU (10th gen Intel Core / 3rd gen Xeon Scalable+). VS 2019 +integration and Eclipse integration are removed in 2025.x. Command-line +workflow (`vtune -collect hotspots`) is unchanged. + +### 4. R-Level Profiling + +For R overhead identification: + +```r +Rprof("profile.out") +result <- MaximizeParsimony(dataset, maxReplicates = 5) +Rprof(NULL) +summaryRprof("profile.out") +``` + +## Known Baselines + +### Latest run: 2026-03-27 by Agent A (round 6: post-T-261/T-262/T-263 phase distribution) + +See "Phase distribution: current thorough preset" section below for updated numbers. +The 2026-03-18 baselines used strategy='none' (TBR-only); the thorough preset +now dominates medium-scale search, making direct comparison impractical. + +### Previous run: 2026-03-18 16:00 by Agent A (v2.0.0, single-agent, quiet machine) + +Previous baselines (2026-03-17) were inflated ~30–40% by multi-agent machine +contention. Scores are identical. Timings below are authoritative. + +### End-to-end benchmarks (3-run medians, 5 reps, strategy='none', EW): + +| Dataset | Tips | Chars | Median (s) | Score | +|---------|------|-------|------------|-------| +| Vinther2008 | 23 | 57 | 0.390 | 79 | +| Agnarsson2004 | 62 | 242 | 1.860 | 778 | +| Zhu2013 | 75 | 253 | 2.720 | 655 | +| Dikow2009 | 88 | 220 | 3.860 | 1614 | + +### Per-phase breakdown (Zhu2013, 5 reps, two runs averaged): + +| Phase | % of time | Avg ms/rep | +|-------|-----------|------------| +| Wagner | <0.1% | <1 | +| TBR | 24–37% | 110–160 | +| XSS | 10% | 35–55 | +| RSS | 2% | 9–13 | +| Ratchet | 24–28% | 90–155 | +| Drift | 25–33% | 90–200 | +| Final TBR | 2% | 7–10 | + +Ratchet (24-28%) and drift (25-33%) dominate. TBR (24-37%) varies +substantially by run. XSS ~10%, RSS ~2%, both stable. + +### Wagner tree construction: Negligible (<0.1% of search time) + +| Dataset | Tips | µs/tree | % of replicate | +|---------|------|---------|----------------| +| Vinther2008 | 23 | 300 | <0.1% | +| Agnarsson2004 | 62 | 1000 | 0.3% | +| Zhu2013 | 75 | 600 | 0.1% | +| Dikow2009 | 88 | 1400 | 0.2% | + +Not a bottleneck at any dataset size. No optimization needed. + +### Parallel scaling (2 threads) + +| Dataset | Reps | 1T (s) | 2T (s) | Speedup | Efficiency | +|---------|------|--------|--------|---------|------------| +| Zhu2013 | 5 | 2.53 | 1.59 | 1.59× | 80% | +| Zhu2013 | 10 | 5.16 | 3.29 | 1.57× | 78% | +| Zhu2013 | 20 | 10.70 | 5.20 | 2.06× | 103%* | +| Zhu2013 | 40 | 18.63 | 11.35 | 1.64× | 82% | +| Dikow2009 | 10 | 7.76 | 5.11 | 1.52× | 76% | + +*Superlinear at 20 reps is stochastic noise (different search paths). + +**Finding:** Typical 2-thread efficiency is 78–82%. The old 1.24× measurement +was a multi-agent machine contention artifact. The implementation (dynamic +work-stealing via `atomic::fetch_add`, mutex-guarded pool) is sound. +Main loss is stochastic load imbalance between replicate times. + +### XSS/RSS effectiveness (5 reps per dataset) + +| Dataset | Tips | XSS hits | XSS avg Δ | XSS avg ms | RSS hits | RSS avg Δ | RSS avg ms | +|---------|------|----------|-----------|------------|----------|-----------|------------| +| Agnarsson2004 | 62 | 3/5 | 3.8 steps | 59 | 0/5 | 0 | 14 | +| Zhu2013 | 75 | 5/5 | 26.6 steps | 43 | 2/5 | 1.0 | 11 | +| Dikow2009 | 88 | 0/5 | 0 | 93 | 1/5 | 3.2 | 29 | + +**Finding:** XSS effectiveness is highly dataset-dependent — from zero +improvement (Dikow2009) to 27-step average improvement (Zhu2013). No obvious +predictor from simple nTip/nChar statistics. XSS cost is ~10% of replicate +time; acceptable when effective but wasted when not. + +RSS is marginal across all datasets (0–3 steps, 2% of time). One exception: +Dikow2009 where RSS found 16 steps while XSS found 0 — suggests they +explore different neighbourhoods. + +### Auto strategy (reference — unchanged from T-066/T-068 study) + +Threshold: ≥75 tips AND nChar < 100 triggers "thorough". Signal-density gate +prevents unnecessary thorough runs on character-rich datasets. + +### R overhead: <0.5% of wall time (confirmed via Rprof, unchanged) + +### Scaling exponent: ~2.82 (TBR pass time vs tips, unchanged) + +### Drift/ratchet cycle tuning (reference — unchanged from T-029 study) + +| Config | Med score | Min score | Med time | Speedup | +|--------|-----------|-----------|----------|---------| +| d5_r5 (default) | 656 | 648 | 5.7s | — | +| d2_r5 | 660 | 646 | 4.1s | 28% | +| d2_r2 | 662 | 656 | 3.8s | 33% | +| d0_r5 | 658 | 650 | 2.8s | 51% | +| d5_r0 | 662 | 660 | 4.8s | 16% | + +Lower score = better. Current defaults: d2_r5. + +### CSS effectiveness: Marginal (adds 2-6% time, no consistent improvement) +Disabled by default (cssRounds=0). + +### Latest EW regression check: 2026-03-19 by Agent A (v2.0.0, post T-115–T-124) + +All datasets pass regression benchmark. EW baselines updated with 7-run medians: + +| Dataset | Tips | Chars | Median (s) | Score (range) | Notes | +|---------|------|-------|------------|---------------|-------| +| Vinther2008 | 23 | 57 | 0.420 | 79 | stable | +| Agnarsson2004 | 62 | 242 | 1.790 | 778 | stable | +| Zhu2013 | 75 | 253 | 3.170 | 648–666 | high variance (2.5–7.6s range) | +| Dikow2009 | 88 | 220 | 4.900 | 1612–1614 | high variance (4.0–12.4s range) | + +Zhu2013/Dikow2009 appear slightly slower than 2026-03-18 baselines (~17–27%) but +within stochastic noise. Phase breakdown unchanged. No regression in C++ engine. +The recent DataSet changes (inapp_state field, HSJ/XFORM modes) have no measurable +effect on EW search paths. + +### HSJ and XFORM scoring baselines: 2026-03-19 by Agent A + +Synthetic hierarchical datasets (valid hierarchy structure: primary + secondary chars, +secondaries are inapplicable when primary absent). 3-run medians, 5 reps per run. + +| Config | Tips | Chars | Blocks | EW (s) | HSJ (s) | XFORM (s) | HSJ/EW | XFORM/EW | +|--------|------|-------|--------|--------|---------|-----------|--------|----------| +| small | 20 | 19 | 3 | 0.020 | 0.010 | 0.020 | 0.5× | 1.0× | +| medium | 40 | 50 | 5 | 0.170 | 0.100 | 0.280 | 0.6× | 1.6× | +| large | 60 | 82 | 8 | 0.610 | 0.360 | 1.330 | 0.6× | 2.2× | +| xlarge | 80 | 120 | 10 | 5.920 | 3.560 | 9.460 | 0.6× | 1.6× | + +**HSJ is faster than EW** (~0.6× at medium/large sizes) because: +1. Fitch candidate screening guards expensive full HSJ rescore — most candidates + are rejected by Fitch before HSJ is called. +2. Hierarchy datasets have a simpler parsimony landscape (secondaries add signal + only when primary is present), leading to faster search convergence. + +**XFORM is slower than EW** (~1.6–2.2× at medium/large sizes) due to Sankoff +cost per candidate. Phase breakdown (large config, 5 reps): + +| Phase | EW avg ms/rep | HSJ avg ms/rep | XFORM avg ms/rep | +|-------|---------------|----------------|------------------| +| TBR | 25 | 23 | 29 | +| XSS | 14 | 7 | 14 | +| RSS | 4 | 2 | 5 | +| Ratchet | 51 | 28 | 86 | +| Drift | 22 | 13 | 36 | +| Final TBR | 2 | 1 | 4 | +| **Total** | **117** | **74** | **174** | + +XFORM overhead concentrated in Ratchet (+69%) and Drift (+64%), which perform +more scoring iterations than TBR. XSS/RSS overhead is negligible. + +**Conclusion:** Both modes are acceptable. XFORM at ~1.7× overhead for real +workflows is reasonable given the algorithmic complexity (Sankoff vs Fitch). +No optimization tasks raised — XFORM at this cost is expected behavior. + +### Hierarchical resampling: 2026-03-19 by Agent A + +Medium config (40 tips, 50 chars, 5 blocks), jackknife, 20 reps: + +| Mode | 1 thread (s) | 2 threads (s) | Speedup | +|------|-------------|--------------|---------| +| Brazeau (C++ parallel) | 5.19 | 2.05 | 2.5× | +| HSJ hierarchical (serial R loop) | 1.76 | 1.64 | 1.1× | +| XFORM hierarchical (serial R loop) | measured via 10-rep: ~1.58 | — | — | + +**Finding 1 (positive):** HSJ/XFORM hierarchical resampling is faster than Brazeau +per-replicate because the block-level resampling units (35 vs 50 units) produce +simpler per-replicate datasets. No performance concern here. + +**Finding 2 (known limitation):** Hierarchical resampling uses a serial R loop +across replicates — `nThreads` only applies within each replicate's internal search. +Brazeau gets full 2.5× at 2 threads; HSJ/XFORM get only ~1.1×. For users running +50–100 jackknife replicates with large HSJ/XFORM datasets, wall time will be ~2× +longer than equivalent Brazeau. This is documented in AGENTS.md as a known future +optimization (C++-level inter-replicate parallelism for hierarchical resampling). +No new task filed — already on the roadmap. + +### Preset tuning benchmark: 2026-03-22 by Agent A + +Compared updated presets (wagnerStarts=3, sprFirst=TRUE, adaptiveLevel=TRUE +for default; wagnerStarts=3, sprFirst=TRUE for thorough) against old presets +(wagnerStarts=1, sprFirst=FALSE, adaptiveLevel=FALSE). 7-run medians via +`MaximizeParsimony()`, strategy=auto, 10 reps, 1 thread. + +| Dataset | Tips | Preset | Old time (s) | New time (s) | Δ time | Old score | New score | +|---------|------|--------|-------------|-------------|--------|-----------|-----------| +| Vinther2008 | 23 | sprint | 0.76 | 0.65 | –14% (noise) | 79 | 79 | +| Agnarsson2004 | 62 | default | 3.59 | 2.41 | **–33%** | 778 | 778 | +| Zhu2013 | 75 | thorough | 23.65 | 24.83 | +5% (noise) | 647 | 648 | +| Dikow2009 | 88 | thorough | 49.19 | 39.24 | **–20%** | 1611 | 1612 | + +**Findings:** +- `adaptiveLevel` in `default` preset: consensus-stability triggers early exit + on easy landscapes (Agnarsson2004), saving 33%. No score regression. +- `sprFirst + wagnerStarts=3` in `thorough`: 20% faster on Dikow2009 (better + starting tree reduces initial TBR descent). Neutral on Zhu2013. +- **Do not enable `adaptiveLevel` in `thorough`**: with 20 ratchet + 12 drift + base, 1.5× scaling creates 30 ratchet + 18 drift per hard replicate, + causing 3–4× slowdowns for only 2–3 step improvement (benchmarked separately). + +### 180-tip large-preset baselines: 2026-03-26 by Agent E (Hamilton HPC, EPYC 7702) + +Dataset: mbank_X30754 (180 taxa, 425 chars, 418 patterns, 40% missing, 20% inapplicable). +Strategy: auto → "large" preset. 5 seeds per budget, single-threaded. + +**Score quality by budget (median, 5 seeds):** + +| Budget | Median score | Range | Reps/seed | +|--------|:-----------:|:-----:|:---------:| +| 30s | 1202 | 1189–1214 | ~1.5 | +| 60s | 1190 | 1190–1202 | ~3 | +| 120s | 1185 | 1171–1189 | ~6 | + +Per-replicate time: median 17.3s (range 13.7–21.2s). MPT enumeration adds +0–2 steps beyond best single-replicate score. + +**Phase distribution (rep 1, 30s budget, 5-seed averages):** + +| Phase | % time | Mean ms | Steps/s | Hit rate | +|-------|:------:|--------:|:-------:|:--------:| +| TBR | 43.6% | 7313 | 91.4 | 5/5 (661 steps avg) | +| Ratchet | 32.2% | 5390 | 4.5 | 5/5 (26.6 steps avg) | +| SA (anneal) | 7.4% | 1241 | 0.8 | 7/50 (14%, 1.3 steps) | +| XSS | 5.4% | 897 | 13.8 | 4/5 | +| Wagner+NNI | 4.7% | 790 | — | starting point | +| RSS | 3.2% | 530 | 4.8 | 3/5 | +| CSS | 2.5% | 424 | 11.2 | 2/5 | +| Final TBR | 1.0% | 174 | 5.2 | 1/5 | + +**SA (simulated annealing) phase is the least productive:** 7.4% of time, +14% hit rate (7/50 reps improved by 1.3 steps on average). Efficiency = +0.8 steps/s, far below ratchet (4.5) or XSS (13.8). annealCycles=3, +annealPhases=5 may be overtuned. Reducing could save ~1.2s/rep → 1 extra +replicate per ~17s saved. + +**Comparison with earlier Intel desktop baselines (T-179, pre-T-206):** + +| Budget | Intel (pre-T-206) | EPYC (post-T-206) | Delta | +|--------|:-:|:-:|:-:| +| 30s | 1276 | 1202 | −74 | +| 60s | 1255 | 1190 | −65 | +| 120s | 1250 | 1185 | −65 | + +The 65–74 step gap is **primarily due to T-206** (outer cycle reset cap), +not hardware. T-206 was merged 2026-03-24 19:27; the Intel baselines were +recorded at 12:56 the same day (pre-T-206). Without the reset cap, each +replicate performed 3–5 pipeline cycles (~51–85s) vs ~17s with cap=0. +At 120s budget: ~2 replicates pre-T-206 vs ~6 post-T-206. Hardware +differences (Intel desktop vs EPYC 7702) are a secondary factor. + +### Phase distribution: current thorough preset (2026-03-27, Agent A, round 6) + +Dataset: Zhu2013 (75t, 253 chars). Strategy: auto → thorough. +3 reps, single-threaded, post-T-261+T-262+T-263. Total: 33.7 s = ~11.2 s/rep. + +| Phase | Calls | Total ms | Mean ms | % | +|-------|:-----:|:--------:|:-------:|:---:| +| Ratchet | 14 | 15617 | 1116 | 46.3% | +| NNI-perturb | 14 | 11565 | 826 | **34.3%** | +| RSS | 14 | 2488 | 178 | 7.4% | +| CSS | 14 | 1477 | 106 | 4.4% | +| XSS | 14 | 1079 | 77 | 3.2% | +| TBR (post-phase) | 14 | 622 | 44 | 1.8% | +| Initial TBR | 3 | 468 | 156 | 1.4% | +| wag+NNI | 2 | 427 | 214 | 1.3% | + +**Key findings vs 2026-03-18 baselines:** + +1. **TBR is no longer a bottleneck** (1.4% + 1.8% = 3.2%). T-261+T-262+T-263 + combined are working — TBR has become fast enough that other phases dominate. + Drift was 25–33% before T-255; its removal freed that budget to more ratchet. + +2. **NNI-perturb at 34.3% with poor efficiency:** + - Hit rate: 14% (2/14 calls improved score) + - Mean improvement when hit: 1 step + - Efficiency: 0.17 steps/s vs ratchet's ~4–8 steps/call at comparable cost + - Cost grows within a replicate (early calls ~300ms, late calls ~1300ms) + - This phase likely over-tuned for 75-tip datasets. Filed **T-274** (P2). + +3. **RSS at 7.4%** — higher than old 2% baseline. With conflict-guided RSS and + outerCycles/reset mechanism creating ~4.7 RSS calls per replicate at ~178ms each + (~837ms/rep). Old uniform RSS: ~11ms/rep. 16× overhead increase. Most of this + is the actual sector TBR cost (more calls × similar per-sector time), not conflict + computation overhead. The reset mechanism is the multiplier. + +4. **wag+NNI at 1.3%**: biased Wagner + 3 starts + NNI warmup adds ~214ms per + replicate start. Negligible at this scale; confirms T-246/NNI-warmup tuning is fine. + +### Round 7 — 2026-06-16 — STANDARD-FITCH path (TNT-parity), first profile + +**Critical distinction:** all rounds above profiled the **NA three-pass path** +(raw `inapplicable.phyData`). The TNT benchmark compares the **standard-Fitch** +path: inapplicable `-` replaced with `?` so `has_na=FALSE` and the flat / 4-wide +(T-245) kernels run — NOT `fitch_na_*`. This path is **much cheaper per replicate** +(0.56 s/rep here) with a *completely different* hotspot mix — the NA three-pass adds +~3-4× scoring work plus subtree_actives bookkeeping. (The round-6 NA ~11 s/rep figure +is NOT a clean comparison — it also ran now-disabled phases, NNI-perturb + outer resets.) +Driver: +`dev/profiling/drivers/fitch-tnt.R` (Zhu2013 `-`→`?`, auto→thorough, nThreads=1). +Score 627 vs TNT 1.6 = 624 (was +11 in March; now +3 — near parity). + +Phase distribution (standard Fitch, attr "timings"): **ratchet 63%**, rss 9.2%, +xss 9.2%, css 6.8%, wagner 5.5%, tbr 4%, final_tbr 2.3%. (Contrast the NA/thorough +round-6 table above — there ratchet 46% + NNI-perturb 34%; NNI-perturb now off.) + +VTune top functions (TreeSearch.dll self, total 2.70 s; names via `nm`): +| % DLL | function | note | +|------:|----------|------| +| 25.1 | `tbr_search` (orchestration self) | candidate-loop control + `vector` collapsed/sector bit-tests + inlined scoring | +| 14.5 | `simd::any_hit_reduce_avx2` | **AT-LIMIT** — disasm: GCC already elides the `hor_or256` store-reload | +| 13.2 | `uppass_node` | scalar update loop; **AT-LIMIT** — vectorising = 1.22× only, nil for 2-state (micro-bench) | +| 6.3 | `any_hit_reduce3_avx2` | SPR-bounded reduce | +| 5.2 | `build_postorder_prealloc` | O(n) rebuild **per clip AND per accept** | +| 4.1 | `fitch_incremental_downpass` | per clip | +| 4.0 | `fitch_indirect_bounded_flat` | SPR candidate scoring | +| ~2.9 | `hash_tree` / `fitch_indirect_length_cached` (scalar) / `validate_topology` | | + +**Conclusion:** per-candidate scoring is at the AVX2/compiler limit. Standard-Fitch +is **bookkeeping- + strategy-bound**: per-clip O(n) work (postorder rebuild + +incremental passes + edge/from_above/vroot construction ≈ 18%) and ratchet (63%) +are the levers — exactly what TNT minimises (Goloboff 1996). This corroborates +`.positai/plans/2026-03-21-tnt-outperformance-analysis.md` (strategy > code). + +**Two build gotchas for VTune on this MinGW/Windows toolchain** (cost an hour): +1. The default R Windows build **strips** the DLL (`DLLFLAGS=-s` in Makeconf) → + VTune shows `func@0x…`/`[Unknown]`. Override with + `MAKEFLAGS="DLLFLAGS=-static-libgcc"` (drops `-s`); the personal `~/.R/Makevars` + supplies `-g` via `CXXFLAGS` (and zeroes `PKG_CXXFLAGS`, so a package + `src/Makevars.win PKG_CXXFLAGS` is ignored). Verify: `objdump -h DLL | grep debug_info`. +2. Even with symbols, VTune's CSV reporter still prints `func@0x…` (MinGW DWARF + unparsed). Resolve names with `nm -C DLL`; the image base (0x2cc1a0000) is stable + across rebuilds, so VTune addresses map 1:1 to `nm` addresses. `addr2line` gives + the source FILE but not always the line (DWARF5 line tables). + +Full round notes + ranked candidates: `dev/profiling/log.md` (Round 3), +`dev/profiling/findings.md` (T-S3a/b/c/d), `dev/profiling/baselines.md`. + +**Wins shipped this round (per-clip allocation churn).** Tier 1 (T-S3a, committed): +hoist per-clip scratch (`dirty`, DFS stacks, preorder) to reusable buffers ≈ **4%**. +Tier 2 (T-S3d, uncommitted): replace the per-clip `unordered_set` +rerooting-dedup with a reusable open-addressed table (generation-stamped O(1) reset) +≈ **3%**. Both behaviour-neutral (score 627 unchanged; scores identical on 6 datasets +× {NA, standard} via `dev/profiling/bench_equiv.R`). On the fast standard-Fitch path, +per-clip *allocation* is a bigger proportional share than on the NA path (T-260 saw +the dedup destructor at ~0.4%, but the full alloc footprint — bucket array + a node +malloc per insert + teardown, every internal-node clip — is several %). + +**emutls gotcha (cost two rebuilds — record for next time).** Tier 2 was *neutral* +when first written as `static thread_local`: MinGW resolves a `thread_local` living +in a **dynamically-loaded DLL** via **emutls** = a function call (`__emutls_get_address`) +on *every access*. For a buffer touched once per clip (Tier 1) that's invisible, but +the dedup table's `insert()` runs *per reroot candidate*, so the per-access TLS call +cancelled the allocation saving. Fix: a **plain local declared once before the loop** +— in a per-thread-entered function (each search thread calls `tbr_search` with its own +`TreeState`) a stack local is already per-thread-safe and has *zero* TLS cost. Rule: +prefer hoisted plain locals over `static thread_local` on per-inner-iteration hot paths; +reserve `thread_local` for buffers inside helpers you can't hoist (Tier 1's case). + +**Verifying from a temp lib (two traps).** (1) `test_dir()` switches CWD to +`tests/testthat/`, so a **relative** `lib.loc` breaks package-data lazy-load +(`cannot open .../data/Rdata.rdb`) even though `library()` loaded fine — pass an +**absolute** `lib.loc`. (2) There is a **flaky `nThreads=2` crash** (exit 127, no R +error) in the parallel test files under `Rscript`; it reproduces on the *unmodified* +build too (not change-specific) and passes on re-run, so the full `ts-` suite is not a +reliable gate from a temp lib — verify behaviour-neutrality with a single-threaded +score-equivalence harness instead (`bench_equiv.R`). [Flaky parallel crash: open +question — real concurrency bug vs Rscript/sandbox thread-resource artifact.] + +## What to Profile + +Status key: ✅ resolved, ⚠ partially explored, ❌ not yet investigated + +1. ✅ **Drift + ratchet inner loops** (50–60% of C++ time combined). Both use + TBR internally. Per-candidate indirect evaluation at memory-throughput + limit (~23 ns at 75 tips per T-075). Cycle counts tuned (d2_r5). + **Drift threshold sensitivity (2026-03-18 Agent E):** AFD={1,3,5,8} × + RFD={0.05,0.1,0.2} on Zhu2013 (75 tips, 15 runs each): no significant + score difference between any config (Wilcoxon p=0.60–1.00). Permissive + thresholds (AFD=8, RFD=0.2) waste time; tight vs default indistinguishable. + On Dikow2009 (88 tips), d2 drift provides no benefit over ratchet alone + (p=0.54); d6 gives 2-step improvement (p=0.006) at 2× time cost. + **Conclusion:** Current defaults (AFD=3, RFD=0.1) are fine. Cycle count + matters more than threshold values. No optimization task raised. + +2. ✅ **Sectorial search effectiveness** (12% of time). XSS effectiveness is + dataset-dependent (0–27 steps). RSS is marginal (0–3 steps). No clear + predictor from simple dataset statistics. Could make XSS adaptive (skip + after N unproductive reps) but time savings would be <10%. + +3. ✅ **Wagner tree construction**: <0.1% of search time. Not a bottleneck. + +4. ✅ **R overhead**: <0.5% of wall time. Not a bottleneck. + +5. ✅ **Parallel scaling**: 78–82% efficiency at 2 threads. Implementation is + sound (dynamic work-stealing, low-contention pool). Main loss is stochastic + load imbalance. No obvious improvement without algorithmic changes. + +6. ✅ **IW scoring overhead** (2026-03-18 Agent E). Compared EW vs IW (k=10, + k=3) on three datasets (5 runs each, d2_r5, 5 reps, serial): + - Vinther2008 (23 tips): IW 64% *faster* (landscape converges quicker) + - Agnarsson2004 (62 tips): IW 26–39% slower + - Zhu2013 (75 tips): IW 40–57% slower + IW overhead scales with dataset size due to per-character weighted delta + computation in indirect scoring. No optimization opportunity — the delta + lookup is already O(n_blocks) per candidate, same as EW Fitch. + +7. ✅ **Fuse effectiveness** (2026-03-18 Agent E). Compared fuseInterval=0 vs + 3 on three datasets (8 runs each, 10 reps): + - Agnarsson2004: identical scores/time (pool deduplicates to 1 tree) + - Zhu2013: identical scores/time + - Dikow2009: negligible overhead (13.65s vs 13.78s with poolSuboptimal=5) + Fuse is cheap when pool is small, free when pool=1. Current default + (fuseInterval=3) is appropriate. No optimization task raised. + +## Comparing Search Strategies: Time-Adjusted Expected Best + +When comparing strategies that differ in per-replicate cost (e.g. NNI→TBR +vs TBR alone), the **median per-replicate score is the wrong metric**. +Multi-start search keeps the best tree across all replicates, so what +matters is the expected minimum from k independent draws, where +k = budget / time_per_replicate. + +A strategy with high variance but occasional excellent scores can dominate +a consistent-but-mediocre one — if it's fast enough to get more draws. + +**Bootstrap estimation:** +```r +expected_best <- function(scores, k, n_boot = 5000) { + mean(replicate(n_boot, min(sample(scores, k, replace = TRUE)))) +} + +# k = budget / median_time_per_rep for each strategy +k <- floor(budget / median_time) +exp_best <- expected_best(observed_scores, k) +``` + +Compare `exp_best` across strategies at fixed budget (e.g. 20s, 60s, 120s). +This naturally trades off per-replicate quality against replicate throughput. + +**When median IS acceptable:** comparing parameter changes on a fixed pipeline +(same time-per-rep), e.g. ratchet perturbation probability. All runs take +roughly the same time, so k is constant and the median is a reasonable proxy. + +See AGENTS.md "NNI in the driven pipeline" for the reference application of +this metric (NNI→TBR vs TBR at 88 and 180 tips). + +## Reporting Format + +For each finding, add to `to-do.md`: + +``` +| T-NNN | P2 | OPEN | — | [Profile] Brief description | X% of time. Potential Y% improvement via Z approach. | +``` + +Include the measurement methodology and baseline numbers so the implementer +can verify the improvement. + +8. ✅ **HSJ scoring overhead** (2026-03-19 Agent A). HSJ is ~0.6× EW wall time + (faster) on synthetic hierarchical data. Fitch screening gates full HSJ rescore + effectively. No optimization needed. + +9. ✅ **XFORM (Sankoff) scoring overhead** (2026-03-19 Agent A). XFORM is ~1.6–2.2× + EW wall time. Overhead concentrated in Ratchet (+69%) and Drift (+64%). This + is expected Sankoff vs Fitch arithmetic cost — no obvious optimization target. + +10. ✅ **Hierarchical resampling parallelism** (2026-03-19 Agent A). Serial R loop + means `nThreads` only applies within each replicate. Brazeau 2T = 2.5× speedup; + HSJ/XFORM hierarchical 2T = 1.1× only. Known limitation, future optimization + (C++-level inter-replicate parallelism for hierarchical resampling). + +11. ✅ **MaddisonSlatkin internal bottlenecks** (2026-03-19 Agent A, T-149). + VTune hotspot collection (software sampling, `-g -fno-omit-frame-pointer` + symbols build) on 57 calls at boundary cases: k=3/n=20–25, k=4/n=14–18, + k=5/n=9–12. Total ~23 s CPU time; 63% in `TreeSearch.dll`. + + **CPU time breakdown within TreeSearch.dll (14.1 s):** + + | Category | CPU (s) | % DLL | + |----------|---------|-------| + | `logB_cache::find` (k=3,4,5) | 2.72 | 19% | + | `SolverT::LogB` compute | 1.88 | 13% | + | `logPVec_cache::find` (k=3,4,5) | 1.91 | 14% | + | `SolverT::LogPVec` compute | 1.24 | 9% | + | `LogPVecKey::operator==` | 1.11 | 8% | + | `StateKeyT::operator==` | 1.01 | 7% | + | `expl`/`_expl_internal` (LogB LSE) | 0.91 | 6% | + | `logRD_cache::find` | 0.74 | 5% | + | `std::isfinite` (all sites) | 0.70 | 5% | + | `vector::~vector` (eviction) | 0.60 | 4% | + | `logconv` actual convolution | 0.20 | 1% | + + **Key findings:** + - `logconv` is only **1%** of DLL time — the Phase 2 vectorization worked + perfectly; the algorithm itself is no longer the bottleneck. + - **Hash map infrastructure dominates** (53% of DLL time): `unordered_map::find` + + key equality checks across the three caches (logB, logPVec, logRD). + Switching to a flat/open-addressing map would help but adds complexity. + - **`expl()` in `LSEAccumulator`** (6%) uses long-double arithmetic. Switching + to `double`/`exp()` would save ~0.7s at negligible precision cost. → **T-151** + - **`std::isfinite`** (5%) routes through `_fpclassify` on MinGW/Windows. + Replacing with `x != NEG_INF` saves the function-call overhead. → **T-152** + - `memcmp` in ucrtbase.dll (1.6 s / 7% of total) is the `StateKeyT::operator==` + fall-through when `cached_hash` and `cached_sum` both match — unavoidable + with the current key design. + + **Estimated combined T-151 + T-152 saving: ~1.4 s (6%) per cold-cache run.** + +## Build and Test (Reminder) + +Always use isolated library: +```bash +R CMD build --no-build-vignettes --no-manual . && R CMD INSTALL --library=.agent-X TreeSearch_*.tar.gz && rm -f TreeSearch_*.tar.gz +Rscript -e "library(TreeSearch, lib.loc='.agent-X'); testthat::test_dir('tests/testthat', filter='ts-')" +``` + +Max 2 CPU cores. Use `nThreads = 2L` at most in benchmarks. diff --git a/dev/expertise/red-team.md b/dev/expertise/red-team.md new file mode 100644 index 000000000..8184ec4eb --- /dev/null +++ b/dev/expertise/red-team.md @@ -0,0 +1,97 @@ +# Red-Team Expertise — TreeSearch (durable wisdom) + +> **This file is the curated "lessons learned" — not the operational machinery.** +> The live rotation now lives under `dev/red-team/` (per the `/red-team` skill): +> - `dev/red-team/focus-areas.md` — the 10-area rotation table + per-area `start_tier` +> - `dev/red-team/log.md` — append-only round log + `last_focus:` pointer +> - `dev/red-team/findings.md` — verified OPEN findings +> +> Run a round with `/red-team`. Keep this file for the *patterns* that recur across +> rounds; keep the *records* in `dev/red-team/`. (Migrated 2026-06-16 from +> `.positai/expertise/red-team.md`, which is now a stub.) + +## Purpose + +Red-teaming reviews code for (i) correctness bugs and (ii) performance issues. Fix +trivial issues inline (and note them in the round log); file non-trivial, *verified* +findings in `dev/red-team/findings.md` (and the dispatcher queue `to-do.md`). + +The goal is **issues fixed per token spent**, not issues found in the abstract. Depth +over breadth: one focused review that finds a real bug beats a broad "all green" sweep. + +## Bug patterns (reference — distilled from past rounds) + +| Pattern | Where to check | Seen in | +|---------|---------------|---------| +| Missing `GetRNGstate()`/`PutRNGstate()` around `unif_rand()` | Any `.cpp` using randomness | — | +| **R RNG API (`unif_rand`/`Get/PutRNGstate`) reached from a worker thread** | Serial helpers (`driven_search`, `resample_search`) called *from* a parallel worker — the worker path may re-enter a serial entry point that brackets R's RNG | T-309 (parallel `Resample()` → `ts_driven.cpp:690` via `ts_resample.cpp`) | +| `std::random_device{}()` ignoring `set.seed()` | Seeding of `std::mt19937`; use `ts::make_rng()` | — | +| `1u << s` undefined behaviour at `s >= 32` | `build_dataset`/`simplify_patterns` token bitmasks (`uint32_t`) when `n_states == 32` | DAT-001 (UBSAN); B's 2026-03-19 `MAX_STATES` guard | +| Division by an unguarded count → `NaN`/`Inf` that defeats a later guard | XPIWE `f = 1 + r*missing/obs` when `obs == 0`; NaN passes `> 0` and `<= floor` comparisons silently | DAT-002; T-311 (LS RSS=0 garbage) | +| `NaN`/`Inf` clamped to a benign value, hiding failure | `return rss > 0 ? rss : 0` turns NaN into a "valid" 0; validate finiteness instead | T-311 (LeastSquares) | +| `build_reduced_dataset` doesn't copy a needed field | Sector / prune-reinsert reduced datasets: `flat_blocks`, `all_weight_one`, `inapp_state`, HSJ (`hierarchy_blocks`/`tip_labels`) & Sankoff fields. Two copies of this function — keep them in sync | T-275, T-303; B 2026-03-19; 44d929a8 | +| Frozen-API return type drift (`as.integer` vs `as.logical`) | `R/SearchControl.R` boolean fields documented as logical | T-310 (`pruneReinsertNni`) | +| GCC-only builtins (`__builtin_popcountll`, etc.) | All `.cpp`/`.h` (need MSVC fallback) | — | +| `.inc`/`.h` changes not triggering recompilation | `ts_fitch_na.inc`, `ts_fitch_na_incr.inc`; `touch src/ts_fitch.cpp` after edits; no `Makevars` header-dep tracking | — | +| Missing `TreeSearch:::` prefix / no `set.seed()` before `sample()` | `tests/testthat/test-ts-*.R` | 2026-05-26 area 8 | +| **"Missing `skip_on_cran()`" that's actually a deliberate Tier-1 file** | Check `tests/testing-strategy.md` tier lists FIRST. Tier-1 `test-ts-*` (`constraint-small`, `memory-layout`, `pool`, `simd`, `splits`, `rep-warning`, `start-tree`) are *intentionally* guardless (run on CRAN); only Tier-2 (any `test-ts-*` NOT in the Tier-1/Tier-3 lists) needs `skip_on_cran()`; Tier-3 stress/bench needs `skip_extended()`. Don't report a Tier-1 file for "missing skip" | 2026-06-16 area 8 (T-322 round refuted 6 false "missing skip" hits, confirmed 2 real: `impose-constraint`, `strategy`) | +| Bare dataset symbol used without `data()` — usually NOT a bug | `inapplicable.phyData` etc. are lazy-loaded (`DESCRIPTION: LazyData: true` + `data/*.rda`), so a bare reference resolves the moment TreeSearch is attached. Only a real bug if the dataset genuinely isn't lazy-loaded | 2026-06-16 area 8 (TS-8-02 refuted) | +| Cross-check test omits a production-required arg → tautology | Both sides then compute the same *non-production* formula, so the test always passes and can't catch a regression. `test-ts-wagner.R` NA+IW omitted `min_steps` (prod always passes `as.integer(MinimumLength(ds, compress=TRUE))`). Verify the test feeds the SAME args production does | T-322 (2026-06-16 area 8) | +| **Cross-engine "discrepancy" that's really mismatched defaults — NOT a bug** | Before filing "engine A ≠ engine B", confirm both compute the *same variant with the same defaults*. `TreeLength()` defaults `extended_iw = TRUE` → **XPIWE** (Goloboff 2014 Ext-3: `f=1+r·missing/obs`, `eff_k=k/f`, `phi=(1+eff_k)/(1+k)`, `Σ fit·w·phi`), but the kernel `ts_fitch_score(min_steps, concavity)` computes **plain IW** (no XPIWE args). They differ by design on NA datasets (irrational per-char values are the `phi`/`eff_k` scaling, not a min-steps bug). Production is consistent: `MaximizeParsimony` sets `useXpiwe <- isTRUE(extended_iw) && is.finite(concavity) && !useProfile` (`MaximizeParsimony.R:813`) so it optimises the same XPIWE `TreeLength` reports. **Verified equalities (Vinther2008):** `TreeLength(extended_iw=FALSE)` == kernel plain IW exactly; `MaximizeParsimony(conc=k)$score` == `TreeLength(extended_iw=TRUE)` rescore exactly. To compare engines, match `extended_iw` on both sides. | area-9 false signal, refuted 2026-06-16 | +| Arg-count mismatch in `TreeSearch-init.c`; `R_PosInf` in Rcpp defaults | After adding/removing Rcpp params; `R/RcppExports.R` after `compileAttributes()` | — | +| Stale state arrays after a rejected move | Restore only covers the clip-to-root path; regraft-to-root nodes keep regrafted values — conservative (screening only) but real | T-235 (SPR); A 2026-03-19 | +| Stale metadata after **equal-score / tabu** rejection | Constraint `map_constraint_nodes`/`compute_dfs_timestamps` re-synced on the `!accepted` path but not the tabu path | T-316 (possible P1) | +| Out-of-range index from R reaching the kernel as `NA_INTEGER` (INT_MIN) | `AdditionTree(sequence=)` → `addition_order[i]-1` underflows → OOB write; validate in R | WGN-01 (P1) | +| **R-layer input validation alone leaves the `:::` kernel entry point unguarded** | After fixing an OOB by validating in the *public* R wrapper, the underlying `// [[Rcpp::export]]` C++ fn is still reachable via `TreeSearch:::` (tests, dev, internal callers) with the bad input. WGN-01 guarded public `AdditionTree`; `:::ts_wagner_tree(addition_order=c(1L))` still **segfaults** (no length/range check at the Rcpp boundary). Mirror the R guard in C++ too (`Rcpp::stop` on non-permutation / wrong length / out-of-range). | T-323 (2026-06-16 area 9) | +| Asymmetric validation/edge-handling between sibling code paths | One branch validates or handles an edge case; its sibling doesn't. `AdditionTree` numeric `sequence=` rejected duplicates but the *character* path didn't → silent taxon-set corruption (WGN-DUP). `PolEscapa` fixed the empty-`qmApp` case (T-302) but left the symmetric empty-`qm` case → phyDat corruption (POL-QM-EMPTY). When you fix or find a guard on one path, grep for its sibling and check parity. | WGN-DUP, POL-QM-EMPTY (2026-06-16 area 9) | +| No revert on worsening move; mask not cleaned up | Sectorial reinsertion, fuse exchange; ratchet perturbation restore paths | B 2026-03-19 | +| `pattern_freq *= 2` per char → exponential blowup when patterns shared | Ratchet `perturb_upweight`/`perturb_mixed`; use `+= 1` | B 2026-03-19 | +| `-Inf − (-Inf) = NaN` in log-space convolution | `.LogCumSumExp` (`R/pp_info_extra_step.r`) when both terms `-Inf` | B 2026-03-20 | + +### Shiny (area 7) — async lifecycle patterns (immature seam, 2026-06-16) +- **Stamp the hash of the dataset that was *prepared*, not the current one**, when an + async task completes — otherwise a mid-task data swap scores against the wrong data + (T-309). +- **Re-entrancy:** `shinyjs::disable()` is an async browser round-trip; guard handlers + with an explicit `searchInProgress` flag, not the disabled button (T-310). +- **`onStop` must write the worker's cancel signal** and `unlink` *all* temp-file + prefixes the module actually creates (`ts_cancel_*`, `ts_progress_*`, `ts_profile_*`), + or workers orphan and `tempdir()` grows (T-311, T-312; see `shiny-app.md` "Issue 6"). +- **Topology dedup must strip branch lengths** before `write.tree()` (it serialises BLs), + or BL-bearing user trees won't dedupe against parsimony trees (T-313). + +## Performance patterns (reference) + +| Pattern | Where to check | +|---------|---------------| +| Unbounded indirect scoring (missing `_bounded` variant) | Search inner loops | +| Full `score_tree()` where incremental / dirty-set would suffice | After clip/regraft; NNI accept calls full `fitch_uppass` (O(n)) | +| `build_postorder()` called unnecessarily | After unclip or snapshot restore | +| Full-tree `TreeState` copy where save/restore (`copy_topology`) suffices | Fuse, sectorial, NNI-perturb | +| `SankoffData` rebuilt every `score_tree()` | XFORM scoring hot path (could cache in DataSet) | +| Long mutex hold across `tree_fuse()` | `ThreadSafePool::fuse_round` (correct, but serialises) | + +## Known fragile areas (reference) + +1. `ts_rcpp.cpp` + `TreeSearch-init.c`: append-only, check arg counts every time. +2. `RcppExports.R/.cpp`: concavity `Inf` → `-1.0`/`HUGE_VAL` sentinel after regen. +3. `.inc`/`.h` files: `touch src/ts_fitch.cpp` after changes (no header-dep tracking). +4. Parallel: `ts_rng.h` `thread_local` must be set before any search call; **no R API + from workers** — beware serial entry points re-entered from a worker. +5. `init_from_edge`: first child → left convention; 1-based R edge → 0-based internal. +6. **Two `build_reduced_dataset` functions** (`ts_sector.cpp`, `ts_prune_reinsert.cpp`) — + asymmetric field-copy footgun; keep in sync, guard HSJ/XFORM where fields are absent. +7. `FlatBlock.active_mask` duplicates `CharBlock.active_mask` — any writer to one must + sync the other (ratchet does; future writers must too). +8. Incremental/dirty-set Fitch rescore (`ts_fitch_na_dirty.h`, `ts_tbr.cpp:1138-1180`): + the crown-jewel correctness risk (T-300 was a systematic delta=−3). It has **no + enduring regression test** beyond T-304 — treat with suspicion after any edit. + +## Reporting format (for `dev/red-team/findings.md`) + +``` +| T-NNN | P1/P2/P3 | | **Title.** | `path:line` | Detail + fix + verifier verdict. | +``` + +Severity: **P1** wrong user-visible result / crash / desk-reject · **P2** wrong on edge +input / frozen-shape inconsistency / search-quality · **P3** robustness / polish. diff --git a/dev/expertise/shiny-app.md b/dev/expertise/shiny-app.md new file mode 100644 index 000000000..b91be89d4 --- /dev/null +++ b/dev/expertise/shiny-app.md @@ -0,0 +1,424 @@ +# Shiny App Expertise — TreeSearch + +## Purpose + +This document provides best practices and troubleshooting guidance for developing and maintaining the TreeSearch Shiny interactive application (`inst/Parsimony/app.R`). The app provides a user-friendly interface for phylogenetic tree search with real-time feedback, logging, and publication-ready visualization. + +## App Architecture + +### High-level Structure + +``` +app.R (3683 lines) +├── UI (lines 264-471) +│ ├── Left sidebar (3-column) +│ │ ├── Data loading (file, package datasets) +│ │ ├── Search controls (configure, start, save log) +│ │ ├── Tree loading and sampling +│ │ └── Display configuration (format, outgroup, etc.) +│ └── Main panel (9-column) +│ ├── Plot area with dynamic sizing +│ ├── Plot controls (size, export, concordance, clustering) +│ └── Tree/space visualization panels (conditional display) +│ +├── Server (lines 506-3683) +│ ├── Logging infrastructure (Write, LogCode, LogComment, etc.) +│ ├── Data loading (UpdateData, Excel/TNT/PhyDat parsers) +│ ├── Tree management (UpdateAllTrees, UpdateActiveTrees, filtering) +│ ├── Search execution (StartSearch, MaximizeParsimony dispatch) +│ ├── Display rendering (consensus, clustering, tree space visualization) +│ ├── User interactions (observeEvent handlers, reactive computations) +│ └── Export functionality (Newick, Nexus, PDF, PNG, R script logging) +│ +└── Supporting Elements + ├── Palettes (56+ color schemes for taxa) + ├── References (formatted bibliography) + ├── Helper functions (Enquote, EnC, Icon, ErrorPlot) + └── Notification system (Notification function wrapping showNotification) +``` + +### Key Reactive Values (lines 508-517) + +- `r$dataFiles`, `r$excelFiles`, `r$treeFiles` — file counters for temp caching +- `r$dataset` — loaded phyDat object +- `r$allTrees`, `r$trees` — all vs. displayed tree subset +- `r$outgroup` — selected outgroup taxa for rooting +- `r$searchWithout` — taxa to exclude from search +- `r$sortTrees` — whether to reorder edges by clade size (for display) +- `r$plotLog`, `r$cmdLogFile` — logging outputs for export + +### Data Flow + +1. **Data load** → `UpdateData()` (line 797) + - Detects file type (Excel, TNT, PhyDat) + - Caches to temp directory + - Logs code for reproducibility + - Attempts to load trees from same file + +2. **Search** → `StartSearch()` (line 1566) + - Builds or uses existing starting tree + - Dispatches to `MaximizeParsimony()` (C++ engine) + - Logs search code with all parameters + - Updates tree display + +3. **Display** → Reactive plot rendering (lines 1731+) + - User selects plot format (individual trees, consensus, clustering, tree space) + - Conditional UI elements show/hide based on selection + - Plots render via R base graphics (not ggplot2) + +## Critical Functions by Purpose + +### Data Loading + +| Function | Lines | Role | +|----------|-------|------| +| `UpdateData()` | 797 | Main dispatcher; handles file/package sources | +| Excel parsing | 830-903 | readxl-based with skip/column controls | +| TNT/PhyDat parsing | 908-949 | Tries multiple formats; caches successfully read files | +| `CacheInput()` | 739 | Copies file to temp for reproducibility | +| Character extraction | 961 | Reads character names/notes for display | + +### Tree Management + +| Function | Lines | Role | +|----------|-------|------| +| `UpdateAllTrees()` | 1145 | Replace all trees; renumber tips consistently | +| `UpdateActiveTrees()` | 1086 | Thin to user-selected range and count | +| `UpdateTreeRange()` | 1067 | Sync range slider with data structures | +| `UpdateNTree()` | 1026 | Update tree count; validate against range | +| `FetchNTree()`, `FetchTreeRange()` | 1012, 1053 | Debounced reactive accessors | + +### Search & Scoring + +| Function | Lines | Role | +|----------|-------|------| +| `StartSearch()` | 1566 | Build starting tree, dispatch MaximizeParsimony, log code | +| `scores()` | 1344 | Cached TreeLength() call on active trees | +| `DisplayTreeScores()` | 1369 | Update results text; show score range and weighting | +| `concavity()` | 1550 | Parse IW exponent or profile mode from input | +| `weighting()` | 1332 | Map UI "on"/"off"/"prof" to concavity values | + +### Rogue Taxon Detection + +| Function | Lines | Role | +|----------|-------|------| +| `Rogues()` | 1775 | Cached Rogue::QuickRogue() call | +| `nNonRogues()` | 1834 | Rogue count at selected p-value | +| `KeptTips()`, `DroppedTips()` | 1949, 1973 | Filter tree tips by rogue analysis | +| `UpdateKeepNTipsRange()` | 1402 | Validate user input; sync with rogue count | + +### Visualization + +| Function | Lines | Role | +|----------|-------|------| +| `PlottedTree()` | 1731 | Consensus or individual tree, rooted/sorted | +| `concordance()` | 1862 | Calculate split support (multiple measures) | +| `LabelConcordance()` | 1876 | Annotate tree with support values | +| `ConsensusPlot()` | 1982 | Render consensus with rogue drop sequence | +| `TipCols()` | 1840 | Color tips by stability (Rogue::ColByStability) | + +### Logging & Export + +| Function | Lines | Role | +|----------|-------|------| +| `BeginLog()` | 590 | Initialize search log with system info | +| `LogCode()`, `LogComment()` | 692, 704 | Append to R script log | +| `Write()` | 524 | Append to temp log file with indentation | +| `StashTrees()` | 745 | Save trees to Nexus in temp for export | + +## Best Practices + +### 1. Reactive Programming Patterns + +**Use `reactive()` for derived values, `bindCache()` for expensive calls:** +```r +# Simple derived value +weighting <- reactive(switch(input$implied.weights, "on" = Inf, ...)) + +# Cached function (re-run only if dependencies change) +scores <- bindCache(reactive({ TreeLength(r$trees, ...) }), + r$treeHash, r$dataHash, concavity()) +``` + +**Avoid:** +- Direct `input$*` reads in observers (use reactive() wrapper) +- Computing the same expensive value multiple times +- Calling `reactive()` inside `observe()`/`observeEvent()` + +### 2. File Handling + +**Always cache input files to temp directory for reproducibility:** +```r +CacheInput("data", fileName) # Copies to tempdir() + DataFileName(counter) +LogCode(paste0("dataFile <- \"", LastFile("data"), "\"")) +``` + +**Supported formats (auto-detect by extension):** +- `.xlsx` / `.xls` — Excel (readxl + configurable skip/columns) +- `.nex` — Nexus (read.nexus) +- `.tre` / `.txt` — TNT or Newick (ReadTntTree or read.tree/read.nexus) +- Any phyDat-compatible text format (ReadAsPhyDat) + +### 3. Logging Code Reproducibility + +**Every significant user action must log equivalent R code:** +```r +LogCode(c( + "newTrees <- MaximizeParsimony(", + " dataset,", + " concavity = 10,", + " maxReplicates = 100", + ")" +)) +``` + +**Use `EnC()` to quote parameters safely:** +```r +# EnC(c("a", "b")) → "c(\"a\", \"b\")" +# EnC("profile") → "\"profile\"" +# EnC(10) → "10" +``` + +**Indentation via `LogIndent()` for nested scopes:** +```r +LogIndent(2) # Indent +2 spaces +LogCode("for (tree in trees) {") +LogIndent(2) +LogCode(" tree <- Consensus(tree, p = 0.5)") +LogIndent(-2) +LogCode("}") +LogIndent(-2) +``` + +### 4. Observing User Input + +**Use debounce for high-frequency inputs (sliders, text boxes):** +```r +PlottedChar <- debounce(reactive({ as.integer(input$plottedChar) }), aJiffy) +``` + +**Use `ignoreInit = TRUE` to skip initialization:** +```r +observeEvent(input$searchConfig, { ... }, ignoreInit = TRUE) +``` + +**Cache tree hashes to detect changes (avoid spurious recalculations):** +```r +observeEvent(r$dataset, { + r$dataHash <- rlang::hash(r$dataset) +}) +r$trees <- thinnedTrees +r$treeHash <- rlang::hash(r$trees) +``` + +### 5. Conditional UI & Show/Hide Elements + +**Use bslib-style id-based show/hide (not class-based):** +```r +# Define in UI with hidden(...) wrapper +hidden(tags$div(id = "displayConfig", ...)) + +# Toggle in server +show("displayConfig", anim = TRUE) # With fade-in animation +hide("displayConfig") # Fade-out +showElement("displayConfig") # JavaScript show() without animation +hideElement("displayConfig") +``` + +**Manage multiple related configs via `ShowConfigs()`:** +```r +observeEvent(input$plotFormat, { + ShowConfigs(switch(input$plotFormat, + "ind" = c("whichTree", "charChooser", "treePlotConfig"), + "cons" = c("consConfig", "branchLegend", "savePlottedTrees"), + "clus" = c("clusConfig", "clusLegend", "savePlottedTrees"), + "" # Default: hide all + )) +}) +``` + +### 6. Modal Dialogs for Configuration + +**Example: Search configuration modal (line 1220):** +```r +observeEvent(input$searchConfig, { + # Pre-populate with current values + updateSelectInput(session, "concavity", selected = input$concavity) + + showModal(modalDialog( + fluidPage(column(6, ...), column(6, ...)), + title = "Tree search settings", + footer = tagList( + modalButton("Close", icon = Icon("rectangle-xmark")), + actionButton("modalGo", "Start search", icon = Icon("magnifying-glass")) + ), + easyClose = TRUE + )) +}) + +observeEvent(input$modalGo, { + removeModal() + StartSearch() +}) +``` + +## Common Issues & Troubleshooting + +### Issue 1: File Upload Not Working + +**Symptom:** User selects file, nothing happens. + +**Checks:** +- File size < `shiny.maxRequestSize` (default 5MB; app sets 1GB at line 4) +- File extension recognized (Excel, TNT, Nexus, text) +- `readxl` installed for Excel files (auto-install at line 831) +- Check browser console for error messages +- If TNT format: tip labels must be inferrable (will try 4 caterpillar orderings) + +### Issue 2: Search Hangs or No Results + +**Symptom:** Click "Search", progress bar shows, but never completes. + +**Checks:** +- Dataset is valid phyDat (not NULL, has tips) +- Tree space not empty or trivial (≥4 tips recommended) +- Replicates/timeout reasonable (maxReplicates ≥ 1, timeout > search time) +- Check `maxSeconds` timeout — if 0, no timeout; if very small, search aborts early +- Parallel mode (nThreads > 1) is non-deterministic; may find different trees + +**Debugging:** +```r +# In console: +ds <- ReadAsPhyDat("data.nex") +attr(ds, "nr") # Check character count +length(ds) # Check taxon count +tree <- AdditionTree(ds) # Should complete quickly +``` + +### Issue 3: Trees Don't Display / Blank Plot + +**Symptom:** Plot area is empty; no error message. + +**Checks:** +- Trees loaded? (r$trees length > 0) +- Dataset loaded? (needed for consensus/character display) +- Display format selected? (default "cons" should show something) +- Outgroup valid? (must be in tree tips) +- Rogue-dropping valid? (can't drop all tips) + +**Debugging:** +```r +# In console: +length(app_env$r$trees) # Should be > 0 +app_env$AnyTrees() # Should be TRUE +app_env$Consensus(app_env$r$trees, p=1) # Should render +``` + +### Issue 4: Logging Code Mismatch + +**Symptom:** Exported R script doesn't reproduce results. + +**Checks:** +- File paths in log correct? (should use temp files like "dataFile-00.txt") +- Parameters logged correctly? (check `Enquote()` results) +- Library calls present? (BeginLog should include all imports) +- Character encoding OK? (use system-appropriate paths) + +**Prevention:** +- Always use `LogCode()` immediately after performing an action +- Test exported script manually in a fresh R session +- Check `tempdir()` for actual cached files + +### Issue 5: Rogue Analysis Crashes or Misses Taxa + +**Symptom:** `Rogues()` returns NULL, or taxa don't appear in drop sequence. + +**Checks:** +- Dataset properly loaded (not NULL) +- Trees properly loaded (at least 1 tree, tip labels match) +- `p` parameter reasonable (0.5 to 1.0; default 1.0 = strict majority rule) +- Run `Rogue::QuickRogue()` manually to test: + ```r + rogues <- Rogue::QuickRogue(r$trees, neverDrop = input$neverDrop, + fullSeq = TRUE, p = consP()) + ``` + +### Issue 6: Memory Leak or Slowdown Over Time + +**Symptom:** App slows down after many searches; process memory grows. + +**Checks:** +- File caching in `tempdir()` consuming space? (e.g., 1000 searches → 1000s of cached files) +- Large tree objects retained? (clear old results before new search) +- Image caches building up? (plots rendered reactively, may leak if observer not cleaned up) + +**Prevention:** +- Periodically clear `tempdir()` (not auto-cleared by default) +- Use `on.exit()` to clean up temporary objects: + ```r + observeEvent(input$clearCache, { + do.call(file.remove, list(dir(tempdir(), full.names=TRUE))) + Notification("Cache cleared", type="message") + }) + ``` + +## Integration with C++ Engine + +### Key Changes from Legacy Morphy + +**Old (MorphyLib):** +```r +# Had to delegate constraints/profile to Morphy() +MaximizeParsimony(dataset, constraint = cons, concavity = "profile") +→ fell back to R-loop Morphy() search +``` + +**New (C++ engine):** +```r +# C++ engine handles everything natively +MaximizeParsimony(dataset, constraint = cons, concavity = "profile", + strategy = "auto", nThreads = 2, verbosity = 1) +``` + +### Strategy Presets (line 1231) + +- **"auto"** — Auto-selects based on dataset size (sprint ≤30, default 31-60, thorough 61+) +- **"sprint"** — 3 ratchet cycles, no drift; minimal sectorial +- **"default"** — 5 ratchet, 2 drift; XSS+RSS+CSS +- **"thorough"** — 20 ratchet, 12 drift; intensive sectorial; adaptive ratchet + +### Weighting Mode (line 1224) + +- **"on"** (Implied) — IW with concavity exponent (k = 10^exponent) +- **"off"** (Equal) — EW (all characters weight 1) +- **"prof"** (Profile) — Profile parsimony (info-theoretic weighting) + +## Testing Checklist + +Before deploying app updates: + +- [ ] Data loads: Excel (with skip/columns), TNT, Nexus, generic text +- [ ] Search runs: EW, IW, profile; small (4 tips), medium (25), large (75+) +- [ ] Logging: exported R script runs in fresh session, reproduces trees +- [ ] Display: individual, consensus, clustering, tree space all render +- [ ] Rogue analysis: correctly identifies and drops unstable taxa +- [ ] Outgroup: rooting works; must be in tree and dataset +- [ ] Export: PDF, PNG, Newick, Nexus files valid +- [ ] Performance: 50+ searches don't slow app significantly +- [ ] Parallel: nThreads=2 works; results reasonable (non-deterministic) +- [ ] Edge cases: 3-tip tree, single-character dataset, all inapplicable, empty pool + +## Performance Tips + +1. **Limit active tree display** — reduce `whichTree` max range if >100 trees +2. **Cache tree hashes** — avoid re-scoring unchanged trees +3. **Use bounded indirect** — ensure TBR/drift/SPR use `_bounded` variants +4. **Debounce slider inputs** — high-frequency slider updates (default aJiffy ≈ 42ms) +5. **Profile big plots** — use `system.time({ ... })` for consensus/space rendering + +## References + +- **app.R**: Main application file (3683 lines) +- **Related packages**: shiny, shinyjs, bslib, TreeTools, TreeSearch, Rogue, TreeDist +- **C++ search**: MaximizeParsimony() documented in `R/MaximizeParsimony.R` +- **Logging infrastructure**: BeginLog, LogCode, Write functions (lines 590-715) diff --git a/dev/expertise/tnt.md b/dev/expertise/tnt.md new file mode 100644 index 000000000..d8c13015a --- /dev/null +++ b/dev/expertise/tnt.md @@ -0,0 +1,87 @@ +# TNT (Tree analysis using New Technology) + +## Installation + +TNT is installed at `C:\Programs\Phylogeny\tnt\`. + +### Executables + +| Path | Version | Notes | +|------|---------|-------| +| `tnt/tnt.exe` | older | **Do not use.** | +| `tnt/TNT-bin/tnt.exe` | 1.6 | **Use this one.** Console/script mode. | +| `tnt/TNT-bin/wTNT.exe` | 1.6 | Windows GUI version. | + +Always use `C:\Programs\Phylogeny\tnt\TNT-bin\tnt.exe` (version 1.6). + +### Invocation + +**Never launch TNT without passing a script file.** TNT defaults to +interactive mode and will block waiting for keyboard input, hanging any +automated pipeline. + +**Correct pattern** — pass a `.run` script as a positional argument with +trailing semicolon: + +```bash +"C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe" "myscript.run;" +``` + +This launches TNT in PISH (batch) mode. It reads and executes the script, +then exits when it hits `quit;`. + +**Critical: script files must use `.run` extension.** TNT interprets `.tnt` +files as data files. If you pass a `.tnt` script, TNT will try to parse it +as data and fail with "Can't open .tnt". + +**Critical: script filenames must be purely alphabetic (no digits or +underscores).** TNT parses the filename as a command line — it splits on +digits and underscores, treating the first alphabetic token as a command. +`bench1.run` → command `bench`; `Vinther2008_EW.run` → command `vinther`. +Safe names: `tntbench.run`, `mytest.run`, `abc.run`. + +**Piping via stdin does NOT work reliably** — `echo "..." | tnt.exe` launches +interactive mode (shows ASCII banner) and may hang. + +**Encoding**: TNT stdout contains non-UTF8 progress bar characters. Use +`iconv(output, from = "", to = "UTF-8", sub = "")` to sanitize before +regex matching in R. + +### TNT script basics + +- Commands are terminated by `;` +- `mxram N;` — set memory (MB); must be first command +- `proc ;` — read data file (TNT `.tnt` or Nexus format) +- `xmult;` — heuristic search (new technology search) +- `xmult=hits N replic M;` — search with convergence/replicate limits +- `piwe = K;` — implied weights with concavity constant K +- `xpiwe = K;` — extended implied weights +- `rseed N;` — set random seed +- `timeout HH:MM:SS;` — set search time limit +- `best;` — report best score and tree count +- `length;` — print tree lengths +- `quit;` — exit TNT (essential for non-interactive use) + +### Data format + +TNT can read NEXUS (`.nex`) files and its own format (`.tnt`). +For NEXUS input, use `proc ;`. + +Export from R: `TreeTools::WriteTntCharacters(phyDat_obj, filepath)`. + +### Output parsing + +TNT stdout contains parseable lines: +- `"Best score: 78."` or `"Best score: 3.80000."` (IW) — best score +- `"N trees retained"` — number of trees found +- `"Best score hit N times."` — convergence hits +- `"Total rearrangements examined: N."` — total rearrangements + +### Score comparability with TreeSearch + +TNT standard Fitch treats inapplicable tokens as a regular character state +(column-based). TreeSearch uses Brazeau et al. (2019) three-pass algorithm. +For datasets with inapplicable characters, TNT EW scores will generally be +≤ TreeSearch EW scores. For IW, both use Goloboff's `e/(k+e)` formula. + +Example: Vinther2008 — TNT EW = 78, TreeSearch EW = 79. diff --git a/dev/ls_validate.R b/dev/ls_validate.R new file mode 100644 index 000000000..84726f5c7 --- /dev/null +++ b/dev/ls_validate.R @@ -0,0 +1,97 @@ +# Validation gate: C++ LS scorer vs phangorn::nnls.tree / designTree. +suppressMessages({ + library(ape) + pkgload::load_all(".", quiet = TRUE) +}) +stopifnot(requireNamespace("phangorn", quietly = TRUE)) + +fit_cpp <- function(tree, D, method) { # method 0=OLS, 1=NNLS + D <- D[tree$tip.label, tree$tip.label] + TreeSearch:::ts_ls_fit(tree$edge, D, NULL, as.integer(method)) +} + +# cophenetic of the rooted tree carrying fitted lengths (root edges are +# returned as (full, 0) so the rooted cophenetic already equals the unrooted). +cophen_cpp <- function(tree, fit) { + t2 <- tree + t2$edge.length <- fit$edge_length + cophenetic(t2) +} + +report <- function(label, ok, extra = "") { + cat(sprintf("%-46s %s %s\n", label, if (ok) "PASS" else "**FAIL**", extra)) + invisible(ok) +} + +set.seed(42) +all_ok <- TRUE + +for (n in c(5, 6, 8, 12)) { + # Raw rtree keeps standard ape numbering (root = n+1) which both + # build_topology_tree and phangorn accept; TreeTools::Preorder sets an + # `order` attribute that trips phangorn's reorder(). + tree <- rtree(n, br = function(k) runif(k, 0.1, 2)) + labs <- tree$tip.label + + ## ---- additive matrix from this very tree ---- + D <- cophenetic(tree)[labs, labs] + + for (meth in c(0, 1)) { + mname <- if (meth == 0) "OLS" else "NNLS" + fit <- fit_cpp(tree, D, meth) + coph <- cophen_cpp(tree, fit)[labs, labs] + add_ok <- max(abs(coph - D)) < 1e-6 && fit$rss < 1e-8 + all_ok <- report(sprintf("n=%2d additive %-4s recovers D, RSS~0", n, mname), + add_ok, sprintf("(rss=%.2e, maxerr=%.2e)", fit$rss, max(abs(coph - D)))) && all_ok + } + + ## ---- phangorn NNLS oracle on the SAME additive matrix ---- + ph <- phangorn::nnls.tree(D, tree, method = "unrooted") + rss_ph <- attr(ph, "RSS"); if (is.null(rss_ph)) rss_ph <- 0 + coph_ph <- cophenetic(ph)[labs, labs] + fit_nnls <- fit_cpp(tree, D, 1) + coph_me <- cophen_cpp(tree, fit_nnls)[labs, labs] + match_ok <- max(abs(coph_me - coph_ph)) < 1e-6 + all_ok <- report(sprintf("n=%2d additive NNLS fitted-dist == phangorn", n), + match_ok, sprintf("(maxdiff=%.2e)", max(abs(coph_me - coph_ph)))) && all_ok + + ## ---- non-additive matrix ---- + Dn <- as.matrix(as.dist(matrix(0, n, n))) + rv <- runif(n * (n - 1) / 2, 0.5, 3) + Dn[lower.tri(Dn)] <- rv; Dn <- Dn + t(Dn) + dimnames(Dn) <- list(labs, labs) + + # phangorn NNLS oracle + ph_n <- phangorn::nnls.tree(Dn, tree, method = "unrooted") + rss_ph_n <- attr(ph_n, "RSS") + if (is.null(rss_ph_n)) { # quadprog branch: recompute + rss_ph_n <- sum((Dn[labs, labs][lower.tri(Dn)] - + cophenetic(ph_n)[labs, labs][lower.tri(Dn)])^2) + } + fit_n <- fit_cpp(tree, Dn, 1) + coph_n <- cophen_cpp(tree, fit_n)[labs, labs] + rss_me_n <- sum((Dn[lower.tri(Dn)] - coph_n[lower.tri(coph_n)])^2) + nnls_ok <- abs(rss_me_n - rss_ph_n) < 1e-5 * max(1, rss_ph_n) && + max(abs(coph_n - cophenetic(ph_n)[labs, labs])) < 1e-5 + all_ok <- report(sprintf("n=%2d non-add NNLS RSS == phangorn", n), + nnls_ok, sprintf("(me=%.5f ph=%.5f)", rss_me_n, rss_ph_n)) && all_ok + + # OLS vs direct normal-equation solve on the unrooted design + ut <- unroot(tree) + X <- as.matrix(phangorn::designTree(ut)) + dm <- Dn[ut$tip.label, ut$tip.label] + y <- dm[lower.tri(dm)] + beta <- solve(crossprod(X), crossprod(X, y)) + rss_direct <- sum((y - X %*% beta)^2) + fit_o <- fit_cpp(tree, Dn, 0) + ols_ok <- abs(fit_o$rss - rss_direct) < 1e-5 * max(1, rss_direct) + all_ok <- report(sprintf("n=%2d non-add OLS RSS == direct solve", n), + ols_ok, sprintf("(me=%.5f direct=%.5f)", fit_o$rss, rss_direct)) && all_ok + + # OLS RSS must be <= NNLS RSS (unconstrained) + mono_ok <- fit_o$rss <= rss_me_n + 1e-8 + all_ok <- report(sprintf("n=%2d non-add OLS RSS <= NNLS RSS", n), mono_ok) && all_ok + cat("\n") +} + +cat(if (all_ok) "ALL VALIDATION CHECKS PASSED\n" else "SOME CHECKS FAILED\n") diff --git a/dev/plans/2026-03-22-1348-full-polytomy-search-for-treesearch-c-engine.md b/dev/plans/2026-03-22-1348-full-polytomy-search-for-treesearch-c-engine.md new file mode 100644 index 000000000..cba1a8009 --- /dev/null +++ b/dev/plans/2026-03-22-1348-full-polytomy-search-for-treesearch-c-engine.md @@ -0,0 +1,450 @@ +# Full Polytomy Search for TreeSearch C++ Engine + +**Status:** IN PROGRESS +**Target branch:** `feature/polytomy-search` (from `cpp-search`) +**Target worktree:** `../TS-Polytomy` + +## Motivation + +The TNT benchmark (2026-03-20, `TS-TNT-bench` worktree) shows TreeSearch +falls 1–14 steps behind TNT on datasets with ≥50 taxa. The TNT +outperformance analysis identifies **tree collapsing during search** as the +single biggest remaining algorithmic gap: + +> "Searches that collapse branches with minimum possible length produce more +> effective searches than criteria which collapse fewer branches, both in +> terms of time needed to complete searches, and ability to find shortest +> trees." — Goloboff (2023), Cladistics 39: 229–238 + +The existing `ts_collapsed.h/.cpp` (clip-skipping) was a partial step +toward this, but benchmarks showed 0% skip rate on standard morphological +data because near-optimal **binary** trees have few zero-length edges. The +key insight is that collapsing those edges into polytomies *changes the +search topology space*, making TBR/SPR more efficient by eliminating +distinctions that carry no phylogenetic signal. + +### Key literature + +| Reference | Key contribution | +|-----------|-----------------| +| Goloboff (1996), "Methods for faster parsimony analysis", Cladistics 12: 199–220 | §"Collapsing The Trees": partial reoptimization, shortest-path shortcut, asymmetric reachability | +| Goloboff & Farris (2001), "Methods for quick consensus estimation", Cladistics 17: S26–S34 | TBR-collapsing rule: collapse all nodes between source/dest for equal-length rearrangements | +| Goloboff (2023), "Searches, implied weights, and tree collapsing", Cladistics 39: 229–238 | Empirical comparison of collapsing criteria during search; "minimum possible length 0" recommended | +| Day et al. (1985) / TreeDist | O(n·k) strict consensus via compatible-splits method; available in TreeDist | + +### Detailed literature notes (from PDF review 2026-03-22) + +**Goloboff 1996 — §"Collapsing The Trees" (pp. 213–218)** + +1. *Shortest-path test (approximate)*: If no node in the path between the + clipped subtree's original position and the destination is "supported" + (has character-state change), the rearranged tree collapses to the same + polytomy as the original. The tree can be discarded without full + reoptimization. This is the core shortcut that our collapsed-region + skipping approximates. + +2. *Asymmetric reachability*: The shortest-path shortcut creates directed + connectivity — swapping on tree A may find B, but swapping on B may not + find A. Goloboff gives an explicit example (5 taxa, `x000 a100 b011 + c111 d111`) where the dichotomous tree A can reach the trichotomous + tree B, but no resolution of B can reach A because the movement would + cross only unsupported nodes. He argues this is acceptable: "heuristic + searches cannot guarantee finding all of the optimal trees, or even any + of them—with or without shortcuts." + +3. *Efficient collapsing via final states*: For characters where the final + state sets don't change after rearrangement (checked by comparing basal + node of clipped subtree against ancestor/descendant of destination + branch), only 10–20% of characters need reoptimization for collapsing. + This maps to our incremental scoring infrastructure. + +4. *Union construct method*: A further optimization that evaluates + destinations "en masse" by computing union state sets for subtrees and + rejecting entire branches when the union construct produces suboptimal + length. Achieved 50% time reduction on congruent datasets (168 taxa), + but no gain on incongruent data. + +**Goloboff & Farris 2001 — "Methods for quick consensus estimation"** + +1. *TBR-collapsing rule*: "when a rearrangement produces a tree of the + same length as the one being swapped, collapsing all of the nodes + between source and destination (and new root, in the case of TBR)." + This is equivalent to saving all equal-length trees and computing their + strict consensus, but uses no extra memory and less time. + +2. *SPR vs TBR collapsing*: TBR-based collapsing eliminates more + spurious groups than SPR-based, with minimal loss of correct groups. + On Zilla (500 taxa): SPR collapsing gives 79.6% true nodes recovered + with 0.63% error rate; TBR gives 79.0% true nodes with 0.48% error + rate. Net effect: TBR collapsing is more reliable. + +3. *RFD (Relative Fit Difference)*: Extends collapsing to suboptimal + trees by measuring `(F-C)/F` where F = favorable fit, C = contradictory + fit. Nodes with RFD below a threshold Q are collapsed. When calculating + rearrangement length, as soon as length increase X > D/(1-Q), the + rearrangement can be abandoned. For Q=0.10, tree collapsing takes only + 5% additional time. This could be a future extension (post-2.0.0). + +4. *Pool benefit*: Collapsing trees during swapping means different + dichotomous trees that differ only in "minor" rearrangements collapse + to the same polytomy. The pool then stores more topologically diverse + trees, improving search effectiveness. This directly validates our + Phase 5 (collapsed-topology pool dedup). + +**Goloboff & Morales 2023 — TNT version 1.6** + +1. *Consensus stabilization*: TNT's driven search can stop when the + strict consensus is stable after N hits — analogous to TreeSearch's + `consensusStableReps`. TNT's parallel mode has a coordinator that + centralizes consensus calculation. + +2. *Parallel architecture*: "Builders" create trees via Wagner+TBR+ + sectorial/ratchet/drift, pass them to a "fuser" task. Similar to + TreeSearch's `ThreadSafePool` pattern but using PVM processes rather + than threads. + +3. *Fast consensus*: The user notes that Day et al. (1985) O(n·k) strict + consensus is available via the TreeDist package. This could replace or + supplement the XOR-hash consensus approximation in `ts_pool.cpp` for + more accurate stability detection. Not needed for the polytomy search + itself, but relevant for improving consensus-stability stopping. + +### What TNT does + +TNT collapses zero-length branches **during search** by default (`collapse +3;` = TBR-rule). After each TBR rearrangement is accepted, zero-length +edges are contracted into polytomies. TBR then operates on the collapsed +(non-binary) tree, which has fewer edges to clip and regraft through. The +key benefits are: + +1. **Fewer TBR candidates**: a polytomous tree with k collapsed edges has + ~2k fewer clip candidates and ~2k fewer regraft positions per clip. +2. **Pool deduplication**: collapsed trees that differ only in unsupported + resolution are identical, preventing the pool from filling with + trivially different trees. +3. **Better convergence**: the search explores "real" topological + differences rather than wasting effort on unsupported resolutions. + +--- + +## Design decision: Approach B (collapsed-edge set, binary internals) + +After reviewing the codebase, **Approach A** (replacing `left[]`/`right[]` +with multi-child representation) would require rewriting every module — TBR, +SPR, NNI, Fitch scoring, NA scoring, incremental scoring, undo stacks, +Wagner construction, constraint checking, sectorial search, fusing, splits. +This is estimated at 10+ weeks and carries extreme regression risk. + +**Approach B** is both faster to implement and closer to what TNT actually +does. TNT stores trees as binary internally but maintains a set of +"collapsed" edges that modify candidate enumeration and pool comparison. +The binary topology is always available for scoring; collapsed edges just +indicate which resolutions are unsupported. + +### Core idea + +Maintain a `std::vector collapsed` flag array alongside the +existing binary `TreeState`. After each accepted TBR/SPR move + full +rescore: + +1. **Recompute collapsed flags** (already implemented in `ts_collapsed.cpp`) +2. **Skip collapsed clips** in TBR/SPR/drift candidate enumeration +3. **Skip collapsed regraft distinctions**: when regrafting into a region + of consecutive collapsed edges, all positions within that region + produce the same score — evaluate only one representative position +4. **Pool comparison uses collapsed form**: two binary trees that collapse + to the same polytomy are treated as duplicates + +### Why this works without changing TreeState + +- Scoring uses the binary tree (exact Fitch downpass/uppass, unchanged) +- Topology manipulation uses binary operations (SPR clip/regraft, unchanged) +- Only candidate **enumeration** changes (skip/merge collapsed regions) +- Pool comparison adds a collapsed-topology hash alongside the existing + binary split hash + +The binary tree is always there as a "refinement" of the collapsed tree. +When a move is accepted that resolves a polytomy (puts signal on a +previously zero-length edge), the collapsed flag simply clears. + +--- + +## Implementation plan + +### Phase 1: Collapsed-region identification (extend existing code) + +**Files:** `src/ts_collapsed.h`, `src/ts_collapsed.cpp` + +The existing `compute_collapsed_flags()` already identifies edges where +clipping cannot improve score. Extend this to also identify **collapsed +regions** — maximal connected subsets of collapsed edges forming a +polytomy: + +```cpp +struct CollapsedRegion { + int representative; // one node in the region (for regraft targeting) + int n_edges; // number of collapsed edges in this region + std::vector nodes; // all nodes with collapsed[node] == 1 in region +}; + +struct CollapsedInfo { + std::vector collapsed; // per-node flag (existing) + std::vector region_id; // per-node: which region (-1 if not collapsed) + std::vector regions; // the collapsed regions + int n_collapsed = 0; // total collapsed edges +}; + +void compute_collapsed_info( + const TreeState& tree, + const DataSet& ds, + CollapsedInfo& info); +``` + +This is a simple post-processing step after the existing flag computation: +BFS/DFS from each collapsed node, grouping connected collapsed edges. + +**Estimated effort:** 1–2 days + +### Phase 2: TBR clip skipping (already partially done) + +**Files:** `src/ts_tbr.cpp` + +The current code already skips collapsed clips when `!collect_pool`. Verify +this is working correctly and add a **diagnostic counter** (`n_collapsed_skipped`) +to the TBR return value for benchmarking. + +No code change needed beyond the diagnostic counter — Phase 1's extended +flags subsume the existing implementation. + +**Estimated effort:** 0.5 days + +### Phase 3: TBR regraft region merging (the main win) + +**Files:** `src/ts_tbr.cpp` + +This is the key new optimization. When evaluating regraft positions for a +non-collapsed clip: + +**Current behavior:** enumerate all main-tree edges as regraft candidates, +evaluate each independently. + +**New behavior:** for each collapsed region, evaluate only **one +representative regraft position** within the region. All positions within a +collapsed region produce identical scores (because the intermediate nodes +have zero cost and identical state sets — exactly the conditions verified +by `compute_collapsed_flags()`). + +Implementation in the TBR regraft loop: +```cpp +for (auto& [above, below] : main_edges) { + // Skip redundant positions within collapsed regions + if (collapsed_info.collapsed[below] && + collapsed_info.region_id[below] == last_evaluated_region) { + continue; // same region, same score — skip + } + last_evaluated_region = collapsed_info.region_id[below]; + + // ... evaluate regraft as before ... +} +``` + +**Correctness argument:** Within a collapsed region, all edges have: +- Zero local cost at parent (condition 1–2 of collapsed flags) +- `prelim[sibling] == prelim[parent]` (condition 3) +- `down2[sibling] == down2[parent]` (condition 4, NA) +- `subtree_actives[sibling] == subtree_actives[parent]` (condition 5, NA) + +Therefore the `final_` states used by `fitch_indirect_length()` at any +edge within the region produce the same `vroot` value, giving identical +scores for all regraft positions in the region. + +**Important subtlety:** The best regraft position's `(above, below)` pair +matters for the actual topology after the move. When a collapsed-region +regraft is chosen, we regraft at the representative position. The resulting +tree will have a different binary resolution of the polytomy, but the same +score and the same collapsed topology. This is equivalent to TNT's behavior. + +**Estimated effort:** 3–5 days (careful correctness verification needed) + +### Phase 4: SPR and drift integration + +**Files:** `src/ts_search.cpp`, `src/ts_drift.cpp` + +Apply the same clip-skipping (already in Phase 2) and regraft-merging +(Phase 3 pattern) to SPR search and drift search. + +For drift: suboptimal-acceptance moves should still skip collapsed clips +(a collapsed clip cannot improve OR change the score, so accepting it +is always a no-op). Regraft merging applies identically. + +**Estimated effort:** 2–3 days + +### Phase 5: Pool deduplication using collapsed form + +**Files:** `src/ts_pool.h`, `src/ts_pool.cpp`, `src/ts_splits.h` + +Currently pool deduplication uses binary split hashes. Two trees that +differ only in unsupported resolution have different split hashes but +should be considered duplicates. + +**Add collapsed-topology hashing:** +1. After computing collapsed flags, identify the "collapsed splits" — + the splits that remain after contracting all collapsed edges. +2. Hash only the non-collapsed splits for pool dedup. +3. Use this hash as the primary dedup key; fall back to binary hash + for trees with no collapsed edges (fully resolved). + +Implementation: +```cpp +uint64_t compute_collapsed_hash( + const TreeState& tree, + const CollapsedInfo& info, + int n_tip); +``` + +This is a filtered version of the existing `compute_splits()` + +`hash_single_split()` pipeline — just skip splits corresponding to +collapsed edges. + +**Estimated effort:** 2–3 days + +### Phase 6: Ratchet interaction + +**Files:** `src/ts_ratchet.cpp` + +During ratchet perturbation, character weights change, which means +collapsed flags must be recomputed after perturbation. The ratchet already +calls `tbr_search()` which recomputes flags after each accepted move, so +this should work automatically. + +**One subtlety:** After ratchet perturbation (upweighting/zeroing chars), +some previously collapsed edges may become non-collapsed (the perturbed +weights create artificial signal). This is correct behavior — the +perturbation should explore the full binary space. + +After ratchet un-perturbation (restoring original weights), the full +rescore will re-establish correct collapsed flags. + +**Estimated effort:** 1 day (verification + edge case testing) + +### Phase 7: Sectorial search interaction + +**Files:** `src/ts_sector.cpp` + +For sectorial search, collapsed flags should be computed on the full tree +and passed to the sector TBR. Within a sector: +- Clip candidates that are collapsed in the full tree remain collapsed +- Regraft merging applies within the sector + +Collapsed flags for the **reduced dataset** (sector subproblem) should be +recomputed from the sector's own scoring, not inherited from the full tree. + +**Estimated effort:** 2–3 days + +### Phase 8: Wagner tree collapsing + +**Files:** `src/ts_wagner.cpp` + +After Wagner tree construction, compute collapsed flags before the first +TBR pass. Wagner trees typically have many zero-length edges (the greedy +construction often creates unsupported resolutions), so this is where +collapsed-region merging may have the biggest per-tree impact. + +**Estimated effort:** 0.5 days + +### Phase 9: Testing + +**Files:** `tests/testthat/test-ts-polytomy-search.R` (Tier 2) + +1. **Region identification:** hand-built trees with known collapsed + regions; verify region count and membership. +2. **Regraft merging correctness:** verify that evaluating all positions + vs. one-per-region gives identical best scores. +3. **Pool collapsed-hash dedup:** two trees differing only in zero-length + resolution are treated as duplicates. +4. **Score equivalence:** driven search with collapsed optimization + produces same or better scores than without. +5. **IW/Profile mode compatibility.** +6. **NA dataset compatibility.** +7. **Ratchet interaction:** collapsed flags correctly update after + perturbation and un-perturbation. +8. **End-to-end regression:** run existing benchmark datasets, verify + no score degradation. + +**Estimated effort:** 3–4 days + +### Phase 10: Benchmarking + +Re-run the TNT benchmark comparison with collapsed search enabled: +- Same 14 datasets, EW Fitch, 10s and 30s timeout +- Compare scores, timing, and replicates completed +- Focus on the 5 datasets where TreeSearch fell behind + +Also measure: +- Collapsed edge percentage per dataset (at optimum) +- Regraft candidates skipped per TBR pass +- Pool duplicate reduction + +**Estimated effort:** 1–2 days + +--- + +## Risk assessment + +| Risk | Severity | Mitigation | +|------|----------|------------| +| Regraft merging incorrectly skips a productive position | HIGH | Formal correctness proof + extensive unit tests; conservative fallback to evaluate all if collapsed count is low | +| Collapsed flags stale after ratchet perturbation | MEDIUM | Flags always recomputed after full_rescore; verify in ratchet tests | +| Pool collapsed-hash collisions (different topologies hash same) | LOW | Conservative direction (over-dedup); hash collision = treat as duplicate = miss one tree, not wrong scores | +| Negligible benefit on dense morphological data | MEDIUM | TNT benchmarks show the benefit is real; if our data shows otherwise, document and stop | +| Interaction with MPT enumeration | HIGH | Collapsed optimizations MUST be disabled during `collect_pool` (equal-score exploration); already guarded in existing code | + +--- + +## Estimated total effort + +| Phase | Days | Cumulative | +|-------|------|------------| +| 1. Collapsed regions | 1–2 | 1–2 | +| 2. TBR clip (existing) | 0.5 | 1.5–2.5 | +| 3. TBR regraft merging | 3–5 | 4.5–7.5 | +| 4. SPR + drift | 2–3 | 6.5–10.5 | +| 5. Pool dedup | 2–3 | 8.5–13.5 | +| 6. Ratchet | 1 | 9.5–14.5 | +| 7. Sectorial | 2–3 | 11.5–17.5 | +| 8. Wagner | 0.5 | 12–18 | +| 9. Testing | 3–4 | 15–22 | +| 10. Benchmarking | 1–2 | 16–24 | + +**Total: 16–24 agent-days.** Substantially less than the 9–13 weeks +estimated for Approach A (full polytomy representation). + +--- + +## Literature review — COMPLETE (2026-03-22) + +All three papers reviewed from PDF. Key algorithmic details extracted +in the "Detailed literature notes" section above. The Goloboff (2023) +paper on collapsing criteria was not available in PDF but its core +recommendation ("minimum possible length 0" during search) is documented +in the AGENTS.md architecture reference. + +--- + +## Success criteria + +1. **Score parity or improvement** on all 14 TNT benchmark datasets + (no regressions) +2. **Measurable collapsed-edge skip rate** (>0%) on at least the harder + datasets (Wortley2006, Eklund2004, Zanol2014, Zhu2013, Giles2015) +3. **All existing tests pass** (1859 ts-* tests + full R-level suite) +4. **New test file** with ≥15 assertions covering all phases + +--- + +## References + +- Goloboff, P. A. (1996). Methods for faster parsimony analysis. Cladistics, 12, 199–220. +- Goloboff, P. A. & Farris, J. S. (2001). Methods for quick consensus estimation. Cladistics, 17, S26–S34. +- Goloboff, P. A. (2023). Searches, implied weights, and tree collapsing. Cladistics, 39, 229–238. +- Goloboff, P. A. & Catalano, S. A. (2016). TNT version 1.5. Cladistics, 32, 221–238. diff --git a/dev/plans/2026-06-16-closing-the-tnt-gap.md b/dev/plans/2026-06-16-closing-the-tnt-gap.md new file mode 100644 index 000000000..22d73248b --- /dev/null +++ b/dev/plans/2026-06-16-closing-the-tnt-gap.md @@ -0,0 +1,306 @@ +# Closing the TNT Gap — Strategic Plan + +Branch: `cpp-search` · Reference: TNT 1.6 · Comparison path: equal-weights Fitch, +apples-to-apples (`-` → `?`). Drafted 2026-06-16. Supersedes the retired +`.positai/plans` strategy thread (see `dev/plans/README.md`). + +## Goal + +Close the **wall-clock** gap to TNT 1.6 on equal-weights Fitch parsimony. TNT +reaches a comparable-quality tree roughly **2× faster**. The kernel is already +refined (we are competitive-to-faster *per candidate*), so the lever is search +strategy — specifically, how many candidate rearrangements we burn per unit of +score improvement. + +## Reframe: three gaps, not one + +| Gap | What it measures | Magnitude | Target? | +|-----|------------------|-----------|---------| +| **A. Scoring method** | Brazeau three-pass vs TNT column-Fitch on *inapplicable* data | +1 … +50 (e.g. Vinther raw 79 vs 78) | **No** — a different, arguably better objective; vanishes under `-`→`?`. | +| **B. Score quality** @ fixed time, apples-to-apples Fitch | TNT finds a shorter tree, same budget | +2/+3 steps, hardest datasets | Small; perturbation-tuning lever now **spent**. | +| **C. Wall-clock** to a comparable score | TNT is ~2× faster | ~1.5–3× | **Yes — the prize.** | + +Empirical confirmation of A vs B in one row (Phase 0, Vinther2008): TreeSearch +Fitch **78** = TNT **78** (gap B = 0); TreeSearch *raw* (Brazeau) **79** (gap A = +1). + +## Diagnosis: C is a candidates-per-improvement gap, concentrated in sectorial search + +`wall-clock = (cost per candidate) × (candidates per unit of improvement)`. + +- **Cost per candidate**: competitive. The raw Fitch kernel may lead TNT; in-search + per-candidate cost carries StateSnapshot/rescore overhead (T-260), but this is + not where the 2× lives. +- **Candidates per improvement**: we are far worse. **First instrumented measurement** + (`bench_tnt_headtohead.R`, candidates-per-improvement mode): on Vinther2008, at the + *same* score (78), TreeSearch evaluated **2.90M** candidate rearrangements vs TNT + **0.46M** — **6.3×**. Even on a tie we burn 6× the work. +- **Where**: TNT's `xmult` is ~67% sectorial search. Our sectorial search runs, but + with equal-score acceptance **hard-coded off** (`ts_sector.h:24 accept_equal=false`, + never set true on the `MaximizeParsimony` path) and approximate HTU scoring. + +## Plan (phased, data-gated) + +**Phase 0 — instrument + baseline.** +- *0a (DONE)*: `candidates_evaluated` — total TBR/SPR-class candidates, accumulated in + `tbr_search` into `DataSet::n_candidates_evaluated`, summed over a serial + `driven_search`, surfaced as `attr(MaximizeParsimony(...), "candidates_evaluated")`. + Behaviour-neutral; valid `nThreads=1` only; excludes NNI-warmup/annealing. +- *0b (DONE)*: `dev/benchmarks/bench_tnt_headtohead.R` — TreeSearch (Fitch + raw) vs + TNT, capturing score, candidates, TNT rearrangements, wall-clock; separates gaps A/B/C. +- *0c (DONE)*: gap-panel baseline (`headtohead_phase0.csv`, 2 seeds, converged). + **Gap B = 0..+3.5 steps** (Zanol +3.5, Wortley +3, Zhu +2.5, Giles +1.5, Dikow/Eklund 0). + **Gap A** (raw − Fitch) = +50/+39/+12 on high-inapplicable Zanol/Giles/Zhu, 0 on + Wortley/Eklund — pure scoring method, tracks inapplicable fraction. **Candidates-per- + improvement ~1.3–1.9×** on real datasets (the Vinther 6.3× was a tiny-dataset outlier), + and TNT lands a *better* score — more efficient on both axes. Wall-clock ≈1.3–2.5× vs + *32-bit* TNT (larger vs fair 64-bit), ≈ or above the candidate ratio (per-candidate + overhead is a co-contributor). + +**Phase 1 — phase-yield diagnosis (DONE; REDIRECTS Phase 2).** `bench_phase_yield.R` +(per-phase wall-clock share + total candidates + `late_frac`): +- **Ratchet dominates wall-clock: 63–83%** (Wortley 83, Eklund 76, Dikow 68, Giles 66, + Zanol 66, Zhu 63). **Sectorial is minor: 7–23%.** final-TBR 2%, init-TBR 2–5%, fuse 0–2%. +- **44–98% of replicates land AFTER the last improvement** (`late_frac`: Eklund 0.98, + Giles 0.90, Wortley 0.81, Dikow 0.79, Zanol 0.61, Zhu 0.44) — large post-convergence waste. +- **Implication:** the cost centre is RATCHET, not sectorial — the *opposite* of TNT + (~67% sectorial). We pour 66–83% of wall-clock into ratchet and still finish +2/+3 worse. + *Caveat (to verify):* this is wall-clock share; sectorial's reduced-dataset candidates are + cheaper per candidate, so a **per-phase CANDIDATE counter** is the next instrumentation to + confirm ratchet also dominates candidate *count*, not just clock. `adaptive_level` likely + scales ratchet_cycles UP on stalled (hard) datasets — pumping effort into the wasteful phase. + +**Phase 2 — REDIRECTED by Phase 1 data. Experiments, cheapest first, each gated on +candidates-per-improvement + score vs baseline; default-off until validated:** +1. **Cut wasted ratchet.** 44–98% of reps are post-convergence. Test tighter stopping + (`perturbStopFactor`, `targetHits`), fewer `ratchetCycles`, and capping/disabling the + `adaptive_level` ratchet up-scaling on stalled datasets. Cheapest, biggest wall-clock lever. +2. **Rebalance ratchet → sectorial.** Shift budget toward sectorial (TNT's efficient phase): + more `xss/rss` rounds, fewer ratchet cycles. +3. **Make sectorial plateau-capable** so leaning on it pays: wire + gate `accept_equal` + (`ts_sector.h:24`, built/off) — Goloboff 2014 flat-landscape lever for high-inapp + Zanol/Giles; + drift-done-right (large `numsub` + equal acceptance). +- *Next instrumentation:* per-phase candidate counter (mirror `PhaseTimings`/`ph_lap`) to + confirm the clock→candidate correspondence before committing to a ratchet rewrite. + +### Phase 2 results (2026-06-16) — cheap/medium levers tested, no robust global win + +Via `bench_p2_levers.R` (gap panel, fixed 20 reps; the fast loop made each round ~90s). +Deltas vs the `auto` baseline (`iterate_baseline_auto.csv`): + +- **Ratchet/sectorial knobs (round 1, `p2_levers.csv`):** `ratchetCycles` {3,6}, + `adaptiveLevel=off`, `xss/rss` rounds doubled, ratchet→sectorial `rebalance` — **none beats + baseline.** Cutting ratchet saves 30–60% candidates but costs +0.5–2.5 steps on hard + datasets (ratchet does real work); more sectorial rounds tie-or-worsen; `adaptiveLevel=off` + is exactly neutral. The `auto` preset is near-Pareto-optimal for these knobs. +- **`accept_equal` (the #1 untried lever; hard-coded on via the fast loop, then reverted):** + neutral-to-worse (Zanol +3, Giles +1), candidates barely move (0 to −3%). **Why it fails + here:** sectorial is only 7–23% of our wall-clock (Phase 1), so its acceptance criterion has + little leverage — the opposite of TNT (~67% sectorial). The built-but-off infrastructure is + not the lever *for our pipeline shape*. +- **Fusing/ordering/starts (round 2, `p2_levers_fuse.csv` 2-seed, `p2_fuse_5seed.csv` 5-seed):** + 5-seed medians confirm a *real but per-dataset* signal: **`wagnerStarts=5` and `intraFuse` + each robustly improve Wortley (−3/−2) and Zhu (−2/−3, → 626 vs TNT 624)** but **regress + Zanol/Giles by +1**; Eklund/Dikow neutral. `fuseAcceptEqual` ≡ `intraFuse`. `clipOrder=2` + saves 22–32% candidates at +1–2 steps (worse). **No feature cleanly separates helped (Wortley + 37t/8st, Zhu 75t/4st) from hurt (Zanol 74t/9st, Giles 78t/4st)** — so no safe global default. + +**Conclusion — apples-to-apples Fitch gap is at the practical parameter-tuning floor.** No +single config improves all panel datasets; the only real gains (−2/−3 on Wortley/Zhu) are +dataset-specific and come with +1 regressions elsewhere, failing the "no regression on any +dataset" ship gate. `accept_equal` (the headline untried lever) has no leverage in our +ratchet-dominated pipeline. Remaining options, by cost: **(a) accept the floor** — declare the +EW-Fitch gap effectively closed (+1/+3 on the hardest datasets), redirect effort; **(b) ship an +opt-in variant** (`intraFuse`/extra Wagner starts in `thorough`) so the Wortley/Zhu wins are +available without touching `auto`; **(c) Phase 3 structural** (branch-collapsing / exact-scoring +sectorial) — weeks-scale, the only thing that could move a ratchet-dominated pipeline toward +TNT's per-candidate frugality, but hard to justify for a residual +1/+3 steps. Recommendation: +**(a)+(b)**, not (c) — the data does not justify a weeks-scale structural rewrite for this gap. + +## Phase 3 design (2026-06-16) — structural options scoped; cheap falsifiable probe first + +A 4-agent design workflow assessed three structural options to cut candidates-per-improvement; +the user opted to commit to Phase 3, so it was scoped before any code. + +- **Branch-collapsing / full polytomy search (Goloboff 2023): REJECTED for this gap.** A 3–6 week + tree-representation + Fitch-kernel + TBR-clip rewrite touching the most-optimized code in the repo + (binary `left/right` in `ts_tree.h`, the 2-input SIMD primitives in `ts_fitch`, TBR clip/regraft). + It attacks the wrong axis (frugality, not the escape/depth ratchet owns), and its mechanism barely + fires: the project already measured ~0% collapsed-edge rate on near-optimal binary morphological + trees, and the advisory collapsed-flag skip (`ts_tbr.cpp:817-820,919-921`) + `add_collapsed` pool + dedup already bank the easy ~80%. +- **Exact-scoring sectorial (CSS): ALREADY ACTIVE on the gap datasets.** `css_search` + (`ts_sector.cpp:1005-1073`) runs full-tree TBR restricted to a `sector_mask` — exact by + construction, no HTU pseudo-tip, no miss-and-revert. `thorough` sets `cssRounds=2`, `large` + `cssRounds=1`, and `auto` routes 65–119t→thorough / ≥120t→large. So "implement exact sectorial" + is largely already done; the residual is a ratchet→CSS budget-**rebalance experiment** (days), not + a kernel rewrite. +- **Union-based region-merging (Lever 1): cheap (days) but likely a no-op.** `compute_collapsed_regions` + (`ts_collapsed.cpp:106-170`) is built but DEAD CODE (zero callers); wiring it merges equal-resolution + regraft positions. The 0%-collapsed-rate finding predicts it barely fires at the optimum on the hard panel. + +**Gap-closure risk: HIGH.** All three reduce candidates-per-improvement (frugality) but none finds +lower-score basins (the escape/depth axis ratchet owns at 63–83% of wall-clock). The +1/+3 most likely +remains after any rewrite — consistent with the Phase 2 floor finding. + +**Decision (data-gated):** do the smallest structural slice that decides the rest — a falsifiable probe: +(1) ratchet→CSS rebalance sweep on the gap panel (`p3_rebalance.csv`); (2) a collapsed-region/edge-rate +probe at the optimum (Lever-1 go/no-go). Escalate to wiring Lever 1 ONLY if the rebalance beats baseline +(no per-dataset regression) AND regions are non-trivial; otherwise confirm the floor and rest on the +shipped (a) accept-floor + (b) opt-in `intensive` preset. Branch-collapsing is pursued only if both +probes reveal a large, real collapsed signal the advisory path leaves on the table — which the existing +data predicts they will not. + +### Phase 3 outcome (2026-06-16) — structural search rewrite NOT justified; pivot to per-candidate wall-clock + +The ratchet→CSS rebalance probe (`p3_rebalance.csv`, 3 seeds) is **FLAT**: every config that trades +ratchet budget for exact CSS saves 28–51% candidates but **regresses the hard datasets** (Zanol +4, +Giles +2); none beats baseline without a per-dataset score regression. With the design verdict +(branch-collapsing wrong-axis + ~0% collapsed rate; CSS already active on the gap datasets) this is the +third convergent confirmation that **the EW-Fitch score gap is at the practical floor and is +landscape/escape-bound, not frugality-bound** — no structural *frugality* lever closes it. The Lever-1 +region-merging precondition (rebalance must beat baseline) failed, so it is not pursued; branch-collapsing +is rejected. + +**Pivot — the movable lever for the original ~2× WALL-CLOCK concern is per-candidate COST, not count.** +The frugality analysis surfaced it as "option 4": VTune (`vtune_tbr_analysis.md`, T-260) puts StateSnapshot +save/restore at ~23% of TBR time (a full ~190 KB memcpy per accepted/rejected move) and a redundant +`reset_states` `std::fill` at ~4%. `apply_tbr_move` already knows the dirty nodes, so selective save/restore +of only those rows is est. **10–16% wall-clock**, **dataset-agnostic, no score trade-off** — it cuts the +time per candidate rather than the candidate count, which is orthogonal to the score floor and directly +targets wall-clock. + +**Correction on inspection (do NOT act on the stale figures above):** +- The `reset_states` `std::fill` (design "fix #2", ~4%) was **already removed in T-261** + (`ts_tree.cpp:265-277` — "every array entry read is written before it is read"). That win is banked. +- The cited StateSnapshot ~23% comes from a VTune doc that **predates T-261** (it *recommended* the + fill removal T-261 then made) and likely T-300's incremental-SPR accept path — so the figure is + **stale and the share has probably shrunk**. The remaining lever (selective `StateSnapshot` + save/restore) is intricate, correctness-critical surgery on the most-optimised code in the repo. +- **Decision:** it must be **re-profiled in a fresh `/profile` (VTune) round** to confirm it is still a + meaningful hotspot *before* the surgery — not done on stale data at the tail of this round. Verification + when pursued: behaviour-neutral via **candidate-identity** (a correct timing optimisation must leave + `candidates_evaluated` and scores bit-identical vs baseline on the iterate gate) + a wall-clock + micro-benchmark for the delta; keep only if identical-and-faster, else revert. + +**Phase 3 — branch-collapsing search** (Goloboff 2023): search the reduced polytomy +tree space, not just skip candidates/dedup as now. Structural swing; pursue only if +Phase 1/2 data shows the candidate-frugality gap justifies it. + +## Challenge 2 closeout (2026-06-17) — ratchet now genuinely disableable; ratchet-OFF still trails TNT + +The "ratchet is untouched / disable it to match TNT" thread (user Challenge 2) is resolved. + +**Ratchet was never disableable.** `ratchetCycles = 0` still ran ratchet via three +stacked floors in `ts_driven.cpp` (ceiling-division `max(1, …)`, an unconditional call +site, and the `adaptive_level` re-floor `max(1, base * scale)`); `ratchet_search` also +runs an initial TBR pass before its cycle loop. All three are now guarded — a no-op for +every preset (all use `ratchetCycles ≥ 3`), covered by `test-ts-ratchet-disable.R`. + +**With ratchet genuinely off, TreeSearch does NOT match TNT.** Patched build, +`adaptiveLevel = FALSE`, TNT-matched core, 4 datasets × 5 seeds, only `ratchetCycles` +varied (`bench_ratchet_axis.R` → `ratchet_axis.csv`). Median gap to TNT `xmult` +(arm − TNT, lower = better): + +| dataset | TNT | R0 (true off) | R1 | R12 | gap R0 | gap R12 | +|---|---|---|---|---|---|---| +| Giles2015 | 670 | 675 | 675 | 672 | +5 | +2 | +| Wortley2006 | 480 | 485 | 487 | 482 | +4 | +2 | +| Zanol2014 | 1262 | 1269 | 1268 | 1267 | +8 | +5 | +| Zhu2013 | 624 | 631 | 631 | 629 | +7 | +5 | + +- Ratchet-off (R0) trails TNT by **+4…+8** on every dataset; even our single best + ratchet-off seed never reaches TNT's median. The deficit is **not** ratchet-caused + (TNT runs no ratchet on these either) → it is structural. +- Ratchet helps **monotonically**: R12 closes the gap to +2…+5 (−2…−3 vs R0) at ~2–3× + wall-clock. "Disable ratchet to match, then switch on to pull ahead" inverts reality — + ratchet is *necessary to approach* TNT; it narrows but never erases the deficit. +- The residual gap is **sectorial / fusing search efficiency** — re-examined against the + published algorithm in the Goloboff-1999 divergence analysis (2026-06-17, + `dev/plans/2026-06-17-sectorial-divergence.md`). + +Memory: `ratchet-not-disableable.md`. (Local TNT is 32-bit, so its wall-clock is not a +fair reference; scores / rearrangement counts are. R0-vs-R12 wall-clock is comparable.) + +## Methodology guardrails + +- **Optimise against candidates-per-improvement** (continuous, low-variance), not + score-at-fixed-time (±2-step lottery on a small panel). +- **Authoritative wall-clock**: Hamilton 64-bit Linux TNT (matches the on-disk + `t264`/`t249` reference scores). The local `tnt.exe` is **32-bit** (PE32/i386) — + its *scores and rearrangement counts are valid* (bitness-independent), but its + *wall-clock is not* a fair reference for our 64-bit build. (User believes a Win64 + TNT exists locally; not found at the standard path — to confirm.) +- Validate on the **MorphoBank validation split**, not just the 14 CRAN datasets. + Report median + min over ≥5 seeds. `nThreads=1`. Everything default-off until gated. + +## Artifacts + +- Harness: [bench_tnt_headtohead.R](../benchmarks/bench_tnt_headtohead.R) +- Metric: `attr(MaximizeParsimony(...), "candidates_evaluated")` (serial) +- Baseline data: `dev/benchmarks/headtohead_phase0.csv` + +## Decisions / dead ends (do not re-propose) + +- **Perturbation escalation** (`stallEscalateFactor`, shipped 2026-06-16): score-neutral + on Wortley/Zanol → this vein is **spent**. It ships off-by-default as a stall safety net. +- **Static perturbation re-tune** (`ratchetPerturbProb=0.15`): refuted — regresses + Zanol/Zhu by +9/+11. +- **Drift / NNI-perturb / prune-reinsert / adaptiveStart in presets**: recorded-negative + (T-274, T-289f, T-190); out of scope except drift's *specific* untested combination above. +- **Raw speed** (AVX2/popcnt): real but won't close the strategic gap. + +## Phase 4 (2026-06-18) — UPDATE: the "floor" was a cost-formula bug; score gap CLOSED + +The Phase 1–3 / Challenge-2 conclusion that the EW-Fitch gap was a +**"landscape/escape-bound floor, not frugality-bound"** with a "competitive +per-candidate kernel" is **superseded**. Those phases predated finding a +correctness bug in the candidate insertion-cost function. + +**Root cause (see `2026-06-18-wagner-insertion-cost-bug.md` + memory +[[wagner-insertion-cost-bug]]).** `fitch_indirect_length*` scored a candidate +insertion edge with the **union of the two endpoints' final states** +(`final[A] | final[D]`), which is not the exact edge set — it undercounts on +ambiguous trees and mis-ranks/mis-cuts moves on resolved ones. This (a) made RAS +Wagner starts **~+30% over the optimum** (near-random greedy placement) and +(b) gave TBR wrong cost magnitudes → wrong cutoffs / early abandonment. The fix +is the exact **directional** edge set `E[D]=combine(prelim[D],up[D])` +(`compute_insertion_edge_sets`, ts_fitch). Shipped to `wagner_tree`, the EW +`tbr_search` SPR scan + rerooting vroot, and `build_ras_sector` (commits +2b299e4b, 93071cae on cpp-search). + +**Result — full `thorough` search now reaches the MPT across the hard panel** +(`diag_gap_panel_postfix.R`, 60s, 3 seeds): + +| dataset | target | post-fix (min / median) | pre-fix floor (Phase 2 / Challenge-2 R12) | +|---|---|---|---| +| Wortley2006 | 480 | **479 / 479 (−1)** | +3 / +2 | +| Zanol2014 | 1261 | **1261 / 1261 (+0)** | +3.5 / +5 | +| Zhu2013 | 624 | **624 / 624 (+0)** | +2.5 / +5 | +| Giles2015 | 670 | **670 / 670 (+0)** | +1.5 / +2 | + +So the gap the plan repeatedly called "structural / escape-bound" was, in +substantial part, this scoring-formula bug. Gap **B is now ~0** (was +1.5..+3.5). + +**Candidates-per-improvement (gap C) reversed.** Vinther2008 (the canonical tie, +pre-fix "6.3× *more* candidates than TNT"): post-fix `bench_tnt_headtohead.R` +gives TS 78 = TNT 78 with **cand_ratio 0.44** — TreeSearch now examines *less +than half* TNT's rearrangements to reach the same score (counters tally slightly +different events, so indicative — but a qualitative reversal). Wall-clock tied on +this small case (0.4s = 0.4s). + +**Disposition of the open threads:** +- **Core TBR/Wagner hill-climbing deficit (task #26): RESOLVED** — root-caused and + fixed; panel now at +0/−1. +- **Drift-done-right for "+1 datasets" (task #25): MOOT** — the +1 datasets + (Zanol/Giles) it targeted are now +0; no score gap remains for drift to close. +- **Race-to-common-target (task #22): target reached** across the panel; + candidates-per-improvement competitive-to-better. The *only* residual is the + authoritative **wall-clock ratio**, which is **Hamilton-gated** (local TNT is + 32-bit) — the still-live wall-clock thread, not a quality gap. + +**Remaining (genuinely open):** authoritative wall-clock vs 64-bit TNT on +Hamilton (the original ~2× concern — now partly addressed by the frugality +reversal, but unconfirmed on large datasets), and the chip's TBR +move-completeness fix (L812/nz/ns; small, poor-start-only — see +[[tbr-rooted-vs-unrooted]]). The per-candidate `StateSnapshot` micro-opt +(above) is independent and still available. diff --git a/dev/plans/2026-06-17-sectsch-escape-mechanism.md b/dev/plans/2026-06-17-sectsch-escape-mechanism.md new file mode 100644 index 000000000..e6bb66ca7 --- /dev/null +++ b/dev/plans/2026-06-17-sectsch-escape-mechanism.md @@ -0,0 +1,169 @@ +# How TNT's `sectsch=rss` escapes a single-sector-optimal tree — mechanism, from primary sources + +Date: 2026-06-17. READ-ONLY analysis (no `src/` edits). Independent of, and CORRECTING, +`2026-06-17-tnt-algorithm-audit.md` (whose RANK-1 hypothesis D1 is refuted below). + +Sources: Goloboff 1999 (Cladistics 15:415-428), full text at +`C:/Users/pjjg18/Zotero/storage/TETHI9A5/.zotero-ft-cache`; TNT defaults +`dev/benchmarks/tnt_defaults.txt`; TNT help `dev/benchmarks/tnt_help.txt`. +New empirical probes this session: `dev/benchmarks/diag_tnt_noglobal_probe.R`, +`diag_tnt_seq_accum.R`; existing oracle `dev/benchmarks/d1_confirm.out`. + +## Headline + +TNT's escape from an identical TNT-`mult` T0 is large and FAST: one RSS round drops +Zanol 1275->1264, a second 1264->1262, then plateaus (`diag_tnt_seq_accum.R`). From the +SAME T0, TreeSearch's TBR finds 0 AND its sectorial finds 0 (`sectorial_shared.csv`: +ts_tbr==ts_sect==start on every gap dataset). + +By process of elimination against the DEFAULTS, with two new TNT-side probes: + +- (d) recursion — OFF by default (`tnt_defaults.txt` "Recursion ... disabled"); `recurse2` + == `default` on all 4 datasets. NULL. +- (b) global-TBR cadence — REFUTED as the primary lever. `sectsch: noglobal;` (kills ALL + global TBR) BARELY changes the escape: Zanol -13->-13, Zhu -8->-8, Wortley -5->-4, + Giles -4->-2. `global 1` (max cadence) does NOT help and slightly HURTS. The -8..-13 + bulk happens with NO global swapping. Goloboff's "globally suboptimal under TBR" framing + is real but is the CLEAN-UP, not the barrier-crosser, for these n=37-76 EW cases. +- (a) accept-equal laterals — REAL but SMALL. `sectsch: equals;` adds ~1 step on EVERY + dataset and REACHES the sectsch target on 2/4 (Zanol -13->-14=1261; Giles -4->-5=670; + Zhu -8->-9; Wortley +1). Default is `noequals`, so laterals are NOT how the bulk escapes, + but they are the final bridge to the endpoint. +- THE BULK (-8..-13) is sequential strict-on-the-reduced-score sector REPLACEMENTS over + TNT's large, overlapping, sub-clade-collapsed sectors — with NO global TBR and NO equal + moves needed. + +## (a) Acceptance criterion — EXACT answer + +Goloboff p.418-419 step 3: "Choose the best among the R+r replications AND the present +resolution for the sector and place it in the whole tree." TNT default (`tnt_defaults.txt` +line 20) = `noequals` = "Not accepting equally good subtrees". So "best ... and the present +resolution" is STRICT: a re-solve replaces the present arrangement only if its REDUCED score +is strictly lower; an equal-length-but-different re-solve is NOT taken by default. With +`equals` ON, equal re-solves are taken (the help: "[no]equals accept equally good subtrees"). + +Crucial invariant (user-verified, re-confirmed in the audit's trace): for EW-Fitch the +from-above HTU makes `reduced = full - const` with const = rest-of-tree standalone Fitch +length, INVARIANT to how the sector re-roots. Therefore a strict reduced-score improvement +is identically a strict FULL-tree improvement (audit §3: 0 gate-bites). TNT's per-move accept +and TreeSearch's `new_score < result.best_score` (`ts_sector.cpp:1140`) are THE SAME GATE for +EW. The gate is NOT the gap. + +## (b) Global-TBR cadence — role, and why it is NOT the escape + +Goloboff step 4 + p.419: "A round of global swapping of the entire tree is made every 5 to +10 replacements, as that number makes it likely that (through clade substitution) the tree +will have become globally suboptimal under TBR." Mechanism as described: accepted sector +substitutions can leave the tree in a state where a cross-region TBR move now improves it; +periodic global TBR harvests that. TNT default (`tnt_defaults.txt` line 18) = global TBR +every 10 substitutions. TreeSearch runs ONE global TBR at the END of all picks +(`ts_sector.cpp:1199-1210`), looped by `rssRounds`. + +EMPIRICAL REFUTATION as the primary lever (`diag_tnt_noglobal_probe.R`): with `noglobal`, +TNT still escapes -13/-8 on Zanol/Zhu. So for these EW cases the barrier is crossed by the +sector replacements themselves; global TBR is a secondary clean-up. (Goloboff's framing is +about Zilla, n=500 — a much larger, cleaner composite-optima case where the cadence matters +more.) + +## (c) Large sector selection — the actual mechanism (Appendix 1 decoded) + +`selectem()` (Appendix 1, OCR-decoded; `5`=`=`,`,`=`<`,`.5`=`>=`,`2`=`-`): +```c +min_sz = (sector_sz * 80) / 100; // sector is 80-100% of cap +for (nod = rand() % root; clad_sz[nod] < min_sz;) // random node, walk UP + nod = anc[nod]; // until clade >= 80% of cap +items = marknodes(list, nod, 0, marker); // mark clade +for (a=items; a--;) if (list[a] < ntax) marker[list[a]] = 2; // all TIPS -> terminals +if (clad_sz[nod] >= sector_sz) { // clade too big: COLLAPSE sub-clades + for (...) if (!marker[x] && ...>=min_sz) { + marknodes(inlist, x, 1, marker); + marker[x] = 2; // sub-clade x becomes ONE composite terminal + if ((cur_sz -= clad_sz[x]-1) <= sector_sz) break; + } +} +marker[nod] = 2; // basal node = HTU terminal +``` +`marknodes` traverses left/right in RANDOM order (`side = 1 & rand()`). + +Two consequences: +1. The sector is LARGE (cap = `min(n/2,45)`; here ~n/2 for n<=90) and obtained by walking UP + from a random node, so it spans many small clades / much of the backbone. +2. When the clade exceeds the cap, whole sub-clades are COLLAPSED into single composite + terminals (their first-pass state set). The reduced RAS+TBR then reshuffles ~n/2 UNITS + that are themselves entire sub-clades, against each other and the rest-of-tree HTU. A move + that relocates one composite unit = transplanting a whole multi-taxon clade across the + backbone in the full tree — a large-radius move. + +TreeSearch instead SELECTS a single EXISTING clade in a size band [6,50] (`ts_sector.h:21-22`, +`rss_search:1037-1044`) and TBR-polishes it (default `ras_starts=1`), which is REDUNDANT with +the global TBR that already produced T0. It never collapses sub-clades, never spans the +backbone, and samples only `n_picks = 2*n_tip/avg_size` ~ 4-5 sectors/round (`ts_sector.cpp:1025`) +vs TNT's ~20-25. + +## (d) Recursion — not part of the default escape + +`tnt_defaults.txt`: "Recursion (user-defined searches) disabled". `recurse2` == `default` +empirically. NULL. + +## Why the audit's RANK-1 hypothesis (D1, frozen HTU attachment) is REFUTED + +The audit argued TNT escapes by letting the HTU float (jointly re-resolve + re-root the +sector). The session left an oracle in code: `TS_FREE_HTU_PROBE` (`ts_sector.cpp:867-888`) +runs, per sector, an UNCONSTRAINED reduced search (HTU = ordinary floating (S+1)th leaf, 20x +RAS+TBR) and prints `<= anchored` — equal on small sectors (the float adds nothing), +strictly WORSE on large sectors (cold from-scratch RAS is weaker than the warm anchored polish: +e.g. Zhu sect76 free=663 vs anchored=631). The 392 `<0, orig=1592 etc.) beats its own bad start by +floating the HTU; that floated score never beats the warm s=0 (=T0) arrangement. Freeing the +HTU forecloses NOTHING. D1 is dead. (This also matches the user's direct finding: 20 RAS+TBR +floating-HTU re-solves per sector of T0 beat T0 on no sector.) + +## RANKING of candidates a-d (by evidence) + +1. **(c) sector geometry + replacement count** — large, overlapping, sub-clade-collapsed, + ~n/2-tip sectors, ~20-25 per round, walked UP from random nodes. This is what makes the + sequence of strictly-full-improving moves AVAILABLE that TreeSearch's small-clade-band, + ras1, ~5-pick sectorial never proposes. PRIMARY. (Consistent with the band-shape probe + finding the lever "real"; the prior "marginal-to-noise" verdict was under END-TO-END with + aggressive ratchet that SUBSUMES it — from a frozen T0 with ratchet OFF it is the lever.) +2. **(a) accept-equal laterals** — `equals` adds the final ~1 step and reaches target on 2/4. + SECONDARY bridge. NOT default, NOT the bulk. +3. **(b) global-TBR cadence** — `noglobal` barely dents the escape on these n<=76 EW cases. + TERTIARY clean-up (matters more at Zilla scale). +4. **(d) recursion** — null. Not in default. +5. **(D1) HTU float** — REFUTED by the in-tree oracle. + +## MINIMAL modification to TreeSearch's strict-descent per-sector sectorial + +To replicate TNT's escape from a frozen T0, in priority order: + +1. **Change sector SELECTION to TNT's `selectem` (PRIMARY).** Replace the "existing-clade-in- + [min,max]" pick (`rss_search:1037-1044, 1078-1088`) with: pick a random node, walk UP via + `parent[]` until `subtree_size >= 0.8 * cap` (cap = `min(n/2,45)`); take that clade; if it + exceeds cap, COLLAPSE descendant sub-clades into composite terminals until size <= cap + (this is exactly `build_reduced_dataset_collapsed`, already in the tree — wire it to the + walk-up selection, not just to oversized clades). This makes large, backbone-spanning, + sub-clade-collapsed sectors whose RAS+TBR re-solve proposes large-radius full-tree moves. +2. **Raise sectors-per-round to TNT's count.** Set `rss_picks_per_round` ~ `T*100/(50*S)` + (TNT's `selfact` law, `tnt_defaults.txt` line 13) ~ 20-25, not `2T/S` ~ 5 + (`ts_sector.cpp:1025`). More sequential replacements per round = more chances to chain. +3. **Set `rasStarts=3` (+3 on score disagreement).** No preset sets it; stays 1 + (`ts_sector.h:29`), which is pure TBR-polish (redundant with global TBR). TNT does 3+3 + (`tnt_defaults.txt` lines 15-16). The re-solve must be a RAS REBUILD to propose new + topologies, not a re-polish. +4. **Turn `accept_equal` ON for the sector re-solve (the bridge step).** Already wired + (`sectorAcceptEqual` -> `params.accept_equal` -> `search_sector`, `ts_rcpp.cpp:1399`). + Worth ~1 step and reaches target on 2/4. NB: keep the strict FULL-tree gate (item 8b) for + safety under NA; for EW it is equivalent anyway. +5. **Global-TBR cadence is LAST.** Optionally move the single end-of-round TBR + (`ts_sector.cpp:1199`) to fire every ~10 accepted replacements INSIDE the pick loop. + Lowest expected yield on these datasets (noglobal barely changed TNT), but cheap and + matches Goloboff for larger data. + +Expected: items 1-3 (geometry + count + RAS rebuild) recover the bulk (-8..-13 from T0); item +4 adds the last step to the sectsch target; item 5 is scale insurance. The HTU-float rewrite +the audit proposed is NOT needed and would not help (refuted). diff --git a/dev/plans/2026-06-17-tnt-algorithm-audit.md b/dev/plans/2026-06-17-tnt-algorithm-audit.md new file mode 100644 index 000000000..6e48f5a27 --- /dev/null +++ b/dev/plans/2026-06-17-tnt-algorithm-audit.md @@ -0,0 +1,300 @@ +# TNT 1.6 "new technology" search vs TreeSearch cpp-search — line-level audit + +Date: 2026-06-17. Branch: `cpp-search`. READ-ONLY audit (no `src/` edits). +Goal: map TNT's xmult / sectsch / ratchet / drift / fuse to our exact code, +flag divergences, rank them by likelihood of explaining TNT's *escape* +advantage on EW-Fitch, and confirm each phase fires. + +Primary sources consulted (all load-bearing): +- TNT 1.6 console help: `help xmult/sectsch/ratchet/drift/tfuse/rseed/mult/bbreak` + -> `dev/benchmarks/tnt_help.txt` +- TNT 1.6 **default settings dump** (`sectsch:; xmult:; ratchet:; drift:; tfuse:;`) + -> `dev/benchmarks/tnt_defaults.txt` (THE authoritative default values) +- Goloboff 1999, *Analyzing Large Data Sets in Reasonable Times* (Cladistics + 15:415-428) — SS/RSS/CSS/MSS, tree-fusing, tree-drifting. Pages 417-422 + App 1. + Local PDF: `C:/Users/pjjg18/Zotero/storage/TETHI9A5/`. +- Nixon 1999 (ratchet) — referenced; algorithm is weight-perturbation + TBR. +- Prior reverse-engineering: memory `tnt-sectorial-recipe.md`, `sector-resolve-status.md`, + `untried-search-ideas.md`; `dev/expertise/tnt.md`. + +The EW-Fitch comparison is triggered by replacing `-` with `?` (no inapplicables), +so **all parsimony scoring is plain Fitch**. This fact is decisive (see §3). + +--- + +## TNT defaults (from `tnt_defaults.txt`) — the reference behaviour + +``` +Sectorial (sectsch): + * Separate matrix-buffer for sectors (xbuf ON) + * Random sector selections; min/max size 0 (=> size law min(n/2,45)/max(45,n/10)) + * Sectors of size BELOW 75 analyzed with 3 RAS+TBR + (+ EXTRA 3 starts if the first 3 produce score differences) <-- combstarts + * Global TBR every 10 substitutions (small AND large sectors) + * NOT accepting equally good subtrees <-- equals OFF by default + +xmult (Extra search settings): + * 4 replications as starting point for each hit + * Each replication autoconstrained (previous + wagner) + * Each replication: random sectorial searches, NO ratchet, + WITH drifting (5 iters), NO hybridization, fusing (1 round) <-- drift+fuse, NOT ratchet + * 1 hit; multiply trees by fusing after hitting best score + +ratchet: 50 iters, 40 subs, equal cycle yes, up/down weight 4 (NOT used by default xmult) +drift: 30 iters, 60 subs, AFD 1, RFD 0.20, reject factor 3.00 +tfuse: 5 rounds, start from best, accept-equal OFF, TBR-swap after fusion +``` + +Goloboff 1999 RSS algorithm (p.418-419), verbatim structure: +> (1) Select a sector at random so the reduced data set has S terminals. +> (2) Do **R replications of RAS + TBR** (saving a single tree) for the reduced +> data set. If the R replications produce trees of the **same length** (S was +> in a non-conflictive region) go to (3); otherwise do **r additional** reps. +> (3) **Choose the best among the R + r replications and the present resolution** +> for the sector and place it in the whole tree. +> (4) Do a round of **global swapping, but only if replacements at (3) have been +> made more than X times.** Go to (1), N times. +> +> "The best size seems to be 35 to 55 nodes. For that value of S, **R = 3 and r = 3**. +> A round of global swapping of the entire tree is made **every 5 to 10 replacements**, +> as that number makes it likely that (through clade substitution) the tree will +> have become **globally suboptimal under TBR**." + +Reduced dataset (p.418 + App 1): internal nodes represented by their **first-pass +state sets**; the basal node (HTU) by the first-pass set calculated upward. The +**HTU is an ordinary terminal** in the reduced RAS+TBR. (App 1 `marknodes` +accumulates a connected sector by clade-size until `<= sector_sz`.) + +--- + +## §1. MAPPING TABLE (TNT step | TNT behaviour | our code | match | divergence) + +| # | TNT step | TNT documented behaviour | Our function : line | Match | Divergence detail | +|---|----------|--------------------------|---------------------|-------|-------------------| +| 1 | Starting trees | `xmult`: 4 RAS replications per hit; addition seq = `ras` (random) | `driven_search` loop `ts_driven.cpp:728`; per-rep Wagner in `run_single_replicate:99-144` (`wagnerStarts` random-order Wagner) | partial | We use 1 replicate stream with `wagnerStarts` (1 sprint / 3 default+thorough) Wagner starts kept-best, then iterate `maxReplicates`. TNT does 4 *independent* RAS per hit then fuses them. We fuse across the replicate pool periodically (`fuseInterval`). Functionally similar; not identical. | +| 2 | Wagner addition | RAS = random addition sequence + greedy placement | `random_wagner_tree` (ts_wagner.cpp) | yes | Matches. (Biased Wagner is an extra TreeSearch option, off by default for EW < 120t.) | +| 3 | Initial hill-climb | RAS+TBR (or SPR then TBR) to local optimum | `run_single_replicate:165-173`: optional NNI/SPR warmup then `tbr_search` to convergence | yes | Matches; `nniFirst=TRUE` adds an NNI warmup TNT lacks but that is only a speed lever. | +| 4 | Sector ORDER in xmult | "css first, then rss, then xss last"; ratchet/drift always FOLLOW sectorial | `run_single_replicate:231-303` runs **XSS, then RSS, then CSS**; ratchet/drift after | **partial** | Order is reversed (we do XSS->RSS->CSS; TNT does CSS->RSS->XSS). Low impact, but note default xmult uses **RSS only** (css/xss off by default). | +| 5 | Sector SELECTION (RSS) | Random connected sector, S terminals, **size law min(n/2,45)**; reduced data set built from first-pass state sets | `rss_search:993-999` selects **eligible internal nodes whose clade size ∈ [min,max]** (a CLADE, not a constructed connected region); `build_reduced_dataset:273` | **partial** | We SELECT an existing clade in a size band ([6,50] default / [6,80] thorough). TNT CONSTRUCTS a connected sector of ~n/2 by accumulating clades (App 1 `marknodes`). Memory's band-shape probe found this lever **real but marginal-to-noise** end-to-end. | +| 6 | Reduced-dataset HTU | Internal nodes = first-pass state sets; basal HTU = first-pass set calculated upward; **HTU is an ordinary terminal** | `build_reduced_dataset:462-475` sets HTU tip-state = `compute_from_above_for_sector` (exact rest-of-tree first-pass set). For EW-Fitch this is **exact** (reduced length = full length − const). | **partial** | Scoring fidelity matches and is EXACT for EW (verified §3). BUT we anchor the HTU at the synthetic root and **freeze it there** — TNT lets it float as a terminal. This is divergence D1 (§2). | +| 7 | Within-sector search | **3 RAS+TBR** (+3 more if scores differ); accept best of R+r + present resolution | `search_sector:773-847`; start 0 = TBR on existing subtree, starts 1.. = `build_ras_sector` RAS rebuild | **partial/no** | Default `rasStarts=1` => **only the TBR-polish of the existing subtree, NO RAS rebuild**. ALL TreeSearch presets leave `rasStarts=1` (never overridden). TNT's default is 3+3. Divergence D3a. Even when `rasStarts=3`, the RAS rebuild keeps the HTU frozen (D1). | +| 8 | Sector ACCEPT | "if a better configuration is found, it is replaced on the whole tree" (best reduced score); full tree may become "globally suboptimal", cleaned by periodic global TBR | `search_sector:829-831` (keep best REDUCED score) THEN `rss_search:1069-1148`: reinsert, full-rescore, **revert unless full tree STRICTLY improves** | **partial** | Double gate: (a) reduced strict-best, (b) full-tree strict-improve revert. Goloboff accepts on the reduced score and tolerates transient full-tree suboptimality. **HOWEVER for EW-Fitch the from-above HTU makes reduced-improve ⟺ full-improve, so gate (b) is a NULL divergence here** (verified §3, 0 gate-bites). Bites only under inexact NA scoring. | +| 9 | Global TBR cadence | Round of global TBR **every 5-10 sector replacements** | `rss_search:1154-1165` runs **one** global TBR at the END of all `rss_picks` | partial | We TBR once per RSS round (and `rssRounds` loops the whole thing). Coarser cadence than TNT's every-5-10. Minor; the per-round outer loop approximates it. | +| 10 | accept-equal subtrees | `equals` OFF by default | `sectorAcceptEqual=FALSE` default; `search_sector` `accept_equal` param | yes | Matches TNT default. (Prior probes: accept_equal "wanders", undirected; not the lever.) | +| 11 | Drift (DFT) | xmult default: **5 drift iters per replication**; suboptimal accepted by RFD = (F−C)/F rejected when > Z = X/(F+J−C); AFD limit; alternate perturbed/unperturbed | `drift_search` (ts_drift.cpp:721); `drift_phase` AFD/RFD logic at :578-695 | yes | Algorithm faithfully implemented (AFD `driftAfdLimit`, RFD `driftRfdLimit`). BUT **all TreeSearch presets set `driftCycles=0`** — drift never runs by default. TNT's xmult default leans on drift. Divergence D3b. | +| 12 | Ratchet | NOT in default xmult; standalone: 50 iters, perturb (up/down weight 4%), short TBR, restore weights, full TBR | `ratchet_search` (ts_ratchet.cpp:136); perturb modes zero/upweight/mixed; initial TBR + cycles | yes | Algorithm matches Nixon 1999 / TNT ratchet. BUT TreeSearch makes ratchet the **primary** escape (12-20 cycles in every preset), the inverse of TNT's xmult default (ratchet off). This is *how TreeSearch approaches TNT* but is a different engine balance. | +| 13 | Tree-fusing | Exchange shared clades (>=5 taxa) between trees; accept improving (equal off by default); TBR after; 5 rounds | `tree_fuse` (ts_fuse.cpp:325): shared-split detection, `replace_subtree`, accept `< score`, TBR cleanup, `max_rounds` | yes | Faithful. `fuseAcceptEqual` off by default (matches). Pool-level fusing every `fuseInterval` reps + optional intra-rep fuse. Reroot-segfault fix on >64t noted in memory NOT yet in cpp-search (`fuse-reroot-segfault.md`) — orthogonal to the gap. | +| 14 | CSS | Consensus-based sector selection; sector-restricted TBR, exact scoring | `css_search` (ts_sector.cpp:1330): `xss_partition` + `tbr_search` with `sector_mask` | partial | We partition (XSS-style) rather than select from a consensus of conflict; the TBR is exact (no HTU). BUT the mask **freezes the sector's attachment** (`ts_tbr.cpp:807,910,1130`): clips and regrafts confined to the clade, so CSS-TBR cannot relocate the whole sector. Divergence D1 (CSS variant). | +| 15 | XSS | N exclusive non-overlapping sectors tiling the whole tree, R rounds, global TBR after each round | `xss_search` (ts_sector.cpp:1172) + `xss_partition:905` | yes | Partitioning + per-sector reduced-dataset rebuild matches. Same HTU-freeze (D1) as RSS since it uses `build_reduced_dataset`+`search_sector`. | +| 16 | Stop rule | `hits N` (default 1); consense stabilization options | `driven_search`: `targetHits`, `consensusStableReps`, `perturbStopFactor` | yes | Matches in spirit; TreeSearch adds time-budget + MPT enumeration tail. | + +--- + +## §2. THE KEY QUESTION — what lets TNT's sectorial cross barriers ours cannot + +Setup the analysis must respect: the memory's **shared-start probe is the clean +signal**. From an *identical* TNT `mult` T0, TNT's RSS sectorial improves +3..+11 +on the gap datasets (Zanol +11, Wortley +7, Zhu +4, Giles +3); **ours improves 0 +on all 6**, having examined up to 1.26M candidates. T0 is already OUR global-TBR +optimum. So the gap is a genuine SECTORIAL quality/escape gap, independent of +wall-clock, and independent of the starting tree. Every prior refutation +(`sector-resolve-status.md`, `tnt-sectorial-recipe.md`) varied the sector REBUILD +or SCORING while holding two things fixed: (i) the strict full-tree accept gate, +and (ii) the **frozen sector↔rest-of-tree attachment**. + +### Candidate (a) — RAS multi-start banking best topology. REFUTED (prior). +`build_ras_sector` + `search_sector(ras_starts)` implemented; shared-start gap +UNCHANGED, 10/12 cases `ts_sect == start` (`sectorial_shared_greedy.csv`). Rebuild +*alone* is null. + +### Candidate (b) — accept equal-length sector solutions. REFUTED (prior). +`accept_equal` walks the plateau but undirected; drifts AWAY from the target basin +(`sector-resolve-status.md` §2). Matches TNT default anyway (`equals` OFF). + +### Candidate (c) — reduced-dataset scoring is approximate where TNT's is exact. +REFUTED for EW, and now **verified empirically** (this audit, §3): under EW-Fitch +the from-above HTU gives reduced-improve ⟺ full-improve (0 gate-bites). Exact-CSS +probe also null (`sectorial_shared_css.csv`). So scoring fidelity is NOT the lever +for the EW case. (It *does* bite under native NA/Brazeau — see §3 — but that is not +the audited comparison.) + +### Candidate (d) — sector SELECTION (clade-band vs constructed region). REAL but SMALL. +Band-shape probe: real (band wins 30/40 rss-only, median −6/−7.5) but ratchet +SUBSUMES it; end-to-end collapses to noise, harms Giles/Zanol on some seeds. Not +the +0→+11 lever on its own. Secondary. + +### >>> THE LEADING, UNTESTED MECHANISM: frozen sector↔rest-of-tree attachment (D1) <<< + +Goloboff's reduced dataset treats the **HTU as an ordinary floating terminal**. His +R RAS+TBR replications therefore choose, *jointly and simultaneously*: + (i) the internal topology of the sector, AND + (ii) **which node of the sector is basal/adjacent to the rest of the tree** — + i.e. where the rest-of-tree (HTU) reattaches. +TBR on the reduced data set can put the HTU terminal anywhere, which corresponds +to **re-rooting the sector relative to the rest of the tree** and simultaneously +re-resolving it. This is a move that crosses an uphill barrier in the FULL tree +in a single accepted step, because the reduced score (exact for EW) drops even as +the global arrangement reorganises. + +TreeSearch forecloses this in **three independent places** — the HTU/attachment is +frozen so the search only ever explores *rebuild-with-fixed-attachment* or +*reroot-alone*, never their product: + +1. **`search_sector` root-structure revert** — `ts_sector.cpp:808-819`. + After the internal TBR, if the HTU and `sr_mapped` are no longer the two direct + children of the synthetic root, the move is **discarded** and the topology reverts + to the pre-TBR snapshot. Any TBR move that floats the HTU (the very move that + re-roots the sector against the rest of the tree) is thrown away. + +2. **`build_ras_sector` insertion restriction** — `ts_sector.cpp:716-748`. + The RAS rebuild seeds with HTU at `new_root`, content rooted at `sr_mapped`, and + restricts candidate edges to the subtree **below `sr_mapped`** ("never a root + edge ... so the HTU stays anchored at new_root"). The HTU is never an addable/ + movable terminal; the rebuild can only re-resolve the clade with the *same* node + kept basal. This is exactly why `build_ras_sector` reproduces the start 10/12. + +3. **CSS sector mask** — `ts_tbr.cpp:807` (`if (sector_mask && !mask[clip_node]) continue`), + `:910` and `:1130` (`if (sector_mask && !mask[below]) continue`). + The mask is the clade only. CSS-TBR can neither clip the `sector_root` edge nor + regraft outside the clade, so it cannot relocate the sector as a whole — the + exact attachment is frozen. + +Global TBR (`rss_search:1154-1165`, the outer-cycle TBR) DOES cover re-rooting the +sector relative to the rest of the tree — **but alone**, on a *fully-resolved* tree +that has already converged (=0 from T0). The sector rebuild covers re-resolution +**but alone**, with attachment frozen (=0). TNT's RAS+TBR-on-reduced-data does +**both at once** (rebuild × free-attachment). That joint move is the one neither +TreeSearch operator can reach, and it is the only structural lever the prior +sessions never isolated (`sector-resolve-status.md` itself flags "its RAS rebuild +holding/accepting equal trees during the rebuild; next probe = log sectors picked" +— the right instinct, wrong mechanism: it is attachment-freedom, not equal-holding). + +"Low root_revert frequency rules this out" is a non-argument: revert only counts +HTU-displacing moves the *polish-TBR proposed*; the RAS rebuild never proposes one +(it is forbidden by construction at :716), so the freeze is baked into construction, +not observable as revert frequency. + +--- + +## §3. EXACTNESS VERIFICATION (settles the accept-gate ranking) + +Discriminating trace (`dev/benchmarks/diag_accept_gate_trace.R`, using the +`TS_SECT_DEBUG=1` REprintf at `ts_sector.cpp:1081`): does a sector ever improve on +the reduced score (`red_best < red_cur`) while the full tree does NOT improve +(`full_new >= full_best`)? That is the only condition under which the strict +full-tree accept gate (item 8b) can foreclose a real sectorial improvement. + +EW-Fitch, Zanol2014, seed 1, rasStarts=3 (`dev/benchmarks/trace_fitch.txt`): +``` +sect[117] red_cur=820 red_best=816 full_new=1331 full_best=1335 STRICT +sect[ 95] red_cur=197 red_best=196 full_new=1330 full_best=1331 STRICT +sect[ 75] red_cur=445 red_best=441 full_new=1326 full_best=1330 STRICT +gate-bites (red improved, full did NOT): 0 +``` +**Every reduced improvement translated 1:1 to a full-tree improvement.** Confirms +the from-above HTU is EXACT for EW-Fitch, so the strict full-tree accept gate +(item 8b) is a **NULL divergence for the audited case**. (The trace only fires on +the accept branch, so it reports the improving sectors; the point is none of them +needed transient full-tree worsening.) Native NA (`-` kept) is where exactness +breaks and the gate can bite (the `WORSE-revert` branch at `rss_search:1143-1148` +exists precisely for that) — but EW-Fitch is the comparison in scope. + +=> The acceptance double-gate is **demoted**: it is the tidy-looking smoking gun +that does not fire for EW. The real lever is the attachment freeze (D1). + +--- + +## §4. PER-PHASE EXECUTION CONFIRMATION (does each phase fire?) + +Install: `.agent-audit` per-agent lib (NOT the shared default), built clean. +`bench_phase_yield.R` with `TS_LIB=.agent-audit`, Wortley2006 + Zanol2014, seeds +1-3, 20s, nThreads=1, `strategy="auto"` (Wortley 37t -> "default"; Zanol 74t -> +"thorough"). Phase columns = % of wall-clock. + +`dev/benchmarks/phase_yield_audit.csv`; medians over 3 seeds: + +| dataset | tips | preset | score_med | reps | late_frac | wagner | init_tbr | sector | ratchet | final_tbr | fuse | +|---------|------|--------|-----------|------|-----------|--------|----------|--------|---------|-----------|------| +| Wortley2006 | 37 | default | 481 | 216 | 0.81 | 6% | 2% | **7%** | **83%** | 2% | 0% | +| Zanol2014 | 74 | thorough | 1264 | 27 | 0.41 | 5% | 5% | **22%** | **66%** | 2% | 0% | + +ALL phases fire (wagner, init_tbr, sector, ratchet, final_tbr all > 0). The +distribution is the **mirror image of TNT**: +- TreeSearch: **ratchet-dominated** (83% Wortley / 66% Zanol), sectorial a thin + slice (7% / 22%), drift absent (presets set `driftCycles=0`), periodic fuse ~0%. +- TNT xmult default: **~67% sectorial**, drift(5)+fuse(1), ratchet **OFF**. + +So the phase that consumes TreeSearch's budget (ratchet) is the phase TNT does NOT +run by default, and the phase TNT leans on (sectorial, with floated-HTU RAS+TBR and +drift) is the under-weighted, structurally-limited slice in our presets (D1+D3). +`late_frac` 0.81 on Wortley means 81% of replicates ran AFTER the last improvement +— effort spent re-finding the same optimum, consistent with the 2x wall-clock gap. +Scores (Wortley 481 vs TNT best 479; Zanol 1264 vs TNT best 1262) reproduce the +documented +1..+3 EW gap, so this install behaves as the gap reports describe. + +--- + +## §5. TOP-3 DIVERGENCES (ranked by likelihood of explaining TNT's escape) + experiments + +### D1 (RANK 1) — Frozen sector↔rest-of-tree attachment; HTU never floats +- Files/lines: `ts_sector.cpp:808-819` (root-structure revert), `ts_sector.cpp:716-748` + (RAS insertion restricted below `sr_mapped`), `ts_tbr.cpp:807/910/1130` (CSS mask). +- Hypothesis (falsifiable): TNT's per-sector RAS+TBR treats the HTU as an ordinary + terminal and so jointly re-resolves the sector AND re-roots it against the rest + of the tree in one accepted, reduced-score-improving step. TreeSearch can do + rebuild-alone (=0 from shared T0) or reroot-alone-via-global-TBR (=0), never the + product. Freeing the HTU will turn the shared-start result from +0 to a + substantial fraction of TNT's +3..+11. +- Experiment: In `build_ras_sector`, make the HTU a normal addable terminal (allow + insertions on ALL reduced-tree edges including the synthetic-root edge), and in + `search_sector` drop the `root_ok` revert (or, equivalently, define reinsertion + by *whichever sector node ends up adjacent to the HTU terminal* and reattach the + rest-of-tree there). Re-run `bench_sectorial_shared.R` from the identical TNT T0. + PREDICT shared-start 0 -> +N on Zanol/Wortley. If still 0, demote and proceed to D2. + (Cheaper precursor: log, per accepted sector, whether the node adjacent to the HTU + in the rebuilt sector differs from the original basal node — if it is ALWAYS the + same, attachment is provably frozen.) + +### D2 (RANK 2) — `rasStarts=1` in every preset (vs TNT 3 + 3-on-disagreement) +- Files/lines: `R/MaximizeParsimony.R:106-216` (no preset sets `rasStarts`; stays + `1L` from `R/SearchControl.R:290`). Engages `search_sector` start-0-only path. +- Hypothesis: even rebuild-alone is null from a converged T0, but rebuild is a + *precondition* for D1 — you cannot exploit a floated HTU without re-resolving the + sector. With the HTU frozen, `rasStarts=3` is null (already shown). With the HTU + floated (D1), `rasStarts>=3` becomes necessary to realise the joint move, and the + "+3 extra starts on score disagreement" (`combstarts`) matters. So D2 is *coupled* + to D1: the only meaningful test of D2 is rasStarts>=3 *with* a floating HTU. +- Experiment: factorial on `bench_sectorial_shared.R` — {HTU frozen, HTU floated} × + {rasStarts 1, 3, 6}. Expect improvement only in the (floated, >=3) cell. + +### D3 (RANK 3) — Engine balance: ratchet-primary vs TNT's sectorial+drift+fuse +- Files/lines: every preset sets `driftCycles=0L` + `ratchetCycles` 12-20 + (`R/MaximizeParsimony.R:107-216`); TNT xmult default = RSS sectorial + drift(5) + + fuse(1), ratchet OFF (`tnt_defaults.txt`). Plus global-TBR cadence: `rss_search:1154` + runs one TBR at end vs TNT every-5-10 replacements. +- Hypothesis: TreeSearch reaches near-TNT quality by substituting an aggressive + ratchet for the sectorial escape it cannot perform (D1). This explains why + ratchet-off TreeSearch trails by +4..+8 (memory) and why ratchet is "necessary + but never erases the gap" — ratchet is a *workaround* for the missing sectorial + move, not an equal. Closing D1 should let drift+fuse (cheaper) replace some + ratchet load, attacking the 2x wall-clock gap simultaneously. +- Experiment: after D1 lands, A/B a TNT-faithful preset (rss sectorial w/ floated + HTU + rasStarts=3, drift=5, ratchet=0, fuse every 2) vs current default at matched + wall-clock on Wortley/Zanol/Zhu/Giles. Predict equal-or-better quality at lower + time if D1 is the true lever. + +--- + +## §6. Caveats / notes for the orchestrator +- `src/` is READ-ONLY here; all line numbers are against the working tree at audit time. +- The `TS_SECT_DEBUG` trace only prints on the sector *accept* branch + (`rss_search:1081` is inside `if (accept && sector_best <= sector_current)`), so it + reports improving sectors, not rejected ones. For a full gate-bite census, the + orchestrator may want a trace on the reject/`WORSE-revert` path too — but the EW + exactness argument (reduced=full−const) makes a bite impossible for EW regardless. +- `candidates_evaluated` is NOT a clean cumulative counter (goes negative across + sector rounds, per memory) — use *score*, not candidate deltas, as the signal. +- Reduced-dataset alloc churn (~19% VTune, memory) and `xbuf` reuse are a wall-clock + lever (TNT reuses a buffer; we rebuild per sector) — orthogonal to the escape gap. +- TNT run-script filenames must be multi-char alphabetic (`helpdump.run`), else TNT + parses the basename as a command. diff --git a/dev/plans/2026-06-18-beam-sectorial.md b/dev/plans/2026-06-18-beam-sectorial.md new file mode 100644 index 000000000..39619da7f --- /dev/null +++ b/dev/plans/2026-06-18-beam-sectorial.md @@ -0,0 +1,271 @@ +# Beam sectorial: pool-aware RSS over a diverse, suboptimal-tolerant buffer + +Date 2026-06-18. Worktree `C:/Users/pjjg18/GitHub/TS-selectem`, branch +`claude/selectem-diversity` (off `cpp-search`). NOT on cpp-search. Env-gated; +default path byte-identical when the flag is unset. + +## The convergent diagnosis (why single-tree sectorial plateaus) +Three independent results all land on the same mechanism: + +1. **Chip (TNT side):** TNT's `sectsch=rss` escape = sectorial run over a RETAINED + diverse SET of equal-optimal trees (shared `hold` buffer). Single-tree strict + sectorial plateaus at ~1267 forever; effort/budget cannot substitute. +2. **Budget-matched TS run (this branch):** our own single-tree sectorial, given + TNT-like budget (20 picks x 30 rounds), reaches only ~1265-1267 (coll30 -4 on + Zanol). Effort is not the lever. +3. **Diverse-starts test (`test_diverse_starts.R`, run bve79389o):** single-tree + sectorial from EACH of TNT's 10 diverse 1271 trees, 30 independent lanes: + **best 1266, 0/30 reach 1261.** Diverse *starts* alone do NOT escape. + +=> The lever is neither effort, nor sector geometry, nor diverse starting points. +It is a **shared, evolving buffer**: improvements found on one tree must become +visible as starting points for later picks, AND the buffer must retain +topologically-diverse trees (including SUBOPTIMAL ones) so a sector re-solve can +reach an arrangement no single frozen tree exposes. + +## The buffer-width subtlety (resolves the hold-1000-vs-10 question) +`hold 1000` is the buffer CAPACITY (explicitly set, not TNT's default); the "10 +trees" is the count `mult=replic 1` deposits. The decisive point: with cap 1000 +and only ~10-50 trees ever present, TNT NEVER purges -> the buffer accumulates +ALL distinct trees found across the whole length range (chip: "lengths 1261-1271 +coexist at the plateau"). That suboptimal diversity fuels cross-topology sector +recombination. + +Our `TreePool` defaults to `suboptimal = 0.0`: `evict()` purges everything worse +than best the instant best improves. Used as-is for a beam it collapses to +best-equal-only and discards exactly the suboptimal diversity that drives escape. +**A faithful beam needs a WIDE buffer: large `suboptimal` (or hold a length band) ++ large `max_size`, retaining diverse trees over a range of lengths.** + +## Architecture (current) +- `driven_search(TreePool& pool, ...)` runs `max_replicates` reps; each rep builds + a fresh start, runs `run_single_replicate` (single-tree sectorial/ratchet/...), + then `pool.add_collapsed(result)`. +- `rss_search(TreeState& tree, ...)` mutates ONE tree; pool only supplies + `split_freq` weighting. Sectorial never reads/writes the pool mid-search. +- `TreePool`: best-equal retention (`suboptimal=0`), collapsed-topology dedup, + diversity-aware eviction when full, `best()`, `all()`, `add_collapsed()`. + +## Proposed design: `beam_sectorial(TreePool& beam, DataSet&, params, cd)` +A new pool-aware sectorial driver, env-gated (`TS_BEAM`), called from the RSS +phase when enabled. Loop: +``` +seed beam with the working tree (+ optional K diversification walks) +for round in 1..rss_rounds: + T = pick_from_beam(beam) # weighted toward better score; random among ties + Tcopy = T + rss_search(Tcopy, ds, sp, cd) # one sector pass, ras_starts re-solves + beam.add_collapsed(Tcopy, score) # WIDE buffer: keeps diverse + suboptimal +return beam.best() +``` +Beam buffer = a `TreePool` constructed with large `suboptimal` (e.g. +N steps, or +a tuned band) and large `max_size`, so it behaves like TNT `hold 1000`. + +### CORRECTED design (advisor gate, 2026-06-18) +The advisor caught a real overreach: the "wide buffer" (Claim B) rests ONLY on the +chip's secondhand "1261-1271 coexist" — the same chip caught conflating capacity +with count — and the probe that would confirm it hung and never ran. The diverse- +starts test (0/30) discriminates write-back vs no-write-back; it does NOT +discriminate best-equal (Claim A) from wide (Claim B), since both have write-back. +"1261-1271 coexist" most likely = lazy-eviction ballast (hold 1000 never hit, so +intermediate trees linger), NOT evidence they are picked from. + +**Ship Claim A first:** best-equal beam, `TreePool` at its default `suboptimal=0` +(its diversity-aware eviction already keeps the spread; do NOT write a new buffer +class). Minimal faithful beam: +1. Seed the beam from the working tree. +2. Each round: pick from the best-equal set (uniform among ties), copy, ONE sector + re-solve, `add_collapsed` back. +3. **`accept_equal` ON in the sector re-solve.** This is the diversity engine: + from a single T0 seed with accept_equal=false, the re-solve returns the tree + unchanged when no strict improvement -> `Tcopy == T` -> add_collapsed sees a + duplicate -> diversity never grows -> beam degenerates to single-tree. Accepting + equal-length rearrangements and writing the DISTINCT ones back is what makes a + beam exist at all. This also explains why accept_equal HURT before (1271): on a + single tree it is a directionless random walk; inside a retained buffer it is + the diversity engine. **Beam + accept_equal together is the actual test.** + +**`suboptimal` is a default-0 KNOB** (already a `TreePool` ctor arg). suboptimal=0 +(A) vs large (B) becomes a one-line experiment AFTER the beam exists -> **drop +probe_hold entirely**; the knob answers buffer-width in-system, faithfully. + +**Budget-match:** total sector searches = rounds x picks_per_rss = ~600 (== the +coll30_20 single-tree baseline: 20 picks x 30 rounds). Count it explicitly or any +win is the budget confound, not the architecture. + +**Integration:** new self-contained `beam_sectorial` with a LOCAL `TreePool`, +invoked in `run_single_replicate`'s RSS phase under `TS_BEAM`. Keeps default path +byte-identical; works under the harness's `maxReplicates=1, tree=T0` (self-seeds +from T0). Do NOT graft any fuse step into the beam loop (74-78 tips = the +[[fuse-reroot-segfault]] >64-tip zone; beam path doesn't fuse, so we are clear). + +**Decision rule:** best-equal beam reaches 1261 -> done, skip the riskier buffer. +Stalls -> THEN widen `suboptimal`, and we will KNOW width is the lever, not guess. + +## Target (define_target.R, ratchet-off TNT mult+sectsch, canonical hold-1000 T0) +| dataset | n | T0 | TNT target | gap | +|---|---|---|---|---| +| Zanol2014 | 74 | 1271 | 1261 | -10 | +| Wortley2006 | 37 | 485 | 480 | -5 | +| Zhu2013 | 75 | 631 | 624 | -7 | +| Giles2015 | 78 | 672 | 670 | -2 | +Single-tree TS baseline from same T0: ~0 (coll30 -4 Zanol/Zhu only). Beam must +beat this to justify the architecture change. + +## RESULTS (bench_beam.R, canonical T0, budget-matched 30 rounds x 20 picks, seeds 1-3) +| configuration | Zanol (tgt 1261) | Zhu (tgt 624) | +|---|---|---| +| single-tree (baseline) | 1267 (-4) | 627 (-4) | +| beam, best-equal (Claim A) | 1266 (-5) | 627 (-4) | +| beam, wide subopt=10 + pick-all (Claim B) | 1266 (-5) | 627 (-4) | +| independent lanes, 10 diverse seeds, NO sharing (prior) | 1266 | - | +| **TNT: shared buffer over 10 diverse seeds** | **1261** | **624** | + +**Both Claim A and Claim B plateau at 1266/627 == the single-tree/diverse-starts +floor.** Buffer WIDTH is moot here, and the reason is structural: + +### Why single-seed beam is seed-starved (the missing ingredient) +`rss_search` NEVER returns a tree worse than its input (it reverts any sector move +that worsens the full score, and its final global TBR only improves). So in a +beam seeded from ONE T0, the only suboptimal trajectory is the T0 seed itself — +there is no source of genuinely diverse suboptimal trees for a wide buffer to hold +or pick from. Widening (Claim B) therefore changes nothing. + +TNT's `mult` supplies ~10 DIVERSE 1271 seeds -> 10 independent descent +trajectories pooled in the shared buffer. The diverse-starts test already showed +those 10 seeds WITHOUT sharing = 1266. The one untested cell is **diverse seeds +WITH shared-buffer write-back** — exactly TNT's recipe. That, not buffer width, is +the next lever. + +## NEXT: multi-seed beam (advisor fork) +Beam must seed from K diverse trees, not one. Implementation options under +consideration (advisor gate before more C++): +- (a) beam generates K seeds internally via RAS+TBR (faithful TNT `mult`); env + TS_BEAM_SEEDS=K. Self-contained; abandons the fixed-T0 comparison (TNT's sectsch + also doesn't start from a fixed T0). +- (b) seed beam from the SAME 10 TNT diverse trees (via plumbing a multiPhylo / + file) — cleanest apples-to-apples vs the diverse-starts 1266, isolates "sharing" + as the sole added variable. +- Budget accounting changes with K seeds (K extra TBR searches); primary question + first ("does multi-seed beam reach 1261 at all"), wall-clock fairness second. + +## MULTI-SEED RESULT — beam architecture RULED OUT as the gap-closer +First multi-seed attempt had a BUG: seeds were generated by random-addition Wagner ++ one TBR pass, which lands ~20-90 steps WORSE than T0 (1291-1358 on Zanol) — +outside the basin — so `add_collapsed` rejected all of them (subopt=10 threshold +1281). The "beamMulti = 1266" was a silent single-seed run. Fixed: seed by +plateau-collecting TBR from T0 (`accept_equal` + `collect_pool`), which gathers +distinct T0-basin (1271) trees directly into the beam. + +After the fix, the beam genuinely seeds 3-4 distinct 1271 trees. Result on Zanol +(seed 2): **1267** — NO better than single-seed. Full picture: +| configuration | Zanol | Zhu | +|---|---|---| +| single-tree | 1267 | 627 | +| beam best-equal, single-seed | 1266 | 627 | +| beam wide, single-seed | 1266 | 627 | +| beam multi-seed (real diverse) + wide + pick-all | 1266-1267 | 627 | +| **TNT target** | **1261** | **624** | + +**Every faithful beam variant plateaus at ~1266/627 — the single-tree + diverse- +starts floor.** What this PROVES: the shared buffer is NOT SUFFICIENT — the chip's +thesis (dev/plans/2026-06-18-tnt-sectsch-superpower.md) that the buffer is the +gap-closer is REFUTED. What this does NOT prove: that the buffer is useless. Every +beam variant calls `rss_search`, which uses the FROZEN-HTU sector re-solve — the +same one single-tree uses. So "beam plateaus where single-tree does" is exactly +what a frozen-HTU bottleneck would produce, whether or not the buffer is useful. +The experiment cannot discriminate "beam useless" from "beam capped by the +re-solve." Defensible claim: **beam-on-frozen-HTU = the floor; the re-solve is the +binding constraint.** Keep the beam behind its flag for a beam+HTU re-test once +the re-solve can float (below). + +## REDIRECT: sector re-solve QUALITY (HTU floating, task #24 / D1) +The ~1266 floor sits exactly +5 above target. My own prior audit +([[sector-resolve-status]], dev/plans/2026-06-17-tnt-algorithm-audit.md, task #24) +pinned a CONFIRMED per-sector quality gap: TNT FLOATS the HTU pseudo-tip during +the sector re-solve (joint re-resolve x re-attach = a barrier crossing); we FREEZE +it. A frozen-HTU re-solve structurally cannot produce the arrangements TNT's can, +so NO buffer/beam machinery closes the gap — the moves aren't reachable. There is +already a scoring-only probe (`TS_FREE_HTU_PROBE`, ts_sector.cpp ~L978) confirming +a free-HTU re-solve finds lower reduced scores. The lever is implementing the +free-HTU re-solve + reattach, not buffer architecture. + +## GATE before building free-HTU reattach (advisor) +Do NOT start the hard D1 reattach on "+5 floor ~ HTU." HTU is "+1/+3 PER SECTOR"; +whether that accumulates to the +5 plateau gap AT THE PLATEAU is unverified. Cheap +check first: run a 1266-RESIDENT tree through rss_search with TS_FREE_HTU_PROBE on, +count `< lever is live where we're stuck; build the reattach. +- Rarely fires at 1266 -> reduced-score headroom gone at plateau; float-HTU won't + help either; floor is something else -> saved days. +Caveat: lower reduced score is necessary not sufficient — the reattach must +REALIZE it on the full tree. Positive probe => "build + verify full-tree drop." + +## After HTU floats: re-test beam+HTU (do not assume) +Once the re-solve can float, run single+HTU vs beam+HTU. THAT discriminates +whether the buffer was ever load-bearing. single+HTU hits 1261 -> beam was a dead +end, delete cleanly. single+HTU stalls but beam+HTU breaks through -> buffer was +necessary, would have wrongly killed it. + +## Separate flag (log, do NOT fold into HTU work) +TS's RAS+TBR-from-random lands 1291-1358 on Zanol = 20-90 steps above T0 (1271). +TS's per-replicate TBR descent is materially WEAKER than TNT's `mult`. Controlled- +for here (fixed T0) but bears on PRODUCTION where TS builds its own starts. Needs +its own investigation. + +## HTU-FLOAT GATE RESULT — float-HTU also RULED OUT for the plateau (Zanol) +Ran TS_FREE_HTU_PROBE on T0 (1271) vs a plateau tree (1267), 50 sector probes each, +[31,99] coll30 sectors (S=30), 20 free RAS+TBR restarts/sector. +- **T0 (1271):** exactly ONE sector (110) fires `<= anchored (cold-search weakness — free RAS+TBR from + random underperforms the anchored search seeded from the existing good subtree). +- **Plateau (1267):** sector 110 now anchored = free = 529 — the -4 headroom is + GONE (the anchored sectorial captured it during the 1271->1267 descent). **ZERO + D1-CONFIRM at the plateau.** +Gate verdict (advisor): detectable free-HTU headroom is EXHAUSTED at the plateau -> +**floating the HTU will NOT break the 1267 floor.** The probe's free search is weak, +but equally so at T0 and plateau, and it DID detect the T0 sector-110 headroom, so +the relative signal (fires at T0, silent at plateau) is trustworthy. This saves the +hard D1 reattach build. NB the necessary-not-sufficient caveat cuts the other way +too: a weak cold search could under-detect, but the T0-vs-plateau contrast holds. + +So BOTH the beam (buffer architecture) AND HTU-floating are ruled out as the +1267->1261 lever for Zanol. The floor is something else. + +## NEW LEAD: core TBR/Wagner hill-climbing quality deficit +Surfaced incidentally: a single TS random-addition Wagner + one TBR pass lands at +**1291-1358 on Zanol = 20-90 steps above T0 (1271)**, whereas TNT's `mult=replic 1` +reaches 1271 (and deposits ~10 trees there). That is a 1.5-7% deficit in BASIC TBR +hill-climbing. If TS's core TBR descent is materially weaker than TNT's, EVERY +component (starts, sector re-solve, polish) inherits it, and no sectorial +architecture change closes the gap. This is the strongest remaining lead and is a +PRODUCTION concern (TS builds its own starts). Verify it's a real deficit, not a +measurement artifact (poor random_wagner start + too-few TBR restarts vs TNT's RAS). + +VERIFIED (2026-06-18, /tmp/tbr_check.R): TS pure Wagner+TBR multistart, ratchet/ +sectorial OFF, on Zanol: +| effort | TS best | +|---|---| +| 1 replicate | 1315 | +| 5 replicates | 1306 | +| 20 replicates | **1287** | +| TNT mult=replic 1 (ONE replicate) | **1271** | +**20 TS replicates (1287) can't match 1 TNT replicate (1271)** — +16 above T0, ++26 above target. NOT single-pass weakness recovered by more starts; a fundamental +core hill-climbing deficit. In production TS is doubly disadvantaged (worse starts +AND worse plateau-escape). The canonical shared-start comparison (both from 1271) +factored the start deficit OUT — which is why the EW work focused on sectorial; but +the start deficit is real and large on its own. CAVEAT: TBR-only here; confirm TNT +`mult` does not swap beyond a single TBR pass (apples-to-apples) — though 20-vs-1 +magnitude makes a pure artifact unlikely. Next: profile/compare TS tbr_search +thoroughness (clip order, max_hits, convergence) vs TNT branch-swapping. + +## Status / session conclusion +Beam built + wired behind TS_BEAM (knobs TS_BEAM_SUBOPT/PICKALL/MAXSIZE/SEEDS/ +DEBUG); default path byte-identical. Findings, in order of confidence: +1. Beam (shared buffer over diverse seeds) does NOT close the Zanol/Zhu gap -> + chip's "buffer is the lever" thesis REFUTED (not sufficient). Keep behind flag. +2. Float-HTU GATED OUT for the plateau (probe headroom exhausted at 1267). +3. NEW LEAD: core TBR/Wagner quality deficit (1291-1358 vs 1271) — investigate next. +All in worktree TS-selectem; nothing on cpp-search. Reported pivot to user. diff --git a/dev/plans/2026-06-18-freeze-big-sector.md b/dev/plans/2026-06-18-freeze-big-sector.md new file mode 100644 index 000000000..ce1f25bd2 --- /dev/null +++ b/dev/plans/2026-06-18-freeze-big-sector.md @@ -0,0 +1,91 @@ +# Freeze-big sector reduction breaks the ratchet-off null (TNT selectem mechanism) + +Date 2026-06-18. Branch `claude/selectem-diversity` (worktree `C:/Users/pjjg18/GitHub/TS-selectem`, +off `cpp-search`). Env-gated, default path byte-identical. NOT yet on cpp-search. + +## Question +Can TreeSearch's RSS sectorial, with **global ratchet/drift OFF**, escape the canonical +frozen T0 the way TNT does? (TNT reaches its scores with ratchet OFF — so matching this is a +**wall-clock** lever, not only quality. See [[tnt-sectorial-recipe]].) + +## Target (define_target.R; ratchet-off TNT `mult`+`sectsch=rss`, canonical hold-1000 T0) +| dataset | n | T0 | TNT ratchet-off sectorial | escape | +|---|---|---|---|---| +| Zanol2014 | 74 | 1271 | 1261 | −10 | +| Wortley2006| 37 | 485 | 480 | −5 | +| Zhu2013 | 75 | 631 | 624 | −7 | +| Giles2015 | 78 | 672 | 670 | −2 | +TreeSearch baseline from the SAME T0 (ts_arms.R, base/coll30): **~0** (Zhu −2 only). Stuck. + +## Mechanism found: freeze-big sector reduction +The existing collapse (`build_reduced_dataset_collapsed`) is **break-big**: expands the largest +sub-clade until `target_tips` units → surviving composites are SMALL leftovers → no large +movable units → null (coll30). TNT `selectem` (Goloboff 1999 App.1) is **freeze-big**: keep +tips individual, FREEZE whole sub-clades (≥0.8·cap) into single composite terminals (random +order) until ≤cap units. Relocating one composite = transplanting a multi-taxon clade as a +**unit** — a large-radius move single-step hill-climbing cannot reach by moving fragments one +at a time. Implemented `build_reduced_dataset_freeze` (env `TS_FREEZE_COLLAPSE`; cap/thresh = +`TS_FREEZE_CAP`/`TS_FREEZE_THRESH` knobs; `TS_FREEZE_RANDOM` = random vs deterministic order; +shares `assemble_reduced` with break-big, refactor verified byte-identical). + +Critical tuning: a USEFUL reduction (many tips + ONE big composite) needs a HIGH freeze +threshold (only freeze near-cap sub-clades, so a freeze *overshoots* cap). Low threshold → +degenerate (one near-whole composite, e.g. clade=74→units=2 maxcomp=73). cap=33 thr=28 worked +for n≈74; must scale with n. + +## Result (ts_arms.R, canonical T0, ratchet/drift OFF, rss-only, 30 rounds, 20 picks, seeds 1-3) +| dataset | target | base/coll30 | freezeHT**det** (H2) | freezeHT**rand** (H1) | +|---|---|---|---|---| +| Zanol | −10 | 0 / 0 | **−4** | **−5** | +| Zhu | −7 | 0 / −2 | **−2** | **−3** | +(cap33 thr28; Wortley/Giles need n-scaled cap — see freezeScaled run.) + +## H1 vs H2 — ANSWERED +Deterministic high-threshold freeze ALREADY breaks the null (Zanol −4, Zhu −2). Randomisation +adds ~+1 step (−5, −3). So per the pre-registered ablation: **H2 (large movable units) is the +PRIMARY lever; per-pass diversity (H1) is a secondary ~+1 increment.** The user's "per-pass +diversity" intuition is real but not the main thing — the missing ingredient was the structural +move type (frozen-clade-as-unit), available even deterministically. + +## n-scaled run (negative field = pct of n; cap .45n thr .38n band [.42n,.99n]) +| dataset | target | freezeScalDet | freezeScaled(rand) | +|---|---|---|---| +| Zanol | −10 | −4 | −5 | +| Zhu | −7 | 0 | −1 | +| Wortley | −5 | 0 | 0 | +| Giles | −2 | 0 | 0 | +n-scaling did NOT unlock Wortley/Giles and made Zhu noisier (signal is 1-5 steps, seed-sensitive). + +## BUDGET CONFOUND — freeze framing COLLAPSES (advisor-caught) +The "freeze breaks the null" claim was confounded: the null (base/coll30) ran at default +~5 picks x 15 rounds (~75 sector searches); the freeze arms ran 20 picks x 30 rounds (~600). +8x more search. Decisive budget-matched run (ALL at 20 picks x 30 rounds, seeds 1-3): +| dataset | target | base20 (small [6,50]) | coll30_20 (break-big) | freezeHT (freeze-big) | +|---|---|---|---|---| +| Zanol | −10 | 0 | **−4** | −5 | +| Zhu | −7 | 0 | **−4** | −3 | +| Wortley | −5 | 0 | 0 | 0 | +| Giles | −2 | 0 | 0 | 0 | +**coll30 (plain break-big) reaches −4 at matched budget == freeze (±1, freeze WORSE on Zhu).** +So freeze-big / "large movable units" (H2) and "per-pass diversity" (H1) are NOT the lever — +they add nothing over the pre-existing break-big collapse. `build_reduced_dataset_freeze` adds +no value; KEEP IT OUT of production. + +## What ACTUALLY breaks the ratchet-off null (corrected) +- `base20` (small-clade [6,50] selection) finds 0 even at 600 searches → small-clade sectorial + is the dead end, regardless of budget. +- `coll30_20` (LARGE-clade [31,99] selection + collapse + ras3 + 600 searches) finds −4 on + Zanol AND Zhu → **large-clade selection + sufficient budget** is the partial lever (~half the + gap). The earlier coll30 "null" (−2 Zhu) was itself a LOW-BUDGET artifact. +- This also implies the memory's "RAS-multistart on large sectors = null" was likely a budget + artifact (it was at default low budget). [[tnt-sectorial-recipe]] selection-quality verdicts + need re-reading through the budget lens. +- Still 0 on Wortley/Giles even at high budget — genuinely unresponsive from this T0. +- (isolation run b42fxd23o: does large-selection ALONE escape, or need collapse/ras3? — fill in) + +## Clean surviving finding +The H1/H2 ablation (freezeHTdet −4 vs rand −5, both 30x20) is internally clean BUT moot now +that coll30 (deterministic break-big) also = −4: freeze machinery is within noise of break-big. +The honest result: **TreeSearch's existing large-clade collapse sectorial DOES partially escape +the frozen T0 (~half the Zanol/Zhu gap) once given TNT-like budget (≈20 picks/round, ≈30 +rounds) — but only on large-clade datasets, and the remaining gap + Wortley/Giles are open.** diff --git a/dev/plans/2026-06-18-tbr-shared-start.md b/dev/plans/2026-06-18-tbr-shared-start.md new file mode 100644 index 000000000..874d0293b --- /dev/null +++ b/dev/plans/2026-06-18-tbr-shared-start.md @@ -0,0 +1,349 @@ +# Isolated-TBR head-to-head: TreeSearch vs TNT 1.6 from identical start trees + +**Date:** 2026-06-18 +**Branch:** `claude/competent-chaum-6ecb56` (worktree off `cpp-search`) +**Question (project lead):** Given the *same* starting tree, how does the score +change after TBR branch-swapping? The ensemble behaviour *should* be identical in +TNT and TreeSearch. Step 1 — is there a meaningful difference? Step 2 (only if +yes) — how does TNT implement TBR, and what explains the difference? + +This investigation isolates **TBR branch-swapping** from the Wagner +starting-tree confounder by feeding the **identical Newick start tree** into both +engines and running TBR to convergence. (The Wagner half is a separate task.) + +## TL;DR + +- **Step 1 = YES, and the difference is large.** From an *identical* poor start + (e.g. a 1478-step Wagner tree on Zanol2014), TNT's TBR reaches ~1265 while + TreeSearch's TBR reaches ~1300–1350. A 40–90 step gap in the hill-climb alone, + with the starting tree held fixed. +- **Step 2 = TreeSearch's TBR neighbourhood is root-dependent.** `tbr_search` + declares convergence at trees that are *not* unrooted-TBR local optima — TNT + improves them, and re-rooting the same tree and re-running TS improves them too. + Proven three independent ways (cross-feed, root-dependence test, code). The + leading interpretation is that TS implements **rooted** TBR (a subset of TNT's + unrooted TBR), with the fixed root blocking root-crossing rearrangements. +- **Quantified:** making TS root-invariant recovers **~half** the gap (a large, + real effect) but leaves a **+15–36 residual** to TNT. So the root-dependent + neighbourhood is a **proven major contributor**, not yet shown to be the whole + cause. This still **redirects the long-standing EW-Fitch gap** away from + "sectorial architecture" toward the TBR move set itself — a concrete, at-least- + half-the-gap, fixable kernel deficiency. + +## Method & comparability controls + +- Datasets are EW-Fitch-converted (inapplicable tokens → `?`), so **both engines + optimise the identical Fitch objective**; `TreeLength` (TS) and TNT `length` + are directly comparable. Verified: T0 round-trips at 1271 in both. +- **TS entry point:** `TreeSearch:::ts_tbr_diagnostics(edge, ...)` — runs TBR to + convergence from a warm-start edge matrix, returns final score + per-pass + trajectory. `acceptEqual=FALSE, maxHits=1` = strict descent to first local + optimum; `acceptEqual=TRUE` = single-tree plateau-walk. +- **TNT entry point:** `bbreak = tbr [no]randclip [no]mulpars;` with `tread` of + the shared start tree and `rseed N`. `bbreak` swaps the *in-memory* tree — it + does **not** re-randomise (verified: bbreak from T0=1271 stays 1271). +- **Two modes:** + - **Mode A — strict single tree:** TS `acceptEqual=F`; TNT `nomulpars hold 1`. + - **Mode B — buffer / plateau:** TS `acceptEqual=T`; TNT `mulpars hold 1000`. + *Asymmetric by construction* (TNT swaps a buffer of equal-length trees; TS + `tbr_search` walks a single tree — no buffer re-swap). Mode B is a mechanism + probe, not a controlled comparison. The headline is Mode A. +- For robustness, TNT's swapped trees are saved (`tsave`) and **re-scored in R + with `TreeLength`** — the final length is never parsed from TNT's stdout. +- Scripts: `dev/benchmarks/tbr_shared_start_lib.R` (helpers), + `tbr_pilot.R`, `tbr_verify.R`, `tbr_crossfeed.R`, `tbr_grid.R`, + `tbr_reroot_recovery.R`. Raw results: `dev/benchmarks/tbr_results/`. + +## Pre-flight gates (all passed) + +1. **Length identity:** TS tree → Newick → TNT `tread` → `length` = 1271 = + `TreeLength`. Objective identical; round-trip faithful. +2. **Both engines seed-stochastic:** TNT `randclip`+`rseed` varies the trajectory + (norandclip is deterministic = 1273 on the 1478 start); TS RANDOM clip order + seeded by `set.seed`. So the "ensemble across seeds" framing is valid. +3. **TS converges genuinely:** strict descent from 1478 = 51 productive passes → + first local optimum (not a truncated run). `bbreak` from T0 holds at 1271 in + both engines. + +## Step 1 — ensemble result (the deliverable table) + +Final length over **6 seeds** per (start tree × engine), from the IDENTICAL +shared start. Six start trees per dataset spanning a quality ladder (two random +topologies, two RAS Wagner, one partially-TBR-optimised, one near-optimal +anchor). `gap` = median(TS) − median(TNT). Raw rows: +`dev/benchmarks/tbr_results/tbr_grid_raw.csv`; shared starts: +`_starts.nwk`. + +### Zanol2014 (n=74), Mode A — strict single-tree TBR + +| start | start len | TNT (min/med/max) | TreeSearch (min/med/max) | gap | +|---|---|---|---|---| +| random1 | 2353 | 1264 / 1267 / 1275 | 1295 / 1318 / 1335 | **+51** | +| random2 | 2274 | 1267 / 1268 / 1272 | 1302 / 1327 / 1336 | **+59** | +| wagner1 | 1711 | 1265 / 1267 / 1270 | 1293 / 1306 / 1327 | **+40** | +| wagner2 | 1584 | 1263 / 1266 / 1274 | 1297 / 1300 / 1312 | **+34** | +| partial | 1516 | 1262 / 1265 / 1271 | 1289 / 1296 / 1316 | **+30** | +| t0anchor | 1271 | 1271 / 1271 / 1271 | 1271 / 1271 / 1271 | +0 | + +### Zanol2014 (n=74), Mode B — buffer (TNT mulpars hold 1000) / TS plateau + +| start | start len | TNT (min/med/max) | TreeSearch (min/med/max) | gap | +|---|---|---|---|---| +| random1 | 2353 | 1262 / 1262 / 1271 | 1289 / 1304 / 1336 | **+42** | +| random2 | 2274 | 1262 / 1265 / 1267 | 1306 / 1328 / 1368 | **+64** | +| wagner1 | 1711 | 1261 / 1262 / 1263 | 1295 / 1304 / 1352 | **+42** | +| wagner2 | 1584 | 1262 / 1263 / 1268 | 1292 / 1308 / 1338 | **+45** | +| partial | 1516 | 1262 / 1263 / 1266 | 1300 / 1310 / 1317 | **+46** | +| t0anchor | 1271 | 1267 / 1267 / 1267 | 1271 / 1271 / 1271 | +4 | + +**Reading.** TNT lands at ~1262–1271 from *any* start (tight, low variance); +TreeSearch lands at ~1289–1368 (≈30–65 steps higher, with much wider spread). +The difference is consistent across the whole quality ladder and both modes. +Two telling cells: (i) at the near-optimal anchor both engines hold 1271 under +strict TBR — TS *can* sit at the optimum; (ii) under Mode B, TNT's buffer +*escapes* 1271 → 1267, while TS stays stuck at 1271 — TNT's neighbourhood +contains moves TS cannot see even at the optimum. + +### Zhu2013 (n=75) — the gap is larger still + +Mode A (strict). Same pattern, bigger magnitude (TNT target ≈ 624): + +| start | start len | TNT (min/med/max) | TreeSearch (min/med/max) | gap | +|---|---|---|---|---| +| random1 | 1833 | 628 / 630 / 634 | 648 / 682 / 779 | **+52** | +| random2 | 1813 | 626 / 630 / 638 | 686 / 716 / 732 | **+86** | +| wagner1 | 1261 | 627 / 632 / 633 | 683 / 691 / 777 | **+60** | +| wagner2 | 1195 | 626 / 632 / 636 | 693 / 734 / 790 | **+102** | +| partial | 1342 | 625 / 627 / 632 | 670 / 706 / 768 | **+79** | +| t0anchor | 631 | 631 / 631 / 631 | 631 / 631 / 631 | +0 | + +Mode B (buffer). TNT reaches the project **target ≈ 624** from random starts: + +| start | start len | TNT (min/med/max) | TreeSearch (min/med/max) | gap | +|---|---|---|---|---| +| random1 | 1833 | 624 / 625 / 627 | 665 / 691 / 731 | **+66** | +| random2 | 1813 | 624 / 624 / 627 | 690 / 728 / 765 | **+103** | +| wagner1 | 1261 | 624 / 626 / 627 | 676 / 688 / 718 | **+62** | +| wagner2 | 1195 | 624 / 626 / 634 | 672 / 729 / 823 | **+103** | +| partial | 1342 | 625 / 625 / 627 | 665 / 692 / 743 | **+67** | +| t0anchor | 631 | 625 / 625 / 625 | 631 / 631 / 631 | +6 | + +The effect is robust across both datasets and **larger** on Zhu (+50 to +100). +Again the buffer escapes the anchor (631 → 625, toward target 624) while TS is +stuck. Step 1 is an unambiguous YES on both datasets. + +## Step 2 — mechanism: TreeSearch's TBR is rooted + +The deficit is **not** "TS reaches a worse basin"; it is **TS terminates before a +true (unrooted) TBR local optimum** because its move set is root-restricted. + +**(a) Reciprocal cross-feed (decisive).** Feed each engine's converged optimum +into the other: + +| Fed tree | Into | Result | +|---|---|---| +| TS local optimum **1302** | TNT `bbreak` nomulpars (deterministic) | → **1267** | +| TS local optimum 1302 | TNT mulpars hold 1000 | → 1262 | +| TNT local optimum **1266** | TS strict TBR | → **1266** (holds; converged) | + +TNT finds strictly-improving moves from a tree TS declared a local optimum, while +TS *holds* TNT's optimum (no wander-above ⇒ no scoring/round-trip artefact; TS +*can* represent it, it just can't path there). ⇒ neighbourhood incompleteness. + +**(b) Root-dependence (engine-internal proof).** Fitch length is root-invariant, +so every re-rooting of the TS 1302 optimum is still length 1302. Re-running TS +strict TBR from those re-rootings: + +| reroot at | Aciculomarphysa | Eunice_fucata | Leodice_americana | Leodice_thomasiana | Mooreonuphis | Palola_B5 | +|---|---|---|---|---|---|---| +| TS TBR final | 1296 | 1295 | **1281** | **1281** | 1291 | 1286 | + +An *unrooted* TBR local optimum would hold at 1302 for every rooting. It does +not (down to 1281) ⇒ **the TS TBR neighbourhood depends on the root.** + +**(c) Code (`src/ts_tbr.cpp`).** The kernel uses a rooted tree representation: +clips whose parent is the root are skipped (L804); only the *smaller* subtree of +each edge is clipped (L812); TBR rerooting is applied only to the **clipped +subtree**, never the main tree. Rearrangements that cross the fixed root are +therefore unreachable — the textbook definition of rooted (vs unrooted) TBR. + +**Confirmation — emulated root-invariance recovers ~half the gap, but not all.** +Wrapping the shipping rooted kernel in an outer reroot-sweep loop (TBR → try +re-rootings → TBR, looped to convergence) over **all 74 tips** (Zanol): + +| start | seed | TS rooted | reroot-invariant (strict) | reroot-inv + plateau | TNT | +|---|---|---|---|---|---| +| wagner (1711) | 1 | 1304 | 1292 | 1289 | **1265** | +| wagner | 2 | 1326 | 1304 | – | **1268** | +| random (2353) | 1 | 1330 | 1284 | 1279 | **1264** | +| random | 2 | 1295 | 1288 | – | **1264** | + +Root-invariance recovers roughly **half** the strict gap (e.g. random seed 1: +1330 → 1284, recovering 46 of the 66 steps to TNT) — a large, real effect that +**proves the root-dependent neighbourhood is a first-order cause.** But a +**+15 to +36 residual to TNT remains**, and it is *not* closed by also allowing +plateau-crossing (reroot-inv + plateau ≈ strict). So root-dependence is a +**proven major contributor, not the whole story.** The residual is either (i) +*incomplete* root-crossing — this emulation reroots only the *converged* tree +between full TBR runs, whereas true unrooted TBR crosses the root *within* every +sweep, so a proper integrated implementation should beat this emulation — or +(ii) a genuine second neighbourhood/acceptance difference (candidate suspects in +`ts_tbr.cpp`: the smaller-subtree-only clipping L812, and the collapsed-edge +pruning L817/L919). Disentangling (i) from (ii) is the natural follow-up. + +**Plateau-crossing is not the gap.** TS single-tree plateau-walking was tested +directly (`acceptEqual=TRUE`, `maxHits` ∈ {1, 5, 50, 500}) and does not help — +TS still lands ~1290–1350, no better than strict descent. So Mode A does not +unfairly deny TS the equal-length moves TNT's `nomulpars` takes; the deficit +survives giving TS those moves. + +## Recommended fix + +Make TBR root-invariant in `ts_tbr.cpp`: evaluate main-tree re-rootings *within* +the neighbourhood (true unrooted TBR — preferred, since the between-pass +emulation already recovers ~half and within-pass should do better), or as a +cheaper first cut an outer reroot-per-round loop (cf. the reroot-per-round fix +already used in tree-fusing). Expect to recover at least half the gap; then +investigate the residual (smaller-subtree clipping L812, collapsed-edge pruning +L817/L919) to close the rest. Re-run this shared-start harness to measure. + +**Output caveat (per project lead):** rerooting *during* search is free — +Fitch length is root-invariant — but the search root is an internal device only. +When the final tree(s) are returned to the user they **must be re-rooted onto the +originally-specified outgroup**, so the displayed topology matches the user's +rooting. The internal reroot must not leak into the user-facing result. + +## Apples-to-apples caveat resolved + +`help mult` confirms plain `mult`/`mult=replic 1` is **one RAS + TBR**; +`ratchet`/`drift`/`fuse` are opt-in flags, off by default. So the prior +"1 TNT `mult` rep → 1271 vs 20 TS reps → 1287" comparison was *not* TNT secretly +running ratchet/sectorial — its only confounder was TNT's own RAS Wagner start. +This shared-start test removes even that, and the gap remains: it is the TBR +move set, not the starting tree. + +--- + +## ADDENDUM (2026-06-18, same day): root cause is kernel move-INCOMPLETENESS, not just rootedness + +The "root-dependence recovers ~half, residual unexplained" reading above is +**superseded**. A gating cross-feed + a kernel-independent neighbourhood probe +pinned the residual precisely. + +### The gating cross-feed (`tbr_reroot_crossfeed.R`) + +Feeding the **all-tips reroot-invariant** TS optimum (≈1284 — which *should* be a +complete unrooted-TBR optimum) into TNT `bbreak`: + +| start | TS reroot-invariant opt | → TNT `nomulpars` | TNT own opt → TS reroot-inv | +|-------|------------------------:|------------------:|----------------------------:| +| wagner s1 | 1292 | **1262** | 1265 → 1265 (holds) | +| wagner s2 | 1304 | **1268** | 1268 → 1268 (holds) | +| random s1 | 1284 | **1270** | 1264 → 1264 (holds) | +| random s2 | 1288 | **1268** | 1264 → 1264 (holds) | + +TNT (even single-tree `nomulpars`) **improves** the TS optimum; TS **holds** TNT's. +Asymmetric ⇒ TNT's TBR neighbourhood strictly contains moves the TS all-tips +search lacks. The residual is **neighbourhood, not basin/path**. + +### The kernel-independent probe (`tbr_neighbourhood_probe.R`) + +`TBRMoves`/`SPRMoves` (→ `all_tbr`/`all_spr` in `rearrange.cpp`, a separate +UNOPTIMISED enumerator — no L812, no collapsed) on the TS 1284 optimum: + +- **43 improving TBR neighbours** (best 1280); **26 improving SPR neighbours** + (best 1280). So the deficit is at the **basic clip+graft (SPR) level**. +- TNT's 1264 optimum: **0 improving** — TNT reaches genuine canonical-TBR optima; + it does **not** exceed textbook TBR. + +So the TS kernel **falsely declares convergence** while real improving moves exist. + +### Mechanism: a STACK of completeness-breaking optimisations in `ts_tbr.cpp` + +Validated with gated fixes behind a new opt-in `TBRParams::unrooted` (default off; +the DEFAULT search is byte-identical — confirmed 1330/1295 unchanged). Each fix +peels missed moves and lowers the all-tips optimum (Zanol2014, random start): + +| kernel state | optimum | enumerator-improving | +|--------------|--------:|---------------------:| +| baseline (shipping) | 1284 | 43 | +| + collapsed pruning OFF | 1284 | 43 — **collapsed is NOT a cause; ruled OUT, keep it** | +| + L812 smaller-subtree skip relaxed | 1274 | 7–15 | +| + nz/ns graft skip fixed | 1270 | 4–6 | + +(≥1 further pruning remains → 4–6; `sp==clip_node` skip and the vp-dedup were +checked and are **sound**, so the residual is something else, not yet pinned.) +Trend: TS converging toward TNT's enumerator-clean 1264. + +The two confirmed bugs: + +- **L812** (`clip_size > n_tip/2` skip): clipping only the smaller side is meant to + reach larger-side moves via fragment-reroot + graft-at-original — but that graft + is killed by the next bug, so **SPR-prune-larger-subtree** moves are lost. +- **nz/ns skip** (`above==nz && below==ns` in the *rerooting* loops): correct for + the non-rerooted SPR loop (it's the identity) but **unsound** in the rerooting + loops — a rerooted fragment regrafted at its original location is a *distinct* + valid move. Fixing nz/ns alone (keeping L812) likely restores L812's soundness — + the preferred production fix (keeps the perf optimisation). + +This is a **correctness/soundness bug in the package's default TBR (and SPR)** — +every TBR search lands at non-canonical-TBR-optimal trees — materially bigger than +the EW-Fitch benchmark itself. The root-dependence finding (L804) is the *separate* +≈half, handled by the reroot mechanism. (Scripts: `tbr_reroot_crossfeed.R`, +`tbr_neighbourhood_probe.R`, `tbr_collapsed_test.R`. Logs in `tbr_results/`.) + +--- + +## FINAL (2026-06-18, post-merge of the directional-vroot fix) + +The "stacked L812/nz/ns enumeration bugs / soundness bug" framing above was +diagnosed on the **pre-fix** build and is **largely superseded**. After merging +`cpp-search` commit `2b299e4b` (the parent's EW-directional **scoring** fix — +"vroot" is a candidate-cost fix, not a root mechanism), the differential oracle +(`tbr_oracle.R`: run the in-kernel `tbr_search` to convergence, assert +`all_tbr`/`all_spr` 0-improving) shows: + +| kernel state | oracle failures (random 12-tip) | +|---|---| +| pre-fix default | 23/40 | +| **post-fix default (directional scoring fix)** | **9/60** | +| post-fix + all-tips rerooting | **0/60** | + +So the *scoring* under-count (union-of-finals → wrong abandonment cutoffs hiding +improving candidates) was the bulk of the apparent incompleteness; **the L812/nz/ns +move-edits were pre-fix artifacts and are not needed** (kept stashed, not applied). +The whole residual is the rooted-representation **root-edge limitation** (cannot +break the root edge; with the smaller-side clip filter also cannot clip edges whose +smaller side holds the root) — covered by re-rooting. + +### What was built (opt-in, default off) + +`TBRParams::unrooted` + an in-kernel **reroot-at-convergence** loop in +`tbr_search` (`ts_tbr.cpp`): after converging at one rooting, re-root at the next +tip and re-descend; stop when a full tip-sweep yields no strict improvement. +Score is root-invariant, so a re-root only changes the representation. Gated to the +plain search (no sector/constraint/tabu/pool). Exposed via `ts_tbr_diagnostics(..., +unrooted=)`. The **default path is unchanged** (`unrooted=FALSE`). + +Validated: oracle in single-call mode (kernel re-roots internally) → **0/60 at +12 tips, 0/40 at 16 tips**; one real-data 74-tip Zanol check → canonical-TBR-clean. + +### Cost / benefit (Zanol2014, `tbr_unrooted_validate.R`) + +| start | rooted len | unrooted len | gain | time × | +|---|--:|--:|--:|--:| +| random (poor) | 1272 | 1265–1271 | 0–7 | ~3× | +| RAS-Wagner (good) | 1267–1280 | 1267–1279 | 0–1 | ~10× | + +Median: rooted 1272 → unrooted 1269 (**gain ≈ 3**, **0–1 from Wagner starts**); +**median ≈ 6.5× wall-clock per `tbr_search` call**. It reaches *true* unrooted-TBR +optima but does **not** close the gap to TNT (1262–1264): 1265–1279 are clean +single-tree optima — the residual to TNT is **basin/escape (multi-tree/buffer)**, a +separate mechanism, not neighbourhood completeness. + +**Recommendation:** the directional scoring fix (already merged) is the real win. +The reroot mechanism is correct but its production value is marginal — production +uses Wagner starts (gain ≈ 0–1) and it costs ~6.5×. Keep it **opt-in** (or for a +final high-effort pass), not the default, unless a cheaper variant (relax-L812 + +direct in-kernel root-edge break, single pass) is built and shown to hold quality. +(Scripts: `tbr_oracle.R`, `tbr_unrooted_validate.R`, `tbr_collapsed_test.R`.) diff --git a/dev/plans/2026-06-18-tnt-sectsch-superpower.md b/dev/plans/2026-06-18-tnt-sectsch-superpower.md new file mode 100644 index 000000000..987286f94 --- /dev/null +++ b/dev/plans/2026-06-18-tnt-sectsch-superpower.md @@ -0,0 +1,224 @@ +# What gives TNT 1.6 `sectsch` its escape — bare-bones reverse-engineering + +Date: 2026-06-18. TNT-ONLY investigation (no TreeSearch `src/`/`R/` edits). Primary +dataset Zanol2014 (74 tips, equal-weights Fitch), confirmed on Wortley2006 & Giles2015. +TNT exe `C:/Programs/Phylogeny/tnt/TNT-bin/tnt.exe` (32-bit; `mxram` ≤ 1024). +All scratch + scripts under `dev/benchmarks/tnt_bare/`. + +## CONCLUSION (one paragraph) + +TNT `sectsch` does **not** escape by any clever per-sector move engine, sector geometry, +global-TBR cadence, drift, or extra search effort. It escapes because it runs sectorial +search over a **retained set of several equally-optimal trees** — `mult` keeps ~10 trees +(default `hold`), and a strict (`noequals`) sectorial sweep over that *topologically diverse* +set reaches the target on every dataset, whereas the identical sweep on a **single** tree +plateaus well above target and *stays there no matter how long it runs* (10× the rounds gives +the identical plateau). The mechanism is **equal-length topological variety**: a strict sector +re-solve only yields a strictly shorter tree when it has access to a *neighbouring* optimal +topology with a different sector arrangement, and a single frozen tree never exposes one. TNT +supplies that variety two ways — (1) the retained diverse optimum set (what it does by +default), and (2) the `equals` option, which lets a *single* tree accept equal-length lateral +sector re-solves and plateau-walk into an improvable configuration. **What TreeSearch should +replicate: drive/maintain the sectorial over a diverse set of equally-optimal trees rather +than polishing one tree** (effort is not a substitute); secondarily, turn ON the already-wired +`sector_accept_equal` lever for the single-tree case. + +Two calibrations (detail in "Is the set… SHARING?"): (i) the cross-lane benefit is real but +**modest and tree-level, not sector-level** — there is NO recombination of sectors across trees +(Goloboff: each sector is re-solved against its own tree); the shared *tree buffer* reuses whole +best-found trees as starting points, which beats "10 independent lanes, pick best" at equal +compute (1261 in 7/15 vs 1/15) though medians tie at 1262. (ii) 1261 is reachable by a lone tree +too, just rarely (~0.5%/restart) — the set raises the hit-rate ~7×, it does not unlock a +forbidden score. + +This **corrects** `2026-06-17-sectsch-escape-mechanism.md`, whose RANK-1 ("sector geometry + +replacement count") is refuted as the escape source and whose RANK-2 demotion of `equals` was +based on a single, atypical seed and a worse (1275) starting basin. See "Reconciliation". + +--- + +## Method & fixtures + +- Fitch matrix: `-`→`?`, equal weights. `WriteTntCharacters` (TreeSearch 2.0.0, lib `.agent-aband`). +- **The `hold` (tree-buffer) value moves the starting basin**: `mult=replic 1` (rseed 1) gives + 1271 under `hold 1000` (10 trees retained) but only **1275** under `hold 1` (1 tree). The TBR + buffer size is itself a lever. The canonical gap is defined at **T0 = 1271**, so the + single-tree start is tree #1 of the 1271 set (scores 1271 in both TNT `score;` and `TreeLength`). +- Bare runs read the start tree **fresh** (`proc `); **no mult/bb/tbr/xmult before sectsch**. + TNT score authoritative; final tree re-scored with `TreeLength` (min over all saved trees, + with `tipLabels` resolved from the data path TNT writes) — matches TNT every time (mapping OK). +- Scripts: `harness.R` (reusable runner+parsers), `driver1..5.R`, `confirm.R`, + `make_single.R`/`setup.R` (fixtures). Live TNT default reports **global TBR every 2 + substitutions in small sectors** — the `tnt_defaults.txt` dump saying "10" is stale. + +### The exact BARE-BONES script (deliverable requirement) +``` +mxram 1024; +report+; +proc data.tnt; ' Fitch matrix +rseed 1; +hold 1000; ' working buffer +proc tee.tre; ' the FIXED single T0=1271 (no search run yet) +sectsch: noglobal noequals nofuse godrift 9999 ; ' strip global-TBR, equals, fuse, drift +sectsch = rss ; ' ...repeated up to 12-300x +score ; +``` +**Result: 1271 → 1271, every round, forever (zero escape).** The simplest possible strict +sectsch on the real T0 does NOT reach target. (Default sectsch — global TBR every 2, 3 +RAS+TBR/sector — is also 1271 from this tree.) So the vanilla strict move-engine is *not* the +source of the escape. + +--- + +## Knob sweep — three start conditions × acceptance rule (Zanol, 12 rounds; `driver2.R`) + +Per-round running-best; final = min `TreeLength` over saved trees (verified equal). + +``` +A: SINGLE 1271 tree (hold 1000) + default 1271 ... -> 1271 + noglobal noequals 1271 ... -> 1271 + global 1 1271 ... -> 1271 (max global TBR: no help) + equals 1265 1263 1263 1263 1262 ... 1261 -> 1261 *** reaches target + equals global 1 1265 1263 ... 1263 -> 1263 (global TBR HURTS equals) + +B: 10-tree 1271 SET (hold 1000) + default (noequals) 1263 1261 ... -> 1261 *** TNT's actual behaviour + noglobal noequals 1265 1264 1263 1263 1261 ... -> 1261 (strict reaches it) + equals 1261 ... -> 1261 + +C: in-memory hold-1 mult = 1275 (the prior doc's start) + noglobal noequals 1272 1269 1264 1262 ... -> 1262 (strict stalls 1 ABOVE target) + equals 1265 1262 1261 ... -> 1261 +``` + +## Controls (`driver3.R`, `driver4.R`) + +- **Acceptance, not cadence/geometry/fuse.** `nofuse` on the set-strict run is byte-identical + to default → the set route is **not** tree-fusing. `global 1` never helps and *hurts* when + combined with `equals`. +- **`equals` accumulates NO buffer diversity** — with `equals` the tree count stays **1** all + the way down to 1261. So `equals` = single-tree lateral *plateau-walking* (temporal variety), + mechanistically distinct from the set route (stored variety). +- **Tree count vs diversity.** 10 *identical* copies + strict → 1263 (partial escape: the + sectorial buffer self-diversifies with tied-length alternatives); 10 *different* trees + + strict → 1261 (full). A single fixed topology + strict is the *only* fully frozen cell. + +## Seed robustness + DIVERSITY-vs-EFFORT (`driver5.R`, Zanol, 30 rounds, seeds 1–6) + +``` +SINGLE-T0 strict : min 1264 median 1267 max 1271 {1271,1264,1267,1267,1267,1266} +SINGLE-T0 equals : min 1261 median 1263 max 1267 +SET(10 diverse) strict : min 1261 median 1261.5 max 1264 +SINGLE-T0 strict @300 rnds : min 1264 median 1267 max 1271 (10x effort = IDENTICAL plateau) +``` +**Single-tree strict plateaus at median 1267 and 10× more rounds changes nothing** → the set's +advantage is **diversity, not compute**. Note single-strict median **1267 == the task's +"TreeSearch reaches ~1267"**, and set/default == TNT's 1261 — the gap is exactly this lever. + +## Cross-dataset confirmation (`confirm.R`, seeds 1–4, 30 rounds, median [min–max]) + +| dataset (target) | single strict | single `equals` | SET strict | **SET default = TNT** | +|--------------------|---------------|-----------------|------------|------------------------| +| Zanol2014 (1261) | 1267 [1264–1271] | 1262.5 [1261–1263] | 1261 [1261–1262] | **1261 [1261–1262]** | +| Wortley2006 (479) | 485 [482–485] | 480.5 [479–485] | 480 [479–482] | **479.5 [479–480]** | +| Giles2015 (670) | 671.5 [671–672] | 670 [670–671] | 670 [670–670] | **670 [670–670]** | + +Universal ordering: `single-strict` (worst, above target) ≫ `single-equals` ≈ `set-strict` ≈ +`set-default` (= target). The retained-set route reaches target on all three; the single-tree +strict route never does. + +## Is the set "10 independent lanes, pick best", or genuine SHARING? (`driver6–C.R`) + +The N trees are processed with `tree` = "all trees" (default): **10 different trees → 10 +incomparable sector sets**, NOT one tree's sectors re-solved 10× (that is the flat 300-round +effort control). The honest answer took several careful tests and overturned earlier wording: + +- **NO sector-level recombination.** Goloboff 1999's RSS re-solves each sector against *its own + tree's* scaffold ("best among the R+r replications AND the present resolution… place it in the + whole tree") and `nofuse` is a no-op. Structure from tree A is **never** spliced into tree B. +- **1261 IS reachable by a lone lane — just rare.** 200 independent single-tree restarts: 1/200 + reached ≤1261 (~0.5%/restart); the set is *not* reaching an otherwise-impossible score + (`driverA.R` TEST1). (An earlier "0/50 → only the set can" was undersampling.) +- **Identical copies in a shared buffer ≈ separate restarts.** 10 copies of one tree, shared + buffer, vs 10 separate restarts of it: ties (1262–1263) — no coupling detectable when the + starting tree is fixed (`driverA.R` TEST2; uninformative for trees that *can* reach 1261). +- **But "together" beats "apart" at EQUAL compute.** Paired, 15 reps, both = 10 trees × 30 + rounds: SET reached 1261 in **7/15**; 10 independent lanes (same trees, take best) **1/15**; + paired setindep 1 (sign test p≈0.04) — `driverB.R`. The 1/15 for + independent matches the ~0.5%/lane rate; the set's 7/15 does not. So the set is **NOT** + equivalent to "10 lanes, pick best." +- **The advantage is NOT sector size.** Forcing a single tree onto sectors up to size 70 + (near-global, n=74) still gives median 1267, 0/6 reaching 1261 — identical to default size 37 + (`driverC.R`). So the shared-size-counter idea is refuted too. +- **By elimination, the channel is the shared TREE buffer (tree-level, not sector-level).** Not + recombination, not size, not effort (300-round flat), not starting-diversity-reaching-the- + unreachable. The 10 lanes draw from and write to one common tree pool, so **whole improved + trees discovered by any lane become starting points reused by the ongoing search** — a + beam/population effect. Corroboration: the set has a 1261 in its buffer by round ~8 + (`driver8.R`), yet *no* isolated lane reaches 1261 even in 30 rounds (0/36 in `driverC`), so + that 1261 cannot be a single independent-lane trajectory. The exact retention/reuse rule is a + TNT-internal (closed source) not line-traced here; the buffer stays diverse (lengths 1261–1271 + coexist at the plateau), so it is not simple best-culling. + +**Bottom line on "collective":** there is cross-lane information flow, but at the granularity of +**whole trees** (the shared buffer pools and reuses the best trees found by any lane), NOT at the +granularity of sectors. Magnitude is modest (medians tie at 1262; the effect is a ~7× higher +chance of hitting the 1261 optimum), but real and reproducible. + +## Sector-size schedule — TNT vs TreeSearch (`driverC.R`, `driverD.R`) + +TNT default sector size = **min(n/2, 45)** → 37 = n/2 for Zanol (n=74). Within ONE `sectsch=rss` +invocation it ramps: do M = (T·100)/((100−selfact)·S) ≈ 3 selections at size S, then S → S×1.75 +(`increase 75`), toward ~n. BUT the size **resets to n/2 at the start of every invocation** +(settings dump is byte-identical after each round: "run 3 sectors of 37 nodes"). So across a +looped search the operative size is just **n/2**. + +And the ramp is inert here: SET with `minsize 37 maxsize 37`, with `… increase 0` (escalation +OFF), and default(escalating) are **identical** (3/6 reach 1261, med 1261.5). Forcing a SINGLE +tree onto fixed sizes 37→70 is also flat (`driverC`: all 0/6, med 1267). **Sector size is not a +lever for this escape; the buffer is.** + +TreeSearch (read-only, `src/ts_sector.cpp:1044-1051`, `R/MaximizeParsimony.R`) does NOT use n/2: +it collects EXISTING internal clades with size in a band [`sectorMinSize`,`sectorMaxSize`] = +[6,50] default (80 thorough, 100 large) and picks ~`2·n_tip/avg_size` ≈ 5 of them (random or +conflict-weighted). So its sectors are a wide size *distribution* of existing clades (max can +EXCEED n/2), vs TNT's single ~n/2 walked-up clade. (The "n/2 capped at 45 / for n<~88" rule is +TNT's `selectem`, not TreeSearch's current code.) Since size is not the lever, this difference +is not what drives the gap. + +## MINIMAL sufficient configuration +- **From the fixed canonical T0=1271 (clean isolation):** the decisive factor is **the number + of distinct equally-optimal trees the sectorial operates over**. One tree (strict) → median + 1267, never target, even at 10× rounds. The 10-tree diverse set (strict, `noequals` = + TNT default) → 1261. Same start, same effort budget; only the retained-set diversity differs. +- **From a forced single tree:** **`sectsch: equals;`** is the single sufficient knob (reaches + 1261 on Zanol seed 1; median 1262–1263 over seeds). Necessary too — every strict single-tree + config plateaus above target. +- *(Aside, not a clean isolation:* in the end-to-end pipeline `hold 1` → 1262 vs `hold 1000` → + 1261, but `hold 1` also shifts the `mult` output to the worse **1275** basin, so that + comparison conflates buffer size with start quality — the clean evidence is the fixed-T0 + single-vs-set contrast above.) + +## Reconciliation with `2026-06-17-sectsch-escape-mechanism.md` +- Its "**-13 strict bulk escape with `noglobal`**" was measured from the **1275** (`hold 1`) + in-memory tree, which is *not* sector-optimal, so strict moves trivially exist — but even + there strict stalls at **1262**, one step above target (block C). From the real T0=1271, + strict does *nothing* on seed 1 and plateaus at 1267 on average. The "bulk" was a worse-basin + artifact, not the T0 escape. +- It ranked sector **geometry/replacement-count #1** and **`equals` a minor #2 bridge**. The + controlled fixed-start experiment inverts this: geometry/cadence/effort do not move the single + T0; variety (set or `equals`) does. Its `noglobal`-barely-changes-it observation is consistent + with mine (`global` is not the lever) — but it concluded the *strict sector replacements* were + the escape, which the diversity-vs-effort control refutes. + +## What the parent TreeSearch project should replicate (priority order) +1. **PRIMARY — sectorial over a retained DIVERSE set of equally-optimal trees**, not a single + polished tree. (Prior doc notes TS picks a single existing clade on one tree.) Effort cannot + substitute. Keep the set of optimal trees `mult`/RAS produces and let sector improvements + propagate across it. +2. **SECONDARY — flip ON `sector_accept_equal`** (`src/ts_driven.h:94`, default `false`, + already plumbed → `SectorParams::accept_equal`, comment "Goloboff 2014 plateau lever"). This + is the single-tree substitute (median ~1262–1263; sometimes hits target). Cheap to test. +3. **NOT the lever:** global-TBR cadence (flat alone, harmful with equals), sector + geometry/sub-clade collapse, recursion, more rounds/effort. Do not invest there for THIS gap. diff --git a/dev/plans/2026-06-18-wagner-insertion-cost-bug.md b/dev/plans/2026-06-18-wagner-insertion-cost-bug.md new file mode 100644 index 000000000..d4211648a --- /dev/null +++ b/dev/plans/2026-06-18-wagner-insertion-cost-bug.md @@ -0,0 +1,153 @@ +# Wagner insertion-cost formula is wrong → +30% starting trees (2026-06-18) + +## TL;DR +TreeSearch's RAS Wagner trees score **~+30% over the optimum** on Zanol2014 +(mean 1664; optimum ~1261) where TNT's no-swap RAS Wagner scores **~+3%** +(1283–1325). Proven cause: the candidate-edge insertion-cost function +`fitch_indirect_length` (src/ts_fitch.cpp) computes the edge "passing set" as the +**union of the two endpoints' FINAL states**, `Y = final(A) | final(D)`. The union +is a superset of the true edge set → it **undercounts** insertion cost → too many +positions look free → greedy stepwise addition degenerates toward arbitrary +(first-found) placement. The error is largest when state sets are most ambiguous +(early Wagner steps), which is why Wagner is hit so hard. + +This affects EVERY search start (RANDOM, GOLOBOFF=the production default, ENTROPY). + +## Evidence (all on canonical Zanol2014, EW Fitch, dev/benchmarks/t0/) +- `bench_wagner.R` (K=8): TSrand mean **1656** (sd 66) vs TNT no-swap RAS + **1300.9** (sd 13); KS D=1.0, p=1.6e-4. Diversity also differs (TSrand + meanPairwise CID 0.77 vs TNT 0.40 — but that's a *symptom* of near-random + placement, not a virtue). +- `diag_wagner_verify.R`: kernel's own score == TreeLength(reconstruction) for + every seed (MATCH) → NOT a reconstruction artifact. The tested public + `AdditionTree()` path reproduces it (~1515–1667). RandomTree ref = 2295. +- `diag_wagner_exact.R` (**decisive**): an EXACT-insertion RAS Wagner (try every + edge, full TreeLength, true argmin) reaches **1295–1309** — TNT parity — on the + SAME addition orders where the fast formula gives 1644–1678 (+342…+370). + ⇒ the algorithm is fine; the fast cost formula is the bug. +- `diag_wagner_bias_scores.R`: RANDOM 1664 / GOLOBOFF(default) 1661 / ENTROPY 1479 + — bias changes only the order, so all inherit the bug. + +## The formula +`fitch_indirect_length(clip_prelim, A, D)` (ts_fitch.cpp:380): +``` +Y = final(A) | final(D) // per character, OR of state words +needs_step = ~any_hit(clip_prelim, Y) & active +extra = popcount(needs_step) * weight +``` +Comment claims union is "exact for non-additive (Goloboff 1996); intersection +would overcount." Empirically the opposite: we UNDERcount. + +## Proposed fix (to validate against the exact-insertion oracle BEFORE shipping) +Classical Fitch result: the set of states on edge (A,D) in MPRs is +`(final(A) ∩ final(D))` if that intersection is non-empty, else +`(final(A) ∪ final(D))` — i.e. **intersect-else-union**, computed per character. +Adding a tip with downpass set T costs `[ T ∩ E == ∅ ]` per character. + +So the candidate fix is to replace the pure union with a per-character +intersect-else-union of the two endpoint finals — the same combine logic the +downpass already uses. Likely localized, but `fitch_indirect_length` has several +siblings that must all change consistently: +- `fitch_indirect_length` / `_bounded` / `_cached` (ts_fitch.cpp) +- `fitch_indirect_bounded_flat` (+ any flat/EW specialisation) +- the NA variant in `ts_fitch_na_incr.h` +- wherever a precomputed `vroot` edge set is built for `_cached` (TBR) — it must + use intersect-else-union too. + +VALIDATION GATE: a fast-formula Wagner must reach the exact-insertion oracle +(~1300 on Zanol) before the fix is accepted. If intersect-else-union of *finals* +doesn't get there, fall back to maintaining a directional uppass view and +combining `prelim[D]` with the incoming view at D. + +## Scope / risk +`fitch_indirect_length*` is shared by Wagner, TBR, sector, prune-reinsert, drift, +temper. A correct (tighter) cost estimate should only HELP candidate ranking, but: +- must re-run the full testthat suite, +- must re-benchmark a short EW search (Wagner+TBR multistart) to confirm + end-to-end improvement and no regression, +- the TBR chip (task: "Compare TBR ensemble: TNT vs TreeSearch") should re-test + after the fix — the SAME formula drives TBR reinsertion scoring, so TBR may be + partially degraded too (less than Wagner, since full-tree final sets are less + ambiguous). + +## CORRECTED fix + VALIDATION (2026-06-18, later) +The "intersect-else-union of FINALS" guess above is WRONG (finals are contaminated +by D's own subtree; it gave +150 over oracle). The proven-correct edge set is the +**directional** message combine: +``` +down[D] = prelim[D] +up[D] = combine(up[parent], prelim[sibling]) // root degree-2: up[child]=prelim[other child] +E(A,D) = (down[D] ∩ up[D]) if non-empty else (down[D] ∪ up[D]) // per character +cost = #chars where T & E == 0 +``` +Reference kernel `ts_wagner_tree_dir` (ts_wagner.cpp) + per-edge probe +`ts_reinsert_scan` (ts_rcpp.cpp) added behind the worktree build. + +VALIDATION (strict gate met): +- Per-edge (ts_reinsert_scan, clip+rescore truth): directional == actual **71/71** + edges (74-tip tree) and **9/9** (12-tip tree); union matches only 4–6/71. +- End-to-end: directional RAS Wagner mean **1308** (Zanol) / **659** (Zhu) == + brute oracle (~1300/657) and TNT band (1283–1325); buggy union = 1631/1189. +- Same-order vs brute: diffs ±27, BOTH directions = pure tie-break noise (greedy + tie-break sensitivity measured at ±15). Tie-break is NOT the lever. +- Speed: directional kernel only **1.7×** slower than buggy (2.0 vs 1.2 ms/tree, + n=74) — full down+up recompute per step is cheap enough for production. + +Two separate defects clarified: +- Cost formula (union→directional): the bug in PRODUCTION wagner_tree. +- Pendant-edge scan: was a bug in my REFERENCE kernel only (used postorder = + internal nodes only). Production's DFS already scans tip edges → not affected. + +NUANCE: on a RESOLVED tree the union ARGMIN is already correct (picks the optimal +edge; only magnitudes wrong). The bug bites during CONSTRUCTION (ambiguous partial +trees). ⇒ TBR/sector (resolved trees) likely far less affected; decide separately +whether their fitch_indirect_length/vroot need the directional set. + +## PRODUCTION PORT — DONE (2026-06-18) +Shared helper `compute_insertion_edge_sets` (ts_fitch.cpp/.h) builds the exact +per-node edge set E[D]=combine(prelim[D],up[D]); callers score with +`fitch_indirect_length_cached(clip_prelim, &E[child], ds, cutoff)` (that helper +already computes [T ∩ vroot == 0], so passing E as the vroot gives the exact +directional cost through existing code). + +- `wagner_tree`: candidate DFS scan now uses E[below] (constraint filter + + incremental rescore unchanged). AdditionTree / random_wagner / biased_wagner + all fixed: RANDOM 1664→1310, GOLOBOFF(default) 1661→1306, ENTROPY 1479→1304. +- `tbr_search`: EW-only path (`ew_directional = !has_na && !use_iw`) — the SPR + scan and the rerooting `vroot_cache` now use E[]; NA (three-pass) and + implied-weights keep union-of-finals (their cached scorers require it). + vroot_cache[ei] = E[main_edges[ei].second]. +- Test `tests/testthat/test-wagner-quality.R`: mean of 8 RAS addition trees + within 8% of the MPT (Zanol 1261, Zhu 624). Fixed ~+4–6%; bug was +30%. + +VERIFICATION: +- Full testthat: **0 failed expectations**. 6 file-level errors are pre-existing + (test-CharacterHierarchy.R / test-LeastSquares.R call bare unexported `.fns`, + invisible under test_dir on an installed pkg — unrelated to this change). EW + score checks pass (Vinther2008 TBR/XSS/Ratchet = 79). +- END-TO-END payoff (pure Wagner+TBR multistart, ratchet/sectorial OFF, Zanol): + | starts | fixed | buggy era | target | + |--------|-------|-----------|--------| + | 1 | 1267 | 1315 | 1261 | + | 5 | 1264 | 1306 | 1261 | + | 20 | 1264 | 1287 | 1261 | + One fixed start now beats twenty buggy starts; +3 over the optimum vs +26. + +## Done before landing +- Validation scaffolding REMOVED: debug exports `ts_wagner_tree_dir`, + `ts_reinsert_scan`, the reference `directional_wagner_tree`, and the + `TS_WAGNER_UNION` env diagnostic (+ their init.c registrations + RcppExports + stubs). The landed commit touches only ts_fitch.cpp/.h, ts_wagner.cpp, + ts_tbr.cpp, test-wagner-quality.R. + +## Remaining (optional / out of scope) +- Perf: `compute_insertion_edge_sets` allocates its up[] scratch per call; reuse + a buffer if a /profile pass flags TBR overhead (UNMEASURED). +- IW/NA insertion cost still uses union-of-finals (separate task if it matters + for those objectives). + +## Status +SHIPPED on cpp-search (commit 2b299e4b, merged + pushed via bf5b9541). Fix + +vroot + test, 0 regressions, end-to-end gap +26→+3 on the core engine; Wagner +distribution now statistically indistinguishable from TNT. Unrooted TBR handled +separately by the chip ([[tbr-rooted-vs-unrooted]]). diff --git a/dev/plans/2026-06-19-component-isolation-profiling.md b/dev/plans/2026-06-19-component-isolation-profiling.md new file mode 100644 index 000000000..30f945212 --- /dev/null +++ b/dev/plans/2026-06-19-component-isolation-profiling.md @@ -0,0 +1,280 @@ +# Component-isolation profiling program (2026-06-19) + +## STATUS (updated 2026-06-21) + +Progress against the per-component "two gates" (AT-LIMIT VTune + shared-start TNT +race) and the standing ordering: + +| Component | Gate 1 — AT-LIMIT | Gate 2 — TNT race | Recipe lever | Verdict | +|---|---|---|---|---| +| **scoring (Fitch EW)** | ✅ AT-LIMIT (T-P5l: AVX2 reduce optimal at n_states=9) | n/a (TNT exposes no scoring loop) | — | **CLOSED** | +| **TBR (keystone)** | ✅ kernel at-limit (T-P5l) · precompute lazy/incremental-VIEW dead by measurement (M46/T-P5j) · scaffolding below-floor (T-P5m) · re-survey de-opaqued, no opaque-bucket prize (T-P5o) | ✅ done (T-P5h3/h4): **quality gapB=0, efficiency≈1** (the "2–4× candidates" was a counting artifact); residual = per-candidate **throughput 1.3–2.3×** on heavy multistate only | getenv hoist **banked, ~20–26 % MISSION wall** (T-P5n/T-S6d) | **CLOSED — incl. MIDDLE-LEVEL algorithm** (T-P5p, 21-agent audit): TS **already implements** quick-TBR's incremental-length method — the directional up-pass IS the slide at the one-combine-per-node floor; `ts_rate` flat-in-N proves the t² asymptotic; residual ~2× = accepted constant factor (mechanism unpinned — reduce + combine each at-limit). **Closed because the cross-cutting kernel (≈½ EW CPU, 96% of sectorial wall is `tbr_search`) is at-limit — NOT because the phase is <8%.** T-P5n/T-P5o "contradiction" = labeling mismatch → incremental-length is **dead-by-solid-argument**. (c) bound-then-verify now **SETTLED dead-by-proof-plus-magnitude (T-P5q, #51)**; only (d) fused edge-set remains (refutable, ~2-6% wash, low-priority, flagged-for-human). | +| **sectorial (xss/rss/css)** | ✅ Round 6 (T-S6a–d): ~96 % of isolated sectorial wall = the inner `tbr_search`; sectorial-specific scaffolding ≤2 %; byte-identical micro-levers ~2.8 % banked (T-S6c) | ✅ **probe-closed (T-S6e), branch `sect-profile-da0f203f` FULLY MERGED at `00967d77`**: the efficiency axis was probed (suppress-trailing-TBR; without-replacement picks) → **AT-LIMIT for safe/behaviour-neutral wins**; the one real lever (consolidate the 3× sequential trailing TBRs in xss→rss→css) is a recipe redesign **handed to #40**. CAVEAT: probe-verdict, **not** a literal TNT-`sectsch` head-to-head race (accepted — other agent's domain, at-limit-by-inheritance). | sector-resolve at parity (#24 float-HTU gated out at plateau) | **BOTH gates closed (sectorial agent); residual lever → #40** | +| **ratchet** | ~AT-LIMIT by inheritance (it is reweight + `tbr_search`; throughput rides the now-closed TBR kernel) | ✅ **DONE (2026-06-21, job `17533025`)**: TS `ts_ratchet_search` vs TNT `ratchet=iter 30` from a shared Wagner start, seeds 1–5 — **cycle-quality PARITY** (same score @ fixed iters: Zanol 1262=1262, Zhu 625=625, Giles 670=670) ⇒ **TNT does NOT reach the optimum in fewer reweight cycles**; wall ~1.8–2.6× = at-limit throughput, no ratchet-specific lever. (Examined-candidate efficiency unmeasured — `RatchetResult` lacks the counter; score+wall are valid.) | `ratchetCycles` 12→6 **banked**, ~20–38 % wall, no quality loss (T-P5d) | **BOTH gates done; recipe lever banked; ratchet = at-limit + cycle-parity** | +| **fuse / drift** | ✅ AT-LIMIT-by-inheritance (#52: `tree_fuse`/`drift_search` both wrap the closed `tbr_search` kernel; drift = the TBR kernel duplicated + an accept rule; no getenv/hidden alloc) | ⏳ low-priority Hamilton-confirmatory (throughput inherited from the closed kernel; fuse race intrinsically awkward — needs a diverse POOL not a single shared start) | fuse >64-tip reroot **crash fix LANDED** (ac8e808a) | **gate-1 done; gate-2 confirmatory/low-priority** | +| **connective tissue (phase 0)** | ✅ AT-LIMIT (read 2026-06-20): in production (default `verbosity=1L`) the per-phase `score_tree` prints are `verbosity>=2`-gated (OFF); only un-gated full rescores are `score_before_cycle`+`score_after_cycle` for the convergence/reset check = **2/outer-cycle**, ~µs each over ~1–few cycles/replicate ⇒ **~0.001% of wall** (one is redundant — `score_before_cycle`≡prior cycle's `score_after_cycle` — but sub-floor, not worth the convergence-logic risk). R.dll 12% already T-P5o'd as amortizable/startup-inflated. Step-switching: each phase owns its state; only orchestrator rebuild = intra-fuse `build_postorder+reset_states` (preset-only, 1/cycle). | — | — | **CLOSED — no addressable production fat** | + +### Mission KPI re-measure (2026-06-21) — REFRAMES "the gap" (see dev/profiling/kpi-2026-06-21.md) + +Fresh Hamilton run on post-fix cpp-search `5ee3ba3c` (getenv hoist + sector levers ++ ratchet 12→6; freshness-asserted). Two robust conclusions + one corrected +overreach: + +1. **QUALITY CLOSED, BANKED (budget-independent).** TS reaches the optimum on + every dataset/seed; TNT's fast configs miss by +1; on Zanol (ns=9) **TS is the + only reliably-1261 config (3/3)** — TS is the *more reliable* engine on hard + data. This is the solid half of parity. +2. **The wall gap is NOT algorithmic.** Candidate-efficiency (COUNT-based, + throughput-independent; `headtohead_phase0.csv`) is `cand_ratio` ≈ 1.2–1.9× + near-parity; per-candidate throughput ≈ 2× at-limit. The KPI's eye-catching + 8–110× is a **default-budget mismatch** (TS `default` = heavy search; TNT + `xmult` default = light), not inefficiency. +3. **Composition #40 is a HYPOTHESIS, not an order-of-magnitude prize** (advisor + correction to my first write-up): the ratio is biggest where wall is cheapest + (Wortley/Giles, seconds); on Zanol — where wall actually hurts — the + thoroughness is **load-bearing** for the reliable optimum. Proven head-room is + only "thorough→default" (same score, ~2× wall = pure waste); whether there is + more *below* default without losing reliability is exactly #40's open question. + Opening diagnostic dispatched: fresh converge-mode h2h (gapB=0 + current + `cand_ratio`, job `17533024`) + the queued ratchet probe (job `17533025`, + coarse — units/work-per-iter confounded, order-of-magnitude only). + +### Structural clarifications (answering the supervising questions, 2026-06-20) + +- **"Thin Sectorial" — is sectorial done, or is there a "fat sectorial" to follow?** + "Thin Sectorial" is just shorthand for the *lean isolation pass* on the ONE + sectorial component (the same treatment TBR got) — there is **no separate "fat + sectorial" component** coming. Sectorial = the single component covering all + three TNT varieties (**XSS / RSS / CSS**). Gate-1 (AT-LIMIT VTune) is done; what + remains is gate-2 (the TNT `sectsch` race) + the efficiency loose-ends, both + owned by the sectorial agent. The *heavier* sectorial questions that surfaced + (consolidating the 3 sequential trailing TBRs in xss→rss→css, sector-size tuning + — T-S6e) are **RECIPE composition (#40)**, a separate axis, not a second + sectorial element. +- **Are fuse / rss / etc. covered, or do they have their own slots?** + - **RSS** (Random Sectorial Search) is **not** a separate component — it is one of + the three sectorial varieties, covered under "sectorial" (Round 6 instrumented + `rss_search` directly). + - **FUSE** (tree fusing) and **DRIFT** each have **their own slot** (component 4, + "fuse / drift — later") and are **not yet isolated/raced**. Drift had QUALITY + work (#25 TNT-faithful drift for +1 datasets); fuse has a pending >64-tip + reroot-crash fix to port ([[fuse-reroot-segfault]]). Both still owe the two + isolation gates. + +### Status: ALL COMPONENTS CLOSED — program complete (2026-06-21) + +Every component is through **both** gates and measured at-limit: +- **scoring** ✅ · **TBR** (kernel + precompute + scorer + middle-level algorithm, + T-P5p) ✅ · **sectorial** ✅ (probe-closed + branch merged `00967d77`) · + **ratchet** ✅ (cycle-parity isolated race, 2026-06-21, job `17533025`) · + **fuse/drift** ✅ (at-limit-by-inheritance; gate-2 low-priority confirmatory) · + **connective tissue** ✅. +- The 2026-06-21 mission KPI (above) confirms the synthesis end-to-end: quality + ≥ TNT, candidate-efficiency ~1.5× near-parity, throughput ~2× at-limit ⇒ the wall + gap is **budget/composition**, NOT per-component throughput. + +Residual TBR thread = only sub-lever (d) per-candidate fused edge-set (bit-identical, +~2-6% predicted wash, flagged-for-human — not a blocker). lever-c SETTLED dead +(T-P5q/#51). Data-class reopens recorded in T-P5p/T-P5q (large-N/molecular → revive +incremental-VIEW; binary/DNA ns≤4 → scalar scorer / S2 split shift; S1 + S3 lemma +data-independent). + +### GATE BEFORE COMPOSITION #40 — fresh-eyes component re-audit (2026-06-21, user-ordered) + +Before tuning the recipe: **"what did we MISS in the individual components?"** An +adversarial completeness pass — independent auditors per component, tasked to +*break* the at-limit verdicts (find an untested lever, a wrong assumption, an +uncovered data-class, a measurement blind-spot), NOT to re-confirm them. The getenv +hoist (~20-26% mission wall, VTune-invisible) is the precedent: the biggest win of +the program was something the standard measurement *missed*. Survivors that are +genuinely new feed back into the relevant component; composition #40 begins only once +this pass is dry. + +**RESULTS (2026-06-21, workflow `wf_24dc492a`, 27 agents, 8 component lenses): +18 candidates → 3 survived adversarial verification → 15 killed as rediscoveries/ +refuted. The core kernel/TBR THROUGHPUT verdicts STAND — no second getenv-class +hidden hotspot.** Confirmed solid-at-limit, nothing new: scoring-kernel, TBR +precompute (incremental-length=quick-TBR already done), ratchet (12→6 banked), +starting-trees/Wagner. The 3 survivors (all MODEST — none a confirmed multi-x win): +- **#55 (rank 1, fuse, HIGH conf) — getenv-class-in-KIND:** all fuse-VALUE evidence + on the >64t mission datasets predates the 2026-06-20 reroot fix (`ac8e808a`); the + recipe's "fuse is free / +1 intraFuse regression" rests on pre-fix runs whose + multi-round path was skipped/truncated/corrupting. Fuse is **unmeasured on correct + code**. (Default `poolSuboptimal=0` ⇒ pool size 1 ⇒ inter-replicate fuse SKIPPED + entirely.) Re-measure dispatched (Hamilton job `17533029`): count productive + `Fuse improved` events with `poolSuboptimal=5`+`intraFuse` on Zanol/Zhu/Giles. + Binary → either a #40 simplification (drop wasted fuse) or a recovered quality + lever on the hardest datasets. **Direct #40 input.** +- **#56 (rank 2, sectorial, MED conf) — NEW throughput lever:** `build_reduced_dataset` + (ts_sector.cpp:431-440) copies the full block structure; `active_mask` is GLOBAL ⇒ + constant-within-sector-but-globally-informative columns scanned at every inner-sector + node (~96% of sectorial wall). Offline (reproduced): ~40-60% fewer SIMD blocks on + Zhu/Zanol/Giles (weakest on Zanol). Column analog of the row-only + `sectorCollapseTarget`. GATED by early-abandonment ⇒ realizable only if front-packing + cuts blocks-reached-before-bail; correctness needs the HTU pseudo-tip state. Net + realistic low-single-digit to ~10%. +- **#57 (rank 3, tbr-scaffold, LOW conf, likely sub-floor):** x4 reroot batch scores + every member to the deepest-bailing member's depth; gross ceiling ~1-2% EW, + ILP-confounded. Cheap wasted-block counter as a kill-gate. + +Net: the at-limit picture holds; the addressable wall stays in orchestration (#40), +and the strongest survivor (fuse) is itself a #40 input. Tasks #55-57; #40 blocked-by +#55,#56. + +### AUDIT FOLLOW-UPS — RESOLVED (2026-06-21, all measured on Hamilton) + +- **#55 fuse → DROP (dead weight).** Probe `17533029`: fuse FIRES on the >64t mission + class (7 attempts/run, 60-70 exchanges/run) but **0 improvements** across + Zanol/Zhu/Giles × pool/intra × 2 seeds. Not pool-collapse — genuinely useless. #40 + input: drop fuse / raise `fuseInterval` for this class (it is already off-by-default + since `poolSuboptimal=0` ⇒ pool size 1). +- **#56 sectorial column reduction → SHIPPED opt-in (`830b8cc3`, `TS_SECT_COLREDUCE`, + off by default).** `reduce_sector_columns_ew` drops constant-within-{sector tips+HTU} + chars (0 Fitch steps ⇒ scores exact) + repacks survivors into fewer n_states-grouped + blocks. Adversarial review (`wf_3727ea63`) caught a CRITICAL stale-`rd.subtree`-stride + OOB that the in-process-toggle A/B had FALSE-PASSED (flag read once at static init ⇒ + both arms ran OFF); fixed. Re-validated (`17533059`): dScore=0 9/9, valgrind clean, + review-verified invariance+bit-arithmetic. **Saving (rss-isolated): Giles 17%, Zhu 9%, + Zanol ~0%** (uniform ns=9 = least reduction = the load-bearing case). `dCand≠0` on + mixed-n_states (block reorder shifts bail timing ⇒ equally-optimal different path; + Zanol uniform = byte-identical) ⇒ **OPT-IN, never a default flip.** Before default-on + for any class: run a sector-score ORACLE (reduced vs full, same topology, mixed state) + — an accept-gated search can't discriminate a masked packing bug. **#56 = a #40 + ingredient (enable per-class where it helps; never Zanol), not standalone.** +- **#57 x4 reroot waste → SETTLED: x4-optimal, force-scalar REJECTED.** Counter probe + (`17533033`) measured X4_WASTE frac=0.137 = ~1.9% EW gross ceiling. The force-scalar + A/B (`17533065`, runtime flag `TS_REROOT_SCALAR`, separate processes) settles the sign: + GATE PASSED (dScore=0 **and** dCand=0 9/9 = byte-identical score+candidates), wall + speedup x4/scalar = Giles 0.939, Zhu 0.945 (scalar **5-6% slower**), Zanol 1.001 (dead + heat); overall 0.946. ⇒ the ~1.9% ceiling is **not realizable** — the x4 ILP (4 + independent `any_hit_reduce` chains) more than covers it; forfeiting it loses 5-6% on + mixed-state and breaks even on ns=9. Flag reverted (measurement-only). Closed. + +### CROSS-CUTTING LEVER characterised (2026-06-21): `clipOrder=2L` = per-class, Zanol-only safe + +The switches reference flagged tips-first clip ordering as an untested cheap throughput +trial. Now measured (`17533071`@20-rep + `17541277`@40-rep, 3 seeds, EW): `clipOrder=2L` +is ~1.25× faster / ~26% fewer candidates overall, but it **biases the search trajectory** +(not byte-identical) and is a **per-class TRADEOFF, not a global win**: +- **Zanol (uniform ns=9): CLEAN win** — 3/3 reach 1261, consistently ~1.5× faster. +- **Zhu (mixed): quality tradeoff** — loses +1 on 1 seed *even at 40 reps* (doubling the + budget did NOT recover it ⇒ a genuine trajectory effect, not under-budget). +- **Giles (mixed): wall unstable** (one seed examined 60% more candidates). +⇒ #40 may enable `clipOrder=2L` **on Zanol-type data only**; it complements +`TS_SECT_COLREDUCE` (clipOrder helps the uniform-ns case col-reduce can't, and hurts the +mixed-state case col-reduce helps). Default stays `0L`. Recorded in the switches doc §3a. + +**Audit follow-ups closed. #40 composition is the next deliberate, supervised move +(gated: recompose-from-scratch on any step-cost change ⇒ all pieces finished first).** + +## Why this reframe + +The previous round optimised "the expensive phase of the current recipe" +(ratchet, ~60%). That produced a real recipe win — `ratchetCycles` 12→6 is +~20–38% wall with no quality loss (findings T-P5d) — **but recipe tuning only +reshuffles component *proportions*; it cannot address the core belief that TNT +is faster *per iteration*.** The framing decomposition left throughput as a +~1.4–2.3× same-machine residual (32-bit lower bound) but never localised it to a +component. + +A winning search combines scoring + TBR + sectorial + ratchet (+ fuse/drift) in +proportions that vary by dataset. We can only responsibly *compose* +"proven-at-limit" components once each has independently been (a) profiled to its +own performance ceiling and (b) raced head-to-head against TNT's equivalent **in +isolation**. + +## STEP 0 (BLOCKS the build) — size the prize on 64-bit first + +Advisor course-correction: do NOT build the shared-start harness until the +per-iteration gap is pinned on the hardware that counts. "TNT faster per +iteration" = rearrangements/second; `framing.R` already has both rates on 32-bit +(thr 1.36–2.30). The only missing number is **64-bit TNT rate on Hamilton**, and +it needs no new harness — the existing `bench_tnt_headtohead.R` budget mode gives +it. It *sizes the whole program*: +- 64-bit TNT ≈ 1.3× our rate ⇒ gap is efficiency/quality; swing there, the + component build is NOT worth it. +- 64-bit TNT ≈ 4–9× ⇒ throughput is the prize; the component build is justified. + +Step-0 deliverables (Hamilton, 64-bit TNT, EW-fitch, ≥3 seeds, several sizes): +1. **Score at equal wall** (budget mode) — unit-free bottom line: does TNT beat + TS at equal 64-bit wall, and by how much? +2. **rearr/sec for both** on the same node — the throughput ratio (mind the unit + caveat below). +3. **Confirm gapB=0 at FULL budget** vs the *same* well-configured TNT — the + premise that the residual is throughput, not quality. (Ablation's 2/3 Zanol + misses were weak-budget; re-test at full budget.) + +Only if Step 0 shows a large 64-bit throughput gap do we proceed to build. + +## Two questions per component + +1. **AT-LIMIT?** VTune the component's hot path in isolation. At the + AVX2 / compiler / memory-bandwidth ceiling, or is there a real optimisation? +2. **vs TNT per-iteration?** Feed the *same starting tree* to both engines; run N + iterations of ONLY that component; compare: + - **score reached** — bitness-independent ⇒ correctness/quality of the + component's neighbourhood (does our TBR reach the same local optimum TNT's + does, from the same start?). + - **count examined** — rearrangements / candidates; bitness-independent ⇒ + efficiency (how many moves to get there?). + - **wall** — 32-bit local TNT = directional + LOWER bound only; 64-bit + Hamilton = authoritative ⇒ THE "faster per iteration" test. + +## Components, isolation entries, metrics + +| component | TS isolated entry | TNT isolated invocation | bitness-free metric | +|---|---|---|---| +| scoring (Fitch EW) | `bench_score_micro.R` / `std::chrono` | `length;` (no loop exposed — hard to race fairly) | ns/score; prior AT-LIMIT T-S3b/c | +| **TBR** (keystone) | `ts_tbr_diagnostics(tree=…)` | `tread ; bbreak=tbr;` (NO xmult) | score@opt, rearr-to-opt | +| sectorial | ✅ `rss_search` instrumented directly (no export needed) | `sectsch` settings, no ratchet/fuse (see [[tnt-sectorial-recipe]]) | score, #sectors | +| ratchet | ✅ `ts_ratchet_search` (exported, RcppExports.R:135) | ✅ `ratchet=iter N;` via **STDIN pipe** (runfile-arg → curses, fails headless) | score@iters, wall (examined-count N/A — `RatchetResult` lacks it) | +| fuse / drift | later | `tfuse` / drift flags | later | + +## Shared-start plumbing (NEW — does not exist in bench_tnt_headtohead.R) + +- Build ONE start tree in TS (e.g. Wagner via the existing builder), per + (dataset, seed). +- TS side: feed via `MaximizeParsimony(tree=…)` or the component diagnostic's + tree argument (`ts_tbr_diagnostics` already takes a tree). +- TNT side: `tread "(newick);"` then the single-component command. **Verify TNT + tree-read format + taxon-index↔name mapping matches `WriteTntCharacters` + ordering** (off-by-one taxon maps would silently invalidate the race). +- Reuse the existing TNT plumbing: `WriteTntCharacters`, alphabetic `.run` + filename, `iconv(…sub="")`, regex on "Best score:" / "Total rearrangements + examined:" (bench_tnt_headtohead.R:56–87). + +## Ordering + +1. **TBR keystone** — shared-start race vs `bbreak=tbr`. Most decisive for + "faster per iteration", and TBR underlies sectorial + ratchet, so its + per-candidate cost propagates everywhere. Do FIRST. +2. **Scoring** — largely settled AT-LIMIT (Round 3); confirm the cross-program + angle only if a fair isolation is feasible. +3. **Sectorial ‖ ratchet** — composition overhead + candidate-selection, on top + of whatever TBR turns out to cost. +4. **Compose** dataset-size-tailored recipes from the proven-at-limit elements + (step (x)). + +## Caveats / gates + +- Local TNT is **32-bit** ⇒ wall directional + lower-bound only; authoritative + wall race = **Hamilton 64-bit**. Counts + scores are bitness-independent and + valid locally. +- Commensurability: TNT "Total rearrangements examined" ≈ our `++n_evaluated` + (confirmed prior, headtohead_phase0). Pin the unit for ratchet "iterations" + and sectsch "sectors" before racing those. +- **EW-fitch only** — NA/inapplicable path is owned by another agent. +- A finding that a component is AT-LIMIT only counts with a micro-bench; a + cross-program score/count parity only counts with the *same* start tree + verified fed to both. +- **Separate throughput from acceptance policy.** TS TBR and TNT `bbreak` differ + in first-vs-best-improvement, clip order, and accept-equal ⇒ they reach + *different* local optima from the same start for reasons that are neither bugs + nor throughput. Report throughput (rearr/sec or rearr-to-fixed-target) and + quality (*which* optimum) on separate axes; pin TNT `bbreak` settings + explicitly, never inherit xmult defaults. +- Stochastic components (ratchet, sectorial) → race as seed *distributions* + (≥3 seeds), not point comparisons. +- Don't race scoring cross-program — TNT exposes no scoring loop. If the TBR + race shows equal rearrangements but slower TS wall, scoring throughput is + implicated for free. diff --git a/dev/plans/2026-06-19-na-directional-feasibility.md b/dev/plans/2026-06-19-na-directional-feasibility.md new file mode 100644 index 000000000..80427697d --- /dev/null +++ b/dev/plans/2026-06-19-na-directional-feasibility.md @@ -0,0 +1,145 @@ +# Cheap directional incremental scoring for NA — feasibility analysis + +**Question (project lead, 2026-06-19):** design a cheap directional incremental +scoring approach for the inapplicable (NA) path — the EW/IW kind that makes +candidate evaluation O(1) after O(n) preprocessing — or prove why it can't be +done. + +**Bottom line.** The *exact* O(1) "EW additive" directional scan is provably +**not available** for NA, for a concrete, code-confirmed reason (below). A richer +fixed-size-message DP that recovers O(1) is **not fundamentally impossible** (NA +parsimony is a linear-time tree DP), but it is **unsolved and research-grade**: +the one directional NA message that exists today (`fitch_na_indirect_length`) is +deliberately *approximate*, precisely because the exact context is non-local. The +**practical** cost lever is therefore incremental *exact* rescore — O(affected), +not O(1) — building on machinery that already exists (`fitch_na_dirty_*`). This is +the recommended direction for task #18. + +--- + +## 1. Why EW/IW directional is O(1) per candidate + +For equal/implied weights the per-edge cost is **2-local**: clipping edge (u,w) +and rejoining, the length is + + len(A) + len(B) + join(prelim_A, prelim_B) + +where `len(A)`, `len(B)` are constants and `join` depends only on the two +preliminary state-sets at the cut. Two facts make the all-candidates scan cheap: + +1. **2-locality** — the cost the candidate adds is a function of just two + fixed-size sets at the broken edge. +2. **Exact, one-pass from-above** — `compute_from_above` yields, at every node, + the prelim set of "everything outside" that node, so the presented set at + *any* rerooting is `join(from_above[sc], prelim[sc])` in O(1). + +`try_root_edge_moves` (EW) uses exactly this: `base_split = best_score - rootjoin` +is constant, and each candidate costs `base_split + fitch_join(stateL, stateR)`. + +## 2. Why the EW additive trick is provably unavailable for NA + +Two independent obstructions, both visible in the code: + +**(a) The NA per-node step is NOT 2-local.** Pass 3 of the Brazeau three-pass +counts a step at a node from (`ts_fitch_na_incr.h:359`): + + needs_step = l_act & r_act & ~(ss_app & any_isect) + +where `ss_app` comes from `final_` (the Pass-2 *uppass*, i.e. whole-tree context) +and `l_act/r_act` from `subtree_actives`. The count depends on the global +applicability resolution (which tree regions are "applicable"), not just two +prelim sets at the cut. The code states this directly: the additive split +"does NOT hold for IW (concave) or NA (3-pass)" (`ts_tbr.cpp`, `try_root_edge_moves` +header), and routes NA to apply+`full_rescore` (`try_root_edge_moves_rescore`). +This is *why* the root-edge completeness fix (2026-06-19) had to use rescore. + +**(b) Structural reason — the region term is not a 2-local edge cost.** The NA +score includes the number of *applicable regions* (maximal connected applicable +components). This is a global connectivity functional. It is not a sum of +symmetric per-edge costs: a single applicable region with 3 inapplicable +neighbours (3 boundary edges, 1 region) and three separate applicable leaves +(3 boundary edges, 3 regions) have identical boundary-edge multisets but +different region counts. So no fixed *symmetric* cost matrix on {states, ⊥} +reproduces the term, and the EW "constant base + 2-local join" form cannot +represent a quantity that changes with global region structure as the halves are +rerooted. + +## 3. Could a richer fixed-size-message DP recover exact O(1)? + +Not impossible in principle. NA parsimony is a **linear-time tree DP** (down1 / +up1 / down2), and any linear tree DP is, by construction, a fixed-size-message +scheme — so an "all-rerootings" directional form is conceivable, with a message +richer than a single set: + +- message = cost table indexed by (boundary state, region-status bit), i.e. + "min internal cost of this fragment given its boundary resolves to state s and + its region is open/closed across the cut"; +- the region term *can* be charged locally in a **directed/rooted** formulation + (charge +1 at each applicable node whose parent is inapplicable = one charge + per region's top), which is fixed-size; +- combine at a join would re-optimise the boundary in O(table²). + +**But three things make this research-grade, not a quick win:** + +1. **The existing directional NA message is already approximate.** + `fitch_na_indirect_length` *is* the attempt at an O(blocks)/candidate NA + message. It approximates the candidate's context by reusing the **base tree's** + `final_` sets at the attachment nodes (`tree.final_[a_base]`, + `tree.final_[d_base]`) — exact for Fitch's from-above, but only a proxy for the + NA uppass, which is not reconstructable as a simple set. That a *sound* version + was not used (and the expensive exact sweep was built instead) is direct + evidence the exact O(1) message is non-trivial. +2. **Exact from-above is the crux.** EW gets exact context from one + `compute_from_above` pass. NA would need an exact *oriented* from-above message + for the down1/up1 algebra AND a region-aware combine; deriving and proving + these correct (especially that (down, up) messages are a sufficient statistic + for re-optimising across an arbitrary new join, including region merge/split) + is the open problem. +3. **Validation burden.** Any candidate design must match `exact_verify_sweep` + (now complete, post the 2026-06-19 root-edge fix) bit-for-bit on a corpus, plus + the oracle 0/N. The history here (T-300 "unresolved −3", the approximate + indirect) shows how easy it is to get subtly wrong. + +**Status: not disproven, but unsolved and high-risk. Do not attempt before the +cheaper lever below is exhausted.** + +## 4. Recommended lever for #18: incremental EXACT rescore (O(affected), not O(1)) + +The sweep currently does `apply_tbr_move + full_rescore` (full Passes 1+2+3, +O(n)) for *every* candidate. Two sub-levers, both reusing existing machinery: + +- **Pruning via the approximate scan.** Run `fitch_na_indirect_length` (cheap) + first and `full_rescore` only candidates it cannot rule out. **Precondition:** + confirm the approximation's *direction* — it is only a safe filter if it never + *over*-estimates the true improvement (i.e. it must not hide a real improver). + If it can under-estimate the resulting length, it can be used as an admissible + lower bound; if it can over-estimate, it cannot prune safely. Verify before use. +- **Localised Pass-3 delta.** `fitch_na_dirty_*` already does incremental Passes + 1+2 over the union of affected rootward paths (built for the SPR accept path). + The remaining O(n) is the Pass-3 step recount (full postorder). Extend the dirty + machinery to accumulate only the Pass-3 *delta* over affected nodes. **Caveat:** + the Pass-2 uppass propagates context from the changed path *down into off-path + subtrees*, so the affected set for an exact NA recount can exceed the path; bound + it carefully (this is exactly where the region term bites). + +Either reduces per-candidate cost from O(n) toward O(depth)/O(affected) — a large +win on the 74/88-tip datasets — without the directional-message risk. + +## 5. Validation harness (already in place) + +- `dev/benchmarks/tbr_oracle_na.R` (real data) and + `dev/benchmarks/tbr_oracle_na_small.R` (fast, high-N) — completeness 0/N. +- `tests/testthat/test-ts-na-complete.R` — pins the Zanol2014 start-#14 optimum. +- For any cost change: assert per-tree score equality vs the current + `full_rescore` path on a corpus (the cost change must be score-transparent), + then re-run the oracles. + +## 6. Recommendation + +1. Do **not** build the exact O(1) directional NA scan now — provably can't reuse + the EW additive form, and the sound richer-message version is research-grade. +2. Pursue **incremental exact rescore** for #18: pruning first (cheapest, verify + approximation direction), then localised Pass-3 delta on top of + `fitch_na_dirty_*`. +3. Keep `exact_verify_sweep` (now complete) as the exact ground-truth oracle that + any faster path is validated against. diff --git a/dev/plans/2026-06-20-fuse-drift-isolation.md b/dev/plans/2026-06-20-fuse-drift-isolation.md new file mode 100644 index 000000000..fe348d707 --- /dev/null +++ b/dev/plans/2026-06-20-fuse-drift-isolation.md @@ -0,0 +1,278 @@ +# Fuse / Drift component isolation (task #52, component-isolation slot 4) + +**Mission:** close TreeSearch's per-iteration wall gap to TNT 1.6 on equal-weights +(EW) Fitch. Quality (gapB) closed; throughput residual ~1.3-2.3× concentrated on +heavy multistate (Zanol/Zhu). NA/IW out of scope. Fitch via `-`→`?`. + +**Where this sits:** TBR is CLOSED on every gate + every algorithmic thread (kernel +at-limit T-P5l / precompute dead M46/T-P5j / scaffolding below-floor T-P5m / +middle-level at-best T-P5p / lever-c dead-by-proof T-P5q). Scoring CLOSED (T-P5l). +Sectorial gate-1 done by the sectorial agent (T-S6a-e: ~96% of isolated sectorial +wall IS the inner `tbr_search` ⇒ at-limit-by-inheritance; ~2.8% byte-identical +micro-levers banked). Ratchet recipe banked (12→6, T-P5d). **Fuse + Drift are the +last untouched component slot** (user-ordered after lever-c, 2026-06-20). Composition +#40 is GATED LAST (user: recompose-from-scratch on any step-cost change ⇒ finish all +pieces first). + +## Two gates per component (the program's contract) +1. **AT-LIMIT?** isolate the component's hot path; is the fuse/drift-SPECIFIC work at + the AVX2/compiler/bandwidth ceiling, or is there a real lever? (chrono decomposition, + like the sectorial agent's `TS_SECT_TIMING`.) +2. **vs TNT per-iteration?** shared-start race: score@opt, count-examined, wall + (32-bit local = lower bound; Hamilton 64-bit = authoritative). + +## Recon findings (2026-06-20, this session) +- **No `getenv` / hot-path CRT in ts_fuse.cpp or ts_drift.cpp** (Grep). The + getenv-class win (13-26% TBR T-P5n; ~22% sectorial T-S6d) is ABSENT here — clean. +- **Wiring (ts_driven.cpp):** drift = outer-cycle step 5 (`drift_search`, gated + `drift_per>0`, ts_driven.cpp:435-445) followed by a TBR polish (:544). Fuse = two + paths: intra-replicate `tree_fuse(result.tree, ds, *pool, fp)` gated + `params.intra_fuse && pool->size()>=1` (:568-587) + inter-replicate every + `fuse_interval` (:954-981). `tree_fuse` runs `tbr_search` internally after each + improvement round (the at-limit kernel) ⇒ STRONG at-limit-by-inheritance prior. +- **Default-share is SMALL:** T-P5c phase table had fuse 0-2.5%, drift not prominent; + both are opt-in / preset-specific, NOT default-mission-wall hogs. So the gate-1 + prize is byte-identical micro-levers + the crash fix, not a big throughput lever. +- **CRASH-FIX PREREQUISITE (verified still ABSENT on cpp-search):** `intraFuse=TRUE` + SEGFAULTS on >64-tip data (Zanol/Zhu/Giles — the heavy mission datasets). + `reroot_at_tip0(recipient)` is called ONCE pre-loop (ts_fuse.cpp:332), NOT per round + (round loop :357 has no re-root); `replace_subtree` (:221) has no + `r_rest.size()!=d_rest.size()` guard. Root cause + tested fix in worktree + `TreeSearch-nonclade` (feature/nonclade-sectors): per-round re-root + size guard, + 9/9 fuse tests, regression test at 80 tips. See MEMORY [[fuse-reroot-segfault]]. + ⇒ Fuse can only RUN on ≤64t (Wortley 37t) today; gate-2 on heavy data + fuse being a + real mission lever both REQUIRE this port. + +## Plan (4 phases) +- **P1 — gate-1 code analysis (no build):** characterize fuse/drift-specific vs + inherited-TBR split; hunt byte-identical micro-levers (allocs-in-loops, redundant + rebuilds, no-op round-trips à la T-S6c sectorial `ras_starts==1`); verify the + crash-fix soundness + produce a port spec. [workflow] +- **P2 — crash-fix port:** apply per-round re-root + `replace_subtree` size guard on a + fresh worktree off cpp-search (named files only, NO shared-branch commit); build + per-agent; run `test-ts-fuse.R` + a >64t no-crash repro (Zanol intraFuse reps2). +- **P3 — empirical gate-1:** chrono decomposition (gated instrument) on small data + (Wortley ≤64t for fuse; drift wherever it runs) confirming the at-limit-by- + inheritance split + any micro-lever wall delta (local iterate-tier, ~seconds). +- **P4 — gate-2 race:** shared-start vs TNT `tfuse` / drift flags → Hamilton 64-bit + (needs P2). score@opt + count + wall as seed distributions. + +## Guardrails (unattended) +Per-agent `R CMD INSTALL` only (never load_all for perf/correctness); builds + ~30s +targeted tests stay LOCAL; heavy/parallel + 64-bit races → Hamilton SLURM (durable +per-cell output); code changes on a WORKTREE, stage named files only, NO commit unless +asked, never touch cpp-search/main broadly ([[concurrent-session-git-hazard]]); no +thread_local hot-path scratch (MinGW emutls); UK 'ize'; `return x;` C++; TreeTools over +ape. ts_sector.cpp belongs to the sectorial agent — do not edit. + +## PROGRESS LOG + + +### 2026-06-20 — P0 — Oriented; recon done; gate-1 analysis workflow launched; crash fix confirmed absent +Recon above. Next: P1 analysis workflow + P2 crash-fix port worktree. + +### 2026-06-20 — P2 DONE — crash fix ported + validated + committed (worktree branch) +Diffed cpp-search ts_fuse.cpp vs nonclade: ONLY the 2-hunk fix differs (per-round +`reroot_at_tip0` after `++result.n_rounds`; `replace_subtree` size guard) — confirms +ts_fuse.cpp otherwise byte-identical. test-ts-fuse.R diff = purely additive (the +80-tip >64 regression test). Ported on isolated worktree +`C:/Users/pjjg18/GitHub/worktrees/TreeSearch/fuse-reroot-port` (branch +`claude/fuse-reroot-port` off cpp-search 8c57c2ec); built per-agent (`.agent-fuse`, +ccache, exit 0); **22/22 fuse tests PASS incl. the 80-tip regression** (NOT_CRAN=true). +Committed `da21f5dc` (2 named files, NOT pushed, NOT on cpp-search). **The fix is a +real CORRECTNESS fix (intraFuse segfaults on all >64t mission data) and should be +merged to cpp-search by the supervisor** (cherry-pick da21f5dc or the 2 hunks). + +### 2026-06-20 — P1 (drift independent read) — drift is at-limit-by-inheritance, CONFIRMED by structure +Read ts_drift.cpp end-to-end: it is the TBR kernel DUPLICATED — `drift_collect_main_edges` +/`drift_collect_subtree_edges`/`drift_fitch_join_states`/`drift_compute_from_above`/ +`drift_apply_tbr_move` are all "mirrored from ts_tbr.cpp", and `drift_phase` is a +tbr_search inner loop with an AFD/RFD accept rule; `drift_search` (cycles) calls +`tbr_search` DIRECTLY for the equal-score (:760) and convergence (:772) phases. So +drift = TBR + accept-rule ⇒ throughput rides the now-closed TBR kernel ⇒ +**at-limit-by-inheritance** (same verdict as sectorial T-S6a). Drift-SPECIFIC code = +the AFD/RFD accept logic + `drift_full_rescore` (full O(N) rescore on accept/decision, +bounded by ≤max_drift_changes accepts) — no getenv, no obvious non-inherited lever. +NB the drift_* duplication is a MAINTAINABILITY smell (copies of TBR code), not a perf +lever. Awaiting P1 workflow for the byte-identical micro-lever hunt + fuse split. + +### 2026-06-20 — P1 DONE + a STANDOUT QUALITY FINDING (bigger than the whole perf surface) +Gate-1 workflow verdict: **fuse + drift = AT-LIMIT-BY-INHERITANCE** (~85-90% inherited +kernel by structure) ⇒ gate-1 now CLOSED across ALL FOUR components (scoring, TBR, +sectorial, fuse/drift). Byte-identical micro-levers found are all sub-0.1% mission +(below sectorial's 2.8%): replace_subtree unordered_map→flat-vector, new_local_cost +hoist, drift_compute_from_above scratch hoist — bank as hygiene, not the point. Tier-2 +(build_postorder_prealloc + elide-triple-rescore) = byte-identical-BY-ARGUMENT only +(touches the RFD-accept local_cost MASK) ⇒ DEFER (sub-floor, needs mask-equality A/B). + +**THE FINDING (confirmed by code, NOT a perf lever — a SEARCH-QUALITY defect):** the +perturbation/secondary engines score candidate moves with `fitch_indirect_length_bounded` +& friends = the **union-of-finals (`final_[A]|final_[D]`) approximation that UNDERCOUNTS** +(ts_fitch.h:118-126 explicitly: edge_set directional `_cached` is "the CORRECT +replacement ... which undercounts"; ts_tbr.cpp:1608-1612 "Exact directional cost ... +replacing the union-of-finals approximation"). The directional fix (a PROVEN real +quality bug: oracle 23/40→9/60 [[tbr-rooted-vs-unrooted]]; Wagner +30% +[[wagner-insertion-cost-bug]]) was ported to the 3 MAIN kernels — `ts_tbr.cpp`(EW/IW +`_cached`), `ts_wagner.cpp`, `ts_sector.cpp`(#27) — but **NOT** to the secondary engines: +- `ts_drift.cpp` :492/499/547/555 (EW+IW) — drift candidate ranking + AFD-gate. +- `ts_prune_reinsert.cpp` :439/448 (EW). +- `ts_search.cpp` :358/365 (EW/IW). +- `ts_temper.cpp` :291 (NA+IW — NA out of scope). +- (tbr_search :1589/1596 NA path also `_bounded`, but NA is another agent's scope.) +Undercounting mis-ranks candidates ⇒ these engines perturb toward mis-scored targets ⇒ +DEGRADED escape efficacy. Mission relevance hinges on (a) which are LIVE on the default +MaximizeParsimony path + their share, (b) magnitude of the discrepancy, (c) oversight vs +deliberate cheap-approx tradeoff (fixing = drift must pay the directional precompute the +way tbr_search does). Launched workflow `fuse-drift-scoring-audit` to settle these. If +real+live+impactful: scope+implement the directional fix per path on a worktree (mirror +tbr_search / #27), local-validate, queue a Hamilton QUALITY A/B; do NOT land unvalidated +(trajectory change). This is the anti-satisficing lead ([[tnt-outperformance-is-diagnostic]]). + +### 2026-06-20 — SCOREAPPROX audit DONE — REAL-BUT-OFF-DEFAULT-PATH (not a recorded-quality bug) +Workflow `perturbation-scoring-audit` (3 lenses + synth). VERDICT: the discrepancy is +REAL (the `_bounded` union-of-finals = the proven Wagner-+30% / TBR-oracle-23→9 +undercount; it mis-ranks candidates AND shifts the drift AFD/RFD accept band), BUT it +is **NOT a recorded-quality bug on any path**. DECISIVE RECONCILIATION (code-read): +every secondary engine uses `_bounded` ONLY to RANK a perturbation/start, then +EXACT-reconverges via `tbr_search`/`nni` (`_cached`) and KEEPS only on STRICT +improvement (prune ts_prune_reinsert.cpp:558-580; anneal ts_driven.cpp:482-486; drift +ts_drift.cpp:760-777) ⇒ a mis-rank only WASTES A CYCLE; the recorded optimum is always +produced by the exact kernels. So gapB=0/efficiency≈1 (measured ~70t) is NOT +contradicted, and this is **NOT the throughput gap's cause**. +- **Live-path map:** drift=opt-in (0% default); **prune_reinsert=preset-only, AUTO in + `large` (≥120t), cycles=5L** (MP.R:224) — the ONLY auto exposure; temper=preset-only + (large, annealCycles=1L, but a DEFENSIBLE tradeoff — 1 edge/step, non-amortizable — + DO NOT convert); spr_search=opt-in (sprFirst=FALSE everywhere). gapB=0 was never + established for the large preset (≥120t) ⇒ prune_reinsert there is "live but untested". +- **EXCEPTION (honest):** spr_search (ts_search.cpp:365) is a HILL-CLIMBER w/ + single-best verify ⇒ a mis-ranked-away improver is SILENTLY MISSED (the real harm + mechanism) — but OFF every preset; ownership uncertain (confirm vs legacy before edit). +- **DOC BUG:** ts_fitch.cpp:385-391 comment "union exact; intersection overcounts" is + BACKWARDS (authoritative header ts_fitch.h:118-126 = union UNDERCOUNTS). Byte-identical + one-line fix; worth landing to stop the misconception re-spawning. +- **FOLLOW-UP (task #53, gated, throughput-NEGATIVE so NOT assumed-good):** (1) fix the + backwards comment; (2) cheap local FLIP-PROBE — enable pruneReinsertCycles on a small + dataset, compute `_cached(edge_set[D])` alongside `_bounded` at ts_prune_reinsert.cpp: + 439/448, log value-disagreements + argmin/best-edge flips; if flips≈0 → CLOSE with no + port; (3) only if material flips → port to `_cached` (mirror #27 build_ras_sector: + one compute_insertion_edge_sets per dropped tip, reused over DFS edges) on a worktree + + time-matched Hamilton A/B (≥120t, ≥10 seeds; ship ONLY if neutral-to-better, since it + adds the ~30%-EW precompute). drift/spr_search ports = defer to opt-in/human. +- **SCOPE GUARDS:** NA scoring paths OUT OF SCOPE (other agents). No measured quality + loss on ANY path ⇒ do NOT over-claim; not the throughput gap. anneal stays `_bounded`. + +### 2026-06-20 — #52 DISPOSITION — fuse/drift gate-1 CLOSED; gate-2 = Hamilton-confirmatory +Gate-1 (AT-LIMIT): CLOSED for fuse + drift (at-limit-by-inheritance) — completes gate-1 +across ALL FOUR components. Crash fix landed (worktree da21f5dc, flagged for merge). +Byte-identical micro-levers found but all sub-0.1% mission (bank as hygiene, optional). +Gate-2 (TNT race): for fuse it is intrinsically awkward (fuse needs a diverse POOL, not +a single shared start) and throughput is inherited from the closed TBR kernel ⇒ a race is +confirmatory; the meaningful fuse/drift question is RECIPE value = composition #40 (gated). +Recommend: gate-2 race for fuse/drift is LOW-priority Hamilton-confirmatory, NOT a blocker. + +### 2026-06-20 — SCOREAPPROX ELEVATION (read ts_prune_reinsert.cpp:412-468) + RATCHET gate-1 +**prune_reinsert is STRONGER than the synth's "wasted cycles":** `expand_and_reinsert` +does INCREMENTAL GREEDY WAGNER reconstruction (wagner_incremental_rescore per tip, :467), +scoring each candidate edge with `_bounded` (:439/448) on a PARTIAL tree = the +CONSTRUCTION regime where the union undercount was measured at +30% (the original Wagner +bug), and it is the VERBATIM greedy-placement pattern Wagner/build_ras_sector (#27) were +fixed for — the sibling was missed. Strict gate still protects the RECORDED score, but +hampered reconstruction ⇒ prune_reinsert escapes LESS effectively ⇒ the large preset +(≥120t, the only auto path) may reach worse optima ⇒ a likely real LARGE-TREE EFFICACY +loss, not just wasted cycles. Flip-probe subtlety flagged in #53 (exact `_cached` needs a +current `prelim` downpass; incremental Wagner maintains `final_`). Recorded in #53 (do NOT +rush a probe that could give false flips). **RATCHET gate-1:** recon (ts_ratchet.cpp) = +NO getenv; ratchet = `perturb_upweight` (cheap O(chars) reweight) + `tbr_search` +(:153/203/209, the closed kernel) ⇒ AT-LIMIT-BY-INHERITANCE, recipe banked (12→6 T-P5d). +⇒ **gate-1 (AT-LIMIT) now COMPLETE across ALL components** (scoring/TBR/sectorial/fuse/ +drift/ratchet). Remaining isolation work = gate-2 TNT races (Hamilton-confirmatory: +sectorial=other agent, ratchet+fuse/drift low-priority) + #53 + composition #40 (gated). + +### 2026-06-20 — #53 RESOLUTION — backwards comment fixed; prune_reinsert Δ-probe DONE; port PREPARED; A/B composition-gated +**(1) DOC BUG FIXED + LANDED on cpp-search** (8671fdaa): ts_fitch.cpp:385-391 backwards +comment ("union exact; intersect overcounts") corrected to match header :118-126 (union +UNDER-counts; the directional edge_set is exact). Doc-only, mission-safe. + +**(2) Δ-PROBE (not flip-count — advisor: tally exact-suboptimality, not edge-identity):** +gated `-DTS_SCOREAPPROX_PROBE` in expand_and_reinsert, non-perturbing (production still +inserted at the `_bounded` choice). Per placement tallied Δ = exact_cost(E_bounded) − +min_E exact_cost(E), exact scorer = compute_insertion_edge_sets + fitch_indirect_length_cached, +no cutoff. `prelim` confirmed current (wagner_incremental_rescore maintains it) + in-tree +fully binary from root ⇒ precompute safe. Result on Zanol (forced pruneReinsertCycles): +**~62% of placements strictly worse, mean ~6 steps, max 37, ~48% greedy-regret SHARE** +(bounded_exact_sum 6406 vs min_exact_sum 4315). Corroborates the validated +30% Wagner bug. + +**(3) PORT PREPARED + VALIDATED (worktree claude/scoreapprox-probe, 41b0d237; NOT cpp-search):** +swapped the two `_bounded` calls for the exact `_cached`+edge_set (mirror ts_wagner.cpp:487). +After port the probe reports **Δ=0 at every placement** (production == exact argmin). Tests: +prune-reinsert 44/0, drift 22/0, ratchet 17/0, tbr 28/0. + +**(4) PATH-RELEVANCE KILL for the heavy A/B (the decisive gate):** prune_reinsert auto-enables +ONLY at nTip≥120 (`large` preset, MP.R:249). **NO mission dataset reaches 120t** — full +inapplicable.phyData roster max = Dikow2009 88t; Zhu2013 75t, Zanol 74t, Giles 78t (all +`thorough` or smaller). So this path runs on ZERO default mission searches ⇒ the 48% is +greedy-regret SHARE on a config the mission suite never triggers, NOT a wall-clock +opportunity. blame: `_bounded` = afbf531f (2026-03-27, original T-266) PREDATES the June +directional fix ⇒ a genuine MISS, not a deliberate large-N tradeoff. `large` polish is NNI +(weaker than TBR) ⇒ regret survives more ⇒ fix WOULD matter at ≥120t. + +**(5) DISPOSITION:** land + time-matched A/B (needs a ≥120t dataset + the `large`-preset +budget tradeoff, since the exact scorer ADDS an O(N·blocks·9-states) precompute the bounded +path skips) = **COMPOSITION #40** (user: composition waits until all pieces finished). Port +is ready + cost-characterizable for that phase. Component made best-known-correct in +isolation; the enable/wall-clock decision is recipe-level. #53 investigation CLOSED. + +**(6) spr_search loose-end RESOLVED (the T-F1 "could silently miss" exception):** +ts_search.cpp `spr_search` (the Fitch SPR, :197) uses the bounded scorer (:365) BUT (a) +fires ONLY when sprFirst=TRUE — FALSE in every preset (off the default path); (b) accepts +ONLY on EXACT `full_rescore` improvement (:388-402) ⇒ can never false-accept, recorded +score always exact (gapB=0 preserved); (c) is a one-shot SPR WARMUP immediately followed by +exact `tbr_search` (ts_driven.cpp `if(!nni_wagner && spr_first){spr_search;} ... tbr_search`) +which re-explores and catches any improver a bounded mis-rank missed. ⇒ the silent-miss is +real-in-principle but MOOTED; porting adds the per-clip precompute for ~zero benefit. NO +ACTION. Remaining bounded sites all benign: drift (opt-in, rank-then-reconverge, T-F1), +temper (preset-only defensible tradeoff, T-F1), ts_rcpp.cpp:2339 (standalone export, not +the recipe). **Scoring-approximation sweep now COMPLETE across the whole search.** + +### 2026-06-20 — PHASE-0 CONNECTIVE TISSUE — CLOSED, no addressable production fat +Read of the driven-search orchestration loop (ts_driven.cpp). Full `score_tree` +(O(N·chars)) call inventory at the DEFAULT verbosity (`verbosity=1L`, MP.R:505): +- **All per-phase score prints are `verbosity>=2`-gated** (XSS/RSS/CSS/ratchet/post-sect/ + NNI/drift/SA/PruneRI/TBR/fuse, ts_driven.cpp:249-588) ⇒ DO NOT fire at default v=1. +- **Interrupt/timeout exit branches** (257/269/299/307/431/505/539/563) ⇒ run once on exit. +- **Per-outer-cycle, un-gated:** `score_before_cycle` (:224) + `score_after_cycle` (:594) + for the convergence/reset check = 2 full rescores/cycle; `score_before_cycle`(N+1) ≡ + `score_after_cycle`(N) (tree unmodified between :594 and next :224) ⇒ one is REDUNDANT. +- **Final:** `result.score = score_tree` (:617) once per replicate. +A full score_tree on Zanol ≈ O(74·210·9) ≈ 140K ops ≈ µs; ~1–few outer cycles/replicate +(outerCycles=1 in `large`) ⇒ total ≈ **0.001% of wall** (seconds of phase work dominate; +score_tree was NOT in the T-P5o hotspot list — consistent). **Step-switching:** each phase +owns/maintains its own prelim/final_ incrementally; the only orchestrator-level state +rebuild is intra-fuse `build_postorder()+reset_states()` (:581-582, preset-only, 1/cycle). +R/C marshalling already T-P5o'd (R.dll 12% = amortizable GC/glue + one-time LoadLibraryA, +startup-inflated by the tiny profiling workload). **VERDICT: Phase-0 AT-LIMIT** — the one +redundant `score_before_cycle` is a sub-floor (~0.001%) bit-identical micro-bank, NOT worth +the convergence-logic risk. This closes the last undone NON-GATED, non-other-agent aspect of +the component-isolation plan. Remaining: gate-2 races (Hamilton-confirmatory; sectorial=other +agent) + composition #40 (gated, where the addressable wall now lives: orchestration / T-S6e). + +### 2026-06-20 — bit-packing reopen CLOSED + cherry-pick build-check + Hamilton-KPI BLOCKED +- **ns=9 representation/bit-packing reopen CLOSED analytically (T-P5r, advisor-gated, no build):** + transposed bitset already bit-dense (9 state-words × 64 patterns = 0.14 op/pattern, 4 states/ + AVX2 instr); states-per-word packing SERIALIZES patterns → strictly worse at 210 patterns; + the scalar/representation reopen is **ns≤4 only**, deader at ns=9 ⇒ residual ~2× heavy- + multistate is a genuine ACCEPTED CONSTANT FACTOR, no representation lever. (The T-P5p + "UNPINNED" tag was the tell — a 21-agent audit had found no concrete scheme.) +- **Cherry-pick build-check PASSED:** clean detached-worktree build of cpp-search HEAD + (ac8e808a fuse fix + 8671fdaa comment) = INSTALL exit 0; fuse 22/0, tbr 28/0, prune-reinsert + 44/0. No stale-object ABI issue ([[stale-object-abi-gotcha]] cleared). Shared branch safe. +- **Hamilton mission-KPI re-measurement (advisor's highest-value non-gated item) — BLOCKED, + FLAGGED FOR USER:** the stale TS-vs-TNT wall gap is worth refreshing (predates getenv ~20-26% + + ratchet 12→6 ~20-38%, which shifted the phase mix). BUT a clean dispatch is blocked: the + **ratchet 12→6 flip is UNCOMMITTED in the shared working tree** (R/SearchControl.R wt=`6L`; + origin/cpp-search AND local HEAD both =`12L`), alongside `M` R/MaximizeParsimony.R + + R/RcppExports.R — another session's in-flight work I must not touch/commit (concurrent-git- + hazard) and not authorized to push. Cloning origin → measures stale ratchet=12; transferring + the wt → bundles unowned multi-session WIP. ⇒ cannot define a clean reproducible code-state + unattended. NEEDS USER: commit the ratchet flip (it's a major banked lever sitting only in the + working tree — at risk of loss on any `git checkout -- .`) + authorize the Hamilton run. diff --git a/dev/plans/2026-06-21-search-switches-for-composition.md b/dev/plans/2026-06-21-search-switches-for-composition.md new file mode 100644 index 000000000..65d3c5e8d --- /dev/null +++ b/dev/plans/2026-06-21-search-switches-for-composition.md @@ -0,0 +1,188 @@ +# MaximizeParsimony search switches — reference for composition (#40) + +**Audience:** the agent composing dataset-tailored recipes (#40). +**Scope:** equal-weights Fitch parsimony (the mission objective; `m[m=="-"]<-"?"`). +NA/inapplicable and IW/XPIWE/profile knobs are listed but flagged out-of-mission. +**Authoritative source:** `R/SearchControl.R` (params + defaults), `R/MaximizeParsimony.R` +(presets + `.AutoStrategy`). Defaults below are the *formal* `SearchControl()` defaults +as of cpp-search `1284bdf2` (2026-06-21). + +**The one framing that should drive #40** (from the 2026-06-21 KPI, `dev/profiling/kpi-2026-06-21.md`): +quality is CLOSED (TS reaches the optimum ≥ TNT on every mission dataset; on Zanol TS is +the *only* reliably-1261 config). Every component is measured **at-limit**. So the wall gap +is **not per-component throughput — it is budget/composition**: the eye-catching 8–110× +KPI ratio is a *default-budget mismatch* (TS `default` runs a heavy search; TNT `xmult` +default runs a light one). #40's job is to spend the right amount of the right effort per +dataset class — **not** to make any single component faster. Recompose from scratch if any +step's cost changes. + +--- + +## 0. Mission dataset roster (what "per class" means here) + +| Dataset | tips | n_states | landscape | notes | +|---|---|---|---|---| +| Wortley2006 | 37 | mixed | flat-ish | small; `sprint`/`default` reach 480 in seconds | +| Giles2015 | 78 | **mixed** | structured | col-reduce helps (17%); reaches 670 | +| Zhu2013 | 75 | **mixed** | structured | col-reduce helps (9%); reaches 624 | +| Zanol2014 | 74 | **uniform ns=9** | hard/structured | the load-bearing case; reaches 1261; col-reduce ~0% | +| Dikow2009 | 88 | mixed | structured | roster max tips (still < 120 → never `large`) | + +**No mission dataset is ≥120 tips** — so the `large` preset and `pruneReinsert` auto-enable +(nTip≥120) never fire on the mission roster. The mixed-vs-uniform-`n_states` split is the +key discriminator for `TS_SECT_COLREDUCE` (below). + +--- + +## 1. Top-level `MaximizeParsimony()` arguments (budget + objective) + +| Arg | Default | When relevant to #40 | +|---|---|---| +| `strategy` | `"auto"` | The starting point. `auto` → `.AutoStrategy(nTip,nChar)` (§2). #40 will likely **override per class** rather than trust auto. | +| `control` | `SearchControl()` | The expert knob bag (§3). Pass a tuned `SearchControl(...)` here. `...` args to `MaximizeParsimony` also forward into the control. | +| `maxReplicates` | `96L` | **The dominant budget lever.** Wall ≈ replicates × per-rep cost. Most of the "8–110×" is here: TS keeps searching long after the optimum is hit. Pair with a stop criterion (`targetHits`, `consensusStableReps`, `perturbStopFactor`) so it *stops* once converged. | +| `targetHits` | `NULL` | Stop after the best score is independently re-found this many times. **The cleanest convergence stop** — set it (e.g. 3–10) to avoid burning budget post-optimum. Interacts with `perturbStopFactor`. | +| `maxSeconds` | `0` (off) | Wall cap. `0`=use replicate budget. For race-style/time-matched composition, set this; reserves `enumTimeFraction` (10%) for MPT enumeration. | +| `concavity` | `Inf` (EW) | `Inf` = equal weights = **the mission**. Finite = IW; `extended_iw`/`xpiwe_*` only matter then. Leave `Inf`. | +| `inapplicable` | `"bgs"` | NA handling — **out of mission** (EW converts `-`→`?`). Ignore. | +| `nThreads` | `1L` | Parallel replicates. >1 speeds wall but (a) RNG/repro differs, (b) a pre-existing NA parallel crash exists (nThreads≥2 on the NA path). For EW mission timing keep `1L` unless deliberately testing throughput. | +| `verbosity` | `1L` | `0` silent; `1` per-phase (production); `≥2` adds per-phase `score_tree` prints (measurable overhead — Phase-0 finding). Keep `0/1` for timing. | +| `tree` | — | Seed a start tree (shared-start races). | +| `constraint` | — | Topological constraints; clears `consensusConstrain`. Out of mission unless asked. | + +--- + +## 2. Strategy presets (the starting recipes) + auto-selection + +`.AutoStrategy(nTip, nChar)`: +- `nTip ≤ 30` → **sprint** +- `nChar < 100` → **default** (flat landscape; thorough is pointless — 0/6 benefited) +- `nTip ≥ 120` → **large** (scaled big-tree preset; **never fires on the mission roster**) +- `nTip ≥ 65` (and nChar ≥ 100) → **thorough** +- else → **default** + +| preset | ratchetCycles | xss/rss/css rounds | sectorMax | wagnerStarts | outerCycles/resets | fuseInterval | extras | +|---|---|---|---|---|---|---|---| +| **sprint** | 3 | 1/0/0 | 50 | 1 | 1/0 | 5 | tabu off; light — `nTip≤30` | +| **default** | **6** (was 12, T-P5d) | 3/1/0 | 50 | 3 | 1/**2** | 3 | `adaptiveLevel=TRUE` | +| **thorough** | 20 | 5/3/2 | 80 | 3 | 2/3 | 2 (acceptEqual) | `ratchetAdaptive`, `adaptiveStart`, ratchetMode=2 | +| **intensive** | 20 | 5/3/2 | 80 | **5** | 2/3 | 2 | opt-in only; +Wagner starts for hardest datasets (±1 tradeoff) | +| **large** | **12** (kept, T-179) | 3/2/1 | 100 | 1 (biased) | 1/0 | 3 | `annealCycles=1`, `pruneReinsertCycles=5`+NNI, biased Wagner; **≥120t only** | + +**Composition note:** the mission roster (37–88t, ≥100 patterns for the hard ones) auto-selects +**`default`** (Wortley, few chars) or **`thorough`** (Giles/Zhu/Zanol/Dikow). The proven headroom +is `thorough → default` on Zhu/Giles (same score, ~2× less wall) — but on Zanol the thoroughness +is **load-bearing** for the reliable 1261. #40's core question: *how far below `default` can each +class go without losing the reliable optimum?* + +--- + +## 3. `SearchControl()` switches, by component — with #40 relevance + +### 3a. TBR core +| switch | default | relevance | +|---|---|---| +| `tbrMaxHits` | `1L` | Equal-score trees held per TBR pass. `1`=fastest descent; thorough uses 3 (more plateau capture, slower). Raise only when MPT diversity matters. | +| `clipOrder` | `0L` (random) | **MEASURED (jobs `17533071`@20-rep + `17541277`@40-rep, 3 seeds) = a per-class TRADEOFF, NOT a safe global win.** `2L` (tips-first) is ~1.25× faster overall / ~26% fewer candidates, but it biases the *trajectory* (not byte-identical): **clean win on Zanol-class** (uniform ns=9 — 3/3 reach 1261, consistently ~1.5× faster at 40-rep); **quality tradeoff on Zhu** (loses +1 on 1 seed even at 2× budget — doubling reps did NOT recover it ⇒ not a budget artifact); **wall unstable on Giles** (one seed 60% *more* candidates). So enable `2L` **per-class on Zanol-type data**; do NOT apply blindly. Complements `TS_SECT_COLREDUCE` — clipOrder helps the uniform-ns case col-reduce can't, and hurts the mixed-state case col-reduce helps. N=3 ⇒ directional. | +| `tabuSize` | `100L` | TBR plateau tabu list. `0`=off (sprint). Larger = more plateau exploration, more memory. Marginal for EW; leave at preset. | + +### 3b. Starting trees (Wagner) +| switch | default | relevance | +|---|---|---| +| `wagnerStarts` | `1L` | Independent random-addition starts per replicate. `default`/`thorough`=3. `intensive`=5 helped the *hardest* datasets (Wortley −3, Zhu −2) but +1 on Zanol/Giles → **per-class**, not global. More starts = more basin diversity = more wall. | +| `wagnerBias` | `0L` (random) | `1`=Goloboff non-ambiguous priority, `2`=entropy. `large` uses `1` (near-optimal Wagner at 180t, saves restarts). For mission sizes random is fine; bias mainly pays at large t. | +| `wagnerBiasTemp` | `0.3` | Softmax selectivity for biased addition. Only matters if `wagnerBias>0`. | +| `nniFirst` | `TRUE` | NNI pass before SPR/TBR. Negligible ≤88t; **accelerates the Wagner descent at ≥100t**. Keep TRUE. | +| `sprFirst` | `FALSE` | SPR before TBR. Off-default; washed by TBR; benign. Leave FALSE. | +| `adaptiveStart` | `FALSE` | Thompson-sampling over start strategies. `thorough`/`intensive` use it. Needs several replicates to learn → **regresses at large-t/low-replicate**; helps multi-rep mid-size. | + +### 3c. Ratchet (the load-bearing perturbation; ~60% of full-EW phase wall) +| switch | default | relevance | +|---|---|---| +| `ratchetCycles` | **`6L`** | **The single biggest banked recipe lever** (12→6 = 20–38% wall, 0 quality loss, T-P5d). `large` keeps 12 (big-tree tradeoff). Ratchet is **load-bearing — do NOT drop to 0** except <~30t (truly-off ≠ TNT; gap is structural). #40 may tune per class (6 is provisional; a size grid will refine). | +| `ratchetPerturbProb` | `0.25` | Per-character perturbation prob. The perturbation *space* was NOT swept (the isolated race used production params). A scheme reaching the optimum in fewer cycles is an **open #40 question** (audit #55-adjacent). | +| `ratchetPerturbMode` | `0L` (zero-weight) | `1`=up-weight, `2`=mixed. `thorough`/`large` use `2`. | +| `ratchetPerturbMaxMoves` | `5L` | TBR moves per perturbation (`0`=auto). Short perturbation + many cycles (ratchet design). | +| `ratchetAdaptive` | `FALSE` | Adjust prob by escape rate. `thorough`/`large` ON. | +| `ratchetTaper` | `FALSE` | Taper prob as pool stabilizes (finer late exploration). Untested mission-wide. | +| `stallEscalateFactor` | `1.0` (off) | >1 escalates perturbation on cross-replicate stall (auto-discovers needed strength). A **runtime-adaptive alternative to hand-tuning** per class — worth a #40 trial on Zanol. | +| `adaptiveLevel` | `FALSE` (TRUE in `default`) | Scale ratchet+drift effort by hit rate. | + +### 3d. Sectorial (TNT's workhorse; ~30% of full-EW phase wall; 96% of *its* wall is `tbr_search`) +| switch | default | relevance | +|---|---|---| +| `xssRounds` / `rssRounds` / `cssRounds` | `3` / `1` / `0` | Exclusive / random / constrained sectorial rounds. The 3 run in **sequence**, each with its own trailing full-tree TBR → **a consolidation candidate** (T-S6e: fusing the 3 sequential trailing TBRs into one is a recipe redesign, needs broad e2e). Tune counts per class; sectorial is where TNT escapes via a diverse retained set. | +| `xssPartitions` / `cssPartitions` | `4` / `4` | Partitions (must be ≥1 — SIGFPE guard). thorough/large use 6. | +| `sectorMinSize` / `sectorMaxSize` | `6` / `50` | Clade-size window. thorough=80, large=100. TNT uses ~min(n/2,45). Bigger sectors = coarser moves, more per-sector cost. | +| `rasStarts` | `1L` | Per-sector RAS+TBR restarts. `3` (TNT-faithful) **closes the rss-ONLY gap (+7/+8→+1, wins time-matched)** but is **REDUNDANT in the full thorough pipeline** at mission sizes (Zanol/Zhu reach optimum at `1`, 60s). **Revisit for larger datasets / shorter budgets** where the full search can't converge. | +| `sectorAcceptEqual` | `FALSE` | Accept equal-score sector resolutions (plateau walking). For flat/NA landscapes; gated out at plateau for EW mission (#24). | +| `sectorMaxHits` | `1L` | Equal-length trees the inner sector TBR holds. Pairs with `sectorAcceptEqual`. | +| `sectorCollapseTarget` | `0L` (off) | Collapse a big sector into ~this many composite terminals (coarse skeleton; Goloboff-1999 reduced dataset). The **row-axis** reduction (cf. `TS_SECT_COLREDUCE` = column-axis, §4). Worth pairing for large sectors. | +| `postRatchetSectorial` | `FALSE` | Re-run XSS+RSS+CSS after ratchet (TNT-interleaved). Adds a full sectorial pass per ratchet — expensive; only if it earns score. | + +### 3e. Drift / annealing / NNI-perturb (alternative escapes) +| switch | default | relevance | +|---|---|---| +| `driftCycles` | `0L` (off) | Tree drifting (Goloboff). #25 added TNT-faithful drift for the +1 datasets. Off in all EW mission presets — a per-class escape to trial. `driftAfdLimit`/`driftRfdLimit` bound accepted suboptimal moves. | +| `nniPerturbCycles` | `0L` (off) | NNI-topology perturbation (complements weight-ratchet). **Measured 69% overhead, zero time-adjusted benefit (T-274)** — leave off unless a class proves otherwise. `nniPerturbFraction=0.5`. | +| `annealCycles` | `0L` (off) | PCSA simulated-annealing perturbation. Effective ≥100t (the `large` preset uses 1 cycle to replace drift). Below mission sizes, unproven. `annealPhases/TStart/TEnd/MovesPerPhase` shape the schedule. | + +### 3f. Prune-reinsert (T-266; a strong large-tree perturbation) +| switch | default | relevance | +|---|---|---| +| `pruneReinsertCycles` | `0L` (off) | Drop tips → restructure backbone → reinsert. **Auto-on only via `large` (nTip≥120) — never on the mission roster.** An **exact-scorer port is prepared** (worktree `claude/scoreapprox-probe 41b0d237`): `expand_and_reinsert` used a union-of-finals approximation (the #27 miss); the exact `fitch_indirect_length_cached` port is validated (Δ=0) and parked for #40 to land + A/B *if a class ≥120t enters scope*. | +| `pruneReinsertDrop` | `0.10` | Fraction dropped/cycle (≥3 tips, keep ≥4). | +| `pruneReinsertSelection` | `0L` (random) | `1`=instability, `2`=missing-data, `3`=combined tip selection. | +| `pruneReinsertTbrMoves` / `FullMoves` | `5` / `0` | Backbone / full-polish TBR budgets. | +| `pruneReinsertNni` | `FALSE` | NNI full-polish instead of TBR — **~5× faster at ≥120t**; `large` uses it (TBR polish was catastrophic at 206t/60s). | + +### 3g. Pool / fusing +| switch | default | relevance | +|---|---|---| +| `fuseInterval` | `3L` | Fuse pool trees every n reps. **AUDIT #55: fuse is DEAD WEIGHT on the >64t mission class** — fires (7×/run, ~70 exchanges) but **0 improvements** on Zanol/Zhu/Giles. Already effectively off (`poolSuboptimal=0`→pool size 1→fuse SKIPPED). **#40: drop fuse / raise `fuseInterval` on this class to reclaim the inter-replicate `score_tree`+`tree_fuse` overhead.** (Fuse may still matter at >88t / with a diverse pool — re-measure if the class changes.) | +| `intraFuse` | `FALSE` | Within-replicate fuse vs pool donors. Also 0 improvements in the #55 probe. | +| `fuseAcceptEqual` | `FALSE` | Accept equal-score fused trees (plateau). thorough/large ON. | +| `poolMaxSize` | `100L` | Max pool trees (≥1; segfault guard). | +| `poolSuboptimal` | `0` | Retain trees within N steps of best. **`0` ⇒ pool collapses to optimal-only ⇒ fuse rarely fires.** Raise (e.g. 5) ONLY if you want fuse/diversity to actually run — but #55 says fuse doesn't pay here, and a diverse pool is TNT's fuse fuel (untested as a TS lever). | + +### 3h. Stopping / outer loop (the budget governors — high-leverage for #40) +| switch | default | relevance | +|---|---|---| +| `consensusStableReps` | `0L` (off) | Stop when the strict consensus is unchanged for N reps. **A convergence stop** (3–5 typical). Pair with `targetHits`; stops at whichever fires first. | +| `perturbStopFactor` | `2L` | Patience: stop after consecutive non-improving reps exceed `(targetHits/hits)·nTip·factor`. Scales patience with progress. `0`=off. **The main "don't over-search" governor** — tune per class. | +| `outerCycles` | `1L` | Repeats of [XSS/RSS/CSS→ratchet→NNI→drift→TBR] per replicate. thorough/intensive=2. More = TNT-style interleaving, more wall. | +| `maxOuterResets` | `0L` | Improvement-triggered resets of the outer counter (`-1`=unlimited). `default`=2, thorough=3. Lets a productive replicate keep going. | +| `enumTimeFraction` | `0.1` | Fraction of `maxSeconds` reserved for MPT enumeration. `0`=disable reserve. Only matters with `maxSeconds>0`. | +| `consensusConstrain` | `FALSE` | Lock pool-consensus splits as constraints after ≥5 reps (focus on uncertain regions). Off-default; only when no user constraint. | + +--- + +## 4. Opt-in env-var levers (not in `SearchControl`) + +| env | default | relevance | +|---|---|---| +| `TS_SECT_COLREDUCE` | unset (off) | **AUDIT #56, shipped `830b8cc3`.** Per-sector column-axis reduction: drops chars constant-within-{sector tips+HTU} + repacks → smaller inner-sector block scan. **Saving: Giles 17%, Zhu 9%, Zanol ~0%** (uniform ns=9 = least reduction = the load-bearing case). **Enable per-class on MIXED-`n_states` data (Giles/Zhu/Dikow); skip on Zanol.** Validated bit-exact (dScore=0 9/9, valgrind clean) but **changes the search trajectory on mixed-state data** (`dCand≠0`, equally-optimal different path) ⇒ **opt-in, NOT a default flip.** **Before enabling by default for any class, run a sector-score ORACLE** (reduced vs full score, same topology, mixed-state) — an accept-gated search can't discriminate a masked packing bug. Read once at static init ⇒ set in the env **before** the R process starts; one process per arm. | + +(`TS_AUDIT_PROBE` is a *compile* flag for measurement counters — not a runtime recipe lever.) + +--- + +## 5. Composition cheat-sheet — session-derived starting recommendations + +These are **hypotheses for #40 to validate**, not settled recipes: + +- **Small / few-char (Wortley-class, <100 patterns or ≤30t):** `default` (or `sprint` ≤30t). Thorough is pointless on flat landscapes (0/6 benefited). Low `maxReplicates` + `targetHits` stop early. +- **Mixed-`n_states` structured (Giles/Zhu/Dikow-class, 65–88t):** `thorough`-ish but trim toward `default`; **enable `TS_SECT_COLREDUCE`** (9–17% sectorial); ratchet 6; **drop fuse**; add a `targetHits`/`perturbStopFactor` stop. `rasStarts=1` (full pipeline converges). +- **Uniform ns=9 hard (Zanol-class, ~74t):** thoroughness is **load-bearing** for the reliable 1261 — do NOT strip ratchet/sectorial aggressively. `TS_SECT_COLREDUCE` gives ~0% here (skip). **`clipOrder=2L` IS a clean ~1.5× throughput win here (measured, 3/3 optima)** — the one place it's safe. The win is otherwise *stopping at the right time*, not running lighter. Consider `stallEscalateFactor>1` to auto-find the perturbation strength. +- **Large (≥120t, not in mission roster):** `large` preset; `pruneReinsert`+NNI; biased Wagner; anneal; **land the prepared exact-scorer port** (41b0d237) + A/B; `rasStarts=3` may re-enter (short-budget/large). +- **Cross-cutting cheap trials:** `clipOrder=2L` is **measured = per-class, Zanol-only safe** (see §3a — ~1.5× on Zanol, but +1 quality cost on Zhu at 2× budget), NOT a global flip; consolidate the 3× sequential trailing sectorial TBRs (T-S6e); the redundant trailing TBRs generally. + +## 6. Hard constraints for #40 + +- **Quality first:** any recipe must still reach the class's known optimum (Wortley 480 / Giles 670 / Zhu 624 / Zanol 1261 / Dikow's best) at full budget. Speed that loses the reliable optimum is a regression, not a win. +- **Recompose from scratch if any step cost changes** (the reason composition waits until all pieces are final). +- **Validate with separate processes per arm** for any env-flag (static-init read) and prefer **candidates_evaluated / score** equality over wall for correctness; wall is the *saving* axis. +- Ratchet is load-bearing (don't zero it >30t); fuse is dead weight on the mission class (do zero it); `TS_SECT_COLREDUCE` is mixed-state-only and never a default. + +--- +*Maintained by the component-isolation/audit workstream. Companion: `dev/plans/2026-06-19-component-isolation-profiling.md` (component verdicts), `dev/profiling/kpi-2026-06-21.md` (the gap reframe).* diff --git a/dev/plans/README.md b/dev/plans/README.md new file mode 100644 index 000000000..37d03fbf1 --- /dev/null +++ b/dev/plans/README.md @@ -0,0 +1,16 @@ +# dev/plans + +Design and strategy documents for TreeSearch development, tracked in git. + +This directory is the **Claude-convention home** for plans. It supersedes the +git-ignored `.positai/plans/` tree (the PositAI tool's plan store), which is +**retired** as of 2026-06-16. Historical PositAI plans remain available locally +under `.positai/plans/` but are no longer added to or maintained; their live +conclusions have been carried forward into the docs here and into the +file-based memory. + +Conventions: +- One markdown file per plan, `YYYY-MM-DD-short-slug.md`. +- Plans are living specs: update status inline as work progresses. +- Durable cross-session facts (one fact each) go in the memory store, not here; + link from a plan to memory by name where useful. diff --git a/dev/plans/impose-constraint-plan.md b/dev/plans/impose-constraint-plan.md new file mode 100644 index 000000000..23fc72f27 --- /dev/null +++ b/dev/plans/impose-constraint-plan.md @@ -0,0 +1,360 @@ +# Plan: C++ `impose_constraint()` for post-hoc topology repair + +## Motivation + +Several operations in the search pipeline can produce constraint-violating +trees but currently lack a way to *repair* violations cheaply: + +| Operation | Current handling | Cost | +|-----------|----------------|------| +| **NNI perturbation** | Disabled entirely when constraints active (T-209) | Loses primary topology-space escape in `thorough` preset | +| **Fuse** | Posthoc check then revert (discard fused tree) | Wastes fuse work; reduces pool diversity under constraints | +| **Drift** | Move rejection (same as TBR) | Narrower exploration under constraints | + +A C++ `impose_constraint(TreeState&, const ConstraintData&)` function would +take an existing tree with minor violations and minimally rearrange it to +satisfy all constraint splits. This is the "repair" complement to the +existing "prevention" approach (`regraft_violates_constraint`). + +**Not in scope:** TBR/SPR candidate screening. Move rejection remains the +right approach there (O(1) per candidate vs. O(n) for fixup + rescore). + +## Existing infrastructure to reuse + +- `map_constraint_nodes(tree, cd)` — identifies which splits are satisfied + (`constraint_node[s] >= 0`) and which are violated (`== -1`). Internally + computes per-node subtree tip bitmasks via postorder traversal, but this + `node_tips` array is a **local variable** (not stored on ConstraintData). + **Refactoring needed:** extract the tip-bitmask computation into a shared + helper `compute_node_tips(tree, n_words)` that both `map_constraint_nodes` + and `impose_constraint` can call. +- `spr_clip(node)` / `spr_regraft(above, below)` — existing SPR primitives + that detach a subtree and reattach it at a new edge. Work on tips and + internal nodes alike. When clip_node is a tip, clip detaches the tip and + frees its parent node; regraft reuses that parent as the new internal node. + **No new topology-manipulation primitives are needed.** +- `split_tips[s * n_words .. (s+1)*n_words - 1]` — target tip set per split + (canonicalized: tip 0 always "outside") +- `update_constraint(tree, cd)` — combined remap + DFS timestamp refresh +- TreeTools `ImposeConstraint()` — R reference implementation (polytomy + backbone + resolution). Our approach is different: minimal surgical repair + rather than full rebuild, preserving perturbation diversity. + +### Thread safety + +Each worker thread in `ts_parallel.cpp` makes a local copy of +`ConstraintData` (lines 95–99). `impose_constraint` mutates `cd` +(via `map_constraint_nodes` and `update_constraint`), which is safe +since each thread operates on its own copy. + +### State array lifecycle + +`impose_constraint` only modifies topology (parent/left/right) and +constraint metadata. It does **not** touch Fitch state arrays (prelim, +final_, local_cost, etc.). The caller is responsible for calling +`tree.reset_states(ds)` + `score_tree(tree, ds)` after repair, which +rebuilds all state arrays from scratch. This means intermediate state +array inconsistency during the SPR moves is harmless. + +## Algorithm + +### High-level + +For each violated split, find the internal node whose subtree is closest to +the target tip set. Identify **subtrees** of misplaced taxa and SPR-move them +to the correct side of the tree. Process splits from smallest to largest (this +is provably safe for compatible constraints; see correctness note below). + +### Detailed steps + +``` +impose_constraint(TreeState& tree, ConstraintData& cd): + + 1. map_constraint_nodes(tree, cd) + -> constraint_node[s] == -1 for violated splits + -> node_tips[] bitmask array computed as side effect + + 2. If no violations, return (common case — free) + + 3. Collect violated splits; sort by popcount ascending (smallest first) + + 4. For each violated split S (in ascending size order): + + a. Rebuild node_tips bitmasks via postorder traversal + (reuse the same buffer; needed because previous split's + moves changed the topology) + + b. Find the "best candidate node" N — the internal node that + MINIMIZES |symmetric_difference(subtree(N), target(S))|: + cost(N) = popcount(node_tips[N] XOR split_tips[S]) + Iterate postorder; keep track of minimum. + + c. Compute misplaced tip sets via bitmask: + move_out_mask = node_tips[N] AND NOT split_tips[S] + (tips in N's subtree that shouldn't be) + move_in_mask = split_tips[S] AND NOT node_tips[N] + (tips outside N's subtree that should be inside) + + d. Find maximal misplaced subtrees (not individual tips): + For each direction (move_out, move_in): + In postorder within the relevant tree region, find nodes + whose subtrees are entirely contained in the misplaced set: + (node_tips[v] & ~move_xxx_mask) == 0 + Keep only maximal ones (parent's subtree is NOT entirely + contained). These are the subtrees to clip. + + e. For each misplaced subtree root M: + - Skip if M is a direct child of the tree root (see edge + cases below) + - Pick a random target edge: + * move_out: DFS from root, collect edges NOT in N's + subtree; pick one uniformly at random + * move_in: DFS from N, collect edges within N's + subtree; pick one uniformly at random + - tree.spr_clip(M) + - tree.spr_regraft(target_above, target_below) + (Each clip-regraft pair is a self-contained SPR. + The single clip_state slot is overwritten each time, + which is fine since we never undo these moves.) + + f. tree.build_postorder() + (makes tree valid for next split's bitmask computation) + + 5. update_constraint(tree, cd) + (remaps constraint nodes + refreshes DFS timestamps for + subsequent TBR/SPR/temper/drift move screening) + + 6. Full rescore (caller's responsibility) +``` + +### Why minimum symmetric difference, not maximum overlap + +The candidate selection criterion is `popcount(node_tips XOR split_tips)`, +which counts the total number of misplaced tips (both directions). +"Maximum overlap" (`popcount(node_tips AND split_tips)`) can prefer nodes +with many extra tips that require more moves: + +| Node | Subtree | Target | Overlap | Sym. diff | +|------|---------|--------|---------|-----------| +| X | {A,B,C} | {A,B,C,D} | 3 | 1 move | +| Y | {A,B,C,D,E,F} | {A,B,C,D} | 4 | 2 moves | + +Max-overlap picks Y; min-symmetric-diff picks X (correct). + +### Why subtrees, not individual tips + +`random_nni_perturb()` swaps subtrees at each edge. After perturbation, +misplaced items are typically contiguous subtrees in the current tree. +With `fraction = 0.5` on a 100-tip tree and a 20-tip constraint clade, +5–10 misplaced tips might share 2–3 subtree roots. Moving subtrees +instead of tips halves the number of SPR operations, and each SPR has +the same cost regardless of subtree size. + +Finding maximal subtrees is cheap with the bitmask infrastructure: +```cpp +// For move_out direction within N's subtree: +for (int node : postorder_within_N) { + uint64_t* nt = &node_tips[node * n_words]; + bool all_misplaced = true; + for (int w = 0; w < n_words; ++w) { + if (nt[w] & ~move_out_mask[w]) { all_misplaced = false; break; } + } + if (all_misplaced) { + // Check parent isn't also all-misplaced (maximality) + // If maximal: add to clip list + } +} +``` + +### Correctness of smallest-first ordering + +For compatible constraints (required by tree construction), splits are either +nested or disjoint: +- **Nested (S1 ⊂ S2):** Fixing S1 first moves tips within S2's boundary. + When S2 is processed, S1's tips are already correctly placed, so S2's + repair only touches non-S1 tips — no interaction. +- **Disjoint:** Fixing S1 moves tips that are not in S2's target set + and vice versa — no interaction. + +Therefore smallest-first is **provably correct for compatible constraints**; +fixing one split cannot violate another. No re-checking of previously +fixed splits is needed. + +## Edge cases + +### Root-adjacent clips + +`spr_clip()` has an awkward path when `clip_node` is a direct child of the +root (`parent[clip_node] == root == n_tip`). The existing code (lines +304–320 of ts_tree.cpp) handles this but the comments acknowledge it's +unusual. If `impose_constraint` ever needs to move a root child, the tree +is likely so heavily violated that repair is the wrong strategy. + +**Guard:** Skip subtrees whose parent is the root. If any remain after +this filter, fall back to `random_constrained_tree()` (full rebuild). + +### Best candidate IS the root + +The root's subtree tip mask has all bits set. Since constraint splits are +canonicalized with tip 0 outside, the root will always have a large +symmetric difference with any split. So the root is never the best +candidate. No special handling needed. + +### Bail-out for heavy violations + +If the total number of subtree moves exceeds a threshold (e.g., n_tip / 4), +the tree is so disrupted that surgical repair offers little advantage over +building fresh. In this case, fall back to `random_constrained_tree()`. +This also guards against pathological NNI perturbation scenarios. + +## Integration points + +### 1. NNI perturbation (highest value) + +In `nni_perturb_search()` (ts_nni_perturb.cpp), between +`random_nni_perturb()` (line 82) and `tree.reset_states(ds)` (line 92): + +```cpp +int n_swaps = random_nni_perturb(tree, params.perturb_fraction); +// NEW: repair constraint violations from blind NNI perturbation +if (n_swaps > 0 && cd && cd->active) { + impose_constraint(tree, *cd); +} +tree.reset_states(ds); +score_tree(tree, ds); +// TBR to local optimum (existing code, constraint-aware) +``` + +Remove the `(!cd || !cd->active)` gate in `run_single_replicate()` +(ts_driven.cpp:322). + +### 2. Fuse (medium value) + +In `driven_search()` (ts_driven.cpp:719–727), replace the discard with +repair: + +```cpp +bool fuse_ok = true; +if (cd && cd->active) { + fuse_ok = !violates_constraint_posthoc(fused, *cd); + if (!fuse_ok) { + impose_constraint(fused, *cd); + fused_score = score_tree(fused, ds); + fuse_ok = true; // repaired + } +} +if (fuse_ok) { + std::vector fused_collapsed; + compute_collapsed_flags(fused, ds, fused_collapsed); + pool.add_collapsed(fused, fused_score, fused_collapsed); +} +``` + +### 3. Sector search (low value — optional) + +In `xss_search()` and `rss_search()` (ts_sector.cpp:746, 876), the +posthoc violation check currently reverts the sector to its previous +topology. An alternative: repair + keep. However, sector violations arise +from a local rebuild that changed the sector–rest-of-tree relationship, +so repair might undo the sector's improvements. **Defer to benchmarking +before committing to this integration.** + +### 4. Random tree (not needed) + +`random_constrained_tree()` already handles this via the polytomy +approach, which is better for building from scratch (constructive, +properly random). `impose_constraint` is not needed here. + +## Testing strategy + +1. **Unit test:** Build a tree that violates a known constraint. + Call `impose_constraint`. Verify all splits satisfied. + Verify score matches `score_tree()` of the result. + +2. **Subtree grouping test:** Build a tree where two tips from the same + subtree are on the wrong side of a constraint. Verify that + `impose_constraint` clips the shared subtree once rather than + processing tips individually (check via move count or topology). + +3. **Round-trip test:** Start from a valid constrained tree. Apply + `random_nni_perturb`. Call `impose_constraint`. Verify constraints + satisfied. Verify the topology is not identical to the original + (perturbation diversity is preserved). + +4. **Multiple constraints test:** Tree violates two nested constraints. + Verify both are repaired in a single `impose_constraint` call. + +5. **Integration test:** Run `MaximizeParsimony` with constraints + + `nniPerturbCycles > 0`. Verify output trees satisfy constraints. + (Currently this combination is impossible because NNI perturb is + disabled.) + +6. **Fuse test:** Run constrained search, verify fuse doesn't discard + all exchanges (check that pool diversity is maintained). + +7. **Bail-out test:** Heavily scramble a constrained tree (fraction ~1.0). + Verify `impose_constraint` falls back to `random_constrained_tree()` + and still produces a valid tree. + +## Prerequisite refactoring + +Extract the node tip-bitmask computation from `map_constraint_nodes()` +(ts_constraint.cpp:130–151) into a standalone helper: + +```cpp +// Compute per-node subtree tip bitmasks via postorder traversal. +// Returns array of size n_node * n_words. +std::vector compute_node_tips(const TreeState& tree, int n_words); +``` + +Then `map_constraint_nodes()` becomes: +```cpp +void map_constraint_nodes(const TreeState& tree, ConstraintData& cd) { + if (!cd.active) return; + auto node_tips = compute_node_tips(tree, cd.n_words); + // ... search for exact matches (existing code) ... +} +``` + +This is a pure extraction — no behaviour change, no new tests needed. + +## Estimated scope + +| Component | Lines | Complexity | Notes | +|-----------|-------|------------|-------| +| `compute_node_tips` helper | ~25 | Low | Extract from `map_constraint_nodes` | +| `impose_constraint` | ~100 | Medium | Min-sym-diff selection, subtree grouping, SPR clip/regraft | +| NNI perturbation integration | ~10 | Low | Remove gate, add call | +| Fuse integration | ~10 | Low | Replace revert with repair | +| Tests | ~100 | Low | 7 test cases | +| **Total** | **~245** | | | + +No new topology-manipulation primitives needed (reuses `spr_clip`/`spr_regraft`). + +## Complexity + +- **Per violated split:** O(n × n_words) for node_tips rebuild + + O(n_internal × n_words) for candidate search + O(k) SPR operations + where k = number of maximal misplaced subtrees (typically 1–3) +- **Total:** O(v × n × n_words) where v = number of violated splits + (usually 0–2 after NNI perturbation) +- **Common case (no violations):** O(n_internal × n_splits × n_words) + for `map_constraint_nodes()`, then immediate return. This is the same + cost as `update_constraint()` which already runs after every accepted + TBR/SPR move, so impose_constraint adds no new overhead in the + no-violation case. + +## Risks + +- **SPR clip/regraft could corrupt tree state** if edge cases aren't + handled (root-adjacent clips, invalid postorder between moves). + Mitigated by: (a) guarding against root-child clips, (b) rebuilding + postorder after each split's moves, (c) assertions on small trees. +- **Repair might undo most of the perturbation** if constraints are + very tight (many splits, large clade coverage). In that case the + NNI perturbation + fixup is little better than the current "skip + entirely" approach. Mitigated by bail-out threshold + monitoring + via benchmarking. +- **Heavy violation = too many moves.** If total moves exceed n_tip/4, + we fall back to `random_constrained_tree()` rather than performing + a large number of SPRs that would effectively rebuild the tree. diff --git a/dev/plans/pr244-examples-hang-brief.md b/dev/plans/pr244-examples-hang-brief.md new file mode 100644 index 000000000..4c9e5c336 --- /dev/null +++ b/dev/plans/pr244-examples-hang-brief.md @@ -0,0 +1,149 @@ +# Brief: diagnose PR #244 "checks hang at examples" + +## Mission + +Find why [PR #244](https://github.com/ms609/TreeSearch/pull/244) (T-302 +`feature/pol-escapa-neg-delta → cpp-search`) cannot complete a full GHA +check cycle. The user describes the symptom as "checks not finishing +because stuck at examples." Three prior runs were cancelled at or near +the 6 h workflow timeout. Confirm whether the issue persists on the +**latest** run, identify the precise stalling step, and propose a fix. + +Report findings + recommended fix to the calling agent in under 400 +words. Do **not** push code without confirmation — diagnostic patches +are OK; structural fixes need sign-off. + +## Background — what's been ruled out + +1. **R_Interactive flush hang** (`b186e801`): R_FlushConsole on captured + stdout pipe filled the buffer in R CMD check subprocesses, causing + indefinite blocking. Fixed in `src/ts_parallel.cpp` on cpp-search. + PR #244 has this fix via merge from cpp-search (`c519496e`). Touches + the parallel progress loop only — affects tests, not examples. + +2. **DEBUG_RESCORE log flood** (T-300 reverted in `b7303ee5`): the broken + incremental rescore printed `DEBUG_RESCORE: diff=-3` on every + accepted move, swamping stdout. Reverted. PR #244 has this revert + via merge. (Note: cpp-search HEAD now has a NEW `DEBUG_RESCORE` + guard from `f531bbcd` + a `DEBUG_NNI_RESCORE` from `2be8228d` — + both should produce **zero** output if the dirty-set rescore is + correct. If GHA shows mismatch lines, the dirty-set fix has a hole; + report that as a separate finding.) + +## What PR #244 actually changes + +Only three files vs `cpp-search`: + +- `NEWS.md` +- `R/PolEscapa.R` — fixes the `LengthAdded` negative-delta bug: + `qmApp <- qmApp[[1L]]` (was a 1-element list, used as a scalar without + unwrap); added a `#Temp` delta clamp for the integer-overflow guard. +- `tests/testthat/test-PolEscapa.R` — adds a token-6 regression test + for the qmApp fix. + +No C++ changes. The hang, if it persists, is either in the R-level +`LengthAdded` example/test or in something inherited from cpp-search +that didn't get exercised on cpp-search's own CI. + +## Recent runs (as of 2026-05-19 06:40 UTC) + +``` +26076620029 R-CMD-check in_progress (~2 h, started 04:40) + ├─ ubuntu-24.04 (release) pass 9m + ├─ ubuntu-24.04 (4.1) pass 10m + ├─ ubuntu-24.04 (devel) pending (>2 h) + ├─ ubuntu-24.04-arm (release) pending (>2 h) + ├─ macos-15-intel (release) pass 19m + ├─ macOS-latest (release) pending (>2 h) + └─ windows-latest (release) pass 25m +26076620046 R-CMD-check-ASAN in_progress + ├─ AddressSanitizer examples pass 43m + ├─ AddressSanitizer vignettes pass 44m + └─ AddressSanitizer tests pending + +26062924642, 26054234185, 26053699158 all CANCELLED (2026-05-18) +``` + +So **ubuntu-24.04 release + 4.1 pass in ~10 min**, but devel + arm + +macOS-latest hang past 2 h. AddressSanitizer **examples passed** (43 m) +— so the literal "examples" stage isn't the problem on every runner. +The user's "stuck at examples" framing may be either (a) misremembered +from a prior cycle where it really did hang there, or (b) referring to +a specific platform that doesn't complete. Verify with the actual logs. + +## Investigation plan + +1. **Confirm the current symptom.** For each pending job in run + `26076620029` and `26076620046`, fetch the live log tail and + identify the most recent output line: + ```bash + gh run view 76668864508 --log 2>&1 | tail -80 # ubuntu-24.04 (devel) + gh run view 76668864476 --log 2>&1 | tail -80 # ubuntu-24.04-arm + gh run view 76668864480 --log 2>&1 | tail -80 # macOS-latest + gh run view 76668864520 --log 2>&1 | tail -80 # ASAN tests + ``` + (Use `gh run view --log-failed` if the job has been killed; for + in-progress use `gh api repos/ms609/TreeSearch/actions/jobs//logs`.) + The last R CMD check stage emitted before the stall is the suspect. + +2. **Re-pull cancelled-run last lines.** Same procedure on runs + `26062924642` (the most recent cancellation pre-cpp-search-merge). + If the cancelled run stalled at a different step than the current + one, the cpp-search merge changed the failure mode — useful signal. + +3. **Inspect candidates uncovered by step 1.** + - If the hang is in `R CMD check` "Running examples..." → look at + `man/LengthAdded.Rd` and any other Rd touched by recent commits. + Vinther2008 `LengthAdded(trees, char)` runs on 9 trees × n_tip + leaves. If T-302's `qmApp <- qmApp[[1L]]` fix accidentally + changes the iteration shape — e.g. now passes a vector where a + scalar was expected, triggering recycling and a many-iteration + inner loop — that would manifest as a slow example. + - If the hang is in `tests/` → focus on `test-PolEscapa.R` (newly + added) and any test that calls `LengthAdded`. + - If the hang is in `vignettes/` → already passed on ASAN, but + check if any non-ASAN job stalls there. + +4. **Local repro.** Once the suspect stage is identified, build PR #244 + locally (`R CMD INSTALL --library=/tmp/pr244 .` after `git + checkout feature/pol-escapa-neg-delta`) and run the offending file + directly: + ```bash + R --no-save -e 'library(TreeSearch, lib.loc="/tmp/pr244"); example(LengthAdded)' + ``` + Time it. Compare to the same invocation on `cpp-search` HEAD + (without the T-302 changes). If the slowdown is real, bisect the + three files in PR #244 to localise. + +5. **Special attention to the `#Temp` delta clamp.** Mentioned in + commit `bebae3a69`. Find it (`git show bebae3a6 -- R/PolEscapa.R`) + and check whether the clamp can fail to terminate a loop on certain + inputs (e.g. NA propagating, or comparing `NA_integer_ > 0` + evaluating to `NA` and being treated as `FALSE`). + +## Tools available + +- `gh` CLI for runs + jobs + logs. +- R 4.7-devel locally (Windows); GHA covers arm/devel/macOS that you + can't repro locally. +- The package builds in ~5 min with the listed `R CMD INSTALL` flags. +- Do **not** start more GHA runs; one in-flight is enough until the + diagnosis is clear. + +## Constraints + +- Token-limited regime: prefer reading one tail of one log over + downloading entire job archives. +- Don't touch `cpp-search` or any other branch's source. If a fix is + needed in cpp-search (because PR #244 inherits something broken), + surface it — don't apply unilaterally. +- If the hang turns out to be a platform-specific R-devel regression + unrelated to PR #244, file as such and stop. + +## Deliverable + +- One section: "Where it hangs" (file + step + last-emitted line). +- One section: "Why" (root cause hypothesis). +- One section: "Proposed fix" (one paragraph) OR "Out of scope — file + upstream" (if R-devel or similar). +- No code commits without confirmation. diff --git a/dev/plans/t300-test.R b/dev/plans/t300-test.R new file mode 100644 index 000000000..895dac9a1 --- /dev/null +++ b/dev/plans/t300-test.R @@ -0,0 +1,99 @@ +library(TreeSearch, lib.loc = "C:/Users/pjjg18/AppData/Local/Temp/ts_t300_lib") +library(TreeTools) + +set.seed(7531) + +# Check whether congreveLamsdellMatrices has inapplicable chars +data("congreveLamsdellMatrices", package = "TreeSearch") +ds <- congreveLamsdellMatrices[[1]] +has_na <- "-" %in% unique(unlist(as.character(ds))) +cat("congreveLamsdellMatrices[[1]] has '-' tokens:", has_na, "\n") +cat("levels:", paste(attr(ds, "levels"), collapse = ","), "\n") +cat("contrast rows:", nrow(attr(ds, "contrast")), "\n") + +# Build a pure-EW dataset (no NA) for sure: take Vinther2008 and replace - with ? +data("inapplicable.phyData", package = "TreeSearch") +ds_v <- inapplicable.phyData[["Vinther2008"]] +cat("\nVinther2008 levels:", paste(attr(ds_v, "levels"), collapse = ","), "\n") + +# Build a synthetic pure-EW dataset using a random small matrix +mat <- matrix(sample(c("0","1"), 30 * 25, replace = TRUE), + nrow = 30, ncol = 25, + dimnames = list(paste0("t", 1:30), paste0("c", 1:25))) +ds_ew <- phangorn::phyDat(mat, type = "USER", levels = c("0", "1")) +cat("\nPure EW synthetic dataset: n_taxa=", length(ds_ew), + " n_chars=", attr(ds_ew, "nr"), "\n", sep = "") + +cat("\n=== Pure EW TBR test ===\n") +set.seed(8421) +sink_path <- tempfile(fileext = ".txt") +sink(sink_path, split = TRUE) +result <- MaximizeParsimony( + ds_ew, + maxReplicates = 5L, + targetHits = 20L, + verbosity = 1L, + nThreads = 1L +) +sink() +log_text <- readLines(sink_path) +n_debug <- sum(grepl("DEBUG_RESCORE", log_text)) +cat("DEBUG_RESCORE mismatch lines emitted (EW):", n_debug, "(expect 0)\n") +if (n_debug > 0) cat(grep("DEBUG_RESCORE", log_text, value = TRUE), sep = "\n") +cat("Final score:", attr(result, "score"), "\n") + +cat("\n=== Pure EW IW test (concavity=10) ===\n") +set.seed(8421) +sink_path2 <- tempfile(fileext = ".txt") +sink(sink_path2, split = TRUE) +result2 <- MaximizeParsimony( + ds_ew, + maxReplicates = 5L, + targetHits = 20L, + verbosity = 1L, + nThreads = 1L, + concavity = 10 +) +sink() +log_text2 <- readLines(sink_path2) +n_debug2 <- sum(grepl("DEBUG_RESCORE", log_text2)) +cat("DEBUG_RESCORE mismatch lines emitted (IW):", n_debug2, "(expect 0)\n") +if (n_debug2 > 0) cat(grep("DEBUG_RESCORE", log_text2, value = TRUE), sep = "\n") +cat("Final IW score:", attr(result2, "score"), "\n") + +cat("\n=== NA TBR test (Vinther2008 EW) ===\n") +set.seed(8421) +sink_path3 <- tempfile(fileext = ".txt") +sink(sink_path3, split = TRUE) +result3 <- MaximizeParsimony( + ds_v, + maxReplicates = 5L, + targetHits = 20L, + verbosity = 1L, + nThreads = 1L +) +sink() +log_text3 <- readLines(sink_path3) +n_debug3 <- sum(grepl("DEBUG_NA_RESCORE", log_text3)) +cat("DEBUG_NA_RESCORE mismatch lines emitted (NA-EW):", n_debug3, "(expect 0)\n") +if (n_debug3 > 0) cat(grep("DEBUG_NA_RESCORE", log_text3, value = TRUE), sep = "\n") +cat("Final NA-EW score:", attr(result3, "score"), "\n") + +cat("\n=== NA TBR IW test (Vinther2008, concavity=10) ===\n") +set.seed(8421) +sink_path4 <- tempfile(fileext = ".txt") +sink(sink_path4, split = TRUE) +result4 <- MaximizeParsimony( + ds_v, + maxReplicates = 5L, + targetHits = 20L, + verbosity = 1L, + nThreads = 1L, + concavity = 10 +) +sink() +log_text4 <- readLines(sink_path4) +n_debug4 <- sum(grepl("DEBUG_NA_RESCORE", log_text4)) +cat("DEBUG_NA_RESCORE mismatch lines emitted (NA-IW):", n_debug4, "(expect 0)\n") +if (n_debug4 > 0) cat(grep("DEBUG_NA_RESCORE", log_text4, value = TRUE), sep = "\n") +cat("Final NA-IW score:", attr(result4, "score"), "\n") diff --git a/dev/profiling/Makevars.vtune b/dev/profiling/Makevars.vtune new file mode 100644 index 000000000..5c28c0e8d --- /dev/null +++ b/dev/profiling/Makevars.vtune @@ -0,0 +1,22 @@ +# Profiling build flags for /profile (VTune/perf): release -O2 + debug symbols. +# +# PREFERRED: use the bundled build-symboled-lib.ps1 (in the /profile skill dir). +# It writes a PKG_CXXFLAGS Makevars like this one, builds an ISOLATED tarball so a +# shared src/ is never touched (safe under concurrent sessions), installs to a +# timestamped lib, and HARD-FAILS if .debug_info is absent. This file is the +# manual fallback. +# +# Debug flags go in PKG_CXXFLAGS (NOT CXXFLAGS): they are then appended to EVERY +# translation unit regardless of C++ standard. Setting only CXXFLAGS is silently +# bypassed for C++17 files (R uses CXX17FLAGS) -- the trap that left +# -fno-omit-frame-pointer off in the 2026-06-17 round (-g still landed by luck of +# the toolchain default). -O2 + -msse2 come from R's default CXX17FLAGS; these +# lines only ADD debug info + ccache. +CCACHE = ccache +CC = $(CCACHE) gcc +CXX = $(CCACHE) g++ +CXX11 = $(CCACHE) g++ +CXX14 = $(CCACHE) g++ +CXX17 = $(CCACHE) g++ +PKG_CFLAGS = -g -fno-omit-frame-pointer +PKG_CXXFLAGS = -g -fno-omit-frame-pointer diff --git a/dev/profiling/PRODUCTION-LEVERS.md b/dev/profiling/PRODUCTION-LEVERS.md new file mode 100644 index 000000000..1054658bd --- /dev/null +++ b/dev/profiling/PRODUCTION-LEVERS.md @@ -0,0 +1,55 @@ +# Sectorial profiling — production-ready levers (Round 6, 2026-06-20) + +This separates the **shippable, byte-identical** wins from the env-gated +measurement scaffolding, per the /profile round on the isolated sectorial +component (findings.md T-S6a–e). + +## What to land: `sector-levers.patch` + +`sector-levers.patch` applies **4 byte-identical micro-levers to `src/ts_sector.cpp` +only**, against pristine cpp-search (`da0f203f`): + +1. **`compute_from_above_for_sector`** — hoist the per-step `new_from_above` + allocation out of the path loop (allocate once + `std::swap`). +2. **`search_sector` `ras_starts==1` fast path** (the default) — skip the + provable no-op `best_*` snapshot + post-loop restore + `build_postorder` + round-trip (the single start's result already sits in `rd.subtree`, and + `reinsert_sector` never reads postorder). +3. **`search_sector` getenv hoist** — `TS_FREE_HTU_PROBE` was read 2–3× per + sector pick; cache it in one `static const bool`. +4. **`rss_search` getenv hoist** — same for the per-accept `TS_SECT_DEBUG`. + +Apply with: `git apply --directory=src dev/profiling/sector-levers.patch` +(the patch paths are file-local; adjust `-p`/`--directory` to your layout). + +### Evidence (byte-identical + faster) +- **Byte-identical:** the CLEAN patched `ts_sector.cpp` (no measurement code) + produces identical per-call score + n_sectors_searched/improved vs pristine + across {Zanol,Zhu,Wortley} × {Wagner,TBR} starts (6/6). The + instrumentation-included build additionally passed 12/12 across 2 seeds and + 8 search test files. +- **Final-build score-identity end to end:** the mission A/B (full + `MaximizeParsimony`, `dev/profiling/drivers/mission-getenv-ab.R`) returned + identical scores on 4 datasets × 3 seeds (625/624/625, 1261×3, 479×3, 272×3). +- **Wall:** ~2.8 % of isolated-sectorial wall (Zanol 48×80 base 3.08→2.99 s, + 8/8 rounds faster); breakdown ras_starts fast path ~0.05 s, getenv hoist + ~0.028 s, from_above swap ~0.006 s — sums exactly to the A/B delta. + +## NOT in this patch (deliberately) + +- **`src/ts_tbr.cpp` is entirely excluded.** My ts_tbr.cpp changes are + (a) env-gated measurement timing and (b) the per-clip `TS_REVERT_CHECK` / + `TS_IW_SCANCHK` / `TS_PHYS_REROOT` getenv hoists — the latter are the + **TBR-agent's production domain** and worth ~20–26 % MISSION-WIDE wall on + their own (findings T-S6d, memory `getenv-ucrt-cost`, spawn_task + `task_2f451c4f`). Land those via a clean compile-out / centralized + debug-flags mechanism on cpp-search, NOT by copying my instrumentation. +- **Measurement-only scaffolding** in the working-tree `ts_sector.cpp` + (`TS_SECT_TIMING` chrono timers + counters, `TS_SECT_NOREROOT`, + `TS_SKIP_RSS_GTBR`, Probe-A counters) — env-gated, zero behaviour change when + off, but should be stripped before merge. The patch above already excludes + all of it. + +## Efficiency axis (T-S6e) — no clean behaviour-neutral win +Probed and recorded; the top idea (suppress the redundant trailing global TBR) +is a speed/quality tradeoff, not safe. See findings.md T-S6e. diff --git a/dev/profiling/baselines.md b/dev/profiling/baselines.md new file mode 100644 index 000000000..1c0f15704 --- /dev/null +++ b/dev/profiling/baselines.md @@ -0,0 +1,62 @@ +# Profiling baselines + +Snapshot of the workloads used by `/profile` and the timings they currently +produce on this machine. Refreshed each round for the area profiled. +`/profile regress` reruns these drivers and flags any > 10 % slowdown. + +Machine context belongs alongside each driver entry (CPU, cores used, +power profile) so future regressions can be compared apples-to-apples. + +## Driver baselines + +| Driver | Dataset / N | Bare wall (s) | Top hotspot (mod=TreeSearch.dll) | % share | Recorded | Machine note | +|-------------------------------------|----------------|---------------|----------------------------------|---------|------------|-----------------------| +| dev/profiling/drivers/ratchet.R | Zhu2013 / 1 rep thorough nThreads=1 | 2.80 (median of 3) | ts_driven_search (>95 %; no VTune; from profvis) | >95 % | 2026-05-18 | Windows 10 i-series, R-devel, .vtune-lib debug build | +| dev/profiling/drivers/tbr-rescore.R | Zhu2013 / 12 ratchet reps nCycles=12 nThreads=1 | 3.9 | ts::fitch_na_score (full_rescore path via callstack) | 18.2 % | 2026-05-19 | Windows 10 EARTHSCI-PJJG18, 2.904 GHz 16-core, R-devel, .vtune-lib-20260519061049 (HEAD c504ea87) | +| dev/profiling/t300_na_bench.R | Zhu2013 / 12 ratchet reps nCycles=12 nThreads=1 | 3.29 (median of 5, score 647) | (post T-300 NA dirty-set; fitch_na_pass3_score expected dominant; not VTune-attributed yet) | n/a | 2026-05-19 | Same machine, HEAD 5b210fdd; 15.2 % wall-time speedup vs c504ea87 baseline (3.88 s median of 3) | +| dev/profiling/drivers/fitch-tnt.R | Zhu2013 `-`→`?` / 8 reps auto→thorough nThreads=1 | 5.57 (0.56 s/rep, score 627) | ts::tbr_search (orchestration self) | 25.1 % | 2026-06-16 | Same machine, HEAD 841eead3, .vtune-lib-20260616052323 (-O2 -g). STANDARD-Fitch path (has_na=FALSE) — TNT-parity objective; TNT 1.6 = 624 | + +### Round 3 top hotspots (TreeSearch.dll, Zhu2013 75t **standard-Fitch** `-`→`?`, total 2.70 s) + +Different path from rounds 1-2 (NA): no `fitch_na_*`; flat/x4 kernels. +Names via `nm` (VTune CSV shows `func@0x…`; image base 0x2cc1a0000 stable). + +| Rank | Function | Self time | % of DLL | Notes | +|------|---------------------------------------|-----------|----------|-------| +| 1 | ts::tbr_search (orchestration) | 0.678 s | 25.1 % | candidate-loop control + collapsed/sector vector bit-tests + inlined scoring | +| 2 | ts::simd::any_hit_reduce_avx2 | 0.392 s | 14.5 % | 2-op Fitch reduce; AT-LIMIT (compiler-optimal, disasm-confirmed) | +| 3 | ts::uppass_node | 0.357 s | 13.2 % | incremental uppass; scalar update loop; AT-LIMIT (1.22× micro-bench, nil for 2-state) | +| 4 | ts::simd::any_hit_reduce3_avx2 | 0.171 s | 6.3 % | 3-op reduce (SPR bounded) | +| 5 | ts::TreeState::build_postorder_prealloc | 0.141 s | 5.2 % | O(n) per clip + per accept — top per-clip-bookkeeping target | +| 6 | ts::fitch_incremental_downpass | 0.110 s | 4.1 % | per clip | +| 7 | ts::fitch_indirect_bounded_flat | 0.109 s | 4.0 % | SPR candidate scoring (flat) | +| 8 | ts::hash_tree | 0.078 s | 2.9 % | pool/tabu dedup | +| 8 | ts::fitch_indirect_length_cached | 0.078 s | 2.9 % | scalar cached (MIXED-ratchet perturbed sub-search) | +| 8 | ts::validate_topology | 0.077 s | 2.9 % | per-accept DFS sanity check (allocates 2 vectors/call) | + +### Round 2 top-5 hotspots (TreeSearch.dll, Zhu2013 75t ratchet) + +| Rank | Function | Self time | % of DLL | Notes | +|------|----------------------------------|-----------|----------|-------| +| 1 | ts::fitch_na_score | 0.585 s | 18.2 % | Full Fitch pass (called via full_rescore → tbr_search, confirmed by callstack) | +| 2 | ts::simd::any_hit_reduce_avx2 | 0.309 s | 9.6 % | SIMD candidate hit reduction — inner evaluation loop | +| 3 | ts::tbr_search (residual) | 0.297 s | 9.3 % | Control-flow overhead not attributed to child callees | +| 4 | ts::fitch_na_pass3_score | 0.281 s | 8.8 % | Incremental scoring uppass (candidate evaluation) | +| 5 | ts::fitch_na_incremental_uppass | 0.110 s | 3.4 % | Incremental uppass after candidate topology | + +**full_rescore (ts_tbr.cpp:1138) total = fitch_na_score + load_tip_states = 0.617 s = 19.2 % of DLL CPU time** +Note: prior S-PROF round 7 estimate was 28 %; measured 19.2 % (see Round 2 log for context). +Context: virtually all of fitch_na_score flows through line 1138 (acceptance path), not line 563 (entry call), because ratchet-driven TBR accepts ~100–200 moves per sub-optimal restart vs 1 entry call. + +## End-to-end reference timings (from `.positai/expertise/profiling.md`) + +Kept here for cross-check against driver-level numbers — *not* a substitute +for the project benchmark suite. + +| Dataset | Tips | Chars | Median wall (s) | Score | Preset | Recorded | +|---------------|------|-------|-----------------|---------|-----------------|------------| +| Vinther2008 | 23 | 57 | 0.42 | 79 | sprint | 2026-03-19 | +| Agnarsson2004 | 62 | 242 | 1.79 | 778 | default | 2026-03-19 | +| Zhu2013 | 75 | 253 | 3.17 | 648–666 | thorough | 2026-03-19 | +| Dikow2009 | 88 | 220 | 4.90 | 1612–14 | thorough | 2026-03-19 | +| mbank_X30754 | 180 | 425 | 17.3 / rep | 1202 | large, 30 s bud | 2026-03-26 | diff --git a/dev/profiling/bench_equiv.R b/dev/profiling/bench_equiv.R new file mode 100644 index 000000000..977b26b57 --- /dev/null +++ b/dev/profiling/bench_equiv.R @@ -0,0 +1,26 @@ +# Tier-2 behaviour-neutrality check: same seed + nThreads=1 => the dedup table +# must produce byte-identical search trajectories => identical scores on EVERY +# dataset and BOTH scoring paths (NA three-pass on raw data; standard Fitch +# after '-'->'?'). Run on after- and before-libs; scores must match exactly. +LIBDIR <- normalizePath(Sys.getenv("TREESEARCH_VTUNE_LIB"), winslash = "/") +suppressMessages(library(TreeSearch, lib.loc = LIBDIR)) +suppressMessages(library(TreeTools)) + +sets <- c("Vinther2008", "Longrich2010", "Sansom2010", "Aria2015", + "Dikow2009", "Zhu2013") + +for (nm in sets) { + raw <- inapplicable.phyData[[nm]] + # NA three-pass path (raw inapplicables) + set.seed(42) + r_na <- suppressWarnings(MaximizeParsimony(raw, maxReplicates = 3L, + nThreads = 1L, strategy = "default", verbosity = 0L)) + # Standard-Fitch path ('-' -> '?') + m <- PhyDatToMatrix(raw, ambigNA = FALSE); m[m == "-"] <- "?" + std <- MatrixToPhyDat(m) + set.seed(42) + r_std <- suppressWarnings(MaximizeParsimony(std, maxReplicates = 3L, + nThreads = 1L, strategy = "default", verbosity = 0L)) + cat(sprintf("%-14s NA=%-8s STD=%-8s\n", + nm, attr(r_na, "score"), attr(r_std, "score"))) +} diff --git a/dev/profiling/bench_escalator.R b/dev/profiling/bench_escalator.R new file mode 100644 index 000000000..8789cfbd8 --- /dev/null +++ b/dev/profiling/bench_escalator.R @@ -0,0 +1,39 @@ +# Smoke validation for stallEscalateFactor (the online stall-escalator). +# Does escalating ratchet perturbation on stall help close the TNT gap on a +# hard dataset? Standard-Fitch path ('-' -> '?'), nThreads=1, several seeds. +# Compares factor=1.0 (off, baseline) against escalation factors. +# env: TREESEARCH_VTUNE_LIB (lib path), TS_DATASET (Wortley2006), +# TS_REPS (30), TS_SECONDS (0 = rep-limited), TS_SEEDS ("1 2 3") +LIBDIR <- normalizePath(Sys.getenv("TREESEARCH_VTUNE_LIB"), winslash = "/") +suppressMessages(library(TreeSearch, lib.loc = LIBDIR)) +suppressMessages(library(TreeTools)) + +dsName <- Sys.getenv("TS_DATASET", unset = "Wortley2006") +nReps <- as.integer(Sys.getenv("TS_REPS", unset = "30")) +maxSec <- as.double(Sys.getenv("TS_SECONDS", unset = "0")) +seeds <- as.integer(strsplit(Sys.getenv("TS_SEEDS", unset = "1 2 3"), " ")[[1]]) +factors <- c(1.0, 1.5, 2.0) # 1.0 = escalator off (baseline) + +raw <- inapplicable.phyData[[dsName]] +m <- PhyDatToMatrix(raw, ambigNA = FALSE) +m[m == "-"] <- "?" # TNT-parity standard Fitch +dataset <- MatrixToPhyDat(m) +cat(sprintf("%s | %d tips | factors {%s} | seeds {%s} | reps %d | %ss\n\n", + dsName, length(dataset), paste(factors, collapse = ", "), + paste(seeds, collapse = ","), nReps, + if (maxSec > 0) maxSec else "rep-limited")) + +for (fac in factors) { + scores <- integer(0) + for (sd in seeds) { + set.seed(sd) + r <- suppressWarnings(MaximizeParsimony( + dataset, maxReplicates = nReps, nThreads = 1L, strategy = "auto", + maxSeconds = maxSec, verbosity = 0L, + control = SearchControl(stallEscalateFactor = fac))) + scores <- c(scores, attr(r, "score")) + } + cat(sprintf("factor %.1f : best=%d median=%.1f scores={%s}\n", + fac, min(scores), median(scores), + paste(scores, collapse = ","))) +} diff --git a/dev/profiling/bench_tier2.R b/dev/profiling/bench_tier2.R new file mode 100644 index 000000000..023d2acce --- /dev/null +++ b/dev/profiling/bench_tier2.R @@ -0,0 +1,37 @@ +# Tier-2 A/B harness: deterministic same-seed runs of the standard-Fitch +# (TNT-parity) search. Score MUST be identical across builds (Tier 2 is +# behaviour-neutral); only wall-clock should move. nThreads=1, fixed seed. +# env: TREESEARCH_VTUNE_LIB (lib path), TS_RUNS (default 10), TS_REPS (8) +LIBDIR <- Sys.getenv("TREESEARCH_VTUNE_LIB") +suppressMessages(library(TreeSearch, lib.loc = LIBDIR)) +suppressMessages(library(TreeTools)) + +raw <- inapplicable.phyData[["Zhu2013"]] +m <- PhyDatToMatrix(raw, ambigNA = FALSE) +m[m == "-"] <- "?" # TNT-parity: standard Fitch +dataset <- MatrixToPhyDat(m) +stopifnot(!("-" %in% attr(dataset, "levels"))) + +N <- as.integer(Sys.getenv("TS_RUNS", "10")) +reps <- as.integer(Sys.getenv("TS_REPS", "8")) + +# One warm-up (page-in, allocator warm) excluded from stats. +set.seed(1) +invisible(suppressWarnings(MaximizeParsimony(dataset, maxReplicates = reps, + nThreads = 1L, strategy = "auto", verbosity = 0L))) + +times <- numeric(N); scores <- numeric(N) +for (i in seq_len(N)) { + set.seed(1) # identical work every run + t0 <- proc.time() + r <- suppressWarnings(MaximizeParsimony(dataset, maxReplicates = reps, + nThreads = 1L, strategy = "auto", verbosity = 0L)) + times[i] <- (proc.time() - t0)["elapsed"] + scores[i] <- attr(r, "score") +} +cat(sprintf("LIB : %s\n", LIBDIR)) +cat(sprintf("scores : %s (must be a single value)\n", + paste(sort(unique(scores)), collapse = ","))) +cat(sprintf("median : %.3f s (min %.3f / max %.3f / mean %.3f / sd %.3f)\n", + median(times), min(times), max(times), mean(times), sd(times))) +cat(sprintf("all : %s\n", paste(sprintf("%.2f", times), collapse = " "))) diff --git a/dev/profiling/drivers/fitch-tnt-profvis.R b/dev/profiling/drivers/fitch-tnt-profvis.R new file mode 100644 index 000000000..3e97dfb64 --- /dev/null +++ b/dev/profiling/drivers/fitch-tnt-profvis.R @@ -0,0 +1,35 @@ +# profvis + Rprof triage for the standard-Fitch search call (R vs C++). +# Confirms there is no per-replicate R loop to port: the search is a single +# .Call, so R overhead is one-time data prep only. +LIBDIR <- "dev/profiling/.vtune-lib-20260616051420" +suppressMessages(library(TreeSearch, lib.loc = LIBDIR)) +suppressMessages(library(TreeTools)) +suppressMessages(library(profvis)) + +raw <- inapplicable.phyData[["Zhu2013"]] +m <- PhyDatToMatrix(raw, ambigNA = FALSE); m[m == "-"] <- "?" +dataset <- MatrixToPhyDat(m) + +# Warm up (compile/JIT, load DLL) outside measurement +invisible(suppressWarnings(MaximizeParsimony(dataset, maxReplicates = 1L, + nThreads = 1L, strategy = "auto", verbosity = 0L))) + +p <- profvis::profvis({ + set.seed(1) + invisible(suppressWarnings(MaximizeParsimony(dataset, maxReplicates = 6L, + nThreads = 1L, strategy = "auto", verbosity = 0L))) +}) +htmlwidgets::saveWidget(p, "dev/profiling/drivers/fitch-tnt-profvis.html", + selfcontained = FALSE) + +# Numeric R-vs-native split via Rprof +tf <- tempfile() +Rprof(tf, interval = 0.005, line.profiling = FALSE) +set.seed(1) +invisible(suppressWarnings(MaximizeParsimony(dataset, maxReplicates = 6L, + nThreads = 1L, strategy = "auto", verbosity = 0L))) +Rprof(NULL) +s <- summaryRprof(tf) +cat("\n=== Top self-time (by.self) ===\n") +print(utils::head(s$by.self, 12)) +cat("\n total.time:", s$sampling.time, "s\n") diff --git a/dev/profiling/drivers/fitch-tnt.R b/dev/profiling/drivers/fitch-tnt.R new file mode 100644 index 000000000..4105b34f5 --- /dev/null +++ b/dev/profiling/drivers/fitch-tnt.R @@ -0,0 +1,68 @@ +# Standard-Fitch TNT-parity profiling driver — Area #5 +# GOAL: profile the *standard Fitch* path that the TNT-parity benchmark uses. +# TNT-parity replaces inapplicable "-" with missing "?" so both engines +# optimise the identical Fitch objective (no Brazeau-Guillerme-Smith NA +# handling). Removing the "-" level makes the C++ engine take has_na=FALSE +# and use the flat / 4-wide (T-245) kernels — a code path NEVER profiled +# before (all prior rounds used the NA path on raw inapplicable.phyData). +# +# Reports per-phase timings via attr(result, "timings") so we can see where +# the standard-Fitch wall-clock actually goes, vs the (stale, NA-path) round-6 +# distribution in dev/expertise/profiling.md. +# +# Params (env): TS_DATASET (default Zhu2013), TS_REPS (default 3), TS_SEED (1) +# nThreads=1 always (apples-to-apples with single-threaded TNT xmult). + +LIBDIR <- Sys.getenv("TREESEARCH_VTUNE_LIB", + unset = "dev/profiling/.vtune-lib-20260616051420") +suppressMessages(library(TreeSearch, lib.loc = LIBDIR)) +suppressMessages(library(TreeTools)) + +ds_name <- Sys.getenv("TS_DATASET", unset = "Zhu2013") +n_reps <- as.integer(Sys.getenv("TS_REPS", unset = "3")) +seed <- as.integer(Sys.getenv("TS_SEED", unset = "1")) + +raw <- inapplicable.phyData[[ds_name]] + +# --- Convert inapplicable "-" -> missing "?" (TNT-parity Fitch objective) --- +m <- PhyDatToMatrix(raw, ambigNA = FALSE) +n_dash <- sum(m == "-") +m[m == "-"] <- "?" +dataset <- MatrixToPhyDat(m) +lv <- attr(dataset, "levels") +stopifnot(!("-" %in% lv)) # confirm standard-Fitch path (has_na = FALSE) + +cat(sprintf("Dataset: %s | %d tips | %d patterns | %d levels (%s) | %d '-' -> '?'\n", + ds_name, length(dataset), attr(dataset, "nr"), + length(lv), paste(lv, collapse = ""), n_dash)) + +# Auto strategy will pick a preset from nTip/nChar; report it. +strat <- TreeSearch:::.AutoStrategy(length(dataset), attr(dataset, "nr")) +cat(sprintf("Auto strategy -> %s\n", strat)) + +set.seed(seed) +t0 <- proc.time() +result <- suppressWarnings( + MaximizeParsimony( + dataset, + maxReplicates = n_reps, + nThreads = 1L, + strategy = "auto", + verbosity = 0L + ) +) +elapsed <- (proc.time() - t0)["elapsed"] + +tm <- attr(result, "timings") +tm <- tm[order(-tm)] +tot <- sum(tm, na.rm = TRUE) + +cat(sprintf("\nElapsed: %.2f s | Score: %s | Reps: %s | MPTs: %s\n", + elapsed, attr(result, "score"), attr(result, "replicates"), + attr(result, "n_topologies"))) +cat(sprintf("Sum of phase timings: %.1f ms\n\n", tot)) +cat("Phase distribution (cumulative ms across all replicates):\n") +for (nm in names(tm)) { + cat(sprintf(" %-22s %8.1f ms %5.1f%%\n", nm, tm[[nm]], + 100 * tm[[nm]] / tot)) +} diff --git a/dev/profiling/drivers/mission-getenv-ab.R b/dev/profiling/drivers/mission-getenv-ab.R new file mode 100644 index 000000000..e3c586316 --- /dev/null +++ b/dev/profiling/drivers/mission-getenv-ab.R @@ -0,0 +1,23 @@ +# Mission-wide A/B for the getenv hoist + T-S6c levers: full MaximizeParsimony +# (ratchet + sectorial + TBR), NOT isolated sectorial. Confirms the per-clip +# getenv finding is cross-cutting (every tbr_search clip, mission-wide), and that +# score is unchanged (byte-identical levers). Run with TREESEARCH_VTUNE_LIB. +LIBDIR <- Sys.getenv("TREESEARCH_VTUNE_LIB", unset = ".agent-sect") +suppressMessages(library(TreeSearch, lib.loc = LIBDIR)) +suppressMessages(library(TreeTools)) +ds_name <- Sys.getenv("TS_DATASET", unset = "Zhu2013") +reps <- as.integer(Sys.getenv("TS_REPS", unset = "3")) +seed <- as.integer(Sys.getenv("TS_SEED", unset = "1")) + +raw <- inapplicable.phyData[[ds_name]] +m <- PhyDatToMatrix(raw, ambigNA = FALSE); m[m == "-"] <- "?" +dataset <- MatrixToPhyDat(m) + +set.seed(seed) +t0 <- proc.time() +res <- suppressWarnings(MaximizeParsimony(dataset, maxReplicates = reps, + nThreads = 1L, strategy = "thorough", + verbosity = 0L)) +el <- (proc.time() - t0)["elapsed"] +cat(sprintf("MISSION %s reps=%d seed=%d : elapsed=%.2f s score=%s\n", + ds_name, reps, seed, el, attr(res, "score"))) diff --git a/dev/profiling/drivers/ratchet.R b/dev/profiling/drivers/ratchet.R new file mode 100644 index 000000000..4ab491d4e --- /dev/null +++ b/dev/profiling/drivers/ratchet.R @@ -0,0 +1,29 @@ +# Ratchet inner-loop profiling driver — Area #2 +# bare: 3.7 s on 2026-05-18 (thorough × 3 reps, nThreads=1) +# Dataset: Zhu2013 (75 tips, 4 states, 43% missing) +# Strategy: "default" preset (ratchetCycles=12), maxReplicates=1, nThreads=1 +# +# What this exercises: ratchet_search() in ts_ratchet.cpp, which loops +# [save_perturb → perturb → tbr_search → restore_perturb → accept/reject] +# for n_cycles iterations around an initial TBR pass. + +library(TreeSearch, lib.loc = ".vtune-lib") + +set.seed(5813) + +dataset <- inapplicable.phyData[["Zhu2013"]] + +# Suppress replicate-count adequacy warning (1 rep is intentional) +t0 <- proc.time() +result <- suppressWarnings( + MaximizeParsimony( + dataset, + maxReplicates = 3L, + targetHits = 1L, + nThreads = 1L, + strategy = "thorough", + verbosity = 0L + ) +) +elapsed <- round((proc.time() - t0)["elapsed"], 1) +cat("Elapsed:", elapsed, "s | Score:", attr(result, "score"), "\n") diff --git a/dev/profiling/drivers/sector-rss.R b/dev/profiling/drivers/sector-rss.R new file mode 100644 index 000000000..788a94ae5 --- /dev/null +++ b/dev/profiling/drivers/sector-rss.R @@ -0,0 +1,92 @@ +# Isolated sectorial (RSS) profiling driver — Area #3 +# +# Isolates ONLY the sector search component via the thin ts_rss_search Rcpp +# wrapper (no ratchet/fuse wrapper). Per the component-isolation plan +# (dev/plans/2026-06-19-component-isolation-profiling.md) and the advisor: +# * Crank rssPicks HIGH and use FEW calls so the trailing global TBR and the +# per-call make_dataset/init_from_edge marshaling (a DRIVER ARTIFACT) are +# amortised — otherwise the profile is mostly TBR, not sectorial. +# * EW-Fitch only ('-' -> '?'); the NA path is owned by another workstream. +# +# Start tree: a Wagner addition tree (non-optimal) so many sectors improve and +# the reinsert + full-tree rescore accept-path is exercised (bucket-2 coverage). +# Set TS_START=tbr to TBR-converge the start first (fewer accepts). +# +# Params (env): TS_DATASET (Zanol2014), TS_PICKS (80), TS_CALLS (12), +# TS_SEED (1), TS_MINSIZE (6), TS_MAXSIZE (50), TS_START (wagner|tbr), +# TS_ACCEPTEQ (0), TS_RATCHET (6 — internal_ratchet_cycles, unused by rss). +# +# bare target: <= 5 s. nThreads=1 (serial; sectorial RNG pulls from R's stream). + +LIBDIR <- Sys.getenv("TREESEARCH_VTUNE_LIB", unset = ".agent-sect") +suppressMessages(library(TreeSearch, lib.loc = LIBDIR)) +suppressMessages(library(TreeTools)) + +ds_name <- Sys.getenv("TS_DATASET", unset = "Zanol2014") +n_picks <- as.integer(Sys.getenv("TS_PICKS", unset = "80")) +n_calls <- as.integer(Sys.getenv("TS_CALLS", unset = "12")) +seed <- as.integer(Sys.getenv("TS_SEED", unset = "1")) +min_size <- as.integer(Sys.getenv("TS_MINSIZE", unset = "6")) +max_size <- as.integer(Sys.getenv("TS_MAXSIZE", unset = "50")) +start_kind <- Sys.getenv("TS_START", unset = "wagner") +accept_eq <- as.integer(Sys.getenv("TS_ACCEPTEQ", unset = "0")) != 0L + +raw <- inapplicable.phyData[[ds_name]] + +# --- EW standard-Fitch objective: inapplicable '-' -> missing '?' --- +m <- PhyDatToMatrix(raw, ambigNA = FALSE) +m[m == "-"] <- "?" +dataset <- MatrixToPhyDat(m) +at <- attributes(dataset) +ds <- list( + contrast = at$contrast, + tip_data = matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE), + weight = at$weight, + levels = at$levels +) +n_tip <- length(dataset) + +# --- Build the start tree once (deterministic) --- +# Seed BEFORE the start build so the (RNG-using) ts_tbr_search start is +# reproducible across processes — required for the byte-identical A/B gate. +set.seed(seed) +wag <- TreeSearch:::ts_wagner_tree(ds$contrast, ds$tip_data, ds$weight, ds$levels) +start_edge <- wag$edge +start_score <- wag$score +if (identical(start_kind, "tbr")) { + tb <- TreeSearch:::ts_tbr_search(start_edge, ds$contrast, ds$tip_data, + ds$weight, ds$levels, maxHits = 1L) + start_edge <- tb$edge + start_score <- tb$score +} + +cat(sprintf("Dataset: %s | %d tips | %d patterns | start(%s)=%g | picks=%d calls=%d\n", + ds_name, n_tip, attr(dataset, "nr"), start_kind, start_score, + n_picks, n_calls)) + +set.seed(seed) +scores <- numeric(n_calls) +n_searched <- integer(n_calls) +n_improved <- integer(n_calls) +t0 <- proc.time() +for (i in seq_len(n_calls)) { + res <- TreeSearch:::ts_rss_search( + start_edge, ds$contrast, ds$tip_data, ds$weight, ds$levels, + minSectorSize = min_size, maxSectorSize = max_size, + acceptEqual = accept_eq, rssPicks = n_picks, + ratchetCycles = 0L, maxHits = 1L) + scores[i] <- res$score + n_searched[i] <- res$n_sectors_searched + n_improved[i] <- res$n_sectors_improved +} +elapsed <- (proc.time() - t0)["elapsed"] + +cat(sprintf("Elapsed: %.2f s | %d calls x %d picks | sectors searched=%d improved=%d\n", + elapsed, n_calls, n_picks, sum(n_searched), sum(n_improved))) +cat(sprintf("Scores: min=%g median=%g max=%g\n", + min(scores), median(scores), max(scores))) +# Gate signal (bit-identical A/B): per-call score + sector counts. +cat("GATE", paste(scores, collapse = ","), "|", + paste(n_searched, collapse = ","), "|", + paste(n_improved, collapse = ","), "\n") diff --git a/dev/profiling/drivers/tbr-rescore.R b/dev/profiling/drivers/tbr-rescore.R new file mode 100644 index 000000000..91a6168c7 --- /dev/null +++ b/dev/profiling/drivers/tbr-rescore.R @@ -0,0 +1,64 @@ +# TBR full-rescore profiling driver — Area #4 +# bare: 3.9 s on 2026-05-19 (Zhu2013 75t, 12 ratchet reps × 12 cycles) +# Dataset: Zhu2013 (75 tips, inapplicable characters) +# Strategy: ts_ratchet_search() called directly (no MaximizeParsimony overhead) +# to match the ratchet context from which the 28 % estimate came. +# Each ratchet cycle: perturb → tbr_search → restore → accept/reject. +# full_rescore at ts_tbr.cpp:1138 fires on every accepted TBR move. +# +# Why ts_ratchet_search rather than ts_tbr_search directly? +# - TBR from a near-optimal tree converges instantly (0 accepts → 0 +# full_rescore-at-acceptance events; not representative). +# - Ratchet perturbs the tree, driving TBR from sub-optimal states that +# generate many accepts and thus many full_rescore calls. +# - Perturbation overhead < 2 % (confirmed in prior round); ratchet time is +# effectively all TBR time. +# +# VTune attribution note: +# full_rescore() is a 2-line static inline under -O2. +# Attribution falls to reset_states() / score_tree() at source lines: +# ts_tbr.cpp:1138 (rescore after acceptance — the T-300 target) +# ts_tbr.cpp:563 (initial full_rescore at tbr_search entry) +# ts_tbr.cpp:1283 (trailing full_rescore at exit) +# Use: vtune -report hotspots -group-by source-line -filter module=TreeSearch.dll + +# Timestamped lib from build 2026-05-19 06:10:49 +LIBDIR <- Sys.getenv("TREESEARCH_VTUNE_LIB", + unset = "dev/profiling/.vtune-lib-20260519061049") +library(TreeSearch, lib.loc = LIBDIR) + +set.seed(5813) + +dataset <- inapplicable.phyData[["Zhu2013"]] +at <- attributes(dataset) +contrast <- at$contrast +tip_data <- matrix(unlist(dataset, use.names = FALSE), + nrow = length(dataset), byrow = TRUE) +weight <- TreeSearch:::.ScaleWeight(at$weight) +levels <- at$levels + +# Starting tree: random unrooted tree (cold start, like a new replicate) +starting_edge <- ape::rtree(length(dataset), tip.label = names(dataset), + rooted = FALSE) +starting_edge <- ape::root(starting_edge, 1L, resolve.root = TRUE)[["edge"]] +stopifnot(starting_edge[1L, 1L] > length(dataset)) # internal node first + +N_REPS <- 12L +t0 <- proc.time() +for (rep in seq_len(N_REPS)) { + set.seed(rep) + result <- TreeSearch:::ts_ratchet_search( + edge = starting_edge, + contrast = contrast, + tip_data = tip_data, + weight = weight, + levels = levels, + nCycles = 12L, + perturbProb = 0.04, + maxHits = 1L + ) + # Keep each rep independent: restart from the same cold tree + # (varying seed ensures different topology visits and thus more accept events) +} +elapsed <- round((proc.time() - t0)["elapsed"], 1) +cat("Elapsed:", elapsed, "s |", N_REPS, "ratchet reps (nCycles=12, Zhu2013 75t) | score:", result$score, "\n") diff --git a/dev/profiling/findings.md b/dev/profiling/findings.md new file mode 100644 index 000000000..f687b1f37 --- /dev/null +++ b/dev/profiling/findings.md @@ -0,0 +1,59 @@ +# Profiling findings + +One row per verified optimisation opportunity, in `to-do.md` paste-ready +format. A finding only lands here if an isolated `std::chrono` micro-bench +reproduces the predicted delta. + +Tags: +- `[Port]` — R loop on the hot path that should move to C++. +- `[Optimise]` — C++ change with verified expected speedup. +- `[AT-LIMIT]` — function is at a hardware ceiling; record so the rotation + skips it in future rounds. + +| ID-suggest | P? | Status | Depends | Headline | Detail (% time, mechanism, verified Δ, micro-bench path) | +|------------|----|--------|---------|----------|---------------------------------------------------------| +| T-300 | P1 | DONE | — | [Optimise] `full_rescore` after accepted TBR move (ts_tbr.cpp:1138): replace with incremental rescore | LANDED (commits f531bbcd EW + 014ccdea NA dirty-set). 19.2 % of NA-path DLL CPU; 15.2 % wall speedup on Zhu2013 NA (3.88→3.29 s). | + +## Round 3 (2026-06-16) — standard-Fitch TNT-parity path (Zhu2013 `-`→`?`, auto→thorough) + +DLL self-CPU total 2.70 s. Names resolved via `nm` (VTune reporter shows +`func@0x…` for MinGW DWARF; image base stable so addresses map 1:1). + +| ID-suggest | P? | Status | Depends | Headline | Detail (% time, mechanism, verified Δ, micro-bench path) | +|------------|----|--------|---------|----------|---------------------------------------------------------| +| T-S3a | P3 | **DONE (verified)** | — | [Optimise] Per-clip allocation churn in TBR helpers — Tier 1 | **IMPLEMENTED 2026-06-16** (uncommitted, in working tree). Converted per-clip scratch vectors to `static thread_local` + clear/assign: `fitch_incremental_uppass` `dirty` (`std::vector`→`char`, allocated EVERY clip — ts_fitch.cpp:225), `collect_main_edges`/`collect_subtree_edges` DFS stacks, `compute_from_above` preorder+stack (ts_tbr.cpp). Per-thread-safe (each search thread owns its TreeState; none re-entrant). **VERIFIED:** Zhu2013 `-`→`?` score 627 unchanged (10/10 runs); ts- suite 3061 assertions, 0 fail; wall-clock 4.00 s→3.84 s = **~4.0%** (non-overlapping medians, TS_REPS=8 TS_SEED=1, identical build flags). Tier 2 (dedup table) now **DONE — see T-S3d (~3%)**. Tier 3a (gate `validate_topology` under NDEBUG, ~2-3%) NOT done — a safety/policy call (removes a release-build invariant check). | +| T-S3d | P3 | **DONE (verified)** | T-S3a | [Optimise] Per-clip rerooting dedup table — Tier 2 | **IMPLEMENTED 2026-06-16** (uncommitted, in working tree). Replaced the per-clip `std::unordered_set seen_vp_hashes` (ts_tbr.cpp ~946 — a bucket-array alloc + a per-insert node malloc on every internal-node clip) with a reusable open-addressed `VpHashSet`: power-of-two table, generation-stamped O(1) `reset()` (bump a counter, no zeroing), Fibonacci-mixed probe (fast_hash is FNV-1a, weak low bits), linear probing. Dedups on the exact 64-bit key ⇒ semantics identical to `unordered_set`. **KEY LESSON (emutls):** first written `static thread_local` → measured ~neutral/slight-regression, because MinGW resolves `thread_local` in a *loaded DLL* via **emutls** (a function call per access) and `insert()` runs per reroot candidate. Re-implemented as a **plain local declared once before the clip loop** (per-thread-safe via the call stack; zero TLS) → **~3% wall-clock** win: interleaved A/B floor **3.72 s vs 3.83 s** Tier1-only, clean-block median 3.75 vs 3.86, score 627 identical (dev/profiling/bench_tier2.R). **Behaviour-neutral:** identical scores on 6 datasets × {NA three-pass, standard Fitch} (dev/profiling/bench_equiv.R, diff empty). NB: the full ts- suite can't be a gate from a temp lib — relative `lib.loc` breaks data lazy-load under testthat's CWD switch (use absolute), and there is a PRE-EXISTING **flaky `nThreads=2` crash** (exit 127, reproduces on Tier1-only — NOT Tier 2; passes on re-run). | +| T-S3b | — | AT-LIMIT | — | [AT-LIMIT] `simd::any_hit_reduce(3)_avx2` (21 % DLL) | Core Fitch reduce. Disasm of `hor_or256` confirms GCC already emits register-only horizontal reduce (vextracti128/vpsrldq/vpor/vmovq) — the store-reload anti-pattern is elided. Compiler-optimal at -O2. No win. | +| T-S3c | — | AT-LIMIT | — | [AT-LIMIT] `uppass_node` scalar state-update loop (13 % DLL) | The update loop (ts_fitch.cpp:54-61) is scalar vs the vectorised `fitch_combine` in downpass. Micro-bench `dev/profiling/microbench/bench_uppass_combine.cpp`: AVX2 version bit-identical (value+changed flag, 0 mismatches) but only **1.22×** at n_states=4, and the 4-wide path does NOT trigger for 2-state (binary) morphology → ~1 % wall-clock. Not worth the incremental-uppass correctness risk (see fitch-scoring memory: dirty-flag invariant is delicate). | +| — | — | NOTE | — | [Strategic] Standard-Fitch is bookkeeping/strategy-bound, not scoring-bound | Per-candidate scoring at AVX2/compiler limit. Parity levers: (a) reduce per-clip O(n) bookkeeping — `build_postorder_prealloc` 5.2 % (rebuilt per clip+accept) + incremental down/uppass 6.4 %; (b) ratchet (63 %) evaluation economy. Aligns with `.positai/plans/2026-03-21-tnt-outperformance-analysis.md` (strategy > code). | + +## Round 5 (2026-06-18) — FRESH post-fix + unrooted-default build (HEAD 25e35be7) + +All rows above are STALE (pre Wagner fix 2b299e4b + pre unrooted-default). On the +fresh build the gap is throughput-only (~1.4–2.3× same-machine, NOT 10×; +efficiency at parity, gapB=0 — see log Round 5 / dev/plans/2026-06-18-gap-framing.md). +VTune on the TBR clip loop (Zanol2014, symboled `.vtune-lib-20260618212528`, +names via `nm`): + +| ID-suggest | P? | Status | Depends | Headline | Detail (% time, mechanism, verified Δ, micro-bench path) | +|------------|----|--------|---------|----------|---------------------------------------------------------| +| T-P5a | **P0** | **CONFIRMED dominant (full-EW)** | — | [Optimise/Algorithm] `compute_insertion_edge_sets` = **27.3 % of total CPU (≈47 % of TS self-CPU)** on the REAL full-EW workload — THE per-iteration deficit vs TNT | Full-EW VTune (dev/profiling/drivers/full-ew-vtune.R, Zanol2014 fitch ×2 reps3, symboled .vtune-lib-20260618212528, resolve_syms.R maps the `combine` lambdas back to the fn by Start Address): **1.96 s / 7.19 s = 27.3 %**, #1 by 2.5×. Breakdown: two scalar `combine` operator() lambdas 0.81+0.67 s + self ≈ 1.96 s; PLUS it drives most ucrtbase memory traffic (memset func@0x180020b2c 0.89 s + vector::assign 0.24 + malloc 0.18 + memcpy 0.08). Core scoring (any_hit_reduce_avx2 0.77 + fitch_indirect_length_cached 0.47 + …) ≈ AT-LIMIT (Round 3). This per-clip **O(N)** directional edge-set recompute is the recently-added unrooted-TBR code; TNT avoids it via INCREMENTAL length updates (Goloboff 1996) ⇒ it IS "what we're missing" per iteration (matches framing: throughput 1.3-2.3×, efficiency~1, so same candidates evaluated more expensively). Explains Step-1's mere 2 %: it removed only the small malloc, NOT the 1.48 s compute or 0.9 s zero-fill. **LEVERS:** (1) skip per-clip zero-fill [Step-2; needs write-before-read invariant + correctness gate; ~targets the 0.9 s memset]; (2) vectorize the combine [1.48 s/21 %; re-examine despite earlier memory-bound/state-poor caveat — absolute cost now too large to dismiss]; (3) **ALGORITHM: amortize/incremental edge-set across clips** [the real Goloboff fix; attacks the whole 27 %+; correctness-critical on the quality-fix fn]. | +| T-P5b | P2 | **Step 1 verified (neutral); small Δ** | T-P5a | [Optimise] `compute_insertion_edge_sets` per-clip alloc + zero-fill (~11–14 %) | **Step 1 (malloc/free hoist, KEEPS zero-fill):** `up`/`pre` now CALLER-owned plain-local buffers passed by ref (emutls lesson T-S3d: NOT thread_local), reused across clips. **Behaviour-neutral VERIFIED 2026-06-19:** score AND `candidates_evaluated` BIT-IDENTICAL base(.agent-p0) vs step1(.bench-step1), 6 EW(fitch) runs × {Wortley,Zhu,Zanol}×seed{1,2} (dev/profiling/drivers/ab.R + ab_compare.R). **End-to-end EW wall ratio 0.981** (~2 %; Wortley 0.887/tiny-abs, Zhu 0.969, Zanol 0.998/flat) — far below the 31 % clip-loop share (see dilution note). **Step 2 (skip zero-fill)** = pending; bisects memset cost from the malloc cost Step 1 just measured as ~2 %. | +| — | — | **NOTE (2026-06-19)** | — | [Dilution] The 31 % is clip-loop-ISOLATED, not full-search | T-P5a's 31 % came from the `ts_tbr_diagnostics` clip-loop driver (pure TBR-to-convergence). Full `MaximizeParsimony` (the MISSION workload) also runs Wagner + sectorial + ratchet reweight + consensus + R glue, so `compute_insertion_edge_sets` is a much smaller slice end-to-end — hence Step 1's ~2 %. RE-PROFILE DONE → see phase table below. | +| T-P5c | **P0** | OPEN | — | [Strategic] FULL-EW phase attribution: **RATCHET = ~60 %**, sectorial ~30 %, all TBR <8 % | Per-phase `result$timings` (driver dev/profiling/drivers/framing-phases.R, traces `ts_driven_search`, `.agent-p0` release DLL, no rebuild). Zhu2013 + Zanol2014 fitch × seed{1,2}, maxReplicates=3: **ratchet 57.9/59.9/60.2/62.4 %**; rss 14.4–17.6 %; xss 8.0–10.5 %; css 7.2–8.5 %; initial tbr 1.5–3.1 %; final_tbr 2.3–2.5 %; wagner 1.7–1.9 %; fuse 0–2.5 %. The mission's center of gravity is the **ratchet phase** (reweight→TBR-re-search cycles), NOT the isolated TBR clip loop. NB the Round-3 "63 % ratchet" was NOT stale — fresh build confirms ~60 %. CAVEAT: the per-clip cluster (compute_insertion_edge_sets + compute_from_above + vroot_cache) is CROSS-CUTTING — TBR runs inside ratchet+sectorial too — so its true inclusive cost needs VTune on a FULL run, not the 5 % `tbr_ms`. NEXT: read ts_ratchet.cpp for per-cycle redundant recompute; then VTune full-EW run, inclusive time on the cluster + ratchet internals. | +| T-P5d | **P0** | OPEN (validate) | — | [Recipe] ratchet is LOAD-BEARING but OVER-PROVISIONED: `ratchetCycles` 12→6 ≈ 20–38 % wall win, no quality loss | Ablation `dev/profiling/drivers/ratchet-ablation.R` + cycles sweep `ratchet-sweep.R`, `.agent-p0`, maxReplicates=6, seeds 1-3. (1) **Ratchet OFF** (`ratchetCycles=0L` — now GENUINELY disables; the [[ratchet-not-disableable]] gotcha is FIXED in-code, ts_driven.cpp:212+319) reaches best-known score only on TINY trees (Longrich 20t 0/3 miss); MISSES by +1..+4 on 37–75t (Wortley 2/3, Wills 1/3, Zanol 3/3, Zhu 3/3). ⇒ sectorial+TBR does NOT substitute for ratchet post-fix; "drop ratchet" refuted except small-dataset (matches TNT dropping it for small n). (2) **`ratchetCycles=6` ≥ 12 on quality at 0.62–0.79× wall** on all 4 (Wortley/Wills 0/3 @0.62–0.63×; Zhu 6c BEATS 12c, 1/3 vs 2/3 miss; Zanol wash at +1, cheaper). (3) Matched-wall off+4×reps substitution works small (Wills 0/3 @0.76 s) but FAILS large (Zanol/Zhu still miss). RECOMMEND `ratchetCycles=6` default + ratchet OFF for n_tip<~30 — VALIDATE at realistic maxReplicates (default 96, not 6) + Hamilton time-matched gate before flipping a user-facing default. | +| — | — | NOTE | — | [Strategic, fresh] The residual gap is per-clip state RECOMPUTATION TNT amortizes | `compute_insertion_edge_sets` + `compute_from_above` + `vroot_cache` rebuilt every clip = the per-clip overhead (~half of per-candidate time, perclip.R). T-P5a/b are the quick behaviour-neutral wins; the high-order lever is incremental cross-clip state maintenance (Goloboff 1996) — bigger, correctness-critical, scope separately. | +| T-P5e | **P1** | **BANKED (validate-merge)** | T-P5a L1 | [Optimise] **Lever 1 — skip per-clip zero-fill** (the big within-clip win) | Subagent (worktree), RELEASE libs, **bit-identical gate PASS** (score + `candidates_evaluated` identical base vs mod, {Wortley,Zhu,Zanol}×seed{1,2} @reps3 AND a reps10 heavy run). Wall: **Zanol2014 −16.4 %, sum −9.4 %** (heavy 10-rep, median of 3); gain concentrates on largest data (zero-fill is O(n_node×total_words)/clip), exactly as predicted. Change: `up`/`pre` caller-owned scratch (NOT thread_local — emutls T-S3d) + non-zeroing size-ensure; `#ifndef NDEBUG` write-before-read completeness guard; 3 call sites threaded (ts_tbr.cpp, ts_sector.cpp, ts_wagner.cpp). **COORDINATION:** parallel session `claude/perclip-edgeset-buf` (worktree TS-perclip) has UNCOMMITTED the SAME signature change but KEEPS the zero-fill (its own comment: "follow-up skips it — see T-P5b"). Lever 1 = that follow-up (superset). User must sequence the merge: perclip hoist → lever-1 zero-fill skip. Neither committed. | +| T-P5f | P2 | **AT-LIMIT** | T-P5a L2 | [AT-LIMIT] **Lever 2 — vectorize the `combine` lambda** — no real win | Subagent (worktree), RELEASE libs, bit-identical gate PASS (same 6 runs). Genuine AVX2 (runtime `cpu_has_avx2`), 4-wide. Wall: **sum −1.2 % (in ±2.5 % jitter), sign flips per-dataset** (Zhu2013 went SLOWER) — the noise signature. Mechanism: `n_states` tiny (Zhu 4, Wortley 8, Zanol 9; most chars fewer) → ~1 vector iter/block; the `set1`/horizontal-OR setup cancels the vector gain; the two combine sweeps are memory-bandwidth-bound (confirms T-S3c). Change is correct but DO NOT merge as a perf win. Any future win here is structural (fuse the two sweeps to halve buffer traffic — see L3a), not wider vectors. | +| T-P5g | **P0** | OPEN (advisor+data gated) | T-P5a L3 | [Algorithm] **Lever 3 — cross-clip amortization is the ONLY route to the asymptotic win; root-caused** | The named "incremental edge sets across clips." **Proof there is no within-clip shortcut:** for a clip of subtree c, `up_e[D]` (divided-tree directional msg) = `up_full[D]` IFF D is an **ancestor of c** (then c ⊆ subtree(D), inside D's complement-of-view, so unchanged); for ALL non-ancestors of c (≈ O(N) nodes), c leaves D's view → `up[D]` changes. So a per-pass full-tree precompute reuses only the O(depth) ancestor-path; the per-clip directional pass is irreducibly O(N). The genuine Goloboff amortization (O(N) **total** directional work/pass vs O(N²)) requires moving the clip **incrementally in tree-order** so adjacent clips share O(1) topology — which means ABANDONING restore-between-clips (Phase 2 `restore_prealloc_undo`+`spr_unclip`) and the cutoff-tightening clip SHUFFLE (`order_clips`). High-risk restructure of a function ≥2 parallel sessions are editing now. Design: dev/plans/2026-06-19-lever3-incremental-edgeset.md. **L3a (fuse the two `combine` sweeps into one preorder pass — `up[D]` then `edge_set[D]` inline while hot) is the safe within-clip remnant: bit-identical, composes w/ Lever 1, smaller marginal win (zero-fill already took the big chunk); offered, not yet measured.** | +| T-P5h | **P0** | **REFRAMES L3b → likely NO-GO** | T-P5g | [Strategic, DECISIVE] **TS per-candidate cost is FLAT in N (W-driven), not an O(N²) blowup ⇒ L3b is constant-factor, not asymptotic** | Hamilton 64-bit (job 17528864, COMPLETED) `framing_64bit.csv`: **TNT side FAILED** (tnt_rate/tnt_wall/throughput all NA — binary produced no output; gold-standard TNT comparison still owed, RE-RUN needed). BUT the TS-side 64-bit `ts_rate` (cand/ms) came through and is **flat across tips**: Wortley 37t≈13.9, Wills 55t≈13.4, Zanol 74t≈12.4, Zhu 75t≈20.1, Giles 78t≈20.9 — variation tracks **W/char-complexity** (heavy Zanol 12.4 vs light Zhu/Giles ~20), NOT tip count. If the per-clip O(N) directional pass were a per-candidate penalty, ts_rate would COLLAPSE with N; it does not (it's O(N) pass ÷ O(N) candidates/clip = O(1)/cand, already amortized). **Reconciles the local-32bit "throughput grows with N" (1.36@37→2.3@75, framing_latest.csv):** that trend is TNT-side (tnt_rate grew 18→44 with N — TNT speeds up per-candidate on bigger trees via cache/amortization) while TS stayed flat. So L3b's cross-clip restructure attacks only the directional pass's CONSTANT-FACTOR per-candidate share (~½ per perclip.R) — same order as L1(zero-fill, banked −16 %)/L3a(fuse) — at high restructure risk and shuffle-loss. **RECOMMEND NO-GO on the L3b restructure;** bank L1 (+measure L3a), treat the per-candidate deficit as constant-factor near-limit, shift to recipe composition (#39/#40). REVISIT L3b only if a SUCCESSFUL 64-bit TNT re-run shows tnt_rate asymptotically diverging from ts_rate in a way ONLY cross-clip amortization could match. | +| T-P5h2 | **P0** | **64-bit TNT CAPTURED — prize is gold-standard 2–3.4×, REOPENS L3b tension** | T-P5h | [Strategic] **Successful 64-bit head-to-head (Hamilton job 17529081, framing_64bit_log.csv)** | The TNT-headless harness fix (stdin-pipe + TERM=dumb, see [[profiling]]) WORKED — `tnt_rearr`/`tnt_rate` now populated (only `tnt_score` regex still misses this xmult format — immaterial). **64-bit rates (cand-or-rearr/ms):** Wortley 37t ts14/tnt17 = **1.2×**; Wills 55t ts13/tnt44 = **3.4×**; Zanol 74t ts12/tnt30 = **2.5×**; Zhu 75t ts20/tnt40 = **2.1×**; Giles 78t ts20/tnt41 = **2.1×**. Confirms T-P5h SHAPE (TS flat/W-driven, TNT rises with N), but the magnitude is **NOT soft 32-bit — it's a gold-standard ~2–3× per-candidate prize**, gapB=0 (all best-known). **TENSION:** T-P5h NO-GO'd L3b as "constant-factor near-limit" — but that constant factor is ~2–3×, and `perclip.R` measured the per-clip cluster (compute_insertion_edge_sets + compute_from_above + vroot_cache, the L3b target) as ~½ of per-candidate time = the bulk of this prize. "Flat in N" rules out an ASYMPTOTIC argument for L3b, NOT a 2× constant-factor one. **NEXT (settles it):** shared-start TBR race (dev/benchmarks/tbr_shared_start_lib.R) from identical t0 — if TNT reaches the same score per candidate ~2× faster (throughput) it's the bookkeeping (L3b reopens); if it's per-candidate parity but fewer candidates (efficiency) the gap is search-trajectory, not bookkeeping. Foundation under both ratchet (#43→#39) and sectorial races. | +| T-P5h4 | **P0** | **RESOLVED: efficiency gap = COUNTING ARTIFACT; only ~2.5× throughput is real, and it's per-candidate OVERHEAD (not scoring, not bound)** | T-P5h3 | [Diagnostic, DECISIVE] Bail-fraction counter (discriminator #2, worktree-isolated -DTS_SCORE_STATS build) | Instrumented `fitch_indirect_length_cached` (bail at ts_fitch.cpp:470). **95–99 % of scorer calls bail early** (~2.5 blocks touched on Zanol; ~1.8 on Giles — **the original "of 210 / of 236" was a UNITS ERROR corrected by T-P5l: 210/236 are the PATTERN counts, NOT block counts; Zanol packs into 4 blocks, so ~2.5 of 4 = ~71 % read, NOT ~1 %; the ~2.5-blocks-touched measurement itself stands**); **fully-scored candidates <1.2 % of n_evaluated** every dataset/seed. Bonus: Giles fic_calls 100K ≪ n_evaluated 585K → ~485K positions rejected UPSTREAM of the scorer (zero_skip=0). ⇒ TS's 2–4× `n_evaluated` lead over TNT (T-P5h3) is a **counting artifact** (TS counts every regraft incl. cheap early-exit; real work ≈ 5–7.5K/descent ≈ TNT's). **EFFICIENCY GAP DISSOLVES.** The ONLY real per-iteration gap is the **~2.5× throughput** (framing 64-bit; BOTH builds 64-bit so NOT bitness — genuine per-candidate cost). Since scoring is 99 %-cheaply-bailed (the bound/cutoff is EXCELLENT — so NOT bound-tightness, advisor's gate #2 → it's bookkeeping-side), the 2.5× lives in per-candidate OVERHEAD *around* the cheap scoring: edge_set_buf lookup + cutoff/divided_length arithmetic + regraft-loop machinery — the cluster TNT avoids via incremental length. **LAST CHECK = discriminator #3:** re-measure per-candidate cost breakdown POST-L1 (perclip.R) — if the edge-set cluster is ~30 % even perfect amortization caps at ~1.4× (can't close 2.5× → L3b stays NO-GO); if ~50 %+ the overhead is the lever. | +| T-P5h3 | **P0** | **SUPERSEDED by T-P5h4 (comparability RESOLVED)** | T-P5h2 | [Diagnostic] Shared-start strict-descent TBR race (dev/benchmarks/tbr_throughput_race.R, .agent-l1, local 32-bit TNT), discriminator #1 (advisor) aimed at LOSERS | From an IDENTICAL Wagner start, TS reaches an equal-or-better optimum but examines **2–4× MORE counted candidates** than TNT: Zanol ts 0.8M/tnt 0.2M (~4×), Zhu 0.6/0.3 (~2×), Giles 0.6/0.2 (~3×); scores parity (TS ≤ TNT by 0–3 steps — validity gate PASS). **TS per-candidate rate is clean & fast (16–21 Mcand/s, kernel-only timing) — matches framing 64-bit ts_rate (~12–20), so TS is NOT per-candidate-crippled.** TNT wall here is pure startup (~0.23 s flat, single-descent too short) ⇒ local isolated *throughput* is unusable; take throughput (~2.5×) from framing xmult instead. **Contradiction forcing discriminator #2:** if throughput(2.5×) AND efficiency(2–4×) were BOTH real, compound wall ≈ 5–10×, but framing whole-search wall_ratio is only ~2.5× ⇒ the candidate counts are almost certainly **NOT comparable** — TS `n_evaluated` counts every regraft incl. cutoff-bails (ts_tbr.cpp:1574); TNT likely omits bound-pruned. **NEXT = bail-fraction counter** in `fitch_indirect_length_cached` (bail at ts_fitch.cpp:470): if TS bails on ~50–75 % of candidates, real fully-scored count ≈ TNT's ⇒ efficiency dissolves, only the ~2.5× throughput remains (then chase WHY: bitness ~2× + at-limit kernel vs bookkeeping). Instrumented in ISOLATED WORKTREE (concurrency hazard: ts_fitch/ts_rcpp also touched by NA/IW agent). | +| T-P5e2 | **P1** | **MERGED to cpp-search 00d73d6a** | T-P5e | [Optimise] Lever 1 landed + re-verified on main checkout | Applied the validated diff to the main checkout (clean apply, both at 78b74147). **Re-gate on this checkout: all 6 EW runs bit-identical** to the documented values (score+cand exact, verify_l1.R); **NA single-threaded bit-identical** (Vinther2008 seeds1–4, na_serial_cmp.R); **276 kernel search tests PASS** (tbr/wagner/sector/ratchet/drift/fitch, NOT_CRAN). Committed 00d73d6a (NOT pushed) + NEWS bullet. **perclip resolved:** branch `claude/perclip-edgeset-buf` is ORPHANED (tip=25e35be7 is a cpp-search ancestor, no unique commits; uncommitted files last touched 2026-06-18 21:42; user confirms no live agent on it) and its changes are a strict SUBSET of Lever 1 (hoist only, keeps zero-fill) → nothing to redeem. **FOUND (out-of-scope):** a pre-existing intermittent CRASH in the **parallel (nThreads≥2) NA** path (Vinther2008) — reproduces on UNMODIFIED baseline (crashed iter 6/8), timing-race, present with TS_EV_NOCACHE=1 too; Lever 1 exonerated (baseline crashes; per-thread-safe). Filed as spawn_task task_3eda6e75 for the NA/IW workstream; repro = dev/profiling/drivers/repro_par.R. | +| T-P5i | P2 | **AT-LIMIT** | T-P5g L3a | [AT-LIMIT] **L3a — fuse the two combine sweeps** — no measurable win | Built fused single-preorder-pass (up[D] then edge_set[D] inline), **bit-identical** (all 6 gate values exact, .agent-l3a). Wall A/B vs committed Lever 1 (.agent-l1), 4 interleaved rounds, full auto search reps10: **Zhu2013 +0.4 %, Zanol2014 +0.2 % (both SLOWER, in noise)**. Mechanism: at mission tree sizes (37–88t) the `up[]` buffer is tiny (~10 KB for Zanol = n_node×W×8) and stays L1/L2-resident, so the two-pass form's second read of up[] is ALREADY a cache hit — fusing saves no memory traffic (it never went to memory). Would only help at vastly larger trees (up[] > cache). Reverted (Lever 1 two-pass form kept). Same lesson as L2: don't merge a non-win as perf. ⇒ within-clip edge-set kernel is now AT-LIMIT post-L1; only L3b (cross-clip, T-P5g/h) remains, and T-P5h leans NO-GO. | +| T-P5j | **P0** | **L3b CLOSED — DEAD by direct footprint+Euler measurement (this dataset class)** | T-P5g/h/i | [Algorithm, DECISIVE] Both incremental schemes fail the realizable-saving gate | Advisor measure-first reframe: 24–33 % is the SHARE; realizable saving is bounded by the per-clip CHANGED-VALUE footprint. Instrumented the from-scratch path (`-DTS_EDGESET_FOOTPRINT`, `src/ts_edgeset_footprint.h`, `ts_edgeset_footprint_report()`; driver `dev/profiling/drivers/l3b_footprint.R`; rows `l3b_footprint.csv`+`l3b_euler_gate.csv`, Wortley/Zanol/Zhu ×3 reps, 11–44k clips). **(1) Scheme-1 (patch-from-full-tree): footprint = 41–68 % of ALL edges change per clip** (Wortley 0.66, Zanol 0.47, Zhu 0.46; GO needed <0.3). Fitch combine does NOT saturate on these EW morphological matrices. Memory-traffic floor even at 0.41: recompute+undo-save+undo-restore ≈ 0.41×3 > 1.0 vs the bandwidth-bound clean sweep ⇒ LOSS. **(2) Euler-tour (cross-clip): per-descend-step delta = 1.14–1.24× the footprint** — one parent→child boundary move flips AS MANY OR MORE views (delta = tiny chunk 5–7 newly-exposed + LARGE common-changes 44–67 pre-existing) ⇒ NO cross-clip locality; a small topological change near the clip propagates view-changes across ~½ the tree via the non-saturating intersect-else-union. This delta is a true FLOOR on any Euler kernel (advisor-verified) and is *optimistic* (omits early-term boundary checks + the measured ~1.2× DFS candidate cost + reroot/restore integration risk). **Edge-set hotspot AT-LIMIT; L3b ABANDONED.** Confirms T-P5h4's "~30 % cluster ⇒ amortization caps ~1.4×" prediction and resolves the T-P5h2 tension (the 2–3× prize is NOT recoverable via incremental edge_set). Worktree code MEASUREMENT-ONLY — nothing to merge; instrumentation KEPT (fp_frac data-dependent, lower on bigger/denser ⇒ molecular/large-N is the REOPEN condition, a ~10-min rerun). **NOT killed:** bound-then-verify/lazy-exact (cheap admissible screen → exact for survivors = TNT quick-TBR; distinct, untested, NOT NOW — scoring already at-limit T-P5h4, and no admissible Fitch-insertion lower bound established). ⇒ per-candidate throughput declared at-limit; pivot to recipe composition + sectorial (#39/#40). **CORRECTED by T-P5k: the closing 'per-candidate throughput at-limit' line OVER-CLAIMED — at-limit was proved only for (a) combine throughput + (b) cross-clip reuse, NOT the production per-candidate path. Scorer thread REOPENED (#46).** | +| T-P5k | **P0** | **REOPENS scorer — per-candidate is NOT at-limit (T-P5j over-claimed)** | T-P5j | [Diagnostic, DECISIVE] VTune per-candidate split, post-L1 symboled (worktree `.vtune-lib-sym`, footprint `#ifdef`-compiled-out → clean production binary; `DLLFLAGS=-static-libgcc` to defeat the `-s` strip), Zanol2014 full-EW | Discriminator #3 (advisor) — queued in T-P5h4, finally run. Flat self + call-tree CSV, module=`TreeSearch.dll`, 6.4s CPU. **Per-candidate cluster ≈ ½ of EW CPU, split ~56% PRECOMPUTE / ~44% CONSUMPTION:** PRECOMPUTE = `compute_insertion_edge_sets` building the full ~210-block directional views = two combine `operator()` lambdas (ts_fitch.cpp, addrs adjacent to the fn) **0.731+0.683=1.41s** + self 0.156 + `uppass_node` 0.154 ≈ **1.8s**; CONSUMPTION = bail-fast scorer `fitch_indirect_length_cached` self 0.424 + `any_hit_reduce_avx2` 0.292 + horiz-reduce intrinsics (`_mm_or_si128` 0.123 + `_mm256_extracti128` 0.048) + `popcount64` 0.046 + regraft-loop machinery (`tbr_search` self 0.340) + `fitch_join_states` 0.063 ≈ **1.4s**. **NEITHER is at-limit as T-P5j asserted:** the combine THROUGHPUT is at-limit (T-P5f/S3b) but it is AVOIDABLE work, not irreducible; consumption is dominated by per-candidate FIXED overhead (SIMD setup paid ~2.5×/cand for a ~2.5-block bail = T-P5f's 'set1/horizontal-OR setup cancels the gain' signal, MIS-FILED at-limit; + loop machinery), NOT the at-limit combine kernel. **Reconciles the 2.5×:** the eager 210-block view-build (T-P5h4: 99% of scorings consume only ~2.5 blocks) ≈ the per-candidate THROUGHPUT gap TNT skips via incremental length. **CAVEATS (honest, advisor guard):** (1) realizable WALL win ~1.2-1.5×, NOT 2.5× — the per-candidate cluster is only ~½ of EW CPU (rest = ratchet reweight, sectorial, hashing, R glue, memory); 2.5× is the per-candidate THROUGHPUT metric, not end-to-end wall. (2) MECHANISM still ambiguous: avoidable-precompute (lazy / incremental-length = TNT quick-TBR) **vs** pervasive constant-factor (TNT does the same work ~2.5× faster via layout/scalar). The lazy route is NOT a slam-dunk — L3b (T-P5j) showed incremental FULL-view maintenance lacks locality here (footprint 41-68%), and lazy-per-block trades amortized O(all-blocks)/edge for un-amortized O(depth×bail-blocks)/cand, an untested tradeoff (the bound-then-verify route, still needs an admissible Fitch-insertion bound). **NEXT:** discriminator #2 micro-bench — scalar-inlined vs SIMD `fitch_indirect_length_cached` in the bail regime (real Zanol clip, millions of calls, ≥3 medians) settles the cheap CONSUMPTION lever; separately scope the bigger PRECOMPUTE/incremental-length prize. `result_p5k_consume` deleted post-round; symboled lib KEPT for #2. **CORRECTED by T-P5l (below): the "~210-block / 98%-unread" model is WRONG — Zanol2014 EW DataSet = 4 blocks × n_states=9 (total_words=36); the scorer reads ~71% of each node's view (mean bail 2.85/4), NOT ~1%. CONSUMPTION scalar-inline lever DEAD; PRECOMPUTE lazy ceiling shrinks to ~29%-unread.** | +| T-P5l | **P0** | **scorer-REDUCE scalar-inline lever DEAD (verified); corrects T-P5k's "210" denominator (CONVERGES w/ T-P5h4)** | T-P5k | [AT-LIMIT] Discriminator #2 bail-regime micro-bench on REAL captured triples, Zanol2014 EW | Standalone `dev/profiling/microbench/bench_scorer_bail.cpp` — FAITHFUL codegen: built `-O2` with NO global `-mavx2` (verified `R CMD config CXXFLAGS` = `-g -O2 -msse2` only), AVX2 reduce under `__attribute__((target("avx2")))` so it stays a NON-inlined call boundary exactly as the package build (the separate any_hit_reduce_avx2 self-time entry in T-P5k). Replays **4000 REAL `(clip_prelim,vroot,cutoff)` triples** captured live from the SPR scorer call site (`-DTS_CAPTURE_TRIPLES` hook ts_tbr.cpp, stride-37 across the whole search; **landed via PKG_CPPFLAGS — PKG_CXXFLAGS is silently clobbered empty by ~/.R/Makevars.win**, see [[profiling]]). **CORRECTNESS:** 0 variant mismatches (BASE=SCALAR=THRESH); **100% of records reproduce production `extra` bit-exactly** → faithful. **BLOCK-COUNT CORRECTION (the big one):** Zanol2014 EW DataSet = **4 blocks, all n_states=9, total_words=36** — NOT "~210 blocks". T-P5h4/T-P5k's "bails ~2.5 of ~210 blocks (99% unread)" made a DENOMINATOR/units error — the ~210 = the PATTERN count (Zanol `nr=210` CONFIRMED, 213 chars; Zhu `nr=253`), mislabelled as the block count; **the "~2.5 blocks touched / 95–99% bail" MEASUREMENT ITSELF STANDS** (213 chars pack into 4 blocks of ≤64). Real bail = **mean 2.85 of 4 blocks** (hist 2:1490 3:1632 4:878; 22% full-scan) ⇒ the scorer reads **~71%** of each node's directional view, not ~1%. **RESULT (median of 9 reps, 3M scorings/rep, clean set):** BASE(AVX2 dispatch) **21.18 ns**; SCALAR(force scalar) **22.28 ns = 0.95× (SLOWER)**; THRESH(scalar if ns<4) **20.91 ns = 1.01× (noise)**. At n_states=9 AVX2 is already optimal — T-P5f's "small-n_states setup cancels the gain" does NOT apply (Zanol alphabet=9, not tiny). ⇒ **the scalar-inline-the-REDUCE lever = DEAD; no rewrite warranted; the SIMD reduce is AT-LIMIT for multistate morph.** This PROVES (was only asserted in T-P5h4/j) the thesis "the gap is bookkeeping, not scoring" — CONVERGENCE, not contradiction. **SCOPE:** the bench covers the reduce (≈⅔ of T-P5k's "consumption" = any_hit_reduce+popcount+scorer loop, ~0.93s); the regraft-loop machinery (`tbr_search` self ~0.34s, the other ⅓) is general loop code, NOT a scalar/SIMD lever — untouched here, no cheap win identified. **IMPLICATIONS:** (1) T-P5k's PRECOMPUTE prize SHRINKS — "98% unread" rested on the bogus 2.5/210; reality ~29% unread/node ⇒ lazy-per-block (a) ceiling ~29% of precompute, and the consumed-UNION across candidates is likely ≈ all 4 blocks → near-zero realizable. **CONFIRMED by the M46 M1 gate (branch `claude/lazy-precompute-m46`, see its `dev/profiling/consumed_union_RESULTS.md`): per-clip consumed-block union frac = 1.00/1.00/0.999 (Wortley/Zanol/Zhu), cost-weighted saving = 0 ⇒ lazy-per-block (a) DEAD; result is count- AND seed-invariant. Two independent passes (this + supervisor) agree. Only incremental-length (b) = TNT quick-TBR remains, an architectural rewrite.** The 2.5× throughput gap does NOT map onto avoidable view-building; only incremental-length (b) = TNT quick-TBR (don't build per-node views at all) is a substantial precompute route, and that's the architectural rewrite. (2) **CONDITIONAL REOPEN:** scalar/threshold MIGHT beat AVX2 for n_states ≤ ~4 (binary 2-state morph / DNA) where AVX2 setup doesn't pay — UNTESTED; mission datasets are multistate (ns≥8). Bench + blob (`ts_triples.bin`) KEPT (gitignored microbench/) for that reopen. `.cap-lib` capture build deletable. **[X-REF T-P5n/o — "kernel at-limit" ≠ "zero overhead around the kernel"]:** this row certifies the SCORER math (reduce/combine) at-limit; it does NOT certify the surrounding scaffolding. A per-clip diagnostic `std::getenv("TS_REVERT_CHECK")` in `tbr_search`'s Phase-2 teardown was costing ~13–22 % of EW wall the whole time, HIDDEN inside VTune's ucrtbase self-time (unnamed `func@0x...` / `strcoll_l`), so this kernel-isolation micro-bench never saw it. Found + banked T-P5n (hoist merged beb52138, f9ca3328); the post-getenv VTune re-survey (T-P5o) confirms getenv now absent. **Lesson:** trust "kernel at-limit" for the math, but µs-scale CRT/getenv calls in hot loops hide in profiler *self-time* buckets right next to the kernel — A/B the wall, don't trust the flat profile's named hotspots. | +| T-P5m | P1 | **TBR keystone CLOSED — `tbr_search` loop self-time is BELOW the worth-it floor (a magnitude verdict, NOT a ceiling claim)** | T-P5k/l | [Below-floor; reading + arithmetic, no fresh collect] Settles the slice T-P5l explicitly PUNTED ("general loop code, no cheap win"). **Measured share (T-P5k):** `tbr_search` self = **0.340 s / 6.4 s EW CPU ≈ 5 %** — the per-candidate scaffolding AROUND the (at-limit T-P5l) scorer CALLS, in the SPR loop (ts_tbr.cpp:1537-1596) + the TBR reroot loop (1651-1835). **Read both loops end-to-end (ts_tbr.cpp:1179-1857):** NO hidden allocation, NO O(N) redundant recompute — every buffer (`from_above`, `vroot_cache`, `edge_set_buf`, `fast_undo`, `seen_vp_hashes`, …) is pre-allocated ONCE outside the clip loop; the loops already carry the T-245 4-wide flat batch (1704), `__builtin_prefetch` (1766), the VpHashSet dedup (T-S3d), and a per-clip `vroot_cache`. **Only nameable micro-lever** = hoist the per-candidate `cutoff = best_candidate − divided_length + 1` (1578/1697/1798/1816) to recompute only when `best_candidate` changes: ~30 M candidates (Zanol n_calls 28.7 M) × ~2.5 cyc ÷ 3 GHz ≈ **0.024 s ≈ 0.4 % EW** — below the /profile worth-it floor (a ~2 %-fn × 50 %-speedup ⇒ ~1 % wall heuristic). Branch-templating `` to drop the null/false `sector_mask`/`constrained` checks = branch-prediction-covered ⇒ noise. **Conflation corrected (advisor):** the 5 % is NOT all "irreducible overhead" — it lumps (a) true loop control (cutoff/skip/accept), (b) the dedup machinery (`memcmp` 1660 + `fast_hash` 1666 + VpHashSet — a VALIDATED scoring-SAVER, T-S3d), and (c) `fitch_join_states` per sub_edge (rerooting's NECESSARY work). So the honest claim is **"no lever above the worth-it floor at 37–78 t,"** NOT "physically irreducible" (a ceiling claim would owe a source-line VTune; this is a magnitude claim already grounded by T-P5k). The only further win is the **lever-b incremental-length rewrite (TNT quick-TBR), separately deferred.** **REOPEN at ≥180 t** — `vroot_cache` leaves L1 (the reason prefetch 1766 exists), shifting the scaffolding/cache balance; same molecular/large-N reopen shape as L3b/T-P5j. ⇒ **TBR ELEMENT CLOSED on both isolation gates: kernel at-limit (T-P5l) + precompute dead (M46/T-P5j) + scaffolding below-floor (this) + shared-start race done (#37/T-P5h3-4). Keystone cleared → sectorial (#39).** No fresh VTune collect — the **6.4s = TOTAL EW `MaximizeParsimony` self-CPU (full search, NOT TBR-only; full-ew-vtune.R post-L1, 7.19s pre-L1 T-P5a)**, already measured T-P5k; so `tbr_search` 0.34s = 5% of the WHOLE search, banked across every phase (ratchet/sectorial call tbr_search internally). **AGGRESSIVE-MODE REFRAME (task #48, supervisor 2026-06-20): the /profile worth-it floor is the WRONG gate for this mission** (never ROI-gated, [[tnt-outperformance-is-diagnostic]]). The BIG-lever verdict (kernel at-limit T-P5l + precompute dead M46/T-P5j) STANDS, but the sub-floor EXACT bit-identical micro-levers are now being BANKED, not skipped: cutoff hoist (re-estimated ~**0.26% EW** once you see the reroot flat path already amortises cutoff_b per batch-of-4 at 1697 — only SPR 1578 + scalar-reroot 1798 recompute per-candidate) + int-vs-double EW accept + the per-sub_edge join/memcmp/hash prologue. Downside bounded at ZERO (byte-identical trajectory ⇒ wall stays-or-improves, never regresses); arbiter = faithful T-P5l-style micro-bench (end-to-end washes in ±2% jitter at <0.5%). Bank each non-regressing lever behind the L1 bit-identical gate. **[X-REF T-P5n — "closed" was about the loop CODE, not a hidden getenv]:** this row's 0.34 s `tbr_search`-self figure counts the LOOP code only; the per-clip `getenv("TS_REVERT_CHECK")` (~13–22 % EW wall) was attributed to ucrtbase self-time, NOT to `tbr_search` self — so "scaffolding below-floor / TBR closed" held for the loop while a profiler-invisible getenv cost remained until T-P5n (hoist merged beb52138). FOLLOW-UP: a per-call hoist of the per-reroot `TS_PHYS_REROOT` getenv (2133 → `phys_reroot` bool at 1277; branch `claude/tbr-phys-reroot-hoist`, byte-identical, ts-tbr/ratchet tests 45/45) clears the last hot/warm-path getenv in `tbr_search`. Read "TBR closed" as "no scoring/loop lever above floor", NOT "no overhead around the kernel". | +| T-P5n | **P0** | **BANKED: per-clip TS_REVERT_CHECK getenv = 13-19% of EW wall (Windows); exhaustive TBR sweep otherwise confirms per-candidate path AT-LIMIT** | T-P5m | [Optimise, MEASURED] Aggressive-mode banking sweep (task #48, branch `claude/tbr-microlevers` off da0f203f, NOT pushed); 51-lever discovery workflow + 2 deep feasibility agents + 3-way attribution A/B | **THE WIN (OVERTURNS T-P5m's "0.4% sub-floor"): a DIAGNOSTIC `std::getenv("TS_REVERT_CHECK")` left in the per-clip teardown (ts_tbr.cpp:1852, ~100k+ calls/search) was costing 13-19% of EW wall.** Hoist to a per-call bool (commit 3a50537e) ⇒ byte-identical (score+candidates_evaluated identical, Wortley/Zhu/Zanol×seed{1,2}). Wall, QUIET machine, same-seed paired, REPS6: **Zanol -13.2% (20/20, p=0), Zhu -19.1% (12/12, p=0)**. **3-way attribution** (base / getenv-restored / fully-hoisted, same-seed, scores byte-identical): the getenv hoist ALONE = the FULL 13-19%; **cutoff hoist (6295c401) + collapsed-empty hoist = +0.00%** (negligible — the verifiers were RIGHT about those; kept, harmless). **WHY MISSED:** getenv is µs-scale on Windows/ucrt (locked env-block linear scan + UTF conv), NOT the sub-ns the verifiers/T-P5m assumed; the FIRST A/B (-0.15%) ran under a 30-agent workflow (±16% noise swamped it); and in the T-P5a VTune the getenv cost likely HID inside ucrtbase "memory traffic" self-time, so no prior round flagged it. **CAVEAT (honest):** magnitude is ENV-SIZE + platform dependent (Windows/ucrt large; Linux cheaper) — this is Rscript-via-Git-Bash; Hamilton/Linux confirmation owed (queued, supervisor); but byte-identical + strictly removes ~100k getenv/search ⇒ unambiguously good regardless. **EXHAUSTIVE SWEEP otherwise CONFIRMS per-candidate/per-clip AT-LIMIT at 37-78t:** lever-b (incremental-length = TNT quick-TBR) **DEAD** (opus feasibility agent: O(1)-slide hits the L3b non-invertibility wall + the irreducible up-pass TNT also pays; the only buildable skip-combine variant = sub-lever(d) which REGRESSES the dominant reroot path because edge_set_buf[below] is REUSED n_sub_edges× → materialization is amortized+beneficial); **batching flat-x4→ratchet/SPR REGRESSES** (kernel read: x4 does 4 SEPARATE reduces, only interleaved for latency-hiding which WASHES when cache-resident at 37-78t, while LOSING per-candidate early-bail 2.85→4 blocks); **vroot-memcpy-elim regresses** (per-access main_edges load > saved copy, row reused n_sub×); scorer reduce **at-limit** (T-P5l). **ONLY remaining real-magnitude TBR item = `build_postorder_prealloc` incremental maintenance (5.2% CPU, per-clip O(n) DFS rebuild) — DESIGN-ONLY, order-dependent, HIGH-risk; NOT done unsupervised, flagged for supervised build+oracle.** **PATTERN:** diagnostic env/CRT calls in hot loops = a hidden cost CLASS — ts_sector.cpp `TS_FREE_HTU_PROBE` (802/848/895) + `TS_SECT_DEBUG` (1147) are PER-SECTOR (moderate) ⇒ flagged for the sectorial agent; cancel-file getenvs (ts_driven:654/ts_parallel:300) ALREADY hoisted (fine). **SUPERVISOR:** merge 3a50537e (the getenv win, P0) + 6295c401 (cutoff, ~0 but exact, optional); `kept_ei`/hoist-valid-ei-list lever (commit 8291bbec, gate 6/6 byte-identical): isolation A/B vs the getenv-fixed mod = **MARGINAL** (Zanol -0.1% wash, Zhu -2.3% median p=0.18) — per-clip kept_ei build cost ≈ saving at 37-78t; scales favorably with tree size; OPTIONAL merge, reopen larger N. | diff --git a/dev/profiling/focus-areas.md b/dev/profiling/focus-areas.md new file mode 100644 index 000000000..f6f9aaaac --- /dev/null +++ b/dev/profiling/focus-areas.md @@ -0,0 +1,64 @@ +# Profiling focus areas + +Ranked by `(estimated wall-time share) × (remaining fixability)`. Areas at the +memory-bandwidth ceiling or already optimised drop to the bottom but remain +visible so the rotation knows to skip them. + +Signals used to build this list: +- Phase distribution from `.positai/expertise/profiling.md` (Zhu2013, 75 t, + thorough preset, post-T-261/T-262/T-263, 2026-03-27). +- Hot Rcpp entries grepped from `src/ts_rcpp.cpp`. +- Active and parked profiling tasks in `to-do.md` (T-274, T-298, T-300, + S-PROF round 7). +- File mtimes vs S-PROF last-run date (2026-05-12). + +Statuses: `NEW` (never profiled), `PROFILED` (profiled, no fix yet), +`OPTIMISED` (fix shipped — re-profile if `src/` changes), `AT-LIMIT` (no +further wins — skip unless code changes), `SKIPPED` (out of rotation). + +| # | Area | Files | Why hot | Last known cost | Last profiled | Status | +|----|-------------------------------------|------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|--------------------------------------------------|---------------|------------| +| 1 | NNI-perturb in driven pipeline | `src/ts_nni_perturb.cpp`, `src/ts_driven.cpp` (perturb call sites) | Disabled in thorough preset via T-274 (`nniPerturbCycles=0L` in `R/MaximizeParsimony.R`) — code on path only when caller sets `nni_perturb_per > 0` | T-274 filed; disabled at R level; re-evaluate only if default changes | 2026-05-18 | AT-LIMIT | +| 2 | Ratchet inner loop | `src/ts_ratchet.cpp`, `src/ts_tbr.cpp` (called from ratchet) | 62 % of inner-loop search time (verbosity=2, Zhu2013 thorough, 2026-05-18); TBR dominates (perturbation overhead < 2 %) | 2.80 s/rep median (Zhu2013 thorough ×1 rep, nThreads=1); T-300 (`full_rescore`) is pending fix | 2026-05-18 | PROFILED | +| 3 | RSS / sector search | `src/ts_sector.cpp`, `src/ts_prune_reinsert.cpp` | THROUGHPUT at-limit by inheritance (R6 2026-06-20): ~96 % is inner+global tbr_search (at-limit kernel); sector scaffolding ≤2 %. Banked T-S6c byte-identical ~2.8 %; T-S6d per-clip getenv ~22 % (TBR-wide). Efficiency axis (work-to-target) untouched. | inner tbr_search-dominated; see findings R6 | 2026-06-20 | AT-LIMIT | +| 4 | TBR full-rescore at acceptance | `src/ts_tbr.cpp:1138` (`full_rescore` after every accepted move) | T-300 RESOLVED — dirty-set incremental rescore landed for SPR accept (EW path `fitch_dirty_*`, NA path `fitch_na_dirty_*`); GHA-green; 15.2 % wall-time speedup on Zhu2013 (3.88 s → 3.29 s) confirmed via dev/profiling/t300_na_bench.R 2026-05-19 | resolved | 2026-05-19 | DONE | +| 5 | quartet_concordance.cpp allocation | `src/quartet_concordance.cpp` | T-298 active PR #242 — matrix allocation hoist already benchmarked; re-profile after merge | hoist-fix in flight | 2026-05-12 | PROFILED | +| 6 | CSS / XSS sector pipeline | `src/ts_sector.cpp`, `src/ts_simplify.cpp` (`ts_simplify_diag` entry) | Same verdict as #3 (R6): XSS uses search_sector (=RSS scaffolding ≤2 % + inner tbr_search); CSS uses sector-masked tbr_search directly — both inner-tbr-dominated ⇒ THROUGHPUT at-limit by inheritance. T-S6c levers + T-S6d getenv apply to all three modes. | inner tbr_search-dominated | 2026-06-20 | AT-LIMIT | +| 7 | Hierarchical resampling parallelism | `src/ts_resample.cpp`, `R/Resample.R` | HSJ/XFORM hierarchical resampling 2-thread speedup 1.1× (vs Brazeau 2.5×) — serial R loop | known limitation (2026-03-19 Agent A) | — | NEW | +| 8 | Simulated-annealing phase | `src/ts_temper.cpp` | 7.4 % at 180 t, 14 % hit rate, 0.8 steps/s — efficiency far below ratchet (4.5) or XSS (13.8) | 1241 ms/rep at 180 t (T-179) | — | NEW | +| 9 | MaddisonSlatkin solver | `src/MaddisonSlatkin.cpp` | Hash-map infrastructure was 53 % of DLL time; T-151/T-152 raised but check if shipped | ~1.4 s (6 %) gain estimated cold-cache | 2026-03-19 | PROFILED | +| 10 | Wagner tree construction | `src/ts_wagner.cpp` | < 0.1 % of search time on all datasets ≤ 88 t | 300–1400 µs / tree | 2026-03-18 | AT-LIMIT | +| 11 | Per-candidate indirect scoring | `src/ts_driven.cpp`, `src/ts_fitch.cpp`, `src/ts_fitch_na_incr.h` | At memory-bandwidth ceiling (~23 ns / 75 tips). T-075 confirmed no further wins. | 23 ns / candidate (75 t) | 2026-03-18 | AT-LIMIT | +| 12 | R-loop search engine (`MaxParsi`) | `R/MaximizeParsimony.R`, `R/TreeSearch.R` | < 0.5 % of wall time (Rprof, 2026-03-18). R is a passenger, not a bottleneck on hot path. | < 0.5 % | 2026-03-18 | AT-LIMIT | +| 13 | **Standard-Fitch path (TNT-parity)** | `src/ts_tbr.cpp`, `src/ts_fitch.cpp`, `src/ts_simd.h`, `src/ts_tree.cpp` | The `-`→`?` path (has_na=FALSE) the TNT benchmark uses; ~20× faster/rep than NA. tbr_search self 25 %, SIMD 21 %, uppass 13 %, per-clip bookkeeping 18 % | Zhu2013 627 in 0.56 s/rep; total DLL 2.70 s/8rep | 2026-06-16 (r3) | PROFILED | +| 13a| → per-clip bookkeeping | `ts_tbr.cpp` (build_postorder, collect_*, compute_from_above), `ts_tree.cpp` | postorder rebuilt every clip+accept (5.2 %) + incremental down/uppass (6.4 %) — TNT minimises exactly this | — | 2026-06-16 | NEW (top code lever) | +| 13b| → SIMD reduce / uppass arithmetic | `ts_simd.h`, `ts_fitch.cpp:54` | any_hit_reduce 21 % (compiler-optimal); uppass scalar loop 1.22× only | — | 2026-06-16 | AT-LIMIT | + +## Notes on the ranking + +- Area #1 (NNI-perturb) is now AT-LIMIT — disabled in the thorough preset + via T-274 (`nniPerturbCycles=0L`); the code path is only active when a + caller explicitly sets that parameter, and profiling dead code is wasteful. +- Areas #2–#3 are the live wins: Ratchet has the largest absolute share + (now ~60–70 % after NNI-perturb disabled) with no per-line profile yet; + RSS grew 16× without a profile pass to confirm cost source. +- Area #4 (T-300 lazy rescore) is PARKED in `to-do.md` but stays in the + rotation because the path is well-understood and the predicted gain is + large; rerun after T-300 lands to verify. +- Areas #5 and #9 are PROFILED — skip unless their files change. +- Areas #10–#12 are AT-LIMIT — recorded so the rotation skips them. +- Per the skill's `init` rules, IO setup, config readers, and CLI parsers + are SKIPPED (not listed) and only profiled on explicit `/profile `. + +## Profvis-required cases + +Per the skill's tool guide, areas with non-trivial R surface area on the hot +path must go through `profvis` before VTune so we don't miss a `[Port]` +finding: + +- Area #7 (hierarchical resampling) — bulk of the loop is in R. +- Area #12 (R-loop search) — already known to be < 0.5 %, but rerun profvis + if `R/MaximizeParsimony.R` or `R/TreeSearch.R` change to confirm. + +All other areas are pure C++ on the hot path and can go straight to VTune +after the dry-run. diff --git a/dev/profiling/kpi-2026-06-21.md b/dev/profiling/kpi-2026-06-21.md new file mode 100644 index 000000000..c7f3be28e --- /dev/null +++ b/dev/profiling/kpi-2026-06-21.md @@ -0,0 +1,108 @@ +# Mission KPI re-measure — TS vs TNT wall, current production cpp-search (2026-06-21) + +**Job:** Hamilton build `17533015` (cpp-search `5ee3ba3c`, freshness-asserted +`ratchetCycles==6L`) → timing jobs `17533016-19`. Harness +`dev/benchmarks/hamilton_timing.R` (TS default+thorough vs TNT +mult-basic/xmult-default/xmult-level10, scored by `TreeLength`, NSEED=3, +maxSeconds=600 — cap never hit). Raw CSVs: `dev/profiling/kpi-2026-06-21/`. + +**Why re-run:** the prior grounding (Jun-18) predated the three largest merged +production wins — the per-clip **getenv hoist** (`beb52138`, ~20–26 % mission +wall), the **sector micro-levers** (`00967d77`), and the **ratchet 12→6 flip** +(`5ee3ba3c`, ~20–38 %). So the old number is badly stale. + +## Results (median over 3 seeds; wall in s) + +| Dataset (tips, ns) | opt | TS default | TS thorough | TNT mult-basic | TNT xmult-default | TNT xmult-level10 | +|---|---|---|---|---|---|---| +| Wortley2006 (37) | 479 | **479** @ 1.8 | 479 @ 2.7 | 479 @ 0.2 | 480–482 @ 0.1 | 479 @ 0.9 | +| Giles2015 | 670 | **670** @ 6.3 | 670 @ 12 | 670 @ 0.4 | 670 @ 0.2 | 670 @ 2.7 | +| Zhu2013 | 624 | **624** @ 23 | 624 @ 47 | 624–626 @ 0.4 | 624 @ 0.2 | 624 @ 2.8 | +| Zanol2014 (ns=9) | 1261 | **1261** @ 39 | 1261 @ 45–86 | 1262 @ 0.4 | 1261–1262 @ 0.2 | 1261 @ 3.2 (+1 NA run) | + +(Bold = TS reaches the optimum on every seed.) + +## Findings + +**1. QUALITY CLOSED — TS ≥ TNT.** TS reaches the optimum on *every* dataset/seed. +TNT's fast configs frequently land **+1** (1262 Zanol, 625/626 Zhu, 480–482 +Wortley). On the hardest dataset (Zanol, ns=9) **TS default is the only config +that reliably hits 1261 (3/3)** — TNT xmult-default hits it 1/3, mult-basic 0/3 +(+1), and even xmult-level10 is flaky (2/3 + one NA/failed run). TS's +thoroughness *buys reliability* on hard data. + +**2. The big wall ratio is a DEFAULT-BUDGET mismatch, not (mostly) inefficiency.** +The KPI compares each engine's *default* run to completion: TS's `default` runs a +HEAVY search (many replicates) while TNT's `xmult` default is a LIGHT one — both +land on the optimum, so the ~8–110× is dominated by *how much each default chooses +to search*, not by per-unit speed. Decomposing +`wall_ratio = (candidates_TS / candidates_TNT) × throughput`: +- **candidate-efficiency ≈ 1.5× (near-parity)** — the converge-mode head-to-head + (`dev/benchmarks/headtohead_phase0.csv`, COUNT-based ⇒ bitness/throughput- + independent) shows `cand_ratio` 1.2–1.9× across *all* datasets when both run a + substantial search. TS examines only ~1.5× the candidates TNT does to converge — + algorithmic efficiency is near-closed, **not** the prize. +- **throughput ≈ 2×** (measured at-limit, T-P5l/T-P5h) — and at equal search effort + `ts_wall/tnt_wall` ≈ 1.6–2× (phase0), consistent with ~1.5× × ~2×. +- **the remaining factor is budget CHOICE** — recoverable in principle. + +**3. Composition #40 is a HYPOTHESIS — real but MODEST and reliability-bounded, NOT +an order-of-magnitude prize.** Three cautions the raw ratio hides: +- **The ratio is biggest where wall is cheapest.** Wortley (1.8 s) and Giles (6 s) + are already fast; the headline ratios are on runs costing seconds. The case that + *motivates* the mission — Zanol (39 s, ns=9) — has the *smallest* head-room. +- **On Zanol the thoroughness is LOAD-BEARING.** TS's heavy default is the *only* + config that reliably hits 1261 (3/3); TNT's light defaults miss (1262). Cutting + Zanol's budget to chase the ratio risks forfeiting the reliability just banked — + you do **not** get to assume 1261 at 4 s. +- **Proven head-room (a floor, not a ceiling):** TS `thorough` reaches the *same* + score as `default` at ~2× the wall on all four ⇒ `thorough` is pure waste here, + so there is *some* real over-provisioning down to (at least) `default`. Whether + there is more *below* `default` is exactly the open #40 question. + +## Ratchet isolation race (#39 gate-2, job `17533025`) + +TS `ts_ratchet_search` vs TNT `ratchet=iter 30` from an identical Wagner start, +seeds 1–5 (`dev/profiling/kpi-2026-06-21/ratchet_race.csv`): + +| dataset | ts_final | tnt_final | ts_wall | tnt_wall | wall_ratio | +|---|---|---|---|---|---| +| Wortley2006 | 481 | 479 | 0.07 | 0.08 | 0.81 | +| Zanol2014 | 1262 | 1262 | 0.43 | 0.16 | 2.63 | +| Zhu2013 | 625 | 625 | 0.30 | 0.17 | 1.77 | +| Giles2015 | 670 | 670 | 0.32 | 0.15 | 2.15 | + +- **Cycle-quality: PARITY.** At a fixed 30-iteration budget, TS and TNT ratchet + reach the *same* score (Zanol 1262=1262, Zhu 625=625, Giles 670=670; Wortley +2 + on a tiny noisy dataset). **TNT does NOT reach the optimum in fewer reweight + cycles** — the gate-2 ratchet question answers *no*. +- **Wall: ~1.8–2.6×**, i.e. the at-limit per-candidate throughput — *not* a + ratchet-specific inefficiency. +- Both engines' *isolated* ratchet lands +1 on hard data (Zanol 1262 not 1261, + Zhu 625 not 624) → the ratchet alone is insufficient; the recipe's other phases + close the last step (a #40 input: ratchet is necessary-not-sufficient). +- **Unit caveat:** TS `total_tbr_moves` (applied moves, ~220) ≠ TNT "rearrangements + examined" (~6 M) — non-commensurable, so the examined-candidate efficiency was + not measured (`RatchetResult` lacks an examined counter); score+wall are the + valid metrics (advisor: order-of-magnitude probe). Score parity is the direct + answer to "fewer cycles?". + +**#39 CLOSED:** sectorial (probe-closed, merged) + ratchet (cycle-parity, +~2× at-limit throughput, no ratchet-specific lever). + +## Implication for the program + +- **Quality: CLOSED and BANKED** — TS ≥ TNT, budget-independent; on hard data TS is + the *more reliable* engine. The solid half of "parity with TNT". +- **Throughput + algorithmic candidate-efficiency: CLOSED** — ~2× throughput + at-limit; ~1.5× candidate-efficiency near-parity (#37 + phase0). The wall gap is + **not** algorithmic. +- **Composition #40 = the OPEN question, framed correctly:** "where can TS's default + safely search *less* without losing the reliable optimum?" Opening diagnostic = + a fresh converge-mode head-to-head (gapB=0 + current `cand_ratio` on the post-fix + build) + a per-difficulty budget-vs-reliability curve. The prize is bounded by the + Zanol-class reliability TS now owns — likely meaningful on easy/medium data, + most constrained on the hard data that matters most. +- Ratchet race (#39, job `17533022`): order-of-magnitude probe only (fixed-iter + design conflates work/iter with iters-to-converge; only a 10×+ `rearr_ratio` is a + clean signal). diff --git a/dev/profiling/kpi-2026-06-21/ratchet_race.csv b/dev/profiling/kpi-2026-06-21/ratchet_race.csv new file mode 100644 index 000000000..16db1e57e --- /dev/null +++ b/dev/profiling/kpi-2026-06-21/ratchet_race.csv @@ -0,0 +1,41 @@ +"dataset","tips","start_len","engine","seed","final_len","rearrangements","wall" +"Wortley2006",37,508,"TNT",1,481,988035,0.0865681171417236 +"Wortley2006",37,508,"TS",1,481,232,0.0694363117218018 +"Wortley2006",37,508,"TNT",2,479,916103,0.0831358432769775 +"Wortley2006",37,508,"TS",2,482,223,0.067197322845459 +"Wortley2006",37,508,"TNT",3,479,998610,0.0887320041656494 +"Wortley2006",37,508,"TS",3,479,227,0.0710844993591309 +"Wortley2006",37,508,"TNT",4,479,927594,0.0838785171508789 +"Wortley2006",37,508,"TS",4,480,222,0.0678009986877441 +"Wortley2006",37,508,"TNT",5,479,933643,0.0837128162384033 +"Wortley2006",37,508,"TS",5,482,216,0.0641212463378906 +"Giles2015",78,703,"TNT",1,670,5995165,0.148613452911377 +"Giles2015",78,703,"TS",1,670,214,0.284638166427612 +"Giles2015",78,703,"TNT",2,670,6034658,0.147968769073486 +"Giles2015",78,703,"TS",2,670,216,0.324378728866577 +"Giles2015",78,703,"TNT",3,672,5295048,0.138198375701904 +"Giles2015",78,703,"TS",3,670,204,0.322666168212891 +"Giles2015",78,703,"TNT",4,670,5905102,0.148568630218506 +"Giles2015",78,703,"TS",4,670,213,0.320085048675537 +"Giles2015",78,703,"TNT",5,670,6459100,0.152313232421875 +"Giles2015",78,703,"TS",5,670,225,0.300060033798218 +"Zhu2013",75,658,"TNT",1,626,6429376,0.160148143768311 +"Zhu2013",75,658,"TS",1,625,228,0.299991369247437 +"Zhu2013",75,658,"TNT",2,625,6221718,0.159862995147705 +"Zhu2013",75,658,"TS",2,626,220,0.276246070861816 +"Zhu2013",75,658,"TNT",3,625,6589881,0.175956964492798 +"Zhu2013",75,658,"TS",3,626,238,0.296953678131104 +"Zhu2013",75,658,"TNT",4,625,6483675,0.170215606689453 +"Zhu2013",75,658,"TS",4,625,224,0.326989889144897 +"Zhu2013",75,658,"TNT",5,625,6715850,0.169048309326172 +"Zhu2013",75,658,"TS",5,625,237,0.325735807418823 +"Zanol2014",74,1310,"TNT",1,1263,5933029,0.163295269012451 +"Zanol2014",74,1310,"TS",1,1261,243,0.42898154258728 +"Zanol2014",74,1310,"TNT",2,1262,5167632,0.158793210983276 +"Zanol2014",74,1310,"TS",2,1263,232,0.437347412109375 +"Zanol2014",74,1310,"TNT",3,1262,6508444,0.169779539108276 +"Zanol2014",74,1310,"TS",3,1262,248,0.41259765625 +"Zanol2014",74,1310,"TNT",4,1261,6557907,0.172445058822632 +"Zanol2014",74,1310,"TS",4,1263,234,0.418765068054199 +"Zanol2014",74,1310,"TNT",5,1263,5534326,0.157648324966431 +"Zanol2014",74,1310,"TS",5,1262,243,0.429876804351807 diff --git a/dev/profiling/kpi-2026-06-21/timing_Giles2015.csv b/dev/profiling/kpi-2026-06-21/timing_Giles2015.csv new file mode 100644 index 000000000..c5450b6d8 --- /dev/null +++ b/dev/profiling/kpi-2026-06-21/timing_Giles2015.csv @@ -0,0 +1,16 @@ +"dataset","target","engine","config","seed","score","over","wall_s" +"Giles2015",670,"TreeSearch","default",1,670,0,5.9 +"Giles2015",670,"TreeSearch","default",2,670,0,6.3 +"Giles2015",670,"TreeSearch","default",3,670,0,9.8 +"Giles2015",670,"TreeSearch","thorough",1,670,0,9.3 +"Giles2015",670,"TreeSearch","thorough",2,670,0,12.2 +"Giles2015",670,"TreeSearch","thorough",3,670,0,13.5 +"Giles2015",670,"TNT","mult-basic",1,670,0,0.4 +"Giles2015",670,"TNT","mult-basic",2,670,0,0.4 +"Giles2015",670,"TNT","mult-basic",3,670,0,0.3 +"Giles2015",670,"TNT","xmult-default",1,670,0,0.2 +"Giles2015",670,"TNT","xmult-default",2,670,0,0.2 +"Giles2015",670,"TNT","xmult-default",3,670,0,0.2 +"Giles2015",670,"TNT","xmult-level10",1,670,0,2.8 +"Giles2015",670,"TNT","xmult-level10",2,670,0,2.7 +"Giles2015",670,"TNT","xmult-level10",3,670,0,2.7 diff --git a/dev/profiling/kpi-2026-06-21/timing_Wortley2006.csv b/dev/profiling/kpi-2026-06-21/timing_Wortley2006.csv new file mode 100644 index 000000000..77db482ee --- /dev/null +++ b/dev/profiling/kpi-2026-06-21/timing_Wortley2006.csv @@ -0,0 +1,16 @@ +"dataset","target","engine","config","seed","score","over","wall_s" +"Wortley2006",480,"TreeSearch","default",1,479,-1,1.8 +"Wortley2006",480,"TreeSearch","default",2,479,-1,1.9 +"Wortley2006",480,"TreeSearch","default",3,479,-1,1.5 +"Wortley2006",480,"TreeSearch","thorough",1,479,-1,2.9 +"Wortley2006",480,"TreeSearch","thorough",2,479,-1,2.7 +"Wortley2006",480,"TreeSearch","thorough",3,479,-1,2.7 +"Wortley2006",480,"TNT","mult-basic",1,479,-1,0.3 +"Wortley2006",480,"TNT","mult-basic",2,479,-1,0.2 +"Wortley2006",480,"TNT","mult-basic",3,479,-1,0.2 +"Wortley2006",480,"TNT","xmult-default",1,482,2,0.1 +"Wortley2006",480,"TNT","xmult-default",2,481,1,0.1 +"Wortley2006",480,"TNT","xmult-default",3,480,0,0.1 +"Wortley2006",480,"TNT","xmult-level10",1,479,-1,0.9 +"Wortley2006",480,"TNT","xmult-level10",2,479,-1,0.9 +"Wortley2006",480,"TNT","xmult-level10",3,479,-1,0.9 diff --git a/dev/profiling/kpi-2026-06-21/timing_Zanol2014.csv b/dev/profiling/kpi-2026-06-21/timing_Zanol2014.csv new file mode 100644 index 000000000..aa400fb08 --- /dev/null +++ b/dev/profiling/kpi-2026-06-21/timing_Zanol2014.csv @@ -0,0 +1,16 @@ +"dataset","target","engine","config","seed","score","over","wall_s" +"Zanol2014",1261,"TreeSearch","default",1,1261,0,38.8 +"Zanol2014",1261,"TreeSearch","default",2,1261,0,39.1 +"Zanol2014",1261,"TreeSearch","default",3,1261,0,38.2 +"Zanol2014",1261,"TreeSearch","thorough",1,1261,0,81.6 +"Zanol2014",1261,"TreeSearch","thorough",2,1261,0,85.6 +"Zanol2014",1261,"TreeSearch","thorough",3,1261,0,44 +"Zanol2014",1261,"TNT","mult-basic",1,1262,1,0.4 +"Zanol2014",1261,"TNT","mult-basic",2,1262,1,0.4 +"Zanol2014",1261,"TNT","mult-basic",3,1262,1,0.4 +"Zanol2014",1261,"TNT","xmult-default",1,1261,0,0.2 +"Zanol2014",1261,"TNT","xmult-default",2,1262,1,0.2 +"Zanol2014",1261,"TNT","xmult-default",3,1262,1,0.2 +"Zanol2014",1261,"TNT","xmult-level10",1,NA,NA,1.7 +"Zanol2014",1261,"TNT","xmult-level10",2,1261,0,3.2 +"Zanol2014",1261,"TNT","xmult-level10",3,1261,0,3.2 diff --git a/dev/profiling/kpi-2026-06-21/timing_Zhu2013.csv b/dev/profiling/kpi-2026-06-21/timing_Zhu2013.csv new file mode 100644 index 000000000..5ecb42786 --- /dev/null +++ b/dev/profiling/kpi-2026-06-21/timing_Zhu2013.csv @@ -0,0 +1,16 @@ +"dataset","target","engine","config","seed","score","over","wall_s" +"Zhu2013",624,"TreeSearch","default",1,624,0,22.8 +"Zhu2013",624,"TreeSearch","default",2,624,0,22.2 +"Zhu2013",624,"TreeSearch","default",3,624,0,23.3 +"Zhu2013",624,"TreeSearch","thorough",1,624,0,50.9 +"Zhu2013",624,"TreeSearch","thorough",2,624,0,44.6 +"Zhu2013",624,"TreeSearch","thorough",3,624,0,46.1 +"Zhu2013",624,"TNT","mult-basic",1,625,1,0.4 +"Zhu2013",624,"TNT","mult-basic",2,626,2,0.4 +"Zhu2013",624,"TNT","mult-basic",3,624,0,0.4 +"Zhu2013",624,"TNT","xmult-default",1,624,0,0.2 +"Zhu2013",624,"TNT","xmult-default",2,624,0,0.2 +"Zhu2013",624,"TNT","xmult-default",3,624,0,0.2 +"Zhu2013",624,"TNT","xmult-level10",1,624,0,2.8 +"Zhu2013",624,"TNT","xmult-level10",2,624,0,2.7 +"Zhu2013",624,"TNT","xmult-level10",3,624,0,2.8 diff --git a/dev/profiling/log.md b/dev/profiling/log.md new file mode 100644 index 000000000..18d43e066 --- /dev/null +++ b/dev/profiling/log.md @@ -0,0 +1,158 @@ +# Profiling rounds — log + +One entry per `/profile` invocation. Most recent at the top. + +Append a new round when you finish step 6 of the round. Update `last_focus:` +at the bottom of the file before saving. + +## Round template + +``` +### Round N — YYYY-MM-DD — area #K () +- Driver: dev/profiling/drivers/.R (bare wall: X.X s) +- Build: .vtune-lib mtime YYYY-MM-DD HH:MM (vs src/ HH:MM) +- profvis: <2 % R overhead | top R line / [Port] finding> +- VTune top 3: , , (module=TreeSearch.dll) +- Finding: [Port|Optimise|AT-LIMIT] short — verified Δ via micro-bench +- Filed: T-NNN row(s) in findings.md +- Cleanup: result__ removed; .vtune-lib +- Next reviewer: +``` + +--- + +### Round 0 — 2026-05-18 — scaffold +- Scaffolded `dev/profiling/` per `/profile init`. +- Built focus-areas.md from the phase distribution in + `.positai/expertise/profiling.md` (Zhu2013 thorough, 2026-03-27) and the + active profiling tasks in `to-do.md` (T-274, T-298, T-300, S-PROF round 7). +- Three live targets ranked first: NNI-perturb (#1), Ratchet (#2), + RSS/sector (#3). +- AT-LIMIT recorded for Wagner (#10), per-candidate indirect scoring (#11), + R-loop search (#12) — rotation will skip these. +- No profiling run on this turn (per skill: do not profile on same turn as + scaffold; user should review the ranking first). + +--- + +### Round 1 — 2026-05-18 — area #2 (Ratchet inner loop) +- Driver: dev/profiling/drivers/ratchet.R (bare wall: 2.80 s median; Zhu2013 thorough ×1 rep) +- Build: .vtune-lib rebuilt 2026-05-18 from TreeSearch_2.0.0.tar.gz (CXXFLAGS=-O2 -g -fno-omit-frame-pointer via dev/profiling/Makevars.vtune; R CMD INSTALL into .vtune-lib) +- profvis: ~3 % R overhead (`MaximizeParsimony` wrapper); `ts_driven_search` dominates → no [Port] finding +- VTune top 3: NOT COLLECTED — VTune not installed on this machine; WPR (`wpr -start CPU`) requires admin. Phase-level data from verbosity=2 used instead: Ratchet 62 % of inner-loop search time (802 ms / 1 301 ms); XSS 3 %, RSS 11 %, CSS 9 %, Wagner+TBR 15 % +- Finding: [Profiled — unverified] Ratchet is a TBR wrapper. Perturbation save/restore overhead is O(n_chars) ≪ TBR time. T-300 (`full_rescore` after every accepted TBR move, `ts_tbr.cpp:1136–1137`) is the actionable target; implementation plan in `.AGENTS/memory/t300-lazy-tbr-rescore.md`. Cannot verify Δ without per-function hotspot data. +- Filed: No new row in findings.md (unverified). T-300 already tracked. Note: also fixed `flat_blocks`/`all_weight_one` missing from `build_reduced_dataset` in ts_sector.cpp (S-RED area-5 finding, done inline). +- Cleanup: No VTune result dirs; tarball removed (TreeSearch_2.0.0.tar.gz); .vtune-lib kept (needed for next round) +- Next reviewer: Install VTune (or run gprof build with `-pg`) to measure `full_rescore` share inside `tbr_search` → then implement T-300 → re-profile to verify speedup. Baseline: 2.80 s/rep (see baselines.md). + +--- + +### Round 2 — 2026-05-19 — area #4 (TBR full-rescore at acceptance) +- Driver: dev/profiling/drivers/tbr-rescore.R (bare wall: 3.9 s; 12 ratchet reps × nCycles=12; Zhu2013 75t nThreads=1) +- Build: dev/profiling/.vtune-lib-20260519061049 (built 2026-05-19 06:10:49 from HEAD c504ea87, src/ clean; CXXFLAGS=-O2 -g -fno-omit-frame-pointer; debug symbols confirmed via objdump .debug_info/.debug_line) +- profvis: <2 % R overhead (258/287 samples in ts_ratchet_search; remaining = loadNamespace startup, not hot path) +- VTune top 5 (TreeSearch.dll, 3.211 s total DLL CPU): + 1. ts::fitch_na_score 0.585 s 18.2 % (full-tree Fitch pass — full_rescore path confirmed via callstack) + 2. ts::simd::any_hit_reduce_avx2 0.309 s 9.6 % (SIMD candidate hit reduction, inner evaluation) + 3. ts::tbr_search (residual) 0.297 s 9.3 % (control-flow overhead outside child callees) + 4. ts::fitch_na_pass3_score 0.281 s 8.8 % (incremental uppass, candidate evaluation) + 5. ts::fitch_na_incremental_uppass 0.110 s 3.4 % +- full_rescore attribution: + - ts::fitch_na_score (self) + load_tip_states = 0.617 s = **19.2 % of DLL time (self-time lower bound)** + - Attribution method: callstack report confirms fitch_na_score → fitch_score_ew → full_rescore → tbr_search → ratchet_search + - ⚠ Caveat: 19.2 % is self-time only. fitch_na_score has SIMD callees (any_hit_reduce_avx2 0.309s, 9.6 %) that are shared with the incremental evaluation path — unknown fraction comes from full_rescore vs incremental. [Unknown source file] 2.076 s (39 %) includes inlined code from both paths. Full_rescore **inclusive time** is plausibly 22–30 %. The prior S-PROF round 7 estimate of 28 % was likely inclusive time and is not contradicted by the 19.2 % self-time measurement — they measure different things. + - full_rescore at line 1138 (acceptance) >> line 563 (entry): ratchet-driven TBR accepts ~100–200 moves per call from perturbed trees vs 1 entry call, so ~99% of full_rescore time is the acceptance-path T-300 target + - Source-line attribution for lines 1138/1283 not available via software sampling (inlined into [Unknown]). +- Finding: [Optimise] T-300 is confirmed: full_rescore after accepted move ≥ 19.2 % of DLL time (inclusive estimate 22–30 %). Incremental path (fitch_na_pass3_score + incr_uppass + incr_downpass = 12.2 % self) already costs less per call. T-300 (in-flight by parallel agent) is justified — predicted gain 15–30 % of DLL time. +- Filed: T-300 row in findings.md (unverified — micro-bench pending T-300 implementation) +- Cleanup: result_tbr-rescore_20260519/ removed; .vtune-lib-20260519061049 deleted +- Next reviewer: After T-300 lands — re-run this driver to verify fitch_na_score drops from 18.2 % toward the incremental path baseline. Also look at ts::simd::any_hit_reduce_avx2 (9.6 %) as next T-300-independent target. + +--- + +### Round 3 — 2026-06-16 — area #13 (standard-Fitch TNT-parity path) — NEW AREA +First profile of the **standard-Fitch** path (inapplicable `-`→`?`, so +`has_na=FALSE`, flat/x4 kernels). Rounds 1-2 profiled the NA three-pass path +on raw `inapplicable.phyData`; that path is ~20× slower per replicate with an +entirely different hotspot mix (`fitch_na_*` dominate). Standard Fitch is the +path the TNT benchmark actually compares against. + +- Driver: dev/profiling/drivers/fitch-tnt.R (bare: 5.57 s / 8 reps = 0.56 s/rep; Zhu2013 75t, auto→thorough, nThreads=1, score 627 vs TNT 624) +- Build: dev/profiling/.vtune-lib-20260616052323 (HEAD 841eead3, -O2 -g) + ⚠ GOTCHA: default Windows R build STRIPS the DLL (`DLLFLAGS=-s` in Makeconf) + → VTune shows `func@0x…`/`[Unknown]`. Override `MAKEFLAGS="DLLFLAGS=-static-libgcc"` + to drop `-s`; verify `objdump -h DLL | grep debug_info` + `nm DLL` (23089 syms). + ⚠ GOTCHA2: even symboled, VTune's CSV reporter emits `func@0x…` (MinGW DWARF + unparsed). Resolve via `nm -C DLL` — image base 0x2cc1a0000 is stable across + builds, so VTune addresses map 1:1 to nm addresses. +- profvis/Rprof: 99.5 % self-time in `ts_driven_search` (single .Call); R <0.5 %; no [Port]. +- Phase dist (attr "timings", 3 reps): ratchet 63.0 %, rss 9.2 %, xss 9.2 %, + css 6.8 %, wagner 5.5 %, tbr 4.0 %, final_tbr 2.3 %, drift/nni/anneal 0 %. +- VTune top fns (TreeSearch.dll self, total 2.70 s; names via nm): + 1. ts::tbr_search (orchestration, 2 ranges) 25.1 % — candidate-loop control + collapsed/sector vector bit-tests + inlined scoring + 2. ts::simd::any_hit_reduce_avx2 14.5 % — core 2-op Fitch reduce + 3. ts::uppass_node 13.2 % — incremental uppass; SCALAR state-update loop (cf. vectorised fitch_combine) + 4. ts::simd::any_hit_reduce3_avx2 6.3 % — 3-op reduce (SPR bounded) + 5. ts::TreeState::build_postorder_prealloc 5.2 % — O(n) postorder rebuild, per clip AND per accept + 6. ts::fitch_incremental_downpass 4.1 % + 7. ts::fitch_indirect_bounded_flat 4.0 % + 8. hash_tree / fitch_indirect_length_cached (scalar) / validate_topology ~2.9 % each + (Scoring SIMD+wrappers ≈ 50 %; per-clip bookkeeping ≈ 18 %; orchestration 25 %.) +- Findings: + [AT-LIMIT] SIMD `any_hit_reduce` (21 %): disasm of `hor_or256` shows GCC + already elides the store-reload (vextracti128/vpsrldq/vpor/vmovq, register-only) + → compiler-optimal. No win. + [AT-LIMIT] `uppass_node` vectorisation (13 %): micro-bench + dev/profiling/microbench/bench_uppass_combine.cpp — AVX2 update loop + bit-identical (value+changed flag) but only **1.22×** at n_states=4, and the + 4-wide path does NOT trigger for 2-state (binary) morph chars → ~1 % wall, + not worth the incremental-uppass correctness risk. + [Optimise, modest] Per-clip/accept allocation churn (~3-4 %): compute_from_above, + collect_main_edges/collect_subtree_edges, validate_topology heap-alloc + std::vector scratch per call (_M_realloc_append 1.1 %). Extend existing + prealloc pattern (work_stack/saved_postorder/clip_actives_buf). Low risk. + [Strategic] Standard-Fitch is **bookkeeping- + strategy-bound**, not + scoring-bound. Per-candidate scoring is at the AVX2/compiler limit; remaining + levers are (a) reduce per-clip O(n) bookkeeping (postorder rebuild + + incremental passes ≈ 18 %), (b) ratchet evaluation economy (63 %). Both align + with the TNT-outperformance analysis (strategy > code). Score near parity. +- Filed: findings.md row T-S3a (allocation churn) + AT-LIMIT rows. +- Cleanup: result_fitch_tnt_* + result_fitch_sym_* removed; stripped lib + .vtune-lib-20260616051420 removed; symboled lib kept pending follow-up. microbench kept. +- Next reviewer: code lever for parity is per-clip bookkeeping (incremental + postorder across clip/unclip), NOT the scoring kernel. Strategy lever: ratchet + eval economy (time-adjusted expected-best). + +--- + +### Round 4 — 2026-06-17 — area #13 (standard-Fitch) — StateSnapshot re-profile + build-protocol hardening +- Trigger: re-confirm the stale "StateSnapshot ~23%" before the deferred + selective save/restore surgery (task #10), on a FRESH symboled build. +- Driver: dev/profiling/drivers/fitch-tnt.R (Zhu2013, 12 reps; bare ~3.5 s) +- Build: **build-symboled-lib.ps1 (NEW, in /profile skill dir)** — isolated + tarball (src/ untouched, concurrent-safe) + PKG_CXXFLAGS `-g -fno-omit-frame-pointer` + + DLLFLAGS=-static-libgcc; HARD-FAILS if no .debug_info. 23,221 syms. + ⚠ GOTCHA caught: the prior `Makevars.vtune` set `CXXFLAGS`, which R SILENTLY + BYPASSES for C++17 (uses CXX17FLAGS) → only ~214 KB .debug_info (partial; -g + on cache-hit TUs only, -fno-omit-frame-pointer absent). PKG_CXXFLAGS fixes it + → 19 MB .debug_info (all TUs). resolve_syms.R maps VTune `func@0x` via `nm`. +- VTune top (resolved, % of total CPU): ts::tbr_search 12.8 %, simd any_hit_reduce + ×2 = 14.2 % (AT-LIMIT), uppass_node 7.7 % (AT-LIMIT), memcpy 5.0 %, malloc+free + 6.4 %, fitch_indirect* ~17 %, build_postorder 3.1 %, save_node_state 2.5 %, + hash_tree 2.3 %. +- Finding: **[AT-LIMIT] selective StateSnapshot (task #10) — NOT worth it.** + save/restore (save_node_state 2.5 % + its memcpy share) ≈ 3-5 % ceiling; + selective restore reclaims ~half → ~2 % for risky surgery on the most + correctness-critical code. The cited "23 %" was stale NA-path (pre-T-261/T-300). + Per-candidate cost is at floor; ~19 % is alloc/copy churn. The 2× gap is + candidates-per-improvement (SEARCH STRATEGY) → see TNT sectorial reverse- + engineering: [[tnt-sectorial-recipe]] memory + dev/benchmarks/tnt_sector_defaults.csv. +- Cleanup: result_statesnap_* removed; old partial-symbol libs removed; kept + .vtune-lib-20260617081344 (validated symboled). build-symboled-lib.ps1 + + resolve_syms.R retained. +- Next reviewer: profile the NEW multi-start sectorial once built (regenerate + the symboled lib with build-symboled-lib.ps1 first — never reuse a stale one). + +--- + +last_focus: 13 diff --git a/dev/profiling/microbench/bench_getenv.cpp b/dev/profiling/microbench/bench_getenv.cpp new file mode 100644 index 000000000..3e169f298 --- /dev/null +++ b/dev/profiling/microbench/bench_getenv.cpp @@ -0,0 +1,21 @@ +// Measure std::getenv cost on this machine's environment (ucrt linear scan). +#include +#include +#include +int main() { + const long N = 5'000'000; + volatile int sink = 0; + // warm + for (long i = 0; i < 100000; ++i) sink += (std::getenv("TS_FREE_HTU_PROBE") != nullptr); + double best = 1e9; + for (int rep = 0; rep < 7; ++rep) { + auto t0 = std::chrono::steady_clock::now(); + for (long i = 0; i < N; ++i) sink += (std::getenv("TS_FREE_HTU_PROBE") != nullptr); + auto t1 = std::chrono::steady_clock::now(); + double ns = std::chrono::duration(t1 - t0).count() / N; + if (ns < best) best = ns; + } + printf("std::getenv: %.1f ns/call (best of 7, N=%ld); sink=%d\n", best, N, sink); + printf("3 getenv/pick x 3840 picks = %.4f s\n", 3.0 * 3840 * best / 1e9); + return 0; +} diff --git a/dev/profiling/microbench/bench_uppass_combine.cpp b/dev/profiling/microbench/bench_uppass_combine.cpp new file mode 100644 index 000000000..2d5c6ef1c --- /dev/null +++ b/dev/profiling/microbench/bench_uppass_combine.cpp @@ -0,0 +1,146 @@ +// Micro-benchmark: scalar vs AVX2 uppass state-update loop. +// +// Isolates the hot loop in ts::uppass_node (ts_fitch.cpp:54-61), which VTune +// shows at 13.2% of DLL self-CPU on the standard-Fitch (Zhu2013, "-"->"?") +// workload. The any_intersect reduce (line 47) is ALREADY vectorised +// (any_hit_reduce_avx2); the *state update* that follows is scalar, unlike +// the analogous fitch_downpass which uses simd::fitch_combine (line 102). +// +// uppass new_val[s] = (anc_final[s] & node_prelim[s] & has_isect) +// | (node_prelim[s] & no_isect) +// plus a "changed" flag (new_val != old final_) that drives the incremental +// dirty-propagation. This bench reproduces BOTH the value and the changed +// flag, asserts the AVX2 path is bit-identical to scalar, then times them. +// +// Build (matches R's flags + AVX2): +// g++ -O2 -mavx2 -std=c++17 -o bench_uppass.exe bench_uppass_combine.cpp +// Run: ./bench_uppass.exe + +#include +#include +#include +#include +#include +#include +#include + +static inline uint64_t popcount64(uint64_t x){ return __builtin_popcountll(x); } + +// ---- horizontal OR of a 256-bit reg (matches ts_simd.h hor_or256) ---- +static inline uint64_t hor_or256(__m256i v){ + __m128i lo = _mm256_castsi256_si128(v); + __m128i hi = _mm256_extracti128_si256(v, 1); + __m128i c = _mm_or_si128(lo, hi); + __m128i s = _mm_srli_si128(c, 8); + c = _mm_or_si128(c, s); + return (uint64_t)_mm_cvtsi128_si64(c); +} +static inline uint64_t any_hit_reduce4(const uint64_t* a, const uint64_t* b){ + __m256i va = _mm256_loadu_si256((const __m256i*)a); + __m256i vb = _mm256_loadu_si256((const __m256i*)b); + return hor_or256(_mm256_and_si256(va, vb)); +} + +// ---- BASELINE: scalar update loop (verbatim from uppass_node) ---- +static inline bool update_scalar(const uint64_t* anc_final, + const uint64_t* node_prelim, + uint64_t* node_final, int n_states, + uint64_t has_isect, uint64_t no_isect){ + bool changed = false; + for (int s = 0; s < n_states; ++s){ + uint64_t isect = anc_final[s] & node_prelim[s]; + uint64_t new_val = (isect & has_isect) | (node_prelim[s] & no_isect); + if (new_val != node_final[s]) changed = true; + node_final[s] = new_val; + } + return changed; +} + +// ---- CANDIDATE: AVX2 update for n_states==4 (+ scalar tail) ---- +static inline bool update_avx2(const uint64_t* anc_final, + const uint64_t* node_prelim, + uint64_t* node_final, int n_states, + uint64_t has_isect, uint64_t no_isect){ + int s = 0; + uint64_t diff_acc = 0; + if (n_states >= 4){ + __m256i H = _mm256_set1_epi64x((long long)has_isect); + __m256i N = _mm256_set1_epi64x((long long)no_isect); + __m256i diff = _mm256_setzero_si256(); + for (; s + 4 <= n_states; s += 4){ + __m256i a = _mm256_loadu_si256((const __m256i*)(anc_final + s)); + __m256i p = _mm256_loadu_si256((const __m256i*)(node_prelim + s)); + __m256i nv = _mm256_or_si256( + _mm256_and_si256(_mm256_and_si256(a, p), H), + _mm256_and_si256(p, N)); + __m256i old = _mm256_loadu_si256((const __m256i*)(node_final + s)); + diff = _mm256_or_si256(diff, _mm256_xor_si256(nv, old)); + _mm256_storeu_si256((__m256i*)(node_final + s), nv); + } + diff_acc |= hor_or256(diff); + } + for (; s < n_states; ++s){ + uint64_t isect = anc_final[s] & node_prelim[s]; + uint64_t new_val = (isect & has_isect) | (node_prelim[s] & no_isect); + diff_acc |= (new_val ^ node_final[s]); + node_final[s] = new_val; + } + return diff_acc != 0; +} + +int main(){ + const int N = 200000; // node-blocks + const int NS = 4; // states (DNA / common morph) + const int REPS = 60; + std::mt19937_64 rng(5813); + // 4-state data: each word has ~half its bits set among 64 chars. + std::vector anc(N*NS), prelim(N*NS), final_a(N*NS), final_b(N*NS); + std::vector hasv(N), nov(N); + for (int i=0;idouble{ + std::vector ms; + for (int r=0;r(t1-t0).count()); + } + std::sort(ms.begin(), ms.end()); + return ms[ms.size()/2]; + }; + + double s_med = run(0); + double a_med = run(1); + printf("scalar : %.3f ms/pass (%.2f ns/node-block)\n", s_med, s_med*1e6/N); + printf("avx2 : %.3f ms/pass (%.2f ns/node-block)\n", a_med, a_med*1e6/N); + printf("speedup: %.2fx\n", s_med/a_med); + return 0; +} diff --git a/dev/profiling/resolve_syms.R b/dev/profiling/resolve_syms.R new file mode 100644 index 000000000..44dfc4a3c --- /dev/null +++ b/dev/profiling/resolve_syms.R @@ -0,0 +1,56 @@ +#!/usr/bin/env Rscript +# Resolve VTune MinGW-DWARF `func@0xADDR` hotspots to demangled C++ names. +# +# VTune 2026's CSV reporter leaves MinGW DWARF symbols unparsed (`func@0xADDR`) +# even when the DLL carries `.debug_info`. This joins the hotspot addresses to +# `nm -C` output by address (nearest function start <= addr) so the flat +# self-time profile is readable. Recurs every profiling round on this toolchain. +# +# Inputs (dump these first): +# vtune -report hotspots -r -group-by function -format=csv \ +# -csv-delimiter="`t" > hs.tsv # TAB delimiter (names have commas) +# nm -C --defined-only > nm.txt +# Usage: Rscript resolve_syms.R [topN] + +args <- commandArgs(trailingOnly = TRUE) +hs_path <- args[1]; nm_path <- args[2] +topN <- if (length(args) >= 3) as.integer(args[3]) else 30L + +hs <- read.delim(hs_path, check.names = FALSE, stringsAsFactors = FALSE) +names(hs)[names(hs) == "CPU Time"] <- "cpu" +hs$addr <- suppressWarnings(as.numeric(hs[["Start Address"]])) + +nm <- readLines(nm_path, warn = FALSE) +mm <- regmatches(nm, regexec("^([0-9a-fA-F]{8,})[ \t]+(\\S)[ \t]+(.*)$", nm)) +ok <- lengths(mm) == 4 +syma <- as.numeric(paste0("0x", vapply(mm[ok], `[`, "", 2))) +symn <- vapply(mm[ok], `[`, "", 4) +o <- order(syma); syma <- syma[o]; symn <- symn[o] + +resolve <- function(a) { + if (is.na(a)) return(NA_character_) + i <- findInterval(a, syma) + if (i < 1) return(NA_character_) + # Return: name + symn[i] +} +hs$resolved <- vapply(hs$addr, resolve, "") +isTS <- hs$Module == "TreeSearch.dll" +hs$name <- ifelse(isTS & !is.na(hs$resolved), hs$resolved, + ifelse(grepl("^func@", hs$Function), + paste0(hs$Module, "!", hs$Function), hs$Function)) + +total <- sum(hs$cpu, na.rm = TRUE) +ts_cpu <- sum(hs$cpu[isTS], na.rm = TRUE) +agg <- aggregate(cpu ~ name + Module, hs, sum) +agg$pct <- 100 * agg$cpu / total +agg <- agg[order(-agg$cpu), ] + +cat(sprintf("Total CPU time (all modules): %.3f s\n", total)) +cat(sprintf("TreeSearch.dll self CPU: %.3f s (%.1f%% of total)\n\n", + ts_cpu, 100 * ts_cpu / total)) +cat(sprintf("%7s %7s %s\n", "self_s", "pct", "function [module]")) +for (i in seq_len(min(topN, nrow(agg)))) { + cat(sprintf("%7.3f %6.1f%% %s [%s]\n", + agg$cpu[i], agg$pct[i], agg$name[i], agg$Module[i])) +} diff --git a/dev/profiling/run_sector_tests.R b/dev/profiling/run_sector_tests.R new file mode 100644 index 000000000..00458b024 --- /dev/null +++ b/dev/profiling/run_sector_tests.R @@ -0,0 +1,24 @@ +# Correctness gate for the sectorial micro-levers + TBR getenv hoists. +# Run against the hot-swapped .agent-sect lib (absolute lib.loc per testthat CWD gotcha). +lib <- normalizePath(".agent-sect") +suppressMessages(library(TreeSearch, lib.loc = lib)) +suppressMessages(library(TreeTools)) +suppressMessages(library(testthat)) +Sys.setenv(NOT_CRAN = "true") + +# testthat auto-sources helper-*.R only via test_dir; source them manually. +helpers <- list.files("tests/testthat", pattern = "^helper.*[.]R$", full.names = TRUE) +for (h in helpers) sys.source(h, envir = globalenv()) + +files <- c("test-ts-sector.R", "test-ts-sector-resolve.R", "test-ts-conflict-sector.R", + "test-ts-tbr-search.R", "test-ts-tbr-dirty-rescore.R", "test-ts-tbr-symmetry.R", + "test-ts-ratchet-search.R", "test-ts-drift-search.R") +for (f in files) { + p <- file.path("tests/testthat", f) + if (!file.exists(p)) { cat("(skip missing", f, ")\n"); next } + cat("\n===========", f, "===========\n") + tryCatch( + test_file(p, reporter = "summary"), + error = function(e) cat("FILE ERROR:", conditionMessage(e), "\n") + ) +} diff --git a/dev/profiling/sector-levers.patch b/dev/profiling/sector-levers.patch new file mode 100644 index 000000000..4e1cffe89 --- /dev/null +++ b/dev/profiling/sector-levers.patch @@ -0,0 +1,134 @@ +--- dev/profiling/_pristine_sector.cpp 2026-06-20 12:46:30.364230000 +0100 ++++ dev/profiling/_levers_sector.cpp 2026-06-20 12:48:39.793265900 +0100 +@@ -55,7 +55,13 @@ + } + } + +- // 3. Walk down the path, computing from_above at each child step ++ // 3. Walk down the path, computing from_above at each child step. ++ // `new_from_above` is allocated ONCE and swapped each step (O(1)) instead of ++ // heap-allocated per step. Byte-identical: the inner loop overwrites every ++ // state word each step, and any padding words (total_words > sum n_states) ++ // start at 0 in both buffers and are never written, so they stay 0 — same as ++ // the original fresh-zeroed allocation. (Sectorial micro-bank, T-S6c.) ++ std::vector new_from_above(tw); + for (size_t i = 0; i + 1 < path.size(); ++i) { + int node = path[i]; + int next = path[i + 1]; // child on the path +@@ -69,7 +75,6 @@ + const uint64_t* sib_prelim = + &tree.prelim[static_cast(sib) * tw]; + +- std::vector new_from_above(tw); + for (int b = 0; b < ds.n_blocks; ++b) { + int off = ds.block_word_offset[b]; + int ns = ds.blocks[b].n_states; +@@ -84,7 +89,7 @@ + new_from_above[off + s] = (isect & any_isect) | (uni & no_isect); + } + } +- from_above_cur = std::move(new_from_above); ++ std::swap(from_above_cur, new_from_above); + } + + std::memcpy(from_above_out.data(), from_above_cur.data(), +@@ -797,10 +802,14 @@ + bool have_best = false; + std::vector best_left, best_right, best_parent; + ++ // search_sector runs once per sector pick (1000s of times/search). std::getenv ++ // is µs-scale on Windows/ucrt (linear env scan), so the per-pick D1-probe gate ++ // is read ONCE into a static, not per pick/per start (T-S6c micro-bank). ++ static const bool _free_htu_probe = std::getenv("TS_FREE_HTU_PROBE") != nullptr; ++ + // D1 confirm (env TS_FREE_HTU_PROBE): T0 sector's reduced length, baseline for + // the floating-HTU free re-solve reported after the loop. +- double probe_orig = std::getenv("TS_FREE_HTU_PROBE") +- ? score_tree(rd.subtree, rd.data) : 0.0; ++ double probe_orig = _free_htu_probe ? score_tree(rd.subtree, rd.data) : 0.0; + + for (int s = 0; s < ras_starts; ++s) { + if (s > 0) { +@@ -845,7 +854,7 @@ + // shorter FULL tree the anchored sectorial throws away. GUARD: also reports + // root_ok, so a null can be told apart from "TBR never floats the HTU" + // (false-negative). Run rasStarts=1 -> this is the warm T0 sector start. +- if (std::getenv("TS_FREE_HTU_PROBE")) { ++ if (_free_htu_probe) { + REprintf("REVERT sect=%d S=%d s=%d orig=%.0f tbr=%.0f root_ok=%d %s\n", + rd.sector_root, rd.n_real_tips, s, original_score, tr.best_score, + root_ok ? 1 : 0, +@@ -868,22 +877,35 @@ + // sector escapes onto a different equal-length arrangement (plateau walk), + // which iterated sector picks then build a strict improvement from. At the + // default ras_starts=1 there is no s>0, so this is a guaranteed no-op. +- bool take = !have_best || this_score < best_score || +- (accept_equal && s > 0 && this_score == best_score); +- if (take) { ++ if (ras_starts == 1) { ++ // Single-start fast path (the default): rd.subtree already holds the ++ // final (TBR-result or reverted) topology, so the best_* snapshot here ++ // and the post-loop restore are a provable no-op round-trip — skip both. ++ // reinsert_sector reads only left/right/parent (never postorder), so the ++ // post-loop build_postorder is also unneeded. (T-S6c micro-bank.) + best_score = this_score; +- best_left = rd.subtree.left; +- best_right = rd.subtree.right; +- best_parent = rd.subtree.parent; + have_best = true; ++ } else { ++ bool take = !have_best || this_score < best_score || ++ (accept_equal && s > 0 && this_score == best_score); ++ if (take) { ++ best_score = this_score; ++ best_left = rd.subtree.left; ++ best_right = rd.subtree.right; ++ best_parent = rd.subtree.parent; ++ have_best = true; ++ } + } + } + + // Restore the best topology found across starts, ready for reinsertion. +- rd.subtree.left = best_left; +- rd.subtree.right = best_right; +- rd.subtree.parent = best_parent; +- rd.subtree.build_postorder(); ++ // (Skipped at ras_starts==1: rd.subtree already holds it — see fast path.) ++ if (ras_starts > 1) { ++ rd.subtree.left = best_left; ++ rd.subtree.right = best_right; ++ rd.subtree.parent = best_parent; ++ rd.subtree.build_postorder(); ++ } + + // D1 SCORING-ONLY CONFIRM (env TS_FREE_HTU_PROBE), NO reinsertion: does an + // UNCONSTRAINED reduced search -- HTU = ordinary floating leaf among rd.data's +@@ -892,7 +914,7 @@ + // PROVES a shorter FULL tree the anchored sectorial cannot reach (audit D1). 20 + // free RAS+TBR restarts so medium sectors reach their true optimum (free >= orig + // on a LARGE sector may be cold-search weakness -- weigh the medium sectors). +- if (std::getenv("TS_FREE_HTU_PROBE")) { ++ if (_free_htu_probe) { + double free_min = HUGE_VAL; + for (int fs = 0; fs < 20; ++fs) { + TreeState ft; +@@ -1019,6 +1041,9 @@ + SectorResult rss_search(TreeState& tree, DataSet& ds, + const SectorParams& params, + ConstraintData* cd) { ++ // Hoist the per-accept debug-trace gate (µs-scale ucrt getenv) to a static ++ // (T-S6c micro-bank). ++ static const bool _sect_debug = std::getenv("TS_SECT_DEBUG") != nullptr; + bool constrained = cd && cd->active && cd->has_posthoc; + // Seed RNG (from R in serial mode, from thread-local in parallel mode) + std::mt19937 rng = ts::make_rng(); +@@ -1144,7 +1169,7 @@ + reinsert_sector(tree, rd); + tree.build_postorder(); + double new_score = score_tree(tree, ds); +- if (std::getenv("TS_SECT_DEBUG")) ++ if (_sect_debug) + REprintf(" sect[%2d] red_cur=%.0f red_best=%.0f full_new=%.0f full_best=%.0f %s\n", + sector_root, sector_current, sector_best, new_score, result.best_score, + new_score < result.best_score ? "STRICT" : diff --git a/dev/profiling/t300_na_bench.R b/dev/profiling/t300_na_bench.R new file mode 100644 index 000000000..87faaa993 --- /dev/null +++ b/dev/profiling/t300_na_bench.R @@ -0,0 +1,70 @@ +# T-300 NA wall-clock A/B +# Usage: Rscript dev/profiling/t300_na_bench.R