use crate::{
optimization::penalty::CompiledModel,
sampler::{SampleResult, Sampler},
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[cfg(feature = "scirs")]
use crate::scirs_stub::scirs2_core::statistics::{MovingAverage, OnlineStats};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AdaptiveStrategy {
ExponentialDecay,
AdaptivePenaltyMethod,
AugmentedLagrangian,
PopulationBased,
MultiArmedBandit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdaptiveConfig {
pub strategy: AdaptiveStrategy,
pub update_interval: usize,
pub learning_rate: f64,
pub momentum: f64,
pub patience: usize,
pub exploration_rate: f64,
pub population_size: usize,
pub history_window: usize,
}
impl Default for AdaptiveConfig {
fn default() -> Self {
Self {
strategy: AdaptiveStrategy::AdaptivePenaltyMethod,
update_interval: 10,
learning_rate: 0.1,
momentum: 0.9,
patience: 5,
exploration_rate: 0.1,
population_size: 10,
history_window: 100,
}
}
}
pub struct AdaptiveOptimizer {
config: AdaptiveConfig,
iteration: usize,
parameter_history: Vec<ParameterState>,
performance_history: Vec<PerformanceMetrics>,
lagrange_multipliers: HashMap<String, f64>,
population: Vec<Individual>,
#[cfg(feature = "scirs")]
stats: OnlineStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterState {
pub iteration: usize,
pub parameters: HashMap<String, f64>,
pub penalty_weights: HashMap<String, f64>,
pub temperature: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceMetrics {
pub iteration: usize,
pub best_energy: f64,
pub avg_energy: f64,
pub constraint_violations: HashMap<String, f64>,
pub feasibility_rate: f64,
pub diversity: f64,
}
#[derive(Debug, Clone)]
struct Individual {
id: usize,
parameters: HashMap<String, f64>,
fitness: f64,
constraint_satisfaction: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdaptiveResult {
pub final_parameters: HashMap<String, f64>,
pub final_penalty_weights: HashMap<String, f64>,
pub convergence_history: Vec<f64>,
pub constraint_history: Vec<HashMap<String, f64>>,
pub total_iterations: usize,
pub best_solution: AdaptiveSampleResult,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdaptiveSampleResult {
pub assignments: HashMap<String, bool>,
pub energy: f64,
}
impl AdaptiveOptimizer {
pub fn new(config: AdaptiveConfig) -> Self {
Self {
config,
iteration: 0,
parameter_history: Vec::new(),
performance_history: Vec::new(),
lagrange_multipliers: HashMap::new(),
population: Vec::new(),
#[cfg(feature = "scirs")]
stats: OnlineStats::new(),
}
}
pub fn optimize<S: Sampler + Clone>(
&mut self,
mut sampler: S,
model: &CompiledModel,
initial_params: HashMap<String, f64>,
initial_penalties: HashMap<String, f64>,
max_iterations: usize,
) -> Result<AdaptiveResult, Box<dyn std::error::Error>> {
let mut current_params = initial_params;
let mut penalty_weights = initial_penalties;
let mut best_solution = None;
let mut best_energy = f64::INFINITY;
match self.config.strategy {
AdaptiveStrategy::PopulationBased => {
self.initialize_population(¤t_params)?;
}
AdaptiveStrategy::AugmentedLagrangian => {
self.initialize_lagrange_multipliers(&penalty_weights);
}
_ => {}
}
let mut no_improvement_count = 0;
for iter in 0..max_iterations {
self.iteration = iter;
let samples =
self.run_sampling(&mut sampler, model, ¤t_params, &penalty_weights)?;
let metrics = self.evaluate_performance(model, &samples)?;
self.performance_history.push(metrics.clone());
if let Some(sample) = samples.iter().min_by(|a, b| {
a.energy
.partial_cmp(&b.energy)
.unwrap_or(std::cmp::Ordering::Equal)
}) {
if sample.energy < best_energy {
best_energy = sample.energy;
best_solution = Some(AdaptiveSampleResult {
assignments: sample.assignments.clone(),
energy: sample.energy,
});
no_improvement_count = 0;
} else {
no_improvement_count += 1;
}
}
if no_improvement_count > self.config.patience {
break;
}
if iter % self.config.update_interval == 0 && iter > 0 {
self.update_parameters(&mut current_params, &mut penalty_weights, &metrics)?;
}
self.parameter_history.push(ParameterState {
iteration: iter,
parameters: current_params.clone(),
penalty_weights: penalty_weights.clone(),
temperature: self.calculate_temperature(iter, max_iterations),
});
}
let convergence_history = self
.performance_history
.iter()
.map(|m| m.best_energy)
.collect();
let constraint_history = self
.performance_history
.iter()
.map(|m| m.constraint_violations.clone())
.collect();
Ok(AdaptiveResult {
final_parameters: current_params,
final_penalty_weights: penalty_weights,
convergence_history,
constraint_history,
total_iterations: self.iteration,
best_solution: best_solution.ok_or("No valid solution found")?,
})
}
fn run_sampling<S: Sampler>(
&self,
sampler: &mut S,
model: &CompiledModel,
params: &HashMap<String, f64>,
penalty_weights: &HashMap<String, f64>,
) -> Result<Vec<SampleResult>, Box<dyn std::error::Error>> {
let penalized_model = self.apply_penalties(model, penalty_weights)?;
sampler.set_parameters(params.clone());
let num_reads = params.get("num_reads").copied().unwrap_or(100.0) as usize;
Ok(sampler.run_qubo(&penalized_model.to_qubo(), num_reads)?)
}
fn evaluate_performance(
&self,
model: &CompiledModel,
samples: &[SampleResult],
) -> Result<PerformanceMetrics, Box<dyn std::error::Error>> {
let energies: Vec<f64> = samples.iter().map(|s| s.energy).collect();
let best_energy = energies.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let avg_energy = energies.iter().sum::<f64>() / energies.len() as f64;
let constraint_violations = self.evaluate_constraint_violations(model, samples)?;
let feasible_count = samples
.iter()
.filter(|s| self.is_feasible(s, &constraint_violations).unwrap_or(false))
.count();
let feasibility_rate = feasible_count as f64 / samples.len() as f64;
let diversity = self.calculate_diversity(samples);
Ok(PerformanceMetrics {
iteration: self.iteration,
best_energy,
avg_energy,
constraint_violations,
feasibility_rate,
diversity,
})
}
fn update_parameters(
&mut self,
params: &mut HashMap<String, f64>,
penalty_weights: &mut HashMap<String, f64>,
metrics: &PerformanceMetrics,
) -> Result<(), Box<dyn std::error::Error>> {
match self.config.strategy {
AdaptiveStrategy::ExponentialDecay => {
self.update_exponential_decay(params, penalty_weights)?;
}
AdaptiveStrategy::AdaptivePenaltyMethod => {
self.update_adaptive_penalty(penalty_weights, metrics)?;
}
AdaptiveStrategy::AugmentedLagrangian => {
self.update_augmented_lagrangian(penalty_weights, metrics)?;
}
AdaptiveStrategy::PopulationBased => {
self.update_population_based(params, penalty_weights, metrics)?;
}
AdaptiveStrategy::MultiArmedBandit => {
self.update_multi_armed_bandit(params, metrics)?;
}
}
Ok(())
}
fn update_exponential_decay(
&self,
params: &mut HashMap<String, f64>,
penalty_weights: &mut HashMap<String, f64>,
) -> Result<(), Box<dyn std::error::Error>> {
let decay_rate = 0.95;
if let Some(temp) = params.get_mut("temperature") {
*temp *= decay_rate;
}
for weight in penalty_weights.values_mut() {
*weight *= 1.0 / decay_rate.sqrt(); }
Ok(())
}
fn update_adaptive_penalty(
&mut self,
penalty_weights: &mut HashMap<String, f64>,
metrics: &PerformanceMetrics,
) -> Result<(), Box<dyn std::error::Error>> {
for (constraint_name, &violation) in &metrics.constraint_violations {
if let Some(weight) = penalty_weights.get_mut(constraint_name) {
if violation > 1e-6 {
*weight *= 1.0 + self.config.learning_rate;
} else {
*weight *= self.config.learning_rate.mul_add(-0.5, 1.0);
}
*weight = weight.clamp(0.001, 1000.0);
}
}
#[cfg(feature = "scirs")]
{
self.stats.update(metrics.best_energy);
}
Ok(())
}
fn update_augmented_lagrangian(
&mut self,
penalty_weights: &mut HashMap<String, f64>,
metrics: &PerformanceMetrics,
) -> Result<(), Box<dyn std::error::Error>> {
for (constraint_name, &violation) in &metrics.constraint_violations {
let multiplier = self
.lagrange_multipliers
.entry(constraint_name.clone())
.or_insert(0.0);
*multiplier += self.config.learning_rate * violation;
if let Some(weight) = penalty_weights.get_mut(constraint_name) {
*weight = 0.5f64.mul_add(weight.sqrt(), multiplier.abs());
}
}
Ok(())
}
fn update_population_based(
&mut self,
params: &mut HashMap<String, f64>,
_penalty_weights: &mut HashMap<String, f64>,
metrics: &PerformanceMetrics,
) -> Result<(), Box<dyn std::error::Error>> {
let fitness_values: Vec<f64> = self
.population
.iter()
.map(|individual| self.evaluate_individual_fitness(individual, metrics))
.collect::<Result<Vec<_>, _>>()?;
for (i, fitness) in fitness_values.into_iter().enumerate() {
self.population[i].fitness = fitness;
}
self.population.sort_by(|a, b| {
b.fitness
.partial_cmp(&a.fitness)
.unwrap_or(std::cmp::Ordering::Equal)
});
if let Some(best) = self.population.first() {
for (key, value) in &best.parameters {
if let Some(param) = params.get_mut(key) {
*param = self
.config
.momentum
.mul_add(*param, (1.0 - self.config.momentum) * value);
}
}
}
let mid = self.population.len() / 2;
let pop_len = self.population.len();
for i in mid..pop_len {
use scirs2_core::random::prelude::*;
let mut rng = thread_rng();
for value in self.population[i].parameters.values_mut() {
if rng.random::<f64>() < 0.3 {
let perturbation = rng.random_range(-0.3..0.3) * value.abs();
*value += perturbation;
}
}
}
Ok(())
}
fn update_multi_armed_bandit(
&mut self,
params: &mut HashMap<String, f64>,
_metrics: &PerformanceMetrics,
) -> Result<(), Box<dyn std::error::Error>> {
use scirs2_core::random::prelude::*;
let mut rng = thread_rng();
for (param_name, param_value) in params.iter_mut() {
if rng.random::<f64>() < self.config.exploration_rate {
let perturbation = rng.random_range(-0.1..0.1) * param_value.abs();
*param_value += perturbation;
} else {
if let Some(best_state) = self.parameter_history.iter().min_by(|a, b| {
let a_metrics = &self.performance_history[a.iteration];
let b_metrics = &self.performance_history[b.iteration];
a_metrics
.best_energy
.partial_cmp(&b_metrics.best_energy)
.unwrap_or(std::cmp::Ordering::Equal)
}) {
if let Some(best_value) = best_state.parameters.get(param_name) {
*param_value += self.config.learning_rate * (best_value - *param_value);
}
}
}
}
Ok(())
}
fn initialize_population(
&mut self,
base_params: &HashMap<String, f64>,
) -> Result<(), Box<dyn std::error::Error>> {
use scirs2_core::random::prelude::*;
let mut rng = thread_rng();
for i in 0..self.config.population_size {
let mut params = base_params.clone();
for value in params.values_mut() {
let perturbation = rng.random_range(-0.2..0.2) * value.abs();
*value += perturbation;
}
self.population.push(Individual {
id: i,
parameters: params,
fitness: 0.0,
constraint_satisfaction: 0.0,
});
}
Ok(())
}
fn initialize_lagrange_multipliers(&mut self, penalty_weights: &HashMap<String, f64>) {
for (constraint_name, &weight) in penalty_weights {
self.lagrange_multipliers
.insert(constraint_name.clone(), weight * 0.1);
}
}
fn apply_penalties(
&self,
model: &CompiledModel,
_penalty_weights: &HashMap<String, f64>,
) -> Result<CompiledModel, Box<dyn std::error::Error>> {
Ok(model.clone())
}
fn evaluate_constraint_violations(
&self,
model: &CompiledModel,
_samples: &[SampleResult],
) -> Result<HashMap<String, f64>, Box<dyn std::error::Error>> {
let mut violations = HashMap::new();
for constraint_name in model.get_constraints().keys() {
violations.insert(constraint_name.clone(), 0.0);
}
Ok(violations)
}
fn is_feasible(
&self,
_sample: &SampleResult,
constraint_violations: &HashMap<String, f64>,
) -> Result<bool, Box<dyn std::error::Error>> {
let max_violation = constraint_violations
.values()
.fold(0.0f64, |a, &b| a.max(b.abs()));
Ok(max_violation < 1e-6)
}
fn calculate_diversity(&self, samples: &[SampleResult]) -> f64 {
if samples.len() < 2 {
return 0.0;
}
let mut total_distance = 0.0;
let mut count = 0;
for i in 0..samples.len() {
for j in i + 1..samples.len() {
let distance = self.hamming_distance(&samples[i], &samples[j]);
total_distance += distance as f64;
count += 1;
}
}
if count > 0 {
total_distance / count as f64
} else {
0.0
}
}
fn hamming_distance(&self, a: &SampleResult, b: &SampleResult) -> usize {
a.assignments
.iter()
.filter(|(var, &val_a)| b.assignments.get(*var).copied().unwrap_or(false) != val_a)
.count()
}
fn calculate_temperature(&self, iteration: usize, max_iterations: usize) -> f64 {
let progress = iteration as f64 / max_iterations as f64;
let initial_temp = 10.0f64;
let final_temp = 0.01f64;
initial_temp * (final_temp / initial_temp).powf(progress)
}
fn evaluate_individual_fitness(
&self,
_individual: &Individual,
metrics: &PerformanceMetrics,
) -> Result<f64, Box<dyn std::error::Error>> {
let objective_score = 1.0 / (1.0 + metrics.best_energy.abs());
let constraint_score = metrics.feasibility_rate;
Ok(0.7f64.mul_add(objective_score, 0.3 * constraint_score))
}
fn perturb_individual(
&self,
individual: &mut Individual,
) -> Result<(), Box<dyn std::error::Error>> {
use scirs2_core::random::prelude::*;
let mut rng = thread_rng();
for value in individual.parameters.values_mut() {
if rng.random::<f64>() < 0.3 {
let perturbation = rng.random_range(-0.3..0.3) * value.abs();
*value += perturbation;
}
}
Ok(())
}
pub fn export_history(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
let export = AdaptiveExport {
config: self.config.clone(),
parameter_history: self.parameter_history.clone(),
performance_history: self.performance_history.clone(),
timestamp: std::time::SystemTime::now(),
};
let json = serde_json::to_string_pretty(&export)?;
std::fs::write(path, json)?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdaptiveExport {
pub config: AdaptiveConfig,
pub parameter_history: Vec<ParameterState>,
pub performance_history: Vec<PerformanceMetrics>,
pub timestamp: std::time::SystemTime,
}
trait SamplerExt {
fn set_parameters(&mut self, params: HashMap<String, f64>);
}
impl<S: Sampler> SamplerExt for S {
fn set_parameters(&mut self, _params: HashMap<String, f64>) {
}
}