use scirs2_core::ndarray::{s, Array1, Array2, Axis};
use std::f64::consts::PI;
use crate::error::{NdimageError, NdimageResult};
#[derive(Debug, Clone)]
pub struct HyperspectralImage {
pub data: Array2<f64>,
pub wavelengths: Option<Array1<f64>>,
}
impl HyperspectralImage {
pub fn new(data: Array2<f64>) -> Self {
Self { data, wavelengths: None }
}
pub fn with_wavelengths(data: Array2<f64>, wavelengths: Array1<f64>) -> NdimageResult<Self> {
let n_bands = data.ncols();
if wavelengths.len() != n_bands {
return Err(NdimageError::InvalidInput(format!(
"wavelengths length {} != n_bands {}",
wavelengths.len(),
n_bands
)));
}
Ok(Self { data, wavelengths: Some(wavelengths) })
}
#[inline]
pub fn n_pixels(&self) -> usize {
self.data.nrows()
}
#[inline]
pub fn n_bands(&self) -> usize {
self.data.ncols()
}
pub fn mean_spectrum(&self) -> Array1<f64> {
self.data.mean_axis(Axis(0)).unwrap_or_else(|| Array1::zeros(self.n_bands()))
}
}
#[inline]
fn norm_sq(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum()
}
#[inline]
fn norm(v: &[f64]) -> f64 {
norm_sq(v).sqrt()
}
#[inline]
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn project_out(v: &Array1<f64>, basis: &Array2<f64>, n_basis: usize) -> Array1<f64> {
let mut result = v.clone();
for k in 0..n_basis {
let col = basis.column(k);
let d = dot(result.as_slice().unwrap_or(&[]), col.to_owned().as_slice().unwrap_or(&[]));
result = result - col.to_owned() * d;
}
result
}
fn thin_svd(a: &Array2<f64>, rank: usize) -> NdimageResult<(Array2<f64>, Array1<f64>, Array2<f64>)> {
let (m, n) = (a.nrows(), a.ncols());
let r = rank.min(m).min(n);
let ata = a.t().dot(a);
let mut q = random_orthonormal_matrix(n, r);
for _ in 0..20 {
let z = ata.dot(&q);
q = qr_factorization(&z, r)?;
}
let b = a.dot(&q);
let mut u = Array2::<f64>::zeros((m, r));
let mut s = Array1::<f64>::zeros(r);
let mut vt = Array2::<f64>::zeros((r, n));
let mut b_ortho = b.clone();
for k in 0..r {
let col: Array1<f64> = b_ortho.column(k).to_owned();
let n_col = norm(col.as_slice().unwrap_or(&[]));
if n_col < 1e-14 {
break;
}
let u_col = &col / n_col;
for j in 0..m {
u[[j, k]] = u_col[j];
}
s[k] = n_col;
for j in (k + 1)..r {
let c: Array1<f64> = b_ortho.column(j).to_owned();
let proj = dot(c.as_slice().unwrap_or(&[]), u_col.as_slice().unwrap_or(&[]));
for i in 0..m {
b_ortho[[i, j]] -= proj * u_col[i];
}
}
let q_col: Array1<f64> = q.column(k).to_owned();
for j in 0..n {
vt[[k, j]] = q_col[j];
}
}
Ok((u, s, vt))
}
fn random_orthonormal_matrix(n: usize, r: usize) -> Array2<f64> {
let mut m = Array2::<f64>::zeros((n, r));
let mut state: u64 = 0x5EED_CAFE_DEAD_BEEF;
for i in 0..n {
for j in 0..r {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let f = ((state >> 11) as f64) / (1u64 << 53) as f64 - 0.5;
m[[i, j]] = f;
}
}
qr_factorization(&m, r).unwrap_or(m)
}
fn qr_factorization(a: &Array2<f64>, r: usize) -> NdimageResult<Array2<f64>> {
let m = a.nrows();
let cols = r.min(a.ncols());
let mut q = Array2::<f64>::zeros((m, cols));
for k in 0..cols {
let mut col: Array1<f64> = a.column(k).to_owned();
for j in 0..k {
let q_j = q.column(j).to_owned();
let p = dot(col.as_slice().unwrap_or(&[]), q_j.as_slice().unwrap_or(&[]));
col = col - q_j * p;
}
let n = norm(col.as_slice().unwrap_or(&[]));
if n < 1e-14 {
if k < m {
q[[k, k]] = 1.0;
}
continue;
}
let unit = col / n;
for i in 0..m {
q[[i, k]] = unit[i];
}
}
Ok(q)
}
fn lstsq(a: &Array2<f64>, b: &Array1<f64>) -> NdimageResult<Array1<f64>> {
let ata = a.t().dot(a); let atb = a.t().dot(b); cholesky_solve(&ata, &atb)
}
fn cholesky_solve(a: &Array2<f64>, b: &Array1<f64>) -> NdimageResult<Array1<f64>> {
let n = a.nrows();
if n != a.ncols() || n != b.len() {
return Err(NdimageError::InvalidInput("cholesky_solve: dimension mismatch".into()));
}
let reg = 1e-10 * a.diag().iter().cloned().fold(f64::NEG_INFINITY, f64::max).max(1e-10);
let mut l = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..=i {
let mut s: f64 = a[[i, j]];
for k in 0..j {
s -= l[[i, k]] * l[[j, k]];
}
if i == j {
let diag = s + reg;
if diag <= 0.0 {
return Err(NdimageError::ComputationError("matrix not positive definite".into()));
}
l[[i, j]] = diag.sqrt();
} else if l[[j, j]].abs() > 1e-15 {
l[[i, j]] = s / l[[j, j]];
}
}
}
let mut y = Array1::<f64>::zeros(n);
for i in 0..n {
let mut s = b[i];
for k in 0..i {
s -= l[[i, k]] * y[k];
}
if l[[i, i]].abs() > 1e-15 {
y[i] = s / l[[i, i]];
}
}
let mut x = Array1::<f64>::zeros(n);
for i in (0..n).rev() {
let mut s = y[i];
for k in (i + 1)..n {
s -= l[[k, i]] * x[k];
}
if l[[i, i]].abs() > 1e-15 {
x[i] = s / l[[i, i]];
}
}
Ok(x)
}
fn lstsq_multi(a: &Array2<f64>, b_mat: &Array2<f64>) -> NdimageResult<Array2<f64>> {
let p = a.ncols();
let n_pix = b_mat.ncols();
let mut x = Array2::<f64>::zeros((p, n_pix));
let ata = a.t().dot(a);
for i in 0..n_pix {
let b_col = b_mat.column(i).to_owned();
let atb = a.t().dot(&b_col);
let xi = cholesky_solve(&ata, &atb)?;
for j in 0..p {
x[[j, i]] = xi[j];
}
}
Ok(x)
}
fn simplex_volume(e: &Array2<f64>) -> f64 {
let (p, k) = (e.nrows(), e.ncols());
if k == 0 || p == 0 {
return 0.0;
}
let e0 = e.column(0).to_owned();
let dim = k - 1;
if dim == 0 {
return 1.0;
}
let mut diff = Array2::<f64>::zeros((p, dim));
for j in 1..k {
for i in 0..p {
diff[[i, j - 1]] = e[[i, j]] - e0[i];
}
}
let gram = diff.t().dot(&diff); gram_determinant(&gram).abs().sqrt()
}
fn gram_determinant(a: &Array2<f64>) -> f64 {
let n = a.nrows();
let mut lu = a.clone();
let mut sign = 1.0_f64;
for col in 0..n {
let mut max_val = lu[[col, col]].abs();
let mut max_row = col;
for row in (col + 1)..n {
if lu[[row, col]].abs() > max_val {
max_val = lu[[row, col]].abs();
max_row = row;
}
}
if max_row != col {
for j in 0..n {
let tmp = lu[[col, j]];
lu[[col, j]] = lu[[max_row, j]];
lu[[max_row, j]] = tmp;
}
sign = -sign;
}
if lu[[col, col]].abs() < 1e-14 {
return 0.0;
}
for row in (col + 1)..n {
let factor = lu[[row, col]] / lu[[col, col]];
for j in col..n {
let val = lu[[col, j]] * factor;
lu[[row, j]] -= val;
}
}
}
let mut det = sign;
for i in 0..n {
det *= lu[[i, i]];
}
det
}
pub fn vertex_component_analysis(
image: &HyperspectralImage,
n_endmembers: usize,
) -> NdimageResult<Array2<f64>> {
let n_pixels = image.n_pixels();
let n_bands = image.n_bands();
if n_endmembers == 0 || n_endmembers > n_pixels {
return Err(NdimageError::InvalidInput(format!(
"n_endmembers must be in 1..=n_pixels, got {}",
n_endmembers
)));
}
let data = &image.data;
let mean = image.mean_spectrum();
let centered = data - &mean.view().insert_axis(Axis(0));
let rank = n_endmembers.min(n_bands).min(n_pixels);
let (_, _s, vt) = thin_svd(¢ered, rank)?;
let projected = centered.dot(&vt.t());
let p = n_endmembers;
let r = projected.ncols();
let dim = r.min(p);
let mut r_mat = Array2::<f64>::zeros((p, n_pixels));
for i in 0..n_pixels {
for j in 0..dim {
r_mat[[j, i]] = projected[[i, j]];
}
r_mat[[p - 1, i]] = 1.0;
}
let mut endmember_cols = Array2::<f64>::zeros((n_bands, p));
let mut a = Array2::<f64>::zeros((p, p));
let init_idx = n_pixels / 2;
for k in 0..p {
a[[k, 0]] = r_mat[[k, init_idx]];
}
let mut selected_indices = vec![0usize; p];
for i in 0..p {
let a_sub = a.slice(s![.., 0..i.max(1)]).to_owned();
let a_orth = qr_factorization(&a_sub, i.max(1))?;
let mut max_val = -1.0_f64;
let mut best_idx = 0usize;
for j in 0..n_pixels {
let col = r_mat.column(j).to_owned();
let proj = project_out(&col, &a_orth, i);
let v = norm(proj.as_slice().unwrap_or(&[]));
if v > max_val {
max_val = v;
best_idx = j;
}
}
selected_indices[i] = best_idx;
let pixel_col = r_mat.column(best_idx).to_owned();
for k in 0..p {
a[[k, i]] = pixel_col[k];
}
let spectrum = data.row(best_idx).to_owned();
for k in 0..n_bands {
endmember_cols[[k, i]] = spectrum[k];
}
}
Ok(endmember_cols)
}
pub fn nfindr(
image: &HyperspectralImage,
n_endmembers: usize,
max_iter: usize,
n_restarts: usize,
) -> NdimageResult<Array2<f64>> {
let n_pixels = image.n_pixels();
let n_bands = image.n_bands();
if n_endmembers < 2 {
return Err(NdimageError::InvalidInput("n_endmembers must be >= 2 for N-FINDR".into()));
}
if n_endmembers > n_pixels {
return Err(NdimageError::InvalidInput(format!(
"n_endmembers {} > n_pixels {}",
n_endmembers, n_pixels
)));
}
let data = &image.data;
let p = n_endmembers;
let reduce_dim = (p - 1).min(n_bands);
let mean = image.mean_spectrum();
let centered = data - &mean.view().insert_axis(Axis(0));
let (_u, _s, vt) = thin_svd(¢ered, reduce_dim)?; let projected = centered.dot(&vt.t());
let build_e_matrix = |indices: &[usize]| -> Array2<f64> {
let mut e = Array2::<f64>::zeros((reduce_dim, p));
for (j, &idx) in indices.iter().enumerate() {
for k in 0..reduce_dim {
e[[k, j]] = projected[[idx, k]];
}
}
e
};
let mut best_vol = -1.0_f64;
let mut best_indices = vec![0usize; p];
let mut lcg_state: u64 = 0xDEAD_BEEF_1337_CAFE;
let lcg_next = |state: &mut u64| -> usize {
*state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
((*state >> 33) as usize) % n_pixels
};
for _restart in 0..n_restarts.max(1) {
let mut indices: Vec<usize> = if _restart == 0 {
(0..p).map(|i| (i * n_pixels / p)).collect()
} else {
let mut idx_set: Vec<usize> = Vec::with_capacity(p);
while idx_set.len() < p {
let candidate = lcg_next(&mut lcg_state);
if !idx_set.contains(&candidate) {
idx_set.push(candidate);
}
}
idx_set
};
let mut current_vol = simplex_volume(&build_e_matrix(&indices));
for _iter in 0..max_iter {
let mut improved = false;
for i in 0..p {
let old_idx = indices[i];
for j in 0..n_pixels {
if indices.contains(&j) && j != old_idx {
continue;
}
indices[i] = j;
let vol = simplex_volume(&build_e_matrix(&indices));
if vol > current_vol {
current_vol = vol;
improved = true;
} else {
indices[i] = old_idx;
}
}
}
if !improved {
break;
}
}
if current_vol > best_vol {
best_vol = current_vol;
best_indices.clone_from(&indices);
}
}
let mut endmembers = Array2::<f64>::zeros((n_bands, p));
for (j, &idx) in best_indices.iter().enumerate() {
let spectrum = data.row(idx).to_owned();
for k in 0..n_bands {
endmembers[[k, j]] = spectrum[k];
}
}
Ok(endmembers)
}
pub fn sisal(
image: &HyperspectralImage,
n_endmembers: usize,
tau: f64,
max_iter: usize,
) -> NdimageResult<Array2<f64>> {
let n_pixels = image.n_pixels();
let n_bands = image.n_bands();
if n_endmembers < 2 {
return Err(NdimageError::InvalidInput("n_endmembers must be >= 2".into()));
}
if tau <= 0.0 {
return Err(NdimageError::InvalidInput("tau must be positive".into()));
}
let p = n_endmembers;
let data = &image.data;
let dim = (p - 1).min(n_bands);
let mean = image.mean_spectrum();
let centered = data - &mean.view().insert_axis(Axis(0));
let (_u, _s, vt) = thin_svd(¢ered, dim)?;
let projected = centered.dot(&vt.t());
let init_img = HyperspectralImage::new(projected.clone());
let e_init = vertex_component_analysis(&init_img, p)?;
let y = projected.t().to_owned();
let mut e = e_init.clone();
let mu = tau / (p as f64);
for _iter in 0..max_iter {
let ete = e.t().dot(&e); let ety = e.t().dot(&y);
let mut a = Array2::<f64>::zeros((p, n_pixels));
for i in 0..n_pixels {
let rhs = ety.column(i).to_owned();
match cholesky_solve(&ete, &rhs) {
Ok(ai) => {
for j in 0..p {
a[[j, i]] = ai[j];
}
}
Err(_) => {
let s: f64 = rhs.iter().map(|x| x.abs()).sum::<f64>().max(1e-10);
for j in 0..p {
a[[j, i]] = rhs[j] / s;
}
}
}
}
let residual = &y - &e.dot(&a); let grad_data = -residual.dot(&a.t());
let ata = e.t().dot(&e); let pinv_e = match cholesky_solve(&ata, &Array1::from_vec(vec![1.0; p])) {
Ok(_) => {
let mut pi = Array2::<f64>::zeros((dim, p));
for k in 0..p {
let mut ek = Array1::<f64>::zeros(p);
ek[k] = 1.0;
if let Ok(sol) = cholesky_solve(&ata, &ek) {
let ep_col = e.dot(&sol); for i in 0..dim {
pi[[i, k]] = ep_col[i];
}
}
}
pi
}
Err(_) => Array2::<f64>::zeros((dim, p)),
};
let step = 1e-3 / (1.0 + _iter as f64 * 0.1);
e = e - (grad_data + &pinv_e * mu) * step;
}
let e_original_t = e.t().dot(&vt); let mut endmembers = Array2::<f64>::zeros((n_bands, p));
for j in 0..p {
for k in 0..n_bands {
endmembers[[k, j]] = e_original_t[[j, k]] + mean[k];
}
}
Ok(endmembers)
}
pub fn abundance_estimation_ucls(
image: &HyperspectralImage,
endmembers: &Array2<f64>,
) -> NdimageResult<Array2<f64>> {
let n_pixels = image.n_pixels();
let n_bands = image.n_bands();
let p = endmembers.ncols();
if endmembers.nrows() != n_bands {
return Err(NdimageError::InvalidInput(format!(
"endmembers.nrows() {} != n_bands {}",
endmembers.nrows(),
n_bands
)));
}
let e = endmembers; let data_t = image.data.t().to_owned();
let ete = e.t().dot(e); let mut abundances = Array2::<f64>::zeros((n_pixels, p));
for i in 0..n_pixels {
let y_i = data_t.column(i).to_owned();
let ety_i = e.t().dot(&y_i); let a_i = cholesky_solve(&ete, &ety_i)?;
for j in 0..p {
abundances[[i, j]] = a_i[j];
}
}
Ok(abundances)
}
pub fn abundance_estimation_ncls(
image: &HyperspectralImage,
endmembers: &Array2<f64>,
) -> NdimageResult<Array2<f64>> {
let n_pixels = image.n_pixels();
let n_bands = image.n_bands();
let p = endmembers.ncols();
if endmembers.nrows() != n_bands {
return Err(NdimageError::InvalidInput(format!(
"endmembers.nrows() {} != n_bands {}",
endmembers.nrows(),
n_bands
)));
}
let e = endmembers;
let ete = e.t().dot(e);
let mut abundances = Array2::<f64>::zeros((n_pixels, p));
for i in 0..n_pixels {
let y_i = image.data.row(i).to_owned();
let ety_i = e.t().dot(&y_i);
let a_i = nnls_active_set(&ete, &ety_i, 200)?;
for j in 0..p {
abundances[[i, j]] = a_i[j];
}
}
Ok(abundances)
}
fn nnls_active_set(ata: &Array2<f64>, atb: &Array1<f64>, max_iter: usize) -> NdimageResult<Array1<f64>> {
let p = ata.nrows();
let mut x = Array1::<f64>::zeros(p);
let mut passive = vec![false; p];
for _outer in 0..max_iter {
let g = ata.dot(&x) - atb;
let mut t_idx = p; let mut t_val = -1e-10_f64;
for j in 0..p {
if !passive[j] && -g[j] > t_val {
t_val = -g[j];
t_idx = j;
}
}
if t_idx == p {
break; }
passive[t_idx] = true;
loop {
let passive_indices: Vec<usize> = (0..p).filter(|&j| passive[j]).collect();
let k = passive_indices.len();
let mut ata_p = Array2::<f64>::zeros((k, k));
let mut atb_p = Array1::<f64>::zeros(k);
for (a, &pa) in passive_indices.iter().enumerate() {
atb_p[a] = atb[pa];
for (b, &pb) in passive_indices.iter().enumerate() {
ata_p[[a, b]] = ata[[pa, pb]];
}
}
let s_p = cholesky_solve(&ata_p, &atb_p)?;
if s_p.iter().all(|&v| v > 0.0) {
for (a, &pa) in passive_indices.iter().enumerate() {
x[pa] = s_p[a];
}
break;
}
let mut alpha = f64::INFINITY;
for (a, &pa) in passive_indices.iter().enumerate() {
if s_p[a] <= 0.0 {
let ratio = x[pa] / (x[pa] - s_p[a]);
if ratio < alpha {
alpha = ratio;
}
}
}
for (a, &pa) in passive_indices.iter().enumerate() {
x[pa] += alpha * (s_p[a] - x[pa]);
if x[pa] < 1e-12 {
x[pa] = 0.0;
passive[pa] = false;
}
}
}
}
x.iter_mut().for_each(|v| {
if *v < 0.0 {
*v = 0.0;
}
});
Ok(x)
}
pub fn abundance_estimation_fcls(
image: &HyperspectralImage,
endmembers: &Array2<f64>,
delta: f64,
) -> NdimageResult<Array2<f64>> {
let n_pixels = image.n_pixels();
let n_bands = image.n_bands();
let p = endmembers.ncols();
if endmembers.nrows() != n_bands {
return Err(NdimageError::InvalidInput(format!(
"endmembers.nrows() {} != n_bands {}",
endmembers.nrows(),
n_bands
)));
}
if delta <= 0.0 {
return Err(NdimageError::InvalidInput("delta must be positive".into()));
}
let mut e_aug = Array2::<f64>::zeros((n_bands + 1, p));
for k in 0..n_bands {
for j in 0..p {
e_aug[[k, j]] = endmembers[[k, j]];
}
}
for j in 0..p {
e_aug[[n_bands, j]] = delta;
}
let ete_aug = e_aug.t().dot(&e_aug);
let mut abundances = Array2::<f64>::zeros((n_pixels, p));
for i in 0..n_pixels {
let y_i = image.data.row(i).to_owned();
let mut y_aug = Array1::<f64>::zeros(n_bands + 1);
for k in 0..n_bands {
y_aug[k] = y_i[k];
}
y_aug[n_bands] = delta;
let ety_aug = e_aug.t().dot(&y_aug); let a_i = nnls_active_set(&ete_aug, &ety_aug, 300)?;
let s: f64 = a_i.iter().sum();
for j in 0..p {
abundances[[i, j]] = if s > 1e-12 { a_i[j] / s } else { 1.0 / p as f64 };
}
}
Ok(abundances)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn make_two_endmember_scene(n_pixels: usize, n_bands: usize) -> (HyperspectralImage, Array2<f64>) {
let mut data = Array2::<f64>::zeros((n_pixels, n_bands));
let e1: Vec<f64> = (0..n_bands).map(|b| (b as f64 / n_bands as f64)).collect();
let e2: Vec<f64> = (0..n_bands).map(|b| 1.0 - b as f64 / n_bands as f64).collect();
let mut endmembers = Array2::<f64>::zeros((n_bands, 2));
for b in 0..n_bands {
endmembers[[b, 0]] = e1[b];
endmembers[[b, 1]] = e2[b];
}
let mut lcg: u64 = 0xABCDEF_1234;
for i in 0..n_pixels {
lcg = lcg.wrapping_mul(6364136223846793005).wrapping_add(1);
let alpha = ((lcg >> 33) as f64) / u32::MAX as f64;
for b in 0..n_bands {
data[[i, b]] = alpha * e1[b] + (1.0 - alpha) * e2[b];
}
}
(HyperspectralImage::new(data), endmembers)
}
#[test]
fn test_vca_returns_correct_shape() {
let (img, _) = make_two_endmember_scene(100, 20);
let e = vertex_component_analysis(&img, 2).expect("VCA failed");
assert_eq!(e.shape(), &[20, 2]);
}
#[test]
fn test_nfindr_returns_correct_shape() {
let (img, _) = make_two_endmember_scene(50, 10);
let e = nfindr(&img, 2, 20, 2).expect("N-FINDR failed");
assert_eq!(e.shape(), &[10, 2]);
}
#[test]
fn test_sisal_returns_correct_shape() {
let (img, _) = make_two_endmember_scene(60, 12);
let e = sisal(&img, 2, 1e-4, 30).expect("SISAL failed");
assert_eq!(e.shape(), &[12, 2]);
}
#[test]
fn test_ucls_shape_and_values() {
let (img, endmembers) = make_two_endmember_scene(40, 10);
let a = abundance_estimation_ucls(&img, &endmembers).expect("UCLS failed");
assert_eq!(a.shape(), &[40, 2]);
for i in 0..40 {
let s = a[[i, 0]] + a[[i, 1]];
assert!((s - 1.0).abs() < 0.05, "UCLS sum={s} for pixel {i}");
}
}
#[test]
fn test_ncls_non_negative() {
let (img, endmembers) = make_two_endmember_scene(40, 10);
let a = abundance_estimation_ncls(&img, &endmembers).expect("NCLS failed");
for &v in a.iter() {
assert!(v >= 0.0, "NCLS produced negative abundance {v}");
}
}
#[test]
fn test_fcls_constraints() {
let (img, endmembers) = make_two_endmember_scene(40, 10);
let max_val = endmembers.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let delta = 10.0 * max_val.max(1.0);
let a = abundance_estimation_fcls(&img, &endmembers, delta).expect("FCLS failed");
for i in 0..40 {
let s: f64 = (0..2).map(|j| a[[i, j]]).sum();
assert!((s - 1.0).abs() < 0.05, "FCLS ASC violated: sum={s}");
for j in 0..2 {
assert!(a[[i, j]] >= -1e-9, "FCLS ANC violated: a={}", a[[i, j]]);
}
}
}
#[test]
fn test_nnls_all_zero_rhs() {
let ata = Array2::<f64>::eye(3);
let atb = Array1::<f64>::zeros(3);
let x = nnls_active_set(&ata, &atb, 50).expect("NNLS failed");
for &v in x.iter() {
assert!(v >= 0.0);
}
}
}