A Discrete-Event Network Simulator
API
Loading...
Searching...
No Matches
plot-orbital-traces.py
Go to the documentation of this file.
1#! /usr/bin/env python3
2
3# Copyright (c) 2024 Centre Tecnologic de Telecomunicacions de Catalunya (CTTC)
4#
5# SPDX-License-Identifier: GPL-2.0-only
6
7import argparse
8
9import matplotlib.pyplot as plt
10import numpy as np
11import pandas as pd
12from matplotlib.animation import FuncAnimation
13
14
15def main():
16 parser = argparse.ArgumentParser(
17 description="Generate an animated MP4 video of satellite orbital traces from CSV data. "
18 "The script reads satellite position data over time and creates a 3D animation "
19 "showing Earth as a wireframe sphere with satellite positions as red dots. "
20 "Optionally (-a)includes antenna orientation vectors as orange lines.",
21 epilog="Examples:\n"
22 " python plot-orbital-traces.py -i traces.csv -o animation.mp4\n"
23 " python plot-orbital-traces.py -i traces.csv -o animation.mp4 -a\n"
24 "\n"
25 "Input CSV format:\n"
26 " Time(ns),Satellite,x,y,z[,x_2,y_2,z_2]\n"
27 " 1000000000,1,6371000,0,0[,6372000,0,0]\n"
28 "\n"
29 "Requires: pandas, matplotlib, numpy, ffmpeg",
30 )
31
32 parser.add_argument(
33 "--input",
34 "-i",
35 help="Path to the input CSV file containing satellite trace data. "
36 "Expected columns: Time (with 'ns' suffix), Satellite, x, y, z. "
37 "For antenna traces: also x_2, y_2, z_2 columns.",
38 required=True,
39 )
40 parser.add_argument(
41 "--output",
42 "-o",
43 help="Path for the output MP4 video file (e.g., 'orbital_animation.mp4'). "
44 "Requires ffmpeg to be installed.",
45 required=True,
46 )
47 parser.add_argument(
48 "--antenna-trace",
49 "-a",
50 help="Include antenna orientation vectors in the animation. "
51 "Requires x_2, y_2, z_2 columns in the input CSV for each satellite.",
52 action="store_true",
53 )
54 parser.add_argument(
55 "--downsample",
56 "-d",
57 type=int,
58 default=1,
59 help="Render every Nth frame (default: 1). "
60 "Use to speed up animation for large traces (e.g., -d 2 halves frame count).",
61 )
62
63 args = parser.parse_args()
64
65 df = pd.read_csv(args.input)
66 # Convert from ns to s (rstrip 'ns' to handle both 'ns' suffix and plain numbers)
67 df["Time"] = df["Time"].astype(str).str.rstrip("ns").astype(float) * 1e-9
68
69 # Get unique timestamps
70 times = np.sort(df["Time"].unique())
71
72 # Pre-group data by time for O(1) per-frame lookup instead of O(n) filtering
73 groups = df.groupby("Time")
74 # Build list of (x_arr, y_arr, z_arr) tuples indexed by frame number
75 frame_data = [
76 (
77 groups.get_group(t)["x"].values,
78 groups.get_group(t)["y"].values,
79 groups.get_group(t)["z"].values,
80 )
81 for t in times
82 ]
83
84 # Pre-group antenna data per frame
85 if args.antenna_trace:
86 frame_antenna = {}
87 for t in times:
88 g = groups.get_group(t)
89 frame_antenna[t] = {}
90 for sat in g["Satellite"].unique():
91 sg = g[g["Satellite"] == sat]
92 frame_antenna[t][sat] = (
93 sg[["x", "y", "z"]].values[0],
94 sg[["x_2", "y_2", "z_2"]].values[0],
95 )
96
97 # Set up the 3D figure
98 fig = plt.figure()
99 ax = fig.add_subplot(111, projection="3d")
100
101 # Plot Earth as a sphere
102 radius = 6371e3
103 u, v = np.mgrid[0 : 2 * np.pi : 50j, 0 : np.pi : 25j]
104 xs = radius * np.cos(u) * np.sin(v)
105 ys = radius * np.sin(u) * np.sin(v)
106 zs = radius * np.cos(v)
107 ax.plot_wireframe(xs, ys, zs, color="lightblue", linewidth=0.6, alpha=0.7)
108
109 # Initialize scatter
110 scatter = ax.scatter([], [], [], color="red", s=5)
111
112 # Initialize line objects for each satellite
113 if args.antenna_trace:
114 lines = {}
115 for sat in df["Satellite"].unique():
116 (line,) = ax.plot([], [], [], alpha=0.7, lw=0.7, color="orange")
117 lines[sat] = line
118 all_sats = list(df["Satellite"].unique())
119
120 # Downsample frames for speed: render every Nth frame
121 step = max(1, args.downsample)
122
123 def make_update(frame_step, times, frame_data, frame_antenna):
124 def update(frame):
125 i = frame * frame_step
126 current_time = times[i]
127 x, y, z = frame_data[i]
128 scatter._offsets3d = (x, y, z)
129 if args.antenna_trace:
130 for sat, line in zip(all_sats, lines.values()):
131 if sat in frame_antenna.get(current_time, {}):
132 pos, ant = frame_antenna[current_time][sat]
133 line.set_data_3d([pos[0], ant[0]], [pos[1], ant[1]], [pos[2], ant[2]])
134 else:
135 line.set_data_3d([], [], [])
136 ax.set_title(f"Time = {current_time:.2f} s")
137 if args.antenna_trace:
138 return scatter, *lines.values()
139 else:
140 return scatter
141
142 return update
143
144 ani = FuncAnimation(
145 fig,
146 make_update(step, times, frame_data, frame_antenna),
147 frames=len(times) // step,
148 interval=100,
149 blit=False,
150 )
151 ani.save(args.output, writer="ffmpeg", fps=30)
152
153
154if __name__ == "__main__":
155 main()