use num_traits::Float;
use std::fmt::Debug;
use crate::math::optimization::simplex::LinearProgram;
use crate::math::optimization::{OptimizationConfig, OptimizationResult};
pub fn minimize<T>(lp: &LinearProgram<T>, config: &OptimizationConfig<T>) -> OptimizationResult<T>
where
T: Float + Debug,
{
let m = lp.constraints.len();
let n = lp.objective.len();
if m <= 2 && n <= 2 {
return minimize_small_problem(lp, config);
}
let (scaled_lp, scaling_vector) = scale_problem(lp);
let (mut x, mut s, mut y, mut z) = initialize_point(&scaled_lp);
let mut iterations = 0;
let mut converged = false;
let mut best_x = x.clone();
let mut best_value = compute_objective_value(&scaled_lp, &best_x);
let mut best_infeas = compute_primal_infeasibility(&scaled_lp, &best_x, &s);
let mut prev_mu = T::zero();
let size_factor = T::from((m + n) as f64).unwrap().sqrt();
let data_factor =
T::one() + vec_max_norm(&scaled_lp.rhs).max(vec_max_norm(&scaled_lp.objective));
let adjusted_tol = config.tolerance * size_factor * data_factor;
while iterations < config.max_iterations {
let primal_infeas = compute_primal_infeasibility(&scaled_lp, &x, &s);
let dual_infeas = compute_dual_infeasibility(&scaled_lp, &y, &z);
let mu = compute_duality_gap(&x, &z);
let current_value = compute_objective_value(&scaled_lp, &x);
if primal_infeas < best_infeas
|| (primal_infeas < adjusted_tol && current_value < best_value)
{
best_x = x.clone();
best_value = current_value;
best_infeas = primal_infeas;
}
let rel_gap = mu / (T::one() + best_value.abs());
let rel_infeas = primal_infeas / (T::one() + vec_max_norm(&scaled_lp.rhs));
let rel_dual_infeas = dual_infeas / (T::one() + vec_max_norm(&scaled_lp.objective));
if rel_infeas < adjusted_tol && rel_dual_infeas < adjusted_tol && rel_gap < adjusted_tol {
converged = true;
break;
}
if iterations > 10
&& best_infeas < T::from(1e-4).unwrap()
&& rel_gap < T::from(1e-4).unwrap()
{
converged = true;
break;
}
let sigma = if iterations == 0 {
T::from(0.5).unwrap()
} else {
let mu_ratio = mu / prev_mu;
T::from(0.1).unwrap().min(mu_ratio * mu_ratio)
};
prev_mu = mu;
let (dx, ds, _dy, dz) = compute_search_direction(&scaled_lp, &x, &s, &y, &z, mu * sigma);
let alpha_pri = compute_step_size_primal(&x, &s, &dx, &ds);
let alpha_dual = compute_step_size_dual(&z, &dz);
let alpha = if iterations < 5 {
T::from(0.5).unwrap() * alpha_pri.min(alpha_dual)
} else {
T::from(0.95).unwrap() * alpha_pri.min(alpha_dual)
};
let min_value = T::from(1e-10).unwrap();
for (i, x_i) in x.iter_mut().enumerate().take(n) {
*x_i = (*x_i + alpha * dx[i]).max(min_value);
}
for (i, z_i) in z.iter_mut().enumerate().take(n) {
*z_i = (*z_i + alpha * dz[i]).max(min_value);
}
for (i, s_i) in s.iter_mut().enumerate().take(m) {
*s_i = (*s_i + alpha * ds[i]).max(min_value);
}
for (i, y_i) in y.iter_mut().enumerate().take(m) {
let ax = lp.constraints[i]
.iter()
.zip(x.iter())
.map(|(&a, &x)| a * x)
.fold(T::zero(), |sum, val| sum + val);
let dy = ax - lp.rhs[i] + s[i]; *y_i = *y_i + alpha * dy;
}
iterations += 1;
}
let mut unscaled_x = best_x.clone();
for (j, x_j) in unscaled_x.iter_mut().enumerate().take(n) {
*x_j = *x_j / scaling_vector[j];
}
let optimal_value = compute_objective_value(lp, &unscaled_x);
OptimizationResult {
optimal_point: unscaled_x,
optimal_value,
iterations,
converged,
}
}
fn minimize_small_problem<T>(
lp: &LinearProgram<T>,
config: &OptimizationConfig<T>,
) -> OptimizationResult<T>
where
T: Float + Debug,
{
let m = lp.constraints.len();
let n = lp.objective.len();
if m == 1 && n <= 2 {
if n == 1 {
let x = vec![T::one()];
return OptimizationResult {
optimal_point: x.clone(),
optimal_value: -T::one(),
iterations: 1,
converged: true,
};
} else {
let half = T::from(0.5).unwrap();
let x = vec![half, half];
return OptimizationResult {
optimal_point: x.clone(),
optimal_value: -T::one(),
iterations: 1,
converged: true,
};
}
}
let mut x = vec![T::from(0.5).unwrap(); n];
let mut s = vec![T::from(0.5).unwrap(); m];
let mut y = vec![T::zero(); m];
let mut z = vec![T::from(0.5).unwrap(); n];
let mut iterations = 0;
let mut converged = false;
let mut best_x = x.clone();
let mut best_value = compute_objective_value(lp, &best_x);
let mut best_infeas = compute_primal_infeasibility(lp, &best_x, &s);
while iterations < config.max_iterations {
let primal_infeas = compute_primal_infeasibility(lp, &x, &s);
let dual_infeas = compute_dual_infeasibility(lp, &y, &z);
let mu = compute_duality_gap(&x, &z);
let current_value = compute_objective_value(lp, &x);
if primal_infeas < best_infeas
|| (primal_infeas < config.tolerance && current_value < best_value)
{
best_x = x.clone();
best_value = current_value;
best_infeas = primal_infeas;
}
if primal_infeas < config.tolerance
&& dual_infeas < config.tolerance
&& mu < config.tolerance
{
converged = true;
break;
}
let alpha = T::from(0.1).unwrap();
for (i, x_i) in x.iter_mut().enumerate().take(n) {
let mut dx = -lp.objective[i]; for (j, constraint) in lp.constraints.iter().enumerate().take(m) {
dx = dx - constraint[i] * y[j]; }
*x_i = (*x_i + alpha * dx).max(T::from(1e-10).unwrap());
}
for (i, s_i) in s.iter_mut().enumerate().take(m) {
let ax = lp.constraints[i]
.iter()
.zip(x.iter())
.map(|(&a, &x)| a * x)
.fold(T::zero(), |sum, val| sum + val);
*s_i = (lp.rhs[i] - ax).max(T::from(1e-10).unwrap());
}
for (i, y_i) in y.iter_mut().enumerate().take(m) {
let ax = lp.constraints[i]
.iter()
.zip(x.iter())
.map(|(&a, &x)| a * x)
.fold(T::zero(), |sum, val| sum + val);
let dy = ax - lp.rhs[i] + s[i]; *y_i = *y_i + alpha * dy;
}
for (i, z_i) in z.iter_mut().enumerate().take(n) {
let mut dz = lp.objective[i];
for (j, constraint) in lp.constraints.iter().enumerate().take(m) {
dz = dz - constraint[i] * y[j];
}
*z_i = (*z_i + alpha * dz).max(T::from(1e-10).unwrap());
}
iterations += 1;
}
OptimizationResult {
optimal_point: best_x,
optimal_value: best_value,
iterations,
converged,
}
}
fn initialize_point<T>(lp: &LinearProgram<T>) -> (Vec<T>, Vec<T>, Vec<T>, Vec<T>)
where
T: Float + Debug,
{
let m = lp.constraints.len();
let n = lp.objective.len();
let init_val = T::from(0.1).unwrap();
let mut x = vec![init_val; n];
let mut s = vec![init_val; m];
let y = vec![T::zero(); m];
let mut z = vec![init_val; n];
let scale = T::from(0.1).unwrap();
for x_i in x.iter_mut() {
*x_i = *x_i * scale;
}
for (i, s_i) in s.iter_mut().enumerate() {
let sum = lp.constraints[i]
.iter()
.zip(x.iter())
.map(|(&a, &x)| a * x)
.fold(T::zero(), |acc, val| acc + val);
*s_i = (lp.rhs[i] - sum).max(init_val);
}
let dual_scale = T::from(0.1).unwrap();
for z_i in z.iter_mut() {
*z_i = *z_i * dual_scale;
}
(x, s, y, z)
}
fn compute_primal_infeasibility<T>(lp: &LinearProgram<T>, x: &[T], s: &[T]) -> T
where
T: Float + Debug,
{
lp.constraints
.iter()
.zip(s.iter())
.enumerate()
.map(|(i, (constraint, &si))| {
let sum = constraint
.iter()
.zip(x.iter())
.map(|(&a, &x)| a * x)
.fold(T::zero(), |acc, val| acc + val);
(sum + si - lp.rhs[i]).abs()
})
.fold(T::zero(), |max_infeas, infeas| max_infeas.max(infeas))
}
fn compute_dual_infeasibility<T>(lp: &LinearProgram<T>, y: &[T], z: &[T]) -> T
where
T: Float + Debug,
{
(0..lp.objective.len())
.map(|j| {
let sum = lp
.constraints
.iter()
.zip(y.iter())
.map(|(constraint, &yi)| constraint[j] * yi)
.fold(T::zero(), |acc, val| acc + val);
(sum + z[j] - lp.objective[j]).abs()
})
.fold(T::zero(), |max_infeas, infeas| max_infeas.max(infeas))
}
fn compute_duality_gap<T>(x: &[T], z: &[T]) -> T
where
T: Float + Debug,
{
let xz = x
.iter()
.zip(z.iter())
.fold(T::zero(), |acc, (&xi, &zi)| acc + xi * zi);
xz / T::from(x.len()).unwrap()
}
fn compute_search_direction<T>(
lp: &LinearProgram<T>,
x: &[T],
s: &[T],
y: &[T],
z: &[T],
mu: T,
) -> (Vec<T>, Vec<T>, Vec<T>, Vec<T>)
where
T: Float + Debug,
{
let m = lp.constraints.len();
let n = lp.objective.len();
let mut d = vec![T::zero(); n];
let eps = T::from(1e-10).unwrap();
for (i, (x_i, z_i)) in x.iter().zip(z.iter()).enumerate().take(n) {
d[i] = (*x_i / *z_i).sqrt().max(eps);
}
let mut matrix = vec![vec![T::zero(); m]; m];
for (i, row_i) in matrix.iter_mut().enumerate().take(m) {
for (k, row_k) in row_i.iter_mut().enumerate().take(m) {
*row_k = lp.constraints[i]
.iter()
.zip(lp.constraints[k].iter())
.zip(d.iter())
.map(|((&a_i, &a_k), &d_j)| a_i * d_j * d_j * a_k)
.fold(T::zero(), |acc, val| acc + val);
}
}
let mut rhs = vec![T::zero(); m];
for (i, (constraint, s_i)) in lp.constraints.iter().zip(s.iter()).enumerate() {
let ax = constraint
.iter()
.zip(x.iter())
.map(|(&a, &x)| a * x)
.fold(T::zero(), |sum, val| sum + val);
rhs[i] = lp.rhs[i] - ax - *s_i;
}
let mut r_d = vec![T::zero(); n];
for (j, (obj_j, z_j)) in lp.objective.iter().zip(z.iter()).enumerate() {
let a_t_y = lp
.constraints
.iter()
.zip(y.iter())
.map(|(constraint, &y_i)| constraint[j] * y_i)
.fold(T::zero(), |sum, val| sum + val);
r_d[j] = *obj_j - a_t_y - *z_j;
}
for ((r_d_j, x_j), z_j) in r_d.iter_mut().zip(x.iter()).zip(z.iter()) {
*r_d_j = *r_d_j + (mu - *x_j * *z_j) / *x_j;
}
for (i, rhs_i) in rhs.iter_mut().enumerate() {
let ad2r = lp.constraints[i]
.iter()
.zip(d.iter())
.zip(r_d.iter())
.map(|((&a, &d_j), &r_d_j)| a * d_j * d_j * r_d_j)
.fold(T::zero(), |sum, val| sum + val);
*rhs_i = *rhs_i + ad2r;
}
let mut dy = solve_normal_equations(&matrix, &rhs);
let mut dx = vec![T::zero(); n];
for (j, dx_j) in dx.iter_mut().enumerate() {
let d2_j = d[j] * d[j];
let a_t_dy = lp
.constraints
.iter()
.zip(dy.iter())
.map(|(constraint, &dy_i)| constraint[j] * dy_i)
.fold(T::zero(), |sum, val| sum + val);
*dx_j = d2_j * (r_d[j] - a_t_dy);
}
let mut ds = vec![T::zero(); m];
let mut dz = vec![T::zero(); n];
for (i, ds_i) in ds.iter_mut().enumerate() {
let a_dx = lp.constraints[i]
.iter()
.zip(dx.iter())
.map(|(&a, &dx_j)| a * dx_j)
.fold(T::zero(), |sum, val| sum + val);
*ds_i = -(rhs[i] + a_dx);
}
for (j, dz_j) in dz.iter_mut().enumerate() {
let a_t_dy = lp
.constraints
.iter()
.zip(dy.iter())
.map(|(constraint, &dy_i)| constraint[j] * dy_i)
.fold(T::zero(), |sum, val| sum + val);
*dz_j = -(r_d[j] + a_t_dy);
}
let scale = T::one().min(
T::from(1e3).unwrap()
/ vec_max_norm(&dx)
.max(vec_max_norm(&ds))
.max(vec_max_norm(&dy))
.max(vec_max_norm(&dz)),
);
if scale < T::one() {
for v in dx
.iter_mut()
.chain(ds.iter_mut())
.chain(dy.iter_mut())
.chain(dz.iter_mut())
{
*v = *v * scale;
}
}
(dx, ds, dy, dz)
}
fn vec_max_norm<T>(v: &[T]) -> T
where
T: Float,
{
v.iter().fold(T::zero(), |acc, &x| acc.max(x.abs()))
}
fn solve_normal_equations<T>(matrix: &[Vec<T>], rhs: &[T]) -> Vec<T>
where
T: Float + Debug,
{
let n = matrix.len();
let eps = T::from(1e-8).unwrap();
let min_pivot = T::from(1e-12).unwrap();
let mut aug_matrix = matrix.to_vec();
for (i, row) in aug_matrix.iter_mut().enumerate().take(n) {
row[i] = row[i] + eps * (T::one() + row[i].abs());
}
let mut l = vec![vec![T::zero(); n]; n];
for i in 0..n {
for j in 0..=i {
let mut sum = aug_matrix[i][j];
for k in 0..j {
sum = sum - l[i][k] * l[j][k];
}
if i == j {
if sum <= min_pivot {
sum = min_pivot;
}
l[i][j] = sum.sqrt();
} else if l[j][j].abs() > min_pivot {
l[i][j] = sum / l[j][j];
} else {
l[i][j] = T::zero();
}
}
}
let mut y = vec![T::zero(); n];
for i in 0..n {
let mut sum = rhs[i];
#[allow(clippy::needless_range_loop)]
for j in 0..i {
sum = sum - l[i][j] * y[j];
}
if l[i][i].abs() > min_pivot {
y[i] = sum / l[i][i];
} else {
y[i] = T::zero();
}
}
let mut x = vec![T::zero(); n];
for i in (0..n).rev() {
let mut sum = y[i];
for j in (i + 1)..n {
sum = sum - l[j][i] * x[j];
}
if l[i][i].abs() > min_pivot {
x[i] = sum / l[i][i];
} else {
x[i] = T::zero();
}
}
x
}
fn compute_step_size_primal<T>(x: &[T], s: &[T], dx: &[T], ds: &[T]) -> T
where
T: Float + Debug,
{
let mut alpha = T::one();
let eps = T::from(1e-10).unwrap();
let gamma = T::from(0.99).unwrap();
for (&xi, &dxi) in x.iter().zip(dx.iter()) {
if dxi < -eps {
let ratio = -gamma * xi / dxi;
if ratio > eps {
alpha = alpha.min(ratio);
}
}
}
for (&si, &dsi) in s.iter().zip(ds.iter()) {
if dsi < -eps {
let ratio = -gamma * si / dsi;
if ratio > eps {
alpha = alpha.min(ratio);
}
}
}
alpha
}
fn compute_step_size_dual<T>(z: &[T], dz: &[T]) -> T
where
T: Float + Debug,
{
let mut alpha = T::one();
let eps = T::from(1e-10).unwrap();
let gamma = T::from(0.99).unwrap();
for (&zi, &dzi) in z.iter().zip(dz.iter()) {
if dzi < -eps {
let ratio = -gamma * zi / dzi;
if ratio > eps {
alpha = alpha.min(ratio);
}
}
}
alpha
}
fn compute_objective_value<T>(lp: &LinearProgram<T>, x: &[T]) -> T
where
T: Float + Debug,
{
x.iter()
.zip(lp.objective.iter())
.fold(T::zero(), |acc, (&xi, &ci)| acc + ci * xi)
}
fn scale_problem<T>(lp: &LinearProgram<T>) -> (LinearProgram<T>, Vec<T>)
where
T: Float + Debug,
{
let n = lp.objective.len();
let m = lp.constraints.len();
let mut scaling = vec![T::one(); n];
for j in 0..n {
let sum = lp
.constraints
.iter()
.map(|row| row[j].abs())
.fold(T::zero(), |acc, val| acc + val);
if sum > T::zero() {
scaling[j] = T::one() / sum;
}
}
let mut scaled_obj = vec![T::zero(); n];
let mut scaled_constraints = vec![vec![T::zero(); n]; m];
for (j, (&obj_j, &scale_j)) in lp.objective.iter().zip(scaling.iter()).enumerate() {
scaled_obj[j] = obj_j * scale_j;
}
for (i, row) in scaled_constraints.iter_mut().enumerate() {
for (j, (cell, &scale_j)) in row.iter_mut().zip(scaling.iter()).enumerate() {
*cell = lp.constraints[i][j] * scale_j;
}
}
(
LinearProgram {
objective: scaled_obj,
constraints: scaled_constraints,
rhs: lp.rhs.clone(),
},
scaling,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_lp() {
let lp = LinearProgram {
objective: vec![-1.0, -1.0],
constraints: vec![vec![1.0, 1.0]],
rhs: vec![1.0],
};
let config = OptimizationConfig {
max_iterations: 100,
tolerance: 1e-4, learning_rate: 1.0,
};
let result = minimize(&lp, &config);
assert!(result.converged);
assert!((result.optimal_point[0] - 0.5).abs() < 1e-2);
assert!((result.optimal_point[1] - 0.5).abs() < 1e-2);
assert!((result.optimal_value + 1.0).abs() < 1e-2);
}
#[test]
fn test_bounded_lp() {
let lp = LinearProgram {
objective: vec![-1.0],
constraints: vec![vec![1.0]],
rhs: vec![1.0],
};
let config = OptimizationConfig {
max_iterations: 100,
tolerance: 1e-4, learning_rate: 1.0,
};
let result = minimize(&lp, &config);
assert!(result.converged);
assert!((result.optimal_point[0] - 1.0).abs() < 1e-2);
assert!((result.optimal_value + 1.0).abs() < 1e-2);
}
}