use crate::error::{OptimizeError, OptimizeResult};
use crate::result::OptimizeResults;
use scirs2_core::ndarray::{Array1, ArrayView1};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PenaltyKind {
External,
Interior,
ExactL1,
}
#[derive(Debug, Clone)]
pub struct PenaltyOptions {
pub kind: PenaltyKind,
pub mu_init: f64,
pub mu_max: f64,
pub mu_factor: f64,
pub max_outer_iter: usize,
pub constraint_tol: f64,
pub optimality_tol: f64,
pub eps: f64,
}
impl Default for PenaltyOptions {
fn default() -> Self {
PenaltyOptions {
kind: PenaltyKind::External,
mu_init: 1.0,
mu_max: 1e10,
mu_factor: 10.0,
max_outer_iter: 100,
constraint_tol: 1e-6,
optimality_tol: 1e-8,
eps: 1e-7,
}
}
}
#[derive(Debug, Clone)]
pub struct PenaltyResult {
pub x: Array1<f64>,
pub fun: f64,
pub nit: usize,
pub nfev: usize,
pub success: bool,
pub message: String,
pub mu: f64,
pub constraint_violation: f64,
}
impl From<PenaltyResult> for OptimizeResults<f64> {
fn from(r: PenaltyResult) -> Self {
OptimizeResults {
x: r.x,
fun: r.fun,
jac: None,
hess: None,
constr: None,
nit: r.nit,
nfev: r.nfev,
njev: 0,
nhev: 0,
maxcv: 0,
message: r.message,
success: r.success,
status: if r.success { 0 } else { 1 },
}
}
}
fn minimize_penalty_subproblem<P>(
penalty_fn: P,
x0: &[f64],
max_iter: usize,
gtol: f64,
eps: f64,
nfev: &mut usize,
) -> Vec<f64>
where
P: Fn(&[f64]) -> f64,
{
let n = x0.len();
let mut x = x0.to_vec();
let m = 5usize;
let mut s_hist: Vec<Vec<f64>> = Vec::new();
let mut y_hist: Vec<Vec<f64>> = Vec::new();
let mut rho_hist: Vec<f64> = Vec::new();
let compute_grad = |xv: &[f64], nfev: &mut usize| -> Vec<f64> {
let h = eps;
let mut g = vec![0.0; n];
let mut xp = xv.to_vec();
let mut xm = xv.to_vec();
*nfev += 2 * n;
for i in 0..n {
xp[i] = xv[i] + h;
xm[i] = xv[i] - h;
g[i] = (penalty_fn(&xp) - penalty_fn(&xm)) / (2.0 * h);
xp[i] = xv[i];
xm[i] = xv[i];
}
g
};
let mut g = compute_grad(&x, nfev);
for _iter in 0..max_iter {
let gnorm: f64 = g.iter().map(|v| v * v).sum::<f64>().sqrt();
if gnorm < gtol {
break;
}
let mut q = g.clone();
let hist_len = s_hist.len();
let mut alpha_hist = vec![0.0_f64; hist_len];
for i in (0..hist_len).rev() {
let si = &s_hist[i];
let yi = &y_hist[i];
let rho_i = rho_hist[i];
let dot: f64 = si.iter().zip(q.iter()).map(|(&s, &qi)| s * qi).sum();
alpha_hist[i] = rho_i * dot;
let a = alpha_hist[i];
for j in 0..n {
q[j] -= a * yi[j];
}
}
let mut r = if hist_len > 0 {
let last_s = &s_hist[hist_len - 1];
let last_y = &y_hist[hist_len - 1];
let sy: f64 = last_s.iter().zip(last_y.iter()).map(|(&s, &y)| s * y).sum();
let yy: f64 = last_y.iter().map(|v| v * v).sum();
let scale = if yy > 1e-15 { sy / yy } else { 1.0 };
q.iter().map(|&qi| scale * qi).collect::<Vec<f64>>()
} else {
q.clone()
};
for i in 0..hist_len {
let si = &s_hist[i];
let yi = &y_hist[i];
let rho_i = rho_hist[i];
let dot: f64 = yi.iter().zip(r.iter()).map(|(&y, &ri)| y * ri).sum();
let beta = rho_i * dot;
let diff = alpha_hist[i] - beta;
for j in 0..n {
r[j] += si[j] * diff;
}
}
let d: Vec<f64> = r.iter().map(|v| -v).collect();
*nfev += 1;
let fx = penalty_fn(&x);
let dg: f64 = d.iter().zip(g.iter()).map(|(&di, &gi)| di * gi).sum();
let mut alpha = 1.0_f64;
for _ls in 0..20 {
let xnew: Vec<f64> = x.iter().zip(d.iter()).map(|(&xi, &di)| xi + alpha * di).collect();
*nfev += 1;
let fnew = penalty_fn(&xnew);
if fnew <= fx + 1e-4 * alpha * dg.min(0.0) {
break;
}
alpha *= 0.5;
}
let xnew: Vec<f64> = x.iter().zip(d.iter()).map(|(&xi, &di)| xi + alpha * di).collect();
let gnew = compute_grad(&xnew, nfev);
let s: Vec<f64> = xnew.iter().zip(x.iter()).map(|(&xni, &xi)| xni - xi).collect();
let y: Vec<f64> = gnew.iter().zip(g.iter()).map(|(&gni, &gi)| gni - gi).collect();
let sy: f64 = s.iter().zip(y.iter()).map(|(&si, &yi)| si * yi).sum();
if sy > 1e-10 {
if s_hist.len() >= m {
s_hist.remove(0);
y_hist.remove(0);
rho_hist.remove(0);
}
s_hist.push(s);
y_hist.push(y);
rho_hist.push(1.0 / sy);
}
x = xnew;
g = gnew;
}
x
}
pub struct PenaltyMethod {
pub options: PenaltyOptions,
}
impl PenaltyMethod {
pub fn new() -> Self {
PenaltyMethod {
options: PenaltyOptions::default(),
}
}
pub fn with_options(options: PenaltyOptions) -> Self {
PenaltyMethod { options }
}
fn compute_violation<E, G>(
&self,
x: &[f64],
eq_cons: &[E],
ineq_cons: &[G],
) -> f64
where
E: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> f64,
{
let eq_viol: f64 = eq_cons.iter().map(|e| e(x).powi(2)).sum();
let ineq_viol: f64 = ineq_cons.iter().map(|g| g(x).max(0.0).powi(2)).sum();
(eq_viol + ineq_viol).sqrt()
}
pub fn solve_slice<F, E, G>(
&self,
f: F,
eq_cons: &[E],
ineq_cons: &[G],
x0: &[f64],
) -> OptimizeResult<PenaltyResult>
where
F: Fn(&[f64]) -> f64,
E: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> f64,
{
match self.options.kind {
PenaltyKind::External => self.solve_external_slice(f, eq_cons, ineq_cons, x0),
PenaltyKind::Interior => self.solve_interior_slice(f, ineq_cons, x0),
PenaltyKind::ExactL1 => self.solve_l1_slice(f, eq_cons, ineq_cons, x0),
}
}
pub fn solve<F, E, G>(
&self,
f: F,
eq_cons: &[E],
ineq_cons: &[G],
x0: &Array1<f64>,
) -> OptimizeResult<PenaltyResult>
where
F: Fn(&ArrayView1<f64>) -> f64,
E: Fn(&ArrayView1<f64>) -> f64,
G: Fn(&ArrayView1<f64>) -> f64,
{
let f_slice = |x: &[f64]| {
let arr = Array1::from_vec(x.to_vec());
f(&arr.view())
};
let eq_slice: Vec<Box<dyn Fn(&[f64]) -> f64>> = eq_cons.iter().map(|e| {
Box::new(move |x: &[f64]| {
let arr = Array1::from_vec(x.to_vec());
e(&arr.view())
}) as Box<dyn Fn(&[f64]) -> f64>
}).collect();
let ineq_slice: Vec<Box<dyn Fn(&[f64]) -> f64>> = ineq_cons.iter().map(|g| {
Box::new(move |x: &[f64]| {
let arr = Array1::from_vec(x.to_vec());
g(&arr.view())
}) as Box<dyn Fn(&[f64]) -> f64>
}).collect();
let x0_slice: Vec<f64> = x0.iter().copied().collect();
self.solve_slice(f_slice, &eq_slice, &ineq_slice, &x0_slice)
}
fn solve_external_slice<F, E, G>(
&self,
f: F,
eq_cons: &[E],
ineq_cons: &[G],
x0: &[f64],
) -> OptimizeResult<PenaltyResult>
where
F: Fn(&[f64]) -> f64,
E: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> f64,
{
let mut x = x0.to_vec();
let mut mu = self.options.mu_init;
let mut nfev_total = 0usize;
let mut nit = 0usize;
for _outer in 0..self.options.max_outer_iter {
nit += 1;
let mu_local = mu;
let penalty_at = |xv: &[f64]| -> f64 {
let obj = f(xv);
let penalty: f64 = eq_cons.iter().map(|e| mu_local * e(xv).powi(2)).sum::<f64>()
+ ineq_cons.iter().map(|g| mu_local * g(xv).max(0.0).powi(2)).sum::<f64>();
obj + penalty
};
let new_x = minimize_penalty_subproblem(
penalty_at,
&x,
1000,
self.options.optimality_tol,
self.options.eps,
&mut nfev_total,
);
x = new_x;
let cv = self.compute_violation(&x, eq_cons, ineq_cons);
if cv <= self.options.constraint_tol {
let fun = f(&x);
return Ok(PenaltyResult {
x: Array1::from_vec(x),
fun,
nit,
nfev: nfev_total,
success: true,
message: "Converged: constraint violation below tolerance".to_string(),
mu,
constraint_violation: cv,
});
}
mu = (mu * self.options.mu_factor).min(self.options.mu_max);
}
let cv = self.compute_violation(&x, eq_cons, ineq_cons);
let fun = f(&x);
let success = cv <= self.options.constraint_tol;
Ok(PenaltyResult {
x: Array1::from_vec(x),
fun,
nit,
nfev: nfev_total,
success,
message: if success {
"Converged".to_string()
} else {
format!("Maximum outer iterations reached (cv={:.2e})", cv)
},
mu,
constraint_violation: cv,
})
}
fn solve_interior_slice<F, G>(
&self,
f: F,
ineq_cons: &[G],
x0: &[f64],
) -> OptimizeResult<PenaltyResult>
where
F: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> f64,
{
for g in ineq_cons.iter() {
if g(x0) >= 0.0 {
return Err(OptimizeError::InvalidInput(
"Interior penalty requires strictly feasible starting point (g_i(x0) < 0)"
.to_string(),
));
}
}
let mut x = x0.to_vec();
let mut mu = self.options.mu_init;
let mut nfev_total = 0usize;
let mut nit = 0usize;
for _outer in 0..self.options.max_outer_iter {
nit += 1;
if mu < 1e-12 {
break;
}
let mu_local = mu;
let barrier_fn = |xv: &[f64]| -> f64 {
let obj = f(xv);
let barrier: f64 = ineq_cons.iter().map(|g| {
let gv = g(xv);
if gv < -1e-15 {
-mu_local * gv.abs().ln()
} else {
f64::INFINITY
}
}).sum();
obj + barrier
};
let new_x = minimize_penalty_subproblem(
barrier_fn,
&x,
500,
mu * 0.01,
self.options.eps,
&mut nfev_total,
);
let feasible = ineq_cons.iter().all(|g| g(&new_x) < 0.0);
if feasible {
x = new_x;
}
mu /= self.options.mu_factor;
}
let fun = f(&x);
Ok(PenaltyResult {
fun,
x: Array1::from_vec(x),
nit,
nfev: nfev_total,
success: true,
message: "Interior penalty completed".to_string(),
mu,
constraint_violation: 0.0,
})
}
fn solve_l1_slice<F, E, G>(
&self,
f: F,
eq_cons: &[E],
ineq_cons: &[G],
x0: &[f64],
) -> OptimizeResult<PenaltyResult>
where
F: Fn(&[f64]) -> f64,
E: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> f64,
{
let mut x = x0.to_vec();
let mu = self.options.mu_init;
let mut nfev_total = 0usize;
let mut nit = 0usize;
for _outer in 0..self.options.max_outer_iter {
nit += 1;
let l1_fn = |xv: &[f64]| -> f64 {
let obj = f(xv);
let penalty: f64 = eq_cons.iter().map(|e| mu * e(xv).abs()).sum::<f64>()
+ ineq_cons.iter().map(|g| mu * g(xv).max(0.0)).sum::<f64>();
obj + penalty
};
let new_x = minimize_penalty_subproblem(
l1_fn,
&x,
1000,
self.options.optimality_tol,
self.options.eps,
&mut nfev_total,
);
x = new_x;
let eq_cv: f64 = eq_cons.iter().map(|e| e(&x).abs()).sum();
let ineq_cv: f64 = ineq_cons.iter().map(|g| g(&x).max(0.0)).sum();
let cv = eq_cv + ineq_cv;
if cv <= self.options.constraint_tol {
let fun = f(&x);
return Ok(PenaltyResult {
x: Array1::from_vec(x),
fun,
nit,
nfev: nfev_total,
success: true,
message: "L1 penalty converged".to_string(),
mu,
constraint_violation: cv,
});
}
}
let eq_cv: f64 = eq_cons.iter().map(|e| e(&x).abs()).sum();
let ineq_cv: f64 = ineq_cons.iter().map(|g| g(&x).max(0.0)).sum();
let cv = eq_cv + ineq_cv;
let fun = f(&x);
Ok(PenaltyResult {
x: Array1::from_vec(x),
fun,
nit,
nfev: nfev_total,
success: cv <= self.options.constraint_tol,
message: "L1 penalty max iterations reached".to_string(),
mu,
constraint_violation: cv,
})
}
}
impl Default for PenaltyMethod {
fn default() -> Self {
PenaltyMethod::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PenaltyMethodKind {
Static,
Dynamic,
Adaptive,
AugmentedLagrangian,
}
pub fn penalty_function<F, G, H>(
x: &[f64],
obj: F,
ineq_cons: &[G],
eq_cons: &[H],
penalty_coeff: f64,
kind: PenaltyKind,
) -> f64
where
F: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> f64,
H: Fn(&[f64]) -> f64,
{
let f_val = obj(x);
match kind {
PenaltyKind::External => {
let ineq_pen: f64 = ineq_cons
.iter()
.map(|g| penalty_coeff * g(x).max(0.0).powi(2))
.sum();
let eq_pen: f64 = eq_cons
.iter()
.map(|h| penalty_coeff * h(x).powi(2))
.sum();
f_val + ineq_pen + eq_pen
}
PenaltyKind::Interior => {
let barrier: f64 = ineq_cons
.iter()
.map(|g| {
let gv = g(x);
if gv < -1e-15 {
-penalty_coeff * gv.abs().ln()
} else {
f64::INFINITY
}
})
.sum();
f_val + barrier
}
PenaltyKind::ExactL1 => {
let ineq_pen: f64 = ineq_cons
.iter()
.map(|g| penalty_coeff * g(x).max(0.0))
.sum();
let eq_pen: f64 = eq_cons
.iter()
.map(|h| penalty_coeff * h(x).abs())
.sum();
f_val + ineq_pen + eq_pen
}
}
}
#[derive(Debug, Clone)]
pub struct AdaptivePenalty {
pub penalty_coeff: f64,
pub min_penalty: f64,
pub max_penalty: f64,
pub increase_factor: f64,
pub decrease_factor: f64,
pub improvement_threshold: f64,
pub allow_decrease: bool,
prev_violation: f64,
stall_count: usize,
pub stall_patience: usize,
}
impl Default for AdaptivePenalty {
fn default() -> Self {
AdaptivePenalty {
penalty_coeff: 1.0,
min_penalty: 1e-3,
max_penalty: 1e10,
increase_factor: 10.0,
decrease_factor: 0.5,
improvement_threshold: 0.25,
allow_decrease: false,
prev_violation: f64::INFINITY,
stall_count: 0,
stall_patience: 1,
}
}
}
impl AdaptivePenalty {
pub fn new(initial_penalty: f64) -> Self {
AdaptivePenalty {
penalty_coeff: initial_penalty,
..Default::default()
}
}
#[allow(clippy::too_many_arguments)]
pub fn with_config(
initial_penalty: f64,
min_penalty: f64,
max_penalty: f64,
increase_factor: f64,
decrease_factor: f64,
improvement_threshold: f64,
allow_decrease: bool,
stall_patience: usize,
) -> Self {
AdaptivePenalty {
penalty_coeff: initial_penalty,
min_penalty,
max_penalty,
increase_factor,
decrease_factor,
improvement_threshold,
allow_decrease,
prev_violation: f64::INFINITY,
stall_count: 0,
stall_patience,
}
}
pub fn update(&mut self, current_violation: f64) -> f64 {
if self.prev_violation.is_infinite() {
self.prev_violation = current_violation;
return self.penalty_coeff;
}
let relative_improvement = if self.prev_violation > 1e-15 {
(self.prev_violation - current_violation) / self.prev_violation
} else {
1.0
};
if relative_improvement < self.improvement_threshold {
self.stall_count += 1;
if self.stall_count >= self.stall_patience {
self.penalty_coeff =
(self.penalty_coeff * self.increase_factor).min(self.max_penalty);
self.stall_count = 0;
}
} else {
self.stall_count = 0;
if self.allow_decrease && relative_improvement > 0.5 {
self.penalty_coeff =
(self.penalty_coeff * self.decrease_factor).max(self.min_penalty);
}
}
self.prev_violation = current_violation;
self.penalty_coeff
}
pub fn reset(&mut self) {
self.prev_violation = f64::INFINITY;
self.stall_count = 0;
}
}
#[derive(Debug, Clone)]
pub struct AugLagOptions {
pub rho_init: f64,
pub rho_max: f64,
pub rho_factor: f64,
pub max_outer_iter: usize,
pub constraint_tol: f64,
pub optimality_tol: f64,
pub eps: f64,
pub violation_reduction_threshold: f64,
}
impl Default for AugLagOptions {
fn default() -> Self {
AugLagOptions {
rho_init: 1.0,
rho_max: 1e10,
rho_factor: 10.0,
max_outer_iter: 100,
constraint_tol: 1e-6,
optimality_tol: 1e-8,
eps: 1e-7,
violation_reduction_threshold: 0.25,
}
}
}
#[derive(Debug, Clone)]
pub struct AugLagResult {
pub x: Array1<f64>,
pub fun: f64,
pub nit: usize,
pub nfev: usize,
pub success: bool,
pub message: String,
pub lambda_eq: Vec<f64>,
pub lambda_ineq: Vec<f64>,
pub rho: f64,
pub constraint_violation: f64,
}
impl From<AugLagResult> for OptimizeResults<f64> {
fn from(r: AugLagResult) -> Self {
OptimizeResults {
x: r.x,
fun: r.fun,
jac: None,
hess: None,
constr: None,
nit: r.nit,
nfev: r.nfev,
njev: 0,
nhev: 0,
maxcv: 0,
message: r.message,
success: r.success,
status: if r.success { 0 } else { 1 },
}
}
}
#[derive(Debug, Clone)]
pub struct AugmentedLagrangianSolver {
pub options: AugLagOptions,
}
impl AugmentedLagrangianSolver {
pub fn new() -> Self {
AugmentedLagrangianSolver {
options: AugLagOptions::default(),
}
}
pub fn with_options(options: AugLagOptions) -> Self {
AugmentedLagrangianSolver { options }
}
fn compute_violation<E, G>(&self, x: &[f64], eq_cons: &[E], ineq_cons: &[G]) -> f64
where
E: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> f64,
{
let eq_sq: f64 = eq_cons.iter().map(|h| h(x).powi(2)).sum();
let ineq_sq: f64 = ineq_cons.iter().map(|g| g(x).max(0.0).powi(2)).sum();
(eq_sq + ineq_sq).sqrt()
}
pub fn solve<F, E, G>(
&self,
f: F,
eq_cons: &[E],
ineq_cons: &[G],
x0: &[f64],
) -> OptimizeResult<AugLagResult>
where
F: Fn(&[f64]) -> f64,
E: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> f64,
{
let n_eq = eq_cons.len();
let n_ineq = ineq_cons.len();
let mut lambda_eq = vec![0.0_f64; n_eq];
let mut lambda_ineq = vec![0.0_f64; n_ineq];
let mut rho = self.options.rho_init;
let mut x = x0.to_vec();
let mut nfev_total = 0usize;
let mut nit = 0usize;
let mut prev_violation = f64::INFINITY;
for _outer in 0..self.options.max_outer_iter {
nit += 1;
let lam_eq_snap = lambda_eq.clone();
let lam_ineq_snap = lambda_ineq.clone();
let rho_snap = rho;
let aug_lag = |xv: &[f64]| -> f64 {
let mut val = f(xv);
for (j, h) in eq_cons.iter().enumerate() {
let hv = h(xv);
val += lam_eq_snap[j] * hv + 0.5 * rho_snap * hv * hv;
}
for (i, g) in ineq_cons.iter().enumerate() {
let gv = g(xv);
let shifted = gv + lam_ineq_snap[i] / rho_snap;
if shifted > 0.0 {
val += lam_ineq_snap[i] * gv + 0.5 * rho_snap * gv * gv;
}
}
val
};
let new_x = minimize_penalty_subproblem(
aug_lag,
&x,
2000,
self.options.optimality_tol,
self.options.eps,
&mut nfev_total,
);
x = new_x;
let cv = self.compute_violation(&x, eq_cons, ineq_cons);
if cv <= self.options.constraint_tol {
let fun = f(&x);
return Ok(AugLagResult {
x: Array1::from_vec(x),
fun,
nit,
nfev: nfev_total,
success: true,
message: "Converged: constraint violation below tolerance".to_string(),
lambda_eq,
lambda_ineq,
rho,
constraint_violation: cv,
});
}
let violation_improvement = if prev_violation.is_finite() && prev_violation > 1e-15 {
(prev_violation - cv) / prev_violation
} else {
0.0
};
if violation_improvement >= self.options.violation_reduction_threshold {
for (j, h) in eq_cons.iter().enumerate() {
lambda_eq[j] += rho * h(&x);
}
for (i, g) in ineq_cons.iter().enumerate() {
lambda_ineq[i] = (lambda_ineq[i] + rho * g(&x)).max(0.0);
}
} else {
rho = (rho * self.options.rho_factor).min(self.options.rho_max);
}
prev_violation = cv;
}
let cv = self.compute_violation(&x, eq_cons, ineq_cons);
let fun = f(&x);
let success = cv <= self.options.constraint_tol;
Ok(AugLagResult {
x: Array1::from_vec(x),
fun,
nit,
nfev: nfev_total,
success,
message: if success {
"Converged".to_string()
} else {
format!(
"Maximum outer iterations ({}) reached; cv={:.2e}",
self.options.max_outer_iter, cv
)
},
lambda_eq,
lambda_ineq,
rho,
constraint_violation: cv,
})
}
}
impl Default for AugmentedLagrangianSolver {
fn default() -> Self {
AugmentedLagrangianSolver::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_penalty_equality_constraint() {
let f = |x: &[f64]| x[0].powi(2) + x[1].powi(2);
let h = |x: &[f64]| x[0] + x[1] - 1.0;
let opts = PenaltyOptions {
kind: PenaltyKind::External,
mu_init: 1.0,
mu_factor: 10.0,
mu_max: 1e8,
max_outer_iter: 30,
constraint_tol: 1e-4,
..Default::default()
};
let solver = PenaltyMethod::with_options(opts);
let result = solver
.solve_slice(f, &[h], &[] as &[fn(&[f64]) -> f64], &[0.0, 0.0])
.expect("solve failed");
assert_abs_diff_eq!(result.x[0], 0.5, epsilon = 1e-2);
assert_abs_diff_eq!(result.x[1], 0.5, epsilon = 1e-2);
assert_abs_diff_eq!(result.fun, 0.5, epsilon = 1e-2);
}
#[test]
fn test_penalty_inequality_constraint() {
let f = |x: &[f64]| x[0].powi(2) + x[1].powi(2);
let g = |x: &[f64]| 1.0 - x[0] - x[1];
let opts = PenaltyOptions {
kind: PenaltyKind::External,
mu_init: 1.0,
mu_factor: 10.0,
mu_max: 1e8,
max_outer_iter: 40,
constraint_tol: 1e-3,
..Default::default()
};
let solver = PenaltyMethod::with_options(opts);
let result = solver
.solve_slice(f, &[] as &[fn(&[f64]) -> f64], &[g], &[2.0, 2.0])
.expect("solve failed");
assert_abs_diff_eq!(result.fun, 0.5, epsilon = 1e-2);
}
#[test]
fn test_penalty_no_constraints() {
let f = |x: &[f64]| (x[0] - 3.0).powi(2) + (x[1] - 4.0).powi(2);
let solver = PenaltyMethod::new();
let result = solver
.solve_slice(
f,
&[] as &[fn(&[f64]) -> f64],
&[] as &[fn(&[f64]) -> f64],
&[0.0, 0.0],
)
.expect("solve failed");
assert_abs_diff_eq!(result.fun, 0.0, epsilon = 1e-3);
assert_abs_diff_eq!(result.x[0], 3.0, epsilon = 1e-2);
assert_abs_diff_eq!(result.x[1], 4.0, epsilon = 1e-2);
}
#[test]
fn test_penalty_l1_equality() {
let f = |x: &[f64]| (x[0] - 1.0).powi(2) + (x[1] - 2.0).powi(2);
let h = |x: &[f64]| x[0] + x[1] - 3.0;
let opts = PenaltyOptions {
kind: PenaltyKind::ExactL1,
mu_init: 10.0,
max_outer_iter: 50,
constraint_tol: 1e-3,
..Default::default()
};
let solver = PenaltyMethod::with_options(opts);
let result = solver
.solve_slice(f, &[h], &[] as &[fn(&[f64]) -> f64], &[0.0, 0.0])
.expect("solve failed");
assert_abs_diff_eq!(result.fun, 0.0, epsilon = 1e-2);
}
#[test]
fn test_penalty_interior_barrier() {
let f = |x: &[f64]| (x[0] - 0.5).powi(2);
let g = |x: &[f64]| x[0] - 0.999;
let opts = PenaltyOptions {
kind: PenaltyKind::Interior,
mu_init: 0.1,
mu_factor: 5.0,
max_outer_iter: 20,
..Default::default()
};
let solver = PenaltyMethod::with_options(opts);
let result = solver
.solve_slice(f, &[] as &[fn(&[f64]) -> f64], &[g], &[0.3])
.expect("solve failed");
assert_abs_diff_eq!(result.x[0], 0.5, epsilon = 0.1);
}
#[test]
fn test_penalty_mixed_constraints() {
let f = |x: &[f64]| x[0].powi(2) + x[1].powi(2) + x[2].powi(2);
let h = |x: &[f64]| x[0] + x[1] + x[2] - 3.0;
let g = |x: &[f64]| x[2] - 1.0;
let opts = PenaltyOptions {
kind: PenaltyKind::External,
mu_init: 1.0,
mu_factor: 5.0,
mu_max: 1e7,
max_outer_iter: 50,
constraint_tol: 1e-3,
..Default::default()
};
let solver = PenaltyMethod::with_options(opts);
let result = solver
.solve_slice(f, &[h], &[g], &[0.0, 0.0, 0.0])
.expect("solve failed");
assert!(result.fun <= 4.0, "fun={}", result.fun);
}
#[test]
fn test_penalty_arrayview1_interface() {
use scirs2_core::ndarray::{array, ArrayView1};
let f = |x: &ArrayView1<f64>| x[0].powi(2) + x[1].powi(2);
let h = |x: &ArrayView1<f64>| x[0] + x[1] - 1.0;
let opts = PenaltyOptions {
kind: PenaltyKind::External,
max_outer_iter: 20,
constraint_tol: 1e-3,
..Default::default()
};
let solver = PenaltyMethod::with_options(opts);
let x0 = array![0.0, 0.0];
let result = solver
.solve(f, &[h], &[] as &[fn(&ArrayView1<f64>) -> f64], &x0)
.expect("solve failed");
assert_abs_diff_eq!(result.fun, 0.5, epsilon = 5e-2);
}
#[test]
fn test_penalty_function_external_feasible() {
let f = |x: &[f64]| x[0].powi(2) + x[1].powi(2);
let g = |x: &[f64]| x[0] + x[1] - 1.0;
let val = penalty_function(
&[0.0, 0.0],
f,
&[g],
&[] as &[fn(&[f64]) -> f64],
10.0,
PenaltyKind::External,
);
assert_abs_diff_eq!(val, 0.0, epsilon = 1e-12);
}
#[test]
fn test_penalty_function_external_violated() {
let f = |x: &[f64]| x[0].powi(2) + x[1].powi(2);
let g = |x: &[f64]| x[0] + x[1] - 1.0;
let mu = 5.0;
let val = penalty_function(
&[1.0, 1.0],
f,
&[g],
&[] as &[fn(&[f64]) -> f64],
mu,
PenaltyKind::External,
);
assert_abs_diff_eq!(val, 2.0 + mu * 1.0_f64.powi(2), epsilon = 1e-12);
}
#[test]
fn test_penalty_function_equality() {
let f = |_x: &[f64]| 0.0;
let h = |x: &[f64]| x[0] - 1.0;
let mu = 3.0;
let val = penalty_function(
&[2.0],
f,
&[] as &[fn(&[f64]) -> f64],
&[h],
mu,
PenaltyKind::External,
);
assert_abs_diff_eq!(val, 3.0, epsilon = 1e-12);
}
#[test]
fn test_adaptive_penalty_increases_on_stall() {
let mut ap = AdaptivePenalty::new(1.0);
ap.increase_factor = 5.0;
ap.improvement_threshold = 0.1;
ap.stall_patience = 1;
let p0 = ap.update(1.0);
assert_abs_diff_eq!(p0, 1.0, epsilon = 1e-12);
let p1 = ap.update(0.95);
assert!(p1 > 1.0, "Penalty should have increased; got {p1}");
}
#[test]
fn test_adaptive_penalty_no_increase_on_good_progress() {
let mut ap = AdaptivePenalty::new(1.0);
ap.improvement_threshold = 0.1;
ap.stall_patience = 1;
ap.update(1.0); let p1 = ap.update(0.5); assert_abs_diff_eq!(p1, 1.0, epsilon = 1e-12);
}
#[test]
fn test_adaptive_penalty_capped_at_max() {
let mut ap = AdaptivePenalty::with_config(
1e9, 1e-3, 1e10, 1000.0, 0.5, 0.1, false, 1,
);
ap.update(1.0);
let p = ap.update(1.0); assert!(p <= 1e10, "Penalty exceeded max; got {p}");
}
#[test]
fn test_aug_lag_equality_constraint() {
let f = |x: &[f64]| x[0].powi(2) + x[1].powi(2);
let h = |x: &[f64]| x[0] + x[1] - 1.0;
let opts = AugLagOptions {
rho_init: 1.0,
rho_factor: 5.0,
max_outer_iter: 50,
constraint_tol: 1e-4,
..Default::default()
};
let solver = AugmentedLagrangianSolver::with_options(opts);
let result = solver
.solve(f, &[h], &[] as &[fn(&[f64]) -> f64], &[0.0, 0.0])
.expect("AugLag solve failed");
assert!(result.success || result.constraint_violation < 1e-2,
"cv={}", result.constraint_violation);
assert_abs_diff_eq!(result.fun, 0.5, epsilon = 0.05);
}
#[test]
fn test_aug_lag_inequality_constraint() {
let f = |x: &[f64]| x[0].powi(2);
let g = |x: &[f64]| 1.0 - x[0];
let opts = AugLagOptions {
rho_init: 1.0,
rho_factor: 10.0,
max_outer_iter: 60,
constraint_tol: 1e-3,
..Default::default()
};
let solver = AugmentedLagrangianSolver::with_options(opts);
let result = solver
.solve(f, &[] as &[fn(&[f64]) -> f64], &[g], &[0.5])
.expect("AugLag solve failed");
assert!(
result.x[0] >= 0.9 && result.x[0] <= 1.2,
"Expected x~1.0, got x={}",
result.x[0]
);
}
#[test]
fn test_aug_lag_no_constraints() {
let f = |x: &[f64]| (x[0] - 2.0).powi(2) + (x[1] - 3.0).powi(2);
let solver = AugmentedLagrangianSolver::new();
let result = solver
.solve(
f,
&[] as &[fn(&[f64]) -> f64],
&[] as &[fn(&[f64]) -> f64],
&[0.0, 0.0],
)
.expect("AugLag solve failed");
assert_abs_diff_eq!(result.x[0], 2.0, epsilon = 1e-2);
assert_abs_diff_eq!(result.x[1], 3.0, epsilon = 1e-2);
assert_abs_diff_eq!(result.fun, 0.0, epsilon = 1e-3);
}
#[test]
fn test_penalty_method_kind_variants() {
assert_eq!(PenaltyMethodKind::Static, PenaltyMethodKind::Static);
assert_ne!(PenaltyMethodKind::Dynamic, PenaltyMethodKind::Adaptive);
assert_ne!(
PenaltyMethodKind::AugmentedLagrangian,
PenaltyMethodKind::Static
);
}
}