Working With Component Dictionaries (CDict)#

This tutorial covers CDict — coordinax’s lightweight interchange format. A CDict is simply a dict[str, Quantity] mapping component names to their values. It is the common representation that all higher objects (Point, Coordinate) can be decomposed into, and that all lower objects (arrays, quantities) can be assembled from.

You will learn how to:

  • Build CDicts by hand and from other types via cdict()

  • Change coordinate system with pt_map

  • Apply transforms with act (requires explicit chart and rep)

  • Upgrade to Point or Coordinate

  • Use CDicts with JAX

Prerequisites: Working With Charts.

Object Levels

Coordinax supports five levels of coordinate representation, each adding more metadata. This tutorial covers CDict.

Level

Type

See tutorial

Coordinate

Coordinate

Coordinate tutorial

Vector

AbstractVector

Point tutorial

CDict

dict[str, Quantity]

this page

Quantity

unxt.Quantity

Quantity tutorial

Array

jax.Array

Array tutorial

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
>>> import jax.numpy as jnp

What Is A CDict?#

A CDict — short for component dictionary — is a plain Python dictionary with string keys (component names) and quantity or array values:

>>> d = {"x": u.Q(1, "km"), "y": u.Q(2, "km"), "z": u.Q(3, "km")}
>>> sorted(d.keys())
['x', 'y', 'z']

>>> d["x"]
Q(1, 'km')

CDicts carry component names but no chart, representation, or frame metadata. When you pass a CDict to functions like act or pt_map, you must supply the chart and representation explicitly.

Building CDicts#

By Hand#

Simply create a dictionary:

>>> d = {"x": u.Q(1, "km"), "y": u.Q(0, "km"), "z": u.Q(0, "km")}
>>> sorted(d.keys())
['x', 'y', 'z']

From A Quantity Via cdict()#

cxc.cdict() splits a quantity’s last axis into named components:

>>> d = cxc.cdict(u.Q([1, 2, 3], "km"))
>>> sorted(d.keys())
['x', 'y', 'z']

>>> d["x"]
Q(1, 'km')

With an explicit chart:

>>> d = cxc.cdict(u.Q([1, 2, 3], "km"), cxc.cart3d)
>>> sorted(d.keys())
['x', 'y', 'z']

From An Array + Unit Via cdict()#

>>> d = cxc.cdict(jnp.array([1.0, 2.0, 3.0]), "km", cxc.cart3d)
>>> sorted(d.keys())
['x', 'y', 'z']

Identity Pass-Through#

Passing an existing CDict returns it unchanged:

>>> d = {"x": u.Q(1, "km"), "y": u.Q(0, "km"), "z": u.Q(0, "km")}
>>> cxc.cdict(d) is d
True

Changing The Chart (Coordinate Conversion)#

Use cxc.pt_map() with explicit source and target charts:

>>> d_cart = {"x": u.Q(1, "km"), "y": u.Q(2, "km"), "z": u.Q(3, "km")}

>>> d_sph = cxc.pt_map(d_cart, cxc.cart3d, cxc.sph3d)
>>> sorted(d_sph.keys())
['phi', 'r', 'theta']

Round-tripping:

>>> d_back = cxc.pt_map(d_sph, cxc.sph3d, cxc.cart3d)
>>> sorted(d_back.keys())
['x', 'y', 'z']

Using CDicts As Tangent Components#

A CDict can also carry tangent-vector components when you provide that meaning explicitly to a manifold-level API.

For example, cxm.angle_between() interprets two CDicts as tangent vectors in the coordinate basis of a chart and uses the manifold metric to compute the angle between them at a base point:

>>> import coordinax.manifolds as cxm

>>> at = {"x": u.Q(0, "m"), "y": u.Q(0, "m")}
>>> uvec = {"x": u.Q(1, "m"), "y": u.Q(0, "m")}
>>> vvec = {"x": u.Q(0, "m"), "y": u.Q(1, "m")}

>>> ang = cxm.angle_between(cxc.cart2d, uvec, vvec, at=at)
>>> jnp.allclose(u.ustrip("rad", ang), jnp.pi / 2)
Array(True, dtype=bool)

This does not mean that arbitrary point CDicts are automatically tangent vectors. The chart and manifold tell coordinax how to interpret the component data.

Applying Transforms#

Use cxfm.act() with explicit chart and representation. A CDict alone does not know which chart it belongs to, so you must provide that context:

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

>>> d = {"x": u.Q(1, "km"), "y": u.Q(0, "km"), "z": u.Q(0, "km")}
>>> result = cxfm.act(rot90z, None, d, cxc.cart3d, cxr.point)
>>> sorted(result.keys())
['x', 'y', 'z']

