Detectors Module

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

DetectorResultdataclass

Unified bulk numpy-based result container. Primary return type for all detectors and simulations.

DetectorProtocolProtocol

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()
class lsurf.detectors.DetectorResult(positions, directions, times, intensities, wavelengths, ray_indices=None, generations=None, polarization_vectors=None, detector_name='unnamed', metadata=<factory>)[source]

Bases: object

Unified bulk numpy-based result container for detector outputs.

This is the primary return type for all detectors and simulations, providing efficient bulk storage and analysis of detected rays.

positions

Intersection positions (meters)

Type:

ndarray, shape (N, 3)

directions

Ray directions at intersection (unit vectors)

Type:

ndarray, shape (N, 3)

times

Time of arrival (seconds)

Type:

ndarray, shape (N,)

intensities

Ray intensity at detection

Type:

ndarray, shape (N,)

wavelengths

Ray wavelength (meters)

Type:

ndarray, shape (N,)

ray_indices

Original ray indices in the source RayBatch

Type:

ndarray, shape (N,), optional

generations

Ray generation (number of surface interactions)

Type:

ndarray, shape (N,), optional

polarization_vectors

3D polarization vectors (electric field direction)

Type:

ndarray, shape (N, 3), optional

detector_name

Name of the detector that produced this result

Type:

str

metadata

Additional metadata (simulation parameters, etc.)

Type:

dict

Examples

>>> result = detector.detect(rays)
>>> print(f"Detected {result.num_rays} rays")
>>> print(f"Total intensity: {result.total_intensity:.3e}")
>>>
>>> # Filter by time window
>>> early = result.filter_by_time(0, 1e-6)
>>>
>>> # Merge multiple results
>>> combined = DetectorResult.merge([result1, result2])
positions: ndarray[tuple[Any, ...], dtype[float32]]
directions: ndarray[tuple[Any, ...], dtype[float32]]
times: ndarray[tuple[Any, ...], dtype[float32]]
intensities: ndarray[tuple[Any, ...], dtype[float32]]
wavelengths: ndarray[tuple[Any, ...], dtype[float32]]
ray_indices: ndarray[tuple[Any, ...], dtype[int32]] | None = None
generations: ndarray[tuple[Any, ...], dtype[int32]] | None = None
polarization_vectors: ndarray[tuple[Any, ...], dtype[float32]] | None = None
detector_name: str = 'unnamed'
metadata: dict[str, Any]
property num_rays: int

Number of detected rays.

property total_intensity: float

Sum of all detected intensities.

property is_empty: bool

Whether the result contains no rays.

compute_statistics()[source]

Compute summary statistics for the detected rays.

Returns:

Dictionary containing: - count: number of rays - total_intensity: sum of intensities - mean_time: average arrival time - std_time: arrival time standard deviation - min_time: earliest arrival - max_time: latest arrival - mean_wavelength: average wavelength - time_spread: max_time - min_time

Return type:

dict

Examples

>>> stats = result.compute_statistics()
>>> print(f"Detected {stats['count']} rays")
>>> print(f"Time spread: {stats['time_spread']:.3e} s")
compute_time_histogram(num_bins=50, time_range=None, weighted=True)[source]

Compute arrival time distribution histogram.

Parameters:
  • num_bins (int) – Number of histogram bins

  • time_range (tuple, optional) – (min, max) time range. If None, uses data range.

  • weighted (bool) – If True, weight by intensity. If False, count rays.

Returns:

  • bin_centers (ndarray) – Bin centers in seconds

  • values (ndarray) – Histogram values (counts or intensity sum per bin)

Return type:

tuple[ndarray[tuple[Any, …], dtype[float64]], ndarray[tuple[Any, …], dtype[float64]]]

Examples

>>> times, counts = result.compute_time_histogram(num_bins=100)
>>> plt.bar(times * 1e9, counts, width=(times[1]-times[0])*1e9)
>>> plt.xlabel('Time (ns)')
compute_angular_distribution(reference_direction, num_bins=50, weighted=True)[source]

Compute angular distribution histogram.

Parameters:
  • reference_direction (ndarray, shape (3,)) – Reference direction for angle calculation

  • num_bins (int) – Number of histogram bins

  • weighted (bool) – If True, weight by intensity. If False, count rays.

Returns:

  • bin_centers (ndarray) – Bin centers in degrees (0-180)

  • values (ndarray) – Histogram values

Return type:

tuple[ndarray[tuple[Any, …], dtype[float64]], ndarray[tuple[Any, …], dtype[float64]]]

Examples

>>> angles, counts = result.compute_angular_distribution(
...     reference_direction=np.array([0, 0, 1])
... )
compute_angular_coordinates(earth_center=None)[source]

Compute angular coordinates for all ray intersection points.

Computes spherical coordinates (latitude/longitude) of intersection points on a detection sphere relative to Earth’s center.

Parameters:

earth_center (ndarray, optional) – Earth center position, default (0, 0, -EARTH_RADIUS)

Returns:

Dictionary with: - ‘elevation’: Latitude angle above equator (radians, -π/2 to π/2) - ‘azimuth’: Longitude angle (radians, -π to π) - ‘zenith’: Zenith angle from north pole (radians, 0 to π) - ‘incidence’: Angle between ray direction and outward radial (radians)

Return type:

dict

Examples

>>> coords = result.compute_angular_coordinates()
>>> elevation_deg = np.degrees(coords['elevation'])
compute_viewing_angle_from_origin(origin=None)[source]

Compute viewing angle from horizontal at specified origin.

Calculates the angle above the horizontal plane (XY plane) when viewing each intersection point from the origin position.

Parameters:

origin (ndarray, optional) – Observer position, default (0, 0, 0)

Returns:

Viewing angle from horizontal in radians (-π/2 to π/2) Positive angles are above horizontal, negative below

Return type:

ndarray

Examples

>>> viewing_angles = result.compute_viewing_angle_from_origin()
>>> print(f"Mean viewing angle: {np.degrees(viewing_angles.mean()):.1f}°")
compute_ray_direction_angles()[source]

