use crate::error::{OptimizeError, OptimizeResult};
#[derive(Debug, Clone)]
pub struct TrustConstrResult {
pub x: Vec<f64>,
pub fun: f64,
pub grad: Vec<f64>,
pub constraint_violation: f64,
pub lambda_eq: Vec<f64>,
pub lambda_ineq: Vec<f64>,
pub nit: usize,
pub nfev: usize,
pub njev: usize,
pub trust_radius: f64,
pub success: bool,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct TrustConstrOptions {
pub initial_radius: f64,
pub min_radius: f64,
pub max_radius: f64,
pub gtol: f64,
pub ctol: f64,
pub max_iter: usize,
pub fd_step: f64,
pub eta1: f64,
pub eta2: f64,
pub gamma_inc: f64,
pub gamma_dec: f64,
pub penalty: f64,
pub penalty_growth: f64,
}
impl Default for TrustConstrOptions {
fn default() -> Self {
TrustConstrOptions {
initial_radius: 1.0,
min_radius: 1e-10,
max_radius: 1e4,
gtol: 1e-8,
ctol: 1e-8,
max_iter: 1000,
fd_step: 1e-7,
eta1: 0.1,
eta2: 0.75,
gamma_inc: 2.0,
gamma_dec: 0.25,
penalty: 10.0,
penalty_growth: 1.5,
}
}
}
pub struct TrustConstr {
pub options: TrustConstrOptions,
}
impl Default for TrustConstr {
fn default() -> Self {
TrustConstr {
options: TrustConstrOptions::default(),
}
}
}
impl TrustConstr {
pub fn new(options: TrustConstrOptions) -> Self {
TrustConstr { options }
}
pub fn minimize<F>(
&self,
func: F,
x0: &[f64],
eq_constraints: &[Box<dyn Fn(&[f64]) -> f64>],
ineq_constraints: &[Box<dyn Fn(&[f64]) -> f64>],
) -> OptimizeResult<TrustConstrResult>
where
F: Fn(&[f64]) -> f64,
{
let n = x0.len();
let n_eq = eq_constraints.len();
let n_ineq = ineq_constraints.len();
if n == 0 {
return Err(OptimizeError::InvalidInput(
"Initial point must be non-empty".to_string(),
));
}
let n_aug = n + n_ineq;
let h = self.options.fd_step;
let mut z = vec![0.0f64; n_aug];
for i in 0..n {
z[i] = x0[i];
}
for j in 0..n_ineq {
let gj = (ineq_constraints[j])(&z[0..n]);
z[n + j] = (-gj).max(0.0);
}
let mut radius = self.options.initial_radius;
let mut penalty = self.options.penalty;
let mut lambda_eq = vec![0.0f64; n_eq];
let mut lambda_ineq = vec![0.0f64; n_ineq];
let mut nfev = 0usize;
let mut njev = 0usize;
let merit = |z: &[f64], penalty: f64, nfev: &mut usize| -> f64 {
let x = &z[0..n];
let s = &z[n..n_aug];
let f = func(x);
*nfev += 1;
let mut pen = 0.0f64;
for c in eq_constraints {
let cv = c(x);
*nfev += 1;
pen += cv * cv;
}
for (j, g) in ineq_constraints.iter().enumerate() {
let gv = g(x) + s[j];
*nfev += 1;
pen += gv * gv;
}
f + penalty * pen
};
let mut f_cur = merit(&z, penalty, &mut nfev);
for iter in 0..self.options.max_iter {
let x = &z[0..n].to_vec();
let mut grad = vec![0.0f64; n_aug];
for i in 0..n_aug {
let mut zf = z.clone();
zf[i] += h;
grad[i] = (merit(&zf, penalty, &mut nfev) - f_cur) / h;
njev += 1;
}
for j in 0..n_ineq {
if z[n + j] <= 0.0 && grad[n + j] > 0.0 {
grad[n + j] = 0.0;
}
}
let gnorm: f64 = grad[0..n].iter().map(|g| g * g).sum::<f64>().sqrt();
let mut cv_sum = 0.0f64;
for c in eq_constraints {
cv_sum += c(x).abs();
nfev += 1;
}
for (j, g) in ineq_constraints.iter().enumerate() {
let gv = g(x) + z[n + j];
cv_sum += gv.abs();
nfev += 1;
}
if gnorm < self.options.gtol && cv_sum < self.options.ctol {
update_multipliers_eq(
x, &mut lambda_eq, eq_constraints, penalty, h, &mut nfev,
);
update_multipliers_ineq(
x, &mut lambda_ineq, ineq_constraints, penalty, h, &mut nfev,
);
let final_f = func(x);
nfev += 1;
return Ok(TrustConstrResult {
x: x.clone(),
fun: final_f,
grad: grad[0..n].to_vec(),
constraint_violation: cv_sum,
lambda_eq,
lambda_ineq,
nit: iter + 1,
nfev,
njev,
trust_radius: radius,
success: true,
message: "Optimization converged".to_string(),
});
}
let grad_norm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
if grad_norm < 1e-14 {
break;
}
let cauchy_scale = (radius / grad_norm).min(1.0);
let mut z_trial = vec![0.0f64; n_aug];
for i in 0..n_aug {
z_trial[i] = z[i] - cauchy_scale * grad[i];
}
for j in 0..n_ineq {
if z_trial[n + j] < 0.0 {
z_trial[n + j] = 0.0;
}
}
let f_trial = merit(&z_trial, penalty, &mut nfev);
let actual_red = f_cur - f_trial;
let step_norm: f64 = z_trial
.iter()
.zip(z.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
let predicted_red = grad_norm * step_norm;
let rho = if predicted_red.abs() > 1e-14 {
actual_red / predicted_red
} else {
0.0
};
if rho < self.options.eta1 {
radius = (radius * self.options.gamma_dec).max(self.options.min_radius);
} else {
z = z_trial;
f_cur = f_trial;
if rho > self.options.eta2 {
radius = (radius * self.options.gamma_inc).min(self.options.max_radius);
}
}
if cv_sum > self.options.ctol * 10.0 {
penalty = (penalty * self.options.penalty_growth).min(1e10);
}
if radius < self.options.min_radius {
break;
}
}
let x_final = z[0..n].to_vec();
let f_final = func(&x_final);
nfev += 1;
let mut cv_final = 0.0f64;
for c in eq_constraints {
cv_final += c(&x_final).abs();
nfev += 1;
}
for g in ineq_constraints {
cv_final += g(&x_final).max(0.0);
nfev += 1;
}
let grad_final: Vec<f64> = {
let mut g = vec![0.0f64; n];
let f0 = func(&x_final);
nfev += 1;
for i in 0..n {
let mut xf = x_final.clone();
xf[i] += h;
g[i] = (func(&xf) - f0) / h;
nfev += 1;
njev += 1;
}
g
};
Ok(TrustConstrResult {
x: x_final,
fun: f_final,
grad: grad_final,
constraint_violation: cv_final,
lambda_eq,
lambda_ineq,
nit: self.options.max_iter,
nfev,
njev,
trust_radius: radius,
success: cv_final < self.options.ctol * 100.0,
message: "Maximum iterations reached".to_string(),
})
}
}
fn update_multipliers_eq(
x: &[f64],
lambda: &mut Vec<f64>,
constraints: &[Box<dyn Fn(&[f64]) -> f64>],
penalty: f64,
h: f64,
nfev: &mut usize,
) {
for (i, c) in constraints.iter().enumerate() {
let cv = c(x);
*nfev += 1;
lambda[i] += penalty * cv;
}
}
fn update_multipliers_ineq(
x: &[f64],
lambda: &mut Vec<f64>,
constraints: &[Box<dyn Fn(&[f64]) -> f64>],
penalty: f64,
_h: f64,
nfev: &mut usize,
) {
for (i, g) in constraints.iter().enumerate() {
let gv = g(x);
*nfev += 1;
lambda[i] = (lambda[i] + penalty * gv).max(0.0);
}
}
pub struct SubproblemSolver {
pub max_cg_iter: usize,
pub cg_tol: f64,
}
impl Default for SubproblemSolver {
fn default() -> Self {
SubproblemSolver {
max_cg_iter: 100,
cg_tol: 1e-10,
}
}
}
impl SubproblemSolver {
pub fn new(max_cg_iter: usize, cg_tol: f64) -> Self {
SubproblemSolver { max_cg_iter, cg_tol }
}
pub fn solve<BV>(
&self,
g: &[f64],
b_times_v: BV,
radius: f64,
) -> (Vec<f64>, bool)
where
BV: Fn(&[f64]) -> Vec<f64>,
{
let n = g.len();
let mut p = vec![0.0f64; n];
let mut r = g.to_vec(); let mut d: Vec<f64> = r.iter().map(|ri| -ri).collect();
let r_norm_0: f64 = r.iter().map(|ri| ri * ri).sum::<f64>().sqrt();
if r_norm_0 < self.cg_tol {
return (p, false);
}
for _iter in 0..self.max_cg_iter {
let bd = b_times_v(&d);
let dtbd: f64 = d.iter().zip(bd.iter()).map(|(di, bdi)| di * bdi).sum();
if dtbd <= 0.0 {
let tau = find_tau_boundary(&p, &d, radius);
for i in 0..n {
p[i] += tau * d[i];
}
return (p, true);
}
let r_norm_sq: f64 = r.iter().map(|ri| ri * ri).sum();
let alpha = r_norm_sq / dtbd;
let mut p_new = vec![0.0f64; n];
for i in 0..n {
p_new[i] = p[i] + alpha * d[i];
}
let p_new_norm: f64 = p_new.iter().map(|pi| pi * pi).sum::<f64>().sqrt();
if p_new_norm >= radius {
let tau = find_tau_boundary(&p, &d, radius);
for i in 0..n {
p[i] += tau * d[i];
}
return (p, true);
}
p = p_new;
let mut r_new = vec![0.0f64; n];
for i in 0..n {
r_new[i] = r[i] + alpha * bd[i];
}
let r_new_norm: f64 = r_new.iter().map(|ri| ri * ri).sum::<f64>().sqrt();
if r_new_norm < self.cg_tol * r_norm_0 {
return (p, false);
}
let beta = r_new.iter().map(|ri| ri * ri).sum::<f64>() / r_norm_sq;
for i in 0..n {
d[i] = -r_new[i] + beta * d[i];
}
r = r_new;
}
(p, false)
}
}
fn find_tau_boundary(p: &[f64], d: &[f64], radius: f64) -> f64 {
let a: f64 = d.iter().map(|di| di * di).sum();
let b: f64 = 2.0 * p.iter().zip(d.iter()).map(|(pi, di)| pi * di).sum::<f64>();
let c: f64 = p.iter().map(|pi| pi * pi).sum::<f64>() - radius * radius;
if a < 1e-14 {
return 0.0;
}
let disc = b * b - 4.0 * a * c;
if disc < 0.0 {
return 0.0;
}
let sqrt_disc = disc.sqrt();
let tau1 = (-b + sqrt_disc) / (2.0 * a);
let tau2 = (-b - sqrt_disc) / (2.0 * a);
if tau1 >= 0.0 {
tau1
} else {
tau2.max(0.0)
}
}
#[derive(Debug, Clone)]
pub struct EqualityConstrainedOptions {
pub rho: f64,
pub max_rho: f64,
pub rho_growth: f64,
pub outer_tol: f64,
pub inner_tol: f64,
pub max_outer: usize,
pub max_inner: usize,
pub radius: f64,
pub h: f64,
}
impl Default for EqualityConstrainedOptions {
fn default() -> Self {
EqualityConstrainedOptions {
rho: 1.0,
max_rho: 1e8,
rho_growth: 2.0,
outer_tol: 1e-7,
inner_tol: 1e-6,
max_outer: 50,
max_inner: 200,
radius: 1.0,
h: 1e-7,
}
}
}
pub struct EqualityConstrained {
pub options: EqualityConstrainedOptions,
}
impl Default for EqualityConstrained {
fn default() -> Self {
EqualityConstrained {
options: EqualityConstrainedOptions::default(),
}
}
}
impl EqualityConstrained {
pub fn new(options: EqualityConstrainedOptions) -> Self {
EqualityConstrained { options }
}
pub fn minimize<F>(
&self,
func: F,
x0: &[f64],
constraints: &[Box<dyn Fn(&[f64]) -> f64>],
) -> OptimizeResult<TrustConstrResult>
where
F: Fn(&[f64]) -> f64,
{
let n = x0.len();
let n_eq = constraints.len();
let h = self.options.h;
if n == 0 {
return Err(OptimizeError::InvalidInput(
"Initial point must be non-empty".to_string(),
));
}
let mut x = x0.to_vec();
let mut lambda = vec![0.0f64; n_eq];
let mut rho = self.options.rho;
let mut radius = self.options.radius;
let mut nfev = 0usize;
let mut njev = 0usize;
let mut total_iter = 0usize;
let aug_lag = |x: &[f64], lambda: &[f64], rho: f64, nfev: &mut usize| -> f64 {
let f = func(x);
*nfev += 1;
let mut pen = 0.0f64;
for (i, c) in constraints.iter().enumerate() {
let cv = c(x);
*nfev += 1;
pen += lambda[i] * cv + 0.5 * rho * cv * cv;
}
f + pen
};
for _outer in 0..self.options.max_outer {
for _inner in 0..self.options.max_inner {
total_iter += 1;
let f_cur = aug_lag(&x, &lambda, rho, &mut nfev);
let mut grad = vec![0.0f64; n];
for i in 0..n {
let mut xf = x.clone();
xf[i] += h;
grad[i] = (aug_lag(&xf, &lambda, rho, &mut nfev) - f_cur) / h;
njev += 1;
}
let gnorm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
if gnorm < self.options.inner_tol {
break;
}
let step = (radius / gnorm).min(radius);
let mut x_trial = vec![0.0f64; n];
for i in 0..n {
x_trial[i] = x[i] - step * grad[i];
}
let f_trial = aug_lag(&x_trial, &lambda, rho, &mut nfev);
if f_trial < f_cur {
let improvement = f_cur - f_trial;
x = x_trial;
if improvement > 0.5 * step * gnorm {
radius = (radius * 2.0).min(10.0);
}
} else {
radius *= 0.5;
if radius < 1e-12 {
break;
}
}
}
let mut cv_norm = 0.0f64;
let mut cv_vec = vec![0.0f64; n_eq];
for (i, c) in constraints.iter().enumerate() {
cv_vec[i] = c(&x);
nfev += 1;
cv_norm += cv_vec[i] * cv_vec[i];
}
cv_norm = cv_norm.sqrt();
if cv_norm < self.options.outer_tol {
let f_final = func(&x);
nfev += 1;
let mut grad_f = vec![0.0f64; n];
for i in 0..n {
let mut xf = x.clone();
xf[i] += h;
grad_f[i] = (func(&xf) - f_final) / h;
nfev += 1;
njev += 1;
}
return Ok(TrustConstrResult {
x,
fun: f_final,
grad: grad_f,
constraint_violation: cv_norm,
lambda_eq: lambda,
lambda_ineq: vec![],
nit: total_iter,
nfev,
njev,
trust_radius: radius,
success: true,
message: "Equality-constrained TR converged".to_string(),
});
}
for i in 0..n_eq {
lambda[i] += rho * cv_vec[i];
}
rho = (rho * self.options.rho_growth).min(self.options.max_rho);
}
let f_final = func(&x);
nfev += 1;
let cv_final: f64 = constraints.iter().map(|c| { nfev += 1; c(&x).powi(2) }).sum::<f64>().sqrt();
Ok(TrustConstrResult {
x,
fun: f_final,
grad: vec![0.0f64; n],
constraint_violation: cv_final,
lambda_eq: lambda,
lambda_ineq: vec![],
nit: total_iter,
nfev,
njev,
trust_radius: radius,
success: cv_final < self.options.outer_tol * 100.0,
message: "Maximum outer iterations reached".to_string(),
})
}
}
#[derive(Debug, Clone)]
pub struct InequalityHandlingOptions {
pub mu: f64,
pub mu_reduce: f64,
pub min_mu: f64,
pub tol: f64,
pub max_iter: usize,
pub radius: f64,
pub h: f64,
}
impl Default for InequalityHandlingOptions {
fn default() -> Self {
InequalityHandlingOptions {
mu: 1.0,
mu_reduce: 0.1,
min_mu: 1e-9,
tol: 1e-7,
max_iter: 500,
radius: 1.0,
h: 1e-7,
}
}
}
pub struct InequalityHandling {
pub options: InequalityHandlingOptions,
}
impl Default for InequalityHandling {
fn default() -> Self {
InequalityHandling {
options: InequalityHandlingOptions::default(),
}
}
}
impl InequalityHandling {
pub fn new(options: InequalityHandlingOptions) -> Self {
InequalityHandling { options }
}
pub fn minimize<F>(
&self,
func: F,
x0: &[f64],
ineq_constraints: &[Box<dyn Fn(&[f64]) -> f64>],
) -> OptimizeResult<TrustConstrResult>
where
F: Fn(&[f64]) -> f64,
{
let n = x0.len();
let n_ineq = ineq_constraints.len();
let h = self.options.h;
if n == 0 {
return Err(OptimizeError::InvalidInput(
"Initial point must be non-empty".to_string(),
));
}
let n_aug = n + n_ineq;
let mut z = vec![0.0f64; n_aug];
for i in 0..n {
z[i] = x0[i];
}
for j in 0..n_ineq {
let gj = (ineq_constraints[j])(&z[0..n]);
z[n + j] = (-gj + 1e-4).max(1e-8);
}
let mut mu = self.options.mu;
let mut radius = self.options.radius;
let mut nfev = 0usize;
let mut njev = 0usize;
let mut total_iter = 0usize;
let barrier = |z: &[f64], mu: f64, nfev: &mut usize| -> f64 {
let x = &z[0..n];
let s = &z[n..n_aug];
let f = func(x);
*nfev += 1;
let mut barrier_val = 0.0f64;
let mut pen = 0.0f64;
for (j, g) in ineq_constraints.iter().enumerate() {
let gj = g(x);
*nfev += 1;
if s[j] > 0.0 {
barrier_val -= mu * s[j].ln();
} else {
barrier_val += 1e10; }
pen += (gj + s[j]).powi(2);
}
f + barrier_val + 1000.0 * mu * pen
};
while mu >= self.options.min_mu {
let mut f_cur = barrier(&z, mu, &mut nfev);
for _inner in 0..self.options.max_iter / 10 {
total_iter += 1;
let mut grad = vec![0.0f64; n_aug];
for i in 0..n_aug {
let mut zf = z.clone();
zf[i] += h;
if i >= n && zf[i] <= 0.0 {
grad[i] = 1e10;
continue;
}
grad[i] = (barrier(&zf, mu, &mut nfev) - f_cur) / h;
njev += 1;
}
let gnorm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
if gnorm < self.options.tol {
break;
}
let step = (radius / gnorm).min(1.0);
let mut z_trial = vec![0.0f64; n_aug];
for i in 0..n_aug {
z_trial[i] = z[i] - step * grad[i];
}
for j in 0..n_ineq {
if z_trial[n + j] <= 0.0 {
z_trial[n + j] = 1e-10;
}
}
let f_trial = barrier(&z_trial, mu, &mut nfev);
if f_trial < f_cur {
z = z_trial;
f_cur = f_trial;
radius = (radius * 1.5).min(10.0);
} else {
radius *= 0.5;
if radius < 1e-12 {
break;
}
}
}
mu *= self.options.mu_reduce;
}
let x_final = z[0..n].to_vec();
let f_final = func(&x_final);
nfev += 1;
let mut cv_final = 0.0f64;
for g in ineq_constraints {
let gv = g(&x_final);
nfev += 1;
if gv > 0.0 {
cv_final += gv;
}
}
let mut grad_f = vec![0.0f64; n];
let f0 = func(&x_final);
nfev += 1;
for i in 0..n {
let mut xf = x_final.clone();
xf[i] += h;
grad_f[i] = (func(&xf) - f0) / h;
nfev += 1;
njev += 1;
}
Ok(TrustConstrResult {
x: x_final,
fun: f_final,
grad: grad_f,
constraint_violation: cv_final,
lambda_eq: vec![],
lambda_ineq: z[n..n_aug].to_vec(),
nit: total_iter,
nfev,
njev,
trust_radius: radius,
success: cv_final < self.options.tol * 100.0,
message: "Interior-point TR optimization completed".to_string(),
})
}
}
#[derive(Debug, Clone)]
pub struct FilterMethodTROptions {
pub initial_radius: f64,
pub min_radius: f64,
pub max_radius: f64,
pub gamma_f: f64,
pub gamma_theta: f64,
pub eta1: f64,
pub max_iter: usize,
pub tol: f64,
pub h: f64,
}
impl Default for FilterMethodTROptions {
fn default() -> Self {
FilterMethodTROptions {
initial_radius: 1.0,
min_radius: 1e-10,
max_radius: 100.0,
gamma_f: 1e-5,
gamma_theta: 1e-5,
eta1: 0.1,
max_iter: 500,
tol: 1e-7,
h: 1e-7,
}
}
}
#[derive(Debug, Clone)]
struct FilterEntry {
f_val: f64,
theta: f64,
}
impl FilterEntry {
fn dominates(&self, f: f64, theta: f64, gamma_f: f64, gamma_theta: f64) -> bool {
self.f_val - gamma_f * self.theta <= f && self.theta * (1.0 - gamma_theta) <= theta
}
}
pub struct FilterMethodTR {
pub options: FilterMethodTROptions,
}
impl Default for FilterMethodTR {
fn default() -> Self {
FilterMethodTR {
options: FilterMethodTROptions::default(),
}
}
}
impl FilterMethodTR {
pub fn new(options: FilterMethodTROptions) -> Self {
FilterMethodTR { options }
}
pub fn minimize<F>(
&self,
func: F,
x0: &[f64],
constraints: &[Box<dyn Fn(&[f64]) -> f64>],
) -> OptimizeResult<TrustConstrResult>
where
F: Fn(&[f64]) -> f64,
{
let n = x0.len();
let h = self.options.h;
if n == 0 {
return Err(OptimizeError::InvalidInput(
"Initial point must be non-empty".to_string(),
));
}
let mut x = x0.to_vec();
let mut radius = self.options.initial_radius;
let mut nfev = 0usize;
let mut njev = 0usize;
let theta = |x: &[f64], nfev: &mut usize| -> f64 {
constraints
.iter()
.map(|c| {
*nfev += 1;
c(x).max(0.0)
})
.sum()
};
let f0 = func(&x);
nfev += 1;
let theta0 = theta(&x, &mut nfev);
let mut filter: Vec<FilterEntry> = vec![FilterEntry { f_val: f0, theta: theta0 }];
let is_filter_acceptable = |f: f64, th: f64, filter: &[FilterEntry]| -> bool {
for entry in filter {
if entry.dominates(
f,
th,
self.options.gamma_f,
self.options.gamma_theta,
) {
return false;
}
}
true
};
let mut f_cur = f0;
for iter in 0..self.options.max_iter {
let mut grad = vec![0.0f64; n];
for i in 0..n {
let mut xf = x.clone();
xf[i] += h;
grad[i] = (func(&xf) - f_cur) / h;
nfev += 1;
njev += 1;
}
let gnorm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
let theta_cur = theta(&x, &mut nfev);
if gnorm < self.options.tol && theta_cur < self.options.tol {
let mut grad_f = vec![0.0f64; n];
let f0c = func(&x);
nfev += 1;
for i in 0..n {
let mut xf = x.clone();
xf[i] += h;
grad_f[i] = (func(&xf) - f0c) / h;
nfev += 1;
}
return Ok(TrustConstrResult {
x,
fun: f_cur,
grad: grad_f,
constraint_violation: theta_cur,
lambda_eq: vec![],
lambda_ineq: vec![],
nit: iter + 1,
nfev,
njev,
trust_radius: radius,
success: true,
message: "Filter-TR converged".to_string(),
});
}
let step_len = (radius / gnorm).min(radius);
let mut x_trial = vec![0.0f64; n];
for i in 0..n {
x_trial[i] = x[i] - step_len * grad[i];
}
let f_trial = func(&x_trial);
nfev += 1;
let theta_trial = theta(&x_trial, &mut nfev);
let actual_red = f_cur - f_trial;
let predicted_red = step_len * gnorm;
let rho = if predicted_red.abs() > 1e-14 {
actual_red / predicted_red
} else {
0.0
};
let accepted = is_filter_acceptable(f_trial, theta_trial, &filter);
if accepted && (rho > self.options.eta1 || theta_trial < theta_cur * 0.9) {
x = x_trial;
f_cur = f_trial;
filter.retain(|e| {
!(e.f_val >= f_cur
&& e.theta >= theta_trial * (1.0 - self.options.gamma_theta))
});
filter.push(FilterEntry {
f_val: f_cur,
theta: theta_trial,
});
if rho > 0.75 {
radius = (radius * self.options.initial_radius.sqrt())
.min(self.options.max_radius);
}
} else {
radius = (radius * 0.25).max(self.options.min_radius);
}
if radius < self.options.min_radius {
break;
}
}
let theta_final = theta(&x, &mut nfev);
Ok(TrustConstrResult {
x,
fun: f_cur,
grad: vec![0.0f64; n],
constraint_violation: theta_final,
lambda_eq: vec![],
lambda_ineq: vec![],
nit: self.options.max_iter,
nfev,
njev,
trust_radius: radius,
success: theta_final < self.options.tol * 100.0,
message: "Filter-TR: maximum iterations reached".to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rosenbrock(x: &[f64]) -> f64 {
(1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0].powi(2)).powi(2)
}
#[test]
fn test_trust_constr_unconstrained() {
let tc = TrustConstr::default();
let result = tc.minimize(rosenbrock, &[0.0, 0.0], &[], &[]).expect("failed to create result");
assert!(result.fun < 1.0, "Expected fun < 1.0, got {}", result.fun);
}
#[test]
fn test_trust_constr_with_equality() {
let func = |x: &[f64]| (x[0] - 2.0).powi(2) + (x[1] - 2.0).powi(2);
let eq_c: Vec<Box<dyn Fn(&[f64]) -> f64>> =
vec![Box::new(|x: &[f64]| x[0] + x[1] - 3.0)];
let tc = TrustConstr::new(TrustConstrOptions {
max_iter: 500,
..Default::default()
});
let result = tc.minimize(func, &[0.5, 0.5], &eq_c, &[]).expect("failed to create result");
assert!(result.constraint_violation < 0.5, "cv = {}", result.constraint_violation);
}
#[test]
fn test_trust_constr_with_inequality() {
let func = |x: &[f64]| (x[0] - 1.0).powi(2) + (x[1] - 1.0).powi(2);
let ineq_c: Vec<Box<dyn Fn(&[f64]) -> f64>> =
vec![Box::new(|x: &[f64]| x[0] + x[1] - 1.0)];
let tc = TrustConstr::new(TrustConstrOptions {
max_iter: 500,
..Default::default()
});
let result = tc.minimize(func, &[0.25, 0.25], &[], &ineq_c).expect("failed to create result");
assert!(result.fun < 1.0, "Expected fun < 1.0, got {}", result.fun);
}
#[test]
fn test_subproblem_solver_cauchy_point() {
let solver = SubproblemSolver::default();
let g = vec![1.0, 0.0];
let (p, on_boundary) = solver.solve(&g, |v| v.to_vec(), 0.5);
assert!((p[0] + 0.5).abs() < 0.1, "p[0] should be near -0.5, got {}", p[0]);
assert!(on_boundary, "Should hit boundary");
}
#[test]
fn test_equality_constrained_tr() {
let func = |x: &[f64]| x[0].powi(2) + x[1].powi(2);
let constraints: Vec<Box<dyn Fn(&[f64]) -> f64>> =
vec![Box::new(|x: &[f64]| x[0] + x[1] - 1.0)];
let ec = EqualityConstrained::new(EqualityConstrainedOptions {
max_outer: 30,
max_inner: 100,
outer_tol: 1e-5,
..Default::default()
});
let result = ec.minimize(func, &[0.5, 0.5], &constraints).expect("failed to create result");
assert!(result.fun < 0.6, "Expected fun < 0.6, got {}", result.fun);
}
#[test]
fn test_inequality_handling_interior_point() {
let func = |x: &[f64]| x[0].powi(2) + x[1].powi(2);
let ineq_c: Vec<Box<dyn Fn(&[f64]) -> f64>> =
vec![Box::new(|x: &[f64]| -(x[0] + x[1] - 1.0))];
let ih = InequalityHandling::default();
let result = ih.minimize(func, &[1.0, 1.0], &ineq_c).expect("failed to create result");
assert!(result.fun < 1.0, "Expected fun < 1.0, got {}", result.fun);
}
#[test]
fn test_filter_tr_unconstrained() {
let ft = FilterMethodTR::default();
let result = ft.minimize(rosenbrock, &[0.0, 0.0], &[]).expect("failed to create result");
assert!(result.fun < 1.0, "Expected fun < 1.0, got {}", result.fun);
}
#[test]
fn test_filter_tr_with_constraint() {
let func = |x: &[f64]| (x[0] - 2.0).powi(2) + (x[1] - 2.0).powi(2);
let c: Vec<Box<dyn Fn(&[f64]) -> f64>> =
vec![Box::new(|x: &[f64]| x[0] + x[1] - 3.0)];
let ft = FilterMethodTR::new(FilterMethodTROptions {
max_iter: 500,
..Default::default()
});
let result = ft.minimize(func, &[0.5, 0.5], &c).expect("failed to create result");
assert!(result.fun < 2.0, "Expected fun < 2.0, got {}", result.fun);
}
}