Working With Points And Coordinates#

This tutorial covers two linked types:

  • Point β€” a position carrying its chart and reference frame, so every number has full provenance: what, where, and from whose perspective.

  • Coordinate β€” a bundle of a Point with named Tangent fibre fields (velocities, displacements, accelerations). Chart conversion and frame changes propagate to all fields at once.

You will learn how to:

  • Construct Point objects with reference frames

  • Convert Point between charts and frames

  • Bundle a Point with Tangent fields into a Coordinate

  • Convert the whole bundle between charts in one call

  • Change reference frame for the whole bundle

  • Use both types with JAX jit and vmap

Prerequisites: Working With Vectors, Working With Tangent Vectors, and Working With Frames.

Setup#

>>> import coordinax.main as cx
>>> import coordinax.charts as cxc
>>> import coordinax.frames as cxf
>>> import coordinax.representations as cxr
>>> import coordinax.transforms as cxfm
>>> import unxt as u
>>> import jax.numpy as jnp
>>> import jax

Part 1: Point β€” A Position With Frame#

A Point stores a position: component data, a chart (coordinate system), and optionally a reference frame β€” the observer from whose perspective the coordinates are expressed. A Point without a frame defaults to NoFrame().

Constructing A Point#

>>> p = cx.Point.from_([1.0, 2.0, 3.0], "km")
>>> print(p)
<Point: chart=Cart3D (x, y, z) [km]
    [1. 2. 3.]>

Pass a frame as the last argument to attach it at construction:

>>> p_alice = cx.Point.from_([1.0, 2.0, 3.0], "km", cxf.alice)
>>> p_alice.frame
Alice()

Or attach a frame to an existing Point:

>>> p_alice2 = cx.Point.from_(p, cxf.alice)
>>> p_alice2.frame
Alice()

For the most explicit construction β€” specifying data, chart, and frame:

>>> p_explicit = cx.Point.from_(
...     {"x": u.Q(1.0, "km"), "y": u.Q(2.0, "km"), "z": u.Q(3.0, "km")},
...     cxc.cart3d, cxf.alice)
>>> p_explicit.chart
Cart3D(M=Rn(3))
>>> p_explicit.frame
Alice()

Changing Chart#

cconvert preserves the frame; only the chart and component values change:

>>> p_sph = p_alice.cconvert(cxc.sph3d)
>>> p_sph.chart
Spherical3D(M=Rn(3))
>>> p_sph.frame
Alice()

Round-tripping:

>>> p_back = p_sph.cconvert(cxc.cart3d)
>>> p_back.chart
Cart3D(M=Rn(3))

Changing Frame#

to_frame transforms the components into the new observer’s frame. The chart is preserved:

>>> p_alex = p_alice.to_frame(cxf.alex)
>>> p_alex.frame
Alex()
>>> p_alex.chart
Cart3D(M=Rn(3))

Identity frame changes are no-ops:

>>> p_alice.to_frame(cxf.alice) is p_alice
True

To apply a rotation and then convert to spherical in one pipeline:

>>> rot90z = cxfm.Rotate.from_euler("z", u.Q(90, "deg"))
>>> rotated_frame = cxf.TransformedReferenceFrame(cxf.alice, rot90z)

>>> p_rotated_sph = p_alice.to_frame(rotated_frame).cconvert(cxc.sph3d)
>>> p_rotated_sph.chart
Spherical3D(M=Rn(3))
>>> isinstance(p_rotated_sph.frame, cxf.TransformedReferenceFrame)
True

See point_objects.md for a full walkthrough of Point including component dictionaries, applying transforms with act, unit conversion, and immutability.


Part 2: Coordinate β€” Point With Tangent Fields#

A Coordinate is a vector bundle: a base Point together with named Tangent fibre fields anchored at it.

Coordinate = Point (base) + {name: Tangent} (fibres)

Chart conversion propagates consistently: the base Point converts by the chart transition map, and each Tangent converts by the Jacobian pushforward at the base. On construction, every fibre field whose frame differs from the base point’s frame is automatically converted to match it, so pv["velocity"].frame == pv.point.frame always holds.

Constructing A Coordinate#

Pass a base Point and any number of named Tangent keyword arguments:

>>> point = cx.Point.from_([1.0, 0.0, 0.0], "m")
>>> vel = cx.Tangent.from_(
...     {"x": u.Q(1.0, "m/s"), "y": u.Q(0.0, "m/s"), "z": u.Q(0.0, "m/s")},
...     cxc.cart3d, cxr.coord_vel,
... )

>>> pv = cx.Coordinate(point=point, velocity=vel)
>>> isinstance(pv, cx.Coordinate)
True

Accessing The Bundle#

Properties delegate to the base point; fibre fields are accessed by name:

