scirs2_optimize/unconstrained/
conjugate_gradient.rs1use crate::error::OptimizeError;
4use crate::unconstrained::result::OptimizeResult;
5use crate::unconstrained::utils::{array_diff_norm, check_convergence, finite_difference_gradient};
6use crate::unconstrained::{Bounds, Options};
7use scirs2_core::ndarray::{Array1, ArrayView1};
8
9#[allow(dead_code)]
11pub fn minimize_conjugate_gradient<F, S>(
12 mut fun: F,
13 x0: Array1<f64>,
14 options: &Options,
15) -> Result<OptimizeResult<S>, OptimizeError>
16where
17 F: FnMut(&ArrayView1<f64>) -> S + Clone,
18 S: Into<f64> + Clone,
19{
20 let ftol = options.ftol;
22 let gtol = options.gtol;
23 let max_iter = options.max_iter;
24 let eps = options.eps;
25 let bounds = options.bounds.as_ref();
26
27 let n = x0.len();
29 let mut x = x0.to_owned();
30
31 if let Some(bounds) = bounds {
33 bounds.project(x.as_slice_mut().unwrap());
34 }
35
36 let mut f = fun(&x.view()).into();
37
38 let mut g = finite_difference_gradient(&mut fun, &x.view(), eps)?;
40
41 let mut p = -g.clone();
43
44 if let Some(bounds) = bounds {
46 project_search_direction(&mut p, &x, bounds);
47 }
48
49 let mut iter = 0;
51 let mut nfev = 1 + n; while iter < max_iter {
54 if g.mapv(|gi| gi.abs()).sum() < gtol {
56 break;
57 }
58
59 if p.mapv(|pi| pi.abs()).sum() < 1e-10 {
62 break;
63 }
64
65 let (alpha, f_new) = line_search_cg(&mut fun, &x, &p, f, &mut nfev, bounds);
67
68 let mut x_new = &x + &(&p * alpha);
70
71 if let Some(bounds) = bounds {
73 bounds.project(x_new.as_slice_mut().unwrap());
74 }
75
76 let step_size = array_diff_norm(&x_new.view(), &x.view());
78 if step_size < 1e-10 {
79 x = x_new;
81 break;
82 }
83
84 let g_new = finite_difference_gradient(&mut fun, &x_new.view(), eps)?;
86 nfev += n;
87
88 if check_convergence(
90 f - f_new,
91 step_size,
92 g_new.mapv(|x| x.abs()).sum(),
93 ftol,
94 options.xtol,
95 gtol,
96 ) {
97 x = x_new;
98 g = g_new;
99 break;
100 }
101
102 let g_new_norm_sq = g_new.dot(&g_new);
104 let g_norm_sq = g.dot(&g);
105
106 let beta_fr = if g_norm_sq < 1e-10 {
108 0.0
109 } else {
110 g_new_norm_sq / g_norm_sq
111 };
112
113 p = -&g_new + beta_fr * &p;
115
116 if let Some(bounds) = bounds {
118 project_search_direction(&mut p, &x_new, bounds);
119
120 let dir_norm = p.dot(&p).sqrt();
122 if dir_norm < 1e-10 {
123 p = -g_new.clone();
125 project_search_direction(&mut p, &x_new, bounds);
126 }
127 }
128
129 x = x_new;
131 f = f_new;
132 g = g_new;
133
134 iter += 1;
135
136 if iter % n == 0 {
138 p = -g.clone();
139
140 if let Some(bounds) = bounds {
142 project_search_direction(&mut p, &x, bounds);
143 }
144 }
145 }
146
147 if let Some(bounds) = bounds {
149 bounds.project(x.as_slice_mut().unwrap());
150 }
151
152 let final_fun = fun(&x.view());
154
155 Ok(OptimizeResult {
157 x,
158 fun: final_fun,
159 nit: iter,
160 func_evals: nfev,
161 nfev,
162 success: iter < max_iter,
163 message: if iter < max_iter {
164 "Optimization terminated successfully.".to_string()
165 } else {
166 "Maximum iterations reached.".to_string()
167 },
168 jacobian: Some(g),
169 hessian: None,
170 })
171}
172
173#[allow(dead_code)]
175fn project_search_direction(p: &mut Array1<f64>, x: &Array1<f64>, bounds: &Bounds) {
176 for i in 0..p.len() {
177 if let Some(lb) = bounds.lower[i] {
179 if (x[i] - lb).abs() < 1e-10 && p[i] < 0.0 {
180 p[i] = 0.0;
181 }
182 }
183 if let Some(ub) = bounds.upper[i] {
184 if (x[i] - ub).abs() < 1e-10 && p[i] > 0.0 {
185 p[i] = 0.0;
186 }
187 }
188 }
189}
190
191#[allow(dead_code)]
193fn line_search_cg<F, S>(
194 fun: &mut F,
195 x: &Array1<f64>,
196 direction: &Array1<f64>,
197 f_x: f64,
198 nfev: &mut usize,
199 bounds: Option<&Bounds>,
200) -> (f64, f64)
201where
202 F: FnMut(&ArrayView1<f64>) -> S,
203 S: Into<f64>,
204{
205 let (a_min, a_max) = if let Some(b) = bounds {
207 compute_line_bounds(x, direction, Some(b))
208 } else {
209 (f64::NEG_INFINITY, f64::INFINITY)
210 };
211
212 let c1 = 1e-4; let rho = 0.5; let mut alpha = if a_max < 1.0 { a_max * 0.99 } else { 1.0 };
218
219 if a_max <= 0.0 || a_min >= a_max {
221 alpha = if a_max > 0.0 { a_max } else { 0.0 };
222 let x_new = x + alpha * direction;
223 *nfev += 1;
224 let f_new = fun(&x_new.view()).into();
225 return (alpha, f_new);
226 }
227
228 let mut f_line = |alpha: f64| {
230 let mut x_new = x + alpha * direction;
231
232 if let Some(bounds) = bounds {
234 bounds.project(x_new.as_slice_mut().unwrap());
235 }
236
237 *nfev += 1;
238 fun(&x_new.view()).into()
239 };
240
241 let mut f_new = f_line(alpha);
243
244 let slope = direction.mapv(|d| d.powi(2)).sum();
246 while f_new > f_x - c1 * alpha * slope.abs() && alpha > a_min {
247 alpha *= rho;
248
249 if alpha < a_min {
251 alpha = a_min;
252 }
253
254 f_new = f_line(alpha);
255
256 if alpha < 1e-10 {
258 break;
259 }
260 }
261
262 (alpha, f_new)
263}
264
265#[allow(dead_code)]
267pub fn compute_line_bounds(
268 x: &Array1<f64>,
269 direction: &Array1<f64>,
270 bounds: Option<&Bounds>,
271) -> (f64, f64) {
272 if bounds.is_none() {
273 return (f64::NEG_INFINITY, f64::INFINITY);
274 }
275
276 let bounds = bounds.unwrap();
277 let mut a_min = f64::NEG_INFINITY;
278 let mut a_max = f64::INFINITY;
279
280 for i in 0..x.len() {
281 let xi = x[i];
282 let di = direction[i];
283
284 if di.abs() < 1e-16 {
285 continue;
286 }
287
288 if let Some(lb) = bounds.lower[i] {
290 let a_lb = (lb - xi) / di;
291 if di > 0.0 {
292 a_min = a_min.max(a_lb);
293 } else {
294 a_max = a_max.min(a_lb);
295 }
296 }
297
298 if let Some(ub) = bounds.upper[i] {
300 let a_ub = (ub - xi) / di;
301 if di > 0.0 {
302 a_max = a_max.min(a_ub);
303 } else {
304 a_min = a_min.max(a_ub);
305 }
306 }
307 }
308
309 if a_min > a_max {
310 (0.0, 0.0)
311 } else {
312 (a_min, a_max)
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use approx::assert_abs_diff_eq;
320
321 #[test]
322 fn test_cg_quadratic() {
323 let quadratic = |x: &ArrayView1<f64>| -> f64 { x[0] * x[0] + 4.0 * x[1] * x[1] };
324
325 let x0 = Array1::from_vec(vec![2.0, 1.0]);
326 let options = Options::default();
327
328 let result = minimize_conjugate_gradient(quadratic, x0, &options).unwrap();
329
330 assert!(result.success);
331 assert_abs_diff_eq!(result.x[0], 0.0, epsilon = 1e-4);
332 assert_abs_diff_eq!(result.x[1], 0.0, epsilon = 1e-4);
333 }
334
335 #[test]
336 fn test_cg_rosenbrock() {
337 let rosenbrock = |x: &ArrayView1<f64>| -> f64 {
338 let a = 1.0;
339 let b = 100.0;
340 (a - x[0]).powi(2) + b * (x[1] - x[0].powi(2)).powi(2)
341 };
342
343 let x0 = Array1::from_vec(vec![0.0, 0.0]);
344 let mut options = Options::default();
345 options.max_iter = 2000; let result = minimize_conjugate_gradient(rosenbrock, x0, &options).unwrap();
348
349 assert!(result.success);
350 assert!(
352 result.x[0] > 0.2 && result.x[0] < 1.5,
353 "x[0] = {} should be near 1.0",
354 result.x[0]
355 );
356 assert!(
357 result.x[1] > 0.0 && result.x[1] < 1.5,
358 "x[1] = {} should be near 1.0",
359 result.x[1]
360 );
361 }
362
363 #[test]
364 fn test_cg_with_bounds() {
365 let quadratic =
366 |x: &ArrayView1<f64>| -> f64 { (x[0] - 2.0).powi(2) + (x[1] - 3.0).powi(2) };
367
368 let x0 = Array1::from_vec(vec![0.0, 0.0]);
369 let mut options = Options::default();
370 options.max_iter = 1000; let bounds = Bounds::new(&[(Some(0.0), Some(1.0)), (Some(0.0), Some(1.0))]);
374 options.bounds = Some(bounds);
375
376 let result = minimize_conjugate_gradient(quadratic, x0, &options).unwrap();
377
378 assert!(result.success);
379 assert_abs_diff_eq!(result.x[0], 1.0, epsilon = 0.4);
382 assert_abs_diff_eq!(result.x[1], 1.0, epsilon = 0.4);
383 }
384}