Compute elevation and azimuth angles of ray directions.

Returns:

Dictionary with: - ‘elevation’: Angle above horizontal plane in radians (-π/2 to π/2) - ‘azimuth’: Azimuth angle in horizontal plane in radians (-π to π)

Return type:

dict

Examples

>>> angles = result.compute_ray_direction_angles()
>>> print(f"Mean elevation: {np.degrees(angles['elevation'].mean()):.1f}°")
filter(mask)[source]

Filter rays by boolean mask.

Parameters:

mask (ndarray of bool) – Boolean mask, True for rays to keep

Returns:

Filtered result containing only selected rays

Return type:

DetectorResult

Examples

>>> high_intensity = result.filter(result.intensities > 0.1)
filter_by_wavelength(min_wavelength, max_wavelength)[source]

Filter rays by wavelength range.

Parameters:
  • min_wavelength (float) – Minimum wavelength in meters

  • max_wavelength (float) – Maximum wavelength in meters

Returns:

Filtered result

Return type:

DetectorResult

Examples

>>> visible = result.filter_by_wavelength(400e-9, 700e-9)
filter_by_time(min_time, max_time)[source]

Filter rays by time range.

Parameters:
  • min_time (float) – Minimum time in seconds

  • max_time (float) – Maximum time in seconds

Returns:

Filtered result

Return type:

DetectorResult

Examples

>>> early_arrivals = result.filter_by_time(0, 1e-6)
filter_by_intensity(min_intensity=0.0, max_intensity=inf)[source]

Filter rays by intensity range.

Parameters:
  • min_intensity (float) – Minimum intensity

  • max_intensity (float) – Maximum intensity

Returns:

Filtered result

Return type:

DetectorResult

Examples

>>> bright = result.filter_by_intensity(min_intensity=0.1)
classmethod empty(detector_name='unnamed')[source]

Create an empty DetectorResult.

Parameters:

detector_name (str) – Name for the detector

Returns:

Empty result with zero rays

Return type:

DetectorResult

Examples

>>> empty = DetectorResult.empty("my_detector")
>>> print(empty.is_empty)  # True
classmethod merge(results)[source]

Merge multiple DetectorResults into one.

Parameters:

results (list of DetectorResult) – Results to merge

Returns:

Combined result

Return type:

DetectorResult

Examples

>>> combined = DetectorResult.merge([result1, result2, result3])
save_npz(filepath)[source]

Save to numpy .npz file.

Parameters:

filepath (str or Path) – Output file path

Examples

>>> result.save_npz("detections.npz")
classmethod load_npz(filepath)[source]

Load from numpy .npz file.

Parameters:

filepath (str or Path) – Input file path

Returns:

Loaded result

Return type:

DetectorResult

Examples

>>> result = DetectorResult.load_npz("detections.npz")
save_hdf5(filepath, compression='gzip')[source]

Save to HDF5 file.

Parameters:
  • filepath (str or Path) – Output file path

  • compression (str) – Compression algorithm (‘gzip’, ‘lzf’, or None)

Examples

>>> result.save_hdf5("detections.h5")
classmethod load_hdf5(filepath)[source]

Load from HDF5 file.

Parameters:

filepath (str or Path) – Input file path

Returns:

Loaded result

Return type:

DetectorResult

Examples

>>> result = DetectorResult.load_hdf5("detections.h5")
to_detection_events()[source]

Convert to list of DetectionEvent objects for backward compatibility.

Returns:

List of individual detection events

Return type:

list of DetectionEvent

Examples

>>> events = result.to_detection_events()
>>> for event in events:
...     print(f"Ray {event.ray_index}: {event.intensity:.3f}")
classmethod from_detection_events(events, detector_name='unnamed', generations=None, polarization_vectors=None)[source]

Create from list of DetectionEvent objects for backward compatibility.

Parameters:
  • events (list of DetectionEvent) – List of detection events

  • detector_name (str) – Detector name

  • generations (ndarray, optional) – Ray generations if available

  • polarization_vectors (ndarray, optional) – Polarization vectors if available

Returns:

Converted result

Return type:

DetectorResult

Examples

>>> result = DetectorResult.from_detection_events(detector.events)
to_recorded_rays()[source]

Convert to RecordedRays for backward compatibility.

Returns:

Converted RecordedRays object

Return type:

RecordedRays

Notes

This is provided for backward compatibility during migration. New code should use DetectorResult directly.

classmethod from_recorded_rays(recorded, detector_name='unnamed', ray_indices=None)[source]

Create from RecordedRays for backward compatibility.

Parameters:
  • recorded (RecordedRays) – RecordedRays object to convert

  • detector_name (str) – Detector name

  • ray_indices (ndarray, optional) – Original ray indices if available

Returns:

Converted result

Return type:

DetectorResult

Notes

This is provided for backward compatibility during migration. New code should use DetectorResult directly.

__repr__()[source]

Return string representation.

__len__()[source]

Return number of detected rays.

__iter__()[source]

Iterate over detection events for backward compatibility.

Yields DetectionEvent objects for each detected ray. New code should access arrays directly instead.

__getitem__(index)[source]

Get a single detection event by index for backward compatibility.

Parameters:

index (int) – Index of the detection event

Returns:

Detection event at the specified index

Return type:

DetectionEvent

__init__(positions, directions, times, intensities, wavelengths, ray_indices=None, generations=None, polarization_vectors=None, detector_name='unnamed', metadata=<factory>)
class lsurf.detectors.DetectorProtocol(*args, **kwargs)[source]

Bases: Protocol

Protocol defining the interface for all detector implementations.

All detectors (small/point detectors and extended/surface detectors) should implement this protocol for consistent behavior.

name

Human-readable detector name for identification

Type:

str

detect(rays) DetectorResult[source]

Detect rays and return results

clear()[source]

Clear accumulated detection data

Examples

