Linear operator models

Operator containers make structure composable without forcing assembly. Every SOLVAX operator is callable and exposes a shape; structured operators also provide a closed-form .T action.

Matrix-free operator

operator = sx.MatrixFreeOperator(
    apply=lambda v: apply_physics(v),
    shape=(n, n),
    transpose_apply=lambda w: apply_adjoint_physics(w),  # optional
)

If transpose_matvec is omitted, the transpose is obtained through jax.linear_transpose. Supply an explicit adjoint when it is cheaper, clearer, or uses a specialized discretization.

The algebraic-transpose identity is

\[ \sum_i(Ax)_iy_i=\sum_jx_j(A^Ty)_j. \]

Test it numerically for every custom operator. For complex reverse-mode programs, distinguish this algebraic transpose from a Hermitian adjoint and validate the JAX cotangent convention used by the complete objective.

Sum operator

For a structured principal part plus matrix-free corrections,

\[ A=A_0+A_1+\cdots+A_p, \]

use:

full = sx.SumOperator((structured_core, matrix_free_tail))

This is the central preconditioning pattern: solve full with FGMRES while using an exact inverse of structured_core as the preconditioner.

Kronecker operator

KroneckerOperator(A, B) represents \(A\otimes B\). If \(A\in\mathbb{R}^{p\times q}\), \(B\in\mathbb{R}^{r\times s}\), and \(X\in\mathbb{R}^{s\times q}\), then

\[ (A\otimes B)\operatorname{vec}(X) =\operatorname{vec}(BXA^T). \]

The reshape identity avoids constructing the full \(pr\times qs\) matrix and costs the two smaller multiplies [VLP93].

operator = sx.KroneckerOperator(A, B)
y = operator(x)
small_reference = jnp.kron(A, B) @ x

Use materialize() only for small tests and diagnostics.

Block-tridiagonal operator

operator = sx.BlockTridiagonalOperator(lower, diag, upper)
y = operator(x_flat)
lower, diag, upper = operator.to_blocks()

The callable expects a flat vector but stores the exact block structure. Its bands feed solvax.direct.block_thomas_factor() without conversion. This lets one object serve as the matrix-free outer operator and the source of a direct preconditioner.

Bordered operator

Constraints, Lagrange multipliers, and gauge conditions often produce

\[\begin{split} K= \begin{bmatrix} A&B\\ C&0 \end{bmatrix}, \qquad K\begin{bmatrix}x\\y\end{bmatrix} = \begin{bmatrix}Ax+By\\Cx\end{bmatrix}. \end{split}\]
K = sx.BorderedOperator(A, b_columns, c_rows)

b_columns.shape == (n, p) and c_rows.shape == (p, n). The callable acts on the concatenated (n + p,) vector.

Schur-projected preconditioner

Given an approximate inverse action \(\widetilde A^{-1}\), form the dense small Schur complement

\[ S=C\widetilde A^{-1}B. \]

For residual \((r_x,r_y)\), the projected application is

\[ y=S^{-1}(C\widetilde A^{-1}r_x-r_y), \]
\[ x=\widetilde A^{-1}(r_x-By). \]
precond = sx.schur_projected_precond(a_inv, b_columns, c_rows)
solution = sx.gmres(K, rhs, precond=precond)

For a general bordered matrix \([[A,B],[C,D]]\) whose border unknowns couple to themselves (e.g. a quasineutrality or potential border), pass the small dense block as d_block=; the Schur complement becomes \(S=C\widetilde A^{-1}B-D\) and the projection is otherwise unchanged.

It is exact when a_inv is \(A^{-1}\) and the dense Schur complement is solved exactly. With an approximate inverse, it is a constraint-aware preconditioner for saddle-point systems [BGL05].

Model selection

Operator

Use when

Avoid when

MatrixFree

only the action is cheap/available

an exact reusable structure is being hidden

Sum

principal part plus perturbations

operands have incompatible shapes

Kronecker

separable tensor-product action

ordering does not match the vectorization identity

BlockTridiagonal

adjacent dense blocks

coupling reaches nonadjacent blocks

Bordered

few global constraints

the constraint block is large enough to need its own sparse model

API summary

Runnable counterparts: examples/11_kronecker.py and examples/12_operators.py.