#[cfg(feature = "least-squares-linear-nonnegative")]
use alloc::{vec, vec::Vec};
use core::fmt;
#[cfg(feature = "least-squares-linear-nonnegative")]
use slatec_core::to_fortran_integer;
#[cfg(feature = "least-squares-linear-nonnegative")]
use slatec_sys::FortranInteger;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MatrixLayout {
ColumnMajor,
}
#[derive(Clone, Copy, Debug)]
pub struct MatrixRef<'a, T> {
data: &'a [T],
rows: usize,
columns: usize,
leading_dimension: usize,
layout: MatrixLayout,
}
impl<'a, T> MatrixRef<'a, T> {
pub fn column_major(
data: &'a [T],
rows: usize,
columns: usize,
leading_dimension: usize,
) -> Result<Self, LinearLeastSquaresError> {
if leading_dimension < rows {
return Err(LinearLeastSquaresError::DimensionMismatch {
argument: "matrix leading_dimension",
});
}
let needed = leading_dimension
.checked_mul(columns)
.ok_or(LinearLeastSquaresError::WorkspaceOverflow)?;
if data.len() < needed {
return Err(LinearLeastSquaresError::DimensionMismatch {
argument: "matrix physical storage",
});
}
Ok(Self {
data,
rows,
columns,
leading_dimension,
layout: MatrixLayout::ColumnMajor,
})
}
pub const fn rows(&self) -> usize {
self.rows
}
pub const fn columns(&self) -> usize {
self.columns
}
pub const fn leading_dimension(&self) -> usize {
self.leading_dimension
}
pub const fn layout(&self) -> MatrixLayout {
self.layout
}
pub fn get(&self, row: usize, column: usize) -> Option<&T> {
if row >= self.rows || column >= self.columns {
return None;
}
self.data.get(
column
.checked_mul(self.leading_dimension)?
.checked_add(row)?,
)
}
pub const fn as_column_major_slice(&self) -> &'a [T] {
self.data
}
}
#[cfg(any(
feature = "least-squares-linear-bounded",
feature = "least-squares-linear-bounded-constrained"
))]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum VariableBounds<T> {
Unbounded,
Lower(T),
Upper(T),
Between {
lower: T,
upper: T,
},
Fixed(T),
}
#[cfg(feature = "least-squares-linear-nonnegative")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VariableConstraint {
Free,
Nonnegative,
}
#[derive(Clone, Copy, Debug)]
#[cfg(feature = "least-squares-linear-nonnegative")]
pub struct NonnegativeLeastSquaresProblem<'a, T> {
pub least_squares_matrix: MatrixRef<'a, T>,
pub least_squares_rhs: &'a [T],
pub equality_matrix: Option<MatrixRef<'a, T>>,
pub equality_rhs: Option<&'a [T]>,
pub variable_constraints: &'a [VariableConstraint],
}
#[cfg(feature = "least-squares-linear-nonnegative")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NonnegativeLeastSquaresStatus {
Converged,
MaximumIterations,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg(feature = "least-squares-linear-nonnegative")]
pub struct NonnegativeLeastSquaresResult<T> {
pub solution: Vec<T>,
pub native_residual_norm: T,
pub least_squares_residual_norm: T,
pub equality_residual_norm: T,
pub status: NonnegativeLeastSquaresStatus,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LinearLeastSquaresError {
EmptyVariables,
EmptyRows,
DimensionMismatch {
argument: &'static str,
},
NonFiniteInput {
argument: &'static str,
index: usize,
},
IntegerOverflow {
argument: &'static str,
},
WorkspaceOverflow,
NativeContractViolation {
detail: &'static str,
},
NativeStatus {
status: i32,
},
}
impl fmt::Display for LinearLeastSquaresError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyVariables => write!(formatter, "nonnegative least squares needs variables"),
Self::EmptyRows => write!(
formatter,
"nonnegative least squares needs equality or least-squares rows"
),
Self::DimensionMismatch { argument } => write!(
formatter,
"nonnegative least-squares dimension mismatch for {argument}"
),
Self::NonFiniteInput { argument, index } => write!(
formatter,
"nonnegative least-squares {argument} at index {index} must be finite"
),
Self::IntegerOverflow { argument } => write!(
formatter,
"nonnegative least-squares {argument} does not fit Fortran INTEGER"
),
Self::WorkspaceOverflow => write!(
formatter,
"nonnegative least-squares workspace arithmetic overflowed"
),
Self::NativeContractViolation { detail } => write!(
formatter,
"native nonnegative least-squares contract was violated: {detail}"
),
Self::NativeStatus { status } => {
write!(formatter, "unknown WNNLS/DWNNLS MODE value {status}")
}
}
}
}
impl std::error::Error for LinearLeastSquaresError {}
#[cfg(feature = "least-squares-linear-nonnegative")]
pub fn solve_nonnegative_least_squares(
problem: NonnegativeLeastSquaresProblem<'_, f64>,
) -> Result<NonnegativeLeastSquaresResult<f64>, LinearLeastSquaresError> {
let prepared = PreparedF64::new(problem)?;
run_f64(prepared)
}
#[cfg(feature = "least-squares-linear-nonnegative")]
pub fn solve_nonnegative_least_squares_f32(
problem: NonnegativeLeastSquaresProblem<'_, f32>,
) -> Result<NonnegativeLeastSquaresResult<f32>, LinearLeastSquaresError> {
let prepared = PreparedF32::new(problem)?;
run_f32(prepared)
}
#[cfg(feature = "least-squares-linear-nonnegative")]
struct PreparedF64<'a> {
problem: NonnegativeLeastSquaresProblem<'a, f64>,
native_to_user: Vec<usize>,
augmented: Vec<f64>,
mdw: usize,
me: usize,
ma: usize,
free: usize,
work: usize,
iwork: usize,
}
#[cfg(feature = "least-squares-linear-nonnegative")]
struct PreparedF32<'a> {
problem: NonnegativeLeastSquaresProblem<'a, f32>,
native_to_user: Vec<usize>,
augmented: Vec<f32>,
mdw: usize,
me: usize,
ma: usize,
free: usize,
work: usize,
iwork: usize,
}
#[cfg(feature = "least-squares-linear-nonnegative")]
macro_rules! impl_prepared {
($name:ident, $scalar:ty) => {
impl<'a> $name<'a> {
fn new(
problem: NonnegativeLeastSquaresProblem<'a, $scalar>,
) -> Result<Self, LinearLeastSquaresError> {
let (me, ma, n) = validate_problem(&problem)?;
let native_to_user = native_to_user(problem.variable_constraints);
let mdw = me
.checked_add(ma)
.ok_or(LinearLeastSquaresError::WorkspaceOverflow)?;
let augmented_len = mdw
.checked_mul(
n.checked_add(1)
.ok_or(LinearLeastSquaresError::WorkspaceOverflow)?,
)
.ok_or(LinearLeastSquaresError::WorkspaceOverflow)?;
let work = mdw
.checked_add(
n.checked_mul(5)
.ok_or(LinearLeastSquaresError::WorkspaceOverflow)?,
)
.ok_or(LinearLeastSquaresError::WorkspaceOverflow)?;
let iwork = mdw
.checked_add(n)
.ok_or(LinearLeastSquaresError::WorkspaceOverflow)?;
let mut augmented = vec![0.0 as $scalar; augmented_len];
fill_augmented(&problem, &native_to_user, &mut augmented, mdw, me, ma);
for (name, value) in [
("MDW", mdw),
("ME", me),
("MA", ma),
("N", n),
(
"L",
native_to_user
.iter()
.take_while(|&&index| {
problem.variable_constraints[index] == VariableConstraint::Free
})
.count(),
),
("WORK length", work),
("IWORK length", iwork),
] {
to_fortran_integer(value)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: name })?;
}
let free = native_to_user
.iter()
.take_while(|&&index| {
problem.variable_constraints[index] == VariableConstraint::Free
})
.count();
Ok(Self {
problem,
native_to_user,
augmented,
mdw,
me,
ma,
free,
work,
iwork,
})
}
}
};
}
#[cfg(feature = "least-squares-linear-nonnegative")]
impl_prepared!(PreparedF64, f64);
#[cfg(feature = "least-squares-linear-nonnegative")]
impl_prepared!(PreparedF32, f32);
#[cfg(feature = "least-squares-linear-nonnegative")]
fn validate_problem<T: Copy + Finite>(
problem: &NonnegativeLeastSquaresProblem<'_, T>,
) -> Result<(usize, usize, usize), LinearLeastSquaresError> {
let n = problem.least_squares_matrix.columns();
if n == 0 {
return Err(LinearLeastSquaresError::EmptyVariables);
}
let ma = problem.least_squares_matrix.rows();
if problem.least_squares_rhs.len() != ma {
return Err(LinearLeastSquaresError::DimensionMismatch {
argument: "least_squares_rhs",
});
}
if problem.variable_constraints.len() != n {
return Err(LinearLeastSquaresError::DimensionMismatch {
argument: "variable_constraints",
});
}
let (me, equality, equality_rhs) = match (problem.equality_matrix, problem.equality_rhs) {
(Some(matrix), Some(rhs)) => (matrix.rows(), Some(matrix), rhs),
(None, None) => (0, None, &[] as &[T]),
_ => {
return Err(LinearLeastSquaresError::DimensionMismatch {
argument: "equality_matrix/equality_rhs",
});
}
};
if ma
.checked_add(me)
.ok_or(LinearLeastSquaresError::WorkspaceOverflow)?
== 0
{
return Err(LinearLeastSquaresError::EmptyRows);
}
if let Some(matrix) = equality {
if matrix.columns() != n {
return Err(LinearLeastSquaresError::DimensionMismatch {
argument: "equality_matrix columns",
});
}
if equality_rhs.len() != me {
return Err(LinearLeastSquaresError::DimensionMismatch {
argument: "equality_rhs",
});
}
}
finite_slice(
problem.least_squares_matrix.as_column_major_slice(),
"least_squares_matrix",
)?;
finite_slice(problem.least_squares_rhs, "least_squares_rhs")?;
if let Some(matrix) = equality {
finite_slice(matrix.as_column_major_slice(), "equality_matrix")?;
}
finite_slice(equality_rhs, "equality_rhs")?;
Ok((me, ma, n))
}
#[cfg(feature = "least-squares-linear-nonnegative")]
trait Finite {
fn finite(self) -> bool;
}
#[cfg(feature = "least-squares-linear-nonnegative")]
impl Finite for f64 {
fn finite(self) -> bool {
self.is_finite()
}
}
#[cfg(feature = "least-squares-linear-nonnegative")]
impl Finite for f32 {
fn finite(self) -> bool {
self.is_finite()
}
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn finite_slice<T: Copy + Finite>(
data: &[T],
argument: &'static str,
) -> Result<(), LinearLeastSquaresError> {
data.iter()
.copied()
.enumerate()
.find(|(_, value)| !value.finite())
.map_or(Ok(()), |(index, _)| {
Err(LinearLeastSquaresError::NonFiniteInput { argument, index })
})
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn native_to_user(constraints: &[VariableConstraint]) -> Vec<usize> {
constraints
.iter()
.enumerate()
.filter_map(|(index, constraint)| {
(*constraint == VariableConstraint::Free).then_some(index)
})
.chain(
constraints
.iter()
.enumerate()
.filter_map(|(index, constraint)| {
(*constraint == VariableConstraint::Nonnegative).then_some(index)
}),
)
.collect()
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn fill_augmented<T: Copy + Default>(
problem: &NonnegativeLeastSquaresProblem<'_, T>,
native_to_user: &[usize],
augmented: &mut [T],
mdw: usize,
me: usize,
ma: usize,
) {
if let (Some(equality), Some(rhs)) = (problem.equality_matrix, problem.equality_rhs) {
for (native_column, &user_column) in native_to_user.iter().enumerate() {
for row in 0..me {
augmented[row + native_column * mdw] =
*equality.get(row, user_column).expect("validated matrix");
}
}
for row in 0..me {
augmented[row + native_to_user.len() * mdw] = rhs[row];
}
}
for (native_column, &user_column) in native_to_user.iter().enumerate() {
for row in 0..ma {
augmented[me + row + native_column * mdw] = *problem
.least_squares_matrix
.get(row, user_column)
.expect("validated matrix");
}
}
for row in 0..ma {
augmented[me + row + native_to_user.len() * mdw] = problem.least_squares_rhs[row];
}
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn run_f64(
mut prepared: PreparedF64<'_>,
) -> Result<NonnegativeLeastSquaresResult<f64>, LinearLeastSquaresError> {
let n = prepared.native_to_user.len();
let mut solution_native = vec![0.0; n];
let mut work = vec![0.0; prepared.work];
let mut iwork = vec![0_i32; prepared.iwork];
iwork[0] = to_fortran_integer(prepared.work).map_err(|_| {
LinearLeastSquaresError::IntegerOverflow {
argument: "WORK length",
}
})?;
iwork[1] = to_fortran_integer(prepared.iwork).map_err(|_| {
LinearLeastSquaresError::IntegerOverflow {
argument: "IWORK length",
}
})?;
let mut mdw = to_fortran_integer(prepared.mdw)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: "MDW" })?;
let mut me = to_fortran_integer(prepared.me)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: "ME" })?;
let mut ma = to_fortran_integer(prepared.ma)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: "MA" })?;
let mut variables = to_fortran_integer(n)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: "N" })?;
let mut free = to_fortran_integer(prepared.free)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: "L" })?;
let mut options = [1.0];
let mut norm = 0.0;
let mut mode = 0;
let _guard = crate::runtime::lock_native();
unsafe {
slatec_sys::linear_least_squares::dwnnls(
prepared.augmented.as_mut_ptr(),
&mut mdw,
&mut me,
&mut ma,
&mut variables,
&mut free,
options.as_mut_ptr(),
solution_native.as_mut_ptr(),
&mut norm,
&mut mode,
iwork.as_mut_ptr(),
work.as_mut_ptr(),
)
};
finish_f64(
prepared.problem,
prepared.native_to_user,
solution_native,
norm,
mode,
)
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn run_f32(
mut prepared: PreparedF32<'_>,
) -> Result<NonnegativeLeastSquaresResult<f32>, LinearLeastSquaresError> {
let n = prepared.native_to_user.len();
let mut solution_native = vec![0.0_f32; n];
let mut work = vec![0.0_f32; prepared.work];
let mut iwork = vec![0_i32; prepared.iwork];
iwork[0] = to_fortran_integer(prepared.work).map_err(|_| {
LinearLeastSquaresError::IntegerOverflow {
argument: "WORK length",
}
})?;
iwork[1] = to_fortran_integer(prepared.iwork).map_err(|_| {
LinearLeastSquaresError::IntegerOverflow {
argument: "IWORK length",
}
})?;
let mut mdw = to_fortran_integer(prepared.mdw)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: "MDW" })?;
let mut me = to_fortran_integer(prepared.me)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: "ME" })?;
let mut ma = to_fortran_integer(prepared.ma)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: "MA" })?;
let mut variables = to_fortran_integer(n)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: "N" })?;
let mut free = to_fortran_integer(prepared.free)
.map_err(|_| LinearLeastSquaresError::IntegerOverflow { argument: "L" })?;
let mut options = [1.0_f32];
let mut norm = 0.0_f32;
let mut mode = 0;
let _guard = crate::runtime::lock_native();
unsafe {
slatec_sys::linear_least_squares::wnnls(
prepared.augmented.as_mut_ptr(),
&mut mdw,
&mut me,
&mut ma,
&mut variables,
&mut free,
options.as_mut_ptr(),
solution_native.as_mut_ptr(),
&mut norm,
&mut mode,
iwork.as_mut_ptr(),
work.as_mut_ptr(),
)
};
finish_f32(
prepared.problem,
prepared.native_to_user,
solution_native,
norm,
mode,
)
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn status(mode: FortranInteger) -> Result<NonnegativeLeastSquaresStatus, LinearLeastSquaresError> {
match mode {
0 => Ok(NonnegativeLeastSquaresStatus::Converged),
1 => Ok(NonnegativeLeastSquaresStatus::MaximumIterations),
2 => Err(LinearLeastSquaresError::NativeContractViolation {
detail: "WNNLS/DWNNLS returned MODE=2 after safe validation",
}),
value => Err(LinearLeastSquaresError::NativeStatus { status: value }),
}
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn finish_f64(
problem: NonnegativeLeastSquaresProblem<'_, f64>,
native_to_user: Vec<usize>,
native: Vec<f64>,
native_residual_norm: f64,
mode: i32,
) -> Result<NonnegativeLeastSquaresResult<f64>, LinearLeastSquaresError> {
let solution = restore(native_to_user, native);
let (least, equality) = norms_f64(problem, &solution);
Ok(NonnegativeLeastSquaresResult {
solution,
native_residual_norm,
least_squares_residual_norm: least,
equality_residual_norm: equality,
status: status(mode)?,
})
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn finish_f32(
problem: NonnegativeLeastSquaresProblem<'_, f32>,
native_to_user: Vec<usize>,
native: Vec<f32>,
native_residual_norm: f32,
mode: i32,
) -> Result<NonnegativeLeastSquaresResult<f32>, LinearLeastSquaresError> {
let solution = restore(native_to_user, native);
let (least, equality) = norms_f32(problem, &solution);
Ok(NonnegativeLeastSquaresResult {
solution,
native_residual_norm,
least_squares_residual_norm: least,
equality_residual_norm: equality,
status: status(mode)?,
})
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn restore<T: Default + Copy>(native_to_user: Vec<usize>, native: Vec<T>) -> Vec<T> {
let mut solution = vec![T::default(); native.len()];
for (native_index, user_index) in native_to_user.into_iter().enumerate() {
solution[user_index] = native[native_index];
}
solution
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn norms_f64(problem: NonnegativeLeastSquaresProblem<'_, f64>, solution: &[f64]) -> (f64, f64) {
let least = norm_f64(
problem.least_squares_matrix,
problem.least_squares_rhs,
solution,
);
let equality = match (problem.equality_matrix, problem.equality_rhs) {
(Some(matrix), Some(rhs)) => norm_f64(matrix, rhs, solution),
_ => 0.0,
};
(least, equality)
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn norms_f32(problem: NonnegativeLeastSquaresProblem<'_, f32>, solution: &[f32]) -> (f32, f32) {
let least = norm_f32(
problem.least_squares_matrix,
problem.least_squares_rhs,
solution,
);
let equality = match (problem.equality_matrix, problem.equality_rhs) {
(Some(matrix), Some(rhs)) => norm_f32(matrix, rhs, solution),
_ => 0.0,
};
(least, equality)
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn norm_f64(matrix: MatrixRef<'_, f64>, rhs: &[f64], solution: &[f64]) -> f64 {
let mut sum = 0.0;
for (row, &rhs_value) in rhs.iter().take(matrix.rows()).enumerate() {
let mut residual = -rhs_value;
for (column, &value) in solution.iter().take(matrix.columns()).enumerate() {
residual += *matrix.get(row, column).expect("validated") * value;
}
sum += residual * residual;
}
sum.sqrt()
}
#[cfg(feature = "least-squares-linear-nonnegative")]
fn norm_f32(matrix: MatrixRef<'_, f32>, rhs: &[f32], solution: &[f32]) -> f32 {
let mut sum = 0.0_f32;
for (row, &rhs_value) in rhs.iter().take(matrix.rows()).enumerate() {
let mut residual = -rhs_value;
for (column, &value) in solution.iter().take(matrix.columns()).enumerate() {
residual += *matrix.get(row, column).expect("validated") * value;
}
sum += residual * residual;
}
sum.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matrix_rejects_short_storage() {
assert!(matches!(
MatrixRef::<f64>::column_major(&[1.0], 2, 1, 2),
Err(LinearLeastSquaresError::DimensionMismatch { .. })
));
}
#[test]
#[cfg(feature = "least-squares-linear-nonnegative")]
fn transform_orders_equality_then_least_squares_and_permuted_columns() {
let a = MatrixRef::column_major(&[1.0_f64, 3.0, 2.0, 4.0], 2, 2, 2).unwrap();
let e = MatrixRef::column_major(&[5.0_f64, 6.0], 1, 2, 1).unwrap();
let p = NonnegativeLeastSquaresProblem {
least_squares_matrix: a,
least_squares_rhs: &[7.0, 8.0],
equality_matrix: Some(e),
equality_rhs: Some(&[9.0]),
variable_constraints: &[VariableConstraint::Nonnegative, VariableConstraint::Free],
};
let prepared = PreparedF64::new(p).unwrap();
assert_eq!(prepared.native_to_user, vec![1, 0]);
assert_eq!(
prepared.augmented,
vec![6.0, 2.0, 4.0, 5.0, 1.0, 3.0, 9.0, 7.0, 8.0]
);
}
#[test]
#[cfg(feature = "least-squares-linear-nonnegative")]
fn invalid_constraint_count_is_prechecked() {
let a = MatrixRef::column_major(&[1.0_f64], 1, 1, 1).unwrap();
assert!(matches!(
solve_nonnegative_least_squares(NonnegativeLeastSquaresProblem {
least_squares_matrix: a,
least_squares_rhs: &[1.0],
equality_matrix: None,
equality_rhs: None,
variable_constraints: &[]
}),
Err(LinearLeastSquaresError::DimensionMismatch { .. })
));
}
}