use crate::error::{OptimizeError, OptimizeResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
use scirs2_linalg::{cholesky, inv, solve, LinalgError};
impl From<LinalgError> for OptimizeError {
fn from(e: LinalgError) -> Self {
OptimizeError::ComputationError(format!("linalg: {}", e))
}
}
#[inline]
fn mat_inner(a: &Array2<f64>, b: &Array2<f64>) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn sym_product(a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
let n = a.nrows();
let mut out = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut v = 0.0_f64;
for k in 0..n {
v += a[[i, k]] * b[[k, j]] + b[[i, k]] * a[[k, j]];
}
out[[i, j]] = v * 0.5;
}
}
out
}
fn frobenius_norm(a: &Array2<f64>) -> f64 {
a.iter().map(|x| x * x).sum::<f64>().sqrt()
}
fn mat_inv(a: &Array2<f64>) -> OptimizeResult<Array2<f64>> {
inv(&a.view(), None).map_err(OptimizeError::from)
}
fn cholesky_lower(a: &Array2<f64>) -> OptimizeResult<Array2<f64>> {
let n = a.nrows();
let mut reg = a.clone();
let eps = 1e-14 * frobenius_norm(a).max(1.0);
for i in 0..n {
reg[[i, i]] += eps;
}
cholesky(®.view(), None).map_err(OptimizeError::from)
}
fn spd_inv(x: &Array2<f64>) -> OptimizeResult<Array2<f64>> {
mat_inv(x)
}
fn is_positive_definite(a: &Array2<f64>) -> bool {
cholesky_lower(a).is_ok()
}
fn regularise_pd(a: &mut Array2<f64>) {
let n = a.nrows();
let norm = frobenius_norm(a);
let delta = 1e-8 * norm.max(1.0);
for i in 0..n {
a[[i, i]] += delta;
}
}
#[derive(Debug, Clone)]
pub struct SDPProblem {
pub c: Array2<f64>,
pub a: Vec<Array2<f64>>,
pub b: Array1<f64>,
}
impl SDPProblem {
pub fn new(
c: Array2<f64>,
a: Vec<Array2<f64>>,
b: Array1<f64>,
) -> OptimizeResult<Self> {
let n = c.nrows();
if c.ncols() != n {
return Err(OptimizeError::ValueError(format!(
"C must be square, got {}×{}",
n,
c.ncols()
)));
}
let m = b.len();
if a.len() != m {
return Err(OptimizeError::ValueError(format!(
"Number of constraint matrices ({}) must equal len(b)={}",
a.len(),
m
)));
}
for (i, ai) in a.iter().enumerate() {
if ai.nrows() != n || ai.ncols() != n {
return Err(OptimizeError::ValueError(format!(
"Constraint matrix A[{}] is {}×{}, expected {}×{}",
i,
ai.nrows(),
ai.ncols(),
n,
n
)));
}
}
Ok(Self { c, a, b })
}
pub fn n(&self) -> usize {
self.c.nrows()
}
pub fn m(&self) -> usize {
self.b.len()
}
}
#[derive(Debug, Clone)]
pub struct SDPSolverConfig {
pub max_iter: usize,
pub tol: f64,
pub mu_init: f64,
pub step_factor: f64,
}
impl Default for SDPSolverConfig {
fn default() -> Self {
Self {
max_iter: 200,
tol: 1e-7,
mu_init: 1.0,
step_factor: 0.95,
}
}
}
#[derive(Debug, Clone)]
pub struct SDPResult {
pub x: Array2<f64>,
pub y: Array1<f64>,
pub s: Array2<f64>,
pub primal_obj: f64,
pub dual_obj: f64,
pub gap: f64,
pub n_iter: usize,
pub converged: bool,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct SDPSolver {
config: SDPSolverConfig,
}
impl SDPSolver {
pub fn new() -> Self {
Self {
config: SDPSolverConfig::default(),
}
}
pub fn with_config(config: SDPSolverConfig) -> Self {
Self { config }
}
pub fn solve(&self, problem: &SDPProblem) -> OptimizeResult<SDPResult> {
let n = problem.n();
let m = problem.m();
let mut x = Array2::<f64>::eye(n);
let mut s = Array2::<f64>::eye(n);
let mut y = Array1::<f64>::zeros(m);
let mut n_iter = 0usize;
let mut converged = false;
let mut message = String::from("maximum iterations reached");
for iter in 0..self.config.max_iter {
n_iter = iter + 1;
let rp = primal_residual(problem, &x);
let rd = dual_residual(problem, &y, &s);
let gap = sdp_duality_gap(&x, &s);
let rp_norm = rp.iter().map(|v| v * v).sum::<f64>().sqrt();
let rd_norm = frobenius_norm(&rd);
if gap.abs() < self.config.tol
&& rp_norm < self.config.tol
&& rd_norm < self.config.tol
{
converged = true;
message = format!(
"Converged in {} iterations (gap={:.2e}, rp={:.2e}, rd={:.2e})",
n_iter, gap, rp_norm, rd_norm
);
break;
}
let mu = mat_inner(&x, &s) / n as f64;
let x_inv = spd_inv(&x)?;
let schur = build_schur_complement(problem, &x, &s, &x_inv)?;
let (dx_aff, dy_aff, ds_aff) =
solve_newton_system(problem, &schur, &rp, &rd, &x, &s, &x_inv, 0.0, mu)?;
let alpha_aff_p = max_step_length_pd(&x, &dx_aff);
let alpha_aff_d = max_step_length_pd(&s, &ds_aff);
let alpha_aff = (alpha_aff_p.min(alpha_aff_d) * self.config.step_factor).min(1.0);
let mu_aff = mat_inner(
&(&x + &(&dx_aff * alpha_aff)),
&(&s + &(&ds_aff * alpha_aff)),
) / n as f64;
let sigma = (mu_aff / mu.max(1e-15)).powi(3).min(1.0);
let (dx, dy, ds) = solve_newton_system(
problem,
&schur,
&rp,
&rd,
&x,
&s,
&x_inv,
sigma * mu,
mu,
)?;
let alpha_p = (max_step_length_pd(&x, &dx) * self.config.step_factor).min(1.0);
let alpha_d = (max_step_length_pd(&s, &ds) * self.config.step_factor).min(1.0);
primal_sdp_step(&mut x, &dx, alpha_p);
dual_sdp_step(&mut y, &mut s, &dy, &ds, alpha_d);
if !is_positive_definite(&x) {
regularise_pd(&mut x);
}
if !is_positive_definite(&s) {
regularise_pd(&mut s);
}
}
let primal_obj = mat_inner(&problem.c, &x);
let dual_obj = problem.b.iter().zip(y.iter()).map(|(bi, yi)| bi * yi).sum();
let gap = sdp_duality_gap(&x, &s);
Ok(SDPResult {
x,
y,
s,
primal_obj,
dual_obj,
gap,
n_iter,
converged,
message,
})
}
}
impl Default for SDPSolver {
fn default() -> Self {
Self::new()
}
}
fn primal_residual(problem: &SDPProblem, x: &Array2<f64>) -> Array1<f64> {
let m = problem.m();
let mut rp = Array1::<f64>::zeros(m);
for i in 0..m {
rp[i] = problem.b[i] - mat_inner(&problem.a[i], x);
}
rp
}
fn dual_residual(problem: &SDPProblem, y: &Array1<f64>, s: &Array2<f64>) -> Array2<f64> {
let n = problem.n();
let m = problem.m();
let mut rd = problem.c.clone();
for i in 0..m {
rd = rd - &(&problem.a[i] * y[i]);
}
rd = rd - s;
rd
}
fn build_schur_complement(
problem: &SDPProblem,
_x: &Array2<f64>,
_s: &Array2<f64>,
x_inv: &Array2<f64>,
) -> OptimizeResult<Array2<f64>> {
let m = problem.m();
let mut m_mat = Array2::<f64>::zeros((m, m));
let n = problem.n();
let mut b_mats: Vec<Array2<f64>> = Vec::with_capacity(m);
for i in 0..m {
let mut bi = Array2::<f64>::zeros((n, n));
for r in 0..n {
for c in 0..n {
let mut v = 0.0_f64;
for k in 0..n {
v += problem.a[i][[r, k]] * x_inv[[k, c]];
}
bi[[r, c]] = v;
}
}
b_mats.push(bi);
}
for i in 0..m {
for j in i..m {
let mut v = 0.0_f64;
for r in 0..n {
for c in 0..n {
v += b_mats[i][[r, c]] * b_mats[j][[c, r]];
}
}
m_mat[[i, j]] = v;
m_mat[[j, i]] = v;
}
}
let eps = 1e-12 * frobenius_norm(&m_mat).max(1.0);
for i in 0..m {
m_mat[[i, i]] += eps;
}
Ok(m_mat)
}
fn solve_newton_system(
problem: &SDPProblem,
schur: &Array2<f64>,
rp: &Array1<f64>,
rd: &Array2<f64>,
x: &Array2<f64>,
s: &Array2<f64>,
x_inv: &Array2<f64>,
sigma_mu: f64,
_mu: f64,
) -> OptimizeResult<(Array2<f64>, Array1<f64>, Array2<f64>)> {
let n = problem.n();
let m = problem.m();
let mut t = rd.clone();
for i in 0..n {
for j in 0..n {
t[[i, j]] += sigma_mu * x_inv[[i, j]];
}
}
let mut x_inv_t = Array2::<f64>::zeros((n, n));
for r in 0..n {
for c in 0..n {
let mut v = 0.0_f64;
for k in 0..n {
v += x_inv[[r, k]] * t[[k, c]];
}
x_inv_t[[r, c]] = v;
}
}
let mut rhs = rp.clone();
for i in 0..m {
let mut ai_xinv = Array2::<f64>::zeros((n, n));
for r in 0..n {
for c in 0..n {
let mut v = 0.0_f64;
for k in 0..n {
v += problem.a[i][[r, k]] * x_inv[[k, c]];
}
ai_xinv[[r, c]] = v;
}
}
let mut tr_val = 0.0_f64;
for r in 0..n {
for c in 0..n {
tr_val += ai_xinv[[r, c]] * x_inv_t[[c, r]];
}
}
rhs[i] += tr_val;
}
let dy = solve(&schur.view(), &rhs.view(), None)?;
let mut ds = rd.clone();
for i in 0..m {
ds = ds - &(&problem.a[i] * dy[i]);
}
let xs = mat_mul(x, s);
let x_ds = mat_mul(x, &ds);
let n_mat = {
let mut nm = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let diag = if i == j { sigma_mu } else { 0.0 };
nm[[i, j]] = diag - xs[[i, j]] - x_ds[[i, j]];
}
}
nm
};
let s_inv = spd_inv(s)?;
let tmp = mat_mul(x_inv, &n_mat);
let tmp2 = mat_mul(&tmp, &s_inv);
let dx = {
let mut d = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
d[[i, j]] = (tmp2[[i, j]] + tmp2[[j, i]]) * 0.5;
}
}
d
};
Ok((dx, dy, ds))
}
fn mat_mul(a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
let (m, k) = (a.nrows(), a.ncols());
let l = b.ncols();
let mut c = Array2::<f64>::zeros((m, l));
for i in 0..m {
for j in 0..l {
let mut v = 0.0_f64;
for p in 0..k {
v += a[[i, p]] * b[[p, j]];
}
c[[i, j]] = v;
}
}
c
}
pub fn primal_sdp_step(x: &mut Array2<f64>, dx: &Array2<f64>, alpha: f64) {
let n = x.nrows();
for i in 0..n {
for j in 0..n {
x[[i, j]] += alpha * dx[[i, j]];
}
}
}
pub fn dual_sdp_step(
y: &mut Array1<f64>,
s: &mut Array2<f64>,
dy: &Array1<f64>,
ds: &Array2<f64>,
alpha: f64,
) {
let m = y.len();
for i in 0..m {
y[i] += alpha * dy[i];
}
let n = s.nrows();
for i in 0..n {
for j in 0..n {
s[[i, j]] += alpha * ds[[i, j]];
}
}
}
pub fn sdp_duality_gap(x: &Array2<f64>, s: &Array2<f64>) -> f64 {
mat_inner(x, s)
}
fn max_step_length_pd(m: &Array2<f64>, dm: &Array2<f64>) -> f64 {
if dm.iter().all(|&v| v.abs() < 1e-15) {
return 1.0;
}
let mut lo = 0.0_f64;
let mut hi = 1.0_f64;
let full = m + &(dm * 1.0_f64);
if is_positive_definite(&full) {
return 1.0;
}
for _ in 0..30 {
let mid = (lo + hi) * 0.5;
let trial = m + &(dm * mid);
if is_positive_definite(&trial) {
lo = mid;
} else {
hi = mid;
}
}
lo
}
#[derive(Debug, Clone)]
pub struct MaxCutSdpResult {
pub sdp_matrix: Array2<f64>,
pub sdp_value: f64,
pub cut: Vec<i8>,
pub cut_value: f64,
pub converged: bool,
}
pub fn max_cut_sdp(w: &ArrayView2<f64>) -> OptimizeResult<MaxCutSdpResult> {
let n = w.nrows();
if w.ncols() != n {
return Err(OptimizeError::ValueError("Weight matrix must be square".into()));
}
let c = w.map(|&v| v * 0.25);
let b = Array1::<f64>::ones(n);
let mut a_mats: Vec<Array2<f64>> = Vec::with_capacity(n);
for k in 0..n {
let mut ak = Array2::<f64>::zeros((n, n));
ak[[k, k]] = 1.0;
a_mats.push(ak);
}
let problem = SDPProblem::new(c, a_mats, b)?;
let solver = SDPSolver::new();
let result = solver.solve(&problem)?;
let x_mat = &result.x;
let v = power_iteration(x_mat, 50);
let cut: Vec<i8> = v.iter().map(|&vi| if vi >= 0.0 { 1 } else { -1 }).collect();
let mut cut_value = 0.0_f64;
for i in 0..n {
for j in (i + 1)..n {
if cut[i] != cut[j] {
cut_value += w[[i, j]];
}
}
}
let sdp_value = result.primal_obj;
Ok(MaxCutSdpResult {
sdp_matrix: result.x,
sdp_value,
cut,
cut_value,
converged: result.converged,
})
}
fn power_iteration(a: &Array2<f64>, iters: usize) -> Array1<f64> {
let n = a.nrows();
let mut v = Array1::<f64>::ones(n);
let norm = (n as f64).sqrt();
for vi in v.iter_mut() {
*vi /= norm;
}
for _ in 0..iters {
let mut w = Array1::<f64>::zeros(n);
for i in 0..n {
for j in 0..n {
w[i] += a[[i, j]] * v[j];
}
}
let w_norm = w.iter().map(|x| x * x).sum::<f64>().sqrt().max(1e-15);
for wi in w.iter_mut() {
*wi /= w_norm;
}
v = w;
}
v
}
#[derive(Debug, Clone)]
pub struct MatrixCompletionSdpResult {
pub completed: Array2<f64>,
pub sdp_value: f64,
pub converged: bool,
}
pub fn matrix_completion_sdp(
p: usize,
q: usize,
observed: &[(usize, usize, f64)],
) -> OptimizeResult<MatrixCompletionSdpResult> {
let nn = p + q;
let c = Array2::<f64>::eye(nn) * 0.5;
let mut a_mats: Vec<Array2<f64>> = Vec::new();
let mut b_vals: Vec<f64> = Vec::new();
for &(row, col, val) in observed {
if row >= p || col >= q {
return Err(OptimizeError::ValueError(format!(
"Observed entry ({}, {}) out of range ({}, {})",
row, col, p, q
)));
}
let col_lifted = p + col;
let mut ak = Array2::<f64>::zeros((nn, nn));
ak[[row, col_lifted]] = 0.5;
ak[[col_lifted, row]] = 0.5;
a_mats.push(ak);
b_vals.push(val);
}
if a_mats.is_empty() {
return Ok(MatrixCompletionSdpResult {
completed: Array2::<f64>::zeros((p, q)),
sdp_value: 0.0,
converged: true,
});
}
let b = Array1::from_vec(b_vals);
let problem = SDPProblem::new(c, a_mats, b)?;
let mut config = SDPSolverConfig::default();
config.tol = 1e-5; let solver = SDPSolver::with_config(config);
let result = solver.solve(&problem)?;
let mut completed = Array2::<f64>::zeros((p, q));
for i in 0..p {
for j in 0..q {
completed[[i, j]] = result.x[[i, p + j]];
}
}
Ok(MatrixCompletionSdpResult {
completed,
sdp_value: result.primal_obj,
converged: result.converged,
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_sdp_duality_gap_zero() {
let x = Array2::<f64>::eye(3);
let s = Array2::<f64>::zeros((3, 3));
assert_abs_diff_eq!(sdp_duality_gap(&x, &s), 0.0, epsilon = 1e-12);
}
#[test]
fn test_sdp_duality_gap_positive() {
let x = Array2::<f64>::eye(2);
let s = Array2::<f64>::eye(2);
assert_abs_diff_eq!(sdp_duality_gap(&x, &s), 2.0, epsilon = 1e-12);
}
#[test]
fn test_primal_sdp_step() {
let mut x = Array2::<f64>::eye(2);
let dx = Array2::<f64>::eye(2) * 0.5;
primal_sdp_step(&mut x, &dx, 0.2);
assert_abs_diff_eq!(x[[0, 0]], 1.1, epsilon = 1e-12);
}
#[test]
fn test_dual_sdp_step() {
let mut y = Array1::<f64>::zeros(2);
let dy = Array1::from_vec(vec![1.0, -1.0]);
let mut s = Array2::<f64>::eye(2);
let ds = Array2::<f64>::eye(2) * (-0.5_f64);
dual_sdp_step(&mut y, &mut s, &dy, &ds, 0.5);
assert_abs_diff_eq!(y[0], 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(y[1], -0.5, epsilon = 1e-12);
assert_abs_diff_eq!(s[[0, 0]], 0.75, epsilon = 1e-12);
}
#[test]
fn test_sdp_simple_1d() {
let c = Array2::<f64>::eye(1);
let mut a0 = Array2::<f64>::zeros((1, 1));
a0[[0, 0]] = 1.0;
let b = Array1::from_vec(vec![1.0]);
let problem = SDPProblem::new(c, vec![a0], b).expect("valid problem");
let solver = SDPSolver::new();
let result = solver.solve(&problem).expect("solver should not fail");
assert_abs_diff_eq!(result.primal_obj, 1.0, epsilon = 1e-4);
}
#[test]
fn test_max_cut_sdp_triangle() {
let mut w = Array2::<f64>::zeros((3, 3));
w[[0, 1]] = 1.0;
w[[1, 0]] = 1.0;
w[[0, 2]] = 1.0;
w[[2, 0]] = 1.0;
w[[1, 2]] = 1.0;
w[[2, 1]] = 1.0;
let result = max_cut_sdp(&w.view()).expect("max_cut_sdp should not fail");
assert!(result.cut_value >= 1.0, "Cut value should be at least 1");
}
#[test]
fn test_matrix_completion_simple() {
let observed = vec![(0, 0, 1.0), (1, 1, 1.0)];
let result = matrix_completion_sdp(2, 2, &observed)
.expect("matrix_completion_sdp should not fail");
assert!(result.completed[[0, 0]].is_finite());
assert!(result.completed[[1, 1]].is_finite());
}
}