Source code for lsurf.detectors

# 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", ]