Skip to content

Trace and determinants

Both have exact forms for structured operators and stochastic estimators for the rest.

Trace

import jax.numpy as jnp
import linox

d = linox.Diagonal(jnp.arange(1.0, 5.0))
value = linox.trace(d)
assert jnp.allclose(value, 10.0)

For a Diagonal or a Kronecker the trace is exact and free — tr(A ⊗ B) = tr(A)·tr(B):

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
a = jax.random.normal(key, (3, 3))
b = jax.random.normal(jax.random.fold_in(key, 1), (4, 4))
kron = linox.Kronecker(linox.Matrix(a), linox.Matrix(b))

assert jnp.allclose(kron.trace(), jnp.trace(jnp.kron(a, b)))

Hutchinson estimation

For an operator with no exact route, estimate tr(A) ≈ (1/M) Σ zᵢᵀ A zᵢ with random probes. Only matvecs are needed:

import jax
import jax.numpy as jnp
import linox
from linox.linalg.approx.hutchinson import hutchinson_trace

n = 100
op = linox.Matrix(jnp.eye(n) + 0.1 * jnp.ones((n, n)))   # trace = 1.1 * n

estimate, stderr = hutchinson_trace(op, jax.random.PRNGKey(0), num_samples=200)

assert jnp.abs(estimate - 1.1 * n) < 5.0
assert stderr > 0

The estimator is unbiased; stderr is the standard error of the mean and shrinks as 1/√M. It is a stochastic answer — two different keys give two different numbers.

hutchinson_diagonal estimates the diagonal the same way, and hutchinson_trace_and_diagonal computes both from shared probes.

Determinants

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
dense = jax.random.normal(key, (4, 4))
spd = dense @ dense.T + 4 * jnp.eye(4)
op = linox.Matrix(spd)

sign, logabs = linox.slogdet(op)
assert jnp.allclose(logabs, jnp.linalg.slogdet(spd)[1])
assert jnp.allclose(linox.logdet(op), logabs)

Prefer slogdet to det: a determinant of a large matrix overflows long before its logarithm does.

For a Kronecker product, det(A ⊗ B) = det(A)^{n_b} · det(B)^{n_a}:

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
a = jax.random.normal(key, (3, 3))
b = jax.random.normal(jax.random.fold_in(key, 1), (3, 3))
spd_a, spd_b = a @ a.T + 3 * jnp.eye(3), b @ b.T + 3 * jnp.eye(3)
kron = linox.Kronecker(linox.Matrix(spd_a), linox.Matrix(spd_b))

_sign, logabs = linox.slogdet(kron)
assert jnp.allclose(logabs, jnp.linalg.slogdet(jnp.kron(spd_a, spd_b))[1], atol=1e-8)

Stochastic Lanczos quadrature

log det A = tr(log A), so a log-determinant is a trace of a matrix function — which Hutchinson probes plus Lanczos can estimate without factorising:

import jax
import jax.numpy as jnp
import linox
from linox.linalg.approx.slq import slq_logdet

key = jax.random.PRNGKey(0)
n = 60
q, _ = jnp.linalg.qr(jax.random.normal(key, (n, n)))
spd = q @ jnp.diag(jnp.linspace(1.0, 20.0, n)) @ q.T
op = linox.Matrix(spd)

estimate, _stderr = slq_logdet(op, key, num_samples=40, m=25)
exact = jnp.linalg.slogdet(spd)[1]

assert jnp.abs(estimate - exact) / jnp.abs(exact) < 0.05

This is the algorithm that makes GP marginal likelihoods tractable at scale — it needs num_samples * m matvecs and no factorisation.

Estimators need a key

Any stochastic path requires an explicit PRNG key. With method="auto" and no key supplied, linox falls back to the exact route rather than inventing randomness.