use crate::error::{OptimizeError, OptimizeResult};
#[derive(Debug, Clone)]
pub struct BendersOptions {
pub max_iter: usize,
pub tol: f64,
pub feas_tol: f64,
pub max_sub_iter: usize,
pub sub_tol: f64,
pub multi_cut: bool,
}
impl Default for BendersOptions {
fn default() -> Self {
BendersOptions {
max_iter: 100,
tol: 1e-7,
feas_tol: 1e-7,
max_sub_iter: 500,
sub_tol: 1e-9,
multi_cut: false,
}
}
}
#[derive(Debug, Clone)]
pub struct BendersResult {
pub x: Vec<f64>,
pub y: Vec<f64>,
pub fun: f64,
pub lower_bound: f64,
pub upper_bound: f64,
pub n_cuts: usize,
pub nit: usize,
pub success: bool,
pub message: String,
}
pub struct BendersDecomposition {
pub options: BendersOptions,
}
impl Default for BendersDecomposition {
fn default() -> Self {
BendersDecomposition {
options: BendersOptions::default(),
}
}
}
impl BendersDecomposition {
pub fn new(options: BendersOptions) -> Self {
BendersDecomposition { options }
}
pub fn solve<FM, FS>(
&self,
master_obj: FM,
sub_obj: FS,
x0: &[f64],
y0: &[f64],
) -> OptimizeResult<BendersResult>
where
FM: Fn(&[f64]) -> f64,
FS: Fn(&[f64], &[f64]) -> f64,
{
let nx = x0.len();
let ny = y0.len();
if nx == 0 {
return Err(OptimizeError::InvalidInput(
"First-stage (master) variables must be non-empty".to_string(),
));
}
let h = 1e-7f64;
let mut x = x0.to_vec();
let mut y = y0.to_vec();
let mut lower_bound = f64::NEG_INFINITY;
let mut upper_bound = f64::INFINITY;
let mut n_cuts = 0usize;
let mut nit = 0usize;
let mut cuts: Vec<(Vec<f64>, f64)> = Vec::new();
for outer in 0..self.options.max_iter {
nit = outer + 1;
let mut y_opt = y.clone();
let mut sub_f = sub_obj(&x, &y_opt);
for _sub in 0..self.options.max_sub_iter {
let mut grad_y = vec![0.0f64; ny];
for i in 0..ny {
let mut yf = y_opt.clone();
yf[i] += h;
grad_y[i] = (sub_obj(&x, &yf) - sub_f) / h;
}
let gnorm: f64 = grad_y.iter().map(|g| g * g).sum::<f64>().sqrt();
if gnorm < self.options.sub_tol {
break;
}
let step = 0.1f64 / (1.0 + gnorm);
let mut y_new = vec![0.0f64; ny];
for i in 0..ny {
y_new[i] = y_opt[i] - step * grad_y[i];
}
let f_new = sub_obj(&x, &y_new);
if f_new < sub_f {
y_opt = y_new;
sub_f = f_new;
} else {
break;
}
}
let mut cut_grad = vec![0.0f64; nx];
for i in 0..nx {
let mut xf = x.clone();
xf[i] += h;
cut_grad[i] = (sub_obj(&xf, &y_opt) - sub_obj(&x, &y_opt)) / h;
}
let cut_offset = sub_f;
cuts.push((cut_grad, cut_offset));
n_cuts += 1;
let master_f = master_obj(&x);
let current_ub = master_f + sub_f;
if current_ub < upper_bound {
upper_bound = current_ub;
y = y_opt.clone();
}
let master_with_cuts = |x: &[f64]| -> f64 {
let base = master_obj(x);
let recourse = cuts
.iter()
.map(|(g, offset)| {
let inner: f64 = g.iter().zip(x.iter()).map(|(gi, xi)| gi * xi).sum();
inner - g.iter().zip(x0.iter()).map(|(gi, xi)| gi * xi).sum::<f64>()
+ offset
})
.fold(f64::NEG_INFINITY, f64::max);
let recourse = if recourse.is_finite() { recourse } else { 0.0 };
base + recourse
};
let mut x_new = x.clone();
let mut master_val = master_with_cuts(&x_new);
for _master_iter in 0..200 {
let mut grad = vec![0.0f64; nx];
for i in 0..nx {
let mut xf = x_new.clone();
xf[i] += h;
grad[i] = (master_with_cuts(&xf) - master_val) / h;
}
let gnorm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
if gnorm < self.options.tol {
break;
}
let step = 0.1f64 / (1.0 + gnorm);
let mut xt = vec![0.0f64; nx];
for i in 0..nx {
xt[i] = x_new[i] - step * grad[i];
}
let ft = master_with_cuts(&xt);
if ft < master_val {
x_new = xt;
master_val = ft;
} else {
break;
}
}
lower_bound = master_val;
x = x_new;
let gap = upper_bound - lower_bound;
if gap.abs() < self.options.tol * (1.0 + upper_bound.abs()) {
break;
}
}
Ok(BendersResult {
x,
y,
fun: upper_bound,
lower_bound,
upper_bound,
n_cuts,
nit,
success: (upper_bound - lower_bound).abs()
< self.options.tol * (1.0 + upper_bound.abs()),
message: "Benders decomposition completed".to_string(),
})
}
}
#[derive(Debug, Clone)]
pub struct DantzigWolfeOptions {
pub max_iter: usize,
pub opt_tol: f64,
pub max_sub_iter: usize,
pub sub_tol: f64,
pub master_step: f64,
}
impl Default for DantzigWolfeOptions {
fn default() -> Self {
DantzigWolfeOptions {
max_iter: 200,
opt_tol: 1e-7,
max_sub_iter: 200,
sub_tol: 1e-9,
master_step: 0.1,
}
}
}
#[derive(Debug, Clone)]
pub struct DantzigWolfeResult {
pub x: Vec<f64>,
pub fun: f64,
pub n_columns: usize,
pub nit: usize,
pub duals: Vec<f64>,
pub success: bool,
pub message: String,
}
pub struct DantzigWolfe {
pub options: DantzigWolfeOptions,
}
impl Default for DantzigWolfe {
fn default() -> Self {
DantzigWolfe {
options: DantzigWolfeOptions::default(),
}
}
}
impl DantzigWolfe {
pub fn new(options: DantzigWolfeOptions) -> Self {
DantzigWolfe { options }
}
pub fn solve<FM, FP>(
&self,
master_obj: FM,
pricing_oracle: FP,
x0: &[f64],
_n_blocks: usize,
) -> OptimizeResult<DantzigWolfeResult>
where
FM: Fn(&[f64]) -> f64,
FP: Fn(&[f64]) -> (Vec<f64>, f64), {
let n = x0.len();
let h = 1e-7f64;
if n == 0 {
return Err(OptimizeError::InvalidInput(
"Initial point must be non-empty".to_string(),
));
}
let mut x = x0.to_vec();
let mut columns: Vec<Vec<f64>> = vec![x.clone()]; let mut weights: Vec<f64> = vec![1.0]; let mut duals = vec![0.0f64; n];
let mut n_iter = 0usize;
let mut n_columns = 1usize;
for outer in 0..self.options.max_iter {
n_iter = outer + 1;
let restricted_obj = |w: &[f64]| -> f64 {
let n_cols = columns.len();
let mut xc = vec![0.0f64; n];
for (j, col) in columns.iter().enumerate() {
let wj = if j < w.len() { w[j] } else { 0.0 };
for i in 0..n {
xc[i] += wj * col[i];
}
}
master_obj(&xc)
};
let nc = columns.len();
let mut f_w = restricted_obj(&weights[..nc.min(weights.len())]);
for _inner in 0..200 {
let mut grad_w = vec![0.0f64; nc];
let w_cur = &weights[..nc.min(weights.len())];
let w_padded: Vec<f64> = {
let mut wp = w_cur.to_vec();
while wp.len() < nc {
wp.push(0.0);
}
wp
};
for j in 0..nc {
let mut wf = w_padded.clone();
let delta = h;
wf[j] += delta;
let sum: f64 = wf.iter().sum();
let wf_norm: Vec<f64> = wf.iter().map(|wj| wj / sum).collect();
grad_w[j] = (restricted_obj(&wf_norm) - f_w) / delta;
}
let gnorm: f64 = grad_w.iter().map(|g| g * g).sum::<f64>().sqrt();
if gnorm < self.options.sub_tol {
break;
}
let step = self.options.master_step / (1.0 + gnorm);
let w_cur_len = w_padded.len();
let mut w_new: Vec<f64> = (0..w_cur_len)
.map(|j| (w_padded[j] - step * grad_w[j]).max(0.0))
.collect();
let sum: f64 = w_new.iter().sum();
if sum > 1e-14 {
for wj in w_new.iter_mut() {
*wj /= sum;
}
}
let f_new = restricted_obj(&w_new);
if f_new < f_w {
weights = w_new;
f_w = f_new;
} else {
break;
}
}
let nc = columns.len();
x = vec![0.0f64; n];
for (j, col) in columns.iter().enumerate() {
let wj = if j < weights.len() { weights[j] } else { 0.0 };
for i in 0..n {
x[i] += wj * col[i];
}
}
let f0 = master_obj(&x);
for i in 0..n {
let mut xf = x.clone();
xf[i] += h;
duals[i] = (master_obj(&xf) - f0) / h;
}
let (proposal, reduced_cost) = pricing_oracle(&duals);
if reduced_cost >= -self.options.opt_tol {
break;
}
columns.push(proposal);
weights.push(0.01); let sum: f64 = weights.iter().sum();
for wj in weights.iter_mut() {
*wj /= sum;
}
n_columns += 1;
}
let fun = master_obj(&x);
Ok(DantzigWolfeResult {
x,
fun,
n_columns,
nit: n_iter,
duals,
success: n_columns < self.options.max_iter,
message: "Dantzig-Wolfe decomposition completed".to_string(),
})
}
}
#[derive(Debug, Clone)]
pub struct AdmmOptions {
pub rho: f64,
pub max_iter: usize,
pub eps_primal: f64,
pub eps_dual: f64,
pub rho_adapt: bool,
pub rho_factor: f64,
pub rho_threshold: f64,
pub inner_iter: usize,
pub inner_tol: f64,
}
impl Default for AdmmOptions {
fn default() -> Self {
AdmmOptions {
rho: 1.0,
max_iter: 1000,
eps_primal: 1e-6,
eps_dual: 1e-6,
rho_adapt: true,
rho_factor: 2.0,
rho_threshold: 10.0,
inner_iter: 50,
inner_tol: 1e-8,
}
}
}
#[derive(Debug, Clone)]
pub struct AdmmResult {
pub x: Vec<f64>,
pub z: Vec<f64>,
pub u: Vec<f64>,
pub fun: f64,
pub primal_residual: f64,
pub dual_residual: f64,
pub nit: usize,
pub success: bool,
pub message: String,
}
pub struct Admm {
pub options: AdmmOptions,
}
impl Default for Admm {
fn default() -> Self {
Admm {
options: AdmmOptions::default(),
}
}
}
impl Admm {
pub fn new(options: AdmmOptions) -> Self {
Admm { options }
}
pub fn solve<F, PG>(
&self,
f_obj: F,
prox_g: PG,
x0: &[f64],
) -> OptimizeResult<AdmmResult>
where
F: Fn(&[f64]) -> f64,
PG: Fn(&[f64], f64) -> Vec<f64>, {
let n = x0.len();
if n == 0 {
return Err(OptimizeError::InvalidInput(
"Initial point must be non-empty".to_string(),
));
}
let h = 1e-7f64;
let mut x = x0.to_vec();
let mut z = x.clone();
let mut u = vec![0.0f64; n];
let mut rho = self.options.rho;
let mut nit = 0usize;
for iter in 0..self.options.max_iter {
nit = iter + 1;
let target_x: Vec<f64> = z.iter().zip(u.iter()).map(|(zi, ui)| zi - ui).collect();
let x_aug = |x: &[f64]| -> f64 {
let f = f_obj(x);
let pen: f64 = x
.iter()
.zip(target_x.iter())
.map(|(xi, ti)| (xi - ti).powi(2))
.sum();
f + 0.5 * rho * pen
};
let mut f_x = x_aug(&x);
for _xi in 0..self.options.inner_iter {
let mut grad = vec![0.0f64; n];
for i in 0..n {
let mut xf = x.clone();
xf[i] += h;
grad[i] = (x_aug(&xf) - f_x) / h;
}
let gnorm: f64 = grad.iter().map(|g| g * g).sum::<f64>().sqrt();
if gnorm < self.options.inner_tol {
break;
}
let step = 1.0 / (rho + gnorm);
let mut x_new: Vec<f64> = x.iter().zip(grad.iter()).map(|(xi, gi)| xi - step * gi).collect();
let f_new = x_aug(&x_new);
if f_new < f_x {
x = x_new;
f_x = f_new;
} else {
let mut s = step * 0.5;
for _ in 0..20 {
x_new = x.iter().zip(grad.iter()).map(|(xi, gi)| xi - s * gi).collect();
let fn2 = x_aug(&x_new);
if fn2 < f_x {
x = x_new;
f_x = fn2;
break;
}
s *= 0.5;
}
break;
}
}
let z_prev = z.clone();
let v: Vec<f64> = x.iter().zip(u.iter()).map(|(xi, ui)| xi + ui).collect();
z = prox_g(&v, rho);
for i in 0..n {
u[i] += x[i] - z[i];
}
let primal_res: f64 = x.iter().zip(z.iter()).map(|(xi, zi)| (xi - zi).powi(2)).sum::<f64>().sqrt();
let dual_res: f64 = z.iter().zip(z_prev.iter()).map(|(zi, zpi)| (rho * (zi - zpi)).powi(2)).sum::<f64>().sqrt();
if self.options.rho_adapt {
if primal_res > self.options.rho_threshold * dual_res {
rho *= self.options.rho_factor;
for ui in u.iter_mut() {
*ui /= self.options.rho_factor;
}
} else if dual_res > self.options.rho_threshold * primal_res {
rho /= self.options.rho_factor;
for ui in u.iter_mut() {
*ui *= self.options.rho_factor;
}
}
}
if primal_res < self.options.eps_primal && dual_res < self.options.eps_dual {
let f_val = f_obj(&x);
return Ok(AdmmResult {
x,
z,
u,
fun: f_val,
primal_residual: primal_res,
dual_residual: dual_res,
nit,
success: true,
message: "ADMM converged".to_string(),
});
}
}
let primal_res: f64 = x.iter().zip(z.iter()).map(|(xi, zi)| (xi - zi).powi(2)).sum::<f64>().sqrt();
let dual_res = 0.0f64; let f_val = f_obj(&x);
Ok(AdmmResult {
x,
z,
u,
fun: f_val,
primal_residual: primal_res,
dual_residual: dual_res,
nit,
success: primal_res < self.options.eps_primal * 100.0,
message: "ADMM: maximum iterations reached".to_string(),
})
}
}
#[derive(Debug, Clone)]
pub struct ProximalBundleOptions {
pub mu: f64,
pub max_iter: usize,
pub m_l: f64,
pub max_bundle_size: usize,
pub tol: f64,
pub min_mu: f64,
pub max_mu: f64,
}
impl Default for ProximalBundleOptions {
fn default() -> Self {
ProximalBundleOptions {
mu: 1.0,
max_iter: 300,
m_l: 0.1,
max_bundle_size: 50,
tol: 1e-6,
min_mu: 1e-8,
max_mu: 1e8,
}
}
}
#[derive(Debug, Clone)]
struct BundleElement {
g: Vec<f64>,
f: f64,
x: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct ProximalBundleResult {
pub x: Vec<f64>,
pub fun: f64,
pub subgrad_norm: f64,
pub n_serious_steps: usize,
pub n_null_steps: usize,
pub nit: usize,
pub success: bool,
pub message: String,
}
pub struct ProximalBundle {
pub options: ProximalBundleOptions,
}
impl Default for ProximalBundle {
fn default() -> Self {
ProximalBundle {
options: ProximalBundleOptions::default(),
}
}
}
impl ProximalBundle {
pub fn new(options: ProximalBundleOptions) -> Self {
ProximalBundle { options }
}
pub fn minimize<FS>(
&self,
func: FS,
x0: &[f64],
) -> OptimizeResult<ProximalBundleResult>
where
FS: Fn(&[f64]) -> (f64, Vec<f64>), {
let n = x0.len();
if n == 0 {
return Err(OptimizeError::InvalidInput(
"Initial point must be non-empty".to_string(),
));
}
let mut y = x0.to_vec(); let (mut f_y, mut g_y) = func(&y);
let mut mu = self.options.mu;
let mut bundle: Vec<BundleElement> = vec![BundleElement {
g: g_y.clone(),
f: f_y,
x: y.clone(),
}];
let mut n_serious = 0usize;
let mut n_null = 0usize;
let mut nit = 0usize;
for iter in 0..self.options.max_iter {
nit = iter + 1;
let gnorm: f64 = g_y.iter().map(|g| g * g).sum::<f64>().sqrt();
if gnorm < self.options.tol {
return Ok(ProximalBundleResult {
x: y,
fun: f_y,
subgrad_norm: gnorm,
n_serious_steps: n_serious,
n_null_steps: n_null,
nit,
success: true,
message: "Proximal bundle converged (subgradient norm)".to_string(),
});
}
let nb = bundle.len();
let mut lambda = vec![1.0f64 / nb as f64; nb];
let dual_obj = |lam: &[f64]| -> f64 {
let mut agg_g = vec![0.0f64; n];
let mut sum_fg = 0.0f64;
for (i, be) in bundle.iter().enumerate() {
let li = if i < lam.len() { lam[i] } else { 0.0 };
for j in 0..n {
agg_g[j] += li * be.g[j];
}
sum_fg += li * (be.f - be.g.iter().zip(be.x.iter()).map(|(gj, xj)| gj * xj).sum::<f64>());
}
let agg_norm_sq: f64 = agg_g.iter().map(|g| g * g).sum();
-(sum_fg - agg_g.iter().zip(y.iter()).map(|(gj, yj)| gj * yj).sum::<f64>()
- 0.5 / mu * agg_norm_sq)
};
let h = 1e-7f64;
let mut f_lam = dual_obj(&lambda);
for _di in 0..200 {
let mut grad_lam = vec![0.0f64; nb];
for i in 0..nb {
let mut lf = lambda.clone();
lf[i] += h;
let sum: f64 = lf.iter().sum();
let lf: Vec<f64> = lf.iter().map(|li| (li / sum).max(0.0)).collect();
grad_lam[i] = (dual_obj(&lf) - f_lam) / h;
}
let gnorm_lam: f64 = grad_lam.iter().map(|g| g * g).sum::<f64>().sqrt();
if gnorm_lam < 1e-10 {
break;
}
let step_lam = 0.1 / (1.0 + gnorm_lam);
let mut l_new: Vec<f64> = lambda
.iter()
.zip(grad_lam.iter())
.map(|(li, gi)| (li - step_lam * gi).max(0.0))
.collect();
let sum: f64 = l_new.iter().sum();
if sum > 1e-14 {
for li in l_new.iter_mut() {
*li /= sum;
}
}
let f_new = dual_obj(&l_new);
if f_new < f_lam {
lambda = l_new;
f_lam = f_new;
} else {
break;
}
}
let mut agg_g = vec![0.0f64; n];
for (i, be) in bundle.iter().enumerate() {
let li = if i < lambda.len() { lambda[i] } else { 0.0 };
for j in 0..n {
agg_g[j] += li * be.g[j];
}
}
let x_trial: Vec<f64> = y.iter().zip(agg_g.iter()).map(|(yj, gj)| yj - gj / mu).collect();
let (f_trial, g_trial) = func(&x_trial);
let f_model: f64 = bundle
.iter()
.map(|be| {
let gx: f64 = be
.g
.iter()
.zip(x_trial.iter())
.zip(be.x.iter())
.map(|((gi, xi), xi_k)| gi * (xi - xi_k))
.sum();
be.f + gx
})
.fold(f64::NEG_INFINITY, f64::max);
let descent = f_y - f_model;
let is_serious = f_trial <= f_y - self.options.m_l * descent.max(0.0);
if is_serious {
y = x_trial.clone();
f_y = f_trial;
g_y = g_trial.clone();
n_serious += 1;
} else {
n_null += 1;
}
bundle.push(BundleElement {
g: g_trial,
f: f_trial,
x: x_trial,
});
if bundle.len() > self.options.max_bundle_size {
bundle.remove(0);
}
if is_serious {
mu = (mu * 0.5).max(self.options.min_mu);
} else if n_null > 3 * (n_serious + 1) {
mu = (mu * 2.0).min(self.options.max_mu);
}
}
let gnorm_final: f64 = g_y.iter().map(|g| g * g).sum::<f64>().sqrt();
Ok(ProximalBundleResult {
x: y,
fun: f_y,
subgrad_norm: gnorm_final,
n_serious_steps: n_serious,
n_null_steps: n_null,
nit,
success: gnorm_final < self.options.tol * 100.0,
message: "Proximal bundle: maximum iterations reached".to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_benders_separable() {
let result = BendersDecomposition::default()
.solve(
|x: &[f64]| x[0].powi(2),
|x: &[f64], y: &[f64]| (x[0] + y[0]).powi(2),
&[1.0],
&[0.0],
)
.expect("unexpected None or Err");
assert!(result.upper_bound < 2.0, "Upper bound {} should be < 2.0", result.upper_bound);
}
#[test]
fn test_dantzig_wolfe_basic() {
let result = DantzigWolfe::default()
.solve(
|x: &[f64]| (x[0] - 1.0).powi(2),
|pi: &[f64]| {
let x_star = (1.0 - pi[0] * 0.5).clamp(0.0, 2.0);
let rc = pi[0] * x_star - pi[0]; (vec![x_star], rc)
},
&[0.5],
1,
)
.expect("unexpected None or Err");
assert!(result.fun < 0.5, "Expected fun < 0.5, got {}", result.fun);
}
#[test]
fn test_admm_consensus() {
let result = Admm::default()
.solve(
|x: &[f64]| x[0].powi(2),
|v: &[f64], rho: f64| {
vec![(1.0 + rho * 0.5 * v[0]) / (1.0 + rho * 0.5)]
},
&[1.0],
)
.expect("unexpected None or Err");
assert!((result.x[0] - 0.5).abs() < 0.1 || result.fun < 1.0,
"fun = {}, x = {:?}", result.fun, result.x);
}
#[test]
fn test_admm_lasso_like() {
let soft_thresh = |v: &[f64], rho: f64| -> Vec<f64> {
let thresh = 1.0 / rho;
v.iter().map(|vi| vi.signum() * (vi.abs() - thresh).max(0.0)).collect()
};
let result = Admm::new(AdmmOptions {
rho: 1.0,
max_iter: 500,
eps_primal: 1e-5,
eps_dual: 1e-5,
..Default::default()
})
.solve(
|x: &[f64]| (x[0] - 3.0).powi(2),
soft_thresh,
&[0.0],
)
.expect("unexpected None or Err");
assert!(result.fun < 5.0, "fun = {}", result.fun);
}
#[test]
fn test_proximal_bundle_smooth() {
let result = ProximalBundle::default()
.minimize(
|x: &[f64]| (x[0].powi(2), vec![2.0 * x[0]]),
&[2.0],
)
.expect("unexpected None or Err");
assert!(result.fun < 0.5, "Expected fun < 0.5, got {}", result.fun);
}
#[test]
fn test_proximal_bundle_max_function() {
let result = ProximalBundle::new(ProximalBundleOptions {
tol: 1e-5,
max_iter: 200,
..Default::default()
})
.minimize(
|x: &[f64]| {
let v = x[0].abs();
let g = if x[0] > 0.0 { 1.0 } else if x[0] < 0.0 { -1.0 } else { 0.0 };
(v, vec![g])
},
&[2.0],
)
.expect("unexpected None or Err");
assert!(result.fun < 0.5, "Expected |x| < 0.5 at optimum, got {}", result.fun);
}
}