add name-tree command and fix --free-loss cost matrix
Introduce the obikmer name-tree subcommand to map numeric leaf labels in phylogenetic tree exports back to taxon names using a reference FASTA file. Correct the --free-loss flag behavior by removing cardinality transition costs from pairwise cost calculations, ensuring sibling gains and losses are priced identically to whole-family events. Update documentation, configuration parameters, and add reference phylogenetic data files.
This commit is contained in:
@@ -129,7 +129,23 @@ fn best_pairing_cost(lost: &[u8], gained: &[u8], p_comp: &[[f64; 4]; 4]) -> f64
|
||||
/// symmetrised (`(cost(A,B)+cost(B,A))/2` — the row-normalised `P` is not
|
||||
/// symmetric in general, but a Sankoff parsimony cost must be, so the
|
||||
/// score is independent of where an unrooted tree gets rooted).
|
||||
pub fn pairwise_cost_matrix(p_card: &[[f64; 5]; 5], p_comp: &[[f64; 4]; 4]) -> [[f64; 16]; 16] {
|
||||
///
|
||||
/// `free_loss`: drop the `P_cardinality(|A|→|B|)` factor entirely (never
|
||||
/// added to `log_p`) — the same low/incomplete-coverage argument that
|
||||
/// justifies recoding whole-family non-detection as `?` (see
|
||||
/// `docmd/theory/evolutionary_distances.md`, "Locus dropout under
|
||||
/// incomplete coverage") applies one level down: whether a genome shows 1
|
||||
/// vs 2 (etc.) detected members of a *present* family is exactly as
|
||||
/// vulnerable to sampling failure as whether the family was detected at
|
||||
/// all. Without this, `∅`-involving transitions are neutralised (via the
|
||||
/// `?` recoding, bypassing this matrix's row/column 0 entirely) but
|
||||
/// cardinality changes *between two otherwise-detected, non-empty* states
|
||||
/// (e.g. `{A} -> {A,C}`) still carried the same calibrated
|
||||
/// `P_cardinality` penalty as any other gain/loss — inconsistent with
|
||||
/// `--free-loss`'s own rationale. With the factor dropped, cost is driven
|
||||
/// only by composition matching (shared-base retention and paired
|
||||
/// substitutions), never by a state pair's cardinality difference alone.
|
||||
pub fn pairwise_cost_matrix(p_card: &[[f64; 5]; 5], p_comp: &[[f64; 4]; 4], free_loss: bool) -> [[f64; 16]; 16] {
|
||||
let mut raw = [[0.0f64; 16]; 16];
|
||||
for a in 0u8..16 {
|
||||
for b in 0u8..16 {
|
||||
@@ -138,10 +154,12 @@ pub fn pairwise_cost_matrix(p_card: &[[f64; 5]; 5], p_comp: &[[f64; 4]; 4]) -> [
|
||||
let gained: Vec<u8> = (0..4).filter(|&i| b & (1 << i) != 0 && a & (1 << i) == 0).collect();
|
||||
|
||||
let mut log_p = 0.0; // accumulate ln(P), so 0.0 = probability 1
|
||||
let card_a = a.count_ones() as usize;
|
||||
let card_b = b.count_ones() as usize;
|
||||
let p_c = p_card[card_a][card_b];
|
||||
log_p += if p_c > 0.0 { p_c.ln() } else { f64::NEG_INFINITY };
|
||||
if !free_loss {
|
||||
let card_a = a.count_ones() as usize;
|
||||
let card_b = b.count_ones() as usize;
|
||||
let p_c = p_card[card_a][card_b];
|
||||
log_p += if p_c > 0.0 { p_c.ln() } else { f64::NEG_INFINITY };
|
||||
}
|
||||
|
||||
for i in 0..4u8 {
|
||||
if shared & (1 << i) != 0 {
|
||||
@@ -231,7 +249,7 @@ mod tests {
|
||||
[0.15, 0.05, 0.7, 0.1],
|
||||
[0.05, 0.15, 0.1, 0.7],
|
||||
];
|
||||
let cost = pairwise_cost_matrix(&p_card, &p_comp);
|
||||
let cost = pairwise_cost_matrix(&p_card, &p_comp, false);
|
||||
for a in 0..16 {
|
||||
assert_eq!(cost[a][a], 0.0);
|
||||
for b in 0..16 {
|
||||
@@ -239,4 +257,36 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_loss_ignores_cardinality_transition_probs() {
|
||||
// A skewed cardinality model (cardinality change made artificially
|
||||
// expensive) must have zero effect on the cost matrix once
|
||||
// `free_loss` is set — the whole point of the flag.
|
||||
let p_card_uniform = [[0.2f64; 5]; 5];
|
||||
let p_card_skewed = [
|
||||
[0.96, 0.01, 0.01, 0.01, 0.01],
|
||||
[0.01, 0.96, 0.01, 0.01, 0.01],
|
||||
[0.01, 0.01, 0.96, 0.01, 0.01],
|
||||
[0.01, 0.01, 0.01, 0.96, 0.01],
|
||||
[0.01, 0.01, 0.01, 0.01, 0.96],
|
||||
];
|
||||
let p_comp = [
|
||||
[0.7, 0.1, 0.15, 0.05],
|
||||
[0.1, 0.7, 0.05, 0.15],
|
||||
[0.15, 0.05, 0.7, 0.1],
|
||||
[0.05, 0.15, 0.1, 0.7],
|
||||
];
|
||||
let cost_uniform = pairwise_cost_matrix(&p_card_uniform, &p_comp, true);
|
||||
let cost_skewed = pairwise_cost_matrix(&p_card_skewed, &p_comp, true);
|
||||
for a in 0..16 {
|
||||
for b in 0..16 {
|
||||
assert!(
|
||||
(cost_uniform[a][b] - cost_skewed[a][b]).abs() < 1e-9,
|
||||
"cost[{a}][{b}] differs between cardinality models under free_loss: {} vs {}",
|
||||
cost_uniform[a][b], cost_skewed[a][b],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user