use super::*;
pub(crate) struct DuchonRadialJetsNd {
pub(crate) phi_r: Array2<f64>,
pub(crate) phi_rr: Array2<f64>,
pub(crate) phi_rrr: Array2<f64>,
}
pub(crate) fn duchon_effective_length_scale(
length_scale: Option<f64>,
centers: ArrayView2<'_, f64>,
) -> f64 {
length_scale.unwrap_or_else(|| {
let n_centers = centers.nrows();
let dim = centers.ncols();
let mut acc = 0.0_f64;
let mut cnt = 0usize;
for i in 0..n_centers.min(8) {
for j in (i + 1)..n_centers.min(8) {
let mut r2 = 0.0_f64;
for a in 0..dim {
let dv = centers[[i, a]] - centers[[j, a]];
r2 += dv * dv;
}
acc += r2.sqrt();
cnt += 1;
}
}
if cnt == 0 || acc <= 0.0 {
1.0
} else {
acc / cnt as f64
}
})
}
pub(crate) fn duchon_radial_jets_nd(
max_order: usize,
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
length_scale: Option<f64>,
nullspace_order: DuchonNullspaceOrder,
power: usize,
caller: &str,
) -> Result<DuchonRadialJetsNd, BasisError> {
let n_rows = t.nrows();
let n_centers = centers.nrows();
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!("{caller}: centers must have at least one column");
}
if t.ncols() != dim {
crate::bail_invalid_basis!(
"{caller}: t has {} cols but centers have {}",
t.ncols(),
dim
);
}
assert!(
(1..=3).contains(&max_order),
"duchon_radial_jets_nd supports radial orders 1..=3; got {max_order}"
);
let effective_order = duchon_effective_nullspace_order(centers, nullspace_order);
let p_order = duchon_p_from_nullspace_order(effective_order);
let (jet_p_order, s_order) = match length_scale {
Some(_) => (p_order, power),
None => (p_order + power, 0usize),
};
let kappa = length_scale.map(|l| 1.0 / l.max(1e-300)).unwrap_or(0.0);
let coeffs = duchon_partial_fraction_coeffs(jet_p_order, s_order, kappa);
let effective_length_scale = duchon_effective_length_scale(length_scale, centers);
let mut phi_r = Array2::<f64>::zeros((n_rows, n_centers));
let mut phi_rr = if max_order >= 2 {
Array2::<f64>::zeros((n_rows, n_centers))
} else {
Array2::<f64>::zeros((0, 0))
};
let mut phi_rrr = if max_order >= 3 {
Array2::<f64>::zeros((n_rows, n_centers))
} else {
Array2::<f64>::zeros((0, 0))
};
for n in 0..n_rows {
for k in 0..n_centers {
let mut r2 = 0.0_f64;
for a in 0..dim {
let dv = t[[n, a]] - centers[[k, a]];
r2 += dv * dv;
}
let r = r2.sqrt();
let jets = duchon_radial_jets(
r,
effective_length_scale,
jet_p_order,
s_order,
dim,
&coeffs,
)?;
phi_r[[n, k]] = jets.phi_r;
if max_order >= 2 {
phi_rr[[n, k]] = jets.phi_rr;
}
if max_order >= 3 {
phi_rrr[[n, k]] = jets.phi_rrr;
}
}
}
Ok(DuchonRadialJetsNd {
phi_r,
phi_rr,
phi_rrr,
})
}
pub(crate) fn radial_basis_cartesian_derivative(
order: usize,
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
coeffs: ArrayView2<'_, f64>,
length_scale: Option<f64>,
nullspace_order: DuchonNullspaceOrder,
power: usize,
) -> Result<Array2<f64>, BasisError> {
assert!(
order == 2 || order == 3,
"radial_basis_cartesian_derivative supports Cartesian orders 2 and 3; got {order}"
);
let n_rows = t.nrows();
let n_centers = centers.nrows();
let d = centers.ncols();
let p_out = coeffs.ncols();
assert_eq!(
coeffs.nrows(),
n_centers,
"radial_basis_cartesian_derivative: coeffs has {} rows but centers have {n_centers}",
coeffs.nrows()
);
let jets = duchon_radial_jets_nd(
order,
t,
centers,
length_scale,
nullspace_order,
power,
"radial_basis_cartesian_derivative",
)?;
let d_pow = d.pow(order as u32);
let mut out = Array2::<f64>::zeros((n_rows, p_out * d_pow));
for n in 0..n_rows {
for k in 0..n_centers {
let mut r2 = 0.0_f64;
for a in 0..d {
let delta = t[[n, a]] - centers[[k, a]];
r2 += delta * delta;
}
let r = r2.sqrt();
match order {
2 => {
for a in 0..d {
for c in 0..d {
let basis_hess = if r == 0.0 {
if a == c { jets.phi_rr[[n, k]] } else { 0.0 }
} else {
let inv_r = 1.0 / r;
let u_a = (t[[n, a]] - centers[[k, a]]) * inv_r;
let u_c = (t[[n, c]] - centers[[k, c]]) * inv_r;
let q = jets.phi_r[[n, k]] * inv_r;
let eye = if a == c { 1.0 } else { 0.0 };
q * eye + (jets.phi_rr[[n, k]] - q) * u_a * u_c
};
if basis_hess == 0.0 {
continue;
}
let m = a * d + c;
for i in 0..p_out {
out[[n, i * d_pow + m]] += coeffs[[k, i]] * basis_hess;
}
}
}
}
_ => {
if r == 0.0 {
continue;
}
let inv_r = 1.0 / r;
let q = jets.phi_r[[n, k]] * inv_r;
let b_coef = (jets.phi_rr[[n, k]] - q) * inv_r;
let a_coef = jets.phi_rrr[[n, k]] - 3.0 * b_coef;
for a in 0..d {
let u_a = (t[[n, a]] - centers[[k, a]]) * inv_r;
for c in 0..d {
let u_c = (t[[n, c]] - centers[[k, c]]) * inv_r;
for e in 0..d {
let u_e = (t[[n, e]] - centers[[k, e]]) * inv_r;
let eye_ac = if a == c { 1.0 } else { 0.0 };
let eye_ae = if a == e { 1.0 } else { 0.0 };
let eye_ce = if c == e { 1.0 } else { 0.0 };
let basis_third = a_coef * u_a * u_c * u_e
+ b_coef * (eye_ac * u_e + eye_ae * u_c + eye_ce * u_a);
if basis_third == 0.0 {
continue;
}
let m = (a * d + c) * d + e;
for i in 0..p_out {
out[[n, i * d_pow + m]] += coeffs[[k, i]] * basis_third;
}
}
}
}
}
}
}
}
Ok(out)
}
pub fn duchon_radial_first_derivative_nd(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
length_scale: Option<f64>,
nullspace_order: DuchonNullspaceOrder,
power: usize,
) -> Result<Array2<f64>, BasisError> {
Ok(duchon_radial_jets_nd(
1,
t,
centers,
length_scale,
nullspace_order,
power,
"duchon_radial_first_derivative_nd",
)?
.phi_r)
}
pub fn duchon_radial_second_derivative_nd(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
length_scale: Option<f64>,
nullspace_order: DuchonNullspaceOrder,
power: usize,
) -> Result<Array2<f64>, BasisError> {
Ok(duchon_radial_jets_nd(
2,
t,
centers,
length_scale,
nullspace_order,
power,
"duchon_radial_second_derivative_nd",
)?
.phi_rr)
}
pub fn duchon_radial_third_derivative_nd(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
length_scale: Option<f64>,
nullspace_order: DuchonNullspaceOrder,
power: usize,
) -> Result<Array2<f64>, BasisError> {
Ok(duchon_radial_jets_nd(
3,
t,
centers,
length_scale,
nullspace_order,
power,
"duchon_radial_third_derivative_nd",
)?
.phi_rrr)
}
pub(crate) fn fill_duchon_1d_polynomial_derivative(
basis: &mut Array2<f64>,
start_col: usize,
t: ArrayView1<'_, f64>,
nullspace_order: DuchonNullspaceOrder,
derivative_order: usize,
) {
let exponents: Vec<usize> = match nullspace_order {
DuchonNullspaceOrder::Zero => vec![0],
DuchonNullspaceOrder::Linear => vec![0, 1],
DuchonNullspaceOrder::Degree(degree) => monomial_exponents(1, degree)
.into_iter()
.map(|exponent| exponent[0])
.collect(),
};
for (offset, exponent) in exponents.into_iter().enumerate() {
if exponent < derivative_order {
continue;
}
let coefficient =
(0..derivative_order).fold(1.0, |acc, step| acc * (exponent - step) as f64);
let remaining = exponent - derivative_order;
for row in 0..t.len() {
basis[[row, start_col + offset]] = if remaining == 0 {
coefficient
} else {
coefficient * t[row].powi(remaining as i32)
};
}
}
}
pub fn duchon_polynomial_first_derivative_nd(
t: ArrayView2<'_, f64>,
nullspace_order: DuchonNullspaceOrder,
) -> Array3<f64> {
let n_rows = t.nrows();
let dim = t.ncols();
let max_degree = match nullspace_order {
DuchonNullspaceOrder::Zero => 0usize,
DuchonNullspaceOrder::Linear => 1usize,
DuchonNullspaceOrder::Degree(k) => k,
};
let exponents = monomial_exponents(dim, max_degree);
let n_poly = exponents.len();
let mut out = Array3::<f64>::zeros((n_rows, n_poly, dim));
if dim == 0 || n_poly == 0 {
return out;
}
for (col, alpha) in exponents.iter().enumerate() {
for axis in 0..dim {
let a_axis = alpha[axis];
if a_axis == 0 {
continue;
}
for row in 0..n_rows {
let mut value = a_axis as f64;
for a in 0..dim {
let exp_a = if a == axis { a_axis - 1 } else { alpha[a] };
if exp_a != 0 {
value *= t[[row, a]].powi(exp_a as i32);
}
}
out[[row, col, axis]] = value;
}
}
}
out
}
pub fn radial_input_location_jet_nd(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
phi_r: ArrayView2<'_, f64>,
) -> Result<Array3<f64>, BasisError> {
let n_rows = t.nrows();
let n_centers = centers.nrows();
let dim = t.ncols();
if phi_r.dim() != (n_rows, n_centers) {
crate::bail_dim_basis!(
"radial_input_location_jet_nd: phi_r shape {:?} != ({n_rows}, {n_centers})",
phi_r.dim()
);
}
if centers.ncols() != dim {
crate::bail_dim_basis!(
"radial_input_location_jet_nd: t has {dim} cols but centers have {}",
centers.ncols()
);
}
let mut out = Array3::<f64>::zeros((n_rows, n_centers, dim));
for n in 0..n_rows {
for k in 0..n_centers {
let mut r2 = 0.0_f64;
for a in 0..dim {
let delta = t[[n, a]] - centers[[k, a]];
r2 += delta * delta;
}
let r = r2.sqrt();
if r <= 1.0e-12 {
continue;
}
let scale = phi_r[[n, k]] / r;
for a in 0..dim {
out[[n, k, a]] = scale * (t[[n, a]] - centers[[k, a]]);
}
}
}
Ok(out)
}
pub fn duchon_sae_atom_basis_with_jet(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
nullspace_order: DuchonNullspaceOrder,
) -> Result<(Array2<f64>, Array3<f64>), BasisError> {
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!(
"duchon_sae_atom_basis_with_jet: centers must have at least one column"
);
}
if t.ncols() != dim {
crate::bail_dim_basis!(
"duchon_sae_atom_basis_with_jet: t has {} cols but centers have {dim}",
t.ncols()
);
}
let effective_order = duchon_effective_nullspace_order(centers, nullspace_order);
let p_order = duchon_p_from_nullspace_order(effective_order);
let s_order: f64 = 0.0;
let poly_block_centers = polynomial_block_from_order(centers, effective_order);
let z = kernel_constraint_nullspace_from_matrix(poly_block_centers.view())?;
let n_kernel = z.ncols();
let pure_poly_coeff =
PolyharmonicBlockCoeff::new(pure_duchon_block_order(p_order, s_order), dim);
let kernel_amp = duchon_kernel_amplification(
centers,
None,
p_order,
duchon_power_to_usize(s_order),
dim,
None,
None,
Some(&pure_poly_coeff),
);
let n_rows = t.nrows();
let n_centers = centers.nrows();
let mut radial_value = Array2::<f64>::zeros((n_rows, n_centers));
for n in 0..n_rows {
for k in 0..n_centers {
let r = euclidean_distance_rows(t, n, centers, k);
radial_value[[n, k]] = pure_poly_coeff.eval(r) * kernel_amp;
}
}
let kernel_design = fast_ab(&radial_value, &z);
let poly_design = polynomial_block_from_order(t, effective_order);
let n_poly = poly_design.ncols();
let mut phi = Array2::<f64>::zeros((n_rows, n_kernel + n_poly));
phi.slice_mut(s![.., ..n_kernel]).assign(&kernel_design);
if n_poly > 0 {
phi.slice_mut(s![.., n_kernel..]).assign(&poly_design);
}
let radial_first = duchon_radial_first_derivative_nd(t, centers, None, effective_order, 0)?;
let radial_jet = radial_input_location_jet_nd(t, centers, radial_first.view())?;
let poly_jet = duchon_polynomial_first_derivative_nd(t, effective_order);
if poly_jet.shape()[1] != n_poly {
crate::bail_dim_basis!(
"duchon_sae_atom_basis_with_jet: polynomial jet has {} columns but design has {n_poly}",
poly_jet.shape()[1]
);
}
let mut jet = Array3::<f64>::zeros((n_rows, n_kernel + n_poly, dim));
for axis in 0..dim {
let projected = radial_jet.index_axis(Axis(2), axis).dot(&z);
let mut block = jet.slice_mut(s![.., ..n_kernel, axis]);
block.assign(&projected);
block *= kernel_amp;
}
jet.slice_mut(s![.., n_kernel.., ..]).assign(&poly_jet);
Ok((phi, jet))
}
pub fn duchon_sae_atom_penalty(
centers: ArrayView2<'_, f64>,
nullspace_order: DuchonNullspaceOrder,
) -> Result<Array2<f64>, BasisError> {
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!(
"duchon_sae_atom_penalty: centers must have at least one column"
);
}
let effective_order = duchon_effective_nullspace_order(centers, nullspace_order);
let p_order = duchon_p_from_nullspace_order(effective_order);
let s_order: f64 = 0.0;
let poly_block_centers = polynomial_block_from_order(centers, effective_order);
let z = kernel_constraint_nullspace_from_matrix(poly_block_centers.view())?;
let n_kernel = z.ncols();
let n_poly = poly_block_centers.ncols();
let pure_poly_coeff =
PolyharmonicBlockCoeff::new(pure_duchon_block_order(p_order, s_order), dim);
let kernel_amp = duchon_kernel_amplification(
centers,
None,
p_order,
duchon_power_to_usize(s_order),
dim,
None,
None,
Some(&pure_poly_coeff),
);
let n_centers = centers.nrows();
let mut k_cc = Array2::<f64>::zeros((n_centers, n_centers));
for i in 0..n_centers {
for j in 0..n_centers {
let r = euclidean_distance_rows(centers, i, centers, j);
k_cc[[i, j]] = pure_poly_coeff.eval(r);
}
}
let kz = fast_ab(&k_cc, &z);
let z_t = z.t().to_owned();
let ztkz = fast_ab(&z_t, &kz);
let amp2 = kernel_amp * kernel_amp;
let m = n_kernel + n_poly;
let mut penalty = Array2::<f64>::zeros((m, m));
for a in 0..n_kernel {
for b in 0..n_kernel {
penalty[[a, b]] = 0.5 * amp2 * (ztkz[[a, b]] + ztkz[[b, a]]);
}
}
Ok(penalty)
}
pub fn duchon_sae_atom_second_jet(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
nullspace_order: DuchonNullspaceOrder,
) -> Result<Array4<f64>, BasisError> {
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!(
"duchon_sae_atom_second_jet: centers must have at least one column"
);
}
if t.ncols() != dim {
crate::bail_dim_basis!(
"duchon_sae_atom_second_jet: t has {} cols but centers have {dim}",
t.ncols()
);
}
let effective_order = duchon_effective_nullspace_order(centers, nullspace_order);
let p_order = duchon_p_from_nullspace_order(effective_order);
let s_order: f64 = 0.0;
let poly_block_centers = polynomial_block_from_order(centers, effective_order);
let z = kernel_constraint_nullspace_from_matrix(poly_block_centers.view())?;
let n_kernel = z.ncols();
let pure_poly_coeff =
PolyharmonicBlockCoeff::new(pure_duchon_block_order(p_order, s_order), dim);
let kernel_amp = duchon_kernel_amplification(
centers,
None,
p_order,
duchon_power_to_usize(s_order),
dim,
None,
None,
Some(&pure_poly_coeff),
);
let n_rows = t.nrows();
let poly_block_t_cols = polynomial_block_from_order(t, effective_order).ncols();
let coeffs = &z * kernel_amp;
let flat =
radial_basis_cartesian_derivative(2, t, centers, coeffs.view(), None, effective_order, 0)?;
let mut out = Array4::<f64>::zeros((n_rows, n_kernel + poly_block_t_cols, dim, dim));
for n in 0..n_rows {
for i in 0..n_kernel {
for a in 0..dim {
for c in 0..dim {
out[[n, i, a, c]] = flat[[n, i * dim * dim + a * dim + c]];
}
}
}
}
let exponents = monomial_exponents(
dim,
match effective_order {
DuchonNullspaceOrder::Zero => 0,
DuchonNullspaceOrder::Linear => 1,
DuchonNullspaceOrder::Degree(k) => k,
},
);
if exponents.len() != poly_block_t_cols {
crate::bail_dim_basis!(
"duchon_sae_atom_second_jet: monomial count {} != polynomial block columns {poly_block_t_cols}",
exponents.len()
);
}
for (col, alpha) in exponents.iter().enumerate() {
for a in 0..dim {
for c in 0..dim {
for row in 0..n_rows {
let coeff_a = alpha[a];
if coeff_a == 0 {
continue;
}
let coeff_c = if a == c {
alpha[c].saturating_sub(1)
} else {
alpha[c]
};
if a != c && coeff_c == 0 {
continue;
}
let lead = (coeff_a as f64) * (coeff_c as f64);
if lead == 0.0 {
continue;
}
let mut value = lead;
for axis in 0..dim {
let mut exp = alpha[axis];
if axis == a {
exp = exp.saturating_sub(1);
}
if axis == c {
exp = exp.saturating_sub(1);
}
if exp != 0 {
value *= t[[row, axis]].powi(exp as i32);
}
}
out[[row, n_kernel + col, a, c]] = value;
}
}
}
}
Ok(out)
}
pub fn duchon_sae_atom_third_jet(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
nullspace_order: DuchonNullspaceOrder,
) -> Result<Array5<f64>, BasisError> {
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!(
"duchon_sae_atom_third_jet: centers must have at least one column"
);
}
if t.ncols() != dim {
crate::bail_dim_basis!(
"duchon_sae_atom_third_jet: t has {} cols but centers have {dim}",
t.ncols()
);
}
let effective_order = duchon_effective_nullspace_order(centers, nullspace_order);
let p_order = duchon_p_from_nullspace_order(effective_order);
let s_order: f64 = 0.0;
let poly_block_centers = polynomial_block_from_order(centers, effective_order);
let z = kernel_constraint_nullspace_from_matrix(poly_block_centers.view())?;
let n_kernel = z.ncols();
let pure_poly_coeff =
PolyharmonicBlockCoeff::new(pure_duchon_block_order(p_order, s_order), dim);
let kernel_amp = duchon_kernel_amplification(
centers,
None,
p_order,
duchon_power_to_usize(s_order),
dim,
None,
None,
Some(&pure_poly_coeff),
);
let n_rows = t.nrows();
let poly_block_t_cols = polynomial_block_from_order(t, effective_order).ncols();
let coeffs = &z * kernel_amp;
let flat =
radial_basis_cartesian_derivative(3, t, centers, coeffs.view(), None, effective_order, 0)?;
let d_pow = dim * dim * dim;
let mut out = Array5::<f64>::zeros((n_rows, n_kernel + poly_block_t_cols, dim, dim, dim));
for n in 0..n_rows {
for i in 0..n_kernel {
for a in 0..dim {
for c in 0..dim {
for e in 0..dim {
out[[n, i, a, c, e]] = flat[[n, i * d_pow + ((a * dim) + c) * dim + e]];
}
}
}
}
}
let exponents = monomial_exponents(
dim,
match effective_order {
DuchonNullspaceOrder::Zero => 0,
DuchonNullspaceOrder::Linear => 1,
DuchonNullspaceOrder::Degree(k) => k,
},
);
if exponents.len() != poly_block_t_cols {
crate::bail_dim_basis!(
"duchon_sae_atom_third_jet: monomial count {} != polynomial block columns {poly_block_t_cols}",
exponents.len()
);
}
let falling = |alpha: usize, k: usize| -> f64 {
let mut acc = 1.0_f64;
for j in 0..k {
acc *= (alpha as f64) - (j as f64);
}
acc
};
for (col, alpha) in exponents.iter().enumerate() {
for a in 0..dim {
if alpha[a] == 0 {
continue;
}
for c in 0..dim {
for e in 0..dim {
let mut order = vec![0usize; dim];
order[a] += 1;
order[c] += 1;
order[e] += 1;
if (0..dim).any(|axis| order[axis] > alpha[axis]) {
continue;
}
let mut lead = 1.0_f64;
for axis in 0..dim {
lead *= falling(alpha[axis], order[axis]);
}
if lead == 0.0 {
continue;
}
for row in 0..n_rows {
let mut value = lead;
for axis in 0..dim {
let exp = alpha[axis] - order[axis];
if exp != 0 {
value *= t[[row, axis]].powi(exp as i32);
}
}
out[[row, n_kernel + col, a, c, e]] = value;
}
}
}
}
}
Ok(out)
}
pub fn build_duchon_basis_design_and_jets(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
length_scale: Option<f64>,
power: f64,
nullspace_order: DuchonNullspaceOrder,
periodic_per_axis: &[bool],
periods: &[f64],
) -> Result<(Array2<f64>, Array3<f64>, Array4<f64>), BasisError> {
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!(
"build_duchon_basis_design_and_jets: centers must have at least one column"
);
}
if t.ncols() != dim {
crate::bail_dim_basis!(
"build_duchon_basis_design_and_jets: t has {} cols but centers have {dim}",
t.ncols()
);
}
if periodic_per_axis.len() != dim {
crate::bail_invalid_basis!(
"build_duchon_basis_design_and_jets: periodic_per_axis must have length {dim}, got {}",
periodic_per_axis.len()
);
}
if periods.len() != dim {
crate::bail_invalid_basis!(
"build_duchon_basis_design_and_jets: periods must have length {dim}, got {}",
periods.len()
);
}
let any_periodic = periodic_per_axis.iter().any(|&p| p);
let n_rows = t.nrows();
let n_centers = centers.nrows();
if any_periodic {
if length_scale.is_some() {
crate::bail_invalid_basis!(
"mixed-periodicity Duchon basis currently only supports the pure polyharmonic spectrum (length_scale=None)"
);
}
if power != 0.0 {
crate::bail_invalid_basis!(
"mixed-periodicity Duchon basis currently requires power = 0 (pure polyharmonic); got power={power}"
);
}
for (j, (&per, &period)) in periodic_per_axis.iter().zip(periods.iter()).enumerate() {
if per && !(period.is_finite() && period > 0.0) {
crate::bail_invalid_basis!(
"axis {j} is periodic but period={period} is not finite & positive"
);
}
}
}
if any_periodic {
return mixed_periodicity_additive_design_and_jets(
t,
centers,
nullspace_order,
periodic_per_axis,
periods,
);
}
let effective_order = duchon_effective_nullspace_order(centers, nullspace_order);
let kernel_nullspace_order = if any_periodic {
DuchonNullspaceOrder::Zero
} else {
effective_order
};
let p_order = duchon_p_from_nullspace_order(kernel_nullspace_order);
let kernel_m = if any_periodic {
duchon_p_from_nullspace_order(nullspace_order)
} else {
p_order
};
let s_order_int = duchon_power_to_usize(power);
let s_order_f = power;
let poly_block_centers = polynomial_block_from_order(centers, kernel_nullspace_order);
let z = kernel_constraint_nullspace_from_matrix(poly_block_centers.view())?;
let n_kernel = z.ncols();
let coeffs = length_scale
.map(|ls| duchon_partial_fraction_coeffs(p_order, s_order_int, 1.0 / ls.max(1e-300)));
let pure_poly_coeff = if length_scale.is_none() {
Some(PolyharmonicBlockCoeff::new(
pure_duchon_block_order(kernel_m, s_order_f),
dim,
))
} else {
None
};
let kernel_amp = if any_periodic {
1.0
} else {
duchon_kernel_amplification(
centers,
length_scale,
p_order,
s_order_int,
dim,
None,
coeffs.as_ref(),
pure_poly_coeff.as_ref(),
)
};
let mut radial_value = Array2::<f64>::zeros((n_rows, n_centers));
let mut radial_first = Array2::<f64>::zeros((n_rows, n_centers));
let mut radial_second = Array2::<f64>::zeros((n_rows, n_centers));
let mut delta = Array3::<f64>::zeros((n_rows, n_centers, dim));
let mut metric_d1 = Array3::<f64>::zeros((n_rows, n_centers, dim));
let mut metric_d2 = Array3::<f64>::zeros((n_rows, n_centers, dim));
let m_pure = pure_duchon_block_order(kernel_m, s_order_f);
let pi = std::f64::consts::PI;
for n in 0..n_rows {
for k in 0..n_centers {
let mut r2 = 0.0_f64;
for a in 0..dim {
let raw = t[[n, a]] - centers[[k, a]];
let (d_a, d1_a, d2_a) = if periodic_per_axis[a] {
let p = periods[a];
let theta = pi * raw / p;
((p / pi) * theta.sin(), theta.cos(), -(pi / p) * theta.sin())
} else {
(raw, 1.0, 0.0)
};
delta[[n, k, a]] = d_a;
metric_d1[[n, k, a]] = d1_a;
metric_d2[[n, k, a]] = d2_a;
r2 += d_a * d_a;
}
let r = r2.sqrt();
let (phi, phi_r, phi_rr) = if pure_poly_coeff.is_some() {
polyharmonic_kernel_triplet(r, m_pure, dim)?
} else {
let jets = duchon_radial_jets(
r,
length_scale.expect("hybrid Duchon requires length_scale"),
p_order,
s_order_int,
dim,
coeffs.as_ref().expect("hybrid Duchon requires coeffs"),
)?;
(jets.phi, jets.phi_r, jets.phi_rr)
};
radial_value[[n, k]] = phi * kernel_amp;
radial_first[[n, k]] = phi_r;
radial_second[[n, k]] = phi_rr;
}
}
let kernel_design = fast_ab(&radial_value, &z);
let poly_design = polynomial_block_from_order(t, kernel_nullspace_order);
let n_poly = poly_design.ncols();
let mut phi_design = Array2::<f64>::zeros((n_rows, n_kernel + n_poly));
phi_design
.slice_mut(s![.., ..n_kernel])
.assign(&kernel_design);
if n_poly > 0 {
phi_design
.slice_mut(s![.., n_kernel..])
.assign(&poly_design);
}
let mut radial_jet = Array3::<f64>::zeros((n_rows, n_centers, dim));
let mut radial_hess = Array4::<f64>::zeros((n_rows, n_centers, dim, dim));
for n in 0..n_rows {
for k in 0..n_centers {
let mut r2 = 0.0_f64;
for a in 0..dim {
let da = delta[[n, k, a]];
r2 += da * da;
}
let r = r2.sqrt();
let phi_r = radial_first[[n, k]];
let phi_rr = radial_second[[n, k]];
if r <= 1.0e-12 {
for a in 0..dim {
let d1a = metric_d1[[n, k, a]];
radial_hess[[n, k, a, a]] = phi_rr * kernel_amp * d1a * d1a;
}
continue;
}
let q = phi_r / r;
let s_scalar = (phi_rr - q) / r2;
for a in 0..dim {
let ga = delta[[n, k, a]] * metric_d1[[n, k, a]];
radial_jet[[n, k, a]] = q * ga * kernel_amp;
}
for a in 0..dim {
let ga = delta[[n, k, a]] * metric_d1[[n, k, a]];
for c in 0..dim {
let gc = delta[[n, k, c]] * metric_d1[[n, k, c]];
let mut value = s_scalar * ga * gc;
if a == c {
let d1a = metric_d1[[n, k, a]];
let curvature = d1a * d1a + delta[[n, k, a]] * metric_d2[[n, k, a]];
value += q * curvature;
}
radial_hess[[n, k, a, c]] = value * kernel_amp;
}
}
}
}
let poly_jet = duchon_polynomial_first_derivative_nd(t, kernel_nullspace_order);
if poly_jet.shape()[1] != n_poly {
crate::bail_dim_basis!(
"build_duchon_basis_design_and_jets: polynomial jet has {} columns but design has {n_poly}",
poly_jet.shape()[1]
);
}
let mut jet = Array3::<f64>::zeros((n_rows, n_kernel + n_poly, dim));
for axis in 0..dim {
let projected = radial_jet.index_axis(Axis(2), axis).dot(&z);
jet.slice_mut(s![.., ..n_kernel, axis]).assign(&projected);
}
if n_poly > 0 {
jet.slice_mut(s![.., n_kernel.., ..]).assign(&poly_jet);
}
let mut hess = Array4::<f64>::zeros((n_rows, n_kernel + n_poly, dim, dim));
for a in 0..dim {
for c in 0..dim {
let slab = radial_hess.slice(s![.., .., a, c]);
let projected = slab.dot(&z);
hess.slice_mut(s![.., ..n_kernel, a, c]).assign(&projected);
}
}
if n_poly > 0 {
let max_degree = match kernel_nullspace_order {
DuchonNullspaceOrder::Zero => 0,
DuchonNullspaceOrder::Linear => 1,
DuchonNullspaceOrder::Degree(deg) => deg,
};
let exponents = monomial_exponents(dim, max_degree);
if exponents.len() != n_poly {
crate::bail_dim_basis!(
"build_duchon_basis_design_and_jets: monomial count {} != polynomial block columns {n_poly}",
exponents.len()
);
}
for (col, alpha) in exponents.iter().enumerate() {
for a in 0..dim {
let coeff_a = alpha[a];
if coeff_a == 0 {
continue;
}
for c in 0..dim {
let coeff_c = if a == c {
alpha[c].saturating_sub(1)
} else {
alpha[c]
};
if a != c && coeff_c == 0 {
continue;
}
let lead = (coeff_a as f64) * (coeff_c as f64);
if lead == 0.0 {
continue;
}
for row in 0..n_rows {
let mut value = lead;
for axis in 0..dim {
let mut exp = alpha[axis];
if axis == a {
exp = exp.saturating_sub(1);
}
if axis == c {
exp = exp.saturating_sub(1);
}
if exp != 0 {
value *= t[[row, axis]].powi(exp as i32);
}
}
hess[[row, n_kernel + col, a, c]] = value;
}
}
}
}
}
Ok((phi_design, jet, hess))
}
fn mixed_periodicity_additive_design_and_jets(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
nullspace_order: DuchonNullspaceOrder,
periodic_per_axis: &[bool],
periods: &[f64],
) -> Result<(Array2<f64>, Array3<f64>, Array4<f64>), BasisError> {
let dim = centers.ncols();
let n_rows = t.nrows();
let n_centers = centers.nrows();
let m = duchon_p_from_nullspace_order(nullspace_order);
let axis_bounds = crate::basis::mixed_periodicity_axis_bounds(centers, periodic_per_axis);
let poly_block_centers =
crate::basis::mixed_periodicity_nullspace_poly_block(centers, m, periodic_per_axis);
let z = kernel_constraint_nullspace_from_matrix(poly_block_centers.view())?;
let n_kernel = z.ncols();
let nonperiodic_axes: Vec<usize> = (0..dim).filter(|&j| !periodic_per_axis[j]).collect();
let max_degree = m.saturating_sub(1);
let exps = monomial_exponents(nonperiodic_axes.len(), max_degree);
let n_poly = exps.len();
let mut value = Array2::<f64>::zeros((n_rows, n_centers));
let mut grad = Array3::<f64>::zeros((n_rows, n_centers, dim));
let mut hdiag = Array3::<f64>::zeros((n_rows, n_centers, dim));
for n in 0..n_rows {
for k in 0..n_centers {
let (v, g, h) = crate::basis::mixed_periodicity_additive_kernel_jet(
t.row(n),
centers.row(k),
m,
periodic_per_axis,
periods,
&axis_bounds,
)?;
value[[n, k]] = v;
for a in 0..dim {
grad[[n, k, a]] = g[a];
hdiag[[n, k, a]] = h[a];
}
}
}
let kernel_design = fast_ab(&value, &z);
let poly_design = crate::basis::mixed_periodicity_nullspace_poly_block(t, m, periodic_per_axis);
if poly_design.ncols() != n_poly {
crate::bail_dim_basis!(
"mixed-periodicity additive jets: polynomial block has {} columns but expected {n_poly}",
poly_design.ncols()
);
}
let mut phi_design = Array2::<f64>::zeros((n_rows, n_kernel + n_poly));
phi_design
.slice_mut(s![.., ..n_kernel])
.assign(&kernel_design);
if n_poly > 0 {
phi_design
.slice_mut(s![.., n_kernel..])
.assign(&poly_design);
}
let mut jet = Array3::<f64>::zeros((n_rows, n_kernel + n_poly, dim));
for axis in 0..dim {
let projected = grad.index_axis(Axis(2), axis).dot(&z);
jet.slice_mut(s![.., ..n_kernel, axis]).assign(&projected);
}
if n_poly > 0 {
for (col, alpha) in exps.iter().enumerate() {
for (local_axis, &deriv_axis) in nonperiodic_axes.iter().enumerate() {
let power = alpha[local_axis];
if power == 0 {
continue;
}
for row in 0..n_rows {
let mut val = power as f64;
for (la, &ax) in nonperiodic_axes.iter().enumerate() {
let mut e = alpha[la];
if la == local_axis {
e = e.saturating_sub(1);
}
if e != 0 {
val *= t[[row, ax]].powi(e as i32);
}
}
jet[[row, n_kernel + col, deriv_axis]] = val;
}
}
}
}
let mut hess = Array4::<f64>::zeros((n_rows, n_kernel + n_poly, dim, dim));
for a in 0..dim {
let slab = hdiag.index_axis(Axis(2), a);
let projected = slab.dot(&z);
hess.slice_mut(s![.., ..n_kernel, a, a]).assign(&projected);
}
if n_poly > 0 {
for (col, alpha) in exps.iter().enumerate() {
for (la_a, &axis_a) in nonperiodic_axes.iter().enumerate() {
let coeff_a = alpha[la_a];
if coeff_a == 0 {
continue;
}
for (la_c, &axis_c) in nonperiodic_axes.iter().enumerate() {
let coeff_c = if la_a == la_c {
alpha[la_c].saturating_sub(1)
} else {
alpha[la_c]
};
if la_a != la_c && coeff_c == 0 {
continue;
}
let lead = (coeff_a as f64) * (coeff_c as f64);
if lead == 0.0 {
continue;
}
for row in 0..n_rows {
let mut val = lead;
for (la, &ax) in nonperiodic_axes.iter().enumerate() {
let mut e = alpha[la];
if la == la_a {
e = e.saturating_sub(1);
}
if la == la_c {
e = e.saturating_sub(1);
}
if e != 0 {
val *= t[[row, ax]].powi(e as i32);
}
}
hess[[row, n_kernel + col, axis_a, axis_c]] = val;
}
}
}
}
}
Ok((phi_design, jet, hess))
}
pub fn matern_radial_first_derivative_nd(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
length_scale: f64,
nu: MaternNu,
) -> Result<Array2<f64>, BasisError> {
let n_rows = t.nrows();
let n_centers = centers.nrows();
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!(
"matern_radial_first_derivative_nd: centers must have at least one column".into(),
);
}
if t.ncols() != dim {
crate::bail_invalid_basis!(
"matern_radial_first_derivative_nd: t has {} cols but centers have {}",
t.ncols(),
dim
);
}
let mut out = Array2::<f64>::zeros((n_rows, n_centers));
for n in 0..n_rows {
for k in 0..n_centers {
let mut r2 = 0.0_f64;
for a in 0..dim {
let dv = t[[n, a]] - centers[[k, a]];
r2 += dv * dv;
}
let r = r2.sqrt();
let (_phi, phi_r, _phi_rr, _ratio) =
matern_kernel_radial_tripletwith_safe_ratio(r, length_scale, nu)?;
out[[n, k]] = phi_r;
}
}
Ok(out)
}
pub fn matern_radial_second_derivative_nd(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
length_scale: f64,
nu: MaternNu,
) -> Result<Array2<f64>, BasisError> {
let n_rows = t.nrows();
let n_centers = centers.nrows();
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!(
"matern_radial_second_derivative_nd: centers must have at least one column".into(),
);
}
if t.ncols() != dim {
crate::bail_invalid_basis!(
"matern_radial_second_derivative_nd: t has {} cols but centers have {}",
t.ncols(),
dim
);
}
let mut out = Array2::<f64>::zeros((n_rows, n_centers));
for n in 0..n_rows {
for k in 0..n_centers {
let mut r2 = 0.0_f64;
for a in 0..dim {
let dv = t[[n, a]] - centers[[k, a]];
r2 += dv * dv;
}
let r = r2.sqrt();
let (_phi, _phi_r, phi_rr, _ratio) =
matern_kernel_radial_tripletwith_safe_ratio(r, length_scale, nu)?;
out[[n, k]] = phi_rr;
}
}
Ok(out)
}
pub(crate) fn matern_metric_weights(dim: usize, aniso: Option<&[f64]>) -> Vec<f64> {
match centered_aniso_contrasts(aniso) {
Some(psi) => psi.iter().map(|&v| (2.0 * v).exp()).collect(),
None => vec![1.0; dim],
}
}
pub fn matern_input_location_jet_nd(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
length_scale: f64,
nu: MaternNu,
aniso_log_scales: Option<&[f64]>,
) -> Result<Array3<f64>, BasisError> {
let n_rows = t.nrows();
let n_centers = centers.nrows();
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!(
"matern_input_location_jet_nd: centers must have at least one column".into(),
);
}
if t.ncols() != dim {
crate::bail_invalid_basis!(
"matern_input_location_jet_nd: t has {} cols but centers have {}",
t.ncols(),
dim
);
}
if let Some(eta) = aniso_log_scales
&& eta.len() != dim
{
crate::bail_invalid_basis!(
"matern_input_location_jet_nd: aniso_log_scales len {} != dim {}",
eta.len(),
dim
);
}
let weights = matern_metric_weights(dim, aniso_log_scales);
let mut out = Array3::<f64>::zeros((n_rows, n_centers, dim));
for n in 0..n_rows {
for k in 0..n_centers {
let mut r2 = 0.0_f64;
for a in 0..dim {
let h = t[[n, a]] - centers[[k, a]];
r2 += weights[a] * h * h;
}
let r = r2.sqrt();
if r <= 0.0 {
continue;
}
let (_phi, phi_r, _phi_rr, _ratio) =
matern_kernel_radial_tripletwith_safe_ratio(r, length_scale, nu)?;
let scale = phi_r / r;
for a in 0..dim {
let h = t[[n, a]] - centers[[k, a]];
out[[n, k, a]] = scale * weights[a] * h;
}
}
}
Ok(out)
}
pub fn matern_input_location_hessian_nd(
t: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
length_scale: f64,
nu: MaternNu,
aniso_log_scales: Option<&[f64]>,
) -> Result<Array4<f64>, BasisError> {
let n_rows = t.nrows();
let n_centers = centers.nrows();
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!(
"matern_input_location_hessian_nd: centers must have at least one column".into(),
);
}
if t.ncols() != dim {
crate::bail_invalid_basis!(
"matern_input_location_hessian_nd: t has {} cols but centers have {}",
t.ncols(),
dim
);
}
if let Some(eta) = aniso_log_scales
&& eta.len() != dim
{
crate::bail_invalid_basis!(
"matern_input_location_hessian_nd: aniso_log_scales len {} != dim {}",
eta.len(),
dim
);
}
let weights = matern_metric_weights(dim, aniso_log_scales);
let mut out = Array4::<f64>::zeros((n_rows, n_centers, dim, dim));
for n in 0..n_rows {
for k in 0..n_centers {
let mut r2 = 0.0_f64;
for a in 0..dim {
let h = t[[n, a]] - centers[[k, a]];
r2 += weights[a] * h * h;
}
let r = r2.sqrt();
let (_phi, phi_r, phi_rr, phi_r_over_r) =
matern_kernel_radial_tripletwith_safe_ratio(r, length_scale, nu)?;
if r <= 0.0 {
for a in 0..dim {
out[[n, k, a, a]] = phi_r_over_r * weights[a];
}
continue;
}
let q = phi_r / r; let inv_r2 = 1.0 / r2;
for a in 0..dim {
let ga = weights[a] * (t[[n, a]] - centers[[k, a]]); for c in 0..dim {
let gc = weights[c] * (t[[n, c]] - centers[[k, c]]);
let mut value = phi_rr * (ga / r) * (gc / r);
value -= q * ga * gc * inv_r2;
if a == c {
value += q * weights[a];
}
out[[n, k, a, c]] = value;
}
}
}
}
Ok(out)
}
pub fn sphere_first_derivative_nd(
points: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
penalty_order: usize,
project_to_tangent: bool,
) -> Result<Array3<f64>, BasisError> {
let n_rows = points.nrows();
let n_centers = centers.nrows();
let dim = points.ncols();
if !(1..=4).contains(&penalty_order) {
crate::bail_invalid_basis!(
"sphere_first_derivative_nd: penalty_order must be in 1..=4; got {penalty_order}"
);
}
if dim == 0 {
crate::bail_invalid_basis!(
"sphere_first_derivative_nd: points must have at least one column".into(),
);
}
if centers.ncols() != dim {
crate::bail_invalid_basis!(
"sphere_first_derivative_nd: points have dim {} but centers have dim {}",
dim,
centers.ncols()
);
}
let tangent_projector =
project_to_tangent.then_some(crate::terms::latent::LatentManifold::Sphere { dim });
let mut out = Array3::<f64>::zeros((n_rows, n_centers, dim));
let mut ambient = Array1::<f64>::zeros(dim);
for n in 0..n_rows {
for k in 0..n_centers {
let mut cos_g = 0.0_f64;
for a in 0..dim {
cos_g += points[[n, a]] * centers[[k, a]];
}
let dk = wahba_sphere_kernel_sobolev_derivative_dcos(cos_g, penalty_order);
for a in 0..dim {
ambient[a] = dk * centers[[k, a]];
}
if let Some(manifold) = &tangent_projector {
let tangent = manifold.project_to_tangent(points.row(n), ambient.view());
for a in 0..dim {
out[[n, k, a]] = tangent[a];
}
} else {
for a in 0..dim {
out[[n, k, a]] = ambient[a];
}
}
}
}
Ok(out)
}
pub(crate) fn spherical_wahba_kernel_jet_with_kind(
data: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
penalty_order: usize,
radians: bool,
kernel: SphereWahbaKernel,
) -> Result<Array3<f64>, BasisError> {
validate_lat_lon_matrix(data, "spherical spline jet data", radians)?;
validate_lat_lon_matrix(centers, "spherical spline jet centers", radians)?;
if !(1..=4).contains(&penalty_order) {
crate::bail_invalid_basis!(
"spherical spline jet penalty_order must be one of 1, 2, 3, 4; got {penalty_order}"
);
}
let n = data.nrows();
let k = centers.nrows();
let deg = if radians {
1.0
} else {
std::f64::consts::PI / 180.0
};
let mut sin_lat_c = Vec::<f64>::with_capacity(k);
let mut cos_lat_c = Vec::<f64>::with_capacity(k);
let mut sin_lon_c = Vec::<f64>::with_capacity(k);
let mut cos_lon_c = Vec::<f64>::with_capacity(k);
for c in centers.outer_iter() {
let (s_lat, c_lat) = (c[0] * deg).sin_cos();
let (s_lon, c_lon) = (c[1] * deg).sin_cos();
sin_lat_c.push(s_lat);
cos_lat_c.push(c_lat);
sin_lon_c.push(s_lon);
cos_lon_c.push(c_lon);
}
let mut out = Array3::<f64>::zeros((n, k, 2));
let err_flag = std::sync::atomic::AtomicBool::new(false);
out.axis_chunks_iter_mut(ndarray::Axis(0), 256)
.into_par_iter()
.enumerate()
.for_each(|(chunk_idx, mut block)| {
let row_offset = chunk_idx * 256;
for (local_i, mut out_row) in block.outer_iter_mut().enumerate() {
let i = row_offset + local_i;
let (sin_lat, cos_lat) = (data[(i, 0)] * deg).sin_cos();
let (sin_lon, cos_lon) = (data[(i, 1)] * deg).sin_cos();
for j in 0..k {
let dlon_cos = cos_lon * cos_lon_c[j] + sin_lon * sin_lon_c[j];
let dlon_sin = sin_lon * cos_lon_c[j] - cos_lon * sin_lon_c[j];
let cos_gamma = sin_lat * sin_lat_c[j] + cos_lat * cos_lat_c[j] * dlon_cos;
let dk =
wahba_sphere_kernel_derivative_dcos_kind(cos_gamma, penalty_order, kernel);
let dcos_dphi = cos_lat * sin_lat_c[j] - sin_lat * cos_lat_c[j] * dlon_cos;
let dcos_dpsi = -cos_lat * cos_lat_c[j] * dlon_sin;
let dphi = dk * dcos_dphi * deg;
let dpsi = dk * dcos_dpsi * deg;
if !dphi.is_finite() || !dpsi.is_finite() {
err_flag.store(true, std::sync::atomic::Ordering::Relaxed);
return;
}
out_row[[j, 0]] = dphi;
out_row[[j, 1]] = dpsi;
}
}
});
if err_flag.load(std::sync::atomic::Ordering::Relaxed) {
crate::bail_invalid_basis!("spherical spline kernel jet produced a non-finite value");
}
Ok(out)
}
pub(crate) fn apply_identifiability_to_jet(raw_jet: &Array3<f64>, z: &Array2<f64>) -> Array3<f64> {
let n = raw_jet.shape()[0];
let k = raw_jet.shape()[1];
let kp = z.ncols();
assert_eq!(
z.nrows(),
k,
"apply_identifiability_to_jet: identifiability transform rows ({}) must match raw jet basis dim ({})",
z.nrows(),
k
);
let mut out = Array3::<f64>::zeros((n, kp, 2));
for axis in 0..2 {
let raw_axis: ndarray::ArrayView2<'_, f64> = raw_jet.index_axis(ndarray::Axis(2), axis);
let projected = raw_axis.dot(z);
out.slice_mut(ndarray::s![.., .., axis]).assign(&projected);
}
out
}
pub(crate) fn spherical_harmonic_jet(
data: ArrayView2<'_, f64>,
max_degree: usize,
radians: bool,
) -> Result<Array3<f64>, BasisError> {
validate_lat_lon_matrix(data, "spherical-harmonic jet", radians)?;
if max_degree < 1 {
crate::bail_invalid_basis!("spherical-harmonic jet max_degree must be >= 1");
}
if max_degree > 32 {
crate::bail_invalid_basis!(
"spherical-harmonic jet max_degree {max_degree} too large; cap is 32"
);
}
let n = data.nrows();
let p = max_degree * (max_degree + 2);
let deg = if radians {
1.0
} else {
std::f64::consts::PI / 180.0
};
let norms = precompute_harmonic_norms(max_degree);
let l_cap = max_degree + 1;
let mut out = Array3::<f64>::zeros((n, p, 2));
let idx = |l: usize, m: usize| l * l_cap + m;
{
let mut row_blocks = out
.axis_chunks_iter_mut(ndarray::Axis(0), 1024)
.collect::<Vec<_>>();
let chunk_size = 1024usize;
row_blocks
.par_iter_mut()
.enumerate()
.for_each(|(chunk_idx, block)| {
let mut p_buf = vec![0.0_f64; l_cap * l_cap];
let row_offset = chunk_idx * chunk_size;
for (local_i, mut out_row) in block.outer_iter_mut().enumerate() {
let i = row_offset + local_i;
let lat_raw = data[(i, 0)] * deg;
let lat =
lat_raw.clamp(-std::f64::consts::FRAC_PI_2, std::f64::consts::FRAC_PI_2);
let lon = data[(i, 1)] * deg;
let cos_lat = lat.cos();
let x = lat.sin();
let somx2 = (1.0 - x * x).max(0.0).sqrt();
let one_minus_x2 = (1.0 - x * x).max(f64::EPSILON);
for slot in p_buf.iter_mut() {
*slot = 0.0;
}
p_buf[idx(0, 0)] = 1.0;
for m in 1..=max_degree {
p_buf[idx(m, m)] = -((2 * m - 1) as f64) * somx2 * p_buf[idx(m - 1, m - 1)];
}
for m in 0..max_degree {
p_buf[idx(m + 1, m)] = ((2 * m + 1) as f64) * x * p_buf[idx(m, m)];
}
for m in 0..=max_degree {
for l in (m + 2)..=max_degree {
p_buf[idx(l, m)] = (((2 * l - 1) as f64) * x * p_buf[idx(l - 1, m)]
- ((l + m - 1) as f64) * p_buf[idx(l - 2, m)])
/ ((l - m) as f64);
}
}
let dp = |l: usize, m: usize| -> f64 {
let p_lm1 = if l >= 1 { p_buf[idx(l - 1, m)] } else { 0.0 };
(-(l as f64) * x * p_buf[idx(l, m)] + ((l + m) as f64) * p_lm1)
/ one_minus_x2
};
let (sin1, cos1) = lon.sin_cos();
let mut sin_buf = [0.0_f64; 33];
let mut cos_buf = [0.0_f64; 33];
sin_buf[0] = 0.0;
cos_buf[0] = 1.0;
if max_degree >= 1 {
sin_buf[1] = sin1;
cos_buf[1] = cos1;
}
let two_cos1 = 2.0 * cos1;
for m in 2..=max_degree {
sin_buf[m] = two_cos1 * sin_buf[m - 1] - sin_buf[m - 2];
cos_buf[m] = two_cos1 * cos_buf[m - 1] - cos_buf[m - 2];
}
let mut col = 0usize;
for l in 1..=max_degree {
for m_pos in (1..=l).rev() {
let nlm = norms[idx(l, m_pos)];
let mf = m_pos as f64;
out_row[[col, 0]] = nlm * sin_buf[m_pos] * dp(l, m_pos) * cos_lat * deg;
out_row[[col, 1]] =
nlm * mf * cos_buf[m_pos] * p_buf[idx(l, m_pos)] * deg;
col += 1;
}
let nl0 = norms[idx(l, 0)];
out_row[[col, 0]] = nl0 * dp(l, 0) * cos_lat * deg;
out_row[[col, 1]] = 0.0;
col += 1;
for m in 1..=l {
let nlm = norms[idx(l, m)];
let mf = m as f64;
out_row[[col, 0]] = nlm * cos_buf[m] * dp(l, m) * cos_lat * deg;
out_row[[col, 1]] = -nlm * mf * sin_buf[m] * p_buf[idx(l, m)] * deg;
col += 1;
}
}
}
});
}
if out.iter().any(|v| !v.is_finite()) {
crate::bail_invalid_basis!("spherical-harmonic jet produced a non-finite value");
}
Ok(out)
}
pub fn spherical_spline_design_jet(
data: ArrayView2<'_, f64>,
spec: &SphericalSplineBasisSpec,
) -> Result<Array3<f64>, BasisError> {
if matches!(spec.method, SphereMethod::Harmonic) {
let l_max = spec
.max_degree
.unwrap_or_else(|| default_spherical_harmonic_degree(data.nrows()));
if !(1..=4).contains(&spec.penalty_order) {
crate::bail_invalid_basis!(
"spherical-harmonic jet penalty_order must be one of 1, 2, 3, 4; got {}",
spec.penalty_order
);
}
return spherical_harmonic_jet(data, l_max, spec.radians);
}
if matches!(spec.wahba_kernel, SphereWahbaKernel::Pseudo) {
let l_max = spec
.max_degree
.unwrap_or_else(|| harmonic_degree_for_wahba_basis_width(spec, data.nrows()));
return spherical_harmonic_jet(data, l_max, spec.radians);
}
validate_lat_lon_matrix(data, "spherical spline jet", spec.radians)?;
let centers = match realized_center_strategy(&spec.center_strategy) {
CenterStrategy::FarthestPoint { num_centers } => {
select_spherical_farthest_point_centers(data, *num_centers, spec.radians)?
}
_ => select_centers_by_strategy(data, &spec.center_strategy)?,
};
validate_lat_lon_matrix(centers.view(), "spherical spline jet centers", spec.radians)?;
if centers.nrows() < 2 {
return Err(BasisError::InsufficientColumnsForConstraint {
found: centers.nrows(),
});
}
let center_kernel = spherical_wahba_kernel_matrix_with_kind(
centers.view(),
centers.view(),
spec.penalty_order,
spec.radians,
spec.wahba_kernel,
)?;
let decomposition =
wahba_low_degree_decomposition(centers.view(), spec.radians, center_kernel.view())?;
let raw_kernel_jet = spherical_wahba_kernel_jet_with_kind(
data,
centers.view(),
spec.penalty_order,
spec.radians,
spec.wahba_kernel,
)?;
let low_jet = if decomposition.low_degree_centers.is_some() {
Some(spherical_harmonic_jet(
data,
SPHERE_UNPENALIZED_LOW_DEGREE,
spec.radians,
)?)
} else {
None
};
let decomposed_jet =
build_wahba_decomposed_jet(&raw_kernel_jet, low_jet.as_ref(), &decomposition);
let raw_width = decomposed_jet.shape()[1];
let z = match &spec.identifiability {
SphericalSplineIdentifiability::FrozenTransform { transform } => {
if transform.nrows() != raw_width {
crate::bail_dim_basis!(
"frozen spherical identifiability transform mismatch: {} raw basis columns but transform has {} rows",
raw_width,
transform.nrows()
);
}
transform.clone()
}
SphericalSplineIdentifiability::CenterSumToZero => Array2::<f64>::eye(raw_width),
};
Ok(apply_identifiability_to_jet(&decomposed_jet, &z))
}