1use scirs2_core::random::prelude::*;
8use scirs2_core::random::ChaCha8Rng;
9use scirs2_core::random::{Rng, SeedableRng};
10use std::collections::HashMap;
11use std::time::{Duration, Instant};
12use thiserror::Error;
13
14use crate::ising::{IsingError, IsingModel};
15use crate::simulator::{
16 AnnealingParams, AnnealingSolution, TemperatureSchedule, TransverseFieldSchedule,
17};
18
19#[derive(Error, Debug)]
21pub enum PopulationAnnealingError {
22 #[error("Ising error: {0}")]
24 IsingError(#[from] IsingError),
25
26 #[error("Invalid parameter: {0}")]
28 InvalidParameter(String),
29
30 #[error("MPI error: {0}")]
32 MpiError(String),
33
34 #[error("Population evolution error: {0}")]
36 EvolutionError(String),
37}
38
39pub type PopulationAnnealingResult<T> = Result<T, PopulationAnnealingError>;
41
42#[derive(Debug, Clone)]
44pub struct PopulationAnnealingConfig {
45 pub population_size: usize,
47
48 pub temperature_schedule: TemperatureSchedule,
50
51 pub initial_temperature: f64,
53
54 pub final_temperature: f64,
56
57 pub num_temperature_steps: usize,
59
60 pub sweeps_per_step: usize,
62
63 pub resampling_frequency: usize,
65
66 pub ess_threshold: f64,
68
69 pub seed: Option<u64>,
71
72 pub timeout: Option<Duration>,
74
75 pub mpi_config: Option<MpiConfig>,
77}
78
79impl Default for PopulationAnnealingConfig {
80 fn default() -> Self {
81 Self {
82 population_size: 1000,
83 temperature_schedule: TemperatureSchedule::Exponential(3.0),
84 initial_temperature: 10.0,
85 final_temperature: 0.01,
86 num_temperature_steps: 100,
87 sweeps_per_step: 100,
88 resampling_frequency: 5,
89 ess_threshold: 0.5,
90 seed: None,
91 timeout: Some(Duration::from_secs(3600)), mpi_config: None,
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct MpiConfig {
100 pub num_processes: usize,
102
103 pub rank: usize,
105
106 pub load_balancing: bool,
108
109 pub communication_frequency: usize,
111}
112
113#[derive(Debug, Clone)]
115pub struct PopulationMember {
116 pub configuration: Vec<i8>,
118
119 pub energy: f64,
121
122 pub weight: f64,
124
125 pub lineage: Vec<usize>,
127}
128
129impl PopulationMember {
130 #[must_use]
132 pub const fn new(configuration: Vec<i8>, energy: f64) -> Self {
133 Self {
134 configuration,
135 energy,
136 weight: 1.0,
137 lineage: vec![],
138 }
139 }
140}
141
142#[derive(Debug, Clone)]
144pub struct PopulationAnnealingSolution {
145 pub best_configuration: Vec<i8>,
147
148 pub best_energy: f64,
150
151 pub final_population: Vec<PopulationMember>,
153
154 pub energy_history: Vec<EnergyStatistics>,
156
157 pub ess_history: Vec<f64>,
159
160 pub runtime: Duration,
162
163 pub num_resamplings: usize,
165
166 pub info: String,
168}
169
170#[derive(Debug, Clone)]
172pub struct EnergyStatistics {
173 pub temperature: f64,
175
176 pub min_energy: f64,
178
179 pub max_energy: f64,
181
182 pub mean_energy: f64,
184
185 pub energy_std: f64,
187
188 pub effective_sample_size: f64,
190}
191
192pub struct PopulationAnnealingSimulator {
194 config: PopulationAnnealingConfig,
196
197 rng: ChaCha8Rng,
199
200 mpi_interface: Option<MpiInterface>,
202}
203
204impl PopulationAnnealingSimulator {
205 pub fn new(config: PopulationAnnealingConfig) -> PopulationAnnealingResult<Self> {
207 Self::validate_config(&config)?;
209
210 let rng = match config.seed {
212 Some(seed) => ChaCha8Rng::seed_from_u64(seed),
213 None => ChaCha8Rng::seed_from_u64(thread_rng().gen()),
214 };
215
216 let mpi_interface = config
218 .mpi_config
219 .as_ref()
220 .map(|mpi_config| MpiInterface::new(mpi_config.clone()))
221 .transpose()?;
222
223 Ok(Self {
224 config,
225 rng,
226 mpi_interface,
227 })
228 }
229
230 fn validate_config(config: &PopulationAnnealingConfig) -> PopulationAnnealingResult<()> {
232 if config.population_size == 0 {
233 return Err(PopulationAnnealingError::InvalidParameter(
234 "Population size must be positive".to_string(),
235 ));
236 }
237
238 if config.initial_temperature <= 0.0 || config.final_temperature <= 0.0 {
239 return Err(PopulationAnnealingError::InvalidParameter(
240 "Temperatures must be positive".to_string(),
241 ));
242 }
243
244 if config.initial_temperature <= config.final_temperature {
245 return Err(PopulationAnnealingError::InvalidParameter(
246 "Initial temperature must be greater than final temperature".to_string(),
247 ));
248 }
249
250 if config.num_temperature_steps == 0 {
251 return Err(PopulationAnnealingError::InvalidParameter(
252 "Number of temperature steps must be positive".to_string(),
253 ));
254 }
255
256 if config.ess_threshold <= 0.0 || config.ess_threshold > 1.0 {
257 return Err(PopulationAnnealingError::InvalidParameter(
258 "ESS threshold must be in (0, 1]".to_string(),
259 ));
260 }
261
262 Ok(())
263 }
264
265 pub fn solve(
267 &mut self,
268 model: &IsingModel,
269 ) -> PopulationAnnealingResult<PopulationAnnealingSolution> {
270 let start_time = Instant::now();
271
272 let mut population = self.initialize_population(model)?;
274
275 let mut energy_history = Vec::new();
277 let mut ess_history = Vec::new();
278 let mut best_energy = f64::INFINITY;
279 let mut best_configuration = vec![];
280 let mut num_resamplings = 0;
281
282 let temperatures = self.generate_temperature_schedule();
284
285 for (step, &temperature) in temperatures.iter().enumerate() {
287 if let Some(timeout) = self.config.timeout {
289 if start_time.elapsed() > timeout {
290 break;
291 }
292 }
293
294 self.monte_carlo_step(model, &mut population, temperature)?;
296
297 let stats = self.calculate_statistics(&population, temperature);
299 energy_history.push(stats.clone());
300 ess_history.push(stats.effective_sample_size);
301
302 for member in &population {
304 if member.energy < best_energy {
305 best_energy = member.energy;
306 best_configuration = member.configuration.clone();
307 }
308 }
309
310 let should_resample = (step + 1) % self.config.resampling_frequency == 0
312 || stats.effective_sample_size
313 < self.config.ess_threshold * self.config.population_size as f64;
314
315 if should_resample {
316 self.resample_population(&mut population, temperature)?;
317 num_resamplings += 1;
318
319 if let Some(mpi) = &mut self.mpi_interface {
321 mpi.exchange_populations(&mut population)?;
322 }
323 }
324 }
325
326 let runtime = start_time.elapsed();
327
328 Ok(PopulationAnnealingSolution {
329 best_configuration,
330 best_energy,
331 final_population: population,
332 energy_history,
333 ess_history,
334 runtime,
335 num_resamplings,
336 info: format!(
337 "Population annealing completed: {} temperature steps, {} resamplings, runtime: {:?}",
338 temperatures.len(), num_resamplings, runtime
339 ),
340 })
341 }
342
343 fn initialize_population(
345 &mut self,
346 model: &IsingModel,
347 ) -> PopulationAnnealingResult<Vec<PopulationMember>> {
348 let mut population = Vec::with_capacity(self.config.population_size);
349
350 for _ in 0..self.config.population_size {
351 let configuration: Vec<i8> = (0..model.num_qubits)
353 .map(|_| if self.rng.gen_bool(0.5) { 1 } else { -1 })
354 .collect();
355
356 let energy = model.energy(&configuration)?;
358
359 population.push(PopulationMember::new(configuration, energy));
360 }
361
362 Ok(population)
363 }
364
365 fn generate_temperature_schedule(&self) -> Vec<f64> {
367 let mut temperatures = Vec::with_capacity(self.config.num_temperature_steps);
368
369 for i in 0..self.config.num_temperature_steps {
370 let t = i as f64 / (self.config.num_temperature_steps - 1) as f64;
371 let temperature =
372 self.config
373 .temperature_schedule
374 .calculate(t, 1.0, self.config.initial_temperature);
375
376 let clamped_temp = temperature.max(self.config.final_temperature);
378 temperatures.push(clamped_temp);
379 }
380
381 temperatures
382 }
383
384 fn monte_carlo_step(
386 &mut self,
387 model: &IsingModel,
388 population: &mut [PopulationMember],
389 temperature: f64,
390 ) -> PopulationAnnealingResult<()> {
391 for member in population.iter_mut() {
392 for _ in 0..self.config.sweeps_per_step {
393 let spin_idx = self.rng.gen_range(0..model.num_qubits);
395
396 let old_spin = member.configuration[spin_idx];
398 member.configuration[spin_idx] = -old_spin;
399
400 let new_energy = model.energy(&member.configuration)?;
401 let delta_e = new_energy - member.energy;
402
403 let accept = delta_e <= 0.0 || {
405 let prob = (-delta_e / temperature).exp();
406 self.rng.gen::<f64>() < prob
407 };
408
409 if accept {
410 member.energy = new_energy;
411 } else {
412 member.configuration[spin_idx] = old_spin;
414 }
415 }
416 }
417
418 Ok(())
419 }
420
421 fn calculate_statistics(
423 &self,
424 population: &[PopulationMember],
425 temperature: f64,
426 ) -> EnergyStatistics {
427 let energies: Vec<f64> = population.iter().map(|m| m.energy).collect();
428
429 let min_energy = energies.iter().copied().fold(f64::INFINITY, f64::min);
430 let max_energy = energies.iter().copied().fold(f64::NEG_INFINITY, f64::max);
431 let mean_energy = energies.iter().sum::<f64>() / energies.len() as f64;
432
433 let variance = energies
434 .iter()
435 .map(|&e| (e - mean_energy).powi(2))
436 .sum::<f64>()
437 / energies.len() as f64;
438 let energy_std = variance.sqrt();
439
440 let min_e = min_energy;
442 let weights: Vec<f64> = energies
443 .iter()
444 .map(|&e| (-(e - min_e) / temperature).exp())
445 .collect();
446
447 let sum_weights = weights.iter().sum::<f64>();
448 let sum_weights_squared = weights.iter().map(|&w| w * w).sum::<f64>();
449
450 let effective_sample_size = if sum_weights_squared > 0.0 {
451 sum_weights * sum_weights / sum_weights_squared
452 } else {
453 population.len() as f64
454 };
455
456 EnergyStatistics {
457 temperature,
458 min_energy,
459 max_energy,
460 mean_energy,
461 energy_std,
462 effective_sample_size,
463 }
464 }
465
466 fn resample_population(
468 &mut self,
469 population: &mut Vec<PopulationMember>,
470 temperature: f64,
471 ) -> PopulationAnnealingResult<()> {
472 if population.is_empty() {
473 return Ok(());
474 }
475
476 let min_energy = population
478 .iter()
479 .map(|m| m.energy)
480 .fold(f64::INFINITY, f64::min);
481 let weights: Vec<f64> = population
482 .iter()
483 .map(|m| (-(m.energy - min_energy) / temperature).exp())
484 .collect();
485
486 let total_weight: f64 = weights.iter().sum();
487 if total_weight <= 0.0 {
488 return Err(PopulationAnnealingError::EvolutionError(
489 "Total weight is zero or negative".to_string(),
490 ));
491 }
492
493 let normalized_weights: Vec<f64> = weights.iter().map(|w| w / total_weight).collect();
495
496 let mut cumulative_weights = vec![0.0; normalized_weights.len()];
498 cumulative_weights[0] = normalized_weights[0];
499 for i in 1..normalized_weights.len() {
500 cumulative_weights[i] = cumulative_weights[i - 1] + normalized_weights[i];
501 }
502
503 let mut new_population = Vec::with_capacity(population.len());
505 for _ in 0..population.len() {
506 let r = self.rng.gen::<f64>();
507 let idx = cumulative_weights
508 .iter()
509 .position(|&w| r <= w)
510 .unwrap_or(population.len() - 1);
511
512 let mut new_member = population[idx].clone();
513 new_member.lineage.push(idx);
514 new_population.push(new_member);
515 }
516
517 *population = new_population;
518 Ok(())
519 }
520}
521
522struct MpiInterface {
524 config: MpiConfig,
525}
526
527impl MpiInterface {
528 const fn new(config: MpiConfig) -> PopulationAnnealingResult<Self> {
529 Ok(Self { config })
532 }
533
534 const fn exchange_populations(
535 &self,
536 _population: &mut Vec<PopulationMember>,
537 ) -> PopulationAnnealingResult<()> {
538 Ok(())
546 }
547}
548
549impl From<PopulationAnnealingSolution> for AnnealingSolution {
551 fn from(result: PopulationAnnealingSolution) -> Self {
552 Self {
553 best_spins: result.best_configuration,
554 best_energy: result.best_energy,
555 repetitions: 1,
556 total_sweeps: 0, runtime: result.runtime,
558 info: result.info,
559 }
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn test_population_annealing_config() {
569 let config = PopulationAnnealingConfig::default();
570 assert!(PopulationAnnealingSimulator::validate_config(&config).is_ok());
571
572 let mut invalid_config = config.clone();
574 invalid_config.population_size = 0;
575 assert!(PopulationAnnealingSimulator::validate_config(&invalid_config).is_err());
576 }
577
578 #[test]
579 fn test_temperature_schedule() {
580 let config = PopulationAnnealingConfig {
581 initial_temperature: 10.0,
582 final_temperature: 0.1,
583 num_temperature_steps: 5,
584 ..Default::default()
585 };
586
587 let mut simulator =
588 PopulationAnnealingSimulator::new(config).expect("failed to create simulator in test");
589 let temperatures = simulator.generate_temperature_schedule();
590
591 assert_eq!(temperatures.len(), 5);
592 assert!(temperatures[0] >= temperatures[4]);
593 assert!(temperatures[4] >= 0.1);
594 }
595
596 #[test]
597 fn test_population_initialization() {
598 let mut model = IsingModel::new(4);
599 model
600 .set_coupling(0, 1, -1.0)
601 .expect("failed to set coupling in test");
602
603 let config = PopulationAnnealingConfig {
604 population_size: 10,
605 seed: Some(42),
606 ..Default::default()
607 };
608
609 let mut simulator =
610 PopulationAnnealingSimulator::new(config).expect("failed to create simulator in test");
611 let population = simulator
612 .initialize_population(&model)
613 .expect("failed to initialize population in test");
614
615 assert_eq!(population.len(), 10);
616 for member in &population {
617 assert_eq!(member.configuration.len(), 4);
618 assert!(member.configuration.iter().all(|&s| s == 1 || s == -1));
619 }
620 }
621
622 #[test]
623 fn test_simple_population_annealing() {
624 let mut model = IsingModel::new(3);
625 model
626 .set_coupling(0, 1, -1.0)
627 .expect("failed to set coupling in test");
628 model
629 .set_coupling(1, 2, -1.0)
630 .expect("failed to set coupling in test");
631
632 let config = PopulationAnnealingConfig {
633 population_size: 50,
634 num_temperature_steps: 10,
635 sweeps_per_step: 10,
636 seed: Some(42),
637 ..Default::default()
638 };
639
640 let mut simulator =
641 PopulationAnnealingSimulator::new(config).expect("failed to create simulator in test");
642 let result = simulator
643 .solve(&model)
644 .expect("failed to solve model in test");
645
646 assert!(result.best_energy <= 0.0); assert_eq!(result.final_population.len(), 50);
648 assert_eq!(result.energy_history.len(), 10);
649 }
650}