Skip to content

✨ feat(manifolds): add chord_distance, the straight line through the ambient - #720

Merged
nstarman merged 2 commits into
GalacticDynamics:mainfrom
nstarman:claude/chord-distance
Aug 18, 2026
Merged

✨ feat(manifolds): add chord_distance, the straight line through the ambient#720
nstarman merged 2 commits into
GalacticDynamics:mainfrom
nstarman:claude/chord-distance

Conversation

@nstarman

@nstarman nstarman commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

The follow-up promised in #715, which is now merged; this stands alone against main.

geodesic_distance measures along the manifold. The other honest answer is the straight line through the space it is embedded in — the tunnel rather than the surface path. On a sphere of radius $R$ separated by a central angle $\theta$:

$$d_{\mathrm{geodesic}} = R,\theta, \qquad d_{\mathrm{chord}} = 2R\sin(\theta/2)$$

They agree to first order and diverge as the points separate: 0.927 against 0.964 at $\theta = 1$ on the unit sphere, and $2R$ against $\pi R$ at antipodes. Which one is wanted depends on the question — a great-circle flight path is the geodesic, a line of sight through the body is the chord — so it is a separate verb rather than a mode of the first.

Implementation

Embed both points, then take the ambient manifold's geodesic_distance, which for flat ambient space is the straight line. So there is no second distance formula to keep in step, and it works for any embedding rather than only spheres.

Refusals are deliberate:

  • Euclidean — it is its own ambient, so its chord is its geodesic. Returning the same number under a second name invites a reader to think two things were measured; the error points at geodesic_distance.
  • No embedding — no ambient space to cut through.

Verification

theta        chord     2 sin(t/2)   geodesic       err
0.0010    0.00100000   0.00100000   0.001000   0.00e+00
0.1000    0.09995834   0.09995834   0.100000   0.00e+00
1.0000    0.95885108   0.95885108   1.000000   1.11e-16
3.1416    2.00000000   2.00000000   3.141593   0.00e+00

symmetry / chart-invariance:  d = swap = lonlat = 0.927111716274
embedded sphere R = 2 m, antipodes:  Distance(4., 'm')   # one diameter

Seven tests pin the analytic form across five separations, symmetry and chart-invariance, that it differs from the geodesic (√2 vs π/2 at a quarter turn — the point of two verbs), the embedded sphere carrying its radius, and flat space refusing.

  • Full suite: 10430 passed, 10 skipped, 1 xfailed
  • prek run --all-files clean, including ty

A fast path where it does pay

geodesic_distance gains one, in the same diff. Its packed overloads unpack their operands into component dicts so a curvilinear chart can be mapped to Cartesian first. In a Cartesian chart there is nothing to map, and plum can skip the unpacking entirely by dispatching on the chart type — 33x for arrays, 3.3x for quantities.

Registered as two signatures rather than one with a Quantity | Array union: each generic overload it overrides is typed for one of them, and a union would be narrower in the chart but wider in the operands, so plum would rank neither more specific and refuse the call as ambiguous.

No matching CDict fast path. The two pt_map calls it would skip are 4.8 µs of a ~3000 µs call — the time is in norm and in wrapping the result as a Distance — so it measured as no gain at all, and would have been dispatch surface bought with nothing.

No fast path for the chord, deliberately

Profiling an eager call:

chord_distance(sph2) total          11624.2 us
  pt_map(p, sph2, sph2)                 4.8 us
  pt_embed(p, unit_sphere)             41.2 us
  geodesic on ambient (2 pts)       11569.7 us   <- 99.5%

The embedding steps a chord-specific shortcut could skip are 0.5% of the call. Hand-rolling the sphere's embedding formula to dodge one pt_map would duplicate TwoSphereIn3D for a fraction of a percent, so the module says so instead.

That 99.5% is one chart transition, which is #719: eager pt_map costs ~5.9 ms against ~21 µs jitted, with 178 dispatch resolutions per call. Fixing it there fixes it here.

Scope

Manifold-level only. geodesic_distance has a Point overload and this does not, so there is an ergonomic asymmetry — whether cx.chord_distance(p, q) should exist at the vector level is a separate decision, happy to add it if you want it.

🤖 Generated with Claude Code

Doc and test-name corrections that came out of review here were split into #724, which is now merged. This branch has been rebased onto it and the duplicate commit dropped, so the diff is chord_distance and the fast path only — tests/unit/vectors/test_point.py and test_jit_parameterized_paths.py are no longer touched here at all.