>>> class MyDetector:
...     def __init__(self, name: str = "My Detector"):
...         self.name = name
...         self._result = DetectorResult.empty(name)
...
...     def detect(self, rays: RayBatch) -> DetectorResult:
...         # Detection logic here
...         return self._result
...
...     def clear(self) -> None:
...         self._result = DetectorResult.empty(self.name)
name: str
detect(rays)[source]

Detect rays and return detection results.

Parameters:

rays (RayBatch) – Ray batch to test for detection

Returns:

Detection results containing all rays that hit this detector

Return type:

DetectorResult

clear()[source]

Clear accumulated detection data.

Resets the detector to its initial state with no recorded detections.

__init__(*args, **kwargs)
class lsurf.detectors.AccumulatingDetectorProtocol(*args, **kwargs)[source]

Bases: DetectorProtocol, Protocol

Protocol for detectors that accumulate results over multiple detect() calls.

These detectors maintain internal state and can return cumulative results.

name

Human-readable detector name

Type:

str

accumulated_result

All accumulated detections since last clear()

Type:

DetectorResult

Examples

>>> detector = SphericalDetector(center=(0, 0, 100), radius=10)
>>> result1 = detector.detect(rays1)
>>> result2 = detector.detect(rays2)
>>> total = detector.accumulated_result  # Contains both result1 and result2
>>> detector.clear()  # Reset accumulation
accumulated_result: DetectorResult
class lsurf.detectors.ExtendedDetectorProtocol(*args, **kwargs)[source]

Bases: DetectorProtocol, Protocol

Protocol for extended (surface) detectors with additional geometric properties.

Extended detectors have a defined geometric surface and can provide additional information about the detection geometry.

name

Human-readable detector name

Type:

str

sphere_radius

Radius of the detection sphere (for spherical detectors)

Type:

float

center

Center position of the detector

Type:

ndarray, shape (3,)

Examples

>>> from lsurf.detectors import RecordingSphereDetector
>>> detector = RecordingSphereDetector(altitude=33000.0)
>>> print(detector.sphere_radius)  # Earth radius + altitude
property sphere_radius: float

Radius of the detection sphere in meters.

property center: NDArray

Center position of the detector.

class lsurf.detectors.Detector(name='Detector')[source]

Bases: ABC

Abstract base class for all detectors.

A detector records rays that pass through its detection volume, capturing their positions, directions, timing, and other properties.

Parameters:

name (str, optional) – Detector name for identification. Default is “Detector”.

name

Detector identifier.

Type:

str

events

Recorded detection events.

Type:

list of DetectionEvent

Notes

Derived classes must implement the detect() method which tests rays against the detector geometry and records detection events.

The base class provides common analysis methods for processing the recorded events.

Examples

Creating a custom detector:

>>> class MyDetector(Detector):
...     def __init__(self, position, radius, name="MyDetector"):
...         super().__init__(name)
...         self.position = np.array(position)
...         self.radius = radius
...
...     def detect(self, rays, current_time=0.0):
...         events = []
...         # ... detection logic ...
...         return events
__init__(name='Detector')[source]

Initialize detector.

Parameters:

name (str, optional) – Detector name for identification. Default is “Detector”.

abstractmethod detect(rays, current_time=0.0)[source]

Check which rays intersect this detector and record events.

Parameters:
  • rays (RayBatch) – Ray batch to test for detection.

  • current_time (float, optional) – Current simulation time for reference. Default is 0.0.

Returns:

List of newly detected events.

Return type:

list of DetectionEvent

Notes

Implementations should: - Only test active rays (rays.active == True) - Create DetectionEvent for each detected ray - Append events to self.events - Return the list of newly created events

clear()[source]

Clear all recorded events.

Resets the detector to its initial state with no recorded events.

get_arrival_times()[source]

Get array of all arrival times.

Returns:

times – Arrival times in seconds for all detected rays.

Return type:

ndarray, shape (N,)

Examples

>>> times = detector.get_arrival_times()
>>> print(f"Mean arrival time: {times.mean():.3e} s")
get_arrival_angles(reference_direction)[source]

Get angles between ray directions and reference direction.

Parameters:

reference_direction (ndarray, shape (3,)) – Reference vector for angle calculation.

Returns:

angles – Angles in radians between each detected ray’s direction and the reference direction.

Return type:

ndarray, shape (N,)

Examples

>>> angles = detector.get_arrival_angles(np.array([0, 0, 1]))
>>> print(f"Mean angle: {np.degrees(angles.mean()):.1f} degrees")
get_intensities()[source]

Get array of all detected intensities.

Returns:

intensities – Intensity values for all detected rays.

Return type:

ndarray, shape (N,)

get_wavelengths()[source]

Get array of all detected wavelengths.

Returns:

wavelengths – Wavelengths in meters for all detected rays.

Return type:

ndarray, shape (N,)

get_positions()[source]

Get array of all detection positions.

Returns:

positions – 3D positions where rays were detected.

Return type:

ndarray, shape (N, 3)

get_total_intensity()[source]

Get sum of all detected intensities.

Returns:

Total detected intensity (power).

Return type:

float

__len__()[source]

Return number of detection events.

__repr__()[source]

Return string representation.

class lsurf.detectors.DetectionEvent(ray_index, position, direction, time, wavelength, intensity)[source]

Bases: object

Represents a single ray detection event.

Immutable record of a ray being detected, including its position, direction, timing, and intensity at the moment of detection.

Parameters:
  • ray_index (int) – Index of the ray in the original RayBatch.

  • position (ndarray, shape (3,)) – 3D position where ray hit detector in meters.

  • direction (ndarray, shape (3,)) – Ray direction at detection (unit vector).

  • time (float) – Arrival time at detector in seconds.

  • wavelength (float) – Ray wavelength in meters.

  • intensity (float) – Ray intensity at detection.

Notes

This class is frozen (immutable) for thread safety and to prevent accidental modification of detection records.

ray_index: int
position: ndarray[tuple[Any, ...], dtype[float32]]
direction: ndarray[tuple[Any, ...], dtype[float32]]
time: float
wavelength: float
intensity: float
__init__(ray_index, position, direction, time, wavelength, intensity)
class lsurf.detectors.SphericalDetector(center, radius, name='Spherical Detector', use_gpu=True)[source]

