use crate::data::{MatrixView, MatrixViewMut};
use crate::fit::{Fit2D, FitReport};
use crate::two_d::polynomial::{ModPoly2DParams, modpoly_into};
use crate::two_d::whittaker::{
Asls2DParams, Whittaker2DWorkspace, asls as asls_2d, solve_fixed_weighted_system,
};
use crate::whittaker::{
AslsParams as Asls1DParams, WhittakerParams as Whittaker1DParams,
WhittakerWorkspace as Whittaker1DWorkspace, asls_into as asls_1d_into,
};
use crate::{BaselineError, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AdaptiveMinmax2DParams {
pub order: usize,
pub max_iter: usize,
pub tol: f64,
}
impl Default for AdaptiveMinmax2DParams {
fn default() -> Self {
Self {
order: 2,
max_iter: 20,
tol: 1.0e-3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct IndividualAxes2DParams {
pub asls: Asls1DParams,
}
impl Default for IndividualAxes2DParams {
fn default() -> Self {
Self {
asls: Asls1DParams {
whittaker: Whittaker1DParams {
lambda: 1.0e4,
..Whittaker1DParams::default()
},
p: 0.01,
},
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct CollabPls2DParams {
pub asls: Asls2DParams,
}
pub fn adaptive_minmax(input: MatrixView<'_>, params: AdaptiveMinmax2DParams) -> Result<Fit2D> {
let mut baseline = vec![0.0; input.len()];
let output = MatrixViewMut::row_major(&mut baseline, input.rows(), input.cols())?;
let report = adaptive_minmax_into(input, params, output)?;
Fit2D::new(baseline, input.rows(), input.cols(), report)
}
pub fn adaptive_minmax_into(
input: MatrixView<'_>,
params: AdaptiveMinmax2DParams,
output: MatrixViewMut<'_>,
) -> Result<FitReport> {
validate_iter_params(params.max_iter, params.tol)?;
modpoly_into(
input,
ModPoly2DParams {
order: params.order,
max_iter: params.max_iter,
tol: params.tol,
},
output,
)
}
pub fn individual_axes(input: MatrixView<'_>, params: IndividualAxes2DParams) -> Result<Fit2D> {
let mut baseline = vec![0.0; input.len()];
let output = MatrixViewMut::row_major(&mut baseline, input.rows(), input.cols())?;
let report = individual_axes_into(input, params, output)?;
Fit2D::new(baseline, input.rows(), input.cols(), report)
}
pub fn individual_axes_into(
input: MatrixView<'_>,
params: IndividualAxes2DParams,
mut output: MatrixViewMut<'_>,
) -> Result<FitReport> {
validate_individual_axes_input(input, &output, params)?;
let rows = input.rows();
let cols = input.cols();
let mut workspace = Whittaker1DWorkspace::new(cols.max(rows));
output.as_mut_slice().fill(0.0);
let mut column_input = vec![0.0; rows];
let mut column_output = vec![0.0; rows];
for col in 0..cols {
for (row, value) in column_input.iter_mut().enumerate() {
let index = row * cols + col;
*value = input.as_slice()[index] - output.as_slice()[index];
}
asls_1d_into(
&column_input,
params.asls,
&mut column_output,
&mut workspace,
)?;
for (row, value) in column_output.iter().enumerate() {
output.as_mut_slice()[row * cols + col] += *value;
}
}
let mut row_input = vec![0.0; cols];
let mut row_output = vec![0.0; cols];
for row in 0..rows {
let start = row * cols;
for (col, value) in row_input.iter_mut().enumerate() {
let index = start + col;
*value = input.as_slice()[index] - output.as_slice()[index];
}
asls_1d_into(&row_input, params.asls, &mut row_output, &mut workspace)?;
for (col, value) in row_output.iter().enumerate() {
output.as_mut_slice()[start + col] += *value;
}
}
Ok(FitReport::new(
params.asls.whittaker.max_iter * 2,
true,
0.0,
))
}
pub fn collab_pls(surfaces: &[MatrixView<'_>], params: CollabPls2DParams) -> Result<Vec<Fit2D>> {
validate_surfaces(surfaces)?;
let rows = surfaces[0].rows();
let cols = surfaces[0].cols();
let len = surfaces[0].len();
let mut average = vec![0.0; len];
for surface in surfaces {
for (target, value) in average.iter_mut().zip(surface.as_slice()) {
*target += value;
}
}
let scale = 1.0 / surfaces.len() as f64;
for value in &mut average {
*value *= scale;
}
let average_view = MatrixView::row_major(&average, rows, cols)?;
let shared_fit = asls_2d(average_view, params.asls)?;
let weights = asls_weights(&average, &shared_fit.baseline, params.asls.p);
let mut workspace = Whittaker2DWorkspace::new(len);
surfaces
.iter()
.map(|surface| {
let mut baseline = vec![0.0; len];
let output = MatrixViewMut::row_major(&mut baseline, rows, cols)?;
let report = solve_fixed_weighted_system(
*surface,
params.asls.whittaker,
&weights,
output,
&mut workspace,
)?;
Fit2D::new(baseline, rows, cols, report)
})
.collect()
}
fn validate_iter_params(max_iter: usize, tol: f64) -> Result<()> {
if max_iter == 0 {
return Err(BaselineError::InvalidParameter {
name: "max_iter",
reason: "must be greater than zero",
});
}
if !tol.is_finite() || tol <= 0.0 {
return Err(BaselineError::InvalidParameter {
name: "tol",
reason: "must be finite and positive",
});
}
Ok(())
}
fn validate_individual_axes_input(
input: MatrixView<'_>,
output: &MatrixViewMut<'_>,
params: IndividualAxes2DParams,
) -> Result<()> {
params.asls.validate()?;
if input.shape() != output.shape() {
return Err(BaselineError::LengthMismatch {
name: "output",
expected: input.len(),
actual: output.len(),
});
}
if input.rows() < 3 || input.cols() < 3 {
return Err(BaselineError::TooShort {
algorithm: "individual_axes",
len: input.len(),
min: 9,
});
}
Ok(())
}
fn validate_surfaces(surfaces: &[MatrixView<'_>]) -> Result<()> {
if surfaces.is_empty() {
return Err(BaselineError::EmptyInput);
}
let shape = surfaces[0].shape();
for surface in surfaces {
if surface.shape() != shape {
return Err(BaselineError::LengthMismatch {
name: "surface",
expected: shape.len(),
actual: surface.len(),
});
}
}
Ok(())
}
fn asls_weights(data: &[f64], baseline: &[f64], p: f64) -> Vec<f64> {
data.iter()
.zip(baseline)
.map(
|(observed, fitted)| {
if observed > fitted { p } else { 1.0 - p }
},
)
.collect()
}
#[cfg(test)]
mod tests {
use super::{IndividualAxes2DParams, individual_axes};
use crate::MatrixView;
#[test]
fn individual_axes_preserves_constant_surface() {
let data = vec![2.0; 30];
let input = MatrixView::row_major(&data, 5, 6).unwrap();
let fit = individual_axes(input, IndividualAxes2DParams::default()).unwrap();
assert!(fit.baseline.iter().all(|value| (*value - 2.0).abs() < 1e-6));
}
}