use scirs2_core::random::prelude::*;
use scirs2_core::random::ChaCha8Rng;
use scirs2_core::random::{Rng, SeedableRng};
use std::collections::HashMap;
use std::time::{Duration, Instant};
use thiserror::Error;
use crate::ising::{IsingError, IsingModel};
use crate::simulator::{
AnnealingParams, AnnealingSolution, TemperatureSchedule, TransverseFieldSchedule,
};
#[derive(Error, Debug)]
pub enum PopulationAnnealingError {
#[error("Ising error: {0}")]
IsingError(#[from] IsingError),
#[error("Invalid parameter: {0}")]
InvalidParameter(String),
#[error("MPI error: {0}")]
MpiError(String),
#[error("Population evolution error: {0}")]
EvolutionError(String),
}
pub type PopulationAnnealingResult<T> = Result<T, PopulationAnnealingError>;
#[derive(Debug, Clone)]
pub struct PopulationAnnealingConfig {
pub population_size: usize,
pub temperature_schedule: TemperatureSchedule,
pub initial_temperature: f64,
pub final_temperature: f64,
pub num_temperature_steps: usize,
pub sweeps_per_step: usize,
pub resampling_frequency: usize,
pub ess_threshold: f64,
pub seed: Option<u64>,
pub timeout: Option<Duration>,
pub mpi_config: Option<MpiConfig>,
}
impl Default for PopulationAnnealingConfig {
fn default() -> Self {
Self {
population_size: 1000,
temperature_schedule: TemperatureSchedule::Exponential(3.0),
initial_temperature: 10.0,
final_temperature: 0.01,
num_temperature_steps: 100,
sweeps_per_step: 100,
resampling_frequency: 5,
ess_threshold: 0.5,
seed: None,
timeout: Some(Duration::from_secs(3600)), mpi_config: None,
}
}
}
#[derive(Debug, Clone)]
pub struct MpiConfig {
pub num_processes: usize,
pub rank: usize,
pub load_balancing: bool,
pub communication_frequency: usize,
}
#[derive(Debug, Clone)]
pub struct PopulationMember {
pub configuration: Vec<i8>,
pub energy: f64,
pub weight: f64,
pub lineage: Vec<usize>,
}
impl PopulationMember {
#[must_use]
pub const fn new(configuration: Vec<i8>, energy: f64) -> Self {
Self {
configuration,
energy,
weight: 1.0,
lineage: vec![],
}
}
}
#[derive(Debug, Clone)]
pub struct PopulationAnnealingSolution {
pub best_configuration: Vec<i8>,
pub best_energy: f64,
pub final_population: Vec<PopulationMember>,
pub energy_history: Vec<EnergyStatistics>,
pub ess_history: Vec<f64>,
pub runtime: Duration,
pub num_resamplings: usize,
pub info: String,
}
#[derive(Debug, Clone)]
pub struct EnergyStatistics {
pub temperature: f64,
pub min_energy: f64,
pub max_energy: f64,
pub mean_energy: f64,
pub energy_std: f64,
pub effective_sample_size: f64,
}
pub struct PopulationAnnealingSimulator {
config: PopulationAnnealingConfig,
rng: ChaCha8Rng,
mpi_interface: Option<MpiInterface>,
}
impl PopulationAnnealingSimulator {
pub fn new(config: PopulationAnnealingConfig) -> PopulationAnnealingResult<Self> {
Self::validate_config(&config)?;
let rng = match config.seed {
Some(seed) => ChaCha8Rng::seed_from_u64(seed),
None => ChaCha8Rng::seed_from_u64(thread_rng().random()),
};
let mpi_interface = config
.mpi_config
.as_ref()
.map(|mpi_config| MpiInterface::new(mpi_config.clone()))
.transpose()?;
Ok(Self {
config,
rng,
mpi_interface,
})
}
fn validate_config(config: &PopulationAnnealingConfig) -> PopulationAnnealingResult<()> {
if config.population_size == 0 {
return Err(PopulationAnnealingError::InvalidParameter(
"Population size must be positive".to_string(),
));
}
if config.initial_temperature <= 0.0 || config.final_temperature <= 0.0 {
return Err(PopulationAnnealingError::InvalidParameter(
"Temperatures must be positive".to_string(),
));
}
if config.initial_temperature <= config.final_temperature {
return Err(PopulationAnnealingError::InvalidParameter(
"Initial temperature must be greater than final temperature".to_string(),
));
}
if config.num_temperature_steps == 0 {
return Err(PopulationAnnealingError::InvalidParameter(
"Number of temperature steps must be positive".to_string(),
));
}
if config.ess_threshold <= 0.0 || config.ess_threshold > 1.0 {
return Err(PopulationAnnealingError::InvalidParameter(
"ESS threshold must be in (0, 1]".to_string(),
));
}
Ok(())
}
pub fn solve(
&mut self,
model: &IsingModel,
) -> PopulationAnnealingResult<PopulationAnnealingSolution> {
let start_time = Instant::now();
let mut population = self.initialize_population(model)?;
let mut energy_history = Vec::new();
let mut ess_history = Vec::new();
let mut best_energy = f64::INFINITY;
let mut best_configuration = vec![];
let mut num_resamplings = 0;
let temperatures = self.generate_temperature_schedule();
for (step, &temperature) in temperatures.iter().enumerate() {
if let Some(timeout) = self.config.timeout {
if start_time.elapsed() > timeout {
break;
}
}
self.monte_carlo_step(model, &mut population, temperature)?;
let stats = self.calculate_statistics(&population, temperature);
energy_history.push(stats.clone());
ess_history.push(stats.effective_sample_size);
for member in &population {
if member.energy < best_energy {
best_energy = member.energy;
best_configuration = member.configuration.clone();
}
}
let should_resample = (step + 1) % self.config.resampling_frequency == 0
|| stats.effective_sample_size
< self.config.ess_threshold * self.config.population_size as f64;
if should_resample {
self.resample_population(&mut population, temperature)?;
num_resamplings += 1;
if let Some(mpi) = &mut self.mpi_interface {
mpi.exchange_populations(&mut population)?;
}
}
}
let runtime = start_time.elapsed();
Ok(PopulationAnnealingSolution {
best_configuration,
best_energy,
final_population: population,
energy_history,
ess_history,
runtime,
num_resamplings,
info: format!(
"Population annealing completed: {} temperature steps, {} resamplings, runtime: {:?}",
temperatures.len(), num_resamplings, runtime
),
})
}
fn initialize_population(
&mut self,
model: &IsingModel,
) -> PopulationAnnealingResult<Vec<PopulationMember>> {
let mut population = Vec::with_capacity(self.config.population_size);
for _ in 0..self.config.population_size {
let configuration: Vec<i8> = (0..model.num_qubits)
.map(|_| if self.rng.random_bool(0.5) { 1 } else { -1 })
.collect();
let energy = model.energy(&configuration)?;
population.push(PopulationMember::new(configuration, energy));
}
Ok(population)
}
fn generate_temperature_schedule(&self) -> Vec<f64> {
let mut temperatures = Vec::with_capacity(self.config.num_temperature_steps);
for i in 0..self.config.num_temperature_steps {
let t = i as f64 / (self.config.num_temperature_steps - 1) as f64;
let temperature =
self.config
.temperature_schedule
.calculate(t, 1.0, self.config.initial_temperature);
let clamped_temp = temperature.max(self.config.final_temperature);
temperatures.push(clamped_temp);
}
temperatures
}
fn monte_carlo_step(
&mut self,
model: &IsingModel,
population: &mut [PopulationMember],
temperature: f64,
) -> PopulationAnnealingResult<()> {
for member in population.iter_mut() {
for _ in 0..self.config.sweeps_per_step {
let spin_idx = self.rng.random_range(0..model.num_qubits);
let old_spin = member.configuration[spin_idx];
member.configuration[spin_idx] = -old_spin;
let new_energy = model.energy(&member.configuration)?;
let delta_e = new_energy - member.energy;
let accept = delta_e <= 0.0 || {
let prob = (-delta_e / temperature).exp();
self.rng.random::<f64>() < prob
};
if accept {
member.energy = new_energy;
} else {
member.configuration[spin_idx] = old_spin;
}
}
}
Ok(())
}
fn calculate_statistics(
&self,
population: &[PopulationMember],
temperature: f64,
) -> EnergyStatistics {
let energies: Vec<f64> = population.iter().map(|m| m.energy).collect();
let min_energy = energies.iter().copied().fold(f64::INFINITY, f64::min);
let max_energy = energies.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let mean_energy = energies.iter().sum::<f64>() / energies.len() as f64;
let variance = energies
.iter()
.map(|&e| (e - mean_energy).powi(2))
.sum::<f64>()
/ energies.len() as f64;
let energy_std = variance.sqrt();
let min_e = min_energy;
let weights: Vec<f64> = energies
.iter()
.map(|&e| (-(e - min_e) / temperature).exp())
.collect();
let sum_weights = weights.iter().sum::<f64>();
let sum_weights_squared = weights.iter().map(|&w| w * w).sum::<f64>();
let effective_sample_size = if sum_weights_squared > 0.0 {
sum_weights * sum_weights / sum_weights_squared
} else {
population.len() as f64
};
EnergyStatistics {
temperature,
min_energy,
max_energy,
mean_energy,
energy_std,
effective_sample_size,
}
}
fn resample_population(
&mut self,
population: &mut Vec<PopulationMember>,
temperature: f64,
) -> PopulationAnnealingResult<()> {
if population.is_empty() {
return Ok(());
}
let min_energy = population
.iter()
.map(|m| m.energy)
.fold(f64::INFINITY, f64::min);
let weights: Vec<f64> = population
.iter()
.map(|m| (-(m.energy - min_energy) / temperature).exp())
.collect();
let total_weight: f64 = weights.iter().sum();
if total_weight <= 0.0 {
return Err(PopulationAnnealingError::EvolutionError(
"Total weight is zero or negative".to_string(),
));
}
let normalized_weights: Vec<f64> = weights.iter().map(|w| w / total_weight).collect();
let mut cumulative_weights = vec![0.0; normalized_weights.len()];
cumulative_weights[0] = normalized_weights[0];
for i in 1..normalized_weights.len() {
cumulative_weights[i] = cumulative_weights[i - 1] + normalized_weights[i];
}
let mut new_population = Vec::with_capacity(population.len());
for _ in 0..population.len() {
let r = self.rng.random::<f64>();
let idx = cumulative_weights
.iter()
.position(|&w| r <= w)
.unwrap_or(population.len() - 1);
let mut new_member = population[idx].clone();
new_member.lineage.push(idx);
new_population.push(new_member);
}
*population = new_population;
Ok(())
}
}
struct MpiInterface {
config: MpiConfig,
}
impl MpiInterface {
const fn new(config: MpiConfig) -> PopulationAnnealingResult<Self> {
Ok(Self { config })
}
const fn exchange_populations(
&self,
_population: &mut Vec<PopulationMember>,
) -> PopulationAnnealingResult<()> {
Ok(())
}
}
impl From<PopulationAnnealingSolution> for AnnealingSolution {
fn from(result: PopulationAnnealingSolution) -> Self {
Self {
best_spins: result.best_configuration,
best_energy: result.best_energy,
repetitions: 1,
total_sweeps: 0, runtime: result.runtime,
info: result.info,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_population_annealing_config() {
let config = PopulationAnnealingConfig::default();
assert!(PopulationAnnealingSimulator::validate_config(&config).is_ok());
let mut invalid_config = config.clone();
invalid_config.population_size = 0;
assert!(PopulationAnnealingSimulator::validate_config(&invalid_config).is_err());
}
#[test]
fn test_temperature_schedule() {
let config = PopulationAnnealingConfig {
initial_temperature: 10.0,
final_temperature: 0.1,
num_temperature_steps: 5,
..Default::default()
};
let mut simulator =
PopulationAnnealingSimulator::new(config).expect("failed to create simulator in test");
let temperatures = simulator.generate_temperature_schedule();
assert_eq!(temperatures.len(), 5);
assert!(temperatures[0] >= temperatures[4]);
assert!(temperatures[4] >= 0.1);
}
#[test]
fn test_population_initialization() {
let mut model = IsingModel::new(4);
model
.set_coupling(0, 1, -1.0)
.expect("failed to set coupling in test");
let config = PopulationAnnealingConfig {
population_size: 10,
seed: Some(42),
..Default::default()
};
let mut simulator =
PopulationAnnealingSimulator::new(config).expect("failed to create simulator in test");
let population = simulator
.initialize_population(&model)
.expect("failed to initialize population in test");
assert_eq!(population.len(), 10);
for member in &population {
assert_eq!(member.configuration.len(), 4);
assert!(member.configuration.iter().all(|&s| s == 1 || s == -1));
}
}
#[test]
fn test_simple_population_annealing() {
let mut model = IsingModel::new(3);
model
.set_coupling(0, 1, -1.0)
.expect("failed to set coupling in test");
model
.set_coupling(1, 2, -1.0)
.expect("failed to set coupling in test");
let config = PopulationAnnealingConfig {
population_size: 50,
num_temperature_steps: 10,
sweeps_per_step: 10,
seed: Some(42),
..Default::default()
};
let mut simulator =
PopulationAnnealingSimulator::new(config).expect("failed to create simulator in test");
let result = simulator
.solve(&model)
.expect("failed to solve model in test");
assert!(result.best_energy <= 0.0); assert_eq!(result.final_population.len(), 50);
assert_eq!(result.energy_history.len(), 10);
}
}