resopt 0.3.0

Declarative constrained residual optimization in Rust
Documentation
use crate::core::{Error, Matrix, Vector};

/// One block of linear equality constraints A_eq x = b_eq.
#[derive(Debug, Clone, PartialEq)]
pub struct LinearEqualities {
    matrix: Matrix,
    rhs: Vector,
}

impl LinearEqualities {
    pub fn new(matrix: Matrix, rhs: Vector) -> Result<Self, Error> {
        if matrix.nrows() != rhs.len() {
            return Err(Error::DimensionMismatch {
                message: format!(
                    "equality matrix row count ({}) must match rhs length ({})",
                    matrix.nrows(),
                    rhs.len()
                ),
            });
        }

        Ok(Self { matrix, rhs })
    }

    pub fn matrix(&self) -> &Matrix {
        &self.matrix
    }

    pub fn rhs(&self) -> &[f64] {
        &self.rhs
    }

    pub fn rows(&self) -> usize {
        self.matrix.nrows()
    }
}

/// One block of linear inequality constraints A_ineq x <= b_ineq.
#[derive(Debug, Clone, PartialEq)]
pub struct LinearInequalities {
    matrix: Matrix,
    rhs: Vector,
}

impl LinearInequalities {
    pub fn new(matrix: Matrix, rhs: Vector) -> Result<Self, Error> {
        if matrix.nrows() != rhs.len() {
            return Err(Error::DimensionMismatch {
                message: format!(
                    "inequality matrix row count ({}) must match rhs length ({})",
                    matrix.nrows(),
                    rhs.len()
                ),
            });
        }

        Ok(Self { matrix, rhs })
    }

    pub fn matrix(&self) -> &Matrix {
        &self.matrix
    }

    pub fn rhs(&self) -> &[f64] {
        &self.rhs
    }

    pub fn rows(&self) -> usize {
        self.matrix.nrows()
    }
}

/// Global variable bounds lower <= x <= upper.
#[derive(Debug, Clone, PartialEq)]
pub struct Bounds {
    lower: Vec<Option<f64>>,
    upper: Vec<Option<f64>>,
}

impl Bounds {
    pub fn new(lower: Vec<Option<f64>>, upper: Vec<Option<f64>>) -> Result<Self, Error> {
        if lower.len() != upper.len() {
            return Err(Error::DimensionMismatch {
                message: format!(
                    "lower bound length ({}) must match upper bound length ({})",
                    lower.len(),
                    upper.len()
                ),
            });
        }

        Ok(Self { lower, upper })
    }

    pub fn free(dim: usize) -> Self {
        Self {
            lower: vec![None; dim],
            upper: vec![None; dim],
        }
    }

    pub fn nonnegative(dim: usize) -> Self {
        Self {
            lower: vec![Some(0.0); dim],
            upper: vec![None; dim],
        }
    }

    pub fn len(&self) -> usize {
        self.lower.len()
    }

    pub fn is_empty(&self) -> bool {
        self.lower.is_empty()
    }

    pub fn lower(&self) -> &[Option<f64>] {
        &self.lower
    }

    pub fn upper(&self) -> &[Option<f64>] {
        &self.upper
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // -- LinearEqualities --

    #[test]
    fn equalities_valid() {
        let m = Matrix::from_row_major(2, 3, vec![1.0; 6]).unwrap();
        let eq = LinearEqualities::new(m, vec![1.0, 2.0]).unwrap();
        assert_eq!(eq.rows(), 2);
        assert_eq!(eq.rhs(), &[1.0, 2.0]);
    }

    #[test]
    fn equalities_dimension_mismatch() {
        let m = Matrix::from_row_major(2, 3, vec![1.0; 6]).unwrap();
        assert!(LinearEqualities::new(m, vec![1.0]).is_err());
    }

    #[test]
    fn equalities_accessors() {
        let m = Matrix::from_row_major(1, 2, vec![3.0, 4.0]).unwrap();
        let eq = LinearEqualities::new(m.clone(), vec![5.0]).unwrap();
        assert_eq!(eq.matrix(), &m);
        assert_eq!(eq.rhs(), &[5.0]);
        assert_eq!(eq.rows(), 1);
    }

    // -- LinearInequalities --

    #[test]
    fn inequalities_valid() {
        let m = Matrix::from_row_major(1, 2, vec![1.0, -1.0]).unwrap();
        let ineq = LinearInequalities::new(m, vec![0.0]).unwrap();
        assert_eq!(ineq.rows(), 1);
        assert_eq!(ineq.rhs(), &[0.0]);
    }

    #[test]
    fn inequalities_dimension_mismatch() {
        let m = Matrix::from_row_major(2, 2, vec![1.0; 4]).unwrap();
        assert!(LinearInequalities::new(m, vec![1.0]).is_err());
    }

    #[test]
    fn inequalities_accessors() {
        let m = Matrix::from_row_major(2, 2, vec![1.0, 0.0, 0.0, 1.0]).unwrap();
        let ineq = LinearInequalities::new(m.clone(), vec![10.0, 20.0]).unwrap();
        assert_eq!(ineq.matrix(), &m);
        assert_eq!(ineq.rhs(), &[10.0, 20.0]);
        assert_eq!(ineq.rows(), 2);
    }

    // -- Bounds --

    #[test]
    fn bounds_free() {
        let b = Bounds::free(3);
        assert_eq!(b.len(), 3);
        assert!(!b.is_empty());
        assert!(b.lower().iter().all(|v| v.is_none()));
        assert!(b.upper().iter().all(|v| v.is_none()));
    }

    #[test]
    fn bounds_nonnegative() {
        let b = Bounds::nonnegative(2);
        assert_eq!(b.lower(), &[Some(0.0), Some(0.0)]);
        assert!(b.upper().iter().all(|v| v.is_none()));
    }

    #[test]
    fn bounds_custom() {
        let b = Bounds::new(vec![Some(-1.0), None], vec![Some(1.0), Some(10.0)]).unwrap();
        assert_eq!(b.len(), 2);
        assert_eq!(b.lower(), &[Some(-1.0), None]);
        assert_eq!(b.upper(), &[Some(1.0), Some(10.0)]);
    }

    #[test]
    fn bounds_length_mismatch() {
        assert!(Bounds::new(vec![None, None], vec![None]).is_err());
    }

    #[test]
    fn bounds_empty() {
        let b = Bounds::free(0);
        assert!(b.is_empty());
        assert_eq!(b.len(), 0);
    }
}