User Guide

This package provides reusable optical material property definitions for fast integration with Geant4 geometries via pyg4ometry. It includes:

  • Ready-to-use functions to attach optical properties (wavelength-dependent and constant) to materials and optical surfaces.

  • A pluggable store mechanism to override or extend material properties without forking the package.

  • Utilities to read spectra from data files and interpolate them with physical units using pint.

See concrete geometry integrations and usage examples in:

These repositories demonstrate how this package is used to populate material properties in full detector geometries.

High-level API: attaching optical properties

Most materials expose convenience functions with the prefix pyg4_<material>_attach_*. These functions add properties to a Geant4 material (or optical surface) using pint-aware helpers that ensure correct units and sorting. Typical property names include:

  • RINDEX: refractive index (dimensionless)

  • ABSLENGTH: absorption length (length)

  • RAYLEIGH: Rayleigh scattering length (length)

  • REFLECTIVITY, EFFICIENCY: surface/optical parameters (dimensionless)

  • WLSABSLENGTH, WLSCOMPONENT, WLSTIMECONSTANT: wavelength-shifting properties

  • SCINTILLATIONCOMPONENT*, SCINTILLATIONTIMECONSTANT*, RESOLUTIONSCALE: scintillation models

Examples of high-level attachers:

Minimal usage outline (pseudo-code):

# Minimal outline with pyg4ometry (exact construction details may vary):
import pint
import pyg4ometry.geant4 as g4

from pygeomoptics.lar import (
    pyg4_lar_attach_rindex,
    pyg4_lar_attach_attenuation,
    pyg4_lar_attach_scintillation,
)

u = pint.get_application_registry().get()
reg = g4.Registry()

# Create your material in pyg4ometry here (example only; adjust to your setup)
lar_mat = g4.Material(name="LAr", density=1.396, registry=reg)  # density in g/cm**3

# Attach optical properties
pyg4_lar_attach_rindex(lar_mat, reg)
pyg4_lar_attach_attenuation(lar_mat, reg, lar_temperature=88.8 * u.K)
pyg4_lar_attach_scintillation(lar_mat, reg)

Under the hood, the helpers use pygeomoptics.pyg4utils to patch pyg4ometry with pint-aware methods:

  • Material.addVecPropertyPint(name, energy, values)

  • Material.addConstPropertyPint(name, value)

These ensure proper unit handling and ascending-energy ordering.

Modifying properties without forking (pluggable store)

Many property functions are decorated with pygeomoptics.store.register_pluggable(). This makes them dynamically replaceable at runtime.

Common operations:

from pygeomoptics import store
from pygeomoptics.pen import pen_refractive_index


# Replace a property implementation
def my_pen_rindex() -> float:
    return 1.52  # new constant refractive index


pen_refractive_index.replace_implementation(my_pen_rindex)

# Query which functions were replaced
assert "pen_refractive_index" in store.get_replaced()

# Reset just this function
pen_refractive_index.reset_implementation()

# Or reset everything back to defaults
store.reset_all_to_original()

Note: Any imported function that is decorated with @store.register_pluggable can be replaced. The wrapper preserves the original function and exposes:

  • wrap.replace_implementation(new_impl)

  • wrap.reset_implementation()

  • wrap.is_original()

  • wrap.original_impl()

Adding a new material

To integrate a new material in the same style:

  1. Provide data-backed property functions

Example:

import numpy as np
import pint
from pygeomoptics import store
from pygeomoptics.utils import readdatafile, InterpolatingGraph

u = pint.get_application_registry()


@store.register_pluggable
def mymat_refractive_index() -> float:
    return 1.37


@store.register_pluggable
def mymat_absorption() -> tuple[u.Quantity, u.Quantity]:
    λ, L = readdatafile("mymat_absorption.dat")  # "# unit1 unit2" header expected
    return λ, L
  1. Implement pyg4 attachers

Example:

import numpy as np
from pygeomoptics.pyg4utils import pyg4_sample_λ
import pint

u = pint.get_application_registry()


def pyg4_mymat_attach_rindex(mat, reg):
    λ = np.array([650.0, 115.0]) * u.nm
    r = [mymat_refractive_index()] * 2
    with u.context("sp"):
        mat.addVecPropertyPint("RINDEX", λ.to("eV"), r)


def pyg4_mymat_attach_absorption(mat, reg):
    λ, L = mymat_absorption()
    with u.context("sp"):
        mat.addVecPropertyPint("ABSLENGTH", λ.to("eV"), L)
  1. Optional: WLS and scintillation

  1. Include data files

  • Place spectral data in an importable package (e.g., the data directory in the package mypkg is importable as mypkg.data)

  • Format: first line header with units (# unit1 unit2), then pairs of numbers; comments allowed after # (after the header line).

CLI helper

A small CLI (defined in pygeomoptics.cli) can generate G4GeneralParticleSource emission spectra:

legend-pygeom-optics g4gps lar_emission out.mac
legend-pygeom-optics g4gps pen_emission out.mac
legend-pygeom-optics g4gps fiber_emission out.mac

This uses the same emission spectra as the attachers.

Practical examples

The three repositories illustrate how materials are defined with their attachers, and how to manage optical surfaces and properties consistently across a large detector model.

Tips and best practices

  • Always use pint quantities and the provided attach helpers to avoid unit mistakes.

  • When interpolating spectra, use InterpolatingGraph to handle extrapolation bounds predictably and similar to Geant4.

  • For wavelength/energy conversions, use a pint context, e.g. with u.context("sp").

  • Prefer overriding pluggable functions instead of patching code. This keeps your runs reproducible and centralized.

  • Keep emission spectra zeroed at sampling boundaries to avoid artifacts (see pen.py, fibers.py, lar.py patterns).