Bases: object

Spherical detector centered at a point.

Detects all rays that pass within a certain radius of the center point. Good for collecting rays from all directions without directional bias.

Parameters:
  • center (tuple of float) – Center position (x, y, z) in meters.

  • radius (float) – Detection radius in meters.

  • name (str, optional) – Detector name. Default is “Spherical Detector”.

  • use_gpu (bool, optional) – Whether to use GPU acceleration when available. Default is True.

center

Detector center position.

Type:

ndarray, shape (3,)

radius

Detection radius.

Type:

float

use_gpu

GPU acceleration flag.

Type:

bool

name

Detector name.

Type:

str

accumulated_result

All accumulated detections since last clear().

Type:

DetectorResult

Notes

Detection is based on the closest approach distance between each ray and the center point. A ray is detected if this distance is less than or equal to the detection radius.

The arrival time is computed assuming the ray travels through air (n approx 1.0) from its current position to the detection point.

Examples

>>> detector = SphericalDetector(
...     center=(0, 0, 100),
...     radius=10.0,
...     use_gpu=True
... )
>>> result = detector.detect(reflected_rays)
>>> print(f"Detected {result.num_rays} rays")
__init__(center, radius, name='Spherical Detector', use_gpu=True)[source]

Initialize spherical detector.

Parameters:
  • center (tuple of float) – Center position (x, y, z) in meters.

  • radius (float) – Detection radius in meters.

  • name (str, optional) – Detector name. Default is “Spherical Detector”.

  • use_gpu (bool, optional) – Whether to use GPU acceleration. Default is True.

property accumulated_result: DetectorResult

All accumulated detections since last clear().

property events: list[DetectionEvent]

list of DetectionEvent objects.

For new code, use accumulated_result instead.

Type:

Backward compatibility

detect(rays, current_time=0.0, accumulate=True)[source]

Detect rays that pass within detection radius.

For each ray, find the closest approach to center. If within radius, record a detection event.

Parameters:
  • rays (RayBatch) – Ray batch to test.

  • current_time (float, optional) – Current simulation time. Default is 0.0.

  • accumulate (bool, optional) – Whether to accumulate results. Default is True.

Returns:

Newly detected rays.

Return type:

DetectorResult

clear()[source]

Clear all recorded detections.

Resets the detector to its initial state with no recorded events.

__repr__()[source]

Return string representation.

__len__()[source]

Return number of detected rays.

get_arrival_times()[source]

Get array of all arrival times.

Returns:

times – Arrival times in seconds for all detected rays.

Return type:

ndarray, shape (N,)

get_arrival_angles(reference_direction)[source]

Get angles between ray directions and reference direction.

Parameters:

reference_direction (ndarray, shape (3,)) – Reference vector for angle calculation.

Returns:

angles – Angles in radians.

Return type:

ndarray, shape (N,)

get_intensities()[source]

Get array of all detected intensities.

Returns:

intensities – Intensity values for all detected rays.

Return type:

ndarray, shape (N,)

get_wavelengths()[source]

Get array of all detected wavelengths.

Returns:

wavelengths – Wavelengths in meters.

Return type:

ndarray, shape (N,)

get_positions()[source]

Get array of all detection positions.

Returns:

positions – 3D positions where rays were detected.

Return type:

ndarray, shape (N, 3)

get_total_intensity()[source]

Get sum of all detected intensities.

Returns:

Total detected intensity.

Return type:

float

class lsurf.detectors.PlanarDetector(center, normal, width, height, name='Planar Detector')[source]

Bases: object

Planar detector with finite rectangular size.

Detects rays that intersect a rectangular plane at a specific position and orientation. Useful for imaging applications and beam profiling.

Parameters:
  • center (tuple of float) – Center position of detector plane (x, y, z) in meters.

  • normal (tuple of float) – Normal vector (defines which direction detector faces).

  • width (float) – Width of detector (in local u direction) in meters.

  • height (float) – Height of detector (in local v direction) in meters.

  • name (str, optional) – Detector name. Default is “Planar Detector”.

center

Detector center position.

Type:

ndarray, shape (3,)

normal

Unit normal vector.

Type:

ndarray, shape (3,)

width

Detector width.

Type:

float

height

Detector height.

Type:

float

u

Local x-axis direction (width direction).

Type:

ndarray, shape (3,)

v

Local y-axis direction (height direction).

Type:

ndarray, shape (3,)

name

Detector name.

Type:

str

accumulated_result

All accumulated detections since last clear().

Type:

DetectorResult

Notes

The local coordinate system is constructed with: - normal: direction the detector faces - u: perpendicular to normal (width direction) - v: perpendicular to both normal and u (height direction)

Only rays traveling toward the front face of the detector (opposite to the normal direction) are detected.

Examples

>>> detector = PlanarDetector(
...     center=(0, 0, 50),
...     normal=(0, 0, -1),  # Facing -z direction
...     width=0.1,
...     height=0.1
... )
>>> result = detector.detect(rays)
__init__(center, normal, width, height, name='Planar Detector')[source]

Initialize planar detector.

Parameters:
  • center (tuple of float) – Center position of detector plane.

  • normal (tuple of float) – Normal vector (detector faces in this direction).

  • width (float) – Detector width in meters.

  • height (float) – Detector height in meters.

  • name (str, optional) – Detector name.

property accumulated_result: DetectorResult

All accumulated detections since last clear().

property events: list[DetectionEvent]

list of DetectionEvent objects.

For new code, use accumulated_result instead.

Type:

Backward compatibility

detect(rays, current_time=0.0, accumulate=True)[source]

Detect rays that intersect the detector plane within bounds.

Parameters:
  • rays (RayBatch) – Ray batch to test.

  • current_time (float, optional) – Current simulation time. Default is 0.0.

  • accumulate (bool, optional) – Whether to accumulate results. Default is True.

Returns:

Newly detected rays.

Return type:

DetectorResult

Notes

