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

Start with collection. It is faster than the full suite and catches collection-time import errors before running tests:

```bash
pytest --co -q
```

Run the normal test gate:

```bash
pytest tests/ -x -q
```

Run a focused space-layer check when working on spaces, geometry, checks, or batching:

```bash
pytest tests/spaces/ -v
```

Run linting:

```bash
ruff check .
```

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/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. Algebraic operators implement composition, sums, scalar multiples, products, stacking, and block structure without eagerly materializing matrices.

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 dispatch. `spacecore/_batching.py` provides shared batching helpers. `spacecore/_checks.py` provides method-level validation decorators used by spaces, operators, and functionals; runtime check intensity is selected from `spacecore/_check_policy.py`, which defines the `CheckLevel` literal (`"none"`, `"cheap"`, `"standard"`, `"strict"`) and the `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 class map

This map lists public concrete class-like exports a contributor is likely to encounter. Abstract contracts such as `BackendOps`, `LinOp`, `Functional`, `VectorSpace`, `CoordinateSpace`, `InnerProduct`, and `SpaceCheck` are discussed above and in the ADRs.

- `ArrayLike` — runtime-checkable protocol for dense or sparse array-like values accepted by public APIs.
- `BackendCheck` — membership check that validates an element belongs to the expected backend family.
- `BackendFamily` — backend-family identifier used by contexts and backend detection.
- `BlockDiagonalLinOp` — product-space operator that applies component operators independently on each block.
- `CGResult` — result tuple returned by conjugate-gradient solves.
- `ComposedFunctional` — functional representing lazy composition of a functional with a linear operator.
- `ComposedLinOp` — lazy composition `A @ B` of compatible linear operators.
- `Context` — execution context containing backend operations, default dtype, and check policy.
- `ContextBound` — base for objects that carry a `Context` and support explicit conversion.
- `DenseArray` — runtime-checkable protocol for dense array-like values.
- `DenseCoordinateSpace` — dense coordinate representation of arrays with a fixed shape and context.
- `DenseLinOp` — dense matrix- or tensor-backed linear operator.
- `DenseVectorSpace` — one-dimensional dense coordinate vector space.
- `DTypeCheck` — membership check enforcing exact dtype membership.
- `DiagonalLinOp` — coordinatewise diagonal matrix-backed operator on one space.
- `ElementwiseJordanSpace` — dense elementwise Jordan algebra space.
- `EuclideanElementwiseJordanSpace` — real Euclidean elementwise Jordan algebra space.
- `EuclideanInnerProduct` — Euclidean inner-product geometry with identity Riesz maps.
- `ExpmMultiplyResult` — result tuple returned by exponential-action routines.
- `HermitianCheck` — membership check for Hermitian matrix structure.
- `HermitianSpace` — dense Hermitian matrix space with spectral Jordan operations.
- `IdentityLinOp` — identity linear operator on one space.
- `InnerProductFunctional` — linear functional represented by an inner product against a representer.
- `JaxOps` — optional JAX backend operations class, exported when JAX is installed.
- `LanczosResult` — result tuple returned by Lanczos routines.
- `LinOpQuadraticForm` — quadratic functional represented by a linear operator.
- `LSQRResult` — result tuple returned by LSQR solves.
- `MatrixFreeLinearFunctional` — linear functional defined by a user-supplied callable and representer behavior.
- `MatrixFreeLinOp` — linear operator defined by user-supplied forward and metric-adjoint callables.
- `NumpyOps` — NumPy/SciPy backend operations class and the baseline backend implementation.
- `PowerIterationResult` — result tuple returned by power iteration.
- `TreeElement` — optional explicit binding of ordered leaves to a tree space.
- `TreeLinOp` — base class for operators with tree-structured domains or codomains.
- `TreeSpace` — finite direct-product coordinate space represented by an optree.
- `TreeSpectralDecomposition` — leafwise spectral data in deterministic tree order.
- `ScaledLinOp` — lazy scalar multiple of a linear operator.
- `ShapeCheck` — membership check enforcing element shape.
- `SparseArray` — runtime-checkable protocol for sparse array-like values.
- `SparseLinOp` — sparse matrix-backed linear operator over coordinate spaces.
- `Space` — context-bound mathematical space with membership validation.
- `SpaceValidationError` — exception raised when an element fails space validation.
- `SquareMatrixCheck` — membership check enforcing square matrix shape.
- `StackedLinOp` — operator mapping one domain into a tree codomain.
- `StackedSpace` — space representing a fixed leading-axis stack of one base space.
- `SumLinOp` — lazy finite sum of operators with common domain and codomain.
- `SumToSingleLinOp` — operator mapping a tree domain into one codomain by summing leaf outputs.
- `TorchOps` — optional Torch backend operations class, exported when Torch is installed.
- `WeightedInnerProduct` — diagonal weighted inner-product geometry with validated positive finite weights.
- `ZeroLinOp` — zero linear operator between two spaces.
- `CuPyOps` — optional CuPy backend operations class, exported when CuPy is installed.

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 |
| `StackedLinOp` | `sc.StackedLinOp.from_operators(parts)` | `X -> TreeSpace[Y_i]` | yes | yes | leafwise forward, summed adjoints |
| `SumToSingleLinOp` | `sc.SumToSingleLinOp.from_operators(parts)` | `TreeSpace[X_i] -> Y` | yes | yes | summed forwards, leafwise adjoints |
| `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 |
| `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

A PR should normally include:

- tests for changed behavior;
- updated docstrings if public behavior changed;
- a changelog entry under `[Unreleased]`;
- documentation updates if contributor-facing behavior changed;
- mathematical invariant statement for changes touching geometry, adjoints, spectral methods, scalar fields, or batching.

Run the relevant checks before requesting review:

```bash
pytest --co -q
pytest tests/ -x -q
ruff check .
```

Use focused tests first while developing, then run the normal 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.