1pub(crate) mod bindings;
18
19use rayon::prelude::*;
20use std::collections::HashMap;
21
22pub fn ising_energy(
28 h: &[(usize, f64)],
29 j: &[((usize, usize), f64)],
30 spins: &[i8],
31 offset: f64,
32) -> f64 {
33 let mut e = offset;
34 for &(i, hi) in h {
35 if i < spins.len() {
36 e += hi * spins[i] as f64;
37 }
38 }
39 for &((i, j_idx), jij) in j {
40 if i < spins.len() && j_idx < spins.len() {
41 e += jij * spins[i] as f64 * spins[j_idx] as f64;
42 }
43 }
44 e
45}
46
47pub fn batch_ising_energy(
49 h: &[(usize, f64)],
50 j: &[((usize, usize), f64)],
51 configs: &[Vec<i8>],
52 offset: f64,
53) -> Vec<f64> {
54 configs
55 .par_iter()
56 .map(|spins| ising_energy(h, j, spins, offset))
57 .collect()
58}
59
60#[inline]
67fn delta_energy(
68 h: &[(usize, f64)],
69 j_by_qubit: &[Vec<(usize, f64)>],
70 spins: &[i8],
71 qubit: usize,
72) -> f64 {
73 let s = spins[qubit] as f64;
74 let h_q = h
75 .iter()
76 .find(|&&(i, _)| i == qubit)
77 .map_or(0.0, |&(_, hi)| hi);
78 let mut local_field = h_q;
79
80 for &(other, jij) in &j_by_qubit[qubit] {
81 local_field += jij * spins[other] as f64;
82 }
83 -2.0 * s * local_field
84}
85
86fn build_j_index(j: &[((usize, usize), f64)], n: usize) -> Vec<Vec<(usize, f64)>> {
88 let mut idx = vec![Vec::new(); n];
89 for &((i, j_idx), jij) in j {
90 if i < n {
91 idx[i].push((j_idx, jij));
92 }
93 if j_idx < n {
94 idx[j_idx].push((i, jij));
95 }
96 }
97 idx
98}
99
100pub fn simulated_annealing(
104 h: &[(usize, f64)],
105 j: &[((usize, usize), f64)],
106 n_qubits: usize,
107 offset: f64,
108 n_sweeps: usize,
109 num_reads: usize,
110 beta_start: f64,
111 beta_end: f64,
112 seed: u64,
113) -> (Vec<i8>, f64, Vec<f64>, Vec<Vec<i8>>) {
114 let j_index = build_j_index(j, n_qubits);
115
116 let results: Vec<(Vec<i8>, f64)> = (0..num_reads)
118 .into_par_iter()
119 .map(|read_idx| {
120 let mut rng = Xoshiro256pp::new(seed.wrapping_add(read_idx as u64));
121
122 let mut spins: Vec<i8> = (0..n_qubits)
124 .map(|_| if rng.next_bit() { 1 } else { -1 })
125 .collect();
126 let mut energy = ising_energy(h, j, &spins, offset);
127
128 for sweep in 0..n_sweeps {
129 let beta = beta_start
130 * ((beta_end / beta_start).powf(sweep as f64 / (n_sweeps - 1).max(1) as f64));
131
132 for qubit in 0..n_qubits {
133 let de = delta_energy(h, &j_index, &spins, qubit);
134
135 if de < 0.0 || rng.next_f64() < (-beta * de).exp() {
136 spins[qubit] *= -1;
137 energy += de;
138 }
139 }
140 }
141
142 (spins, energy)
143 })
144 .collect();
145
146 let mut best_energy = f64::INFINITY;
147 let mut best_spins = vec![1i8; n_qubits];
148 let mut all_energies = Vec::with_capacity(num_reads);
149 let mut all_samples = Vec::with_capacity(num_reads);
150
151 for (spins, energy) in results {
152 all_energies.push(energy);
153 if energy < best_energy {
154 best_energy = energy;
155 best_spins = spins.clone();
156 }
157 all_samples.push(spins);
158 }
159
160 (best_spins, best_energy, all_energies, all_samples)
161}
162
163#[allow(clippy::type_complexity)] pub fn gauge_transform(
170 h: &[(usize, f64)],
171 j: &[((usize, usize), f64)],
172 gauge: &[i8],
173) -> (Vec<(usize, f64)>, Vec<((usize, usize), f64)>) {
174 let h_new: Vec<(usize, f64)> = h
175 .iter()
176 .map(|&(i, hi)| {
177 let g = if i < gauge.len() {
178 gauge[i] as f64
179 } else {
180 1.0
181 };
182 (i, g * hi)
183 })
184 .collect();
185
186 let j_new: Vec<((usize, usize), f64)> = j
187 .iter()
188 .map(|&((i, j_idx), jij)| {
189 let gi = if i < gauge.len() {
190 gauge[i] as f64
191 } else {
192 1.0
193 };
194 let gj = if j_idx < gauge.len() {
195 gauge[j_idx] as f64
196 } else {
197 1.0
198 };
199 ((i, j_idx), gi * gj * jij)
200 })
201 .collect();
202
203 (h_new, j_new)
204}
205
206pub fn generate_gauges(n_qubits: usize, n_gauges: usize, seed: u64) -> Vec<Vec<i8>> {
208 (0..n_gauges)
209 .into_par_iter()
210 .map(|g| {
211 let mut rng = Xoshiro256pp::new(seed.wrapping_add(g as u64 * 7919));
212 (0..n_qubits)
213 .map(|_| if rng.next_bit() { 1 } else { -1 })
214 .collect()
215 })
216 .collect()
217}
218
219pub fn greedy_partition(
225 n_qubits: usize,
226 j: &[((usize, usize), f64)],
227 max_partition_size: usize,
228) -> Vec<Vec<usize>> {
229 let mut neighbors: HashMap<usize, Vec<(usize, f64)>> = HashMap::new();
231 for &((i, j_idx), jij) in j {
232 neighbors.entry(i).or_default().push((j_idx, jij.abs()));
233 neighbors.entry(j_idx).or_default().push((i, jij.abs()));
234 }
235
236 let mut remaining: Vec<bool> = vec![true; n_qubits];
237 let mut partitions: Vec<Vec<usize>> = Vec::new();
238
239 let mut n_remaining = n_qubits;
240 while n_remaining > 0 {
241 let seed = remaining.iter().position(|&r| r).unwrap();
243 let mut partition = vec![seed];
244 remaining[seed] = false;
245 n_remaining -= 1;
246
247 while partition.len() < max_partition_size && n_remaining > 0 {
248 let mut best: Option<usize> = None;
249 let mut best_score = -1.0f64;
250
251 for &q in &partition {
252 if let Some(nbrs) = neighbors.get(&q) {
253 for &(n, score) in nbrs {
254 if n < n_qubits && remaining[n] && score > best_score {
255 best = Some(n);
256 best_score = score;
257 }
258 }
259 }
260 }
261
262 let chosen = best.unwrap_or_else(|| remaining.iter().position(|&r| r).unwrap());
263
264 partition.push(chosen);
265 remaining[chosen] = false;
266 n_remaining -= 1;
267 }
268
269 partitions.push(partition);
270 }
271
272 partitions
273}
274
275struct Xoshiro256pp {
278 s: [u64; 4],
279}
280
281impl Xoshiro256pp {
282 fn new(seed: u64) -> Self {
283 let mut s = [0u64; 4];
285 let mut z = seed;
286 for slot in &mut s {
287 z = z.wrapping_add(0x9e3779b97f4a7c15);
288 z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
289 z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
290 *slot = z ^ (z >> 31);
291 }
292 Self { s }
293 }
294
295 #[inline]
296 fn next_u64(&mut self) -> u64 {
297 let result = (self.s[0].wrapping_add(self.s[3]))
298 .rotate_left(23)
299 .wrapping_add(self.s[0]);
300 let t = self.s[1] << 17;
301 self.s[2] ^= self.s[0];
302 self.s[3] ^= self.s[1];
303 self.s[1] ^= self.s[2];
304 self.s[0] ^= self.s[3];
305 self.s[2] ^= t;
306 self.s[3] = self.s[3].rotate_left(45);
307 result
308 }
309
310 #[inline]
311 fn next_f64(&mut self) -> f64 {
312 (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
313 }
314
315 #[inline]
316 fn next_bit(&mut self) -> bool {
317 self.next_u64() & 1 == 1
318 }
319}
320
321#[cfg(test)]
324mod tests {
325 use super::*;
326
327 type HTerms = Vec<(usize, f64)>;
328 type JTerms = Vec<((usize, usize), f64)>;
329
330 fn simple_model() -> (HTerms, JTerms) {
331 let h = vec![(0, 0.1), (1, -0.2), (2, 0.0)];
332 let j = vec![((0, 1), -1.0), ((1, 2), 0.5)];
333 (h, j)
334 }
335
336 #[test]
337 fn test_ising_energy_all_up() {
338 let (h, j) = simple_model();
339 let spins = vec![1, 1, 1];
340 let e = ising_energy(&h, &j, &spins, 0.0);
341 assert!((e - (-0.6)).abs() < 1e-10);
343 }
344
345 #[test]
346 fn test_ising_energy_mixed() {
347 let (h, j) = simple_model();
348 let spins = vec![1, -1, 1];
349 let e = ising_energy(&h, &j, &spins, 0.0);
350 assert!((e - 0.8).abs() < 1e-10);
352 }
353
354 #[test]
355 fn test_batch_energy() {
356 let (h, j) = simple_model();
357 let configs = vec![vec![1, 1, 1], vec![1, -1, 1], vec![-1, -1, -1]];
358 let energies = batch_ising_energy(&h, &j, &configs, 0.0);
359 assert_eq!(energies.len(), 3);
360 assert!((energies[0] - (-0.6)).abs() < 1e-10);
361 }
362
363 #[test]
364 fn test_sa_finds_ground_state() {
365 let h = vec![(0, 0.0), (1, 0.0)];
367 let j = vec![((0, 1), -1.0)];
368 let (best_spins, best_energy, _, _) =
369 simulated_annealing(&h, &j, 2, 0.0, 5000, 100, 0.1, 20.0, 42);
370 assert!(best_energy <= -0.99, "energy = {}", best_energy);
372 let recomputed = ising_energy(&h, &j, &best_spins, 0.0);
377 assert!(
378 (best_energy - recomputed).abs() < 1e-9,
379 "tracker {} != recomputed {}",
380 best_energy,
381 recomputed
382 );
383 }
384
385 #[test]
386 fn test_sa_planted_ferromagnetic_8q() {
387 let n = 8;
393 let h: Vec<(usize, f64)> = (0..n).map(|i| (i, -1.0)).collect();
394 let mut j: Vec<((usize, usize), f64)> = Vec::new();
395 for i in 0..n {
396 for k in (i + 1)..n {
397 j.push(((i, k), -1.0));
398 }
399 }
400 let (best_spins, best_energy, _, _) =
401 simulated_annealing(&h, &j, n, 0.0, 2000, 50, 0.1, 10.0, 42);
402 let recomputed = ising_energy(&h, &j, &best_spins, 0.0);
403 assert!(
404 (best_energy - recomputed).abs() < 1e-9,
405 "tracker {} != recomputed {}",
406 best_energy,
407 recomputed
408 );
409 assert!(
411 best_energy >= -36.0 - 1e-9,
412 "best_energy={} below planted GS=-36",
413 best_energy
414 );
415 assert!(
417 best_energy <= -35.0,
418 "SA failed to reach planted GS: {}",
419 best_energy
420 );
421 }
422
423 #[test]
424 fn test_sa_returns_correct_counts() {
425 let h = vec![(0, 0.0)];
426 let j = vec![];
427 let (_, _, energies, samples) = simulated_annealing(&h, &j, 1, 0.0, 100, 5, 0.1, 5.0, 42);
428 assert_eq!(energies.len(), 5);
429 assert_eq!(samples.len(), 5);
430 }
431
432 #[test]
433 fn test_gauge_transform_preserves_magnitudes() {
434 let h = vec![(0, 1.0), (1, -2.0)];
435 let j = vec![((0, 1), 0.5)];
436 let gauge = vec![1, -1];
437 let (h_new, j_new) = gauge_transform(&h, &j, &gauge);
438 assert!((h_new[0].1 - 1.0).abs() < 1e-10);
439 assert!((h_new[1].1 - 2.0).abs() < 1e-10); assert!((j_new[0].1 - (-0.5)).abs() < 1e-10); }
442
443 #[test]
444 fn test_generate_gauges() {
445 let gauges = generate_gauges(5, 10, 42);
446 assert_eq!(gauges.len(), 10);
447 for g in &gauges {
448 assert_eq!(g.len(), 5);
449 for &v in g {
450 assert!(v == 1 || v == -1);
451 }
452 }
453 }
454
455 #[test]
456 fn test_greedy_partition_small() {
457 let j = vec![((0, 1), -1.0), ((1, 2), 0.5)];
458 let parts = greedy_partition(3, &j, 100);
459 assert_eq!(parts.len(), 1); }
461
462 #[test]
463 fn test_greedy_partition_forced_split() {
464 let j = vec![((0, 1), -1.0), ((1, 2), 0.5), ((2, 3), 0.8), ((3, 4), 0.3)];
465 let parts = greedy_partition(5, &j, 2);
466 assert!(parts.len() >= 3);
467 for p in &parts {
468 assert!(p.len() <= 2);
469 }
470 }
471
472 #[test]
473 fn test_xoshiro_deterministic() {
474 let mut rng1 = Xoshiro256pp::new(42);
475 let mut rng2 = Xoshiro256pp::new(42);
476 for _ in 0..100 {
477 assert_eq!(rng1.next_u64(), rng2.next_u64());
478 }
479 }
480
481 #[test]
482 fn test_xoshiro_f64_range() {
483 let mut rng = Xoshiro256pp::new(123);
484 for _ in 0..1000 {
485 let v = rng.next_f64();
486 assert!((0.0..1.0).contains(&v));
487 }
488 }
489}