1use crate::{
2 ActiveDesignError, BayesianQuadrature, ScalarNormalPosterior, VarianceReductionAcquisition,
3};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ActiveTermination {
8 VarianceToleranceReached,
10 EvaluationBudgetReached,
12 CandidatesExhausted,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct ActiveDesignStep {
19 point: f64,
20 value: f64,
21 predicted_variance_reduction: f64,
22 posterior_variance: f64,
23}
24
25impl ActiveDesignStep {
26 #[must_use]
28 pub const fn point(self) -> f64 {
29 self.point
30 }
31
32 #[must_use]
34 pub const fn value(self) -> f64 {
35 self.value
36 }
37
38 #[must_use]
40 pub const fn predicted_variance_reduction(self) -> f64 {
41 self.predicted_variance_reduction
42 }
43
44 #[must_use]
46 pub const fn posterior_variance(self) -> f64 {
47 self.posterior_variance
48 }
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub struct ActiveDesignResult {
54 nodes: Vec<f64>,
55 values: Vec<f64>,
56 remaining_candidates: Vec<f64>,
57 steps: Vec<ActiveDesignStep>,
58 posterior: ScalarNormalPosterior,
59 termination: ActiveTermination,
60}
61
62impl ActiveDesignResult {
63 #[must_use]
65 pub fn nodes(&self) -> &[f64] {
66 &self.nodes
67 }
68
69 #[must_use]
71 pub fn values(&self) -> &[f64] {
72 &self.values
73 }
74
75 #[must_use]
77 pub fn remaining_candidates(&self) -> &[f64] {
78 &self.remaining_candidates
79 }
80
81 #[must_use]
83 pub fn steps(&self) -> &[ActiveDesignStep] {
84 &self.steps
85 }
86
87 #[must_use]
89 pub const fn posterior(&self) -> ScalarNormalPosterior {
90 self.posterior
91 }
92
93 #[must_use]
95 pub const fn termination(&self) -> ActiveTermination {
96 self.termination
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq)]
102pub struct ActiveBayesianQuadrature {
103 quadrature: BayesianQuadrature,
104 acquisition: VarianceReductionAcquisition,
105}
106
107impl ActiveBayesianQuadrature {
108 #[must_use]
110 pub const fn new(quadrature: BayesianQuadrature) -> Self {
111 let acquisition = VarianceReductionAcquisition::new(
112 quadrature.kernel(),
113 quadrature.measure(),
114 quadrature.jitter(),
115 );
116 Self {
117 quadrature,
118 acquisition,
119 }
120 }
121
122 #[must_use]
124 pub const fn quadrature(&self) -> BayesianQuadrature {
125 self.quadrature
126 }
127
128 pub fn run<F>(
140 &self,
141 initial_nodes: &[f64],
142 initial_values: &[f64],
143 candidates: &[f64],
144 max_new_evaluations: usize,
145 variance_tolerance: f64,
146 mut function: F,
147 ) -> Result<ActiveDesignResult, ActiveDesignError>
148 where
149 F: FnMut(f64) -> f64,
150 {
151 if !variance_tolerance.is_finite() {
152 return Err(ActiveDesignError::NonFiniteVarianceTolerance);
153 }
154 if variance_tolerance < 0.0 {
155 return Err(ActiveDesignError::NegativeVarianceTolerance);
156 }
157 if candidates.iter().any(|candidate| !candidate.is_finite()) {
158 return Err(ActiveDesignError::NonFiniteCandidate);
159 }
160
161 let mut nodes = initial_nodes.to_vec();
162 let mut values = initial_values.to_vec();
163 let mut remaining_candidates = candidates.to_vec();
164 let mut steps = Vec::new();
165 let mut posterior = self.quadrature.posterior(&nodes, &values)?;
166
167 if posterior.variance() <= variance_tolerance {
168 return Ok(ActiveDesignResult {
169 nodes,
170 values,
171 remaining_candidates,
172 steps,
173 posterior,
174 termination: ActiveTermination::VarianceToleranceReached,
175 });
176 }
177
178 for _ in 0..max_new_evaluations {
179 if remaining_candidates.is_empty() {
180 return Ok(ActiveDesignResult {
181 nodes,
182 values,
183 remaining_candidates,
184 steps,
185 posterior,
186 termination: ActiveTermination::CandidatesExhausted,
187 });
188 }
189
190 let selected = self
191 .acquisition
192 .select_best(&nodes, &remaining_candidates)?;
193 let point = selected.point();
194 let value = function(point);
195 if !value.is_finite() {
196 return Err(ActiveDesignError::NonFiniteFunctionValue);
197 }
198
199 remaining_candidates.remove(selected.index());
200 nodes.push(point);
201 values.push(value);
202 posterior = self.quadrature.posterior(&nodes, &values)?;
203 steps.push(ActiveDesignStep {
204 point,
205 value,
206 predicted_variance_reduction: selected.variance_reduction(),
207 posterior_variance: posterior.variance(),
208 });
209
210 if posterior.variance() <= variance_tolerance {
211 return Ok(ActiveDesignResult {
212 nodes,
213 values,
214 remaining_candidates,
215 steps,
216 posterior,
217 termination: ActiveTermination::VarianceToleranceReached,
218 });
219 }
220 }
221
222 let termination = if remaining_candidates.is_empty() {
223 ActiveTermination::CandidatesExhausted
224 } else {
225 ActiveTermination::EvaluationBudgetReached
226 };
227
228 Ok(ActiveDesignResult {
229 nodes,
230 values,
231 remaining_candidates,
232 steps,
233 posterior,
234 termination,
235 })
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::{ActiveBayesianQuadrature, ActiveTermination};
242 use crate::{ActiveDesignError, BayesianQuadrature, GaussianMeasure, RbfKernel};
243
244 fn fixture() -> ActiveBayesianQuadrature {
245 let kernel = RbfKernel::new(1.0, 1.0).expect("kernel parameters are valid");
246 let measure = GaussianMeasure::new(0.0, 1.0).expect("measure parameters are valid");
247 ActiveBayesianQuadrature::new(BayesianQuadrature::new(kernel, measure, 1.0e-12))
248 }
249
250 #[test]
251 fn sequential_run_adds_selected_candidates_and_removes_them() {
252 let active = fixture();
253 let result = active
254 .run(
255 &[-1.0, 1.0],
256 &[1.0, 1.0],
257 &[-0.5, 0.0, 0.5, 2.0],
258 2,
259 0.0,
260 |x| x * x,
261 )
262 .expect("active design should be valid");
263
264 assert_eq!(result.nodes().len(), 4);
265 assert_eq!(result.values().len(), 4);
266 assert_eq!(result.steps().len(), 2);
267 assert_eq!(result.remaining_candidates().len(), 2);
268 for step in result.steps() {
269 assert!(result.nodes().contains(&step.point()));
270 assert!(!result.remaining_candidates().contains(&step.point()));
271 assert!(step.predicted_variance_reduction() >= 0.0);
272 }
273 }
274
275 #[test]
276 fn posterior_variance_is_non_increasing_across_active_steps() {
277 let active = fixture();
278 let initial = active
279 .quadrature()
280 .posterior(&[-1.0, 1.0], &[1.0, 1.0])
281 .expect("initial posterior is valid");
282 let result = active
283 .run(
284 &[-1.0, 1.0],
285 &[1.0, 1.0],
286 &[-2.0, -0.5, 0.0, 0.5, 2.0],
287 3,
288 0.0,
289 f64::cos,
290 )
291 .expect("active design should be valid");
292
293 let mut previous = initial.variance();
294 for step in result.steps() {
295 assert!(step.posterior_variance() <= previous + 1.0e-12);
296 previous = step.posterior_variance();
297 }
298 }
299
300 #[test]
301 fn stops_immediately_when_variance_tolerance_is_already_met() {
302 let active = fixture();
303 let initial = active
304 .quadrature()
305 .posterior(&[-1.0, 0.0, 1.0], &[1.0, 0.0, 1.0])
306 .expect("initial posterior is valid");
307 let result = active
308 .run(
309 &[-1.0, 0.0, 1.0],
310 &[1.0, 0.0, 1.0],
311 &[-0.5, 0.5],
312 5,
313 initial.variance(),
314 |x| x * x,
315 )
316 .expect("active design should be valid");
317
318 assert_eq!(
319 result.termination(),
320 ActiveTermination::VarianceToleranceReached
321 );
322 assert!(result.steps().is_empty());
323 }
324
325 #[test]
326 fn reports_evaluation_budget_termination() {
327 let active = fixture();
328 let result = active
329 .run(&[-1.0, 1.0], &[1.0, 1.0], &[-0.5, 0.0, 0.5], 1, 0.0, |x| {
330 x * x
331 })
332 .expect("active design should be valid");
333
334 assert_eq!(
335 result.termination(),
336 ActiveTermination::EvaluationBudgetReached
337 );
338 assert_eq!(result.steps().len(), 1);
339 }
340
341 #[test]
342 fn reports_candidate_exhaustion() {
343 let active = fixture();
344 let result = active
345 .run(&[-1.0, 1.0], &[1.0, 1.0], &[0.0], 3, 0.0, |x| x * x)
346 .expect("active design should be valid");
347
348 assert_eq!(result.termination(), ActiveTermination::CandidatesExhausted);
349 assert_eq!(result.steps().len(), 1);
350 assert!(result.remaining_candidates().is_empty());
351 }
352
353 #[test]
354 fn rejects_invalid_tolerance_and_function_values() {
355 let active = fixture();
356 assert_eq!(
357 active.run(&[-1.0, 1.0], &[1.0, 1.0], &[0.0], 1, -1.0, |x| x),
358 Err(ActiveDesignError::NegativeVarianceTolerance)
359 );
360 assert_eq!(
361 active.run(&[-1.0, 1.0], &[1.0, 1.0], &[0.0], 1, 0.0, |_| f64::NAN),
362 Err(ActiveDesignError::NonFiniteFunctionValue)
363 );
364 }
365}