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
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
+ )