Conversation
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
Binds
LinOp's abstract endpoints toVectorSpaceinstead ofCoordinateSpace, and makes every operation that genuinely needs coordinates, batching, or an inner product require that capability explicitly.Closes #66.
Checklist
pytest tests/ -x -q) — 3718 passed, 170 skipped; ruff clean[Unreleased]Mathematical invariant
A linear operator needs only the vector-space structure of its endpoints:
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
which needs an inner product on both endpoints — not coordinates.
CoordinateSpaceandInnerProductSpaceare siblings underVectorSpace, so the old bound never implied that requirement either; after this PR it is checked rather than assumed, andrapplyraisesCapabilityErroron an endpoint without geometry.This is also why the new bound is
VectorSpaceand notInnerProductSpace: the latter would have narrowed the accepted set and dropped bareTreeSpaceandStackedSpace, 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 failuremetric_rapplydocuments as undetectable on a Euclidean geometry. It now builds its probe element from the space's ownoneswhere one exists, andtest_strict_probe_runs_on_noncoordinate_space_and_catches_a_wrong_adjointfeeds 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.
6b354dd— extract space capabilities into one module (ADR-005), addCapabilityError,require,probe_element,registry_key6fba97c— gate materialization, batching and geometry; repair the adjoint probe; drop theshapepre-filter from endpoint compatibilitydafbd0c— bind both endpoints toVectorSpace(three lines)daa508d— a test space with an inner product and no coordinate surface01ed363— exportCapabilityErrorfbf68be— changelogNot in this PR
Functional, any sharedMappingsupertype, scalar-field objects, and the twenty coordinate-dependent sites inspacecore/linalg/. Solver defaults derived from dimension still assumesizeand 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