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,272 @@
|
|
|
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
|
+
Gaussian Beam Source Implementation
|
|
36
|
+
|
|
37
|
+
Provides a Gaussian beam source following paraxial beam optics.
|
|
38
|
+
Suitable for modeling focused laser beams.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
>>> from surface_roughness.sources import GaussianBeam
|
|
43
|
+
>>>
|
|
44
|
+
>>> source = GaussianBeam(
|
|
45
|
+
... waist_position=(0, 0, 0),
|
|
46
|
+
... direction=(0, 0, 1),
|
|
47
|
+
... waist_radius=1e-3, # 1 mm waist
|
|
48
|
+
... num_rays=5000,
|
|
49
|
+
... wavelength=1064e-9, # Nd:YAG
|
|
50
|
+
... power=10e-3
|
|
51
|
+
... )
|
|
52
|
+
>>> rays = source.generate()
|
|
53
|
+
|
|
54
|
+
References
|
|
55
|
+
----------
|
|
56
|
+
.. [1] Siegman, A. E. (1986). Lasers. University Science Books.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
import numpy as np
|
|
60
|
+
|
|
61
|
+
from ..utilities.ray_data import RayBatch
|
|
62
|
+
from .base import RaySource
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GaussianBeam(RaySource):
|
|
66
|
+
"""
|
|
67
|
+
Gaussian beam with specified waist and Rayleigh range.
|
|
68
|
+
|
|
69
|
+
Implements paraxial Gaussian beam using ray approximation.
|
|
70
|
+
Each ray represents a wavefront normal, with intensity weighted
|
|
71
|
+
according to the Gaussian profile.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
waist_position : tuple of float
|
|
76
|
+
Position of beam waist (x, y, z) in meters.
|
|
77
|
+
direction : tuple of float
|
|
78
|
+
Beam axis direction (dx, dy, dz), will be normalized.
|
|
79
|
+
waist_radius : float
|
|
80
|
+
Beam waist radius (w₀) in meters.
|
|
81
|
+
num_rays : int
|
|
82
|
+
Number of rays to generate.
|
|
83
|
+
wavelength : float
|
|
84
|
+
Wavelength in meters. Must be monochromatic for Gaussian beam.
|
|
85
|
+
power : float, optional
|
|
86
|
+
Total beam power in watts. Default is 1.0.
|
|
87
|
+
|
|
88
|
+
Attributes
|
|
89
|
+
----------
|
|
90
|
+
waist_position : ndarray, shape (3,)
|
|
91
|
+
Beam waist position.
|
|
92
|
+
direction : ndarray, shape (3,)
|
|
93
|
+
Normalized beam direction.
|
|
94
|
+
waist_radius : float
|
|
95
|
+
Beam waist radius w₀.
|
|
96
|
+
rayleigh_range : float
|
|
97
|
+
Rayleigh range z_R = πw₀²/λ.
|
|
98
|
+
|
|
99
|
+
Notes
|
|
100
|
+
-----
|
|
101
|
+
The Gaussian beam has intensity profile:
|
|
102
|
+
|
|
103
|
+
I(r) = I₀ exp(-2r²/w²)
|
|
104
|
+
|
|
105
|
+
where w is the beam radius at distance z from the waist:
|
|
106
|
+
|
|
107
|
+
w(z) = w₀ √(1 + (z/z_R)²)
|
|
108
|
+
|
|
109
|
+
and z_R = πw₀²/λ is the Rayleigh range.
|
|
110
|
+
|
|
111
|
+
This implementation generates rays at the waist position with
|
|
112
|
+
parallel directions (plane wavefront at waist). Ray intensities
|
|
113
|
+
are weighted according to the Gaussian profile.
|
|
114
|
+
|
|
115
|
+
Examples
|
|
116
|
+
--------
|
|
117
|
+
>>> # 1 mm waist Nd:YAG laser
|
|
118
|
+
>>> source = GaussianBeam(
|
|
119
|
+
... waist_position=(0, 0, 0),
|
|
120
|
+
... direction=(0, 0, 1),
|
|
121
|
+
... waist_radius=1e-3,
|
|
122
|
+
... num_rays=5000,
|
|
123
|
+
... wavelength=1064e-9,
|
|
124
|
+
... power=10e-3
|
|
125
|
+
... )
|
|
126
|
+
>>> print(f"Rayleigh range: {source.rayleigh_range:.3f} m")
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
waist_position: tuple[float, float, float],
|
|
132
|
+
direction: tuple[float, float, float],
|
|
133
|
+
waist_radius: float,
|
|
134
|
+
num_rays: int,
|
|
135
|
+
wavelength: float,
|
|
136
|
+
power: float = 1.0,
|
|
137
|
+
):
|
|
138
|
+
"""
|
|
139
|
+
Initialize Gaussian beam.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
waist_position : tuple of float
|
|
144
|
+
Position of beam waist (x, y, z) in meters.
|
|
145
|
+
direction : tuple of float
|
|
146
|
+
Beam axis direction, will be normalized.
|
|
147
|
+
waist_radius : float
|
|
148
|
+
Beam waist radius w₀ in meters.
|
|
149
|
+
num_rays : int
|
|
150
|
+
Number of rays to generate.
|
|
151
|
+
wavelength : float
|
|
152
|
+
Wavelength in meters.
|
|
153
|
+
power : float, optional
|
|
154
|
+
Total beam power in watts. Default is 1.0.
|
|
155
|
+
|
|
156
|
+
Raises
|
|
157
|
+
------
|
|
158
|
+
ValueError
|
|
159
|
+
If wavelength is a tuple (polychromatic not supported),
|
|
160
|
+
or if waist_radius <= 0.
|
|
161
|
+
"""
|
|
162
|
+
if isinstance(wavelength, tuple):
|
|
163
|
+
raise ValueError("Gaussian beam requires monochromatic wavelength")
|
|
164
|
+
|
|
165
|
+
super().__init__(num_rays, wavelength, power)
|
|
166
|
+
self.waist_position = np.array(waist_position, dtype=np.float32)
|
|
167
|
+
self.waist_radius = waist_radius
|
|
168
|
+
|
|
169
|
+
# Normalize direction
|
|
170
|
+
direction_arr = np.array(direction, dtype=np.float32)
|
|
171
|
+
self.direction = direction_arr / np.linalg.norm(direction_arr)
|
|
172
|
+
|
|
173
|
+
# Compute Rayleigh range
|
|
174
|
+
self.rayleigh_range = np.pi * waist_radius**2 / wavelength
|
|
175
|
+
|
|
176
|
+
if waist_radius <= 0:
|
|
177
|
+
raise ValueError("waist_radius must be positive")
|
|
178
|
+
|
|
179
|
+
def generate(self) -> RayBatch:
|
|
180
|
+
"""
|
|
181
|
+
Generate Gaussian beam.
|
|
182
|
+
|
|
183
|
+
Creates rays at the waist position with Gaussian-distributed
|
|
184
|
+
positions and parallel directions.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
RayBatch
|
|
189
|
+
Ray batch with Gaussian intensity distribution.
|
|
190
|
+
|
|
191
|
+
Notes
|
|
192
|
+
-----
|
|
193
|
+
Positions are sampled from a 2D Gaussian distribution with
|
|
194
|
+
σ = w₀/2. Intensities are weighted by the Gaussian profile
|
|
195
|
+
to accurately represent the beam's energy distribution.
|
|
196
|
+
"""
|
|
197
|
+
rays = self._allocate_rays()
|
|
198
|
+
|
|
199
|
+
# Create perpendicular basis
|
|
200
|
+
v1, v2 = self._create_perpendicular_basis(self.direction)
|
|
201
|
+
|
|
202
|
+
# Generate positions at waist with Gaussian distribution
|
|
203
|
+
# Use w0/2 as sigma for sampling to get good coverage
|
|
204
|
+
sigma = self.waist_radius / 2
|
|
205
|
+
x_local = np.random.normal(0, sigma, self.num_rays)
|
|
206
|
+
y_local = np.random.normal(0, sigma, self.num_rays)
|
|
207
|
+
|
|
208
|
+
# Positions at waist
|
|
209
|
+
rays.positions[:] = (
|
|
210
|
+
self.waist_position
|
|
211
|
+
+ x_local[:, np.newaxis] * v1
|
|
212
|
+
+ y_local[:, np.newaxis] * v2
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# At waist, rays are parallel to axis (plane wavefront)
|
|
216
|
+
rays.directions[:] = self.direction
|
|
217
|
+
|
|
218
|
+
# Adjust intensities according to Gaussian profile
|
|
219
|
+
# I(r) ∝ exp(-2r²/w₀²)
|
|
220
|
+
r_squared = x_local**2 + y_local**2
|
|
221
|
+
rays.intensities[:] *= np.exp(-2 * r_squared / self.waist_radius**2)
|
|
222
|
+
|
|
223
|
+
# Renormalize to conserve power
|
|
224
|
+
rays.intensities[:] *= self.power / np.sum(rays.intensities)
|
|
225
|
+
|
|
226
|
+
# Monochromatic wavelength
|
|
227
|
+
rays.wavelengths[:] = self.wavelength
|
|
228
|
+
|
|
229
|
+
return rays
|
|
230
|
+
|
|
231
|
+
def beam_radius_at(self, z: float) -> float:
|
|
232
|
+
"""
|
|
233
|
+
Compute beam radius at distance z from waist.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
z : float
|
|
238
|
+
Distance from waist along beam axis in meters.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
float
|
|
243
|
+
Beam radius w(z) in meters.
|
|
244
|
+
|
|
245
|
+
Notes
|
|
246
|
+
-----
|
|
247
|
+
w(z) = w₀ √(1 + (z/z_R)²)
|
|
248
|
+
"""
|
|
249
|
+
return self.waist_radius * np.sqrt(1 + (z / self.rayleigh_range) ** 2)
|
|
250
|
+
|
|
251
|
+
def divergence_angle(self) -> float:
|
|
252
|
+
"""
|
|
253
|
+
Compute far-field divergence angle.
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
-------
|
|
257
|
+
float
|
|
258
|
+
Half-angle divergence in radians.
|
|
259
|
+
|
|
260
|
+
Notes
|
|
261
|
+
-----
|
|
262
|
+
θ = λ / (π w₀)
|
|
263
|
+
"""
|
|
264
|
+
return self.wavelength / (np.pi * self.waist_radius)
|
|
265
|
+
|
|
266
|
+
def __repr__(self) -> str:
|
|
267
|
+
"""Return string representation."""
|
|
268
|
+
return (
|
|
269
|
+
f"GaussianBeam(waist_position={self.waist_position.tolist()}, "
|
|
270
|
+
f"waist_radius={self.waist_radius:.2e}, "
|
|
271
|
+
f"rayleigh_range={self.rayleigh_range:.3f})"
|
|
272
|
+
)
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
Parallel Beam from Explicit Position Array
|
|
36
|
+
|
|
37
|
+
Provides a source where ray positions are explicitly specified and all rays
|
|
38
|
+
share a common direction. Useful for atmospheric propagation studies where
|
|
39
|
+
rays are launched from specific impact parameters or grid points.
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
>>> from lsurf.sources import ParallelBeamFromPositions
|
|
44
|
+
>>> import numpy as np
|
|
45
|
+
>>>
|
|
46
|
+
>>> # Create rays at different altitudes, all traveling horizontally
|
|
47
|
+
>>> positions = np.array([
|
|
48
|
+
... [-1000, 0, 100],
|
|
49
|
+
... [-1000, 0, 200],
|
|
50
|
+
... [-1000, 0, 300],
|
|
51
|
+
... ])
|
|
52
|
+
>>> source = ParallelBeamFromPositions(
|
|
53
|
+
... positions=positions,
|
|
54
|
+
... direction=(1, 0, 0),
|
|
55
|
+
... wavelength=532e-9,
|
|
56
|
+
... )
|
|
57
|
+
>>> rays = source.generate()
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
import numpy as np
|
|
61
|
+
import numpy.typing as npt
|
|
62
|
+
|
|
63
|
+
from ..utilities.ray_data import RayBatch
|
|
64
|
+
from .base import RaySource
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ParallelBeamFromPositions(RaySource):
|
|
68
|
+
"""
|
|
69
|
+
Parallel rays from explicitly specified positions.
|
|
70
|
+
|
|
71
|
+
All rays share the same direction, making this ideal for:
|
|
72
|
+
- Atmospheric refraction studies with specific impact parameters
|
|
73
|
+
- Plane wave propagation through inhomogeneous media
|
|
74
|
+
- Grid-based ray launching for wavefront analysis
|
|
75
|
+
|
|
76
|
+
Unlike CollimatedBeam which generates positions in a disk, this source
|
|
77
|
+
accepts arbitrary position arrays, enabling custom spatial distributions.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
positions : array_like, shape (N, 3)
|
|
82
|
+
Starting positions for each ray in meters.
|
|
83
|
+
direction : tuple of float
|
|
84
|
+
Direction vector for all rays (will be normalized).
|
|
85
|
+
wavelength : float or tuple of float, optional
|
|
86
|
+
Single wavelength (m) or (min, max) range. Default is 532 nm.
|
|
87
|
+
power : float, optional
|
|
88
|
+
Total source power in watts. Default is 1.0.
|
|
89
|
+
|
|
90
|
+
Attributes
|
|
91
|
+
----------
|
|
92
|
+
positions : ndarray, shape (N, 3)
|
|
93
|
+
Ray starting positions.
|
|
94
|
+
direction : ndarray, shape (3,)
|
|
95
|
+
Normalized ray direction.
|
|
96
|
+
|
|
97
|
+
Examples
|
|
98
|
+
--------
|
|
99
|
+
>>> # Rays at different impact parameters for atmospheric study
|
|
100
|
+
>>> impact_params = np.linspace(0, 10000, 100)
|
|
101
|
+
>>> positions = np.column_stack([
|
|
102
|
+
... -np.sqrt((R + 100e3)**2 - (R + impact_params)**2), # x
|
|
103
|
+
... np.zeros_like(impact_params), # y
|
|
104
|
+
... impact_params # z
|
|
105
|
+
... ])
|
|
106
|
+
>>> source = ParallelBeamFromPositions(positions, direction=(1, 0, 0))
|
|
107
|
+
>>> rays = source.generate()
|
|
108
|
+
>>> propagator.propagate(rays, total_distance=500e3, step_size=100)
|
|
109
|
+
|
|
110
|
+
>>> # Regular grid of rays
|
|
111
|
+
>>> x, y = np.meshgrid(np.linspace(-1, 1, 10), np.linspace(-1, 1, 10))
|
|
112
|
+
>>> positions = np.column_stack([x.ravel(), y.ravel(), np.zeros(100)])
|
|
113
|
+
>>> source = ParallelBeamFromPositions(positions, direction=(0, 0, 1))
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
positions: npt.ArrayLike,
|
|
119
|
+
direction: tuple[float, float, float],
|
|
120
|
+
wavelength: float | tuple[float, float] = 532e-9,
|
|
121
|
+
power: float = 1.0,
|
|
122
|
+
):
|
|
123
|
+
"""
|
|
124
|
+
Initialize parallel ray source.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
positions : array_like, shape (N, 3)
|
|
129
|
+
Starting positions for each ray in meters.
|
|
130
|
+
direction : tuple of float
|
|
131
|
+
Direction vector for all rays (will be normalized).
|
|
132
|
+
wavelength : float or tuple of float, optional
|
|
133
|
+
Wavelength in meters or (min, max) range. Default is 532 nm.
|
|
134
|
+
power : float, optional
|
|
135
|
+
Total source power in watts. Default is 1.0.
|
|
136
|
+
|
|
137
|
+
Raises
|
|
138
|
+
------
|
|
139
|
+
ValueError
|
|
140
|
+
If positions shape is invalid or direction is zero vector.
|
|
141
|
+
"""
|
|
142
|
+
# Convert and validate positions
|
|
143
|
+
self._positions = np.asarray(positions, dtype=np.float32)
|
|
144
|
+
if self._positions.ndim == 1:
|
|
145
|
+
self._positions = self._positions.reshape(1, 3)
|
|
146
|
+
if self._positions.ndim != 2 or self._positions.shape[1] != 3:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"positions must have shape (N, 3), got {self._positions.shape}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
num_rays = self._positions.shape[0]
|
|
152
|
+
|
|
153
|
+
# Initialize base class
|
|
154
|
+
super().__init__(num_rays, wavelength, power)
|
|
155
|
+
|
|
156
|
+
# Normalize direction
|
|
157
|
+
direction_arr = np.array(direction, dtype=np.float32)
|
|
158
|
+
norm = np.linalg.norm(direction_arr)
|
|
159
|
+
if norm < 1e-10:
|
|
160
|
+
raise ValueError("direction cannot be zero vector")
|
|
161
|
+
self.direction = direction_arr / norm
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def positions(self) -> npt.NDArray[np.float32]:
|
|
165
|
+
"""Ray starting positions, shape (N, 3)."""
|
|
166
|
+
return self._positions
|
|
167
|
+
|
|
168
|
+
def generate(self) -> RayBatch:
|
|
169
|
+
"""
|
|
170
|
+
Generate parallel ray batch.
|
|
171
|
+
|
|
172
|
+
Creates rays at the specified positions, all with the same direction.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
RayBatch
|
|
177
|
+
Ray batch with parallel rays ready for propagation.
|
|
178
|
+
"""
|
|
179
|
+
rays = self._allocate_rays()
|
|
180
|
+
|
|
181
|
+
# Set positions from input array
|
|
182
|
+
rays.positions[:] = self._positions
|
|
183
|
+
|
|
184
|
+
# All rays have same direction
|
|
185
|
+
rays.directions[:] = self.direction
|
|
186
|
+
|
|
187
|
+
# Assign wavelengths
|
|
188
|
+
self._assign_wavelengths(rays)
|
|
189
|
+
|
|
190
|
+
return rays
|
|
191
|
+
|
|
192
|
+
def __repr__(self) -> str:
|
|
193
|
+
"""Return string representation."""
|
|
194
|
+
return (
|
|
195
|
+
f"ParallelBeamFromPositions(num_rays={self.num_rays}, "
|
|
196
|
+
f"direction={self.direction.tolist()})"
|
|
197
|
+
)
|
lsurf/sources/point.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
Point Source Implementation
|
|
36
|
+
|
|
37
|
+
Provides an isotropic point source that emits rays uniformly in all
|
|
38
|
+
directions from a single point.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
>>> from surface_roughness.sources import PointSource
|
|
43
|
+
>>>
|
|
44
|
+
>>> source = PointSource(
|
|
45
|
+
... position=(0, 0, 0),
|
|
46
|
+
... num_rays=10000,
|
|
47
|
+
... wavelength=532e-9, # Green laser
|
|
48
|
+
... power=1e-3
|
|
49
|
+
... )
|
|
50
|
+
>>> rays = source.generate()
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
import numpy as np
|
|
54
|
+
|
|
55
|
+
from ..utilities.ray_data import RayBatch
|
|
56
|
+
from .base import RaySource
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PointSource(RaySource):
|
|
60
|
+
"""
|
|
61
|
+
Isotropic point source emitting in all directions.
|
|
62
|
+
|
|
63
|
+
Generates rays from a single point with directions uniformly
|
|
64
|
+
distributed over the unit sphere.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
position : tuple of float
|
|
69
|
+
Source position (x, y, z) in meters.
|
|
70
|
+
num_rays : int
|
|
71
|
+
Number of rays to generate.
|
|
72
|
+
wavelength : float or tuple of float
|
|
73
|
+
Single wavelength (m) or (min, max) range.
|
|
74
|
+
power : float, optional
|
|
75
|
+
Total source power in watts. Default is 1.0.
|
|
76
|
+
|
|
77
|
+
Attributes
|
|
78
|
+
----------
|
|
79
|
+
position : ndarray, shape (3,)
|
|
80
|
+
Source position in meters.
|
|
81
|
+
|
|
82
|
+
Notes
|
|
83
|
+
-----
|
|
84
|
+
The angular distribution is uniform over the full 4π steradians.
|
|
85
|
+
Each ray carries equal intensity (power / num_rays).
|
|
86
|
+
|
|
87
|
+
Examples
|
|
88
|
+
--------
|
|
89
|
+
>>> # Monochromatic point source
|
|
90
|
+
>>> source = PointSource(
|
|
91
|
+
... position=(0, 0, 0),
|
|
92
|
+
... num_rays=10000,
|
|
93
|
+
... wavelength=532e-9,
|
|
94
|
+
... power=1e-3
|
|
95
|
+
... )
|
|
96
|
+
>>> rays = source.generate()
|
|
97
|
+
|
|
98
|
+
>>> # Polychromatic source (white light LED)
|
|
99
|
+
>>> source = PointSource(
|
|
100
|
+
... position=(0, 0.1, 0),
|
|
101
|
+
... num_rays=5000,
|
|
102
|
+
... wavelength=(400e-9, 700e-9),
|
|
103
|
+
... power=0.5
|
|
104
|
+
... )
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
position: tuple[float, float, float],
|
|
110
|
+
num_rays: int,
|
|
111
|
+
wavelength: float | tuple[float, float],
|
|
112
|
+
power: float = 1.0,
|
|
113
|
+
):
|
|
114
|
+
"""
|
|
115
|
+
Initialize point source.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
position : tuple of float
|
|
120
|
+
Source position (x, y, z) in meters.
|
|
121
|
+
num_rays : int
|
|
122
|
+
Number of rays to generate.
|
|
123
|
+
wavelength : float or tuple of float
|
|
124
|
+
Wavelength in meters or (min, max) range.
|
|
125
|
+
power : float, optional
|
|
126
|
+
Total source power in watts. Default is 1.0.
|
|
127
|
+
"""
|
|
128
|
+
super().__init__(num_rays, wavelength, power)
|
|
129
|
+
self.position = np.array(position, dtype=np.float32)
|
|
130
|
+
|
|
131
|
+
def generate(self) -> RayBatch:
|
|
132
|
+
"""
|
|
133
|
+
Generate isotropic ray distribution.
|
|
134
|
+
|
|
135
|
+
Creates rays emanating from the source position with directions
|
|
136
|
+
uniformly distributed over the unit sphere.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
RayBatch
|
|
141
|
+
Ray batch with isotropic direction distribution.
|
|
142
|
+
|
|
143
|
+
Notes
|
|
144
|
+
-----
|
|
145
|
+
Uses the standard method for uniform sphere sampling:
|
|
146
|
+
- θ uniformly distributed in [0, 2π)
|
|
147
|
+
- cos(φ) uniformly distributed in [-1, 1]
|
|
148
|
+
"""
|
|
149
|
+
rays = self._allocate_rays()
|
|
150
|
+
|
|
151
|
+
# All rays start at same position
|
|
152
|
+
rays.positions[:] = self.position
|
|
153
|
+
|
|
154
|
+
# Generate uniform distribution on sphere
|
|
155
|
+
theta = np.random.uniform(0, 2 * np.pi, self.num_rays)
|
|
156
|
+
cos_phi = np.random.uniform(-1, 1, self.num_rays)
|
|
157
|
+
sin_phi = np.sqrt(1 - cos_phi**2)
|
|
158
|
+
|
|
159
|
+
rays.directions[:, 0] = sin_phi * np.cos(theta)
|
|
160
|
+
rays.directions[:, 1] = sin_phi * np.sin(theta)
|
|
161
|
+
rays.directions[:, 2] = cos_phi
|
|
162
|
+
|
|
163
|
+
self._assign_wavelengths(rays)
|
|
164
|
+
|
|
165
|
+
return rays
|
|
166
|
+
|
|
167
|
+
def __repr__(self) -> str:
|
|
168
|
+
"""Return string representation."""
|
|
169
|
+
return (
|
|
170
|
+
f"PointSource(position={self.position.tolist()}, "
|
|
171
|
+
f"num_rays={self.num_rays})"
|
|
172
|
+
)
|