iactrace.core

Low-level ray tracing components. Most users should use the Telescope and Camera classes instead of these functions directly.

Render Engine

iactrace.core.render_optics(optical_groups, obstruction_groups, sources, values, source_type)[source]

Render sources through the optics; return one flat RayBundle.

Materialises the full (n_elements * n_sources * n_samples,) ray buffer. Use render_optics_accumulate() when only a small aggregate (image, response matrix, …) is needed.

iactrace.core.trace_optics(optical_groups, obstruction_groups, ray_origins, ray_directions, values, record_trajectory=False)[source]

Trace rays from arbitrary origins through full optical system.

Args:

optical_groups: List of OpticalElementGroup (combined mirrors + lenses). obstruction_groups: List of ObstructionGroup. ray_origins: (n_rays, 3). ray_directions: (n_rays, 3), normalized. values: (n_rays,). record_trajectory: When True, also collect the per-stage hit points and

return them as a Trajectory alongside the RayBundle. Off by default; when off, no trajectory is built and nothing extra is computed (mirrors the trace_chain() record_trajectory option).

Returns:

A TraceResult. Its rays are in 3D space after all optical stages; its trajectory is None unless record_trajectory was set, in which case the Trajectory holds the source point followed by each stage’s landing point (world frame), (n_stages + 1, n_rays, 3). It ends on the last optic – this kernel knows no camera.

Ray Bundle

class iactrace.core.RayBundle[source]

Bases: Module

Bundle of rays through the optical system.

Carries ray positions, directions, weights, path lengths, and a per-ray liveness flag.

The frame of origins / directions is implicit and depends on where the bundle came from: Telescope.render and Telescope.trace return rays in the camera-local frame so they can be fed straight into Camera.collect / Camera.image.

Liveness vs throughput. IACTrace keeps the two ways a ray can be “lost” on separate axes:

  • alive (bool) answers “is this a valid, still-propagating ray?”. It is flipped off only by geometry / occlusion loss: a ray that misses every element in a stage, lands outside an aperture, is blocked by an obstruction, or misses the sensor. Once False it stays False (an absorbing state), and the origins / directions of a dead ray are meaningless — always mask geometry with alive before reading positions.

  • values (float >= 0) is the radiometric throughput of a live ray. Every physical coefficient multiplies into it — primary sampling weight, reflectivity / transmittance, quantum efficiency, concentrator throughput. A live ray may legitimately reach 0 (a perfectly absorbing coating, total internal reflection); that is distinct from a dead ray and is not recorded on the alive axis.

As an invariant a dead ray always carries values == 0, so the image / response-matrix sums (which add values) need no masking; the alive flag exists so per-ray consumers can tell why a ray is dark. “Carries light” is simply alive & (values > 0).

By the time the bundle reaches Camera.collect the entries of values are photoelectrons, not raw photons.

Attributes:
origins: Ray positions in 3D (n_rays, 3). Meaningful only where

alive is True.

directions: Ray direction vectors (n_rays, 3). Meaningful only

where alive is True.

values: Throughput-weighted ray intensities (n_rays,). path_length: Accumulated optical path length per ray

(n_rays,), in metres.

n: Per-ray refractive index of the medium each ray is

currently propagating in (n_rays,). Carried so downstream consumers (sensor intersection, focal-surface analysis) can weight the final geometric leg correctly.

alive: Per-ray liveness flag (n_rays,), boolean. True for a

valid, still-propagating ray. Defaults to all-True at construction, i.e. a freshly built bundle is fully alive.

__init__(origins, directions, values, path_length, n, alive=None)[source]
origins = <dataclasses._MISSING_TYPE object>
directions = <dataclasses._MISSING_TYPE object>
values = <dataclasses._MISSING_TYPE object>
path_length = <dataclasses._MISSING_TYPE object>
n = <dataclasses._MISSING_TYPE object>
alive = <dataclasses._MISSING_TYPE object>
replace(**changes)[source]

Copy with the given fields replaced (functional update).

rays.replace(values=v) reads better than re-listing all six fields; unknown field names raise TypeError.

to_frame(origin, rotation)[source]

Express these rays in the local frame given by origin + Euler rotation.

origin is the new frame’s position in the current frame; rotation are XYZ Euler angles in degrees.

This is a pure coordinate transform: it moves origins and directions and leaves values / path_length / n / alive untouched.

Optical Element Composition

class iactrace.core.OpticalElementGroup[source]

Bases: Module

Optical element group composing surface + aperture + interaction.

All optical elements (mirrors, lenses, slabs) are instances of this class configured with appropriate modules.

__init__(positions, rotations, surface, aperture, interaction_module, sample_key, optical_stage=0, n_samples=100, bsdf=None)[source]
positions = <dataclasses._MISSING_TYPE object>
rotations = <dataclasses._MISSING_TYPE object>
surface = <dataclasses._MISSING_TYPE object>
aperture = <dataclasses._MISSING_TYPE object>
interaction_module = <dataclasses._MISSING_TYPE object>
optical_stage = <dataclasses._MISSING_TYPE object>
n_samples = <dataclasses._MISSING_TYPE object>
bsdf = <dataclasses._MISSING_TYPE object>
sample_key = <dataclasses._MISSING_TYPE object>
property n_elements
property interaction
property kind

