use crate::core::{
Bounds, ConstrainedResidualProblem, Error, LinearEqualities, LinearInequalities,
LinearResidual, Loss, Matrix, TikhonovRegularization, Vector,
};
#[derive(Debug, Default, Clone)]
pub struct ConstrainedResidualProblemBuilder {
matrix: Option<Matrix>,
target: Option<Vector>,
residual: Option<LinearResidual>,
loss: Option<Loss>,
equalities: Vec<LinearEqualities>,
inequalities: Vec<LinearInequalities>,
bounds: Option<Bounds>,
regularization: Option<TikhonovRegularization>,
}
impl ConstrainedResidualProblemBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn matrix(mut self, matrix: Matrix) -> Self {
self.matrix = Some(matrix);
self
}
pub fn target(mut self, target: Vector) -> Self {
self.target = Some(target);
self
}
pub fn residual(mut self, residual: LinearResidual) -> Self {
self.residual = Some(residual);
self
}
pub fn loss(mut self, loss: Loss) -> Self {
self.loss = Some(loss);
self
}
pub fn add_equalities(mut self, equalities: LinearEqualities) -> Self {
self.equalities.push(equalities);
self
}
pub fn add_inequalities(mut self, inequalities: LinearInequalities) -> Self {
self.inequalities.push(inequalities);
self
}
pub fn bounds(mut self, bounds: Bounds) -> Self {
self.bounds = Some(bounds);
self
}
pub fn regularization(mut self, regularization: TikhonovRegularization) -> Self {
self.regularization = Some(regularization);
self
}
pub fn build(self) -> Result<ConstrainedResidualProblem, Error> {
let residual = match (self.residual, self.matrix, self.target) {
(Some(residual), None, None) => residual,
(None, Some(matrix), Some(target)) => LinearResidual::new(matrix, target)?,
(Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
return Err(Error::InvalidParameter {
message: "provide either `residual(...)` or (`matrix(...)` and `target(...)`), but not both"
.to_string(),
});
}
_ => return Err(Error::MissingField { field: "residual" }),
};
let loss = self.loss.ok_or(Error::MissingField { field: "loss" })?;
let mut problem = ConstrainedResidualProblem::new(residual, loss)?;
for eq in self.equalities {
problem = problem.add_equalities(eq)?;
}
for ineq in self.inequalities {
problem = problem.add_inequalities(ineq)?;
}
if let Some(bounds) = self.bounds {
problem = problem.with_bounds(bounds)?;
}
if let Some(regularization) = self.regularization {
problem = problem.with_regularization(regularization)?;
}
Ok(problem)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ProblemClass;
#[test]
fn minimal_with_matrix_and_target() {
let m = Matrix::from_row_major(2, 3, vec![1.0; 6]).unwrap();
let p = ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![1.0, 2.0])
.loss(Loss::L2Squared)
.build()
.unwrap();
assert_eq!(p.x_dim(), 3);
assert_eq!(p.residual_dim(), 2);
assert_eq!(p.class(), ProblemClass::Unconstrained);
}
#[test]
fn minimal_with_residual() {
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 p = ConstrainedResidualProblemBuilder::new()
.residual(residual)
.loss(Loss::L1)
.build()
.unwrap();
assert_eq!(p.loss(), &Loss::L1);
}
#[test]
fn with_equalities() {
let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
let eq_m = Matrix::from_row_major(1, 2, vec![1.0, 1.0]).unwrap();
let eq = LinearEqualities::new(eq_m, vec![1.0]).unwrap();
let p = ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![0.0, 0.0])
.loss(Loss::L2Squared)
.add_equalities(eq)
.build()
.unwrap();
assert_eq!(p.equalities().len(), 1);
assert_eq!(p.class(), ProblemClass::EqualityConstrained);
}
#[test]
fn with_inequalities() {
let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
let ineq_m = Matrix::from_row_major(1, 2, vec![1.0, -1.0]).unwrap();
let ineq = LinearInequalities::new(ineq_m, vec![0.0]).unwrap();
let p = ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![0.0, 0.0])
.loss(Loss::LInf)
.add_inequalities(ineq)
.build()
.unwrap();
assert_eq!(p.inequalities().len(), 1);
assert_eq!(p.class(), ProblemClass::InequalityConstrained);
}
#[test]
fn with_bounds() {
let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
let p = ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![0.0, 0.0])
.loss(Loss::L2Squared)
.bounds(Bounds::nonnegative(2))
.build()
.unwrap();
assert!(p.bounds().is_some());
assert_eq!(p.class(), ProblemClass::Bounded);
}
#[test]
fn with_regularization() {
let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
let p = ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![0.0, 0.0])
.loss(Loss::L2Squared)
.regularization(TikhonovRegularization::ridge(2, 1.0).unwrap())
.build()
.unwrap();
assert!(p.regularization().is_some());
assert_eq!(p.summary().regularization_dim, 2);
}
#[test]
fn mixed_constrained() {
let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
let eq_m = Matrix::from_row_major(1, 2, vec![1.0, 1.0]).unwrap();
let ineq_m = Matrix::from_row_major(1, 2, vec![1.0, 0.0]).unwrap();
let p = ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![0.0, 0.0])
.loss(Loss::Huber { delta: 1.0 })
.add_equalities(LinearEqualities::new(eq_m, vec![1.0]).unwrap())
.add_inequalities(LinearInequalities::new(ineq_m, vec![5.0]).unwrap())
.build()
.unwrap();
assert_eq!(p.class(), ProblemClass::MixedConstrained);
}
#[test]
fn missing_residual() {
let result = ConstrainedResidualProblemBuilder::new()
.loss(Loss::L2Squared)
.build();
assert_eq!(
result.unwrap_err(),
Error::MissingField { field: "residual" }
);
}
#[test]
fn missing_loss() {
let m = Matrix::from_row_major(1, 1, vec![1.0]).unwrap();
let result = ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![1.0])
.build();
assert_eq!(result.unwrap_err(), Error::MissingField { field: "loss" });
}
#[test]
fn both_residual_and_matrix() {
let m = Matrix::from_row_major(1, 1, vec![1.0]).unwrap();
let residual = LinearResidual::new(m.clone(), vec![1.0]).unwrap();
let result = ConstrainedResidualProblemBuilder::new()
.residual(residual)
.matrix(m)
.loss(Loss::L2Squared)
.build();
match result.unwrap_err() {
Error::InvalidParameter { message } => assert!(message.contains("not both")),
other => panic!("unexpected: {:?}", other),
}
}
#[test]
fn residual_dimension_mismatch() {
let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
assert!(ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![1.0])
.loss(Loss::L2Squared)
.build()
.is_err());
}
#[test]
fn equality_dimension_mismatch() {
let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
let eq_m = Matrix::from_row_major(1, 3, vec![1.0; 3]).unwrap();
assert!(ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![1.0, 2.0])
.loss(Loss::L2Squared)
.add_equalities(LinearEqualities::new(eq_m, vec![1.0]).unwrap())
.build()
.is_err());
}
#[test]
fn bounds_dimension_mismatch() {
let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
assert!(ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![1.0, 2.0])
.loss(Loss::L2Squared)
.bounds(Bounds::free(5))
.build()
.is_err());
}
#[test]
fn regularization_dimension_mismatch() {
let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
assert!(ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![1.0, 2.0])
.loss(Loss::L2Squared)
.regularization(TikhonovRegularization::ridge(5, 1.0).unwrap())
.build()
.is_err());
}
#[test]
fn invalid_huber_delta() {
let m = Matrix::from_row_major(1, 1, vec![1.0]).unwrap();
assert!(ConstrainedResidualProblemBuilder::new()
.matrix(m)
.target(vec![1.0])
.loss(Loss::Huber { delta: -1.0 })
.build()
.is_err());
}
}