use crate::problem::Problem;
use crate::rng::{mix_seed, Rng};
#[derive(Debug, Clone, Copy)]
pub enum Behavioral {
BelowCost(f64),
TopFraction(f64),
TopN(usize),
}
#[derive(Debug, Clone, Copy)]
pub struct Glue {
pub samples: usize,
pub behavioral: Behavioral,
pub seed: u64,
}
impl Default for Glue {
fn default() -> Self {
Glue {
samples: 10_000,
behavioral: Behavioral::TopFraction(0.1),
seed: 42,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GlueSample {
pub x: Vec<f64>,
pub cost: f64,
pub weight: f64,
}
#[derive(Debug, Clone)]
pub struct GlueResult {
pub behavioral: Vec<GlueSample>,
pub cutoff: f64,
pub total_samples: usize,
pub evaluations: usize,
}
impl Glue {
pub fn run(&self, problem: &(dyn Problem + Sync)) -> GlueResult {
let bounds = problem.bounds();
let n_samples = self.samples.max(1);
let sample = |i: usize| -> GlueSample {
let mut rng = Rng::new(mix_seed(self.seed, i as u64));
let x: Vec<f64> = bounds
.iter()
.map(|&(lo, hi)| rng.uniform_in(lo, hi))
.collect();
let c = problem.objective(&x);
GlueSample {
x,
cost: if c.is_finite() { c } else { f64::INFINITY },
weight: 0.0,
}
};
#[cfg(feature = "rayon")]
let mut all: Vec<GlueSample> = {
use rayon::prelude::*;
(0..n_samples).into_par_iter().map(sample).collect()
};
#[cfg(not(feature = "rayon"))]
let mut all: Vec<GlueSample> = (0..n_samples).map(sample).collect();
all.sort_by(|a, b| {
a.cost
.partial_cmp(&b.cost)
.unwrap_or(std::cmp::Ordering::Equal)
});
let cutoff = match self.behavioral {
Behavioral::BelowCost(c) => c,
Behavioral::TopFraction(frac) => {
let k = ((frac.clamp(0.0, 1.0) * n_samples as f64).round() as usize)
.clamp(1, n_samples);
all[k - 1].cost
}
Behavioral::TopN(nn) => {
let k = nn.clamp(1, n_samples);
all[k - 1].cost
}
};
let mut behavioral: Vec<GlueSample> = all
.into_iter()
.filter(|s| s.cost.is_finite() && s.cost <= cutoff)
.collect();
let raw: Vec<f64> = behavioral
.iter()
.map(|s| (cutoff - s.cost).max(0.0))
.collect();
let total: f64 = raw.iter().sum();
let k = behavioral.len();
for (s, &l) in behavioral.iter_mut().zip(&raw) {
s.weight = if total > 0.0 {
l / total
} else {
1.0 / k.max(1) as f64
};
}
GlueResult {
behavioral,
cutoff,
total_samples: n_samples,
evaluations: n_samples,
}
}
}
impl GlueResult {
pub fn acceptance_rate(&self) -> f64 {
self.behavioral.len() as f64 / self.total_samples.max(1) as f64
}
pub fn is_empty(&self) -> bool {
self.behavioral.is_empty()
}
pub fn parameter_mean(&self, j: usize) -> f64 {
self.behavioral.iter().map(|s| s.weight * s.x[j]).sum()
}
pub fn parameter_quantile(&self, j: usize, q: f64) -> f64 {
let pairs: Vec<(f64, f64)> = self.behavioral.iter().map(|s| (s.x[j], s.weight)).collect();
weighted_quantile(pairs, q)
}
pub fn weighted_quantile(&self, values: &[f64], q: f64) -> f64 {
let pairs: Vec<(f64, f64)> = values
.iter()
.zip(&self.behavioral)
.map(|(&v, s)| (v, s.weight))
.collect();
weighted_quantile(pairs, q)
}
}
fn weighted_quantile(mut pairs: Vec<(f64, f64)>, q: f64) -> f64 {
if pairs.is_empty() {
return f64::NAN;
}
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let total: f64 = pairs.iter().map(|p| p.1).sum();
let target = q.clamp(0.0, 1.0) * total;
let mut cum = 0.0;
for (v, w) in &pairs {
cum += w;
if cum >= target {
return *v;
}
}
pairs.last().unwrap().0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::problem::func;
#[test]
fn weighted_quantile_basic() {
let pairs: Vec<(f64, f64)> = (0..5).map(|v| (v as f64, 0.2)).collect();
assert_eq!(weighted_quantile(pairs.clone(), 0.5), 2.0);
assert_eq!(
weighted_quantile(pairs, 0.0_f64.max(f64::MIN_POSITIVE)),
0.0
);
}
#[test]
fn recovers_behavioral_region() {
let p = func(vec![(-10.0, 10.0)], |x| (x[0] - 3.0).powi(2));
let res = Glue {
samples: 20_000,
behavioral: Behavioral::BelowCost(1.0),
seed: 1,
}
.run(&p);
assert!(!res.is_empty());
for s in &res.behavioral {
assert!((2.0..=4.0).contains(&s.x[0]), "x out of band: {}", s.x[0]);
}
assert!((res.parameter_mean(0) - 3.0).abs() < 0.1);
let lo = res.parameter_quantile(0, 0.05);
let hi = res.parameter_quantile(0, 0.95);
assert!(
lo < 3.0 && hi > 3.0 && lo >= 2.0 && hi <= 4.0,
"[{lo}, {hi}]"
);
assert!(
(res.acceptance_rate() - 0.1).abs() < 0.03,
"acc {}",
res.acceptance_rate()
);
}
#[test]
fn top_fraction_keeps_expected_count() {
let p = func(vec![(0.0, 1.0)], |x| x[0]);
let res = Glue {
samples: 1000,
behavioral: Behavioral::TopFraction(0.2),
seed: 5,
}
.run(&p);
assert_eq!(res.behavioral.len(), 200);
assert!(res.behavioral[0].cost <= res.behavioral[1].cost);
}
#[test]
fn is_deterministic() {
let p = func(vec![(-5.0, 5.0), (-5.0, 5.0)], |x| {
x.iter().map(|v| v * v).sum()
});
let cfg = Glue {
samples: 5000,
behavioral: Behavioral::TopN(100),
seed: 9,
};
let a = cfg.run(&p);
let b = cfg.run(&p);
assert_eq!(a.behavioral, b.behavioral);
assert_eq!(a.cutoff, b.cutoff);
}
}