use crate::sampler::{SampleResult, Sampler};
use scirs2_core::ndarray::Array2;
use scirs2_core::random::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::time::{Duration, Instant};
pub struct AdaptiveOptimizer {
samplers: Vec<(String, Box<dyn Sampler>)>,
performance_history: PerformanceHistory,
problem_analyzer: ProblemAnalyzer,
strategy_selector: StrategySelector,
#[allow(dead_code)]
parameter_tuner: ParameterTuner,
learning_rate: f64,
#[allow(dead_code)]
exploration_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceHistory {
feature_performance: HashMap<ProblemFeatures, AlgorithmPerformance>,
recent_runs: VecDeque<RunRecord>,
best_solutions: HashMap<String, BestSolution>,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProblemFeatures {
size_category: SizeCategory,
density_category: DensityCategory,
structure_type: StructureType,
constraint_complexity: ConstraintComplexity,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum SizeCategory {
Tiny, Small, Medium, Large, VeryLarge, }
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum DensityCategory {
Sparse, Medium, Dense, }
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum StructureType {
Random,
Regular,
Hierarchical,
Modular,
Unknown,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConstraintComplexity {
None,
Simple,
Moderate,
Complex,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlgorithmPerformance {
success_rate: f64,
avg_quality: f64,
avg_time_ms: f64,
n_runs: usize,
best_params: HashMap<String, f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunRecord {
problem_id: String,
algorithm: String,
parameters: HashMap<String, f64>,
quality: f64,
time_ms: f64,
success: bool,
features: ProblemFeatures,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BestSolution {
problem_id: String,
best_energy: f64,
algorithm: String,
time_to_solution: f64,
solution: HashMap<String, bool>,
}
pub struct ProblemAnalyzer {
extractors: Vec<Box<dyn FeatureExtractor>>,
}
trait FeatureExtractor: Send + Sync {
fn extract(&self, qubo: &Array2<f64>) -> HashMap<String, f64>;
}
pub struct StrategySelector {
strategy: SelectionStrategy,
#[allow(dead_code)]
performance_threshold: f64,
}
#[derive(Debug, Clone)]
pub enum SelectionStrategy {
ThompsonSampling,
UCB { c: f64 },
EpsilonGreedy { epsilon: f64 },
Adaptive,
}
pub struct ParameterTuner {
#[allow(dead_code)]
param_ranges: HashMap<String, (f64, f64)>,
#[allow(dead_code)]
tuning_method: TuningMethod,
#[allow(dead_code)]
tuning_history: HashMap<String, Vec<(HashMap<String, f64>, f64)>>,
}
#[derive(Debug, Clone)]
pub enum TuningMethod {
Grid { resolution: usize },
Random { n_trials: usize },
Bayesian,
Evolutionary { population_size: usize },
}
impl Default for AdaptiveOptimizer {
fn default() -> Self {
Self::new()
}
}
impl AdaptiveOptimizer {
pub fn new() -> Self {
Self {
samplers: vec![
(
"SA".to_string(),
Box::new(crate::sampler::SASampler::new(None)),
),
(
"GA".to_string(),
Box::new(crate::sampler::GASampler::new(None)),
),
],
performance_history: PerformanceHistory {
feature_performance: HashMap::new(),
recent_runs: VecDeque::with_capacity(1000),
best_solutions: HashMap::new(),
},
problem_analyzer: ProblemAnalyzer::new(),
strategy_selector: StrategySelector::new(SelectionStrategy::Adaptive),
parameter_tuner: ParameterTuner::new(TuningMethod::Bayesian),
learning_rate: 0.1,
exploration_rate: 0.2,
}
}
pub fn add_sampler(&mut self, name: String, sampler: Box<dyn Sampler>) {
self.samplers.push((name, sampler));
}
#[must_use]
pub const fn with_learning_rate(mut self, rate: f64) -> Self {
self.learning_rate = rate;
self
}
pub fn optimize(
&mut self,
qubo: &Array2<f64>,
var_map: &HashMap<String, usize>,
time_limit: Duration,
) -> Result<OptimizationResult, String> {
let start_time = Instant::now();
let problem_id = self.generate_problem_id(qubo);
let features = self.problem_analyzer.analyze(qubo);
let (algorithm, parameters) = self.select_algorithm_and_params(&features)?;
let sampler = self.configure_sampler(&algorithm, ¶meters)?;
let mut best_result: Option<SampleResult> = None;
let mut iterations = 0;
let mut improvement_history = Vec::new();
while start_time.elapsed() < time_limit {
iterations += 1;
let shots = self.calculate_shot_count(&features, start_time.elapsed(), time_limit);
match sampler.run_qubo(&(qubo.clone(), var_map.clone()), shots) {
Ok(results) => {
for result in results {
let should_update = best_result
.as_ref()
.map_or(true, |best| result.energy < best.energy);
if should_update {
improvement_history
.push((start_time.elapsed().as_secs_f64(), result.energy));
best_result = Some(result);
}
}
}
Err(e) => {
self.record_run(RunRecord {
problem_id,
algorithm: algorithm.clone(),
parameters: parameters.clone(),
quality: f64::INFINITY,
time_ms: start_time.elapsed().as_millis() as f64,
success: false,
features,
});
return Err(format!("Sampler error: {e:?}"));
}
}
if self.check_convergence(&improvement_history) {
break;
}
if iterations % 10 == 0 {
self.adjust_parameters(&mut parameters.clone(), &improvement_history);
}
}
let total_time = start_time.elapsed();
if let Some(best) = best_result {
self.record_run(RunRecord {
problem_id: problem_id.clone(),
algorithm: algorithm.clone(),
parameters,
quality: best.energy,
time_ms: total_time.as_millis() as f64,
success: true,
features: features.clone(),
});
self.update_best_solution(problem_id, &best, &algorithm, total_time.as_secs_f64());
Ok(OptimizationResult {
best_solution: best.assignments,
best_energy: best.energy,
algorithm_used: algorithm,
time_taken: total_time,
iterations,
improvement_history,
features,
})
} else {
Err("No solution found".to_string())
}
}
fn generate_problem_id(&self, qubo: &Array2<f64>) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
qubo.shape().hash(&mut hasher);
let n = qubo.shape()[0];
for i in (0..n).step_by((n / 10).max(1)) {
for j in (0..n).step_by((n / 10).max(1)) {
(qubo[[i, j]].to_bits()).hash(&mut hasher);
}
}
format!("prob_{:x}", hasher.finish())
}
fn select_algorithm_and_params(
&self,
features: &ProblemFeatures,
) -> Result<(String, HashMap<String, f64>), String> {
let perf = self.performance_history.feature_performance.get(features);
match &self.strategy_selector.strategy {
SelectionStrategy::Adaptive => {
if let Some(perf) = perf {
if perf.n_runs > 10 && perf.success_rate > 0.8 {
let algorithm = self.get_best_algorithm_for_features(features);
let params = perf.best_params.clone();
Ok((algorithm, params))
} else {
self.explore_new_algorithm(features)
}
} else {
self.select_by_heuristics(features)
}
}
SelectionStrategy::ThompsonSampling => self.thompson_sampling_select(features),
SelectionStrategy::UCB { c } => self.ucb_select(features, *c),
SelectionStrategy::EpsilonGreedy { epsilon } => {
if thread_rng().random::<f64>() < *epsilon {
self.random_select()
} else {
self.greedy_select(features)
}
}
}
}
fn select_by_heuristics(
&self,
features: &ProblemFeatures,
) -> Result<(String, HashMap<String, f64>), String> {
let algorithm = match (&features.size_category, &features.density_category) {
(SizeCategory::Tiny | SizeCategory::Small, _) => "SA",
(_, DensityCategory::Sparse) => "GA",
(SizeCategory::Medium, DensityCategory::Medium) => "SA",
_ => "GA",
};
let params = self.get_default_params(algorithm);
Ok((algorithm.to_string(), params))
}
fn get_default_params(&self, algorithm: &str) -> HashMap<String, f64> {
let mut params = HashMap::new();
match algorithm {
"SA" => {
params.insert("beta_min".to_string(), 0.1);
params.insert("beta_max".to_string(), 10.0);
params.insert("sweeps".to_string(), 1000.0);
}
"GA" => {
params.insert("population_size".to_string(), 100.0);
params.insert("elite_fraction".to_string(), 0.1);
params.insert("mutation_rate".to_string(), 0.01);
}
_ => {}
}
params
}
fn configure_sampler(
&self,
algorithm: &str,
parameters: &HashMap<String, f64>,
) -> Result<Box<dyn Sampler>, String> {
match algorithm {
"SA" => {
let mut sampler = crate::sampler::SASampler::new(None);
if let Some(&beta_min) = parameters.get("beta_min") {
if let Some(&beta_max) = parameters.get("beta_max") {
sampler = sampler.with_beta_range(beta_min, beta_max);
}
}
if let Some(&sweeps) = parameters.get("sweeps") {
sampler = sampler.with_sweeps(sweeps as usize);
}
Ok(Box::new(sampler))
}
"GA" => {
let mut sampler = crate::sampler::GASampler::new(None);
if let Some(&pop_size) = parameters.get("population_size") {
sampler = sampler.with_population_size(pop_size as usize);
}
if let Some(&elite) = parameters.get("elite_fraction") {
sampler = sampler.with_elite_fraction(elite);
}
if let Some(&mutation) = parameters.get("mutation_rate") {
sampler = sampler.with_mutation_rate(mutation);
}
Ok(Box::new(sampler))
}
_ => Err(format!("Unknown algorithm: {algorithm}")),
}
}
fn calculate_shot_count(
&self,
features: &ProblemFeatures,
elapsed: Duration,
time_limit: Duration,
) -> usize {
let remaining_fraction = 1.0 - (elapsed.as_secs_f64() / time_limit.as_secs_f64());
let base_shots = match features.size_category {
SizeCategory::Tiny => 10,
SizeCategory::Small => 50,
SizeCategory::Medium => 100,
SizeCategory::Large => 200,
SizeCategory::VeryLarge => 500,
};
((base_shots as f64) * remaining_fraction.sqrt()) as usize
}
fn check_convergence(&self, history: &[(f64, f64)]) -> bool {
if history.len() < 10 {
return false;
}
let recent = &history[history.len() - 10..];
let best_recent = recent
.iter()
.map(|(_, e)| *e)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let best_overall = history
.iter()
.map(|(_, e)| *e)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
match (best_recent, best_overall) {
(Some(recent), Some(overall)) => (recent - overall).abs() < 1e-6,
_ => false,
}
}
fn adjust_parameters(&mut self, params: &mut HashMap<String, f64>, history: &[(f64, f64)]) {
if history.len() < 2 {
return;
}
let Some(last_entry) = history.last() else {
return;
};
let recent_improvement = last_entry.1 - history[history.len() - 2].1;
if recent_improvement < 0.0 {
for (_, value) in params.iter_mut() {
*value *= 1.0 + self.learning_rate;
}
} else {
for (_, value) in params.iter_mut() {
*value *= self.learning_rate.mul_add(-0.5, 1.0);
}
}
}
fn record_run(&mut self, record: RunRecord) {
self.performance_history
.recent_runs
.push_back(record.clone());
if self.performance_history.recent_runs.len() > 1000 {
self.performance_history.recent_runs.pop_front();
}
let perf = self
.performance_history
.feature_performance
.entry(record.features.clone())
.or_insert_with(|| AlgorithmPerformance {
success_rate: 0.0,
avg_quality: 0.0,
avg_time_ms: 0.0,
n_runs: 0,
best_params: HashMap::new(),
});
let n = perf.n_runs as f64;
perf.avg_quality = perf.avg_quality.mul_add(n, record.quality) / (n + 1.0);
perf.avg_time_ms = perf.avg_time_ms.mul_add(n, record.time_ms) / (n + 1.0);
perf.success_rate = perf
.success_rate
.mul_add(n, if record.success { 1.0 } else { 0.0 })
/ (n + 1.0);
perf.n_runs += 1;
if record.success && (perf.best_params.is_empty() || record.quality < perf.avg_quality) {
perf.best_params = record.parameters;
}
}
fn update_best_solution(
&mut self,
problem_id: String,
result: &SampleResult,
algorithm: &str,
time: f64,
) {
let entry = self
.performance_history
.best_solutions
.entry(problem_id.clone())
.or_insert_with(|| BestSolution {
problem_id,
best_energy: f64::INFINITY,
algorithm: String::new(),
time_to_solution: 0.0,
solution: HashMap::new(),
});
if result.energy < entry.best_energy {
entry.best_energy = result.energy;
entry.algorithm = algorithm.to_string();
entry.time_to_solution = time;
entry.solution = result.assignments.clone();
}
}
fn get_best_algorithm_for_features(&self, _features: &ProblemFeatures) -> String {
"SA".to_string()
}
fn explore_new_algorithm(
&self,
_features: &ProblemFeatures,
) -> Result<(String, HashMap<String, f64>), String> {
let idx = thread_rng().random_range(0..self.samplers.len());
let algorithm = self.samplers[idx].0.clone();
let params = self.get_default_params(&algorithm);
Ok((algorithm, params))
}
fn thompson_sampling_select(
&self,
_features: &ProblemFeatures,
) -> Result<(String, HashMap<String, f64>), String> {
self.random_select()
}
fn ucb_select(
&self,
features: &ProblemFeatures,
_c: f64,
) -> Result<(String, HashMap<String, f64>), String> {
self.select_by_heuristics(features)
}
fn random_select(&self) -> Result<(String, HashMap<String, f64>), String> {
let idx = thread_rng().random_range(0..self.samplers.len());
let algorithm = self.samplers[idx].0.clone();
let params = self.get_default_params(&algorithm);
Ok((algorithm, params))
}
fn greedy_select(
&self,
features: &ProblemFeatures,
) -> Result<(String, HashMap<String, f64>), String> {
if let Some(perf) = self.performance_history.feature_performance.get(features) {
let algorithm = self.get_best_algorithm_for_features(features);
Ok((algorithm, perf.best_params.clone()))
} else {
self.select_by_heuristics(features)
}
}
pub fn save_history(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(&self.performance_history)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn load_history(&mut self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
let json = std::fs::read_to_string(path)?;
self.performance_history = serde_json::from_str(&json)?;
Ok(())
}
}
impl ProblemAnalyzer {
fn new() -> Self {
Self {
extractors: vec![
Box::new(BasicFeatureExtractor),
Box::new(StructureFeatureExtractor),
],
}
}
fn analyze(&self, qubo: &Array2<f64>) -> ProblemFeatures {
let n = qubo.shape()[0];
let mut all_features = HashMap::new();
for extractor in &self.extractors {
all_features.extend(extractor.extract(qubo));
}
let size_category = match n {
0..=9 => SizeCategory::Tiny,
10..=49 => SizeCategory::Small,
50..=199 => SizeCategory::Medium,
200..=999 => SizeCategory::Large,
_ => SizeCategory::VeryLarge,
};
let density = all_features.get("density").copied().unwrap_or(0.5);
let density_category = match density {
d if d < 0.1 => DensityCategory::Sparse,
d if d < 0.5 => DensityCategory::Medium,
_ => DensityCategory::Dense,
};
let structure_score = all_features.get("structure_score").copied().unwrap_or(0.0);
let structure_type = if structure_score < 0.2 {
StructureType::Random
} else if structure_score < 0.5 {
StructureType::Regular
} else {
StructureType::Hierarchical
};
ProblemFeatures {
size_category,
density_category,
structure_type,
constraint_complexity: ConstraintComplexity::None, }
}
}
struct BasicFeatureExtractor;
impl FeatureExtractor for BasicFeatureExtractor {
fn extract(&self, qubo: &Array2<f64>) -> HashMap<String, f64> {
let n = qubo.shape()[0];
let mut features = HashMap::new();
features.insert("size".to_string(), n as f64);
let non_zeros = qubo.iter().filter(|&&x| x.abs() > 1e-10).count();
features.insert("density".to_string(), non_zeros as f64 / (n * n) as f64);
let values: Vec<f64> = qubo.iter().copied().collect();
features.insert(
"mean".to_string(),
values.iter().sum::<f64>() / values.len() as f64,
);
features.insert(
"max".to_string(),
values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)),
);
features.insert(
"min".to_string(),
values.iter().fold(f64::INFINITY, |a, &b| a.min(b)),
);
features
}
}
struct StructureFeatureExtractor;
impl FeatureExtractor for StructureFeatureExtractor {
fn extract(&self, qubo: &Array2<f64>) -> HashMap<String, f64> {
let mut features = HashMap::new();
let n = qubo.shape()[0];
let mut diag_sum = 0.0;
let mut total_sum = 0.0;
for i in 0..n {
diag_sum += qubo[[i, i]].abs();
for j in 0..n {
total_sum += qubo[[i, j]].abs();
}
}
features.insert(
"diagonal_dominance".to_string(),
if total_sum > 0.0 {
diag_sum / total_sum
} else {
0.0
},
);
let mut symmetry_score = 0.0;
for i in 0..n {
for j in i + 1..n {
let diff = (qubo[[i, j]] - qubo[[j, i]]).abs();
let avg = f64::midpoint(qubo[[i, j]].abs(), qubo[[j, i]].abs());
if avg > 1e-10 {
symmetry_score += 1.0 - diff / avg;
}
}
}
features.insert(
"symmetry".to_string(),
symmetry_score / ((n * (n - 1)) / 2) as f64,
);
let diagonal_dominance = features.get("diagonal_dominance").copied().unwrap_or(0.0);
let symmetry = features.get("symmetry").copied().unwrap_or(0.0);
let structure_score = diagonal_dominance * 0.5 + symmetry * 0.5;
features.insert("structure_score".to_string(), structure_score);
features
}
}
impl StrategySelector {
const fn new(strategy: SelectionStrategy) -> Self {
Self {
strategy,
performance_threshold: 0.8,
}
}
}
impl ParameterTuner {
fn new(method: TuningMethod) -> Self {
Self {
param_ranges: HashMap::new(),
tuning_method: method,
tuning_history: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct OptimizationResult {
pub best_solution: HashMap<String, bool>,
pub best_energy: f64,
pub algorithm_used: String,
pub time_taken: Duration,
pub iterations: usize,
pub improvement_history: Vec<(f64, f64)>,
pub features: ProblemFeatures,
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_adaptive_optimizer() {
let mut optimizer = AdaptiveOptimizer::new();
let mut qubo = array![[0.0, -1.0, 0.5], [-1.0, 0.0, -0.5], [0.5, -0.5, 0.0]];
let mut var_map = HashMap::new();
var_map.insert("x".to_string(), 0);
var_map.insert("y".to_string(), 1);
var_map.insert("z".to_string(), 2);
let mut result = optimizer
.optimize(&qubo, &var_map, Duration::from_secs(1))
.expect("Optimization should succeed for valid QUBO");
assert!(!result.best_solution.is_empty());
assert!(result.best_energy < 0.0);
assert!(!result.improvement_history.is_empty());
}
#[test]
#[ignore]
fn test_problem_analyzer() {
let analyzer = ProblemAnalyzer::new();
let small_sparse = Array2::eye(10);
let features = analyzer.analyze(&small_sparse);
assert_eq!(features.size_category, SizeCategory::Small);
assert_eq!(features.density_category, DensityCategory::Sparse);
let large_dense = Array2::ones((500, 500));
let features = analyzer.analyze(&large_dense);
assert_eq!(features.size_category, SizeCategory::Large);
assert_eq!(features.density_category, DensityCategory::Dense);
}
}