Source code for optiland.nonsequential.backends.numpy_backend

"""NumPy CPU backend for Non-Sequential Raytracing.

Owns the forward fast-path Monte Carlo loop via ArrayBackend.

Kramer Harrison, 2026
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

from optiland.nonsequential.backends.array_backend import ArrayBackend
from optiland.nonsequential.rng import NSQRng

if TYPE_CHECKING:
    from optiland.nonsequential.components.base import BaseComponent
    from optiland.nonsequential.ray_bundle import NSQRayBundle


[docs] class NumpyBackend(ArrayBackend): """CPU 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 :meth:`intersect_scene` and RNG. Attributes: rng: Keyed PCG32 RNG (see :mod:`optiland.nonsequential.rng`). seed: RNG seed stored for internal use. """ def __init__(self, seed: int | None = None) -> None: """Initialize NumpyBackend. Args: seed: Optional random seed for reproducibility. """ self.seed = seed self.rng = NSQRng(seed)
[docs] def intersect_scene( self, rays: NSQRayBundle, components: list[BaseComponent], ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Find nearest intersection of each ray with all scene components. Args: rays: Current ray bundle (NumPy arrays). components: List of scene components. Returns: ``(t_min, hit_normals, component_indices, hit_n_geom)``. """ N = rays.num_rays t_min = np.full(N, np.inf) hit_normals = np.zeros((N, 3)) hit_n_geom = np.zeros((N, 3)) comp_indices = np.full(N, -1, dtype=np.int32) for i, comp in enumerate(components): t_c, normals_c, hit_c, n_geom_c = comp.intersect(rays) better = hit_c & (t_c < t_min) t_min = np.where(better, t_c, t_min) hit_normals = np.where(better[:, None], normals_c, hit_normals) hit_n_geom = np.where(better[:, None], n_geom_c, hit_n_geom) comp_indices = np.where(better, i, comp_indices) return t_min, hit_normals, comp_indices, hit_n_geom
def _maybe_compact(self, rays: NSQRayBundle) -> NSQRayBundle: """Compact dead rays after every bounce for the NumPy fast path. Removes rays where ``alive=False`` so subsequent intersection tests skip them, giving a significant speedup when many rays die early. Args: rays: Current ray bundle. Returns: Compacted ray bundle containing only alive rays. """ return rays.compact()