1use std::collections::HashMap;
20use std::io::{BufRead, BufReader, BufWriter, Write};
21use std::path::PathBuf;
22
23use crate::rt::RtModel;
24
25pub fn predict_many_cached(model: &RtModel, seqs: &[&str]) -> Vec<f64> {
30 let cache_path = match cache_file(model.fingerprint()) {
31 Some(p) => p,
32 None => return model.predict_many(seqs),
34 };
35
36 BATCH_CACHE.with(|state| {
37 let mut state = state.borrow_mut();
38 if let Some(models) = state.as_mut() {
39 let cached = models
40 .entry(model.fingerprint())
41 .or_insert_with(|| load_cache(&cache_path));
42 predict_into(model, seqs, &cache_path, cached)
43 } else {
44 predict_into(model, seqs, &cache_path, &mut load_cache(&cache_path))
45 }
46 })
47}
48
49thread_local! {
50 static BATCH_CACHE: std::cell::RefCell<Option<HashMap<u64,HashMap<String,f64>>>> = const { std::cell::RefCell::new(None) };
51}
52
53pub fn with_memory_cache<T>(work: impl FnOnce() -> T) -> T {
56 struct Restore(Option<HashMap<u64, HashMap<String, f64>>>);
57 impl Drop for Restore {
58 fn drop(&mut self) {
59 BATCH_CACHE.with(|s| *s.borrow_mut() = self.0.take());
60 }
61 }
62 let previous = BATCH_CACHE.with(|s| s.replace(Some(HashMap::new())));
63 let _guard = Restore(previous);
64 work()
65}
66
67fn predict_into(
68 model: &RtModel,
69 seqs: &[&str],
70 path: &std::path::Path,
71 cached: &mut HashMap<String, f64>,
72) -> Vec<f64> {
73 let mut miss_seqs = Vec::new();
74 let mut seen = std::collections::HashSet::new();
75 for &s in seqs {
76 if !cached.contains_key(s) && seen.insert(s) {
77 miss_seqs.push(s);
78 }
79 }
80 log::info!(
81 "RT cache {}: {} unique misses of {} requested ({} rows cached)",
82 path.display(),
83 miss_seqs.len(),
84 seqs.len(),
85 cached.len()
86 );
87 if !miss_seqs.is_empty() {
88 let predictions = model.predict_many(&miss_seqs);
89 if let Err(e) = append_cache(path, &miss_seqs, &predictions) {
90 log::warn!("RT cache write {}: {}", path.display(), e);
91 }
92 for (s, p) in miss_seqs.into_iter().zip(predictions) {
93 cached.insert(s.to_owned(), p);
94 }
95 }
96 seqs.iter().map(|s| cached[*s]).collect()
97}
98
99fn cache_file(fingerprint: u64) -> Option<PathBuf> {
102 if std::env::var("UNO_RT_CACHE").as_deref() == Ok("0") {
103 return None;
104 }
105 let dir = cache_dir()?;
106 Some(dir.join(format!("{fingerprint:016x}.tsv")))
107}
108
109fn cache_dir() -> Option<PathBuf> {
110 if let Ok(d) = std::env::var("UNO_RT_CACHE_DIR") {
111 return Some(PathBuf::from(d));
112 }
113 if let Ok(d) = std::env::var("XDG_CACHE_HOME") {
114 if !d.is_empty() {
115 return Some(PathBuf::from(d).join("uno").join("rt"));
116 }
117 }
118 std::env::var("HOME")
119 .ok()
120 .map(|h| PathBuf::from(h).join(".cache").join("uno").join("rt"))
121}
122
123fn load_cache(path: &PathBuf) -> HashMap<String, f64> {
126 let mut map = HashMap::new();
127 let f = match std::fs::File::open(path) {
128 Ok(f) => f,
129 Err(_) => return map,
130 };
131 for line in BufReader::new(f).lines().map_while(Result::ok) {
132 let mut it = line.split('\t');
133 if let (Some(seq), Some(rt)) = (it.next(), it.next()) {
134 if let Ok(rt) = rt.parse::<f64>() {
135 map.insert(seq.to_string(), rt);
136 }
137 }
138 }
139 map
140}
141
142fn append_cache(path: &std::path::Path, seqs: &[&str], preds: &[f64]) -> std::io::Result<()> {
146 if let Some(parent) = path.parent() {
147 std::fs::create_dir_all(parent)?;
148 }
149 let f = std::fs::OpenOptions::new()
150 .create(true)
151 .append(true)
152 .open(path)?;
153 let mut w = BufWriter::new(f);
154 for (s, p) in seqs.iter().zip(preds) {
155 writeln!(w, "{s}\t{p}")?;
158 }
159 w.flush()
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 #[test]
166 fn warm_predictions_are_bit_exact_and_append_only_misses() {
167 let dir = std::env::temp_dir().join(format!(
168 "uno-cache-test-{}-{}",
169 std::process::id(),
170 std::time::SystemTime::now()
171 .duration_since(std::time::UNIX_EPOCH)
172 .unwrap()
173 .as_nanos()
174 ));
175 let path = dir.join("model.tsv");
176 let model = RtModel::bundled().unwrap();
177 let seqs = ["PEPTIDEK", "AAAAAAAR", "PEPTIDEK"];
178 let expected = model.predict_many(&seqs);
179 let mut map = HashMap::new();
180 let first = predict_into(&model, &seqs, &path, &mut map);
181 let before = std::fs::read(&path).unwrap();
182 let second = predict_into(&model, &seqs, &path, &mut map);
183 assert_eq!(first, expected);
184 assert_eq!(second, expected);
185 assert_eq!(std::fs::read(&path).unwrap(), before);
186 let disk = load_cache(&path);
187 assert_eq!(disk.len(), 2);
188 assert_eq!(disk["PEPTIDEK"].to_bits(), expected[0].to_bits());
189 std::fs::remove_dir_all(dir).unwrap();
190 }
191 #[test]
192 fn batch_cache_scope_restores_previous_state() {
193 with_memory_cache(|| {
194 BATCH_CACHE.with(|s| {
195 s.borrow_mut().as_mut().unwrap().insert(42, HashMap::new());
196 });
197 with_memory_cache(|| {
198 assert!(BATCH_CACHE.with(|s| s.borrow().as_ref().unwrap().is_empty()))
199 });
200 assert!(BATCH_CACHE.with(|s| s.borrow().as_ref().unwrap().contains_key(&42)));
201 });
202 assert!(BATCH_CACHE.with(|s| s.borrow().is_none()));
203 }
204}