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