diff --git a/where_fi/cli/__init__.py b/where_fi/cli/__init__.py index 131a52d..98fe07f 100644 --- a/where_fi/cli/__init__.py +++ b/where_fi/cli/__init__.py @@ -1,6 +1,7 @@ import logging import multiprocessing as mp -from typing import Any +from typing import Any, cast +from queue import Queue import numpy as np import numpy.typing as npt @@ -63,4 +64,50 @@ def heatmap() -> None: logger.info("Finished processing CSI data") +@app.command() +def phase_analysis( + subcarrier: int = 0, rx_antenna: int = 0, tx_antenna: int = 0 +) -> None: + """ + Visualise the phase information in the CSI data received from the antennas. + + The data goes through the same preprocessing steps as the heatmap command, but + instead of going through the AoA estimation, we simply analyse the phase of the + selected subcarrier and antenna. + """ + + preprocessor = Preprocessor() + + # Start webapp in background process + webapp_queue: "mp.Queue[visualise.VisualiserData]" = mp.Queue(config.sample_rate) + webapp = mp.Process(target=visualise.start, args=(webapp_queue,)) + webapp.start() + + subcarrier_phase: Queue[float] = Queue(config.sample_rate) + + def visualise_data(data: npt.NDArray[Any], dtype: visualise.figures.Figure) -> None: + if not webapp_queue.full(): + webapp_queue.put(visualise.VisualiserData(data, dtype)) + + def callback(antenna_data: npt.NDArray[np.complex64]) -> None: + logger.info(f"Got final CSI data with shape {antenna_data.shape}") + visualise_data(antenna_data, visualise.figures.Figure.RAW_CSI) + + processed = preprocessor.preprocess(antenna_data, visualiser=visualise_data) + visualise_data(processed, visualise.figures.Figure.PROCESSED_CSI) + + phase = cast(float, np.angle(processed[subcarrier, rx_antenna, tx_antenna])) + + if subcarrier_phase.full(): + subcarrier_phase.get() + subcarrier_phase.put(phase) + + visualise_data( + np.array(subcarrier_phase), visualise.figures.Figure.PHASE_ANALYSIS + ) + + globals.csi_producer(csi_callback=callback) + logger.info("Finished processing CSI data") + + app.add_typer(file.app, name="file", help="Commands for working with CSI files") diff --git a/where_fi/visualise/server/__init__.py b/where_fi/visualise/server/__init__.py index bf63216..ff4adc5 100644 --- a/where_fi/visualise/server/__init__.py +++ b/where_fi/visualise/server/__init__.py @@ -30,7 +30,7 @@ class FigureServer(figure_pb2_grpc.FigureServiceServicer): self, request: figure_pb2.FigureRequest, context: grpc.ServicerContext ) -> Generator[figure_pb2.Figure, None, None]: for figure_group in figures.Figure: - for figure in figures.all_figures[figure_group].figures: + for figure in figure_group.figure_class().figures: yield figure def GetFigureUpdate( diff --git a/where_fi/visualise/server/figures.py b/where_fi/visualise/server/figures.py index 64f2a60..70c125b 100644 --- a/where_fi/visualise/server/figures.py +++ b/where_fi/visualise/server/figures.py @@ -58,10 +58,17 @@ class SimpleLineChart: ) def update( - self, new_data: npt.NDArray[np.complex128], labels: Sequence[str] + self, + new_data: npt.NDArray[np.complex128], + labels: Sequence[str], + x_values: None | npt.NDArray[np.float64] = None, ) -> FigureUpdate: lines = [ - line_pb2.LineChartData.Line(y=new_data[i], label=labels[i]) + line_pb2.LineChartData.Line( + y=new_data[i], + label=labels[i], + x=x_values[i] if x_values is not None else None, + ) for i in range(new_data.shape[0]) ] return { @@ -156,12 +163,28 @@ class HeatmapFigure(SpecificFigure): return {self.figure.uuid: figure_pb2.FigureData(heatmap=heatmap)} +class EmpiricalCDF(SpecificFigure): + """Helper class to create a graph of the empirical cumulative distribution function + of a set of observations. + """ + + def __init__(self, title: str, x_label: str) -> None: + self.chart = SimpleLineChart(title, x_label, "Cumulative Probability") + self.figures = [self.chart.figure] + + def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate: + new_data = np.expand_dims(np.sort(new_data), 0) + y_values = np.expand_dims(np.linspace(0, 1, new_data.size), 0) + return self.chart.update(y_values, labels=["Frequency"], x_values=new_data) + + class Figure(Enum): RAW_CSI = 0 UNWRAPPED_PHASE = 1 PROCESSED_CSI = 2 MUSIC_EIGENVALUES = 3 AOA_HEATMAP = 4 + PHASE_ANALYSIS = 5 def figure_class(self) -> SpecificFigure: return all_figures[self] @@ -187,4 +210,5 @@ all_figures = { ), Figure.MUSIC_EIGENVALUES: MusicEigenvalueHistogram(), Figure.AOA_HEATMAP: HeatmapFigure(), + Figure.PHASE_ANALYSIS: EmpiricalCDF("Phase Analysis", "Phase"), }