Only detects rays that: - Are traveling toward the front face (opposite to normal) - Intersect the plane in the forward direction - Hit within the width x height bounds

clear()[source]

Clear all recorded detections.

Resets the detector to its initial state with no recorded events.

get_image(bins_u=100, bins_v=100)[source]

Generate intensity image from detection events.

Parameters:
  • bins_u (int, optional) – Number of bins in u direction. Default is 100.

  • bins_v (int, optional) – Number of bins in v direction. Default is 100.

Returns:

  • u_centers (ndarray, shape (bins_u,)) – Bin centers in u direction (meters).

  • v_centers (ndarray, shape (bins_v,)) – Bin centers in v direction (meters).

  • image (ndarray, shape (bins_v, bins_u)) – Intensity image (sum of intensities per bin).

Return type:

tuple[ndarray[tuple[Any, …], dtype[float64]], ndarray[tuple[Any, …], dtype[float64]], ndarray[tuple[Any, …], dtype[float64]]]

__repr__()[source]

Return string representation.

__len__()[source]

Return number of detected rays.

get_arrival_times()[source]

Get array of all arrival times.

get_arrival_angles(reference_direction)[source]

Get angles between ray directions and reference direction.

get_intensities()[source]

Get array of all detected intensities.

get_wavelengths()[source]

Get array of all detected wavelengths.

get_positions()[source]

Get array of all detection positions.

get_total_intensity()[source]

Get sum of all detected intensities.

class lsurf.detectors.DirectionalDetector(position, direction, acceptance_angle, radius, name='Directional Detector')[source]

Bases: object

Detector with angular acceptance cone.

Detects rays that pass within a specified radius AND arrive within a specified angular acceptance cone. Useful for modeling detectors with limited field of view such as telescopes or fiber couplers.

Parameters:
  • position (tuple of float) – Detector position (x, y, z) in meters.

  • direction (tuple of float) – Direction detector is pointing (acceptance cone axis).

  • acceptance_angle (float) – Half-angle of acceptance cone in radians.

  • radius (float) – Detection radius at position in meters.

  • name (str, optional) – Detector name. Default is “Directional Detector”.

position

Detector position.

Type:

ndarray, shape (3,)

direction

Unit vector pointing in detector viewing direction.

Type:

ndarray, shape (3,)

acceptance_angle

Acceptance cone half-angle in radians.

Type:

float

radius

Detection radius.

Type:

float

name

Detector name.

Type:

str

accumulated_result

All accumulated detections since last clear().

Type:

DetectorResult

Notes

A ray is detected if: 1. Its closest approach to the detector position is within radius 2. The angle between the ray direction and the detector direction

(considering the ray as incoming) is within acceptance_angle

Examples

>>> # 10-degree acceptance cone detector
>>> detector = DirectionalDetector(
...     position=(0, 0, 100),
...     direction=(0, 0, -1),
...     acceptance_angle=np.radians(10),
...     radius=5.0
... )
>>> result = detector.detect(rays)
>>> print(f"Detected {result.num_rays} rays within acceptance cone")
__init__(position, direction, acceptance_angle, radius, name='Directional Detector')[source]

Initialize directional detector.

Parameters:
  • position (tuple of float) – Detector position in meters.

  • direction (tuple of float) – Direction detector is pointing.

  • acceptance_angle (float) – Acceptance cone half-angle in radians.

  • radius (float) – Detection radius in meters.

  • name (str, optional) – Detector name.

property accumulated_result: DetectorResult

All accumulated detections since last clear().

property events: list[DetectionEvent]

list of DetectionEvent objects.

For new code, use accumulated_result instead.

Type:

Backward compatibility

detect(rays, current_time=0.0, accumulate=True)[source]

Detect rays within acceptance cone and detection radius.

Parameters:
  • rays (RayBatch) – Ray batch to test.

  • current_time (float, optional) – Current simulation time. Default is 0.0.

  • accumulate (bool, optional) – Whether to accumulate results. Default is True.

Returns:

Newly detected rays.

Return type:

DetectorResult

clear()[source]

Clear all recorded detections.

Resets the detector to its initial state with no recorded events.

__repr__()[source]

Return string representation.

__len__()[source]

Return number of detected rays.

get_arrival_times()[source]

Get array of all arrival times.

get_arrival_angles(reference_direction)[source]

Get angles between ray directions and reference direction.

get_intensities()[source]

Get array of all detected intensities.

get_wavelengths()[source]

Get array of all detected wavelengths.

get_positions()[source]

Get array of all detection positions.

get_total_intensity()[source]

Get sum of all detected intensities.

class lsurf.detectors.RecordingSphereDetector(altitude=33000.0, earth_center=(0, 0, -6371000.0), earth_radius=6371000.0, name='Recording Sphere')[source]

Bases: object

Spherical detection surface at a specified altitude above Earth.

Records all rays that intersect the sphere, capturing full ray state for later analysis. Used for Earth-scale simulations where the sphere is centered on Earth’s center.

Parameters:
  • altitude (float) – Altitude above Earth’s surface in meters (default 33 km)

  • earth_center (tuple of float) – Center of Earth, default (0, 0, -EARTH_RADIUS)

  • earth_radius (float) – Earth radius in meters

  • name (str) – Detector name for identification

altitude

Altitude above Earth’s surface in meters.

Type:

float

sphere_radius

Radius of detection sphere (earth_radius + altitude).

Type:

float

center

Center position (Earth’s center).

Type:

ndarray, shape (3,)

name

Detector name.

Type:

str

accumulated_result

All accumulated detections since last clear().

Type:

DetectorResult

Notes

The recording sphere has radius = earth_radius + altitude, centered at earth_center. This creates a sphere that surrounds Earth at the specified altitude.

Examples

>>> # Detector at 33 km altitude
>>> detector = RecordingSphereDetector(altitude=33000.0)
>>> result = detector.detect(rays)
>>>
>>> # Access angular coordinates
>>> coords = result.compute_angular_coordinates()
__init__(altitude=33000.0, earth_center=(0, 0, -6371000.0), earth_radius=6371000.0, name='Recording Sphere')[source]

