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,319 @@
|
|
|
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
|
+
Grid Inhomogeneous Model (3D Grid Data)
|
|
36
|
+
|
|
37
|
+
Base class for 3D inhomogeneous materials defined on a regular grid.
|
|
38
|
+
User provides a 3D array of refractive index values.
|
|
39
|
+
GPU support is automatic via trilinear interpolation.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from typing import TYPE_CHECKING
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
from numpy.typing import NDArray
|
|
48
|
+
|
|
49
|
+
from .material_field import MaterialField
|
|
50
|
+
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from ...propagation.kernels.registry import PropagationKernelID, PropagatorID
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class GridInhomogeneousModel(MaterialField):
|
|
56
|
+
"""
|
|
57
|
+
Base class for 3D inhomogeneous materials defined on a grid.
|
|
58
|
+
|
|
59
|
+
User provides a 3D array of refractive index values. The system provides:
|
|
60
|
+
- Trilinear interpolation for arbitrary positions
|
|
61
|
+
- Automatic gradient computation via finite differences
|
|
62
|
+
- GPU support via 3D array interpolation
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
name : str
|
|
67
|
+
Name of the material model
|
|
68
|
+
n_grid : ndarray (nx, ny, nz)
|
|
69
|
+
Refractive index values on 3D grid
|
|
70
|
+
bounds : tuple
|
|
71
|
+
((x_min, x_max), (y_min, y_max), (z_min, z_max))
|
|
72
|
+
gradient_grid : ndarray (nx, ny, nz, 3), optional
|
|
73
|
+
Pre-computed gradients. If None, computed via finite differences.
|
|
74
|
+
alpha_grid : ndarray (nx, ny, nz), optional
|
|
75
|
+
Absorption coefficient values on 3D grid. If None, defaults to zeros.
|
|
76
|
+
|
|
77
|
+
Example
|
|
78
|
+
-------
|
|
79
|
+
>>> n_grid = np.ones((100, 100, 50)) * 1.000293
|
|
80
|
+
>>> n_grid += generate_turbulence(100, 100, 50) # Add perturbations
|
|
81
|
+
>>> material = GridInhomogeneousModel(
|
|
82
|
+
... name="Turbulent Field",
|
|
83
|
+
... n_grid=n_grid,
|
|
84
|
+
... bounds=((-5000, 5000), (-5000, 5000), (0, 25000))
|
|
85
|
+
... )
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# =========================================================================
|
|
89
|
+
# COMPATIBILITY DECLARATIONS
|
|
90
|
+
# =========================================================================
|
|
91
|
+
@classmethod
|
|
92
|
+
def _init_compatibility(cls):
|
|
93
|
+
"""Initialize compatibility declarations (called once on first use)."""
|
|
94
|
+
from ...propagation.kernels.registry import PropagationKernelID, PropagatorID
|
|
95
|
+
|
|
96
|
+
if not cls._supported_kernels:
|
|
97
|
+
cls._supported_kernels = [
|
|
98
|
+
PropagationKernelID.GRID_EULER,
|
|
99
|
+
PropagationKernelID.GRID_RK4,
|
|
100
|
+
]
|
|
101
|
+
cls._default_kernel = PropagationKernelID.GRID_RK4
|
|
102
|
+
cls._supported_propagators = [
|
|
103
|
+
PropagatorID.GPU_GRADIENT,
|
|
104
|
+
PropagatorID.CPU_GRADIENT,
|
|
105
|
+
]
|
|
106
|
+
cls._default_propagator = PropagatorID.GPU_GRADIENT
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
name: str = "Grid Inhomogeneous",
|
|
111
|
+
n_grid: NDArray[np.float32] | None = None,
|
|
112
|
+
bounds: tuple[tuple[float, float], ...] | None = None,
|
|
113
|
+
gradient_grid: NDArray[np.float32] | None = None,
|
|
114
|
+
alpha_grid: NDArray[np.float32] | None = None,
|
|
115
|
+
kernel: PropagationKernelID | None = None,
|
|
116
|
+
propagator: PropagatorID | None = None,
|
|
117
|
+
):
|
|
118
|
+
# Initialize compatibility declarations before calling super().__init__
|
|
119
|
+
self._init_compatibility()
|
|
120
|
+
|
|
121
|
+
super().__init__(name, kernel=kernel, propagator=propagator)
|
|
122
|
+
self._is_homogeneous = False
|
|
123
|
+
|
|
124
|
+
if n_grid is None:
|
|
125
|
+
raise ValueError("n_grid is required")
|
|
126
|
+
if bounds is None:
|
|
127
|
+
raise ValueError("bounds is required")
|
|
128
|
+
if len(bounds) != 3:
|
|
129
|
+
raise ValueError("bounds must have 3 elements: ((x_min, x_max), ...)")
|
|
130
|
+
|
|
131
|
+
self._n_grid = n_grid.astype(np.float32)
|
|
132
|
+
self._bounds = bounds
|
|
133
|
+
self._shape = n_grid.shape
|
|
134
|
+
|
|
135
|
+
# Compute grid spacing
|
|
136
|
+
(x_min, x_max), (y_min, y_max), (z_min, z_max) = bounds
|
|
137
|
+
nx, ny, nz = self._shape
|
|
138
|
+
self._dx = (x_max - x_min) / (nx - 1) if nx > 1 else 1.0
|
|
139
|
+
self._dy = (y_max - y_min) / (ny - 1) if ny > 1 else 1.0
|
|
140
|
+
self._dz = (z_max - z_min) / (nz - 1) if nz > 1 else 1.0
|
|
141
|
+
self._origin = (x_min, y_min, z_min)
|
|
142
|
+
|
|
143
|
+
# Compute or store gradient grid
|
|
144
|
+
if gradient_grid is not None:
|
|
145
|
+
self._grad_grid = gradient_grid.astype(np.float32)
|
|
146
|
+
else:
|
|
147
|
+
self._grad_grid = self._compute_gradient_grid()
|
|
148
|
+
|
|
149
|
+
# Store or create zero absorption grid
|
|
150
|
+
if alpha_grid is not None:
|
|
151
|
+
self._alpha_grid = alpha_grid.astype(np.float32)
|
|
152
|
+
else:
|
|
153
|
+
self._alpha_grid = np.zeros(self._shape, dtype=np.float32)
|
|
154
|
+
|
|
155
|
+
def _compute_gradient_grid(self) -> NDArray[np.float32]:
|
|
156
|
+
"""Compute gradient via finite differences."""
|
|
157
|
+
grad_x = np.gradient(self._n_grid, self._dx, axis=0)
|
|
158
|
+
grad_y = np.gradient(self._n_grid, self._dy, axis=1)
|
|
159
|
+
grad_z = np.gradient(self._n_grid, self._dz, axis=2)
|
|
160
|
+
|
|
161
|
+
return np.stack([grad_x, grad_y, grad_z], axis=-1).astype(np.float32)
|
|
162
|
+
|
|
163
|
+
def _trilinear_interpolate(
|
|
164
|
+
self,
|
|
165
|
+
x: float | NDArray,
|
|
166
|
+
y: float | NDArray,
|
|
167
|
+
z: float | NDArray,
|
|
168
|
+
grid: NDArray,
|
|
169
|
+
) -> float | NDArray:
|
|
170
|
+
"""Trilinear interpolation in 3D grid."""
|
|
171
|
+
x = np.asarray(x)
|
|
172
|
+
y = np.asarray(y)
|
|
173
|
+
z = np.asarray(z)
|
|
174
|
+
|
|
175
|
+
# Normalize to grid coordinates
|
|
176
|
+
x_norm = (x - self._origin[0]) / self._dx
|
|
177
|
+
y_norm = (y - self._origin[1]) / self._dy
|
|
178
|
+
z_norm = (z - self._origin[2]) / self._dz
|
|
179
|
+
|
|
180
|
+
# Clamp to valid range
|
|
181
|
+
x_norm = np.clip(x_norm, 0, self._shape[0] - 1.001)
|
|
182
|
+
y_norm = np.clip(y_norm, 0, self._shape[1] - 1.001)
|
|
183
|
+
z_norm = np.clip(z_norm, 0, self._shape[2] - 1.001)
|
|
184
|
+
|
|
185
|
+
# Get integer indices and fractions
|
|
186
|
+
x0 = np.floor(x_norm).astype(int)
|
|
187
|
+
y0 = np.floor(y_norm).astype(int)
|
|
188
|
+
z0 = np.floor(z_norm).astype(int)
|
|
189
|
+
|
|
190
|
+
x1 = np.minimum(x0 + 1, self._shape[0] - 1)
|
|
191
|
+
y1 = np.minimum(y0 + 1, self._shape[1] - 1)
|
|
192
|
+
z1 = np.minimum(z0 + 1, self._shape[2] - 1)
|
|
193
|
+
|
|
194
|
+
xd = x_norm - x0
|
|
195
|
+
yd = y_norm - y0
|
|
196
|
+
zd = z_norm - z0
|
|
197
|
+
|
|
198
|
+
# Trilinear interpolation
|
|
199
|
+
if grid.ndim == 3:
|
|
200
|
+
# Scalar field
|
|
201
|
+
c000 = grid[x0, y0, z0]
|
|
202
|
+
c001 = grid[x0, y0, z1]
|
|
203
|
+
c010 = grid[x0, y1, z0]
|
|
204
|
+
c011 = grid[x0, y1, z1]
|
|
205
|
+
c100 = grid[x1, y0, z0]
|
|
206
|
+
c101 = grid[x1, y0, z1]
|
|
207
|
+
c110 = grid[x1, y1, z0]
|
|
208
|
+
c111 = grid[x1, y1, z1]
|
|
209
|
+
|
|
210
|
+
c00 = c000 * (1 - xd) + c100 * xd
|
|
211
|
+
c01 = c001 * (1 - xd) + c101 * xd
|
|
212
|
+
c10 = c010 * (1 - xd) + c110 * xd
|
|
213
|
+
c11 = c011 * (1 - xd) + c111 * xd
|
|
214
|
+
|
|
215
|
+
c0 = c00 * (1 - yd) + c10 * yd
|
|
216
|
+
c1 = c01 * (1 - yd) + c11 * yd
|
|
217
|
+
|
|
218
|
+
return c0 * (1 - zd) + c1 * zd
|
|
219
|
+
else:
|
|
220
|
+
# Vector field (gradient)
|
|
221
|
+
result = []
|
|
222
|
+
for i in range(grid.shape[-1]):
|
|
223
|
+
result.append(self._trilinear_interpolate(x, y, z, grid[..., i]))
|
|
224
|
+
return tuple(result)
|
|
225
|
+
|
|
226
|
+
def get_refractive_index(
|
|
227
|
+
self,
|
|
228
|
+
x: float | NDArray[np.float64],
|
|
229
|
+
y: float | NDArray[np.float64],
|
|
230
|
+
z: float | NDArray[np.float64],
|
|
231
|
+
wavelength: float | NDArray[np.float64],
|
|
232
|
+
) -> float | NDArray[np.float64]:
|
|
233
|
+
"""Get refractive index via trilinear interpolation."""
|
|
234
|
+
return self._trilinear_interpolate(x, y, z, self._n_grid)
|
|
235
|
+
|
|
236
|
+
def get_refractive_index_gradient(
|
|
237
|
+
self,
|
|
238
|
+
x: float | NDArray[np.float64],
|
|
239
|
+
y: float | NDArray[np.float64],
|
|
240
|
+
z: float | NDArray[np.float64],
|
|
241
|
+
wavelength: float | NDArray[np.float64],
|
|
242
|
+
) -> tuple[
|
|
243
|
+
float | NDArray[np.float64],
|
|
244
|
+
float | NDArray[np.float64],
|
|
245
|
+
float | NDArray[np.float64],
|
|
246
|
+
]:
|
|
247
|
+
"""Get gradient via trilinear interpolation in gradient grid."""
|
|
248
|
+
return self._trilinear_interpolate(x, y, z, self._grad_grid)
|
|
249
|
+
|
|
250
|
+
def get_absorption_coefficient(
|
|
251
|
+
self,
|
|
252
|
+
x: float | NDArray[np.float64],
|
|
253
|
+
y: float | NDArray[np.float64],
|
|
254
|
+
z: float | NDArray[np.float64],
|
|
255
|
+
wavelength: float | NDArray[np.float64],
|
|
256
|
+
) -> float | NDArray[np.float64]:
|
|
257
|
+
"""Return absorption coefficient (default: 0)."""
|
|
258
|
+
if isinstance(x, np.ndarray):
|
|
259
|
+
return np.zeros_like(x)
|
|
260
|
+
return 0.0
|
|
261
|
+
|
|
262
|
+
def get_scattering_coefficient(
|
|
263
|
+
self,
|
|
264
|
+
x: float | NDArray[np.float64],
|
|
265
|
+
y: float | NDArray[np.float64],
|
|
266
|
+
z: float | NDArray[np.float64],
|
|
267
|
+
wavelength: float | NDArray[np.float64],
|
|
268
|
+
) -> float | NDArray[np.float64]:
|
|
269
|
+
"""Return scattering coefficient (default: 0)."""
|
|
270
|
+
if isinstance(x, np.ndarray):
|
|
271
|
+
return np.zeros_like(x)
|
|
272
|
+
return 0.0
|
|
273
|
+
|
|
274
|
+
# =========================================================================
|
|
275
|
+
# GPU INTERFACE
|
|
276
|
+
# =========================================================================
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def gpu_material_id(self) -> int:
|
|
280
|
+
"""Return GPU material ID for kernel dispatch."""
|
|
281
|
+
from ...propagation.propagator_protocol import GPUMaterialID
|
|
282
|
+
|
|
283
|
+
return GPUMaterialID.GRID_INHOMOGENEOUS
|
|
284
|
+
|
|
285
|
+
def get_gpu_kernels(self) -> dict:
|
|
286
|
+
"""Return GPU kernels for propagation."""
|
|
287
|
+
from ...propagation.kernels.propagation import (
|
|
288
|
+
_kernel_grid_inhomogeneous_euler,
|
|
289
|
+
_kernel_grid_inhomogeneous_rk4,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
"euler": _kernel_grid_inhomogeneous_euler,
|
|
294
|
+
"rk4": _kernel_grid_inhomogeneous_rk4,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
def get_gpu_parameters(self) -> tuple:
|
|
298
|
+
"""Return scalar parameters for GPU kernel."""
|
|
299
|
+
(x_min, x_max), (y_min, y_max), (z_min, z_max) = self._bounds
|
|
300
|
+
nx, ny, nz = self._shape
|
|
301
|
+
return (
|
|
302
|
+
float(x_min),
|
|
303
|
+
float(y_min),
|
|
304
|
+
float(z_min),
|
|
305
|
+
float(self._dx),
|
|
306
|
+
float(self._dy),
|
|
307
|
+
float(self._dz),
|
|
308
|
+
int(nx),
|
|
309
|
+
int(ny),
|
|
310
|
+
int(nz),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def get_gpu_arrays(self) -> dict:
|
|
314
|
+
"""Return device arrays for GPU kernel."""
|
|
315
|
+
return {
|
|
316
|
+
"n_grid": self._n_grid,
|
|
317
|
+
"grad_grid": self._grad_grid,
|
|
318
|
+
"alpha_grid": self._alpha_grid,
|
|
319
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
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
|
+
Homogeneous Material Implementation
|
|
36
|
+
|
|
37
|
+
A material with uniform optical properties throughout the volume.
|
|
38
|
+
Supports wavelength-dependent refractive index via Sellmeier or Cauchy models.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from collections.abc import Callable
|
|
44
|
+
from typing import TYPE_CHECKING
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
from numpy.typing import NDArray
|
|
48
|
+
|
|
49
|
+
from .material_field import MaterialField
|
|
50
|
+
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from ...propagation.kernels.registry import PropagatorID
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class HomogeneousMaterial(MaterialField):
|
|
56
|
+
"""
|
|
57
|
+
Homogeneous material with constant optical properties.
|
|
58
|
+
|
|
59
|
+
Properties are uniform throughout space but can depend on wavelength.
|
|
60
|
+
Supports analytic dispersion models (Sellmeier, Cauchy) or custom functions.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
name : str
|
|
65
|
+
Descriptive name for this material.
|
|
66
|
+
refractive_index : float or callable
|
|
67
|
+
Refractive index value or function f(wavelength) -> n.
|
|
68
|
+
If float, uses constant n for all wavelengths.
|
|
69
|
+
If callable, wavelength is in meters.
|
|
70
|
+
absorption_coef : float, optional
|
|
71
|
+
Absorption coefficient α in m⁻¹. Default is 0.
|
|
72
|
+
scattering_coef : float, optional
|
|
73
|
+
Scattering coefficient μ_s in m⁻¹. Default is 0.
|
|
74
|
+
anisotropy : float, optional
|
|
75
|
+
Henyey-Greenstein anisotropy factor g ∈ [-1, 1]. Default is 0.
|
|
76
|
+
|
|
77
|
+
Attributes
|
|
78
|
+
----------
|
|
79
|
+
refractive_index : float or callable
|
|
80
|
+
Refractive index specification.
|
|
81
|
+
absorption_coef : float
|
|
82
|
+
Absorption coefficient in m⁻¹.
|
|
83
|
+
scattering_coef : float
|
|
84
|
+
Scattering coefficient in m⁻¹.
|
|
85
|
+
anisotropy : float
|
|
86
|
+
Scattering anisotropy factor.
|
|
87
|
+
|
|
88
|
+
Examples
|
|
89
|
+
--------
|
|
90
|
+
>>> # Constant refractive index
|
|
91
|
+
>>> glass = HomogeneousMaterial("Glass", 1.5)
|
|
92
|
+
|
|
93
|
+
>>> # Wavelength-dependent (Cauchy dispersion)
|
|
94
|
+
>>> def cauchy_n(wavelength):
|
|
95
|
+
... wl_um = wavelength * 1e6
|
|
96
|
+
... return 1.5 + 0.01 / wl_um**2
|
|
97
|
+
>>> glass = HomogeneousMaterial("Glass", cauchy_n)
|
|
98
|
+
|
|
99
|
+
>>> # With absorption (colored glass)
|
|
100
|
+
>>> colored_glass = HomogeneousMaterial(
|
|
101
|
+
... "Red Glass", 1.52, absorption_coef=0.1
|
|
102
|
+
... )
|
|
103
|
+
|
|
104
|
+
Notes
|
|
105
|
+
-----
|
|
106
|
+
For homogeneous materials:
|
|
107
|
+
- Rays propagate in straight lines (no gradient-driven bending)
|
|
108
|
+
- Reflection/refraction occurs only at interfaces
|
|
109
|
+
- Beer-Lambert absorption: I(d) = I₀ exp(-αd)
|
|
110
|
+
- No GPU kernels are needed (straight-line propagation)
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
# =========================================================================
|
|
114
|
+
# COMPATIBILITY DECLARATIONS
|
|
115
|
+
# =========================================================================
|
|
116
|
+
@classmethod
|
|
117
|
+
def _init_compatibility(cls):
|
|
118
|
+
"""Initialize compatibility declarations (called once on first use)."""
|
|
119
|
+
from ...propagation.kernels.registry import PropagatorID
|
|
120
|
+
|
|
121
|
+
if not cls._supported_propagators:
|
|
122
|
+
# Homogeneous materials don't need GPU kernels - straight-line propagation
|
|
123
|
+
cls._supported_kernels = [] # No GPU kernels needed
|
|
124
|
+
cls._default_kernel = None
|
|
125
|
+
cls._supported_propagators = [PropagatorID.CPU_GRADIENT]
|
|
126
|
+
cls._default_propagator = PropagatorID.CPU_GRADIENT
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
name: str,
|
|
131
|
+
refractive_index: float | Callable[[float], float],
|
|
132
|
+
absorption_coef: float = 0.0,
|
|
133
|
+
scattering_coef: float = 0.0,
|
|
134
|
+
anisotropy: float = 0.0,
|
|
135
|
+
propagator: PropagatorID | None = None,
|
|
136
|
+
):
|
|
137
|
+
"""
|
|
138
|
+
Initialize homogeneous material.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
name : str
|
|
143
|
+
Descriptive name for this material.
|
|
144
|
+
refractive_index : float or callable
|
|
145
|
+
Refractive index value or function f(wavelength) -> n.
|
|
146
|
+
absorption_coef : float, optional
|
|
147
|
+
Absorption coefficient α in m⁻¹. Default is 0.
|
|
148
|
+
scattering_coef : float, optional
|
|
149
|
+
Scattering coefficient μ_s in m⁻¹. Default is 0.
|
|
150
|
+
anisotropy : float, optional
|
|
151
|
+
Henyey-Greenstein anisotropy factor g ∈ [-1, 1]. Default is 0.
|
|
152
|
+
propagator : PropagatorID, optional
|
|
153
|
+
Override the default propagator. Only CPU_GRADIENT is supported.
|
|
154
|
+
|
|
155
|
+
Raises
|
|
156
|
+
------
|
|
157
|
+
ValueError
|
|
158
|
+
If anisotropy is outside [-1, 1].
|
|
159
|
+
"""
|
|
160
|
+
# Initialize compatibility declarations before calling super().__init__
|
|
161
|
+
self._init_compatibility()
|
|
162
|
+
|
|
163
|
+
# Homogeneous materials don't use GPU kernels
|
|
164
|
+
super().__init__(name, kernel=None, propagator=propagator)
|
|
165
|
+
self._is_homogeneous = True
|
|
166
|
+
|
|
167
|
+
# Validate inputs
|
|
168
|
+
if not -1.0 <= anisotropy <= 1.0:
|
|
169
|
+
raise ValueError(f"Anisotropy must be in [-1, 1], got {anisotropy}")
|
|
170
|
+
if absorption_coef < 0:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Absorption coefficient must be >= 0, got {absorption_coef}"
|
|
173
|
+
)
|
|
174
|
+
if scattering_coef < 0:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Scattering coefficient must be >= 0, got {scattering_coef}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Store properties
|
|
180
|
+
self.refractive_index = refractive_index
|
|
181
|
+
self.absorption_coef = absorption_coef
|
|
182
|
+
self.scattering_coef = scattering_coef
|
|
183
|
+
self.anisotropy = anisotropy
|
|
184
|
+
|
|
185
|
+
# Check if n is callable or constant
|
|
186
|
+
self._n_is_callable = callable(refractive_index)
|
|
187
|
+
|
|
188
|
+
def get_refractive_index(
|
|
189
|
+
self,
|
|
190
|
+
x: float | NDArray[np.float64],
|
|
191
|
+
y: float | NDArray[np.float64],
|
|
192
|
+
z: float | NDArray[np.float64],
|
|
193
|
+
wavelength: float | NDArray[np.float64],
|
|
194
|
+
) -> float | NDArray[np.float64]:
|
|
195
|
+
"""
|
|
196
|
+
Get refractive index (position-independent, wavelength-dependent).
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
x, y, z : float or ndarray
|
|
201
|
+
Position coordinates in meters (ignored for homogeneous).
|
|
202
|
+
wavelength : float or ndarray
|
|
203
|
+
Wavelength in meters.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
n : float or ndarray
|
|
208
|
+
Refractive index.
|
|
209
|
+
"""
|
|
210
|
+
if self._n_is_callable:
|
|
211
|
+
if isinstance(wavelength, np.ndarray):
|
|
212
|
+
# Vectorize the callable
|
|
213
|
+
return np.array([self.refractive_index(wl) for wl in wavelength])
|
|
214
|
+
return self.refractive_index(wavelength)
|
|
215
|
+
else:
|
|
216
|
+
if isinstance(x, np.ndarray):
|
|
217
|
+
return np.full_like(x, self.refractive_index)
|
|
218
|
+
return self.refractive_index
|
|
219
|
+
|
|
220
|
+
def get_refractive_index_gradient(
|
|
221
|
+
self,
|
|
222
|
+
x: float | NDArray[np.float64],
|
|
223
|
+
y: float | NDArray[np.float64],
|
|
224
|
+
z: float | NDArray[np.float64],
|
|
225
|
+
wavelength: float | NDArray[np.float64],
|
|
226
|
+
) -> tuple[
|
|
227
|
+
float | NDArray[np.float64],
|
|
228
|
+
float | NDArray[np.float64],
|
|
229
|
+
float | NDArray[np.float64],
|
|
230
|
+
]:
|
|
231
|
+
"""
|
|
232
|
+
Get refractive index gradient (always zero for homogeneous).
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
x, y, z : float or ndarray
|
|
237
|
+
Position coordinates in meters.
|
|
238
|
+
wavelength : float or ndarray
|
|
239
|
+
Wavelength in meters (unused).
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
grad_n : tuple of (float or ndarray)
|
|
244
|
+
(0, 0, 0) since ∇n = 0 for homogeneous materials.
|
|
245
|
+
"""
|
|
246
|
+
if isinstance(x, np.ndarray):
|
|
247
|
+
zeros = np.zeros_like(x)
|
|
248
|
+
return (zeros, zeros, zeros)
|
|
249
|
+
return (0.0, 0.0, 0.0)
|
|
250
|
+
|
|
251
|
+
def get_absorption_coefficient(
|
|
252
|
+
self,
|
|
253
|
+
x: float | NDArray[np.float64],
|
|
254
|
+
y: float | NDArray[np.float64],
|
|
255
|
+
z: float | NDArray[np.float64],
|
|
256
|
+
wavelength: float | NDArray[np.float64],
|
|
257
|
+
) -> float | NDArray[np.float64]:
|
|
258
|
+
"""
|
|
259
|
+
Get absorption coefficient (constant for homogeneous).
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
x, y, z : float or ndarray
|
|
264
|
+
Position coordinates in meters (ignored).
|
|
265
|
+
wavelength : float or ndarray
|
|
266
|
+
Wavelength in meters (currently ignored, could extend).
|
|
267
|
+
|
|
268
|
+
Returns
|
|
269
|
+
-------
|
|
270
|
+
alpha : float or ndarray
|
|
271
|
+
Absorption coefficient in m⁻¹.
|
|
272
|
+
"""
|
|
273
|
+
if isinstance(x, np.ndarray):
|
|
274
|
+
return np.full_like(x, self.absorption_coef)
|
|
275
|
+
return self.absorption_coef
|
|
276
|
+
|
|
277
|
+
def get_scattering_coefficient(
|
|
278
|
+
self,
|
|
279
|
+
x: float | NDArray[np.float64],
|
|
280
|
+
y: float | NDArray[np.float64],
|
|
281
|
+
z: float | NDArray[np.float64],
|
|
282
|
+
wavelength: float | NDArray[np.float64],
|
|
283
|
+
) -> float | NDArray[np.float64]:
|
|
284
|
+
"""
|
|
285
|
+
Get scattering coefficient (constant for homogeneous).
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
x, y, z : float or ndarray
|
|
290
|
+
Position coordinates in meters (ignored).
|
|
291
|
+
wavelength : float or ndarray
|
|
292
|
+
Wavelength in meters (currently ignored, could extend).
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
mu_s : float or ndarray
|
|
297
|
+
Scattering coefficient in m⁻¹.
|
|
298
|
+
"""
|
|
299
|
+
if isinstance(x, np.ndarray):
|
|
300
|
+
return np.full_like(x, self.scattering_coef)
|
|
301
|
+
return self.scattering_coef
|
|
302
|
+
|
|
303
|
+
def get_anisotropy_factor(
|
|
304
|
+
self,
|
|
305
|
+
x: float | NDArray[np.float64],
|
|
306
|
+
y: float | NDArray[np.float64],
|
|
307
|
+
z: float | NDArray[np.float64],
|
|
308
|
+
wavelength: float | NDArray[np.float64],
|
|
309
|
+
) -> float | NDArray[np.float64]:
|
|
310
|
+
"""
|
|
311
|
+
Get scattering anisotropy factor (constant for homogeneous).
|
|
312
|
+
|
|
313
|
+
Parameters
|
|
314
|
+
----------
|
|
315
|
+
x, y, z : float or ndarray
|
|
316
|
+
Position coordinates in meters (ignored).
|
|
317
|
+
wavelength : float or ndarray
|
|
318
|
+
Wavelength in meters (ignored).
|
|
319
|
+
|
|
320
|
+
Returns
|
|
321
|
+
-------
|
|
322
|
+
g : float or ndarray
|
|
323
|
+
Anisotropy factor.
|
|
324
|
+
"""
|
|
325
|
+
if isinstance(x, np.ndarray):
|
|
326
|
+
return np.full_like(x, self.anisotropy)
|
|
327
|
+
return self.anisotropy
|
|
328
|
+
|
|
329
|
+
def __repr__(self) -> str:
|
|
330
|
+
"""Return string representation with key properties."""
|
|
331
|
+
if self._n_is_callable:
|
|
332
|
+
n_str = "n(λ)"
|
|
333
|
+
else:
|
|
334
|
+
n_str = f"n={self.refractive_index:.4f}"
|
|
335
|
+
|
|
336
|
+
props = [n_str]
|
|
337
|
+
if self.absorption_coef > 0:
|
|
338
|
+
props.append(f"α={self.absorption_coef:.2e}")
|
|
339
|
+
if self.scattering_coef > 0:
|
|
340
|
+
props.append(f"μ_s={self.scattering_coef:.2e}")
|
|
341
|
+
|
|
342
|
+
return f"<HomogeneousMaterial('{self.name}', {', '.join(props)})>"
|