Compare commits

...

No commits in common. "main" and "main" have entirely different histories.
main ... main

48 changed files with 1394 additions and 632 deletions

2
.gitattributes vendored
View File

@ -1 +1,3 @@
*.sigmf-data filter=lfs diff=lfs merge=lfs -text
*.sigmf-meta filter=lfs diff=lfs merge=lfs -text
*.h5 filter=lfs diff=lfs merge=lfs -text

View File

@ -0,0 +1,60 @@
data:
batch_size: 32
dataset_params:
iq_key: iq_data
label_key: labels
drop_last: false
kind: iq_h5
num_workers: 0
persistent_workers: false
pin_memory: false
test_path: /opt/qmb/riahub/dataset/qoherent/ash_test_demo_repo/main/datasets/test_demo_v1.0.0.h5
test_split: 0
train_path: /opt/qmb/riahub/dataset/qoherent/ash_test_demo_repo/main/datasets/train_demo_v1.0.0.h5
validation_path: /opt/qmb/riahub/dataset/qoherent/ash_test_demo_repo/main/datasets/val_demo_v1.0.0.h5
validation_split: 0
evaluation:
capture_predictions: true
enabled: true
params:
save_confusion: true
split: test
export:
dynamic_batch: true
dynamic_width: false
enabled: true
file_name: demo_model.onnx
opset_version: 17
strict: false
use_dynamo: true
use_onnxsim: false
model:
name: VTCNN1
params: {}
optimization:
loss:
name: cross_entropy
params: {}
optimizer:
name: adam
params:
amsgrad: false
eps: 1e-08
lr: 0.001
weight_decay: 0
runtime:
amp_enabled: false
autocast_dtype: float32
checkpoint_every_n_epochs: 1
component_modules:
- /opt/qmb/riahub/model/src/qmb/models/RadioML/VTCNN1.py
device: auto
epochs: 1
progress_bar: false
seed: 42
task:
name: classification
params:
save_artifacts: true
selection_metric: accuracy
selection_mode: max

View File

