use alloc::{vec, vec::Vec};
use core::fmt;
use slatec_core::to_fortran_integer;
use slatec_sys::FortranInteger;
use crate::linear_least_squares::MatrixRef;
pub use crate::linear_least_squares::VariableBounds;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum BoundedLeastSquaresScaling {
#[default]
Nominal,
EuclideanColumns,
Identity,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct BoundedLeastSquaresOptions {
pub scaling: BoundedLeastSquaresScaling,
pub maximum_iterations: Option<usize>,
}
#[derive(Clone, Copy, Debug)]
pub struct BoundedLeastSquaresProblem<'a, T> {
pub matrix: MatrixRef<'a, T>,
pub rhs: &'a [T],
pub bounds: &'a [VariableBounds<T>],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BoundedLeastSquaresStatus {
Converged,
MaximumIterations,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BoundedLeastSquaresResult<T> {
pub solution: Vec<T>,
pub residual_norm: T,
pub status: BoundedLeastSquaresStatus,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BoundedLeastSquaresError {
EmptyVariables,
EmptyRows,
DimensionMismatch {
argument: &'static str,
},
NonFiniteInput {
argument: &'static str,
index: usize,
},
InconsistentBounds {
index: usize,
},
InvalidIterationLimit,
IntegerOverflow {
argument: &'static str,
},
WorkspaceOverflow,
NativeContractViolation {
detail: &'static str,
},
NativeStatus {
status: i32,
},
}
impl fmt::Display for BoundedLeastSquaresError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyVariables => write!(formatter, "bounded least squares needs variables"),
Self::EmptyRows => write!(formatter, "bounded least squares needs matrix rows"),
Self::DimensionMismatch { argument } => {
write!(
formatter,
"bounded least-squares dimension mismatch for {argument}"
)
}
Self::NonFiniteInput { argument, index } => write!(
formatter,
"bounded least-squares {argument} at index {index} must be finite"
),
Self::InconsistentBounds { index } => {
write!(
formatter,
"bounded least-squares bounds at index {index} are inconsistent"
)
}
Self::InvalidIterationLimit => {
write!(
formatter,
"bounded least-squares iteration limit must be positive"
)
}
Self::IntegerOverflow { argument } => write!(
formatter,
"bounded least-squares {argument} does not fit Fortran INTEGER"
),
Self::WorkspaceOverflow => {
write!(
formatter,
"bounded least-squares workspace arithmetic overflowed"
)
}
Self::NativeContractViolation { detail } => write!(
formatter,
"native bounded least-squares contract was violated: {detail}"
),
Self::NativeStatus { status } => {
write!(formatter, "unknown SBOLS/DBOLS MODE value {status}")
}
}
}
}
impl std::error::Error for BoundedLeastSquaresError {}
pub fn solve_bounded_least_squares(
problem: BoundedLeastSquaresProblem<'_, f64>,
options: BoundedLeastSquaresOptions,
) -> Result<BoundedLeastSquaresResult<f64>, BoundedLeastSquaresError> {
run_f64(prepare(problem, options)?)
}
pub fn solve_bounded_least_squares_f32(
problem: BoundedLeastSquaresProblem<'_, f32>,
options: BoundedLeastSquaresOptions,
) -> Result<BoundedLeastSquaresResult<f32>, BoundedLeastSquaresError> {
run_f32(prepare(problem, options)?)
}
struct Prepared<'a, T> {
augmented: Vec<T>,
lower: Vec<T>,
upper: Vec<T>,
bound_types: Vec<FortranInteger>,
options: Vec<FortranInteger>,
rows: usize,
variables: usize,
real_workspace: usize,
integer_workspace: usize,
_problem: BoundedLeastSquaresProblem<'a, T>,
}
fn prepare<T: Copy + Default + Finite + PartialOrd>(
problem: BoundedLeastSquaresProblem<'_, T>,
options: BoundedLeastSquaresOptions,
) -> Result<Prepared<'_, T>, BoundedLeastSquaresError> {
let rows = problem.matrix.rows();
let variables = problem.matrix.columns();
if rows == 0 {
return Err(BoundedLeastSquaresError::EmptyRows);
}
if variables == 0 {
return Err(BoundedLeastSquaresError::EmptyVariables);
}
if problem.rhs.len() != rows {
return Err(BoundedLeastSquaresError::DimensionMismatch { argument: "rhs" });
}
if problem.bounds.len() != variables {
return Err(BoundedLeastSquaresError::DimensionMismatch { argument: "bounds" });
}
finite_slice(problem.matrix.as_column_major_slice(), "matrix")?;
finite_slice(problem.rhs, "rhs")?;
let (lower, upper, bound_types) = encode_bounds(problem.bounds)?;
let columns = variables
.checked_add(1)
.ok_or(BoundedLeastSquaresError::WorkspaceOverflow)?;
let augmented_len = rows
.checked_mul(columns)
.ok_or(BoundedLeastSquaresError::WorkspaceOverflow)?;
let mut augmented = vec![T::default(); augmented_len];
for column in 0..variables {
for row in 0..rows {
augmented[row + column * rows] = *problem
.matrix
.get(row, column)
.expect("MatrixRef checked logical dimensions");
}
}
for row in 0..rows {
augmented[row + variables * rows] = problem.rhs[row];
}
let real_workspace = variables
.checked_mul(5)
.ok_or(BoundedLeastSquaresError::WorkspaceOverflow)?;
let integer_workspace = variables
.checked_mul(2)
.ok_or(BoundedLeastSquaresError::WorkspaceOverflow)?;
for (name, value) in [
("MDW", rows),
("MROWS", rows),
("NCOLS", variables),
("RW length", real_workspace),
("IW length", integer_workspace),
] {
to_fortran_integer(value)
.map_err(|_| BoundedLeastSquaresError::IntegerOverflow { argument: name })?;
}
let option_array = option_array(options)?;
Ok(Prepared {
augmented,
lower,
upper,
bound_types,
options: option_array,
rows,
variables,
real_workspace,
integer_workspace,
_problem: problem,
})
}
fn option_array(
options: BoundedLeastSquaresOptions,
) -> Result<Vec<FortranInteger>, BoundedLeastSquaresError> {
let scaling = match options.scaling {
BoundedLeastSquaresScaling::Nominal => None,
BoundedLeastSquaresScaling::EuclideanColumns => Some(2),
BoundedLeastSquaresScaling::Identity => Some(3),
};
let iterations = match options.maximum_iterations {
Some(value) => {
if value == 0 {
return Err(BoundedLeastSquaresError::InvalidIterationLimit);
}
Some(to_fortran_integer(value).map_err(|_| {
BoundedLeastSquaresError::IntegerOverflow {
argument: "maximum_iterations",
}
})?)
}
None => None,
};
Ok(match (scaling, iterations) {
(None, None) => vec![99],
(Some(scale), None) => vec![3, scale, 99],
(None, Some(limit)) => vec![5, 4, 99, 4, limit, 99],
(Some(scale), Some(limit)) => vec![3, scale, 5, 6, 99, 4, limit, 99],
})
}
type EncodedBounds<T> = (Vec<T>, Vec<T>, Vec<FortranInteger>);
fn encode_bounds<T: Copy + Default + Finite + PartialOrd>(
bounds: &[VariableBounds<T>],
) -> Result<EncodedBounds<T>, BoundedLeastSquaresError> {
let mut lower = vec![T::default(); bounds.len()];
let mut upper = vec![T::default(); bounds.len()];
let mut types = vec![0; bounds.len()];
for (index, bound) in bounds.iter().copied().enumerate() {
match bound {
VariableBounds::Unbounded => types[index] = 4,
VariableBounds::Lower(value) => {
finite(value, "lower bound", index)?;
lower[index] = value;
types[index] = 1;
}
VariableBounds::Upper(value) => {
finite(value, "upper bound", index)?;
upper[index] = value;
types[index] = 2;
}
VariableBounds::Between {
lower: lo,
upper: hi,
} => {
finite(lo, "lower bound", index)?;
finite(hi, "upper bound", index)?;
if lo > hi {
return Err(BoundedLeastSquaresError::InconsistentBounds { index });
}
lower[index] = lo;
upper[index] = hi;
types[index] = 3;
}
VariableBounds::Fixed(value) => {
finite(value, "fixed bound", index)?;
lower[index] = value;
upper[index] = value;
types[index] = 3;
}
}
}
Ok((lower, upper, types))
}
fn run_f64(
mut prepared: Prepared<'_, f64>,
) -> Result<BoundedLeastSquaresResult<f64>, BoundedLeastSquaresError> {
let mut solution = vec![0.0; prepared.variables];
let mut real_workspace = vec![0.0; prepared.real_workspace];
let mut integer_workspace = vec![0; prepared.integer_workspace];
let mut leading_dimension = native_integer("MDW", prepared.rows)?;
let mut rows = native_integer("MROWS", prepared.rows)?;
let mut variables = native_integer("NCOLS", prepared.variables)?;
let mut norm = 0.0;
let mut mode = 0;
let _lock = crate::runtime::lock_native();
let _errors = crate::runtime::permit_recoverable_native_statuses();
unsafe {
slatec_sys::linear_least_squares::dbols(
prepared.augmented.as_mut_ptr(),
&mut leading_dimension,
&mut rows,
&mut variables,
prepared.lower.as_mut_ptr(),
prepared.upper.as_mut_ptr(),
prepared.bound_types.as_mut_ptr(),
prepared.options.as_mut_ptr(),
solution.as_mut_ptr(),
&mut norm,
&mut mode,
real_workspace.as_mut_ptr(),
integer_workspace.as_mut_ptr(),
)
};
finish(solution, norm, mode)
}
fn run_f32(
mut prepared: Prepared<'_, f32>,
) -> Result<BoundedLeastSquaresResult<f32>, BoundedLeastSquaresError> {
let mut solution = vec![0.0_f32; prepared.variables];
let mut real_workspace = vec![0.0_f32; prepared.real_workspace];
let mut integer_workspace = vec![0; prepared.integer_workspace];
let mut leading_dimension = native_integer("MDW", prepared.rows)?;
let mut rows = native_integer("MROWS", prepared.rows)?;
let mut variables = native_integer("NCOLS", prepared.variables)?;
let mut norm = 0.0_f32;
let mut mode = 0;
let _lock = crate::runtime::lock_native();
let _errors = crate::runtime::permit_recoverable_native_statuses();
unsafe {
slatec_sys::linear_least_squares::sbols(
prepared.augmented.as_mut_ptr(),
&mut leading_dimension,
&mut rows,
&mut variables,
prepared.lower.as_mut_ptr(),
prepared.upper.as_mut_ptr(),
prepared.bound_types.as_mut_ptr(),
prepared.options.as_mut_ptr(),
solution.as_mut_ptr(),
&mut norm,
&mut mode,
real_workspace.as_mut_ptr(),
integer_workspace.as_mut_ptr(),
)
};
finish(solution, norm, mode)
}
fn native_integer(
argument: &'static str,
value: usize,
) -> Result<FortranInteger, BoundedLeastSquaresError> {
to_fortran_integer(value).map_err(|_| BoundedLeastSquaresError::IntegerOverflow { argument })
}
fn finish<T: Copy + Finite>(
solution: Vec<T>,
residual_norm: T,
mode: FortranInteger,
) -> Result<BoundedLeastSquaresResult<T>, BoundedLeastSquaresError> {
finite_slice(&solution, "native solution")?;
finite(residual_norm, "native residual norm", 0)?;
let status = match mode {
value if value >= 0 => BoundedLeastSquaresStatus::Converged,
-22 => BoundedLeastSquaresStatus::MaximumIterations,
value @ (-38..=-2) => {
return Err(BoundedLeastSquaresError::NativeContractViolation {
detail: native_contract_detail(value),
});
}
value => return Err(BoundedLeastSquaresError::NativeStatus { status: value }),
};
Ok(BoundedLeastSquaresResult {
solution,
residual_norm,
status,
})
}
fn native_contract_detail(mode: FortranInteger) -> &'static str {
match mode {
-17..=-2 => "SBOLS/DBOLS rejected safe dimensions, bounds, or option storage",
-38..=-23 => {
"SBOLSM/DBOLSM rejected an internally constructed option or workspace contract"
}
_ => "SBOLS/DBOLS returned an unexpected contract status",
}
}
trait Finite {
fn finite(self) -> bool;
}
impl Finite for f64 {
fn finite(self) -> bool {
self.is_finite()
}
}
impl Finite for f32 {
fn finite(self) -> bool {
self.is_finite()
}
}
fn finite<T: Finite>(
value: T,
argument: &'static str,
index: usize,
) -> Result<(), BoundedLeastSquaresError> {
if value.finite() {
Ok(())
} else {
Err(BoundedLeastSquaresError::NonFiniteInput { argument, index })
}
}
fn finite_slice<T: Copy + Finite>(
data: &[T],
argument: &'static str,
) -> Result<(), BoundedLeastSquaresError> {
data.iter()
.copied()
.enumerate()
.find(|(_, value)| !value.finite())
.map_or(Ok(()), |(index, _)| {
Err(BoundedLeastSquaresError::NonFiniteInput { argument, index })
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_every_bound_variant() {
let (lower, upper, kinds) = encode_bounds(&[
VariableBounds::Unbounded,
VariableBounds::Lower(1.0_f64),
VariableBounds::Upper(2.0),
VariableBounds::Between {
lower: -1.0,
upper: 3.0,
},
VariableBounds::Fixed(4.0),
])
.unwrap();
assert_eq!(kinds, vec![4, 1, 2, 3, 3]);
assert_eq!(lower, vec![0.0, 1.0, 0.0, -1.0, 4.0]);
assert_eq!(upper, vec![0.0, 0.0, 2.0, 3.0, 4.0]);
}
#[test]
fn options_keep_raw_protocol_private() {
assert_eq!(
option_array(BoundedLeastSquaresOptions::default()).unwrap(),
vec![99]
);
assert_eq!(
option_array(BoundedLeastSquaresOptions {
scaling: BoundedLeastSquaresScaling::Identity,
maximum_iterations: Some(7),
})
.unwrap(),
vec![3, 3, 5, 6, 99, 4, 7, 99]
);
assert!(matches!(
option_array(BoundedLeastSquaresOptions {
scaling: BoundedLeastSquaresScaling::Nominal,
maximum_iterations: Some(0),
}),
Err(BoundedLeastSquaresError::InvalidIterationLimit)
));
}
#[test]
fn augmented_storage_is_owned_column_major() {
let matrix = MatrixRef::column_major(&[1.0_f64, 3.0, 2.0, 4.0], 2, 2, 2).unwrap();
let prepared = prepare(
BoundedLeastSquaresProblem {
matrix,
rhs: &[5.0, 6.0],
bounds: &[VariableBounds::Unbounded, VariableBounds::Lower(0.0)],
},
BoundedLeastSquaresOptions::default(),
)
.unwrap();
assert_eq!(prepared.augmented, vec![1.0, 3.0, 2.0, 4.0, 5.0, 6.0]);
}
#[test]
fn inconsistent_interval_is_rejected_before_native_execution() {
assert!(matches!(
encode_bounds(&[VariableBounds::<f64>::Between {
lower: 2.0,
upper: 1.0,
}]),
Err(BoundedLeastSquaresError::InconsistentBounds { index: 0 })
));
}
}