From 45861f40477015d93d47a3722f785e2bc4be9165 Mon Sep 17 00:00:00 2001 From: Ayzen Date: Fri, 2 Oct 2026 16:05:22 +0300 Subject: [PATCH] sar3d: GPU port of the notebook with four views The sar3d processor is rewritten as a checked port of Multi_antenna_3D_clean_v1.ipynb: NVRTC kernels, timing from the trace stamps, notebook parameter names and one cubic SAR_VOXEL_M. It adds front, side, top and 3D views, drops gpr_cpp and the parity tools, and the GUI speed now applies only while the socket speed is ignored. --- .gitignore | 2 - Makefile | 87 +- README.md | 68 +- .../gpr_cpp/CMakeLists.txt | 78 - data_acq_and_processing/gpr_cpp/README.md | 85 - .../gpr_cpp/build_cmake.sh | 87 - .../gpr_cpp/examples/standalone_bp.cpp | 159 - .../gpr_cpp/include/gpr/ascan.hpp | 81 - .../gpr_cpp/include/gpr/config.hpp | 114 - .../gpr_cpp/include/gpr/device.hpp | 52 - .../gpr_cpp/include/gpr/geometry.hpp | 41 - .../gpr_cpp/include/gpr/nvrtc_kernel.hpp | 25 - .../gpr_cpp/include/gpr/sar3d.hpp | 55 - .../gpr_cpp/include/gpr/types.hpp | 25 - data_acq_and_processing/gpr_cpp/src/ascan.cpp | 250 -- .../gpr_cpp/src/device.cpp | 34 - .../gpr_cpp/src/geometry.cpp | 104 - .../gpr_cpp/src/kernel_src.hpp | 83 - .../gpr_cpp/src/nvrtc_kernel.cpp | 48 - data_acq_and_processing/gpr_cpp/src/sar3d.cpp | 233 - .../third_party/pocketfft/pocketfft_hdronly.h | 3853 ----------------- .../data_processor/include/data_processor.hpp | 10 +- .../include/processing_live_config.hpp | 50 +- .../data_processor/src/data_processor.cpp | 32 +- .../src/processing_live_config.cpp | 35 +- .../processors/include/sar3d_processor.hpp | 10 +- .../processing/processors/src/sar3d_gpu.cpp | 471 ++ .../processing/processors/src/sar3d_gpu.hpp | 91 + .../processors/src/sar3d_processor.cpp | 1211 +++--- .../processing/tools/sar3d_parity_main.cpp | 349 -- python_app/gui/app_window.py | 3 - .../live_processing_mixin.py | 72 +- .../app_window_config/profile_io_mixin.py | 56 +- .../app_window_config/state_builders.py | 26 +- .../app_window_plot/sar3d_plot_mixin.py | 270 +- .../gui/controllers/app_window_ui_mixin.py | 27 +- .../sections/processing_section.py | 323 +- python_app/gui/runtime/sar3d_views.py | 30 - .../hardware_full/laser_control/protocol.py | 7 +- python_app/models/gui_profile_codec.py | 274 +- python_app/models/gui_profile_schema.py | 36 +- .../orchestration/live_processing_config.py | 59 +- python_app/scripts/replay_smoke_run.py | 262 -- python_app/scripts/sar3d_reference_check.py | 389 -- python_app/scripts/sar3d_volume_compare.py | 89 - python_app/tests/test_gui_profile_codec.py | 26 + .../tests/test_socket_speed_controls.py | 65 + requirements.txt | 6 +- .../run_config_replay_vna.example.json | 17 +- start.sh | 21 +- 50 files changed, 1737 insertions(+), 8144 deletions(-) delete mode 100644 data_acq_and_processing/gpr_cpp/CMakeLists.txt delete mode 100644 data_acq_and_processing/gpr_cpp/README.md delete mode 100644 data_acq_and_processing/gpr_cpp/build_cmake.sh delete mode 100644 data_acq_and_processing/gpr_cpp/examples/standalone_bp.cpp delete mode 100644 data_acq_and_processing/gpr_cpp/include/gpr/ascan.hpp delete mode 100644 data_acq_and_processing/gpr_cpp/include/gpr/config.hpp delete mode 100644 data_acq_and_processing/gpr_cpp/include/gpr/device.hpp delete mode 100644 data_acq_and_processing/gpr_cpp/include/gpr/geometry.hpp delete mode 100644 data_acq_and_processing/gpr_cpp/include/gpr/nvrtc_kernel.hpp delete mode 100644 data_acq_and_processing/gpr_cpp/include/gpr/sar3d.hpp delete mode 100644 data_acq_and_processing/gpr_cpp/include/gpr/types.hpp delete mode 100644 data_acq_and_processing/gpr_cpp/src/ascan.cpp delete mode 100644 data_acq_and_processing/gpr_cpp/src/device.cpp delete mode 100644 data_acq_and_processing/gpr_cpp/src/geometry.cpp delete mode 100644 data_acq_and_processing/gpr_cpp/src/kernel_src.hpp delete mode 100644 data_acq_and_processing/gpr_cpp/src/nvrtc_kernel.cpp delete mode 100644 data_acq_and_processing/gpr_cpp/src/sar3d.cpp delete mode 100644 data_acq_and_processing/gpr_cpp/third_party/pocketfft/pocketfft_hdronly.h create mode 100644 data_acq_and_processing/processing/processors/src/sar3d_gpu.cpp create mode 100644 data_acq_and_processing/processing/processors/src/sar3d_gpu.hpp delete mode 100644 data_acq_and_processing/processing/tools/sar3d_parity_main.cpp delete mode 100644 python_app/gui/runtime/sar3d_views.py delete mode 100644 python_app/scripts/replay_smoke_run.py delete mode 100644 python_app/scripts/sar3d_reference_check.py delete mode 100644 python_app/scripts/sar3d_volume_compare.py create mode 100644 python_app/tests/test_socket_speed_controls.py diff --git a/.gitignore b/.gitignore index 7bdfab8..deb8359 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ __pycache__/ *.py[codz] *$py.class build* -# build* иначе прячет скрипт сборки вендоренного gpr_cpp (его build/ остаётся игнорируемым) -!data_acq_and_processing/gpr_cpp/build_cmake.sh .codex .DS_Store # C extensions diff --git a/Makefile b/Makefile index 679386f..0e83b4a 100644 --- a/Makefile +++ b/Makefile @@ -8,43 +8,28 @@ VISA_LDFLAGS := $(LDFLAGS) -lvisa BUILD_DIR := build BIN_DIR := $(BUILD_DIR)/bin -# --- gpr_cpp: vendored C++/CUDA 3D SAR library (originally from VNA_AlGo). -# Built automatically by the $(GPR_CPP_LIB) rule below via its own CMake -# (build_cmake.sh). CUDA headers and libnvrtc come from the pip nvidia-* -# packages inside this repo's venv (same detection as build_cmake.sh); when -# those are absent (e.g. Jetson, where CUDA comes with JetPack) the system -# toolkit at /usr/local/cuda is used. libcuda is always the system driver. -GPR_CPP_DIR ?= data_acq_and_processing/gpr_cpp -GPR_CPP_LIB ?= $(GPR_CPP_DIR)/build/libgpr_core_cpp.a -# Absolute so the derived -rpath (libnvrtc dir) survives running the binary -# from outside the repo root. -GPR_VENV ?= $(CURDIR)/.venv -GPR_CUDA_INC_DIR ?= $(dir $(firstword $(wildcard $(GPR_VENV)/lib/python*/site-packages/nvidia/*/include/nvrtc.h))) -# cuda.h can live in a DIFFERENT pip package than nvrtc.h (Jetson: cuda_runtime -# vs cuda_nvrtc); the unified cu13 package on dev machines has both in one dir. -GPR_CUDA_RT_INC_DIR ?= $(dir $(firstword $(wildcard $(GPR_VENV)/lib/python*/site-packages/nvidia/*/include/cuda.h))) -GPR_NVRTC_LIB ?= $(firstword $(wildcard $(GPR_VENV)/lib/python*/site-packages/nvidia/*/lib/libnvrtc.so*)) -# System-CUDA fallback. An empty GPR_CUDA_INC_DIR would also corrupt the compile -# command: a bare `-I` swallows the next argument. -ifeq ($(strip $(GPR_CUDA_INC_DIR)),) -GPR_CUDA_INC_DIR := /usr/local/cuda/include +# --- CUDA for the sar3d processor (optional). Its kernels are compiled at run +# time by NVRTC and launched through the CUDA driver API, so no nvcc is needed: +# only nvrtc.h, cuda.h, libnvrtc and the NVIDIA driver's libcuda. Headers and +# libnvrtc come from the pip nvidia-cuda-* packages in .venv (requirements.txt, +# x86_64) or from a system toolkit in /usr/local/cuda (Jetson). Without them +# data_processor still builds; sar3d then reports that CUDA is missing. +CUDA_ROOTS := $(wildcard $(CURDIR)/.venv/lib/python3*/site-packages/nvidia/*) /usr/local/cuda +CUDA_NVRTC_HEADER := $(firstword $(wildcard $(addsuffix /include/nvrtc.h,$(CUDA_ROOTS)))) +CUDA_DRIVER_HEADER := $(firstword $(wildcard $(addsuffix /include/cuda.h,$(CUDA_ROOTS)))) +CUDA_NVRTC_LIB := $(firstword $(wildcard $(addsuffix /lib/libnvrtc.so.*,$(CUDA_ROOTS)) $(addsuffix /lib64/libnvrtc.so.*,$(CUDA_ROOTS)))) +CUDA_DRIVER_LIB := $(firstword $(wildcard /usr/lib/*-linux-gnu/libcuda.so.1 /usr/lib/*-linux-gnu/nvidia/libcuda.so.1 /usr/lib64/libcuda.so.1)) +ifneq ($(and $(CUDA_NVRTC_HEADER),$(CUDA_DRIVER_HEADER),$(CUDA_NVRTC_LIB),$(CUDA_DRIVER_LIB)),) +SAR3D_CXXFLAGS := -DSAR3D_WITH_CUDA $(addprefix -I,$(sort $(dir $(CUDA_NVRTC_HEADER) $(CUDA_DRIVER_HEADER)))) +# The rpath lets data_processor find the pip libnvrtc without LD_LIBRARY_PATH. +SAR3D_LDFLAGS := $(CUDA_NVRTC_LIB) $(CUDA_DRIVER_LIB) -Wl,-rpath,$(dir $(CUDA_NVRTC_LIB)) endif -ifeq ($(strip $(GPR_CUDA_RT_INC_DIR)),) -GPR_CUDA_RT_INC_DIR := /usr/local/cuda/include -endif -ifeq ($(strip $(GPR_NVRTC_LIB)),) -GPR_NVRTC_LIB := $(firstword $(wildcard /usr/local/cuda/lib64/libnvrtc.so*)) -endif -# Deduplicated CUDA include flags (a single dir on dev machines). -GPR_CUDA_INC_FLAGS := $(addprefix -I,$(sort $(GPR_CUDA_INC_DIR) $(GPR_CUDA_RT_INC_DIR))) -GPR_CUDA_LIB_DIR := $(dir $(GPR_NVRTC_LIB)) -# libnvrtc from pip ships only as libnvrtc.so.N (no .so symlink), so link the -# full path; the rpath makes the binary find it without LD_LIBRARY_PATH. -DATA_PROCESSOR_LDFLAGS := $(LDFLAGS) $(GPR_CPP_LIB) $(GPR_NVRTC_LIB) -lcuda -Wl,-rpath,$(GPR_CUDA_LIB_DIR) +# Rewritten only when the CUDA setup changes, so installing (or removing) CUDA +# rebuilds the sar3d GPU code instead of keeping a stale build. +SAR3D_CUDA_STAMP := $(BUILD_DIR)/sar3d_cuda.flags +$(shell mkdir -p $(BUILD_DIR) && echo '$(SAR3D_CXXFLAGS) $(SAR3D_LDFLAGS)' | cmp -s - $(SAR3D_CUDA_STAMP) || echo '$(SAR3D_CXXFLAGS) $(SAR3D_LDFLAGS)' > $(SAR3D_CUDA_STAMP)) INCLUDES := \ - -I$(GPR_CPP_DIR)/include \ - $(GPR_CUDA_INC_FLAGS) \ -Idata_acq_and_processing/common_cpp/ipc/include \ -Idata_acq_and_processing/common_cpp/config/include \ -Idata_acq_and_processing/third_party \ @@ -92,6 +77,7 @@ PROCESSOR_SOURCES := \ data_acq_and_processing/processing/processors/src/gpr_processor.cpp \ data_acq_and_processing/processing/processors/src/passthrough_processor.cpp \ data_acq_and_processing/processing/processors/src/sar3d_processor.cpp \ + data_acq_and_processing/processing/processors/src/sar3d_gpu.cpp \ data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp \ data_acq_and_processing/processing/data_processor/src/data_processor.cpp \ data_acq_and_processing/processing/data_processor/src/main.cpp \ @@ -139,27 +125,12 @@ TARGETS := \ $(BIN_DIR)/data_preprocessor \ $(BIN_DIR)/data_processor -.PHONY: all clean kamil_adc_collector sar3d_parity +.PHONY: all clean kamil_adc_collector all: $(TARGETS) kamil_adc_collector: $(BIN_DIR)/kamil_adc_collector -# On-demand parity check of the sar3d processor against the reference notebook -# volumes (scripts/sar3d_volume_compare.py diffs the dumped volume against a -# *_volume.npz export). Not part of `all`. -SAR3D_PARITY_OBJS := $(addprefix $(BUILD_DIR)/, \ - data_acq_and_processing/processing/tools/sar3d_parity_main.o \ - data_acq_and_processing/processing/processors/src/sar3d_processor.o \ - data_acq_and_processing/common_cpp/config/src/run_config.o \ - data_acq_and_processing/common_cpp/ipc/src/shared_types.o) - -sar3d_parity: $(BIN_DIR)/sar3d_parity - -$(BIN_DIR)/sar3d_parity: $(SAR3D_PARITY_OBJS) $(GPR_CPP_LIB) - @mkdir -p $(BIN_DIR) - $(CXX) $(SAR3D_PARITY_OBJS) -o $@ $(DATA_PROCESSOR_LDFLAGS) - $(BIN_DIR)/sweep_orchestrator: $(SWEEP_ORCH_OBJS) @mkdir -p $(BIN_DIR) $(CXX) $(SWEEP_ORCH_OBJS) -o $@ $(ORCH_LDFLAGS) @@ -168,17 +139,13 @@ $(BIN_DIR)/data_preprocessor: $(PREPROCESSOR_OBJS) @mkdir -p $(BIN_DIR) $(CXX) $(PREPROCESSOR_OBJS) -o $@ $(LDFLAGS) -$(BIN_DIR)/data_processor: $(DATA_PROCESSOR_OBJS) $(GPR_CPP_LIB) +$(BIN_DIR)/data_processor: $(DATA_PROCESSOR_OBJS) $(SAR3D_CUDA_STAMP) @mkdir -p $(BIN_DIR) - $(CXX) $(DATA_PROCESSOR_OBJS) -o $@ $(DATA_PROCESSOR_LDFLAGS) + $(CXX) $(DATA_PROCESSOR_OBJS) -o $@ $(LDFLAGS) $(SAR3D_LDFLAGS) + $(if $(SAR3D_LDFLAGS),,@echo "note: CUDA not found, data_processor built without the sar3d GPU backend") -# Vendored gpr_cpp static lib: delegate to its CMake build (also produces the -# standalone_bp example used by sar3d_parity cross-checks). -GPR_CPP_SRC_FILES := $(wildcard $(GPR_CPP_DIR)/src/*) $(wildcard $(GPR_CPP_DIR)/include/gpr/*) \ - $(wildcard $(GPR_CPP_DIR)/examples/*) $(GPR_CPP_DIR)/CMakeLists.txt $(GPR_CPP_DIR)/build_cmake.sh - -$(GPR_CPP_LIB): $(GPR_CPP_SRC_FILES) - GPR_BUILD_PYTHON=OFF VENV=$(abspath $(GPR_VENV)) bash $(GPR_CPP_DIR)/build_cmake.sh +$(BUILD_DIR)/data_acq_and_processing/processing/processors/src/sar3d_gpu.o: CXXFLAGS += $(SAR3D_CXXFLAGS) +$(BUILD_DIR)/data_acq_and_processing/processing/processors/src/sar3d_gpu.o: $(SAR3D_CUDA_STAMP) # The collector compiles into its own build/kamil/ tree with collector-only # includes (no VISA), so the shared driver/config sources it reuses never collide @@ -201,4 +168,4 @@ $(BUILD_DIR)/%.o: %.cpp -include $(DEPFILES) clean: - rm -rf $(BUILD_DIR) $(GPR_CPP_DIR)/build + rm -rf $(BUILD_DIR) diff --git a/README.md b/README.md index aef7fb1..4e28b3a 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ simply skips to the freshest frame: 2. **Preprocessing** — `data_preprocessor` applies through/OSL calibration, reference subtraction, and optional frequency-domain notch filtering. 3. **Processing** — `data_processor` runs the selected processing mode and emits - results (pass-through traces, B-scan images, or GPR images with detected - objects). Detected objects are additionally broadcast to subscribers over a TCP - locator server. + results (pass-through traces, B-scan images, GPR images with detected objects, + or 3D SAR views). Detected objects are additionally broadcast to subscribers + over a TCP locator server. The **Python application** (`python_app`) starts and monitors the C++ binaries, reads the result rings, renders the live view with pyqtgraph, and exposes the same @@ -55,12 +55,19 @@ controls plus a pixel-identical live plot through an embedded web UI. - **bscan** — depth-versus-sweep B-scan heatmap. - **gpr** — coherent back-projection GPR imaging with object detection. - **legacy_gpr** — ellipse-intersection MIMO localization (point and extended/region modes). +- **sar3d** — 3D SAR of the reference notebook `Multi_antenna_3D_clean_v1.ipynb` + on an NVIDIA GPU: front (X-Y), side (Z-Y), top (X-Z) and 3D views of the imaged + volume. Its settings carry the notebook's parameter names; the scene window is + fixed (X −2…2 m, height −0.5…2 m above the ground), its range extent follows + `MIN_DEPTH`/`MAX_DEPTH`, and the voxels are cubes with edge `SAR_VOXEL_M` + (default 2 cm). The radar motion is corrected from each trace's capture + timestamps, so the sweep timing needs no parameters. ## Repository layout | Path | Contents | | --- | --- | -| `data_acq_and_processing/` | C++ pipeline: `sweep_orchestrator/`, `preprocessing/`, `processing/` (including `locator/`), shared `common_cpp/` (config, IPC rings), the vendored `gpr_cpp/` C++/CUDA 3D SAR library, and vendored `third_party/` (Eigen, nlohmann/json). | +| `data_acq_and_processing/` | C++ pipeline: `sweep_orchestrator/`, `preprocessing/`, `processing/` (including `locator/`), shared `common_cpp/` (config, IPC rings), and vendored `third_party/` (Eigen, nlohmann/json). | | `python_app/` | Python application: `gui/`, `orchestration/` (process supervisor, shared-memory readers, config), `hardware_full/` (device drivers and services), `webui/`, `storage/`, `models/`, `workflows/`, `scripts/` (producers and tools), and `tests/`. | | `run_config_examples/` | Ready-made configuration profiles, one per hardware setup. | | `deploy/` | systemd daemon installer for Raspberry Pi appliances. | @@ -74,13 +81,13 @@ controls plus a pixel-identical live plot through an embedded web UI. specific (GPIO12/13/18/19, `dtoverlay=pwm-2chan`). - A C++20 compiler and `make` — Eigen and nlohmann/json are vendored, so no extra C++ dependencies are needed. - **Python 3.10 or newer**, in a virtual environment. -- For the `sar3d` processor (3D SAR): an NVIDIA GPU with the driver installed - (`libcuda`) and `cmake` (the vendored `gpr_cpp` library at - `data_acq_and_processing/gpr_cpp/` builds via its own CMake, invoked - automatically by `make`). CUDA headers/libnvrtc are taken from the pip - `nvidia-*` packages in this repo's venv (installed by `requirements.txt` on - x86_64), otherwise from the system toolkit at `/usr/local/cuda` - (Jetson/JetPack, apt `cuda-toolkit`). +- For the `sar3d` processor: an NVIDIA GPU with its driver (`libcuda`). The + kernels are compiled at run time by NVRTC, so there is no nvcc step; `make` + needs only `nvrtc.h`, `cuda.h` and `libnvrtc`, taken from the pip + `nvidia-cuda-*` packages in `.venv` (`requirements.txt`, x86_64; CUDA 13 covers + Turing and newer GPUs) or from a system toolkit in `/usr/local/cuda` (Jetson). + Without them everything else still builds, and sar3d reports that CUDA is + missing — a Raspberry Pi needs none of it. ## Build and run @@ -93,20 +100,9 @@ python3 -m venv .venv make # build the C++ binaries into build/bin/ ``` -`make` also builds the vendored 3D SAR library -(`data_acq_and_processing/gpr_cpp/`, CMake + NVRTC — needs `cmake`; CUDA -headers come from the pip `nvidia-*` packages in `.venv`, or `/usr/local/cuda` -as a fallback). - -To verify the sar3d processor against the gpr_cpp reference on the recorded -dataset, build and run the on-demand parity tool: - -```bash -make sar3d_parity -./build/bin/sar3d_parity -# compare the printed peak/argmax with -# ./data_acq_and_processing/gpr_cpp/build/standalone_bp on the same arguments -``` +`make` builds the sar3d GPU code when it finds CUDA (see Requirements) and +prints a note when it does not. After installing the CUDA requirements into an +existing venv, run `make` again: it notices the change and rebuilds that part. Run the desktop GUI: @@ -134,17 +130,12 @@ for the CUDA-backed `sar3d` processor: # radar.replay.dataset_dir in the profile at the local copy — the path in # run_config_examples/*.json is machine-specific. -# 2. normal launch; the vendored gpr_cpp builds against the JetPack system CUDA -# (requirements.txt installs no pip nvidia-* packages on aarch64, and -# build_cmake.sh falls back to /usr/local/cuda automatically; override with -# CUDA_INC=... CUDA_LIB=... if the toolkit lives elsewhere): +# 2. normal launch; make takes the JetPack CUDA from /usr/local/cuda +# (requirements.txt installs no pip nvidia-* packages on aarch64) and the +# driver's /usr/lib/aarch64-linux-gnu/nvidia/libcuda.so.1: ./start.sh --profile run_config_examples/run_config_replay_vna.example.json ``` -If linking fails with `cannot find -lcuda`, the driver stub lives at -`/usr/local/cuda/lib64/stubs/libcuda.so`; on JetPack the real driver is -`/usr/lib/aarch64-linux-gnu/nvidia/libcuda.so.1` (present after boot). - ## Running without hardware (emulation) Two ways to run the full pipeline with no radar attached: @@ -175,14 +166,13 @@ A ready profile for the VNA_AlGo tank005 dataset is provided: --config run_config_examples/run_config_replay_vna.example.json # interactive GUI (mode sar3d is pre-selected by the profile's gui section): ./start.sh --profile run_config_examples/run_config_replay_vna.example.json -# or a fully headless end-to-end check: -.venv/bin/python -m python_app.scripts.replay_smoke_run --duration 30 ``` -Note: the sar3d processor allocates GPU memory; if the desktop (e.g. a browser) -has filled the GPU, sar3d logs `out of memory` and publishes empty results until -memory frees. Reduce `sar3d_grid_nx/ny/nz` in the processing panel to shrink -the footprint. +The sar3d processor keeps about 75 MB of buffers on the GPU at the default +2 cm voxel and depth 2…8 m (plus the CUDA context). The volume grows as +1/`SAR_VOXEL_M`³: 1 cm takes about 600 MB and ran about 6 times slower in +testing. If the GPU has no room, it logs the CUDA error once and publishes empty +results until memory frees. The web interface is served in-process at `http://:8080`. It mirrors the live plot and offers Start / Stop / Capture controls and the active processor settings. diff --git a/data_acq_and_processing/gpr_cpp/CMakeLists.txt b/data_acq_and_processing/gpr_cpp/CMakeLists.txt deleted file mode 100644 index 59afccb..0000000 --- a/data_acq_and_processing/gpr_cpp/CMakeLists.txt +++ /dev/null @@ -1,78 +0,0 @@ -# 3.22 = apt cmake на Ubuntu 22.04 (Jetson/JetPack); фич новее здесь нет. -cmake_minimum_required(VERSION 3.22) -project(gpr_cpp LANGUAGES CXX) # CXX-only: CUDA компилируется в рантайме через NVRTC - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() -set(CMAKE_CXX_FLAGS_RELEASE "-O3") - -# bindings/pymodule.cpp не вендорен в radar_system — включение потребует вернуть его из VNA_AlGo -option(GPR_BUILD_PYTHON "Build pybind11 module" OFF) -option(GPR_BUILD_TESTS "Build C++ parity tests" OFF) -option(GPR_BUILD_EXAMPLES "Build standalone C++ example" ON) - -# Python окружение (venv) — источник pybind11 и pip CUDA-библиотек (nvrtc, cudart). -if(NOT DEFINED PYTHON_EXECUTABLE) - set(PYTHON_EXECUTABLE python3) -endif() - -# CUDA include/lib из pip-пакетов nvidia-*; задаются -DGPR_CUDA_INC / -DGPR_CUDA_LIB -# (детект делает build_cmake.sh). Заголовки: nvrtc.h, cuda.h (Driver API). -# GPR_CUDA_INC может быть СПИСКОМ каталогов (через ';'): в раздельных pip-пакетах -# (Jetson: cuda_nvrtc + cuda_runtime) nvrtc.h и cuda.h лежат в разных include/. -if(NOT DEFINED GPR_CUDA_INC OR NOT DEFINED GPR_CUDA_LIB) - message(FATAL_ERROR "Задайте -DGPR_CUDA_INC= и -DGPR_CUDA_LIB=") -endif() - -# libcuda (драйвер) — системный -find_library(CUDA_DRIVER_LIB cuda PATHS /usr/lib/x86_64-linux-gnu /usr/lib/aarch64-linux-gnu - /usr/lib REQUIRED) -# libnvrtc из pip -find_library(NVRTC_LIB NAMES nvrtc libnvrtc.so.13 libnvrtc.so.12 nvrtc.so - PATHS ${GPR_CUDA_LIB} REQUIRED NO_DEFAULT_PATH) - -# PocketFFT (header-only, vendored) -add_library(pocketfft INTERFACE) -target_include_directories(pocketfft INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/third_party/pocketfft) - -# --- ядро библиотеки (usable без Python) --- -add_library(gpr_core_cpp STATIC - src/geometry.cpp - src/ascan.cpp - src/device.cpp - src/nvrtc_kernel.cpp - src/sar3d.cpp) -target_include_directories(gpr_core_cpp PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include - ${GPR_CUDA_INC}) -target_link_libraries(gpr_core_cpp PUBLIC pocketfft ${NVRTC_LIB} ${CUDA_DRIVER_LIB}) -# rpath на каталог pip libnvrtc — чтобы бинарник/модуль грузились без LD_LIBRARY_PATH -set_target_properties(gpr_core_cpp PROPERTIES POSITION_INDEPENDENT_CODE ON - BUILD_RPATH "${GPR_CUDA_LIB}" INSTALL_RPATH "${GPR_CUDA_LIB}") - -if(GPR_BUILD_PYTHON) - execute_process(COMMAND ${PYTHON_EXECUTABLE} -m pybind11 --cmakedir - OUTPUT_VARIABLE PB11_DIR OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE PB11_RC) - if(PB11_RC EQUAL 0) - find_package(pybind11 CONFIG REQUIRED HINTS ${PB11_DIR}) - else() - find_package(pybind11 CONFIG REQUIRED) - endif() - pybind11_add_module(_gpr_cpp bindings/pymodule.cpp) - target_link_libraries(_gpr_cpp PRIVATE gpr_core_cpp) - set_target_properties(_gpr_cpp PROPERTIES BUILD_RPATH "${GPR_CUDA_LIB}" INSTALL_RPATH "${GPR_CUDA_LIB}") -endif() - -if(GPR_BUILD_TESTS) - add_executable(gpr_test_parity tests/cpp/test_parity.cpp) - target_link_libraries(gpr_test_parity PRIVATE gpr_core_cpp) -endif() - -if(GPR_BUILD_EXAMPLES) - add_executable(standalone_bp examples/standalone_bp.cpp) - target_link_libraries(standalone_bp PRIVATE gpr_core_cpp) - set_target_properties(standalone_bp PROPERTIES BUILD_RPATH "${GPR_CUDA_LIB}" INSTALL_RPATH "${GPR_CUDA_LIB}") -endif() diff --git a/data_acq_and_processing/gpr_cpp/README.md b/data_acq_and_processing/gpr_cpp/README.md deleted file mode 100644 index 637c81f..0000000 --- a/data_acq_and_processing/gpr_cpp/README.md +++ /dev/null @@ -1,85 +0,0 @@ -# gpr_cpp — C++/CUDA счёт-ядро MIMO GPR 3D SAR - -> Инфраструктура (сборка, NVRTC, PocketFFT, npy) вендорена из репозитория -> VNA_AlGo (`gpr_cpp/`, коммит `544dc59`) без python-биндингов -> (`GPR_BUILD_PYTHON=OFF`). АЛГОРИТМ с сентября 2026 переписан: это порт -> эталонного ноутбука `Multi_antenna_3D_clean_v1.ipynb` (ячейки 0 и 4), а не -> старого `gpr_core`/Horns_3D. Паритет с ноутбуком проверяется инструментом -> `sar3d_parity` + `python_app/scripts/sar3d_volume_compare.py` по -> референсным `*_volume.npz`. Собирается из корневого Makefile radar_system -> (цель `$(GPR_CPP_LIB)` вызывает `build_cmake.sh`). - -## Что внутри - -**В C++ (`libgpr_core_cpp`):** сегментированный тайминг кадра по per-trace -меткам захвата (`compute_pair_timestamps`), препроцессинг A-сканов (bg-subtract, -полная фазовая коррекция движения intra+pair до IFFT, k0-вставка + Ханн + IFFT -через PocketFFT, нормировка пар), полнообластная ground-сетка -(`build_full_region_grid`), 3D SAR апертурный объём (одно CUDA-ядро на всю -апертуру; геометрия в double, A-сканы float32) и финализация -(`finish_volume`: |acc/cnt| → маска глубины → нормировка без Y-краёв → гаусс -по (Z,X) → нормировка; возвращает объём [0,1] + level). - -## Ключевое решение: CUDA без nvcc (NVRTC + Driver API) - -На целевых машинах (x86+T600, Jetson) нет полного CUDA toolkit — только runtime + -**NVRTC** (как у cupy). Поэтому ядро **не компилируется nvcc'ом**: его исходник -встроен строкой ([src/kernel_src.hpp](src/kernel_src.hpp)), компилируется в рантайме -через NVRTC в **cubin под точную арх устройства** (запрос compute capability → -`sm_XY`, без JIT PTX), грузится и запускается через **CUDA Driver API**. Хост-часть -собирается обычным g++. Плюс: без toolkit и без привязки к SM — один код для -x86 (sm_75) и Jetson (sm_87). - -## Сборка - -```bash -bash build_cmake.sh # авто-детект pip CUDA-путей; собирает всё -# артефакты в build/: libgpr_core_cpp.a, _gpr_cpp*.so, standalone_bp -``` -Требуется: g++ ≥13, cmake ≥3.24, pybind11 (`pip install pybind11`), pip-пакеты -`nvidia-*` (nvrtc+cudart, идут с cupy), системный `libcuda`. rpath на каталог -libnvrtc прописан — бинарники/модуль грузятся без `LD_LIBRARY_PATH`. - -## Публичный C++ API ([include/gpr/](include/gpr/)) - -```cpp -#include "gpr/config.hpp" // GeometryConfig/BPConfig/MotionConfig/SARConfig (дефолты = ноутбук) -#include "gpr/ascan.hpp" // compute_pair_timestamps, preprocess_snapshot(...) -> AScanSet -#include "gpr/sar3d.hpp" // SarEngine::aperture_volume -> VolumeRaw; finish_volume -> FinishedVolume -#include "gpr/geometry.hpp" // frame_offsets, frame_weights, build_full_region_grid - -gpr::SarEngine engine(geom, bp, mot, sar); // компилит ядро под устройство -auto raw = engine.aperture_volume(frames, grid); // один запуск ядра на апертуру -auto fin = gpr::finish_volume(raw, grid, mot.pitch_deg, bp, sar); // объём [0,1] + level -``` -Пример без Python: [examples/standalone_bp.cpp](examples/standalone_bp.cpp) — грузит -S21 из `.npy`, строит объём, печатает максимум. -```bash -./build/standalone_bp 0038_id39_ns11361438137486 -``` - -## Python-модуль - -```python -import _gpr_cpp # из gpr_cpp/build -eng = _gpr_cpp.SarEngine(_gpr_cpp.GeometryConfig(), _gpr_cpp.BPConfig(), - _gpr_cpp.MotionConfig(), _gpr_cpp.SARConfig()) -a = _gpr_cpp.preprocess_snapshot(s21, freq, background, mot, geom, bp, True) # -> AScanSet -acc, cnt = eng.aperture_volume([(a, dx, dy, dz, w), ...], gx, gy, gz) # numpy -``` - -## Паритет с ноутбуком (проверено на x86 T600, датасет 20260922_mine_fast_true) - -Кадры 0034/0035/0036 против референсных `*_volume.npz` ноутбука -(`make sar3d_parity` + `python_app/scripts/sar3d_volume_compare.py`): -max|Δ| ≤ 1.6e-6 вне ножевой границы маски глубины, argmax и level совпадают -(level rel ≤ 7e-7). Python-эталон той же цепочки: -`python_app/scripts/sar3d_reference_check.py`. - -## Статус - -- [x] x86 (T600, sm_75, CUDA 13): собрано, паритет, standalone работает. -- [ ] Jetson (Orin NX, sm_87, CUDA 12.6): порт тривиален (NVRTC сам берёт sm_87); - собрать `build_cmake.sh` на Jetson с его venv. -- Опц.: ветка `impl='cpp'` в `gpr_core/sar3d.py` (адаптер `gpr_cpp_backend.py`), - кэш device-A-сканов (сейчас каждый вызов грузит стек на GPU). diff --git a/data_acq_and_processing/gpr_cpp/build_cmake.sh b/data_acq_and_processing/gpr_cpp/build_cmake.sh deleted file mode 100644 index 1af3377..0000000 --- a/data_acq_and_processing/gpr_cpp/build_cmake.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env bash -# CMake-сборка libgpr_core_cpp + standalone (без nvcc; NVRTC/driver). -# -# Источник CUDA (nvrtc.h / libnvrtc), по убыванию приоритета: -# 1. env CUDA_INC / CUDA_LIB — заданы явно; -# 2. pip-пакеты nvidia-* в venv ($VENV, по умолчанию ../../.venv — venv -# radar_system) — дев-машина; -# 3. системный toolkit /usr/local/cuda — Jetson/JetPack, apt cuda-toolkit. -# -# GPR_BUILD_PYTHON=ON вернул бы pybind-модуль, но bindings/ не вендорен в -# radar_system (см. README) — по умолчанию OFF. -set -eu -HERE="$(cd "$(dirname "$0")" && pwd)" -VENV="${VENV:-$HERE/../../.venv}" -PY="$VENV/bin/python" -GPR_BUILD_PYTHON="${GPR_BUILD_PYTHON:-OFF}" - -CUDA_INC="${CUDA_INC:-}" -CUDA_LIB="${CUDA_LIB:-}" - -if [ -z "$CUDA_INC" ] || [ -z "$CUDA_LIB" ]; then - if [ -x "$PY" ]; then - # Печатается только когда найдены nvrtc.h, cuda.h И библиотека — частичная - # детекция не может подсунуть мусорный путь. nvrtc.h и cuda.h могут лежать - # в РАЗНЫХ каталогах (раздельные pip-пакеты cuda_nvrtc/cuda_runtime на - # Jetson) — тогда CUDA_INC становится ';'-списком для CMake. - PIP_CUDA=$("$PY" - <<'P' 2>/dev/null || true -import glob, os, site -sp = site.getsitepackages()[0] -nvrtc_hdr = glob.glob(os.path.join(sp, 'nvidia', '**', 'include', 'nvrtc.h'), recursive=True) -cuda_hdr = glob.glob(os.path.join(sp, 'nvidia', '**', 'include', 'cuda.h'), recursive=True) -lib = sorted(glob.glob(os.path.join(sp, 'nvidia', '**', 'lib', 'libnvrtc.so*'), recursive=True)) -if nvrtc_hdr and cuda_hdr and lib: - inc_dirs = [] - for hdr in (nvrtc_hdr[0], cuda_hdr[0]): - d = os.path.dirname(hdr) - if d not in inc_dirs: - inc_dirs.append(d) - print(';'.join(inc_dirs)) - print(os.path.dirname(lib[0])) -P -) - if [ -n "$PIP_CUDA" ]; then - CUDA_INC=$(printf '%s\n' "$PIP_CUDA" | sed -n 1p) - CUDA_LIB=$(printf '%s\n' "$PIP_CUDA" | sed -n 2p) - fi - fi -fi - -if [ -z "$CUDA_INC" ] || [ -z "$CUDA_LIB" ]; then - if [ -e /usr/local/cuda/include/nvrtc.h ]; then - CUDA_INC=/usr/local/cuda/include - CUDA_LIB=/usr/local/cuda/lib64 - echo "[build_cmake] pip nvidia-* не найдены; использую системный CUDA: $CUDA_INC" - else - echo "[build_cmake] CUDA не найден: нет ни pip nvidia-* в $VENV, ни /usr/local/cuda." >&2 - echo "[build_cmake] Задайте CUDA_INC= и CUDA_LIB=." >&2 - exit 1 - fi -fi - -PYTHON_ARG=() -if [ "$GPR_BUILD_PYTHON" = "ON" ] && [ -x "$PY" ]; then - PYTHON_ARG=(-DPYTHON_EXECUTABLE="$PY") -fi - -# cmake: сначала из venv (pip install cmake — свежий без sudo), потом системный. -CMAKE="$VENV/bin/cmake" -if [ ! -x "$CMAKE" ]; then - CMAKE=$(command -v cmake || true) -fi -if [ -z "$CMAKE" ]; then - echo "[build_cmake] cmake не найден. Поставьте его в venv: \"$VENV/bin/pip\" install cmake" >&2 - echo "[build_cmake] (или системно: sudo apt install cmake)" >&2 - exit 1 -fi - -"$CMAKE" -S "$HERE" -B "$HERE/build" \ - ${PYTHON_ARG[@]+"${PYTHON_ARG[@]}"} \ - -DGPR_BUILD_PYTHON="$GPR_BUILD_PYTHON" \ - -DGPR_CUDA_INC="$CUDA_INC" \ - -DGPR_CUDA_LIB="$CUDA_LIB" \ - -DCMAKE_BUILD_TYPE=Release "$@" 2>&1 | tail -8 -"$CMAKE" --build "$HERE/build" -j4 2>&1 | tail -15 -echo "--- артефакты ---" -ls -la "$HERE/build/libgpr_core_cpp.a" "$HERE/build/standalone_bp" "$HERE"/build/_gpr_cpp*.so 2>/dev/null | awk '{print $5,$9}' -echo "CUDA_LIB=$CUDA_LIB (для LD_LIBRARY_PATH при запуске)" diff --git a/data_acq_and_processing/gpr_cpp/examples/standalone_bp.cpp b/data_acq_and_processing/gpr_cpp/examples/standalone_bp.cpp deleted file mode 100644 index e89ddf0..0000000 --- a/data_acq_and_processing/gpr_cpp/examples/standalone_bp.cpp +++ /dev/null @@ -1,159 +0,0 @@ -// Автономный пример: C++ без Python. Грузит S21 снапшота из .npy, -// строит A-сканы и 3D SAR-объём апертуры, печатает максимум |acc/cnt|. -// -// Использование: -// standalone_bp -// где preprocessed_dir содержит папки снапшотов с i{rx}_o{tx}_s21.npy и _freq.npy. -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "gpr/ascan.hpp" -#include "gpr/config.hpp" -#include "gpr/geometry.hpp" -#include "gpr/sar3d.hpp" - -using namespace gpr; - -// --- минимальный загрузчик numpy .npy (v1.0, 1-D complex128 или float64) --- -struct NpyArray { std::vector f64; std::vector c128v; bool is_complex = false; }; - -static NpyArray load_npy(const std::string& path) { - std::ifstream in(path, std::ios::binary); - if (!in) throw std::runtime_error("не открыть " + path); - char magic[6]; in.read(magic, 6); - uint8_t ver[2]; in.read((char*)ver, 2); - uint16_t hlen; in.read((char*)&hlen, 2); - std::string hdr(hlen, '\0'); in.read(&hdr[0], hlen); - auto sp = hdr.find("'shape':"); - auto lp = hdr.find('(', sp), rp = hdr.find(')', sp); - long n = std::stol(hdr.substr(lp + 1, rp - lp - 1)); - NpyArray a; - auto has = [&](const char* s) { return hdr.find(s) != std::string::npos; }; - if (has("c8")) { // complex64 → c128 - a.is_complex = true; a.c128v.resize(n); - std::vector> tmp(n); - in.read((char*)tmp.data(), n * sizeof(std::complex)); - for (long i = 0; i < n; ++i) a.c128v[i] = c128(tmp[i].real(), tmp[i].imag()); - } else if (has("c16")) { // complex128 - a.is_complex = true; a.c128v.resize(n); - in.read((char*)a.c128v.data(), n * sizeof(c128)); - } else if (has("f4")) { // float32 → double - a.f64.resize(n); - std::vector tmp(n); in.read((char*)tmp.data(), n * sizeof(float)); - for (long i = 0; i < n; ++i) a.f64[i] = tmp[i]; - } else { // float64 - a.f64.resize(n); in.read((char*)a.f64.data(), n * sizeof(double)); - } - return a; -} - -int main(int argc, char** argv) { - if (argc < 3) { printf("usage: %s \n", argv[0]); return 1; } - std::string root = argv[1], center = argv[2]; - - GeometryConfig geom; geom.finalize(); - BPConfig bp; MotionConfig mot; SARConfig sar; - auto pairs = geom.pair_keys(); - auto tx = geom.output_idx, rx = geom.input_idx; - std::sort(tx.begin(), tx.end()); std::sort(rx.begin(), rx.end()); - - // список снапшотов, выбор апертуры вокруг center - std::vector snaps; - if (DIR* d = opendir(root.c_str())) { - for (dirent* e; (e = readdir(d));) { - std::string n = e->d_name; - if (!n.empty() && isdigit(n[0])) snaps.push_back(n); - } - closedir(d); - } - std::sort(snaps.begin(), snaps.end()); - int ci = -1; for (int i = 0; i < (int)snaps.size(); ++i) if (snaps[i] == center) ci = i; - if (ci < 0) { printf("центр %s не найден\n", center.c_str()); return 1; } - int lo = std::max(0, ci - sar.frame_radius), hi = std::min((int)snaps.size(), ci + sar.frame_radius + 1); - std::vector aperture(snaps.begin() + lo, snaps.begin() + hi); - printf("апертура: %d кадров вокруг %s\n", (int)aperture.size(), center.c_str()); - - auto load_snapshot = [&](const std::string& snap, - std::map>& s21, - std::map>& freq) { - for (int p = 0; p < (int)pairs.size(); ++p) { - int i_tx = pairs[p].first, i_rx = pairs[p].second; - int phys_tx = tx[i_tx], phys_rx = rx[i_rx]; - char fn[512]; - snprintf(fn, sizeof(fn), "%s/%s/i%d_o%d_s21.npy", root.c_str(), snap.c_str(), phys_rx, phys_tx); - s21[{i_tx, i_rx}] = load_npy(fn).c128v; - snprintf(fn, sizeof(fn), "%s/%s/i%d_o%d_freq.npy", root.c_str(), snap.c_str(), phys_rx, phys_tx); - freq[{i_tx, i_rx}] = load_npy(fn).f64; - } - }; - - // Демо без чтения meta.json: равномерные сегменты по интервалу между - // соседними снапшотами (реальные per-trace окна читает sar3d_parity). - double acq_s = 0.0; - { - std::vector stamps; - for (auto& s : snaps) { - auto pos = s.rfind("_ns"); - if (pos != std::string::npos) stamps.push_back(std::stod(s.substr(pos + 3)) * 1e-9); - } - std::sort(stamps.begin(), stamps.end()); - if (stamps.size() >= 2) acq_s = stamps[1] - stamps[0]; - } - auto windows = uniform_tx_windows(geom.output_idx, acq_s, mot.tx_per_segment); - auto ts = compute_pair_timestamps(windows, acq_s, geom.output_idx, geom, mot); - SarEngine engine(geom, bp, mot, sar); - - // держим AScanSet живыми - std::vector> keep; - std::vector frames; - std::vector frame_times; - for (auto& snap : aperture) { - // время из суффикса _nsNNNN - auto pos = snap.rfind("_ns"); - double t = (pos != std::string::npos) ? std::stod(snap.substr(pos + 3)) * 1e-9 : 0.0; - frame_times.push_back(t); - } - double t_ref = frame_times[ci - lo]; - auto offs = frame_offsets(frame_times, t_ref, mot); - auto wts = frame_weights(offs, sar.edge_frame_weight); - - for (int f = 0; f < (int)aperture.size(); ++f) { - std::map> s21; - std::map> freq; - load_snapshot(aperture[f], s21, freq); - auto a = std::make_shared( - preprocess_snapshot(s21, freq, nullptr, mot, geom, bp, ts, true)); - keep.push_back(a); - frames.push_back(ApertureFrame{a.get(), offs[f], wts[f]}); - } - - // ноутбучные границы сцены (размах антенн ±2 м, полоса глубин) вместо - // фиксированного окна по умолчанию - notebook_grid_bounds(geom, bp, sar, mot.pitch_deg, 2.0, - sar.x_min_m, sar.x_max_m, sar.z_min_m, sar.z_max_m); - Grid grid = build_full_region_grid(sar); - printf("сетка %dx%dx%d, счёт объёма...\n", grid.NX(), grid.NY(), grid.NZ()); - - VolumeRaw vol = engine.aperture_volume(frames, grid); - FinishedVolume fin = finish_volume(vol, grid, mot.pitch_deg, bp, sar); - - double vmax = 0; size_t amax = 0; - for (size_t i = 0; i < fin.volume.size(); ++i) - if (fin.volume[i] > vmax) { vmax = fin.volume[i]; amax = i; } - int NX = vol.NX, NZ = vol.NZ; - int ix = amax % NX, iz = (amax / NX) % NZ, iy = amax / ((size_t)NX * NZ); - printf("МАКСИМУМ объёма: %.4f (level %.4e) в вокселе (ix=%d,iy=%d,iz=%d)\n", - vmax, fin.level, ix, iy, iz); - printf(" X=%.1f см, Y=%.1f см, Z=%.1f см\n", - grid.x[ix] * 100, grid.y[iy] * 100, grid.z[iz] * 100); - printf("OK: автономный C++ путь отработал без Python.\n"); - return 0; -} diff --git a/data_acq_and_processing/gpr_cpp/include/gpr/ascan.hpp b/data_acq_and_processing/gpr_cpp/include/gpr/ascan.hpp deleted file mode 100644 index 293ea7a..0000000 --- a/data_acq_and_processing/gpr_cpp/include/gpr/ascan.hpp +++ /dev/null @@ -1,81 +0,0 @@ -// Препроцессинг S21 → комплексные A-сканы (host, float64). -// Порт ячейки 0 Multi_antenna_3D_clean_v1.ipynb (segmented_interleaved тайминг, -// frequency-domain коррекция движения, k0-IFFT, нормировка пар). -#pragma once -#include -#include -#include -#include - -#include "gpr/config.hpp" -#include "gpr/types.hpp" - -namespace gpr { - -using PairKey = std::pair; - -// Тайминг пары в модели segmented_interleaved: пара живёт в сегменте своего Tx -// (окно [segment_start, segment_start+segment_time] от начала акквизиции), -// внутри сегмента передатчики чередуются по частоте (slot из n_slots). -struct PairTiming { - int tx_interleave_slot = 0; - int tx_interleave_slots = 1; - double segment_start_s = 0.0; - double segment_time_s = 0.0; - double t_center_s = 0.0; // центр окна сегмента - // Грубый межсегментный сдвиг: dtau центра сегмента относительно центра - // акквизиции кадра (2*dz/v, dz = dir*speed*dt*cos(look)). - double dtau_pair_s = 0.0; -}; -using PairTimestamps = std::map; - -// Окна захвата по Tx (output_pos → [start,end] сек от начала акквизиции). -using TxWindows = std::map>; - -// Fallback без per-trace меток: сегменты по tx_per_segment штук делят кадр -// [0, acq_s] поровну (медианная модель ноутбука compute_pair_timestamps). -TxWindows uniform_tx_windows(const std::vector& tx_order, double acq_s, int tx_per_segment); - -// Порт compute_pair_timestamps_for_snapshot: Tx с одинаковым окном — один -// сегмент (сегменты по возрастанию окна), слот — порядок в tx_order (полный -// аппаратный порядок из run.combos; выбранные каналы его не меняют). -PairTimestamps compute_pair_timestamps(const TxWindows& tx_windows_s, double acq_s, - const std::vector& tx_order, - const GeometryConfig&, const MotionConfig&); - -// A-сканы одного снапшота, общая ось времени у всех пар (assert). -struct AScanSet { - int n_pairs = 0; - int n_fft = 0; - double t0 = 0.0, dt = 0.0; // общая ось: t_k = t0 + k*dt - std::vector pairs; // порядок как в geom.pair_keys() - std::vector H; // [n_pairs * n_fft] float64 - const c128* pair_ptr(int p) const { return H.data() + (size_t)p * n_fft; } -}; - -// Порт sar_pair_ascans: bg-subtract → phase correction → k0-IFFT → норма. -// s21/freq/background — по ключам (i_tx,i_rx); background может быть nullptr. -AScanSet preprocess_snapshot( - const std::map>& s21, - const std::map>& freq, - const std::map>* background, - const MotionConfig&, const GeometryConfig&, const BPConfig&, - const PairTimestamps&, bool normalize = true); - -// Отдельные шаги (для тестов/переиспользования): -// Полная фазовая модель движения до IFFT: dtau_intra(f) + dtau_pair одной -// фазой exp(sign*j*2*pi*f*dtau_total) (порт apply_intra_sweep_phase_correction). -std::vector apply_intra_sweep_phase_correction( - const std::vector& s21, const std::vector& freq_full, - const PairTiming&, const MotionConfig&, double wave_speed, - double f_start, double f_stop); - -// Возвращает t (ось), h (комплексный A-скан). n_fft через out-параметр. -void compute_ascan_bp(const std::vector& s21, const std::vector& freq, - double f_start, double f_stop, bool window, int oversample, - std::vector& t_out, std::vector& h_out); - -double pair_norm_scale(const std::vector& h, const std::vector& t, - double wave_speed, const BPConfig&); - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/include/gpr/config.hpp b/data_acq_and_processing/gpr_cpp/include/gpr/config.hpp deleted file mode 100644 index a2cf966..0000000 --- a/data_acq_and_processing/gpr_cpp/include/gpr/config.hpp +++ /dev/null @@ -1,114 +0,0 @@ -// Конфигурация счёт-ядра. Дефолты 1:1 повторяют Multi_antenna_3D_clean_v1.ipynb -// (ячейки 0 и 4) — эталонный ноутбук 3D SAR. -#pragma once -#include -#include -#include -#include -#include -#include - -namespace gpr { - -struct GeometryConfig { - std::vector input_idx{0, 1, 2, 3}; - std::vector output_idx{0, 1, 2, 3}; - // (x,y,z) метры; порядок соответствует input_idx/output_idx - std::vector> tx_positions{ - {-0.88, 0.0, 0.0}, {0.89, 0.0, 0.0}, {-0.33, 0.445, 0.0}, {0.33, 0.445, 0.0}}; - std::vector> rx_positions{ - {0.23, 0.0, 0.0}, {0.54, 0.15, 0.0}, {-0.55, 0.3, 0.0}, {-0.23, 0.0, 0.0}}; - double eps_r = 1.0; - - // --- производные (заполняются finalize) --- - std::vector x_tx, y_tx, z_tx; // отсортированы по phys-индексу - std::vector x_rx, y_rx, z_rx; - int n_tx = 0, n_rx = 0; - double v = 0.0; - - void finalize() { - auto sort_xyz = [](std::vector idx, const std::vector>& pos, - std::vector& X, std::vector& Y, std::vector& Z) { - std::vector order(idx.size()); - for (size_t i = 0; i < idx.size(); ++i) order[i] = (int)i; - std::sort(order.begin(), order.end(), [&](int a, int b) { return idx[a] < idx[b]; }); - X.clear(); Y.clear(); Z.clear(); - for (int o : order) { X.push_back(pos[o][0]); Y.push_back(pos[o][1]); Z.push_back(pos[o][2]); } - }; - sort_xyz(output_idx, tx_positions, x_tx, y_tx, z_tx); - sort_xyz(input_idx, rx_positions, x_rx, y_rx, z_rx); - n_tx = (int)output_idx.size(); - n_rx = (int)input_idx.size(); - v = 3e8 / std::sqrt(eps_r); - } - - std::vector> pair_keys() const { - std::vector> p; - for (int i = 0; i < n_tx; ++i) - for (int j = 0; j < n_rx; ++j) p.emplace_back(i, j); - return p; - } -}; - -struct BPConfig { - double f_start = 2.0e9; - double f_stop = 6.0e9; - double min_depth = 2.5; - double max_depth = 8.0; - int oversample = 8; - bool window = true; // окно Ханна на полосе - bool pair_normalize = true; - double pair_norm_percentile = 50.0; - double pair_norm_depth_min = 2.5; - double pair_norm_depth_max = 8.0; - double pair_norm_eps = 1e-15; - // Компенсация геометрического расхождения 1/(Rtx*Rrx), нормированная на - // точку под виртуальным центром пары на глубине comp_ref_depth НА ПЛОСКОСТИ - // ЗЕМЛИ (высота (-ground_depth + d*sin p)/cos p). - double comp_range_power = 0.3; - double comp_weight_max = 5.0; - double comp_ref_depth = 5.0; -}; - -struct MotionConfig { - double speed_m_s = 1.45; - double look_angle_deg = 17.5; - // Наклон рамы для ground-координат и смещений кадров; в ноутбуке - // BP_GROUND_PITCH_DEG=None означает «равен look_angle». - double pitch_deg = 17.5; - double direction_sign = +1.0; - // Внутрисвиповая (по частоте) фазовая коррекция движения. - bool apply_freq_phase_correction = true; - // Грубая межсегментная коррекция (dtau сегмента относительно центра кадра), - // вносится той же фазовой моделью до IFFT (режим 'frequency_domain'). - bool apply_pair_motion_correction = true; - double motion_phase_sign = -1.0; - // Fallback-разбиение кадра на сегменты, когда per-trace меток захвата нет. - int tx_per_segment = 2; -}; - -struct SARConfig { - int frame_radius = 0; // 0 = один снимок (физическая вертикальная база) - double edge_frame_weight = 0.8; // вес крайнего кадра апертуры - bool coord_mode_ground = true; // 'ground' vs 'radar_local' - bool use_compensation = true; - // Высота НИЖНЕГО ряда антенн над землёй; Y=0 сцены — поверхность земли. - double ground_depth_m = 1.25; - // Явные границы сцены [м]: X — поперёк, Y — высота над землёй, Z — вдаль. - // Воксели с радарной глубиной вне [min_depth, max_depth] всё равно - // обнуляются маской, так что Z-окно шире полосы глубин безопасно. - double x_min_m = -2.0; - double x_max_m = 2.0; - double y_min_m = -0.5; - double y_max_m = 2.0; - double z_min_m = 0.0; - double z_max_m = 8.0; - std::array voxel_m{0.025, 0.02, 0.02}; // шаг (X, Y, Z) - // Финализация объёма: нормировка без верхних/нижних norm_edge_margin_m по Y - // (там ложный гребень постоянной дальности), сглаживание по (Z,X) в каждом - // Y-плане. - double norm_edge_margin_m = 0.3; - double smooth_sigma = 1.5; -}; - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/include/gpr/device.hpp b/data_acq_and_processing/gpr_cpp/include/gpr/device.hpp deleted file mode 100644 index f3ce243..0000000 --- a/data_acq_and_processing/gpr_cpp/include/gpr/device.hpp +++ /dev/null @@ -1,52 +0,0 @@ -// CUDA Driver API RAII: контекст, поток, буфер устройства. -#pragma once -#include - -#include -#include -#include - -namespace gpr { - -void cu_check(CUresult r, const char* what); - -// Инициализация драйвера + primary context на device 0. Возвращает (major,minor) CC. -struct CudaContext { - CUdevice dev = 0; - CUcontext ctx = nullptr; - int cc_major = 0, cc_minor = 0; - CudaContext(); - ~CudaContext(); - std::string arch_flag() const; // "--gpu-architecture=sm_XY" -}; - -template -class DeviceBuffer { - CUdeviceptr ptr_ = 0; - size_t n_ = 0; - - public: - DeviceBuffer() = default; - explicit DeviceBuffer(size_t n) { alloc(n); } - ~DeviceBuffer() { free(); } - DeviceBuffer(const DeviceBuffer&) = delete; - DeviceBuffer& operator=(const DeviceBuffer&) = delete; - DeviceBuffer(DeviceBuffer&& o) noexcept : ptr_(o.ptr_), n_(o.n_) { o.ptr_ = 0; o.n_ = 0; } - - void alloc(size_t n) { - free(); - if (n) cu_check(cuMemAlloc(&ptr_, n * sizeof(T)), "cuMemAlloc"); - n_ = n; - } - void free() { - if (ptr_) cuMemFree(ptr_); - ptr_ = 0; n_ = 0; - } - void upload(const T* host, size_t n) { cu_check(cuMemcpyHtoD(ptr_, host, n * sizeof(T)), "HtoD"); } - void download(T* host, size_t n) const { cu_check(cuMemcpyDtoH(host, ptr_, n * sizeof(T)), "DtoH"); } - CUdeviceptr ptr() const { return ptr_; } - CUdeviceptr* addr() { return &ptr_; } - size_t size() const { return n_; } -}; - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/include/gpr/geometry.hpp b/data_acq_and_processing/gpr_cpp/include/gpr/geometry.hpp deleted file mode 100644 index cd26194..0000000 --- a/data_acq_and_processing/gpr_cpp/include/gpr/geometry.hpp +++ /dev/null @@ -1,41 +0,0 @@ -// Геометрия: опорное расхождение пары, смещения/веса кадров, построение сетки. -#pragma once -#include - -#include "gpr/config.hpp" -#include "gpr/types.hpp" - -namespace gpr { - -// 1/geo_ref пары (порт pair_geo_ref, скаляр float64). -double pair_geo_ref(const GeometryConfig&, const BPConfig&, int i_tx, int i_rx, double yy); - -// Смещения кадров относительно опорного (порт sar_frame_offsets). -// frame_times[k] — время кадра (сек, от ns-таймстемпа); t_ref — время опорного. -std::vector frame_offsets(const std::vector& frame_times, double t_ref, - const MotionConfig&); - -// Косинусные веса апертуры (порт sar_frame_weights); dt берётся относительно центра. -std::vector frame_weights(const std::vector& offsets, double edge_weight); - -// Высота плоскости земли (радарные оси) на радарной глубине depth: -// y = (-ground_depth + depth*sin(pitch)) / cos(pitch) (порт bp_plane_y). -double ground_plane_y(double depth, double ground_depth_m, double pitch_rad); - -// Сетка сцены по явным границам SARConfig (x/y/z_min..max, шаг voxel_m); -// оси — linspace c числом точек round(span/step)+1, как в ноутбуке. -Grid build_full_region_grid(const SARConfig&); - -// Ноутбучные границы (sar_build_full_region_grid): X — размах антенн ±margin, -// Z — полоса, покрывающая радарные глубины [min_depth, max_depth] на всех -// высотах Y. Используются parity-инструментом для сверки с эталонными npz. -void notebook_grid_bounds(const GeometryConfig&, const BPConfig&, const SARConfig&, - double pitch_deg, double x_margin_m, - double& x_min, double& x_max, double& z_min, double& z_max); - -// point_to_scene: оси радара → сцена (поворот на pitch + подъём на ground_depth, -// если ground; Y=0 сцены — земля). -std::array point_to_scene(double x, double y, double z, const MotionConfig&, - const SARConfig&); - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/include/gpr/nvrtc_kernel.hpp b/data_acq_and_processing/gpr_cpp/include/gpr/nvrtc_kernel.hpp deleted file mode 100644 index de194af..0000000 --- a/data_acq_and_processing/gpr_cpp/include/gpr/nvrtc_kernel.hpp +++ /dev/null @@ -1,25 +0,0 @@ -// Компиляция строки CUDA-ядра через NVRTC → cubin под арх устройства → CUfunction. -#pragma once -#include - -#include - -namespace gpr { - -class NvrtcKernel { - CUmodule module_ = nullptr; - CUfunction func_ = nullptr; - - public: - NvrtcKernel() = default; - ~NvrtcKernel(); - NvrtcKernel(const NvrtcKernel&) = delete; - NvrtcKernel& operator=(const NvrtcKernel&) = delete; - - // Компилирует src в cubin под arch_flag ("--gpu-architecture=sm_XY"), - // загружает модуль и берёт функцию func_name. - void compile(const char* src, const std::string& arch_flag, const char* func_name); - CUfunction func() const { return func_; } -}; - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/include/gpr/sar3d.hpp b/data_acq_and_processing/gpr_cpp/include/gpr/sar3d.hpp deleted file mode 100644 index db6fc30..0000000 --- a/data_acq_and_processing/gpr_cpp/include/gpr/sar3d.hpp +++ /dev/null @@ -1,55 +0,0 @@ -// 3D SAR: сборка взвешенного объёма апертуры за один запуск ядра. -#pragma once -#include -#include - -#include "gpr/ascan.hpp" -#include "gpr/config.hpp" -#include "gpr/device.hpp" -#include "gpr/geometry.hpp" -#include "gpr/nvrtc_kernel.hpp" -#include "gpr/types.hpp" - -namespace gpr { - -// Один кадр апертуры: A-сканы + смещение + вес. -struct ApertureFrame { - const AScanSet* ascans; - FrameOffset offset; - double weight; -}; - -// Сырой результат объёма (до finish_volume). -struct VolumeRaw { - std::vector acc; // [NY*NZ*NX] - std::vector cnt; // [NY*NZ*NX] - int NX = 0, NY = 0, NZ = 0; -}; - -// Финальный объём (порт sar_finish_volume): |acc/cnt| → маска глубины → -// нормировка по Y-планам без краёв → гаусс по (Z,X) → нормировка → clip. -struct FinishedVolume { - std::vector volume; // [NY*NZ*NX], нормирован на ~[0,1] - double level = 0.0; // масштаб, на который поделён объём - int NX = 0, NY = 0, NZ = 0; -}; - -FinishedVolume finish_volume(const VolumeRaw&, const Grid&, double pitch_deg, - const BPConfig&, const SARConfig&); - -class SarEngine { - public: - SarEngine(const GeometryConfig&, const BPConfig&, const MotionConfig&, const SARConfig&); - // Весь объём апертуры за один запуск ядра; возвращает сырые acc/cnt (host). - VolumeRaw aperture_volume(const std::vector& frames, const Grid&); - - private: - GeometryConfig geom_; - BPConfig bp_; - MotionConfig mot_; - SARConfig sar_; - std::unique_ptr ctx_; - std::unique_ptr kernel_; -}; - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/include/gpr/types.hpp b/data_acq_and_processing/gpr_cpp/include/gpr/types.hpp deleted file mode 100644 index ab02ce3..0000000 --- a/data_acq_and_processing/gpr_cpp/include/gpr/types.hpp +++ /dev/null @@ -1,25 +0,0 @@ -// Базовые типы счёт-ядра. -#pragma once -#include -#include -#include - -namespace gpr { - -using c64 = std::complex; // complex64, layout == cuComplex (2×float) -using c128 = std::complex; // float64 для препроцессинга/IFFT - -// Ground-scene сетка (порт dict из geometry.py: x,y,z оси в метрах). -struct Grid { - std::vector x, y, z; - int NX() const { return (int)x.size(); } - int NY() const { return (int)y.size(); } - int NZ() const { return (int)z.size(); } -}; - -// Смещение антенн кадра апертуры (порт sar_frame_offsets). -struct FrameOffset { - double dx_m = 0.0, dy_m = 0.0, dz_m = 0.0, dt_s = 0.0, horizontal_m = 0.0; -}; - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/src/ascan.cpp b/data_acq_and_processing/gpr_cpp/src/ascan.cpp deleted file mode 100644 index a273812..0000000 --- a/data_acq_and_processing/gpr_cpp/src/ascan.cpp +++ /dev/null @@ -1,250 +0,0 @@ -#include "gpr/ascan.hpp" - -#include -#include -#include -#include - -#include "pocketfft_hdronly.h" - -namespace gpr { - -TxWindows uniform_tx_windows(const std::vector& tx_order, double acq_s, int tx_per_segment) { - TxWindows windows; - if (tx_order.empty()) return windows; - int per_seg = std::max(1, tx_per_segment); - int n_seg = std::max(1, (int)((tx_order.size() + per_seg - 1) / per_seg)); - double t_seg = acq_s / n_seg; - for (size_t k = 0; k < tx_order.size(); ++k) { - int seg = (int)k / per_seg; - windows[tx_order[k]] = {seg * t_seg, seg * t_seg + t_seg}; - } - return windows; -} - -PairTimestamps compute_pair_timestamps(const TxWindows& tx_windows_s, double acq_s, - const std::vector& tx_order, - const GeometryConfig& geom, const MotionConfig& mot) { - // Аппаратный порядок: сперва tx_order (run.combos), затем прочие ключи окон. - std::vector hw_order; - for (int tx : tx_order) - if (tx_windows_s.count(tx) && std::find(hw_order.begin(), hw_order.end(), tx) == hw_order.end()) - hw_order.push_back(tx); - for (auto& [tx, _] : tx_windows_s) - if (std::find(hw_order.begin(), hw_order.end(), tx) == hw_order.end()) - hw_order.push_back(tx); - - // Tx с одинаковым окном — один сегмент; сегменты по возрастанию окна, - // слот — порядок появления в hw_order (порт группировки ноутбука). - std::vector, std::vector>> groups; - for (int tx : hw_order) { - auto win = tx_windows_s.at(tx); - auto found = std::find_if(groups.begin(), groups.end(), - [&](const auto& g) { return g.first == win; }); - if (found == groups.end()) - groups.push_back({win, {tx}}); - else - found->second.push_back(tx); - } - std::sort(groups.begin(), groups.end(), - [](const auto& a, const auto& b) { return a.first < b.first; }); - - struct SegSlot { double t0, t1; int slot, n_slots; }; - std::map seg_of; - for (auto& [win, txs] : groups) - for (size_t slot = 0; slot < txs.size(); ++slot) - seg_of[txs[slot]] = {win.first, win.second, (int)slot, (int)txs.size()}; - - double cos_theta = std::cos(mot.look_angle_deg * M_PI / 180.0); - double t_ref = 0.5 * acq_s; - PairTimestamps ts; - for (int i = 0; i < geom.n_tx; ++i) { - auto it = seg_of.find(geom.output_idx[i]); - if (it == seg_of.end()) - throw std::runtime_error("pair timestamps: нет окна для Tx"); - const SegSlot& s = it->second; - PairTiming timing; - timing.tx_interleave_slot = s.slot; - timing.tx_interleave_slots = s.n_slots; - timing.segment_start_s = s.t0; - timing.segment_time_s = s.t1 - s.t0; - timing.t_center_s = 0.5 * (s.t0 + s.t1); - double dz = mot.direction_sign * mot.speed_m_s * (timing.t_center_s - t_ref) * cos_theta; - timing.dtau_pair_s = 2.0 * dz / geom.v; - for (int j = 0; j < geom.n_rx; ++j) ts[{i, j}] = timing; - } - return ts; -} - -static double median_of_diff(const std::vector& f) { - std::vector d(f.size() - 1); - for (size_t i = 0; i + 1 < f.size(); ++i) d[i] = f[i + 1] - f[i]; - std::sort(d.begin(), d.end()); - size_t n = d.size(); - return (n % 2) ? d[n / 2] : 0.5 * (d[n / 2 - 1] + d[n / 2]); -} - -std::vector apply_intra_sweep_phase_correction( - const std::vector& s21, const std::vector& freq_full, - const PairTiming& info, const MotionConfig& cfg, double wave_speed, - double f_start, double f_stop) { - std::vector out = s21; - int n_full = (int)freq_full.size(); - // индексы в полосе - std::vector idx; - for (int i = 0; i < n_full; ++i) - if (freq_full[i] >= f_start && freq_full[i] <= f_stop) idx.push_back(i); - if (idx.empty()) throw std::runtime_error("phase corr: пустая полоса"); - if (n_full < 2) return out; - - // Сегмент проходит все n_full частот за своё окно; на каждой частоте по - // очереди работают его n_slots передатчиков, поэтому событие частоты i для - // слота slot имеет номер (n_slots*i + slot) из (n_slots*n_full). - int n_slots = std::max(1, info.tx_interleave_slots); - int slot = info.tx_interleave_slot; - double dt_event = info.segment_time_s / ((double)n_slots * n_full); - double cos_theta = std::cos(cfg.look_angle_deg * M_PI / 180.0); - double pair_dtau = cfg.apply_pair_motion_correction ? info.dtau_pair_s : 0.0; - if (!cfg.apply_freq_phase_correction && pair_dtau == 0.0) return out; - for (int i : idx) { - double dtau_intra = 0.0; - if (cfg.apply_freq_phase_correction) { - double t_abs = info.segment_start_s + (double)(n_slots * i + slot) * dt_event; - double dt_intra = t_abs - info.t_center_s; - dtau_intra = 2.0 * cfg.direction_sign * cfg.speed_m_s * cos_theta * dt_intra / wave_speed; - } - double phi = 2.0 * M_PI * freq_full[i] * (dtau_intra + pair_dtau); - double ang = cfg.motion_phase_sign * phi; - out[i] *= c128(std::cos(ang), std::sin(ang)); - } - return out; -} - -static int next_pow2(int x) { - int p = 1; - while (p < x) p <<= 1; - return p; -} - -void compute_ascan_bp(const std::vector& s21, const std::vector& freq, - double f_start, double f_stop, bool window, int oversample, - std::vector& t_out, std::vector& h_out) { - std::vector freq_cut; - std::vector s_cut; - for (size_t i = 0; i < freq.size(); ++i) - if (freq[i] >= f_start && freq[i] <= f_stop) { - freq_cut.push_back(freq[i]); - s_cut.push_back(s21[i]); - } - if (freq_cut.size() < 2) throw std::runtime_error("ascan: <2 точек в полосе"); - - double df = median_of_diff(freq_cut); - int n = (int)freq_cut.size(); - int k0 = (int)std::nearbyint(freq_cut[0] / df); // round-half-to-even как python round - int min_len = 2 * (k0 + n - 1); - int n_fft_base = next_pow2(min_len); - int n_fft = n_fft_base * oversample; - double dt = 1.0 / ((double)n_fft * df); - - // окно Ханна (numpy np.hanning: 0.5 - 0.5*cos(2*pi*i/(N-1))) - std::vector H(n_fft, c128(0.0, 0.0)); - for (int i = 0; i < n; ++i) { - c128 v = s_cut[i]; - if (window) { - double w = 0.5 - 0.5 * std::cos(2.0 * M_PI * i / (n - 1)); - v *= w; - } - H[k0 + i] = v; - } - - // IFFT через pocketfft (backward + 1/n_fft == numpy ifft) - h_out.assign(n_fft, c128(0.0, 0.0)); - pocketfft::shape_t shape{(size_t)n_fft}; - pocketfft::stride_t stride{(ptrdiff_t)sizeof(c128)}; - pocketfft::c2c(shape, stride, stride, {0}, pocketfft::BACKWARD, - H.data(), h_out.data(), (double)(1.0 / n_fft)); - - t_out.resize(n_fft); - for (int i = 0; i < n_fft; ++i) t_out[i] = i * dt; -} - -double pair_norm_scale(const std::vector& h, const std::vector& t, - double wave_speed, const BPConfig& bp) { - std::vector vals; - vals.reserve(h.size()); - for (size_t i = 0; i < h.size(); ++i) { - double z = t[i] * wave_speed / 2.0; - if (z >= bp.pair_norm_depth_min && z <= bp.pair_norm_depth_max) - vals.push_back(std::abs(h[i])); - } - if (vals.empty()) - for (auto& c : h) vals.push_back(std::abs(c)); - std::sort(vals.begin(), vals.end()); - // numpy.percentile linear interpolation - double q = bp.pair_norm_percentile / 100.0; - double pos = q * (vals.size() - 1); - int lo = (int)std::floor(pos); - int hi = std::min(lo + 1, (int)vals.size() - 1); - double scale = vals[lo] + (pos - lo) * (vals[hi] - vals[lo]); - if (!std::isfinite(scale) || scale <= bp.pair_norm_eps) scale = 1.0; - return scale; -} - -AScanSet preprocess_snapshot( - const std::map>& s21, - const std::map>& freq, - const std::map>* background, - const MotionConfig& mot, const GeometryConfig& geom, const BPConfig& bp, - const PairTimestamps& ts, bool normalize) { - AScanSet out; - out.pairs = geom.pair_keys(); - out.n_pairs = (int)out.pairs.size(); - - std::vector> Hs; - std::vector> Ts; - for (auto& key : out.pairs) { - auto its = s21.find(key); - if (its == s21.end()) throw std::runtime_error("preprocess: нет пары в s21"); - std::vector s = its->second; // копия - if (background) { - auto ib = background->find(key); - if (ib != background->end()) - for (size_t i = 0; i < s.size(); ++i) s[i] -= ib->second[i]; - } - const std::vector& fr = freq.at(key); - auto itt = ts.find(key); - if (itt != ts.end()) - s = apply_intra_sweep_phase_correction(s, fr, itt->second, mot, geom.v, - bp.f_start, bp.f_stop); - std::vector t; - std::vector h; - compute_ascan_bp(s, fr, bp.f_start, bp.f_stop, bp.window, bp.oversample, t, h); - Hs.push_back(std::move(h)); - Ts.push_back(std::move(t)); - } - - // общая ось времени — проверка - int n_fft = (int)Ts[0].size(); - double t0 = Ts[0][0]; - double dt = (Ts[0].back() - t0) / (n_fft - 1); - for (auto& t : Ts) - if ((int)t.size() != n_fft || std::abs(t[0] - t0) > 1e-18) - throw std::runtime_error("A-сканы: разная ось времени у пар"); - - if (normalize) { - for (int p = 0; p < out.n_pairs; ++p) { - double scale = pair_norm_scale(Hs[p], Ts[p], geom.v, bp); - for (auto& c : Hs[p]) c /= (scale + bp.pair_norm_eps); - } - } - - out.n_fft = n_fft; - out.t0 = t0; - out.dt = dt; - out.H.resize((size_t)out.n_pairs * n_fft); - for (int p = 0; p < out.n_pairs; ++p) - std::copy(Hs[p].begin(), Hs[p].end(), out.H.begin() + (size_t)p * n_fft); - return out; -} - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/src/device.cpp b/data_acq_and_processing/gpr_cpp/src/device.cpp deleted file mode 100644 index c60432c..0000000 --- a/data_acq_and_processing/gpr_cpp/src/device.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "gpr/device.hpp" - -#include - -namespace gpr { - -void cu_check(CUresult r, const char* what) { - if (r != CUDA_SUCCESS) { - const char* s = nullptr; - cuGetErrorString(r, &s); - throw std::runtime_error(std::string("CUDA error in ") + what + ": " + (s ? s : "?")); - } -} - -CudaContext::CudaContext() { - cu_check(cuInit(0), "cuInit"); - cu_check(cuDeviceGet(&dev, 0), "cuDeviceGet"); - cu_check(cuDeviceGetAttribute(&cc_major, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, dev), - "cc_major"); - cu_check(cuDeviceGetAttribute(&cc_minor, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, dev), - "cc_minor"); - cu_check(cuDevicePrimaryCtxRetain(&ctx, dev), "primaryCtxRetain"); - cu_check(cuCtxSetCurrent(ctx), "ctxSetCurrent"); -} - -CudaContext::~CudaContext() { - if (ctx) cuDevicePrimaryCtxRelease(dev); -} - -std::string CudaContext::arch_flag() const { - return "--gpu-architecture=sm_" + std::to_string(cc_major) + std::to_string(cc_minor); -} - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/src/geometry.cpp b/data_acq_and_processing/gpr_cpp/src/geometry.cpp deleted file mode 100644 index 8012416..0000000 --- a/data_acq_and_processing/gpr_cpp/src/geometry.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "gpr/geometry.hpp" - -#include -#include - -namespace gpr { - -double pair_geo_ref(const GeometryConfig& g, const BPConfig& bp, int i_tx, int i_rx, double yy) { - double xc = (g.x_tx[i_tx] + g.x_rx[i_rx]) / 2.0; - double Rtx = std::sqrt(std::pow(xc - g.x_tx[i_tx], 2) + std::pow(yy - g.y_tx[i_tx], 2) + - std::pow(bp.comp_ref_depth - g.z_tx[i_tx], 2)); - double Rrx = std::sqrt(std::pow(xc - g.x_rx[i_rx], 2) + std::pow(yy - g.y_rx[i_rx], 2) + - std::pow(bp.comp_ref_depth - g.z_rx[i_rx], 2)); - return 1.0 / (Rtx * Rrx + 1e-12) + 1e-30; -} - -std::vector frame_offsets(const std::vector& frame_times, double t_ref, - const MotionConfig& cfg) { - std::vector out(frame_times.size()); - double pitch = cfg.pitch_deg * M_PI / 180.0; - for (size_t k = 0; k < frame_times.size(); ++k) { - double t = frame_times[k] - t_ref; - double h = cfg.direction_sign * cfg.speed_m_s * t; - out[k].dx_m = 0.0; - out[k].dy_m = h * std::sin(pitch); - out[k].dz_m = h * std::cos(pitch); - out[k].dt_s = t; - out[k].horizontal_m = h; - } - return out; -} - -std::vector frame_weights(const std::vector& offs, double edge_weight) { - size_t n = offs.size(); - std::vector w(n, 1.0); - double edge = std::min(std::max(edge_weight, 0.0), 1.0); - if (n == 0 || edge >= 1.0) return w; - double dmax = 0.0; - for (auto& o : offs) dmax = std::max(dmax, std::abs(o.dt_s)); - if (dmax <= 1e-12) return w; - for (size_t i = 0; i < n; ++i) { - double r = std::min(std::abs(offs[i].dt_s) / dmax, 1.0); - w[i] = edge + (1.0 - edge) * 0.5 * (1.0 + std::cos(M_PI * r)); - } - return w; -} - -std::array point_to_scene(double x, double y, double z, const MotionConfig& mot, - const SARConfig& sar) { - if (!sar.coord_mode_ground) return {x, y, z}; - double p = mot.pitch_deg * M_PI / 180.0; - double ys = y * std::cos(p) - z * std::sin(p) + sar.ground_depth_m; - double zs = y * std::sin(p) + z * std::cos(p); - return {x, ys, zs}; -} - -double ground_plane_y(double depth, double ground_depth_m, double pitch_rad) { - return (-ground_depth_m + depth * std::sin(pitch_rad)) / std::cos(pitch_rad); -} - -Grid build_full_region_grid(const SARConfig& sar) { - // numpy: linspace(lo, hi, max(round(span/step)+1, 2)); nearbyint = банковское - // округление python round() - auto axis = [](double lo, double hi, double step, std::vector& out) { - int n = std::max((int)std::nearbyint((hi - lo) / step) + 1, 2); - out.resize(n); - for (int i = 0; i < n; ++i) out[i] = lo + (hi - lo) * i / (double)(n - 1); - }; - Grid g; - axis(sar.x_min_m, sar.x_max_m, sar.voxel_m[0], g.x); - axis(sar.y_min_m, sar.y_max_m, sar.voxel_m[1], g.y); - axis(sar.z_min_m, sar.z_max_m, sar.voxel_m[2], g.z); - return g; -} - -void notebook_grid_bounds(const GeometryConfig& geom, const BPConfig& bp, const SARConfig& sar, - double pitch_deg, double x_margin_m, - double& x_min, double& x_max, double& z_min, double& z_max) { - double x_lo = geom.x_tx[0], x_hi = geom.x_tx[0]; - for (double x : geom.x_tx) { x_lo = std::min(x_lo, x); x_hi = std::max(x_hi, x); } - for (double x : geom.x_rx) { x_lo = std::min(x_lo, x); x_hi = std::max(x_hi, x); } - x_min = x_lo - x_margin_m; - x_max = x_hi + x_margin_m; - - // Радарная z линейна по Z сцены при фиксированной высоте: - // z_rad(Y, Z=0) = -(Y - ground_depth)*sin(p), z = (depth - z_rad)/cos(p). - double p = pitch_deg * M_PI / 180.0; - double cos_p = std::cos(p), sin_p = std::sin(p); - double z_lo = 0.0, z_hi = 0.0; - bool first = true; - for (double y : {sar.y_min_m, sar.y_max_m}) { - for (double depth : {bp.min_depth, bp.max_depth}) { - double z_rad0 = -(y - sar.ground_depth_m) * sin_p; - double z = (depth - z_rad0) / cos_p; - if (first) { z_lo = z_hi = z; first = false; } - z_lo = std::min(z_lo, z); - z_hi = std::max(z_hi, z); - } - } - z_min = z_lo; - z_max = z_hi; -} - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/src/kernel_src.hpp b/data_acq_and_processing/gpr_cpp/src/kernel_src.hpp deleted file mode 100644 index 6ca1da7..0000000 --- a/data_acq_and_processing/gpr_cpp/src/kernel_src.hpp +++ /dev/null @@ -1,83 +0,0 @@ -// Исходник CUDA-ядра для NVRTC (порт sar_frame_contribution из -// Multi_antenna_3D_clean_v1.ipynb). Self-contained complex. -// -// Геометрия (координаты, дальности, tau, вес компенсации) считается в double: -// эталон ноутбука — float64, а в float32 позиция выборки A-скана (тысячи -// отсчётов) теряет ~3e-4 отсчёта и расходится с эталоном на ~1e-4. A-сканы -// остаются float32 — их значения O(1) и линейная интерполяция не теряет. -#pragma once - -namespace gpr { - -inline const char* APERTURE_KERNEL_SRC = R"CUDA( -struct cf { float x, y; }; - -// Взвешенный объём апертуры: один поток на воксель, внутренний цикл по -// n_contrib = n_frames*n_pairs. Обрабатывается диапазон вокселей -// [gid_begin, gid_end) — хост запускает ядро чанками, переиспользуя небольшие -// acc/cnt-буферы (полный объём в VRAM не обязан помещаться: на десктопе с -// занятой другими процессами картой свободных может быть <200 МБ). -extern "C" __global__ void bp_aperture_contribution( - const cf* __restrict__ H, - const double t0, const double inv_dt, const int n_fft, const double inv_v, - const double* __restrict__ xt, const double* __restrict__ yt, const double* __restrict__ zt, - const double* __restrict__ xr, const double* __restrict__ yr, const double* __restrict__ zr, - const double* __restrict__ inv_geo_ref, const float* __restrict__ wgt, - const double* __restrict__ gx, const double* __restrict__ gy, const double* __restrict__ gz, - const int NX, const int NY, const int NZ, const int n_contrib, - const long gid_begin, const long gid_end, - const double cos_p, const double sin_p, const double ground_depth, - const double min_depth, const double max_depth, - const double comp_pow, const double w_max, const int use_comp, - cf* __restrict__ acc, float* __restrict__ cnt) -{ - for (long gid = gid_begin + blockIdx.x * blockDim.x + threadIdx.x; - gid < gid_end; gid += (long)gridDim.x * blockDim.x) { - int ix = gid % NX; - int iz = (gid / NX) % NZ; - int iy = gid / ((long)NX * NZ); - double x = gx[ix]; - double zs = gz[iz]; - // Сцена → оси радара: Y=0 сцены — земля, ground_depth ниже нижнего ряда - // антенн (порт sar_scene_to_radar_yz). - double y0 = gy[iy] - ground_depth; - double y_rad = y0 * cos_p + zs * sin_p; - double z_rad = -y0 * sin_p + zs * cos_p; - bool depth_ok = (z_rad >= min_depth) && (z_rad <= max_depth); - - double ax = 0.0, ay = 0.0; - float c = 0.f; - for (int i = 0; i < n_contrib; ++i) { - double dxt = x - xt[i], dyt = y_rad - yt[i], dzt = z_rad - zt[i]; - double dxr = x - xr[i], dyr = y_rad - yr[i], dzr = z_rad - zr[i]; - double Rt = sqrt(dxt*dxt + dyt*dyt + dzt*dzt); - double Rr = sqrt(dxr*dxr + dyr*dyr + dzr*dzr); - double pos = ((Rt + Rr) * inv_v - t0) * inv_dt; - bool inside = (pos >= 0.0) && (pos <= (double)(n_fft - 1)); - if (!inside || !depth_ok) continue; - int k = (int)floor(pos); - if (k < 0) k = 0; - if (k > n_fft - 2) k = n_fft - 2; - float f = (float)(pos - (double)k); - const cf* h = H + (long)i * n_fft; - float hx = h[k].x + f * (h[k+1].x - h[k].x); - float hy = h[k].y + f * (h[k+1].y - h[k].y); - double w = (double)wgt[i]; - if (use_comp) { - double ratio = (1.0/(Rt*Rr + 1e-12) + 1e-30) * inv_geo_ref[i]; - double ww = 1.0/(pow(ratio, comp_pow) + 1e-12); - ww = fmin(fmax(ww, 0.0), w_max); - w *= ww; - } - ax += w * (double)hx; - ay += w * (double)hy; - c += wgt[i]; - } - acc[gid - gid_begin].x = (float)ax; - acc[gid - gid_begin].y = (float)ay; - cnt[gid - gid_begin] = c; - } -} -)CUDA"; - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/src/nvrtc_kernel.cpp b/data_acq_and_processing/gpr_cpp/src/nvrtc_kernel.cpp deleted file mode 100644 index 0fce0d6..0000000 --- a/data_acq_and_processing/gpr_cpp/src/nvrtc_kernel.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "gpr/nvrtc_kernel.hpp" - -#include - -#include -#include -#include - -#include "gpr/device.hpp" - -namespace gpr { - -static void nvrtc_check(nvrtcResult r, const char* what) { - if (r != NVRTC_SUCCESS) - throw std::runtime_error(std::string("NVRTC error in ") + what + ": " + - nvrtcGetErrorString(r)); -} - -NvrtcKernel::~NvrtcKernel() { - if (module_) cuModuleUnload(module_); -} - -void NvrtcKernel::compile(const char* src, const std::string& arch_flag, const char* func_name) { - nvrtcProgram prog; - nvrtc_check(nvrtcCreateProgram(&prog, src, "gpr_kernel.cu", 0, nullptr, nullptr), - "createProgram"); - std::vector opts{arch_flag.c_str()}; - nvrtcResult cr = nvrtcCompileProgram(prog, (int)opts.size(), opts.data()); - if (cr != NVRTC_SUCCESS) { - size_t logsz = 0; - nvrtcGetProgramLogSize(prog, &logsz); - std::string log(logsz, '\0'); - if (logsz) nvrtcGetProgramLog(prog, &log[0]); - nvrtcDestroyProgram(&prog); - throw std::runtime_error("NVRTC compile failed:\n" + log); - } - // cubin под точную арх (без JIT PTX — драйвер грузит SASS напрямую) - size_t cubsz = 0; - nvrtc_check(nvrtcGetCUBINSize(prog, &cubsz), "getCUBINSize"); - std::string cubin(cubsz, '\0'); - nvrtc_check(nvrtcGetCUBIN(prog, &cubin[0]), "getCUBIN"); - nvrtcDestroyProgram(&prog); - - cu_check(cuModuleLoadDataEx(&module_, cubin.data(), 0, nullptr, nullptr), "moduleLoad"); - cu_check(cuModuleGetFunction(&func_, module_, func_name), "getFunction"); -} - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/src/sar3d.cpp b/data_acq_and_processing/gpr_cpp/src/sar3d.cpp deleted file mode 100644 index 97fb721..0000000 --- a/data_acq_and_processing/gpr_cpp/src/sar3d.cpp +++ /dev/null @@ -1,233 +0,0 @@ -#include "gpr/sar3d.hpp" - -#include -#include -#include - -#include "kernel_src.hpp" - -namespace gpr { - -SarEngine::SarEngine(const GeometryConfig& g, const BPConfig& bp, const MotionConfig& mot, - const SARConfig& sar) - : geom_(g), bp_(bp), mot_(mot), sar_(sar) { - geom_.finalize(); - ctx_ = std::make_unique(); - kernel_ = std::make_unique(); - kernel_->compile(APERTURE_KERNEL_SRC, ctx_->arch_flag(), "bp_aperture_contribution"); -} - -VolumeRaw SarEngine::aperture_volume(const std::vector& frames, const Grid& grid) { - if (frames.empty()) throw std::runtime_error("aperture_volume: нет кадров"); - auto pairs = geom_.pair_keys(); - int n_pairs = (int)pairs.size(); - int n_frames = (int)frames.size(); - int n_contrib = n_frames * n_pairs; - - const AScanSet& a0 = *frames[0].ascans; - int n_fft = a0.n_fft; - double t0 = a0.t0, dt = a0.dt; - - int NX = grid.NX(), NY = grid.NY(), NZ = grid.NZ(); - size_t nvox = (size_t)NX * NY * NZ; - - double pitch = mot_.pitch_deg * M_PI / 180.0; - // Опорное расхождение пары — на плоскости земли на глубине comp_ref_depth. - double geo_ref_y = sar_.coord_mode_ground - ? ground_plane_y(bp_.comp_ref_depth, sar_.ground_depth_m, pitch) - : 0.0; - - // Плоские массивы по (кадр, пара) - std::vector H_stack((size_t)n_contrib * n_fft); - std::vector xt(n_contrib), yt(n_contrib), zt(n_contrib); - std::vector xr(n_contrib), yr(n_contrib), zr(n_contrib); - std::vector igr(n_contrib); - std::vector wgt(n_contrib); - int idx = 0; - for (int f = 0; f < n_frames; ++f) { - const AScanSet& a = *frames[f].ascans; - if (a.n_fft != n_fft || std::abs(a.t0 - t0) > 1e-18) - throw std::runtime_error("кадры апертуры: разная ось времени"); - double dx = frames[f].offset.dx_m, dy = frames[f].offset.dy_m, dz = frames[f].offset.dz_m; - float w = (float)frames[f].weight; - for (int p = 0; p < n_pairs; ++p) { - int i = pairs[p].first, j = pairs[p].second; - // A-скан пары p → в стек (каст c128→c64) - const c128* src = a.pair_ptr(p); - c64* dst = H_stack.data() + (size_t)idx * n_fft; - for (int k = 0; k < n_fft; ++k) dst[k] = c64((float)src[k].real(), (float)src[k].imag()); - xt[idx] = geom_.x_tx[i] + dx; yt[idx] = geom_.y_tx[i] + dy; - zt[idx] = geom_.z_tx[i] + dz; - xr[idx] = geom_.x_rx[j] + dx; yr[idx] = geom_.y_rx[j] + dy; - zr[idx] = geom_.z_rx[j] + dz; - igr[idx] = 1.0 / pair_geo_ref(geom_, bp_, i, j, geo_ref_y); - wgt[idx] = w; - ++idx; - } - } - - double cos_p = 1.0, sin_p = 0.0, ground_depth = 0.0; - if (sar_.coord_mode_ground) { - cos_p = std::cos(pitch); - sin_p = std::sin(pitch); - ground_depth = sar_.ground_depth_m; - } - - // Заливка на устройство - DeviceBuffer dH(H_stack.size()); dH.upload(H_stack.data(), H_stack.size()); - DeviceBuffer dxt(n_contrib), dyt(n_contrib), dzt(n_contrib); - DeviceBuffer dxr(n_contrib), dyr(n_contrib), dzr(n_contrib); - DeviceBuffer digr(n_contrib); - DeviceBuffer dwgt(n_contrib); - dxt.upload(xt.data(), n_contrib); dyt.upload(yt.data(), n_contrib); dzt.upload(zt.data(), n_contrib); - dxr.upload(xr.data(), n_contrib); dyr.upload(yr.data(), n_contrib); dzr.upload(zr.data(), n_contrib); - digr.upload(igr.data(), n_contrib); dwgt.upload(wgt.data(), n_contrib); - DeviceBuffer dgx(NX), dgy(NY), dgz(NZ); - dgx.upload(grid.x.data(), NX); dgy.upload(grid.y.data(), NY); dgz.upload(grid.z.data(), NZ); - - // Объём считается чанками с переиспользуемыми acc/cnt-буферами: полные - // буферы (~115 МБ на сетку 9.6 млн вокселей) могут не влезать в остаток - // VRAM, когда карту делят десктоп-процессы. - const size_t chunk_vox = std::min(nvox, 2'000'000); - DeviceBuffer dacc(chunk_vox); - DeviceBuffer dcnt(chunk_vox); - - double inv_dt = 1.0 / dt, inv_v = 1.0 / geom_.v; - double min_d = bp_.min_depth, max_d = bp_.max_depth; - double comp_pow = bp_.comp_range_power, w_max = bp_.comp_weight_max; - int use_comp = sar_.use_compensation ? 1 : 0; - - VolumeRaw out; - out.NX = NX; out.NY = NY; out.NZ = NZ; - out.acc.resize(nvox); out.cnt.resize(nvox); - int block = 256; - for (size_t begin = 0; begin < nvox; begin += chunk_vox) { - const size_t end = std::min(nvox, begin + chunk_vox); - long gid_begin = (long)begin, gid_end = (long)end; - void* args[] = { - dH.addr(), &t0, &inv_dt, &n_fft, &inv_v, - dxt.addr(), dyt.addr(), dzt.addr(), dxr.addr(), dyr.addr(), dzr.addr(), - digr.addr(), dwgt.addr(), dgx.addr(), dgy.addr(), dgz.addr(), - &NX, &NY, &NZ, &n_contrib, &gid_begin, &gid_end, - &cos_p, &sin_p, &ground_depth, &min_d, &max_d, - &comp_pow, &w_max, &use_comp, dacc.addr(), dcnt.addr()}; - int gridDim = (int)std::min(65535, ((long)(end - begin) + block - 1) / block); - cu_check(cuLaunchKernel(kernel_->func(), gridDim, 1, 1, block, 1, 1, 0, 0, args, nullptr), - "launch aperture"); - cu_check(cuCtxSynchronize(), "sync"); - dacc.download(out.acc.data() + begin, end - begin); - dcnt.download(out.cnt.data() + begin, end - begin); - } - return out; -} - -namespace { - -// scipy.ndimage.gaussian_filter: ядро радиуса int(truncate*sigma+0.5), reflect. -std::vector gaussian_kernel_1d(double sigma, int radius) { - std::vector w(2 * radius + 1); - double sum = 0.0; - for (int i = -radius; i <= radius; ++i) { - w[i + radius] = std::exp(-0.5 * i * i / (sigma * sigma)); - sum += w[i + radius]; - } - for (auto& v : w) v /= sum; - return w; -} - -// Корреляция вдоль оси с reflect-границами ('d c b a | a b c d'). -inline int reflect_index(int i, int n) { - while (i < 0 || i >= n) { - if (i < 0) i = -i - 1; - if (i >= n) i = 2 * n - i - 1; - } - return i; -} - -} // namespace - -FinishedVolume finish_volume(const VolumeRaw& raw, const Grid& grid, double pitch_deg, - const BPConfig& bp, const SARConfig& sar) { - int NX = raw.NX, NY = raw.NY, NZ = raw.NZ; - size_t nvox = (size_t)NX * NY * NZ; - FinishedVolume out; - out.NX = NX; out.NY = NY; out.NZ = NZ; - - std::vector vol(nvox); - for (size_t i = 0; i < nvox; ++i) - vol[i] = std::abs(c128(raw.acc[i].real(), raw.acc[i].imag())) / ((double)raw.cnt[i] + 1e-12); - - // Маска глубины в радарных осях (в ядре невалидные вкладов не получали, - // но |0/(0+eps)| уже 0; обнуление здесь повторяет ноутбук буквально). - if (sar.coord_mode_ground) { - double p = pitch_deg * M_PI / 180.0; - double cos_p = std::cos(p), sin_p = std::sin(p); - for (int iy = 0; iy < NY; ++iy) { - double y0 = grid.y[iy] - sar.ground_depth_m; - for (int iz = 0; iz < NZ; ++iz) { - double z_rad = -y0 * sin_p + grid.z[iz] * cos_p; - if (z_rad >= bp.min_depth && z_rad <= bp.max_depth) continue; - double* row = vol.data() + ((size_t)iy * NZ + iz) * NX; - std::fill(row, row + NX, 0.0); - } - } - } - - // Плоскости Y для нормировки: без верхних/нижних norm_edge_margin_m. - std::vector ref_plane(NY, true); - double margin = sar.norm_edge_margin_m; - double y_span = grid.y.back() - grid.y.front(); - if (margin > 0.0 && y_span > 2.5 * margin) { - for (int iy = 0; iy < NY; ++iy) - ref_plane[iy] = grid.y[iy] >= grid.y.front() + margin && - grid.y[iy] <= grid.y.back() - margin; - } - auto ref_max = [&](const std::vector& v) { - double m = 0.0; - for (int iy = 0; iy < NY; ++iy) { - if (!ref_plane[iy]) continue; - const double* plane = v.data() + (size_t)iy * NZ * NX; - for (size_t i = 0; i < (size_t)NZ * NX; ++i) m = std::max(m, plane[i]); - } - return m + 1e-12; - }; - - double level_raw = ref_max(vol); - for (auto& v : vol) v /= level_raw; - - // Гаусс sigma=(0, s, s): сглаживание по Z и X внутри каждого Y-плана - // (порядок осей как у scipy: сначала ось 1, затем ось 2). - if (sar.smooth_sigma > 0.0) { - int radius = (int)(4.0 * sar.smooth_sigma + 0.5); - const auto kernel = gaussian_kernel_1d(sar.smooth_sigma, radius); - std::vector tmp((size_t)NZ * NX); - for (int iy = 0; iy < NY; ++iy) { - double* plane = vol.data() + (size_t)iy * NZ * NX; - // вдоль Z (ось 1) - for (int iz = 0; iz < NZ; ++iz) - for (int ix = 0; ix < NX; ++ix) { - double s = 0.0; - for (int k = -radius; k <= radius; ++k) - s += kernel[k + radius] * plane[(size_t)reflect_index(iz + k, NZ) * NX + ix]; - tmp[(size_t)iz * NX + ix] = s; - } - // вдоль X (ось 2) - for (int iz = 0; iz < NZ; ++iz) - for (int ix = 0; ix < NX; ++ix) { - double s = 0.0; - for (int k = -radius; k <= radius; ++k) - s += kernel[k + radius] * tmp[(size_t)iz * NX + reflect_index(ix + k, NX)]; - plane[(size_t)iz * NX + ix] = s; - } - } - } - - double level_smooth = ref_max(vol); - out.volume.resize(nvox); - for (size_t i = 0; i < nvox; ++i) - out.volume[i] = (float)std::max(vol[i] / level_smooth, 0.0); - out.level = level_raw * level_smooth; - return out; -} - -} // namespace gpr diff --git a/data_acq_and_processing/gpr_cpp/third_party/pocketfft/pocketfft_hdronly.h b/data_acq_and_processing/gpr_cpp/third_party/pocketfft/pocketfft_hdronly.h deleted file mode 100644 index de6c8d6..0000000 --- a/data_acq_and_processing/gpr_cpp/third_party/pocketfft/pocketfft_hdronly.h +++ /dev/null @@ -1,3853 +0,0 @@ -/* -This file is part of pocketfft. - -Copyright (C) 2010-2024 Max-Planck-Society -Copyright (C) 2019-2020 Peter Bell - -For the odd-sized DCT-IV transforms: - Copyright (C) 2003, 2007-14 Matteo Frigo - Copyright (C) 2003, 2007-14 Massachusetts Institute of Technology - -For the prev_good_size search: - Copyright (C) 2024 Tan Ping Liang, Peter Bell - -For the safeguards against integer overflow in good_size search: - Copyright (C) 2024 Cris Luengo - -Authors: Martin Reinecke, Peter Bell - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. -* Neither the name of the copyright holder nor the names of its contributors may - be used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -#ifndef POCKETFFT_HDRONLY_H -#define POCKETFFT_HDRONLY_H - -#ifndef __cplusplus -#error This file is C++ and requires a C++ compiler. -#endif - -#if !(__cplusplus >= 201103L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201103L)) -#error This file requires at least C++11 support. -#endif - -#ifndef POCKETFFT_NAMESPACE -#define POCKETFFT_NAMESPACE pocketfft -#endif - -#ifndef POCKETFFT_CACHE_SIZE -#define POCKETFFT_CACHE_SIZE 0 -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#if POCKETFFT_CACHE_SIZE!=0 -#include -#include -#endif - -#ifndef POCKETFFT_NO_MULTITHREADING -#include -#include -#include -#include -#include -#include -#include - -#ifdef POCKETFFT_PTHREADS -# include -#endif -#endif - -#if defined(__GNUC__) -#define POCKETFFT_NOINLINE __attribute__((noinline)) -#define POCKETFFT_RESTRICT __restrict__ -#elif defined(_MSC_VER) -#define POCKETFFT_NOINLINE __declspec(noinline) -#define POCKETFFT_RESTRICT __restrict -#else -#define POCKETFFT_NOINLINE -#define POCKETFFT_RESTRICT -#endif - -namespace POCKETFFT_NAMESPACE { - -namespace detail { -using std::size_t; -using std::ptrdiff_t; - -// Always use std:: for functions -template T cos(T) = delete; -template T sin(T) = delete; -template T sqrt(T) = delete; - -using shape_t = std::vector; -using stride_t = std::vector; - -constexpr bool FORWARD = true, - BACKWARD = false; - -// only enable vector support for gcc>=5.0 and clang>=5.0 -#ifndef POCKETFFT_NO_VECTORS -#define POCKETFFT_NO_VECTORS -#if defined(__INTEL_COMPILER) -// do nothing. This is necessary because this compiler also sets __GNUC__. -#elif defined(__clang__) -// AppleClang has their own version numbering -#ifdef __apple_build_version__ -# if (__clang_major__ > 9) || (__clang_major__ == 9 && __clang_minor__ >= 1) -# undef POCKETFFT_NO_VECTORS -# endif -#elif __clang_major__ >= 5 -# undef POCKETFFT_NO_VECTORS -#endif -#elif defined(__GNUC__) -#if __GNUC__>=5 -#undef POCKETFFT_NO_VECTORS -#endif -#endif -#endif - -template struct VLEN { static constexpr size_t val=1; }; - -#ifndef POCKETFFT_NO_VECTORS -#if (defined(__AVX512F__)) -template<> struct VLEN { static constexpr size_t val=16; }; -template<> struct VLEN { static constexpr size_t val=8; }; -#elif (defined(__AVX__)) -template<> struct VLEN { static constexpr size_t val=8; }; -template<> struct VLEN { static constexpr size_t val=4; }; -#elif (defined(__SSE2__)) -template<> struct VLEN { static constexpr size_t val=4; }; -template<> struct VLEN { static constexpr size_t val=2; }; -#elif (defined(__VSX__)) -template<> struct VLEN { static constexpr size_t val=4; }; -template<> struct VLEN { static constexpr size_t val=2; }; -#elif (defined(__ARM_NEON__) || defined(__ARM_NEON)) -template<> struct VLEN { static constexpr size_t val=4; }; -template<> struct VLEN { static constexpr size_t val=2; }; -#elif (defined(__riscv_vector) || defined(__riscv_v_intrinsic)) -template<> struct VLEN { static constexpr size_t val=8; }; -template<> struct VLEN { static constexpr size_t val=4; }; -#else -#define POCKETFFT_NO_VECTORS -#endif -#endif - -// std::aligned_alloc is a bit cursed ... it doesn't exist on MacOS < 10.15 -// and in musl, and other OSes seem to have even more peculiarities. -// Let's unconditionally work around it for now. -#if defined(POCKETFFT_USE_POSIX_MEMALIGN) && (defined(__APPLE__) || defined(__unix__)) -// Use posix_memalign on POSIX systems when explicitly enabled. -// The portable aligned_alloc below stores metadata at ptr[-1], which conflicts -// with ASAN's heap redzone and causes intermittent bus errors. -inline void *aligned_alloc(size_t align, size_t size) - { - align = std::max(align, sizeof(void*)); // posix_memalign requires align >= sizeof(void*) - void *ptr = nullptr; - if (posix_memalign(&ptr, align, size) != 0) - throw std::bad_alloc(); - return ptr; - } -inline void aligned_dealloc(void *ptr) - { free(ptr); } -#else -# if 0 -//#if (__cplusplus >= 201703L) && (!defined(__MINGW32__)) && (!defined(_MSC_VER)) && (__MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_15) -inline void *aligned_alloc(size_t align, size_t size) - { - // aligned_alloc() requires that the requested size is a multiple of "align" - void *ptr = ::aligned_alloc(align,(size+align-1)&(~(align-1))); - if (!ptr) throw std::bad_alloc(); - return ptr; - } -inline void aligned_dealloc(void *ptr) - { free(ptr); } -#else // portable emulation -inline void *aligned_alloc(size_t align, size_t size) - { - align = std::max(align, alignof(max_align_t)); - void *ptr = malloc(size+align); - if (!ptr) throw std::bad_alloc(); - void *res = reinterpret_cast - ((reinterpret_cast(ptr) & ~(uintptr_t(align-1))) + uintptr_t(align)); - (reinterpret_cast(res))[-1] = ptr; - return res; - } -inline void aligned_dealloc(void *ptr) - { if (ptr) free((reinterpret_cast(ptr))[-1]); } -# endif -#endif - -template class arr - { - private: - T *p; - size_t sz; - -#if defined(POCKETFFT_NO_VECTORS) - static T *ralloc(size_t num) - { - if (num==0) return nullptr; - void *res = malloc(num*sizeof(T)); - if (!res) throw std::bad_alloc(); - return reinterpret_cast(res); - } - static void dealloc(T *ptr) - { free(ptr); } -#else - static T *ralloc(size_t num) - { - if (num==0) return nullptr; - void *ptr = aligned_alloc(64, num*sizeof(T)); - return static_cast(ptr); - } - static void dealloc(T *ptr) - { aligned_dealloc(ptr); } -#endif - - public: - arr() : p(nullptr), sz(0) {} - explicit arr(size_t n) : p(ralloc(n)), sz(n) {} - arr(arr &&other) noexcept - : p(other.p), sz(other.sz) - { other.p=nullptr; other.sz=0; } - ~arr() { dealloc(p); } - - void resize(size_t n) - { - if (n==sz) return; - dealloc(p); - p = ralloc(n); - sz = n; - } - - T &operator[](size_t idx) { return p[idx]; } - const T &operator[](size_t idx) const { return p[idx]; } - - T *data() { return p; } - const T *data() const { return p; } - - size_t size() const { return sz; } - }; - -template struct cmplx { - T r, i; - cmplx() = default; - cmplx(T r_, T i_) : r(r_), i(i_) {} - void Set(T r_, T i_) { r=r_; i=i_; } - void Set(T r_) { r=r_; i=T(0); } - cmplx &operator+= (const cmplx &other) - { r+=other.r; i+=other.i; return *this; } - templatecmplx &operator*= (T2 other) - { r*=other; i*=other; return *this; } - templatecmplx &operator*= (const cmplx &other) - { - T tmp = r*other.r - i*other.i; - i = r*other.i + i*other.r; - r = tmp; - return *this; - } - templatecmplx &operator+= (const cmplx &other) - { r+=other.r; i+=other.i; return *this; } - templatecmplx &operator-= (const cmplx &other) - { r-=other.r; i-=other.i; return *this; } - template auto operator* (const T2 &other) const - -> cmplx - { return {r*other, i*other}; } - template auto operator+ (const cmplx &other) const - -> cmplx - { return {r+other.r, i+other.i}; } - template auto operator- (const cmplx &other) const - -> cmplx - { return {r-other.r, i-other.i}; } - template auto operator* (const cmplx &other) const - -> cmplx - { return {r*other.r-i*other.i, r*other.i + i*other.r}; } - template auto special_mul (const cmplx &other) const - -> cmplx - { - using Tres = cmplx; - return fwd ? Tres(r*other.r+i*other.i, i*other.r-r*other.i) - : Tres(r*other.r-i*other.i, r*other.i+i*other.r); - } -}; -template inline void PM(T &a, T &b, T c, T d) - { a=c+d; b=c-d; } -template inline void PMINPLACE(T &a, T &b) - { T t = a; a+=b; b=t-b; } -template inline void MPINPLACE(T &a, T &b) - { T t = a; a-=b; b=t+b; } -template cmplx conj(const cmplx &a) - { return {a.r, -a.i}; } -template void special_mul (const cmplx &v1, const cmplx &v2, cmplx &res) - { - res = fwd ? cmplx(v1.r*v2.r+v1.i*v2.i, v1.i*v2.r-v1.r*v2.i) - : cmplx(v1.r*v2.r-v1.i*v2.i, v1.r*v2.i+v1.i*v2.r); - } - -template void ROT90(cmplx &a) - { auto tmp_=a.r; a.r=-a.i; a.i=tmp_; } -template void ROTX90(cmplx &a) - { auto tmp_= fwd ? -a.r : a.r; a.r = fwd ? a.i : -a.i; a.i=tmp_; } - -// -// twiddle factor section -// -template class sincos_2pibyn - { - private: - using Thigh = typename std::conditional<(sizeof(T)>sizeof(double)), T, double>::type; - size_t N, mask, shift; - arr> v1, v2; - - static cmplx calc(size_t x, size_t n, Thigh ang) - { - x<<=3; - if (x<4*n) // first half - { - if (x<2*n) // first quadrant - { - if (x(std::cos(Thigh(x)*ang), std::sin(Thigh(x)*ang)); - return cmplx(std::sin(Thigh(2*n-x)*ang), std::cos(Thigh(2*n-x)*ang)); - } - else // second quadrant - { - x-=2*n; - if (x(-std::sin(Thigh(x)*ang), std::cos(Thigh(x)*ang)); - return cmplx(-std::cos(Thigh(2*n-x)*ang), std::sin(Thigh(2*n-x)*ang)); - } - } - else - { - x=8*n-x; - if (x<2*n) // third quadrant - { - if (x(std::cos(Thigh(x)*ang), -std::sin(Thigh(x)*ang)); - return cmplx(std::sin(Thigh(2*n-x)*ang), -std::cos(Thigh(2*n-x)*ang)); - } - else // fourth quadrant - { - x-=2*n; - if (x(-std::sin(Thigh(x)*ang), -std::cos(Thigh(x)*ang)); - return cmplx(-std::cos(Thigh(2*n-x)*ang), -std::sin(Thigh(2*n-x)*ang)); - } - } - } - - public: - POCKETFFT_NOINLINE explicit sincos_2pibyn(size_t n) - : N(n) - { - constexpr auto pi = 3.141592653589793238462643383279502884197L; - Thigh ang = Thigh(0.25L*pi/n); - size_t nval = (n+2)/2; - shift = 1; - while((size_t(1)< operator[](size_t idx) const - { - if (2*idx<=N) - { - auto x1=v1[idx&mask], x2=v2[idx>>shift]; - return cmplx(T(x1.r*x2.r-x1.i*x2.i), T(x1.r*x2.i+x1.i*x2.r)); - } - idx = N-idx; - auto x1=v1[idx&mask], x2=v2[idx>>shift]; - return cmplx(T(x1.r*x2.r-x1.i*x2.i), -T(x1.r*x2.i+x1.i*x2.r)); - } - }; - -struct util // hack to avoid duplicate symbols - { - static POCKETFFT_NOINLINE size_t largest_prime_factor (size_t n) - { - size_t res=1; - while ((n&1)==0) - { res=2; n>>=1; } - for (size_t x=3; x*x<=n; x+=2) - while ((n%x)==0) - { res=x; n/=x; } - if (n>1) res=n; - return res; - } - - static POCKETFFT_NOINLINE double cost_guess (size_t n) - { - constexpr double lfp=1.1; // penalty for non-hardcoded larger factors - size_t ni=n; - double result=0.; - while ((n&1)==0) - { result+=2; n>>=1; } - for (size_t x=3; x*x<=n; x+=2) - while ((n%x)==0) - { - result+= (x<=5) ? double(x) : lfp*double(x); // penalize larger prime factors - n/=x; - } - if (n>1) result+=(n<=5) ? double(n) : lfp*double(n); - return result*double(ni); - } - - /* inner workings of good_size_cmplx() */ - template - static POCKETFFT_NOINLINE UIntT good_size_cmplx_typed(UIntT n) - { - static_assert(std::numeric_limits::is_integer && (!std::numeric_limits::is_signed), - "type must be unsigned integer"); - if (n<=12) return n; - if (n>std::numeric_limits::max()/11/2) - { - // The algorithm below doesn't work for this value, the multiplication can overflow. - if (sizeof(UIntT)(n); - if (res<=std::numeric_limits::max()) - return static_cast(res); - } - // Otherwise, this size is ridiculously large, people shouldn't be computing FFTs this large. - throw std::runtime_error("FFT size is too large."); - } - - UIntT bestfac=2*n; - for (UIntT f11=1; f11n) - { - if (x>=1; - } - else - return n; - } - } - return bestfac; - } - /* returns the smallest composite of 2, 3, 5, 7 and 11 which is >= n */ - static POCKETFFT_NOINLINE size_t good_size_cmplx(size_t n) - { - return good_size_cmplx_typed(n); - } - /* returns the smallest composite of 2, 3, 5, 7 and 11 which is >= n - and a multiple of required_factor. */ - static POCKETFFT_NOINLINE size_t good_size_cmplx(size_t n, - size_t required_factor) - { - if (required_factor<1) - throw std::runtime_error("required factor must not be 0"); - return good_size_cmplx((n+required_factor-1)/required_factor) * required_factor; - } - - /* inner workings of good_size_real() */ - template - static POCKETFFT_NOINLINE UIntT good_size_real_typed(UIntT n) - { - static_assert(std::numeric_limits::is_integer && (!std::numeric_limits::is_signed), - "type must be unsigned integer"); - if (n<=6) return n; - if (n>std::numeric_limits::max()/5/2) - { - // The algorithm below doesn't work for this value, the multiplication can overflow. - if (sizeof(UIntT)(n); - if (res<=std::numeric_limits::max()) - return static_cast(res); - } - // Otherwise, this size is ridiculously large, people shouldn't be computing FFTs this large. - throw std::runtime_error("FFT size is too large."); - } - - UIntT bestfac=2*n; - for (UIntT f5=1; f5n) - { - if (x>=1; - } - else - return n; - } - } - return bestfac; - } - /* returns the smallest composite of 2, 3, 5 which is >= n */ - static POCKETFFT_NOINLINE size_t good_size_real(size_t n) - { - return good_size_real_typed(n); - } - /* returns the smallest composite of 2, 3, 5 which is >= n - and a multiple of required_factor. */ - static POCKETFFT_NOINLINE size_t good_size_real(size_t n, - size_t required_factor) - { - if (required_factor<1) - throw std::runtime_error("required factor must not be 0"); - return good_size_real((n+required_factor-1)/required_factor) * required_factor; - } - - /* inner workings of prev_good_size_cmplx() */ - template - static POCKETFFT_NOINLINE UIntT prev_good_size_cmplx_typed(UIntT n) - { - static_assert(std::numeric_limits::is_integer && (!std::numeric_limits::is_signed), - "type must be unsigned integer"); - if (n<=12) return n; - if (n>std::numeric_limits::max()/11) - { - // The algorithm below doesn't work for this value, the multiplication can overflow. - if (sizeof(UIntT)(n); - if (res<=std::numeric_limits::max()) - return static_cast(res); - } - // Otherwise, this size is ridiculously large, people shouldn't be computing FFTs this large. - throw std::runtime_error("FFT size is too large."); - } - - UIntT bestfound = 1; - for (UIntT f11 = 1;f11 <= n; f11 *= 11) - for (UIntT f117 = f11; f117 <= n; f117 *= 7) - for (UIntT f1175 = f117; f1175 <= n; f1175 *= 5) - { - UIntT x = f1175; - while (x*2 <= n) x *= 2; - if (x > bestfound) bestfound = x; - while (true) - { - if (x * 3 <= n) x *= 3; - else if (x % 2 == 0) x /= 2; - else break; - - if (x > bestfound) bestfound = x; - } - } - return bestfound; - } - /* returns the largest composite of 2, 3, 5, 7 and 11 which is <= n */ - static POCKETFFT_NOINLINE size_t prev_good_size_cmplx(size_t n) - { - return prev_good_size_cmplx_typed(n); - } - - /* inner workings of prev_good_size_real() */ - template - static POCKETFFT_NOINLINE UIntT prev_good_size_real_typed(UIntT n) - { - static_assert(std::numeric_limits::is_integer && (!std::numeric_limits::is_signed), - "type must be unsigned integer"); - if (n<=6) return n; - if (n>std::numeric_limits::max()/5) - { - // The algorithm below doesn't work for this value, the multiplication can overflow. - if (sizeof(UIntT)(n); - if (res<=std::numeric_limits::max()) - return static_cast(res); - } - // Otherwise, this size is ridiculously large, people shouldn't be computing FFTs this large. - throw std::runtime_error("FFT size is too large."); - } - - UIntT bestfound = 1; - for (UIntT f5 = 1; f5 <= n; f5 *= 5) - { - UIntT x = f5; - while (x*2 <= n) x *= 2; - if (x > bestfound) bestfound = x; - while (true) - { - if (x * 3 <= n) x *= 3; - else if (x % 2 == 0) x /= 2; - else break; - - if (x > bestfound) bestfound = x; - } - } - return bestfound; - } - /* returns the largest composite of 2, 3, 5 which is <= n */ - static POCKETFFT_NOINLINE size_t prev_good_size_real(size_t n) - { - return prev_good_size_real_typed(n); - } - - static size_t prod(const shape_t &shape) - { - size_t res=1; - for (auto sz: shape) - res*=sz; - return res; - } - - static POCKETFFT_NOINLINE void sanity_check(const shape_t &shape, - const stride_t &stride_in, const stride_t &stride_out, bool inplace) - { - auto ndim = shape.size(); - if (ndim<1) throw std::runtime_error("ndim must be >= 1"); - if ((stride_in.size()!=ndim) || (stride_out.size()!=ndim)) - throw std::runtime_error("stride dimension mismatch"); - if (inplace && (stride_in!=stride_out)) - throw std::runtime_error("stride mismatch"); - } - - static POCKETFFT_NOINLINE void sanity_check(const shape_t &shape, - const stride_t &stride_in, const stride_t &stride_out, bool inplace, - const shape_t &axes) - { - sanity_check(shape, stride_in, stride_out, inplace); - auto ndim = shape.size(); - shape_t tmp(ndim,0); - for (auto ax : axes) - { - if (ax>=ndim) throw std::invalid_argument("bad axis number"); - if (++tmp[ax]>1) throw std::invalid_argument("axis specified repeatedly"); - } - } - - static POCKETFFT_NOINLINE void sanity_check(const shape_t &shape, - const stride_t &stride_in, const stride_t &stride_out, bool inplace, - size_t axis) - { - sanity_check(shape, stride_in, stride_out, inplace); - if (axis>=shape.size()) throw std::invalid_argument("bad axis number"); - } - -#ifdef POCKETFFT_NO_MULTITHREADING - static size_t thread_count (size_t /*nthreads*/, const shape_t &/*shape*/, - size_t /*axis*/, size_t /*vlen*/) - { return 1; } -#else - static size_t thread_count (size_t nthreads, const shape_t &shape, - size_t axis, size_t vlen) - { - if (nthreads==1) return 1; - size_t size = prod(shape); - size_t parallel = size / (shape[axis] * vlen); - if (shape[axis] < 1000) - parallel /= 4; - size_t max_threads = nthreads == 0 ? - std::thread::hardware_concurrency() : nthreads; - return std::max(size_t(1), std::min(parallel, max_threads)); - } -#endif - }; - -namespace threading { - -#ifdef POCKETFFT_NO_MULTITHREADING - -constexpr inline size_t thread_id() { return 0; } -constexpr inline size_t num_threads() { return 1; } - -template -void thread_map(size_t /* nthreads */, Func f) - { f(); } - -#else - -inline size_t &thread_id() - { - static thread_local size_t thread_id_=0; - return thread_id_; - } -inline size_t &num_threads() - { - static thread_local size_t num_threads_=1; - return num_threads_; - } -static const size_t max_threads = std::max(1u, std::thread::hardware_concurrency()); - -class latch - { - std::atomic num_left_; - std::mutex mut_; - std::condition_variable completed_; - using lock_t = std::unique_lock; - - public: - explicit latch(size_t n): num_left_(n) {} - - void count_down() - { - lock_t lock(mut_); - if (--num_left_) - return; - completed_.notify_all(); - } - - void wait() - { - lock_t lock(mut_); - completed_.wait(lock, [this]{ return is_ready(); }); - } - bool is_ready() { return num_left_ == 0; } - }; - -template class concurrent_queue - { - std::queue q_; - std::mutex mut_; - std::atomic size_; - using lock_t = std::lock_guard; - - public: - - void push(T val) - { - lock_t lock(mut_); - ++size_; - q_.push(std::move(val)); - } - - bool try_pop(T &val) - { - if (size_ == 0) return false; - lock_t lock(mut_); - // Queue might have been emptied while we acquired the lock - if (q_.empty()) return false; - - val = std::move(q_.front()); - --size_; - q_.pop(); - return true; - } - - bool empty() const { return size_==0; } - }; - -// C++ allocator with support for over-aligned types -template struct aligned_allocator - { - using value_type = T; - template - explicit aligned_allocator(const aligned_allocator&) {} - aligned_allocator() = default; - - T *allocate(size_t n) - { - void* mem = aligned_alloc(alignof(T), n*sizeof(T)); - return static_cast(mem); - } - - void deallocate(T *p, size_t /*n*/) - { aligned_dealloc(p); } - }; - -class thread_pool - { - // A reasonable guess, probably close enough for most hardware - static constexpr size_t cache_line_size = 64; - struct alignas(cache_line_size) worker - { - std::thread thread; - std::condition_variable work_ready; - std::mutex mut; - std::atomic_flag busy_flag = ATOMIC_FLAG_INIT; - std::function work; - - void worker_main( - std::atomic &shutdown_flag, - std::atomic &unscheduled_tasks, - concurrent_queue> &overflow_work) - { - using lock_t_inner = std::unique_lock; - bool expect_work = true; - while (!shutdown_flag || expect_work) - { - std::function local_work; - if (expect_work || unscheduled_tasks == 0) - { - lock_t_inner lock(mut); - // Wait until there is work to be executed - work_ready.wait(lock, [&]{ return (work || shutdown_flag); }); - local_work.swap(work); - expect_work = false; - } - - bool marked_busy = false; - if (local_work) - { - marked_busy = true; - local_work(); - } - - if (!overflow_work.empty()) - { - if (!marked_busy && busy_flag.test_and_set()) - { - expect_work = true; - continue; - } - marked_busy = true; - - while (overflow_work.try_pop(local_work)) - { - --unscheduled_tasks; - local_work(); - } - } - - if (marked_busy) busy_flag.clear(); - } - } - }; - - concurrent_queue> overflow_work_; - std::mutex mut_; - std::vector> workers_; - std::atomic shutdown_; - std::atomic unscheduled_tasks_; - using lock_t = std::lock_guard; - - void create_threads() - { - lock_t lock(mut_); - size_t nthreads=workers_.size(); - for (size_t i=0; ibusy_flag.clear(); - worker_i->work = nullptr; - worker_i->thread = std::thread([worker_i, this] - { - worker_i->worker_main(shutdown_, unscheduled_tasks_, overflow_work_); - }); - } - catch (...) - { - shutdown_locked(); - throw; - } - } - } - - void shutdown_locked() - { - shutdown_ = true; - for (auto &worker_i : workers_) - worker_i.work_ready.notify_all(); - - for (auto &worker_i : workers_) - if (worker_i.thread.joinable()) - worker_i.thread.join(); - } - - public: - explicit thread_pool(size_t nthreads): - workers_(nthreads) - { create_threads(); } - - thread_pool(): thread_pool(max_threads) {} - - ~thread_pool() { shutdown(); } - - void submit(std::function work) - { - lock_t lock(mut_); - if (shutdown_) - throw std::runtime_error("Work item submitted after shutdown"); - - ++unscheduled_tasks_; - - // First check for any idle workers and wake those - for (auto &worker_i : workers_) - if (!worker_i.busy_flag.test_and_set()) - { - --unscheduled_tasks_; - { - lock_t lock_inner(worker_i.mut); - worker_i.work = std::move(work); - } - worker_i.work_ready.notify_one(); - return; - } - - // If no workers were idle, push onto the overflow queue for later - overflow_work_.push(std::move(work)); - } - - void shutdown() - { - lock_t lock(mut_); - shutdown_locked(); - } - - void restart() - { - shutdown_ = false; - create_threads(); - } - }; - -inline thread_pool & get_pool() - { - static thread_pool pool; -#ifdef POCKETFFT_PTHREADS - static std::once_flag f; - std::call_once(f, - []{ - pthread_atfork( - +[]{ get_pool().shutdown(); }, // prepare - +[]{ get_pool().restart(); }, // parent - +[]{ get_pool().restart(); } // child - ); - }); -#endif - - return pool; - } - -/** Map a function f over nthreads */ -template -void thread_map(size_t nthreads, Func f) - { - if (nthreads == 0) - nthreads = max_threads; - - if (nthreads == 1) - { f(); return; } - - auto & pool = get_pool(); - latch counter(nthreads); - std::exception_ptr ex; - std::mutex ex_mut; - for (size_t i=0; i lock(ex_mut); - ex = std::current_exception(); - } - counter.count_down(); - }); - } - counter.wait(); - if (ex) - std::rethrow_exception(ex); - } - -#endif - -} - -// -// complex FFTPACK transforms -// - -template class cfftp - { - private: - struct fctdata - { - size_t fct; - cmplx *tw, *tws; - }; - - size_t length; - arr> mem; - std::vector fact; - - void add_factor(size_t factor) - { fact.push_back({factor, nullptr, nullptr}); } - -template void pass2 (size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const cmplx * POCKETFFT_RESTRICT wa) const - { - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+2*c)]; }; - auto WA = [wa, ido](size_t x, size_t i) - { return wa[i-1+x*(ido-1)]; }; - - if (ido==1) - for (size_t k=0; k(CC(i,0,k)-CC(i,1,k),WA(0,i),CH(i,k,1)); - } - } - } - -#define POCKETFFT_PREP3(idx) \ - T t0 = CC(idx,0,k), t1, t2; \ - PM (t1,t2,CC(idx,1,k),CC(idx,2,k)); \ - CH(idx,k,0)=t0+t1; -#define POCKETFFT_PARTSTEP3a(u1,u2,twr,twi) \ - { \ - T ca=t0+t1*twr; \ - T cb{-t2.i*twi, t2.r*twi}; \ - PM(CH(0,k,u1),CH(0,k,u2),ca,cb) ;\ - } -#define POCKETFFT_PARTSTEP3b(u1,u2,twr,twi) \ - { \ - T ca=t0+t1*twr; \ - T cb{-t2.i*twi, t2.r*twi}; \ - special_mul(ca+cb,WA(u1-1,i),CH(i,k,u1)); \ - special_mul(ca-cb,WA(u2-1,i),CH(i,k,u2)); \ - } -template void pass3 (size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const cmplx * POCKETFFT_RESTRICT wa) const - { - constexpr T0 tw1r=-0.5, - tw1i= (fwd ? -1: 1) * T0(0.8660254037844386467637231707529362L); - - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+3*c)]; }; - auto WA = [wa, ido](size_t x, size_t i) - { return wa[i-1+x*(ido-1)]; }; - - if (ido==1) - for (size_t k=0; k void pass4 (size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const cmplx * POCKETFFT_RESTRICT wa) const - { - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+4*c)]; }; - auto WA = [wa, ido](size_t x, size_t i) - { return wa[i-1+x*(ido-1)]; }; - - if (ido==1) - for (size_t k=0; k(t4); - PM(CH(0,k,0),CH(0,k,2),t2,t3); - PM(CH(0,k,1),CH(0,k,3),t1,t4); - } - else - for (size_t k=0; k(t4); - PM(CH(0,k,0),CH(0,k,2),t2,t3); - PM(CH(0,k,1),CH(0,k,3),t1,t4); - } - for (size_t i=1; i(t4); - CH(i,k,0) = t2+t3; - special_mul(t1+t4,WA(0,i),CH(i,k,1)); - special_mul(t2-t3,WA(1,i),CH(i,k,2)); - special_mul(t1-t4,WA(2,i),CH(i,k,3)); - } - } - } - -#define POCKETFFT_PREP5(idx) \ - T t0 = CC(idx,0,k), t1, t2, t3, t4; \ - PM (t1,t4,CC(idx,1,k),CC(idx,4,k)); \ - PM (t2,t3,CC(idx,2,k),CC(idx,3,k)); \ - CH(idx,k,0).r=t0.r+t1.r+t2.r; \ - CH(idx,k,0).i=t0.i+t1.i+t2.i; - -#define POCKETFFT_PARTSTEP5a(u1,u2,twar,twbr,twai,twbi) \ - { \ - T ca,cb; \ - ca.r=t0.r+twar*t1.r+twbr*t2.r; \ - ca.i=t0.i+twar*t1.i+twbr*t2.i; \ - cb.i=twai*t4.r twbi*t3.r; \ - cb.r=-(twai*t4.i twbi*t3.i); \ - PM(CH(0,k,u1),CH(0,k,u2),ca,cb); \ - } - -#define POCKETFFT_PARTSTEP5b(u1,u2,twar,twbr,twai,twbi) \ - { \ - T ca,cb; \ - ca.r=t0.r+twar*t1.r+twbr*t2.r; \ - ca.i=t0.i+twar*t1.i+twbr*t2.i; \ - cb.i=twai*t4.r twbi*t3.r; \ - cb.r=-(twai*t4.i twbi*t3.i); \ - special_mul(ca+cb,WA(u1-1,i),CH(i,k,u1)); \ - special_mul(ca-cb,WA(u2-1,i),CH(i,k,u2)); \ - } -template void pass5 (size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const cmplx * POCKETFFT_RESTRICT wa) const - { - constexpr T0 tw1r= T0(0.3090169943749474241022934171828191L), - tw1i= (fwd ? -1: 1) * T0(0.9510565162951535721164393333793821L), - tw2r= T0(-0.8090169943749474241022934171828191L), - tw2i= (fwd ? -1: 1) * T0(0.5877852522924731291687059546390728L); - - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+5*c)]; }; - auto WA = [wa, ido](size_t x, size_t i) - { return wa[i-1+x*(ido-1)]; }; - - if (ido==1) - for (size_t k=0; k(da,WA(u1-1,i),CH(i,k,u1)); \ - special_mul(db,WA(u2-1,i),CH(i,k,u2)); \ - } - -template void pass7(size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const cmplx * POCKETFFT_RESTRICT wa) const - { - constexpr T0 tw1r= T0(0.6234898018587335305250048840042398L), - tw1i= (fwd ? -1 : 1) * T0(0.7818314824680298087084445266740578L), - tw2r= T0(-0.2225209339563144042889025644967948L), - tw2i= (fwd ? -1 : 1) * T0(0.9749279121818236070181316829939312L), - tw3r= T0(-0.9009688679024191262361023195074451L), - tw3i= (fwd ? -1 : 1) * T0(0.433883739117558120475768332848359L); - - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+7*c)]; }; - auto WA = [wa, ido](size_t x, size_t i) - { return wa[i-1+x*(ido-1)]; }; - - if (ido==1) - for (size_t k=0; k void ROTX45(T &a) const - { - constexpr T0 hsqt2=T0(0.707106781186547524400844362104849L); - if (fwd) - { auto tmp_=a.r; a.r=hsqt2*(a.r+a.i); a.i=hsqt2*(a.i-tmp_); } - else - { auto tmp_=a.r; a.r=hsqt2*(a.r-a.i); a.i=hsqt2*(a.i+tmp_); } - } -template void ROTX135(T &a) const - { - constexpr T0 hsqt2=T0(0.707106781186547524400844362104849L); - if (fwd) - { auto tmp_=a.r; a.r=hsqt2*(a.i-a.r); a.i=hsqt2*(-tmp_-a.i); } - else - { auto tmp_=a.r; a.r=hsqt2*(-a.r-a.i); a.i=hsqt2*(tmp_-a.i); } - } - -template void pass8 (size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const cmplx * POCKETFFT_RESTRICT wa) const - { - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+8*c)]; }; - auto WA = [wa, ido](size_t x, size_t i) - { return wa[i-1+x*(ido-1)]; }; - - if (ido==1) - for (size_t k=0; k(a3); - - ROTX90(a7); - PMINPLACE(a5,a7); - ROTX45(a5); - ROTX135(a7); - - PM(a0,a4,CC(0,0,k),CC(0,4,k)); - PM(a2,a6,CC(0,2,k),CC(0,6,k)); - PM(CH(0,k,0),CH(0,k,4),a0+a2,a1); - PM(CH(0,k,2),CH(0,k,6),a0-a2,a3); - ROTX90(a6); - PM(CH(0,k,1),CH(0,k,5),a4+a6,a5); - PM(CH(0,k,3),CH(0,k,7),a4-a6,a7); - } - else - for (size_t k=0; k(a3); - - ROTX90(a7); - PMINPLACE(a5,a7); - ROTX45(a5); - ROTX135(a7); - - PM(a0,a4,CC(0,0,k),CC(0,4,k)); - PM(a2,a6,CC(0,2,k),CC(0,6,k)); - PM(CH(0,k,0),CH(0,k,4),a0+a2,a1); - PM(CH(0,k,2),CH(0,k,6),a0-a2,a3); - ROTX90(a6); - PM(CH(0,k,1),CH(0,k,5),a4+a6,a5); - PM(CH(0,k,3),CH(0,k,7),a4-a6,a7); - } - for (size_t i=1; i(a7); - PMINPLACE(a1,a3); - ROTX90(a3); - PMINPLACE(a5,a7); - ROTX45(a5); - ROTX135(a7); - PM(a0,a4,CC(i,0,k),CC(i,4,k)); - PM(a2,a6,CC(i,2,k),CC(i,6,k)); - PMINPLACE(a0,a2); - CH(i,k,0) = a0+a1; - special_mul(a0-a1,WA(3,i),CH(i,k,4)); - special_mul(a2+a3,WA(1,i),CH(i,k,2)); - special_mul(a2-a3,WA(5,i),CH(i,k,6)); - ROTX90(a6); - PMINPLACE(a4,a6); - special_mul(a4+a5,WA(0,i),CH(i,k,1)); - special_mul(a4-a5,WA(4,i),CH(i,k,5)); - special_mul(a6+a7,WA(2,i),CH(i,k,3)); - special_mul(a6-a7,WA(6,i),CH(i,k,7)); - } - } - } - - -#define POCKETFFT_PREP11(idx) \ - T t1 = CC(idx,0,k), t2, t3, t4, t5, t6, t7, t8, t9, t10, t11; \ - PM (t2,t11,CC(idx,1,k),CC(idx,10,k)); \ - PM (t3,t10,CC(idx,2,k),CC(idx, 9,k)); \ - PM (t4,t9 ,CC(idx,3,k),CC(idx, 8,k)); \ - PM (t5,t8 ,CC(idx,4,k),CC(idx, 7,k)); \ - PM (t6,t7 ,CC(idx,5,k),CC(idx, 6,k)); \ - CH(idx,k,0).r=t1.r+t2.r+t3.r+t4.r+t5.r+t6.r; \ - CH(idx,k,0).i=t1.i+t2.i+t3.i+t4.i+t5.i+t6.i; - -#define POCKETFFT_PARTSTEP11a0(u1,u2,x1,x2,x3,x4,x5,y1,y2,y3,y4,y5,out1,out2) \ - { \ - T ca = t1 + t2*x1 + t3*x2 + t4*x3 + t5*x4 +t6*x5, \ - cb; \ - cb.i=y1*t11.r y2*t10.r y3*t9.r y4*t8.r y5*t7.r; \ - cb.r=-(y1*t11.i y2*t10.i y3*t9.i y4*t8.i y5*t7.i ); \ - PM(out1,out2,ca,cb); \ - } -#define POCKETFFT_PARTSTEP11a(u1,u2,x1,x2,x3,x4,x5,y1,y2,y3,y4,y5) \ - POCKETFFT_PARTSTEP11a0(u1,u2,x1,x2,x3,x4,x5,y1,y2,y3,y4,y5,CH(0,k,u1),CH(0,k,u2)) -#define POCKETFFT_PARTSTEP11(u1,u2,x1,x2,x3,x4,x5,y1,y2,y3,y4,y5) \ - { \ - T da,db; \ - POCKETFFT_PARTSTEP11a0(u1,u2,x1,x2,x3,x4,x5,y1,y2,y3,y4,y5,da,db) \ - special_mul(da,WA(u1-1,i),CH(i,k,u1)); \ - special_mul(db,WA(u2-1,i),CH(i,k,u2)); \ - } - -template void pass11 (size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const cmplx * POCKETFFT_RESTRICT wa) const - { - constexpr T0 tw1r= T0(0.8412535328311811688618116489193677L), - tw1i= (fwd ? -1 : 1) * T0(0.5406408174555975821076359543186917L), - tw2r= T0(0.4154150130018864255292741492296232L), - tw2i= (fwd ? -1 : 1) * T0(0.9096319953545183714117153830790285L), - tw3r= T0(-0.1423148382732851404437926686163697L), - tw3i= (fwd ? -1 : 1) * T0(0.9898214418809327323760920377767188L), - tw4r= T0(-0.6548607339452850640569250724662936L), - tw4i= (fwd ? -1 : 1) * T0(0.7557495743542582837740358439723444L), - tw5r= T0(-0.9594929736144973898903680570663277L), - tw5i= (fwd ? -1 : 1) * T0(0.2817325568414296977114179153466169L); - - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+11*c)]; }; - auto WA = [wa, ido](size_t x, size_t i) - { return wa[i-1+x*(ido-1)]; }; - - if (ido==1) - for (size_t k=0; k void passg (size_t ido, size_t ip, - size_t l1, T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const cmplx * POCKETFFT_RESTRICT wa, - const cmplx * POCKETFFT_RESTRICT csarr) const - { - const size_t cdim=ip; - size_t ipph = (ip+1)/2; - size_t idl1 = ido*l1; - - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - auto CC = [cc,ido,cdim](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+cdim*c)]; }; - auto CX = [cc, ido, l1](size_t a, size_t b, size_t c) -> T& - { return cc[a+ido*(b+l1*c)]; }; - auto CX2 = [cc, idl1](size_t a, size_t b) -> T& - { return cc[a+idl1*b]; }; - auto CH2 = [ch, idl1](size_t a, size_t b) -> const T& - { return ch[a+idl1*b]; }; - - arr> wal(ip); - wal[0] = cmplx(1., 0.); - for (size_t i=1; i(csarr[i].r,fwd ? -csarr[i].i : csarr[i].i); - - for (size_t k=0; kip) iwal-=ip; - cmplx xwal=wal[iwal]; - iwal+=l; if (iwal>ip) iwal-=ip; - cmplx xwal2=wal[iwal]; - for (size_t ik=0; ikip) iwal-=ip; - cmplx xwal=wal[iwal]; - for (size_t ik=0; ik(x1,wa[idij],CX(i,k,j)); - idij=(jc-1)*(ido-1)+i-1; - special_mul(x2,wa[idij],CX(i,k,jc)); - } - } - } - } - -template void pass_all(T c[], T0 fct) const - { - if (length==1) { c[0]*=fct; return; } - size_t l1=1; - arr ch(length); - T *p1=c, *p2=ch.data(); - - for(size_t k1=0; k1 (ido, l1, p1, p2, fact[k1].tw); - else if(ip==8) - pass8(ido, l1, p1, p2, fact[k1].tw); - else if(ip==2) - pass2(ido, l1, p1, p2, fact[k1].tw); - else if(ip==3) - pass3 (ido, l1, p1, p2, fact[k1].tw); - else if(ip==5) - pass5 (ido, l1, p1, p2, fact[k1].tw); - else if(ip==7) - pass7 (ido, l1, p1, p2, fact[k1].tw); - else if(ip==11) - pass11 (ido, l1, p1, p2, fact[k1].tw); - else - { - passg(ido, ip, l1, p1, p2, fact[k1].tw, fact[k1].tws); - std::swap(p1,p2); - } - std::swap(p1,p2); - l1=l2; - } - if (p1!=c) - { - if (fct!=1.) - for (size_t i=0; i void exec(T c[], T0 fct, bool fwd) const - { fwd ? pass_all(c, fct) : pass_all(c, fct); } - - private: - POCKETFFT_NOINLINE void factorize() - { - size_t len=length; - while ((len&7)==0) - { add_factor(8); len>>=3; } - while ((len&3)==0) - { add_factor(4); len>>=2; } - if ((len&1)==0) - { - len>>=1; - // factor 2 should be at the front of the factor list - add_factor(2); - std::swap(fact[0].fct, fact.back().fct); - } - for (size_t divisor=3; divisor*divisor<=len; divisor+=2) - while ((len%divisor)==0) - { - add_factor(divisor); - len/=divisor; - } - if (len>1) add_factor(len); - } - - size_t twsize() const - { - size_t twsize=0, l1=1; - for (size_t k=0; k11) - twsize+=ip; - l1*=ip; - } - return twsize; - } - - void comp_twiddle() - { - sincos_2pibyn twiddle(length); - size_t l1=1; - size_t memofs=0; - for (size_t k=0; k11) - { - fact[k].tws=mem.data()+memofs; - memofs+=ip; - for (size_t j=0; j class rfftp - { - private: - struct fctdata - { - size_t fct; - T0 *tw, *tws; - }; - - size_t length; - arr mem; - std::vector fact; - - void add_factor(size_t factor) - { fact.push_back({factor, nullptr, nullptr}); } - -/* (a+ib) = conj(c+id) * (e+if) */ -template inline void MULPM - (T1 &a, T1 &b, T2 c, T2 d, T3 e, T3 f) const - { a=c*e+d*f; b=c*f-d*e; } - -template void radf2 (size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const T0 * POCKETFFT_RESTRICT wa) const - { - auto WA = [wa,ido](size_t x, size_t i) { return wa[i+x*(ido-1)]; }; - auto CC = [cc,ido,l1](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+l1*c)]; }; - auto CH = [ch,ido](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+2*c)]; }; - - for (size_t k=0; k void radf3(size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const T0 * POCKETFFT_RESTRICT wa) const - { - constexpr T0 taur=-0.5, taui=T0(0.8660254037844386467637231707529362L); - - auto WA = [wa,ido](size_t x, size_t i) { return wa[i+x*(ido-1)]; }; - auto CC = [cc,ido,l1](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+l1*c)]; }; - auto CH = [ch,ido](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+3*c)]; }; - - for (size_t k=0; k void radf4(size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const T0 * POCKETFFT_RESTRICT wa) const - { - constexpr T0 hsqt2=T0(0.707106781186547524400844362104849L); - - auto WA = [wa,ido](size_t x, size_t i) { return wa[i+x*(ido-1)]; }; - auto CC = [cc,ido,l1](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+l1*c)]; }; - auto CH = [ch,ido](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+4*c)]; }; - - for (size_t k=0; k void radf5(size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const T0 * POCKETFFT_RESTRICT wa) const - { - constexpr T0 tr11= T0(0.3090169943749474241022934171828191L), - ti11= T0(0.9510565162951535721164393333793821L), - tr12= T0(-0.8090169943749474241022934171828191L), - ti12= T0(0.5877852522924731291687059546390728L); - - auto WA = [wa,ido](size_t x, size_t i) { return wa[i+x*(ido-1)]; }; - auto CC = [cc,ido,l1](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+l1*c)]; }; - auto CH = [ch,ido](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+5*c)]; }; - - for (size_t k=0; k void radfg(size_t ido, size_t ip, size_t l1, - T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const T0 * POCKETFFT_RESTRICT wa, const T0 * POCKETFFT_RESTRICT csarr) const - { - const size_t cdim=ip; - size_t ipph=(ip+1)/2; - size_t idl1 = ido*l1; - - auto CC = [cc,ido,cdim](size_t a, size_t b, size_t c) -> T& - { return cc[a+ido*(b+cdim*c)]; }; - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> const T& - { return ch[a+ido*(b+l1*c)]; }; - auto C1 = [cc,ido,l1] (size_t a, size_t b, size_t c) -> T& - { return cc[a+ido*(b+l1*c)]; }; - auto C2 = [cc,idl1] (size_t a, size_t b) -> T& - { return cc[a+idl1*b]; }; - auto CH2 = [ch,idl1] (size_t a, size_t b) -> T& - { return ch[a+idl1*b]; }; - - if (ido>1) - { - for (size_t j=1, jc=ip-1; j=ip) iang-=ip; - T0 ar1=csarr[2*iang], ai1=csarr[2*iang+1]; - iang+=l; if (iang>=ip) iang-=ip; - T0 ar2=csarr[2*iang], ai2=csarr[2*iang+1]; - iang+=l; if (iang>=ip) iang-=ip; - T0 ar3=csarr[2*iang], ai3=csarr[2*iang+1]; - iang+=l; if (iang>=ip) iang-=ip; - T0 ar4=csarr[2*iang], ai4=csarr[2*iang+1]; - for (size_t ik=0; ik=ip) iang-=ip; - T0 ar1=csarr[2*iang], ai1=csarr[2*iang+1]; - iang+=l; if (iang>=ip) iang-=ip; - T0 ar2=csarr[2*iang], ai2=csarr[2*iang+1]; - for (size_t ik=0; ik=ip) iang-=ip; - T0 ar=csarr[2*iang], ai=csarr[2*iang+1]; - for (size_t ik=0; ik void radb2(size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const T0 * POCKETFFT_RESTRICT wa) const - { - auto WA = [wa,ido](size_t x, size_t i) { return wa[i+x*(ido-1)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+2*c)]; }; - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - - for (size_t k=0; k void radb3(size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const T0 * POCKETFFT_RESTRICT wa) const - { - constexpr T0 taur=-0.5, taui=T0(0.8660254037844386467637231707529362L); - - auto WA = [wa,ido](size_t x, size_t i) { return wa[i+x*(ido-1)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+3*c)]; }; - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - - for (size_t k=0; k void radb4(size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const T0 * POCKETFFT_RESTRICT wa) const - { - constexpr T0 sqrt2=T0(1.414213562373095048801688724209698L); - - auto WA = [wa,ido](size_t x, size_t i) { return wa[i+x*(ido-1)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+4*c)]; }; - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - - for (size_t k=0; k void radb5(size_t ido, size_t l1, - const T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const T0 * POCKETFFT_RESTRICT wa) const - { - constexpr T0 tr11= T0(0.3090169943749474241022934171828191L), - ti11= T0(0.9510565162951535721164393333793821L), - tr12= T0(-0.8090169943749474241022934171828191L), - ti12= T0(0.5877852522924731291687059546390728L); - - auto WA = [wa,ido](size_t x, size_t i) { return wa[i+x*(ido-1)]; }; - auto CC = [cc,ido](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+5*c)]; }; - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - - for (size_t k=0; k void radbg(size_t ido, size_t ip, size_t l1, - T * POCKETFFT_RESTRICT cc, T * POCKETFFT_RESTRICT ch, - const T0 * POCKETFFT_RESTRICT wa, const T0 * POCKETFFT_RESTRICT csarr) const - { - const size_t cdim=ip; - size_t ipph=(ip+1)/ 2; - size_t idl1 = ido*l1; - - auto CC = [cc,ido,cdim](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+cdim*c)]; }; - auto CH = [ch,ido,l1](size_t a, size_t b, size_t c) -> T& - { return ch[a+ido*(b+l1*c)]; }; - auto C1 = [cc,ido,l1](size_t a, size_t b, size_t c) -> const T& - { return cc[a+ido*(b+l1*c)]; }; - auto C2 = [cc,idl1](size_t a, size_t b) -> T& - { return cc[a+idl1*b]; }; - auto CH2 = [ch,idl1](size_t a, size_t b) -> T& - { return ch[a+idl1*b]; }; - - for (size_t k=0; kip) iang-=ip; - T0 ar1=csarr[2*iang], ai1=csarr[2*iang+1]; - iang+=l; if(iang>ip) iang-=ip; - T0 ar2=csarr[2*iang], ai2=csarr[2*iang+1]; - iang+=l; if(iang>ip) iang-=ip; - T0 ar3=csarr[2*iang], ai3=csarr[2*iang+1]; - iang+=l; if(iang>ip) iang-=ip; - T0 ar4=csarr[2*iang], ai4=csarr[2*iang+1]; - for (size_t ik=0; ikip) iang-=ip; - T0 ar1=csarr[2*iang], ai1=csarr[2*iang+1]; - iang+=l; if(iang>ip) iang-=ip; - T0 ar2=csarr[2*iang], ai2=csarr[2*iang+1]; - for (size_t ik=0; ikip) iang-=ip; - T0 war=csarr[2*iang], wai=csarr[2*iang+1]; - for (size_t ik=0; ik void copy_and_norm(T *c, T *p1, T0 fct) const - { - if (p1!=c) - { - if (fct!=1.) - for (size_t i=0; i void exec(T c[], T0 fct, bool r2hc) const - { - if (length==1) { c[0]*=fct; return; } - size_t nf=fact.size(); - arr ch(length); - T *p1=c, *p2=ch.data(); - - if (r2hc) - for(size_t k1=0, l1=length; k1>=2; } - if ((len%2)==0) - { - len>>=1; - // factor 2 should be at the front of the factor list - add_factor(2); - std::swap(fact[0].fct, fact.back().fct); - } - for (size_t divisor=3; divisor*divisor<=len; divisor+=2) - while ((len%divisor)==0) - { - add_factor(divisor); - len/=divisor; - } - if (len>1) add_factor(len); - } - - size_t twsize() const - { - size_t twsz=0, l1=1; - for (size_t k=0; k5) twsz+=2*ip; - l1*=ip; - } - return twsz; - } - - void comp_twiddle() - { - sincos_2pibyn twid(length); - size_t l1=1; - T0 *ptr=mem.data(); - for (size_t k=0; k5) // special factors required by *g functions - { - fact[k].tws=ptr; ptr+=2*ip; - fact[k].tws[0] = 1.; - fact[k].tws[1] = 0.; - for (size_t i=2, ic=2*ip-2; i<=ic; i+=2, ic-=2) - { - fact[k].tws[i ] = twid[i/2*(length/ip)].r; - fact[k].tws[i+1] = twid[i/2*(length/ip)].i; - fact[k].tws[ic] = twid[i/2*(length/ip)].r; - fact[k].tws[ic+1] = -twid[i/2*(length/ip)].i; - } - } - l1*=ip; - } - } - - public: - POCKETFFT_NOINLINE explicit rfftp(size_t length_) - : length(length_) - { - if (length==0) throw std::runtime_error("zero-length FFT requested"); - if (length==1) return; - factorize(); - mem.resize(twsize()); - comp_twiddle(); - } -}; - -// -// complex Bluestein transforms -// - -template class fftblue - { - private: - size_t n, n2; - cfftp plan; - arr> mem; - cmplx *bk, *bkf; - - template void fft(cmplx c[], T0 fct) const - { - arr> akf(n2); - - /* initialize a_k and FFT it */ - for (size_t m=0; m(c[m],bk[m],akf[m]); - auto zero = akf[0]*T0(0); - for (size_t m=n; m(bkf[0]); - for (size_t m=1; m<(n2+1)/2; ++m) - { - akf[m] = akf[m].template special_mul(bkf[m]); - akf[n2-m] = akf[n2-m].template special_mul(bkf[m]); - } - if ((n2&1)==0) - akf[n2/2] = akf[n2/2].template special_mul(bkf[n2/2]); - - /* inverse FFT */ - plan.exec (akf.data(),1.,false); - - /* multiply by b_k */ - for (size_t m=0; m(bk[m])*fct; - } - - public: - POCKETFFT_NOINLINE explicit fftblue(size_t length) - : n(length), n2(util::good_size_cmplx(n*2-1)), plan(n2), mem(n+n2/2+1), - bk(mem.data()), bkf(mem.data()+n) - { - /* initialize b_k */ - sincos_2pibyn tmp(2*n); - bk[0].Set(1, 0); - - size_t coeff=0; - for (size_t m=1; m=2*n) coeff-=2*n; - bk[m] = tmp[coeff]; - } - - /* initialize the zero-padded, Fourier transformed b_k. Add normalisation. */ - arr> tbkf(n2); - T0 xn2 = T0(1)/T0(n2); - tbkf[0] = bk[0]*xn2; - for (size_t m=1; m void exec(cmplx c[], T0 fct, bool fwd) const - { fwd ? fft(c,fct) : fft(c,fct); } - - template void exec_r(T c[], T0 fct, bool fwd) - { - arr> tmp(n); - if (fwd) - { - auto zero = T0(0)*c[0]; - for (size_t m=0; m(tmp.data(),fct); - c[0] = tmp[0].r; - std::copy_n (&tmp[1].r, n-1, &c[1]); - } - else - { - tmp[0].Set(c[0],c[0]*0); - std::copy_n (c+1, n-1, &tmp[1].r); - if ((n&1)==0) tmp[n/2].i=T0(0)*c[0]; - for (size_t m=1; 2*m(tmp.data(),fct); - for (size_t m=0; m class pocketfft_c - { - private: - std::unique_ptr> packplan; - std::unique_ptr> blueplan; - size_t len; - - public: - POCKETFFT_NOINLINE explicit pocketfft_c(size_t length) - : len(length) - { - if (length==0) throw std::runtime_error("zero-length FFT requested"); - size_t tmp = (length<50) ? 0 : util::largest_prime_factor(length); - if (tmp*tmp <= length) - { - packplan=std::unique_ptr>(new cfftp(length)); - return; - } - double comp1 = util::cost_guess(length); - double comp2 = 2*util::cost_guess(util::good_size_cmplx(2*length-1)); - comp2*=1.5; /* fudge factor that appears to give good overall performance */ - if (comp2>(new fftblue(length)); - else - packplan=std::unique_ptr>(new cfftp(length)); - } - - template POCKETFFT_NOINLINE void exec(cmplx c[], T0 fct, bool fwd) const - { packplan ? packplan->exec(c,fct,fwd) : blueplan->exec(c,fct,fwd); } - - size_t length() const { return len; } - }; - -// -// flexible (FFTPACK/Bluestein) real-valued 1D transform -// - -template class pocketfft_r - { - private: - std::unique_ptr> packplan; - std::unique_ptr> blueplan; - size_t len; - - public: - POCKETFFT_NOINLINE explicit pocketfft_r(size_t length) - : len(length) - { - if (length==0) throw std::runtime_error("zero-length FFT requested"); - size_t tmp = (length<50) ? 0 : util::largest_prime_factor(length); - if (tmp*tmp <= length) - { - packplan=std::unique_ptr>(new rfftp(length)); - return; - } - double comp1 = 0.5*util::cost_guess(length); - double comp2 = 2*util::cost_guess(util::good_size_cmplx(2*length-1)); - comp2*=1.5; /* fudge factor that appears to give good overall performance */ - if (comp2>(new fftblue(length)); - else - packplan=std::unique_ptr>(new rfftp(length)); - } - - template POCKETFFT_NOINLINE void exec(T c[], T0 fct, bool fwd) const - { packplan ? packplan->exec(c,fct,fwd) : blueplan->exec_r(c,fct,fwd); } - - size_t length() const { return len; } - }; - - -// -// sine/cosine transforms -// - -template class T_dct1 - { - private: - pocketfft_r fftplan; - - public: - POCKETFFT_NOINLINE explicit T_dct1(size_t length) - : fftplan(2*(length-1)) {} - - template POCKETFFT_NOINLINE void exec(T c[], T0 fct, bool ortho, - int /*type*/, bool /*cosine*/) const - { - constexpr T0 sqrt2=T0(1.414213562373095048801688724209698L); - size_t N=fftplan.length(), n=N/2+1; - if (ortho) - { c[0]*=sqrt2; c[n-1]*=sqrt2; } - arr tmp(N); - tmp[0] = c[0]; - for (size_t i=1; i class T_dst1 - { - private: - pocketfft_r fftplan; - - public: - POCKETFFT_NOINLINE explicit T_dst1(size_t length) - : fftplan(2*(length+1)) {} - - template POCKETFFT_NOINLINE void exec(T c[], T0 fct, - bool /*ortho*/, int /*type*/, bool /*cosine*/) const - { - size_t N=fftplan.length(), n=N/2-1; - arr tmp(N); - tmp[0] = tmp[n+1] = c[0]*0; - for (size_t i=0; i class T_dcst23 - { - private: - pocketfft_r fftplan; - std::vector twiddle; - - public: - POCKETFFT_NOINLINE explicit T_dcst23(size_t length) - : fftplan(length), twiddle(length) - { - sincos_2pibyn tw(4*length); - for (size_t i=0; i POCKETFFT_NOINLINE void exec(T c[], T0 fct, bool ortho, - int type, bool cosine) const - { - constexpr T0 sqrt2=T0(1.414213562373095048801688724209698L); - size_t N=length(); - size_t NS2 = (N+1)/2; - if (type==2) - { - if (!cosine) - for (size_t k=1; k class T_dcst4 - { - private: - size_t N; - std::unique_ptr> fft; - std::unique_ptr> rfft; - arr> C2; - - public: - POCKETFFT_NOINLINE explicit T_dcst4(size_t length) - : N(length), - fft((N&1) ? nullptr : new pocketfft_c(N/2)), - rfft((N&1)? new pocketfft_r(N) : nullptr), - C2((N&1) ? 0 : N/2) - { - if ((N&1)==0) - { - sincos_2pibyn tw(16*N); - for (size_t i=0; i POCKETFFT_NOINLINE void exec(T c[], T0 fct, - bool /*ortho*/, int /*type*/, bool cosine) const - { - size_t n2 = N/2; - if (!cosine) - for (size_t k=0, kc=N-1; k y(N); - { - size_t i=0, m=n2; - for (; mexec(y.data(), fct, true); - { - auto SGN = [](size_t i) - { - constexpr T0 sqrt2=T0(1.414213562373095048801688724209698L); - return (i&2) ? -sqrt2 : sqrt2; - }; - c[n2] = y[0]*SGN(n2+1); - size_t i=0, i1=1, k=1; - for (; k> y(n2); - for(size_t i=0; iexec(y.data(), fct, true); - for(size_t i=0, ic=n2-1; i std::shared_ptr get_plan(size_t length) - { -#if POCKETFFT_CACHE_SIZE==0 - return std::make_shared(length); -#else - constexpr size_t nmax=POCKETFFT_CACHE_SIZE; - static std::array, nmax> cache; - static std::array last_access{{0}}; - static size_t access_counter = 0; - static std::mutex mut; - - auto find_in_cache = [&]() -> std::shared_ptr - { - for (size_t i=0; ilength()==length)) - { - // no need to update if this is already the most recent entry - if (last_access[i]!=access_counter) - { - last_access[i] = ++access_counter; - // Guard against overflow - if (access_counter == 0) - last_access.fill(0); - } - return cache[i]; - } - - return nullptr; - }; - - { - std::lock_guard lock(mut); - auto p = find_in_cache(); - if (p) return p; - } - auto plan = std::make_shared(length); - { - std::lock_guard lock(mut); - auto p = find_in_cache(); - if (p) return p; - - size_t lru = 0; - for (size_t i=1; i class cndarr: public arr_info - { - protected: - const char *d; - - public: - cndarr(const void *data_, const shape_t &shape_, const stride_t &stride_) - : arr_info(shape_, stride_), - d(reinterpret_cast(data_)) {} - const T &operator[](ptrdiff_t ofs) const - { return *reinterpret_cast(d+ofs); } - }; - -template class ndarr: public cndarr - { - public: - ndarr(void *data_, const shape_t &shape_, const stride_t &stride_) - : cndarr::cndarr(const_cast(data_), shape_, stride_) - {} - T &operator[](ptrdiff_t ofs) - { return *reinterpret_cast(const_cast(cndarr::d+ofs)); } - }; - -template class multi_iter - { - private: - shape_t pos; - const arr_info &iarr, &oarr; - ptrdiff_t p_ii, p_i[N], str_i, p_oi, p_o[N], str_o; - size_t idim, rem; - - void advance_i() - { - for (int i_=int(pos.size())-1; i_>=0; --i_) - { - auto i = size_t(i_); - if (i==idim) continue; - p_ii += iarr.stride(i); - p_oi += oarr.stride(i); - if (++pos[i] < iarr.shape(i)) - return; - pos[i] = 0; - p_ii -= ptrdiff_t(iarr.shape(i))*iarr.stride(i); - p_oi -= ptrdiff_t(oarr.shape(i))*oarr.stride(i); - } - } - - public: - multi_iter(const arr_info &iarr_, const arr_info &oarr_, size_t idim_) - : pos(iarr_.ndim(), 0), iarr(iarr_), oarr(oarr_), p_ii(0), - str_i(iarr.stride(idim_)), p_oi(0), str_o(oarr.stride(idim_)), - idim(idim_), rem(iarr.size()/iarr.shape(idim)) - { - auto nshares = threading::num_threads(); - if (nshares==1) return; - if (nshares==0) throw std::runtime_error("can't run with zero threads"); - auto myshare = threading::thread_id(); - if (myshare>=nshares) throw std::runtime_error("impossible share requested"); - size_t nbase = rem/nshares; - size_t additional = rem%nshares; - size_t lo = myshare*nbase + ((myshare=0; --i_) - { - auto i = size_t(i_); - p += arr.stride(i); - if (++pos[i] < arr.shape(i)) - return; - pos[i] = 0; - p -= ptrdiff_t(arr.shape(i))*arr.stride(i); - } - } - ptrdiff_t ofs() const { return p; } - size_t remaining() const { return rem; } - }; - -class rev_iter - { - private: - shape_t pos; - const arr_info &arr; - std::vector rev_axis; - std::vector rev_jump; - size_t last_axis, last_size; - shape_t shp; - ptrdiff_t p, rp; - size_t rem; - - public: - rev_iter(const arr_info &arr_, const shape_t &axes) - : pos(arr_.ndim(), 0), arr(arr_), rev_axis(arr_.ndim(), 0), - rev_jump(arr_.ndim(), 1), p(0), rp(0) - { - for (auto ax: axes) - rev_axis[ax]=1; - last_axis = axes.back(); - last_size = arr.shape(last_axis)/2 + 1; - shp = arr.shape(); - shp[last_axis] = last_size; - rem=1; - for (auto i: shp) - rem *= i; - } - void advance() - { - --rem; - for (int i_=int(pos.size())-1; i_>=0; --i_) - { - auto i = size_t(i_); - p += arr.stride(i); - if (!rev_axis[i]) - rp += arr.stride(i); - else - { - rp -= arr.stride(i); - if (rev_jump[i]) - { - rp += ptrdiff_t(arr.shape(i))*arr.stride(i); - rev_jump[i] = 0; - } - } - if (++pos[i] < shp[i]) - return; - pos[i] = 0; - p -= ptrdiff_t(shp[i])*arr.stride(i); - if (rev_axis[i]) - { - rp -= ptrdiff_t(arr.shape(i)-shp[i])*arr.stride(i); - rev_jump[i] = 1; - } - else - rp -= ptrdiff_t(shp[i])*arr.stride(i); - } - } - ptrdiff_t ofs() const { return p; } - ptrdiff_t rev_ofs() const { return rp; } - size_t remaining() const { return rem; } - }; - -template struct VTYPE {}; -template using vtype_t = typename VTYPE::type; - -#ifndef POCKETFFT_NO_VECTORS -template<> struct VTYPE - { - using type = float __attribute__ ((vector_size (VLEN::val*sizeof(float)))); - }; -template<> struct VTYPE - { - using type = double __attribute__ ((vector_size (VLEN::val*sizeof(double)))); - }; -template<> struct VTYPE - { - using type = long double __attribute__ ((vector_size (VLEN::val*sizeof(long double)))); - }; -#endif - -template arr alloc_tmp(const shape_t &shape, - size_t axsize, size_t elemsize) - { - auto othersize = util::prod(shape)/axsize; - auto tmpsize = axsize*((othersize>=VLEN::val) ? VLEN::val : 1); - return arr(tmpsize*elemsize); - } -template arr alloc_tmp(const shape_t &shape, - const shape_t &axes, size_t elemsize) - { - size_t fullsize=util::prod(shape); - size_t tmpsize=0; - for (size_t i=0; i=VLEN::val) ? VLEN::val : 1); - if (sz>tmpsize) tmpsize=sz; - } - return arr(tmpsize*elemsize); - } - -template void copy_input(const multi_iter &it, - const cndarr> &src, cmplx> *POCKETFFT_RESTRICT dst) - { - for (size_t i=0; i void copy_input(const multi_iter &it, - const cndarr &src, vtype_t *POCKETFFT_RESTRICT dst) - { - for (size_t i=0; i void copy_input(const multi_iter &it, - const cndarr &src, T *POCKETFFT_RESTRICT dst) - { - if (dst == &src[it.iofs(0)]) return; // in-place - for (size_t i=0; i void copy_output(const multi_iter &it, - const cmplx> *POCKETFFT_RESTRICT src, ndarr> &dst) - { - for (size_t i=0; i void copy_output(const multi_iter &it, - const vtype_t *POCKETFFT_RESTRICT src, ndarr &dst) - { - for (size_t i=0; i void copy_output(const multi_iter &it, - const T *POCKETFFT_RESTRICT src, ndarr &dst) - { - if (src == &dst[it.oofs(0)]) return; // in-place - for (size_t i=0; i struct add_vec { using type = vtype_t; }; -template struct add_vec> - { using type = cmplx>; }; -template using add_vec_t = typename add_vec::type; - -template -POCKETFFT_NOINLINE void general_nd(const cndarr &in, ndarr &out, - const shape_t &axes, T0 fct, size_t nthreads, const Exec & exec, - const bool allow_inplace=true) - { - std::shared_ptr plan; - - for (size_t iax=0; iaxlength())) - plan = get_plan(len); - - threading::thread_map( - util::thread_count(nthreads, in.shape(), axes[iax], VLEN::val), - [&] { - constexpr auto vlen = VLEN::val; - auto storage = alloc_tmp(in.shape(), len, sizeof(T)); - const auto &tin(iax==0? in : out); - multi_iter it(tin, out, axes[iax]); -#ifndef POCKETFFT_NO_VECTORS - if (vlen>1) - while (it.remaining()>=vlen) - { - it.advance(vlen); - auto tdatav = reinterpret_cast *>(storage.data()); - exec(it, tin, out, tdatav, *plan, fct); - } -#endif - while (it.remaining()>0) - { - it.advance(1); - auto buf = allow_inplace && it.stride_out() == sizeof(T) ? - &out[it.oofs(0)] : reinterpret_cast(storage.data()); - exec(it, tin, out, buf, *plan, fct); - } - }); // end of parallel region - fct = T0(1); // factor has been applied, use 1 for remaining axes - } - } - -struct ExecC2C - { - bool forward; - - template void operator () ( - const multi_iter &it, const cndarr> &in, - ndarr> &out, T * buf, const pocketfft_c &plan, T0 fct) const - { - copy_input(it, in, buf); - plan.exec(buf, fct, forward); - copy_output(it, buf, out); - } - }; - -template void copy_hartley(const multi_iter &it, - const vtype_t *POCKETFFT_RESTRICT src, ndarr &dst) - { - for (size_t j=0; j void copy_hartley(const multi_iter &it, - const T *POCKETFFT_RESTRICT src, ndarr &dst) - { - dst[it.oofs(0)] = src[0]; - size_t i=1, i1=1, i2=it.length_out()-1; - for (i=1; i void copy_FHT(const multi_iter &it, - const vtype_t *POCKETFFT_RESTRICT src, ndarr &dst) - { - for (size_t j=0; j void copy_FHT(const multi_iter &it, - const T *POCKETFFT_RESTRICT src, ndarr &dst) - { - dst[it.oofs(0)] = src[0]; - size_t i=1, i1=1, i2=it.length_out()-1; - for (i=1; i void operator () ( - const multi_iter &it, const cndarr &in, ndarr &out, - T * buf, const pocketfft_r &plan, T0 fct) const - { - copy_input(it, in, buf); - plan.exec(buf, fct, true); - copy_hartley(it, buf, out); - } - }; -struct ExecFHT - { - template void operator () ( - const multi_iter &it, const cndarr &in, ndarr &out, - T * buf, const pocketfft_r &plan, T0 fct) const - { - copy_input(it, in, buf); - plan.exec(buf, fct, true); - copy_FHT(it, buf, out); - } - }; - -struct ExecDcst - { - bool ortho; - int type; - bool cosine; - - template - void operator () (const multi_iter &it, const cndarr &in, - ndarr &out, T * buf, const Tplan &plan, T0 fct) const - { - copy_input(it, in, buf); - plan.exec(buf, fct, ortho, type, cosine); - copy_output(it, buf, out); - } - }; - -template POCKETFFT_NOINLINE void general_r2c( - const cndarr &in, ndarr> &out, size_t axis, bool forward, T fct, - size_t nthreads) - { - auto plan = get_plan>(in.shape(axis)); - size_t len=in.shape(axis); - threading::thread_map( - util::thread_count(nthreads, in.shape(), axis, VLEN::val), - [&] { - constexpr auto vlen = VLEN::val; - auto storage = alloc_tmp(in.shape(), len, sizeof(T)); - multi_iter it(in, out, axis); -#ifndef POCKETFFT_NO_VECTORS - if (vlen>1) - while (it.remaining()>=vlen) - { - it.advance(vlen); - auto tdatav = reinterpret_cast *>(storage.data()); - copy_input(it, in, tdatav); - plan->exec(tdatav, fct, true); - for (size_t j=0; j0) - { - it.advance(1); - auto tdata = reinterpret_cast(storage.data()); - copy_input(it, in, tdata); - plan->exec(tdata, fct, true); - out[it.oofs(0)].Set(tdata[0]); - size_t i=1, ii=1; - if (forward) - for (; i POCKETFFT_NOINLINE void general_c2r( - const cndarr> &in, ndarr &out, size_t axis, bool forward, T fct, - size_t nthreads) - { - auto plan = get_plan>(out.shape(axis)); - size_t len=out.shape(axis); - threading::thread_map( - util::thread_count(nthreads, in.shape(), axis, VLEN::val), - [&] { - constexpr auto vlen = VLEN::val; - auto storage = alloc_tmp(out.shape(), len, sizeof(T)); - multi_iter it(in, out, axis); -#ifndef POCKETFFT_NO_VECTORS - if (vlen>1) - while (it.remaining()>=vlen) - { - it.advance(vlen); - auto tdatav = reinterpret_cast *>(storage.data()); - for (size_t j=0; jexec(tdatav, fct, false); - copy_output(it, tdatav, out); - } -#endif - while (it.remaining()>0) - { - it.advance(1); - auto tdata = reinterpret_cast(storage.data()); - tdata[0]=in[it.iofs(0)].r; - { - size_t i=1, ii=1; - if (forward) - for (; iexec(tdata, fct, false); - copy_output(it, tdata, out); - } - }); // end of parallel region - } - -struct ExecR2R - { - bool r2h, forward; - - template void operator () ( - const multi_iter &it, const cndarr &in, ndarr &out, T * buf, - const pocketfft_r &plan, T0 fct) const - { - copy_input(it, in, buf); - if ((!r2h) && forward) - for (size_t i=2; i void c2c(const shape_t &shape, const stride_t &stride_in, - const stride_t &stride_out, const shape_t &axes, bool forward, - const std::complex *data_in, std::complex *data_out, T fct, - size_t nthreads=1) - { - if (util::prod(shape)==0) return; - util::sanity_check(shape, stride_in, stride_out, data_in==data_out, axes); - cndarr> ain(data_in, shape, stride_in); - ndarr> aout(data_out, shape, stride_out); - general_nd>(ain, aout, axes, fct, nthreads, ExecC2C{forward}); - } - -template void dct(const shape_t &shape, - const stride_t &stride_in, const stride_t &stride_out, const shape_t &axes, - int type, const T *data_in, T *data_out, T fct, bool ortho, size_t nthreads=1) - { - if ((type<1) || (type>4)) throw std::invalid_argument("invalid DCT type"); - if (util::prod(shape)==0) return; - util::sanity_check(shape, stride_in, stride_out, data_in==data_out, axes); - cndarr ain(data_in, shape, stride_in); - ndarr aout(data_out, shape, stride_out); - const ExecDcst exec{ortho, type, true}; - if (type==1) - general_nd>(ain, aout, axes, fct, nthreads, exec); - else if (type==4) - general_nd>(ain, aout, axes, fct, nthreads, exec); - else - general_nd>(ain, aout, axes, fct, nthreads, exec); - } - -template void dst(const shape_t &shape, - const stride_t &stride_in, const stride_t &stride_out, const shape_t &axes, - int type, const T *data_in, T *data_out, T fct, bool ortho, size_t nthreads=1) - { - if ((type<1) || (type>4)) throw std::invalid_argument("invalid DST type"); - if (util::prod(shape)==0) return; - util::sanity_check(shape, stride_in, stride_out, data_in==data_out, axes); - cndarr ain(data_in, shape, stride_in); - ndarr aout(data_out, shape, stride_out); - const ExecDcst exec{ortho, type, false}; - if (type==1) - general_nd>(ain, aout, axes, fct, nthreads, exec); - else if (type==4) - general_nd>(ain, aout, axes, fct, nthreads, exec); - else - general_nd>(ain, aout, axes, fct, nthreads, exec); - } - -template void r2c(const shape_t &shape_in, - const stride_t &stride_in, const stride_t &stride_out, size_t axis, - bool forward, const T *data_in, std::complex *data_out, T fct, - size_t nthreads=1) - { - if (util::prod(shape_in)==0) return; - util::sanity_check(shape_in, stride_in, stride_out, false, axis); - cndarr ain(data_in, shape_in, stride_in); - shape_t shape_out(shape_in); - shape_out[axis] = shape_in[axis]/2 + 1; - ndarr> aout(data_out, shape_out, stride_out); - general_r2c(ain, aout, axis, forward, fct, nthreads); - } - -template void r2c(const shape_t &shape_in, - const stride_t &stride_in, const stride_t &stride_out, const shape_t &axes, - bool forward, const T *data_in, std::complex *data_out, T fct, - size_t nthreads=1) - { - if (util::prod(shape_in)==0) return; - util::sanity_check(shape_in, stride_in, stride_out, false, axes); - r2c(shape_in, stride_in, stride_out, axes.back(), forward, data_in, data_out, - fct, nthreads); - if (axes.size()==1) return; - - shape_t shape_out(shape_in); - shape_out[axes.back()] = shape_in[axes.back()]/2 + 1; - auto newaxes = shape_t{axes.begin(), --axes.end()}; - c2c(shape_out, stride_out, stride_out, newaxes, forward, data_out, data_out, - T(1), nthreads); - } - -template void c2r(const shape_t &shape_out, - const stride_t &stride_in, const stride_t &stride_out, size_t axis, - bool forward, const std::complex *data_in, T *data_out, T fct, - size_t nthreads=1) - { - if (util::prod(shape_out)==0) return; - util::sanity_check(shape_out, stride_in, stride_out, false, axis); - shape_t shape_in(shape_out); - shape_in[axis] = shape_out[axis]/2 + 1; - cndarr> ain(data_in, shape_in, stride_in); - ndarr aout(data_out, shape_out, stride_out); - general_c2r(ain, aout, axis, forward, fct, nthreads); - } - -template void c2r(const shape_t &shape_out, - const stride_t &stride_in, const stride_t &stride_out, const shape_t &axes, - bool forward, const std::complex *data_in, T *data_out, T fct, - size_t nthreads=1) - { - if (util::prod(shape_out)==0) return; - if (axes.size()==1) - return c2r(shape_out, stride_in, stride_out, axes[0], forward, - data_in, data_out, fct, nthreads); - util::sanity_check(shape_out, stride_in, stride_out, false, axes); - auto shape_in = shape_out; - shape_in[axes.back()] = shape_out[axes.back()]/2 + 1; - auto nval = util::prod(shape_in); - stride_t stride_inter(shape_in.size()); - stride_inter.back() = sizeof(cmplx); - for (int i=int(shape_in.size())-2; i>=0; --i) - stride_inter[size_t(i)] = - stride_inter[size_t(i+1)]*ptrdiff_t(shape_in[size_t(i+1)]); - arr> tmp(nval); - auto newaxes = shape_t{axes.begin(), --axes.end()}; - c2c(shape_in, stride_in, stride_inter, newaxes, forward, data_in, tmp.data(), - T(1), nthreads); - c2r(shape_out, stride_inter, stride_out, axes.back(), forward, - tmp.data(), data_out, fct, nthreads); - } - -template void r2r_fftpack(const shape_t &shape, - const stride_t &stride_in, const stride_t &stride_out, const shape_t &axes, - bool real2hermitian, bool forward, const T *data_in, T *data_out, T fct, - size_t nthreads=1) - { - if (util::prod(shape)==0) return; - util::sanity_check(shape, stride_in, stride_out, data_in==data_out, axes); - cndarr ain(data_in, shape, stride_in); - ndarr aout(data_out, shape, stride_out); - general_nd>(ain, aout, axes, fct, nthreads, - ExecR2R{real2hermitian, forward}); - } - -template void r2r_separable_hartley(const shape_t &shape, - const stride_t &stride_in, const stride_t &stride_out, const shape_t &axes, - const T *data_in, T *data_out, T fct, size_t nthreads=1) - { - if (util::prod(shape)==0) return; - util::sanity_check(shape, stride_in, stride_out, data_in==data_out, axes); - cndarr ain(data_in, shape, stride_in); - ndarr aout(data_out, shape, stride_out); - general_nd>(ain, aout, axes, fct, nthreads, ExecHartley{}, - false); - } - -template void r2r_genuine_hartley(const shape_t &shape, - const stride_t &stride_in, const stride_t &stride_out, const shape_t &axes, - const T *data_in, T *data_out, T fct, size_t nthreads=1) - { - if (util::prod(shape)==0) return; - if (axes.size()==1) - return r2r_separable_hartley(shape, stride_in, stride_out, axes, data_in, - data_out, fct, nthreads); - util::sanity_check(shape, stride_in, stride_out, data_in==data_out, axes); - shape_t tshp(shape); - tshp[axes.back()] = tshp[axes.back()]/2+1; - arr> tdata(util::prod(tshp)); - stride_t tstride(shape.size()); - tstride.back()=sizeof(std::complex); - for (size_t i=tstride.size()-1; i>0; --i) - tstride[i-1]=tstride[i]*ptrdiff_t(tshp[i]); - r2c(shape, stride_in, tstride, axes, true, data_in, tdata.data(), fct, nthreads); - cndarr> atmp(tdata.data(), tshp, tstride); - ndarr aout(data_out, shape, stride_out); - simple_iter iin(atmp); - rev_iter iout(aout, axes); - while(iin.remaining()>0) - { - auto v = atmp[iin.ofs()]; - aout[iout.ofs()] = v.r+v.i; - aout[iout.rev_ofs()] = v.r-v.i; - iin.advance(); iout.advance(); - } - } - -template void r2r_separable_fht(const shape_t &shape, - const stride_t &stride_in, const stride_t &stride_out, const shape_t &axes, - const T *data_in, T *data_out, T fct, size_t nthreads=1) - { - if (util::prod(shape)==0) return; - util::sanity_check(shape, stride_in, stride_out, data_in==data_out, axes); - cndarr ain(data_in, shape, stride_in); - ndarr aout(data_out, shape, stride_out); - general_nd>(ain, aout, axes, fct, nthreads, ExecFHT{}, - false); - } - -template void r2r_genuine_fht(const shape_t &shape, - const stride_t &stride_in, const stride_t &stride_out, const shape_t &axes, - const T *data_in, T *data_out, T fct, size_t nthreads=1) - { - if (util::prod(shape)==0) return; - if (axes.size()==1) - return r2r_separable_fht(shape, stride_in, stride_out, axes, data_in, - data_out, fct, nthreads); - util::sanity_check(shape, stride_in, stride_out, data_in==data_out, axes); - shape_t tshp(shape); - tshp[axes.back()] = tshp[axes.back()]/2+1; - arr> tdata(util::prod(tshp)); - stride_t tstride(shape.size()); - tstride.back()=sizeof(std::complex); - for (size_t i=tstride.size()-1; i>0; --i) - tstride[i-1]=tstride[i]*ptrdiff_t(tshp[i]); - r2c(shape, stride_in, tstride, axes, true, data_in, tdata.data(), fct, nthreads); - cndarr> atmp(tdata.data(), tshp, tstride); - ndarr aout(data_out, shape, stride_out); - simple_iter iin(atmp); - rev_iter iout(aout, axes); - while(iin.remaining()>0) - { - auto v = atmp[iin.ofs()]; - aout[iout.ofs()] = v.r-v.i; - aout[iout.rev_ofs()] = v.r+v.i; - iin.advance(); iout.advance(); - } - } - -} // namespace detail - -using detail::FORWARD; -using detail::BACKWARD; -using detail::shape_t; -using detail::stride_t; -using detail::c2c; -using detail::c2r; -using detail::r2c; -using detail::r2r_fftpack; -using detail::r2r_separable_hartley; -using detail::r2r_genuine_hartley; -using detail::r2r_separable_fht; -using detail::r2r_genuine_fht; -using detail::dct; -using detail::dst; - -} // namespace pocketfft - -#undef POCKETFFT_NOINLINE -#undef POCKETFFT_RESTRICT - -#endif // POCKETFFT_HDRONLY_H diff --git a/data_acq_and_processing/processing/data_processor/include/data_processor.hpp b/data_acq_and_processing/processing/data_processor/include/data_processor.hpp index 813866e..777094d 100644 --- a/data_acq_and_processing/processing/data_processor/include/data_processor.hpp +++ b/data_acq_and_processing/processing/data_processor/include/data_processor.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -40,11 +41,10 @@ class DataProcessor { [[nodiscard]] auto resolve_processor(const ProcessingLiveConfig& live_config) -> ProcessorInterface&; [[nodiscard]] auto should_replay_entire_history(const ProcessingLiveConfig& live_config) const -> bool; - // Merge live config with the latest socket-supplied speed (if any and if - // not suppressed by `ignore_socket_speed`). The returned config is what - // actually drives processing for this tick. - [[nodiscard]] auto resolve_effective_live_config(const ProcessingLiveConfig& live_config) const - -> ProcessingLiveConfig; + // Speed streamed by a locator client (`vlc`), which then replaces + // `gpr_speed_m_s`; nullopt when `ignore_socket_speed` is set or no fresh + // value exists, so the speed from the live config stays in force. + [[nodiscard]] auto socket_speed(const ProcessingLiveConfig& live_config) const -> std::optional; // Convert one processed collection into a locator filter spec, derived // from the live config and current processor mode. diff --git a/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp b/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp index a0dfe2b..a21323a 100644 --- a/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp +++ b/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp @@ -90,51 +90,17 @@ struct ProcessingLiveConfig { float gpr_visible_x_max_m = 2.0F; float gpr_visible_z_min_m = 0.0F; float gpr_visible_z_max_m = 14.0F; - // 3D SAR (sar3d processor) parameters; defaults mirror the reference - // notebook Multi_antenna_3D_clean_v1.ipynb (cells 0/4). Frequency band, - // depth gate, antenna selection and motion (speed, look angle, direction, - // phase correction) are shared with the gpr_* fields. + // 3D SAR (sar3d processor) parameters, named after the reference notebook + // Multi_antenna_3D_clean_v1.ipynb (SAR_FRAME_RADIUS, BP_GROUND_DEPTH_M, ...). + // The band, depth gate, antenna selection, speed and look angle are the + // shared gpr_* fields above. std::uint32_t sar3d_frame_radius = 0U; - float sar3d_edge_frame_weight = 0.8F; - // Frame pitch for ground coordinates and inter-frame offsets. Override - // disabled -> the pitch follows gpr_look_angle_deg (notebook - // BP_GROUND_PITCH_DEG = None). - bool sar3d_pitch_override_enabled = false; - float sar3d_pitch_deg = 17.5F; - // Height of the LOWER antenna row above the ground; scene Y=0 is the ground. - float sar3d_ground_depth_m = 1.25F; - // Scene grid bounds (metres): X across, Y height above the ground, Z along - // the ground away from the radar. Voxels whose radar depth falls outside - // [gpr_min_depth, gpr_max_depth] are zeroed by the mask regardless. - // Doubles, not floats: the parity tool feeds the notebook-derived bounds - // through here and a float roundtrip shifts the grid by ~1e-7 m, flipping - // knife-edge voxels of the depth mask against the reference volumes. - double sar3d_x_min_m = -2.0; - double sar3d_x_max_m = 2.0; - double sar3d_y_min_m = -0.5; - double sar3d_y_max_m = 2.0; - double sar3d_z_min_m = 0.0; - double sar3d_z_max_m = 8.0; - float sar3d_voxel_x_m = 0.025F; - float sar3d_voxel_y_m = 0.02F; - float sar3d_voxel_z_m = 0.02F; - // Volume finalization: normalization ignores this much of the grid's Y - // edges (constant-range ridge), then Gaussian smoothing over (Z, X). - float sar3d_norm_edge_margin_m = 0.3F; - float sar3d_smooth_sigma = 1.5F; - // Geometric spreading compensation 1/(Rtx*Rrx)^power, capped. + // Edge of the cubic voxel: the notebook's SAR_VOXEL_M with one value for X, Y, Z. + float sar3d_voxel_m = 0.02F; + float sar3d_bp_ground_depth_m = 1.25F; float sar3d_comp_range_power = 0.3F; - // Fallback frame segmentation when per-trace capture stamps are absent. - std::uint32_t sar3d_tx_per_segment = 2U; bool sar3d_use_compensation = true; - bool sar3d_normalize_pairs = true; - // Debug: when non-empty, the processor dumps every finished volume to this - // directory (raw float32 + json header). Used by the parity tool only. - std::string sar3d_volume_dump_dir{}; - // Bottom-view projection angle around the vertical Y axis, degrees: - // 0 = front view (X horizontal), 90 = side view (Z horizontal), - // intermediate values project along the rotated horizontal direction. - float sar3d_bottom_view_deg = 0.0F; + bool sar3d_normalize_each_frame = true; // When true, the data_processor ignores socket-supplied `vlc` updates and // keeps using `gpr_speed_m_s` from this file. Mirrored from the GUI's // "ignore socket speed" checkbox. diff --git a/data_acq_and_processing/processing/data_processor/src/data_processor.cpp b/data_acq_and_processing/processing/data_processor/src/data_processor.cpp index b963992..20f8663 100644 --- a/data_acq_and_processing/processing/data_processor/src/data_processor.cpp +++ b/data_acq_and_processing/processing/data_processor/src/data_processor.cpp @@ -73,19 +73,16 @@ void DataProcessor::run(const std::atomic& stop_requested) { while (!stop_requested.load(std::memory_order_relaxed)) { try { - const auto live_config_raw = live_config_loader_.refresh_if_needed(); - const auto live_config = resolve_effective_live_config(live_config_raw); + // The socket speed is read once per tick: it both drives this tick's + // processing and, when it changed, triggers a reprocess on its own, + // even when the live revision is unchanged. + auto live_config = live_config_loader_.refresh_if_needed(); + const std::optional current_socket_speed = socket_speed(live_config); + if (current_socket_speed.has_value()) { + live_config.gpr_speed_m_s = static_cast(*current_socket_speed); + } const auto live_revision = live_config_loader_.revision(); auto& processor = resolve_processor(live_config); - - // The effective config folds in latest_socket_speed(); a change there is - // a reprocess trigger on its own, even when the live revision is unchanged. - // Mirror the gating in resolve_effective_live_config(): only consider the - // socket speed when it is actually applied to the effective config. - const std::optional current_socket_speed = - (live_config_raw.ignore_socket_speed || locator_server_ == nullptr) - ? std::nullopt - : locator_server_->latest_socket_speed(); const bool socket_speed_dirty = current_socket_speed != last_reprocessed_socket_speed; if (live_revision != last_replayed_revision || socket_speed_dirty) { @@ -213,18 +210,11 @@ auto DataProcessor::should_replay_entire_history(const ProcessingLiveConfig& liv return requested_mode == "bscan"; } -auto DataProcessor::resolve_effective_live_config(const ProcessingLiveConfig& live_config) const - -> ProcessingLiveConfig { +auto DataProcessor::socket_speed(const ProcessingLiveConfig& live_config) const -> std::optional { if (live_config.ignore_socket_speed || locator_server_ == nullptr) { - return live_config; + return std::nullopt; } - const auto socket_speed = locator_server_->latest_socket_speed(); - if (!socket_speed.has_value()) { - return live_config; - } - ProcessingLiveConfig effective = live_config; - effective.gpr_speed_m_s = static_cast(*socket_speed); - return effective; + return locator_server_->latest_socket_speed(); } auto DataProcessor::build_locator_filter(const ProcessingLiveConfig& live_config) const diff --git a/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp b/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp index eca2b23..2ef9432 100644 --- a/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp +++ b/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp @@ -390,16 +390,9 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s config.sar3d_frame_radius = parse_u32_number(*found, "processing.sar3d_frame_radius"); } for (const auto& [key, target] : { - std::pair{"sar3d_edge_frame_weight", &config.sar3d_edge_frame_weight}, - std::pair{"sar3d_pitch_deg", &config.sar3d_pitch_deg}, - std::pair{"sar3d_ground_depth_m", &config.sar3d_ground_depth_m}, - std::pair{"sar3d_voxel_x_m", &config.sar3d_voxel_x_m}, - std::pair{"sar3d_voxel_y_m", &config.sar3d_voxel_y_m}, - std::pair{"sar3d_voxel_z_m", &config.sar3d_voxel_z_m}, - std::pair{"sar3d_norm_edge_margin_m", &config.sar3d_norm_edge_margin_m}, - std::pair{"sar3d_smooth_sigma", &config.sar3d_smooth_sigma}, + std::pair{"sar3d_voxel_m", &config.sar3d_voxel_m}, + std::pair{"sar3d_bp_ground_depth_m", &config.sar3d_bp_ground_depth_m}, std::pair{"sar3d_comp_range_power", &config.sar3d_comp_range_power}, - std::pair{"sar3d_bottom_view_deg", &config.sar3d_bottom_view_deg}, }) { if (const auto found = root.find(key); found != root.end()) { if (!found->is_number()) { @@ -409,30 +402,8 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s } } for (const auto& [key, target] : { - std::pair{"sar3d_x_min_m", &config.sar3d_x_min_m}, - std::pair{"sar3d_x_max_m", &config.sar3d_x_max_m}, - std::pair{"sar3d_y_min_m", &config.sar3d_y_min_m}, - std::pair{"sar3d_y_max_m", &config.sar3d_y_max_m}, - std::pair{"sar3d_z_min_m", &config.sar3d_z_min_m}, - std::pair{"sar3d_z_max_m", &config.sar3d_z_max_m}, - }) { - if (const auto found = root.find(key); found != root.end()) { - if (!found->is_number()) { - throw std::runtime_error(std::string("processing.") + key + " must be number"); - } - *target = found->get(); - } - } - if (const auto found = root.find("sar3d_tx_per_segment"); found != root.end()) { - config.sar3d_tx_per_segment = parse_u32_number(*found, "processing.sar3d_tx_per_segment"); - if (config.sar3d_tx_per_segment == 0U) { - throw std::runtime_error("processing.sar3d_tx_per_segment must be >= 1"); - } - } - for (const auto& [key, target] : { - std::pair{"sar3d_pitch_override_enabled", &config.sar3d_pitch_override_enabled}, std::pair{"sar3d_use_compensation", &config.sar3d_use_compensation}, - std::pair{"sar3d_normalize_pairs", &config.sar3d_normalize_pairs}, + std::pair{"sar3d_normalize_each_frame", &config.sar3d_normalize_each_frame}, }) { if (const auto found = root.find(key); found != root.end()) { if (!found->is_boolean()) { diff --git a/data_acq_and_processing/processing/processors/include/sar3d_processor.hpp b/data_acq_and_processing/processing/processors/include/sar3d_processor.hpp index 09bb6f3..a203d31 100644 --- a/data_acq_and_processing/processing/processors/include/sar3d_processor.hpp +++ b/data_acq_and_processing/processing/processors/include/sar3d_processor.hpp @@ -8,9 +8,11 @@ namespace radar::processing { -// 3D SAR over a synthesized aperture, computed by the external gpr_cpp -// CUDA/NVRTC library (VNA_AlGo). Emits 2D max-projections of the volume -// (side XZ and top XY views) as ImageF32 collection payloads. +// 3D SAR of the reference notebook Multi_antenna_3D_clean_v1.ipynb on an NVIDIA +// GPU. Publishes max-projections of the normalized volume as ImageF32 payloads: +// sar3d_front (X-Y), sar3d_side (Z-Y), sar3d_top (X-Z) and sar3d_oblique (the +// notebook's 3D scene view, axes in the plane of sar3d_oblique_basis), plus +// sar3d_peak = [level, X, Y, Z] of the volume maximum. class Sar3dProcessor final : public ProcessorInterface { public: Sar3dProcessor(); @@ -25,8 +27,6 @@ class Sar3dProcessor final : public ProcessorInterface { ) -> ipc::ResultCollection override; private: - // Pimpl keeps gpr/CUDA headers (cuda.h, nvrtc.h) out of the shared - // processor include path; only sar3d_processor.cpp needs them. struct State; std::unique_ptr state_; }; diff --git a/data_acq_and_processing/processing/processors/src/sar3d_gpu.cpp b/data_acq_and_processing/processing/processors/src/sar3d_gpu.cpp new file mode 100644 index 0000000..40f7654 --- /dev/null +++ b/data_acq_and_processing/processing/processors/src/sar3d_gpu.cpp @@ -0,0 +1,471 @@ +#include "sar3d_gpu.hpp" + +#include + +#ifdef SAR3D_WITH_CUDA +#include +#include + +#include +#include +#include +#include +#include +#endif + +namespace radar::processing::sar3d { + +#ifdef SAR3D_WITH_CUDA +namespace { + +// Compiled at run time by NVRTC for the installed GPU, so the build needs no +// nvcc. Arithmetic is float32 apart from a few per-voxel terms: FP64 runs at +// 1/32-1/64 of the FP32 rate on consumer GPUs, and float32 already keeps the +// A-scan sample position to ~1e-3 of a sample of the 8x oversampled A-scan. +// Volumes are [y][z][x] with x fastest; their sizes stay below 2^31 (checked on +// the host), so the hot index arithmetic is 32-bit. +constexpr const char* kKernelSource = R"CUDA( +struct Contribution { + float tx[3]; + float rx[3]; + float weight; + float inv_reference_ranges; +}; + +// numpy.fft.ifft of a band spectrum placed at bins first_bin.. of a size-N +// spectrum (N a power of two), summed directly: the band is a few hundred +// bins of thousands. One thread per (pair, sample); the phase index is exact. +extern "C" __global__ void inverse_dft( + const float2* __restrict__ bands, const int points, const int first_bin, + const int size, const int items, float2* __restrict__ ascans) +{ + const int item = blockIdx.x * blockDim.x + threadIdx.x; + if (item >= items) return; + const unsigned k = (unsigned)(item % size); + const unsigned mask = (unsigned)size - 1u; + const float2* band = bands + (item / size) * points; + unsigned phase = (unsigned)(((unsigned long long)first_bin * k) & mask); + const float turns_per_index = 2.0f / (float)size; + float re = 0.0f, im = 0.0f; + for (int n = 0; n < points; ++n) { + float s, c; + sincospif((float)phase * turns_per_index, &s, &c); + const float2 b = band[n]; + re += b.x * c - b.y * s; + im += b.x * s + b.y * c; + phase = (phase + k) & mask; + } + ascans[item] = make_float2(re / (float)size, im / (float)size); +} + +// Coherent backprojection, one thread per voxel: |sum w*comp*h| / sum w over the +// valid (frame, pair) terms (notebook sar_frame_contribution and the first line +// of sar_finish_volume). +extern "C" __global__ void backproject( + const float2* __restrict__ ascans, const int samples, + const Contribution* __restrict__ contributions, const int count, + const double* __restrict__ gx, const double* __restrict__ gy, const double* __restrict__ gz, + const int nx, const int nz, const int voxels, + const double cos_p, const double sin_p, const double ground_depth, + const double min_depth, const double max_depth, + const float samples_per_metre, const int use_compensation, + const float comp_power, const float comp_max, + float* __restrict__ volume) +{ + const int voxel = blockIdx.x * blockDim.x + threadIdx.x; + if (voxel >= voxels) return; + const int row = voxel / nx; + // Scene -> radar axes (sar_scene_to_radar_yz). The depth gate is decided in + // double so voxels on its edge are classified like in the reference. + const double y0 = gy[row / nz] - ground_depth; + const double zs = gz[row % nz]; + const double depth = -y0 * sin_p + zs * cos_p; + if (depth < min_depth || depth > max_depth) { + volume[voxel] = 0.0f; + return; + } + const float px = (float)gx[voxel - row * nx]; + const float py = (float)(y0 * cos_p + zs * sin_p); + const float pz = (float)depth; + const float last = (float)(samples - 1); + float re = 0.0f, im = 0.0f, weights = 0.0f; + for (int c = 0; c < count; ++c) { + const Contribution k = contributions[c]; + const float dxt = px - k.tx[0], dyt = py - k.tx[1], dzt = pz - k.tx[2]; + const float dxr = px - k.rx[0], dyr = py - k.rx[1], dzr = pz - k.rx[2]; + const float rt = sqrtf(dxt * dxt + dyt * dyt + dzt * dzt); + const float rr = sqrtf(dxr * dxr + dyr * dyr + dzr * dzr); + const float position = (rt + rr) * samples_per_metre; + if (position > last) continue; + const int i = min((int)position, samples - 2); + const float frac = position - (float)i; + const float2* row_ascan = ascans + (long long)c * samples; + const float2 a = row_ascan[i]; + const float2 b = row_ascan[i + 1]; + float w = k.weight; + if (use_compensation) { + // sar_compensation_weight: 1 / (geo / geo_ref)^power, clipped. + w *= fminf(__powf(rt * rr * k.inv_reference_ranges, comp_power), comp_max); + } + re += w * (a.x + frac * (b.x - a.x)); + im += w * (a.y + frac * (b.y - a.y)); + weights += k.weight; + } + volume[voxel] = sqrtf(re * re + im * im) / (weights + 1e-12f); +} + +// One pass of scipy.ndimage.gaussian_filter1d (mode 'reflect') along Z or X. +extern "C" __global__ void smooth( + const float* __restrict__ src, float* __restrict__ dst, + const float* __restrict__ taps, const int radius, + const int nx, const int nz, const int voxels, const int along_z) +{ + const int voxel = blockIdx.x * blockDim.x + threadIdx.x; + if (voxel >= voxels) return; + const int length = along_z ? nz : nx; + const int index = along_z ? (voxel / nx) % nz : voxel % nx; + const int stride = along_z ? nx : 1; + float sum = 0.0f; + for (int k = -radius; k <= radius; ++k) { + int j = index + k; + while (j < 0 || j >= length) j = j < 0 ? -j - 1 : 2 * length - j - 1; // d c b a | a b c d | d c b a + sum += taps[k + radius] * src[voxel + (j - index) * stride]; + } + dst[voxel] = sum; +} + +// dst[o] = max over r of src[(o / inner) * outer_stride + (o % inner) * inner_stride + r * reduce_stride]. +extern "C" __global__ void max_project( + const float* __restrict__ src, float* __restrict__ dst, const int outputs, const int inner, + const int outer_stride, const int inner_stride, const int reduce, const int reduce_stride) +{ + const int o = blockIdx.x * blockDim.x + threadIdx.x; + if (o >= outputs) return; + const float* p = src + (o / inner) * outer_stride + (o % inner) * inner_stride; + float m = 0.0f; + for (int r = 0; r < reduce; ++r) m = fmaxf(m, p[r * reduce_stride]); + dst[o] = m; +} + +// Orthographic max projection. The volume is non-negative, so float bit +// patterns order like the values and an integer atomicMax does the job. The +// pixel is found in double, so it does not hinge on float rounding. +extern "C" __global__ void project_oblique( + const float* __restrict__ volume, + const double* __restrict__ gx, const double* __restrict__ gy, const double* __restrict__ gz, + const int nx, const int nz, const int voxels, + const double ux, const double uy, const double uz, const double vx, const double vy, const double vz, + const double u0, const double v0, const double inv_pixel, const int nu, const int nv, + unsigned int* __restrict__ image) +{ + const int voxel = blockIdx.x * blockDim.x + threadIdx.x; + if (voxel >= voxels) return; + const float value = volume[voxel]; + if (!(value > 0.0f)) return; + const int row = voxel / nx; + const double x = gx[voxel - row * nx]; + const double y = gy[row / nz]; + const double z = gz[row % nz]; + const int pu = __double2int_rn((ux * x + uy * y + uz * z - u0) * inv_pixel); + const int pv = __double2int_rn((vx * x + vy * y + vz * z - v0) * inv_pixel); + if (pu < 0 || pu >= nu || pv < 0 || pv >= nv) return; + atomicMax(image + pv * nu + pu, __float_as_uint(value)); +} +)CUDA"; + +constexpr int kBlockSize = 256; + +void check(CUresult result, const char* what) { + if (result != CUDA_SUCCESS) { + const char* text = nullptr; + cuGetErrorString(result, &text); + throw std::runtime_error(std::string{"CUDA "} + what + ": " + (text != nullptr ? text : "unknown error")); + } +} + +void check(nvrtcResult result, const char* what) { + if (result != NVRTC_SUCCESS) { + throw std::runtime_error(std::string{"NVRTC "} + what + ": " + nvrtcGetErrorString(result)); + } +} + +[[nodiscard]] auto checked_int(std::size_t value, const char* what) -> int { + if (value > static_cast(INT_MAX)) { + throw std::runtime_error(std::string{"sar3d "} + what + " exceeds the 32-bit kernel indexing"); + } + return static_cast(value); +} + +// Native code when this NVRTC knows the GPU's architecture, otherwise PTX for +// the newest architecture it knows, which the driver compiles on load. +[[nodiscard]] auto compile_kernels(CUdevice device) -> std::string { + int major = 0; + int minor = 0; + check(cuDeviceGetAttribute(&major, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR, device), "compute capability"); + check(cuDeviceGetAttribute(&minor, CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR, device), "compute capability"); + const int device_arch = 10 * major + minor; + int arch_count = 0; + check(nvrtcGetNumSupportedArchs(&arch_count), "supported architectures"); + std::vector archs(static_cast(arch_count)); + check(nvrtcGetSupportedArchs(archs.data()), "supported architectures"); + int target = 0; + for (const int arch : archs) { + if (arch <= device_arch) { + target = std::max(target, arch); + } + } + if (target == 0) { + throw std::runtime_error( + "GPU sm_" + std::to_string(device_arch) + " is older than every architecture this NVRTC supports" + ); + } + const bool native = target == device_arch; + const std::string arch_option = + std::string{native ? "--gpu-architecture=sm_" : "--gpu-architecture=compute_"} + std::to_string(target); + + nvrtcProgram raw_program = nullptr; + check(nvrtcCreateProgram(&raw_program, kKernelSource, "sar3d.cu", 0, nullptr, nullptr), "create program"); + const auto destroy = [](nvrtcProgram program) { nvrtcDestroyProgram(&program); }; + const std::unique_ptr, decltype(destroy)> program(raw_program, destroy); + + const char* options[] = {arch_option.c_str()}; + if (nvrtcCompileProgram(program.get(), 1, options) != NVRTC_SUCCESS) { + std::size_t log_size = 0U; + nvrtcGetProgramLogSize(program.get(), &log_size); + std::string log(log_size, '\0'); + nvrtcGetProgramLog(program.get(), log.data()); + throw std::runtime_error("NVRTC failed to compile the sar3d kernels:\n" + log); + } + const auto image_size = native ? nvrtcGetCUBINSize : nvrtcGetPTXSize; + const auto image_data = native ? nvrtcGetCUBIN : nvrtcGetPTX; + std::size_t size = 0U; + check(image_size(program.get(), &size), "kernel image size"); + std::string image(size, '\0'); + check(image_data(program.get(), image.data()), "kernel image"); + return image; +} + +// Device allocation that only grows, so steady-state frames never reallocate. +class DeviceBuffer { + public: + DeviceBuffer() = default; + DeviceBuffer(const DeviceBuffer&) = delete; + auto operator=(const DeviceBuffer&) -> DeviceBuffer& = delete; + ~DeviceBuffer() { + if (pointer_ != 0U) { + cuMemFree(pointer_); + } + } + + auto reserve(std::size_t bytes) -> CUdeviceptr { + if (bytes > capacity_) { + if (pointer_ != 0U) { + check(cuMemFree(pointer_), "free"); + pointer_ = 0U; + capacity_ = 0U; + } + check(cuMemAlloc(&pointer_, bytes), "allocate"); + capacity_ = bytes; + } + return pointer_; + } + + template + auto upload(std::span values) -> CUdeviceptr { + reserve(values.size_bytes()); + if (!values.empty()) { + check(cuMemcpyHtoD(pointer_, values.data(), values.size_bytes()), "upload"); + } + return pointer_; + } + + [[nodiscard]] auto pointer() const -> CUdeviceptr { + return pointer_; + } + + private: + CUdeviceptr pointer_ = 0U; + std::size_t capacity_ = 0U; +}; + +template +[[nodiscard]] auto download(CUdeviceptr source, std::size_t count) -> std::vector { + std::vector values(count); + check(cuMemcpyDtoH(values.data(), source, sizeof(T) * count), "download"); + return values; +} + +// One thread per item; argument types must match the kernel signature exactly. +template +void launch(CUfunction kernel, int items, Args... args) { + void* parameters[] = {static_cast(&args)...}; + const auto blocks = static_cast((items + kBlockSize - 1) / kBlockSize); + check(cuLaunchKernel(kernel, blocks, 1U, 1U, kBlockSize, 1U, 1U, 0U, nullptr, parameters, nullptr), "launch"); +} + +} // namespace + +struct Gpu::Impl { + // Declared first, destroyed last: buffers and the module need the context. + struct Context { + CUdevice device = 0; + CUcontext handle = nullptr; + Context() { + check(cuInit(0U), "init"); + check(cuDeviceGet(&device, 0), "device"); + check(cuDevicePrimaryCtxRetain(&handle, device), "context"); + if (const CUresult result = cuCtxSetCurrent(handle); result != CUDA_SUCCESS) { + cuDevicePrimaryCtxRelease(device); + check(result, "context"); + } + } + ~Context() { cuDevicePrimaryCtxRelease(device); } + } context; + struct Module { + CUmodule handle = nullptr; + explicit Module(const std::string& image) { check(cuModuleLoadData(&handle, image.data()), "load kernels"); } + ~Module() { cuModuleUnload(handle); } + [[nodiscard]] auto function(const char* name) const -> CUfunction { + CUfunction kernel = nullptr; + check(cuModuleGetFunction(&kernel, handle, name), name); + return kernel; + } + } module{compile_kernels(context.device)}; + CUfunction inverse_dft = module.function("inverse_dft"); + CUfunction backproject = module.function("backproject"); + CUfunction smooth = module.function("smooth"); + CUfunction max_project = module.function("max_project"); + CUfunction project_oblique = module.function("project_oblique"); + int smooth_radius = 0; + DeviceBuffer taps; + DeviceBuffer bands; + DeviceBuffer axes; + DeviceBuffer contributions; + DeviceBuffer ascans; + DeviceBuffer volume; + DeviceBuffer scratch; + DeviceBuffer images; + + ~Impl() { cuCtxSetCurrent(context.handle); } +}; + +Gpu::Gpu(double smooth_sigma_voxels) : impl_(std::make_unique()) { + // scipy.ndimage.gaussian_filter1d taps: radius int(4 sigma + 0.5), normalized. + const int radius = static_cast(4.0 * smooth_sigma_voxels + 0.5); + std::vector weights{}; + for (int k = -radius; k <= radius; ++k) { + weights.push_back(std::exp(-0.5 * k * k / (smooth_sigma_voxels * smooth_sigma_voxels))); + } + double total = 0.0; + for (const double weight : weights) { + total += weight; + } + std::vector taps{}; + for (const double weight : weights) { + taps.push_back(static_cast(weight / total)); + } + impl_->smooth_radius = radius; + impl_->taps.upload(std::span(taps)); +} + +Gpu::~Gpu() = default; + +auto Gpu::inverse_dft( + std::span> bands, + std::size_t points, + std::size_t first_bin, + std::size_t size +) -> std::vector> { + Impl& gpu = *impl_; + check(cuCtxSetCurrent(gpu.context.handle), "context"); + const std::size_t samples = bands.size() / points * size; + const int items = checked_int(samples, "A-scan stack"); + const CUdeviceptr input = gpu.bands.upload(bands); + const CUdeviceptr output = gpu.ascans.reserve(sizeof(std::complex) * samples); + launch(gpu.inverse_dft, items, input, checked_int(points, "band"), checked_int(first_bin, "first bin"), + checked_int(size, "A-scan"), items, output); + return download>(output, samples); +} + +auto Gpu::project(const VolumeRequest& request) -> Projections { + Impl& gpu = *impl_; + check(cuCtxSetCurrent(gpu.context.handle), "context"); + + const int nx = checked_int(request.x.size(), "grid"); + const int ny = checked_int(request.y.size(), "grid"); + const int nz = checked_int(request.z.size(), "grid"); + const int voxels = checked_int(request.x.size() * request.y.size() * request.z.size(), "grid"); + const int samples = checked_int(request.samples_per_ascan, "A-scan"); + + std::vector axes(request.x.begin(), request.x.end()); + axes.insert(axes.end(), request.y.begin(), request.y.end()); + axes.insert(axes.end(), request.z.begin(), request.z.end()); + const CUdeviceptr gx = gpu.axes.upload(std::span(axes)); + const CUdeviceptr gy = gx + sizeof(double) * request.x.size(); + const CUdeviceptr gz = gy + sizeof(double) * request.y.size(); + const CUdeviceptr contributions = gpu.contributions.upload(request.contributions); + const CUdeviceptr ascans = gpu.ascans.upload(request.ascans); + const CUdeviceptr volume = gpu.volume.reserve(sizeof(float) * static_cast(voxels)); + const CUdeviceptr scratch = gpu.scratch.reserve(sizeof(float) * static_cast(voxels)); + + launch(gpu.backproject, voxels, ascans, samples, contributions, + checked_int(request.contributions.size(), "aperture"), gx, gy, gz, nx, nz, voxels, + std::cos(request.pitch_rad), std::sin(request.pitch_rad), request.ground_depth_m, + request.min_depth_m, request.max_depth_m, static_cast(request.samples_per_metre), + static_cast(request.use_compensation), request.comp_range_power, request.comp_weight_max, volume); + // Separable Gaussian with sigma (0, s, s) over [y][z][x]: Z first, then X, like scipy. + const CUdeviceptr taps = gpu.taps.pointer(); + launch(gpu.smooth, voxels, volume, scratch, taps, gpu.smooth_radius, nx, nz, voxels, 1); + launch(gpu.smooth, voxels, scratch, volume, taps, gpu.smooth_radius, nx, nz, voxels, 0); + + const ObliqueView& view = request.oblique; + const int front_size = ny * nx; + const int side_size = ny * nz; + const int top_size = nz * nx; + const int oblique_size = checked_int(static_cast(view.nv) * static_cast(view.nu), "view"); + const auto image_floats = static_cast(front_size + side_size + top_size + oblique_size); + const CUdeviceptr front = gpu.images.reserve(sizeof(float) * image_floats); + const CUdeviceptr side = front + sizeof(float) * static_cast(front_size); + const CUdeviceptr top = side + sizeof(float) * static_cast(side_size); + const CUdeviceptr oblique = top + sizeof(float) * static_cast(top_size); + const int plane = nz * nx; + launch(gpu.max_project, front_size, volume, front, front_size, nx, plane, 1, nz, nx); + launch(gpu.max_project, side_size, volume, side, side_size, nz, plane, nx, nx, 1); + launch(gpu.max_project, top_size, volume, top, top_size, nx, nx, 1, ny, plane); + check(cuMemsetD32(oblique, 0U, static_cast(oblique_size)), "clear"); + launch(gpu.project_oblique, voxels, volume, gx, gy, gz, nx, nz, voxels, view.u[0], view.u[1], view.u[2], + view.v[0], view.v[1], view.v[2], view.u0, view.v0, 1.0 / view.pixel_m, view.nu, view.nv, oblique); + + return Projections{ + .front = download(front, static_cast(front_size)), + .side = download(side, static_cast(side_size)), + .top = download(top, static_cast(top_size)), + .oblique = download(oblique, static_cast(oblique_size)), + }; +} + +#else // !SAR3D_WITH_CUDA + +struct Gpu::Impl {}; + +Gpu::Gpu(double /*smooth_sigma_voxels*/) { + throw std::runtime_error( + "data_processor was built without CUDA (nvrtc.h, cuda.h, libnvrtc or libcuda not found), " + "so sar3d cannot run; install the CUDA requirements and rebuild" + ); +} + +Gpu::~Gpu() = default; + +auto Gpu::inverse_dft(std::span>, std::size_t, std::size_t, std::size_t) + -> std::vector> { + return {}; +} + +auto Gpu::project(const VolumeRequest& /*request*/) -> Projections { + return {}; +} + +#endif + +} // namespace radar::processing::sar3d diff --git a/data_acq_and_processing/processing/processors/src/sar3d_gpu.hpp b/data_acq_and_processing/processing/processors/src/sar3d_gpu.hpp new file mode 100644 index 0000000..e3e4007 --- /dev/null +++ b/data_acq_and_processing/processing/processors/src/sar3d_gpu.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +// GPU half of the sar3d processor: A-scans of the band spectra, and the +// backprojection of an aperture into the 3D volume, smoothed and projected. +// Only the projections of the volume leave the GPU. +namespace radar::processing::sar3d { + +// One (aperture frame, Tx/Rx pair) term of the backprojection sum. The layout +// is shared with the CUDA kernel. +struct Contribution { + std::array tx{}; // Tx in radar axes, shifted by the frame offset [m] + std::array rx{}; // Rx in radar axes, shifted by the frame offset [m] + float weight = 0.0F; // aperture frame weight + float inv_reference_ranges = 0.0F; // 1 / (Rtx * Rrx) at the compensation reference point +}; +static_assert(sizeof(Contribution) == 8U * sizeof(float)); + +// Orthographic view of the volume: a voxel at scene point p lands in pixel +// (round((u.p - u0) / pixel), round((v.p - v0) / pixel)). +struct ObliqueView { + std::array u{}; // screen right, scene axes (X, Y, Z) + std::array v{}; // screen up + double u0 = 0.0; + double v0 = 0.0; + double pixel_m = 0.0; + int nu = 0; + int nv = 0; +}; + +struct VolumeRequest { + // Scene axes [m]: X across, Y height above the ground, Z along the ground. + std::span x; + std::span y; + std::span z; + double pitch_rad = 0.0; // radar tilt that maps scene axes onto radar axes + double ground_depth_m = 0.0; // height of the radar axes origin above the ground + double min_depth_m = 0.0; // radar-depth gate; voxels outside it stay empty + double max_depth_m = 0.0; + std::span contributions; + // One A-scan per contribution, back to back; sample k is the Tx+Rx path k / samples_per_metre. + std::span> ascans; + std::size_t samples_per_ascan = 0U; + double samples_per_metre = 0.0; + bool use_compensation = true; + float comp_range_power = 0.0F; + float comp_weight_max = 0.0F; + ObliqueView oblique{}; +}; + +// Max-projections of the smoothed, not yet normalized volume (row-major). +struct Projections { + std::vector front; // [y][x], maximum along Z + std::vector side; // [y][z], maximum along X + std::vector top; // [z][x], maximum along Y + std::vector oblique; // [v][u] +}; + +// CUDA context with the kernels compiled for the installed GPU. Construction +// throws when no usable GPU is present or the processor was built without CUDA. +class Gpu { + public: + explicit Gpu(double smooth_sigma_voxels); + ~Gpu(); + Gpu(const Gpu&) = delete; + auto operator=(const Gpu&) -> Gpu& = delete; + + // numpy.fft.ifft of each band spectrum placed at bins [first_bin, first_bin + + // points) of a `size`-point spectrum; `bands` holds the bands back to back. + [[nodiscard]] auto inverse_dft( + std::span> bands, + std::size_t points, + std::size_t first_bin, + std::size_t size + ) -> std::vector>; + + // Backprojects, applies the Gaussian smoothing over (Z, X) and projects. + [[nodiscard]] auto project(const VolumeRequest& request) -> Projections; + + private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace radar::processing::sar3d diff --git a/data_acq_and_processing/processing/processors/src/sar3d_processor.cpp b/data_acq_and_processing/processing/processors/src/sar3d_processor.cpp index 4bf0d7e..1d1295f 100644 --- a/data_acq_and_processing/processing/processors/src/sar3d_processor.cpp +++ b/data_acq_and_processing/processing/processors/src/sar3d_processor.cpp @@ -1,475 +1,723 @@ #include "sar3d_processor.hpp" #include +#include #include #include +#include +#include #include -#include -#include #include #include #include -#include +#include +#include +#include #include +#include +#include +#include #include -#include "gpr/ascan.hpp" -#include "gpr/config.hpp" -#include "gpr/geometry.hpp" -#include "gpr/sar3d.hpp" +#include "sar3d_gpu.hpp" +// Port of the 3D SAR part of the reference notebook Multi_antenna_3D_clean_v1.ipynb +// (cells 0 and 4). Comments name the notebook functions and constants each step +// follows; the parameters keep the notebook names in the GUI. namespace radar::processing { namespace { -constexpr std::size_t kAscanCacheLimit = 64U; +using std::numbers::pi; -void hash_mix(std::size_t& seed, std::uint64_t value) { - seed ^= value + 0x9e3779b97f4a7c15ULL + (seed << 6U) + (seed >> 2U); -} +// Notebook constants that are not live parameters. +constexpr double kWaveSpeed = 3e8; // v, eps_r = 1 +constexpr std::size_t kOversample = 8U; // BP_OVERSAMPLE +constexpr double kPairNormPercentile = 50.0; // PAIR_NORM_PERCENTILE +constexpr double kPairNormEps = 1e-15; // PAIR_NORM_EPS +constexpr double kCompRefDepth = 5.0; // COMP_REF_DEPTH +constexpr float kCompRangeWeightMax = 5.0F; // COMP_RANGE_WEIGHT_MAX +constexpr double kEdgeFrameWeight = 0.8; // SAR_EDGE_FRAME_WEIGHT +constexpr double kSmoothSigma = 1.5; // BP_SMOOTH_SIGMA, voxels +constexpr double kNormEdgeMargin = 0.3; // SAR_NORM_EDGE_MARGIN_M +// Scene window: X across, Y height above the ground (SAR_Y_RANGE_M). Z along the +// ground follows the depth gate. +constexpr double kSceneXMin = -2.0; +constexpr double kSceneXMax = 2.0; +constexpr double kSceneYMin = -0.5; +constexpr double kSceneYMax = 2.0; +static_assert(kSceneYMax - kSceneYMin > 2.5 * kNormEdgeMargin, "else sar_norm_reference_mask keeps every plane"); +// The 3D view looks along the camera of the notebook's 3D scene +// (sar_scene_layout: Plotly eye (1.7, -2.2, 0.85) in its axes X, Z, Y). +constexpr std::array kCameraEyeXzy{1.7, -2.2, 0.85}; -void hash_mix(std::size_t& seed, double value) { - hash_mix(seed, std::bit_cast(value)); -} +// Live parameters of one processing pass (notebook names). +struct Settings { + std::vector inputs{}; // INPUT_IDX, ascending + std::vector outputs{}; // OUTPUT_IDX, ascending + std::vector> rx{}; // per selected input, radar axes [m] + std::vector> tx{}; // per selected output, radar axes [m] + double f_start_hz = 0.0; // F_START + double f_stop_hz = 0.0; // F_STOP + double min_depth_m = 0.0; // MIN_DEPTH + double max_depth_m = 0.0; // MAX_DEPTH + double speed_m_s = 0.0; // SPEED_M_S + double look_angle_rad = 0.0; // LOOK_ANGLE_DEG + double ground_depth_m = 0.0; // BP_GROUND_DEPTH_M + std::size_t frame_radius = 0U; // SAR_FRAME_RADIUS + double voxel_m = 0.0; // SAR_VOXEL_M, one edge for X, Y and Z + float comp_range_power = 0.0F; // COMP_RANGE_POWER + bool use_compensation = true; // SAR_USE_COMPENSATION + bool normalize_each_frame = true; // SAR_NORMALIZE_EACH_FRAME -[[nodiscard]] auto contains_u32(const std::vector& values, std::uint32_t value) -> bool { - return std::find(values.begin(), values.end(), value) != values.end(); -} + // Everything the cached A-scans depend on. + [[nodiscard]] auto ascan_key() const { + return std::make_tuple(inputs, outputs, f_start_hz, f_stop_hz, min_depth_m, max_depth_m, speed_m_s, + look_angle_rad, normalize_each_frame); + } +}; +using AscanKey = decltype(std::declval().ascan_key()); -// Requested positions filtered to the available set; empty request selects all. -// Mirrors the coherent-BP processor's selection semantics. -[[nodiscard]] auto selected_positions( +// Requested positions that have antenna geometry, ascending; no request selects them all. +template +void select_antennas( const std::vector& requested, - std::vector available -) -> std::vector { - std::sort(available.begin(), available.end()); - available.erase(std::unique(available.begin(), available.end()), available.end()); - if (requested.empty()) { - return available; - } - std::vector result{}; - for (const auto value : requested) { - if (contains_u32(available, value) && !contains_u32(result, value)) { - result.push_back(value); + const std::vector& geometry, + Position position_of, + std::vector& positions, + std::vector>& coordinates +) { + for (const auto& entry : geometry) { + const auto position = position_of(entry); + if (requested.empty() || std::find(requested.begin(), requested.end(), position) != requested.end()) { + positions.push_back(position); } } - std::sort(result.begin(), result.end()); - return result; + std::sort(positions.begin(), positions.end()); + positions.erase(std::unique(positions.begin(), positions.end()), positions.end()); + for (const auto position : positions) { + const auto& entry = *std::find_if(geometry.begin(), geometry.end(), [&](const Entry& candidate) { + return position_of(candidate) == position; + }); + coordinates.push_back({entry.x_m, entry.y_m, entry.z_m}); + } } -struct Selection { - // Sorted ascending switch positions; the logical pair key (i_tx, i_rx) is - // the index of a position within these lists. - std::vector inputs{}; - std::vector outputs{}; +[[nodiscard]] auto read_settings(const config::RunConfig& run_config, const ProcessingLiveConfig& live) -> Settings { + Settings settings{}; + select_antennas(live.gpr_input_positions, run_config.gpr.rx_geometry, + [](const config::GprRxGeometry& entry) { return entry.input_pos; }, + settings.inputs, settings.rx); + select_antennas(live.gpr_output_positions, run_config.gpr.tx_geometry, + [](const config::GprTxGeometry& entry) { return entry.output_pos; }, + settings.outputs, settings.tx); + if (settings.inputs.empty() || settings.outputs.empty()) { + throw std::runtime_error("no selected Tx/Rx position has gpr geometry"); + } + settings.f_start_hz = static_cast(live.gpr_start_freq_mhz) * 1e6; + settings.f_stop_hz = static_cast(live.gpr_stop_freq_mhz) * 1e6; + settings.min_depth_m = live.gpr_min_depth_m; + settings.max_depth_m = live.gpr_max_depth_m; + settings.speed_m_s = live.gpr_speed_m_s; + settings.look_angle_rad = static_cast(live.gpr_look_angle_deg) * (pi / 180.0); + settings.ground_depth_m = live.sar3d_bp_ground_depth_m; + settings.frame_radius = live.sar3d_frame_radius; + settings.voxel_m = live.sar3d_voxel_m; + settings.comp_range_power = live.sar3d_comp_range_power; + settings.use_compensation = live.sar3d_use_compensation; + settings.normalize_each_frame = live.sar3d_normalize_each_frame; + if (!(settings.f_stop_hz > settings.f_start_hz)) { + throw std::runtime_error("F_STOP must be above F_START"); + } + if (!(settings.max_depth_m > settings.min_depth_m)) { + throw std::runtime_error("MAX_DEPTH must be above MIN_DEPTH"); + } + if (!(settings.voxel_m > 0.0)) { + throw std::runtime_error("SAR_VOXEL_M must be positive"); + } + return settings; +} + +// numpy.percentile with linear interpolation. +[[nodiscard]] auto percentile(std::vector values, double q) -> double { + const double position = q / 100.0 * static_cast(values.size() - 1U); + const auto lower = static_cast(position); + const auto lower_it = values.begin() + static_cast(lower); + std::nth_element(values.begin(), lower_it, values.end()); + if (lower + 1U == values.size()) { + return *lower_it; + } + const double upper = *std::min_element(lower_it + 1, values.end()); + return *lower_it + (position - static_cast(lower)) * (upper - *lower_it); +} + +// When the samples of a trace were measured, relative to the acquisition centre +// of its collection (compute_pair_timestamps_for_snapshot and +// compute_frequency_sample_times). Traces sharing a capture window come from +// one sweep: at every frequency point its stimulus stages - the outputs, which +// are numbered in device stage order - take turns while all inputs receive. +struct TraceClock { + double start_s = 0.0; // sweep start + double event_s = 0.0; // sweep duration / (stages * points) + long long stage = 0; + long long stages = 1; + + [[nodiscard]] auto sample_time_s(std::size_t point) const -> double { + return start_s + static_cast(stages * static_cast(point) + stage) * event_s; + } }; -[[nodiscard]] auto build_selection( - const config::RunConfig& run_config, - const ProcessingLiveConfig& live_config -) -> Selection { - std::vector available_inputs{}; - available_inputs.reserve(run_config.gpr.rx_geometry.size()); - for (const auto& entry : run_config.gpr.rx_geometry) { - available_inputs.push_back(entry.input_pos); - } - std::vector available_outputs{}; - available_outputs.reserve(run_config.gpr.tx_geometry.size()); - for (const auto& entry : run_config.gpr.tx_geometry) { - available_outputs.push_back(entry.output_pos); - } - Selection selection{}; - selection.inputs = selected_positions(live_config.gpr_input_positions, std::move(available_inputs)); - selection.outputs = selected_positions(live_config.gpr_output_positions, std::move(available_outputs)); - return selection; -} - -[[nodiscard]] auto build_geometry( - const config::RunConfig& run_config, - const Selection& selection -) -> gpr::GeometryConfig { - gpr::GeometryConfig geom{}; - geom.input_idx.clear(); - geom.output_idx.clear(); - geom.rx_positions.clear(); - geom.tx_positions.clear(); - for (const auto position : selection.inputs) { - for (const auto& entry : run_config.gpr.rx_geometry) { - if (entry.input_pos == position) { - geom.input_idx.push_back(static_cast(position)); - geom.rx_positions.push_back({ - static_cast(entry.x_m), - static_cast(entry.y_m), - static_cast(entry.z_m), - }); - break; - } - } - } - for (const auto position : selection.outputs) { - for (const auto& entry : run_config.gpr.tx_geometry) { - if (entry.output_pos == position) { - geom.output_idx.push_back(static_cast(position)); - geom.tx_positions.push_back({ - static_cast(entry.x_m), - static_cast(entry.y_m), - static_cast(entry.z_m), - }); - break; - } - } - } - geom.eps_r = static_cast(run_config.gpr.relative_permittivity); - geom.finalize(); - return geom; -} - -[[nodiscard]] auto build_bp_config(const ProcessingLiveConfig& live_config) -> gpr::BPConfig { - gpr::BPConfig bp{}; - bp.f_start = static_cast(live_config.gpr_start_freq_mhz) * 1e6; - bp.f_stop = static_cast(live_config.gpr_stop_freq_mhz) * 1e6; - bp.min_depth = static_cast(live_config.gpr_min_depth_m); - bp.max_depth = static_cast(live_config.gpr_max_depth_m); - bp.pair_normalize = live_config.sar3d_normalize_pairs; - bp.pair_norm_depth_min = bp.min_depth; - bp.pair_norm_depth_max = bp.max_depth; - bp.comp_range_power = static_cast(live_config.sar3d_comp_range_power); - return bp; -} - -[[nodiscard]] auto build_motion_config(const ProcessingLiveConfig& live_config) -> gpr::MotionConfig { - gpr::MotionConfig motion{}; - motion.speed_m_s = static_cast(live_config.gpr_speed_m_s); - motion.look_angle_deg = static_cast(live_config.gpr_look_angle_deg); - // Notebook BP_GROUND_PITCH_DEG = None -> the frame pitch follows the look angle. - motion.pitch_deg = live_config.sar3d_pitch_override_enabled - ? static_cast(live_config.sar3d_pitch_deg) - : static_cast(live_config.gpr_look_angle_deg); - motion.direction_sign = static_cast(live_config.gpr_direction_sign); - motion.apply_freq_phase_correction = live_config.gpr_apply_freq_phase_correction; - motion.tx_per_segment = static_cast(std::max(1U, live_config.sar3d_tx_per_segment)); - return motion; -} - -[[nodiscard]] auto build_sar_config(const ProcessingLiveConfig& live_config) -> gpr::SARConfig { - gpr::SARConfig sar{}; - sar.frame_radius = static_cast(std::max(0U, live_config.sar3d_frame_radius)); - sar.edge_frame_weight = static_cast(live_config.sar3d_edge_frame_weight); - sar.ground_depth_m = static_cast(live_config.sar3d_ground_depth_m); - sar.x_min_m = static_cast(live_config.sar3d_x_min_m); - sar.x_max_m = static_cast(live_config.sar3d_x_max_m); - sar.y_min_m = static_cast(live_config.sar3d_y_min_m); - sar.y_max_m = static_cast(live_config.sar3d_y_max_m); - sar.z_min_m = static_cast(live_config.sar3d_z_min_m); - sar.z_max_m = static_cast(live_config.sar3d_z_max_m); - sar.voxel_m = { - static_cast(live_config.sar3d_voxel_x_m), - static_cast(live_config.sar3d_voxel_y_m), - static_cast(live_config.sar3d_voxel_z_m), - }; - sar.norm_edge_margin_m = static_cast(live_config.sar3d_norm_edge_margin_m); - sar.smooth_sigma = static_cast(live_config.sar3d_smooth_sigma); - sar.use_compensation = live_config.sar3d_use_compensation; - return sar; -} - -// Hardware Tx order: first occurrence of each output in run_combos. Deliberately -// ignores the live Tx selection so segment/slot assignment stays a hardware -// property (notebook tx_order_full). -[[nodiscard]] auto tx_order_full(const config::RunConfig& run_config) -> std::vector { - std::vector order{}; - for (const auto& combo : run_config.run_combos) { - const int output = static_cast(combo.output_pos); - if (std::find(order.begin(), order.end(), output) == order.end()) { - order.push_back(output); - } - } - return order; -} - -[[nodiscard]] auto ascan_fingerprint( - const Selection& selection, - const gpr::GeometryConfig& geom, - const gpr::BPConfig& bp, - const gpr::MotionConfig& motion -) -> std::size_t { - std::size_t seed = 0U; - for (const auto position : selection.inputs) { - hash_mix(seed, static_cast(position)); - } - for (const auto position : selection.outputs) { - hash_mix(seed, static_cast(static_cast(position) | (1ULL << 40U))); - } - hash_mix(seed, geom.eps_r); - hash_mix(seed, bp.f_start); - hash_mix(seed, bp.f_stop); - hash_mix(seed, bp.pair_norm_depth_min); - hash_mix(seed, bp.pair_norm_depth_max); - hash_mix(seed, static_cast(bp.pair_normalize ? 1U : 0U)); - hash_mix(seed, motion.speed_m_s); - hash_mix(seed, motion.look_angle_deg); - hash_mix(seed, motion.direction_sign); - hash_mix(seed, static_cast(motion.apply_freq_phase_correction ? 1U : 0U)); - hash_mix(seed, static_cast(motion.tx_per_segment)); - return seed; -} - -[[nodiscard]] auto engine_fingerprint( - const gpr::GeometryConfig& geom, - const gpr::BPConfig& bp, - const gpr::MotionConfig& motion, - const gpr::SARConfig& sar -) -> std::size_t { - std::size_t seed = 1U; - for (std::size_t index = 0U; index < geom.tx_positions.size(); ++index) { - hash_mix(seed, static_cast(geom.output_idx[index])); - hash_mix(seed, geom.tx_positions[index][0]); - hash_mix(seed, geom.tx_positions[index][1]); - hash_mix(seed, geom.tx_positions[index][2]); - } - for (std::size_t index = 0U; index < geom.rx_positions.size(); ++index) { - hash_mix(seed, static_cast(geom.input_idx[index])); - hash_mix(seed, geom.rx_positions[index][0]); - hash_mix(seed, geom.rx_positions[index][1]); - hash_mix(seed, geom.rx_positions[index][2]); - } - hash_mix(seed, geom.eps_r); - hash_mix(seed, bp.f_start); - hash_mix(seed, bp.f_stop); - hash_mix(seed, bp.min_depth); - hash_mix(seed, bp.max_depth); - hash_mix(seed, bp.comp_range_power); - hash_mix(seed, bp.comp_weight_max); - hash_mix(seed, bp.comp_ref_depth); - hash_mix(seed, motion.pitch_deg); - hash_mix(seed, sar.ground_depth_m); - hash_mix(seed, static_cast(sar.use_compensation ? 1U : 0U)); - return seed; -} - -// Midpoint of the collection's capture window; monotonic_ns is the fallback for -// producers that never stamped capture times. -[[nodiscard]] auto collection_time_s(const ipc::PreprocessedCollection& collection) -> double { - if (collection.capture_end_ns > collection.capture_start_ns) { - return 0.5 * (static_cast(collection.capture_start_ns) + static_cast(collection.capture_end_ns)) * 1e-9; - } - return static_cast(collection.monotonic_ns) * 1e-9; -} - -// Real frame timing from the per-trace capture stamps: traces measured in one -// segment share one window on both producers, so the first window seen per Tx -// reconstructs the segments exactly (notebook snapshot_acquisition_timing). -// Invalid when any trace carries the zero "unmeasured" sentinel. -struct SnapshotTiming { - gpr::TxWindows windows{}; // seconds relative to the first trace start - double acq_s = 0.0; - double center_abs_s = 0.0; // absolute acquisition centre (frame time) - bool valid = false; +struct CollectionTiming { + double center_s = 0.0; // absolute acquisition centre, positions the frame in the aperture + std::vector clocks; // per collection trace }; -[[nodiscard]] auto snapshot_timing(const ipc::PreprocessedCollection& collection) -> SnapshotTiming { - SnapshotTiming timing{}; - if (collection.traces.empty()) { - return timing; - } - std::map> by_tx{}; +// Timing from the per-trace capture windows (snapshot_acquisition_timing); every +// trace takes part, so the stages do not depend on the INPUT_IDX/OUTPUT_IDX choice. +[[nodiscard]] auto collection_timing(const ipc::PreprocessedCollection& collection) -> CollectionTiming { + std::map, std::vector> sweep_outputs{}; std::uint64_t first = std::numeric_limits::max(); std::uint64_t last = 0U; for (const auto& trace : collection.traces) { if (trace.capture_end_ns <= trace.capture_start_ns) { - return timing; + throw std::runtime_error("collection has traces without a capture window"); } - by_tx.try_emplace( - static_cast(trace.combo.output_pos), - std::pair{trace.capture_start_ns, trace.capture_end_ns} - ); + sweep_outputs[{trace.capture_start_ns, trace.capture_end_ns}].push_back(trace.combo.output_pos); first = std::min(first, trace.capture_start_ns); last = std::max(last, trace.capture_end_ns); } - for (const auto& [tx, window] : by_tx) { - timing.windows[tx] = { - static_cast(window.first - first) * 1e-9, - static_cast(window.second - first) * 1e-9, - }; + for (auto& [window, outputs] : sweep_outputs) { + std::sort(outputs.begin(), outputs.end()); + outputs.erase(std::unique(outputs.begin(), outputs.end()), outputs.end()); + } + + CollectionTiming timing{}; + const auto center_x2_ns = static_cast(first + last); + timing.center_s = 0.5e-9 * static_cast(center_x2_ns); + for (const auto& trace : collection.traces) { + const auto& stages = sweep_outputs.at({trace.capture_start_ns, trace.capture_end_ns}); + const auto stage = std::find(stages.begin(), stages.end(), trace.combo.output_pos) - stages.begin(); + TraceClock clock{}; + clock.start_s = + 0.5e-9 * static_cast(2 * static_cast(trace.capture_start_ns) - center_x2_ns); + clock.stage = stage; + clock.stages = static_cast(stages.size()); + clock.event_s = 1e-9 * static_cast(trace.capture_end_ns - trace.capture_start_ns) / + static_cast(clock.stages * static_cast(trace.frequency_hz.size())); + timing.clocks.push_back(clock); } - timing.acq_s = static_cast(last - first) * 1e-9; - timing.center_abs_s = 0.5 * (static_cast(first) + static_cast(last)) * 1e-9; - timing.valid = true; return timing; } -// Debug dump for the parity tool: raw float32 volume [iy][iz][ix] plus a json -// header with the axes and level. -void dump_volume( - const std::string& dir, - std::uint64_t collection_id, - const gpr::FinishedVolume& finished, - const gpr::Grid& grid -) { - char name[64]; - std::snprintf(name, sizeof(name), "/sar3d_volume_%llu", - static_cast(collection_id)); - const std::string base = dir + name; - std::ofstream bin(base + ".f32", std::ios::binary | std::ios::trunc); - bin.write( - reinterpret_cast(finished.volume.data()), - static_cast(finished.volume.size() * sizeof(float)) - ); - std::ofstream meta(base + ".json", std::ios::trunc); - meta.precision(17); - meta << "{\"nx\": " << finished.NX << ", \"ny\": " << finished.NY - << ", \"nz\": " << finished.NZ << ", \"level\": " << finished.level; - const auto write_axis = [&meta](const char* key, const std::vector& axis) { - meta << ", \"" << key << "\": ["; - for (std::size_t index = 0U; index < axis.size(); ++index) { - meta << (index > 0U ? "," : "") << axis[index]; +// Band of one trace, ready for the IFFT (apply_intra_sweep_phase_correction and +// compute_ascan_bp up to the IFFT): the radar motion removed as a phase, then +// the Hann window over the band. +struct Band { + std::size_t first_bin = 0U; // k0, the IFFT bin of the first band point + std::size_t size = 0U; // IFFT length: 2(k0 + n - 1) rounded up to a power of two, x BP_OVERSAMPLE + double df_hz = 0.0; // median frequency step + std::vector> spectrum{}; +}; + +[[nodiscard]] auto trace_band(const ipc::SweepTraceBlock& trace, const TraceClock& clock, const Settings& settings) + -> Band { + const auto& frequency = trace.frequency_hz; + if (trace.s21.size() != frequency.size()) { + throw std::runtime_error("trace has different S21 and frequency lengths"); + } + std::vector points{}; + for (std::size_t point = 0U; point < frequency.size(); ++point) { + if (frequency[point] >= settings.f_start_hz && frequency[point] <= settings.f_stop_hz) { + points.push_back(point); } - meta << "]"; - }; - write_axis("x_m", grid.x); - write_axis("y_m", grid.y); - write_axis("z_m", grid.z); - meta << "}\n"; + } + if (points.size() < 2U) { + throw std::runtime_error("fewer than two frequency points between F_START and F_STOP"); + } + std::vector steps{}; + for (std::size_t index = 0U; index + 1U < points.size(); ++index) { + steps.push_back(static_cast(frequency[points[index + 1U]]) - frequency[points[index]]); + } + Band band{}; + band.df_hz = percentile(std::move(steps), 50.0); + if (!(band.df_hz > 0.0)) { + throw std::runtime_error("trace frequencies do not increase"); + } + band.first_bin = static_cast(std::nearbyint(frequency[points.front()] / band.df_hz)); + band.size = std::bit_ceil(2U * (band.first_bin + points.size() - 1U)) * kOversample; + + // The radar moves SPEED_M_S * cos(LOOK_ANGLE_DEG) along range; its offset + // from the collection centre at the sample time becomes the phase + // exp(-j 4 pi f offset / v): the pair and the intra-sweep corrections in one + // (MOTION_PHASE_SIGN = -1, direction +1). + const double phase_per_hz_s = -4.0 * pi * settings.speed_m_s * std::cos(settings.look_angle_rad) / kWaveSpeed; + const auto last = static_cast(points.size() - 1U); + for (std::size_t index = 0U; index < points.size(); ++index) { + const std::size_t point = points[index]; + const double hann = 0.5 - 0.5 * std::cos(2.0 * pi * static_cast(index) / last); + const std::complex sample(trace.s21[point].re, trace.s21[point].im); + band.spectrum.emplace_back( + sample * std::polar(hann, phase_per_hz_s * frequency[point] * clock.sample_time_s(point)) + ); + } + return band; } -[[nodiscard]] auto build_image_payload( - const std::string& processing_name, +// SAR_NORMALIZE_EACH_FRAME: divide a channel by the PAIR_NORM_PERCENTILE of |h| +// inside the depth gate (sar_pair_ascans). +void normalize_ascan(std::span> ascan, double dt_s, const Settings& settings) { + std::vector magnitudes{}; + for (std::size_t index = 0U; index < ascan.size(); ++index) { + const double depth = static_cast(index) * dt_s * kWaveSpeed / 2.0; + if (depth >= settings.min_depth_m && depth <= settings.max_depth_m) { + magnitudes.push_back(std::abs(std::complex(ascan[index]))); + } + } + if (magnitudes.empty()) { + for (const auto& sample : ascan) { + magnitudes.push_back(std::abs(std::complex(sample))); + } + } + double scale = percentile(std::move(magnitudes), kPairNormPercentile); + if (!std::isfinite(scale) || scale <= kPairNormEps) { + scale = 1.0; + } + const auto divisor = static_cast(scale + kPairNormEps); + for (auto& sample : ascan) { + sample /= divisor; + } +} + +// A-scans of every selected Tx/Rx pair of one collection, (Tx, Rx) order. +struct Frame { + double center_s = 0.0; + double dt_s = 0.0; + std::size_t samples = 0U; // per A-scan + std::vector> ascans{}; // [pair][sample] +}; + +[[nodiscard]] auto compute_frame( + const ipc::PreprocessedCollection& collection, + const Settings& settings, + sar3d::Gpu& gpu +) -> Frame { + const auto timing = collection_timing(collection); + std::vector bands{}; + for (const auto output : settings.outputs) { + for (const auto input : settings.inputs) { + const auto& traces = collection.traces; + const auto trace = std::find_if(traces.begin(), traces.end(), [&](const auto& candidate) { + return candidate.combo.input_pos == input && candidate.combo.output_pos == output; + }); + if (trace == traces.end()) { + throw std::runtime_error( + "collection has no trace for input " + std::to_string(input) + ", output " + std::to_string(output) + ); + } + const auto& clock = timing.clocks[static_cast(trace - traces.begin())]; + bands.push_back(trace_band(*trace, clock, settings)); + } + } + const Band& first = bands.front(); + std::vector> spectra{}; + for (const auto& band : bands) { + if (band.first_bin != first.first_bin || band.size != first.size || band.df_hz != first.df_hz || + band.spectrum.size() != first.spectrum.size()) { + throw std::runtime_error("traces of one collection have different frequency grids"); + } + spectra.insert(spectra.end(), band.spectrum.begin(), band.spectrum.end()); + } + Frame frame{}; + frame.center_s = timing.center_s; + frame.samples = first.size; + frame.dt_s = 1.0 / (static_cast(first.size) * first.df_hz); + frame.ascans = gpu.inverse_dft(spectra, first.spectrum.size(), first.first_bin, first.size); + if (settings.normalize_each_frame) { + for (std::size_t offset = 0U; offset < frame.ascans.size(); offset += frame.samples) { + normalize_ascan(std::span(frame.ascans).subspan(offset, frame.samples), frame.dt_s, settings); + } + } + return frame; +} + +// numpy.linspace(start, stop, count) with count >= 2. +[[nodiscard]] auto linspace(double start, double stop, std::size_t count) -> std::vector { + std::vector axis(count); + const double step = (stop - start) / static_cast(count - 1U); + for (std::size_t index = 0U; index < count; ++index) { + axis[index] = static_cast(index) * step + start; + } + axis.back() = stop; + return axis; +} + +// Axis with about the given pitch (sar_build_full_region_grid: round(span/pitch)+1 points). +[[nodiscard]] auto grid_axis(double start, double stop, double pitch) -> std::vector { + return linspace(start, stop, static_cast(std::max(std::nearbyint((stop - start) / pitch) + 1.0, 2.0))); +} + +struct Grid { + std::vector x{}; + std::vector y{}; + std::vector z{}; +}; + +// Scene grid (sar_build_full_region_grid, 'ground' axes): Y = 0 is the ground, +// Z runs along the ground and spans every point of the window whose radar depth +// is inside [MIN_DEPTH, MAX_DEPTH] at some height of the window. +[[nodiscard]] auto scene_grid(const Settings& settings) -> Grid { + const double cos_p = std::cos(settings.look_angle_rad); + const double sin_p = std::sin(settings.look_angle_rad); + double z_min = std::numeric_limits::infinity(); + double z_max = -std::numeric_limits::infinity(); + for (const double height : {kSceneYMin, kSceneYMax}) { + const double depth_at_zero = -(height - settings.ground_depth_m) * sin_p; + for (const double depth : {settings.min_depth_m, settings.max_depth_m}) { + const double z = (depth - depth_at_zero) / cos_p; + z_min = std::min(z_min, z); + z_max = std::max(z_max, z); + } + } + return { + grid_axis(kSceneXMin, kSceneXMax, settings.voxel_m), + grid_axis(kSceneYMin, kSceneYMax, settings.voxel_m), + grid_axis(z_min, z_max, settings.voxel_m), + }; +} + +// Cosine taper over the aperture: 1 at the centre frame, SAR_EDGE_FRAME_WEIGHT at +// the farthest one (sar_frame_weights). +[[nodiscard]] auto frame_weights(const std::vector& offsets_s) -> std::vector { + double farthest = 0.0; + for (const double offset : offsets_s) { + farthest = std::max(farthest, std::abs(offset)); + } + std::vector weights(offsets_s.size(), 1.0); + if (farthest <= 1e-12) { + return weights; + } + for (std::size_t index = 0U; index < offsets_s.size(); ++index) { + const double r = std::min(std::abs(offsets_s[index]) / farthest, 1.0); + weights[index] = kEdgeFrameWeight + (1.0 - kEdgeFrameWeight) * 0.5 * (1.0 + std::cos(pi * r)); + } + return weights; +} + +// 1 / (Rtx * Rrx) at the compensation reference point of a pair: under its +// midpoint, COMP_REF_DEPTH ahead, on the ground (sar_pair_geo_ref, bp_plane_y). +[[nodiscard]] auto inverse_reference_ranges( + const std::array& tx, + const std::array& rx, + const Settings& settings +) -> float { + const double pitch = settings.look_angle_rad; + const std::array point{ + 0.5 * (tx[0] + rx[0]), + (-settings.ground_depth_m + kCompRefDepth * std::sin(pitch)) / std::cos(pitch), + kCompRefDepth, + }; + const auto range = [&point](const std::array& antenna) { + return std::hypot(point[0] - antenna[0], point[1] - antenna[1], point[2] - antenna[2]); + }; + return static_cast(1.0 / (range(tx) * range(rx))); +} + +// Orthographic view along the notebook scene camera. Its axes are built in +// Plotly's (X, Z, Y) frame, where the camera math is the familiar one, and then +// reordered to scene (X, Y, Z). +struct Oblique { + sar3d::ObliqueView view{}; + std::vector u{}; + std::vector v{}; +}; + +[[nodiscard]] auto oblique_view(const Grid& grid, double pixel_m) -> Oblique { + const auto& eye = kCameraEyeXzy; + const double eye_norm = std::hypot(eye[0], eye[1], eye[2]); + const std::array forward{-eye[0] / eye_norm, -eye[1] / eye_norm, -eye[2] / eye_norm}; + const double horizontal = std::hypot(forward[0], forward[1]); + const std::array right{forward[1] / horizontal, -forward[0] / horizontal, 0.0}; // forward x up + const std::array up{ + right[1] * forward[2] - right[2] * forward[1], + right[2] * forward[0] - right[0] * forward[2], + right[0] * forward[1] - right[1] * forward[0], + }; + Oblique oblique{}; + auto& view = oblique.view; + view.u = {right[0], right[2], right[1]}; + view.v = {up[0], up[2], up[1]}; + view.pixel_m = pixel_m; + + double u_max = -std::numeric_limits::infinity(); + double v_max = u_max; + view.u0 = std::numeric_limits::infinity(); + view.v0 = view.u0; + for (const double x : {grid.x.front(), grid.x.back()}) { + for (const double y : {grid.y.front(), grid.y.back()}) { + for (const double z : {grid.z.front(), grid.z.back()}) { + const double u = view.u[0] * x + view.u[1] * y + view.u[2] * z; + const double v = view.v[0] * x + view.v[1] * y + view.v[2] * z; + view.u0 = std::min(view.u0, u); + view.v0 = std::min(view.v0, v); + u_max = std::max(u_max, u); + v_max = std::max(v_max, v); + } + } + } + view.nu = static_cast(std::lround((u_max - view.u0) / view.pixel_m)) + 1; + view.nv = static_cast(std::lround((v_max - view.v0) / view.pixel_m)) + 1; + for (int index = 0; index < view.nu; ++index) { + oblique.u.push_back(view.u0 + index * view.pixel_m); + } + for (int index = 0; index < view.nv; ++index) { + oblique.v.push_back(view.v0 + index * view.pixel_m); + } + return oblique; +} + +[[nodiscard]] auto to_float(const std::vector& values) -> std::vector { + return {values.begin(), values.end()}; +} + +[[nodiscard]] auto to_float(const std::array& point) -> std::array { + return {static_cast(point[0]), static_cast(point[1]), static_cast(point[2])}; +} + +[[nodiscard]] auto distance(const std::array& a, const std::array& b) -> double { + return std::hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]); +} + +[[nodiscard]] auto image_payload( + std::string name, const std::vector& x_axis, const std::vector& y_axis, std::vector values ) -> ipc::ResultPayload { ipc::ResultPayload payload{}; - payload.processing_name = processing_name; + payload.processing_name = std::move(name); payload.kind = ipc::ResultKind::ImageF32; - payload.image_x_axis.reserve(x_axis.size()); - for (const auto value : x_axis) { - payload.image_x_axis.push_back(static_cast(value)); - } - payload.image_y_axis.reserve(y_axis.size()); - for (const auto value : y_axis) { - payload.image_y_axis.push_back(static_cast(value)); - } + payload.image_x_axis = to_float(x_axis); + payload.image_y_axis = to_float(y_axis); payload.image_values = std::move(values); return payload; } +[[nodiscard]] auto table_payload(std::string name, std::uint32_t columns, std::vector values) + -> ipc::ResultPayload { + ipc::ResultPayload payload{}; + payload.processing_name = std::move(name); + payload.kind = ipc::ResultKind::TableF32; + payload.table_columns = columns; + payload.table_values = std::move(values); + return payload; +} + } // namespace struct Sar3dProcessor::State { - struct CacheEntry { - double time_s = 0.0; - // Null when the collection could not be preprocessed (e.g. missing - // pairs); cached so a bad collection is not retried every frame. - std::shared_ptr ascans{}; - }; - - // Keyed by collection_id (monotonically increasing), so evicting from the - // front drops the oldest collections. - std::map ascan_cache{}; - std::size_t ascan_fp = 0U; - - std::unique_ptr engine{}; - std::size_t engine_fp = 0U; - + // A-scans by collection id for `frames_key`; null marks a collection that + // could not be processed, so its error is reported once and not retried. + std::map> frames{}; + AscanKey frames_key{}; + std::unique_ptr gpu{}; + std::string gpu_error{}; std::string last_error{}; - std::uint64_t suppressed_errors = 0U; + std::uint64_t repeated_errors = 0U; - auto get_or_compute_ascans( - const ipc::PreprocessedCollection& collection, - const Selection& selection, - const gpr::GeometryConfig& geom, - const gpr::BPConfig& bp, - const gpr::MotionConfig& motion, - const std::vector& tx_order - ) -> const CacheEntry& { - if (const auto found = ascan_cache.find(collection.collection_id); found != ascan_cache.end()) { + auto frame(const ipc::PreprocessedCollection& collection, const Settings& settings, sar3d::Gpu& gpu) + -> std::shared_ptr { + if (const auto found = frames.find(collection.collection_id); found != frames.end()) { return found->second; } + std::shared_ptr frame{}; + try { + frame = std::make_shared(compute_frame(collection, settings, gpu)); + } catch (const std::exception& error) { + report_error(error.what()); + } + frames.emplace(collection.collection_id, frame); + return frame; + } - CacheEntry entry{}; - auto timing = snapshot_timing(collection); - // The segmented model needs a window for every selected Tx; fall back to - // the uniform split otherwise (notebook median model). - if (timing.valid) { - for (const auto output : geom.output_idx) { - if (timing.windows.find(output) == timing.windows.end()) { - timing.valid = false; - break; + // Set up once: compiling the kernels takes seconds, so a failure is kept + // instead of being retried on every frame. + auto gpu_engine() -> sar3d::Gpu& { + if (gpu == nullptr && gpu_error.empty()) { + try { + gpu = std::make_unique(kSmoothSigma); + } catch (const std::exception& error) { + gpu_error = error.what(); + } + } + if (gpu == nullptr) { + throw std::runtime_error(gpu_error); + } + return *gpu; + } + + auto render( + const ipc::PreprocessedCollection& collection, + std::span previous, + const Settings& settings + ) -> std::vector { + auto& gpu = gpu_engine(); + if (settings.ascan_key() != frames_key) { + frames.clear(); + frames_key = settings.ascan_key(); + } + const auto newest = frame(collection, settings, gpu); + if (newest == nullptr) { + return {}; + } + // Aperture: the newest 2r+1 collections, centred r collections before + // the newest one, so the image trails the input by r frames. Collections + // that left the aperture are dropped from the cache. + std::vector> aperture{}; + std::map> kept{{collection.collection_id, newest}}; + for (const auto& member : previous.last(std::min(previous.size(), 2U * settings.frame_radius))) { + auto member_frame = frame(member, settings, gpu); + kept.emplace(member.collection_id, member_frame); + const bool same_axis = member_frame != nullptr && member_frame->samples == newest->samples && + member_frame->dt_s == newest->dt_s; + if (same_axis) { + aperture.push_back(std::move(member_frame)); + } + } + aperture.push_back(newest); + frames = std::move(kept); + const std::size_t centre_index = + aperture.size() > settings.frame_radius ? aperture.size() - 1U - settings.frame_radius : 0U; + const Frame& centre = *aperture[centre_index]; + + std::vector offsets_s{}; + for (const auto& member : aperture) { + offsets_s.push_back(member->center_s - centre.center_s); + } + const auto weights = frame_weights(offsets_s); + const Grid grid = scene_grid(settings); + const double cos_p = std::cos(settings.look_angle_rad); + const double sin_p = std::sin(settings.look_angle_rad); + std::vector> corners{}; // of the scene box, in radar axes + for (const double x : {grid.x.front(), grid.x.back()}) { + for (const double y : {grid.y.front(), grid.y.back()}) { + for (const double z : {grid.z.front(), grid.z.back()}) { + const double y0 = y - settings.ground_depth_m; + corners.push_back({x, y0 * cos_p + z * sin_p, -y0 * sin_p + z * cos_p}); } } } - entry.time_s = timing.valid ? timing.center_abs_s : collection_time_s(collection); + std::vector contributions{}; + double longest_path = 0.0; + for (std::size_t index = 0U; index < aperture.size(); ++index) { + // Horizontal travel since the centre frame, in radar axes (sar_frame_offsets). + const double travel = settings.speed_m_s * offsets_s[index]; + const auto shifted = [&](const std::array& antenna) { + return std::array{antenna[0], antenna[1] + travel * sin_p, antenna[2] + travel * cos_p}; + }; + for (const auto& tx : settings.tx) { + for (const auto& rx : settings.rx) { + const auto tx_now = shifted(tx); + const auto rx_now = shifted(rx); + for (const auto& corner : corners) { + longest_path = std::max(longest_path, distance(corner, tx_now) + distance(corner, rx_now)); + } + contributions.push_back({ + to_float(tx_now), to_float(rx_now), static_cast(weights[index]), + inverse_reference_ranges(tx, rx, settings), + }); + } + } + } + // A Tx+Rx path is convex, so the box corners bound it: samples beyond the + // longest path are never read and are not sent to the GPU. + const double samples_per_metre = 1.0 / (kWaveSpeed * newest->dt_s); + const std::size_t samples = + std::min(newest->samples, static_cast(std::ceil(longest_path * samples_per_metre)) + 2U); + std::vector> ascans{}; + for (const auto& member : aperture) { + const std::span> rows = member->ascans; + for (std::size_t offset = 0U; offset < rows.size(); offset += member->samples) { + const auto row = rows.subspan(offset, samples); + ascans.insert(ascans.end(), row.begin(), row.end()); + } + } - std::map> s21{}; - std::map> freq{}; - for (const auto& trace : collection.traces) { - const auto input_it = std::find(selection.inputs.begin(), selection.inputs.end(), trace.combo.input_pos); - const auto output_it = std::find(selection.outputs.begin(), selection.outputs.end(), trace.combo.output_pos); - if (input_it == selection.inputs.end() || output_it == selection.outputs.end()) { + const Oblique oblique = oblique_view(grid, settings.voxel_m); + auto images = gpu.project({ + .x = grid.x, + .y = grid.y, + .z = grid.z, + .pitch_rad = settings.look_angle_rad, + .ground_depth_m = settings.ground_depth_m, + .min_depth_m = settings.min_depth_m, + .max_depth_m = settings.max_depth_m, + .contributions = contributions, + .ascans = ascans, + .samples_per_ascan = samples, + .samples_per_metre = samples_per_metre, + .use_compensation = settings.use_compensation, + .comp_range_power = settings.comp_range_power, + .comp_weight_max = kCompRangeWeightMax, + .oblique = oblique.view, + }); + + // Normalization level and maximum over the Y planes clear of the window + // edges, where a constant-range ridge leaves the grid and would set a + // false maximum (sar_norm_reference_mask, sar_finish_volume, + // sar_global_max_index). The level is the volume maximum, so the + // published projections peak at 1. + const std::size_t nx = grid.x.size(); + const std::size_t nz = grid.z.size(); + float level = 0.0F; + std::size_t peak_x = 0U; + std::size_t peak_y = 0U; + for (std::size_t iy = 0U; iy < grid.y.size(); ++iy) { + if (grid.y[iy] < grid.y.front() + kNormEdgeMargin || grid.y[iy] > grid.y.back() - kNormEdgeMargin) { continue; } - const gpr::PairKey key{ - static_cast(output_it - selection.outputs.begin()), - static_cast(input_it - selection.inputs.begin()), - }; - auto& samples = s21[key]; - samples.clear(); - samples.reserve(trace.s21.size()); - for (const auto& sample : trace.s21) { - samples.emplace_back(static_cast(sample.re), static_cast(sample.im)); + for (std::size_t ix = 0U; ix < nx; ++ix) { + if (images.front[iy * nx + ix] > level) { + level = images.front[iy * nx + ix]; + peak_x = ix; + peak_y = iy; + } } - auto& axis = freq[key]; - axis.assign(trace.frequency_hz.begin(), trace.frequency_hz.end()); } - - if (s21.size() == selection.inputs.size() * selection.outputs.size()) { - gpr::TxWindows windows{}; - double acq_s = 0.0; - if (timing.valid) { - windows = std::move(timing.windows); - acq_s = timing.acq_s; - } else { - if (collection.capture_end_ns > collection.capture_start_ns) { - acq_s = static_cast(collection.capture_end_ns - collection.capture_start_ns) * 1e-9; - } - auto order = tx_order; - for (const auto output : geom.output_idx) { - if (std::find(order.begin(), order.end(), output) == order.end()) { - order.push_back(output); - } - } - windows = gpr::uniform_tx_windows(order, acq_s, motion.tx_per_segment); - } - try { - const auto timestamps = gpr::compute_pair_timestamps(windows, acq_s, tx_order, geom, motion); - entry.ascans = std::make_shared(gpr::preprocess_snapshot( - s21, freq, /*background=*/nullptr, motion, geom, bp, timestamps, bp.pair_normalize - )); - } catch (const std::exception& exc) { - // A malformed collection (e.g. empty band after a sweep change) - // is cached as a skipped frame instead of failing the aperture. - report_error(std::string{"preprocess_snapshot: "} + exc.what()); - entry.ascans = nullptr; + const auto side_row = std::span(images.side).subspan(peak_y * nz, nz); + const auto peak_z = static_cast(std::ranges::max_element(side_row) - side_row.begin()); + const float scale = level > 0.0F ? 1.0F / level : 0.0F; + for (auto* image : {&images.front, &images.side, &images.top, &images.oblique}) { + for (auto& value : *image) { + value *= scale; } } - auto [inserted, _] = ascan_cache.emplace(collection.collection_id, std::move(entry)); - while (ascan_cache.size() > kAscanCacheLimit) { - ascan_cache.erase(ascan_cache.begin()); + std::vector payloads{}; + payloads.push_back(image_payload("sar3d_front", grid.x, grid.y, std::move(images.front))); + payloads.push_back(image_payload("sar3d_side", grid.z, grid.y, std::move(images.side))); + payloads.push_back(image_payload("sar3d_top", grid.x, grid.z, std::move(images.top))); + payloads.push_back(image_payload("sar3d_oblique", oblique.u, oblique.v, std::move(images.oblique))); + std::vector basis(oblique.view.u.begin(), oblique.view.u.end()); + basis.insert(basis.end(), oblique.view.v.begin(), oblique.view.v.end()); + payloads.push_back(table_payload("sar3d_oblique_basis", 3U, std::move(basis))); + if (level > 0.0F) { + payloads.push_back(table_payload("sar3d_peak", 4U, {level, static_cast(grid.x[peak_x]), + static_cast(grid.y[peak_y]), + static_cast(grid.z[peak_z])})); } - return inserted->second; + return payloads; } void report_error(const std::string& message) { if (message == last_error) { - ++suppressed_errors; + ++repeated_errors; return; } - if (suppressed_errors > 0U) { - std::cerr << "sar3d: previous error repeated " << suppressed_errors << " times\n"; + if (repeated_errors > 0U) { + std::cerr << "sar3d: previous error repeated " << repeated_errors << " times\n"; } std::cerr << "sar3d: " << message << '\n'; last_error = message; - suppressed_errors = 0U; + repeated_errors = 0U; } }; @@ -490,187 +738,14 @@ auto Sar3dProcessor::process_collection( ipc::ResultCollection results{}; results.collection_id = collection.collection_id; results.monotonic_ns = collection.monotonic_ns; - try { - const auto selection = build_selection(run_config, live_config); - if (selection.inputs.empty() || selection.outputs.empty()) { - return results; - } - const auto geom = build_geometry(run_config, selection); - const auto bp = build_bp_config(live_config); - const auto motion = build_motion_config(live_config); - const auto sar = build_sar_config(live_config); - if (!(bp.max_depth > bp.min_depth) || !(bp.f_stop > bp.f_start)) { - return results; - } - - const auto ascan_fp = ascan_fingerprint(selection, geom, bp, motion); - if (ascan_fp != state_->ascan_fp) { - state_->ascan_cache.clear(); - state_->ascan_fp = ascan_fp; - } - - // Aperture window: the newest 2r+1 collections, current one last. - const std::size_t window_size = 2U * static_cast(sar.frame_radius) + 1U; - std::vector window{}; - window.reserve(window_size); - const std::size_t history_take = std::min(previous_collections.size(), window_size - 1U); - for (std::size_t index = previous_collections.size() - history_take; index < previous_collections.size(); ++index) { - window.push_back(&previous_collections[index]); - } - window.push_back(&collection); - - const auto tx_order = tx_order_full(run_config); - std::vector> ascan_sets{}; - std::vector frame_times{}; - ascan_sets.reserve(window.size()); - frame_times.reserve(window.size()); - for (const auto* entry_collection : window) { - const auto& entry = - state_->get_or_compute_ascans(*entry_collection, selection, geom, bp, motion, tx_order); - if (entry.ascans == nullptr) { - continue; - } - ascan_sets.push_back(entry.ascans); - frame_times.push_back(entry.time_s); - } - if (ascan_sets.empty()) { - return results; - } - - // aperture_volume requires one shared time axis; drop frames whose axis - // differs from the current collection's (e.g. after a sweep-grid change). - const auto& reference = *ascan_sets.back(); - for (std::size_t index = ascan_sets.size(); index-- > 0U;) { - const auto& candidate = *ascan_sets[index]; - if (candidate.n_fft != reference.n_fft || std::abs(candidate.t0 - reference.t0) > 1e-18 || - candidate.n_pairs != reference.n_pairs) { - ascan_sets.erase(ascan_sets.begin() + static_cast(index)); - frame_times.erase(frame_times.begin() + static_cast(index)); - } - } - if (ascan_sets.empty()) { - return results; - } - - // Symmetric aperture: the reference frame sits `radius` frames before the - // newest one, so the published volume lags the input by `radius` frames. - const std::size_t center_index = - ascan_sets.size() > static_cast(sar.frame_radius) - ? ascan_sets.size() - 1U - static_cast(sar.frame_radius) - : 0U; - const auto offsets = gpr::frame_offsets(frame_times, frame_times[center_index], motion); - const auto weights = gpr::frame_weights(offsets, sar.edge_frame_weight); - std::vector frames{}; - frames.reserve(ascan_sets.size()); - for (std::size_t index = 0U; index < ascan_sets.size(); ++index) { - frames.push_back(gpr::ApertureFrame{ascan_sets[index].get(), offsets[index], weights[index]}); - } - - const auto engine_fp = engine_fingerprint(geom, bp, motion, sar); - if (state_->engine == nullptr || state_->engine_fp != engine_fp) { - state_->engine = std::make_unique(geom, bp, motion, sar); - state_->engine_fp = engine_fp; - } - - const gpr::Grid grid = gpr::build_full_region_grid(sar); - const gpr::VolumeRaw raw = state_->engine->aperture_volume(frames, grid); - // Full notebook finalization: |acc/cnt|, depth gate, normalization - // without the Y-edge planes, Gaussian smoothing over (Z, X), clip. - const gpr::FinishedVolume finished = - gpr::finish_volume(raw, grid, motion.pitch_deg, bp, sar); - - if (!live_config.sar3d_volume_dump_dir.empty()) { - dump_volume(live_config.sar3d_volume_dump_dir, collection.collection_id, finished, grid); - } - - // Two projections match the legacy render conventions (X across, - // Z range/"вдаль", Y up): - // - top view `sar3d_proj_xz`: max over Y, axes X (horizontal) / Z; - // - bottom view `sar3d_proj_bottom`: vertical axis is always Y, the - // horizontal axis u is the ground direction rotated by - // `sar3d_bottom_view_deg` around Y: 0° = front view (u = X, max - // along Z), 90° = side view (u = Z, max along X), intermediate - // angles project along the rotated perpendicular. - const int nx = finished.NX; - const int ny = finished.NY; - const int nz = finished.NZ; - - const double theta_deg = std::clamp(static_cast(live_config.sar3d_bottom_view_deg), 0.0, 90.0); - const double theta_rad = theta_deg * M_PI / 180.0; - const double cos_t = std::cos(theta_rad); - const double sin_t = std::sin(theta_rad); - // Exact-axis fast paths keep the front/side views on the native grid. - const bool front_view = theta_deg <= 1e-9; - const bool side_view = theta_deg >= 90.0 - 1e-9; - std::vector u_axis{}; - if (front_view) { - u_axis = grid.x; - } else if (side_view) { - u_axis = grid.z; - } else { - const double u_a = grid.x.front() * cos_t + grid.z.front() * sin_t; - const double u_b = grid.x.front() * cos_t + grid.z.back() * sin_t; - const double u_c = grid.x.back() * cos_t + grid.z.front() * sin_t; - const double u_d = grid.x.back() * cos_t + grid.z.back() * sin_t; - const double u_min = std::min(std::min(u_a, u_b), std::min(u_c, u_d)); - const double u_max = std::max(std::max(u_a, u_b), std::max(u_c, u_d)); - u_axis.resize(static_cast(nx)); - for (int iu = 0; iu < nx; ++iu) { - u_axis[static_cast(iu)] = u_min + (u_max - u_min) * iu / static_cast(nx - 1); - } - } - const int nu = static_cast(u_axis.size()); - const double u_start = u_axis.front(); - const double inv_du = nu > 1 ? static_cast(nu - 1) / (u_axis.back() - u_start) : 0.0; - - // The volume is already normalized to ~[0, 1] by finish_volume, so the - // projections are published as-is; the scalar payload carries `level` - // (the absolute scale the volume was divided by). - std::vector projection_xz(static_cast(nz) * nx, 0.0F); - std::vector projection_bottom(static_cast(ny) * nu, 0.0F); - for (int iy = 0; iy < ny; ++iy) { - for (int iz = 0; iz < nz; ++iz) { - const std::size_t row = (static_cast(iy) * nz + iz) * nx; - const std::size_t bottom_row = static_cast(iy) * nu; - const double z_sin_t = grid.z[iz] * sin_t; - for (int ix = 0; ix < nx; ++ix) { - const float value = finished.volume[row + static_cast(ix)]; - auto& xz_cell = projection_xz[static_cast(iz) * nx + ix]; - xz_cell = std::max(xz_cell, value); - int bin = ix; - if (side_view) { - bin = iz; - } else if (!front_view) { - const double u = grid.x[ix] * cos_t + z_sin_t; - bin = static_cast(std::lround((u - u_start) * inv_du)); - bin = std::clamp(bin, 0, nu - 1); - } - auto& bottom_cell = projection_bottom[bottom_row + static_cast(bin)]; - bottom_cell = std::max(bottom_cell, value); - } - } - } - - results.collection_payloads.push_back( - build_image_payload("sar3d_proj_xz", grid.x, grid.z, std::move(projection_xz)) - ); - results.collection_payloads.push_back( - build_image_payload("sar3d_proj_bottom", u_axis, grid.y, std::move(projection_bottom)) - ); - ipc::ResultPayload peak_payload{}; - peak_payload.processing_name = "sar3d_peak"; - peak_payload.kind = ipc::ResultKind::ScalarF32; - peak_payload.scalar_value = static_cast(finished.level); - results.collection_payloads.push_back(std::move(peak_payload)); - } catch (const std::exception& exc) { - // A CUDA/NVRTC or data error must not kill the data_processor loop: - // publish an empty result and keep going. Repeated identical errors are - // logged once with a suppression count. - state_->report_error(exc.what()); - results.collection_payloads.clear(); + results.collection_payloads = + state_->render(collection, previous_collections, read_settings(run_config, live_config)); + } catch (const std::exception& error) { + // A data or CUDA error must not stop the data_processor loop: publish an + // empty result and keep going. + state_->report_error(error.what()); } - return results; } diff --git a/data_acq_and_processing/processing/tools/sar3d_parity_main.cpp b/data_acq_and_processing/processing/tools/sar3d_parity_main.cpp deleted file mode 100644 index e480e28..0000000 --- a/data_acq_and_processing/processing/tools/sar3d_parity_main.cpp +++ /dev/null @@ -1,349 +0,0 @@ -// Parity check of the sar3d processor against the reference notebook -// Multi_antenna_3D_clean_v1.ipynb. -// -// Feeds a preprocessed snapshot of an extracted replay dataset through -// Sar3dProcessor exactly the way the live pipeline would (geometry and combo -// order from the dataset's config_profile.json, per-trace capture windows from -// the snapshot's meta.json) and dumps the finished volume for comparison with -// the notebook's *_volume.npz export: -// -// sar3d_parity [--dump-dir ] -// python_app/scripts/sar3d_volume_compare.py /sar3d_volume_ -// -// contains config_profile.json and preprocessed//. -// Defaults mirror the parameters the reference volumes were built with -// (f 2..6 GHz, depth 2.5..8 m, speed 1.45 m/s, look angle 17.5 deg). -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "gpr/config.hpp" -#include "gpr/geometry.hpp" -#include "processing_live_config.hpp" -#include "run_config.hpp" -#include "sar3d_processor.hpp" -#include "shared_types.hpp" - -namespace { - -// Minimal numpy .npy loader (v1.0, 1-D complex128/complex64/float64/float32). -struct NpyArray { - std::vector f64{}; - std::vector> c128{}; -}; - -NpyArray load_npy(const std::string& path) { - std::ifstream in(path, std::ios::binary); - if (!in) { - throw std::runtime_error("cannot open " + path); - } - char magic[6]; - in.read(magic, 6); - std::uint8_t version[2]; - in.read(reinterpret_cast(version), 2); - std::uint16_t header_len = 0; - in.read(reinterpret_cast(&header_len), 2); - std::string header(header_len, '\0'); - in.read(header.data(), header_len); - const auto shape_pos = header.find("'shape':"); - const auto left = header.find('(', shape_pos); - const auto right = header.find(')', shape_pos); - const long count = std::stol(header.substr(left + 1, right - left - 1)); - - NpyArray array{}; - const auto has = [&header](const char* token) { return header.find(token) != std::string::npos; }; - if (has("c8")) { - array.c128.resize(count); - std::vector> tmp(count); - in.read(reinterpret_cast(tmp.data()), count * sizeof(std::complex)); - for (long i = 0; i < count; ++i) { - array.c128[i] = std::complex(tmp[i].real(), tmp[i].imag()); - } - } else if (has("c16")) { - array.c128.resize(count); - in.read(reinterpret_cast(array.c128.data()), count * sizeof(std::complex)); - } else if (has("f4")) { - array.f64.resize(count); - std::vector tmp(count); - in.read(reinterpret_cast(tmp.data()), count * sizeof(float)); - for (long i = 0; i < count; ++i) { - array.f64[i] = tmp[i]; - } - } else { - array.f64.resize(count); - in.read(reinterpret_cast(array.f64.data()), count * sizeof(double)); - } - return array; -} - -// Live config matching the reference notebook run (npz meta parameters). -// The default program grid is a fixed window (X -2..2, Z 0..8), while the -// notebook derives its bounds from the antenna span and the depth gate — so -// the notebook bounds are recomputed here and passed explicitly. -auto build_live_config(const radar::config::RunConfig& run_config) - -> radar::processing::ProcessingLiveConfig { - radar::processing::ProcessingLiveConfig live{}; - live.processor_mode = "sar3d"; - live.gpr_start_freq_mhz = 2000.0F; - live.gpr_stop_freq_mhz = 6000.0F; - live.gpr_min_depth_m = 2.5F; - live.gpr_max_depth_m = 8.0F; - live.gpr_speed_m_s = 1.45F; - live.gpr_look_angle_deg = 17.5F; - live.gpr_direction_sign = 1.0F; - live.gpr_apply_freq_phase_correction = true; - - gpr::GeometryConfig geom{}; - geom.input_idx.clear(); - geom.output_idx.clear(); - geom.rx_positions.clear(); - geom.tx_positions.clear(); - for (const auto& entry : run_config.gpr.rx_geometry) { - geom.input_idx.push_back(static_cast(entry.input_pos)); - geom.rx_positions.push_back({entry.x_m, entry.y_m, entry.z_m}); - } - for (const auto& entry : run_config.gpr.tx_geometry) { - geom.output_idx.push_back(static_cast(entry.output_pos)); - geom.tx_positions.push_back({entry.x_m, entry.y_m, entry.z_m}); - } - geom.finalize(); - gpr::BPConfig bp{}; - bp.min_depth = live.gpr_min_depth_m; - bp.max_depth = live.gpr_max_depth_m; - gpr::SARConfig sar{}; - double x_min = 0.0, x_max = 0.0, z_min = 0.0, z_max = 0.0; - gpr::notebook_grid_bounds(geom, bp, sar, live.gpr_look_angle_deg, /*x_margin_m=*/2.0, - x_min, x_max, z_min, z_max); - live.sar3d_x_min_m = x_min; - live.sar3d_x_max_m = x_max; - live.sar3d_z_min_m = z_min; - live.sar3d_z_max_m = z_max; - return live; -} - -auto load_collection( - const radar::config::RunConfig& run_config, - const std::string& root, - const std::string& snapshot, - std::uint64_t collection_id -) -> radar::ipc::PreprocessedCollection { - radar::ipc::PreprocessedCollection collection{}; - collection.collection_id = collection_id; - - // Per-trace capture windows from the snapshot's meta.json; the segmented - // timing model is reconstructed from them exactly like in the notebook. - std::map, std::pair> windows{}; - { - std::ifstream in(root + "/preprocessed/" + snapshot + "/meta.json"); - if (in) { - nlohmann::json meta{}; - in >> meta; - for (const auto& trace : meta.value("traces", nlohmann::json::array())) { - if (!trace.contains("capture_start_ns") || !trace.contains("capture_end_ns")) { - continue; - } - windows[{trace.at("input").get(), trace.at("output").get()}] = { - trace.at("capture_start_ns").get(), - trace.at("capture_end_ns").get(), - }; - } - } - } - - std::uint64_t first = 0U; - std::uint64_t last = 0U; - for (const auto& [key, window] : windows) { - if (first == 0U || window.first < first) { - first = window.first; - } - last = std::max(last, window.second); - } - if (first == 0U) { - // No meta labels: fall back to the `_ns<...>` folder suffix. - const auto ns_pos = snapshot.rfind("_ns"); - if (ns_pos == std::string::npos) { - throw std::runtime_error("snapshot has no meta.json and no _ns suffix: " + snapshot); - } - last = static_cast(std::stoull(snapshot.substr(ns_pos + 3))); - first = last; - } - collection.capture_start_ns = first; - collection.capture_end_ns = last; - collection.monotonic_ns = (first + last) / 2U; - - for (const auto& combo : run_config.run_combos) { - char path[512]; - std::snprintf(path, sizeof(path), "%s/preprocessed/%s/i%u_o%u_s21.npy", - root.c_str(), snapshot.c_str(), combo.input_pos, combo.output_pos); - const auto s21 = load_npy(path); - std::snprintf(path, sizeof(path), "%s/preprocessed/%s/i%u_o%u_freq.npy", - root.c_str(), snapshot.c_str(), combo.input_pos, combo.output_pos); - const auto freq = load_npy(path); - - radar::ipc::SweepTraceBlock trace{}; - trace.combo = combo; - trace.frequency_hz.assign(freq.f64.begin(), freq.f64.end()); - trace.s21.reserve(s21.c128.size()); - for (const auto& sample : s21.c128) { - trace.s21.push_back(radar::ipc::Complex32{ - .re = static_cast(sample.real()), - .im = static_cast(sample.imag()), - }); - } - trace.s11.resize(trace.s21.size()); - if (const auto found = windows.find({combo.input_pos, combo.output_pos}); found != windows.end()) { - trace.capture_start_ns = found->second.first; - trace.capture_end_ns = found->second.second; - } - collection.traces.push_back(std::move(trace)); - } - return collection; -} - -void print_projection_argmax( - const radar::ipc::ResultPayload& payload, - const char* col_axis_name, - const char* row_axis_name -) { - std::size_t argmax = 0U; - float best = -1.0F; - for (std::size_t index = 0U; index < payload.image_values.size(); ++index) { - if (payload.image_values[index] > best) { - best = payload.image_values[index]; - argmax = index; - } - } - const auto nx = payload.image_x_axis.size(); - const auto col = argmax % nx; - const auto row = argmax / nx; - std::printf( - "%s: max=%.6f at %s=%.1f cm, %s=%.1f cm\n", - payload.processing_name.c_str(), - static_cast(best), - col_axis_name, - static_cast(payload.image_x_axis[col]) * 100.0, - row_axis_name, - static_cast(payload.image_y_axis[row]) * 100.0 - ); -} - -} // namespace - -int main(int argc, char** argv) { - if (argc < 3) { - std::printf("usage: %s [--dump-dir ]\n", argv[0]); - return 1; - } - const std::string root = argv[1]; - const std::string center = argv[2]; - std::string dump_dir{}; - for (int index = 3; index + 1 < argc; ++index) { - if (std::string{argv[index]} == "--dump-dir") { - dump_dir = argv[index + 1]; - } - } - - const std::string preprocessed = root + "/preprocessed"; - std::vector snapshots{}; - if (DIR* dir = opendir(preprocessed.c_str())) { - for (dirent* entry = nullptr; (entry = readdir(dir)) != nullptr;) { - const std::string name = entry->d_name; - if (!name.empty() && (std::isdigit(name[0]) != 0)) { - snapshots.push_back(name); - } - } - closedir(dir); - } - std::sort(snapshots.begin(), snapshots.end()); - const auto center_it = std::find(snapshots.begin(), snapshots.end(), center); - if (center_it == snapshots.end()) { - std::printf("center %s not found in %s\n", center.c_str(), preprocessed.c_str()); - return 1; - } - const auto center_index = static_cast(center_it - snapshots.begin()); - - const auto run_config = radar::config::load_run_config(root + "/config_profile.json"); - if (run_config.gpr.tx_geometry.empty() || run_config.gpr.rx_geometry.empty()) { - std::printf("FAIL: config_profile.json has no gpr geometry\n"); - return 1; - } - - auto live = build_live_config(run_config); - live.sar3d_volume_dump_dir = dump_dir; - const auto radius = static_cast(live.sar3d_frame_radius); - if (center_index < radius || center_index + radius >= snapshots.size()) { - std::printf("not enough snapshots around center for radius %zu\n", radius); - return 1; - } - - // The processor centers the aperture `radius` frames before the newest - // collection, so feeding [center-r .. center+r] in order reproduces the - // symmetric aperture around `center`. - std::vector history{}; - for (std::size_t index = center_index - radius; index <= center_index + radius; ++index) { - history.push_back(load_collection(run_config, root, snapshots[index], index)); - } - std::printf("aperture: %zu frames around %s\n", history.size(), center.c_str()); - - radar::processing::Sar3dProcessor processor{}; - radar::ipc::ResultCollection result{}; - for (std::size_t index = 0U; index < history.size(); ++index) { - result = processor.process_collection( - run_config, - history[index], - std::span(history.data(), index), - live - ); - } - - if (result.collection_payloads.empty()) { - std::printf("FAIL: sar3d produced no payloads\n"); - return 1; - } - for (const auto& payload : result.collection_payloads) { - if (payload.kind == radar::ipc::ResultKind::ScalarF32) { - std::printf("%s (volume level): %.9e\n", - payload.processing_name.c_str(), static_cast(payload.scalar_value)); - } - } - for (const auto& payload : result.collection_payloads) { - if (payload.kind == radar::ipc::ResultKind::ImageF32 && payload.processing_name == "sar3d_proj_xz") { - print_projection_argmax(payload, "X", "Z"); - } - } - - // Bottom view across the selectable angles: the target's u coordinate must - // move smoothly from its X (front, 0 deg) to its Z (side, 90 deg). - const auto last_previous = - std::span(history.data(), history.size() - 1U); - for (const float angle_deg : {0.0F, 30.0F, 60.0F, 90.0F}) { - auto live_angle = live; - live_angle.sar3d_volume_dump_dir.clear(); - live_angle.sar3d_bottom_view_deg = angle_deg; - const auto angle_result = - processor.process_collection(run_config, history.back(), last_previous, live_angle); - for (const auto& payload : angle_result.collection_payloads) { - if (payload.kind == radar::ipc::ResultKind::ImageF32 && payload.processing_name == "sar3d_proj_bottom") { - std::printf("bottom @ %2.0f deg -> ", static_cast(angle_deg)); - print_projection_argmax(payload, "u", "Y"); - } - } - } - if (!dump_dir.empty()) { - std::printf("volume dumped to %s/sar3d_volume_%llu.{f32,json}\n", - dump_dir.c_str(), - static_cast(history.back().collection_id)); - } - std::printf("OK: sar3d processor path completed\n"); - return 0; -} diff --git a/python_app/gui/app_window.py b/python_app/gui/app_window.py index 5ca02b0..71078a9 100644 --- a/python_app/gui/app_window.py +++ b/python_app/gui/app_window.py @@ -371,9 +371,6 @@ class AppWindow( self._gpr_region_contours = [] self._gpr_geometry_signature = None self._gpr_selected_geometry = None - self._sar3d_lookup_table = None - self._sar3d_xz_image_item = None - self._sar3d_xy_image_item = None self._phase_viewbox = None self._history_run_signature = None self._processor_run_signature = None diff --git a/python_app/gui/controllers/app_window_config/live_processing_mixin.py b/python_app/gui/controllers/app_window_config/live_processing_mixin.py index b5745fd..7fa1d06 100644 --- a/python_app/gui/controllers/app_window_config/live_processing_mixin.py +++ b/python_app/gui/controllers/app_window_config/live_processing_mixin.py @@ -13,7 +13,6 @@ from PyQt6.QtWidgets import ( ) from python_app.gui.runtime.constraints import validate_processing_mode_constraints -from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label from python_app.orchestration.live_processing_config import ProcessingLiveConfig _GPR_MODES = ("gpr", "legacy_gpr") @@ -83,32 +82,17 @@ _WEB_LIVE_SCHEMA = [ ("gpr_visible_z_max_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_max_m", {"legacy_gpr": "_legacy_gpr_visible_z_max_m"})), ("gpr_motion_mode", "Motion", ("gpr",), _attr("_gpr_motion_mode")), ("gpr_look_angle_deg", "Motion", _GPR_LIKE_MODES, _dual("_gpr_look_angle_deg", {"legacy_gpr": "_legacy_gpr_look_angle_deg", "sar3d": "_sar3d_look_angle_deg"})), - ("gpr_direction_sign", "Motion", ("gpr", "sar3d"), _dual("_gpr_direction_sign", {"sar3d": "_sar3d_direction_sign"})), - ("gpr_apply_freq_phase_correction", "Motion", ("legacy_gpr", "sar3d"), _dual("_legacy_gpr_apply_freq_phase_correction", {"sar3d": "_sar3d_apply_freq_phase_correction"})), + ("gpr_direction_sign", "Motion", ("gpr",), _attr("_gpr_direction_sign")), + ("gpr_apply_freq_phase_correction", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_apply_freq_phase_correction")), ("gpr_reference_mode", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_reference_mode")), ("ignore_socket_speed", "Motion", _GPR_LIKE_MODES, _dual("_gpr_ignore_socket_speed_enabled", {"legacy_gpr": "_legacy_gpr_ignore_socket_speed_enabled", "sar3d": "_sar3d_ignore_socket_speed_enabled"})), ("gpr_speed_m_s", "Motion", _GPR_LIKE_MODES, _dual("_gpr_speed_m_s", {"legacy_gpr": "_legacy_gpr_speed_m_s", "sar3d": "_sar3d_speed_m_s"})), ("sar3d_frame_radius", "3D SAR", ("sar3d",), _attr("_sar3d_frame_radius")), - ("sar3d_edge_frame_weight", "3D SAR", ("sar3d",), _attr("_sar3d_edge_frame_weight")), - ("sar3d_pitch_override_enabled", "3D SAR", ("sar3d",), _attr("_sar3d_pitch_override_enabled")), - ("sar3d_pitch_deg", "3D SAR", ("sar3d",), _attr("_sar3d_pitch_deg")), - ("sar3d_ground_depth_m", "3D SAR", ("sar3d",), _attr("_sar3d_ground_depth_m")), - ("sar3d_x_min_m", "3D SAR", ("sar3d",), _attr("_sar3d_x_min_m")), - ("sar3d_x_max_m", "3D SAR", ("sar3d",), _attr("_sar3d_x_max_m")), - ("sar3d_y_min_m", "3D SAR", ("sar3d",), _attr("_sar3d_y_min_m")), - ("sar3d_y_max_m", "3D SAR", ("sar3d",), _attr("_sar3d_y_max_m")), - ("sar3d_z_min_m", "3D SAR", ("sar3d",), _attr("_sar3d_z_min_m")), - ("sar3d_z_max_m", "3D SAR", ("sar3d",), _attr("_sar3d_z_max_m")), - ("sar3d_voxel_x_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_x_m")), - ("sar3d_voxel_y_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_y_m")), - ("sar3d_voxel_z_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_z_m")), - ("sar3d_norm_edge_margin_m", "3D SAR", ("sar3d",), _attr("_sar3d_norm_edge_margin_m")), - ("sar3d_smooth_sigma", "3D SAR", ("sar3d",), _attr("_sar3d_smooth_sigma")), + ("sar3d_voxel_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_m")), + ("sar3d_bp_ground_depth_m", "3D SAR", ("sar3d",), _attr("_sar3d_bp_ground_depth_m")), ("sar3d_comp_range_power", "3D SAR", ("sar3d",), _attr("_sar3d_comp_range_power")), - ("sar3d_tx_per_segment", "3D SAR", ("sar3d",), _attr("_sar3d_tx_per_segment")), ("sar3d_use_compensation", "3D SAR", ("sar3d",), _attr("_sar3d_use_compensation")), - ("sar3d_normalize_pairs", "3D SAR", ("sar3d",), _attr("_sar3d_normalize_pairs")), - ("sar3d_bottom_view_deg", "3D SAR", ("sar3d",), _attr("_sar3d_bottom_view")), + ("sar3d_normalize_each_frame", "3D SAR", ("sar3d",), _attr("_sar3d_normalize_each_frame")), ] _WEB_LIVE_GETTERS = {field: getter for field, _group, _modes, getter in _WEB_LIVE_SCHEMA} @@ -259,7 +243,7 @@ class AppWindowLiveProcessingMixin: :meth:`_is_gpr_processing_mode`; this predicate gates only the shared behaviors (result drain on settings changes, geometry page visibility). """ - return mode in set(_GPR_LIKE_MODES) + return mode in _GPR_LIKE_MODES def _validate_processing_mode_selection(self, mode: str) -> None: """Validate requested processing mode against current stable/live GUI state.""" @@ -295,10 +279,6 @@ class AppWindowLiveProcessingMixin: # Switch-position CSV text -> integer lists. values["gpr_input_positions"] = self._parse_csv_int_list(values["gpr_input_positions"]) values["gpr_output_positions"] = self._parse_csv_int_list(values["gpr_output_positions"]) - # Bottom-view combo label -> projection angle in degrees. - values["sar3d_bottom_view_deg"] = sar3d_bottom_view_deg_from_label( - str(values["sar3d_bottom_view_deg"]) - ) # Order-normalize the pass-through Y range so a reversed entry still works. y_min_db, y_max_db = values["pass_through_y_min_db"], values["pass_through_y_max_db"] values["pass_through_y_min_db"] = min(y_min_db, y_max_db) @@ -424,13 +404,23 @@ class AppWindowLiveProcessingMixin: except Exception as exc: # noqa: BLE001 self._show_exception("Failed to update live processing settings", exc) - def _on_legacy_gpr_ignore_socket_speed_toggled(self, ignore_socket_speed: bool) -> None: - """Reflect socket/manual speed authority in the GUI and live config.""" - try: - self._legacy_gpr_speed_m_s.setEnabled(bool(ignore_socket_speed)) - self._write_live_processing_config() - except Exception as exc: # noqa: BLE001 - self._show_exception("Failed to update socket-speed mode", exc) + def _sync_socket_speed_controls(self) -> None: + """Enable each GPR-like speed field only while its page ignores the socket speed. + + Unchecked, the speed a locator client streams drives the processing and the + field only stands in while no client sends one, so it must not look editable. + """ + for checkbox, speed in ( + (self._gpr_ignore_socket_speed_enabled, self._gpr_speed_m_s), + (self._legacy_gpr_ignore_socket_speed_enabled, self._legacy_gpr_speed_m_s), + (self._sar3d_ignore_socket_speed_enabled, self._sar3d_speed_m_s), + ): + speed.setEnabled(checkbox.isChecked()) + + def _on_ignore_socket_speed_toggled(self, *_args) -> None: + """Switch the speed authority between the socket client and the GUI field.""" + self._sync_socket_speed_controls() + self._on_processing_live_settings_changed() def _on_gpr_visual_settings_changed(self, *_args) -> None: """Redraw current GPR result using updated GUI-only render settings.""" @@ -556,13 +546,13 @@ class AppWindowLiveProcessingMixin: elif mode == "sar3d": self._log( "Processing mode selected: sar3d " - f"(inputs={self._sar3d_input_positions_input.text().strip() or ''}, " - f"outputs={self._sar3d_output_positions_input.text().strip() or ''}, " - f"depth={self._sar3d_min_depth_m.value():g}..{self._sar3d_max_depth_m.value():g} m, " - f"freq={self._sar3d_start_freq_mhz.value():g}..{self._sar3d_stop_freq_mhz.value():g} MHz, " - f"frame_radius={self._sar3d_frame_radius.value()}, " - f"voxel={self._sar3d_voxel_x_m.value():g}x{self._sar3d_voxel_y_m.value():g}x{self._sar3d_voxel_z_m.value():g} m, " - f"ground_depth={self._sar3d_ground_depth_m.value():g} m)" + f"(INPUT_IDX={self._sar3d_input_positions_input.text().strip() or ''}, " + f"OUTPUT_IDX={self._sar3d_output_positions_input.text().strip() or ''}, " + f"F={self._sar3d_start_freq_mhz.value():g}..{self._sar3d_stop_freq_mhz.value():g} MHz, " + f"DEPTH={self._sar3d_min_depth_m.value():g}..{self._sar3d_max_depth_m.value():g} m, " + f"SAR_VOXEL_M={self._sar3d_voxel_m.value():g}, " + f"LOOK_ANGLE_DEG={self._sar3d_look_angle_deg.value():g}, " + f"SAR_FRAME_RADIUS={self._sar3d_frame_radius.value()})" ) def _clear_history_mode_caches(self) -> None: @@ -574,7 +564,7 @@ class AppWindowLiveProcessingMixin: self._clear_trace_plots() if hasattr(self, "_gpr_plot"): self._clear_gpr_plot() - if hasattr(self, "_sar3d_xz_plot"): + if hasattr(self, "_sar3d_plots"): self._clear_sar3d_plot() def _redraw_after_history_deletion(self) -> None: diff --git a/python_app/gui/controllers/app_window_config/profile_io_mixin.py b/python_app/gui/controllers/app_window_config/profile_io_mixin.py index 2c84d8d..4de81ca 100644 --- a/python_app/gui/controllers/app_window_config/profile_io_mixin.py +++ b/python_app/gui/controllers/app_window_config/profile_io_mixin.py @@ -10,7 +10,6 @@ from pathlib import Path from PyQt6.QtCore import QSignalBlocker from PyQt6.QtWidgets import QFileDialog -from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_label_from_deg from python_app.models.gui_profile_model import GuiProfileModel from python_app.models.run_config_model import RunConfigModel from python_app.orchestration.preprocess_assets import ( @@ -346,28 +345,12 @@ class AppWindowConfigProfileIOMixin: self._sar3d_speed_m_s, self._sar3d_ignore_socket_speed_enabled, self._sar3d_look_angle_deg, - self._sar3d_direction_sign, - self._sar3d_apply_freq_phase_correction, self._sar3d_frame_radius, - self._sar3d_edge_frame_weight, - self._sar3d_pitch_override_enabled, - self._sar3d_pitch_deg, - self._sar3d_ground_depth_m, - self._sar3d_x_min_m, - self._sar3d_x_max_m, - self._sar3d_y_min_m, - self._sar3d_y_max_m, - self._sar3d_z_min_m, - self._sar3d_z_max_m, - self._sar3d_voxel_x_m, - self._sar3d_voxel_y_m, - self._sar3d_voxel_z_m, - self._sar3d_norm_edge_margin_m, - self._sar3d_smooth_sigma, + self._sar3d_voxel_m, + self._sar3d_bp_ground_depth_m, self._sar3d_comp_range_power, - self._sar3d_tx_per_segment, self._sar3d_use_compensation, - self._sar3d_normalize_pairs, + self._sar3d_normalize_each_frame, self._save_count, self._record_count, self._save_path_input, @@ -532,11 +515,9 @@ class AppWindowConfigProfileIOMixin: self._legacy_gpr_start_freq_mhz.setValue(float(gui_state.processing.legacy_gpr.start_freq_mhz)) self._legacy_gpr_stop_freq_mhz.setValue(float(gui_state.processing.legacy_gpr.stop_freq_mhz)) self._legacy_gpr_speed_m_s.setValue(float(gui_state.processing.legacy_gpr.speed_m_s)) - ignore_socket_speed_enabled = bool( - gui_state.processing.legacy_gpr.ignore_socket_speed_enabled + self._legacy_gpr_ignore_socket_speed_enabled.setChecked( + bool(gui_state.processing.legacy_gpr.ignore_socket_speed_enabled) ) - self._legacy_gpr_ignore_socket_speed_enabled.setChecked(ignore_socket_speed_enabled) - self._legacy_gpr_speed_m_s.setEnabled(ignore_socket_speed_enabled) self._legacy_gpr_look_angle_deg.setValue(float(gui_state.processing.legacy_gpr.look_angle_deg)) self._legacy_gpr_apply_freq_phase_correction.setChecked( bool(gui_state.processing.legacy_gpr.apply_freq_phase_correction) @@ -566,32 +547,13 @@ class AppWindowConfigProfileIOMixin: self._sar3d_speed_m_s.setValue(float(sar3d_state.speed_m_s)) self._sar3d_ignore_socket_speed_enabled.setChecked(bool(sar3d_state.ignore_socket_speed_enabled)) self._sar3d_look_angle_deg.setValue(float(sar3d_state.look_angle_deg)) - self._sar3d_direction_sign.setValue(float(sar3d_state.direction_sign)) - self._sar3d_apply_freq_phase_correction.setChecked(bool(sar3d_state.apply_freq_phase_correction)) self._sar3d_frame_radius.setValue(int(sar3d_state.frame_radius)) - self._sar3d_edge_frame_weight.setValue(float(sar3d_state.edge_frame_weight)) - self._sar3d_pitch_override_enabled.setChecked(bool(sar3d_state.pitch_override_enabled)) - self._sar3d_pitch_deg.setValue(float(sar3d_state.pitch_deg)) - self._sar3d_ground_depth_m.setValue(float(sar3d_state.ground_depth_m)) - self._sar3d_x_min_m.setValue(float(sar3d_state.x_min_m)) - self._sar3d_x_max_m.setValue(float(sar3d_state.x_max_m)) - self._sar3d_y_min_m.setValue(float(sar3d_state.y_min_m)) - self._sar3d_y_max_m.setValue(float(sar3d_state.y_max_m)) - self._sar3d_z_min_m.setValue(float(sar3d_state.z_min_m)) - self._sar3d_z_max_m.setValue(float(sar3d_state.z_max_m)) - self._sar3d_voxel_x_m.setValue(float(sar3d_state.voxel_x_m)) - self._sar3d_voxel_y_m.setValue(float(sar3d_state.voxel_y_m)) - self._sar3d_voxel_z_m.setValue(float(sar3d_state.voxel_z_m)) - self._sar3d_norm_edge_margin_m.setValue(float(sar3d_state.norm_edge_margin_m)) - self._sar3d_smooth_sigma.setValue(float(sar3d_state.smooth_sigma)) + self._sar3d_voxel_m.setValue(float(sar3d_state.voxel_m)) + self._sar3d_bp_ground_depth_m.setValue(float(sar3d_state.bp_ground_depth_m)) self._sar3d_comp_range_power.setValue(float(sar3d_state.comp_range_power)) - self._sar3d_tx_per_segment.setValue(int(sar3d_state.tx_per_segment)) self._sar3d_use_compensation.setChecked(bool(sar3d_state.use_compensation)) - self._sar3d_normalize_pairs.setChecked(bool(sar3d_state.normalize_pairs)) - self._set_combo_current_text( - self._sar3d_bottom_view, - sar3d_bottom_view_label_from_deg(float(sar3d_state.bottom_view_deg)), - ) + self._sar3d_normalize_each_frame.setChecked(bool(sar3d_state.normalize_each_frame)) + self._sync_socket_speed_controls() self._save_count.setValue(int(gui_state.data_actions.save_count)) self._record_count.setValue(int(gui_state.data_actions.record_count)) diff --git a/python_app/gui/controllers/app_window_config/state_builders.py b/python_app/gui/controllers/app_window_config/state_builders.py index f9a47ee..b7ba641 100644 --- a/python_app/gui/controllers/app_window_config/state_builders.py +++ b/python_app/gui/controllers/app_window_config/state_builders.py @@ -25,7 +25,6 @@ from python_app.orchestration.preprocess_assets import ( VISIBLE_PREPROCESS_ASSET_KEYS, preprocess_asset_model, ) -from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label from python_app.storage.npz_store import radar_key_from_config @@ -434,31 +433,12 @@ class AppWindowConfigStateBuildersMixin: speed_m_s=float(self._sar3d_speed_m_s.value()), ignore_socket_speed_enabled=bool(self._sar3d_ignore_socket_speed_enabled.isChecked()), look_angle_deg=float(self._sar3d_look_angle_deg.value()), - direction_sign=float(self._sar3d_direction_sign.value()), - apply_freq_phase_correction=bool(self._sar3d_apply_freq_phase_correction.isChecked()), frame_radius=int(self._sar3d_frame_radius.value()), - edge_frame_weight=float(self._sar3d_edge_frame_weight.value()), - pitch_override_enabled=bool(self._sar3d_pitch_override_enabled.isChecked()), - pitch_deg=float(self._sar3d_pitch_deg.value()), - ground_depth_m=float(self._sar3d_ground_depth_m.value()), - x_min_m=float(self._sar3d_x_min_m.value()), - x_max_m=float(self._sar3d_x_max_m.value()), - y_min_m=float(self._sar3d_y_min_m.value()), - y_max_m=float(self._sar3d_y_max_m.value()), - z_min_m=float(self._sar3d_z_min_m.value()), - z_max_m=float(self._sar3d_z_max_m.value()), - voxel_x_m=float(self._sar3d_voxel_x_m.value()), - voxel_y_m=float(self._sar3d_voxel_y_m.value()), - voxel_z_m=float(self._sar3d_voxel_z_m.value()), - norm_edge_margin_m=float(self._sar3d_norm_edge_margin_m.value()), - smooth_sigma=float(self._sar3d_smooth_sigma.value()), + voxel_m=float(self._sar3d_voxel_m.value()), + bp_ground_depth_m=float(self._sar3d_bp_ground_depth_m.value()), comp_range_power=float(self._sar3d_comp_range_power.value()), - tx_per_segment=int(self._sar3d_tx_per_segment.value()), use_compensation=bool(self._sar3d_use_compensation.isChecked()), - normalize_pairs=bool(self._sar3d_normalize_pairs.isChecked()), - bottom_view_deg=sar3d_bottom_view_deg_from_label( - self._sar3d_bottom_view.currentText() - ), + normalize_each_frame=bool(self._sar3d_normalize_each_frame.isChecked()), ), ), data_actions=GuiDataActionsStateModel( diff --git a/python_app/gui/controllers/app_window_plot/sar3d_plot_mixin.py b/python_app/gui/controllers/app_window_plot/sar3d_plot_mixin.py index 4710124..35ab550 100644 --- a/python_app/gui/controllers/app_window_plot/sar3d_plot_mixin.py +++ b/python_app/gui/controllers/app_window_plot/sar3d_plot_mixin.py @@ -1,162 +1,158 @@ -"""3D SAR projection rendering helpers. +"""3D SAR views of the volume published by the sar3d processor. -The sar3d processor publishes two normalized max-projections of the imaged -volume as collection-level ImageF32 payloads, matching the legacy render -conventions (X across, Z range/"вдаль", Y up): +Scene axes: X across, Y height above the ground (Y = 0 is the ground), Z along +the ground away from the radar. Payloads, the volume normalized to a peak of 1: -- ``sar3d_proj_xz`` — top view: X (m) horizontal, Z/range (m) vertical - (max over the height Y), like the classic GPR heatmap; -- ``sar3d_proj_bottom`` — switchable bottom view: height Y (m) vertical, the - horizontal axis is the ground direction rotated by ``sar3d_bottom_view_deg`` - around Y (0° = front view X, 90° = side view Z, 30°/60° in between); - -plus ``sar3d_peak`` (ScalarF32), the volume peak before normalization. Both -projections are drawn on their own PlotWidgets stacked in one plot-stack page -(top view on top), mirroring how the pass-through page stacks its two plots. +- ``sar3d_front``, ``sar3d_side``, ``sar3d_top``: maximum projections onto X-Y + (looking ahead from the radar), Z-Y (from the side) and X-Z (from above); +- ``sar3d_oblique``: orthographic projection along the camera of the notebook's + 3D scene; ``sar3d_oblique_basis`` (2x3) gives its image axes u and v as scene + directions, which places the scene box, the ground and the maximum on it; +- ``sar3d_peak``: [level, X, Y, Z] of the volume maximum before normalization. """ from __future__ import annotations -from PyQt6.QtCore import QRectF +import itertools + import numpy as np import pyqtgraph as pg +from PyQt6.QtCore import QRectF, Qt -from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label +from python_app.gui.controllers.app_window_plot.bscan_plot_mixin import build_lut from python_app.models.dataset_model import ResultCollection -# Fraction of the projection maximum below which the heatmap is not highlighted -# (same as cloud-threshold=0.75 in the VNA_AlGo 3D SAR animation): the color -# scale is stretched over [frac, 1], everything below falls into the darkest -# LUT color. -HEATMAP_DISPLAY_THRESHOLD_FRAC = 0.75 +# Colours start at this fraction of the maximum; weaker voxels stay transparent, +# like those under the threshold of the notebook's point cloud. +DISPLAY_THRESHOLD = 0.75 + +# payload -> (title, x label, y label, scene axes (0 = X, 1 = Y, 2 = Z) along the plot axes) +SAR3D_VIEWS: dict[str, tuple[str, str, str, tuple[int, int] | None]] = { + "sar3d_front": ("Front (X-Y)", "X", "Y", (0, 1)), + "sar3d_side": ("Side (Z-Y)", "Z", "Y", (2, 1)), + "sar3d_top": ("Top (X-Z)", "X", "Z", (0, 2)), + "sar3d_oblique": ("3D", "", "", None), +} + +# Corners of the scene box indexed by bits (x, y, z); an edge joins corners one bit apart. +_BOX_EDGES = [(a, b) for a, b in itertools.combinations(range(8), 2) if bin(a ^ b).count("1") == 1] +# Axis names go on the edges meeting at the bottom corner nearest the camera (X max, Y min, Z min). +_AXIS_EDGES = {"X": (0, 1), "Y": (1, 3), "Z": (1, 5)} + + +def _image_rect(x_axis: np.ndarray, y_axis: np.ndarray) -> QRectF: + """Rectangle covering the pixels centred on the axis samples.""" + dx = (x_axis[-1] - x_axis[0]) / max(len(x_axis) - 1, 1) + dy = (y_axis[-1] - y_axis[0]) / max(len(y_axis) - 1, 1) + return QRectF(x_axis[0] - dx / 2, y_axis[0] - dy / 2, x_axis[-1] - x_axis[0] + dx, y_axis[-1] - y_axis[0] + dy) class AppWindowSar3dPlotMixin: - """Renders 3D SAR volume projections published by the sar3d processor.""" + """Draws the 3D SAR views.""" - def _sar3d_bottom_view_labels(self) -> tuple[str, str]: - """Return (plot title, horizontal axis label) for the active bottom view. + def _init_sar3d_views(self) -> None: + """Create the persistent items of the four 3D SAR plots.""" + lut = build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"]) # the GPR palette + lut = np.column_stack([lut, np.full(len(lut), 255, dtype=np.uint8)]) + lut[0, 3] = 0 # below DISPLAY_THRESHOLD + self._sar3d_images = {} + self._sar3d_peaks = {} + for name, (_title, x_label, y_label, _axes) in SAR3D_VIEWS.items(): + plot = self._sar3d_plots[name] + self._disable_gpr_plot_interaction(plot, plot.getViewBox()) + plot.getViewBox().enableAutoRange(x=False, y=False) + image = pg.ImageItem(axisOrder="row-major", lut=lut) + plot.addItem(image) + # Hollow, so the response at the maximum stays visible. + peak = pg.ScatterPlotItem(symbol="star", size=18, pen=pg.mkPen("w", width=1.5), brush=None) + peak.setZValue(3) + plot.addItem(peak) + self._sar3d_images[name] = image + self._sar3d_peaks[name] = peak + if x_label: + plot.showGrid(x=True, y=True, alpha=0.2) + plot.setLabel("bottom", x_label, units="m") + plot.setLabel("left", y_label, units="m") + if y_label == "Y": + ground = pg.InfiniteLine(pos=0.0, angle=0, pen=pg.mkPen("w", width=1, style=Qt.PenStyle.DashLine)) + ground.setZValue(2) + plot.addItem(ground) - The plot surfaces are created before the processing section builds the - bottom-view combo, so fall back to the front view until it exists. - """ - combo = getattr(self, "_sar3d_bottom_view", None) - degrees = sar3d_bottom_view_deg_from_label(combo.currentText()) if combo is not None else 0.0 - if degrees <= 1e-9: - return "3D SAR front view (X-Y)", "X" - if degrees >= 90.0 - 1e-9: - return "3D SAR side view (Z-Y)", "Range Z" - return f"3D SAR view @ {degrees:g}\N{DEGREE SIGN}", f"u({degrees:g}\N{DEGREE SIGN})" - - def _configure_sar3d_plot_axes(self) -> None: - """Apply persistent axis labels and view settings to both projection plots.""" - _bottom_title, bottom_x_label = self._sar3d_bottom_view_labels() - for plot, x_label, y_label in ( - (self._sar3d_xz_plot, "X", "Range Z"), - (self._sar3d_xy_plot, bottom_x_label, "Height Y"), - ): - plot_item = plot.getPlotItem() - plot_item.showAxis("left", show=True) - plot_item.showAxis("bottom", show=True) - plot_item.setClipToView(True) - plot.setLabel("bottom", x_label, units="m") - plot.setLabel("left", y_label, units="m") - view_box = plot.getViewBox() - view_box.invertY(False) - view_box.enableAutoRange(x=False, y=False) - self._disable_gpr_plot_interaction(plot, view_box) - - def _ensure_sar3d_plot_items(self) -> None: - """Create persistent projection image items once and reuse them on redraw.""" - if self._sar3d_xz_image_item is not None: - return - - self._configure_sar3d_plot_axes() - if self._sar3d_lookup_table is None: - # Same palette as the GPR accumulator so both imagers read alike. - self._sar3d_lookup_table = self._build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"]) - - self._sar3d_xz_image_item = pg.ImageItem(axisOrder="row-major") - self._sar3d_xz_image_item.setZValue(0) - self._sar3d_xz_image_item.hide() - self._sar3d_xz_plot.addItem(self._sar3d_xz_image_item) - - self._sar3d_xy_image_item = pg.ImageItem(axisOrder="row-major") - self._sar3d_xy_image_item.setZValue(0) - self._sar3d_xy_image_item.hide() - self._sar3d_xy_plot.addItem(self._sar3d_xy_image_item) + oblique = self._sar3d_plots["sar3d_oblique"] + oblique.hideAxis("bottom") + oblique.hideAxis("left") + oblique.setAspectLocked(True) + # The notebook scene's ground, under the image so it shows through weak voxels. + self._sar3d_ground = pg.PlotCurveItem( + pen=pg.mkPen(115, 105, 88), brush=pg.mkBrush(115, 105, 88, 110), fillLevel="enclosed" + ) + self._sar3d_ground.setZValue(-1) + oblique.addItem(self._sar3d_ground) + self._sar3d_box = pg.PlotCurveItem(pen=pg.mkPen(190, 190, 190, 170), connect="pairs") + self._sar3d_box.setZValue(2) + oblique.addItem(self._sar3d_box) + self._sar3d_axis_labels = {} + for axis_name in _AXIS_EDGES: + label = pg.TextItem(axis_name, color=(190, 190, 190), anchor=(0.5, 0.5)) + oblique.addItem(label) + self._sar3d_axis_labels[axis_name] = label def _clear_sar3d_plot(self) -> None: - """Clear both 3D SAR projection surfaces.""" - if not hasattr(self, "_sar3d_xz_plot"): - return - self._configure_sar3d_plot_axes() - if self._sar3d_xz_image_item is not None: - self._sar3d_xz_image_item.hide() - if self._sar3d_xy_image_item is not None: - self._sar3d_xy_image_item.hide() - bottom_title, _bottom_x_label = self._sar3d_bottom_view_labels() - self._sar3d_xz_plot.setTitle("3D SAR top view (X-Z)") - self._sar3d_xy_plot.setTitle(bottom_title) - - def _draw_sar3d_projection(self, plot: pg.PlotWidget, image_item: pg.ImageItem, payload) -> bool: - """Draw one normalized projection payload onto its plot; return success.""" - image = np.asarray(payload.image, dtype=np.float32) - x_axis = np.asarray(payload.image_x_axis, dtype=np.float32) - y_axis = np.asarray(payload.image_y_axis, dtype=np.float32) - if image.ndim != 2 or image.size == 0 or x_axis.size == 0 or y_axis.size == 0: - image_item.hide() - return False - - x_min = float(x_axis[0]) - x_max = float(x_axis[-1]) - y_min = float(y_axis[0]) - y_max = float(y_axis[-1]) - rect = QRectF(x_min, y_min, max(x_max - x_min, 1e-6), max(y_max - y_min, 1e-6)) - - image_item.setImage(image, autoLevels=False) - image_item.setRect(rect) - image_item.setLookupTable(self._sar3d_lookup_table) - # Projections are normalized to [0, 1] by the processor; stretch the - # colors over [threshold, 1] like the 3D animation cloud cutoff. - image_item.setLevels((HEATMAP_DISPLAY_THRESHOLD_FRAC, 1.0)) - image_item.show() - plot.setXRange(x_min, x_max, padding=0.0) - plot.setYRange(y_min, y_max, padding=0.0) - return True + """Clear all four 3D SAR views.""" + for name, (title, *_rest) in SAR3D_VIEWS.items(): + self._sar3d_images[name].hide() + self._sar3d_peaks[name].setData([], []) + self._sar3d_plots[name].setTitle(title) + self._sar3d_box.setData([], []) + self._sar3d_ground.setData([], []) def _draw_sar3d_projections(self, collection: ResultCollection) -> bool: - """Draw the latest collection-level 3D SAR top and bottom projections.""" - xz_payload = self._collection_payload_by_name(collection, "sar3d_proj_xz", kind=3) - bottom_payload = self._collection_payload_by_name(collection, "sar3d_proj_bottom", kind=3) - if xz_payload is None and bottom_payload is None: + """Draw the 3D SAR views of a result collection; return success.""" + payloads = {name: self._collection_payload_by_name(collection, name, kind=3) for name in SAR3D_VIEWS} + basis = self._collection_payload_by_name(collection, "sar3d_oblique_basis", kind=4) + if basis is None or any(payload is None or payload.image.size == 0 for payload in payloads.values()): self._clear_sar3d_plot() return False + peak = self._collection_payload_by_name(collection, "sar3d_peak", kind=4) + level, *peak_xyz = peak.table[0] if peak is not None else (None,) - for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot): - plot.setUpdatesEnabled(False) - try: - self._ensure_sar3d_plot_items() - # Axis labels track the bottom-view selector (payload axes change with it). - self._configure_sar3d_plot_axes() - xz_drawn = xz_payload is not None and self._draw_sar3d_projection( - self._sar3d_xz_plot, self._sar3d_xz_image_item, xz_payload - ) - bottom_drawn = bottom_payload is not None and self._draw_sar3d_projection( - self._sar3d_xy_plot, self._sar3d_xy_image_item, bottom_payload - ) - if not xz_drawn and not bottom_drawn: - self._clear_sar3d_plot() - return False + for name, payload in payloads.items(): + title, _x_label, _y_label, axes = SAR3D_VIEWS[name] + rect = _image_rect(payload.image_x_axis, payload.image_y_axis) + image = self._sar3d_images[name] + image.setImage(payload.image, autoLevels=False, levels=(DISPLAY_THRESHOLD, 1.0)) + image.setRect(rect) + image.show() + plot = self._sar3d_plots[name] + plot.setRange(rect, padding=0.0) + if peak_xyz: + position = basis.table @ np.asarray(peak_xyz) if axes is None else [peak_xyz[axis] for axis in axes] + self._sar3d_peaks[name].setData([position[0]], [position[1]]) + else: + self._sar3d_peaks[name].setData([], []) + plot.setTitle(title) - peak_payload = self._collection_payload_by_name(collection, "sar3d_peak", kind=2) - peak_suffix = "" - if peak_payload is not None: - peak_suffix = f" | peak={float(peak_payload.scalar_value):.3g}" - bottom_title, _bottom_x_label = self._sar3d_bottom_view_labels() - self._sar3d_xz_plot.setTitle(f"3D SAR top view (X-Z){peak_suffix}") - self._sar3d_xy_plot.setTitle(bottom_title) - finally: - for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot): - plot.setUpdatesEnabled(True) + # Scene box and ground on the oblique view, from the extents of the other views. + front, top = payloads["sar3d_front"], payloads["sar3d_top"] + bounds = [ + (front.image_x_axis[0], front.image_x_axis[-1]), + (front.image_y_axis[0], front.image_y_axis[-1]), + (top.image_y_axis[0], top.image_y_axis[-1]), + ] + corners = np.array([[bounds[axis][(index >> axis) & 1] for axis in range(3)] for index in range(8)]) + projected = corners @ basis.table.T + box = projected[np.ravel(_BOX_EDGES)] + self._sar3d_box.setData(box[:, 0], box[:, 1]) + centre = projected.mean(axis=0) + for axis_name, (start, end) in _AXIS_EDGES.items(): + middle = (projected[start] + projected[end]) / 2 + self._sar3d_axis_labels[axis_name].setPos(*(middle + 0.08 * (middle - centre))) # just outside + (x0, x1), _, (z0, z1) = bounds + ground = (np.array([[x0, 0.0, z0], [x1, 0.0, z0], [x1, 0.0, z1], [x0, 0.0, z1], [x0, 0.0, z0]]) @ basis.table.T).T + self._sar3d_ground.setData(ground[0], ground[1]) + if level is not None: + x, y, z = peak_xyz + self._sar3d_plots["sar3d_oblique"].setTitle( + f"3D | max X {x:+.2f}, Y {y:+.2f}, Z {z:.2f} m | level {level:.3g}" + ) return True diff --git a/python_app/gui/controllers/app_window_ui_mixin.py b/python_app/gui/controllers/app_window_ui_mixin.py index 85737c2..d7d2665 100644 --- a/python_app/gui/controllers/app_window_ui_mixin.py +++ b/python_app/gui/controllers/app_window_ui_mixin.py @@ -13,6 +13,7 @@ from __future__ import annotations from PyQt6.QtWidgets import ( QComboBox, QFrame, + QGridLayout, QGroupBox, QHBoxLayout, QLabel, @@ -25,6 +26,7 @@ from PyQt6.QtWidgets import ( ) import pyqtgraph as pg +from python_app.gui.controllers.app_window_plot.sar3d_plot_mixin import SAR3D_VIEWS from python_app.gui.controllers.sections import ( build_data_actions_group, build_primary_actions_group, @@ -141,23 +143,16 @@ class AppWindowUiMixin: self._plot_stack.addWidget(self._gpr_plot) def _build_sar3d_plot_page(self) -> None: - """Create 3D SAR page with stacked side (X-Z) and top (X-Y) projection plots.""" - # 3D SAR surface: container with two projection heatmaps, mirroring the - # pass-through page layout (two stacked PlotWidgets on one stack page). + """Create the 3D SAR page: front, side, top and 3D views in a 2x2 grid.""" self._sar3d_plots_container = QWidget() - sar3d_layout = QVBoxLayout(self._sar3d_plots_container) - sar3d_layout.setContentsMargins(0, 0, 0, 0) - sar3d_layout.setSpacing(6) - - self._sar3d_xz_plot = self._create_plot_widget(background="#0f141c") - self._sar3d_xz_plot.showGrid(x=True, y=True, alpha=0.2) - sar3d_layout.addWidget(self._sar3d_xz_plot, stretch=1) - - self._sar3d_xy_plot = self._create_plot_widget(background="#0f141c") - self._sar3d_xy_plot.showGrid(x=True, y=True, alpha=0.2) - sar3d_layout.addWidget(self._sar3d_xy_plot, stretch=1) - - self._configure_sar3d_plot_axes() + layout = QGridLayout(self._sar3d_plots_container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(6) + self._sar3d_plots = {} + for index, name in enumerate(SAR3D_VIEWS): + self._sar3d_plots[name] = self._create_plot_widget(background="#0f141c") + layout.addWidget(self._sar3d_plots[name], index // 2, index % 2) + self._init_sar3d_views() self._clear_sar3d_plot() self._plot_stack.addWidget(self._sar3d_plots_container) diff --git a/python_app/gui/controllers/sections/processing_section.py b/python_app/gui/controllers/sections/processing_section.py index 8c1bf24..29b0689 100644 --- a/python_app/gui/controllers/sections/processing_section.py +++ b/python_app/gui/controllers/sections/processing_section.py @@ -20,9 +20,13 @@ from PyQt6.QtWidgets import ( from python_app.gui.controllers.app_window_config.state_builders import GUI_SAVE_HISTORY_LIMIT from python_app.gui.controllers.sections.layout_helpers import FormRow, build_two_column_form_widget -from python_app.gui.runtime.sar3d_views import ( - SAR3D_BOTTOM_VIEW_CHOICES, - sar3d_bottom_view_label_from_deg, + + +# Same semantics on every GPR-like page; the data_processor applies them. +_IGNORE_SOCKET_SPEED_TOOLTIP = ( + "Checked: the speed below drives the motion correction. Unchecked: the speed a locator " + "socket client streams ('vlc') does, and the field below only stands in while no client " + "has sent one in the last 5 s." ) @@ -290,10 +294,7 @@ def build_processing_group(owner) -> QGroupBox: owner._gpr_ignore_socket_speed_enabled = QCheckBox("Ignore socket speed") owner._gpr_ignore_socket_speed_enabled.setChecked(bool(gpr_live_defaults.ignore_socket_speed_enabled)) - owner._gpr_ignore_socket_speed_enabled.setToolTip( - "When checked, use the speed below for motion correction instead of the " - "value streamed over the locator socket." - ) + owner._gpr_ignore_socket_speed_enabled.setToolTip(_IGNORE_SOCKET_SPEED_TOOLTIP) owner._gpr_speed_m_s = QDoubleSpinBox() owner._gpr_speed_m_s.setDecimals(3) @@ -475,12 +476,7 @@ def build_processing_group(owner) -> QGroupBox: owner._legacy_gpr_ignore_socket_speed_enabled.setChecked( bool(legacy_gpr_defaults.ignore_socket_speed_enabled) ) - # When the box is unchecked, an external TCP client controls the speed - # through the C++ locator server; disable the spinner so the GUI value - # cannot silently win against the live socket value. - owner._legacy_gpr_speed_m_s.setEnabled( - bool(legacy_gpr_defaults.ignore_socket_speed_enabled) - ) + owner._legacy_gpr_ignore_socket_speed_enabled.setToolTip(_IGNORE_SOCKET_SPEED_TOOLTIP) owner._legacy_gpr_look_angle_deg = QDoubleSpinBox() owner._legacy_gpr_look_angle_deg.setDecimals(2) @@ -575,26 +571,15 @@ def build_processing_group(owner) -> QGroupBox: ) owner._processing_mode_pages.addWidget(legacy_gpr_page) - # 3D SAR page: shared gpr_* knobs get their own widget set (like legacy_gpr) - # plus the sar3d-specific aperture/volume knobs consumed by the C++ processor. + # 3D SAR page. The labels are the names of the reference notebook + # Multi_antenna_3D_clean_v1.ipynb; the shared gpr_* fields get their own + # widgets here, like on the legacy page. owner._sar3d_input_positions_input = QLineEdit(str(sar3d_defaults.input_positions)) owner._sar3d_input_positions_input.setPlaceholderText("0,1,2") owner._sar3d_output_positions_input = QLineEdit(str(sar3d_defaults.output_positions)) owner._sar3d_output_positions_input.setPlaceholderText("0,1") - owner._sar3d_min_depth_m = QDoubleSpinBox() - owner._sar3d_min_depth_m.setDecimals(2) - owner._sar3d_min_depth_m.setRange(0.0, 50.0) - owner._sar3d_min_depth_m.setSingleStep(0.1) - owner._sar3d_min_depth_m.setValue(float(sar3d_defaults.min_depth_m)) - - owner._sar3d_max_depth_m = QDoubleSpinBox() - owner._sar3d_max_depth_m.setDecimals(2) - owner._sar3d_max_depth_m.setRange(0.1, 50.0) - owner._sar3d_max_depth_m.setSingleStep(0.1) - owner._sar3d_max_depth_m.setValue(float(sar3d_defaults.max_depth_m)) - owner._sar3d_start_freq_mhz = QDoubleSpinBox() owner._sar3d_start_freq_mhz.setDecimals(1) owner._sar3d_start_freq_mhz.setRange(100.0, 8800.0) @@ -607,223 +592,98 @@ def build_processing_group(owner) -> QGroupBox: owner._sar3d_stop_freq_mhz.setSingleStep(10.0) owner._sar3d_stop_freq_mhz.setValue(float(sar3d_defaults.stop_freq_mhz)) + owner._sar3d_min_depth_m = QDoubleSpinBox() + owner._sar3d_min_depth_m.setDecimals(2) + owner._sar3d_min_depth_m.setRange(0.0, 50.0) + owner._sar3d_min_depth_m.setSingleStep(0.1) + owner._sar3d_min_depth_m.setValue(float(sar3d_defaults.min_depth_m)) + owner._sar3d_min_depth_m.setToolTip("Radar depth gate start (m); the scene grid along Z follows the gate.") + + owner._sar3d_max_depth_m = QDoubleSpinBox() + owner._sar3d_max_depth_m.setDecimals(2) + owner._sar3d_max_depth_m.setRange(0.1, 50.0) + owner._sar3d_max_depth_m.setSingleStep(0.1) + owner._sar3d_max_depth_m.setValue(float(sar3d_defaults.max_depth_m)) + owner._sar3d_max_depth_m.setToolTip("Radar depth gate end (m); the scene grid along Z follows the gate.") + + owner._sar3d_voxel_m = QDoubleSpinBox() + owner._sar3d_voxel_m.setDecimals(3) + owner._sar3d_voxel_m.setRange(0.01, 0.1) + owner._sar3d_voxel_m.setSingleStep(0.005) + owner._sar3d_voxel_m.setValue(float(sar3d_defaults.voxel_m)) + owner._sar3d_voxel_m.setToolTip( + "Edge of the cubic voxel (m), the same along X, Y and Z. Smaller is finer and slower: " + "halving it makes 8 times more voxels." + ) + owner._sar3d_look_angle_deg = QDoubleSpinBox() owner._sar3d_look_angle_deg.setDecimals(2) owner._sar3d_look_angle_deg.setRange(-90.0, 90.0) owner._sar3d_look_angle_deg.setSingleStep(0.5) owner._sar3d_look_angle_deg.setValue(float(sar3d_defaults.look_angle_deg)) owner._sar3d_look_angle_deg.setToolTip( - "Radar look angle vs the range axis (deg). Used by intra-sweep motion correction." - ) - - owner._sar3d_direction_sign = QDoubleSpinBox() - owner._sar3d_direction_sign.setDecimals(0) - owner._sar3d_direction_sign.setRange(-1.0, 1.0) - owner._sar3d_direction_sign.setSingleStep(2.0) - owner._sar3d_direction_sign.setValue(float(sar3d_defaults.direction_sign)) - owner._sar3d_direction_sign.setToolTip( - "Motion direction along range: +1 (later frequencies deeper) or -1 (shallower)." + "Downward tilt of the radar (deg): motion correction along range and the ground scene axes." ) owner._sar3d_ignore_socket_speed_enabled = QCheckBox("Ignore socket speed") owner._sar3d_ignore_socket_speed_enabled.setChecked(bool(sar3d_defaults.ignore_socket_speed_enabled)) - owner._sar3d_ignore_socket_speed_enabled.setToolTip( - "When checked, use the speed below for motion correction instead of the " - "value streamed over the locator socket." - ) + owner._sar3d_ignore_socket_speed_enabled.setToolTip(_IGNORE_SOCKET_SPEED_TOOLTIP) owner._sar3d_speed_m_s = QDoubleSpinBox() owner._sar3d_speed_m_s.setDecimals(3) owner._sar3d_speed_m_s.setRange(-100.0, 100.0) owner._sar3d_speed_m_s.setSingleStep(0.05) owner._sar3d_speed_m_s.setValue(float(sar3d_defaults.speed_m_s)) - owner._sar3d_speed_m_s.setToolTip( - "Radar speed (m/s) used by intra-sweep motion correction when 'Ignore socket speed' is on." - ) - - owner._sar3d_apply_freq_phase_correction = QCheckBox("Apply intra-sweep phase correction") - owner._sar3d_apply_freq_phase_correction.setChecked(bool(sar3d_defaults.apply_freq_phase_correction)) owner._sar3d_frame_radius = QSpinBox() - owner._sar3d_frame_radius.setRange(0, 25) + owner._sar3d_frame_radius.setRange(0, 24) owner._sar3d_frame_radius.setValue(int(sar3d_defaults.frame_radius)) owner._sar3d_frame_radius.setToolTip( - "Synthetic-aperture half width in frames: 2*radius+1 consecutive frames form one aperture." + "Aperture of 2*r+1 consecutive frames. 0: the current frame only (needs a vertical antenna " + "baseline); >0 adds aperture from motion and needs an accurate speed. The image lags by r frames." ) - owner._sar3d_edge_frame_weight = QDoubleSpinBox() - owner._sar3d_edge_frame_weight.setDecimals(2) - owner._sar3d_edge_frame_weight.setRange(0.0, 1.0) - owner._sar3d_edge_frame_weight.setSingleStep(0.05) - owner._sar3d_edge_frame_weight.setValue(float(sar3d_defaults.edge_frame_weight)) - owner._sar3d_edge_frame_weight.setToolTip( - "Weight of the outermost aperture frames (1 = flat window, 0 = strong taper)." - ) - - owner._sar3d_pitch_override_enabled = QCheckBox("Override pitch") - owner._sar3d_pitch_override_enabled.setChecked(bool(sar3d_defaults.pitch_override_enabled)) - owner._sar3d_pitch_override_enabled.setToolTip( - "When unchecked, the frame pitch follows the look angle (notebook " - "BP_GROUND_PITCH_DEG = None); check to set it separately below." - ) - - owner._sar3d_pitch_deg = QDoubleSpinBox() - owner._sar3d_pitch_deg.setDecimals(2) - owner._sar3d_pitch_deg.setRange(-90.0, 90.0) - owner._sar3d_pitch_deg.setSingleStep(0.5) - owner._sar3d_pitch_deg.setValue(float(sar3d_defaults.pitch_deg)) - owner._sar3d_pitch_deg.setToolTip( - "Frame pitch (deg) used for ground coordinates and aperture offsets when the override is on." - ) - - owner._sar3d_ground_depth_m = QDoubleSpinBox() - owner._sar3d_ground_depth_m.setDecimals(2) - owner._sar3d_ground_depth_m.setRange(0.0, 10.0) - owner._sar3d_ground_depth_m.setSingleStep(0.05) - owner._sar3d_ground_depth_m.setValue(float(sar3d_defaults.ground_depth_m)) - owner._sar3d_ground_depth_m.setToolTip( - "Height of the lower antenna row above the ground (m); scene Y=0 is the ground." - ) - - owner._sar3d_x_min_m = QDoubleSpinBox() - owner._sar3d_x_min_m.setDecimals(2) - owner._sar3d_x_min_m.setRange(-50.0, 50.0) - owner._sar3d_x_min_m.setSingleStep(0.1) - owner._sar3d_x_min_m.setValue(float(sar3d_defaults.x_min_m)) - - owner._sar3d_x_max_m = QDoubleSpinBox() - owner._sar3d_x_max_m.setDecimals(2) - owner._sar3d_x_max_m.setRange(-50.0, 50.0) - owner._sar3d_x_max_m.setSingleStep(0.1) - owner._sar3d_x_max_m.setValue(float(sar3d_defaults.x_max_m)) - - owner._sar3d_z_min_m = QDoubleSpinBox() - owner._sar3d_z_min_m.setDecimals(2) - owner._sar3d_z_min_m.setRange(-50.0, 50.0) - owner._sar3d_z_min_m.setSingleStep(0.1) - owner._sar3d_z_min_m.setValue(float(sar3d_defaults.z_min_m)) - - owner._sar3d_z_max_m = QDoubleSpinBox() - owner._sar3d_z_max_m.setDecimals(2) - owner._sar3d_z_max_m.setRange(-50.0, 50.0) - owner._sar3d_z_max_m.setSingleStep(0.1) - owner._sar3d_z_max_m.setValue(float(sar3d_defaults.z_max_m)) - - owner._sar3d_y_min_m = QDoubleSpinBox() - owner._sar3d_y_min_m.setDecimals(2) - owner._sar3d_y_min_m.setRange(-10.0, 10.0) - owner._sar3d_y_min_m.setSingleStep(0.1) - owner._sar3d_y_min_m.setValue(float(sar3d_defaults.y_min_m)) - - owner._sar3d_y_max_m = QDoubleSpinBox() - owner._sar3d_y_max_m.setDecimals(2) - owner._sar3d_y_max_m.setRange(-10.0, 10.0) - owner._sar3d_y_max_m.setSingleStep(0.1) - owner._sar3d_y_max_m.setValue(float(sar3d_defaults.y_max_m)) - - owner._sar3d_voxel_x_m = QDoubleSpinBox() - owner._sar3d_voxel_x_m.setDecimals(3) - owner._sar3d_voxel_x_m.setRange(0.005, 1.0) - owner._sar3d_voxel_x_m.setSingleStep(0.005) - owner._sar3d_voxel_x_m.setValue(float(sar3d_defaults.voxel_x_m)) - - owner._sar3d_voxel_y_m = QDoubleSpinBox() - owner._sar3d_voxel_y_m.setDecimals(3) - owner._sar3d_voxel_y_m.setRange(0.005, 1.0) - owner._sar3d_voxel_y_m.setSingleStep(0.005) - owner._sar3d_voxel_y_m.setValue(float(sar3d_defaults.voxel_y_m)) - - owner._sar3d_voxel_z_m = QDoubleSpinBox() - owner._sar3d_voxel_z_m.setDecimals(3) - owner._sar3d_voxel_z_m.setRange(0.005, 1.0) - owner._sar3d_voxel_z_m.setSingleStep(0.005) - owner._sar3d_voxel_z_m.setValue(float(sar3d_defaults.voxel_z_m)) - - owner._sar3d_norm_edge_margin_m = QDoubleSpinBox() - owner._sar3d_norm_edge_margin_m.setDecimals(2) - owner._sar3d_norm_edge_margin_m.setRange(0.0, 5.0) - owner._sar3d_norm_edge_margin_m.setSingleStep(0.05) - owner._sar3d_norm_edge_margin_m.setValue(float(sar3d_defaults.norm_edge_margin_m)) - owner._sar3d_norm_edge_margin_m.setToolTip( - "Grid Y edges excluded from volume normalization (constant-range ridge)." - ) - - owner._sar3d_smooth_sigma = QDoubleSpinBox() - owner._sar3d_smooth_sigma.setDecimals(2) - owner._sar3d_smooth_sigma.setRange(0.0, 10.0) - owner._sar3d_smooth_sigma.setSingleStep(0.25) - owner._sar3d_smooth_sigma.setValue(float(sar3d_defaults.smooth_sigma)) - owner._sar3d_smooth_sigma.setToolTip("Gaussian smoothing sigma over (Z, X) planes, voxels.") + owner._sar3d_bp_ground_depth_m = QDoubleSpinBox() + owner._sar3d_bp_ground_depth_m.setDecimals(2) + owner._sar3d_bp_ground_depth_m.setRange(0.0, 10.0) + owner._sar3d_bp_ground_depth_m.setSingleStep(0.05) + owner._sar3d_bp_ground_depth_m.setValue(float(sar3d_defaults.bp_ground_depth_m)) + owner._sar3d_bp_ground_depth_m.setToolTip("Height of the lower antenna row above the ground (m); scene Y = 0 is the ground.") owner._sar3d_comp_range_power = QDoubleSpinBox() owner._sar3d_comp_range_power.setDecimals(2) owner._sar3d_comp_range_power.setRange(0.0, 2.0) owner._sar3d_comp_range_power.setSingleStep(0.05) owner._sar3d_comp_range_power.setValue(float(sar3d_defaults.comp_range_power)) - owner._sar3d_comp_range_power.setToolTip("Geometric spreading compensation power.") + owner._sar3d_comp_range_power.setToolTip("Geometric spreading compensation (Rtx*Rrx)^power, capped at 5.") - owner._sar3d_tx_per_segment = QSpinBox() - owner._sar3d_tx_per_segment.setRange(1, 64) - owner._sar3d_tx_per_segment.setValue(int(sar3d_defaults.tx_per_segment)) - owner._sar3d_tx_per_segment.setToolTip( - "Fallback Tx-per-segment split of a frame when per-trace capture stamps are absent." - ) - - owner._sar3d_use_compensation = QCheckBox("Use per-voxel compensation") + owner._sar3d_use_compensation = QCheckBox("SAR_USE_COMPENSATION") owner._sar3d_use_compensation.setChecked(bool(sar3d_defaults.use_compensation)) - owner._sar3d_normalize_pairs = QCheckBox("Normalize Tx/Rx pairs") - owner._sar3d_normalize_pairs.setChecked(bool(sar3d_defaults.normalize_pairs)) - - owner._sar3d_bottom_view = QComboBox() - owner._sar3d_bottom_view.addItems([label for label, _deg in SAR3D_BOTTOM_VIEW_CHOICES]) - owner._set_combo_current_text( - owner._sar3d_bottom_view, - sar3d_bottom_view_label_from_deg(float(sar3d_defaults.bottom_view_deg)), - ) - owner._sar3d_bottom_view.setToolTip( - "Bottom plot projection: front view (X-Y), side view (Z-Y) or an " - "intermediate rotation around the vertical axis. The top plot always " - "shows the X-Z top view." - ) + owner._sar3d_normalize_each_frame = QCheckBox("SAR_NORMALIZE_EACH_FRAME") + owner._sar3d_normalize_each_frame.setChecked(bool(sar3d_defaults.normalize_each_frame)) + owner._sar3d_normalize_each_frame.setToolTip("Scale every Tx/Rx A-scan by its median inside the depth gate.") sar3d_page = _build_processing_mode_page( owner._processing_mode_pages, [ - ("Input positions", owner._sar3d_input_positions_input), - ("Output positions", owner._sar3d_output_positions_input), - ("Min depth m", owner._sar3d_min_depth_m), - ("Max depth m", owner._sar3d_max_depth_m), - ("Start MHz", owner._sar3d_start_freq_mhz), - ("Stop MHz", owner._sar3d_stop_freq_mhz), - ("Look angle deg", owner._sar3d_look_angle_deg), - ("Direction sign", owner._sar3d_direction_sign), + ("INPUT_IDX", owner._sar3d_input_positions_input), + ("OUTPUT_IDX", owner._sar3d_output_positions_input), + ("F_START MHz", owner._sar3d_start_freq_mhz), + ("F_STOP MHz", owner._sar3d_stop_freq_mhz), + ("MIN_DEPTH", owner._sar3d_min_depth_m), + ("MAX_DEPTH", owner._sar3d_max_depth_m), + ("SAR_VOXEL_M", owner._sar3d_voxel_m), + ("LOOK_ANGLE_DEG", owner._sar3d_look_angle_deg), owner._sar3d_ignore_socket_speed_enabled, - ("Speed m/s", owner._sar3d_speed_m_s), - owner._sar3d_apply_freq_phase_correction, - ("Frame radius", owner._sar3d_frame_radius), - ("Edge frame weight", owner._sar3d_edge_frame_weight), - owner._sar3d_pitch_override_enabled, - ("Pitch deg", owner._sar3d_pitch_deg), - ("Ground depth m", owner._sar3d_ground_depth_m), - ("X min m", owner._sar3d_x_min_m), - ("X max m", owner._sar3d_x_max_m), - ("Y min m", owner._sar3d_y_min_m), - ("Y max m", owner._sar3d_y_max_m), - ("Z min m", owner._sar3d_z_min_m), - ("Z max m", owner._sar3d_z_max_m), - ("Voxel X m", owner._sar3d_voxel_x_m), - ("Voxel Y m", owner._sar3d_voxel_y_m), - ("Voxel Z m", owner._sar3d_voxel_z_m), - ("Norm edge margin m", owner._sar3d_norm_edge_margin_m), - ("Smooth sigma", owner._sar3d_smooth_sigma), - ("Comp range power", owner._sar3d_comp_range_power), - ("Tx per segment", owner._sar3d_tx_per_segment), - ("Bottom view", owner._sar3d_bottom_view), + ("SPEED_M_S", owner._sar3d_speed_m_s), + ("SAR_FRAME_RADIUS", owner._sar3d_frame_radius), + ("BP_GROUND_DEPTH_M", owner._sar3d_bp_ground_depth_m), + ("COMP_RANGE_POWER", owner._sar3d_comp_range_power), owner._sar3d_use_compensation, - owner._sar3d_normalize_pairs, + owner._sar3d_normalize_each_frame, ], - split_index=13, + split_index=8, ) owner._processing_mode_pages.addWidget(sar3d_page) @@ -858,7 +718,7 @@ def build_processing_group(owner) -> QGroupBox: owner._gpr_motion_mode.currentTextChanged.connect(owner._on_processing_live_settings_changed) owner._gpr_look_angle_deg.valueChanged.connect(owner._on_processing_live_settings_changed) owner._gpr_direction_sign.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._gpr_ignore_socket_speed_enabled.toggled.connect(owner._on_processing_live_settings_changed) + owner._gpr_ignore_socket_speed_enabled.toggled.connect(owner._on_ignore_socket_speed_toggled) owner._gpr_speed_m_s.valueChanged.connect(owner._on_processing_live_settings_changed) owner._gpr_start_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed) owner._gpr_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed) @@ -885,9 +745,7 @@ def build_processing_group(owner) -> QGroupBox: owner._legacy_gpr_start_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed) owner._legacy_gpr_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed) owner._legacy_gpr_speed_m_s.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._legacy_gpr_ignore_socket_speed_enabled.toggled.connect( - owner._on_legacy_gpr_ignore_socket_speed_toggled - ) + owner._legacy_gpr_ignore_socket_speed_enabled.toggled.connect(owner._on_ignore_socket_speed_toggled) owner._legacy_gpr_look_angle_deg.valueChanged.connect(owner._on_processing_live_settings_changed) owner._legacy_gpr_apply_freq_phase_correction.toggled.connect(owner._on_processing_live_settings_changed) owner._legacy_gpr_reference_mode.currentTextChanged.connect(owner._on_processing_live_settings_changed) @@ -901,36 +759,23 @@ def build_processing_group(owner) -> QGroupBox: owner._legacy_gpr_visible_z_max_m.valueChanged.connect(owner._on_gpr_locator_window_changed) owner._sar3d_input_positions_input.editingFinished.connect(owner._on_processing_live_settings_changed) owner._sar3d_output_positions_input.editingFinished.connect(owner._on_processing_live_settings_changed) - owner._sar3d_min_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_max_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_start_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_look_angle_deg.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_direction_sign.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_ignore_socket_speed_enabled.toggled.connect(owner._on_processing_live_settings_changed) - owner._sar3d_speed_m_s.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_apply_freq_phase_correction.toggled.connect(owner._on_processing_live_settings_changed) - owner._sar3d_frame_radius.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_edge_frame_weight.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_pitch_override_enabled.toggled.connect(owner._on_processing_live_settings_changed) - owner._sar3d_pitch_deg.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_ground_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_x_min_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_x_max_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_z_min_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_z_max_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_y_min_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_y_max_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_voxel_x_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_voxel_y_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_voxel_z_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_norm_edge_margin_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_smooth_sigma.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_comp_range_power.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_tx_per_segment.valueChanged.connect(owner._on_processing_live_settings_changed) + for widget in ( + owner._sar3d_start_freq_mhz, + owner._sar3d_stop_freq_mhz, + owner._sar3d_min_depth_m, + owner._sar3d_max_depth_m, + owner._sar3d_voxel_m, + owner._sar3d_look_angle_deg, + owner._sar3d_speed_m_s, + owner._sar3d_frame_radius, + owner._sar3d_bp_ground_depth_m, + owner._sar3d_comp_range_power, + ): + widget.valueChanged.connect(owner._on_processing_live_settings_changed) + owner._sar3d_ignore_socket_speed_enabled.toggled.connect(owner._on_ignore_socket_speed_toggled) owner._sar3d_use_compensation.toggled.connect(owner._on_processing_live_settings_changed) - owner._sar3d_normalize_pairs.toggled.connect(owner._on_processing_live_settings_changed) - owner._sar3d_bottom_view.currentTextChanged.connect(owner._on_processing_live_settings_changed) + owner._sar3d_normalize_each_frame.toggled.connect(owner._on_processing_live_settings_changed) + owner._sync_socket_speed_controls() owner._set_processing_mode_page(owner._processing_mode.currentText()) return group diff --git a/python_app/gui/runtime/sar3d_views.py b/python_app/gui/runtime/sar3d_views.py deleted file mode 100644 index b5dda7c..0000000 --- a/python_app/gui/runtime/sar3d_views.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Shared mapping for the sar3d bottom-view projection choices. - -The bottom plot of the sar3d mode projects the imaged volume along a -horizontal direction rotated around the vertical Y axis: 0° = front view -(X horizontal), 90° = side view (Z horizontal). One shared mapping keeps the -combo box, the live-config build, the profile persistence and the plot labels -consistent. -""" - -from __future__ import annotations - -SAR3D_BOTTOM_VIEW_CHOICES: tuple[tuple[str, float], ...] = ( - ("front (0°)", 0.0), - ("30°", 30.0), - ("60°", 60.0), - ("side (90°)", 90.0), -) - - -def sar3d_bottom_view_deg_from_label(label: str) -> float: - """Map a bottom-view combo label to its projection angle in degrees.""" - for choice_label, degrees in SAR3D_BOTTOM_VIEW_CHOICES: - if choice_label == label: - return degrees - return 0.0 - - -def sar3d_bottom_view_label_from_deg(degrees: float) -> str: - """Map a projection angle to the nearest bottom-view combo label.""" - return min(SAR3D_BOTTOM_VIEW_CHOICES, key=lambda choice: abs(choice[1] - float(degrees)))[0] diff --git a/python_app/hardware_full/laser_control/protocol.py b/python_app/hardware_full/laser_control/protocol.py index 0ae7c24..283d29f 100644 --- a/python_app/hardware_full/laser_control/protocol.py +++ b/python_app/hardware_full/laser_control/protocol.py @@ -4,6 +4,7 @@ Encodes commands to wire bytes, decodes device responses, and manages the serial port connection to the laser control board. """ +import errno import logging import struct from typing import Optional @@ -204,9 +205,9 @@ class Protocol: exclusive=True, ) except serial.SerialException as exc: - # pyserial raises SerialException with errno EAGAIN/EWOULDBLOCK (11) - # when another process already holds the exclusive flock. - if getattr(exc, "errno", None) in (11,) or "exclusively lock" in str(exc).lower(): + # pyserial raises SerialException with errno EWOULDBLOCK when another + # process already holds the exclusive flock. + if getattr(exc, "errno", None) == errno.EWOULDBLOCK or "exclusively lock" in str(exc).lower(): logger.error("Laser serial port '%s' is busy (held by another program)", port) raise PortBusyError(port) from exc logger.error("Cannot open laser serial port '%s': %s", port, exc) diff --git a/python_app/models/gui_profile_codec.py b/python_app/models/gui_profile_codec.py index 866bc54..306b822 100644 --- a/python_app/models/gui_profile_codec.py +++ b/python_app/models/gui_profile_codec.py @@ -2,6 +2,7 @@ from __future__ import annotations +import dataclasses import logging from typing import Any @@ -172,6 +173,21 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel: legacy_gpr_context, ) + sar3d_context = "gui.processing.sar3d" + sar3d_defaults = GuiSar3dStateModel() + + def sar3d_string(key: str) -> str: + return _optional_string(sar3d_object, key, getattr(sar3d_defaults, key), sar3d_context) + + def sar3d_bool(key: str) -> bool: + return _optional_bool(sar3d_object, key, getattr(sar3d_defaults, key), sar3d_context) + + def sar3d_int(key: str) -> int: + return _optional_int(sar3d_object, key, getattr(sar3d_defaults, key), sar3d_context) + + def sar3d_float(key: str) -> float: + return _optional_float(sar3d_object, key, getattr(sar3d_defaults, key), sar3d_context) + gui.processing = GuiProcessingStateModel( selected_mode=selected_mode, pass_through=GuiPassThroughStateModel( @@ -455,198 +471,21 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel: visible_z_max_m=legacy_float("visible_z_max_m", gui.processing.legacy_gpr.visible_z_max_m), ), sar3d=GuiSar3dStateModel( - input_positions=_optional_string( - sar3d_object, - "input_positions", - gui.processing.sar3d.input_positions, - "gui.processing.sar3d", - ), - output_positions=_optional_string( - sar3d_object, - "output_positions", - gui.processing.sar3d.output_positions, - "gui.processing.sar3d", - ), - min_depth_m=_optional_float( - sar3d_object, - "min_depth_m", - gui.processing.sar3d.min_depth_m, - "gui.processing.sar3d", - ), - max_depth_m=_optional_float( - sar3d_object, - "max_depth_m", - gui.processing.sar3d.max_depth_m, - "gui.processing.sar3d", - ), - start_freq_mhz=_optional_float( - sar3d_object, - "start_freq_mhz", - gui.processing.sar3d.start_freq_mhz, - "gui.processing.sar3d", - ), - stop_freq_mhz=_optional_float( - sar3d_object, - "stop_freq_mhz", - gui.processing.sar3d.stop_freq_mhz, - "gui.processing.sar3d", - ), - speed_m_s=_optional_float( - sar3d_object, - "speed_m_s", - gui.processing.sar3d.speed_m_s, - "gui.processing.sar3d", - ), - ignore_socket_speed_enabled=_optional_bool( - sar3d_object, - "ignore_socket_speed_enabled", - gui.processing.sar3d.ignore_socket_speed_enabled, - "gui.processing.sar3d", - ), - look_angle_deg=_optional_float( - sar3d_object, - "look_angle_deg", - gui.processing.sar3d.look_angle_deg, - "gui.processing.sar3d", - ), - direction_sign=_optional_float( - sar3d_object, - "direction_sign", - gui.processing.sar3d.direction_sign, - "gui.processing.sar3d", - ), - apply_freq_phase_correction=_optional_bool( - sar3d_object, - "apply_freq_phase_correction", - gui.processing.sar3d.apply_freq_phase_correction, - "gui.processing.sar3d", - ), - frame_radius=_optional_int( - sar3d_object, - "frame_radius", - gui.processing.sar3d.frame_radius, - "gui.processing.sar3d", - ), - edge_frame_weight=_optional_float( - sar3d_object, - "edge_frame_weight", - gui.processing.sar3d.edge_frame_weight, - "gui.processing.sar3d", - ), - pitch_override_enabled=_optional_bool( - sar3d_object, - "pitch_override_enabled", - gui.processing.sar3d.pitch_override_enabled, - "gui.processing.sar3d", - ), - pitch_deg=_optional_float( - sar3d_object, - "pitch_deg", - gui.processing.sar3d.pitch_deg, - "gui.processing.sar3d", - ), - ground_depth_m=_optional_float( - sar3d_object, - "ground_depth_m", - gui.processing.sar3d.ground_depth_m, - "gui.processing.sar3d", - ), - x_min_m=_optional_float( - sar3d_object, - "x_min_m", - gui.processing.sar3d.x_min_m, - "gui.processing.sar3d", - ), - x_max_m=_optional_float( - sar3d_object, - "x_max_m", - gui.processing.sar3d.x_max_m, - "gui.processing.sar3d", - ), - y_min_m=_optional_float( - sar3d_object, - "y_min_m", - gui.processing.sar3d.y_min_m, - "gui.processing.sar3d", - ), - y_max_m=_optional_float( - sar3d_object, - "y_max_m", - gui.processing.sar3d.y_max_m, - "gui.processing.sar3d", - ), - z_min_m=_optional_float( - sar3d_object, - "z_min_m", - gui.processing.sar3d.z_min_m, - "gui.processing.sar3d", - ), - z_max_m=_optional_float( - sar3d_object, - "z_max_m", - gui.processing.sar3d.z_max_m, - "gui.processing.sar3d", - ), - voxel_x_m=_optional_float( - sar3d_object, - "voxel_x_m", - gui.processing.sar3d.voxel_x_m, - "gui.processing.sar3d", - ), - voxel_y_m=_optional_float( - sar3d_object, - "voxel_y_m", - gui.processing.sar3d.voxel_y_m, - "gui.processing.sar3d", - ), - voxel_z_m=_optional_float( - sar3d_object, - "voxel_z_m", - gui.processing.sar3d.voxel_z_m, - "gui.processing.sar3d", - ), - norm_edge_margin_m=_optional_float( - sar3d_object, - "norm_edge_margin_m", - gui.processing.sar3d.norm_edge_margin_m, - "gui.processing.sar3d", - ), - smooth_sigma=_optional_float( - sar3d_object, - "smooth_sigma", - gui.processing.sar3d.smooth_sigma, - "gui.processing.sar3d", - ), - comp_range_power=_optional_float( - sar3d_object, - "comp_range_power", - gui.processing.sar3d.comp_range_power, - "gui.processing.sar3d", - ), - tx_per_segment=_optional_int( - sar3d_object, - "tx_per_segment", - gui.processing.sar3d.tx_per_segment, - "gui.processing.sar3d", - ), - use_compensation=_optional_bool( - sar3d_object, - "use_compensation", - gui.processing.sar3d.use_compensation, - "gui.processing.sar3d", - ), - normalize_pairs=_optional_bool( - sar3d_object, - "normalize_pairs", - gui.processing.sar3d.normalize_pairs, - "gui.processing.sar3d", - ), - bottom_view_deg=_optional_float( - sar3d_object, - "bottom_view_deg", - gui.processing.sar3d.bottom_view_deg, - "gui.processing.sar3d", - ), + input_positions=sar3d_string("input_positions"), + output_positions=sar3d_string("output_positions"), + min_depth_m=sar3d_float("min_depth_m"), + max_depth_m=sar3d_float("max_depth_m"), + start_freq_mhz=sar3d_float("start_freq_mhz"), + stop_freq_mhz=sar3d_float("stop_freq_mhz"), + speed_m_s=sar3d_float("speed_m_s"), + ignore_socket_speed_enabled=sar3d_bool("ignore_socket_speed_enabled"), + look_angle_deg=sar3d_float("look_angle_deg"), + frame_radius=sar3d_int("frame_radius"), + voxel_m=sar3d_float("voxel_m"), + bp_ground_depth_m=sar3d_float("bp_ground_depth_m"), + comp_range_power=sar3d_float("comp_range_power"), + use_compensation=sar3d_bool("use_compensation"), + normalize_each_frame=sar3d_bool("normalize_each_frame"), ), ) if gui.processing.selected_mode not in {"pass_through", "bscan", "gpr", "legacy_gpr", "sar3d"}: @@ -689,20 +528,8 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel: raise ValueError("gui.processing.legacy_gpr.min_visible_pair_count must be >= 1") if gui.processing.sar3d.frame_radius < 0: raise ValueError("gui.processing.sar3d.frame_radius must be >= 0") - if not 0.0 <= gui.processing.sar3d.edge_frame_weight <= 1.0: - raise ValueError("gui.processing.sar3d.edge_frame_weight must be within [0, 1]") - if min(gui.processing.sar3d.voxel_x_m, gui.processing.sar3d.voxel_y_m, gui.processing.sar3d.voxel_z_m) <= 0.0: - raise ValueError("gui.processing.sar3d.voxel_x/y/z_m must each be > 0") - if gui.processing.sar3d.x_max_m <= gui.processing.sar3d.x_min_m: - raise ValueError("gui.processing.sar3d.x_max_m must be > x_min_m") - if gui.processing.sar3d.y_max_m <= gui.processing.sar3d.y_min_m: - raise ValueError("gui.processing.sar3d.y_max_m must be > y_min_m") - if gui.processing.sar3d.z_max_m <= gui.processing.sar3d.z_min_m: - raise ValueError("gui.processing.sar3d.z_max_m must be > z_min_m") - if gui.processing.sar3d.ground_depth_m < 0.0: - raise ValueError("gui.processing.sar3d.ground_depth_m must be >= 0") - if gui.processing.sar3d.tx_per_segment < 1: - raise ValueError("gui.processing.sar3d.tx_per_segment must be >= 1") + if not gui.processing.sar3d.voxel_m > 0.0: + raise ValueError("gui.processing.sar3d.voxel_m must be > 0") data_actions_object = _as_dict(gui_object.get("data_actions"), "gui.data_actions") gui.data_actions = GuiDataActionsStateModel( @@ -854,40 +681,7 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]: "visible_z_min_m": gui.processing.legacy_gpr.visible_z_min_m, "visible_z_max_m": gui.processing.legacy_gpr.visible_z_max_m, }, - "sar3d": { - "input_positions": gui.processing.sar3d.input_positions, - "output_positions": gui.processing.sar3d.output_positions, - "min_depth_m": gui.processing.sar3d.min_depth_m, - "max_depth_m": gui.processing.sar3d.max_depth_m, - "start_freq_mhz": gui.processing.sar3d.start_freq_mhz, - "stop_freq_mhz": gui.processing.sar3d.stop_freq_mhz, - "speed_m_s": gui.processing.sar3d.speed_m_s, - "ignore_socket_speed_enabled": gui.processing.sar3d.ignore_socket_speed_enabled, - "look_angle_deg": gui.processing.sar3d.look_angle_deg, - "direction_sign": gui.processing.sar3d.direction_sign, - "apply_freq_phase_correction": gui.processing.sar3d.apply_freq_phase_correction, - "frame_radius": gui.processing.sar3d.frame_radius, - "edge_frame_weight": gui.processing.sar3d.edge_frame_weight, - "pitch_override_enabled": gui.processing.sar3d.pitch_override_enabled, - "pitch_deg": gui.processing.sar3d.pitch_deg, - "ground_depth_m": gui.processing.sar3d.ground_depth_m, - "x_min_m": gui.processing.sar3d.x_min_m, - "x_max_m": gui.processing.sar3d.x_max_m, - "y_min_m": gui.processing.sar3d.y_min_m, - "y_max_m": gui.processing.sar3d.y_max_m, - "z_min_m": gui.processing.sar3d.z_min_m, - "z_max_m": gui.processing.sar3d.z_max_m, - "voxel_x_m": gui.processing.sar3d.voxel_x_m, - "voxel_y_m": gui.processing.sar3d.voxel_y_m, - "voxel_z_m": gui.processing.sar3d.voxel_z_m, - "norm_edge_margin_m": gui.processing.sar3d.norm_edge_margin_m, - "smooth_sigma": gui.processing.sar3d.smooth_sigma, - "comp_range_power": gui.processing.sar3d.comp_range_power, - "tx_per_segment": gui.processing.sar3d.tx_per_segment, - "use_compensation": gui.processing.sar3d.use_compensation, - "normalize_pairs": gui.processing.sar3d.normalize_pairs, - "bottom_view_deg": gui.processing.sar3d.bottom_view_deg, - }, + "sar3d": dataclasses.asdict(gui.processing.sar3d), }, "data_actions": { "save_count": gui.data_actions.save_count, diff --git a/python_app/models/gui_profile_schema.py b/python_app/models/gui_profile_schema.py index 7b3dd84..ca7c722 100644 --- a/python_app/models/gui_profile_schema.py +++ b/python_app/models/gui_profile_schema.py @@ -127,45 +127,25 @@ class GuiLegacyGprStateModel: class GuiSar3dStateModel: """UI-only defaults for the 3D SAR (sar3d) live settings. - The band, depth gate, antenna selection and motion knobs mirror the shared - gpr_* live-config fields; the sar3d_* knobs mirror the reference notebook - (Multi_antenna_3D_clean_v1.ipynb) defaults used by the C++ sar3d processor. + Values are the reference notebook's (Multi_antenna_3D_clean_v1.ipynb), whose + names the GUI shows: INPUT_IDX, F_START, MIN_DEPTH, LOOK_ANGLE_DEG, ... """ input_positions: str = "" output_positions: str = "" - min_depth_m: float = 2.5 + min_depth_m: float = 2.0 max_depth_m: float = 8.0 - start_freq_mhz: float = 2000.0 + start_freq_mhz: float = 1500.0 stop_freq_mhz: float = 6000.0 speed_m_s: float = 0.0 ignore_socket_speed_enabled: bool = False - look_angle_deg: float = 17.5 - direction_sign: float = 1.0 - apply_freq_phase_correction: bool = True + look_angle_deg: float = 19.5 frame_radius: int = 0 - edge_frame_weight: float = 0.8 - pitch_override_enabled: bool = False - pitch_deg: float = 17.5 - ground_depth_m: float = 1.25 - x_min_m: float = -2.0 - x_max_m: float = 2.0 - y_min_m: float = -0.5 - y_max_m: float = 2.0 - z_min_m: float = 0.0 - z_max_m: float = 8.0 - voxel_x_m: float = 0.025 - voxel_y_m: float = 0.02 - voxel_z_m: float = 0.02 - norm_edge_margin_m: float = 0.3 - smooth_sigma: float = 1.5 + voxel_m: float = 0.02 + bp_ground_depth_m: float = 1.25 comp_range_power: float = 0.3 - tx_per_segment: int = 2 use_compensation: bool = True - normalize_pairs: bool = True - # Bottom-view projection angle around the vertical Y axis, degrees: - # 0 = front view (X-Y), 90 = side view (Z-Y). - bottom_view_deg: float = 0.0 + normalize_each_frame: bool = True @dataclass(slots=True) diff --git a/python_app/orchestration/live_processing_config.py b/python_app/orchestration/live_processing_config.py index 87e45d4..54d870a 100644 --- a/python_app/orchestration/live_processing_config.py +++ b/python_app/orchestration/live_processing_config.py @@ -72,40 +72,16 @@ class ProcessingLiveConfig: gpr_visible_x_max_m: float = 2.0 gpr_visible_z_min_m: float = 0.0 gpr_visible_z_max_m: float = 14.0 - # 3D SAR (sar3d processor) parameters; defaults mirror the reference - # notebook Multi_antenna_3D_clean_v1.ipynb. The frequency band, depth gate, - # antenna selection and motion parameters are shared with the gpr_* fields. + # 3D SAR (sar3d processor) parameters, named after the reference notebook + # Multi_antenna_3D_clean_v1.ipynb (SAR_FRAME_RADIUS, BP_GROUND_DEPTH_M, ...). + # The band, depth gate, antenna selection, speed and look angle are the + # shared gpr_* fields. sar3d_frame_radius: int = 0 - sar3d_edge_frame_weight: float = 0.8 - # Frame pitch override; disabled -> the pitch follows gpr_look_angle_deg. - sar3d_pitch_override_enabled: bool = False - sar3d_pitch_deg: float = 17.5 - # Height of the lower antenna row above the ground (scene Y=0 = ground). - sar3d_ground_depth_m: float = 1.25 - # Scene grid bounds (metres): X across, Y height above the ground, Z along - # the ground away from the radar. Voxels outside the radar depth gate are - # zeroed by the mask regardless of the Z window. - sar3d_x_min_m: float = -2.0 - sar3d_x_max_m: float = 2.0 - sar3d_y_min_m: float = -0.5 - sar3d_y_max_m: float = 2.0 - sar3d_z_min_m: float = 0.0 - sar3d_z_max_m: float = 8.0 - sar3d_voxel_x_m: float = 0.025 - sar3d_voxel_y_m: float = 0.02 - sar3d_voxel_z_m: float = 0.02 - # Volume finalization: Y-edge margin excluded from normalization, Gaussian - # smoothing sigma over (Z, X) planes. - sar3d_norm_edge_margin_m: float = 0.3 - sar3d_smooth_sigma: float = 1.5 + sar3d_voxel_m: float = 0.02 # SAR_VOXEL_M, one edge for X, Y and Z + sar3d_bp_ground_depth_m: float = 1.25 sar3d_comp_range_power: float = 0.3 - # Fallback frame segmentation when per-trace capture stamps are absent. - sar3d_tx_per_segment: int = 2 sar3d_use_compensation: bool = True - sar3d_normalize_pairs: bool = True - # Bottom-view projection angle around the vertical Y axis, degrees: - # 0 = front view (X horizontal), 90 = side view (Z horizontal). - sar3d_bottom_view_deg: float = 0.0 + sar3d_normalize_each_frame: bool = True # When true, the C++ data_processor ignores socket-supplied vlc updates # and keeps using `gpr_speed_m_s` from this file. ignore_socket_speed: bool = False @@ -171,26 +147,11 @@ class ProcessingLiveConfig: "gpr_visible_z_min_m": float(self.gpr_visible_z_min_m), "gpr_visible_z_max_m": float(self.gpr_visible_z_max_m), "sar3d_frame_radius": int(self.sar3d_frame_radius), - "sar3d_edge_frame_weight": float(self.sar3d_edge_frame_weight), - "sar3d_pitch_override_enabled": bool(self.sar3d_pitch_override_enabled), - "sar3d_pitch_deg": float(self.sar3d_pitch_deg), - "sar3d_ground_depth_m": float(self.sar3d_ground_depth_m), - "sar3d_x_min_m": float(self.sar3d_x_min_m), - "sar3d_x_max_m": float(self.sar3d_x_max_m), - "sar3d_y_min_m": float(self.sar3d_y_min_m), - "sar3d_y_max_m": float(self.sar3d_y_max_m), - "sar3d_z_min_m": float(self.sar3d_z_min_m), - "sar3d_z_max_m": float(self.sar3d_z_max_m), - "sar3d_voxel_x_m": float(self.sar3d_voxel_x_m), - "sar3d_voxel_y_m": float(self.sar3d_voxel_y_m), - "sar3d_voxel_z_m": float(self.sar3d_voxel_z_m), - "sar3d_norm_edge_margin_m": float(self.sar3d_norm_edge_margin_m), - "sar3d_smooth_sigma": float(self.sar3d_smooth_sigma), + "sar3d_voxel_m": float(self.sar3d_voxel_m), + "sar3d_bp_ground_depth_m": float(self.sar3d_bp_ground_depth_m), "sar3d_comp_range_power": float(self.sar3d_comp_range_power), - "sar3d_tx_per_segment": int(self.sar3d_tx_per_segment), "sar3d_use_compensation": bool(self.sar3d_use_compensation), - "sar3d_normalize_pairs": bool(self.sar3d_normalize_pairs), - "sar3d_bottom_view_deg": float(self.sar3d_bottom_view_deg), + "sar3d_normalize_each_frame": bool(self.sar3d_normalize_each_frame), "ignore_socket_speed": bool(self.ignore_socket_speed), "reprocess_current_result": bool(self.reprocess_current_result), "history_command_seq": int(self.history_command_seq), diff --git a/python_app/scripts/replay_smoke_run.py b/python_app/scripts/replay_smoke_run.py deleted file mode 100644 index 50bf745..0000000 --- a/python_app/scripts/replay_smoke_run.py +++ /dev/null @@ -1,262 +0,0 @@ -"""Headless smoke: replay the recorded VNA_AlGo dataset through the full pipeline. - -Loads the replay profile, creates neutral preprocess sets, launches the whole -pipeline (replay_raw_producer -> data_preprocessor -> data_processor) via the -ProcessSupervisor with unique ring names, forces the sar3d processor through -processing_live.json, and asserts that: - -- sar3d projection payloads arrive with a positive peak; -- collection ids increase monotonically (including across a dataset loop); -- the preprocessor is transparent: a preprocessed_tap trace equals the - recorded snapshot's s21 within float32 rounding. - -Usage: - python -m python_app.scripts.replay_smoke_run \ - [--config run_config_examples/run_config_replay_vna.example.json] \ - [--duration 20.0] -""" - -from __future__ import annotations - -import argparse -import json -import logging -import os -from pathlib import Path -import time - -import numpy as np - -from python_app.models.run_config_model import RunConfigModel -from python_app.orchestration.config_writer import ConfigWriter -from python_app.orchestration.live_processing_config import ( - ProcessingLiveConfig, - ProcessingLiveConfigWriter, -) -from python_app.orchestration.process_supervisor import ProcessSupervisor -from python_app.orchestration.shm import ShmRingReader -from python_app.orchestration.shm.decoder import PREPROC_MAGIC, decode_trace_collection -from python_app.storage.npz_store import NpzStore, radar_key_from_config -from python_app.workflows.kamil_adc_neutral_preprocess import build_neutral_s21_sets - -logger = logging.getLogger(__name__) - -PROJECT_ROOT = Path(__file__).resolve().parents[2] - - -def _make_unique_ring_name(prefix: str) -> str: - """Build unique POSIX SHM ring name.""" - return f"/{prefix}_{os.getpid()}_{time.monotonic_ns()}" - - -def _sar3d_live_config(profile_path: Path) -> ProcessingLiveConfig: - """Build a sar3d live config from the profile's gui.processing.sar3d section. - - RunConfigModel does not carry the GUI section (it belongs to the GUI profile - layer), so the sar3d defaults are read straight from the profile JSON. - """ - payload = json.loads(profile_path.read_text(encoding="utf-8")) - sar3d = ( - payload.get("gui", {}).get("processing", {}).get("sar3d", {}) - if isinstance(payload, dict) - else {} - ) - - def _positions(text: str) -> list[int]: - return [int(value) for value in str(text).split(",") if value.strip()] - - live = ProcessingLiveConfig(processor_mode="sar3d", ignore_socket_speed=True) - if not isinstance(sar3d, dict) or not sar3d: - return live - live.gpr_input_positions = _positions(sar3d.get("input_positions", "")) - live.gpr_output_positions = _positions(sar3d.get("output_positions", "")) - live.gpr_min_depth_m = float(sar3d.get("min_depth_m", live.gpr_min_depth_m)) - live.gpr_max_depth_m = float(sar3d.get("max_depth_m", live.gpr_max_depth_m)) - live.gpr_start_freq_mhz = float(sar3d.get("start_freq_mhz", live.gpr_start_freq_mhz)) - live.gpr_stop_freq_mhz = float(sar3d.get("stop_freq_mhz", live.gpr_stop_freq_mhz)) - live.gpr_speed_m_s = float(sar3d.get("speed_m_s", live.gpr_speed_m_s)) - live.gpr_look_angle_deg = float(sar3d.get("look_angle_deg", live.gpr_look_angle_deg)) - live.gpr_direction_sign = float(sar3d.get("direction_sign", live.gpr_direction_sign)) - live.gpr_apply_freq_phase_correction = bool( - sar3d.get("apply_freq_phase_correction", live.gpr_apply_freq_phase_correction) - ) - live.sar3d_frame_radius = int(sar3d.get("frame_radius", live.sar3d_frame_radius)) - live.sar3d_edge_frame_weight = float( - sar3d.get("edge_frame_weight", live.sar3d_edge_frame_weight) - ) - live.sar3d_pitch_override_enabled = bool( - sar3d.get("pitch_override_enabled", live.sar3d_pitch_override_enabled) - ) - live.sar3d_pitch_deg = float(sar3d.get("pitch_deg", live.sar3d_pitch_deg)) - live.sar3d_ground_depth_m = float(sar3d.get("ground_depth_m", live.sar3d_ground_depth_m)) - live.sar3d_x_min_m = float(sar3d.get("x_min_m", live.sar3d_x_min_m)) - live.sar3d_x_max_m = float(sar3d.get("x_max_m", live.sar3d_x_max_m)) - live.sar3d_y_min_m = float(sar3d.get("y_min_m", live.sar3d_y_min_m)) - live.sar3d_y_max_m = float(sar3d.get("y_max_m", live.sar3d_y_max_m)) - live.sar3d_z_min_m = float(sar3d.get("z_min_m", live.sar3d_z_min_m)) - live.sar3d_z_max_m = float(sar3d.get("z_max_m", live.sar3d_z_max_m)) - live.sar3d_voxel_x_m = float(sar3d.get("voxel_x_m", live.sar3d_voxel_x_m)) - live.sar3d_voxel_y_m = float(sar3d.get("voxel_y_m", live.sar3d_voxel_y_m)) - live.sar3d_voxel_z_m = float(sar3d.get("voxel_z_m", live.sar3d_voxel_z_m)) - live.sar3d_norm_edge_margin_m = float( - sar3d.get("norm_edge_margin_m", live.sar3d_norm_edge_margin_m) - ) - live.sar3d_smooth_sigma = float(sar3d.get("smooth_sigma", live.sar3d_smooth_sigma)) - live.sar3d_comp_range_power = float( - sar3d.get("comp_range_power", live.sar3d_comp_range_power) - ) - live.sar3d_tx_per_segment = int(sar3d.get("tx_per_segment", live.sar3d_tx_per_segment)) - live.sar3d_use_compensation = bool(sar3d.get("use_compensation", live.sar3d_use_compensation)) - live.sar3d_normalize_pairs = bool(sar3d.get("normalize_pairs", live.sar3d_normalize_pairs)) - live.sar3d_bottom_view_deg = float(sar3d.get("bottom_view_deg", live.sar3d_bottom_view_deg)) - return live - - -def _check_preprocessor_transparency( - tap_reader: ShmRingReader, dataset_dir: Path -) -> bool: - """Compare one preprocessed trace against the recorded snapshot's s21.""" - payload = tap_reader.peek_latest_payload() - if payload is None: - logger.warning("No preprocessed_tap payload available for transparency check") - return False - preprocessed = decode_trace_collection(payload, PREPROC_MAGIC) - - # Any snapshot works: neutral preprocessing is time-invariant, so match the - # trace against every recorded snapshot and accept the best one. - trace = preprocessed.traces[0] - combo_file = f"i{trace.combo.input}_o{trace.combo.output}_s21.npy" - best_delta = np.inf - for folder in sorted(p for p in dataset_dir.iterdir() if p.name[:1].isdigit()): - recorded = np.load(folder / combo_file).astype(np.complex64) - if recorded.size != np.asarray(trace.s21).size: - continue - delta = float(np.max(np.abs(np.asarray(trace.s21) - recorded))) - best_delta = min(best_delta, delta) - if best_delta == 0.0: - break - logger.info("Preprocessor transparency: max|delta| vs best snapshot = %.3e", best_delta) - return best_delta < 1e-6 - - -def main() -> int: - """Run the replay pipeline smoke and report pass/fail.""" - parser = argparse.ArgumentParser(description="Headless replay pipeline smoke") - parser.add_argument( - "--config", - type=Path, - default=PROJECT_ROOT / "run_config_examples/run_config_replay_vna.example.json", - ) - parser.add_argument("--duration", type=float, default=20.0) - args = parser.parse_args() - - logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") - - config = RunConfigModel.load_from_path(args.config) - config.ensure_combos() - # expanduser: "~/..." dataset paths from machine-portable profiles. - dataset_dir = Path(config.radar.replay.dataset_dir).expanduser() - if not dataset_dir.is_dir(): - raise RuntimeError(f"Replay dataset not found: {dataset_dir}") - - config.rings.raw.name = _make_unique_ring_name("radar_raw_replay_smoke") - config.rings.raw_tap.name = _make_unique_ring_name("radar_raw_tap_replay_smoke") - config.rings.preprocessed.name = _make_unique_ring_name("radar_preprocessed_replay_smoke") - config.rings.preprocessed_tap.name = _make_unique_ring_name("radar_preprocessed_tap_replay_smoke") - config.rings.results.name = _make_unique_ring_name("radar_results_replay_smoke") - # Private live-config path so the smoke never clobbers the GUI's file. - live_path = PROJECT_ROOT / "python_app/runtime/processing_live_replay_smoke.json" - config.runtime.processing_live_config_path = str( - live_path.relative_to(PROJECT_ROOT) - ) - - radar_key = radar_key_from_config( - model_name=config.radar.model, - serial=config.radar.serial, - sweep_start_hz=config.radar.sweep.start_hz, - sweep_stop_hz=config.radar.sweep.stop_hz, - sweep_points=config.radar.sweep.points, - ifbw_hz=config.radar.sweep.if_bandwidth_hz, - power_dbm=config.radar.sweep.power_dbm, - extra_serials=config.radar_key_extra_parts() or None, - ) - store = NpzStore(PROJECT_ROOT / "python_app/data") - set_name = config.preprocess.s21.calibration.set_name or "neutral_replay" - if set_name not in store.list_sets("s21_calibration", radar_key): - calibration, reference = build_neutral_s21_sets(config) - store.save_set("s21_calibration", radar_key, set_name, calibration) - store.save_set("s21_reference", radar_key, set_name, reference) - logger.info("Created neutral sets '%s' (radar_key=%s)", set_name, radar_key) - config.preprocess.s21.calibration.set_name = set_name - config.preprocess.s21.reference.set_name = set_name - - ProcessingLiveConfigWriter(live_path).write(_sar3d_live_config(args.config)) - - config_writer = ConfigWriter(PROJECT_ROOT / "python_app/runtime") - config_writer.prepare_preprocess_bundles(store, radar_key, config) - config_path = config_writer.write( - config, PROJECT_ROOT / "python_app/runtime/run_config_replay_smoke.json" - ) - - supervisor = ProcessSupervisor(PROJECT_ROOT) - result_reader: ShmRingReader | None = None - tap_reader: ShmRingReader | None = None - failures: list[str] = [] - try: - supervisor.start(config_path) - result_reader = ShmRingReader(config.rings.results.name, open_timeout_s=15.0) - tap_reader = ShmRingReader(config.rings.preprocessed_tap.name, open_timeout_s=15.0) - - deadline = time.monotonic() + args.duration - received = 0 - sar3d_results = 0 - last_peak = 0.0 - last_id = 0 - ids_monotonic = True - while time.monotonic() < deadline: - result = result_reader.pop_result_collection() - if result is None: - time.sleep(0.02) - continue - received += 1 - if last_id and result.collection_id <= last_id: - ids_monotonic = False - last_id = result.collection_id - names = {p.processing_name: p for p in result.collection_payloads} - if "sar3d_proj_xz" in names and "sar3d_proj_bottom" in names: - sar3d_results += 1 - peak_payload = names.get("sar3d_peak") - if peak_payload is not None: - last_peak = float(peak_payload.scalar_value) - - logger.info( - "Results: total=%d, with sar3d projections=%d, last collection_id=%d, last peak=%.4e", - received, sar3d_results, last_id, last_peak, - ) - if received == 0: - failures.append("no result collections received") - if sar3d_results == 0: - failures.append("no sar3d projection payloads received") - if not (last_peak > 0.0): - failures.append(f"sar3d peak is not positive: {last_peak}") - if not ids_monotonic: - failures.append("collection ids were not monotonically increasing") - if not _check_preprocessor_transparency(tap_reader, dataset_dir): - failures.append("preprocessor is not transparent vs recorded snapshots") - finally: - supervisor.stop_all() - if result_reader is not None: - result_reader.close() - if tap_reader is not None: - tap_reader.close() - - if failures: - for failure in failures: - logger.error("FAIL: %s", failure) - return 1 - logger.info("REPLAY SMOKE OK") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/python_app/scripts/sar3d_reference_check.py b/python_app/scripts/sar3d_reference_check.py deleted file mode 100644 index 3b6b962..0000000 --- a/python_app/scripts/sar3d_reference_check.py +++ /dev/null @@ -1,389 +0,0 @@ -"""Reference implementation of the sar3d volume from Multi_antenna_3D_clean_v1.ipynb. - -Executable specification for the C++ port: recomputes a single-frame -(aperture radius 0) 3D SAR volume from a preprocessed snapshot exactly the -way the notebook does, then compares it against a reference *_volume.npz -exported by the notebook. - -Usage: - python sar3d_reference_check.py - - is an extracted replay dataset (contains config_profile.json -and preprocessed//i*_o*_{s21,freq}.npy + meta.json). The snapshot -name and all physics parameters are taken from the npz meta. - -Exits non-zero when the recomputed volume differs from the reference by more -than the tolerance. The only expected difference is the Gaussian smoothing -(manual separable filter vs scipy) at the 1e-6 level. -""" - -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path - -import numpy as np - -TOLERANCE = 2e-6 - -C_LIGHT = 3e8 - -# Notebook constants not recorded in the npz meta (cell 0 / cell 4). -BP_OVERSAMPLE = 8 -BP_WINDOW = True -PAIR_NORM_PERCENTILE = 50.0 -PAIR_NORM_EPS = 1e-15 -COMP_RANGE_POWER = 0.3 -COMP_RANGE_WEIGHT_MAX = 5.0 -COMP_REF_DEPTH = 5.0 -BP_SMOOTH_SIGMA = 1.5 -SAR_Y_RANGE_M = (-0.5, 2.0) -SAR_NORM_EDGE_MARGIN_M = 0.3 -X_MARGIN_M = 2.0 -MOTION_PHASE_SIGN = -1.0 -DIRECTION_SIGN = +1.0 - - -def gaussian_kernel1d(sigma: float, radius: int) -> np.ndarray: - x = np.arange(-radius, radius + 1, dtype=float) - phi = np.exp(-0.5 / (sigma * sigma) * x * x) - return phi / phi.sum() - - -def correlate1d_reflect(arr: np.ndarray, weights: np.ndarray, axis: int) -> np.ndarray: - r = (len(weights) - 1) // 2 - a = np.moveaxis(arr, axis, -1) - pad = np.concatenate([a[..., r - 1::-1], a, a[..., :-r - 1:-1]], axis=-1) - out = np.zeros_like(a) - for k, w in enumerate(weights): - out += w * pad[..., k:k + a.shape[-1]] - return np.moveaxis(out, -1, axis) - - -def gaussian_filter(arr: np.ndarray, sigma: tuple[float, ...], truncate: float = 4.0) -> np.ndarray: - """scipy.ndimage.gaussian_filter equivalent (mode='reflect').""" - out = np.asarray(arr, dtype=float).copy() - for axis, s in enumerate(sigma): - if s <= 0: - continue - radius = int(truncate * float(s) + 0.5) - out = correlate1d_reflect(out, gaussian_kernel1d(float(s), radius), axis) - return out - - -def load_geometry(profile_path: Path) -> dict: - profile = json.loads(profile_path.read_text()) - gpr = profile["gpr"] - tx_geom = {int(e["output_pos"]): (e["x_m"], e.get("y_m", 0.0), e.get("z_m", 0.0)) - for e in gpr["tx_geometry"]} - rx_geom = {int(e["input_pos"]): (e["x_m"], e.get("y_m", 0.0), e.get("z_m", 0.0)) - for e in gpr["rx_geometry"]} - combos = profile["run"]["combos"] - pair_order = [(int(c["output"]), int(c["input"])) for c in combos] - tx_order_full: list[int] = [] - for tx, _ in pair_order: - if tx not in tx_order_full: - tx_order_full.append(tx) - output_idx = sorted(tx_geom) - input_idx = sorted(rx_geom) - tx_xyz = np.array([tx_geom[i] for i in output_idx], dtype=float) - rx_xyz = np.array([rx_geom[i] for i in input_idx], dtype=float) - return { - "output_idx": output_idx, "input_idx": input_idx, - "tx_xyz": tx_xyz, "rx_xyz": rx_xyz, - "pair_order": pair_order, "tx_order_full": tx_order_full, - } - - -def snapshot_timing(snapshot_dir: Path) -> dict | None: - """Per-Tx capture windows from the snapshot's meta.json trace labels.""" - try: - meta = json.loads((snapshot_dir / "meta.json").read_text()) - except OSError: - return None - traces = [t for t in meta.get("traces", []) - if "capture_start_ns" in t and "capture_end_ns" in t and "output" in t] - if not traces: - return None - first = min(int(t["capture_start_ns"]) for t in traces) - last = max(int(t["capture_end_ns"]) for t in traces) - by_tx: dict[int, tuple[int, int]] = {} - for t in traces: - by_tx.setdefault(int(t["output"]), (int(t["capture_start_ns"]), int(t["capture_end_ns"]))) - return { - "acq_s": (last - first) * 1e-9, - "center_abs_s": 0.5 * (first + last) * 1e-9, - "tx_window_s": {tx: ((a - first) * 1e-9, (b - first) * 1e-9) for tx, (a, b) in by_tx.items()}, - } - - -def pair_timestamps(geometry: dict, timing: dict, speed_m_s: float, look_angle_deg: float, - wave_speed: float) -> dict: - """Segment/slot assignment and coarse pair dtau (notebook compute_pair_timestamps_for_snapshot).""" - windows = timing["tx_window_s"] - hw_order = [tx for tx in geometry["tx_order_full"] if tx in windows] - hw_order += [tx for tx in sorted(windows) if tx not in hw_order] - groups: dict[tuple[float, float], list[int]] = {} - for tx in hw_order: - groups.setdefault(windows[tx], []).append(tx) - seg_of = {} - for seg_idx, win in enumerate(sorted(groups)): - for slot, tx in enumerate(groups[win]): - seg_of[tx] = (seg_idx, slot, len(groups[win])) - cos_theta = np.cos(np.radians(look_angle_deg)) - t_ref = 0.5 * timing["acq_s"] - tx_map = {p: k for k, p in enumerate(geometry["output_idx"])} - rx_map = {p: k for k, p in enumerate(geometry["input_idx"])} - ts = {} - for tx_phys, rx_phys in geometry["pair_order"]: - _, slot, n_slots = seg_of[tx_phys] - t0, t1 = windows[tx_phys] - t_center = 0.5 * (t0 + t1) - dz_motion = DIRECTION_SIGN * speed_m_s * (t_center - t_ref) * cos_theta - ts[(tx_map[tx_phys], rx_map[rx_phys])] = { - "slot": slot, "n_slots": n_slots, - "segment_start_s": t0, "segment_time_s": t1 - t0, - "t_center_s": t_center, - "dtau_motion_s": 2.0 * dz_motion / wave_speed, - } - return ts - - -def timing_abs_center(snapshot_dir: Path) -> float: - timing = snapshot_timing(snapshot_dir) - if timing is None: - raise RuntimeError(f"meta.json trace labels required in {snapshot_dir}") - return timing["center_abs_s"] - - -def load_mimo(snapshot_dir: Path, geometry: dict) -> tuple[dict, dict]: - s21_data, freq_data = {}, {} - for f in snapshot_dir.glob("i*_o*_s21.npy"): - parts = f.stem.split("_") - rx_phys = int(parts[0][1:]) - tx_phys = int(parts[1][1:]) - if tx_phys not in geometry["output_idx"] or rx_phys not in geometry["input_idx"]: - continue - key = (geometry["output_idx"].index(tx_phys), geometry["input_idx"].index(rx_phys)) - s21_data[key] = np.load(f) - freq_data[key] = np.asarray(np.load(f.parent / f"i{rx_phys}_o{tx_phys}_freq.npy"), dtype=float) - return s21_data, freq_data - - -def apply_phase_correction(s21: np.ndarray, freq_full: np.ndarray, info: dict, - f_start: float, f_stop: float, speed_m_s: float, - look_angle_deg: float, wave_speed: float) -> np.ndarray: - """Coarse pair + intra-sweep motion correction as one phase ramp before the IFFT.""" - idx = np.flatnonzero((freq_full >= f_start) & (freq_full <= f_stop)) - n_full = len(freq_full) - dt_event = info["segment_time_s"] / (info["n_slots"] * n_full) - t_abs = info["segment_start_s"] + (info["n_slots"] * idx + info["slot"]) * dt_event - dt_intra = t_abs - info["t_center_s"] - dtau_intra = 2.0 * DIRECTION_SIGN * speed_m_s * np.cos(np.radians(look_angle_deg)) * dt_intra / wave_speed - total_dtau = dtau_intra + info["dtau_motion_s"] - phi = 2.0 * np.pi * freq_full[idx] * total_dtau - out = np.array(s21, dtype=np.complex128, copy=True) - out[idx] *= np.exp(1j * MOTION_PHASE_SIGN * phi) - return out - - -def compute_ascan(s21: np.ndarray, freq: np.ndarray, f_start: float, f_stop: float): - """Oversampled complex A-scan with the notebook's frequency-shifted IFFT.""" - mask = (freq >= f_start) & (freq <= f_stop) - freq_cut = freq[mask] - s21_cut = s21[mask] - df = float(np.median(np.diff(freq_cut))) - n = len(freq_cut) - k0 = int(round(freq_cut[0] / df)) - n_fft = (1 << int(np.ceil(np.log2(2 * (k0 + n - 1))))) * BP_OVERSAMPLE - dt = 1.0 / (n_fft * df) - s = s21_cut * np.hanning(n) if BP_WINDOW else s21_cut.copy() - spectrum = np.zeros(n_fft, dtype=np.complex128) - spectrum[k0:k0 + n] = s - return np.arange(n_fft, dtype=float) * dt, np.fft.ifft(spectrum) - - -def interp_complex(tau: np.ndarray, t_axis: np.ndarray, h_axis: np.ndarray) -> np.ndarray: - n = t_axis.size - dt = (float(t_axis[-1]) - float(t_axis[0])) / (n - 1) - pos = (tau - float(t_axis[0])) / dt - inside = (pos >= 0.0) & (pos <= n - 1) - k = np.clip(np.floor(pos), 0, n - 2).astype(np.intp) - frac = pos - k - out = h_axis[k] + frac * (h_axis[k + 1] - h_axis[k]) - return np.where(inside, out, 0.0 + 0.0j) - - -def axis_linspace(lo: float, hi: float, step: float) -> np.ndarray: - return np.linspace(lo, hi, max(int(round((hi - lo) / step)) + 1, 2)) - - -def compute_volume(dataset_root: Path, meta: dict) -> dict: - """Single-frame volume via the notebook's ANIMATION path. - - The reference npz files were exported from animation frames: the base grid - is shifted by the radar displacement between the frame and the scene-origin - snapshot (meta reference_snapshot), antennas get the same offset, and the - depth window follows the radar (depth_shift). Physically it all cancels for - an aperture of radius 0, but replicating the exact float arithmetic is what - makes the comparison bit-tight at the depth-mask knife edge. - """ - geometry = load_geometry(dataset_root / "config_profile.json") - snapshot_dir = dataset_root / "preprocessed" / meta["snapshot"] - - eps_r = 1.0 - wave_speed = C_LIGHT / np.sqrt(eps_r) - f_start = float(meta["f_start_hz"]) - f_stop = float(meta["f_stop_hz"]) - min_depth = float(meta["min_depth_m"]) - max_depth = float(meta["max_depth_m"]) - speed = float(meta["speed_m_s"]) - look_angle = float(meta["look_angle_deg"]) - ground_depth = float(meta["ground_depth_m"]) - voxel = [float(a) for a in meta["voxel_m"]] - - x_tx, y_tx, z_tx = geometry["tx_xyz"].T - x_rx, y_rx, z_rx = geometry["rx_xyz"].T - n_tx, n_rx = len(x_tx), len(x_rx) - - timing = snapshot_timing(snapshot_dir) - if timing is None: - raise RuntimeError(f"meta.json trace labels required in {snapshot_dir}") - ts = pair_timestamps(geometry, timing, speed, look_angle, wave_speed) - - s21_data, freq_data = load_mimo(snapshot_dir, geometry) - ascan_h, ascan_t = {}, {} - for key, s21 in s21_data.items(): - corrected = apply_phase_correction( - s21, freq_data[key], ts[key], f_start, f_stop, speed, look_angle, wave_speed) - t_axis, h = compute_ascan(corrected, freq_data[key], f_start, f_stop) - z_axis = t_axis * wave_speed / 2 - m = (z_axis >= min_depth) & (z_axis <= max_depth) - scale = float(np.percentile(np.abs(h)[m], PAIR_NORM_PERCENTILE)) - if not np.isfinite(scale) or scale <= PAIR_NORM_EPS: - scale = 1.0 - ascan_h[key] = h / (scale + PAIR_NORM_EPS) - ascan_t[key] = t_axis - - pitch = np.radians(look_angle) - cos_p, sin_p = np.cos(pitch), np.sin(pitch) - - def scene_to_radar_yz(y_scene, z_scene): - y0 = y_scene - ground_depth - return y0 * cos_p + z_scene * sin_p, -y0 * sin_p + z_scene * cos_p - - x_ant = np.concatenate([x_tx, x_rx]) - y_lo, y_hi = SAR_Y_RANGE_M - z_span = [(d - float(scene_to_radar_yz(y, 0.0)[1])) / cos_p - for y in (y_lo, y_hi) for d in (min_depth, max_depth)] - gx = axis_linspace(float(x_ant.min() - X_MARGIN_M), float(x_ant.max() + X_MARGIN_M), voxel[0]) - gy = axis_linspace(y_lo, y_hi, voxel[1]) - gz = axis_linspace(min(z_span), max(z_span), voxel[2]) - - # Displacement of this frame relative to the scene-origin snapshot - # (notebook sar_frame_offsets + anim_frame_displacement). - off_dy = off_dz = 0.0 - disp_y = disp_z = 0.0 - reference_name = meta.get("reference_snapshot", meta["snapshot"]) - if reference_name != meta["snapshot"]: - ref_timing = snapshot_timing(dataset_root / "preprocessed" / reference_name) - if ref_timing is None: - raise RuntimeError(f"meta.json trace labels required in {reference_name}") - dt = timing_abs_center(snapshot_dir) - ref_timing["center_abs_s"] - horizontal = DIRECTION_SIGN * speed * dt - off_dy = float(horizontal * np.sin(pitch)) - off_dz = float(horizontal * np.cos(pitch)) - # Scene displacement = difference of two transformed points; the ground - # lift cancels, leaving essentially (0, ~0, horizontal). - disp_y = (off_dy * cos_p - off_dz * sin_p + ground_depth) - ground_depth - disp_z = off_dy * sin_p + off_dz * cos_p - gy_s = gy + disp_y - gz_s = gz + disp_z - depth_shift = off_dz - grid_xx, grid_zz = np.meshgrid(gx, gz_s) - - ground_plane_y = (-ground_depth + COMP_REF_DEPTH * sin_p) / cos_p - - def pair_geo_ref(i, j): - xc = (x_tx[i] + x_rx[j]) / 2.0 - r_tx = np.sqrt((xc - x_tx[i]) ** 2 + (ground_plane_y - y_tx[i]) ** 2 + (COMP_REF_DEPTH - z_tx[i]) ** 2) - r_rx = np.sqrt((xc - x_rx[j]) ** 2 + (ground_plane_y - y_rx[j]) ** 2 + (COMP_REF_DEPTH - z_rx[j]) ** 2) - return 1.0 / (r_tx * r_rx + 1e-12) + 1e-30 - - geo_ref = {(i, j): pair_geo_ref(i, j) for i in range(n_tx) for j in range(n_rx)} - dx2_tx = [(grid_xx - x_tx[i]) ** 2 for i in range(n_tx)] - dx2_rx = [(grid_xx - x_rx[j]) ** 2 for j in range(n_rx)] - - acc_vol = np.zeros((gy.size, gz.size, gx.size), dtype=np.complex128) - cnt_vol = np.zeros(acc_vol.shape, dtype=float) - for iy, y_plane in enumerate(gy_s): - y_rad, z_rad = scene_to_radar_yz(y_plane, grid_zz) - depth_ok = (z_rad - depth_shift >= min_depth) & (z_rad - depth_shift <= max_depth) - r_tx_all = [np.sqrt(dx2_tx[i] + (y_rad - (y_tx[i] + off_dy)) ** 2 + (z_rad - (z_tx[i] + off_dz)) ** 2) - for i in range(n_tx)] - r_rx_all = [np.sqrt(dx2_rx[j] + (y_rad - (y_rx[j] + off_dy)) ** 2 + (z_rad - (z_rx[j] + off_dz)) ** 2) - for j in range(n_rx)] - acc = np.zeros_like(grid_xx, dtype=np.complex128) - cnt = np.zeros_like(grid_xx, dtype=float) - for i in range(n_tx): - for j in range(n_rx): - key = (i, j) - if key not in ascan_h: - continue - r_tx, r_rx = r_tx_all[i], r_rx_all[j] - tau = (r_tx + r_rx) / wave_speed - t_axis = ascan_t[key] - valid = depth_ok & (tau >= t_axis[0]) & (tau <= t_axis[-1]) - h = interp_complex(tau, t_axis, ascan_h[key]) - ratio = (1.0 / (r_tx * r_rx + 1e-12) + 1e-30) / geo_ref[key] - weight = np.clip(1.0 / (ratio ** COMP_RANGE_POWER + 1e-12), 0.0, COMP_RANGE_WEIGHT_MAX) - acc += np.where(valid, h * weight, 0.0 + 0.0j) - cnt += valid - acc_vol[iy] = acc - cnt_vol[iy] = cnt - - volume = np.abs(acc_vol / (cnt_vol + 1e-12)) - z_rad_planes = np.asarray([scene_to_radar_yz(yp, grid_zz)[1] - depth_shift for yp in gy_s]) - volume = np.where((z_rad_planes >= min_depth) & (z_rad_planes <= max_depth), volume, 0.0) - ref_planes = (gy_s >= gy_s[0] + SAR_NORM_EDGE_MARGIN_M) & (gy_s <= gy_s[-1] - SAR_NORM_EDGE_MARGIN_M) - level_raw = volume[ref_planes].max() + 1e-12 - volume = gaussian_filter(volume / level_raw, sigma=(0.0, BP_SMOOTH_SIGMA, BP_SMOOTH_SIGMA)) - level_smooth = volume[ref_planes].max() + 1e-12 - volume = np.clip(volume / level_smooth, 0.0, None).astype(np.float32) - return {"volume": volume, "level": float(level_raw * level_smooth), "x": gx, "y": gy, "z": gz} - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - parser.add_argument("dataset_root", type=Path, - help="extracted dataset dir (config_profile.json + preprocessed/)") - parser.add_argument("reference_npz", type=Path, help="reference *_volume.npz from the notebook") - args = parser.parse_args() - - ref = np.load(args.reference_npz) - meta = json.loads(str(ref["meta"])) - print(f"snapshot: {meta['snapshot']}") - result = compute_volume(args.dataset_root, meta) - - volume, ref_volume = result["volume"], ref["volume"] - axes_ok = all(np.allclose(result[k], ref[f"{k}_m"]) for k in ("x", "y", "z")) - level_rel = abs(result["level"] - meta["level"]) / abs(meta["level"]) - print(f"axes match: {axes_ok}; level rel diff: {level_rel:.3e} " - f"(mine {result['level']:.9f}, ref {meta['level']:.9f})") - if volume.shape != ref_volume.shape: - print(f"FAIL: shape mismatch {volume.shape} vs {ref_volume.shape}") - return 1 - diff = np.abs(volume.astype(np.float64) - ref_volume.astype(np.float64)) - argmax = np.unravel_index(int(np.argmax(volume)), volume.shape) - ref_argmax = np.unravel_index(int(np.argmax(ref_volume)), ref_volume.shape) - print(f"max |diff| = {diff.max():.3e}, mean = {diff.mean():.3e}") - print(f"argmax: mine {argmax}, ref {ref_argmax}") - ok = axes_ok and diff.max() <= TOLERANCE and argmax == ref_argmax and level_rel <= TOLERANCE - print("OK" if ok else "FAIL") - return 0 if ok else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/python_app/scripts/sar3d_volume_compare.py b/python_app/scripts/sar3d_volume_compare.py deleted file mode 100644 index 1bb21b7..0000000 --- a/python_app/scripts/sar3d_volume_compare.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Compare a sar3d volume dumped by the C++ processor with a notebook npz export. - -Usage: - python sar3d_volume_compare.py - - is the path prefix written by `sar3d_parity --dump-dir` (or by the -processor with `sar3d_volume_dump_dir` set): .f32 holds the raw -float32 volume [iy][iz][ix], .json its axes and level. - -The C++ path accumulates the backprojection in float32 (CUDA kernel), so the -comparison is tolerance-based rather than bitwise; geometry is computed in -double, keeping the expected difference at the 1e-5 level. Voxels within the -smoothing radius of the depth-mask boundary are reported separately: there the -float32 knife-edge can flip whole boundary voxels (the same effect exists -between two float64 runs with shifted grids). -""" - -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path - -import numpy as np - -MAX_DIFF_TOLERANCE = 1e-4 -LEVEL_REL_TOLERANCE = 1e-4 -# Gaussian smoothing radius (sigma 1.5 -> radius 6) around the depth-mask edge. -EDGE_EXCLUSION_VOXELS = 7 - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - parser.add_argument("dump_base", type=Path, help="path prefix of the .f32/.json dump") - parser.add_argument("reference_npz", type=Path, help="reference *_volume.npz from the notebook") - args = parser.parse_args() - - header = json.loads(Path(str(args.dump_base) + ".json").read_text()) - shape = (header["ny"], header["nz"], header["nx"]) - volume = np.fromfile(str(args.dump_base) + ".f32", dtype=np.float32).reshape(shape) - - ref = np.load(args.reference_npz) - ref_volume = ref["volume"] - meta = json.loads(str(ref["meta"])) - print(f"snapshot: {meta['snapshot']}") - if volume.shape != ref_volume.shape: - print(f"FAIL: shape mismatch {volume.shape} vs {ref_volume.shape}") - return 1 - - axes_ok = all( - np.allclose(np.asarray(header[key]), ref[key]) - for key in ("x_m", "y_m", "z_m") - ) - level_rel = abs(header["level"] - meta["level"]) / abs(meta["level"]) - print(f"axes match: {axes_ok}; level rel diff: {level_rel:.3e} " - f"(dump {header['level']:.9f}, ref {meta['level']:.9f})") - - diff = np.abs(volume.astype(np.float64) - ref_volume.astype(np.float64)) - - # Depth-mask boundary band: voxels whose (iy, iz) sit within the smoothing - # radius of a masked/unmasked transition. Reconstructed from the reference - # parameters exactly like the processor computes the mask. - pitch = np.radians(float(meta.get("ground_pitch_deg") or meta["look_angle_deg"])) - y0 = np.asarray(ref["y_m"]) - float(meta["ground_depth_m"]) - z_rad = -y0[:, None] * np.sin(pitch) + np.asarray(ref["z_m"])[None, :] * np.cos(pitch) - mask = (z_rad >= float(meta["min_depth_m"])) & (z_rad <= float(meta["max_depth_m"])) - edge = np.zeros_like(mask) - r = EDGE_EXCLUSION_VOXELS - for shift in range(-r, r + 1): - edge |= np.roll(mask, shift, axis=1) != mask - interior = ~edge - - argmax = np.unravel_index(int(np.argmax(volume)), volume.shape) - ref_argmax = np.unravel_index(int(np.argmax(ref_volume)), ref_volume.shape) - interior_diff = diff[interior[:, :, None] & np.ones(volume.shape, dtype=bool)] - print(f"max |diff| (all voxels) = {diff.max():.3e}") - print(f"max |diff| (off depth-mask edge) = {interior_diff.max():.3e}, " - f"mean = {interior_diff.mean():.3e}") - print(f"argmax: dump {argmax}, ref {ref_argmax}") - - ok = (axes_ok and level_rel <= LEVEL_REL_TOLERANCE and argmax == ref_argmax - and interior_diff.max() <= MAX_DIFF_TOLERANCE) - print("OK" if ok else "FAIL") - return 0 if ok else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/python_app/tests/test_gui_profile_codec.py b/python_app/tests/test_gui_profile_codec.py index fdd5099..cba565c 100644 --- a/python_app/tests/test_gui_profile_codec.py +++ b/python_app/tests/test_gui_profile_codec.py @@ -8,6 +8,7 @@ from python_app.models.gui_profile_model import ( GuiPassThroughStateModel, GuiProcessingStateModel, GuiProfileModel, + GuiSar3dStateModel, GuiStateModel, ) @@ -53,6 +54,31 @@ class GuiProfileCodecTest(unittest.TestCase): twice = GuiProfileModel.from_dict(once).to_dict() self.assertEqual(once["gui"], twice["gui"]) + def test_sar3d_section_round_trips(self) -> None: + sar3d = GuiSar3dStateModel( + input_positions="0,2", + output_positions="1,3", + min_depth_m=2.5, + max_depth_m=9.0, + start_freq_mhz=2000.0, + stop_freq_mhz=5500.0, + speed_m_s=1.42, + ignore_socket_speed_enabled=True, + look_angle_deg=6.5, + frame_radius=3, + voxel_m=0.035, + bp_ground_depth_m=1.1, + comp_range_power=0.5, + use_compensation=False, + normalize_each_frame=False, + ) + profile = GuiProfileModel(gui=GuiStateModel(processing=GuiProcessingStateModel(sar3d=sar3d))) + + decoded = GuiProfileModel.from_dict(profile.to_dict()) + + assert decoded.gui is not None + self.assertEqual(decoded.gui.processing.sar3d, sar3d) + def test_missing_gui_section_yields_none(self) -> None: self.assertIsNone(GuiProfileModel.from_dict({}).gui) diff --git a/python_app/tests/test_socket_speed_controls.py b/python_app/tests/test_socket_speed_controls.py new file mode 100644 index 0000000..d0e6bd1 --- /dev/null +++ b/python_app/tests/test_socket_speed_controls.py @@ -0,0 +1,65 @@ +"""Speed authority between a locator socket client and the GUI field. + +The data_processor uses the speed a socket client streams unless the live config +sets `ignore_socket_speed`; then `gpr_speed_m_s` from the GUI field applies. Each +GPR-like page (gpr, legacy_gpr, sar3d) has its own checkbox and field, and the +field is editable only while it is the one in force. +""" + +from __future__ import annotations + +import os +import unittest +from pathlib import Path + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") +from PyQt6.QtWidgets import QApplication # noqa: E402 + +from python_app.gui.app_window import AppWindow # noqa: E402 + +_app: QApplication | None = None +_window: AppWindow | None = None + +# mode -> (checkbox, speed field) attributes of its page +_PAGES = { + "gpr": ("_gpr_ignore_socket_speed_enabled", "_gpr_speed_m_s"), + "legacy_gpr": ("_legacy_gpr_ignore_socket_speed_enabled", "_legacy_gpr_speed_m_s"), + "sar3d": ("_sar3d_ignore_socket_speed_enabled", "_sar3d_speed_m_s"), +} + + +def setUpModule() -> None: + global _app, _window + _app = QApplication.instance() or QApplication([]) + _window = AppWindow(Path(".")) + + +def tearDownModule() -> None: + if _window is not None: + _window.close() + + +class SocketSpeedControlsTest(unittest.TestCase): + def test_speed_field_is_editable_only_while_the_socket_is_ignored(self) -> None: + assert _window is not None + for mode, (checkbox_name, speed_name) in _PAGES.items(): + checkbox, speed = getattr(_window, checkbox_name), getattr(_window, speed_name) + for ignore in (True, False): + checkbox.setChecked(ignore) + self.assertEqual(speed.isEnabled(), ignore, msg=f"{mode}, ignore={ignore}") + + def test_live_config_takes_the_active_page(self) -> None: + assert _window is not None + for mode, (checkbox_name, speed_name) in _PAGES.items(): + _window._set_combo_current_text(_window._processing_mode, mode) + getattr(_window, checkbox_name).setChecked(True) + getattr(_window, speed_name).setValue(1.25) + config = _window._live_processing_config() + self.assertTrue(config.ignore_socket_speed, msg=mode) + self.assertAlmostEqual(config.gpr_speed_m_s, 1.25, msg=mode) + getattr(_window, checkbox_name).setChecked(False) + self.assertFalse(_window._live_processing_config().ignore_socket_speed, msg=mode) + + +if __name__ == "__main__": + unittest.main() diff --git a/requirements.txt b/requirements.txt index 6522b48..773ef96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,8 +12,8 @@ pyqtgraph>=0.13.7 rpi-hardware-pwm>=0.2.2,<1 fastapi>=0.110 # embedded web UI (browser view + controls), served in-process uvicorn[standard]>=0.29 # ASGI server for the web UI, run on a background thread -# CUDA headers + libnvrtc for building the vendored gpr_cpp library (3D SAR). -# x86_64 dev machines only: on Jetson (aarch64) CUDA comes with JetPack at -# /usr/local/cuda and build_cmake.sh falls back to it automatically. +# nvrtc.h, cuda.h and libnvrtc for the sar3d processor's GPU code (kernels are +# compiled at run time, no nvcc). x86_64 only: on Jetson (aarch64) CUDA comes +# with JetPack at /usr/local/cuda, where the Makefile finds it. nvidia-cuda-nvrtc==13.3.33; platform_machine == "x86_64" # nvrtc.h + libnvrtc nvidia-cuda-runtime==13.3.29; platform_machine == "x86_64" # cuda.h (Driver API) diff --git a/run_config_examples/run_config_replay_vna.example.json b/run_config_examples/run_config_replay_vna.example.json index 5e545eb..da0dddb 100644 --- a/run_config_examples/run_config_replay_vna.example.json +++ b/run_config_examples/run_config_replay_vna.example.json @@ -178,21 +178,12 @@ "speed_m_s": 0.854, "ignore_socket_speed_enabled": true, "look_angle_deg": 6.0, - "direction_sign": 1.0, - "apply_freq_phase_correction": true, "frame_radius": 6, - "edge_frame_weight": 0.75, - "pitch_deg": 6.0, - "center_depth_m": 5.6, - "half_width_x_m": 1.8, - "half_width_y_m": 1.0, - "half_width_z_m": 1.8, - "grid_nx": 150, - "grid_ny": 120, - "grid_nz": 160, + "voxel_m": 0.02, + "bp_ground_depth_m": 1.25, + "comp_range_power": 0.3, "use_compensation": true, - "normalize_pairs": true, - "bottom_view_deg": 0.0 + "normalize_each_frame": true } }, "preprocess_dialog": { diff --git a/start.sh b/start.sh index 726d617..e280110 100755 --- a/start.sh +++ b/start.sh @@ -133,10 +133,27 @@ except Exception: echo "[start.sh] Detected radar model: ${RADAR_MODEL} (config: ${config_path})" } +# radar.replay.dataset_dir of a config, empty unless it replays a recording. +replay_dataset_dir() { + python3 -c ' +import json, sys +try: + radar = json.load(open(sys.argv[1])).get("radar", {}) + print(str(radar.get("replay", {}).get("dataset_dir", "")).strip()) +except Exception: + print("") +' "$1" 2>/dev/null +} + # Acquisition producer for the active model, mirroring the process supervisor's -# selection so --producer-only behaves identically to a full pipeline launch. +# selection so --producer-only behaves identically to a full pipeline launch: a +# replay dataset takes precedence over the model's producer. producer_command() { local config_path="$1" + if [[ -n "$(replay_dataset_dir "${config_path}")" ]]; then + printf '%s\0' "${PYTHON_CMD}" -m python_app.scripts.replay_raw_producer --config "${config_path}" + return + fi case "${RADAR_MODEL}" in kamil_adc) printf '%s\0' "${PYTHON_CMD}" -m python_app.scripts.kamil_adc_raw_producer --config "${config_path}" ;; @@ -244,7 +261,7 @@ ensure_system_dependencies() { return fi - echo "[start.sh] Installing system dependency: libusb-1.0 dev headers..." + echo "[start.sh] Installing system dependencies: libusb-1.0 dev headers (and Qt xcb libraries on apt)..." if command -v apt-get >/dev/null 2>&1; then run_privileged apt-get update run_privileged apt-get install -y pkg-config libusb-1.0-0-dev libxcb-cursor0 libxkbcommon-x11-0