Coordinate Visualization
This example provides interactive 3D visualizations of coordinate transforms.
Overview
Visualizing coordinate transformations helps understand:
Rotation effects: How rotations change frame orientation
Projection distortions: Map projection artifacts
Frame relationships: ECEF, ENU, NED orientations
Interpolation paths: Quaternion vs Euler interpolation
Key Concepts
Frame axes: Visualizing coordinate system orientation
Transformation chains: Sequential rotations
Geodetic surface: Earth ellipsoid visualization
Great circles: Shortest paths on sphere
Rotation Axes: Visualizing how rotations affect coordinate frame orientation.
Euler Sequences: ZYX, ZXZ, and other Euler angle conventions produce different rotation paths.
SLERP Interpolation: Spherical linear interpolation provides smooth rotation paths between orientations.
Spherical Coordinates: Converting between Cartesian and spherical coordinate systems.
Code Highlights
The example demonstrates:
3D plotting of coordinate frames
Animated rotation sequences
Interactive frame selection
Projection comparison views
Source Code
1"""
2Interactive 3D Coordinate System Visualization.
3
4This example demonstrates:
51. 3D visualization of coordinate transformations
62. Interactive rotation matrix visualization
73. Quaternion SLERP animation
84. Geodetic to ECEF coordinate plotting
9
10Run with: python examples/coordinate_visualization.py
11"""
12
13import sys
14from pathlib import Path
15
16sys.path.insert(0, str(Path(__file__).parent.parent))
17
18# Output directory for generated plots
19OUTPUT_DIR = Path(__file__).parent.parent / "docs" / "_static" / "images" / "examples"
20OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
21
22import numpy as np # noqa: E402
23import plotly.graph_objects as go # noqa: E402
24from plotly.subplots import make_subplots # noqa: E402
25
26from pytcl.coordinate_systems import ( # noqa: E402
27 euler2rotmat,
28 quat2rotmat,
29 rotx,
30 roty,
31 rotz,
32 slerp,
33 sphere2cart,
34)
35from pytcl.navigation import geodetic_to_ecef # noqa: E402
36
37
38def plot_rotation_axes() -> go.Figure:
39 """
40 Visualize how rotation matrices transform coordinate axes.
41
42 Creates an interactive 3D plot showing:
43 - Original coordinate axes (XYZ)
44 - Rotated axes after applying rotx, roty, rotz
45 """
46 fig = make_subplots(
47 rows=1,
48 cols=3,
49 specs=[[{"type": "scene"}, {"type": "scene"}, {"type": "scene"}]],
50 subplot_titles=[
51 "Rotation about X (45°)",
52 "Rotation about Y (45°)",
53 "Rotation about Z (45°)",
54 ],
55 )
56
57 # Original axes
58 _origin = np.array([0, 0, 0]) # noqa: F841
59 x_axis = np.array([1, 0, 0])
60 y_axis = np.array([0, 1, 0])
61 z_axis = np.array([0, 0, 1])
62
63 def add_axes(fig, R, col, original_alpha=0.3):
64 """Add original and rotated axes to subplot."""
65 # Original axes (faded)
66 for axis, color, name in [
67 (x_axis, "red", "X"),
68 (y_axis, "green", "Y"),
69 (z_axis, "blue", "Z"),
70 ]:
71 fig.add_trace(
72 go.Scatter3d(
73 x=[0, axis[0]],
74 y=[0, axis[1]],
75 z=[0, axis[2]],
76 mode="lines",
77 line=dict(color=color, width=3),
78 opacity=original_alpha,
79 name=f"Original {name}",
80 showlegend=(col == 1),
81 ),
82 row=1,
83 col=col,
84 )
85
86 # Rotated axes
87 for axis, color, name in [
88 (x_axis, "red", "X'"),
89 (y_axis, "green", "Y'"),
90 (z_axis, "blue", "Z'"),
91 ]:
92 rotated = R @ axis
93 fig.add_trace(
94 go.Scatter3d(
95 x=[0, rotated[0]],
96 y=[0, rotated[1]],
97 z=[0, rotated[2]],
98 mode="lines+markers",
99 line=dict(color=color, width=5),
100 marker=dict(size=4),
101 name=f"Rotated {name}",
102 showlegend=(col == 1),
103 ),
104 row=1,
105 col=col,
106 )
107
108 # Apply rotations
109 angle = np.pi / 4 # 45 degrees
110 add_axes(fig, rotx(angle), col=1)
111 add_axes(fig, roty(angle), col=2)
112 add_axes(fig, rotz(angle), col=3)
113
114 # Update layout for each scene
115 for i in range(1, 4):
116 fig.update_scenes(
117 dict(
118 xaxis=dict(range=[-1.5, 1.5], title="X"),
119 yaxis=dict(range=[-1.5, 1.5], title="Y"),
120 zaxis=dict(range=[-1.5, 1.5], title="Z"),
121 aspectmode="cube",
122 ),
123 row=1,
124 col=i,
125 )
126
127 fig.update_layout(
128 title="Rotation Matrix Visualization (45° rotations)",
129 width=1400,
130 height=500,
131 )
132
133 return fig
134
135
136def plot_euler_rotation_sequence() -> go.Figure:
137 """
138 Visualize Euler angle rotation sequence (ZYX - aerospace convention).
139
140 Shows how yaw, pitch, roll are applied sequentially.
141 """
142 fig = make_subplots(
143 rows=1,
144 cols=4,
145 specs=[[{"type": "scene"}] * 4],
146 subplot_titles=[
147 "Initial",
148 "After Yaw (Z)",
149 "After Pitch (Y)",
150 "After Roll (X)",
151 ],
152 )
153
154 # Define Euler angles
155 yaw = np.radians(30)
156 pitch = np.radians(20)
157 roll = np.radians(15)
158
159 # Rotation matrices at each stage
160 R0 = np.eye(3)
161 R1 = rotz(yaw)
162 R2 = R1 @ roty(pitch)
163 R3 = R2 @ rotx(roll)
164
165 # Also compute full rotation for comparison
166 R_full = euler2rotmat([yaw, pitch, roll], "ZYX")
167 assert np.allclose(R3, R_full), "Rotation matrices should match"
168
169 # Create a simple airplane shape
170 def airplane_points():
171 """Generate points representing an airplane."""
172 # Fuselage
173 fuselage = np.array(
174 [
175 [1, 0, 0], # nose
176 [-1, 0, 0], # tail
177 ]
178 )
179 # Wings
180 wings = np.array(
181 [
182 [0, 0.8, 0], # left wing
183 [0, -0.8, 0], # right wing
184 ]
185 )
186 # Tail fin
187 tail = np.array(
188 [
189 [-0.8, 0, 0],
190 [-1, 0, 0.3],
191 ]
192 )
193 return fuselage, wings, tail
194
195 def add_airplane(fig, R, col):
196 """Add rotated airplane to subplot."""
197 fuselage, wings, tail = airplane_points()
198
199 # Rotate all points
200 fuselage_rot = (R @ fuselage.T).T
201 wings_rot = (R @ wings.T).T
202 tail_rot = (R @ tail.T).T
203
204 # Fuselage
205 fig.add_trace(
206 go.Scatter3d(
207 x=fuselage_rot[:, 0],
208 y=fuselage_rot[:, 1],
209 z=fuselage_rot[:, 2],
210 mode="lines",
211 line=dict(color="blue", width=8),
212 name="Fuselage",
213 showlegend=(col == 1),
214 ),
215 row=1,
216 col=col,
217 )
218
219 # Wings
220 fig.add_trace(
221 go.Scatter3d(
222 x=wings_rot[:, 0],
223 y=wings_rot[:, 1],
224 z=wings_rot[:, 2],
225 mode="lines",
226 line=dict(color="red", width=6),
227 name="Wings",
228 showlegend=(col == 1),
229 ),
230 row=1,
231 col=col,
232 )
233
234 # Tail
235 fig.add_trace(
236 go.Scatter3d(
237 x=tail_rot[:, 0],
238 y=tail_rot[:, 1],
239 z=tail_rot[:, 2],
240 mode="lines",
241 line=dict(color="green", width=4),
242 name="Tail",
243 showlegend=(col == 1),
244 ),
245 row=1,
246 col=col,
247 )
248
249 # Add airplane at each rotation stage
250 for i, R in enumerate([R0, R1, R2, R3], 1):
251 add_airplane(fig, R, col=i)
252
253 # Update layout
254 for i in range(1, 5):
255 fig.update_scenes(
256 dict(
257 xaxis=dict(range=[-1.5, 1.5], title="X"),
258 yaxis=dict(range=[-1.5, 1.5], title="Y"),
259 zaxis=dict(range=[-1.5, 1.5], title="Z"),
260 aspectmode="cube",
261 camera=dict(eye=dict(x=1.5, y=1.5, z=1.0)),
262 ),
263 row=1,
264 col=i,
265 )
266
267 fig.update_layout(
268 title=(
269 f"Euler Rotation Sequence (ZYX): Yaw={np.degrees(yaw):.0f}°, "
270 f"Pitch={np.degrees(pitch):.0f}°, Roll={np.degrees(roll):.0f}°"
271 ),
272 width=1600,
273 height=500,
274 )
275
276 return fig
277
278
279def plot_quaternion_slerp() -> go.Figure:
280 """
281 Visualize quaternion SLERP interpolation.
282
283 Shows smooth interpolation between two orientations.
284 """
285 fig = go.Figure()
286
287 # Start and end quaternions
288 # Identity (no rotation)
289 q1 = np.array([1, 0, 0, 0])
290
291 # 90 degree rotation about Z axis
292 angle = np.pi / 2
293 q2 = np.array([np.cos(angle / 2), 0, 0, np.sin(angle / 2)])
294
295 # Interpolation steps
296 n_steps = 20
297 t_values = np.linspace(0, 1, n_steps)
298
299 # Colors for interpolation
300 colors = [
301 f"rgb({int(255 * (1 - t))}, {int(100 + 155 * t)}, {int(255 * t)})"
302 for t in t_values
303 ]
304
305 # Reference vector to rotate
306 v = np.array([1, 0, 0])
307
308 # Add interpolated vectors
309 for i, t in enumerate(t_values):
310 q_interp = slerp(q1, q2, t)
311 R = quat2rotmat(q_interp)
312 v_rot = R @ v
313
314 fig.add_trace(
315 go.Scatter3d(
316 x=[0, v_rot[0]],
317 y=[0, v_rot[1]],
318 z=[0, v_rot[2]],
319 mode="lines+markers",
320 line=dict(color=colors[i], width=4),
321 marker=dict(size=4, color=colors[i]),
322 name=f"t={t:.2f}",
323 showlegend=(i % 5 == 0),
324 )
325 )
326
327 # Add arc showing the path
328 arc_points = []
329 for t in np.linspace(0, 1, 100):
330 q_interp = slerp(q1, q2, t)
331 R = quat2rotmat(q_interp)
332 arc_points.append(R @ v)
333 arc_points = np.array(arc_points)
334
335 fig.add_trace(
336 go.Scatter3d(
337 x=arc_points[:, 0],
338 y=arc_points[:, 1],
339 z=arc_points[:, 2],
340 mode="lines",
341 line=dict(color="gray", width=2, dash="dash"),
342 name="SLERP path",
343 )
344 )
345
346 fig.update_layout(
347 title="Quaternion SLERP Interpolation (0° to 90° about Z-axis)",
348 scene=dict(
349 xaxis=dict(range=[-1.5, 1.5], title="X"),
350 yaxis=dict(range=[-1.5, 1.5], title="Y"),
351 zaxis=dict(range=[-1.5, 1.5], title="Z"),
352 aspectmode="cube",
353 ),
354 width=800,
355 height=700,
356 )
357
358 return fig
359
360
361def plot_spherical_coordinates() -> go.Figure:
362 """
363 Visualize spherical coordinate system.
364
365 Shows the relationship between Cartesian and spherical coordinates.
366 """
367 fig = go.Figure()
368
369 # Generate a grid of points on a sphere
370 n_az = 24
371 n_el = 12
372 r = 1.0
373
374 # Azimuth lines (constant azimuth, varying elevation)
375 for az in np.linspace(0, 2 * np.pi, n_az, endpoint=False):
376 el_range = np.linspace(-np.pi / 2, np.pi / 2, 50)
377 points = np.array(
378 [sphere2cart(r, az, el, system_type="az-el") for el in el_range]
379 )
380 fig.add_trace(
381 go.Scatter3d(
382 x=points[:, 0],
383 y=points[:, 1],
384 z=points[:, 2],
385 mode="lines",
386 line=dict(color="lightblue", width=1),
387 showlegend=False,
388 )
389 )
390
391 # Elevation lines (constant elevation, varying azimuth)
392 for el in np.linspace(-np.pi / 2 + 0.1, np.pi / 2 - 0.1, n_el):
393 az_range = np.linspace(0, 2 * np.pi, 50)
394 points = np.array(
395 [sphere2cart(r, az, el, system_type="az-el") for az in az_range]
396 )
397 fig.add_trace(
398 go.Scatter3d(
399 x=points[:, 0],
400 y=points[:, 1],
401 z=points[:, 2],
402 mode="lines",
403 line=dict(color="lightgreen", width=1),
404 showlegend=False,
405 )
406 )
407
408 # Highlight a specific point
409 test_az = np.radians(45)
410 test_el = np.radians(30)
411 test_point = sphere2cart(r, test_az, test_el, system_type="az-el")
412
413 # Add the point
414 fig.add_trace(
415 go.Scatter3d(
416 x=[test_point[0]],
417 y=[test_point[1]],
418 z=[test_point[2]],
419 mode="markers",
420 marker=dict(size=10, color="red"),
421 name=f"Point (az={np.degrees(test_az):.0f}°, el={np.degrees(test_el):.0f}°)",
422 )
423 )
424
425 # Add lines showing the coordinates
426 # Line from origin to projection on xy-plane
427 proj_xy = np.array([test_point[0], test_point[1], 0])
428 fig.add_trace(
429 go.Scatter3d(
430 x=[0, proj_xy[0]],
431 y=[0, proj_xy[1]],
432 z=[0, 0],
433 mode="lines",
434 line=dict(color="blue", width=3, dash="dash"),
435 name="XY projection",
436 )
437 )
438
439 # Line from projection to point (showing elevation)
440 fig.add_trace(
441 go.Scatter3d(
442 x=[proj_xy[0], test_point[0]],
443 y=[proj_xy[1], test_point[1]],
444 z=[0, test_point[2]],
445 mode="lines",
446 line=dict(color="green", width=3, dash="dash"),
447 name="Elevation",
448 )
449 )
450
451 # Line from origin to point (range)
452 fig.add_trace(
453 go.Scatter3d(
454 x=[0, test_point[0]],
455 y=[0, test_point[1]],
456 z=[0, test_point[2]],
457 mode="lines",
458 line=dict(color="red", width=3),
459 name="Range vector",
460 )
461 )
462
463 # Add coordinate axes
464 axis_len = 1.3
465 for axis, color, name in [
466 ([axis_len, 0, 0], "red", "X"),
467 ([0, axis_len, 0], "green", "Y"),
468 ([0, 0, axis_len], "blue", "Z"),
469 ]:
470 fig.add_trace(
471 go.Scatter3d(
472 x=[0, axis[0]],
473 y=[0, axis[1]],
474 z=[0, axis[2]],
475 mode="lines+text",
476 line=dict(color=color, width=4),
477 text=["", name],
478 textposition="top center",
479 name=f"{name}-axis",
480 showlegend=False,
481 )
482 )
483
484 fig.update_layout(
485 title="Spherical Coordinate System (az-el convention)",
486 scene=dict(
487 xaxis=dict(range=[-1.5, 1.5], title="X"),
488 yaxis=dict(range=[-1.5, 1.5], title="Y"),
489 zaxis=dict(range=[-1.5, 1.5], title="Z"),
490 aspectmode="cube",
491 ),
492 width=900,
493 height=800,
494 )
495
496 return fig
497
498
499def plot_earth_coordinates() -> go.Figure:
500 """
501 Visualize geodetic coordinates on Earth.
502
503 Shows major cities and their ECEF coordinates.
504 """
505 fig = go.Figure()
506
507 # Create a sphere representing Earth
508 u = np.linspace(0, 2 * np.pi, 100)
509 v = np.linspace(0, np.pi, 50)
510 R_earth = 6371000 # meters (approximate)
511 scale = 1e-6 # Scale to make numbers manageable
512
513 x = R_earth * scale * np.outer(np.cos(u), np.sin(v))
514 y = R_earth * scale * np.outer(np.sin(u), np.sin(v))
515 z = R_earth * scale * np.outer(np.ones(np.size(u)), np.cos(v))
516
517 fig.add_trace(
518 go.Surface(
519 x=x,
520 y=y,
521 z=z,
522 colorscale=[[0, "lightblue"], [1, "lightblue"]],
523 opacity=0.6,
524 showscale=False,
525 name="Earth",
526 )
527 )
528
529 # Major cities with their geodetic coordinates
530 cities = {
531 "New York": (40.7128, -74.0060),
532 "London": (51.5074, -0.1278),
533 "Tokyo": (35.6762, 139.6503),
534 "Sydney": (-33.8688, 151.2093),
535 "São Paulo": (-23.5505, -46.6333),
536 "Cairo": (30.0444, 31.2357),
537 }
538
539 # Convert to ECEF and plot
540 city_x, city_y, city_z = [], [], []
541 city_names = []
542
543 for name, (lat_deg, lon_deg) in cities.items():
544 lat = np.radians(lat_deg)
545 lon = np.radians(lon_deg)
546 ecef = geodetic_to_ecef(lat, lon, 0)
547 city_x.append(ecef[0] * scale)
548 city_y.append(ecef[1] * scale)
549 city_z.append(ecef[2] * scale)
550 city_names.append(name)
551
552 fig.add_trace(
553 go.Scatter3d(
554 x=city_x,
555 y=city_y,
556 z=city_z,
557 mode="markers+text",
558 marker=dict(size=8, color="red"),
559 text=city_names,
560 textposition="top center",
561 name="Cities",
562 )
563 )
564
565 # Add equator
566 eq_lon = np.linspace(0, 2 * np.pi, 100)
567 eq_ecef = np.array([geodetic_to_ecef(0, lon, 0) for lon in eq_lon])
568 fig.add_trace(
569 go.Scatter3d(
570 x=eq_ecef[:, 0] * scale,
571 y=eq_ecef[:, 1] * scale,
572 z=eq_ecef[:, 2] * scale,
573 mode="lines",
574 line=dict(color="yellow", width=3),
575 name="Equator",
576 )
577 )
578
579 # Add prime meridian
580 pm_lat = np.linspace(-np.pi / 2, np.pi / 2, 100)
581 pm_ecef = np.array([geodetic_to_ecef(lat, 0, 0) for lat in pm_lat])
582 fig.add_trace(
583 go.Scatter3d(
584 x=pm_ecef[:, 0] * scale,
585 y=pm_ecef[:, 1] * scale,
586 z=pm_ecef[:, 2] * scale,
587 mode="lines",
588 line=dict(color="orange", width=3),
589 name="Prime Meridian",
590 )
591 )
592
593 fig.update_layout(
594 title="Earth: Geodetic to ECEF Coordinate Conversion",
595 scene=dict(
596 xaxis=dict(title="X (1000 km)"),
597 yaxis=dict(title="Y (1000 km)"),
598 zaxis=dict(title="Z (1000 km)"),
599 aspectmode="data",
600 ),
601 width=900,
602 height=800,
603 )
604
605 return fig
606
607
608def main():
609 """Run coordinate visualization examples."""
610 print("Coordinate System Visualization Examples")
611 print("=" * 50)
612
613 # 1. Rotation axes
614 print("\n1. Generating rotation axes visualization...")
615 fig1 = plot_rotation_axes()
616 fig1.write_html(str(OUTPUT_DIR / "coord_viz_rotation_axes.html"))
617 print(f" Saved to {OUTPUT_DIR / 'coord_viz_rotation_axes.html'}")
618
619 # 2. Euler rotation sequence
620 print("\n2. Generating Euler rotation sequence...")
621 fig2 = plot_euler_rotation_sequence()
622 fig2.write_html(str(OUTPUT_DIR / "coord_viz_euler_sequence.html"))
623 print(f" Saved to {OUTPUT_DIR / 'coord_viz_euler_sequence.html'}")
624
625 # 3. Quaternion SLERP
626 print("\n3. Generating quaternion SLERP visualization...")
627 fig3 = plot_quaternion_slerp()
628 fig3.write_html(str(OUTPUT_DIR / "coord_viz_slerp.html"))
629 print(f" Saved to {OUTPUT_DIR / 'coord_viz_slerp.html'}")
630
631 # 4. Spherical coordinates
632 print("\n4. Generating spherical coordinates visualization...")
633 fig4 = plot_spherical_coordinates()
634 fig4.write_html(str(OUTPUT_DIR / "coord_viz_spherical.html"))
635 print(f" Saved to {OUTPUT_DIR / 'coord_viz_spherical.html'}")
636
637 # 5. Earth coordinates
638 print("\n5. Generating Earth coordinates visualization...")
639 fig5 = plot_earth_coordinates()
640 fig5.write_html(str(OUTPUT_DIR / "coord_viz_earth.html"))
641 print(f" Saved to {OUTPUT_DIR / 'coord_viz_earth.html'}")
642
643 # Show all figures
644 print("\nOpening visualizations in browser...")
645 fig1.show()
646 fig2.show()
647 fig3.show()
648 fig4.show()
649 fig5.show()
650
651 print("\nDone!")
652
653
654if __name__ == "__main__":
655 main()
Running the Example
python examples/coordinate_visualization.py
See Also
Coordinate Systems - Coordinate conversion functions
Navigation and Geodesy - Geodetic operations