lwm-temporal / docs /dynamic_scenario_pipeline.md
wi-lab's picture
Update scenario gen and docs
95923eb

Dynamic Scenario Pipeline Documentation

This document provides a full reference for the dynamic-scenario generator built on top of DeepMIMO data. It covers architecture, interpolation logic, CLI usage, programmatic entry points, and details on the saved outputs.

Contents

  1. Overview
  2. Core Modules
  3. Execution Flow
  4. CLI Usage
  5. Programmatic Usage
  6. Output Structure
  7. Trajectory Behaviour and Interpolation Details
  8. Visualization and Plot Styling
  9. Extending the Pipeline
  10. Troubleshooting

1. Overview

The dynamic scenario generator produces per-user channel sequences by interpolating DeepMIMO user-grid data along simulated vehicle and pedestrian trajectories. The main goals are:

  • respect the road layout implicit in the DeepMIMO user grid;
  • simulate realistic motion (straight segments, turns at intersections, stops at dead ends);
  • interpolate channel ray parameters between user locations;
  • save both the complex channels and rich metadata (positions, velocities, Doppler, etc.).

Artifacts are stored under examples/ by default and include a “slim” channel file, a full metadata file, and a visualization of the environment.


2. Core Modules

deepmimo_adapter.py

Responsible for loading DeepMIMO v4 datasets and returning data in a backward-compatible format:

  • Supplies load_deepmimo_user_data(scenario, scenarios_dir, max_paths, load_params, array_dtype, logger).
  • Handles multiple TX sets by selecting the one with the highest aggregate power.
  • Converts object arrays returned by DeepMIMO into stacked numpy arrays (per-user matrices).
  • Exposes user data through _LazyPathAccessor so legacy code can read paths[idx] and receive a dict matching the DeepMIMO v3 format ( power, phase, ToA, DoA_theta, DoA_phi, num_paths, LoS, etc.).

scenario_generation.py

Contains the main orchestration logic:

  • AntennaArrayConfig, TrafficConfig, GridConfig, ScenarioSamplingConfig, ScenarioGenerationConfig: dataclasses describing configuration state.
  • DynamicScenarioGenerator.generate(overwrite=False): entry point that produces a ScenarioGenerationResult with file paths and the in-memory payload.
  • Helper functions handling geometry and interpolation.

3. Execution Flow

Step 1: Loading DeepMIMO data

deepmimo_data = load_deepmimo_user_data(
    cfg.scenario,
    scenarios_dir=cfg.scenarios_dir,
    load_params=cfg.deepmimo_load_params,
    max_paths=cfg.deepmimo_max_paths,
    array_dtype=cfg.deepmimo_array_dtype,
    logger=self.logger,
)
path_exist = np.asarray(deepmimo_data["user"]["LoS"])
pos_total = np.asarray(deepmimo_data["user"]["location"])
  • path_exist indicates LoS (1), NLoS (0), or no path (-1) per user grid point.
  • pos_total is the xyz coordinate of each user (meters).

Step 2: Inferring road geometry

  1. _infer_grid_step(positions) — if GridConfig.auto_step_size is true, computes a typical spacing between user-grid points using k-NN distances; otherwise uses the user-provided GridConfig.step_size.
  2. filter_road_positions(...) — filters the user points that lie inside vertical or horizontal “bands” defined by road_width and road_center_spacing. Points falling inside both bands are tagged as intersections.
  3. create_grid_road_network(...) — constructs a directed graph (NetworkX) connecting road nodes separated by roughly one grid_step, respecting lane directions and turn allowances.

Step 3: Generating trajectories

  • Vehicles: generate_n_smooth_grid_trajectories walks the graph:
    • chooses random start nodes biased toward the interior of the road graph (KD-tree sampling);
    • at each step, picks forward edges if available, optionally turning at intersections with probability turn_probability;
    • if no forward neighbor exists (dead end, trapped node, etc.), the walk terminates early and that trajectory attempt is discarded — another start node will be sampled until enough valid paths are found or max_attempts is reached.
  • Pedestrians: generate_n_pedestrian_trajectories performs shorter random walks over valid positions using a step size and angular noise.
  • Mapping back to DeepMIMO indices: get_trajectory_indices builds a dictionary from positions to DeepMIMO user indices and maps each trajectory to a list of grid user IDs.

Step 4: Sampling continuous motion

