Push zpwxxpnpktps #67
@@ -5,7 +5,7 @@ use obifastwrite::{JsonVal, write_record};
|
|||||||
use obikphylo::siblings::SnpAlignment;
|
use obikphylo::siblings::SnpAlignment;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use super::sankoff::{state_index_table, STATE_SYMBOL};
|
use super::sankoff::{STATE_SYMBOL, state_index_table};
|
||||||
|
|
||||||
// ── Sankoff-calibrated data → IQ-TREE custom ML model + recoded alignment ──
|
// ── Sankoff-calibrated data → IQ-TREE custom ML model + recoded alignment ──
|
||||||
//
|
//
|
||||||
@@ -85,7 +85,8 @@ impl CompactAlphabet {
|
|||||||
/// two exports.
|
/// two exports.
|
||||||
fn drop_ascertainment_noninformative(alignment: &SnpAlignment) -> SnpAlignment {
|
fn drop_ascertainment_noninformative(alignment: &SnpAlignment) -> SnpAlignment {
|
||||||
let n_sites = alignment.sequences.first().map(|s| s.len()).unwrap_or(0);
|
let n_sites = alignment.sequences.first().map(|s| s.len()).unwrap_or(0);
|
||||||
let keep: Vec<bool> = (0..n_sites).map(|site| {
|
let keep: Vec<bool> = (0..n_sites)
|
||||||
|
.map(|site| {
|
||||||
let mut first: Option<u8> = None;
|
let mut first: Option<u8> = None;
|
||||||
for seq in &alignment.sequences {
|
for seq in &alignment.sequences {
|
||||||
let b = seq[site];
|
let b = seq[site];
|
||||||
@@ -99,10 +100,19 @@ fn drop_ascertainment_noninformative(alignment: &SnpAlignment) -> SnpAlignment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
false // all calls missing, or all calls agree — non-informative
|
false // all calls missing, or all calls agree — non-informative
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let sequences = alignment.sequences.iter()
|
let sequences = alignment
|
||||||
.map(|seq| seq.iter().zip(keep.iter()).filter(|&(_, &k)| k).map(|(&b, _)| b).collect())
|
.sequences
|
||||||
|
.iter()
|
||||||
|
.map(|seq| {
|
||||||
|
seq.iter()
|
||||||
|
.zip(keep.iter())
|
||||||
|
.filter(|&(_, &k)| k)
|
||||||
|
.map(|(&b, _)| b)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
SnpAlignment { sequences }
|
SnpAlignment { sequences }
|
||||||
}
|
}
|
||||||
@@ -114,8 +124,14 @@ fn drop_ascertainment_noninformative(alignment: &SnpAlignment) -> SnpAlignment {
|
|||||||
/// states into the missing-data treatment before a second
|
/// states into the missing-data treatment before a second
|
||||||
/// `compact_alphabet` pass, without duplicating that treatment's logic.
|
/// `compact_alphabet` pass, without duplicating that treatment's logic.
|
||||||
fn recode_symbols_as_absent(alignment: &SnpAlignment, symbols: &[u8]) -> SnpAlignment {
|
fn recode_symbols_as_absent(alignment: &SnpAlignment, symbols: &[u8]) -> SnpAlignment {
|
||||||
let sequences = alignment.sequences.iter()
|
let sequences = alignment
|
||||||
.map(|seq| seq.iter().map(|&b| if symbols.contains(&b) { b'-' } else { b }).collect())
|
.sequences
|
||||||
|
.iter()
|
||||||
|
.map(|seq| {
|
||||||
|
seq.iter()
|
||||||
|
.map(|&b| if symbols.contains(&b) { b'-' } else { b })
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
SnpAlignment { sequences }
|
SnpAlignment { sequences }
|
||||||
}
|
}
|
||||||
@@ -151,11 +167,16 @@ fn compact_alphabet(alignment: &SnpAlignment, free_loss: bool) -> CompactAlphabe
|
|||||||
}
|
}
|
||||||
|
|
||||||
let total: u64 = compact_to_old.iter().map(|&old| counts[old as usize]).sum();
|
let total: u64 = compact_to_old.iter().map(|&old| counts[old as usize]).sum();
|
||||||
let freq: Vec<f64> = compact_to_old.iter()
|
let freq: Vec<f64> = compact_to_old
|
||||||
|
.iter()
|
||||||
.map(|&old| counts[old as usize] as f64 / total as f64)
|
.map(|&old| counts[old as usize] as f64 / total as f64)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
CompactAlphabet { old_to_compact, compact_to_old, freq }
|
CompactAlphabet {
|
||||||
|
old_to_compact,
|
||||||
|
compact_to_old,
|
||||||
|
freq,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write `<prefix>_iqtree_states.csv`: the mapping from IQ-TREE's own
|
/// Write `<prefix>_iqtree_states.csv`: the mapping from IQ-TREE's own
|
||||||
@@ -168,7 +189,8 @@ fn compact_alphabet(alignment: &SnpAlignment, free_loss: bool) -> CompactAlphabe
|
|||||||
/// (e.g. "state 0 has zero exchangeability with everything else") can't be
|
/// (e.g. "state 0 has zero exchangeability with everything else") can't be
|
||||||
/// traced back to which real state that is.
|
/// traced back to which real state that is.
|
||||||
fn write_iqtree_states_csv(alphabet: &CompactAlphabet, output: &Option<PathBuf>) -> String {
|
fn write_iqtree_states_csv(alphabet: &CompactAlphabet, output: &Option<PathBuf>) -> String {
|
||||||
let path = output.as_ref()
|
let path = output
|
||||||
|
.as_ref()
|
||||||
.map(|p| format!("{}_iqtree_states.csv", p.display()))
|
.map(|p| format!("{}_iqtree_states.csv", p.display()))
|
||||||
.unwrap_or_else(|| "iqtree_states.csv".into());
|
.unwrap_or_else(|| "iqtree_states.csv".into());
|
||||||
let mut f = BufWriter::new(std::fs::File::create(&path).unwrap_or_else(|e| {
|
let mut f = BufWriter::new(std::fs::File::create(&path).unwrap_or_else(|e| {
|
||||||
@@ -177,7 +199,12 @@ fn write_iqtree_states_csv(alphabet: &CompactAlphabet, output: &Option<PathBuf>)
|
|||||||
}));
|
}));
|
||||||
writeln!(f, "iqtree_symbol,canonical_symbol,frequency").unwrap();
|
writeln!(f, "iqtree_symbol,canonical_symbol,frequency").unwrap();
|
||||||
for (compact, &old) in alphabet.compact_to_old.iter().enumerate() {
|
for (compact, &old) in alphabet.compact_to_old.iter().enumerate() {
|
||||||
writeln!(f, "{},{},{}", IQTREE_STATE_SYMBOL[compact], STATE_SYMBOL[old as usize], alphabet.freq[compact]).unwrap();
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{},{},{}",
|
||||||
|
IQTREE_STATE_SYMBOL[compact], STATE_SYMBOL[old as usize], alphabet.freq[compact]
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
@@ -186,10 +213,15 @@ fn write_iqtree_states_csv(alphabet: &CompactAlphabet, output: &Option<PathBuf>)
|
|||||||
/// `-m <file>+ASC` reads. Returns the path, so the caller can print a
|
/// `-m <file>+ASC` reads. Returns the path, so the caller can print a
|
||||||
/// single combined "how to run this" message once the alignment is also
|
/// single combined "how to run this" message once the alignment is also
|
||||||
/// written.
|
/// written.
|
||||||
fn write_iqtree_model(matrix: &[[f64; 16]; 16], alphabet: &CompactAlphabet, output: &Option<PathBuf>) -> String {
|
fn write_iqtree_model(
|
||||||
|
matrix: &[[f64; 16]; 16],
|
||||||
|
alphabet: &CompactAlphabet,
|
||||||
|
output: &Option<PathBuf>,
|
||||||
|
) -> String {
|
||||||
let rate = |old_i: u8, old_j: u8| (-matrix[old_i as usize][old_j as usize]).exp();
|
let rate = |old_i: u8, old_j: u8| (-matrix[old_i as usize][old_j as usize]).exp();
|
||||||
|
|
||||||
let model_path = output.as_ref()
|
let model_path = output
|
||||||
|
.as_ref()
|
||||||
.map(|p| format!("{}_iqtree.model", p.display()))
|
.map(|p| format!("{}_iqtree.model", p.display()))
|
||||||
.unwrap_or_else(|| "iqtree.model".into());
|
.unwrap_or_else(|| "iqtree.model".into());
|
||||||
let mut f = BufWriter::new(std::fs::File::create(&model_path).unwrap_or_else(|e| {
|
let mut f = BufWriter::new(std::fs::File::create(&model_path).unwrap_or_else(|e| {
|
||||||
@@ -198,12 +230,30 @@ fn write_iqtree_model(matrix: &[[f64; 16]; 16], alphabet: &CompactAlphabet, outp
|
|||||||
}));
|
}));
|
||||||
for i in 1..alphabet.k() {
|
for i in 1..alphabet.k() {
|
||||||
let row: Vec<String> = (0..i)
|
let row: Vec<String> = (0..i)
|
||||||
.map(|j| format!("{:.6}", rate(alphabet.compact_to_old[i], alphabet.compact_to_old[j])))
|
.map(|j| {
|
||||||
|
format!(
|
||||||
|
"{:.6}",
|
||||||
|
rate(alphabet.compact_to_old[i], alphabet.compact_to_old[j])
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
writeln!(f, "{}", row.join(" ")).unwrap();
|
writeln!(f, "{}", row.join(" ")).unwrap();
|
||||||
}
|
}
|
||||||
writeln!(f, "{}", alphabet.freq.iter().map(|p| format!("{p:.6}")).collect::<Vec<_>>().join(" ")).unwrap();
|
writeln!(
|
||||||
info!("IQ-TREE model file → {model_path} ({} of 16 states present in the alignment)", alphabet.k());
|
f,
|
||||||
|
"{}",
|
||||||
|
alphabet
|
||||||
|
.freq
|
||||||
|
.iter()
|
||||||
|
.map(|p| format!("{p:.6}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
info!(
|
||||||
|
"IQ-TREE model file → {model_path} ({} of 16 states present in the alignment)",
|
||||||
|
alphabet.k()
|
||||||
|
);
|
||||||
model_path
|
model_path
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +270,8 @@ fn write_iqtree_alignment(
|
|||||||
) -> (String, usize) {
|
) -> (String, usize) {
|
||||||
let iupac_to_state = state_index_table();
|
let iupac_to_state = state_index_table();
|
||||||
|
|
||||||
let fasta_path = output.as_ref()
|
let fasta_path = output
|
||||||
|
.as_ref()
|
||||||
.map(|p| format!("{}_iqtree.fasta", p.display()))
|
.map(|p| format!("{}_iqtree.fasta", p.display()))
|
||||||
.unwrap_or_else(|| "iqtree.fasta".into());
|
.unwrap_or_else(|| "iqtree.fasta".into());
|
||||||
let mut f = BufWriter::new(std::fs::File::create(&fasta_path).unwrap_or_else(|e| {
|
let mut f = BufWriter::new(std::fs::File::create(&fasta_path).unwrap_or_else(|e| {
|
||||||
@@ -229,7 +280,9 @@ fn write_iqtree_alignment(
|
|||||||
}));
|
}));
|
||||||
let n_sites = alignment.sequences.first().map(|s| s.len()).unwrap_or(0);
|
let n_sites = alignment.sequences.first().map(|s| s.len()).unwrap_or(0);
|
||||||
for (label, seq) in labels.iter().zip(alignment.sequences.iter()) {
|
for (label, seq) in labels.iter().zip(alignment.sequences.iter()) {
|
||||||
let recoded: Vec<u8> = seq.iter().map(|&b| {
|
let recoded: Vec<u8> = seq
|
||||||
|
.iter()
|
||||||
|
.map(|&b| {
|
||||||
if free_loss && b == b'-' {
|
if free_loss && b == b'-' {
|
||||||
return b'?';
|
return b'?';
|
||||||
}
|
}
|
||||||
@@ -238,8 +291,15 @@ fn write_iqtree_alignment(
|
|||||||
let compact = alphabet.old_to_compact[old]
|
let compact = alphabet.old_to_compact[old]
|
||||||
.expect("state occurs in the alignment, so it must have a compact index");
|
.expect("state occurs in the alignment, so it must have a compact index");
|
||||||
IQTREE_STATE_SYMBOL[compact as usize] as u8
|
IQTREE_STATE_SYMBOL[compact as usize] as u8
|
||||||
}).collect();
|
})
|
||||||
write_record(&recoded, label, &[("n_sites", JsonVal::Num(n_sites as u64))], &mut f).unwrap_or_else(|e| {
|
.collect();
|
||||||
|
write_record(
|
||||||
|
&recoded,
|
||||||
|
label,
|
||||||
|
&[("n_sites", JsonVal::Num(n_sites as u64))],
|
||||||
|
&mut f,
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
eprintln!("error writing {fasta_path}: {e}");
|
eprintln!("error writing {fasta_path}: {e}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
});
|
});
|
||||||
@@ -283,7 +343,10 @@ pub(super) fn write_iqtree(
|
|||||||
// instability warnings).
|
// instability warnings).
|
||||||
let refiltered;
|
let refiltered;
|
||||||
let alignment = if free_loss {
|
let alignment = if free_loss {
|
||||||
let low_freq_symbols: Vec<u8> = alphabet.compact_to_old.iter().zip(alphabet.freq.iter())
|
let low_freq_symbols: Vec<u8> = alphabet
|
||||||
|
.compact_to_old
|
||||||
|
.iter()
|
||||||
|
.zip(alphabet.freq.iter())
|
||||||
.filter(|&(_, &f)| f < min_freq)
|
.filter(|&(_, &f)| f < min_freq)
|
||||||
.map(|(&old, _)| STATE_SYMBOL[old as usize] as u8)
|
.map(|(&old, _)| STATE_SYMBOL[old as usize] as u8)
|
||||||
.collect();
|
.collect();
|
||||||
@@ -297,7 +360,10 @@ pub(super) fn write_iqtree(
|
|||||||
info!(
|
info!(
|
||||||
"--iqtree-min-freq {min_freq}: {} rare state(s) ({}) recoded as missing, {before} → {after} sites",
|
"--iqtree-min-freq {min_freq}: {} rare state(s) ({}) recoded as missing, {before} → {after} sites",
|
||||||
low_freq_symbols.len(),
|
low_freq_symbols.len(),
|
||||||
low_freq_symbols.iter().map(|&b| b as char).collect::<String>(),
|
low_freq_symbols
|
||||||
|
.iter()
|
||||||
|
.map(|&b| b as char)
|
||||||
|
.collect::<String>(),
|
||||||
);
|
);
|
||||||
alphabet = compact_alphabet(&refiltered, free_loss);
|
alphabet = compact_alphabet(&refiltered, free_loss);
|
||||||
&refiltered
|
&refiltered
|
||||||
@@ -308,9 +374,11 @@ pub(super) fn write_iqtree(
|
|||||||
|
|
||||||
let states_path = write_iqtree_states_csv(&alphabet, output);
|
let states_path = write_iqtree_states_csv(&alphabet, output);
|
||||||
let model_path = write_iqtree_model(matrix, &alphabet, output);
|
let model_path = write_iqtree_model(matrix, &alphabet, output);
|
||||||
let (fasta_path, n_sites) = write_iqtree_alignment(alignment, labels, &alphabet, output, free_loss);
|
let (fasta_path, n_sites) =
|
||||||
|
write_iqtree_alignment(alignment, labels, &alphabet, output, free_loss);
|
||||||
|
|
||||||
let prefix_name = output.as_ref()
|
let prefix_name = output
|
||||||
|
.as_ref()
|
||||||
.and_then(|p| p.file_name())
|
.and_then(|p| p.file_name())
|
||||||
.map(|n| format!("{}_iqtree", n.to_string_lossy()))
|
.map(|n| format!("{}_iqtree", n.to_string_lossy()))
|
||||||
.unwrap_or_else(|| "iqtree".into());
|
.unwrap_or_else(|| "iqtree".into());
|
||||||
@@ -318,7 +386,9 @@ pub(super) fn write_iqtree(
|
|||||||
"IQ-TREE alignment → {fasta_path} ({n_sites} sites, {} states)\n\
|
"IQ-TREE alignment → {fasta_path} ({n_sites} sites, {} states)\n\
|
||||||
IQ-TREE state mapping → {states_path}\n\
|
IQ-TREE state mapping → {states_path}\n\
|
||||||
Run with:\n \
|
Run with:\n \
|
||||||
iqtree3 -s {fasta_path} --seqtype MORPH -m {model_path}+ASC --prefix {prefix_name} -T AUTO",
|
iqtree3 -s {fasta_path} --seqtype MORPH -m {model_path}+ASC --prefix {prefix_name} -T AUTO\n\
|
||||||
|
\n\
|
||||||
|
options -alrt 1000 -B 1000 can be added to evaluate robustness of the tree",
|
||||||
alphabet.k()
|
alphabet.k()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -336,11 +406,7 @@ mod tests {
|
|||||||
// ("absent") was still being counted despite being recoded to `?`
|
// ("absent") was still being counted despite being recoded to `?`
|
||||||
// (IQ-TREE's own missing symbol) in the alignment actually written.
|
// (IQ-TREE's own missing symbol) in the alignment actually written.
|
||||||
let alignment = SnpAlignment {
|
let alignment = SnpAlignment {
|
||||||
sequences: vec![
|
sequences: vec![vec![b'A', b'-'], vec![b'C', b'-'], vec![b'-', b'G']],
|
||||||
vec![b'A', b'-'],
|
|
||||||
vec![b'C', b'-'],
|
|
||||||
vec![b'-', b'G'],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let alphabet = compact_alphabet(&alignment, true);
|
let alphabet = compact_alphabet(&alignment, true);
|
||||||
@@ -351,18 +417,18 @@ mod tests {
|
|||||||
alphabet.compact_to_old
|
alphabet.compact_to_old
|
||||||
);
|
);
|
||||||
let sum: f64 = alphabet.freq.iter().sum();
|
let sum: f64 = alphabet.freq.iter().sum();
|
||||||
assert!((sum - 1.0).abs() < 1e-9, "frequencies must sum to 1, got {sum} ({:?})", alphabet.freq);
|
assert!(
|
||||||
|
(sum - 1.0).abs() < 1e-9,
|
||||||
|
"frequencies must sum to 1, got {sum} ({:?})",
|
||||||
|
alphabet.freq
|
||||||
|
);
|
||||||
assert_eq!(alphabet.k(), 3, "A, C, G — 3 real states, `-` excluded");
|
assert_eq!(alphabet.k(), 3, "A, C, G — 3 real states, `-` excluded");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn without_free_loss_absent_state_is_counted_normally() {
|
fn without_free_loss_absent_state_is_counted_normally() {
|
||||||
let alignment = SnpAlignment {
|
let alignment = SnpAlignment {
|
||||||
sequences: vec![
|
sequences: vec![vec![b'A', b'-'], vec![b'C', b'-'], vec![b'-', b'G']],
|
||||||
vec![b'A', b'-'],
|
|
||||||
vec![b'C', b'-'],
|
|
||||||
vec![b'-', b'G'],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let alphabet = compact_alphabet(&alignment, false);
|
let alphabet = compact_alphabet(&alignment, false);
|
||||||
@@ -372,7 +438,11 @@ mod tests {
|
|||||||
"state 0 (absent, recoded from '-') must be counted when --free-loss is off"
|
"state 0 (absent, recoded from '-') must be counted when --free-loss is off"
|
||||||
);
|
);
|
||||||
let sum: f64 = alphabet.freq.iter().sum();
|
let sum: f64 = alphabet.freq.iter().sum();
|
||||||
assert!((sum - 1.0).abs() < 1e-9, "frequencies must sum to 1, got {sum} ({:?})", alphabet.freq);
|
assert!(
|
||||||
|
(sum - 1.0).abs() < 1e-9,
|
||||||
|
"frequencies must sum to 1, got {sum} ({:?})",
|
||||||
|
alphabet.freq
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -380,19 +450,21 @@ mod tests {
|
|||||||
// 'A' (state 1) and 'G' (state 4) occur, '-' (state 0) excluded by
|
// 'A' (state 1) and 'G' (state 4) occur, '-' (state 0) excluded by
|
||||||
// --free-loss — compact index 0 -> 'A', compact index 1 -> 'G'.
|
// --free-loss — compact index 0 -> 'A', compact index 1 -> 'G'.
|
||||||
let alignment = SnpAlignment {
|
let alignment = SnpAlignment {
|
||||||
sequences: vec![
|
sequences: vec![vec![b'A', b'-'], vec![b'-', b'G']],
|
||||||
vec![b'A', b'-'],
|
|
||||||
vec![b'-', b'G'],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
let alphabet = compact_alphabet(&alignment, true);
|
let alphabet = compact_alphabet(&alignment, true);
|
||||||
let output = Some(std::env::temp_dir().join(format!("obikmer_test_iqtree_states_{}", std::process::id())));
|
let output = Some(
|
||||||
|
std::env::temp_dir().join(format!("obikmer_test_iqtree_states_{}", std::process::id())),
|
||||||
|
);
|
||||||
|
|
||||||
let path = write_iqtree_states_csv(&alphabet, &output);
|
let path = write_iqtree_states_csv(&alphabet, &output);
|
||||||
let csv = std::fs::read_to_string(&path).unwrap();
|
let csv = std::fs::read_to_string(&path).unwrap();
|
||||||
std::fs::remove_file(&path).ok();
|
std::fs::remove_file(&path).ok();
|
||||||
let mut lines = csv.lines();
|
let mut lines = csv.lines();
|
||||||
assert_eq!(lines.next(), Some("iqtree_symbol,canonical_symbol,frequency"));
|
assert_eq!(
|
||||||
|
lines.next(),
|
||||||
|
Some("iqtree_symbol,canonical_symbol,frequency")
|
||||||
|
);
|
||||||
assert_eq!(lines.next(), Some("0,A,0.5"));
|
assert_eq!(lines.next(), Some("0,A,0.5"));
|
||||||
assert_eq!(lines.next(), Some("1,G,0.5"));
|
assert_eq!(lines.next(), Some("1,G,0.5"));
|
||||||
assert!(lines.next().is_none());
|
assert!(lines.next().is_none());
|
||||||
@@ -407,7 +479,11 @@ mod tests {
|
|||||||
// at 1/62, well below the 0.05 threshold used here.
|
// at 1/62, well below the 0.05 threshold used here.
|
||||||
let mut sequences: Vec<Vec<u8>> = vec![Vec::new(); 3];
|
let mut sequences: Vec<Vec<u8>> = vec![Vec::new(); 3];
|
||||||
for i in 0..20 {
|
for i in 0..20 {
|
||||||
let (a, b, c) = if i % 2 == 0 { (b'A', b'C', b'A') } else { (b'C', b'A', b'C') };
|
let (a, b, c) = if i % 2 == 0 {
|
||||||
|
(b'A', b'C', b'A')
|
||||||
|
} else {
|
||||||
|
(b'C', b'A', b'C')
|
||||||
|
};
|
||||||
sequences[0].push(a);
|
sequences[0].push(a);
|
||||||
sequences[1].push(b);
|
sequences[1].push(b);
|
||||||
sequences[2].push(c);
|
sequences[2].push(c);
|
||||||
@@ -418,15 +494,24 @@ mod tests {
|
|||||||
let alignment = SnpAlignment { sequences };
|
let alignment = SnpAlignment { sequences };
|
||||||
let labels = vec!["g1".to_string(), "g2".to_string(), "g3".to_string()];
|
let labels = vec!["g1".to_string(), "g2".to_string(), "g3".to_string()];
|
||||||
let matrix = [[0.0f64; 16]; 16];
|
let matrix = [[0.0f64; 16]; 16];
|
||||||
let prefix = std::env::temp_dir().join(format!("obikmer_test_iqtree_minfreq_{}", std::process::id()));
|
let prefix = std::env::temp_dir().join(format!(
|
||||||
|
"obikmer_test_iqtree_minfreq_{}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
let output = Some(prefix.clone());
|
let output = Some(prefix.clone());
|
||||||
|
|
||||||
write_iqtree(&matrix, &alignment, &labels, &output, true, 0.05);
|
write_iqtree(&matrix, &alignment, &labels, &output, true, 0.05);
|
||||||
|
|
||||||
let states_path = format!("{}_iqtree_states.csv", prefix.display());
|
let states_path = format!("{}_iqtree_states.csv", prefix.display());
|
||||||
let csv = std::fs::read_to_string(&states_path).unwrap();
|
let csv = std::fs::read_to_string(&states_path).unwrap();
|
||||||
assert!(!csv.contains(",M,"), "M (freq ~1/62) must be folded into missing under --iqtree-min-freq 0.05, got:\n{csv}");
|
assert!(
|
||||||
assert!(csv.contains(",A,") && csv.contains(",C,"), "A/C must survive (well above threshold), got:\n{csv}");
|
!csv.contains(",M,"),
|
||||||
|
"M (freq ~1/62) must be folded into missing under --iqtree-min-freq 0.05, got:\n{csv}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
csv.contains(",A,") && csv.contains(",C,"),
|
||||||
|
"A/C must survive (well above threshold), got:\n{csv}"
|
||||||
|
);
|
||||||
|
|
||||||
for suffix in ["_iqtree_states.csv", "_iqtree.model", "_iqtree.fasta"] {
|
for suffix in ["_iqtree_states.csv", "_iqtree.model", "_iqtree.fasta"] {
|
||||||
std::fs::remove_file(format!("{}{suffix}", prefix.display())).ok();
|
std::fs::remove_file(format!("{}{suffix}", prefix.display())).ok();
|
||||||
|
|||||||
Reference in New Issue
Block a user