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)]
16pub enum AnnealingError {
17 #[error("Ising error: {0}")]
19 IsingError(#[from] IsingError),
20
21 #[error("Invalid annealing schedule: {0}")]
23 InvalidSchedule(String),
24
25 #[error("Invalid annealing parameter: {0}")]
27 InvalidParameter(String),
28
29 #[error("Annealing timeout after {0:?}")]
31 Timeout(Duration),
32}
33
34pub type AnnealingResult<T> = Result<T, AnnealingError>;
36
37#[derive(Debug, Clone)]
42pub enum TransverseFieldSchedule {
43 Linear,
45
46 Exponential(f64), Custom(fn(f64, f64) -> f64),
51}
52
53impl TransverseFieldSchedule {
54 #[must_use]
56 pub fn calculate(&self, t: f64, t_f: f64, a_0: f64) -> f64 {
57 match self {
58 Self::Linear => a_0 * (1.0 - t / t_f),
59 Self::Exponential(alpha) => a_0 * (-alpha * t / t_f).exp(),
60 Self::Custom(func) => func(t, t_f),
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
70pub enum TemperatureSchedule {
71 Linear,
73
74 Exponential(f64), Geometric(f64, f64), Custom(fn(f64, f64) -> f64),
82}
83
84impl TemperatureSchedule {
85 #[must_use]
87 pub fn calculate(&self, t: f64, t_f: f64, t_0: f64) -> f64 {
88 match self {
89 Self::Linear => t_0 * (1.0 - t / t_f),
90 Self::Exponential(alpha) => t_0 * (-alpha * t / t_f).exp(),
91 Self::Geometric(alpha, delta_t) => t_0 * alpha.powf(t / delta_t),
92 Self::Custom(func) => func(t, t_f),
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct AnnealingParams {
100 pub initial_transverse_field: f64,
102
103 pub transverse_field_schedule: TransverseFieldSchedule,
105
106 pub initial_temperature: f64,
108
109 pub final_temperature: f64,
111
112 pub temperature_schedule: TemperatureSchedule,
114
115 pub num_sweeps: usize,
117
118 pub updates_per_sweep: Option<usize>,
120
121 pub num_repetitions: usize,
123
124 pub seed: Option<u64>,
126
127 pub timeout: Option<f64>,
129
130 pub trotter_slices: usize,
132}
133
134impl AnnealingParams {
135 #[must_use]
137 pub const fn new() -> Self {
138 Self {
139 initial_transverse_field: 2.0,
140 transverse_field_schedule: TransverseFieldSchedule::Linear,
141 initial_temperature: 2.0,
142 final_temperature: 0.01,
143 temperature_schedule: TemperatureSchedule::Exponential(3.0),
144 num_sweeps: 1000,
145 updates_per_sweep: None,
146 num_repetitions: 10,
147 seed: None,
148 timeout: Some(60.0), trotter_slices: 20,
150 }
151 }
152
153 pub fn validate(&self) -> AnnealingResult<()> {
155 if self.initial_transverse_field <= 0.0 || !self.initial_transverse_field.is_finite() {
157 return Err(AnnealingError::InvalidParameter(format!(
158 "Initial transverse field must be positive and finite, got {}",
159 self.initial_transverse_field
160 )));
161 }
162
163 if self.initial_temperature <= 0.0 || !self.initial_temperature.is_finite() {
165 return Err(AnnealingError::InvalidParameter(format!(
166 "Initial temperature must be positive and finite, got {}",
167 self.initial_temperature
168 )));
169 }
170
171 if self.final_temperature <= 0.0 || !self.final_temperature.is_finite() {
173 return Err(AnnealingError::InvalidParameter(format!(
174 "Final temperature must be positive and finite, got {}",
175 self.final_temperature
176 )));
177 }
178
179 if self.num_sweeps == 0 {
181 return Err(AnnealingError::InvalidParameter(
182 "Number of sweeps must be positive".to_string(),
183 ));
184 }
185
186 if self.num_repetitions == 0 {
188 return Err(AnnealingError::InvalidParameter(
189 "Number of repetitions must be positive".to_string(),
190 ));
191 }
192
193 if let Some(timeout) = self.timeout {
195 if timeout <= 0.0 || !timeout.is_finite() {
196 return Err(AnnealingError::InvalidParameter(format!(
197 "Timeout must be positive and finite, got {timeout}"
198 )));
199 }
200 }
201
202 if self.trotter_slices == 0 {
204 return Err(AnnealingError::InvalidParameter(
205 "Number of Trotter slices must be positive".to_string(),
206 ));
207 }
208
209 Ok(())
210 }
211}
212
213impl Default for AnnealingParams {
214 fn default() -> Self {
215 Self::new()
216 }
217}
218
219#[derive(Debug, Clone)]
221pub struct AnnealingSolution {
222 pub best_spins: Vec<i8>,
224
225 pub best_energy: f64,
227
228 pub repetitions: usize,
230
231 pub total_sweeps: usize,
233
234 pub runtime: Duration,
236
237 pub info: String,
239}
240
241#[derive(Debug, Clone)]
246pub struct QuantumAnnealingSimulator {
247 params: AnnealingParams,
249}
250
251impl QuantumAnnealingSimulator {
252 pub fn new(params: AnnealingParams) -> AnnealingResult<Self> {
254 params.validate()?;
256
257 Ok(Self { params })
258 }
259
260 pub fn with_default_params() -> AnnealingResult<Self> {
262 Self::new(AnnealingParams::default())
263 }
264}
265
266impl Default for QuantumAnnealingSimulator {
267 fn default() -> Self {
268 Self::with_default_params().expect("Default parameters should be valid")
269 }
270}
271
272impl QuantumAnnealingSimulator {
273 pub fn solve(&self, model: &IsingModel) -> AnnealingResult<AnnealingSolution> {
275 let start_time = Instant::now();
277
278 let mut rng = match self.params.seed {
280 Some(seed) => ChaCha8Rng::seed_from_u64(seed),
281 None => ChaCha8Rng::seed_from_u64(thread_rng().gen()),
282 };
283
284 let num_qubits = model.num_qubits;
286 let mut best_spins = vec![1; num_qubits]; let mut best_energy = match model.energy(&best_spins) {
288 Ok(energy) => energy,
289 Err(err) => return Err(AnnealingError::IsingError(err)),
290 };
291
292 let mut total_sweeps = 0;
294 let mut completed_repetitions = 0;
295
296 let updates_per_sweep = self.params.updates_per_sweep.unwrap_or(num_qubits);
298
299 let trotter_slices = self.params.trotter_slices;
301
302 for _ in 0..self.params.num_repetitions {
304 let mut trotter_spins = vec![vec![0; num_qubits]; trotter_slices];
306 for slice in &mut trotter_spins {
307 for spin in slice.iter_mut() {
308 *spin = if rng.random_bool(0.5) { 1 } else { -1 };
309 }
310 }
311
312 for sweep in 0..self.params.num_sweeps {
314 if let Some(timeout) = self.params.timeout {
316 let elapsed = start_time.elapsed().as_secs_f64();
317 if elapsed > timeout {
318 return Err(AnnealingError::Timeout(Duration::from_secs_f64(elapsed)));
319 }
320 }
321
322 let t = sweep as f64 / self.params.num_sweeps as f64;
324 let t_f = 1.0; let transverse_field = self.params.transverse_field_schedule.calculate(
328 t,
329 t_f,
330 self.params.initial_transverse_field,
331 );
332 let temperature = self.params.temperature_schedule.calculate(
333 t,
334 t_f,
335 self.params.initial_temperature,
336 );
337
338 let j_perp = -0.5
340 * temperature
341 * (trotter_slices as f64)
342 * (transverse_field / temperature).ln_1p().abs();
343
344 for _ in 0..updates_per_sweep {
346 let qubit = rng.random_range(0..num_qubits);
348 let slice = rng.random_range(0..trotter_slices);
349
350 let current_spin = trotter_spins[slice][qubit];
352 let new_spin = -current_spin;
353
354 trotter_spins[slice][qubit] = new_spin;
356
357 let mut delta_e = match model.energy(&trotter_spins[slice]) {
359 Ok(energy_new) => {
360 trotter_spins[slice][qubit] = current_spin;
362 let energy_old = model.energy(&trotter_spins[slice])?;
363 energy_new - energy_old
364 }
365 Err(err) => return Err(AnnealingError::IsingError(err)),
366 };
367
368 let prev_slice = (slice + trotter_slices - 1) % trotter_slices;
370 let next_slice = (slice + 1) % trotter_slices;
371
372 let new_spin_f64 = f64::from(new_spin);
374 let current_spin_f64 = f64::from(current_spin);
375 let neighbor_sum = f64::from(
376 trotter_spins[prev_slice][qubit] + trotter_spins[next_slice][qubit],
377 );
378
379 delta_e += j_perp * new_spin_f64 * neighbor_sum;
380 delta_e -= j_perp * current_spin_f64 * neighbor_sum;
381
382 let accept = delta_e <= 0.0 || {
384 let p = (-delta_e / temperature).exp();
385 rng.random_range(0.0..1.0) < p
386 };
387
388 if accept {
390 trotter_spins[slice][qubit] = new_spin;
391 }
392 }
393
394 total_sweeps += 1;
396 }
397
398 let mut avg_spins = vec![0; num_qubits];
400 for qubit in 0..num_qubits {
401 let sum: i32 = trotter_spins
402 .iter()
403 .map(|slice| i32::from(slice[qubit]))
404 .sum();
405 avg_spins[qubit] = if sum >= 0 { 1 } else { -1 };
406 }
407
408 match model.energy(&avg_spins) {
410 Ok(energy) => {
411 if energy < best_energy {
412 best_energy = energy;
413 best_spins = avg_spins;
414 }
415 }
416 Err(err) => return Err(AnnealingError::IsingError(err)),
417 }
418
419 completed_repetitions += 1;
421 }
422
423 let runtime = start_time.elapsed();
425
426 Ok(AnnealingSolution {
428 best_spins,
429 best_energy,
430 repetitions: completed_repetitions,
431 total_sweeps,
432 runtime,
433 info: format!(
434 "Performed {completed_repetitions} repetitions with {total_sweeps} total sweeps in {runtime:?}"
435 ),
436 })
437 }
438}
439
440#[derive(Debug, Clone)]
445pub struct ClassicalAnnealingSimulator {
446 params: AnnealingParams,
448}
449
450impl ClassicalAnnealingSimulator {
451 pub fn new(params: AnnealingParams) -> AnnealingResult<Self> {
453 params.validate()?;
455
456 Ok(Self { params })
457 }
458
459 pub fn with_default_params() -> AnnealingResult<Self> {
461 Self::new(AnnealingParams::default())
462 }
463}
464
465impl Default for ClassicalAnnealingSimulator {
466 fn default() -> Self {
467 Self::with_default_params().expect("Default parameters should be valid")
468 }
469}
470
471impl ClassicalAnnealingSimulator {
472 pub fn solve(&self, model: &IsingModel) -> AnnealingResult<AnnealingSolution> {
474 let start_time = Instant::now();
476
477 let mut rng = match self.params.seed {
479 Some(seed) => ChaCha8Rng::seed_from_u64(seed),
480 None => ChaCha8Rng::seed_from_u64(thread_rng().gen()),
481 };
482
483 let num_qubits = model.num_qubits;
485 let mut best_spins = vec![1; num_qubits]; let mut best_energy = match model.energy(&best_spins) {
487 Ok(energy) => energy,
488 Err(err) => return Err(AnnealingError::IsingError(err)),
489 };
490
491 let mut total_sweeps = 0;
493 let mut completed_repetitions = 0;
494
495 let updates_per_sweep = self.params.updates_per_sweep.unwrap_or(num_qubits);
497
498 for _ in 0..self.params.num_repetitions {
500 let mut spins = vec![0; num_qubits];
502 for spin in &mut spins {
503 *spin = if rng.random_bool(0.5) { 1 } else { -1 };
504 }
505
506 let mut current_energy = match model.energy(&spins) {
508 Ok(energy) => energy,
509 Err(err) => return Err(AnnealingError::IsingError(err)),
510 };
511
512 for sweep in 0..self.params.num_sweeps {
514 if let Some(timeout) = self.params.timeout {
516 let elapsed = start_time.elapsed().as_secs_f64();
517 if elapsed > timeout {
518 return Err(AnnealingError::Timeout(Duration::from_secs_f64(elapsed)));
519 }
520 }
521
522 let t = sweep as f64 / self.params.num_sweeps as f64;
524 let t_f = 1.0; let temperature = self.params.temperature_schedule.calculate(
528 t,
529 t_f,
530 self.params.initial_temperature,
531 );
532
533 for _ in 0..updates_per_sweep {
535 let qubit = rng.random_range(0..num_qubits);
537
538 let current_spin = spins[qubit];
540 let new_spin = -current_spin;
541
542 spins[qubit] = new_spin;
544
545 let new_energy = match model.energy(&spins) {
547 Ok(energy) => energy,
548 Err(err) => return Err(AnnealingError::IsingError(err)),
549 };
550
551 let delta_e = new_energy - current_energy;
552
553 let accept = delta_e <= 0.0 || {
555 let p = (-delta_e / temperature).exp();
556 rng.random_range(0.0..1.0) < p
557 };
558
559 if accept {
561 current_energy = new_energy;
562 } else {
563 spins[qubit] = current_spin;
565 }
566 }
567
568 total_sweeps += 1;
570 }
571
572 if current_energy < best_energy {
574 best_energy = current_energy;
575 best_spins = spins.clone();
576 }
577
578 completed_repetitions += 1;
580 }
581
582 let runtime = start_time.elapsed();
584
585 Ok(AnnealingSolution {
587 best_spins,
588 best_energy,
589 repetitions: completed_repetitions,
590 total_sweeps,
591 runtime,
592 info: format!(
593 "Performed {completed_repetitions} repetitions with {total_sweeps} total sweeps in {runtime:?}"
594 ),
595 })
596 }
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602 #[allow(unused_imports)]
603 use crate::ising::QuboModel;
604
605 #[test]
606 fn test_annealing_params() {
607 let params = AnnealingParams::default();
609
610 assert!(params.validate().is_ok());
612
613 let mut invalid_params = params.clone();
615 invalid_params.initial_temperature = 0.0;
616 assert!(invalid_params.validate().is_err());
617
618 invalid_params = params.clone();
619 invalid_params.num_sweeps = 0;
620 assert!(invalid_params.validate().is_err());
621 }
622
623 #[test]
624 fn test_classical_annealing_simple() {
625 let mut model = IsingModel::new(2);
627 model
628 .set_coupling(0, 1, -1.0)
629 .expect("Failed to set coupling"); let mut params = AnnealingParams::default();
633 params.seed = Some(42);
634 params.num_sweeps = 100;
635 params.num_repetitions = 5;
636
637 let simulator =
638 ClassicalAnnealingSimulator::new(params).expect("Failed to create simulator");
639
640 let result = simulator.solve(&model).expect("Failed to solve model");
642
643 assert_eq!(result.best_spins.len(), 2);
645 assert!(
646 (result.best_spins[0] == 1 && result.best_spins[1] == 1)
647 || (result.best_spins[0] == -1 && result.best_spins[1] == -1)
648 );
649
650 assert_eq!(result.best_energy, -1.0);
652 }
653
654 #[test]
655 fn test_quantum_annealing_simple() {
656 let mut model = IsingModel::new(2);
658 model
659 .set_coupling(0, 1, -1.0)
660 .expect("Failed to set coupling"); let mut params = AnnealingParams::default();
664 params.seed = Some(42);
665 params.num_sweeps = 100;
666 params.num_repetitions = 5;
667 params.trotter_slices = 10;
668
669 let simulator =
670 QuantumAnnealingSimulator::new(params).expect("Failed to create quantum simulator");
671
672 let result = simulator.solve(&model).expect("Failed to solve model");
674
675 assert_eq!(result.best_spins.len(), 2);
677 assert!(
678 (result.best_spins[0] == 1 && result.best_spins[1] == 1)
679 || (result.best_spins[0] == -1 && result.best_spins[1] == -1)
680 );
681
682 assert_eq!(result.best_energy, -1.0);
684 }
685
686 #[test]
687 fn test_classical_annealing_frustrated() {
688 let mut model = IsingModel::new(3);
690 model
691 .set_coupling(0, 1, -1.0)
692 .expect("Failed to set coupling"); model
694 .set_coupling(1, 2, -1.0)
695 .expect("Failed to set coupling"); model
697 .set_coupling(0, 2, 1.0)
698 .expect("Failed to set coupling"); let mut params = AnnealingParams::default();
702 params.seed = Some(42);
703 params.num_sweeps = 200;
704 params.num_repetitions = 10;
705
706 let simulator =
707 ClassicalAnnealingSimulator::new(params).expect("Failed to create simulator");
708
709 let result = simulator.solve(&model).expect("Failed to solve model");
711
712 assert!(result.best_energy <= -1.0);
714 }
715}