Development
Guide for contributing to Thor.
Setup
Prerequisites
- All installation requirements
- uv for Python dependencies
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:
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-timeFP.ip(VAR): cast to the compile-timeIP(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 todocs/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 requiresgit add -fsince.gitignoreblocks*.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:
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):
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:
Uploading
Stage the file anywhere on disk, then run:
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
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\briefDoxygen 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.