_build_tracks(...)

  • Draws a nominal speed per vehicle/pedestrian within the configured range.
  • Calls sample_continuous_along_polyline with (trajectory positions, user indices, nominal speed, sample_dt, horizon, speed_profile=None):
    • constructs cumulative arc lengths along the polyline;
    • advances a fixed speed * dt distance per sample (clamped to the end of the polyline);
    • finds the corresponding segment and interpolation factor alpha, yielding continuous positions between grid points;
    • stores the segment endpoints as (user_idx0, user_idx1) along with alpha and the segment direction vector.
  • After resampling, per-sample velocities are recomputed from the actual XY displacement (step_xy / dt) so Doppler reflects the true motion; accelerations are the finite differences of this velocity series.
  • Each track dictionary contains speed, pos, pairs, alpha, vdir, the derived vel/acc, and step_xy.

Step 5: Interpolating ray parameters

In _channels_for_tracks, for each sample:

i0, i1 = pair
ray_interp = interpolate_ray_params(deepmimo_data, i0, i1, float(alpha))
  • Finds the two user entries (deepmimo_data["user"]["paths"][i0], [i1]).
  • Selects the top min(num_paths[i0], num_paths[i1]) rays by power.
  • Interpolates:
    • powers, ToAs: linear blend ((1-α) * val0 + α * val1).
    • angles: unwraps to avoid jumps before blending.
    • phase: blends via complex exponentials to maintain continuity.
    • LoS flag: OR between the two user LoS flags.
  • Computes per-path Doppler: v_proj = -⟨aoa_unit, vdir⟩ * instantaneous_speed. This ties Doppler to the actual speed profile.
  • Populates ray_interp with Doppler_vel and elapsed_time = k * sample_dt.

Step 6: Channel synthesis

pred, _, _ = generate_channel_from_interpolated_ray(ray_interp, antenna_cfg, carrier_frequency_hz)
channel_sequence.append(np.asarray(pred[0]).squeeze(0))
  • Builds DeepMIMO parameter sets for Tx/Rx arrays and OFDM configuration.
  • Calls generate_mimo_channel, which applies Doppler and phase updates in the frequency domain.
  • Each sample yields a complex tensor [n_tx_ant, n_subcarriers] (Rx is collapsed to 1 by default).

Step 7: Packaging outputs and plotting

  • Channels, positions, velocities, accelerations, Doppler, angles, delays are stacked across tracks.
  • Simplified channel (channel_cont or channel) is saved at output_path; the entire metadata dict (payload) goes to full_output_path.
  • Environment plot (environment.png) shows LoS/NLoS users, road nodes, trajectories, start markers. Discrete trajectories are drawn as polylines; the speed profile influences the sampled positions.

4. CLI Usage

Main entry point: python -m LWMTemporal.cli.generate_dynamic_data. Key flags:

  • --scenario-name, --scenarios-dir.
  • Road inference: --road-width, --road-spacing, --grid-step, --disable-auto-grid-step.
  • Traffic: --num-vehicles, --num-pedestrians, --vehicle-speed, --ped-speed, --turn-probability, --max-attempts.
  • Sampling: --time-steps, --sample-dt, --continuous-length.
  • DeepMIMO: --max-paths, --scenarios-dir, --scenario-range.
  • Output directories: --output-dir, --full-output-dir, --figures-dir. Defaults: examples/data, examples/full_data, examples/figs.
  • Misc: --regen-dims, --channel-dims (CSV), --seed, --overwrite, --log-dir.

Example:

python -m LWMTemporal.cli.generate_dynamic_data \
  --scenario-name city_1_losangeles_3p5 \
  --scenarios-dir deepmimo_scenarios \
  --num-vehicles 200 \
  --num-pedestrians 15 \
  --time-steps 20 \
  --sample-dt 0.001 \
  --tx-horizontal 32 \
  --tx-vertical 1 \
  --subcarriers 32 \
  --road-width 2 \
  --road-spacing 8 \
  --vehicle-speed 0 30 \
  --turn-probability 0.1 \
  --max-paths 25 \
  --figures-dir examples/figs \
  --overwrite

5. Programmatic Usage

5.1 Using the CLI main() function

from LWMTemporal.cli.generate_dynamic_data import main

