Skip to content

Raytracer Postprocessing

The thor.raytracer module (part of thor-rt) provides visualization tools for raytracer output stored in Zarr format (.zr directories).

Data Format

The raytracer writes output as Zarr stores:

output.zr/
└── projections/
    ├── temperature         # static field (single frame)
    ├── emissivity_0        # frame 0
    ├── emissivity_1        # frame 1
    └── ...

Each field array carries zarr attributes with metadata (e.g., Parameters.length_unit_cm, Config, field units).

Fields with a _N suffix form frame sequences for movie rendering. Fields without a suffix are static (single-frame).

Plotting a Single Frame

From Python

import zarr
from thor.raytracer.postprocess.projections import do_projection_raytrace_base

zarr_obj = zarr.open("my_sim/output.zr", mode="r")
do_projection_raytrace_base(
    zarr_obj,
    fieldname="emissivity",
    frame_id=0,
    width_pkpc=500.0,
    show=True,
)

From CLI

thor-tools raytracer plot-frame ./my_sim/ --field emissivity --frame 0

do_projection_raytrace_base

Core rendering function. Reads a field from the Zarr store, applies colormap/vrange settings, and renders it on a matplotlib axes.

do_projection_raytrace_base(
    zarr_obj,
    fieldname,
    show=False,
    width_pkpc=None,
    vranges_tmp=None,
    scalebar_conf=None,
    scalebar_size=-1.0,
    frame_id=0,
    ax=None,
    rgba_process_dict=None,
    swapaxes=False,
    yflip=False,
    custom_render_options=None,
    app_path=None,
    pixel_perfect=False,
    dpi=100,
    config_filename="config.yaml",
    sb_convert=False,
    metadata_field=None,
    verbose=False,
)

Key Parameters

Parameter Type Default Description
zarr_obj zarr.Group required Opened Zarr store
fieldname str required Field name in projections/ group
frame_id int 0 Frame index (appended as _N suffix)
width_pkpc float or None None Physical extent in proper kpc. Auto-computed from config if omitted.
ax matplotlib.axes.Axes or None None Target axes. A new figure is created if omitted.
show bool False Call plt.show() after rendering
vranges_tmp dict or None None Override value ranges: {fieldname: (vmin, vmax)}
custom_render_options dict or None None Override colormap, labels, etc. (loaded from render_options.yaml)
app_path str or Path or None None Application directory (for loading config and render options)
config_filename str "config.yaml" Config file name within app_path
sb_convert bool False Convert field data to surface brightness units
metadata_field str or None None Field name for looking up units/labels (if different from fieldname due to populate mapping)
pixel_perfect bool False Match figure size exactly to data resolution
dpi int 100 Output DPI
scalebar_conf dict or None None Scalebar styling (color, position, font, etc.)
scalebar_size float -1.0 Scalebar length in pkpc. Negative or zero disables it.
swapaxes bool False Swap x and y axes of the data
yflip bool False Flip data along y-axis
verbose bool False Debug output

Returns: (im, ax) — matplotlib AxesImage and Axes objects.

Render Configuration

config.yaml

The raytracer reads box size, redshift, and field mappings from the simulation config:

raytracer:
  outputpath: output.zr
  linename: FeXXV
  populate:
    Emissivity: "cloudyemission_Fe25 1.85040A"
  operators:
    projections:
      npixels: [1024, 1024]
      widths: [1.0, 1.0]
      fields:
        - Temperature
        - Emissivity

The populate mapping connects projection field names to source dataset fields. This mapping is used to resolve colormaps, labels, and unit conversions.

render_options.yaml

Optional per-simulation render customization. Place in the application directory alongside .zr output:

# Resolution and output
oversampling_factor: 2.0
framerate: 24
file_suffix: "hires"
interpolation: "bilinear"     # imshow interpolation method (default: "none")

# Colormaps, ranges, and labels (keyed by lowercase field name)
cmaps:
  temperature: "inferno"
  emissivity: "plasma"
vranges:
  temperature: [1e4, 1e7]
  surface_brightness:
    generic: [1e-24, 1e-18]
labels:
  temperature: "T [K]"

# Overlays
scalebar_size: 100.0          # pkpc, 0 to disable
show_colorbar: true
surface_brightness: true      # convert emissivity fields to SB units
watermark:
  label: "My Simulation"
  fontsize: 8

Plot Configuration

The plotconf module provides default colormaps, value ranges, and axis labels for known fields:

from thor.raytracer.postprocess.plotconf import get_cmap, get_vrange, get_label

cmap = get_cmap("temperature")    # returns matplotlib Colormap object
vmin, vmax = get_vrange("temperature")
label = get_label("temperature")  # returns LaTeX label string

Movie Rendering

From CLI

# Render all frames to MP4
thor-tools raytracer render-movie ./my_sim/ --field emissivity --fps 24

# Parallel rendering with 8 workers, keep frame PNGs
thor-tools raytracer render-movie ./my_sim/ -j 8 --keep-frames

Programmatic Movie Workflow

import zarr
from pathlib import Path
from thor.raytracer.postprocess.projections import do_projection_raytrace_base
import matplotlib.pyplot as plt

app_path = Path("./my_sim")
zarr_obj = zarr.open(str(app_path / "output.zr"), mode="r")

# Render individual frames
for i in range(100):
    fig, ax = plt.subplots(figsize=(8, 8))
    do_projection_raytrace_base(
        zarr_obj,
        fieldname="emissivity",
        frame_id=i,
        ax=ax,
        app_path=app_path,
    )
    fig.savefig(f"frames/frame_{i:03d}.png", dpi=150)
    plt.close(fig)

Then assemble with ffmpeg:

ffmpeg -framerate 24 -i frames/frame_%03d.png -c:v libx264 -pix_fmt yuv420p movie.mp4

Tip

The render-movie CLI command handles ffmpeg invocation, parallel frame rendering, and encoder detection automatically. Use the programmatic approach only when you need custom per-frame logic.

Listing Available Fields

from thor.raytracer.postprocess.projections import list_fields_info

zarr_obj = zarr.open("my_sim/output.zr", mode="r")
list_fields_info(zarr_obj, for_movie=True)  # prints fields with frame counts

Or from the CLI:

thor-tools raytracer plot-frame ./my_sim/ --list-fields
thor-tools raytracer render-movie ./my_sim/ --list-fields

Physical Extent

The projection extent in physical kpc is computed from the simulation config:

from thor.raytracer.postprocess.plotconf import calculate_physical_extent, load_config_yaml

config = load_config_yaml(app_path, "config.yaml")
extent = calculate_physical_extent(config, boxsize)
# Returns: (x_min, x_max, y_min, y_max) in pkpc

Box size is extracted via thor.common.boxsize.get_boxsize() from the config's dataset section or the simulation HDF5 header.

Surface Brightness Conversion

To convert raw projection data to surface brightness units:

from thor.raytracer.postprocess.plotconf import convert_to_surface_brightness

converted_data, field_attrs = convert_to_surface_brightness(
    data, field_attrs, img_size_pkpc, delta_area, config, fieldname
)

Or pass sb_convert=True to do_projection_raytrace_base.