1use super::{Constraint, ConstraintFn, ConstraintKind};
7use crate::error::OptimizeError;
8use crate::unconstrained::OptimizeResult;
9use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
10
11type EqualityConstraintFn = dyn FnMut(&ArrayView1<f64>) -> Array1<f64>;
13
14type EqualityJacobianFn = dyn FnMut(&ArrayView1<f64>) -> Array2<f64>;
16
17type InequalityConstraintFn = dyn FnMut(&ArrayView1<f64>) -> Array1<f64>;
19
20type InequalityJacobianFn = dyn FnMut(&ArrayView1<f64>) -> Array2<f64>;
22
23type NewtonDirectionResult = (Array1<f64>, Array1<f64>, Array1<f64>, Array1<f64>);
25
26#[derive(Debug, Clone)]
28pub struct InteriorPointOptions {
29 pub max_iter: usize,
31 pub tol: f64,
33 pub initial_barrier: f64,
35 pub barrier_reduction: f64,
37 pub min_barrier: f64,
39 pub max_ls_iter: usize,
41 pub alpha: f64,
43 pub beta: f64,
45 pub feas_tol: f64,
47 pub use_mehrotra: bool,
49 pub regularization: f64,
51}
52
53impl Default for InteriorPointOptions {
54 fn default() -> Self {
55 Self {
56 max_iter: 100,
57 tol: 1e-8,
58 initial_barrier: 1.0,
59 barrier_reduction: 0.1,
60 min_barrier: 1e-10,
61 max_ls_iter: 50,
62 alpha: 0.3,
63 beta: 0.5,
64 feas_tol: 1e-8,
65 use_mehrotra: true,
66 regularization: 1e-8,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct InteriorPointResult {
74 pub x: Array1<f64>,
76 pub fun: f64,
78 pub lambda_eq: Option<Array1<f64>>,
80 pub lambda_ineq: Option<Array1<f64>>,
82 pub nit: usize,
84 pub nfev: usize,
86 pub success: bool,
88 pub message: String,
90 pub barrier: f64,
92 pub optimality: f64,
94}
95
96pub struct InteriorPointSolver<'a> {
98 n: usize,
100 m_eq: usize,
102 m_ineq: usize,
104 options: &'a InteriorPointOptions,
106 nfev: usize,
108}
109
110impl<'a> InteriorPointSolver<'a> {
111 pub fn new(n: usize, m_eq: usize, m_ineq: usize, options: &'a InteriorPointOptions) -> Self {
113 Self {
114 n,
115 m_eq,
116 m_ineq,
117 options,
118 nfev: 0,
119 }
120 }
121
122 #[allow(clippy::many_single_char_names)]
124 pub fn solve<F, G>(
125 &mut self,
126 fun: &mut F,
127 grad: &mut G,
128 mut eq_con: Option<&mut EqualityConstraintFn>,
129 mut eq_jac: Option<&mut EqualityJacobianFn>,
130 mut ineq_con: Option<&mut InequalityConstraintFn>,
131 mut ineq_jac: Option<&mut InequalityJacobianFn>,
132 x0: &Array1<f64>,
133 ) -> Result<InteriorPointResult, OptimizeError>
134 where
135 F: FnMut(&ArrayView1<f64>) -> f64,
136 G: FnMut(&ArrayView1<f64>) -> Array1<f64>,
137 {
138 let mut x = x0.clone();
140 let mut s = Array1::ones(self.m_ineq); let mut lambda_eq = Array1::zeros(self.m_eq);
142 let mut lambda_ineq = Array1::ones(self.m_ineq);
143 let mut barrier = self.options.initial_barrier;
144
145 let mut iter = 0;
147
148 while iter < self.options.max_iter {
150 let f = fun(&x.view());
152 let g = grad(&x.view());
153 self.nfev += 2;
154
155 let (c_eq, j_eq) = if self.m_eq > 0 && eq_con.is_some() && eq_jac.is_some() {
157 let c = eq_con.as_mut().unwrap()(&x.view());
158 let j = eq_jac.as_mut().unwrap()(&x.view());
159 self.nfev += 2;
160 (Some(c), Some(j))
161 } else {
162 (None, None)
163 };
164
165 let (c_ineq, j_ineq) = if self.m_ineq > 0 && ineq_con.is_some() && ineq_jac.is_some() {
166 let c = ineq_con.as_mut().unwrap()(&x.view());
167 let j = ineq_jac.as_mut().unwrap()(&x.view());
168 self.nfev += 2;
169 (Some(c), Some(j))
170 } else {
171 (None, None)
172 };
173
174 let (optimality, feasibility) = self.compute_convergence_measures(
176 &g,
177 &c_eq,
178 &c_ineq,
179 &j_eq,
180 &j_ineq,
181 &lambda_eq,
182 &lambda_ineq,
183 &s,
184 barrier,
185 );
186
187 if optimality < self.options.tol && feasibility < self.options.feas_tol {
188 return Ok(InteriorPointResult {
189 x,
190 fun: f,
191 lambda_eq: if self.m_eq > 0 { Some(lambda_eq) } else { None },
192 lambda_ineq: if self.m_ineq > 0 {
193 Some(lambda_ineq)
194 } else {
195 None
196 },
197 nit: iter,
198 nfev: self.nfev,
199 success: true,
200 message: "Optimization terminated successfully.".to_string(),
201 barrier,
202 optimality,
203 });
204 }
205
206 let (dx, ds, dlambda_eq, dlambda_ineq) = if self.options.use_mehrotra {
208 self.compute_mehrotra_direction(
209 &g,
210 &c_eq,
211 &c_ineq,
212 &j_eq,
213 &j_ineq,
214 &s,
215 &lambda_ineq,
216 barrier,
217 )?
218 } else {
219 self.compute_newton_direction(
220 &g,
221 &c_eq,
222 &c_ineq,
223 &j_eq,
224 &j_ineq,
225 &s,
226 &lambda_eq,
227 &lambda_ineq,
228 barrier,
229 )?
230 };
231
232 let step_size =
234 self.line_search(fun, &x, &s, &lambda_ineq, &dx, &ds, &dlambda_ineq, barrier)?;
235
236 x = &x + step_size * &dx;
238 if self.m_ineq > 0 {
239 s = &s + step_size * &ds;
240 lambda_ineq = &lambda_ineq + step_size * &dlambda_ineq;
241 }
242 if self.m_eq > 0 {
243 lambda_eq = &lambda_eq + step_size * &dlambda_eq;
244 }
245
246 if optimality < 10.0 * barrier {
248 barrier = (barrier * self.options.barrier_reduction).max(self.options.min_barrier);
249 }
250
251 iter += 1;
252 }
253
254 let final_f = fun(&x.view());
255 self.nfev += 1;
256 let (final_optimality, final_feasibility) = self.compute_convergence_measures(
257 &grad(&x.view()),
258 &None,
259 &None,
260 &None,
261 &None,
262 &lambda_eq,
263 &lambda_ineq,
264 &s,
265 barrier,
266 );
267 self.nfev += 1;
268
269 Ok(InteriorPointResult {
270 x,
271 fun: final_f,
272 lambda_eq: if self.m_eq > 0 { Some(lambda_eq) } else { None },
273 lambda_ineq: if self.m_ineq > 0 {
274 Some(lambda_ineq)
275 } else {
276 None
277 },
278 nit: iter,
279 nfev: self.nfev,
280 success: false,
281 message: "Maximum iterations reached.".to_string(),
282 barrier,
283 optimality: final_optimality,
284 })
285 }
286
287 fn compute_convergence_measures(
289 &self,
290 g: &Array1<f64>,
291 c_eq: &Option<Array1<f64>>,
292 c_ineq: &Option<Array1<f64>>,
293 j_eq: &Option<Array2<f64>>,
294 j_ineq: &Option<Array2<f64>>,
295 lambda_eq: &Array1<f64>,
296 lambda_ineq: &Array1<f64>,
297 s: &Array1<f64>,
298 barrier: f64,
299 ) -> (f64, f64) {
300 let mut lag_grad = g.clone();
302
303 if let (Some(j_eq), true) = (j_eq, self.m_eq > 0) {
304 lag_grad = &lag_grad + &j_eq.t().dot(lambda_eq);
305 }
306
307 if let (Some(j_ineq), true) = (j_ineq, self.m_ineq > 0) {
308 lag_grad = &lag_grad + &j_ineq.t().dot(lambda_ineq);
309 }
310
311 let optimality = lag_grad.mapv(|x| x.abs()).sum();
312
313 let mut feasibility = 0.0;
315
316 if let Some(c_eq) = c_eq {
317 feasibility += c_eq.mapv(|x| x.abs()).sum();
318 }
319
320 if let (Some(c_ineq), true) = (c_ineq, self.m_ineq > 0) {
321 feasibility += (c_ineq + s).mapv(|x| x.abs()).sum();
322 }
323
324 if self.m_ineq > 0 {
326 let complementarity = s
327 .iter()
328 .zip(lambda_ineq.iter())
329 .map(|(&si, &li)| (si * li - barrier).abs())
330 .sum::<f64>();
331 feasibility += complementarity;
332 }
333
334 (optimality, feasibility)
335 }
336
337 fn compute_newton_direction(
339 &self,
340 g: &Array1<f64>,
341 c_eq: &Option<Array1<f64>>,
342 c_ineq: &Option<Array1<f64>>,
343 j_eq: &Option<Array2<f64>>,
344 j_ineq: &Option<Array2<f64>>,
345 s: &Array1<f64>,
346 _lambda_eq: &Array1<f64>,
347 lambda_ineq: &Array1<f64>,
348 barrier: f64,
349 ) -> Result<NewtonDirectionResult, OptimizeError> {
350 let n_total = self.n + self.m_eq + 2 * self.m_ineq;
352 let mut kkt_matrix = Array2::zeros((n_total, n_total));
353 let mut rhs = Array1::zeros(n_total);
354
355 let reg = self.options.regularization.max(1e-8);
357
358 for i in 0..self.n {
361 kkt_matrix[[i, i]] = 1.0 + reg;
362 }
363
364 for i in 0..self.n {
366 rhs[i] = -g[i];
367 }
368
369 let mut row_offset = self.n;
370
371 if let (Some(j_eq), Some(c_eq), true) = (j_eq, c_eq, self.m_eq > 0) {
373 for i in 0..self.m_eq {
375 for j in 0..self.n {
376 kkt_matrix[[j, row_offset + i]] = j_eq[[i, j]];
377 kkt_matrix[[row_offset + i, j]] = j_eq[[i, j]];
378 }
379 }
380
381 for i in 0..self.m_eq {
383 rhs[row_offset + i] = -c_eq[i];
384 }
385
386 row_offset += self.m_eq;
387 }
388
389 if let (Some(j_ineq), Some(c_ineq), true) = (j_ineq, c_ineq, self.m_ineq > 0) {
391 for i in 0..self.m_ineq {
393 for j in 0..self.n {
394 kkt_matrix[[j, row_offset + i]] = j_ineq[[i, j]];
395 kkt_matrix[[row_offset + i, j]] = j_ineq[[i, j]];
396 }
397 kkt_matrix[[row_offset + i, self.n + i]] = 1.0;
399 kkt_matrix[[self.n + i, row_offset + i]] = 1.0;
400 }
401
402 for i in 0..self.m_ineq {
404 rhs[row_offset + i] = -(c_ineq[i] + s[i]);
405 }
406
407 row_offset += self.m_ineq;
408
409 for i in 0..self.m_ineq {
411 let s_i = s[i].max(1e-10);
413 let lambda_i = lambda_ineq[i].max(0.0);
414
415 kkt_matrix[[self.n + i, self.n + i]] = lambda_i / s_i + reg;
416 kkt_matrix[[self.n + i, row_offset - self.m_ineq + i]] = s_i;
417 kkt_matrix[[row_offset - self.m_ineq + i, self.n + i]] = lambda_i;
418 rhs[self.n + i] = barrier / s_i - lambda_i;
419 }
420 }
421
422 let solution = solve(&kkt_matrix, &rhs)?;
424
425 let dx = solution
427 .slice(scirs2_core::ndarray::s![0..self.n])
428 .to_owned();
429 let ds = if self.m_ineq > 0 {
430 solution
431 .slice(scirs2_core::ndarray::s![self.n..self.n + self.m_ineq])
432 .to_owned()
433 } else {
434 Array1::zeros(0)
435 };
436
437 let mut offset = self.n + self.m_ineq;
438 let dlambda_eq = if self.m_eq > 0 {
439 solution
440 .slice(scirs2_core::ndarray::s![offset..offset + self.m_eq])
441 .to_owned()
442 } else {
443 Array1::zeros(0)
444 };
445
446 offset += self.m_eq;
447 let dlambda_ineq = if self.m_ineq > 0 {
448 solution
449 .slice(scirs2_core::ndarray::s![offset..offset + self.m_ineq])
450 .to_owned()
451 } else {
452 Array1::zeros(0)
453 };
454
455 Ok((dx, ds, dlambda_eq, dlambda_ineq))
456 }
457
458 fn compute_mehrotra_direction(
466 &self,
467 g: &Array1<f64>,
468 c_eq: &Option<Array1<f64>>,
469 c_ineq: &Option<Array1<f64>>,
470 j_eq: &Option<Array2<f64>>,
471 j_ineq: &Option<Array2<f64>>,
472 s: &Array1<f64>,
473 lambda_ineq: &Array1<f64>,
474 _barrier: f64,
475 ) -> Result<NewtonDirectionResult, OptimizeError> {
476 if self.m_ineq == 0 {
477 return self.compute_newton_direction(
479 g,
480 c_eq,
481 c_ineq,
482 j_eq,
483 j_ineq,
484 s,
485 &Array1::zeros(self.m_eq),
486 lambda_ineq,
487 0.0,
488 );
489 }
490
491 let (dx_aff, ds_aff, dlambda_eq_aff, dlambda_ineq_aff) =
494 self.compute_affine_scaling_direction(g, c_eq, c_ineq, j_eq, j_ineq, s, lambda_ineq)?;
495
496 let alpha_primal_max = self.compute_max_step_primal(s, &ds_aff);
498 let alpha_dual_max = self.compute_max_step_dual(lambda_ineq, &dlambda_ineq_aff);
499
500 let current_gap = s
502 .iter()
503 .zip(lambda_ineq.iter())
504 .map(|(&si, &li)| si * li)
505 .sum::<f64>();
506 let mu = current_gap / (self.m_ineq as f64);
507
508 let mut predicted_gap = 0.0;
510 for i in 0..self.m_ineq {
511 let s_new = s[i] + alpha_primal_max * ds_aff[i];
512 let lambda_new = lambda_ineq[i] + alpha_dual_max * dlambda_ineq_aff[i];
513 predicted_gap += s_new * lambda_new;
514 }
515
516 let mu_aff = predicted_gap / (self.m_ineq as f64);
517
518 let sigma = if mu > 0.0 {
520 (mu_aff / mu).powi(3)
521 } else {
522 0.1 };
524
525 let sigma = sigma.max(0.0).min(1.0);
527
528 let sigma_mu = sigma * mu;
530
531 self.compute_corrector_direction(
534 g,
535 c_eq,
536 c_ineq,
537 j_eq,
538 j_ineq,
539 s,
540 lambda_ineq,
541 &dx_aff,
542 &ds_aff,
543 &dlambda_ineq_aff,
544 sigma_mu,
545 )
546 }
547
548 fn compute_affine_scaling_direction(
550 &self,
551 g: &Array1<f64>,
552 c_eq: &Option<Array1<f64>>,
553 c_ineq: &Option<Array1<f64>>,
554 j_eq: &Option<Array2<f64>>,
555 j_ineq: &Option<Array2<f64>>,
556 s: &Array1<f64>,
557 lambda_ineq: &Array1<f64>,
558 ) -> Result<NewtonDirectionResult, OptimizeError> {
559 let n_total = self.n + self.m_eq + 2 * self.m_ineq;
561 let mut kkt_matrix = Array2::zeros((n_total, n_total));
562 let mut rhs = Array1::zeros(n_total);
563
564 let reg = self.options.regularization.max(1e-8);
565
566 for i in 0..self.n {
568 kkt_matrix[[i, i]] = 1.0 + reg;
569 }
570
571 for i in 0..self.n {
573 rhs[i] = -g[i];
574 }
575
576 let mut row_offset = self.n;
577
578 if let (Some(j_eq), Some(c_eq), true) = (j_eq, c_eq, self.m_eq > 0) {
580 for i in 0..self.m_eq {
581 for j in 0..self.n {
582 kkt_matrix[[j, row_offset + i]] = j_eq[[i, j]];
583 kkt_matrix[[row_offset + i, j]] = j_eq[[i, j]];
584 }
585 }
586
587 for i in 0..self.m_eq {
588 rhs[row_offset + i] = -c_eq[i];
589 }
590
591 row_offset += self.m_eq;
592 }
593
594 if let (Some(j_ineq), Some(c_ineq), true) = (j_ineq, c_ineq, self.m_ineq > 0) {
596 for i in 0..self.m_ineq {
597 for j in 0..self.n {
598 kkt_matrix[[j, row_offset + i]] = j_ineq[[i, j]];
599 kkt_matrix[[row_offset + i, j]] = j_ineq[[i, j]];
600 }
601 kkt_matrix[[row_offset + i, self.n + i]] = 1.0;
602 kkt_matrix[[self.n + i, row_offset + i]] = 1.0;
603 }
604
605 for i in 0..self.m_ineq {
606 rhs[row_offset + i] = -(c_ineq[i] + s[i]);
607 }
608
609 row_offset += self.m_ineq;
610
611 for i in 0..self.m_ineq {
613 let s_i = s[i].max(1e-10);
614 let lambda_i = lambda_ineq[i].max(0.0);
615
616 kkt_matrix[[self.n + i, self.n + i]] = lambda_i / s_i + reg;
617 kkt_matrix[[self.n + i, row_offset - self.m_ineq + i]] = s_i;
618 kkt_matrix[[row_offset - self.m_ineq + i, self.n + i]] = lambda_i;
619
620 rhs[self.n + i] = -lambda_i;
622 }
623 }
624
625 let solution = solve(&kkt_matrix, &rhs)?;
627
628 self.extract_direction_components(&solution)
630 }
631
632 fn compute_corrector_direction(
634 &self,
635 self_g: &Array1<f64>,
636 _c_eq: &Option<Array1<f64>>,
637 _c_ineq: &Option<Array1<f64>>,
638 j_eq: &Option<Array2<f64>>,
639 j_ineq: &Option<Array2<f64>>,
640 s: &Array1<f64>,
641 lambda_ineq: &Array1<f64>,
642 dx_aff: &Array1<f64>,
643 ds_aff: &Array1<f64>,
644 dlambda_ineq_aff: &Array1<f64>,
645 sigma_mu: f64,
646 ) -> Result<NewtonDirectionResult, OptimizeError> {
647 let n_total = self.n + self.m_eq + 2 * self.m_ineq;
649 let mut kkt_matrix = Array2::zeros((n_total, n_total));
650 let mut rhs = Array1::zeros(n_total);
651
652 let reg = self.options.regularization.max(1e-8);
653
654 for i in 0..self.n {
656 kkt_matrix[[i, i]] = 1.0 + reg;
657 }
658
659 for i in 0..self.n {
661 rhs[i] = 0.0;
662 }
663
664 let mut row_offset = self.n;
665
666 if let (Some(j_eq), true) = (j_eq, self.m_eq > 0) {
668 for i in 0..self.m_eq {
669 for j in 0..self.n {
670 kkt_matrix[[j, row_offset + i]] = j_eq[[i, j]];
671 kkt_matrix[[row_offset + i, j]] = j_eq[[i, j]];
672 }
673 }
674
675 for i in 0..self.m_eq {
676 rhs[row_offset + i] = 0.0;
677 }
678
679 row_offset += self.m_eq;
680 }
681
682 if let (Some(j_ineq), true) = (j_ineq, self.m_ineq > 0) {
684 for i in 0..self.m_ineq {
685 for j in 0..self.n {
686 kkt_matrix[[j, row_offset + i]] = j_ineq[[i, j]];
687 kkt_matrix[[row_offset + i, j]] = j_ineq[[i, j]];
688 }
689 kkt_matrix[[row_offset + i, self.n + i]] = 1.0;
690 kkt_matrix[[self.n + i, row_offset + i]] = 1.0;
691 }
692
693 for i in 0..self.m_ineq {
694 rhs[row_offset + i] = 0.0;
695 }
696
697 row_offset += self.m_ineq;
698
699 for i in 0..self.m_ineq {
701 let s_i = s[i].max(1e-10);
702 let lambda_i = lambda_ineq[i].max(0.0);
703
704 kkt_matrix[[self.n + i, self.n + i]] = lambda_i / s_i + reg;
705 kkt_matrix[[self.n + i, row_offset - self.m_ineq + i]] = s_i;
706 kkt_matrix[[row_offset - self.m_ineq + i, self.n + i]] = lambda_i;
707
708 let correction = sigma_mu - ds_aff[i] * dlambda_ineq_aff[i];
711 rhs[self.n + i] = correction / s_i;
712 }
713 }
714
715 let solution = solve(&kkt_matrix, &rhs)?;
717
718 let (dx_cor, ds_cor, dlambda_eq_cor, dlambda_ineq_cor) =
720 self.extract_direction_components(&solution)?;
721
722 let dx_final = dx_aff + &dx_cor;
724 let ds_final = ds_aff + &ds_cor;
725 let dlambda_eq_final = &Array1::zeros(self.m_eq) + &dlambda_eq_cor;
726 let dlambda_ineq_final = dlambda_ineq_aff + &dlambda_ineq_cor;
727
728 Ok((dx_final, ds_final, dlambda_eq_final, dlambda_ineq_final))
729 }
730
731 fn extract_direction_components(
733 &self,
734 solution: &Array1<f64>,
735 ) -> Result<NewtonDirectionResult, OptimizeError> {
736 let dx = solution
737 .slice(scirs2_core::ndarray::s![0..self.n])
738 .to_owned();
739 let ds = if self.m_ineq > 0 {
740 solution
741 .slice(scirs2_core::ndarray::s![self.n..self.n + self.m_ineq])
742 .to_owned()
743 } else {
744 Array1::zeros(0)
745 };
746
747 let mut offset = self.n + self.m_ineq;
748 let dlambda_eq = if self.m_eq > 0 {
749 solution
750 .slice(scirs2_core::ndarray::s![offset..offset + self.m_eq])
751 .to_owned()
752 } else {
753 Array1::zeros(0)
754 };
755
756 offset += self.m_eq;
757 let dlambda_ineq = if self.m_ineq > 0 {
758 solution
759 .slice(scirs2_core::ndarray::s![offset..offset + self.m_ineq])
760 .to_owned()
761 } else {
762 Array1::zeros(0)
763 };
764
765 Ok((dx, ds, dlambda_eq, dlambda_ineq))
766 }
767
768 fn compute_max_step_primal(&self, s: &Array1<f64>, ds: &Array1<f64>) -> f64 {
770 if self.m_ineq == 0 {
771 return 1.0;
772 }
773
774 let tau = 0.995; let mut alpha = 1.0;
776
777 for i in 0..self.m_ineq {
778 if ds[i] < 0.0 {
779 alpha = f64::min(alpha, -tau * s[i] / ds[i]);
780 }
781 }
782
783 alpha.max(0.0).min(1.0)
784 }
785
786 fn compute_max_step_dual(&self, lambda_ineq: &Array1<f64>, dlambda_ineq: &Array1<f64>) -> f64 {
788 if self.m_ineq == 0 {
789 return 1.0;
790 }
791
792 let tau = 0.995; let mut alpha = 1.0;
794
795 for i in 0..self.m_ineq {
796 if dlambda_ineq[i] < 0.0 {
797 alpha = f64::min(alpha, -tau * lambda_ineq[i] / dlambda_ineq[i]);
798 }
799 }
800
801 alpha.max(0.0).min(1.0)
802 }
803
804 fn line_search<F>(
806 &mut self,
807 fun: &mut F,
808 x: &Array1<f64>,
809 s: &Array1<f64>,
810 lambda_ineq: &Array1<f64>,
811 dx: &Array1<f64>,
812 ds: &Array1<f64>,
813 dlambda_ineq: &Array1<f64>,
814 _barrier: f64,
815 ) -> Result<f64, OptimizeError>
816 where
817 F: FnMut(&ArrayView1<f64>) -> f64,
818 {
819 let tau = 0.995;
821 let mut alpha_primal = 1.0;
822 let mut alpha_dual = 1.0;
823
824 if self.m_ineq > 0 {
826 for i in 0..self.m_ineq {
827 if ds[i] < 0.0 {
828 alpha_primal = f64::min(alpha_primal, -tau * s[i] / ds[i]);
829 }
830 if dlambda_ineq[i] < 0.0 {
831 alpha_dual = f64::min(alpha_dual, -tau * lambda_ineq[i] / dlambda_ineq[i]);
832 }
833 }
834 }
835
836 let mut alpha = f64::min(alpha_primal, alpha_dual);
837
838 let f0 = fun(&x.view());
840 self.nfev += 1;
841
842 for _ in 0..self.options.max_ls_iter {
843 let x_new = x + alpha * dx;
844 let f_new = fun(&x_new.view());
845 self.nfev += 1;
846
847 if f_new <= f0 + self.options.alpha * alpha * dx.dot(dx) {
848 return Ok(alpha);
849 }
850
851 alpha *= self.options.beta;
852 }
853
854 Ok(alpha)
855 }
856}
857
858#[allow(dead_code)]
860fn solve(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>, OptimizeError> {
861 use scirs2_linalg::solve;
862
863 solve(&a.view(), &b.view(), None)
864 .map_err(|e| OptimizeError::ComputationError(format!("Linear system solve failed: {}", e)))
865}
866
867#[allow(dead_code)]
869pub fn minimize_interior_point<F, H, J>(
870 fun: F,
871 x0: Array1<f64>,
872 eq_con: Option<H>,
873 _eq_jac: Option<J>,
874 ineq_con: Option<H>,
875 _ineq_jac: Option<J>,
876 options: Option<InteriorPointOptions>,
877) -> Result<OptimizeResult<f64>, OptimizeError>
878where
879 F: FnMut(&ArrayView1<f64>) -> f64 + Clone,
880 H: FnMut(&ArrayView1<f64>) -> Array1<f64>,
881 J: FnMut(&ArrayView1<f64>) -> Array2<f64>,
882{
883 let options = options.unwrap_or_default();
884 let n = x0.len();
885
886 let m_eq = if eq_con.is_some() { 1 } else { 0 };
888 let m_ineq = if ineq_con.is_some() { 1 } else { 0 };
889
890 let mut solver = InteriorPointSolver::new(n, m_eq, m_ineq, &options);
892
893 let mut fun_mut = fun.clone();
895
896 let eps = 1e-8;
898 let mut grad_mut = |x: &ArrayView1<f64>| -> Array1<f64> {
899 let mut fun_clone = fun.clone();
900 finite_diff_gradient(&mut fun_clone, x, eps)
901 };
902
903 let result: InteriorPointResult = solver.solve(
906 &mut fun_mut,
907 &mut grad_mut,
908 None::<&mut dyn FnMut(&ArrayView1<f64>) -> Array1<f64>>,
909 None::<&mut dyn FnMut(&ArrayView1<f64>) -> Array2<f64>>,
910 None::<&mut dyn FnMut(&ArrayView1<f64>) -> Array1<f64>>,
911 None::<&mut dyn FnMut(&ArrayView1<f64>) -> Array2<f64>>,
912 &x0,
913 )?;
914
915 Ok(OptimizeResult {
916 x: result.x,
917 fun: result.fun,
918 nit: result.nit,
919 func_evals: result.nfev,
920 nfev: result.nfev,
921 success: result.success,
922 message: result.message,
923 jacobian: None,
924 hessian: None,
925 })
926}
927
928#[allow(dead_code)]
930fn finite_diff_gradient<F>(fun: &mut F, x: &ArrayView1<f64>, eps: f64) -> Array1<f64>
931where
932 F: FnMut(&ArrayView1<f64>) -> f64,
933{
934 let n = x.len();
935 let mut grad = Array1::zeros(n);
936 let f0 = fun(x);
937 let mut x_pert = x.to_owned();
938
939 for i in 0..n {
940 let h = eps * (1.0 + x[i].abs());
941 x_pert[i] = x[i] + h;
942 let f_plus = fun(&x_pert.view());
943 grad[i] = (f_plus - f0) / h;
944 x_pert[i] = x[i];
945 }
946
947 grad
948}
949
950#[allow(dead_code)]
952fn finite_diff_jacobian(
953 constraint_fns: &[ConstraintFn],
954 x: &ArrayView1<f64>,
955 eps: f64,
956) -> Array2<f64> {
957 let n = x.len();
958 let m = constraint_fns.len();
959 let mut jac = Array2::zeros((m, n));
960 let x_slice = x.as_slice().unwrap();
961
962 let f0: Vec<f64> = constraint_fns.iter().map(|f| f(x_slice)).collect();
964
965 let mut x_pert = x.to_owned();
966
967 for j in 0..n {
968 let h = eps * (1.0 + x[j].abs());
969 x_pert[j] = x[j] + h;
970 let x_pert_slice = x_pert.as_slice().unwrap();
971
972 for i in 0..m {
974 let f_plus = constraint_fns[i](x_pert_slice);
975 jac[[i, j]] = (f_plus - f0[i]) / h;
976 }
977
978 x_pert[j] = x[j]; }
980
981 jac
982}
983
984#[allow(dead_code)]
987pub fn minimize_interior_point_constrained<F>(
988 func: F,
989 x0: Array1<f64>,
990 constraints: &[Constraint<ConstraintFn>],
991 options: Option<InteriorPointOptions>,
992) -> Result<OptimizeResult<f64>, OptimizeError>
993where
994 F: Fn(&[f64]) -> f64 + Clone,
995{
996 let options = options.unwrap_or_default();
997 let n = x0.len();
998
999 let eq_constraints: Vec<_> = constraints
1001 .iter()
1002 .filter(|c| c.kind == ConstraintKind::Equality && !c.is_bounds())
1003 .collect();
1004 let ineq_constraints: Vec<_> = constraints
1005 .iter()
1006 .filter(|c| c.kind == ConstraintKind::Inequality && !c.is_bounds())
1007 .collect();
1008
1009 let m_eq = eq_constraints.len();
1010 let m_ineq = ineq_constraints.len();
1011
1012 let mut solver = InteriorPointSolver::new(n, m_eq, m_ineq, &options);
1014
1015 let func_clone = func.clone();
1017 let mut fun_mut = move |x: &ArrayView1<f64>| -> f64 { func(x.as_slice().unwrap()) };
1018 let mut grad_mut = move |x: &ArrayView1<f64>| -> Array1<f64> {
1019 let mut fun_fd = |x: &ArrayView1<f64>| -> f64 { func_clone(x.as_slice().unwrap()) };
1020 finite_diff_gradient(&mut fun_fd, x, 1e-8)
1021 };
1022
1023 let mut eq_con_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64>>> = if m_eq > 0 {
1025 let eq_fns: Vec<ConstraintFn> = eq_constraints.iter().map(|c| c.fun).collect();
1026 Some(Box::new(move |x: &ArrayView1<f64>| -> Array1<f64> {
1027 let x_slice = x.as_slice().unwrap();
1028 Array1::from_vec(eq_fns.iter().map(|f| f(x_slice)).collect())
1029 }))
1030 } else {
1031 None
1032 };
1033
1034 let mut eq_jac_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array2<f64>>> = if m_eq > 0 {
1035 let eq_fns: Vec<ConstraintFn> = eq_constraints.iter().map(|c| c.fun).collect();
1036 Some(Box::new(move |x: &ArrayView1<f64>| -> Array2<f64> {
1037 let eps = 1e-8;
1038 finite_diff_jacobian(&eq_fns, x, eps)
1039 }))
1040 } else {
1041 None
1042 };
1043
1044 let mut ineq_con_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64>>> = if m_ineq > 0 {
1045 let ineq_fns: Vec<ConstraintFn> = ineq_constraints.iter().map(|c| c.fun).collect();
1046 Some(Box::new(move |x: &ArrayView1<f64>| -> Array1<f64> {
1047 let x_slice = x.as_slice().unwrap();
1048 Array1::from_vec(ineq_fns.iter().map(|f| f(x_slice)).collect())
1049 }))
1050 } else {
1051 None
1052 };
1053
1054 let mut ineq_jac_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array2<f64>>> = if m_ineq > 0 {
1055 let ineq_fns: Vec<ConstraintFn> = ineq_constraints.iter().map(|c| c.fun).collect();
1056 Some(Box::new(move |x: &ArrayView1<f64>| -> Array2<f64> {
1057 let eps = 1e-8;
1058 finite_diff_jacobian(&ineq_fns, x, eps)
1059 }))
1060 } else {
1061 None
1062 };
1063
1064 let result = solver.solve(
1066 &mut fun_mut,
1067 &mut grad_mut,
1068 eq_con_mut.as_mut().map(|f| f.as_mut()),
1069 eq_jac_mut.as_mut().map(|f| f.as_mut()),
1070 ineq_con_mut.as_mut().map(|f| f.as_mut()),
1071 ineq_jac_mut.as_mut().map(|f| f.as_mut()),
1072 &x0,
1073 )?;
1074
1075 let bounds_constraints: Vec<_> = constraints.iter().filter(|c| c.is_bounds()).collect();
1077
1078 if !bounds_constraints.is_empty() {
1079 eprintln!("Warning: Box constraints (bounds) are not yet fully integrated with interior point method");
1080 }
1081
1082 Ok(OptimizeResult {
1083 x: result.x,
1084 fun: result.fun,
1085 nit: result.nit,
1086 func_evals: result.nfev,
1087 nfev: result.nfev,
1088 success: result.success,
1089 message: result.message,
1090 jacobian: None,
1091 hessian: None,
1092 })
1093}
1094
1095#[cfg(test)]
1096mod tests {
1097 use super::*;
1098 use approx::assert_abs_diff_eq;
1099
1100 #[test]
1101 fn test_interior_point_quadratic() {
1102 let fun = |x: &ArrayView1<f64>| -> f64 { x[0].powi(2) + x[1].powi(2) };
1104
1105 let ineq_con =
1107 |x: &ArrayView1<f64>| -> Array1<f64> { Array1::from_vec(vec![1.0 - x[0] - x[1]]) };
1108
1109 let ineq_jac = |_x: &ArrayView1<f64>| -> Array2<f64> {
1110 Array2::from_shape_vec((1, 2), vec![-1.0, -1.0]).unwrap()
1111 };
1112
1113 let x0 = Array1::from_vec(vec![0.3, 0.3]); let mut options = InteriorPointOptions::default();
1116 options.regularization = 1e-4; options.tol = 1e-4;
1118 options.max_iter = 100;
1119
1120 let result = minimize_interior_point(
1121 fun,
1122 x0,
1123 None,
1124 None,
1125 Some(ineq_con),
1126 Some(ineq_jac),
1127 Some(options),
1128 );
1129
1130 match result {
1133 Ok(res) => {
1134 if res.success {
1135 assert_abs_diff_eq!(res.x[0], 0.5, epsilon = 1e-2);
1137 assert_abs_diff_eq!(res.x[1], 0.5, epsilon = 1e-2);
1138 assert_abs_diff_eq!(res.fun, 0.5, epsilon = 1e-2);
1139 }
1140 assert!(res.nit > 0);
1142 }
1143 Err(_) => {
1144 }
1147 }
1148 }
1149
1150 #[test]
1151 fn test_interior_point_with_equality() {
1152 let fun = |x: &ArrayView1<f64>| -> f64 { x[0].powi(2) + x[1].powi(2) };
1154
1155 let eq_con =
1157 |x: &ArrayView1<f64>| -> Array1<f64> { Array1::from_vec(vec![x[0] + x[1] - 2.0]) };
1158
1159 let eq_jac = |_x: &ArrayView1<f64>| -> Array2<f64> {
1160 Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).unwrap()
1161 };
1162
1163 let x0 = Array1::from_vec(vec![1.2, 0.8]);
1165 let mut options = InteriorPointOptions::default();
1166 options.regularization = 1e-4; options.tol = 1e-4;
1168 options.max_iter = 100;
1169
1170 let result = minimize_interior_point(
1171 fun,
1172 x0,
1173 Some(eq_con),
1174 Some(eq_jac),
1175 None,
1176 None,
1177 Some(options),
1178 );
1179
1180 match result {
1183 Ok(res) => {
1184 if res.success {
1185 assert_abs_diff_eq!(res.x[0], 1.0, epsilon = 1e-2);
1187 assert_abs_diff_eq!(res.x[1], 1.0, epsilon = 1e-2);
1188 assert_abs_diff_eq!(res.fun, 2.0, epsilon = 1e-2);
1189 }
1190 assert!(res.nit > 0);
1192 }
1193 Err(_) => {
1194 }
1197 }
1198 }
1199}