MUSIC eigenvalue visualisation

One of the issues I've been facing was that the range of the eigenvalues
was quite large, and I couldn't easily see a natural threshold to set to
separate the signal and noise subspaces.

This visualisation provides a histogram for this scenario, to assist in
choosing the parameter (which is different in different configurations).
This commit is contained in:
Christos Falas 2025-03-04 13:48:32 +00:00
parent 342357bd0c
commit c452e44e1a
No known key found for this signature in database
6 changed files with 78 additions and 8 deletions

View File

@ -57,8 +57,7 @@ def heatmap() -> None:
logger.info(f"Processed CSI data with shape {processed.shape}") logger.info(f"Processed CSI data with shape {processed.shape}")
processed_tensor = torch.tensor(processed, device=device) processed_tensor = torch.tensor(processed, device=device)
aoa.update(processed_tensor) aoa.update(processed_tensor)
heatmap = aoa.heatmap() aoa.heatmap(visualiser=visualise_data)
visualise_data(heatmap, visualise.DataType.HEATMAP)
globals.csi_producer(csi_callback=callback) globals.csi_producer(csi_callback=callback)
logger.info("Finished processing CSI data") logger.info("Finished processing CSI data")

View File

@ -1,11 +1,13 @@
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Any, Callable
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
import torch import torch
from ..config import config from ..config import config
from ..visualise import server as visualise
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -111,16 +113,24 @@ class AoA:
assert steering.shape == (N, self.N_rx // 2, self.N_subcarriers // 2) assert steering.shape == (N, self.N_rx // 2, self.N_subcarriers // 2)
return steering.reshape(N, -1) return steering.reshape(N, -1)
def evaluate(self, theta: torch.Tensor, tof: torch.Tensor) -> torch.Tensor: def evaluate(
self,
theta: torch.Tensor,
tof: torch.Tensor,
visualiser: None
| Callable[[npt.NDArray[Any], visualise.DataType], None] = None,
) -> torch.Tensor:
R = torch.mean(self.historical_autocorr, dim=0) R = torch.mean(self.historical_autocorr, dim=0)
# The smallest eigenvectors span the noise subspace, # The smallest eigenvectors span the noise subspace,
# and the largest span the signal subspace. # and the largest span the signal subspace.
logger.debug(f"Calculating eigenvectors of R: {R.shape}") logger.debug(f"Calculating eigenvectors of R: {R.shape}")
eigvals, eigvecs = torch.linalg.eig(R) eigvals, eigvecs = torch.linalg.eig(R)
if visualiser:
visualiser(eigvals.numpy(), visualise.DataType.EIGENVALUES)
assert isinstance(eigvals, torch.Tensor) assert isinstance(eigvals, torch.Tensor)
assert isinstance(eigvecs, torch.Tensor) assert isinstance(eigvecs, torch.Tensor)
logger.info(f"Eigenvalues: {eigvals}") logger.debug(f"Eigenvalues: {eigvals}")
E_n = eigvecs[:, torch.abs(eigvals) < config.music.eigval_threshold] E_n = eigvecs[:, torch.abs(eigvals) < config.music.eigval_threshold]
logger.debug(f"Signal subspace: {E_n.shape}") logger.debug(f"Signal subspace: {E_n.shape}")
@ -136,7 +146,10 @@ class AoA:
c: torch.Tensor = 1 / (steering_h @ E_n @ E_n_H @ steering) c: torch.Tensor = 1 / (steering_h @ E_n @ E_n_H @ steering)
return torch.abs(c)[:, 0, 0] return torch.abs(c)[:, 0, 0]
def heatmap(self) -> npt.NDArray[np.float32]: def heatmap(
self,
visualiser: None | Callable[[npt.NDArray[Any], visualise.DataType], None],
) -> npt.NDArray[np.float32]:
thetas = np.linspace( thetas = np.linspace(
0, np.pi, config.music.heatmap.theta_resolution, dtype=np.float32 0, np.pi, config.music.heatmap.theta_resolution, dtype=np.float32
) )
@ -153,12 +166,16 @@ class AoA:
evaluated = self.evaluate( evaluated = self.evaluate(
torch.tensor(thetas_mesh.reshape(-1)), torch.tensor(thetas_mesh.reshape(-1)),
torch.tensor(tofs_mesh.reshape(-1)), torch.tensor(tofs_mesh.reshape(-1)),
visualiser=visualiser,
) )
logger.debug(f"Evaluated heatmap: {evaluated.shape}") logger.debug(f"Evaluated heatmap: {evaluated.shape}")
heatmap: npt.NDArray[np.float32] = evaluated.reshape( heatmap: npt.NDArray[np.float32] = evaluated.reshape(
config.music.heatmap.tof_resolution, config.music.heatmap.tof_resolution,
config.music.heatmap.theta_resolution, config.music.heatmap.theta_resolution,
).numpy(force=True) ).numpy(force=True)
if visualiser:
visualiser(heatmap, visualise.DataType.HEATMAP)
return heatmap return heatmap

View File

@ -42,6 +42,24 @@ const plotData = computed(() => {
type: 'heatmap' as const, type: 'heatmap' as const,
}, },
] ]
case 'histogram':
const bin_start = data.value.figure.histogram.bins.slice(0, -1)
const bin_end = data.value.figure.histogram.bins.slice(1)
const bin_center = bin_start.map((start, i) => (start + bin_end[i]) / 2)
const bin_width = bin_start.map((start, i) => bin_end[i] - start)
console.log('Histogram data:', bin_center, bin_width)
console.log(
'Sizes:',
bin_center.length,
bin_width.length,
data.value.figure.histogram.data.length,
)
return data.value.figure.histogram.data.map((series) => ({
x: bin_center,
y: series.data,
width: bin_width,
type: 'bar' as const,
}))
} }
} }
return [] return []
@ -49,8 +67,8 @@ const plotData = computed(() => {
const layout = { const layout = {
title: { text: figure.title }, title: { text: figure.title },
xaxis: { title: { text: figure.xLabel } }, xaxis: { title: { text: figure.xLabel }, type: figure.logx ? 'log' : undefined },
yaxis: { title: { text: figure.yLabel } }, yaxis: { title: { text: figure.yLabel }, type: figure.logy ? 'log' : undefined },
height: 700, height: 700,
} }

View File

@ -2,12 +2,16 @@ syntax = "proto3";
import "figure_type/line.proto"; import "figure_type/line.proto";
import "figure_type/heatmap.proto"; import "figure_type/heatmap.proto";
import "figure_type/histogram.proto";
message Figure { message Figure {
string uuid = 1; string uuid = 1;
string title = 2; string title = 2;
string x_label = 3; string x_label = 3;
string y_label = 4; string y_label = 4;
bool logx = 5;
bool logy = 6;
} }
message FigureData { message FigureData {
@ -15,6 +19,7 @@ message FigureData {
oneof figure { oneof figure {
LineChartData line = 2; LineChartData line = 2;
HeatmapData heatmap = 3; HeatmapData heatmap = 3;
HistogramData histogram = 4;
} }
} }

View File

@ -0,0 +1,9 @@
syntax = "proto3";
message HistogramSeries { repeated float data = 1; }
message HistogramData {
string uuid = 1;
repeated HistogramSeries data = 2;
repeated float bins = 3;
}

View File

@ -13,7 +13,7 @@ import numpy as np
import numpy.typing as npt import numpy.typing as npt
from .generated import figure_pb2, figure_pb2_grpc from .generated import figure_pb2, figure_pb2_grpc
from .generated.figure_type import heatmap_pb2, line_pb2 from .generated.figure_type import heatmap_pb2, histogram_pb2, line_pb2
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -26,6 +26,7 @@ class DataType(Enum):
UNWRAPPED_PHASE = 2 UNWRAPPED_PHASE = 2
PROCESSED_CSI = 3 PROCESSED_CSI = 3
HEATMAP = 4 HEATMAP = 4
EIGENVALUES = 5
@dataclass @dataclass
@ -47,6 +48,13 @@ figures = {
x_label="Subcarrier", x_label="Subcarrier",
y_label="Unwrapped phase", y_label="Unwrapped phase",
), ),
"aoa_eig": figure_pb2.Figure(
uuid=str(uuid.uuid4()),
title="AoA Eigenvalues",
x_label="Eigenvalue",
y_label="Frequency",
logx=True,
),
"processed_phase": figure_pb2.Figure( "processed_phase": figure_pb2.Figure(
uuid=str(uuid.uuid4()), uuid=str(uuid.uuid4()),
title="Preprocessed CSI Phase", title="Preprocessed CSI Phase",
@ -130,6 +138,20 @@ def add_data(dtype: DataType, new_data: npt.NDArray[np.complex128]) -> None:
linechart = line_pb2.LineChartData(lines=lines) linechart = line_pb2.LineChartData(lines=lines)
for q in clients.get(uuid, []): for q in clients.get(uuid, []):
q.put(figure_pb2.FigureData(uuid=uuid, line=linechart)) q.put(figure_pb2.FigureData(uuid=uuid, line=linechart))
case DataType.EIGENVALUES:
uuid = figures["aoa_eig"].uuid
magn = np.abs(new_data)
bins = np.logspace(np.log10(magn.min()), np.log10(magn.max()), 10)
hist, _ = np.histogram(magn, bins=bins)
logger.info(f"bins: {bins}, hist: {hist}")
logger.info(
f"Updating histogram with {len(bins)} bins, and {len(hist)} bars"
)
logger.info(bins)
series = [histogram_pb2.HistogramSeries(data=hist)]
histogram = histogram_pb2.HistogramData(data=series, bins=bins)
for q in clients.get(uuid, []):
q.put(figure_pb2.FigureData(uuid=uuid, histogram=histogram))
def listen_for_data(data_queue: "mp.Queue[VisualiserData]") -> None: def listen_for_data(data_queue: "mp.Queue[VisualiserData]") -> None: