A Discrete-Event Network Simulator
API
Loading...
Searching...
No Matches
three-gpp-channel-consistency-example.py
Go to the documentation of this file.
1#!/bin/python3
2
3# SPDX-License-Identifier: GPL-2.0-only
4
5import matplotlib.animation as animation
6import matplotlib.patches as patches
7import matplotlib.pyplot as plt
8import numpy as np
9import pandas as pd
10from matplotlib.lines import Line2D
11
12#
13# Plot the traces generated by three-gpp-channel-consistency-example
14#
15
16# -----------------------------------------------------------------------------
17# Load data
18# -----------------------------------------------------------------------------
19df = pd.read_csv("3gpp-channel-consistency-output.txt", sep=r"\s+", comment="#")
20
21# Column names from the ns-3 trace
22TIME_COL = "Time[s]"
23TX_X_COL = "TxPosX[m]"
24TX_Y_COL = "TxPosY[m]"
25RX_X_COL = "RxPosX[m]"
26RX_Y_COL = "RxPosY[m]"
27STATE_COL = "ChannelState"
28SNR_COL = "SNR[dB]"
29
30# Convenience series
31time = df[TIME_COL]
32tx_x = df[TX_X_COL]
33tx_y = df[TX_Y_COL]
34rx_x = df[RX_X_COL]
35rx_y = df[RX_Y_COL]
36snr = df[SNR_COL]
37
38# Channel state (for LOS/NLOS vertical lines)
39state = df[STATE_COL].astype(str)
40
41# -----------------------------------------------------------------------------
42# Varying rates
43# -----------------------------------------------------------------------------
44snr_diff = snr.diff().fillna(0.0)
45df["SNRdiff"] = snr_diff
46# -----------------------------------------------------------------------------
47# Compute LOS/NLOS transition times and types
48# -----------------------------------------------------------------------------
49prev_state = state.shift(1, fill_value=state.iloc[0])
50
51times_LN = [] # LOS -> NLOS(NLOSv)
52times_NL = [] # NLOS(NLOSv) -> LOS
53
54for t, s_prev, s_cur in zip(time, prev_state, state):
55 if s_prev == s_cur:
56 continue
57
58 s_prev_is_nlos = s_prev.startswith("NLOS")
59 s_cur_is_nlos = s_cur.startswith("NLOS")
60
61 if s_prev == "LOS" and s_cur_is_nlos:
62 times_LN.append(t)
63 elif s_prev_is_nlos and s_cur == "LOS":
64 times_NL.append(t)
65
66
67def add_state_vlines(ax, add_legend=False):
68 """Draw vertical lines for LOS/NLOS transitions, with different types."""
69 # NLOS -> LOS (green, solid)
70 for t in times_NL:
71 ax.axvline(t, color="green", linestyle="-", linewidth=0.9, alpha=0.6)
72 # LOS -> NLOS (red, dashed)
73 for t in times_LN:
74 ax.axvline(t, color="red", linestyle="--", linewidth=0.9, alpha=0.6)
75
76 if add_legend:
77 legend_lines = [
78 Line2D([0], [0], color="green", linestyle="-", linewidth=1.2, label="NLOS → LOS"),
79 Line2D([0], [0], color="red", linestyle="--", linewidth=1.2, label="LOS → NLOS"),
80 ]
81 ax.legend(handles=legend_lines, loc="upper right", fontsize=8)
82
83
84# -----------------------------------------------------------------------------
85# Global plot params
86# -----------------------------------------------------------------------------
87zoom_window_s = 0.05 # SNR zoom window (example: 0.05 = 50 ms)
88
89t_min = time.min()
90t_max = time.max()
91
92# SNR limits
93snr_min = snr.min()
94snr_max = snr.max()
95snr_margin = 3.0
96# snr_ymin = snr_min - snr_margin
97# snr_ymax = snr_max + snr_margin
98snr_ymax = 30
99snr_ymin = -60
100
101
102# ΔSNR limits: symmetric around 0 with minimum span
103max_abs_dsnr = float(np.max(np.abs(snr_diff)))
104if max_abs_dsnr < 1.0:
105 max_abs_dsnr = 1.0 # at least +/- 1 dB
106# snr_diff_ymin = -1.2 * max_abs_dsnr
107# snr_diff_ymax = +1.2 * max_abs_dsnr
108
109snr_diff_ymin = -35
110snr_diff_ymax = +35
111
112
113# -----------------------------------------------------------------------------
114# Frame downsampling for animation (lightweight)
115# Simulation: 40 s @ 1 ms => ~40 000 samples
116# We cap GIF to ~800 frames => ~1 frame each 50 ms
117# -----------------------------------------------------------------------------
118n_samples = len(df)
119max_frames = 800 # target max frames in the GIF
120frame_step = max(1, n_samples // max_frames)
121frame_indices = range(0, n_samples, frame_step)
122
123# -----------------------------------------------------------------------------
124# 2×2 Animated Figure:
125# row 0: [map, SNR global]
126# row 1: [SNR zoom, ΔSNR]
127# -----------------------------------------------------------------------------
128fig, axes = plt.subplots(2, 2, figsize=(24, 9), dpi=150)
129
130ax_map = axes[0, 0]
131ax_snr = axes[0, 1]
132ax_snr_zoom = axes[1, 0]
133ax_snr_diff = axes[1, 1]
134
135# -----------------------------------------------------------------------------
136# MAP
137# -----------------------------------------------------------------------------
138ax_map.set_xlabel("X [m]")
139ax_map.set_ylabel("Y [m]")
140ax_map.set_aspect("equal")
141
142ax_map.set_xlim(-25, 600)
143ax_map.set_ylim(-25, 1000)
144
145tx_circle = patches.Circle((0.0, 0.0), 5.0, color="blue", alpha=0.35)
146rx_circle = patches.Circle((0.0, 0.0), 5.0, color="red", alpha=0.35)
147ax_map.add_patch(tx_circle)
148ax_map.add_patch(rx_circle)
149
150buildings = pd.read_csv(
151 "3gpp-channel-consistency-buildings.txt", sep=r"\s+", comment="#", header=None
152)
153
154for _, b in buildings.iterrows():
155 x0, y0, x1, y1 = b
156 rect = patches.Rectangle((x0, y0), x1 - x0, y1 - y0, color="gray", alpha=0.5)
157 ax_map.add_patch(rect)
158
159# -----------------------------------------------------------------------------
160# SNR Global
161# -----------------------------------------------------------------------------
162ax_snr.set_xlabel("Time [s]")
163ax_snr.set_ylabel("SNR [dB]")
164ax_snr.grid(True)
165ax_snr.set_xlim(t_min, t_max)
166ax_snr.set_ylim(snr_ymin, snr_ymax)
167(snr_line,) = ax_snr.plot([], [], "k-", linewidth=1.5)
168
169add_state_vlines(ax_snr, add_legend=True)
170
171# -----------------------------------------------------------------------------
172# SNR Zoom
173# -----------------------------------------------------------------------------
174ax_snr_zoom.set_xlabel("Time [s]")
175ax_snr_zoom.set_ylabel("SNR [dB] (zoom)")
176ax_snr_zoom.grid(True)
177ax_snr_zoom.set_ylim(snr_ymin, snr_ymax)
178(snr_zoom_line,) = ax_snr_zoom.plot([], [], "k-", linewidth=1.5)
179
180add_state_vlines(ax_snr_zoom)
181
182# -----------------------------------------------------------------------------
183# ΔSNR (global)
184# -----------------------------------------------------------------------------
185ax_snr_diff.set_xlabel("Time [s]")
186ax_snr_diff.set_ylabel("ΔSNR [dB]")
187ax_snr_diff.grid(True)
188ax_snr_diff.set_xlim(t_min, t_max)
189ax_snr_diff.set_ylim(snr_diff_ymin, snr_diff_ymax)
190(snr_diff_line,) = ax_snr_diff.plot([], [], "k-", linewidth=1.5)
191
192add_state_vlines(ax_snr_diff)
193
194# -----------------------------------------------------------------------------
195# Static SNR + ΔSNR figure (2×1)
196# -----------------------------------------------------------------------------
197fig_snr, axes_snr = plt.subplots(2, 1, figsize=(10, 6), sharex=True)
198
199axes_snr[0].plot(time, snr, "k-", linewidth=1.2)
200axes_snr[0].set_ylabel("SNR [dB]")
201axes_snr[0].grid(True)
202add_state_vlines(axes_snr[0], add_legend=True)
203
204axes_snr[1].plot(time, snr_diff, "k-", linewidth=1.2)
205axes_snr[1].set_ylabel("ΔSNR [dB]")
206axes_snr[1].set_xlabel("Time [s]")
207axes_snr[1].grid(True)
208add_state_vlines(axes_snr[1])
209
210axes_snr[0].set_xlim(t_min, t_max)
211axes_snr[0].set_ylim(snr_ymin, snr_ymax)
212axes_snr[1].set_ylim(snr_diff_ymin, snr_diff_ymax)
213fig_snr.tight_layout()
214fig_snr.savefig("channel_consistency_snr_and_dsnr.png", dpi=300, bbox_inches="tight")
215plt.close(fig_snr)
216
217
218# -----------------------------------------------------------------------------
219# -----------------------------------------------------------------------------
220# Animation update function (uses downsampled frame_indices)
221# -----------------------------------------------------------------------------
222def update(frame_idx):
223 # frame_idx is an actual index into df (from frame_indices)
224 t_now = time.iloc[frame_idx]
225
226 # Map
227 tx_circle.set_center((tx_x.iloc[frame_idx], tx_y.iloc[frame_idx]))
228 rx_circle.set_center((rx_x.iloc[frame_idx], rx_y.iloc[frame_idx]))
229 ax_map.set_title(f"Time = {t_now:.3f} s")
230
231 # Global SNR
232 snr_line.set_data(time.iloc[: frame_idx + 1], snr.iloc[: frame_idx + 1])
233
234 # SNR zoom
235 t0 = max(t_min, t_now - zoom_window_s / 2.0)
236 t1 = min(t_max, t_now + zoom_window_s / 2.0)
237 mask = (time >= t0) & (time <= t1)
238
239 ax_snr_zoom.set_xlim(t0, t1)
240 snr_zoom_line.set_data(time[mask], snr[mask])
241
242 # ΔSNR (global)
243 snr_diff_line.set_data(time.iloc[: frame_idx + 1], snr_diff.iloc[: frame_idx + 1])
244
245 return (tx_circle, rx_circle, snr_line, snr_zoom_line, snr_diff_line)
246
247
248# -----------------------------------------------------------------------------
249# Run animation (downsampled frames, lighter GIF)
250# -----------------------------------------------------------------------------
251ani = animation.FuncAnimation(
252 fig,
253 update,
254 frames=frame_indices,
255 interval=0,
256 blit=False,
257)
258
259ani.save("3gpp-channel-consistency.gif", writer="pillow", fps=25, dpi=90)