User-facing element kind, derived from the interaction module.

check_aperture(x, y, element_idx)[source]
transform_to_world()[source]

Compute geometry from current surface params and transform to world coordinates.

Samples are generated at call time using the stored n_samples and sample_key.

Returns:

Tuple of (points_world, normals_world, weights) arrays.

sample_primary_geometry(roughness_salt)[source]

Sample this group’s aperture, with this group’s surface roughness applied.

Args:
roughness_salt: Integer folded into this group’s sample_key

to draw the roughness perturbation, keeping it independent from the aperture-sampling draw and from other call sites sharing the same sample_key.

Returns:

Tuple of (points_world, normals_world, weights) arrays, as transform_to_world(), with normals_world perturbed.

intersect(element_idx, origins, directions)[source]

Intersect world-frame rays with element element_idx.

Args:

element_idx: Index of the element within this group. origins, directions: (n_rays, 3) rays in world coordinates.

Returns:

Tuple of (t, points_world, normals_world), each (n_rays, ...). t is inf where the surface hit falls outside the element’s aperture.

intersect_t(element_idx, origins, directions)[source]

Hit distance only, for the nearest-hit search over a stage.

Same t as intersect(), minus the surface point and normal.

Args:

element_idx: Index of the element within this group. origins, directions: (n_rays, 3) rays in world coordinates.

Returns:

(n_rays,) hit distances, inf where the ray misses the surface or lands outside the element’s aperture.

hit_geometry(element_idx, origins, directions)[source]

World-frame hit point and normal, for a per-ray element index.

The counterpart to intersect_t(): once the search knows which element each ray settled on, this evaluates the surface there, once.

Args:

element_idx: (n_rays,) per-ray element index. origins, directions: (n_rays, 3) rays in world coordinates.

Returns:

Tuple of (points_world, normals_world), each (n_rays, 3).

perturb_normals(normals, roughness_salt, element_idx=None)[source]

Apply this group’s own BSDF surface-roughness perturbation.

roughness_salt is folded into this group’s sample_key, so independent call sites drawing separate perturbations for the same group should pass distinct salts.

apply_interaction(directions, normals, points, element_idx, current_n)[source]

Apply this group’s physical interaction (reflect/refract/slab) at a hit.

See Interaction.apply() for the return value.

interact(directions, normals, points, element_idx, current_n, roughness_salt)[source]

Perturb normals for roughness, then apply the physical interaction.

See Interaction.apply() for the return value.

Apertures

class iactrace.core.Aperture[source]

Bases: Module

Abstract base for aperture modules.

abstractmethod check(x, y, element_idx)[source]
abstractmethod sample(key, n_samples)[source]
abstractmethod get_area_data()[source]
abstractmethod area_fn(data)[source]
__init__()
class iactrace.core.DiskAperture[source]

Bases: Aperture

Circular or annular aperture defined by outer and inner radii.

Supports solid disks (inner_radii=0) and annular rings.

Attributes:

radii: Outer radius per element (N,) inner_radii: Inner radius per element (N,), 0 for solid disk

radii = <dataclasses._MISSING_TYPE object>
inner_radii = <dataclasses._MISSING_TYPE object>
check(x, y, element_idx)[source]

Check if point (x, y) is within the aperture of the given element.

sample(key, n_samples)[source]

Sample uniform 2D points on each element’s annular aperture.

Args:

key: JAX PRNG key n_samples: Number of samples per element

Returns:

(N, n_samples, 2) array of 2D sample points

get_area_data()[source]

Return per-element data for area computation, vmapped over elements.

Returns:

Array of shape (N, 2) with [inner_radius, outer_radius] per element.

area_fn(data)[source]

Compute aperture area from a single element’s area data.

Args:

data: [inner_radius, outer_radius] (2,)

Returns:

Annular area (scalar)

__init__(radii, inner_radii)
class iactrace.core.PolygonAperture[source]

Bases: Aperture

Convex polygon aperture defined by vertices.

All elements in a group must have the same number of vertices (required for JAX array batching).

Attributes:

vertices: Polygon vertices per element (N, K, 2), CCW order n_vertices: Number of vertices per polygon (static, same for all)

vertices = <dataclasses._MISSING_TYPE object>
n_vertices = <dataclasses._MISSING_TYPE object>
check(x, y, element_idx)[source]

Check if point (x, y) is within the polygon aperture.

sample(key, n_samples)[source]

Sample uniform 2D points on each element’s polygon aperture.

Args:

key: JAX PRNG key n_samples: Number of samples per element

Returns:

(N, n_samples, 2) array of 2D sample points

get_area_data()[source]

Return per-element data for area computation, vmapped over elements.

Returns:

Vertices array (N, K, 2).

area_fn(data)[source]

Compute polygon area from a single element’s vertices.

Args:

data: Polygon vertices (K, 2)

Returns:

Polygon area (scalar)

