Skip to main content

sc_neurocore_engine/dna/
bindings.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Commercial license available
3// © Concepts 1996–2026 Miroslav Šotek. All rights reserved.
4// © Code 2020–2026 Miroslav Šotek. All rights reserved.
5// ORCID: 0009-0009-3560-0851
6// Contact: www.anulum.li | protoscience@anulum.li
7// SC-NeuroCore — DNA acceleration PyO3 bindings
8
9//! Python bindings for the DNA strand-displacement acceleration primitives.
10
11use numpy::IntoPyArray;
12use pyo3::prelude::*;
13use pyo3::types::PyDict;
14use pyo3::IntoPyObject;
15
16use crate::dna;
17
18// ── DNA acceleration PyO3 wrappers ───────────────────────────────────
19
20#[pyfunction]
21fn py_dna_design_sequence(_py: Python<'_>, length: usize, seed: u64) -> String {
22    String::from_utf8(dna::design_sequence(length, seed)).unwrap_or_default()
23}
24
25#[pyfunction]
26fn py_dna_design_orthogonal_set(
27    _py: Python<'_>,
28    count: usize,
29    length: usize,
30    seed: u64,
31) -> Vec<String> {
32    dna::design_orthogonal_set(count, length, seed)
33        .into_iter()
34        .map(|s| String::from_utf8(s).unwrap_or_default())
35        .collect()
36}
37
38#[pyfunction]
39fn py_dna_check_cross_hybridization<'py>(
40    py: Python<'py>,
41    sequences: Vec<String>,
42    threshold: usize,
43) -> PyResult<Py<PyAny>> {
44    let seqs: Vec<Vec<u8>> = sequences.into_iter().map(|s| s.into_bytes()).collect();
45    let flags = dna::check_cross_hybridization(&seqs, threshold);
46    let result: Vec<Py<PyAny>> = flags
47        .into_iter()
48        .map(|(i, j, score)| {
49            let d = PyDict::new(py);
50            d.set_item("strand_a", i).unwrap();
51            d.set_item("strand_b", j).unwrap();
52            d.set_item("score", score).unwrap();
53            d.into_any().unbind()
54        })
55        .collect();
56    Ok(result.into_pyobject(py)?.into())
57}
58
59#[pyfunction]
60#[pyo3(signature = (
61    gate_types, gate_inputs, gate_outputs, gate_thresholds, gate_leaks,
62    input_names, input_concs, duration_s=1800.0, dt=1.0,
63    k_hyb=3e5, k_disp=1.0, temperature_c=37.0, use_rk4=true
64))]
65#[allow(clippy::too_many_arguments)]
66fn py_dna_simulate_kinetics<'py>(
67    py: Python<'py>,
68    gate_types: Vec<String>,
69    gate_inputs: Vec<Vec<String>>,
70    gate_outputs: Vec<String>,
71    gate_thresholds: Vec<f64>,
72    gate_leaks: Vec<f64>,
73    input_names: Vec<String>,
74    input_concs: Vec<f64>,
75    duration_s: f64,
76    dt: f64,
77    k_hyb: f64,
78    k_disp: f64,
79    temperature_c: f64,
80    use_rk4: bool,
81) -> PyResult<Py<PyAny>> {
82    let gates: Vec<dna::DnaGateSpec> = gate_types
83        .iter()
84        .zip(gate_inputs.iter())
85        .zip(gate_outputs.iter())
86        .zip(gate_thresholds.iter())
87        .zip(gate_leaks.iter())
88        .map(|((((gt, gi), go), th), lk)| {
89            let gate_type = match gt.to_uppercase().as_str() {
90                "AND" => dna::DnaGateType::And,
91                "OR" => dna::DnaGateType::Or,
92                "NOT" => dna::DnaGateType::Not,
93                "THRESHOLD" => dna::DnaGateType::Threshold,
94                "MUX" => dna::DnaGateType::Mux,
95                "AMPLIFIER" => dna::DnaGateType::Amplifier,
96                "BUFFER" => dna::DnaGateType::Buffer,
97                "NAND" => dna::DnaGateType::Nand,
98                "XOR" => dna::DnaGateType::Xor,
99                _ => dna::DnaGateType::And,
100            };
101            dna::DnaGateSpec {
102                gate_type,
103                input_names: gi.clone(),
104                output_name: go.clone(),
105                threshold: *th,
106                leak_rate: *lk,
107            }
108        })
109        .collect();
110
111    let mut inputs = std::collections::HashMap::new();
112    for (name, conc) in input_names.into_iter().zip(input_concs) {
113        inputs.insert(name, conc);
114    }
115
116    let config = dna::KineticConfig {
117        k_hyb,
118        k_disp,
119        temperature_c,
120        max_conc: 200.0,
121        use_rk4,
122    };
123
124    let result = dna::simulate_kinetics(&gates, &inputs, duration_s, dt, &config);
125
126    let dict = PyDict::new(py);
127    for (key, trace) in result {
128        dict.set_item(key, trace.into_pyarray(py))?;
129    }
130    Ok(dict.into_any().unbind())
131}
132
133#[pyfunction]
134#[pyo3(signature = (sequence, min_stem=4, min_loop=3))]
135fn py_dna_detect_hairpins(
136    _py: Python<'_>,
137    sequence: &str,
138    min_stem: usize,
139    min_loop: usize,
140) -> Vec<(usize, usize, usize)> {
141    dna::detect_hairpins(sequence.as_bytes(), min_stem, min_loop)
142}
143
144pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
145    m.add_function(wrap_pyfunction!(py_dna_design_sequence, m)?)?;
146    m.add_function(wrap_pyfunction!(py_dna_design_orthogonal_set, m)?)?;
147    m.add_function(wrap_pyfunction!(py_dna_check_cross_hybridization, m)?)?;
148    m.add_function(wrap_pyfunction!(py_dna_simulate_kinetics, m)?)?;
149    m.add_function(wrap_pyfunction!(py_dna_detect_hairpins, m)?)?;
150    Ok(())
151}