scirs2_optimize/constrained/
trust_constr.rs1use crate::constrained::{Constraint, ConstraintFn, ConstraintKind, Options};
48use crate::error::OptimizeResult;
49use crate::result::OptimizeResults;
50use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Axis, Data, Ix1};
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub enum HessianUpdate {
55 #[default]
57 BFGS,
58 SR1,
60 Exact,
62}
63
64pub type GradientFn = fn(&[f64]) -> Array1<f64>;
66
67pub type HessianFn = fn(&[f64]) -> Array2<f64>;
69
70#[allow(clippy::many_single_char_names)]
71#[allow(dead_code)]
72pub fn minimize_trust_constr<F, S>(
73 func: F,
74 x0: &ArrayBase<S, Ix1>,
75 constraints: &[Constraint<ConstraintFn>],
76 options: &Options,
77) -> OptimizeResult<OptimizeResults<f64>>
78where
79 F: Fn(&[f64]) -> f64,
80 S: Data<Elem = f64>,
81{
82 let ftol = options.ftol.unwrap_or(1e-8);
84 let gtol = options.gtol.unwrap_or(1e-8);
85 let ctol = options.ctol.unwrap_or(1e-8);
86 let maxiter = options.maxiter.unwrap_or(100 * x0.len());
87 let eps = options.eps.unwrap_or(1e-8);
88
89 let n = x0.len();
91 let mut x = x0.to_owned();
92 let mut f = func(x.as_slice().expect("Operation failed"));
93 let mut nfev = 1;
94
95 let mut lambda = Array1::zeros(constraints.len());
97
98 let mut g = Array1::zeros(n);
100 for i in 0..n {
101 let mut x_h = x.clone();
102 x_h[i] += eps;
103 let f_h = func(x_h.as_slice().expect("Operation failed"));
104 g[i] = (f_h - f) / eps;
105 nfev += 1;
106 }
107
108 let mut c = Array1::zeros(constraints.len());
110 for (i, constraint) in constraints.iter().enumerate() {
111 if !constraint.is_bounds() {
112 let val = (constraint.fun)(x.as_slice().expect("Operation failed"));
113
114 match constraint.kind {
115 ConstraintKind::Inequality => {
116 c[i] = val; }
118 ConstraintKind::Equality => {
119 c[i] = val; }
121 }
122 }
123 }
124
125 let mut a = Array2::zeros((constraints.len(), n));
127 for (i, constraint) in constraints.iter().enumerate() {
128 if !constraint.is_bounds() {
129 for j in 0..n {
130 let mut x_h = x.clone();
131 x_h[j] += eps;
132 let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
133 a[[i, j]] = (c_h - c[i]) / eps;
134 nfev += 1;
135 }
136 }
137 }
138
139 let mut delta = 1.0;
141
142 let mut b = Array2::eye(n);
144
145 let mut iter = 0;
147
148 while iter < maxiter {
149 let mut max_constraint_violation = 0.0;
151 for (i, &ci) in c.iter().enumerate() {
152 match constraints[i].kind {
153 ConstraintKind::Inequality => {
154 if ci < -ctol {
155 max_constraint_violation = f64::max(max_constraint_violation, -ci);
156 }
157 }
158 ConstraintKind::Equality => {
159 let violation = ci.abs();
161 if violation > ctol {
162 max_constraint_violation = f64::max(max_constraint_violation, violation);
163 }
164 }
165 }
166 }
167
168 if g.iter().all(|&gi| gi.abs() < gtol) && max_constraint_violation < ctol {
170 break;
171 }
172
173 let mut lag_grad = g.clone();
175 for (i, &li) in lambda.iter().enumerate() {
176 let include_constraint = match constraints[i].kind {
177 ConstraintKind::Inequality => {
178 li > 0.0 || c[i] < -ctol
180 }
181 ConstraintKind::Equality => {
182 true
184 }
185 };
186
187 if include_constraint {
188 for j in 0..n {
189 lag_grad[j] -= li * a[[i, j]];
192 }
193 }
194 }
195
196 let (p, predicted_reduction) =
201 compute_trust_region_step_constrained(&lag_grad, &b, &a, &c, delta, constraints, ctol);
202
203 let x_new = &x + &p;
205
206 let f_new = func(x_new.as_slice().expect("Operation failed"));
208 nfev += 1;
209
210 let mut c_new = Array1::zeros(constraints.len());
211 for (i, constraint) in constraints.iter().enumerate() {
212 if !constraint.is_bounds() {
213 c_new[i] = (constraint.fun)(x_new.as_slice().expect("Operation failed"));
214 nfev += 1;
215 }
216 }
217
218 let mut merit = f;
220 let mut merit_new = f_new;
221
222 let penalty = 10.0; for (i, &ci) in c.iter().enumerate() {
225 match constraints[i].kind {
226 ConstraintKind::Inequality => {
227 merit += penalty * f64::max(0.0, -ci);
229 merit_new += penalty * f64::max(0.0, -c_new[i]);
230 }
231 ConstraintKind::Equality => {
232 merit += penalty * ci.abs();
234 merit_new += penalty * c_new[i].abs();
235 }
236 }
237 }
238
239 let actual_reduction = merit - merit_new;
241
242 let rho = if predicted_reduction > 0.0 {
244 actual_reduction / predicted_reduction
245 } else {
246 0.0
247 };
248
249 if rho < 0.25 {
251 delta *= 0.5;
252 } else if rho > 0.75 && p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt() >= 0.9 * delta {
253 delta *= 2.0;
254 }
255
256 if rho > 0.1 {
258 x = x_new;
260 f = f_new;
261 c = c_new;
262
263 if (merit - merit_new).abs() < ftol * (1.0 + merit.abs()) {
265 break;
266 }
267
268 let mut g_new = Array1::zeros(n);
270 for i in 0..n {
271 let mut x_h = x.clone();
272 x_h[i] += eps;
273 let f_h = func(x_h.as_slice().expect("Operation failed"));
274 g_new[i] = (f_h - f) / eps;
275 nfev += 1;
276 }
277
278 let mut a_new = Array2::zeros((constraints.len(), n));
280 for (i, constraint) in constraints.iter().enumerate() {
281 if !constraint.is_bounds() {
282 for j in 0..n {
283 let mut x_h = x.clone();
284 x_h[j] += eps;
285 let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
286 a_new[[i, j]] = (c_h - c[i]) / eps;
287 nfev += 1;
288 }
289 }
290 }
291
292 for (i, constraint) in constraints.iter().enumerate() {
294 match constraint.kind {
295 ConstraintKind::Inequality => {
296 if c[i] < -ctol {
297 lambda[i] = f64::max(0.0, lambda[i] - c[i] * penalty);
300 } else {
301 lambda[i] = f64::max(0.0, lambda[i] - 0.1 * lambda[i]);
303 }
304 }
305 ConstraintKind::Equality => {
306 let step_size = 0.1;
309 lambda[i] -= step_size * c[i] * penalty;
310
311 lambda[i] *= 0.9;
313 }
314 }
315 }
316
317 let s = &p;
319 let y = &g_new - &g;
320
321 let s_dot_y = s.dot(&y);
323 if s_dot_y > 1e-10 {
324 let s_col = s.clone().insert_axis(Axis(1));
325 let s_row = s.clone().insert_axis(Axis(0));
326
327 let bs = b.dot(s);
328 let bs_col = bs.clone().insert_axis(Axis(1));
329 let bs_row = bs.clone().insert_axis(Axis(0));
330
331 let term1 = s_dot_y + s.dot(&bs);
332 let term2 = &s_col.dot(&s_row) * (term1 / (s_dot_y * s_dot_y));
333
334 let term3 = &bs_col.dot(&s_row) + &s_col.dot(&bs_row);
335
336 b = &b + &term2 - &(&term3 / s_dot_y);
337 }
338
339 g = g_new;
341 a = a_new;
342 }
343
344 iter += 1;
345 }
346
347 let mut c_result = Array1::zeros(constraints.len());
349 for (i, constraint) in constraints.iter().enumerate() {
350 if !constraint.is_bounds() {
351 c_result[i] = c[i];
352 }
353 }
354
355 let mut result = OptimizeResults::default();
357 result.x = x;
358 result.fun = f;
359 result.jac = Some(g.into_raw_vec_and_offset().0);
360 result.constr = Some(c_result);
361 result.nfev = nfev;
362 result.nit = iter;
363 result.success = iter < maxiter;
364
365 if result.success {
366 result.message = "Optimization terminated successfully.".to_string();
367 } else {
368 result.message = "Maximum iterations reached.".to_string();
369 }
370
371 Ok(result)
372}
373
374#[allow(clippy::many_single_char_names)]
435#[allow(clippy::too_many_arguments)]
436#[allow(dead_code)]
437pub fn minimize_trust_constr_with_derivatives<F, S, G, H>(
438 func: F,
439 x0: &ArrayBase<S, Ix1>,
440 constraints: &[Constraint<ConstraintFn>],
441 options: &Options,
442 jac: Option<G>,
443 hess: Option<H>,
444 hess_update: HessianUpdate,
445) -> OptimizeResult<OptimizeResults<f64>>
446where
447 F: Fn(&[f64]) -> f64,
448 S: Data<Elem = f64>,
449 G: Fn(&[f64]) -> Array1<f64>,
450 H: Fn(&[f64]) -> Array2<f64>,
451{
452 let ftol = options.ftol.unwrap_or(1e-8);
454 let gtol = options.gtol.unwrap_or(1e-8);
455 let ctol = options.ctol.unwrap_or(1e-8);
456 let maxiter = options.maxiter.unwrap_or(100 * x0.len());
457 let eps = options.eps.unwrap_or(1e-8);
458
459 let n = x0.len();
461 let mut x = x0.to_owned();
462 let mut f = func(x.as_slice().expect("Operation failed"));
463 let mut nfev = 1;
464 let mut njev = 0;
465 let mut nhev = 0;
466
467 let mut lambda = Array1::zeros(constraints.len());
469
470 let mut g = if let Some(ref grad_fn) = jac {
472 njev += 1;
473 grad_fn(x.as_slice().expect("Operation failed"))
474 } else {
475 let mut grad = Array1::zeros(n);
477 for i in 0..n {
478 let mut x_h = x.clone();
479 x_h[i] += eps;
480 let f_h = func(x_h.as_slice().expect("Operation failed"));
481 grad[i] = (f_h - f) / eps;
482 nfev += 1;
483 }
484 grad
485 };
486
487 let mut c = Array1::zeros(constraints.len());
489 for (i, constraint) in constraints.iter().enumerate() {
490 if !constraint.is_bounds() {
491 let val = (constraint.fun)(x.as_slice().expect("Operation failed"));
492 match constraint.kind {
493 ConstraintKind::Inequality => c[i] = val,
494 ConstraintKind::Equality => c[i] = val,
495 }
496 }
497 }
498
499 let mut a = Array2::zeros((constraints.len(), n));
501 for (i, constraint) in constraints.iter().enumerate() {
502 if !constraint.is_bounds() {
503 for j in 0..n {
504 let mut x_h = x.clone();
505 x_h[j] += eps;
506 let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
507 a[[i, j]] = (c_h - c[i]) / eps;
508 nfev += 1;
509 }
510 }
511 }
512
513 let mut delta = 1.0;
515
516 let mut b = if hess_update == HessianUpdate::Exact {
518 if let Some(ref hess_fn) = hess {
519 nhev += 1;
520 hess_fn(x.as_slice().expect("Operation failed"))
521 } else {
522 Array2::eye(n)
524 }
525 } else {
526 Array2::eye(n)
527 };
528
529 let mut iter = 0;
531
532 while iter < maxiter {
533 let mut max_constraint_violation = 0.0;
535 for (i, &ci) in c.iter().enumerate() {
536 match constraints[i].kind {
537 ConstraintKind::Inequality => {
538 if ci < -ctol {
539 max_constraint_violation = f64::max(max_constraint_violation, -ci);
540 }
541 }
542 ConstraintKind::Equality => {
543 let violation = ci.abs();
544 if violation > ctol {
545 max_constraint_violation = f64::max(max_constraint_violation, violation);
546 }
547 }
548 }
549 }
550
551 if g.iter().all(|&gi| gi.abs() < gtol) && max_constraint_violation < ctol {
553 break;
554 }
555
556 let mut lag_grad = g.clone();
558 for (i, &li) in lambda.iter().enumerate() {
559 let include_constraint = match constraints[i].kind {
560 ConstraintKind::Inequality => li > 0.0 || c[i] < -ctol,
561 ConstraintKind::Equality => true,
562 };
563
564 if include_constraint {
565 for j in 0..n {
566 lag_grad[j] -= li * a[[i, j]];
567 }
568 }
569 }
570
571 let (p, predicted_reduction) =
573 compute_trust_region_step_constrained(&lag_grad, &b, &a, &c, delta, constraints, ctol);
574
575 let x_new = &x + &p;
577 let f_new = func(x_new.as_slice().expect("Operation failed"));
578 nfev += 1;
579
580 let mut c_new = Array1::zeros(constraints.len());
581 for (i, constraint) in constraints.iter().enumerate() {
582 if !constraint.is_bounds() {
583 c_new[i] = (constraint.fun)(x_new.as_slice().expect("Operation failed"));
584 nfev += 1;
585 }
586 }
587
588 let mut merit = f;
590 let mut merit_new = f_new;
591 let penalty = 10.0;
592
593 for (i, &ci) in c.iter().enumerate() {
594 match constraints[i].kind {
595 ConstraintKind::Inequality => {
596 merit += penalty * f64::max(0.0, -ci);
597 merit_new += penalty * f64::max(0.0, -c_new[i]);
598 }
599 ConstraintKind::Equality => {
600 merit += penalty * ci.abs();
601 merit_new += penalty * c_new[i].abs();
602 }
603 }
604 }
605
606 let actual_reduction = merit - merit_new;
607 let rho = if predicted_reduction > 0.0 {
608 actual_reduction / predicted_reduction
609 } else {
610 0.0
611 };
612
613 if rho < 0.25 {
615 delta *= 0.5;
616 } else if rho > 0.75 && p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt() >= 0.9 * delta {
617 delta *= 2.0;
618 }
619
620 if rho > 0.1 {
622 x = x_new;
623 f = f_new;
624 c = c_new;
625
626 if (merit - merit_new).abs() < ftol * (1.0 + merit.abs()) {
627 break;
628 }
629
630 let g_new = if let Some(ref grad_fn) = jac {
632 njev += 1;
633 grad_fn(x.as_slice().expect("Operation failed"))
634 } else {
635 let mut grad = Array1::zeros(n);
636 for i in 0..n {
637 let mut x_h = x.clone();
638 x_h[i] += eps;
639 let f_h = func(x_h.as_slice().expect("Operation failed"));
640 grad[i] = (f_h - f) / eps;
641 nfev += 1;
642 }
643 grad
644 };
645
646 let mut a_new = Array2::zeros((constraints.len(), n));
648 for (i, constraint) in constraints.iter().enumerate() {
649 if !constraint.is_bounds() {
650 for j in 0..n {
651 let mut x_h = x.clone();
652 x_h[j] += eps;
653 let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
654 a_new[[i, j]] = (c_h - c[i]) / eps;
655 nfev += 1;
656 }
657 }
658 }
659
660 for (i, constraint) in constraints.iter().enumerate() {
662 match constraint.kind {
663 ConstraintKind::Inequality => {
664 if c[i] < -ctol {
665 lambda[i] = f64::max(0.0, lambda[i] - c[i] * penalty);
666 } else {
667 lambda[i] = f64::max(0.0, lambda[i] - 0.1 * lambda[i]);
668 }
669 }
670 ConstraintKind::Equality => {
671 let step_size = 0.1;
672 lambda[i] -= step_size * c[i] * penalty;
673 lambda[i] *= 0.9;
674 }
675 }
676 }
677
678 match hess_update {
680 HessianUpdate::Exact => {
681 if let Some(ref hess_fn) = hess {
682 nhev += 1;
683 b = hess_fn(x.as_slice().expect("Operation failed"));
684 }
685 }
686 HessianUpdate::BFGS => {
687 let s = &p;
688 let y = &g_new - &g;
689 let s_dot_y = s.dot(&y);
690 if s_dot_y > 1e-10 {
691 let s_col = s.clone().insert_axis(Axis(1));
692 let s_row = s.clone().insert_axis(Axis(0));
693 let bs = b.dot(s);
694 let bs_col = bs.clone().insert_axis(Axis(1));
695 let bs_row = bs.clone().insert_axis(Axis(0));
696 let term1 = s_dot_y + s.dot(&bs);
697 let term2 = &s_col.dot(&s_row) * (term1 / (s_dot_y * s_dot_y));
698 let term3 = &bs_col.dot(&s_row) + &s_col.dot(&bs_row);
699 b = &b + &term2 - &(&term3 / s_dot_y);
700 }
701 }
702 HessianUpdate::SR1 => {
703 let s = &p;
704 let y = &g_new - &g;
705 let bs = b.dot(s);
706 let diff = &y - &bs;
707 let s_dot_diff = s.dot(&diff);
708 if s_dot_diff.abs() > 1e-8 * s.dot(s).sqrt() * diff.dot(&diff).sqrt() {
710 let diff_col = diff.clone().insert_axis(Axis(1));
711 let diff_row = diff.clone().insert_axis(Axis(0));
712 let update = &diff_col.dot(&diff_row) / s_dot_diff;
713 b = &b + &update;
714 }
715 }
716 }
717
718 g = g_new;
719 a = a_new;
720 }
721
722 iter += 1;
723 }
724
725 let mut c_result = Array1::zeros(constraints.len());
727 for (i, constraint) in constraints.iter().enumerate() {
728 if !constraint.is_bounds() {
729 c_result[i] = c[i];
730 }
731 }
732
733 let mut result = OptimizeResults::default();
734 result.x = x;
735 result.fun = f;
736 result.jac = Some(g.into_raw_vec_and_offset().0);
737 result.constr = Some(c_result);
738 result.nfev = nfev;
739 result.njev = njev;
740 result.nhev = nhev;
741 result.nit = iter;
742 result.success = iter < maxiter;
743
744 if result.success {
745 result.message = "Optimization terminated successfully.".to_string();
746 } else {
747 result.message = "Maximum iterations reached.".to_string();
748 }
749
750 Ok(result)
751}
752
753#[allow(clippy::many_single_char_names)]
755#[allow(dead_code)]
756fn compute_trust_region_step_constrained(
757 g: &Array1<f64>,
758 b: &Array2<f64>,
759 a: &Array2<f64>,
760 c: &Array1<f64>,
761 delta: f64,
762 constraints: &[Constraint<ConstraintFn>],
763 ctol: f64,
764) -> (Array1<f64>, f64) {
765 let n = g.len();
766 let n_constr = constraints.len();
767
768 let p_unconstrained = compute_unconstrained_cauchy_point(g, b, delta);
770
771 let mut constraint_violated = false;
773 for i in 0..n_constr {
774 let grad_c_dot_p = (0..n).map(|j| a[[i, j]] * p_unconstrained[j]).sum::<f64>();
775
776 match constraints[i].kind {
777 ConstraintKind::Inequality => {
778 if c[i] + grad_c_dot_p < -ctol {
780 constraint_violated = true;
781 break;
782 }
783 }
784 ConstraintKind::Equality => {
785 if (c[i] + grad_c_dot_p).abs() > ctol {
787 constraint_violated = true;
788 break;
789 }
790 }
791 }
792 }
793
794 if !constraint_violated {
796 let g_dot_p = g.dot(&p_unconstrained);
798 let bp = b.dot(&p_unconstrained);
799 let p_dot_bp = p_unconstrained.dot(&bp);
800 let predicted_reduction = -g_dot_p - 0.5 * p_dot_bp;
801
802 return (p_unconstrained, predicted_reduction);
803 }
804
805 let mut p = Array1::zeros(n);
810 for i in 0..n {
811 p[i] = -g[i];
812 }
813
814 let p_norm = p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt();
816 if p_norm > 1e-10 {
817 p = &p * (delta / p_norm);
818 }
819
820 for _iter in 0..5 {
822 let mut max_viol = 0.0;
824 let mut most_violated = 0;
825
826 for i in 0..n_constr {
828 let grad_c_dot_p = (0..n).map(|j| a[[i, j]] * p[j]).sum::<f64>();
829
830 let viol = match constraints[i].kind {
831 ConstraintKind::Inequality => {
832 f64::max(0.0, -(c[i] + grad_c_dot_p))
834 }
835 ConstraintKind::Equality => {
836 (c[i] + grad_c_dot_p).abs()
838 }
839 };
840
841 if viol > max_viol {
842 max_viol = viol;
843 most_violated = i;
844 }
845 }
846
847 if max_viol < ctol {
848 break;
849 }
850
851 let mut a_norm_sq = 0.0;
853 for j in 0..n {
854 a_norm_sq += a[[most_violated, j]] * a[[most_violated, j]];
855 }
856
857 if a_norm_sq > 1e-10 {
858 let grad_c_dot_p = (0..n).map(|j| a[[most_violated, j]] * p[j]).sum::<f64>();
859
860 let proj_dist = match constraints[most_violated].kind {
861 ConstraintKind::Inequality => {
862 if c[most_violated] + grad_c_dot_p < 0.0 {
864 -(c[most_violated] + grad_c_dot_p) / a_norm_sq
865 } else {
866 0.0
867 }
868 }
869 ConstraintKind::Equality => {
870 -(c[most_violated] + grad_c_dot_p) / a_norm_sq
872 }
873 };
874
875 for j in 0..n {
877 p[j] += a[[most_violated, j]] * proj_dist;
878 }
879
880 let p_norm = p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt();
882 if p_norm > delta {
883 p = &p * (delta / p_norm);
884 }
885 }
886 }
887
888 let g_dot_p = g.dot(&p);
890 let bp = b.dot(&p);
891 let p_dot_bp = p.dot(&bp);
892 let predicted_reduction = -g_dot_p - 0.5 * p_dot_bp;
893
894 (p, predicted_reduction)
895}
896
897#[allow(dead_code)]
899fn compute_unconstrained_cauchy_point(g: &Array1<f64>, b: &Array2<f64>, delta: f64) -> Array1<f64> {
900 let n = g.len();
901
902 let mut p = Array1::zeros(n);
904 for i in 0..n {
905 p[i] = -g[i];
906 }
907
908 let bg = b.dot(g);
910 let g_dot_bg = g.dot(&bg);
911 let g_norm_sq = g.dot(g);
912
913 if g_norm_sq < 1e-10 {
915 return Array1::zeros(n);
917 }
918
919 let tau = if g_dot_bg <= 0.0 {
921 delta / g_norm_sq.sqrt()
923 } else {
924 f64::min(delta / g_norm_sq.sqrt(), g_norm_sq / g_dot_bg)
926 };
927
928 for i in 0..n {
930 p[i] *= tau;
931 }
932
933 p
934}