Tutorial: Export CSI Movies¶
This notebook exports two MP4 files from the processed RF xarray:
- a phase movie on the antenna plane
- a power movie on the antenna plane
Each frame corresponds to one valid (experiment_id, cycle_id) pair. When you select multiple experiment IDs, the notebook merges them into one measurement sequence and can sample frames evenly to keep the video length manageable.
Use this notebook when you already have the processed .nc file and want a compact visual summary of how the CSI changes as the rover moves through the room.
# Optional: uncomment when this Jupyter kernel misses the plotting dependencies.
# import sys
# !{sys.executable} -m pip install matplotlib numpy requests xarray pyyaml
from pathlib import Path
import importlib.util
import sys
NOTEBOOK_DIR = Path.cwd().resolve()
for candidate_dir in (
NOTEBOOK_DIR,
NOTEBOOK_DIR / "tutorials",
NOTEBOOK_DIR / "processing" / "tutorials",
):
if (candidate_dir / "csi_plot_utils.py").exists():
NOTEBOOK_DIR = candidate_dir.resolve()
break
else:
raise ImportError(f"Could not locate csi_plot_utils.py from {Path.cwd().resolve()}")
UTILS_PATH = NOTEBOOK_DIR / "csi_plot_utils.py"
PROCESSING_DIR = NOTEBOOK_DIR.parent
PROJECT_ROOT = PROCESSING_DIR.parent
REPO_ROOT = PROJECT_ROOT
spec = importlib.util.spec_from_file_location("csi_plot_utils", UTILS_PATH)
csi = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = csi
spec.loader.exec_module(csi)
REQUESTED_EXPERIMENT_IDS = None # Example: ["EXP003", "EXP005"]. None means: use all experiment IDs in the opened dataset.
DATASET_PATH = None # Set this to a specific .nc file when you do not want the newest match.
MOVIE_OUTPUT_DIR = None # None means: write into ./results/tutorial_movies.
PHASE_MOVIE_NAME = "phase_rover_merged.mp4"
POWER_MOVIE_NAME = "power_rover_merged.mp4"
MOVIE_MAX_FRAMES = csi.DEFAULT_MOVIE_MAX_FRAMES
MOVIE_FPS = csi.DEFAULT_MOVIE_FPS
if REQUESTED_EXPERIMENT_IDS is None:
ds, dataset_path = csi.open_dataset(dataset_path=DATASET_PATH)
EXPERIMENT_IDS = csi.available_experiment_ids(ds)
else:
EXPERIMENT_IDS = csi.normalize_experiment_ids(REQUESTED_EXPERIMENT_IDS)
ds, dataset_path = csi.open_dataset(experiment_id=EXPERIMENT_IDS, dataset_path=DATASET_PATH)
antenna_positions = csi.load_antenna_positions()
output_dir = REPO_ROOT / "results" / "tutorial_movies" if MOVIE_OUTPUT_DIR is None else Path(MOVIE_OUTPUT_DIR)
output_dir.mkdir(parents=True, exist_ok=True)
movie_frames = csi.movie_frame_table(ds, EXPERIMENT_IDS, max_frames=MOVIE_MAX_FRAMES)
print(f"Loaded dataset: {dataset_path}")
print(f"Selected experiment IDs: {EXPERIMENT_IDS}")
print(f"Movie output directory: {output_dir}")
print(
f"Movie frames: {movie_frames.attrs['frame_count']} "
f"(sampled={movie_frames.attrs['sampled']}, total_valid_positions={movie_frames.attrs['total_valid_positions']})"
)
print(f"Loaded {len(antenna_positions)} antenna positions from: {csi.POSITIONS_URL}")
Loaded dataset: C:\Users\Calle\OneDrive\Documenten\GitHub\ELLIIIT-dataset-26\results\csi_EXP003__EXP005__EXP006__EXP007__EXP008__EXP009__EXP010__EXP011__EXP012.nc Selected experiment IDs: ['EXP003', 'EXP005', 'EXP006', 'EXP007', 'EXP008', 'EXP009', 'EXP010', 'EXP011', 'EXP012'] Movie output directory: C:\Users\Calle\OneDrive\Documenten\GitHub\ELLIIIT-dataset-26\results\tutorial_movies Movie frames: 250 (sampled=True, total_valid_positions=5011) Loaded 140 antenna positions from: https://raw.githubusercontent.com/techtile-by-dramco/techtile-description/refs/heads/main/geometry/techtile_antenna_locations.yml
Inspect The Export Plan¶
The table below shows which (experiment_id, cycle_id) pairs will become movie frames.
If the merged dataset contains more valid rover positions than MOVIE_MAX_FRAMES, the helper samples frames evenly across the full sequence. This keeps the MP4 readable while still covering the whole measurement set.
movie_frames
<xarray.Dataset> Size: 20kB
Dimensions: (measurement_index: 250)
Coordinates:
* measurement_index (measurement_index) int64 2kB 0 1 2 ... 248 249
Data variables:
experiment_id (measurement_index) <U6 6kB 'EXP003' ... 'EXP012'
cycle_id (measurement_index) int64 2kB 1 21 41 ... 763 784
rover_x (measurement_index) float64 2kB 1.918 ... 5.286
rover_y (measurement_index) float64 2kB 2.865 ... 2.954
rover_z (measurement_index) float64 2kB 0.7394 ... 0.7578
csi_host_count (measurement_index) int64 2kB 42 42 42 ... 41 41
source_measurement_index (measurement_index) int64 2kB 0 20 ... 4989 5010
Attributes:
experiment_ids: ['EXP003', 'EXP005', 'EXP006', 'EXP007', 'EXP008'...
total_valid_positions: 5011
frame_count: 250
requested_max_frames: 250
sampled: TrueExport The Phase Movie¶
This movie keeps the antenna layout fixed and updates the CSI phase value at every active receiver tile. The light gray dots show all valid rover positions in the selected dataset slice, the orange points show the already visited movie frames, and the red star marks the current rover position.
phase_movie_path = csi.export_spatial_phase_movie(
ds,
EXPERIMENT_IDS,
output_dir / PHASE_MOVIE_NAME,
antenna_positions=antenna_positions,
max_frames=MOVIE_MAX_FRAMES,
fps=MOVIE_FPS,
)
print(f"Saved phase movie: {phase_movie_path}")
Saved phase movie: C:\Users\Calle\OneDrive\Documenten\GitHub\ELLIIIT-dataset-26\results\tutorial_movies\phase_rover_merged.mp4
Export The Power Movie¶
This movie uses the same frame sequence, but colors the active antennas by CSI power in dB. The color scale is fixed across all exported frames so you can compare strong and weak regions over the whole movie.
power_movie_path = csi.export_spatial_power_movie(
ds,
EXPERIMENT_IDS,
output_dir / POWER_MOVIE_NAME,
antenna_positions=antenna_positions,
max_frames=MOVIE_MAX_FRAMES,
fps=MOVIE_FPS,
)
print(f"Saved power movie: {power_movie_path}")
Saved power movie: C:\Users\Calle\OneDrive\Documenten\GitHub\ELLIIIT-dataset-26\results\tutorial_movies\power_rover_merged.mp4
Notes¶
- Default output goes to
results/tutorial_movies/. - If you want to refresh the GitHub Pages media files, point
MOVIE_OUTPUT_DIRatdocs/public/media/. - The exported MP4s do not need access to the raw NAS logs. They only use the processed
.ncfile plus the Techtile antenna geometry map.
ds.close()