use crate::{
BayesianQuadratureError, GaussianConditioner, GaussianMeasure, KernelIntegral, KernelMean,
RbfKernel, ScalarKernel, ScalarNormalPosterior,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BayesianQuadrature {
kernel: RbfKernel,
measure: GaussianMeasure,
jitter: f64,
}
impl BayesianQuadrature {
#[must_use]
pub const fn new(kernel: RbfKernel, measure: GaussianMeasure, jitter: f64) -> Self {
Self {
kernel,
measure,
jitter,
}
}
#[must_use]
pub const fn kernel(&self) -> RbfKernel {
self.kernel
}
#[must_use]
pub const fn measure(&self) -> GaussianMeasure {
self.measure
}
#[must_use]
pub const fn jitter(&self) -> f64 {
self.jitter
}
pub fn posterior(
&self,
nodes: &[f64],
values: &[f64],
) -> Result<ScalarNormalPosterior, BayesianQuadratureError> {
validate_observations(nodes, values)?;
let dimension = nodes.len();
let mut gram = Vec::with_capacity(dimension * dimension);
for &left in nodes {
for &right in nodes {
gram.push(self.kernel.covariance(left, right));
}
}
let kernel_mean: Vec<f64> = nodes
.iter()
.map(|&node| self.kernel.kernel_mean(&self.measure, node))
.collect();
let conditioner = GaussianConditioner::new(&gram, dimension, self.jitter)?;
let alpha = conditioner.solve(values)?;
let v = conditioner.solve(&kernel_mean)?;
let posterior_mean = dot(&kernel_mean, &alpha);
let prior_integral_variance = self.kernel.kernel_integral(&self.measure);
let raw_variance = prior_integral_variance - dot(&kernel_mean, &v);
let posterior_variance =
non_negative_roundoff_variance(raw_variance, prior_integral_variance)?;
Ok(ScalarNormalPosterior::new(
posterior_mean,
posterior_variance,
)?)
}
}
fn validate_observations(nodes: &[f64], values: &[f64]) -> Result<(), BayesianQuadratureError> {
if nodes.is_empty() {
return Err(BayesianQuadratureError::EmptyObservations);
}
if nodes.len() != values.len() {
return Err(BayesianQuadratureError::ObservationLengthMismatch);
}
if nodes.iter().any(|value| !value.is_finite()) {
return Err(BayesianQuadratureError::NonFiniteObservationNode);
}
if values.iter().any(|value| !value.is_finite()) {
return Err(BayesianQuadratureError::NonFiniteObservationValue);
}
Ok(())
}
fn dot(left: &[f64], right: &[f64]) -> f64 {
debug_assert_eq!(left.len(), right.len());
left.iter().zip(right).map(|(x, y)| x * y).sum()
}
fn non_negative_roundoff_variance(value: f64, scale: f64) -> Result<f64, BayesianQuadratureError> {
if value >= 0.0 {
return Ok(value);
}
let tolerance = 64.0 * f64::EPSILON * scale.abs().max(1.0);
if value >= -tolerance {
Ok(0.0)
} else {
Err(BayesianQuadratureError::MateriallyNegativePosteriorVariance { value, tolerance })
}
}
#[cfg(test)]
mod tests {
use super::{BayesianQuadrature, non_negative_roundoff_variance};
use crate::{
BayesianQuadratureError, ConditioningError, GaussianMeasure, KernelIntegral, KernelMean,
RbfKernel,
};
const TOLERANCE: f64 = 1.0e-11;
fn assert_close(actual: f64, expected: f64) {
let scale = expected.abs().max(1.0);
assert!(
(actual - expected).abs() <= TOLERANCE * scale,
"expected {expected:.16e}, got {actual:.16e}"
);
}
fn fixture() -> BayesianQuadrature {
let kernel = RbfKernel::new(1.0, 1.2).expect("kernel parameters are valid");
let measure = GaussianMeasure::new(0.0, 1.0).expect("measure parameters are valid");
BayesianQuadrature::new(kernel, measure, 1.0e-12)
}
#[test]
fn one_observation_matches_closed_form_conditioning() {
let quadrature = fixture();
let posterior = quadrature
.posterior(&[0.0], &[2.0])
.expect("one-node posterior is valid");
let z = quadrature.kernel().kernel_mean(&quadrature.measure(), 0.0);
let kappa = quadrature.kernel().kernel_integral(&quadrature.measure());
let denominator = quadrature.kernel().signal_variance() + quadrature.jitter();
assert_close(posterior.mean(), z * 2.0 / denominator);
assert_close(posterior.variance(), kappa - z * z / denominator);
}
#[test]
fn zero_observations_produce_zero_posterior_mean() {
let quadrature = fixture();
let posterior = quadrature
.posterior(&[-1.0, 0.0, 1.0], &[0.0, 0.0, 0.0])
.expect("posterior is valid");
assert_close(posterior.mean(), 0.0);
assert!(posterior.variance() >= 0.0);
}
#[test]
fn posterior_variance_does_not_depend_on_observed_values() {
let quadrature = fixture();
let first = quadrature
.posterior(&[-0.75, 0.25, 1.5], &[1.0, 2.0, -1.0])
.expect("posterior is valid");
let second = quadrature
.posterior(&[-0.75, 0.25, 1.5], &[10.0, -5.0, 3.0])
.expect("posterior is valid");
assert_close(first.variance(), second.variance());
}
#[test]
fn posterior_variance_is_no_greater_than_prior_integral_variance() {
let quadrature = fixture();
let posterior = quadrature
.posterior(&[-2.0, -0.5, 0.5, 2.0], &[1.0, 0.5, -0.5, -1.0])
.expect("posterior is valid");
let prior_variance = quadrature.kernel().kernel_integral(&quadrature.measure());
assert!(posterior.variance() >= 0.0);
assert!(posterior.variance() <= prior_variance + TOLERANCE);
}
#[test]
fn rejects_invalid_observations() {
let quadrature = fixture();
assert_eq!(
quadrature.posterior(&[], &[]),
Err(BayesianQuadratureError::EmptyObservations)
);
assert_eq!(
quadrature.posterior(&[0.0], &[1.0, 2.0]),
Err(BayesianQuadratureError::ObservationLengthMismatch)
);
assert_eq!(
quadrature.posterior(&[f64::NAN], &[1.0]),
Err(BayesianQuadratureError::NonFiniteObservationNode)
);
assert_eq!(
quadrature.posterior(&[0.0], &[f64::INFINITY]),
Err(BayesianQuadratureError::NonFiniteObservationValue)
);
}
#[test]
fn duplicate_nodes_fail_without_jitter() {
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");
let quadrature = BayesianQuadrature::new(kernel, measure, 0.0);
let result = quadrature.posterior(&[0.0, 0.0], &[1.0, 1.0]);
assert!(matches!(
result,
Err(BayesianQuadratureError::Conditioning(
ConditioningError::NotPositiveDefinite
))
));
}
#[test]
fn duplicate_nodes_can_be_regularized_with_explicit_jitter() {
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");
let quadrature = BayesianQuadrature::new(kernel, measure, 1.0e-8);
let posterior = quadrature
.posterior(&[0.0, 0.0], &[1.0, 1.0])
.expect("jitter regularizes duplicate nodes");
assert!(posterior.mean().is_finite());
assert!(posterior.variance() >= 0.0);
}
#[test]
fn tiny_negative_variance_is_clamped_to_zero() {
let scale = 2.0;
let tiny_negative = -32.0 * f64::EPSILON * scale;
assert_eq!(
non_negative_roundoff_variance(tiny_negative, scale),
Ok(0.0)
);
}
#[test]
fn material_negative_variance_is_rejected() {
let result = non_negative_roundoff_variance(-1.0e-6, 1.0);
assert!(matches!(
result,
Err(BayesianQuadratureError::MateriallyNegativePosteriorVariance { .. })
));
}
}