use super::{Constraint, ConstraintKind};
use crate::error::OptimizeError;
use crate::unconstrained::OptimizeResult;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
use std::cell::RefCell;
use std::rc::Rc;
type EqualityConstraintFn<'a> = dyn FnMut(&ArrayView1<f64>) -> Array1<f64> + 'a;
type EqualityJacobianFn<'a> = dyn FnMut(&ArrayView1<f64>) -> Array2<f64> + 'a;
type InequalityConstraintFn<'a> = dyn FnMut(&ArrayView1<f64>) -> Array1<f64> + 'a;
type InequalityJacobianFn<'a> = dyn FnMut(&ArrayView1<f64>) -> Array2<f64> + 'a;
type NewtonDirectionResult = (Array1<f64>, Array1<f64>, Array1<f64>, Array1<f64>);
#[derive(Debug, Clone)]
pub struct InteriorPointOptions {
pub max_iter: usize,
pub tol: f64,
pub initial_barrier: f64,
pub barrier_reduction: f64,
pub min_barrier: f64,
pub max_ls_iter: usize,
pub alpha: f64,
pub beta: f64,
pub feas_tol: f64,
pub use_mehrotra: bool,
pub regularization: f64,
}
impl Default for InteriorPointOptions {
fn default() -> Self {
Self {
max_iter: 100,
tol: 1e-8,
initial_barrier: 1.0,
barrier_reduction: 0.1,
min_barrier: 1e-10,
max_ls_iter: 50,
alpha: 0.3,
beta: 0.5,
feas_tol: 1e-8,
use_mehrotra: true,
regularization: 1e-8,
}
}
}
#[derive(Debug, Clone)]
pub struct InteriorPointResult {
pub x: Array1<f64>,
pub fun: f64,
pub lambda_eq: Option<Array1<f64>>,
pub lambda_ineq: Option<Array1<f64>>,
pub nit: usize,
pub nfev: usize,
pub success: bool,
pub message: String,
pub barrier: f64,
pub optimality: f64,
}
pub struct InteriorPointSolver<'a> {
n: usize,
m_eq: usize,
m_ineq: usize,
options: &'a InteriorPointOptions,
nfev: usize,
hessian_approx: Array2<f64>,
merit_penalty: f64,
}
impl<'a> InteriorPointSolver<'a> {
pub fn new(n: usize, m_eq: usize, m_ineq: usize, options: &'a InteriorPointOptions) -> Self {
Self {
n,
m_eq,
m_ineq,
options,
nfev: 0,
hessian_approx: Array2::eye(n),
merit_penalty: 1.0,
}
}
fn lagrangian_gradient(
&self,
g: &Array1<f64>,
j_eq: &Option<Array2<f64>>,
lambda_eq: &Array1<f64>,
j_ineq: &Option<Array2<f64>>,
lambda_ineq: &Array1<f64>,
) -> Array1<f64> {
let mut lag_grad = g.clone();
if let (Some(j_eq), true) = (j_eq, self.m_eq > 0) {
lag_grad = &lag_grad + &j_eq.t().dot(lambda_eq);
}
if let (Some(j_ineq), true) = (j_ineq, self.m_ineq > 0) {
lag_grad = &lag_grad + &j_ineq.t().dot(lambda_ineq);
}
lag_grad
}
fn update_hessian_bfgs(&mut self, s: &Array1<f64>, y: &Array1<f64>) {
let n = self.n;
if n == 0 {
return;
}
let hs = self.hessian_approx.dot(s);
let shs = s.dot(&hs);
if !(shs > 0.0) || !shs.is_finite() {
return;
}
let sty = s.dot(y);
let theta = if sty >= 0.2 * shs {
1.0
} else {
(0.8 * shs) / (shs - sty)
}
.clamp(0.0, 1.0);
let y_damped = if theta >= 1.0 {
y.clone()
} else {
theta * y + (1.0 - theta) * &hs
};
let sty_damped = s.dot(&y_damped);
if sty_damped.abs() < 1e-12 || !sty_damped.is_finite() {
return;
}
for i in 0..n {
for j in 0..n {
let updated = self.hessian_approx[[i, j]] - hs[i] * hs[j] / shs
+ y_damped[i] * y_damped[j] / sty_damped;
self.hessian_approx[[i, j]] = updated;
}
}
}
fn write_hessian_block(&self, kkt_matrix: &mut Array2<f64>, reg: f64) {
for i in 0..self.n {
for j in 0..self.n {
kkt_matrix[[i, j]] = self.hessian_approx[[i, j]];
}
kkt_matrix[[i, i]] += reg;
}
}
#[allow(clippy::many_single_char_names)]
pub fn solve<F, G>(
&mut self,
fun: &mut F,
grad: &mut G,
mut eq_con: Option<&mut EqualityConstraintFn<'_>>,
mut eq_jac: Option<&mut EqualityJacobianFn<'_>>,
mut ineq_con: Option<&mut InequalityConstraintFn<'_>>,
mut ineq_jac: Option<&mut InequalityJacobianFn<'_>>,
x0: &Array1<f64>,
) -> Result<InteriorPointResult, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> f64,
G: FnMut(&ArrayView1<f64>) -> Array1<f64> + ?Sized,
{
let mut x = x0.clone();
let mut s = Array1::ones(self.m_ineq); let mut lambda_eq = Array1::zeros(self.m_eq);
let mut lambda_ineq = Array1::ones(self.m_ineq);
let mut barrier = self.options.initial_barrier;
let mut iter = 0;
let mut prev_lag_grad: Option<Array1<f64>> = None;
let mut prev_x: Option<Array1<f64>> = None;
while iter < self.options.max_iter {
let f = fun(&x.view());
let g = grad(&x.view());
self.nfev += 2;
let (c_eq, j_eq) = if self.m_eq > 0 && eq_con.is_some() && eq_jac.is_some() {
let c = eq_con.as_mut().expect("Operation failed")(&x.view());
let j = eq_jac.as_mut().expect("Operation failed")(&x.view());
self.nfev += 2;
(Some(c), Some(j))
} else {
(None, None)
};
let (c_ineq, j_ineq) = if self.m_ineq > 0 && ineq_con.is_some() && ineq_jac.is_some() {
let c = ineq_con.as_mut().expect("Operation failed")(&x.view());
let j = ineq_jac.as_mut().expect("Operation failed")(&x.view());
self.nfev += 2;
(Some(c), Some(j))
} else {
(None, None)
};
let lag_grad = self.lagrangian_gradient(&g, &j_eq, &lambda_eq, &j_ineq, &lambda_ineq);
if let (Some(prev_grad), Some(prev_x)) = (prev_lag_grad.as_ref(), prev_x.as_ref()) {
let y = &lag_grad - prev_grad;
let step = &x - prev_x;
self.update_hessian_bfgs(&step, &y);
}
prev_lag_grad = Some(lag_grad);
prev_x = Some(x.clone());
let (optimality, feasibility) = self.compute_convergence_measures(
&g,
&c_eq,
&c_ineq,
&j_eq,
&j_ineq,
&lambda_eq,
&lambda_ineq,
&s,
);
if optimality < self.options.tol && feasibility < self.options.feas_tol {
return Ok(InteriorPointResult {
x,
fun: f,
lambda_eq: if self.m_eq > 0 { Some(lambda_eq) } else { None },
lambda_ineq: if self.m_ineq > 0 {
Some(lambda_ineq)
} else {
None
},
nit: iter,
nfev: self.nfev,
success: true,
message: "Optimization terminated successfully.".to_string(),
barrier,
optimality,
});
}
let (dx, ds, dlambda_eq, dlambda_ineq) = if self.options.use_mehrotra {
self.compute_mehrotra_direction(
&g,
&c_eq,
&c_ineq,
&j_eq,
&j_ineq,
&s,
&lambda_eq,
&lambda_ineq,
barrier,
)?
} else {
self.compute_newton_direction(
&g,
&c_eq,
&c_ineq,
&j_eq,
&j_ineq,
&s,
&lambda_eq,
&lambda_ineq,
barrier,
)?
};
let step_size = self.line_search(
fun,
eq_con.as_deref_mut(),
ineq_con.as_deref_mut(),
&x,
&s,
&lambda_ineq,
&dx,
&ds,
&dlambda_ineq,
&g,
&c_eq,
&c_ineq,
barrier,
)?;
x = &x + step_size * &dx;
if self.m_ineq > 0 {
s = &s + step_size * &ds;
lambda_ineq = &lambda_ineq + step_size * &dlambda_ineq;
}
if self.m_eq > 0 {
lambda_eq = &lambda_eq + step_size * &dlambda_eq;
}
barrier = self.update_barrier_parameter(barrier, &s, &lambda_ineq, optimality);
iter += 1;
}
let final_f = fun(&x.view());
self.nfev += 1;
let (final_optimality, final_feasibility) = self.compute_convergence_measures(
&grad(&x.view()),
&None,
&None,
&None,
&None,
&lambda_eq,
&lambda_ineq,
&s,
);
self.nfev += 1;
Ok(InteriorPointResult {
x,
fun: final_f,
lambda_eq: if self.m_eq > 0 { Some(lambda_eq) } else { None },
lambda_ineq: if self.m_ineq > 0 {
Some(lambda_ineq)
} else {
None
},
nit: iter,
nfev: self.nfev,
success: false,
message: "Maximum iterations reached.".to_string(),
barrier,
optimality: final_optimality,
})
}
fn compute_convergence_measures(
&self,
g: &Array1<f64>,
c_eq: &Option<Array1<f64>>,
c_ineq: &Option<Array1<f64>>,
j_eq: &Option<Array2<f64>>,
j_ineq: &Option<Array2<f64>>,
lambda_eq: &Array1<f64>,
lambda_ineq: &Array1<f64>,
s: &Array1<f64>,
) -> (f64, f64) {
let lag_grad = self.lagrangian_gradient(g, j_eq, lambda_eq, j_ineq, lambda_ineq);
let optimality = lag_grad.mapv(|x| x.abs()).sum();
let mut feasibility = 0.0;
if let Some(c_eq) = c_eq {
feasibility += c_eq.mapv(|x| x.abs()).sum();
}
if let (Some(c_ineq), true) = (c_ineq, self.m_ineq > 0) {
feasibility += (c_ineq + s).mapv(|x| x.abs()).sum();
}
if self.m_ineq > 0 {
let complementarity = s
.iter()
.zip(lambda_ineq.iter())
.map(|(&si, &li)| (si * li).abs())
.sum::<f64>();
feasibility += complementarity;
}
(optimality, feasibility)
}
fn update_barrier_parameter(
&self,
barrier: f64,
s: &Array1<f64>,
lambda_ineq: &Array1<f64>,
optimality: f64,
) -> f64 {
if !(optimality < 10.0 * barrier) {
return barrier;
}
if self.m_ineq == 0 {
return (barrier * self.options.barrier_reduction).max(self.options.min_barrier);
}
let m = self.m_ineq as f64;
let products: Vec<f64> = s
.iter()
.zip(lambda_ineq.iter())
.map(|(&si, &li)| si * li)
.collect();
let gap_mean = products.iter().sum::<f64>() / m;
if !(gap_mean > 0.0) || !gap_mean.is_finite() {
return (barrier * self.options.barrier_reduction).max(self.options.min_barrier);
}
let min_gap = products.iter().copied().fold(f64::INFINITY, f64::min);
let centering_ratio = (min_gap / gap_mean).clamp(0.0, 1.0);
let sigma = (1.0 - centering_ratio).powi(3).clamp(1e-3, 0.9);
let mu_candidate = sigma * gap_mean;
mu_candidate.clamp(self.options.min_barrier, barrier)
}
fn compute_newton_direction(
&self,
g: &Array1<f64>,
c_eq: &Option<Array1<f64>>,
c_ineq: &Option<Array1<f64>>,
j_eq: &Option<Array2<f64>>,
j_ineq: &Option<Array2<f64>>,
s: &Array1<f64>,
lambda_eq: &Array1<f64>,
lambda_ineq: &Array1<f64>,
barrier: f64,
) -> Result<NewtonDirectionResult, OptimizeError> {
let n_total = self.n + self.m_eq + 2 * self.m_ineq;
let mut kkt_matrix = Array2::zeros((n_total, n_total));
let mut rhs = Array1::zeros(n_total);
let reg = self.options.regularization.max(1e-8);
self.write_hessian_block(&mut kkt_matrix, reg);
let lag_grad = self.lagrangian_gradient(g, j_eq, lambda_eq, j_ineq, lambda_ineq);
for i in 0..self.n {
rhs[i] = -lag_grad[i];
}
let mut row_offset = self.n + self.m_ineq;
if let (Some(j_eq), Some(c_eq), true) = (j_eq, c_eq, self.m_eq > 0) {
for i in 0..self.m_eq {
for j in 0..self.n {
kkt_matrix[[j, row_offset + i]] = j_eq[[i, j]];
kkt_matrix[[row_offset + i, j]] = j_eq[[i, j]];
}
}
for i in 0..self.m_eq {
rhs[row_offset + i] = -c_eq[i];
}
row_offset += self.m_eq;
}
if let (Some(j_ineq), Some(c_ineq), true) = (j_ineq, c_ineq, self.m_ineq > 0) {
for i in 0..self.m_ineq {
for j in 0..self.n {
kkt_matrix[[j, row_offset + i]] = j_ineq[[i, j]];
kkt_matrix[[row_offset + i, j]] = j_ineq[[i, j]];
}
}
for i in 0..self.m_ineq {
rhs[row_offset + i] = -(c_ineq[i] + s[i]);
}
for i in 0..self.m_ineq {
let s_i = s[i].max(1e-10);
let lambda_i = lambda_ineq[i].max(0.0);
kkt_matrix[[self.n + i, self.n + i]] = lambda_i / s_i + reg;
kkt_matrix[[self.n + i, row_offset + i]] = 1.0;
kkt_matrix[[row_offset + i, self.n + i]] = 1.0;
rhs[self.n + i] = barrier / s_i - lambda_i;
}
}
let solution = solve(&kkt_matrix, &rhs)?;
let dx = solution
.slice(scirs2_core::ndarray::s![0..self.n])
.to_owned();
let ds = if self.m_ineq > 0 {
solution
.slice(scirs2_core::ndarray::s![self.n..self.n + self.m_ineq])
.to_owned()
} else {
Array1::zeros(0)
};
let mut offset = self.n + self.m_ineq;
let dlambda_eq = if self.m_eq > 0 {
solution
.slice(scirs2_core::ndarray::s![offset..offset + self.m_eq])
.to_owned()
} else {
Array1::zeros(0)
};
offset += self.m_eq;
let dlambda_ineq = if self.m_ineq > 0 {
solution
.slice(scirs2_core::ndarray::s![offset..offset + self.m_ineq])
.to_owned()
} else {
Array1::zeros(0)
};
Ok((dx, ds, dlambda_eq, dlambda_ineq))
}
fn compute_mehrotra_direction(
&self,
g: &Array1<f64>,
c_eq: &Option<Array1<f64>>,
c_ineq: &Option<Array1<f64>>,
j_eq: &Option<Array2<f64>>,
j_ineq: &Option<Array2<f64>>,
s: &Array1<f64>,
lambda_eq: &Array1<f64>,
lambda_ineq: &Array1<f64>,
_barrier: f64,
) -> Result<NewtonDirectionResult, OptimizeError> {
if self.m_ineq == 0 {
return self.compute_newton_direction(
g,
c_eq,
c_ineq,
j_eq,
j_ineq,
s,
lambda_eq,
lambda_ineq,
0.0,
);
}
let (dx_aff, ds_aff, dlambda_eq_aff, dlambda_ineq_aff) = self
.compute_affine_scaling_direction(
g,
c_eq,
c_ineq,
j_eq,
j_ineq,
s,
lambda_eq,
lambda_ineq,
)?;
let alpha_primal_max = self.compute_max_step_primal(s, &ds_aff);
let alpha_dual_max = self.compute_max_step_dual(lambda_ineq, &dlambda_ineq_aff);
let current_gap = s
.iter()
.zip(lambda_ineq.iter())
.map(|(&si, &li)| si * li)
.sum::<f64>();
let mu = current_gap / (self.m_ineq as f64);
let mut predicted_gap = 0.0;
for i in 0..self.m_ineq {
let s_new = s[i] + alpha_primal_max * ds_aff[i];
let lambda_new = lambda_ineq[i] + alpha_dual_max * dlambda_ineq_aff[i];
predicted_gap += s_new * lambda_new;
}
let mu_aff = predicted_gap / (self.m_ineq as f64);
let sigma = if mu > 0.0 {
(mu_aff / mu).powi(3)
} else {
0.1 };
let sigma = sigma.max(0.0).min(1.0);
let sigma_mu = sigma * mu;
self.compute_corrector_direction(
g,
c_eq,
c_ineq,
j_eq,
j_ineq,
s,
lambda_ineq,
&dx_aff,
&ds_aff,
&dlambda_ineq_aff,
sigma_mu,
)
}
fn compute_affine_scaling_direction(
&self,
g: &Array1<f64>,
c_eq: &Option<Array1<f64>>,
c_ineq: &Option<Array1<f64>>,
j_eq: &Option<Array2<f64>>,
j_ineq: &Option<Array2<f64>>,
s: &Array1<f64>,
lambda_eq: &Array1<f64>,
lambda_ineq: &Array1<f64>,
) -> Result<NewtonDirectionResult, OptimizeError> {
let n_total = self.n + self.m_eq + 2 * self.m_ineq;
let mut kkt_matrix = Array2::zeros((n_total, n_total));
let mut rhs = Array1::zeros(n_total);
let reg = self.options.regularization.max(1e-8);
self.write_hessian_block(&mut kkt_matrix, reg);
let lag_grad = self.lagrangian_gradient(g, j_eq, lambda_eq, j_ineq, lambda_ineq);
for i in 0..self.n {
rhs[i] = -lag_grad[i];
}
let mut row_offset = self.n + self.m_ineq;
if let (Some(j_eq), Some(c_eq), true) = (j_eq, c_eq, self.m_eq > 0) {
for i in 0..self.m_eq {
for j in 0..self.n {
kkt_matrix[[j, row_offset + i]] = j_eq[[i, j]];
kkt_matrix[[row_offset + i, j]] = j_eq[[i, j]];
}
}
for i in 0..self.m_eq {
rhs[row_offset + i] = -c_eq[i];
}
row_offset += self.m_eq;
}
if let (Some(j_ineq), Some(c_ineq), true) = (j_ineq, c_ineq, self.m_ineq > 0) {
for i in 0..self.m_ineq {
for j in 0..self.n {
kkt_matrix[[j, row_offset + i]] = j_ineq[[i, j]];
kkt_matrix[[row_offset + i, j]] = j_ineq[[i, j]];
}
}
for i in 0..self.m_ineq {
rhs[row_offset + i] = -(c_ineq[i] + s[i]);
}
for i in 0..self.m_ineq {
let s_i = s[i].max(1e-10);
let lambda_i = lambda_ineq[i].max(0.0);
kkt_matrix[[self.n + i, self.n + i]] = lambda_i / s_i + reg;
kkt_matrix[[self.n + i, row_offset + i]] = 1.0;
kkt_matrix[[row_offset + i, self.n + i]] = 1.0;
rhs[self.n + i] = -lambda_i;
}
}
let solution = solve(&kkt_matrix, &rhs)?;
self.extract_direction_components(&solution)
}
fn compute_corrector_direction(
&self,
self_g: &Array1<f64>,
_c_eq: &Option<Array1<f64>>,
_c_ineq: &Option<Array1<f64>>,
j_eq: &Option<Array2<f64>>,
j_ineq: &Option<Array2<f64>>,
s: &Array1<f64>,
lambda_ineq: &Array1<f64>,
dx_aff: &Array1<f64>,
ds_aff: &Array1<f64>,
dlambda_ineq_aff: &Array1<f64>,
sigma_mu: f64,
) -> Result<NewtonDirectionResult, OptimizeError> {
let n_total = self.n + self.m_eq + 2 * self.m_ineq;
let mut kkt_matrix = Array2::zeros((n_total, n_total));
let mut rhs = Array1::zeros(n_total);
let reg = self.options.regularization.max(1e-8);
self.write_hessian_block(&mut kkt_matrix, reg);
for i in 0..self.n {
rhs[i] = 0.0;
}
let mut row_offset = self.n + self.m_ineq;
if let (Some(j_eq), true) = (j_eq, self.m_eq > 0) {
for i in 0..self.m_eq {
for j in 0..self.n {
kkt_matrix[[j, row_offset + i]] = j_eq[[i, j]];
kkt_matrix[[row_offset + i, j]] = j_eq[[i, j]];
}
}
for i in 0..self.m_eq {
rhs[row_offset + i] = 0.0;
}
row_offset += self.m_eq;
}
if let (Some(j_ineq), true) = (j_ineq, self.m_ineq > 0) {
for i in 0..self.m_ineq {
for j in 0..self.n {
kkt_matrix[[j, row_offset + i]] = j_ineq[[i, j]];
kkt_matrix[[row_offset + i, j]] = j_ineq[[i, j]];
}
}
for i in 0..self.m_ineq {
rhs[row_offset + i] = 0.0;
}
for i in 0..self.m_ineq {
let s_i = s[i].max(1e-10);
let lambda_i = lambda_ineq[i].max(0.0);
kkt_matrix[[self.n + i, self.n + i]] = lambda_i / s_i + reg;
kkt_matrix[[self.n + i, row_offset + i]] = 1.0;
kkt_matrix[[row_offset + i, self.n + i]] = 1.0;
let correction = sigma_mu - ds_aff[i] * dlambda_ineq_aff[i];
rhs[self.n + i] = correction / s_i;
}
}
let solution = solve(&kkt_matrix, &rhs)?;
let (dx_cor, ds_cor, dlambda_eq_cor, dlambda_ineq_cor) =
self.extract_direction_components(&solution)?;
let dx_final = dx_aff + &dx_cor;
let ds_final = ds_aff + &ds_cor;
let dlambda_eq_final = &Array1::zeros(self.m_eq) + &dlambda_eq_cor;
let dlambda_ineq_final = dlambda_ineq_aff + &dlambda_ineq_cor;
Ok((dx_final, ds_final, dlambda_eq_final, dlambda_ineq_final))
}
fn extract_direction_components(
&self,
solution: &Array1<f64>,
) -> Result<NewtonDirectionResult, OptimizeError> {
let dx = solution
.slice(scirs2_core::ndarray::s![0..self.n])
.to_owned();
let ds = if self.m_ineq > 0 {
solution
.slice(scirs2_core::ndarray::s![self.n..self.n + self.m_ineq])
.to_owned()
} else {
Array1::zeros(0)
};
let mut offset = self.n + self.m_ineq;
let dlambda_eq = if self.m_eq > 0 {
solution
.slice(scirs2_core::ndarray::s![offset..offset + self.m_eq])
.to_owned()
} else {
Array1::zeros(0)
};
offset += self.m_eq;
let dlambda_ineq = if self.m_ineq > 0 {
solution
.slice(scirs2_core::ndarray::s![offset..offset + self.m_ineq])
.to_owned()
} else {
Array1::zeros(0)
};
Ok((dx, ds, dlambda_eq, dlambda_ineq))
}
fn compute_max_step_primal(&self, s: &Array1<f64>, ds: &Array1<f64>) -> f64 {
if self.m_ineq == 0 {
return 1.0;
}
let tau = 0.995; let mut alpha = 1.0;
for i in 0..self.m_ineq {
if ds[i] < 0.0 {
alpha = f64::min(alpha, -tau * s[i] / ds[i]);
}
}
alpha.max(0.0).min(1.0)
}
fn compute_max_step_dual(&self, lambda_ineq: &Array1<f64>, dlambda_ineq: &Array1<f64>) -> f64 {
if self.m_ineq == 0 {
return 1.0;
}
let tau = 0.995; let mut alpha = 1.0;
for i in 0..self.m_ineq {
if dlambda_ineq[i] < 0.0 {
alpha = f64::min(alpha, -tau * lambda_ineq[i] / dlambda_ineq[i]);
}
}
alpha.max(0.0).min(1.0)
}
fn l1_infeasibility(c_eq: &Option<Array1<f64>>, c_ineq_plus_s: &Option<Array1<f64>>) -> f64 {
let mut infeas = 0.0;
if let Some(c_eq) = c_eq {
infeas += c_eq.iter().map(|v| v.abs()).sum::<f64>();
}
if let Some(c) = c_ineq_plus_s {
infeas += c.iter().map(|v| v.abs()).sum::<f64>();
}
infeas
}
fn merit_value(
&self,
f: f64,
s: &Array1<f64>,
barrier: f64,
c_eq: &Option<Array1<f64>>,
c_ineq_plus_s: &Option<Array1<f64>>,
nu: f64,
) -> f64 {
let mut phi = f;
if self.m_ineq > 0 {
phi -= barrier * s.iter().map(|&si| si.max(1e-300).ln()).sum::<f64>();
}
phi + nu * Self::l1_infeasibility(c_eq, c_ineq_plus_s)
}
#[allow(clippy::too_many_arguments)]
fn line_search<F>(
&mut self,
fun: &mut F,
mut eq_con: Option<&mut EqualityConstraintFn<'_>>,
mut ineq_con: Option<&mut InequalityConstraintFn<'_>>,
x: &Array1<f64>,
s: &Array1<f64>,
lambda_ineq: &Array1<f64>,
dx: &Array1<f64>,
ds: &Array1<f64>,
dlambda_ineq: &Array1<f64>,
g: &Array1<f64>,
c_eq: &Option<Array1<f64>>,
c_ineq: &Option<Array1<f64>>,
barrier: f64,
) -> Result<f64, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
let tau = 0.995;
let mut alpha_primal = 1.0;
let mut alpha_dual = 1.0;
if self.m_ineq > 0 {
for i in 0..self.m_ineq {
if ds[i] < 0.0 {
alpha_primal = f64::min(alpha_primal, -tau * s[i] / ds[i]);
}
if dlambda_ineq[i] < 0.0 {
alpha_dual = f64::min(alpha_dual, -tau * lambda_ineq[i] / dlambda_ineq[i]);
}
}
}
let alpha_max = f64::min(alpha_primal, alpha_dual).clamp(0.0, 1.0);
let f0 = fun(&x.view());
self.nfev += 1;
let c_ineq_plus_s0 = c_ineq.as_ref().map(|c| c + s);
let infeas0 = Self::l1_infeasibility(c_eq, &c_ineq_plus_s0);
let barrier_dir_deriv = g.dot(dx)
- if self.m_ineq > 0 {
barrier
* s.iter()
.zip(ds.iter())
.map(|(&si, &dsi)| dsi / si.max(1e-300))
.sum::<f64>()
} else {
0.0
};
let margin = 0.1;
if infeas0 > 1e-12 {
let nu_required = (barrier_dir_deriv / ((1.0 - margin) * infeas0)).max(0.0);
if self.merit_penalty < nu_required {
self.merit_penalty = nu_required + 1.0;
}
}
let nu = self.merit_penalty;
let dir_deriv = {
let d = barrier_dir_deriv - nu * infeas0;
if d.is_finite() && d < 0.0 {
d
} else {
-1e-10 * (1.0 + f0.abs())
}
};
let phi0 = self.merit_value(f0, s, barrier, c_eq, &c_ineq_plus_s0, nu);
let eval_eq = c_eq.is_some();
let eval_ineq = c_ineq.is_some();
let mut alpha = alpha_max;
for _ in 0..self.options.max_ls_iter {
let x_new = x + alpha * dx;
let f_new = fun(&x_new.view());
self.nfev += 1;
let s_new = if self.m_ineq > 0 {
s + alpha * ds
} else {
s.clone()
};
let c_eq_new: Option<Array1<f64>> = if eval_eq {
if let Some(f) = eq_con.as_mut() {
self.nfev += 1;
Some(f(&x_new.view()))
} else {
None
}
} else {
None
};
let c_ineq_new: Option<Array1<f64>> = if eval_ineq {
if let Some(f) = ineq_con.as_mut() {
self.nfev += 1;
Some(f(&x_new.view()))
} else {
None
}
} else {
None
};
let c_ineq_plus_s_new = c_ineq_new.map(|c| c + &s_new);
let phi_new =
self.merit_value(f_new, &s_new, barrier, &c_eq_new, &c_ineq_plus_s_new, nu);
if phi_new <= phi0 + self.options.alpha * alpha * dir_deriv {
return Ok(alpha);
}
alpha *= self.options.beta;
}
Ok(alpha)
}
}
#[allow(dead_code)]
fn solve(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>, OptimizeError> {
use scirs2_linalg::solve;
solve(&a.view(), &b.view(), None)
.map_err(|e| OptimizeError::ComputationError(format!("Linear system solve failed: {}", e)))
}
#[allow(dead_code, clippy::too_many_arguments)]
pub fn minimize_interior_point<F, G, H, J>(
fun: F,
grad_fn: Option<G>,
x0: Array1<f64>,
eq_con: Option<H>,
eq_jac: Option<J>,
ineq_con: Option<H>,
ineq_jac: Option<J>,
options: Option<InteriorPointOptions>,
) -> Result<OptimizeResult<f64>, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> f64 + Clone,
G: FnMut(&ArrayView1<f64>) -> Array1<f64>,
H: FnMut(&ArrayView1<f64>) -> Array1<f64>,
J: FnMut(&ArrayView1<f64>) -> Array2<f64>,
{
let options = options.unwrap_or_default();
let n = x0.len();
let mut eq_con = eq_con;
let mut ineq_con = ineq_con;
let m_eq = match eq_con.as_mut() {
Some(f) => f(&x0.view()).len(),
None => 0,
};
let m_ineq = match ineq_con.as_mut() {
Some(f) => f(&x0.view()).len(),
None => 0,
};
let mut solver = InteriorPointSolver::new(n, m_eq, m_ineq, &options);
let mut fun_mut = fun.clone();
let eps = 1e-8;
let mut fd_fun = fun.clone();
let mut grad_owned: Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64>> = match grad_fn {
Some(g) => Box::new(g),
None => Box::new(move |x: &ArrayView1<f64>| finite_diff_gradient(&mut fd_fun, x, eps)),
};
let eq_con_shared = eq_con.map(|f| Rc::new(RefCell::new(f)));
let ineq_con_shared = ineq_con.map(|f| Rc::new(RefCell::new(f)));
let mut eq_con_owned: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64>>> =
eq_con_shared.as_ref().map(|shared| {
let shared = Rc::clone(shared);
Box::new(move |x: &ArrayView1<f64>| shared.borrow_mut()(x))
as Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64>>
});
let mut eq_jac_owned: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array2<f64>>> =
match (eq_jac, eq_con_shared.as_ref()) {
(Some(j), _) => Some(Box::new(j)),
(None, Some(shared)) => {
let shared = Rc::clone(shared);
Some(Box::new(move |x: &ArrayView1<f64>| {
let mut con = |xv: &ArrayView1<f64>| shared.borrow_mut()(xv);
finite_diff_jacobian_fn(&mut con, x, eps)
}))
}
(None, None) => None,
};
let mut ineq_con_owned: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64>>> =
ineq_con_shared.as_ref().map(|shared| {
let shared = Rc::clone(shared);
Box::new(move |x: &ArrayView1<f64>| shared.borrow_mut()(x))
as Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64>>
});
let mut ineq_jac_owned: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array2<f64>>> =
match (ineq_jac, ineq_con_shared.as_ref()) {
(Some(j), _) => Some(Box::new(j)),
(None, Some(shared)) => {
let shared = Rc::clone(shared);
Some(Box::new(move |x: &ArrayView1<f64>| {
let mut con = |xv: &ArrayView1<f64>| shared.borrow_mut()(xv);
finite_diff_jacobian_fn(&mut con, x, eps)
}))
}
(None, None) => None,
};
let result: InteriorPointResult = solver.solve(
&mut fun_mut,
&mut *grad_owned,
eq_con_owned.as_deref_mut(),
eq_jac_owned.as_deref_mut(),
ineq_con_owned.as_deref_mut(),
ineq_jac_owned.as_deref_mut(),
&x0,
)?;
Ok(OptimizeResult {
x: result.x,
fun: result.fun,
nit: result.nit,
func_evals: result.nfev,
nfev: result.nfev,
success: result.success,
message: result.message,
jacobian: None,
hessian: None,
})
}
#[allow(dead_code)]
fn finite_diff_gradient<F>(fun: &mut F, x: &ArrayView1<f64>, eps: f64) -> Array1<f64>
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
let n = x.len();
let mut grad = Array1::zeros(n);
let f0 = fun(x);
let mut x_pert = x.to_owned();
for i in 0..n {
let h = eps * (1.0 + x[i].abs());
x_pert[i] = x[i] + h;
let f_plus = fun(&x_pert.view());
grad[i] = (f_plus - f0) / h;
x_pert[i] = x[i];
}
grad
}
#[allow(dead_code)]
fn finite_diff_jacobian_fn<C>(con: &mut C, x: &ArrayView1<f64>, eps: f64) -> Array2<f64>
where
C: FnMut(&ArrayView1<f64>) -> Array1<f64>,
{
let n = x.len();
let f0 = con(x);
let m = f0.len();
let mut jac = Array2::zeros((m, n));
let mut x_pert = x.to_owned();
for j in 0..n {
let h = eps * (1.0 + x[j].abs());
x_pert[j] = x[j] + h;
let f_plus = con(&x_pert.view());
for i in 0..m {
jac[[i, j]] = (f_plus[i] - f0[i]) / h;
}
x_pert[j] = x[j];
}
jac
}
#[allow(dead_code)]
fn finite_diff_jacobian_constraints(
constraints: &[&Constraint],
x: &ArrayView1<f64>,
eps: f64,
) -> Array2<f64> {
let n = x.len();
let m = constraints.len();
let mut jac = Array2::zeros((m, n));
let x_slice = x.as_slice().expect("Operation failed");
let f0: Vec<f64> = constraints.iter().map(|c| (c.fun)(x_slice)).collect();
let mut needs_fd = vec![true; m];
for (i, c) in constraints.iter().enumerate() {
if let Some(ref jac_fn) = c.jac {
let grad = jac_fn(x_slice);
if grad.len() == n {
for j in 0..n {
jac[[i, j]] = grad[j];
}
needs_fd[i] = false;
}
}
}
if needs_fd.iter().any(|&b| b) {
let mut x_pert = x.to_owned();
for j in 0..n {
let h = eps * (1.0 + x[j].abs());
x_pert[j] = x[j] + h;
let x_pert_slice = x_pert.as_slice().expect("Operation failed");
for i in 0..m {
if needs_fd[i] {
let f_plus = (constraints[i].fun)(x_pert_slice);
jac[[i, j]] = (f_plus - f0[i]) / h;
}
}
x_pert[j] = x[j]; }
}
jac
}
#[allow(dead_code)]
pub fn minimize_interior_point_constrained<F>(
func: F,
x0: Array1<f64>,
constraints: &[Constraint],
options: Option<InteriorPointOptions>,
) -> Result<OptimizeResult<f64>, OptimizeError>
where
F: Fn(&[f64]) -> f64 + Clone,
{
let options = options.unwrap_or_default();
let n = x0.len();
let eq_constraints: Vec<_> = constraints
.iter()
.filter(|c| c.kind == ConstraintKind::Equality && !c.is_bounds())
.collect();
let ineq_constraints: Vec<_> = constraints
.iter()
.filter(|c| c.kind == ConstraintKind::Inequality && !c.is_bounds())
.collect();
let m_eq = eq_constraints.len();
let m_ineq = ineq_constraints.len();
let mut solver = InteriorPointSolver::new(n, m_eq, m_ineq, &options);
let func_clone = func.clone();
let mut fun_mut =
move |x: &ArrayView1<f64>| -> f64 { func(x.as_slice().expect("Operation failed")) };
let mut grad_mut = move |x: &ArrayView1<f64>| -> Array1<f64> {
let mut fun_fd =
|x: &ArrayView1<f64>| -> f64 { func_clone(x.as_slice().expect("Operation failed")) };
finite_diff_gradient(&mut fun_fd, x, 1e-8)
};
#[allow(clippy::type_complexity)]
let mut eq_con_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64> + '_>> = if m_eq > 0 {
Some(Box::new(|x: &ArrayView1<f64>| -> Array1<f64> {
let x_slice = x.as_slice().expect("Operation failed");
Array1::from_vec(eq_constraints.iter().map(|c| (c.fun)(x_slice)).collect())
}))
} else {
None
};
#[allow(clippy::type_complexity)]
let mut eq_jac_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array2<f64> + '_>> = if m_eq > 0 {
Some(Box::new(|x: &ArrayView1<f64>| -> Array2<f64> {
let eps = 1e-8;
finite_diff_jacobian_constraints(&eq_constraints, x, eps)
}))
} else {
None
};
#[allow(clippy::type_complexity)]
let mut ineq_con_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array1<f64> + '_>> =
if m_ineq > 0 {
Some(Box::new(|x: &ArrayView1<f64>| -> Array1<f64> {
let x_slice = x.as_slice().expect("Operation failed");
Array1::from_vec(ineq_constraints.iter().map(|c| (c.fun)(x_slice)).collect())
}))
} else {
None
};
#[allow(clippy::type_complexity)]
let mut ineq_jac_mut: Option<Box<dyn FnMut(&ArrayView1<f64>) -> Array2<f64> + '_>> =
if m_ineq > 0 {
Some(Box::new(|x: &ArrayView1<f64>| -> Array2<f64> {
let eps = 1e-8;
finite_diff_jacobian_constraints(&ineq_constraints, x, eps)
}))
} else {
None
};
let result = solver.solve(
&mut fun_mut,
&mut grad_mut,
eq_con_mut.as_mut().map(|f| f.as_mut()),
eq_jac_mut.as_mut().map(|f| f.as_mut()),
ineq_con_mut.as_mut().map(|f| f.as_mut()),
ineq_jac_mut.as_mut().map(|f| f.as_mut()),
&x0,
)?;
let bounds_constraints: Vec<_> = constraints.iter().filter(|c| c.is_bounds()).collect();
if !bounds_constraints.is_empty() {
eprintln!("Warning: Box constraints (bounds) are not yet fully integrated with interior point method");
}
Ok(OptimizeResult {
x: result.x,
fun: result.fun,
nit: result.nit,
func_evals: result.nfev,
nfev: result.nfev,
success: result.success,
message: result.message,
jacobian: None,
hessian: None,
})
}
#[cfg(test)]
#[path = "interior_point_tests.rs"]
mod tests;