Initialize recording sphere detector.

Parameters:
  • altitude (float) – Altitude above Earth’s surface in meters.

  • earth_center (tuple of float) – Center of Earth.

  • earth_radius (float) – Earth radius in meters.

  • name (str) – Detector name.

property sphere_radius: float

Radius of the detection sphere in meters.

property center: ndarray[tuple[Any, ...], dtype[float64]]

Center position of the sphere (Earth’s center).

property accumulated_result: DetectorResult

All accumulated detections since last clear().

property events: list[DetectionEvent]

list of DetectionEvent objects.

For new code, use accumulated_result instead.

Type:

Backward compatibility

detect(rays, current_time=0.0, accumulate=True, compute_travel_time=True, speed_of_light=299792458.0, max_propagation_distance=None)[source]

Detect rays intersecting the recording sphere.

Parameters:
  • rays (RayBatch) – Rays to detect

  • current_time (float) – Current simulation time (unused, for interface compatibility)

  • accumulate (bool) – Whether to accumulate results. Default is True.

  • compute_travel_time (bool) – If True, add travel time to intersection to ray’s accumulated time

  • speed_of_light (float) – Speed of light for time computation

  • max_propagation_distance (float, optional) – Maximum distance rays can propagate before detection (meters). If None, no limit is applied.

Returns:

Detection results for all intersecting rays

Return type:

DetectorResult

detect_rays(rays, compute_travel_time=True, speed_of_light=299792458.0, max_propagation_distance=None)[source]

Detect rays (alias for detect with accumulate=False).

This method is provided for backward compatibility with code that used the detect_rays() method.

Parameters:
  • rays (RayBatch) – Rays to detect

  • compute_travel_time (bool) – If True, add travel time to intersection

  • speed_of_light (float) – Speed of light for time computation

  • max_propagation_distance (float, optional) – Maximum distance rays can propagate

Returns:

Detection results

Return type:

DetectorResult

clear()[source]

Clear all recorded detections.

Resets the detector to its initial state with no recorded events.

__repr__()[source]

Return string representation.

__len__()[source]

Return number of detected rays.

get_arrival_times()[source]

Get array of all arrival times.

get_arrival_angles(reference_direction)[source]

Get angles between ray directions and reference direction.

get_intensities()[source]

Get array of all detected intensities.

get_wavelengths()[source]

Get array of all detected wavelengths.

get_positions()[source]

Get array of all detection positions.

get_total_intensity()[source]

Get sum of all detected intensities.

class lsurf.detectors.LocalRecordingSphereDetector(radius=33000.0, center=(0, 0, 0), name='Local Recording Sphere')[source]

Bases: object

Spherical detection surface centered at a specified position.

Records all rays that intersect the sphere, useful for local-scale simulations without Earth curvature effects.

Parameters:
  • radius (float) – Sphere radius in meters (default 33 km)

  • center (tuple of float) – Center position (default (0, 0, 0))

  • name (str) – Detector name for identification

sphere_radius

Radius of detection sphere in meters.

Type:

float

center

Center position of the sphere.

Type:

ndarray, shape (3,)

name

Detector name.

Type:

str

accumulated_result

All accumulated detections since last clear().

Type:

DetectorResult

Examples

>>> # Local detector at origin with 33 km radius
>>> detector = LocalRecordingSphereDetector(radius=33000.0)
>>> result = detector.detect(rays)
>>>
>>> # Detector at specific location
>>> detector = LocalRecordingSphereDetector(
...     radius=10000.0,
...     center=(1000.0, 0.0, 500.0)
... )
__init__(radius=33000.0, center=(0, 0, 0), name='Local Recording Sphere')[source]

Initialize local recording sphere detector.

Parameters:
  • radius (float) – Sphere radius in meters.

  • center (tuple of float) – Center position.

  • name (str) – Detector name.

property sphere_radius: float

Radius of the detection sphere in meters.

property center: ndarray[tuple[Any, ...], dtype[float64]]

Center position of the sphere.

property accumulated_result: DetectorResult

All accumulated detections since last clear().

property events: list[DetectionEvent]

list of DetectionEvent objects.

For new code, use accumulated_result instead.

Type:

Backward compatibility

detect(rays, current_time=0.0, accumulate=True, compute_travel_time=True, speed_of_light=299792458.0)[source]

Detect rays intersecting the recording sphere.

Parameters:
  • rays (RayBatch) – Rays to detect

  • current_time (float) – Current simulation time (unused, for interface compatibility)

  • accumulate (bool) – Whether to accumulate results. Default is True.

  • compute_travel_time (bool) – If True, add travel time to intersection to ray’s accumulated time

  • speed_of_light (float) – Speed of light for time computation

Returns:

Detection results for all intersecting rays

Return type:

DetectorResult

detect_rays(rays, compute_travel_time=True, speed_of_light=299792458.0)[source]

Detect rays (alias for detect with accumulate=False).

This method is provided for backward compatibility with code that used the detect_rays() method.

Parameters:
  • rays (RayBatch) – Rays to detect

  • compute_travel_time (bool) – If True, add travel time to intersection

  • speed_of_light (float) – Speed of light for time computation

Returns:

Detection results

Return type:

DetectorResult

record_rays(rays)[source]

Record rays (backward compatibility alias for detect).

Parameters:

rays (RayBatch) – Rays to record

Returns:

Detection results

Return type:

DetectorResult

clear()[source]

Clear all recorded detections.

Resets the detector to its initial state with no recorded events.

__repr__()[source]

Return string representation.

__len__()[source]

Return number of detected rays.

get_arrival_times()[source]

Get array of all arrival times.

get_arrival_angles(reference_direction)[source]

Get angles between ray directions and reference direction.

get_intensities()[source]

Get array of all detected intensities.

get_wavelengths()[source]

Get array of all detected wavelengths.

get_positions()[source]

Get array of all detection positions.

get_total_intensity()[source]

Get sum of all detected intensities.

lsurf.detectors.RecordingSphereBase

alias of RecordingSphereDetector

