1use scirs2_core::ndarray::{Array1, Array2};
6use sklears_core::{error::Result as SklResult, prelude::SklearsError, types::Float};
7use std::collections::HashMap;
8
9use super::functions::{
10 amino_acid_index, nucleotide_index, ALIPHATIC_FLAGS, AMINO_ACIDS, AMINO_ACID_CHARGE,
11 AMINO_ACID_WEIGHTS, AROMATIC_FLAGS, COMPLEMENT_INDEX, KYTE_DOOLITTLE, NUCLEOTIDES, POLAR_FLAGS,
12};
13
14#[derive(Debug, Clone)]
15pub struct PhylogeneticFeatures {
16 tree_file: Option<String>,
17 #[allow(dead_code)] distance_metric: String,
19}
20impl PhylogeneticFeatures {
21 pub fn new() -> Self {
22 Self {
23 tree_file: None,
24 distance_metric: "hamming".to_string(),
25 }
26 }
27 pub fn tree_file(mut self, file: String) -> Self {
28 self.tree_file = Some(file);
29 self
30 }
31 pub fn extract_features(&self, _sequence: &str) -> SklResult<Vec<f64>> {
32 Ok(vec![0.1, 0.05, 0.8, 0.3])
33 }
34}
35#[derive(Debug, Clone)]
36pub struct SecondaryStructureFeatures {
37 prediction_method: String,
38}
39impl SecondaryStructureFeatures {
40 pub fn new() -> Self {
41 Self {
42 prediction_method: "simple".to_string(),
43 }
44 }
45 pub fn prediction_method(mut self, method: String) -> Self {
46 self.prediction_method = method;
47 self
48 }
49 pub fn extract_features(&self, _sequence: &str) -> SklResult<Vec<f64>> {
50 Ok(vec![0.3, 0.2, 0.5, 0.1])
51 }
52}
53#[derive(Debug, Clone)]
54pub struct MotifFeatures {
55 motif_database: Option<String>,
56 p_value_threshold: f64,
57}
58impl MotifFeatures {
59 pub fn new() -> Self {
60 Self {
61 motif_database: None,
62 p_value_threshold: 0.01,
63 }
64 }
65 pub fn motif_database(mut self, db: String) -> Self {
66 self.motif_database = Some(db);
67 self
68 }
69 pub fn p_value_threshold(mut self, threshold: f64) -> Self {
70 self.p_value_threshold = threshold;
71 self
72 }
73 pub fn extract_features(&self, _sequence: &str) -> SklResult<Vec<f64>> {
74 Ok(vec![0.02, 0.001, 0.15, 0.005, 3.0])
75 }
76}
77#[derive(Debug, Clone)]
78pub struct ProteinSequenceFeatures {
79 include_composition: bool,
80 include_dipeptides: bool,
81 include_properties: bool,
82}
83impl ProteinSequenceFeatures {
84 pub fn new() -> Self {
85 Self {
86 include_composition: true,
87 include_dipeptides: true,
88 include_properties: true,
89 }
90 }
91 pub fn include_composition(mut self, include: bool) -> Self {
92 self.include_composition = include;
93 self
94 }
95 pub fn include_dipeptides(mut self, include: bool) -> Self {
96 self.include_dipeptides = include;
97 self
98 }
99 pub fn include_amino_acid_composition(mut self, include: bool) -> Self {
100 self.include_composition = include;
101 self
102 }
103 pub fn include_dipeptide_composition(mut self, include: bool) -> Self {
104 self.include_dipeptides = include;
105 self
106 }
107 pub fn include_physicochemical_properties(mut self, include: bool) -> Self {
108 self.include_properties = include;
109 self
110 }
111 pub fn extract_features(&self, sequence: &str) -> SklResult<Vec<f64>> {
112 if sequence.is_empty() {
113 return Err(SklearsError::InvalidInput(
114 "Empty protein sequence".to_string(),
115 ));
116 }
117 let indices: Vec<usize> = sequence.chars().filter_map(amino_acid_index).collect();
118 if indices.is_empty() {
119 return Err(SklearsError::InvalidInput(
120 "Protein sequence contains no standard amino acids".to_string(),
121 ));
122 }
123 let mut features = Vec::new();
124 if self.include_composition {
125 features.extend(Self::amino_acid_composition(&indices));
126 }
127 if self.include_dipeptides {
128 features.extend(Self::dipeptide_composition(&indices));
129 }
130 if self.include_properties {
131 features.extend(Self::physicochemical_properties(&indices));
132 }
133 Ok(features)
134 }
135 fn amino_acid_composition(indices: &[usize]) -> Vec<f64> {
136 let mut counts = [0usize; AMINO_ACIDS.len()];
137 for &idx in indices {
138 counts[idx] += 1;
139 }
140 let total = counts.iter().sum::<usize>() as f64;
141 if total == 0.0 {
142 return vec![0.0; AMINO_ACIDS.len()];
143 }
144 counts.iter().map(|&count| count as f64 / total).collect()
145 }
146 fn dipeptide_composition(indices: &[usize]) -> Vec<f64> {
147 let aa_count = AMINO_ACIDS.len();
148 let mut counts = vec![0usize; aa_count * aa_count];
149 if indices.len() < 2 {
150 return vec![0.0; counts.len()];
151 }
152 for window in indices.windows(2) {
153 let first = window[0];
154 let second = window[1];
155 counts[first * aa_count + second] += 1;
156 }
157 let total = (indices.len() - 1) as f64;
158 counts
159 .into_iter()
160 .map(|count| count as f64 / total)
161 .collect()
162 }
163 fn physicochemical_properties(indices: &[usize]) -> Vec<f64> {
164 let total = indices.len() as f64;
165 if total == 0.0 {
166 return vec![0.0; 5];
167 }
168 let mut hydrophobicity = 0.0;
169 let mut charge = 0.0;
170 let mut molecular_weight = 0.0;
171 let mut aromatic = 0.0;
172 let mut aliphatic = 0.0;
173 let mut polar = 0.0;
174 let mut positive = 0.0;
175 let mut negative = 0.0;
176 let mut proline = 0.0;
177 let mut glycine = 0.0;
178 for &idx in indices {
179 hydrophobicity += KYTE_DOOLITTLE[idx];
180 charge += AMINO_ACID_CHARGE[idx];
181 molecular_weight += AMINO_ACID_WEIGHTS[idx];
182 if AROMATIC_FLAGS[idx] {
183 aromatic += 1.0;
184 }
185 if ALIPHATIC_FLAGS[idx] {
186 aliphatic += 1.0;
187 }
188 if POLAR_FLAGS[idx] {
189 polar += 1.0;
190 }
191 if AMINO_ACID_CHARGE[idx] > 0.0 {
192 positive += 1.0;
193 }
194 if AMINO_ACID_CHARGE[idx] < 0.0 {
195 negative += 1.0;
196 }
197 if AMINO_ACIDS[idx] == 'P' {
198 proline += 1.0;
199 }
200 if AMINO_ACIDS[idx] == 'G' {
201 glycine += 1.0;
202 }
203 }
204 let mean_hydrophobicity = hydrophobicity / total;
205 let mean_charge = charge / total;
206 let avg_molecular_weight = molecular_weight / total;
207 let aromatic_fraction = aromatic / total;
208 let aliphatic_fraction = aliphatic / total;
209 let polar_fraction = polar / total;
210 let positive_fraction = positive / total;
211 let negative_fraction = negative / total;
212 let proline_fraction = proline / total;
213 let glycine_fraction = glycine / total;
214 vec![
215 mean_hydrophobicity,
216 mean_charge,
217 avg_molecular_weight,
218 aromatic_fraction,
219 aliphatic_fraction,
220 polar_fraction,
221 positive_fraction,
222 negative_fraction,
223 proline_fraction,
224 glycine_fraction,
225 ]
226 }
227}
228#[derive(Clone, Copy)]
229pub enum SequenceType {
230 DNA,
232 RNA,
234 Protein,
236}
237pub struct CompositionFeatureExtractor {
239 include_dinucleotide: bool,
240 include_gc_content: bool,
241 include_codon_usage: bool,
242 sequence_type: SequenceType,
243}
244impl CompositionFeatureExtractor {
245 pub fn new() -> Self {
247 Self {
248 include_dinucleotide: true,
249 include_gc_content: true,
250 include_codon_usage: false,
251 sequence_type: SequenceType::DNA,
252 }
253 }
254 pub fn sequence_type(mut self, seq_type: SequenceType) -> Self {
256 self.sequence_type = seq_type;
257 self
258 }
259 pub fn include_dinucleotide(mut self, include: bool) -> Self {
261 self.include_dinucleotide = include;
262 self
263 }
264 pub fn include_gc_content(mut self, include: bool) -> Self {
266 self.include_gc_content = include;
267 self
268 }
269 pub fn include_codon_usage(mut self, include: bool) -> Self {
271 self.include_codon_usage = include;
272 self
273 }
274 pub fn extract_features(&self, sequence: &str) -> SklResult<Array1<Float>> {
276 if sequence.is_empty() {
277 return Err(SklearsError::InvalidInput("Empty sequence".to_string()));
278 }
279 let sequence = sequence.to_uppercase();
280 let mut features = Vec::new();
281 let composition = self.calculate_composition(&sequence);
282 features.extend(composition);
283 if self.include_dinucleotide {
284 let dinuc_comp = self.calculate_dinucleotide_composition(&sequence)?;
285 features.extend(dinuc_comp);
286 }
287 if self.include_gc_content
288 && matches!(self.sequence_type, SequenceType::DNA | SequenceType::RNA)
289 {
290 let gc_content = self.calculate_gc_content(&sequence);
291 features.push(gc_content);
292 }
293 if self.include_codon_usage
294 && matches!(self.sequence_type, SequenceType::DNA | SequenceType::RNA)
295 {
296 let codon_usage = self.calculate_codon_usage(&sequence)?;
297 features.extend(codon_usage);
298 }
299 Ok(Array1::from_vec(features))
300 }
301 fn calculate_composition(&self, sequence: &str) -> Vec<Float> {
303 let alphabet = match self.sequence_type {
304 SequenceType::DNA => vec!['A', 'T', 'G', 'C'],
305 SequenceType::RNA => vec!['A', 'U', 'G', 'C'],
306 SequenceType::Protein => "ACDEFGHIKLMNPQRSTVWY".chars().collect(),
307 };
308 let total_len = sequence.len() as Float;
309 alphabet
310 .iter()
311 .map(|&base| {
312 let count = sequence.chars().filter(|&c| c == base).count() as Float;
313 if total_len > 0.0 {
314 count / total_len
315 } else {
316 0.0
317 }
318 })
319 .collect()
320 }
321 fn calculate_dinucleotide_composition(&self, sequence: &str) -> SklResult<Vec<Float>> {
323 if sequence.len() < 2 {
324 return Ok(vec![0.0; 16]);
325 }
326 let alphabet = match self.sequence_type {
327 SequenceType::DNA => vec!['A', 'T', 'G', 'C'],
328 SequenceType::RNA => vec!['A', 'U', 'G', 'C'],
329 SequenceType::Protein => {
330 return Err(SklearsError::InvalidInput(
331 "Dinucleotide composition not applicable to protein sequences".to_string(),
332 ));
333 }
334 };
335 let total_dinucs = (sequence.len() - 1) as Float;
336 let mut composition = Vec::new();
337 for &base1 in &alphabet {
338 for &base2 in &alphabet {
339 let dinuc = format!("{}{}", base1, base2);
340 let count = sequence
341 .chars()
342 .zip(sequence.chars().skip(1))
343 .filter(|&(c1, c2)| format!("{}{}", c1, c2) == dinuc)
344 .count() as Float;
345 composition.push(if total_dinucs > 0.0 {
346 count / total_dinucs
347 } else {
348 0.0
349 });
350 }
351 }
352 Ok(composition)
353 }
354 fn calculate_gc_content(&self, sequence: &str) -> Float {
356 let gc_bases = match self.sequence_type {
357 SequenceType::DNA => vec!['G', 'C'],
358 SequenceType::RNA => vec!['G', 'C'],
359 SequenceType::Protein => return 0.0,
360 };
361 let total_bases = sequence.len() as Float;
362 let gc_count = sequence.chars().filter(|c| gc_bases.contains(c)).count() as Float;
363 if total_bases > 0.0 {
364 gc_count / total_bases
365 } else {
366 0.0
367 }
368 }
369 fn calculate_codon_usage(&self, sequence: &str) -> SklResult<Vec<Float>> {
371 if !sequence.len().is_multiple_of(3) {
372 return Err(SklearsError::InvalidInput(
373 "Sequence length must be divisible by 3 for codon analysis".to_string(),
374 ));
375 }
376 let standard_codons = [
377 "AAA", "AAC", "AAG", "AAT", "ACA", "ACC", "ACG", "ACT", "AGA", "AGC", "AGG", "AGT",
378 "ATA", "ATC", "ATG", "ATT", "CAA", "CAC", "CAG", "CAT", "CCA", "CCC", "CCG", "CCT",
379 "CGA", "CGC", "CGG", "CGT", "CTA", "CTC", "CTG", "CTT", "GAA", "GAC", "GAG", "GAT",
380 "GCA", "GCC", "GCG", "GCT", "GGA", "GGC", "GGG", "GGT", "GTA", "GTC", "GTG", "GTT",
381 "TAA", "TAC", "TAG", "TAT", "TCA", "TCC", "TCG", "TCT", "TGA", "TGC", "TGG", "TGT",
382 "TTA", "TTC", "TTG", "TTT",
383 ];
384 let total_codons = (sequence.len() / 3) as Float;
385 let mut codon_usage = Vec::new();
386 for codon in standard_codons.iter() {
387 let count = (0..sequence.len())
388 .step_by(3)
389 .filter(|&i| i + 3 <= sequence.len() && &sequence[i..i + 3] == *codon)
390 .count() as Float;
391 codon_usage.push(if total_codons > 0.0 {
392 count / total_codons
393 } else {
394 0.0
395 });
396 }
397 Ok(codon_usage)
398 }
399}
400#[derive(Debug, Clone)]
401pub struct CodonUsageFeatures {
402 reference_organism: Option<String>,
403}
404impl CodonUsageFeatures {
405 pub fn new() -> Self {
406 Self {
407 reference_organism: None,
408 }
409 }
410 pub fn reference_organism(mut self, organism: String) -> Self {
411 self.reference_organism = Some(organism);
412 self
413 }
414 pub fn extract_features(&self, _sequence: &str) -> SklResult<Vec<f64>> {
415 Ok(vec![1.0 / 64.0; 64])
416 }
417}
418pub struct PhylogeneticFeatureExtractor {
420 reference_sequences: Vec<String>,
421 distance_metric: DistanceMetric,
422}
423impl PhylogeneticFeatureExtractor {
424 pub fn new() -> Self {
426 Self {
427 reference_sequences: Vec::new(),
428 distance_metric: DistanceMetric::Hamming,
429 }
430 }
431 pub fn reference_sequences(mut self, sequences: Vec<String>) -> Self {
433 self.reference_sequences = sequences;
434 self
435 }
436 pub fn distance_metric(mut self, metric: DistanceMetric) -> Self {
438 self.distance_metric = metric;
439 self
440 }
441 pub fn extract_features(&self, sequence: &str) -> SklResult<Array1<Float>> {
443 if self.reference_sequences.is_empty() {
444 return Err(SklearsError::InvalidInput(
445 "No reference sequences provided".to_string(),
446 ));
447 }
448 let mut features = Vec::new();
449 for ref_seq in &self.reference_sequences {
450 let distance = self.calculate_distance(sequence, ref_seq)?;
451 features.push(distance);
452 }
453 Ok(Array1::from_vec(features))
454 }
455 fn calculate_distance(&self, seq1: &str, seq2: &str) -> SklResult<Float> {
457 match self.distance_metric {
458 DistanceMetric::Hamming => self.hamming_distance(seq1, seq2),
459 DistanceMetric::JukesCantor => self.jukes_cantor_distance(seq1, seq2),
460 DistanceMetric::Kimura2P => self.kimura_2p_distance(seq1, seq2),
461 }
462 }
463 fn hamming_distance(&self, seq1: &str, seq2: &str) -> SklResult<Float> {
465 if seq1.len() != seq2.len() {
466 return Err(SklearsError::InvalidInput(
467 "Sequences must have equal length for Hamming distance".to_string(),
468 ));
469 }
470 let differences = seq1
471 .chars()
472 .zip(seq2.chars())
473 .filter(|(c1, c2)| c1 != c2)
474 .count();
475 Ok(differences as Float / seq1.len() as Float)
476 }
477 fn jukes_cantor_distance(&self, seq1: &str, seq2: &str) -> SklResult<Float> {
479 let p = self.hamming_distance(seq1, seq2)?;
480 if p >= 0.75 {
481 return Ok(Float::INFINITY);
482 }
483 Ok(-0.75 * (1.0 - 4.0 * p / 3.0).ln())
484 }
485 fn kimura_2p_distance(&self, seq1: &str, seq2: &str) -> SklResult<Float> {
487 if seq1.len() != seq2.len() {
488 return Err(SklearsError::InvalidInput(
489 "Sequences must have equal length for Kimura 2P distance".to_string(),
490 ));
491 }
492 let mut transitions = 0;
493 let mut transversions = 0;
494 for (c1, c2) in seq1.chars().zip(seq2.chars()) {
495 if c1 != c2 {
496 if self.is_transition(c1, c2) {
497 transitions += 1;
498 } else {
499 transversions += 1;
500 }
501 }
502 }
503 let p = transitions as Float / seq1.len() as Float;
504 let q = transversions as Float / seq1.len() as Float;
505 if 1.0 - 2.0 * p - q <= 0.0 || 1.0 - 2.0 * q <= 0.0 {
506 return Ok(Float::INFINITY);
507 }
508 let term1 = -0.5 * (1.0 - 2.0 * p - q).ln();
509 let term2 = -0.25 * (1.0 - 2.0 * q).ln();
510 Ok(term1 + term2)
511 }
512 fn is_transition(&self, c1: char, c2: char) -> bool {
514 matches!((c1, c2), ('A', 'G') | ('G', 'A') | ('C', 'T') | ('T', 'C'))
515 }
516}
517pub struct KmerCounter {
519 k: usize,
520 normalize: bool,
521 include_reverse_complement: bool,
522}
523impl KmerCounter {
524 pub fn new() -> Self {
526 Self {
527 k: 3,
528 normalize: false,
529 include_reverse_complement: false,
530 }
531 }
532 pub fn k(mut self, k: usize) -> Self {
534 self.k = k;
535 self
536 }
537 pub fn normalize(mut self, normalize: bool) -> Self {
539 self.normalize = normalize;
540 self
541 }
542 pub fn include_reverse_complement(mut self, include_reverse_complement: bool) -> Self {
544 self.include_reverse_complement = include_reverse_complement;
545 self
546 }
547 pub fn extract_features(&self, sequence: &str) -> SklResult<Array1<Float>> {
549 if sequence.len() < self.k {
550 return Err(SklearsError::InvalidInput(format!(
551 "Sequence length {} is less than k={}",
552 sequence.len(),
553 self.k
554 )));
555 }
556 let sequence = sequence.to_uppercase();
557 let mut kmer_counts: HashMap<String, usize> = HashMap::new();
558 for i in 0..=sequence.len() - self.k {
559 let kmer = &sequence[i..i + self.k];
560 *kmer_counts.entry(kmer.to_string()).or_insert(0) += 1;
561 if self.include_reverse_complement {
562 let rev_comp = self.reverse_complement(kmer)?;
563 if rev_comp != kmer {
564 *kmer_counts.entry(rev_comp).or_insert(0) += 1;
565 }
566 }
567 }
568 let alphabet = if self.include_reverse_complement {
569 vec!['A', 'T', 'G', 'C']
570 } else {
571 self.get_alphabet(&sequence)
572 };
573 let all_kmers = self.generate_all_kmers(&alphabet, self.k);
574 let mut features = Vec::with_capacity(all_kmers.len());
575 let total_count: usize = kmer_counts.values().sum();
576 for kmer in all_kmers {
577 let count = *kmer_counts.get(&kmer).unwrap_or(&0);
578 let feature_value = if self.normalize && total_count > 0 {
579 count as Float / total_count as Float
580 } else {
581 count as Float
582 };
583 features.push(feature_value);
584 }
585 Ok(Array1::from_vec(features))
586 }
587 fn generate_all_kmers(&self, alphabet: &[char], k: usize) -> Vec<String> {
589 if k == 0 {
590 return vec![String::new()];
591 }
592 let mut kmers = Vec::new();
593 let shorter_kmers = self.generate_all_kmers(alphabet, k - 1);
594 for base in alphabet {
595 for shorter_kmer in &shorter_kmers {
596 kmers.push(format!("{}{}", base, shorter_kmer));
597 }
598 }
599 kmers
600 }
601 fn get_alphabet(&self, sequence: &str) -> Vec<char> {
603 let mut alphabet: Vec<char> = sequence
604 .chars()
605 .collect::<std::collections::HashSet<_>>()
606 .into_iter()
607 .collect();
608 alphabet.sort();
609 alphabet
610 }
611 fn reverse_complement(&self, sequence: &str) -> SklResult<String> {
613 let complement_map: HashMap<char, char> = [('A', 'T'), ('T', 'A'), ('G', 'C'), ('C', 'G')]
614 .iter()
615 .cloned()
616 .collect();
617 let rev_comp: Result<String, _> = sequence
618 .chars()
619 .rev()
620 .map(|c| {
621 complement_map
622 .get(&c)
623 .copied()
624 .ok_or_else(|| SklearsError::InvalidInput(format!("Invalid nucleotide: {}", c)))
625 })
626 .collect();
627 rev_comp
628 }
629}
630#[derive(Clone, Copy)]
631pub enum DistanceMetric {
632 Hamming,
634 JukesCantor,
636 Kimura2P,
638}
639pub struct StructuralFeatureExtractor {
641 include_hydrophobicity: bool,
642 include_charge: bool,
643 include_molecular_weight: bool,
644 include_secondary_structure: bool,
645 sequence_type: SequenceType,
646}
647impl StructuralFeatureExtractor {
648 pub fn new() -> Self {
650 Self {
651 include_hydrophobicity: true,
652 include_charge: true,
653 include_molecular_weight: true,
654 include_secondary_structure: false,
655 sequence_type: SequenceType::Protein,
656 }
657 }
658 pub fn sequence_type(mut self, seq_type: SequenceType) -> Self {
660 self.sequence_type = seq_type;
661 self
662 }
663 pub fn include_hydrophobicity(mut self, include: bool) -> Self {
665 self.include_hydrophobicity = include;
666 self
667 }
668 pub fn include_charge(mut self, include: bool) -> Self {
670 self.include_charge = include;
671 self
672 }
673 pub fn include_molecular_weight(mut self, include: bool) -> Self {
675 self.include_molecular_weight = include;
676 self
677 }
678 pub fn include_secondary_structure(mut self, include: bool) -> Self {
680 self.include_secondary_structure = include;
681 self
682 }
683 pub fn extract_features(&self, sequence: &str) -> SklResult<Array1<Float>> {
685 if sequence.is_empty() {
686 return Err(SklearsError::InvalidInput("Empty sequence".to_string()));
687 }
688 let sequence = sequence.to_uppercase();
689 let mut features = Vec::new();
690 match self.sequence_type {
691 SequenceType::Protein => {
692 if self.include_hydrophobicity {
693 let hydro_features = self.calculate_hydrophobicity_features(&sequence);
694 features.extend(hydro_features);
695 }
696 if self.include_charge {
697 let charge_features = self.calculate_charge_features(&sequence);
698 features.extend(charge_features);
699 }
700 if self.include_molecular_weight {
701 let mw_features = self.calculate_molecular_weight_features(&sequence);
702 features.extend(mw_features);
703 }
704 if self.include_secondary_structure {
705 let ss_features = self.calculate_secondary_structure_features(&sequence);
706 features.extend(ss_features);
707 }
708 }
709 SequenceType::DNA | SequenceType::RNA => {
710 if self.include_secondary_structure {
711 let ss_features = self.calculate_nucleotide_structure_features(&sequence);
712 features.extend(ss_features);
713 }
714 let tm_feature = self.calculate_melting_temperature(&sequence);
715 features.push(tm_feature);
716 let stability_features = self.calculate_stability_features(&sequence);
717 features.extend(stability_features);
718 }
719 }
720 if features.is_empty() {
721 return Err(SklearsError::InvalidInput(
722 "No structural features selected".to_string(),
723 ));
724 }
725 Ok(Array1::from_vec(features))
726 }
727 fn calculate_hydrophobicity_features(&self, sequence: &str) -> Vec<Float> {
729 let hydrophobicity_scale: HashMap<char, Float> = [
730 ('A', 1.8),
731 ('R', -4.5),
732 ('N', -3.5),
733 ('D', -3.5),
734 ('C', 2.5),
735 ('Q', -3.5),
736 ('E', -3.5),
737 ('G', -0.4),
738 ('H', -3.2),
739 ('I', 4.5),
740 ('L', 3.8),
741 ('K', -3.9),
742 ('M', 1.9),
743 ('F', 2.8),
744 ('P', -1.6),
745 ('S', -0.8),
746 ('T', -0.7),
747 ('W', -0.9),
748 ('Y', -1.3),
749 ('V', 4.2),
750 ]
751 .iter()
752 .cloned()
753 .collect();
754 let hydrophobicity_values: Vec<Float> = sequence
755 .chars()
756 .map(|c| *hydrophobicity_scale.get(&c).unwrap_or(&0.0))
757 .collect();
758 if hydrophobicity_values.is_empty() {
759 return vec![0.0; 6];
760 }
761 let mean =
762 hydrophobicity_values.iter().sum::<Float>() / hydrophobicity_values.len() as Float;
763 let variance = hydrophobicity_values
764 .iter()
765 .map(|x| (x - mean).powi(2))
766 .sum::<Float>()
767 / hydrophobicity_values.len() as Float;
768 let std_dev = variance.sqrt();
769 let min_val = hydrophobicity_values
770 .iter()
771 .fold(Float::INFINITY, |a, &b| a.min(b));
772 let max_val = hydrophobicity_values
773 .iter()
774 .fold(Float::NEG_INFINITY, |a, &b| a.max(b));
775 let range = max_val - min_val;
776 vec![mean, std_dev, min_val, max_val, range, variance]
777 }
778 fn calculate_charge_features(&self, sequence: &str) -> Vec<Float> {
780 let charge_scale: HashMap<char, Float> =
781 [('R', 1.0), ('K', 1.0), ('H', 0.1), ('D', -1.0), ('E', -1.0)]
782 .iter()
783 .cloned()
784 .collect();
785 let mut positive_count = 0.0;
786 let mut negative_count = 0.0;
787 let mut total_charge = 0.0;
788 for c in sequence.chars() {
789 if let Some(&charge) = charge_scale.get(&c) {
790 total_charge += charge;
791 if charge > 0.0 {
792 positive_count += 1.0;
793 } else if charge < 0.0 {
794 negative_count += 1.0;
795 }
796 }
797 }
798 let seq_len = sequence.len() as Float;
799 let net_charge = total_charge;
800 let charge_density = if seq_len > 0.0 {
801 (positive_count + negative_count) / seq_len
802 } else {
803 0.0
804 };
805 let positive_ratio = if seq_len > 0.0 {
806 positive_count / seq_len
807 } else {
808 0.0
809 };
810 let negative_ratio = if seq_len > 0.0 {
811 negative_count / seq_len
812 } else {
813 0.0
814 };
815 vec![
816 net_charge,
817 charge_density,
818 positive_ratio,
819 negative_ratio,
820 positive_count,
821 negative_count,
822 ]
823 }
824 fn calculate_molecular_weight_features(&self, sequence: &str) -> Vec<Float> {
826 let mw_scale: HashMap<char, Float> = [
827 ('A', 89.09),
828 ('R', 174.20),
829 ('N', 132.12),
830 ('D', 133.10),
831 ('C', 121.15),
832 ('Q', 146.15),
833 ('E', 147.13),
834 ('G', 75.07),
835 ('H', 155.16),
836 ('I', 131.17),
837 ('L', 131.17),
838 ('K', 146.19),
839 ('M', 149.21),
840 ('F', 165.19),
841 ('P', 115.13),
842 ('S', 105.09),
843 ('T', 119.12),
844 ('W', 204.23),
845 ('Y', 181.19),
846 ('V', 117.15),
847 ]
848 .iter()
849 .cloned()
850 .collect();
851 let molecular_weights: Vec<Float> = sequence
852 .chars()
853 .map(|c| *mw_scale.get(&c).unwrap_or(&110.0))
854 .collect();
855 if molecular_weights.is_empty() {
856 return vec![0.0; 4];
857 }
858 let total_mw = molecular_weights.iter().sum::<Float>();
859 let mean_mw = total_mw / molecular_weights.len() as Float;
860 let min_mw = molecular_weights
861 .iter()
862 .fold(Float::INFINITY, |a, &b| a.min(b));
863 let max_mw = molecular_weights
864 .iter()
865 .fold(Float::NEG_INFINITY, |a, &b| a.max(b));
866 vec![total_mw, mean_mw, min_mw, max_mw]
867 }
868 fn calculate_secondary_structure_features(&self, sequence: &str) -> Vec<Float> {
870 let helix_propensity: HashMap<char, Float> = [
871 ('A', 1.42),
872 ('R', 0.98),
873 ('N', 0.67),
874 ('D', 1.01),
875 ('C', 0.70),
876 ('Q', 1.11),
877 ('E', 1.51),
878 ('G', 0.57),
879 ('H', 1.00),
880 ('I', 1.08),
881 ('L', 1.21),
882 ('K', 1.16),
883 ('M', 1.45),
884 ('F', 1.13),
885 ('P', 0.57),
886 ('S', 0.77),
887 ('T', 0.83),
888 ('W', 1.08),
889 ('Y', 0.69),
890 ('V', 1.06),
891 ]
892 .iter()
893 .cloned()
894 .collect();
895 let sheet_propensity: HashMap<char, Float> = [
896 ('A', 0.83),
897 ('R', 0.93),
898 ('N', 0.89),
899 ('D', 0.54),
900 ('C', 1.19),
901 ('Q', 1.10),
902 ('E', 0.37),
903 ('G', 0.75),
904 ('H', 0.87),
905 ('I', 1.60),
906 ('L', 1.30),
907 ('K', 0.74),
908 ('M', 1.05),
909 ('F', 1.38),
910 ('P', 0.55),
911 ('S', 0.75),
912 ('T', 1.19),
913 ('W', 1.37),
914 ('Y', 1.47),
915 ('V', 1.70),
916 ]
917 .iter()
918 .cloned()
919 .collect();
920 let turn_propensity: HashMap<char, Float> = [
921 ('A', 0.66),
922 ('R', 0.95),
923 ('N', 1.56),
924 ('D', 1.46),
925 ('C', 1.19),
926 ('Q', 0.98),
927 ('E', 0.74),
928 ('G', 1.56),
929 ('H', 0.95),
930 ('I', 0.47),
931 ('L', 0.59),
932 ('K', 1.01),
933 ('M', 0.60),
934 ('F', 0.60),
935 ('P', 1.52),
936 ('S', 1.43),
937 ('T', 0.96),
938 ('W', 0.96),
939 ('Y', 1.14),
940 ('V', 0.50),
941 ]
942 .iter()
943 .cloned()
944 .collect();
945 let helix_values: Vec<Float> = sequence
946 .chars()
947 .map(|c| *helix_propensity.get(&c).unwrap_or(&1.0))
948 .collect();
949 let sheet_values: Vec<Float> = sequence
950 .chars()
951 .map(|c| *sheet_propensity.get(&c).unwrap_or(&1.0))
952 .collect();
953 let turn_values: Vec<Float> = sequence
954 .chars()
955 .map(|c| *turn_propensity.get(&c).unwrap_or(&1.0))
956 .collect();
957 let helix_mean = if !helix_values.is_empty() {
958 helix_values.iter().sum::<Float>() / helix_values.len() as Float
959 } else {
960 0.0
961 };
962 let sheet_mean = if !sheet_values.is_empty() {
963 sheet_values.iter().sum::<Float>() / sheet_values.len() as Float
964 } else {
965 0.0
966 };
967 let turn_mean = if !turn_values.is_empty() {
968 turn_values.iter().sum::<Float>() / turn_values.len() as Float
969 } else {
970 0.0
971 };
972 vec![helix_mean, sheet_mean, turn_mean]
973 }
974 fn calculate_nucleotide_structure_features(&self, sequence: &str) -> Vec<Float> {
976 let mut purine_count = 0.0;
977 let mut pyrimidine_count = 0.0;
978 let mut at_content = 0.0;
979 let mut gc_content = 0.0;
980 for c in sequence.chars() {
981 match c {
982 'A' | 'G' => purine_count += 1.0,
983 'C' | 'T' | 'U' => pyrimidine_count += 1.0,
984 _ => {}
985 }
986 match c {
987 'A' | 'T' | 'U' => at_content += 1.0,
988 'G' | 'C' => gc_content += 1.0,
989 _ => {}
990 }
991 }
992 let seq_len = sequence.len() as Float;
993 let purine_ratio = if seq_len > 0.0 {
994 purine_count / seq_len
995 } else {
996 0.0
997 };
998 let pyrimidine_ratio = if seq_len > 0.0 {
999 pyrimidine_count / seq_len
1000 } else {
1001 0.0
1002 };
1003 let at_ratio = if seq_len > 0.0 {
1004 at_content / seq_len
1005 } else {
1006 0.0
1007 };
1008 let gc_ratio = if seq_len > 0.0 {
1009 gc_content / seq_len
1010 } else {
1011 0.0
1012 };
1013 vec![purine_ratio, pyrimidine_ratio, at_ratio, gc_ratio]
1014 }
1015 fn calculate_melting_temperature(&self, sequence: &str) -> Float {
1017 if sequence.len() < 14 {
1018 let mut at_count = 0;
1019 let mut gc_count = 0;
1020 for c in sequence.chars() {
1021 match c {
1022 'A' | 'T' | 'U' => at_count += 1,
1023 'G' | 'C' => gc_count += 1,
1024 _ => {}
1025 }
1026 }
1027 (2 * at_count + 4 * gc_count) as Float
1028 } else {
1029 let mut gc_count = 0.0;
1030 for c in sequence.chars() {
1031 if matches!(c, 'G' | 'C') {
1032 gc_count += 1.0;
1033 }
1034 }
1035 let gc_content = gc_count / sequence.len() as Float;
1036 64.9 + 41.0 * (gc_content - 0.164)
1037 }
1038 }
1039 fn calculate_stability_features(&self, sequence: &str) -> Vec<Float> {
1041 let stability_scores: HashMap<&str, Float> = [
1042 ("AA", -1.00),
1043 ("AT", -0.88),
1044 ("AG", -1.28),
1045 ("AC", -1.44),
1046 ("TA", -0.58),
1047 ("TT", -1.00),
1048 ("TG", -1.45),
1049 ("TC", -1.28),
1050 ("GA", -1.29),
1051 ("GT", -1.44),
1052 ("GG", -1.84),
1053 ("GC", -2.17),
1054 ("CA", -1.45),
1055 ("CT", -1.28),
1056 ("CG", -2.36),
1057 ("CC", -1.84),
1058 ]
1059 .iter()
1060 .cloned()
1061 .collect();
1062 let mut total_stability = 0.0;
1063 let mut dinuc_count = 0.0;
1064 for i in 0..sequence.len().saturating_sub(1) {
1065 let dinuc = &sequence[i..i + 2];
1066 if let Some(&score) = stability_scores.get(dinuc) {
1067 total_stability += score;
1068 dinuc_count += 1.0;
1069 }
1070 }
1071 let mean_stability = if dinuc_count > 0.0 {
1072 total_stability / dinuc_count
1073 } else {
1074 0.0
1075 };
1076 let mut palindrome_count = 0.0;
1077 for i in 0..sequence.len().saturating_sub(1) {
1078 let dinuc = &sequence[i..i + 2];
1079 if dinuc == "AT" || dinuc == "TA" || dinuc == "GC" || dinuc == "CG" {
1080 palindrome_count += 1.0;
1081 }
1082 }
1083 let palindrome_ratio = if sequence.len() > 1 {
1084 palindrome_count / (sequence.len() - 1) as Float
1085 } else {
1086 0.0
1087 };
1088 vec![mean_stability, palindrome_ratio, total_stability]
1089 }
1090}
1091#[derive(Debug, Clone)]
1092pub struct DNASequenceFeatures {
1093 include_reverse_complement: bool,
1094 include_nucleotide_composition: bool,
1095 include_dinucleotide_composition: bool,
1096 include_trinucleotide_composition: bool,
1097}
1098impl DNASequenceFeatures {
1099 pub fn new() -> Self {
1100 Self {
1101 include_reverse_complement: false,
1102 include_nucleotide_composition: true,
1103 include_dinucleotide_composition: false,
1104 include_trinucleotide_composition: false,
1105 }
1106 }
1107 pub fn include_reverse_complement(mut self, include: bool) -> Self {
1108 self.include_reverse_complement = include;
1109 self
1110 }
1111 pub fn include_nucleotide_composition(mut self, include: bool) -> Self {
1112 self.include_nucleotide_composition = include;
1113 self
1114 }
1115 pub fn include_dinucleotide_composition(mut self, include: bool) -> Self {
1116 self.include_dinucleotide_composition = include;
1117 self
1118 }
1119 pub fn include_trinucleotide_composition(mut self, include: bool) -> Self {
1120 self.include_trinucleotide_composition = include;
1121 self
1122 }
1123 pub fn extract_features(&self, sequence: &str) -> SklResult<Vec<f64>> {
1124 if sequence.is_empty() {
1125 return Err(SklearsError::InvalidInput("Empty DNA sequence".to_string()));
1126 }
1127 let indices: Vec<usize> = sequence.chars().filter_map(nucleotide_index).collect();
1128 if indices.is_empty() {
1129 return Err(SklearsError::InvalidInput(
1130 "DNA sequence contains no standard nucleotides".to_string(),
1131 ));
1132 }
1133 let rc_indices: Vec<usize> = if self.include_reverse_complement {
1134 indices
1135 .iter()
1136 .rev()
1137 .map(|&idx| COMPLEMENT_INDEX[idx])
1138 .collect()
1139 } else {
1140 Vec::new()
1141 };
1142 let rc_slice = rc_indices.as_slice();
1143 let mut features = Vec::new();
1144 if self.include_nucleotide_composition {
1145 features.extend(Self::kmer_frequencies(&indices, rc_slice, 1));
1146 }
1147 if self.include_dinucleotide_composition {
1148 features.extend(Self::kmer_frequencies(&indices, rc_slice, 2));
1149 }
1150 if self.include_trinucleotide_composition {
1151 features.extend(Self::kmer_frequencies(&indices, rc_slice, 3));
1152 }
1153 Ok(features)
1154 }
1155 fn kmer_frequencies(indices: &[usize], rc_indices: &[usize], k: usize) -> Vec<f64> {
1156 let (mut counts, mut total) = Self::count_kmers(indices, k);
1157 if !rc_indices.is_empty() {
1158 let (rc_counts, rc_total) = Self::count_kmers(rc_indices, k);
1159 total += rc_total;
1160 for (acc, val) in counts.iter_mut().zip(rc_counts) {
1161 *acc += val;
1162 }
1163 }
1164 if total == 0 {
1165 vec![0.0; counts.len()]
1166 } else {
1167 counts
1168 .into_iter()
1169 .map(|count| count as f64 / total as f64)
1170 .collect()
1171 }
1172 }
1173 fn count_kmers(sequence: &[usize], k: usize) -> (Vec<usize>, usize) {
1174 let alphabet_size = NUCLEOTIDES.len();
1175 let num_patterns = alphabet_size.pow(k as u32);
1176 let mut counts = vec![0usize; num_patterns];
1177 if k == 0 || sequence.len() < k {
1178 return (counts, 0);
1179 }
1180 for window in sequence.windows(k) {
1181 let mut idx = 0usize;
1182 for &base in window {
1183 idx = (idx << 2) | base;
1184 }
1185 counts[idx] += 1;
1186 }
1187 let total = sequence.len() + 1 - k;
1188 (counts, total)
1189 }
1190}
1191pub struct SequenceMotifExtractor {
1193 motif_length: usize,
1194 min_frequency: usize,
1195 max_mismatches: usize,
1196}
1197impl SequenceMotifExtractor {
1198 pub fn new() -> Self {
1200 Self {
1201 motif_length: 4,
1202 min_frequency: 2,
1203 max_mismatches: 0,
1204 }
1205 }
1206 pub fn motif_length(mut self, length: usize) -> Self {
1208 self.motif_length = length;
1209 self
1210 }
1211 pub fn min_frequency(mut self, frequency: usize) -> Self {
1213 self.min_frequency = frequency;
1214 self
1215 }
1216 pub fn max_mismatches(mut self, mismatches: usize) -> Self {
1218 self.max_mismatches = mismatches;
1219 self
1220 }
1221 pub fn extract_features(&self, sequences: &[String]) -> SklResult<Array2<Float>> {
1223 if sequences.is_empty() {
1224 return Err(SklearsError::InvalidInput(
1225 "Empty sequence list".to_string(),
1226 ));
1227 }
1228 let motifs = self.find_frequent_motifs(sequences)?;
1229 let mut features = Array2::zeros((sequences.len(), motifs.len()));
1230 for (seq_idx, sequence) in sequences.iter().enumerate() {
1231 for (motif_idx, motif) in motifs.iter().enumerate() {
1232 let count = self.count_motif_occurrences(sequence, motif);
1233 features[[seq_idx, motif_idx]] = count as Float;
1234 }
1235 }
1236 Ok(features)
1237 }
1238 fn find_frequent_motifs(&self, sequences: &[String]) -> SklResult<Vec<String>> {
1240 let mut motif_counts: HashMap<String, usize> = HashMap::new();
1241 for sequence in sequences {
1242 let sequence = sequence.to_uppercase();
1243 if sequence.len() < self.motif_length {
1244 continue;
1245 }
1246 for i in 0..=sequence.len() - self.motif_length {
1247 let motif = &sequence[i..i + self.motif_length];
1248 *motif_counts.entry(motif.to_string()).or_insert(0) += 1;
1249 }
1250 }
1251 let frequent_motifs: Vec<String> = motif_counts
1252 .into_iter()
1253 .filter(|(_, count)| *count >= self.min_frequency)
1254 .map(|(motif, _)| motif)
1255 .collect();
1256 Ok(frequent_motifs)
1257 }
1258 fn count_motif_occurrences(&self, sequence: &str, motif: &str) -> usize {
1260 let sequence = sequence.to_uppercase();
1261 let motif = motif.to_uppercase();
1262 if sequence.len() < motif.len() {
1263 return 0;
1264 }
1265 let mut count = 0;
1266 for i in 0..=sequence.len() - motif.len() {
1267 let subseq = &sequence[i..i + motif.len()];
1268 if self.hamming_distance(subseq, &motif) <= self.max_mismatches {
1269 count += 1;
1270 }
1271 }
1272 count
1273 }
1274 fn hamming_distance(&self, seq1: &str, seq2: &str) -> usize {
1276 if seq1.len() != seq2.len() {
1277 return usize::MAX;
1278 }
1279 seq1.chars()
1280 .zip(seq2.chars())
1281 .map(|(c1, c2)| if c1 == c2 { 0 } else { 1 })
1282 .sum()
1283 }
1284}
1285#[derive(Debug, Clone)]
1286pub struct GCContentFeatures {
1287 window_size: Option<usize>,
1288 step_size: Option<usize>,
1289}
1290impl GCContentFeatures {
1291 pub fn new() -> Self {
1292 Self {
1293 window_size: None,
1294 step_size: None,
1295 }
1296 }
1297 pub fn window_size(mut self, size: usize) -> Self {
1298 self.window_size = Some(size);
1299 self
1300 }
1301 pub fn step_size(mut self, step: usize) -> Self {
1302 self.step_size = Some(step.max(1));
1303 self
1304 }
1305 pub fn extract_features(&self, sequence: &str) -> SklResult<Vec<f64>> {
1306 if sequence.is_empty() {
1307 return Err(SklearsError::InvalidInput("Empty sequence".to_string()));
1308 }
1309 let sequence = sequence.to_ascii_uppercase();
1310 let bytes = sequence.as_bytes();
1311 let len = bytes.len();
1312 let window = self.window_size.unwrap_or(len);
1313 if window == 0 || window > len {
1314 return Err(SklearsError::InvalidInput(format!(
1315 "Window size {} is invalid for sequence length {}",
1316 window, len
1317 )));
1318 }
1319 let step = self.step_size.unwrap_or(window);
1320 if step == 0 {
1321 return Err(SklearsError::InvalidInput(
1322 "Step size must be greater than zero".to_string(),
1323 ));
1324 }
1325 let mut features = Vec::new();
1326 let mut start = 0;
1327 while start + window <= len {
1328 let slice = &bytes[start..start + window];
1329 let gc_count = slice.iter().filter(|&&b| b == b'G' || b == b'C').count();
1330 features.push(gc_count as f64 / window as f64);
1331 start += step;
1332 }
1333 if features.is_empty() {
1334 features.push(0.0);
1335 }
1336 Ok(features)
1337 }
1338}