📝 docs(spec): specify the embedding and metric-representation verbs - #735
Conversation
There was a problem hiding this comment.
Pull request overview
Adds missing specification entries to the authoritative docs/spec.md for four public manifold/embedding verbs (metric_representation, pt_embed, pt_project, embedded_twosphere), documenting their intent, dispatch shapes, and doctested examples in the sections where the underlying machinery is described.
Changes:
- Specify
metric_representationas a structural query returning the metric-matrix container class (not an evaluated matrix). - Specify embedding/projection behavior via
pt_embed/pt_project, including examples for spherical vs Cartesian ambient charts. - Specify the
embedded_twosphereconvenience constructor and its relationship toEmbeddedManifold.
Suppressed comments (3)
docs/spec.md:4537
- The listed
coordinax.vectors.Pointoverload is too broad and drops theusyskwarg. In code the Point overload is specifically(Point, HyperSphericalManifold, *, usys=None)(and it returns aPoint, not aCDict). The signature list should reflect the actual dispatch.
cxm.pt_project(p, chart, M, /, *, usys=None) # HyperSphericalManifold
cxm.pt_project(p, M, /) # coordinax.vectors.Point
docs/spec.md:4542
pt_projectdoes not always return aCDict: when called with acoordinax.vectors.Point, the Point overload returns aPointwith its data/chart updated. The Return section should document this so callers know what to expect.
**Return:**
- A `CDict` in the intrinsic chart's components.
docs/spec.md:4592
- The
embedded_twospheresignature showsambient=cxc.spherical3d, but there is nocoordinax.charts.spherical3dsymbol in the codebase; the default ambient chart iscxc.sph3d(aSpherical3Dinstance). This signature should match the actual default.
cxm.embedded_twosphere(radius, ambient=cxc.spherical3d)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #735 +/- ##
=======================================
Coverage 96.59% 96.59%
=======================================
Files 266 266
Lines 8934 8934
=======================================
Hits 8630 8630
Misses 304 304 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
3426d40 to
4827a6c
Compare
Four verbs that were public and unspecified: `metric_representation`, `pt_embed`, `pt_project` and `embedded_twosphere`. Placed with the machinery they belong to rather than in the verb cluster -- `metric_representation` after `metric_matrix`, whose sibling it is, and the embedding trio after `EmbeddedManifold`. `metric_representation` is the one most worth writing down, because its signature invites the wrong reading. It returns a **class**, `DiagonalMetric` or `DenseMetric`, not a populated matrix, and takes no base point: sparsity structure belongs to the (manifold, chart) pair, not to where you stand on it. That is what lets `metric_matrix` keep the O(n) diagonal path instead of contracting a full einsum, and why an `EmbeddedManifold` declares dense -- a pullback through an arbitrary embedding has no reason to be diagonal even when the intrinsic chart is orthogonal. `pt_project` is a projection and not merely an inverse: it is defined on all of the ambient and round-trips exactly only for points already on the surface, which is why it carries overloads `pt_embed` does not. One inconsistency found and recorded rather than smoothed over: the container of a projected angle tracks the ambient chart. Round-tripping through `Spherical3D` gives `Angle(1.04719755, 'rad')`; through `Cart3D`, the same value and unit in a plain `Q(1.04719755, 'rad')`. Written down as observed behaviour with a note that an angle is an element of S^1 and the container is what carries that, so the ambient chart arguably should not decide it. Two things this cost, both worth naming: - The `pt_embed` example first used the Cartesian ambient at the equator, which routes through `cos(pi/2)` and puts `1.2246468e-16` in the output. Moved to the north pole, where the same route is exact. - A `!!! warning` with a nested ```pycon block made sybil raise `ValueError: <Region ...> overlaps <Region ...>` at collection -- the outer indented block and the inner fence both parse as doctests. Flattened to prose plus a plain example. Explanatory prose sits outside the `pycon` fences, per the review on GalacticDynamics#733. All 24 examples verified under `pytest docs/spec.md`, which is the harness that sets `JAX_ENABLE_X64`; 0 failures inside the four added sections. The 17 pre-existing failures elsewhere in the file are GalacticDynamics#733's subject. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Audit prompted by the review on GalacticDynamics#734, whose middle finding was a signature that omitted a keyword the implementation accepts. Checking these four the same way turned up the same class of error and one wrong rule. `pt_project` has seven overloads and the spec listed five. The two missing are the explicit-embedding-map form and the `(from_chart, EmbeddedChart)` form, and the `Point` overload takes `usys` like every other -- it was written as `pt_project(p, M, /)`, promising a narrower API than exists. The diagonal rule was stated as a property of orthogonality and is not. It is declared per (manifold, chart) pair, and the counter-example is on the sphere already: S2 / sph2 DiagonalMetric S2 / lonlat_sph2 DiagonalMetric S2 / loncoslat_sph2 DenseMetric Reworded to say the chart decides, with `loncoslat_sph2` shown as a worked example rather than left as a trap for whoever assumed the manifold decides. Also pinned what the `(AbstractManifold, AbstractChart)` fallback actually returns -- `DenseMetric`, verified -- and why that is the right default: dense is only slower, diagonal would be wrong. 331 passed; `prek run --all-files` clean; docs build succeeds.
4827a6c to
1e78f11
Compare
Review caught the prose claiming "the same call works whether you hold an
`EmbeddedManifold`, an `EmbeddedChart`, or a bare `AbstractEmbeddingMap`". It
does not. Only the first two have a two-argument form:
pt_embed(p, EmbeddedManifold) -> ok
pt_embed(p, EmbeddedChart) -> ok
pt_embed(p, AbstractEmbeddingMap) -> NotFoundLookupError
Same for `pt_project`. The signature blocks were right -- they list the bare map
only in its four-argument form -- so it was the summary above them that
overreached.
Checking why turned up something worth recording rather than paraphrasing: the
embedding map is not missing the information a two-argument form would need.
`TwoSphereIn3D` carries `intrinsic` and `ambient`, both concrete charts. So the
requirement to pass `from_chart` and `to_chart` is an asymmetry in the API, not
a consequence of the map being underspecified. Written that way, with the
raising call named in the dispatch bullet so the error is findable by the
message a reader will actually hit.
Also aligned the trailing comments in the `pt_embed` signature block; the fourth
line had none while the other three did.
365 passed; `prek run --all-files` clean; docs build succeeds.
`coord_dimensions` says which of a chart's components are angular. Nothing made
the stored container agree, so it tracked whichever arithmetic produced the
value instead: `Angle` survives a copy and degrades to `Quantity` through
`Angle + Quantity` or any trigonometric call. The same coordinate in the same
chart therefore came back differently depending on the route taken to reach it:
pt_map(cart3d -> sph3d) {'r': Q, 'theta': Q, 'phi': Q}
pt_map(sph2 -> lonlat) {'lon': Angle, 'lat': Q} <- within one call
pt_project via Spherical3D Angle via Cart3D Quantity
That last pair is the inconsistency GalacticDynamics#735 recorded as "observed behaviour rather
than endorsed". It is fixed here, and that spec entry now states the rule.
BREAKING CHANGE: this alters pytree *structure*, not just a repr. `Angle` and
`Quantity` are distinct pytree nodes --
PyTreeDef({'theta': CustomNode(Angle[...], [*])})
PyTreeDef({'theta': CustomNode(Quantity[...], [*])})
-- so code that builds a point with `u.Q` in an angular slot and combines it
structurally with a library-returned point must now build `u.Angle`. Two such
cases already existed in this repo: a `jax.tree.map` over a hand-built and a
returned point, and a `lax.cond` whose pass-through branch returned the
caller's container while the converting branch returned `Angle`. `lax.cond`
rejects branches of differing structure, so that one was a hard failure.
The argument for accepting the break is that the structure is *currently*
route-dependent, which is the worse version of the same problem: `jit` cache
misses and `tree_map` failures decided by how a point was obtained rather than
by what it is. Canonicalising makes structure a property of the chart.
Rules the implementation had to learn, each from a failure rather than from
first principles:
- Promote only what `Angle` accepts. A sphere built with a bare `radius=1` puts
dimensionless values in angular slots, and `Angle` rejects them.
Canonicalising a container must never make a value invalid.
- Return the input object when nothing changed, so `pt_map(p, chart, chart) is
p` survives. `tests/usage` asserts it.
- Exit early when the chart has no angular component at all. `pt_map` is
already dispatch-bound (GalacticDynamics#719); every Cartesian transition now costs one tuple
scan.
Identity is kept for canonical input and deliberately not for non-canonical
input: the alternative is the no-op route preserving a container that every
other route would have normalised, which is the bug.
Applied at the 40 CDict-returning `pt_map`/`pt_embed`/`pt_project` rules and at
`cdict`, rather than at the public entry alone -- the explicit-manifold form
dispatches straight to the specific pair and would otherwise bypass it. The new
sweep covers both forms across every chart pair and fails 88 cases without the
change.
11067 passed; `prek run --all-files` clean; docs build succeeds.
`coord_dimensions` says which of a chart's components are angular. Nothing made
the stored container agree, so it tracked whichever arithmetic produced the
value instead: `Angle` survives a copy and degrades to `Quantity` through
`Angle + Quantity` or any trigonometric call. The same coordinate in the same
chart therefore came back differently depending on the route taken to reach it:
pt_map(cart3d -> sph3d) {'r': Q, 'theta': Q, 'phi': Q}
pt_map(sph2 -> lonlat) {'lon': Angle, 'lat': Q} <- within one call
pt_project via Spherical3D Angle via Cart3D Quantity
That last pair is the inconsistency GalacticDynamics#735 recorded as "observed behaviour rather
than endorsed". It is fixed here, and that spec entry now states the rule.
BREAKING CHANGE: this alters pytree *structure*, not just a repr. `Angle` and
`Quantity` are distinct pytree nodes --
PyTreeDef({'theta': CustomNode(Angle[...], [*])})
PyTreeDef({'theta': CustomNode(Quantity[...], [*])})
-- so code that builds a point with `u.Q` in an angular slot and combines it
structurally with a library-returned point must now build `u.Angle`. Two such
cases already existed in this repo: a `jax.tree.map` over a hand-built and a
returned point, and a `lax.cond` whose pass-through branch returned the
caller's container while the converting branch returned `Angle`. `lax.cond`
rejects branches of differing structure, so that one was a hard failure.
The argument for accepting the break is that the structure is *currently*
route-dependent, which is the worse version of the same problem: `jit` cache
misses and `tree_map` failures decided by how a point was obtained rather than
by what it is. Canonicalising makes structure a property of the chart.
Rules the implementation had to learn, each from a failure rather than from
first principles:
- Promote only what `Angle` accepts. A sphere built with a bare `radius=1` puts
dimensionless values in angular slots, and `Angle` rejects them.
Canonicalising a container must never make a value invalid.
- Return the input object when nothing changed, so `pt_map(p, chart, chart) is
p` survives. `tests/usage` asserts it.
- Exit early when the chart has no angular component at all. `pt_map` is
already dispatch-bound (GalacticDynamics#719); every Cartesian transition now costs one tuple
scan.
Identity is kept for canonical input and deliberately not for non-canonical
input: the alternative is the no-op route preserving a container that every
other route would have normalised, which is the bug.
Applied at the 40 CDict-returning `pt_map`/`pt_embed`/`pt_project` rules and at
`cdict`, rather than at the public entry alone -- the explicit-manifold form
dispatches straight to the specific pair and would otherwise bypass it. The new
sweep covers both forms across every chart pair and fails 88 cases without the
change.
11067 passed; `prek run --all-files` clean; docs build succeeds.
…rs (#752) `coord_dimensions` says which of a chart's components are angular; the stored container now agrees. Previously it tracked whichever arithmetic produced the value -- `Angle` degrades to `Quantity` through `Angle + Quantity` or any trigonometric call -- so the same coordinate in the same chart came back differently depending on the route taken to reach it. BREAKING CHANGE: `Angle` and `Quantity` are distinct pytree nodes, so this alters pytree *structure*. Code that builds a point with `u.Q` in an angular slot and combines it structurally with a library-returned point -- `jax.tree.map`, `lax.cond` branches, `jit` cache keys -- must now build `u.Angle`. Values, units and ordinary arithmetic are unaffected. Behaviour: - Only values `Angle` accepts are promoted. A dimensionless value in an angular slot is left alone rather than made invalid. - A point already canonical is returned as-is, so `pt_map(p, chart, chart) is p` holds. A non-canonical one is canonicalised, and so is a new dict. - Charts with no angular component exit on a tuple scan. Applied at the 40 CDict-returning `pt_map`/`pt_embed`/`pt_project` rules and at `cdict`, not at the public entry alone: the explicit-manifold form dispatches straight to the specific pair and bypasses it. Promotion goes through `AbstractQuantity._mk`, whose precondition holds here -- value and unit come off an `AbstractQuantity`, and the angular dimension is established immediately above. Two components cost 8.7us against 1418us for the checked constructor; `cart3d -> sph3d` is 2.67 ms against `main`'s 3.43 ms. This settles the `pt_project` container anomaly #735 recorded as "observed behaviour rather than endorsed"; that spec entry now states the rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four public verbs that had no spec entry:
metric_representation,pt_embed,pt_project,embedded_twosphere.Placed with the machinery they belong to rather than in the verb cluster —
metric_representationright aftermetric_matrix, whose sibling it is, and the embedding trio afterEmbeddedManifold. That also keeps this diff clear of #732/#733/#734, which all insert near the verbs.metric_representationThe one most worth writing down, because the name invites the wrong reading. It returns a class, not a populated matrix, and takes no base point:
Sparsity structure belongs to the (manifold, chart) pair, not to where you stand on it. That is what lets$O(n)$ diagonal path (#686) instead of contracting a full einsum — and why an
metric_matrixkeep theEmbeddedManifolddeclares dense: a pullback through an arbitrary embedding has no reason to be diagonal even when the intrinsic chart is orthogonal.pt_embed/pt_projectRecorded that
pt_projectis a genuine projection, not merely an inverse — defined on all of the ambient, round-tripping exactly only for points already on the surface. That is why it carries overloadspt_embeddoes not, including aHyperSphericalManifoldpair and aPointone: projecting onto a sphere is meaningful without anEmbeddedManifoldto hand, while embedding from one is not.One inconsistency found — recorded, not smoothed over
The container of a projected angle tracks the ambient chart:
pt_project(...)["theta"]Spherical3DAngle(1.04719755, 'rad')Cart3DQ(1.04719755, 'rad')Same value, same unit, different container. Written down as observed behaviour with a note that an angle is an element of$S^1$ and the container is what carries that, so the ambient chart arguably should not decide it. If that's a bug rather than a quirk, this is the line to change.
Two things this cost, both worth naming
pt_embedCartesian example first used the equator, which routes throughcos(pi/2)and puts1.2246468e-16in the output. Moved to the north pole, where the same route is exact. Same trap as in ✅ test(spec): putdocs/spec.mdexamples under CI #733 — the Cartesian ambient is a reliable source of float noise.!!! warningadmonition with a nested```pyconblock made sybil fail at collection, not as a test failure:ValueError: <Region ...> overlaps <Region ...>. The outer indented block and the inner fence both parse as doctests. Flattened to prose plus a plain example. Worth knowing before anyone else nests one.Verification
All 24 examples verified under
pytest docs/spec.md— the harness that setsJAX_ENABLE_X64, so the literals are the ones CI will compare against. 0 failures inside the four added sections, checked by mapping every reported failure line against the section ranges rather than eyeballing.prek run --all-filesclean;uv run --frozen nox -s docssucceeds with warnings as errors.The 17 pre-existing failures elsewhere in
docs/spec.mdare untouched — they are #733's subject.🤖 Generated with Claude Code