Skip to content

Low-rank operators

A rank-r update to an n×n matrix needs O(nr) storage, and the Woodbury identity turns a solve against it into a solve of size r.

Operator Represents
LowRank(U, S, V) U diag(S) Vᵀ
SymmetricLowRank(U, S) U diag(S) Uᵀ
IsotropicScalingPlusSymmetricLowRank(s, U, S) s·I + U diag(S) Uᵀ
PositiveDiagonalPlusSymmetricLowRank(D, LR) D + U diag(S) Uᵀ
import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
u = jax.random.normal(key, (8, 2))
s = jnp.ones(2)

lr = linox.SymmetricLowRank(u, s)
assert lr.shape == (8, 8)
assert jnp.allclose(linox.todense(lr), u @ jnp.diag(s) @ u.T)

The rewrites that matter

Diagonal-plus-low-rank is recognised from the arithmetic and solved by Woodbury:

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
d = linox.Diagonal(jnp.arange(1.0, 9.0))
lr = linox.SymmetricLowRank(jax.random.normal(key, (8, 2)), jnp.ones(2))

combined = d + lr
assert type(combined).__name__ == "PositiveDiagonalPlusSymmetricLowRank"

x = linox.solve(combined, jnp.ones(8))
assert jnp.linalg.norm(linox.todense(combined) @ x - jnp.ones(8)) < 1e-8

Solving (D + U S Uᵀ)x = b this way costs a diagonal solve plus an r×r solve, never an n×n factorisation.

Similarly for a scalar shift:

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
lr = linox.SymmetricLowRank(jax.random.normal(key, (8, 2)), jnp.ones(2))

shifted = 2.0 * linox.Identity(8) + lr
assert type(shifted).__name__ == "IsotropicScalingPlusSymmetricLowRank"

Rectangular low rank

LowRank takes separate left and right factors:

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
u = jax.random.normal(key, (6, 2))
v = jax.random.normal(jax.random.fold_in(key, 1), (4, 2))
s = jnp.ones(2)

lr = linox.LowRank(u, s, v)
assert jnp.allclose(linox.todense(lr), u @ jnp.diag(s) @ v.T)

LowRank.shape is wrong for rectangular factors

With differently shaped U and V, .shape reports (m, m) while todense() correctly gives (m, n). The matvec computes the right numbers, but anything trusting .shape — composition checks, shape-based dispatch — will misbehave. Tracked as a known bug; prefer square factors until it is fixed.

Where these come from

Low-rank updates are the natural shape of a Laplace approximation, a limited-memory quasi-Newton Hessian, or a GP posterior after a rank-r observation. In each case the base is diagonal or scalar and the update is thin, which is exactly the pattern the rewrites above match.