use resopt::{
Bounds, ConstrainedResidualProblem, ConstrainedResidualProblemBuilder, DefaultSolver,
LinearEqualities, LinearInequalities, LinearResidual, Loss, Matrix, ProblemClass, SolveStatus,
Solver, TikhonovRegularization,
};
#[test]
fn unconstrained_l2_problem_pipeline() {
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();
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());
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);
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);
}
#[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());
}
#[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);
}
#[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);
}
#[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);
}
#[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);
}