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()andsphere2cart()geodetic2ecef()andecef2geodetic()ecef2enu()andenu2ecef()euler2rotmat()androtmat2euler(), withrotmat2quat()andquat2rotmat()for quaternion conversionsslerp()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
Coordinate Visualization - 3D visualizations
Navigation and Geodesy - Geodetic calculations
3D Target Tracking - Using coordinates in tracking