Copilot AI lite review requested due to automatic review settings August 14, 2026 13:08
@github-actions github-actions Bot added 📝 Add / update documentation Add or update documentation. ✅ Add / update / pass tests Add, update, or pass tests. ✨ Introduce new features Introduce new features. 🐛 Fix a bug Fix a bug. 💥 Introduce breaking changes Introduce breaking changes. labels Aug 14, 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

This PR adds a new manifold-level distance verb, chord_distance, for measuring straight-line (ambient) distance through an embedding, and completes the separationgeodesic_distance API transition across code, tests, and docs/spec.

Changes:

  • Introduces chord_distance manifold dispatches (with deliberate refusals for Euclidean and non-embedded manifolds) and exports it in the public API.
  • Replaces the old separation implementation with geodesic_distance (manifold-geometry based), and updates vector Point overload wiring and tests accordingly.
  • Updates documentation/tutorials/spec to reflect geodesic_distance semantics and Minkowski refusals (with a few doc mismatches noted in review comments).

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/unit/vectors/test_point.py Updates Point-level distance tests to call cx.geodesic_distance.
tests/unit/manifolds/test_separation_dispatch.py Removes obsolete separation dispatch tests.
tests/unit/manifolds/test_metric_arg_is_honoured.py Updates metric-arg honoring test to use geodesic_distance.
tests/unit/manifolds/test_interval.py Updates Minkowski interval tests/docs to reference geodesic_distance.
tests/unit/manifolds/test_geodesic_distance_dispatch.py Adds dispatch coverage for geodesic_distance and chord_distance.
tests/unit/charts/test_jit_parameterized_paths.py Updates JIT regression test to use geodesic_distance.
src/coordinax/vectors/_src/register_geodesic_distance.py Implements Point overload for geodesic_distance and delegates to manifold API.
src/coordinax/vectors/_src/init.py Switches registration import from register_separation to register_geodesic_distance.
src/coordinax/vectors/init.py Re-exports geodesic_distance instead of separation.
src/coordinax/manifolds/lorentzian.py Updates lorentzian namespace docstring wording around Minkowski/interval.
src/coordinax/manifolds/init.py Re-exports geodesic_distance and new chord_distance.
src/coordinax/_src/minkowski/causality.py Updates guidance text to reference geodesic_distance instead of separation.
src/coordinax/_src/manifolds/separation.py Removes legacy separation implementation.
src/coordinax/_src/manifolds/quadratic_form.py Updates references from separation to geodesic_distance.
src/coordinax/_src/manifolds/interval.py Clarifies interval semantics vs geodesic_distance and updates wording.
src/coordinax/_src/manifolds/geodesic_distance.py Adds manifold-based geodesic_distance dispatch implementations and fast paths.
src/coordinax/_src/manifolds/chord_distance.py Adds new chord_distance dispatch implementations via embedding + ambient distance.
src/coordinax/_src/manifolds/init.py Registers new manifold dispatch modules (geodesic_distance, chord_distance).
src/coordinax/init.py Exports geodesic_distance and chord_distance at top-level coordinax.
packages/coordinaxs.api/src/coordinaxs/api/manifolds.py Renames abstract API to geodesic_distance and adds abstract chord_distance.
docs/tutorials/special_relativity.md Updates tutorial to use geodesic_distance refusal + interval guidance.
docs/spec.md Updates authoritative spec section for geodesic_distance.
docs/guides/charts.md Updates Minkowski chart guidance to reference geodesic_distance.
docs/api/manifolds.md Updates API docs list for geodesic_distance/interval (needs chord_distance mention).
Suppressed comments (3)

