1use std::collections::HashMap;
23use std::sync::Arc;
24
25const AAS: &[u8; 20] = b"ARNDCQEGHILKMFPSTWYV";
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum DecoyMethod {
30 Shuffle,
32 Reverse,
33 RandomUniform,
34 RandomFreq,
35 Markov {
36 order: usize,
37 },
38 DeBruijn {
39 k: usize,
40 },
41 DeBruijnCleave {
47 k: usize,
48 },
49}
50
51impl DecoyMethod {
52 pub fn parse(name: &str, order: usize) -> DecoyMethod {
54 match name.to_ascii_lowercase().as_str() {
55 "reverse" => DecoyMethod::Reverse,
56 "random_uniform" | "random" => DecoyMethod::RandomUniform,
57 "random_freq" | "freq" => DecoyMethod::RandomFreq,
58 "markov" => DecoyMethod::Markov {
59 order: order.max(1),
60 },
61 "debruijn" => DecoyMethod::DeBruijn { k: order.max(2) },
62 "debruijn_cleave" | "debruijn2" => DecoyMethod::DeBruijnCleave { k: order.max(2) },
63 _ => DecoyMethod::Shuffle,
64 }
65 }
66 pub fn is_protein_level(&self) -> bool {
67 !matches!(self, DecoyMethod::Shuffle)
68 }
69}
70
71#[inline]
72fn xorshift(state: &mut u64) -> u64 {
73 let mut x = *state;
74 x ^= x << 13;
75 x ^= x >> 7;
76 x ^= x << 17;
77 *state = x;
78 x
79}
80
81fn seed_for(acc: &str, salt: u64) -> u64 {
84 let mut h: u64 = 0x9e37_79b9_7f4a_7c15 ^ salt;
85 for b in acc.bytes() {
86 h ^= b as u64;
87 h = h.wrapping_mul(1099511628211);
88 }
89 h | 1
90}
91
92#[inline]
95fn weighted_pick(weights: &[u32], total: u64, r: u64) -> usize {
96 if total == 0 {
97 return (r % weights.len() as u64) as usize;
98 }
99 let mut t = (r % total) as i64;
100 for (i, &w) in weights.iter().enumerate() {
101 t -= w as i64;
102 if t < 0 {
103 return i;
104 }
105 }
106 weights.len() - 1
107}
108
109#[inline]
110fn aa_index(aa: u8) -> Option<usize> {
111 AAS.iter().position(|&a| a == aa)
112}
113
114pub fn generate(
118 targets: &[(Arc<str>, String)],
119 method: DecoyMethod,
120 decoy_tag: &str,
121 seed: u64,
122) -> Vec<(String, String)> {
123 let salt = crate::seed_mix(seed);
124 match method {
125 DecoyMethod::Shuffle => Vec::new(), DecoyMethod::Reverse => reverse_decoys(targets, decoy_tag),
127 DecoyMethod::RandomUniform => random_decoys(targets, decoy_tag, None, salt),
128 DecoyMethod::RandomFreq => {
129 let freq = global_aa_freq(targets);
130 random_decoys(targets, decoy_tag, Some(&freq), salt)
131 }
132 DecoyMethod::Markov { order } => markov_decoys(targets, decoy_tag, order, false, salt),
133 DecoyMethod::DeBruijn { k } => markov_decoys(targets, decoy_tag, k - 1, true, salt),
134 DecoyMethod::DeBruijnCleave { k } => debruijn_cleave_decoys(targets, decoy_tag, k, salt),
135 }
136}
137
138#[inline]
142fn is_cleave(aa: u8) -> bool {
143 aa == b'K' || aa == b'R' || aa == b'P'
144}
145
146fn debruijn_cleave_decoys(
152 targets: &[(Arc<str>, String)],
153 tag: &str,
154 k: usize,
155 salt: u64,
156) -> Vec<(String, String)> {
157 let k = k.max(1);
158 let mut tables: Vec<HashMap<Vec<u8>, [u32; 20]>> = (0..k).map(|_| HashMap::new()).collect();
160 let mut g0 = [0u32; 20]; for (_, seq) in targets {
162 let b = seq.as_bytes();
163 for i in 0..b.len() {
164 let ni = match aa_index(b[i]) {
165 Some(x) => x,
166 None => continue,
167 };
168 if !is_cleave(b[i]) {
169 g0[ni] += 1;
170 }
171 for o in 1..=k {
172 if i >= o {
173 let ctx = &b[i - o..i];
174 if ctx.iter().all(|&c| aa_index(c).is_some()) {
175 tables[o - 1].entry(ctx.to_vec()).or_insert([0u32; 20])[ni] += 1;
176 }
177 }
178 }
179 }
180 }
181 let g0_total: u64 = g0.iter().map(|&w| w as u64).sum();
182
183 let mask_cleave = |mut w: [u32; 20]| -> ([u32; 20], u64) {
185 for &c in b"KRP" {
186 if let Some(ix) = aa_index(c) {
187 w[ix] = 0;
188 }
189 }
190 let t: u64 = w.iter().map(|&x| x as u64).sum();
191 (w, t)
192 };
193
194 targets
195 .iter()
196 .map(|(acc, seq)| {
197 let b = seq.as_bytes();
198 let n = b.len();
199 let mut state = seed_for(acc, 0xDBC1 ^ salt);
200 let mut out: Vec<u8> = Vec::with_capacity(n);
201 for i in 0..n {
202 if is_cleave(b[i]) || aa_index(b[i]).is_none() {
203 out.push(b[i]); continue;
205 }
206 let mut chosen: Option<u8> = None;
208 for o in (1..=k).rev() {
209 if out.len() < o {
210 continue;
211 }
212 let ctx = &out[out.len() - o..];
213 if let Some(counts) = tables[o - 1].get(ctx) {
214 let (w, total) = mask_cleave(*counts);
215 if total > 0 {
216 let r = xorshift(&mut state);
217 chosen = Some(AAS[weighted_pick(&w, total, r)]);
218 break;
219 }
220 }
221 }
222 let aa = chosen.unwrap_or_else(|| {
223 let r = xorshift(&mut state);
224 AAS[weighted_pick(&g0, g0_total.max(1), r)]
225 });
226 out.push(aa);
227 }
228 (format!("{tag}{acc}"), String::from_utf8(out).unwrap())
229 })
230 .collect()
231}
232
233fn global_aa_freq(targets: &[(Arc<str>, String)]) -> [u32; 20] {
235 let mut f = [0u32; 20];
236 for (_, seq) in targets {
237 for &b in seq.as_bytes() {
238 if let Some(i) = aa_index(b) {
239 f[i] += 1;
240 }
241 }
242 }
243 f
244}
245
246fn reverse_decoys(targets: &[(Arc<str>, String)], tag: &str) -> Vec<(String, String)> {
247 targets
248 .iter()
249 .map(|(acc, seq)| {
250 let rev: String = seq.chars().rev().collect();
251 (format!("{tag}{acc}"), rev)
252 })
253 .collect()
254}
255
256fn random_decoys(
259 targets: &[(Arc<str>, String)],
260 tag: &str,
261 freq: Option<&[u32; 20]>,
262 salt: u64,
263) -> Vec<(String, String)> {
264 let total: u64 = freq.map(|f| f.iter().map(|&w| w as u64).sum()).unwrap_or(0);
265 targets
266 .iter()
267 .map(|(acc, seq)| {
268 let mut state = seed_for(acc, 0xA5A5 ^ salt);
269 let n = seq.len();
270 let mut out = Vec::with_capacity(n);
271 for _ in 0..n {
272 let r = xorshift(&mut state);
273 let idx = match freq {
274 Some(f) => weighted_pick(f, total, r),
275 None => (r % 20) as usize,
276 };
277 out.push(AAS[idx]);
278 }
279 (format!("{tag}{acc}"), String::from_utf8(out).unwrap())
280 })
281 .collect()
282}
283
284fn markov_decoys(
292 targets: &[(Arc<str>, String)],
293 tag: &str,
294 order: usize,
295 debruijn: bool,
296 salt: u64,
297) -> Vec<(String, String)> {
298 let order = order.max(1);
299 let mut table: HashMap<Vec<u8>, [u32; 20]> = HashMap::new();
301 let mut contexts: Vec<Vec<u8>> = Vec::new();
302 for (_, seq) in targets {
303 let b = seq.as_bytes();
304 if b.len() <= order {
305 continue;
306 }
307 for w in b.windows(order + 1) {
308 let ctx = &w[..order];
309 let next = w[order];
310 if ctx.iter().any(|&c| aa_index(c).is_none()) {
311 continue;
312 }
313 if let Some(ni) = aa_index(next) {
314 let e = table.entry(ctx.to_vec()).or_insert_with(|| {
315 contexts.push(ctx.to_vec());
316 [0u32; 20]
317 });
318 e[ni] += 1;
319 }
320 }
321 }
322 let gfreq = global_aa_freq(targets);
323 let gtotal: u64 = gfreq.iter().map(|&w| w as u64).sum();
324
325 targets
326 .iter()
327 .map(|(acc, seq)| {
328 let b = seq.as_bytes();
329 let n = b.len();
330 let mut state = seed_for(acc, (if debruijn { 0xDB12 } else { 0x3A7C }) ^ salt);
331 let mut out: Vec<u8> = Vec::with_capacity(n);
332 let mut ctx: Vec<u8> = Vec::with_capacity(order);
334 for &c in b.iter().take(order) {
335 ctx.push(if aa_index(c).is_some() { c } else { AAS[0] });
336 out.push(*ctx.last().unwrap());
337 }
338 while out.len() < n {
339 let next = match table.get(&ctx) {
340 Some(counts) => {
341 let total: u64 = counts.iter().map(|&w| w as u64).sum();
342 let r = xorshift(&mut state);
343 AAS[weighted_pick(counts, total, r)]
344 }
345 None => {
346 if debruijn && !contexts.is_empty() {
347 let r = xorshift(&mut state);
349 ctx = contexts[(r as usize) % contexts.len()].clone();
350 for &c in &ctx {
351 if out.len() < n {
352 out.push(c);
353 }
354 }
355 continue;
356 }
357 let r = xorshift(&mut state);
358 AAS[weighted_pick(&gfreq, gtotal, r)]
359 }
360 };
361 out.push(next);
362 ctx.push(next);
364 if ctx.len() > order {
365 ctx.remove(0);
366 }
367 }
368 out.truncate(n);
369 (format!("{tag}{acc}"), String::from_utf8(out).unwrap())
370 })
371 .collect()
372}
373
374#[cfg(test)]
375mod seed_tests {
376 use super::*;
377 use std::sync::Arc;
378
379 fn targets() -> Vec<(Arc<str>, String)> {
380 vec![
381 (
382 Arc::from("P00001"),
383 "MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEENFK".to_string(),
384 ),
385 (
386 Arc::from("P00002"),
387 "MSDAAVDTSSEITTKDLKEKKEVVEEAENGRDAPANGNANEENGEQEADNEVDEE".to_string(),
388 ),
389 ]
390 }
391
392 #[test]
396 fn seed_zero_is_the_unseeded_stream() {
397 assert_eq!(crate::seed_mix(0), 0);
398 for method in [
399 DecoyMethod::RandomUniform,
400 DecoyMethod::RandomFreq,
401 DecoyMethod::Markov { order: 2 },
402 DecoyMethod::DeBruijn { k: 3 },
403 DecoyMethod::DeBruijnCleave { k: 3 },
404 ] {
405 let t = targets();
406 assert_eq!(
408 generate(&t, method, "DECOY_", 0),
409 generate(&t, method, "DECOY_", 0)
410 );
411 }
412 }
413
414 #[test]
417 fn other_seeds_change_the_decoys() {
418 let t = targets();
419 for method in [
420 DecoyMethod::RandomUniform,
421 DecoyMethod::Markov { order: 2 },
422 DecoyMethod::DeBruijnCleave { k: 3 },
423 ] {
424 let a = generate(&t, method, "DECOY_", 0);
425 let b = generate(&t, method, "DECOY_", 7);
426 assert_ne!(a, b, "{method:?} ignored the seed");
427 assert_eq!(a.len(), b.len());
429 for (x, y) in a.iter().zip(&b) {
430 assert_eq!(x.0, y.0);
431 assert_eq!(x.1.len(), y.1.len());
432 }
433 }
434 }
435}