From 069d1cd87615a152aaa2fe7001b52449ac43cc00 Mon Sep 17 00:00:00 2001 From: Pavlo Pelikh Date: Thu, 14 May 2026 12:09:07 -0300 Subject: [PATCH] Improve accessibility and backend-portable examples --- README.md | 246 ++++++++++++++++++++--------- docs/source/index.rst | 271 ++++++++++++++++++++++++++++---- docs/source/tutorials/index.rst | 8 +- 3 files changed, 423 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index c306e95..2459fb9 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,142 @@ # SpaceCore -SpaceCore is a lightweight backend-agnostic library for working with vector spaces and linear operators. +SpaceCore exists for writing numerical algorithms once, independently of the +array backend. -It provides a small set of abstractions for: +For example, the same algorithm can run with NumPy for debugging, JAX for +JIT/autodiff, and Torch for tensor workflows, while preserving the same +mathematical spaces and linear operators. -- backend-aware numerical operations -- contexts carrying backend and dtype information -- structured vector spaces -- structured linear operators -- conversion between compatible contexts +## What problem does SpaceCore solve? -## Installation +Numerical algorithms often start as clear NumPy code and later need to move to +JAX, Torch, or another array system. Without a backend boundary, that migration +usually leaks through the whole implementation: array constructors, dtype +handling, inner products, sparse support, and linear-operator conventions all +become backend-specific. -Base install: +SpaceCore keeps those choices in a `Context`, while algorithms work with +mathematical objects: -```bash -pip install spacecore +- a `Space` knows the structure and geometry of its elements; +- a `LinOp` maps one space to another; +- backend-specific array creation and operations live behind `BackendOps`. + +The result is ordinary Python code whose core numerical logic is not tied to +one array library. + +Mental model: + +```text +BackendOps -> Context -> Space/LinOp -> Algorithm ``` -With JAX support: +## Write once, run twice -```bash -pip install "spacecore[jax]" +This gradient descent loop uses only the `Space` and `LinOp` APIs. It does not +know whether the arrays are NumPy arrays, JAX arrays, or Torch tensors. + +```python +import numpy as np +import spacecore as sc + + +def as_numpy(x): + if hasattr(x, "detach"): + return x.detach().cpu().numpy() + return np.asarray(x) + + +def make_problem(ctx): + X = sc.VectorSpace((3,), ctx) + Y = sc.VectorSpace((2,), ctx) + + A = sc.DenseLinOp( + ctx.asarray([[1.0, 2.0, 3.0], [0.0, 1.0, 0.0]]), + dom=X, + cod=Y, + ctx=ctx, + ) + x = ctx.asarray([1.0, 0.0, -1.0]) + b = ctx.asarray([0.5, 0.25]) + return X, Y, A, x, b + + +def gradient_step(X, A, x, b, eta): + r = A.apply(x) - b + grad = A.rapply(r) + return X.axpy(-eta, grad, x) + + +def run_gradient_descent(X, A, x, b, eta, steps): + for _ in range(steps): + x = gradient_step(X, A, x, b, eta) + return x ``` -With PyTorch support: +Run it with NumPy: -```bash -pip install "spacecore[torch]" +```python +np_ctx = sc.Context(sc.NumpyOps(), dtype="float64") +X, Y, A, x, b = make_problem(np_ctx) +x_numpy = run_gradient_descent(X, A, x, b, eta=0.1, steps=5) +print(as_numpy(x_numpy)) ``` -* `spacecore[jax]`: installs optional JAX support. -* GPU users should install the appropriate CUDA-enabled JAX build first, following the official JAX installation guide. -* `spacecore[torch]`: installs optional PyTorch support for `torch.Tensor` backends. -* GPU users should install the appropriate CUDA-enabled PyTorch build first, following the official PyTorch installation guide. +Later, run the same problem and the same `run_gradient_descent` with JAX: -## Main concepts +```python +import jax + +jax.config.update("jax_enable_x64", True) + +jax_ctx = sc.Context(sc.JaxOps(), dtype="float64") +X, Y, A, x, b = make_problem(jax_ctx) +x_jax = run_gradient_descent(X, A, x, b, eta=0.1, steps=5) +print(as_numpy(x_jax)) + +print(np.allclose(as_numpy(x_numpy), as_numpy(x_jax))) +``` + +Run it the same way with Torch: + +```python +torch_ctx = sc.Context(sc.TorchOps(), dtype="float64") +X, Y, A, x, b = make_problem(torch_ctx) +x_torch = run_gradient_descent(X, A, x, b, eta=0.1, steps=5) +print(as_numpy(x_torch)) + +print(np.allclose(as_numpy(x_numpy), as_numpy(x_torch))) +``` + +All three backends produce the same result: + +```text +[ 1.184125 0.3411875 -0.447625 ] +[ 1.184125 0.3411875 -0.447625 ] +True +[ 1.184125 0.3411875 -0.447625 ] +True +``` + +If you do not want to enable JAX 64-bit mode, use a supported dtype such as +`"float32"`. + +## What SpaceCore is not + +SpaceCore is not an optimizer and not a NumPy/JAX/Torch replacement. It provides +backend-aware spaces, operators, and context handling so you can write your own +algorithms without wiring them to one array library. + +## Core concepts ### `Context` -A `Context` specifies how objects are represented, in particular: +A `Context` specifies how objects are represented: -* backend (`NumPy`, `JAX`, `PyTorch`, etc.) -* dtype -* validation/conversion behavior +- backend operations (`NumpyOps`, `JaxOps`, `TorchOps`, etc.); +- default dtype; +- runtime validation behavior. Constructors resolve contexts in priority order: explicit `ctx=...`, then contexts inferred from inputs, then the global default context. Advanced code @@ -52,80 +145,88 @@ that needs this resolution step directly can call ### `Space` -A `Space` describes the structure of objects space, for example: +A `Space` describes the structure and geometry of values: -* `VectorSpace` - Euclidean space -* `HermitianSpace` - space of Hermitian (symmetric) matrices -* `ProductSpace` - Cartesian product of spaces +- `VectorSpace` for Euclidean vectors and tensors; +- `HermitianSpace` for Hermitian or symmetric matrices; +- `ProductSpace` for Cartesian products of spaces. + +Algorithms should use space methods such as `zeros`, `add`, `scale`, `axpy`, +`inner`, `norm`, `flatten`, and `unflatten` instead of hard-coding backend array +operations. ### `LinOp` -A `LinOp` represents a linear operator between spaces, for example: +A `LinOp` represents a linear operator between spaces: -* `DenseLinOp` - linear operator represented by dense matrix -* `SparseLinOp` - linear operator represented by sparse matrix -* `BlockDiagonalLinOp` - linear operator from $X_1 \times \dots \times X_k$ to $Y_1 \times \dots \times Y_k$ -* `StackedLinOp` - linear operator from $X$ to $Y_1 \times \dots \times Y_k$ -* `SumToSingleLinOp` - linear operator from $X_1 \times \dots \times X_k$ to $Y$ +- `DenseLinOp` for dense matrix or tensor operators; +- `SparseLinOp` for sparse operators; +- `BlockDiagonalLinOp` for block-diagonal product-space operators; +- `StackedLinOp` for operators from one space into a product space; +- `SumToSingleLinOp` for operators from a product space into one space. -## Minimal example +Operators expose `apply` and `rapply`, so algorithms can use a linear map and +its adjoint without depending on the storage format. -```python -import numpy as np -import spacecore as sc +## Who should use this? -sc.set_context('numpy', dtype='float64') +SpaceCore is aimed at people writing optimization, inverse-problem, optimal +transport, semidefinite programming, or scientific ML algorithms that should not +be tied to one backend. -X = sc.VectorSpace((3,)) -Y = sc.VectorSpace((2,)) +It is most useful when you want the mathematical model to stay stable while the +execution backend changes. -A = np.array( - [[1.0, 2.0, 3.0], - [0.0, 1.0, 0.0]] -) -linop = sc.DenseLinOp( - A, - dom=X, - cod=Y, -) +## Installation -x = X.ctx.asarray([1.0, 0.0, -1.0]) -y = linop.apply(x) +Base install: -print(y) +```bash +pip install spacecore ``` -PyTorch tensors can be used by selecting the `torch` backend: +With JAX support: -```python -import torch -import spacecore as sc +```bash +pip install "spacecore[jax]" +``` -ctx = sc.Context(sc.TorchOps(), dtype=torch.float64) -X = sc.VectorSpace((3,), ctx) -x = ctx.asarray([1.0, 2.0, 3.0]) +With PyTorch support: -print(X.inner(x, x)) +```bash +pip install "spacecore[torch]" ``` -## Status +- `spacecore[jax]` installs optional JAX support. +- GPU users should install the appropriate CUDA-enabled JAX build first, + following the official JAX installation guide. +- `spacecore[torch]` installs optional PyTorch support for `torch.Tensor` + backends. +- GPU users should install the appropriate CUDA-enabled PyTorch build first, + following the official PyTorch installation guide. + +For local development: -SpaceCore is currently experimental and under active development. -The public API may still evolve. +```bash +python -m pip install -e ".[dev]" +``` -## Tutorials +## Full example -See the Sphinx documentation under `docs/source/` for tutorials, design notes, -API reference, and release notes. +For a complete example of regularized optimal transport problem, [see](https://pavlo3p.github.io/SpaceCore/tutorials/regularized_ot.html) +the model is written once and solved with NumPy/JAX backends and +its [notebook](https://github.com/Pavlo3P/SpaceCore/blob/master/tutorials/6_Regularized_Opt_Transport.ipynb). ## Documentation +The hosted documentation is available [here](https://pavlo3p.github.io/SpaceCore/). + The documentation website is built with Sphinx from `docs/source`. Install the documentation dependencies: ```bash -pip install -e ".[docs]" +python -m pip install -e ".[docs]" ``` Build the local HTML documentation: @@ -134,6 +235,11 @@ Build the local HTML documentation: sphinx-build -b html docs/source docs/build/html ``` +## Status + +SpaceCore is currently experimental and under active development. The public API +may still evolve. + ## License Apache License 2.0 diff --git a/docs/source/index.rst b/docs/source/index.rst index 25e3df6..709aae8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,51 +1,264 @@ -SpaceCore documentation -======================= +SpaceCore +========= -SpaceCore is a lightweight Python library for backend-agnostic vector spaces -and linear operators. It is designed for numerical algorithms that should work -with structured spaces and multiple array backends without hard-coding NumPy or -JAX throughout the algorithm. +SpaceCore exists for writing numerical algorithms once, independently of the +array backend. -The documentation has three layers: +For example, the same algorithm can run with NumPy for debugging, JAX for +JIT/autodiff, and Torch for tensor workflows, while preserving the same +mathematical spaces and linear operators. -* :doc:`tutorials/index` explain the main ideas with examples. -* :doc:`design/index` describes the policies and design choices behind the API. -* :doc:`api/index` provides explicit object-level API reference pages. -* :doc:`release_notes` records user-visible changes by release. +What problem does SpaceCore solve? +---------------------------------- -Core model ----------- +Numerical algorithms often start as clear NumPy code and later need to move to +JAX, Torch, or another array system. Without a backend boundary, that migration +usually leaks through the whole implementation: array constructors, dtype +handling, inner products, sparse support, and linear-operator conventions all +become backend-specific. -SpaceCore code is organized around three questions: +SpaceCore keeps those choices in a ``Context``, while algorithms work with +mathematical objects: -* Which backend executes array operations? -* Which space does each value belong to? -* Which linear map connects one space to another? +* a ``Space`` knows the structure and geometry of its elements; +* a ``LinOp`` maps one space to another; +* backend-specific array creation and operations live behind ``BackendOps``. -In notation, a linear operator maps one space to another: +The result is ordinary Python code whose core numerical logic is not tied to +one array library. -.. math:: +Mental model: - A : X \to Y. +.. code-block:: text -Quick example -------------- + BackendOps -> Context -> Space/LinOp -> Algorithm + +Write once, run twice +--------------------- + +This gradient descent loop uses only the ``Space`` and ``LinOp`` APIs. It does +not know whether the arrays are NumPy arrays, JAX arrays, or Torch tensors. .. code-block:: python import numpy as np import spacecore as sc - sc.set_context("numpy", dtype="float64", enable_checks=True) - X = sc.VectorSpace((3,)) - Y = sc.VectorSpace((2,)) + def as_numpy(x): + if hasattr(x, "detach"): + return x.detach().cpu().numpy() + return np.asarray(x) + + + def make_problem(ctx): + X = sc.VectorSpace((3,), ctx) + Y = sc.VectorSpace((2,), ctx) + + A = sc.DenseLinOp( + ctx.asarray([[1.0, 2.0, 3.0], [0.0, 1.0, 0.0]]), + dom=X, + cod=Y, + ctx=ctx, + ) + x = ctx.asarray([1.0, 0.0, -1.0]) + b = ctx.asarray([0.5, 0.25]) + return X, Y, A, x, b + + + def gradient_step(X, A, x, b, eta): + r = A.apply(x) - b + grad = A.rapply(r) + return X.axpy(-eta, grad, x) + + + def run_gradient_descent(X, A, x, b, eta, steps): + for _ in range(steps): + x = gradient_step(X, A, x, b, eta) + return x + +Run it with NumPy: + +.. code-block:: python + + np_ctx = sc.Context(sc.NumpyOps(), dtype="float64") + X, Y, A, x, b = make_problem(np_ctx) + x_numpy = run_gradient_descent(X, A, x, b, eta=0.1, steps=5) + print(as_numpy(x_numpy)) + +Later, run the same problem and the same ``run_gradient_descent`` with JAX: + +.. code-block:: python + + import jax + + jax.config.update("jax_enable_x64", True) + + jax_ctx = sc.Context(sc.JaxOps(), dtype="float64") + X, Y, A, x, b = make_problem(jax_ctx) + x_jax = run_gradient_descent(X, A, x, b, eta=0.1, steps=5) + print(as_numpy(x_jax)) + + print(np.allclose(as_numpy(x_numpy), as_numpy(x_jax))) + +Run it the same way with Torch: + +.. code-block:: python + + torch_ctx = sc.Context(sc.TorchOps(), dtype="float64") + X, Y, A, x, b = make_problem(torch_ctx) + x_torch = run_gradient_descent(X, A, x, b, eta=0.1, steps=5) + print(as_numpy(x_torch)) + + print(np.allclose(as_numpy(x_numpy), as_numpy(x_torch))) + +All three backends produce the same result: + +.. code-block:: text + + [ 1.184125 0.3411875 -0.447625 ] + [ 1.184125 0.3411875 -0.447625 ] + True + [ 1.184125 0.3411875 -0.447625 ] + True + +If you do not want to enable JAX 64-bit mode, use a supported dtype such as +``"float32"``. + +What SpaceCore is not +--------------------- + +SpaceCore is not an optimizer and not a NumPy/JAX/Torch replacement. It +provides backend-aware spaces, operators, and context handling so you can write +your own algorithms without wiring them to one array library. + +Core concepts +------------- + +``Context`` +~~~~~~~~~~~ + +A ``Context`` specifies how objects are represented: + +* backend operations (``NumpyOps``, ``JaxOps``, ``TorchOps``, etc.); +* default dtype; +* runtime validation behavior. + +Constructors resolve contexts in priority order: explicit ``ctx=...``, then +contexts inferred from inputs, then the global default context. Advanced code +that needs this resolution step directly can call +``spacecore.resolve_context_priority(...)``. + +``Space`` +~~~~~~~~~ + +A ``Space`` describes the structure and geometry of values: + +* ``VectorSpace`` for Euclidean vectors and tensors; +* ``HermitianSpace`` for Hermitian or symmetric matrices; +* ``ProductSpace`` for Cartesian products of spaces. + +Algorithms should use space methods such as ``zeros``, ``add``, ``scale``, +``axpy``, ``inner``, ``norm``, ``flatten``, and ``unflatten`` instead of +hard-coding backend array operations. + +``LinOp`` +~~~~~~~~~ + +A ``LinOp`` represents a linear operator between spaces: + +* ``DenseLinOp`` for dense matrix or tensor operators; +* ``SparseLinOp`` for sparse operators; +* ``BlockDiagonalLinOp`` for block-diagonal product-space operators; +* ``StackedLinOp`` for operators from one space into a product space; +* ``SumToSingleLinOp`` for operators from a product space into one space. + +Operators expose ``apply`` and ``rapply``, so algorithms can use a linear map +and its adjoint without depending on the storage format. + +Who should use this? +-------------------- + +SpaceCore is aimed at people writing optimization, inverse-problem, optimal +transport, semidefinite programming, or scientific ML algorithms that should not +be tied to one backend. + +It is most useful when you want the mathematical model to stay stable while the +execution backend changes. + +Installation +------------ + +Base install: + +.. code-block:: bash + + pip install spacecore + +With JAX support: + +.. code-block:: bash + + pip install "spacecore[jax]" + +With PyTorch support: + +.. code-block:: bash + + pip install "spacecore[torch]" + +* ``spacecore[jax]`` installs optional JAX support. +* GPU users should install the appropriate CUDA-enabled JAX build first, + following the official JAX installation guide. +* ``spacecore[torch]`` installs optional PyTorch support for ``torch.Tensor`` + backends. +* GPU users should install the appropriate CUDA-enabled PyTorch build first, + following the official PyTorch installation guide. + +For local development: + +.. code-block:: bash + + python -m pip install -e ".[dev]" + +Full example +------------ + +For a complete example of regularized optimal transport problem, +`see `_ the +model is written once and solved with NumPy/JAX backends and its +`notebook `_. + +Documentation +------------- + +The hosted documentation is available +`here `_. + +The documentation website is built with Sphinx from ``docs/source``. + +Install the documentation dependencies: + +.. code-block:: bash + + python -m pip install -e ".[docs]" + +Build the local HTML documentation: + +.. code-block:: bash + + sphinx-build -b html docs/source docs/build/html + +Status +------ + +SpaceCore is currently experimental and under active development. The public API +may still evolve. - A = np.array([[1.0, 2.0, 3.0], [0.0, 1.0, 0.0]]) - op = sc.DenseLinOp(A, dom=X, cod=Y) +License +------- - x = X.ctx.asarray([1.0, 0.0, -1.0]) - y = op.apply(x) +Apache License 2.0 .. toctree:: :maxdepth: 2 diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst index 65a3f0f..f8d8077 100644 --- a/docs/source/tutorials/index.rst +++ b/docs/source/tutorials/index.rst @@ -5,9 +5,11 @@ These pages are the Sphinx versions of the tutorial notebooks. The first tutorials explain the core abstractions before the API reference: backend execution, contexts, spaces, linear operators, and conversion policy. -The regularized optimal transport tutorial is an application example. It keeps -the executed cell outputs in the rendered docs so the convergence diagnostics -and transport-plan plots can be inspected directly. +The :doc:`regularized optimal transport tutorial ` is an +application example. It keeps the executed cell outputs in the rendered docs so +the convergence diagnostics and transport-plan plots can be inspected directly. +The notebook is also available +`on GitHub `_. .. toctree:: :maxdepth: 1