packages/coordinaxs.api/src/coordinaxs/api/manifolds.py:87

  • The abstract geodesic_distance docstring still describes the old separation semantics (norm of coordinate difference in the given chart). This is now incorrect: geodesic_distance is computed from manifold geometry (symmetric, chart-invariant), and Minkowski/unknown manifolds can refuse rather than approximate. Updating this docstring will prevent API users from relying on the removed behavior.
    """Distance between two points on a manifold.

    The straight-line distance is the manifold `norm` of the two points'
    coordinate difference, evaluated in the chart they are given in (exact for a
    flat manifold).  Dispatches on the inputs: two `~coordinax.vectors.Point`

docs/spec.md:3002

  • chord_distance is introduced as a new public manifold measurement, but docs/spec.md (the authoritative spec) has no corresponding spec entry. Given the repo’s spec-first contract, this new API should be specified here alongside geodesic_distance (including its refusals for Euclidean/no-embedding and the fact it currently has no Point overload).
    ```

(software-spec-abstractatlas)=

tests/unit/manifolds/test_geodesic_distance_dispatch.py:72

  • The test class name TestIndefiniteMetricSeparation still refers to separation, but the behavior under test is geodesic_distance refusing indefinite metrics. Renaming reduces ambiguity (especially now that separation no longer exists).
class TestIndefiniteMetricSeparation:
    """`geodesic_distance` inherits `norm`'s guard instead of returning ``nan``.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/coordinaxs.api/src/coordinaxs/api/manifolds.py Outdated
Comment thread src/coordinax/_src/manifolds/interval.py Outdated
Comment thread tests/unit/manifolds/test_geodesic_distance_dispatch.py Outdated
Comment thread tests/unit/vectors/test_point.py Outdated
Comment thread tests/unit/charts/test_jit_parameterized_paths.py Outdated
Comment thread src/coordinax/manifolds/lorentzian.py Outdated
Comment thread docs/api/manifolds.md Outdated
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.55%. Comparing base (bc52af2) to head (64674a4).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #720      +/-   ##
==========================================
- Coverage   96.55%   96.55%   -0.01%     
==========================================
  Files         265      266       +1     
  Lines        8793     8849      +56     
==========================================
+ Hits         8490     8544      +54     
- Misses        303      305       +2     

☔ 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.

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

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

docs/api/manifolds.md:52

  • chord_distance is introduced as a new public API (exported from coordinax.manifolds / coordinax). Per the repo’s spec-first workflow, this should also be documented in the authoritative docs/spec.md (and/or the relevant spec section for manifolds/embeddings), otherwise the implementation and user-facing docs can drift from the source of truth.
- `chord_distance`: length of the straight line between two points _through the ambient space_ they are embedded in -- the tunnel rather than the surface path. A different measurement from `geodesic_distance`, not an approximation to it: on a sphere of radius $R$ separated by a central angle $\theta$, the geodesic is $R\theta$ and the chord is $2R\sin(\theta/2)$. Needs an embedding, so a manifold that is its own ambient (anything Euclidean) raises `NotImplementedError` and points at `geodesic_distance`

Comment thread src/coordinax/_src/manifolds/chord_distance.py

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

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

nstarman and others added 2 commits August 18, 2026 14:56
…e ambient

`geodesic_distance` measures along the manifold. The other honest answer is
the straight line *through* the space it is embedded in -- the tunnel rather
than the surface path. On a sphere of radius R separated by a central angle t:

    geodesic = R t,        chord = 2 R sin(t / 2)

They agree to first order and diverge as the points separate: 0.927 against
0.964 at t = 1 on the unit sphere, and 2R against pi R at antipodes. Which one
is wanted depends on the question -- a great-circle flight path is the
geodesic, a line of sight through the body is the chord -- so it is a separate
verb rather than a mode of the first.

Implemented by embedding both points and taking the ambient manifold's
`geodesic_distance`, which for flat ambient space *is* the straight line. There
is no second distance formula to keep in step, and it works for any embedding,
not only spheres.

A Euclidean manifold is refused: it is its own ambient, so its chord is its
geodesic, and returning the same number under a second name invites the reader
to think two things were measured. A manifold with no embedding is refused too.

Matches `2 R sin(t / 2)` to 1.1e-16, symmetric and chart-invariant to 1e-16.

No fast path, deliberately: ~99.5% of an eager call is the ambient
`geodesic_distance`, and ~0.5% the embedding steps a shortcut could skip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The oldest-dependencies job failed all seven `TestChordDistance` cases with

    TypeError: Unexpected input type for array: Quantity

`chord_distance` returns a dimensionless `Quantity`, and the tests fed it to
raw `jax.numpy.asarray`, which on jax 0.7.2 raises.

Rebasing onto GalacticDynamics#725 narrows why that spelling was wrong. unxt 2.0.2 is the
floor now, and its `Quantity.__array__` raises `UnitConversionError` for
anything dimensionful rather than returning a bare array -- so `jnp.asarray`
is no longer the silent stripper the first version of this commit described,
and neither is `float`. The conclusion survives the correction: on the oldest
supported jax it still raises for *every* `Quantity`, dimensionless included,
which is the failure the `check_oldest` job reported. Same reasoning GalacticDynamics#716
records for the strips it touched.