main([
    "--scenario-name", "city_0_newyork_3p5",
    "--scenarios-dir", "deepmimo_scenarios",
    "--num-vehicles", "120",
    "--num-pedestrians", "20",
    "--time-steps", "40",
    "--sample-dt", "0.1",
    "--tx-horizontal", "32",
    "--subcarriers", "32",
    "--output-dir", "examples/data",
    "--full-output-dir", "examples/full_data",
    "--figures-dir", "examples/figs/newyork",
    "--overwrite",
])

5.2 Direct generator invocation

from pathlib import Path
from LWMTemporal.data.scenario_generation import (
    AntennaArrayConfig,
    DynamicScenarioGenerator,
    GridConfig,
    ScenarioGenerationConfig,
    ScenarioSamplingConfig,
    TrafficConfig,
)

config = ScenarioGenerationConfig(
    scenario="city_0_newyork_3p5",
    antenna=AntennaArrayConfig(tx_horizontal=32, tx_vertical=1, subcarriers=32),
    sampling=ScenarioSamplingConfig(time_steps=20, sample_dt=0.01),
    traffic=TrafficConfig(num_vehicles=120, num_pedestrians=20, turn_probability=0.1),
    grid=GridConfig(road_width=2.0, road_center_spacing=8.0),
    output_dir=Path("examples/data"),
    full_output_dir=Path("examples/full_data"),
    figures_dir=Path("examples/figs/newyork"),
    scenarios_dir=Path("deepmimo_scenarios"),
    deepmimo_max_paths=25,
)

generator = DynamicScenarioGenerator(config)
result = generator.generate(overwrite=True)
dataset = result.payload

dataset is the same dictionary saved in the _full.p file.

5.3 Notebook quick start

from pathlib import Path
from LWMTemporal.data.scenario_generation import (
    AntennaArrayConfig,
    DynamicScenarioGenerator,
    GridConfig,
    ScenarioGenerationConfig,
    ScenarioSamplingConfig,
    TrafficConfig,
)

config = ScenarioGenerationConfig(
    scenario="parow",
    antenna=AntennaArrayConfig(tx_horizontal=32, tx_vertical=1, subcarriers=32),
    sampling=ScenarioSamplingConfig(time_steps=20, sample_dt=1e-3),
    traffic=TrafficConfig(
        num_vehicles=120,
        num_pedestrians=20,
        turn_probability=0.1,
        vehicle_speed_range=(0 / 3.6, 20 / 3.6),
    ),
    grid=GridConfig(road_width=2.0, road_center_spacing=8.0),
    output_dir=Path("examples/data"),
    full_output_dir=Path("examples/full_data"),
    figures_dir=Path("examples/figs/newyork"),
    scenarios_dir=Path("deepmimo_scenarios"),
    deepmimo_max_paths=25,
)

generator = DynamicScenarioGenerator(config)
dataset = generator.generate(overwrite=True).payload

From the returned payload you can immediately explore both the continuous tensors and the discrete metadata captured at each grid point:

ue_idx = 10
channel_cont = dataset["channel_cont"][ue_idx]
channel_disc = dataset["discrete"]["channel"][ue_idx]
angle_disc = dataset["angle_discrete"][ue_idx]
delay_disc = dataset["delay_discrete"][ue_idx]
doppler_disc = dataset["doppler_vel_discrete"][ue_idx]
vel_disc = dataset["vel_discrete"][ue_idx]
acc_disc = dataset["acc_discrete"][ue_idx]

# Use the shared AngleDelayProcessor utilities for plotting
from LWMTemporal.data.angle_delay import AngleDelayProcessor, AngleDelayConfig
from examples.ad_temporal_evolutiton import configure_style, pick_bins, plot_curves

configure_style()
proc = AngleDelayProcessor(AngleDelayConfig(keep_percentage=0.25))
ad_trim = proc.truncate_delay_bins(proc.forward(channel_cont))[0]
pick_list = pick_bins(ad_trim, k=3, coords=None)
plot_curves(ad_trim, pick_list, Path("examples/data/figs/ad_curves.png"), title="Continuous vs Discrete")

In this workflow:

  • channel_cont holds the interpolated continuous channel sequence; channel_disc contains the original DeepMIMO channels sampled at the discrete grid points.
  • angle_discrete, delay_discrete, and doppler_vel_discrete list per-path AoA/AoD delays and Doppler values for each discrete time step.
  • vel_discrete / acc_discrete give the per-sample speed and acceleration derived from the discrete trajectory geometry.
  • The AngleDelay processor converts any channel tensor (continuous or discrete) into the truncated angle-delay domain, after which the plotting utilities can highlight temporal evolution and GIF exporters can visualize the heatmaps.

