use std::collections::HashMap;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::PathBuf;
use crate::rt::RtModel;
pub fn predict_many_cached(model: &RtModel, seqs: &[&str]) -> Vec<f64> {
let cache_path = match cache_file(model.fingerprint()) {
Some(p) => p,
None => return model.predict_many(seqs),
};
BATCH_CACHE.with(|state| {
let mut state = state.borrow_mut();
if let Some(models) = state.as_mut() {
let cached = models
.entry(model.fingerprint())
.or_insert_with(|| load_cache(&cache_path));
predict_into(model, seqs, &cache_path, cached)
} else {
predict_into(model, seqs, &cache_path, &mut load_cache(&cache_path))
}
})
}
thread_local! {
static BATCH_CACHE: std::cell::RefCell<Option<HashMap<u64,HashMap<String,f64>>>> = const { std::cell::RefCell::new(None) };
}
pub fn with_memory_cache<T>(work: impl FnOnce() -> T) -> T {
struct Restore(Option<HashMap<u64, HashMap<String, f64>>>);
impl Drop for Restore {
fn drop(&mut self) {
BATCH_CACHE.with(|s| *s.borrow_mut() = self.0.take());
}
}
let previous = BATCH_CACHE.with(|s| s.replace(Some(HashMap::new())));
let _guard = Restore(previous);
work()
}
fn predict_into(
model: &RtModel,
seqs: &[&str],
path: &std::path::Path,
cached: &mut HashMap<String, f64>,
) -> Vec<f64> {
let mut miss_seqs = Vec::new();
let mut seen = std::collections::HashSet::new();
for &s in seqs {
if !cached.contains_key(s) && seen.insert(s) {
miss_seqs.push(s);
}
}
log::info!(
"RT cache {}: {} unique misses of {} requested ({} rows cached)",
path.display(),
miss_seqs.len(),
seqs.len(),
cached.len()
);
if !miss_seqs.is_empty() {
let predictions = model.predict_many(&miss_seqs);
if let Err(e) = append_cache(path, &miss_seqs, &predictions) {
log::warn!("RT cache write {}: {}", path.display(), e);
}
for (s, p) in miss_seqs.into_iter().zip(predictions) {
cached.insert(s.to_owned(), p);
}
}
seqs.iter().map(|s| cached[*s]).collect()
}
fn cache_file(fingerprint: u64) -> Option<PathBuf> {
if std::env::var("UNO_RT_CACHE").as_deref() == Ok("0") {
return None;
}
let dir = cache_dir()?;
Some(dir.join(format!("{fingerprint:016x}.tsv")))
}
fn cache_dir() -> Option<PathBuf> {
if let Ok(d) = std::env::var("UNO_RT_CACHE_DIR") {
return Some(PathBuf::from(d));
}
if let Ok(d) = std::env::var("XDG_CACHE_HOME") {
if !d.is_empty() {
return Some(PathBuf::from(d).join("uno").join("rt"));
}
}
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".cache").join("uno").join("rt"))
}
fn load_cache(path: &PathBuf) -> HashMap<String, f64> {
let mut map = HashMap::new();
let f = match std::fs::File::open(path) {
Ok(f) => f,
Err(_) => return map,
};
for line in BufReader::new(f).lines().map_while(Result::ok) {
let mut it = line.split('\t');
if let (Some(seq), Some(rt)) = (it.next(), it.next()) {
if let Ok(rt) = rt.parse::<f64>() {
map.insert(seq.to_string(), rt);
}
}
}
map
}
fn append_cache(path: &std::path::Path, seqs: &[&str], preds: &[f64]) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
let mut w = BufWriter::new(f);
for (s, p) in seqs.iter().zip(preds) {
writeln!(w, "{s}\t{p}")?;
}
w.flush()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn warm_predictions_are_bit_exact_and_append_only_misses() {
let dir = std::env::temp_dir().join(format!(
"uno-cache-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let path = dir.join("model.tsv");
let model = RtModel::bundled().unwrap();
let seqs = ["PEPTIDEK", "AAAAAAAR", "PEPTIDEK"];
let expected = model.predict_many(&seqs);
let mut map = HashMap::new();
let first = predict_into(&model, &seqs, &path, &mut map);
let before = std::fs::read(&path).unwrap();
let second = predict_into(&model, &seqs, &path, &mut map);
assert_eq!(first, expected);
assert_eq!(second, expected);
assert_eq!(std::fs::read(&path).unwrap(), before);
let disk = load_cache(&path);
assert_eq!(disk.len(), 2);
assert_eq!(disk["PEPTIDEK"].to_bits(), expected[0].to_bits());
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn batch_cache_scope_restores_previous_state() {
with_memory_cache(|| {
BATCH_CACHE.with(|s| {
s.borrow_mut().as_mut().unwrap().insert(42, HashMap::new());
});
with_memory_cache(|| {
assert!(BATCH_CACHE.with(|s| s.borrow().as_ref().unwrap().is_empty()))
});
assert!(BATCH_CACHE.with(|s| s.borrow().as_ref().unwrap().contains_key(&42)));
});
assert!(BATCH_CACHE.with(|s| s.borrow().is_none()));
}
}