Skip to main content

sklears_feature_extraction/biological/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use 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 retained for future phylogenetic distance computation
18    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
231    DNA,
232    /// RNA
233    RNA,
234    /// Protein
235    Protein,
236}
237/// Compositional features extractor for biological sequences
238pub struct CompositionFeatureExtractor {
239    include_dinucleotide: bool,
240    include_gc_content: bool,
241    include_codon_usage: bool,
242    sequence_type: SequenceType,
243}
244impl CompositionFeatureExtractor {
245    /// Create a new composition feature extractor
246    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    /// Set sequence type
255    pub fn sequence_type(mut self, seq_type: SequenceType) -> Self {
256        self.sequence_type = seq_type;
257        self
258    }
259    /// Include dinucleotide composition
260    pub fn include_dinucleotide(mut self, include: bool) -> Self {
261        self.include_dinucleotide = include;
262        self
263    }
264    /// Include GC content (for DNA/RNA)
265    pub fn include_gc_content(mut self, include: bool) -> Self {
266        self.include_gc_content = include;
267        self
268    }
269    /// Include codon usage (for DNA/RNA sequences)
270    pub fn include_codon_usage(mut self, include: bool) -> Self {
271        self.include_codon_usage = include;
272        self
273    }
274    /// Extract compositional features from a sequence
275    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    /// Calculate basic nucleotide/amino acid composition
302    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    /// Calculate dinucleotide composition
322    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    /// Calculate GC content
355    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    /// Calculate codon usage
370    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}
418/// Phylogenetic features extractor
419pub struct PhylogeneticFeatureExtractor {
420    reference_sequences: Vec<String>,
421    distance_metric: DistanceMetric,
422}
423impl PhylogeneticFeatureExtractor {
424    /// Create a new phylogenetic feature extractor
425    pub fn new() -> Self {
426        Self {
427            reference_sequences: Vec::new(),
428            distance_metric: DistanceMetric::Hamming,
429        }
430    }
431    /// Set reference sequences for distance calculation
432    pub fn reference_sequences(mut self, sequences: Vec<String>) -> Self {
433        self.reference_sequences = sequences;
434        self
435    }
436    /// Set distance metric
437    pub fn distance_metric(mut self, metric: DistanceMetric) -> Self {
438        self.distance_metric = metric;
439        self
440    }
441    /// Extract phylogenetic features (distances to reference sequences)
442    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    /// Calculate distance between two sequences
456    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    /// Calculate Hamming distance
464    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    /// Calculate Jukes-Cantor distance
478    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    /// Calculate Kimura 2-parameter distance
486    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    /// Check if substitution is a transition
513    fn is_transition(&self, c1: char, c2: char) -> bool {
514        matches!((c1, c2), ('A', 'G') | ('G', 'A') | ('C', 'T') | ('T', 'C'))
515    }
516}
517/// K-mer counter for biological sequences
518pub struct KmerCounter {
519    k: usize,
520    normalize: bool,
521    include_reverse_complement: bool,
522}
523impl KmerCounter {
524    /// Create a new K-mer counter
525    pub fn new() -> Self {
526        Self {
527            k: 3,
528            normalize: false,
529            include_reverse_complement: false,
530        }
531    }
532    /// Set the k-mer length
533    pub fn k(mut self, k: usize) -> Self {
534        self.k = k;
535        self
536    }
537    /// Set whether to normalize counts
538    pub fn normalize(mut self, normalize: bool) -> Self {
539        self.normalize = normalize;
540        self
541    }
542    /// Set whether to include reverse complement for DNA sequences
543    pub fn include_reverse_complement(mut self, include_reverse_complement: bool) -> Self {
544        self.include_reverse_complement = include_reverse_complement;
545        self
546    }
547    /// Extract k-mer features from a sequence
548    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    /// Generate all possible k-mers for given alphabet
588    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    /// Get alphabet from sequence
602    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    /// Get reverse complement of DNA sequence
612    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
633    Hamming,
634    /// JukesCantor
635    JukesCantor,
636    /// Kimura2P
637    Kimura2P,
638}
639/// Structural features extractor for biological sequences
640pub 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    /// Create a new structural feature extractor
649    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    /// Set sequence type
659    pub fn sequence_type(mut self, seq_type: SequenceType) -> Self {
660        self.sequence_type = seq_type;
661        self
662    }
663    /// Include hydrophobicity features
664    pub fn include_hydrophobicity(mut self, include: bool) -> Self {
665        self.include_hydrophobicity = include;
666        self
667    }
668    /// Include charge features
669    pub fn include_charge(mut self, include: bool) -> Self {
670        self.include_charge = include;
671        self
672    }
673    /// Include molecular weight features
674    pub fn include_molecular_weight(mut self, include: bool) -> Self {
675        self.include_molecular_weight = include;
676        self
677    }
678    /// Include secondary structure features
679    pub fn include_secondary_structure(mut self, include: bool) -> Self {
680        self.include_secondary_structure = include;
681        self
682    }
683    /// Extract structural features from a sequence
684    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    /// Calculate hydrophobicity features for protein sequences
728    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    /// Calculate charge features for protein sequences
779    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    /// Calculate molecular weight features for protein sequences
825    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    /// Calculate secondary structure propensity features
869    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    /// Calculate nucleotide structure features for DNA/RNA
975    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    /// Calculate melting temperature for DNA/RNA
1016    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    /// Calculate stability features for DNA/RNA
1040    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}
1191/// Sequence motif finder
1192pub struct SequenceMotifExtractor {
1193    motif_length: usize,
1194    min_frequency: usize,
1195    max_mismatches: usize,
1196}
1197impl SequenceMotifExtractor {
1198    /// Create a new sequence motif extractor
1199    pub fn new() -> Self {
1200        Self {
1201            motif_length: 4,
1202            min_frequency: 2,
1203            max_mismatches: 0,
1204        }
1205    }
1206    /// Set motif length
1207    pub fn motif_length(mut self, length: usize) -> Self {
1208        self.motif_length = length;
1209        self
1210    }
1211    /// Set minimum frequency for motif detection
1212    pub fn min_frequency(mut self, frequency: usize) -> Self {
1213        self.min_frequency = frequency;
1214        self
1215    }
1216    /// Set maximum allowed mismatches
1217    pub fn max_mismatches(mut self, mismatches: usize) -> Self {
1218        self.max_mismatches = mismatches;
1219        self
1220    }
1221    /// Extract motif features from sequences
1222    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    /// Find frequent motifs in sequences
1239    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    /// Count occurrences of a motif in a sequence (with mismatches)
1259    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    /// Calculate Hamming distance between two sequences
1275    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}