Compare commits

...

3 Commits

Author SHA1 Message Date
Christos Falas
c4196503d8
improve antenna order detection 2025-02-24 15:02:21 +00:00
Christos Falas
28d096af44
unwrap CSI phase 2025-02-24 15:02:21 +00:00
Christos Falas
61be734752
Add heatmap visualisation 2025-02-24 15:02:15 +00:00
7 changed files with 93 additions and 31 deletions

View File

@ -52,7 +52,7 @@ def heatmap() -> None:
def callback(antenna_data: npt.NDArray[np.complex64]) -> None: def callback(antenna_data: npt.NDArray[np.complex64]) -> None:
logger.info(f"Got final CSI data with shape {antenna_data.shape}") logger.info(f"Got final CSI data with shape {antenna_data.shape}")
visualise_data(antenna_data, visualise.DataType.RAW_CSI) visualise_data(antenna_data, visualise.DataType.RAW_CSI)
processed = preprocessor.preprocess(antenna_data) processed = preprocessor.preprocess(antenna_data, visualiser=visualise_data)
visualise_data(processed, visualise.DataType.PROCESSED_CSI) visualise_data(processed, visualise.DataType.PROCESSED_CSI)
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)

View File

@ -1,11 +1,13 @@
import logging import logging
from queue import Queue from queue import Queue
from typing import Any, Callable
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
from scipy.signal import butter, correlate, sosfilt, sosfilt_zi from scipy.signal import butter, correlate, sosfilt, sosfilt_zi
from ..config import config from ..config import config
from ..visualise import server as visualise
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@ -26,9 +28,14 @@ class Preprocessor:
output="sos", output="sos",
) )
def preprocess(self, h: npt.NDArray[np.complex64]) -> npt.NDArray[np.complex64]: def preprocess(
self,
h: npt.NDArray[np.complex64],
visualiser: None
| Callable[[npt.NDArray[Any], visualise.DataType], None] = None,
) -> npt.NDArray[np.complex64]:
# CSI data is not available for pilot subcarriers. # CSI data is not available for pilot subcarriers.
h_hat = np.where( h_hat: npt.NDArray[np.complex64] = np.where(
np.expand_dims(h[:, 0, 0] == 0, axis=(1, 2)), np.expand_dims(h[:, 0, 0] == 0, axis=(1, 2)),
correlate(h, [[[1 / 2]], [[0]], [[1 / 2]]], mode="same"), correlate(h, [[[1 / 2]], [[0]], [[1 / 2]]], mode="same"),
h, h,
@ -38,12 +45,33 @@ class Preprocessor:
h_hat = h_hat[:: config.preprocessing.subcarrier_step, :, :] h_hat = h_hat[:: config.preprocessing.subcarrier_step, :, :]
# logger.info(f"CSI shape: {h_hat.shape}") # logger.info(f"CSI shape: {h_hat.shape}")
h_hat = np.multiply(h_hat, h_hat.conj() / abs(h_hat.conj())) # h_hat = np.multiply(h_hat, h_hat.conj() / abs(h_hat.conj()))
h_hat = np.nan_to_num(h_hat) h_hat = np.nan_to_num(h_hat)
# h_hat = correlate(h_hat, np.ones((3, 1, 1)) / 3) # h_hat = correlate(h_hat, np.ones((3, 1, 1)) / 3)
h_hat = correlate(h_hat, [[[1 / 4]], [[1 / 2]], [[1 / 4]]], mode="valid") # h_hat = correlate(h_hat, [[[1 / 4]], [[1 / 2]], [[1 / 4]]], mode="valid")
# Unwrap phase and remove linear fit
print(h_hat.shape)
unwrapped = np.unwrap(np.angle(h_hat[:, :, 0]), axis=0).reshape(
h_hat.shape[0], h_hat.shape[1], 1
)
if visualiser:
visualiser(unwrapped, visualise.DataType.UNWRAPPED_PHASE)
for antenna in range(h_hat.shape[1]):
tau, rho = np.linalg.lstsq(
np.vstack([np.arange(h_hat.shape[0]), np.ones(h_hat.shape[0])]).T,
unwrapped[:, antenna, 0],
)[0]
h_hat[:, antenna, 0] = np.abs(h_hat[:, antenna, 0]) * np.exp(
1j
* (
np.angle(h_hat[:, antenna, 0])
- (tau * np.arange(h_hat.shape[0]) + rho)
)
)
return h_hat
# Assume that all csi matrices will have the same shape # Assume that all csi matrices will have the same shape
if self.long_term_avg.shape != h_hat.shape: if self.long_term_avg.shape != h_hat.shape:
self.long_term_avg = np.zeros(h_hat.shape, dtype=np.complex64) self.long_term_avg = np.zeros(h_hat.shape, dtype=np.complex64)

View File

@ -11,13 +11,13 @@ AntennaIdentifier = tuple[ingest.Host, int]
order: list[AntennaIdentifier] = [] order: list[AntennaIdentifier] = []
prev_unplugged: set[AntennaIdentifier] = set() prev_unplugged: set[AntennaIdentifier] = set()
long_antenna_average: dict[AntennaIdentifier, deque[int]] = { long_antenna_average: dict[AntennaIdentifier, deque[int]] = {
(host, 0): deque(maxlen=10 * config.sample_rate) for host in config.receive_hosts (host, 0): deque(maxlen=15 * config.sample_rate) for host in config.receive_hosts
} | {(host, 1): deque(maxlen=10 * config.sample_rate) for host in config.receive_hosts} } | {(host, 1): deque(maxlen=15 * config.sample_rate) for host in config.receive_hosts}
short_antenna_average: dict[AntennaIdentifier, deque[int]] = { short_antenna_average: dict[AntennaIdentifier, deque[int]] = {
(host, 0): deque(maxlen=config.sample_rate) for host in config.receive_hosts (host, 0): deque(maxlen=2 * config.sample_rate) for host in config.receive_hosts
} | {(host, 1): deque(maxlen=config.sample_rate) for host in config.receive_hosts} } | {(host, 1): deque(maxlen=2 * config.sample_rate) for host in config.receive_hosts}
RSSI_THRESHOLD = 8 RSSI_THRESHOLD = 10
def average(data: Collection[Any]) -> float: def average(data: Collection[Any]) -> float:

View File

@ -6,4 +6,5 @@ server/generated: protos/*.proto
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 server/generated/ protoc --proto-path=protos/
frontend/src/grpc: protos/*.proto frontend/src/grpc: protos/*.proto
find protos/ -name "*.proto" | xargs npm exec --prefix frontend/ protoc --ts_out frontend/src/grpc -I protos/ mkdir -p frontend/src/grpc
cd frontend && find ../protos/ -name "*.proto" | xargs npx protoc --ts_out=src/grpc -I../protos/

View File

@ -11,6 +11,18 @@ import { computed } from 'vue'
const { figure, paused = false } = defineProps<{ figure: Figure; paused?: boolean }>() const { figure, paused = false } = defineProps<{ figure: Figure; paused?: boolean }>()
const cancel = ref<boolean>(false) const cancel = ref<boolean>(false)
function reshape(data: number[], width: number) {
if (data.length % width != 0) {
throw new Error('Data length is not divisible by width')
}
const height = data.length / width
const result: number[][] = new Array(height)
for (let i = 0; i < height; i++) {
result[i] = data.slice(i * width, (i + 1) * width)
}
return result
}
const data = ref<FigureData | null>(null) const data = ref<FigureData | null>(null)
const plotData = computed(() => { const plotData = computed(() => {
if (data.value) { if (data.value) {
@ -24,16 +36,12 @@ const plotData = computed(() => {
color: line.color ? line.color : undefined, color: line.color ? line.color : undefined,
})) }))
case 'heatmap': case 'heatmap':
return [] return [
/* data.value.figure.heatmap.lines.map((line) => { {
return { z: reshape(data.value.figure.heatmap.data, data.value.figure.heatmap.width),
x: line.x, type: 'heatmap' as const,
y: line.y, },
type: 'scatter', ]
name: line.label,
color: line.color,
}
})*/
} }
} }
return [] return []
@ -43,6 +51,7 @@ const layout = {
title: { text: figure.title }, title: { text: figure.title },
xaxis: { title: { text: figure.xLabel } }, xaxis: { title: { text: figure.xLabel } },
yaxis: { title: { text: figure.yLabel } }, yaxis: { title: { text: figure.yLabel } },
height: 700,
} }
function updateGraph() { function updateGraph() {

View File

@ -2,9 +2,8 @@ syntax = "proto3";
message HeatmapData { message HeatmapData {
string uuid = 1; string uuid = 1;
repeated float x = 2; repeated float data = 2;
repeated float y = 3; uint32 width = 3;
uint32 width = 4; uint32 height = 4;
uint32 height = 5; string cmap = 5;
string cmap = 6;
} }

View File

@ -23,8 +23,9 @@ clients_lock = threading.Lock()
class DataType(Enum): class DataType(Enum):
RAW_CSI = 1 RAW_CSI = 1
PROCESSED_CSI = 2 UNWRAPPED_PHASE = 2
HEATMAP = 3 PROCESSED_CSI = 3
HEATMAP = 4
@dataclass @dataclass
@ -40,6 +41,12 @@ figures = {
x_label="Subcarrier", x_label="Subcarrier",
y_label="Phase", y_label="Phase",
), ),
"unwrapped_phase": figure_pb2.Figure(
uuid=str(uuid.uuid4()),
title="Unwrapped CSI Phase",
x_label="Subcarrier",
y_label="Unwrapped phase",
),
"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",
@ -48,9 +55,9 @@ figures = {
), ),
"aoa_heatmap": figure_pb2.Figure( "aoa_heatmap": figure_pb2.Figure(
uuid=str(uuid.uuid4()), uuid=str(uuid.uuid4()),
title="Preprocessed CSI Phase", title="AoA Heatmap",
x_label="Subcarrier", x_label="ToF",
y_label="Phase", y_label="AoA",
), ),
} }
@ -104,7 +111,25 @@ def add_data(dtype: DataType, new_data: npt.NDArray[np.complex128]) -> None:
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.HEATMAP: case DataType.HEATMAP:
uuid = figures["aoa_heatmap"].uuid
heatmap = heatmap_pb2.HeatmapData(
uuid=uuid,
data=new_data.flatten(),
width=new_data.shape[1],
height=new_data.shape[0],
)
for q in clients.get(uuid, []):
q.put(figure_pb2.FigureData(uuid=uuid, heatmap=heatmap))
pass pass
case DataType.UNWRAPPED_PHASE:
uuid = figures["unwrapped_phase"].uuid
lines = [
line_pb2.LineChartData.Line(y=new_data[:, i, 0], label=f"Antenna {i}")
for i in range(new_data.shape[1])
]
linechart = line_pb2.LineChartData(lines=lines)
for q in clients.get(uuid, []):
q.put(figure_pb2.FigureData(uuid=uuid, line=linechart))
def listen_for_data(data_queue: "mp.Queue[VisualiserData]") -> None: def listen_for_data(data_queue: "mp.Queue[VisualiserData]") -> None: