1#![allow(clippy::needless_range_loop)]
4
5pub(crate) mod driver;
16pub(crate) mod geometry;
17pub(crate) mod init;
18pub(crate) mod rescue;
19pub(crate) mod trsbox;
20
21#[cfg(test)]
22mod parity;
23
24use std::marker::PhantomData;
25
26use crate::core::constraint::BoxConstraints;
27use crate::core::inner::InitialState;
28use crate::core::math::{Scalar, VectorLen};
29use crate::core::problem::{CostFunction, Problem};
30use crate::core::solver::Solver;
31use crate::core::state::BobyqaState;
32use crate::core::termination::TerminationReason;
33
34use driver::{BobyqaWork, Transition};
35
36pub struct Bounded;
40
41pub struct Bobyqa<Mode = Bounded, F = f64> {
118 rho_beg: F,
119 rho_end: F,
120 npt: Option<usize>,
121 work: Option<BobyqaWork<F>>,
124 _mode: PhantomData<fn() -> Mode>,
125}
126
127impl<F: Scalar> Bobyqa<Bounded, F> {
128 pub fn new() -> Self {
131 Self {
132 rho_beg: F::from_f64(1.0).expect("1.0 representable"),
133 rho_end: F::from_f64(1e-6).expect("1e-6 representable"),
134 npt: None,
135 work: None,
136 _mode: PhantomData,
137 }
138 }
139
140 pub fn with_rho_beg(mut self, rho_beg: F) -> Self {
142 self.rho_beg = rho_beg;
143 self
144 }
145
146 pub fn with_rho_end(mut self, rho_end: F) -> Self {
148 self.rho_end = rho_end;
149 self
150 }
151
152 pub fn with_npt(mut self, npt: usize) -> Self {
155 self.npt = Some(npt);
156 self
157 }
158}
159
160impl<F: Scalar> Default for Bobyqa<Bounded, F> {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166fn fill_from<V, F>(template: &V, slice: &[F]) -> V
168where
169 V: Clone + std::ops::IndexMut<usize, Output = F>,
170 F: Copy,
171{
172 let mut v = template.clone();
173 for (i, &x) in slice.iter().enumerate() {
174 v[i] = x;
175 }
176 v
177}
178
179fn to_vec<V, F>(v: &V, n: usize) -> Vec<F>
181where
182 V: std::ops::Index<usize, Output = F>,
183 F: Copy,
184{
185 (0..n).map(|i| v[i]).collect()
186}
187
188impl<V, F> InitialState<V> for Bobyqa<Bounded, F>
189where
190 F: Scalar,
191 V: Clone,
192{
193 type State = BobyqaState<V, F>;
194 fn seed(&self, x: &V) -> Self::State {
195 BobyqaState::new(x.clone())
196 }
197}
198
199impl<P, V, F> Solver<P, BobyqaState<V, F>> for Bobyqa<Bounded, F>
200where
201 F: Scalar,
202 P: CostFunction<Param = V, Output = F> + BoxConstraints,
203 V: Clone
204 + VectorLen
205 + std::ops::Index<usize, Output = F>
206 + std::ops::IndexMut<usize, Output = F>,
207{
208 type Error = P::Error;
209
210 fn init(
211 &mut self,
212 problem: &mut Problem<P>,
213 mut state: BobyqaState<V, F>,
214 ) -> Result<BobyqaState<V, F>, Self::Error> {
215 let n = state.param.vec_len();
216 assert!(n >= 1, "Bobyqa requires a non-empty start point");
217 let npt = self.npt.unwrap_or(2 * n + 1);
218
219 let x0 = to_vec(&state.param, n);
220 let lower = to_vec(problem.inner().lower(), n);
221 let upper = to_vec(problem.inner().upper(), n);
222 let template = state.param.clone();
223
224 let (work, best_x, best_f) = {
225 let mut eval =
226 |slice: &[F]| -> Result<F, P::Error> { problem.cost(&fill_from(&template, slice)) };
227 BobyqaWork::try_init(
228 x0,
229 &lower,
230 &upper,
231 self.rho_beg,
232 self.rho_end,
233 npt,
234 &mut eval,
235 )?
236 };
237
238 state.param = fill_from(&template, &best_x);
239 state.cost = Some(best_f);
240 state.rho = work.rho();
241 self.work = Some(work);
242 Ok(state)
243 }
244
245 fn next_iter(
246 &mut self,
247 problem: &mut Problem<P>,
248 mut state: BobyqaState<V, F>,
249 ) -> Result<(BobyqaState<V, F>, Option<TerminationReason>), Self::Error> {
250 let template = state.param.clone();
251 let work = self
252 .work
253 .as_mut()
254 .expect("Bobyqa::init must run before next_iter");
255
256 let out = {
257 let mut eval =
258 |slice: &[F]| -> Result<F, P::Error> { problem.cost(&fill_from(&template, slice)) };
259 work.step(&mut eval)?
260 };
261 state.rho = work.rho();
262
263 let mut best_f = state.cost.expect("Bobyqa::init seeds the cost");
266 for (xabs, f_new) in &out.evaluated {
267 if *f_new < best_f {
268 best_f = *f_new;
269 state.param = fill_from(&template, xabs);
270 state.cost = Some(best_f);
271 }
272 }
273
274 let reason = match out.transition {
275 Transition::Converged => Some(TerminationReason::SolverConverged),
276 Transition::Continue | Transition::RhoReduced => None,
277 };
278 Ok((state, reason))
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use crate::core::constraint::BoxConstraints;
285 use crate::core::problem::CostFunction;
286 use crate::{Bobyqa, BobyqaState, Executor, MaxCostEvals};
287
288 struct Quad {
289 lower: Vec<f64>,
290 upper: Vec<f64>,
291 }
292 impl CostFunction for Quad {
293 type Param = Vec<f64>;
294 type Output = f64;
295 type Error = std::convert::Infallible;
296 fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
297 Ok((x[0] - 1.0).powi(2) + 2.0 * (x[1] + 2.0).powi(2))
298 }
299 }
300 impl BoxConstraints for Quad {
301 fn lower(&self) -> &Vec<f64> {
302 &self.lower
303 }
304 fn upper(&self) -> &Vec<f64> {
305 &self.upper
306 }
307 }
308
309 #[test]
311 fn converges_interior_minimum() {
312 let problem = Quad {
313 lower: vec![-5.0, -5.0],
314 upper: vec![5.0, 5.0],
315 };
316 let result = Executor::new(
317 problem,
318 Bobyqa::new().with_rho_beg(0.5).with_rho_end(1e-8),
319 BobyqaState::new(vec![0.0, 0.0]),
320 )
321 .terminate_on(MaxCostEvals(500))
322 .run()
323 .unwrap();
324 let x = result.best_param();
325 assert!((x[0] - 1.0).abs() < 1e-5, "x0 = {}", x[0]);
326 assert!((x[1] + 2.0).abs() < 1e-5, "x1 = {}", x[1]);
327 assert!(result.best_cost() < 1e-9, "cost = {}", result.best_cost());
328 }
329
330 #[test]
334 fn narrow_box_revises_rho_beg() {
335 let problem = Quad {
336 lower: vec![0.9, -2.2],
338 upper: vec![1.1, -1.8],
339 };
340 let result = Executor::new(problem, Bobyqa::new(), BobyqaState::new(vec![1.0, -2.0]))
341 .terminate_on(MaxCostEvals(500))
342 .run()
343 .unwrap();
344 let x = result.best_param();
345 assert!((x[0] - 1.0).abs() < 1e-3, "x0 = {}", x[0]);
346 assert!((x[1] + 2.0).abs() < 1e-3, "x1 = {}", x[1]);
347 }
348
349 #[test]
351 fn infeasible_start_converges() {
352 let problem = Quad {
353 lower: vec![-3.0, -3.0],
354 upper: vec![3.0, 3.0],
355 };
356 let result = Executor::new(
357 problem,
358 Bobyqa::new().with_rho_beg(0.5).with_rho_end(1e-8),
359 BobyqaState::new(vec![100.0, -100.0]),
360 )
361 .terminate_on(MaxCostEvals(500))
362 .run()
363 .unwrap();
364 let x = result.best_param();
365 assert!((x[0] - 1.0).abs() < 1e-5, "x0 = {}", x[0]);
366 assert!((x[1] + 2.0).abs() < 1e-5, "x1 = {}", x[1]);
367 }
368
369 #[test]
372 fn converges_to_active_bound() {
373 struct Bowl {
374 lower: Vec<f64>,
375 upper: Vec<f64>,
376 }
377 impl CostFunction for Bowl {
378 type Param = Vec<f64>;
379 type Output = f64;
380 type Error = std::convert::Infallible;
381 fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
382 Ok((x[0] - 5.0).powi(2) + (x[1] - 5.0).powi(2))
383 }
384 }
385 impl BoxConstraints for Bowl {
386 fn lower(&self) -> &Vec<f64> {
387 &self.lower
388 }
389 fn upper(&self) -> &Vec<f64> {
390 &self.upper
391 }
392 }
393 let problem = Bowl {
394 lower: vec![-2.0, -2.0],
395 upper: vec![2.0, 2.0],
396 };
397 let result = Executor::new(
398 problem,
399 Bobyqa::new().with_rho_beg(0.5).with_rho_end(1e-8),
400 BobyqaState::new(vec![0.0, 0.0]),
401 )
402 .terminate_on(MaxCostEvals(500))
403 .run()
404 .unwrap();
405 let x = result.best_param();
406 assert!((x[0] - 2.0).abs() < 1e-5, "x0 = {}", x[0]);
407 assert!((x[1] - 2.0).abs() < 1e-5, "x1 = {}", x[1]);
408 }
409
410 #[test]
412 fn converges_rosenbrock_box() {
413 struct Rosen {
414 lower: Vec<f64>,
415 upper: Vec<f64>,
416 }
417 impl CostFunction for Rosen {
418 type Param = Vec<f64>;
419 type Output = f64;
420 type Error = std::convert::Infallible;
421 fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
422 Ok(100.0 * (x[1] - x[0] * x[0]).powi(2) + (1.0 - x[0]).powi(2))
423 }
424 }
425 impl BoxConstraints for Rosen {
426 fn lower(&self) -> &Vec<f64> {
427 &self.lower
428 }
429 fn upper(&self) -> &Vec<f64> {
430 &self.upper
431 }
432 }
433 let problem = Rosen {
434 lower: vec![-5.0, -5.0],
435 upper: vec![5.0, 5.0],
436 };
437 let result = Executor::new(
438 problem,
439 Bobyqa::new().with_rho_beg(0.5).with_rho_end(1e-6),
440 BobyqaState::new(vec![-1.2, 1.0]),
441 )
442 .terminate_on(MaxCostEvals(2000))
443 .run()
444 .unwrap();
445 let x = result.best_param();
446 assert!((x[0] - 1.0).abs() < 1e-3, "x0 = {}", x[0]);
447 assert!((x[1] - 1.0).abs() < 1e-3, "x1 = {}", x[1]);
448 assert!(result.best_cost() < 1e-6, "cost = {}", result.best_cost());
449 }
450}