scirs2_optimize/least_squares/
bounded.rs1use crate::error::OptimizeResult;
44use crate::result::OptimizeResults;
45use crate::unconstrained::Bounds;
46use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Data, Ix1};
47
48#[derive(Debug, Clone)]
50pub struct BoundedOptions {
51 pub max_iter: usize,
53
54 pub max_nfev: Option<usize>,
56
57 pub xtol: f64,
59
60 pub ftol: f64,
62
63 pub gtol: f64,
65
66 pub initial_trust_radius: f64,
68
69 pub max_trust_radius: f64,
71
72 pub diff_step: f64,
74}
75
76impl Default for BoundedOptions {
77 fn default() -> Self {
78 BoundedOptions {
79 max_iter: 100,
80 max_nfev: None,
81 xtol: 1e-8,
82 ftol: 1e-8,
83 gtol: 1e-8,
84 initial_trust_radius: 1.0,
85 max_trust_radius: 1000.0,
86 diff_step: 1e-8,
87 }
88 }
89}
90
91#[allow(dead_code)]
105pub fn bounded_least_squares<F, J, D, S1, S2>(
106 residuals: F,
107 x0: &ArrayBase<S1, Ix1>,
108 bounds: Option<Bounds>,
109 jacobian: Option<J>,
110 data: &ArrayBase<S2, Ix1>,
111 options: Option<BoundedOptions>,
112) -> OptimizeResult<OptimizeResults<f64>>
113where
114 F: Fn(&[f64], &[D]) -> Array1<f64>,
115 J: Fn(&[f64], &[D]) -> Array2<f64>,
116 D: Clone,
117 S1: Data<Elem = f64>,
118 S2: Data<Elem = D>,
119{
120 let options = options.unwrap_or_default();
121
122 if bounds.is_none() {
124 }
127
128 trust_region_reflective(residuals, x0, bounds, jacobian, data, &options)
130}
131
132#[allow(dead_code)]
134fn trust_region_reflective<F, J, D, S1, S2>(
135 residuals: F,
136 x0: &ArrayBase<S1, Ix1>,
137 bounds: Option<Bounds>,
138 jacobian: Option<J>,
139 data: &ArrayBase<S2, Ix1>,
140 options: &BoundedOptions,
141) -> OptimizeResult<OptimizeResults<f64>>
142where
143 F: Fn(&[f64], &[D]) -> Array1<f64>,
144 J: Fn(&[f64], &[D]) -> Array2<f64>,
145 D: Clone,
146 S1: Data<Elem = f64>,
147 S2: Data<Elem = D>,
148{
149 let mut x = x0.to_owned();
150 let m = x.len();
151
152 if let Some(ref b) = bounds {
154 x = project_to_bounds(&x, b);
155 }
156
157 let max_nfev = options.max_nfev.unwrap_or(options.max_iter * m * 10);
158 let mut nfev = 0;
159 let mut njev = 0;
160 let mut iter = 0;
161
162 let mut trust_radius = options.initial_trust_radius;
163 let max_trust_radius = options.max_trust_radius;
164
165 let compute_numerical_jacobian =
167 |x_val: &Array1<f64>, res_val: &Array1<f64>| -> (Array2<f64>, usize) {
168 let eps = options.diff_step;
169 let n = res_val.len();
170 let mut jac = Array2::zeros((n, m));
171 let mut count = 0;
172
173 for j in 0..m {
174 let mut x_h = x_val.clone();
175 x_h[j] += eps;
176
177 if let Some(ref b) = bounds {
179 x_h = project_to_bounds(&x_h, b);
180 }
181
182 let res_h = residuals(x_h.as_slice().unwrap(), data.as_slice().unwrap());
183 count += 1;
184
185 for i in 0..n {
186 jac[[i, j]] = (res_h[i] - res_val[i]) / eps;
187 }
188 }
189
190 (jac, count)
191 };
192
193 while iter < options.max_iter && nfev < max_nfev {
195 let res = residuals(x.as_slice().unwrap(), data.as_slice().unwrap());
197 nfev += 1;
198 let _n = res.len();
199
200 let cost = 0.5 * res.iter().map(|&r| r * r).sum::<f64>();
202
203 let (jac, jac_evals) = match &jacobian {
205 Some(jac_fn) => {
206 let j = jac_fn(x.as_slice().unwrap(), data.as_slice().unwrap());
207 njev += 1;
208 (j, 0)
209 }
210 None => {
211 let (j, count) = compute_numerical_jacobian(&x, &res);
212 nfev += count;
213 (j, count)
214 }
215 };
216
217 let gradient = jac.t().dot(&res);
219
220 let proj_grad = compute_projected_gradient(&x, &gradient, &bounds);
222
223 if proj_grad.iter().all(|&g| g.abs() < options.gtol) {
225 let mut result = OptimizeResults::<f64>::default();
226 result.x = x;
227 result.fun = cost;
228 result.nfev = nfev;
229 result.njev = njev;
230 result.nit = iter;
231 result.success = true;
232 result.message = "Optimization terminated successfully.".to_string();
233 return Ok(result);
234 }
235
236 let step = solve_trust_region_bounds(&jac, &res, &gradient, trust_radius, &x, &bounds);
238
239 let step_norm = step.iter().map(|&s| s * s).sum::<f64>().sqrt();
241 if step_norm < options.xtol {
242 let mut result = OptimizeResults::<f64>::default();
243 result.x = x;
244 result.fun = cost;
245 result.nfev = nfev;
246 result.njev = njev;
247 result.nit = iter;
248 result.success = true;
249 result.message = "Converged (step size tolerance)".to_string();
250 return Ok(result);
251 }
252
253 let mut x_new = &x + &step;
255
256 if let Some(ref b) = bounds {
258 x_new = project_to_bounds(&x_new, b);
259 }
260
261 let res_new = residuals(x_new.as_slice().unwrap(), data.as_slice().unwrap());
263 nfev += 1;
264 let cost_new = 0.5 * res_new.iter().map(|&r| r * r).sum::<f64>();
265
266 let actual_reduction = cost - cost_new;
268 let predicted_reduction = compute_predicted_reduction(&jac, &res, &step);
269
270 let rho = if predicted_reduction.abs() > 1e-10 {
272 actual_reduction / predicted_reduction
273 } else {
274 0.0
275 };
276
277 if rho < 0.25 {
279 trust_radius *= 0.25;
280 } else if rho > 0.75 && step_norm >= 0.9 * trust_radius {
281 trust_radius = (2.0 * trust_radius).min(max_trust_radius);
282 }
283
284 if rho > 0.01 {
286 if actual_reduction.abs() < options.ftol * cost {
288 let mut result = OptimizeResults::<f64>::default();
289 result.x = x_new;
290 result.fun = cost_new;
291 result.nfev = nfev;
292 result.njev = njev;
293 result.nit = iter;
294 result.success = true;
295 result.message = "Converged (function tolerance)".to_string();
296 return Ok(result);
297 }
298
299 x = x_new;
300 }
301
302 iter += 1;
303 }
304
305 let res_final = residuals(x.as_slice().unwrap(), data.as_slice().unwrap());
307 let final_cost = 0.5 * res_final.iter().map(|&r| r * r).sum::<f64>();
308
309 let mut result = OptimizeResults::<f64>::default();
310 result.x = x;
311 result.fun = final_cost;
312 result.nfev = nfev;
313 result.njev = njev;
314 result.nit = iter;
315 result.success = false;
316 result.message = "Maximum iterations reached".to_string();
317
318 Ok(result)
319}
320
321#[allow(dead_code)]
323fn project_to_bounds(x: &Array1<f64>, bounds: &Bounds) -> Array1<f64> {
324 let mut x_proj = x.clone();
325
326 for i in 0..x.len() {
327 if let Some(lb) = bounds.lower[i] {
328 x_proj[i] = x_proj[i].max(lb);
329 }
330 if let Some(ub) = bounds.upper[i] {
331 x_proj[i] = x_proj[i].min(ub);
332 }
333 }
334
335 x_proj
336}
337
338#[allow(dead_code)]
340fn compute_projected_gradient(
341 x: &Array1<f64>,
342 gradient: &Array1<f64>,
343 bounds: &Option<Bounds>,
344) -> Array1<f64> {
345 let mut proj_grad = gradient.clone();
346
347 if let Some(b) = bounds {
348 for i in 0..x.len() {
349 if let Some(lb) = b.lower[i] {
351 if (x[i] - lb).abs() < 1e-10 && gradient[i] > 0.0 {
352 proj_grad[i] = 0.0;
353 }
354 }
355
356 if let Some(ub) = b.upper[i] {
358 if (x[i] - ub).abs() < 1e-10 && gradient[i] < 0.0 {
359 proj_grad[i] = 0.0;
360 }
361 }
362 }
363 }
364
365 proj_grad
366}
367
368#[allow(dead_code)]
370fn solve_trust_region_bounds(
371 jac: &Array2<f64>,
372 _res: &Array1<f64>,
373 gradient: &Array1<f64>,
374 trust_radius: f64,
375 x: &Array1<f64>,
376 bounds: &Option<Bounds>,
377) -> Array1<f64> {
378 let m = x.len();
379
380 let jt_j = jac.t().dot(jac);
382 let neg_gradient = -gradient;
383
384 let gn_step = if let Some(step) = solve(&jt_j, &neg_gradient) {
386 step
387 } else {
388 -gradient / gradient.iter().map(|&g| g * g).sum::<f64>().sqrt()
390 };
391
392 let mut step = gn_step;
394
395 if let Some(b) = bounds {
396 for i in 0..m {
397 if let Some(lb) = b.lower[i] {
399 let max_step_down = x[i] - lb;
400 if step[i] < -max_step_down {
401 step[i] = -max_step_down;
402 }
403 }
404
405 if let Some(ub) = b.upper[i] {
406 let max_step_up = ub - x[i];
407 if step[i] > max_step_up {
408 step[i] = max_step_up;
409 }
410 }
411 }
412 }
413
414 let step_norm = step.iter().map(|&s| s * s).sum::<f64>().sqrt();
416 if step_norm > trust_radius {
417 step *= trust_radius / step_norm;
418 }
419
420 step
421}
422
423#[allow(dead_code)]
425fn compute_predicted_reduction(jac: &Array2<f64>, res: &Array1<f64>, step: &Array1<f64>) -> f64 {
426 let jac_step = jac.dot(step);
427 let linear_term = res.dot(&jac_step);
428 let quadratic_term = 0.5 * jac_step.dot(&jac_step);
429
430 -(linear_term + quadratic_term)
431}
432
433#[allow(dead_code)]
435fn solve(a: &Array2<f64>, b: &Array1<f64>) -> Option<Array1<f64>> {
436 use scirs2_linalg::solve;
437
438 solve(&a.view(), &b.view(), None).ok()
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use scirs2_core::ndarray::array;
445
446 #[test]
447 fn test_bounded_least_squares_simple() {
448 fn residual(x: &[f64], data: &[f64]) -> Array1<f64> {
450 array![
451 x[0] + 2.0 * x[1] - 2.0,
452 x[0] - x[1] - 1.0,
453 x[0] + x[1] - 1.5
454 ]
455 }
456
457 let x0 = array![0.0, 0.0];
458
459 let bounds = Bounds::new(&[(Some(0.0), Some(2.0)), (Some(-1.0), Some(1.0))]);
461
462 let result = bounded_least_squares(
463 residual,
464 &x0,
465 Some(bounds),
466 None::<fn(&[f64], &[f64]) -> Array2<f64>>,
467 &array![],
468 None,
469 )
470 .unwrap();
471
472 assert!(result.success);
473 assert!(result.x[0] >= 0.0 && result.x[0] <= 2.0);
475 assert!(result.x[1] >= -1.0 && result.x[1] <= 1.0);
476 }
477
478 #[test]
479 fn test_bounded_vs_unbounded() {
480 fn residual(x: &[f64], data: &[f64]) -> Array1<f64> {
482 array![
483 x[0] - 5.0, x[1] - 3.0 ]
486 }
487
488 let x0 = array![0.0, 0.0];
489
490 let bounds = Bounds::new(&[(Some(0.0), Some(1.0)), (Some(0.0), Some(1.0))]);
492
493 let result = bounded_least_squares(
494 residual,
495 &x0,
496 Some(bounds),
497 None::<fn(&[f64], &[f64]) -> Array2<f64>>,
498 &array![],
499 None,
500 )
501 .unwrap();
502
503 assert!(result.success);
504 assert!((result.x[0] - 1.0).abs() < 1e-6);
506 assert!((result.x[1] - 1.0).abs() < 1e-6);
507 }
508
509 #[test]
510 fn test_project_to_bounds() {
511 let x = array![2.5, -0.5, 0.5];
512 let bounds = Bounds::new(&[
513 (Some(0.0), Some(2.0)),
514 (Some(-1.0), Some(1.0)),
515 (None, None),
516 ]);
517
518 let x_proj = project_to_bounds(&x, &bounds);
519
520 assert_eq!(x_proj[0], 2.0); assert_eq!(x_proj[1], -0.5); assert_eq!(x_proj[2], 0.5); }
524}