pyzag.operators
The pyzag.operators subpackage defines the abstract block-operator
interfaces the solver runs against, plus the concrete dense backend. New in
pyzag 2.0.
Mathematical model
The solver works on chunks of a recursive nonlinear system. Within a chunk of
steps the unknowns are stacked into a block vector

with logical shape (nblk, batch_size, block_size) – axis 0 is the block /
time index, axis 1 is the batch, and the last axis holds one block’s entries. A
backend is free to store this however it likes (dense tensor, per-group list,
sparse) as long as it honours that logical shape.
A block operator
is a linear map defined purely by its action –
it need not materialize a matrix. The two required actions are the matrix-vector
product and its transpose,

exposed as matvec() and
t_matvec(). A
solvable block operator additionally solves the block system

via solve().
For lookback 1 the chunk residual Jacobian is block lower-bidiagonal: with
diagonal blocks
and subdiagonal blocks
,

and all other blocks are zero. Newton solves
each iteration.
This bidiagonal solve is provided two ways: Thomas (sequential forward
substitution) and parallel cyclic reduction (PCR), which repeatedly halves a
power-of-two window by eliminating the odd blocks using
-products. The PCR working state is carried opaquely across levels
by a PCRState. The
BlockJacobian abstraction owns the per-chunk
Jacobian and knows how to build both the forward system (for Newton) and the
transposed adjoint system (for the reverse/adjoint sweep).
Note
The concrete bidiagonal solve operators that consume these interfaces –
BidiagonalForwardOperator and the Thomas / PCR / hybrid factorizations
that implement the
action described above – live in
pyzag.chunktime, since they are solver machinery rather than storage
backends.
Abstract interfaces
Every backend implements these ABCs from pyzag.operators.base. The contract
is behavioral: a backend may use any storage as long as the logical block
ordering and shapes are preserved.
Block vectors
- class pyzag.operators.base.BlockVector
Bases:
ABCAbstract interface for a logical packed block vector.
Logical conventions
A block vector consists of nblk logical blocks. Solver code treats block vectors as block-major: axis 0 indexes logical blocks.
For the current bidiagonal solvers, the logical shape is
(nblk, batch_size, block_size)
- where:
axis 0 is the logical block / time index
axis 1 is the batch index
axis 2 is the local vector entries for one block
Backend freedom
A backend may store data in any representation it wants (dense, structured, sparse, factored, etc.) as long as:
nblk, batch_size, block_size describe the logical shape
all methods preserve the same logical block ordering
- abstract property device: device
Execution device for torch-backed implementations.
- abstract property dtype: dtype
Data type for torch-backed implementations.
- abstract property nblk: int
Number of logical blocks in this vector.
- abstract property batch_size: int
Logical batch size.
- abstract property block_size: int
Logical size of one block (last axis).
- abstractmethod clone() BlockVector
Return a safe copy of the vector.
- abstractmethod norm(dim: int = -1) Tensor
Compute the norm along dim. Returns a raw tensor (used for scalar convergence checks; the result has a different shape than a block vector and should not be wrapped).
- abstractmethod flatten() Tensor
Flatten to a raw tensor of shape
(batch_size, ndof): batch-major, with every block and state entry for a given batch element concatenated along the last axis.
- abstractmethod where(mask: Tensor, other: BlockVector) BlockVector
Batch-axis conditional combination,
torch.whereconvention: returnsselfwheremaskis True, elseother.maskis a raw tensor of shape(batch_size,); backends broadcast it over the block and state axes. Used by the Newton step for partial updates.
- abstractmethod scale_batches(factor: Tensor) BlockVector
Per-batch scalar broadcast: multiply each batch element by the corresponding entry of
factor(raw tensor of shape(batch_size,), broadcast over block and state axes). Used by line-search backtracking.
- abstractmethod flip(dim: int) BlockVector
Return a new vector with axis dim reversed.
- neg() BlockVector
Return the negated vector (alias for
-self).
- abstractmethod classmethod cat(vectors: Sequence[BlockVector], dim: int = 0) BlockVector
Concatenate a sequence of compatible block vectors along dim.
- abstractmethod classmethod zeros_like(other: BlockVector) BlockVector
Construct a zero-filled block vector with the same shape as other.
Block operators
- class pyzag.operators.base.BlockOperator
Bases:
ABCAbstract interface for a logical packed block operator.
Logical conventions
The operator consists of nblk logical blocks. The solver treats block vectors passed to this operator as block-major.
Backend freedom
- A backend may store its blocks in any representation it wants as long as:
nblk reports the correct logical number of blocks
batch_size describes the logical block action
all methods preserve the same logical block ordering
- abstract property device: device
Execution device for torch-backed implementations.
- abstract property dtype: dtype
Data type for torch-backed implementations.
- abstract property nblk: int
Number of logical blocks in this operator.
- abstract property batch_size: int
Logical batch size expected in block-major vector inputs.
- abstractmethod matvec(x: BlockVector) BlockVector
Apply the operator to a block vector x.
- Required:
x.nblk == self.nblk
- abstractmethod t_matvec(x: BlockVector) BlockVector
Apply the transpose of the operator to a block vector x.
- Required:
x.nblk == self.nblk
- abstractmethod clone() BlockOperator
Return a safe copy of the operator.
- abstractmethod pad_front(n: int = 1) BlockOperator
Return an operator with n leading dummy logical blocks (creates new data, not a view).
- class pyzag.operators.base.SolvableBlockOperator
Bases:
BlockOperatorBlock operator supporting direct block solves and PCR-based reduction.
- abstractmethod solve(rhs: BlockVector) BlockVector
Solve the block system A x = rhs.
- Required:
rhs.nblk == self.nblk
- abstractmethod pcr_init(B: BlockOperator, v: BlockVector) PCRState
Initialise the backend-native PCR working state for a power-of-two window.
selfis the diagonal operator (A),Bis the subdiagonal (already padded with one dummy leading block soB.nblk == self.nblk), andvis the RHS slice for this window.The backend allocates internal working tensors (e.g. adds the extra leading dimension used by the Dense cyclic-shift trick) and returns an opaque
PCRState.
- abstractmethod pcr_reduce_level(state: PCRState, level: int) PCRState
Apply one PCR level to the working state.
Updates the RHS vector and subdiagonal via A-inverse products, then applies the backend-native cyclic interleaving (
as_stridedfor Dense). Returns the updatedPCRStateready for the next level or forpcr_finalize().level(0-based) lets the backend compute the correct stride pattern without the caller knowing about the internal working shape.
- abstractmethod pcr_finalize(state: PCRState) tuple[BlockOperator, BlockVector]
Extract the reduced
(B_red, v_red)from the final PCR state.Returns a pair with
nblk == window_size - 1, suitable for writing back into the full B and v_work arrays inpyzag.chunktime.BidiagonalPCRFactorization.
- class pyzag.operators.base.PCRState
Bases:
ABCOpaque PCR working state managed by the backend across levels.
Created by
SolvableBlockOperator.pcr_init(), updated bySolvableBlockOperator.pcr_reduce_level(), and consumed bySolvableBlockOperator.pcr_finalize(). Callers inchunktime.pytreat this as an opaque handle and never inspect its contents.
Block Jacobians
- class pyzag.operators.base.BlockJacobian
Bases:
ABCAbstract per-chunk Jacobian for a recursive nonlinear system.
Logical model
For lookback = 1 (the only case the solver currently supports), the chunk Jacobian represents the linearization of
R[k] = f(x[k-1], x[k])overk = 1..nblk_steps. The two structural pieces are:the diagonal:
dR[k]/dx[k]fork = 1..nblk_stepsthe subdiagonal:
dR[k]/dx[k-1]fork = 1..nblk_steps
The boundary subdiagonal blocks couple a chunk to its neighbours. In forward time order,
sub[0](k = 1,dR[1]/dx[0]) couples the chunk’s first residual to the lookback / previous chunk’s last state; the remaining subdiagonal blocks are internal to the chunk’s bidiagonal system. The adjoint pass walks time in reverse (seeas_adjoint_walk()) and couples to the previously-processed (adjoint-order) chunk through index 0 of the walk-order subdiagonal – which is the original last forward block, notsub[0].Time order is forward (low index = early time). The adjoint walk methods internalize all reversal – callers use
couple_prev_chunk()and must NOT reach into storage to flip, reorder, or pick a boundary block themselves.Backend freedom
Backends may store the diagonal/subdiagonal in any layout (dense per block, structured/arrowhead, sparse, factored). The contract is purely behavioral.
- abstract property device: device
Execution device for torch-backed implementations.
- abstract property dtype: dtype
Data type for torch-backed implementations.
- abstract property nblk_steps: int
Number of residual rows in the chunk.
- abstract property batch_size: int
Logical batch size.
- abstract property block_size: int
Per-step state size (the user-facing
n).
- abstractmethod forward_system(inverse_operator) BidiagonalForwardOperator
Build the chunk’s forward bidiagonal system, ready for Newton.
Returns a
pyzag.chunktime.BidiagonalForwardOperatorwhose diagonalAhasnblk == nblk_stepsand whose subdiagonalBhasnblk == nblk_steps - 1.- Parameters:
inverse_operator – factory used to build the inverse (e.g.
pyzag.chunktime.BidiagonalThomasFactorization).
- abstractmethod adjoint_system(inverse_operator)
Build the chunk’s adjoint bidiagonal solve operator, in adjoint-walk order.
Transposes are baked in. The first-row / first+last-col slicing (
J[1, 1:].TandJ[0, 1:-1].Tafterflip(1)) is also baked in. Unlikeforward_system(), which returns aBidiagonalForwardOperator(so Newton can call.inverse()between iterations), this returns the inverse / solve operator directly: applying.matvec(rhs)on the returned object yields the adjoint solution. This matches howblock_update_adjointconsumes the result with a single linear solve per chunk.This method should be called on a
BlockJacobianreturned byas_adjoint_walk().
- abstractmethod solve_terminal_adjoint(g_terminal: Tensor) BlockVector
Compute
-A_terminal^{-T} @ g_terminalfor the very last forward-time step of the trajectory.Returns a single-block
BlockVector(nblk == 1forlookback == 1; for higher lookback this would be alookback-block vector). The returned vector must not aliasg_terminal.
- abstractmethod couple_prev_chunk(a_first: BlockVector) BlockVector
Compute the inter-chunk adjoint coupling
B_boundary^T @ a_first.a_firstis a single-blockBlockVectorholding the previous chunk’s adjoint tail (in adjoint-walk order). Returns a single-blockBlockVectorto be subtracted into the current chunk’s RHS first row (nblk == 1forlookback == 1;lookback-block for higher lookback).
- abstractmethod as_adjoint_walk() BlockJacobian
Return a
BlockJacobianwhose forward time order is the reverse of this one. Backends are free to implement lazily (e.g. via a flag) so that no copy happens unless storage requires it. Replaces directflipcalls in solver code.
Dense backend
The dense backend is the default implementation and reproduces the pyzag 1.x
behavior. Block vectors are stored as (nblk, batch, state) tensors and block
operators as (nblk, sbat, sblk, sblk) tensors. Each abstract interface above
has a dense counterpart below.
- class pyzag.operators.dense.DenseBlockVector(data: Tensor)
Bases:
BlockVectorDense tensor-backed packed block vector.
- Parameters:
data (torch.Tensor) – shape (nblk, sbat, sblk)
- property device: device
Execution device of the backing tensor.
- property dtype: dtype
Data type of the backing tensor.
- property nblk: int
Number of logical blocks (axis 0 of
data).
- property batch_size: int
Logical batch size (axis 1 of
data).
- property block_size: int
Logical size of one block (last axis of
data).
- clone() DenseBlockVector
Return a safe copy backed by a cloned tensor.
- norm(dim: int = -1) Tensor
Compute the norm along
dim. Returns a raw tensor (used for scalar convergence checks; not wrapped as a block vector).
- flatten() Tensor
Flatten to a raw
(batch_size, nblk * block_size)tensor: transpose the block axis behind the batch axis and flatten the rest, so each batch element’s entries are concatenated along the last axis.
- where(mask: Tensor, other: BlockVector) DenseBlockVector
Batch-axis conditional combination (
torch.whereconvention): returnselfwheremaskis True, elseother.maskis a(batch_size,)tensor broadcast over the block and state axes.
- scale_batches(factor: Tensor) DenseBlockVector
Multiply each batch element by its own scalar from
factor(shape(batch_size,)), reshaped to(1, batch_size, 1, ...)so it broadcasts over the block and state axes rather than the trailing (state) axis. Used by line-search backtracking.
- flip(dim: int) DenseBlockVector
Return a new vector with axis
dimreversed.
- classmethod cat(vectors: Sequence[BlockVector], dim: int = 0) DenseBlockVector
Concatenate a sequence of
DenseBlockVectoralongdim.
- classmethod zeros(nblk: int, batch_size: int, block_size: int, dtype: dtype, device: device) DenseBlockVector
Return a zero-filled DenseBlockVector of the given shape.
- classmethod zeros_like(other: BlockVector) DenseBlockVector
Return a zero-filled
DenseBlockVectorwith the same shape asother.
- classmethod empty(nblk: int, batch_size: int, block_size: int, dtype: dtype, device: device) DenseBlockVector
Return an uninitialized DenseBlockVector of the given shape.
- class pyzag.operators.dense.DenseBlockOperator(data: Tensor, lu: Tensor | None = None, pivots: Tensor | None = None)
Bases:
SolvableBlockOperatorDense tensor-backed packed block operator.
Implements
SolvableBlockOperator(LU-basedsolveand PCR primitivespcr_init/pcr_reduce_level/pcr_finalize).The cached LU factorization (
self.lu/self.pivots) is materialized lazily on the firstsolveorpcr_initcall. Usefactored()to construct with eager factorization for cases where you know LU will be needed (e.g., the diagonal of a Newton system). Slicing, cloning, and in-place assignment preserve the cached LU when present, so chained Thomas-styleA[i:i+1].solve(...)calls don’t re-factor every block.- Parameters:
data (torch.Tensor) – shape
(nblk, sbat, sblk, sblk).lu (torch.Tensor, optional) – pre-computed LU factor with the same shape as
data.pivots (torch.Tensor, optional) – pre-computed pivots, shape
(nblk, sbat, sblk). Must be paired withlu.
- classmethod factored(data: Tensor) DenseBlockOperator
Construct with eager LU factorization. Use when the caller knows
solveorpcr_initwill be invoked.
- property device: device
Execution device of the backing tensor.
- property dtype: dtype
Data type of the backing tensor.
- property nblk: int
Number of logical blocks (axis 0 of
data).
- property batch_size: int
Logical batch size expected in block-major vector inputs.
- matvec(x: BlockVector) DenseBlockVector
Apply the (block-diagonal) operator to
x: per block, the matrix-vector productA_k x_k. Requiresx.nblk == self.nblk.
- t_matvec(x: BlockVector) DenseBlockVector
Apply the transpose of the operator to
x(per block,A_k^T x_k). Requiresx.nblk == self.nblk.
- solve(rhs: BlockVector) DenseBlockVector
Solve the block system
A x = rhsper block via the cached LU factorization (materialized lazily on first call). Requiresrhs.nblk == self.nblk.
- clone() DenseBlockOperator
Return a safe copy, cloning the cached LU factors if present.
- pad_front(n: int = 1) DenseBlockOperator
Return an operator with
nleading zero (dummy) blocks prepended. Creates new data rather than a view; the result is un-factored since padding changes the per-block layout.
- pcr_init(B: BlockOperator, v: BlockVector) DensePCRState
Initialise Dense PCR working state for a power-of-two window.
The state owns its
b/vworking buffers (cloned here), so the in-place reductions inpcr_reduce_level()never mutate the caller’s operator/vector – honouring the base-class contract that the backend allocates internal working tensors.
- pcr_reduce_level(state: PCRState, level: int) DensePCRState
Apply one Dense PCR level: update v and B, then cyclic-shift all four tensors.
- pcr_finalize(state: PCRState) tuple[DenseBlockOperator, DenseBlockVector]
Extract (B_red, v_red) with nblk = window_size - 1 from the final PCR state.
- classmethod identity(nblk: int, batch_size: int, block_size: int, dtype: dtype, device: device) DenseBlockOperator
Return an identity DenseBlockOperator of the given shape.
- classmethod from_diagonal(data: Tensor) DenseBlockOperator
Return a DenseBlockOperator built from per-block diagonal data.
- class pyzag.operators.dense.DensePCRState(lu: Tensor, pivots: Tensor, b: Tensor, v: Tensor)
Bases:
PCRStateDense backend PCR working state.
Holds the four working tensors used by the Dense cyclic-shift PCR kernel. Each tensor has an extra leading dimension prepended by
pcr_init()so that_dense_pcr_cyclic_shift()can double it at each level.- lu
shape
(1, nblk, sbat, sblk, sblk)initially.- Type:
torch.Tensor
- pivots
shape
(1, nblk, sbat, sblk)initially.- Type:
torch.Tensor
- b
shape
(1, nblk, sbat, sblk, sblk)initially.- Type:
torch.Tensor
- v
shape
(1, nblk, sbat, sblk, 1)initially.- Type:
torch.Tensor
- class pyzag.operators.dense.DenseBlockJacobian(diag: Tensor, sub: Tensor, _reversed: bool = False)
Bases:
BlockJacobianDense tensor-backed per-chunk Jacobian.
Storage is canonical (forward-time order). The
as_adjoint_walk()method returns a sibling instance that shares the same underlying tensors and only flips a private flag; the four adjoint methods read the data with appropriate slicing/indexing under that flag, so a physicalflipnever materializes.- Parameters:
diag (torch.Tensor) – per-step diagonal blocks
dR[k]/dx[k], shape(nblk_steps, batch, n, n).sub (torch.Tensor) – per-step subdiagonal blocks
dR[k]/dx[k-1], shape(nblk_steps, batch, n, n). The first blocksub[0]is the inter-chunk boundary coupling to the lookback / previous chunk’s last state.
- classmethod from_stacked(J: Tensor) DenseBlockJacobian
Construct from a stacked tensor of shape
(2, nblk_steps, batch, n, n)whereJ[1]is the diagonal andJ[0]is the subdiagonal (withJ[0, 0]being the boundary block).
- property device: device
Execution device of the backing tensors.
- property dtype: dtype
Data type of the backing tensors.
- property nblk_steps: int
Number of residual rows in the chunk (axis 0 of
diag).
- property batch_size: int
Logical batch size (axis 1 of
diag).
- property block_size: int
Per-step state size (last axis of
diag).
- forward_system(inverse_operator)
Build the chunk’s forward bidiagonal system for Newton: diagonal
A= the (factored) per-stepdiagblocks and subdiagonalB=sub[1:], wrapped in aBidiagonalForwardOperator. Must be called on a forward-walk Jacobian.
- adjoint_system(inverse_operator)
Build the chunk’s adjoint bidiagonal solve operator, in adjoint-walk order with transposes baked in: drop the first walked diagonal block (handled by the terminal seed) and the first/last walked subdiagonal blocks. Must be called on the walk-order Jacobian from
as_adjoint_walk(); applying.matvec(rhs)yields the adjoint solution.
- solve_terminal_adjoint(g_terminal: Tensor) DenseBlockVector
Compute
-A_terminal^{-T} @ g_terminalfor the very last forward-time step, returned as a single-blockDenseBlockVectorthat does not aliasg_terminal.
- couple_prev_chunk(a_first: BlockVector) DenseBlockVector
Compute the inter-chunk adjoint coupling
B_boundary^T @ a_first, wherea_firstis the previous chunk’s adjoint tail (single-block, in walk order). Returns a single-blockDenseBlockVectorto subtract into the current chunk’s first-row RHS.
- as_adjoint_walk() DenseBlockJacobian
Return a sibling Jacobian whose forward time order is reversed. This shares the same underlying tensors and only toggles a private flag, so no physical
flipmaterializes.
Dense helpers
- pyzag.operators.dense.batch_lu_solve(lu: Tensor, pivots: Tensor, rhs: Tensor) Tensor
Batched version of torch.linalg.lu_solve that accepts separate LU and pivot tensors.