__init__(vertices, n_vertices)

Interactions

class iactrace.core.Interaction[source]

Bases: Module

Abstract base for optical interaction modules.

abstract property interaction_type
abstract property kind

User-facing element kind.

focal_scale(n_outside=1.0)[source]

Curvature<->focal-length scale factor for this interaction.

curvature = 1 / (scale * focal_length): 2 for mirrors, n_inside - n_outside for a single refracting surface. Returns None where a focal length is not a meaningful concept (slabs), letting the caller decide how to report that.

abstractmethod apply(directions, normals, points, element_idx, current_n)[source]

Apply the interaction at hit points.

Args:
directions, normals, points, element_idx: per-ray geometry

at the surface hit.

current_n: per-ray refractive index of the medium the ray

is currently propagating in. Used as the incident-side index for refraction physics, so OPL is exact even through stacked refractive surfaces.

Returns a 5-tuple (new_directions, new_positions, coefficients, opl_internal, new_n).

__init__()
class iactrace.core.ReflectInteraction[source]

Bases: Interaction

Reflection interaction for mirrors.

Per-ray coefficient:

reflectivity_scalar[idx] * reflectivity(cos_theta_i, idx)

When reflectivity is None (the default) the angular factor is unity, i.e. an ideal angle-independent mirror with response reflectivity_scalar. Provide a TabulatedCoating (or any Coating) to model a measured R(theta) curve. Reflection does not change the medium: new_n == current_n.

Attributes:
reflectivity: Angle-dependent coating, or None for a flat

angular response.

reflectivity_scalar: Per-element bulk multiplier in [0, 1],

shape (N,). Operations such as set_reflectivity() write here, leaving the coating untouched.

reflectivity = <dataclasses._MISSING_TYPE object>
reflectivity_scalar = <dataclasses._MISSING_TYPE object>
property interaction_type
property kind

User-facing element kind.

focal_scale(n_outside=1.0)[source]

Curvature<->focal-length scale factor for this interaction.

curvature = 1 / (scale * focal_length): 2 for mirrors, n_inside - n_outside for a single refracting surface. Returns None where a focal length is not a meaningful concept (slabs), letting the caller decide how to report that.

with_reflectivity_scalar(reflectivity_scalar)[source]

Return a copy with the bulk reflectivity multiplier replaced.

scaled_reflectivity(factor)[source]

Return a copy with the bulk reflectivity multiplier scaled by factor.

apply(directions, normals, points, element_idx, current_n)[source]

Apply the interaction at hit points.

Args:
directions, normals, points, element_idx: per-ray geometry

at the surface hit.

current_n: per-ray refractive index of the medium the ray

is currently propagating in. Used as the incident-side index for refraction physics, so OPL is exact even through stacked refractive surfaces.

Returns a 5-tuple (new_directions, new_positions, coefficients, opl_internal, new_n).

__init__(reflectivity, reflectivity_scalar)
class iactrace.core.RefractInteraction[source]

Bases: Interaction

Single-surface refraction interaction for lenses.

Per-ray coefficient:

transmittance_scalar[idx] * angular_response(cos_theta_i)

When transmittance is None (the default) the angular response is fresnel_unpolarized() evaluated from current_n (the medium the ray is currently in) and n_inside (the far side of this surface). Snell’s law is always applied to bend the ray.

Semantically, this represents the ray crossing a single interface from one medium into another. A real glass body (e.g. a biconvex lens) is modelled as two consecutive RefractInteraction stages, front then back surface, and the render loop’s per-ray medium tracker carries the correct index through the glass interior between them, so OPL is exact.

Attributes:
n_inside: Refractive index on the far side of this surface,

per element (N,). “Far side” means the medium the ray transmits into: for a front surface this is the glass index, for a back surface it is the ambient index.

transmittance: Angle-dependent coating, or None for

bare-interface Fresnel transmittance.

transmittance_scalar: Per-element bulk multiplier in [0, 1],

shape (N,).

n_inside = <dataclasses._MISSING_TYPE object>
transmittance = <dataclasses._MISSING_TYPE object>
transmittance_scalar = <dataclasses._MISSING_TYPE object>
property interaction_type
property kind

User-facing element kind.

focal_scale(n_outside=1.0)[source]

Curvature<->focal-length scale factor for this interaction.

curvature = 1 / (scale * focal_length): 2 for mirrors, n_inside - n_outside for a single refracting surface. Returns None where a focal length is not a meaningful concept (slabs), letting the caller decide how to report that.

with_n_inside(n_inside)[source]

Return a copy with the refractive index replaced.

with_transmittance_scalar(transmittance_scalar)[source]

Return a copy with the bulk transmittance multiplier replaced (clipped to [0, 1]).

scaled_transmittance(factor)[source]

Return a copy with the bulk transmittance multiplier scaled by factor.

apply(directions, normals, points, element_idx, current_n)[source]

Apply the interaction at hit points.

Args:
directions, normals, points, element_idx: per-ray geometry

at the surface hit.

current_n: per-ray refractive index of the medium the ray

is currently propagating in. Used as the incident-side index for refraction physics, so OPL is exact even through stacked refractive surfaces.

