quantrs2_anneal/
population_annealing.rs

1//! Population annealing with MPI support
2//!
3//! This module implements population annealing, a parallel sampling algorithm
4//! that maintains a population of configurations and periodically resamples
5//! based on Boltzmann weights.
6
7use 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/// Errors that can occur during population annealing
20#[derive(Error, Debug)]
21pub enum PopulationAnnealingError {
22    /// Ising model error
23    #[error("Ising error: {0}")]
24    IsingError(#[from] IsingError),
25
26    /// Invalid parameter
27    #[error("Invalid parameter: {0}")]
28    InvalidParameter(String),
29
30    /// MPI communication error
31    #[error("MPI error: {0}")]
32    MpiError(String),
33
34    /// Population evolution error
35    #[error("Population evolution error: {0}")]
36    EvolutionError(String),
37}
38
39/// Result type for population annealing operations
40pub type PopulationAnnealingResult<T> = Result<T, PopulationAnnealingError>;
41
42/// Configuration for population annealing
43#[derive(Debug, Clone)]
44pub struct PopulationAnnealingConfig {
45    /// Population size
46    pub population_size: usize,
47
48    /// Temperature schedule
49    pub temperature_schedule: TemperatureSchedule,
50
51    /// Initial temperature
52    pub initial_temperature: f64,
53
54    /// Final temperature
55    pub final_temperature: f64,
56
57    /// Number of temperature steps
58    pub num_temperature_steps: usize,
59
60    /// Number of Monte Carlo sweeps per temperature step
61    pub sweeps_per_step: usize,
62
63    /// Resampling frequency (in temperature steps)
64    pub resampling_frequency: usize,
65
66    /// Effective sample size threshold for resampling
67    pub ess_threshold: f64,
68
69    /// Random seed
70    pub seed: Option<u64>,
71
72    /// Maximum runtime
73    pub timeout: Option<Duration>,
74
75    /// MPI configuration
76    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)), // 1 hour
92            mpi_config: None,
93        }
94    }
95}
96
97/// MPI configuration for distributed population annealing
98#[derive(Debug, Clone)]
99pub struct MpiConfig {
100    /// Number of MPI processes
101    pub num_processes: usize,
102
103    /// Current process rank
104    pub rank: usize,
105
106    /// Enable load balancing
107    pub load_balancing: bool,
108
109    /// Communication frequency (in temperature steps)
110    pub communication_frequency: usize,
111}
112
113/// Individual configuration in the population
114#[derive(Debug, Clone)]
115pub struct PopulationMember {
116    /// Spin configuration
117    pub configuration: Vec<i8>,
118
119    /// Energy of the configuration
120    pub energy: f64,
121
122    /// Weight (for resampling)
123    pub weight: f64,
124
125    /// Ancestor lineage (for tracking)
126    pub lineage: Vec<usize>,
127}
128
129impl PopulationMember {
130    /// Create a new population member
131    #[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/// Population annealing results
143#[derive(Debug, Clone)]
144pub struct PopulationAnnealingSolution {
145    /// Best configuration found
146    pub best_configuration: Vec<i8>,
147
148    /// Best energy found
149    pub best_energy: f64,
150
151    /// Final population
152    pub final_population: Vec<PopulationMember>,
153
154    /// Energy statistics over time
155    pub energy_history: Vec<EnergyStatistics>,
156
157    /// Effective sample size history
158    pub ess_history: Vec<f64>,
159
160    /// Total runtime
161    pub runtime: Duration,
162
163    /// Number of resampling events
164    pub num_resamplings: usize,
165
166    /// Additional info
167    pub info: String,
168}
169
170/// Energy statistics for the population
171#[derive(Debug, Clone)]
172pub struct EnergyStatistics {
173    /// Temperature at this step
174    pub temperature: f64,
175
176    /// Minimum energy in population
177    pub min_energy: f64,
178
179    /// Maximum energy in population
180    pub max_energy: f64,
181
182    /// Mean energy
183    pub mean_energy: f64,
184
185    /// Energy standard deviation
186    pub energy_std: f64,
187
188    /// Effective sample size
189    pub effective_sample_size: f64,
190}
191
192/// Population annealing simulator
193pub struct PopulationAnnealingSimulator {
194    /// Configuration
195    config: PopulationAnnealingConfig,
196
197    /// Random number generator
198    rng: ChaCha8Rng,
199
200    /// MPI interface (optional)
201    mpi_interface: Option<MpiInterface>,
202}
203
204impl PopulationAnnealingSimulator {
205    /// Create a new population annealing simulator
206    pub fn new(config: PopulationAnnealingConfig) -> PopulationAnnealingResult<Self> {
207        // Validate configuration
208        Self::validate_config(&config)?;
209
210        // Initialize RNG
211        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        // Initialize MPI interface if configured
217        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    /// Validate configuration parameters
231    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    /// Solve an Ising model using population annealing
266    pub fn solve(
267        &mut self,
268        model: &IsingModel,
269    ) -> PopulationAnnealingResult<PopulationAnnealingSolution> {
270        let start_time = Instant::now();
271
272        // Initialize population
273        let mut population = self.initialize_population(model)?;
274
275        // Track statistics
276        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        // Temperature schedule
283        let temperatures = self.generate_temperature_schedule();
284
285        // Main annealing loop
286        for (step, &temperature) in temperatures.iter().enumerate() {
287            // Check timeout
288            if let Some(timeout) = self.config.timeout {
289                if start_time.elapsed() > timeout {
290                    break;
291                }
292            }
293
294            // Perform Monte Carlo sweeps
295            self.monte_carlo_step(model, &mut population, temperature)?;
296
297            // Calculate weights and statistics
298            let stats = self.calculate_statistics(&population, temperature);
299            energy_history.push(stats.clone());
300            ess_history.push(stats.effective_sample_size);
301
302            // Update best solution
303            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            // Resampling decision
311            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                // MPI communication if enabled
320                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    /// Initialize the population with random configurations
344    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            // Generate random configuration
352            let configuration: Vec<i8> = (0..model.num_qubits)
353                .map(|_| if self.rng.gen_bool(0.5) { 1 } else { -1 })
354                .collect();
355
356            // Calculate energy
357            let energy = model.energy(&configuration)?;
358
359            population.push(PopulationMember::new(configuration, energy));
360        }
361
362        Ok(population)
363    }
364
365    /// Generate temperature schedule
366    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            // Ensure we don't go below final temperature
377            let clamped_temp = temperature.max(self.config.final_temperature);
378            temperatures.push(clamped_temp);
379        }
380
381        temperatures
382    }
383
384    /// Perform Monte Carlo step for the entire population
385    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                // Choose random spin to flip
394                let spin_idx = self.rng.gen_range(0..model.num_qubits);
395
396                // Calculate energy change
397                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                // Metropolis acceptance
404                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                    // Revert the change
413                    member.configuration[spin_idx] = old_spin;
414                }
415            }
416        }
417
418        Ok(())
419    }
420
421    /// Calculate population statistics
422    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        // Calculate effective sample size
441        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    /// Resample population based on Boltzmann weights
467    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        // Calculate Boltzmann weights
477        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        // Normalize weights
494        let normalized_weights: Vec<f64> = weights.iter().map(|w| w / total_weight).collect();
495
496        // Cumulative distribution
497        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        // Resample
504        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
522/// MPI interface for distributed population annealing
523struct MpiInterface {
524    config: MpiConfig,
525}
526
527impl MpiInterface {
528    const fn new(config: MpiConfig) -> PopulationAnnealingResult<Self> {
529        // In a real implementation, this would initialize MPI
530        // For now, we just store the config
531        Ok(Self { config })
532    }
533
534    const fn exchange_populations(
535        &self,
536        _population: &mut Vec<PopulationMember>,
537    ) -> PopulationAnnealingResult<()> {
538        // In a real implementation, this would:
539        // 1. Gather population statistics from all processes
540        // 2. Perform load balancing if needed
541        // 3. Exchange population members between processes
542        // 4. Redistribute populations based on performance
543
544        // For now, this is a placeholder
545        Ok(())
546    }
547}
548
549/// Convert population annealing result to standard annealing solution
550impl 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, // Would need to track this
557            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        // Test invalid configuration
573        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); // Should find good solution
647        assert_eq!(result.final_population.len(), 50);
648        assert_eq!(result.energy_history.len(), 10);
649    }
650}