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/base.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
Ray Source Base Class
|
|
36
|
+
|
|
37
|
+
Defines the abstract base class for all ray sources in the raytracing
|
|
38
|
+
framework. Sources generate initial ray conditions for simulation.
|
|
39
|
+
|
|
40
|
+
Design Notes
|
|
41
|
+
------------
|
|
42
|
+
- Follows Interface Segregation Principle: focused on ray generation
|
|
43
|
+
- Derived classes implement specific spatial/angular distributions
|
|
44
|
+
- Ray intensities are normalized to conserve total power
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from abc import ABC, abstractmethod
|
|
48
|
+
|
|
49
|
+
import numpy as np
|
|
50
|
+
from numpy.typing import NDArray
|
|
51
|
+
|
|
52
|
+
from ..utilities.ray_data import RayBatch, create_ray_batch
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RaySource(ABC):
|
|
56
|
+
"""
|
|
57
|
+
Abstract base class for ray sources.
|
|
58
|
+
|
|
59
|
+
A ray source defines initial conditions for a ray batch, including
|
|
60
|
+
spatial distribution, angular distribution, wavelength spectrum,
|
|
61
|
+
and intensity distribution.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
num_rays : int
|
|
66
|
+
Number of rays to generate. Must be positive.
|
|
67
|
+
wavelength : float or tuple of float
|
|
68
|
+
Single wavelength in meters for monochromatic source, or
|
|
69
|
+
(min, max) tuple for polychromatic source.
|
|
70
|
+
power : float, optional
|
|
71
|
+
Total source power in watts. Default is 1.0.
|
|
72
|
+
|
|
73
|
+
Attributes
|
|
74
|
+
----------
|
|
75
|
+
num_rays : int
|
|
76
|
+
Number of rays generated by this source.
|
|
77
|
+
wavelength : float or tuple
|
|
78
|
+
Wavelength specification.
|
|
79
|
+
power : float
|
|
80
|
+
Total source power.
|
|
81
|
+
|
|
82
|
+
Notes
|
|
83
|
+
-----
|
|
84
|
+
Derived classes must implement the `generate()` method which returns
|
|
85
|
+
a fully initialized RayBatch.
|
|
86
|
+
|
|
87
|
+
The ray intensities should sum to the total power (conservation of energy).
|
|
88
|
+
When generating rays, use `_allocate_rays()` to create the batch with
|
|
89
|
+
proper initialization and `_assign_wavelengths()` to set wavelengths.
|
|
90
|
+
|
|
91
|
+
Examples
|
|
92
|
+
--------
|
|
93
|
+
Creating a custom source:
|
|
94
|
+
|
|
95
|
+
>>> class MySource(RaySource):
|
|
96
|
+
... def __init__(self, position, num_rays, wavelength, power=1.0):
|
|
97
|
+
... super().__init__(num_rays, wavelength, power)
|
|
98
|
+
... self.position = np.array(position)
|
|
99
|
+
...
|
|
100
|
+
... def generate(self):
|
|
101
|
+
... rays = self._allocate_rays()
|
|
102
|
+
... rays.positions[:] = self.position
|
|
103
|
+
... # Set directions...
|
|
104
|
+
... self._assign_wavelengths(rays)
|
|
105
|
+
... return rays
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
num_rays: int,
|
|
111
|
+
wavelength: float | tuple[float, float],
|
|
112
|
+
power: float = 1.0,
|
|
113
|
+
):
|
|
114
|
+
"""
|
|
115
|
+
Initialize ray source.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
num_rays : int
|
|
120
|
+
Number of rays to generate. Must be positive.
|
|
121
|
+
wavelength : float or tuple of float
|
|
122
|
+
Single wavelength (m) or (min, max) range.
|
|
123
|
+
power : float, optional
|
|
124
|
+
Total source power in watts. Default is 1.0.
|
|
125
|
+
|
|
126
|
+
Raises
|
|
127
|
+
------
|
|
128
|
+
ValueError
|
|
129
|
+
If num_rays <= 0, power <= 0, wavelength <= 0, or
|
|
130
|
+
wavelength range is invalid.
|
|
131
|
+
"""
|
|
132
|
+
self.num_rays = num_rays
|
|
133
|
+
self.wavelength = wavelength
|
|
134
|
+
self.power = power
|
|
135
|
+
|
|
136
|
+
# Validate parameters
|
|
137
|
+
if num_rays <= 0:
|
|
138
|
+
raise ValueError("num_rays must be positive")
|
|
139
|
+
if power <= 0:
|
|
140
|
+
raise ValueError("power must be positive")
|
|
141
|
+
|
|
142
|
+
if isinstance(wavelength, tuple):
|
|
143
|
+
if len(wavelength) != 2:
|
|
144
|
+
raise ValueError("wavelength range must be (min, max)")
|
|
145
|
+
if wavelength[0] >= wavelength[1]:
|
|
146
|
+
raise ValueError("wavelength min must be less than max")
|
|
147
|
+
if wavelength[0] <= 0:
|
|
148
|
+
raise ValueError("wavelength values must be positive")
|
|
149
|
+
elif wavelength <= 0:
|
|
150
|
+
raise ValueError("wavelength must be positive")
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def generate(self) -> RayBatch:
|
|
154
|
+
"""
|
|
155
|
+
Generate ray batch with initial conditions.
|
|
156
|
+
|
|
157
|
+
Creates and initializes a RayBatch with positions, directions,
|
|
158
|
+
wavelengths, and intensities according to the source configuration.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
RayBatch
|
|
163
|
+
Initialized rays ready for propagation.
|
|
164
|
+
|
|
165
|
+
Notes
|
|
166
|
+
-----
|
|
167
|
+
Implementations should ensure:
|
|
168
|
+
- All rays are marked as active
|
|
169
|
+
- Directions are normalized
|
|
170
|
+
- Intensities sum to total power
|
|
171
|
+
- Wavelengths are set appropriately
|
|
172
|
+
"""
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
def _allocate_rays(self) -> RayBatch:
|
|
176
|
+
"""
|
|
177
|
+
Allocate ray batch with uniform intensity distribution.
|
|
178
|
+
|
|
179
|
+
Creates a RayBatch with all rays active and intensities set
|
|
180
|
+
so they sum to the total power.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
RayBatch
|
|
185
|
+
Allocated ray batch with intensities initialized.
|
|
186
|
+
|
|
187
|
+
Notes
|
|
188
|
+
-----
|
|
189
|
+
Called by generate() implementations to create the ray batch.
|
|
190
|
+
"""
|
|
191
|
+
rays = create_ray_batch(num_rays=self.num_rays)
|
|
192
|
+
rays.active[:] = True
|
|
193
|
+
rays.intensities[:] = self.power / self.num_rays
|
|
194
|
+
return rays
|
|
195
|
+
|
|
196
|
+
def _assign_wavelengths(self, rays: RayBatch) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Assign wavelengths to rays.
|
|
199
|
+
|
|
200
|
+
For monochromatic sources, sets all wavelengths to the single value.
|
|
201
|
+
For polychromatic sources, samples uniformly from the range.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
rays : RayBatch
|
|
206
|
+
Ray batch to assign wavelengths to.
|
|
207
|
+
|
|
208
|
+
Notes
|
|
209
|
+
-----
|
|
210
|
+
Called by generate() implementations after setting positions
|
|
211
|
+
and directions.
|
|
212
|
+
"""
|
|
213
|
+
if isinstance(self.wavelength, tuple):
|
|
214
|
+
# Uniform distribution over range
|
|
215
|
+
rays.wavelengths[:] = np.random.uniform(
|
|
216
|
+
self.wavelength[0], self.wavelength[1], self.num_rays
|
|
217
|
+
).astype(np.float32)
|
|
218
|
+
else:
|
|
219
|
+
# Monochromatic
|
|
220
|
+
rays.wavelengths[:] = self.wavelength
|
|
221
|
+
|
|
222
|
+
def _create_perpendicular_basis(
|
|
223
|
+
self, direction: NDArray[np.float32]
|
|
224
|
+
) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
|
|
225
|
+
"""
|
|
226
|
+
Create two unit vectors perpendicular to a direction.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
direction : ndarray, shape (3,)
|
|
231
|
+
Direction vector (must be normalized).
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
v1 : ndarray, shape (3,)
|
|
236
|
+
First perpendicular unit vector.
|
|
237
|
+
v2 : ndarray, shape (3,)
|
|
238
|
+
Second perpendicular unit vector (perpendicular to both
|
|
239
|
+
direction and v1).
|
|
240
|
+
|
|
241
|
+
Notes
|
|
242
|
+
-----
|
|
243
|
+
Used for generating positions/directions in a plane perpendicular
|
|
244
|
+
to the beam axis.
|
|
245
|
+
"""
|
|
246
|
+
if abs(direction[2]) < 0.9:
|
|
247
|
+
v1 = np.cross(direction, np.array([0, 0, 1], dtype=np.float32))
|
|
248
|
+
else:
|
|
249
|
+
v1 = np.cross(direction, np.array([1, 0, 0], dtype=np.float32))
|
|
250
|
+
v1 = v1 / np.linalg.norm(v1)
|
|
251
|
+
v2 = np.cross(direction, v1)
|
|
252
|
+
return v1.astype(np.float32), v2.astype(np.float32)
|
|
253
|
+
|
|
254
|
+
def __repr__(self) -> str:
|
|
255
|
+
"""Return string representation."""
|
|
256
|
+
wl_str = (
|
|
257
|
+
f"({self.wavelength[0]:.2e}, {self.wavelength[1]:.2e})"
|
|
258
|
+
if isinstance(self.wavelength, tuple)
|
|
259
|
+
else f"{self.wavelength:.2e}"
|
|
260
|
+
)
|
|
261
|
+
return (
|
|
262
|
+
f"{self.__class__.__name__}(num_rays={self.num_rays}, "
|
|
263
|
+
f"wavelength={wl_str}, power={self.power})"
|
|
264
|
+
)
|
|
@@ -0,0 +1,252 @@
|
|
|
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
|
+
Collimated Beam Source Implementation
|
|
36
|
+
|
|
37
|
+
Provides a collimated (parallel) beam source with optional spatial intensity
|
|
38
|
+
profiles. Useful for modeling laser beams and plane wave illumination.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
>>> from surface_roughness.sources import CollimatedBeam
|
|
43
|
+
>>>
|
|
44
|
+
>>> source = CollimatedBeam(
|
|
45
|
+
... center=(0, 0, -10),
|
|
46
|
+
... direction=(0, 0, 1),
|
|
47
|
+
... radius=0.001, # 1 mm
|
|
48
|
+
... num_rays=5000,
|
|
49
|
+
... wavelength=633e-9, # HeNe laser
|
|
50
|
+
... power=5e-3
|
|
51
|
+
... )
|
|
52
|
+
>>> rays = source.generate()
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from typing import Literal
|
|
56
|
+
|
|
57
|
+
import numpy as np
|
|
58
|
+
|
|
59
|
+
from ..utilities.ray_data import RayBatch
|
|
60
|
+
from .base import RaySource
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CollimatedBeam(RaySource):
|
|
64
|
+
"""
|
|
65
|
+
Collimated beam with parallel rays.
|
|
66
|
+
|
|
67
|
+
Generates rays with identical directions and positions distributed
|
|
68
|
+
in a circular cross-section. Supports uniform and Gaussian intensity
|
|
69
|
+
profiles.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
center : tuple of float
|
|
74
|
+
Beam center position (x, y, z) in meters.
|
|
75
|
+
direction : tuple of float
|
|
76
|
+
Beam propagation direction (dx, dy, dz), will be normalized.
|
|
77
|
+
radius : float
|
|
78
|
+
Beam radius in meters.
|
|
79
|
+
num_rays : int
|
|
80
|
+
Number of rays to generate.
|
|
81
|
+
wavelength : float or tuple of float
|
|
82
|
+
Single wavelength (m) or (min, max) range.
|
|
83
|
+
power : float, optional
|
|
84
|
+
Total beam power in watts. Default is 1.0.
|
|
85
|
+
profile : {'uniform', 'gaussian'}, optional
|
|
86
|
+
Spatial intensity profile. Default is 'uniform'.
|
|
87
|
+
|
|
88
|
+
Attributes
|
|
89
|
+
----------
|
|
90
|
+
center : ndarray, shape (3,)
|
|
91
|
+
Beam center position.
|
|
92
|
+
direction : ndarray, shape (3,)
|
|
93
|
+
Normalized beam direction.
|
|
94
|
+
radius : float
|
|
95
|
+
Beam radius.
|
|
96
|
+
profile : str
|
|
97
|
+
Intensity profile type.
|
|
98
|
+
|
|
99
|
+
Notes
|
|
100
|
+
-----
|
|
101
|
+
For Gaussian profile, the radius corresponds to 2σ (where σ is the
|
|
102
|
+
standard deviation of the Gaussian). Ray intensities are weighted
|
|
103
|
+
according to the Gaussian distribution.
|
|
104
|
+
|
|
105
|
+
Ray timing is initialized so that all rays cross the reference plane
|
|
106
|
+
(at center) at time=0. This ensures coherent phase fronts.
|
|
107
|
+
|
|
108
|
+
Examples
|
|
109
|
+
--------
|
|
110
|
+
>>> # Uniform circular beam
|
|
111
|
+
>>> source = CollimatedBeam(
|
|
112
|
+
... center=(0, 0, 0),
|
|
113
|
+
... direction=(0, 0, 1),
|
|
114
|
+
... radius=1e-3,
|
|
115
|
+
... num_rays=5000,
|
|
116
|
+
... wavelength=633e-9,
|
|
117
|
+
... power=5e-3
|
|
118
|
+
... )
|
|
119
|
+
|
|
120
|
+
>>> # Gaussian beam profile
|
|
121
|
+
>>> source = CollimatedBeam(
|
|
122
|
+
... center=(0, 0, 0),
|
|
123
|
+
... direction=(0, 0, 1),
|
|
124
|
+
... radius=2e-3,
|
|
125
|
+
... num_rays=10000,
|
|
126
|
+
... wavelength=1064e-9,
|
|
127
|
+
... power=1.0,
|
|
128
|
+
... profile='gaussian'
|
|
129
|
+
... )
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
center: tuple[float, float, float],
|
|
135
|
+
direction: tuple[float, float, float],
|
|
136
|
+
radius: float,
|
|
137
|
+
num_rays: int,
|
|
138
|
+
wavelength: float | tuple[float, float],
|
|
139
|
+
power: float = 1.0,
|
|
140
|
+
profile: Literal["uniform", "gaussian"] = "uniform",
|
|
141
|
+
):
|
|
142
|
+
"""
|
|
143
|
+
Initialize collimated beam.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
center : tuple of float
|
|
148
|
+
Beam center position (x, y, z) in meters.
|
|
149
|
+
direction : tuple of float
|
|
150
|
+
Beam propagation direction, will be normalized.
|
|
151
|
+
radius : float
|
|
152
|
+
Beam radius in meters.
|
|
153
|
+
num_rays : int
|
|
154
|
+
Number of rays to generate.
|
|
155
|
+
wavelength : float or tuple of float
|
|
156
|
+
Wavelength in meters or (min, max) range.
|
|
157
|
+
power : float, optional
|
|
158
|
+
Total beam power in watts. Default is 1.0.
|
|
159
|
+
profile : {'uniform', 'gaussian'}, optional
|
|
160
|
+
Spatial intensity profile. Default is 'uniform'.
|
|
161
|
+
|
|
162
|
+
Raises
|
|
163
|
+
------
|
|
164
|
+
ValueError
|
|
165
|
+
If radius <= 0 or profile not in {'uniform', 'gaussian'}.
|
|
166
|
+
"""
|
|
167
|
+
super().__init__(num_rays, wavelength, power)
|
|
168
|
+
self.center = np.array(center, dtype=np.float32)
|
|
169
|
+
self.radius = radius
|
|
170
|
+
self.profile = profile
|
|
171
|
+
|
|
172
|
+
# Normalize direction
|
|
173
|
+
direction_arr = np.array(direction, dtype=np.float32)
|
|
174
|
+
self.direction = direction_arr / np.linalg.norm(direction_arr)
|
|
175
|
+
|
|
176
|
+
if radius <= 0:
|
|
177
|
+
raise ValueError("radius must be positive")
|
|
178
|
+
if profile not in ("uniform", "gaussian"):
|
|
179
|
+
raise ValueError("profile must be 'uniform' or 'gaussian'")
|
|
180
|
+
|
|
181
|
+
def generate(self) -> RayBatch:
|
|
182
|
+
"""
|
|
183
|
+
Generate collimated beam.
|
|
184
|
+
|
|
185
|
+
Creates rays with parallel directions and positions sampled
|
|
186
|
+
in a disk perpendicular to the beam direction.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
RayBatch
|
|
191
|
+
Ray batch with collimated ray directions.
|
|
192
|
+
|
|
193
|
+
Notes
|
|
194
|
+
-----
|
|
195
|
+
For uniform profile, positions are uniformly distributed in a disk.
|
|
196
|
+
For Gaussian profile, positions follow a 2D Gaussian distribution
|
|
197
|
+
and intensities are weighted accordingly.
|
|
198
|
+
"""
|
|
199
|
+
rays = self._allocate_rays()
|
|
200
|
+
|
|
201
|
+
# All rays have same direction
|
|
202
|
+
rays.directions[:] = self.direction
|
|
203
|
+
|
|
204
|
+
# Create perpendicular basis
|
|
205
|
+
v1, v2 = self._create_perpendicular_basis(self.direction)
|
|
206
|
+
|
|
207
|
+
if self.profile == "uniform":
|
|
208
|
+
# Uniform disk sampling
|
|
209
|
+
r = self.radius * np.sqrt(np.random.uniform(0, 1, self.num_rays))
|
|
210
|
+
theta = np.random.uniform(0, 2 * np.pi, self.num_rays)
|
|
211
|
+
|
|
212
|
+
x_local = r * np.cos(theta)
|
|
213
|
+
y_local = r * np.sin(theta)
|
|
214
|
+
else: # gaussian
|
|
215
|
+
# Gaussian profile (radius = 2*sigma)
|
|
216
|
+
sigma = self.radius / 2
|
|
217
|
+
x_local = np.random.normal(0, sigma, self.num_rays)
|
|
218
|
+
y_local = np.random.normal(0, sigma, self.num_rays)
|
|
219
|
+
|
|
220
|
+
# Adjust intensities for Gaussian profile
|
|
221
|
+
r_squared = x_local**2 + y_local**2
|
|
222
|
+
rays.intensities[:] *= np.exp(-r_squared / (2 * sigma**2))
|
|
223
|
+
# Renormalize to conserve power
|
|
224
|
+
rays.intensities[:] *= self.power / np.sum(rays.intensities)
|
|
225
|
+
|
|
226
|
+
# Convert to 3D positions
|
|
227
|
+
rays.positions[:] = (
|
|
228
|
+
self.center + x_local[:, np.newaxis] * v1 + y_local[:, np.newaxis] * v2
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Initialize accumulated_time for coherent phase front
|
|
232
|
+
# All rays should cross the reference plane (at center) at t=0
|
|
233
|
+
c = 299792458.0 # Speed of light in vacuum
|
|
234
|
+
n = 1.0 # Assume initial medium is air/vacuum
|
|
235
|
+
|
|
236
|
+
# Distance along beam direction from center to each ray position
|
|
237
|
+
offset_along_beam = np.sum(
|
|
238
|
+
(rays.positions - self.center) * self.direction, axis=1
|
|
239
|
+
)
|
|
240
|
+
# Time offset: negative for rays ahead, positive for rays behind
|
|
241
|
+
rays.accumulated_time[:] = -offset_along_beam * n / c
|
|
242
|
+
|
|
243
|
+
self._assign_wavelengths(rays)
|
|
244
|
+
|
|
245
|
+
return rays
|
|
246
|
+
|
|
247
|
+
def __repr__(self) -> str:
|
|
248
|
+
"""Return string representation."""
|
|
249
|
+
return (
|
|
250
|
+
f"CollimatedBeam(center={self.center.tolist()}, "
|
|
251
|
+
f"radius={self.radius}, profile='{self.profile}')"
|
|
252
|
+
)
|