Returns a 5-tuple (new_directions, new_positions, coefficients, opl_internal, new_n).

__init__(n_inside, transmittance, transmittance_scalar)
class iactrace.core.SlabInteraction[source]

Bases: Interaction

Parallel-sided slab (window) interaction.

Per-ray coefficient:

transmittance_scalar[idx] * angular_response

When transmittance is None (the default) the angular response is the standard Fresnel product at the two faces: by parallel-slab symmetry and Stokes reciprocity, both faces share the same single-face Fresnel coefficient so the result simplifies to T_face^2. Provide a Coating to override with a vendor- supplied T(theta) curve for the complete slab; the coating fully replaces the Fresnel product. The TIR mask from the underlying geometry gates out invalid rays either way.

The ray enters from its current medium (current_n), refracts into the slab material, traverses it, and refracts back out into the same medium; slabs assume the ambient is symmetric across them, which is the usual case for a window. opl_internal is the per-ray n_in * L inside the slab.

Attributes:

n_inside: Per-element slab refractive index, shape (N,). thickness: Per-element slab thickness, shape (N,). transmittance: Angle-dependent coating, or None for the

bare-window Fresnel product.

transmittance_scalar: Per-element bulk multiplier in [0, 1],

shape (N,).

n_inside = <dataclasses._MISSING_TYPE object>
thickness = <dataclasses._MISSING_TYPE object>
transmittance = <dataclasses._MISSING_TYPE object>
transmittance_scalar = <dataclasses._MISSING_TYPE object>
property interaction_type
property kind

User-facing element kind.

with_n_inside(n_inside)[source]

Return a copy with the slab refractive index replaced.

with_thickness(thickness)[source]

Return a copy with the slab thickness replaced.

with_transmittance_scalar(transmittance_scalar)[source]

Return a copy with the bulk transmittance multiplier replaced (clipped to [0, 1]).

scaled_transmittance(factor)[source]

Return a copy with the bulk transmittance multiplier scaled by factor.

apply(directions, normals, points, element_idx, current_n)[source]

Apply the interaction at hit points.

Args:
directions, normals, points, element_idx: per-ray geometry

at the surface hit.

current_n: per-ray refractive index of the medium the ray

is currently propagating in. Used as the incident-side index for refraction physics, so OPL is exact even through stacked refractive surfaces.

Returns a 5-tuple (new_directions, new_positions, coefficients, opl_internal, new_n).

__init__(n_inside, thickness, transmittance, transmittance_scalar)
class iactrace.core.InteractionType[source]

Bases: Enum

Type of optical interaction at a surface.

REFLECT = 'reflect'
REFRACT = 'refract'
SLAB = 'slab'

Coatings

Angle-dependent reflectivity / transmittance applied at an interaction.

class iactrace.core.Coating[source]

Bases: Module

Abstract base for angle-dependent optical coatings.

A coating maps the incidence-angle cosine of each ray to a coefficient in [0, 1] (reflectance or transmittance, depending on the surface type). All subclasses must return an array broadcastable to cos_theta_i.shape.

__init__()
class iactrace.core.ConstantCoating[source]

Bases: Coating

Angle-independent per-element coating.

Attributes:

values: Per-element coefficient in [0, 1], shape (N,).

values = <dataclasses._MISSING_TYPE object>
__init__(values)
class iactrace.core.TabulatedCoating[source]

Bases: Coating

Linear interpolation over a shared angle grid.

Attributes:
cos_table: cos(angle) lookup axis, sorted ascending, shape

(K,). cos_theta_i = 1 -> normal incidence, cos_theta_i = 0 -> grazing.

values: Per-element coefficient values aligned with

cos_table, shape (N, K).

cos_table = <dataclasses._MISSING_TYPE object>
values = <dataclasses._MISSING_TYPE object>
classmethod from_degrees(angles_deg, values, n_elements)[source]

Build a TabulatedCoating from human-readable angles.

Args:
angles_deg: Sample angles in degrees, shape (K,). Don’t

need to be sorted since they are reordered into cos-ascending form internally.

values: Coefficient values. (K,) is broadcast to all

n_elements elements; (N, K) is used as-is and must match n_elements along the first axis.

n_elements: Number of elements N in the enclosing group.

Returns:

A ready-to-use coating with the cos-ascending lookup table precomputed.

__init__(cos_table, values)

BSDF (surface scattering)

class iactrace.core.BSDF[source]

Bases: Module

Abstract base for surface scattering models.

Subclasses implement _sample_perturbation() which returns (angles, scale) for a given shape and element-index resolver. The base class handles tangent-frame construction and applies the perturbation.

perturb_normals(normals, key, element_idx=None)[source]

Perturb surface normals.

Works for any leading shape: (n_rays, 3) with per-ray element_idx, or (N, S, 3) with element_idx=None when the element dimension is already present.

Args:

normals: (…, 3). key: JAX PRNG key. element_idx: Per-ray element index, or None for batch mode.

Returns:

Perturbed normals (…, 3).

__init__()
class iactrace.core.GaussianBSDF[source]

Bases: BSDF

Single-Gaussian surface roughness model.

Perturbs surface normals by Gaussian-distributed random angles. This is the standard model for surface microroughness.

Attributes:
scale: Per-element roughness sigma in arcseconds (N,).

Zero means perfect specular (no perturbation).

scale = <dataclasses._MISSING_TYPE object>
__init__(scale)
class iactrace.core.DoubleGaussianBSDF[source]

Bases: BSDF

Mixture of two Gaussians for surfaces with multi-scale roughness.

Models surfaces that have both fine-scale microroughness (narrow component) and broader scattering from mid-spatial-frequency errors (wide component). Each ray’s perturbation is drawn from the narrow Gaussian with probability (1 - mix_weight) or from the wide Gaussian with probability mix_weight.

Attributes:

scale_narrow: Per-element narrow-component sigma in arcseconds (N,). scale_wide: Per-element wide-component sigma in arcseconds (N,). mix_weight: Per-element probability of the wide component (N,),

values in [0, 1].

scale_narrow = <dataclasses._MISSING_TYPE object>
scale_wide = <dataclasses._MISSING_TYPE object>
mix_weight = <dataclasses._MISSING_TYPE object>
__init__(scale_narrow, scale_wide, mix_weight)

Optical Physics

Functions for ray-surface interactions:

iactrace.core.interactions.reflect(direction, normal)[source]

Reflect a ray’s direction off a surface.

Args:

direction: Ray direction (3,), pointing into the surface. normal: Surface normal (3,), pointing outward.

Returns:

reflected: Reflected direction (3,). cos_i: Cosine of the incidence angle (non-negative).

iactrace.core.interactions.refract(direction, normal, n1, n2)[source]

Refract a ray’s direction through an interface (Snell’s law).

Handles rays from either side of the surface by flipping the normal if needed. On total internal reflection, the reflected direction is returned in place of the refracted one and tir is True.

Args:

direction: Ray direction (3,), normalized. normal: Surface normal (3,), normalized, pointing outward. n1: Refractive index of the incident medium. n2: Refractive index of the transmitted medium.

Returns:

refracted: Refracted direction (3,), or reflected if TIR. cos_i: Cosine of the incidence angle (non-negative, with the

ambient-vs-internal side correctly resolved).

tir: True if total internal reflection occurred.

iactrace.core.interactions.refract_slab(direction, normal, position, n_out, n_in, thickness)[source]

Refract a ray through a parallel-sided slab (window).

Args:

direction: Ray direction (3,), normalized. normal: Front-surface normal (3,), pointing outward. position: Entry point in world coordinates (3,). n_out: Refractive index of the ambient medium. n_in: Refractive index of the slab material. thickness: Slab thickness in the same units as position.

Returns:

exit_direction: Ray direction after leaving the slab. exit_position: World-space point where the ray exits. cos_i: Cosine of the incidence angle on the slab from outside. valid: True iff no total internal reflection occurred at

either face.

path_length: Geometric distance the ray travels inside the

slab, in the same units as thickness. Multiplied by n_in by the caller to obtain the OPL contribution n_in * L.

iactrace.core.coatings.fresnel_unpolarized(cos_theta_i, n1, n2)[source]

Unpolarized Fresnel reflection and transmission coefficients.

The standard formula for an ideal bare dielectric interface, used as the implicit default by RefractInteraction and SlabInteraction when no explicit Coating is supplied. Average of s- and p-polarized intensities.

The transmitted angle is derived internally from Snell’s law, so only the incidence cosine and the two indices are needed. Total internal reflection (sin^2(theta_t) > 1) collapses cos_theta_t to zero, which the formula correctly turns into R = 1, T = 0.

Args:

cos_theta_i: Cosine of the incidence angle. n1: Refractive index of the incident medium. n2: Refractive index of the transmitted medium.

Returns:

R: Reflectance in [0, 1]. T: Transmittance T = 1 - R.

Surfaces

Surface-figure models. SurfaceGroup is the base; the concrete groups below can be combined with SumSurfaceGroup (e.g. an aspheric base plus a per-facet Zernike figure error).

class iactrace.core.SurfaceGroup[source]

Bases: Module

Abstract base for batched surface parameters of N optical elements.

A SurfaceGroup stores per-element surface geometry and provides: - Sag/normal computation for the transform pipeline (vmapped per element) - Per-element sag and ray intersection for rendering and visualization

Subclasses must store an offsets array of shape (N, 2) and implement the abstract methods below. See AsphericSurfaceGroup for a concrete example.

offsets = <dataclasses._MISSING_TYPE object>
compute_sag_and_normal_at(x, y)[source]

Compute surface point and normal at (x, y) for a single element.

Args:

x: x-coordinate (scalar). y: y-coordinate (scalar).

Returns:

Tuple of (point, normal) where point is (3,) and normal is (3,), normalized.

sag_at(element_idx, x, y)[source]

Compute surface sag z(x, y) for a single element.

Used by the visualization module for mesh generation.

Args:

element_idx: Element index within the group. x: x-coordinate in local frame (scalar). y: y-coordinate in local frame (scalar).

Returns:

z: Surface sag at (x, y) relative to the element’s decenter.

intersect_t_at(element_idx, ray_origin, ray_direction, max_iter=10, tol=None)[source]

Ray parameter and local landing point, without evaluating the surface.

Args:

element_idx: Element index within the group. ray_origin: Ray origin in local coordinates (3,). ray_direction: Ray direction (3,). max_iter: Maximum Newton-Raphson iterations. tol: Absolute residual tolerance, or None to derive it per ray.

Returns:

Tuple of (t, x, y): the intersection distance (inf on a miss) and the local in-surface coordinates of the landing point. On a miss the coordinates are those of the ray’s closest approach, so they stay finite for downstream masking.

intersect_at(element_idx, ray_origin, ray_direction, max_iter=10, tol=None)[source]

Intersect a ray with a single element’s surface.

Used by the render pipeline for per-ray intersection. Generic over the surface type: _intersect_t() resolves the ray parameter (the closed-form root for pure conics, Newton-refined from _t_guess() otherwise); point / normal follow from the sag at the hit. On a miss (t = inf) they are evaluated at the ray origin, so they stay finite for downstream masking.

Args:

element_idx: Element index within the group. ray_origin: Ray origin in local coordinates (3,). ray_direction: Ray direction (3,). max_iter: Maximum Newton-Raphson iterations. tol: Absolute residual tolerance, or None (the default) to derive

it per ray from the coordinate magnitudes. See newton_raphson_intersect().

Returns:
Tuple of (t, point, normal):
  • t: Intersection distance (scalar), inf on a miss.

  • point: Intersection point (3,).

  • normal: Surface normal at intersection (3,).

__init__(offsets)
class iactrace.core.SumSurfaceGroup[source]

Bases: SurfaceGroup

Composite surface whose sag is the sum of its components’ sags.

The composite’s own offsets decenter the whole patch; component offsets (usually zero) are applied first, inside each component’s _sag_local.

Attributes:
components: Tuple of component SurfaceGroup instances, each

sized to the same N.

offsets: Per-element in-surface decenter for the composite (N, 2)

(inherited).

__init__(components, offsets=None)[source]
components = <dataclasses._MISSING_TYPE object>
class iactrace.core.AsphericSurfaceGroup[source]

Bases: SurfaceGroup

Batched aspheric surface parameters for N optical elements.

A conic + even-polynomial surface used by OpticalElementGroup. When sliced/vmapped to a single element, each becomes a scalar-parameter surface; the generic SurfaceGroup machinery then handles sag, normal, and intersection. The conic provides a closed-form intersection initial guess via _t_guess().

Attributes:

curvatures: Per-element curvatures (N,) conics: Per-element conic constants (N,) aspherics: Per-element even aspheric coefficients [A4, A6, ...]

(N, K); column i multiplies r^(2i + 4). See sag_raw().

offsets: Per-element in-surface decenter (N, 2) (inherited)

curvatures = <dataclasses._MISSING_TYPE object>
conics = <dataclasses._MISSING_TYPE object>
aspherics = <dataclasses._MISSING_TYPE object>
__init__(offsets, curvatures, conics, aspherics)
class iactrace.core.ZernikeSurfaceGroup[source]

Bases: SurfaceGroup

Standalone Zernike figure surface for N optical elements.

Represents a surface whose height is a sum of RMS-normalized Noll Zernike polynomials, independent of any conic/aspheric base. Use it on its own to describe a pure figure-error surface, or as a term inside a SumSurfaceGroup to add a measured / random figure error on top of another surface.

The normal is obtained by autodiff of the sag (inherited from SurfaceGroup), and the intersection uses the inherited tangent-plane initial guess, which is appropriate for the shallow surfaces figure errors produce.

Attributes:
coeffs: Per-element Noll coefficients in metres, shape (N, J) with

J <= 11. Column m is Noll index m + 1 (Z1 = piston).

r_norm: Per-element normalization radius in metres, shape (N,).

rho = 1 at this radius.

offsets: Per-element in-surface decenter (N, 2) (inherited).

__init__(coeffs, r_norm, offsets=None)[source]
coeffs = <dataclasses._MISSING_TYPE object>
r_norm = <dataclasses._MISSING_TYPE object>
class iactrace.core.FreeformSurfaceGroup[source]

Bases: SurfaceGroup

Per-element freeform surface defined by a bicubically interpolated grid.

Each element carries a regular (H, W) height map sampled over a rectangular domain; the sag at arbitrary (x, y) is the Catmull-Rom bicubic interpolation of that map.

For a strongly curved freeform, compose it on top of an AsphericSurfaceGroup in a SumSurfaceGroup (base term first) so the conic supplies the intersection initial guess.

Attributes:
grid_z: Per-element height samples (N, H, W) in metres. ``grid_z[n,

j, i]`` is the height of element n at column i (x), row j (y).

x0, y0: Per-element grid origin (N,) — the coordinate of column /

row 0.

dx, dy: Per-element grid spacing (N,) along x / y. offsets: Per-element in-surface decenter (N, 2) (inherited).

__init__(grid_z, x0, y0, dx, dy, offsets=None)[source]
grid_z = <dataclasses._MISSING_TYPE object>
x0 = <dataclasses._MISSING_TYPE object>
y0 = <dataclasses._MISSING_TYPE object>
dx = <dataclasses._MISSING_TYPE object>
dy = <dataclasses._MISSING_TYPE object>
classmethod from_extent(grid_z, half_width, half_height, offsets=None)[source]

Build from a grid centred on the origin spanning a given extent.

The grid columns span [-half_width, half_width] and rows span [-half_height, half_height]. half_width / half_height may be scalar (shared) or per-element (N,).

iactrace.core.surfaces.sag(x, y, offset, curvature, conic, aspheric)[source]

Compute surface sag z(x,y) in local mirror coordinates.

Args:

x: x-coordinate in local mirror frame (scalar) y: y-coordinate in local mirror frame (scalar) offset: (x0, y0) offset on parent surface (2,) curvature: Surface curvature (1/radius) conic: Conic constant k aspheric: Even aspheric coefficients [A4, A6, ...] (K,); entry

i multiplies r^(2i + 4). See sag_raw().

Returns:

z: Surface sag at (x, y) relative to offset point

iactrace.core.surfaces.compute_sag_and_normal(x, y, offset, curvature, conic, aspheric)[source]

Compute surface point and normal at (x, y) with given parameters.

Args:

x: x-coordinate in local mirror frame (scalar) y: y-coordinate in local mirror frame (scalar) offset: (x0, y0) offset on parent surface (2,) curvature: Surface curvature (1/radius) conic: Conic constant k aspheric: Even aspheric coefficients [A4, A6, ...] (K,); entry

i multiplies r^(2i + 4). See sag_raw().

Returns:

point: 3D surface point (3,) normal: Surface normal (3,), normalized

iactrace.core.zernike_terms(u, v)[source]

RMS-normalized Noll Zernike polynomials Z1..Z11.

Evaluated in normalized Cartesian coordinates u = x / r_norm and v = y / r_norm (so the unit disk is u^2 + v^2 <= 1). The terms are written as Cartesian polynomials rather than via (rho, phi) so the gradient is smooth everywhere, including the origin.

With the Noll RMS normalization each term has unit RMS over the unit disk, so a coefficient in metres equals that aberration’s RMS surface contribution in metres. If your surface is not circular, you have to rescale.

Args:

u: Normalized x-coordinate (scalar or array). v: Normalized y-coordinate (scalar or array).

Returns:

Array with the 11 lowest Noll terms stacked on the last axis, in Noll order: piston, tilt x/y, defocus, astigmatism (oblique/vertical), coma (vertical/horizontal), trefoil (vertical/oblique), primary spherical.

iactrace.core.bicubic_interp(grid, u, v)[source]

Bicubic (Catmull-Rom) interpolation of a height grid.

Args:
grid: Height samples (H, W); grid[j, i] is the height at grid

column i (x) and row j (y).

u: Fractional column coordinate (x in grid units), scalar. v: Fractional row coordinate (y in grid units), scalar.

Returns:

Interpolated height (scalar). Exact at grid nodes. Queries outside the grid are clamped to the edge (flat extrapolation), keeping the Newton intersection well-behaved when it strays off the patch.

Intersection Functions

Geometric ray-primitive intersection tests (in iactrace.core.intersections):

iactrace.core.intersections.intersect_plane(ray_origin, ray_direction, plane_center, plane_rotation)[source]

Intersect ray with a plane defined by center and rotation matrix.

Args:

ray_origin: Ray origin (3,) ray_direction: Ray direction (3,), assumed normalized plane_center: Plane center (3,) plane_rotation: Rotation matrix (3, 3) - Z-axis is normal

Returns:

Tuple of (2D coordinates on plane (2,), t parameter (scalar))

iactrace.core.intersections.intersect_sphere(ray_origin, ray_direction, center, radius)[source]

Intersect ray with sphere.

Args:

ray_origin: Ray origin (3,) ray_direction: Ray direction (3,), assumed normalized center: Sphere center (3,) radius: Sphere radius (scalar)

Returns:

t parameter of nearest intersection, jnp.inf if no hit

iactrace.core.intersections.intersect_cylinder(ray_origin, ray_direction, p1, p2, radius)[source]

Intersect ray with a finite cylinder, end caps included.

Args:

ray_origin: Ray origin (3,) ray_direction: Ray direction (3,), assumed normalized p1: First endpoint of cylinder axis (3,) p2: Second endpoint of cylinder axis (3,) radius: Cylinder radius (scalar)

Returns:

t parameter of nearest intersection, jnp.inf if no hit

iactrace.core.intersections.intersect_open_cylinder(ray_origin, ray_direction, p1, p2, radius)[source]

Intersect ray with finite cylinder without end caps (curved surface only).

Args:

ray_origin: Ray origin (3,) ray_direction: Ray direction (3,), assumed normalized p1: First endpoint of cylinder axis (3,) p2: Second endpoint of cylinder axis (3,) radius: Cylinder radius (scalar)

