Expand description
Taylor-jet tower algebra: write each family’s row log-likelihood ONCE,
derive the entire RowKernel<K> derivative tower mechanically (#932).
§The object
Tower4<K> is a truncated multivariate Taylor scalar in K primary
variables, carrying the value and ALL partial derivatives through fourth
order as full (unsymmetrized) tensors:
v ℓ
g[a] ∂ℓ/∂p_a
h[a][b] ∂²ℓ/∂p_a∂p_b
t3[abc] ∂³ℓ/∂p_a∂p_b∂p_c
t4[abcd] ∂⁴ℓ/∂p_a∂p_b∂p_c∂p_dArithmetic (+ − × ÷, scalar mixes) propagates the tower by the exact
Leibniz rule; unary transcendentals propagate by the exact multivariate
Faà di Bruno formula given a [f, f′, f″, f‴, f⁗] stack evaluated at the
inner value. This is truncated Taylor ALGEBRA — exact derivatives of the
evaluated expression, not finite differences, not an approximation —
fully compatible with the exact-REML-only policy.
One evaluation of a row NLL program at seeded variables yields, in a
single pass, every channel the gam_models::row_kernel::RowKernel trait
demands: row_kernel (value/∇/H), row_third_contracted(dir) (contract
t3 with dir), and row_fourth_contracted(u, v) (contract t4 with
u and v). The directional cross-channels that hand-written towers
drop (#736’s residual gap) cannot be dropped here: there is no separate
“channel” to forget — every derivative of the one expression is carried.
§Why this exists (the bug genus)
Every family today hand-writes its tower: value in one function,
gradient in another, pdfthird_derivative/pdffourth_derivative,
entry/exit-specific cross blocks — thousands of lines of calculus that
drift. #736 was a sign flip in a hand-written cross-Hessian block,
invisible until a new consumer touched it; #948 is a derivative path
that is not the derivative of the evaluated row loss (clamped-μ
surrogate); the objective↔gradient desync class is the same disease at
the criterion level. A tower-derived kernel is exact-by-construction:
the value channel IS the production loss expression, so its derivative
channels cannot desync from it.
§Relation to jet_partitions::MultiDirJet
The tree already carries a directional jet (bitmask coefficients over
distinct seeded directions, heap-allocated, Bell-partition compose) used
inside the marginal-slope and latent-survival families. It answers “the
derivative along THESE specific directions” and must be re-seeded and
re-evaluated per direction tuple (e.g. 10 symmetric (a,b) pairs for a
K=4 fourth contraction). Tower4 answers ALL of them from one
evaluation: contraction happens AFTER differentiation, as plain linear
algebra on the stored tensors. Use MultiDirJet when you need a handful
of directions of a huge-K expression; use Tower4 when you need the
complete small-K tower — which is exactly the RowKernel<K≤4> shape.
The [f64; 5] unary-derivative stacks
(unary_derivatives_neglog_phi, …) are signature-compatible with
Tower4::compose_unary, so the families’ existing special-function
stacks are directly reusable.
§Stability discipline (why this is NOT autodiff)
Differentiating the primal code path inherits its instabilities: a jet
pushed through a naive ln(1 + e^η) is garbage in the saturated tail
even though the true derivative σ(η) is benign there. This module
therefore splits responsibility: humans own primitive stability,
the algebra owns combinatorics. Tail-critical special functions enter
a program ONLY as hand-certified [f64; 5] derivative stacks through
Tower4::compose_unary — the same stacks the families already write
(unary_derivatives_neglog_phi and friends, built on erfcx/log_ndtr) —
and the tower mechanizes only the Leibniz/Faà di Bruno composition,
which is where hand-written towers actually fail (#736 was a
composition sign flip, not a primitive error). Program authors must use
a stable primitive stack wherever the f64 production loss does; the
convenience methods (exp, ln, sqrt, …) are for expressions whose
arguments are tame by construction.
§Storage convention
Tensors are stored FULL, not symmetric-packed: t4 for K=4 is 256
doubles where 35 would do. This is deliberate clarity-over-speed for the
oracle role — indexing is trivially auditable, contraction loops are
obvious, and the redundancy is itself a checked invariant (the algebra
only ever writes symmetric values). Symmetric packing is a later,
profile-justified optimization behind the same API.
§Deployment ladder (#932)
- This module: the algebra + the program seam + the oracle.
- Universal oracle: every hand-written
RowKernelgains a CI test asserting channel-by-channel agreement with aRowProgramwritten once — seeverify_kernel_channels. This alone would have caught #736 at introduction. - Derive every channel through
program_row_kernel,program_third_contracted,program_fourth_contracted, orprogram_full_tower, selecting only the representation its consumer needs while retaining one expression. - New families (#914/#916/#917 ZI/ordinal/expectile, #921’s location-
scale port) implement ONLY
RowProgramand get an exact fourth-order tower for the price of writing the likelihood.
Structs§
- Kernel
Channels - One row’s worth of hand-written kernel outputs, as claimed by a
RowKernelimplementation, packaged for verification against the tower truth. Plain data (no trait coupling) so any kernel — whatever its visibility — can be audited from its own test module. - Tower2
- Truncated SECOND-order multivariate Taylor scalar in
Kvariables. - Tower3
- Truncated THIRD-order multivariate Taylor scalar in
Kvariables. - Tower4
- Truncated fourth-order multivariate Taylor scalar in
Kvariables.
Traits§
- RowProgram
- The single source of truth #932 asks for: a family’s row negative
log-likelihood written ONCE over the generic
crate::jet_scalar::JetScalarinterface, from which everyRowKernel(gam-models) derivative channel is mechanically derived. A family implements ONLY this (plus its linear Jacobian wiring, which is family data, not calculus) — it cannot author an independent derivative tower, because there is no other channel to author.
Functions§
- digamma
- Scalar digamma ψ(x) for x>0. Bit-identical to
digamma_derivative_stack(x)[0]and toln_gamma_derivative_stack(x)[1], but evaluates ONLY ψ — the four higher polygammas those[f64; 5]stacks build are pure discarded work at a scalar consumer that reads a single element. Hot-path row kernels that need only the digamma value (e.g. the GAMLSS Beta observed cross weight) call this instead of indexing[0]off a full derivative stack. - digamma_
derivative_ stack - ln_
gamma_ derivative_ stack - ln_
gamma_ derivative_ stack_ order2 - program_
fourth_ contracted - Derive the
row_fourth_contracted(u, v)channelΣ_{cd} ℓ_{abcd} u_c v_dfrom aRowProgramat the two-seed scalarcrate::jet_scalar::TwoSeed, WITHOUT materialising the denset4. - program_
full_ tower - Derive every channel
(v, g, h, t3, t4)in one pass from aRowProgramat the full denseTower4scalar. - program_
row_ kernel - Derive the
row_kernelchannel(nll, ∇, H)from aRowProgramat the value/gradient/Hessian scalarcrate::jet_scalar::Order2, WITHOUT materialising any third / fourth tensor. - program_
third_ contracted - Derive the
row_third_contracted(dir)channelΣ_c ℓ_{abc} dir_cfrom aRowProgramat the one-seed scalarcrate::jet_scalar::OneSeed, WITHOUT materialising the denset3. - trigamma
- Scalar trigamma ψ′(x) for x>0. Bit-identical to
trigamma_derivative_stack(x)[0](both bottom out inpolygamma_positive(1, x)), but evaluates ONLY ψ′ — the four higher polygammas (orders 2–5) the[f64; 5]stack builds are discarded at a[0]consumer. Used by the dispersion-channel Fisher-information row kernels (NB2ψ′(θ)−ψ′(θ+μ), Betaμψ′(μφ)−(1−μ)ψ′((1−μ)φ)) which read the trigamma value alone.