Source code for iactrace.core.ray_bundle

from __future__ import annotations

import dataclasses

import equinox as eqx
import jax.numpy as jnp
from jax import Array

from .transforms import euler_to_matrix

DEFAULT_WAVELENGTH = 400.0


[docs] class RayBundle(eqx.Module): """Bundle of rays through the optical system. 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, ready for Camera.collect / Camera.image. By the time the bundle reaches Camera.collect the entries of values are photoelectrons, not raw photons. Attributes ---------- origins : ndarray (n_rays, 3) Ray positions. Meaningful only where alive. directions : ndarray (n_rays, 3) Ray directions. Meaningful only where alive. values : ndarray (n_rays,) Throughput-weighted intensities. path_length : ndarray (n_rays,) Accumulated optical path length, in metres. n : ndarray (n_rays,) Refractive index of the medium each ray is in, carried so downstream consumers can weight the final geometric leg. wavelength : ndarray (n_rays,) Per-ray wavelength. alive : ndarray (n_rays,) Per-ray liveness, all-True at construction. """ origins: Array directions: Array values: Array path_length: Array n: Array wavelength: Array alive: Array
[docs] def __init__( self, origins: Array, directions: Array, values: Array, path_length: Array, n: Array, alive: Array | None = None, wavelength: Array | None = None, ) -> None: self.origins = origins self.directions = directions self.values = values self.path_length = path_length self.n = n self.wavelength = ( jnp.full(values.shape[0], DEFAULT_WAVELENGTH) if wavelength is None else jnp.asarray(wavelength) ) self.alive = ( jnp.ones(values.shape[0], dtype=bool) if alive is None else jnp.asarray(alive, dtype=bool) )
[docs] def replace(self, **changes: Array) -> RayBundle: """Copy with the given fields replaced; unknown names raise TypeError.""" fields = {f.name: getattr(self, f.name) for f in dataclasses.fields(self)} unknown = set(changes) - set(fields) if unknown: raise TypeError(f"RayBundle has no field(s) {sorted(unknown)}") return RayBundle(**{**fields, **changes})
[docs] def to_frame(self, origin: Array, rotation: Array) -> RayBundle: """Transform rays to the frame given by origin + Euler rotation.""" rot = euler_to_matrix(rotation) return self.replace( origins=(self.origins - origin) @ rot, directions=self.directions @ rot, )