From 128db645644266f6e2acd42014a1c3e5b5ea6bf6 Mon Sep 17 00:00:00 2001
From: Eric Coissac cardcomp::tests::underflow_prone_transition_gets_finite_cost_no
where the old exp()-first code would have underflowed to 0.0, cost
asserted finite). Every pre-existing cardcomp test still passes
unchanged (numerically identical results when no underflow occurs).
--iqtree-min-freq: rare states still perturb IQ-TREE after the underflow fix (implemented 2026-08-15)After the pairwise_cost_matrix fix above, the user re-checked
+_iqtree_states.csv on the real phyloskims_sal_vac run: no more exact
+zeros, but N (all 4 bases), V/H/D (3 bases each) sit at
+8×10⁻⁵–4×10⁻⁴, and B (3 bases) just above 10⁻³ — all four
+3-simultaneous-base states plus N, exactly the states the underflow fix
+was papering over the symptom of. iqtree3 kept emitting "Numerical
+underflow for lh-derivative" on this same run — a model with several
+near-zero-frequency states is still numerically marginal for ML
+optimisation even once no cost/exchangeability entry is exactly
+infinite/zero.
Decided and implemented: extend --free-loss's existing "sampling
+failure, not true signal" treatment of non-detection to rare
+multi-base states too — biologically, a family showing 3–4 simultaneously
+observed central bases across genomes is far more likely to be
+assembly/detection noise than a genuinely ancestral, widely-preserved
+polymorphism, the same reasoning already used for -.
Mechanism (obikmer/src/cmd/phylo/iqtree.rs, write_iqtree): a
+second filtering pass, active only under --free-loss. After the first
+compact_alphabet call (post ascertainment-filtering), any state with
+freq < --iqtree-min-freq (default 0.001) has every occurrence recoded
+to - (recode_symbols_as_absent, a new small helper) — reusing the
+existing -→? machinery in compact_alphabet/write_iqtree_alignment
+rather than duplicating it, since a state recoded to - flows through
+exactly the same downstream path a genuinely-absent call already does.
+drop_ascertainment_noninformative then re-runs on the recoded alignment
+(a site can become non-informative once its only distinguishing call was
+the now-recoded rare state), and compact_alphabet is recomputed a third
+time on the result — this final alphabet is what _iqtree_states.csv/
+_iqtree.model/_iqtree.fasta all get built from, so the three files
+stay consistent with each other by construction, same as before this
+change.
Not a fixed-point loop (recoding one rare state can't, by construction, +lower another already-above-threshold state's frequency enough to also +cross the threshold, since removing calls only redistributes mass upward +among the survivors) — a single extra pass is sufficient.
+--iqtree-min-freq 0 disables the filter (no state can have frequency
+< 0), recovering the pre-this-change behavior exactly.
Covered by iqtree::tests::iqtree_min_freq_folds_rare_states_into_missing
+(20 common A/C sites + one site introducing a single, otherwise-informative
+M call at ~1/62 frequency, --iqtree-min-freq 0.05; asserts M absent
+from the written _iqtree_states.csv and A/C still present). Full
+workspace cargo test green.
The Mash mutation-rate model this discussion contrasts with: (Fan et al. 2015; Marbl Lab 2026)1 2.
diff --git a/DevDocMD/theory/evolutionary_distances.md b/DevDocMD/theory/evolutionary_distances.md index d2d227bf..d1cf4927 100644 --- a/DevDocMD/theory/evolutionary_distances.md +++ b/DevDocMD/theory/evolutionary_distances.md @@ -2132,6 +2132,56 @@ where the old `exp()`-first code would have underflowed to `0.0`, cost asserted finite). Every pre-existing `cardcomp` test still passes unchanged (numerically identical results when no underflow occurs). +## `--iqtree-min-freq`: rare states still perturb IQ-TREE after the underflow fix (implemented 2026-08-15) + +After the `pairwise_cost_matrix` fix above, the user re-checked +`_iqtree_states.csv` on the real `phyloskims_sal_vac` run: no more exact +zeros, but `N` (all 4 bases), `V`/`H`/`D` (3 bases each) sit at +`8×10⁻⁵`–`4×10⁻⁴`, and `B` (3 bases) just above `10⁻³` — all four +3-simultaneous-base states plus `N`, exactly the states the underflow fix +was papering over the symptom of. `iqtree3` kept emitting "Numerical +underflow for lh-derivative" on this same run — a model with several +near-zero-frequency states is still numerically marginal for ML +optimisation even once no cost/exchangeability entry is *exactly* +infinite/zero. + +**Decided and implemented**: extend `--free-loss`'s existing "sampling +failure, not true signal" treatment of non-detection to rare +multi-base states too — biologically, a family showing 3–4 simultaneously +observed central bases across genomes is far more likely to be +assembly/detection noise than a genuinely ancestral, widely-preserved +polymorphism, the same reasoning already used for `-`. + +**Mechanism** (`obikmer/src/cmd/phylo/iqtree.rs`, `write_iqtree`): a +*second* filtering pass, active only under `--free-loss`. After the first +`compact_alphabet` call (post ascertainment-filtering), any state with +`freq < --iqtree-min-freq` (default `0.001`) has every occurrence recoded +to `-` (`recode_symbols_as_absent`, a new small helper) — reusing the +*existing* `-`→`?` machinery in `compact_alphabet`/`write_iqtree_alignment` +rather than duplicating it, since a state recoded to `-` flows through +exactly the same downstream path a genuinely-absent call already does. +`drop_ascertainment_noninformative` then re-runs on the recoded alignment +(a site can become non-informative once its only distinguishing call was +the now-recoded rare state), and `compact_alphabet` is recomputed a third +time on the result — this final alphabet is what `_iqtree_states.csv`/ +`_iqtree.model`/`_iqtree.fasta` all get built from, so the three files +stay consistent with each other by construction, same as before this +change. + +Not a fixed-point loop (recoding one rare state can't, by construction, +lower another *already-above-threshold* state's frequency enough to also +cross the threshold, since removing calls only redistributes mass upward +among the survivors) — a single extra pass is sufficient. + +`--iqtree-min-freq 0` disables the filter (no state can have frequency +`< 0`), recovering the pre-this-change behavior exactly. + +Covered by `iqtree::tests::iqtree_min_freq_folds_rare_states_into_missing` +(20 common A/C sites + one site introducing a single, otherwise-informative +`M` call at ~1/62 frequency, `--iqtree-min-freq 0.05`; asserts `M` absent +from the written `_iqtree_states.csv` and `A`/`C` still present). Full +workspace `cargo test` green. + ## References The Mash mutation-rate model this discussion contrasts with: diff --git a/UserDocMD/usage/phylo.md b/UserDocMD/usage/phylo.md index 8d0fe5a5..7da90cc9 100644 --- a/UserDocMD/usage/phylo.md +++ b/UserDocMD/usage/phylo.md @@ -124,6 +124,7 @@ The first `phylo` run on a given index that uses `--entropy`/`--entropy-sd` pays | `--tnt` | off | Also write a TNT script (implies `--sankoff`) | | `--phyg` | off | Also write PhyG input files (implies `--sankoff`) | | `--iqtree` | off | Also write an IQ-TREE custom model and alignment (implies `--sankoff`) | +| `--iqtree-min-freq` | `0.001` | With `--iqtree --free-loss`: also treat as missing any state rarer than this in the alignment | | `--sankoff-cost-scale` | `100` | Integer scaling factor applied to costs before rounding (required by TNT/PhyG's integer-only cost commands) | ### The 16-state model @@ -232,9 +233,13 @@ Matrix layout (`_dist.csv`, `_shared.csv`, and every other "CSV matrix" below): **`_iqtree.model`** (`--iqtree`) — lower-triangular exchangeability matrix $R(a,b) = e^{-\text{cost}(a,b)}$ (one row of increasing length per state, whitespace-separated, PAML order), followed by one line of empirical state frequencies. Only states actually occurring in the alignment are kept, compactly renumbered `0..k-1`. -**`_iqtree.fasta`** (`--iqtree`) — alignment recoded to that same compact `0..k-1` alphabet (symbols `0-9A-F`). Under `--free-loss`, non-detection becomes `?` and columns left non-informative once missing calls are ignored are dropped first (required for `+ASC`). Run with: +**`_iqtree.fasta`** (`--iqtree`) — alignment recoded to that same compact `0..k-1` alphabet (symbols `0-9A-F`). Under `--free-loss`, non-detection becomes `?` and columns left non-informative once missing calls are ignored are dropped first (required for `+ASC`); with `--iqtree-min-freq` also set (the default), any state rarer than that threshold is folded into the same `?` treatment, and non-informative columns are re-checked and dropped again after that. Run with: ``` iqtree3 -s--sankoff)--iqtree-min-freq0.001--iqtree --free-loss: also treat as missing any state rarer than this in the alignment--sankoff-cost-scale100_sankoff.tcm (--phyg) — first line: the 16-symbol alphabet plus a trailing gap symbol (17 total). Each following line: one row of the integer-scaled, metric-closed cost matrix (17 values — the extra gap column/row reuses the cost to/from the empty state 0, since it's never actually triggered).
_sankoff.pg (--phyg) — script: read(prefasta:..., tcm:...) against _sankoff.fasta/_sankoff.tcm, a default 300s/4-instance search, report(...) writing <prefix>_sankoff.tre (bare numeric labels, as for --tnt). Run with phyg from the output directory (the script uses relative file names). Feed the tree plus _sankoff.fasta into name-tree for taxon names.
_iqtree.model (--iqtree) — lower-triangular exchangeability matrix \(R(a,b) = e^{-\text{cost}(a,b)}\) (one row of increasing length per state, whitespace-separated, PAML order), followed by one line of empirical state frequencies. Only states actually occurring in the alignment are kept, compactly renumbered 0..k-1.
_iqtree.fasta (--iqtree) — alignment recoded to that same compact 0..k-1 alphabet (symbols 0-9A-F). Under --free-loss, non-detection becomes ? and columns left non-informative once missing calls are ignored are dropped first (required for +ASC). Run with:
+
_iqtree.fasta (--iqtree) — alignment recoded to that same compact 0..k-1 alphabet (symbols 0-9A-F). Under --free-loss, non-detection becomes ? and columns left non-informative once missing calls are ignored are dropped first (required for +ASC); with --iqtree-min-freq also set (the default), any state rarer than that threshold is folded into the same ? treatment, and non-informative columns are re-checked and dropped again after that. Run with:
iqtree3 -s <prefix>_iqtree.fasta --seqtype MORPH -m <prefix>_iqtree.model+ASC --prefix <prefix>_iqtree -T AUTO
_iqtree_states.csv (--iqtree) — one row per state actually kept in _iqtree.model/_iqtree.fasta (header iqtree_symbol,canonical_symbol,frequency): iqtree_symbol is the compact 0-9A-F symbol as written in those two files, canonical_symbol is the matching _sankoff_matrix.csv state, frequency is that state's empirical frequency at full precision (_iqtree.model's own frequency line is rounded to 6 decimals). Under --free-loss, absent (0/?) is never a kept state, so it never appears here. Use this file to identify which real state a given row/column of _iqtree.model's matrix corresponds to — e.g. to check whether a state showing zero exchangeability with everything else is expected (a state combination that never co-occurs with anything else in this data) or worth investigating further.
_iqtree_states.csv (--iqtree) — one row per state actually kept in _iqtree.model/_iqtree.fasta (header iqtree_symbol,canonical_symbol,frequency): iqtree_symbol is the compact 0-9A-F symbol as written in those two files, canonical_symbol is the matching _sankoff_matrix.csv state, frequency is that state's empirical frequency at full precision (_iqtree.model's own frequency line is rounded to 6 decimals). Under --free-loss, absent (0/?) is never a kept state, so it never appears here — nor does any state --iqtree-min-freq folded away for being too rare. Use this file to identify which real state a given row/column of _iqtree.model's matrix corresponds to — e.g. to check whether a state showing zero exchangeability with everything else is expected (a state combination that never co-occurs with anything else in this data) or worth investigating further.
--iqtree-min-freqStates that combine 3 or 4 central bases at once (IUPAC V/H/K.../N) are inherently rare — and, on real data, rare enough that they can make iqtree3 itself numerically unstable ("Numerical underflow for lh-derivative" warnings, near-degenerate likelihood optimization). They're also more likely to be assembly/detection noise than genuine, widely-shared multi-way polymorphism, the same "sampling failure, not true signal" reasoning --free-loss already applies to non-detection. With --free-loss set, --iqtree-min-freq (default 0.001, i.e. one in a thousand) extends that same missing-data treatment to any state below this frequency, not just absence. Check _iqtree_states.csv to see exactly which states survived and at what frequency; set --iqtree-min-freq 0 to disable this and keep every state that occurs at all (the old behavior). Has no effect without --free-loss — there is no missing-data symbol to fold rare states into otherwise.