Skip to content

Abstraction for the pyzag input tensor#21

Open
hdt5kt wants to merge 13 commits into
applied-material-modeling:mainfrom
hdt5kt:development
Open

Abstraction for the pyzag input tensor#21
hdt5kt wants to merge 13 commits into
applied-material-modeling:mainfrom
hdt5kt:development

Conversation

@hdt5kt
Copy link
Copy Markdown

@hdt5kt hdt5kt commented Mar 11, 2026

Instead of (nblk, nbatch, nstate, nstate), the abstraction is now (nblk, xxxxx).

  • Abstraction class is in base.py
  • Dense matrix specifc (old behavior is in dense.py
  • Updated nonlinear.py and chunktime.py accordingly
  • PCR performance is uncertain, probably need another layer of abstraction for PCR algorithm

@hdt5kt hdt5kt changed the title Abstraction for the pyzag input matrix Abstraction for the pyzag input tensor Mar 11, 2026
@hdt5kt
Copy link
Copy Markdown
Author

hdt5kt commented Mar 11, 2026

@reverendbedford here is the PR, the failed is in the documentation for generating github page. Feel free to change anything as you see fit.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a packed “block operator” abstraction to decouple chunktime/nonlinear from assuming raw dense tensors for bidiagonal block systems, while providing a dense-backed implementation to preserve prior behavior.

Changes:

  • Added BlockOperator / SolvableBlockOperator / BlockOperatorBuilder abstractions and a dense implementation (DenseBlockOperator, DenseBlockLUFactorizedOperator, DenseBlockOperatorBuilder).
  • Updated chunktime bidiagonal operators/factorizations and nonlinear to construct and operate on these block-operator objects.
  • Updated existing tests to use the new abstractions and added a new dense block-operator algebra test suite.

Reviewed changes

Copilot reviewed 7 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
pyzag/operators/base.py Introduces abstract interfaces for packed block operators and builders.
pyzag/operators/dense.py Adds dense tensor-backed operator implementations + LU-factorized variant + builder.
pyzag/chunktime.py Migrates bidiagonal operator/factorizations to consume BlockOperator abstractions; updates PCR/Thomas paths.
pyzag/nonlinear.py Uses a BlockOperatorBuilder to wrap Jacobian blocks before constructing bidiagonal operators/solvers.
test/test_chunktime.py Updates chunktime tests to use dense operator classes and a new dense-matrix reference path.
test/test_block_operator_dense.py New tests validating dense block-operator algebra and builder behavior.
test/test_initial_conditions.py Removes a comment line near the manual seed call.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyzag/operators/dense.py Outdated
Comment on lines +25 to +27
from .base import BlockOperator, SolvableBlockOperator, BlockOperatorBuilder
import torch

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pyzag/operators is introduced as a new subpackage, but the directory currently has no __init__.py and pyproject.toml does not configure setuptools to discover namespace packages. This can lead to wheels/sdists missing pyzag.operators.* and runtime import failures after installation. Consider adding pyzag/operators/__init__.py (simplest) or explicitly configuring namespace package discovery in the packaging config.

Copilot uses AI. Check for mistakes.
Comment thread pyzag/operators/dense.py
Comment on lines +192 to +204
self.data = data
self.lu, self.pivots, _ = torch.linalg.lu_factor_ex(data)

# lazy transpose factorization
self.t_lu = None
self.t_pivots = None

def _ensure_transpose_lu(self):
if self.t_lu is None or self.t_pivots is None:
self.t_lu, self.t_pivots, _ = torch.linalg.lu_factor_ex(
self.data.transpose(-1, -2)
)

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

torch.linalg.lu_factor_ex returns an info tensor indicating singular/ill-conditioned blocks. The current code discards it (_) and proceeds, which can silently produce incorrect results or NaNs during lu_solve. Please capture and validate info (e.g., raise if any nonzero) for both the forward and transpose factorizations.

Copilot uses AI. Check for mistakes.
Comment thread pyzag/operators/dense.py Outdated

def empty_like(self, nblk):
shape = (nblk,) + self.data.shape[1:]
return DenseBlockLUFactorizedOperator(
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DenseBlockLUFactorizedOperator.empty_like() currently builds a new factorized operator from torch.empty(...), which immediately LU-factorizes uninitialized memory. Besides being expensive, this can yield invalid LU/pivots and hard-to-debug failures if the caller expects to fill data later. Consider either (a) returning a non-factorized DenseBlockOperator here, (b) allocating a safe default (e.g., identity blocks) before factorization, or (c) raising NotImplementedError to avoid unsafe use.

Suggested change
return DenseBlockLUFactorizedOperator(
# Return a non-factorized operator to avoid LU-factorizing uninitialized memory.
return DenseBlockOperator(

Copilot uses AI. Check for mistakes.
Comment thread pyzag/operators/base.py Outdated
pass

@abstractmethod
def empty_like(self, nlbk):
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The abstract method empty_like uses the parameter name nlbk, but implementations use nblk. While this won’t break positional calls, it’s inconsistent and can confuse readers/type checkers. Rename the parameter to nblk here to match the rest of the API.

Suggested change
def empty_like(self, nlbk):
def empty_like(self, nblk):

Copilot uses AI. Check for mistakes.
Comment thread pyzag/chunktime.py

if B.nblk != A.nblk - 1:
raise ValueError("B must have nblk = A.nblk - 1.")

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BidiagonalOperator.__init__ validates block counts, but it doesn’t validate that A and B agree on batch_size, dtype/device, and block_shape. Mismatches will produce hard-to-diagnose shape/device errors later in matvec/solve. Consider adding explicit checks here (e.g., A.batch_size == B.batch_size and A.block_shape == B.block_shape).

Suggested change
# Ensure that A and B are fully compatible; otherwise subsequent
# matvec/solve operations may fail with obscure shape/device errors.
if getattr(A, "batch_size", None) != getattr(B, "batch_size", None):
raise ValueError(
f"A and B must have the same batch_size "
f"(got A.batch_size={getattr(A, 'batch_size', None)}, "
f"B.batch_size={getattr(B, 'batch_size', None)})."
)
if getattr(A, "dtype", None) != getattr(B, "dtype", None):
raise ValueError(
f"A and B must have the same dtype "
f"(got A.dtype={getattr(A, 'dtype', None)}, "
f"B.dtype={getattr(B, 'dtype', None)})."
)
if getattr(A, "device", None) != getattr(B, "device", None):
raise ValueError(
f"A and B must be on the same device "
f"(got A.device={getattr(A, 'device', None)}, "
f"B.device={getattr(B, 'device', None)})."
)
if getattr(A, "block_shape", None) != getattr(B, "block_shape", None):
raise ValueError(
"A and B must have the same block_shape "
f"(got A.block_shape={getattr(A, 'block_shape', None)}, "
f"B.block_shape={getattr(B, 'block_shape', None)})."
)

Copilot uses AI. Check for mistakes.
Comment thread pyzag/chunktime.py
Comment on lines +330 to +337
def __init__(self, A, B, *args, **kwargs):
if isinstance(A, DenseBlockOperator):
A = DenseBlockLUFactorizedOperator(A.data)
super().__init__(A, B, *args, **kwargs)

Args:
v (torch.tensor): tensor of shape (nblk, sbat, sblk)
"""
return thomas_solve(self.lu, self.pivots, self.B, v)
def matvec(self, v):
"""call the thomas_solve"""
return thomas_solve(self.A, self.B, v)
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BidiagonalThomasFactorization ultimately calls A.solve_lower_bidiagonal(...), which requires that A support solve/t_solve (i.e., be a SolvableBlockOperator). Currently only the DenseBlockOperator case is converted; other BlockOperator implementations would fail at runtime. Consider validating A is solvable (or converting via a builder/factory) and raising a clear error if not.

Copilot uses AI. Check for mistakes.
Comment on lines 45 to +51
def _gen_operators(self):
self.blk_A = torch.rand(self.nblk, self.sbat, self.sblk, self.sblk)
self.blk_B = (
torch.rand(self.nblk - 1, self.sbat, self.sblk, self.sblk) / 10
) # Diagonal dominance
self.blk_B = torch.rand(self.nblk - 1, self.sbat, self.sblk, self.sblk) / 10

# Diagonal blocks must be solvable operators
self.Aop = DenseBlockLUFactorizedOperator(self.blk_A)
self.Bop = DenseBlockOperator(self.blk_B)
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.blk_A is generated as a fully random dense matrix, but the test now factorizes it (DenseBlockLUFactorizedOperator(self.blk_A)), which can become flaky if any block is singular/ill-conditioned. To make the test deterministic, consider constructing blk_A to be well-conditioned/invertible (e.g., add a scaled identity per block, similar to test_block_operator_dense.py).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 10 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyzag/nonlinear.py
Comment on lines 29 to +32
from pyzag import chunktime

from pyzag.operators.dense import DenseBlockOperatorBuilder

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pyzag/operators is introduced as a new subpackage, but the repository currently has no pyzag/operators/__init__.py. With the current pyproject.toml (setuptools default package discovery), subdirectories without __init__.py typically aren’t included in built distributions, which would make this import fail in an installed wheel/sdist. Add pyzag/operators/__init__.py (or explicitly configure namespace package discovery in packaging config) to ensure pyzag.operators.* is shipped and importable.

Copilot uses AI. Check for mistakes.
Comment thread pyzag/operators/dense.py Outdated
return A_ops, B_ops

def make_adjoint_blocks(self, J):
A_ops = DenseBlockOperator(J[1, 1:].transpose(-1, -2))
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make_adjoint_blocks() returns A_ops as a plain DenseBlockOperator, but the adjoint path calls self.direct_solve_operator(A_ops, B_ops). If direct_solve_operator is BidiagonalPCRFactorization/hybrid, it requires A to implement PCRFactorizedDiagonalOps and will raise a TypeError. Consider returning a DenseBlockLUFactorizedOperator for the adjoint diagonal blocks (factorize J[1, 1:].T) so PCR/hybrid methods work for adjoint solves too.

Suggested change
A_ops = DenseBlockOperator(J[1, 1:].transpose(-1, -2))
A_ops = DenseBlockLUFactorizedOperator(J[1, 1:].transpose(-1, -2))

Copilot uses AI. Check for mistakes.
Comment thread pyzag/operators/dense.py Outdated
Comment on lines +386 to +389
shape = (nblk,) + self.data.shape[1:]
return DenseBlockLUFactorizedOperator(
torch.empty(shape, dtype=self.dtype, device=self.device)
)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DenseBlockLUFactorizedOperator.empty_like() currently constructs a new DenseBlockLUFactorizedOperator(torch.empty(...)), which immediately runs lu_factor_ex on uninitialized memory. That’s unnecessarily expensive and can yield meaningless LU/pivots. Prefer allocating data/lu/pivots tensors directly and using from_factored(...) (or a dedicated constructor) so empty_like is a cheap allocation-only operation.

Suggested change
shape = (nblk,) + self.data.shape[1:]
return DenseBlockLUFactorizedOperator(
torch.empty(shape, dtype=self.dtype, device=self.device)
)
data_shape = (nblk,) + self.data.shape[1:]
lu_shape = (nblk,) + self.lu.shape[1:]
pivots_shape = (nblk,) + self.pivots.shape[1:]
data = torch.empty(data_shape, dtype=self.dtype, device=self.device)
lu = torch.empty(lu_shape, dtype=self.lu.dtype, device=self.lu.device)
pivots = torch.empty(
pivots_shape, dtype=self.pivots.dtype, device=self.pivots.device
)
return DenseBlockLUFactorizedOperator.from_factored(data, lu, pivots)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 11 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyzag/operators/dense.py
raise ValueError("DenseBlockLUFactorizedOperator requires square blocks.")

self.data = data
self.lu, self.pivots, _ = torch.linalg.lu_factor_ex(data)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

torch.linalg.lu_factor_ex returns an info tensor indicating singular/failed factorizations. The current implementation ignores this, so singular blocks can silently propagate and cause confusing downstream failures (or NaNs) during solves. Consider checking info and raising a clear error if any entry is nonzero (or optionally allowing the caller to decide how to handle singular blocks).

Suggested change
self.lu, self.pivots, _ = torch.linalg.lu_factor_ex(data)
self.lu, self.pivots, info = torch.linalg.lu_factor_ex(data)
if torch.any(info != 0):
bad_blocks = torch.nonzero(info != 0, as_tuple=False).flatten().tolist()
raise RuntimeError(
"LU factorization failed for one or more dense blocks "
f"(info={info.tolist()}, failing block indices={bad_blocks}). "
"This usually indicates a singular block or another factorization failure."
)

Copilot uses AI. Check for mistakes.
Comment thread pyzag/nonlinear.py
Comment on lines +364 to +369
# this is to make sure it is backward compatible
self.block_operator_builder = (
DenseBlockOperatorBuilder()
if block_operator_builder is None
else block_operator_builder
)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this is “backward compatible”, but the solver now always wraps Jacobian blocks into BlockOperator instances and BidiagonalForwardOperator now enforces BlockOperator inputs. Any existing direct_solve_operator implementation that previously accepted raw tensors will break. Either provide an adapter path (e.g., accept tensor inputs and wrap them with the default builder) or update the API/docs/comment to clearly state the new required signature (direct_solve_operator(A_ops, B_ops)).

Copilot uses AI. Check for mistakes.
Comment thread pyzag/chunktime.py
for the data in the corresponding
tensor. These values d can
range from -(n-1) to (n-1)
now handles abstractions
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring "now handles abstractions" is misleading here: SquareBatchedBlockDiagonalMatrix still assumes data entries are raw tensors (uses .shape, .dtype, .device) and won’t accept BlockOperator instances. Please either update the implementation to accept the new operator abstractions or adjust the docstring to reflect the actual expected inputs (tensors).

Suggested change
now handles abstractions
Square batched block-diagonal matrix backed by raw tensor blocks.
The entries in ``data`` are expected to be tensors with ``shape``,
``dtype``, and ``device`` attributes. This class does not currently
accept generic ``BlockOperator`` abstractions as block entries.

Copilot uses AI. Check for mistakes.
Comment thread pyzag/chunktime.py
storing the nblk main diagonal blocks
B (torch.tensor): tensor of shape (nblk-1,sbat,sblk,sblk)
storing the nblk-1 off diagonal blocks
This is now handled abstractions.
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class docstring is grammatically unclear ("This is now handled abstractions"). Consider updating it to a clear sentence (e.g., "This is now handled by block-operator abstractions.") so the intent is understandable when browsing generated docs.

Suggested change
This is now handled abstractions.
This is now handled by block-operator abstractions.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

@reverendbedford reverendbedford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from the specific comments:

  1. You are missing a lot of "proper" docstrings, including an "Args", "Keyword Args", "Returns" description of what the method/function does. Not including these will break the documentation.
  2. You should change things around so that conceptually NonlinearRecursiveFunction returns the operators instead of the block Jacobians (rather than set things up in RecursiveNonlinearEquationSolver with the "builder" class. However, to avoid breaking backward compatibility you can instead add a new parent class on top ofNonlinearRecursiveFunction called OperatorJacobianNonlinearRecursiveFunction or something, and make sure RecursiveNonlinearEquationSolver works with this class. Then redefine NonlinearRecursiveFunction as a child of that new base class, where the forward operator first calls the actual torch function and then wraps the Jacobian with the dense operators.
  3. Consider some of the copilot requests, particularly the ones fixing your spelling errors and suggesting some additional error checking.

Comment thread pyzag/operators/dense.py
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must be a way to better organize the class structure so that DenseBlockOperator and DenseBlockLUFactorizedOperator don't have all the copy-pasted code. Maybe a DenseOperator class you can mixin or similar.


import torch

# Ensure test consistency
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhh, why remove the comment?

Comment thread pyzag/chunktime.py
storing the nblk main diagonal blocks
B (torch.tensor): tensor of shape (nblk-1,sbat,sblk,sblk)
storing the nblk-1 off diagonal blocks
This is now handled abstractions.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need a better comment explaining what this class now does.

Comment thread pyzag/chunktime.py
return thomas_solve(self.A, self.B, v)


########## NEW PCR #################
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want these comments in the final PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants