lsurf 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lsurf/__init__.py +471 -0
- lsurf/analysis/__init__.py +107 -0
- lsurf/analysis/healpix_utils.py +418 -0
- lsurf/analysis/sphere_viz.py +1280 -0
- lsurf/cli/__init__.py +48 -0
- lsurf/cli/build.py +398 -0
- lsurf/cli/config_schema.py +318 -0
- lsurf/cli/gui_cmd.py +76 -0
- lsurf/cli/interactive.py +850 -0
- lsurf/cli/main.py +81 -0
- lsurf/cli/run.py +806 -0
- lsurf/detectors/__init__.py +266 -0
- lsurf/detectors/analysis.py +289 -0
- lsurf/detectors/base.py +284 -0
- lsurf/detectors/constant_size_rings.py +485 -0
- lsurf/detectors/directional.py +45 -0
- lsurf/detectors/extended/__init__.py +73 -0
- lsurf/detectors/extended/local_sphere.py +353 -0
- lsurf/detectors/extended/recording_sphere.py +368 -0
- lsurf/detectors/planar.py +45 -0
- lsurf/detectors/protocol.py +187 -0
- lsurf/detectors/recording_spheres.py +63 -0
- lsurf/detectors/results.py +1140 -0
- lsurf/detectors/small/__init__.py +79 -0
- lsurf/detectors/small/directional.py +330 -0
- lsurf/detectors/small/planar.py +401 -0
- lsurf/detectors/small/spherical.py +450 -0
- lsurf/detectors/spherical.py +45 -0
- lsurf/geometry/__init__.py +199 -0
- lsurf/geometry/builder.py +478 -0
- lsurf/geometry/cell.py +228 -0
- lsurf/geometry/cell_geometry.py +247 -0
- lsurf/geometry/detector_arrays.py +1785 -0
- lsurf/geometry/geometry.py +222 -0
- lsurf/geometry/surface_analysis.py +375 -0
- lsurf/geometry/validation.py +91 -0
- lsurf/gui/__init__.py +51 -0
- lsurf/gui/app.py +903 -0
- lsurf/gui/core/__init__.py +39 -0
- lsurf/gui/core/scene.py +343 -0
- lsurf/gui/core/simulation.py +264 -0
- lsurf/gui/renderers/__init__.py +40 -0
- lsurf/gui/renderers/ray_renderer.py +353 -0
- lsurf/gui/renderers/source_renderer.py +505 -0
- lsurf/gui/renderers/surface_renderer.py +477 -0
- lsurf/gui/views/__init__.py +48 -0
- lsurf/gui/views/config_editor.py +3199 -0
- lsurf/gui/views/properties.py +257 -0
- lsurf/gui/views/results.py +291 -0
- lsurf/gui/views/scene_tree.py +180 -0
- lsurf/gui/views/viewport_3d.py +555 -0
- lsurf/gui/views/visualizations.py +712 -0
- lsurf/materials/__init__.py +169 -0
- lsurf/materials/base/__init__.py +64 -0
- lsurf/materials/base/full_inhomogeneous.py +208 -0
- lsurf/materials/base/grid_inhomogeneous.py +319 -0
- lsurf/materials/base/homogeneous.py +342 -0
- lsurf/materials/base/material_field.py +527 -0
- lsurf/materials/base/simple_inhomogeneous.py +418 -0
- lsurf/materials/base/spectral_inhomogeneous.py +497 -0
- lsurf/materials/implementations/__init__.py +120 -0
- lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
- lsurf/materials/implementations/duct_atmosphere.py +390 -0
- lsurf/materials/implementations/exponential_atmosphere.py +435 -0
- lsurf/materials/implementations/gaussian_lens.py +120 -0
- lsurf/materials/implementations/interpolated_data.py +123 -0
- lsurf/materials/implementations/layered_atmosphere.py +134 -0
- lsurf/materials/implementations/linear_gradient.py +109 -0
- lsurf/materials/implementations/linsley_atmosphere.py +764 -0
- lsurf/materials/implementations/standard_materials.py +126 -0
- lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
- lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
- lsurf/materials/utils/__init__.py +77 -0
- lsurf/materials/utils/constants.py +45 -0
- lsurf/materials/utils/device_functions.py +117 -0
- lsurf/materials/utils/dispersion.py +160 -0
- lsurf/materials/utils/factories.py +142 -0
- lsurf/propagation/__init__.py +91 -0
- lsurf/propagation/detector_gpu.py +67 -0
- lsurf/propagation/gpu_device_rays.py +294 -0
- lsurf/propagation/kernels/__init__.py +175 -0
- lsurf/propagation/kernels/absorption/__init__.py +61 -0
- lsurf/propagation/kernels/absorption/grid.py +240 -0
- lsurf/propagation/kernels/absorption/simple.py +232 -0
- lsurf/propagation/kernels/absorption/spectral.py +410 -0
- lsurf/propagation/kernels/detection/__init__.py +64 -0
- lsurf/propagation/kernels/detection/protocol.py +102 -0
- lsurf/propagation/kernels/detection/spherical.py +255 -0
- lsurf/propagation/kernels/device_functions.py +790 -0
- lsurf/propagation/kernels/fresnel/__init__.py +64 -0
- lsurf/propagation/kernels/fresnel/protocol.py +97 -0
- lsurf/propagation/kernels/fresnel/standard.py +258 -0
- lsurf/propagation/kernels/intersection/__init__.py +79 -0
- lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
- lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
- lsurf/propagation/kernels/intersection/plane.py +166 -0
- lsurf/propagation/kernels/intersection/protocol.py +95 -0
- lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
- lsurf/propagation/kernels/intersection/sphere.py +190 -0
- lsurf/propagation/kernels/propagation/__init__.py +85 -0
- lsurf/propagation/kernels/propagation/grid.py +527 -0
- lsurf/propagation/kernels/propagation/protocol.py +105 -0
- lsurf/propagation/kernels/propagation/simple.py +460 -0
- lsurf/propagation/kernels/propagation/spectral.py +875 -0
- lsurf/propagation/kernels/registry.py +331 -0
- lsurf/propagation/kernels/surface/__init__.py +72 -0
- lsurf/propagation/kernels/surface/bisection.py +232 -0
- lsurf/propagation/kernels/surface/detection.py +402 -0
- lsurf/propagation/kernels/surface/reduction.py +166 -0
- lsurf/propagation/propagator_protocol.py +222 -0
- lsurf/propagation/propagators/__init__.py +101 -0
- lsurf/propagation/propagators/detector_handler.py +354 -0
- lsurf/propagation/propagators/factory.py +200 -0
- lsurf/propagation/propagators/fresnel_handler.py +305 -0
- lsurf/propagation/propagators/gpu_gradient.py +566 -0
- lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
- lsurf/propagation/propagators/gradient.py +429 -0
- lsurf/propagation/propagators/intersection_handler.py +327 -0
- lsurf/propagation/propagators/material_propagator.py +398 -0
- lsurf/propagation/propagators/signed_distance_handler.py +522 -0
- lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
- lsurf/propagation/propagators/surface_interaction.py +616 -0
- lsurf/propagation/propagators/surface_propagator.py +719 -0
- lsurf/py.typed +1 -0
- lsurf/simulation/__init__.py +70 -0
- lsurf/simulation/config.py +164 -0
- lsurf/simulation/orchestrator.py +462 -0
- lsurf/simulation/result.py +299 -0
- lsurf/simulation/simulation.py +262 -0
- lsurf/sources/__init__.py +128 -0
- lsurf/sources/base.py +264 -0
- lsurf/sources/collimated.py +252 -0
- lsurf/sources/custom.py +409 -0
- lsurf/sources/diverging.py +228 -0
- lsurf/sources/gaussian.py +272 -0
- lsurf/sources/parallel_from_positions.py +197 -0
- lsurf/sources/point.py +172 -0
- lsurf/sources/uniform_diverging.py +258 -0
- lsurf/surfaces/__init__.py +184 -0
- lsurf/surfaces/cpu/__init__.py +50 -0
- lsurf/surfaces/cpu/curved_wave.py +463 -0
- lsurf/surfaces/cpu/gerstner_wave.py +381 -0
- lsurf/surfaces/cpu/wave_params.py +118 -0
- lsurf/surfaces/gpu/__init__.py +72 -0
- lsurf/surfaces/gpu/annular_plane.py +453 -0
- lsurf/surfaces/gpu/bounded_plane.py +390 -0
- lsurf/surfaces/gpu/curved_wave.py +483 -0
- lsurf/surfaces/gpu/gerstner_wave.py +377 -0
- lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
- lsurf/surfaces/gpu/plane.py +299 -0
- lsurf/surfaces/gpu/recording_sphere.py +587 -0
- lsurf/surfaces/gpu/sphere.py +311 -0
- lsurf/surfaces/protocol.py +336 -0
- lsurf/surfaces/registry.py +373 -0
- lsurf/utilities/__init__.py +175 -0
- lsurf/utilities/detector_analysis.py +814 -0
- lsurf/utilities/fresnel.py +628 -0
- lsurf/utilities/interactions.py +1215 -0
- lsurf/utilities/propagation.py +602 -0
- lsurf/utilities/ray_data.py +532 -0
- lsurf/utilities/recording_sphere.py +745 -0
- lsurf/utilities/time_spread.py +463 -0
- lsurf/visualization/__init__.py +329 -0
- lsurf/visualization/absorption_plots.py +334 -0
- lsurf/visualization/atmospheric_plots.py +754 -0
- lsurf/visualization/common.py +348 -0
- lsurf/visualization/detector_plots.py +1350 -0
- lsurf/visualization/detector_sphere_plots.py +1173 -0
- lsurf/visualization/fresnel_plots.py +1061 -0
- lsurf/visualization/ocean_simulation_plots.py +999 -0
- lsurf/visualization/polarization_plots.py +916 -0
- lsurf/visualization/raytracing_plots.py +1521 -0
- lsurf/visualization/ring_detector_plots.py +1867 -0
- lsurf/visualization/time_spread_plots.py +531 -0
- lsurf-1.0.0.dist-info/METADATA +381 -0
- lsurf-1.0.0.dist-info/RECORD +180 -0
- lsurf-1.0.0.dist-info/WHEEL +5 -0
- lsurf-1.0.0.dist-info/entry_points.txt +2 -0
- lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
- lsurf-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
# The Clear BSD License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2026 Tobias Heibges
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
|
7
|
+
# modification, are permitted (subject to the limitations in the disclaimer
|
|
8
|
+
# below) provided that the following conditions are met:
|
|
9
|
+
#
|
|
10
|
+
# * Redistributions of source code must retain the above copyright notice,
|
|
11
|
+
# this list of conditions and the following disclaimer.
|
|
12
|
+
#
|
|
13
|
+
# * Redistributions in binary form must reproduce the above copyright
|
|
14
|
+
# notice, this list of conditions and the following disclaimer in the
|
|
15
|
+
# documentation and/or other materials provided with the distribution.
|
|
16
|
+
#
|
|
17
|
+
# * Neither the name of the copyright holder nor the names of its
|
|
18
|
+
# contributors may be used to endorse or promote products derived from this
|
|
19
|
+
# software without specific prior written permission.
|
|
20
|
+
#
|
|
21
|
+
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
|
22
|
+
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
23
|
+
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
24
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
25
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
26
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
27
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
28
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
29
|
+
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
30
|
+
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
31
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
32
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
Surface Interaction Processor
|
|
36
|
+
|
|
37
|
+
Handles surface interaction physics based on surface role:
|
|
38
|
+
- DETECTOR: Record ray data, terminate ray
|
|
39
|
+
- ABSORBER: Terminate ray
|
|
40
|
+
- OPTICAL: Compute Fresnel coefficients, generate reflected/refracted rays
|
|
41
|
+
|
|
42
|
+
This is the second component in the propagator architecture:
|
|
43
|
+
MaterialPropagator -> SurfaceInteractionProcessor -> SimulationOrchestrator
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from dataclasses import dataclass, field
|
|
49
|
+
from typing import TYPE_CHECKING
|
|
50
|
+
|
|
51
|
+
import numpy as np
|
|
52
|
+
import numpy.typing as npt
|
|
53
|
+
|
|
54
|
+
from ...geometry.cell_geometry import CellGeometry
|
|
55
|
+
from ...surfaces import Surface, SurfaceRole
|
|
56
|
+
from ...utilities.fresnel import (
|
|
57
|
+
compute_reflection_direction,
|
|
58
|
+
compute_refraction_direction,
|
|
59
|
+
fresnel_coefficients,
|
|
60
|
+
initialize_polarization_vectors,
|
|
61
|
+
transform_polarization_reflection,
|
|
62
|
+
transform_polarization_refraction,
|
|
63
|
+
)
|
|
64
|
+
from ...utilities.ray_data import RayBatch, create_ray_batch, merge_ray_batches
|
|
65
|
+
|
|
66
|
+
if TYPE_CHECKING:
|
|
67
|
+
from ...geometry import Geometry
|
|
68
|
+
from ...materials import MaterialField
|
|
69
|
+
from ...utilities.recording_sphere import RecordedRays
|
|
70
|
+
from .surface_propagator import HitData
|
|
71
|
+
|
|
72
|
+
# Speed of light
|
|
73
|
+
SPEED_OF_LIGHT = 299792458.0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class SurfaceCrossing:
|
|
78
|
+
"""
|
|
79
|
+
Information about rays crossing a specific surface.
|
|
80
|
+
|
|
81
|
+
Attributes
|
|
82
|
+
----------
|
|
83
|
+
surface_idx : int
|
|
84
|
+
Index of the surface in the geometry.
|
|
85
|
+
surface : Surface
|
|
86
|
+
The surface object.
|
|
87
|
+
ray_indices : ndarray
|
|
88
|
+
Original indices of rays that hit this surface.
|
|
89
|
+
hit_positions : ndarray, shape (M, 3)
|
|
90
|
+
Intersection positions.
|
|
91
|
+
hit_directions : ndarray, shape (M, 3)
|
|
92
|
+
Ray directions at intersection.
|
|
93
|
+
hit_normals : ndarray, shape (M, 3)
|
|
94
|
+
Surface normals at intersection points.
|
|
95
|
+
from_front : ndarray, shape (M,)
|
|
96
|
+
Whether rays hit from the front side (normal direction).
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
surface_idx: int
|
|
100
|
+
surface: Surface
|
|
101
|
+
ray_indices: npt.NDArray[np.int32]
|
|
102
|
+
hit_positions: npt.NDArray[np.float32]
|
|
103
|
+
hit_directions: npt.NDArray[np.float32]
|
|
104
|
+
hit_normals: npt.NDArray[np.float32]
|
|
105
|
+
from_front: npt.NDArray[np.bool_]
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def num_hits(self) -> int:
|
|
109
|
+
"""Number of rays that hit this surface."""
|
|
110
|
+
return len(self.ray_indices)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class OpticalSurfaceHit:
|
|
115
|
+
"""
|
|
116
|
+
Record of hits on an optical surface.
|
|
117
|
+
|
|
118
|
+
Attributes
|
|
119
|
+
----------
|
|
120
|
+
surface_name : str
|
|
121
|
+
Name of the surface.
|
|
122
|
+
positions : ndarray, shape (N, 3)
|
|
123
|
+
Hit positions.
|
|
124
|
+
directions : ndarray, shape (N, 3)
|
|
125
|
+
Ray directions at hit.
|
|
126
|
+
intensities : ndarray, shape (N,)
|
|
127
|
+
Ray intensities.
|
|
128
|
+
wavelengths : ndarray, shape (N,)
|
|
129
|
+
Ray wavelengths.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
surface_name: str
|
|
133
|
+
positions: npt.NDArray[np.float32]
|
|
134
|
+
directions: npt.NDArray[np.float32]
|
|
135
|
+
intensities: npt.NDArray[np.float32]
|
|
136
|
+
wavelengths: npt.NDArray[np.float32]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class SurfaceInteractionResult:
|
|
141
|
+
"""
|
|
142
|
+
Result from processing surface interactions.
|
|
143
|
+
|
|
144
|
+
Attributes
|
|
145
|
+
----------
|
|
146
|
+
continuing_rays : RayBatch or None
|
|
147
|
+
Rays that should continue propagating (from optical surfaces).
|
|
148
|
+
reflected_rays : RayBatch or None
|
|
149
|
+
Reflected rays from optical surfaces.
|
|
150
|
+
refracted_rays : RayBatch or None
|
|
151
|
+
Refracted rays from optical surfaces.
|
|
152
|
+
detector_hits : dict
|
|
153
|
+
Mapping from detector name to RecordedRays.
|
|
154
|
+
absorbed_count : int
|
|
155
|
+
Number of rays absorbed.
|
|
156
|
+
detected_count : int
|
|
157
|
+
Number of rays detected.
|
|
158
|
+
optical_hits : list of OpticalSurfaceHit
|
|
159
|
+
If track_surface_hits was enabled, contains hit data for optical surfaces.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
continuing_rays: RayBatch | None = None
|
|
163
|
+
reflected_rays: RayBatch | None = None
|
|
164
|
+
refracted_rays: RayBatch | None = None
|
|
165
|
+
detector_hits: dict = field(default_factory=dict)
|
|
166
|
+
absorbed_count: int = 0
|
|
167
|
+
detected_count: int = 0
|
|
168
|
+
optical_hits: list[OpticalSurfaceHit] = field(default_factory=list)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class SurfaceInteractionProcessor:
|
|
172
|
+
"""
|
|
173
|
+
Processes ray-surface interactions based on surface role.
|
|
174
|
+
|
|
175
|
+
For each surface type:
|
|
176
|
+
- DETECTOR: Record ray data, terminate ray
|
|
177
|
+
- ABSORBER: Terminate ray
|
|
178
|
+
- OPTICAL: Compute Fresnel coefficients, generate reflected/refracted rays
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
surfaces : list of Surface
|
|
183
|
+
List of surfaces in the geometry.
|
|
184
|
+
polarization : str, optional
|
|
185
|
+
Polarization state for Fresnel calculations: 's', 'p', or 'unpolarized'.
|
|
186
|
+
Default is 'unpolarized'.
|
|
187
|
+
track_polarization_vector : bool, optional
|
|
188
|
+
Whether to track 3D polarization vectors through interactions.
|
|
189
|
+
Default is False.
|
|
190
|
+
|
|
191
|
+
Examples
|
|
192
|
+
--------
|
|
193
|
+
>>> from lsurf.surfaces import PlaneSurface, SurfaceRole
|
|
194
|
+
>>> detector = PlaneSurface(
|
|
195
|
+
... point=(0, 0, 1000),
|
|
196
|
+
... normal=(0, 0, 1),
|
|
197
|
+
... role=SurfaceRole.DETECTOR,
|
|
198
|
+
... )
|
|
199
|
+
>>> processor = SurfaceInteractionProcessor([detector])
|
|
200
|
+
>>> result = processor.process_hits(rays, hit_data)
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
def __init__(
|
|
204
|
+
self,
|
|
205
|
+
surfaces: list[Surface],
|
|
206
|
+
polarization: str = "unpolarized",
|
|
207
|
+
track_polarization_vector: bool = False,
|
|
208
|
+
geometry: "Geometry | None" = None,
|
|
209
|
+
):
|
|
210
|
+
self._surfaces = list(surfaces)
|
|
211
|
+
self._polarization = polarization
|
|
212
|
+
self._track_polarization_vector = track_polarization_vector
|
|
213
|
+
self._geometry = geometry
|
|
214
|
+
self._is_cell_geometry = isinstance(geometry, CellGeometry)
|
|
215
|
+
|
|
216
|
+
# Build indices by role
|
|
217
|
+
self._detector_indices: list[int] = []
|
|
218
|
+
self._optical_indices: list[int] = []
|
|
219
|
+
self._absorber_indices: list[int] = []
|
|
220
|
+
|
|
221
|
+
for i, surface in enumerate(surfaces):
|
|
222
|
+
if surface.role == SurfaceRole.DETECTOR:
|
|
223
|
+
self._detector_indices.append(i)
|
|
224
|
+
elif surface.role == SurfaceRole.OPTICAL:
|
|
225
|
+
self._optical_indices.append(i)
|
|
226
|
+
elif surface.role == SurfaceRole.ABSORBER:
|
|
227
|
+
self._absorber_indices.append(i)
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def surfaces(self) -> list[Surface]:
|
|
231
|
+
"""List of surfaces."""
|
|
232
|
+
return self._surfaces
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def polarization(self) -> str:
|
|
236
|
+
"""Polarization state for Fresnel calculations."""
|
|
237
|
+
return self._polarization
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def num_detectors(self) -> int:
|
|
241
|
+
"""Number of detector surfaces."""
|
|
242
|
+
return len(self._detector_indices)
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def num_optical(self) -> int:
|
|
246
|
+
"""Number of optical surfaces."""
|
|
247
|
+
return len(self._optical_indices)
|
|
248
|
+
|
|
249
|
+
def process_hits(
|
|
250
|
+
self,
|
|
251
|
+
rays: RayBatch,
|
|
252
|
+
hit_data: "HitData",
|
|
253
|
+
track_surface_hits: bool = False,
|
|
254
|
+
) -> SurfaceInteractionResult:
|
|
255
|
+
"""
|
|
256
|
+
Process all surface hits from a propagation leg.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
rays : RayBatch
|
|
261
|
+
Original ray batch (before hit_data was recorded).
|
|
262
|
+
hit_data : HitData
|
|
263
|
+
Hit information from MaterialPropagator.
|
|
264
|
+
track_surface_hits : bool, optional
|
|
265
|
+
If True, store hit positions for optical surfaces in result.optical_hits.
|
|
266
|
+
Default is False.
|
|
267
|
+
|
|
268
|
+
Returns
|
|
269
|
+
-------
|
|
270
|
+
SurfaceInteractionResult
|
|
271
|
+
Result containing detector hits, reflected/refracted rays, etc.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
result = SurfaceInteractionResult()
|
|
275
|
+
all_reflected: list[RayBatch] = []
|
|
276
|
+
all_refracted: list[RayBatch] = []
|
|
277
|
+
|
|
278
|
+
# Group hits by surface
|
|
279
|
+
for surface_idx, surface in enumerate(self._surfaces):
|
|
280
|
+
mask = hit_data.hit_surface_idx == surface_idx
|
|
281
|
+
if not np.any(mask):
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
# Extract hit data for this surface
|
|
285
|
+
crossing = self._extract_crossing(
|
|
286
|
+
rays, hit_data, surface_idx, surface, mask
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Process based on role
|
|
290
|
+
if surface.role == SurfaceRole.DETECTOR:
|
|
291
|
+
recorded = self._process_detector_hit(rays, crossing)
|
|
292
|
+
surface_name = surface.name or f"detector_{surface_idx}"
|
|
293
|
+
result.detector_hits[surface_name] = recorded
|
|
294
|
+
result.detected_count += crossing.num_hits
|
|
295
|
+
|
|
296
|
+
elif surface.role == SurfaceRole.ABSORBER:
|
|
297
|
+
result.absorbed_count += crossing.num_hits
|
|
298
|
+
|
|
299
|
+
elif surface.role == SurfaceRole.OPTICAL:
|
|
300
|
+
# Store hit data if tracking enabled
|
|
301
|
+
if track_surface_hits:
|
|
302
|
+
surface_name = surface.name or f"optical_{surface_idx}"
|
|
303
|
+
result.optical_hits.append(
|
|
304
|
+
OpticalSurfaceHit(
|
|
305
|
+
surface_name=surface_name,
|
|
306
|
+
positions=crossing.hit_positions.copy(),
|
|
307
|
+
directions=crossing.hit_directions.copy(),
|
|
308
|
+
intensities=rays.intensities[crossing.ray_indices].copy(),
|
|
309
|
+
wavelengths=rays.wavelengths[crossing.ray_indices].copy(),
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
reflected, refracted = self._process_optical_hit(rays, crossing)
|
|
314
|
+
if reflected is not None and reflected.num_rays > 0:
|
|
315
|
+
all_reflected.append(reflected)
|
|
316
|
+
if refracted is not None and refracted.num_rays > 0:
|
|
317
|
+
all_refracted.append(refracted)
|
|
318
|
+
|
|
319
|
+
# Merge all reflected/refracted rays
|
|
320
|
+
if all_reflected:
|
|
321
|
+
result.reflected_rays = merge_ray_batches(all_reflected)
|
|
322
|
+
if all_refracted:
|
|
323
|
+
result.refracted_rays = merge_ray_batches(all_refracted)
|
|
324
|
+
|
|
325
|
+
return result
|
|
326
|
+
|
|
327
|
+
def _extract_crossing(
|
|
328
|
+
self,
|
|
329
|
+
rays: RayBatch,
|
|
330
|
+
hit_data: "HitData",
|
|
331
|
+
surface_idx: int,
|
|
332
|
+
surface: Surface,
|
|
333
|
+
mask: npt.NDArray[np.bool_],
|
|
334
|
+
) -> SurfaceCrossing:
|
|
335
|
+
"""Extract crossing data for a specific surface."""
|
|
336
|
+
ray_indices = np.where(mask)[0].astype(np.int32)
|
|
337
|
+
hit_positions = hit_data.hit_positions[mask]
|
|
338
|
+
hit_directions = hit_data.hit_directions[mask]
|
|
339
|
+
|
|
340
|
+
# Compute surface normals at hit positions
|
|
341
|
+
hit_normals = surface.normal_at(hit_positions, hit_directions)
|
|
342
|
+
|
|
343
|
+
# Determine if hits are from front (ray going opposite to normal)
|
|
344
|
+
# cos(angle) = -direction . normal
|
|
345
|
+
cos_angle = -np.sum(hit_directions * hit_normals, axis=1)
|
|
346
|
+
from_front = cos_angle > 0
|
|
347
|
+
|
|
348
|
+
# Flip normals for back-side hits to always face the ray
|
|
349
|
+
hit_normals[~from_front] = -hit_normals[~from_front]
|
|
350
|
+
|
|
351
|
+
return SurfaceCrossing(
|
|
352
|
+
surface_idx=surface_idx,
|
|
353
|
+
surface=surface,
|
|
354
|
+
ray_indices=ray_indices,
|
|
355
|
+
hit_positions=hit_positions,
|
|
356
|
+
hit_directions=hit_directions,
|
|
357
|
+
hit_normals=hit_normals,
|
|
358
|
+
from_front=from_front,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def _get_materials_at_crossing(
|
|
362
|
+
self,
|
|
363
|
+
crossing: SurfaceCrossing,
|
|
364
|
+
) -> tuple["MaterialField | None", "MaterialField | None"]:
|
|
365
|
+
"""
|
|
366
|
+
Get materials for a surface crossing.
|
|
367
|
+
|
|
368
|
+
For standard Geometry, returns (surface.material_front, surface.material_back).
|
|
369
|
+
For CellGeometry, queries cells to determine materials at hit positions.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
crossing : SurfaceCrossing
|
|
374
|
+
The surface crossing information.
|
|
375
|
+
|
|
376
|
+
Returns
|
|
377
|
+
-------
|
|
378
|
+
tuple[MaterialField | None, MaterialField | None]
|
|
379
|
+
(material_front, material_back) for this crossing.
|
|
380
|
+
"""
|
|
381
|
+
surface = crossing.surface
|
|
382
|
+
|
|
383
|
+
if not self._is_cell_geometry or self._geometry is None:
|
|
384
|
+
# Standard geometry: use surface's front/back materials
|
|
385
|
+
return surface.material_front, surface.material_back
|
|
386
|
+
|
|
387
|
+
# CellGeometry: query cells for materials
|
|
388
|
+
# For cell geometry, we need to determine material on each side
|
|
389
|
+
# by checking which cell each side belongs to
|
|
390
|
+
cell_geom: CellGeometry = self._geometry # type: ignore
|
|
391
|
+
|
|
392
|
+
# Get a representative position slightly on each side of the surface
|
|
393
|
+
# Use the first hit position and offset by a small amount in normal direction
|
|
394
|
+
if crossing.num_hits == 0:
|
|
395
|
+
return None, None
|
|
396
|
+
|
|
397
|
+
# Use hit normals (which are oriented toward the ray)
|
|
398
|
+
# For front material: offset in normal direction (toward where ray came from)
|
|
399
|
+
# For back material: offset opposite to normal (where ray is going)
|
|
400
|
+
sample_pos = crossing.hit_positions[0:1].astype(np.float64)
|
|
401
|
+
sample_normal = crossing.hit_normals[0:1].astype(np.float64)
|
|
402
|
+
|
|
403
|
+
# Offset to check material on each side
|
|
404
|
+
offset = 0.001 # Small offset
|
|
405
|
+
front_pos = sample_pos + offset * sample_normal
|
|
406
|
+
back_pos = sample_pos - offset * sample_normal
|
|
407
|
+
|
|
408
|
+
# Query cell geometry for materials
|
|
409
|
+
front_results = cell_geom.get_material_at(front_pos)
|
|
410
|
+
back_results = cell_geom.get_material_at(back_pos)
|
|
411
|
+
|
|
412
|
+
# Extract materials (first result that covers this position)
|
|
413
|
+
mat_front = None
|
|
414
|
+
mat_back = None
|
|
415
|
+
|
|
416
|
+
for material, mask in front_results:
|
|
417
|
+
if mask[0]:
|
|
418
|
+
mat_front = material
|
|
419
|
+
break
|
|
420
|
+
|
|
421
|
+
for material, mask in back_results:
|
|
422
|
+
if mask[0]:
|
|
423
|
+
mat_back = material
|
|
424
|
+
break
|
|
425
|
+
|
|
426
|
+
return mat_front, mat_back
|
|
427
|
+
|
|
428
|
+
def _process_detector_hit(
|
|
429
|
+
self,
|
|
430
|
+
rays: RayBatch,
|
|
431
|
+
crossing: SurfaceCrossing,
|
|
432
|
+
) -> "RecordedRays":
|
|
433
|
+
"""Process hits on a detector surface."""
|
|
434
|
+
from ...utilities.recording_sphere import RecordedRays
|
|
435
|
+
|
|
436
|
+
idx = crossing.ray_indices
|
|
437
|
+
|
|
438
|
+
# Get polarization vectors if available
|
|
439
|
+
polarization_vectors = None
|
|
440
|
+
if self._track_polarization_vector and rays.polarization_vector is not None:
|
|
441
|
+
polarization_vectors = rays.polarization_vector[idx]
|
|
442
|
+
|
|
443
|
+
return RecordedRays(
|
|
444
|
+
positions=crossing.hit_positions.copy(),
|
|
445
|
+
directions=crossing.hit_directions.copy(),
|
|
446
|
+
times=rays.accumulated_time[idx].copy(),
|
|
447
|
+
intensities=rays.intensities[idx].copy(),
|
|
448
|
+
wavelengths=rays.wavelengths[idx].copy(),
|
|
449
|
+
generations=rays.generations[idx].copy(),
|
|
450
|
+
polarization_vectors=polarization_vectors,
|
|
451
|
+
ray_indices=idx, # Track original ray indices for correct mapping
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def _process_optical_hit(
|
|
455
|
+
self,
|
|
456
|
+
rays: RayBatch,
|
|
457
|
+
crossing: SurfaceCrossing,
|
|
458
|
+
) -> tuple[RayBatch | None, RayBatch | None]:
|
|
459
|
+
"""
|
|
460
|
+
Process hits on an optical surface.
|
|
461
|
+
|
|
462
|
+
Computes Fresnel coefficients and generates reflected/refracted rays.
|
|
463
|
+
"""
|
|
464
|
+
idx = crossing.ray_indices
|
|
465
|
+
positions = crossing.hit_positions
|
|
466
|
+
directions = crossing.hit_directions
|
|
467
|
+
normals = crossing.hit_normals
|
|
468
|
+
from_front = crossing.from_front
|
|
469
|
+
num_hits = crossing.num_hits
|
|
470
|
+
|
|
471
|
+
if num_hits == 0:
|
|
472
|
+
return None, None
|
|
473
|
+
|
|
474
|
+
# Get ray properties
|
|
475
|
+
wavelengths = rays.wavelengths[idx]
|
|
476
|
+
intensities = rays.intensities[idx]
|
|
477
|
+
accumulated_time = rays.accumulated_time[idx]
|
|
478
|
+
generations = rays.generations[idx]
|
|
479
|
+
|
|
480
|
+
# Handle polarization vectors
|
|
481
|
+
polarization_vectors = None
|
|
482
|
+
if self._track_polarization_vector:
|
|
483
|
+
if rays.polarization_vector is not None:
|
|
484
|
+
polarization_vectors = rays.polarization_vector[idx]
|
|
485
|
+
else:
|
|
486
|
+
polarization_vectors = initialize_polarization_vectors(
|
|
487
|
+
directions, polarization=self._polarization
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Get materials based on hit direction
|
|
491
|
+
# from_front: light coming from front material
|
|
492
|
+
# from_back: light coming from back material
|
|
493
|
+
mat_front, mat_back = self._get_materials_at_crossing(crossing)
|
|
494
|
+
|
|
495
|
+
if mat_front is None or mat_back is None:
|
|
496
|
+
# Missing materials, treat as pass-through
|
|
497
|
+
return None, None
|
|
498
|
+
|
|
499
|
+
# Compute refractive indices
|
|
500
|
+
# n1 = material ray is coming FROM
|
|
501
|
+
# n2 = material ray is going TO
|
|
502
|
+
n1_values = np.empty(num_hits, dtype=np.float32)
|
|
503
|
+
n2_values = np.empty(num_hits, dtype=np.float32)
|
|
504
|
+
|
|
505
|
+
for i in range(num_hits):
|
|
506
|
+
pos = positions[i]
|
|
507
|
+
wl = wavelengths[i]
|
|
508
|
+
if from_front[i]:
|
|
509
|
+
n1_values[i] = mat_front.get_refractive_index(
|
|
510
|
+
pos[0], pos[1], pos[2], wl
|
|
511
|
+
)
|
|
512
|
+
n2_values[i] = mat_back.get_refractive_index(pos[0], pos[1], pos[2], wl)
|
|
513
|
+
else:
|
|
514
|
+
n1_values[i] = mat_back.get_refractive_index(pos[0], pos[1], pos[2], wl)
|
|
515
|
+
n2_values[i] = mat_front.get_refractive_index(
|
|
516
|
+
pos[0], pos[1], pos[2], wl
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Compute incident angle cosine
|
|
520
|
+
cos_theta_i = np.abs(np.sum(directions * normals, axis=1))
|
|
521
|
+
|
|
522
|
+
# Compute Fresnel coefficients
|
|
523
|
+
R, T = fresnel_coefficients(
|
|
524
|
+
n1_values, n2_values, cos_theta_i, self._polarization
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
# Get separate R_s, R_p if tracking polarization
|
|
528
|
+
R_s = R_p = None
|
|
529
|
+
if self._track_polarization_vector:
|
|
530
|
+
R_s, _ = fresnel_coefficients(n1_values, n2_values, cos_theta_i, "s")
|
|
531
|
+
R_p, _ = fresnel_coefficients(n1_values, n2_values, cos_theta_i, "p")
|
|
532
|
+
|
|
533
|
+
# Compute reflected directions
|
|
534
|
+
reflected_directions = compute_reflection_direction(directions, normals)
|
|
535
|
+
reflected_intensities = intensities * R
|
|
536
|
+
|
|
537
|
+
# Compute refracted directions
|
|
538
|
+
refracted_directions, tir_mask = compute_refraction_direction(
|
|
539
|
+
directions, normals, n1_values, n2_values
|
|
540
|
+
)
|
|
541
|
+
refracted_intensities = intensities * T
|
|
542
|
+
refracted_intensities[tir_mask] = 0.0
|
|
543
|
+
|
|
544
|
+
# Create reflected ray batch
|
|
545
|
+
reflected_rays = create_ray_batch(
|
|
546
|
+
num_rays=num_hits,
|
|
547
|
+
enable_polarization_vector=self._track_polarization_vector,
|
|
548
|
+
)
|
|
549
|
+
# Offset slightly to avoid re-intersection
|
|
550
|
+
reflected_rays.positions[:] = positions + 0.01 * reflected_directions
|
|
551
|
+
reflected_rays.directions[:] = reflected_directions
|
|
552
|
+
reflected_rays.wavelengths[:] = wavelengths
|
|
553
|
+
reflected_rays.intensities[:] = reflected_intensities
|
|
554
|
+
reflected_rays.optical_path_lengths[:] = rays.optical_path_lengths[idx]
|
|
555
|
+
reflected_rays.geometric_path_lengths[:] = rays.geometric_path_lengths[idx]
|
|
556
|
+
reflected_rays.accumulated_time[:] = accumulated_time
|
|
557
|
+
reflected_rays.generations[:] = generations + 1
|
|
558
|
+
reflected_rays.domain_ids[:] = rays.domain_ids[idx]
|
|
559
|
+
reflected_rays.active[:] = reflected_intensities > 1e-10
|
|
560
|
+
|
|
561
|
+
# Transform polarization vectors for reflection
|
|
562
|
+
if self._track_polarization_vector and polarization_vectors is not None:
|
|
563
|
+
reflected_pol = transform_polarization_reflection(
|
|
564
|
+
polarization_vectors,
|
|
565
|
+
directions,
|
|
566
|
+
reflected_directions,
|
|
567
|
+
normals,
|
|
568
|
+
R_s=R_s,
|
|
569
|
+
R_p=R_p,
|
|
570
|
+
)
|
|
571
|
+
reflected_rays.polarization_vector[:] = reflected_pol
|
|
572
|
+
|
|
573
|
+
# Create refracted ray batch
|
|
574
|
+
refracted_rays = create_ray_batch(
|
|
575
|
+
num_rays=num_hits,
|
|
576
|
+
enable_polarization_vector=self._track_polarization_vector,
|
|
577
|
+
)
|
|
578
|
+
# Offset slightly to avoid re-intersection
|
|
579
|
+
refracted_rays.positions[:] = positions + 0.01 * refracted_directions
|
|
580
|
+
refracted_rays.directions[:] = refracted_directions
|
|
581
|
+
refracted_rays.wavelengths[:] = wavelengths
|
|
582
|
+
refracted_rays.intensities[:] = refracted_intensities
|
|
583
|
+
refracted_rays.optical_path_lengths[:] = rays.optical_path_lengths[idx]
|
|
584
|
+
refracted_rays.geometric_path_lengths[:] = rays.geometric_path_lengths[idx]
|
|
585
|
+
refracted_rays.accumulated_time[:] = accumulated_time
|
|
586
|
+
refracted_rays.generations[:] = generations + 1
|
|
587
|
+
refracted_rays.domain_ids[:] = rays.domain_ids[idx]
|
|
588
|
+
refracted_rays.active[:] = (refracted_intensities > 1e-10) & (~tir_mask)
|
|
589
|
+
|
|
590
|
+
# Transform polarization vectors for refraction
|
|
591
|
+
if self._track_polarization_vector and polarization_vectors is not None:
|
|
592
|
+
refracted_pol = transform_polarization_refraction(
|
|
593
|
+
polarization_vectors,
|
|
594
|
+
directions,
|
|
595
|
+
refracted_directions,
|
|
596
|
+
normals,
|
|
597
|
+
)
|
|
598
|
+
refracted_pol[tir_mask] = 0.0
|
|
599
|
+
refracted_rays.polarization_vector[:] = refracted_pol
|
|
600
|
+
|
|
601
|
+
# Compact to remove inactive rays
|
|
602
|
+
reflected_rays = reflected_rays.compact()
|
|
603
|
+
refracted_rays = refracted_rays.compact()
|
|
604
|
+
|
|
605
|
+
return reflected_rays, refracted_rays
|
|
606
|
+
|
|
607
|
+
def get_surface(self, idx: int) -> Surface:
|
|
608
|
+
"""Get surface by index."""
|
|
609
|
+
return self._surfaces[idx]
|
|
610
|
+
|
|
611
|
+
def get_surface_by_name(self, name: str) -> Surface | None:
|
|
612
|
+
"""Get surface by name."""
|
|
613
|
+
for surface in self._surfaces:
|
|
614
|
+
if surface.name == name:
|
|
615
|
+
return surface
|
|
616
|
+
return None
|