193 lines
5.7 KiB
Python
193 lines
5.7 KiB
Python
|
G
|
import numpy as np
|
||
|
|
import plotly.graph_objects as go
|
||
|
|
import scipy.signal as signal
|
||
|
|
from plotly.graph_objs import Figure
|
||
|
|
from scipy.fft import fft, fftshift
|
||
|
|
|
||
|
|
from utils.data import Recording
|
||
|
|
|
||
|
|
|
||
|
|
def spectrogram(rec: Recording, thumbnail: bool = False) -> Figure:
|
||
|
|
"""Create a spectrogram for the recording.
|
||
|
|
|
||
|
|
:param rec: Signal to plot.
|
||
|
|
:type rec: utils.data.Recording
|
||
|
|
:param thumbnail: Whether to return a small thumbnail version or full plot.
|
||
|
|
:type thumbnail: bool
|
||
|
|
|
||
|
|
:return: Spectrogram, as a Plotly figure.
|
||
|
|
"""
|
||
|
|
complex_signal = rec.data[0]
|
||
|
|
sample_rate = int(rec.metadata.get("sample_rate", 1))
|
||
|
|
plot_length = len(complex_signal)
|
||
|
|
|
||
|
|
# Determine FFT size
|
||
|
|
if plot_length < 2000:
|
||
|
|
fft_size = 64
|
||
|
|
elif plot_length < 10000:
|
||
|
|
fft_size = 256
|
||
|
|
elif plot_length < 1000000:
|
||
|
|
fft_size = 1024
|
||
|
|
else:
|
||
|
|
fft_size = 2048
|
||
|
|
|
||
|
|
frequencies, times, Sxx = signal.spectrogram(
|
||
|
|
complex_signal,
|
||
|
|
fs=sample_rate,
|
||
|
|
nfft=fft_size,
|
||
|
|
nperseg=fft_size,
|
||
|
|
noverlap=fft_size // 8,
|
||
|
|
scaling="density",
|
||
|
|
mode="complex",
|
||
|
|
return_onesided=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Convert complex values to amplitude and then to log scale for visualization
|
||
|
|
Sxx_magnitude = np.abs(Sxx)
|
||
|
|
Sxx_log = np.log10(Sxx_magnitude + 1e-6)
|
||
|
|
|
||
|
|
# Normalize spectrogram values between 0 and 1 for plotting
|
||
|
|
Sxx_log_shifted = Sxx_log - np.min(Sxx_log)
|
||
|
|
Sxx_log_norm = Sxx_log_shifted / np.max(Sxx_log_shifted)
|
||
|
|
|
||
|
|
# Shift frequency bins and spectrogram rows so frequencies run from negative to positive
|
||
|
|
frequencies_shifted = np.fft.fftshift(frequencies)
|
||
|
|
Sxx_shifted = np.fft.fftshift(Sxx_log_norm, axes=0)
|
||
|
|
|
||
|
|
fig = go.Figure(
|
||
|
|
data=go.Heatmap(
|
||
|
|
z=Sxx_shifted,
|
||
|
|
x=times / 1e6,
|
||
|
|
y=frequencies_shifted,
|
||
|
|
colorscale="Viridis",
|
||
|
|
zmin=0,
|
||
|
|
zmax=1,
|
||
|
|
reversescale=False,
|
||
|
|
showscale=False,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
if thumbnail:
|
||
|
|
fig.update_xaxes(showticklabels=False)
|
||
|
|
fig.update_yaxes(showticklabels=False)
|
||
|
|
fig.update_layout(
|
||
|
|
template="plotly_dark",
|
||
|
|
width=200,
|
||
|
|
height=100,
|
||
|
|
margin=dict(l=5, r=5, t=5, b=5),
|
||
|
|
xaxis=dict(scaleanchor=None),
|
||
|
|
yaxis=dict(scaleanchor=None),
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
fig.update_layout(
|
||
|
|
title="Spectrogram",
|
||
|
|
xaxis_title="Time [s]",
|
||
|
|
yaxis_title="Frequency [Hz]",
|
||
|
|
template="plotly_dark",
|
||
|
|
height=300,
|
||
|
|
width=800,
|
||
|
|
)
|
||
|
|
|
||
|
|
return fig
|
||
|
|
|
||
|
|
|
||
|
|
def iq_time_series(rec: Recording) -> Figure:
|
||
|
|
"""Create a time series plot of the real and imaginary parts of signal.
|
||
|
|
|
||
|
|
:param rec: Signal to plot.
|
||
|
|
:type rec: utils.data.Recording
|
||
|
|
|
||
|
|
:return: Time series plot as a Plotly figure.
|
||
|
|
"""
|
||
|
|
complex_signal = rec.data[0]
|
||
|
|
sample_rate = int(rec.metadata.get("sample_rate", 1))
|
||
|
|
plot_length = len(complex_signal)
|
||
|
|
t = np.arange(0, plot_length, 1) / sample_rate
|
||
|
|
|
||
|
|
fig = go.Figure()
|
||
|
|
fig.add_trace(go.Scatter(x=t, y=complex_signal.real, mode="lines", name="I (In-phase)", line=dict(width=0.6)))
|
||
|
|
fig.add_trace(go.Scatter(x=t, y=complex_signal.imag, mode="lines", name="Q (Quadrature)", line=dict(width=0.6)))
|
||
|
|
|
||
|
|
fig.update_layout(
|
||
|
|
title="IQ Time Series",
|
||
|
|
xaxis_title="Time [s]",
|
||
|
|
yaxis_title="Amplitude",
|
||
|
|
template="plotly_dark",
|
||
|
|
height=300,
|
||
|
|
width=800,
|
||
|
|
showlegend=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
return fig
|
||
|
|
|
||
|
|
|
||
|
|
def frequency_spectrum(rec: Recording) -> Figure:
|
||
|
|
"""Create a frequency spectrum plot from the recording.
|
||
|
|
|
||
|
|
:param rec: Input signal to plot.
|
||
|
|
:type rec: utils.data.Recording
|
||
|
|
|
||
|
|
:return: Frequency spectrum as a Plotly figure.
|
||
|
|
"""
|
||
|
|
complex_signal = rec.data[0]
|
||
|
|
center_frequency = int(rec.metadata.get("center_frequency", 0))
|
||
|
|
sample_rate = int(rec.metadata.get("sample_rate", 1))
|
||
|
|
|
||
|
|
epsilon = 1e-10
|
||
|
|
spectrum = np.abs(fftshift(fft(complex_signal)))
|
||
|
|
freqs = np.linspace(-sample_rate / 2, sample_rate / 2, len(complex_signal)) + center_frequency
|
||
|
|
log_spectrum = np.log10(spectrum + epsilon)
|
||
|
|
scaled_log_spectrum = (log_spectrum - log_spectrum.min()) / (log_spectrum.max() - log_spectrum.min())
|
||
|
|
|
||
|
|
fig = go.Figure()
|
||
|
|
fig.add_trace(go.Scatter(x=freqs, y=scaled_log_spectrum, mode="lines", name="Spectrum", line=dict(width=0.4)))
|
||
|
|
|
||
|
|
fig.update_layout(
|
||
|
|
title="Frequency Spectrum",
|
||
|
|
xaxis_title="Frequency [Hz]",
|
||
|
|
yaxis_title="Magnitude",
|
||
|
|
yaxis_type="log",
|
||
|
|
template="plotly_dark",
|
||
|
|
height=300,
|
||
|
|
width=800,
|
||
|
|
showlegend=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
return fig
|
||
|
|
|
||
|
|
|
||
|
|
def constellation(rec: Recording) -> Figure:
|
||
|
|
"""Create a constellation plot from the recording.
|
||
|
|
|
||
|
|
:param rec: Input signal to plot.
|
||
|
|
:type rec: utils.data.Recording
|
||
|
|
|
||
|
|
:return: Constellation as a Plotly figure.
|
||
|
|
"""
|
||
|
|
complex_signal = rec.data[0]
|
||
|
|
|
||
|
|
# Downsample the IQ samples to a target number of points
|
||
|
|
# This reduces the amount of data plotted, improving performance and interactivity
|
||
|
|
# without losing significant detail in the constellation visualization.
|
||
|
|
target_number_of_points = 5000
|
||
|
|
step = max(1, len(complex_signal) // target_number_of_points)
|
||
|
|
i_ds = complex_signal.real[::step]
|
||
|
|
q_ds = complex_signal.imag[::step]
|
||
|
|
|
||
|
|
fig = go.Figure()
|
||
|
|
fig.add_trace(go.Scatter(x=i_ds, y=q_ds, mode="lines", name="Constellation", line=dict(width=0.2)))
|
||
|
|
|
||
|
|
fig.update_layout(
|
||
|
|
title="Constellation",
|
||
|
|
xaxis_title="In-phase (I)",
|
||
|
|
yaxis_title="Quadrature (Q)",
|
||
|
|
template="plotly_dark",
|
||
|
|
height=400,
|
||
|
|
width=400,
|
||
|
|
showlegend=False,
|
||
|
|
xaxis=dict(range=[-1.1, 1.1]),
|
||
|
|
yaxis=dict(range=[-1.1, 1.1]),
|
||
|
|
)
|
||
|
|
|
||
|
|
return fig
|