Ray Tracer Driver
The RayTracer driver follows deterministic ray paths through the simulation domain (vs. the MCRT driver's stochastic photon transport). Use it for:
- Projections: column density maps, temperature-weighted projections
- Volume rendering: 3D visualization with transfer functions
- Line tracers: quantities along specific sight lines
Architecture
flowchart TD
subgraph RayTracer["RayTracer Driver"]
init[Initialize] --> run[run]
run --> proj[run_projections]
run --> trace[run_tracers]
run --> abs[run_absorptiongrids]
end
subgraph Operators["Operators"]
proj --> ProjOp[ProjectionOperator]
proj --> VolOp[VolumeRenderOperator]
trace --> TracerOp[TracerOperator]
abs --> AbsOp[OpticalDepthGridOperator]
end
subgraph Kernel["SYCL Kernel"]
ProjOp --> kernel[evolve_step]
VolOp --> kernel
TracerOp --> kernel
AbsOp --> kernel
kernel --> field[Field Access]
end
field --> smart[SmartFieldAccessor]
field --> dynamic[DynamicFieldAccessor]
Operators
ProjectionOperator
Computes 2D projections by integrating field values along rays:
Where:
- \(q(s)\) is the quantity being projected (e.g., Temperature)
- \(w(s)\) is the weight field (typically Density)
- \(L\) is the path length through the domain
Configuration example:
raytracer:
dist_max: 0.7 # maximum ray distance (box widths)
max_step: 1e-2 # maximum step length
outputpath: output.zr
overwrite: true
linename: FeXXV # required for line-specific fields
populate: # map derived fields to loaded dataset fields
Emissivity: "cloudyemission_Fe25 1.85040A"
operators:
projections:
mode: manual
view: orthogonal # orthogonal, perspective, or equirectangular
position: [0.5, 0.5, 0.0]
direction: [0.0, 0.0, 1.0]
up: [0.0, 1.0, 0.0]
npixels: [512, 512] # [width, height] in pixels
widths: [1.0, 1.0] # field of view [x, y] in box units
fields:
- Temperature
- Density
use_weighting: true # weight projections by density
perform_averaging: true # normalize by total weight (density-weighted average)
Ray Tracer Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
dist_max |
float | - | Maximum ray distance (in box widths) |
max_step |
float | 1.0 |
Maximum step length along the ray |
min_step |
float | 0 |
Minimum step length |
outputpath |
string | required | Output file path |
overwrite |
bool | false |
Overwrite existing output |
linename |
string | - | Spectral line name (required for line-specific fields) |
populate |
dict | - | Maps derived field names to dataset field names |
kernel_stats |
bool | false |
Compute kernel execution statistics |
Ray Sources
Most raytracer examples put camera keys (mode, view, position,
direction, npixels, ...) directly under an operator block. Operators can
also use a ray_source: block, which selects a named ray generator and makes
the ray layout explicit.
Supported ray_source.type values:
| Type | Layout | Description |
|---|---|---|
camera_plane |
Grid2D |
Manual orthographic or perspective camera plane. Uses camera keys such as position, direction or center, up, npixels, widths, and fov_up. |
camera_equirectangular |
Grid2D |
Manual full-sky equirectangular camera. Uses position, npixels, and the camera orientation keys. |
camera_healpix |
Healpix |
Manual HEALPix camera. Requires nside; ordering defaults to ring. |
random_uniform |
Scatter |
Unordered rays with positions sampled uniformly in the normalized unit box. Directions are either fixed or sampled isotropically. |
grid_centers |
Scatter |
One ray per cell of a uniform N³ (or [nx,ny,nz]) lattice — volume-uniform seeding. |
dataset_points |
Scatter |
One ray per dataset cell center (Voronoi sites), optionally a random count subsample. Particle/Voronoi datasets only. |
The camera_* sources are the ray_source: spelling of the manual camera path:
type determines the camera view, so mode/view are not needed inside the
block. Camera animation modes (rotate, traverse, nframes, and explicit
frame lists) remain on the legacy inline camera configuration. stereoscopic
is rejected under ray_source: because non-image operators size their buffers
from the mono ray layout.
Random uniform rays
random_uniform emits n_rays rays with start positions
in normalized box coordinates. It sets each ray origin to the sampled position.
The output layout is Scatter, so rays have no pixel, image, or HEALPix
ordering.
| Parameter | Type | Default | Description |
|---|---|---|---|
type |
string | required | Must be random_uniform. |
n_rays |
int | required | Number of rays to emit; must be greater than zero. |
direction_distribution |
string | fixed |
fixed uses one direction for every ray; isotropic samples each direction uniformly on the unit sphere. |
direction |
list | required for fixed |
Direction vector for direction_distribution: fixed; it is normalized before use and must be non-zero. |
seed |
int | 42 |
32-bit RNG seed. The same seed gives the same ray list. |
Example with random positions and isotropic random directions:
raytracer:
operators:
tracers:
ray_source:
type: random_uniform
n_rays: 10000
direction_distribution: isotropic
seed: 12345
fields: [Density]
Example with random positions and a shared fixed direction:
raytracer:
operators:
tracers:
ray_source:
type: random_uniform
n_rays: 10000
direction_distribution: fixed
direction: [0.0, 0.0, 1.0]
seed: 12345
fields: [Density]
Uniform-grid cell centers
grid_centers puts one ray at the center of each cell of a uniform N³ (or
[nx, ny, nz]) lattice — a repeatable, volume-uniform seeding of the box.
| Parameter | Type | Default | Description |
|---|---|---|---|
type |
string | required | grid_centers. |
ngrid |
int or list | required | N for an N×N×N lattice, or [nx, ny, nz]. |
direction, direction_distribution, seed |
As for random_uniform. |
Dataset point centers
dataset_points seeds one ray per dataset cell — for a Voronoi mesh, the
cell-generating sites — or a random count subsample (0 = all, reproducible
from seed). Where the grid is volume-uniform, this samples per cell, so rays
concentrate where the cells are smallest, i.e. in the dense gas. Needs a dataset
with per-cell positions (Voronoi/particle); others report no points and stop.
| Parameter | Type | Default | Description |
|---|---|---|---|
type |
string | required | dataset_points. |
count |
int | 0 |
Cells to subsample (0 = all). |
direction, direction_distribution, seed |
As for random_uniform. |
Scatter layout is currently useful for TracerOperator on one MPI rank.
Multi-rank tracer runs reject Scatter sources because distributed skewer
merging needs a meaningful camera depth order. Structured-output operators also
reject Scatter: ProjectionOperator requires Grid2D or Healpix, while
VolumeRenderOperator, OpticalDepthGridOperator, and
CoherenceLengthOperator require Grid2D.
VolumeRenderOperator
Performs volume rendering with density-weighted field values for 3D visualization.
TracerOperator
Records a per-ray profile along each sight line: instead of collapsing the
ray to a single pixel (as ProjectionOperator does), it stores an ordered list
of segments with, for each, the segment's path length and an accumulated
quantity (e.g. the mean field value over that segment). Output is therefore a
ragged, per-ray structure ("skewers"), not a 2D image — so it accepts any ray
layout (Grid2D, Healpix, or Scatter ray sources).
The operator is built from four orthogonal, JIT-specialized choices:
- What ends a segment — the close rule.
- What is accumulated within a segment — the accumulator.
- How the ray itself propagates — the propagation mode (straight, or field-aligned / curved).
- What ends the whole ray — the stop rule (in addition to the always-on
dist_max/ domain-exit / stuck-ray terminators).
Segment model: close rule + accumulator
close_rule |
A new segment begins when… |
|---|---|
every_cell (default) |
every cell crossing — one segment per cell (the classic tracer/skewer) |
angle_threshold |
the propagation/field direction has rotated past angle_threshold from the segment's seed direction — one segment per coherent run (the coherence-length segmentation, now carrying a quantity) |
accumulator |
Stored per segment (contributions) |
|---|---|
weighted_mean (default) |
weight-weighted mean of the traced fields[0] over the segment. weight: density (default) → mass-weighted; weight: none → path-length-weighted (spatial) mean |
length_only |
nothing — only the segment length is recorded (reproduces CoherenceLengthOperator output) |
Only these (close_rule, accumulator) pairs are supported; any other
combination terminates at startup:
close_rule |
accumulator |
Result |
|---|---|---|
every_cell |
weighted_mean |
per-cell mean quantity (default tracer) |
angle_threshold |
length_only |
per-coherence-segment length (≡ coherence length) |
angle_threshold |
weighted_mean |
per-coherence-segment length and mean quantity (e.g. mean |B| per coherence segment) |
Curved-ray propagation
By default rays travel straight. Setting propagation_mode: field_aligned makes
each ray follow a vector field (direction_field) cell-by-cell — it becomes
a streamline. The ray source then only seeds the field-line entry points; the
path is determined by the field. This is the same DirectionFunctor abstraction
the CoherenceLengthOperator uses, so a field-aligned tracer measures, e.g., the
coherence length of a magnetic field line and the mean |B| along it in one pass.
field_aligned requires raytracer.dist_max
A field line need not ever leave the box — with PBC, or on a closed/winding
streamline, it can trace forever without a cap. Set raytracer.dist_max to the
longest arc length you want to follow (a path-length budget, not the sqrt(3)
straight-line diagonal). Near-null cells (|direction_field| <= min_field_magnitude)
don't steer the ray — it coasts through with its previous direction — and a
stuck-ray guard ends any zero-progress ray, logging a count.
Ray-stop rule
The close rule decides where a segment ends; the stop rule decides where
the whole ray ends. They are orthogonal. By default (stop_rule: never) a ray
runs until one of the always-on terminators fires — raytracer.dist_max, leaving
the domain, or the stuck-ray guard. stop_rule: angle_threshold adds an early
termination: the ray ends when direction_field bends past stop_angle_threshold.
stop_rule |
The ray terminates when… |
|---|---|
never (default) |
only the standard terminators fire (dist_max / domain exit / stuck guard) |
angle_threshold |
the direction_field direction rotates past stop_angle_threshold (the breaking cell is not logged — before-add semantics) |
stop_rule: angle_threshold requires close_rule: every_cell
The angle stop is only allowed with the per-cell close rule — that is its sole
intended pairing ("log every cell until the field bends"). With close_rule:
angle_threshold the ray already segments at each bend, so an angle stop on top
would just truncate at the first segment (redundant); THOR rejects that combo at
startup. It also keeps the kernel-instantiation count down (the StopAtAngleThreshold
variant is compiled only for every_cell).
stop_angle_reference chooses what the rotation is measured against, exactly like
angle_reference does for the close rule:
continuous(default) — compares each cell to the previous one, so the ray stops at the first adjacent cell-to-cell bend exceeding the threshold.cumulative— compares each cell to the ray's seed direction, so the ray stops once the field has drifted past the threshold relative to where it started (there is no re-seed — a stop ends the ray).
The angle is measured on the raw field direction — the stop does not apply
flip_anti_parallel. A ~180° reversal of the field between cells therefore exceeds
any sub-180° threshold and terminates the ray (a field reversal is treated as a
genuine coherence break). This matches the close rule's angle convention; note it
differs from propagation_mode: field_aligned, which does flip so the ray keeps
heading forward through a reversal — so a field-aligned ray can coast through a
reversal while the stop rule ends it there.
The canonical pairing is close_rule: every_cell + stop_rule: angle_threshold:
it logs every cell (full per-cell fidelity) but only until the field bends,
giving a bounded, physically meaningful per-ray length. That bound is what keeps a
per-cell trace tractable at full resolution — an unbounded every_cell trace runs
to dist_max and emits orders of magnitude more entries.
Angle stop vs. angle close — when to use which
To measure the coherence-length distribution, use close_rule:
angle_threshold (it emits one length per coherent run, many per ray). To log
the per-cell trajectory up to the first bend, use close_rule: every_cell +
stop_rule: angle_threshold. A cumulative angle stop paired with a
reduction is largely redundant with the angle close — the stop's value is
realized when paired with per-cell logging.
Configuration
The camera-grid keys (mode, view, position, direction, up, npixels,
widths, …) are shared with the projection operators — see
Projection Parameters, View Types, and
Camera Modes. The tracer-specific keys:
| Parameter | Type | Default | Description |
|---|---|---|---|
fields |
list | [Density] |
Quantities to accumulate per segment, in one pass (up to MAX_TRACE_FIELDS, default 8; see accumulators.h). fields[0] is the primary (routed through the field accessor; may be specialized/JIT); fields[1..] are extra array-backed fields sampled directly from the dataset. Recipe/wildcard fields (e.g. MagneticFieldMagnitude) resolve here. Single-field output is the contributions dataset (unchanged); multi-field writes one contributions_<field> dataset per field. Multi-field requires accumulator: weighted_mean and a single MPI rank. |
close_rule |
string | every_cell |
Segment boundary rule (see above). |
accumulator |
string | weighted_mean |
Per-segment accumulation (see above). |
weight |
string | density |
weighted_mean weighting: density (mass-weighted) or none (path-length / spatial). Note: with none and a specialized/computed primary field, Density must still be loaded (it's used as a placeholder weight and then ignored). |
angle_threshold |
float (deg) | 30.0 |
angle_threshold close rule: segment ends past this deviation. |
angle_reference |
string | cumulative |
cumulative = deviation from the segment seed direction; continuous = deviation between consecutive cells. |
min_segment_length |
float | 0.0 |
Discard segments shorter than this (box units). Suppresses sub-resolution grazing-crossing micro-segments on unstructured meshes. 0 = off. |
stop_rule |
string | never |
Whole-ray termination rule (see above). never or angle_threshold. |
stop_angle_threshold |
float (deg) | 30.0 |
angle_threshold stop rule: the ray ends when direction_field bends past this. |
stop_angle_reference |
string | continuous |
continuous = bend between consecutive cells (stop at first sharp kink); cumulative = bend from the ray's seed direction. |
propagation_mode |
string | straight |
straight or field_aligned. |
direction_field |
string | velocity |
Vector field the ray follows (field_aligned) and/or the close rule / stop rule measures (angle_threshold). Resolves <name>X/Y/Z (or velocity → VelocityX/Y/Z). |
flip_anti_parallel |
bool | false |
field_aligned: flip the new direction on a negative dot with the prior one. Set true for magnetic fields (a field line has no inherent +/− sign); leave false for directional fields like velocity. |
min_field_magnitude |
float | 1.0e-10 |
Cells with |direction_field| below this are treated as nulls — the ray coasts straight through with frozen direction (no segment contribution). |
max_segments_per_ray |
int | 100 |
Initial per-ray segment buffer; grows dynamically if a ray exceeds it. |
write_tracer_origins |
bool | true |
Write each ray's origin position to the output. |
Output
Written under the tracers/<field>/ zarr group as flat arrays indexed by a
per-ray offsets table. The written arrays are compacted: ray i's
entries occupy [offsets[i], offsets[i+1]) exactly (last ray runs to the end
of the array), so its segment count is offsets[i+1] - offsets[i] — no
padding. Naming note: the array called lengths is not a segment count:
under weighted_mean it holds the per-segment path lengths (in normalized
box units); under length_only it stays zero and the segment length is
written to contributions instead (see the table).
| Array | dtype | Layout | Meaning |
|---|---|---|---|
offsets |
uint32 | one per ray | start index of ray i's slice in the flat arrays |
lengths |
float | flat (segments) | per-segment path length, normalized box units (weighted_mean; zeros under length_only) |
contributions |
float | flat (segments) | per-segment accumulated quantity (weighted_mean: the mean; length_only: the segment length itself) |
origins |
float | one per ray | ray origin position (when write_tracer_origins) |
Distributed (MPI) support
TracerOperator has MPI support for the legacy/default tracer mode:
close_rule: every_cell, accumulator: weighted_mean,
propagation_mode: straight, and stop_rule: never. This path is covered by
the tracer_mpi, tracer_mpi_pbc, and tracer_mpi_pbc_offaxis regression
tests, which compare multi-rank output against a one-rank baseline.
The newer tracer modes are single-rank only. propagation_mode:
field_aligned, close_rule: angle_threshold, and stop_rule: angle_threshold
terminate at startup under MPI: field-aligned rays bend away from the
precomputed per-rank segment table, angle-threshold segmentation would over-split
at every rank boundary, and the angle stop's per-ray reference cannot follow a
ray across a rank handoff. Scatter ray sources (e.g. random_uniform) are
likewise single-rank because they have no meaningful camera depth axis to merge.
Examples
Classic per-cell skewer (default), Density along an orthographic grid:
raytracer:
operators:
tracers:
mode: manual
view: orthogonal
position: [0.01, 0.5, 0.5]
direction: [1.0, 0.0, 0.0]
npixels: [256, 256]
widths: [0.98, 0.98]
fields: [Density]
# close_rule: every_cell, accumulator: weighted_mean, weight: density (defaults)
Magnetic field-line coherence length and mean |B| per segment (curved ray):
raytracer:
dist_max: 1.1 # REQUIRED for field_aligned; max path length to trace
operators:
tracers:
mode: manual
view: orthogonal
position: [0.01, 0.5, 0.5]
direction: [1.0, 0.0, 0.0] # seeds entry pixels only; path follows B
npixels: [640, 640]
widths: [0.98, 0.98]
fields: [MagneticFieldMagnitude]
close_rule: angle_threshold
accumulator: weighted_mean
weight: none # path-length-weighted (spatial) mean |B|
angle_threshold: 90.0
min_segment_length: 2.71e-8 # ~1 pc: drop sub-resolution micro-segments
propagation_mode: field_aligned
direction_field: MagneticField
flip_anti_parallel: true # required for magnetic (sign-agnostic) fields
min_field_magnitude: 1.0e-20
Log every cell of a magnetic field line until it kinks (per-cell
trajectory dump with a bounded, physically meaningful length — tractable at full
resolution because each ray stops at its first bend rather than running to
dist_max):
raytracer:
dist_max: 1.1 # REQUIRED for field_aligned; max path length to trace
operators:
tracers:
mode: manual
view: orthogonal
position: [0.01, 0.5, 0.5]
direction: [1.0, 0.0, 0.0]
npixels: [640, 640]
widths: [0.98, 0.98]
fields: [PositionX, PositionY, PositionZ, MagneticFieldMagnitude]
close_rule: every_cell # one entry per cell (full per-cell path)
accumulator: weighted_mean
weight: none
propagation_mode: field_aligned
direction_field: MagneticField
flip_anti_parallel: true
stop_rule: angle_threshold # end the ray at the first bend...
stop_angle_threshold: 30.0
stop_angle_reference: continuous # ...measured cell-to-cell
Projection Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode |
string | required | Camera mode (see below) |
view |
string | "orthogonal" |
Projection type (see below) |
fields |
list | required | Fields to project |
npixels |
list | [128, 128] |
Resolution as [width, height] |
widths |
list | - | Field of view [x, y] in box units |
position |
list | [0.5, 0.5, 0.5] |
Camera position [x, y, z] in normalized coords |
direction |
list | - | View direction [x, y, z] (unit vector) |
up |
list | [0, 1, 0] |
Up vector [x, y, z] (unit vector) |
nframes |
int | 1 |
Number of frames (for rotate/traverse modes) |
use_weighting |
bool | true |
Weight projections by density field |
perform_averaging |
bool | true |
Normalize by total weight (density-weighted average) |
invert |
bool | false |
Invert ray direction |
start_dist |
float | mode-dependent | Offset each ray forward from the camera by this distance (normalized box units), so integration skips an inner sphere of radius start_dist. Default is 0.001 for orthogonal/perspective/equirectangular (legacy epsilon) and 0 for healpix. Useful for healpix-from-galaxy-center maps where the central SFR cell would otherwise dominate every line of sight. |
View Types
| Value | Description |
|---|---|
"orthogonal" |
Parallel projection (default) |
"perspective" |
Perspective projection with field of view |
"equirectangular" |
360° equirectangular projection |
"healpix" |
Equal-area all-sky map in HEALPix RING ordering |
For perspective view, an additional fov_up parameter (in degrees, default 90) controls the vertical field of view.
For healpix, a single camera position emits one ray per HEALPix pixel; the
output is a 1D zarr array of length 12 * nside² (RING ordering). Required
extra keys:
| Parameter | Type | Default | Description |
|---|---|---|---|
nside |
int | required | HEALPix resolution; must be a positive power of two. |
ordering |
string | "ring" |
RING ordering only (NESTED not yet supported). |
The angular frame is box z-up: theta is measured from +z, phi
increases from +x toward +y, regardless of camera basis. The dataset
carries a healpix attribute block (nside, ordering, frame, npix)
in its zarr attributes; downstream Python code can call
healpy.pix2ang(nside, ipix) directly on the index. Example:
projections:
mode: manual
view: healpix
nside: 16 # 12 * 16² = 3072 pixels (Smith+19 convention)
position: [0.5, 0.5, 0.5]
start_dist: 0.005 # optional: skip inner 0.005 box-units (e.g. central SFR core)
fields: [Density, Temperature]
stereoscopic is not supported in healpix mode.
Camera Modes
The mode parameter controls how rays are generated across frames:
| Mode | Description |
|---|---|
manual |
Explicit position, direction, and up vectors |
rotate |
Rotate camera around a center point over nframes |
traverse |
Move camera along direction by travel_distance over nframes |
Mode-specific parameters:
rotate: requirescenter(point to orbit around)traverse: requirestravel_distance(total distance to travel alongdirection)
Field Access
Fields are selected at runtime; see JIT Field Access for the dispatch mechanism.
Output
Results are written to a Zarr store (.zr directory):
- Projections: 2D arrays of shape
(npixels_y, npixels_x) - Volume renders: RGB(A) images
- Tracers: per-ray field profiles
See Raytracer Postprocessing for reading the output.