resopt 0.3.0

Declarative constrained residual optimization in Rust
Documentation
use resopt::{
    Bounds, ConstrainedResidualProblem, ConstrainedResidualProblemBuilder, DefaultSolver,
    LinearEqualities, LinearInequalities, LinearResidual, Loss, Matrix, ProblemClass, SolveStatus,
    Solver, TikhonovRegularization,
};

// ---------------------------------------------------------------------------
// Full pipeline: build -> summarize -> classify -> solve
// ---------------------------------------------------------------------------

#[test]
fn unconstrained_l2_problem_pipeline() {
    // minimize 1/2 ||Ax - b||_2^2
    let a = Matrix::from_row_major(3, 2, vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0]).unwrap();
    let b = vec![1.0, 2.0, 3.0];

    let problem = ConstrainedResidualProblemBuilder::new()
        .matrix(a)
        .target(b)
        .loss(Loss::L2Squared)
        .build()
        .unwrap();

    // Verify structure
    assert_eq!(problem.x_dim(), 2);
    assert_eq!(problem.residual_dim(), 3);
    assert_eq!(problem.class(), ProblemClass::Unconstrained);
    assert!(problem.equalities().is_empty());
    assert!(problem.inequalities().is_empty());
    assert!(problem.bounds().is_none());

    // Summary
    let summary = problem.summary();
    assert_eq!(summary.x_dim, 2);
    assert_eq!(summary.residual_dim, 3);
    assert_eq!(summary.equality_blocks, 0);
    assert_eq!(summary.equality_rows, 0);
    assert_eq!(summary.inequality_blocks, 0);
    assert_eq!(summary.inequality_rows, 0);
    assert!(!summary.has_bounds);
    assert!(!summary.has_regularization);
    assert_eq!(summary.regularization_dim, 0);
    assert_eq!(summary.loss, Loss::L2Squared);
    assert_eq!(summary.class, ProblemClass::Unconstrained);

    // Solve (placeholder returns NotImplemented)
    let result = problem.solve().unwrap();
    #[cfg(feature = "clarabel")]
    {
        assert_eq!(result.status(), SolveStatus::Solved);
        assert!(result.is_solved());
        assert!(result.solution().is_some());
    }
    #[cfg(not(feature = "clarabel"))]
    {
        assert_eq!(result.status(), SolveStatus::NotImplemented);
        assert!(!result.is_solved());
        assert!(result.diagnostics().iterations() == 0);
    }
}

#[test]
fn equality_constrained_problem_pipeline() {
    let a = Matrix::from_row_major(2, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
    let eq_a = Matrix::from_row_major(1, 3, vec![1.0, 1.0, 1.0]).unwrap();
    let eq = LinearEqualities::new(eq_a, vec![1.0]).unwrap();

    let problem = ConstrainedResidualProblemBuilder::new()
        .matrix(a)
        .target(vec![0.0, 0.0])
        .loss(Loss::L1)
        .add_equalities(eq)
        .build()
        .unwrap();

    assert_eq!(problem.class(), ProblemClass::EqualityConstrained);

    let summary = problem.summary();
    assert_eq!(summary.equality_blocks, 1);
    assert_eq!(summary.equality_rows, 1);

    let result = DefaultSolver::new().solve(&problem).unwrap();
    assert_eq!(result.status(), SolveStatus::NotImplemented);
}

#[test]
fn inequality_constrained_problem_pipeline() {
    let a = Matrix::from_row_major(2, 2, vec![1.0, 0.0, 0.0, 1.0]).unwrap();
    let ineq_a = Matrix::from_row_major(2, 2, vec![1.0, 0.0, 0.0, 1.0]).unwrap();
    let ineq = LinearInequalities::new(ineq_a, vec![10.0, 10.0]).unwrap();

    let problem = ConstrainedResidualProblemBuilder::new()
        .matrix(a)
        .target(vec![5.0, 5.0])
        .loss(Loss::LInf)
        .add_inequalities(ineq)
        .build()
        .unwrap();

    assert_eq!(problem.class(), ProblemClass::InequalityConstrained);

    let summary = problem.summary();
    assert_eq!(summary.inequality_blocks, 1);
    assert_eq!(summary.inequality_rows, 2);
}

#[test]
fn bounded_problem_pipeline() {
    let a = Matrix::from_row_major(2, 2, vec![1.0, 0.0, 0.0, 1.0]).unwrap();
    let bounds = Bounds::nonnegative(2);

    let problem = ConstrainedResidualProblemBuilder::new()
        .matrix(a)
        .target(vec![1.0, 2.0])
        .loss(Loss::Huber { delta: 0.5 })
        .bounds(bounds)
        .build()
        .unwrap();

    assert_eq!(problem.class(), ProblemClass::Bounded);
    assert!(problem.bounds().is_some());

    let summary = problem.summary();
    assert!(summary.has_bounds);
    assert_eq!(summary.loss, Loss::Huber { delta: 0.5 });
}

#[test]
fn mixed_constrained_problem_pipeline() {
    let a =
        Matrix::from_row_major(3, 3, vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]).unwrap();

    let eq_a = Matrix::from_row_major(1, 3, vec![1.0, 1.0, 1.0]).unwrap();
    let eq = LinearEqualities::new(eq_a, vec![1.0]).unwrap();

    let ineq_a = Matrix::from_row_major(1, 3, vec![1.0, 0.0, 0.0]).unwrap();
    let ineq = LinearInequalities::new(ineq_a, vec![5.0]).unwrap();

    let bounds = Bounds::new(
        vec![Some(0.0), Some(0.0), None],
        vec![None, None, Some(10.0)],
    )
    .unwrap();

    let problem = ConstrainedResidualProblemBuilder::new()
        .matrix(a)
        .target(vec![1.0, 2.0, 3.0])
        .loss(Loss::L2Squared)
        .add_equalities(eq)
        .add_inequalities(ineq)
        .bounds(bounds)
        .build()
        .unwrap();

    assert_eq!(problem.class(), ProblemClass::MixedConstrained);

    let summary = problem.summary();
    assert_eq!(summary.x_dim, 3);
    assert_eq!(summary.residual_dim, 3);
    assert_eq!(summary.equality_blocks, 1);
    assert_eq!(summary.inequality_blocks, 1);
    assert!(summary.has_bounds);

    let result = problem.solve().unwrap();
    #[cfg(feature = "clarabel")]
    {
        assert_eq!(result.status(), SolveStatus::Solved);
        assert!(result.is_solved());
    }
    #[cfg(not(feature = "clarabel"))]
    {
        assert_eq!(result.status(), SolveStatus::NotImplemented);
    }
    assert!(result.diagnostics().solve_time_seconds() >= 0.0);
}

