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