Skip to content

Commit 8333741

Browse files
committed
✨ feat: tangent representations
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
1 parent 2eaba5a commit 8333741

17 files changed

Lines changed: 2446 additions & 5 deletions

File tree

docs/api/representations.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,19 @@ This is separate from charts and manifolds:
4040
{'r': Array(3.74165739, dtype=float64, ...),
4141
'theta': Array(0.64052231, dtype=float64),
4242
'phi': Array(1.10714872, dtype=float64, ...)}
43+
44+
# Change tangent components between basis conventions in the same chart.
45+
>>> v = {"r": u.Q(1.0, "km/s"), "theta": u.Q(0.0, "rad/s"), "phi": u.Q(0.0, "rad/s")}
46+
>>> at = {"r": u.Q(2.0, "km"), "theta": u.Q(3.0, "rad"), "phi": u.Q(4.0, "rad")}
47+
>>> v2 = cxr.change_basis(v, cxc.cart2d, cxr.coord_basis, cxr.phys_basis, at=at)
48+
>>> v2
49+
{'r': Q(1., 'km / s'), 'theta': Q(0., 'rad / s'), 'phi': Q(0., 'rad / s')}
4350
```
4451

4552
## Functional API
4653

4754
- `cconvert`: representation-aware coordinate conversion API
55+
- `change_basis`: same-chart tangent basis conversion API
4856
- `cmap`: partial-function builder around `cconvert`
4957
- `guess_basis_kind`: infer basis kind from dimensions/data
5058
- `guess_geometry_kind`: infer geometric kind from dimensions/data
@@ -58,6 +66,7 @@ For point data, `cconvert` dispatches through chart-level point conversion laws.
5866
### Conversion Functions
5967

6068
- `cconvert`: convert data across charts/representations
69+
- `change_basis`: change tangent basis without changing chart
6170
- `cmap`: build reusable conversion callables
6271
- `guess_basis_kind`: infer a basis kind
6372
- `guess_geometry_kind`: infer a geometry kind
@@ -92,6 +101,8 @@ For point data, `cconvert` dispatches through chart-level point conversion laws.
92101

93102
- Representations are orthogonal to charts: chart choice and representation choice are independent concerns.
94103
- The current built-in flow is point-first, centered on `(point_geom, no_basis, loc)`.
104+
- `change_basis` is currently limited to tangent data in Cartesian charts, with `CoordinateBasis` $
105+
ightleftarrows$ `PhysicalBasis` conversions.
95106

96107
```{eval-rst}
97108

docs/guides/representations.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,40 @@ Current built-in representation conversions are point-first:
138138

139139
The representation design is intentionally extensible. Future geometric kinds (for example tangent and cotangent objects) can use different transformation categories (such as Jacobian pushforward/pullback) while keeping the same chart and manifold interfaces.
140140

141+
## Tangent Basis Changes
142+
143+
`change_basis` handles a narrower problem than `cconvert`: it keeps the chart fixed and only changes how tangent components are interpreted with respect to a basis.
144+
145+
In the current v1 implementation, this is intentionally limited to tangent data in Cartesian charts:
146+
147+
- supported basis changes: `CoordinateBasis` $
148+
ightleftarrows$ `PhysicalBasis`
149+
- supported representations: tangent representations such as `coord_disp` and `phys_disp`
150+
- unsupported: point data (`NoBasis`) and non-Cartesian charts
151+
152+
```{code-block} python
153+
>>> import coordinax.charts as cxc
154+
>>> import coordinax.representations as cxr
155+
156+
>>> v = {"x": 1.0, "y": 0.0}
157+
>>> at = {"x": 2.0, "y": 3.0}
158+
159+
>>> cxr.change_basis(v, cxc.cart2d, cxr.coord_basis, cxr.phys_basis, at=at)
160+
{'x': 1.0, 'y': 0.0}
161+
162+
>>> cxr.change_basis(v, cxc.cart2d, cxr.coord_disp, cxr.phys_disp, at=at)
163+
{'x': 1.0, 'y': 0.0}
164+
```
165+
166+
In Cartesian charts the coordinate basis and physical basis coincide, so the component values are unchanged. The API exists so code can state basis intent explicitly and remain extensible to nontrivial basis changes later.
167+
141168
## Quick Reference
142169

