1#![deny(missing_docs)]
3
4use ndarray::{Array1, Array2, Axis};
5
6pub fn nelder_mead<F>(f: F, start: Array1<f64>, step: f64, max_iter: usize, tol: f64) -> Array1<f64>
8where
9 F: Fn(&Array1<f64>) -> f64,
10{
11 let n = start.len();
12 let mut simplex: Vec<(Array1<f64>, f64)> = Vec::with_capacity(n + 1);
13 simplex.push((start.clone(), f(&start)));
14 for i in 0..n {
15 let mut p = start.clone();
16 p[i] += step;
17 simplex.push((p.clone(), f(&p)));
18 }
19 for _ in 0..max_iter {
20 simplex.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
21 let best = simplex[0].1;
22 let worst = simplex[n].1;
23 if (worst - best).abs() < tol {
24 break;
25 }
26 let mut centroid = Array1::<f64>::zeros(n);
28 for (p, _) in simplex.iter().take(n) {
29 centroid += p;
30 }
31 centroid /= n as f64;
32 let reflection = ¢roid + (¢roid - &simplex[n].0);
34 let f_ref = f(&reflection);
35 if f_ref < simplex[0].1 {
36 let expansion = ¢roid + 2.0 * (&reflection - ¢roid);
37 let f_exp = f(&expansion);
38 if f_exp < f_ref {
39 simplex[n] = (expansion, f_exp);
40 } else {
41 simplex[n] = (reflection, f_ref);
42 }
43 } else if f_ref < simplex[n - 1].1 {
44 simplex[n] = (reflection, f_ref);
45 } else {
46 let contraction = ¢roid + 0.5 * (&simplex[n].0 - ¢roid);
47 let f_con = f(&contraction);
48 if f_con < simplex[n].1 {
49 simplex[n] = (contraction, f_con);
50 } else {
51 let best_point = simplex[0].0.clone();
53 for (p, val) in simplex.iter_mut().take(n + 1).skip(1) {
54 *p = &best_point + 0.5 * (&*p - &best_point);
55 *val = f(p);
56 }
57 }
58 }
59 }
60 simplex.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
61 simplex[0].0.clone()
62}
63
64pub fn bfgs<F, G>(f: F, grad: G, start: Array1<f64>, max_iter: usize, tol: f64) -> Array1<f64>
66where
67 F: Fn(&Array1<f64>) -> f64,
68 G: Fn(&Array1<f64>) -> Array1<f64>,
69{
70 let n = start.len();
71 let mut x = start;
72 let mut h = Array2::<f64>::eye(n);
73 for _ in 0..max_iter {
74 let g = grad(&x);
75 if g.iter().map(|v| v * v).sum::<f64>().sqrt() < tol {
76 break;
77 }
78 let p = -h.dot(&g);
79 let mut alpha = 1.0;
81 let fx = f(&x);
82 let mut x_new = &x + &(alpha * &p);
83 let mut f_new = f(&x_new);
84 while f_new > fx && alpha > 1e-8 {
85 alpha *= 0.5;
86 x_new = &x + &(alpha * &p);
87 f_new = f(&x_new);
88 }
89 let s = &x_new - &x;
90 let g_new = grad(&x_new);
91 let y = &g_new - &g;
92 let ys = y.dot(&s);
93 if ys.abs() < 1e-12 {
94 break;
95 }
96 let rho = 1.0 / ys;
97 let i = Array2::<f64>::eye(n);
98 let sy = s
99 .view()
100 .insert_axis(Axis(1))
101 .dot(&y.view().insert_axis(Axis(0)));
102 let ys_mat = y
103 .view()
104 .insert_axis(Axis(1))
105 .dot(&s.view().insert_axis(Axis(0)));
106 h = (i.clone() - rho * sy).dot(&h).dot(&(i - rho * ys_mat))
107 + rho
108 * s.view()
109 .insert_axis(Axis(1))
110 .dot(&s.view().insert_axis(Axis(0)));
111 x = x_new;
112 }
113 x
114}
115
116pub fn lbfgs<F, G>(
118 f: F,
119 grad: G,
120 start: Array1<f64>,
121 max_iter: usize,
122 tol: f64,
123 m: usize,
124) -> Array1<f64>
125where
126 F: Fn(&Array1<f64>) -> f64,
127 G: Fn(&Array1<f64>) -> Array1<f64>,
128{
129 let mut x = start;
130 let mut s_list: Vec<Array1<f64>> = Vec::new();
131 let mut y_list: Vec<Array1<f64>> = Vec::new();
132 let mut rho: Vec<f64> = Vec::new();
133 for _ in 0..max_iter {
134 let g = grad(&x);
135 let g_norm = g.iter().map(|v| v * v).sum::<f64>().sqrt();
136 if g_norm < tol {
137 break;
138 }
139 let mut q = g.clone();
140 let mut alpha = Vec::new();
141 for i in (0..s_list.len()).rev() {
142 let a = rho[i] * s_list[i].dot(&q);
143 alpha.push(a);
144 q = &q - &(a * &y_list[i]);
145 }
146 let mut r = q; for (i, s) in s_list.iter().enumerate() {
148 let beta = rho[i] * y_list[i].dot(&r);
149 let a = alpha[s_list.len() - 1 - i];
150 r = &r + &(s * (a - beta));
151 }
152 let p = -r;
153 let mut step = 1.0;
155 let fx = f(&x);
156 let mut x_new = &x + &(step * &p);
157 let mut f_new = f(&x_new);
158 while f_new > fx && step > 1e-8 {
159 step *= 0.5;
160 x_new = &x + &(step * &p);
161 f_new = f(&x_new);
162 }
163 let s = &x_new - &x;
164 let g_new = grad(&x_new);
165 let y = &g_new - &g;
166 let ys = y.dot(&s);
167 if ys.abs() < 1e-12 {
168 break;
169 }
170 if s_list.len() == m {
171 s_list.remove(0);
172 y_list.remove(0);
173 rho.remove(0);
174 }
175 s_list.push(s.clone());
176 y_list.push(y.clone());
177 rho.push(1.0 / ys);
178 x = x_new;
179 }
180 x
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use ndarray::{array, Array1};
187 use ndarray_npy::ReadNpyExt;
188 use scir_core::assert_close;
189 use std::{fs::File, path::PathBuf};
190
191 fn fixture_path(name: &str) -> Option<PathBuf> {
192 let base = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../fixtures");
193 let path = base.join(name);
194 if path.exists() {
195 Some(path)
196 } else {
197 None
198 }
199 }
200
201 fn fixture_array(name: &str, test_name: &str) -> Option<Array1<f64>> {
202 let Some(path) = fixture_path(name) else {
203 eprintln!("[scir-optimize] fixture {name} missing; skipping {test_name}");
204 return None;
205 };
206
207 let Ok(file) = File::open(&path) else {
208 eprintln!("[scir-optimize] fixture {name} could not be opened; skipping {test_name}");
209 return None;
210 };
211
212 match ReadNpyExt::read_npy(file) {
213 Ok(values) => Some(values),
214 Err(err) => {
215 eprintln!(
216 "[scir-optimize] fixture {name} read failed ({err}); skipping {test_name}"
217 );
218 None
219 }
220 }
221 }
222
223 fn rosenbrock(x: &Array1<f64>) -> f64 {
224 (1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0].powi(2)).powi(2)
225 }
226
227 fn rosenbrock_grad(x: &Array1<f64>) -> Array1<f64> {
228 array![
229 -2.0 * (1.0 - x[0]) - 400.0 * x[0] * (x[1] - x[0].powi(2)),
230 200.0 * (x[1] - x[0].powi(2))
231 ]
232 }
233
234 fn himmelblau(x: &Array1<f64>) -> f64 {
235 (x[0].powi(2) + x[1] - 11.0).powi(2) + (x[0] + x[1].powi(2) - 7.0).powi(2)
236 }
237
238 fn himmelblau_grad(x: &Array1<f64>) -> Array1<f64> {
239 array![
240 4.0 * x[0] * (x[0].powi(2) + x[1] - 11.0) + 2.0 * (x[0] + x[1].powi(2) - 7.0),
241 2.0 * (x[0].powi(2) + x[1] - 11.0) + 4.0 * x[1] * (x[0] + x[1].powi(2) - 7.0)
242 ]
243 }
244
245 #[test]
246 fn rosenbrock_nelder_mead_matches_fixture() {
247 let Some(expected) = fixture_array(
248 "rosenbrock_nelder.npy",
249 "rosenbrock_nelder_mead_matches_fixture",
250 ) else {
251 return;
252 };
253 let result = nelder_mead(rosenbrock, array![-1.2, 1.0], 1.0, 2000, 1e-8);
254 assert_close!(&result, &expected, array, atol = 1e-5, rtol = 1e-5);
255 }
256
257 #[test]
258 fn rosenbrock_bfgs_matches_fixture() {
259 let Some(expected) =
260 fixture_array("rosenbrock_bfgs.npy", "rosenbrock_bfgs_matches_fixture")
261 else {
262 return;
263 };
264 let result = bfgs(rosenbrock, rosenbrock_grad, array![-1.2, 1.0], 2000, 1e-8);
265 assert_close!(&result, &expected, array, atol = 1e-5, rtol = 1e-5);
266 }
267
268 #[test]
269 fn himmelblau_nelder_mead_matches_fixture() {
270 let Some(expected) = fixture_array(
271 "himmelblau_nelder.npy",
272 "himmelblau_nelder_mead_matches_fixture",
273 ) else {
274 return;
275 };
276 let result = nelder_mead(himmelblau, array![0.0, 0.0], 0.5, 2000, 1e-8);
277 assert_close!(&result, &expected, array, atol = 1e-4, rtol = 1e-5);
278 }
279
280 #[test]
281 fn himmelblau_bfgs_matches_fixture() {
282 let Some(expected) =
283 fixture_array("himmelblau_bfgs.npy", "himmelblau_bfgs_matches_fixture")
284 else {
285 return;
286 };
287 let result = bfgs(himmelblau, himmelblau_grad, array![0.0, 0.0], 2000, 1e-8);
288 assert_close!(&result, &expected, array, atol = 1e-5, rtol = 1e-5);
289 }
290
291 #[test]
292 fn rosenbrock_lbfgs_matches_fixture() {
293 let Some(expected) =
294 fixture_array("rosenbrock_lbfgs.npy", "rosenbrock_lbfgs_matches_fixture")
295 else {
296 return;
297 };
298 let result = lbfgs(
299 rosenbrock,
300 rosenbrock_grad,
301 array![-1.2, 1.0],
302 2000,
303 1e-8,
304 5,
305 );
306 assert_close!(&result, &expected, array, atol = 1e-5, rtol = 1e-5);
307 }
308
309 #[test]
310 fn himmelblau_lbfgs_matches_fixture() {
311 let Some(expected) =
312 fixture_array("himmelblau_lbfgs.npy", "himmelblau_lbfgs_matches_fixture")
313 else {
314 return;
315 };
316 let result = lbfgs(himmelblau, himmelblau_grad, array![0.0, 0.0], 2000, 1e-8, 5);
317 assert_close!(&result, &expected, array, atol = 1e-5, rtol = 1e-5);
318 }
319}