use crate::{
ActiveDesignError, BayesianQuadrature, ScalarNormalPosterior, VarianceReductionAcquisition,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActiveTermination {
VarianceToleranceReached,
EvaluationBudgetReached,
CandidatesExhausted,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ActiveDesignStep {
point: f64,
value: f64,
predicted_variance_reduction: f64,
posterior_variance: f64,
}
impl ActiveDesignStep {
#[must_use]
pub const fn point(self) -> f64 {
self.point
}
#[must_use]
pub const fn value(self) -> f64 {
self.value
}
#[must_use]
pub const fn predicted_variance_reduction(self) -> f64 {
self.predicted_variance_reduction
}
#[must_use]
pub const fn posterior_variance(self) -> f64 {
self.posterior_variance
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ActiveDesignResult {
nodes: Vec<f64>,
values: Vec<f64>,
remaining_candidates: Vec<f64>,
steps: Vec<ActiveDesignStep>,
posterior: ScalarNormalPosterior,
termination: ActiveTermination,
}
impl ActiveDesignResult {
#[must_use]
pub fn nodes(&self) -> &[f64] {
&self.nodes
}
#[must_use]
pub fn values(&self) -> &[f64] {
&self.values
}
#[must_use]
pub fn remaining_candidates(&self) -> &[f64] {
&self.remaining_candidates
}
#[must_use]
pub fn steps(&self) -> &[ActiveDesignStep] {
&self.steps
}
#[must_use]
pub const fn posterior(&self) -> ScalarNormalPosterior {
self.posterior
}
#[must_use]
pub const fn termination(&self) -> ActiveTermination {
self.termination
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ActiveBayesianQuadrature {
quadrature: BayesianQuadrature,
acquisition: VarianceReductionAcquisition,
}
impl ActiveBayesianQuadrature {
#[must_use]
pub const fn new(quadrature: BayesianQuadrature) -> Self {
let acquisition = VarianceReductionAcquisition::new(
quadrature.kernel(),
quadrature.measure(),
quadrature.jitter(),
);
Self {
quadrature,
acquisition,
}
}
#[must_use]
pub const fn quadrature(&self) -> BayesianQuadrature {
self.quadrature
}
pub fn run<F>(
&self,
initial_nodes: &[f64],
initial_values: &[f64],
candidates: &[f64],
max_new_evaluations: usize,
variance_tolerance: f64,
mut function: F,
) -> Result<ActiveDesignResult, ActiveDesignError>
where
F: FnMut(f64) -> f64,
{
if !variance_tolerance.is_finite() {
return Err(ActiveDesignError::NonFiniteVarianceTolerance);
}
if variance_tolerance < 0.0 {
return Err(ActiveDesignError::NegativeVarianceTolerance);
}
if candidates.iter().any(|candidate| !candidate.is_finite()) {
return Err(ActiveDesignError::NonFiniteCandidate);
}
let mut nodes = initial_nodes.to_vec();
let mut values = initial_values.to_vec();
let mut remaining_candidates = candidates.to_vec();
let mut steps = Vec::new();
let mut posterior = self.quadrature.posterior(&nodes, &values)?;
if posterior.variance() <= variance_tolerance {
return Ok(ActiveDesignResult {
nodes,
values,
remaining_candidates,
steps,
posterior,
termination: ActiveTermination::VarianceToleranceReached,
});
}
for _ in 0..max_new_evaluations {
if remaining_candidates.is_empty() {
return Ok(ActiveDesignResult {
nodes,
values,
remaining_candidates,
steps,
posterior,
termination: ActiveTermination::CandidatesExhausted,
});
}
let selected = self
.acquisition
.select_best(&nodes, &remaining_candidates)?;
let point = selected.point();
let value = function(point);
if !value.is_finite() {
return Err(ActiveDesignError::NonFiniteFunctionValue);
}
remaining_candidates.remove(selected.index());
nodes.push(point);
values.push(value);
posterior = self.quadrature.posterior(&nodes, &values)?;
steps.push(ActiveDesignStep {
point,
value,
predicted_variance_reduction: selected.variance_reduction(),
posterior_variance: posterior.variance(),
});
if posterior.variance() <= variance_tolerance {
return Ok(ActiveDesignResult {
nodes,
values,
remaining_candidates,
steps,
posterior,
termination: ActiveTermination::VarianceToleranceReached,
});
}
}
let termination = if remaining_candidates.is_empty() {
ActiveTermination::CandidatesExhausted
} else {
ActiveTermination::EvaluationBudgetReached
};
Ok(ActiveDesignResult {
nodes,
values,
remaining_candidates,
steps,
posterior,
termination,
})
}
}
#[cfg(test)]
mod tests {
use super::{ActiveBayesianQuadrature, ActiveTermination};
use crate::{ActiveDesignError, BayesianQuadrature, GaussianMeasure, RbfKernel};
fn fixture() -> ActiveBayesianQuadrature {
let kernel = RbfKernel::new(1.0, 1.0).expect("kernel parameters are valid");
let measure = GaussianMeasure::new(0.0, 1.0).expect("measure parameters are valid");
ActiveBayesianQuadrature::new(BayesianQuadrature::new(kernel, measure, 1.0e-12))
}
#[test]
fn sequential_run_adds_selected_candidates_and_removes_them() {
let active = fixture();
let result = active
.run(
&[-1.0, 1.0],
&[1.0, 1.0],
&[-0.5, 0.0, 0.5, 2.0],
2,
0.0,
|x| x * x,
)
.expect("active design should be valid");
assert_eq!(result.nodes().len(), 4);
assert_eq!(result.values().len(), 4);
assert_eq!(result.steps().len(), 2);
assert_eq!(result.remaining_candidates().len(), 2);
for step in result.steps() {
assert!(result.nodes().contains(&step.point()));
assert!(!result.remaining_candidates().contains(&step.point()));
assert!(step.predicted_variance_reduction() >= 0.0);
}
}
#[test]
fn posterior_variance_is_non_increasing_across_active_steps() {
let active = fixture();
let initial = active
.quadrature()
.posterior(&[-1.0, 1.0], &[1.0, 1.0])
.expect("initial posterior is valid");
let result = active
.run(
&[-1.0, 1.0],
&[1.0, 1.0],
&[-2.0, -0.5, 0.0, 0.5, 2.0],
3,
0.0,
f64::cos,
)
.expect("active design should be valid");
let mut previous = initial.variance();
for step in result.steps() {
assert!(step.posterior_variance() <= previous + 1.0e-12);
previous = step.posterior_variance();
}
}
#[test]
fn stops_immediately_when_variance_tolerance_is_already_met() {
let active = fixture();
let initial = active
.quadrature()
.posterior(&[-1.0, 0.0, 1.0], &[1.0, 0.0, 1.0])
.expect("initial posterior is valid");
let result = active
.run(
&[-1.0, 0.0, 1.0],
&[1.0, 0.0, 1.0],
&[-0.5, 0.5],
5,
initial.variance(),
|x| x * x,
)
.expect("active design should be valid");
assert_eq!(
result.termination(),
ActiveTermination::VarianceToleranceReached
);
assert!(result.steps().is_empty());
}
#[test]
fn reports_evaluation_budget_termination() {
let active = fixture();
let result = active
.run(&[-1.0, 1.0], &[1.0, 1.0], &[-0.5, 0.0, 0.5], 1, 0.0, |x| {
x * x
})
.expect("active design should be valid");
assert_eq!(
result.termination(),
ActiveTermination::EvaluationBudgetReached
);
assert_eq!(result.steps().len(), 1);
}
#[test]
fn reports_candidate_exhaustion() {
let active = fixture();
let result = active
.run(&[-1.0, 1.0], &[1.0, 1.0], &[0.0], 3, 0.0, |x| x * x)
.expect("active design should be valid");
assert_eq!(result.termination(), ActiveTermination::CandidatesExhausted);
assert_eq!(result.steps().len(), 1);
assert!(result.remaining_candidates().is_empty());
}
#[test]
fn rejects_invalid_tolerance_and_function_values() {
let active = fixture();
assert_eq!(
active.run(&[-1.0, 1.0], &[1.0, 1.0], &[0.0], 1, -1.0, |x| x),
Err(ActiveDesignError::NegativeVarianceTolerance)
);
assert_eq!(
active.run(&[-1.0, 1.0], &[1.0, 1.0], &[0.0], 1, 0.0, |_| f64::NAN),
Err(ActiveDesignError::NonFiniteFunctionValue)
);
}
}