use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use faer::Side;
use crate::linalg::faer_ndarray::FaerEigh;
use super::{BasisError, MeasureJetBand};
pub(crate) const PROFILE_CUTOFF: f64 = 3.0;
pub(crate) const PSEUDOINVERSE_RTOL: f64 = 64.0 * f64::EPSILON;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LIndex {
pub row: usize,
pub col: usize,
}
pub struct MeasureJetAnisotropyJets {
pub q: Array2<f64>,
pub indices: Vec<LIndex>,
pub d_first: Vec<Array2<f64>>,
pub d_second: Vec<Array2<f64>>,
}
impl MeasureJetAnisotropyJets {
#[inline]
pub fn n_active(&self) -> usize {
self.indices.len()
}
#[inline]
pub fn second(&self, a: usize, b: usize) -> &Array2<f64> {
&self.d_second[a * self.indices.len() + b]
}
}
pub fn lower_triangular_indices(d: usize) -> Vec<LIndex> {
let mut idx = Vec::with_capacity(d * (d + 1) / 2);
for col in 0..d {
for row in col..d {
idx.push(LIndex { row, col });
}
}
idx
}
pub(crate) struct NormalizedFactor {
pub(crate) m: Array2<f64>,
pub(crate) dm: Vec<Array2<f64>>,
pub(crate) d2m: Vec<Array2<f64>>,
}
pub(crate) fn build_normalized_factor(
l: ArrayView2<'_, f64>,
indices: &[LIndex],
) -> Result<NormalizedFactor, BasisError> {
let d = l.nrows();
if l.ncols() != d {
crate::bail_dim_basis!(
"measure-jet anisotropy needs a square lower-triangular L, got {:?}",
l.dim()
);
}
if d == 0 {
crate::bail_invalid_basis!("measure-jet anisotropy needs a non-empty ambient metric");
}
for k in 0..d {
if !(l[(k, k)].is_finite() && l[(k, k)] > 0.0) {
crate::bail_invalid_basis!(
"measure-jet anisotropy needs a positive-definite L: diagonal entry L[{k},{k}] = {} is not finite and positive",
l[(k, k)]
);
}
for c in (k + 1)..d {
if l[(k, c)] != 0.0 {
crate::bail_invalid_basis!(
"measure-jet anisotropy L must be lower-triangular: upper entry L[{k},{c}] = {} is nonzero",
l[(k, c)]
);
}
if !l[(c, k)].is_finite() {
crate::bail_invalid_basis!(
"measure-jet anisotropy L has a non-finite entry L[{c},{k}]"
);
}
}
}
let n = indices.len();
let l_owned = l.to_owned();
let inv_d = 1.0 / d as f64;
let mut f_first = vec![0.0_f64; n];
let mut f_second = vec![0.0_f64; n * n];
for (a, ia) in indices.iter().enumerate() {
if ia.row == ia.col {
let lkk = l_owned[(ia.row, ia.row)];
f_first[a] = -inv_d / lkk;
f_second[a * n + a] = inv_d / (lkk * lkk);
}
}
let g = (-inv_d * {
let mut s = 0.0;
for k in 0..d {
s += l_owned[(k, k)].ln();
}
s
})
.exp();
let mut g_first = vec![0.0_f64; n];
let mut g_second = vec![0.0_f64; n * n];
for a in 0..n {
g_first[a] = g * f_first[a];
}
for a in 0..n {
for b in 0..n {
g_second[a * n + b] = g * (f_first[a] * f_first[b] + f_second[a * n + b]);
}
}
let m = &l_owned * g;
let mut dm = Vec::with_capacity(n);
for a in 0..n {
let ia = indices[a];
let mut ma = &l_owned * g_first[a];
ma[(ia.row, ia.col)] += g;
dm.push(ma);
}
let mut d2m = Vec::with_capacity(n * n);
for a in 0..n {
let ia = indices[a];
for b in 0..n {
let ib = indices[b];
let mut mab = &l_owned * g_second[a * n + b];
mab[(ia.row, ia.col)] += g_first[b];
mab[(ib.row, ib.col)] += g_first[a];
d2m.push(mab);
}
}
Ok(NormalizedFactor { m, dm, d2m })
}
pub(crate) struct MetricDist2 {
pub(crate) dm2: Array2<f64>,
}
pub(crate) fn metric_sq_dists(centers: ArrayView2<'_, f64>, m: ArrayView2<'_, f64>) -> MetricDist2 {
let n = centers.nrows();
let y = centers.dot(&m);
let yn: Vec<f64> = y.outer_iter().map(|r| r.dot(&r)).collect();
let g = y.dot(&y.t());
let mut dm2 = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
dm2[(i, j)] = (yn[i] + yn[j] - 2.0 * g[(i, j)]).max(0.0);
}
}
MetricDist2 { dm2 }
}
pub(crate) struct EighPinv {
pub(crate) evals: Array1<f64>,
pub(crate) evecs: Array2<f64>,
pub(crate) inv: Array1<f64>,
pub(crate) pinv: Array2<f64>,
}
pub(crate) fn eigh_pinv(a: &Array2<f64>, label: &str) -> Result<EighPinv, BasisError> {
let n = a.nrows();
let (evals, evecs) = a.eigh(Side::Lower).map_err(|e| {
BasisError::InvalidInput(format!(
"measure-jet anisotropy pseudo-inverse `{label}` eigendecomposition failed: {e}"
))
})?;
let lam_max = evals.iter().fold(0.0_f64, |acc, v| acc.max((*v).max(0.0)));
let rank_tol = PSEUDOINVERSE_RTOL * (n.max(1) as f64) * lam_max;
let mut inv = Array1::<f64>::zeros(n);
let mut scaled = evecs.clone();
for k in 0..n {
let lam = evals[k].max(0.0);
let iv = if lam > rank_tol { 1.0 / lam } else { 0.0 };
inv[k] = iv;
let mut col = scaled.column_mut(k);
col.mapv_inplace(|v| v * iv);
}
let pinv = scaled.dot(&evecs.t());
Ok(EighPinv {
evals,
evecs,
inv,
pinv,
})
}
pub(crate) fn pinv_first_deriv(ep: &EighPinv, gdot: &Array2<f64>) -> Array2<f64> {
let n = ep.evals.len();
let vt_g = ep.evecs.t().dot(gdot);
let mhat = vt_g.dot(&ep.evecs); let mut core = Array2::<f64>::zeros((n, n));
for p in 0..n {
for q in 0..n {
core[(p, q)] = pinv_div1(ep, p, q) * mhat[(p, q)];
}
}
ep.evecs.dot(&core).dot(&ep.evecs.t())
}
#[inline]
pub(crate) fn pinv_active(ep: &EighPinv, i: usize) -> bool {
ep.inv[i] != 0.0
}
#[inline]
pub(crate) fn pinv_value(ep: &EighPinv, i: usize) -> f64 {
if pinv_active(ep, i) {
ep.inv[i]
} else {
0.0
}
}
#[inline]
pub(crate) fn pinv_prime(ep: &EighPinv, i: usize) -> f64 {
if pinv_active(ep, i) {
-ep.inv[i] * ep.inv[i]
} else {
0.0
}
}
#[inline]
pub(crate) fn pinv_half_second(ep: &EighPinv, i: usize) -> f64 {
if pinv_active(ep, i) {
ep.inv[i] * ep.inv[i] * ep.inv[i]
} else {
0.0
}
}
pub(crate) fn pinv_div1(ep: &EighPinv, i: usize, j: usize) -> f64 {
if i == j {
return pinv_prime(ep, i);
}
let li = ep.evals[i];
let lj = ep.evals[j];
let denom = li - lj;
let scale = li.abs().max(lj.abs()).max(1.0);
if denom.abs() <= 16.0 * f64::EPSILON * scale {
if pinv_active(ep, i) == pinv_active(ep, j) {
0.5 * (pinv_prime(ep, i) + pinv_prime(ep, j))
} else {
0.0
}
} else {
(pinv_value(ep, i) - pinv_value(ep, j)) / denom
}
}
pub(crate) fn pinv_div2(ep: &EighPinv, i: usize, k: usize, j: usize) -> f64 {
if i == k && k == j {
return pinv_half_second(ep, i);
}
let li = ep.evals[i];
let lk = ep.evals[k];
let lj = ep.evals[j];
if i == j {
let h = lk - li;
let scale = li.abs().max(lk.abs()).max(1.0);
if h.abs() <= 16.0 * f64::EPSILON * scale {
return pinv_half_second(ep, i);
}
return (pinv_value(ep, k) - pinv_value(ep, i) - pinv_prime(ep, i) * h) / (h * h);
}
if i == k {
let denom = li - lj;
let scale = li.abs().max(lj.abs()).max(1.0);
if denom.abs() <= 16.0 * f64::EPSILON * scale {
return pinv_half_second(ep, i);
}
return (pinv_prime(ep, i) - pinv_div1(ep, i, j)) / denom;
}
if k == j {
let denom = li - lj;
let scale = li.abs().max(lj.abs()).max(1.0);
if denom.abs() <= 16.0 * f64::EPSILON * scale {
return pinv_half_second(ep, j);
}
return (pinv_div1(ep, i, j) - pinv_prime(ep, j)) / denom;
}
let denom = li - lj;
let scale = li.abs().max(lj.abs()).max(1.0);
if denom.abs() <= 16.0 * f64::EPSILON * scale {
let h = lk - li;
if h.abs() <= 16.0 * f64::EPSILON * scale {
pinv_half_second(ep, i)
} else {
(pinv_value(ep, k) - pinv_value(ep, i) - pinv_prime(ep, i) * h) / (h * h)
}
} else {
(pinv_div1(ep, i, k) - pinv_div1(ep, k, j)) / denom
}
}
pub(crate) fn pinv_second_deriv(
ep: &EighPinv,
gx: &Array2<f64>,
gy: &Array2<f64>,
gxy: &Array2<f64>,
) -> Array2<f64> {
let n = ep.evals.len();
let gx_hat = ep.evecs.t().dot(gx).dot(&ep.evecs);
let gy_hat = ep.evecs.t().dot(gy).dot(&ep.evecs);
let gxy_hat = ep.evecs.t().dot(gxy).dot(&ep.evecs);
let mut core = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut value = pinv_div1(ep, i, j) * gxy_hat[(i, j)];
for k in 0..n {
value += pinv_div2(ep, i, k, j)
* (gx_hat[(i, k)] * gy_hat[(k, j)] + gy_hat[(i, k)] * gx_hat[(k, j)]);
}
core[(i, j)] = value;
}
}
ep.evecs.dot(&core).dot(&ep.evecs.t())
}
pub(crate) struct BlockForms {
pub(crate) r: Array2<f64>,
pub(crate) dr: Vec<Array2<f64>>,
pub(crate) d2r: Vec<Array2<f64>>,
pub(crate) q: f64,
pub(crate) dq: Vec<f64>,
pub(crate) d2q: Vec<f64>,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn block_residual_jets(
phi: &Array2<f64>, masses_local: &Array1<f64>, m: ArrayView2<'_, f64>, dm: &[Array2<f64>], d2m: &[Array2<f64>], n_active: usize,
) -> BlockForms {
let ml = phi.nrows();
let n = n_active;
let psi = phi.dot(&m);
let mut dpsi: Vec<Array2<f64>> = Vec::with_capacity(n);
for a in 0..n {
dpsi.push(phi.dot(&dm[a]));
}
let mut d2psi: Vec<Array2<f64>> = Vec::with_capacity(n * n);
for a in 0..n {
for b in 0..n {
d2psi.push(phi.dot(&d2m[a * n + b]));
}
}
let mut w = Array1::<f64>::zeros(ml);
let mut dw: Vec<Array1<f64>> = (0..n).map(|_| Array1::<f64>::zeros(ml)).collect();
let mut d2w: Vec<Array1<f64>> = (0..n * n).map(|_| Array1::<f64>::zeros(ml)).collect();
for a in 0..ml {
let psi_a = psi.row(a);
let e = -0.5 * psi_a.dot(&psi_a);
let wa = masses_local[a] * e.exp();
w[a] = wa;
let mut ex = vec![0.0_f64; n];
for x in 0..n {
ex[x] = -psi_a.dot(&dpsi[x].row(a));
dw[x][a] = wa * ex[x];
}
for x in 0..n {
for y in 0..n {
let dpx = dpsi[x].row(a);
let dpy = dpsi[y].row(a);
let d2p = d2psi[x * n + y].row(a);
let exy = -(dpx.dot(&dpy) + psi_a.dot(&d2p));
d2w[x * n + y][a] = wa * (ex[x] * ex[y] + exy);
}
}
}
let d = phi.ncols();
let q = w.sum();
let mut dq = vec![0.0_f64; n];
let mut d2q = vec![0.0_f64; n * n];
for x in 0..n {
dq[x] = dw[x].sum();
}
for x in 0..n {
for y in 0..n {
d2q[x * n + y] = d2w[x * n + y].sum();
}
}
let mut pvec = Array1::<f64>::zeros(d);
for a in 0..ml {
for k in 0..d {
pvec[k] += w[a] * psi[(a, k)];
}
}
let mut dpvec: Vec<Array1<f64>> = (0..n).map(|_| Array1::<f64>::zeros(d)).collect();
for x in 0..n {
for a in 0..ml {
for k in 0..d {
dpvec[x][k] += dw[x][a] * psi[(a, k)] + w[a] * dpsi[x][(a, k)];
}
}
}
let mut d2pvec: Vec<Array1<f64>> = (0..n * n).map(|_| Array1::<f64>::zeros(d)).collect();
for x in 0..n {
for y in 0..n {
let dst = &mut d2pvec[x * n + y];
for a in 0..ml {
for k in 0..d {
dst[k] += d2w[x * n + y][a] * psi[(a, k)]
+ dw[x][a] * dpsi[y][(a, k)]
+ dw[y][a] * dpsi[x][(a, k)]
+ w[a] * d2psi[x * n + y][(a, k)];
}
}
}
}
let amean = &pvec / q;
let mut damean: Vec<Array1<f64>> = Vec::with_capacity(n);
for x in 0..n {
damean.push((&dpvec[x] - &(&amean * dq[x])) / q);
}
let mut d2amean: Vec<Array1<f64>> = Vec::with_capacity(n * n);
for x in 0..n {
for y in 0..n {
let term = (&d2pvec[x * n + y]) / q
- (&(&dpvec[x] * dq[y]) + &(&dpvec[y] * dq[x]) + &(&pvec * d2q[x * n + y]))
/ (q * q)
+ &(&pvec * (2.0 * dq[x] * dq[y] / (q * q * q)));
d2amean.push(term);
}
}
let bmat = |wv: &Array1<f64>,
psiv: &Array2<f64>,
am: &Array1<f64>|
-> Array2<f64> {
let mut bb = Array2::<f64>::zeros((ml, d));
for a in 0..ml {
for k in 0..d {
bb[(a, k)] = wv[a] * (psiv[(a, k)] - am[k]);
}
}
bb
};
let b = bmat(&w, &psi, &amean);
let mut db: Vec<Array2<f64>> = Vec::with_capacity(n);
for x in 0..n {
let mut bb = Array2::<f64>::zeros((ml, d));
for a in 0..ml {
for k in 0..d {
bb[(a, k)] = dw[x][a] * (psi[(a, k)] - amean[k])
+ w[a] * (dpsi[x][(a, k)] - damean[x][k]);
}
}
db.push(bb);
}
let mut d2b: Vec<Array2<f64>> = Vec::with_capacity(n * n);
for x in 0..n {
for y in 0..n {
let mut bb = Array2::<f64>::zeros((ml, d));
for a in 0..ml {
for k in 0..d {
bb[(a, k)] = d2w[x * n + y][a] * (psi[(a, k)] - amean[k])
+ dw[x][a] * (dpsi[y][(a, k)] - damean[y][k])
+ dw[y][a] * (dpsi[x][(a, k)] - damean[x][k])
+ w[a] * (d2psi[x * n + y][(a, k)] - d2amean[x * n + y][k]);
}
}
d2b.push(bb);
}
}
let hmat = |wv: &Array1<f64>, psiv: &Array2<f64>| -> Array2<f64> {
let mut hh = Array2::<f64>::zeros((d, d));
for a in 0..ml {
for r in 0..d {
for c in 0..d {
hh[(r, c)] += wv[a] * psiv[(a, r)] * psiv[(a, c)];
}
}
}
hh
};
let hh = hmat(&w, &psi);
let mut dhh: Vec<Array2<f64>> = Vec::with_capacity(n);
for x in 0..n {
let mut hd = Array2::<f64>::zeros((d, d));
for a in 0..ml {
for r in 0..d {
for c in 0..d {
hd[(r, c)] += dw[x][a] * psi[(a, r)] * psi[(a, c)]
+ w[a] * dpsi[x][(a, r)] * psi[(a, c)]
+ w[a] * psi[(a, r)] * dpsi[x][(a, c)];
}
}
}
dhh.push(hd);
}
let mut d2hh: Vec<Array2<f64>> = Vec::with_capacity(n * n);
for x in 0..n {
for y in 0..n {
let mut hd = Array2::<f64>::zeros((d, d));
for a in 0..ml {
for r in 0..d {
for c in 0..d {
let pr = psi[(a, r)];
let pc = psi[(a, c)];
let dprx = dpsi[x][(a, r)];
let dpcx = dpsi[x][(a, c)];
let dpry = dpsi[y][(a, r)];
let dpcy = dpsi[y][(a, c)];
let d2pr = d2psi[x * n + y][(a, r)];
let d2pc = d2psi[x * n + y][(a, c)];
hd[(r, c)] += d2w[x * n + y][a] * pr * pc
+ dw[x][a] * (dpry * pc + pr * dpcy)
+ dw[y][a] * (dprx * pc + pr * dpcx)
+ w[a] * (d2pr * pc + dprx * dpcy + dpry * dpcx + pr * d2pc);
}
}
}
d2hh.push(hd);
}
}
let outer = |u: &Array1<f64>, v: &Array1<f64>| -> Array2<f64> {
let mut o = Array2::<f64>::zeros((d, d));
for r in 0..d {
for c in 0..d {
o[(r, c)] = u[r] * v[c];
}
}
o
};
let g = &(&hh / q) - &outer(&amean, &amean);
let mut dg: Vec<Array2<f64>> = Vec::with_capacity(n);
for x in 0..n {
let dhq = &(&dhh[x] / q) - &(&hh * (dq[x] / (q * q)));
let dout = &outer(&damean[x], &amean) + &outer(&amean, &damean[x]);
dg.push(&dhq - &dout);
}
let mut d2g: Vec<Array2<f64>> = Vec::with_capacity(n * n);
for x in 0..n {
for y in 0..n {
let d2hq = &(&d2hh[x * n + y] / q)
- &(&(&dhh[x] * (dq[y] / (q * q)))
+ &(&dhh[y] * (dq[x] / (q * q)))
+ &(&hh * (d2q[x * n + y] / (q * q))))
+ &(&hh * (2.0 * dq[x] * dq[y] / (q * q * q)));
let d2out = &outer(&d2amean[x * n + y], &amean)
+ &outer(&damean[x], &damean[y])
+ &outer(&damean[y], &damean[x])
+ &outer(&amean, &d2amean[x * n + y]);
d2g.push(&d2hq - &d2out);
}
}
let ep = eigh_pinv(&g, "local affine Gram").unwrap_or_else(|_| {
EighPinv {
evals: Array1::zeros(d),
evecs: Array2::eye(d),
inv: Array1::zeros(d),
pinv: Array2::zeros((d, d)),
}
});
let gpinv = ep.pinv.clone();
let mut dgpinv: Vec<Array2<f64>> = Vec::with_capacity(n);
for x in 0..n {
dgpinv.push(pinv_first_deriv(&ep, &dg[x]));
}
let mut d2gpinv: Vec<Array2<f64>> = Vec::with_capacity(n * n);
for x in 0..n {
for y in 0..n {
d2gpinv.push(pinv_second_deriv(&ep, &dg[x], &dg[y], &d2g[x * n + y]));
}
}
let triple = |bb: &Array2<f64>, gp: &Array2<f64>| -> Array2<f64> { bb.dot(gp).dot(&bb.t()) };
let p = triple(&b, &gpinv);
let mut dp: Vec<Array2<f64>> = Vec::with_capacity(n);
for x in 0..n {
let t1 = db[x].dot(&gpinv).dot(&b.t());
let t2 = b.dot(&dgpinv[x]).dot(&b.t());
let t3 = b.dot(&gpinv).dot(&db[x].t());
dp.push(&(&t1 + &t2) + &t3);
}
let mut d2p: Vec<Array2<f64>> = Vec::with_capacity(n * n);
for x in 0..n {
for y in 0..n {
let bx = &db[x];
let by = &db[y];
let bxy = &d2b[x * n + y];
let gx = &dgpinv[x];
let gy = &dgpinv[y];
let gxy = &d2gpinv[x * n + y];
let mut acc = bxy.dot(&gpinv).dot(&b.t());
acc += &bx.dot(gy).dot(&b.t());
acc += &bx.dot(&gpinv).dot(&by.t());
acc += &by.dot(gx).dot(&b.t());
acc += &b.dot(gxy).dot(&b.t());
acc += &b.dot(gx).dot(&by.t());
acc += &by.dot(&gpinv).dot(&bx.t());
acc += &b.dot(gy).dot(&bx.t());
acc += &b.dot(&gpinv).dot(&bxy.t());
d2p.push(acc);
}
}
let assemble_r = |wv: &Array1<f64>,
qv: f64,
pv: &Array2<f64>|
-> Array2<f64> {
let mut rr = Array2::<f64>::zeros((ml, ml));
for a in 0..ml {
for c in 0..ml {
rr[(a, c)] = -wv[a] * wv[c] / qv - pv[(a, c)] / qv;
}
rr[(a, a)] += wv[a];
}
rr
};
let r = assemble_r(&w, q, &p);
let mut dr: Vec<Array2<f64>> = Vec::with_capacity(n);
for x in 0..n {
let mut rr = Array2::<f64>::zeros((ml, ml));
for a in 0..ml {
for c in 0..ml {
let wwt_d = (dw[x][a] * w[c] + w[a] * dw[x][c]) / q
- w[a] * w[c] * dq[x] / (q * q);
let pd = dp[x][(a, c)] / q - p[(a, c)] * dq[x] / (q * q);
rr[(a, c)] = -wwt_d - pd;
}
rr[(a, a)] += dw[x][a];
}
dr.push(rr);
}
let mut d2r: Vec<Array2<f64>> = Vec::with_capacity(n * n);
for x in 0..n {
for y in 0..n {
let qx = dq[x];
let qy = dq[y];
let qxy = d2q[x * n + y];
let mut rr = Array2::<f64>::zeros((ml, ml));
for a in 0..ml {
for c in 0..ml {
let num = w[a] * w[c];
let num_x = dw[x][a] * w[c] + w[a] * dw[x][c];
let num_y = dw[y][a] * w[c] + w[a] * dw[y][c];
let num_xy = d2w[x * n + y][a] * w[c]
+ dw[x][a] * dw[y][c]
+ dw[y][a] * dw[x][c]
+ w[a] * d2w[x * n + y][c];
let wwt_d2 = num_xy / q
- (num_x * qy + num_y * qx + num * qxy) / (q * q)
+ 2.0 * num * qx * qy / (q * q * q);
let pn = p[(a, c)];
let pnx = dp[x][(a, c)];
let pny = dp[y][(a, c)];
let pnxy = d2p[x * n + y][(a, c)];
let p_d2 = pnxy / q - (pnx * qy + pny * qx + pn * qxy) / (q * q)
+ 2.0 * pn * qx * qy / (q * q * q);
rr[(a, c)] = -wwt_d2 - p_d2;
}
rr[(a, a)] += d2w[x * n + y][a];
}
d2r.push(rr);
}
}
BlockForms {
r,
dr,
d2r,
q,
dq,
d2q,
}
}
pub fn measure_jet_anisotropy_energy_form(
centers: ArrayView2<'_, f64>,
masses: ArrayView1<'_, f64>,
band: &MeasureJetBand,
order_s: f64,
alpha: f64,
l: ArrayView2<'_, f64>,
) -> Result<Array2<f64>, BasisError> {
Ok(measure_jet_anisotropy_energy_form_with_jets(
centers, masses, band, order_s, alpha, l,
)?
.q)
}
pub fn measure_jet_anisotropy_energy_form_with_jets(
centers: ArrayView2<'_, f64>,
masses: ArrayView1<'_, f64>,
band: &MeasureJetBand,
order_s: f64,
alpha: f64,
l: ArrayView2<'_, f64>,
) -> Result<MeasureJetAnisotropyJets, BasisError> {
let m_centers = centers.nrows();
let d = centers.ncols();
if l.nrows() != d || l.ncols() != d {
crate::bail_dim_basis!(
"measure-jet anisotropy metric L must be {d}×{d} to match the ambient dimension, got {:?}",
l.dim()
);
}
if masses.len() != m_centers {
crate::bail_dim_basis!(
"measure-jet anisotropy mass/center mismatch: {} masses for {} centers",
masses.len(),
m_centers
);
}
if band.eps.is_empty() || band.eps.iter().any(|e| !(e.is_finite() && *e > 0.0)) {
crate::bail_invalid_basis!("measure-jet anisotropy needs a nonempty positive scale band");
}
if !(order_s.is_finite() && order_s > 0.0 && order_s < 2.0) {
crate::bail_invalid_basis!(
"measure-jet order s must lie in (0, 2) for the affine-jet energy; got {order_s}"
);
}
if !alpha.is_finite() {
crate::bail_invalid_basis!("measure-jet anisotropy needs a finite alpha; got {alpha}");
}
if masses.iter().any(|v| !(v.is_finite() && *v >= 0.0)) {
crate::bail_invalid_basis!("measure-jet anisotropy needs finite nonnegative center masses");
}
let indices = lower_triangular_indices(d);
let n = indices.len();
let nf = build_normalized_factor(l, &indices)?;
let md = metric_sq_dists(centers, nf.m.view());
let mut q_form = Array2::<f64>::zeros((m_centers, m_centers));
let mut d_first: Vec<Array2<f64>> =
(0..n).map(|_| Array2::<f64>::zeros((m_centers, m_centers))).collect();
let mut d_second: Vec<Array2<f64>> =
(0..n * n).map(|_| Array2::<f64>::zeros((m_centers, m_centers))).collect();
for &eps in &band.eps {
let cutoff2 = (PROFILE_CUTOFF * eps) * (PROFILE_CUTOFF * eps);
let intrinsic_dim = d as f64;
let eta = 2.0 * order_s + intrinsic_dim * (2.0 - 2.0 * alpha);
let scale_weight = band.log_step * eps.powf(-eta);
let net_radius2 = 0.25 * eps * eps;
let mut outer: Vec<usize> = Vec::new();
for i in 0..m_centers {
if masses[i] <= 0.0 {
continue;
}
let covered = outer.iter().any(|&o| md.dm2[(i, o)] <= net_radius2);
if !covered {
outer.push(i);
}
}
let mut net_mass = vec![0.0_f64; m_centers];
for i in 0..m_centers {
if masses[i] <= 0.0 {
continue;
}
let mut best = f64::INFINITY;
let mut best_o = usize::MAX;
for &o in &outer {
if md.dm2[(i, o)] < best {
best = md.dm2[(i, o)];
best_o = o;
}
}
if best_o != usize::MAX {
net_mass[best_o] += masses[i];
}
}
for &i in &outer {
let mut idx: Vec<usize> = Vec::new();
for j in 0..m_centers {
if md.dm2[(i, j)] <= cutoff2 {
idx.push(j);
}
}
let ml = idx.len();
let mut phi = Array2::<f64>::zeros((ml, d));
let mut masses_local = Array1::<f64>::zeros(ml);
for (a, &j) in idx.iter().enumerate() {
for k in 0..d {
phi[(a, k)] = (centers[(j, k)] - centers[(i, k)]) / eps;
}
masses_local[a] = masses[j];
}
let q_block: f64 = idx
.iter()
.enumerate()
.map(|(a, &j)| masses_local[a] * (-md.dm2[(i, j)] / (2.0 * eps * eps)).exp())
.sum();
if !(q_block > 0.0) {
continue;
}
let blk = block_residual_jets(
&phi,
&masses_local,
nf.m.view(),
&nf.dm,
&nf.d2m,
n,
);
let base = scale_weight * net_mass[i] * q_block.powf(1.0 - 2.0 * alpha);
let beta = 1.0 - 2.0 * alpha;
for (a, &ja) in idx.iter().enumerate() {
for (c, &jc) in idx.iter().enumerate() {
q_form[(ja, jc)] += base * blk.r[(a, c)];
for x in 0..n {
let qx_over_q = blk.dq[x] / blk.q;
d_first[x][(ja, jc)] +=
base * (blk.dr[x][(a, c)] + beta * qx_over_q * blk.r[(a, c)]);
}
for x in 0..n {
for y in 0..n {
let qx_over_q = blk.dq[x] / blk.q;
let qy_over_q = blk.dq[y] / blk.q;
let qxy_over_q = blk.d2q[x * n + y] / blk.q;
let density_d2 =
beta * qxy_over_q + beta * (beta - 1.0) * qx_over_q * qy_over_q;
d_second[x * n + y][(ja, jc)] += base
* (blk.d2r[x * n + y][(a, c)]
+ beta * qx_over_q * blk.dr[y][(a, c)]
+ beta * qy_over_q * blk.dr[x][(a, c)]
+ density_d2 * blk.r[(a, c)]);
}
}
}
}
}
}
let sym = |a: Array2<f64>| (&a + &a.t()) * 0.5;
let q = sym(q_form);
let d_first: Vec<Array2<f64>> = d_first.into_iter().map(sym).collect();
let d_second: Vec<Array2<f64>> = d_second.into_iter().map(sym).collect();
Ok(MeasureJetAnisotropyJets {
q,
indices,
d_first,
d_second,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::terms::basis::{measure_jet_band, measure_jet_energy_form};
use ndarray::array;
pub(crate) fn two_cluster_centers() -> (Array2<f64>, Array1<f64>) {
let centers = array![
[0.00, 0.00],
[0.31, 0.05],
[0.58, -0.07],
[0.93, 0.11],
[1.22, 0.02],
[1.49, -0.04],
[3.10, 2.00],
[3.42, 2.13],
[3.71, 1.91],
[4.05, 2.07],
[4.33, 1.96],
[4.61, 2.12],
];
let m = centers.nrows();
let masses = Array1::<f64>::from_elem(m, 1.0 / m as f64);
(centers, masses)
}
pub(crate) fn band_for(centers: &Array2<f64>) -> MeasureJetBand {
measure_jet_band(centers.view(), 0).expect("band")
}
#[test]
pub(crate) fn identity_metric_reproduces_isotropic_bit_for_bit() {
let (centers, masses) = two_cluster_centers();
let band = band_for(¢ers);
let (s0, a0) = (1.3, 0.8);
let l = Array2::<f64>::eye(2);
let q_aniso = measure_jet_anisotropy_energy_form(
centers.view(),
masses.view(),
&band,
s0,
a0,
l.view(),
)
.expect("aniso energy");
let q_iso = measure_jet_energy_form(centers.view(), masses.view(), &band, s0, a0, 1e-3)
.expect("iso energy");
assert_eq!(q_aniso.dim(), q_iso.dim());
for (a, b) in q_aniso.iter().zip(q_iso.iter()) {
assert_eq!(
a.to_bits(),
b.to_bits(),
"Ā = I must reproduce the isotropic energy bit-for-bit: {a} vs {b}"
);
}
}
#[test]
pub(crate) fn l_jets_match_finite_differences() {
let (centers, masses) = two_cluster_centers();
let band = band_for(¢ers);
let (s0, a0) = (1.3, 0.8);
let l0 = array![[1.30, 0.00], [-0.45, 0.80]];
let jets = measure_jet_anisotropy_energy_form_with_jets(
centers.view(),
masses.view(),
&band,
s0,
a0,
l0.view(),
)
.expect("jets");
let q_plain = measure_jet_anisotropy_energy_form(
centers.view(),
masses.view(),
&band,
s0,
a0,
l0.view(),
)
.expect("plain");
for (a, b) in jets.q.iter().zip(q_plain.iter()) {
assert_eq!(a.to_bits(), b.to_bits(), "value drift {a} vs {b}");
}
let eval = |l: &Array2<f64>| {
measure_jet_anisotropy_energy_form(
centers.view(),
masses.view(),
&band,
s0,
a0,
l.view(),
)
.expect("energy")
};
let perturb = |idx: LIndex, delta: f64| {
let mut l = l0.clone();
l[(idx.row, idx.col)] += delta;
l
};
let h = 1e-4;
let n = jets.n_active();
for a in 0..n {
let ia = jets.indices[a];
let plus = eval(&perturb(ia, h));
let minus = eval(&perturb(ia, -h));
let fd = (&plus - &minus) / (2.0 * h);
let scale = fd.iter().fold(1e-30_f64, |acc, v| acc.max(v.abs()));
for (an, fdv) in jets.d_first[a].iter().zip(fd.iter()) {
assert!(
(an - fdv).abs() <= 5e-5 * scale,
"∂Q/∂L[{},{}] mismatch: analytic {an:.6e} vs FD {fdv:.6e} (scale {scale:.3e})",
ia.row,
ia.col
);
}
}
for a in 0..n {
let ia = jets.indices[a];
let plus = eval(&perturb(ia, h));
let center = eval(&l0);
let minus = eval(&perturb(ia, -h));
let fd = (&(&plus + &minus) - &(¢er * 2.0)) / (h * h);
let scale = fd.iter().fold(1e-30_f64, |acc, v| acc.max(v.abs()));
for (an, fdv) in jets.second(a, a).iter().zip(fd.iter()) {
assert!(
(an - fdv).abs() <= 5e-5 * scale,
"∂²Q/∂L[{},{}]² mismatch: analytic {an:.6e} vs FD {fdv:.6e} (scale {scale:.3e})",
ia.row,
ia.col
);
}
}
for a in 0..n {
let ia = jets.indices[a];
for b in (a + 1)..n {
let ib = jets.indices[b];
let mut lpp = l0.clone();
lpp[(ia.row, ia.col)] += h;
lpp[(ib.row, ib.col)] += h;
let mut lpm = l0.clone();
lpm[(ia.row, ia.col)] += h;
lpm[(ib.row, ib.col)] -= h;
let mut lmp = l0.clone();
lmp[(ia.row, ia.col)] -= h;
lmp[(ib.row, ib.col)] += h;
let mut lmm = l0.clone();
lmm[(ia.row, ia.col)] -= h;
lmm[(ib.row, ib.col)] -= h;
let pp = eval(&lpp);
let pm = eval(&lpm);
let mp = eval(&lmp);
let mm = eval(&lmm);
let fd = (&(&pp - &pm) - &(&mp - &mm)) / (4.0 * h * h);
let scale = fd.iter().fold(1e-30_f64, |acc, v| acc.max(v.abs()));
for (an, fdv) in jets.second(a, b).iter().zip(fd.iter()) {
assert!(
(an - fdv).abs() <= 5e-5 * scale,
"∂²Q/∂L[{},{}]∂L[{},{}] mismatch: analytic {an:.6e} vs FD {fdv:.6e} (scale {scale:.3e})",
ia.row,
ia.col,
ib.row,
ib.col
);
}
for (sab, sba) in jets.second(a, b).iter().zip(jets.second(b, a).iter()) {
assert!((sab - sba).abs() <= 1e-12 * (1.0 + sab.abs()));
}
}
}
}
#[test]
pub(crate) fn det_normalization_is_scale_invariant() {
let (centers, masses) = two_cluster_centers();
let band = band_for(¢ers);
let (s0, a0) = (1.1, 0.9);
let l0 = array![[0.90, 0.00], [0.35, 1.40]];
let q_ref = measure_jet_anisotropy_energy_form(
centers.view(),
masses.view(),
&band,
s0,
a0,
l0.view(),
)
.expect("ref");
for &c in &[0.25_f64, 0.5, 2.0, 7.5] {
let lc = &l0 * c;
let q_c = measure_jet_anisotropy_energy_form(
centers.view(),
masses.view(),
&band,
s0,
a0,
lc.view(),
)
.expect("scaled");
let scale = q_ref.iter().fold(0.0_f64, |acc, v| acc.max(v.abs()));
assert!(scale > 0.0, "energy is identically zero");
for (a, b) in q_c.iter().zip(q_ref.iter()) {
assert!(
(a - b).abs() <= 1e-10 * scale,
"scale c = {c} changed the normalized energy: {a:.6e} vs {b:.6e}"
);
}
}
}
#[test]
pub(crate) fn anisotropic_energy_annihilates_constants() {
let (centers, masses) = two_cluster_centers();
let band = band_for(¢ers);
let l = array![[1.20, 0.00], [-0.30, 0.95]];
let q = measure_jet_anisotropy_energy_form(
centers.view(),
masses.view(),
&band,
1.5,
1.0,
l.view(),
)
.expect("energy");
let m = q.nrows();
let ones = Array1::<f64>::ones(m);
let qv = q.dot(&ones);
let scale = q.iter().fold(0.0_f64, |acc, v| acc.max(v.abs()));
assert!(scale > 0.0, "energy is identically zero");
for (i, v) in qv.iter().enumerate() {
assert!(
v.abs() <= 1e-10 * scale,
"Q·1 leak at row {i}: {v:.3e} vs scale {scale:.3e}"
);
}
}
}