use crate::constrained::{Constraint, ConstraintFn, ConstraintKind, Options};
use crate::error::OptimizeResult;
use crate::result::OptimizeResults;
use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Axis, Data, Ix1};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HessianUpdate {
#[default]
BFGS,
SR1,
Exact,
}
pub type GradientFn = fn(&[f64]) -> Array1<f64>;
pub type HessianFn = fn(&[f64]) -> Array2<f64>;
#[allow(clippy::many_single_char_names)]
#[allow(dead_code)]
pub fn minimize_trust_constr<F, S>(
func: F,
x0: &ArrayBase<S, Ix1>,
constraints: &[Constraint<ConstraintFn>],
options: &Options,
) -> OptimizeResult<OptimizeResults<f64>>
where
F: Fn(&[f64]) -> f64,
S: Data<Elem = f64>,
{
let ftol = options.ftol.unwrap_or(1e-8);
let gtol = options.gtol.unwrap_or(1e-8);
let ctol = options.ctol.unwrap_or(1e-8);
let maxiter = options.maxiter.unwrap_or(100 * x0.len());
let eps = options.eps.unwrap_or(1e-8);
let n = x0.len();
let mut x = x0.to_owned();
let mut f = func(x.as_slice().expect("Operation failed"));
let mut nfev = 1;
let mut lambda = Array1::zeros(constraints.len());
let mut g = Array1::zeros(n);
for i in 0..n {
let mut x_h = x.clone();
x_h[i] += eps;
let f_h = func(x_h.as_slice().expect("Operation failed"));
g[i] = (f_h - f) / eps;
nfev += 1;
}
let mut c = Array1::zeros(constraints.len());
for (i, constraint) in constraints.iter().enumerate() {
if !constraint.is_bounds() {
let val = (constraint.fun)(x.as_slice().expect("Operation failed"));
match constraint.kind {
ConstraintKind::Inequality => {
c[i] = val; }
ConstraintKind::Equality => {
c[i] = val; }
}
}
}
let mut a = Array2::zeros((constraints.len(), n));
for (i, constraint) in constraints.iter().enumerate() {
if !constraint.is_bounds() {
for j in 0..n {
let mut x_h = x.clone();
x_h[j] += eps;
let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
a[[i, j]] = (c_h - c[i]) / eps;
nfev += 1;
}
}
}
let mut delta = 1.0;
let mut b = Array2::eye(n);
let mut iter = 0;
while iter < maxiter {
let mut max_constraint_violation = 0.0;
for (i, &ci) in c.iter().enumerate() {
match constraints[i].kind {
ConstraintKind::Inequality => {
if ci < -ctol {
max_constraint_violation = f64::max(max_constraint_violation, -ci);
}
}
ConstraintKind::Equality => {
let violation = ci.abs();
if violation > ctol {
max_constraint_violation = f64::max(max_constraint_violation, violation);
}
}
}
}
if g.iter().all(|&gi| gi.abs() < gtol) && max_constraint_violation < ctol {
break;
}
let mut lag_grad = g.clone();
for (i, &li) in lambda.iter().enumerate() {
let include_constraint = match constraints[i].kind {
ConstraintKind::Inequality => {
li > 0.0 || c[i] < -ctol
}
ConstraintKind::Equality => {
true
}
};
if include_constraint {
for j in 0..n {
lag_grad[j] -= li * a[[i, j]];
}
}
}
let (p, predicted_reduction) =
compute_trust_region_step_constrained(&lag_grad, &b, &a, &c, delta, constraints, ctol);
let x_new = &x + &p;
let f_new = func(x_new.as_slice().expect("Operation failed"));
nfev += 1;
let mut c_new = Array1::zeros(constraints.len());
for (i, constraint) in constraints.iter().enumerate() {
if !constraint.is_bounds() {
c_new[i] = (constraint.fun)(x_new.as_slice().expect("Operation failed"));
nfev += 1;
}
}
let mut merit = f;
let mut merit_new = f_new;
let penalty = 10.0; for (i, &ci) in c.iter().enumerate() {
match constraints[i].kind {
ConstraintKind::Inequality => {
merit += penalty * f64::max(0.0, -ci);
merit_new += penalty * f64::max(0.0, -c_new[i]);
}
ConstraintKind::Equality => {
merit += penalty * ci.abs();
merit_new += penalty * c_new[i].abs();
}
}
}
let actual_reduction = merit - merit_new;
let rho = if predicted_reduction > 0.0 {
actual_reduction / predicted_reduction
} else {
0.0
};
if rho < 0.25 {
delta *= 0.5;
} else if rho > 0.75 && p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt() >= 0.9 * delta {
delta *= 2.0;
}
if rho > 0.1 {
x = x_new;
f = f_new;
c = c_new;
if (merit - merit_new).abs() < ftol * (1.0 + merit.abs()) {
break;
}
let mut g_new = Array1::zeros(n);
for i in 0..n {
let mut x_h = x.clone();
x_h[i] += eps;
let f_h = func(x_h.as_slice().expect("Operation failed"));
g_new[i] = (f_h - f) / eps;
nfev += 1;
}
let mut a_new = Array2::zeros((constraints.len(), n));
for (i, constraint) in constraints.iter().enumerate() {
if !constraint.is_bounds() {
for j in 0..n {
let mut x_h = x.clone();
x_h[j] += eps;
let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
a_new[[i, j]] = (c_h - c[i]) / eps;
nfev += 1;
}
}
}
for (i, constraint) in constraints.iter().enumerate() {
match constraint.kind {
ConstraintKind::Inequality => {
if c[i] < -ctol {
lambda[i] = f64::max(0.0, lambda[i] - c[i] * penalty);
} else {
lambda[i] = f64::max(0.0, lambda[i] - 0.1 * lambda[i]);
}
}
ConstraintKind::Equality => {
let step_size = 0.1;
lambda[i] -= step_size * c[i] * penalty;
lambda[i] *= 0.9;
}
}
}
let s = &p;
let y = &g_new - &g;
let s_dot_y = s.dot(&y);
if s_dot_y > 1e-10 {
let s_col = s.clone().insert_axis(Axis(1));
let s_row = s.clone().insert_axis(Axis(0));
let bs = b.dot(s);
let bs_col = bs.clone().insert_axis(Axis(1));
let bs_row = bs.clone().insert_axis(Axis(0));
let term1 = s_dot_y + s.dot(&bs);
let term2 = &s_col.dot(&s_row) * (term1 / (s_dot_y * s_dot_y));
let term3 = &bs_col.dot(&s_row) + &s_col.dot(&bs_row);
b = &b + &term2 - &(&term3 / s_dot_y);
}
g = g_new;
a = a_new;
}
iter += 1;
}
let mut c_result = Array1::zeros(constraints.len());
for (i, constraint) in constraints.iter().enumerate() {
if !constraint.is_bounds() {
c_result[i] = c[i];
}
}
let mut result = OptimizeResults::default();
result.x = x;
result.fun = f;
result.jac = Some(g.into_raw_vec_and_offset().0);
result.constr = Some(c_result);
result.nfev = nfev;
result.nit = iter;
result.success = iter < maxiter;
if result.success {
result.message = "Optimization terminated successfully.".to_string();
} else {
result.message = "Maximum iterations reached.".to_string();
}
Ok(result)
}
#[allow(clippy::many_single_char_names)]
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub fn minimize_trust_constr_with_derivatives<F, S, G, H>(
func: F,
x0: &ArrayBase<S, Ix1>,
constraints: &[Constraint<ConstraintFn>],
options: &Options,
jac: Option<G>,
hess: Option<H>,
hess_update: HessianUpdate,
) -> OptimizeResult<OptimizeResults<f64>>
where
F: Fn(&[f64]) -> f64,
S: Data<Elem = f64>,
G: Fn(&[f64]) -> Array1<f64>,
H: Fn(&[f64]) -> Array2<f64>,
{
let ftol = options.ftol.unwrap_or(1e-8);
let gtol = options.gtol.unwrap_or(1e-8);
let ctol = options.ctol.unwrap_or(1e-8);
let maxiter = options.maxiter.unwrap_or(100 * x0.len());
let eps = options.eps.unwrap_or(1e-8);
let n = x0.len();
let mut x = x0.to_owned();
let mut f = func(x.as_slice().expect("Operation failed"));
let mut nfev = 1;
let mut njev = 0;
let mut nhev = 0;
let mut lambda = Array1::zeros(constraints.len());
let mut g = if let Some(ref grad_fn) = jac {
njev += 1;
grad_fn(x.as_slice().expect("Operation failed"))
} else {
let mut grad = Array1::zeros(n);
for i in 0..n {
let mut x_h = x.clone();
x_h[i] += eps;
let f_h = func(x_h.as_slice().expect("Operation failed"));
grad[i] = (f_h - f) / eps;
nfev += 1;
}
grad
};
let mut c = Array1::zeros(constraints.len());
for (i, constraint) in constraints.iter().enumerate() {
if !constraint.is_bounds() {
let val = (constraint.fun)(x.as_slice().expect("Operation failed"));
match constraint.kind {
ConstraintKind::Inequality => c[i] = val,
ConstraintKind::Equality => c[i] = val,
}
}
}
let mut a = Array2::zeros((constraints.len(), n));
for (i, constraint) in constraints.iter().enumerate() {
if !constraint.is_bounds() {
for j in 0..n {
let mut x_h = x.clone();
x_h[j] += eps;
let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
a[[i, j]] = (c_h - c[i]) / eps;
nfev += 1;
}
}
}
let mut delta = 1.0;
let mut b = if hess_update == HessianUpdate::Exact {
if let Some(ref hess_fn) = hess {
nhev += 1;
hess_fn(x.as_slice().expect("Operation failed"))
} else {
Array2::eye(n)
}
} else {
Array2::eye(n)
};
let mut iter = 0;
while iter < maxiter {
let mut max_constraint_violation = 0.0;
for (i, &ci) in c.iter().enumerate() {
match constraints[i].kind {
ConstraintKind::Inequality => {
if ci < -ctol {
max_constraint_violation = f64::max(max_constraint_violation, -ci);
}
}
ConstraintKind::Equality => {
let violation = ci.abs();
if violation > ctol {
max_constraint_violation = f64::max(max_constraint_violation, violation);
}
}
}
}
if g.iter().all(|&gi| gi.abs() < gtol) && max_constraint_violation < ctol {
break;
}
let mut lag_grad = g.clone();
for (i, &li) in lambda.iter().enumerate() {
let include_constraint = match constraints[i].kind {
ConstraintKind::Inequality => li > 0.0 || c[i] < -ctol,
ConstraintKind::Equality => true,
};
if include_constraint {
for j in 0..n {
lag_grad[j] -= li * a[[i, j]];
}
}
}
let (p, predicted_reduction) =
compute_trust_region_step_constrained(&lag_grad, &b, &a, &c, delta, constraints, ctol);
let x_new = &x + &p;
let f_new = func(x_new.as_slice().expect("Operation failed"));
nfev += 1;
let mut c_new = Array1::zeros(constraints.len());
for (i, constraint) in constraints.iter().enumerate() {
if !constraint.is_bounds() {
c_new[i] = (constraint.fun)(x_new.as_slice().expect("Operation failed"));
nfev += 1;
}
}
let mut merit = f;
let mut merit_new = f_new;
let penalty = 10.0;
for (i, &ci) in c.iter().enumerate() {
match constraints[i].kind {
ConstraintKind::Inequality => {
merit += penalty * f64::max(0.0, -ci);
merit_new += penalty * f64::max(0.0, -c_new[i]);
}
ConstraintKind::Equality => {
merit += penalty * ci.abs();
merit_new += penalty * c_new[i].abs();
}
}
}
let actual_reduction = merit - merit_new;
let rho = if predicted_reduction > 0.0 {
actual_reduction / predicted_reduction
} else {
0.0
};
if rho < 0.25 {
delta *= 0.5;
} else if rho > 0.75 && p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt() >= 0.9 * delta {
delta *= 2.0;
}
if rho > 0.1 {
x = x_new;
f = f_new;
c = c_new;
if (merit - merit_new).abs() < ftol * (1.0 + merit.abs()) {
break;
}
let g_new = if let Some(ref grad_fn) = jac {
njev += 1;
grad_fn(x.as_slice().expect("Operation failed"))
} else {
let mut grad = Array1::zeros(n);
for i in 0..n {
let mut x_h = x.clone();
x_h[i] += eps;
let f_h = func(x_h.as_slice().expect("Operation failed"));
grad[i] = (f_h - f) / eps;
nfev += 1;
}
grad
};
let mut a_new = Array2::zeros((constraints.len(), n));
for (i, constraint) in constraints.iter().enumerate() {
if !constraint.is_bounds() {
for j in 0..n {
let mut x_h = x.clone();
x_h[j] += eps;
let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
a_new[[i, j]] = (c_h - c[i]) / eps;
nfev += 1;
}
}
}
for (i, constraint) in constraints.iter().enumerate() {
match constraint.kind {
ConstraintKind::Inequality => {
if c[i] < -ctol {
lambda[i] = f64::max(0.0, lambda[i] - c[i] * penalty);
} else {
lambda[i] = f64::max(0.0, lambda[i] - 0.1 * lambda[i]);
}
}
ConstraintKind::Equality => {
let step_size = 0.1;
lambda[i] -= step_size * c[i] * penalty;
lambda[i] *= 0.9;
}
}
}
match hess_update {
HessianUpdate::Exact => {
if let Some(ref hess_fn) = hess {
nhev += 1;
b = hess_fn(x.as_slice().expect("Operation failed"));
}
}
HessianUpdate::BFGS => {
let s = &p;
let y = &g_new - &g;
let s_dot_y = s.dot(&y);
if s_dot_y > 1e-10 {
let s_col = s.clone().insert_axis(Axis(1));
let s_row = s.clone().insert_axis(Axis(0));
let bs = b.dot(s);
let bs_col = bs.clone().insert_axis(Axis(1));
let bs_row = bs.clone().insert_axis(Axis(0));
let term1 = s_dot_y + s.dot(&bs);
let term2 = &s_col.dot(&s_row) * (term1 / (s_dot_y * s_dot_y));
let term3 = &bs_col.dot(&s_row) + &s_col.dot(&bs_row);
b = &b + &term2 - &(&term3 / s_dot_y);
}
}
HessianUpdate::SR1 => {
let s = &p;
let y = &g_new - &g;
let bs = b.dot(s);
let diff = &y - &bs;
let s_dot_diff = s.dot(&diff);
if s_dot_diff.abs() > 1e-8 * s.dot(s).sqrt() * diff.dot(&diff).sqrt() {
let diff_col = diff.clone().insert_axis(Axis(1));
let diff_row = diff.clone().insert_axis(Axis(0));
let update = &diff_col.dot(&diff_row) / s_dot_diff;
b = &b + &update;
}
}
}
g = g_new;
a = a_new;
}
iter += 1;
}
let mut c_result = Array1::zeros(constraints.len());
for (i, constraint) in constraints.iter().enumerate() {
if !constraint.is_bounds() {
c_result[i] = c[i];
}
}
let mut result = OptimizeResults::default();
result.x = x;
result.fun = f;
result.jac = Some(g.into_raw_vec_and_offset().0);
result.constr = Some(c_result);
result.nfev = nfev;
result.njev = njev;
result.nhev = nhev;
result.nit = iter;
result.success = iter < maxiter;
if result.success {
result.message = "Optimization terminated successfully.".to_string();
} else {
result.message = "Maximum iterations reached.".to_string();
}
Ok(result)
}
#[allow(clippy::many_single_char_names)]
#[allow(dead_code)]
fn compute_trust_region_step_constrained(
g: &Array1<f64>,
b: &Array2<f64>,
a: &Array2<f64>,
c: &Array1<f64>,
delta: f64,
constraints: &[Constraint<ConstraintFn>],
ctol: f64,
) -> (Array1<f64>, f64) {
let n = g.len();
let n_constr = constraints.len();
let p_unconstrained = compute_unconstrained_cauchy_point(g, b, delta);
let mut constraint_violated = false;
for i in 0..n_constr {
let grad_c_dot_p = (0..n).map(|j| a[[i, j]] * p_unconstrained[j]).sum::<f64>();
match constraints[i].kind {
ConstraintKind::Inequality => {
if c[i] + grad_c_dot_p < -ctol {
constraint_violated = true;
break;
}
}
ConstraintKind::Equality => {
if (c[i] + grad_c_dot_p).abs() > ctol {
constraint_violated = true;
break;
}
}
}
}
if !constraint_violated {
let g_dot_p = g.dot(&p_unconstrained);
let bp = b.dot(&p_unconstrained);
let p_dot_bp = p_unconstrained.dot(&bp);
let predicted_reduction = -g_dot_p - 0.5 * p_dot_bp;
return (p_unconstrained, predicted_reduction);
}
let mut p = Array1::zeros(n);
for i in 0..n {
p[i] = -g[i];
}
let p_norm = p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt();
if p_norm > 1e-10 {
p = &p * (delta / p_norm);
}
for _iter in 0..5 {
let mut max_viol = 0.0;
let mut most_violated = 0;
for i in 0..n_constr {
let grad_c_dot_p = (0..n).map(|j| a[[i, j]] * p[j]).sum::<f64>();
let viol = match constraints[i].kind {
ConstraintKind::Inequality => {
f64::max(0.0, -(c[i] + grad_c_dot_p))
}
ConstraintKind::Equality => {
(c[i] + grad_c_dot_p).abs()
}
};
if viol > max_viol {
max_viol = viol;
most_violated = i;
}
}
if max_viol < ctol {
break;
}
let mut a_norm_sq = 0.0;
for j in 0..n {
a_norm_sq += a[[most_violated, j]] * a[[most_violated, j]];
}
if a_norm_sq > 1e-10 {
let grad_c_dot_p = (0..n).map(|j| a[[most_violated, j]] * p[j]).sum::<f64>();
let proj_dist = match constraints[most_violated].kind {
ConstraintKind::Inequality => {
if c[most_violated] + grad_c_dot_p < 0.0 {
-(c[most_violated] + grad_c_dot_p) / a_norm_sq
} else {
0.0
}
}
ConstraintKind::Equality => {
-(c[most_violated] + grad_c_dot_p) / a_norm_sq
}
};
for j in 0..n {
p[j] += a[[most_violated, j]] * proj_dist;
}
let p_norm = p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt();
if p_norm > delta {
p = &p * (delta / p_norm);
}
}
}
let g_dot_p = g.dot(&p);
let bp = b.dot(&p);
let p_dot_bp = p.dot(&bp);
let predicted_reduction = -g_dot_p - 0.5 * p_dot_bp;
(p, predicted_reduction)
}
#[allow(dead_code)]
fn compute_unconstrained_cauchy_point(g: &Array1<f64>, b: &Array2<f64>, delta: f64) -> Array1<f64> {
let n = g.len();
let mut p = Array1::zeros(n);
for i in 0..n {
p[i] = -g[i];
}
let bg = b.dot(g);
let g_dot_bg = g.dot(&bg);
let g_norm_sq = g.dot(g);
if g_norm_sq < 1e-10 {
return Array1::zeros(n);
}
let tau = if g_dot_bg <= 0.0 {
delta / g_norm_sq.sqrt()
} else {
f64::min(delta / g_norm_sq.sqrt(), g_norm_sq / g_dot_bg)
};
for i in 0..n {
p[i] *= tau;
}
p
}