@ -0,0 +1,385 @@
name: QMB Training
on:
push:
branches: [ "main" ]
paths:
- ".riahub/workflows/train.yaml"
pull_request:
branches: [ "main" ]
paths:
- ".riahub/workflows/train.yaml"
permissions:
contents: read
actions: read
jobs:
QMB-Training:
runs-on: "whitehorse-p40-qmb"
env:
RIAHUB_BASE_URL: ${{ vars.RIAHUB_BASE_URL || secrets.RIAHUB_BASE_URL || '' }}
QMB_OUTPUT_ROOT: "/opt/qmb/outputs"
QMB_TASK_REPO_ROOT: "/opt/qmb/task_repos"
steps:
- name: Display basic runner info
run: |
echo "Runner OS: ${{ runner.os }}"
echo "Runner Architecture: ${{ runner.arch }}"
- name: Print CPU information
run: |
lscpu
- name: Print GPU information
run: |
if command -v nvidia-smi &> /dev/null; then
nvidia-smi
else
echo "No NVIDIA GPU available."
fi
- name: Download Selected Model
env:
RIAHUB_USER: ${{ secrets.QMBDEMO_USER }}
RIAHUB_TOKEN: ${{ secrets.QMBDEMO_TOKEN }}
run: |
set -euo pipefail
DEFAULT_BASE_URL="https://riahub.ai"
BASE_URL_SOURCE=${RIAHUB_BASE_URL:-$DEFAULT_BASE_URL}
BASE_URL_SOURCE="${BASE_URL_SOURCE%/}"
build_base_candidates() {
local raw="$1"
if [[ "$raw" =~ ^https?:// ]]; then
echo "$raw"
if [[ "$raw" == http://* ]]; then
echo "https://${raw#http://}"
elif [[ "$raw" == https://* ]]; then
echo "http://${raw#https://}"
fi
return
fi
echo "https://$raw"
echo "http://$raw"
}
REPO_PATH="/qoherent/qmb-mvp.git"
REL_PATH="src/qmb/models/RadioML/VTCNN1.py"
REF="db9dffb7d382993cf46f392c34a47e4cdd59585e"
DEST_PATH="/opt/qmb/riahub/model/src/qmb/models/RadioML/VTCNN1.py"
TMP_DIR=$(mktemp -d)
cleanup() { sudo rm -rf "$TMP_DIR"; }
trap cleanup EXIT
mapfile -t BASE_CANDIDATES < <(build_base_candidates "$BASE_URL_SOURCE")
CLONED=0
for base in "${BASE_CANDIDATES[@]}"; do
base="${base%/}"
REPO_URL="${base}${REPO_PATH}"
AUTHED_URL=$(printf '%s' "$REPO_URL" | sed -E "s#^(https?://)#\\1${RIAHUB_USER}:${RIAHUB_TOKEN}@#")
echo "Cloning model repo from $REPO_URL"
sudo rm -rf "$TMP_DIR"
if sudo git clone --filter=blob:none --no-checkout "$AUTHED_URL" "$TMP_DIR"; then
CLONED=1
break
fi
done
if [[ "$CLONED" -ne 1 ]]; then
echo "Failed to clone model repo using base URL candidates derived from: $BASE_URL_SOURCE" >&2
exit 1
fi
if ! command -v git-lfs >/dev/null 2>&1; then
sudo apt-get update -y
sudo apt-get install -y git-lfs
fi
sudo git -C "$TMP_DIR" lfs install --local || true
sudo git -C "$TMP_DIR" sparse-checkout init --no-cone
sudo git -C "$TMP_DIR" sparse-checkout set --no-cone -- "$REL_PATH"
sudo git -C "$TMP_DIR" fetch --depth=1 origin "$REF"
sudo git -C "$TMP_DIR" -c advice.detachedHead=false checkout FETCH_HEAD
sudo git -C "$TMP_DIR" lfs fetch origin --include="$REL_PATH" --exclude="" || true
sudo git -C "$TMP_DIR" lfs checkout || true
sudo mkdir -p "$(dirname "$DEST_PATH")"
sudo cp -f "$TMP_DIR/$REL_PATH" "$DEST_PATH"
sudo git -C "$TMP_DIR" remote remove origin || true
sudo git -C "$TMP_DIR" config --local --unset-all http.extraheader || true
- name: Checkout Training Dataset
env:
RIAHUB_USER: ${{ secrets.QMBDEMO_USER }}
RIAHUB_TOKEN: ${{ secrets.QMBDEMO_TOKEN }}
run: |
set -euo pipefail
DEFAULT_BASE_URL="https://riahub.ai"
BASE_URL_SOURCE=${RIAHUB_BASE_URL:-$DEFAULT_BASE_URL}
BASE_URL_SOURCE="${BASE_URL_SOURCE%/}"
build_base_candidates() {
local raw="$1"
if [[ "$raw" =~ ^https?:// ]]; then
echo "$raw"
if [[ "$raw" == http://* ]]; then
echo "https://${raw#http://}"
elif [[ "$raw" == https://* ]]; then
echo "http://${raw#https://}"
fi
return
fi
echo "https://$raw"
echo "http://$raw"
}
REPO_PATH="/qoherent/ash_test_demo_repo.git"
DEST_ROOT="/opt/qmb/riahub/dataset/qoherent/ash_test_demo_repo/main"
sudo mkdir -p "$(dirname "$DEST_ROOT")"
mapfile -t BASE_CANDIDATES < <(build_base_candidates "$BASE_URL_SOURCE")
CLONED=0
for base in "${BASE_CANDIDATES[@]}"; do
base="${base%/}"
REPO_URL="${base}${REPO_PATH}"
AUTHED_URL=$(printf '%s' "$REPO_URL" | sed -E "s#^(https?://)#\\1${RIAHUB_USER}:${RIAHUB_TOKEN}@#")
echo "Cloning dataset repo from $REPO_URL"
sudo rm -rf "$DEST_ROOT"
if sudo git clone --filter=blob:none --no-checkout "$AUTHED_URL" "$DEST_ROOT"; then
CLONED=1
break
fi
done
if [[ "$CLONED" -ne 1 ]]; then
echo "Failed to clone dataset repo using base URL candidates derived from: $BASE_URL_SOURCE" >&2
exit 1
fi
if ! command -v git-lfs >/dev/null 2>&1; then
sudo apt-get update -y
sudo apt-get install -y git-lfs
fi
sudo git -C "$DEST_ROOT" lfs install --local || true
sudo git -C "$DEST_ROOT" sparse-checkout init --no-cone
sudo git -C "$DEST_ROOT" sparse-checkout set --no-cone -- \
"datasets/train_demo_v1.0.0.h5"
sudo git -C "$DEST_ROOT" fetch --depth=1 origin "6f4e96e49bdf6634a5a2ceb9fee5bae379ae7021"
sudo git -C "$DEST_ROOT" -c advice.detachedHead=false checkout FETCH_HEAD
sudo git -C "$DEST_ROOT" lfs fetch origin --include="datasets/train_demo_v1.0.0.h5" --exclude="" || true
sudo git -C "$DEST_ROOT" lfs checkout || true
sudo git -C "$DEST_ROOT" remote remove origin || true
sudo git -C "$DEST_ROOT" config --local --unset-all http.extraheader || true
- name: Checkout Validation Dataset
env:
RIAHUB_USER: ${{ secrets.QMBDEMO_USER }}
RIAHUB_TOKEN: ${{ secrets.QMBDEMO_TOKEN }}
run: |
set -euo pipefail
DEFAULT_BASE_URL="https://riahub.ai"
BASE_URL_SOURCE=${RIAHUB_BASE_URL:-$DEFAULT_BASE_URL}
BASE_URL_SOURCE="${BASE_URL_SOURCE%/}"
build_base_candidates() {
local raw="$1"
if [[ "$raw" =~ ^https?:// ]]; then
echo "$raw"
if [[ "$raw" == http://* ]]; then
echo "https://${raw#http://}"
elif [[ "$raw" == https://* ]]; then
echo "http://${raw#https://}"
fi
return
fi
echo "https://$raw"
echo "http://$raw"
}
REPO_PATH="/qoherent/ash_test_demo_repo.git"
DEST_ROOT="/opt/qmb/riahub/dataset/qoherent/ash_test_demo_repo/main"
sudo mkdir -p "$(dirname "$DEST_ROOT")"
mapfile -t BASE_CANDIDATES < <(build_base_candidates "$BASE_URL_SOURCE")
CLONED=0
for base in "${BASE_CANDIDATES[@]}"; do
base="${base%/}"
REPO_URL="${base}${REPO_PATH}"
AUTHED_URL=$(printf '%s' "$REPO_URL" | sed -E "s#^(https?://)#\\1${RIAHUB_USER}:${RIAHUB_TOKEN}@#")
echo "Cloning dataset repo from $REPO_URL"
sudo rm -rf "$DEST_ROOT"
if sudo git clone --filter=blob:none --no-checkout "$AUTHED_URL" "$DEST_ROOT"; then
CLONED=1
break
fi
done
if [[ "$CLONED" -ne 1 ]]; then
echo "Failed to clone dataset repo using base URL candidates derived from: $BASE_URL_SOURCE" >&2
exit 1
fi
if ! command -v git-lfs >/dev/null 2>&1; then
sudo apt-get update -y
sudo apt-get install -y git-lfs
fi
sudo git -C "$DEST_ROOT" lfs install --local || true
sudo git -C "$DEST_ROOT" sparse-checkout init --no-cone
sudo git -C "$DEST_ROOT" sparse-checkout set --no-cone -- \
"datasets/val_demo_v1.0.0.h5"
sudo git -C "$DEST_ROOT" fetch --depth=1 origin "6e347846fbf0270fc5547a4522a93b5b411925e6"
sudo git -C "$DEST_ROOT" -c advice.detachedHead=false checkout FETCH_HEAD
sudo git -C "$DEST_ROOT" lfs fetch origin --include="datasets/val_demo_v1.0.0.h5" --exclude="" || true
sudo git -C "$DEST_ROOT" lfs checkout || true
sudo git -C "$DEST_ROOT" remote remove origin || true
sudo git -C "$DEST_ROOT" config --local --unset-all http.extraheader || true
- name: Checkout Test Dataset
env:
RIAHUB_USER: ${{ secrets.QMBDEMO_USER }}
RIAHUB_TOKEN: ${{ secrets.QMBDEMO_TOKEN }}
run: |
set -euo pipefail
DEFAULT_BASE_URL="https://riahub.ai"
BASE_URL_SOURCE=${RIAHUB_BASE_URL:-$DEFAULT_BASE_URL}
BASE_URL_SOURCE="${BASE_URL_SOURCE%/}"
build_base_candidates() {
local raw="$1"
if [[ "$raw" =~ ^https?:// ]]; then
echo "$raw"
if [[ "$raw" == http://* ]]; then
echo "https://${raw#http://}"
elif [[ "$raw" == https://* ]]; then
echo "http://${raw#https://}"
fi
return
fi
echo "https://$raw"
echo "http://$raw"
}
REPO_PATH="/qoherent/ash_test_demo_repo.git"
DEST_ROOT="/opt/qmb/riahub/dataset/qoherent/ash_test_demo_repo/main"
sudo mkdir -p "$(dirname "$DEST_ROOT")"
mapfile -t BASE_CANDIDATES < <(build_base_candidates "$BASE_URL_SOURCE")
CLONED=0
for base in "${BASE_CANDIDATES[@]}"; do
base="${base%/}"
REPO_URL="${base}${REPO_PATH}"
AUTHED_URL=$(printf '%s' "$REPO_URL" | sed -E "s#^(https?://)#\\1${RIAHUB_USER}:${RIAHUB_TOKEN}@#")
echo "Cloning dataset repo from $REPO_URL"
sudo rm -rf "$DEST_ROOT"
if sudo git clone --filter=blob:none --no-checkout "$AUTHED_URL" "$DEST_ROOT"; then
CLONED=1
break
fi
done
if [[ "$CLONED" -ne 1 ]]; then
echo "Failed to clone dataset repo using base URL candidates derived from: $BASE_URL_SOURCE" >&2
exit 1
fi
if ! command -v git-lfs >/dev/null 2>&1; then
sudo apt-get update -y
sudo apt-get install -y git-lfs
fi
sudo git -C "$DEST_ROOT" lfs install --local || true
sudo git -C "$DEST_ROOT" sparse-checkout init --no-cone
sudo git -C "$DEST_ROOT" sparse-checkout set --no-cone -- \
"datasets/test_demo_v1.0.0.h5"
sudo git -C "$DEST_ROOT" fetch --depth=1 origin "82ed23a403a6766c85229092f25a879cdf64da86"
sudo git -C "$DEST_ROOT" -c advice.detachedHead=false checkout FETCH_HEAD
sudo git -C "$DEST_ROOT" lfs fetch origin --include="datasets/test_demo_v1.0.0.h5" --exclude="" || true
sudo git -C "$DEST_ROOT" lfs checkout || true
sudo git -C "$DEST_ROOT" remote remove origin || true
sudo git -C "$DEST_ROOT" config --local --unset-all http.extraheader || true
- name: Checkout configs
uses: actions/checkout@v5
with:
sparse-checkout: .riahub/train_configs
- name: Copy configs into qmb folder
run: |
mkdir -p /opt/qmb/configs/
sudo cp -r ${{ github.workspace }}/.riahub/train_configs/* /opt/qmb/configs/
- name: List QMB project contents
run: |
ls -lha /opt/qmb
ls -lh /opt/qmb/wheel
- name: List Downloaded RIA Hub contents
run: |
ls -lh /opt/qmb/riahub || true
ls -lh /opt/qmb/riahub/model || true
ls -lh /opt/qmb/riahub/dataset || true
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install Python dependencies
run: |
set -euo pipefail
uv pip install --system --index-url https://pypi.org/simple --upgrade /opt/qmb/wheel/*.whl
TORCH_INDEX_URL="https://download.pytorch.org/whl/cpu"
TORCH_REASON="no NVIDIA GPU detected"
if command -v nvidia-smi &> /dev/null; then
CAP_LINES="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true)"
if [[ -z "$CAP_LINES" ]]; then
CAP_LINES="$(nvidia-smi -q 2>/dev/null | awk -F: '/Compute Capability/ {print $2}')"
fi
CAP_MAX="$(echo "$CAP_LINES" | awk '{gsub(/[^0-9.]/,""); if ($0=="") next; if ($0+0>max) max=$0+0} END {if (max>0) print max}')"
if [[ -n "$CAP_MAX" ]]; then
if awk -v cap="$CAP_MAX" 'BEGIN{exit !(cap>=7.5)}'; then
TORCH_INDEX_URL="https://download.pytorch.org/whl/cu130"
TORCH_REASON="compute capability ${CAP_MAX} >= 7.5"
else
TORCH_INDEX_URL="https://download.pytorch.org/whl/cu126"
TORCH_REASON="compute capability ${CAP_MAX} < 7.5"
fi
fi
fi
echo "Installing PyTorch from ${TORCH_INDEX_URL} (${TORCH_REASON})."
uv pip install --system --index-url "$TORCH_INDEX_URL" --upgrade --force-reinstall torch torchvision
uv pip install --system --index-url https://pypi.org/simple --upgrade "onnxscript>=0.7.0" "onnx-ir>=0.2.1" onnx onnxruntime timm
- name: Run Training Script
run: |
cd /opt/qmb
export PYTHONPATH="$QMB_TASK_REPO_ROOT:${PYTHONPATH:-}"
#source .venv/bin/activate
qmb train --config /opt/qmb/configs/train.yaml
- name: Collect training artifacts
if: always()
run: |
set -euo pipefail
ARTIFACT_DIR="${{ github.workspace }}/.riahub/artifacts/training"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"
if [[ -d "$QMB_OUTPUT_ROOT" ]]; then
while IFS= read -r -d '' file; do
rel="${file#${QMB_OUTPUT_ROOT}/}"
if [[ "$rel" == "$file" ]]; then
rel="$(basename "$file")"
fi
mkdir -p "$ARTIFACT_DIR/$(dirname "$rel")"
cp "$file" "$ARTIFACT_DIR/$rel"
done < <(
find "$QMB_OUTPUT_ROOT" -type f \( \
-path "*/checkpoints/best.pt" -o \
-path "*/checkpoints/best.ckpt" -o \
-name "*.onnx" -o \
-path "*/evaluation/*/confusion_matrix.png" -o \
-path "*/evaluation/*/parameter_sweeps/*.png" \
\) -print0
)
else
echo "QMB output root not found: $QMB_OUTPUT_ROOT"
fi
echo "Collected training artifacts:"
find "$ARTIFACT_DIR" -type f -print | sort || true
- name: ⬆️ Upload training artifacts
if: always()
uses: actions/upload-artifact@v3
with:
name: training-artifacts
path: ${{ github.workspace }}/.riahub/artifacts/training
if-no-files-found: warn
# committed at 2026-05-27T03:13:26.168613+00:00

869
Applications/Demo_App.json Normal file
View File

@ -0,0 +1,869 @@
{
"app_name": "new_blocks.json",
"ops": [
{
"id": "69bd3deeff031ee6e72c0a9d-1776345526320",
"name": "pluto_source",
"type": "source",
"description": "PlutoSDR RX source operator. Acquires raw IQ samples from an Analog Devices PlutoSDR (AD9361) via libiio and emits them as shared complex vectors.",
"position": {
"x": 68.5360685699228,
"y": 357.7951264299131
},
"class_name": "PlutoSourceOp",
"is_builtin": false,
"inputs": [],
"outputs": [
{
"name": "iq_samples",
"type": "std::shared_ptr<std::vector<ComplexType>>"
}
],
"specs": [
{
"parameter": "ip_addr_",
"key": "ip_addr",
"headline": "Pluto IP Address",
"description": "IP address of the PlutoSDR device",
"type": "std::string",
"default": "192.168.3.1"
},
{
"parameter": "bandwidth_mhz_",
"key": "bandwidth_mhz",
"headline": "RF bandwidth",
"description": "RF analog bandwidth in MHz",
"type": "double",
"default": 2
},
{
"parameter": "sample_rate_mhz_",
"key": "sample_rate_mhz",
"headline": "RX sample rate",
"description": "RX baseband sample rate in MS/s",
"type": "double",
"default": 2.5
},
{
"parameter": "lo_frequency_ghz_",
"key": "lo_frequency_ghz",
"headline": "RX LO Frequency",
"description": "RF local oscillator frequency in GHz",
"type": "double",
"default": 0.0999
},
{
"parameter": "rx_gain_db_",
"key": "rx_gain_db",
"headline": "RX Gain",
"description": "Receiver gain in dB",
"type": "double",
"default": 20
},
{
"parameter": "rf_port_",
"key": "rf_port",
"headline": "RF port",
"description": "RX channel index",
"type": "int",
"default": 0
},
{
"parameter": "buffer_size_",
"key": "buffer_size",
"headline": "Buffer size",
"description": "Number of IQ sample pairs per buffer",
"type": "int",
"default": 4096
},
{
"parameter": "poll_rate_ms_",
"key": "poll_rate_ms",
"headline": "Poll Rate",
"description": "Minimum milliseconds between buffer refills",
"type": "int",
"default": 10
}
],
"dependencies": [
"#include <iio.h>",
"#include <iostream>",
"#include <cmath>",
"#include <cstdlib>"
],
"private_members": [
"struct iio_context* context_ = nullptr;",
"struct iio_device* rx_device_ = nullptr;",
"struct iio_channel* ch_i_ = nullptr;",
"struct iio_channel* ch_q_ = nullptr;",
"struct iio_buffer* buffer_ = nullptr;",
"std::chrono::high_resolution_clock::time_point last_read_time_;",
"std::atomic<double> runtime_rx_freq_ghz_{3.425};",
"std::atomic<double> runtime_rx_gain_db_{30.0};",
"std::atomic<bool> rx_params_dirty_{false};"
],
"has_initialize": true,
"initialize_body": "HOLOSCAN_LOG_INFO(\"Initializing Pluto RX stream\");\n Operator::initialize();\n runtime_rx_freq_ghz_.store(lo_frequency_ghz_.get());\n runtime_rx_gain_db_.store(rx_gain_db_.get());\n struct stream_cfg rxcfg {};\n rxcfg.bw_hz = MHZ(bandwidth_mhz_.get());\n rxcfg.fs_hz = MHZ(sample_rate_mhz_.get());\n rxcfg.lo_hz = GHZ(lo_frequency_ghz_.get());\n rxcfg.rfport = \"A_BALANCED\";\n const int port_index = rf_port_.get();\n std::string uri = \"ip:\" + ip_addr_.get();\n context_ = iio_create_context_from_uri(uri.c_str());\n if (!context_) return;\n if (!get_ad9361_stream_dev(context_, RX, &rx_device_)) return;\n if (!cfg_ad9361_streaming_ch(context_, &rxcfg, RX, port_index)) return;\n get_ad9361_stream_ch(RX, rx_device_, port_index, &ch_i_);\n get_ad9361_stream_ch(RX, rx_device_, port_index + 1, &ch_q_);\n iio_channel_enable(ch_i_);\n iio_channel_enable(ch_q_);\n buffer_ = iio_device_create_buffer(rx_device_, buffer_size_.get(), false);",
"compute_body": "auto samples_ptr = std::make_shared<std::vector<ComplexType>>();\n samples_ptr->reserve(buffer_size_.get());\n if (context_ && buffer_) {\n iio_buffer_refill(buffer_);\n const size_t step = iio_buffer_step(buffer_);\n const char* start = static_cast<char*>(iio_buffer_first(buffer_, ch_i_));\n const char* end = static_cast<char*>(iio_buffer_end(buffer_));\n for (const char* p = start; p < end; p += step) {\n const auto raw = reinterpret_cast<const int16_t*>(p);\n samples_ptr->emplace_back(raw[0], raw[1]);\n }\n } else {\n for (int i = 0; i < buffer_size_.get(); ++i) {\n samples_ptr->emplace_back(static_cast<float>(rand()%100), static_cast<float>(rand()%100));\n }\n }\n op_output.emit(samples_ptr, \"iq_samples\");",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0aa0-1776345543621",
"name": "preprocessor",
"type": "preprocessing",
"description": "Converts complex IQ samples into a GXF tensor suitable for ML inference. Separates real and imaginary components into interleaved float format with shape [1, 2, N].",
"position": {
"x": 414.64197818208436,
"y": 544.7366288284782
},
"class_name": "PreprocessorOp",
"is_builtin": false,
"inputs": [
{
"name": "iq_samples",
"type": "std::shared_ptr<std::vector<ComplexType>>"
}
],
"outputs": [
{
"name": "in_tensor",
"type": "gxf::Entity"
}
],
"specs": [
{
"parameter": "allocator_",
"key": "allocator",
"headline": "Alloc",
"description": "Memory allocator for tensor creation",
"type": "std::shared_ptr<holoscan::Allocator>",
"default": "pool_resource"
},
{
"parameter": "in_tensor_name_",
"key": "in_tensor_name",
"headline": "Name",
"description": "Name of the output tensor in the GXF entity",
"type": "std::string",
"default": "input"
}
],
"dependencies": [
"#include <gxf/std/tensor.hpp>"
],
"private_members": [],
"has_initialize": false,
"initialize_body": null,
"compute_body": "auto maybe_s = op_input.receive<std::shared_ptr<std::vector<ComplexType>>>(\"iq_samples\"); if(!maybe_s) return; const auto& s = *maybe_s.value(); auto [re,im] = convert_and_normalize_separate(s); auto ent = nvidia::gxf::Entity::New(context.context()).value(); auto tensor = ent.add<nvidia::gxf::Tensor>(in_tensor_name_.get().c_str()).value(); auto alloc = nvidia::gxf::Handle<nvidia::gxf::Allocator>::Create(context.context(), allocator_->gxf_cid()).value(); tensor->template reshape<float>(nvidia::gxf::Shape({1,2,static_cast<int>(s.size())}), nvidia::gxf::MemoryStorageType::kHost, alloc); float* d = tensor->template data<float>().value(); for(size_t i=0;i<s.size();++i){ d[2*i]=re[i]; d[2*i+1]=im[i]; } op_output.emit(ent);",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0aa1-1776345548184",
"name": "spectrogram",
"type": "preprocessing",
"description": "FFT-based spectrogram generator using FFTW3. Produces RGBA waterfall rows with configurable colormap, windowing, and ADC input scaling. Emits only new rows for efficient incremental rendering.",
"position": {
"x": 503.6426850135131,
"y": 41.98771759801264
},
"class_name": "SpectrogramOp",
"is_builtin": false,
"inputs": [
{
"name": "iq_samples",
"type": "std::shared_ptr<std::vector<ComplexType>>"
}
],
"outputs": [
{
"name": "new_rows",
"type": "std::shared_ptr<std::vector<uint8_t>>"
},
{
"name": "spectrogram_width",
"type": "int"
},
{
"name": "spectrogram_height",
"type": "int"
},
{
"name": "offset_row",
"type": "int"
},
{
"name": "num_new_rows",
"type": "int"
}
],
"specs": [
{
"parameter": "fft_size_",
"key": "fft_size",
"headline": "FFT Size",
"description": "Number of FFT points",
"type": "int",
"default": 2048
},
{
"parameter": "history_depth_",
"key": "history_depth",
"headline": "History Depth",
"description": "Number of rows in the circular waterfall buffer",
"type": "int",
"default": 256
},
{
"parameter": "hop_size_",
"key": "hop_size",
"headline": "Hop Size",
"description": "Samples between successive FFT frames",
"type": "int",
"default": 2048
},
{
"parameter": "db_min_",
"key": "db_min",
"headline": "Min dB",
"description": "Floor of the dB dynamic range for colormap mapping",
"type": "float",
"default": -80
},
{
"parameter": "db_max_",
"key": "db_max",
"headline": "Max dB",
"description": "Ceiling of the dB dynamic range for colormap mapping",
"type": "float",
"default": 0
},
{
"parameter": "input_scale_",
"key": "input_scale",
"headline": "Input Scale",
"description": "Scale factor for raw input samples, baked into window coefficients (e.g. 1/2048 for PlutoSDR int16, 1.0 for normalized float)",
"type": "float",
"default": 0.000488281
}
],
"dependencies": [
"#include <complex>",
"#include <vector>",
"#include <cmath>",
"#include <algorithm>",
"#include <cstring>",
"#define M_PI 3.14159265358979323846",
"#include <fftw3.h>"
],
"private_members": [
"fftwf_plan fft_plan_ = nullptr;",
"fftwf_complex* fft_in_ = nullptr;",
"fftwf_complex* fft_out_ = nullptr;",
"std::vector<float> window_coeffs_;",
"std::vector<uint8_t> texture_data_;",
"std::vector<uint32_t> colormap_lut_;",
"int write_row_ = 0;",
"static float fast_log2f(float x) { union { float f; uint32_t i; } u; u.f = x; return (float)((int32_t)u.i - 1064866805) * 8.2629582881927490e-8f; }",
"int prev_write_row_ = 0;"
],
"has_initialize": true,
"initialize_body": "holoscan::Operator::initialize(); int n = fft_size_.get(); int depth = history_depth_.get(); fft_in_ = fftwf_alloc_complex(n); fft_out_ = fftwf_alloc_complex(n); fft_plan_ = fftwf_plan_dft_1d(n, fft_in_, fft_out_, FFTW_FORWARD, FFTW_MEASURE); window_coeffs_.resize(n); const float adc_scale = input_scale_.get(); for (int i = 0; i < n; ++i) { window_coeffs_[i] = 0.5f * (1.0f - std::cos(2.0f * M_PI * i / (n - 1))) * adc_scale; } write_row_ = 0; /* Purple-to-yellow (Plasma) colormap */ colormap_lut_.resize(1024); const float stops[][4] = { {0.00f, 13.0f, 8.0f, 135.0f}, {0.25f, 126.0f, 3.0f, 168.0f}, {0.50f, 203.0f, 70.0f, 121.0f}, {0.75f, 249.0f, 149.0f, 64.0f}, {1.00f, 240.0f, 249.0f, 33.0f} }; const int ns = 5; for (int i = 0; i < 1024; ++i) { float t = i / 1023.0f; int seg = 0; for (int s = 0; s < ns - 1; ++s) { if (t >= stops[s][0]) seg = s; } float lt = (t - stops[seg][0]) / (stops[seg+1][0] - stops[seg][0]); lt = std::clamp(lt, 0.0f, 1.0f); uint8_t r = static_cast<uint8_t>(stops[seg][1] + (stops[seg+1][1] - stops[seg][1]) * lt); uint8_t g = static_cast<uint8_t>(stops[seg][2] + (stops[seg+1][2] - stops[seg][2]) * lt); uint8_t b = static_cast<uint8_t>(stops[seg][3] + (stops[seg+1][3] - stops[seg][3]) * lt); colormap_lut_[i] = (255u << 24) | (b << 16) | (g << 8) | r; } texture_data_.assign(n * depth * 4, 0); HOLOSCAN_LOG_INFO(\"FFTW plan created for N={}\", n);",
"compute_body": "auto s_in = op_input.receive<std::shared_ptr<std::vector<ComplexType>>>(\"iq_samples\"); if (!s_in || s_in.value()->empty()) return; const auto& samples = *s_in.value(); const int n = fft_size_.get(); const int hop = hop_size_.get(); const int depth = history_depth_.get(); const float db_min = db_min_.get(); const float db_range = db_max_.get() - db_min; const float inv_n2 = 1.0f / (float)(n * n); const float log2_to_db = 3.01029995663981f; prev_write_row_ = write_row_; for (size_t start = 0; start + n <= samples.size(); start += hop) { for (int i = 0; i < n; ++i) { fft_in_[i][0] = samples[start + i].real() * window_coeffs_[i]; fft_in_[i][1] = samples[start + i].imag() * window_coeffs_[i]; } fftwf_execute(fft_plan_); uint32_t* row_ptr = reinterpret_cast<uint32_t*>(texture_data_.data()) + (write_row_ * n); for (int i = 0; i < n; ++i) { int si = (i + n/2) % n; float re = fft_out_[si][0], im = fft_out_[si][1]; float mag_sq = (re * re + im * im) * inv_n2; float db = log2_to_db * fast_log2f(mag_sq + 1e-20f); float norm = std::clamp((db - db_min) / db_range, 0.0f, 1.0f); row_ptr[i] = colormap_lut_[static_cast<int>(norm * 1023.0f)]; } write_row_ = (write_row_ + 1) % depth; } /* Emit only the new rows */ int num_new = (write_row_ - prev_write_row_ + depth) % depth; if (num_new > 0) { auto new_rows = std::make_shared<std::vector<uint8_t>>(num_new * n * 4); for (int r = 0; r < num_new; ++r) { int src_row = (prev_write_row_ + r) % depth; std::memcpy(new_rows->data() + r * n * 4, texture_data_.data() + src_row * n * 4, n * 4); } op_output.emit(new_rows, \"new_rows\"); op_output.emit(n, \"spectrogram_width\"); op_output.emit(depth, \"spectrogram_height\"); op_output.emit(write_row_, \"offset_row\"); op_output.emit(num_new, \"num_new_rows\"); }",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0a9b-1776345577288",
"name": "inference_builtin",
"type": "inference",
"description": "Built-in Holoscan InferenceOp. Runs ONNX Runtime or TensorRT inference on GXF tensor entities.",
"position": {
"x": 651.8201808383262,
"y": 542.6597535841762
},
"class_name": "InferenceOp",
"is_builtin": true,
"inputs": [
{
"name": "receivers",
"type": "gxf::Entity"
}
],
"outputs": [
{
"name": "transmitter",
"type": "gxf::Entity"
}
],
"specs": [
{
"parameter": "backend",
"key": "backend",
"headline": "Backend",
"description": "Inference backend: onnxrt or trt",
"type": "std::string",
"default": "onnxrt"
},
{
"parameter": "model_path_map_",
"key": "model_path_map",
"headline": "Model Path Map",
"description": "Map of model name to ONNX/TRT model file path",
"type": "std::map<std::string, std::string>",
"default": "interference_recognition_fp32.onnx"
},
{
"parameter": "pre_processor_map_",
"key": "pre_processor_map",
"headline": "Pre-processor Map",
"description": "Map of model name to input tensor names",
"type": "std::map<std::string, std::vector<std::string>>",
"default": "input"
},
{
"parameter": "inference_map_",
"key": "inference_map",
"headline": "Inference Map",
"description": "Map of model name to output tensor names",
"type": "std::map<std::string, std::vector<std::string>>",
"default": "output"
},
{
"parameter": "input_on_cuda",
"key": "input_on_cuda",
"headline": "Input on CUDA",
"description": null,
"type": "bool",
"default": false
},
{
"parameter": "output_on_cuda",
"key": "output_on_cuda",
"headline": "Output on CUDA",
"description": null,
"type": "bool",
"default": false
},
{
"parameter": "transmit_on_cuda",
"key": "transmit_on_cuda",
"headline": "Transmit on CUDA",
"description": null,
"type": "bool",
"default": false
},
{
"parameter": "allocator",
"key": "allocator",
"headline": "Allocator",
"description": null,
"type": "std::shared_ptr<holoscan::Allocator>",
"default": "pool_resource"
}
],
"dependencies": [],
"private_members": [],
"has_initialize": false,
"initialize_body": null,
"compute_body": "",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0a9f-1776345584342",
"name": "postprocessor",
"type": "postprocessing",
"description": "Extracts classification logits from inference output tensor, applies softmax, and emits probabilities as a shared float vector. Supports both CPU and CUDA tensors.",
"position": {
"x": 1008.3165490811291,
"y": 545.574956436883
},
"class_name": "PostprocessorOp",
"is_builtin": false,
"inputs": [
{
"name": "out_tensor",
"type": "gxf::Entity"
}
],
"outputs": [
{
"name": "predictions",
"type": "std::shared_ptr<std::vector<float>>"
}
],
"specs": [
{
"parameter": "out_tensor_name_",
"key": "out_tensor_name",
"headline": "Output Tensor Name",
"description": "Name of the output tensor to extract from the GXF entity",
"type": "std::string",
"default": "output"
}
],
"dependencies": [
"#include <gxf/std/tensor.hpp>",
"#include <cstring>",
"#include <vector>",
"#include <memory>",
"#include <cuda_runtime_api.h>"
],
"private_members": [],
"has_initialize": false,
"initialize_body": null,
"compute_body": "auto maybe_in_message = op_input.receive<gxf::Entity>(\"out_tensor\");\n if (!maybe_in_message) return;\n auto maybe_tensor = maybe_in_message.value().get<holoscan::Tensor>(out_tensor_name_.get().c_str());\n if (!maybe_tensor) { HOLOSCAN_LOG_ERROR(\"Output tensor '{}' not found\", out_tensor_name_.get()); return; }\n const auto& shape = maybe_tensor->shape();\n if (shape.empty()) { HOLOSCAN_LOG_ERROR(\"Invalid tensor shape: empty\"); return; }\n const auto classes_dim = shape.back();\n if (classes_dim <= 0 || classes_dim > 65536) { HOLOSCAN_LOG_ERROR(\"Invalid class dimension: {}\", classes_dim); return; }\n const size_t num_classes = static_cast<size_t>(classes_dim);\n const size_t needed_bytes = num_classes * sizeof(float);\n if (maybe_tensor->nbytes() < needed_bytes) {\n HOLOSCAN_LOG_ERROR(\"Output tensor '{}' too small: {} bytes (need {})\", out_tensor_name_.get(), maybe_tensor->nbytes(), needed_bytes);\n return;\n }\n DLDevice dev = maybe_tensor->device();\n DLDataType dtype = maybe_tensor->dtype();\n if (dtype.code != kDLFloat || dtype.bits != 32) {\n HOLOSCAN_LOG_ERROR(\"Output tensor '{}' is not float32 (code={}, bits={})\", out_tensor_name_.get(), static_cast<int>(dtype.code), static_cast<int>(dtype.bits));\n return;\n }\n const void* raw_ptr = maybe_tensor->data();\n if (!raw_ptr) { HOLOSCAN_LOG_ERROR(\"Null tensor data pointer\"); return; }\n std::vector<float> host_logits(num_classes);\n if (dev.device_type == kDLCPU) {\n std::memcpy(host_logits.data(), raw_ptr, needed_bytes);\n } else if (dev.device_type == kDLCUDA) {\n cudaError_t err = cudaMemcpy(host_logits.data(), raw_ptr, needed_bytes, cudaMemcpyDeviceToHost);\n if (err != cudaSuccess) {\n HOLOSCAN_LOG_ERROR(\"cudaMemcpy DeviceToHost failed for '{}': {}\", out_tensor_name_.get(), cudaGetErrorString(err));\n return;\n }\n } else {\n HOLOSCAN_LOG_ERROR(\"Unsupported output tensor device type {} for '{}'\", static_cast<int>(dev.device_type), out_tensor_name_.get());\n return;\n }\n auto probs = std::make_shared<std::vector<float>>(softmax(host_logits.data(), num_classes));\n if (probs->empty()) { HOLOSCAN_LOG_ERROR(\"Softmax output is empty\"); return; }\n op_output.emit(probs, \"predictions\");",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0aa2-1776345597091",
"name": "spectrogram_dashboard",
"type": "sink",
"description": "Flexible HTTP dashboard server. Accepts optional spectrogram RGBA rows, prediction maps, and emits TX control messages. Serves spectrogram data and status JSON via a POSIX HTTP server. All inputs are optional — connect only what you need.",
"position": {
"x": 1534.7404760043617,
"y": 27.35610520025739
},
"class_name": "SpectrogramDashboardOp",
"is_builtin": false,
"inputs": [
{
"name": "new_rows",
"type": "std::shared_ptr<std::vector<uint8_t>>"
},
{
"name": "spectrogram_width",
"type": "int"
},
{
"name": "spectrogram_height",
"type": "int"
},
{
"name": "offset_row",
"type": "int"
},
{
"name": "num_new_rows",
"type": "int"
},
{
"name": "predictions",
"type": "std::map<std::string, float>"
}
],
"outputs": [
{
"name": "tx_control_message",
"type": "std::shared_ptr<RadioControlMessage>"
}
],
"specs": [
{
"parameter": "websocket_port_",
"key": "websocket_port",
"headline": "HTTP Port",
"description": "Port number for the HTTP data server",
"type": "int",
"default": 8080
},
{
"parameter": "web_root_path_",
"key": "web_root_path",
"headline": "Web Root Path",
"description": "Root directory for web assets (used for file-based data export)",
"type": "std::string",
"default": "workspace/app/web"
},
{
"parameter": "update_rate_hz_",
"key": "update_rate_hz",
"headline": "Update Rate (Hz)",
"description": "Target update rate for the dashboard data",
"type": "int",
"default": 30
}
],
"dependencies": [
"#include <sys/socket.h>",
"#include <netinet/in.h>",
"#include <arpa/inet.h>",
"#include <unistd.h>",
"#include <iomanip>",
"#include <sstream>",
"#include <algorithm>",
"#include <cmath>",
"#include <thread>",
"#include <atomic>",
"#include <mutex>",
"#include <set>",
"#include <chrono>",
"#include <fstream>",
"#include \"../utils/utils.hpp\""
],
"private_members": [
"int server_socket_ = -1;",
"std::thread server_thread_;",
"std::atomic<bool> server_running_{false};",
"std::map<std::string, float> smoothed_predictions_;",
"std::vector<uint8_t> current_rgba_buffer_;",
"int current_width_ = 0;",
"int current_height_ = 0;",
"int current_offset_ = 0;",
"std::mutex data_mutex_;",
"double tx_center_freq_mhz_ = 3425.0;",
"double tx_gain_db_ = -10.0;",
"bool tx_enabled_ = false;",
"int tx_signal_type_ = 6;",
"std::vector<std::pair<int,double>> pending_msgs_;",
"std::chrono::steady_clock::time_point last_update_;",
"const std::chrono::milliseconds update_interval_{33};",
"void cleanup() { if (server_running_.load()) { server_running_.store(false); if (server_socket_ >= 0) { close(server_socket_); } if (server_thread_.joinable()) { server_thread_.join(); } } }",
"std::string generate_json_response() { std::stringstream ss; ss << \"{\\\"width\\\":\" << current_width_ << \",\"; ss << \"\\\"height\\\":\" << current_height_ << \",\"; ss << \"\\\"offset_row\\\":\" << current_offset_ << \",\"; ss << \"\\\"tx_enabled\\\":\" << (tx_enabled_ ? \"true\" : \"false\") << \",\"; ss << \"\\\"tx_freq_mhz\\\":\" << tx_center_freq_mhz_ << \",\"; ss << \"\\\"tx_gain_db\\\":\" << tx_gain_db_ << \",\"; ss << \"\\\"tx_signal_type\\\":\" << tx_signal_type_ << \",\"; ss << \"\\\"predictions\\\":{\"; bool first = true; for (const auto& [label, prob] : smoothed_predictions_) { if (!first) ss << \",\"; ss << \"\\\"\" << label << \"\\\":\" << prob; first = false; } ss << \"}}\"; return ss.str(); }",
"void handle_client(int client_socket) { char req_buf[4096] = {0}; int bytes_read = ::read(client_socket, req_buf, sizeof(req_buf) - 1); if (bytes_read <= 0) { close(client_socket); return; } std::string request(req_buf, bytes_read); if (request.find(\"OPTIONS \") == 0) { std::string resp = \"HTTP/1.1 204 No Content\\r\\nAccess-Control-Allow-Origin: *\\r\\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\\r\\nAccess-Control-Allow-Headers: Content-Type\\r\\n\\r\\n\"; send(client_socket, resp.c_str(), resp.length(), MSG_NOSIGNAL); return; } if (request.find(\"POST /tx_control\") != std::string::npos) { std::string body; auto bp = request.find(\"\\r\\n\\r\\n\"); if (bp != std::string::npos) body = request.substr(bp + 4); { std::lock_guard<std::mutex> lock(data_mutex_); if (body.find(\"\\\"toggle_tx\\\"\") != std::string::npos) { tx_enabled_ = !tx_enabled_; pending_msgs_.push_back({tx_enabled_ ? 4 : 5, 0.0}); } if (body.find(\"\\\"set_signal_type\\\"\") != std::string::npos) { auto vp = body.find(\"\\\"value\\\":\"); if (vp != std::string::npos) { tx_signal_type_ = std::atoi(body.c_str() + vp + 8); pending_msgs_.push_back({7, static_cast<double>(tx_signal_type_)}); } } if (body.find(\"\\\"set_freq\\\"\") != std::string::npos) { auto vp = body.find(\"\\\"value\\\":\"); if (vp != std::string::npos) { double fv = std::atof(body.c_str() + vp + 8); tx_center_freq_mhz_ = fv; pending_msgs_.push_back({1, fv}); } } if (body.find(\"\\\"set_gain\\\"\") != std::string::npos) { auto vp = body.find(\"\\\"value\\\":\"); if (vp != std::string::npos) { double gv = std::atof(body.c_str() + vp + 8); tx_gain_db_ = gv; pending_msgs_.push_back({3, gv}); } } } std::string rb; { std::lock_guard<std::mutex> lock(data_mutex_); rb = generate_json_response(); } std::string resp = \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nAccess-Control-Allow-Origin: *\\r\\nContent-Length: \" + std::to_string(rb.size()) + \"\\r\\n\\r\\n\" + rb; send(client_socket, resp.c_str(), resp.length(), MSG_NOSIGNAL); return; } bool is_raw = (request.find(\"GET /spectrogram.raw\") != std::string::npos); std::lock_guard<std::mutex> lock(data_mutex_); if (is_raw && !current_rgba_buffer_.empty()) { std::string header = \"HTTP/1.1 200 OK\\r\\nContent-Type: application/octet-stream\\r\\nAccess-Control-Allow-Origin: *\\r\\nCache-Control: no-store\\r\\nContent-Length: \" + std::to_string(current_rgba_buffer_.size()) + \"\\r\\n\\r\\n\"; send(client_socket, header.c_str(), header.length(), MSG_NOSIGNAL); size_t total_sent = 0; while (total_sent < current_rgba_buffer_.size()) { ssize_t sent = send(client_socket, current_rgba_buffer_.data() + total_sent, current_rgba_buffer_.size() - total_sent, MSG_NOSIGNAL); if (sent <= 0) break; total_sent += sent; } } else { std::string body = generate_json_response(); std::string response = \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nAccess-Control-Allow-Origin: *\\r\\nCache-Control: no-store\\r\\nContent-Length: \" + std::to_string(body.size()) + \"\\r\\n\\r\\n\" + body; send(client_socket, response.c_str(), response.length(), MSG_NOSIGNAL); } }",
"void run_server() { while (server_running_.load()) { struct sockaddr_in address; int addrlen = sizeof(address); int client_socket = accept(server_socket_, (struct sockaddr*)&address, (socklen_t*)&addrlen); if (client_socket >= 0) { handle_client(client_socket); close(client_socket); } } }",
"void write_data_file() { std::lock_guard<std::mutex> lock(data_mutex_); if (!current_rgba_buffer_.empty() && !web_root_path_.get().empty()) { std::string path = web_root_path_.get() + \"/public/spectrogram.raw\"; std::ofstream file(path, std::ios::binary); file.write(reinterpret_cast<const char*>(current_rgba_buffer_.data()), current_rgba_buffer_.size()); } }"
],
"has_initialize": true,
"initialize_body": "holoscan::Operator::initialize();\n \n server_socket_ = socket(AF_INET, SOCK_STREAM, 0);\n if (server_socket_ < 0) {\n HOLOSCAN_LOG_ERROR(\"Failed to create socket\");\n return;\n }\n \n int opt = 1;\n setsockopt(server_socket_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));\n \n struct sockaddr_in address;\n address.sin_family = AF_INET;\n address.sin_addr.s_addr = INADDR_ANY;\n address.sin_port = htons(websocket_port_.get());\n \n if (bind(server_socket_, (struct sockaddr*)&address, sizeof(address)) < 0) {\n HOLOSCAN_LOG_ERROR(\"Failed to bind socket to port {}\", websocket_port_.get());\n close(server_socket_);\n return;\n }\n \n if (listen(server_socket_, 3) < 0) {\n HOLOSCAN_LOG_ERROR(\"Failed to listen on socket\");\n close(server_socket_);\n return;\n }\n \n server_running_.store(true);\n server_thread_ = std::thread([this]() { run_server(); });\n \n last_update_ = std::chrono::steady_clock::now();\n HOLOSCAN_LOG_INFO(\"HTTP server started on port {}\", websocket_port_.get());",
"compute_body": "auto spec_in = op_input.receive<std::shared_ptr<std::vector<uint8_t>>>(\"new_rows\");\n auto w_in = op_input.receive<int>(\"spectrogram_width\");\n auto h_in = op_input.receive<int>(\"spectrogram_height\");\n auto offset_in = op_input.receive<int>(\"offset_row\");\n auto nr_in = op_input.receive<int>(\"num_new_rows\");\n auto p_in = op_input.receive<std::map<std::string, float>>(\"predictions\");\n \n {\n std::lock_guard<std::mutex> lock(data_mutex_);\n if (spec_in && w_in && h_in && offset_in && nr_in) {\n int w = w_in.value();\n int h = h_in.value();\n int num_new = nr_in.value();\n size_t buf_size = static_cast<size_t>(w) * h * 4;\n if (current_rgba_buffer_.size() != buf_size) current_rgba_buffer_.resize(buf_size, 0);\n current_width_ = w;\n current_height_ = h;\n current_offset_ = offset_in.value();\n if (num_new > 0) {\n const auto& new_data = *spec_in.value();\n int row_bytes = w * 4;\n int start_row = (current_offset_ - num_new + h) % h;\n for (int r = 0; r < num_new; ++r) {\n int dst_row = (start_row + r) % h;\n std::memcpy(current_rgba_buffer_.data() + dst_row * row_bytes,\n new_data.data() + r * row_bytes, row_bytes);\n }\n }\n }\n if (p_in) {\n const float alpha = 0.15f; auto raw = p_in.value(); if (smoothed_predictions_.empty()) { smoothed_predictions_ = raw; } else { for (auto& [label, val] : raw) { smoothed_predictions_[label] = alpha * val + (1.0f - alpha) * smoothed_predictions_[label]; } }\n }\n }\n \n { std::lock_guard<std::mutex> lock2(data_mutex_);\n if (!pending_msgs_.empty()) {\n auto [t, v] = pending_msgs_.front();\n pending_msgs_.erase(pending_msgs_.begin());\n auto msg = std::make_shared<RadioControlMessage>();\n msg->type = static_cast<RadioControlMessage::Type>(t);\n msg->value = v;\n op_output.emit(msg, \"tx_control_message\");\n }\n }",
"class": "",
"compute": ""
},
{
"id": "69bd3deeff031ee6e72c0a9c-1776345675756",
"name": "model_mapper",
"type": "postprocessing",
"description": "Maps numeric prediction probabilities to human-readable class labels. Accepts a shared float vector and emits a label-to-probability map.",
"position": {
"x": 1319.1869982595924,
"y": 551.0619693437972
},
"class_name": "ModelMapperOp",
"is_builtin": false,
"inputs": [
{
"name": "predictions",
"type": "std::shared_ptr<std::vector<float>>"
}
],
"outputs": [
{
"name": "labeled_predictions",
"type": "std::map<std::string, float>"
}
],
"specs": [
{
"parameter": "labels_",
"key": "labels",
"headline": "Class Labels",
"description": "Ordered list of class label strings corresponding to prediction indices",
"type": "std::vector<std::string>",
"default": "[\"Bleedover\", \"Collisions\", \"Parallel\", \"Birdies\", \"CTNB\", \"LFM\", \"None\", \"Jamming\"]"
}
],
"dependencies": [
"#include <map>",
"#include <string>",
"#include <vector>",
"#include <memory>"
],
"private_members": [],
"has_initialize": false,
"initialize_body": null,
"compute_body": "auto p = op_input.receive<std::shared_ptr<std::vector<float>>>(\"predictions\"); if(!p) return; const auto& probs = *p.value(); size_t n = probs.size(); std::map<std::string,float> r; for(size_t i=0;i<n;++i){ r[(i<labels_.get().size())?labels_.get()[i]:std::to_string(i)] = probs[i]; } op_output.emit(r, \"labeled_predictions\");",
"class": "",
"compute": ""
},
{
"id": "69d80d08aabdb365e3b84e14-1776361788176",
"name": "PlutoTXOp",
"type": "sink",
"description": "Transmits IQ samples through an ADALM-Pluto SDR via libiio.",
"position": {
"x": 2040.5281414494543,
"y": 50.98952706451257
},
"class_name": "PlutoTXOp",
"is_builtin": false,
"inputs": [
{
"name": "signal_in",
"type": "std::shared_ptr<std::vector<std::complex<float>>>"
}
],
"outputs": [],
"specs": [
{
"parameter": "uri_",
"key": "uri",
"headline": "Pluto URI",
"description": null,
"type": "std::string",
"default": "192.168.3.1"
},
{
"parameter": "sample_rate_",
"key": "sample_rate",
"headline": "TX sample rate in Hz",
"description": null,
"type": "int64_t",
"default": 2500000
},
{
"parameter": "center_freq_",
"key": "center_freq",
"headline": "TX center frequency in Hz",
"description": null,
"type": "int64_t",
"default": "2415500000"
},
{
"parameter": "bandwidth_",
"key": "bandwidth",
"headline": "TX RF bandwidth in Hz",
"description": null,
"type": "int64_t",
"default": 2000000
},
{
"parameter": "attenuation_",
"key": "attenuation",
"headline": "TX attenuation in mdB (negative)",
"description": null,
"type": "int64_t",
"default": "0"
}
],
"dependencies": [
"#include <ria/operator.hpp>",
"#include <iio.h>",
"#include <complex>",
"#include <vector>"
],
"private_members": [
"struct iio_context* ctx_ = nullptr;",
"struct iio_device* phy_ = nullptr;",
"struct iio_device* txdev_ = nullptr;",
"struct iio_channel* tx0_i_ = nullptr;",
"struct iio_channel* tx0_q_ = nullptr;",
"struct iio_buffer* txbuf_ = nullptr;"
],
"has_initialize": true,
"initialize_body": "ria::Operator::initialize();\nctx_ = iio_create_context_from_uri(uri_.get().c_str());\nif (!ctx_) { std::cerr << \"Cannot connect to Pluto at \" << uri_.get() << std::endl; return; }\nphy_ = iio_context_find_device(ctx_, \"ad9361-phy\");\niio_channel_attr_write_longlong(iio_device_find_channel(phy_, \"altvoltage1\", true), \"frequency\", center_freq_.get());\niio_channel_attr_write_longlong(iio_device_find_channel(phy_, \"voltage0\", true), \"sampling_frequency\", sample_rate_.get());\niio_channel_attr_write_longlong(iio_device_find_channel(phy_, \"voltage0\", true), \"rf_bandwidth\", bandwidth_.get());\niio_channel_attr_write_longlong(iio_device_find_channel(phy_, \"voltage0\", true), \"hardwaregain\", attenuation_.get());\ntxdev_ = iio_context_find_device(ctx_, \"cf-ad9361-dds-core-lpc\");\ntx0_i_ = iio_device_find_channel(txdev_, \"voltage0\", true);\ntx0_q_ = iio_device_find_channel(txdev_, \"voltage1\", true);\niio_channel_enable(tx0_i_);\niio_channel_enable(tx0_q_);\ntxbuf_ = iio_device_create_buffer(txdev_, 1024, false);",
"compute_body": "auto signal = input.receive<std::shared_ptr<std::vector<std::complex<float>>>>(\"signal_in\").value();\nint16_t* buf = static_cast<int16_t*>(iio_buffer_first(txbuf_, tx0_i_));\nsize_t n = std::min(signal->size(), static_cast<size_t>(1024));\nfor (size_t i = 0; i < n; ++i) {\n buf[2*i] = static_cast<int16_t>((*signal)[i].real() * 2048.0f);\n buf[2*i+1] = static_cast<int16_t>((*signal)[i].imag() * 2048.0f);\n}\niio_buffer_push(txbuf_);",
"class": "",
"compute": ""
}
],
"flows": [
{
"upstream": "69bd3deeff031ee6e72c0a9d-1776345526320",
"downstream": "69bd3deeff031ee6e72c0aa0-1776345543621",
"port_pairs": {
"iq_samples": "iq_samples"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "iq_samples → iq_samples",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa0-1776345543621",
"downstream": "69bd3deeff031ee6e72c0a9b-1776345577288",
"port_pairs": {
"in_tensor": "receivers"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "in_tensor → receivers",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0a9b-1776345577288",
"downstream": "69bd3deeff031ee6e72c0a9f-1776345584342",
"port_pairs": {
"transmitter": "out_tensor"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "transmitter → out_tensor",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"num_new_rows": "num_new_rows"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "num_new_rows → num_new_rows",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"offset_row": "offset_row"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "offset_row → offset_row",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"spectrogram_height": "spectrogram_height"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "spectrogram_height → spectrogram_height",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"spectrogram_width": "spectrogram_width"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "spectrogram_width → spectrogram_width",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"new_rows": "new_rows"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "new_rows → new_rows",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0a9f-1776345584342",
"downstream": "69bd3deeff031ee6e72c0a9c-1776345675756",
"port_pairs": {
"predictions": "predictions"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "predictions → predictions",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0a9c-1776345675756",
"downstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"port_pairs": {
"labeled_predictions": "predictions"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "labeled_predictions → predictions",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0aa2-1776345597091",
"downstream": "69d80d08aabdb365e3b84e14-1776361788176",
"port_pairs": {
"tx_control_message": "signal_in"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "tx_control_message → signal_in",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
},
{
"upstream": "69bd3deeff031ee6e72c0a9d-1776345526320",
"downstream": "69bd3deeff031ee6e72c0aa1-1776345548184",
"port_pairs": {
"iq_samples": "iq_samples"
},
"sourcePosition": "right",
"targetPosition": "left",
"label": "iq_samples → iq_samples",
"animated": false,
"style": {
"stroke": "#4183c4",
"strokeWidth": 1.5,
"strokeDasharray": "5 4",
"opacity": 0.7
}
}
]
}

BIN
datasets/ash_test.h5 (Stored with Git LFS)

Binary file not shown.

BIN
datasets/test_demo_v1.0.0.h5 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
datasets/train_demo_v1.0.0.h5 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
datasets/val_demo_v1.0.0.h5 (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.