Skip to content

Development

Guide for contributing to Thor.

Setup

Prerequisites

Clone and Build

git clone https://github.com/cbyrohl/thor.git
cd thor
./scripts/init-submodules.sh base

# Install dev dependencies
uv sync --group=ci-tests --group=docs

# Debug build
cmake -S . -B build \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_CXX_COMPILER=acpp

cmake --build build

Memory Usage

Compilation can require large amounts of memory. If you run out of memory during the build, reduce the number of concurrent builders by adding -j1 to the build command:

cmake --build build -j1

Vector components

Vec3<T> and Vec2<T> own fixed-size std::array storage. In C++, use v.x(), v.y() and (for Vec3) v.z() to read or modify named components, or v[i] for a runtime component index. These accessors return references to the same elements. Indexing is unchecked: valid indices are 0–2 for Vec3 and 0–1 for Vec2. v.data() exposes the contiguous component array; it does not allocate memory.

Code migrating from the separate-member representation must replace .x, .y and .z with accessor calls. Use explicit component reads instead of decomposing a vector with a structured binding. Arithmetic, formatting and YAML coordinate order remain the same. Python bindings retain the writable v.x, v.y and v.z properties.

MPI reflection registers the references returned by the component accessors. Vectors remain standard-layout and trivially copyable for SYCL captures and transfers.

Project Structure

thor/
├── src/
│   ├── datasets/           # Dataset implementations
│   ├── drivers/            # Simulation drivers
│   ├── interactors/        # Define photon interactions
│   ├── outputprocessors/   # Output handling
│   ├── photongenerators/   # MC package spawning
│   ├── rngs/               # Random number generators
│   └── main.cpp            # Entry point
├── tests/                  # Unit tests (Catch2)
├── regression_tests/       # End-to-end simulation tests
├── scripts/                # Utility scripts (PractRand testing, etc.)
└── extern/                 # (submodule) dependencies

Key Components

  • Datasets (src/datasets/): Computational meshes and geometry
  • Drivers (src/drivers/): Simulation algorithms (MCRT, ray tracing)

Code Style

C++20 with SYCL, formatted via ./.clang-format.

  • sp(VAR), dp(VAR), fp(VAR): cast to single, double, or compile-time FP.
  • ip(VAR): cast to the compile-time IP (32- or 64-bit signed integer).
  • Mark device-callable functions with SYCL_EXTERNAL.

Testing

The project includes both unit and regression tests. For a detailed explanation of the testing frameworks and how to run tests, please see the Testing documentation.

Documentation media assets

Where a media file lives depends on whether it should version with the repository:

  • Videos and animations (*.webm, *.mp4, animated *.webp) are never committed. They are hosted on the rolling GitHub release at thor-rt/docs-assets and referenced from markdown as full URLs.
  • Raster figures (*.png) also go to the docs-assets release by default, especially large or presentation-style renders. Commit a PNG to docs/assets/images/ only when it should track a code revision (benchmarks, validation plots, cookbook figures regenerated from committed scripts) or when it is small and stable. Committing one requires git add -f since .gitignore blocks *.png. The friction is deliberate: a committed PNG should be a conscious decision.
  • Vector figures (*.svg) are committed normally; they are small and diff as text.

Figure style (scripts/thorplot.py)

Plots that go into the docs should use the shared house style so every figure looks consistent. scripts/thorplot.py provides the THOR look — a minimal, slide-style aesthetic (light by default; dark=True for dark pages). It offers two colour strategies: a fixed per-code map (code_color) for neutral comparisons, and a "hero" treatment (hero_colors, bold accent for one series + muted peers) for THOR-centric figures. Plus small helpers:

import thorplot as tp
with tp.style():                     # or tp.style(dark=True)
    fig, ax = plt.subplots(figsize=tp.FIGSIZE_WIDE)
    ax.plot(x, y, color=tp.code_color("thor"))
    tp.strip_axes(ax); tp.legend_top(ax)
tp.save_doc_figure(fig, "my_plot")   # -> docs/assets/images/my_plot.svg

save_doc_figure writes SVG by default — vector-crisp at any zoom, smaller than PNG for line plots, and (unlike *.png) not blocked by .gitignore, so it commits without -f and diffs as text. Pass fmt="png" for raster-heavy figures (volume renders, dense scatter) where SVG would balloon.

Use tp.save_doc_figure() for all documentation exports, including figures rendered in companion repositories. Pass output_dir=... to save elsewhere and dpi=... to set raster resolution. The helper keeps exports transparent even when called outside tp.style(). Direct fig.savefig() calls inside the style also default to transparency. Use transparent=False for opaque standalone exports.

Generate both a light and a dark version and reference them with mkdocs-material's light/dark suffixes so the figure follows the page theme:

![alt](assets/images/my_plot.svg#only-light)
![alt](assets/images/my_plot_dark.svg#only-dark)

scripts/plot_benchmark.py is a worked example (it renders the Neufeld benchmark figure both ways).

The docs page stretches every image to the content-column width, so figures authored at different widths display their fonts at different sizes. The full-width reference is tp.DOC_FULL_WIDTH (12.2 in). Author full-column figures at that width; for anything narrower, embed at the proportional column width so fonts render at the intended size (save_doc_figure prints the attribute to use):

![alt](assets/images/my_hexbin.png#only-light){ width="57%" }

For 2D coordinate maps (imshow panels), the house look is minimal: no ticks, axis labels, or frames — a scale bar replaces the axes, and colorbars are frameless with markless ticks. The helpers are in thorplot too:

with tp.style(dark=dark):
    fig, axes = plt.subplots(1, 2, constrained_layout=True)
    for ax, m, title in zip(axes, maps, titles):
        im = ax.imshow(m, extent=extent, norm=norm, cmap="plasma")
        tp.imshow_panel(ax, title)       # strip ticks/labels/frame
    tp.scalebar(axes[0], 10.0, "10 pMpc")  # one bar; panels share the scale
    tp.colorbar_minimal(fig, im, axes, label=r"$\langle L \rangle$ [pMpc]")
    tp.suptitle_left(fig, "...", dark=dark)

Figure-generating scripts that only need data committed to this repo live in scripts/plot_*.py. Scripts that need external snapshots or long runs live in the companion thor_setups repo (setups/<topic>/python/, importing thorplot from a side-by-side thor checkout). In that case record the provenance next to the embed in the page source:

<!-- figure-source: thor_setups/setups/coherence_length (plot_tracer_stats.py) -->

Uploading

Stage the file anywhere on disk, then run:

./scripts/upload-docs-asset.sh path/to/banner.webp path/to/clip.webm

The script bootstraps the docs-assets release on first use, uploads with --clobber (re-uploads overwrite), and prints the canonical URLs to paste into markdown.

URL pattern

https://github.com/thor-rt/docs-assets/releases/download/docs-assets/<filename>

Naming rules

  • Filenames are unique on the release. Don't re-upload to the same name unless you intend to replace the live content for every page that references it; bump a version suffix (banner_v2.webp) when in doubt.
  • Orphaned assets stay until cleaned up explicitly: gh release delete-asset docs-assets --repo thor-rt/docs-assets <filename>.

Sanity checks

# What's currently on the release
gh release view docs-assets --repo thor-rt/docs-assets --json assets -q '.assets[].name'

# What URLs the docs reference
grep -rhoE 'releases/download/docs-assets/[A-Za-z0-9._-]+' docs/ | sort -u

If something is in the second list but not the first, it's a broken image on the deployed site. If only in the first, it's an orphan.

AI Authorship Attribution

This project uses AI-assisted development (Claude Code) extensively. AI-generated code is tagged for transparency:

  • Functions: [AI-Claude] tag at the end of the \brief Doxygen line
  • Tests: [AI] tag in Catch2 test tags, e.g., [mytag][AI]
  • Shell scripts: [AI-Claude] at end of first description comment line
  • Python modules: [AI-Claude] at end of first docstring line

Tags are removed after in-depth human review and approval.

Contributing

Contribute by submitting pull requests from your fork and the new feature branch.

Report issues at GitHub Issues.