Returns:

t parameter of nearest intersection, jnp.inf if no hit

iactrace.core.intersections.intersect_box(ray_origin, ray_direction, p1, p2)[source]

Intersect ray with AABB box.

Args:

ray_origin: Ray origin (3,) ray_direction: Ray direction (3,), assumed normalized p1: lower edge of the bounding box (3,) p2: upper diagonal edge of the bounding box (3,)

Returns:

t parameter of nearest intersection, jnp.inf if no hit

iactrace.core.intersections.intersect_oriented_box(ray_origin, ray_direction, center, half_extents, rotation)[source]

Intersect ray with oriented bounding box.

Args:

ray_origin: Ray origin (3,) ray_direction: Ray direction (3,), assumed normalized center: Box center (3,) half_extents: Half-sizes along local axes (3,) rotation: Rotation matrix (3, 3) transforming local to world coords

Returns:

t parameter of nearest intersection, jnp.inf if no hit

iactrace.core.intersections.intersect_triangle(ray_origin, ray_direction, v0, v1, v2)[source]

Intersect ray with triangle using Moeller-Trumbore algorithm.

Args:

ray_origin: Ray origin (3,) ray_direction: Ray direction (3,), assumed normalized v0, v1, v2: Triangle vertices (3,) each

Returns:

t parameter of intersection, jnp.inf if no hit

iactrace.core.intersections.intersect_conic(ray_origin, ray_direction, curvature, conic)[source]

Compute closed-form ray-conic intersection parameter.

Args:

ray_origin: Ray origin (3,) ray_direction: Ray direction (3,), assumed normalized curvature: Surface curvature (1/radius) conic: Conic constant (0=sphere, -1=paraboloid, <-1=hyperboloid, >-1=ellipsoid)

Returns:

t: Ray parameter at the nearest forward intersection on the sag branch, inf if there is none.

Obstruction Groups

Classes for modeling ray obstructions:

class iactrace.core.ObstructionGroup[source]

Bases: Module

Base class for grouped obstructions.

Subclasses supply their intersection kernel and stacked parameters via _primitive(); the two traversal strategies are shared from here.

abstractmethod __len__()[source]

Number of obstruction primitives in this group.

intersect(ray_origin, ray_direction)[source]

Returns min t across all primitives in group.

intersect_batch(origins, directions)[source]

Nearest hit distance per ray, for (n_rays, 3) rays.

Same answer as vmap(self.intersect), but chooses how to walk the primitives based on how many rays there are.

__init__()
class iactrace.core.CylinderGroup[source]

Bases: ObstructionGroup

Group of cylinders for efficient batched intersection.

__init__(p1, p2, r)[source]
p1 = <dataclasses._MISSING_TYPE object>
p2 = <dataclasses._MISSING_TYPE object>
r = <dataclasses._MISSING_TYPE object>
class iactrace.core.OpenCylinderGroup[source]

Bases: ObstructionGroup

Group of open cylinders (no end caps) for efficient batched intersection.

An open cylinder is a finite cylindrical surface without circular caps at the ends. Useful for modeling tubes, pipes, or hollow cylindrical structures where rays can pass through the ends.

__init__(p1, p2, r)[source]
p1 = <dataclasses._MISSING_TYPE object>
p2 = <dataclasses._MISSING_TYPE object>
r = <dataclasses._MISSING_TYPE object>
class iactrace.core.BoxGroup[source]

Bases: ObstructionGroup

Group of axis-aligned boxes for efficient batched intersection.

__init__(p1, p2)[source]
p1 = <dataclasses._MISSING_TYPE object>
p2 = <dataclasses._MISSING_TYPE object>
class iactrace.core.SphereGroup[source]

Bases: ObstructionGroup

Group of spheres for efficient batched intersection.

__init__(centers, radii)[source]
centers = <dataclasses._MISSING_TYPE object>
radii = <dataclasses._MISSING_TYPE object>
class iactrace.core.OrientedBoxGroup[source]

Bases: ObstructionGroup

Group of oriented boxes for efficient batched intersection.

__init__(centers, half_extents, rotations)[source]
centers = <dataclasses._MISSING_TYPE object>
half_extents = <dataclasses._MISSING_TYPE object>
rotations = <dataclasses._MISSING_TYPE object>
class iactrace.core.TriangleGroup[source]

Bases: ObstructionGroup

Group of triangles for efficient batched intersection.

__init__(v0, v1, v2)[source]
v0 = <dataclasses._MISSING_TYPE object>
v1 = <dataclasses._MISSING_TYPE object>
v2 = <dataclasses._MISSING_TYPE object>

Transforms

Coordinate transformation utilities:

iactrace.core.euler_to_matrix(tip_tilt_rotation)[source]

Convert Euler angles (degrees) to rotation matrix.

The angles are applied extrinsically, x then y then z:

R = Rz(rz) @ Ry(ry) @ Rx(rx)

Args:

tip_tilt_rotation: Euler angles (rx, ry, rz) in degrees

Returns:

Rotation matrix (3, 3)