rustyhmmer 0.1.1

Pure-Rust HMMER3 hmmsearch with byte-identical --tblout output
Documentation
use crate::pipeline::{dombias_bits, Hit, Model};
use crate::seqio::Seq;
use rayon::prelude::*;

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Cutoff {
    GatheringGa,
    TrustedTc,
    NoiseNc,
    Evalue(f64),
}

#[derive(Clone, Debug)]
pub struct HmmHit {
    pub query_name: String,
    pub query_acc: String,
    pub model_len: usize,
    pub target_name: String,
    pub seq_score: f32,
    pub seq_bias: f32,
    pub seq_evalue: f64,
    pub best_dom_score: f32,
    pub best_dom_bias: f32,
    pub best_dom_evalue: f64,
    pub env_from: i64,
    pub env_to: i64,
    pub n_domains: i32,
}

pub struct HmmAnnotator {
    models: Vec<Model>,
    cutoff: Cutoff,
}

impl HmmAnnotator {
    pub fn from_hmm_file(path: &str) -> Result<Self, String> {
        let hmms = crate::hmmfile::P7Hmm::read_all(path).map_err(|e| e.to_string())?;
        if hmms.is_empty() {
            return Err(format!("no models in {path}"));
        }
        Ok(Self {
            models: hmms.into_iter().map(Model::new).collect(),
            cutoff: Cutoff::Evalue(10.0),
        })
    }

    pub fn from_models(models: Vec<Model>) -> Self {
        Self {
            models,
            cutoff: Cutoff::Evalue(10.0),
        }
    }

    pub fn with_cutoff(mut self, cutoff: Cutoff) -> Self {
        self.cutoff = cutoff;
        self
    }

    pub fn len(&self) -> usize {
        self.models.len()
    }
    pub fn is_empty(&self) -> bool {
        self.models.is_empty()
    }

    pub fn search(&self, seqs: &[Seq]) -> Vec<HmmHit> {
        let z = seqs.len() as f64;
        let mut out = Vec::new();
        for model in &self.models {
            let seq_bit_t: Option<f64> = match self.cutoff {
                Cutoff::GatheringGa => model.hmm.cutoffs.ga.map(|(t1, _)| t1),
                Cutoff::TrustedTc => model.hmm.cutoffs.tc.map(|(t1, _)| t1),
                Cutoff::NoiseNc => model.hmm.cutoffs.nc.map(|(t1, _)| t1),
                Cutoff::Evalue(_) => None,
            };
            if !matches!(self.cutoff, Cutoff::Evalue(_)) && seq_bit_t.is_none() {
                continue;
            }

            let hits: Vec<Hit> = seqs
                .par_iter()
                .filter_map(|s| model.search_one(s))
                .collect();

            let qname = &model.hmm.name;
            let qacc = model.hmm.acc.clone().unwrap_or_default();
            for h in &hits {
                let reportable = match self.cutoff {
                    Cutoff::Evalue(e) => h.lnp.exp() * z <= e,
                    _ => (h.score as f64) >= seq_bit_t.unwrap(),
                };
                if !reportable {
                    continue;
                }
                let bd = &h.domains[h.best];
                out.push(HmmHit {
                    query_name: qname.clone(),
                    query_acc: qacc.clone(),
                    model_len: model.hmm.m,
                    target_name: h.name.clone(),
                    seq_score: h.score,
                    seq_bias: h.bias(),
                    seq_evalue: h.lnp.exp() * z,
                    best_dom_score: bd.bitscore,
                    best_dom_bias: dombias_bits(bd.dombias),
                    best_dom_evalue: bd.lnp.exp() * z,
                    env_from: bd.ienv,
                    env_to: bd.jenv,
                    n_domains: h.ndom,
                });
            }
        }
        out
    }
}