Coordinate Systems

This example demonstrates coordinate conversions, rotations, and projections.

Overview

Coordinate transformations are fundamental for tracking:

  • Cartesian-Spherical: Range, azimuth, elevation conversions

  • Geodetic-ECEF: Earth-fixed coordinates

  • Local frames: ENU, NED transformations

  • Map projections: UTM, Mercator, Lambert

Coordinate Systems

Cartesian (x, y, z)
  • Standard 3D coordinates

  • Used for state estimation

Spherical (range, azimuth, elevation)
  • Sensor-centric measurements

  • Radar and lidar output

Geodetic (latitude, longitude, altitude)
  • Geographic coordinates

  • Navigation reference

ECEF (Earth-Centered, Earth-Fixed)
  • Rotates with Earth

  • GPS coordinates

ENU/NED (Local Tangent Plane)
  • East-North-Up or North-East-Down

  • Local navigation frame

Rotation Representations

  • Rotation matrices: 3x3 orthogonal matrices

  • Quaternions: 4D unit vectors, singularity-free

  • Euler angles: Roll, pitch, yaw

  • Axis-angle: Rotation axis and angle

Code Highlights

The example demonstrates:

  • cart2sphere() and sphere2cart()

  • geodetic2ecef() and ecef2geodetic()

  • ecef2enu() and enu2ecef()

  • euler2rotmat() and rotmat2euler(), with rotmat2quat() and quat2rotmat() for quaternion conversions

  • slerp() for quaternion interpolation

Source Code

  1"""
  2Coordinate Systems Example.
  3
  4This example demonstrates:
  51. Cartesian to spherical coordinate conversions
  62. Geodetic (WGS84) to ECEF transformations
  73. Local tangent plane (ENU/NED) coordinates
  84. Rotation matrices and quaternions
  95. Jacobian-based covariance transformations
 10
 11Run with: python examples/coordinate_systems.py
 12"""
 13
 14import sys
 15from pathlib import Path
 16
 17sys.path.insert(0, str(Path(__file__).parent.parent))
 18
 19import os
 20
 21import numpy as np  # noqa: E402
 22import plotly.graph_objects as go  # noqa: E402
 23from plotly.subplots import make_subplots  # noqa: E402
 24
 25from pytcl.coordinate_systems import (  # noqa: E402
 26    cart2sphere,
 27    cross_covariance_transform,
 28    ecef2enu,
 29    ecef2geodetic,
 30    ecef2ned,
 31    enu2ecef,
 32    euler2rotmat,
 33    geodetic2ecef,
 34    quat2rotmat,
 35    quat_multiply,
 36    rotmat2euler,
 37    rotmat2quat,
 38    rotz,
 39    slerp,
 40    sphere2cart,
 41    spherical_jacobian_inv,
 42)
 43
 44SHOW_PLOTS = os.environ.get("PYTCL_SHOW_PLOTS", "1") != "0"
 45OUTPUT_DIR = Path(__file__).resolve().parent / "output"
 46
 47
 48def spherical_conversions_demo() -> None:
 49    """Demonstrate spherical coordinate conversions."""
 50    print("=" * 60)
 51    print("1. SPHERICAL COORDINATE CONVERSIONS")
 52    print("=" * 60)
 53
 54    # Define a point in Cartesian coordinates
 55    cart_point = np.array([1000.0, 2000.0, 500.0])  # meters
 56    print(
 57        f"\nCartesian point: x={cart_point[0]:.1f}, y={cart_point[1]:.1f}, "
 58        f"z={cart_point[2]:.1f} m"
 59    )
 60
 61    # Convert to spherical (range, azimuth, elevation)
 62    r, az, el = cart2sphere(cart_point)
 63    print("\nSpherical coordinates:")
 64    print(f"  Range:     {r:.2f} m")
 65    print(f"  Azimuth:   {np.degrees(az):.2f} deg")
 66    print(f"  Elevation: {np.degrees(el):.2f} deg")
 67
 68    # Convert back to Cartesian
 69    cart_recovered = sphere2cart(r, az, el)
 70    print(
 71        f"\nRecovered Cartesian: x={cart_recovered[0]:.1f}, "
 72        f"y={cart_recovered[1]:.1f}, z={cart_recovered[2]:.1f} m"
 73    )
 74
 75    # Verify roundtrip
 76    error = np.linalg.norm(cart_point - cart_recovered)
 77    print(f"Roundtrip error: {error:.2e} m")
 78
 79
 80def geodetic_conversions_demo() -> None:
 81    """Demonstrate geodetic coordinate conversions."""
 82    print("\n" + "=" * 60)
 83    print("2. GEODETIC (WGS84) COORDINATE CONVERSIONS")
 84    print("=" * 60)
 85
 86    # Define a geodetic point (Washington DC area)
 87    lat = np.radians(38.9072)  # Latitude in radians
 88    lon = np.radians(-77.0369)  # Longitude in radians
 89    alt = 100.0  # Altitude in meters
 90
 91    print("\nGeodetic coordinates:")
 92    print(f"  Latitude:  {np.degrees(lat):.4f} deg")
 93    print(f"  Longitude: {np.degrees(lon):.4f} deg")
 94    print(f"  Altitude:  {alt:.1f} m")
 95
 96    # Convert to ECEF
 97    ecef = geodetic2ecef(lat, lon, alt)
 98    print("\nECEF coordinates:")
 99    print(f"  X: {ecef[0] / 1000:.3f} km")
100    print(f"  Y: {ecef[1] / 1000:.3f} km")
101    print(f"  Z: {ecef[2] / 1000:.3f} km")
102
103    # Convert back to geodetic
104    lat_r, lon_r, alt_r = ecef2geodetic(ecef)
105    print("\nRecovered geodetic:")
106    print(f"  Latitude:  {np.degrees(lat_r):.4f} deg")
107    print(f"  Longitude: {np.degrees(lon_r):.4f} deg")
108    print(f"  Altitude:  {alt_r:.1f} m")
109
110
111def local_tangent_plane_demo() -> None:
112    """Demonstrate local tangent plane (ENU/NED) conversions."""
113    print("\n" + "=" * 60)
114    print("3. LOCAL TANGENT PLANE (ENU/NED) CONVERSIONS")
115    print("=" * 60)
116
117    # Reference point (origin of local frame)
118    ref_lat = np.radians(38.9072)
119    ref_lon = np.radians(-77.0369)
120
121    # Compute ECEF reference point for altitude = 0
122    ref_ecef = geodetic2ecef(ref_lat, ref_lon, 0.0)
123
124    # Target point 1 km East, 2 km North, 100 m Up from reference
125    enu_offset = np.array([1000.0, 2000.0, 100.0])  # East, North, Up
126    print("\nENU offset from reference:")
127    print(f"  East:  {enu_offset[0]:.1f} m")
128    print(f"  North: {enu_offset[1]:.1f} m")
129    print(f"  Up:    {enu_offset[2]:.1f} m")
130
131    # Convert ENU to ECEF
132    target_ecef = enu2ecef(enu_offset, ref_lat, ref_lon, ref_ecef)
133    print("\nTarget ECEF coordinates:")
134    print(f"  X: {target_ecef[0] / 1000:.3f} km")
135    print(f"  Y: {target_ecef[1] / 1000:.3f} km")
136    print(f"  Z: {target_ecef[2] / 1000:.3f} km")
137
138    # Convert ECEF back to ENU
139    enu_recovered = ecef2enu(target_ecef, ref_lat, ref_lon, ref_ecef)
140    print("\nRecovered ENU:")
141    print(f"  East:  {enu_recovered[0]:.1f} m")
142    print(f"  North: {enu_recovered[1]:.1f} m")
143    print(f"  Up:    {enu_recovered[2]:.1f} m")
144
145    # Also show NED (North, East, Down) - common in aviation
146    ned = ecef2ned(target_ecef, ref_lat, ref_lon, ref_ecef)
147    print("\nNED coordinates (aviation convention):")
148    print(f"  North: {ned[0]:.1f} m")
149    print(f"  East:  {ned[1]:.1f} m")
150    print(f"  Down:  {ned[2]:.1f} m")
151
152
153def rotation_demo() -> None:
154    """Demonstrate rotation matrices and Euler angles."""
155    print("\n" + "=" * 60)
156    print("4. ROTATION MATRICES AND EULER ANGLES")
157    print("=" * 60)
158
159    # Create individual rotation matrices
160    roll = np.radians(10.0)  # Roll about X
161    pitch = np.radians(20.0)  # Pitch about Y
162    yaw = np.radians(30.0)  # Yaw about Z
163
164    print("\nEuler angles (ZYX convention):")
165    print(f"  Roll (X):  {np.degrees(roll):.1f} deg")
166    print(f"  Pitch (Y): {np.degrees(pitch):.1f} deg")
167    print(f"  Yaw (Z):   {np.degrees(yaw):.1f} deg")
168
169    # Combined rotation (ZYX order: yaw, then pitch, then roll)
170    # euler2rotmat expects [angle1, angle2, angle3] for the sequence
171    R = euler2rotmat([yaw, pitch, roll], sequence="ZYX")
172    print("\nRotation matrix (3x3):")
173    print(R)
174
175    # Verify it's a proper rotation (det = 1, orthogonal)
176    print(f"\nDeterminant: {np.linalg.det(R):.6f} (should be 1)")
177    print(f"R @ R.T = I check: {np.allclose(R @ R.T, np.eye(3))}")
178
179    # Extract Euler angles back
180    angles_recovered = rotmat2euler(R, sequence="ZYX")
181    yaw_r, pitch_r, roll_r = angles_recovered
182    print("\nRecovered Euler angles:")
183    print(f"  Roll:  {np.degrees(roll_r):.1f} deg")
184    print(f"  Pitch: {np.degrees(pitch_r):.1f} deg")
185    print(f"  Yaw:   {np.degrees(yaw_r):.1f} deg")
186
187
188def quaternion_demo() -> None:
189    """Demonstrate quaternion operations and interpolation."""
190    print("\n" + "=" * 60)
191    print("5. QUATERNIONS AND SLERP INTERPOLATION")
192    print("=" * 60)
193
194    # Create a rotation matrix (ZYX = yaw, pitch, roll order)
195    roll, pitch, yaw = np.radians(15.0), np.radians(25.0), np.radians(45.0)
196    R = euler2rotmat([yaw, pitch, roll], sequence="ZYX")
197
198    # Convert to quaternion [w, x, y, z]
199    q = rotmat2quat(R)
200    print("\nQuaternion [w, x, y, z]:")
201    print(f"  q = [{q[0]:.4f}, {q[1]:.4f}, {q[2]:.4f}, {q[3]:.4f}]")
202    print(f"  Norm: {np.linalg.norm(q):.6f} (should be 1)")
203
204    # Convert back to rotation matrix
205    R_recovered = quat2rotmat(q)
206    print(f"\nRotation matrix roundtrip check: {np.allclose(R, R_recovered)}")
207
208    # Quaternion multiplication (composing rotations)
209    q2 = rotmat2quat(rotz(np.radians(90.0)))  # 90 deg yaw rotation
210    q_composed = quat_multiply(q, q2)
211    print("\nComposed quaternion (original + 90 deg yaw):")
212    print(
213        f"  q = [{q_composed[0]:.4f}, {q_composed[1]:.4f}, "
214        f"{q_composed[2]:.4f}, {q_composed[3]:.4f}]"
215    )
216
217    # SLERP interpolation between two orientations
218    print("\nSLERP interpolation (identity to 90 deg yaw):")
219    q_start = np.array([1.0, 0.0, 0.0, 0.0])  # Identity
220    q_end = rotmat2quat(rotz(np.radians(90.0)))
221
222    for t in [0.0, 0.25, 0.5, 0.75, 1.0]:
223        q_interp = slerp(q_start, q_end, t)
224        R_interp = quat2rotmat(q_interp)
225        # rotmat2euler returns [angle1, angle2, angle3] for ZYX = [yaw, pitch, roll]
226        angles = rotmat2euler(R_interp, sequence="ZYX")
227        yaw_interp = angles[0]
228        print(f"  t={t:.2f}: yaw = {np.degrees(yaw_interp):.1f} deg")
229
230
231def jacobian_covariance_demo() -> None:
232    """Demonstrate Jacobian-based covariance transformation."""
233    print("\n" + "=" * 60)
234    print("6. JACOBIAN-BASED COVARIANCE TRANSFORMATION")
235    print("=" * 60)
236
237    # Sensor measures in spherical coordinates with uncertainty
238    r = 5000.0  # Range in meters
239    az = np.radians(45.0)  # Azimuth
240    el = np.radians(10.0)  # Elevation
241
242    # Measurement covariance in spherical coordinates
243    sigma_r = 10.0  # Range std (meters)
244    sigma_az = np.radians(0.5)  # Azimuth std (radians)
245    sigma_el = np.radians(0.5)  # Elevation std (radians)
246
247    P_spherical = np.diag([sigma_r**2, sigma_az**2, sigma_el**2])
248
249    print("\nSpherical measurement:")
250    print(f"  Range:     {r:.1f} +/- {sigma_r:.1f} m")
251    print(f"  Azimuth:   {np.degrees(az):.1f} +/- {np.degrees(sigma_az):.2f} deg")
252    print(f"  Elevation: {np.degrees(el):.1f} +/- {np.degrees(sigma_el):.2f} deg")
253
254    # Get Jacobian of Cartesian w.r.t. spherical at this point
255    # spherical_jacobian_inv: d[x,y,z] = J @ d[r,az,el]
256    J = spherical_jacobian_inv(r, az, el)
257
258    print("\nJacobian (dCartesian/dSpherical):")
259    print(J)
260
261    # Transform covariance to Cartesian
262    P_cartesian = cross_covariance_transform(P_spherical, J)
263
264    print("\nCartesian covariance matrix:")
265    print(P_cartesian)
266
267    # Extract position uncertainties
268    sigma_x = np.sqrt(P_cartesian[0, 0])
269    sigma_y = np.sqrt(P_cartesian[1, 1])
270    sigma_z = np.sqrt(P_cartesian[2, 2])
271
272    # Convert mean to Cartesian
273    cart = sphere2cart(r, az, el)
274    print("\nCartesian position with uncertainties:")
275    print(f"  x = {cart[0]:.1f} +/- {sigma_x:.1f} m")
276    print(f"  y = {cart[1]:.1f} +/- {sigma_y:.1f} m")
277    print(f"  z = {cart[2]:.1f} +/- {sigma_z:.1f} m")
278
279
280def main() -> None:
281    """Run all coordinate system demonstrations."""
282    print("\nCoordinate Systems Examples")
283    print("=" * 60)
284    print("Demonstrating pytcl coordinate transformation capabilities")
285
286    spherical_conversions_demo()
287    geodetic_conversions_demo()
288    local_tangent_plane_demo()
289    rotation_demo()
290    quaternion_demo()
291    jacobian_covariance_demo()
292
293    # Visualization
294    visualize_coordinate_transforms()
295
296    print("\n" + "=" * 60)
297    print("Done!")
298    print("=" * 60)
299
300
301def visualize_coordinate_transforms() -> None:
302    """Visualize coordinate system transformations."""
303    print("\nGenerating coordinate transform visualization...")
304
305    # Create a grid of points in spherical coordinates
306    r = 1000.0
307    azimuths = np.linspace(0, 360, 9)
308    elevations = np.linspace(-90, 90, 5)
309
310    points_cart = []
311    for az in azimuths:
312        for el in elevations:
313            cart = sphere2cart(r, np.radians(az), np.radians(el))
314            points_cart.append(cart)
315
316    points_cart = np.array(points_cart)
317
318    # Create 3D scatter plot
319    fig = go.Figure()
320
321    fig.add_trace(
322        go.Scatter3d(
323            x=points_cart[:, 0],
324            y=points_cart[:, 1],
325            z=points_cart[:, 2],
326            mode="markers",
327            marker=dict(size=5, color="blue", opacity=0.8),
328            name="Spherical Coords (Cartesian)",
329        )
330    )
331
332    fig.update_layout(
333        title="Spherical to Cartesian Coordinate Transformation",
334        scene=dict(
335            xaxis_title="X (m)",
336            yaxis_title="Y (m)",
337            zaxis_title="Z (m)",
338            camera=dict(eye=dict(x=1.5, y=1.5, z=1.5)),
339        ),
340        height=600,
341        width=800,
342    )
343
344    if SHOW_PLOTS:
345        fig.show()
346    else:
347        OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
348        fig.write_html(
349            str(OUTPUT_DIR / "coordinate_systems.html"),
350            include_plotlyjs="cdn",
351            div_id="coordinate_systems",
352        )
353
354
355if __name__ == "__main__":
356    main()

Running the Example

python examples/coordinate_systems.py

See Also