use cvxrust::prelude::*;
const TOL: f64 = 1e-4;
struct TestCase {
name: &'static str,
build: fn() -> (Problem, f64),
}
fn minimize_test_cases() -> Vec<TestCase> {
vec![
TestCase {
name: "sum_nonneg_constraint",
build: || {
let x = variable(5);
let prob = Problem::minimize(sum(&x))
.subject_to([x.ge(constant(1.0))])
.build();
(prob, 5.0)
},
},
TestCase {
name: "sum_equality_constraint",
build: || {
let x = variable(3);
let prob = Problem::minimize(sum(&x))
.subject_to([x.eq(constant(2.0))])
.build();
(prob, 6.0)
},
},
TestCase {
name: "sum_upper_bound",
build: || {
let x = variable(4);
let neg_sum = &constant(-1.0) * &sum(&x);
let prob = Problem::minimize(neg_sum)
.subject_to([x.le(constant(3.0))])
.build();
(prob, -12.0)
},
},
TestCase {
name: "weighted_sum",
build: || {
let x = variable(1);
let y = variable(1);
let obj = &(&constant(2.0) * &x) + &(&constant(3.0) * &y);
let prob = Problem::minimize(obj)
.subject_to([x.ge(constant(1.0)), y.ge(constant(2.0))])
.build();
(prob, 8.0)
},
},
TestCase {
name: "norm2_equality",
build: || {
let x = variable(5);
let prob = Problem::minimize(norm2(&x))
.subject_to([sum(&x).eq(constant(5.0))])
.build();
(prob, 5.0_f64.sqrt())
},
},
TestCase {
name: "norm2_zero",
build: || {
let x = variable(3);
let obj = &norm2(&x) + &constant(1.0);
let prob = Problem::minimize(obj)
.subject_to([x.eq(constant(0.0))])
.build();
(prob, 1.0)
},
},
TestCase {
name: "norm1_equality",
build: || {
let x = variable(3);
let prob = Problem::minimize(norm1(&x))
.subject_to([sum(&x).eq(constant(3.0))])
.build();
(prob, 3.0)
},
},
TestCase {
name: "norm_inf_equality",
build: || {
let x = variable(4);
let prob = Problem::minimize(norm_inf(&x))
.subject_to([sum(&x).eq(constant(4.0))])
.build();
(prob, 1.0)
},
},
TestCase {
name: "sum_squares_equality",
build: || {
let x = variable(2);
let prob = Problem::minimize(sum_squares(&x))
.subject_to([sum(&x).eq(constant(2.0))])
.build();
(prob, 2.0)
},
},
TestCase {
name: "sum_squares_nonneg",
build: || {
let x = variable(3);
let prob = Problem::minimize(sum_squares(&x))
.subject_to([x.ge(constant(1.0))])
.build();
(prob, 3.0)
},
},
TestCase {
name: "abs_equality",
build: || {
let x = variable(3);
let prob = Problem::minimize(sum(&abs(&x)))
.subject_to([x.eq(constant_vec(vec![-1.0, 2.0, -3.0]))])
.build();
(prob, 6.0)
},
},
TestCase {
name: "pos_nonneg",
build: || {
let x = variable(2);
let prob = Problem::minimize(sum(&pos(&x)))
.subject_to([x.ge(constant(-2.0)), sum(&x).eq(constant(1.0))])
.build();
(prob, 1.0)
},
},
TestCase {
name: "maximum_bound",
build: || {
let x = variable(1);
let y = variable(1);
let prob = Problem::minimize(max2(&x, &y))
.subject_to([x.ge(constant(1.0)), y.ge(constant(2.0))])
.build();
(prob, 2.0)
},
},
TestCase {
name: "box_constraints",
build: || {
let x = variable(3);
let prob = Problem::minimize(sum(&x))
.subject_to([x.ge(constant(1.0)), x.le(constant(2.0))])
.build();
(prob, 3.0)
},
},
TestCase {
name: "mixed_constraints",
build: || {
let x = variable(3);
let prob = Problem::minimize(norm2(&x))
.subject_to([x.ge(constant(0.0)), sum(&x).eq(constant(3.0))])
.build();
(prob, 3.0_f64.sqrt())
},
},
]
}
fn maximize_test_cases() -> Vec<TestCase> {
vec![
TestCase {
name: "maximize_sum_upper_bound",
build: || {
let x = variable(3);
let prob = Problem::maximize(sum(&x))
.subject_to([x.le(constant(2.0))])
.build();
(prob, 6.0)
},
},
TestCase {
name: "maximize_minimum",
build: || {
let x = variable(1);
let y = variable(1);
let prob = Problem::maximize(min2(&x, &y))
.subject_to([x.le(constant(3.0)), y.le(constant(2.0))])
.build();
(prob, 2.0)
},
},
TestCase {
name: "maximize_neg_norm",
build: || {
let x = variable(2);
let neg_norm = &constant(-1.0) * &norm2(&x);
let prob = Problem::maximize(neg_norm)
.subject_to([sum(&x).eq(constant(0.0))])
.build();
(prob, 0.0)
},
},
]
}
fn infeasible_test_cases() -> Vec<(&'static str, Problem)> {
vec![
("infeasible_bounds", {
let x = variable(3);
Problem::minimize(sum(&x))
.subject_to([x.ge(constant(1.0)), x.le(constant(0.0))])
.build()
}),
("infeasible_equality", {
let x = variable(1);
Problem::minimize(sum(&x))
.subject_to([x.eq(constant(1.0)), x.eq(constant(2.0))])
.build()
}),
]
}
fn unbounded_test_cases() -> Vec<(&'static str, Problem)> {
vec![
("unbounded_below", {
let x = variable(3);
Problem::minimize(sum(&x))
.subject_to([x.le(constant(1.0))])
.build()
}),
("unbounded_above", {
let x = variable(3);
Problem::maximize(sum(&x))
.subject_to([x.ge(constant(1.0))])
.build()
}),
]
}
#[test]
fn test_minimize_atoms() {
for case in minimize_test_cases() {
let (prob, expected) = (case.build)();
assert!(prob.is_dcp(), "Problem '{}' should be DCP", case.name);
let result = prob.solve();
assert!(
result.is_ok(),
"Problem '{}' should solve: {:?}",
case.name,
result.err()
);
let solution = result.unwrap();
assert_eq!(
solution.status,
SolveStatus::Optimal,
"Problem '{}' should be optimal, got {:?}",
case.name,
solution.status
);
let value = solution.value.expect("should have value");
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < TOL,
"Problem '{}': expected {}, got {} (rel_err={})",
case.name,
expected,
value,
rel_err
);
}
}
#[test]
fn test_maximize_atoms() {
for case in maximize_test_cases() {
let (prob, expected) = (case.build)();
assert!(prob.is_dcp(), "Problem '{}' should be DCP", case.name);
let result = prob.solve();
assert!(
result.is_ok(),
"Problem '{}' should solve: {:?}",
case.name,
result.err()
);
let solution = result.unwrap();
assert_eq!(
solution.status,
SolveStatus::Optimal,
"Problem '{}' should be optimal, got {:?}",
case.name,
solution.status
);
let value = solution.value.expect("should have value");
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < TOL,
"Problem '{}': expected {}, got {} (rel_err={})",
case.name,
expected,
value,
rel_err
);
}
}
#[test]
fn test_infeasible() {
for (name, prob) in infeasible_test_cases() {
let result = prob.solve();
match result {
Err(CvxError::SolverError(msg)) if msg.contains("infeasible") => {
}
Ok(solution) if solution.status == SolveStatus::Infeasible => {
}
other => {
panic!("Problem '{}' should be infeasible, got {:?}", name, other);
}
}
}
}
#[test]
fn test_unbounded() {
for (name, prob) in unbounded_test_cases() {
let result = prob.solve();
match result {
Err(CvxError::SolverError(msg)) if msg.contains("unbounded") => {
}
Ok(solution) if solution.status == SolveStatus::Unbounded => {
}
other => {
panic!("Problem '{}' should be unbounded, got {:?}", name, other);
}
}
}
}
#[test]
fn test_bug_sparse_constant_becomes_zero() {
use nalgebra_sparse::{CooMatrix, CscMatrix};
let mut coo = CooMatrix::new(3, 1);
coo.push(0, 0, 1.0);
coo.push(1, 0, 2.0);
coo.push(2, 0, 3.0);
let sparse_vec: CscMatrix<f64> = CscMatrix::from(&coo);
let sparse_const = constant_sparse(sparse_vec);
let x = variable(3);
let prob = Problem::minimize(sum(&x))
.subject_to([x.ge(sparse_const)])
.build();
assert!(prob.is_dcp(), "Problem should be DCP");
let solution = prob.solve().expect("Should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("Should have value");
let expected = 6.0; let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < TOL,
"BUG: Sparse constant became zeros! Expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_bug_right_matmul_broken() {
use nalgebra::DMatrix;
let a_matrix = DMatrix::from_vec(2, 2, vec![2.0, 0.0, 0.0, 3.0]);
let a = constant_dmatrix(a_matrix);
let x = variable((1, 2));
let y = matmul(&x, &a);
let prob = Problem::minimize(sum(&y))
.subject_to([x.ge(constant(1.0))])
.build();
assert!(prob.is_dcp(), "Problem should be DCP");
let solution = prob.solve().expect("Should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("Should have value");
let expected = 5.0; let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < TOL,
"BUG: Right matmul x @ A didn't multiply! Expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_bug_left_matmul_sparse_broken() {
use nalgebra_sparse::{CooMatrix, CscMatrix};
let mut coo = CooMatrix::new(2, 2);
coo.push(0, 0, 2.0);
coo.push(1, 1, 3.0);
let a_sparse: CscMatrix<f64> = CscMatrix::from(&coo);
let a = constant_sparse(a_sparse);
let x = variable(2);
let y = matmul(&a, &x);
let prob = Problem::minimize(sum(&y))
.subject_to([x.ge(constant(1.0))])
.build();
assert!(prob.is_dcp(), "Problem should be DCP");
let solution = prob.solve().expect("Should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("Should have value");
let expected = 5.0; let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < TOL,
"BUG: Left matmul with sparse A @ x didn't multiply! Expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_primal_values() {
let x = variable(3);
let result = Problem::minimize(sum(&x))
.subject_to([x.ge(constant(2.0))])
.solve()
.expect("should solve");
assert_eq!(result.status, SolveStatus::Optimal);
let x_val = result.get_value(x.variable_id().unwrap());
assert!(x_val.is_some(), "should have primal value for x");
let arr = x_val.unwrap();
match arr {
Array::Dense(m) => {
for i in 0..m.nrows() {
for j in 0..m.ncols() {
assert!(
(m[(i, j)] - 2.0).abs() < TOL,
"x[{},{}] should be ~2.0, got {}",
i,
j,
m[(i, j)]
);
}
}
}
_ => panic!("expected dense array"),
}
}
#[test]
fn test_neg_part() {
let x = variable(3);
let prob = Problem::minimize(sum(&neg_part(&x)))
.subject_to([sum(&x).eq(constant(0.0))])
.build();
assert!(prob.is_dcp(), "neg_part problem should be DCP");
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
assert!(
value.abs() < TOL,
"neg_part optimal should be 0, got {}",
value
);
}
#[test]
fn test_quad_form_psd() {
use nalgebra::DMatrix;
let p_mat = DMatrix::from_vec(2, 2, vec![2.0, 0.0, 0.0, 3.0]);
let p = constant_dmatrix(p_mat);
let x = variable(2);
let prob = Problem::minimize(quad_form(&x, &p))
.subject_to([sum(&x).eq(constant(1.0))])
.build();
assert!(
prob.is_dcp(),
"quad_form(x, PSD) should be DCP for minimize"
);
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = 1.2;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < TOL,
"quad_form expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_hstack() {
let x = variable(3);
let y = variable(3);
let h = hstack(vec![x.clone(), y.clone()]);
let prob = Problem::minimize(sum(&h))
.subject_to([x.ge(constant(1.0)), y.ge(constant(2.0))])
.build();
assert!(prob.is_dcp(), "hstack problem should be DCP");
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = 3.0 * 1.0 + 3.0 * 2.0; let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < TOL,
"hstack expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_weighted_sum() {
use nalgebra::DMatrix;
let x = variable(3);
let c_mat = DMatrix::from_vec(1, 3, vec![1.0, 2.0, 3.0]);
let c = constant_dmatrix(c_mat);
let prob = Problem::minimize(matmul(&c, &x))
.subject_to([x.ge(constant(1.0))])
.build();
assert!(prob.is_dcp(), "weighted sum problem should be DCP");
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = 6.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < TOL,
"weighted sum expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_transpose_in_constraint() {
let x = variable((2, 3)); let _xt = transpose(&x);
let prob = Problem::minimize(sum(&x))
.subject_to([x.ge(constant(1.0))])
.build();
assert!(prob.is_dcp(), "transpose problem should be DCP");
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = 6.0; let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(rel_err < TOL, "expected {}, got {}", expected, value);
}
#[test]
fn test_minimize_concave_not_dcp() {
let x = variable(3);
let prob = Problem::minimize(min2(&x, &constant(0.0))).build();
assert!(!prob.is_dcp(), "minimize(concave) should not be DCP");
}
#[test]
fn test_maximize_convex_not_dcp() {
let x = variable(3);
let prob = Problem::maximize(norm2(&x)).build();
assert!(!prob.is_dcp(), "maximize(convex) should not be DCP");
}
#[test]
fn test_composition_violation() {
let x = variable(3);
let inner = norm2(&x);
let outer = norm2(&inner);
let prob = Problem::minimize(outer).build();
assert!(!prob.is_dcp(), "norm(norm(x)) should not be DCP");
}
#[test]
fn test_affine_leq_convex_not_dcp() {
let x = variable(3);
let prob = Problem::minimize(sum(&x))
.subject_to([sum(&x).le(norm2(&x))]) .build();
assert!(!prob.is_dcp(), "affine <= convex should not be DCP");
}
#[test]
fn test_single_element() {
let x = variable(1);
let prob = Problem::minimize(sum(&x))
.subject_to([x.ge(constant(5.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
assert!(
(value - 5.0).abs() < TOL,
"single element: expected 5, got {}",
value
);
}
#[test]
fn test_constant_objective() {
let x = variable(3);
let prob = Problem::minimize(constant(1.0))
.subject_to([x.ge(constant(0.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
assert!(
(value - 1.0).abs() < TOL,
"constant obj: expected 1, got {}",
value
);
}
#[test]
fn test_tight_constraints() {
let x = variable(2);
let prob = Problem::minimize(sum(&x))
.subject_to([
sum(&x).eq(constant(3.0)),
(&x - &constant_vec(vec![0.0, 2.0])).eq(constant_vec(vec![2.0, -1.0])),
])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
assert!(
(value - 3.0).abs() < TOL,
"tight constraints: expected 3, got {}",
value
);
}
#[test]
fn test_scale_100_variables_lp() {
let x = variable(100);
let prob = Problem::minimize(sum(&x))
.subject_to([x.ge(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve 100-var LP");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = 100.0;
assert!(
(value - expected).abs() < TOL * 100.0,
"100-var LP: expected {}, got {}",
expected,
value
);
}
#[test]
fn test_scale_50_variables_socp() {
let x = variable(50);
let prob = Problem::minimize(norm2(&x))
.subject_to([sum(&x).eq(constant(50.0))])
.build();
let solution = prob.solve().expect("should solve 50-var SOCP");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = (50.0_f64).sqrt();
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < TOL,
"50-var SOCP: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_scale_30_variables_qp() {
let x = variable(30);
let prob = Problem::minimize(sum_squares(&x))
.subject_to([sum(&x).eq(constant(30.0))])
.build();
let solution = prob.solve().expect("should solve 30-var QP");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = 30.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < TOL,
"30-var QP: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
mod stress_tests {
use super::*;
use nalgebra::DMatrix;
const STRESS_TOL: f64 = 1e-3;
#[test]
fn test_quad_form_non_diagonal() {
let p_mat = DMatrix::from_vec(2, 2, vec![2.0, 1.0, 1.0, 2.0]);
let p = constant_dmatrix(p_mat);
let x = variable(2);
let prob = Problem::minimize(quad_form(&x, &p))
.subject_to([sum(&x).eq(constant(1.0))])
.build();
assert!(prob.is_dcp(), "quad_form with PSD should be DCP");
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = 1.5;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"non-diagonal quad_form: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_quad_form_3x3() {
let p_mat = DMatrix::from_vec(3, 3, vec![1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0]);
let p = constant_dmatrix(p_mat);
let x = variable(3);
let prob = Problem::minimize(quad_form(&x, &p))
.subject_to([sum(&x).eq(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 6.0 / 11.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"3x3 quad_form: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_nested_matmul() {
let a_mat = DMatrix::from_vec(2, 3, vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
let b_mat = DMatrix::from_vec(3, 2, vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
let a = constant_dmatrix(a_mat.clone());
let b = constant_dmatrix(b_mat.clone());
let x = variable(2);
let bx = matmul(&b, &x);
let y = matmul(&a, &bx);
let prob = Problem::minimize(sum(&y))
.subject_to([x.ge(constant(1.0))])
.build();
assert!(prob.is_dcp(), "nested matmul should be DCP");
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = 2.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"nested matmul: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_matmul_nonsquare() {
let a_mat = DMatrix::from_vec(
2,
4,
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, ],
);
let a = constant_dmatrix(a_mat);
let x = variable(4);
let prob = Problem::minimize(sum(&matmul(&a, &x)))
.subject_to([x.ge(constant(0.0)), sum(&x).eq(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 3.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"nonsquare matmul: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_right_matmul_row_vector() {
let a_mat = DMatrix::from_vec(
3,
2,
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, ],
);
let a = constant_dmatrix(a_mat);
let x = variable((1, 3));
let prob = Problem::minimize(sum(&matmul(&x, &a)))
.subject_to([x.ge(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 21.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"right matmul: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_multi_variable_objective() {
let x = variable(3);
let y = variable(2);
let z = variable(1);
let obj = &sum(&x) + &(&constant(2.0) * &sum(&y)) + &(&constant(3.0) * &sum(&z));
let prob = Problem::minimize(obj)
.subject_to([
x.ge(constant(1.0)),
y.ge(constant(1.0)),
z.ge(constant(1.0)),
])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 10.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"multi-var obj: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_shared_variable() {
let x = variable(3);
let prob = Problem::minimize(norm2(&x))
.subject_to([
sum(&x).ge(constant(3.0)),
x.ge(constant(0.0)),
x.le(constant(2.0)),
])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 3.0_f64.sqrt();
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"shared var: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_variable_multiple_constraints() {
let x = variable(3);
let prob = Problem::minimize(sum(&x))
.subject_to([
norm2(&x).le(constant(2.0)),
sum(&x).ge(constant(1.0)),
x.ge(constant(0.0)),
])
.build();
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
assert!(
(value - 1.0).abs() < STRESS_TOL,
"multi-constraint: expected ~1, got {}",
value
);
}
#[test]
fn test_vstack_objective() {
let x = variable(2);
let y = variable(2);
let stacked = vstack(vec![x.clone(), y.clone()]);
let prob = Problem::minimize(norm2(&stacked))
.subject_to([sum(&x).eq(constant(1.0)), sum(&y).eq(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 1.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"vstack obj: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_vstack_constraint() {
let x = variable(2);
let y = variable(2);
let stacked = vstack(vec![x.clone(), y.clone()]);
let prob = Problem::minimize(&sum(&x) + &sum(&y))
.subject_to([stacked.ge(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 4.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"vstack constraint: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_small_coefficients() {
let x = variable(3);
let small = constant(1e-6);
let prob = Problem::minimize(&small * &sum(&x))
.subject_to([x.ge(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 3e-6;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"small coeff: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_large_coefficients() {
let x = variable(3);
let large = constant(1e6);
let prob = Problem::minimize(&large * &sum(&x))
.subject_to([x.ge(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 3e6;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"large coeff: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_near_degenerate() {
let x = variable(2);
let c1 = sum(&x).eq(constant(1.0));
let c2 = (&x + &constant_vec(vec![0.0, 0.0001])).eq(constant_vec(vec![0.5, 0.5001]));
let prob = Problem::minimize(sum(&x)).subject_to([c1, c2]).build();
let result = prob.solve();
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_mixed_eq_ineq() {
let x = variable(4);
let _x01 = &constant_vec(vec![1.0, 1.0, 0.0, 0.0]);
let _x23 = &constant_vec(vec![0.0, 0.0, 1.0, 1.0]);
let c01 = DMatrix::from_vec(1, 4, vec![1.0, 1.0, 0.0, 0.0]);
let c23 = DMatrix::from_vec(1, 4, vec![0.0, 0.0, 1.0, 1.0]);
let prob = Problem::minimize(sum(&x))
.subject_to([
matmul(&constant_dmatrix(c01), &x).eq(constant(2.0)),
matmul(&constant_dmatrix(c23), &x).ge(constant(1.0)),
x.ge(constant(0.0)),
])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 3.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"mixed eq/ineq: expected {}, got {} (rel_err={})",
expected,
value,
rel_err
);
}
#[test]
fn test_multiple_socp() {
let x = variable(2);
let y = variable(2);
let prob = Problem::minimize(&sum(&x) + &sum(&y))
.subject_to([
norm2(&x).le(constant(1.0)),
norm2(&y).le(constant(1.0)),
sum(&x).ge(constant(0.5)),
sum(&y).ge(constant(0.5)),
])
.build();
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
assert!(
(0.99..=1.5).contains(&value),
"multiple SOCP: got {}",
value
);
}
#[test]
fn test_regression_sparse_constant_constraint() {
use nalgebra_sparse::{CooMatrix, CscMatrix};
let mut coo = CooMatrix::new(3, 1);
coo.push(1, 0, 5.0); let sparse: CscMatrix<f64> = CscMatrix::from(&coo);
let x = variable(3);
let prob = Problem::minimize(sum(&x))
.subject_to([x.ge(constant_sparse(sparse))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 5.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"sparse const regression: expected {}, got {}",
expected,
value
);
}
#[test]
fn test_regression_sparse_matmul() {
use nalgebra_sparse::{CooMatrix, CscMatrix};
let mut coo = CooMatrix::new(3, 3);
coo.push(0, 0, 2.0);
coo.push(1, 1, 3.0);
coo.push(2, 2, 4.0);
let sparse: CscMatrix<f64> = CscMatrix::from(&coo);
let a = constant_sparse(sparse);
let x = variable(3);
let prob = Problem::minimize(sum(&matmul(&a, &x)))
.subject_to([x.ge(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 9.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"sparse matmul regression: expected {}, got {}",
expected,
value
);
}
#[test]
fn test_scale_200_lp() {
let x = variable(200);
let prob = Problem::minimize(sum(&x))
.subject_to([x.ge(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve 200-var LP");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
assert!(
(value - 200.0).abs() < 1.0,
"200-var LP: expected ~200, got {}",
value
);
}
#[test]
fn test_scale_100_mixed() {
let x = variable(100);
let prob = Problem::minimize(norm2(&x))
.subject_to([sum(&x).eq(constant(100.0)), x.ge(constant(0.0))])
.build();
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = 10.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"100-var mixed: expected {}, got {}",
expected,
value
);
}
#[test]
fn test_scale_50_qp() {
let x = variable(50);
let prob = Problem::minimize(sum_squares(&x))
.subject_to([sum(&x).eq(constant(50.0))])
.build();
let solution = prob.solve().expect("should solve");
assert_eq!(solution.status, SolveStatus::Optimal);
let value = solution.value.expect("should have value");
let expected = 50.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"50-var QP: expected {}, got {}",
expected,
value
);
}
#[test]
fn test_deep_nesting() {
let x = variable(3);
let e1 = &x + &constant(1.0);
let e2 = &constant(2.0) * &e1;
let e3 = &e2 - &constant(1.0);
let e4 = &e3 + &constant(0.5);
let prob = Problem::minimize(sum(&e4))
.subject_to([x.ge(constant(0.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 4.5;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"deep nesting: expected {}, got {}",
expected,
value
);
}
#[test]
fn test_expression_reuse() {
let x = variable(3);
let y = sum(&x);
let prob = Problem::minimize(&y + &y)
.subject_to([x.ge(constant(1.0))])
.build();
let solution = prob.solve().expect("should solve");
let value = solution.value.expect("should have value");
let expected = 6.0;
let rel_err = (value - expected).abs() / (1.0 + expected.abs());
assert!(
rel_err < STRESS_TOL,
"expr reuse: expected {}, got {}",
expected,
value
);
}
}
#[test]
fn test_variable_product_not_dcp() {
let x = variable(1);
let y = variable(1);
let prod = &x * &y;
assert_eq!(prod.curvature(), cvxrust::dcp::Curvature::Unknown);
let prob = Problem::minimize(prod).build();
assert!(!prob.is_dcp());
let result = prob.solve();
assert!(result.is_err());
if let Err(e) = result {
let msg = format!("{}", e);
assert!(
msg.contains("not DCP") || msg.contains("NotDcp"),
"Expected NotDcp error, got: {}",
msg
);
}
}
#[test]
fn test_variable_matmul_not_dcp() {
let x = variable(3);
let y = variable(3);
let prod = matmul(&x, &transpose(&y));
assert_eq!(prod.curvature(), cvxrust::dcp::Curvature::Unknown);
let prob = Problem::minimize(sum(&prod)).build();
assert!(!prob.is_dcp());
}
#[test]
fn test_index_slice_in_constraint() {
use cvxrust::atoms::slice;
let x = variable(5);
let x_first = slice(&x, 0, 3); let x_last = slice(&x, 3, 5);
let prob = Problem::minimize(sum(&x))
.subject_to([x_first.ge(1.0), x_last.ge(2.0)])
.build();
assert!(prob.is_dcp());
let solution = prob.solve().unwrap();
assert_eq!(solution.status, SolveStatus::Optimal);
let val = solution.value.unwrap();
assert!((val - 7.0).abs() < TOL, "Expected 7.0, got {}", val);
let x_vals = &solution[&x];
for i in 0..3 {
assert!(
(x_vals[(i, 0)] - 1.0).abs() < TOL,
"x[{}] should be ~1.0",
i
);
}
for i in 3..5 {
assert!(
(x_vals[(i, 0)] - 2.0).abs() < TOL,
"x[{}] should be ~2.0",
i
);
}
}
#[test]
fn test_index_single_element() {
use cvxrust::atoms::index;
let x = variable(3);
let x0 = index(&x, 0);
let prob = Problem::minimize(x0)
.subject_to([x.ge(0.0), sum(&x).eq(5.0)])
.build();
assert!(prob.is_dcp());
let solution = prob.solve().unwrap();
assert_eq!(solution.status, SolveStatus::Optimal);
let x_vals = &solution[&x];
assert!(x_vals[(0, 0)].abs() < TOL, "x[0] should be ~0");
assert!((x_vals[(1, 0)] + x_vals[(2, 0)] - 5.0).abs() < TOL);
}
#[test]
fn test_transpose_in_matmul() {
let x = VariableBuilder::new(Shape::vector(2)).build();
let a = constant_matrix(vec![1.0, 3.0, 5.0, 2.0, 4.0, 6.0], 3, 2);
let b = constant_vec(vec![1.0, 2.0, 3.0]);
let residual = &matmul(&a, &x) - &b;
let obj = sum_squares(&residual);
let prob = Problem::minimize(obj).build();
assert!(prob.is_dcp());
let solution = prob.solve().unwrap();
assert_eq!(solution.status, SolveStatus::Optimal);
let at = transpose(&a);
let ata = matmul(&at, &a); let atax = matmul(&ata, &x); assert_eq!(atax.shape(), Shape::vector(2));
}