Skip to main content

quantrs2_tytan/
parallel_tempering.rs

1//! Parallel tempering implementation for enhanced sampling.
2//!
3//! This module provides parallel tempering algorithms for better
4//! exploration of the solution space in quantum annealing problems.
5
6#![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
13/// Parallel tempering sampler that runs multiple chains at different temperatures
14pub struct ParallelTemperingSampler {
15    /// Number of parallel chains
16    num_chains: usize,
17    /// Temperature schedule for the chains
18    temperatures: Vec<f64>,
19    /// Number of sweeps per chain
20    sweeps: usize,
21    /// Random number generator
22    rng: StdRng,
23}
24
25impl ParallelTemperingSampler {
26    /// Create a new parallel tempering sampler
27    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        // Create geometric temperature schedule
32        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    /// Set custom temperature schedule
45    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    /// Run parallel tempering on a QUBO problem
52    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            // Initialize chains with random states
63            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            // Run parallel tempering
72            for _ in 0..self.sweeps {
73                // Update each chain
74                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                // Attempt replica exchanges
80                for i in 0..(self.num_chains - 1) {
81                    if self.rng.random::<f64>() < 0.1 {
82                        // 10% exchange probability
83                        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            // Extract best solution (from lowest temperature chain)
97            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    /// Perform a Metropolis update on a chain
116    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    /// Calculate energy of a configuration
144    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        // Convert dynamic array to 2D for QUBO processing
180        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}