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,222 @@
|
|
|
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
|
+
Immutable Geometry Container
|
|
36
|
+
|
|
37
|
+
Holds the result of a GeometryBuilder.build() call.
|
|
38
|
+
Provides convenient accessors for surfaces, detectors, and materials.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
|
|
43
|
+
from ..surfaces import Surface
|
|
44
|
+
from ..materials import MaterialField
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class Geometry:
|
|
49
|
+
"""
|
|
50
|
+
Immutable container for simulation geometry.
|
|
51
|
+
|
|
52
|
+
Created by GeometryBuilder.build(). Provides access to surfaces,
|
|
53
|
+
detectors, and named media by name or index.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
surfaces : tuple of Surface
|
|
58
|
+
All optical/absorber surfaces in the geometry.
|
|
59
|
+
detectors : tuple of Surface
|
|
60
|
+
All detector surfaces in the geometry.
|
|
61
|
+
background_material : MaterialField
|
|
62
|
+
The background/ambient material (from set_background() medium).
|
|
63
|
+
media : dict
|
|
64
|
+
Mapping from medium name to MaterialField.
|
|
65
|
+
surface_names : dict
|
|
66
|
+
Mapping from surface name to index in surfaces tuple.
|
|
67
|
+
detector_names : dict
|
|
68
|
+
Mapping from detector name to index in detectors tuple.
|
|
69
|
+
|
|
70
|
+
Examples
|
|
71
|
+
--------
|
|
72
|
+
>>> geometry = builder.build()
|
|
73
|
+
>>> ocean = geometry.get_surface("ocean")
|
|
74
|
+
>>> detector = geometry.get_detector("detector_35km")
|
|
75
|
+
>>> atmosphere_material = geometry.get_medium("atmosphere")
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
surfaces: tuple[Surface, ...]
|
|
79
|
+
detectors: tuple[Surface, ...]
|
|
80
|
+
background_material: MaterialField
|
|
81
|
+
media: dict[str, MaterialField]
|
|
82
|
+
surface_names: dict[str, int]
|
|
83
|
+
detector_names: dict[str, int]
|
|
84
|
+
|
|
85
|
+
def get_medium(self, name: str) -> MaterialField:
|
|
86
|
+
"""
|
|
87
|
+
Get the material for a named medium.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
name : str
|
|
92
|
+
The name of the medium.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
MaterialField
|
|
97
|
+
The material for the medium.
|
|
98
|
+
|
|
99
|
+
Raises
|
|
100
|
+
------
|
|
101
|
+
KeyError
|
|
102
|
+
If no medium with the given name exists.
|
|
103
|
+
"""
|
|
104
|
+
if name not in self.media:
|
|
105
|
+
available = ", ".join(sorted(self.media.keys()))
|
|
106
|
+
raise KeyError(f"No medium named '{name}'. Available: {available}")
|
|
107
|
+
return self.media[name]
|
|
108
|
+
|
|
109
|
+
def get_surface(self, name: str) -> Surface:
|
|
110
|
+
"""
|
|
111
|
+
Get a surface by name.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
name : str
|
|
116
|
+
The name of the surface.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
Surface
|
|
121
|
+
The surface with the given name.
|
|
122
|
+
|
|
123
|
+
Raises
|
|
124
|
+
------
|
|
125
|
+
KeyError
|
|
126
|
+
If no surface with the given name exists.
|
|
127
|
+
"""
|
|
128
|
+
if name not in self.surface_names:
|
|
129
|
+
available = ", ".join(sorted(self.surface_names.keys()))
|
|
130
|
+
raise KeyError(f"No surface named '{name}'. Available: {available}")
|
|
131
|
+
return self.surfaces[self.surface_names[name]]
|
|
132
|
+
|
|
133
|
+
def get_surface_index(self, name: str) -> int:
|
|
134
|
+
"""
|
|
135
|
+
Get the index of a surface by name.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
name : str
|
|
140
|
+
The name of the surface.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
int
|
|
145
|
+
The index of the surface in the surfaces tuple.
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
KeyError
|
|
150
|
+
If no surface with the given name exists.
|
|
151
|
+
"""
|
|
152
|
+
if name not in self.surface_names:
|
|
153
|
+
available = ", ".join(sorted(self.surface_names.keys()))
|
|
154
|
+
raise KeyError(f"No surface named '{name}'. Available: {available}")
|
|
155
|
+
return self.surface_names[name]
|
|
156
|
+
|
|
157
|
+
def get_detector(self, name: str) -> Surface:
|
|
158
|
+
"""
|
|
159
|
+
Get a detector by name.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
name : str
|
|
164
|
+
The name of the detector.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
Surface
|
|
169
|
+
The detector with the given name.
|
|
170
|
+
|
|
171
|
+
Raises
|
|
172
|
+
------
|
|
173
|
+
KeyError
|
|
174
|
+
If no detector with the given name exists.
|
|
175
|
+
"""
|
|
176
|
+
if name not in self.detector_names:
|
|
177
|
+
available = ", ".join(sorted(self.detector_names.keys()))
|
|
178
|
+
raise KeyError(f"No detector named '{name}'. Available: {available}")
|
|
179
|
+
return self.detectors[self.detector_names[name]]
|
|
180
|
+
|
|
181
|
+
def get_detector_index(self, name: str) -> int:
|
|
182
|
+
"""
|
|
183
|
+
Get the index of a detector by name.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
name : str
|
|
188
|
+
The name of the detector.
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
int
|
|
193
|
+
The index of the detector in the detectors tuple.
|
|
194
|
+
|
|
195
|
+
Raises
|
|
196
|
+
------
|
|
197
|
+
KeyError
|
|
198
|
+
If no detector with the given name exists.
|
|
199
|
+
"""
|
|
200
|
+
if name not in self.detector_names:
|
|
201
|
+
available = ", ".join(sorted(self.detector_names.keys()))
|
|
202
|
+
raise KeyError(f"No detector named '{name}'. Available: {available}")
|
|
203
|
+
return self.detector_names[name]
|
|
204
|
+
|
|
205
|
+
def to_surface_list(self) -> list[Surface]:
|
|
206
|
+
"""
|
|
207
|
+
Convert all surfaces and detectors to a list for SurfacePropagator.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
list of Surface
|
|
212
|
+
All surfaces and detectors as a mutable list.
|
|
213
|
+
"""
|
|
214
|
+
return list(self.surfaces) + list(self.detectors)
|
|
215
|
+
|
|
216
|
+
def __len__(self) -> int:
|
|
217
|
+
"""Return the total number of surfaces and detectors."""
|
|
218
|
+
return len(self.surfaces) + len(self.detectors)
|
|
219
|
+
|
|
220
|
+
def __iter__(self):
|
|
221
|
+
"""Iterate over all surfaces and detectors."""
|
|
222
|
+
return iter(self.surfaces + self.detectors)
|
|
@@ -0,0 +1,375 @@
|
|
|
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
|
+
Surface Relationship Analysis
|
|
36
|
+
|
|
37
|
+
Tools for analyzing geometric relationships between surfaces to detect
|
|
38
|
+
potential material assignment conflicts.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from enum import Enum, auto
|
|
45
|
+
from typing import TYPE_CHECKING
|
|
46
|
+
|
|
47
|
+
import numpy as np
|
|
48
|
+
|
|
49
|
+
if TYPE_CHECKING:
|
|
50
|
+
from numpy.typing import NDArray
|
|
51
|
+
|
|
52
|
+
from ..surfaces import Surface
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SurfaceRelationship(Enum):
|
|
56
|
+
"""Classification of the geometric relationship between two surfaces."""
|
|
57
|
+
|
|
58
|
+
DISJOINT = auto() # Surfaces don't overlap in the domain of interest
|
|
59
|
+
PARALLEL = auto() # Surfaces are parallel (planes) or concentric (spheres)
|
|
60
|
+
NESTED = auto() # One surface completely contains the other
|
|
61
|
+
INTERSECTING = auto() # Surfaces intersect, creating multiple regions
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class SurfaceAnalysisResult:
|
|
66
|
+
"""
|
|
67
|
+
Result of analyzing the relationship between two surfaces.
|
|
68
|
+
|
|
69
|
+
Attributes
|
|
70
|
+
----------
|
|
71
|
+
relationship : SurfaceRelationship
|
|
72
|
+
The geometric relationship between the surfaces.
|
|
73
|
+
details : str
|
|
74
|
+
Human-readable explanation of the analysis.
|
|
75
|
+
materials_consistent : bool
|
|
76
|
+
Whether material assignments are consistent for this relationship.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
relationship: SurfaceRelationship
|
|
80
|
+
details: str
|
|
81
|
+
materials_consistent: bool
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Tolerance for parallel detection (cosine of angle between normals)
|
|
85
|
+
PARALLEL_TOLERANCE = 1e-6
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def are_planes_parallel(
|
|
89
|
+
normal1: NDArray[np.float64],
|
|
90
|
+
normal2: NDArray[np.float64],
|
|
91
|
+
tolerance: float = PARALLEL_TOLERANCE,
|
|
92
|
+
) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
Check if two planes are parallel based on their normals.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
normal1 : ndarray, shape (3,)
|
|
99
|
+
Normal vector of the first plane.
|
|
100
|
+
normal2 : ndarray, shape (3,)
|
|
101
|
+
Normal vector of the second plane.
|
|
102
|
+
tolerance : float, optional
|
|
103
|
+
Tolerance for parallel detection. Default is 1e-6.
|
|
104
|
+
Planes are considered parallel if |dot(n1, n2)| > 1 - tolerance.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
bool
|
|
109
|
+
True if the planes are parallel (or anti-parallel).
|
|
110
|
+
"""
|
|
111
|
+
# Normalize vectors
|
|
112
|
+
n1 = np.asarray(normal1, dtype=np.float64)
|
|
113
|
+
n2 = np.asarray(normal2, dtype=np.float64)
|
|
114
|
+
|
|
115
|
+
n1 = n1 / np.linalg.norm(n1)
|
|
116
|
+
n2 = n2 / np.linalg.norm(n2)
|
|
117
|
+
|
|
118
|
+
# Check if parallel (dot product magnitude close to 1)
|
|
119
|
+
dot = np.abs(np.dot(n1, n2))
|
|
120
|
+
return dot > (1.0 - tolerance)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def are_spheres_concentric(
|
|
124
|
+
center1: NDArray[np.float64],
|
|
125
|
+
center2: NDArray[np.float64],
|
|
126
|
+
tolerance: float = 1e-6,
|
|
127
|
+
) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Check if two spheres are concentric (same center).
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
center1 : ndarray, shape (3,)
|
|
134
|
+
Center of the first sphere.
|
|
135
|
+
center2 : ndarray, shape (3,)
|
|
136
|
+
Center of the second sphere.
|
|
137
|
+
tolerance : float, optional
|
|
138
|
+
Distance tolerance for considering centers equal.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
bool
|
|
143
|
+
True if the spheres share the same center.
|
|
144
|
+
"""
|
|
145
|
+
c1 = np.asarray(center1, dtype=np.float64)
|
|
146
|
+
c2 = np.asarray(center2, dtype=np.float64)
|
|
147
|
+
return np.linalg.norm(c1 - c2) < tolerance
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _get_plane_normal(surface: Surface) -> NDArray[np.float64] | None:
|
|
151
|
+
"""Extract normal from a plane surface, or None if not a plane."""
|
|
152
|
+
# Check for PlaneSurface or BoundedPlaneSurface by looking for normal attribute
|
|
153
|
+
if hasattr(surface, "normal"):
|
|
154
|
+
return np.asarray(surface.normal, dtype=np.float64)
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_sphere_center(surface: Surface) -> NDArray[np.float64] | None:
|
|
159
|
+
"""Extract center from a sphere surface, or None if not a sphere."""
|
|
160
|
+
if hasattr(surface, "center"):
|
|
161
|
+
return np.asarray(surface.center, dtype=np.float64)
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def analyze_surface_pair(
|
|
166
|
+
surface1: Surface,
|
|
167
|
+
surface2: Surface,
|
|
168
|
+
) -> SurfaceAnalysisResult:
|
|
169
|
+
"""
|
|
170
|
+
Analyze the geometric relationship between two surfaces.
|
|
171
|
+
|
|
172
|
+
Determines if surfaces are parallel, concentric, nested, or intersecting,
|
|
173
|
+
and checks whether material assignments are consistent.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
surface1 : Surface
|
|
178
|
+
First surface to analyze.
|
|
179
|
+
surface2 : Surface
|
|
180
|
+
Second surface to analyze.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
SurfaceAnalysisResult
|
|
185
|
+
Analysis result including relationship type and material consistency.
|
|
186
|
+
|
|
187
|
+
Notes
|
|
188
|
+
-----
|
|
189
|
+
For intersecting surfaces with different materials, the front/back model
|
|
190
|
+
cannot consistently assign materials to all regions. This function detects
|
|
191
|
+
such conflicts.
|
|
192
|
+
"""
|
|
193
|
+
from ..surfaces import SurfaceRole
|
|
194
|
+
|
|
195
|
+
# Skip non-optical surfaces - they don't define materials
|
|
196
|
+
if surface1.role != SurfaceRole.OPTICAL or surface2.role != SurfaceRole.OPTICAL:
|
|
197
|
+
return SurfaceAnalysisResult(
|
|
198
|
+
relationship=SurfaceRelationship.DISJOINT,
|
|
199
|
+
details="Non-optical surfaces do not define material regions.",
|
|
200
|
+
materials_consistent=True,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Check for plane-plane relationship
|
|
204
|
+
normal1 = _get_plane_normal(surface1)
|
|
205
|
+
normal2 = _get_plane_normal(surface2)
|
|
206
|
+
|
|
207
|
+
if normal1 is not None and normal2 is not None:
|
|
208
|
+
return _analyze_plane_pair(surface1, surface2, normal1, normal2)
|
|
209
|
+
|
|
210
|
+
# Check for sphere-sphere relationship
|
|
211
|
+
center1 = _get_sphere_center(surface1)
|
|
212
|
+
center2 = _get_sphere_center(surface2)
|
|
213
|
+
|
|
214
|
+
if center1 is not None and center2 is not None:
|
|
215
|
+
return _analyze_sphere_pair(surface1, surface2, center1, center2)
|
|
216
|
+
|
|
217
|
+
# Check for plane-sphere relationship
|
|
218
|
+
if normal1 is not None and center2 is not None:
|
|
219
|
+
return _analyze_plane_sphere(surface1, surface2)
|
|
220
|
+
|
|
221
|
+
if center1 is not None and normal2 is not None:
|
|
222
|
+
return _analyze_plane_sphere(surface2, surface1)
|
|
223
|
+
|
|
224
|
+
# Unknown surface types - assume potentially intersecting
|
|
225
|
+
# Check if materials are the same (consistent even if intersecting)
|
|
226
|
+
consistent = _check_material_consistency(surface1, surface2)
|
|
227
|
+
return SurfaceAnalysisResult(
|
|
228
|
+
relationship=SurfaceRelationship.INTERSECTING,
|
|
229
|
+
details=(
|
|
230
|
+
f"Cannot determine geometric relationship for surface types "
|
|
231
|
+
f"{type(surface1).__name__} and {type(surface2).__name__}. "
|
|
232
|
+
f"Assuming potentially intersecting."
|
|
233
|
+
),
|
|
234
|
+
materials_consistent=consistent,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _analyze_plane_pair(
|
|
239
|
+
surface1: Surface,
|
|
240
|
+
surface2: Surface,
|
|
241
|
+
normal1: NDArray[np.float64],
|
|
242
|
+
normal2: NDArray[np.float64],
|
|
243
|
+
) -> SurfaceAnalysisResult:
|
|
244
|
+
"""Analyze relationship between two planes."""
|
|
245
|
+
if are_planes_parallel(normal1, normal2):
|
|
246
|
+
return SurfaceAnalysisResult(
|
|
247
|
+
relationship=SurfaceRelationship.PARALLEL,
|
|
248
|
+
details="Planes are parallel - no intersection.",
|
|
249
|
+
materials_consistent=True,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Non-parallel planes always intersect
|
|
253
|
+
consistent = _check_material_consistency(surface1, surface2)
|
|
254
|
+
angle_rad = np.arccos(np.clip(np.abs(np.dot(normal1, normal2)), 0, 1))
|
|
255
|
+
angle_deg = np.degrees(angle_rad)
|
|
256
|
+
|
|
257
|
+
details = (
|
|
258
|
+
f"Planes intersect at {angle_deg:.1f}° creating 4 quadrants. "
|
|
259
|
+
f"Surface '{surface1.name}' has front={_material_name(surface1.material_front)}, "
|
|
260
|
+
f"back={_material_name(surface1.material_back)}. "
|
|
261
|
+
f"Surface '{surface2.name}' has front={_material_name(surface2.material_front)}, "
|
|
262
|
+
f"back={_material_name(surface2.material_back)}."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if not consistent:
|
|
266
|
+
details += (
|
|
267
|
+
" Materials conflict: each quadrant must satisfy constraints from "
|
|
268
|
+
"both surfaces, but the assigned materials are incompatible."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return SurfaceAnalysisResult(
|
|
272
|
+
relationship=SurfaceRelationship.INTERSECTING,
|
|
273
|
+
details=details,
|
|
274
|
+
materials_consistent=consistent,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _analyze_sphere_pair(
|
|
279
|
+
surface1: Surface,
|
|
280
|
+
surface2: Surface,
|
|
281
|
+
center1: NDArray[np.float64],
|
|
282
|
+
center2: NDArray[np.float64],
|
|
283
|
+
) -> SurfaceAnalysisResult:
|
|
284
|
+
"""Analyze relationship between two spheres."""
|
|
285
|
+
if are_spheres_concentric(center1, center2):
|
|
286
|
+
# Concentric spheres create nested regions (like onion layers)
|
|
287
|
+
return SurfaceAnalysisResult(
|
|
288
|
+
relationship=SurfaceRelationship.NESTED,
|
|
289
|
+
details="Spheres are concentric - creates nested regions.",
|
|
290
|
+
materials_consistent=True,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Non-concentric spheres may or may not intersect depending on radii
|
|
294
|
+
# For simplicity, we check material consistency
|
|
295
|
+
consistent = _check_material_consistency(surface1, surface2)
|
|
296
|
+
|
|
297
|
+
return SurfaceAnalysisResult(
|
|
298
|
+
relationship=SurfaceRelationship.INTERSECTING,
|
|
299
|
+
details="Spheres have different centers - may intersect.",
|
|
300
|
+
materials_consistent=consistent,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _analyze_plane_sphere(
|
|
305
|
+
plane: Surface,
|
|
306
|
+
sphere: Surface,
|
|
307
|
+
) -> SurfaceAnalysisResult:
|
|
308
|
+
"""Analyze relationship between a plane and sphere."""
|
|
309
|
+
# A plane always intersects a sphere (unless the sphere is entirely on one side,
|
|
310
|
+
# but we can't easily determine that without bounds)
|
|
311
|
+
consistent = _check_material_consistency(plane, sphere)
|
|
312
|
+
|
|
313
|
+
return SurfaceAnalysisResult(
|
|
314
|
+
relationship=SurfaceRelationship.INTERSECTING,
|
|
315
|
+
details=(
|
|
316
|
+
f"Plane '{plane.name}' and sphere '{sphere.name}' may intersect. "
|
|
317
|
+
f"Material consistency check required."
|
|
318
|
+
),
|
|
319
|
+
materials_consistent=consistent,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _check_material_consistency(
|
|
324
|
+
surface1: Surface,
|
|
325
|
+
surface2: Surface,
|
|
326
|
+
) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
Check if two surfaces have consistent material assignments for intersection.
|
|
329
|
+
|
|
330
|
+
For intersecting surfaces, the front/back model is only consistent if
|
|
331
|
+
all materials are the same OR if the surfaces share materials in a
|
|
332
|
+
compatible way.
|
|
333
|
+
|
|
334
|
+
The simplest consistent case is when both surfaces use the same material
|
|
335
|
+
on both sides (e.g., both are "air" everywhere).
|
|
336
|
+
"""
|
|
337
|
+
mat1_front = surface1.material_front
|
|
338
|
+
mat1_back = surface1.material_back
|
|
339
|
+
mat2_front = surface2.material_front
|
|
340
|
+
mat2_back = surface2.material_back
|
|
341
|
+
|
|
342
|
+
# If any material is None, we can't validate
|
|
343
|
+
if any(m is None for m in [mat1_front, mat1_back, mat2_front, mat2_back]):
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
# Consistent if all four materials are the same
|
|
347
|
+
if mat1_front is mat1_back is mat2_front is mat2_back:
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
# Consistent if each surface has the same material on both sides
|
|
351
|
+
# (even if different between surfaces - this means no refraction at that surface)
|
|
352
|
+
if mat1_front is mat1_back and mat2_front is mat2_back:
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
# For intersecting surfaces with different front/back materials on both,
|
|
356
|
+
# we have a conflict: the 4 quadrants can't all be assigned consistently
|
|
357
|
+
#
|
|
358
|
+
# Quadrant analysis:
|
|
359
|
+
# Q1 (front₁ ∩ front₂): must be mat1_front AND mat2_front
|
|
360
|
+
# Q2 (back₁ ∩ front₂): must be mat1_back AND mat2_front
|
|
361
|
+
# Q3 (front₁ ∩ back₂): must be mat1_front AND mat2_back
|
|
362
|
+
# Q4 (back₁ ∩ back₂): must be mat1_back AND mat2_back
|
|
363
|
+
#
|
|
364
|
+
# This is only satisfiable if the materials form a compatible pattern
|
|
365
|
+
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _material_name(material) -> str:
|
|
370
|
+
"""Get a display name for a material."""
|
|
371
|
+
if material is None:
|
|
372
|
+
return "None"
|
|
373
|
+
if hasattr(material, "name"):
|
|
374
|
+
return str(material.name)
|
|
375
|
+
return type(material).__name__
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
Geometry Validation Exceptions
|
|
36
|
+
|
|
37
|
+
Exception classes for geometry validation errors.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GeometryValidationError(Exception):
|
|
42
|
+
"""Base exception for geometry validation errors."""
|
|
43
|
+
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class IntersectingSurfacesError(GeometryValidationError):
|
|
48
|
+
"""
|
|
49
|
+
Raised when non-parallel surfaces have inconsistent material assignments.
|
|
50
|
+
|
|
51
|
+
Two non-parallel surfaces divide space into 4 quadrants. With the simple
|
|
52
|
+
front/back material model, all 4 quadrants would need to have the same
|
|
53
|
+
material on each side of both surfaces, which is over-constrained when
|
|
54
|
+
the surfaces have different materials.
|
|
55
|
+
|
|
56
|
+
To resolve this error, either:
|
|
57
|
+
1. Use parallel surfaces that don't create conflicting regions
|
|
58
|
+
2. Assign the same material to both sides where conflicts occur
|
|
59
|
+
3. Use the cell-based API (add_surface_only + add_cell) for explicit
|
|
60
|
+
region-by-region material assignment
|
|
61
|
+
|
|
62
|
+
Attributes
|
|
63
|
+
----------
|
|
64
|
+
surface1_name : str
|
|
65
|
+
Name of the first conflicting surface.
|
|
66
|
+
surface2_name : str
|
|
67
|
+
Name of the second conflicting surface.
|
|
68
|
+
details : str
|
|
69
|
+
Detailed explanation of the conflict.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
surface1_name: str,
|
|
75
|
+
surface2_name: str,
|
|
76
|
+
details: str = "",
|
|
77
|
+
):
|
|
78
|
+
self.surface1_name = surface1_name
|
|
79
|
+
self.surface2_name = surface2_name
|
|
80
|
+
self.details = details
|
|
81
|
+
|
|
82
|
+
message = (
|
|
83
|
+
f"Surfaces '{surface1_name}' and '{surface2_name}' intersect "
|
|
84
|
+
f"with inconsistent material assignments.\n"
|
|
85
|
+
f"{details}\n\n"
|
|
86
|
+
f"To resolve this, either:\n"
|
|
87
|
+
f" 1. Use parallel surfaces\n"
|
|
88
|
+
f" 2. Assign the same material to conflicting sides\n"
|
|
89
|
+
f" 3. Use the cell-based API: add_surface_only() + add_cell()"
|
|
90
|
+
)
|
|
91
|
+
super().__init__(message)
|