Use this section as a drop-in reference when building your own notebooks or scripts around the generator results; it covers dataset construction, metadata inspection, and angle-delay visualization end-to-end.


6. Output Structure

payload (full metadata) includes:

  • scenario: scenario name.
  • channel_cont / channel: complex array of shape [n_tracks, time_steps, n_tx_ant, n_subcarriers].
  • pos_cont, vel_cont, acc_cont: continuous motion sequences.
  • doppler_vel_cont, angle_cont, delay_cont: lists of per-path values aligned with channel_cont.
  • pos_step_xy: norm of XY displacement between samples (useful for sanity checks of speed).
  • index_discrete: DeepMIMO user indices visited by each discrete trajectory.
  • channel_discrete: the raw DeepMIMO channels gathered at the discrete user indices.
  • pos_discrete: original polylines before continuous sampling.
  • angle_discrete, delay_discrete, doppler_vel_discrete: per-path metadata matching each discrete time step.
  • vel_discrete, acc_discrete: per-sample speed and acceleration derived from the discrete trajectories.
  • continuous: dict mirroring the continuous keys (channel, pos, vel, acc, doppler_vel, angle, delay).
  • discrete: dict exposing index, channel, pos, angle, delay, doppler_vel, vel, and acc for the discrete trajectories.
  • grid_step, sample_dt, car_speed_range, etc.: metadata from configuration.

The slim channel file (in examples/data) typically contains just the channel tensor (channel_cont).


7. Trajectory Behaviour and Interpolation Details

  • Road graph: derived from DeepMIMO user coordinates; nodes represent potential road points, edges connect neighbors at roughly the inferred grid spacing.
  • Turns: only attempted at points flagged as intersections (overlap between vertical and horizontal road bands) with probability turn_probability. Otherwise vehicles continue straight if possible.
  • Dead ends: if a walk runs out of valid neighbors, that trajectory attempt is discarded and a new start is sampled instead of padding or stalling.
  • Sampling abstraction: sample_continuous_along_polyline uses the per-sample speed profile to integrate distances; continuous positions stay on the shape of the discrete trajectory.
  • Ray interpolation: linear blend of power/ToA/angles/phase with path selection based on strongest rays. Doppler is tied to the instantaneous speed and segment direction.
  • Acceleration: computed via finite difference of successive velocities and stored in acc_cont.

8. Visualization and Plot Styling

  • LoS/NLoS/inactive users are plotted with a subdued commercial palette.
  • Road graph nodes appear as faint grey dots.
  • Vehicle trajectories are solid lines with start markers; pedestrian trajectories are dashed lines. Start markers are white circles with colored edges.
  • Only the continuous sampled positions are plotted (no discrete fallback), so the dot trails match the actual motion.
  • Figure is saved with bbox_inches="tight", pad_inches=0.1 to minimize whitespace.

9. Extending the Pipeline

Potential customization points:

  • Road inference logic (e.g., apply map data instead of inferred spacing).
  • Speed profile rules (longer or non-linear ramps).
  • Interpolation strategy (e.g., weighting by path lengths or angular similarity).
  • Channel synthesis (switch to time-domain or multi-antenna Rx configurations).
  • Visualization (brand colors, overlays, additional annotations).

The codebase keeps these components modular to ease experimentation.


10. Troubleshooting

  • Scenario download prompts: Accept (y) when asked, or manually place the downloaded folder in deepmimo_scenarios.
  • Sparse road nodes / weird turns: Adjust --road-width / --road-spacing, increase --max-attempts, or supply --grid-step.
  • Channels not changing with sample_dt: Ensure the scenario is long enough; if the road segments are too short, the slowdown will trigger early.
  • Doppler checks: Access dataset["doppler_vel_cont"] to verify per-path Doppler values; they should reflect the instantaneous speed profile.
  • Visualization tweaks: _save_environment_plot can be edited to change DPI, color scheme, or annotation style.

This guide should provide enough detail to understand and extend the dynamic scenario generator. For further questions or customizations, reach out in the project discussions or open an issue.