Skip to content

Solving

import jax
import jax.numpy as jnp
import linox

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

x = linox.solve(op, jnp.ones(6))
assert jnp.linalg.norm(linox.todense(op) @ x - jnp.ones(6)) < 1e-10

solve dispatches on the operator: a Diagonal divides, a Kronecker solves through its factors, a diagonal-plus-low-rank uses Woodbury, and a general matrix factorises.

Failure is reported

A singular system raises rather than returning finite nonsense:

import jax
import jax.numpy as jnp
import linox
from linox import LinearSolveError

u = jax.random.normal(jax.random.PRNGKey(0), (6, 3))
singular = linox.Matrix(u @ u.T)          # rank 3

try:
    linox.solve(singular, jnp.ones(6))
    raise AssertionError("expected a failure")
except LinearSolveError as exc:
    assert exc.result in (linox.RESULTS.singular, linox.RESULTS.nonfinite_output)

This matters because the failure mode is not obvious: the returned values were finite, of magnitude 10¹⁶, with no NaN to trip over.

Two opt-outs:

import jax
import jax.numpy as jnp
import linox

u = jax.random.normal(jax.random.PRNGKey(0), (6, 3))
singular = linox.Matrix(u @ u.T)

x = linox.solve(singular, jnp.ones(6), throw=False)          # accept it
x, info = linox.solve(singular, jnp.ones(6), throw=False, return_info=True)

assert info.result != linox.RESULTS.successful
assert "residual" in info.stats

RESULTS covers successful, singular, max_steps_reached, breakdown, stagnation, conlim, nonfinite_input and nonfinite_output.

Under jit the outcome is a traced value and cannot be raised at trace time; the failure is reported by a runtime callback, and info.result is available to branch on inside the computation.

Methods

method= Solver Requires
"auto" by operator size and structure —
"exact" the structured or dense factorisation square, nonsingular
"cg" preconditioned conjugate gradients symmetric positive definite
"lsmr" LSMR anything, including rectangular
import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
q, _ = jnp.linalg.qr(jax.random.normal(key, (40, 40)))
spd = linox.Matrix(q @ jnp.diag(jnp.linspace(1.0, 50.0, 40)) @ q.T)
b = jnp.ones(40)

exact = linox.solve(spd, b, method="exact")
iterative = linox.solve(spd, b, method="cg")

assert jnp.allclose(exact, iterative, atol=1e-4)

Conjugate gradients

CG needs only matvecs, so a matrix-free operator stays matrix-free. It accepts a preconditioner:

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
q, _ = jnp.linalg.qr(jax.random.normal(key, (60, 60)))
scale = jnp.diag(jnp.logspace(0, 4, 60))
badly_scaled = scale @ (q @ jnp.diag(jnp.linspace(1.0, 100.0, 60)) @ q.T) @ scale
op = linox.Matrix(badly_scaled)
b = jnp.ones(60)

jacobi = linox.Diagonal(1.0 / jnp.diag(badly_scaled))
x = linox.solve(op, b, method="cg", preconditioner=jacobi, maxiter=200)

assert jnp.linalg.norm(badly_scaled @ x - b) / jnp.linalg.norm(b) < 1e-4

On that system, plain CG does not converge within 60 iterations and the preconditioned version does.

Iteration count versus gradients

lax.while_loop has no reverse-mode rule, and its counter is only visible from inside it. Both are available, one CG run either way:

import jax
import jax.numpy as jnp
import linox
from linox.linalg.approx.cg import cg_solve

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

x, info = cg_solve(op, b)                            # differentiable
assert "itn" not in info

x, info = cg_solve(op, b, track_iterations=True)     # exact count
assert int(info["itn"]) > 0

Least squares

For rectangular or rank-deficient systems, use the pseudo-inverse:

import jax
import jax.numpy as jnp
import linox

key = jax.random.PRNGKey(0)
a = jax.random.normal(key, (8, 4))
b = jnp.ones(8)

x = linox.pinverse(linox.Matrix(a)) @ b
assert jnp.allclose(x, jnp.linalg.lstsq(a, b, rcond=None)[0], atol=1e-8)

solve on a rectangular operator raises a shape error — it solves square systems.