1use rand::RngExt;
18use rand::SeedableRng;
19use rand_xoshiro::Xoshiro256PlusPlus;
20use rayon::prelude::*;
21use std::collections::HashMap;
22
23const ALPHABET: [u8; 4] = [b'A', b'C', b'G', b'T'];
26const GC_LOW: f64 = 0.40;
27const GC_HIGH: f64 = 0.60;
28const MAX_HOMOPOLYMER: usize = 3;
29#[allow(dead_code)] const DEFAULT_TOEHOLD: usize = 7;
31const R_GAS: f64 = 1.987e-3; pub fn design_sequence(length: usize, seed: u64) -> Vec<u8> {
37 let mut rng = Xoshiro256PlusPlus::seed_from_u64(seed);
38 let mut seq = Vec::with_capacity(length);
39
40 for _ in 0..length * 10 {
41 if seq.len() >= length {
42 break;
43 }
44 let base = ALPHABET[rng.random_range(0..4)];
45
46 if seq.len() >= MAX_HOMOPOLYMER {
48 let tail = &seq[seq.len() - MAX_HOMOPOLYMER..];
49 if tail.iter().all(|&b| b == base) {
50 continue;
51 }
52 }
53
54 seq.push(base);
55
56 if seq.len() == length {
58 let gc = gc_content(&seq);
59 if !(GC_LOW..=GC_HIGH).contains(&gc) {
60 seq.clear();
61 }
62 }
63 }
64
65 while seq.len() < length {
67 let base = ALPHABET[rng.random_range(0..4)];
68 seq.push(base);
69 }
70 seq.truncate(length);
71 seq
72}
73
74pub fn design_orthogonal_set(count: usize, length: usize, seed: u64) -> Vec<Vec<u8>> {
76 let mut result = Vec::with_capacity(count);
77 for i in 0..count {
78 result.push(design_sequence(length, seed.wrapping_add(i as u64)));
79 }
80 result
81}
82
83#[inline]
84fn gc_content(seq: &[u8]) -> f64 {
85 if seq.is_empty() {
86 return 0.5;
87 }
88 let gc = seq.iter().filter(|&&b| b == b'G' || b == b'C').count();
89 gc as f64 / seq.len() as f64
90}
91
92pub fn complement(seq: &[u8]) -> Vec<u8> {
94 seq.iter()
95 .map(|&b| match b {
96 b'A' => b'T',
97 b'T' => b'A',
98 b'C' => b'G',
99 b'G' => b'C',
100 _ => b'N',
101 })
102 .collect()
103}
104
105pub fn reverse_complement(seq: &[u8]) -> Vec<u8> {
107 let mut rc = complement(seq);
108 rc.reverse();
109 rc
110}
111
112fn alignment_score(a: &[u8], b: &[u8]) -> usize {
116 let rc_b = reverse_complement(b);
117 longest_common_substring(a, &rc_b)
118}
119
120fn longest_common_substring(a: &[u8], b: &[u8]) -> usize {
121 let n = a.len();
122 let m = b.len();
123 let mut max_len = 0usize;
124 let mut prev = vec![0usize; m + 1];
125 let mut curr = vec![0usize; m + 1];
126
127 for i in 1..=n {
128 for j in 1..=m {
129 if a[i - 1] == b[j - 1] {
130 curr[j] = prev[j - 1] + 1;
131 if curr[j] > max_len {
132 max_len = curr[j];
133 }
134 } else {
135 curr[j] = 0;
136 }
137 }
138 std::mem::swap(&mut prev, &mut curr);
139 curr.iter_mut().for_each(|x| *x = 0);
140 }
141 max_len
142}
143
144pub fn check_cross_hybridization(
147 sequences: &[Vec<u8>],
148 threshold: usize,
149) -> Vec<(usize, usize, usize)> {
150 let n = sequences.len();
151 if n < 2 {
152 return vec![];
153 }
154
155 let pairs: Vec<(usize, usize)> = (0..n)
157 .flat_map(|i| (i + 1..n).map(move |j| (i, j)))
158 .collect();
159
160 pairs
162 .par_iter()
163 .filter_map(|&(i, j)| {
164 let score = alignment_score(&sequences[i], &sequences[j]);
165 if score >= threshold {
166 Some((i, j, score))
167 } else {
168 None
169 }
170 })
171 .collect()
172}
173
174#[derive(Clone, Copy, Debug)]
178pub enum DnaGateType {
179 And,
180 Or,
181 Not,
182 Threshold,
183 Mux,
184 Amplifier,
185 Buffer,
186 Nand,
187 Xor,
188}
189
190#[derive(Clone, Debug)]
192pub struct DnaGateSpec {
193 pub gate_type: DnaGateType,
194 pub input_names: Vec<String>,
195 pub output_name: String,
196 pub threshold: f64,
197 pub leak_rate: f64,
198}
199
200pub struct KineticConfig {
202 pub k_hyb: f64,
203 pub k_disp: f64,
204 pub temperature_c: f64,
205 pub max_conc: f64,
206 pub use_rk4: bool,
207}
208
209impl Default for KineticConfig {
210 fn default() -> Self {
211 Self {
212 k_hyb: 3e5,
213 k_disp: 1.0,
214 temperature_c: 37.0,
215 max_conc: 200.0,
216 use_rk4: true,
217 }
218 }
219}
220
221#[inline]
223fn arrhenius_scale(k_ref: f64, temperature_c: f64, ea_kcal: f64) -> f64 {
224 let t_ref = 310.15; let t_op = temperature_c + 273.15;
226 k_ref * (-(ea_kcal / R_GAS) * (1.0 / t_op - 1.0 / t_ref)).exp()
227}
228
229fn compute_k_eff(gate: &DnaGateSpec, inputs: &HashMap<String, f64>, config: &KineticConfig) -> f64 {
231 let k_hyb = arrhenius_scale(config.k_hyb, config.temperature_c, 15.0);
232 let k_disp = arrhenius_scale(config.k_disp, config.temperature_c, 15.0);
233
234 let k_eff = match gate.gate_type {
235 DnaGateType::And => {
236 let concs: Vec<f64> = gate
237 .input_names
238 .iter()
239 .map(|n| *inputs.get(n).unwrap_or(&0.0))
240 .collect();
241 let all_present = concs.iter().all(|&c| c > 0.0);
242 let min_c = concs.iter().cloned().fold(f64::MAX, f64::min);
243 k_hyb * min_c * 1e-9 * if all_present { 1.0 } else { 0.0 }
244 }
245 DnaGateType::Or => {
246 let concs: Vec<f64> = gate
247 .input_names
248 .iter()
249 .map(|n| *inputs.get(n).unwrap_or(&0.0))
250 .collect();
251 let max_c = concs.iter().cloned().fold(0.0f64, f64::max);
252 k_hyb * max_c * 1e-9
253 }
254 DnaGateType::Not => {
255 let inp = *inputs.get(&gate.input_names[0]).unwrap_or(&0.0);
256 k_disp * (1.0 - (inp / config.max_conc).min(1.0))
257 }
258 DnaGateType::Threshold => {
259 let inp = *inputs.get(&gate.input_names[0]).unwrap_or(&0.0);
260 let excess = (inp - gate.threshold * config.max_conc).max(0.0);
261 k_hyb * excess * 1e-9
262 }
263 DnaGateType::Mux => {
264 let sel = *inputs.get(&gate.input_names[0]).unwrap_or(&0.0);
265 let a = *inputs.get(&gate.input_names[1]).unwrap_or(&0.0);
266 let b = *inputs.get(&gate.input_names[2]).unwrap_or(&0.0);
267 let sel_frac = (sel / config.max_conc).min(1.0);
268 k_hyb * (sel_frac * a + (1.0 - sel_frac) * b) * 1e-9
269 }
270 DnaGateType::Amplifier => {
271 let inp = *inputs.get(&gate.input_names[0]).unwrap_or(&0.0);
272 k_hyb * inp * 1e-9 * 5.0
273 }
274 DnaGateType::Buffer => {
275 let inp = *inputs.get(&gate.input_names[0]).unwrap_or(&0.0);
276 k_disp * (inp / config.max_conc).min(1.0)
277 }
278 _ => 0.0,
279 };
280
281 k_eff + gate.leak_rate
282}
283
284pub fn simulate_kinetics(
286 gates: &[DnaGateSpec],
287 input_concentrations: &HashMap<String, f64>,
288 duration_s: f64,
289 dt: f64,
290 config: &KineticConfig,
291) -> HashMap<String, Vec<f64>> {
292 let n_steps = (duration_s / dt) as usize;
293 let max_conc = config.max_conc;
294
295 let mut result: HashMap<String, Vec<f64>> = HashMap::new();
296
297 let time: Vec<f64> = (0..n_steps).map(|t| t as f64 * dt).collect();
299 result.insert("time".to_string(), time);
300
301 for gate in gates {
302 let k_eff = compute_k_eff(gate, input_concentrations, config);
303 let mut conc = vec![0.0f64; n_steps];
304
305 if config.use_rk4 {
306 for t in 1..n_steps {
307 let c = conc[t - 1];
308 let k1 = k_eff * (max_conc - c) * dt;
309 let k2 = k_eff * (max_conc - (c + k1 / 2.0)) * dt;
310 let k3 = k_eff * (max_conc - (c + k2 / 2.0)) * dt;
311 let k4 = k_eff * (max_conc - (c + k3)) * dt;
312 conc[t] = (c + (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0)
313 .max(0.0)
314 .min(max_conc);
315 }
316 } else {
317 for t in 1..n_steps {
318 let d = k_eff * (max_conc - conc[t - 1]) * dt;
319 conc[t] = (conc[t - 1] + d).max(0.0).min(max_conc);
320 }
321 }
322
323 result.insert(gate.output_name.clone(), conc);
324 }
325
326 result
327}
328
329pub fn detect_hairpins(seq: &[u8], min_stem: usize, min_loop: usize) -> Vec<(usize, usize, usize)> {
333 let n = seq.len();
334 let mut hairpins = Vec::new();
335
336 if n < min_stem * 2 + min_loop {
337 return hairpins;
338 }
339
340 let wc = |a: u8, b: u8| -> bool {
341 matches!(
342 (a, b),
343 (b'A', b'T') | (b'T', b'A') | (b'C', b'G') | (b'G', b'C')
344 )
345 };
346
347 for i in 0..n.saturating_sub(min_stem * 2 + min_loop) {
348 for stem_len in min_stem..12.min((n - i) / 2) {
349 let loop_start = i + stem_len;
350 for loop_len in min_loop..10.min(n - loop_start - stem_len + 1) {
351 let j = loop_start + loop_len;
352 if j + stem_len > n {
353 break;
354 }
355 let matches = (0..stem_len)
356 .filter(|&k| wc(seq[i + k], seq[j + stem_len - 1 - k]))
357 .count();
358 if matches >= stem_len {
359 hairpins.push((i, stem_len, loop_len));
360 }
361 }
362 }
363 }
364 hairpins
365}
366
367#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_design_sequence_length() {
375 let seq = design_sequence(30, 42);
376 assert_eq!(seq.len(), 30);
377 }
378
379 #[test]
380 fn test_design_sequence_alphabet() {
381 let seq = design_sequence(50, 42);
382 assert!(seq.iter().all(|&b| ALPHABET.contains(&b)));
383 }
384
385 #[test]
386 fn test_gc_content_balanced() {
387 let seq = design_sequence(40, 42);
388 let gc = gc_content(&seq);
389 assert!((0.3..=0.7).contains(&gc), "GC={gc}");
390 }
391
392 #[test]
393 fn test_complement() {
394 assert_eq!(complement(b"ACGT"), b"TGCA");
395 }
396
397 #[test]
398 fn test_reverse_complement() {
399 assert_eq!(reverse_complement(b"ACGT"), b"ACGT");
400 }
401
402 #[test]
403 fn test_cross_hybridization_self() {
404 let seqs = vec![b"ACGTACGTACGT".to_vec(), b"ACGTACGTACGT".to_vec()];
405 let flags = check_cross_hybridization(&seqs, 4);
406 assert!(!flags.is_empty());
407 }
408
409 #[test]
410 fn test_cross_hybridization_orthogonal() {
411 let seqs = design_orthogonal_set(3, 30, 42);
412 let flags = check_cross_hybridization(&seqs, 20);
413 assert!(flags.is_empty());
414 }
415
416 #[test]
417 fn test_simulate_kinetics_and_gate() {
418 let gates = vec![DnaGateSpec {
419 gate_type: DnaGateType::And,
420 input_names: vec!["A".to_string(), "B".to_string()],
421 output_name: "C".to_string(),
422 threshold: 0.5,
423 leak_rate: 1e-7,
424 }];
425 let mut inputs = HashMap::new();
426 inputs.insert("A".to_string(), 200.0);
427 inputs.insert("B".to_string(), 200.0);
428
429 let result = simulate_kinetics(&gates, &inputs, 1800.0, 1.0, &KineticConfig::default());
430 let c = result.get("C").unwrap();
431 assert!(c.last().unwrap() > &50.0);
432 }
433
434 #[test]
435 fn test_arrhenius_higher_temp_faster() {
436 let k37 = arrhenius_scale(3e5, 37.0, 15.0);
437 let k25 = arrhenius_scale(3e5, 25.0, 15.0);
438 assert!(k37 > k25);
439 }
440
441 #[test]
442 fn test_detect_hairpins_short() {
443 let hps = detect_hairpins(b"ACGT", 4, 3);
444 assert!(hps.is_empty());
445 }
446
447 #[test]
448 fn test_lcs() {
449 let score = longest_common_substring(b"ACGTACGT", b"ACGTACGT");
450 assert_eq!(score, 8);
451 }
452}