1use crate::error::{OptimizeError, OptimizeResult};
11
12#[derive(Debug, Clone)]
18pub struct TrustConstrResult {
19 pub x: Vec<f64>,
21 pub fun: f64,
23 pub grad: Vec<f64>,
25 pub constraint_violation: f64,
27 pub lambda_eq: Vec<f64>,
29 pub lambda_ineq: Vec<f64>,
31 pub nit: usize,
33 pub nfev: usize,
35 pub njev: usize,
37 pub trust_radius: f64,
39 pub success: bool,
41 pub message: String,
43}
44
45#[derive(Debug, Clone)]
51pub struct TrustConstrOptions {
52 pub initial_radius: f64,
54 pub min_radius: f64,
56 pub max_radius: f64,
58 pub gtol: f64,
60 pub ctol: f64,
62 pub max_iter: usize,
64 pub fd_step: f64,
66 pub eta1: f64,
68 pub eta2: f64,
70 pub gamma_inc: f64,
72 pub gamma_dec: f64,
74 pub penalty: f64,
76 pub penalty_growth: f64,
78}
79
80impl Default for TrustConstrOptions {
81 fn default() -> Self {
82 TrustConstrOptions {
83 initial_radius: 1.0,
84 min_radius: 1e-10,
85 max_radius: 1e4,
86 gtol: 1e-8,
87 ctol: 1e-8,
88 max_iter: 1000,
89 fd_step: 1e-7,
90 eta1: 0.1,
91 eta2: 0.75,
92 gamma_inc: 2.0,
93 gamma_dec: 0.25,
94 penalty: 10.0,
95 penalty_growth: 1.5,
96 }
97 }
98}
99
100pub struct TrustConstr {
108 pub options: TrustConstrOptions,
110}
111
112impl Default for TrustConstr {
113 fn default() -> Self {
114 TrustConstr {
115 options: TrustConstrOptions::default(),
116 }
117 }
118}
119
120impl TrustConstr {
121 pub fn new(options: TrustConstrOptions) -> Self {
123 TrustConstr { options }
124 }
125
126 pub fn minimize<F>(
136 &self,
137 func: F,
138 x0: &[f64],
139 eq_constraints: &[Box<dyn Fn(&[f64]) -> f64>],
140 ineq_constraints: &[Box<dyn Fn(&[f64]) -> f64>],
141 ) -> OptimizeResult<TrustConstrResult>
142 where
143 F: Fn(&[f64]) -> f64,
144 {
145 let n = x0.len();
146 let n_eq = eq_constraints.len();
147 let n_ineq = ineq_constraints.len();
148
149 if n == 0 {
150 return Err(OptimizeError::InvalidInput(
151 "Initial point must be non-empty".to_string(),
152 ));
153 }
154
155 let h = self.options.fd_step;
165
166 let mut x = x0.to_vec();
167 let mut radius = self.options.initial_radius;
168 let mut penalty = self.options.penalty;
169 let mut lambda_eq = vec![0.0f64; n_eq];
170 let mut lambda_ineq = vec![0.0f64; n_ineq];
171 let mut nfev = 0usize;
172 let mut njev = 0usize;
173
174 let aug_lag =
176 |xv: &[f64], lam_eq: &[f64], lam_ineq: &[f64], rho: f64, nfev: &mut usize| -> f64 {
177 let fv = func(xv);
178 *nfev += 1;
179 let mut val = fv;
180 for (i, c) in eq_constraints.iter().enumerate() {
181 let cv = c(xv);
182 *nfev += 1;
183 val += lam_eq[i] * cv + 0.5 * rho * cv * cv;
184 }
185 for (j, g) in ineq_constraints.iter().enumerate() {
186 let gv = g(xv);
187 *nfev += 1;
188 let sigma = gv + lam_ineq[j] / rho;
190 if sigma > 0.0 {
191 val += lam_ineq[j] * gv + 0.5 * rho * gv * gv;
192 } else {
193 val -= lam_ineq[j] * lam_ineq[j] / (2.0 * rho);
195 }
196 }
197 val
198 };
199
200 let mut bfgs_h: Vec<Vec<f64>> = (0..n)
202 .map(|i| {
203 let mut row = vec![0.0f64; n];
204 row[i] = 1.0;
205 row
206 })
207 .collect();
208 let mut prev_x: Option<Vec<f64>> = None;
209 let mut prev_grad: Option<Vec<f64>> = None;
210
211 let aug_lag_grad = |xv: &[f64],
213 lam_eq: &[f64],
214 lam_ineq: &[f64],
215 rho: f64,
216 nfev: &mut usize,
217 njev: &mut usize|
218 -> Vec<f64> {
219 *njev += 1;
220 let f0 = aug_lag(xv, lam_eq, lam_ineq, rho, nfev);
221 let mut g = vec![0.0f64; n];
222 let mut xp = xv.to_vec();
223 for i in 0..n {
224 xp[i] = xv[i] + h;
225 g[i] = (aug_lag(&xp, lam_eq, lam_ineq, rho, nfev) - f0) / h;
226 xp[i] = xv[i];
227 }
228 g
229 };
230
231 let mut total_iter = 0usize;
232 let outer_iters = 30usize;
234 let inner_iters = (self.options.max_iter / outer_iters).max(10);
235
236 for _outer in 0..outer_iters {
237 let mut f_cur = aug_lag(&x, &lambda_eq, &lambda_ineq, penalty, &mut nfev);
239
240 for _inner in 0..inner_iters {
241 total_iter += 1;
242
243 let grad =
244 aug_lag_grad(&x, &lambda_eq, &lambda_ineq, penalty, &mut nfev, &mut njev);
245 let gnorm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
246
247 if let (Some(ref px), Some(ref pg)) = (&prev_x, &prev_grad) {
249 let s: Vec<f64> = x.iter().zip(px.iter()).map(|(xi, pxi)| xi - pxi).collect();
250 let y: Vec<f64> = grad
251 .iter()
252 .zip(pg.iter())
253 .map(|(gi, pgi)| gi - pgi)
254 .collect();
255 let sy: f64 = s.iter().zip(y.iter()).map(|(si, yi)| si * yi).sum();
256 let hs: Vec<f64> = (0..n)
257 .map(|i| {
258 bfgs_h[i]
259 .iter()
260 .zip(s.iter())
261 .map(|(hi, si)| hi * si)
262 .sum::<f64>()
263 })
264 .collect();
265 let sths: f64 = s.iter().zip(hs.iter()).map(|(si, hsi)| si * hsi).sum();
266 let sy_damp = sy.max(0.2 * sths);
267 if sy_damp.abs() > 1e-10 && sths.abs() > 1e-10 {
268 for i in 0..n {
269 for j in 0..n {
270 bfgs_h[i][j] += y[i] * y[j] / sy_damp - hs[i] * hs[j] / sths;
271 }
272 }
273 }
274 }
275 prev_x = Some(x.clone());
276 prev_grad = Some(grad.clone());
277
278 if gnorm < self.options.gtol {
279 break;
280 }
281
282 let d = {
284 let mut a: Vec<Vec<f64>> = bfgs_h.iter().map(|row| row.to_vec()).collect();
285 let mut b: Vec<f64> = grad.iter().map(|&gi| -gi).collect();
286 for i in 0..n {
287 a[i][i] += 1e-6;
288 }
289 tc_gaussian_elim(&mut a, &mut b, n)
290 .unwrap_or_else(|| grad.iter().map(|&gi| -gi * 0.01).collect())
291 };
292
293 let d_norm: f64 = d.iter().map(|v| v * v).sum::<f64>().sqrt();
295 let scale = if d_norm > radius {
296 radius / d_norm
297 } else {
298 1.0
299 };
300 let d_scaled: Vec<f64> = d.iter().map(|&v| v * scale).collect();
301
302 let x_trial: Vec<f64> = x
304 .iter()
305 .zip(d_scaled.iter())
306 .map(|(xi, di)| xi + di)
307 .collect();
308 let f_trial = aug_lag(&x_trial, &lambda_eq, &lambda_ineq, penalty, &mut nfev);
309
310 let actual_red = f_cur - f_trial;
311 let pred_red: f64 = grad
312 .iter()
313 .zip(d_scaled.iter())
314 .map(|(gi, di)| -gi * di)
315 .sum::<f64>();
316
317 let rho = if pred_red > 1e-14 {
318 actual_red / pred_red
319 } else if actual_red > 0.0 {
320 1.0
321 } else {
322 0.0
323 };
324
325 if rho < self.options.eta1 {
326 radius = (radius * self.options.gamma_dec).max(self.options.min_radius);
327 } else {
328 x = x_trial;
329 f_cur = f_trial;
330 if rho > self.options.eta2 {
331 radius = (radius * self.options.gamma_inc).min(self.options.max_radius);
332 }
333 }
334
335 if radius < self.options.min_radius {
336 break;
337 }
338 }
339
340 let mut cv_eq_sum = 0.0f64;
342 for c in eq_constraints.iter() {
343 cv_eq_sum += c(&x).abs();
344 nfev += 1;
345 }
346 let mut cv_ineq_sum = 0.0f64;
347 for g in ineq_constraints.iter() {
348 let gv = g(&x);
349 nfev += 1;
350 cv_ineq_sum += gv.max(0.0);
351 }
352 let cv_sum = cv_eq_sum + cv_ineq_sum;
353
354 let grad_final =
356 aug_lag_grad(&x, &lambda_eq, &lambda_ineq, penalty, &mut nfev, &mut njev);
357 let gnorm_final: f64 = grad_final.iter().map(|g| g * g).sum::<f64>().sqrt();
358
359 if gnorm_final < self.options.gtol && cv_sum < self.options.ctol {
360 let final_f = func(&x);
361 nfev += 1;
362 return Ok(TrustConstrResult {
363 x: x.clone(),
364 fun: final_f,
365 grad: grad_final,
366 constraint_violation: cv_sum,
367 lambda_eq,
368 lambda_ineq,
369 nit: total_iter,
370 nfev,
371 njev,
372 trust_radius: radius,
373 success: true,
374 message: "Optimization converged".to_string(),
375 });
376 }
377
378 for (i, c) in eq_constraints.iter().enumerate() {
380 let cv = c(&x);
381 nfev += 1;
382 lambda_eq[i] += penalty * cv;
383 }
384 for (j, g) in ineq_constraints.iter().enumerate() {
386 let gv = g(&x);
387 nfev += 1;
388 lambda_ineq[j] = (lambda_ineq[j] + penalty * gv).max(0.0);
389 }
390
391 if cv_sum > self.options.ctol * 10.0 {
393 penalty = (penalty * self.options.penalty_growth).min(1e8);
394 radius = self.options.initial_radius;
397 bfgs_h = (0..n)
399 .map(|i| {
400 let mut row = vec![0.0f64; n];
401 row[i] = 1.0;
402 row
403 })
404 .collect();
405 prev_x = None;
406 prev_grad = None;
407 }
408 }
409
410 let x_final = x;
411 let f_final = func(&x_final);
412 nfev += 1;
413
414 let mut cv_final = 0.0f64;
415 for c in eq_constraints {
416 cv_final += c(&x_final).abs();
417 nfev += 1;
418 }
419 for g in ineq_constraints {
420 cv_final += g(&x_final).max(0.0);
421 nfev += 1;
422 }
423
424 let grad_final: Vec<f64> = {
425 let mut g = vec![0.0f64; n];
426 let f0 = func(&x_final);
427 nfev += 1;
428 for i in 0..n {
429 let mut xf = x_final.clone();
430 xf[i] += h;
431 g[i] = (func(&xf) - f0) / h;
432 nfev += 1;
433 njev += 1;
434 }
435 g
436 };
437
438 Ok(TrustConstrResult {
439 x: x_final,
440 fun: f_final,
441 grad: grad_final,
442 constraint_violation: cv_final,
443 lambda_eq,
444 lambda_ineq,
445 nit: total_iter,
446 nfev,
447 njev,
448 trust_radius: radius,
449 success: cv_final < self.options.ctol * 100.0,
450 message: "Maximum iterations reached".to_string(),
451 })
452 }
453}
454
455fn tc_gaussian_elim(a: &mut Vec<Vec<f64>>, b: &mut Vec<f64>, n: usize) -> Option<Vec<f64>> {
458 for col in 0..n {
459 let mut max_row = col;
460 let mut max_val = a[col][col].abs();
461 for row in (col + 1)..n {
462 if a[row][col].abs() > max_val {
463 max_val = a[row][col].abs();
464 max_row = row;
465 }
466 }
467 if max_val < 1e-14 {
468 return None;
469 }
470 a.swap(col, max_row);
471 b.swap(col, max_row);
472
473 let pivot = a[col][col];
474 for row in (col + 1)..n {
475 let factor = a[row][col] / pivot;
476 for k in col..n {
477 let val = a[col][k] * factor;
478 a[row][k] -= val;
479 }
480 let bv = b[col] * factor;
481 b[row] -= bv;
482 }
483 }
484
485 let mut x = vec![0.0f64; n];
486 for i in (0..n).rev() {
487 let mut sum = b[i];
488 for j in (i + 1)..n {
489 sum -= a[i][j] * x[j];
490 }
491 if a[i][i].abs() < 1e-14 {
492 return None;
493 }
494 x[i] = sum / a[i][i];
495 }
496 Some(x)
497}
498
499fn update_multipliers_eq(
501 x: &[f64],
502 lambda: &mut Vec<f64>,
503 constraints: &[Box<dyn Fn(&[f64]) -> f64>],
504 penalty: f64,
505 h: f64,
506 nfev: &mut usize,
507) {
508 for (i, c) in constraints.iter().enumerate() {
509 let cv = c(x);
510 *nfev += 1;
511 lambda[i] += penalty * cv;
513 }
514}
515
516fn update_multipliers_ineq(
518 x: &[f64],
519 lambda: &mut Vec<f64>,
520 constraints: &[Box<dyn Fn(&[f64]) -> f64>],
521 penalty: f64,
522 _h: f64,
523 nfev: &mut usize,
524) {
525 for (i, g) in constraints.iter().enumerate() {
526 let gv = g(x);
527 *nfev += 1;
528 lambda[i] = (lambda[i] + penalty * gv).max(0.0);
530 }
531}
532
533pub struct SubproblemSolver {
544 pub max_cg_iter: usize,
546 pub cg_tol: f64,
548}
549
550impl Default for SubproblemSolver {
551 fn default() -> Self {
552 SubproblemSolver {
553 max_cg_iter: 100,
554 cg_tol: 1e-10,
555 }
556 }
557}
558
559impl SubproblemSolver {
560 pub fn new(max_cg_iter: usize, cg_tol: f64) -> Self {
562 SubproblemSolver {
563 max_cg_iter,
564 cg_tol,
565 }
566 }
567
568 pub fn solve<BV>(&self, g: &[f64], b_times_v: BV, radius: f64) -> (Vec<f64>, bool)
578 where
579 BV: Fn(&[f64]) -> Vec<f64>,
580 {
581 let n = g.len();
582 let mut p = vec![0.0f64; n];
583 let mut r = g.to_vec(); let mut d: Vec<f64> = r.iter().map(|ri| -ri).collect(); let r_norm_0: f64 = r.iter().map(|ri| ri * ri).sum::<f64>().sqrt();
587 if r_norm_0 < self.cg_tol {
588 return (p, false);
589 }
590
591 for _iter in 0..self.max_cg_iter {
592 let bd = b_times_v(&d);
593
594 let dtbd: f64 = d.iter().zip(bd.iter()).map(|(di, bdi)| di * bdi).sum();
596
597 if dtbd <= 0.0 {
598 let tau = find_tau_boundary(&p, &d, radius);
600 for i in 0..n {
601 p[i] += tau * d[i];
602 }
603 return (p, true);
604 }
605
606 let r_norm_sq: f64 = r.iter().map(|ri| ri * ri).sum();
607 let alpha = r_norm_sq / dtbd;
608
609 let mut p_new = vec![0.0f64; n];
611 for i in 0..n {
612 p_new[i] = p[i] + alpha * d[i];
613 }
614
615 let p_new_norm: f64 = p_new.iter().map(|pi| pi * pi).sum::<f64>().sqrt();
617 if p_new_norm >= radius {
618 let tau = find_tau_boundary(&p, &d, radius);
620 for i in 0..n {
621 p[i] += tau * d[i];
622 }
623 return (p, true);
624 }
625
626 p = p_new;
627
628 let mut r_new = vec![0.0f64; n];
630 for i in 0..n {
631 r_new[i] = r[i] + alpha * bd[i];
632 }
633
634 let r_new_norm: f64 = r_new.iter().map(|ri| ri * ri).sum::<f64>().sqrt();
635 if r_new_norm < self.cg_tol * r_norm_0 {
636 return (p, false);
637 }
638
639 let beta = r_new.iter().map(|ri| ri * ri).sum::<f64>() / r_norm_sq;
640 for i in 0..n {
641 d[i] = -r_new[i] + beta * d[i];
642 }
643 r = r_new;
644 }
645
646 (p, false)
647 }
648}
649
650fn find_tau_boundary(p: &[f64], d: &[f64], radius: f64) -> f64 {
652 let a: f64 = d.iter().map(|di| di * di).sum();
653 let b: f64 = 2.0 * p.iter().zip(d.iter()).map(|(pi, di)| pi * di).sum::<f64>();
654 let c: f64 = p.iter().map(|pi| pi * pi).sum::<f64>() - radius * radius;
655
656 if a < 1e-14 {
657 return 0.0;
658 }
659
660 let disc = b * b - 4.0 * a * c;
661 if disc < 0.0 {
662 return 0.0;
663 }
664
665 let sqrt_disc = disc.sqrt();
666 let tau1 = (-b + sqrt_disc) / (2.0 * a);
667 let tau2 = (-b - sqrt_disc) / (2.0 * a);
668
669 if tau1 >= 0.0 {
670 tau1
671 } else {
672 tau2.max(0.0)
673 }
674}
675
676#[derive(Debug, Clone)]
682pub struct EqualityConstrainedOptions {
683 pub rho: f64,
685 pub max_rho: f64,
687 pub rho_growth: f64,
689 pub outer_tol: f64,
691 pub inner_tol: f64,
693 pub max_outer: usize,
695 pub max_inner: usize,
697 pub radius: f64,
699 pub h: f64,
701}
702
703impl Default for EqualityConstrainedOptions {
704 fn default() -> Self {
705 EqualityConstrainedOptions {
706 rho: 1.0,
707 max_rho: 1e8,
708 rho_growth: 2.0,
709 outer_tol: 1e-7,
710 inner_tol: 1e-6,
711 max_outer: 50,
712 max_inner: 200,
713 radius: 1.0,
714 h: 1e-7,
715 }
716 }
717}
718
719pub struct EqualityConstrained {
729 pub options: EqualityConstrainedOptions,
731}
732
733impl Default for EqualityConstrained {
734 fn default() -> Self {
735 EqualityConstrained {
736 options: EqualityConstrainedOptions::default(),
737 }
738 }
739}
740
741impl EqualityConstrained {
742 pub fn new(options: EqualityConstrainedOptions) -> Self {
744 EqualityConstrained { options }
745 }
746
747 pub fn minimize<F>(
749 &self,
750 func: F,
751 x0: &[f64],
752 constraints: &[Box<dyn Fn(&[f64]) -> f64>],
753 ) -> OptimizeResult<TrustConstrResult>
754 where
755 F: Fn(&[f64]) -> f64,
756 {
757 let n = x0.len();
758 let n_eq = constraints.len();
759 let h = self.options.h;
760
761 if n == 0 {
762 return Err(OptimizeError::InvalidInput(
763 "Initial point must be non-empty".to_string(),
764 ));
765 }
766
767 let mut x = x0.to_vec();
768 let mut lambda = vec![0.0f64; n_eq];
769 let mut rho = self.options.rho;
770 let mut radius = self.options.radius;
771 let mut nfev = 0usize;
772 let mut njev = 0usize;
773 let mut total_iter = 0usize;
774
775 let aug_lag = |x: &[f64], lambda: &[f64], rho: f64, nfev: &mut usize| -> f64 {
777 let f = func(x);
778 *nfev += 1;
779 let mut pen = 0.0f64;
780 for (i, c) in constraints.iter().enumerate() {
781 let cv = c(x);
782 *nfev += 1;
783 pen += lambda[i] * cv + 0.5 * rho * cv * cv;
784 }
785 f + pen
786 };
787
788 for _outer in 0..self.options.max_outer {
789 for _inner in 0..self.options.max_inner {
791 total_iter += 1;
792
793 let f_cur = aug_lag(&x, &lambda, rho, &mut nfev);
794
795 let mut grad = vec![0.0f64; n];
797 for i in 0..n {
798 let mut xf = x.clone();
799 xf[i] += h;
800 grad[i] = (aug_lag(&xf, &lambda, rho, &mut nfev) - f_cur) / h;
801 njev += 1;
802 }
803
804 let gnorm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
805 if gnorm < self.options.inner_tol {
806 break;
807 }
808
809 let step = (radius / gnorm).min(radius);
811 let mut x_trial = vec![0.0f64; n];
812 for i in 0..n {
813 x_trial[i] = x[i] - step * grad[i];
814 }
815
816 let f_trial = aug_lag(&x_trial, &lambda, rho, &mut nfev);
817
818 if f_trial < f_cur {
820 let improvement = f_cur - f_trial;
821 x = x_trial;
822 if improvement > 0.5 * step * gnorm {
823 radius = (radius * 2.0).min(10.0);
824 }
825 } else {
826 radius *= 0.5;
827 if radius < 1e-12 {
828 break;
829 }
830 }
831 }
832
833 let mut cv_norm = 0.0f64;
835 let mut cv_vec = vec![0.0f64; n_eq];
836 for (i, c) in constraints.iter().enumerate() {
837 cv_vec[i] = c(&x);
838 nfev += 1;
839 cv_norm += cv_vec[i] * cv_vec[i];
840 }
841 cv_norm = cv_norm.sqrt();
842
843 if cv_norm < self.options.outer_tol {
844 let f_final = func(&x);
845 nfev += 1;
846
847 let mut grad_f = vec![0.0f64; n];
849 for i in 0..n {
850 let mut xf = x.clone();
851 xf[i] += h;
852 grad_f[i] = (func(&xf) - f_final) / h;
853 nfev += 1;
854 njev += 1;
855 }
856
857 return Ok(TrustConstrResult {
858 x,
859 fun: f_final,
860 grad: grad_f,
861 constraint_violation: cv_norm,
862 lambda_eq: lambda,
863 lambda_ineq: vec![],
864 nit: total_iter,
865 nfev,
866 njev,
867 trust_radius: radius,
868 success: true,
869 message: "Equality-constrained TR converged".to_string(),
870 });
871 }
872
873 for i in 0..n_eq {
875 lambda[i] += rho * cv_vec[i];
876 }
877
878 rho = (rho * self.options.rho_growth).min(self.options.max_rho);
880 }
881
882 let f_final = func(&x);
883 nfev += 1;
884 let cv_final: f64 = constraints
885 .iter()
886 .map(|c| {
887 nfev += 1;
888 c(&x).powi(2)
889 })
890 .sum::<f64>()
891 .sqrt();
892
893 Ok(TrustConstrResult {
894 x,
895 fun: f_final,
896 grad: vec![0.0f64; n],
897 constraint_violation: cv_final,
898 lambda_eq: lambda,
899 lambda_ineq: vec![],
900 nit: total_iter,
901 nfev,
902 njev,
903 trust_radius: radius,
904 success: cv_final < self.options.outer_tol * 100.0,
905 message: "Maximum outer iterations reached".to_string(),
906 })
907 }
908}
909
910#[derive(Debug, Clone)]
916pub struct InequalityHandlingOptions {
917 pub mu: f64,
919 pub mu_reduce: f64,
921 pub min_mu: f64,
923 pub tol: f64,
925 pub max_iter: usize,
927 pub radius: f64,
929 pub h: f64,
931}
932
933impl Default for InequalityHandlingOptions {
934 fn default() -> Self {
935 InequalityHandlingOptions {
936 mu: 1.0,
937 mu_reduce: 0.1,
938 min_mu: 1e-9,
939 tol: 1e-7,
940 max_iter: 500,
941 radius: 1.0,
942 h: 1e-7,
943 }
944 }
945}
946
947pub struct InequalityHandling {
957 pub options: InequalityHandlingOptions,
959}
960
961impl Default for InequalityHandling {
962 fn default() -> Self {
963 InequalityHandling {
964 options: InequalityHandlingOptions::default(),
965 }
966 }
967}
968
969impl InequalityHandling {
970 pub fn new(options: InequalityHandlingOptions) -> Self {
972 InequalityHandling { options }
973 }
974
975 pub fn minimize<F>(
977 &self,
978 func: F,
979 x0: &[f64],
980 ineq_constraints: &[Box<dyn Fn(&[f64]) -> f64>],
981 ) -> OptimizeResult<TrustConstrResult>
982 where
983 F: Fn(&[f64]) -> f64,
984 {
985 let n = x0.len();
986 let n_ineq = ineq_constraints.len();
987 let h = self.options.h;
988
989 if n == 0 {
990 return Err(OptimizeError::InvalidInput(
991 "Initial point must be non-empty".to_string(),
992 ));
993 }
994
995 let n_aug = n + n_ineq;
997 let mut z = vec![0.0f64; n_aug];
998 for i in 0..n {
999 z[i] = x0[i];
1000 }
1001 for j in 0..n_ineq {
1003 let gj = (ineq_constraints[j])(&z[0..n]);
1004 z[n + j] = (-gj + 1e-4).max(1e-8);
1005 }
1006
1007 let mut mu = self.options.mu;
1008 let mut radius = self.options.radius;
1009 let mut nfev = 0usize;
1010 let mut njev = 0usize;
1011 let mut total_iter = 0usize;
1012
1013 let barrier = |z: &[f64], mu: f64, nfev: &mut usize| -> f64 {
1015 let x = &z[0..n];
1016 let s = &z[n..n_aug];
1017 let f = func(x);
1018 *nfev += 1;
1019 let mut barrier_val = 0.0f64;
1020 let mut pen = 0.0f64;
1021 for (j, g) in ineq_constraints.iter().enumerate() {
1022 let gj = g(x);
1023 *nfev += 1;
1024 if s[j] > 0.0 {
1025 barrier_val -= mu * s[j].ln();
1026 } else {
1027 barrier_val += 1e10; }
1029 pen += (gj + s[j]).powi(2);
1031 }
1032 f + barrier_val + 1000.0 * mu * pen
1033 };
1034
1035 while mu >= self.options.min_mu {
1036 let mut f_cur = barrier(&z, mu, &mut nfev);
1037
1038 for _inner in 0..self.options.max_iter / 10 {
1039 total_iter += 1;
1040
1041 let mut grad = vec![0.0f64; n_aug];
1043 for i in 0..n_aug {
1044 let mut zf = z.clone();
1045 zf[i] += h;
1046 if i >= n && zf[i] <= 0.0 {
1048 grad[i] = 1e10;
1049 continue;
1050 }
1051 grad[i] = (barrier(&zf, mu, &mut nfev) - f_cur) / h;
1052 njev += 1;
1053 }
1054
1055 let gnorm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
1056 if gnorm < self.options.tol {
1057 break;
1058 }
1059
1060 let step = (radius / gnorm).min(1.0);
1062 let mut z_trial = vec![0.0f64; n_aug];
1063 for i in 0..n_aug {
1064 z_trial[i] = z[i] - step * grad[i];
1065 }
1066 for j in 0..n_ineq {
1068 if z_trial[n + j] <= 0.0 {
1069 z_trial[n + j] = 1e-10;
1070 }
1071 }
1072
1073 let f_trial = barrier(&z_trial, mu, &mut nfev);
1074
1075 if f_trial < f_cur {
1076 z = z_trial;
1077 f_cur = f_trial;
1078 radius = (radius * 1.5).min(10.0);
1079 } else {
1080 radius *= 0.5;
1081 if radius < 1e-12 {
1082 break;
1083 }
1084 }
1085 }
1086
1087 mu *= self.options.mu_reduce;
1088 }
1089
1090 let x_final = z[0..n].to_vec();
1091 let f_final = func(&x_final);
1092 nfev += 1;
1093
1094 let mut cv_final = 0.0f64;
1095 for g in ineq_constraints {
1096 let gv = g(&x_final);
1097 nfev += 1;
1098 if gv > 0.0 {
1099 cv_final += gv;
1100 }
1101 }
1102
1103 let mut grad_f = vec![0.0f64; n];
1104 let f0 = func(&x_final);
1105 nfev += 1;
1106 for i in 0..n {
1107 let mut xf = x_final.clone();
1108 xf[i] += h;
1109 grad_f[i] = (func(&xf) - f0) / h;
1110 nfev += 1;
1111 njev += 1;
1112 }
1113
1114 Ok(TrustConstrResult {
1115 x: x_final,
1116 fun: f_final,
1117 grad: grad_f,
1118 constraint_violation: cv_final,
1119 lambda_eq: vec![],
1120 lambda_ineq: z[n..n_aug].to_vec(),
1121 nit: total_iter,
1122 nfev,
1123 njev,
1124 trust_radius: radius,
1125 success: cv_final < self.options.tol * 100.0,
1126 message: "Interior-point TR optimization completed".to_string(),
1127 })
1128 }
1129}
1130
1131#[derive(Debug, Clone)]
1137pub struct FilterMethodTROptions {
1138 pub initial_radius: f64,
1140 pub min_radius: f64,
1142 pub max_radius: f64,
1144 pub gamma_f: f64,
1146 pub gamma_theta: f64,
1148 pub eta1: f64,
1150 pub max_iter: usize,
1152 pub tol: f64,
1154 pub h: f64,
1156}
1157
1158impl Default for FilterMethodTROptions {
1159 fn default() -> Self {
1160 FilterMethodTROptions {
1161 initial_radius: 1.0,
1162 min_radius: 1e-10,
1163 max_radius: 100.0,
1164 gamma_f: 1e-5,
1165 gamma_theta: 1e-5,
1166 eta1: 0.1,
1167 max_iter: 500,
1168 tol: 1e-7,
1169 h: 1e-7,
1170 }
1171 }
1172}
1173
1174#[derive(Debug, Clone)]
1176struct FilterEntry {
1177 f_val: f64,
1178 theta: f64,
1179}
1180
1181impl FilterEntry {
1182 fn dominates(&self, f: f64, theta: f64, gamma_f: f64, gamma_theta: f64) -> bool {
1184 self.f_val - gamma_f * self.theta <= f && self.theta * (1.0 - gamma_theta) <= theta
1185 }
1186}
1187
1188pub struct FilterMethodTR {
1197 pub options: FilterMethodTROptions,
1199}
1200
1201impl Default for FilterMethodTR {
1202 fn default() -> Self {
1203 FilterMethodTR {
1204 options: FilterMethodTROptions::default(),
1205 }
1206 }
1207}
1208
1209impl FilterMethodTR {
1210 pub fn new(options: FilterMethodTROptions) -> Self {
1212 FilterMethodTR { options }
1213 }
1214
1215 pub fn minimize<F>(
1223 &self,
1224 func: F,
1225 x0: &[f64],
1226 constraints: &[Box<dyn Fn(&[f64]) -> f64>],
1227 ) -> OptimizeResult<TrustConstrResult>
1228 where
1229 F: Fn(&[f64]) -> f64,
1230 {
1231 let n = x0.len();
1232 let h = self.options.h;
1233
1234 if n == 0 {
1235 return Err(OptimizeError::InvalidInput(
1236 "Initial point must be non-empty".to_string(),
1237 ));
1238 }
1239
1240 let mut x = x0.to_vec();
1241 let mut radius = self.options.initial_radius;
1242 let mut nfev = 0usize;
1243 let mut njev = 0usize;
1244
1245 let theta = |x: &[f64], nfev: &mut usize| -> f64 {
1247 constraints
1248 .iter()
1249 .map(|c| {
1250 *nfev += 1;
1251 c(x).max(0.0)
1252 })
1253 .sum()
1254 };
1255
1256 let f0 = func(&x);
1257 nfev += 1;
1258 let theta0 = theta(&x, &mut nfev);
1259
1260 let mut filter: Vec<FilterEntry> = vec![FilterEntry {
1262 f_val: f0,
1263 theta: theta0,
1264 }];
1265
1266 let is_filter_acceptable = |f: f64, th: f64, filter: &[FilterEntry]| -> bool {
1267 for entry in filter {
1268 if entry.dominates(f, th, self.options.gamma_f, self.options.gamma_theta) {
1269 return false;
1270 }
1271 }
1272 true
1273 };
1274
1275 let mut f_cur = f0;
1276
1277 for iter in 0..self.options.max_iter {
1278 let mut grad = vec![0.0f64; n];
1280 for i in 0..n {
1281 let mut xf = x.clone();
1282 xf[i] += h;
1283 grad[i] = (func(&xf) - f_cur) / h;
1284 nfev += 1;
1285 njev += 1;
1286 }
1287
1288 let gnorm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
1289 let theta_cur = theta(&x, &mut nfev);
1290
1291 if gnorm < self.options.tol && theta_cur < self.options.tol {
1292 let mut grad_f = vec![0.0f64; n];
1293 let f0c = func(&x);
1294 nfev += 1;
1295 for i in 0..n {
1296 let mut xf = x.clone();
1297 xf[i] += h;
1298 grad_f[i] = (func(&xf) - f0c) / h;
1299 nfev += 1;
1300 }
1301 return Ok(TrustConstrResult {
1302 x,
1303 fun: f_cur,
1304 grad: grad_f,
1305 constraint_violation: theta_cur,
1306 lambda_eq: vec![],
1307 lambda_ineq: vec![],
1308 nit: iter + 1,
1309 nfev,
1310 njev,
1311 trust_radius: radius,
1312 success: true,
1313 message: "Filter-TR converged".to_string(),
1314 });
1315 }
1316
1317 let step_len = (radius / gnorm).min(radius);
1319 let mut x_trial = vec![0.0f64; n];
1320 for i in 0..n {
1321 x_trial[i] = x[i] - step_len * grad[i];
1322 }
1323
1324 let f_trial = func(&x_trial);
1325 nfev += 1;
1326 let theta_trial = theta(&x_trial, &mut nfev);
1327
1328 let actual_red = f_cur - f_trial;
1330 let predicted_red = step_len * gnorm;
1331 let rho = if predicted_red.abs() > 1e-14 {
1332 actual_red / predicted_red
1333 } else {
1334 0.0
1335 };
1336
1337 let accepted = is_filter_acceptable(f_trial, theta_trial, &filter);
1339
1340 if accepted && (rho > self.options.eta1 || theta_trial < theta_cur * 0.9) {
1341 x = x_trial;
1343 f_cur = f_trial;
1344
1345 filter.retain(|e| {
1347 !(e.f_val >= f_cur && e.theta >= theta_trial * (1.0 - self.options.gamma_theta))
1348 });
1349 filter.push(FilterEntry {
1350 f_val: f_cur,
1351 theta: theta_trial,
1352 });
1353
1354 if rho > 0.75 {
1356 radius =
1357 (radius * self.options.initial_radius.sqrt()).min(self.options.max_radius);
1358 }
1359 } else {
1360 radius = (radius * 0.25).max(self.options.min_radius);
1362 }
1363
1364 if radius < self.options.min_radius {
1365 break;
1366 }
1367 }
1368
1369 let theta_final = theta(&x, &mut nfev);
1370
1371 Ok(TrustConstrResult {
1372 x,
1373 fun: f_cur,
1374 grad: vec![0.0f64; n],
1375 constraint_violation: theta_final,
1376 lambda_eq: vec![],
1377 lambda_ineq: vec![],
1378 nit: self.options.max_iter,
1379 nfev,
1380 njev,
1381 trust_radius: radius,
1382 success: theta_final < self.options.tol * 100.0,
1383 message: "Filter-TR: maximum iterations reached".to_string(),
1384 })
1385 }
1386}
1387
1388#[cfg(test)]
1393mod tests {
1394 use super::*;
1395
1396 fn rosenbrock(x: &[f64]) -> f64 {
1397 (1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0].powi(2)).powi(2)
1398 }
1399
1400 #[test]
1401 fn test_trust_constr_unconstrained() {
1402 let tc = TrustConstr::default();
1403 let result = tc
1404 .minimize(rosenbrock, &[0.0, 0.0], &[], &[])
1405 .expect("failed to create result");
1406 assert!(result.fun < 1.0, "Expected fun < 1.0, got {}", result.fun);
1407 }
1408
1409 #[test]
1410 fn test_trust_constr_with_equality() {
1411 let func = |x: &[f64]| (x[0] - 2.0).powi(2) + (x[1] - 2.0).powi(2);
1413 let eq_c: Vec<Box<dyn Fn(&[f64]) -> f64>> = vec![Box::new(|x: &[f64]| x[0] + x[1] - 3.0)];
1414
1415 let tc = TrustConstr::new(TrustConstrOptions {
1416 max_iter: 500,
1417 ..Default::default()
1418 });
1419 let result = tc
1420 .minimize(func, &[0.5, 0.5], &eq_c, &[])
1421 .expect("failed to create result");
1422 assert!(
1423 result.constraint_violation < 0.5,
1424 "cv = {}",
1425 result.constraint_violation
1426 );
1427 }
1428
1429 #[test]
1430 fn test_trust_constr_with_inequality() {
1431 let func = |x: &[f64]| (x[0] - 1.0).powi(2) + (x[1] - 1.0).powi(2);
1433 let ineq_c: Vec<Box<dyn Fn(&[f64]) -> f64>> = vec![Box::new(|x: &[f64]| x[0] + x[1] - 1.0)];
1434
1435 let tc = TrustConstr::new(TrustConstrOptions {
1436 max_iter: 500,
1437 ..Default::default()
1438 });
1439 let result = tc
1440 .minimize(func, &[0.25, 0.25], &[], &ineq_c)
1441 .expect("failed to create result");
1442 assert!(result.fun < 1.0, "Expected fun < 1.0, got {}", result.fun);
1443 }
1444
1445 #[test]
1446 fn test_subproblem_solver_cauchy_point() {
1447 let solver = SubproblemSolver::default();
1448 let g = vec![1.0, 0.0];
1449 let (p, on_boundary) = solver.solve(&g, |v| v.to_vec(), 0.5);
1451 assert!(
1452 (p[0] + 0.5).abs() < 0.1,
1453 "p[0] should be near -0.5, got {}",
1454 p[0]
1455 );
1456 assert!(on_boundary, "Should hit boundary");
1457 }
1458
1459 #[test]
1460 fn test_equality_constrained_tr() {
1461 let func = |x: &[f64]| x[0].powi(2) + x[1].powi(2);
1462 let constraints: Vec<Box<dyn Fn(&[f64]) -> f64>> =
1463 vec![Box::new(|x: &[f64]| x[0] + x[1] - 1.0)];
1464
1465 let ec = EqualityConstrained::new(EqualityConstrainedOptions {
1466 max_outer: 30,
1467 max_inner: 100,
1468 outer_tol: 1e-5,
1469 ..Default::default()
1470 });
1471 let result = ec
1472 .minimize(func, &[0.5, 0.5], &constraints)
1473 .expect("failed to create result");
1474 assert!(result.fun < 0.6, "Expected fun < 0.6, got {}", result.fun);
1476 }
1477
1478 #[test]
1479 fn test_inequality_handling_interior_point() {
1480 let func = |x: &[f64]| x[0].powi(2) + x[1].powi(2);
1482 let ineq_c: Vec<Box<dyn Fn(&[f64]) -> f64>> =
1483 vec![Box::new(|x: &[f64]| -(x[0] + x[1] - 1.0))];
1484
1485 let ih = InequalityHandling::default();
1486 let result = ih
1487 .minimize(func, &[1.0, 1.0], &ineq_c)
1488 .expect("failed to create result");
1489 assert!(result.fun < 1.0, "Expected fun < 1.0, got {}", result.fun);
1490 }
1491
1492 #[test]
1493 fn test_filter_tr_unconstrained() {
1494 let ft = FilterMethodTR::default();
1495 let result = ft
1496 .minimize(rosenbrock, &[0.0, 0.0], &[])
1497 .expect("failed to create result");
1498 assert!(result.fun < 1.0, "Expected fun < 1.0, got {}", result.fun);
1499 }
1500
1501 #[test]
1502 fn test_filter_tr_with_constraint() {
1503 let func = |x: &[f64]| (x[0] - 2.0).powi(2) + (x[1] - 2.0).powi(2);
1504 let c: Vec<Box<dyn Fn(&[f64]) -> f64>> = vec![Box::new(|x: &[f64]| x[0] + x[1] - 3.0)];
1506
1507 let ft = FilterMethodTR::new(FilterMethodTROptions {
1508 max_iter: 500,
1509 ..Default::default()
1510 });
1511 let result = ft
1512 .minimize(func, &[0.5, 0.5], &c)
1513 .expect("failed to create result");
1514 assert!(result.fun < 2.0, "Expected fun < 2.0, got {}", result.fun);
1515 }
1516}