143170
- Need only a same-manifold coordinate rewrite: `pt_map`
144171
- Need general point mapping behavior: `pt_map`
145172
- Need manifold compatibility checks: manifold methods like `M.pt_map`
146173
- Need representation-aware conversions: `cconvert`
174+
- Need same-chart tangent basis conversion: `change_basis`
147175
- Need reusable conversion callables: `cmap`
148176
- Need to infer basis kind from data: `guess_basis_kind`
149177
- Need to infer semantic kind from data: `guess_semantic_kind`

docs/spec.md

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ A non-exhaustive table of exported objects are:
946946
| `coordinax.angles` | `AbstractAngle`, `Angle`, `wrap_to` |
947947
| `coordinax.distances` | `AbstractDistance`, `Distance` |
948948
| `coordinax.charts` | `CartesianProductChart`, </br> `cartesian_chart`, `guess_chart`, `cdict`, `pt_map`, `jac_pt_map`, </br> `cart0d`, </br> `cart1d`, `radial1d`, `time1d`, </br> `cart2d`, `polar2d`, </br> `cart3d`, `cyl3d`, `sph3d`, `lonlat_sph3d`, `loncoslat_sph3d`, `math_sph3d`, </br> `cartnd`, </br> `spacetimect` |
949-
| `coordinax.representations` | `cconvert`, </br> `Representation`, `point`, `coord_disp`, `coord_vel`, `coord_acc`, `phys_disp`, `phys_vel`, `phys_acc`, </br> `PointGeometry`, `point_geom`, `TangentGeometry`, `tangent_geom`, </br> `NoBasis`, `no_basis`, `CoordinateBasis`, `coord_basis`, `PhysicalBasis`, `phys_basis`, </br> `Location`, `loc`, `Displacement`, `dpl`, `Velocity`, `vel`, `Acceleration`, `acc`, </br> `guess_geometry_kind`, `guess_semantic_kind`, `guess_rep` |
949+
| `coordinax.representations` | `cconvert`, `change_basis`, `tangent_map`, </br> `Representation`, `point`, `coord_disp`, `coord_vel`, `coord_acc`, `phys_disp`, `phys_vel`, `phys_acc`, </br> `PointGeometry`, `point_geom`, `TangentGeometry`, `tangent_geom`, </br> `NoBasis`, `no_basis`, `CoordinateBasis`, `coord_basis`, `PhysicalBasis`, `phys_basis`, </br> `Location`, `loc`, `Displacement`, `dpl`, `Velocity`, `vel`, `Acceleration`, `acc`, </br> `guess_geometry_kind`, `guess_semantic_kind`, `guess_rep` |
950950
| `coordinax.vectors` | `Point`, `ToUnitsOptions` |
951951
| `coordinax.manifolds` | `guess_manifold`, `scale_factors`, `angle_between`, </br> `EuclideanManifold`, `EuclideanMetric`, `euclidean3d`, </br> `EmbeddedManifold`, `EmbeddedChart` </br> `twosphere`, `embedded_twosphere`, </br> `CustomManifold`,`CustomAtlas`, |
952952
| `coordinax.transforms` | `act`, `simplify`, `compose`, `materialize_transform`, </br> `AbstractTransform`, `Identity`, `Composed`, `Translate`, `Rotate`, `Reflect`, `Scale`, `Shear`, `identity`, </br> `AbstractTransformGroup`, `IdentityGroup`, `DiffeomorphismGroup`, `AffineGroup`, `EuclideanGroup`, `OrthogonalGroup`, `SpecialOrthogonalGroup`, `PoincareGroup`, `LorentzGroup`, `ProperOrthochronousLorentzGroup` |
@@ -1728,6 +1728,44 @@ A representation is therefore **not** the same thing as a chart: the chart deter
17281728

17291729
- Inherits all failure semantics from `guess_geometry_kind`.
17301730

