Working With Vector Objects#
This tutorial covers Vector β coordinaxβs self-contained geometric coordinate container. A Vector stores data together with its chart (coordinate system), representation (transformation law), and manifold, so every number carries its mathematical meaning.
You will learn how to:
Construct vectors from dictionaries, arrays, and quantities
Change coordinate system with
cconvertApply transforms with
actConvert units per-component
Access data fields and inspect metadata
Use vectors with JAX
jitandvmap
Prerequisites: Working With Charts.
Object Levels
Coordinax supports a few levels of coordinate representation, each adding
more metadata. This tutorial covers Point.
Level |
Type |
See tutorial |
|---|---|---|
Coordinate bundle |
|
|
Point & Tangent |
|
this page, Tangent tutorial |
CDict |
|
|
Quantity |
|
|
Array |
|
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
What Is A Vector?#
A Vector bundles four things together:
Vector = data + chart + representation + manifold
data: a dictionary mapping component names to values (quantities or arrays)
chart: the coordinate system (e.g. Cartesian, spherical)
representation: how the data transforms (point, tangent, etc.)
manifold: the geometric space the data lives in
Because all metadata is attached, Vector operations like cconvert and act need no extra arguments β the vector knows everything about itself.
Constructing Vectors#
Vector.from_() supports flexible input patterns.
From An Array And Unit (Simplest)#
The chart is inferred from the array length (3 β cart3d):
>>> v = cx.Point.from_([1, 2, 3], "m")
>>> v.chart
Cart3D(M=Rn(3))
>>> sorted(v.data.keys())
['x', 'y', 'z']
Shape inference:
Length 3 β
cart3dLength 2 β
cart2dLength 1 β
cart1d
From A Quantity#
>>> q = u.Q([1, 2, 3], "m")
>>> v = cx.Point.from_(q)
>>> v.chart
Cart3D(M=Rn(3))
From A Component Dictionary#
The most explicit pattern β every component is named:
>>> v = cx.Point.from_(
... {"x": u.Q(1, "m"), "y": u.Q(2, "m"), "z": u.Q(3, "m")}
... )
>>> v.chart
Cart3D(M=Rn(3))
With Explicit Chart#
Override the inferred chart:
>>> v = cx.Point.from_(
... {"r": u.Q(1, "m"), "theta": u.Q(0.5, "rad"), "phi": u.Q(1.0, "rad")},
... cxc.sph3d,
... )
>>> v.chart
Spherical3D(M=Rn(3))
With Explicit Chart And Representation#
>>> v = cx.Point.from_(
... {"x": u.Q(1, "m"), "y": u.Q(2, "m"), "z": u.Q(3, "m")},
... cxc.cart3d,
... cxr.point,
... )
>>> print(v.rep)
Representation(geom_kind=PointGeometry(), basis=NoBasis(), semantic_kind=Location())
Passthrough#
>>> v1 = cx.Point.from_([1, 2, 3], "m")
>>> v2 = cx.Point.from_(v1)
>>> v2 is v1
True
Inspecting Vector Metadata#
>>> v = cx.Point.from_([1, 2, 3], "m")
>>> v.chart
Cart3D(M=Rn(3))
>>> v.rep
point
>>> v.M
Rn(3)
>>> sorted(v.data.keys())
['x', 'y', 'z']
>>> v.data["x"]
Q(1, 'm')
Reading Metric Diagonals#
Because a vector carries both its chart and its manifold, you can ask for the metric diagonal entries at the represented location:
>>> import coordinax.manifolds as cxm
>>> v = cx.Point.from_(
... {"r": u.Q(2, "km"), "theta": u.Angle(jnp.pi / 2, "rad"), "phi": u.Angle(0, "rad")},
... cxc.sph3d,
... )
>>> gdiag = cxm.scale_factors(v.chart, at=v.data)
>>> gdiag.shape
(3,)
>>> gdiag.unit.to_string()
'(, km2 / rad2, km2 / rad2)'
scale_factors returns the diagonal metric entries \(g_{ii}\) as a 1-D QMatrix, so each direction can keep its own unit.
Changing The Chart (Coordinate Conversion)#
Use cx.cconvert() to transform between coordinate systems. The geometric point is preserved; the chart and component values change:
>>> v_cart = cx.Point.from_(
... {"x": u.Q(1, "km"), "y": u.Q(2, "km"), "z": u.Q(3, "km")}
... )
>>> v_sph = cx.cconvert(v_cart, cxc.sph3d)
>>> v_sph.chart
Spherical3D(M=Rn(3))
>>> sorted(v_sph.data.keys())
['phi', 'r', 'theta']
Round-tripping:
>>> v_back = cx.cconvert(v_sph, cxc.cart3d)
>>> v_back.chart
Cart3D(M=Rn(3))
Cartesian β Cylindrical β Spherical#
>>> v = cx.Point.from_({"x": u.Q(1, "km"), "y": u.Q(1, "km"), "z": u.Q(1, "km")})
>>> v_cyl = cx.cconvert(v, cxc.cyl3d)
>>> v_cyl.chart
Cylindrical3D(M=Rn(3))
>>> v_sph = cx.cconvert(v_cyl, cxc.sph3d)
>>> v_sph.chart
Spherical3D(M=Rn(3))
Applying Transforms#
Use cxfm.act() to apply a transform. Because Vector is self-contained, no extra arguments are needed:
>>> rot90z = cxfm.Rotate.from_euler("z", u.Q(90, "deg"))
>>> v = cx.Point.from_({"x": u.Q(1, "km"), "y": u.Q(0, "km"), "z": u.Q(0, "km")})
>>> rotated = cxfm.act(rot90z, None, v)
>>> isinstance(rotated, cx.Point)
True
Translation:
>>> shift = cxfm.Translate.from_([1, 2, 3], "km")
>>> v_origin = cx.Point.from_({"x": u.Q(0, "km"), "y": u.Q(0, "km"), "z": u.Q(0, "km")})
>>> shifted = cxfm.act(shift, None, v_origin)
>>> shifted.data["x"]
Q(1, 'km')
>>> shifted.data["y"]
Q(2, 'km')
>>> shifted.data["z"]
Q(3, 'km')
The second argument is tau (time parameter) β pass None for static transforms. For time-dependent transforms, see the Time-Dependent Frames tutorial.
Unit Conversion#
Convert units per-component with u.uconvert():
>>> v = cx.Point.from_([1000, 2000, 3000], "m")
>>> v_km = u.uconvert({"x": "km", "y": "km", "z": "km"}, v)
>>> v_km.data["x"]
Q(1., 'km')
>>> v_km.data["y"]
Q(2., 'km')
Immutability#
Vectors are immutable. To create a modified copy, use equinox.tree_at():
>>> import equinox as eqx
>>> v = cx.Point.from_({"x": u.Q(1, "m"), "y": u.Q(2, "m"), "z": u.Q(3, "m")})
>>> v2 = eqx.tree_at(lambda t: t.data["x"], v, u.Q(10, "m"))
>>> v.data["x"]
Q(1, 'm')
>>> v2.data["x"]
Q(10, 'm')
JAX Integration#
Vectors are JAX PyTrees (via Equinox), so all JAX transformations work out of the box.
JIT Compilation#
>>> @jax.jit
... def rotate_to_spherical(v):
... r = cxfm.act(cxfm.Rotate.from_euler("z", u.Q(90, "deg")), None, v)
... return cx.cconvert(r, cxc.sph3d)
>>> v = cx.Point.from_({"x": u.Q(1.0, "km"), "y": u.Q(0.0, "km"), "z": u.Q(0.0, "km")})
>>> result = rotate_to_spherical(v)
>>> result.chart
Spherical3D(M=Rn(3))
Upgrading To A Coordinate#
A Point represents a location. If you also need to carry tangent quantities like velocity or acceleration at that point, bundle the Point with Tangent fields into a Coordinate:
>>> import coordinax.representations as cxr
>>> import unxt as u
>>> 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=cx.Point.from_([1.0, 0.0, 0.0], "m"), velocity=vel)
>>> pv_sph = pv.cconvert(cxc.sph3d)
>>> pv_sph["velocity"].chart
Spherical3D(M=Rn(3))
See the Coordinate tutorial for the full walkthrough.
Tangent on its own is not an upgrade of Point β it is a separate type for tangent-space quantities that exist independently of a base location. See the Tangent tutorial if you need to work with tangent vectors directly.
Attach a reference frame to a Point to track the observer:
>>> v = cx.Point.from_({"x": u.Q(1, "km"), "y": u.Q(2, "km"), "z": u.Q(3, "km")})
>>> coord = cx.Point.from_(v, cxf.alice)
>>> coord.frame
Alice()
>>> coord.chart
Cart3D(M=Rn(3))
Promoting a Point to a Displacement#
Sometimes you have a Point whose component data you want to reinterpret as a tangent-space displacement rather than an absolute location β for example, when feeding a position offset into an operation that expects a Tangent.
change_basis handles this in one call. The component data is unchanged; only the geometric type changes from PointGeometry to TangentGeometry with Displacement semantics:
>>> pt = cx.Point.from_([1.0, 2.0, 3.0], "m")
>>> disp = cxr.change_basis(pt, cxr.coord_basis)
>>> disp
Tangent( {'x': Q(1., 'm'), 'y': Q(2., 'm'), 'z': Q(3., 'm')},
chart=Cart3D(M=Rn(3)), basis=coord_basis, semantic=dpl )
You can request the physical (orthonormal) basis instead:
>>> disp_phys = cxr.change_basis(pt, cxr.phys_basis)
>>> print(disp_phys.rep)
Representation(
geom_kind=TangentGeometry(), basis=PhysicalBasis(), semantic_kind=Displacement()
)
See the Tangent guide for more on basis and semantic kinds.
When To Use Point#
Choose Point when:
You need chart and representation metadata attached to your data.
You want
cconvertandactto work without extra arguments.You do not need reference-frame tracking (no
to_frame).You are doing geometry: chart conversions, transform pipelines, or building higher-level abstractions.
If you need tangent fields (velocity, displacement, acceleration) bundled with the point, use Coordinate. See the Coordinate tutorial.
If you need standalone tangent quantities (without a base point), use Tangent. See the Tangent tutorial.
If you want to reinterpret a Pointβs data as a displacement, use cxr.change_basis(pt, cxr.coord_basis) to promote it to a Tangent with Displacement semantics.
If you need frame tracking, attach a frame to a Point. See the Coordinate tutorial.
If you want a lighter representation, use a component dictionary (CDict). See the CDict tutorial.