1use scirs2_core::random::prelude::*;
7use scirs2_core::random::ChaCha8Rng;
8use scirs2_core::random::{Rng, SeedableRng};
9use std::time::{Duration, Instant};
10use thiserror::Error;
11
12use crate::ising::{IsingError, IsingModel};
13
14#[derive(Error, Debug, Clone)]
16#[non_exhaustive]
17pub enum AnnealingError {
18 #[error("Ising error: {0}")]
20 IsingError(#[from] IsingError),
21
22 #[error("Invalid annealing schedule: {0}")]
24 InvalidSchedule(String),
25
26 #[error("Invalid annealing parameter: {0}")]
28 InvalidParameter(String),
29
30 #[error("Annealing timeout after {0:?}")]
32 Timeout(Duration),
33}
34
35pub type AnnealingResult<T> = Result<T, AnnealingError>;
37
38#[derive(Debug, Clone)]
43pub enum TransverseFieldSchedule {
44 Linear,
46
47 Exponential(f64), Custom(fn(f64, f64) -> f64),
52}
53
54impl TransverseFieldSchedule {
55 #[must_use]
57 pub fn calculate(&self, t: f64, t_f: f64, a_0: f64) -> f64 {
58 match self {
59 Self::Linear => a_0 * (1.0 - t / t_f),
60 Self::Exponential(alpha) => a_0 * (-alpha * t / t_f).exp(),
61 Self::Custom(func) => func(t, t_f),
62 }
63 }
64}
65
66#[derive(Debug, Clone)]
71pub enum TemperatureSchedule {
72 Linear,
74
75 Exponential(f64), Geometric(f64, f64), Custom(fn(f64, f64) -> f64),
83}
84
85impl TemperatureSchedule {
86 #[must_use]
88 pub fn calculate(&self, t: f64, t_f: f64, t_0: f64) -> f64 {
89 match self {
90 Self::Linear => t_0 * (1.0 - t / t_f),
91 Self::Exponential(alpha) => t_0 * (-alpha * t / t_f).exp(),
92 Self::Geometric(alpha, delta_t) => t_0 * alpha.powf(t / delta_t),
93 Self::Custom(func) => func(t, t_f),
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct AnnealingParams {
101 pub initial_transverse_field: f64,
103
104 pub transverse_field_schedule: TransverseFieldSchedule,
106
107 pub initial_temperature: f64,
109
110 pub final_temperature: f64,
112
113 pub temperature_schedule: TemperatureSchedule,
115
116 pub num_sweeps: usize,
118
119 pub updates_per_sweep: Option<usize>,
121
122 pub num_repetitions: usize,
124
125 pub seed: Option<u64>,
127
128 pub timeout: Option<f64>,
130
131 pub trotter_slices: usize,
133}
134
135impl AnnealingParams {
136 #[must_use]
138 pub const fn new() -> Self {
139 Self {
140 initial_transverse_field: 2.0,
141 transverse_field_schedule: TransverseFieldSchedule::Linear,
142 initial_temperature: 2.0,
143 final_temperature: 0.01,
144 temperature_schedule: TemperatureSchedule::Exponential(3.0),
145 num_sweeps: 1000,
146 updates_per_sweep: None,
147 num_repetitions: 10,
148 seed: None,
149 timeout: Some(60.0), trotter_slices: 20,
151 }
152 }
153
154 pub fn validate(&self) -> AnnealingResult<()> {
156 if self.initial_transverse_field <= 0.0 || !self.initial_transverse_field.is_finite() {
158 return Err(AnnealingError::InvalidParameter(format!(
159 "Initial transverse field must be positive and finite, got {}",
160 self.initial_transverse_field
161 )));
162 }
163
164 if self.initial_temperature <= 0.0 || !self.initial_temperature.is_finite() {
166 return Err(AnnealingError::InvalidParameter(format!(
167 "Initial temperature must be positive and finite, got {}",
168 self.initial_temperature
169 )));
170 }
171
172 if self.final_temperature <= 0.0 || !self.final_temperature.is_finite() {
174 return Err(AnnealingError::InvalidParameter(format!(
175 "Final temperature must be positive and finite, got {}",
176 self.final_temperature
177 )));
178 }
179
180 if self.num_sweeps == 0 {
182 return Err(AnnealingError::InvalidParameter(
183 "Number of sweeps must be positive".to_string(),
184 ));
185 }
186
187 if self.num_repetitions == 0 {
189 return Err(AnnealingError::InvalidParameter(
190 "Number of repetitions must be positive".to_string(),
191 ));
192 }
193
194 if let Some(timeout) = self.timeout {
196 if timeout <= 0.0 || !timeout.is_finite() {
197 return Err(AnnealingError::InvalidParameter(format!(
198 "Timeout must be positive and finite, got {timeout}"
199 )));
200 }
201 }
202
203 if self.trotter_slices == 0 {
205 return Err(AnnealingError::InvalidParameter(
206 "Number of Trotter slices must be positive".to_string(),
207 ));
208 }
209
210 Ok(())
211 }
212}
213
214impl Default for AnnealingParams {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220#[derive(Debug, Clone)]
222pub struct AnnealingSolution {
223 pub best_spins: Vec<i8>,
225
226 pub best_energy: f64,
228
229 pub repetitions: usize,
231
232 pub total_sweeps: usize,
234
235 pub runtime: Duration,
237
238 pub info: String,
240}
241
242#[derive(Debug, Clone)]
268pub struct QuantumAnnealingSimulator {
269 params: AnnealingParams,
271}
272
273impl QuantumAnnealingSimulator {
274 pub fn new(params: AnnealingParams) -> AnnealingResult<Self> {
276 params.validate()?;
278
279 Ok(Self { params })
280 }
281
282 pub fn with_default_params() -> AnnealingResult<Self> {
284 Self::new(AnnealingParams::default())
285 }
286}
287
288impl Default for QuantumAnnealingSimulator {
289 fn default() -> Self {
290 Self::with_default_params().expect("Default parameters should be valid")
291 }
292}
293
294impl QuantumAnnealingSimulator {
295 pub fn solve(&self, model: &IsingModel) -> AnnealingResult<AnnealingSolution> {
297 let start_time = Instant::now();
299
300 let mut rng = match self.params.seed {
302 Some(seed) => ChaCha8Rng::seed_from_u64(seed),
303 None => ChaCha8Rng::seed_from_u64(thread_rng().random()),
304 };
305
306 let num_qubits = model.num_qubits;
308 let mut best_spins = vec![1; num_qubits]; let mut best_energy = match model.energy(&best_spins) {
310 Ok(energy) => energy,
311 Err(err) => return Err(AnnealingError::IsingError(err)),
312 };
313
314 let mut total_sweeps = 0;
316 let mut completed_repetitions = 0;
317
318 let updates_per_sweep = self.params.updates_per_sweep.unwrap_or(num_qubits);
320
321 let trotter_slices = self.params.trotter_slices;
323
324 for _ in 0..self.params.num_repetitions {
326 let mut trotter_spins = vec![vec![0; num_qubits]; trotter_slices];
328 for slice in &mut trotter_spins {
329 for spin in slice.iter_mut() {
330 *spin = if rng.random_bool(0.5) { 1 } else { -1 };
331 }
332 }
333
334 for sweep in 0..self.params.num_sweeps {
336 if let Some(timeout) = self.params.timeout {
338 let elapsed = start_time.elapsed().as_secs_f64();
339 if elapsed > timeout {
340 return Err(AnnealingError::Timeout(Duration::from_secs_f64(elapsed)));
341 }
342 }
343
344 let t = sweep as f64 / self.params.num_sweeps as f64;
346 let t_f = 1.0; let transverse_field = self.params.transverse_field_schedule.calculate(
350 t,
351 t_f,
352 self.params.initial_transverse_field,
353 );
354 let temperature = self.params.temperature_schedule.calculate(
355 t,
356 t_f,
357 self.params.initial_temperature,
358 );
359
360 let j_perp = -0.5
362 * temperature
363 * (trotter_slices as f64)
364 * (transverse_field / temperature).ln_1p().abs();
365
366 for _ in 0..updates_per_sweep {
368 let qubit = rng.random_range(0..num_qubits);
370 let slice = rng.random_range(0..trotter_slices);
371
372 let current_spin = trotter_spins[slice][qubit];
374 let new_spin = -current_spin;
375
376 trotter_spins[slice][qubit] = new_spin;
378
379 let mut delta_e = match model.energy(&trotter_spins[slice]) {
381 Ok(energy_new) => {
382 trotter_spins[slice][qubit] = current_spin;
384 let energy_old = model.energy(&trotter_spins[slice])?;
385 energy_new - energy_old
386 }
387 Err(err) => return Err(AnnealingError::IsingError(err)),
388 };
389
390 let prev_slice = (slice + trotter_slices - 1) % trotter_slices;
392 let next_slice = (slice + 1) % trotter_slices;
393
394 let new_spin_f64 = f64::from(new_spin);
396 let current_spin_f64 = f64::from(current_spin);
397 let neighbor_sum = f64::from(
398 trotter_spins[prev_slice][qubit] + trotter_spins[next_slice][qubit],
399 );
400
401 delta_e += j_perp * new_spin_f64 * neighbor_sum;
402 delta_e -= j_perp * current_spin_f64 * neighbor_sum;
403
404 let accept = delta_e <= 0.0 || {
406 let p = (-delta_e / temperature).exp();
407 rng.random_range(0.0..1.0) < p
408 };
409
410 if accept {
412 trotter_spins[slice][qubit] = new_spin;
413 }
414 }
415
416 total_sweeps += 1;
418 }
419
420 let mut avg_spins = vec![0; num_qubits];
422 for qubit in 0..num_qubits {
423 let sum: i32 = trotter_spins
424 .iter()
425 .map(|slice| i32::from(slice[qubit]))
426 .sum();
427 avg_spins[qubit] = if sum >= 0 { 1 } else { -1 };
428 }
429
430 match model.energy(&avg_spins) {
432 Ok(energy) => {
433 if energy < best_energy {
434 best_energy = energy;
435 best_spins = avg_spins;
436 }
437 }
438 Err(err) => return Err(AnnealingError::IsingError(err)),
439 }
440
441 completed_repetitions += 1;
443 }
444
445 let runtime = start_time.elapsed();
447
448 Ok(AnnealingSolution {
450 best_spins,
451 best_energy,
452 repetitions: completed_repetitions,
453 total_sweeps,
454 runtime,
455 info: format!(
456 "Performed {completed_repetitions} repetitions with {total_sweeps} total sweeps in {runtime:?}"
457 ),
458 })
459 }
460}
461
462#[derive(Debug, Clone)]
490pub struct ClassicalAnnealingSimulator {
491 params: AnnealingParams,
493}
494
495impl ClassicalAnnealingSimulator {
496 pub fn new(params: AnnealingParams) -> AnnealingResult<Self> {
498 params.validate()?;
500
501 Ok(Self { params })
502 }
503
504 pub fn with_default_params() -> AnnealingResult<Self> {
506 Self::new(AnnealingParams::default())
507 }
508}
509
510impl Default for ClassicalAnnealingSimulator {
511 fn default() -> Self {
512 Self::with_default_params().expect("Default parameters should be valid")
513 }
514}
515
516impl ClassicalAnnealingSimulator {
517 pub fn solve(&self, model: &IsingModel) -> AnnealingResult<AnnealingSolution> {
519 let start_time = Instant::now();
521
522 let mut rng = match self.params.seed {
524 Some(seed) => ChaCha8Rng::seed_from_u64(seed),
525 None => ChaCha8Rng::seed_from_u64(thread_rng().random()),
526 };
527
528 let num_qubits = model.num_qubits;
530 let mut best_spins = vec![1; num_qubits]; let mut best_energy = match model.energy(&best_spins) {
532 Ok(energy) => energy,
533 Err(err) => return Err(AnnealingError::IsingError(err)),
534 };
535
536 let mut total_sweeps = 0;
538 let mut completed_repetitions = 0;
539
540 let updates_per_sweep = self.params.updates_per_sweep.unwrap_or(num_qubits);
542
543 for _ in 0..self.params.num_repetitions {
545 let mut spins = vec![0; num_qubits];
547 for spin in &mut spins {
548 *spin = if rng.random_bool(0.5) { 1 } else { -1 };
549 }
550
551 let mut current_energy = match model.energy(&spins) {
553 Ok(energy) => energy,
554 Err(err) => return Err(AnnealingError::IsingError(err)),
555 };
556
557 for sweep in 0..self.params.num_sweeps {
559 if let Some(timeout) = self.params.timeout {
561 let elapsed = start_time.elapsed().as_secs_f64();
562 if elapsed > timeout {
563 return Err(AnnealingError::Timeout(Duration::from_secs_f64(elapsed)));
564 }
565 }
566
567 let t = sweep as f64 / self.params.num_sweeps as f64;
569 let t_f = 1.0; let temperature = self.params.temperature_schedule.calculate(
573 t,
574 t_f,
575 self.params.initial_temperature,
576 );
577
578 for _ in 0..updates_per_sweep {
580 let qubit = rng.random_range(0..num_qubits);
582
583 let current_spin = spins[qubit];
585 let new_spin = -current_spin;
586
587 spins[qubit] = new_spin;
589
590 let new_energy = match model.energy(&spins) {
592 Ok(energy) => energy,
593 Err(err) => return Err(AnnealingError::IsingError(err)),
594 };
595
596 let delta_e = new_energy - current_energy;
597
598 let accept = delta_e <= 0.0 || {
600 let p = (-delta_e / temperature).exp();
601 rng.random_range(0.0..1.0) < p
602 };
603
604 if accept {
606 current_energy = new_energy;
607 } else {
608 spins[qubit] = current_spin;
610 }
611 }
612
613 total_sweeps += 1;
615 }
616
617 if current_energy < best_energy {
619 best_energy = current_energy;
620 best_spins = spins.clone();
621 }
622
623 completed_repetitions += 1;
625 }
626
627 let runtime = start_time.elapsed();
629
630 Ok(AnnealingSolution {
632 best_spins,
633 best_energy,
634 repetitions: completed_repetitions,
635 total_sweeps,
636 runtime,
637 info: format!(
638 "Performed {completed_repetitions} repetitions with {total_sweeps} total sweeps in {runtime:?}"
639 ),
640 })
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 #[allow(unused_imports)]
648 use crate::ising::QuboModel;
649
650 #[test]
651 fn test_annealing_params() {
652 let params = AnnealingParams::default();
654
655 assert!(params.validate().is_ok());
657
658 let mut invalid_params = params.clone();
660 invalid_params.initial_temperature = 0.0;
661 assert!(invalid_params.validate().is_err());
662
663 invalid_params = params.clone();
664 invalid_params.num_sweeps = 0;
665 assert!(invalid_params.validate().is_err());
666 }
667
668 #[test]
669 fn test_classical_annealing_simple() {
670 let mut model = IsingModel::new(2);
672 model
673 .set_coupling(0, 1, -1.0)
674 .expect("Failed to set coupling"); let mut params = AnnealingParams::default();
678 params.seed = Some(42);
679 params.num_sweeps = 100;
680 params.num_repetitions = 5;
681
682 let simulator =
683 ClassicalAnnealingSimulator::new(params).expect("Failed to create simulator");
684
685 let result = simulator.solve(&model).expect("Failed to solve model");
687
688 assert_eq!(result.best_spins.len(), 2);
690 assert!(
691 (result.best_spins[0] == 1 && result.best_spins[1] == 1)
692 || (result.best_spins[0] == -1 && result.best_spins[1] == -1)
693 );
694
695 assert_eq!(result.best_energy, -1.0);
697 }
698
699 #[test]
700 fn test_quantum_annealing_simple() {
701 let mut model = IsingModel::new(2);
703 model
704 .set_coupling(0, 1, -1.0)
705 .expect("Failed to set coupling"); let mut params = AnnealingParams::default();
709 params.seed = Some(42);
710 params.num_sweeps = 100;
711 params.num_repetitions = 5;
712 params.trotter_slices = 10;
713
714 let simulator =
715 QuantumAnnealingSimulator::new(params).expect("Failed to create quantum simulator");
716
717 let result = simulator.solve(&model).expect("Failed to solve model");
719
720 assert_eq!(result.best_spins.len(), 2);
722 assert!(
723 (result.best_spins[0] == 1 && result.best_spins[1] == 1)
724 || (result.best_spins[0] == -1 && result.best_spins[1] == -1)
725 );
726
727 assert_eq!(result.best_energy, -1.0);
729 }
730
731 #[test]
732 fn test_classical_annealing_frustrated() {
733 let mut model = IsingModel::new(3);
735 model
736 .set_coupling(0, 1, -1.0)
737 .expect("Failed to set coupling"); model
739 .set_coupling(1, 2, -1.0)
740 .expect("Failed to set coupling"); model
742 .set_coupling(0, 2, 1.0)
743 .expect("Failed to set coupling"); let mut params = AnnealingParams::default();
747 params.seed = Some(42);
748 params.num_sweeps = 200;
749 params.num_repetitions = 10;
750
751 let simulator =
752 ClassicalAnnealingSimulator::new(params).expect("Failed to create simulator");
753
754 let result = simulator.solve(&model).expect("Failed to solve model");
756
757 assert!(result.best_energy <= -1.0);
759 }
760}