1use super::sa::{adaptive_schedule, Anneal, Schedule};
20use super::SaResult;
21use crate::rng::{mix_seed, Rng};
22
23#[derive(Debug, Clone, Copy)]
25pub struct ParallelTempering {
26 pub replicas: usize,
28 pub iterations: usize,
31 pub schedule: Schedule,
33 pub seed: u64,
35}
36
37impl Default for ParallelTempering {
38 fn default() -> Self {
39 ParallelTempering {
40 replicas: 8,
41 iterations: 100_000,
42 schedule: Schedule::adaptive(),
43 seed: 0,
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct TemperingResult<S> {
51 pub best: SaResult<S>,
53 pub replicas: Vec<SaResult<S>>,
56}
57
58struct Replica<S> {
60 state: S,
61 energy: f64,
62 temp: f64,
63 rng: Rng,
64 best_state: S,
65 best_energy: f64,
66}
67
68impl ParallelTempering {
69 pub fn optimize<A>(&self, problem: &A) -> TemperingResult<A::State>
72 where
73 A: Anneal + Sync,
74 A::State: Send,
75 {
76 let k = self.replicas.max(2);
77 let mut swap_rng = Rng::new(self.seed);
79 let (t_hot, t_cold) = match self.schedule {
80 Schedule::Geometric { t_start, t_end } => (t_start, t_end),
81 Schedule::Adaptive { fallback } => adaptive_schedule(problem, fallback, &mut swap_rng),
82 };
83
84 let temps: Vec<f64> = (0..k)
86 .map(|i| {
87 let frac = i as f64 / (k - 1) as f64;
88 t_cold * (t_hot / t_cold).powf(frac)
89 })
90 .collect();
91
92 let mut replicas: Vec<Replica<A::State>> = (0..k)
93 .map(|i| {
94 let mut rng = Rng::new(mix_seed(self.seed, i as u64 + 1));
96 let state = problem.initial(&mut rng);
97 let energy = problem.energy(&state);
98 Replica {
99 best_state: state.clone(),
100 best_energy: energy,
101 state,
102 energy,
103 temp: temps[i],
104 rng,
105 }
106 })
107 .collect();
108
109 let sweep_len = problem.sweep_len();
110 if sweep_len > 0 && self.iterations > 0 {
111 let n_sweeps = (self.iterations / sweep_len).max(1);
112 const FLOOR: f64 = 1e-2;
115 let cool = FLOOR.powf(1.0 / n_sweeps as f64);
116 let mut scale = 1.0;
117 for _ in 0..n_sweeps {
118 for (r, &base) in replicas.iter_mut().zip(&temps) {
119 r.temp = base * scale;
120 }
121 advance_all(problem, &mut replicas, sweep_len);
122 attempt_swaps(&mut replicas, &mut swap_rng);
123 scale *= cool;
124 }
125 }
126
127 let replica_results: Vec<SaResult<A::State>> = replicas
128 .into_iter()
129 .map(|r| SaResult {
130 best: r.best_state,
131 energy: r.best_energy,
132 })
133 .collect();
134
135 let best_idx = (0..replica_results.len())
138 .min_by(|&a, &b| {
139 replica_results[a]
140 .energy
141 .partial_cmp(&replica_results[b].energy)
142 .unwrap_or(std::cmp::Ordering::Equal)
143 .then(a.cmp(&b))
144 })
145 .expect("at least two replicas");
146 let best = replica_results[best_idx].clone();
147
148 TemperingResult {
149 best,
150 replicas: replica_results,
151 }
152 }
153}
154
155#[cfg(feature = "rayon")]
159fn advance_all<A>(problem: &A, replicas: &mut [Replica<A::State>], sweep_len: usize)
160where
161 A: Anneal + Sync,
162 A::State: Send,
163{
164 use rayon::prelude::*;
165 replicas
166 .par_iter_mut()
167 .for_each(|r| advance(problem, r, sweep_len));
168}
169
170#[cfg(not(feature = "rayon"))]
171fn advance_all<A: Anneal>(problem: &A, replicas: &mut [Replica<A::State>], sweep_len: usize) {
172 for r in replicas.iter_mut() {
173 advance(problem, r, sweep_len);
174 }
175}
176
177fn advance<A: Anneal>(problem: &A, r: &mut Replica<A::State>, n_moves: usize) {
179 for _ in 0..n_moves {
180 let mv = problem.propose(&r.state, &mut r.rng);
181 let delta = problem.delta(&r.state, &mv);
182 if delta < 0.0 || r.rng.uniform() < (-delta / r.temp).exp() {
183 problem.apply(&mut r.state, mv);
184 r.energy += delta;
185 if r.energy < r.best_energy {
186 r.best_energy = r.energy;
187 r.best_state = r.state.clone();
188 }
189 }
190 }
191}
192
193fn attempt_swaps<S>(replicas: &mut [Replica<S>], swap_rng: &mut Rng) {
196 for c in 0..replicas.len() - 1 {
197 let beta_cold = 1.0 / replicas[c].temp;
198 let beta_hot = 1.0 / replicas[c + 1].temp;
199 let arg = (beta_cold - beta_hot) * (replicas[c].energy - replicas[c + 1].energy);
200 if arg >= 0.0 || swap_rng.uniform() < arg.exp() {
201 let (left, right) = replicas.split_at_mut(c + 1);
202 std::mem::swap(&mut left[c].state, &mut right[0].state);
203 std::mem::swap(&mut left[c].energy, &mut right[0].energy);
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 struct Partition {
215 weights: Vec<f64>,
216 }
217 impl Partition {
218 fn diff(&self, x: &[bool]) -> f64 {
219 self.weights
220 .iter()
221 .zip(x)
222 .map(|(w, &b)| if b { *w } else { -*w })
223 .sum()
224 }
225 }
226 impl Anneal for Partition {
227 type State = Vec<bool>;
228 type Move = usize;
229 fn initial(&self, _r: &mut Rng) -> Vec<bool> {
230 vec![false; self.weights.len()]
231 }
232 fn energy(&self, x: &Vec<bool>) -> f64 {
233 self.diff(x).powi(2)
234 }
235 fn propose(&self, x: &Vec<bool>, r: &mut Rng) -> usize {
236 r.index(x.len())
237 }
238 fn delta(&self, x: &Vec<bool>, &i: &usize) -> f64 {
239 let d = self.diff(x);
240 let step = if x[i] { -2.0 } else { 2.0 } * self.weights[i];
241 (d + step).powi(2) - d.powi(2)
242 }
243 fn apply(&self, x: &mut Vec<bool>, i: usize) {
244 x[i] = !x[i];
245 }
246 fn sweep_len(&self) -> usize {
247 self.weights.len()
248 }
249 }
250
251 #[test]
252 fn finds_perfect_partition() {
253 let p = Partition {
254 weights: vec![8.0, 7.0, 6.0, 5.0, 4.0, 9.0, 3.0, 2.0],
255 };
256 let pt = ParallelTempering {
257 replicas: 6,
258 iterations: 8000,
259 schedule: Schedule::adaptive(),
260 seed: 1,
261 };
262 let res = pt.optimize(&p);
263 assert!(res.best.energy < 1e-9, "energy {}", res.best.energy);
264 assert_eq!(res.replicas.len(), 6);
265 }
266
267 #[test]
268 fn is_deterministic() {
269 let p = Partition {
270 weights: vec![5.0, 3.0, 9.0, 7.0, 1.0, 8.0, 4.0],
271 };
272 let pt = ParallelTempering {
273 replicas: 5,
274 iterations: 5000,
275 schedule: Schedule::Geometric {
276 t_start: 50.0,
277 t_end: 0.01,
278 },
279 seed: 7,
280 };
281 let a = pt.optimize(&p);
282 let b = pt.optimize(&p);
283 assert_eq!(a.best.best, b.best.best);
284 assert_eq!(a.best.energy, b.best.energy);
285 }
286}