Skip to main content

sc_neurocore_engine/quantum/
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 — Quantum annealing PyO3 bindings
8
9//! Python bindings for the quantum annealing acceleration primitives.
10
11use pyo3::prelude::*;
12use pyo3::types::PyDict;
13
14use crate::quantum;
15
16/// Compute Ising energy for a spin configuration.
17///
18/// Args:
19///     h_indices, h_values: linear biases (parallel arrays)
20///     j_i, j_j, j_values: quadratic couplings (parallel arrays)
21///     spins: spin configuration (+1/-1)
22///     offset: constant energy offset
23#[pyfunction]
24#[pyo3(signature = (h_indices, h_values, j_i, j_j, j_values, spins, offset=0.0))]
25fn py_qa_ising_energy(
26    _py: Python<'_>,
27    h_indices: Vec<usize>,
28    h_values: Vec<f64>,
29    j_i: Vec<usize>,
30    j_j: Vec<usize>,
31    j_values: Vec<f64>,
32    spins: Vec<i8>,
33    offset: f64,
34) -> f64 {
35    let h: Vec<(usize, f64)> = h_indices.into_iter().zip(h_values).collect();
36    let j: Vec<((usize, usize), f64)> = j_i.into_iter().zip(j_j).zip(j_values).collect();
37    quantum::ising_energy(&h, &j, &spins, offset)
38}
39
40/// Batch compute Ising energies for many configurations.
41#[pyfunction]
42#[pyo3(signature = (h_indices, h_values, j_i, j_j, j_values, configs, offset=0.0))]
43fn py_qa_batch_ising_energy(
44    _py: Python<'_>,
45    h_indices: Vec<usize>,
46    h_values: Vec<f64>,
47    j_i: Vec<usize>,
48    j_j: Vec<usize>,
49    j_values: Vec<f64>,
50    configs: Vec<Vec<i8>>,
51    offset: f64,
52) -> Vec<f64> {
53    let h: Vec<(usize, f64)> = h_indices.into_iter().zip(h_values).collect();
54    let j: Vec<((usize, usize), f64)> = j_i.into_iter().zip(j_j).zip(j_values).collect();
55    quantum::batch_ising_energy(&h, &j, &configs, offset)
56}
57
58/// Run simulated annealing on an Ising model (Rust-accelerated).
59///
60/// Returns dict with best_spins, best_energy, energies, samples.
61#[pyfunction]
62#[pyo3(signature = (h_indices, h_values, j_i, j_j, j_values, n_qubits, offset=0.0, n_sweeps=1000, num_reads=10, beta_start=0.1, beta_end=10.0, seed=42))]
63fn py_qa_simulated_annealing<'py>(
64    py: Python<'py>,
65    h_indices: Vec<usize>,
66    h_values: Vec<f64>,
67    j_i: Vec<usize>,
68    j_j: Vec<usize>,
69    j_values: Vec<f64>,
70    n_qubits: usize,
71    offset: f64,
72    n_sweeps: usize,
73    num_reads: usize,
74    beta_start: f64,
75    beta_end: f64,
76    seed: u64,
77) -> PyResult<Py<PyAny>> {
78    let h: Vec<(usize, f64)> = h_indices.into_iter().zip(h_values).collect();
79    let j: Vec<((usize, usize), f64)> = j_i.into_iter().zip(j_j).zip(j_values).collect();
80
81    let (best_spins, best_energy, all_energies, all_samples) = quantum::simulated_annealing(
82        &h, &j, n_qubits, offset, n_sweeps, num_reads, beta_start, beta_end, seed,
83    );
84
85    let dict = PyDict::new(py);
86    dict.set_item("best_spins", best_spins)?;
87    dict.set_item("best_energy", best_energy)?;
88    dict.set_item("energies", all_energies)?;
89    dict.set_item("samples", all_samples)?;
90    dict.set_item("n_sweeps", n_sweeps)?;
91    dict.set_item("num_reads", num_reads)?;
92    dict.set_item("backend", "rust")?;
93    Ok(dict.into_any().unbind())
94}
95
96/// Apply gauge transform to Ising biases and couplings.
97#[pyfunction]
98#[allow(clippy::type_complexity)] // tuple shape mirrors Python's QUBO format
99fn py_qa_gauge_transform(
100    _py: Python<'_>,
101    h_indices: Vec<usize>,
102    h_values: Vec<f64>,
103    j_i: Vec<usize>,
104    j_j: Vec<usize>,
105    j_values: Vec<f64>,
106    gauge: Vec<i8>,
107) -> (Vec<(usize, f64)>, Vec<((usize, usize), f64)>) {
108    let h: Vec<(usize, f64)> = h_indices.into_iter().zip(h_values).collect();
109    let j: Vec<((usize, usize), f64)> = j_i.into_iter().zip(j_j).zip(j_values).collect();
110    quantum::gauge_transform(&h, &j, &gauge)
111}
112
113/// Generate random gauge vectors.
114#[pyfunction]
115#[pyo3(signature = (n_qubits, n_gauges=10, seed=42))]
116fn py_qa_generate_gauges(
117    _py: Python<'_>,
118    n_qubits: usize,
119    n_gauges: usize,
120    seed: u64,
121) -> Vec<Vec<i8>> {
122    quantum::generate_gauges(n_qubits, n_gauges, seed)
123}
124
125/// Greedy graph partitioning for problem decomposition.
126#[pyfunction]
127#[pyo3(signature = (n_qubits, j_i, j_j, j_values, max_partition_size=64))]
128fn py_qa_greedy_partition(
129    _py: Python<'_>,
130    n_qubits: usize,
131    j_i: Vec<usize>,
132    j_j: Vec<usize>,
133    j_values: Vec<f64>,
134    max_partition_size: usize,
135) -> Vec<Vec<usize>> {
136    let j: Vec<((usize, usize), f64)> = j_i.into_iter().zip(j_j).zip(j_values).collect();
137    quantum::greedy_partition(n_qubits, &j, max_partition_size)
138}
139
140pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
141    m.add_function(wrap_pyfunction!(py_qa_ising_energy, m)?)?;
142    m.add_function(wrap_pyfunction!(py_qa_batch_ising_energy, m)?)?;
143    m.add_function(wrap_pyfunction!(py_qa_simulated_annealing, m)?)?;
144    m.add_function(wrap_pyfunction!(py_qa_gauge_transform, m)?)?;
145    m.add_function(wrap_pyfunction!(py_qa_generate_gauges, m)?)?;
146    m.add_function(wrap_pyfunction!(py_qa_greedy_partition, m)?)?;
147    Ok(())
148}