Skip to content

Use the RF Xarray

Most readers of this documentation should start here:

  • you already have a processed RF .nc file
  • you may also have the processed acoustic companion .nc file
  • you want to inspect rover positions, CSI, and acoustic chirps
  • you do not need the NAS, the raw RF result files, or the raw acoustic recordings

The tutorial track assumes that the difficult part is already done: the datasets have already been extracted and filtered into NetCDF/xarray files such as:

results/csi_<experiment_id>.nc
results/acoustic_<experiment_id>.nc

There are two companion processed datasets:

  • results/csi_<experiment_id>.nc is the RF dataset
  • results/acoustic_<experiment_id>.nc is the acoustic dataset

They share the same measurement keys, experiment_id and cycle_id, so they can be joined back to the same physical rover stop.

The RF dataset contains the complex wireless channel between the transmitter on the rover and the active ceiling receiver entries selected by hostname.

The acoustic dataset contains the raw sampled received chirp recorded at the microphones after the chirp was sent by the omnidirectional speaker.

So the split is:

  • RF .nc: complex channel information plus the joined rover pose
  • acoustic .nc: raw sampled chirp waveforms for the same measurement points

If you are using the helper module from the notebooks:

from processing.tutorials import csi_plot_utils as csi
ds, dataset_path = csi.open_dataset(dataset_path="results/csi_EXP003.nc")
print(dataset_path)

If you want the lowest-level xarray entry point:

import xarray as xr
ds = xr.open_dataset("results/csi_EXP003.nc")

The acoustic companion file is opened directly with xarray:

import xarray as xr
acoustic_ds = xr.open_dataset("results/acoustic_EXP003.nc")

The processed RF NetCDF stores:

  • coordinates:
    • experiment_id
    • cycle_id
    • hostname
  • rover variables with shape (experiment_id, cycle_id):
    • rover_x
    • rover_y
    • rover_z
    • position_available
  • RF variables with shape (experiment_id, cycle_id, hostname):
    • csi_real
    • csi_imag
    • csi_available

This is the main physical meaning of the RF file:

  • one (experiment_id, cycle_id) pair identifies one rover stop
  • the same pair gives one complex RF response across the active ceiling receiver entries
  • csi_real + 1j * csi_imag reconstructs the complex channel quantity for those receiver entries

The quickest sanity checks are:

ds.sizes
ds.coords
ds.data_vars
ds.attrs

The processed acoustic NetCDF stores:

  • coordinates:
    • experiment_id
    • cycle_id
    • microphone_label
    • sample_index
  • acoustic data variable with shape (experiment_id, cycle_id, microphone_label, sample_index):
    • values

This is the main physical meaning of the acoustic file:

  • one (experiment_id, cycle_id) pair identifies one rover stop
  • values contains the raw sampled received chirp at that stop
  • microphone_label selects which microphone channel recorded that chirp
  • the chirp was emitted by the omnidirectional speaker and captured without collapsing it into a single RF-style channel coefficient

Useful first checks are:

acoustic_ds.sizes
acoustic_ds.coords
acoustic_ds.data_vars
acoustic_ds.attrs

The stored CSI components are split into real and imaginary parts:

csi = ds["csi_real"] + 1j * ds["csi_imag"]
phase = np.angle(csi)
amplitude = np.abs(csi)

The RF and acoustic files are not duplicates of each other.

  • the RF file tells you the complex channel from the transmitter to the ceiling receivers
  • the acoustic file tells you the raw received chirp waveforms from the omnidirectional speaker
  • the shared keys experiment_id and cycle_id let you align both with the same rover position

Typical workflow:

  • select the rover stop from the RF dataset
  • use the same (experiment_id, cycle_id) in the acoustic dataset
  • compare RF channel behavior and acoustic waveforms for that one physical location
  • experiment_id selects one logical run
  • cycle_id selects one physical rover stop inside that run
  • hostname selects one RF receiver host or tile

That means:

  • one (experiment_id, cycle_id) pair gives you one rover pose
  • the same pair also gives you one CSI vector across the active hostnames
  • selecting one experiment_id leaves a cycle_id x hostname RF slice
  • cycle_id is a shared sparse axis across experiments
  • the xarray indexes are the named coordinates experiment_id, cycle_id, and hostname
  • csi_available is the authoritative mask for whether a host/cycle entry exists
  • rover coordinates can still be NaN, so always check position_available and finite coordinates before using a pose
  • some original rover stops may be absent because duplicate consecutive positions are filtered before the dataset is written

Safe starting pattern:

exp = ds.sel(experiment_id="EXP003")
cycle_mask = exp["csi_available"].any(dim="hostname")
cycle_ids = exp["cycle_id"].values[cycle_mask.values]
csi = (exp["csi_real"] + 1j * exp["csi_imag"]).sel(cycle_id=cycle_ids)
position_ok = (
exp["position_available"].sel(cycle_id=cycle_ids) > 0
) & (
np.isfinite(exp["rover_x"].sel(cycle_id=cycle_ids))
) & (
np.isfinite(exp["rover_y"].sel(cycle_id=cycle_ids))
) & (
np.isfinite(exp["rover_z"].sel(cycle_id=cycle_ids))
)

If You Need Provenance Or Extraction Details

Section titled “If You Need Provenance Or Extraction Details”

Most users can stop here and move on to the notebooks.

If you are one of the maintainers who needs to know how these .nc files were built from raw RF logs, rover positions, and acoustic captures, see:

Once you have a .nc file, continue with: