# The Clear BSD License
#
# Copyright (c) 2026 Tobias Heibges
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted (subject to the limitations in the disclaimer
# below) provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Detectors Module - Ray Collection and Measurement
This module provides detector classes for measuring ray arrival times,
positions, angles, and intensities. Useful for simulating experimental
measurements.
Module Structure
----------------
small/
Point detectors (SphericalDetector, PlanarDetector, DirectionalDetector)
extended/
Surface detectors (RecordingSphereDetector, LocalRecordingSphereDetector)
Primary Classes
---------------
DetectorResult : dataclass
Unified bulk numpy-based result container. Primary return type for
all detectors and simulations.
DetectorProtocol : Protocol
Protocol defining the detector interface.
Available Detectors
-------------------
SphericalDetector
Spherical detector for omnidirectional collection.
PlanarDetector
Rectangular planar detector for imaging.
DirectionalDetector
Detector with angular acceptance cone.
RecordingSphereDetector
Spherical surface at altitude for Earth-scale simulations.
LocalRecordingSphereDetector
Spherical surface centered at origin for local simulations.
Analysis Functions
------------------
compute_angular_distribution
Histogram of ray arrival angles.
compute_time_distribution
Histogram of ray arrival times.
compute_intensity_distribution
Histogram of detected intensities.
compute_wavelength_distribution
Histogram of detected wavelengths.
compute_statistics
Summary statistics for detection events.
Interface
---------
All detectors implement the detect() method:
>>> result = detector.detect(rays)
Returns a DetectorResult object with bulk numpy arrays.
For backward compatibility, result.to_detection_events() returns
a list of DetectionEvent objects.
Examples
--------
>>> from lsurf.detectors import SphericalDetector, DetectorResult
>>>
>>> # Create a spherical detector at 100m altitude
>>> sphere = SphericalDetector(
... center=(0, 0, 100),
... radius=10.0,
... name="Far-field detector"
... )
>>> result = sphere.detect(reflected_rays)
>>> print(f"Detected {result.num_rays} rays")
>>> print(f"Total intensity: {result.total_intensity:.3e}")
>>>
>>> # For backward compatibility
>>> events = result.to_detection_events()
"""
# GPU functions are loaded lazily to avoid circular import issues
_detect_multi_spherical_gpu = None
_gpu_loaded = False
def _load_gpu_functions():
"""Lazily load GPU functions to avoid circular import issues."""
global _detect_multi_spherical_gpu, _gpu_loaded
if _gpu_loaded:
return
_gpu_loaded = True
try:
from ..propagation.detector_gpu import (
detect_multi_spherical_gpu,
)
_detect_multi_spherical_gpu = detect_multi_spherical_gpu
except ImportError:
pass
# Import primary result class
from .results import DetectorResult
# Import protocol
from .protocol import (
DetectorProtocol,
AccumulatingDetectorProtocol,
ExtendedDetectorProtocol,
AnyDetector,
)
# Import base classes (for backward compatibility)
from .base import DetectionEvent, Detector
# Import analysis functions
from .analysis import (
compute_angular_distribution,
compute_intensity_distribution,
compute_statistics,
compute_time_distribution,
compute_wavelength_distribution,
)
# Import small detectors
from .small import (
SphericalDetector,
PlanarDetector,
DirectionalDetector,
)
# Import extended detectors
from .extended import (
RecordingSphereDetector,
LocalRecordingSphereDetector,
RecordingSphere,
LocalRecordingSphere,
)
# Import constant-size detector rings
from .constant_size_rings import (
ConstantSizeDetectorRings,
create_default_detector_rings,
)
# Keep backward compatibility imports from old locations
# These are aliases pointing to the new locations
RecordingSphereBase = RecordingSphereDetector # Base class no longer needed
[docs]
def detect_multi_position_gpu(
rays, detector_centers, detector_radius, threads_per_block=256
):
"""
GPU-accelerated detection across multiple spherical detectors.
Wrapper function that accepts a RayBatch object for backward compatibility.
Parameters
----------
rays : RayBatch
Ray batch to detect.
detector_centers : ndarray, shape (M, 3)
Detector center positions.
detector_radius : float
Detector radius (same for all).
threads_per_block : int, optional
CUDA threads per block. Default is 256.
Returns
-------
hit_counts : ndarray, shape (M,)
Number of rays hitting each detector.
hit_intensities : ndarray, shape (M,)
Sum of intensities for rays hitting each detector.
"""
import numpy as np
_load_gpu_functions()
if _detect_multi_spherical_gpu is None:
raise ImportError("GPU detection functions not available")
return _detect_multi_spherical_gpu(
ray_positions=rays.positions.astype(np.float32),
ray_directions=rays.directions.astype(np.float32),
ray_active=rays.active,
ray_times=rays.accumulated_time.astype(np.float32),
ray_intensities=rays.intensities.astype(np.float32),
detector_centers=detector_centers.astype(np.float32),
detector_radius=float(detector_radius),
threads_per_block=threads_per_block,
)
[docs]
def detect_multi_spherical_gpu(*args, **kwargs):
"""GPU-accelerated multi-detector detection. See detect_multi_position_gpu."""
_load_gpu_functions()
if _detect_multi_spherical_gpu is None:
raise ImportError("GPU detection functions not available")
return _detect_multi_spherical_gpu(*args, **kwargs)
__all__ = [
# Primary result class
"DetectorResult",
# Protocol
"DetectorProtocol",
"AccumulatingDetectorProtocol",
"ExtendedDetectorProtocol",
"AnyDetector",
# Base classes (backward compatibility)
"Detector",
"DetectionEvent",
# Small detectors
"SphericalDetector",
"PlanarDetector",
"DirectionalDetector",
# Extended detectors
"RecordingSphereDetector",
"LocalRecordingSphereDetector",
# Backward compatibility aliases
"RecordingSphereBase",
"RecordingSphere",
"LocalRecordingSphere",
# Analysis functions
"compute_angular_distribution",
"compute_time_distribution",
"compute_intensity_distribution",
"compute_wavelength_distribution",
"compute_statistics",
# GPU functions
"detect_multi_spherical_gpu",
"detect_multi_position_gpu",
# Constant-size detector rings
"ConstantSizeDetectorRings",
"create_default_detector_rings",
]