1731+
!!! info `tangent_map`
1732+
1733+
Transform a tangent vector from one chart to another.
1734+
1735+
**Signature:**
1736+
1737+
```text
1738+
tangent_map(v, from_chart, from_geom, from_rep, to_chart, to_geom, to_rep, /, *, at) -> CDict
1739+
```
1740+
1741+
A 4-argument shorthand form is also supported:
1742+
1743+
```text
1744+
tangent_map(v, from_chart, from_rep, to_chart, /, *, at) -> CDict
1745+
```
1746+
1747+
**Arguments:**
1748+
1749+
- `v`: `CDict` — tangent vector components in `from_chart` with basis `from_rep.basis`.
1750+
- `from_chart`: source chart.
1751+
- `from_geom`: source geometry (e.g. `TangentGeometry`).
1752+
- `from_rep`: source `Representation` (must have `TangentGeometry`).
1753+
- `to_chart`: target chart.
1754+
- `to_geom`: target geometry.
1755+
- `to_rep`: target `Representation`.
1756+
- `at`: `CDict` — base point in `from_chart` coordinates at which the tangent space is attached. Required for non-Cartesian charts (since the Jacobian depends on the base point).
1757+
1758+
**Semantics by basis:**
1759+
1760+
- **`CoordinateBasis`**: delegates to `jac_pt_map(at, from_chart, to_chart)` to obtain the Jacobian $J^j{}_i = \partial\tilde{q}^j/\partial q^i$, then applies $\tilde{v}^j = J^j{}_i v^i$.
1761+
- **`PhysicalBasis`**: fetch the orthonormal frame matrices $B_{\rm from}$ (columns = physical basis vectors in Cartesian) and $B_{\rm to}$ via `frame_cart`, compute $R = B_{\rm to}^T B_{\rm from}$, apply $\hat{v}' = R \hat{v}$.
1762+
1763+
**Same-chart optimisation:** when `from_chart is to_chart`, returns `v` unchanged.
1764+
1765+
**`cconvert` integration:** `cconvert` dispatches to `tangent_map` when the source representation has `TangentGeometry`, passing `at` through the `at` keyword argument.
1766+
1767+
**Same-chart basis conversion:** `change_basis(v, chart, from_basis, to_basis, /, *, at)` changes tangent component conventions without changing charts. In v1 it is defined only for Cartesian charts and `CoordinateBasis` $
1768+
17311769
</br>
17321770

