Oryx: Multi-node Radio Channel Sounding for UAVs and Ground-stations

The Oryx dataset contains radio channel measurements between stationary (ground- and rooftop-mounted) and mobile (UAV- and vehicle-mounted) transceivers in a multi-static setup. A variety of dynamic passive objects were present in the measured scenarios, allowing for verification of radar detection, estimation and tracking algorithms. In addition to the measured channel frequency responses, the position of all radio nodes and passive objects were recorded using high-accuracy GNSS RTK devices.

PropertyValue
Center Frequency3.75 GHz
Signal TypeOFDM
OFDM Symbol Duration16 us
Bandwidth (Usable)60 (48) MHz
Number of TXs1
Number of RXs7

Introduction

Oryx is a part of an outdoor measurement campaign that took place in Ilmenau, Germany in August 2024. The goal of the campaign was the provisioning of datasets that enable the study of SISO radio channels between stationary and mobile multi-static transceivers relevant to UAV scenarios. A specific focus is the recording of UAV-to-UAV “side-link” channels measured in the multiple passive targets with varying trajectories are present. Additional scenarios recorded infrastructure-to-infrastructure channels in the presence of passive vehicular and UAV objects with well-defined movement trajectories.

Getting Started

This dataset will soon be published at a data repository. Once downloaded, use the Python snippets provided to load the data.

Recorded Data

The datasets exported and described here contain the complex time-varying channel frequency response (channel transfer function). Relevant meta-data such as the values of time and frequency at which the response is measured are available. Additionally, position information (latitude, longitude and height above sea level) for all participating nodes measured via a RTK-device are exported.

Terminology

  • Node: An object participating in the measurement for which positions are logged
    • Radio Node: Node equipped with radio receiver and/or transmitter
    • Passive Node: Node without radio equipment

Measurement Setup

A detailed description of the hardware setup and measurement procedure was published in the articles that can be found here and here.

Location

The measurements took place at the parking lot behind the Ernst-Abbe-Zentrum building at the campus of Technical University of Ilmenau, located at Ehrenbergstraße 29, 98693 Ilmenau.

Applications

This dataset has a number of possible applications, for e.g.

  • Verification of radar algorithms (object detection, tracking and localization)
  • Verification of ISAC algorithms (radio resource allocation)
  • Characterization of UAV-to-UAV channels and UAV-to-Infrastructure channels

Measurement Runs

run1

Participating Nodes

Node NameMounted OnMovementNode Type
uav1UAVquasi-stationary (hovering)Radio: TX
RX1Roof-topStaticRadio: RX
RX2Roof-topStaticRadio: RX
RX3GroundStaticRadio: RX
RX4GroundStaticRadio: RX
uav2UAVQuasi-stationary (hovering)Radio: RX
uav3UAVQuasi-stationary (hovering)Radio: RX
uav4UAVQuasi-stationary (hovering)Radio: RX
uav_evtolUAVDynamicPassive

Scenario Overview

The following figures visualize the 3-D and 2-D position of the radio and passive nodes:

Exemplary Radargram Plots

The following figure depicts the range-velocity spectrum for all 7 receiver links.

Data Format

Directory Structure

The dataset is organized into directories corresponding to the different measurement nodes:

run1/
├── uav_evtol/                          <=== Passive Node
│   └── Data/
│       └── Location.h5
│
├── uav1_to_Rx1/                        <=== Stationary Radio Receiver
│   └── Data/
│       ├── FrequencyResponses.h5
│       ├── LocationRx.h5
│       └── LocationTx.h5
│
├── uav1_to_Rx2/
├── uav1_to_Rx3/
├── uav1_to_Rx4/
├── uav1_to_uav2/                       <=== UAV-mounted Radio Receiver
├── uav1_to_uav3/
└── uav1_to_uav4/

Every radio node directory contains a FrequencyResponses.h5, LocationRx.h5 and LocationTx.h5 file while a passive node directory contains a Location.h5 file.

HDF5 File Structure

The datasets and data groups of the HDF5 files can be explored using a tool such as h5ls available here. For convenience, the outputs for each type of file present in our dataset is presented here:

FrequencyResponses.h5
$h5ls -r FrequencyResponses.h5 
/                        Group
/FrequencyResponses      Group
/FrequencyResponses/Data Dataset {46875, 768}
/FrequencyResponses/MetaData Group
/FrequencyResponses/MetaData/Frequency Group
/FrequencyResponses/MetaData/Frequency/Frequency Dataset {768}
/FrequencyResponses/MetaData/Frequency/Index Dataset {768}
/FrequencyResponses/MetaData/Snapshot Group
/FrequencyResponses/MetaData/Snapshot/Index Dataset {46875}
/FrequencyResponses/MetaData/Snapshot/TimeStamp Dataset {46875}
LocationRx.h5
h5ls -r LocationRx.h5 
/                        Group
/PoseData                Group
/PoseData/Height         Dataset {750/Inf}
/PoseData/Latitude       Dataset {750/Inf}
/PoseData/Longitude      Dataset {750/Inf}
/PoseData/MetaData       Group
/PoseData/MetaData/Snapshot Group
/PoseData/MetaData/Snapshot/Index Dataset {750}
/PoseData/MetaData/Snapshot/TimeStamp Dataset {750}
Location.h5
$h5ls -r Location.h5 
/                        Group
/PoseData                Group
/PoseData/Height         Dataset {375/Inf}
/PoseData/Latitude       Dataset {375/Inf}
/PoseData/Longitude      Dataset {375/Inf}
/PoseData/MetaData       Group
/PoseData/MetaData/Snapshot Group
/PoseData/MetaData/Snapshot/Index Dataset {375}
/PoseData/MetaData/Snapshot/TimeStamp Dataset {375}

Data Preprocessing

– Coming Soon –

Useful Information

Static vs Dynamic Nodes

The Location*.h5 files for static nodes (RX1, RX2, RX3, RX4) were measured before the start of the measurement period. Hence, the timestamps do not correspond to the measurement time interval. When using the positions of the static nodes, simply average the positions values in each dataset.

Interpolating Position Information

While the data in each HDF5 file corresponds to the same time span, the data sampling rate is naturally different for the recorded frequency responses (60 MHz) and position data (~10 Hz). Furthermore, position information sampling rates could differ between nodes. If position information is required at the same rate as the frequency responses (for e.g. for the purpose of calculating ground-truth passive object parameters and comparing with the observed channel response), it is recommended to perform cubic spline interpolation on the /PoseData/Height, /PoseData/Latitude and /PoseData/Longitude datasets. A minimal snippet using Python and SciPy is provided below:

import h5py
import numpy as np
from scipy.interpolate import CubicSpline

# Open the HDF5 file
with h5py.File("Location.h5", "r") as f:
    # Read timestamps and position datasets
    ts = f["/PoseData/MetaData/Snapshot/TimeStamp"][:]
    lat = f["/PoseData/Latitude"][:]
    lon = f["/PoseData/Longitude"][:]
    hgt = f["/PoseData/Height"][:]

# Example: target timestamps where we want interpolated positions
# (e.g., section of timestamps of 
# FrequencyResponses.h5:/FrequencyResponses/MetaData/Snapshot/TimeStamp)
target_ts = np.linspace(ts.min(), ts.max(), 1000)

# Perform cubic spline interpolation for each coordinate
lat_interp = CubicSpline(ts, lat, extrapolate=None)(target_ts)
lon_interp = CubicSpline(ts, lon, extrapolate=None)(target_ts)
hgt_interp = CubicSpline(ts, hgt, extrapolate=None)(target_ts)

# Example usage: print first few interpolated points
print("Interpolated positions (lat, lon, height):")
for i in range(5):
    print(f"{lat_interp[i]:.6f}, {lon_interp[i]:.6f}, {hgt_interp[i]:.2f}")

Loading Channel Data

The complex channel transfer function is stored as a compound datatype in an HDF5 dataset located at the path /FrequencyResponses/Data. The fields named “real” and “imag” are used to represent the real and imaginary parts, respectively, of the complex values. This is illustrated with the following Python function:

import h5py
import numpy as np

def load_complex_channel_data(file_path, sample_indices):
    """
    Loads complex channel data and associated axes.
    Arguments:
        file_path: str: Path to FrequencyResponses.h5 file
        sample_indices: Tuple[int, int]: A slice (start, stop) defining the
            slow-time (snapshot) samples to load from file.
    Returns:
        complex_data: np.ndarray: 2-D array (slow-time, frequency)
        ts: np.ndarray: array of timestamps corresponding to the loaded samples
        ff: np.ndarray: array of frequency values
    """
    sample_indices_slice = slice(sample_indices[0], sample_indices[1])
    timestamp_path = "/FrequencyResponses/MetaData/Snapshot/TimeStamp"
    frequencies_path = "/FrequencyResponses/MetaData/Frequency/Frequency"

    with h5py.File(file_path, "r") as f:
        # Read timestamp, frequency axes and compound dataset
        ts = f[timestamp_path][sample_indices_slice]
        ts_unitscaler = f[timestamp_path].attrs["UnitScaler"]

        ff = f[frequencies_path][:]
        ff_scaler = f[frequencies_path].attrs["UnitScaler"]

        data = f["/FrequencyResponses/Data"][sample_indices_slice]

    complex_data = data["real"] + 1j * data["imag"]

    return (
        complex_data,
        ts * ts_unitscaler,
        ff * ff_scaler,
    )

Plotting Delay-Doppler Representation

A common step in radar-like applications is the caluclation of the delay-Doppler spreading function. The following Python script plots the magnitude of the delay-Doppler spreading function in dB:

import matplotlib.pyplot as plt

# load 50->562 slow-time samples
# complex_data has dims (slow-time, sub-carriers)
complex_data, ts, ff = load_complex_channel_data(
    ".../uav1_to_Rx1/Data/FrequencyResponses.h5",
    (50, 562),
)

# transform slow-time to Doppler frequency
dd_map = np.fft.fftshift(np.fft.fft(complex_data, axis=0))

# trasnform sub-carriers to delay
dd_map = np.fft.ifft(np.fft.ifftshift(dd_map, axes=1), axis=1)

# create axes for plotting
doppler_axis = np.fft.fftshift(np.fft.fftfreq(len(ts), d=(ts[1] - ts[0])))

delay_axis = np.fft.ifftshift(np.fft.fftfreq(len(ff), d=(ff[1] - ff[0])))
delay_axis = delay_axis - delay_axis.min()

plt.figure(figsize=(8, 6))
plt.imshow(
    10 * np.log10(np.abs(dd_map) ** 2),
    extent=[delay_axis[0], delay_axis[-1], doppler_axis[0], doppler_axis[-1]],
    aspect="auto",
    origin="lower",
)
plt.xlabel("Delay (s)")
plt.ylabel("Doppler Frequency (Hz)")
plt.title("Delay-Doppler Map")
plt.colorbar(label="Power (dB)")
plt.show()

External References