Contributor documentation#
The canonical contributor documentation lives in docs/dev/contributing.
The Markdown files are included here so they are reachable from the built docs
without adding a Markdown parser dependency to Sphinx.
Setup#
# Contributor setup
SpaceCore supports Python 3.11 and newer. The minimal contributor setup uses the core NumPy/SciPy dependency set plus development tools.
## Clone and install
```bash
git clone https://github.com/Pavlo3P/SpaceCore
cd SpaceCore
pip install -e ".[dev]"
```
`.[dev]` is the minimal contributor install. It installs SpaceCore in editable mode, the default NumPy-backed runtime dependencies, and contributor tools such as pytest and Ruff.
Use the fuller optional-backend install when you are working on backend-specific behavior:
```bash
pip install -e ".[jax,torch,dev]"
```
The `[jax,torch,dev]` install is for contributors changing JAX or Torch paths. Optional backend tests should skip cleanly when JAX or Torch is absent, so they are not required for ordinary NumPy-backed changes.
## Verify the environment
The collection, test and lint commands are in [CONTRIBUTING.md](../../../CONTRIBUTING.md);
collection (`pytest --co -q`) is the fastest check and catches import errors first. When
working on spaces, geometry, checks or batching, run the focused suite first:
```bash
pytest tests/spaces/ -v
```
The minimal `.[dev]` install should run the NumPy-backed test suite. Tests for optional backends should either run when their packages are installed or skip without causing the NumPy contributor workflow to fail.
Architecture#
# Contributor architecture guide
SpaceCore is organized around mathematical contracts first and array containers second. A contributor should decide which layer owns a behavior before changing code: backend libraries own raw array execution, while SpaceCore owns spaces, geometry, validation, typed maps, and solver contracts.
## Backend layer
`spacecore/backend/` defines the backend abstraction. `BackendOps` is the contract implemented by concrete operation providers such as `NumpyOps` and optional JAX, Torch, and CuPy implementations. Core code should prefer `ops.method(...)` over direct backend calls when behavior must be portable.
The backend layer assumes Array API-style dense operations where possible, but it does not pretend that sparse arrays, dtype promotion, devices, tracing, mutation, or control flow are identical across NumPy, JAX, Torch, and CuPy. Backend detection is conservative: contexts and arrays are associated with a registered backend family, and optional backend classes are available only when their dependencies import successfully.
SpaceCore owns context normalization, validation hooks, conversion entry points, sparse/dense predicates, vectorization wrappers, and backend-independent control-flow helpers. NumPy, SciPy, JAX, Torch, and CuPy own their native array semantics, device placement, autograd/tracing behavior, kernel execution, sparse storage details, and low-level dtype behavior.
## Space layer
`spacecore/space/` represents mathematical structure, not merely array shapes. `VectorSpace` is the abstract linear-space contract for zero, add, scale, and axpy operations. Coordinate spaces add shape, size, flattening, unflattening, and batching hooks. Dense coordinate and dense vector spaces provide concrete dense array representations; `TreeSpace` and `TreeElement` represent finite direct products organized by an `optree` definition, and `StackedSpace` represents a fixed leading-axis stack of one base space.
Spaces define valid elements through membership checks. Checks cover backend ownership, shape, dtype, product structure, Hermitian structure, square-matrix structure, and component membership. Spaces also own scalar-field-relevant dtype expectations, zero/add/scale behavior, and batching helpers such as batch flattening and unflattening.
Inner-product geometry is part of the space contract. Euclidean and weighted geometries define inner products, norms, and Riesz maps where available. Code that changes geometry must preserve the declared mathematical structure, not just pass array-shape tests.
## LinOp and Functional layers
`spacecore/mapping/` holds `Mapping`, the common parent of `LinOp` and `Functional`, with the lazy algebra both families share: the operator-overload rule table, the scaled/sum/zero nodes and the scalar helpers. It also derives equality from a class's declared `_fields` (see `docs/source/design/extending.rst`).
`spacecore/linop/` contains typed maps between domain and codomain spaces. `apply(x)` evaluates the forward map `A x`; `rapply(y)` evaluates the metric adjoint `A^sharp y`; `.H` returns an adjoint view that swaps those actions. `LinOp` owns these checked public methods and subclasses implement the check-free cores `_apply_core`, `_rapply_core` (and optionally the batched ones), which composites and solvers call directly. Algebraic operators implement composition, sums, scalar multiples, and block structure without eagerly materializing matrices; `BlockMatrixLinOp` also covers stacking one space into a tree (one column) and summing a tree into one space (one row).
Matrix-backed operators own a coordinate matrix or tensor, so they can compute metric adjoints with Riesz maps. Matrix-free operators do not own a matrix. `MatrixFreeLinOp.rapply` is a user-supplied assertion that the callable is already the correct metric adjoint for the declared spaces. SpaceCore validates membership when checks are enabled, but it does not derive or correct matrix-free adjoints.
`spacecore/functional/` contains scalar-valued maps. Functionals expose values, gradients where available, and pull-backs through LinOp composition. Gradients are space elements and must respect the domain geometry. Batched application paths should agree row-by-row with scalar `apply` or `rapply` behavior.
## Linalg layer
`spacecore/linalg/` contains iterative algorithms such as CG, LSQR, Lanczos, power iteration, and exponential action. These routines use space operations for vector arithmetic and norms, use LinOp contracts for forward and adjoint products, and avoid assuming a concrete matrix unless documented.
Solver assumptions are mathematical assumptions over declared spaces: Hermitian, positive-definite, residual, adjoint, and norm statements are with respect to the relevant space geometry. Backend independence comes from using `BackendOps` control-flow and array helpers rather than backend-specific logic in solver bodies.
## Cross-cutting infrastructure
`spacecore/contextual/` owns context-bound objects, context normalization, default-context state, backend registration, and conversion, which `ContextBound` derives from a class's declared `_fields`; `PyTreeNode` in `spacecore/backend/_container.py` derives the pytree hooks from the same declaration. `spacecore/_batching.py` provides shared batching helpers. `spacecore/_checks.py` provides the method-level validation decorators used by spaces, operators, and functionals, and defines the `CheckLevel` literal (`"none"`, `"cheap"`, `"standard"`, `"strict"`) with its `CHECK_LEVELS` ordering. Tree-structured handling lives with the space implementations in `spacecore/space/concrete/_tree_space.py` (using `optree`) rather than in a top-level helper module. `spacecore/_version.py` is the package version source used by packaging and docs.
## Public names
The public API is `spacecore.__all__`; every exported name is documented, from its
docstring, on the API pages under `docs/source/api/`. To see the current list:
```bash
python -c "import spacecore; print(sorted(spacecore.__all__))"
```
Optional backends (`JaxOps`, `TorchOps`, `CuPyOps`) appear only when their package is
installed. `BackendFamily` and the other backend internals live in `spacecore.backend`.
LinOp generators#
# LinOp generator coverage
Generated LinOp tests live in `tests/generators/linops.py`. Each `LinOpCase`
stores the operator in `obj` and the following reference data: domain,
codomain, scalar inputs, batched inputs, direct `apply` and `rapply` results,
an optional coordinate matrix, and explicit batching/conversion capability
flags.
## Public family inventory
| Family | Constructor | Domain to codomain | `.H` | Batch | Reference |
| --- | --- | --- | --- | --- | --- |
| `DenseLinOp` | `sc.DenseLinOp(A, X, Y)` | coordinate `X -> Y` | yes | yes | dense matrix action and metric adjoint |
| `SparseLinOp` | `sc.SparseLinOp(A, X, Y)` | coordinate `X -> Y` | yes | yes | dense equivalent; skip unsupported sparse backends |
| `DiagonalLinOp` | `sc.DiagonalLinOp(d, X)` | `X -> X` | yes | yes | elementwise diagonal and conjugate diagonal |
| `IdentityLinOp` | `sc.IdentityLinOp(X)` | `X -> X` | yes | yes | unchanged input |
| `ZeroLinOp` | `sc.ZeroLinOp(X, Y)` | `X -> Y` | yes | yes | space-owned zero values |
| `MatrixFreeLinOp` | `sc.MatrixFreeLinOp(apply, rapply, X, Y)` | any compatible `X -> Y` | yes | yes | explicit forward and true metric-adjoint callables |
| `ScaledLinOp` | `sc.ScaledLinOp(alpha, A)` | same as `A` | yes | yes | `alpha A` and `conj(alpha) A^sharp` |
| `SumLinOp` | `sc.SumLinOp((A, B))` | common `X -> Y` | yes | yes | sum of direct references |
| `ComposedLinOp` | `sc.ComposedLinOp(B, A)` | `X -> Z` through `Y` | yes | yes | composed matrices/functions |
| `BlockDiagonalLinOp` | `sc.BlockDiagonalLinOp(block_tree)` | `TreeSpace[X_i] -> TreeSpace[Y_i]` | yes | yes | leafwise application |
| `BlockMatrixLinOp` | `sc.BlockMatrixLinOp(rows)` | `TreeSpace[X_j] -> TreeSpace[Y_i]` | yes | yes | row sums and adjoint column sums |
| one-column `BlockMatrixLinOp` | `sc.BlockMatrixLinOp.stack(parts)` | `X -> TreeSpace[Y_i]` | yes | yes | leafwise forward, summed adjoints |
| one-row `BlockMatrixLinOp` | `sc.BlockMatrixLinOp.from_operators(parts)` | `TreeSpace[X_i] -> Y` | yes | yes | summed forwards, leafwise adjoints |
| `TreeLinOp` | abstract base | structured endpoints | inherited | inherited | no direct case; cover every concrete subclass |
## Adding a case
1. Build backend values from deterministic NumPy data and convert them through
the case `Context`.
2. Store both forward and reverse references. For coordinate matrix `A` and
metric matrices `G_X`, `G_Y`, the reverse matrix is
`solve(G_X, A.conj().T @ G_Y)`, not merely `A.conj().T`.
3. Provide leading-axis batch inputs and direct batched references. A
`TreeSpace` batch is a tree whose leaves each carry the same leading axis.
4. For block operators, use `TreeSpace` exclusively. Block-matrix forward
references are row sums; reverse references are sums down the adjoint
columns.
5. Set `supports_batching` or `supports_conversion` to false only when the
public family genuinely does not support the operation, and add a focused
test for the documented error.
6. Add the concrete class to the inventory assertion in
`tests/linops/test_generated_linop_laws.py`.
Describe structured operator inputs and outputs as `TreeSpace` values and do
not add legacy structured-space compatibility paths to new generator code.
Prerequisites#
# Contributor prerequisites
SpaceCore does not abstract away the mathematics. It gives mathematical structures to objects. A geometrically incorrect contribution that passes tests is still wrong; mathematical correctness is part of review.
| Contribution type | Mathematical prerequisite | Pointer |
| --- | --- | --- |
| Adding or fixing a docstring | None | `setup.md` |
| Adding a test for existing behavior | Basic NumPy and pytest | `setup.md` |
| New check in `_checks.py` | Linear algebra: shapes, dtypes, validation cost | ADR-014 |
| Dtype or scalar-field behavior | Scalar fields, dtype promotion, backend array semantics | ADR-015 |
| New `InnerProduct` subclass | Inner product spaces and Riesz maps | ADR-004 and ADR-009 |
| New matrix-backed `LinOp` | Linear maps and adjoints | LinOp ADRs |
| New matrix-free `LinOp` | Linear maps, metric adjoints, and algorithm knowledge | LinOp ADRs |
| New linalg method | Algorithm theory and convergence assumptions | Must cite a reference in PR |
| Extending Jordan hierarchy | Jordan algebras and spectral calculus | ADR-012 |
| New Space type | Functional analysis and all relevant lower-level contracts | Architecture ADRs |
Use the lightest prerequisite that is honest for the change. Documentation-only and test-only contributions should not require solver theory. Geometry, adjoint, scalar-field, batching, and spectral changes require explicit mathematical reasoning in the PR because tests can miss invalid assumptions.
Useful ADR entry points:
- ADR-003 for space hierarchy and coordinate representation.
- ADR-004 for inner products and geometry.
- ADR-007 and ADR-008 for LinOp contracts and subclasses.
- ADR-009 for metric adjoints and Riesz maps.
- ADR-011 for linalg solver contracts.
- ADR-012 for Jordan spectral behavior.
- ADR-014 for validation/check policy.
- ADR-015 for dtype defaults and scalar-field planning.
Process#
# Contributor process
## Branch naming
Use one of these branch prefixes:
```text
feature/<short-name>
fix/<short-name>
docs/<short-name>
test/<short-name>
```
Choose the prefix by the dominant intent of the change. Split unrelated work instead of hiding multiple intents in one branch.
### Branch and merge policy
Contributor infrastructure and documentation may be drafted on a feature branch.
They count toward a release only after they are merged into the default branch.
Contributor docs stranded on side branches do not satisfy the release gate.
## Before opening a PR
The PR checklist and the check commands are in [CONTRIBUTING.md](../../../CONTRIBUTING.md).
Use focused tests while developing, then run the full gate before the PR is ready.
## Review order
Review checks correctness first, then style. Formatting, naming, and cleanup matter, but they do not compensate for an invalid mathematical contract.
For mathematical code, review should check:
- domain and codomain spaces;
- scalar field;
- inner-product assumptions;
- adjoint identity;
- validation level;
- backend behavior;
- batching behavior where applicable.
A PR touching optional backends should state whether NumPy behavior is unchanged and whether optional backend tests run or skip in a clean minimal install.
## Blocked PRs
Blocked PRs should be narrowed:
- document the blocker;
- split unrelated work;
- merge completed safe parts;
- open follow-up issues for design questions.
Do not keep a large PR open only because one design question is unresolved. If the safe subset has a clear contract and tests, split it out.
## Patch release policy
A patch release is appropriate for:
- regression fix;
- packaging fix;
- documentation fix that affects install/use;
- optional-backend import or skip fix.
Feature additions, broad refactors, and changed mathematical contracts should target a normal minor release unless maintainers explicitly decide otherwise.
Labels#
# GitHub labels
This page defines the intended SpaceCore issue and pull request label taxonomy. Maintainers can recreate these labels with:
```bash
scripts/setup_labels.sh
```
## Contributor-readiness labels
`good-first-issue` is only for tasks where the issue body gives the exact file, an example to follow, and a concrete done condition. A contributor should be able to complete it without asking a design question.
`help-wanted` marks well-specified work that may require one clarification.
`needs-design` means the design direction is not settled. Do not assign these issues to new contributors.
## Type labels
`documentation` marks documentation-only work.
`test` marks tests and test infrastructure work.
`fix` marks bug fixes or correctness fixes.
`bug` marks reports of incorrect behavior or regressions.
`feature` marks new features or API extensions.
## Component labels
`backend` covers `BackendOps`, `Context`, Array API integration, and optional backend behavior.
`space` covers spaces, elements, inner products, capabilities, and checks.
`linop` covers linear operators, adjoints, matrix-backed operators, and matrix-free operators.
`functional` covers functionals, gradients, and pull-backs.
`linalg` covers iterative algorithms and solver infrastructure.
`docs` covers the documentation site, tutorials, and developer documentation.
`ci` covers continuous integration, release checks, and automation.
`release` covers release preparation, changelog updates, tagging, and packaging.
## Maintainer rules
`good-first-issue` is restrictive. Do not use it for tasks involving unsettled design, new mathematical abstractions, dtype/field policy changes, batching redesign, metric-adjoint semantics, or block-operator design unless the issue body pins the exact file, expected behavior, and done condition.
`needs-design` means the task is intentionally not ready for implementation. New contributors should not be assigned these issues.
Every geometry-, adjoint-, spectral-, field-, or batching-related issue should contain a mathematical note.