Field objects for scalars - #69
Merged
Merged
Conversation
`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>
`Functional` binds two endpoints, defaulting the codomain to `dom.scalars` so no existing constructor call changes. The codomain is independent of the domain's coefficient field: a functional on a real space may be complex-valued, and conversion preserves that rather than promoting the domain. With a codomain object, the 17 `out_scalar=True` decorators become `out_space="codomain"` and the parallel branch leaves `checked_method`. Scalar output is now validated by ordinary space membership through the field's `ScalarShapeCheck`. The 4 `out_batched_scalar=True` sites stay, deliberately. That flag asserts `shape == (N,)`; the codomain route goes through `_run_checks(..., allow_leading=True)` and accepts any leading dimensions, which is strictly weaker. A `Field` cannot say "exactly one leading axis" without `stacked`, which it must not have, and ADR-006 models the batch axis as a leading array axis rather than as a space. Output checks follow the functional's policy, not the field's, which `out_scalar` did implicitly by reading the functional's check level. That needs a copy of the codomain at the functional's level, and building it as `type(cod)(ctx, check_level=...)` breaks any `Field` subclass whose constructor takes more -- silently, by binding the context to the wrong parameter. `ContextBound._with_check_level` routes through the subclass's own `_convert` instead, which is the extension point that already has to know how to rebuild it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR-010 keeps its decision and gains what now enforces it: the codomain is a `Field`, the type bound no longer separates a functional from a linear operator into a one-dimensional codomain, and an explicit construction-time check does. It also records why exact batched scalar validation keeps its own flag. ADR-015 gains the object-valued accessor beside the two string ones. The changelog marks the storage-dtype rejection breaking: it lives on `Space.__init__`, so it binds every space rather than only the field-backed numerical ones, and a downstream space over an integer or Boolean dtype now raises at construction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`scripts/docstring_audit.py --check` is a CI gate and it failed on eight numpydoc issues the change introduced: the `cod` parameter on the four functional subclasses that now accept it, `ctx` and `check_level` on the three field classes, and a summary line sharing the opening quotes on `Field`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 of 4 tasks
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
Gives the scalar field an object:
Functionalgains aFieldcodomain instead of an implicit string, andout_space="codomain"replaces the parallelout_scalarvalidation branch.Closes #68. Follows #67, which bound
LinOpendpoints toVectorSpace.Checklist
pytest tests/ -x -q) — 3766 passed, 170 skipped; ruff clean[Unreleased]Mathematical invariant
The field is the one-dimensional vector space over itself, with
so it is an inner-product space and a functional is a map into it. It is not a coordinate space: coordinates are a choice of basis for a representation, and a scalar has no such freedom.
This does not reopen ADR-010. Naming the codomain is not the same as making a functional a linear operator into it — the objection was to the class collapse, which erases the Riesz gradient, not to the field having a name. That separation used to be enforced by
LinOp'sCoordinateSpacebound; since #67 widened it toVectorSpace, and aFieldis aVectorSpace, the bound no longer separates them.LinOp.__init__now rejects aFieldcodomain explicitly, in the same commit that introducesField.Entries and coefficients stay distinct, as ADR-015 decided.
Space.scalarsbinds the coefficient field on the same backend without touching storage: a Hermitian space overcomplex128yields aRealFieldoverfloat64and remainscomplex128.What deliberately did not change
The 4
out_batched_scalar=Truesites keep their flag. It assertsshape == (N,); the codomain route goes through_run_checks(..., allow_leading=True)and accepts any leading dimensions, which is strictly weaker. AFieldcannot express "exactly one leading axis" withoutstacked, which it must not have.test_exact_batch_check_is_stronger_than_scalar_field_batch_membershippins the difference, and ADR-010 records the reason.Breaking
Space.__init__now rejects integer and Boolean storage, and a complex coefficient field over real storage. The guard binds every space, not only the field-backed numerical ones. Marked breaking in the changelog.Commits
Each was tested before it was made.
b85608e—Field,RealField,ComplexField,Space.scalars, the storage guards, and the ADR-010 guardaec8454—Functional's codomain, the 17out_scalarconversions, andContextBound._with_check_level73e9072— ADR-010, ADR-015, changelogNot in this PR
Mappingand the shared lazy algebra (0.4.5), andspacecore/linalg/. Release-gate items not run here: JIT audit, docs build, backends beyond the default environment.🤖 Generated with Claude Code