resopt 0.3.0

Declarative constrained residual optimization in Rust
Documentation
#[cfg(feature = "clarabel")]
use resopt::{
    Bounds, ClarabelSolver, ConstrainedResidualProblem, LinearEqualities, LinearInequalities,
    LinearResidual, Loss, Matrix, SolveResult, SolveStatus, Solver, TikhonovRegularization,
};

#[cfg(feature = "clarabel")]
fn assert_slice_close(actual: &[f64], expected: &[f64], tol: f64) {
    assert_eq!(actual.len(), expected.len());

    for (idx, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
        assert!(
            (a - e).abs() <= tol,
            "mismatch at index {idx}: actual={a}, expected={e}, tol={tol}"
        );
    }
}

#[cfg(feature = "clarabel")]
fn identity_problem(target: Vec<f64>) -> ConstrainedResidualProblem {
    let matrix = Matrix::from_row_major(2, 2, vec![1.0, 0.0, 0.0, 1.0]).unwrap();
    let residual = LinearResidual::new(matrix, target).unwrap();
    ConstrainedResidualProblem::new(residual, Loss::L2Squared).unwrap()
}

#[cfg(feature = "clarabel")]
fn solve(problem: &ConstrainedResidualProblem) -> resopt::SolveResult {
    ClarabelSolver::new().solve(problem).unwrap()
}

#[cfg(feature = "clarabel")]
fn assert_solved_x(
    problem: &ConstrainedResidualProblem,
    expected_x: &[f64],
    tol: f64,
) -> SolveResult {
    let result = solve(problem);
    assert_eq!(result.status(), SolveStatus::Solved);

    let solution = result.solution().unwrap();
    assert_slice_close(solution.x(), expected_x, tol);
    assert!(
        result
            .diagnostics()
            .max_constraint_violation()
            .unwrap_or(f64::INFINITY)
            <= 1e-8
    );

    result
}

#[cfg(feature = "clarabel")]
fn sum_leq_constraint(rhs: f64) -> LinearInequalities {
    LinearInequalities::new(
        Matrix::from_row_major(1, 2, vec![1.0, 1.0]).unwrap(),
        vec![rhs],
    )
    .unwrap()
}

#[cfg(feature = "clarabel")]
#[test]
fn test_unconstrained_identity_problem() {
    let problem = identity_problem(vec![2.0, -3.0]);
    assert_solved_x(&problem, &[2.0, -3.0], 1e-8);
}

#[cfg(feature = "clarabel")]
#[test]
fn test_box_constraints() {
    let problem = identity_problem(vec![3.0, -2.0])
        .with_bounds(Bounds::new(vec![Some(0.0), Some(-1.0)], vec![Some(2.0), Some(4.0)]).unwrap())
        .unwrap();

    assert_solved_x(&problem, &[2.0, -1.0], 1e-7);
}

#[cfg(feature = "clarabel")]
#[test]
fn test_equality_constraint() {
    let equality = LinearEqualities::new(
        Matrix::from_row_major(1, 2, vec![1.0, 1.0]).unwrap(),
        vec![1.0],
    )
    .unwrap();

    let problem = identity_problem(vec![2.0, 0.0])
        .add_equalities(equality)
        .unwrap();

    assert_solved_x(&problem, &[1.5, -0.5], 1e-7);
}

#[cfg(feature = "clarabel")]
#[test]
fn test_inequality_constraint() {
    let problem = identity_problem(vec![3.0, 3.0])
        .add_inequalities(sum_leq_constraint(4.0))
        .unwrap();

    assert_solved_x(&problem, &[2.0, 2.0], 1e-7);
}

#[cfg(feature = "clarabel")]
#[test]
fn test_kkt_conditions_hold() {
    let problem = identity_problem(vec![3.0, 3.0])
        .add_inequalities(sum_leq_constraint(4.0))
        .unwrap();

    let result = assert_solved_x(&problem, &[2.0, 2.0], 1e-7);

    let x = result.solution().unwrap().x();
    let lambda = 2.0;
    let constraint_value = x[0] + x[1] - 4.0;
    let gradient = [2.0 * (x[0] - 3.0), 2.0 * (x[1] - 3.0)];
    let stationarity = [gradient[0] + lambda, gradient[1] + lambda];

    assert!(constraint_value <= 1e-8, "primal feasibility violated");
    assert!(lambda >= 0.0, "dual feasibility violated");
    assert!(
        (lambda * constraint_value).abs() <= 1e-6,
        "complementarity violated"
    );
    assert_slice_close(&stationarity, &[0.0, 0.0], 1e-6);
}

#[cfg(feature = "clarabel")]
#[test]
fn test_tikhonov_ridge_identity_problem() {
    let problem = identity_problem(vec![2.0, -3.0])
        .with_regularization(TikhonovRegularization::ridge(2, 1.0).unwrap())
        .unwrap();

    let result = assert_solved_x(&problem, &[1.0, -1.5], 1e-7);
    assert!((result.objective_value().unwrap() - 3.25).abs() <= 1e-7);
}

#[cfg(feature = "clarabel")]
#[test]
fn test_general_tikhonov_regularization() {
    let regularization = TikhonovRegularization::new(
        2.0,
        Matrix::from_row_major(1, 2, vec![1.0, -1.0]).unwrap(),
        vec![0.0],
    )
    .unwrap();

    let problem = identity_problem(vec![2.0, 0.0])
        .with_regularization(regularization)
        .unwrap();

    assert_solved_x(&problem, &[1.2, 0.8], 1e-6);
}