// ---------------------------------------------------------------------------
// Problem constructed directly (without builder)
// ---------------------------------------------------------------------------

#[test]
fn direct_construction_and_chaining() {
    let m = Matrix::from_row_major(2, 2, vec![1.0, 0.0, 0.0, 1.0]).unwrap();
    let residual = LinearResidual::new(m, vec![1.0, 2.0]).unwrap();

    let eq_m = Matrix::from_row_major(1, 2, vec![1.0, 1.0]).unwrap();
    let eq = LinearEqualities::new(eq_m, vec![3.0]).unwrap();

    let problem = ConstrainedResidualProblem::new(residual, Loss::L2Squared)
        .unwrap()
        .add_equalities(eq)
        .unwrap()
        .with_bounds(Bounds::free(2))
        .unwrap();

    assert_eq!(problem.class(), ProblemClass::MixedConstrained);
    assert_eq!(problem.equalities().len(), 1);
    assert!(problem.bounds().is_some());
}

#[test]
fn problem_with_tikhonov_regularization_pipeline() {
    let problem = ConstrainedResidualProblemBuilder::new()
        .matrix(Matrix::from_row_major(2, 2, vec![1.0, 0.0, 0.0, 1.0]).unwrap())
        .target(vec![2.0, -3.0])
        .loss(Loss::L2Squared)
        .regularization(TikhonovRegularization::ridge(2, 1.0).unwrap())
        .build()
        .unwrap();

    let summary = problem.summary();
    assert!(summary.has_regularization);
    assert_eq!(summary.regularization_dim, 2);

    let result = problem.solve().unwrap();
    #[cfg(feature = "clarabel")]
    {
        assert_eq!(result.status(), SolveStatus::Solved);
        let x = result.solution().unwrap().x();
        assert!((x[0] - 1.0).abs() <= 1e-6);
        assert!((x[1] + 1.5).abs() <= 1e-6);
    }
    #[cfg(not(feature = "clarabel"))]
    {
        assert_eq!(result.status(), SolveStatus::NotImplemented);
    }
}

#[test]
fn direct_construction_add_constraint_wrong_dim_fails() {
    let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
    let residual = LinearResidual::new(m, vec![1.0, 2.0]).unwrap();
    let problem = ConstrainedResidualProblem::new(residual, Loss::L2Squared).unwrap();

    let bad_eq_m = Matrix::from_row_major(1, 5, vec![1.0; 5]).unwrap();
    let bad_eq = LinearEqualities::new(bad_eq_m, vec![1.0]).unwrap();

    assert!(problem.add_equalities(bad_eq).is_err());
}