lsurf.detectors.RecordingSphere

alias of RecordingSphereDetector

lsurf.detectors.LocalRecordingSphere

alias of LocalRecordingSphereDetector

lsurf.detectors.compute_angular_distribution(events, reference_direction, num_bins=50)[source]

Compute angular distribution histogram.

Parameters:
  • events (list of DetectionEvent) – Detection events to analyze.

  • reference_direction (ndarray, shape (3,)) – Reference direction for angle calculation.

  • num_bins (int, optional) – Number of histogram bins. Default is 50.

Returns:

  • bin_centers (ndarray, shape (num_bins,)) – Bin centers in degrees.

  • counts (ndarray, shape (num_bins,)) – Number of events in each bin.

Return type:

tuple[ndarray[tuple[Any, …], dtype[float64]], ndarray[tuple[Any, …], dtype[int64]]]

Examples

>>> angles, counts = compute_angular_distribution(
...     detector.events,
...     reference_direction=np.array([0, 0, 1]),
...     num_bins=90  # 2-degree bins
... )
>>> import matplotlib.pyplot as plt
>>> plt.bar(angles, counts, width=2)
>>> plt.xlabel('Angle (degrees)')
>>> plt.ylabel('Count')
lsurf.detectors.compute_time_distribution(events, num_bins=50, time_range=None)[source]

Compute arrival time distribution histogram.

Parameters:
  • events (list of DetectionEvent) – Detection events to analyze.

  • num_bins (int, optional) – Number of histogram bins. Default is 50.

  • time_range (tuple of float, optional) – (min, max) time range for histogram. If None, uses data range.

Returns:

  • bin_centers (ndarray, shape (N,)) – Bin centers in seconds.

  • counts (ndarray, shape (N,)) – Number of events in each bin.

Return type:

tuple[ndarray[tuple[Any, …], dtype[float64]], ndarray[tuple[Any, …], dtype[int64]]]

Notes

Automatically handles cases where all times are very similar by reducing the number of bins to avoid empty histograms.

Examples

>>> times, counts = compute_time_distribution(
...     detector.events,
...     num_bins=100
... )
>>> print(f"Time spread: {times.max() - times.min():.3e} s")
lsurf.detectors.compute_intensity_distribution(events, num_bins=50)[source]

Compute intensity distribution histogram.

Parameters:
  • events (list of DetectionEvent) – Detection events to analyze.

  • num_bins (int, optional) – Number of histogram bins. Default is 50.

Returns:

  • bin_centers (ndarray, shape (num_bins,)) – Bin centers (intensity values).

  • counts (ndarray, shape (num_bins,)) – Number of events in each bin.

Return type:

tuple[ndarray[tuple[Any, …], dtype[float64]], ndarray[tuple[Any, …], dtype[int64]]]

lsurf.detectors.compute_wavelength_distribution(events, num_bins=50)[source]

Compute wavelength distribution histogram.

Parameters:
  • events (list of DetectionEvent) – Detection events to analyze.

  • num_bins (int, optional) – Number of histogram bins. Default is 50.

Returns:

  • bin_centers (ndarray, shape (num_bins,)) – Bin centers in meters.

  • counts (ndarray, shape (num_bins,)) – Number of events in each bin.

Return type:

tuple[ndarray[tuple[Any, …], dtype[float64]], ndarray[tuple[Any, …], dtype[int64]]]

lsurf.detectors.compute_statistics(events)[source]

Compute summary statistics for detection events.

Parameters:

events (list of DetectionEvent) – Detection events to analyze.

Returns:

stats – Dictionary containing: - count: number of events - total_intensity: sum of intensities - mean_time: average arrival time - std_time: arrival time standard deviation - min_time: earliest arrival - max_time: latest arrival - mean_wavelength: average wavelength - time_spread: max_time - min_time

Return type:

dict

Examples

>>> stats = compute_statistics(detector.events)
>>> print(f"Detected {stats['count']} rays")
>>> print(f"Total intensity: {stats['total_intensity']:.3e}")
>>> print(f"Time spread: {stats['time_spread']:.3e} s")
lsurf.detectors.detect_multi_spherical_gpu(*args, **kwargs)[source]

GPU-accelerated multi-detector detection. See detect_multi_position_gpu.

lsurf.detectors.detect_multi_position_gpu(rays, detector_centers, detector_radius, threads_per_block=256)[source]

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.

class lsurf.detectors.ConstantSizeDetectorRings(detector_radial_size=10000.0, detector_altitude=33000.0, max_elevation_deg=90.0, min_elevation_deg=-2.0, earth_radius=6371000.0, ring_boundaries_deg=None, ring_centers_deg=None, ring_distances=None, n_rings=0, detector_sphere_radius=0.0)[source]

Bases: object

Constant-size detector ring geometry with no shadowing.

Creates a set of annular detector rings centered on a sphere at fixed altitude above Earth’s surface. Each ring has constant physical radial width, with angular size varying based on distance from origin.

Adjacent rings touch exactly (no overlap, no gaps) when viewed from origin, ensuring complete angular coverage with no shadowing.

Parameters:
  • detector_radial_size (float) – Physical radial width of each detector in meters (default: 10 km)

  • detector_altitude (float) – Altitude of detector sphere above Earth’s surface in meters (default: 33 km)

  • max_elevation_deg (float) – Maximum elevation angle from horizontal (90° = zenith) (default: 90°)

  • min_elevation_deg (float) – Minimum elevation angle, where to stop generating rings (default: -2°)

  • earth_radius (float) – Earth radius in meters (default: EARTH_RADIUS constant)

ring_boundaries_deg

Elevation angles of ring boundaries (N+1 values for N rings)

Type:

ndarray

ring_centers_deg

Elevation angles of ring centers (N values)

Type:

ndarray

ring_distances

Distances from origin to ring centers in meters (N values)

Type:

ndarray

n_rings

Number of detector rings

Type:

int

detector_sphere_radius

Radius of detector sphere from Earth center in meters

Type:

float

Examples

