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
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
all: server/generated frontend/src/grpc
|
||||
all: generated frontend/src/grpc
|
||||
|
||||
server/generated: protos/*.proto
|
||||
mkdir -p server/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 protol --create-package --in-place --python-out server/generated/ protoc --proto-path=protos/
|
||||
generated: protos/*.proto
|
||||
mkdir -p 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 generated/ protoc --proto-path=protos/
|
||||
|
||||
frontend/src/grpc: protos/*.proto
|
||||
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.typing as npt
|
||||
|
||||
from ..generated import figure_pb2, figure_pb2_grpc
|
||||
from . import figures
|
||||
from .generated import figure_pb2, figure_pb2_grpc
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -22,6 +22,11 @@ class VisualiserData:
|
||||
|
||||
|
||||
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(
|
||||
self, request: figure_pb2.FigureRequest, context: grpc.ServicerContext
|
||||
) -> Generator[figure_pb2.Figure, None, None]:
|
||||
@ -32,15 +37,15 @@ class FigureServer(figure_pb2_grpc.FigureServiceServicer):
|
||||
def GetFigureUpdate(
|
||||
self, request: figure_pb2.FigureDataRequest, context: grpc.ServicerContext
|
||||
) -> Generator[figure_pb2.FigureData, None, None]:
|
||||
logger.info(
|
||||
self.logger.info(
|
||||
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()
|
||||
if request.uuid not in clients:
|
||||
clients[request.uuid] = []
|
||||
clients[request.uuid].append(q)
|
||||
if request.uuid not in self.clients:
|
||||
self.clients[request.uuid] = []
|
||||
self.clients[request.uuid].append(q)
|
||||
|
||||
try:
|
||||
while context.is_active():
|
||||
@ -49,24 +54,22 @@ class FigureServer(figure_pb2_grpc.FigureServiceServicer):
|
||||
except queue.Empty:
|
||||
pass
|
||||
finally:
|
||||
with clients_lock:
|
||||
clients[request.uuid].remove(q)
|
||||
with self.clients_lock:
|
||||
self.clients[request.uuid].remove(q)
|
||||
|
||||
|
||||
class Webapp:
|
||||
def __init__(self) -> None:
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.active = True
|
||||
|
||||
self.clients: dict[str, list[queue.Queue[figure_pb2.FigureData]]] = {}
|
||||
self.clients_lock = threading.Lock()
|
||||
self.figure_server = FigureServer()
|
||||
|
||||
def add_data(
|
||||
self, dtype: figures.Figure, new_data: npt.NDArray[np.complex128]
|
||||
) -> None:
|
||||
updates = figures.all_figures[dtype].update(new_data)
|
||||
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)
|
||||
|
||||
def listen_for_data(self, data_queue: "mp.Queue[VisualiserData]") -> None:
|
||||
@ -82,12 +85,14 @@ class Webapp:
|
||||
data = data_queue.get()
|
||||
|
||||
def start(self, data_queue: "mp.Queue[VisualiserData]") -> None:
|
||||
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
||||
figure_pb2_grpc.add_FigureServiceServicer_to_server(FigureServer(), server)
|
||||
server.add_insecure_port("[::]:50051")
|
||||
server.add_insecure_port("0.0.0.0:50051")
|
||||
grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
||||
figure_pb2_grpc.add_FigureServiceServicer_to_server(
|
||||
self.figure_server, grpc_server
|
||||
)
|
||||
grpc_server.add_insecure_port("[::]:50051")
|
||||
grpc_server.add_insecure_port("0.0.0.0:50051")
|
||||
self.logger.info("Starting server on port 50051")
|
||||
server.start()
|
||||
grpc_server.start()
|
||||
self.logger.info("Server started")
|
||||
|
||||
data_thread = threading.Thread(target=self.listen_for_data, args=(data_queue,))
|
||||
@ -96,5 +101,5 @@ class Webapp:
|
||||
while self.active:
|
||||
time.sleep(1)
|
||||
self.logger.debug("Server is running")
|
||||
server.stop(0.5)
|
||||
grpc_server.stop(0.5)
|
||||
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):
|
||||
"""A figure that plots data for each antenna separately.
|
||||
|
||||
@ -165,19 +189,81 @@ class HeatmapFigure(SpecificFigure):
|
||||
|
||||
class EmpiricalCDF(SpecificFigure):
|
||||
"""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:
|
||||
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)
|
||||
def update(self, new_data: npt.NDArray[np.float64]) -> FigureUpdate:
|
||||
new_data = np.sort(new_data, axis=-1)
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
RAW_CSI = 0
|
||||
UNWRAPPED_PHASE = 1
|
||||
@ -210,5 +296,5 @@ all_figures = {
|
||||
),
|
||||
Figure.MUSIC_EIGENVALUES: MusicEigenvalueHistogram(),
|
||||
Figure.AOA_HEATMAP: HeatmapFigure(),
|
||||
Figure.PHASE_ANALYSIS: EmpiricalCDF("Phase Analysis", "Phase"),
|
||||
Figure.PHASE_ANALYSIS: RandomVariable("Phase"),
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user