gam_terms/basis/measure_jet_anisotropy.rs
1//! Learned ambient anisotropy for the measure-jet energy.
2//!
3//! The isotropic measure-jet energy [`super::measure_jet_energy_form`] treats
4//! the ambient coordinates with a Euclidean local Gram: the Gaussian kernel
5//! weight is `exp(−‖δ‖²/2ε²)` and the local affine features are `δ/ε` with
6//! `δ = x_j − x_i`. This module generalizes that Euclidean inner product to a
7//! learned Mahalanobis metric
8//!
9//! ```text
10//! A = L Lᵀ, Ā = A / det(A)^(1/d) (det-normalized, det Ā = 1),
11//! ```
12//!
13//! parametrized by the lower-triangular Cholesky factor `L` (d×d). The metric
14//! enters every local block through the SINGLE substitution
15//!
16//! ```text
17//! ⟨u, v⟩ ↦ uᵀ Ā v ,
18//! ```
19//!
20//! which is realized exactly by transforming the centers once with the
21//! det-normalized factor `M = L / det(L)^(1/d)` (so `M Mᵀ = Ā`, `det M = 1`):
22//!
23//! ```text
24//! ‖δ M‖² = δ Ā δᵀ (metric squared distance → kernel),
25//! (δ/ε)M = metric local affine features,
26//! Y = X M (transformed row centers; E_A(X) ≡ E_I(Y)).
27//! ```
28//!
29//! Because the local affine residual projects each block's center values onto
30//! `span{1, local affine coords}` and `M` is invertible, the projection is
31//! reparametrization-invariant: the metric reaches the energy ONLY through the
32//! kernel weights `w` and the (linearly transformed) features. With `Ā = I`
33//! (`M = I`, `Y = X`) the construction collapses to the isotropic energy
34//! bit-for-bit — that is the contract the first oracle test pins.
35//!
36//! To learn `L` by REML the energy needs exact first and second derivatives
37//! `∂E/∂L_ij`, `∂²E/∂L_ij∂L_kl`. They are produced from the SAME local block
38//! walk as the value (no second assembly that could drift from the first),
39//! by carrying, per requested `L`-direction, the exact first/second
40//! directional derivatives of every metric-dependent block quantity — the
41//! transformed features, the Gaussian weights, the weighted mean, `B`, `G`,
42//! `G⁺` and the residual — through the closed-form product/chain rules.
43//!
44//! All ∂/∂L jets are FD-gated in this module's tests against central
45//! differences of the energy (rel tol `5e-5`, step `h = 1e-4`, the
46//! second-difference-optimal step mirroring `measure_jet_smooth`'s own jet
47//! gates).
48
49use ndarray::Array2;
50
51/// A single requested derivative direction in `L`-space: the lower-triangular
52/// entry `(i, j)` with `i >= j`. The zeroth-order "direction" (the value
53/// itself) is handled separately; this names the active first-order channels.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub struct LIndex {
56 /// Row of the lower-triangular factor entry (`>= col`).
57 pub row: usize,
58 /// Column of the lower-triangular factor entry (`<= row`).
59 pub col: usize,
60}
61
62/// The anisotropic energy together with its exact first and second jets with
63/// respect to the lower-triangular Cholesky factor entries of `L`.
64///
65/// `indices[a]` names the `(row, col)` of the `a`-th active lower-triangular
66/// entry (column-major over the lower triangle: for each column `j`, rows
67/// `j..d`). `d_first[a] = ∂Q/∂L_{indices[a]}`, and `d_second[(a, b)]` (stored
68/// for the full pair grid, symmetric in `a, b`) is
69/// `∂²Q/∂L_{indices[a]}∂L_{indices[b]}`.
70pub struct MeasureJetAnisotropyJets {
71 /// The det-normalized anisotropic energy form (m×m, symmetric PSD).
72 pub q: Array2<f64>,
73 /// Active lower-triangular `L`-entry indices, in the derivative order.
74 pub indices: Vec<LIndex>,
75 /// First derivatives `∂Q/∂L_a`, one m×m form per active index.
76 pub d_first: Vec<Array2<f64>>,
77 /// Second derivatives `∂²Q/∂L_a∂L_b`, indexed by `a*n + b` over the
78 /// `n = indices.len()` active entries (full symmetric grid).
79 pub d_second: Vec<Array2<f64>>,
80}
81
82impl MeasureJetAnisotropyJets {
83 /// Number of active lower-triangular derivative channels.
84 #[inline]
85 pub fn n_active(&self) -> usize {
86 self.indices.len()
87 }
88
89 /// Borrow the second-derivative form `∂²Q/∂L_a∂L_b`.
90 #[inline]
91 pub fn second(&self, a: usize, b: usize) -> &Array2<f64> {
92 &self.d_second[a * self.indices.len() + b]
93 }
94}
95
96// ----------------------------------------------------------------------------
97// Det-normalized factor M = L / det(L)^(1/d) and its exact L-jets.
98// ----------------------------------------------------------------------------
99
100// ----------------------------------------------------------------------------
101// Per-block algebra and its exact L-jets.
102// ----------------------------------------------------------------------------
103
104// ----------------------------------------------------------------------------
105// Top-level energy and L-jets.
106// ----------------------------------------------------------------------------
107