#[cfg(all(not(target_arch = "wasm32"), feature = "nn-scoring"))]
pub mod nn {
use std::sync::Arc;
use anyhow::{Context, Result};
use chematic::fp::{EcfpConfig, ecfp};
use tract_onnx::prelude::*;
use crate::chem_env::{Molecule, RetroRule, mol_from_smiles};
const ECFP_CONFIG: EcfpConfig = EcfpConfig {
radius: 2,
nbits: 2048,
use_chirality: false,
use_double_fold: false,
};
pub struct TemplateScorer {
model: Arc<TypedSimplePlan>,
pub top_k: usize,
pub rules_offset: usize,
}
impl TemplateScorer {
pub fn from_path(path: &str, top_k: usize, rules_offset: usize) -> Result<Self> {
let model = tract_onnx::onnx()
.model_for_path(path)
.with_context(|| format!("failed to load ONNX model from {path}"))?
.with_input_fact(
0,
InferenceFact::dt_shape(f32::datum_type(), [1usize, 2048usize]),
)?
.into_optimized()
.context("failed to optimize ONNX model")?
.into_runnable()
.context("failed to create runnable ONNX plan")?;
Ok(Self {
model,
top_k,
rules_offset,
})
}
fn fingerprint(mol: &Molecule) -> Vec<f32> {
let bv = ecfp(mol, &ECFP_CONFIG);
(0..2048)
.map(|i| if bv.get(i) { 1.0_f32 } else { 0.0_f32 })
.collect()
}
pub fn top_k_indices(&self, target_smiles: &str, n_rules: usize) -> Vec<usize> {
let offset = self.rules_offset.min(n_rules);
let n_file = n_rules - offset;
let fallback = || (0..n_rules).collect::<Vec<_>>();
if n_file == 0 {
return fallback();
}
let Ok(mol) = mol_from_smiles(target_smiles) else {
return fallback();
};
let bits = Self::fingerprint(&mol);
let arr = match tract_ndarray::Array2::<f32>::from_shape_vec((1, 2048), bits) {
Ok(a) => a,
Err(_) => return fallback(),
};
let input: TVec<TValue> = tvec![arr.into_tvalue()];
let outputs = match self.model.run(input) {
Ok(o) => o,
Err(_) => return fallback(),
};
let scores: Vec<f32> = match outputs[0].to_plain_array_view::<f32>() {
Ok(v) => v.iter().copied().collect(),
Err(_) => return fallback(),
};
let k = self.top_k.min(n_file).min(scores.len());
let mut file_indices: Vec<usize> = (0..scores.len().min(n_file)).collect();
file_indices.sort_by(|&a, &b| {
scores[b]
.partial_cmp(&scores[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
file_indices.truncate(k);
let mut result: Vec<usize> = (0..offset).collect();
result.extend(file_indices.iter().map(|&i| offset + i));
result
}
pub fn filter_rules<'a>(
&self,
rules: &'a [RetroRule],
target_smiles: &str,
) -> Vec<&'a RetroRule> {
self.top_k_indices(target_smiles, rules.len())
.into_iter()
.filter_map(|i| rules.get(i))
.collect()
}
}
}
#[cfg(not(all(not(target_arch = "wasm32"), feature = "nn-scoring")))]
pub mod nn {}