Skip to content

How to compute per-modifier contributions with Rcontrib

Rcontrib is a builder class that wraps the rcontrib CLI tool. It traces rays through a scene and records how much each ray's radiance was contributed by each named modifier (material or light source). This makes it the core tool for:

  • Computing daylight coefficients per window group
  • Separating direct and indirect contributions
  • The view matrix step in matrix-based methods (3PM, 4PM, 5PM)

Basic usage

Instantiate Rcontrib with the input rays (as bytes), the octree path, and any ray-tracing parameters. Then call .add_modifier() for each modifier you want to track, and finally call the object to execute.

import pyradiance as pr

# Input rays: one "ox oy oz dx dy dz" line per ray
rays = b"0.5 0.5 0.8  0 0 1\n"

rc = pr.Rcontrib(
    inp=rays,
    octree="scene.oct",
    params=["-ab", "1", "-ad", "1000", "-lw", "1e-4"],
)
rc.add_modifier(modifier="sky_glow")

result = rc()
print(result.decode())

Each output row contains the RGB contribution from sky_glow to the corresponding input ray. The row order matches the input rays one-to-one.

Tracking multiple modifiers

Call .add_modifier() once per modifier. Rcontrib supports method chaining:

rc = (
    pr.Rcontrib(inp=rays, octree="scene.oct", params=["-ab", "1"])
    .add_modifier(modifier="window_north", output="north.hdr")
    .add_modifier(modifier="window_south", output="south.hdr")
)
rc()

When output is set, each modifier's result is written to a separate file instead of stdout.

Using cal expressions and binning

rcontrib can bin contributions by direction using a cal expression. This is the basis for the view matrix (V-matrix) in the 3-phase method, where each sky patch is a separate bin.

import pyradiance as pr

with open("sensors.txt", "rb") as f:
    rays = f.read()

rc = pr.Rcontrib(
    inp=rays,
    octree="scene.oct",
    params=["-ab", "5", "-ad", "10000", "-lw", "1e-4", "-c", "1000"],
    yres=len(rays.splitlines()),   # one row per sensor
    inform="a",
    outform="f",
)
rc.add_modifier(
    modifier="skyglow",
    calfile="reinhart.cal",        # binning cal file from Radiance library
    expression="MF:4",             # Reinhart MF:4 subdivision
    nbins="Nrbins",                # number of bins expression from cal file
    binv="rbin",                   # bin index expression from cal file
    output="vmtx.dmx",
)
rc()

The resulting vmtx.dmx is the V-matrix used in dctimestep() or Rmtxop.

Loading modifier names from a file

For scenes with many modifiers (e.g., multiple window groups defined in a separate file), use modifier_path instead of modifier to pass a file containing one modifier name per line:

rc = pr.Rcontrib(inp=rays, octree="scene.oct")
rc.add_modifier(modifier_path="window_modifiers.txt")
result = rc()

Parallel processing

Set nproc to use multiple CPU cores. Multi-process support on Windows is experimental; on Linux/macOS it is fully supported.

rc = pr.Rcontrib(
    inp=rays,
    octree="scene.oct",
    nproc=8,
    params=["-ab", "2"],
)
rc.add_modifier(modifier="skyglow")
result = rc()

Complete example: separating window contributions

This example shows how to compute the contribution of two window groups (win_east and win_west) to a grid of floor sensors.

import pyradiance as pr
import numpy as np

# Build the sensor ray list: 5×5 grid on the floor at z=0.8 m, facing up
xs = np.linspace(0.5, 4.5, 5)
ys = np.linspace(0.5, 4.5, 5)
rays = b""
for x in xs:
    for y in ys:
        rays += f"{x:.2f} {y:.2f} 0.8  0 0 1\n".encode()

# Run rcontrib for both window modifiers simultaneously
rc = (
    pr.Rcontrib(
        inp=rays,
        octree="room.oct",
        params=["-ab", "2", "-ad", "4096", "-lw", "1e-5"],
    )
    .add_modifier(modifier="win_east")
    .add_modifier(modifier="win_west")
)
result = rc()

# Parse ASCII output into a numpy array
# With the default outform="a", values are space-separated floats.
# rcontrib writes all modifier outputs concatenated: first all rays for
# modifier 1, then all rays for modifier 2.
nrays = len(rays.splitlines())
values = list(map(float, result.decode().split()))
# Shape: (n_modifiers, n_rays, 3_channels)
arr = np.array(values).reshape(2, nrays, 3)

# Luminance via standard RGB coefficients
east_luminance = arr[0] @ [47.4, 119.9, 11.6]   # shape (nrays,)
west_luminance = arr[1] @ [47.4, 119.9, 11.6]