add figure extractor
Used to store the protobuffer messages into files, so that they can be plotted as part of the dissertation
This commit is contained in:
parent
6be12beb49
commit
6f454c6190
2
where_fi/visualise/.gitignore
vendored
2
where_fi/visualise/.gitignore
vendored
@ -1,2 +1,2 @@
|
|||||||
server/generated
|
generated
|
||||||
frontend/src/grpc
|
frontend/src/grpc
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
all: server/generated frontend/src/grpc
|
all: generated frontend/src/grpc
|
||||||
|
|
||||||
server/generated: protos/*.proto
|
generated: protos/*.proto
|
||||||
mkdir -p server/generated
|
mkdir -p generated
|
||||||
find protos/ -type f -name "*.proto" | xargs uv run python -m grpc_tools.protoc -Iprotos --python_out=server/generated --pyi_out=server/generated --grpc_python_out=server/generated
|
find protos/ -type f -name "*.proto" | xargs uv run python -m grpc_tools.protoc -Iprotos --python_out=generated --pyi_out=generated --grpc_python_out=generated --mypy_grpc_out=generated
|
||||||
find protos/ -type f -name "*.proto" | xargs uv run protol --create-package --in-place --python-out server/generated/ protoc --proto-path=protos/
|
|
||||||
|
find protos/ -type f -name "*.proto" | xargs uv run protol --create-package --in-place --python-out generated/ protoc --proto-path=protos/
|
||||||
|
|
||||||
frontend/src/grpc: protos/*.proto
|
frontend/src/grpc: protos/*.proto
|
||||||
mkdir -p frontend/src/grpc
|
mkdir -p frontend/src/grpc
|
||||||
|
|||||||
20
where_fi/visualise/extract/__init__.py
Normal file
20
where_fi/visualise/extract/__init__.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import grpc
|
||||||
|
|
||||||
|
from ..generated import figure_pb2, figure_pb2_grpc
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
# NOTE(gRPC Python Team): .close() is possible on a channel and should be
|
||||||
|
# used in circumstances in which the with statement does not fit the needs
|
||||||
|
# of the code.
|
||||||
|
with grpc.insecure_channel("localhost:50051") as channel:
|
||||||
|
stub = figure_pb2_grpc.FigureServiceStub(channel)
|
||||||
|
for figure in stub.GetFigure(figure_pb2.FigureRequest()):
|
||||||
|
print(f"Figure: {figure}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig()
|
||||||
|
run()
|
||||||
89
where_fi/visualise/extract/__main__.py
Normal file
89
where_fi/visualise/extract/__main__.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import grpc
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from ..generated import figure_pb2, figure_pb2_grpc
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def list() -> None:
|
||||||
|
"""
|
||||||
|
List all figures from the gRPC service.
|
||||||
|
"""
|
||||||
|
print("Connecting to the server...")
|
||||||
|
with grpc.insecure_channel("localhost:50051") as channel:
|
||||||
|
stub = figure_pb2_grpc.FigureServiceStub(channel)
|
||||||
|
print("Retrieving available figures...\n")
|
||||||
|
figures = [x for x in stub.GetFigure(figure_pb2.FigureRequest())]
|
||||||
|
|
||||||
|
if not figures:
|
||||||
|
print("No figures found.")
|
||||||
|
else:
|
||||||
|
for i, figure in enumerate(figures, 1):
|
||||||
|
print(f"{i}. Figure: {figure.title} (ID: {figure.uuid})")
|
||||||
|
|
||||||
|
choice = typer.prompt("Which figure to extract?", type=int)
|
||||||
|
assert isinstance(choice, int)
|
||||||
|
if 1 <= choice <= len(figures):
|
||||||
|
selected_figure = figures[choice - 1]
|
||||||
|
print(f"You selected: {selected_figure.title}")
|
||||||
|
extract(selected_figure.uuid)
|
||||||
|
else:
|
||||||
|
print("Invalid selection. Exiting.")
|
||||||
|
|
||||||
|
|
||||||
|
def get_figure(uuid: str) -> tuple[figure_pb2.Figure, figure_pb2.FigureData]:
|
||||||
|
"""
|
||||||
|
Get the figure data for a specific UUID.
|
||||||
|
"""
|
||||||
|
print(f"Connecting to the server to extract data for UUID: {uuid}...")
|
||||||
|
with grpc.insecure_channel("localhost:50051") as channel:
|
||||||
|
stub = figure_pb2_grpc.FigureServiceStub(channel)
|
||||||
|
all_figures = {x.uuid: x for x in stub.GetFigure(figure_pb2.FigureRequest())}
|
||||||
|
figure_data = stub.GetFigureUpdate(figure_pb2.FigureDataRequest(uuid=uuid))
|
||||||
|
for data in figure_data:
|
||||||
|
return (all_figures[uuid], data)
|
||||||
|
raise ValueError(f"Figure with UUID {uuid} not found.")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def extract(uuid: str, output: Path | None = None) -> None:
|
||||||
|
"""
|
||||||
|
Extract data for a specific figure identified by its UUID.
|
||||||
|
"""
|
||||||
|
_, figure = get_figure(uuid)
|
||||||
|
if not output:
|
||||||
|
output = cast(Path, typer.prompt("Enter output file path:", type=Path))
|
||||||
|
with open(output, "wb") as f:
|
||||||
|
f.write(figure.SerializeToString())
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def plot(uuid: str) -> None:
|
||||||
|
"""
|
||||||
|
Plot the figure data for a specific UUID.
|
||||||
|
"""
|
||||||
|
figure, data = get_figure(uuid)
|
||||||
|
if data.line:
|
||||||
|
for line in data.line.lines:
|
||||||
|
plt.plot(
|
||||||
|
line.x if line.x else np.arange(len(line.y)), line.y, label=line.label
|
||||||
|
)
|
||||||
|
plt.xlabel(figure.x_label)
|
||||||
|
plt.ylabel(figure.y_label)
|
||||||
|
plt.title(figure.title)
|
||||||
|
elif figure.heatmap:
|
||||||
|
plt.imshow(figure.heatmap.data, cmap="hot", interpolation="nearest")
|
||||||
|
elif figure.histogram:
|
||||||
|
plt.hist(figure.histogram.data, bins=figure.histogram.bins)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
@ -11,8 +11,8 @@ import grpc
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import numpy.typing as npt
|
import numpy.typing as npt
|
||||||
|
|
||||||
|
from ..generated import figure_pb2, figure_pb2_grpc
|
||||||
from . import figures
|
from . import figures
|
||||||
from .generated import figure_pb2, figure_pb2_grpc
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -22,6 +22,11 @@ class VisualiserData:
|
|||||||
|
|
||||||
|
|
||||||
class FigureServer(figure_pb2_grpc.FigureServiceServicer):
|
class FigureServer(figure_pb2_grpc.FigureServiceServicer):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self.clients: dict[str, list[queue.Queue[figure_pb2.FigureData]]] = {}
|
||||||
|
self.clients_lock = threading.Lock()
|
||||||
|
|
||||||
def GetFigure(
|
def GetFigure(
|
||||||
self, request: figure_pb2.FigureRequest, context: grpc.ServicerContext
|
self, request: figure_pb2.FigureRequest, context: grpc.ServicerContext
|
||||||
) -> Generator[figure_pb2.Figure, None, None]:
|
) -> Generator[figure_pb2.Figure, None, None]:
|
||||||
@ -32,15 +37,15 @@ class FigureServer(figure_pb2_grpc.FigureServiceServicer):
|
|||||||
def GetFigureUpdate(
|
def GetFigureUpdate(
|
||||||
self, request: figure_pb2.FigureDataRequest, context: grpc.ServicerContext
|
self, request: figure_pb2.FigureDataRequest, context: grpc.ServicerContext
|
||||||
) -> Generator[figure_pb2.FigureData, None, None]:
|
) -> Generator[figure_pb2.FigureData, None, None]:
|
||||||
logger.info(
|
self.logger.info(
|
||||||
f"Received request for figure data stream for figure {request.uuid}"
|
f"Received request for figure data stream for figure {request.uuid}"
|
||||||
)
|
)
|
||||||
|
|
||||||
with clients_lock:
|
with self.clients_lock:
|
||||||
q: queue.Queue[figure_pb2.FigureData] = queue.Queue()
|
q: queue.Queue[figure_pb2.FigureData] = queue.Queue()
|
||||||
if request.uuid not in clients:
|
if request.uuid not in self.clients:
|
||||||
clients[request.uuid] = []
|
self.clients[request.uuid] = []
|
||||||
clients[request.uuid].append(q)
|
self.clients[request.uuid].append(q)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while context.is_active():
|
while context.is_active():
|
||||||
@ -49,24 +54,22 @@ class FigureServer(figure_pb2_grpc.FigureServiceServicer):
|
|||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
with clients_lock:
|
with self.clients_lock:
|
||||||
clients[request.uuid].remove(q)
|
self.clients[request.uuid].remove(q)
|
||||||
|
|
||||||
|
|
||||||
class Webapp:
|
class Webapp:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
self.active = True
|
self.active = True
|
||||||
|
self.figure_server = FigureServer()
|
||||||
self.clients: dict[str, list[queue.Queue[figure_pb2.FigureData]]] = {}
|
|
||||||
self.clients_lock = threading.Lock()
|
|
||||||
|
|
||||||
def add_data(
|
def add_data(
|
||||||
self, dtype: figures.Figure, new_data: npt.NDArray[np.complex128]
|
self, dtype: figures.Figure, new_data: npt.NDArray[np.complex128]
|
||||||
) -> None:
|
) -> None:
|
||||||
updates = figures.all_figures[dtype].update(new_data)
|
updates = figures.all_figures[dtype].update(new_data)
|
||||||
for fig_id, update in updates.items():
|
for fig_id, update in updates.items():
|
||||||
for client in self.clients.get(fig_id, []):
|
for client in self.figure_server.clients.get(fig_id, []):
|
||||||
client.put(update)
|
client.put(update)
|
||||||
|
|
||||||
def listen_for_data(self, data_queue: "mp.Queue[VisualiserData]") -> None:
|
def listen_for_data(self, data_queue: "mp.Queue[VisualiserData]") -> None:
|
||||||
@ -82,12 +85,14 @@ class Webapp:
|
|||||||
data = data_queue.get()
|
data = data_queue.get()
|
||||||
|
|
||||||
def start(self, data_queue: "mp.Queue[VisualiserData]") -> None:
|
def start(self, data_queue: "mp.Queue[VisualiserData]") -> None:
|
||||||
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
||||||
figure_pb2_grpc.add_FigureServiceServicer_to_server(FigureServer(), server)
|
figure_pb2_grpc.add_FigureServiceServicer_to_server(
|
||||||
server.add_insecure_port("[::]:50051")
|
self.figure_server, grpc_server
|
||||||
server.add_insecure_port("0.0.0.0:50051")
|
)
|
||||||
|
grpc_server.add_insecure_port("[::]:50051")
|
||||||
|
grpc_server.add_insecure_port("0.0.0.0:50051")
|
||||||
self.logger.info("Starting server on port 50051")
|
self.logger.info("Starting server on port 50051")
|
||||||
server.start()
|
grpc_server.start()
|
||||||
self.logger.info("Server started")
|
self.logger.info("Server started")
|
||||||
|
|
||||||
data_thread = threading.Thread(target=self.listen_for_data, args=(data_queue,))
|
data_thread = threading.Thread(target=self.listen_for_data, args=(data_queue,))
|
||||||
@ -96,5 +101,5 @@ class Webapp:
|
|||||||
while self.active:
|
while self.active:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
self.logger.debug("Server is running")
|
self.logger.debug("Server is running")
|
||||||
server.stop(0.5)
|
grpc_server.stop(0.5)
|
||||||
data_thread.join()
|
data_thread.join()
|
||||||
|
|||||||
@ -79,6 +79,30 @@ class SimpleLineChart:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PerSubcarrierFigure(SpecificFigure):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
figures: Sequence[SimpleLineChart],
|
||||||
|
funcs: Sequence[Callable[[npt.NDArray[Any]], npt.NDArray[Any]]],
|
||||||
|
) -> None:
|
||||||
|
self.charts = figures
|
||||||
|
self.figures = [figure.figure for figure in figures]
|
||||||
|
self.funcs = funcs
|
||||||
|
|
||||||
|
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
||||||
|
"""
|
||||||
|
Update the figure with new data.
|
||||||
|
|
||||||
|
The data is expected to be in the shape (subcarriers, data).
|
||||||
|
"""
|
||||||
|
subcarrier_labels = [f"Subcarrier {i}" for i in range(new_data.shape[0])]
|
||||||
|
updates = [
|
||||||
|
figure.update(func(new_data), labels=subcarrier_labels)
|
||||||
|
for func, figure in zip(self.funcs, self.charts, strict=True)
|
||||||
|
]
|
||||||
|
return reduce((lambda a, b: a | b), updates)
|
||||||
|
|
||||||
|
|
||||||
class PerAntennaFigure(SpecificFigure):
|
class PerAntennaFigure(SpecificFigure):
|
||||||
"""A figure that plots data for each antenna separately.
|
"""A figure that plots data for each antenna separately.
|
||||||
|
|
||||||
@ -165,19 +189,81 @@ class HeatmapFigure(SpecificFigure):
|
|||||||
|
|
||||||
class EmpiricalCDF(SpecificFigure):
|
class EmpiricalCDF(SpecificFigure):
|
||||||
"""Helper class to create a graph of the empirical cumulative distribution function
|
"""Helper class to create a graph of the empirical cumulative distribution function
|
||||||
of a set of observations.
|
and probability density functions of a set of observations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, title: str, x_label: str) -> None:
|
def __init__(self, title: str, x_label: str) -> None:
|
||||||
self.chart = SimpleLineChart(title, x_label, "Cumulative Probability")
|
self.chart = SimpleLineChart(title, x_label, "Cumulative Probability")
|
||||||
self.figures = [self.chart.figure]
|
self.figures = [self.chart.figure]
|
||||||
|
|
||||||
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
def update(self, new_data: npt.NDArray[np.float64]) -> FigureUpdate:
|
||||||
new_data = np.expand_dims(np.sort(new_data), 0)
|
new_data = np.sort(new_data, axis=-1)
|
||||||
y_values = np.expand_dims(np.linspace(0, 1, new_data.size), 0)
|
if new_data.ndim == 1:
|
||||||
|
new_data = np.expand_dims(new_data, 0)
|
||||||
|
y_values = np.repeat(
|
||||||
|
np.linspace(0, 1, new_data.shape[-1])[np.newaxis, :],
|
||||||
|
new_data.shape[0],
|
||||||
|
axis=0,
|
||||||
|
)
|
||||||
return self.chart.update(y_values, labels=["Frequency"], x_values=new_data)
|
return self.chart.update(y_values, labels=["Frequency"], x_values=new_data)
|
||||||
|
|
||||||
|
|
||||||
|
class RandomVariable(SpecificFigure):
|
||||||
|
def get_cdf(
|
||||||
|
self, new_data: npt.NDArray[np.float64]
|
||||||
|
) -> tuple[npt.NDArray[np.floating[Any]], npt.NDArray[np.floating[Any]]]:
|
||||||
|
"""Get the cumulative distribution function of the data.
|
||||||
|
|
||||||
|
The data is expected to be in the shape (lines, data) or (data,) for single-line
|
||||||
|
charts.
|
||||||
|
"""
|
||||||
|
new_data = np.sort(new_data, axis=-1)
|
||||||
|
if new_data.ndim == 1:
|
||||||
|
new_data = np.expand_dims(new_data, 0)
|
||||||
|
y_values = np.expand_dims(np.linspace(0, 1, new_data.size), 0)
|
||||||
|
else:
|
||||||
|
y_values = np.repeat(
|
||||||
|
np.linspace(0, 1, new_data.shape[-1])[np.newaxis, :],
|
||||||
|
new_data.shape[0],
|
||||||
|
axis=0,
|
||||||
|
)
|
||||||
|
return new_data, y_values
|
||||||
|
|
||||||
|
def __init__(self, x_label: str, line_type: str = "Subcarrier") -> None:
|
||||||
|
self.charts = [
|
||||||
|
# SimpleLineChart(
|
||||||
|
# f"{x_label} Probability Density", x_label, "Probability Density"
|
||||||
|
# ),
|
||||||
|
SimpleLineChart(
|
||||||
|
f"{x_label} Cumulative Distribution", x_label, "Cumulative Probability"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
self.funcs = [
|
||||||
|
# self.get_pdf,
|
||||||
|
self.get_cdf
|
||||||
|
]
|
||||||
|
self.figures = [figure.figure for figure in self.charts]
|
||||||
|
|
||||||
|
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
||||||
|
"""
|
||||||
|
Update the figure with new data.
|
||||||
|
|
||||||
|
The data is expected to be in the shape (subcarriers, data).
|
||||||
|
"""
|
||||||
|
subcarrier_labels = [f"Subcarrier {i}" for i in range(new_data.shape[0])]
|
||||||
|
updates: list[FigureUpdate] = []
|
||||||
|
for func, figure in zip(self.funcs, self.charts, strict=True):
|
||||||
|
x, y = func(new_data)
|
||||||
|
updates.append(
|
||||||
|
figure.update(
|
||||||
|
y,
|
||||||
|
labels=subcarrier_labels,
|
||||||
|
x_values=x,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return reduce((lambda a, b: a | b), updates)
|
||||||
|
|
||||||
|
|
||||||
class Figure(Enum):
|
class Figure(Enum):
|
||||||
RAW_CSI = 0
|
RAW_CSI = 0
|
||||||
UNWRAPPED_PHASE = 1
|
UNWRAPPED_PHASE = 1
|
||||||
@ -210,5 +296,5 @@ all_figures = {
|
|||||||
),
|
),
|
||||||
Figure.MUSIC_EIGENVALUES: MusicEigenvalueHistogram(),
|
Figure.MUSIC_EIGENVALUES: MusicEigenvalueHistogram(),
|
||||||
Figure.AOA_HEATMAP: HeatmapFigure(),
|
Figure.AOA_HEATMAP: HeatmapFigure(),
|
||||||
Figure.PHASE_ANALYSIS: EmpiricalCDF("Phase Analysis", "Phase"),
|
Figure.PHASE_ANALYSIS: RandomVariable("Phase"),
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user