Skip to content

LinOp endpoints on VectorSpace instead of CoordinateSpace - #67

Merged
Pavlo3P merged 6 commits into
0.4.4from
66-task-linop-endpoints-on-vectorspace-instead-of-coordinatespace
Sep 12, 2026
Merged

Pavlo3P merged 6 commits into
0.4.4from
66-task-linop-endpoints-on-vectorspace-instead-of-coordinatespace

Conversation

@Pavlo3P

@Pavlo3P Pavlo3P commented Sep 12, 2026

Copy link
Copy Markdown
Owner

What this PR does

Binds LinOp's abstract endpoints to VectorSpace instead of CoordinateSpace, and makes every operation that genuinely needs coordinates, batching, or an inner product require that capability explicitly.

Closes #66.

Checklist

  • Tests pass (pytest tests/ -x -q) — 3718 passed, 170 skipped; ruff clean
  • Docstring added or updated, if public behavior changed
  • CHANGELOG entry added under [Unreleased]
  • If this PR touches geometry, adjoints, spectral methods, scalar fields, or batching: describe the mathematical invariant being preserved or added below.

Mathematical invariant

A linear operator needs only the vector-space structure of its endpoints:

A(x + y) = A x + A y,    A(a x) = a A x.

Coordinates are a representation of a space, not part of that contract, so requiring them at the type level was a category error.

The metric adjoint is the one operation that genuinely needs more. It is defined by

<A x, y>_Y = <x, A# y>_X,

which needs an inner product on both endpoints — not coordinates. CoordinateSpace and InnerProductSpace are siblings under VectorSpace, so the old bound never implied that requirement either; after this PR it is checked rather than assumed, and rapply raises CapabilityError on an endpoint without geometry.

This is also why the new bound is VectorSpace and not InnerProductSpace: the latter would have narrowed the accepted set and dropped bare TreeSpace and StackedSpace, which are coordinate spaces with no inner product.

MatrixFreeLinOp's strict adjoint probe is preserved rather than weakened. It previously required coordinates and so silently skipped every non-coordinate space — the failure metric_rapply documents as undetectable on a Euclidean geometry. It now builds its probe element from the space's own ones where one exists, and test_strict_probe_runs_on_noncoordinate_space_and_catches_a_wrong_adjoint feeds it a coordinate transpose on a non-Euclidean non-coordinate space and asserts it is rejected.

Commits

Each of the first four was tested before it was committed. The first three must leave the suite identical, since they add checks without widening anything.

  1. 6b354dd — extract space capabilities into one module (ADR-005), add CapabilityError, require, probe_element, registry_key
  2. 6fba97c — gate materialization, batching and geometry; repair the adjoint probe; drop the shape pre-filter from endpoint compatibility
  3. dafbd0c — bind both endpoints to VectorSpace (three lines)
  4. daa508d — a test space with an inner product and no coordinate surface
  5. 01ed363 — export CapabilityError
  6. fbf68be — changelog

Not in this PR

Functional, any shared Mapping supertype, scalar-field objects, and the twenty coordinate-dependent sites in spacecore/linalg/. Solver defaults derived from dimension still assume size and need an explicit iteration limit on a space without it — that triage is outstanding.

Release-gate items not run here: JIT audit, docs build, backends other than the default environment.

🤖 Generated with Claude Code

Pavlo3P and others added 6 commits September 12, 2026 15:18
Move `_space_capabilities` out of `_tree_space.py` into
`spacecore/space/_capabilities.py`, add a coordinate member, and add
`CapabilityError` plus a `require(space, capability, operation)` helper
so a missing capability names itself instead of surfacing as an
`AttributeError`.

Capability queries stay `isinstance` tests against classes, which is what
ADR-005 requires. `SupportsOnes` is the one structural exception: `ones`
is optional on spaces that are otherwise unrelated, so it cannot be a
nominal base, and `probe_element` uses it to build a deterministic
non-zero element from either `ones` or the coordinate surface.

Adding a capability changed what `_space_capabilities` returns, which the
tree and stacked class registries are keyed on. `registry_key` strips the
capabilities that every registry member already has, and both the
registration tables and both lookups go through it, so the next capability
added is declared in one place rather than silently breaking dispatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only three groups of operator operations need more than `zeros`/`add`/
`scale`: materialization (`to_dense`, `to_matrix`, `fuse`, basis probes),
batching (`vapply`, `rvapply`, `_batched_inner`), and `rapply`, whose
metric-adjoint contract needs an inner product. Each now requires its
capability up front and raises `CapabilityError` naming the operation.

`to_dense` and `to_matrix` check before touching `shape`: `to_matrix`
catches `TypeError` around its batched path and falls back to a loop that
is equally coordinate-dependent, so a check inside that block would be
swallowed and fail later instead.

`_check_adjoint_consistency` now requires only `InnerProductSpace` and
builds its probe through `probe_element`, so a space without coordinates
is still checked when it can produce a non-zero element. It was silently
skipping every such space, and `metric_rapply` documents that a wrong
adjoint is undetectable on a Euclidean geometry.

`_same_space_for_algebra` no longer compares `shape`. It was a pre-filter;
the type identity, `same_math` and convert-equality checks that follow it
already decide compatibility, and `CoordinateSpace._eq_algebra` compares
shape, so mismatched shapes are still rejected.

Bounds are unchanged here, so the suite must pass exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A linear operator needs only the vector-space structure of its endpoints.
Coordinates are a representation, so requiring them at the type level was
a category error; every operation that genuinely needs them checks for
them as of the previous commit.

`VectorSpace` and not `InnerProductSpace`: the latter is a *sibling* of
`CoordinateSpace`, not a supertype, so binding to it would narrow the
accepted set and drop bare `TreeSpace` and `StackedSpace`, which are
coordinate spaces with no inner product.

Concrete operators are unaffected: `SparseLinOp` and `DiagonalLinOp`
declare `LinOp[CoordinateSpace, CoordinateSpace]` themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`MetricArraySpace` is a `VectorSpace` with a diagonal non-Euclidean inner
product and none of `shape`, `size`, `flatten`, `unflatten` or `stacked`.
Over it, `apply`, `rapply`, `+`, scalar `*`, `@` and `.H` work, and the
metric adjoint is checked against the identity `<Ax,y>_Y = <x,A#y>_X` with
an assertion that it differs from the coordinate transpose -- a Euclidean
geometry cannot tell the two apart.

`to_dense`, `to_matrix`, `vapply` and `rvapply` must raise
`CapabilityError` rather than `AttributeError`, across every node class
and all three check levels, and a space with no inner product must fail
`rapply` the same way.

Two of these pin the review fixes: a wrong adjoint on a non-coordinate
space is now caught by the strict probe (it was silently skipped), and
`registry_key` keeps stacked dispatch reaching the inner-product
specialization now that the coordinate capability is reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gating commits raise it directly at callers, who must be able to catch
it without importing from `spacecore._errors`. It joins
`SpaceValidationError` in `spacecore.space` and at the top level, which is
where the other space-level error already lives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Pavlo3P Pavlo3P added this to the SpaceCore 0.4.4 milestone Sep 12, 2026
@Pavlo3P
Pavlo3P merged commit 06caa10 into 0.4.4 Sep 12, 2026
5 checks passed
@Pavlo3P
Pavlo3P deleted the 66-task-linop-endpoints-on-vectorspace-instead-of-coordinatespace branch September 12, 2026 19:48
Pavlo3P added a commit that referenced this pull request Sep 12, 2026
`Field(InnerProductSpace)` is the scalar field as a one-dimensional space
over itself, with `inner(a, b) = conj(a) * b` and Euclidean geometry. It is
pointedly not a `CoordinateSpace`: the surface a scalar cannot honour --
`size`, `flatten`, `stacked` -- lives there, so the hierarchy already cuts
in the right place. `RealField` and `ComplexField` are the concrete forms,
public alongside `Field`.

`ScalarShapeCheck` carries `minimum_level = "standard"` to match the
`out_scalar` branch it will replace; at `cheap` it would tighten validation
rather than preserve it. Under batched membership it permits leading axes,
because ordinary space membership cannot require exactly one.

`Space.scalars` returns the coefficient field on the same backend without
touching the owning space's storage: a Hermitian space over `complex128`
yields a `RealField` over `float64` and stays `complex128` itself.
`Space.__init__` now rejects integer and Boolean storage, and a complex
coefficient field over real storage -- the latter was accepted silently,
since `_check_scalar` only rejects non-real multipliers when the field is
real.

The ADR-010 guard lands here rather than later because this commit opens
the hole: `Field` is a `VectorSpace`, and `LinOp` endpoints have been bound
to `VectorSpace` since #67, so `LinOp[X, Field]` would otherwise satisfy
the bound and reinstate the class collapse ADR-010 rejected. No type bound
expresses "a vector space that is not the field", so `LinOp.__init__`
rejects a `Field` codomain explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Pavlo3P Pavlo3P mentioned this pull request Sep 12, 2026
4 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant