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.gen::<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.gen::<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.gen::<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.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    /// Calculate energy of a configuration
143    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        // Convert dynamic array to 2D for QUBO processing
179        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}