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
130 .into_iter()
131 .map(|r| {
132 let energy = problem.energy(&r.best_state);
133 SaResult {
134 best: r.best_state,
135 energy,
136 }
137 })
138 .collect();
139
140 let best_idx = (0..replica_results.len())
143 .min_by(|&a, &b| {
144 replica_results[a]
145 .energy
146 .partial_cmp(&replica_results[b].energy)
147 .unwrap_or(std::cmp::Ordering::Equal)
148 .then(a.cmp(&b))
149 })
150 .expect("at least two replicas");
151 let best = replica_results[best_idx].clone();
152
153 TemperingResult {
154 best,
155 replicas: replica_results,
156 }
157 }
158}
159
160#[cfg(feature = "rayon")]
164fn advance_all<A>(problem: &A, replicas: &mut [Replica<A::State>], sweep_len: usize)
165where
166 A: Anneal + Sync,
167 A::State: Send,
168{
169 use rayon::prelude::*;
170 replicas
171 .par_iter_mut()
172 .for_each(|r| advance(problem, r, sweep_len));
173}
174
175#[cfg(not(feature = "rayon"))]
176fn advance_all<A: Anneal>(problem: &A, replicas: &mut [Replica<A::State>], sweep_len: usize) {
177 for r in replicas.iter_mut() {
178 advance(problem, r, sweep_len);
179 }
180}
181
182fn advance<A: Anneal>(problem: &A, r: &mut Replica<A::State>, n_moves: usize) {
184 for _ in 0..n_moves {
185 let mv = problem.propose(&r.state, &mut r.rng);
186 let delta = problem.delta(&r.state, &mv);
187 if delta < 0.0 || r.rng.uniform() < (-delta / r.temp).exp() {
188 problem.apply(&mut r.state, mv);
189 r.energy += delta;
190 if r.energy < r.best_energy {
191 r.best_energy = r.energy;
192 r.best_state = r.state.clone();
193 }
194 }
195 }
196}
197
198fn attempt_swaps<S>(replicas: &mut [Replica<S>], swap_rng: &mut Rng) {
206 for c in 0..replicas.len() - 1 {
207 let beta_cold = 1.0 / replicas[c].temp;
208 let beta_hot = 1.0 / replicas[c + 1].temp;
209 let arg = (beta_cold - beta_hot) * (replicas[c].energy - replicas[c + 1].energy);
210 if arg >= 0.0 || swap_rng.uniform() < arg.exp() {
211 let (left, right) = replicas.split_at_mut(c + 1);
212 std::mem::swap(&mut left[c].state, &mut right[0].state);
213 std::mem::swap(&mut left[c].energy, &mut right[0].energy);
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 struct Partition {
225 weights: Vec<f64>,
226 }
227 impl Partition {
228 fn diff(&self, x: &[bool]) -> f64 {
229 self.weights
230 .iter()
231 .zip(x)
232 .map(|(w, &b)| if b { *w } else { -*w })
233 .sum()
234 }
235 }
236 impl Anneal for Partition {
237 type State = Vec<bool>;
238 type Move = usize;
239 fn initial(&self, _r: &mut Rng) -> Vec<bool> {
240 vec![false; self.weights.len()]
241 }
242 fn energy(&self, x: &Vec<bool>) -> f64 {
243 self.diff(x).powi(2)
244 }
245 fn propose(&self, x: &Vec<bool>, r: &mut Rng) -> usize {
246 r.index(x.len())
247 }
248 fn delta(&self, x: &Vec<bool>, &i: &usize) -> f64 {
249 let d = self.diff(x);
250 let step = if x[i] { -2.0 } else { 2.0 } * self.weights[i];
251 (d + step).powi(2) - d.powi(2)
252 }
253 fn apply(&self, x: &mut Vec<bool>, i: usize) {
254 x[i] = !x[i];
255 }
256 fn sweep_len(&self) -> usize {
257 self.weights.len()
258 }
259 }
260
261 #[test]
262 fn finds_perfect_partition() {
263 let p = Partition {
264 weights: vec![8.0, 7.0, 6.0, 5.0, 4.0, 9.0, 3.0, 2.0],
265 };
266 let pt = ParallelTempering {
267 replicas: 6,
268 iterations: 8000,
269 schedule: Schedule::adaptive(),
270 seed: 1,
271 };
272 let res = pt.optimize(&p);
273 assert!(res.best.energy < 1e-9, "energy {}", res.best.energy);
274 assert_eq!(res.replicas.len(), 6);
275 }
276
277 #[test]
278 fn is_deterministic() {
279 let p = Partition {
280 weights: vec![5.0, 3.0, 9.0, 7.0, 1.0, 8.0, 4.0],
281 };
282 let pt = ParallelTempering {
283 replicas: 5,
284 iterations: 5000,
285 schedule: Schedule::Geometric {
286 t_start: 50.0,
287 t_end: 0.01,
288 },
289 seed: 7,
290 };
291 let a = pt.optimize(&p);
292 let b = pt.optimize(&p);
293 assert_eq!(a.best.best, b.best.best);
294 assert_eq!(a.best.energy, b.best.energy);
295 }
296}