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,485 @@
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
+ Constant-Size Detector Rings with No Shadowing.
36
+
37
+ This module provides a detector ring geometry where each ring has constant
38
+ physical size (radial width) regardless of distance from origin. Adjacent rings
39
+ touch exactly (no shadowing) to provide complete angular coverage.
40
+
41
+ The detector centers lie on a sphere at fixed altitude above Earth's surface,
42
+ with normals pointing toward the origin (0,0,0).
43
+ """
44
+
45
+ from dataclasses import dataclass
46
+
47
+ import numpy as np
48
+ from scipy.optimize import brentq
49
+
50
+ from ..surfaces import EARTH_RADIUS
51
+
52
+
53
+ @dataclass
54
+ class ConstantSizeDetectorRings:
55
+ """
56
+ Constant-size detector ring geometry with no shadowing.
57
+
58
+ Creates a set of annular detector rings centered on a sphere at fixed
59
+ altitude above Earth's surface. Each ring has constant physical radial
60
+ width, with angular size varying based on distance from origin.
61
+
62
+ Adjacent rings touch exactly (no overlap, no gaps) when viewed from origin,
63
+ ensuring complete angular coverage with no shadowing.
64
+
65
+ Parameters
66
+ ----------
67
+ detector_radial_size : float
68
+ Physical radial width of each detector in meters (default: 10 km)
69
+ detector_altitude : float
70
+ Altitude of detector sphere above Earth's surface in meters (default: 33 km)
71
+ max_elevation_deg : float
72
+ Maximum elevation angle from horizontal (90° = zenith) (default: 90°)
73
+ min_elevation_deg : float
74
+ Minimum elevation angle, where to stop generating rings (default: -2°)
75
+ earth_radius : float
76
+ Earth radius in meters (default: EARTH_RADIUS constant)
77
+
78
+ Attributes
79
+ ----------
80
+ ring_boundaries_deg : ndarray
81
+ Elevation angles of ring boundaries (N+1 values for N rings)
82
+ ring_centers_deg : ndarray
83
+ Elevation angles of ring centers (N values)
84
+ ring_distances : ndarray
85
+ Distances from origin to ring centers in meters (N values)
86
+ n_rings : int
87
+ Number of detector rings
88
+ detector_sphere_radius : float
89
+ Radius of detector sphere from Earth center in meters
90
+
91
+ Examples
92
+ --------
93
+ >>> rings = ConstantSizeDetectorRings(
94
+ ... detector_radial_size=10000.0, # 10 km
95
+ ... detector_altitude=33000.0, # 33 km
96
+ ... )
97
+ >>> print(f"Created {rings.n_rings} rings")
98
+ Created 17 rings
99
+ >>> print(f"Coverage: {rings.ring_boundaries_deg[-1]:.1f}° to {rings.ring_boundaries_deg[0]:.1f}°")
100
+ Coverage: -2.3° to 90.0°
101
+ """
102
+
103
+ detector_radial_size: float = 10000.0 # 10 km
104
+ detector_altitude: float = 33000.0 # 33 km
105
+ max_elevation_deg: float = 90.0 # Zenith
106
+ min_elevation_deg: float = -2.0 # Stop 2 deg below horizontal
107
+ earth_radius: float = EARTH_RADIUS
108
+
109
+ # Computed attributes (set in __post_init__)
110
+ ring_boundaries_deg: np.ndarray = None
111
+ ring_centers_deg: np.ndarray = None
112
+ ring_distances: np.ndarray = None
113
+ n_rings: int = 0
114
+ detector_sphere_radius: float = 0.0
115
+
116
+ def __post_init__(self):
117
+ """Compute ring geometry after initialization."""
118
+ self.detector_sphere_radius = self.earth_radius + self.detector_altitude
119
+ self._compute_ring_geometry()
120
+
121
+ @property
122
+ def detector_half_width(self) -> float:
123
+ """Physical half-width of each detector in meters."""
124
+ return self.detector_radial_size / 2
125
+
126
+ def distance_at_elevation(self, elev_deg: float) -> float:
127
+ """
128
+ Compute distance from origin (0,0,0) to detector sphere at given elevation.
129
+
130
+ Uses the formula:
131
+ d(θ) = -sin(θ)·R_E + √(R_d² - R_E²·cos²(θ))
132
+
133
+ where R_E = Earth radius, R_d = detector sphere radius, θ = elevation angle.
134
+
135
+ Parameters
136
+ ----------
137
+ elev_deg : float
138
+ Elevation angle from horizontal in degrees (90° = zenith)
139
+
140
+ Returns
141
+ -------
142
+ float
143
+ Distance from origin to intersection point (meters)
144
+
145
+ Raises
146
+ ------
147
+ ValueError
148
+ If no intersection exists at the given elevation
149
+ """
150
+ elev_rad = np.radians(elev_deg)
151
+ cos_e, sin_e = np.cos(elev_rad), np.sin(elev_rad)
152
+ discriminant = self.detector_sphere_radius**2 - self.earth_radius**2 * cos_e**2
153
+ if discriminant < 0:
154
+ raise ValueError(f"No intersection at elevation {elev_deg}°")
155
+ return -sin_e * self.earth_radius + np.sqrt(discriminant)
156
+
157
+ def point_at_elevation(
158
+ self, elev_deg: float, azimuth_deg: float = 0.0
159
+ ) -> np.ndarray:
160
+ """
161
+ Compute 3D point on detector sphere at given elevation and azimuth.
162
+
163
+ Parameters
164
+ ----------
165
+ elev_deg : float
166
+ Elevation angle from horizontal in degrees
167
+ azimuth_deg : float
168
+ Azimuth angle in degrees (0 = +x direction)
169
+
170
+ Returns
171
+ -------
172
+ ndarray
173
+ (x, y, z) position in meters
174
+ """
175
+ dist = self.distance_at_elevation(elev_deg)
176
+ elev_rad = np.radians(elev_deg)
177
+ az_rad = np.radians(azimuth_deg)
178
+ x = dist * np.cos(elev_rad) * np.cos(az_rad)
179
+ y = dist * np.cos(elev_rad) * np.sin(az_rad)
180
+ z = dist * np.sin(elev_rad)
181
+ return np.array([x, y, z])
182
+
183
+ def find_detector_center(self, theta_top_deg: float) -> float:
184
+ """
185
+ Find detector center elevation such that top edge is at theta_top.
186
+
187
+ For a detector with physical half-width w and center at elevation θ_c,
188
+ the angular half-width as seen from origin is:
189
+ α = arctan(w / d(θ_c))
190
+
191
+ The top edge elevation is θ_top = θ_c + α.
192
+
193
+ This function solves for θ_c given θ_top using brentq root finding.
194
+
195
+ Parameters
196
+ ----------
197
+ theta_top_deg : float
198
+ Desired elevation angle of top edge (degrees)
199
+
200
+ Returns
201
+ -------
202
+ float
203
+ Center elevation angle (degrees)
204
+ """
205
+ hw = self.detector_half_width
206
+
207
+ def residual(theta_c_deg):
208
+ dist = self.distance_at_elevation(theta_c_deg)
209
+ alpha_deg = np.degrees(np.arctan(hw / dist))
210
+ return theta_top_deg - (theta_c_deg + alpha_deg)
211
+
212
+ # Search bounds: center must be below top edge
213
+ lower_bound = max(theta_top_deg - 45.0, -10.0)
214
+ upper_bound = theta_top_deg - 0.001
215
+
216
+ return brentq(residual, lower_bound, upper_bound)
217
+
218
+ def angular_width_at_ring(self, ring_index: int) -> float:
219
+ """
220
+ Compute angular width of a ring as seen from origin.
221
+
222
+ Parameters
223
+ ----------
224
+ ring_index : int
225
+ Index of the ring (0 = nearest to zenith)
226
+
227
+ Returns
228
+ -------
229
+ float
230
+ Angular width in degrees
231
+ """
232
+ if ring_index < 0 or ring_index >= self.n_rings:
233
+ raise ValueError(
234
+ f"Ring index {ring_index} out of range [0, {self.n_rings})"
235
+ )
236
+ dist = self.ring_distances[ring_index]
237
+ return 2 * np.degrees(np.arctan(self.detector_half_width / dist))
238
+
239
+ def horizontal_distance_at_elevation(self, elev_deg: float) -> float:
240
+ """
241
+ Compute horizontal distance from origin to detector at given elevation.
242
+
243
+ Parameters
244
+ ----------
245
+ elev_deg : float
246
+ Elevation angle in degrees
247
+
248
+ Returns
249
+ -------
250
+ float
251
+ Horizontal distance in meters
252
+ """
253
+ dist = self.distance_at_elevation(elev_deg)
254
+ return dist * np.cos(np.radians(elev_deg))
255
+
256
+ def get_ring_horizontal_distances(self) -> np.ndarray:
257
+ """
258
+ Get horizontal distances for all ring boundaries.
259
+
260
+ Returns
261
+ -------
262
+ ndarray
263
+ Horizontal distances in meters for each ring boundary
264
+ """
265
+ return np.array(
266
+ [self.horizontal_distance_at_elevation(e) for e in self.ring_boundaries_deg]
267
+ )
268
+
269
+ def _compute_ring_geometry(self):
270
+ """
271
+ Build ring boundaries with constant physical size (no shadowing).
272
+
273
+ Algorithm:
274
+ 1. Start at θ_boundary[0] = max_elevation_deg (zenith)
275
+ 2. Find center θ_c such that top edge is at θ_boundary[i]
276
+ 3. Compute bottom edge: θ_bottom = θ_c - α = 2·θ_c - θ_top
277
+ 4. Set θ_boundary[i+1] = θ_bottom
278
+ 5. Repeat until θ_boundary < min_elevation_deg
279
+ """
280
+ boundaries = [self.max_elevation_deg]
281
+ centers = []
282
+ distances = []
283
+
284
+ current_top = self.max_elevation_deg
285
+ while current_top > self.min_elevation_deg:
286
+ # Find center such that top edge is at current_top
287
+ theta_c = self.find_detector_center(current_top)
288
+ dist_c = self.distance_at_elevation(theta_c)
289
+
290
+ # Angular half-width at this distance
291
+ alpha_deg = np.degrees(np.arctan(self.detector_half_width / dist_c))
292
+
293
+ # Bottom edge (no-shadowing: adjacent ring's top = this ring's bottom)
294
+ theta_bottom = theta_c - alpha_deg
295
+
296
+ centers.append(theta_c)
297
+ distances.append(dist_c)
298
+ boundaries.append(theta_bottom)
299
+
300
+ current_top = theta_bottom
301
+
302
+ self.ring_boundaries_deg = np.array(boundaries)
303
+ self.ring_centers_deg = np.array(centers)
304
+ self.ring_distances = np.array(distances)
305
+ self.n_rings = len(centers)
306
+
307
+ def summary(self) -> str:
308
+ """
309
+ Return a summary string of the detector ring configuration.
310
+
311
+ Returns
312
+ -------
313
+ str
314
+ Multi-line summary of configuration
315
+ """
316
+ lines = [
317
+ "Constant-Size Detector Ring Configuration:",
318
+ f" Detector altitude: {self.detector_altitude/1000:.1f} km",
319
+ f" Sphere radius: {self.detector_sphere_radius/1000:.1f} km",
320
+ f" Detector radial size: {self.detector_radial_size/1000:.1f} km",
321
+ f" Elevation range: {self.ring_boundaries_deg[-1]:.2f}° to {self.ring_boundaries_deg[0]:.2f}°",
322
+ f" Number of rings: {self.n_rings}",
323
+ ]
324
+
325
+ if self.n_rings > 0:
326
+ aw0 = self.angular_width_at_ring(0)
327
+ aw_last = self.angular_width_at_ring(self.n_rings - 1)
328
+ lines.extend(
329
+ [
330
+ f" Ring 0 (zenith): {self.ring_distances[0]/1000:.1f} km, {aw0:.2f}° angular width",
331
+ f" Ring {self.n_rings-1} (edge): {self.ring_distances[-1]/1000:.1f} km, {aw_last:.2f}° angular width",
332
+ ]
333
+ )
334
+
335
+ return "\n".join(lines)
336
+
337
+ def get_ring_info(self, ring_index: int) -> dict:
338
+ """
339
+ Get detailed information about a specific ring.
340
+
341
+ Parameters
342
+ ----------
343
+ ring_index : int
344
+ Index of the ring
345
+
346
+ Returns
347
+ -------
348
+ dict
349
+ Dictionary with ring properties
350
+ """
351
+ if ring_index < 0 or ring_index >= self.n_rings:
352
+ raise ValueError(
353
+ f"Ring index {ring_index} out of range [0, {self.n_rings})"
354
+ )
355
+
356
+ return {
357
+ "ring_index": ring_index,
358
+ "center_elevation_deg": self.ring_centers_deg[ring_index],
359
+ "top_elevation_deg": self.ring_boundaries_deg[ring_index],
360
+ "bottom_elevation_deg": self.ring_boundaries_deg[ring_index + 1],
361
+ "distance_m": self.ring_distances[ring_index],
362
+ "distance_km": self.ring_distances[ring_index] / 1000,
363
+ "angular_width_deg": self.angular_width_at_ring(ring_index),
364
+ "horizontal_distance_m": self.horizontal_distance_at_elevation(
365
+ self.ring_centers_deg[ring_index]
366
+ ),
367
+ }
368
+
369
+ def azimuth_bins_for_ring(
370
+ self, ring_index: int, az_bin_size_m: float, az_range_deg: float = 10.0
371
+ ) -> tuple[int, np.ndarray, np.ndarray]:
372
+ """
373
+ Compute azimuthal bins of constant physical size for a ring.
374
+
375
+ At each ring distance, computes how many bins of the given physical
376
+ width fit within the ±az_range azimuth range.
377
+
378
+ Parameters
379
+ ----------
380
+ ring_index : int
381
+ Index of the ring
382
+ az_bin_size_m : float
383
+ Physical azimuthal bin size in meters
384
+ az_range_deg : float
385
+ Azimuth range in degrees (±this value from beam direction)
386
+
387
+ Returns
388
+ -------
389
+ n_bins : int
390
+ Number of azimuthal bins for this ring
391
+ az_edges_deg : ndarray
392
+ Azimuth bin edges in degrees (n_bins + 1 values)
393
+ az_centers_deg : ndarray
394
+ Azimuth bin centers in degrees (n_bins values)
395
+ """
396
+ if ring_index < 0 or ring_index >= self.n_rings:
397
+ raise ValueError(
398
+ f"Ring index {ring_index} out of range [0, {self.n_rings})"
399
+ )
400
+
401
+ dist = self.ring_distances[ring_index]
402
+
403
+ # Arc length for full azimuth range at this distance
404
+ # Arc = distance * angle_radians (for small angles on a sphere from origin)
405
+ total_arc_m = dist * np.radians(2 * az_range_deg)
406
+
407
+ # Number of bins that fit
408
+ n_bins = max(1, int(np.round(total_arc_m / az_bin_size_m)))
409
+
410
+ # Compute bin edges in degrees
411
+ az_edges_deg = np.linspace(-az_range_deg, az_range_deg, n_bins + 1)
412
+ az_centers_deg = (az_edges_deg[:-1] + az_edges_deg[1:]) / 2
413
+
414
+ return n_bins, az_edges_deg, az_centers_deg
415
+
416
+ def get_constant_size_grid(
417
+ self, az_bin_size_m: float, az_range_deg: float = 10.0
418
+ ) -> list[dict]:
419
+ """
420
+ Get a grid of constant-size bins across all rings.
421
+
422
+ Each bin has approximately constant physical size:
423
+ - Radial size: detector_radial_size (same for all rings)
424
+ - Azimuthal size: az_bin_size_m (variable number of bins per ring)
425
+
426
+ Parameters
427
+ ----------
428
+ az_bin_size_m : float
429
+ Physical azimuthal bin size in meters
430
+ az_range_deg : float
431
+ Azimuth range in degrees (±this value)
432
+
433
+ Returns
434
+ -------
435
+ list of dict
436
+ List of bin specifications with keys:
437
+ - ring_idx: int
438
+ - az_bin_idx: int
439
+ - n_az_bins: int (total azimuth bins for this ring)
440
+ - az_lo_deg, az_hi_deg: float
441
+ - az_center_deg: float
442
+ - distance_m: float
443
+ - bin_area_m2: float (approximate)
444
+ """
445
+ grid = []
446
+
447
+ for ring_idx in range(self.n_rings):
448
+ n_az_bins, az_edges, az_centers = self.azimuth_bins_for_ring(
449
+ ring_idx, az_bin_size_m, az_range_deg
450
+ )
451
+ dist = self.ring_distances[ring_idx]
452
+
453
+ # Approximate bin area (radial_size * azimuthal_arc)
454
+ az_width_rad = np.radians(az_edges[1] - az_edges[0])
455
+ az_arc_m = dist * az_width_rad
456
+ bin_area = self.detector_radial_size * az_arc_m
457
+
458
+ for az_bin_idx in range(n_az_bins):
459
+ grid.append(
460
+ {
461
+ "ring_idx": ring_idx,
462
+ "az_bin_idx": az_bin_idx,
463
+ "n_az_bins": n_az_bins,
464
+ "az_lo_deg": az_edges[az_bin_idx],
465
+ "az_hi_deg": az_edges[az_bin_idx + 1],
466
+ "az_center_deg": az_centers[az_bin_idx],
467
+ "distance_m": dist,
468
+ "elev_center_deg": self.ring_centers_deg[ring_idx],
469
+ "bin_area_m2": bin_area,
470
+ }
471
+ )
472
+
473
+ return grid
474
+
475
+
476
+ def create_default_detector_rings() -> ConstantSizeDetectorRings:
477
+ """
478
+ Create detector rings with default parameters (10 km size, 33 km altitude).
479
+
480
+ Returns
481
+ -------
482
+ ConstantSizeDetectorRings
483
+ Detector ring configuration with default parameters
484
+ """
485
+ return ConstantSizeDetectorRings()
@@ -0,0 +1,45 @@
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
+ Directional Detector Implementation (Backward Compatibility)
36
+
37
+ This module re-exports DirectionalDetector from the new location
38
+ for backward compatibility. New code should import from:
39
+ lsurf.detectors or lsurf.detectors.small
40
+ """
41
+
42
+ # Re-export from new location for backward compatibility
43
+ from .small.directional import DirectionalDetector
44
+
45
+ __all__ = ["DirectionalDetector"]
@@ -0,0 +1,73 @@
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
+ Extended (Surface) Detectors Submodule
36
+
37
+ This submodule contains extended detectors that are large detection surfaces
38
+ (e.g., recording spheres) for capturing rays at altitude or across large areas.
39
+
40
+ Available Detectors
41
+ -------------------
42
+ RecordingSphereDetector
43
+ Spherical detection surface at altitude above Earth for global simulations.
44
+ LocalRecordingSphereDetector
45
+ Spherical detection surface centered at a point for local simulations.
46
+
47
+ Examples
48
+ --------
49
+ >>> from lsurf.detectors.extended import RecordingSphereDetector, LocalRecordingSphereDetector
50
+ >>>
51
+ >>> # Earth-scale: satellite at 33 km altitude
52
+ >>> earth_detector = RecordingSphereDetector(altitude=33000.0)
53
+ >>> result = earth_detector.detect(rays)
54
+ >>>
55
+ >>> # Local-scale: 33 km radius sphere at origin
56
+ >>> local_detector = LocalRecordingSphereDetector(radius=33000.0)
57
+ >>> result = local_detector.detect(rays)
58
+ """
59
+
60
+ from .recording_sphere import RecordingSphereDetector
61
+ from .local_sphere import LocalRecordingSphereDetector
62
+
63
+ # Backwards compatibility aliases
64
+ RecordingSphere = RecordingSphereDetector
65
+ LocalRecordingSphere = LocalRecordingSphereDetector
66
+
67
+ __all__ = [
68
+ "RecordingSphereDetector",
69
+ "LocalRecordingSphereDetector",
70
+ # Backwards compatibility
71
+ "RecordingSphere",
72
+ "LocalRecordingSphere",
73
+ ]