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,478 @@
|
|
|
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 Builder
|
|
36
|
+
|
|
37
|
+
Fluent interface for constructing simulation geometries.
|
|
38
|
+
Supports any Surface implementation with named media for material consistency.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from dataclasses import replace
|
|
44
|
+
from typing import Self
|
|
45
|
+
|
|
46
|
+
from ..surfaces import Surface, SurfaceRole
|
|
47
|
+
from ..materials import MaterialField
|
|
48
|
+
|
|
49
|
+
from .geometry import Geometry
|
|
50
|
+
from .cell import Cell, HalfSpace
|
|
51
|
+
from .cell_geometry import CellGeometry
|
|
52
|
+
from .surface_analysis import analyze_surface_pair, SurfaceRelationship
|
|
53
|
+
from .validation import IntersectingSurfacesError
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GeometryBuilder:
|
|
57
|
+
"""
|
|
58
|
+
Fluent builder for constructing simulation geometries.
|
|
59
|
+
|
|
60
|
+
Provides a simple interface for registering named media and adding
|
|
61
|
+
surfaces with validation for duplicate names, surface limits, and
|
|
62
|
+
material consistency.
|
|
63
|
+
|
|
64
|
+
Supports two modes:
|
|
65
|
+
1. Standard mode: Use add_surface(surface, front=, back=) for simple geometries
|
|
66
|
+
2. Cell mode: Use add_surface_only() + add_cell() for complex geometries
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
--------
|
|
70
|
+
Standard mode (parallel planes):
|
|
71
|
+
|
|
72
|
+
>>> from lsurf.geometry import GeometryBuilder
|
|
73
|
+
>>> from lsurf.materials import WATER, ExponentialAtmosphere
|
|
74
|
+
>>> from lsurf.surfaces import SphereSurface, PlaneSurface, SurfaceRole
|
|
75
|
+
>>>
|
|
76
|
+
>>> EARTH_RADIUS = 6.371e6
|
|
77
|
+
>>> atmosphere = ExponentialAtmosphere()
|
|
78
|
+
>>>
|
|
79
|
+
>>> ocean = SphereSurface(
|
|
80
|
+
... center=(0, 0, -EARTH_RADIUS),
|
|
81
|
+
... radius=EARTH_RADIUS,
|
|
82
|
+
... role=SurfaceRole.OPTICAL,
|
|
83
|
+
... name="ocean",
|
|
84
|
+
... )
|
|
85
|
+
>>> detector = PlaneSurface(
|
|
86
|
+
... point=(0, 0, 35000),
|
|
87
|
+
... normal=(0, 0, 1),
|
|
88
|
+
... role=SurfaceRole.DETECTOR,
|
|
89
|
+
... name="detector_35km",
|
|
90
|
+
... )
|
|
91
|
+
>>>
|
|
92
|
+
>>> geometry = (
|
|
93
|
+
... GeometryBuilder()
|
|
94
|
+
... .register_medium("atmosphere", atmosphere)
|
|
95
|
+
... .register_medium("ocean", WATER)
|
|
96
|
+
... .set_background("atmosphere")
|
|
97
|
+
... .add_surface(ocean, front="atmosphere", back="ocean")
|
|
98
|
+
... .add_detector(detector)
|
|
99
|
+
... .build()
|
|
100
|
+
... )
|
|
101
|
+
|
|
102
|
+
Cell mode (non-parallel planes with different materials):
|
|
103
|
+
|
|
104
|
+
>>> plane_x = PlaneSurface(point=(0, 0, 0), normal=(1, 0, 0), ...)
|
|
105
|
+
>>> plane_y = PlaneSurface(point=(0, 0, 0), normal=(0, 1, 0), ...)
|
|
106
|
+
>>>
|
|
107
|
+
>>> geometry = (
|
|
108
|
+
... GeometryBuilder()
|
|
109
|
+
... .register_medium("air", AIR)
|
|
110
|
+
... .register_medium("water", WATER)
|
|
111
|
+
... .register_medium("glass", GLASS)
|
|
112
|
+
... .register_medium("vacuum", VACUUM)
|
|
113
|
+
... .set_background("air")
|
|
114
|
+
... .add_surface_only(plane_x)
|
|
115
|
+
... .add_surface_only(plane_y)
|
|
116
|
+
... .add_cell("air", ("plane_x", True), ("plane_y", True)) # Q1
|
|
117
|
+
... .add_cell("water", ("plane_x", False), ("plane_y", True)) # Q2
|
|
118
|
+
... .add_cell("glass", ("plane_x", True), ("plane_y", False)) # Q3
|
|
119
|
+
... .add_cell("vacuum", ("plane_x", False), ("plane_y", False)) # Q4
|
|
120
|
+
... .build()
|
|
121
|
+
... )
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
self._media: dict[str, MaterialField] = {}
|
|
126
|
+
self._background_medium: str | None = None
|
|
127
|
+
self._surfaces: list[Surface] = []
|
|
128
|
+
self._detectors: list[Surface] = []
|
|
129
|
+
self._surface_names: dict[str, int] = {}
|
|
130
|
+
self._detector_names: dict[str, int] = {}
|
|
131
|
+
|
|
132
|
+
# Cell mode state
|
|
133
|
+
self._cells: list[Cell] = []
|
|
134
|
+
self._cell_mode: bool = False
|
|
135
|
+
self._surface_only_names: set[str] = set() # Surfaces added without materials
|
|
136
|
+
|
|
137
|
+
def register_medium(self, name: str, material: MaterialField) -> Self:
|
|
138
|
+
"""
|
|
139
|
+
Register a named medium with its material.
|
|
140
|
+
|
|
141
|
+
A medium is a named reference to a material. Surfaces reference
|
|
142
|
+
media by name, ensuring material consistency when multiple
|
|
143
|
+
surfaces share the same medium.
|
|
144
|
+
|
|
145
|
+
If the same medium name is registered twice, validates that the
|
|
146
|
+
same material instance is used.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
name : str
|
|
151
|
+
Name of the medium.
|
|
152
|
+
material : MaterialField
|
|
153
|
+
Material for this medium.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
Self
|
|
158
|
+
The builder instance for chaining.
|
|
159
|
+
|
|
160
|
+
Raises
|
|
161
|
+
------
|
|
162
|
+
ValueError
|
|
163
|
+
If medium already registered with different material instance.
|
|
164
|
+
"""
|
|
165
|
+
if name in self._media:
|
|
166
|
+
if self._media[name] is not material:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"Medium '{name}' already registered with different material instance"
|
|
169
|
+
)
|
|
170
|
+
self._media[name] = material
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def set_background(self, medium_name: str) -> Self:
|
|
174
|
+
"""
|
|
175
|
+
Set the background/propagation medium by name.
|
|
176
|
+
|
|
177
|
+
The medium must be registered before calling this method.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
medium_name : str
|
|
182
|
+
Name of the medium to use as background.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
Self
|
|
187
|
+
The builder instance for chaining.
|
|
188
|
+
|
|
189
|
+
Raises
|
|
190
|
+
------
|
|
191
|
+
ValueError
|
|
192
|
+
If the medium is not registered.
|
|
193
|
+
"""
|
|
194
|
+
if medium_name not in self._media:
|
|
195
|
+
raise ValueError(f"Medium '{medium_name}' not registered")
|
|
196
|
+
self._background_medium = medium_name
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
def add_surface(
|
|
200
|
+
self,
|
|
201
|
+
surface: Surface,
|
|
202
|
+
front: str,
|
|
203
|
+
back: str,
|
|
204
|
+
) -> Self:
|
|
205
|
+
"""
|
|
206
|
+
Add a surface with materials from named media.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
surface : Surface
|
|
211
|
+
Surface geometry (materials will be replaced).
|
|
212
|
+
front : str
|
|
213
|
+
Medium name for the front side material.
|
|
214
|
+
back : str
|
|
215
|
+
Medium name for the back side material.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
Self
|
|
220
|
+
The builder instance for chaining.
|
|
221
|
+
|
|
222
|
+
Raises
|
|
223
|
+
------
|
|
224
|
+
ValueError
|
|
225
|
+
If surface name is duplicate, medium names are not registered,
|
|
226
|
+
or cell mode is active.
|
|
227
|
+
"""
|
|
228
|
+
if self._cell_mode:
|
|
229
|
+
raise ValueError(
|
|
230
|
+
"Cannot use add_surface() in cell mode. "
|
|
231
|
+
"Use add_surface_only() instead, or don't call add_cell()."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
self._validate_name(surface.name)
|
|
235
|
+
|
|
236
|
+
# Lookup materials from media
|
|
237
|
+
front_material = self._get_medium_material(front)
|
|
238
|
+
back_material = self._get_medium_material(back)
|
|
239
|
+
|
|
240
|
+
# Create new surface with correct materials using dataclasses.replace()
|
|
241
|
+
new_surface = replace(
|
|
242
|
+
surface,
|
|
243
|
+
material_front=front_material,
|
|
244
|
+
material_back=back_material,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
self._surface_names[surface.name] = len(self._surfaces)
|
|
248
|
+
self._surfaces.append(new_surface)
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def add_surface_only(self, surface: Surface) -> Self:
|
|
252
|
+
"""
|
|
253
|
+
Add a surface without material assignment (for cell mode).
|
|
254
|
+
|
|
255
|
+
Use this with add_cell() to define complex geometries where
|
|
256
|
+
surfaces intersect and the simple front/back model is insufficient.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
surface : Surface
|
|
261
|
+
Surface geometry. Materials should be left as None.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
Self
|
|
266
|
+
The builder instance for chaining.
|
|
267
|
+
|
|
268
|
+
Raises
|
|
269
|
+
------
|
|
270
|
+
ValueError
|
|
271
|
+
If surface name is duplicate.
|
|
272
|
+
"""
|
|
273
|
+
self._validate_name(surface.name)
|
|
274
|
+
|
|
275
|
+
self._surface_names[surface.name] = len(self._surfaces)
|
|
276
|
+
self._surfaces.append(surface)
|
|
277
|
+
self._surface_only_names.add(surface.name)
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
def add_cell(
|
|
281
|
+
self,
|
|
282
|
+
medium_name: str,
|
|
283
|
+
*conditions: tuple[str, bool],
|
|
284
|
+
name: str = "",
|
|
285
|
+
) -> Self:
|
|
286
|
+
"""
|
|
287
|
+
Define a cell (region) by half-space intersection.
|
|
288
|
+
|
|
289
|
+
A cell is a region of space defined by being on specific sides
|
|
290
|
+
of multiple surfaces. Each condition specifies a surface and
|
|
291
|
+
which side (front=True means signed_distance > 0).
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
medium_name : str
|
|
296
|
+
Name of the medium for this cell.
|
|
297
|
+
*conditions : tuple[str, bool]
|
|
298
|
+
Each condition is (surface_name, front) where:
|
|
299
|
+
- surface_name: Name of a surface added with add_surface_only()
|
|
300
|
+
- front: True for front side (signed_distance > 0),
|
|
301
|
+
False for back side (signed_distance < 0)
|
|
302
|
+
name : str, optional
|
|
303
|
+
Optional human-readable name for the cell.
|
|
304
|
+
|
|
305
|
+
Returns
|
|
306
|
+
-------
|
|
307
|
+
Self
|
|
308
|
+
The builder instance for chaining.
|
|
309
|
+
|
|
310
|
+
Raises
|
|
311
|
+
------
|
|
312
|
+
ValueError
|
|
313
|
+
If medium is not registered or surface name not found.
|
|
314
|
+
|
|
315
|
+
Examples
|
|
316
|
+
--------
|
|
317
|
+
>>> builder.add_cell("air", ("plane_x", True), ("plane_y", True), name="Q1")
|
|
318
|
+
"""
|
|
319
|
+
# Enable cell mode
|
|
320
|
+
self._cell_mode = True
|
|
321
|
+
|
|
322
|
+
# Validate medium exists
|
|
323
|
+
if medium_name not in self._media:
|
|
324
|
+
available = ", ".join(sorted(self._media.keys()))
|
|
325
|
+
raise ValueError(
|
|
326
|
+
f"Medium '{medium_name}' not registered. Available: {available}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Validate surface names exist
|
|
330
|
+
for surface_name, _front in conditions:
|
|
331
|
+
if surface_name not in self._surface_names:
|
|
332
|
+
available = ", ".join(sorted(self._surface_names.keys()))
|
|
333
|
+
raise ValueError(
|
|
334
|
+
f"Surface '{surface_name}' not found. Available: {available}"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Create half-spaces and cell
|
|
338
|
+
half_spaces = tuple(
|
|
339
|
+
HalfSpace(surface_name=cond[0], front=cond[1]) for cond in conditions
|
|
340
|
+
)
|
|
341
|
+
cell = Cell(half_spaces=half_spaces, medium_name=medium_name, name=name)
|
|
342
|
+
self._cells.append(cell)
|
|
343
|
+
|
|
344
|
+
return self
|
|
345
|
+
|
|
346
|
+
def add_detector(self, detector: Surface) -> Self:
|
|
347
|
+
"""
|
|
348
|
+
Add a detector Surface to the geometry.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
detector : Surface
|
|
353
|
+
Any object implementing the Surface protocol.
|
|
354
|
+
Must have a unique `name` attribute.
|
|
355
|
+
|
|
356
|
+
Returns
|
|
357
|
+
-------
|
|
358
|
+
Self
|
|
359
|
+
The builder instance for chaining.
|
|
360
|
+
|
|
361
|
+
Raises
|
|
362
|
+
------
|
|
363
|
+
ValueError
|
|
364
|
+
If detector name is duplicate.
|
|
365
|
+
"""
|
|
366
|
+
self._validate_name(detector.name)
|
|
367
|
+
|
|
368
|
+
self._detector_names[detector.name] = len(self._detectors)
|
|
369
|
+
self._detectors.append(detector)
|
|
370
|
+
return self
|
|
371
|
+
|
|
372
|
+
def _get_medium_material(self, medium_name: str) -> MaterialField:
|
|
373
|
+
"""Get material for a medium, raising if not registered."""
|
|
374
|
+
if medium_name not in self._media:
|
|
375
|
+
available = ", ".join(sorted(self._media.keys()))
|
|
376
|
+
raise ValueError(
|
|
377
|
+
f"Medium '{medium_name}' not registered. Available: {available}"
|
|
378
|
+
)
|
|
379
|
+
return self._media[medium_name]
|
|
380
|
+
|
|
381
|
+
def _validate_surface_consistency(self) -> None:
|
|
382
|
+
"""
|
|
383
|
+
Validate that surface material assignments are consistent.
|
|
384
|
+
|
|
385
|
+
For non-parallel surfaces with different front/back materials,
|
|
386
|
+
raises IntersectingSurfacesError.
|
|
387
|
+
"""
|
|
388
|
+
optical_surfaces = [s for s in self._surfaces if s.role == SurfaceRole.OPTICAL]
|
|
389
|
+
|
|
390
|
+
# Check all pairs
|
|
391
|
+
for i, surf1 in enumerate(optical_surfaces):
|
|
392
|
+
for surf2 in optical_surfaces[i + 1 :]:
|
|
393
|
+
result = analyze_surface_pair(surf1, surf2)
|
|
394
|
+
|
|
395
|
+
if (
|
|
396
|
+
result.relationship == SurfaceRelationship.INTERSECTING
|
|
397
|
+
and not result.materials_consistent
|
|
398
|
+
):
|
|
399
|
+
raise IntersectingSurfacesError(
|
|
400
|
+
surface1_name=surf1.name,
|
|
401
|
+
surface2_name=surf2.name,
|
|
402
|
+
details=result.details,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def build(self, validate: bool = True) -> Geometry | CellGeometry:
|
|
406
|
+
"""
|
|
407
|
+
Build the immutable Geometry object.
|
|
408
|
+
|
|
409
|
+
Parameters
|
|
410
|
+
----------
|
|
411
|
+
validate : bool, optional
|
|
412
|
+
If True (default), validates that surface material assignments
|
|
413
|
+
are consistent. Set to False to skip validation.
|
|
414
|
+
|
|
415
|
+
Returns
|
|
416
|
+
-------
|
|
417
|
+
Geometry or CellGeometry
|
|
418
|
+
Standard Geometry if using add_surface(), or CellGeometry if
|
|
419
|
+
using add_surface_only() + add_cell().
|
|
420
|
+
|
|
421
|
+
Raises
|
|
422
|
+
------
|
|
423
|
+
ValueError
|
|
424
|
+
If background medium not set or OPTICAL surfaces missing materials.
|
|
425
|
+
IntersectingSurfacesError
|
|
426
|
+
If non-parallel surfaces have conflicting material assignments
|
|
427
|
+
(only when validate=True).
|
|
428
|
+
"""
|
|
429
|
+
if self._background_medium is None:
|
|
430
|
+
raise ValueError("Background medium not set. Call set_background() first.")
|
|
431
|
+
|
|
432
|
+
# Cell mode: return CellGeometry
|
|
433
|
+
if self._cell_mode:
|
|
434
|
+
if not self._cells:
|
|
435
|
+
raise ValueError(
|
|
436
|
+
"Cell mode enabled but no cells defined. "
|
|
437
|
+
"Call add_cell() at least once."
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
return CellGeometry(
|
|
441
|
+
surfaces=tuple(self._surfaces),
|
|
442
|
+
detectors=tuple(self._detectors),
|
|
443
|
+
background_material=self._media[self._background_medium],
|
|
444
|
+
media=dict(self._media),
|
|
445
|
+
surface_names=dict(self._surface_names),
|
|
446
|
+
detector_names=dict(self._detector_names),
|
|
447
|
+
cells=tuple(self._cells),
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Standard mode: validate and return Geometry
|
|
451
|
+
# Validate all OPTICAL surfaces have materials
|
|
452
|
+
for surface in self._surfaces:
|
|
453
|
+
if surface.role == SurfaceRole.OPTICAL:
|
|
454
|
+
if surface.material_front is None or surface.material_back is None:
|
|
455
|
+
raise ValueError(
|
|
456
|
+
f"OPTICAL surface '{surface.name}' missing materials. "
|
|
457
|
+
"Use add_surface(surface, front='medium', back='medium')."
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Validate surface consistency
|
|
461
|
+
if validate:
|
|
462
|
+
self._validate_surface_consistency()
|
|
463
|
+
|
|
464
|
+
return Geometry(
|
|
465
|
+
surfaces=tuple(self._surfaces),
|
|
466
|
+
detectors=tuple(self._detectors),
|
|
467
|
+
background_material=self._media[self._background_medium],
|
|
468
|
+
media=dict(self._media),
|
|
469
|
+
surface_names=dict(self._surface_names),
|
|
470
|
+
detector_names=dict(self._detector_names),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def _validate_name(self, name: str) -> None:
|
|
474
|
+
"""Validate that a name is unique across surfaces and detectors."""
|
|
475
|
+
if name in self._surface_names:
|
|
476
|
+
raise ValueError(f"Duplicate surface name: '{name}'")
|
|
477
|
+
if name in self._detector_names:
|
|
478
|
+
raise ValueError(f"Duplicate detector name: '{name}'")
|
lsurf/geometry/cell.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
Cell-Based Geometry Definition
|
|
36
|
+
|
|
37
|
+
Provides explicit region-by-region material assignment for complex geometries
|
|
38
|
+
where the simple front/back model is insufficient.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from typing import TYPE_CHECKING
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from numpy.typing import NDArray
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class HalfSpace:
|
|
54
|
+
"""
|
|
55
|
+
Defines one side of a surface (half-space).
|
|
56
|
+
|
|
57
|
+
A half-space is the region on one side of a surface, identified by
|
|
58
|
+
whether the signed distance is positive (front) or negative (back).
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
surface_name : str
|
|
63
|
+
Name of the surface that defines this half-space.
|
|
64
|
+
front : bool
|
|
65
|
+
If True, the half-space is where signed_distance > 0 (front side).
|
|
66
|
+
If False, the half-space is where signed_distance < 0 (back side).
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
--------
|
|
70
|
+
>>> # Front side of plane_x
|
|
71
|
+
>>> hs1 = HalfSpace("plane_x", front=True)
|
|
72
|
+
>>> # Back side of plane_y
|
|
73
|
+
>>> hs2 = HalfSpace("plane_y", front=False)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
surface_name: str
|
|
77
|
+
front: bool
|
|
78
|
+
|
|
79
|
+
def __repr__(self) -> str:
|
|
80
|
+
side = "front" if self.front else "back"
|
|
81
|
+
return f"HalfSpace({self.surface_name!r}, {side})"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class Cell:
|
|
86
|
+
"""
|
|
87
|
+
A region of space defined by the intersection of half-spaces.
|
|
88
|
+
|
|
89
|
+
A cell represents a region where all half-space conditions are satisfied
|
|
90
|
+
simultaneously. The material/medium assigned to this cell applies to
|
|
91
|
+
all points within the region.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
half_spaces : tuple of HalfSpace
|
|
96
|
+
Half-space conditions that must all be satisfied for a point
|
|
97
|
+
to be in this cell.
|
|
98
|
+
medium_name : str
|
|
99
|
+
Name of the medium (material) for this cell.
|
|
100
|
+
name : str, optional
|
|
101
|
+
Optional human-readable name for the cell.
|
|
102
|
+
|
|
103
|
+
Examples
|
|
104
|
+
--------
|
|
105
|
+
>>> # Define a cell for the region where x > 0 and y > 0
|
|
106
|
+
>>> cell = Cell(
|
|
107
|
+
... half_spaces=(
|
|
108
|
+
... HalfSpace("plane_x", front=True),
|
|
109
|
+
... HalfSpace("plane_y", front=True),
|
|
110
|
+
... ),
|
|
111
|
+
... medium_name="air",
|
|
112
|
+
... name="Q1",
|
|
113
|
+
... )
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
half_spaces: tuple[HalfSpace, ...]
|
|
117
|
+
medium_name: str
|
|
118
|
+
name: str = ""
|
|
119
|
+
|
|
120
|
+
def contains(
|
|
121
|
+
self,
|
|
122
|
+
signed_distances: dict[str, NDArray[np.float64]],
|
|
123
|
+
) -> NDArray[np.bool_]:
|
|
124
|
+
"""
|
|
125
|
+
Determine which positions are contained in this cell.
|
|
126
|
+
|
|
127
|
+
A position is in the cell if it satisfies all half-space conditions:
|
|
128
|
+
- For HalfSpace(surface, front=True): signed_distance[surface] > 0
|
|
129
|
+
- For HalfSpace(surface, front=False): signed_distance[surface] < 0
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
signed_distances : dict[str, NDArray]
|
|
134
|
+
Mapping from surface name to signed distance arrays.
|
|
135
|
+
All arrays must have the same shape (N,).
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
NDArray[np.bool_], shape (N,)
|
|
140
|
+
True for positions that are inside this cell.
|
|
141
|
+
|
|
142
|
+
Raises
|
|
143
|
+
------
|
|
144
|
+
KeyError
|
|
145
|
+
If a required surface name is not in signed_distances.
|
|
146
|
+
"""
|
|
147
|
+
if not self.half_spaces:
|
|
148
|
+
# Empty half-spaces means no constraints - everything is inside
|
|
149
|
+
# Get shape from any signed distance array
|
|
150
|
+
for arr in signed_distances.values():
|
|
151
|
+
return np.ones(arr.shape, dtype=np.bool_)
|
|
152
|
+
return np.array([], dtype=np.bool_)
|
|
153
|
+
|
|
154
|
+
# Start with all True
|
|
155
|
+
first_hs = self.half_spaces[0]
|
|
156
|
+
sd = signed_distances[first_hs.surface_name]
|
|
157
|
+
mask = sd > 0 if first_hs.front else sd < 0
|
|
158
|
+
|
|
159
|
+
# AND with remaining half-spaces
|
|
160
|
+
for hs in self.half_spaces[1:]:
|
|
161
|
+
sd = signed_distances[hs.surface_name]
|
|
162
|
+
condition = sd > 0 if hs.front else sd < 0
|
|
163
|
+
mask = mask & condition
|
|
164
|
+
|
|
165
|
+
return mask
|
|
166
|
+
|
|
167
|
+
def __repr__(self) -> str:
|
|
168
|
+
if self.name:
|
|
169
|
+
return f"Cell({self.name!r}, medium={self.medium_name!r})"
|
|
170
|
+
conditions = ", ".join(repr(hs) for hs in self.half_spaces)
|
|
171
|
+
return f"Cell([{conditions}], medium={self.medium_name!r})"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def create_half_space(surface_name: str, front: bool) -> HalfSpace:
|
|
175
|
+
"""
|
|
176
|
+
Create a HalfSpace from surface name and side.
|
|
177
|
+
|
|
178
|
+
Convenience function for creating half-spaces.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
surface_name : str
|
|
183
|
+
Name of the surface.
|
|
184
|
+
front : bool
|
|
185
|
+
True for front side (signed_distance > 0),
|
|
186
|
+
False for back side (signed_distance < 0).
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
HalfSpace
|
|
191
|
+
The created half-space.
|
|
192
|
+
"""
|
|
193
|
+
return HalfSpace(surface_name=surface_name, front=front)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def create_cell(
|
|
197
|
+
medium_name: str,
|
|
198
|
+
*conditions: tuple[str, bool],
|
|
199
|
+
name: str = "",
|
|
200
|
+
) -> Cell:
|
|
201
|
+
"""
|
|
202
|
+
Create a Cell from medium name and half-space conditions.
|
|
203
|
+
|
|
204
|
+
Convenience function for creating cells with a cleaner syntax.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
medium_name : str
|
|
209
|
+
Name of the medium for this cell.
|
|
210
|
+
*conditions : tuple[str, bool]
|
|
211
|
+
Each condition is (surface_name, front) defining a half-space.
|
|
212
|
+
name : str, optional
|
|
213
|
+
Optional name for the cell.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
Cell
|
|
218
|
+
The created cell.
|
|
219
|
+
|
|
220
|
+
Examples
|
|
221
|
+
--------
|
|
222
|
+
>>> # Q1: front of plane_x AND front of plane_y
|
|
223
|
+
>>> cell = create_cell("air", ("plane_x", True), ("plane_y", True), name="Q1")
|
|
224
|
+
"""
|
|
225
|
+
half_spaces = tuple(
|
|
226
|
+
HalfSpace(surface_name=cond[0], front=cond[1]) for cond in conditions
|
|
227
|
+
)
|
|
228
|
+
return Cell(half_spaces=half_spaces, medium_name=medium_name, name=name)
|