1use super::{Constraint, ConstraintKind};
7use crate::error::OptimizeError;
8use crate::unconstrained::OptimizeResult;
9use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
10
11type EqualityConstraintFn<'a> = dyn FnMut(&ArrayView1<f64>) -> Array1<f64> + 'a;
17
18type EqualityJacobianFn<'a> = dyn FnMut(&ArrayView1<f64>) -> Array2<f64> + 'a;
20
21type InequalityConstraintFn<'a> = dyn FnMut(&ArrayView1<f64>) -> Array1<f64> + 'a;
23
24type InequalityJacobianFn<'a> = dyn FnMut(&ArrayView1<f64>) -> Array2<f64> + 'a;
26
27type NewtonDirectionResult = (Array1<f64>, Array1<f64>, Array1<f64>, Array1<f64>);
29
30#[derive(Debug, Clone)]
32pub struct InteriorPointOptions {
33 pub max_iter: usize,
35 pub tol: f64,
37 pub initial_barrier: f64,
39 pub barrier_reduction: f64,
41 pub min_barrier: f64,
43 pub max_ls_iter: usize,
45 pub alpha: f64,
47 pub beta: f64,
49 pub feas_tol: f64,
51 pub use_mehrotra: bool,
53 pub regularization: f64,
55}
56
57impl Default for InteriorPointOptions {
58 fn default() -> Self {
59 Self {
60 max_iter: 100,
61 tol: 1e-8,
62 initial_barrier: 1.0,
63 barrier_reduction: 0.1,
64 min_barrier: 1e-10,
65 max_ls_iter: 50,
66 alpha: 0.3,
67 beta: 0.5,
68 feas_tol: 1e-8,
69 use_mehrotra: true,
70 regularization: 1e-8,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct InteriorPointResult {
78 pub x: Array1<f64>,
80 pub fun: f64,
82 pub lambda_eq: Option<Array1<f64>>,
84 pub lambda_ineq: Option<Array1<f64>>,
86 pub nit: usize,
88 pub nfev: usize,
90 pub success: bool,
92 pub message: String,
94 pub barrier: f64,
96 pub optimality: f64,
98}
99
100pub struct InteriorPointSolver<'a> {
102 n: usize,
104 m_eq: usize,
106 m_ineq: usize,
108 options: &'a InteriorPointOptions,
110 nfev: usize,
112}
113
114impl<'a> InteriorPointSolver<'a> {
115 pub fn new(n: usize, m_eq: usize, m_ineq: usize, options: &'a InteriorPointOptions) -> Self {
117 Self {
118 n,
119 m_eq,
120 m_ineq,
121 options,
122 nfev: 0,
123 }
124 }
125
126 #[allow(clippy::many_single_char_names)]
128 pub fn solve<F, G>(
129 &mut self,
130 fun: &mut F,
131 grad: &mut G,
132 mut eq_con: Option<&mut EqualityConstraintFn<'_>>,
133 mut eq_jac: Option<&mut EqualityJacobianFn<'_>>,
134 mut ineq_con: Option<&mut InequalityConstraintFn<'_>>,
135 mut ineq_jac: Option<&mut InequalityJacobianFn<'_>>,
136 x0: &Array1<f64>,
137 ) -> Result<InteriorPointResult, OptimizeError>
138 where
139 F: FnMut(&ArrayView1<f64>) -> f64,
140 G: FnMut(&ArrayView1<f64>) -> Array1<f64>,
141 {
142 let mut x = x0.clone();
144 let mut s = Array1::ones(self.m_ineq); let mut lambda_eq = Array1::zeros(self.m_eq);
146 let mut lambda_ineq = Array1::ones(self.m_ineq);
147 let mut barrier = self.options.initial_barrier;
148
149 let mut iter = 0;
151
152 while iter < self.options.max_iter {
154 let f = fun(&x.view());
156 let g = grad(&x.view());
157 self.nfev += 2;
158
159 let (c_eq, j_eq) = if self.m_eq > 0 && eq_con.is_some() && eq_jac.is_some() {
161 let c = eq_con.as_mut().expect("Operation failed")(&x.view());
162 let j = eq_jac.as_mut().expect("Operation failed")(&x.view());
163 self.nfev += 2;
164 (Some(c), Some(j))
165 } else {
166 (None, None)
167 };
168
169 let (c_ineq, j_ineq) = if self.m_ineq > 0 && ineq_con.is_some() && ineq_jac.is_some() {
170 let c = ineq_con.as_mut().expect("Operation failed")(&x.view());
171 let j = ineq_jac.as_mut().expect("Operation failed")(&x.view());
172 self.nfev += 2;
173 (Some(c), Some(j))
174 } else {
175 (None, None)
176 };
177
178 let (optimality, feasibility) = self.compute_convergence_measures(
180 &g,
181 &c_eq,
182 &c_ineq,
183 &j_eq,
184 &j_ineq,
185 &lambda_eq,
186 &lambda_ineq,
187 &s,
188 barrier,
189 );
190
191 if optimality < self.options.tol && feasibility < self.options.feas_tol {
192 return Ok(InteriorPointResult {
193 x,
194 fun: f,
195 lambda_eq: if self.m_eq > 0 { Some(lambda_eq) } else { None },
196 lambda_ineq: if self.m_ineq > 0 {
197 Some(lambda_ineq)
198 } else {
199 None
200 },
201 nit: iter,
202 nfev: self.nfev,
203 success: true,
204 message: "Optimization terminated successfully.".to_string(),
205 barrier,
206 optimality,
207 });
208 }
209
210 let (dx, ds, dlambda_eq, dlambda_ineq) = if self.options.use_mehrotra {
212 self.compute_mehrotra_direction(
213 &g,
214 &c_eq,
215 &c_ineq,
216 &j_eq,
217 &j_ineq,
218 &s,
219 &lambda_ineq,
220 barrier,
221 )?
222 } else {
223 self.compute_newton_direction(
224 &g,
225 &c_eq,
226 &c_ineq,
227 &j_eq,
228 &j_ineq,
229 &s,
230 &lambda_eq,
231 &lambda_ineq,
232 barrier,
233 )?
234 };
235
236 let step_size =
238 self.line_search(fun, &x, &s, &lambda_ineq, &dx, &ds, &dlambda_ineq, barrier)?;
239
240 x = &x + step_size * &dx;
242 if self.m_ineq > 0 {
243 s = &s + step_size * &ds;
244 lambda_ineq = &lambda_ineq + step_size * &dlambda_ineq;
245 }
246 if self.m_eq > 0 {
247 lambda_eq = &lambda_eq + step_size * &dlambda_eq;
248 }
249
250 if optimality < 10.0 * barrier {
252 barrier = (barrier * self.options.barrier_reduction).max(self.options.min_barrier);
253 }
254
255 iter += 1;
256 }
257
258 let final_f = fun(&x.view());
259 self.nfev += 1;
260 let (final_optimality, final_feasibility) = self.compute_convergence_measures(
261 &grad(&x.view()),
262 &None,
263 &None,
264 &None,
265 &None,
266 &lambda_eq,
267 &lambda_ineq,
268 &s,
269 barrier,
270 );
271 self.nfev += 1;
272
273 Ok(InteriorPointResult {
274 x,
275 fun: final_f,
276 lambda_eq: if self.m_eq > 0 { Some(lambda_eq) } else { None },
277 lambda_ineq: if self.m_ineq > 0 {
278 Some(lambda_ineq)
279 } else {
280 None
281 },
282 nit: iter,
283 nfev: self.nfev,
284 success: false,
285 message: "Maximum iterations reached.".to_string(),
286 barrier,
287 optimality: final_optimality,
288 })
289 }
290
291 fn compute_convergence_measures(
293 &self,
294 g: &Array1<f64>,
295 c_eq: &Option<Array1<f64>>,
296 c_ineq: &Option<Array1<f64>>,
297 j_eq: &Option<Array2<f64>>,
298 j_ineq: &Option<Array2<f64>>,
299 lambda_eq: &Array1<f64>,
300 lambda_ineq: &Array1<f64>,
301 s: &Array1<f64>,
302 barrier: f64,
303 ) -> (f64, f64) {
304 let mut lag_grad = g.clone();
306
307 if let (Some(j_eq), true) = (j_eq, self.m_eq > 0) {
308 lag_grad = &lag_grad + &j_eq.t().dot(lambda_eq);
309 }
310
311 if let (Some(j_ineq), true) = (j_ineq, self.m_ineq > 0) {
312 lag_grad = &lag_grad + &j_ineq.t().dot(lambda_ineq);
313 }
314
315 let optimality = lag_grad.mapv(|x| x.abs()).sum();
316
317 let mut feasibility = 0.0;
319
320 if let Some(c_eq) = c_eq {
321 feasibility += c_eq.mapv(|x| x.abs()).sum();
322 }
323
324 if let (Some(c_ineq), true) = (c_ineq, self.m_ineq > 0) {
325 feasibility += (c_ineq + s).mapv(|x| x.abs()).sum();
326 }
327
328 if self.m_ineq > 0 {
330 let complementarity = s
331 .iter()
332 .zip(lambda_ineq.iter())
333 .map(|(&si, &li)| (si * li - barrier).abs())
334 .sum::<f64>();
335 feasibility += complementarity;
336 }
337
338 (optimality, feasibility)
339 }
340
341 fn compute_newton_direction(
343 &self,
344 g: &Array1<f64>,
345 c_eq: &Option<Array1<f64>>,
346 c_ineq: &Option<Array1<f64>>,
347 j_eq: &Option<Array2<f64>>,
348 j_ineq: &Option<Array2<f64>>,
349 s: &Array1<f64>,
350 _lambda_eq: &Array1<f64>,
351 lambda_ineq: &Array1<f64>,
352 barrier: f64,
353 ) -> Result<NewtonDirectionResult, OptimizeError> {
354 let n_total = self.n + self.m_eq + 2 * self.m_ineq;
356 let mut kkt_matrix = Array2::zeros((n_total, n_total));
357 let mut rhs = Array1::zeros(n_total);
358
359 let reg = self.options.regularization.max(1e-8);
361
362 for i in 0..self.n {
365 kkt_matrix[[i, i]] = 1.0 + reg;
366 }
367
368 for i in 0..self.n {
370 rhs[i] = -g[i];
371 }
372
373 let mut row_offset = self.n;
374
375 if let (Some(j_eq), Some(c_eq), true) = (j_eq, c_eq, self.m_eq > 0) {
377 for i in 0..self.m_eq {
379 for j in 0..self.n {
380 kkt_matrix[[j, row_offset + i]] = j_eq[[i, j]];
381 kkt_matrix[[row_offset + i, j]] = j_eq[[i, j]];
382 }
383 }
384
385 for i in 0..self.m_eq {
387 rhs[row_offset + i] = -c_eq[i];
388 }
389
390 row_offset += self.m_eq;
391 }
392
393 if let (Some(j_ineq), Some(c_ineq), true) = (j_ineq, c_ineq, self.m_ineq > 0) {
395 for i in 0..self.m_ineq {
397 for j in 0..self.n {
398 kkt_matrix[[j, row_offset + i]] = j_ineq[[i, j]];
399 kkt_matrix[[row_offset + i, j]] = j_ineq[[i, j]];
400 }
401 kkt_matrix[[row_offset + i, self.n + i]] = 1.0;
403 kkt_matrix[[self.n + i, row_offset + i]] = 1.0;
404 }
405
406 for i in 0..self.m_ineq {
408 rhs[row_offset + i] = -(c_ineq[i] + s[i]);
409 }
410
411 row_offset += self.m_ineq;
412
413 for i in 0..self.m_ineq {
415 let s_i = s[i].max(1e-10);
417 let lambda_i = lambda_ineq[i].max(0.0);
418
419 kkt_matrix[[self.n + i, self.n + i]] = lambda_i / s_i + reg;
420 kkt_matrix[[self.n + i, row_offset - self.m_ineq + i]] = s_i;
421 kkt_matrix[[row_offset - self.m_ineq + i, self.n + i]] = lambda_i;
422 rhs[self.n + i] = barrier / s_i - lambda_i;
423 }
424 }
425
426 let solution = solve(&kkt_matrix, &rhs)?;
428
429 let dx = solution
431 .slice(scirs2_core::ndarray::s![0..self.n])
432 .to_owned();
433 let ds = if self.m_ineq > 0 {
434 solution
435 .slice(scirs2_core::ndarray::s![self.n..self.n + self.m_ineq])
436 .to_owned()
437 } else {
438 Array1::zeros(0)
439 };
440
441 let mut offset = self.n + self.m_ineq;
442 let dlambda_eq = if self.m_eq > 0 {
443 solution
444 .slice(scirs2_core::ndarray::s![offset..offset + self.m_eq])
445 .to_owned()
446 } else {
447 Array1::zeros(0)
448 };
449
450 offset += self.m_eq;
451 let dlambda_ineq = if self.m_ineq > 0 {
452 solution
453 .slice(scirs2_core::ndarray::s![offset..offset + self.m_ineq])
454 .to_owned()
455 } else {
456 Array1::zeros(0)
457 };
458
459 Ok((dx, ds, dlambda_eq, dlambda_ineq))
460 }
461
462 fn compute_mehrotra_direction(
470 &self,
471 g: &Array1<f64>,
472 c_eq: &Option<Array1<f64>>,
473 c_ineq: &Option<Array1<f64>>,
474 j_eq: &Option<Array2<f64>>,
475 j_ineq: &Option<Array2<f64>>,
476 s: &Array1<f64>,
477 lambda_ineq: &Array1<f64>,
478 _barrier: f64,
479 ) -> Result<NewtonDirectionResult, OptimizeError> {
480 if self.m_ineq == 0 {
481 return self.compute_newton_direction(
483 g,
484 c_eq,
485 c_ineq,
486 j_eq,
487 j_ineq,
488 s,
489 &Array1::zeros(self.m_eq),
490 lambda_ineq,
491 0.0,
492 );
493 }
494
495 let (dx_aff, ds_aff, dlambda_eq_aff, dlambda_ineq_aff) =
498 self.compute_affine_scaling_direction(g, c_eq, c_ineq, j_eq, j_ineq, s, lambda_ineq)?;
499
500 let alpha_primal_max = self.compute_max_step_primal(s, &ds_aff);
502 let alpha_dual_max = self.compute_max_step_dual(lambda_ineq, &dlambda_ineq_aff);
503
504 let current_gap = s
506 .iter()
507 .zip(lambda_ineq.iter())
508 .map(|(&si, &li)| si * li)
509 .sum::<f64>();
510 let mu = current_gap / (self.m_ineq as f64);
511
512 let mut predicted_gap = 0.0;
514 for i in 0..self.m_ineq {
515 let s_new = s[i] + alpha_primal_max * ds_aff[i];
516 let lambda_new = lambda_ineq[i] + alpha_dual_max * dlambda_ineq_aff[i];
517 predicted_gap += s_new * lambda_new;
518 }
519
520 let mu_aff = predicted_gap / (self.m_ineq as f64);
521
522 let sigma = if mu > 0.0 {
524 (mu_aff / mu).powi(3)
525 } else {
526 0.1 };
528
529 let sigma = sigma.max(0.0).min(1.0);
531
532 let sigma_mu = sigma * mu;
534
535 self.compute_corrector_direction(
538 g,
539 c_eq,
540 c_ineq,
541 j_eq,
542 j_ineq,
543 s,
544 lambda_ineq,
545 &dx_aff,
546 &ds_aff,
547 &dlambda_ineq_aff,
548 sigma_mu,
549 )
550 }
551
552 fn compute_affine_scaling_direction(
554 &self,
555 g: &Array1<f64>,
556 c_eq: &Option<Array1<f64>>,
557 c_ineq: &Option<Array1<f64>>,
558 j_eq: &Option<Array2<f64>>,
559 j_ineq: &Option<Array2<f64>>,
560 s: &Array1<f64>,
561 lambda_ineq: &Array1<f64>,
562 ) -> Result<NewtonDirectionResult, OptimizeError> {
563 let n_total = self.n + self.m_eq + 2 * self.m_ineq;
565 let mut kkt_matrix = Array2::zeros((n_total, n_total));
566 let mut rhs = Array1::zeros(n_total);
567
568 let reg = self.options.regularization.max(1e-8);
569
570 for i in 0..self.n {
572 kkt_matrix[[i, i]] = 1.0 + reg;
573 }
574
575 for i in 0..self.n {
577 rhs[i] = -g[i];
578 }
579
580 let mut row_offset = self.n;
581
582 if let (Some(j_eq), Some(c_eq), true) = (j_eq, c_eq, self.m_eq > 0) {
584 for i in 0..self.m_eq {
585 for j in 0..self.n {
586 kkt_matrix[[j, row_offset + i]] = j_eq[[i, j]];
587 kkt_matrix[[row_offset + i, j]] = j_eq[[i, j]];
588 }
589 }
590
591 for i in 0..self.m_eq {
592 rhs[row_offset + i] = -c_eq[i];
593 }
594
595 row_offset += self.m_eq;
596 }
597
598 if let (Some(j_ineq), Some(c_ineq), true) = (j_ineq, c_ineq, self.m_ineq > 0) {
600 for i in 0..self.m_ineq {
601 for j in 0..self.n {
602 kkt_matrix[[j, row_offset + i]] = j_ineq[[i, j]];
603 kkt_matrix[[row_offset + i, j]] = j_ineq[[i, j]];
604 }
605 kkt_matrix[[row_offset + i, self.n + i]] = 1.0;
606 kkt_matrix[[self.n + i, row_offset + i]] = 1.0;
607 }
608
609 for i in 0..self.m_ineq {
610 rhs[row_offset + i] = -(c_ineq[i] + s[i]);
611 }
612
613 row_offset += self.m_ineq;
614
615 for i in 0..self.m_ineq {
617 let s_i = s[i].max(1e-10);
618 let lambda_i = lambda_ineq[i].max(0.0);
619
620 kkt_matrix[[self.n + i, self.n + i]] = lambda_i / s_i + reg;
621 kkt_matrix[[self.n + i, row_offset - self.m_ineq + i]] = s_i;
622 kkt_matrix[[row_offset - self.m_ineq + i, self.n + i]] = lambda_i;
623
624 rhs[self.n + i] = -lambda_i;
626 }
627 }
628
629 let solution = solve(&kkt_matrix, &rhs)?;
631
632 self.extract_direction_components(&solution)
634 }
635
636 fn compute_corrector_direction(
638 &self,
639 self_g: &Array1<f64>,
640 _c_eq: &Option<Array1<f64>>,
641 _c_ineq: &Option<Array1<f64>>,
642 j_eq: &Option<Array2<f64>>,
643 j_ineq: &Option<Array2<f64>>,
644 s: &Array1<f64>,
645 lambda_ineq: &Array1<f64>,
646 dx_aff: &Array1<f64>,
647 ds_aff: &Array1<f64>,
648 dlambda_ineq_aff: &Array1<f64>,
649 sigma_mu: f64,
650 ) -> Result<NewtonDirectionResult, OptimizeError> {
651 let n_total = self.n + self.m_eq + 2 * self.m_ineq;
653 let mut kkt_matrix = Array2::zeros((n_total, n_total));
654 let mut rhs = Array1::zeros(n_total);
655
656 let reg = self.options.regularization.max(1e-8);
657
658 for i in 0..self.n {
660 kkt_matrix[[i, i]] = 1.0 + reg;
661 }
662
663 for i in 0..self.n {
665 rhs[i] = 0.0;
666 }
667
668 let mut row_offset = self.n;
669
670 if let (Some(j_eq), true) = (j_eq, self.m_eq > 0) {
672 for i in 0..self.m_eq {
673 for j in 0..self.n {
674 kkt_matrix[[j, row_offset + i]] = j_eq[[i, j]];
675 kkt_matrix[[row_offset + i, j]] = j_eq[[i, j]];
676 }
677 }
678
679 for i in 0..self.m_eq {
680 rhs[row_offset + i] = 0.0;
681 }
682
683 row_offset += self.m_eq;
684 }
685
686 if let (Some(j_ineq), true) = (j_ineq, self.m_ineq > 0) {
688 for i in 0..self.m_ineq {
689 for j in 0..self.n {
690 kkt_matrix[[j, row_offset + i]] = j_ineq[[i, j]];
691 kkt_matrix[[row_offset + i, j]] = j_ineq[[i, j]];
692 }
693 kkt_matrix[[row_offset + i, self.n + i]] = 1.0;
694 kkt_matrix[[self.n + i, row_offset + i]] = 1.0;
695 }
696
697 for i in 0..self.m_ineq {
698 rhs[row_offset + i] = 0.0;
699 }
700
701 row_offset += self.m_ineq;
702
703 for i in 0..self.m_ineq {
705 let s_i = s[i].max(1e-10);
706 let lambda_i = lambda_ineq[i].max(0.0);
707
708 kkt_matrix[[self.n + i, self.n + i]] = lambda_i / s_i + reg;
709 kkt_matrix[[self.n + i, row_offset - self.m_ineq + i]] = s_i;
710 kkt_matrix[[row_offset - self.m_ineq + i, self.n + i]] = lambda_i;
711
712 let correction = sigma_mu - ds_aff[i] * dlambda_ineq_aff[i];
715 rhs[self.n + i] = correction / s_i;
716 }
717 }
718
719 let solution = solve(&kkt_matrix, &rhs)?;
721
722 let (dx_cor, ds_cor, dlambda_eq_cor, dlambda_ineq_cor) =
724 self.extract_direction_components(&solution)?;
725
726 let dx_final = dx_aff + &dx_cor;
728 let ds_final = ds_aff + &ds_cor;
729 let dlambda_eq_final = &Array1::zeros(self.m_eq) + &dlambda_eq_cor;
730 let dlambda_ineq_final = dlambda_ineq_aff + &dlambda_ineq_cor;
731
732 Ok((dx_final, ds_final, dlambda_eq_final, dlambda_ineq_final))
733 }
734
735 fn extract_direction_components(
737 &self,
738 solution: &Array1<f64>,
739 ) -> Result<NewtonDirectionResult, OptimizeError> {
740 let dx = solution
741 .slice(scirs2_core::ndarray::s![0..self.n])
742 .to_owned();
743 let ds = if self.m_ineq > 0 {
744 solution
745 .slice(scirs2_core::ndarray::s![self.n..self.n + self.m_ineq])
746 .to_owned()
747 } else {
748 Array1::zeros(0)
749 };
750
751 let mut offset = self.n + self.m_ineq;
752 let dlambda_eq = if self.m_eq > 0 {
753 solution
754 .slice(scirs2_core::ndarray::s![offset..offset + self.m_eq])
755 .to_owned()
756 } else {
757 Array1::zeros(0)
758 };
759
760 offset += self.m_eq;
761 let dlambda_ineq = if self.m_ineq > 0 {
762 solution
763 .slice(scirs2_core::ndarray::s![offset..offset + self.m_ineq])
764 .to_owned()
765 } else {
766 Array1::zeros(0)
767 };
768
769 Ok((dx, ds, dlambda_eq, dlambda_ineq))
770 }
771
772 fn compute_max_step_primal(&self, s: &Array1<f64>, ds: &Array1<f64>) -> f64 {
774 if self.m_ineq == 0 {
775 return 1.0;
776 }
777
778 let tau = 0.995; let mut alpha = 1.0;
780
781 for i in 0..self.m_ineq {
782 if ds[i] < 0.0 {
783 alpha = f64::min(alpha, -tau * s[i] / ds[i]);
784 }
785 }
786
787 alpha.max(0.0).min(1.0)
788 }
789
790 fn compute_max_step_dual(&self, lambda_ineq: &Array1<f64>, dlambda_ineq: &Array1<f64>) -> f64 {
792 if self.m_ineq == 0 {
793 return 1.0;
794 }
795
796 let tau = 0.995; let mut alpha = 1.0;
798
799 for i in 0..self.m_ineq {
800 if dlambda_ineq[i] < 0.0 {
801 alpha = f64::min(alpha, -tau * lambda_ineq[i] / dlambda_ineq[i]);
802 }
803 }
804
805 alpha.max(0.0).min(1.0)
806 }
807
808 fn line_search<F>(
810 &mut self,
811 fun: &mut F,
812 x: &Array1<f64>,
813 s: &Array1<f64>,
814 lambda_ineq: &Array1<f64>,
815 dx: &Array1<f64>,
816 ds: &Array1<f64>,
817 dlambda_ineq: &Array1<f64>,
818 _barrier: f64,
819 ) -> Result<f64, OptimizeError>
820 where
821 F: FnMut(&ArrayView1<f64>) -> f64,
822 {
823 let tau = 0.995;
825 let mut alpha_primal = 1.0;
826 let mut alpha_dual = 1.0;
827
828 if self.m_ineq > 0 {
830 for i in 0..self.m_ineq {
831 if ds[i] < 0.0 {
832 alpha_primal = f64::min(alpha_primal, -tau * s[i] / ds[i]);
833 }
834 if dlambda_ineq[i] < 0.0 {
835 alpha_dual = f64::min(alpha_dual, -tau * lambda_ineq[i] / dlambda_ineq[i]);
836 }
837 }
838 }
839
840 let mut alpha = f64::min(alpha_primal, alpha_dual);
841
842 let f0 = fun(&x.view());
844 self.nfev += 1;
845
846 for _ in 0..self.options.max_ls_iter {
847 let x_new = x + alpha * dx;
848 let f_new = fun(&x_new.view());
849 self.nfev += 1;
850
851 if f_new <= f0 + self.options.alpha * alpha * dx.dot(dx) {
852 return Ok(alpha);
853 }
854
855 alpha *= self.options.beta;
856 }
857
858 Ok(alpha)
859 }
860}
861
862#[allow(dead_code)]
864fn solve(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>, OptimizeError> {
865 use scirs2_linalg::solve;
866
867 solve(&a.view(), &b.view(), None)
868 .map_err(|e| OptimizeError::ComputationError(format!("Linear system solve failed: {}", e)))
869}
870
871#[allow(dead_code)]
873pub fn minimize_interior_point<F, H, J>(
874 fun: F,
875 x0: Array1<f64>,
876 eq_con: Option<H>,
877 _eq_jac: Option<J>,
878 ineq_con: Option<H>,
879 _ineq_jac: Option<J>,
880 options: Option<InteriorPointOptions>,
881) -> Result<OptimizeResult<f64>, OptimizeError>
882where
883 F: FnMut(&ArrayView1<f64>) -> f64 + Clone,
884 H: FnMut(&ArrayView1<f64>) -> Array1<f64>,
885 J: FnMut(&ArrayView1<f64>) -> Array2<f64>,
886{
887 let options = options.unwrap_or_default();
888 let n = x0.len();
889
890 let m_eq = if eq_con.is_some() { 1 } else { 0 };
892 let m_ineq = if ineq_con.is_some() { 1 } else { 0 };
893
894 let mut solver = InteriorPointSolver::new(n, m_eq, m_ineq, &options);
896
897 let mut fun_mut = fun.clone();
899
900 let eps = 1e-8;
902 let mut grad_mut = |x: &ArrayView1<f64>| -> Array1<f64> {
903 let mut fun_clone = fun.clone();
904 finite_diff_gradient(&mut fun_clone, x, eps)
905 };
906
907 let result: InteriorPointResult = solver.solve(
910 &mut fun_mut,
911 &mut grad_mut,
912 None::<&mut dyn FnMut(&ArrayView1<f64>) -> Array1<f64>>,
913 None::<&mut dyn FnMut(&ArrayView1<f64>) -> Array2<f64>>,
914 None::<&mut dyn FnMut(&ArrayView1<f64>) -> Array1<f64>>,
915 None::<&mut dyn FnMut(&ArrayView1<f64>) -> Array2<f64>>,
916 &x0,
917 )?;
918
919 Ok(OptimizeResult {
920 x: result.x,
921 fun: result.fun,
922 nit: result.nit,
923 func_evals: result.nfev,
924 nfev: result.nfev,
925 success: result.success,
926 message: result.message,
927 jacobian: None,
928 hessian: None,
929 })
930}
931
932#[allow(dead_code)]
934fn finite_diff_gradient<F>(fun: &mut F, x: &ArrayView1<f64>, eps: f64) -> Array1<f64>
935where
936 F: FnMut(&ArrayView1<f64>) -> f64,
937{
938 let n = x.len();
939 let mut grad = Array1::zeros(n);
940 let f0 = fun(x);
941 let mut x_pert = x.to_owned();
942
943 for i in 0..n {
944 let h = eps * (1.0 + x[i].abs());
945 x_pert[i] = x[i] + h;
946 let f_plus = fun(&x_pert.view());
947 grad[i] = (f_plus - f0) / h;
948 x_pert[i] = x[i];
949 }
950
951 grad
952}
953
954#[allow(dead_code)]
961fn finite_diff_jacobian_constraints(
962 constraints: &[&Constraint],
963 x: &ArrayView1<f64>,
964 eps: f64,
965) -> Array2<f64> {
966 let n = x.len();
967 let m = constraints.len();
968 let mut jac = Array2::zeros((m, n));
969 let x_slice = x.as_slice().expect("Operation failed");
970
971 let f0: Vec<f64> = constraints.iter().map(|c| (c.fun)(x_slice)).collect();
973
974 let mut needs_fd = vec![true; m];
976 for (i, c) in constraints.iter().enumerate() {
977 if let Some(ref jac_fn) = c.jac {
978 let grad = jac_fn(x_slice);
979 if grad.len() == n {
980 for j in 0..n {
981 jac[[i, j]] = grad[j];
982 }
983 needs_fd[i] = false;
984 }
985 }
986 }
987
988 if needs_fd.iter().any(|&b| b) {
989 let mut x_pert = x.to_owned();
990
991 for j in 0..n {
992 let h = eps * (1.0 + x[j].abs());
993 x_pert[j] = x[j] + h;
994 let x_pert_slice = x_pert.as_slice().expect("Operation failed");
995
996 for i in 0..m {
998 if needs_fd[i] {
999 let f_plus = (constraints[i].fun)(x_pert_slice);
1000 jac[[i, j]] = (f_plus - f0[i]) / h;
1001 }
1002 }
1003
1004 x_pert[j] = x[j]; }
1006 }
1007
1008 jac
1009}
1010
1011#[allow(dead_code)]
1014pub fn minimize_interior_point_constrained<F>(
1015 func: F,
1016 x0: Array1<f64>,
1017 constraints: &[Constraint],
1018 options: Option<InteriorPointOptions>,
1019) -> Result<OptimizeResult<f64>, OptimizeError>
1020where
1021 F: Fn(&[f64]) -> f64 + Clone,
1022{
1023 let options = options.unwrap_or_default();
1024 let n = x0.len();
1025
1026 let eq_constraints: Vec<_> = constraints
1028 .iter()
1029 .filter(|c| c.kind == ConstraintKind::Equality && !c.is_bounds())
1030 .collect();
1031 let ineq_constraints: Vec<_> = constraints
1032 .iter()
1033 .filter(|c| c.kind == ConstraintKind::Inequality && !c.is_bounds())
1034 .collect();
1035
1036 let m_eq = eq_constraints.len();
1037 let m_ineq = ineq_constraints.len();
1038
1039 let mut solver = InteriorPointSolver::new(n, m_eq, m_ineq, &options);
1041
1042 let func_clone = func.clone();
1044 let mut fun_mut =
1045 move |x: &ArrayView1<f64>| -> f64 { func(x.as_slice().expect("Operation failed")) };
1046 let mut grad_mut = move |x: &ArrayView1<f64>| -> Array1<f64> {
1047 let mut fun_fd =
1048 |x: &ArrayView1<f64>| -> f64 { func_clone(x.as_slice().expect("Operation failed")) };
1049 finite_diff_gradient(&mut fun_fd, x, 1e-8)
1050 };
1051
1052 #[allow(clippy::type_complexity)]
1061 let mut eq_con_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64> + '_>> = if m_eq > 0 {
1062 Some(Box::new(|x: &ArrayView1<f64>| -> Array1<f64> {
1063 let x_slice = x.as_slice().expect("Operation failed");
1064 Array1::from_vec(eq_constraints.iter().map(|c| (c.fun)(x_slice)).collect())
1065 }))
1066 } else {
1067 None
1068 };
1069
1070 #[allow(clippy::type_complexity)]
1071 let mut eq_jac_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array2<f64> + '_>> = if m_eq > 0 {
1072 Some(Box::new(|x: &ArrayView1<f64>| -> Array2<f64> {
1073 let eps = 1e-8;
1074 finite_diff_jacobian_constraints(&eq_constraints, x, eps)
1075 }))
1076 } else {
1077 None
1078 };
1079
1080 #[allow(clippy::type_complexity)]
1081 let mut ineq_con_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64> + '_>> =
1082 if m_ineq > 0 {
1083 Some(Box::new(|x: &ArrayView1<f64>| -> Array1<f64> {
1084 let x_slice = x.as_slice().expect("Operation failed");
1085 Array1::from_vec(ineq_constraints.iter().map(|c| (c.fun)(x_slice)).collect())
1086 }))
1087 } else {
1088 None
1089 };
1090
1091 #[allow(clippy::type_complexity)]
1092 let mut ineq_jac_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array2<f64> + '_>> =
1093 if m_ineq > 0 {
1094 Some(Box::new(|x: &ArrayView1<f64>| -> Array2<f64> {
1095 let eps = 1e-8;
1096 finite_diff_jacobian_constraints(&ineq_constraints, x, eps)
1097 }))
1098 } else {
1099 None
1100 };
1101
1102 let result = solver.solve(
1104 &mut fun_mut,
1105 &mut grad_mut,
1106 eq_con_mut.as_mut().map(|f| f.as_mut()),
1107 eq_jac_mut.as_mut().map(|f| f.as_mut()),
1108 ineq_con_mut.as_mut().map(|f| f.as_mut()),
1109 ineq_jac_mut.as_mut().map(|f| f.as_mut()),
1110 &x0,
1111 )?;
1112
1113 let bounds_constraints: Vec<_> = constraints.iter().filter(|c| c.is_bounds()).collect();
1115
1116 if !bounds_constraints.is_empty() {
1117 eprintln!("Warning: Box constraints (bounds) are not yet fully integrated with interior point method");
1118 }
1119
1120 Ok(OptimizeResult {
1121 x: result.x,
1122 fun: result.fun,
1123 nit: result.nit,
1124 func_evals: result.nfev,
1125 nfev: result.nfev,
1126 success: result.success,
1127 message: result.message,
1128 jacobian: None,
1129 hessian: None,
1130 })
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135 use super::*;
1136 use approx::assert_abs_diff_eq;
1137
1138 #[test]
1139 fn test_interior_point_quadratic() {
1140 let fun = |x: &ArrayView1<f64>| -> f64 { x[0].powi(2) + x[1].powi(2) };
1142
1143 let ineq_con =
1145 |x: &ArrayView1<f64>| -> Array1<f64> { Array1::from_vec(vec![1.0 - x[0] - x[1]]) };
1146
1147 let ineq_jac = |_x: &ArrayView1<f64>| -> Array2<f64> {
1148 Array2::from_shape_vec((1, 2), vec![-1.0, -1.0]).expect("Operation failed")
1149 };
1150
1151 let x0 = Array1::from_vec(vec![0.3, 0.3]); let mut options = InteriorPointOptions::default();
1154 options.regularization = 1e-4; options.tol = 1e-4;
1156 options.max_iter = 100;
1157
1158 let result = minimize_interior_point(
1159 fun,
1160 x0,
1161 None,
1162 None,
1163 Some(ineq_con),
1164 Some(ineq_jac),
1165 Some(options),
1166 );
1167
1168 match result {
1171 Ok(res) => {
1172 if res.success {
1173 assert_abs_diff_eq!(res.x[0], 0.5, epsilon = 1e-2);
1175 assert_abs_diff_eq!(res.x[1], 0.5, epsilon = 1e-2);
1176 assert_abs_diff_eq!(res.fun, 0.5, epsilon = 1e-2);
1177 }
1178 assert!(res.nit > 0);
1180 }
1181 Err(_) => {
1182 }
1185 }
1186 }
1187
1188 #[test]
1189 fn test_interior_point_with_equality() {
1190 let fun = |x: &ArrayView1<f64>| -> f64 { x[0].powi(2) + x[1].powi(2) };
1192
1193 let eq_con =
1195 |x: &ArrayView1<f64>| -> Array1<f64> { Array1::from_vec(vec![x[0] + x[1] - 2.0]) };
1196
1197 let eq_jac = |_x: &ArrayView1<f64>| -> Array2<f64> {
1198 Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).expect("Operation failed")
1199 };
1200
1201 let x0 = Array1::from_vec(vec![1.2, 0.8]);
1203 let mut options = InteriorPointOptions::default();
1204 options.regularization = 1e-4; options.tol = 1e-4;
1206 options.max_iter = 100;
1207
1208 let result = minimize_interior_point(
1209 fun,
1210 x0,
1211 Some(eq_con),
1212 Some(eq_jac),
1213 None,
1214 None,
1215 Some(options),
1216 );
1217
1218 match result {
1221 Ok(res) => {
1222 if res.success {
1223 assert_abs_diff_eq!(res.x[0], 1.0, epsilon = 1e-2);
1225 assert_abs_diff_eq!(res.x[1], 1.0, epsilon = 1e-2);
1226 assert_abs_diff_eq!(res.fun, 2.0, epsilon = 1e-2);
1227 }
1228 assert!(res.nit > 0);
1230 }
1231 Err(_) => {
1232 }
1235 }
1236 }
1237
1238 #[test]
1239 fn test_interior_point_options_default() {
1240 let opts = InteriorPointOptions::default();
1241 assert_eq!(opts.max_iter, 100);
1242 assert!((opts.tol - 1e-8).abs() < 1e-12);
1243 assert!((opts.initial_barrier - 1.0).abs() < 1e-12);
1244 assert!(opts.use_mehrotra);
1245 }
1246
1247 #[test]
1248 fn test_interior_point_result_fields() {
1249 let fun = |x: &ArrayView1<f64>| -> f64 { x[0].powi(2) + x[1].powi(2) };
1251
1252 let ineq_con =
1254 |x: &ArrayView1<f64>| -> Array1<f64> { Array1::from_vec(vec![10.0 - x[0] - x[1]]) };
1255
1256 let ineq_jac = |_x: &ArrayView1<f64>| -> Array2<f64> {
1257 Array2::from_shape_vec((1, 2), vec![-1.0, -1.0]).expect("Operation failed")
1258 };
1259
1260 let x0 = Array1::from_vec(vec![1.0, 1.0]);
1261 let options = InteriorPointOptions::default();
1262
1263 let result = minimize_interior_point(
1264 fun,
1265 x0,
1266 None,
1267 None,
1268 Some(ineq_con),
1269 Some(ineq_jac),
1270 Some(options),
1271 );
1272
1273 match result {
1274 Ok(res) => {
1275 assert!(res.nit > 0);
1277 assert!(res.nfev > 0);
1278 assert!(res.fun.is_finite());
1279 assert!(!res.message.is_empty());
1280 }
1281 Err(_) => {
1282 }
1284 }
1285 }
1286
1287 #[test]
1288 fn test_interior_point_multiple_constraints() {
1289 let fun = |x: &ArrayView1<f64>| -> f64 { x[0].powi(2) + x[1].powi(2) };
1293
1294 let ineq_con = |x: &ArrayView1<f64>| -> Array1<f64> {
1295 Array1::from_vec(vec![
1296 1.0 - x[0] - x[1], 1.0 - x[0] + x[1], ])
1299 };
1300
1301 let ineq_jac = |_x: &ArrayView1<f64>| -> Array2<f64> {
1302 Array2::from_shape_vec((2, 2), vec![-1.0, -1.0, -1.0, 1.0]).expect("Operation failed")
1303 };
1304
1305 let x0 = Array1::from_vec(vec![0.3, 0.3]);
1306 let mut options = InteriorPointOptions::default();
1307 options.regularization = 1e-4;
1308 options.tol = 1e-3;
1309
1310 let result = minimize_interior_point(
1311 fun,
1312 x0,
1313 None,
1314 None,
1315 Some(ineq_con),
1316 Some(ineq_jac),
1317 Some(options),
1318 );
1319
1320 match result {
1321 Ok(res) => {
1322 assert!(res.nit > 0);
1324 }
1325 Err(_) => {
1326 }
1328 }
1329 }
1330
1331 #[test]
1332 fn test_interior_point_3d_problem() {
1333 let fun = |x: &ArrayView1<f64>| -> f64 { x[0].powi(2) + x[1].powi(2) + x[2].powi(2) };
1335
1336 let ineq_con = |x: &ArrayView1<f64>| -> Array1<f64> {
1337 Array1::from_vec(vec![3.0 - x[0] - x[1] - x[2]])
1338 };
1339
1340 let ineq_jac = |_x: &ArrayView1<f64>| -> Array2<f64> {
1341 Array2::from_shape_vec((1, 3), vec![-1.0, -1.0, -1.0]).expect("Operation failed")
1342 };
1343
1344 let x0 = Array1::from_vec(vec![0.5, 0.5, 0.5]);
1345 let mut options = InteriorPointOptions::default();
1346 options.regularization = 1e-4;
1347 options.tol = 1e-3;
1348
1349 let result = minimize_interior_point(
1350 fun,
1351 x0,
1352 None,
1353 None,
1354 Some(ineq_con),
1355 Some(ineq_jac),
1356 Some(options),
1357 );
1358
1359 match result {
1360 Ok(res) => {
1361 if res.success {
1362 assert!(
1364 res.fun < 5.0,
1365 "Should find reasonable solution, got {}",
1366 res.fun
1367 );
1368 }
1369 assert!(res.nit > 0);
1370 }
1371 Err(_) => {
1372 }
1374 }
1375 }
1376
1377 #[test]
1378 fn test_interior_point_constrained_helper() {
1379 use crate::constrained::{Constraint, ConstraintKind};
1381
1382 let func = |x: &[f64]| -> f64 { x[0].powi(2) + x[1].powi(2) };
1383
1384 fn ineq_constraint(x: &[f64]) -> f64 {
1385 1.0 - x[0] - x[1] }
1387
1388 let x0 = Array1::from_vec(vec![0.1, 0.1]);
1389 let constraints = vec![Constraint::new(ineq_constraint, ConstraintKind::Inequality)];
1390
1391 let options = InteriorPointOptions {
1392 tol: 1e-3,
1393 max_iter: 50,
1394 regularization: 1e-4,
1395 ..Default::default()
1396 };
1397
1398 let result = minimize_interior_point_constrained(func, x0, &constraints, Some(options));
1399 assert!(result.is_ok() || result.is_err());
1401 }
1402
1403 #[test]
1404 fn test_interior_point_barrier_reduction() {
1405 let opts = InteriorPointOptions {
1407 initial_barrier: 10.0,
1408 barrier_reduction: 0.1,
1409 min_barrier: 1e-10,
1410 ..Default::default()
1411 };
1412
1413 let barrier_after_one_step = opts.initial_barrier * opts.barrier_reduction;
1414 assert!(barrier_after_one_step < opts.initial_barrier);
1415 assert!(barrier_after_one_step > opts.min_barrier);
1416 }
1417}