17331771
### Geometric Kind
@@ -2625,6 +2663,90 @@ $$g_{ij}(q) = g_p\!\left(\frac{\partial}{\partial q^i}, \frac{\partial}{\partial
26252663
- [`MinkowskiMetric`](#software-spec-minkowskimetric): Lorentzian metric $\eta = \operatorname{diag}(-1, 1, 1, 1)$ on Minkowski spacetime; diagonal in the canonical Cartesian spacetime chart.
26262664
- [`HyperSphericalMetric`](#software-spec-hypersphericalmetric): round metric on $S^{n-1}$; diagonal entries follow the cumulative-sine rule $g_{kk} = \prod_{j < k}\sin^2\!\theta_j$.
26272665

2666+
(software-spec-abstractdiagonalmetric)=
2667+
2668+
!!! info `AbstractDiagonalMetric`
2669+
2670+
`AbstractDiagonalMetric` is an abstract subclass of `AbstractMetric` for metrics whose matrix is diagonal at every base point in every compatible chart.
2671+
2672+
A metric is **diagonal** (equivalently, the coordinate chart is an **orthogonal coordinate system**) when all off-diagonal entries of the metric matrix vanish:
2673+
2674+
$$g_{ij}(p) = 0 \quad \text{for } i \neq j \quad \forall\, p \in U.$$
2675+
2676+
The coordinate basis vectors $\partial/\partial q^i$ are mutually orthogonal. The diagonal entries $g_{ii}(p)$ give the squared scale factors
2677+
2678+
$$h_i(p)^2 = g_{ii}(p),$$
2679+
2680+
and the infinitesimal line element simplifies to
2681+
2682+
$$ds^2 = \sum_i g_{ii}(q)\,(dq^i)^2.$$
2683+
2684+
**Role: structural marker, not behavioral interface.**
2685+
2686+
`AbstractDiagonalMetric` adds no new abstract methods beyond those of `AbstractMetric`. Its sole purpose is to **declare** that `metric_matrix` will always return a diagonal matrix. This allows:
2687+
2688+
- Dispatch specialisations that compute `scale_factors` more efficiently
2689+
(e.g., extracting only the diagonal of $g$, or using squared Jacobian
2690+
column norms instead of the full $J^\top J$).
2691+
- Type-level distinction between orthogonal and general metrics in
2692+
multiple-dispatch registrations.
2693+
2694+
**Subclassing contract:**
2695+
2696+
Subclasses must implement the two abstract members inherited from
2697+
`AbstractMetric`:
2698+
2699+
- `signature` (abstract property): a tuple of $\pm 1$ of length `ndim`
2700+
encoding the metric signature in coordinate order. Positive entries
2701+
are Riemannian (space-like); a ``-1`` entry is pseudo-Riemannian
2702+
(time-like).
2703+
- `metric_matrix(chart, /, *, at, usys=None)` (abstract method): must
2704+
return a diagonal `QuantityMatrix` (or plain `Array`) of shape
2705+
`(ndim, ndim)` with all off-diagonal entries exactly zero.
2706+
2707+
All other behavioral requirements of `AbstractMetric` also apply:
2708+
immutability (frozen dataclass), static JAX PyTree registration, and
2709+
`jit`/`vmap` compatibility.
2710+
2711+
**Relationship to `AbstractMetric.is_diagonal`:**
2712+
2713+
`AbstractMetric.is_diagonal(chart, at=at)` inspects the metric matrix at
2714+
a **specific base point** and returns a `bool`.
2715+
`AbstractDiagonalMetric` makes this an unconditional **structural
2716+
promise** across all base points: instances are always diagonal,
2717+
regardless of which chart or point is supplied.
2718+
2719+
**Concrete subclasses (built-in):**
2720+
2721+
| Class | Manifold | Diagonal in |
2722+
|-------|----------|-------------|
2723+
| [`EuclideanMetric`](#software-spec-euclideanmetric) | $\mathbb{R}^n$ | Cartesian charts; orthogonal curvilinear charts via $g = J^\top J$ |
2724+
| [`HyperSphericalMetric`](#software-spec-hypersphericalmetric) | $S^{n-1}$ | Intrinsic hyperspherical chart; cumulative-sine rule $g_{kk} = \prod_{j<k}\sin^2\!\theta_j$ |
2725+
| [`MinkowskiMetric`](#software-spec-minkowskimetric) | $\mathbb{R}^{1,3}$ | Canonical Cartesian spacetime chart $\eta = \operatorname{diag}(-1,1,1,1)$ |
2726+
2727+
**Example**
2728+
2729+
```pycon
2730+
>>> from coordinax.manifolds._src.diagonal import AbstractDiagonalMetric
2731+
>>> import coordinax.manifolds as cxm
2732+
2733+
>>> isinstance(cxm.EuclideanMetric(3), AbstractDiagonalMetric)
2734+
True
2735+
2736+
>>> isinstance(cxm.MinkowskiMetric(), AbstractDiagonalMetric)
2737+
True
2738+
2739+
>>> import unxt as u
2740+
>>> isinstance(
2741+
... cxm.InducedMetric(
2742+
... cxm.TwoSphereIn3D(radius=u.Q(1.0, "m")),
2743+
... cxm.EuclideanMetric(3),
2744+
... ),
2745+
... AbstractDiagonalMetric,
2746+
... )
2747+
False
2748+
```
2749+
26282750
(software-spec-abstractmanifold)=
26292751

26302752
!!! info `AbstractManifold`

docs/tutorials/cdict_objects.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,25 @@ Identity transform returns the exact same object:
184184
True
185185
```
186186

187+
## Changing Tangent Basis In-Place
188+
189+
Use `cxr.change_basis()` when a CDict already lives in the correct chart and you only need to reinterpret tangent components with respect to a different basis.
190+
191+
In v1 this supports Cartesian tangent data only. For Cartesian charts, coordinate and physical bases coincide, so the values are preserved.
192+
193+
```{code-block} python
194+
>>> d = {"x": 1.0, "y": 0.0}
195+
>>> at = {"x": 2.0, "y": 3.0}
196+
197+
>>> cxr.change_basis(d, cxc.cart2d, cxr.coord_basis, cxr.phys_basis, at=at)
198+
{'x': 1.0, 'y': 0.0}
199+
200+
>>> cxr.change_basis(d, cxc.cart2d, cxr.coord_disp, cxr.phys_disp, at=at)
201+
{'x': 1.0, 'y': 0.0}
202+
```
203+
204+
Use `cconvert()` instead when the chart itself must change.
205+
187206
## Upgrading To A Point
188207

189208
Promote a CDict to a `Point` by providing chart context:

packages/coordinax.api/src/coordinax/api/representations.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
__all__ = (
44
"add",
55
"cconvert",
6+
"change_basis",
67
"guess_basis_kind",
78
"guess_geometry_kind",
89
"guess_rep",
@@ -15,6 +16,42 @@
1516
import plum
1617

1718

19+
@plum.dispatch.abstract
20+
def change_basis(*args: Any, **kwargs: Any) -> Any:
21+
"""Change the basis of a tangent vector's components.
22+
23+
Supports both basis-object and representation-object overloads.
24+
Tangent-only in v1: only CoordinateBasis <-> PhysicalBasis conversions are supported.
25+
26+
Examples
27+
--------
28+
>>> import coordinax.representations as cxr
29+
>>> import coordinax.charts as cxc
30+
>>> v = {"x": 1.0, "y": 0.0}
31+
>>> at = {"x": 1.0, "y": 0.0}
32+
>>> cxr.change_basis(v, cxc.cart2d, cxr.coord_basis, cxr.phys_basis, at=at)
33+
{'x': 1.0, 'y': 0.0}
34+
"""
35+
raise NotImplementedError # pragma: no cover
36+
37+
38+
@plum.dispatch.abstract
39+
def tangent_map(*args: Any, **kwargs: Any) -> Any:
40+
"""Compute the tangent map (Jacobian) of a chart transition.
41+
42+
This is an abstract API definition. See the main coordinax package for
43+
concrete implementations.
44+
45+
Examples
46+
--------
47+
>>> import jax.numpy as jnp
48+
>>> import coordinax.charts as cxc
49+
>>> import coordinax.representations as cxr
50+
51+
"""
52+
raise NotImplementedError # pragma: no cover
53+
54+
1855
@plum.dispatch.abstract
1956
def cconvert(*args: Any, **kwargs: Any) -> Any:
2057
"""Transform the current vector to the target chart.

src/coordinax/main/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@
7878
"phys_disp",
7979
"phys_vel",
8080
"phys_acc",
81+
"tangent_map",
82+
"change_basis",
8183
# vectors
8284
"Point",
8385
"ToUnitsOptions",
@@ -134,6 +136,7 @@
134136
acc,
135137
add,
136138
cconvert,
139+
change_basis,
137140
coord_acc,
138141
coord_basis,
139142
coord_disp,
@@ -149,6 +152,7 @@
149152
point_geom,
150153
subtract,
151154
tangent_geom,
155+
tangent_map,
152156
vel,
153157
)
154158
from coordinax.transforms import (

src/coordinax/representations/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@
9696
"guess_rep",
9797
"guess_semantic_kind",
9898
"subtract",
99+
"tangent_map",
100+
"change_basis",
99101
# Representations
100102
"Representation",
101103
"point",
@@ -154,6 +156,7 @@
154156
TangentGeometry,
155157
Velocity,
156158
acc,
159+
change_basis,
157160
cmap,
158161
coord_acc,
159162
coord_basis,
@@ -169,6 +172,7 @@
169172
point,
170173
point_geom,
171174
tangent_geom,
175+
tangent_map,
172176
vel,
173177
)
174178
from coordinax.api.representations import (
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Representations."""
22

33
from .basis import *
4+
from .basis_change import *
45
from .core import *
56
from .geom import *
67
from .guess import *
78
from .register_cx import *
89
from .rep import *
910
from .semantics import *
11+
from .tangent_map import *

0 commit comments

Comments
 (0)