Skip to content

Commit 46bdbc8

Browse files
authored
Update predict.rs
1 parent 9597867 commit 46bdbc8

1 file changed

Lines changed: 78 additions & 166 deletions

File tree

src/predict.rs

Lines changed: 78 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,200 +1,112 @@
1-
// src/predict.rs
2-
1+
//! This module handles the `predict` subcommand.
2+
//!
3+
//! Its primary responsibilities are:
4+
//! 1. Loading a unified model bundle from a single file.
5+
//! 2. Reading new genome sequences from a FASTA file.
6+
//! 3. Transforming these sequences into feature vectors in parallel.
7+
//! 4. Using the loaded model to predict the class for each sequence.
8+
//! 5. Writing the predictions to an output file.
9+
10+
use anyhow::{anyhow, Context, Result};
311
use clap::Parser;
4-
use chrono::Local;
512
use flate2::read::GzDecoder;
6-
use indicatif::ProgressBar;
13+
use indicatif::{ProgressBar, ProgressStyle};
14+
use log::info;
15+
use rayon::prelude::*;
716
use serde::{Deserialize, Serialize};
817
use smartcore::ensemble::random_forest_classifier::RandomForestClassifier;
918
use smartcore::linalg::basic::matrix::DenseMatrix;
1019
use std::collections::HashMap;
11-
use std::error::Error;
1220
use std::fs::File;
1321
use std::io::{BufRead, BufReader, Write};
14-
use std::time::Instant;
15-
16-
const DEFAULT_KMER_SIZE: usize = 6;
1722

