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.random::<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.random::<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.random::<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.random_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.random::<f64>() < (-delta_energy / temperature).exp()
137 {
138 chain[idx] = new_value;
139 }
140 }
141 }
142
143 fn calculate_energy(&self, config: &[i32], matrix: &Array<f64, Ix2>) -> f64 {
145 let mut energy = 0.0;
146 let n = config.len();
147
148 for i in 0..n {
149 for j in 0..n {
150 energy += matrix[[i, j]] * config[i] as f64 * config[j] as f64;
151 }
152 }
153
154 energy
155 }
156}
157
158impl Sampler for ParallelTemperingSampler {
159 fn run_qubo(
160 &self,
161 qubo: &(Array<f64, Ix2>, HashMap<String, usize>),
162 num_reads: usize,
163 ) -> SamplerResult<Vec<SampleResult>> {
164 let (matrix, var_map) = qubo;
165 let mut sampler = Self {
166 num_chains: self.num_chains,
167 temperatures: self.temperatures.clone(),
168 sweeps: self.sweeps,
169 rng: StdRng::from_seed([42; 32]),
170 };
171 sampler.run_parallel_tempering(matrix, var_map, num_reads)
172 }
173
174 fn run_hobo(
175 &self,
176 hobo: &(Array<f64, IxDyn>, HashMap<String, usize>),
177 shots: usize,
178 ) -> SamplerResult<Vec<SampleResult>> {
179 let (matrix_dyn, var_map) = hobo;
181
182 if matrix_dyn.ndim() != 2 {
183 return Err(SamplerError::InvalidParameter(
184 "HOBO matrix must be 2D for parallel tempering".into(),
185 ));
186 }
187
188 let matrix_2d = matrix_dyn
189 .clone()
190 .into_dimensionality::<Ix2>()
191 .map_err(|_| SamplerError::InvalidParameter("Failed to convert matrix to 2D".into()))?;
192
193 let mut sampler = Self {
194 num_chains: self.num_chains,
195 temperatures: self.temperatures.clone(),
196 sweeps: self.sweeps,
197 rng: StdRng::from_seed([42; 32]),
198 };
199 sampler.run_parallel_tempering(&matrix_2d, var_map, shots)
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use scirs2_core::ndarray::Array2;
207
208 #[test]
209 fn test_parallel_tempering_basic() {
210 let mut matrix = Array2::<f64>::zeros((2, 2));
211 matrix[[0, 0]] = -1.0;
212 matrix[[1, 1]] = -1.0;
213 matrix[[0, 1]] = 2.0;
214 matrix[[1, 0]] = 2.0;
215
216 let mut var_map = HashMap::new();
217 var_map.insert("x".to_string(), 0);
218 var_map.insert("y".to_string(), 1);
219
220 let sampler = ParallelTemperingSampler::new(Some(4), Some(100));
221 let result = sampler.run_qubo(&(matrix, var_map), 10);
222
223 assert!(result.is_ok());
224 let solutions = result.expect("run_qubo should return valid solutions");
225 assert_eq!(solutions.len(), 10);
226 }
227}