apxchol for Python¶
Solve sparse Laplacian and SDDM systems with reusable approximate-Cholesky factors. NumPy is required. Install the SciPy extra for sparse-matrix helpers.
Install and solve¶
python -m pip install "apxchol[scipy]"
For an unreleased checkout, use the source installation below; pip installs the published version.
Wheels work on CPU without a GPU. Compatible hardware enables the included GPU backend; no CUDA Toolkit or Homebrew installation is needed.
Platform |
CPU requirement |
Optional GPU backend |
|---|---|---|
Linux |
x86-64-v2 or newer |
NVIDIA CUDA |
macOS 15+ |
Apple silicon (arm64) |
Apple Metal |
Wheels support CPython 3.11–3.15 and PyPy 7.3.23 (Python 3.11, pp73).
On PyPy, install apxchol and use the NumPy-only interface.
import apxchol
import numpy as np
from scipy.sparse import diags
n = 1000
# Tridiagonal matrix: 3 on the diagonal, -1 on each neighboring diagonal.
A = diags([-1.0, 3.0, -1.0], [-1, 0, 1], shape=(n, n), format="csc")
b = np.ones(n)
solver = apxchol.factorize(A)
result = solver.solve(b, rtol=1e-8)
assert result.converged
x = result.x
For one solve, use apxchol.solve(A, b). Results include x, iters,
residual and converged. Convergence is always checked against the original
A and b; inspect converged for success.
See the shared binding capabilities for comparison with MATLAB/Octave.
Matrix inputs¶
Pass the matrix A in A @ x = b. For a graph’s adjacency matrix, first use
A = apxchol.laplacian(Adj) to form its Laplacian. See supported matrices
and RHS requirements.
Any SciPy sparse format is converted to sorted, duplicate-free CSC without changing the input. Supply the full symmetric matrix, including both entries of each off-diagonal pair. Complex inputs are rejected, even with zero imaginary parts.
NumPy-only input¶
python -m pip install apxchol installs the solver without SciPy. Supply the
three zero-based CSC arrays directly; all usual solver options still apply:
solver = apxchol.Solver.from_csc(data, indices, indptr, backend="auto")
result = solver.solve(b)
The matrix is square with len(indptr) - 1 rows. Integer indices may be 32- or
64-bit; the native importer sorts columns and sums duplicate entries. SciPy
is needed for factorize(A), laplacian, aslinearoperator, and sparse factor
exports. NumPy-only solves and preconditioner application do not import it.
Reuse, batches and initial guesses¶
Subsequent solves reuse the factor and workspace:
first = solver.solve(b)
b2 = np.arange(n, dtype=float)
B = np.column_stack((b, b2))
batch = solver.solve(B) # native batched solve, one RHS per column
assert batch.converged.all()
Vector inputs have shape (n,); batches have shape (n, nrhs). result.x
and apply outputs preserve that shape and use float64. Batch convergence
fields contain one value per column.
A warm start supplies an initial solution estimate:
result = solver.solve(b2, x0=first.x)
Solve argument |
Meaning |
|---|---|
|
Positive, finite relative residual tolerance; omitted uses the native default |
|
Nonnegative iteration budget; omitted uses the native default |
|
Initial guess with the same shape as the RHS; omitted starts from zero |
|
Writable, C- or F-contiguous float64 array with the RHS shape; returned as |
The native reference shows the compiled tolerance and iteration defaults.
Overlapping inputs are copied before writing out. Use separate solver
instances for concurrent calls; each solver shares mutable workspace.
Save and reload¶
Save a reusable solver’s operator and factor, then choose the solve backend when loading. These methods also work without SciPy:
solver.save("system.apxchol")
restored = apxchol.Solver.load("system.apxchol", backend="cpu")
result = restored.solve(b)
Saving requires keep_factor=True, the reusable solver’s default. It creates
a new file and refuses to replace an existing one. The archive stores the
original operator and FP32 factor; RHSs, initial guesses and device workspace
are not saved. Loading adopts the factor without repeating factorization.
backend="auto" adopts on CPU; backend="gpu" uploads to a compatible GPU.
Storage and preparation options use the same defaults as construction. Pass
lump_max_mass again if the saved operator requires positive-entry lumping.
Setup options¶
Pass these keywords to factorize or the one-shot solve:
Keyword |
Meaning |
|---|---|
|
|
|
CPU thread ceiling; |
|
Sampling seed; |
|
|
|
Retain exportable arrays; defaults to |
|
|
Read the chosen route from solver.backend and storage type from
solver.factor_storage_dtype. See backend and thread selection
for automatic selection and explicit-request behavior.
Storage defaults to FP32 on CPU/Metal and FP16 on CUDA. This affects triangular-solve storage, not export or outer solve precision. Unsupported FP16 requests fail; FP64 storage is unavailable. See precision.
Advanced options¶
These share the native defaults and can usually be left unset. GPU routes support fewer construction choices.
Keyword |
Meaning |
|---|---|
|
Degree cutoff quantile; |
|
Positive degree multiplier, used only when |
|
Prefer lower-degree vertices in selection conflicts |
|
Maximum degree for exact clique elimination; |
|
Residual pivot order: |
|
CPU triangular-solve scheduling: |
|
Relative factor-entry threshold in |
|
Nonnegative diagonal fraction added during factor construction; |
|
Allowed positive off-diagonal mass fraction in |
|
Print setup diagnostics; off by default |
Unknown keywords raise ValueError. The Python reference
and native option reference describe the controls.
Use with a SciPy solver¶
Use the built-in solver.solve for the standard PCG path. To use apxchol as a
preconditioner for another SciPy iterative method:
from scipy.sparse.linalg import cg
M = solver.aslinearoperator()
x, info = cg(A, b, M=M, rtol=1e-8)
SciPy controls convergence. Use this when you need its algorithm or interface. With a GPU factor, each application exchanges vectors with the host; the built-in GPU solve keeps the iteration on the device.
Export the factor¶
factorize retains exportable arrays by default. Exported factor values are
FP32, regardless of backend or internal storage:
P = solver.P # P[original vertex] = elimination position
G = solver.chol() # lower CSC factor, including its diagonal
L, D = solver.L, solver.D # unit lower CSC factor and diagonal
p = P.argsort()
# A[p][:, p] ≈ G @ G.T ≈ L @ diags(D) @ L.T
chol(), L and D return independent copies. For a Laplacian, they represent
the nonsingular subspace; see the factor reference.
Use keep_factor=False to save the extra host factor copy when export is
unnecessary. Repeated solves, P, factor_nnz and fill_ratio remain available.
Matrix import is limited to signed 32-bit dimensions and stored-entry counts. The native library’s wide-index build options do not enlarge this binding.
Build from source¶
From a repository checkout, install a development build and run its tests:
python -m pip install -e './python[test,scipy]'
python -m pytest python/tests -v
The -e option links the installed package to the local Python source.
Re-run the install command after changing C++ code. Local builds tune for the
build machine; see build instructions.
Set CMAKE_ARGS="-DAPXCHOL_DEVICE=CUDA" or CMAKE_ARGS="-DAPXCHOL_DEVICE=METAL"
for a GPU source build, with its native dependencies installed. Without build
isolation, install pybind11 scikit-build-core first. License.