`.ustrip("")` at the five call sites, naming the unit expected rather than
relying on a conversion that reads as incidental. One `jnp.asarray` wrapped
around an already-stripped `ustrip("rad")` goes too -- it was the last of the
pattern left in the class, and it converted nothing.

The doctests keep `float(...)`, which is now load-bearing rather than lax: it
raises if the unit-sphere chord ever stops being dimensionless, and the
embedded case prints `Distance(4., 'm')` with the unit intact.

This should also clear `codecov/project`: that job uploads the coverage
report, so its failure withheld it. `chord_distance.py` is at 100% and
`codecov/patch` passed throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nstarman
nstarman force-pushed the claude/chord-distance branch from 510e636 to 64674a4 Compare August 18, 2026 18:59
@nstarman
nstarman merged commit 5b65408 into GalacticDynamics:main Aug 18, 2026
18 checks passed
@nstarman
nstarman deleted the claude/chord-distance branch August 18, 2026 19:49
nstarman added a commit that referenced this pull request Aug 18, 2026
* 📝 docs(spec): specify `chord_distance` and `interval`

Of the manifold measurement verbs, `norm`, `angle_between` and
`geodesic_distance` each carry a `software-spec-*` section; `chord_distance`
(#720) never had one, and `interval` had eight passing mentions but no entry of
its own. Both are public, both are reachable as `cx.*`, and neither was
specified.

`chord_distance` follows the `geodesic_distance` entry, since the two are a
pair: same signature shape, opposite question. Its section records that the
result carries the ambient's length unit for an `EmbeddedManifold` but is
*dimensionless* on the bare `HyperSphericalManifold` -- whose canonical
embedding is the unit sphere, so the chord is a pure ratio. That is a real
asymmetry with `geodesic_distance`, which returns an `Angle` on the same
manifold, and it is the sort of thing a spec exists to pin: an arc is an angle,
a chord is not.

`interval`'s section leads with what it is for -- the signed form is defined
where `norm` has no real value and `geodesic_distance` refuses outright -- and
then states plainly that it is **not** a distance. It is neither chart-invariant
nor symmetric, and both were measured rather than asserted:

    same two points, flat plane:  cart2d 5.0 m^2   polar2d 3.467 m^2
                                  geodesic_distance 2.236 m in both
    sph2, one pair:               (a,b) 0.813 rad^2  (b,a) 0.999 rad^2

So `interval == geodesic_distance**2` holds only on a flat manifold *in
Cartesian coordinates*; flatness alone is not enough, since a curvilinear chart
on flat space has a varying metric. Same correction #724 made to the docstrings,
now stated where the contract lives.

Also updates `geodesic_distance`'s "sits alongside `norm` and `angle_between` as
the third manifold measurement", which stopped being complete when
`chord_distance` landed, and cross-links the two.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
nstarman added a commit to nstarman/coordinax that referenced this pull request Aug 20, 2026
`geodesic_distance` has one and `chord_distance` did not, so the two verbs were
usable at different levels -- GalacticDynamics#720 flagged the asymmetry and left it open.

It could not be written the same way. `geodesic_distance`'s overload brings
both operands into a Cartesian chart, which for a chord is exactly wrong: a
Euclidean manifold is its own ambient, so every call would land on the case
`chord_distance` refuses, and an intrinsic sphere chart has no global Cartesian
representation to convert to at all -- `geodesic_distance(p, q)` on `sph2`
raises `NoGlobalCartesianChartError` today for that reason.

The second operand is mapped into the first's chart instead, and the
measurement delegated to the manifold-level rule. The chord is a property of
the embedding, and the intrinsic chart is what carries it.

Frame-strict, matching `geodesic_distance`, and refusing a cross-manifold pair.

Eleven tests: the analytic `2 sin(dphi / 2)` across five separations, symmetry,
chart-invariance with operands in different charts, that it differs from the
geodesic (sqrt(2) against pi/2, the point of two verbs), flat space refusing,
and the cross-frame refusal.

241 passed across `tests/unit/vectors`; `nox -s precommit` clean.
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. ✅ Add / update / pass tests Add, update, or pass tests. 🐛 Fix a bug Fix a bug. 💥 Introduce breaking changes Introduce breaking changes. ✨ Introduce new features Introduce new features.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants