quantrs2_tytan/
parallel_tempering.rs1#![allow(dead_code)]
7
8use crate::sampler::{SampleResult, Sampler, SamplerError, SamplerResult};
9use scirs2_core::ndarray::{Array, Ix2, IxDyn};
10use scirs2_core::random::prelude::*;
11use std::collections::HashMap;
12
13pub struct ParallelTemperingSampler {
15 num_chains: usize,
17 temperatures: Vec<f64>,
19 sweeps: usize,
21 rng: StdRng,
23}
24
25impl ParallelTemperingSampler {
26 pub fn new(num_chains: Option<usize>, sweeps: Option<usize>) -> Self {
28 let num_chains = num_chains.unwrap_or(8);
29 let sweeps = sweeps.unwrap_or(1000);
30
31 let temperatures = (0..num_chains)
33 .map(|i| 1.0 * (10.0_f64).powf(i as f64 / (num_chains - 1) as f64))
34 .collect();
35
36 Self {
37 num_chains,
38 temperatures,
39 sweeps,
40 rng: StdRng::from_seed([42; 32]),
41 }
42 }
43
44 pub fn with_temperatures(mut self, temperatures: Vec<f64>) -> Self {
46 self.num_chains = temperatures.len();
47 self.temperatures = temperatures;
48 self
49 }
50
51 fn run_parallel_tempering(
53 &mut self,
54 matrix: &Array<f64, Ix2>,
55 var_map: &HashMap<String, usize>,
56 num_reads: usize,
57 ) -> SamplerResult<Vec<SampleResult>> {
58 let n = matrix.nrows();
59 let mut best_solutions = Vec::new();
60
61 for _ in 0..num_reads {
62 let mut chains: Vec<Vec<i32>> = (0..self.num_chains)
64 .map(|_| {
65 (0..n)
66 .map(|_| i32::from(self.rng.gen::<f64>() >= 0.5))
67 .collect()
68 })
69 .collect();
70
71 for _ in 0..self.sweeps {
73 for (chain_idx, chain) in chains.iter_mut().enumerate() {
75 let temperature = self.temperatures[chain_idx];
76 self.metropolis_update(chain, matrix, temperature);
77 }
78
79 for i in 0..(self.num_chains - 1) {
81 if self.rng.gen::<f64>() < 0.1 {
82 let energy_i = self.calculate_energy(&chains[i], matrix);
84 let energy_j = self.calculate_energy(&chains[i + 1], matrix);
85
86 let delta = (energy_j - energy_i)
87 * (1.0 / self.temperatures[i] - 1.0 / self.temperatures[i + 1]);
88
89 if delta <= 0.0 || self.rng.gen::<f64>() < (-delta).exp() {
90 chains.swap(i, i + 1);
91 }
92 }
93 }
94 }
95
96 let best_chain = &chains[0];
98 let energy = self.calculate_energy(best_chain, matrix);
99
100 let mut assignments = HashMap::new();
101 for (var_name, &idx) in var_map {
102 assignments.insert(var_name.clone(), best_chain[idx] == 1);
103 }
104
105 best_solutions.push(SampleResult {
106 assignments,
107 energy,
108 occurrences: 1,
109 });
110 }
111
112 Ok(best_solutions)
113 }
114
115 fn metropolis_update(
117 &mut self,
118 chain: &mut Vec<i32>,
119 matrix: &Array<f64, Ix2>,
120 temperature: f64,
121 ) {
122 let n = chain.len();
123
124 for _ in 0..n {
125 let idx = self.rng.gen_range(0..n);
126 let old_value = chain[idx];
127 let new_value = 1 - old_value;
128
129 let old_energy = self.calculate_energy(chain, matrix);
130 chain[idx] = new_value;
131 let new_energy = self.calculate_energy(chain, matrix);
132 chain[idx] = old_value;
133
134 let delta_energy = new_energy - old_energy;
135
136 if delta_energy <= 0.0 || self.rng.gen::<f64>() < (-delta_energy / temperature).exp() {
137 chain[idx] = new_value;
138 }
139 }
140 }
141
142 fn calculate_energy(&self, config: &[i32], matrix: &Array<f64, Ix2>) -> f64 {
144 let mut energy = 0.0;
145 let n = config.len();
146
147 for i in 0..n {
148 for j in 0..n {
149 energy += matrix[[i, j]] * config[i] as f64 * config[j] as f64;
150 }
151 }
152
153 energy
154 }
155}
156
157impl Sampler for ParallelTemperingSampler {
158 fn run_qubo(
159 &self,
160 qubo: &(Array<f64, Ix2>, HashMap<String, usize>),
161 num_reads: usize,
162 ) -> SamplerResult<Vec<SampleResult>> {
163 let (matrix, var_map) = qubo;
164 let mut sampler = Self {
165 num_chains: self.num_chains,
166 temperatures: self.temperatures.clone(),
167 sweeps: self.sweeps,
168 rng: StdRng::from_seed([42; 32]),
169 };
170 sampler.run_parallel_tempering(matrix, var_map, num_reads)
171 }
172
173 fn run_hobo(
174 &self,
175 hobo: &(Array<f64, IxDyn>, HashMap<String, usize>),
176 shots: usize,
177 ) -> SamplerResult<Vec<SampleResult>> {
178 let (matrix_dyn, var_map) = hobo;
180
181 if matrix_dyn.ndim() != 2 {
182 return Err(SamplerError::InvalidParameter(
183 "HOBO matrix must be 2D for parallel tempering".into(),
184 ));
185 }
186
187 let matrix_2d = matrix_dyn
188 .clone()
189 .into_dimensionality::<Ix2>()
190 .map_err(|_| SamplerError::InvalidParameter("Failed to convert matrix to 2D".into()))?;
191
192 let mut sampler = Self {
193 num_chains: self.num_chains,
194 temperatures: self.temperatures.clone(),
195 sweeps: self.sweeps,
196 rng: StdRng::from_seed([42; 32]),
197 };
198 sampler.run_parallel_tempering(&matrix_2d, var_map, shots)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use scirs2_core::ndarray::Array2;
206
207 #[test]
208 fn test_parallel_tempering_basic() {
209 let mut matrix = Array2::<f64>::zeros((2, 2));
210 matrix[[0, 0]] = -1.0;
211 matrix[[1, 1]] = -1.0;
212 matrix[[0, 1]] = 2.0;
213 matrix[[1, 0]] = 2.0;
214
215 let mut var_map = HashMap::new();
216 var_map.insert("x".to_string(), 0);
217 var_map.insert("y".to_string(), 1);
218
219 let sampler = ParallelTemperingSampler::new(Some(4), Some(100));
220 let result = sampler.run_qubo(&(matrix, var_map), 10);
221
222 assert!(result.is_ok());
223 let solutions = result.expect("run_qubo should return valid solutions");
224 assert_eq!(solutions.len(), 10);
225 }
226}