>>> pv.chart        # delegates to pv.point.chart
Cart3D(M=Rn(3))
>>> pv.frame        # delegates to pv.point.frame
NoFrame()
>>> list(pv.keys())
['velocity']
>>> isinstance(pv["velocity"], cx.Tangent)
True
>>> pv["velocity"].semantic
vel

Multiple Fibre Fields#

A Coordinate can hold any number of named tangent fields:

>>> acc = cx.Tangent.from_(
...     {"x": u.Q(0.0, "m/s^2"), "y": u.Q(0.0, "m/s^2"), "z": u.Q(-9.8, "m/s^2")},
...     cxc.cart3d, cxr.coord_acc,
... )

>>> pv2 = cx.Coordinate(point=point, velocity=vel, acceleration=acc)
>>> sorted(pv2.keys())
['acceleration', 'velocity']
>>> pv2["acceleration"].semantic
acc

Converting Charts (The Whole Bundle)#

cconvert converts all fields at once:

  1. Base Point converts by the chart transition map.

  2. Each Tangent converts by the Jacobian pushforward at the base point.

>>> pv_sph = pv.cconvert(cxc.sph3d)
>>> pv_sph.point.chart
Spherical3D(M=Rn(3))
>>> pv_sph["velocity"].chart
Spherical3D(M=Rn(3))

Round-tripping:

>>> pv_back = pv_sph.cconvert(cxc.cart3d)
>>> pv_back.point.chart
Cart3D(M=Rn(3))

Override the target chart for individual fields with field_charts:

>>> pv_mixed = pv.cconvert(cxc.sph3d, field_charts={"velocity": cxc.cyl3d})
>>> pv_mixed.point.chart
Spherical3D(M=Rn(3))
>>> pv_mixed["velocity"].chart
Cylindrical3D(M=Rn(3))

Changing Reference Frame#

to_frame moves all fields into the new frame. Construct the framed pieces first, then bundle:

>>> point_alice = cx.Point.from_([1.0, 0.0, 0.0], "m", cxf.alice)
>>> vel_alice = cx.Tangent.from_(vel, cxf.alice)
>>> pv_alice = cx.Coordinate(point=point_alice, velocity=vel_alice)

>>> pv_alex = pv_alice.to_frame(cxf.alex)
>>> pv_alex.frame
Alex()
>>> pv_alex["velocity"].frame
Alex()

Automatic Frame Alignment#

If a fibre field’s frame differs from the base point’s frame on construction, it is automatically converted to the base’s frame. No manual conversion needed:

>>> vel_alex = cx.Tangent.from_(vel, cxf.alex)

>>> # point is alice, vel is alex β€” vel is silently converted to alice
>>> pv_auto = cx.Coordinate(point=point_alice, velocity=vel_alex)
>>> pv_auto["velocity"].frame
Alice()

Combined Pipeline: Frame + Chart#

Frame changes and chart conversions can be chained:

>>> result = pv_alice.to_frame(rotated_frame).cconvert(cxc.sph3d)
>>> result.point.chart
Spherical3D(M=Rn(3))
>>> isinstance(result.frame, cxf.TransformedReferenceFrame)
True

Indexing A Batched Coordinate#

Integer/slice indexing applies to the base point and all fibre fields together:

>>> pts = cx.Point.from_(jnp.ones((4, 3)), "m")
>>> vels = cx.Tangent.from_(jnp.zeros((4, 3)), "m/s")
>>> pv_batch = cx.Coordinate(point=pts, velocity=vels)

>>> pv_0 = pv_batch[0]
>>> pv_0.point.shape
()

JAX Integration#

Both Point and Coordinate are JAX PyTrees and work with all JAX transformations.

JIT Compilation#

>>> to_spherical = jax.jit(lambda coord: coord.cconvert(cxc.sph3d))

>>> result = to_spherical(pv)
>>> result.point.chart
Spherical3D(M=Rn(3))

Vectorisation With vmap#

>>> pts_batch = cx.Point.from_(
...     jnp.stack([jnp.array([1.0, 0.0, 0.0]), jnp.array([0.0, 1.0, 0.0])]), "m"
... )
>>> vels_batch = cx.Tangent.from_(jnp.zeros((2, 3)), "m/s")
>>> pv_batch2 = cx.Coordinate(point=pts_batch, velocity=vels_batch)

>>> pv_sph_batch = jax.vmap(to_spherical)(pv_batch2)
>>> pv_sph_batch.point.chart
Spherical3D(M=Rn(3))

When To Use#

You have

Use

Position only

Point (Part 1 of this tutorial)

Velocity / displacement / acceleration only

Tangent β€” see Tangent tutorial

Position + tangent field(s)

Coordinate (Part 2 of this tutorial)

Use Coordinate when you need chart conversion to apply the correct transformation law to every field automatically, or when frame changes must propagate to all fields at once.