Working With Frames#
This guide provides a conceptual introduction to coordinax reference frames and transformation groups, with practical workflows. For API reference, see the frames module reference.
Motivation: Why Reference Frames?#
In a single inertial reference frame, coordinates are simple: a point is a point, and vectors obey standard rules.
Real problems often involve multiple observers:
N-body simulations: sun-centered, galactocentric, and particle-relative frames coexist
Galactic dynamics: ICRS (celestial), Galactocentric (rotating), and stream frames
Spacecraft: inertial frame, spacecraft body frame, instrument frame
Relativistic physics: different time-orthogonal hypersurfaces
Rather than manually converting every coordinate every time, coordinax allows you to define frames once and relate them via transformations.
The Reference Frame Model#
A reference frame \(F\) represents a spatial observer’s perspective on coordinates.
Active (Moving) Frame View:
Different frames produce different coordinate representations of the same point:
Frame Transition (Active Transformation):
A transformation \(T: F \to F'\) acts on coordinates by the operator attached to that frame change:
In coordinax, this is an active transformation: applying the operator moves the represented point data on the manifold.
Transformation Groups: Mathematical Classification#
Transformations are classified by the geometric structures they preserve. This classification lives in transformation groups.
Group Hierarchy (ASCII Tree)#
DiffeomorphismGroup
├─ AffineGroup
│ ├─ EuclideanGroup
│ │ ├─ OrthogonalGroup
│ │ │ └─ SpecialOrthogonalGroup
│ │ └─ LorentzGroup
│ │ └─ ProperOrthochronousLorentzGroup
│ └─ PoincareGroup
└─ IdentityGroup
Reading this tree: arrows point toward more restrictive groups. Moving down = stronger constraints.
Group Semantics#
Identity Group#
Preserves: everything
Allows: only the identity map
Math: \(T(x) = x\)
Use: null placeholder
Diffeomorphism Group#
Preserves: smooth structure
Allows: any smooth invertible map
Math: \(T: M \to M\), \(T\) and \(T^{-1}\) smooth
Use: most general transformations
Affine Group#
Preserves: parallelism, ratios along lines
Allows: linear map + translation
Math: \(T(x) = A x + b\) with \(A\) invertible
Use: coordinate systems with linear structure
Examples: shearing, scaling, rotation, translation
Euclidean Group (Rigid Motions)#
Preserves: distances \(\|T(x) - T(y)\| = \|x - y\|\), angles
Allows: rotations, translations, reflections
Math: Orthogonal \(Q\) (preserves metric) + translation \(b\): \(T(x) = Qx + b\)
Use: rigid body motion, non-expanding cosmologies
Examples: spacecraft body-frame rotations, galactic proper motion
Orthogonal Group#
Preserves: angles, inner product \(\langle T(x), T(y) \rangle = \langle x, y \rangle\)
Allows: rotations and reflections about origin
Math: \(Q^T Q = I\) (preserves metric)
Use: rotations and reflections with fixed origin (no translation)
Examples: coordinate system alignment
Special Orthogonal Group (SO(n))#
Preserves: distances, angles, orientation (handedness)
Allows: proper rotations only (no reflections)
Math: \(Q^T Q = I\) and \(\det(Q) = +1\)
Use: when orientation matters (cross products, angular momentum)
Examples: spinning reference frames, gyroscope orientation
Lorentz Group (Special Relativity)#
Preserves: spacetime intervals \(|ds|^2 = -(dt)^2 + (dx)^2 + (dy)^2 + (dz)^2\)
Allows: boosts (Lorentz transformations) mixing space and time
Math: \(\Lambda^T \eta \Lambda = \eta\) (preserves Minkowski metric)
Use: relativistic coordinate transformations
Examples: reference frames moving at relativistic speeds
Proper Orthochronous Lorentz Group#
Preserves: spacetime intervals, spatial and temporal orientations
Allows: only “physical” Lorentz transformations
Math: Lorentz transforms with \(\det(\Lambda) = +1\) and forward time-direction
Use: real-world relativistic transformations
Examples: actual spacecraft boosts, particle collision frames
Poincaré Group#
Preserves: spacetime intervals (like Lorentz + translations)
Allows: Lorentz transformations + spacetime translations
Math: Semidirect product of Lorentz group and spacetime translation
Use: most general relativistic frame transitions
Examples: combining boosts and general spacetime repositioning
Why Groups Matter#
Group membership answers: “What geometric properties does this transformation preserve?”
# Example: Are distances preserved?
# Yes if: transform ∈ Euclidean ⊂ Affine
# No if: transform ∈ Affine ⊂ Diffeomorphism
This enables:
Correctness: ensure physically meaningful transforms
Dispatch: select correct numerical methods based on group
Optimization: simplify or cancel transforms knowing properties
Building Transformations#
Primitive Transforms#
Identity (Do Nothing)#
import coordinax.frames as cxf
import coordinax.transforms as cxfm
t_id = cxfm.Identity()
# cxf.identity is the same instance: Identity()
Translation (Displacement)#
import coordinax.frames as cxf
import coordinax.charts as cxc
import coordinax.transforms as cxfm
import unxt as u
# Translate by (1, 0, 0)
t_translate = cxfm.Translate({"x": 1, "y": 0, "z": 0}, chart=cxc.cart3d)
This is in the Euclidean group (preserves distances and angles).
Rotation#
import jax.numpy as jnp
import math
# Rotate around z-axis by π/2
theta = math.pi / 2
R = jnp.array(
[
[math.cos(theta), -math.sin(theta), 0.0],
[math.sin(theta), math.cos(theta), 0.0],
[0.0, 0.0, 1.0],
]
)
t_rotate = cxfm.Rotate(R)
This is in the Special Orthogonal group (proper rotations, orientation-preserving).
Reflection#
import coordinax.transforms as cxfm
import unxt as u
# Reflect across the yz-plane
t_reflect = cxfm.Reflect.from_normal([1.0, 0.0, 0.0])
q = u.Q([1.0, 2.0, 3.0], "km")
cxfm.act(t_reflect, None, q)
# Q([-1., 2., 3.], 'km')
This is in the Orthogonal group (distance-preserving, orientation-reversing).
Composition (Chaining Transforms)#
Use the | operator to compose:
import coordinax.charts as cxc
import coordinax.transforms as cxfm
import jax.numpy as jnp
import math
t1 = cxfm.Translate({"x": 1, "y": 0, "z": 0}, chart=cxc.cart3d)
theta = math.pi / 2
R = jnp.array(
[
[math.cos(theta), -math.sin(theta), 0.0],
[math.sin(theta), math.cos(theta), 0.0],
[0.0, 0.0, 1.0],
]
)
t2 = cxfm.Rotate(R)
# Compose: apply t1 first, then t2
composed = t2 | t1
Evaluation order (right-to-left):
Inversion (Reversing)#
# Inverse transforms are not yet available via cxf.inverse().
# Construct the reverse manually:
t_original = cxfm.Translate({"x": 1, "y": 0, "z": 0}, chart=cxc.cart3d)
t_inverse = cxfm.Translate({"x": -1, "y": 0, "z": 0}, chart=cxc.cart3d)
# These cancel out:
cancelled = t_inverse | t_original # Equivalent to identity
Simplification (Optimization)#
import coordinax.frames as cxf
import coordinax.charts as cxc
import coordinax.transforms as cxfm
import jax.numpy as jnp
import math
# Build a complex composition
t1 = cxfm.Translate({"x": 1, "y": 0, "z": 0}, chart=cxc.cart3d)
theta = math.pi / 2
R = jnp.array(
[
[math.cos(theta), -math.sin(theta), 0.0],
[math.sin(theta), math.cos(theta), 0.0],
[0.0, 0.0, 1.0],
]
)
t2 = cxfm.Rotate(R)
t3 = cxfm.Translate({"x": -1, "y": 0, "z": 0}, chart=cxc.cart3d)
complex_transform = t3 | t2 | t1
# Simplify: reduces nesting and cancels identities
simplified = cxfm.simplify(complex_transform)
# Both are mathematically equivalent, but simplified is more efficient
Working With Reference Frames#
A reference frame defines a spatial observer. Built-in example frames:
import coordinax.frames as cxf
alice = cxf.Alice() # stationary frame at origin
alex = cxf.Alex() # stationary frame offset from Alice
no_frame = cxf.NoFrame() # identity (null) frame
Frame Transitions#
Relate two frames via a transformation:
# Get the transformation FROM alice TO alex
transform_alice_to_alex = cxf.frame_transition(alice, alex)
# This is a transform operator that can be applied
import coordinax.main as cx
v_in_alice = cx.Point.from_([1, 2, 3], "m", cxc.cart3d)
v_in_alex = cxfm.act(transform_alice_to_alex, None, v_in_alice)
Custom Frames#
Define domain-specific reference frames by subclassing:
# Custom frame sketch (illustrative — fill in frame_transition logic):
#
# import coordinax.frames as cxf
# from coordinax.frames import AbstractReferenceFrame, AbstractTransform
#
# class RotatingFrame(AbstractReferenceFrame):
# """A frame rotating at constant angular velocity."""
#
# omega: float = 1.0 # rad/s
#
# def frame_transition(self,
# to_frame: AbstractReferenceFrame) -> AbstractTransform:
# """Compute transform to another frame."""
# ...
For astronomical applications, coordinax.astro provides pre-built frames:
# (If coordinax.astro is installed)
import coordinax.astro as cxastro
icrs = cxastro.ICRS() # Celestial reference frame
galactocentric = cxastro.Galactocentric()
# Transition between them
xform = cxf.frame_transition(icrs, galactocentric)
Coordinate Objects In Frame Workflows#
A Coordinate is a Vector attached to a reference frame. This is often the most direct way to express “this data is measured by this observer”:
import coordinax.main as cx
import coordinax.frames as cxf
coord = cx.Point.from_([1, 2, 3], "kpc", cxf.alice)
print(coord.frame) # Alice()
print(coord.chart) # Cart3D(M=Rn(3))
Frame Transformations On Coordinates#
Use to_frame() when you want to apply the operator associated with changing observers. In the active convention, this moves the represented point data into the target frame.
import coordinax.main as cx
import coordinax.frames as cxf
coord_alice = cx.Point.from_([1, 2, 3], "m", cxf.alice)
coord_alex = coord_alice.to_frame(cxf.alex)
print(coord_alice.frame) # Alice()
print(coord_alex.frame) # Alex()
You can also compute and apply the frame transition operator explicitly:
op = cxf.frame_transition(cxf.alice, cxf.alex)
coord_alex_2 = op(coord_alice)
Frame Change vs Chart Change#
to_frame() changes reference frame. cconvert() changes chart. These answer different questions and can be chained.
import coordinax.charts as cxc
coord_cart = cx.Point.from_([1, 2, 3], "m", cxf.alice)
coord_sph = coord_cart.cconvert(cxc.sph3d)
print(coord_cart.frame, coord_cart.chart) # Alice(), Cart3D(M=Rn(3))
print(coord_sph.frame, coord_sph.chart) # Alice(), Spherical3D()
For a full frame-oriented workflow (constructor patterns, frame+chart pipelines, and JAX batching), see Working With Vectors.
Practical Workflow: Rotating Frame#
Here’s a complete example: observe a rotating planet from an inertial frame.
import coordinax.frames as cxf
import coordinax.main as cx
import coordinax.charts as cxc
import coordinax.transforms as cxfm
import jax.numpy as jnp
import math
# Define frames
inertial = cxf.alice # Fixed reference frame
# Define rotation: planet rotates 0.1 rad around z-axis
# Build a rotation matrix explicitly
theta = 0.1 # radians
R = jnp.array(
[
[math.cos(theta), -math.sin(theta), 0.0],
[math.sin(theta), math.cos(theta), 0.0],
[0.0, 0.0, 1.0],
]
)
rotating_frame = cxf.TransformedReferenceFrame(inertial, cxfm.Rotate(R))
# Observe a point in the inertial frame
position_inertial = cx.Point.from_([1, 0, 0], "m", cxc.cart3d)
# Get the transition operator
xform = cxf.frame_transition(inertial, rotating_frame)
# Apply to get position in rotating frame (act takes op, tau, x)
position_rotating = cxfm.act(xform, None, position_inertial)
print(position_rotating.data) # Different coordinates, same point
JAX Integration Patterns#
# JAX integration sketch (illustrative):
#
# import jax
# import coordinax.frames as cxf
# import coordinax.main as cx
#
# @jax.jit
# def batch_transform_points(frame1, frame2, vectors):
# transform = cxf.frame_transition(frame1, frame2)
# return jax.vmap(lambda v: cxfm.act(transform, None, v))(vectors)
#
# Result is JIT-compiled and efficient
Common Pitfalls#
1. Composition Order#
# RIGHT: apply t1 first, then t2
result = t2 | t1
# WRONG: this applies t2 first
result = t1 | t2 # Different!
2. Active vs Passive#
Coordinax uses active transformations:
# Active: apply the frame-transition operator to the point data
# v_rotated = cxfm.act(rotate_transform, None, v) # RIGHT (act takes op, tau, x)
# Passive language is only for comparison with other conventions
# In coordinax, think: "apply the transform" rather than "just relabel coordinates"
3. Forgetting to Simplify#
import coordinax.frames as cxf
import coordinax.charts as cxc
import coordinax.transforms as cxfm
import jax.numpy as jnp
import math
# Redefine for this example
_theta = math.pi / 2
_R = jnp.array(
[
[math.cos(_theta), -math.sin(_theta), 0.0],
[math.sin(_theta), math.cos(_theta), 0.0],
[0.0, 0.0, 1.0],
]
)
_t1 = cxfm.Translate({"x": 1, "y": 0, "z": 0}, chart=cxc.cart3d)
_t2 = cxfm.Rotate(_R)
_t3 = cxfm.Translate({"x": -1, "y": 0, "z": 0}, chart=cxc.cart3d)
_t4 = cxfm.Translate({"x": 0, "y": 1, "z": 0}, chart=cxc.cart3d)
_t5 = cxfm.Translate({"x": 0, "y": -1, "z": 0}, chart=cxc.cart3d)
_t6 = cxfm.Translate({"x": 0, "y": 0, "z": 1}, chart=cxc.cart3d)
# Complex nested transforms are slow
complex_t = _t6 | _t5 | _t4 | _t3 | _t2 | _t1
# Simplify for performance
simple_t = cxfm.simplify(complex_t)
4. Group Constraints#
Not all transforms are valid on all manifolds. Let the type system help:
# A manifold might require Euclidean transforms only
# Attempting affine (shearing) might raise TypeError
Summary: Workflow Pattern#
Define frames and their relationships
Compute transformations via
frame_transition()Apply to vectors via
act()Compose multiple transformations with
|Simplify to optimize
Vectorize with
vmapfor batchingDifferentiate with
gradif needed
This workflow enables clean, composable code for multi-frame simulations.