#[test]
fn direct_construction_bounds_wrong_dim_fails() {
    let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
    let residual = LinearResidual::new(m, vec![1.0, 2.0]).unwrap();
    let problem = ConstrainedResidualProblem::new(residual, Loss::L2Squared).unwrap();

    assert!(problem.with_bounds(Bounds::free(10)).is_err());
}

// ---------------------------------------------------------------------------
// Multiple constraint blocks
// ---------------------------------------------------------------------------

#[test]
fn multiple_equality_and_inequality_blocks() {
    let a = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();

    let eq1_m = Matrix::from_row_major(1, 2, vec![1.0, 0.0]).unwrap();
    let eq1 = LinearEqualities::new(eq1_m, vec![1.0]).unwrap();

    let eq2_m = Matrix::from_row_major(1, 2, vec![0.0, 1.0]).unwrap();
    let eq2 = LinearEqualities::new(eq2_m, vec![2.0]).unwrap();

    let ineq_m = Matrix::from_row_major(2, 2, vec![1.0, 1.0, -1.0, -1.0]).unwrap();
    let ineq = LinearInequalities::new(ineq_m, vec![10.0, 0.0]).unwrap();

    let problem = ConstrainedResidualProblemBuilder::new()
        .matrix(a)
        .target(vec![0.0, 0.0])
        .loss(Loss::L2Squared)
        .add_equalities(eq1)
        .add_equalities(eq2)
        .add_inequalities(ineq)
        .build()
        .unwrap();

    let summary = problem.summary();
    assert_eq!(summary.equality_blocks, 2);
    assert_eq!(summary.equality_rows, 2);
    assert_eq!(summary.inequality_blocks, 1);
    assert_eq!(summary.inequality_rows, 2);
    assert_eq!(summary.class, ProblemClass::MixedConstrained);
}

// ---------------------------------------------------------------------------
// Solver via trait object
// ---------------------------------------------------------------------------

#[test]
fn solver_trait_object() {
    let solver: Box<dyn Solver> = Box::new(DefaultSolver::new());

    let m = Matrix::from_row_major(1, 1, vec![1.0]).unwrap();
    let residual = LinearResidual::new(m, vec![0.0]).unwrap();
    let problem = ConstrainedResidualProblem::new(residual, Loss::L2Squared).unwrap();

    let result = solver.solve(&problem).unwrap();
    #[cfg(feature = "clarabel")]
    assert_eq!(result.status(), SolveStatus::Solved);
    #[cfg(not(feature = "clarabel"))]
    assert_eq!(result.status(), SolveStatus::NotImplemented);
}

// ---------------------------------------------------------------------------
// Problem validation
// ---------------------------------------------------------------------------

#[test]
fn valid_problem_passes_validation() {
    let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
    let residual = LinearResidual::new(m, vec![1.0, 2.0]).unwrap();
    let problem = ConstrainedResidualProblem::new(residual, Loss::L2Squared).unwrap();
    assert!(problem.validate().is_ok());
}

#[test]
fn problem_clone_and_eq() {
    let m = Matrix::from_row_major(1, 2, vec![1.0, 2.0]).unwrap();
    let residual = LinearResidual::new(m, vec![3.0]).unwrap();
    let p = ConstrainedResidualProblem::new(residual, Loss::L1).unwrap();
    let p2 = p.clone();
    assert_eq!(p, p2);
}

// ---------------------------------------------------------------------------
// Summary consistency
// ---------------------------------------------------------------------------

#[test]
fn summary_reflects_all_additions() {
    let a = Matrix::from_row_major(4, 3, vec![1.0; 12]).unwrap();
    let eq_m = Matrix::from_row_major(2, 3, vec![1.0; 6]).unwrap();
    let ineq_m = Matrix::from_row_major(3, 3, vec![1.0; 9]).unwrap();

    let problem = ConstrainedResidualProblemBuilder::new()
        .matrix(a)
        .target(vec![0.0; 4])
        .loss(Loss::L2Squared)
        .add_equalities(LinearEqualities::new(eq_m, vec![0.0; 2]).unwrap())
        .add_inequalities(LinearInequalities::new(ineq_m, vec![0.0; 3]).unwrap())
        .bounds(Bounds::free(3))
        .build()
        .unwrap();

    let s = problem.summary();
    assert_eq!(s.x_dim, 3);
    assert_eq!(s.residual_dim, 4);
    assert_eq!(s.equality_blocks, 1);
    assert_eq!(s.equality_rows, 2);
    assert_eq!(s.inequality_blocks, 1);
    assert_eq!(s.inequality_rows, 3);
    assert!(s.has_bounds);
    assert!(!s.has_regularization);
    assert_eq!(s.class, ProblemClass::MixedConstrained);
}