Skip to main content

orphos_core/
engine.rs

1use std::marker::PhantomData;
2use std::path::Path;
3use std::sync::OnceLock;
4
5use crate::algorithms::dynamic_programming::eliminate_bad_genes;
6use crate::algorithms::dynamic_programming::predict_genes;
7use crate::algorithms::gene_finding::GeneBuilder;
8use crate::config::OrphosConfig;
9use crate::constants::MIN_SEQUENCE_LENGTH;
10use crate::metagenomic::bins;
11use crate::metagenomic::get_preset_training_ref;
12use crate::node::{
13    add_nodes, calculate_dicodon_gene, raw_coding_score, rbs_score, record_gc_bias,
14    record_overlapping_starts, reset_node_scores, score_nodes, sort_nodes_by_position,
15};
16use crate::results::{OrphosResults, SequenceInfo};
17use crate::sequence::calc_most_gc_frame;
18use crate::sequence::encoded::EncodedSequence;
19use crate::sequence::read_fasta_sequences;
20use crate::training::non_sd_training::train_starts_nonsd;
21use crate::training::sd_training::train_starts_sd;
22use crate::training::should_use_sd;
23use crate::types::Gene;
24use crate::types::{CodonType, Node, OrphosError, Training};
25use bio::bio_types::strand::Strand;
26use rayon::prelude::*;
27
28static RAYON_POOL_INIT: OnceLock<()> = OnceLock::new();
29
30/// Marker trait for Orphos training state.
31///
32/// This trait is used in the type-state pattern to enforce that training
33/// must be performed before gene prediction. It's implemented by both
34/// [`Untrained`] and [`Trained`] marker types.
35pub trait TrainingState {}
36
37/// Marker type indicating an untrained Orphos instance.
38///
39/// A [`Orphos<Untrained>`] instance can perform training operations
40/// but cannot find genes until training is complete.
41#[derive(Debug, Clone)]
42pub struct Untrained;
43
44/// Marker type indicating a trained Orphos instance.
45///
46/// A [`Orphos<Trained>`] instance has completed training and can
47/// perform gene prediction operations.
48#[derive(Debug, Clone)]
49pub struct Trained;
50
51impl TrainingState for Untrained {}
52impl TrainingState for Trained {}
53
54/// Main Orphos configuration and execution engine.
55///
56/// This struct uses the type-state pattern with the `S` type parameter
57/// to ensure training is performed before gene prediction. The state
58/// transitions from [`Untrained`] to [`Trained`] via the training methods.
59///
60/// # Type Parameters
61///
62/// * `S` - The training state, either [`Untrained`] or [`Trained`]
63///
64/// # Examples
65///
66/// ```rust,no_run
67/// use orphos_core::engine::UntrainedOrphos;
68/// use orphos_core::config::OrphosConfig;
69/// use orphos_core::sequence::encoded::EncodedSequence;
70///
71/// // Create an untrained instance
72/// let mut orphos = UntrainedOrphos::new();
73///
74/// // Encode a sequence
75/// let sequence = b"ATGAAACGCATTAGCACCACCATT...";
76/// let encoded = EncodedSequence::without_masking(sequence);
77///
78/// // Train on the sequence and get results
79/// let trained = orphos.train_single_genome(&encoded)?;
80///
81/// // Use the higher-level API to analyze sequences
82/// use orphos_core::OrphosAnalyzer;
83/// let mut analyzer = OrphosAnalyzer::new(OrphosConfig::default());
84/// let results = analyzer.analyze_sequence("ATGAAACGCATTAGCACCACCATT...", None)?;
85/// println!("Found {} genes", results.genes.len());
86/// # Ok::<(), orphos_core::types::OrphosError>(())
87/// ```
88#[derive(Debug, Default)]
89pub struct Orphos<S: TrainingState> {
90    /// Configuration options for gene prediction
91    pub config: OrphosConfig,
92    /// Training data obtained from the genome
93    training: Option<Training>,
94    /// Type-state marker (zero-sized)
95    _state: PhantomData<S>,
96}
97
98/// Type alias for an untrained Orphos instance.
99///
100/// Use this when you need to perform training on a new genome.
101pub type UntrainedOrphos = Orphos<Untrained>;
102
103/// Type alias for a trained Orphos instance.
104///
105/// Use this when you have already trained on a genome and want to find genes.
106pub type TrainedOrphos = Orphos<Trained>;
107
108impl UntrainedOrphos {
109    /// Creates a new untrained Orphos instance with default configuration.
110    ///
111    /// # Examples
112    ///
113    /// ```rust
114    /// use orphos_core::engine::UntrainedOrphos;
115    ///
116    /// let orphos = UntrainedOrphos::new();
117    /// ```
118    pub fn new() -> Self {
119        Self {
120            config: OrphosConfig::default(),
121            training: None,
122            _state: PhantomData,
123        }
124    }
125
126    /// Creates a new untrained Orphos instance with custom configuration.
127    ///
128    /// # Arguments
129    ///
130    /// * `config` - Configuration options for gene prediction
131    ///
132    /// # Errors
133    ///
134    /// Returns [`OrphosError`] if thread pool configuration fails.
135    ///
136    /// # Examples
137    ///
138    /// ```rust
139    /// use orphos_core::engine::UntrainedOrphos;
140    /// use orphos_core::config::{OrphosConfig, OutputFormat};
141    ///
142    /// let config = OrphosConfig {
143    ///     closed_ends: true,
144    ///     output_format: OutputFormat::Gff,
145    ///     ..Default::default()
146    /// };
147    ///
148    /// let orphos = UntrainedOrphos::with_config(config)?;
149    /// # Ok::<(), orphos_core::types::OrphosError>(())
150    /// ```
151    pub fn with_config(config: OrphosConfig) -> Result<Self, OrphosError> {
152        if config.circular && config.closed_ends {
153            return Err(OrphosError::InvalidSequence(
154                "Invalid configuration: circular and closed_ends cannot both be true".to_string(),
155            ));
156        }
157
158        let orphos = Self {
159            config,
160            training: None,
161            _state: PhantomData,
162        };
163
164        if let Some(num_threads) = orphos.config.num_threads {
165            RAYON_POOL_INIT.get_or_init(|| {
166                // Ignore error if pool was already initialized by a previous call
167                let _ = rayon::ThreadPoolBuilder::new()
168                    .num_threads(num_threads)
169                    .build_global();
170            });
171        }
172
173        Ok(orphos)
174    }
175
176    /// Trains the model on a single complete genome sequence.
177    ///
178    /// This method analyzes the sequence to build a statistical model of gene
179    /// characteristics including:
180    /// - Start codon usage (ATG, GTG, TTG)
181    /// - Ribosome binding site motifs
182    /// - Codon usage patterns
183    /// - GC content bias
184    ///
185    /// # Arguments
186    ///
187    /// * `encoded_sequence` - The genome sequence encoded in bitmap format
188    ///
189    /// # Returns
190    ///
191    /// A [`TrainedOrphos`] instance ready for gene prediction.
192    ///
193    /// # Errors
194    ///
195    /// Returns [`OrphosError::InvalidSequence`] if:
196    /// - The sequence is shorter than [`MIN_SEQUENCE_LENGTH`]
197    /// - The sequence contains invalid characters
198    /// - Training fails to converge
199    ///
200    /// # Examples
201    ///
202    /// ```rust,no_run
203    /// use orphos_core::engine::UntrainedOrphos;
204    /// use orphos_core::sequence::encoded::EncodedSequence;
205    ///
206    /// let mut orphos = UntrainedOrphos::new();
207    /// let sequence = b"ATGAAACGCATTAGCACCACCATT...";
208    /// let encoded = EncodedSequence::without_masking(sequence);
209    ///
210    /// let trained = orphos.train_single_genome(&encoded)?;
211    /// # Ok::<(), orphos_core::types::OrphosError>(())
212    /// ```
213    pub fn train_single_genome(
214        &mut self,
215        encoded_sequence: &EncodedSequence,
216    ) -> Result<TrainedOrphos, OrphosError> {
217        let sequence_length = encoded_sequence.sequence_length;
218        if sequence_length < MIN_SEQUENCE_LENGTH {
219            return Err(OrphosError::InvalidSequence(format!(
220                "Sequence too short for gene prediction: {} bp (minimum {} bp required)",
221                sequence_length, MIN_SEQUENCE_LENGTH
222            )));
223        }
224
225        if !self.config.quiet {
226            eprintln!(
227                "Training on single genome ({} bp, {:.2}% GC)...",
228                sequence_length,
229                encoded_sequence.gc_content * 100.0
230            );
231        }
232        let mut training = Training {
233            gc_content: 0.0,
234            ..Default::default()
235        };
236        training.uses_shine_dalgarno = false;
237        training.gc_bias_factors = [0.0; 3];
238
239        training.gc_content = encoded_sequence.gc_content;
240
241        let mut nodes = Vec::new();
242        let num_nodes = add_nodes(
243            encoded_sequence,
244            &mut nodes,
245            self.config.closed_ends,
246            self.config.circular,
247            &training,
248        )?;
249
250        if !self.config.quiet {
251            eprintln!(
252                "Located {} potential start/stop nodes, closed {}",
253                num_nodes, self.config.closed_ends
254            );
255        }
256
257        sort_nodes_by_position(&mut nodes);
258
259        let gc_frame = calc_most_gc_frame(&encoded_sequence.forward_sequence, sequence_length);
260
261        record_gc_bias(&gc_frame, &mut nodes, &mut training);
262
263        if !self.config.quiet {
264            eprintln!(
265                "Frame bias scores: {:.8} {:.8} {:.8}",
266                training.gc_bias_factors[0],
267                training.gc_bias_factors[1],
268                training.gc_bias_factors[2]
269            );
270        }
271
272        record_overlapping_starts(&mut nodes, &training, false);
273
274        let initial_path = predict_genes(&mut nodes, &training, false).unwrap_or(0);
275        calculate_dicodon_gene(
276            &mut training,
277            &encoded_sequence.forward_sequence,
278            &encoded_sequence.reverse_complement_sequence,
279            sequence_length,
280            &nodes,
281            initial_path,
282        );
283
284        raw_coding_score(
285            &encoded_sequence.forward_sequence,
286            &encoded_sequence.reverse_complement_sequence,
287            sequence_length,
288            &mut nodes,
289            &training,
290        );
291
292        rbs_score(
293            &encoded_sequence.forward_sequence,
294            &encoded_sequence.reverse_complement_sequence,
295            sequence_length,
296            &mut nodes,
297            &training,
298        );
299
300        train_starts_sd(
301            &encoded_sequence.forward_sequence,
302            &encoded_sequence.reverse_complement_sequence,
303            sequence_length,
304            &nodes,
305            &mut training,
306        );
307        training.uses_shine_dalgarno = should_use_sd(&training);
308        if self.config.force_non_sd {
309            training.uses_shine_dalgarno = false;
310        }
311
312        if !training.uses_shine_dalgarno {
313            train_starts_nonsd(
314                &encoded_sequence.forward_sequence,
315                &encoded_sequence.reverse_complement_sequence,
316                sequence_length,
317                &mut nodes,
318                &mut training,
319            );
320        }
321
322        if !self.config.quiet {
323            eprintln!("Training complete!");
324        }
325
326        Ok(Orphos {
327            config: self.config.clone(),
328            training: Some(training),
329            _state: PhantomData,
330        })
331    }
332
333    pub fn train_meta_genome(
334        &mut self,
335        _encoded_sequence: &EncodedSequence,
336    ) -> Result<TrainedOrphos, OrphosError> {
337        if !self.config.quiet {
338            eprintln!("Request: Metagenomic, Phase: Training");
339            eprintln!("Initializing training files...");
340        }
341        // initialize metagenomic bins;
342        if !self.config.quiet {
343            eprintln!("Metagenomic training initialized.");
344        }
345        Ok(Orphos {
346            config: self.config.clone(),
347            training: None,
348            _state: PhantomData,
349        })
350    }
351}
352
353impl TrainedOrphos {
354    /// Creates a new trained Orphos instance with pre-computed training data.
355    ///
356    /// This is useful when you have previously computed training data that you
357    /// want to reuse for gene prediction on multiple sequences.
358    ///
359    /// # Arguments
360    ///
361    /// * `config` - Configuration options for gene prediction
362    /// * `training` - Pre-computed training data
363    ///
364    /// # Examples
365    ///
366    /// ```rust,no_run
367    /// use orphos_core::engine::TrainedOrphos;
368    /// use orphos_core::config::OrphosConfig;
369    /// use orphos_core::types::Training;
370    ///
371    /// let config = OrphosConfig::default();
372    /// let training = Training::default(); // In practice, load from file
373    ///
374    /// let trained = TrainedOrphos::new(config, training);
375    /// ```
376    pub const fn new(config: OrphosConfig, training: Training) -> Self {
377        Self {
378            config,
379            training: Some(training),
380            _state: PhantomData,
381        }
382    }
383
384    /// Finds genes in a single genome sequence using the trained model.
385    ///
386    /// Uses dynamic programming to find the optimal set of non-overlapping genes
387    /// based on the statistical model built during training. This method performs:
388    ///
389    /// 1. Node generation (start/stop codon detection)
390    /// 2. Node scoring (coding potential, RBS, start codon usage)
391    /// 3. Dynamic programming gene selection
392    /// 4. Gene quality filtering
393    /// 5. Start position refinement
394    ///
395    /// # Arguments
396    ///
397    /// * `encoded_sequence` - The genome sequence encoded in bitmap format
398    ///
399    /// # Returns
400    ///
401    /// A vector of [`Gene`] predictions sorted by position. Returns an empty
402    /// vector if no genes are found.
403    ///
404    /// # Errors
405    ///
406    /// Returns [`OrphosError::InvalidSequence`] if:
407    /// - The Orphos instance is not properly trained
408    /// - Node generation fails
409    /// - Sequence contains invalid characters
410    ///
411    /// # Examples
412    ///
413    /// ```rust,no_run
414    /// use orphos_core::engine::UntrainedOrphos;
415    /// use orphos_core::sequence::encoded::EncodedSequence;
416    ///
417    /// let mut orphos = UntrainedOrphos::new();
418    /// let sequence = b"ATGAAACGCATTAGCACCACCATT...";
419    /// let encoded = EncodedSequence::without_masking(sequence);
420    ///
421    /// let trained = orphos.train_single_genome(&encoded)?;
422    ///
423    /// // Use OrphosAnalyzer for a higher-level API to find genes
424    /// use orphos_core::OrphosAnalyzer;
425    /// use orphos_core::config::OrphosConfig;
426    /// let mut analyzer = OrphosAnalyzer::new(OrphosConfig::default());
427    /// let results = analyzer.analyze_sequence("ATGAAACGCATTAGCACCACCATT...", None)?;
428    /// for gene in &results.genes {
429    ///     println!("Gene at {}-{} (strand: {:?})",
430    ///              gene.coordinates.begin, gene.coordinates.end, gene.coordinates.strand);
431    /// }
432    /// # Ok::<(), orphos_core::types::OrphosError>(())
433    /// ```
434    fn find_genes_single(
435        &self,
436        encoded_sequence: &EncodedSequence,
437    ) -> Result<Vec<Gene>, OrphosError> {
438        let mut nodes = Vec::new();
439        let training = self
440            .training
441            .as_ref()
442            .ok_or_else(|| OrphosError::InvalidSequence("Orphos is not trained".to_string()))?;
443
444        let _num_nodes = add_nodes(
445            encoded_sequence,
446            &mut nodes,
447            self.config.closed_ends,
448            self.config.circular,
449            training,
450        )?;
451
452        sort_nodes_by_position(&mut nodes);
453
454        // Tap the training state right before scoring to ensure parity at inference time
455
456        score_nodes(
457            encoded_sequence,
458            &mut nodes,
459            training,
460            self.config.closed_ends,
461            false,
462        )?;
463
464        record_overlapping_starts(&mut nodes, training, true);
465
466        let gene_path = match predict_genes(&mut nodes, training, true) {
467            Some(path) => path,
468            None => {
469                // No genes found - return empty gene list
470                return Ok(vec![]);
471            }
472        };
473
474        eliminate_bad_genes(&mut nodes, Some(gene_path), training);
475
476        let genes = GeneBuilder::from_nodes(&nodes, gene_path, training, 1)
477            .with_tweaked_starts()
478            .with_annotations()
479            .build();
480
481        Ok(genes)
482    }
483
484    fn find_genes_meta(
485        &self,
486        encoded_sequence: &EncodedSequence,
487    ) -> Result<(Vec<Gene>, usize), OrphosError> {
488        if !self.config.quiet {
489            eprintln!("Request: Metagenomic, Phase: Gene Finding");
490        }
491        let mut low = 0.88495 * encoded_sequence.gc_content - 0.0102337;
492        if low > 0.65 {
493            low = 0.65;
494        }
495        let mut high = 0.86596 * encoded_sequence.gc_content + 0.1131991;
496        if high < 0.35 {
497            high = 0.35;
498        }
499        let mut max_score = -100.0;
500        let mut max_phase = 0;
501
502        let mut nodes = Vec::new();
503        let mut genes = Vec::new();
504
505        for (i, _bin) in bins().iter().enumerate() {
506            let preset_training = get_preset_training_ref(i).unwrap();
507            if i == 0
508                || preset_training.translation_table
509                    != get_preset_training_ref(i - 1).unwrap().translation_table
510            {
511                let _num_nodes = add_nodes(
512                    encoded_sequence,
513                    &mut nodes,
514                    self.config.closed_ends,
515                    self.config.circular,
516                    preset_training,
517                )?;
518                sort_nodes_by_position(&mut nodes);
519            }
520            if preset_training.gc_content < low || preset_training.gc_content > high {
521                continue;
522            }
523            reset_node_scores(&mut nodes);
524            score_nodes(
525                encoded_sequence,
526                &mut nodes,
527                preset_training,
528                self.config.closed_ends,
529                true,
530            )?;
531            record_overlapping_starts(&mut nodes, preset_training, true);
532            let gene_path = predict_genes(&mut nodes, preset_training, true);
533            if let Some(path) = gene_path
534                && let Some(node) = nodes.get(path)
535                && node.scores.total_score > max_score
536            {
537                max_phase = i;
538                max_score = node.scores.total_score;
539                eliminate_bad_genes(&mut nodes, gene_path, preset_training);
540                genes = GeneBuilder::from_nodes(&nodes, path, preset_training, 1)
541                    .with_tweaked_starts()
542                    .with_annotations()
543                    .build();
544            }
545            // Recover the nodes for the best of the runs.
546        }
547        nodes.clear();
548        let best_training = get_preset_training_ref(max_phase).unwrap();
549        let _ = add_nodes(
550            encoded_sequence,
551            &mut nodes,
552            self.config.closed_ends,
553            self.config.circular,
554            best_training,
555        )?;
556        sort_nodes_by_position(&mut nodes);
557        score_nodes(
558            encoded_sequence,
559            &mut nodes,
560            best_training,
561            self.config.closed_ends,
562            true,
563        )?;
564        // Update display_score from re-scored nodes for GFF column 6
565        // This matches Prodigal's behavior where column 6 uses re-scored values
566        // but the attribute scores stay from the original scoring during the loop
567        update_display_scores(&mut genes, &nodes);
568        Ok((genes, max_phase))
569    }
570}
571
572/// Update gene display_score from re-scored nodes for GFF column 6 output.
573/// Matches Prodigal's behavior where column 6 uses re-scored values.
574fn update_display_scores(genes: &mut [Gene], nodes: &[Node]) {
575    for gene in genes.iter_mut() {
576        // Find the start node by matching position and strand
577        let target_pos = if gene.coordinates.strand == Strand::Forward {
578            gene.coordinates.begin.saturating_sub(1)
579        } else {
580            gene.coordinates.end.saturating_sub(1)
581        };
582
583        let lo = nodes.partition_point(|n| n.position.index < target_pos);
584        if let Some(start_node) = nodes[lo..]
585            .iter()
586            .take_while(|n| n.position.index == target_pos)
587            .find(|n| {
588                n.position.strand == gene.coordinates.strand
589                    && n.position.codon_type != CodonType::Stop
590            })
591        {
592            gene.display_score =
593                Some(start_node.scores.coding_score + start_node.scores.start_score);
594        }
595    }
596}
597
598/// High-level gene finding analyzer with automatic training.
599///
600/// This struct provides a simplified interface for gene prediction that handles
601/// training automatically. It's the recommended entry point for most users.
602///
603/// Unlike the type-safe [`Orphos`] struct, `OrphosAnalyzer` manages training
604/// internally and provides convenient methods for analyzing sequences from various
605/// sources (files, strings, byte slices).
606///
607/// # Modes
608///
609/// - **Single Genome Mode** (default): Trains on each sequence individually for
610///   optimal accuracy on complete genomes
611/// - **Metagenomic Mode**: Uses pre-computed models for fragmented sequences
612///
613/// # Examples
614///
615/// ## Analyze a sequence string
616///
617/// ```rust,no_run
618/// use orphos_core::{OrphosAnalyzer, config::OrphosConfig};
619///
620/// let mut analyzer = OrphosAnalyzer::new(OrphosConfig::default());
621///
622/// let sequence = "ATGAAACGCATTAGCACCACCATT...";
623/// let results = analyzer.analyze_sequence(sequence, Some("genome1".to_string()))?;
624///
625/// println!("Found {} genes in {} bp sequence",
626///          results.genes.len(),
627///          results.sequence_info.length);
628/// # Ok::<(), orphos_core::types::OrphosError>(())
629/// ```
630///
631/// ## Analyze a FASTA file
632///
633/// ```rust,no_run
634/// use orphos_core::{OrphosAnalyzer, config::OrphosConfig};
635///
636/// let analyzer = OrphosAnalyzer::new(OrphosConfig::default());
637/// let results = analyzer.analyze_fasta_file("genome.fasta")?;
638///
639/// for result in results {
640///     println!("Sequence: {}", result.sequence_info.header);
641///     println!("  Genes: {}", result.genes.len());
642///     println!("  GC%: {:.2}", result.sequence_info.gc_content * 100.0);
643/// }
644/// # Ok::<(), orphos_core::types::OrphosError>(())
645/// ```
646///
647/// ## With custom configuration
648///
649/// ```rust,no_run
650/// use orphos_core::{OrphosAnalyzer, config::{OrphosConfig, OutputFormat}};
651///
652/// let config = OrphosConfig {
653///     closed_ends: true,
654///     mask_n_runs: true,
655///     output_format: OutputFormat::Gff,
656///     num_threads: Some(4),
657///     ..Default::default()
658/// };
659///
660/// let analyzer = OrphosAnalyzer::new(config);
661/// # Ok::<(), orphos_core::types::OrphosError>(())
662/// ```
663#[derive(Debug)]
664pub struct OrphosAnalyzer {
665    /// Configuration options for gene prediction
666    pub config: OrphosConfig,
667}
668
669impl OrphosAnalyzer {
670    /// Creates a new analyzer with the specified configuration.
671    ///
672    /// # Arguments
673    ///
674    /// * `config` - Configuration options for gene prediction
675    ///
676    /// # Examples
677    ///
678    /// ```rust
679    /// use orphos_core::{OrphosAnalyzer, config::OrphosConfig};
680    ///
681    /// let analyzer = OrphosAnalyzer::new(OrphosConfig::default());
682    /// ```
683    pub const fn new(config: OrphosConfig) -> Self {
684        Self { config }
685    }
686
687    /// Analyzes sequences from a FASTA file.
688    ///
689    /// Reads all sequences from the FASTA file and performs gene prediction on each.
690    /// In single genome mode, each sequence is trained and analyzed independently.
691    ///
692    /// # Arguments
693    ///
694    /// * `path` - Path to the FASTA file
695    ///
696    /// # Returns
697    ///
698    /// A vector of [`OrphosResults`], one for each sequence in the file.
699    ///
700    /// # Errors
701    ///
702    /// Returns [`OrphosError`] if:
703    /// - The file cannot be read
704    /// - The FASTA format is invalid
705    /// - Any sequence fails analysis
706    ///
707    /// # Examples
708    ///
709    /// ```rust,no_run
710    /// use orphos_core::{OrphosAnalyzer, config::OrphosConfig};
711    ///
712    /// let analyzer = OrphosAnalyzer::new(OrphosConfig::default());
713    /// let results = analyzer.analyze_fasta_file("genomes.fasta")?;
714    ///
715    /// for (i, result) in results.iter().enumerate() {
716    ///     println!("Sequence {}: {} genes", i + 1, result.genes.len());
717    /// }
718    /// # Ok::<(), orphos_core::types::OrphosError>(())
719    /// ```
720    pub fn analyze_fasta_file<P: AsRef<Path>>(
721        &self,
722        path: P,
723    ) -> Result<Vec<OrphosResults>, OrphosError> {
724        let sequences = read_fasta_sequences(path.as_ref().to_str().unwrap())?;
725
726        sequences
727            .into_par_iter()
728            .map(|(header, description, seq_bytes)| {
729                self.analyze_sequence_bytes(&seq_bytes, header, description)
730            })
731            .collect()
732    }
733
734    /// Analyzes a single sequence from a string.
735    ///
736    /// Converts the string sequence to bytes and performs gene prediction.
737    /// This is a convenience method for analyzing sequences already loaded
738    /// into memory.
739    ///
740    /// # Arguments
741    ///
742    /// * `sequence` - DNA sequence string (A, T, G, C, N)
743    /// * `header` - Optional sequence identifier (defaults to "Orphos_Seq_1")
744    ///
745    /// # Returns
746    ///
747    /// [`OrphosResults`] containing genes, training data, and sequence info.
748    ///
749    /// # Errors
750    ///
751    /// Returns [`OrphosError`] if:
752    /// - The sequence is too short (< 20,000 bp recommended)
753    /// - Training fails
754    /// - Gene prediction fails
755    ///
756    /// # Examples
757    ///
758    /// ```rust,no_run
759    /// use orphos_core::{OrphosAnalyzer, config::OrphosConfig};
760    ///
761    /// let analyzer = OrphosAnalyzer::new(OrphosConfig::default());
762    ///
763    /// let sequence = "ATGAAACGCATTAGCACCACCATT...";
764    /// let results = analyzer.analyze_sequence(sequence, Some("E. coli K12".to_string()))?;
765    ///
766    /// println!("Analyzed: {}", results.sequence_info.header);
767    /// println!("Found {} genes", results.genes.len());
768    /// println!("GC content: {:.2}%", results.sequence_info.gc_content * 100.0);
769    /// # Ok::<(), orphos_core::types::OrphosError>(())
770    /// ```
771    pub fn analyze_sequence(
772        &self,
773        sequence: &str,
774        header: Option<String>,
775    ) -> Result<OrphosResults, OrphosError> {
776        let seq_bytes = sequence.as_bytes();
777        let header = header.unwrap_or_else(|| "Orphos_Seq_1".to_string());
778
779        self.analyze_sequence_bytes(seq_bytes, header, None)
780    }
781
782    /// Analyzes a single sequence from raw bytes.
783    ///
784    /// This is the core analysis method used by other convenience methods.
785    /// It handles the complete workflow: encoding, training, and gene prediction.
786    ///
787    /// # Arguments
788    ///
789    /// * `sequence` - Raw DNA sequence bytes (ASCII: A, T, G, C, N)
790    /// * `header` - Sequence identifier for output
791    /// * `description` - Optional sequence description
792    ///
793    /// # Returns
794    ///
795    /// [`OrphosResults`] containing:
796    /// - Predicted genes with coordinates and scores
797    /// - Training parameters used
798    /// - Sequence statistics (length, GC content)
799    ///
800    /// # Errors
801    ///
802    /// Returns [`OrphosError`] if:
803    /// - The sequence is too short for reliable training
804    /// - Sequence encoding fails
805    /// - Training or prediction fails
806    ///
807    /// # Examples
808    ///
809    /// ```rust,no_run
810    /// use orphos_core::{OrphosAnalyzer, config::OrphosConfig};
811    ///
812    /// let analyzer = OrphosAnalyzer::new(OrphosConfig::default());
813    ///
814    /// let sequence = b"ATGAAACGCATTAGCACCACCATT...";
815    /// let results = analyzer.analyze_sequence_bytes(
816    ///     sequence,
817    ///     "sequence1".to_string(),
818    ///     Some("E. coli genome".to_string())
819    /// )?;
820    ///
821    /// println!("Found {} genes", results.genes.len());
822    /// # Ok::<(), orphos_core::types::OrphosError>(())
823    /// ```
824    pub fn analyze_sequence_bytes(
825        &self,
826        sequence: &[u8],
827        header: String,
828        description: Option<String>,
829    ) -> Result<OrphosResults, OrphosError> {
830        let sequence_length = sequence.len();
831        let encoded_sequence = self.encode_sequence(sequence);
832
833        let mut untrained_orphos = UntrainedOrphos::with_config(self.config.clone())?;
834        let trained_orphos = if self.config.metagenomic {
835            untrained_orphos.train_meta_genome(&encoded_sequence)?
836        } else {
837            untrained_orphos.train_single_genome(&encoded_sequence)?
838        };
839        let (genes, metagenomic_model, best_training) = if self.config.metagenomic {
840            let (genes, best_phase) = trained_orphos.find_genes_meta(&encoded_sequence)?;
841            let bin = &bins()[best_phase];
842            let training = get_preset_training_ref(best_phase).unwrap();
843            let model_desc = format!(
844                "{}|{}|{}|{:.1}|{}|{}",
845                bin.id,
846                bin.name,
847                bin.domain,
848                bin.gc_percent,
849                training.translation_table,
850                if training.uses_shine_dalgarno { 1 } else { 0 }
851            );
852            (genes, Some(model_desc), training.clone())
853        } else {
854            let genes = trained_orphos.find_genes_single(&encoded_sequence)?;
855            let training = trained_orphos.training.clone().unwrap_or_default();
856            (genes, None, training)
857        };
858
859        let num_genes = genes.len();
860        Ok(OrphosResults {
861            genes,
862            training_used: best_training,
863            sequence_info: SequenceInfo {
864                length: sequence_length,
865                gc_content: encoded_sequence.gc_content,
866                num_genes,
867                header,
868                description,
869            },
870            metagenomic_model,
871        })
872    }
873
874    /// Encodes a sequence to bitmap format for efficient processing.
875    ///
876    /// Converts the DNA sequence into a compact binary representation that
877    /// enables fast nucleotide lookups. Optionally masks runs of N characters.
878    ///
879    /// # Arguments
880    ///
881    /// * `sequence` - Raw DNA sequence bytes
882    ///
883    /// # Returns
884    ///
885    /// An [`EncodedSequence`] with forward and reverse-complement strands
886    /// encoded in bitmap format.
887    fn encode_sequence(&self, sequence: &[u8]) -> EncodedSequence {
888        if self.config.mask_n_runs {
889            EncodedSequence::with_masking(sequence)
890        } else {
891            EncodedSequence::without_masking(sequence)
892        }
893    }
894}
895
896#[cfg(test)]
897mod tests {
898    use super::*;
899    use crate::config::OutputFormat;
900    use crate::constants::TEST_SEQUENCE_REPEAT_FACTOR;
901    use std::env;
902    use std::fs;
903
904    // Helper function to create a simple test sequence
905    fn create_test_sequence() -> Vec<u8> {
906        // Simple DNA sequence with some genes
907        "ATGAAACGTAAATAG".as_bytes().to_vec()
908    }
909
910    // Helper function to create a longer test sequence for training
911    fn create_training_sequence() -> Vec<u8> {
912        // A longer sequence suitable for training (> 20,000 bp recommended)
913        let basic_gene = "ATGAAACGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTAAATAG";
914        basic_gene
915            .repeat(TEST_SEQUENCE_REPEAT_FACTOR)
916            .as_bytes()
917            .to_vec()
918    }
919
920    // Helper function to create an EncodedSequence for testing
921    fn create_encoded_sequence_for_test(seq: &[u8]) -> EncodedSequence {
922        EncodedSequence::without_masking(seq)
923    }
924
925    #[test]
926    fn test_training_state_traits() {
927        // Test that training state markers implement required traits
928        let _untrained: Untrained = Untrained;
929        let _trained: Trained = Trained;
930
931        // Test that they can be cloned and debugged
932        let untrained_clone = _untrained.clone();
933        let trained_clone = _trained.clone();
934
935        assert_eq!(format!("{:?}", untrained_clone), "Untrained");
936        assert_eq!(format!("{:?}", trained_clone), "Trained");
937    }
938
939    #[test]
940    fn test_untrained_orphos_new() {
941        let orphos = UntrainedOrphos::new();
942
943        assert!(!orphos.config.metagenomic);
944        assert!(!orphos.config.closed_ends);
945        assert!(!orphos.config.mask_n_runs);
946        assert!(!orphos.config.force_non_sd);
947        assert!(!orphos.config.quiet);
948        assert_eq!(orphos.config.output_format, OutputFormat::Genbank);
949        assert!(orphos.training.is_none());
950    }
951
952    #[test]
953    fn test_untrained_orphos_with_config() {
954        let config = OrphosConfig {
955            metagenomic: true,
956            closed_ends: true,
957            quiet: true,
958            ..OrphosConfig::default()
959        };
960
961        let result = UntrainedOrphos::with_config(config.clone());
962        assert!(result.is_ok());
963
964        let orphos = result.unwrap();
965        assert!(orphos.config.metagenomic);
966        assert!(orphos.config.closed_ends);
967        assert!(orphos.config.quiet);
968        assert!(orphos.training.is_none());
969    }
970
971    #[test]
972    fn test_untrained_orphos_with_thread_config() {
973        let config = OrphosConfig {
974            num_threads: Some(2),
975            ..OrphosConfig::default()
976        };
977
978        let result = UntrainedOrphos::with_config(config);
979        // Thread configuration might fail depending on system
980        // Expected to potentially fail
981        let _ = result;
982    }
983
984    #[test]
985    fn test_untrained_orphos_with_invalid_thread_config() {
986        let config = OrphosConfig {
987            num_threads: Some(0),
988            ..OrphosConfig::default()
989        };
990
991        let result = UntrainedOrphos::with_config(config);
992        // Thread configuration might fail depending on system
993        // Rayon might handle it gracefully or it might fail
994        let _ = result;
995    }
996
997    #[test]
998    fn test_untrained_orphos_with_conflicting_topology_config() {
999        let config = OrphosConfig {
1000            circular: true,
1001            closed_ends: true,
1002            ..OrphosConfig::default()
1003        };
1004
1005        let result = UntrainedOrphos::with_config(config);
1006        assert!(result.is_err());
1007        if let Err(OrphosError::InvalidSequence(msg)) = result {
1008            assert!(msg.contains("circular"));
1009            assert!(msg.contains("closed_ends"));
1010        } else {
1011            panic!("Expected InvalidSequence error");
1012        }
1013    }
1014
1015    #[test]
1016    fn test_trained_orphos_new() {
1017        let config = OrphosConfig::default();
1018        let training = Training::default();
1019
1020        let orphos = TrainedOrphos::new(config, training);
1021
1022        assert!(orphos.training.is_some());
1023    }
1024
1025    #[test]
1026    fn test_train_single_genome_basic() {
1027        let mut orphos = UntrainedOrphos::new();
1028        let sequence = create_training_sequence();
1029        let encoded_sequence = create_encoded_sequence_for_test(&sequence);
1030
1031        let result = orphos.train_single_genome(&encoded_sequence);
1032
1033        assert!(result.is_ok());
1034        let trained = result.unwrap();
1035        assert!(trained.training.is_some());
1036    }
1037
1038    #[test]
1039    fn test_train_single_genome_quiet_mode() {
1040        let config = OrphosConfig {
1041            quiet: true,
1042            ..OrphosConfig::default()
1043        };
1044        let mut orphos = UntrainedOrphos::with_config(config).unwrap();
1045
1046        let sequence = create_training_sequence();
1047        let encoded_sequence = create_encoded_sequence_for_test(&sequence);
1048
1049        let result = orphos.train_single_genome(&encoded_sequence);
1050
1051        assert!(result.is_ok());
1052    }
1053
1054    #[test]
1055    fn test_train_single_genome_force_non_sd() {
1056        let config = OrphosConfig {
1057            force_non_sd: true,
1058            ..OrphosConfig::default()
1059        };
1060        let mut orphos = UntrainedOrphos::with_config(config).unwrap();
1061
1062        let sequence = create_training_sequence();
1063        let encoded_sequence = create_encoded_sequence_for_test(&sequence);
1064
1065        let result = orphos.train_single_genome(&encoded_sequence);
1066
1067        assert!(result.is_ok());
1068        let trained = result.unwrap();
1069        let training = trained.training.unwrap();
1070        assert!(!training.uses_shine_dalgarno); // Should be forced to false
1071    }
1072
1073    #[test]
1074    fn test_trained_orphos_find_genes_single() {
1075        // First create a trained orphos
1076        let mut orphos = UntrainedOrphos::new();
1077        let sequence = create_training_sequence();
1078        let encoded_sequence = create_encoded_sequence_for_test(&sequence);
1079
1080        let trained = orphos.train_single_genome(&encoded_sequence).unwrap();
1081
1082        // Now test gene finding
1083        let test_seq = create_test_sequence();
1084        let test_encoded_sequence = create_encoded_sequence_for_test(&test_seq);
1085
1086        let result = trained.find_genes_single(&test_encoded_sequence);
1087
1088        assert!(result.is_ok());
1089        let _genes = result.unwrap();
1090        // Should find genes in the sequence - number could be 0 for very short sequences
1091    }
1092
1093    #[test]
1094    fn test_trained_orphos_find_genes_without_training() {
1095        let config = OrphosConfig::default();
1096        let orphos = TrainedOrphos {
1097            config,
1098            training: None,
1099            _state: PhantomData,
1100        };
1101
1102        let test_seq = create_test_sequence();
1103        let test_encoded_sequence = create_encoded_sequence_for_test(&test_seq);
1104
1105        let result = orphos.find_genes_single(&test_encoded_sequence);
1106
1107        assert!(result.is_err());
1108        if let Err(OrphosError::InvalidSequence(msg)) = result {
1109            assert!(msg.contains("not trained"));
1110        } else {
1111            panic!("Expected InvalidSequence error");
1112        }
1113    }
1114
1115    #[test]
1116    fn test_orphos_analyzer_new() {
1117        let config = OrphosConfig::default();
1118        let analyzer = OrphosAnalyzer::new(config.clone());
1119
1120        assert_eq!(analyzer.config.metagenomic, config.metagenomic);
1121        assert_eq!(analyzer.config.closed_ends, config.closed_ends);
1122    }
1123
1124    #[test]
1125    fn test_analyze_sequence_basic() {
1126        let config = OrphosConfig::default();
1127        let analyzer = OrphosAnalyzer::new(config);
1128
1129        let sequence =
1130            "ATGAAACGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTAAATAG".repeat(300);
1131
1132        let result = analyzer.analyze_sequence(&sequence, None);
1133        assert!(result.is_ok());
1134
1135        let analysis = result.unwrap();
1136        assert_eq!(analysis.sequence_info.header, "Orphos_Seq_1");
1137        assert_eq!(analysis.sequence_info.length, sequence.len());
1138        assert!(
1139            analysis.sequence_info.gc_content >= 0.0 && analysis.sequence_info.gc_content <= 1.0
1140        );
1141        assert!(analysis.metagenomic_model.is_none());
1142    }
1143
1144    #[test]
1145    fn test_analyze_sequence_with_header() {
1146        let config = OrphosConfig::default();
1147        let analyzer = OrphosAnalyzer::new(config);
1148
1149        let sequence =
1150            "ATGAAACGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTAAATAG".repeat(300);
1151        let header = Some("test_sequence".to_string());
1152
1153        let result = analyzer.analyze_sequence(&sequence, header);
1154        assert!(result.is_ok());
1155
1156        let analysis = result.unwrap();
1157        assert_eq!(analysis.sequence_info.header, "test_sequence");
1158    }
1159
1160    #[test]
1161    fn test_analyze_sequence_bytes() {
1162        let config = OrphosConfig::default();
1163        let analyzer = OrphosAnalyzer::new(config);
1164
1165        let sequence =
1166            "ATGAAACGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTAAATAG".repeat(300);
1167        let seq_bytes = sequence.as_bytes();
1168        let header = "test_sequence".to_string();
1169        let description = Some("Test description".to_string());
1170
1171        let result =
1172            analyzer.analyze_sequence_bytes(seq_bytes, header.clone(), description.clone());
1173        assert!(result.is_ok());
1174
1175        let analysis = result.unwrap();
1176        assert_eq!(analysis.sequence_info.header, header);
1177        assert_eq!(analysis.sequence_info.description, description);
1178        assert_eq!(analysis.sequence_info.length, seq_bytes.len());
1179    }
1180
1181    #[test]
1182    fn test_analyze_sequence_metagenomic_config() {
1183        let config = OrphosConfig {
1184            metagenomic: true,
1185            ..OrphosConfig::default()
1186        };
1187        let analyzer = OrphosAnalyzer::new(config);
1188
1189        let sequence =
1190            "ATGAAACGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTAAATAG".repeat(300);
1191
1192        let result = analyzer.analyze_sequence(&sequence, None);
1193        assert!(result.is_ok());
1194
1195        let analysis = result.unwrap();
1196        assert!(analysis.metagenomic_model.is_some());
1197        // Metagenomic model string format: "id|name|domain|gc_percent|transl_table|uses_sd"
1198        let model = analysis.metagenomic_model.unwrap();
1199        assert!(
1200            model.contains("|"),
1201            "Expected metagenomic model description with | separator"
1202        );
1203    }
1204
1205    // #[test]
1206    // fn test_encode_sequence_basic() {
1207    //     let config = OrphosConfig::default();
1208    //     let analyzer = OrphosAnalyzer::new(config);
1209
1210    //     let sequence = b"ATCG";
1211    //     let mut encoded = vec![0u8; (sequence.len() * 2).div_ceil(8)];
1212    //     let mut unknown = vec![0u8; sequence.len().div_ceil(8)];
1213    //     let mut masks = Vec::new();
1214
1215    //     let result = analyzer.encode_sequence(sequence, &mut encoded, &mut unknown, &mut masks);
1216    //     assert!(result.is_ok());
1217
1218    //     let gc_content = result.unwrap();
1219    //     assert!((0.0..=1.0).contains(&gc_content));
1220    // }
1221
1222    // #[test]
1223    // fn test_encode_sequence_with_mask_n_runs() {
1224    //     let config = OrphosConfig {
1225    //         mask_n_runs: true,
1226    //         ..OrphosConfig::default()
1227    //     };
1228    //     let analyzer = OrphosAnalyzer::new(config);
1229
1230    //     let sequence = b"ATCGNNNNGCAT";
1231    //     let mut encoded = vec![0u8; (sequence.len() * 2).div_ceil(8)];
1232    //     let mut unknown = vec![0u8; sequence.len().div_ceil(8)];
1233    //     let mut masks = Vec::new();
1234
1235    //     let result = analyzer.encode_sequence(sequence, &mut encoded, &mut unknown, &mut masks);
1236    //     assert!(result.is_ok());
1237
1238    //     // N-run masking behavior depends on implementation - might not always create masks
1239    //     // So we just check that it doesn't crash
1240    // }
1241
1242    #[test]
1243    fn test_analyze_fasta_file_not_found() {
1244        let config = OrphosConfig::default();
1245        let analyzer = OrphosAnalyzer::new(config);
1246
1247        let result = analyzer.analyze_fasta_file("nonexistent_file.fa");
1248        assert!(result.is_err());
1249    }
1250
1251    #[test]
1252    fn test_analyze_fasta_file_metagenomic() {
1253        let config = OrphosConfig {
1254            metagenomic: true,
1255            ..OrphosConfig::default()
1256        };
1257        let analyzer = OrphosAnalyzer::new(config);
1258
1259        // Create a temporary FASTA file
1260        let fasta_content = ">test_seq\nATCG\n";
1261        let temp_dir = env::temp_dir();
1262        let temp_file = temp_dir.join("test_metagenomic.fa");
1263        fs::write(&temp_file, fasta_content).unwrap();
1264
1265        let result = analyzer.analyze_fasta_file(&temp_file);
1266        assert!(result.is_ok());
1267
1268        let results = result.unwrap();
1269        assert_eq!(results.len(), 1); // One result per sequence in the file
1270        assert!(results[0].genes.is_empty()); // But no genes found in 4bp sequence
1271
1272        let _ = fs::remove_file(temp_file);
1273    }
1274
1275    #[test]
1276    fn test_analyze_fasta_file_single_genome() {
1277        let config = OrphosConfig::default();
1278        let analyzer = OrphosAnalyzer::new(config);
1279
1280        // Create a temporary FASTA file with a longer sequence for training
1281        let sequence =
1282            "ATGAAACGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTAAATAG".repeat(300);
1283        let fasta_content = format!(">test_seq\n{}\n", sequence);
1284        let temp_dir = env::temp_dir();
1285        let temp_file = temp_dir.join("test_single_genome.fa");
1286        fs::write(&temp_file, fasta_content).unwrap();
1287
1288        let result = analyzer.analyze_fasta_file(&temp_file);
1289        assert!(result.is_ok());
1290
1291        let results = result.unwrap();
1292        assert_eq!(results.len(), 1);
1293        assert_eq!(results[0].sequence_info.header, "test_seq");
1294
1295        let _ = fs::remove_file(temp_file);
1296    }
1297
1298    #[test]
1299    fn test_analyze_empty_sequence() {
1300        let config = OrphosConfig::default();
1301        let analyzer = OrphosAnalyzer::new(config);
1302
1303        let sequence = "";
1304        let result = analyzer.analyze_sequence(sequence, None);
1305
1306        assert!(result.is_err());
1307        if let Err(e) = result {
1308            // Should be an InvalidSequence error about being too short
1309            match e {
1310                OrphosError::InvalidSequence(msg) => {
1311                    assert!(msg.contains("too short"));
1312                }
1313                _ => panic!("Expected InvalidSequence error for empty sequence"),
1314            }
1315        }
1316    }
1317
1318    #[test]
1319    fn test_analyze_very_short_sequence() {
1320        let config = OrphosConfig::default();
1321        let analyzer = OrphosAnalyzer::new(config);
1322
1323        let sequence = "ATG"; // Very short sequence (3 bp)
1324        let result = analyzer.analyze_sequence(sequence, None);
1325
1326        assert!(result.is_err());
1327        if let Err(e) = result {
1328            // Should be an InvalidSequence error about being too short
1329            match e {
1330                OrphosError::InvalidSequence(msg) => {
1331                    assert!(msg.contains("too short"));
1332                }
1333                _ => panic!("Expected InvalidSequence error for very short sequence"),
1334            }
1335        }
1336    }
1337
1338    #[test]
1339    fn test_config_cloning() {
1340        let config1 = OrphosConfig::default();
1341        let orphos1 = UntrainedOrphos::with_config(config1.clone()).unwrap();
1342
1343        let config2 = orphos1.config.clone();
1344        let _orphos2 = UntrainedOrphos::with_config(config2).unwrap();
1345    }
1346
1347    #[test]
1348    fn test_debug_formatting() {
1349        let orphos = UntrainedOrphos::new();
1350        let debug_str = format!("{:?}", orphos);
1351        assert!(debug_str.contains("Orphos"));
1352        assert!(debug_str.contains("config"));
1353
1354        let analyzer = OrphosAnalyzer::new(OrphosConfig::default());
1355        let debug_str2 = format!("{:?}", analyzer);
1356        assert!(debug_str2.contains("OrphosAnalyzer"));
1357        assert!(debug_str2.contains("config"));
1358    }
1359
1360    #[test]
1361    fn test_type_aliases() {
1362        // Test that type aliases work correctly
1363        let _untrained: UntrainedOrphos = UntrainedOrphos::new();
1364        let _trained: TrainedOrphos =
1365            TrainedOrphos::new(OrphosConfig::default(), Training::default());
1366
1367        assert_eq!(
1368            std::any::type_name::<UntrainedOrphos>(),
1369            std::any::type_name::<Orphos<Untrained>>()
1370        );
1371        assert_eq!(
1372            std::any::type_name::<TrainedOrphos>(),
1373            std::any::type_name::<Orphos<Trained>>()
1374        );
1375    }
1376
1377    #[test]
1378    fn test_training_state_phantom_data() {
1379        let untrained = UntrainedOrphos::new();
1380        let trained = TrainedOrphos::new(OrphosConfig::default(), Training::default());
1381
1382        assert_eq!(std::mem::size_of_val(&untrained._state), 0);
1383        assert_eq!(std::mem::size_of_val(&trained._state), 0);
1384    }
1385
1386    #[test]
1387    fn test_analyzer_multiple_sequences() {
1388        let config = OrphosConfig::default();
1389        let analyzer = OrphosAnalyzer::new(config);
1390
1391        let sequence1 =
1392            "ATGAAACGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTCGTAAATAG".repeat(150);
1393        let sequence2 =
1394            "ATGCCCGGGAAATTTCCCGGGAAATTTCCCGGGAAATTTCCCGGGAAATTTCCCGGGAAATAG".repeat(200);
1395
1396        let result1 = analyzer.analyze_sequence(&sequence1, Some("seq1".to_string()));
1397        let result2 = analyzer.analyze_sequence(&sequence2, Some("seq2".to_string()));
1398
1399        assert!(result1.is_ok());
1400        assert!(result2.is_ok());
1401
1402        let analysis1 = result1.unwrap();
1403        let analysis2 = result2.unwrap();
1404
1405        assert_eq!(analysis1.sequence_info.header, "seq1");
1406        assert_eq!(analysis2.sequence_info.header, "seq2");
1407        assert_ne!(
1408            analysis1.sequence_info.length,
1409            analysis2.sequence_info.length
1410        );
1411    }
1412
1413    #[test]
1414    fn test_error_handling_edge_cases() {
1415        let config = OrphosConfig::default();
1416        let analyzer = OrphosAnalyzer::new(config);
1417
1418        // Test with sequence containing invalid characters
1419        let invalid_sequence = "ATCGXYZ123";
1420        let result = analyzer.analyze_sequence(invalid_sequence, None);
1421
1422        // Should either handle gracefully or return appropriate error
1423        let _ = result;
1424    }
1425}