use clarabel::{algebra::*, solver::*};
use cova_algebra::tensors::{DMatrix, DVector};
use crate::{
SolverError, SolverResult,
traits::{OptimizationProblem, Solution},
};
#[derive(Debug, Clone)]
pub struct AdmmParams {
pub rho: f64,
pub primal_tolerance: f64,
pub dual_tolerance: f64,
pub max_iterations: usize,
pub alpha: f64,
}
impl Default for AdmmParams {
fn default() -> Self {
Self {
rho: 1.0,
primal_tolerance: 1e-6,
dual_tolerance: 1e-6,
max_iterations: 1000,
alpha: 1.0,
}
}
}
#[derive(Debug)]
pub struct AdmmSolver {
params: AdmmParams,
}
impl AdmmSolver {
pub fn new() -> Self { Self { params: AdmmParams::default() } }
pub fn with_params(params: AdmmParams) -> Self { Self { params } }
pub fn solve_qp_admm<F>(
&mut self,
p: &DMatrix<f64>,
q: &DVector<f64>,
a: &DMatrix<f64>,
b: &DMatrix<f64>,
c: &DVector<f64>,
z_update: F,
) -> SolverResult<Solution>
where
F: Fn(&DVector<f64>, &DVector<f64>, f64) -> DVector<f64>,
{
let n = q.len();
let m = c.len();
let z_dim = b.ncols();
let mut x = DVector::zeros(n);
let mut z = DVector::zeros(z_dim);
let mut u = DVector::zeros(m);
let ata = a.transpose() * a;
let p_aug = p + self.params.rho * &ata;
for iteration in 0..self.params.max_iterations {
let z_old = z.clone();
let rhs = b * &z - c + &u;
let q_aug = q + self.params.rho * (a.transpose() * &rhs);
x = self.solve_x_update(&p_aug, &q_aug)?;
let ax_plus_u = a * &x + &u;
let z_target = &ax_plus_u + c;
z = z_update(&z_target, &z_old, self.params.rho);
let residual = a * &x + b * &z - c;
u = &u + self.params.rho * &residual;
let primal_residual = residual.norm();
let dual_residual = self.params.rho * (a.transpose() * (&z - &z_old)).norm();
if primal_residual <= self.params.primal_tolerance
&& dual_residual <= self.params.dual_tolerance
{
let objective_value = 0.5f64.mul_add(x.dot(&(p * &x)), q.dot(&x));
return Ok(Solution {
x,
objective_value,
iterations: iteration as u64 + 1,
converged: true,
termination: "Converged".to_string(),
});
}
}
let objective_value = 0.5f64.mul_add(x.dot(&(p * &x)), q.dot(&x));
Ok(Solution {
x,
objective_value,
iterations: self.params.max_iterations as u64,
converged: false,
termination: "MaxIterations".to_string(),
})
}
fn solve_x_update(&self, p: &DMatrix<f64>, q: &DVector<f64>) -> SolverResult<DVector<f64>> {
let n = q.len();
let (col_offsets, row_indices, values) = dense_to_csc(p);
let p_csc = CscMatrix::new(n, n, col_offsets, row_indices, values);
let q_vec: Vec<f64> = q.iter().cloned().collect();
let a_csc = CscMatrix::new(0, n, vec![0; n + 1], vec![], vec![]);
let b_vec: Vec<f64> = Vec::new();
let cones: Vec<SupportedConeT<f64>> = Vec::new();
let settings = DefaultSettingsBuilder::default().max_iter(1000).verbose(false).build().unwrap();
let mut solver =
DefaultSolver::new(&p_csc, &q_vec, &a_csc, &b_vec, &cones, settings).map_err(|e| {
SolverError::NumericalError { message: format!("Failed to create Clarabel solver: {e:?}") }
})?;
solver.solve();
let result = solver.solution;
match result.status {
SolverStatus::Solved => Ok(DVector::from_vec(result.x)),
_ => Err(SolverError::NumericalError {
message: format!("Clarabel failed with status: {:?}", result.status),
}),
}
}
pub fn solve_lasso(
&mut self,
a: &DMatrix<f64>,
b: &DVector<f64>,
lambda: f64,
) -> SolverResult<Solution> {
let n = a.ncols();
let p = 2.0 * (a.transpose() * a);
let q = -2.0 * (a.transpose() * b);
let a_constraint = DMatrix::identity(n, n);
let b_constraint = -DMatrix::identity(n, n);
let c = DVector::zeros(n);
let z_update = move |target: &DVector<f64>, _z_old: &DVector<f64>, rho: f64| {
let threshold = lambda / rho;
target.map(|val| {
if val > threshold {
val - threshold
} else if val < -threshold {
val + threshold
} else {
0.0
}
})
};
self.solve_qp_admm(&p, &q, &a_constraint, &b_constraint, &c, z_update)
}
pub fn solve_basis_pursuit(
&mut self,
a: &DMatrix<f64>,
b: &DVector<f64>,
) -> SolverResult<Solution> {
self.solve_lasso(a, b, 1e-8)
}
}
fn dense_to_csc(matrix: &DMatrix<f64>) -> (Vec<usize>, Vec<usize>, Vec<f64>) {
let nrows = matrix.nrows();
let ncols = matrix.ncols();
let mut col_offsets = vec![0];
let mut row_indices = Vec::new();
let mut values = Vec::new();
for j in 0..ncols {
for i in 0..nrows {
let val = matrix[(i, j)];
if val.abs() > 1e-15 {
row_indices.push(i);
values.push(val);
}
}
col_offsets.push(row_indices.len());
}
(col_offsets, row_indices, values)
}
impl Default for AdmmSolver {
fn default() -> Self { Self::new() }
}
impl OptimizationProblem for AdmmSolver {
fn dimension(&self) -> usize {
0
}
fn solve(&self) -> SolverResult<Solution> {
Err(SolverError::InvalidProblem {
message: "ADMM solver requires specific problem setup via solve_lasso or solve_basis_pursuit"
.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lasso_simple() {
let a = DMatrix::from_row_slice(2, 2, &[1.0, 0.0, 0.0, 1.0]);
let b = DVector::from_vec(vec![1.0, 2.0]);
let lambda = 0.1;
let mut solver = AdmmSolver::new();
let result = solver.solve_lasso(&a, &b, lambda).unwrap();
assert!(result.converged);
assert!(result.x.len() == 2);
}
#[test]
fn test_basis_pursuit() {
let a = DMatrix::from_row_slice(1, 2, &[1.0, 1.0]);
let b = DVector::from_vec(vec![1.0]);
let mut solver = AdmmSolver::new();
let result = solver.solve_basis_pursuit(&a, &b).unwrap();
assert!(result.converged);
let residual = (&a * &result.x - &b).norm();
assert!(residual < 1e-3);
}
}