Coordinate Systems

The library provides comprehensive coordinate system conversions and rotation representations. All angles are in radians.

Coordinate Conversions

Cartesian and Spherical

cart2sphere returns a (range, azimuth, elevation) tuple. The system_type keyword selects the convention: 'standard' (physics convention, polar angle from +z) or 'az-el' (tracking convention, elevation from the xy-plane):

import numpy as np

from pytcl.coordinate_systems import cart2sphere, sphere2cart

# Cartesian to spherical (range, azimuth, elevation)
point = np.array([100.0, 200.0, 50.0])
r, az, el = cart2sphere(point, system_type='az-el')
print(f"range={r:.3f}, azimuth={az:.6f}, elevation={el:.6f}")

# Spherical to Cartesian
cart = sphere2cart(r, az, el, system_type='az-el')
print(f"cartesian: {cart.round(6)}")

Output:

range=229.129, azimuth=1.107149, elevation=0.219988
cartesian: [100. 200.  50.]

Geodetic and ECEF

Convert between geodetic coordinates (latitude, longitude, altitude) and Earth-Centered, Earth-Fixed (ECEF) Cartesian coordinates. geodetic2ecef returns an ECEF array; ecef2geodetic takes an ECEF array and returns a (lat, lon, alt) tuple:

from pytcl.coordinate_systems import geodetic2ecef, ecef2geodetic

# Geodetic to ECEF (angles in radians)
lat = np.deg2rad(40.0)   # 40 degrees North
lon = np.deg2rad(-75.0)  # 75 degrees West
alt = 100.0              # meters

ecef = geodetic2ecef(lat, lon, alt)
print(f"ECEF: {ecef.round(1)}")

# ECEF to geodetic
lat2, lon2, alt2 = ecef2geodetic(ecef)
print(f"lat={np.rad2deg(lat2):.6f} deg, lon={np.rad2deg(lon2):.6f} deg, "
      f"alt={alt2:.3f} m")

Output:

ECEF: [ 1266345.7 -4726066.6  4078049.9]
lat=40.000000 deg, lon=-75.000000 deg, alt=100.000 m

ENU and NED

Local tangent plane coordinates. ecef2enu takes the ECEF point, the reference latitude/longitude, and the reference point’s ECEF position:

from pytcl.coordinate_systems import ecef2enu, enu2ecef

# Convert ECEF to local East-North-Up about an origin
origin_lat, origin_lon = np.deg2rad(40.0), np.deg2rad(-75.0)
origin_ecef = geodetic2ecef(origin_lat, origin_lon, 0.0)

enu = ecef2enu(ecef, origin_lat, origin_lon, ecef_ref=origin_ecef)
print(f"ENU: {enu.round(3)}")

Output:

ENU: [  0.   0. 100.]

ned2ecef, ecef2ned, enu2ned, ned2enu, and geodetic2enu cover the remaining local-frame conversions.

Rotation Representations

The library supports multiple rotation representations and conversions between them.

Rotation Matrices

Elementary rotations about principal axes:

from pytcl.coordinate_systems import rotx, roty, rotz

# Rotation about x-axis by 30 degrees
Rx = rotx(np.deg2rad(30))

# Combined rotation
roll, pitch, yaw = 0.1, 0.2, 0.3
R = rotz(yaw) @ roty(pitch) @ rotx(roll)

Quaternions

Unit quaternions for 3D rotations. Quaternions are built from other representations (rotmat2quat, euler2quat) and converted back with quat2rotmat / quat2euler:

from pytcl.coordinate_systems import (
    axisangle2rotmat,
    rotmat2quat,
    quat2rotmat,
    quat_multiply,
    quat_rotate,
)

# Create quaternion from axis-angle (via a rotation matrix)
axis = np.array([0.0, 0.0, 1.0])  # z-axis
angle = np.pi / 4                 # 45 degrees
q = rotmat2quat(axisangle2rotmat(axis, angle))

# Convert to rotation matrix
R = quat2rotmat(q)

# Rotate a vector
v = np.array([1.0, 0.0, 0.0])
v_rotated = quat_rotate(q, v)
print(f"Rotated vector: {v_rotated.round(6)}")

Output:

Rotated vector: [0.707107 0.707107 0.      ]

quat_conjugate, quat_inverse, and slerp (spherical linear interpolation) are also available.

Euler Angles

euler2rotmat takes a sequence of angles and a rotation order (default "ZYX", i.e. yaw-pitch-roll); rotmat2euler returns the angles as an array in the same order:

from pytcl.coordinate_systems import euler2rotmat, rotmat2euler

# Euler angles (yaw, pitch, roll) to rotation matrix, ZYX order
R = euler2rotmat([yaw, pitch, roll], sequence="ZYX")

# Rotation matrix to Euler angles
angles = rotmat2euler(R, sequence="ZYX")
print(f"yaw={angles[0]:.3f}, pitch={angles[1]:.3f}, roll={angles[2]:.3f}")

Output:

yaw=0.300, pitch=0.200, roll=0.100

Axis-Angle

from pytcl.coordinate_systems import axisangle2rotmat, rotmat2axisangle

axis = np.array([0.0, 0.0, 1.0])
angle = np.pi / 4
R = axisangle2rotmat(axis, angle)

axis_out, angle_out = rotmat2axisangle(R)
print(f"axis={axis_out.round(6)}, angle={angle_out:.6f}")

Output:

axis=[0. 0. 1.], angle=0.785398

Coordinate Jacobians

Jacobians for coordinate transformations are essential for filter design:

from pytcl.coordinate_systems import (
    spherical_jacobian,
    spherical_jacobian_inv,
    geodetic_jacobian,
)

# Jacobian of the Cartesian-to-spherical transformation at a point
J = spherical_jacobian([100.0, 200.0, 50.0], system_type="standard")
print(f"Jacobian shape: {J.shape}")

# Jacobian of the geodetic-to-ECEF transformation
Jg = geodetic_jacobian(lat, lon, alt)

Output:

Jacobian shape: (3, 3)

spherical_jacobian can be used as the measurement Jacobian H in an EKF with spherical (range/angle) measurements. polar_jacobian, enu_jacobian, ned_jacobian, ruv_jacobian, and numerical_jacobian cover other measurement models.

WGS84 Ellipsoid

The library uses the WGS84 ellipsoid for geodetic calculations:

from pytcl.core import WGS84

print(f"Semi-major axis: {WGS84.a} m")
print(f"Flattening: {WGS84.f}")
print(f"Eccentricity: {WGS84.e}")

Output:

Semi-major axis: 6378137.0 m
Flattening: 0.0033528106647474805
Eccentricity: 0.08181919084262149