From 7a1c0a364a3ff4b3b95e5d2a718557fa951accec Mon Sep 17 00:00:00 2001 From: Ariel Rokem Date: Wed, 24 Jun 2026 11:55:02 -0700 Subject: [PATCH 1/7] Convert files to md for transition to uniform sphinx/myst look and feel. --- docs/api_layers.md | 73 +++++ docs/api_layers.rst | 82 ------ docs/{benchmarks.rst => benchmarks.md} | 128 ++++----- docs/building.md | 141 +++++++++ docs/building.rst | 152 ---------- docs/concepts.md | 107 +++++++ docs/concepts.rst | 116 -------- docs/conf.py | 23 +- docs/index.md | 71 +++++ docs/index.rst | 63 ---- docs/integration.md | 309 ++++++++++++++++++++ docs/integration.rst | 316 --------------------- docs/{introduction.rst => introduction.md} | 43 +-- docs/linting.md | 57 ++++ docs/linting.rst | 62 ---- docs/nifti.md | 40 +++ docs/nifti.rst | 44 --- docs/quick_start.md | 95 +++++++ docs/quick_start.rst | 100 ------- docs/reading.md | 151 ++++++++++ docs/reading.rst | 159 ----------- docs/requirements.txt | 3 +- docs/spatial_queries.md | 57 ++++ docs/spatial_queries.rst | 61 ---- docs/spec.md | 120 ++++++++ docs/spec.rst | 130 --------- docs/streaming.md | 165 +++++++++++ docs/streaming.rst | 166 ----------- docs/writing.md | 67 +++++ docs/writing.rst | 71 ----- 30 files changed, 1562 insertions(+), 1610 deletions(-) create mode 100644 docs/api_layers.md delete mode 100644 docs/api_layers.rst rename docs/{benchmarks.rst => benchmarks.md} (59%) create mode 100644 docs/building.md delete mode 100644 docs/building.rst create mode 100644 docs/concepts.md delete mode 100644 docs/concepts.rst create mode 100644 docs/index.md delete mode 100644 docs/index.rst create mode 100644 docs/integration.md delete mode 100644 docs/integration.rst rename docs/{introduction.rst => introduction.md} (53%) create mode 100644 docs/linting.md delete mode 100644 docs/linting.rst create mode 100644 docs/nifti.md delete mode 100644 docs/nifti.rst create mode 100644 docs/quick_start.md delete mode 100644 docs/quick_start.rst create mode 100644 docs/reading.md delete mode 100644 docs/reading.rst create mode 100644 docs/spatial_queries.md delete mode 100644 docs/spatial_queries.rst create mode 100644 docs/spec.md delete mode 100644 docs/spec.rst create mode 100644 docs/streaming.md delete mode 100644 docs/streaming.rst create mode 100644 docs/writing.md delete mode 100644 docs/writing.rst diff --git a/docs/api_layers.md b/docs/api_layers.md new file mode 100644 index 0000000..c9da464 --- /dev/null +++ b/docs/api_layers.md @@ -0,0 +1,73 @@ +# API Layers + +trx-cpp provides three complementary interfaces. Understanding when to use +each one avoids boilerplate and makes code easier to reason about. + +## AnyTrxFile — runtime-typed + +{class}`trx::AnyTrxFile` is the simplest entry point. It reads the positions +dtype directly from the file and exposes all arrays as {class}`trx::TypedArray` +objects with a `dtype` string field. Use this when you only have a file path +and do not need to perform arithmetic on positions at the C++ type level. + +```cpp +auto trx = trx::load_any("tracks.trx"); + +std::cout << trx.positions.dtype << "\n"; // e.g. "float16" +std::cout << trx.num_streamlines() << "\n"; + +// Access positions as any floating-point type you choose. +auto pos = trx.positions.as_matrix(); // (NB_VERTICES, 3) + +trx.close(); +``` + +## TrxFile\
— compile-time typed + +{class}`trx::TrxFile` is templated on the positions dtype (`Eigen::half`, +`float`, or `double`). Positions and DPV arrays are exposed as +`Eigen::Matrix` directly — no element-wise conversion. Use this +when the dtype is known, or when you are performing per-vertex arithmetic and +want the compiler to enforce type consistency. + +```cpp +auto reader = trx::load("tracks.trx"); +auto& trx = *reader; + +// trx.streamlines->_data is Eigen::Matrix +std::cout << trx.streamlines->_data.rows() << " vertices\n"; + +reader->close(); +``` + +The recommended typed entry point is {func}`trx::with_trx_reader`, which +detects the dtype at runtime and dispatches to the correct instantiation: + +```cpp +trx::with_trx_reader("tracks.trx", [](auto& trx) { + // trx is TrxFile
for the detected dtype + std::cout << trx.num_vertices() << "\n"; +}); +``` + +## TrxReader\
— RAII lifetime management + +{class}`trx::TrxReader` is a thin RAII wrapper around {class}`trx::TrxFile`. +When a TRX zip is loaded, trx-cpp extracts it to a temporary directory. +`TrxReader` owns that directory and removes it when it goes out of scope, +ensuring no temporary files are leaked. + +In most cases you do not need to instantiate `TrxReader` directly. The +convenience functions `trx::load_any` and `trx::with_trx_reader` handle +the lifetime automatically. `TrxReader` is available for advanced use cases +where explicit control over the backing resource lifetime is required — for +example, when passing a `TrxFile` across a function boundary and needing the +temporary directory to outlive the calling scope. + +## Summary + +| Class | Dtype | Best for | +| --------------- | --------------------------- | ------------------------- | +| `AnyTrxFile` | Runtime (read from file) | Inspection, generic tools | +| `TrxFile
` | Compile-time | Per-vertex computation | +| `TrxReader
` | Compile-time + RAII cleanup | Explicit lifetime control | diff --git a/docs/api_layers.rst b/docs/api_layers.rst deleted file mode 100644 index 20b5366..0000000 --- a/docs/api_layers.rst +++ /dev/null @@ -1,82 +0,0 @@ -API Layers -========== - -trx-cpp provides three complementary interfaces. Understanding when to use -each one avoids boilerplate and makes code easier to reason about. - -AnyTrxFile — runtime-typed --------------------------- - -:class:`trx::AnyTrxFile` is the simplest entry point. It reads the positions -dtype directly from the file and exposes all arrays as :class:`trx::TypedArray` -objects with a ``dtype`` string field. Use this when you only have a file path -and do not need to perform arithmetic on positions at the C++ type level. - -.. code-block:: cpp - - auto trx = trx::load_any("tracks.trx"); - - std::cout << trx.positions.dtype << "\n"; // e.g. "float16" - std::cout << trx.num_streamlines() << "\n"; - - // Access positions as any floating-point type you choose. - auto pos = trx.positions.as_matrix(); // (NB_VERTICES, 3) - - trx.close(); - -TrxFile
— compile-time typed ---------------------------------- - -:class:`trx::TrxFile` is templated on the positions dtype (``Eigen::half``, -``float``, or ``double``). Positions and DPV arrays are exposed as -``Eigen::Matrix`` directly — no element-wise conversion. Use this -when the dtype is known, or when you are performing per-vertex arithmetic and -want the compiler to enforce type consistency. - -.. code-block:: cpp - - auto reader = trx::load("tracks.trx"); - auto& trx = *reader; - - // trx.streamlines->_data is Eigen::Matrix - std::cout << trx.streamlines->_data.rows() << " vertices\n"; - - reader->close(); - -The recommended typed entry point is :func:`trx::with_trx_reader`, which -detects the dtype at runtime and dispatches to the correct instantiation: - -.. code-block:: cpp - - trx::with_trx_reader("tracks.trx", [](auto& trx) { - // trx is TrxFile
for the detected dtype - std::cout << trx.num_vertices() << "\n"; - }); - -TrxReader
— RAII lifetime management ----------------------------------------- - -:class:`trx::TrxReader` is a thin RAII wrapper around :class:`trx::TrxFile`. -When a TRX zip is loaded, trx-cpp extracts it to a temporary directory. -``TrxReader`` owns that directory and removes it when it goes out of scope, -ensuring no temporary files are leaked. - -In most cases you do not need to instantiate ``TrxReader`` directly. The -convenience functions ``trx::load_any`` and ``trx::with_trx_reader`` handle -the lifetime automatically. ``TrxReader`` is available for advanced use cases -where explicit control over the backing resource lifetime is required — for -example, when passing a ``TrxFile`` across a function boundary and needing the -temporary directory to outlive the calling scope. - -Summary -------- - -+--------------------+----------------------------------+-----------------------------+ -| Class | Dtype | Best for | -+====================+==================================+=============================+ -| ``AnyTrxFile`` | Runtime (read from file) | Inspection, generic tools | -+--------------------+----------------------------------+-----------------------------+ -| ``TrxFile
`` | Compile-time | Per-vertex computation | -+--------------------+----------------------------------+-----------------------------+ -| ``TrxReader
`` | Compile-time + RAII cleanup | Explicit lifetime control | -+--------------------+----------------------------------+-----------------------------+ diff --git a/docs/benchmarks.rst b/docs/benchmarks.md similarity index 59% rename from docs/benchmarks.rst rename to docs/benchmarks.md index 2f70525..02246f7 100644 --- a/docs/benchmarks.rst +++ b/docs/benchmarks.md @@ -1,72 +1,70 @@ -Benchmarks -========== +# Benchmarks This page documents the benchmarking suite and how to interpret the results. The benchmarks are designed for realistic tractography workloads (HPC scale), not for CI. They focus on file size, throughput, and interactive spatial queries. -Data ----- +## Data All benchmarks use a real dataset from the Human Connectome Project (Young Adult dataset). The original size of the `tck` file is 18.5GB. The streamlines were generated by `tckgen` and the data per vertex were generated by `tcksift2`. This data was converted to TRX format (float16 positions) with nibabel. -TRX size vs streamline count ----------------------------- +## TRX size vs streamline count This benchmark creates subsets of the reference TRX file and measures the on-disk size for different streamline counts. This also calculates the size with and without additional features such as DPS, DPV and groups. -.. figure:: _static/benchmarks/trx_size_vs_streamlines.png - :alt: TRX file size vs streamlines - :align: center +:::{figure} _static/benchmarks/trx_size_vs_streamlines.png +:align: center +:alt: TRX file size vs streamlines - File size (MB) as a function of streamline count. +File size (MB) as a function of streamline count. +::: -Translate + stream write throughput ------------------------------------ +## Translate + stream write throughput This benchmark simulates loading a TRX file, applying a spatial transform, and saving it to a new TRX file (preserving DPV, DPS, groups, etc.). The workflow is: 1. Load the input TRX file (decompresses to a temp directory). -2. Copy all metadata files (header, offsets) and data array directories (``dps``, ``dpv``, - ``groups``) to a fresh output directory using kernel-level filesystem copies - (``fcopyfile`` on macOS, ``sendfile`` on Linux). The data never passes through the +2. Copy all metadata files (header, offsets) and data array directories (`dps`, `dpv`, + `groups`) to a fresh output directory using kernel-level filesystem copies + (`fcopyfile` on macOS, `sendfile` on Linux). The data never passes through the process's mapped address space. 3. Release DPS/DPV mmaps immediately after the copy so their pages are not counted against the benchmark's RSS. 4. Iterate through every position point, apply a +1 mm translation in x/y/z, and stream the translated positions directly to the output directory. -5. Pack the output directory into a ``.trx`` zip archive using ``zip_source_file`` +5. Pack the output directory into a `.trx` zip archive using `zip_source_file` (libzip buffered I/O), which reads files in chunks without mapping them into the process address space. -The reported ``max_rss_kb`` is the **per-iteration RSS delta** — the peak increase in +The reported `max_rss_kb` is the **per-iteration RSS delta** — the peak increase in resident memory observed during that single iteration, measured with -``mach_task_basic_info`` (macOS) or ``/proc/self/status`` ``VmRSS`` (Linux). This avoids +`mach_task_basic_info` (macOS) or `/proc/self/status` `VmRSS` (Linux). This avoids contamination from previous benchmark iterations whose pages may still be resident. -.. figure:: _static/benchmarks/trx_translate_write_time.png - :alt: Translate + stream write time - :align: center +:::{figure} _static/benchmarks/trx_translate_write_time.png +:align: center +:alt: Translate + stream write time - End-to-end time for translating and rewriting streamlines. +End-to-end time for translating and rewriting streamlines. +::: -.. figure:: _static/benchmarks/trx_translate_write_rss.png - :alt: Translate + stream write RSS - :align: center +:::{figure} _static/benchmarks/trx_translate_write_rss.png +:align: center +:alt: Translate + stream write RSS - Peak RSS delta during translate + stream write. Because DPS/DPV are copied at the - filesystem level and their mmaps are released before position processing begins, only - the positions chunk buffer (configurable via ``TRX_BENCH_CHUNK_BYTES``, default 1 GB) - and the output stream contribute to the measured RSS. +Peak RSS delta during translate + stream write. Because DPS/DPV are copied at the +filesystem level and their mmaps are released before position processing begins, only +the positions chunk buffer (configurable via `TRX_BENCH_CHUNK_BYTES`, default 1 GB) +and the output stream contribute to the measured RSS. +::: -Spatial slab query latency --------------------------- +## Spatial slab query latency Streamlines need to be visualized. One common method for this is to break the tractogram into spatial slabs and query each slab individually. @@ -78,36 +76,36 @@ This benchmark uses the Axis-aligned bounding box (AABB) method to create subset After computing the AABBs, it issues 20 spatial queries using 5 mm slabs that sweep through the tractogram volume. Each slab query mimics a GUI slice update and records its timing so distributions can be visualized. To keep results representative of interactive use, each query returns at most 500 randomly sampled -streamlines from all those intersecting the slab (configurable via ``TRX_BENCH_MAX_QUERY_STREAMLINES``). +streamlines from all those intersecting the slab (configurable via `TRX_BENCH_MAX_QUERY_STREAMLINES`). -.. figure:: _static/benchmarks/trx_query_slab_timings.png - :alt: Slab query timings - :align: center +:::{figure} _static/benchmarks/trx_query_slab_timings.png +:align: center +:alt: Slab query timings - Distribution of per-slab query latency. +Distribution of per-slab query latency. +::: +## Running the benchmarks -Running the benchmarks ----------------------- - -Build into ``build-release`` (the default expected by ``run_benchmarks.sh``), then use +Build into `build-release` (the default expected by `run_benchmarks.sh`), then use the helper script to run all groups and generate plots: -.. code-block:: bash - - cmake -S . -B build-release \ - -DCMAKE_BUILD_TYPE=Release \ - -DTRX_BUILD_BENCHMARKS=ON - cmake --build build-release --target bench_trx_realdata +```bash +cmake -S . -B build-release \ + -DCMAKE_BUILD_TYPE=Release \ + -DTRX_BUILD_BENCHMARKS=ON +cmake --build build-release --target bench_trx_realdata - # Run all benchmark groups (filesize, translate+write, query) in sequence. - ./bench/run_benchmarks.sh +# Run all benchmark groups (filesize, translate+write, query) in sequence. +./bench/run_benchmarks.sh - # Generate plots into docs/_static/benchmarks. - Rscript bench/plot_bench.R --bench-dir bench --out-dir docs/_static/benchmarks +# Generate plots into docs/_static/benchmarks. +Rscript bench/plot_bench.R --bench-dir bench --out-dir docs/_static/benchmarks +``` -``run_benchmarks.sh`` accepts the following flags: +`run_benchmarks.sh` accepts the following flags: +```{eval-rst} .. list-table:: :widths: 30 70 :header-rows: 1 @@ -125,9 +123,11 @@ the helper script to run all groups and generate plots: run time manageable; ``full`` runs every combination. * - ``--verbose`` - Enable per-benchmark progress logging. +``` -Environment variables (set before calling ``run_benchmarks.sh`` or the binary directly): +Environment variables (set before calling `run_benchmarks.sh` or the binary directly): +```{eval-rst} .. list-table:: :widths: 35 65 :header-rows: 1 @@ -144,26 +144,26 @@ Environment variables (set before calling ``run_benchmarks.sh`` or the binary di - If set, per-slab query latencies are appended as JSONL to this path. * - ``TRX_RSS_SAMPLES_PATH`` - If set, time-series RSS samples during file-size runs are appended as JSONL. +``` Example — slower storage with a reduced streamline cap: -.. code-block:: bash - - TRX_BENCH_BUFFER_MULTIPLIER=4 \ - TRX_BENCH_MAX_STREAMLINES=5000000 \ - ./bench/run_benchmarks.sh \ - --reference /path/to/reference.trx \ - --out-dir bench/results_hdd +```bash +TRX_BENCH_BUFFER_MULTIPLIER=4 \ +TRX_BENCH_MAX_STREAMLINES=5000000 \ + ./bench/run_benchmarks.sh \ + --reference /path/to/reference.trx \ + --out-dir bench/results_hdd +``` -RSS measurement -~~~~~~~~~~~~~~~ +### RSS measurement -The ``max_rss_kb`` counter in every benchmark reports the **per-iteration RSS delta**: +The `max_rss_kb` counter in every benchmark reports the **per-iteration RSS delta**: the maximum increase in resident memory observed during a single benchmark iteration, sampled at the start and end of each iteration. This avoids contamination from earlier iterations whose pages may still be resident in the OS page cache. -On macOS the current RSS is read via ``mach_task_basic_info.resident_size``; on Linux -via ``VmRSS`` in ``/proc/self/status``. The ``run_benchmarks.sh`` script runs each +On macOS the current RSS is read via `mach_task_basic_info.resident_size`; on Linux +via `VmRSS` in `/proc/self/status`. The `run_benchmarks.sh` script runs each benchmark group (filesize, translate, query) as a separate process to prevent cross-group RSS accumulation. diff --git a/docs/building.md b/docs/building.md new file mode 100644 index 0000000..b67dc79 --- /dev/null +++ b/docs/building.md @@ -0,0 +1,141 @@ +# Building + +## Dependencies + +Required: + +- C++17 compiler +- zlib (`zlib1g-dev` / `zlib-devel` / Homebrew `zlib`) + +libzip and Eigen 3.4+ are fetched automatically by CMake when not found +locally — no manual installation required. + +## Installing dependencies + +The examples below include GoogleTest, which is only required when building +the tests. Ninja is optional but recommended. + +On Debian-based systems: + +```bash +sudo apt-get install \ + zlib1g-dev \ + ninja-build \ + libgtest-dev +``` + +On Mac OS, you can install the dependencies with brew: + +```bash +brew install googletest ninja zlib +``` + +On Windows, you can install the dependencies through vcpkg and chocolatey: + +```powershell +choco install ninja -y +vcpkg install gtest zlib +``` + +## Building to use in other projects + +The recommended way to consume trx-cpp is via `add_subdirectory` or +FetchContent — this always works regardless of how libzip is resolved. +When using either approach, **zlib is the only dependency you need +pre-installed**; libzip and Eigen 3.4+ are fetched automatically if not +already present. Because Eigen is a public dependency of trx-cpp, your +code can use `Eigen3::Eigen` (and include Eigen headers) without a +separate `find_package(Eigen3)` call. + +```cmake +# CMakeLists.txt of your project +add_subdirectory(path/to/trx-cpp) # vendored copy +target_link_libraries(your_target PRIVATE trx-cpp::trx) + +# — or via FetchContent — +include(FetchContent) +FetchContent_Declare(trx-cpp + GIT_REPOSITORY https://github.com/tee-ar-ex/trx-cpp.git + GIT_TAG main) +FetchContent_MakeAvailable(trx-cpp) +target_link_libraries(your_target PRIVATE trx-cpp::trx) +``` + +**Installing trx-cpp** (`cmake --install`) requires libzip to be available +as a system-installed package with CMake config support. When libzip is +auto-fetched by CMake (the default), the install step is automatically skipped +with a status message. + +To enable installation, first install libzip through your package manager: + +```bash +# Debian/Ubuntu +sudo apt-get install libzip-dev + +# macOS +brew install libzip +``` + +```powershell +# Windows (vcpkg) +vcpkg install libzip +``` + +Then configure and install trx-cpp normally: + +```bash +cmake -S . -B build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DTRX_BUILD_EXAMPLES=OFF \ + -DTRX_ENABLE_INSTALL=ON \ + -DCMAKE_INSTALL_PREFIX=/path/to/installation/directory +cmake --build build --config Release +cmake --install build +``` + +After installation, consume the package with: + +```cmake +find_package(trx-cpp CONFIG REQUIRED) +target_link_libraries(your_target PRIVATE trx-cpp::trx) +``` + +Key CMake options: + +- `TRX_ENABLE_INSTALL`: Install package config and targets (default ON for top-level builds) +- `TRX_BUILD_EXAMPLES`: Build example executables (default ON) +- `TRX_BUILD_TESTS`: Build tests (default OFF) +- `TRX_BUILD_DOCS`: Build docs with Doxygen/Sphinx (default OFF) +- `TRX_ENABLE_CLANG_TIDY`: Run clang-tidy during builds (default OFF) +- `TRX_USE_CONAN`: Use Conan setup in `cmake/ConanSetup.cmake` (default OFF) +- `TRX_FETCH_EIGEN`: Fetch Eigen3 with FetchContent when not found locally (default ON) + +## Building for testing + +```bash +cmake -S . -B build \ + -G Ninja \ + -DTRX_BUILD_TESTS=ON \ + -DTRX_ENABLE_NIFTI=ON \ + -DTRX_BUILD_EXAMPLES=OFF + +cmake --build build +ctest --test-dir build --output-on-failure +``` + +Tests require GTest to be discoverable by CMake (e.g., via a system package or +`GTest_DIR`). If GTest is not found, tests will be skipped. +zlib must be discoverable by CMake (`ZLIB::ZLIB`), including for NIfTI I/O. + +## Building documentation: + +Building the docs requires both Doxygen and `sphinx-build` on your PATH. + +```bash +cmake -S . -B build \ + -G Ninja \ + -DTRX_BUILD_DOCS=ON + +cmake --build build --target docs +``` diff --git a/docs/building.rst b/docs/building.rst deleted file mode 100644 index 49c27fd..0000000 --- a/docs/building.rst +++ /dev/null @@ -1,152 +0,0 @@ -Building -======== - -Dependencies ------------- - -Required: - -- C++17 compiler -- zlib (``zlib1g-dev`` / ``zlib-devel`` / Homebrew ``zlib``) - -libzip and Eigen 3.4+ are fetched automatically by CMake when not found -locally — no manual installation required. - - -Installing dependencies ------------------------- - -The examples below include GoogleTest, which is only required when building -the tests. Ninja is optional but recommended. - -On Debian-based systems: - -.. code-block:: bash - - sudo apt-get install \ - zlib1g-dev \ - ninja-build \ - libgtest-dev - -On Mac OS, you can install the dependencies with brew: - -.. code-block:: bash - - brew install googletest ninja zlib - - -On Windows, you can install the dependencies through vcpkg and chocolatey: - -.. code-block:: powershell - - choco install ninja -y - vcpkg install gtest zlib - - -Building to use in other projects ---------------------------------- - -The recommended way to consume trx-cpp is via ``add_subdirectory`` or -FetchContent — this always works regardless of how libzip is resolved. -When using either approach, **zlib is the only dependency you need -pre-installed**; libzip and Eigen 3.4+ are fetched automatically if not -already present. Because Eigen is a public dependency of trx-cpp, your -code can use ``Eigen3::Eigen`` (and include Eigen headers) without a -separate ``find_package(Eigen3)`` call. - -.. code-block:: cmake - - # CMakeLists.txt of your project - add_subdirectory(path/to/trx-cpp) # vendored copy - target_link_libraries(your_target PRIVATE trx-cpp::trx) - - # — or via FetchContent — - include(FetchContent) - FetchContent_Declare(trx-cpp - GIT_REPOSITORY https://github.com/tee-ar-ex/trx-cpp.git - GIT_TAG main) - FetchContent_MakeAvailable(trx-cpp) - target_link_libraries(your_target PRIVATE trx-cpp::trx) - -**Installing trx-cpp** (``cmake --install``) requires libzip to be available -as a system-installed package with CMake config support. When libzip is -auto-fetched by CMake (the default), the install step is automatically skipped -with a status message. - -To enable installation, first install libzip through your package manager: - -.. code-block:: bash - - # Debian/Ubuntu - sudo apt-get install libzip-dev - - # macOS - brew install libzip - -.. code-block:: powershell - - # Windows (vcpkg) - vcpkg install libzip - -Then configure and install trx-cpp normally: - -.. code-block:: bash - - cmake -S . -B build \ - -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DTRX_BUILD_EXAMPLES=OFF \ - -DTRX_ENABLE_INSTALL=ON \ - -DCMAKE_INSTALL_PREFIX=/path/to/installation/directory - cmake --build build --config Release - cmake --install build - -After installation, consume the package with: - -.. code-block:: cmake - - find_package(trx-cpp CONFIG REQUIRED) - target_link_libraries(your_target PRIVATE trx-cpp::trx) - -Key CMake options: - -- ``TRX_ENABLE_INSTALL``: Install package config and targets (default ON for top-level builds) -- ``TRX_BUILD_EXAMPLES``: Build example executables (default ON) -- ``TRX_BUILD_TESTS``: Build tests (default OFF) -- ``TRX_BUILD_DOCS``: Build docs with Doxygen/Sphinx (default OFF) -- ``TRX_ENABLE_CLANG_TIDY``: Run clang-tidy during builds (default OFF) -- ``TRX_USE_CONAN``: Use Conan setup in ``cmake/ConanSetup.cmake`` (default OFF) -- ``TRX_FETCH_EIGEN``: Fetch Eigen3 with FetchContent when not found locally (default ON) - - -Building for testing --------------------- - -.. code-block:: bash - - cmake -S . -B build \ - -G Ninja \ - -DTRX_BUILD_TESTS=ON \ - -DTRX_ENABLE_NIFTI=ON \ - -DTRX_BUILD_EXAMPLES=OFF - - cmake --build build - ctest --test-dir build --output-on-failure - -Tests require GTest to be discoverable by CMake (e.g., via a system package or -``GTest_DIR``). If GTest is not found, tests will be skipped. -zlib must be discoverable by CMake (``ZLIB::ZLIB``), including for NIfTI I/O. - - -Building documentation: ------------------------ - -Building the docs requires both Doxygen and ``sphinx-build`` on your PATH. - -.. code-block:: bash - - cmake -S . -B build \ - -G Ninja \ - -DTRX_BUILD_DOCS=ON - - cmake --build build --target docs diff --git a/docs/concepts.md b/docs/concepts.md new file mode 100644 index 0000000..1556e87 --- /dev/null +++ b/docs/concepts.md @@ -0,0 +1,107 @@ +# Core Concepts + +This page explains how TRX-cpp represents tractography data on disk and in +memory. + +## The TRX format + +A TRX file is a ZIP archive (or on-disk directory) whose layout directly +encodes its data model: + +- `header.json` — spatial metadata +- `positions.` — all streamline vertices in a single flat array +- `offsets.` — prefix-sum index from streamlines into positions +- `dpv/.` — per-vertex metadata arrays +- `dps/.` — per-streamline metadata arrays +- `groups/.uint32` — named index sets of streamlines +- `dpg//.` — per-group metadata arrays + +Coordinates are stored in **RAS+ world space** (millimeters), matching the +convention used by MRtrix3 `.tck` and NIfTI qform outputs. + +## Positions array + +All streamline vertices are stored in a single flat matrix of shape +`(NB_VERTICES, 3)`. Keeping all vertices contiguous enables efficient +memory mapping and avoids per-streamline allocations. + +In trx-cpp, `positions` is backed by a `mio::shared_mmap_sink` and +exposed as an `Eigen::Matrix` view, giving zero-copy read access after +the file is mapped. + +## Offsets and the sentinel + +`offsets` is a prefix-sum index of length `NB_STREAMLINES + 1`. Element +*i* is the offset in `positions` of the first vertex of streamline *i*. +The final element is a **sentinel** equal to `NB_VERTICES`, which makes +length computation trivial without special-casing the last streamline: + +```cpp +size_t length_i = offsets[i + 1] - offsets[i]; +``` + +This design avoids per-streamline allocations and makes slicing the global +positions array fast and uniform. + +## Data per vertex (DPV) + +A DPV array stores one value per vertex in `positions`. It has shape +`(NB_VERTICES, 1)` for scalar fields or `(NB_VERTICES, N)` for +vector-valued fields. Typical uses: + +- FA values along the tract +- Per-point RGB colors +- Confidence or weight measures per vertex + +DPV arrays live under `dpv/` and are memory-mapped in the same way as +`positions`. + +## Data per streamline (DPS) + +A DPS array stores one value per streamline. It has shape +`(NB_STREAMLINES, 1)` or `(NB_STREAMLINES, N)`. Typical uses: + +- Mean FA or average curvature per tract +- Per-streamline cluster labels +- Tractography algorithm weights + +DPS arrays live under `dps/` and are loaded as typed matrix fields. If the +on-disk dtype differs from the requested typed reader dtype, values are +converted during load. + +## Groups + +A group is a named list of streamline indices stored as a `uint32` array +under `groups/`. Groups enable sparse, overlapping labeling: a streamline +can belong to multiple groups, and groups can have different sizes. Typical +uses: + +- Bundle labels (`CST_L`, `CC`, `SLF_R`, ...) +- Cluster assignments from QuickBundles or similar algorithms +- Connectivity subsets (streamlines connecting two ROIs) + +## Data per group (DPG) + +DPG attaches metadata to a group. Each group folder `dpg//` can +contain any number of scalar or vector arrays. Typical uses: + +- Mean FA across the bundle +- Per-bundle display color +- Volume or surface-area estimates + +In `TrxFile
`, groups are discovered at load time but loaded lazily on +first `get_group_members(name)` access, then cached in memory. DPG fields are +loaded as typed matrices (with dtype conversion when needed). + +## Header + +`header.json` stores: + +- `VOXEL_TO_RASMM` — 4×4 affine mapping voxel indices to RAS+ world + coordinates (mm) +- `DIMENSIONS` — reference image grid dimensions as three `uint16` values +- `NB_STREAMLINES` — number of streamlines (`uint32`) +- `NB_VERTICES` — total number of vertices across all streamlines (`uint64`) + +The header is primarily for human readability and downstream compatibility. +The authoritative sizes come from the array dimensions themselves. diff --git a/docs/concepts.rst b/docs/concepts.rst deleted file mode 100644 index ce6edf9..0000000 --- a/docs/concepts.rst +++ /dev/null @@ -1,116 +0,0 @@ -Core Concepts -============= - -This page explains how TRX-cpp represents tractography data on disk and in -memory. - -The TRX format --------------- - -A TRX file is a ZIP archive (or on-disk directory) whose layout directly -encodes its data model: - -- ``header.json`` — spatial metadata -- ``positions.`` — all streamline vertices in a single flat array -- ``offsets.`` — prefix-sum index from streamlines into positions -- ``dpv/.`` — per-vertex metadata arrays -- ``dps/.`` — per-streamline metadata arrays -- ``groups/.uint32`` — named index sets of streamlines -- ``dpg//.`` — per-group metadata arrays - -Coordinates are stored in **RAS+ world space** (millimeters), matching the -convention used by MRtrix3 ``.tck`` and NIfTI qform outputs. - -Positions array ---------------- - -All streamline vertices are stored in a single flat matrix of shape -``(NB_VERTICES, 3)``. Keeping all vertices contiguous enables efficient -memory mapping and avoids per-streamline allocations. - -In trx-cpp, ``positions`` is backed by a ``mio::shared_mmap_sink`` and -exposed as an ``Eigen::Matrix`` view, giving zero-copy read access after -the file is mapped. - -Offsets and the sentinel ------------------------- - -``offsets`` is a prefix-sum index of length ``NB_STREAMLINES + 1``. Element -*i* is the offset in ``positions`` of the first vertex of streamline *i*. -The final element is a **sentinel** equal to ``NB_VERTICES``, which makes -length computation trivial without special-casing the last streamline: - -.. code-block:: cpp - - size_t length_i = offsets[i + 1] - offsets[i]; - -This design avoids per-streamline allocations and makes slicing the global -positions array fast and uniform. - -Data per vertex (DPV) ---------------------- - -A DPV array stores one value per vertex in ``positions``. It has shape -``(NB_VERTICES, 1)`` for scalar fields or ``(NB_VERTICES, N)`` for -vector-valued fields. Typical uses: - -- FA values along the tract -- Per-point RGB colors -- Confidence or weight measures per vertex - -DPV arrays live under ``dpv/`` and are memory-mapped in the same way as -``positions``. - -Data per streamline (DPS) -------------------------- - -A DPS array stores one value per streamline. It has shape -``(NB_STREAMLINES, 1)`` or ``(NB_STREAMLINES, N)``. Typical uses: - -- Mean FA or average curvature per tract -- Per-streamline cluster labels -- Tractography algorithm weights - -DPS arrays live under ``dps/`` and are loaded as typed matrix fields. If the -on-disk dtype differs from the requested typed reader dtype, values are -converted during load. - -Groups ------- - -A group is a named list of streamline indices stored as a ``uint32`` array -under ``groups/``. Groups enable sparse, overlapping labeling: a streamline -can belong to multiple groups, and groups can have different sizes. Typical -uses: - -- Bundle labels (``CST_L``, ``CC``, ``SLF_R``, ...) -- Cluster assignments from QuickBundles or similar algorithms -- Connectivity subsets (streamlines connecting two ROIs) - -Data per group (DPG) --------------------- - -DPG attaches metadata to a group. Each group folder ``dpg//`` can -contain any number of scalar or vector arrays. Typical uses: - -- Mean FA across the bundle -- Per-bundle display color -- Volume or surface-area estimates - -In ``TrxFile
``, groups are discovered at load time but loaded lazily on -first ``get_group_members(name)`` access, then cached in memory. DPG fields are -loaded as typed matrices (with dtype conversion when needed). - -Header ------- - -``header.json`` stores: - -- ``VOXEL_TO_RASMM`` — 4×4 affine mapping voxel indices to RAS+ world - coordinates (mm) -- ``DIMENSIONS`` — reference image grid dimensions as three ``uint16`` values -- ``NB_STREAMLINES`` — number of streamlines (``uint32``) -- ``NB_VERTICES`` — total number of vertices across all streamlines (``uint64``) - -The header is primarily for human readability and downstream compatibility. -The authoritative sizes come from the array dimensions themselves. diff --git a/docs/conf.py b/docs/conf.py index 6d8a612..bc9d24d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,6 +4,7 @@ author = "trx-cpp contributors" extensions = [ + "myst_parser", "breathe", "exhale", "sphinx.ext.autosectionlabel", @@ -13,12 +14,28 @@ templates_path = ["_templates"] exclude_patterns = ["_build"] -html_theme = "sphinx_rtd_theme" +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + +html_theme = "pydata_sphinx_theme" html_theme_options = { - "collapse_navigation": False, - "navigation_depth": 3, + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/tee-ar-ex/trx-cpp", + "icon": "fa-brands fa-github", + }, + ], + "show_toc_level": 2, } +myst_enable_extensions = [ + "colon_fence", + "deflist", +] + breathe_projects = { "trx-cpp": os.path.join(os.path.dirname(__file__), "_build", "doxygen", "xml"), } diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..5e157f4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,71 @@ +# TRX-cpp Documentation + +```{image} https://readthedocs.org/projects/trx-cpp/badge/?version=latest +:alt: Documentation Status +:target: https://trx-cpp.readthedocs.io/en/latest/ +``` + +```{image} https://codecov.io/gh/tee-ar-ex/trx-cpp/branch/main/graph/badge.svg +:alt: codecov +:target: https://codecov.io/gh/tee-ar-ex/trx-cpp +``` + +A C++17 library for reading, writing, and memory-mapping the TRX tractography +format. + +```{toctree} +:caption: Getting Started +:maxdepth: 2 + +introduction +quick_start +building +``` + +```{toctree} +:caption: User Guide +:maxdepth: 2 + +concepts +api_layers +reading +writing +streaming +spatial_queries +nifti +``` + +```{toctree} +:caption: Integration Guide +:maxdepth: 2 + +integration +``` + +```{toctree} +:caption: Performance +:maxdepth: 2 + +benchmarks +``` + +```{toctree} +:caption: TRX Format +:maxdepth: 2 + +spec +``` + +```{toctree} +:caption: Contributing +:maxdepth: 2 + +linting +``` + +```{toctree} +:caption: API Reference +:maxdepth: 2 + +api/library_root +``` diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index c8bfb7b..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,63 +0,0 @@ -TRX-cpp Documentation -===================== - -.. image:: https://readthedocs.org/projects/trx-cpp/badge/?version=latest - :target: https://trx-cpp.readthedocs.io/en/latest/ - :alt: Documentation Status - -.. image:: https://codecov.io/gh/tee-ar-ex/trx-cpp/branch/main/graph/badge.svg - :target: https://codecov.io/gh/tee-ar-ex/trx-cpp - :alt: codecov - -A C++17 library for reading, writing, and memory-mapping the TRX tractography -format. - -.. toctree:: - :maxdepth: 2 - :caption: Getting Started - - introduction - quick_start - building - -.. toctree:: - :maxdepth: 2 - :caption: User Guide - - concepts - api_layers - reading - writing - streaming - spatial_queries - nifti - -.. toctree:: - :maxdepth: 2 - :caption: Integration Guide - - integration - -.. toctree:: - :maxdepth: 2 - :caption: Performance - - benchmarks - -.. toctree:: - :maxdepth: 2 - :caption: TRX Format - - spec - -.. toctree:: - :maxdepth: 2 - :caption: Contributing - - linting - -.. toctree:: - :maxdepth: 2 - :caption: API Reference - - api/library_root diff --git a/docs/integration.md b/docs/integration.md new file mode 100644 index 0000000..1c4ce78 --- /dev/null +++ b/docs/integration.md @@ -0,0 +1,309 @@ +# Integration Guide + +This page provides worked examples for integrating trx-cpp into common +tractography frameworks. Each example shows how to map the framework's +internal streamline representation to TRX and back. + +All examples assume that coordinates are already in RAS+ world space +(millimeters). If your framework uses a different coordinate convention, +apply the appropriate affine transform before writing to TRX. A common case +is LPS+ (used by ITK-based tools such as MITK), where you negate the x and y +components to convert to RAS+. + +## MRtrix3 + +MRtrix3 tracks are stored as `GeneratedTrack` objects +(`std::vector`) produced by the tracking engine. +Coordinates are in RAS+ world space and map directly to TRX `positions`. + +**Bulk conversion** — when all streamlines are available in memory: + +```cpp +#include +#include "dwi/tractography/tracking/generated_track.h" + +using MR::DWI::Tractography::Tracking::GeneratedTrack; + +void write_trx_from_mrtrix(const std::vector& tracks, + const std::string& out_path) { + std::vector accepted; + size_t total_vertices = 0; + for (const auto& tck : tracks) { + if (tck.get_status() != GeneratedTrack::status_t::ACCEPTED) continue; + accepted.push_back(&tck); + total_vertices += tck.size(); + } + + trx::TrxFile trx(total_vertices, accepted.size()); + + auto& positions = trx.streamlines->_data; + auto& offsets = trx.streamlines->_offsets; + auto& lengths = trx.streamlines->_lengths; + + size_t cursor = 0; + offsets(0) = 0; + for (size_t i = 0; i < accepted.size(); ++i) { + const auto& tck = *accepted[i]; + lengths(i) = static_cast(tck.size()); + offsets(i + 1) = offsets(i) + tck.size(); + for (const auto& pt : tck) { + positions(cursor, 0) = pt.x(); + positions(cursor, 1) = pt.y(); + positions(cursor, 2) = pt.z(); + ++cursor; + } + } + + trx.save(out_path, ZIP_CM_STORE); + trx.close(); +} +``` + +**Streaming** — appending as each streamline is accepted, without buffering: + +```cpp +#include +#include "dwi/tractography/tracking/generated_track.h" + +using MR::DWI::Tractography::Tracking::GeneratedTrack; + +trx::TrxStream trx_stream; + +void on_streamline(const GeneratedTrack& tck) { + std::vector xyz; + xyz.reserve(tck.size() * 3); + for (const auto& pt : tck) { + xyz.push_back(pt[0]); + xyz.push_back(pt[1]); + xyz.push_back(pt[2]); + } + trx_stream.push_streamline(xyz); +} + +// Call once after all streamlines are generated: +trx_stream.finalize("tracks.trx", ZIP_CM_STORE); +``` + +## DSI Studio + +DSI Studio stores tractography in `tract_model.cpp` as +`std::vector>` with interleaved XYZ values. Cluster +assignments, per-streamline scalars, and along-tract scalars map cleanly to +TRX groups, DPS, and DPV respectively. + +```cpp +std::vector> streamlines = /* DSI Studio tract_data */; +std::vector cluster_ids = /* one per streamline */; + +size_t total_vertices = 0; +for (const auto& sl : streamlines) total_vertices += sl.size() / 3; + +trx::TrxFile trx(total_vertices, streamlines.size()); +auto& positions = trx.streamlines->_data; +auto& offsets = trx.streamlines->_offsets; +auto& lengths = trx.streamlines->_lengths; + +size_t cursor = 0; +offsets(0) = 0; +for (size_t i = 0; i < streamlines.size(); ++i) { + const auto& sl = streamlines[i]; + const size_t pts = sl.size() / 3; + lengths(i) = static_cast(pts); + offsets(i + 1) = offsets(i) + pts; + for (size_t p = 0; p < pts; ++p, ++cursor) { + positions(cursor, 0) = sl[p * 3 + 0]; + positions(cursor, 1) = sl[p * 3 + 1]; + positions(cursor, 2) = sl[p * 3 + 2]; + } +} + +std::map> clusters; +for (size_t i = 0; i < cluster_ids.size(); ++i) { + clusters[cluster_ids[i]].push_back(static_cast(i)); +} +for (const auto& [label, indices] : clusters) { + trx.add_group_from_indices("cluster_" + std::to_string(label), indices); +} + +trx.save("out.trx", ZIP_CM_STORE); +trx.close(); +``` + +## nibrary / dmriTrekker + +nibrary uses `Streamline = std::vector` and +`Tractogram = std::vector`. Coordinates are in the same world +space as MRtrix3 `.tck` (RAS+) and map directly to TRX `positions`. + +**Write nibrary streamlines to TRX:** + +```cpp +using NIBR::Streamline; +using NIBR::Tractogram; + +Tractogram nibr = /* nibrary tractogram */; + +size_t total_vertices = 0; +for (const auto& sl : nibr) total_vertices += sl.size(); + +trx::TrxFile trx_out(total_vertices, nibr.size()); +auto& positions = trx_out.streamlines->_data; +auto& offsets = trx_out.streamlines->_offsets; +auto& lengths = trx_out.streamlines->_lengths; + +size_t cursor = 0; +offsets(0) = 0; +for (size_t i = 0; i < nibr.size(); ++i) { + const auto& sl = nibr[i]; + lengths(i) = static_cast(sl.size()); + offsets(i + 1) = offsets(i) + sl.size(); + for (size_t p = 0; p < sl.size(); ++p, ++cursor) { + positions(cursor, 0) = sl[p][0]; + positions(cursor, 1) = sl[p][1]; + positions(cursor, 2) = sl[p][2]; + } +} + +trx_out.save("tracks.trx", ZIP_CM_STORE); +trx_out.close(); +``` + +**Read TRX into nibrary-style streamlines:** + +```cpp +auto trx_in = trx::load_any("tracks.trx"); +const auto pos = trx_in.positions.as_matrix(); +const auto offs = trx_in.offsets.as_matrix(); + +Tractogram out; +out.reserve(trx_in.num_streamlines()); +for (size_t i = 0; i < trx_in.num_streamlines(); ++i) { + const size_t start = static_cast(offs(i, 0)); + const size_t end = static_cast(offs(i + 1, 0)); + Streamline sl; + sl.reserve(end - start); + for (size_t j = start; j < end; ++j) { + sl.push_back({pos(j, 0), pos(j, 1), pos(j, 2)}); + } + out.push_back(std::move(sl)); +} + +trx_in.close(); +``` + +## MITK Diffusion + +MITK Diffusion stores streamlines as `BundleType` +(`std::vector`), where `FiberType` is +`std::deque>`. + +**Coordinate system note:** MITK/ITK uses LPS+ physical coordinates. TRX +expects RAS+. Negate the x and y components when writing to TRX, and negate +them again when reading back. + +```cpp +#include +#include +#include +#include + +using FiberType = std::deque>; +using BundleType = std::vector; + +void mitk_bundle_to_trx(const BundleType& bundle, const std::string& out_path) { + size_t total_vertices = 0; + for (const auto& fiber : bundle) total_vertices += fiber.size(); + + trx::TrxFile trx(total_vertices, bundle.size()); + auto& positions = trx.streamlines->_data; + auto& offsets = trx.streamlines->_offsets; + auto& lengths = trx.streamlines->_lengths; + + size_t cursor = 0; + offsets(0) = 0; + for (size_t i = 0; i < bundle.size(); ++i) { + const auto& fiber = bundle[i]; + lengths(i) = static_cast(fiber.size()); + offsets(i + 1) = offsets(i) + fiber.size(); + for (const auto& pt : fiber) { + positions(cursor, 0) = -pt[0]; // LPS -> RAS: negate x + positions(cursor, 1) = -pt[1]; // LPS -> RAS: negate y + positions(cursor, 2) = pt[2]; + ++cursor; + } + } + + trx.save(out_path, ZIP_CM_STORE); + trx.close(); +} + +BundleType trx_to_mitk_bundle(const std::string& trx_path) { + auto trx = trx::load_any(trx_path); + const auto pos = trx.positions.as_matrix(); + const auto offs = trx.offsets.as_matrix(); + + BundleType bundle; + bundle.reserve(trx.num_streamlines()); + for (size_t i = 0; i < trx.num_streamlines(); ++i) { + const size_t start = static_cast(offs(i, 0)); + const size_t end = static_cast(offs(i + 1, 0)); + FiberType fiber(end - start); + for (size_t j = start; j < end; ++j) { + fiber[j - start][0] = -pos(j, 0); // RAS -> LPS + fiber[j - start][1] = -pos(j, 1); + fiber[j - start][2] = pos(j, 2); + } + bundle.push_back(std::move(fiber)); + } + + trx.close(); + return bundle; +} +``` + +## SlicerDMRI + +SlicerDMRI represents tractography as `vtkPolyData` inside a +`vtkMRMLFiberBundleNode`. TRX structures map to VTK data arrays as follows: + +- TRX `positions` + `offsets` → polydata points and polyline cells. + Each streamline becomes one line cell; point coordinates are in RAS+. +- TRX DPV → `PointData` arrays named `TRX_DPV_`. +- TRX DPS → `CellData` arrays named `TRX_DPS_`. +- TRX groups → a per-streamline `TRX_GroupId` label array in `CellData`, + with a `TRX_GroupNames` name table in `FieldData`. + +On save, the storage node exports only arrays with the `TRX_DPV_` or +`TRX_DPS_` prefix back to TRX, ensuring clean round-trips without +extraneous fields. + +**Visualization in the Slicer GUI:** + +- DPV arrays appear in the fiber bundle display controls for per-point + coloring (e.g., FA along the fiber). +- DPS arrays support per-streamline coloring or thresholding. +- Groups can be visualized by coloring on `TRX_GroupId` and using + thresholding or selection filters to isolate specific group IDs. + +## ITK-SNAP + +ITK-SNAP uses LPS+ physical coordinates. TRX positions are in RAS+, so +negate the x and y components in both directions when converting. + +Streamlines can be added to slice views by implementing a renderer delegate +in the slice rendering pipeline: + +- `GUI/Qt/Components/SliceViewPanel` installs renderer delegates. +- `GUI/Renderer/GenericSliceRenderer` and `SliceRendererDelegate` define + the overlay API (lines, polylines). +- `CrosshairsRenderer` and `PolygonDrawingRenderer` show how to draw + line-based primitives. + +A streamline renderer delegate would: + +1. Filter streamlines intersecting the current slice plane (using cached + AABBs from {func}`trx::TrxFile::build_streamline_aabbs` for speed). +2. Project 3D RAS+ points to slice coordinates via + `GenericSliceModel::MapImageToSlice` (after negating x/y to convert to + LPS+). +3. Draw each trajectory with `DrawPolyLine` in the render context. diff --git a/docs/integration.rst b/docs/integration.rst deleted file mode 100644 index e462c95..0000000 --- a/docs/integration.rst +++ /dev/null @@ -1,316 +0,0 @@ -Integration Guide -================= - -This page provides worked examples for integrating trx-cpp into common -tractography frameworks. Each example shows how to map the framework's -internal streamline representation to TRX and back. - -All examples assume that coordinates are already in RAS+ world space -(millimeters). If your framework uses a different coordinate convention, -apply the appropriate affine transform before writing to TRX. A common case -is LPS+ (used by ITK-based tools such as MITK), where you negate the x and y -components to convert to RAS+. - -MRtrix3 -------- - -MRtrix3 tracks are stored as ``GeneratedTrack`` objects -(``std::vector``) produced by the tracking engine. -Coordinates are in RAS+ world space and map directly to TRX ``positions``. - -**Bulk conversion** — when all streamlines are available in memory: - -.. code-block:: cpp - - #include - #include "dwi/tractography/tracking/generated_track.h" - - using MR::DWI::Tractography::Tracking::GeneratedTrack; - - void write_trx_from_mrtrix(const std::vector& tracks, - const std::string& out_path) { - std::vector accepted; - size_t total_vertices = 0; - for (const auto& tck : tracks) { - if (tck.get_status() != GeneratedTrack::status_t::ACCEPTED) continue; - accepted.push_back(&tck); - total_vertices += tck.size(); - } - - trx::TrxFile trx(total_vertices, accepted.size()); - - auto& positions = trx.streamlines->_data; - auto& offsets = trx.streamlines->_offsets; - auto& lengths = trx.streamlines->_lengths; - - size_t cursor = 0; - offsets(0) = 0; - for (size_t i = 0; i < accepted.size(); ++i) { - const auto& tck = *accepted[i]; - lengths(i) = static_cast(tck.size()); - offsets(i + 1) = offsets(i) + tck.size(); - for (const auto& pt : tck) { - positions(cursor, 0) = pt.x(); - positions(cursor, 1) = pt.y(); - positions(cursor, 2) = pt.z(); - ++cursor; - } - } - - trx.save(out_path, ZIP_CM_STORE); - trx.close(); - } - -**Streaming** — appending as each streamline is accepted, without buffering: - -.. code-block:: cpp - - #include - #include "dwi/tractography/tracking/generated_track.h" - - using MR::DWI::Tractography::Tracking::GeneratedTrack; - - trx::TrxStream trx_stream; - - void on_streamline(const GeneratedTrack& tck) { - std::vector xyz; - xyz.reserve(tck.size() * 3); - for (const auto& pt : tck) { - xyz.push_back(pt[0]); - xyz.push_back(pt[1]); - xyz.push_back(pt[2]); - } - trx_stream.push_streamline(xyz); - } - - // Call once after all streamlines are generated: - trx_stream.finalize("tracks.trx", ZIP_CM_STORE); - -DSI Studio ----------- - -DSI Studio stores tractography in ``tract_model.cpp`` as -``std::vector>`` with interleaved XYZ values. Cluster -assignments, per-streamline scalars, and along-tract scalars map cleanly to -TRX groups, DPS, and DPV respectively. - -.. code-block:: cpp - - std::vector> streamlines = /* DSI Studio tract_data */; - std::vector cluster_ids = /* one per streamline */; - - size_t total_vertices = 0; - for (const auto& sl : streamlines) total_vertices += sl.size() / 3; - - trx::TrxFile trx(total_vertices, streamlines.size()); - auto& positions = trx.streamlines->_data; - auto& offsets = trx.streamlines->_offsets; - auto& lengths = trx.streamlines->_lengths; - - size_t cursor = 0; - offsets(0) = 0; - for (size_t i = 0; i < streamlines.size(); ++i) { - const auto& sl = streamlines[i]; - const size_t pts = sl.size() / 3; - lengths(i) = static_cast(pts); - offsets(i + 1) = offsets(i) + pts; - for (size_t p = 0; p < pts; ++p, ++cursor) { - positions(cursor, 0) = sl[p * 3 + 0]; - positions(cursor, 1) = sl[p * 3 + 1]; - positions(cursor, 2) = sl[p * 3 + 2]; - } - } - - std::map> clusters; - for (size_t i = 0; i < cluster_ids.size(); ++i) { - clusters[cluster_ids[i]].push_back(static_cast(i)); - } - for (const auto& [label, indices] : clusters) { - trx.add_group_from_indices("cluster_" + std::to_string(label), indices); - } - - trx.save("out.trx", ZIP_CM_STORE); - trx.close(); - -nibrary / dmriTrekker ---------------------- - -nibrary uses ``Streamline = std::vector`` and -``Tractogram = std::vector``. Coordinates are in the same world -space as MRtrix3 ``.tck`` (RAS+) and map directly to TRX ``positions``. - -**Write nibrary streamlines to TRX:** - -.. code-block:: cpp - - using NIBR::Streamline; - using NIBR::Tractogram; - - Tractogram nibr = /* nibrary tractogram */; - - size_t total_vertices = 0; - for (const auto& sl : nibr) total_vertices += sl.size(); - - trx::TrxFile trx_out(total_vertices, nibr.size()); - auto& positions = trx_out.streamlines->_data; - auto& offsets = trx_out.streamlines->_offsets; - auto& lengths = trx_out.streamlines->_lengths; - - size_t cursor = 0; - offsets(0) = 0; - for (size_t i = 0; i < nibr.size(); ++i) { - const auto& sl = nibr[i]; - lengths(i) = static_cast(sl.size()); - offsets(i + 1) = offsets(i) + sl.size(); - for (size_t p = 0; p < sl.size(); ++p, ++cursor) { - positions(cursor, 0) = sl[p][0]; - positions(cursor, 1) = sl[p][1]; - positions(cursor, 2) = sl[p][2]; - } - } - - trx_out.save("tracks.trx", ZIP_CM_STORE); - trx_out.close(); - -**Read TRX into nibrary-style streamlines:** - -.. code-block:: cpp - - auto trx_in = trx::load_any("tracks.trx"); - const auto pos = trx_in.positions.as_matrix(); - const auto offs = trx_in.offsets.as_matrix(); - - Tractogram out; - out.reserve(trx_in.num_streamlines()); - for (size_t i = 0; i < trx_in.num_streamlines(); ++i) { - const size_t start = static_cast(offs(i, 0)); - const size_t end = static_cast(offs(i + 1, 0)); - Streamline sl; - sl.reserve(end - start); - for (size_t j = start; j < end; ++j) { - sl.push_back({pos(j, 0), pos(j, 1), pos(j, 2)}); - } - out.push_back(std::move(sl)); - } - - trx_in.close(); - -MITK Diffusion --------------- - -MITK Diffusion stores streamlines as ``BundleType`` -(``std::vector``), where ``FiberType`` is -``std::deque>``. - -**Coordinate system note:** MITK/ITK uses LPS+ physical coordinates. TRX -expects RAS+. Negate the x and y components when writing to TRX, and negate -them again when reading back. - -.. code-block:: cpp - - #include - #include - #include - #include - - using FiberType = std::deque>; - using BundleType = std::vector; - - void mitk_bundle_to_trx(const BundleType& bundle, const std::string& out_path) { - size_t total_vertices = 0; - for (const auto& fiber : bundle) total_vertices += fiber.size(); - - trx::TrxFile trx(total_vertices, bundle.size()); - auto& positions = trx.streamlines->_data; - auto& offsets = trx.streamlines->_offsets; - auto& lengths = trx.streamlines->_lengths; - - size_t cursor = 0; - offsets(0) = 0; - for (size_t i = 0; i < bundle.size(); ++i) { - const auto& fiber = bundle[i]; - lengths(i) = static_cast(fiber.size()); - offsets(i + 1) = offsets(i) + fiber.size(); - for (const auto& pt : fiber) { - positions(cursor, 0) = -pt[0]; // LPS -> RAS: negate x - positions(cursor, 1) = -pt[1]; // LPS -> RAS: negate y - positions(cursor, 2) = pt[2]; - ++cursor; - } - } - - trx.save(out_path, ZIP_CM_STORE); - trx.close(); - } - - BundleType trx_to_mitk_bundle(const std::string& trx_path) { - auto trx = trx::load_any(trx_path); - const auto pos = trx.positions.as_matrix(); - const auto offs = trx.offsets.as_matrix(); - - BundleType bundle; - bundle.reserve(trx.num_streamlines()); - for (size_t i = 0; i < trx.num_streamlines(); ++i) { - const size_t start = static_cast(offs(i, 0)); - const size_t end = static_cast(offs(i + 1, 0)); - FiberType fiber(end - start); - for (size_t j = start; j < end; ++j) { - fiber[j - start][0] = -pos(j, 0); // RAS -> LPS - fiber[j - start][1] = -pos(j, 1); - fiber[j - start][2] = pos(j, 2); - } - bundle.push_back(std::move(fiber)); - } - - trx.close(); - return bundle; - } - -SlicerDMRI ----------- - -SlicerDMRI represents tractography as ``vtkPolyData`` inside a -``vtkMRMLFiberBundleNode``. TRX structures map to VTK data arrays as follows: - -- TRX ``positions`` + ``offsets`` → polydata points and polyline cells. - Each streamline becomes one line cell; point coordinates are in RAS+. -- TRX DPV → ``PointData`` arrays named ``TRX_DPV_``. -- TRX DPS → ``CellData`` arrays named ``TRX_DPS_``. -- TRX groups → a per-streamline ``TRX_GroupId`` label array in ``CellData``, - with a ``TRX_GroupNames`` name table in ``FieldData``. - -On save, the storage node exports only arrays with the ``TRX_DPV_`` or -``TRX_DPS_`` prefix back to TRX, ensuring clean round-trips without -extraneous fields. - -**Visualization in the Slicer GUI:** - -- DPV arrays appear in the fiber bundle display controls for per-point - coloring (e.g., FA along the fiber). -- DPS arrays support per-streamline coloring or thresholding. -- Groups can be visualized by coloring on ``TRX_GroupId`` and using - thresholding or selection filters to isolate specific group IDs. - -ITK-SNAP --------- - -ITK-SNAP uses LPS+ physical coordinates. TRX positions are in RAS+, so -negate the x and y components in both directions when converting. - -Streamlines can be added to slice views by implementing a renderer delegate -in the slice rendering pipeline: - -- ``GUI/Qt/Components/SliceViewPanel`` installs renderer delegates. -- ``GUI/Renderer/GenericSliceRenderer`` and ``SliceRendererDelegate`` define - the overlay API (lines, polylines). -- ``CrosshairsRenderer`` and ``PolygonDrawingRenderer`` show how to draw - line-based primitives. - -A streamline renderer delegate would: - -1. Filter streamlines intersecting the current slice plane (using cached - AABBs from :func:`trx::TrxFile::build_streamline_aabbs` for speed). -2. Project 3D RAS+ points to slice coordinates via - ``GenericSliceModel::MapImageToSlice`` (after negating x/y to convert to - LPS+). -3. Draw each trajectory with ``DrawPolyLine`` in the render context. diff --git a/docs/introduction.rst b/docs/introduction.md similarity index 53% rename from docs/introduction.rst rename to docs/introduction.md index 2ac528a..25b3567 100644 --- a/docs/introduction.rst +++ b/docs/introduction.md @@ -1,50 +1,53 @@ -Introduction -============ +# Introduction TRX-cpp is a C++17 library for reading, writing, and memory-mapping the -`TRX tractography format `_. TRX is +[TRX tractography format](https://github.com/tee-ar-ex/trx-spec). TRX is a ZIP-based container for fiber tract geometry and associated metadata, designed for large-scale diffusion MRI tractography. -Features --------- +## Features **Zero-copy memory mapping** - Streamline positions, per-vertex data (DPV), and per-streamline data (DPS) - are exposed as ``Eigen::Map`` views directly over memory-mapped files. + +: Streamline positions, per-vertex data (DPV), and per-streamline data (DPS) + are exposed as `Eigen::Map` views directly over memory-mapped files. Accessing a 10 M-streamline dataset does not require loading the full array into RAM. **Streaming writes** - :class:`trx::TrxStream` appends streamlines incrementally and finalizes to + +: {class}`trx::TrxStream` appends streamlines incrementally and finalizes to a TRX archive or directory once tracking is complete. Suitable for tractography pipelines where the total count is unknown at the start. **Spatial queries** - Build per-streamline axis-aligned bounding boxes (AABBs) and extract spatial + +: Build per-streamline axis-aligned bounding boxes (AABBs) and extract spatial subsets in sub-millisecond time per query. Designed for interactive slice-view workflows that need to filter streamlines as the user moves through a volume. **Typed and type-erased APIs** - :class:`trx::TrxFile` is templated on the positions dtype for compile-time - type safety. :class:`trx::AnyTrxFile` reads the dtype from disk and + +: {class}`trx::TrxFile` is templated on the positions dtype for compile-time + type safety. {class}`trx::AnyTrxFile` reads the dtype from disk and dispatches at runtime — useful when the file format is not known in advance. **ZIP and directory storage** - Read and write ``.trx`` zip archives and plain on-disk directories with the + +: Read and write `.trx` zip archives and plain on-disk directories with the same API. Directory storage is convenient for in-place access; zip storage is convenient for distribution and transfer. **Optional NIfTI support** - Read qform/sform affines from ``.nii``, ``.hdr``, or ``.nii.gz`` and embed + +: Read qform/sform affines from `.nii`, `.hdr`, or `.nii.gz` and embed them in the TRX header for consistent coordinate interpretation downstream. -Where to go next ----------------- +## Where to go next -- :doc:`quick_start` — install the library and write a first program -- :doc:`building` — full dependency and build options reference -- :doc:`concepts` — how TRX-cpp represents streamlines and metadata internally -- :doc:`api_layers` — choosing between ``AnyTrxFile``, ``TrxFile
``, and - ``TrxReader
`` +- {doc}`quick_start` — install the library and write a first program +- {doc}`building` — full dependency and build options reference +- {doc}`concepts` — how TRX-cpp represents streamlines and metadata internally +- {doc}`api_layers` — choosing between `AnyTrxFile`, `TrxFile
`, and + `TrxReader
` diff --git a/docs/linting.md b/docs/linting.md new file mode 100644 index 0000000..6b1f24b --- /dev/null +++ b/docs/linting.md @@ -0,0 +1,57 @@ +# Linting and Style Checks + +This repo includes `.clang-tidy` and `.clang-format` at the root, plus +MRtrix-inspired helper scripts for formatting and style checks. + +## Prerequisites + +- `clang-format` available on `PATH` + - macOS (Homebrew): `brew install llvm` (or `llvm@17`) and ensure `clang-format` is on `PATH` + - Ubuntu: `sudo apt-get install clang-format` + +- For `check_syntax` on macOS, GNU grep is required (`brew install grep`, + then it will use `ggrep`). + +## clang-format (bulk formatting) + +```bash +./clang-format-all +``` + +You can target a specific clang-format binary: + +```bash +./clang-format-all --executable /path/to/clang-format +``` + +## check_syntax (style rules) + +Run the MRtrix-style checks against the C++ sources: + +```bash +./check_syntax +``` + +Results are written to `syntax.log` when issues are found. + +## clang-tidy + +Generate a build with compile commands, then run clang-tidy (matches CI): + +```bash +cmake -S . -B build \ + -DTRX_USE_CONAN=OFF \ + -DTRX_BUILD_EXAMPLES=ON \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +run-clang-tidy -p build $(git ls-files '*.cpp' '*.h' '*.hpp' '*.tpp' ':!third_party/**') +``` + +To run clang-tidy automatically during builds: + +```bash +cmake -S . -B build -DTRX_ENABLE_CLANG_TIDY=ON +cmake --build build +``` + +If you have `run-clang-tidy` installed (LLVM extras), you can lint everything +tracked by the repo (excluding `third_party`), which matches CI. diff --git a/docs/linting.rst b/docs/linting.rst deleted file mode 100644 index da1153b..0000000 --- a/docs/linting.rst +++ /dev/null @@ -1,62 +0,0 @@ -Linting and Style Checks -======================== - -This repo includes ``.clang-tidy`` and ``.clang-format`` at the root, plus -MRtrix-inspired helper scripts for formatting and style checks. - -Prerequisites -------------- - -- ``clang-format`` available on ``PATH`` - - macOS (Homebrew): ``brew install llvm`` (or ``llvm@17``) and ensure - ``clang-format`` is on ``PATH`` - - Ubuntu: ``sudo apt-get install clang-format`` -- For ``check_syntax`` on macOS, GNU grep is required (``brew install grep``, - then it will use ``ggrep``). - -clang-format (bulk formatting) ------------------------------- - -.. code-block:: bash - - ./clang-format-all - -You can target a specific clang-format binary: - -.. code-block:: bash - - ./clang-format-all --executable /path/to/clang-format - -check_syntax (style rules) --------------------------- - -Run the MRtrix-style checks against the C++ sources: - -.. code-block:: bash - - ./check_syntax - -Results are written to ``syntax.log`` when issues are found. - -clang-tidy ----------- - -Generate a build with compile commands, then run clang-tidy (matches CI): - -.. code-block:: bash - - cmake -S . -B build \ - -DTRX_USE_CONAN=OFF \ - -DTRX_BUILD_EXAMPLES=ON \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - run-clang-tidy -p build $(git ls-files '*.cpp' '*.h' '*.hpp' '*.tpp' ':!third_party/**') - -To run clang-tidy automatically during builds: - -.. code-block:: bash - - cmake -S . -B build -DTRX_ENABLE_CLANG_TIDY=ON - cmake --build build - -If you have ``run-clang-tidy`` installed (LLVM extras), you can lint everything -tracked by the repo (excluding ``third_party``), which matches CI. diff --git a/docs/nifti.md b/docs/nifti.md new file mode 100644 index 0000000..14d542a --- /dev/null +++ b/docs/nifti.md @@ -0,0 +1,40 @@ +# NIfTI Header Support + +When trx-cpp is built with `TRX_ENABLE_NIFTI=ON`, the optional NIfTI I/O +module can read qform/sform affines from `.nii`, `.hdr`, or `.nii.gz` +files and embed them in the TRX `VOXEL_TO_RASMM` header field. + +This is primarily useful when the TRX file must interoperate with the +`.trk` (TrackVis) format, which stores coordinates in voxel space and +relies on the NIfTI header for the voxel-to-world transform. + +## Attach a NIfTI affine to a TRX file + +```cpp +#include +#include + +Eigen::Matrix4f affine = trx::read_nifti_voxel_to_rasmm("reference.nii.gz"); + +auto trx = trx::load("tracks.trx"); +trx->set_voxel_to_rasmm(affine); +trx->save("tracks_with_ref.trx"); +trx->close(); +``` + +## Notes + +- The qform is preferred when present. If only the sform is available, it is + orthogonalized to a qform-equivalent matrix, consistent with ITK's NIfTI + handling. +- The qform/sform logic is adapted from nibabel's MIT-licensed implementation + (see `third_party/nibabel/LICENSE`). +- zlib must be discoverable by CMake to build `.nii.gz` decompression + support. + +## Enable at build time + +```bash +cmake -S . -B build -DTRX_ENABLE_NIFTI=ON +cmake --build build +``` diff --git a/docs/nifti.rst b/docs/nifti.rst deleted file mode 100644 index ccb284b..0000000 --- a/docs/nifti.rst +++ /dev/null @@ -1,44 +0,0 @@ -NIfTI Header Support -==================== - -When trx-cpp is built with ``TRX_ENABLE_NIFTI=ON``, the optional NIfTI I/O -module can read qform/sform affines from ``.nii``, ``.hdr``, or ``.nii.gz`` -files and embed them in the TRX ``VOXEL_TO_RASMM`` header field. - -This is primarily useful when the TRX file must interoperate with the -``.trk`` (TrackVis) format, which stores coordinates in voxel space and -relies on the NIfTI header for the voxel-to-world transform. - -Attach a NIfTI affine to a TRX file -------------------------------------- - -.. code-block:: cpp - - #include - #include - - Eigen::Matrix4f affine = trx::read_nifti_voxel_to_rasmm("reference.nii.gz"); - - auto trx = trx::load("tracks.trx"); - trx->set_voxel_to_rasmm(affine); - trx->save("tracks_with_ref.trx"); - trx->close(); - -Notes ------ - -- The qform is preferred when present. If only the sform is available, it is - orthogonalized to a qform-equivalent matrix, consistent with ITK's NIfTI - handling. -- The qform/sform logic is adapted from nibabel's MIT-licensed implementation - (see ``third_party/nibabel/LICENSE``). -- zlib must be discoverable by CMake to build ``.nii.gz`` decompression - support. - -Enable at build time ---------------------- - -.. code-block:: bash - - cmake -S . -B build -DTRX_ENABLE_NIFTI=ON - cmake --build build diff --git a/docs/quick_start.md b/docs/quick_start.md new file mode 100644 index 0000000..34282d2 --- /dev/null +++ b/docs/quick_start.md @@ -0,0 +1,95 @@ +# Quick Start + +This page walks through getting trx-cpp installed and writing a minimal +program that loads a TRX file, prints basic statistics, and saves a copy. + +## Prerequisites + +- A C++17 compiler (GCC ≥ 7, Clang ≥ 5, MSVC 2019+) +- CMake ≥ 3.14 +- zlib development headers/libraries + +`libzip` and `Eigen 3.4+` are discovered from the local system when +available and fetched automatically by CMake otherwise. + +See {doc}`building` for platform-specific installation commands. + +## Install trx-cpp + +Build and install the library so it can be found by `find_package`: + +```bash +git clone https://github.com/tee-ar-ex/trx-cpp.git +cmake -S trx-cpp -B trx-cpp/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DTRX_BUILD_EXAMPLES=OFF \ + -DTRX_ENABLE_INSTALL=ON \ + -DCMAKE_INSTALL_PREFIX=$HOME/.local +cmake --build trx-cpp/build --config Release +cmake --install trx-cpp/build +``` + +Alternatively, add trx-cpp as a subdirectory in your project (no install step +needed): + +```cmake +add_subdirectory(path/to/trx-cpp) +target_link_libraries(my_app PRIVATE trx-cpp::trx) +``` + +## Write a first program + +Create a `CMakeLists.txt` and a `main.cpp`: + +```cmake +# CMakeLists.txt +cmake_minimum_required(VERSION 3.14) +project(hello_trx) + +find_package(trx-cpp CONFIG REQUIRED) + +add_executable(hello_trx main.cpp) +target_link_libraries(hello_trx PRIVATE trx-cpp::trx) +``` + +```cpp +// main.cpp +#include +#include + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "usage: hello_trx \n"; + return 1; + } + + auto trx = trx::load_any(argv[1]); + + std::cout << "streamlines : " << trx.num_streamlines() << "\n"; + std::cout << "vertices : " << trx.num_vertices() << "\n"; + std::cout << "dtype : " << trx.positions.dtype << "\n"; + + for (const auto& [name, arr] : trx.data_per_streamline) { + std::cout << "dps/" << name + << " (" << arr.rows() << " x " << arr.cols() << ")\n"; + } + + trx.close(); + return 0; +} +``` + +Build and run: + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +./build/hello_trx /path/to/tracks.trx +``` + +## Next steps + +- {doc}`reading` — access streamline positions and metadata +- {doc}`writing` — create and save TRX files +- {doc}`streaming` — stream streamlines without buffering the full dataset +- {doc}`api_layers` — understand the three API layers diff --git a/docs/quick_start.rst b/docs/quick_start.rst deleted file mode 100644 index 5afedf8..0000000 --- a/docs/quick_start.rst +++ /dev/null @@ -1,100 +0,0 @@ -Quick Start -=========== - -This page walks through getting trx-cpp installed and writing a minimal -program that loads a TRX file, prints basic statistics, and saves a copy. - -Prerequisites -------------- - -- A C++17 compiler (GCC ≥ 7, Clang ≥ 5, MSVC 2019+) -- CMake ≥ 3.14 -- zlib development headers/libraries - -``libzip`` and ``Eigen 3.4+`` are discovered from the local system when -available and fetched automatically by CMake otherwise. - -See :doc:`building` for platform-specific installation commands. - -Install trx-cpp ---------------- - -Build and install the library so it can be found by ``find_package``: - -.. code-block:: bash - - git clone https://github.com/tee-ar-ex/trx-cpp.git - cmake -S trx-cpp -B trx-cpp/build \ - -DCMAKE_BUILD_TYPE=Release \ - -DTRX_BUILD_EXAMPLES=OFF \ - -DTRX_ENABLE_INSTALL=ON \ - -DCMAKE_INSTALL_PREFIX=$HOME/.local - cmake --build trx-cpp/build --config Release - cmake --install trx-cpp/build - -Alternatively, add trx-cpp as a subdirectory in your project (no install step -needed): - -.. code-block:: cmake - - add_subdirectory(path/to/trx-cpp) - target_link_libraries(my_app PRIVATE trx-cpp::trx) - -Write a first program ---------------------- - -Create a ``CMakeLists.txt`` and a ``main.cpp``: - -.. code-block:: cmake - - # CMakeLists.txt - cmake_minimum_required(VERSION 3.14) - project(hello_trx) - - find_package(trx-cpp CONFIG REQUIRED) - - add_executable(hello_trx main.cpp) - target_link_libraries(hello_trx PRIVATE trx-cpp::trx) - -.. code-block:: cpp - - // main.cpp - #include - #include - - int main(int argc, char* argv[]) { - if (argc < 2) { - std::cerr << "usage: hello_trx \n"; - return 1; - } - - auto trx = trx::load_any(argv[1]); - - std::cout << "streamlines : " << trx.num_streamlines() << "\n"; - std::cout << "vertices : " << trx.num_vertices() << "\n"; - std::cout << "dtype : " << trx.positions.dtype << "\n"; - - for (const auto& [name, arr] : trx.data_per_streamline) { - std::cout << "dps/" << name - << " (" << arr.rows() << " x " << arr.cols() << ")\n"; - } - - trx.close(); - return 0; - } - -Build and run: - -.. code-block:: bash - - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release - cmake --build build - ./build/hello_trx /path/to/tracks.trx - -Next steps ----------- - -- :doc:`reading` — access streamline positions and metadata -- :doc:`writing` — create and save TRX files -- :doc:`streaming` — stream streamlines without buffering the full dataset -- :doc:`api_layers` — understand the three API layers diff --git a/docs/reading.md b/docs/reading.md new file mode 100644 index 0000000..2da06e6 --- /dev/null +++ b/docs/reading.md @@ -0,0 +1,151 @@ +# Reading TRX Files + +This page covers the common patterns for loading and inspecting TRX data. +See {doc}`api_layers` for guidance on choosing between `AnyTrxFile` and +`TrxFile
`. + +## Load and inspect + +The simplest entry point is {func}`trx::load_any`, which detects the dtype +from the file and returns an {class}`trx::AnyTrxFile`: + +```cpp +#include + +auto trx = trx::load_any("/path/to/tracks.trx"); + +std::cout << "dtype : " << trx.positions.dtype << "\n"; +std::cout << "streamlines : " << trx.num_streamlines() << "\n"; +std::cout << "vertices : " << trx.num_vertices() << "\n"; + +trx.close(); +``` + +## Access positions and offsets + +Positions and offsets are exposed as {class}`trx::TypedArray` objects. Call +`as_matrix()` to obtain an `Eigen::Map` view without copying data: + +```cpp +auto pos = trx.positions.as_matrix(); // (NB_VERTICES, 3) +auto offs = trx.offsets.as_matrix(); // (NB_STREAMLINES + 1, 1) + +for (size_t i = 0; i < trx.num_streamlines(); ++i) { + const size_t start = static_cast(offs(i, 0)); + const size_t end = static_cast(offs(i + 1, 0)); + // vertices for streamline i: pos.block(start, 0, end - start, 3) +} +``` + +## Access DPV and DPS + +Per-vertex (DPV) and per-streamline (DPS) metadata are stored in +`std::map` containers: + +```cpp +// List all DPS fields +for (const auto& [name, arr] : trx.data_per_streamline) { + std::cout << "dps/" << name + << " (" << arr.rows() << " x " << arr.cols() << ")\n"; +} + +// Access a specific DPV field +auto fa = trx.data_per_vertex.at("fa").as_matrix(); // (NB_VERTICES, 1) +``` + +## Access groups + +```cpp +// AnyTrxFile: groups are TypedArray values. +for (const auto& [name, arr] : trx.groups) { + std::cout << "group " << name << ": " + << arr.rows << " streamlines\n"; +} + +auto ids = trx.groups.at("CST_L").as_matrix(); // (N, 1) +``` + +## Typed access via TrxFile\
+ +When the dtype is known ahead of time, use {func}`trx::load` for a typed +view. Positions and DPV arrays are exposed as `Eigen::Matrix` +directly, avoiding element-wise conversion: + +```cpp +auto reader = trx::load("tracks.trx"); +auto& trx = *reader; + +// trx.streamlines->_data is Eigen::Matrix +// trx.streamlines->_offsets is Eigen::Matrix + +// Groups are discovered at load time but loaded lazily on first access. +for (const auto& kv : trx.groups) { + const auto& name = kv.first; + const auto* group = trx.get_group_members(name); // lazy load + cache + if (group == nullptr) { + continue; + } + std::cout << name << ": " << group->_matrix.rows() << " members\n"; +} + +reader->close(); +``` + +## Iterating streamlines without copying + +Because TRX positions are memory-mapped, the full positions array is never +read into RAM at once — the OS pages in only the regions you touch. You do +not need a separate "streaming reader" to process a 10 M-streamline file +without exhausting memory. + +To iterate over each streamline with zero per-streamline allocation, use +{func}`trx::TrxFile::for_each_streamline`. The callback receives the +streamline index, the start row in `_data`, and the number of vertices: + +```cpp +auto reader = trx::load("tracks.trx"); +auto& trx = *reader; + +trx.for_each_streamline([&](size_t idx, uint64_t start, uint64_t length) { + // Zero-copy block view of this streamline's vertices. + auto pts = trx.streamlines->_data.block( + static_cast(start), 0, + static_cast(length), 3); + + // pts is an Eigen expression — no heap allocation. + // Example: compute the centroid. + Eigen::Vector3f centroid = pts.colwise().mean(); +}); + +reader->close(); +``` + +For random access to a single streamline, use +{func}`trx::TrxFile::get_streamline`. This copies the vertices into a +`std::vector` and is convenient for one-off lookups, but avoid it in +tight loops over large tractograms: + +```cpp +auto pts = trx.get_streamline(42); // std::vector> +``` + +## Chunk-based iteration (AnyTrxFile) + +{class}`trx::AnyTrxFile` provides `for_each_positions_chunk`, which +iterates the positions buffer in fixed-size byte chunks. This is useful +for transcoding or checksum passes that process all vertices but do not +need the per-streamline boundary structure: + +```cpp +auto trx = trx::load_any("tracks.trx"); + +trx.for_each_positions_chunk( + 4 * 1024 * 1024, // 4 MB chunks + [](trx::TrxScalarType dtype, const void* data, + size_t point_offset, size_t point_count) { + // data points to point_count * 3 values of the given dtype, + // starting at global vertex index point_offset. + }); + +trx.close();; +``` diff --git a/docs/reading.rst b/docs/reading.rst deleted file mode 100644 index 6389011..0000000 --- a/docs/reading.rst +++ /dev/null @@ -1,159 +0,0 @@ -Reading TRX Files -================= - -This page covers the common patterns for loading and inspecting TRX data. -See :doc:`api_layers` for guidance on choosing between ``AnyTrxFile`` and -``TrxFile
``. - -Load and inspect ----------------- - -The simplest entry point is :func:`trx::load_any`, which detects the dtype -from the file and returns an :class:`trx::AnyTrxFile`: - -.. code-block:: cpp - - #include - - auto trx = trx::load_any("/path/to/tracks.trx"); - - std::cout << "dtype : " << trx.positions.dtype << "\n"; - std::cout << "streamlines : " << trx.num_streamlines() << "\n"; - std::cout << "vertices : " << trx.num_vertices() << "\n"; - - trx.close(); - -Access positions and offsets ------------------------------ - -Positions and offsets are exposed as :class:`trx::TypedArray` objects. Call -``as_matrix()`` to obtain an ``Eigen::Map`` view without copying data: - -.. code-block:: cpp - - auto pos = trx.positions.as_matrix(); // (NB_VERTICES, 3) - auto offs = trx.offsets.as_matrix(); // (NB_STREAMLINES + 1, 1) - - for (size_t i = 0; i < trx.num_streamlines(); ++i) { - const size_t start = static_cast(offs(i, 0)); - const size_t end = static_cast(offs(i + 1, 0)); - // vertices for streamline i: pos.block(start, 0, end - start, 3) - } - -Access DPV and DPS ------------------- - -Per-vertex (DPV) and per-streamline (DPS) metadata are stored in -``std::map`` containers: - -.. code-block:: cpp - - // List all DPS fields - for (const auto& [name, arr] : trx.data_per_streamline) { - std::cout << "dps/" << name - << " (" << arr.rows() << " x " << arr.cols() << ")\n"; - } - - // Access a specific DPV field - auto fa = trx.data_per_vertex.at("fa").as_matrix(); // (NB_VERTICES, 1) - -Access groups -------------- - -.. code-block:: cpp - - // AnyTrxFile: groups are TypedArray values. - for (const auto& [name, arr] : trx.groups) { - std::cout << "group " << name << ": " - << arr.rows << " streamlines\n"; - } - - auto ids = trx.groups.at("CST_L").as_matrix(); // (N, 1) - -Typed access via TrxFile
----------------------------- - -When the dtype is known ahead of time, use :func:`trx::load` for a typed -view. Positions and DPV arrays are exposed as ``Eigen::Matrix`` -directly, avoiding element-wise conversion: - -.. code-block:: cpp - - auto reader = trx::load("tracks.trx"); - auto& trx = *reader; - - // trx.streamlines->_data is Eigen::Matrix - // trx.streamlines->_offsets is Eigen::Matrix - - // Groups are discovered at load time but loaded lazily on first access. - for (const auto& kv : trx.groups) { - const auto& name = kv.first; - const auto* group = trx.get_group_members(name); // lazy load + cache - if (group == nullptr) { - continue; - } - std::cout << name << ": " << group->_matrix.rows() << " members\n"; - } - - reader->close(); - -Iterating streamlines without copying --------------------------------------- - -Because TRX positions are memory-mapped, the full positions array is never -read into RAM at once — the OS pages in only the regions you touch. You do -not need a separate "streaming reader" to process a 10 M-streamline file -without exhausting memory. - -To iterate over each streamline with zero per-streamline allocation, use -:func:`trx::TrxFile::for_each_streamline`. The callback receives the -streamline index, the start row in ``_data``, and the number of vertices: - -.. code-block:: cpp - - auto reader = trx::load("tracks.trx"); - auto& trx = *reader; - - trx.for_each_streamline([&](size_t idx, uint64_t start, uint64_t length) { - // Zero-copy block view of this streamline's vertices. - auto pts = trx.streamlines->_data.block( - static_cast(start), 0, - static_cast(length), 3); - - // pts is an Eigen expression — no heap allocation. - // Example: compute the centroid. - Eigen::Vector3f centroid = pts.colwise().mean(); - }); - - reader->close(); - -For random access to a single streamline, use -:func:`trx::TrxFile::get_streamline`. This copies the vertices into a -``std::vector`` and is convenient for one-off lookups, but avoid it in -tight loops over large tractograms: - -.. code-block:: cpp - - auto pts = trx.get_streamline(42); // std::vector> - -Chunk-based iteration (AnyTrxFile) ------------------------------------- - -:class:`trx::AnyTrxFile` provides ``for_each_positions_chunk``, which -iterates the positions buffer in fixed-size byte chunks. This is useful -for transcoding or checksum passes that process all vertices but do not -need the per-streamline boundary structure: - -.. code-block:: cpp - - auto trx = trx::load_any("tracks.trx"); - - trx.for_each_positions_chunk( - 4 * 1024 * 1024, // 4 MB chunks - [](trx::TrxScalarType dtype, const void* data, - size_t point_offset, size_t point_count) { - // data points to point_count * 3 values of the given dtype, - // starting at global vertex index point_offset. - }); - - trx.close();; diff --git a/docs/requirements.txt b/docs/requirements.txt index ef182b1..2cbf9a4 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ sphinx breathe exhale -sphinx_rtd_theme +pydata-sphinx-theme +myst-parser diff --git a/docs/spatial_queries.md b/docs/spatial_queries.md new file mode 100644 index 0000000..2911e09 --- /dev/null +++ b/docs/spatial_queries.md @@ -0,0 +1,57 @@ +# Spatial Queries + +trx-cpp can build per-streamline axis-aligned bounding boxes (AABBs) and +use them to extract spatial subsets efficiently. This is useful for +interactive slice-view updates or region-of-interest filtering. + +## Query by bounding box + +Pass minimum and maximum corners in RAS+ world coordinates (mm): + +```cpp +#include + +auto trx = trx::load("/path/to/tracks.trx"); + +std::array min_corner{-10.0f, -10.0f, -10.0f}; +std::array max_corner{ 10.0f, 10.0f, 10.0f}; + +auto subset = trx->query_aabb(min_corner, max_corner); +subset->save("subset.trx", ZIP_CM_STORE); +subset->close(); +``` + +## Precompute the AABB cache + +When issuing multiple spatial queries on the same file — for example, as a +user scrubs through slices in a viewer — precompute the AABB cache once and +pass it to each query: + +```cpp +auto aabbs = trx->build_streamline_aabbs(); + +// Query 1 +auto s1 = trx->query_aabb(min1, max1, &aabbs); + +// Query 2 — reuses the same cached bounding boxes +auto s2 = trx->query_aabb(min2, max2, &aabbs); + +// Optionally build the AABB cache for the result as well +auto s3 = trx->query_aabb(min3, max3, &aabbs, /*build_aabbs_for_result=*/true); +``` + +AABBs are stored in `float16` for memory efficiency. Comparisons are +performed in `float32` to avoid precision issues at the boundary. + +## Subset by streamline IDs + +If you have a list of streamline indices from a prior step (clustering, +spatial query, manual selection), create a subset directly: + +```cpp +std::vector ids{0, 4, 42, 99}; +auto subset = trx->subset_streamlines(ids); + +subset->save("subset_by_id.trx", ZIP_CM_STORE); +subset->close(); +``` diff --git a/docs/spatial_queries.rst b/docs/spatial_queries.rst deleted file mode 100644 index c3e85e9..0000000 --- a/docs/spatial_queries.rst +++ /dev/null @@ -1,61 +0,0 @@ -Spatial Queries -=============== - -trx-cpp can build per-streamline axis-aligned bounding boxes (AABBs) and -use them to extract spatial subsets efficiently. This is useful for -interactive slice-view updates or region-of-interest filtering. - -Query by bounding box ---------------------- - -Pass minimum and maximum corners in RAS+ world coordinates (mm): - -.. code-block:: cpp - - #include - - auto trx = trx::load("/path/to/tracks.trx"); - - std::array min_corner{-10.0f, -10.0f, -10.0f}; - std::array max_corner{ 10.0f, 10.0f, 10.0f}; - - auto subset = trx->query_aabb(min_corner, max_corner); - subset->save("subset.trx", ZIP_CM_STORE); - subset->close(); - -Precompute the AABB cache --------------------------- - -When issuing multiple spatial queries on the same file — for example, as a -user scrubs through slices in a viewer — precompute the AABB cache once and -pass it to each query: - -.. code-block:: cpp - - auto aabbs = trx->build_streamline_aabbs(); - - // Query 1 - auto s1 = trx->query_aabb(min1, max1, &aabbs); - - // Query 2 — reuses the same cached bounding boxes - auto s2 = trx->query_aabb(min2, max2, &aabbs); - - // Optionally build the AABB cache for the result as well - auto s3 = trx->query_aabb(min3, max3, &aabbs, /*build_aabbs_for_result=*/true); - -AABBs are stored in ``float16`` for memory efficiency. Comparisons are -performed in ``float32`` to avoid precision issues at the boundary. - -Subset by streamline IDs -------------------------- - -If you have a list of streamline indices from a prior step (clustering, -spatial query, manual selection), create a subset directly: - -.. code-block:: cpp - - std::vector ids{0, 4, 42, 99}; - auto subset = trx->subset_streamlines(ids); - - subset->save("subset_by_id.trx", ZIP_CM_STORE); - subset->close(); diff --git a/docs/spec.md b/docs/spec.md new file mode 100644 index 0000000..4fbf4b4 --- /dev/null +++ b/docs/spec.md @@ -0,0 +1,120 @@ +# TRX Format Specification + +This page documents the on-disk layout and data model of the TRX +tractography format. TRX-cpp is an implementation of this specification; +the authoritative specification repository is at +. + +## General layout + +A TRX file is either an **uncompressed or compressed ZIP archive**, or a +**plain directory**. In both cases the internal structure is identical: the +file hierarchy describes the data, and filename components encode metadata. + +- Each file's **basename** is the name of the metadata field. +- Each file's **extension** is the dtype (e.g., `float16`, `uint32`). +- Multi-component arrays encode the number of components between the basename + and the extension (e.g., `positions.3.float16` has 3 components per row). + Single-component arrays may omit this field for readability. + +All arrays use **C-style (row-major) memory layout** and **little-endian +byte order**. + +Compression is optional. Use `ZIP_STORE` for uncompressed storage; use +`ZIP_DEFLATE` if compression is desired. Compressed files must be +decompressed before memory-mapping. + +## Header + +`header.json` is a JSON dictionary with the following fields: + +| Field | Type | Description | +| -------------- | ------------------ | ------------------------- | +| VOXEL_TO_RASMM | 4×4 array of float | Affine from voxel to RAS+ | +| DIMENSIONS | list of 3 uint16 | Reference image grid size | +| NB_STREAMLINES | uint32 | Number of streamlines | +| NB_VERTICES | uint64 | Total number of vertices | + +The header is primarily for human readability and downstream compatibility +checks. The authoritative array sizes come from the data arrays themselves. + +## Arrays + +**positions** (`positions.float16` / `positions.float32` / `positions.float64`) + +: All streamline vertices as a contiguous C array of shape `(NB_VERTICES, 3)`. + Stored in **RAS+ world space (millimeters)**, matching the MRtrix3 `.tck` + convention. + +**offsets** (`offsets.uint32` / `offsets.uint64`) + +: Prefix-sum index of length `NB_STREAMLINES + 1`. Element *i* is the index + in `positions` of the first vertex of streamline *i*. The final element + is a sentinel equal to `NB_VERTICES`. + + Streamline length: `length_i = offsets[i+1] - offsets[i]`. + +**dpv — data per vertex** (`dpv/.`) + +: Shape `(NB_VERTICES, 1)` or `(NB_VERTICES, N)`. Values are aligned + with `positions` row-by-row. + +**dps — data per streamline** (`dps/.`) + +: Shape `(NB_STREAMLINES, 1)` or `(NB_STREAMLINES, N)`. Values are + aligned with streamlines. + +**groups** (`groups/.uint32`) + +: Variable-length index arrays. Each file lists the 0-based indices of + streamlines belonging to the named group. All indices must satisfy + `0 ≤ id < NB_STREAMLINES`. Groups are non-exclusive: a streamline may + appear in multiple groups. + +**dpg — data per group** (`dpg//.`) + +: Shape `(1,)` or `(N,)`. Each subdirectory corresponds to one group. + Not all metadata fields need to be present in every group. + +## Accepted dtypes + +| Signed | Unsigned | Float | +| ------ | -------- | ------- | +| int8 | uint8 | float16 | +| int16 | uint16 | float32 | +| int32 | uint32 | float64 | +| int64 | uint64 | | + +## Example structure + +```text +OHBM_demo.trx +├── dpg/ +│ ├── AF_L/ +│ │ ├── mean_fa.float16 +│ │ ├── shuffle_colors.3.uint8 +│ │ └── volume.uint32 +│ ├── AF_R/ CC/ CST_L/ CST_R/ SLF_L/ SLF_R/ +├── dpv/ +│ ├── color_x.uint8 +│ ├── color_y.uint8 +│ ├── color_z.uint8 +│ └── fa.float16 +├── dps/ +│ ├── algo.uint8 +│ ├── algo.json +│ ├── clusters_QB.uint16 +│ ├── commit_colors.3.uint8 +│ └── commit_weights.float32 +├── groups/ +│ ├── AF_L.uint32 +│ ├── AF_R.uint32 +│ ├── CC.uint32 +│ ├── CST_L.uint32 +│ ├── CST_R.uint32 +│ ├── SLF_L.uint32 +│ └── SLF_R.uint32 +├── header.json +├── offsets.uint64 +└── positions.3.float16 +``` diff --git a/docs/spec.rst b/docs/spec.rst deleted file mode 100644 index 2ea3ca6..0000000 --- a/docs/spec.rst +++ /dev/null @@ -1,130 +0,0 @@ -TRX Format Specification -======================== - -This page documents the on-disk layout and data model of the TRX -tractography format. TRX-cpp is an implementation of this specification; -the authoritative specification repository is at -https://github.com/tee-ar-ex/trx-spec. - -General layout --------------- - -A TRX file is either an **uncompressed or compressed ZIP archive**, or a -**plain directory**. In both cases the internal structure is identical: the -file hierarchy describes the data, and filename components encode metadata. - -- Each file's **basename** is the name of the metadata field. -- Each file's **extension** is the dtype (e.g., ``float16``, ``uint32``). -- Multi-component arrays encode the number of components between the basename - and the extension (e.g., ``positions.3.float16`` has 3 components per row). - Single-component arrays may omit this field for readability. - -All arrays use **C-style (row-major) memory layout** and **little-endian -byte order**. - -Compression is optional. Use ``ZIP_STORE`` for uncompressed storage; use -``ZIP_DEFLATE`` if compression is desired. Compressed files must be -decompressed before memory-mapping. - -Header ------- - -``header.json`` is a JSON dictionary with the following fields: - -+------------------+----------------------------+---------------------------+ -| Field | Type | Description | -+==================+============================+===========================+ -| VOXEL_TO_RASMM | 4×4 array of float | Affine from voxel to RAS+ | -+------------------+----------------------------+---------------------------+ -| DIMENSIONS | list of 3 uint16 | Reference image grid size | -+------------------+----------------------------+---------------------------+ -| NB_STREAMLINES | uint32 | Number of streamlines | -+------------------+----------------------------+---------------------------+ -| NB_VERTICES | uint64 | Total number of vertices | -+------------------+----------------------------+---------------------------+ - -The header is primarily for human readability and downstream compatibility -checks. The authoritative array sizes come from the data arrays themselves. - -Arrays ------- - -**positions** (``positions.float16`` / ``positions.float32`` / ``positions.float64``) - All streamline vertices as a contiguous C array of shape ``(NB_VERTICES, 3)``. - Stored in **RAS+ world space (millimeters)**, matching the MRtrix3 ``.tck`` - convention. - -**offsets** (``offsets.uint32`` / ``offsets.uint64``) - Prefix-sum index of length ``NB_STREAMLINES + 1``. Element *i* is the index - in ``positions`` of the first vertex of streamline *i*. The final element - is a sentinel equal to ``NB_VERTICES``. - - Streamline length: ``length_i = offsets[i+1] - offsets[i]``. - -**dpv — data per vertex** (``dpv/.``) - Shape ``(NB_VERTICES, 1)`` or ``(NB_VERTICES, N)``. Values are aligned - with ``positions`` row-by-row. - -**dps — data per streamline** (``dps/.``) - Shape ``(NB_STREAMLINES, 1)`` or ``(NB_STREAMLINES, N)``. Values are - aligned with streamlines. - -**groups** (``groups/.uint32``) - Variable-length index arrays. Each file lists the 0-based indices of - streamlines belonging to the named group. All indices must satisfy - ``0 ≤ id < NB_STREAMLINES``. Groups are non-exclusive: a streamline may - appear in multiple groups. - -**dpg — data per group** (``dpg//.``) - Shape ``(1,)`` or ``(N,)``. Each subdirectory corresponds to one group. - Not all metadata fields need to be present in every group. - -Accepted dtypes ---------------- - -+----------+----------+----------+ -| Signed | Unsigned | Float | -+==========+==========+==========+ -| int8 | uint8 | float16 | -+----------+----------+----------+ -| int16 | uint16 | float32 | -+----------+----------+----------+ -| int32 | uint32 | float64 | -+----------+----------+----------+ -| int64 | uint64 | | -+----------+----------+----------+ - -Example structure ------------------ - -.. code-block:: text - - OHBM_demo.trx - ├── dpg/ - │ ├── AF_L/ - │ │ ├── mean_fa.float16 - │ │ ├── shuffle_colors.3.uint8 - │ │ └── volume.uint32 - │ ├── AF_R/ CC/ CST_L/ CST_R/ SLF_L/ SLF_R/ - ├── dpv/ - │ ├── color_x.uint8 - │ ├── color_y.uint8 - │ ├── color_z.uint8 - │ └── fa.float16 - ├── dps/ - │ ├── algo.uint8 - │ ├── algo.json - │ ├── clusters_QB.uint16 - │ ├── commit_colors.3.uint8 - │ └── commit_weights.float32 - ├── groups/ - │ ├── AF_L.uint32 - │ ├── AF_R.uint32 - │ ├── CC.uint32 - │ ├── CST_L.uint32 - │ ├── CST_R.uint32 - │ ├── SLF_L.uint32 - │ └── SLF_R.uint32 - ├── header.json - ├── offsets.uint64 - └── positions.3.float16 diff --git a/docs/streaming.md b/docs/streaming.md new file mode 100644 index 0000000..66f7ea5 --- /dev/null +++ b/docs/streaming.md @@ -0,0 +1,165 @@ +# Streaming Writes + +{class}`trx::TrxStream` is an append-only writer for cases where the total +streamline count is not known ahead of time. It writes to temporary files and +finalizes to a standard TRX archive or directory when complete. + +```{eval-rst} +.. note:: + + ``TrxStream`` is **not** thread-safe for concurrent writes. Use a single + writer thread (or the main thread) to append to the stream, while other + threads generate streamlines and deliver them via a queue. +``` + +## Single-threaded streaming + +```cpp +#include + +trx::TrxStream stream("float16"); + +for (/* each generated streamline */) { + std::vector> points = /* ... */; + stream.push_streamline(points); +} + +stream.finalize("tracks.trx", ZIP_CM_STORE); +``` + +## Multi-threaded producer / single writer + +Worker threads generate streamlines and push batches into a queue. A +dedicated writer thread owns the `TrxStream` and consumes from the queue. + +```cpp +#include +#include +#include +#include +#include + +struct Batch { + std::vector>> streamlines; +}; + +std::mutex mtx; +std::condition_variable cv; +std::queue q; +bool done = false; + +// Producer: generates streamlines, pushes batches into the queue. +auto producer = [&]() { + Batch batch; + batch.streamlines.reserve(1000); + for (int i = 0; i < 1000; ++i) { + batch.streamlines.push_back(/* generate points */); + } + { + std::lock_guard lock(mtx); + q.push(std::move(batch)); + } + cv.notify_one(); +}; + +// Writer: owns TrxStream, appends batches from the queue. +trx::TrxStream stream("float16"); +auto writer = [&]() { + for (;;) { + std::unique_lock lock(mtx); + cv.wait(lock, [&] { return done || !q.empty(); }); + if (q.empty() && done) return; + Batch batch = std::move(q.front()); + q.pop(); + lock.unlock(); + for (const auto& pts : batch.streamlines) { + stream.push_streamline(pts); + } + } +}; + +std::thread writer_thread(writer); +std::thread t1(producer), t2(producer); +t1.join(); t2.join(); +{ std::lock_guard lock(mtx); done = true; } +cv.notify_all(); +writer_thread.join(); + +stream.finalize("tracks.trx", ZIP_CM_STORE); +``` + +## MRtrix-style write kernel + +MRtrix3 uses a multi-threaded producer stage and a single-writer kernel to +serialize output. The same pattern works with TRX by encapsulating the +`TrxStream` inside a kernel object: + +```cpp +struct TrxWriteKernel { + explicit TrxWriteKernel(const std::string& path) + : stream("float16"), out_path(path) {} + + void operator()(const std::vector>>& batch) { + for (const auto& pts : batch) { + stream.push_streamline(pts); + } + } + + void finalize() { + stream.finalize(out_path, ZIP_CM_STORE); + } + +private: + trx::TrxStream stream; + std::string out_path; +}; +``` + +The key rule is: **only the writer thread touches \`\`TrxStream\`\`**, while +worker threads only generate streamlines. + +## Process-based sharding + +For very large tractograms generated in parallel processes, each process can +write to a shard directory and a parent process merges the shards afterward. + +`TrxStream` provides two finalization methods for directory output: + +- `finalize_directory()` — removes any existing directory before writing. + Safe for single-process workflows where you control the full lifecycle. +- `finalize_directory_persistent()` — does **not** remove existing + directories. Required when a parent process pre-creates the output + directory. + +Recommended multiprocess pattern: + +1. **Parent** pre-creates shard directories. +2. Each **child** calls `finalize_directory_persistent()` after appending + all streamlines. +3. Child writes a sentinel file (e.g., `SHARD_OK`) to signal completion. +4. **Parent** waits for all sentinels, then merges shards. + +```cpp +// Parent: pre-create shard directories +for (size_t i = 0; i < num_shards; ++i) { + std::filesystem::create_directories("shards/shard_" + std::to_string(i)); +} + +// Child: write shard and signal completion +trx::TrxStream stream("float16"); +// ... push_streamline calls ... +stream.finalize_directory_persistent("/path/to/shards/shard_0"); +std::ofstream ok("/path/to/shards/shard_0/SHARD_OK"); +ok << "ok\n"; + +// Parent: merge shards after all SHARD_OK files are present. +// See bench/bench_trx_stream.cpp for a reference merge implementation. +``` + +```{eval-rst} +.. note:: + Use ``finalize_directory()`` for single-process writes where you want a + clean output state. Use ``finalize_directory_persistent()`` for + multiprocess workflows to avoid removing directories that may be checked + for existence by other processes. +``` diff --git a/docs/streaming.rst b/docs/streaming.rst deleted file mode 100644 index b1e9736..0000000 --- a/docs/streaming.rst +++ /dev/null @@ -1,166 +0,0 @@ -Streaming Writes -================ - -:class:`trx::TrxStream` is an append-only writer for cases where the total -streamline count is not known ahead of time. It writes to temporary files and -finalizes to a standard TRX archive or directory when complete. - -.. note:: - - ``TrxStream`` is **not** thread-safe for concurrent writes. Use a single - writer thread (or the main thread) to append to the stream, while other - threads generate streamlines and deliver them via a queue. - -Single-threaded streaming --------------------------- - -.. code-block:: cpp - - #include - - trx::TrxStream stream("float16"); - - for (/* each generated streamline */) { - std::vector> points = /* ... */; - stream.push_streamline(points); - } - - stream.finalize("tracks.trx", ZIP_CM_STORE); - -Multi-threaded producer / single writer ----------------------------------------- - -Worker threads generate streamlines and push batches into a queue. A -dedicated writer thread owns the ``TrxStream`` and consumes from the queue. - -.. code-block:: cpp - - #include - #include - #include - #include - #include - - struct Batch { - std::vector>> streamlines; - }; - - std::mutex mtx; - std::condition_variable cv; - std::queue q; - bool done = false; - - // Producer: generates streamlines, pushes batches into the queue. - auto producer = [&]() { - Batch batch; - batch.streamlines.reserve(1000); - for (int i = 0; i < 1000; ++i) { - batch.streamlines.push_back(/* generate points */); - } - { - std::lock_guard lock(mtx); - q.push(std::move(batch)); - } - cv.notify_one(); - }; - - // Writer: owns TrxStream, appends batches from the queue. - trx::TrxStream stream("float16"); - auto writer = [&]() { - for (;;) { - std::unique_lock lock(mtx); - cv.wait(lock, [&] { return done || !q.empty(); }); - if (q.empty() && done) return; - Batch batch = std::move(q.front()); - q.pop(); - lock.unlock(); - for (const auto& pts : batch.streamlines) { - stream.push_streamline(pts); - } - } - }; - - std::thread writer_thread(writer); - std::thread t1(producer), t2(producer); - t1.join(); t2.join(); - { std::lock_guard lock(mtx); done = true; } - cv.notify_all(); - writer_thread.join(); - - stream.finalize("tracks.trx", ZIP_CM_STORE); - -MRtrix-style write kernel --------------------------- - -MRtrix3 uses a multi-threaded producer stage and a single-writer kernel to -serialize output. The same pattern works with TRX by encapsulating the -``TrxStream`` inside a kernel object: - -.. code-block:: cpp - - struct TrxWriteKernel { - explicit TrxWriteKernel(const std::string& path) - : stream("float16"), out_path(path) {} - - void operator()(const std::vector>>& batch) { - for (const auto& pts : batch) { - stream.push_streamline(pts); - } - } - - void finalize() { - stream.finalize(out_path, ZIP_CM_STORE); - } - - private: - trx::TrxStream stream; - std::string out_path; - }; - -The key rule is: **only the writer thread touches ``TrxStream``**, while -worker threads only generate streamlines. - -Process-based sharding ------------------------ - -For very large tractograms generated in parallel processes, each process can -write to a shard directory and a parent process merges the shards afterward. - -``TrxStream`` provides two finalization methods for directory output: - -- ``finalize_directory()`` — removes any existing directory before writing. - Safe for single-process workflows where you control the full lifecycle. -- ``finalize_directory_persistent()`` — does **not** remove existing - directories. Required when a parent process pre-creates the output - directory. - -Recommended multiprocess pattern: - -1. **Parent** pre-creates shard directories. -2. Each **child** calls ``finalize_directory_persistent()`` after appending - all streamlines. -3. Child writes a sentinel file (e.g., ``SHARD_OK``) to signal completion. -4. **Parent** waits for all sentinels, then merges shards. - -.. code-block:: cpp - - // Parent: pre-create shard directories - for (size_t i = 0; i < num_shards; ++i) { - std::filesystem::create_directories("shards/shard_" + std::to_string(i)); - } - - // Child: write shard and signal completion - trx::TrxStream stream("float16"); - // ... push_streamline calls ... - stream.finalize_directory_persistent("/path/to/shards/shard_0"); - std::ofstream ok("/path/to/shards/shard_0/SHARD_OK"); - ok << "ok\n"; - - // Parent: merge shards after all SHARD_OK files are present. - // See bench/bench_trx_stream.cpp for a reference merge implementation. - -.. note:: - Use ``finalize_directory()`` for single-process writes where you want a - clean output state. Use ``finalize_directory_persistent()`` for - multiprocess workflows to avoid removing directories that may be checked - for existence by other processes. diff --git a/docs/writing.md b/docs/writing.md new file mode 100644 index 0000000..8790c0e --- /dev/null +++ b/docs/writing.md @@ -0,0 +1,67 @@ +# Writing TRX Files + +This page covers creating TRX files from scratch and saving loaded files. +For append-only streaming writes when the total count is not known ahead of +time, see {doc}`streaming`. + +## Create and save a TRX file + +Allocate a {class}`trx::TrxFile` with the desired number of vertices and +streamlines, fill the positions and offsets arrays, then call `save`: + +```cpp +#include + +const size_t nb_vertices = 500; +const size_t nb_streamlines = 10; + +trx::TrxFile trx(nb_vertices, nb_streamlines); + +auto& positions = trx.streamlines->_data; // (NB_VERTICES, 3) +auto& offsets = trx.streamlines->_offsets; // (NB_STREAMLINES + 1, 1) +auto& lengths = trx.streamlines->_lengths; // (NB_STREAMLINES, 1) + +size_t cursor = 0; +offsets(0) = 0; +for (size_t i = 0; i < nb_streamlines; ++i) { + const size_t len = 50; // 50 vertices per streamline in this example + lengths(i) = static_cast(len); + offsets(i + 1) = offsets(i) + len; + for (size_t j = 0; j < len; ++j, ++cursor) { + positions(cursor, 0) = /* x */; + positions(cursor, 1) = /* y */; + positions(cursor, 2) = /* z */; + } +} + +trx.save("tracks.trx", ZIP_CM_STORE); +trx.close(); +``` + +Pass `ZIP_CM_DEFLATE` instead of `ZIP_CM_STORE` to enable compression. +Compression reduces file size at the cost of slower read/write throughput; +`ZIP_CM_STORE` is preferred for large files accessed over fast storage. + +## Modify and re-save a loaded file + +```cpp +auto trx = trx::load_any("tracks.trx"); + +// Add or update a header field. +auto header_obj = trx.header.object_items(); +header_obj["COMMENT"] = "processed by my_tool"; +trx.header = json11::Json(header_obj); + +trx.save("tracks_annotated.trx", ZIP_CM_STORE); +trx.close(); +``` + +## Save as a directory + +Pass a directory path (without a `.trx` extension) to write an unzipped +TRX directory instead of a zip archive. Directory output avoids ZIP overhead +and is faster for large files on spinning disks: + +```cpp +trx.save("/path/to/output_dir"); +``` diff --git a/docs/writing.rst b/docs/writing.rst deleted file mode 100644 index 07829fd..0000000 --- a/docs/writing.rst +++ /dev/null @@ -1,71 +0,0 @@ -Writing TRX Files -================= - -This page covers creating TRX files from scratch and saving loaded files. -For append-only streaming writes when the total count is not known ahead of -time, see :doc:`streaming`. - -Create and save a TRX file --------------------------- - -Allocate a :class:`trx::TrxFile` with the desired number of vertices and -streamlines, fill the positions and offsets arrays, then call ``save``: - -.. code-block:: cpp - - #include - - const size_t nb_vertices = 500; - const size_t nb_streamlines = 10; - - trx::TrxFile trx(nb_vertices, nb_streamlines); - - auto& positions = trx.streamlines->_data; // (NB_VERTICES, 3) - auto& offsets = trx.streamlines->_offsets; // (NB_STREAMLINES + 1, 1) - auto& lengths = trx.streamlines->_lengths; // (NB_STREAMLINES, 1) - - size_t cursor = 0; - offsets(0) = 0; - for (size_t i = 0; i < nb_streamlines; ++i) { - const size_t len = 50; // 50 vertices per streamline in this example - lengths(i) = static_cast(len); - offsets(i + 1) = offsets(i) + len; - for (size_t j = 0; j < len; ++j, ++cursor) { - positions(cursor, 0) = /* x */; - positions(cursor, 1) = /* y */; - positions(cursor, 2) = /* z */; - } - } - - trx.save("tracks.trx", ZIP_CM_STORE); - trx.close(); - -Pass ``ZIP_CM_DEFLATE`` instead of ``ZIP_CM_STORE`` to enable compression. -Compression reduces file size at the cost of slower read/write throughput; -``ZIP_CM_STORE`` is preferred for large files accessed over fast storage. - -Modify and re-save a loaded file ---------------------------------- - -.. code-block:: cpp - - auto trx = trx::load_any("tracks.trx"); - - // Add or update a header field. - auto header_obj = trx.header.object_items(); - header_obj["COMMENT"] = "processed by my_tool"; - trx.header = json11::Json(header_obj); - - trx.save("tracks_annotated.trx", ZIP_CM_STORE); - trx.close(); - -Save as a directory -------------------- - -Pass a directory path (without a ``.trx`` extension) to write an unzipped -TRX directory instead of a zip archive. Directory output avoids ZIP overhead -and is faster for large files on spinning disks: - -.. code-block:: cpp - - trx.save("/path/to/output_dir"); From dfbde3085c4cf7cd844e0a15398ecb25776d6310 Mon Sep 17 00:00:00 2001 From: Ariel Rokem Date: Wed, 24 Jun 2026 12:04:07 -0700 Subject: [PATCH 2/7] Deploy docs with GHA instead of RTD. So we can be uniform across all projects, please. --- .readthedocs.yaml | 29 ----------------------------- README.md | 6 +++--- docs/index.md | 4 ++-- 3 files changed, 5 insertions(+), 34 deletions(-) delete mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index 3a6876b..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,29 +0,0 @@ -version: 2 - -build: - os: ubuntu-22.04 - tools: - python: "3.11" - apt_packages: - - cmake - - g++ - - pkg-config - - zlib1g-dev - - libeigen3-dev - - libzip-dev - - ninja-build - - zipcmp - - zipmerge - - ziptool - - jobs: - pre_build: - - cmake -S . -B build -DTRX_BUILD_DOCS=ON - - cmake --build build --target docs - -python: - install: - - requirements: docs/requirements.txt - -sphinx: - configuration: docs/conf.py diff --git a/README.md b/README.md index be71291..f6e30b0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TRX-cpp -[![Documentation](https://readthedocs.org/projects/trx-cpp/badge/?version=latest)](https://trx-cpp.readthedocs.io/en/latest/) +[![Docs](https://img.shields.io/badge/docs-github.io-blue?style=flat&logo=github)](https://tee-ar-ex.github.io/trx-cpp/) [![codecov](https://codecov.io/gh/tee-ar-ex/trx-cpp/branch/main/graph/badge.svg)](https://codecov.io/gh/tee-ar-ex/trx-cpp) A C++17 library for reading, writing, and memory-mapping the [TRX tractography format](https://github.com/tee-ar-ex/trx-spec) — efficient storage for large-scale tractography data. @@ -39,11 +39,11 @@ auto positions = trx.positions.as_matrix(); // (NB_VERTICES, 3) trx.close(); ``` -See [Building](https://trx-cpp.readthedocs.io/en/latest/building.html) for platform-specific dependency installation and [Quick Start](https://trx-cpp.readthedocs.io/en/latest/quick_start.html) for a complete first program. +See [Building](https://tee-ar-ex.github.io/trx-cpp/building.html) for platform-specific dependency installation and [Quick Start](https://tee-ar-ex.github.io/trx-cpp/quick_start.html) for a complete first program. ## Documentation -Full documentation is at **[trx-cpp.readthedocs.io](https://trx-cpp.readthedocs.io/en/latest/)**. +Full documentation is at **[tee-ar-ex.github.io/trx-cpp](https://tee-ar-ex.github.io/trx-cpp/)**. ## Third-party notices diff --git a/docs/index.md b/docs/index.md index 5e157f4..1fe9b7a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,8 @@ # TRX-cpp Documentation -```{image} https://readthedocs.org/projects/trx-cpp/badge/?version=latest +```{image} https://img.shields.io/badge/docs-github.io-blue?style=flat&logo=github :alt: Documentation Status -:target: https://trx-cpp.readthedocs.io/en/latest/ +:target: https://tee-ar-ex.github.io/trx-cpp/ ``` ```{image} https://codecov.io/gh/tee-ar-ex/trx-cpp/branch/main/graph/badge.svg From 4db72452084f1d1fdd5568a5d0a96feb50301335 Mon Sep 17 00:00:00 2001 From: Ariel Rokem Date: Wed, 24 Jun 2026 12:08:25 -0700 Subject: [PATCH 3/7] Adds the GHA config. --- .github/workflows/deploy-docs.yml | 77 +++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/deploy-docs.yml diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..cf694e2 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,77 @@ +name: Deploy docs + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: ${{ github.workflow }}-${{ + github.event_name == 'pull_request' + && format('pr-{0}', github.event.number) + || github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake g++ pkg-config \ + zlib1g-dev libeigen3-dev libzip-dev \ + ninja-build doxygen + + - name: Install Python dependencies + run: python3 -m pip install -r docs/requirements.txt + + - name: Build docs + run: | + cmake -S . -B build -DTRX_BUILD_DOCS=ON + cmake --build build --target docs + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + name: ${{ github.event_name == 'pull_request' + && format('github-pages-preview-{0}', github.event.number) + || 'github-pages' }} + path: docs/_build/html + + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + + deploy-preview: + if: github.event_name == 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages-preview-${{ github.event.number }} + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy PR preview + id: deployment + uses: actions/deploy-pages@v4 + with: + preview: true + artifact_name: github-pages-preview-${{ github.event.number }} From b693d9bf4d8fba1de362fe094375067ce9070fd0 Mon Sep 17 00:00:00 2001 From: Ariel Rokem Date: Wed, 24 Jun 2026 12:18:15 -0700 Subject: [PATCH 4/7] Configure build more thoroughly. --- .github/workflows/deploy-docs.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index cf694e2..e86efb7 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -36,9 +36,21 @@ jobs: - name: Install Python dependencies run: python3 -m pip install -r docs/requirements.txt + - name: Configure trx-cpp + run: | + cmake -S . -B build \ + -G Ninja \ + -DTRX_BUILD_DOCS=ON \ + -DTRX_BUILD_TESTS=ON \ + -DTRX_BUILD_EXAMPLES=ON \ + -DTRX_ENABLE_NIFTI=ON \ + -DGTest_DIR=${GITHUB_WORKSPACE}/deps/googletest/install/lib/cmake/GTest \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_FLAGS="--coverage" \ + -DCMAKE_CXX_FLAGS="--coverage" + - name: Build docs run: | - cmake -S . -B build -DTRX_BUILD_DOCS=ON cmake --build build --target docs - name: Upload artifact From db43823587225e6bc0396b1c175f80a5621dfaad Mon Sep 17 00:00:00 2001 From: Ariel Rokem Date: Wed, 24 Jun 2026 12:43:27 -0700 Subject: [PATCH 5/7] Install more apt dependencies. --- .github/workflows/deploy-docs.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index e86efb7..9eeafd1 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -27,11 +27,22 @@ jobs: - name: Install system dependencies run: | + sudo apt-get update + sudo apt-get install -y software-properties-common + sudo add-apt-repository universe sudo apt-get update sudo apt-get install -y \ - cmake g++ pkg-config \ - zlib1g-dev libeigen3-dev libzip-dev \ - ninja-build doxygen + cmake \ + g++ \ + pkg-config \ + zlib1g-dev \ + libeigen3-dev \ + libzip-dev \ + zipcmp \ + zipmerge \ + ziptool \ + ninja-build \ + doxygen - name: Install Python dependencies run: python3 -m pip install -r docs/requirements.txt From bbb67c0b6f1498429dc8b79fab8a88e13291ac9a Mon Sep 17 00:00:00 2001 From: Ariel Rokem Date: Wed, 24 Jun 2026 12:46:08 -0700 Subject: [PATCH 6/7] Upgrade this action. --- .github/workflows/deploy-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 9eeafd1..10e163f 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -82,7 +82,7 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 deploy-preview: if: github.event_name == 'pull_request' @@ -94,7 +94,7 @@ jobs: steps: - name: Deploy PR preview id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 with: preview: true artifact_name: github-pages-preview-${{ github.event.number }} From b5ef0e880c8a3e42295771a369a439598129ed5c Mon Sep 17 00:00:00 2001 From: Ariel Rokem Date: Wed, 24 Jun 2026 12:52:00 -0700 Subject: [PATCH 7/7] Get rid of deploy-preview, which cannot be run on fork PRs. We'll need to download the artifact to preview. --- .github/workflows/deploy-docs.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 10e163f..a0dc69d 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -83,18 +83,3 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v5 - - deploy-preview: - if: github.event_name == 'pull_request' - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages-preview-${{ github.event.number }} - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Deploy PR preview - id: deployment - uses: actions/deploy-pages@v5 - with: - preview: true - artifact_name: github-pages-preview-${{ github.event.number }}