splineax 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.3
2
+ Name: splineax
3
+ Version: 0.2.0
4
+ Summary: Sparse linear operators and solvers for Lineax
5
+ Author: Nardi Lam
6
+ Author-email: Nardi Lam <mail@nardilam.nl>
7
+ Requires-Dist: lineax>=0.1.1
8
+ Requires-Dist: jax>=0.4
9
+ Requires-Dist: equinox>=0.11
10
+ Requires-Dist: jaxtyping>=0.2
11
+ Requires-Dist: splineax-klujax>=0.5.0.post1
12
+ Requires-Dist: asdex>=0.5.1
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+
16
+ # splineax
17
+
18
+ Sparse linear operators and direct solvers for
19
+ [Lineax](https://github.com/patrick-kidger/lineax).
20
+
21
+ `splineax` lets you keep a linear system in its native sparse storage
22
+ (`jax.experimental.sparse.BCOO` / `BCSR`) and solve it with a sparse *direct* solver that
23
+ plugs straight into `lineax.linear_solve`. It also interfaces with [asdex](https://github.com/adrhill/asdex) for calculating sparse Jacobians and using them as operators.
24
+
25
+ - **Operators**: `BCOOLinearOperator`, `BCSRLinearOperator`, `SparseJacobianLinearOperator`.
26
+ - **Solvers**: `Spsolve` (any backend), `KLU` (CPU-only, SuiteSparse, factorization reuse),
27
+ and `AutoSparseLinearSolver` (picks one based on the platform).
28
+ - A `SparseLinearSolver` protocol for separating factorization from solving.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install git+https://github.com/nardi/splineax.git@v0.1.1
34
+ ```
35
+
36
+ ## Example
37
+
38
+ Solve a 10000 x 10000 system. As a dense matrix it would need 10^8 entries, but kept
39
+ sparse it has only ~3 x 10^4 nonzeros, and the solver never materialises the dense form.
40
+
41
+ ```python
42
+ import jax.numpy as jnp
43
+ import lineax as lx
44
+ import numpy as np
45
+ from jax.experimental.sparse import BCOO
46
+
47
+ import splineax
48
+
49
+ n = 10000
50
+ np.random.seed(0)
51
+
52
+ # A large, randomly sparse matrix with a heavy diagonal (so it is invertible).
53
+ diagonal_indices = np.stack([np.arange(n), np.arange(n)], axis=1)
54
+ off_diagonal_indices = np.unique(np.random.randint(0, n, size=(2 * n, 2)), axis=0)
55
+ indices = jnp.concatenate([diagonal_indices, off_diagonal_indices])
56
+ values = jnp.concatenate(
57
+ [
58
+ np.full(n, float(n)),
59
+ np.random.uniform(low=-1, high=1, size=off_diagonal_indices.shape[0]),
60
+ ]
61
+ )
62
+ matrix = BCOO((values, indices), shape=(n, n)).sum_duplicates()
63
+
64
+ operator = splineax.BCOOLinearOperator(matrix)
65
+ vectors = [jnp.ones(n), jnp.arange(n) % 2]
66
+ solver = splineax.AutoSparseLinearSolver()
67
+
68
+ # Calculate factorization once...
69
+ with solver.factorize(operator) as factorized_state:
70
+ # ...and reuse for multiple solves.
71
+ solution = lx.linear_solve(
72
+ operator, vectors[0], solver=solver, state=factorized_state
73
+ )
74
+ assert jnp.allclose(matrix @ solution.value, vectors[0], atol=1e-4)
75
+
76
+ solution = lx.linear_solve(
77
+ operator, vectors[1], solver=solver, state=factorized_state
78
+ )
79
+ assert jnp.allclose(matrix @ solution.value, vectors[1], atol=1e-4)
80
+ ```
81
+
82
+ ## Documentation
83
+
84
+ Build the docs locally with `uv run mkdocs serve`, or view the [user guide and API reference here](https://nardi.github.io/splineax).
@@ -0,0 +1,69 @@
1
+ # splineax
2
+
3
+ Sparse linear operators and direct solvers for
4
+ [Lineax](https://github.com/patrick-kidger/lineax).
5
+
6
+ `splineax` lets you keep a linear system in its native sparse storage
7
+ (`jax.experimental.sparse.BCOO` / `BCSR`) and solve it with a sparse *direct* solver that
8
+ plugs straight into `lineax.linear_solve`. It also interfaces with [asdex](https://github.com/adrhill/asdex) for calculating sparse Jacobians and using them as operators.
9
+
10
+ - **Operators**: `BCOOLinearOperator`, `BCSRLinearOperator`, `SparseJacobianLinearOperator`.
11
+ - **Solvers**: `Spsolve` (any backend), `KLU` (CPU-only, SuiteSparse, factorization reuse),
12
+ and `AutoSparseLinearSolver` (picks one based on the platform).
13
+ - A `SparseLinearSolver` protocol for separating factorization from solving.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install git+https://github.com/nardi/splineax.git@v0.1.1
19
+ ```
20
+
21
+ ## Example
22
+
23
+ Solve a 10000 x 10000 system. As a dense matrix it would need 10^8 entries, but kept
24
+ sparse it has only ~3 x 10^4 nonzeros, and the solver never materialises the dense form.
25
+
26
+ ```python
27
+ import jax.numpy as jnp
28
+ import lineax as lx
29
+ import numpy as np
30
+ from jax.experimental.sparse import BCOO
31
+
32
+ import splineax
33
+
34
+ n = 10000
35
+ np.random.seed(0)
36
+
37
+ # A large, randomly sparse matrix with a heavy diagonal (so it is invertible).
38
+ diagonal_indices = np.stack([np.arange(n), np.arange(n)], axis=1)
39
+ off_diagonal_indices = np.unique(np.random.randint(0, n, size=(2 * n, 2)), axis=0)
40
+ indices = jnp.concatenate([diagonal_indices, off_diagonal_indices])
41
+ values = jnp.concatenate(
42
+ [
43
+ np.full(n, float(n)),
44
+ np.random.uniform(low=-1, high=1, size=off_diagonal_indices.shape[0]),
45
+ ]
46
+ )
47
+ matrix = BCOO((values, indices), shape=(n, n)).sum_duplicates()
48
+
49
+ operator = splineax.BCOOLinearOperator(matrix)
50
+ vectors = [jnp.ones(n), jnp.arange(n) % 2]
51
+ solver = splineax.AutoSparseLinearSolver()
52
+
53
+ # Calculate factorization once...
54
+ with solver.factorize(operator) as factorized_state:
55
+ # ...and reuse for multiple solves.
56
+ solution = lx.linear_solve(
57
+ operator, vectors[0], solver=solver, state=factorized_state
58
+ )
59
+ assert jnp.allclose(matrix @ solution.value, vectors[0], atol=1e-4)
60
+
61
+ solution = lx.linear_solve(
62
+ operator, vectors[1], solver=solver, state=factorized_state
63
+ )
64
+ assert jnp.allclose(matrix @ solution.value, vectors[1], atol=1e-4)
65
+ ```
66
+
67
+ ## Documentation
68
+
69
+ Build the docs locally with `uv run mkdocs serve`, or view the [user guide and API reference here](https://nardi.github.io/splineax).
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "splineax"
3
+ version = "0.2.0"
4
+ description = "Sparse linear operators and solvers for Lineax"
5
+ readme = "README.md"
6
+ authors = [{ name = "Nardi Lam", email = "mail@nardilam.nl" }]
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "lineax>=0.1.1",
10
+ "jax>=0.4",
11
+ "equinox>=0.11",
12
+ "jaxtyping>=0.2",
13
+ "splineax-klujax>=0.5.0.post1",
14
+ "asdex>=0.5.1",
15
+ ]
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.11.7,<0.12.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "mkdocs-material>=9.7.6",
24
+ "mkdocstrings[python]>=0.27",
25
+ "pytest>=9.1.0",
26
+ "pytest-markdown-docs>=0.9.2",
27
+ "ruff>=0.15.17",
28
+ "ty>=0.0.59",
29
+ ]
30
+
31
+ [tool.pytest.ini_options]
32
+ # Execute the code examples in the markdown docs (and README) as tests via
33
+ # pytest-markdown-docs. The superfences syntax lets dependent code blocks share state
34
+ # with a `{.python continuation}` fence, which also renders under Material for MkDocs.
35
+ addopts = "--markdown-docs --markdown-docs-syntax superfences"
36
+ testpaths = ["tests", "docs", "README.md"]
37
+
38
+ [tool.ruff.lint]
39
+ ignore = ["F722"]
40
+ select = ["E4", "E7", "E9", "F", "I"]
@@ -0,0 +1,30 @@
1
+ from .operators import (
2
+ BCOOLinearOperator as BCOOLinearOperator,
3
+ )
4
+ from .operators import (
5
+ BCSRLinearOperator as BCSRLinearOperator,
6
+ )
7
+ from .operators import (
8
+ JacobianColoring as JacobianColoring,
9
+ )
10
+ from .operators import (
11
+ SparseJacobianLinearOperator as SparseJacobianLinearOperator,
12
+ )
13
+ from .operators import (
14
+ SparseJacobianLinearOperatorColoring as SparseJacobianLinearOperatorColoring,
15
+ )
16
+ from .solvers import (
17
+ KLU as KLU,
18
+ )
19
+ from .solvers import (
20
+ AbstractSparseLinearSolver as AbstractSparseLinearSolver,
21
+ )
22
+ from .solvers import (
23
+ AutoSparseLinearSolver as AutoSparseLinearSolver,
24
+ )
25
+ from .solvers import (
26
+ SparseLinearSolver as SparseLinearSolver,
27
+ )
28
+ from .solvers import (
29
+ Spsolve as Spsolve,
30
+ )
@@ -0,0 +1,7 @@
1
+ from ._bcoo import BCOOLinearOperator as BCOOLinearOperator
2
+ from ._bcsr import BCSRLinearOperator as BCSRLinearOperator
3
+ from ._jacobian import JacobianColoring as JacobianColoring
4
+ from ._jacobian import SparseJacobianLinearOperator as SparseJacobianLinearOperator
5
+ from ._jacobian import (
6
+ SparseJacobianLinearOperatorColoring as SparseJacobianLinearOperatorColoring,
7
+ )
@@ -0,0 +1,78 @@
1
+ import equinox as eqx
2
+ import jax
3
+ import jax.numpy as jnp
4
+ from jax.experimental.sparse import BCOO
5
+ from jaxtyping import Array, Inexact
6
+ from lineax import AbstractLinearOperator, is_symmetric
7
+ from lineax._tags import transpose_tags
8
+
9
+ from ._operations import (
10
+ register_sparse_operator,
11
+ sparse_as_matrix,
12
+ sparse_in_structure,
13
+ sparse_mv,
14
+ sparse_out_structure,
15
+ )
16
+
17
+
18
+ class BCOOLinearOperator(AbstractLinearOperator):
19
+ """Wraps a `jax.experimental.sparse.BCOO` array into a linear operator.
20
+
21
+ If the matrix has shape `(a, b)` then matrix-vector multiplication (`self.mv`) is
22
+ defined in the usual way: as accepting a vector of shape `(b,)` and returning a
23
+ vector of shape `(a,)`.
24
+ """
25
+
26
+ matrix: Inexact[BCOO, "a b"]
27
+ tags: frozenset[object] = eqx.field(static=True)
28
+
29
+ def __init__(
30
+ self, matrix: Inexact[BCOO, "a b"], tags: object | frozenset[object] = ()
31
+ ):
32
+ """**Arguments:**
33
+
34
+ - `matrix`: a two-dimensional `BCOO` array. For an array with shape `(a, b)`
35
+ then this operator can perform matrix-vector products on a vector of shape
36
+ `(b,)` to return a vector of shape `(a,)`.
37
+ - `tags`: any tags indicating whether this matrix has any particular properties,
38
+ like symmetry or positive-definite-ness. Note that these properties are
39
+ unchecked and you may get incorrect values elsewhere if these tags are
40
+ wrong.
41
+ """
42
+ if matrix.ndim != 2:
43
+ raise ValueError(
44
+ "`BCOOLinearOperator(matrix=...)` should be 2-dimensional."
45
+ )
46
+ if not jnp.issubdtype(matrix.dtype, jnp.inexact):
47
+ matrix = BCOO(
48
+ (matrix.data.astype(float), matrix.indices), shape=matrix.shape
49
+ )
50
+ self.matrix = matrix
51
+ self.tags = tags if isinstance(tags, frozenset) else frozenset([tags])
52
+
53
+ def mv(self, vector: Inexact[Array, " b"]) -> Inexact[Array, " a"]:
54
+ return sparse_mv(self.matrix, vector)
55
+
56
+ def as_matrix(self) -> Inexact[Array, "a b"]:
57
+ return sparse_as_matrix(self.matrix)
58
+
59
+ def transpose(self) -> "BCOOLinearOperator":
60
+ if is_symmetric(self):
61
+ return self
62
+ matrix_T: BCOO = self.matrix.T
63
+ return BCOOLinearOperator(matrix_T, transpose_tags(self.tags))
64
+
65
+ def in_structure(self) -> jax.ShapeDtypeStruct:
66
+ return sparse_in_structure(self)
67
+
68
+ def out_structure(self) -> jax.ShapeDtypeStruct:
69
+ return sparse_out_structure(self)
70
+
71
+ def _conj(self) -> "BCOOLinearOperator":
72
+ matrix = BCOO(
73
+ (self.matrix.data.conj(), self.matrix.indices), shape=self.matrix.shape
74
+ )
75
+ return BCOOLinearOperator(matrix, self.tags)
76
+
77
+
78
+ register_sparse_operator(BCOOLinearOperator)
@@ -0,0 +1,82 @@
1
+ import equinox as eqx
2
+ import jax
3
+ import jax.numpy as jnp
4
+ from jax.experimental.sparse import BCOO, BCSR
5
+ from jaxtyping import Array, Inexact
6
+ from lineax import AbstractLinearOperator, is_symmetric
7
+ from lineax._tags import transpose_tags
8
+
9
+ from ._operations import (
10
+ register_sparse_operator,
11
+ sparse_as_matrix,
12
+ sparse_in_structure,
13
+ sparse_mv,
14
+ sparse_out_structure,
15
+ )
16
+
17
+
18
+ class BCSRLinearOperator(AbstractLinearOperator):
19
+ """Wraps a `jax.experimental.sparse.BCSR` array into a linear operator.
20
+
21
+ If the matrix has shape `(a, b)` then matrix-vector multiplication (`self.mv`) is
22
+ defined in the usual way: as accepting a vector of shape `(b,)` and returning a
23
+ vector of shape `(a,)`.
24
+ """
25
+
26
+ matrix: Inexact[BCSR, "a b"]
27
+ tags: frozenset[object] = eqx.field(static=True)
28
+
29
+ def __init__(
30
+ self, matrix: Inexact[BCSR, "a b"], tags: object | frozenset[object] = ()
31
+ ):
32
+ """**Arguments:**
33
+
34
+ - `matrix`: a two-dimensional `BCSR` array. For an array with shape `(a, b)`
35
+ then this operator can perform matrix-vector products on a vector of shape
36
+ `(b,)` to return a vector of shape `(a,)`.
37
+ - `tags`: any tags indicating whether this matrix has any particular properties,
38
+ like symmetry or positive-definite-ness. Note that these properties are
39
+ unchecked and you may get incorrect values elsewhere if these tags are
40
+ wrong.
41
+ """
42
+ if matrix.ndim != 2:
43
+ raise ValueError(
44
+ "`BCSRLinearOperator(matrix=...)` should be 2-dimensional."
45
+ )
46
+ if not jnp.issubdtype(matrix.dtype, jnp.inexact):
47
+ matrix = BCSR(
48
+ (matrix.data.astype(jnp.float32), matrix.indices, matrix.indptr),
49
+ shape=matrix.shape,
50
+ )
51
+ self.matrix = matrix
52
+ self.tags = tags if isinstance(tags, frozenset) else frozenset([tags])
53
+
54
+ def mv(self, vector: Inexact[Array, " b"]) -> Inexact[Array, " a"]:
55
+ return sparse_mv(self.matrix, vector)
56
+
57
+ def as_matrix(self) -> Inexact[Array, "a b"]:
58
+ return sparse_as_matrix(self.matrix)
59
+
60
+ def transpose(self) -> "BCSRLinearOperator":
61
+ if is_symmetric(self):
62
+ return self
63
+ # `BCSR.transpose` is not implemented in JAX; round-trip through `BCOO`.
64
+ matrix_T_bcoo: BCOO = self.matrix.to_bcoo().T
65
+ matrix_T = BCSR.from_bcoo(matrix_T_bcoo)
66
+ return BCSRLinearOperator(matrix_T, transpose_tags(self.tags))
67
+
68
+ def in_structure(self) -> jax.ShapeDtypeStruct:
69
+ return sparse_in_structure(self)
70
+
71
+ def out_structure(self) -> jax.ShapeDtypeStruct:
72
+ return sparse_out_structure(self)
73
+
74
+ def _conj(self) -> "BCSRLinearOperator":
75
+ matrix = BCSR(
76
+ (self.matrix.data.conj(), self.matrix.indices, self.matrix.indptr),
77
+ shape=self.matrix.shape,
78
+ )
79
+ return BCSRLinearOperator(matrix, self.tags)
80
+
81
+
82
+ register_sparse_operator(BCSRLinearOperator)