use super::*;
pub fn periodic_bspline_first_derivative_nd(
t: ArrayView2<'_, f64>,
data_range: (f64, f64),
degree: usize,
num_basis: usize,
) -> Result<Array3<f64>, BasisError> {
if t.ncols() != 1 {
crate::bail_invalid_basis!(
"periodic_bspline_first_derivative_nd: t must have exactly 1 column; got {}",
t.ncols()
);
}
if degree == 0 {
crate::bail_invalid_basis!("periodic_bspline_first_derivative_nd requires degree >= 1");
}
if num_basis < degree + 1 {
crate::bail_invalid_basis!(
"periodic_bspline_first_derivative_nd requires num_basis >= degree + 1 (got num_basis={num_basis}, degree={degree})"
);
}
let (start, end) = data_range;
if !(start.is_finite() && end.is_finite()) || end <= start {
crate::bail_invalid_basis!(
"periodic_bspline_first_derivative_nd: data_range must be finite and ordered, got {data_range:?}"
);
}
let period = end - start;
let n_rows = t.nrows();
let t_col = t.column(0);
let mut phi = vec![0.0_f64; num_basis];
let mut dphi = vec![0.0_f64; num_basis];
let mut out = Array3::<f64>::zeros((n_rows, num_basis, 1));
for row in 0..n_rows {
let xi = t_col[row];
if !xi.is_finite() {
crate::bail_invalid_basis!(
"periodic_bspline_first_derivative_nd: non-finite latent at row {row}"
);
}
let rowsum =
fill_periodic_bspline_unnormalized_value_row(xi, start, period, degree, &mut phi);
if !rowsum.is_finite() || rowsum <= 0.0 {
crate::bail_invalid_basis!(
"periodic_bspline_first_derivative_nd: non-positive rowsum at row {row}: {rowsum}"
);
}
let rowsum_derivative =
fill_periodic_bspline_unnormalized_derivative_row(xi, start, period, degree, &mut dphi);
if !rowsum_derivative.is_finite() {
crate::bail_invalid_basis!(
"periodic_bspline_first_derivative_nd: non-finite rowsum derivative at row {row}: {rowsum_derivative}"
);
}
let rowsum_squared = rowsum * rowsum;
for i in 0..num_basis {
out[[row, i, 0]] = dphi[i] / rowsum - phi[i] * rowsum_derivative / rowsum_squared;
}
}
Ok(out)
}
pub fn bspline_tensor_first_derivative(
t: ArrayView2<'_, f64>,
knots_per_axis: &[ArrayView1<'_, f64>],
degrees: &[usize],
) -> Result<Array3<f64>, BasisError> {
let n_axes = t.ncols();
if knots_per_axis.len() != n_axes || degrees.len() != n_axes {
crate::bail_invalid_basis!(
"bspline_tensor_first_derivative: t has {n_axes} axes but received \
{} knot vectors and {} degrees",
knots_per_axis.len(),
degrees.len(),
);
}
if n_axes == 0 {
crate::bail_invalid_basis!(
"bspline_tensor_first_derivative: t must have at least one axis".into(),
);
}
let n_rows = t.nrows();
let mut k_per_axis = Vec::<usize>::with_capacity(n_axes);
let mut total = 1usize;
for a in 0..n_axes {
let k = knots_per_axis[a]
.len()
.checked_sub(degrees[a] + 1)
.ok_or_else(|| {
BasisError::InvalidInput(format!(
"bspline_tensor_first_derivative: axis {a} knot vector too short \
for degree {}",
degrees[a]
))
})?;
k_per_axis.push(k);
total = total.checked_mul(k).ok_or_else(|| {
BasisError::InvalidInput(
"bspline_tensor_first_derivative: tensor-product basis size overflow".into(),
)
})?;
}
let mut out = Array3::<f64>::zeros((n_rows, total, n_axes));
let mut values_per_axis: Vec<Vec<f64>> = k_per_axis.iter().map(|&k| vec![0.0; k]).collect();
let mut derivs_per_axis: Vec<Vec<f64>> = k_per_axis.iter().map(|&k| vec![0.0; k]).collect();
let mut value_scratch_per_axis: Vec<internal::BsplineScratch> = degrees
.iter()
.map(|&d| internal::BsplineScratch::new(d))
.collect();
let mut lower_basis_per_axis: Vec<Vec<f64>> = knots_per_axis
.iter()
.zip(degrees.iter())
.map(|(knots, &d)| vec![0.0; knots.len().saturating_sub(d)])
.collect();
let mut lower_scratch_per_axis: Vec<internal::BsplineScratch> = degrees
.iter()
.map(|&d| internal::BsplineScratch::new(d.saturating_sub(1)))
.collect();
let mut idx = vec![0usize; n_axes];
let mut prefix = vec![1.0; n_axes + 1];
let mut suffix = vec![1.0; n_axes + 1];
for n in 0..n_rows {
for a in 0..n_axes {
internal::evaluate_splines_at_point_into(
t[[n, a]],
degrees[a],
knots_per_axis[a],
&mut values_per_axis[a],
&mut value_scratch_per_axis[a],
);
evaluate_bspline_derivative_scalar_into(
t[[n, a]],
knots_per_axis[a],
degrees[a],
&mut derivs_per_axis[a],
&mut lower_basis_per_axis[a],
&mut lower_scratch_per_axis[a],
)?;
}
for k in 0..total {
let mut rem = k;
for a in (0..n_axes).rev() {
idx[a] = rem % k_per_axis[a];
rem /= k_per_axis[a];
}
prefix[0] = 1.0;
for a in 0..n_axes {
prefix[a + 1] = prefix[a] * values_per_axis[a][idx[a]];
}
suffix[n_axes] = 1.0;
for a in (0..n_axes).rev() {
suffix[a] = suffix[a + 1] * values_per_axis[a][idx[a]];
}
for axis in 0..n_axes {
let leave_one_out = prefix[axis] * suffix[axis + 1];
out[[n, k, axis]] = derivs_per_axis[axis][idx[axis]] * leave_one_out;
}
}
}
Ok(out)
}
#[inline]
pub(crate) fn periodic_distance_1d(x: f64, c: f64, period: f64) -> f64 {
let dx = (x - c).rem_euclid(period).abs();
dx.min(period - dx).abs()
}
pub(crate) fn even_bernoulli_polynomial(degree: usize, t: f64) -> Result<f64, BasisError> {
let t2 = t * t;
match degree {
2 => Ok(t2 - t + 1.0 / 6.0),
4 => Ok(t2 * t2 - 2.0 * t2 * t + t2 - 1.0 / 30.0),
6 => {
let t4 = t2 * t2;
let t6 = t4 * t2;
Ok(t6 - 3.0 * t4 * t + 2.5 * t4 - 0.5 * t2 + 1.0 / 42.0)
}
8 => {
let t4 = t2 * t2;
let t6 = t4 * t2;
let t8 = t4 * t4;
Ok(
t8 - 4.0 * t6 * t + (14.0 / 3.0) * t6 - (7.0 / 3.0) * t4 + (2.0 / 3.0) * t2
- 1.0 / 30.0,
)
}
other => Err(BasisError::InvalidInput(format!(
"periodic Duchon Bernoulli kernel only implemented for B_{{2m}} with m ∈ {{1, 2, 3, 4}}; got degree {other}"
))),
}
}
pub(crate) fn periodic_duchon_kernel_bernoulli(
r: f64,
m: usize,
period: f64,
) -> Result<f64, BasisError> {
if !period.is_finite() || period <= 0.0 {
crate::bail_invalid_basis!(
"periodic Duchon kernel requires positive finite period; got {period}"
);
}
if m == 0 {
crate::bail_invalid_basis!("periodic Duchon order m must be at least 1");
}
let t = (r / period).rem_euclid(1.0);
let sign = if m % 2 == 1 { 1.0 } else { -1.0 };
Ok(sign * even_bernoulli_polynomial(2 * m, t)?)
}
pub(crate) fn even_bernoulli_polynomial_derivatives(
degree: usize,
s: f64,
) -> Result<(f64, f64), BasisError> {
let s2 = s * s;
match degree {
2 => Ok((2.0 * s - 1.0, 2.0)),
4 => {
let d1 = 4.0 * s2 * s - 6.0 * s2 + 2.0 * s;
let d2 = 12.0 * s2 - 12.0 * s + 2.0;
Ok((d1, d2))
}
6 => {
let s3 = s2 * s;
let s4 = s2 * s2;
let s5 = s4 * s;
let d1 = 6.0 * s5 - 15.0 * s4 + 10.0 * s3 - s;
let d2 = 30.0 * s4 - 60.0 * s3 + 30.0 * s2 - 1.0;
Ok((d1, d2))
}
8 => {
let s3 = s2 * s;
let s4 = s2 * s2;
let s5 = s4 * s;
let s6 = s4 * s2;
let s7 = s6 * s;
let d1 = 8.0 * s7 - 28.0 * s6 + 28.0 * s5 - (28.0 / 3.0) * s3 + (4.0 / 3.0) * s;
let d2 = 56.0 * s6 - 168.0 * s5 + 140.0 * s4 - 28.0 * s2 + 4.0 / 3.0;
Ok((d1, d2))
}
other => Err(BasisError::InvalidInput(format!(
"periodic Duchon Bernoulli kernel derivative only implemented for B_{{2m}} with m ∈ {{1, 2, 3, 4}}; got degree {other}"
))),
}
}
pub(crate) fn periodic_duchon_kernel_bernoulli_triplet(
r: f64,
m: usize,
period: f64,
) -> Result<(f64, f64, f64), BasisError> {
if !period.is_finite() || period <= 0.0 {
crate::bail_invalid_basis!(
"periodic Duchon kernel requires positive finite period; got {period}"
);
}
if m == 0 {
crate::bail_invalid_basis!("periodic Duchon order m must be at least 1");
}
let s = (r / period).rem_euclid(1.0);
let sign = if m % 2 == 1 { 1.0 } else { -1.0 };
let phi = sign * even_bernoulli_polynomial(2 * m, s)?;
let (b1, b2) = even_bernoulli_polynomial_derivatives(2 * m, s)?;
let dphi_dr = sign * b1 / period;
let d2phi_dr2 = sign * b2 / (period * period);
Ok((phi, dphi_dr, d2phi_dr2))
}
pub(crate) fn collapse_periodic_endpoint(
centers: Array2<f64>,
left: f64,
period: f64,
) -> Array2<f64> {
if period <= 0.0 || !period.is_finite() {
return centers;
}
let tol = period.max(1.0) * 1.0e-10;
let col = centers.column(0);
let n_rows = col.len();
let mut seen_left = false;
let keep: Vec<usize> = (0..n_rows)
.filter(|&i| {
if periodic_distance_1d(col[i], left, period) <= tol {
if seen_left {
return false;
}
seen_left = true;
}
true
})
.collect();
if keep.len() == n_rows {
return centers;
}
let mut trimmed = Array2::<f64>::zeros((keep.len(), centers.ncols()));
for (out_row, &src_row) in keep.iter().enumerate() {
for c in 0..centers.ncols() {
trimmed[[out_row, c]] = centers[[src_row, c]];
}
}
trimmed
}
pub(crate) fn build_periodic_duchon_basis_1d(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
centers: Array2<f64>,
workspace: &mut BasisWorkspace,
) -> Result<BasisBuildResult, BasisError> {
if data.ncols() != 1 {
crate::bail_invalid_basis!(
"periodic Duchon smooths currently require exactly one covariate"
);
}
let explicit_period = spec
.periodic
.as_ref()
.and_then(|axes| axes.first().copied().flatten());
let (centers, left, period) =
prepare_periodic_duchon_centers_1d_with_period(centers, explicit_period)?;
let user_m = duchon_p_from_nullspace_order(spec.nullspace_order);
let effective_nullspace_order = DuchonNullspaceOrder::Zero;
let p_order = duchon_p_from_nullspace_order(effective_nullspace_order);
let s_order = spec.power_as_usize();
validate_duchon_kernel_orders(spec.length_scale, p_order, s_order as f64, 1)?;
let z = kernel_constraint_nullspace(
centers.view(),
effective_nullspace_order,
&mut workspace.cache,
)?;
let kernel_cols = z.ncols();
let mut basis = Array2::<f64>::zeros((data.nrows(), kernel_cols + 1));
let coeffs = spec
.length_scale
.map(|ls| duchon_partial_fraction_coeffs(p_order, s_order, 1.0 / ls.max(1e-300)));
let pure_poly_coeff = if spec.length_scale.is_none() {
Some(PolyharmonicBlockCoeff::new(
(pure_duchon_block_order(p_order, s_order as f64)) as f64,
1,
))
} else {
None
};
let kernel_amp = duchon_kernel_amplification(
centers.view(),
spec.length_scale,
p_order,
s_order,
1,
None,
coeffs.as_ref(),
pure_poly_coeff.as_ref(),
);
let centers_col0: Vec<f64> = centers.column(0).to_vec();
let n_data = data.nrows();
let k_centers = centers_col0.len();
let len_scale = spec.length_scale;
let mut raw_kernel = Array2::<f64>::zeros((n_data, k_centers));
let err_flag = std::sync::atomic::AtomicBool::new(false);
let amp = kernel_amp;
if pure_poly_coeff.is_some() {
raw_kernel
.axis_chunks_iter_mut(ndarray::Axis(0), 1024)
.into_par_iter()
.enumerate()
.for_each(|(chunk_idx, mut block)| {
let row_offset = chunk_idx * 1024;
for (local_i, mut out_row) in block.outer_iter_mut().enumerate() {
let i = row_offset + local_i;
let x = wrap_to_period(data[[i, 0]], left, period);
for j in 0..k_centers {
let r = periodic_distance_1d(x, centers_col0[j], period);
match periodic_duchon_kernel_bernoulli(r, user_m, period) {
Ok(v) => out_row[j] = v,
Err(_) => {
err_flag.store(true, std::sync::atomic::Ordering::Relaxed);
return;
}
}
}
}
});
} else {
raw_kernel
.axis_chunks_iter_mut(ndarray::Axis(0), 1024)
.into_par_iter()
.enumerate()
.for_each(|(chunk_idx, mut block)| {
let row_offset = chunk_idx * 1024;
for (local_i, mut out_row) in block.outer_iter_mut().enumerate() {
let i = row_offset + local_i;
let x = wrap_to_period(data[[i, 0]], left, period);
for j in 0..k_centers {
let r = periodic_distance_1d(x, centers_col0[j], period);
match duchon_matern_kernel_general_from_distance(
r,
len_scale,
p_order,
s_order,
1,
coeffs.as_ref(),
) {
Ok(v) => out_row[j] = v * amp,
Err(_) => {
err_flag.store(true, std::sync::atomic::Ordering::Relaxed);
return;
}
}
}
}
});
}
if err_flag.load(std::sync::atomic::Ordering::Relaxed) {
crate::bail_invalid_basis!("periodic Duchon kernel evaluation produced a non-finite value");
}
let design_kernel = fast_ab(&raw_kernel, &z);
basis
.slice_mut(s![.., 0..kernel_cols])
.assign(&design_kernel);
basis.column_mut(kernel_cols).fill(1.0);
let mut center_kernel = Array2::<f64>::zeros((centers.nrows(), centers.nrows()));
fill_symmetric_from_row_kernel(&mut center_kernel, |i, j| {
let r = periodic_distance_1d(centers[[i, 0]], centers[[j, 0]], period);
if pure_poly_coeff.is_some() {
periodic_duchon_kernel_bernoulli(r, user_m, period)
} else {
Ok(duchon_matern_kernel_general_from_distance(
r,
spec.length_scale,
p_order,
s_order,
1,
coeffs.as_ref(),
)? * kernel_amp)
}
})?;
let omega = fast_ab(&fast_atb(&z, ¢er_kernel), &z);
let mut penalty = Array2::<f64>::zeros((basis.ncols(), basis.ncols()));
penalty
.slice_mut(s![0..kernel_cols, 0..kernel_cols])
.assign(&omega);
let base_design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(basis));
let identifiability_transform = spatial_identifiability_transform_from_design_matrix(
data,
&base_design,
&spec.identifiability,
"periodic Duchon",
)?;
let (design, primary) = if let Some(transform) = identifiability_transform.as_ref() {
let design = wrap_dense_design_with_transform(base_design, transform, "periodic Duchon")?;
let transformed = fast_ab(&fast_atb(transform, &penalty), transform);
(design, transformed)
} else {
(base_design, penalty)
};
let candidates = vec![normalize_penalty_candidate(
primary,
1,
PenaltySource::Primary,
)];
let (penalties, nullspace_dims, penaltyinfo, null_eigenvectors, ops) =
filter_active_penalty_candidates_with_ops(candidates)?;
Ok(BasisBuildResult {
design,
penalties,
nullspace_dims,
penaltyinfo,
ops,
null_eigenvectors,
joint_null_rotation: None,
metadata: BasisMetadata::Duchon {
centers,
length_scale: spec.length_scale,
periodic: Some(vec![Some(period)]),
power: spec.power,
nullspace_order: effective_nullspace_order,
identifiability_transform,
input_scales: None,
aniso_log_scales: None,
operator_collocation_points: None,
},
kronecker_factored: None,
})
}
#[inline]
pub(crate) fn duchon_mixed_periodicity_distance(
x: ArrayView1<'_, f64>,
y: ArrayView1<'_, f64>,
periodic_per_axis: &[bool],
periods: &[f64],
) -> f64 {
let d = x.len();
assert_eq!(d, y.len());
assert_eq!(d, periodic_per_axis.len());
assert_eq!(d, periods.len());
let mut acc = 0.0_f64;
for j in 0..d {
let delta = if periodic_per_axis[j] {
let p = periods[j];
(p / std::f64::consts::PI) * (std::f64::consts::PI * (x[j] - y[j]) / p).sin()
} else {
x[j] - y[j]
};
acc += delta * delta;
}
acc.sqrt()
}
pub(crate) fn build_duchon_basis_mixed_periodicity(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
centers: Array2<f64>,
periodic_per_axis: &[bool],
periods: &[f64],
workspace: &mut BasisWorkspace,
) -> Result<BasisBuildResult, BasisError> {
let d = data.ncols();
if d == 0 {
crate::bail_invalid_basis!("Duchon basis requires at least one covariate dimension");
}
if periodic_per_axis.len() != d {
crate::bail_invalid_basis!(
"periodic_per_axis must have length d={d}, got {}",
periodic_per_axis.len()
);
}
if periods.len() != d {
crate::bail_invalid_basis!("periods must have length d={d}, got {}", periods.len());
}
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 centers.ncols() != d {
crate::bail_invalid_basis!(
"centers ncols={} does not match data ncols={d}",
centers.ncols()
);
}
if spec.length_scale.is_some() {
crate::bail_invalid_basis!(
"mixed-periodicity Duchon basis currently only supports the pure polyharmonic spectrum (length_scale=None)"
);
}
if spec.power != 0.0 {
crate::bail_invalid_basis!(
"mixed-periodicity Duchon basis currently requires power = 0 (pure polyharmonic); got power={}",
spec.power
);
}
let user_m = duchon_p_from_nullspace_order(spec.nullspace_order);
let effective_nullspace_order = DuchonNullspaceOrder::Zero;
let p_order = duchon_p_from_nullspace_order(effective_nullspace_order);
let s_order_int = 0usize;
validate_duchon_kernel_orders(None, p_order, s_order_int as f64, d)?;
let z = kernel_constraint_nullspace(
centers.view(),
effective_nullspace_order,
&mut workspace.cache,
)?;
let kernel_cols = z.ncols();
let m_kernel = pure_duchon_block_order(user_m, s_order_int as f64);
let ppc = PolyharmonicBlockCoeff::new(m_kernel, d);
let centers_owned = centers.clone();
let k_centers = centers_owned.nrows();
let n_data = data.nrows();
let mut raw_kernel = Array2::<f64>::zeros((n_data, k_centers));
raw_kernel
.axis_chunks_iter_mut(ndarray::Axis(0), 1024)
.into_par_iter()
.enumerate()
.for_each(|(chunk_idx, mut block)| {
let row_offset = chunk_idx * 1024;
for (local_i, mut out_row) in block.outer_iter_mut().enumerate() {
let i = row_offset + local_i;
let x_row = data.row(i);
for j in 0..k_centers {
let c_row = centers_owned.row(j);
let r =
duchon_mixed_periodicity_distance(x_row, c_row, periodic_per_axis, periods);
out_row[j] = ppc.eval(r);
}
}
});
let design_kernel = fast_ab(&raw_kernel, &z);
let mut basis = Array2::<f64>::zeros((n_data, kernel_cols + 1));
basis
.slice_mut(s![.., 0..kernel_cols])
.assign(&design_kernel);
basis.column_mut(kernel_cols).fill(1.0);
let mut center_kernel = Array2::<f64>::zeros((k_centers, k_centers));
fill_symmetric_from_row_kernel(&mut center_kernel, |i, j| {
let r = duchon_mixed_periodicity_distance(
centers_owned.row(i),
centers_owned.row(j),
periodic_per_axis,
periods,
);
Ok(ppc.eval(r))
})?;
let omega = fast_ab(&fast_atb(&z, ¢er_kernel), &z);
let mut penalty = Array2::<f64>::zeros((basis.ncols(), basis.ncols()));
penalty
.slice_mut(s![0..kernel_cols, 0..kernel_cols])
.assign(&omega);
let base_design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(basis));
let identifiability_transform = spatial_identifiability_transform_from_design_matrix(
data,
&base_design,
&spec.identifiability,
"mixed-periodicity Duchon",
)?;
let (design, primary) = if let Some(transform) = identifiability_transform.as_ref() {
let design =
wrap_dense_design_with_transform(base_design, transform, "mixed-periodicity Duchon")?;
let transformed = fast_ab(&fast_atb(transform, &penalty), transform);
(design, transformed)
} else {
(base_design, penalty)
};
let candidates = vec![normalize_penalty_candidate(
primary,
1,
PenaltySource::Primary,
)];
let (penalties, nullspace_dims, penaltyinfo, null_eigenvectors, ops) =
filter_active_penalty_candidates_with_ops(candidates)?;
Ok(BasisBuildResult {
design,
penalties,
nullspace_dims,
penaltyinfo,
ops,
null_eigenvectors,
joint_null_rotation: None,
metadata: BasisMetadata::Duchon {
centers: centers_owned,
length_scale: None,
periodic: Some(
periodic_per_axis
.iter()
.zip(periods.iter())
.map(|(&is_periodic, &period)| if is_periodic { Some(period) } else { None })
.collect(),
),
power: spec.power,
nullspace_order: effective_nullspace_order,
identifiability_transform,
input_scales: None,
aniso_log_scales: None,
operator_collocation_points: None,
},
kronecker_factored: None,
})
}
pub fn build_duchon_basis_mixed_periodicity_auto(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
periodic_per_axis: &[bool],
periods: Option<&[f64]>,
) -> Result<BasisBuildResult, BasisError> {
let mut workspace = BasisWorkspace::default();
let centers = select_centers_by_strategy(data, &spec.center_strategy)?;
assert_spatial_centers_below_large_scale_cap(data.ncols(), centers.view())?;
let d = data.ncols();
if periodic_per_axis.len() != d {
crate::bail_invalid_basis!(
"periodic_per_axis must have length d={d}, got {}",
periodic_per_axis.len()
);
}
let resolved_periods: Vec<f64> = match periods {
Some(p) => {
if p.len() != d {
crate::bail_invalid_basis!("periods must have length d={d}, got {}", p.len());
}
p.to_vec()
}
None => {
let mut out = vec![1.0_f64; d];
for j in 0..d {
if periodic_per_axis[j] {
let col = centers.column(j);
let left = col.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let right = col.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
if !left.is_finite() || !right.is_finite() || left >= right {
return Err(BasisError::InvalidRange(left, right));
}
out[j] = right - left;
}
}
out
}
};
if d == 1 && periodic_per_axis[0] {
let mut periodic_spec = spec.clone();
periodic_spec.periodic = Some(vec![Some(resolved_periods[0])]);
return build_periodic_duchon_basis_1d(data, &periodic_spec, centers, &mut workspace);
}
build_duchon_basis_mixed_periodicity(
data,
spec,
centers,
periodic_per_axis,
&resolved_periods,
&mut workspace,
)
}
pub fn duchon_cubic_default(dim: usize) -> (DuchonNullspaceOrder, f64) {
(DuchonNullspaceOrder::Linear, (dim as f64 - 1.0) / 2.0)
}
pub(crate) fn duchon_native_penalty_candidates(
centers: ArrayView2<'_, f64>,
length_scale: Option<f64>,
power: f64,
nullspace_order: DuchonNullspaceOrder,
aniso_log_scales: Option<&[f64]>,
kernel_transform: &Array2<f64>,
outer_identifiability: Option<&Array2<f64>>,
poly_cols: usize,
) -> Result<Vec<PenaltyCandidate>, BasisError> {
let dim = centers.ncols();
if dim == 0 {
crate::bail_invalid_basis!(
"duchon_native_penalty_candidates: centers must have at least one column"
);
}
let k = centers.nrows();
let z = kernel_transform;
let n_kernel = z.ncols();
let p_order = duchon_p_from_nullspace_order(nullspace_order);
let s_int = duchon_power_to_usize(power);
let pure = length_scale.is_none();
let pure_poly_coeff = if pure {
Some(PolyharmonicBlockCoeff::new(
pure_duchon_block_order(p_order, power),
dim,
))
} else {
None
};
let coeffs =
length_scale.map(|ls| duchon_partial_fraction_coeffs(p_order, s_int, 1.0 / ls.max(1e-300)));
let kernel_amp = duchon_kernel_amplification(
centers,
length_scale,
p_order,
s_int,
dim,
aniso_log_scales,
coeffs.as_ref(),
pure_poly_coeff.as_ref(),
);
let axis_scales = aniso_log_scales.map(aniso_axis_scales);
let mut center_kernel = Array2::<f64>::zeros((k, k));
fill_symmetric_from_row_kernel(&mut center_kernel, |i, j| {
let r = if let Some(scales) = axis_scales.as_deref() {
aniso_distance_rows_with_scales(centers, i, centers, j, scales)
} else {
euclidean_distance_rows(centers, i, centers, j)
};
if let Some(ppc) = pure_poly_coeff.as_ref() {
Ok(ppc.eval(r))
} else {
duchon_matern_kernel_general_from_distance(
r,
length_scale,
p_order,
s_int,
dim,
coeffs.as_ref(),
)
}
})?;
let amp2 = kernel_amp * kernel_amp;
let omega = {
let zt_k = fast_atb(z, ¢er_kernel);
fast_ab(&zt_k, z).mapv(|v| v * amp2)
};
let n_pre = n_kernel + poly_cols;
let mut primary_pre = Array2::<f64>::zeros((n_pre, n_pre));
primary_pre
.slice_mut(s![..n_kernel, ..n_kernel])
.assign(&omega);
let primary = symmetrize(&project_penalty_matrix(&primary_pre, outer_identifiability));
let shrink = if poly_cols > 1 {
let mut shrink_pre = Array2::<f64>::zeros((n_pre, n_pre));
for col in (n_kernel + 1)..n_pre {
shrink_pre[[col, col]] = 1.0;
}
let shrink = symmetrize(&project_penalty_matrix(&shrink_pre, outer_identifiability));
Some(shrink)
} else {
None
};
let mut out = Vec::new();
out.push(normalize_penalty_candidate(
primary,
0,
PenaltySource::Primary,
));
if let Some(shrink) = shrink {
out.push(normalize_penalty_candidate(
shrink,
0,
PenaltySource::DoublePenaltyNullspace,
));
}
Ok(out)
}
pub(crate) const DUCHON_COLLOCATION_OVERSAMPLE: usize = 3;
pub(crate) fn duchon_operator_penalty_candidates(
collocation_points: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
operator_penalties: &DuchonOperatorPenaltySpec,
length_scale: Option<f64>,
power: f64,
nullspace_order: DuchonNullspaceOrder,
per_axis_relevance: bool,
identifiability_transform: Option<&Array2<f64>>,
workspace: &mut BasisWorkspace,
) -> Result<Vec<PenaltyCandidate>, BasisError> {
let want_mass = matches!(operator_penalties.mass, OperatorPenaltySpec::Active { .. });
let mut want_tension = matches!(
operator_penalties.tension,
OperatorPenaltySpec::Active { .. }
);
let mut want_stiffness = matches!(
operator_penalties.stiffness,
OperatorPenaltySpec::Active { .. }
);
let effective_order = duchon_effective_nullspace_order(centers, nullspace_order);
let p_order = duchon_p_from_nullspace_order(effective_order);
let dim = centers.ncols();
let two_pps = 2.0 * (p_order as f64 + power);
want_tension = want_tension && two_pps > dim as f64 + 1.0;
want_stiffness = want_stiffness && two_pps > dim as f64 + 2.0;
if !want_mass && !want_tension && !want_stiffness {
return Ok(Vec::new());
}
let mut effective_spec = operator_penalties.clone();
if !want_tension {
effective_spec.tension = OperatorPenaltySpec::Disabled;
}
if !want_stiffness {
effective_spec.stiffness = OperatorPenaltySpec::Disabled;
}
let max_op = duchon_max_active_operator_derivative_order(&effective_spec);
let ops = build_duchon_collocation_operator_matriceswithworkspace(
centers,
collocation_points,
None,
length_scale,
power,
nullspace_order,
None,
identifiability_transform.map(|t| t.view()),
max_op,
workspace,
)?;
let kernel_nullspace = ops.kernel_nullspace_transform.as_ref();
let poly_cols = ops.polynomial_block_cols;
let split_tension = per_axis_relevance && want_tension;
let factory_spec = if split_tension {
let mut spec = effective_spec.clone();
spec.tension = OperatorPenaltySpec::Disabled;
spec
} else {
effective_spec
};
let mut candidates = if let Some(length_scale) = length_scale {
operator_penalty_candidates_closed_form(
centers,
&ops.d0,
&ops.d1,
&ops.d2,
&factory_spec,
p_order,
duchon_power_to_usize(power),
length_scale,
None,
kernel_nullspace,
poly_cols,
identifiability_transform,
)
} else {
operator_penalty_candidates_closed_form_pure(
centers,
&ops.d0,
&ops.d1,
&ops.d2,
&factory_spec,
p_order,
power,
None,
kernel_nullspace,
poly_cols,
identifiability_transform,
)
};
if split_tension {
for axis in 0..dim {
let d1_axis = ops.d1.slice(s![axis..; dim, ..]).to_owned();
candidates.push(normalize_penalty_candidate(
symmetrize(&fast_ata(&d1_axis)),
0,
PenaltySource::OperatorRelevance { axis },
));
}
}
Ok(candidates)
}