1mod permutation;
5
6use std::{collections::HashMap, fmt};
7
8use itertools::Itertools;
9
10use crate::seq::file::SeqFile;
11
12use crate::alignment::SeqType::{Nucleic, Protein};
13
14const UC_CONS_THRESHOLD: f64 = 0.8; const LC_CONS_THRESHOLD: f64 = 0.2; type ResidueDistribution = HashMap<char, f64>;
19type ResidueCounts = HashMap<char, u64>;
20
21#[derive(PartialEq, Clone, Copy, Debug)]
22pub enum SeqType {
23 Nucleic,
24 Protein,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub enum RefSpec {
29 Consensus,
30 Rank(usize),
31}
32
33pub enum RefSpecError {
34 MalformedInt(String),
35 ZeroRef,
36 RefTooLarge(usize),
37}
38
39#[derive(Debug, PartialEq)]
40pub enum LoHiState {
41 Low,
42 High,
43}
44
45impl fmt::Display for RefSpecError {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 let err_msg = match self {
48 RefSpecError::MalformedInt(mfi) => format!("Malformed integer {}", mfi),
49 RefSpecError::ZeroRef => "Ref # must be > 0".to_string(),
50 RefSpecError::RefTooLarge(max) => format!("Ref # too large (max {})", max),
51 };
52 write!(f, "{}", err_msg)
53 }
54}
55
56pub struct Alignment {
57 pub headers: Vec<String>,
58 pub sequences: Vec<String>,
59 pub consensus: String,
69 pub entropies: Vec<f64>,
70 pub densities: Vec<f64>,
71
72 pub id_wrt_reference: Vec<f64>, pub relative_seq_len: Vec<f64>,
81 pub macromolecule_type: SeqType,
82 ref_spec: RefSpec,
85}
86
87#[derive(Debug, PartialEq)]
88struct BestResidue {
89 residue: char,
90 frequency: u64,
91}
92
93impl Alignment {
94 pub fn from_file(seq_file: SeqFile) -> Alignment {
96 let mut headers: Vec<String> = Vec::new();
97 let mut sequences: Vec<String> = Vec::new();
98 let mut max_len: usize = 0;
99 for record in seq_file {
100 headers.push(record.header);
101 let l = record.sequence.len();
102 sequences.push(record.sequence);
103 if l > max_len {
104 max_len = l;
105 }
106 }
107 sequences
110 .iter_mut()
111 .for_each(|s| *s = format!("{:<width$}", s, width = max_len));
112 let first_seq = sequences.first();
114 let macromolecule_type = seq_type(first_seq.expect("No sequence found."));
115 let consensus = consensus(&sequences, macromolecule_type);
116 let entropies = entropies(&sequences);
117 let densities = densities(&sequences);
118 let id_wrt_reference = sequences
119 .iter()
120 .map(|seq| percent_identity(seq, &consensus))
121 .collect();
122 let relative_seq_len = sequences.iter().map(|seq| seq_len_nogaps(seq)).collect();
123
124 Alignment {
125 headers,
126 sequences,
127 consensus,
128 entropies,
129 densities,
130 id_wrt_reference,
131 relative_seq_len,
132 macromolecule_type,
133 ref_spec: RefSpec::Consensus,
134 }
135 }
136
137 #[allow(dead_code)]
140 pub fn from_vecs(hdrs: Vec<String>, seqs: Vec<String>) -> Alignment {
141 assert_eq!(hdrs.len(), seqs.len());
142 let headers = hdrs;
143 let sequences = seqs;
144 let first_seq = sequences.first();
145 let macromolecule_type = seq_type(first_seq.expect("No sequence found."));
146 let consensus = consensus(&sequences, macromolecule_type);
147 let entropies = entropies(&sequences);
148 let densities = densities(&sequences);
149 let id_wrt_reference = sequences
150 .iter()
151 .map(|seq| percent_identity(seq, &consensus))
152 .collect();
153 let relative_seq_len = sequences.iter().map(|seq| seq_len_nogaps(seq)).collect();
154
155 Alignment {
156 headers,
157 sequences,
158 consensus,
159 entropies,
160 densities,
161 id_wrt_reference,
162 relative_seq_len,
163 macromolecule_type,
164 ref_spec: RefSpec::Consensus,
165 }
166 }
167
168 pub fn num_seq(&self) -> usize {
169 self.sequences.len()
170 }
171
172 pub fn aln_len(&self) -> usize {
174 self.sequences[0].len()
175 }
176
177 pub fn macromolecule_type(&self) -> SeqType {
178 self.macromolecule_type
179 }
180
181 pub fn get_ref_spec(&self) -> RefSpec {
182 self.ref_spec
183 }
184
185 pub fn set_ref_spec(&mut self, spec: RefSpec) -> Result<(), RefSpecError> {
186 match spec {
187 RefSpec::Rank(rk) if rk >= self.num_seq() => {
190 return Err(RefSpecError::RefTooLarge(self.num_seq()));
191 }
192 _ => self.ref_spec = spec,
193 }
194 let reference = self.reference();
196 self.id_wrt_reference = self
197 .sequences
198 .iter()
199 .map(|seq| percent_identity(seq, &reference))
200 .collect();
201 Ok(())
202 }
203
204 pub fn reference(&self) -> String {
205 match self.ref_spec {
206 RefSpec::Consensus => self.consensus.clone(),
207 RefSpec::Rank(rk) => self.sequences[rk].clone(),
208 }
209 }
210}
211
212fn res_count(sequences: &Vec<String>, col: usize) -> ResidueCounts {
213 let mut freqs: ResidueCounts = HashMap::new();
214 for seq in sequences {
215 let residue = seq.as_bytes()[col] as char;
216 *freqs.entry(residue).or_insert(0) += 1;
217 }
218 freqs
219}
220
221pub fn consensus(sequences: &Vec<String>, seq_type: SeqType) -> String {
228 let mut consensus = String::new();
229 for j in 0..sequences[0].len() {
230 let dist = res_count(sequences, j); let br = best_residue(&dist, seq_type);
232 let rel_freq: f64 = (br.frequency as f64 / sequences.len() as f64) as f64;
233 if rel_freq >= UC_CONS_THRESHOLD {
234 consensus.push(br.residue.to_ascii_uppercase());
235 } else if rel_freq >= LC_CONS_THRESHOLD {
236 if br.residue.is_alphabetic() {
237 consensus.push(br.residue.to_ascii_lowercase());
238 } else {
239 consensus.push(br.residue);
241 }
242 } else {
243 consensus.push('*');
244 }
245 }
246 consensus
247}
248
249pub fn entropies(sequences: &Vec<String>) -> Vec<f64> {
250 let mut entropies: Vec<f64> = Vec::new();
251 for j in 0..sequences[0].len() {
252 let dist = res_count(sequences, j);
253 let freq = to_freq_distrib(&dist);
254 let e = entropy(&freq);
255 entropies.push(e);
256 }
257 entropies
258}
259
260pub fn col_density(sequences: &Vec<String>, col: usize) -> f64 {
261 let mut mass = 0;
262 for seq in sequences {
263 match seq.as_bytes()[col] as char {
264 'a'..='z' | 'A'..='Z' => mass += 1,
265 '-' | '.' | ' ' => {}
266 other => {
267 panic!("Character {other} unexpected in an alignment.\nThis might be due to file format, please see option -f.");
268 }
269 }
270 }
271 mass as f64 / sequences.len() as f64
272}
273
274pub fn densities(sequences: &Vec<String>) -> Vec<f64> {
275 (0..sequences[0].len())
276 .map(|col| col_density(sequences, col))
277 .collect()
278}
279
280fn iupac_ambiguity_code(amb_nt: &mut [char]) -> char {
281 let mut normalized_nt = amb_nt
282 .into_iter()
283 .map(|nt| nt.to_ascii_lowercase())
284 .collect::<Vec<char>>();
285 normalized_nt.sort();
286
287 let normalized_nt_as_string = normalized_nt.into_iter().join("");
288
289 match normalized_nt_as_string.as_str() {
290 "a" => 'a', "ac" => 'm', "acg" => 'v', "acgt" => 'n', "act" => 'h', "ag" => 'r', "agt" => 'd', "at" => 'w', "c" => 'c', "cg" => 's', "cgt" => 'b', "ct" => 'y', "g" => 'g', "gt" => 'k', "t" => 't', &_ => 'n', }
307}
308
309fn best_residue(counts: &ResidueCounts, seq_type: SeqType) -> BestResidue {
310 let max_freq = counts.values().max().unwrap();
311 let mut most_frequent_residues = counts .keys()
313 .filter(|&&k| counts.get(&k) == Some(max_freq))
314 .map(|&k| k)
315 .collect::<Vec<char>>();
316
317 let residue = if most_frequent_residues.len() == 1 {
318 most_frequent_residues[0]
319 } else if SeqType::Protein == seq_type {
320 'X'
321 } else {
322 iupac_ambiguity_code(&mut most_frequent_residues)
323 };
324
325 BestResidue {
326 residue: residue,
327 frequency: *max_freq,
328 }
329}
330
331fn to_freq_distrib(counts: &ResidueCounts) -> ResidueDistribution {
336 let total_counts: u64 = counts
337 .iter()
338 .filter(|(res, _count)| **res != '-')
339 .map(|(_res, count)| count)
340 .sum();
341 let mut distrib = ResidueDistribution::new();
342 for (residue, count) in counts.iter() {
343 if *residue == '-' {
344 continue;
345 }
346 distrib.insert(*residue, *count as f64 / total_counts as f64);
347 }
348 distrib
349}
350
351fn entropy(freqs: &ResidueDistribution) -> f64 {
352 let residues: Vec<&char> = freqs.keys().filter(|&&r| r != '-').collect();
354 let sum: f64 = residues
355 .into_iter()
356 .map(|res| {
357 let p = *freqs.get(res).unwrap();
358 p * p.ln()
359 })
360 .sum();
361
362 -sum
363}
364
365fn percent_identity(s1: &str, s2: &str) -> f64 {
366 let num_identical = s1
367 .chars()
368 .zip(s2.chars())
369 .filter(|(c1, c2)| c1.eq_ignore_ascii_case(c2))
370 .count();
371 num_identical as f64 / s1.len() as f64
372}
373
374fn seq_len_nogaps(s: &str) -> f64 {
375 s.chars().filter(|c| c.is_alphabetic()).count() as f64 / s.len() as f64
376}
377
378fn seq_type(sequence: &str) -> SeqType {
379 let counts = sequence.to_lowercase().chars().counts();
380 let counts_u64: HashMap<char, u64> = counts.into_iter().map(|(k, v)| (k, v as u64)).collect();
381 let frequencies = to_freq_distrib(&counts_u64);
382 let nt_freq: f64 = *frequencies.get(&'a').unwrap_or(&0.0)
383 + *frequencies.get(&'c').unwrap_or(&0.0)
384 + *frequencies.get(&'g').unwrap_or(&0.0)
385 + *frequencies.get(&'t').unwrap_or(&0.0)
386 + *frequencies.get(&'u').unwrap_or(&0.0);
387 if nt_freq > 0.75 {
389 Nucleic
390 } else {
391 Protein
392 }
393}
394
395pub fn mark_lohi(metric: &[f64], threshold: f64) -> Vec<LoHiState> {
396 assert!(!threshold.is_nan(), "threshold must not be NaN");
397 metric
398 .iter()
399 .map(|&v| {
400 assert!(!v.is_nan(), "metric value must not be NaN");
401
402 if v < threshold {
403 LoHiState::Low
404 } else {
405 LoHiState::High
406 }
407 })
408 .collect()
409}
410
411pub fn find_hi_runs(lohi_states: &[LoHiState]) -> Vec<(usize, usize)> {
412 let mut runs = Vec::new();
413 let mut run_start: Option<usize> = None;
414
415 for (i, state) in lohi_states.iter().enumerate() {
416 match (run_start, state) {
417 (None, LoHiState::High) => {
419 run_start = Some(i);
420 }
421 (Some(start), LoHiState::Low) => {
423 runs.push((start, i - start));
424 run_start = None;
425 }
426 _ => {}
427 }
428 }
429
430 if let Some(start) = run_start {
432 runs.push((start, lohi_states.len() - start));
433 }
434
435 runs
436}
437
438pub fn merge_hi_runs(runs: &[(usize, usize)], threshold: usize) -> Vec<(usize, usize)> {
439 let mut merged_runs = Vec::new();
440
441 if runs.is_empty() {
442 return merged_runs;
443 }
444
445 merged_runs.push(runs[0]);
446
447 for &(cur_run_start, cur_run_len) in &runs[1..] {
448 let (prev_run_start, prev_run_len) = *merged_runs.last().unwrap();
449
450 let low_run_start = prev_run_start + prev_run_len;
451 let low_run_len = cur_run_start - low_run_start;
452
453 if low_run_len >= threshold {
454 merged_runs.push((cur_run_start, cur_run_len));
455 } else {
456 merged_runs.last_mut().unwrap().1 += low_run_len + cur_run_len;
457 }
458 }
459
460 merged_runs
461}
462
463#[cfg(test)]
464mod tests {
465 use crate::alignment::{
466 best_residue, consensus, densities, entropies, entropy, find_hi_runs, mark_lohi,
467 merge_hi_runs, percent_identity, res_count, seq_len_nogaps, seq_type, to_freq_distrib,
468 Alignment, BestResidue, LoHiState, RefSpec, ResidueCounts, ResidueDistribution, SeqType,
469 SeqType::{Nucleic, Protein},
470 };
471 use crate::seq::fasta::read_fasta_file;
472 use approx::assert_relative_eq;
473 use std::collections::HashMap;
474
475 #[test]
476 fn test_read_aln() {
477 let fasta1 = read_fasta_file("./data/test2.fas").unwrap();
478 let aln1 = Alignment::from_file(fasta1);
479 assert_eq!("seq1", aln1.headers[0]);
480 assert_eq!("seq2", aln1.headers[1]);
481 assert_eq!("seq3", aln1.headers[2]);
482 assert_eq!("TTGCCG-CGA", aln1.sequences[0]);
483 assert_eq!("TTCCCGGCGA", aln1.sequences[1]);
484 assert_eq!("TTACCG-CAA", aln1.sequences[2]);
485 }
486
487 #[test]
488 fn test_consensus() {
489 let fasta2 = read_fasta_file("data/test-cons.fas").unwrap();
490 let aln2 = Alignment::from_file(fasta2);
491 assert_eq!("AQw-n", consensus(&aln2.sequences, SeqType::Protein));
495 }
496
497 #[test]
498 fn test_res_count() {
499 let fasta2 = read_fasta_file("data/test-cons.fas").unwrap();
500 let aln2 = Alignment::from_file(fasta2);
501 let mut d0: ResidueCounts = HashMap::new();
502 d0.insert('A', 6);
503 assert_eq!(d0, res_count(&aln2.sequences, 0));
504
505 let mut d1: ResidueCounts = HashMap::new();
506 d1.insert('Q', 5);
507 d1.insert('T', 1);
508 assert_eq!(d1, res_count(&aln2.sequences, 1));
509
510 let mut d2: ResidueCounts = HashMap::new();
511 d2.insert('W', 2);
512 d2.insert('I', 1);
513 d2.insert('S', 1);
514 d2.insert('D', 1);
515 d2.insert('F', 1);
516 assert_eq!(d2, res_count(&aln2.sequences, 2));
517
518 let mut d3: ResidueCounts = HashMap::new();
519 d3.insert('-', 3);
520 d3.insert('K', 2);
521 d3.insert('L', 1);
522 assert_eq!(d3, res_count(&aln2.sequences, 3));
523 }
524
525 #[test]
526 fn test_most_frequent_residue() {
527 let d0: ResidueCounts = HashMap::from([('A', 6)]);
528 let mut exp: BestResidue = BestResidue {
529 residue: 'A',
530 frequency: 6,
531 };
532 assert_eq!(exp, best_residue(&d0, SeqType::Nucleic));
533
534 let d1: ResidueCounts = HashMap::from([('Q', 5), ('T', 1)]);
535 exp = BestResidue {
536 residue: 'Q',
537 frequency: 5,
538 };
539 assert_eq!(exp, best_residue(&d1, SeqType::Protein));
540
541 let d2: ResidueCounts = HashMap::from([('W', 2), ('I', 1), ('S', 1), ('D', 1), ('F', 1)]);
542 exp = BestResidue {
543 residue: 'W',
544 frequency: 2,
545 };
546 assert_eq!(exp, best_residue(&d2, SeqType::Protein));
547
548 let d4: ResidueCounts = HashMap::from([('-', 3), ('K', 2), ('L', 1)]);
551 exp = BestResidue {
552 residue: '-',
553 frequency: 3,
554 };
555 assert_eq!(exp, best_residue(&d4, SeqType::Protein));
556 }
557
558 #[test]
559 fn test_to_freq_distrib() {
560 let eps = 0.001;
561 let counts: ResidueCounts = HashMap::from([('K', 3), ('L', 3), ('G', 6), ('-', 6)]);
562 let rfreqs = to_freq_distrib(&counts);
563 assert_relative_eq!(0.25, *rfreqs.get(&'K').unwrap(), epsilon = eps);
564 assert_relative_eq!(0.25, *rfreqs.get(&'L').unwrap(), epsilon = eps);
565 assert_relative_eq!(0.5, *rfreqs.get(&'G').unwrap(), epsilon = eps);
566 }
567
568 #[test]
569 fn test_entropy_1() {
570 let eps = 0.00001;
571 let distrib: ResidueDistribution = ResidueDistribution::from([('A', 1.0)]);
572 assert_relative_eq!(0.0, entropy(&distrib), epsilon = eps);
573 }
574
575 #[test]
576 fn test_entropy_2() {
577 let eps = 0.00001;
578 let distrib: ResidueDistribution = ResidueDistribution::from([('A', 0.5), ('F', 0.5)]);
579 assert_relative_eq!(std::f64::consts::LN_2, entropy(&distrib), epsilon = eps);
584 }
585
586 #[test]
587 fn test_entropy_3() {
588 let eps = 0.00001;
589 let distrib: ResidueDistribution =
590 ResidueDistribution::from([('A', 0.5), ('F', 0.25), ('T', 0.25)]);
591 assert_relative_eq!(1.0397207708399179, entropy(&distrib), epsilon = eps);
592 }
593
594 #[test]
595 fn test_entropies() {
596 let fasta2 = read_fasta_file("data/test-cons.fas").unwrap();
597 let aln2 = Alignment::from_file(fasta2);
598 let entrs = entropies(&aln2.sequences);
599 let eps = 0.001;
600 assert_relative_eq!(0.0, entrs[0], epsilon = eps);
601 assert_relative_eq!(0.4505, entrs[1], epsilon = eps);
602 assert_relative_eq!(1.5607, entrs[2], epsilon = eps);
603 assert_relative_eq!(0.6365, entrs[3], epsilon = eps);
604 }
605
606 #[test]
607 fn test_density() {
608 let fasta = read_fasta_file("data/test-density.msa").unwrap();
609 let aln = Alignment::from_file(fasta);
610 let dens = densities(&aln.sequences);
611 assert_eq!(1.0, dens[0]);
612 assert_eq!(0.8, dens[1]);
613 assert_eq!(0.6, dens[2]);
614 assert_eq!(0.4, dens[3]);
615 assert_eq!(0.2, dens[4]);
616 assert_eq!(0.0, dens[5]);
617 }
618
619 #[test]
620 fn test_order_aln() {
621 let fasta = read_fasta_file("./data/test4.aln").unwrap();
622 let aln1 = Alignment::from_file(fasta);
623 assert_eq!("Zea_001", aln1.headers[0]);
625 assert_eq!("Rana_002", aln1.headers[1]);
626 assert_eq!("Panthera_050", aln1.headers[49]);
627 assert_eq!("tgctgttcgtcaaAgtaggcc", aln1.sequences[0]);
628 assert_eq!("tgctgttAgAcaaagtaggcc", aln1.sequences[1]);
629 assert_eq!("tgctgttcgtcaaagtaggcc", aln1.sequences[49]);
630 }
631
632 #[test]
633 fn test_similarity_00() {
634 let s1 = "GAATTC";
635 assert_eq!(percent_identity(s1, s1), 1.0);
636 }
637
638 #[test]
639 fn test_similarity_05() {
640 let s1 = "GAATTC";
641 let s2 = "GAA---";
642 assert_eq!(percent_identity(s1, s2), 0.5);
643 }
644
645 #[test]
646 fn test_similarity_10() {
647 let s1 = "GAATTC";
648 let s2 = "gaattc";
649 assert_eq!(percent_identity(s1, s2), 1.0);
650 }
651
652 #[test]
653 fn test_seq_len_nogaps_00() {
654 assert_eq!(seq_len_nogaps("atgc"), 1.0);
655 }
656
657 #[test]
658 fn test_seq_len_nogaps_05() {
659 assert_eq!(seq_len_nogaps("a-gc"), 0.75);
660 }
661
662 #[test]
663 fn test_seq_len_nogaps_10() {
664 assert_eq!(seq_len_nogaps("--.-"), 0.0);
665 }
666
667 #[test]
668 fn test_seq_type_00() {
669 assert_eq!(Nucleic, seq_type("GAATTC"));
670 }
671
672 #[test]
673 fn test_seq_type_05() {
674 assert_eq!(Protein, seq_type("HGTSDA"));
675 }
676
677 #[test]
678 fn test_seq_type_10() {
679 assert_eq!(Nucleic, seq_type("cgatgcacgatgcncagtgtuucgatcga"));
680 }
681
682 #[test]
683 fn test_seq_type_15() {
684 assert_eq!(Nucleic, seq_type("UUTGAU"));
685 }
686
687 #[test]
689 fn test_unequal_seq_len() {
690 let fasta = read_fasta_file("./data/test5.aln").unwrap();
691 let _ = Alignment::from_file(fasta);
692 }
693
694 #[test]
696 fn test_vec_ctor_00() {
697 let hdrs = vec![
698 String::from("Leo"),
699 String::from("Tigris"),
700 String::from("Pardus"),
701 String::from("Onca"),
702 ];
703 let seqs = vec![
704 String::from("catgcatatg"),
705 String::from("aatgcatatg"),
706 String::from("tatgcatatg"),
707 String::from("gatgcatatg"),
708 ];
709 let aln = Alignment::from_vecs(hdrs, seqs);
710 assert_eq!(4, aln.num_seq());
711 assert_eq!(10, aln.aln_len());
712 assert_eq!(SeqType::Nucleic, aln.macromolecule_type());
713 assert_eq!("Onca", aln.headers[3]);
714 assert_eq!("gatgcatatg", aln.sequences[3]);
715 }
716
717 #[test]
719 fn test_reference_specifier() {
720 let hdrs = vec![
721 String::from("frugilegus"),
722 String::from("monedula"),
723 String::from("corax"),
724 String::from("corone"),
725 String::from("cornix"),
726 ];
727 let seqs = vec![
728 String::from("catgcatatg"),
729 String::from("aatgcatatg"),
730 String::from("tatgcatatg"),
731 String::from("tatgcatatg"),
732 String::from("gatgcatatg"),
733 ];
734 let mut aln = Alignment::from_vecs(hdrs, seqs);
735 assert_eq!(RefSpec::Consensus, aln.get_ref_spec());
737 assert_eq!("tATGCATATG", aln.reference());
738 let _ = aln.set_ref_spec(RefSpec::Rank(0));
740 assert_eq!(RefSpec::Rank(0), aln.get_ref_spec());
741 assert_eq!("catgcatatg", aln.reference());
742 let _ = aln.set_ref_spec(RefSpec::Consensus);
744 assert_eq!(RefSpec::Consensus, aln.get_ref_spec());
745 assert_eq!("tATGCATATG", aln.reference());
746 }
747
748 #[test]
750 fn test_pct_id_wrt_ref() {
751 let hdrs = vec![
752 String::from("frugilegus"),
753 String::from("monedula"),
754 String::from("corax"),
755 String::from("corone"),
756 String::from("cornix"),
757 ];
758 let seqs = vec![
760 String::from("A---"),
761 String::from("AC--"),
762 String::from("ACG-"),
763 String::from("ACGT"),
764 String::from("ACGT"),
765 ];
766 let mut aln = Alignment::from_vecs(hdrs, seqs);
767 assert_eq!("ACg-", aln.reference());
769 assert_eq!(vec![0.5, 0.75, 1.0, 0.75, 0.75], aln.id_wrt_reference);
770 let _ = aln.set_ref_spec(RefSpec::Rank(0));
772 assert_eq!("A---", aln.reference());
773 assert_eq!(vec![1.0, 0.75, 0.5, 0.25, 0.25], aln.id_wrt_reference);
774 let _ = aln.set_ref_spec(RefSpec::Consensus);
776 assert_eq!("ACg-", aln.reference());
777 assert_eq!(vec![0.5, 0.75, 1.0, 0.75, 0.75], aln.id_wrt_reference);
778 }
779
780 #[test]
781 fn test_mark_lohi() {
782 let metric = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
783 assert_eq!(
784 mark_lohi(&metric, 0.8),
785 vec![
786 LoHiState::Low,
787 LoHiState::Low,
788 LoHiState::Low,
789 LoHiState::Low,
790 LoHiState::Low,
791 LoHiState::Low,
792 LoHiState::Low,
793 LoHiState::High,
794 LoHiState::High,
795 LoHiState::High,
796 ]
797 );
798 }
799
800 #[test]
801 fn test_find_hi_runs() {
802 let lohi_states = vec![
803 LoHiState::High,
804 LoHiState::Low,
805 LoHiState::Low,
806 LoHiState::Low,
807 LoHiState::High, LoHiState::High,
809 LoHiState::High,
810 LoHiState::High,
811 LoHiState::Low,
812 LoHiState::Low,
813 LoHiState::Low,
814 LoHiState::Low,
815 LoHiState::High, LoHiState::High,
817 LoHiState::High,
818 LoHiState::High,
819 LoHiState::High,
820 LoHiState::High,
821 LoHiState::Low,
822 LoHiState::Low,
823 LoHiState::Low,
824 LoHiState::High,
825 LoHiState::High,
826 ];
827 assert_eq!(
828 find_hi_runs(&lohi_states),
829 vec![(0, 1), (4, 4), (12, 6), (21, 2)]
830 );
831 }
832
833 #[test]
834 fn test_find_hi_runs_all_high() {
835 let lohi_states = vec![LoHiState::High, LoHiState::High, LoHiState::High];
836 assert_eq!(find_hi_runs(&lohi_states), vec![(0, 3)]);
837 }
838
839 #[test]
840 fn test_find_hi_runs_all_low() {
841 let lohi_states = vec![LoHiState::Low, LoHiState::Low, LoHiState::Low];
842 assert_eq!(find_hi_runs(&lohi_states), vec![]);
843 }
844
845 #[test]
846 fn test_merge_hi_runs_single_run() {
847 assert_eq!(merge_hi_runs(&[(5, 3)], 3), vec![(5, 3)]);
848 }
849
850 #[test]
851 fn test_merge_hi_runs_merges_short_gaps_only() {
852 let runs = vec![(0, 4), (6, 2), (11, 3), (16, 2), (22, 2)];
862
863 assert_eq!(merge_hi_runs(&runs, 3), vec![(0, 8), (11, 7), (22, 2)]);
864 }
865
866 #[test]
871 fn test_consensus_is_stable_across_calls() {
872 let seqs = vec![
875 "AGGCTC".to_string(),
876 "AGGCAC".to_string(),
877 "ACGTGC".to_string(),
878 ];
879 let consensus1 = consensus(&seqs, SeqType::Nucleic);
880 let consensus2 = consensus(&seqs, SeqType::Nucleic);
881 let consensus3 = consensus(&seqs, SeqType::Nucleic);
882 assert_eq!(
883 consensus1, consensus2,
884 "consensus changed between calls: '{}' vs '{}'",
885 consensus1, consensus2
886 );
887 assert_eq!(
888 consensus2, consensus3,
889 "consensus changed between calls: '{}' vs '{}'",
890 consensus2, consensus3
891 );
892 }
893
894 #[test]
895 fn test_consensus_uses_iupac_codes_for_tied_nucleotides() {
896 let seqs = vec![
900 "AGGCTC".to_string(),
901 "AGGCAC".to_string(),
902 "ACGTGC".to_string(),
903 ];
904 let cons = consensus(&seqs, SeqType::Nucleic);
905 assert!(
908 cons.chars().nth(4).unwrap().to_ascii_lowercase() == 'd',
909 "position 4 should resolve to IUPAC code 'd' for A/G/T tie, got '{}'",
910 cons.chars().nth(4).unwrap()
911 );
912 }
913
914 #[test]
915 fn test_consensus_tie_breakpoint_by_frequency() {
916 let seqs = vec![
919 "ACT".to_string(),
920 "ACT".to_string(),
921 "ATT".to_string(),
922 "ATT".to_string(),
923 ];
924 let cons = consensus(&seqs, SeqType::Nucleic);
928 let pos1 = cons.chars().nth(1).unwrap().to_ascii_lowercase();
930 assert_eq!(
931 pos1, 'y',
932 "position 1 should resolve to IUPAC code 'y' for C/T tie, got '{}'",
933 pos1
934 );
935 }
936}