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
lsurf/sources/custom.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
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
|
+
Custom Ray Source with Per-Ray Properties
|
|
36
|
+
|
|
37
|
+
Provides a source where each ray's position, direction, intensity, and
|
|
38
|
+
wavelength can be individually specified. Ideal for:
|
|
39
|
+
- Chromatic dispersion simulations with different wavelengths per ray
|
|
40
|
+
- Custom ray distributions for specialized optical setups
|
|
41
|
+
- Importing ray data from external sources
|
|
42
|
+
- Testing and validation scenarios
|
|
43
|
+
|
|
44
|
+
Examples
|
|
45
|
+
--------
|
|
46
|
+
>>> from lsurf.sources import CustomRaySource
|
|
47
|
+
>>> import numpy as np
|
|
48
|
+
>>>
|
|
49
|
+
>>> # Create RGB rays from same position but different directions
|
|
50
|
+
>>> positions = np.array([
|
|
51
|
+
... [0, 0, 0],
|
|
52
|
+
... [0, 0, 0],
|
|
53
|
+
... [0, 0, 0],
|
|
54
|
+
... ])
|
|
55
|
+
>>> directions = np.array([
|
|
56
|
+
... [1, 0, 0.01], # Slight upward tilt
|
|
57
|
+
... [1, 0, 0], # Horizontal
|
|
58
|
+
... [1, 0, -0.01], # Slight downward tilt
|
|
59
|
+
... ])
|
|
60
|
+
>>> wavelengths = np.array([700e-9, 550e-9, 450e-9]) # RGB
|
|
61
|
+
>>> intensities = np.array([1.0, 1.0, 1.0])
|
|
62
|
+
>>>
|
|
63
|
+
>>> source = CustomRaySource(
|
|
64
|
+
... positions=positions,
|
|
65
|
+
... directions=directions,
|
|
66
|
+
... wavelengths=wavelengths,
|
|
67
|
+
... intensities=intensities,
|
|
68
|
+
... )
|
|
69
|
+
>>> rays = source.generate()
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
import numpy as np
|
|
73
|
+
import numpy.typing as npt
|
|
74
|
+
|
|
75
|
+
from ..utilities.ray_data import RayBatch, create_ray_batch
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CustomRaySource:
|
|
79
|
+
"""
|
|
80
|
+
Fully customizable ray source with per-ray properties.
|
|
81
|
+
|
|
82
|
+
Each ray can have its own position, direction, wavelength, and intensity.
|
|
83
|
+
This provides maximum flexibility for specialized simulations.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
positions : array_like, shape (N, 3)
|
|
88
|
+
Starting position for each ray in meters.
|
|
89
|
+
directions : array_like, shape (N, 3)
|
|
90
|
+
Direction vector for each ray (will be normalized).
|
|
91
|
+
wavelengths : array_like, shape (N,)
|
|
92
|
+
Wavelength for each ray in meters.
|
|
93
|
+
intensities : array_like, shape (N,), optional
|
|
94
|
+
Intensity/power for each ray in watts. If None, defaults to
|
|
95
|
+
uniform distribution with total power of 1.0.
|
|
96
|
+
|
|
97
|
+
Attributes
|
|
98
|
+
----------
|
|
99
|
+
num_rays : int
|
|
100
|
+
Number of rays.
|
|
101
|
+
positions : ndarray, shape (N, 3)
|
|
102
|
+
Ray starting positions.
|
|
103
|
+
directions : ndarray, shape (N, 3)
|
|
104
|
+
Normalized ray directions.
|
|
105
|
+
wavelengths : ndarray, shape (N,)
|
|
106
|
+
Per-ray wavelengths.
|
|
107
|
+
intensities : ndarray, shape (N,)
|
|
108
|
+
Per-ray intensities.
|
|
109
|
+
power : float
|
|
110
|
+
Total power (sum of intensities).
|
|
111
|
+
|
|
112
|
+
Examples
|
|
113
|
+
--------
|
|
114
|
+
>>> # Chromatic dispersion study - same start, different wavelengths
|
|
115
|
+
>>> n_rays = 100
|
|
116
|
+
>>> positions = np.tile([0, 0, 1000], (n_rays, 1)) # All at same point
|
|
117
|
+
>>> directions = np.tile([1, 0, 0], (n_rays, 1)) # All same direction
|
|
118
|
+
>>> wavelengths = np.linspace(400e-9, 700e-9, n_rays) # Visible spectrum
|
|
119
|
+
>>> source = CustomRaySource(positions, directions, wavelengths)
|
|
120
|
+
>>> rays = source.generate()
|
|
121
|
+
|
|
122
|
+
>>> # Fan of rays for refraction analysis
|
|
123
|
+
>>> angles = np.linspace(-0.1, 0.1, 50) # +/- 5.7 degrees
|
|
124
|
+
>>> positions = np.zeros((50, 3))
|
|
125
|
+
>>> directions = np.column_stack([
|
|
126
|
+
... np.cos(angles),
|
|
127
|
+
... np.zeros(50),
|
|
128
|
+
... np.sin(angles)
|
|
129
|
+
... ])
|
|
130
|
+
>>> wavelengths = np.full(50, 550e-9)
|
|
131
|
+
>>> source = CustomRaySource(positions, directions, wavelengths)
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
positions: npt.ArrayLike,
|
|
137
|
+
directions: npt.ArrayLike,
|
|
138
|
+
wavelengths: npt.ArrayLike,
|
|
139
|
+
intensities: npt.ArrayLike | None = None,
|
|
140
|
+
):
|
|
141
|
+
"""
|
|
142
|
+
Initialize custom ray source.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
positions : array_like, shape (N, 3)
|
|
147
|
+
Starting position for each ray in meters.
|
|
148
|
+
directions : array_like, shape (N, 3)
|
|
149
|
+
Direction vector for each ray (will be normalized).
|
|
150
|
+
wavelengths : array_like, shape (N,)
|
|
151
|
+
Wavelength for each ray in meters.
|
|
152
|
+
intensities : array_like, shape (N,), optional
|
|
153
|
+
Intensity for each ray. If None, uniform distribution is used.
|
|
154
|
+
|
|
155
|
+
Raises
|
|
156
|
+
------
|
|
157
|
+
ValueError
|
|
158
|
+
If array shapes are inconsistent or invalid.
|
|
159
|
+
"""
|
|
160
|
+
# Convert and validate positions
|
|
161
|
+
self._positions = np.asarray(positions, dtype=np.float32)
|
|
162
|
+
if self._positions.ndim == 1:
|
|
163
|
+
self._positions = self._positions.reshape(1, 3)
|
|
164
|
+
if self._positions.ndim != 2 or self._positions.shape[1] != 3:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"positions must have shape (N, 3), got {self._positions.shape}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self._num_rays = self._positions.shape[0]
|
|
170
|
+
|
|
171
|
+
# Convert and validate directions
|
|
172
|
+
self._directions = np.asarray(directions, dtype=np.float32)
|
|
173
|
+
if self._directions.ndim == 1:
|
|
174
|
+
self._directions = self._directions.reshape(1, 3)
|
|
175
|
+
if self._directions.shape != (self._num_rays, 3):
|
|
176
|
+
raise ValueError(
|
|
177
|
+
f"directions must have shape ({self._num_rays}, 3), "
|
|
178
|
+
f"got {self._directions.shape}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Normalize directions
|
|
182
|
+
norms = np.linalg.norm(self._directions, axis=1, keepdims=True)
|
|
183
|
+
if np.any(norms < 1e-10):
|
|
184
|
+
raise ValueError("direction vectors cannot be zero")
|
|
185
|
+
self._directions = self._directions / norms
|
|
186
|
+
|
|
187
|
+
# Convert and validate wavelengths
|
|
188
|
+
self._wavelengths = np.asarray(wavelengths, dtype=np.float32).ravel()
|
|
189
|
+
if self._wavelengths.shape[0] != self._num_rays:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"wavelengths must have shape ({self._num_rays},), "
|
|
192
|
+
f"got {self._wavelengths.shape}"
|
|
193
|
+
)
|
|
194
|
+
if np.any(self._wavelengths <= 0):
|
|
195
|
+
raise ValueError("wavelengths must be positive")
|
|
196
|
+
|
|
197
|
+
# Convert and validate intensities
|
|
198
|
+
if intensities is None:
|
|
199
|
+
# Default: uniform distribution with total power = 1.0
|
|
200
|
+
self._intensities = np.full(
|
|
201
|
+
self._num_rays, 1.0 / self._num_rays, dtype=np.float32
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
self._intensities = np.asarray(intensities, dtype=np.float32).ravel()
|
|
205
|
+
if self._intensities.shape[0] != self._num_rays:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"intensities must have shape ({self._num_rays},), "
|
|
208
|
+
f"got {self._intensities.shape}"
|
|
209
|
+
)
|
|
210
|
+
if np.any(self._intensities < 0):
|
|
211
|
+
raise ValueError("intensities cannot be negative")
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def num_rays(self) -> int:
|
|
215
|
+
"""Number of rays."""
|
|
216
|
+
return self._num_rays
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def positions(self) -> npt.NDArray[np.float32]:
|
|
220
|
+
"""Ray starting positions, shape (N, 3)."""
|
|
221
|
+
return self._positions
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def directions(self) -> npt.NDArray[np.float32]:
|
|
225
|
+
"""Normalized ray directions, shape (N, 3)."""
|
|
226
|
+
return self._directions
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def wavelengths(self) -> npt.NDArray[np.float32]:
|
|
230
|
+
"""Per-ray wavelengths in meters, shape (N,)."""
|
|
231
|
+
return self._wavelengths
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def intensities(self) -> npt.NDArray[np.float32]:
|
|
235
|
+
"""Per-ray intensities, shape (N,)."""
|
|
236
|
+
return self._intensities
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def power(self) -> float:
|
|
240
|
+
"""Total power (sum of intensities)."""
|
|
241
|
+
return float(np.sum(self._intensities))
|
|
242
|
+
|
|
243
|
+
def generate(self) -> RayBatch:
|
|
244
|
+
"""
|
|
245
|
+
Generate ray batch with specified properties.
|
|
246
|
+
|
|
247
|
+
Creates a RayBatch with positions, directions, wavelengths, and
|
|
248
|
+
intensities as specified in the constructor.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
RayBatch
|
|
253
|
+
Ray batch ready for propagation.
|
|
254
|
+
"""
|
|
255
|
+
rays = create_ray_batch(num_rays=self._num_rays)
|
|
256
|
+
|
|
257
|
+
# Set all per-ray properties
|
|
258
|
+
rays.positions[:] = self._positions
|
|
259
|
+
rays.directions[:] = self._directions
|
|
260
|
+
rays.wavelengths[:] = self._wavelengths
|
|
261
|
+
rays.intensities[:] = self._intensities
|
|
262
|
+
rays.active[:] = True
|
|
263
|
+
|
|
264
|
+
return rays
|
|
265
|
+
|
|
266
|
+
def __repr__(self) -> str:
|
|
267
|
+
"""Return string representation."""
|
|
268
|
+
wl_min = self._wavelengths.min()
|
|
269
|
+
wl_max = self._wavelengths.max()
|
|
270
|
+
if wl_min == wl_max:
|
|
271
|
+
wl_str = f"{wl_min:.2e}m"
|
|
272
|
+
else:
|
|
273
|
+
wl_str = f"[{wl_min:.2e}, {wl_max:.2e}]m"
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
f"CustomRaySource(num_rays={self._num_rays}, "
|
|
277
|
+
f"wavelengths={wl_str}, power={self.power:.2e}W)"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def from_spectral_fan(
|
|
282
|
+
cls,
|
|
283
|
+
origin: tuple[float, float, float],
|
|
284
|
+
direction: tuple[float, float, float],
|
|
285
|
+
wavelength_range: tuple[float, float],
|
|
286
|
+
num_rays: int,
|
|
287
|
+
total_power: float = 1.0,
|
|
288
|
+
) -> "CustomRaySource":
|
|
289
|
+
"""
|
|
290
|
+
Create rays with same position/direction but varying wavelengths.
|
|
291
|
+
|
|
292
|
+
Convenience factory for chromatic dispersion studies.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
origin : tuple of float
|
|
297
|
+
Starting position for all rays (x, y, z) in meters.
|
|
298
|
+
direction : tuple of float
|
|
299
|
+
Direction for all rays (will be normalized).
|
|
300
|
+
wavelength_range : tuple of float
|
|
301
|
+
(min_wavelength, max_wavelength) in meters.
|
|
302
|
+
num_rays : int
|
|
303
|
+
Number of rays to create.
|
|
304
|
+
total_power : float, optional
|
|
305
|
+
Total power distributed uniformly. Default is 1.0 W.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
CustomRaySource
|
|
310
|
+
Source with spectral distribution.
|
|
311
|
+
|
|
312
|
+
Examples
|
|
313
|
+
--------
|
|
314
|
+
>>> # Visible spectrum from single point
|
|
315
|
+
>>> source = CustomRaySource.from_spectral_fan(
|
|
316
|
+
... origin=(0, 0, 0),
|
|
317
|
+
... direction=(1, 0, 0),
|
|
318
|
+
... wavelength_range=(400e-9, 700e-9),
|
|
319
|
+
... num_rays=100,
|
|
320
|
+
... )
|
|
321
|
+
"""
|
|
322
|
+
positions = np.tile(origin, (num_rays, 1)).astype(np.float32)
|
|
323
|
+
directions = np.tile(direction, (num_rays, 1)).astype(np.float32)
|
|
324
|
+
wavelengths = np.linspace(
|
|
325
|
+
wavelength_range[0], wavelength_range[1], num_rays
|
|
326
|
+
).astype(np.float32)
|
|
327
|
+
intensities = np.full(num_rays, total_power / num_rays, dtype=np.float32)
|
|
328
|
+
|
|
329
|
+
return cls(positions, directions, wavelengths, intensities)
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def from_angular_fan(
|
|
333
|
+
cls,
|
|
334
|
+
origin: tuple[float, float, float],
|
|
335
|
+
base_direction: tuple[float, float, float],
|
|
336
|
+
angle_range: tuple[float, float],
|
|
337
|
+
num_rays: int,
|
|
338
|
+
wavelength: float = 550e-9,
|
|
339
|
+
total_power: float = 1.0,
|
|
340
|
+
fan_axis: str = "vertical",
|
|
341
|
+
) -> "CustomRaySource":
|
|
342
|
+
"""
|
|
343
|
+
Create rays from same position with varying angles.
|
|
344
|
+
|
|
345
|
+
Convenience factory for refraction/reflection studies.
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
origin : tuple of float
|
|
350
|
+
Starting position for all rays (x, y, z) in meters.
|
|
351
|
+
base_direction : tuple of float
|
|
352
|
+
Central direction of the fan (will be normalized).
|
|
353
|
+
angle_range : tuple of float
|
|
354
|
+
(min_angle, max_angle) deviation from base direction in radians.
|
|
355
|
+
num_rays : int
|
|
356
|
+
Number of rays to create.
|
|
357
|
+
wavelength : float, optional
|
|
358
|
+
Wavelength for all rays. Default is 550 nm.
|
|
359
|
+
total_power : float, optional
|
|
360
|
+
Total power distributed uniformly. Default is 1.0 W.
|
|
361
|
+
fan_axis : str, optional
|
|
362
|
+
'vertical' for z-rotation, 'horizontal' for y-rotation.
|
|
363
|
+
Default is 'vertical'.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
CustomRaySource
|
|
368
|
+
Source with angular distribution.
|
|
369
|
+
|
|
370
|
+
Examples
|
|
371
|
+
--------
|
|
372
|
+
>>> # Fan of rays spanning +/- 10 degrees
|
|
373
|
+
>>> source = CustomRaySource.from_angular_fan(
|
|
374
|
+
... origin=(0, 0, 1000),
|
|
375
|
+
... base_direction=(1, 0, 0),
|
|
376
|
+
... angle_range=(-0.17, 0.17), # ~10 degrees
|
|
377
|
+
... num_rays=50,
|
|
378
|
+
... )
|
|
379
|
+
"""
|
|
380
|
+
positions = np.tile(origin, (num_rays, 1)).astype(np.float32)
|
|
381
|
+
|
|
382
|
+
# Normalize base direction
|
|
383
|
+
base = np.array(base_direction, dtype=np.float32)
|
|
384
|
+
base = base / np.linalg.norm(base)
|
|
385
|
+
|
|
386
|
+
# Create angles
|
|
387
|
+
angles = np.linspace(angle_range[0], angle_range[1], num_rays)
|
|
388
|
+
|
|
389
|
+
# Create directions by rotating base direction
|
|
390
|
+
directions = np.zeros((num_rays, 3), dtype=np.float32)
|
|
391
|
+
if fan_axis == "vertical":
|
|
392
|
+
# Rotate in xz plane (vertical fan)
|
|
393
|
+
cos_a = np.cos(angles)
|
|
394
|
+
sin_a = np.sin(angles)
|
|
395
|
+
directions[:, 0] = base[0] * cos_a - base[2] * sin_a
|
|
396
|
+
directions[:, 1] = base[1]
|
|
397
|
+
directions[:, 2] = base[0] * sin_a + base[2] * cos_a
|
|
398
|
+
else:
|
|
399
|
+
# Rotate in xy plane (horizontal fan)
|
|
400
|
+
cos_a = np.cos(angles)
|
|
401
|
+
sin_a = np.sin(angles)
|
|
402
|
+
directions[:, 0] = base[0] * cos_a - base[1] * sin_a
|
|
403
|
+
directions[:, 1] = base[0] * sin_a + base[1] * cos_a
|
|
404
|
+
directions[:, 2] = base[2]
|
|
405
|
+
|
|
406
|
+
wavelengths = np.full(num_rays, wavelength, dtype=np.float32)
|
|
407
|
+
intensities = np.full(num_rays, total_power / num_rays, dtype=np.float32)
|
|
408
|
+
|
|
409
|
+
return cls(positions, directions, wavelengths, intensities)
|
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
Diverging Beam Source Implementation
|
|
36
|
+
|
|
37
|
+
Provides a diverging beam source with angular spread, suitable for modeling
|
|
38
|
+
fiber optic outputs, LEDs, and other extended sources.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
>>> from surface_roughness.sources import DivergingBeam
|
|
43
|
+
>>>
|
|
44
|
+
>>> source = DivergingBeam(
|
|
45
|
+
... origin=(0, 0, 0),
|
|
46
|
+
... mean_direction=(0, 0, 1),
|
|
47
|
+
... divergence_angle=0.05, # ~2.9 degrees
|
|
48
|
+
... num_rays=1000,
|
|
49
|
+
... wavelength=850e-9,
|
|
50
|
+
... power=1.0
|
|
51
|
+
... )
|
|
52
|
+
>>> rays = source.generate()
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
import numpy as np
|
|
56
|
+
|
|
57
|
+
from ..utilities.ray_data import RayBatch
|
|
58
|
+
from .base import RaySource
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DivergingBeam(RaySource):
|
|
62
|
+
"""
|
|
63
|
+
Beam with angular divergence.
|
|
64
|
+
|
|
65
|
+
Generates rays from a single point with directions distributed
|
|
66
|
+
within a cone around the mean direction. Suitable for modeling
|
|
67
|
+
fiber optic outputs, LEDs, and similar diverging sources.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
origin : tuple of float
|
|
72
|
+
Source position (x, y, z) in meters.
|
|
73
|
+
mean_direction : tuple of float
|
|
74
|
+
Mean beam direction (dx, dy, dz), will be normalized.
|
|
75
|
+
divergence_angle : float
|
|
76
|
+
Half-angle divergence in radians (cone half-angle).
|
|
77
|
+
Must be in range (0, π/2).
|
|
78
|
+
num_rays : int
|
|
79
|
+
Number of rays to generate.
|
|
80
|
+
wavelength : float or tuple of float
|
|
81
|
+
Single wavelength (m) or (min, max) range.
|
|
82
|
+
power : float, optional
|
|
83
|
+
Total source power in watts. Default is 1.0.
|
|
84
|
+
|
|
85
|
+
Attributes
|
|
86
|
+
----------
|
|
87
|
+
origin : ndarray, shape (3,)
|
|
88
|
+
Source position.
|
|
89
|
+
mean_direction : ndarray, shape (3,)
|
|
90
|
+
Normalized mean beam direction.
|
|
91
|
+
divergence_angle : float
|
|
92
|
+
Cone half-angle in radians.
|
|
93
|
+
|
|
94
|
+
Notes
|
|
95
|
+
-----
|
|
96
|
+
The angular distribution is uniform within the cone. For Lambertian
|
|
97
|
+
sources (cosine distribution), a different implementation would be
|
|
98
|
+
needed.
|
|
99
|
+
|
|
100
|
+
Examples
|
|
101
|
+
--------
|
|
102
|
+
>>> # Fiber output with 0.1 rad NA
|
|
103
|
+
>>> source = DivergingBeam(
|
|
104
|
+
... origin=(0, 0, 0),
|
|
105
|
+
... mean_direction=(0, 0, 1),
|
|
106
|
+
... divergence_angle=0.1,
|
|
107
|
+
... num_rays=5000,
|
|
108
|
+
... wavelength=1550e-9,
|
|
109
|
+
... power=1e-3
|
|
110
|
+
... )
|
|
111
|
+
|
|
112
|
+
>>> # LED with wide angle
|
|
113
|
+
>>> source = DivergingBeam(
|
|
114
|
+
... origin=(0, 0.1, 0),
|
|
115
|
+
... mean_direction=(0, 0, 1),
|
|
116
|
+
... divergence_angle=np.radians(30), # 30 degrees
|
|
117
|
+
... num_rays=10000,
|
|
118
|
+
... wavelength=(400e-9, 700e-9),
|
|
119
|
+
... power=0.5
|
|
120
|
+
... )
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
origin: tuple[float, float, float],
|
|
126
|
+
mean_direction: tuple[float, float, float],
|
|
127
|
+
divergence_angle: float,
|
|
128
|
+
num_rays: int,
|
|
129
|
+
wavelength: float | tuple[float, float],
|
|
130
|
+
power: float = 1.0,
|
|
131
|
+
):
|
|
132
|
+
"""
|
|
133
|
+
Initialize diverging beam.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
origin : tuple of float
|
|
138
|
+
Source position (x, y, z) in meters.
|
|
139
|
+
mean_direction : tuple of float
|
|
140
|
+
Mean beam direction, will be normalized.
|
|
141
|
+
divergence_angle : float
|
|
142
|
+
Cone half-angle in radians.
|
|
143
|
+
num_rays : int
|
|
144
|
+
Number of rays to generate.
|
|
145
|
+
wavelength : float or tuple of float
|
|
146
|
+
Wavelength in meters or (min, max) range.
|
|
147
|
+
power : float, optional
|
|
148
|
+
Total source power in watts. Default is 1.0.
|
|
149
|
+
|
|
150
|
+
Raises
|
|
151
|
+
------
|
|
152
|
+
ValueError
|
|
153
|
+
If divergence_angle not in (0, π/2).
|
|
154
|
+
"""
|
|
155
|
+
super().__init__(num_rays, wavelength, power)
|
|
156
|
+
self.origin = np.array(origin, dtype=np.float32)
|
|
157
|
+
self.divergence_angle = divergence_angle
|
|
158
|
+
|
|
159
|
+
# Normalize mean direction
|
|
160
|
+
mean_direction_arr = np.array(mean_direction, dtype=np.float32)
|
|
161
|
+
self.mean_direction = mean_direction_arr / np.linalg.norm(mean_direction_arr)
|
|
162
|
+
|
|
163
|
+
if divergence_angle <= 0 or divergence_angle >= np.pi / 2:
|
|
164
|
+
raise ValueError("divergence_angle must be in (0, π/2)")
|
|
165
|
+
|
|
166
|
+
def generate(self) -> RayBatch:
|
|
167
|
+
"""
|
|
168
|
+
Generate diverging beam.
|
|
169
|
+
|
|
170
|
+
Creates rays from the origin with directions uniformly distributed
|
|
171
|
+
within a cone around the mean direction.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
RayBatch
|
|
176
|
+
Ray batch with diverging direction distribution.
|
|
177
|
+
|
|
178
|
+
Notes
|
|
179
|
+
-----
|
|
180
|
+
Uses spherical coordinates relative to the mean direction to
|
|
181
|
+
generate uniformly distributed directions within the cone.
|
|
182
|
+
"""
|
|
183
|
+
rays = self._allocate_rays()
|
|
184
|
+
|
|
185
|
+
# All rays start at origin
|
|
186
|
+
rays.positions[:] = self.origin
|
|
187
|
+
|
|
188
|
+
# Create perpendicular basis for direction perturbation
|
|
189
|
+
v1, v2 = self._create_perpendicular_basis(self.mean_direction)
|
|
190
|
+
|
|
191
|
+
# Generate random angles within cone
|
|
192
|
+
# Azimuthal angle: uniform in [0, 2π)
|
|
193
|
+
theta = np.random.uniform(0, 2 * np.pi, self.num_rays)
|
|
194
|
+
# Polar angle: uniform in [0, divergence_angle]
|
|
195
|
+
# For uniform distribution on cone cap, should sample cos(phi) uniformly
|
|
196
|
+
# but for simplicity we use uniform phi for now
|
|
197
|
+
phi = np.random.uniform(0, self.divergence_angle, self.num_rays)
|
|
198
|
+
|
|
199
|
+
# Compute perturbed directions
|
|
200
|
+
sin_phi = np.sin(phi)
|
|
201
|
+
cos_phi = np.cos(phi)
|
|
202
|
+
|
|
203
|
+
# Direction in local coordinates (z = mean_direction)
|
|
204
|
+
local_x = sin_phi * np.cos(theta)
|
|
205
|
+
local_y = sin_phi * np.sin(theta)
|
|
206
|
+
local_z = cos_phi
|
|
207
|
+
|
|
208
|
+
# Transform to global coordinates
|
|
209
|
+
rays.directions[:] = (
|
|
210
|
+
local_x[:, np.newaxis] * v1
|
|
211
|
+
+ local_y[:, np.newaxis] * v2
|
|
212
|
+
+ local_z[:, np.newaxis] * self.mean_direction
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Normalize (should already be normalized, but ensure numerical stability)
|
|
216
|
+
norms = np.linalg.norm(rays.directions, axis=1, keepdims=True)
|
|
217
|
+
rays.directions[:] /= norms
|
|
218
|
+
|
|
219
|
+
self._assign_wavelengths(rays)
|
|
220
|
+
|
|
221
|
+
return rays
|
|
222
|
+
|
|
223
|
+
def __repr__(self) -> str:
|
|
224
|
+
"""Return string representation."""
|
|
225
|
+
return (
|
|
226
|
+
f"DivergingBeam(origin={self.origin.tolist()}, "
|
|
227
|
+
f"divergence_angle={self.divergence_angle:.4f})"
|
|
228
|
+
)
|