Nonsequential Ray Tracing API#
Public API reference for the non-sequential (NSQ) engine. For the conceptual overview and tutorials see the NSQ gallery; for the capability envelope and known limitations see NSQ Limitations & Roadmap; for the architecture and differentiability contract see the developer guide.
Note
Pre-release. NSQ has never shipped in a tagged Optiland release, so
the API may still change without a deprecation cycle. Scenes are built
almost entirely through the *Config dataclasses — the
Configuration reference below gives a
parameter table for each.
Scene & tracer#
- class NSQScene[source]#
Bases:
objectSingle user-facing entry point for Non-Sequential Raytracing.
NSQScene owns three typed registries and exposes builder methods for common optical elements (lenses, mirrors, doublets), sources, and detectors. The tracer works on flat surface/source/detector lists exposed via read-only properties.
Usage:
scene = NSQScene() scene.add_source('S1', cs, PointSourceConfig(...)) scene.add_lens('L1', cs, LensConfig(...)) scene.add_detector('D1', cs, IrradianceDetectorConfig(...)) result = scene.trace(num_rays=1_000_000, seed=42)
Mutation note: Components are held by reference. Modifying a component’s CoordinateSystem or geometry after scene construction is valid and takes effect on the next trace() call. If a BVH acceleration structure is later introduced, call scene.invalidate_cache() after any structural change.
- Variables:
component_registry – Registry of named compound components.
source_registry – Registry of named sources.
detector_registry – Registry of named detectors.
- add_component(name: str, component: BaseComponent) None[source]#
Add a raw BaseComponent (advanced use).
- Parameters:
name – Unique name for this component in the registry.
component – Pre-built BaseComponent to register.
- add_detector(name: str, cs: CoordinateSystem, config) None[source]#
Add a detector to the scene.
- Parameters:
name – Unique string name for the detector.
cs – Coordinate system for the detector.
config – Detector config dataclass (IrradianceDetectorConfig, SpectralDetectorConfig, FarFieldDetectorConfig, or RayDatabaseConfig).
- add_doublet(name: str, cs: CoordinateSystem, config: DoubletConfig) None[source]#
Add a cemented achromatic doublet to the scene.
- Parameters:
name – Unique name for the doublet in the registry.
cs – Front-vertex coordinate system.
config – DoubletConfig describing the doublet geometry.
- add_lens(name: str, cs: CoordinateSystem, config: LensConfig) None[source]#
Add a single refractive lens to the scene.
- Parameters:
name – Unique name for the lens in the registry.
cs – Front-vertex coordinate system.
config – LensConfig describing the lens geometry.
- add_mirror(name: str, cs: CoordinateSystem, config: MirrorConfig) None[source]#
Add a reflective mirror to the scene.
- Parameters:
name – Unique name for the mirror in the registry.
cs – Surface coordinate system.
config – MirrorConfig describing the mirror geometry.
- add_source(name: str, cs: CoordinateSystem, config) None[source]#
Add a source to the scene.
- Parameters:
name – Unique string name for the source.
cs – Coordinate system for the source.
config – Source config dataclass (PointSourceConfig, CollimatedSourceConfig, or ExtendedSourceConfig).
- property detector_names: list[str]#
Names of the registered detectors, in registration order.
These are the keys of
SimulationResult.detectors.
- property detectors#
Ordered list of all registered detectors.
- classmethod from_json(path: str | os.PathLike) NSQScene[source]#
Load a scene from a versioned JSON file.
The loaded scene is plain-valued: all numeric parameters are Python floats, not tensors. Re-wrap parameters in
torch.tensor(..., requires_grad=True)if you need gradients.- Parameters:
path – Path to a JSON file previously written by
to_json().- Returns:
Reconstructed
NSQScene.- Raises:
FileNotFoundError – If
pathdoes not exist.ValueError – If the
nsq_schema_versionis missing or does not match the current loader.
Example:
scene = NSQScene.from_json("my_scene.json")
- remove_component(name: str) None[source]#
Remove a compound component by name.
- Parameters:
name – Registry name of the component to remove.
- remove_detector(name: str) None[source]#
Remove a detector by name.
- Parameters:
name – Registry name of the detector to remove.
- remove_source(name: str) None[source]#
Remove a source by name.
- Parameters:
name – Registry name of the source to remove.
- property sources#
Ordered list of all registered sources.
- property surfaces: list[BaseComponent]#
Flat list of all component sub-surfaces in registration order.
- to_json(path: str | os.PathLike) None[source]#
Serialize the scene to a versioned JSON file.
Serializes all components, sources, and detectors. Simulation results and accumulated detector data are not included.
Tensor values (e.g.
torch.Tensorparameters) are detached and written as plain floats.requires_gradis not persisted; to differentiate a loaded scene, re-wrap the relevant parameters intorch.tensor(..., requires_grad=True)after loading.- Parameters:
path – Destination file path (created or overwritten).
- Raises:
TypeError – If a component/source/detector type is not serializable.
ValueError – If a material cannot be round-tripped (e.g. no catalog name is available).
Example:
scene.to_json("my_scene.json")
- trace(num_rays: int, max_depth: int = 16, min_flux_fraction: float = 1e-06, batch_size: int = 16384, seed: int | None = None, backend: TracerBackend | None = None, record_paths: bool | int = False) SimulationResult[source]#
Run the Monte Carlo simulation and return results.
- Parameters:
num_rays – Total rays to launch.
max_depth – Maximum surface hits per ray.
min_flux_fraction – Russian-roulette threshold, relative to per-ray initial flux – combined with the scene’s
sampling_policy.rr_start_flux(the larger of the two wins). Below threshold, rays are killed with an unbiased probability and survivors’ flux is boosted accordingly, rather than truncated outright.batch_size – Rays per processing batch. Does not change the result, only the speed; see
DEFAULT_BATCH_SIZE.seed – RNG seed.
backend – TracerBackend to use. Defaults to NumpyBackend or TorchBackend based on the active
optiland.backend.record_paths –
False(default) records nothing.Truerecords every ray’s full path. A positiveintrecords an approximately that-many-ray subset, selected deterministically byray_idhash, so a large trace stays cheap while still yielding a bounded sample for visualization/diagnosis, e.g.scene.trace(num_rays=10_000_000, record_paths=1_000).
- Returns:
SimulationResult with per-detector results and statistics.
- validate() None[source]#
Validate the scene for common configuration errors.
- Raises:
ValueError – If no sources or no detectors are registered.
- view(result: SimulationResult | None = None, **kwargs) None[source]#
Render a 2D (matplotlib) cross-section of the scene.
- Parameters:
result – Optional SimulationResult with ray paths to overlay.
**kwargs – Forwarded to NSQViewer2D.view().
- view3d(result: SimulationResult | None = None, **kwargs) None[source]#
Render a 3D (VTK) scene visualization.
- Parameters:
result – Optional SimulationResult with ray paths to overlay.
**kwargs – Forwarded to NSQViewer3D.view().
- class NSQTracer(scene: NSQScene, backend: TracerBackend | None = None)[source]#
Bases:
objectThin coordinator: holds backend configuration, delegates trace loop.
The coordinator pattern decouples scene construction from backend selection. The full simulation loop lives in the backend.
Usage:
tracer = NSQTracer(scene) result = tracer.trace(num_rays=1_000_000, seed=42)
- Variables:
scene – The NSQScene to simulate.
backend – TracerBackend instance (defaults to NumpyBackend or TorchBackend based on the active
optiland.backend).
- trace(num_rays: int, max_depth: int = 16, min_flux_fraction: float = 1e-06, batch_size: int = 16384, seed: int | None = None, backend: TracerBackend | None = None, record_paths: bool | int = False) SimulationResult[source]#
Run the simulation and return results.
- Parameters:
num_rays – Total rays to launch.
max_depth – Maximum surface interactions per ray before termination.
min_flux_fraction – Kill threshold relative to per-ray initial flux.
batch_size – Rays per processing batch. Does not change the result, only the speed; see
DEFAULT_BATCH_SIZE.seed – RNG seed for reproducibility.
backend – Backend override. Uses constructor backend if not given. Auto-selects from active
optiland.backendif neither is provided.record_paths –
Falserecords nothing,Truerecords every ray’s path, and a positiveintrecords an approximately that-many-ray subset selected deterministically byray_idhash – seeoptiland.nonsequential.path_recording.
- Returns:
SimulationResult.
- class SimulationResult(detectors: dict[str, object] = <factory>, num_rays_total: int = 0, num_rays_absorbed: int = 0, num_rays_escaped: int = 0, num_rays_flux_killed: int = 0, num_rays_depth_killed: int = 0, total_flux_in: float = 0.0, total_flux_detected: float = 0.0, total_flux_absorbed: float = 0.0, total_flux_bulk_absorbed: float = 0.0, total_flux_escaped: float = 0.0, total_flux_lost: float = 0.0, flux_conservation_error: float = 0.0, trace_time_sec: float = 0.0, ray_paths: dict | None = None, diagnostics: ~optiland.nonsequential.diagnostics.Diagnostics = <factory>)[source]#
Bases:
objectTop-level result returned by NSQTracer.trace() / NSQScene.trace().
- Variables:
detectors (dict[str, object]) – Per-detector result objects, keyed by detector name.
num_rays_total (int) – Total number of rays launched.
num_rays_absorbed (int) – Rays terminated by absorbing components.
num_rays_escaped (int) – Rays that left the scene with no hit.
num_rays_flux_killed (int) – Rays killed for falling below flux threshold.
num_rays_depth_killed (int) – Rays killed for exceeding max_depth.
total_flux_in (float) – Total flux launched by all sources [W].
total_flux_detected (float) – Total flux recorded on all detectors [W].
total_flux_absorbed (float) – Flux absorbed by AbsorbingComponents [W].
total_flux_bulk_absorbed (float) – Flux lost to Beer-Lambert bulk absorption while travelling through an absorbing medium (k > 0), e.g. tinted glass – distinct from
total_flux_absorbed, which is surface (AbsorbingComponent) absorption only [W].total_flux_escaped (float) – Flux carried by escaped rays [W].
total_flux_lost (float) – Flux lost to flux/depth kill [W].
flux_conservation_error (float) –
|flux_in - detected - absorbed - bulk_absorbed - escaped - lost| / flux_in.trace_time_sec (float) – Wall-clock time for the trace [s].
ray_paths (dict | None) – Optional per-ray event log dict (
{"events": structured_array}), populated whenrecord_pathsis truthy – seeoptiland.nonsequential.path_recording.diagnostics (optiland.nonsequential.diagnostics.Diagnostics) – Self-diagnosing summary of this trace – depth truncation, roulette loss, unreached geometry, per -detector sampling quality, and a threshold-based warning list. See
report()andoptiland.nonsequential.diagnostics.
- diagnostics: Diagnostics#
Sources#
Sources subpackage for Non-Sequential Raytracing.
- class BaseNSQSource(cs: CoordinateSystem, spectrum: Spectrum, total_flux: float = 1.0)[source]#
Bases:
ABCAbstract base class for non-sequential ray sources.
- Variables:
cs – Coordinate system defining source position/orientation.
spectrum – Wavelength distribution for Monte Carlo sampling.
total_flux – Source total flux [W] (or photons/sec).
- abstractmethod generate(ray_id: np.ndarray, rng: NSQRng) NSQRayBundle[source]#
Generate one ray per id in global coordinates.
- Each ray carries:
position sampled from source geometry
direction sampled from source emission pattern
wavelength sampled from spectrum (Monte Carlo)
initial flux = total_flux / len(ray_id)
All random draws are keyed by
ray_id(atbounce=0), so a ray’s birth-time sampling depends only on its own id – never onbatch_sizeor on the order sources/batches are processed in.- Parameters:
ray_id – Unique identifiers for the rays to generate, shape (N,).
rng – Keyed PCG32 RNG.
- Returns:
NSQRayBundle with all rays alive,
ray_idset, and flux = total_flux / len(ray_id).
- class CollimatedSource(cs: CoordinateSystem, spectrum: Spectrum, total_flux: float = 1.0, aperture_radius: float = 5.0, profile: Literal['tophat', 'gaussian'] = 'tophat', gaussian_sigma: float | None = None, medium=None)[source]#
Bases:
BaseNSQSourceParallel collimated beam with circular aperture.
All rays propagate along the local +z axis (after coordinate transformation to global frame). Intensity profile is either top-hat (uniform) or truncated Gaussian.
- Variables:
cs – Coordinate system (local z = beam propagation axis).
spectrum – Wavelength distribution.
total_flux – Total beam flux [W].
aperture_radius – Beam aperture radius [mm].
profile – Intensity profile (‘tophat’ or ‘gaussian’).
gaussian_sigma – Gaussian sigma [mm] (used when profile=’gaussian’).
medium – Medium the source is embedded in.
- generate(ray_id: np.ndarray, rng: NSQRng) NSQRayBundle[source]#
Generate collimated rays in global coordinates.
Positions are sampled within the circular aperture. All directions are along the local +z axis.
- Parameters:
ray_id – Unique identifiers for the rays to generate, shape (N,).
rng – Keyed PCG32 RNG.
- Returns:
NSQRayBundle with all rays alive and parallel directions.
- class CollimatedSourceConfig(spectrum: Spectrum, total_flux: float = 1.0, total_flux_lumens: float | None = None, aperture_radius: float = 1.0, profile: str = 'tophat', gaussian_sigma: float | None = None, medium: NSQMaterial | None = None)[source]#
Bases:
objectConfiguration for a CollimatedSource.
- Variables:
spectrum (Spectrum) – Wavelength distribution.
total_flux (float) – Total emitted flux [W]. Ignored (with a warning) when
total_flux_lumensis also set.total_flux_lumens (float | None) – Total emitted flux [lm], converted to watts via
spectrum– seePointSourceConfig.total_flux_lumens.aperture_radius (float) – Beam semi-diameter [mm].
profile (str) – Spatial profile –
'tophat'or'gaussian'.gaussian_sigma (float | None) – Gaussian sigma [mm]. Defaults to aperture_radius / 2.
medium (NSQMaterial | None) – Medium the source is embedded in (default: vacuum).
- medium: NSQMaterial | None = None#
- class ExtendedSource(cs: CoordinateSystem, spectrum: Spectrum, total_flux: float = 1.0, width: float = 1.0, height: float = 1.0, aperture_radius: float | None = None, half_angle_deg: float = 90.0, medium=None)[source]#
Bases:
BaseNSQSourceUniform area emitter on a rectangular or circular surface.
The source surface lies in the local x-y plane (z=0). Emission direction is either Lambertian (cosine-weighted hemisphere) or confined to a cone around the local +z axis.
- Variables:
cs – Coordinate system (local z = emission axis).
spectrum – Wavelength distribution.
total_flux – Total emitted flux [W].
width – Source width [mm] (used for rectangular aperture).
height – Source height [mm] (used for rectangular aperture).
aperture_radius – Circular aperture radius [mm]. If set, overrides width/height for a circular source.
half_angle_deg – Half-angle of emission cone [deg]. 90 = Lambertian hemisphere.
medium – Medium the source is embedded in.
- class ExtendedSourceConfig(spectrum: Spectrum, total_flux: float = 1.0, total_flux_lumens: float | None = None, width: float = 1.0, height: float = 1.0, aperture_radius: float | None = None, half_angle_deg: float = 90.0, medium: NSQMaterial | None = None)[source]#
Bases:
objectConfiguration for an ExtendedSource.
- Variables:
spectrum (Spectrum) – Wavelength distribution.
total_flux (float) – Total emitted flux [W]. Ignored (with a warning) when
total_flux_lumensis also set.total_flux_lumens (float | None) – Total emitted flux [lm], converted to watts via
spectrum– seePointSourceConfig.total_flux_lumens.width (float) – Source width [mm].
height (float) – Source height [mm].
aperture_radius (float | None) – Circular aperture radius [mm]. If set, overrides width/height for a circular source.
half_angle_deg (float) – Half-angle of the emission cone [deg]. Below 90 the rays are distributed uniformly within the cone; at 90 or above they are cosine-weighted over the full hemisphere (Lambertian), and values above 90 behave the same as 90.
medium (NSQMaterial | None) – Medium the source is embedded in (default: vacuum).
- medium: NSQMaterial | None = None#
- class PointSource(cs: CoordinateSystem, spectrum: Spectrum, total_flux: float = 1.0, half_angle_deg: float = 90.0, medium=None)[source]#
Bases:
BaseNSQSourcePoint source emitting rays from a single position.
Emission can be isotropic (full sphere) or confined to a cone around the local +z axis.
- Variables:
cs – Coordinate system (origin = source position).
spectrum – Wavelength distribution.
total_flux – Total emitted flux [W].
half_angle_deg – Half-angle of the emission cone [deg]. 180 = isotropic.
medium – Medium the source is embedded in.
- generate(ray_id: np.ndarray, rng: NSQRng) NSQRayBundle[source]#
Generate rays from a point source in global coordinates.
Directions are sampled uniformly within the emission cone using spherical coordinates. Wavelengths are Monte Carlo sampled from the spectrum.
- Parameters:
ray_id – Unique identifiers for the rays to generate, shape (N,).
rng – Keyed PCG32 RNG.
- Returns:
NSQRayBundle with all rays alive.
- class PointSourceConfig(spectrum: Spectrum, total_flux: float = 1.0, total_flux_lumens: float | None = None, half_angle_deg: float = 90.0, medium: NSQMaterial | None = None)[source]#
Bases:
objectConfiguration for a PointSource.
- Variables:
spectrum (Spectrum) – Wavelength distribution.
total_flux (float) – Total emitted flux [W]. Ignored (with a warning) when
total_flux_lumensis also set.total_flux_lumens (float | None) – Total emitted flux [lm], converted to watts using
spectrumviaoptiland.nonsequential.units.lumens_to_watts(). Takes precedence overtotal_fluxwhen set. Raises ifspectrumhas negligible overlap with the visible band.half_angle_deg (float) – Half-angle of the emission cone [deg]. 90 = hemisphere, 180 = full sphere (isotropic).
medium (NSQMaterial | None) – Medium the source is embedded in (default: vacuum).
- medium: NSQMaterial | None = None#
- class SourceRegistry[source]#
Bases:
objectNamed registry of NSQ sources.
- Variables:
_registry – Ordered dict mapping name -> BaseNSQSource.
- add(name: str, source: BaseNSQSource) None[source]#
Add a source.
- Parameters:
name – Unique identifier.
source – Source to register.
- Raises:
KeyError – If a source with
namealready exists.
- get(name: str) BaseNSQSource[source]#
Retrieve a source by name.
- Parameters:
name – Name of the source.
- Returns:
The registered source.
- Raises:
KeyError – If no source with
nameexists.
- remove(name: str) None[source]#
Remove a source by name.
- Parameters:
name – Name of the source to remove.
- Raises:
KeyError – If no source with
nameexists.
- property sources: list[BaseNSQSource]#
Ordered list of all registered sources.
- class Spectrum(wavelengths: numpy.ndarray, weights: numpy.ndarray)[source]#
Bases:
objectWavelength distribution for Monte Carlo sampling.
- Variables:
wavelengths (numpy.ndarray) – Wavelength values [µm].
weights (numpy.ndarray) – Relative spectral power weights (unnormalized).
- classmethod monochromatic(wavelength: float) Spectrum[source]#
Create a monochromatic spectrum at a single wavelength.
- Parameters:
wavelength – Wavelength [µm].
- Returns:
A Spectrum with a single wavelength.
- sample(ray_id: np.ndarray, bounce: np.ndarray, rng: NSQRng) np.ndarray[source]#
Sample one wavelength per ray from the spectrum.
Uses inverse-CDF (quantile) sampling.
- Parameters:
ray_id – Per-ray identifiers, shape (N,).
bounce – Per-ray bounce/step index, shape (N,) or scalar.
rng – Keyed PCG32 RNG.
- Returns:
Sampled wavelengths [µm], shape (N,).
- wavelengths: numpy.ndarray#
- weights: numpy.ndarray#
Components#
NSQ components subpackage.
- class AbsorbingComponent(cs: CoordinateSystem, geometry: ComponentGeometry, material_front: NSQMaterial = NSQMaterial(optiland_material=None, bsdf=None), name: str = '')[source]#
Bases:
BaseComponentAbsorbing surface that terminates all rays on contact.
Use for light traps, aperture stops, and baffles.
- Variables:
cs – Coordinate system.
geometry – Surface geometry.
name – Optional label.
- property bounding_box: AABB#
Axis-aligned bounding box in global coordinates.
- Returns:
AABB for this component.
- interact(rays: NSQRayBundle, t: np.ndarray, normals: np.ndarray, hit_mask: np.ndarray, rng: NSQRng, bsdf_ir: BsdfIR, n_geom: np.ndarray, sampling: SamplingPolicy | None = None, forced_branch: str | None = None) None[source]#
Kill all rays that hit this component (in-place).
- Parameters:
rays – Ray bundle updated in-place.
t – Hit distances [mm], shape (N,).
normals – Surface normals in global frame, shape (N, 3).
hit_mask – True for rays hitting this component, shape (N,).
rng – Keyed PCG32 RNG (unused).
bsdf_ir – Unused – absorbing surfaces never scatter.
n_geom – Unused – absorbing surfaces never determine sidedness.
sampling – Unused – an absorber has no stochastic branch (D2, PR11).
forced_branch – Unused – bounded splitting only applies to
RefractiveComponent.
- intersect(rays: NSQRayBundle) tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]#
Find the nearest intersection of alive rays with this component.
Transforms rays to local frame, delegates to geometry, then transforms normals back to global frame.
- Parameters:
rays – The ray bundle in global coordinates.
- Returns:
t: Per-ray distances [mm], shape (N,). inf if no hit.
normals: Surface normals in global frame, shape (N, 3).
hit_mask: Boolean hit mask, shape (N,).
- n_geom: Geometric (unflipped, direction-independent)
surface normal in global frame, shape (N, 3). See
ComponentGeometry.ray_intersect().
- Return type:
Tuple (t, normals, hit_mask, n_geom) in global frame
- class BaseComponent(cs: CoordinateSystem, geometry: ComponentGeometry, material_front: NSQMaterial, material_back: NSQMaterial, bsdf: BaseBSDF | None = None, name: str = '', scatter_fraction: float = 1.0)[source]#
Bases:
ABCAbstract base class for all non-sequential optical components.
Components define the geometry and optical interaction (reflection, refraction, absorption) for a surface in the NSQ scene.
- Variables:
cs – Coordinate system defining position and orientation in global frame.
geometry – Shape of the component surface.
material_front – Medium on the front side (normal-facing side).
material_back – Medium on the back side.
bsdf – Optional scatter model. None means specular-only.
name – Optional human-readable label.
- property bounding_box: AABB#
Axis-aligned bounding box in global coordinates.
- Returns:
AABB for this component.
- abstractmethod interact(rays: NSQRayBundle, t: np.ndarray, normals: np.ndarray, hit_mask: np.ndarray, rng: NSQRng, bsdf_ir: BsdfIR, n_geom: np.ndarray, sampling: SamplingPolicy | None = None, forced_branch: str | None = None) None[source]#
Apply optical interaction at hit points (in-place).
Updates ray positions, directions, flux, n_current, bounce, and alive status for rays that hit this component.
This is a private implementation detail of the reference NumPy/Torch interpreters (
optiland.nonsequential.ir.interpreter .apply_primitive_interactions), not the engine’s public dispatch contract – a non-Python backend never calls it.bsdf_iris what makes the dispatch IR-driven: whether to route a hit ray throughself.bsdfis decided frombsdf_ir.kind(verified to matchself.bsdf’s actual type by the caller), not from a bareself.bsdf is not Nonecheck.- Parameters:
rays – Ray bundle to update in-place.
t – Hit distances [mm], shape (N,).
normals – Surface normals in global frame, shape (N, 3).
hit_mask – True for rays that hit this component, shape (N,).
rng – Keyed PCG32 RNG for stochastic interactions.
bsdf_ir – This surface’s lowered BSDF descriptor (
BsdfIR(kind= "none")when no scatter model is attached), matchingself.bsdf.n_geom – Geometric (unflipped) surface normal in global frame, shape (N, 3): points from
material_fronttowardmaterial_back.RefractiveComponentuses this, not index proximity, to determine which material a ray is entering.sampling – The scene’s rare-path sampling policy. Only
RefractiveComponentconsults it, to resolve the Fresnel reflect/transmit branch probability;Noneis treated as the default (unbiased,reflect_prob="fresnel") policy.forced_branch –
"reflect"or"transmit"to deterministically force the branch instead of drawing it, orNonefor the normal stochastic draw. Used only by the NumPy forward engine’s bounded-splitting orchestration (PR11;optiland.nonsequential.ir.interpreter) to build both children of a split ray; ignored by every component exceptRefractiveComponent.
- intersect(rays: NSQRayBundle) tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray][source]#
Find the nearest intersection of alive rays with this component.
Transforms rays to local frame, delegates to geometry, then transforms normals back to global frame.
- Parameters:
rays – The ray bundle in global coordinates.
- Returns:
t: Per-ray distances [mm], shape (N,). inf if no hit.
normals: Surface normals in global frame, shape (N, 3).
hit_mask: Boolean hit mask, shape (N,).
- n_geom: Geometric (unflipped, direction-independent)
surface normal in global frame, shape (N, 3). See
ComponentGeometry.ray_intersect().
- Return type:
Tuple (t, normals, hit_mask, n_geom) in global frame
- class ComponentRegistry[source]#
Bases:
objectNamed registry of compound components.
Stores
CompoundComponentobjects keyed by name and provides the tracer with a flat list ofBaseComponentsurfaces.- Variables:
_registry – Ordered dict mapping name -> CompoundComponent.
- add(name: str, component: CompoundComponent) None[source]#
Add a compound component under the given name.
- Parameters:
name – Unique identifier for the component.
component – The compound component to register.
- Raises:
KeyError – If a component with
namealready exists.
- property compounds: list[CompoundComponent]#
Ordered list of all registered compound components.
- Returns:
List of compound components in registration order.
- get(name: str) CompoundComponent[source]#
Retrieve a compound component by name.
- Parameters:
name – Name of the component.
- Returns:
The registered compound component.
- Raises:
KeyError – If no component with
nameexists.
- remove(name: str) None[source]#
Remove a component by name.
- Parameters:
name – Name of the component to remove.
- Raises:
KeyError – If no component with
nameexists.
- property surfaces: list[BaseComponent]#
Flat list of all sub-surfaces in registration order.
- Returns:
Concatenated list of sub-surfaces from all compound components.
- class CompoundComponent[source]#
Bases:
ABCAbstract base class for multi-surface optical elements.
A
CompoundComponentis a logical grouping of one or moreBaseComponentsurfaces that collectively form a single optical element. Compound components are stored in theComponentRegistry; when the tracer needs a flat surface list it callssurfaceson each compound.Subclasses must implement
name,surfaces, andcoordinate_system.- abstract property coordinate_system: CoordinateSystem#
Primary coordinate system (front vertex for lenses, surface for mirrors).
- abstract property surfaces: list[BaseComponent]#
Ordered flat list of sub-surfaces for intersection testing.
- class Doublet(name: str, cs: CoordinateSystem, config: DoubletConfig)[source]#
Bases:
CompoundComponentCemented achromatic doublet.
Assembles five physical surfaces:
Front face – refractive, conic (crown element).
Cemented interface – refractive, conic (crown->flint).
Back face – refractive, conic (flint element).
Crown edge – cylindrical frustum, absorbing.
Flint edge – cylindrical frustum, absorbing.
Unlike
Lens, a doublet is two closed solids (the crown element and the flint element) sharing the cemented interface as a common boundary surface – so the edge is split into a crown segment and a flint segment at the cemented interface’s axial position, and each element is validated as its ownVolume.- Variables:
_name – Registry name.
_cs – Front-vertex coordinate system.
_config – DoubletConfig.
_surfaces – Built list of sub-surfaces.
_volumes – The two validated
Volumeinstances (crown, flint).
- property coordinate_system: CoordinateSystem#
Front-vertex coordinate system.
- property surfaces: list[BaseComponent]#
Ordered flat list of sub-surfaces.
- class DoubletConfig(r1: float, r2: float, r3: float, thickness1: float, thickness2: float, material1: str | NSQMaterial, material2: str | NSQMaterial, aperture_radius: float, conic1: float = 0.0, conic2: float = 0.0, conic3: float = 0.0, front: SurfaceConfig | None = None, cemented: SurfaceConfig | None = None, back: SurfaceConfig | None = None, edge: SurfaceConfig | None = None)[source]#
Bases:
objectConfiguration for a cemented achromatic doublet.
Surfaces in order (front -> back): front face, cemented interface, back face, edge.
- Variables:
r1 (float) – Front radius of curvature [mm].
r2 (float) – Cemented interface radius of curvature [mm].
r3 (float) – Back radius of curvature [mm].
thickness1 (float) – Thickness of the crown element [mm].
thickness2 (float) – Thickness of the flint element [mm].
material1 (str | NSQMaterial) – Crown element glass name or NSQMaterial.
material2 (str | NSQMaterial) – Flint element glass name or NSQMaterial.
aperture_radius (float) – Common semi-diameter for all surfaces [mm].
conic1 (float) – Conic constant of the front face.
conic2 (float) – Conic constant of the cemented interface.
conic3 (float) – Conic constant of the back face.
front (SurfaceConfig | None) – Per-surface overrides for the front face.
cemented (SurfaceConfig | None) – Per-surface overrides for the cemented interface.
back (SurfaceConfig | None) – Per-surface overrides for the back face.
edge (SurfaceConfig | None) – Per-surface overrides for the edge surface.
- back: SurfaceConfig | None = None#
- cemented: SurfaceConfig | None = None#
- edge: SurfaceConfig | None = None#
- front: SurfaceConfig | None = None#
- material1: str | NSQMaterial#
- material2: str | NSQMaterial#
- class InteractionType(*values)[source]#
Bases:
EnumOptical interaction type for a single surface.
- Variables:
REFRACTIVE – Surface refracts (and optionally reflects via Fresnel).
REFLECTIVE – Surface reflects only; no transmission.
ABSORBING – Surface absorbs all incident rays.
- ABSORBING = 'absorbing'#
- REFLECTIVE = 'reflective'#
- REFRACTIVE = 'refractive'#
- class Lens(name: str, cs: CoordinateSystem, config: LensConfig)[source]#
Bases:
CompoundComponentSingle refractive lens element.
Assembles up to four physical surfaces:
Front face – refractive, conic.
Back face – refractive, conic.
Edge – cylindrical frustum, absorbing by default.
Rim – annular plane, absorbing; only when
front_aperture_radius != back_aperture_radius.
The built surfaces are validated as a single closed
Volume(watertight, consistently outward-oriented) at construction time – a lens whose faces and edge do not actually close up raisesNonWatertightVolumeErrorimmediately rather than producing silently wrong flux accounting later.- Variables:
_name – Registry name.
_cs – Front-vertex coordinate system.
_config – LensConfig describing the lens geometry.
_surfaces – Built list of sub-surfaces.
_volume – The validated
Volumethese surfaces form.
- property coordinate_system: CoordinateSystem#
Front-vertex coordinate system.
- property surfaces: list[BaseComponent]#
Ordered flat list of sub-surfaces.
- class LensConfig(r1: float, r2: float, thickness: float, material: str | NSQMaterial, front_aperture_radius: float, back_aperture_radius: float | None = None, conic1: float = 0.0, conic2: float = 0.0, front: SurfaceConfig | None = None, back: SurfaceConfig | None = None, edge: SurfaceConfig | None = None, rim: SurfaceConfig | None = None)[source]#
Bases:
objectConfiguration for a single-element refractive lens.
The lens assembles up to four physical surfaces:
Front face – refractive, conic.
Back face – refractive, conic.
Edge – cylindrical frustum, absorbing by default.
Rim – annular plane, absorbing; only created when
front_aperture_radius != back_aperture_radius.
- Variables:
r1 (float) – Front vertex radius of curvature [mm]. Positive = centre of curvature on +z side.
r2 (float) – Back vertex radius of curvature [mm].
thickness (float) – Centre thickness of the lens [mm].
material (str | NSQMaterial) – Glass name (e.g.
'N-BK7') or a ready-madeNSQMaterialinstance.front_aperture_radius (float) – Semi-diameter of the front face [mm].
back_aperture_radius (float | None) – Semi-diameter of the back face [mm]. Defaults to
front_aperture_radiuswhenNone.conic1 (float) – Conic constant of the front face (0 = sphere).
conic2 (float) – Conic constant of the back face (0 = sphere).
front (SurfaceConfig | None) – Per-surface overrides for the front face.
back (SurfaceConfig | None) – Per-surface overrides for the back face.
edge (SurfaceConfig | None) – Per-surface overrides for the edge (barrel) surface.
rim (SurfaceConfig | None) – Per-surface overrides for the rim annulus (only used when apertures differ).
- back: SurfaceConfig | None = None#
- edge: SurfaceConfig | None = None#
- front: SurfaceConfig | None = None#
- material: str | NSQMaterial#
- rim: SurfaceConfig | None = None#
- class Mirror(name: str, cs: CoordinateSystem, config: MirrorConfig)[source]#
Bases:
CompoundComponentSingle reflective mirror surface.
Wraps a
ConicGeometry(or any overridden geometry) inside aReflectiveComponent. The user can attach a custom BSDF viaMirrorConfig.surface.bsdf.Unlike
Lens/Doublet, this is not wrapped in aVolume: a mirror is a single open reflective surface with vacuum on both sides, not a closed solid with an interior medium, so watertightness has nothing to check.- Variables:
_name – Registry name.
_cs – Surface coordinate system.
_config – MirrorConfig.
_surfaces – Single-element list containing the reflective surface.
- property coordinate_system: CoordinateSystem#
Mirror surface coordinate system.
- property surfaces: list[BaseComponent]#
Single-element list containing the reflective surface.
- class MirrorConfig(radius: float, reflectance: object, conic: float = 0.0, aperture_radius: float = 25.0, surface: SurfaceConfig | None = None)[source]#
Bases:
objectConfiguration for a single reflective mirror surface.
- Variables:
radius (float) – Vertex radius of curvature [mm]. Negative = concave when oriented with the normal pointing toward +z.
reflectance (object) – Mirror reflectance: a constant in [0, 1], a wavelength-dependent
callable(wavelength_um) -> reflectance, or an unpolarizedoptiland.coatings.BaseCoating(e.g.SimpleCoating). Required – there is no implicit perfect-mirror default: a mirror built without specifying how much light it reflects is a modelling bug, not a 100% reflector. Overridden per-surface bysurface.reflectance.conic (float) – Conic constant (0 = sphere, -1 = paraboloid, etc.).
aperture_radius (float) – Semi-diameter [mm].
surface (optiland.nonsequential.components.configs.SurfaceConfig | None) – Per-surface overrides (e.g. to attach a scatter BSDF).
- surface: SurfaceConfig | None = None#
- exception NonWatertightVolumeError[source]#
Bases:
ExceptionA Volume’s boundary surfaces do not form a closed, consistently outward-oriented solid.
Raised at
Volumeconstruction, never as a warning: a leak in the boundary lets rays enter or exit a solid without the medium stack (or, in this revamp, the per-surface geometric sidedness check) noticing, which is exactly the class of silent-wrong-answer failure this validation exists to prevent.
- class ReflectiveComponent(cs: CoordinateSystem, geometry: ComponentGeometry, reflectance: float | Callable[[be.ndarray], be.ndarray] | BaseCoating, bsdf: BaseBSDF | None = None, material_front: NSQMaterial = NSQMaterial(optiland_material=None, bsdf=None), name: str = '', scatter_fraction: float = 1.0)[source]#
Bases:
BaseComponentPurely reflective optical element (mirror, baffle).
Reflects rays specularly (or via BSDF). Does not transmit.
- Variables:
cs – Coordinate system.
geometry – Surface geometry.
reflectance – Constant, callable(wavelength_um), or unpolarized BaseCoating giving the fraction of flux reflected.
bsdf – Optional BSDF for scatter. None = specular mirror.
name – Optional label.
- property bounding_box: AABB#
Axis-aligned bounding box in global coordinates.
- Returns:
AABB for this component.
- interact(rays: NSQRayBundle, t: np.ndarray, normals: np.ndarray, hit_mask: np.ndarray, rng: NSQRng, bsdf_ir: BsdfIR, n_geom: np.ndarray, sampling: SamplingPolicy | None = None, forced_branch: str | None = None) None[source]#
Apply specular (or BSDF) reflection at hit points (in-place).
- Parameters:
rays – Ray bundle updated in-place.
t – Hit distances [mm], shape (N,).
normals – Surface normals in global frame, shape (N, 3).
hit_mask – True for rays hitting this component, shape (N,).
rng – Keyed PCG32 RNG. Draws are keyed by this ray’s own id and its bounce count as of this interaction.
bsdf_ir – This surface’s lowered BSDF descriptor. Whether the scatter branch below runs at all is decided from
bsdf_ir.kind != "none"(verified by the caller to matchself.bsdf), not fromself.bsdf is not None.n_geom – Unused – a mirror never transmits, so it never needs to determine which medium a ray is entering.
sampling – Unused – a mirror has no Fresnel reflect/transmit branch to importance-bias; its reflectance is applied as a deterministic flux weight, not a stochastic draw.
forced_branch – Unused – bounded splitting only applies to
RefractiveComponent’s Fresnel branch.
- intersect(rays: NSQRayBundle) tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]#
Find the nearest intersection of alive rays with this component.
Transforms rays to local frame, delegates to geometry, then transforms normals back to global frame.
- Parameters:
rays – The ray bundle in global coordinates.
- Returns:
t: Per-ray distances [mm], shape (N,). inf if no hit.
normals: Surface normals in global frame, shape (N, 3).
hit_mask: Boolean hit mask, shape (N,).
- n_geom: Geometric (unflipped, direction-independent)
surface normal in global frame, shape (N, 3). See
ComponentGeometry.ray_intersect().
- Return type:
Tuple (t, normals, hit_mask, n_geom) in global frame
- class RefractiveComponent(cs: CoordinateSystem, geometry: ComponentGeometry, material_front: NSQMaterial, material_back: NSQMaterial, bsdf: BaseBSDF | None = None, name: str = '', scatter_fraction: float = 1.0, coating: BaseCoating | None = None)[source]#
Bases:
BaseComponentRefractive optical element (lens, prism, window).
At each interface, Fresnel splitting uses the detached-sample / attached-weight scheme: the branch decision (reflect vs transmit) is drawn from a detached probability, while the throughput weight carries the attached reflectance so gradients flow through material parameters.
The two materials name the media on either side of the surface, and the component works out which one a ray is leaving by comparing the ray direction against the surface’s geometric normal (
n_geom, fixed per surface point, pointing frommaterial_fronttowardmaterial_back) – never by comparing refractive index values. Crossing direction therefore does not matter: the same surface refracts correctly for a ray on its way in, for a ghost or retro-reflection coming back through, and for the far side of a closed solid modelled as a single geometry, even when the two adjacent media have nearly identical indices (a cemented doublet, oil immersion).- Variables:
cs – Coordinate system.
geometry – Surface geometry.
material_front – Medium on the front (normal-facing) side.
material_back – Medium on the back side.
bsdf – Optional BSDF for scatter. None = specular.
name – Optional label.
- property bounding_box: AABB#
Axis-aligned bounding box in global coordinates.
- Returns:
AABB for this component.
- interact(rays: NSQRayBundle, t: np.ndarray, normals: np.ndarray, hit_mask: np.ndarray, rng: NSQRng, bsdf_ir: BsdfIR, n_geom: np.ndarray, sampling: SamplingPolicy | None = None, forced_branch: str | None = None) None[source]#
Apply Fresnel refraction/reflection at hit points (in-place).
Uses detached-sample / attached-weight Fresnel: the reflect/transmit branch decision is drawn from a detached probability so stochastic choices do not block gradients; the throughput weight multiplier carries the attached reflectance so ∂flux/∂R is non-zero. When
self.coatingis set, its R/T replace the bare Fresnel values (still forced to R=1/T=0 under TIR, where no coating can restore a transmitted wave).- Parameters:
rays – Ray bundle updated in-place.
t – Hit distances [mm], shape (N,).
normals – Surface normals in global frame, shape (N, 3).
hit_mask – True for rays hitting this component, shape (N,).
rng – Keyed PCG32 RNG (used for detached sampling only). Draws are keyed by this ray’s own id and its bounce count as of this interaction, so they are independent of batch_size, compaction, and every other ray in the bundle.
bsdf_ir – This surface’s lowered BSDF descriptor. Whether the scatter branch below runs at all is decided from
bsdf_ir.kind != "none"(verified by the caller to matchself.bsdf), not fromself.bsdf is not None.n_geom – Geometric surface normal in global frame, shape (N, 3), fixed per surface point and pointing from
material_fronttowardmaterial_back. Used, notrays.n_current, to determine which material a ray is entering.sampling – The scene’s rare-path sampling policy. Resolves the reflect-branch sampling probability – see
optiland.nonsequential.sampling.resolve_reflect_prob().Nonedefaults toreflect_prob="fresnel"(today’s behaviour). Ignored whenforced_branchis set.forced_branch –
"reflect"or"transmit"to deterministically force the branch (weight = R or T exactly, no importance division) instead of drawing it stochastically. Used only by the NumPy forward engine’s bounded-splitting orchestration to build both children of a split ray.
- intersect(rays: NSQRayBundle) tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]#
Find the nearest intersection of alive rays with this component.
Transforms rays to local frame, delegates to geometry, then transforms normals back to global frame.
- Parameters:
rays – The ray bundle in global coordinates.
- Returns:
t: Per-ray distances [mm], shape (N,). inf if no hit.
normals: Surface normals in global frame, shape (N, 3).
hit_mask: Boolean hit mask, shape (N,).
- n_geom: Geometric (unflipped, direction-independent)
surface normal in global frame, shape (N, 3). See
ComponentGeometry.ray_intersect().
- Return type:
Tuple (t, normals, hit_mask, n_geom) in global frame
- class SurfaceConfig(bsdf: BaseBSDF | None = None, scatter_fraction: float = 1.0, coating: object | None = None, aperture_radius: float | None = None, interaction: InteractionType | None = None, reflectance: object | None = None)[source]#
Bases:
objectOptional per-surface overrides within a compound component.
All fields default to
None, meaning “use the compound-level default.” When a field is set, it overrides the compound’s default for that surface.- Variables:
bsdf (BaseBSDF | None) – Custom BSDF for this surface. Routes rays through the scatter model instead of the surface’s specular/refractive behaviour.
scatter_fraction (float) – Probability in [0, 1] that a ray striking this surface is routed through
bsdfrather than following the specular or refractive path. The default of 1.0 sends every ray to the BSDF, turning the surface into a pure diffuser. Set it below 1 to model a partially scattering surface, e.g. 0.1 for a surface that scatters a tenth of the light and transmits the rest. Ignored whenbsdfis None.coating (object | None) – An
optiland.coatings.BaseCoatingfor a refractive surface (e.g. an AR coating). When set, its reflectance/ transmittance replace the bare Fresnel calculation, so NSQ and the sequential engine agree on R. Must be an unpolarized coating (SimpleCoating); aBaseCoatingPolarizedinstance (Jones- matrix based –FresnelCoating,ThinFilmCoating, …) raisesNotImplementedErrorrather than being silently degraded to its scalar average. Ignored on absorbing surfaces.aperture_radius (float | None) – Semi-diameter override [mm]. Overrides the aperture computed from the compound config.
interaction (InteractionType | None) – Force a specific interaction type on this surface.
reflectance (object | None) – Required when
interactionselectsInteractionType.REFLECTIVE: a constant in [0, 1], a wavelength-dependentcallable(wavelength_um) -> reflectance, or an unpolarizedBaseCoating. SeeReflectiveComponent– constructing a reflective surface without one raises rather than defaulting to a perfect mirror.
- interaction: InteractionType | None = None#
- class Volume(name: str, boundary: list[BaseComponent], interior: NSQMaterial, _skip_validation: bool = False)[source]#
Bases:
objectA closed, outward-oriented solid built from boundary surfaces.
Validated at construction: every boundary surface’s rim must be met by a neighbour’s rim (watertightness), and the boundary must enclose its own estimated interior point consistently from every direction (orientation). Both checks raise
NonWatertightVolumeErrorrather than warning – a leaky or misoriented boundary produces silently wrong flux accounting, exactly the failure class this validation exists to catch at construction time instead of at trace time.- Variables:
name (str) – Human-readable label.
boundary (list[BaseComponent]) – Closed, outward-oriented list of boundary surfaces.
interior (NSQMaterial) – The medium inside this volume.
- boundary: list[BaseComponent]#
- static difference(*parts: object) None[source]#
Not implemented: true CSG difference needs a boolean surface evaluator.
- Raises:
NotImplementedError – Always. See
intersection().
- interior: NSQMaterial#
- static intersection(*parts: object) None[source]#
Not implemented: true CSG intersection needs a boolean surface evaluator.
- Raises:
NotImplementedError – Always. Construct the intersected geometry directly with analytic primitives, or use
union()for concatenating already-disjoint boundary surfaces.
- static union(*parts: Volume | list[BaseComponent] | BaseComponent) list[BaseComponent][source]#
Concatenate already-disjoint boundary surfaces into one list.
This is the CSG operation this revamp implements: gluing separately constructed, non-overlapping boundary pieces together (the stated use cases – a lens with a flat, a light pipe with a chamfer) are boundary concatenation, not boolean surface evaluation. The result is not itself validated; pass it to
Volumeto check it.- Parameters:
*parts – Any mix of
Volumeinstances, lists of components, or single components.- Returns:
The concatenated boundary list, in argument order.
Geometry#
Geometry subpackage for NSQ components.
- class AABB(min_corner: numpy.ndarray, max_corner: numpy.ndarray)[source]#
Bases:
objectAxis-aligned bounding box in global coordinates.
- Variables:
min_corner (numpy.ndarray) – Minimum (x, y, z) corner [mm], shape (3,).
max_corner (numpy.ndarray) – Maximum (x, y, z) corner [mm], shape (3,).
- intersects_ray(origins: numpy.ndarray, directions: numpy.ndarray) numpy.ndarray[source]#
Test ray-AABB intersection (slab method).
- Parameters:
origins – Ray origins, shape (N, 3).
directions – Ray directions (unit vectors), shape (N, 3).
- Returns:
Boolean mask of rays that intersect the AABB, shape (N,).
- max_corner: numpy.ndarray#
- min_corner: numpy.ndarray#
- class AnalyticGeometry[source]#
Bases:
ComponentGeometry,ABCABC for analytic geometry primitives.
Analytic geometries implement closed-form intersection formulas, enabling pure-GPU computation with no BVH traversal.
- abstractmethod bounding_box(transform: tuple[numpy.ndarray, numpy.ndarray]) AABB#
Return axis-aligned bounding box in global coordinates.
- Parameters:
transform – Tuple (translation, rotation_matrix) defining the local-to-global transformation. rotation_matrix is (3, 3), transforming column vectors local->global.
- Returns:
AABB in global coordinates.
- abstractmethod ray_intersect(origins: numpy.ndarray, directions: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]#
Find ray intersections with this geometry in local coordinates.
- Parameters:
origins – Ray origins in local frame, shape (N, 3) [mm].
directions – Ray directions in local frame, shape (N, 3), unit vectors.
- Returns:
t: Distance to nearest hit, shape (N,). inf if no hit.
- normals: Hit surface normals in local frame, shape (N, 3).
Normals point toward the ray origin side (outward) – used for shading/reflection/refraction math.
hit_mask: True where ray actually hits, shape (N,) bool.
- n_geom: The same surface normal before the “flip to face
the incoming ray” step, shape (N, 3). Fixed per surface point, independent of which side the ray approached from (D-1, D11 4.7): every geometry orients this so it points from the
material_frontside toward thematerial_backside – the contractRefractiveComponentrelies on to determine which material a ray is entering without comparing refractive index values. Components that never need sidedness (reflective, absorbing) ignore it.
- Return type:
A tuple (t, normals, hit_mask, n_geom) where
- class ComponentGeometry[source]#
Bases:
ABCAbstract base for component geometry.
Subclasses implement ray_intersect() for their specific shape. Geometry operates in the component’s LOCAL coordinate frame.
- abstractmethod bounding_box(transform: tuple[numpy.ndarray, numpy.ndarray]) AABB[source]#
Return axis-aligned bounding box in global coordinates.
- Parameters:
transform – Tuple (translation, rotation_matrix) defining the local-to-global transformation. rotation_matrix is (3, 3), transforming column vectors local->global.
- Returns:
AABB in global coordinates.
- abstractmethod ray_intersect(origins: numpy.ndarray, directions: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray][source]#
Find ray intersections with this geometry in local coordinates.
- Parameters:
origins – Ray origins in local frame, shape (N, 3) [mm].
directions – Ray directions in local frame, shape (N, 3), unit vectors.
- Returns:
t: Distance to nearest hit, shape (N,). inf if no hit.
- normals: Hit surface normals in local frame, shape (N, 3).
Normals point toward the ray origin side (outward) – used for shading/reflection/refraction math.
hit_mask: True where ray actually hits, shape (N,) bool.
- n_geom: The same surface normal before the “flip to face
the incoming ray” step, shape (N, 3). Fixed per surface point, independent of which side the ray approached from (D-1, D11 4.7): every geometry orients this so it points from the
material_frontside toward thematerial_backside – the contractRefractiveComponentrelies on to determine which material a ray is entering without comparing refractive index values. Components that never need sidedness (reflective, absorbing) ignore it.
- Return type:
A tuple (t, normals, hit_mask, n_geom) where
- class ConicGeometry(radius: float, conic: float, aperture_radius: float)[source]#
Bases:
AnalyticGeometryConic section surface (z = f(r)) centred on the local z-axis.
The surface vertex is at the local origin. The aperture is circular with radius aperture_radius.
- Variables:
radius – Radius of curvature at the vertex [mm]. Positive = centre of curvature on +z side.
conic – Conic constant K.
aperture_radius – Semi-aperture radius [mm].
- bounding_box(transform: tuple[numpy.ndarray, numpy.ndarray]) AABB[source]#
Return AABB in global coordinates.
- Parameters:
transform – (translation, rotation_matrix).
- Returns:
AABB in global frame.
- ray_intersect(origins: numpy.ndarray, directions: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray][source]#
Intersect rays with the conic surface.
Solved in closed form: the sag form in the module docstring is algebraically the quadric
c * (x^2 + y^2) + (1 + K) * c * z^2 - 2 * z = 0,
so substituting p(t) = o + t*d gives a quadratic in t. The same equation covers the flat limit (c = 0), where it becomes linear.
The quadric is the whole conic, including points the sag function does not describe (the far side of an ellipsoid, the second branch of a hyperboloid), which
_root_valid()rejects.- Parameters:
origins – Ray origins in local frame, shape (N, 3) [mm].
directions – Ray directions in local frame, shape (N, 3).
- Returns:
(t, normals, hit_mask, n_geom). n_geom points toward local +z (the
material_backside by contract; seeComponentGeometry.ray_intersect()).
- class FinitePlaneGeometry(width: float = 10.0, height: float = 10.0, aperture_radius: float | None = None)[source]#
Bases:
AnalyticGeometryFinite planar surface at z=0 in local coordinates.
Supports rectangular (width x height) or circular (aperture_radius) apertures. If aperture_radius is set, it takes precedence.
- Variables:
width – Rectangular half-width [mm] along local x. Used when aperture_radius is None.
height – Rectangular half-height [mm] along local y. Used when aperture_radius is None.
aperture_radius – Circular aperture radius [mm]. If set, the active region is a disk of this radius.
- bounding_box(transform: tuple[numpy.ndarray, numpy.ndarray]) AABB[source]#
Return AABB for this finite plane in global coordinates.
- Parameters:
transform – (translation, rotation_matrix).
- Returns:
AABB in global frame.
- ray_intersect(origins: numpy.ndarray, directions: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray][source]#
Intersect rays with the finite plane.
- Parameters:
origins – Ray origins in local frame, shape (N, 3).
directions – Ray directions in local frame, shape (N, 3).
- Returns:
(t, normals, hit_mask, n_geom). n_geom is the fixed local +z (the
material_backside by contract; seeComponentGeometry.ray_intersect()).
- class MeshGeometry(mesh: object)[source]#
Bases:
AnalyticGeometryTriangulated surface backed by a trimesh.Trimesh object.
Intersection uses trimesh’s ray caster (pyembree if installed, else the pure-Python fallback). GPU transfer is required on each intersection step for CuPy arrays; analytic geometry is preferred for GPU performance.
- Variables:
mesh – The underlying trimesh.Trimesh object.
- bounding_box(transform: tuple[numpy.ndarray, numpy.ndarray]) AABB[source]#
Return AABB for the mesh in global coordinates.
- Parameters:
transform – (translation, rotation_matrix).
- Returns:
AABB in global frame.
- ray_intersect(origins: numpy.ndarray, directions: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray][source]#
Intersect rays with the mesh using trimesh BVH.
Arrays are converted to NumPy for trimesh, then results are converted back to the original array type (CuPy if needed).
- Parameters:
origins – Ray origins in local frame, shape (N, 3) [mm].
directions – Ray directions in local frame, shape (N, 3).
- Returns:
(t, normals, hit_mask, n_geom). n_geom is trimesh’s raw
face_normalsvalue, i.e. thematerial_backside by contract (seeComponentGeometry.ray_intersect()) is whichever side the mesh’s face winding points away from – consistently outward for a properly wound (CCW, right-hand rule) closed mesh.
- class ParaboloidGeometry(radius: float, aperture_radius: float)[source]#
Bases:
ConicGeometryConvenience subclass for paraboloid (conic constant K = -1).
- Variables:
radius – Radius of curvature [mm].
aperture_radius – Semi-aperture [mm].
- bounding_box(transform: tuple[numpy.ndarray, numpy.ndarray]) AABB#
Return AABB in global coordinates.
- Parameters:
transform – (translation, rotation_matrix).
- Returns:
AABB in global frame.
- ray_intersect(origins: numpy.ndarray, directions: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]#
Intersect rays with the conic surface.
Solved in closed form: the sag form in the module docstring is algebraically the quadric
c * (x^2 + y^2) + (1 + K) * c * z^2 - 2 * z = 0,
so substituting p(t) = o + t*d gives a quadratic in t. The same equation covers the flat limit (c = 0), where it becomes linear.
The quadric is the whole conic, including points the sag function does not describe (the far side of an ellipsoid, the second branch of a hyperboloid), which
_root_valid()rejects.- Parameters:
origins – Ray origins in local frame, shape (N, 3) [mm].
directions – Ray directions in local frame, shape (N, 3).
- Returns:
(t, normals, hit_mask, n_geom). n_geom points toward local +z (the
material_backside by contract; seeComponentGeometry.ray_intersect()).
- class PlaneGeometry[source]#
Bases:
AnalyticGeometryInfinite flat plane at z=0 in local coordinates, normal along +z.
The plane equation in local frame is: z = 0.
- bounding_box(transform: tuple[numpy.ndarray, numpy.ndarray]) AABB[source]#
Return infinite AABB (plane is unbounded).
- Parameters:
transform – (translation, rotation_matrix).
- Returns:
Infinite AABB.
- ray_intersect(origins: numpy.ndarray, directions: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray][source]#
Intersect rays with the infinite plane z=0.
- Parameters:
origins – Ray origins in local frame, shape (N, 3).
directions – Ray directions in local frame, shape (N, 3).
- Returns:
(t, normals, hit_mask, n_geom). n_geom is the fixed local +z (the
material_backside by contract; seeComponentGeometry.ray_intersect()).
- class SphereGeometry(radius: float, aperture_radius: float | None = None)[source]#
Bases:
AnalyticGeometryFull sphere centred at the local origin.
The sphere equation: x^2 + y^2 + z^2 = radius^2.
- Variables:
radius – Sphere radius [mm].
aperture_radius – Optional aperture limit [mm]. Only the part of the sphere within this transverse radius is considered.
- bounding_box(transform: tuple[numpy.ndarray, numpy.ndarray]) AABB[source]#
Return AABB for the sphere in global coordinates.
- Parameters:
transform – (translation, rotation_matrix).
- Returns:
AABB in global frame.
- ray_intersect(origins: numpy.ndarray, directions: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray][source]#
Intersect rays with the sphere.
Uses the analytic quadratic solution. Returns the nearest positive hit.
- Parameters:
origins – Ray origins in local frame, shape (N, 3) [mm].
directions – Ray directions in local frame, shape (N, 3), unit.
- Returns:
(t, normals, hit_mask, n_geom). n_geom points inward, toward the sphere centre – the
material_backside by contract (seeComponentGeometry.ray_intersect()): a standalone sphere used as a refractive surface (e.g. a ball lens) should be built with the exterior medium asmaterial_frontand the interior asmaterial_back, matching the convention every compound builder (Lens,Doublet) already uses.
- class AnnularPlaneGeometry(inner_radius: float, outer_radius: float, z_offset: float = 0.0)[source]#
Bases:
AnalyticGeometryFlat annular ring at
z = z_offsetin the local frame.The annulus extends from
inner_radiustoouter_radiusin the radial direction. Rays that hit the z = z_offset plane inside the annular band register a hit; rays outside or on the inner hole do not.- Variables:
inner_radius – Inner radius of the annulus [mm].
outer_radius – Outer radius of the annulus [mm].
z_offset – Axial position of the plane in local frame [mm].
- bounding_box(transform: tuple[numpy.ndarray, numpy.ndarray]) AABB[source]#
Return AABB of the annulus in global coordinates.
- Parameters:
transform – (translation, rotation_matrix).
- Returns:
AABB in global frame.
- ray_intersect(origins: numpy.ndarray, directions: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray][source]#
Intersect rays with the annular plane.
Uses the standard plane intersection: t = (z_offset - oz) / dz, then checks that the hit radius is within [inner_radius, outer_radius].
- Parameters:
origins – Ray origins in local frame, shape (N, 3) [mm].
directions – Ray directions in local frame, shape (N, 3).
- Returns:
(t, normals, hit_mask, n_geom) all in local frame. n_geom is the fixed local +z (the
material_backside by contract; seeComponentGeometry.ray_intersect()).
- class CylindricalFrustumGeometry(r_front: float, r_back: float, z_front: float, z_back: float)[source]#
Bases:
AnalyticGeometryLateral surface of a truncated cone (frustum) along the local z-axis.
The frustum connects a circle of radius
r_frontatz = z_frontto a circle of radiusr_backatz = z_back. Only the lateral (barrel) surface is modelled – the two end-caps are handled by separate geometry objects (e.g. ConicGeometry for lens faces).- Variables:
r_front – Radius at the front rim [mm].
r_back – Radius at the back rim [mm].
z_front – Axial position of the front rim in local frame [mm].
z_back – Axial position of the back rim in local frame [mm].
- bounding_box(transform: tuple[numpy.ndarray, numpy.ndarray]) AABB[source]#
Return AABB of the frustum in global coordinates.
- Parameters:
transform – (translation, rotation_matrix).
- Returns:
AABB in global frame.
- ray_intersect(origins: numpy.ndarray, directions: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray][source]#
Intersect rays with the frustum lateral surface.
The frustum surface satisfies:
x^2 + y^2 = r(z)^2
where
r(z) = r_front + slope*(z - z_front)andslope = (r_back - r_front)/(z_back - z_front).Substituting the ray parametric equations yields a quadratic in t. Both roots are tested; the smallest positive root within the axial range [z_front, z_back] is returned.
- Parameters:
origins – Ray origins in local frame, shape (N, 3) [mm].
directions – Ray directions in local frame, shape (N, 3).
- Returns:
(t, normals, hit_mask, n_geom) all in local frame. n_geom points radially outward from the frustum axis – this geometry is only ever used for edge/barrel surfaces with the same material on both sides, so it never needs to satisfy the
material_backsidedness contract inComponentGeometry.ray_intersect().
BSDF and scattering#
BSDF subpackage for non-sequential raytracing.
- class BaseBSDF[source]#
Bases:
ABCAbstract base class for bidirectional scattering distribution functions.
All BSDF implementations must support vectorized operation over N rays. Array operations must be compatible with both NumPy and CuPy arrays.
Lobes are explicitly REFLECT or TRANSMIT:
sample()returns, alongside each scattered direction, whether that particular ray’s draw landed in the reflective hemisphere (same side as the incident ray) or the transmissive one (far side). A surface’s own optical topology decides what that means physically – a mirror has no far side to transmit into, soReflectiveComponentignores the flag, whileRefractiveComponentuses it to pick which of its two adjacent media (material_front/material_back) a scattered ray is now in: the medium a scattered ray ends up in is decided by its own lobe choice, never by the independent Fresnel branch draw that only applies to unscattered rays.- abstractmethod reflectance(incident_dirs: np.ndarray, normals: np.ndarray, wavelengths: np.ndarray) np.ndarray[source]#
Total hemispherical reflectance for Russian-roulette decisions.
- Parameters:
incident_dirs – Incident ray directions, shape (N, 3), unit vectors.
normals – Surface normals at hit points, shape (N, 3), unit vectors.
wavelengths – Per-ray wavelengths [nm], shape (N,).
- Returns:
Total reflectance values in [0, 1], shape (N,).
- abstractmethod sample(num_rays: int, incident_dirs: np.ndarray, normals: np.ndarray, wavelengths: np.ndarray, rng: NSQRng, ray_id: np.ndarray, bounce: np.ndarray) tuple[np.ndarray, np.ndarray, np.ndarray][source]#
Sample scattered ray directions, flux weights, and lobe side.
- Parameters:
num_rays – Number of rays to scatter.
incident_dirs – Incident ray directions, shape (N, 3), unit vectors.
normals – Surface normals at hit points, shape (N, 3), unit vectors pointing toward the incoming ray side.
wavelengths – Per-ray wavelengths [nm], shape (N,).
rng – Keyed PCG32 RNG.
ray_id – Per-ray identifiers, shape (N,), for keying the draw.
bounce – Per-ray bounce/step index, shape (N,), for keying the draw.
- Returns:
scattered_dirs: Scattered unit direction vectors, shape (N, 3).
flux_weights: Relative flux weights in [0, 1], shape (N,).
transmitted: Boolean mask, shape (N,). True where the returned direction is on the transmissive (far) side of the surface; False for the reflective (incident) side. A purely reflective BSDF (e.g.
SpecularBRDF) returns all-False.
- Return type:
A tuple (scattered_dirs, flux_weights, transmitted) where
- class HarveyShackBSDF(b0: float, l0: float, s: float, transmissive_fraction: float = 0.0)[source]#
Bases:
BaseBSDFHarvey-Shack / ABg scatter model for surface micro-roughness.
The ABg model is a simplified form of the Harvey-Shack theory:
BSDF(beta - beta0) = b0 / (1 + |beta - beta0| / l0)^s
where beta and beta0 are direction cosines of the scattered and specular directions, b0 is the scatter level at beta=beta0, l0 is the break frequency, and s is the roll-off slope.
- Variables:
b0 – Scatter amplitude at zero angle [sr^-1].
l0 – Break-point spatial frequency (dimensionless direction cosine).
s – Power-law roll-off slope (positive).
transmissive_fraction – Probability in [0, 1] that a given scatter event blurs the undeviated straight-through ray (the transmissive lobe, e.g. a diffuser sheet) instead of the specular reflection. Defaults to 0.0: a purely reflective blur, identical to this class’s behaviour before D-5.
- reflectance(incident_dirs: numpy.ndarray, normals: numpy.ndarray, wavelengths: numpy.ndarray) numpy.ndarray[source]#
Return the fraction of incident power redistributed by the lobe.
The sampler conserves energy (rays keep full flux and are only redirected), so this is 1.0. The ABg scatter level itself is
total_integrated_scatter, which is what ascatter_fractionshould be set to for a physically scaled halo.- Parameters:
incident_dirs – Incident directions, shape (N, 3).
normals – Surface normals, shape (N, 3).
wavelengths – Wavelengths [µm], shape (N,).
- Returns:
Approximate reflectance values, shape (N,).
- sample(num_rays: int, incident_dirs: np.ndarray, normals: np.ndarray, wavelengths: np.ndarray, rng: NSQRng, ray_id: np.ndarray, bounce: np.ndarray) tuple[np.ndarray, np.ndarray, np.ndarray][source]#
Sample scattered directions from the ABg lobe about a reference ray.
The scatter offset is drawn directly from the ABg distribution in direction-cosine space: the radial magnitude
|beta - beta0|comes from a tabulated inverse CDF ofBSDF(beta) * 2 * pi * betaand the azimuth is uniform. Rays keep their full flux, so the surface acts as a mirror (or diffuser sheet) whose reflection (or straight-through transmission) is blurred by the ABg lobe: a polished surface (smalll0) stays near-specular/near-collimated, a rough one spreads.A per-ray draw against
transmissive_fractionpicks the reference ray the lobe is centred on: the specular reflection for a reflective draw, or the undeviated straight-through ray (the incident direction itself, unrefracted) for a transmissive one. Both references are expressed in the same tangent frame aboutnormals, so the existingspec_normal_sign(the reference ray’s own sign againstnormals) places the reconstructed sample on the correct side automatically – no separate branch is needed downstream.To model the physically scaled picture instead, a bright specular beam plus a faint scatter halo, set the surface’s
scatter_fractiontototal_integrated_scatter. Then a TIS fraction of the rays enter the halo and the rest reflect specularly.Sampling the lobe directly matters: drawing from a cosine-weighted hemisphere and correcting with a clipped
BSDF / cosweight (the previous approach) puts essentially every sample where the ABg lobe is negligible, which drove surface throughput to ~1e-7 of the incident flux and made the model behave as a black absorber.Sampling is detached (numpy); weights are plain scalars.
- Parameters:
num_rays – Number of rays.
incident_dirs – Incident directions, shape (N, 3).
normals – Surface normals, shape (N, 3).
wavelengths – Wavelengths [µm], shape (N,).
rng – Keyed PCG32 RNG.
ray_id – Per-ray identifiers, shape (N,).
bounce – Per-ray bounce/step index, shape (N,).
- Returns:
(scattered_dirs, flux_weights, transmitted).
- class LambertianBSDF(reflectance_value: float = 1.0, transmissive_fraction: float = 0.0)[source]#
Bases:
BaseBSDFCosine-weighted Lambertian diffuse scatter.
- Variables:
reflectance_value – Hemispherical diffuse reflectance in [0, 1].
transmissive_fraction – Probability in [0, 1] that a given scatter event samples the transmissive hemisphere (the far side of the surface, e.g. a ground-glass diffuser) instead of the reflective one. Defaults to 0.0: a pure diffuse reflector, identical to this class’s behaviour before D-5.
- reflectance(incident_dirs: numpy.ndarray, normals: numpy.ndarray, wavelengths: numpy.ndarray) numpy.ndarray[source]#
Return total hemispherical reflectance.
- Parameters:
incident_dirs – Incident directions, shape (N, 3).
normals – Surface normals, shape (N, 3).
wavelengths – Wavelengths [µm], shape (N,).
- Returns:
Array of reflectance_value, shape (N,).
- sample(num_rays: int, incident_dirs: np.ndarray, normals: np.ndarray, wavelengths: np.ndarray, rng: NSQRng, ray_id: np.ndarray, bounce: np.ndarray) tuple[np.ndarray, np.ndarray, np.ndarray][source]#
Sample cosine-weighted hemisphere directions around +/- normals.
Uses Malley’s method: sample uniform disk, project to hemisphere. A per-ray draw against
transmissive_fractionpicks whether that hemisphere is centred onnormals(reflective) or-normals(transmissive). Sampling is detached (keyed PCG32); weights are plain scalars.- Parameters:
num_rays – Number of rays.
incident_dirs – Incident directions, shape (N, 3).
normals – Surface normals, shape (N, 3), pointing toward ray side.
wavelengths – Wavelengths [µm], shape (N,).
rng – Keyed PCG32 RNG.
ray_id – Per-ray identifiers, shape (N,).
bounce – Per-ray bounce/step index, shape (N,).
- Returns:
(scattered_dirs, flux_weights, transmitted); flux_weights = reflectance_value for every ray (the lobe redistributes energy within whichever hemisphere it lands in, it does not remove it).
- class SpecularBRDF[source]#
Bases:
BaseBSDFPerfect specular reflector (mirror).
The scattered direction is the specular reflection of the incident ray. Flux weight is always 1.0 (no energy loss at the surface itself).
- reflectance(incident_dirs: np.ndarray, normals: np.ndarray, wavelengths: np.ndarray) np.ndarray[source]#
Return reflectance = 1.0 (perfect mirror).
- Parameters:
incident_dirs – Incident directions, shape (N, 3).
normals – Surface normals, shape (N, 3).
wavelengths – Wavelengths [µm], shape (N,).
- Returns:
Array of ones, shape (N,).
- sample(num_rays: int, incident_dirs: np.ndarray, normals: np.ndarray, wavelengths: np.ndarray, rng: NSQRng | None = None, ray_id: np.ndarray | None = None, bounce: np.ndarray | None = None) tuple[np.ndarray, np.ndarray, np.ndarray][source]#
Sample specular reflection directions.
- Parameters:
num_rays – Number of rays.
incident_dirs – Incident directions, shape (N, 3).
normals – Surface normals, shape (N, 3).
wavelengths – Wavelengths [µm], shape (N,).
rng – Unused for specular BRDF.
ray_id – Unused for specular BRDF.
bounce – Unused for specular BRDF.
- Returns:
(reflected_dirs, ones, all_false) – reflected unit vectors, unit weights, and an all-False transmitted mask (a mirror lobe has no far side).
- class TabulatedBSDF(path: str | Path, transmissive_fraction: float = 0.0)[source]#
Bases:
BaseBSDFBSDF loaded from tabulated data (CSV or Zemax scatter file).
The file must contain columns: theta_i [deg], theta_s [deg], bsdf_value. The BSDF is assumed azimuthally symmetric (phi-independent).
- Variables:
path – Path to the scatter data file.
transmissive_fraction – Probability in [0, 1] that a given scatter event samples the transmissive hemisphere (the far side of the surface) instead of the reflective one. Defaults to 0.0: a purely reflective scatter, identical to this class’s behaviour before D-5. The tabulated data itself is treated as hemisphere-relative (
theta_smeasured from whichever normal the draw lands on), not as a combined BRDF+BTDF table.
- reflectance(incident_dirs: numpy.ndarray, normals: numpy.ndarray, wavelengths: numpy.ndarray) numpy.ndarray[source]#
Approximate total hemispherical reflectance from tabulated data.
- Parameters:
incident_dirs – Incident directions, shape (N, 3).
normals – Surface normals, shape (N, 3).
wavelengths – Wavelengths [µm], shape (N,).
- Returns:
Reflectance values, shape (N,).
- sample(num_rays: int, incident_dirs: np.ndarray, normals: np.ndarray, wavelengths: np.ndarray, rng: NSQRng, ray_id: np.ndarray, bounce: np.ndarray) tuple[np.ndarray, np.ndarray, np.ndarray][source]#
Sample scattered directions from the tabulated BSDF.
Uses importance sampling via Lambertian hemisphere + BSDF weighting. A per-ray draw against
transmissive_fractionpicks whether that hemisphere is centred onnormals(reflective) or-normals(transmissive);theta_i/theta_sare measured from whichever normal the ray’s draw landed on. Sampling is detached (keyed PCG32).- Parameters:
num_rays – Number of rays.
incident_dirs – Incident directions, shape (N, 3).
normals – Surface normals, shape (N, 3).
wavelengths – Wavelengths [µm], shape (N,).
rng – Keyed PCG32 RNG.
ray_id – Per-ray identifiers, shape (N,).
bounce – Per-ray bounce/step index, shape (N,).
- Returns:
(scattered_dirs, flux_weights, transmitted).
Detectors#
Detectors subpackage for Non-Sequential Raytracing.
- class BaseDetector(cs: CoordinateSystem, geometry: ComponentGeometry, name: str = '', absorb: bool = True)[source]#
Bases:
ABCAbstract base class for detectors in the NSQ scene.
Detectors record ray data at a surface. They intersect rays (via their geometry) and accumulate hit data across simulation batches.
Detectors are absorbing by default: a ray that reaches a detector is recorded and then terminated. Setting
absorb=Falserecords the ray but lets it continue unchanged, so a detector can be tilted into a converging beam to sample it mid-system without terminating it. Several detectors may share one scene; among the detectors a ray would still reach, only the nearest one sees it, so stacking absorbing detectors down a beam records the beam at the nearest plane and nothing beyond it – useabsorb=False(or trace one scene per plane) to profile a beam at several planes.- Variables:
cs – Coordinate system defining detector position and orientation.
geometry – Surface geometry that defines the detector area.
name – Optional human-readable label.
absorb – Whether a hit terminates the ray. False => the ray is recorded and passes through unaffected.
- abstractmethod get_result()[source]#
Return the accumulated result object.
- Returns:
A result object (IrradianceMap, FarFieldPattern, etc.).
- intersect(rays: NSQRayBundle) tuple[np.ndarray, np.ndarray, np.ndarray][source]#
Find ray intersections with this detector surface.
- Parameters:
rays – Ray bundle in global coordinates.
- Returns:
Tuple (t, normals, hit_mask) in global frame.
- abstractmethod record(rays: NSQRayBundle, t: np.ndarray, hit_mask: np.ndarray) None[source]#
Accumulate ray data for rays that hit this detector.
- Parameters:
rays – Current ray bundle. Positions have NOT yet been advanced to the hit point; use t to compute hit positions.
t – Hit distances [mm], shape (N,).
hit_mask – Boolean mask of rays hitting this detector, shape (N,).
- class DetectorRegistry[source]#
Bases:
objectNamed registry of NSQ detectors.
- Variables:
_registry – Ordered dict mapping name -> BaseDetector.
- add(name: str, detector: BaseDetector) None[source]#
Add a detector.
- Parameters:
name – Unique identifier.
detector – Detector to register.
- Raises:
KeyError – If a detector with
namealready exists.
- property detectors: list[BaseDetector]#
Ordered list of all registered detectors.
- get(name: str) BaseDetector[source]#
Retrieve a detector by name.
- Parameters:
name – Name of the detector.
- Returns:
The registered detector.
- Raises:
KeyError – If no detector with
nameexists.
- class FarFieldDetector(cs: CoordinateSystem, theta_max_deg: float, num_bins_theta: int, num_bins_phi: int, aperture_radius: float = 1000000.0, name: str = '', absorb: bool = True)[source]#
Bases:
BaseDetectorAccumulates angular flux distribution in the far field.
Records ray directions at the detector surface and bins them into a polar (theta, phi) histogram.
- Variables:
cs – Coordinate system.
theta_max_deg – Maximum polar angle to record [deg].
num_bins_theta – Number of polar angle bins.
num_bins_phi – Number of azimuthal angle bins.
- get_result() FarFieldPattern[source]#
Return the accumulated far-field pattern.
- Returns:
FarFieldPattern with intensity [W/sr].
- intersect(rays: NSQRayBundle) tuple[np.ndarray, np.ndarray, np.ndarray]#
Find ray intersections with this detector surface.
- Parameters:
rays – Ray bundle in global coordinates.
- Returns:
Tuple (t, normals, hit_mask) in global frame.
- record(rays: NSQRayBundle, t: np.ndarray, hit_mask: np.ndarray) None[source]#
Accumulate angular flux from hit rays.
Converts ray directions to (theta, phi) in local detector frame and bins.
- Parameters:
rays – Current ray bundle.
t – Hit distances [mm], shape (N,).
hit_mask – Boolean mask of hitting rays, shape (N,).
- class FarFieldDetectorConfig(num_theta: int = 90, num_phi: int = 360, absorb: bool = True)[source]#
Bases:
objectConfiguration for a FarFieldDetector.
- Variables:
- class IrradianceDetector(cs: CoordinateSystem, width: float, height: float, num_pixels_x: int, num_pixels_y: int, splat: Literal['bilinear', 'gaussian', 'hard'] = 'bilinear', splat_sigma: float = 0.5, name: str = '', absorb: bool = True)[source]#
Bases:
BaseDetector2D irradiance map detector on a planar rectangular surface.
Records flux in a pixel grid with differentiable splatting.
- Variables:
cs – Coordinate system.
width – Detector width [mm].
height – Detector height [mm].
num_pixels_x – Number of pixels along x.
num_pixels_y – Number of pixels along y.
splat – Splatting mode — ‘bilinear’, ‘gaussian’, or ‘hard’.
splat_sigma – Gaussian splat sigma in pixels (used when splat=’gaussian’).
- get_result() IrradianceMap[source]#
Return the accumulated irradiance map.
- Returns:
IrradianceMap with irradiance [W/mm^2] computed from stored flux. The
dataattribute of the returned map is the attached flat flux buffer that supports gradient computation.
- intersect(rays: NSQRayBundle) tuple[np.ndarray, np.ndarray, np.ndarray]#
Find ray intersections with this detector surface.
- Parameters:
rays – Ray bundle in global coordinates.
- Returns:
Tuple (t, normals, hit_mask) in global frame.
- record(rays: NSQRayBundle, t: np.ndarray, hit_mask: np.ndarray) None[source]#
Accumulate flux from hit rays into the pixel grid.
Computes hit positions in local detector frame and accumulates flux using the configured splatting mode.
- Parameters:
rays – Current ray bundle (positions not yet advanced to hit point).
t – Hit distances [mm], shape (N,).
hit_mask – Boolean mask of hitting rays, shape (N,).
- class IrradianceDetectorConfig(width: float, height: float, num_pixels_x: int = 256, num_pixels_y: int = 256, splat: Literal['bilinear', 'gaussian', 'hard'] = 'bilinear', splat_sigma: float = 0.5, absorb: bool = True)[source]#
Bases:
objectConfiguration for an IrradianceDetector.
- Variables:
width (float) – Detector width [mm].
height (float) – Detector height [mm].
num_pixels_x (int) – Number of pixels along x.
num_pixels_y (int) – Number of pixels along y.
splat (Literal['bilinear', 'gaussian', 'hard']) – Splatting mode — ‘bilinear’, ‘gaussian’, or ‘hard’.
splat_sigma (float) – Gaussian splat sigma in pixels (used when splat=’gaussian’).
absorb (bool) – Whether a hit terminates the ray. False makes the detector transmissive: the hit is recorded and the ray continues on its unchanged direction, enabling mid-system beam sampling.
- class RayDatabaseConfig(width: float, height: float, max_rays: int = 0, absorb: bool = True)[source]#
Bases:
objectConfiguration for a RayDatabaseDetector.
- Variables:
- class RayDatabaseDetector(cs: CoordinateSystem, geometry: ComponentGeometry, store_rays: bool = True, max_rays: int | None = None, name: str = '', absorb: bool = True)[source]#
Bases:
BaseDetectorStores individual ray phase-space data at the detector surface.
When store_rays=False, only aggregated flux is stored for maximum throughput. When store_rays=True, full per-ray data is accumulated.
- Variables:
cs – Coordinate system.
geometry – Surface geometry defining the detector extent.
store_rays – If True, store individual ray data. If False, only aggregate flux (faster for GPU runs).
max_rays – Maximum number of rays to store. If set, uses a circular buffer. None means unlimited.
- get_result() RayDatabase[source]#
Return accumulated ray database.
- Returns:
RayDatabase with stored ray phase-space data.
- intersect(rays: NSQRayBundle) tuple[np.ndarray, np.ndarray, np.ndarray]#
Find ray intersections with this detector surface.
- Parameters:
rays – Ray bundle in global coordinates.
- Returns:
Tuple (t, normals, hit_mask) in global frame.
- class SpectralDetector(cs: CoordinateSystem, width: float, height: float, num_pixels_x: int, num_pixels_y: int, wavelength_bins: np.ndarray, splat: Literal['bilinear', 'gaussian', 'hard'] = 'bilinear', splat_sigma: float = 0.5, name: str = '', absorb: bool = True)[source]#
Bases:
BaseDetectorPer-wavelength irradiance detector on a planar rectangular surface.
Records flux in a 3D (x, y, wl) grid.
- Variables:
cs – Coordinate system.
width – Detector width [mm].
height – Detector height [mm].
num_pixels_x – Number of pixels along x.
num_pixels_y – Number of pixels along y.
wavelength_bins – Wavelength bin edges [µm].
splat – Spatial splatting mode – ‘bilinear’, ‘gaussian’, or ‘hard’. Splatting is spatial (x, y) only; the wavelength bin is always hard-assigned.
splat_sigma – Gaussian splat sigma in pixels (used when
splat='gaussian').
- get_result() SpectralResult[source]#
Return accumulated spectral result.
- Returns:
SpectralResult with irradiance [W/mm^2] per pixel per wavelength bin.
- intersect(rays: NSQRayBundle) tuple[np.ndarray, np.ndarray, np.ndarray]#
Find ray intersections with this detector surface.
- Parameters:
rays – Ray bundle in global coordinates.
- Returns:
Tuple (t, normals, hit_mask) in global frame.
- class SpectralDetectorConfig(width: float, height: float, num_pixels_x: int = 256, num_pixels_y: int = 256, wl_min: float = 0.4, wl_max: float = 0.7, num_bins: int = 100, splat: Literal['bilinear', 'gaussian', 'hard'] = 'bilinear', splat_sigma: float = 0.5, absorb: bool = True)[source]#
Bases:
objectConfiguration for a SpectralDetector.
- Variables:
width (float) – Detector width [mm].
height (float) – Detector height [mm].
num_pixels_x (int) – Number of pixels along x.
num_pixels_y (int) – Number of pixels along y.
wl_min (float) – Minimum wavelength for spectral binning [µm].
wl_max (float) – Maximum wavelength for spectral binning [µm].
num_bins (int) – Number of wavelength bins.
splat (Literal['bilinear', 'gaussian', 'hard']) – Spatial (x, y) splatting mode — ‘bilinear’, ‘gaussian’, or ‘hard’. The wavelength bin is always hard-assigned.
splat_sigma (float) – Gaussian splat sigma in pixels (used when
splat='gaussian').absorb (bool) – Whether a hit terminates the ray.
Note
Wavelengths are in micrometres, matching
Spectrumand every other wavelength in Optiland. Visible light spans 0.4-0.7 µm, so a detector spanning the visible iswl_min=0.4, wl_max=0.7.
Results#
Results subpackage for Non-Sequential Raytracing.
- class FarFieldPattern(intensity: numpy.ndarray, theta: numpy.ndarray, phi: numpy.ndarray, total_flux: float, num_rays_hit: int)[source]#
Bases:
objectAngular flux distribution in the far field.
- Variables:
intensity – Intensity [W/sr], shape (n_theta, n_phi).
theta – Polar angle bin centres [deg], shape (n_theta,).
phi – Azimuthal angle bin centres [deg], shape (n_phi,).
total_flux – Total flux recorded [W].
num_rays_hit – Number of rays recorded.
- plot(ax=None, projection: str = 'polar', **kwargs)[source]#
Plot the far-field pattern.
- Parameters:
ax – Optional Matplotlib Axes. If None, a new figure is created.
projection – Plot projection type (‘polar’ or ‘cartesian’).
**kwargs – Additional arguments passed to the plot function.
- Returns:
The Matplotlib Figure object.
- class IrradianceMap(irradiance: numpy.ndarray, x_coords: numpy.ndarray, y_coords: numpy.ndarray, total_flux, num_rays_hit: int, data=None)[source]#
Bases:
object2D irradiance distribution on a planar detector.
- Variables:
data – Flat accumulated flux buffer (be-array, shape ny*nx). This is the differentiable handle; call
.data.backward()to propagate gradients through the detector image.Nonewhen constructed from legacy hard-splatted data.irradiance – Irradiance [W/mm^2], shape (ny, nx), as a NumPy array.
x_coords – Bin centre x-coordinates [mm], shape (nx,).
y_coords – Bin centre y-coordinates [mm], shape (ny,).
total_flux – Total flux recorded [W]. Attached to the active backend’s autograd graph – a torch.Tensor when the underlying data is, so
result.total_flux.backward()propagates a gradient. Usetotal_flux_floatfor printing or any consumer that expects a plain Python float.num_rays_hit – Number of rays recorded on this detector.
- plot(ax=None, **kwargs)[source]#
Plot the irradiance map.
- Parameters:
ax – Optional Matplotlib Axes. If None, a new figure is created.
**kwargs – Additional arguments passed to imshow.
- Returns:
The Matplotlib Figure object.
- save(path: str | Path) None[source]#
Save irradiance map to a .npz file.
- Parameters:
path – Output file path.
- to_numpy() numpy.ndarray[source]#
Return the irradiance array as a NumPy array.
- Returns:
The irradiance array, shape (ny, nx).
- class RayDatabase(x: numpy.ndarray, y: numpy.ndarray, z: numpy.ndarray, L: numpy.ndarray, M: numpy.ndarray, N: numpy.ndarray, flux: numpy.ndarray, wavelength: numpy.ndarray)[source]#
Bases:
objectPhase-space record of individual rays at a detector surface.
- Variables:
x – Ray positions x [mm].
y – Ray positions y [mm].
z – Ray positions z [mm].
L – Ray direction cosine x (unit vector).
M – Ray direction cosine y.
N – Ray direction cosine z.
flux – Per-ray flux [W].
wavelength – Per-ray wavelength [µm].
- save(path: str | Path) None[source]#
Save ray database to a .npz file.
- Parameters:
path – Output file path.
- to_dataframe()[source]#
Return ray data as a pandas DataFrame.
- Returns:
DataFrame with columns x, y, z, L, M, N, flux, wavelength.
- Raises:
ImportError – If pandas is not installed.
- class SpectralResult(irradiance: numpy.ndarray, x_coords: numpy.ndarray, y_coords: numpy.ndarray, wavelengths: numpy.ndarray, total_flux: float, num_rays_hit: int)[source]#
Bases:
objectPer-wavelength irradiance on a planar detector.
- Variables:
irradiance – Irradiance per wavelength bin [W/mm^2], shape (ny, nx, n_lambda). Flux is binned, not divided by bin width, so summing over the last axis gives the broadband irradiance.
x_coords – Bin centre x-coordinates [mm].
y_coords – Bin centre y-coordinates [mm].
wavelengths – Wavelength bin centres [µm].
total_flux – Total flux recorded [W].
num_rays_hit – Number of rays recorded.
Diagnostics#
Self-diagnosing simulation results.
SimulationResult.diagnostics turns several silent failure modes the NSQ
engine could previously produce – a scene that depth-truncates most of its
flux, a detector whose map is pure shot noise, a surface no ray ever
touches, roulette eating an unexpected fraction of the beam – into an
explicit, inspectable object with a threshold-based warning list, rather
than numbers a user has to know to go looking for.
Kramer Harrison, 2026
- class DetectorDiagnostic(name: str, num_rays_hit: int, num_pixels: int | None, mean_hits_per_pixel: float | None, undersampled: bool, rays_needed_for_5pct: int | None)[source]#
Bases:
objectPer-detector sampling-quality diagnostic.
- Variables:
name (str) – Detector’s registry name.
num_rays_hit (int) – Rays recorded on this detector.
num_pixels (int | None) – Pixel/bin count of the detector’s map, or
Nonefor detector kinds with no grid (e.g.RayDatabaseDetector).mean_hits_per_pixel (float | None) –
num_rays_hit / num_pixels, orNonewhennum_pixelsisNone.undersampled (bool) – True when
mean_hits_per_pixelis below_UNDERSAMPLED_HITS_PER_PIXEL– shot noise dominates the map. Always False whennum_pixelsisNone(nothing to under-sample against).rays_needed_for_5pct (int | None) – Estimated total ray count (scaled from this trace’s
num_rays_total) that would bring this detector to ~5% relative Poisson error, assuming ray count scales linearly with hits (true when flux/geometry are unchanged).Nonewhennum_pixelsisNoneor no rays hit at all.
- class Diagnostics(depth_truncated_flux_fraction: float = 0.0, rr_killed_flux_fraction: float = 0.0, flux_conservation_error: float = 0.0, unreached_geometry: tuple[str, ...] = (), detectors: tuple[~optiland.nonsequential.diagnostics.DetectorDiagnostic, ...] = <factory>, medium_stack_underflows: int = 0, split_budget_saturated: bool = False)[source]#
Bases:
objectSelf-diagnosing summary of one trace.
Every field is computed during the trace at negligible extra cost (a few running counters and one pass over
scene.detectorsat the end) – this is diagnosis, not a second simulation.- Variables:
depth_truncated_flux_fraction (float) – Fraction of
total_flux_inkilled by the hardmax_depthcutoff. This is the one loss mechanism that is an inherent, reported bias – not something roulette or importance sampling can fix – so a nonzero value is a direct signal to raisemax_depthif the deep paths matter.rr_killed_flux_fraction (float) – Fraction of
total_flux_inkilled by Russian roulette, including bounded-splitting budget culling. Unbiased in expectation; large values mean the per-trace estimator is noisy, not necessarily wrong.flux_conservation_error (float) – Copied from
flux_conservation_errorfor convenience – see that field’s docstring.unreached_geometry (tuple[str, ...]) – Names of scene components no ray ever hit (nearest-hit or otherwise) over the whole trace. Usually a misplaced or mis-oriented surface, but can be a deliberately unused spare aperture – reported, not assumed to be a bug.
detectors (tuple[optiland.nonsequential.diagnostics.DetectorDiagnostic, ...]) – Per-detector sampling-quality diagnostics, in
scene.detectorsorder.medium_stack_underflows (int) – Total pop-on-empty-stack events across all rays this trace (see
NSQRayBundle.medium_stack/RefractiveComponent.interact). This is a diagnostic cross-check layered on top of the geometricn_geomsidedness resolution – it never affects the physics (n1/n2 are always resolved geometrically, never from the stack) – so a nonzero value flags a likely geometry defect (a volume boundary surface reused inconsistently, or two separately constructedNSQMaterialinstances standing in for what should be one physical medium) without itself changing any traced result.split_budget_saturated (bool) – True if bounded splitting ever hit its
split_budgetcap during this trace, meaning some spawned ghost-path rays were roulette-terminated rather than all being kept. NumPy backend only; always False otherwise.
- detectors: tuple[DetectorDiagnostic, ...]#
- build_diagnostics(scene: NSQScene, hit_component_ids: set[int], num_rays_total: int, total_flux_in: float, total_flux_depth_killed: float, total_flux_rr_killed: float, flux_conservation_error: float, split_budget_saturated: bool, detector_results: dict[str, object], medium_stack_underflows: int = 0) Diagnostics[source]#
Assemble a
Diagnosticsfrom one trace’s bookkeeping.Shared by both reference backends so the diagnostic definitions cannot drift between them.
- Parameters:
scene – The traced scene.
hit_component_ids – Indices into
scene.surfacesthat were the nearest hit for at least one ray at least once.num_rays_total – Total rays launched this trace.
total_flux_in – Total launched flux [W].
total_flux_depth_killed – Flux killed by the hard
max_depthcutoff.total_flux_rr_killed – Flux killed by Russian roulette (including split-budget culling).
flux_conservation_error – The trace’s flux-ledger closure error.
split_budget_saturated – Whether bounded splitting ever hit its cap.
detector_results –
{name: get_result()}for every detector, inscene.detectorsorder.medium_stack_underflows – Total medium-stack pop-on-empty events across the trace (see
Diagnostics).
- Returns:
The assembled diagnostics.
Photometric units#
Photometric conversion layer.
The NSQ radiometric core is unchanged: everything inside the trace stays in watts, W/mm^2, and micrometres. This module is a read-only conversion layer on top of finished results (and, for sources, an input-side lumens-to-watts helper) – it never touches the trace loop.
Guardrail : converting a monochromatic result outside the visible band, or a spectrum with negligible V(lambda) overlap, raises rather than returning a near-zero photometric value that looks like a valid (if dim) answer. Silently returning ~0 is the same defect class as D-2 (an accepted configuration that quietly does nothing).
Kramer Harrison, 2026
- class PhotometricMap(data: numpy.ndarray, x_coords: numpy.ndarray, y_coords: numpy.ndarray, total: float, quantity: str, weighting: Literal['photopic', 'scotopic'])[source]#
Bases:
objectA 2D photometric map – illuminance [lux] or luminance-equivalent.
Mirrors
IrradianceMap’s shape so it can be plotted the same way, but the values and units are photometric, not radiometric.- Variables:
data – Photometric map, shape (ny, nx). Lux for
quantity= "illuminance".x_coords – Bin centre x-coordinates [mm], shape (nx,).
y_coords – Bin centre y-coordinates [mm], shape (ny,).
total – Total luminous flux [lm] over the whole map.
quantity – The photometric quantity computed (
"illuminance").weighting –
"photopic"or"scotopic".
- class PhotometricScalar(value: float, quantity: str, weighting: Literal['photopic', 'scotopic'])[source]#
Bases:
objectA single photometric quantity (e.g. total luminous flux).
- Variables:
value – The photometric value.
quantity –
"luminous_flux"(lumens).weighting –
"photopic"or"scotopic".
- lumens_to_watts(total_flux_lm: float, spectrum: Spectrum, weighting: Weighting = 'photopic') float[source]#
Convert a source’s lumens to the radiometric watts NSQ traces in.
- Parameters:
total_flux_lm – Source output in lumens.
spectrum – The source’s
Spectrum– its shape (not absolute scale) determines the conversion.weighting –
"photopic"(default) or"scotopic".
- Returns:
Radiant flux [W] such that a source emitting this many watts, with this spectral shape, emits
total_flux_lmlumens.- Raises:
ValueError – If the spectrum has negligible overlap with the chosen V(lambda) curve (guardrail) – e.g. a monochromatic 1.5 um source has no photopic lumen equivalent worth reporting.
- luminous_efficacy_of_spectrum(wavelengths_um: numpy.ndarray, weights: numpy.ndarray, weighting: Literal['photopic', 'scotopic'] = 'photopic') float[source]#
Luminous efficacy [lm/W] of a normalised spectral power distribution.
- Parameters:
wavelengths_um – Wavelength samples [um].
weights – Relative spectral power at each wavelength (need not be normalised; only the shape matters).
weighting –
"photopic"or"scotopic".
- Returns:
Km * sum(weights * V(wavelengths)) / sum(weights)– the spectrum-averaged luminous efficacy, in [0, Km].
- to_photometric(result: IrradianceMap | SpectralResult | FarFieldPattern, quantity: Literal['illuminance', 'luminous_flux'] = 'illuminance', weighting: Weighting = 'photopic', wavelength_um: float | None = None) PhotometricMap | PhotometricScalar[source]#
Convert a detector result to a photometric quantity.
The radiometric result is unaffected; this returns a new object.
- Parameters:
result – An
IrradianceMap,SpectralResult, orFarFieldPattern(fromSimulationResult.detectors[name]).quantity –
"illuminance"(lux, per-pixel map) or"luminous_flux"(total lumens, scalar).weighting –
"photopic"(default, CIE 1931) or"scotopic"(CIE 1951).wavelength_um – Required when
resultcarries no per-wavelength breakdown of its own (i.e. anything butSpectralResult) – the monochromatic wavelength to weight by.
- Returns:
A
PhotometricMapforquantity="illuminance", or aPhotometricScalarforquantity="luminous_flux".- Raises:
ValueError – If the result’s spectral content – explicit
wavelength_umor the result’s own wavelength bins – has negligible overlap with the chosen V(lambda) curve (guardrail: this would otherwise silently return ~0).TypeError – If
resulthas neither a pixel grid nor per -wavelength data (e.g. aRayDatabase), orquantityis not recognised.
- v_lambda(wavelength_um: float | np.ndarray, weighting: Weighting = 'photopic')[source]#
Evaluate V(lambda) (or V’(lambda)) at one or more wavelengths.
- Parameters:
wavelength_um – Wavelength(s) [um].
weighting –
"photopic"(default) or"scotopic".
- Returns:
The luminous efficiency function value(s), 0 outside the tabulated visible band – correct physics (the eye has no response there), not a missing-data placeholder. Callers that must distinguish “genuinely zero” from “out of band” should check
VISIBLE_BAND_UMthemselves;to_photometric()andlumens_to_watts()do this via the negligible-overlap guardrail.
Materials#
NSQ Material subpackage.
- class NSQMaterial(optiland_material: BaseMaterial | None = None, bsdf: BaseBSDF | None = None)[source]#
Bases:
objectThin differentiable adapter over optiland.materials.BaseMaterial.
Evaluates
n(wavelength_um)without any grad-severing casts (nofloat(), nonp.asarray()) so the result stays in the autograd graph when using the Torch backend.- Variables:
optiland_material (BaseMaterial | None) – Underlying material model. None means vacuum (n=1).
bsdf (BaseBSDF | None) – Optional surface scatter model.
- classmethod from_glass(name: str) NSQMaterial[source]#
Resolve a glass catalog name to an NSQMaterial.
- Parameters:
name – Glass catalog name (e.g.
'N-BK7','SF11').- Returns:
NSQMaterial wrapping the resolved BaseMaterial.
- Raises:
ValueError – If the glass name is not found in the catalog.
- k(wavelength_um: WavelengthInput) WavelengthInput[source]#
Extinction coefficient at the given wavelength(s).
Feeds Beer-Lambert bulk absorption:
alpha = 4*pi*k/wavelength_um[1/um], matchingoptiland.propagation.homogeneous .HomogeneousPropagationso NSQ and the sequential engine attenuate a glass path by the same amount.- Parameters:
wavelength_um – Wavelength(s) in micrometres [µm].
- Returns:
Extinction coefficient (dimensionless). Returns
0.0(scalar) for vacuum when input is a scalar, or a zeros-like array/tensor matching the input shape for array inputs.
- n(wavelength_um: WavelengthInput) WavelengthInput[source]#
Refractive index at the given wavelength(s).
Differentiable: when
wavelength_umis a torch Tensor withrequires_grad=True, the returned value carries attached gradients. Nofloat()ornp.asarray()casts are applied to the result.- Parameters:
wavelength_um – Wavelength(s) in micrometres [µm]. Accepts Python float, NumPy ndarray, or torch Tensor.
- Returns:
Refractive index with the same array type as the input. Returns
1.0(scalar) for vacuum when input is a scalar, or a ones-like array/tensor matching the input shape for array inputs.
Backends#
Backends subpackage for Non-Sequential Raytracing.
- class NumpyBackend(seed: int | None = None)[source]#
Bases:
ArrayBackendCPU backend using NumPy for all array operations.
This is the default fallback backend. All ray data remains in host (CPU) memory throughout the simulation. The full Monte Carlo trace loop lives in ArrayBackend; this class provides the NumPy-specific
intersect_scene()and RNG.- Variables:
rng – Keyed PCG32 RNG (see
optiland.nonsequential.rng).seed – RNG seed stored for internal use.
- intersect_scene(rays: NSQRayBundle, components: list[BaseComponent]) tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray][source]#
Find nearest intersection of each ray with all scene components.
- Parameters:
rays – Current ray bundle (NumPy arrays).
components – List of scene components.
- Returns:
(t_min, hit_normals, component_indices, hit_n_geom).
- trace(scene: NSQScene, num_rays: int, max_depth: int = 16, min_flux_fraction: float = 1e-06, batch_size: int = 16384, seed: int | None = None, record_paths: bool | int = False) SimulationResult#
Run the full Monte Carlo simulation.
- Parameters:
scene – The NSQScene to simulate.
num_rays – Total rays to launch.
max_depth – Maximum surface hits per ray.
min_flux_fraction – Russian-roulette threshold, relative to per-ray initial flux – combined with the scene’s
sampling_policy.rr_start_flux(the larger of the two wins). Below threshold, rays are killed with an unbiased probability and survivors’ flux is boosted accordingly, rather than truncated outright.batch_size – Rays per processing batch. Does not change the result, only the speed; see
DEFAULT_BATCH_SIZE.seed – RNG seed for reproducibility.
record_paths –
False(default) records nothing.Truerecords every ray’s full path – fine for small traces, but O(rays x bounces) memory for large ones. A positiveintrecords an approximately that-many-ray subset, selected by a PCG32 hash ofray_idso the trace stays full-size and cheap while a bounded, deterministic sample is available for visualization/diagnosis – e.g.scene.trace(num_rays=10_000_000, record_paths=1_000).
- Returns:
SimulationResult.
- class TorchBackend(seed: int | None = None, gradient_mode: Literal['autograd'] = 'autograd')[source]#
Bases:
TracerBackendDifferentiable PyTorch backend for NSQ raytracing.
Uses
optiland.backend(configured to torch) for all computation. The fixed-depth wavefront loop lets PyTorch build an autograd graph through the entire trace so thatresult.detectors[name].data.backward()propagates gradients to scene parameters.Compaction is disabled: dead rays (
alive=False) carry zero throughput and participate in all operations as no-ops; the tensor shape stays fixed across bounces so the graph remains clean.Gradient strategy is “autograd” (naive attached graph) in v1. A pluggable
gradient_modeseam is provided for future Path Replay Backpropagation.- Variables:
seed – RNG seed.
gradient_mode – Gradient strategy (currently only “autograd”).
rng – Keyed PCG32 RNG for detached sampling decisions (see
optiland.nonsequential.rng).
- intersect_scene(rays: NSQRayBundle, components: list[BaseComponent]) tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray][source]#
Find nearest intersection of each ray with all scene components.
t_min and hit_normals stay in the torch graph (attached to geometry parameters). comp_indices are numpy ints (no grad needed). n_geom is purely geometric (never a function of a differentiable material parameter), but is still built via be.where to stay consistent with the rest of this method and to support a differentiable geometry (radius, conic, …) tilting n_geom itself.
- Parameters:
rays – Current ray bundle.
components – List of scene components.
- Returns:
(t_min, hit_normals, component_indices, hit_n_geom).
- trace(scene: NSQScene, num_rays: int, max_depth: int = 16, min_flux_fraction: float = 1e-06, batch_size: int = 16384, seed: int | None = None, record_paths: bool | int = False) SimulationResult[source]#
Run the differentiable fixed-depth trace.
- Parameters:
scene – NSQScene to simulate.
num_rays – Total rays to launch.
max_depth – Fixed number of bounces. Rays exceeding this are depth-killed. Memory scales O(num_rays × max_depth).
min_flux_fraction – Russian-roulette threshold, relative to per-ray initial flux – combined with the scene’s
sampling_policy.rr_start_flux(the larger of the two wins). Below threshold, rays are killed with an unbiased probability and survivors’ flux is boosted accordingly, rather than truncated outright.batch_size – Rays per processing batch (forward pass only). Does not change the result, only the speed; see
DEFAULT_BATCH_SIZE.seed – RNG seed override (overrides constructor seed if provided).
record_paths –
Falserecords nothing,Truerecords every ray’s path (numpy, detached), and a positiveintrecords an approximately that-many-ray subset selected deterministically byray_idhash – seeoptiland.nonsequential.path_recording.
- Returns:
SimulationResult with differentiable detector
datatensors.
- class TracerBackend[source]#
Bases:
ABCAbstract backend for the NSQ Monte Carlo trace loop.
The full simulation loop – ray generation, intersection, interaction, detection – is delegated to the backend implementation.
NumpyBackendprovides the default CPU implementation. A futureOptiXBackendfromoptiland-rtwould replace the entire loop with NVIDIA OptiX kernel dispatch.New backends implement
trace()and are passed toNSQScene.trace(backend=...)orNSQTracer.trace(backend=...).- abstractmethod trace(scene: NSQScene, num_rays: int, max_depth: int = 16, min_flux_fraction: float = 1e-06, batch_size: int = 16384, seed: int | None = None, record_paths: bool | int = False) SimulationResult[source]#
Run the full simulation and return results.
- Parameters:
scene – The NSQScene to trace (provides flat surface/source/detector lists via
surfaces,sources,detectors).num_rays – Total number of rays to launch.
max_depth – Maximum surface interactions per ray before termination.
min_flux_fraction – Rays whose flux drops below
min_flux_fraction * (total_flux / num_rays)are killed.batch_size – Number of rays per processing batch. Does not change the result, only the speed; see
DEFAULT_BATCH_SIZE.seed – RNG seed for reproducibility.
record_paths –
Falserecords nothing,Truerecords every ray’s full phase-space path bounce-by-bounce, and a positiveintrecords an approximately that-many-ray subset selected deterministically byray_idhash – seeoptiland.nonsequential.path_recording.
- Returns:
SimulationResultwith per-detector results and global statistics.
Scene IR, sampling policy, and RNG#
The declarative, backend-portable scene description (see the developer guide), the rare-path sampling policy, and the counter-based PCG32 RNG.
Scene IR – the backend-portable, data-only description of an NSQ scene.
optiland.nonsequential.ir is the single change that is hardest to
retrofit later: a scene becomes describable as plain data
(SceneIR) rather than as live Python objects with
methods. lower() converts a live
NSQScene into that data form.
This is what makes a third-party backend (Mitsuba 3, OptiX) possible without
reworking the NumPy/Torch reference engines again: any backend that can
interpret a SceneIR can trace the scene.
optiland.nonsequential.backends builds a fresh SceneIR at the
start of every trace() call and drives its per-bounce interaction
dispatch from it (see interpreter) rather than branching on the
Python class of each scene.surfaces entry – the reference
interpreter of this data, not the scene’s source of truth.
Translatability checklist#
Every physics feature added to NSQ from here on must satisfy these rules:
No Python-side per-hit state. All interaction behaviour is expressible as
(BsdfIR, MediumIR)data, not as a callback.Every parameter is a backend array or a plain scalar – never a closure, never an object with behaviour.
No unbounded loops in the interaction path; iteration counts are compile-time constants or scene-level parameters.
Transforms are
(4, 4)matrices, notCoordinateSystemobjects (which carry parent-chain behaviour).Everything in the IR is serializable to JSON without loss.
Kramer Harrison, 2026
- class BsdfIR(kind: ~typing.Literal['none', 'specular', 'lambertian', 'harvey_shack', 'tabulated'], params: dict[str, ~typing.Any] = <factory>)[source]#
Bases:
objectA surface scatter model, as plain data.
paramsis kind-specific and mirrors the correspondingBaseBSDFsubclass’s constructor arguments exactly, so lowering is a direct field copy with no interpretation:"none":{}"specular":{}(always reflective; no transmissive lobe)"lambertian":{"reflectance_value": <float>, "transmissive_fraction": <float>}"harvey_shack":{"b0": <float>, "l0": <float>, "s": <float>, "transmissive_fraction": <float>}"tabulated":{"path": <str>, "transmissive_fraction": <float>}
transmissive_fractionis the probability that a given scatter event samples the transmissive (far-side) hemisphere instead of the reflective one; it defaults to 0.0 on every kind that has it, so an un-set BSDF scatters exactly as it did before D-5.- Variables:
- class EmitterIR(id: int, kind: Literal['point', 'collimated', 'extended'], to_world: numpy.ndarray, params: dict[str, Any], medium_id: int | None, name: str = '')[source]#
Bases:
objectA ray source, as plain data.
- Variables:
id (int) – Index into
SceneIR.emitters.kind (Literal['point', 'collimated', 'extended']) – Source family;
paramsis interpreted according to it.to_world (numpy.ndarray) –
(4, 4)homogeneous local -> global transform.params (dict[str, Any]) – Kind-specific parameters, including
total_fluxand thespectrumdict ({"wavelengths": [...], "weights": [...]}).medium_id (int | None) – Index into
SceneIR.mediafor the medium the source is embedded in, orNonefor vacuum.name (str) – Human-readable label.
- to_world: numpy.ndarray#
- class MediumIR(id: int, name: str, n_model: dict[str, Any], k_model: dict[str, Any] | None = None)[source]#
Bases:
objectA single optical medium, as plain data.
n_model/k_modeldescribe how to evaluate dispersion/absorption without embedding a live evaluator: a dict tagged by"kind"rather than a bound method, so the description survives a JSON round-trip and a non-Python backend can interpret it. Both use the same three kinds and are always populated together bylower():{"kind": "constant", "n": <float>}/{"kind": "constant", "k": <float>}– wavelength-independent (vacuum isn=1.0,k=0.0).{"kind": "catalog", "name": <str>}– a glass catalog name (e.g."N-BK7"), resolved viaNSQMaterial.from_glass()the same wayoptiland.nonsequential.serializationalready round-trips materials.{"kind": "opaque"}– a non-catalog material kept only for dispatch (lower(scene, strict=False)); not losslessly serializable.
A custom dispersion model with no catalog name cannot be lowered losslessly and raises at lowering time (see
lower()) rather than silently dropping to a constant approximation.Note that
k_modelhere is descriptive only, matching the existingn_modelprecedent: the interpreter’s Beer-Lambert absorption (D-13, seeoptiland.nonsequential.backends.array_backend.ArrayBackend.trace) readsk(wavelength)from the liveNSQMaterialonrays.k_current, never from this IR.- Variables:
- class PrimitiveIR(id: int, kind: Literal['conic', 'plane', 'annulus', 'frustum', 'sphere', 'mesh'], to_world: numpy.ndarray, params: dict[str, Any], bsdf: BsdfIR, interior_medium_id: int, exterior_medium_id: int, volume_id: int | None, component_kind: Literal['refractive', 'reflective', 'absorbing'], scatter_fraction: float, name: str = '')[source]#
Bases:
objectOne surface, as plain data.
- Variables:
id (int) – Index into
SceneIR.primitives; also referenced byinterior_medium_id/exterior_medium_idof neighbouring volumes onceVolumeobjects are wired into the IR from these ids (Volumeitself already exists incomponents/volume.py; the IR-level wiring does not yet).kind (Literal['conic', 'plane', 'annulus', 'frustum', 'sphere', 'mesh']) – Geometry family;
paramsis interpreted according to it.to_world (numpy.ndarray) –
(4, 4)homogeneous local -> global transform.params (dict[str, Any]) – Kind-specific geometry parameters, e.g. for
"conic":{"radius": ..., "conic": ..., "aperture_radius": ...}. Values may be plain floats or backend arrays (a differentiabletorch.Tensorstays attached; the translatability checklist only forbids closures and stateful objects, not autograd-carrying arrays).bsdf (optiland.nonsequential.ir.bsdf_ir.BsdfIR) – Attached scatter model, if any (
BsdfIR(kind="none")when the surface is bare specular/refractive/absorbing).interior_medium_id (int) – Index into
SceneIR.mediafor the medium this surface bounds on its back side. Descriptive metadata only: the authoritative sidedness determination lives in each geometry’sn_geom(seeComponentGeometry.ray_intersectandRefractiveComponent.interact), not in these ids – the interpreter still reads the medium directly off the live component. No volume topology is wired through the IR yet.exterior_medium_id (int) – As above, for the front side.
volume_id (int | None) – Index into
SceneIR.volumes, orNonewhen this primitive is not (yet) a volume boundary. AlwaysNoneuntil volumes are wired into the IR.component_kind (Literal['refractive', 'reflective', 'absorbing']) – Which physical interaction this primitive’s hit dispatches to (see
ComponentKind).scatter_fraction (float) – Probability that a hit ray is routed through
bsdfrather than the specular/refractive path.name (str) – Human-readable label, for diagnostics.
- to_world: numpy.ndarray#
- class RngContract(algorithm: str = 'pcg32', version: int = 1)[source]#
Bases:
objectWhich RNG algorithm the scene’s random draws are contracted to.
Not a trace seed –
trace(seed=...)stays a per-call argument. This documents the algorithm every conforming backend must implement: PCG32, keyed by(seed, ray_id, bounce, event_slot). Seeoptiland.nonsequential.rng.- Variables:
algorithm (str) – RNG algorithm identifier.
version (int) – Key-layout version, bumped if the
(seed, ray_id, bounce, event_slot)mixing scheme inoptiland.nonsequential.rngever changes incompatibly.
- class SamplingPolicy(reflect_prob: float | Literal['fresnel', 'auto'] = 'fresnel', split_depth: int = 0, split_budget: float = 4.0, rr_start_flux: float = 0.001)[source]#
Bases:
objectRare-path sampling policy.
Every default reproduces the engine’s pre-PR11 forward behaviour exactly:
reflect_prob="fresnel"is the unconditional Fresnel-probability branch that always existed, andsplit_depth=0means “never split,” which was the only mode that existed before PR11. Set on a scene viaNSQScene.sampling_policy.- Variables:
reflect_prob (float | Literal['fresnel', 'auto']) – Importance-sampling probability for the reflect branch (see
optiland.nonsequential.sampling.resolve_reflect_prob())."fresnel"uses the Fresnel reflectance itself (today’s behaviour);"auto"clamps it into[0.25, 0.75]; an explicit float fixes the probability. Works on both backends and under autograd – the branch decision is always drawn from a detached probability with a compensating attached weight, so only the variance changes, never the expectation.split_depth (int) – NumPy forward engine only; bounded bounce-splitting depth (see
optiland.nonsequential.backends.array_backend).0= never split (the only mode the Torch backend supports – it forcessplit_depth=0and warns if the scene sets a nonzero value, since fixed tensor shapes are required for the autograd graph).split_budget (float) – Cap on live rays during splitting, as a multiple of
batch_size. Unused whilesplit_depth=0. Rays spawned beyond the cap are Russian-rouletted, not dropped.rr_start_flux (float) – Russian-roulette threshold, as a fraction of per-ray initial flux (see
optiland.nonsequential.sampling.russian_roulette()). Replaces the old biased hard kill belowmin_fluxon both backends.
- class SceneIR(primitives: tuple[~optiland.nonsequential.ir.scene_ir.PrimitiveIR, ...], volumes: tuple[~optiland.nonsequential.ir.scene_ir.VolumeIR, ...], media: tuple[~optiland.nonsequential.ir.medium_ir.MediumIR, ...], emitters: tuple[~optiland.nonsequential.ir.scene_ir.EmitterIR, ...], sensors: tuple[~optiland.nonsequential.ir.scene_ir.SensorIR, ...], rng: ~optiland.nonsequential.ir.scene_ir.RngContract = <factory>, sampling: ~optiland.nonsequential.ir.scene_ir.SamplingPolicy = <factory>)[source]#
Bases:
objectThe complete, backend-portable scene description.
- Variables:
primitives (tuple[optiland.nonsequential.ir.scene_ir.PrimitiveIR, ...]) – All surfaces (from every compound component’s flat
.surfaceslist), inscene.surfacesorder.volumes (tuple[optiland.nonsequential.ir.scene_ir.VolumeIR, ...]) – Always
()(seeVolumeIR).media (tuple[optiland.nonsequential.ir.medium_ir.MediumIR, ...]) – Every distinct medium referenced by a primitive or emitter, deduplicated by catalog name (or the single shared vacuum entry).
emitters (tuple[optiland.nonsequential.ir.scene_ir.EmitterIR, ...]) – All sources, in
scene.sourcesorder.sensors (tuple[optiland.nonsequential.ir.scene_ir.SensorIR, ...]) – All detectors, in
scene.detectorsorder.rng (optiland.nonsequential.ir.scene_ir.RngContract) – RNG algorithm contract (see
RngContract).sampling (optiland.nonsequential.ir.scene_ir.SamplingPolicy) – Rare-path sampling policy (see
SamplingPolicy).
- primitives: tuple[PrimitiveIR, ...]#
- rng: RngContract#
- sampling: SamplingPolicy#
- class SensorIR(id: int, kind: Literal['irradiance', 'spectral', 'far_field', 'ray_database'], to_world: numpy.ndarray, params: dict[str, Any], primitive_id: int | None = None, absorb: bool = True, name: str = '')[source]#
Bases:
objectA detector, as plain data.
- Variables:
id (int) – Index into
SceneIR.sensors.kind (Literal['irradiance', 'spectral', 'far_field', 'ray_database']) – Detector family;
paramsis interpreted according to it.to_world (numpy.ndarray) –
(4, 4)homogeneous local -> global transform.params (dict[str, Any]) – Kind-specific parameters (extents, pixel counts, splat, …).
primitive_id (int | None) – Index into
SceneIR.primitives, reserved for a future unification of detectors into the primitive list itself. AlwaysNone– detectors are dispatched byoptiland.nonsequential.detectors.dispatch, a single nearest-hit routine shared by both reference backends (PR10 deleted the two near-duplicate_intersect_detectorsimplementations that previously lived onArrayBackend/TorchBackendand had diverged in their grad-attachment semantics; D-10).absorb (bool) – Whether a hit terminates the ray.
False=> transmissive, mid-system sampling: the hit is recorded and the ray continues unchanged. Implemented as of PR10; mirrors the live detector’sBaseDetector.absorb.name (str) – Human-readable label.
- to_world: numpy.ndarray#
- class VolumeIR(id: int, name: str, boundary_primitive_ids: tuple[int, ...], interior_medium_id: int)[source]#
Bases:
objectA closed, outward-oriented set of boundary primitives (reserved).
Not populated by
lower()–SceneIR.volumesis always(). Medium sidedness does not need aVolumeregistry to be correct: each geometry’sn_geomfixes the front/back determination directly (seeRefractiveComponent.interact).Volumeitself – watertightness validation, CSG composition, andLens/Doublet/Mirrorbuilt on top of it – already exists inoptiland.nonsequential.components.volume; only the IR-level wiring (populating this dataclass from a live scene’s volumes) remains. This dataclass is defined now so the IR’s shape will not need to change again when that wiring is added.- Variables:
- apply_primitive_interactions(rays: NSQRayBundle, ir: SceneIR, components: list[BaseComponent], t_min: object, hit_normals: object, hit_n_geom: object, comp_idx: np.ndarray, comp_first_np: np.ndarray, rng: NSQRng, log_hit_fn: LogHitFn | None = None, ray_id_allocator: RayIdAllocator | None = None) NSQRayBundle | None[source]#
Apply each hit primitive’s interaction to
rays, in-place.This is the shared per-bounce “which surface did each ray hit, and what happens” step – previously duplicated almost verbatim between
ArrayBackend.trace()andTorchBackend.trace()(backend-specific only in whethert_min/hit_normalsare eager NumPy arrays or attached Torch tensors, whichoptiland.backendalready abstracts).Dispatch is IR-driven: primitives are visited in
ir.primitivesorder (not by iteratingcomponentsand asking “is this the hit one”), and each hit is checked against its recordedComponentKind/BsdfIRbefore the live component’sinteract()executes the physics (see the module docstring for why the physics itself still lives on the component).- Parameters:
rays – Ray bundle to update in-place.
ir – The scene’s lowered IR (built once per
trace()call).components –
scene.surfaces, in the same orderir.primitiveswas built from –components[i]is the live objectir.primitives[i]was lowered from.t_min – Per-ray nearest-primitive hit distance, shape (N,).
hit_normals – Per-ray nearest-primitive hit normal, shape (N, 3).
hit_n_geom – Per-ray nearest-primitive geometric (unflipped) normal, shape (N, 3); see
ComponentGeometry.ray_intersect.comp_idx – Per-ray index into
ir.primitives/componentsof the nearest-hit primitive, or -1. NumPy int array.comp_first_np – Per-ray mask: True where a primitive (not a detector) is this ray’s nearest hit and should be processed this bounce. NumPy bool array.
rng – Keyed PCG32 RNG.
log_hit_fn – Optional
(rays, mask, primitive_name, t_offset)callback for path recording, matching each backend’s_log_hitsclosure.ray_id_allocator –
(n) -> int64 ndarrayofnfresh, previously -unused ray ids. Required to enable bounded splitting (D2, PR11,ir.sampling.split_depth > 0) – omit (the default) on the Torch backend, which forcessplit_depth=0and never spawns rays (fixed tensor shapes are required for the autograd graph).
- Returns:
A new
NSQRayBundleof transmit-branch children spawned by bounded splitting this bounce, orNoneif none were spawned (splitting disabled, no eligible hits, orray_id_allocatorwas not given). The caller is responsible for merging this into the live bundle – seeoptiland.nonsequential.backends.array_backend.ArrayBackend.trace().
- assert_bsdf_matches(bsdf: object | None, bsdf_ir: BsdfIR) None[source]#
Raise if a live BSDF’s type disagrees with its lowered
BsdfIR.- Parameters:
bsdf – The live BSDF
bsdf_irwas lowered from (orNone).bsdf_ir – The corresponding
BsdfIR.
- Raises:
RuntimeError – If the live BSDF’s type no longer matches what
lower()recorded.
- assert_component_kind_matches(component: BaseComponent, primitive: PrimitiveIR) None[source]#
Raise if a live component’s type disagrees with its lowered IR kind.
Cheap (no per-ray cost): only compares two short strings. Called once per hit primitive per bounce, never per ray.
- Parameters:
component – The live component
primitivewas lowered from.primitive – The corresponding
PrimitiveIR.
- Raises:
RuntimeError – If the live component’s interaction type no longer matches what
lower()recorded – a lowering/interpreter drift bug, not a user configuration error.
- lower(scene: NSQScene, *, strict: bool = True) SceneIR[source]#
Lower a live
NSQSceneto a data-onlySceneIR.- Parameters:
scene – The scene to lower. Not mutated.
strict – Forwarded to
_MediumRegistry.get_id()for every material referenced by a surface or a source. Defaults to True (everyMediumIRmust be losslessly identifiable – vacuum or catalog-backed), which is what JSON export and the translatability checklist require. The backends passstrict=Falsefor thelower()call they make before everytrace(): that IR only needs to drive dispatch (D-1 sidedness is resolved from geometry, not from a medium id – seeoptiland.nonsequential.components.refractive), so a custom, non-catalog material must not block tracing the way it would block serialization.
- Returns:
The scene, described as plain data.
- Raises:
TypeError – If the scene contains a component, BSDF, source, or detector type this revamp does not yet know how to lower.
ValueError – If
strictand a material cannot be losslessly identified (mirrorsNSQScene.to_json()’s existing limitation).
- scene_ir_from_dict(d: dict) SceneIR[source]#
Reconstruct a
SceneIRfrom a dict produced byscene_ir_to_dict().- Parameters:
d – Dict previously produced by
scene_ir_to_dict().- Returns:
The reconstructed
SceneIR.
- scene_ir_to_dict(scene_ir: SceneIR) dict[source]#
Serialize a
SceneIRto a JSON-safe dict, losslessly.- Parameters:
scene_ir – The IR to serialize.
- Returns:
A dict built only from JSON-safe primitives, restorable via
scene_ir_from_dict().
Counter-based PCG32 RNG for Non-Sequential Raytracing.
Every stochastic decision in the NSQ engine is a pure function of a key
(seed, ray_id, bounce, event_slot[, offset]) – there is no shared
mutable stream. This is what makes results bit-identical across
batch_size, across NumPy compaction vs. Torch’s fixed-shape bundles, and
across any two conforming backends: a ray’s random numbers depend only on
its own identity, never on which other rays happen to be alive in the same
batch or in what order components were visited.
Algorithm#
This is the standard O’Neill PCG32 (XSH-RR 64/32), the same generator used
by Mitsuba 3 (pcg32.h) and satisfied by the per-launch-index
counter-based PRNGs conventional in OptiX kernels:
state_{k+1} = state_k * MULT + inc (mod 2**64) output_k = xsh_rr(state_k) (32-bit)
inc (the odd-valued stream selector) is derived from (seed, ray_id,
event_slot) via SplitMix64, so every ray gets its own independent stream
per event slot. bounce (plus an optional offset for multi-draw
slots such as rejection sampling) selects which output in that stream via
PCG32’s jump-ahead identity – a closed-form function of the LCG step count,
computed by the standard doubling algorithm in O(64) fixed iterations. This
is what “counter derived arithmetically rather than by stateful advance”
means: computing output number k never requires having computed outputs
0..k-1 first, and no RNG object needs to persist state between calls.
Honest scope of the guarantee: the random-number stream per
(ray_id, bounce, event_slot) is bit-identical everywhere this module is
used. Final float results are not guaranteed bit-identical across
NumPy/Torch/CPU/GPU, because floating-point summation order and
transcendental implementations differ – only the random decisions and the
code path they select are guaranteed identical.
Kramer Harrison, 2026
- class EventSlot(*values)[source]#
Bases:
IntEnumDiscriminates independent PCG32 streams within one (ray, bounce).
Every stochastic decision draws from its own slot, so adding, removing, or reordering an unrelated decision can never perturb another decision’s stream (defect D-8: a shared, position-dependent stream).
- BSDF_LOBE_BRANCH = 10#
- BSDF_U1 = 7#
- BSDF_U2 = 8#
- FRESNEL_BRANCH = 5#
- PATH_SAMPLE = 11#
- RR = 9#
- SCATTER_BRANCH = 6#
- SOURCE_U1 = 0#
- SOURCE_U2 = 1#
- SOURCE_U3 = 2#
- SOURCE_U4 = 3#
- SOURCE_WAVELENGTH = 4#
- class NSQRng(seed: int | None = None)[source]#
Bases:
objectKeyed PCG32 RNG for one trace.
Unlike
numpy.random.Generator, this carries no advancing internal state: every draw is a pure function of(seed, ray_id, bounce, event_slot), so the result never depends onbatch_size, on whether the NumPy backend has compacted dead rays out of the bundle, or on the order in which scene components were visited.- Variables:
seed – Trace-level RNG seed (defaults to 0 if none was given, so a trace is always reproducible even when the user does not pass one explicitly).
- uniform(ray_id: numpy.ndarray, bounce: numpy.ndarray, event_slot: int, offset: int = 0) numpy.ndarray[source]#
Draw one uniform float per ray, in [0, 1).
- Parameters:
ray_id – Per-ray identifiers, shape (N,).
bounce – Per-ray bounce/step index, shape (N,) or scalar.
event_slot –
EventSlotvalue or plain int.offset – See
pcg32_uint32().
- Returns:
float64 array in [0, 1), shape (N,).
- pcg32_uint32(seed: int, ray_id: numpy.ndarray, bounce: numpy.ndarray, event_slot: int, offset: int = 0) numpy.ndarray[source]#
Draw one PCG32 32-bit output per key, as a pure function of the key.
- Parameters:
seed – Trace-level RNG seed.
ray_id – Per-ray identifiers, shape (N,). Must be non-negative.
bounce – Per-ray bounce/step index, shape (N,) or a scalar broadcastable to (N,). Must be non-negative.
event_slot – Which independent stream within (ray_id, bounce) to draw from – an
EventSlotvalue or plain int.offset – Extra step count added to
bouncefor multi-draw slots (e.g. successive attempts in a rejection sampler) that need a fresh, deterministic value without consuming a new event slot.
- Returns:
uint32 array, shape (N,).
- pcg32_uniform(seed: int, ray_id: numpy.ndarray, bounce: numpy.ndarray, event_slot: int, offset: int = 0) numpy.ndarray[source]#
Draw one PCG32-derived uniform float per key, in [0, 1).
- Parameters:
seed – Trace-level RNG seed.
ray_id – Per-ray identifiers, shape (N,).
bounce – Per-ray bounce/step index, shape (N,) or scalar.
event_slot –
EventSlotvalue or plain int.offset – See
pcg32_uint32().
- Returns:
float64 array in [0, 1), shape (N,).
Serialization#
Versioned JSON serialization for NSQ scenes.
Provides scene_to_dict() and scene_from_dict() which convert an
NSQScene to and from a plain
JSON-serializable dict.
Schema version#
The top-level key "nsq_schema_version" is 1. NSQ has never been
officially released, so there is exactly one schema and no compatibility or
migration machinery for an earlier one: a file whose nsq_schema_version
does not match the current loader is refused with a generic mismatch error
naming both versions. Since NSQ is still pre-release, the physics and the
schema can both change without notice; a scene built against an older
checkout should be rebuilt from its original construction code (or
converted again via
sequential_to_nonsequential()) against
the current API.
Tensor handling#
All PyTorch tensors are detached and serialized as plain Python floats or
lists before writing. requires_grad is not persisted. A scene loaded
from JSON is plain-valued; users must re-wrap parameters in
torch.tensor(..., requires_grad=True) to enable differentiation after
loading.
Coordinate systems#
Only the local (x, y, z, rx, ry, rz) components are serialized. Nested
reference_cs chains are serialized recursively.
Materials#
String catalog names (e.g. 'N-BK7') round-trip as strings.
NSQMaterial instances
with an underlying optiland material round-trip via the catalog name stored on
the material object. NSQMaterial vacuum (optiland_material=None) is
serialized as null.
Not serialized#
SimulationResult/ detector dataRay databases
Mesh geometry file content (only the file path is stored)
Kramer Harrison, 2026
- scene_from_dict(d: dict) NSQScene[source]#
Reconstruct an
NSQScenefrom a serialized dict.Validates
"nsq_schema_version"before loading. A loaded scene is plain-valued; all parameters are plain Python floats, not tensors.- Parameters:
d – Dict previously produced by
scene_to_dict()or read from a JSON file written byNSQScene.to_json().- Returns:
Reconstructed
NSQScene.- Raises:
ValueError – If
"nsq_schema_version"is missing or does not matchNSQ_SCHEMA_VERSION. NSQ has never been officially released, so there is no compatibility mode or auto-migration for an older schema – a mismatched file must be rebuilt against the current API.ValueError – If any component/source/detector type is unknown.
- scene_from_json(path: str | os.PathLike) NSQScene[source]#
Load an
NSQScenefrom a versioned JSON file.This is the low-level implementation called by
NSQScene.from_json().- Parameters:
path – Path to the JSON file previously written by
scene_to_json()orNSQScene.to_json().- Returns:
Reconstructed
NSQScene.- Raises:
FileNotFoundError – If
pathdoes not exist.ValueError – If the schema version is missing or does not match.
- scene_to_dict(scene: NSQScene) dict[source]#
Convert an
NSQSceneto a JSON-serializable dict.The returned dict includes a top-level
"nsq_schema_version"key. Simulation results and detector data are not included.- Parameters:
scene – The scene to serialize.
- Returns:
JSON-serializable dict representing the scene structure.
- Raises:
TypeError – If any component, source, or detector type is not supported.
ValueError – If any material cannot be round-tripped.
Visualization#
- class NSQViewer2D(scene: NSQScene)[source]#
Bases:
BaseViewer2D2D cross-section viewer for non-sequential scenes.
Renders compound components, sources (as markers), and detectors onto a Matplotlib figure. Optionally overlays a random sample of ray paths when a
SimulationResultis provided.The renderer registry maps compound-component types to
ComponentRenderer2Dinstances. Default renderers are registered forLens,Mirror, and detectors.- Variables:
scene – The NSQScene to visualize.
_renderer_registry – Mapping from component class to renderer.
- register_renderer(component_type: type, renderer) None[source]#
Register a custom 2D renderer for a compound-component type.
- Parameters:
component_type – The class to bind the renderer to.
renderer – ComponentRenderer2D instance.
- view(result: SimulationResult | None = None, *, theme=None, projection: str = 'YZ', num_rays: int = 100, figsize: tuple[int, int] | None = None, title: str | None = None, xlim: tuple | None = None, ylim: tuple | None = None, ax: Axes | None = None, color_by: str = 'source') tuple[Figure, Axes][source]#
Render the scene cross-section to a Matplotlib figure.
When result is provided and contains
ray_paths, those paths are used for the ray overlay without running a new trace. If result isNone(or itsray_pathsisNone) andnum_rays > 0, a fresh trace is run internally.- Parameters:
result – Optional SimulationResult. If its
ray_pathsdict is populated it is used for the ray overlay directly.theme – Optional theme object for colours and styles. If None, the active theme is used.
projection – Projection plane –
'YZ'(default),'XZ', or'XY'.num_rays – Number of ray segments to sample and overlay. Defaults to 100. Pass 0 to skip ray drawing entirely.
figsize – Figure size override (width, height). None -> theme default.
title – Optional axes title. Defaults to
"NSQ Scene -- 2D Cross-Section".xlim – Optional (xmin, xmax) axis limits.
ylim – Optional (ymin, ymax) axis limits.
ax – Optional existing axes to plot into.
color_by – Ray colouring strategy –
'source'(default),'bounce', or'segment'.
- Returns:
(fig, ax) tuple.
- class NSQViewer3D(scene: NSQScene)[source]#
Bases:
BaseViewer3D3D VTK viewer for non-sequential scenes.
Renders compound components and detectors as VTK actors. Optionally overlays ray-path polylines when a SimulationResult with a RayDatabase is provided.
The renderer registry maps compound-component types to
ComponentRenderer3Dinstances. Default renderers are registered for Lens, Mirror, and detectors.- Variables:
scene – The NSQScene to visualize.
_renderer_registry – Mapping from component class to renderer.
- register_renderer(component_type: type, renderer) None[source]#
Register a custom 3D renderer for a compound-component type.
- Parameters:
component_type – The class to bind the renderer to.
renderer – ComponentRenderer3D instance.
- view(result: SimulationResult | None = None, *, num_rays: int = 100, dark_mode: bool = False, figsize: tuple[int, int] = (1200, 800), color_by: str = 'source') None[source]#
Render the scene in a VTK interactive window.
When result is provided and contains
ray_paths, those paths are used for the ray overlay without running a new trace. If result isNone(or itsray_pathsisNone) andnum_rays > 0, a fresh trace is run internally.- Parameters:
result – Optional SimulationResult. If its
ray_pathsdict is populated it is used for the ray overlay directly.num_rays – Number of ray segments to overlay. Pass 0 to skip ray drawing entirely.
dark_mode – Use a dark background if True.
figsize – (width, height) of the VTK window in pixels.
color_by – Ray colouring strategy –
'source'(default),'bounce', or'segment'.
- Raises:
ImportError – If VTK is not installed.
Converter#
Sequential -> Non-Sequential Converter.
Provides sequential_to_nonsequential() to convert an
Optic instance into a fully-populated
NSQScene.
Differentiable-ready output#
The returned scene stores all geometric parameters as plain Python
float values extracted from the sequential surfaces. This is
intentional: the converter does not know which parameters the user wants to
differentiate. To optimize a parameter with PyTorch, re-assign it as a
tensor after conversion:
scene = sequential_to_nonsequential(optic)
# Access the config on the compound component:
cfg = scene.component_registry.get("L1")._config
cfg.r1 = torch.tensor(cfg.r1, requires_grad=True)
Kramer Harrison, 2026
- exception ConversionError[source]#
Bases:
ExceptionRaised when a sequential surface or element cannot be converted to NSQ.
- class ConversionReport(coated_surfaces: list[str] = <factory>, uncoated_surfaces: list[str] = <factory>, mirror_reflectance_defaulted: list[str] = <factory>, estimated_apertures: list[str] = <factory>, polarization_dropped: bool = False)[source]#
Bases:
objectStructured record of what a conversion dropped or approximated.
Attached to the returned scene as
scene.conversion_reportrather than requiring the caller to parse warning text – the whole point of this class is that “what did the converter have to guess or drop” is inspectable data, not something a user has to have watched the log for.- Variables:
coated_surfaces (list[str]) – Names of refractive surfaces whose sequential unpolarized coating was carried over to
SurfaceConfig.coating(so NSQ and the sequential engine agree on R).uncoated_surfaces (list[str]) – Names of refractive surfaces with no usable coating in the sequential system; these get bare Fresnel reflection/refraction in NSQ, which the sequential engine never applies (transmission is always 100% there).
mirror_reflectance_defaulted (list[str]) – Names of mirrors with no usable scalar reflectance in the sequential system, defaulted to a perfect reflector (R=1.0) – NSQ has no implicit-mirror default, so the converter must supply something, and a perfect reflector is the most visible (least silently-wrong) choice.
estimated_apertures (list[str]) – Names of surfaces whose aperture radius was not explicitly set in the sequential system and had to be estimated from paraxial ray heights (or, failing that, a fixed 10 mm fallback) rather than read directly.
polarization_dropped (bool) – True if any sequential surface had a polarization-sensitive (Jones-matrix) coating – NSQ rays carry no polarization state, so these are dropped entirely, not approximated.
- sequential_to_nonsequential(optic: Optic, *, num_rays: int = 1000, detector_width: float | None = None, detector_height: float | None = None, detector_pixels: tuple[int, int] = (512, 512), beam_diameter: float | None = None, half_angle_deg: float | None = None) NSQScene[source]#
Convert a sequential Optic to a non-sequential NSQScene.
The image surface is converted to an IrradianceDetector. Each sequential field is mapped to one NSQ source (CollimatedSource for angle fields, PointSource for object-height fields). Consecutive refractive surfaces are grouped into Lens or Doublet compound components.
- Parameters:
optic – Sequential optical system to convert.
num_rays – Default ray count for visualization sources. Does not affect scene.trace() – pass num_rays there directly.
detector_width – Semi-width of the image detector [mm]. Defaults to 2x the paraxial image height.
detector_height – Semi-height of the image detector [mm]. Defaults to detector_width.
detector_pixels – (num_pixels_x, num_pixels_y) for the irradiance detector.
beam_diameter – Override entrance pupil diameter for collimated sources [mm]. Defaults to paraxial EPD.
half_angle_deg – Override cone half-angle for point sources [degrees]. Defaults to paraxial marginal-ray angle at the object plane.
- Returns:
NSQScene populated with lens/mirror components, sources, and a detector.
scene.conversion_reportis aConversionReportlisting everything the converter dropped or had to approximate – coatings, apertures, mirror reflectance, polarization – as structured data rather than only warning text.- Raises:
ConversionError – If any surface has an unsupported geometry type (coordinate breaks, diffraction gratings, NURBS, Zernike freeforms, or lens elements with more than 3 surfaces).
- Warns:
UserWarning – NSQ surfaces with no carried-over coating apply bare Fresnel reflection/refraction; sequential surfaces always transmit. See
scene.conversion_report.uncoated_surfacesfor exactly which surfaces this applies to.
Configuration reference#
Scenes are built by passing a *Config dataclass to the corresponding
scene.add_* builder. The tables below list every field, its type, units, and
default. Differentiable fields are typed float | Tensor: under the
PyTorch backend they may be passed as torch.Tensor leaves to receive
gradients via loss.backward().
Conventions: wavelengths in micrometres (µm), lengths/positions in
millimetres (mm), angles in degrees where a field name ends in
_deg, otherwise radians.
Sources#
PointSourceConfig — infinitesimal point emitting into a cone.
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
|
(required) |
Wavelength distribution (µm). |
|
float | Tensor |
|
Total emitted flux [W]. |
|
float |
|
Emission cone half-angle [deg]; 90 = hemisphere, 180 = isotropic. |
|
|
|
Embedding medium (default vacuum). |
CollimatedSourceConfig — parallel beam.
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
|
(required) |
Wavelength distribution (µm). |
|
float | Tensor |
|
Total emitted flux [W]. |
|
float | Tensor |
|
Beam semi-diameter [mm]. |
|
str |
|
Spatial profile: |
|
float | None |
|
Gaussian sigma [mm]; defaults to |
|
|
|
Embedding medium (default vacuum). |
ExtendedSourceConfig — spatially + angularly extended emitter.
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
|
(required) |
Wavelength distribution (µm). |
|
float | Tensor |
|
Total emitted flux [W]. |
|
float | Tensor |
|
Source width [mm]. |
|
float | Tensor |
|
Source height [mm]. |
|
float | None |
|
Circular aperture radius [mm]; overrides width/height when set. |
|
float |
|
Emission cone half-angle [deg]. Rays are uniform within the cone below 90; at 90 and above they are cosine-weighted over the full hemisphere (Lambertian), and values above 90 behave as 90. |
|
|
|
Embedding medium (default vacuum). |
Components#
LensConfig — single refractive element (up to four surfaces).
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
float | Tensor |
(required) |
Front vertex radius of curvature [mm]; + = centre of curvature on +z. |
|
float | Tensor |
(required) |
Back vertex radius of curvature [mm]. |
|
float | Tensor |
(required) |
Centre thickness [mm]. |
|
str | |
(required) |
Glass name (e.g. |
|
float | Tensor |
(required) |
Front-face semi-diameter [mm]. |
|
float | None |
|
Back-face semi-diameter [mm]; defaults to |
|
float | Tensor |
|
Front-face conic constant (0 = sphere). |
|
float | Tensor |
|
Back-face conic constant. |
|
|
|
Per-surface overrides (see SurfaceConfig). |
DoubletConfig — cemented achromatic doublet.
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
float | Tensor |
(required) |
Front radius of curvature [mm]. |
|
float | Tensor |
(required) |
Cemented-interface radius of curvature [mm]. |
|
float | Tensor |
(required) |
Back radius of curvature [mm]. |
|
float | Tensor |
(required) |
Crown element thickness [mm]. |
|
float | Tensor |
(required) |
Flint element thickness [mm]. |
|
str | |
(required) |
Crown / flint glass name or |
|
float | Tensor |
(required) |
Common semi-diameter for all surfaces [mm]. |
|
float | Tensor |
|
Conic constants of front / cemented / back faces. |
|
|
|
Per-surface overrides. |
MirrorConfig — single reflective surface.
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
float | Tensor |
(required) |
Vertex radius of curvature [mm]; − = concave (normal toward +z). |
|
float | Callable | |
(required) |
Mirror reflectance: a constant in [0, 1], a
|
|
float | Tensor |
|
Conic constant (0 = sphere, −1 = paraboloid). |
|
float | Tensor |
|
Semi-diameter [mm]. |
|
|
|
Per-surface override (e.g. attach a scatter BSDF or override
|
SurfaceConfig — optional per-surface overrides within a compound component.
All fields default to None (use the compound-level default).
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
|
|
Scatter model. Rays routed to it are scattered instead of being refracted or specularly reflected. |
|
float |
|
Probability that a ray striking this surface is routed through
|
|
|
|
An |
|
float | None |
|
Semi-diameter override [mm]. |
|
|
|
Force a specific interaction (REFRACTIVE / REFLECTIVE / ABSORBING). |
|
float | Callable | |
|
Required when |
Detectors#
IrradianceDetectorConfig — 2-D spatial flux map.
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
float |
(required) |
Detector extent [mm]. |
|
int |
|
Pixel grid resolution. |
|
|
|
Splatting mode. |
|
float |
|
Gaussian splat sigma in pixels (used when |
|
bool |
|
Whether a hit terminates the ray. |
SpectralDetectorConfig — per-wavelength irradiance.
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
float |
(required) |
Detector extent [mm]. |
|
int |
|
Pixel grid resolution. |
|
float |
|
Spectral binning range [µm] (same convention as |
|
int |
|
Number of wavelength bins. |
|
|
|
Only |
|
float |
|
Reserved for future use. |
|
bool |
|
Whether a hit terminates the ray. |
FarFieldDetectorConfig — angular intensity pattern.
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
int |
|
Number of polar-angle bins. |
|
int |
|
Number of azimuthal-angle bins. |
|
bool |
|
Whether a hit terminates the ray. |
RayDatabaseConfig — full phase-space record.
Field |
Type |
Default |
Meaning |
|---|---|---|---|
|
float |
(required) |
Detector extent [mm]. |
|
int |
|
Maximum rays to store (0 = unlimited). |
|
bool |
|
Whether a hit terminates the ray. |