>>> rings = ConstantSizeDetectorRings(
...     detector_radial_size=10000.0,  # 10 km
...     detector_altitude=33000.0,      # 33 km
... )
>>> print(f"Created {rings.n_rings} rings")
Created 17 rings
>>> print(f"Coverage: {rings.ring_boundaries_deg[-1]:.1f}° to {rings.ring_boundaries_deg[0]:.1f}°")
Coverage: -2.3° to 90.0°
detector_radial_size: float = 10000.0
detector_altitude: float = 33000.0
max_elevation_deg: float = 90.0
min_elevation_deg: float = -2.0
earth_radius: float = 6371000.0
ring_boundaries_deg: ndarray = None
ring_centers_deg: ndarray = None
ring_distances: ndarray = None
n_rings: int = 0
detector_sphere_radius: float = 0.0
__post_init__()[source]

Compute ring geometry after initialization.

property detector_half_width: float

Physical half-width of each detector in meters.

distance_at_elevation(elev_deg)[source]

Compute distance from origin (0,0,0) to detector sphere at given elevation.

Uses the formula:

d(θ) = -sin(θ)·R_E + √(R_d² - R_E²·cos²(θ))

where R_E = Earth radius, R_d = detector sphere radius, θ = elevation angle.

Parameters:

elev_deg (float) – Elevation angle from horizontal in degrees (90° = zenith)

Returns:

Distance from origin to intersection point (meters)

Return type:

float

Raises:

ValueError – If no intersection exists at the given elevation

point_at_elevation(elev_deg, azimuth_deg=0.0)[source]

Compute 3D point on detector sphere at given elevation and azimuth.

Parameters:
  • elev_deg (float) – Elevation angle from horizontal in degrees

  • azimuth_deg (float) – Azimuth angle in degrees (0 = +x direction)

Returns:

(x, y, z) position in meters

Return type:

ndarray

find_detector_center(theta_top_deg)[source]

Find detector center elevation such that top edge is at theta_top.

For a detector with physical half-width w and center at elevation θ_c, the angular half-width as seen from origin is:

α = arctan(w / d(θ_c))

The top edge elevation is θ_top = θ_c + α.

This function solves for θ_c given θ_top using brentq root finding.

Parameters:

theta_top_deg (float) – Desired elevation angle of top edge (degrees)

Returns:

Center elevation angle (degrees)

Return type:

float

angular_width_at_ring(ring_index)[source]

Compute angular width of a ring as seen from origin.

Parameters:

ring_index (int) – Index of the ring (0 = nearest to zenith)

Returns:

Angular width in degrees

Return type:

float

horizontal_distance_at_elevation(elev_deg)[source]

Compute horizontal distance from origin to detector at given elevation.

Parameters:

elev_deg (float) – Elevation angle in degrees

Returns:

Horizontal distance in meters

Return type:

float

get_ring_horizontal_distances()[source]

Get horizontal distances for all ring boundaries.

Returns:

Horizontal distances in meters for each ring boundary

Return type:

ndarray

summary()[source]

Return a summary string of the detector ring configuration.

Returns:

Multi-line summary of configuration

Return type:

str

get_ring_info(ring_index)[source]

Get detailed information about a specific ring.

Parameters:

ring_index (int) – Index of the ring

Returns:

Dictionary with ring properties

Return type:

dict

azimuth_bins_for_ring(ring_index, az_bin_size_m, az_range_deg=10.0)[source]

Compute azimuthal bins of constant physical size for a ring.

At each ring distance, computes how many bins of the given physical width fit within the ±az_range azimuth range.

Parameters:
  • ring_index (int) – Index of the ring

  • az_bin_size_m (float) – Physical azimuthal bin size in meters

  • az_range_deg (float) – Azimuth range in degrees (±this value from beam direction)

Returns:

  • n_bins (int) – Number of azimuthal bins for this ring

  • az_edges_deg (ndarray) – Azimuth bin edges in degrees (n_bins + 1 values)

  • az_centers_deg (ndarray) – Azimuth bin centers in degrees (n_bins values)

Return type:

tuple[int, ndarray, ndarray]

get_constant_size_grid(az_bin_size_m, az_range_deg=10.0)[source]

Get a grid of constant-size bins across all rings.

Each bin has approximately constant physical size: - Radial size: detector_radial_size (same for all rings) - Azimuthal size: az_bin_size_m (variable number of bins per ring)

Parameters:
  • az_bin_size_m (float) – Physical azimuthal bin size in meters

  • az_range_deg (float) – Azimuth range in degrees (±this value)

Returns:

List of bin specifications with keys: - ring_idx: int - az_bin_idx: int - n_az_bins: int (total azimuth bins for this ring) - az_lo_deg, az_hi_deg: float - az_center_deg: float - distance_m: float - bin_area_m2: float (approximate)

Return type:

list of dict

__init__(detector_radial_size=10000.0, detector_altitude=33000.0, max_elevation_deg=90.0, min_elevation_deg=-2.0, earth_radius=6371000.0, ring_boundaries_deg=None, ring_centers_deg=None, ring_distances=None, n_rings=0, detector_sphere_radius=0.0)
lsurf.detectors.create_default_detector_rings()[source]

Create detector rings with default parameters (10 km size, 33 km altitude).

Returns:

Detector ring configuration with default parameters

Return type:

ConstantSizeDetectorRings

Detector Classes

Detector([name])

Abstract base class for all detectors.

SphericalDetector(center, radius[, name, ...])

Spherical detector centered at a point.

PlanarDetector(center, normal, width, height)

Planar detector with finite rectangular size.

DirectionalDetector(position, direction, ...)

Detector with angular acceptance cone.

Detection Analysis

compute_angular_distribution(events, ...[, ...])

Compute angular distribution histogram.

compute_time_distribution(events[, ...])

Compute arrival time distribution histogram.

compute_intensity_distribution(events[, ...])

Compute intensity distribution histogram.

compute_wavelength_distribution(events[, ...])

Compute wavelength distribution histogram.

compute_statistics(events)

Compute summary statistics for detection events.