18-
/// Converts a genomic sequence into overlapping k-mers.
19-
fn kmerize(sequence: &str, k: usize) -> String {
20-
if sequence.len() < k {
21-
return String::new();
22-
}
23-
(0..=sequence.len() - k)
24-
.map(|i| &sequence[i..i + k])
25-
.collect::<Vec<&str>>()
26-
.join(" ")
23+
#[derive(Serialize, Deserialize, Debug)]
24+
pub struct ModelConfig { pub kmer_size: usize, }
25+
#[derive(Serialize, Deserialize, Debug)]
26+
pub struct ModelBundle {
27+
pub config: ModelConfig,
28+
pub vectorizer: CountVectorizer,
29+
pub label_encoder: LabelEncoder,
30+
pub model: RandomForestClassifier<f64, usize, DenseMatrix<f64>, Vec<usize>>,
2731
}
2832

29-
/// A simple count vectorizer that splits texts on whitespace.
30-
#[derive(Serialize, Deserialize, Debug)]
31-
pub struct CountVectorizer {
32-
pub vocabulary: HashMap<String, usize>,
33-
pub feature_names: Vec<String>,
33+
#[derive(Parser, Debug)]
34+
pub struct PredictArgs {
35+
#[arg(short = 'i', long)] pub input: String,
36+
#[arg(short = 'm', long)] pub model: String,
37+
#[arg(short = 'o', long)] pub output: String,
38+
#[arg(short = 't', long)] pub threads: Option<usize>,
3439
}
3540

41+
#[derive(Serialize, Deserialize, Debug)]
42+
pub struct CountVectorizer { pub vocabulary: HashMap<String, usize>, #[allow(dead_code)] pub feature_names: Vec<String> }
3643
impl CountVectorizer {
37-
pub fn new() -> Self {
38-
Self {
39-
vocabulary: HashMap::new(),
40-
feature_names: Vec::new(),
41-
}
42-
}
43-
pub fn fit<T: AsRef<str>>(&mut self, texts: &[T]) {
44-
let mut freq: HashMap<String, usize> = HashMap::new();
45-
for text in texts {
46-
for token in text.as_ref().split_whitespace() {
47-
*freq.entry(token.to_string()).or_insert(0) += 1;
48-
}
49-
}
50-
let mut freq_vec: Vec<(String, usize)> = freq.into_iter().collect();
51-
freq_vec.sort_by(|a, b| b.1.cmp(&a.1));
52-
self.vocabulary = freq_vec
53-
.iter()
54-
.enumerate()
55-
.map(|(i, (token, _))| (token.clone(), i))
56-
.collect();
57-
self.feature_names = freq_vec.into_iter().map(|(token, _)| token).collect();
58-
}
5944
pub fn transform<T: AsRef<str> + Sync>(&self, texts: &[T]) -> Vec<Vec<f64>> {
60-
texts
61-
.iter()
62-
.map(|text| {
63-
let n_features = self.vocabulary.len();
64-
let mut counts = vec![0.0; n_features];
65-
for token in text.as_ref().split_whitespace() {
66-
if let Some(&idx) = self.vocabulary.get(token) {
67-
counts[idx] += 1.0;
68-
}
69-
}
70-
counts
71-
})
72-
.collect()
45+
texts.par_iter().map(|text| {
46+
let mut counts = vec![0.0; self.vocabulary.len()];
47+
for token in text.as_ref().split_whitespace() { if let Some(&idx) = self.vocabulary.get(token) { counts[idx] += 1.0; } }
48+
counts
49+
}).collect()
7350
}
7451
}
75-
76-
/// Label encoder that maps labels (strings) to numeric values.
7752
#[derive(Serialize, Deserialize, Debug)]
78-
pub struct LabelEncoder {
79-
pub label_to_int: HashMap<String, usize>,
80-
pub int_to_label: Vec<String>,
81-
}
53+
pub struct LabelEncoder { #[allow(dead_code)] pub label_to_int: HashMap<String, usize>, pub int_to_label: Vec<String> }
8254

83-
impl LabelEncoder {
84-
pub fn new() -> Self {
85-
Self {
86-
label_to_int: HashMap::new(),
87-
int_to_label: Vec::new(),
88-
}
89-
}
90-
pub fn fit<T: AsRef<str>>(&mut self, labels: &[T]) {
91-
for label in labels {
92-
let label_str = label.as_ref();
93-
if !self.label_to_int.contains_key(label_str) {
94-
let index = self.int_to_label.len();
95-
self.label_to_int.insert(label_str.to_string(), index);
96-
self.int_to_label.push(label_str.to_string());
97-
}
98-
}
99-
}
100-
pub fn transform<T: AsRef<str>>(&self, labels: &[T]) -> Vec<usize> {
101-
labels
102-
.iter()
103-
.map(|label| *self.label_to_int.get(label.as_ref()).unwrap())
104-
.collect()
105-
}
106-
}
107-
108-
/// Command-line arguments for the predict subcommand.
109-
#[derive(Parser, Debug)]
110-
pub struct PredictArgs {
111-
/// Input FASTA file (multi-FASTA; header in format "Lineage_sequenceID")
112-
#[arg(long)]
113-
pub fasta: String,
114-
/// Base name of the saved model (expects files: <model_base>_rf_model.bin.gz, etc.)
115-
#[arg(long)]
116-
pub model_base: String,
117-
/// Output file where predictions will be written.
118-
#[arg(long)]
119-
pub output: String,
120-
/// k-mer size (default is 6)
121-
#[arg(long, default_value_t = DEFAULT_KMER_SIZE)]
122-
pub kmer_size: usize,
55+
fn kmerize(sequence: &str, k: usize) -> String {
56+
if sequence.len() < k { return String::new(); }
57+
(0..=sequence.len() - k).map(|i| &sequence[i..i + k]).collect::<Vec<&str>>().join(" ")
12358
}
12459

125-
fn read_fasta_for_prediction(path: &str) -> Result<Vec<(String, String)>, Box<dyn Error>> {
60+
fn read_fasta_for_prediction(path: &str) -> Result<Vec<(String, String)>> {
12661
let file = File::open(path)?;
127-
let reader = BufReader::new(file);
128-
let pb = ProgressBar::new_spinner();
129-
pb.set_message("Processing FASTA records for prediction...");
62+
let file_size = file.metadata()?.len();
63+
let pb = ProgressBar::new(file_size);
64+
pb.set_style(ProgressStyle::default_bar()
65+
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")?
66+
.progress_chars("#>-"));
67+
68+
let reader = BufReader::new(pb.wrap_read(file));
13069
let mut records = Vec::new();
131-
let mut current_header = String::new();
132-
let mut current_seq = String::new();
70+
let mut current_header = String::new(); let mut current_seq = String::new();
71+
13372
for line in reader.lines() {
13473
let line = line?;
13574
if line.starts_with('>') {
136-
if !current_header.is_empty() {
137-
records.push((current_header.clone(), current_seq.clone()));
138-
pb.inc(1);
139-
}
140-
current_header = line.trim_start_matches('>').to_string();
141-
current_seq.clear();
142-
} else {
143-
current_seq.push_str(line.trim());
144-
}
145-
}
146-
if !current_header.is_empty() {
147-
records.push((current_header.clone(), current_seq.clone()));
148-
pb.inc(1);
75+
if !current_header.is_empty() { records.push((current_header.clone(), current_seq.clone())); }
76+
current_header = line.trim_start_matches('>').to_string(); current_seq.clear();
77+
} else { current_seq.push_str(line.trim()); }
14978
}
150-
pb.finish_with_message("Finished processing FASTA records.");
79+
if !current_header.is_empty() { records.push((current_header, current_seq)); }
80+
81+
pb.finish_with_message("FASTA reading complete");
15182
Ok(records)
15283
}
15384

154-
/// Main function for the predict subcommand.
155-
pub fn run(args: PredictArgs) -> Result<(), Box<dyn Error>> {
156-
println!("INFO: System start time: {}", Local::now().format("%Y-%m-%d %H:%M:%S"));
157-
let vectorizer_path = format!("{}_vectorizer.bin.gz", args.model_base);
158-
let label_encoder_path = format!("{}_label_encoder.bin.gz", args.model_base);
159-
let model_path = format!("{}_rf_model.bin.gz", args.model_base);
85+
pub fn run(args: PredictArgs) -> Result<()> {
86+
if let Some(n) = args.threads { rayon::ThreadPoolBuilder::new().num_threads(n).build_global()?; }
16087

161-
// Load artifacts.
162-
let vec_file = File::open(&vectorizer_path)?;
163-
let mut vec_decoder = GzDecoder::new(vec_file);
164-
let vectorizer: CountVectorizer = bincode::deserialize_from(&mut vec_decoder)?;
165-
println!("INFO: Loaded vectorizer from {}", vectorizer_path);
166-
167-
let label_file = File::open(&label_encoder_path)?;
168-
let mut label_decoder = GzDecoder::new(label_file);
169-
let label_encoder: LabelEncoder = bincode::deserialize_from(&mut label_decoder)?;
170-
println!("INFO: Loaded label encoder from {}", label_encoder_path);
171-
172-
let model_file = File::open(&model_path)?;
88+
info!("▶ Loading model bundle from {}", args.model);
89+
let model_file = File::open(&args.model)?;
17390
let mut model_decoder = GzDecoder::new(model_file);
174-
let model: RandomForestClassifier<f64, usize, DenseMatrix<f64>, Vec<usize>> =
175-
bincode::deserialize_from(&mut model_decoder)?;
176-
println!("INFO: Loaded model from {}", model_path);
177-
178-
println!("INFO: Reading input FASTA file: {}", args.fasta);
179-
let records = read_fasta_for_prediction(&args.fasta)?;
180-
if records.is_empty() {
181-
return Err("No records found in the input FASTA file.".into());
182-
}
183-
println!("INFO: Read {} records.", records.len());
184-
185-
let texts: Vec<String> = records.iter().map(|(_, seq)| kmerize(seq, args.kmer_size)).collect();
186-
let x_data = vectorizer.transform(&texts);
187-
let x_matrix = DenseMatrix::from_2d_vec(&x_data)
188-
.map_err(|_| "Failed to create feature matrix")?;
189-
190-
let predict_start = Instant::now();
191-
let y_pred = model.predict(&x_matrix)
192-
.map_err(|e| format!("Error during prediction: {:?}", e))?;
193-
println!("INFO: Prediction completed in {:.2} seconds.", predict_start.elapsed().as_secs_f32());
194-
195-
let default_prediction = String::from("Unknown");
91+
let bundle: ModelBundle = bincode::deserialize_from(&mut model_decoder)
92+
.context("Failed to deserialize the model bundle.")?;
93+
info!(" Model bundle loaded. Using k-mer size: {}", bundle.config.kmer_size);
94+
95+
info!("▶ Reading input FASTA file: {}", args.input);
96+
let records = read_fasta_for_prediction(&args.input)?;
97+
if records.is_empty() { return Err(anyhow!("No records found in the input FASTA file.")); }
98+
99+
info!("▶ Generating k-mers and transforming features...");
100+
let texts: Vec<String> = records.par_iter().map(|(_, seq)| kmerize(seq, bundle.config.kmer_size)).collect();
101+
let x_data = bundle.vectorizer.transform(&texts);
102+
let x_matrix = DenseMatrix::from_2d_vec(&x_data)?;
103+
104+
info!("▶ Predicting lineages...");
105+
let y_pred = bundle.model.predict(&x_matrix)?;
106+
107+
let default_prediction = "Unknown".to_string();
196108
let predictions: Vec<String> = y_pred.iter().map(|&class| {
197-
label_encoder.int_to_label.get(class).cloned().unwrap_or_else(|| default_prediction.clone())
109+
bundle.label_encoder.int_to_label.get(class).cloned().unwrap_or(default_prediction.clone())
198110
}).collect();
199111

200112
let mut output_file = File::create(&args.output)?;
@@ -203,7 +115,7 @@ pub fn run(args: PredictArgs) -> Result<(), Box<dyn Error>> {
203115
let pred = predictions.get(i).unwrap_or(&default_prediction);
204116
writeln!(output_file, "{}\t{}", header, pred)?;
205117
}
206-
println!("INFO: Predictions written to {}", args.output);
207-
println!("INFO: System finish time: {}", Local::now().format("%Y-%m-%d %H:%M:%S"));
118+
119+
info!("✅ Predictions written to {}", args.output);
208120
Ok(())
209121
}

0 commit comments

Comments
 (0)