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 aPointwith namedTangentfibre fields (velocities, displacements, accelerations). Chart conversion and frame changes propagate to all fields at once.
You will learn how to:
Construct
Pointobjects with reference framesConvert
Pointbetween charts and framesBundle a
PointwithTangentfields into aCoordinateConvert the whole bundle between charts in one call
Change reference frame for the whole bundle
Use both types with JAX
jitandvmap
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:
Base
Pointconverts by the chart transition map.Each
Tangentconverts 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 |
|
Velocity / displacement / acceleration only |
|
Position + tangent field(s) |
|
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.