Skip to content

📝 docs(spec): specify the embedding and metric-representation verbs - #735

Merged
nstarman merged 3 commits into
GalacticDynamics:mainfrom
nstarman:claude/spec-plumbing-verbs
Aug 19, 2026
Merged

📝 docs(spec): specify the embedding and metric-representation verbs#735
nstarman merged 3 commits into
GalacticDynamics:mainfrom
nstarman:claude/spec-plumbing-verbs

Conversation

@nstarman

Copy link
Copy Markdown
Contributor

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_representation right after metric_matrix, whose sibling it is, and the embedding trio after EmbeddedManifold. That also keeps this diff clear of #732/#733/#734, which all insert near the verbs.

metric_representation

The 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:

>>> cxm.metric_representation(cxm.R3, cxc.cart3d).__name__
'DiagonalMetric'
>>> cxm.metric_representation(cxm.embedded_twosphere(radius=u.Q(2.0, "km")), cxc.sph2).__name__
'DenseMetric'

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 (#686) 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_embed / pt_project

Recorded that pt_project is 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 overloads pt_embed does not, including a HyperSphericalManifold pair and a Point one: projecting onto a sphere is meaningful without an EmbeddedManifold to hand, while embedding from one is not.

One inconsistency found — recorded, not smoothed over

The container of a projected angle tracks the ambient chart:

ambient pt_project(...)["theta"]
Spherical3D Angle(1.04719755, 'rad')
Cart3D Q(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

  • The pt_embed Cartesian example first used 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. Same trap as in ✅ test(spec): put docs/spec.md examples under CI #733 — the Cartesian ambient is a reliable source of float noise.
  • A !!! warning admonition with a nested ```pycon block 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 sets JAX_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-files clean; uv run --frozen nox -s docs succeeds with warnings as errors.

The 17 pre-existing failures elsewhere in docs/spec.md are untouched — they are #733's subject.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings August 18, 2026 20:54
@github-actions github-actions Bot added the 📝 Add / update documentation Add or update documentation. label Aug 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_representation as 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_twosphere convenience constructor and its relationship to EmbeddedManifold.
Suppressed comments (3)

docs/spec.md:4537

  • The listed coordinax.vectors.Point overload is too broad and drops the usys kwarg. In code the Point overload is specifically (Point, HyperSphericalManifold, *, usys=None) (and it returns a Point, not a CDict). 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_project does not always return a CDict: when called with a coordinax.vectors.Point, the Point overload returns a Point with 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_twosphere signature shows ambient=cxc.spherical3d, but there is no coordinax.charts.spherical3d symbol in the codebase; the default ambient chart is cxc.sph3d (a Spherical3D instance). 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.

Comment thread docs/spec.md Outdated
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.59%. Comparing base (4020513) to head (a467ed6).
⚠️ Report is 2 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@nstarman
nstarman force-pushed the claude/spec-plumbing-verbs branch 2 times, most recently from 3426d40 to 4827a6c Compare August 18, 2026 23:00
nstarman and others added 2 commits August 18, 2026 19:22
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.
@nstarman
nstarman force-pushed the claude/spec-plumbing-verbs branch from 4827a6c to 1e78f11 Compare August 18, 2026 23:22
@nstarman nstarman added this to the v0.24.0 milestone Aug 19, 2026
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.
@nstarman
nstarman merged commit c6f6e63 into GalacticDynamics:main Aug 19, 2026
16 of 17 checks passed
@nstarman
nstarman deleted the claude/spec-plumbing-verbs branch August 19, 2026 03:32
nstarman added a commit to nstarman/coordinax that referenced this pull request Aug 19, 2026
`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.
nstarman added a commit to nstarman/coordinax that referenced this pull request Aug 19, 2026
`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.
nstarman added a commit that referenced this pull request Aug 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📝 Add / update documentation Add or update documentation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants