Skip to content

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:

\[ P(x,y) = \int_0^L q(s) \cdot w(s) \, ds \]

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 (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

x, y, z ~ Uniform([0, 1))

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 (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:

  1. What ends a segment — the close rule.
  2. What is accumulated within a segment — the accumulator.
  3. How the ray itself propagates — the propagation mode (straight, or field-aligned / curved).
  4. 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 velocityVelocityX/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: requires center (point to orbit around)
  • traverse: requires travel_distance (total distance to travel along direction)

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.