Translation:

>>> shift = cxfm.Translate.from_([1, 2, 3], "km")

>>> d_origin = {"x": u.Q(0, "km"), "y": u.Q(0, "km"), "z": u.Q(0, "km")}
>>> result = cxfm.act(shift, None, d_origin, cxc.cart3d, cxr.point)
>>> result["x"]
Q(1, 'km')
>>> result["y"]
Q(2, 'km')
>>> result["z"]
Q(3, 'km')

Identity transform returns the exact same object:

>>> d = {"x": u.Q(1, "km"), "y": u.Q(2, "km"), "z": u.Q(3, "km")}
>>> result = cxfm.act(cxfm.Identity(), None, d)
>>> result is d
True

Changing Tangent Basis#

Use cxr.change_basis() when you want to change the tangent basis of a CDict without changing its chart, geometry, or semantics.

>>> d = {"x": 1.0, "y": 0.0}
>>> at = {"x": 2.0, "y": 3.0}

>>> cxr.change_basis(d, cxc.cart2d, cxr.coord_basis, cxr.phys_basis, at=at)
{'x': 1.0, 'y': 0.0}

>>> cxr.change_basis(d, cxc.cart2d, cxr.coord_disp, cxr.phys_disp, at=at)
{'x': 1.0, 'y': 0.0}

Use cconvert() instead when the chart itself must change.

Upgrading To A Point#

Promote a CDict to a Point by providing chart context:

>>> d = {"x": u.Q(1, "km"), "y": u.Q(2, "km"), "z": u.Q(3, "km")}

>>> v = cx.Point.from_(d, cxc.cart3d)
>>> v.chart
Cart3D(M=Rn(3))

>>> isinstance(v, cx.Point)
True

With chart inference (keys are recognized as Cartesian):

>>> v = cx.Point.from_(d)
>>> v.chart
Cart3D(M=Rn(3))

Upgrading To A Coordinate#

Go all the way to a Coordinate by providing chart and frame:

>>> d = {"x": u.Q(1, "km"), "y": u.Q(2, "km"), "z": u.Q(3, "km")}

>>> coord = cx.Point.from_(d, cxc.cart3d, cxf.alice)
>>> coord.chart
Cart3D(M=Rn(3))
>>> coord.frame
Alice()

Data Access#

CDicts support all standard dictionary operations:

>>> d = {"x": u.Q(1, "km"), "y": u.Q(2, "km"), "z": u.Q(3, "km")}

>>> d["x"]
Q(1, 'km')

>>> sorted(d.keys())
['x', 'y', 'z']

>>> len(d)
3

>>> "x" in d
True

Spacetime CDicts#

CDicts work equally well with 4D spacetime charts. galileanct is the pre-defined Galilean spacetime chart with components (ct, x, y, z):

>>> st = {
...     "ct": u.Q(0.0, "km"),
...     "x": u.Q(1.0, "km"),
...     "y": u.Q(0.0, "km"),
...     "z": u.Q(0.0, "km"),
... }

>>> sorted(st.keys())
['ct', 'x', 'y', 'z']

Convert the spatial part from Cartesian to spherical without touching ct:

>>> st_sph = cxc.pt_map(st, cxc.galileanct, cxc.GalileanCT(cxc.sph3d))
>>> sorted(st_sph.keys())
['ct', 'phi', 'r', 'theta']

>>> st_sph["ct"]
Q(0., 'km')

JAX Integration#

Python dicts are native JAX PyTrees, so CDicts work with jit and vmap directly:

>>> @jax.jit
... def rotate_cdict(d):
...     rot = cxfm.Rotate.from_euler("z", u.Q(90, "deg"))
...     return cxfm.act(rot, None, d, cxc.cart3d, cxr.point)

>>> import jax

>>> d = {"x": u.Q(1.0, "km"), "y": u.Q(0.0, "km"), "z": u.Q(0.0, "km")}
>>> result = rotate_cdict(d)
>>> sorted(result.keys())
['x', 'y', 'z']

When To Use CDict#

Choose CDict when:

  • You want a lightweight intermediate format without the overhead of constructing a full Point.

  • You are building interop layers with non-coordinax code.

  • You need standard dict operations (iteration, merging, filtering).

  • You are inside a performance-sensitive inner loop where allocating Point objects is undesirable.

Trade-off: CDicts require you to pass chart and representation explicitly to every function call. If you find yourself repeating cxc.cart3d, cxr.point everywhere, upgrade to Point. See the Point tutorial.

If you want even less overhead and are willing to manage units externally, use a bare Quantity. See the Quantity tutorial.