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.
Files changed (180) hide show
  1. lsurf/__init__.py +471 -0
  2. lsurf/analysis/__init__.py +107 -0
  3. lsurf/analysis/healpix_utils.py +418 -0
  4. lsurf/analysis/sphere_viz.py +1280 -0
  5. lsurf/cli/__init__.py +48 -0
  6. lsurf/cli/build.py +398 -0
  7. lsurf/cli/config_schema.py +318 -0
  8. lsurf/cli/gui_cmd.py +76 -0
  9. lsurf/cli/interactive.py +850 -0
  10. lsurf/cli/main.py +81 -0
  11. lsurf/cli/run.py +806 -0
  12. lsurf/detectors/__init__.py +266 -0
  13. lsurf/detectors/analysis.py +289 -0
  14. lsurf/detectors/base.py +284 -0
  15. lsurf/detectors/constant_size_rings.py +485 -0
  16. lsurf/detectors/directional.py +45 -0
  17. lsurf/detectors/extended/__init__.py +73 -0
  18. lsurf/detectors/extended/local_sphere.py +353 -0
  19. lsurf/detectors/extended/recording_sphere.py +368 -0
  20. lsurf/detectors/planar.py +45 -0
  21. lsurf/detectors/protocol.py +187 -0
  22. lsurf/detectors/recording_spheres.py +63 -0
  23. lsurf/detectors/results.py +1140 -0
  24. lsurf/detectors/small/__init__.py +79 -0
  25. lsurf/detectors/small/directional.py +330 -0
  26. lsurf/detectors/small/planar.py +401 -0
  27. lsurf/detectors/small/spherical.py +450 -0
  28. lsurf/detectors/spherical.py +45 -0
  29. lsurf/geometry/__init__.py +199 -0
  30. lsurf/geometry/builder.py +478 -0
  31. lsurf/geometry/cell.py +228 -0
  32. lsurf/geometry/cell_geometry.py +247 -0
  33. lsurf/geometry/detector_arrays.py +1785 -0
  34. lsurf/geometry/geometry.py +222 -0
  35. lsurf/geometry/surface_analysis.py +375 -0
  36. lsurf/geometry/validation.py +91 -0
  37. lsurf/gui/__init__.py +51 -0
  38. lsurf/gui/app.py +903 -0
  39. lsurf/gui/core/__init__.py +39 -0
  40. lsurf/gui/core/scene.py +343 -0
  41. lsurf/gui/core/simulation.py +264 -0
  42. lsurf/gui/renderers/__init__.py +40 -0
  43. lsurf/gui/renderers/ray_renderer.py +353 -0
  44. lsurf/gui/renderers/source_renderer.py +505 -0
  45. lsurf/gui/renderers/surface_renderer.py +477 -0
  46. lsurf/gui/views/__init__.py +48 -0
  47. lsurf/gui/views/config_editor.py +3199 -0
  48. lsurf/gui/views/properties.py +257 -0
  49. lsurf/gui/views/results.py +291 -0
  50. lsurf/gui/views/scene_tree.py +180 -0
  51. lsurf/gui/views/viewport_3d.py +555 -0
  52. lsurf/gui/views/visualizations.py +712 -0
  53. lsurf/materials/__init__.py +169 -0
  54. lsurf/materials/base/__init__.py +64 -0
  55. lsurf/materials/base/full_inhomogeneous.py +208 -0
  56. lsurf/materials/base/grid_inhomogeneous.py +319 -0
  57. lsurf/materials/base/homogeneous.py +342 -0
  58. lsurf/materials/base/material_field.py +527 -0
  59. lsurf/materials/base/simple_inhomogeneous.py +418 -0
  60. lsurf/materials/base/spectral_inhomogeneous.py +497 -0
  61. lsurf/materials/implementations/__init__.py +120 -0
  62. lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
  63. lsurf/materials/implementations/duct_atmosphere.py +390 -0
  64. lsurf/materials/implementations/exponential_atmosphere.py +435 -0
  65. lsurf/materials/implementations/gaussian_lens.py +120 -0
  66. lsurf/materials/implementations/interpolated_data.py +123 -0
  67. lsurf/materials/implementations/layered_atmosphere.py +134 -0
  68. lsurf/materials/implementations/linear_gradient.py +109 -0
  69. lsurf/materials/implementations/linsley_atmosphere.py +764 -0
  70. lsurf/materials/implementations/standard_materials.py +126 -0
  71. lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
  72. lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
  73. lsurf/materials/utils/__init__.py +77 -0
  74. lsurf/materials/utils/constants.py +45 -0
  75. lsurf/materials/utils/device_functions.py +117 -0
  76. lsurf/materials/utils/dispersion.py +160 -0
  77. lsurf/materials/utils/factories.py +142 -0
  78. lsurf/propagation/__init__.py +91 -0
  79. lsurf/propagation/detector_gpu.py +67 -0
  80. lsurf/propagation/gpu_device_rays.py +294 -0
  81. lsurf/propagation/kernels/__init__.py +175 -0
  82. lsurf/propagation/kernels/absorption/__init__.py +61 -0
  83. lsurf/propagation/kernels/absorption/grid.py +240 -0
  84. lsurf/propagation/kernels/absorption/simple.py +232 -0
  85. lsurf/propagation/kernels/absorption/spectral.py +410 -0
  86. lsurf/propagation/kernels/detection/__init__.py +64 -0
  87. lsurf/propagation/kernels/detection/protocol.py +102 -0
  88. lsurf/propagation/kernels/detection/spherical.py +255 -0
  89. lsurf/propagation/kernels/device_functions.py +790 -0
  90. lsurf/propagation/kernels/fresnel/__init__.py +64 -0
  91. lsurf/propagation/kernels/fresnel/protocol.py +97 -0
  92. lsurf/propagation/kernels/fresnel/standard.py +258 -0
  93. lsurf/propagation/kernels/intersection/__init__.py +79 -0
  94. lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
  95. lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
  96. lsurf/propagation/kernels/intersection/plane.py +166 -0
  97. lsurf/propagation/kernels/intersection/protocol.py +95 -0
  98. lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
  99. lsurf/propagation/kernels/intersection/sphere.py +190 -0
  100. lsurf/propagation/kernels/propagation/__init__.py +85 -0
  101. lsurf/propagation/kernels/propagation/grid.py +527 -0
  102. lsurf/propagation/kernels/propagation/protocol.py +105 -0
  103. lsurf/propagation/kernels/propagation/simple.py +460 -0
  104. lsurf/propagation/kernels/propagation/spectral.py +875 -0
  105. lsurf/propagation/kernels/registry.py +331 -0
  106. lsurf/propagation/kernels/surface/__init__.py +72 -0
  107. lsurf/propagation/kernels/surface/bisection.py +232 -0
  108. lsurf/propagation/kernels/surface/detection.py +402 -0
  109. lsurf/propagation/kernels/surface/reduction.py +166 -0
  110. lsurf/propagation/propagator_protocol.py +222 -0
  111. lsurf/propagation/propagators/__init__.py +101 -0
  112. lsurf/propagation/propagators/detector_handler.py +354 -0
  113. lsurf/propagation/propagators/factory.py +200 -0
  114. lsurf/propagation/propagators/fresnel_handler.py +305 -0
  115. lsurf/propagation/propagators/gpu_gradient.py +566 -0
  116. lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
  117. lsurf/propagation/propagators/gradient.py +429 -0
  118. lsurf/propagation/propagators/intersection_handler.py +327 -0
  119. lsurf/propagation/propagators/material_propagator.py +398 -0
  120. lsurf/propagation/propagators/signed_distance_handler.py +522 -0
  121. lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
  122. lsurf/propagation/propagators/surface_interaction.py +616 -0
  123. lsurf/propagation/propagators/surface_propagator.py +719 -0
  124. lsurf/py.typed +1 -0
  125. lsurf/simulation/__init__.py +70 -0
  126. lsurf/simulation/config.py +164 -0
  127. lsurf/simulation/orchestrator.py +462 -0
  128. lsurf/simulation/result.py +299 -0
  129. lsurf/simulation/simulation.py +262 -0
  130. lsurf/sources/__init__.py +128 -0
  131. lsurf/sources/base.py +264 -0
  132. lsurf/sources/collimated.py +252 -0
  133. lsurf/sources/custom.py +409 -0
  134. lsurf/sources/diverging.py +228 -0
  135. lsurf/sources/gaussian.py +272 -0
  136. lsurf/sources/parallel_from_positions.py +197 -0
  137. lsurf/sources/point.py +172 -0
  138. lsurf/sources/uniform_diverging.py +258 -0
  139. lsurf/surfaces/__init__.py +184 -0
  140. lsurf/surfaces/cpu/__init__.py +50 -0
  141. lsurf/surfaces/cpu/curved_wave.py +463 -0
  142. lsurf/surfaces/cpu/gerstner_wave.py +381 -0
  143. lsurf/surfaces/cpu/wave_params.py +118 -0
  144. lsurf/surfaces/gpu/__init__.py +72 -0
  145. lsurf/surfaces/gpu/annular_plane.py +453 -0
  146. lsurf/surfaces/gpu/bounded_plane.py +390 -0
  147. lsurf/surfaces/gpu/curved_wave.py +483 -0
  148. lsurf/surfaces/gpu/gerstner_wave.py +377 -0
  149. lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
  150. lsurf/surfaces/gpu/plane.py +299 -0
  151. lsurf/surfaces/gpu/recording_sphere.py +587 -0
  152. lsurf/surfaces/gpu/sphere.py +311 -0
  153. lsurf/surfaces/protocol.py +336 -0
  154. lsurf/surfaces/registry.py +373 -0
  155. lsurf/utilities/__init__.py +175 -0
  156. lsurf/utilities/detector_analysis.py +814 -0
  157. lsurf/utilities/fresnel.py +628 -0
  158. lsurf/utilities/interactions.py +1215 -0
  159. lsurf/utilities/propagation.py +602 -0
  160. lsurf/utilities/ray_data.py +532 -0
  161. lsurf/utilities/recording_sphere.py +745 -0
  162. lsurf/utilities/time_spread.py +463 -0
  163. lsurf/visualization/__init__.py +329 -0
  164. lsurf/visualization/absorption_plots.py +334 -0
  165. lsurf/visualization/atmospheric_plots.py +754 -0
  166. lsurf/visualization/common.py +348 -0
  167. lsurf/visualization/detector_plots.py +1350 -0
  168. lsurf/visualization/detector_sphere_plots.py +1173 -0
  169. lsurf/visualization/fresnel_plots.py +1061 -0
  170. lsurf/visualization/ocean_simulation_plots.py +999 -0
  171. lsurf/visualization/polarization_plots.py +916 -0
  172. lsurf/visualization/raytracing_plots.py +1521 -0
  173. lsurf/visualization/ring_detector_plots.py +1867 -0
  174. lsurf/visualization/time_spread_plots.py +531 -0
  175. lsurf-1.0.0.dist-info/METADATA +381 -0
  176. lsurf-1.0.0.dist-info/RECORD +180 -0
  177. lsurf-1.0.0.dist-info/WHEEL +5 -0
  178. lsurf-1.0.0.dist-info/entry_points.txt +2 -0
  179. lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
  180. 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
+ )