use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
use rayon::prelude::*;
use crate::banded::BandedCholesky;
use crate::error::{Result, invalid};
use crate::integrate::{Weight, inner, inner_derivative, integral, integral_derivative};
use crate::profile::Parameter;
use crate::space::Space;
use crate::sparse::Csr;
pub fn inner_products(a: &Space, b: &Space, weight: Weight) -> Csr {
let rows: Vec<(Vec<usize>, Vec<f64>)> = a
.elements()
.par_iter()
.map_init(
|| (Vec::new(), Vec::new()),
|(candidates, knots), e| {
b.candidates(e.lower(), e.upper(), candidates);
let mut idx = Vec::with_capacity(candidates.len());
let mut val = Vec::with_capacity(candidates.len());
for &j in candidates.iter() {
let v = inner(e, &b.elements()[j], weight, knots);
if v != 0.0 {
idx.push(j);
val.push(v);
}
}
(idx, val)
},
)
.collect();
Csr::from_rows(a.len(), b.len(), rows)
}
pub fn gram(a: &Space, weight: Weight) -> Csr {
inner_products(a, a, weight)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Normalization {
Integral,
RowSum,
None,
}
pub fn measure(
kernels: &Space,
basis: &Space,
normalization: Normalization,
weight: Weight,
) -> Csr {
let mut w = inner_products(kernels, basis, weight);
let norms = match normalization {
Normalization::Integral => kernels.integrals(weight),
Normalization::RowSum => w.row_sums(),
Normalization::None => return w,
};
let factors: Vec<f64> = norms
.iter()
.map(|&n| if n != 0.0 { 1.0 / n } else { 0.0 })
.collect();
w.scale_rows(&factors);
w
}
pub fn measure_derivative(
kernels: &Space,
basis: &Space,
normalization: Normalization,
weight: Weight,
p: Parameter,
) -> Result<(Csr, Csr)> {
type Row = (Vec<usize>, Vec<f64>, Vec<f64>, f64, f64);
let rows: Vec<Result<Row>> = kernels
.elements()
.par_iter()
.map_init(
|| (Vec::new(), Vec::new()),
|(candidates, knots), k| {
let jumps = k.jumps(p)?;
basis.candidates(k.lower(), k.upper(), candidates);
let mut idx = Vec::with_capacity(candidates.len());
let mut val = Vec::with_capacity(candidates.len());
let mut der = Vec::with_capacity(candidates.len());
for &j in candidates.iter() {
let b = &basis.elements()[j];
let v = inner(k, b, weight, knots);
let d = inner_derivative(k, b, p, weight, &jumps, knots)?;
if v != 0.0 || d != 0.0 {
idx.push(j);
val.push(v);
der.push(d);
}
}
let (n, dn) = match normalization {
Normalization::Integral => (
integral(k, weight, knots),
integral_derivative(k, p, weight, &jumps, knots)?,
),
Normalization::RowSum => (val.iter().sum(), der.iter().sum()),
Normalization::None => (1.0, 0.0),
};
Ok((idx, val, der, n, dn))
},
)
.collect();
let mut w_rows = Vec::with_capacity(rows.len());
let mut d_rows = Vec::with_capacity(rows.len());
for row in rows {
let (idx, val, der, n, dn) = row?;
let inv = if n != 0.0 { 1.0 / n } else { 0.0 };
let w: Vec<f64> = val.iter().map(|v| v * inv).collect();
let d: Vec<f64> = der
.iter()
.zip(&w)
.map(|(a, wi)| (a - wi * dn) * inv)
.collect();
w_rows.push((idx.clone(), w));
d_rows.push((idx, d));
}
Ok((
Csr::from_rows(kernels.len(), basis.len(), w_rows),
Csr::from_rows(kernels.len(), basis.len(), d_rows),
))
}
pub fn evaluate(basis: &Space, x: &[f64]) -> Result<Csr> {
Ok(inner_products(&Space::deltas(x)?, basis, Weight::Uniform))
}
fn solve_rows(chol: &BandedCholesky, rhs: &mut Array2<f64>) {
rhs.axis_iter_mut(Axis(0))
.into_par_iter()
.for_each(|mut row| chol.solve_in_place(row.as_slice_mut().expect("contiguous")));
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Bounds {
Local,
Global { lo: f64, hi: f64 },
}
#[derive(Debug, Clone)]
pub struct Projection {
gram_matrix: Csr,
gram: BandedCholesky,
cross: Csr,
dst_mass: Vec<f64>,
}
impl Projection {
pub fn new(src: &Space, dst: &Space, weight: Weight) -> Result<Self> {
let gram_matrix = gram(dst, weight);
Ok(Projection {
gram: BandedCholesky::factor(&gram_matrix)?,
gram_matrix,
cross: inner_products(dst, src, weight),
dst_mass: dst.integrals(weight),
})
}
pub fn n_src(&self) -> usize {
self.cross.ncols()
}
pub fn n_dst(&self) -> usize {
self.cross.nrows()
}
pub fn cross(&self) -> &Csr {
&self.cross
}
pub fn apply(&self, f: ArrayView1<f64>) -> Array1<f64> {
let mut g = self.cross.apply(f);
self.gram
.solve_in_place(g.as_slice_mut().expect("fresh array is contiguous"));
g
}
pub fn apply_last_axis(&self, f: ArrayView2<f64>) -> Array2<f64> {
let mut g = self.cross.apply_last_axis(f);
solve_rows(&self.gram, &mut g);
g
}
pub fn apply_bounded_last_axis(&self, f: ArrayView2<f64>, bounds: Bounds) -> Array2<f64> {
let mut rhs = self.cross.apply_last_axis(f);
let mut g = rhs.clone();
solve_rows(&self.gram, &mut g);
ndarray::Zip::from(g.axis_iter_mut(Axis(0)))
.and(rhs.axis_iter_mut(Axis(0)))
.and(f.axis_iter(Axis(0)))
.par_for_each(|mut gr, r, fr| {
let (lo, hi) = self.bounds(&fr.to_vec(), bounds);
let g0 = gr.to_vec();
let r = r.to_vec();
let c = bounded_least_squares(&self.gram_matrix, &r, &g0, &self.dst_mass, &lo, &hi)
.unwrap_or_else(|| {
let mut c = g0.clone();
caas(&mut c, &self.dst_mass, &lo, &hi);
c
});
gr.assign(&ArrayView1::from(&c));
});
g
}
fn bounds(&self, f: &[f64], bounds: Bounds) -> (Vec<f64>, Vec<f64>) {
let n = self.n_dst();
match bounds {
Bounds::Global { lo, hi } => (vec![lo; n], vec![hi; n]),
Bounds::Local => (0..n)
.map(|i| {
let idx = self.cross.row(i).0;
if idx.is_empty() {
(f64::NEG_INFINITY, f64::INFINITY)
} else {
idx.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), &j| {
(a.min(f[j]), b.max(f[j]))
})
}
})
.unzip(),
}
}
pub fn to_dense(&self) -> Array2<f64> {
let mut mt = self.cross.transpose().to_dense();
solve_rows(&self.gram, &mut mt);
mt.reversed_axes().as_standard_layout().into_owned()
}
}
fn bounded_least_squares(
gram: &Csr,
r: &[f64],
g: &[f64],
m: &[f64],
lo: &[f64],
hi: &[f64],
) -> Option<Vec<f64>> {
#[derive(Clone, Copy, PartialEq)]
enum State {
Free,
Lower,
Upper,
}
let n = g.len();
let target: f64 = m.iter().zip(g).map(|(a, b)| a * b).sum();
let diag: Vec<f64> = (0..n)
.map(|i| {
let (idx, val) = gram.row(i);
idx.iter().position(|&j| j == i).map_or(1.0, |k| val[k])
})
.collect();
let mut c: Vec<f64> = (0..n).map(|i| g[i].clamp(lo[i], hi[i])).collect();
let mut lambda = vec![0.0; n];
let mut state = vec![State::Free; n];
for i in 0..n {
if g[i] > hi[i] {
state[i] = State::Upper;
} else if g[i] < lo[i] {
state[i] = State::Lower;
}
}
for _ in 0..100 {
for i in 0..n {
match state[i] {
State::Lower => c[i] = lo[i],
State::Upper => c[i] = hi[i],
State::Free => {}
}
}
let free: Vec<usize> = (0..n).filter(|&i| state[i] == State::Free).collect();
let mu;
if free.is_empty() {
mu = 0.0;
} else {
let sub = gram.submatrix(&free);
let chol = BandedCholesky::factor(&sub).ok()?;
let mut a: Vec<f64> = free
.iter()
.map(|&i| {
let (idx, val) = gram.row(i);
let fixed: f64 = idx
.iter()
.zip(val)
.filter(|&(&j, _)| state[j] != State::Free)
.map(|(&j, v)| v * c[j])
.sum();
r[i] - fixed
})
.collect();
let mut b: Vec<f64> = free.iter().map(|&i| m[i]).collect();
chol.solve_in_place(&mut a);
chol.solve_in_place(&mut b);
let mb: f64 = free.iter().zip(&b).map(|(&i, v)| m[i] * v).sum();
let ma: f64 = free.iter().zip(&a).map(|(&i, v)| m[i] * v).sum();
let fixed_mass: f64 = (0..n)
.filter(|&i| state[i] != State::Free)
.map(|i| m[i] * c[i])
.sum();
mu = if mb.abs() > 0.0 {
(ma + fixed_mass - target) / mb
} else {
0.0
};
for (k, &i) in free.iter().enumerate() {
c[i] = a[k] - mu * b[k];
}
}
for i in 0..n {
lambda[i] = if state[i] == State::Free {
0.0
} else {
let (idx, val) = gram.row(i);
let gc: f64 = idx.iter().zip(val).map(|(&j, v)| v * c[j]).sum();
r[i] - gc - mu * m[i]
};
}
let mut changed = false;
for i in 0..n {
let new = if lo[i] == hi[i] {
State::Lower
} else if lambda[i] + diag[i] * (c[i] - hi[i]) > 0.0 {
State::Upper
} else if lambda[i] + diag[i] * (c[i] - lo[i]) < 0.0 {
State::Lower
} else {
State::Free
};
if new != state[i] {
changed = true;
state[i] = new;
}
}
if !changed {
return Some(c);
}
}
None
}
fn caas(g: &mut [f64], mass: &[f64], lo: &[f64], hi: &[f64]) {
let n = g.len();
let mut deficit = 0.0; for i in 0..n {
let c = g[i].clamp(lo[i], hi[i]);
deficit += mass[i] * (g[i] - c);
g[i] = c;
}
let room = |i: usize, up: bool| {
let r = if up { hi[i] - g[i] } else { g[i] - lo[i] };
if r.is_finite() { r } else { 0.0 }
};
if deficit != 0.0 {
let up = deficit > 0.0;
let total: f64 = (0..n).map(|i| mass[i] * room(i, up)).sum();
if total > 0.0 {
let f = (deficit.abs() / total).min(1.0);
let shifts: Vec<f64> = (0..n).map(|i| f * room(i, up)).collect();
for i in 0..n {
g[i] += if up { shifts[i] } else { -shifts[i] };
}
}
}
}
fn difference_matrix(n: usize, order: usize) -> Csr {
let mut coeffs = vec![1.0];
for _ in 0..order {
let mut next = vec![0.0; coeffs.len() + 1];
for (k, c) in coeffs.iter().enumerate() {
next[k + 1] += c;
next[k] -= c;
}
coeffs = next;
}
let m = n.saturating_sub(order);
let rows = (0..m)
.map(|i| ((i..=i + order).collect(), coeffs.clone()))
.collect();
Csr::from_rows(m, n, rows)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Penalty {
Derivative(usize),
Difference(usize),
}
#[derive(Debug, Clone)]
pub struct Fit {
normal: BandedCholesky,
design: Csr,
design_t: Csr,
}
impl Fit {
pub fn new(
kernels: &Space,
model: &Space,
normalization: Normalization,
weight: Weight,
smoothing: f64,
penalty: Penalty,
) -> Result<Self> {
if !(smoothing.is_finite() && smoothing >= 0.0) {
return invalid("smoothing must be finite and >= 0");
}
let design = measure(kernels, model, normalization, weight);
let mut normal = design.gram_columns();
if smoothing > 0.0 {
let p = match penalty {
Penalty::Difference(r) if model.len() > r => {
Some(difference_matrix(model.len(), r).gram_columns())
}
Penalty::Difference(_) => None,
Penalty::Derivative(r) => Some(gram(&model.derivative(r)?, Weight::Uniform)),
};
if let Some(p) = p {
let tr = p.trace();
if tr <= 0.0 {
return invalid(format!(
"the {penalty:?} penalty is zero for this model (degree too low?)"
));
}
normal = normal.add_scaled(&p, smoothing * normal.trace() / tr);
}
}
let normal = BandedCholesky::factor(&normal).map_err(|e| match e {
crate::Error::Singular(m) => crate::Error::Singular(format!(
"{m}; the data do not determine every model coefficient, so add smoothing"
)),
other => other,
})?;
let design_t = design.transpose();
Ok(Fit {
normal,
design,
design_t,
})
}
pub fn n_data(&self) -> usize {
self.design.nrows()
}
pub fn n_model(&self) -> usize {
self.design.ncols()
}
pub fn design(&self) -> &Csr {
&self.design
}
pub fn apply(&self, data: ArrayView1<f64>) -> Array1<f64> {
let mut c = self.design_t.apply(data);
self.normal
.solve_in_place(c.as_slice_mut().expect("contiguous"));
c
}
pub fn apply_last_axis(&self, data: ArrayView2<f64>) -> Array2<f64> {
let mut c = self.design_t.apply_last_axis(data);
solve_rows(&self.normal, &mut c);
c
}
pub fn to_dense(&self) -> Array2<f64> {
let mut w = self.design.to_dense();
solve_rows(&self.normal, &mut w);
w.reversed_axes().as_standard_layout().into_owned()
}
}