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,628 @@
|
|
|
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
|
+
Fresnel Equations
|
|
36
|
+
|
|
37
|
+
Computes reflection and transmission coefficients at interfaces between
|
|
38
|
+
different media using Fresnel equations.
|
|
39
|
+
|
|
40
|
+
Functions
|
|
41
|
+
---------
|
|
42
|
+
fresnel_coefficients
|
|
43
|
+
Compute Fresnel reflection and transmission coefficients
|
|
44
|
+
compute_reflection_direction
|
|
45
|
+
Compute reflected ray direction
|
|
46
|
+
compute_refraction_direction
|
|
47
|
+
Compute refracted ray direction (Snell's law)
|
|
48
|
+
|
|
49
|
+
References
|
|
50
|
+
----------
|
|
51
|
+
.. [1] Born, M., & Wolf, E. (1999). Principles of Optics (7th ed.).
|
|
52
|
+
Cambridge University Press. Chapter 1.5.
|
|
53
|
+
.. [2] Hecht, E. (2017). Optics (5th ed.). Pearson. Chapter 4.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
import numpy as np
|
|
57
|
+
from numpy.typing import NDArray
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def fresnel_coefficients(
|
|
61
|
+
n1: NDArray[np.float32] | float,
|
|
62
|
+
n2: NDArray[np.float32] | float,
|
|
63
|
+
cos_theta_i: NDArray[np.float32],
|
|
64
|
+
polarization: str = "unpolarized",
|
|
65
|
+
) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
|
|
66
|
+
"""
|
|
67
|
+
Compute Fresnel reflection and transmission coefficients.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
n1 : float or ndarray
|
|
72
|
+
Refractive index of incident medium
|
|
73
|
+
n2 : float or ndarray
|
|
74
|
+
Refractive index of transmitted medium
|
|
75
|
+
cos_theta_i : ndarray, shape (N,)
|
|
76
|
+
Cosine of incident angle (dot product of direction and normal)
|
|
77
|
+
polarization : str, optional
|
|
78
|
+
Polarization state: 's', 'p', or 'unpolarized' (default)
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
R : ndarray, shape (N,)
|
|
83
|
+
Reflection coefficient (fraction of intensity reflected)
|
|
84
|
+
T : ndarray, shape (N,)
|
|
85
|
+
Transmission coefficient (fraction of intensity transmitted)
|
|
86
|
+
|
|
87
|
+
Notes
|
|
88
|
+
-----
|
|
89
|
+
For unpolarized light, we average s and p polarizations.
|
|
90
|
+
Total internal reflection occurs when n1 > n2 and angle exceeds critical.
|
|
91
|
+
|
|
92
|
+
The Fresnel equations for intensity (not amplitude) are:
|
|
93
|
+
- s-polarization: R_s = |r_s|², T_s = (n2*cos_theta_t)/(n1*cos_theta_i) * |t_s|²
|
|
94
|
+
- p-polarization: R_p = |r_p|², T_p = (n2*cos_theta_t)/(n1*cos_theta_i) * |t_p|²
|
|
95
|
+
|
|
96
|
+
Examples
|
|
97
|
+
--------
|
|
98
|
+
>>> # Air to glass at 45 degrees
|
|
99
|
+
>>> cos_theta_i = np.cos(np.radians(45))
|
|
100
|
+
>>> R, T = fresnel_coefficients(1.0, 1.5, cos_theta_i)
|
|
101
|
+
>>> print(f"Reflection: {R:.3f}, Transmission: {T:.3f}")
|
|
102
|
+
"""
|
|
103
|
+
# Ensure arrays
|
|
104
|
+
cos_theta_i = np.atleast_1d(cos_theta_i).astype(np.float32)
|
|
105
|
+
n1 = np.atleast_1d(n1).astype(np.float32)
|
|
106
|
+
n2 = np.atleast_1d(n2).astype(np.float32)
|
|
107
|
+
|
|
108
|
+
# Broadcast to same shape
|
|
109
|
+
n_ratio = n1 / n2
|
|
110
|
+
|
|
111
|
+
# Compute cos(theta_t) using Snell's law
|
|
112
|
+
# n1*sin(theta_i) = n2*sin(theta_t)
|
|
113
|
+
# sin²(theta_t) = (n1/n2)² * sin²(theta_i)
|
|
114
|
+
# cos²(theta_t) = 1 - sin²(theta_t)
|
|
115
|
+
|
|
116
|
+
sin_theta_i_sq = 1.0 - cos_theta_i**2
|
|
117
|
+
sin_theta_t_sq = (n_ratio**2) * sin_theta_i_sq
|
|
118
|
+
|
|
119
|
+
# Check for total internal reflection
|
|
120
|
+
tir_mask = sin_theta_t_sq > 1.0
|
|
121
|
+
|
|
122
|
+
# Compute cos(theta_t) for non-TIR cases
|
|
123
|
+
cos_theta_t = np.sqrt(np.clip(1.0 - sin_theta_t_sq, 0, 1))
|
|
124
|
+
|
|
125
|
+
# Fresnel equations for amplitude coefficients
|
|
126
|
+
# s-polarization (TE): Electric field perpendicular to plane of incidence
|
|
127
|
+
r_s_num = n1 * cos_theta_i - n2 * cos_theta_t
|
|
128
|
+
r_s_den = n1 * cos_theta_i + n2 * cos_theta_t
|
|
129
|
+
r_s = r_s_num / (r_s_den + 1e-10) # Avoid division by zero
|
|
130
|
+
|
|
131
|
+
# p-polarization (TM): Electric field parallel to plane of incidence
|
|
132
|
+
r_p_num = n2 * cos_theta_i - n1 * cos_theta_t
|
|
133
|
+
r_p_den = n2 * cos_theta_i + n1 * cos_theta_t
|
|
134
|
+
r_p = r_p_num / (r_p_den + 1e-10)
|
|
135
|
+
|
|
136
|
+
# Intensity reflection coefficients
|
|
137
|
+
R_s = r_s**2
|
|
138
|
+
R_p = r_p**2
|
|
139
|
+
|
|
140
|
+
# Handle total internal reflection
|
|
141
|
+
R_s = np.where(tir_mask, 1.0, R_s)
|
|
142
|
+
R_p = np.where(tir_mask, 1.0, R_p)
|
|
143
|
+
|
|
144
|
+
# Combine based on polarization
|
|
145
|
+
if polarization == "s":
|
|
146
|
+
R = R_s
|
|
147
|
+
elif polarization == "p":
|
|
148
|
+
R = R_p
|
|
149
|
+
else: # unpolarized
|
|
150
|
+
R = 0.5 * (R_s + R_p)
|
|
151
|
+
|
|
152
|
+
# Transmission coefficient (energy conservation)
|
|
153
|
+
T = 1.0 - R
|
|
154
|
+
|
|
155
|
+
# Ensure physical bounds
|
|
156
|
+
R = np.clip(R, 0.0, 1.0)
|
|
157
|
+
T = np.clip(T, 0.0, 1.0)
|
|
158
|
+
|
|
159
|
+
return R.astype(np.float32), T.astype(np.float32)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def compute_reflection_direction(
|
|
163
|
+
incident: NDArray[np.float32],
|
|
164
|
+
normal: NDArray[np.float32],
|
|
165
|
+
) -> NDArray[np.float32]:
|
|
166
|
+
"""
|
|
167
|
+
Compute reflected ray direction using law of reflection.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
incident : ndarray, shape (N, 3)
|
|
172
|
+
Incident ray directions (should be normalized)
|
|
173
|
+
normal : ndarray, shape (N, 3)
|
|
174
|
+
Surface normals at intersection points (should be normalized)
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
reflected : ndarray, shape (N, 3)
|
|
179
|
+
Reflected ray directions (normalized)
|
|
180
|
+
|
|
181
|
+
Notes
|
|
182
|
+
-----
|
|
183
|
+
Reflection formula: r = d - 2(d·n)n
|
|
184
|
+
where d is incident direction and n is surface normal.
|
|
185
|
+
|
|
186
|
+
The normal should point toward the incident side.
|
|
187
|
+
|
|
188
|
+
Examples
|
|
189
|
+
--------
|
|
190
|
+
>>> # Reflect ray at 45° off horizontal surface
|
|
191
|
+
>>> incident = np.array([[1/np.sqrt(2), 0, -1/np.sqrt(2)]])
|
|
192
|
+
>>> normal = np.array([[0, 0, 1]])
|
|
193
|
+
>>> reflected = compute_reflection_direction(incident, normal)
|
|
194
|
+
>>> print(reflected) # Should be [1/√2, 0, 1/√2]
|
|
195
|
+
"""
|
|
196
|
+
# Compute dot product: incident · normal
|
|
197
|
+
dot_in = np.sum(incident * normal, axis=1, keepdims=True)
|
|
198
|
+
|
|
199
|
+
# Reflection formula: r = d - 2(d·n)n
|
|
200
|
+
reflected = incident - 2.0 * dot_in * normal
|
|
201
|
+
|
|
202
|
+
# Normalize (should already be normalized, but ensure it)
|
|
203
|
+
norms = np.linalg.norm(reflected, axis=1, keepdims=True)
|
|
204
|
+
reflected = reflected / (norms + 1e-10)
|
|
205
|
+
|
|
206
|
+
return reflected.astype(np.float32)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def compute_refraction_direction(
|
|
210
|
+
incident: NDArray[np.float32],
|
|
211
|
+
normal: NDArray[np.float32],
|
|
212
|
+
n1: NDArray[np.float32] | float,
|
|
213
|
+
n2: NDArray[np.float32] | float,
|
|
214
|
+
) -> tuple[NDArray[np.float32], NDArray[np.bool_]]:
|
|
215
|
+
"""
|
|
216
|
+
Compute refracted ray direction using Snell's law.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
incident : ndarray, shape (N, 3)
|
|
221
|
+
Incident ray directions (should be normalized)
|
|
222
|
+
normal : ndarray, shape (N, 3)
|
|
223
|
+
Surface normals at intersection points (should be normalized)
|
|
224
|
+
n1 : float or ndarray
|
|
225
|
+
Refractive index of incident medium
|
|
226
|
+
n2 : float or ndarray
|
|
227
|
+
Refractive index of transmitted medium
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
refracted : ndarray, shape (N, 3)
|
|
232
|
+
Refracted ray directions (normalized)
|
|
233
|
+
For TIR cases, returns zero vector
|
|
234
|
+
tir_mask : ndarray, shape (N,)
|
|
235
|
+
Boolean mask indicating total internal reflection
|
|
236
|
+
|
|
237
|
+
Notes
|
|
238
|
+
-----
|
|
239
|
+
Snell's law: n1*sin(θ1) = n2*sin(θ2)
|
|
240
|
+
|
|
241
|
+
Vector form of Snell's law:
|
|
242
|
+
t = (n1/n2)[d - (d·n)n] - n*sqrt(1 - (n1/n2)²*[1-(d·n)²])
|
|
243
|
+
|
|
244
|
+
Total internal reflection occurs when (n1/n2)²*[1-(d·n)²] > 1
|
|
245
|
+
|
|
246
|
+
Examples
|
|
247
|
+
--------
|
|
248
|
+
>>> # Air to glass at normal incidence
|
|
249
|
+
>>> incident = np.array([[0, 0, -1]])
|
|
250
|
+
>>> normal = np.array([[0, 0, 1]])
|
|
251
|
+
>>> refracted, tir = compute_refraction_direction(incident, normal, 1.0, 1.5)
|
|
252
|
+
>>> print(refracted) # Should be [0, 0, -1] (straight through)
|
|
253
|
+
>>> print(tir) # Should be False
|
|
254
|
+
"""
|
|
255
|
+
# Ensure arrays
|
|
256
|
+
n1 = np.atleast_1d(n1).astype(np.float32)
|
|
257
|
+
n2 = np.atleast_1d(n2).astype(np.float32)
|
|
258
|
+
|
|
259
|
+
# Compute cos(theta_i) = -incident · normal
|
|
260
|
+
# (negative because incident and normal point in opposite directions)
|
|
261
|
+
cos_theta_i = -np.sum(incident * normal, axis=1)
|
|
262
|
+
|
|
263
|
+
# Compute n1/n2 ratio
|
|
264
|
+
n_ratio = n1 / n2
|
|
265
|
+
|
|
266
|
+
# Check for total internal reflection
|
|
267
|
+
# sin²(theta_t) = (n1/n2)² * sin²(theta_i) = (n1/n2)² * (1 - cos²(theta_i))
|
|
268
|
+
sin_theta_t_sq = (n_ratio**2) * (1.0 - cos_theta_i**2)
|
|
269
|
+
tir_mask = sin_theta_t_sq > 1.0
|
|
270
|
+
|
|
271
|
+
# Compute cos(theta_t)
|
|
272
|
+
cos_theta_t = np.sqrt(np.clip(1.0 - sin_theta_t_sq, 0, 1))
|
|
273
|
+
|
|
274
|
+
# Vector form of Snell's law
|
|
275
|
+
# t = (n1/n2) * incident + [(n1/n2)*cos(theta_i) - cos(theta_t)] * normal
|
|
276
|
+
refracted = (
|
|
277
|
+
n_ratio[:, np.newaxis] * incident
|
|
278
|
+
+ (n_ratio * cos_theta_i - cos_theta_t)[:, np.newaxis] * normal
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Set TIR rays to zero vector (they won't be transmitted)
|
|
282
|
+
refracted[tir_mask] = 0.0
|
|
283
|
+
|
|
284
|
+
# Normalize
|
|
285
|
+
norms = np.linalg.norm(refracted, axis=1, keepdims=True)
|
|
286
|
+
refracted = np.where(norms > 1e-10, refracted / norms, 0.0)
|
|
287
|
+
|
|
288
|
+
return refracted.astype(np.float32), tir_mask
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def compute_polarization_basis(
|
|
292
|
+
ray_directions: NDArray[np.float32],
|
|
293
|
+
surface_normals: NDArray[np.float32],
|
|
294
|
+
) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
|
|
295
|
+
"""
|
|
296
|
+
Compute local s and p polarization basis vectors at surface intersections.
|
|
297
|
+
|
|
298
|
+
At a surface, the plane of incidence contains both the ray direction and
|
|
299
|
+
the surface normal. The polarization basis is:
|
|
300
|
+
- s-polarization (TE): perpendicular to plane of incidence
|
|
301
|
+
- p-polarization (TM): in plane of incidence, perpendicular to ray direction
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
ray_directions : ndarray, shape (N, 3)
|
|
306
|
+
Ray direction vectors (should be normalized)
|
|
307
|
+
surface_normals : ndarray, shape (N, 3)
|
|
308
|
+
Surface normal vectors (should be normalized)
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
s_vectors : ndarray, shape (N, 3)
|
|
313
|
+
S-polarization unit vectors (perpendicular to plane of incidence)
|
|
314
|
+
p_vectors : ndarray, shape (N, 3)
|
|
315
|
+
P-polarization unit vectors (in plane of incidence, perpendicular to ray)
|
|
316
|
+
|
|
317
|
+
Notes
|
|
318
|
+
-----
|
|
319
|
+
The s-vector is computed as: ŝ = (d × n) / |d × n|
|
|
320
|
+
The p-vector is computed as: p̂ = (d × ŝ) (perpendicular to both ray and s)
|
|
321
|
+
|
|
322
|
+
For rays at normal incidence (d parallel to n), we use a default reference
|
|
323
|
+
direction to define the basis.
|
|
324
|
+
"""
|
|
325
|
+
# Compute s-vector: perpendicular to plane of incidence
|
|
326
|
+
# s = ray_direction × normal
|
|
327
|
+
s_vectors = np.cross(ray_directions, surface_normals)
|
|
328
|
+
s_norms = np.linalg.norm(s_vectors, axis=1, keepdims=True)
|
|
329
|
+
|
|
330
|
+
# Handle degenerate case (normal incidence: ray parallel to normal)
|
|
331
|
+
# Use a default reference direction (global Y or X)
|
|
332
|
+
degenerate_mask = s_norms.squeeze() < 1e-6
|
|
333
|
+
|
|
334
|
+
if np.any(degenerate_mask):
|
|
335
|
+
# For degenerate cases, pick a perpendicular direction
|
|
336
|
+
# Try Y axis first, if parallel to ray, use X
|
|
337
|
+
y_axis = np.array([0, 1, 0], dtype=np.float32)
|
|
338
|
+
x_axis = np.array([1, 0, 0], dtype=np.float32)
|
|
339
|
+
|
|
340
|
+
for i in np.where(degenerate_mask)[0]:
|
|
341
|
+
# Check if Y axis is parallel to ray direction
|
|
342
|
+
if abs(np.dot(ray_directions[i], y_axis)) > 0.99:
|
|
343
|
+
s_vectors[i] = x_axis
|
|
344
|
+
else:
|
|
345
|
+
s_vectors[i] = np.cross(ray_directions[i], y_axis)
|
|
346
|
+
# Recompute norms for degenerate cases
|
|
347
|
+
s_norms[degenerate_mask] = np.linalg.norm(
|
|
348
|
+
s_vectors[degenerate_mask], axis=1, keepdims=True
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Normalize s-vectors
|
|
352
|
+
s_vectors = s_vectors / np.maximum(s_norms, 1e-10)
|
|
353
|
+
|
|
354
|
+
# Compute p-vector: perpendicular to ray direction, in plane of incidence
|
|
355
|
+
# p = ray_direction × s_vector
|
|
356
|
+
p_vectors = np.cross(ray_directions, s_vectors)
|
|
357
|
+
|
|
358
|
+
# Normalize p-vectors
|
|
359
|
+
p_norms = np.linalg.norm(p_vectors, axis=1, keepdims=True)
|
|
360
|
+
p_vectors = p_vectors / np.maximum(p_norms, 1e-10)
|
|
361
|
+
|
|
362
|
+
return s_vectors.astype(np.float32), p_vectors.astype(np.float32)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def transform_polarization_reflection(
|
|
366
|
+
polarization_vectors: NDArray[np.float32],
|
|
367
|
+
incident_directions: NDArray[np.float32],
|
|
368
|
+
reflected_directions: NDArray[np.float32],
|
|
369
|
+
surface_normals: NDArray[np.float32],
|
|
370
|
+
R_s: NDArray[np.float32] | None = None,
|
|
371
|
+
R_p: NDArray[np.float32] | None = None,
|
|
372
|
+
) -> NDArray[np.float32]:
|
|
373
|
+
"""
|
|
374
|
+
Transform polarization vectors through reflection with optional Fresnel weighting.
|
|
375
|
+
|
|
376
|
+
Upon reflection, the s-polarization component (perpendicular to plane of
|
|
377
|
+
incidence) maintains its direction, while the p-polarization component
|
|
378
|
+
(in plane of incidence) has its component along the propagation reversed.
|
|
379
|
+
|
|
380
|
+
When R_s and R_p are provided, the electric field amplitudes are weighted
|
|
381
|
+
by sqrt(R_s) and sqrt(R_p) respectively, causing unpolarized light to
|
|
382
|
+
become partially polarized after reflection (more s-polarized since R_s > R_p
|
|
383
|
+
for most angles).
|
|
384
|
+
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
polarization_vectors : ndarray, shape (N, 3)
|
|
388
|
+
Input polarization vectors (E-field direction, unit vectors)
|
|
389
|
+
incident_directions : ndarray, shape (N, 3)
|
|
390
|
+
Incident ray directions
|
|
391
|
+
reflected_directions : ndarray, shape (N, 3)
|
|
392
|
+
Reflected ray directions
|
|
393
|
+
surface_normals : ndarray, shape (N, 3)
|
|
394
|
+
Surface normal vectors
|
|
395
|
+
R_s : ndarray, shape (N,), optional
|
|
396
|
+
Fresnel reflectance for s-polarization. If provided with R_p,
|
|
397
|
+
applies amplitude weighting sqrt(R_s) to s-component.
|
|
398
|
+
R_p : ndarray, shape (N,), optional
|
|
399
|
+
Fresnel reflectance for p-polarization. If provided with R_s,
|
|
400
|
+
applies amplitude weighting sqrt(R_p) to p-component.
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
reflected_polarization : ndarray, shape (N, 3)
|
|
405
|
+
Polarization vectors after reflection (normalized)
|
|
406
|
+
|
|
407
|
+
Notes
|
|
408
|
+
-----
|
|
409
|
+
The Fresnel weighting works on electric field amplitude:
|
|
410
|
+
- E_s_out = sqrt(R_s) * E_s_in
|
|
411
|
+
- E_p_out = sqrt(R_p) * E_p_in
|
|
412
|
+
|
|
413
|
+
Since intensity I ∝ |E|², this means:
|
|
414
|
+
- I_s_out = R_s * I_s_in
|
|
415
|
+
- I_p_out = R_p * I_p_in
|
|
416
|
+
|
|
417
|
+
For unpolarized light (random E direction), after reflection the light
|
|
418
|
+
becomes partially s-polarized because R_s > R_p (except at normal incidence).
|
|
419
|
+
"""
|
|
420
|
+
# Compute incident s and p basis
|
|
421
|
+
s_inc, p_inc = compute_polarization_basis(incident_directions, surface_normals)
|
|
422
|
+
|
|
423
|
+
# Project input polarization onto s and p components
|
|
424
|
+
E_s = np.sum(polarization_vectors * s_inc, axis=1, keepdims=True)
|
|
425
|
+
E_p = np.sum(polarization_vectors * p_inc, axis=1, keepdims=True)
|
|
426
|
+
|
|
427
|
+
# Apply Fresnel weighting to electric field amplitudes if provided
|
|
428
|
+
if R_s is not None and R_p is not None:
|
|
429
|
+
# Weight by sqrt(R) since E amplitude, not intensity
|
|
430
|
+
E_s = E_s * np.sqrt(R_s[:, np.newaxis])
|
|
431
|
+
E_p = E_p * np.sqrt(R_p[:, np.newaxis])
|
|
432
|
+
|
|
433
|
+
# For reflection:
|
|
434
|
+
# - s-component: same direction (perpendicular to plane of incidence stays same)
|
|
435
|
+
# - p-component: direction changes because ray direction changes
|
|
436
|
+
# Compute reflected s and p basis
|
|
437
|
+
s_refl, p_refl = compute_polarization_basis(reflected_directions, surface_normals)
|
|
438
|
+
|
|
439
|
+
# Reconstruct polarization in reflected basis
|
|
440
|
+
# The s-component direction is preserved
|
|
441
|
+
# The p-component gets a sign flip (the component of E parallel to the
|
|
442
|
+
# interface stays, the perpendicular component flips)
|
|
443
|
+
reflected_polarization = E_s * s_refl + E_p * p_refl
|
|
444
|
+
|
|
445
|
+
# Normalize
|
|
446
|
+
norms = np.linalg.norm(reflected_polarization, axis=1, keepdims=True)
|
|
447
|
+
reflected_polarization = reflected_polarization / np.maximum(norms, 1e-10)
|
|
448
|
+
|
|
449
|
+
return reflected_polarization.astype(np.float32)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def transform_polarization_refraction(
|
|
453
|
+
polarization_vectors: NDArray[np.float32],
|
|
454
|
+
incident_directions: NDArray[np.float32],
|
|
455
|
+
refracted_directions: NDArray[np.float32],
|
|
456
|
+
surface_normals: NDArray[np.float32],
|
|
457
|
+
) -> NDArray[np.float32]:
|
|
458
|
+
"""
|
|
459
|
+
Transform polarization vectors through refraction.
|
|
460
|
+
|
|
461
|
+
Upon refraction, the s-polarization component stays perpendicular to the
|
|
462
|
+
plane of incidence, and the p-polarization component stays in the plane.
|
|
463
|
+
The basis vectors change because the ray direction changes.
|
|
464
|
+
|
|
465
|
+
Parameters
|
|
466
|
+
----------
|
|
467
|
+
polarization_vectors : ndarray, shape (N, 3)
|
|
468
|
+
Input polarization vectors (E-field direction, unit vectors)
|
|
469
|
+
incident_directions : ndarray, shape (N, 3)
|
|
470
|
+
Incident ray directions
|
|
471
|
+
refracted_directions : ndarray, shape (N, 3)
|
|
472
|
+
Refracted ray directions
|
|
473
|
+
surface_normals : ndarray, shape (N, 3)
|
|
474
|
+
Surface normal vectors
|
|
475
|
+
|
|
476
|
+
Returns
|
|
477
|
+
-------
|
|
478
|
+
refracted_polarization : ndarray, shape (N, 3)
|
|
479
|
+
Polarization vectors after refraction (normalized)
|
|
480
|
+
"""
|
|
481
|
+
# Compute incident s and p basis
|
|
482
|
+
s_inc, p_inc = compute_polarization_basis(incident_directions, surface_normals)
|
|
483
|
+
|
|
484
|
+
# Project input polarization onto s and p components
|
|
485
|
+
E_s = np.sum(polarization_vectors * s_inc, axis=1, keepdims=True)
|
|
486
|
+
E_p = np.sum(polarization_vectors * p_inc, axis=1, keepdims=True)
|
|
487
|
+
|
|
488
|
+
# Compute refracted s and p basis
|
|
489
|
+
s_refr, p_refr = compute_polarization_basis(refracted_directions, surface_normals)
|
|
490
|
+
|
|
491
|
+
# Reconstruct polarization in refracted basis
|
|
492
|
+
# Both components maintain their character (s stays s, p stays p)
|
|
493
|
+
refracted_polarization = E_s * s_refr + E_p * p_refr
|
|
494
|
+
|
|
495
|
+
# Normalize
|
|
496
|
+
norms = np.linalg.norm(refracted_polarization, axis=1, keepdims=True)
|
|
497
|
+
refracted_polarization = refracted_polarization / np.maximum(norms, 1e-10)
|
|
498
|
+
|
|
499
|
+
return refracted_polarization.astype(np.float32)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def initialize_polarization_vectors(
|
|
503
|
+
ray_directions: NDArray[np.float32],
|
|
504
|
+
polarization: str = "unpolarized",
|
|
505
|
+
reference_direction: NDArray[np.float32] = None,
|
|
506
|
+
) -> NDArray[np.float32]:
|
|
507
|
+
"""
|
|
508
|
+
Initialize polarization vectors for rays.
|
|
509
|
+
|
|
510
|
+
Parameters
|
|
511
|
+
----------
|
|
512
|
+
ray_directions : ndarray, shape (N, 3)
|
|
513
|
+
Ray direction vectors (should be normalized)
|
|
514
|
+
polarization : str, optional
|
|
515
|
+
Initial polarization state:
|
|
516
|
+
- 'unpolarized' or 'random': random polarization perpendicular to ray
|
|
517
|
+
- 's' or 'horizontal': horizontal polarization (perpendicular to vertical plane)
|
|
518
|
+
- 'p' or 'vertical': vertical polarization (in vertical plane)
|
|
519
|
+
- 'custom': use reference_direction projected onto plane perpendicular to ray
|
|
520
|
+
reference_direction : ndarray, shape (3,), optional
|
|
521
|
+
Reference direction for 'custom' polarization. Will be projected onto
|
|
522
|
+
the plane perpendicular to each ray.
|
|
523
|
+
|
|
524
|
+
Returns
|
|
525
|
+
-------
|
|
526
|
+
polarization_vectors : ndarray, shape (N, 3)
|
|
527
|
+
Initial polarization vectors (unit vectors perpendicular to ray directions)
|
|
528
|
+
"""
|
|
529
|
+
n_rays = len(ray_directions)
|
|
530
|
+
|
|
531
|
+
if polarization in ["s", "horizontal"]:
|
|
532
|
+
# S-polarization: perpendicular to vertical (Z-containing) plane
|
|
533
|
+
# Use global Z as reference to define "horizontal"
|
|
534
|
+
z_axis = np.array([0, 0, 1], dtype=np.float32)
|
|
535
|
+
|
|
536
|
+
# s = ray × Z (horizontal direction perpendicular to ray)
|
|
537
|
+
pol_vectors = np.cross(ray_directions, z_axis)
|
|
538
|
+
norms = np.linalg.norm(pol_vectors, axis=1, keepdims=True)
|
|
539
|
+
|
|
540
|
+
# Handle rays parallel to Z
|
|
541
|
+
parallel_mask = norms.squeeze() < 1e-6
|
|
542
|
+
if np.any(parallel_mask):
|
|
543
|
+
# For vertical rays, use X as horizontal direction
|
|
544
|
+
pol_vectors[parallel_mask] = np.array([1, 0, 0], dtype=np.float32)
|
|
545
|
+
norms[parallel_mask] = 1.0
|
|
546
|
+
|
|
547
|
+
pol_vectors = pol_vectors / np.maximum(norms, 1e-10)
|
|
548
|
+
|
|
549
|
+
elif polarization in ["p", "vertical"]:
|
|
550
|
+
# P-polarization: in vertical plane containing the ray
|
|
551
|
+
# First get horizontal direction, then p = ray × horizontal
|
|
552
|
+
z_axis = np.array([0, 0, 1], dtype=np.float32)
|
|
553
|
+
horizontal = np.cross(ray_directions, z_axis)
|
|
554
|
+
h_norms = np.linalg.norm(horizontal, axis=1, keepdims=True)
|
|
555
|
+
|
|
556
|
+
# Handle rays parallel to Z
|
|
557
|
+
parallel_mask = h_norms.squeeze() < 1e-6
|
|
558
|
+
if np.any(parallel_mask):
|
|
559
|
+
horizontal[parallel_mask] = np.array([1, 0, 0], dtype=np.float32)
|
|
560
|
+
h_norms[parallel_mask] = 1.0
|
|
561
|
+
|
|
562
|
+
horizontal = horizontal / np.maximum(h_norms, 1e-10)
|
|
563
|
+
|
|
564
|
+
# p = ray × horizontal (vertical component perpendicular to ray)
|
|
565
|
+
pol_vectors = np.cross(ray_directions, horizontal)
|
|
566
|
+
norms = np.linalg.norm(pol_vectors, axis=1, keepdims=True)
|
|
567
|
+
pol_vectors = pol_vectors / np.maximum(norms, 1e-10)
|
|
568
|
+
|
|
569
|
+
elif polarization == "custom" and reference_direction is not None:
|
|
570
|
+
# Project reference direction onto plane perpendicular to each ray
|
|
571
|
+
ref = np.array(reference_direction, dtype=np.float32)
|
|
572
|
+
ref = ref / np.linalg.norm(ref)
|
|
573
|
+
|
|
574
|
+
# For each ray, project ref onto plane perpendicular to ray
|
|
575
|
+
# proj = ref - (ref · ray) * ray
|
|
576
|
+
dots = np.sum(ray_directions * ref, axis=1, keepdims=True)
|
|
577
|
+
pol_vectors = ref - dots * ray_directions
|
|
578
|
+
norms = np.linalg.norm(pol_vectors, axis=1, keepdims=True)
|
|
579
|
+
|
|
580
|
+
# Handle rays parallel to reference direction
|
|
581
|
+
parallel_mask = norms.squeeze() < 1e-6
|
|
582
|
+
if np.any(parallel_mask):
|
|
583
|
+
# Fall back to arbitrary perpendicular
|
|
584
|
+
y_axis = np.array([0, 1, 0], dtype=np.float32)
|
|
585
|
+
fallback = np.cross(ray_directions[parallel_mask], y_axis)
|
|
586
|
+
fallback_norms = np.linalg.norm(fallback, axis=1, keepdims=True)
|
|
587
|
+
# If still degenerate, use X
|
|
588
|
+
still_degen = fallback_norms.squeeze() < 1e-6
|
|
589
|
+
if np.any(still_degen):
|
|
590
|
+
x_axis = np.array([1, 0, 0], dtype=np.float32)
|
|
591
|
+
fallback[still_degen] = np.cross(
|
|
592
|
+
ray_directions[parallel_mask][still_degen], x_axis
|
|
593
|
+
)
|
|
594
|
+
fallback_norms[still_degen] = np.linalg.norm(
|
|
595
|
+
fallback[still_degen], axis=1, keepdims=True
|
|
596
|
+
)
|
|
597
|
+
pol_vectors[parallel_mask] = fallback / np.maximum(fallback_norms, 1e-10)
|
|
598
|
+
norms[parallel_mask] = 1.0
|
|
599
|
+
|
|
600
|
+
pol_vectors = pol_vectors / np.maximum(norms, 1e-10)
|
|
601
|
+
|
|
602
|
+
else: # unpolarized or random
|
|
603
|
+
# Generate random polarization perpendicular to each ray
|
|
604
|
+
# First generate random vectors
|
|
605
|
+
rng = np.random.default_rng()
|
|
606
|
+
random_vecs = rng.standard_normal((n_rays, 3)).astype(np.float32)
|
|
607
|
+
|
|
608
|
+
# Project onto plane perpendicular to ray
|
|
609
|
+
dots = np.sum(ray_directions * random_vecs, axis=1, keepdims=True)
|
|
610
|
+
pol_vectors = random_vecs - dots * ray_directions
|
|
611
|
+
norms = np.linalg.norm(pol_vectors, axis=1, keepdims=True)
|
|
612
|
+
|
|
613
|
+
# Handle any degenerate cases
|
|
614
|
+
degen_mask = norms.squeeze() < 1e-6
|
|
615
|
+
if np.any(degen_mask):
|
|
616
|
+
# Try again with different random vectors
|
|
617
|
+
new_random = rng.standard_normal((np.sum(degen_mask), 3)).astype(np.float32)
|
|
618
|
+
dots_new = np.sum(
|
|
619
|
+
ray_directions[degen_mask] * new_random, axis=1, keepdims=True
|
|
620
|
+
)
|
|
621
|
+
pol_vectors[degen_mask] = new_random - dots_new * ray_directions[degen_mask]
|
|
622
|
+
norms[degen_mask] = np.linalg.norm(
|
|
623
|
+
pol_vectors[degen_mask], axis=1, keepdims=True
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
pol_vectors = pol_vectors / np.maximum(norms, 1e-10)
|
|
627
|
+
|
|
628
|
+
return pol_vectors.astype(np.float32)
|