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,1061 @@
|
|
|
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 Reflection Visualization
|
|
36
|
+
|
|
37
|
+
Functions for plotting Fresnel reflection curves, Brewster angle validation,
|
|
38
|
+
and related optical analysis.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from typing import TYPE_CHECKING, Optional
|
|
42
|
+
|
|
43
|
+
import matplotlib.pyplot as plt
|
|
44
|
+
import numpy as np
|
|
45
|
+
from matplotlib.figure import Figure
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from ..utilities.ray_data import RayBatch
|
|
49
|
+
|
|
50
|
+
from .common import save_figure
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def plot_fresnel_reflection(
|
|
54
|
+
incident_rays: "RayBatch",
|
|
55
|
+
reflected_rays: "RayBatch",
|
|
56
|
+
refracted_rays: Optional["RayBatch"],
|
|
57
|
+
surface_normal: tuple[float, float, float],
|
|
58
|
+
surface_angle_deg: float,
|
|
59
|
+
n1: float,
|
|
60
|
+
n2: float,
|
|
61
|
+
wavelength: float,
|
|
62
|
+
fresnel_func: callable | None = None,
|
|
63
|
+
figsize: tuple[float, float] = (16, 10),
|
|
64
|
+
save_path: str | None = None,
|
|
65
|
+
) -> Figure:
|
|
66
|
+
"""
|
|
67
|
+
Create comprehensive Fresnel reflection visualization.
|
|
68
|
+
|
|
69
|
+
Shows ray paths, intensity distributions, Fresnel curves, and energy summary.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
incident_rays : RayBatch
|
|
74
|
+
Original incident rays.
|
|
75
|
+
reflected_rays : RayBatch
|
|
76
|
+
Reflected rays from surface interaction.
|
|
77
|
+
refracted_rays : RayBatch, optional
|
|
78
|
+
Refracted (transmitted) rays.
|
|
79
|
+
surface_normal : tuple
|
|
80
|
+
Surface normal vector (nx, ny, nz).
|
|
81
|
+
surface_angle_deg : float
|
|
82
|
+
Surface tilt angle in degrees.
|
|
83
|
+
n1 : float
|
|
84
|
+
Refractive index of incident medium.
|
|
85
|
+
n2 : float
|
|
86
|
+
Refractive index of transmission medium.
|
|
87
|
+
wavelength : float
|
|
88
|
+
Optical wavelength in meters.
|
|
89
|
+
fresnel_func : callable, optional
|
|
90
|
+
Function to compute Fresnel coefficients: fresnel_func(n1, n2, cos_theta, pol).
|
|
91
|
+
If not provided, uses surface_roughness.utilities.fresnel.fresnel_coefficients.
|
|
92
|
+
figsize : tuple
|
|
93
|
+
Figure size.
|
|
94
|
+
save_path : str, optional
|
|
95
|
+
Path to save figure.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
Figure
|
|
100
|
+
Matplotlib figure with 6 subplots.
|
|
101
|
+
"""
|
|
102
|
+
# Import fresnel function if not provided
|
|
103
|
+
if fresnel_func is None:
|
|
104
|
+
from ..utilities.fresnel import fresnel_coefficients
|
|
105
|
+
|
|
106
|
+
fresnel_func = fresnel_coefficients
|
|
107
|
+
|
|
108
|
+
normal = np.array(surface_normal)
|
|
109
|
+
angle_rad = np.radians(surface_angle_deg)
|
|
110
|
+
|
|
111
|
+
# Calculate powers
|
|
112
|
+
num_rays = len(incident_rays.positions)
|
|
113
|
+
incident_power = np.sum(incident_rays.intensities)
|
|
114
|
+
|
|
115
|
+
num_reflected = np.sum(reflected_rays.active) if reflected_rays else 0
|
|
116
|
+
reflected_power = (
|
|
117
|
+
np.sum(reflected_rays.intensities[reflected_rays.active])
|
|
118
|
+
if reflected_rays
|
|
119
|
+
else 0
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
num_refracted = np.sum(refracted_rays.active) if refracted_rays else 0
|
|
123
|
+
refracted_power = (
|
|
124
|
+
np.sum(refracted_rays.intensities[refracted_rays.active])
|
|
125
|
+
if refracted_rays
|
|
126
|
+
else 0
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
total_output = reflected_power + refracted_power
|
|
130
|
+
|
|
131
|
+
# Compute Fresnel coefficients at this angle
|
|
132
|
+
cos_theta = np.cos(angle_rad)
|
|
133
|
+
R_unpol, T_unpol = fresnel_func(n1, n2, cos_theta, "unpolarized")
|
|
134
|
+
R_s, T_s = fresnel_func(n1, n2, cos_theta, "s")
|
|
135
|
+
R_p, T_p = fresnel_func(n1, n2, cos_theta, "p")
|
|
136
|
+
|
|
137
|
+
# Convert to scalar if needed
|
|
138
|
+
R_unpol = float(R_unpol[0]) if hasattr(R_unpol, "__len__") else float(R_unpol)
|
|
139
|
+
T_unpol = float(T_unpol[0]) if hasattr(T_unpol, "__len__") else float(T_unpol)
|
|
140
|
+
R_s = float(R_s[0]) if hasattr(R_s, "__len__") else float(R_s)
|
|
141
|
+
R_p = float(R_p[0]) if hasattr(R_p, "__len__") else float(R_p)
|
|
142
|
+
|
|
143
|
+
# Create figure
|
|
144
|
+
fig = plt.figure(figsize=figsize, constrained_layout=True)
|
|
145
|
+
gs = fig.add_gridspec(2, 3)
|
|
146
|
+
|
|
147
|
+
# 1. Side view (XZ plane)
|
|
148
|
+
ax1 = fig.add_subplot(gs[0, 0])
|
|
149
|
+
ax1.set_title("Side View (XZ Plane)", fontweight="bold")
|
|
150
|
+
|
|
151
|
+
# Plot incident rays
|
|
152
|
+
ax1.scatter(
|
|
153
|
+
incident_rays.positions[:, 0] * 1e3,
|
|
154
|
+
incident_rays.positions[:, 2] * 1e3,
|
|
155
|
+
c="blue",
|
|
156
|
+
s=5,
|
|
157
|
+
alpha=0.5,
|
|
158
|
+
label="Incident",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Plot reflected rays
|
|
162
|
+
if reflected_rays and num_reflected > 0:
|
|
163
|
+
ax1.scatter(
|
|
164
|
+
reflected_rays.positions[:, 0] * 1e3,
|
|
165
|
+
reflected_rays.positions[:, 2] * 1e3,
|
|
166
|
+
c="red",
|
|
167
|
+
s=5,
|
|
168
|
+
alpha=0.5,
|
|
169
|
+
label="Reflected",
|
|
170
|
+
)
|
|
171
|
+
# Show ray direction arrows
|
|
172
|
+
for i in range(0, min(50, len(reflected_rays.positions)), 5):
|
|
173
|
+
if reflected_rays.active[i]:
|
|
174
|
+
start = reflected_rays.positions[i]
|
|
175
|
+
end = start + 0.05 * reflected_rays.directions[i]
|
|
176
|
+
ax1.plot(
|
|
177
|
+
[start[0] * 1e3, end[0] * 1e3],
|
|
178
|
+
[start[2] * 1e3, end[2] * 1e3],
|
|
179
|
+
"r-",
|
|
180
|
+
alpha=0.3,
|
|
181
|
+
linewidth=0.5,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Plot refracted rays
|
|
185
|
+
if refracted_rays and num_refracted > 0:
|
|
186
|
+
ax1.scatter(
|
|
187
|
+
refracted_rays.positions[:, 0] * 1e3,
|
|
188
|
+
refracted_rays.positions[:, 2] * 1e3,
|
|
189
|
+
c="green",
|
|
190
|
+
s=5,
|
|
191
|
+
alpha=0.5,
|
|
192
|
+
label="Refracted",
|
|
193
|
+
)
|
|
194
|
+
for i in range(0, min(50, len(refracted_rays.positions)), 5):
|
|
195
|
+
if refracted_rays.active[i]:
|
|
196
|
+
start = refracted_rays.positions[i]
|
|
197
|
+
end = start + 0.05 * refracted_rays.directions[i]
|
|
198
|
+
ax1.plot(
|
|
199
|
+
[start[0] * 1e3, end[0] * 1e3],
|
|
200
|
+
[start[2] * 1e3, end[2] * 1e3],
|
|
201
|
+
"g-",
|
|
202
|
+
alpha=0.3,
|
|
203
|
+
linewidth=0.5,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Draw surface line
|
|
207
|
+
x_range = np.linspace(-20, 20, 100)
|
|
208
|
+
z_surface = -x_range * np.tan(angle_rad)
|
|
209
|
+
ax1.plot(x_range, z_surface, "k-", linewidth=2, label="Surface")
|
|
210
|
+
|
|
211
|
+
# Draw normal vector
|
|
212
|
+
normal_end = 15e-3 * normal
|
|
213
|
+
ax1.arrow(
|
|
214
|
+
0,
|
|
215
|
+
0,
|
|
216
|
+
normal_end[0] * 1e3,
|
|
217
|
+
normal_end[2] * 1e3,
|
|
218
|
+
head_width=2,
|
|
219
|
+
head_length=2,
|
|
220
|
+
fc="black",
|
|
221
|
+
ec="black",
|
|
222
|
+
linewidth=2,
|
|
223
|
+
)
|
|
224
|
+
ax1.text(
|
|
225
|
+
normal_end[0] * 1e3 + 2,
|
|
226
|
+
normal_end[2] * 1e3 + 2,
|
|
227
|
+
"n",
|
|
228
|
+
fontsize=12,
|
|
229
|
+
fontweight="bold",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
ax1.set_xlabel("X (mm)")
|
|
233
|
+
ax1.set_ylabel("Z (mm)")
|
|
234
|
+
ax1.legend()
|
|
235
|
+
ax1.grid(True, alpha=0.3)
|
|
236
|
+
ax1.set_aspect("equal")
|
|
237
|
+
ax1.set_xlim(-15, 15)
|
|
238
|
+
ax1.set_ylim(-60, 20)
|
|
239
|
+
|
|
240
|
+
# 2. Top view (XY plane)
|
|
241
|
+
ax2 = fig.add_subplot(gs[0, 1])
|
|
242
|
+
ax2.set_title("Top View (XY Plane)", fontweight="bold")
|
|
243
|
+
|
|
244
|
+
if reflected_rays and num_reflected > 0:
|
|
245
|
+
scatter = ax2.scatter(
|
|
246
|
+
reflected_rays.positions[:, 0] * 1e3,
|
|
247
|
+
reflected_rays.positions[:, 1] * 1e3,
|
|
248
|
+
c=reflected_rays.intensities,
|
|
249
|
+
s=20,
|
|
250
|
+
cmap="hot",
|
|
251
|
+
alpha=0.6,
|
|
252
|
+
)
|
|
253
|
+
plt.colorbar(scatter, ax=ax2, label="Intensity")
|
|
254
|
+
ax2.axhline(0, color="k", linewidth=2, label="Surface")
|
|
255
|
+
ax2.set_xlabel("X (mm)")
|
|
256
|
+
ax2.set_ylabel("Y (mm)")
|
|
257
|
+
ax2.legend()
|
|
258
|
+
ax2.grid(True, alpha=0.3)
|
|
259
|
+
ax2.set_aspect("equal")
|
|
260
|
+
|
|
261
|
+
# 3. Intensity distribution
|
|
262
|
+
ax3 = fig.add_subplot(gs[0, 2])
|
|
263
|
+
ax3.set_title("Intensity Distribution", fontweight="bold")
|
|
264
|
+
|
|
265
|
+
if reflected_rays and num_reflected > 0:
|
|
266
|
+
intensities_reflected = reflected_rays.intensities[reflected_rays.active]
|
|
267
|
+
ax3.hist(
|
|
268
|
+
intensities_reflected, bins=30, alpha=0.7, color="red", label="Reflected"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if refracted_rays and num_refracted > 0:
|
|
272
|
+
intensities_refracted = refracted_rays.intensities[refracted_rays.active]
|
|
273
|
+
ax3.hist(
|
|
274
|
+
intensities_refracted, bins=30, alpha=0.7, color="green", label="Refracted"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
ax3.set_xlabel("Intensity")
|
|
278
|
+
ax3.set_ylabel("Count")
|
|
279
|
+
ax3.legend()
|
|
280
|
+
ax3.grid(True, alpha=0.3)
|
|
281
|
+
|
|
282
|
+
# 4. Fresnel reflection curves
|
|
283
|
+
ax4 = fig.add_subplot(gs[1, 0])
|
|
284
|
+
ax4.set_title("Fresnel Reflection vs Angle", fontweight="bold")
|
|
285
|
+
|
|
286
|
+
angles = np.linspace(0, 90, 200)
|
|
287
|
+
cos_angles = np.cos(np.radians(angles))
|
|
288
|
+
|
|
289
|
+
R_unpol_curve = []
|
|
290
|
+
R_s_curve = []
|
|
291
|
+
R_p_curve = []
|
|
292
|
+
|
|
293
|
+
for cos_th in cos_angles:
|
|
294
|
+
R_u, _ = fresnel_func(n1, n2, cos_th, "unpolarized")
|
|
295
|
+
R_s_val, _ = fresnel_func(n1, n2, cos_th, "s")
|
|
296
|
+
R_p_val, _ = fresnel_func(n1, n2, cos_th, "p")
|
|
297
|
+
R_unpol_curve.append(float(R_u[0]) if hasattr(R_u, "__len__") else float(R_u))
|
|
298
|
+
R_s_curve.append(
|
|
299
|
+
float(R_s_val[0]) if hasattr(R_s_val, "__len__") else float(R_s_val)
|
|
300
|
+
)
|
|
301
|
+
R_p_curve.append(
|
|
302
|
+
float(R_p_val[0]) if hasattr(R_p_val, "__len__") else float(R_p_val)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
ax4.plot(angles, R_unpol_curve, "b-", label="Unpolarized", linewidth=2)
|
|
306
|
+
ax4.plot(angles, R_s_curve, "r--", label="s-polarization", linewidth=2)
|
|
307
|
+
ax4.plot(angles, R_p_curve, "g--", label="p-polarization", linewidth=2)
|
|
308
|
+
ax4.axvline(
|
|
309
|
+
surface_angle_deg,
|
|
310
|
+
color="orange",
|
|
311
|
+
linestyle=":",
|
|
312
|
+
linewidth=2,
|
|
313
|
+
label=f"Current ({surface_angle_deg}°)",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
brewster_angle = np.degrees(np.arctan(n2 / n1))
|
|
317
|
+
ax4.axvline(
|
|
318
|
+
brewster_angle,
|
|
319
|
+
color="purple",
|
|
320
|
+
linestyle=":",
|
|
321
|
+
linewidth=2,
|
|
322
|
+
label=f"Brewster ({brewster_angle:.1f}°)",
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
ax4.set_xlabel("Angle of Incidence (°)")
|
|
326
|
+
ax4.set_ylabel("Reflectance R")
|
|
327
|
+
ax4.legend()
|
|
328
|
+
ax4.grid(True, alpha=0.3)
|
|
329
|
+
ax4.set_xlim(0, 90)
|
|
330
|
+
ax4.set_ylim(0, 1)
|
|
331
|
+
|
|
332
|
+
# 5. Direction vectors
|
|
333
|
+
ax5 = fig.add_subplot(gs[1, 1])
|
|
334
|
+
ax5.set_title("Ray Directions (unit vectors)", fontweight="bold")
|
|
335
|
+
|
|
336
|
+
sample_indices = np.random.choice(num_rays, min(100, num_rays), replace=False)
|
|
337
|
+
|
|
338
|
+
# Incident
|
|
339
|
+
ax5.scatter(
|
|
340
|
+
incident_rays.directions[sample_indices, 0],
|
|
341
|
+
incident_rays.directions[sample_indices, 2],
|
|
342
|
+
c="blue",
|
|
343
|
+
s=20,
|
|
344
|
+
alpha=0.5,
|
|
345
|
+
label="Incident",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Reflected
|
|
349
|
+
if reflected_rays and num_reflected > 0:
|
|
350
|
+
n_sample = min(100, len(reflected_rays.directions))
|
|
351
|
+
sample_reflected = np.random.choice(
|
|
352
|
+
len(reflected_rays.directions), n_sample, replace=False
|
|
353
|
+
)
|
|
354
|
+
ax5.scatter(
|
|
355
|
+
reflected_rays.directions[sample_reflected, 0],
|
|
356
|
+
reflected_rays.directions[sample_reflected, 2],
|
|
357
|
+
c="red",
|
|
358
|
+
s=20,
|
|
359
|
+
alpha=0.5,
|
|
360
|
+
label="Reflected",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Refracted
|
|
364
|
+
if refracted_rays and num_refracted > 0:
|
|
365
|
+
n_sample = min(100, len(refracted_rays.directions))
|
|
366
|
+
sample_refracted = np.random.choice(
|
|
367
|
+
len(refracted_rays.directions), n_sample, replace=False
|
|
368
|
+
)
|
|
369
|
+
ax5.scatter(
|
|
370
|
+
refracted_rays.directions[sample_refracted, 0],
|
|
371
|
+
refracted_rays.directions[sample_refracted, 2],
|
|
372
|
+
c="green",
|
|
373
|
+
s=20,
|
|
374
|
+
alpha=0.5,
|
|
375
|
+
label="Refracted",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Surface normal
|
|
379
|
+
ax5.arrow(
|
|
380
|
+
0,
|
|
381
|
+
0,
|
|
382
|
+
normal[0],
|
|
383
|
+
normal[2],
|
|
384
|
+
head_width=0.1,
|
|
385
|
+
head_length=0.1,
|
|
386
|
+
fc="black",
|
|
387
|
+
ec="black",
|
|
388
|
+
linewidth=2,
|
|
389
|
+
)
|
|
390
|
+
ax5.text(normal[0] + 0.1, normal[2] + 0.1, "n", fontsize=12, fontweight="bold")
|
|
391
|
+
|
|
392
|
+
ax5.set_xlabel("Direction X")
|
|
393
|
+
ax5.set_ylabel("Direction Z")
|
|
394
|
+
ax5.legend()
|
|
395
|
+
ax5.grid(True, alpha=0.3)
|
|
396
|
+
ax5.set_aspect("equal")
|
|
397
|
+
ax5.set_xlim(-1, 1)
|
|
398
|
+
ax5.set_ylim(-1, 1)
|
|
399
|
+
|
|
400
|
+
# 6. Power summary
|
|
401
|
+
ax6 = fig.add_subplot(gs[1, 2])
|
|
402
|
+
ax6.axis("off")
|
|
403
|
+
|
|
404
|
+
measured_R = reflected_power / total_output if total_output > 0 else 0
|
|
405
|
+
energy_error = (
|
|
406
|
+
abs(incident_power - total_output) / incident_power * 100
|
|
407
|
+
if incident_power > 0
|
|
408
|
+
else 0
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
summary_text = f"""
|
|
412
|
+
SUMMARY
|
|
413
|
+
═══════════════════════════
|
|
414
|
+
|
|
415
|
+
Incident Beam:
|
|
416
|
+
• Power: {incident_power:.4f} W
|
|
417
|
+
• Wavelength: {wavelength*1e9:.0f} nm
|
|
418
|
+
• Rays: {num_rays:,}
|
|
419
|
+
|
|
420
|
+
Surface:
|
|
421
|
+
• n₁ = {n1:.4f}
|
|
422
|
+
• n₂ = {n2:.4f}
|
|
423
|
+
• Angle: {surface_angle_deg}°
|
|
424
|
+
|
|
425
|
+
Fresnel Theory:
|
|
426
|
+
• R (unpol): {R_unpol:.4f}
|
|
427
|
+
• T (unpol): {T_unpol:.4f}
|
|
428
|
+
|
|
429
|
+
Simulation Results:
|
|
430
|
+
• Reflected: {reflected_power:.4f} W
|
|
431
|
+
• Refracted: {refracted_power:.4f} W
|
|
432
|
+
• R measured: {measured_R:.4f}
|
|
433
|
+
|
|
434
|
+
Energy Conservation:
|
|
435
|
+
• Total: {total_output:.4f} W
|
|
436
|
+
• Error: {energy_error:.2f}%
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
ax6.text(
|
|
440
|
+
0.1,
|
|
441
|
+
0.95,
|
|
442
|
+
summary_text,
|
|
443
|
+
transform=ax6.transAxes,
|
|
444
|
+
fontsize=10,
|
|
445
|
+
verticalalignment="top",
|
|
446
|
+
fontfamily="monospace",
|
|
447
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Main title
|
|
451
|
+
fig.suptitle(
|
|
452
|
+
f"Fresnel Reflection Analysis at {surface_angle_deg}°",
|
|
453
|
+
fontsize=16,
|
|
454
|
+
fontweight="bold",
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if save_path:
|
|
458
|
+
save_figure(fig, save_path)
|
|
459
|
+
|
|
460
|
+
return fig
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _plot_brewster_main_comparison(
|
|
464
|
+
ax,
|
|
465
|
+
angles_theory_deg: np.ndarray,
|
|
466
|
+
R_s_theory: np.ndarray,
|
|
467
|
+
R_p_theory: np.ndarray,
|
|
468
|
+
angles_sim_deg: np.ndarray,
|
|
469
|
+
R_s_sim: np.ndarray,
|
|
470
|
+
R_p_sim: np.ndarray,
|
|
471
|
+
n1: float,
|
|
472
|
+
n2: float,
|
|
473
|
+
brewster_angle_deg: float,
|
|
474
|
+
brewster_sim_deg: float,
|
|
475
|
+
measured_brewster_deg: float,
|
|
476
|
+
min_theory_reflection: float,
|
|
477
|
+
min_sim_reflection: float,
|
|
478
|
+
) -> None:
|
|
479
|
+
"""
|
|
480
|
+
Plot main Fresnel reflection comparison (theory vs simulation).
|
|
481
|
+
|
|
482
|
+
Parameters
|
|
483
|
+
----------
|
|
484
|
+
ax : matplotlib.axes.Axes
|
|
485
|
+
Axes to plot on.
|
|
486
|
+
angles_theory_deg : ndarray
|
|
487
|
+
Theory angle array in degrees.
|
|
488
|
+
R_s_theory : ndarray
|
|
489
|
+
Theoretical s-polarization reflectance.
|
|
490
|
+
R_p_theory : ndarray
|
|
491
|
+
Theoretical p-polarization reflectance.
|
|
492
|
+
angles_sim_deg : ndarray
|
|
493
|
+
Simulation angle array in degrees.
|
|
494
|
+
R_s_sim : ndarray
|
|
495
|
+
Simulated s-polarization reflectance.
|
|
496
|
+
R_p_sim : ndarray
|
|
497
|
+
Simulated p-polarization reflectance.
|
|
498
|
+
n1 : float
|
|
499
|
+
Refractive index of incident medium.
|
|
500
|
+
n2 : float
|
|
501
|
+
Refractive index of transmission medium.
|
|
502
|
+
brewster_angle_deg : float
|
|
503
|
+
Theoretical Brewster angle in degrees.
|
|
504
|
+
brewster_sim_deg : float
|
|
505
|
+
Simulated Brewster angle in degrees.
|
|
506
|
+
measured_brewster_deg : float
|
|
507
|
+
Measured Brewster angle from theory minimum.
|
|
508
|
+
min_theory_reflection : float
|
|
509
|
+
Minimum theoretical R_p value.
|
|
510
|
+
min_sim_reflection : float
|
|
511
|
+
Minimum simulated R_p value.
|
|
512
|
+
"""
|
|
513
|
+
# Theory curves
|
|
514
|
+
ax.plot(
|
|
515
|
+
angles_theory_deg,
|
|
516
|
+
R_s_theory,
|
|
517
|
+
"r-",
|
|
518
|
+
linewidth=2.5,
|
|
519
|
+
alpha=0.7,
|
|
520
|
+
label="Theory: s-pol (TE)",
|
|
521
|
+
)
|
|
522
|
+
ax.plot(
|
|
523
|
+
angles_theory_deg,
|
|
524
|
+
R_p_theory,
|
|
525
|
+
"g-",
|
|
526
|
+
linewidth=2.5,
|
|
527
|
+
alpha=0.7,
|
|
528
|
+
label="Theory: p-pol (TM)",
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Simulated data points
|
|
532
|
+
ax.plot(
|
|
533
|
+
angles_sim_deg,
|
|
534
|
+
R_s_sim,
|
|
535
|
+
"rs",
|
|
536
|
+
markersize=6,
|
|
537
|
+
markeredgewidth=1,
|
|
538
|
+
markeredgecolor="darkred",
|
|
539
|
+
markerfacecolor="lightcoral",
|
|
540
|
+
label="Simulation: s-pol",
|
|
541
|
+
zorder=5,
|
|
542
|
+
alpha=0.8,
|
|
543
|
+
)
|
|
544
|
+
ax.plot(
|
|
545
|
+
angles_sim_deg,
|
|
546
|
+
R_p_sim,
|
|
547
|
+
"go",
|
|
548
|
+
markersize=6,
|
|
549
|
+
markeredgewidth=1,
|
|
550
|
+
markeredgecolor="darkgreen",
|
|
551
|
+
markerfacecolor="lightgreen",
|
|
552
|
+
label="Simulation: p-pol",
|
|
553
|
+
zorder=5,
|
|
554
|
+
alpha=0.8,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Mark Brewster angles
|
|
558
|
+
ax.axvline(
|
|
559
|
+
brewster_angle_deg,
|
|
560
|
+
color="orange",
|
|
561
|
+
linestyle=":",
|
|
562
|
+
linewidth=2.5,
|
|
563
|
+
label=f"Theory Brewster ({brewster_angle_deg:.2f}°)",
|
|
564
|
+
)
|
|
565
|
+
ax.axvline(
|
|
566
|
+
brewster_sim_deg,
|
|
567
|
+
color="purple",
|
|
568
|
+
linestyle="--",
|
|
569
|
+
linewidth=2.5,
|
|
570
|
+
label=f"Simulated Brewster ({brewster_sim_deg:.0f}°)",
|
|
571
|
+
)
|
|
572
|
+
ax.plot(
|
|
573
|
+
measured_brewster_deg,
|
|
574
|
+
min_theory_reflection,
|
|
575
|
+
"ko",
|
|
576
|
+
markersize=10,
|
|
577
|
+
label=f"Theory R_p minimum ({min_theory_reflection:.5f})",
|
|
578
|
+
)
|
|
579
|
+
ax.plot(
|
|
580
|
+
brewster_sim_deg,
|
|
581
|
+
min_sim_reflection,
|
|
582
|
+
"mo",
|
|
583
|
+
markersize=10,
|
|
584
|
+
label=f"Sim R_p minimum ({min_sim_reflection:.5f})",
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
ax.set_xlabel("Angle of Incidence (degrees)", fontsize=12, fontweight="bold")
|
|
588
|
+
ax.set_ylabel("Reflectance R", fontsize=12, fontweight="bold")
|
|
589
|
+
ax.set_title(
|
|
590
|
+
"Fresnel Reflection: Theory vs Simulation", fontsize=14, fontweight="bold"
|
|
591
|
+
)
|
|
592
|
+
ax.legend(loc="best", fontsize=10)
|
|
593
|
+
ax.grid(True, alpha=0.3)
|
|
594
|
+
ax.set_xlim(0, 90)
|
|
595
|
+
ax.set_ylim(1e-6, 1)
|
|
596
|
+
ax.set_yscale("log")
|
|
597
|
+
|
|
598
|
+
# Add text box with key info
|
|
599
|
+
textstr = f"n₁ = {n1:.4f}\nn₂ = {n2:.4f}\nθ_B = {brewster_angle_deg:.2f}°"
|
|
600
|
+
props = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.8}
|
|
601
|
+
ax.text(
|
|
602
|
+
0.02,
|
|
603
|
+
0.98,
|
|
604
|
+
textstr,
|
|
605
|
+
transform=ax.transAxes,
|
|
606
|
+
fontsize=11,
|
|
607
|
+
verticalalignment="top",
|
|
608
|
+
bbox=props,
|
|
609
|
+
family="monospace",
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _plot_brewster_zoom(
|
|
614
|
+
ax,
|
|
615
|
+
angles_theory_deg: np.ndarray,
|
|
616
|
+
R_s_theory: np.ndarray,
|
|
617
|
+
R_p_theory: np.ndarray,
|
|
618
|
+
angles_sim_deg: np.ndarray,
|
|
619
|
+
R_s_sim: np.ndarray,
|
|
620
|
+
R_p_sim: np.ndarray,
|
|
621
|
+
brewster_angle_deg: float,
|
|
622
|
+
brewster_sim_deg: float,
|
|
623
|
+
measured_brewster_deg: float,
|
|
624
|
+
min_theory_reflection: float,
|
|
625
|
+
min_sim_reflection: float,
|
|
626
|
+
zoom_range: float = 10.0,
|
|
627
|
+
) -> None:
|
|
628
|
+
"""
|
|
629
|
+
Plot zoomed view near Brewster angle.
|
|
630
|
+
|
|
631
|
+
Parameters
|
|
632
|
+
----------
|
|
633
|
+
ax : matplotlib.axes.Axes
|
|
634
|
+
Axes to plot on.
|
|
635
|
+
angles_theory_deg : ndarray
|
|
636
|
+
Theory angle array in degrees.
|
|
637
|
+
R_s_theory : ndarray
|
|
638
|
+
Theoretical s-polarization reflectance.
|
|
639
|
+
R_p_theory : ndarray
|
|
640
|
+
Theoretical p-polarization reflectance.
|
|
641
|
+
angles_sim_deg : ndarray
|
|
642
|
+
Simulation angle array in degrees.
|
|
643
|
+
R_s_sim : ndarray
|
|
644
|
+
Simulated s-polarization reflectance.
|
|
645
|
+
R_p_sim : ndarray
|
|
646
|
+
Simulated p-polarization reflectance.
|
|
647
|
+
brewster_angle_deg : float
|
|
648
|
+
Theoretical Brewster angle in degrees.
|
|
649
|
+
brewster_sim_deg : float
|
|
650
|
+
Simulated Brewster angle in degrees.
|
|
651
|
+
measured_brewster_deg : float
|
|
652
|
+
Measured Brewster angle from theory minimum.
|
|
653
|
+
min_theory_reflection : float
|
|
654
|
+
Minimum theoretical R_p value.
|
|
655
|
+
min_sim_reflection : float
|
|
656
|
+
Minimum simulated R_p value.
|
|
657
|
+
zoom_range : float, optional
|
|
658
|
+
Range in degrees around Brewster angle to display.
|
|
659
|
+
"""
|
|
660
|
+
zoom_mask = (angles_theory_deg >= brewster_angle_deg - zoom_range) & (
|
|
661
|
+
angles_theory_deg <= brewster_angle_deg + zoom_range
|
|
662
|
+
)
|
|
663
|
+
zoom_sim_mask = (angles_sim_deg >= brewster_angle_deg - zoom_range) & (
|
|
664
|
+
angles_sim_deg <= brewster_angle_deg + zoom_range
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# Theory
|
|
668
|
+
ax.plot(
|
|
669
|
+
angles_theory_deg[zoom_mask],
|
|
670
|
+
R_s_theory[zoom_mask],
|
|
671
|
+
"r-",
|
|
672
|
+
linewidth=2,
|
|
673
|
+
label="Theory: s-pol",
|
|
674
|
+
alpha=0.7,
|
|
675
|
+
)
|
|
676
|
+
ax.plot(
|
|
677
|
+
angles_theory_deg[zoom_mask],
|
|
678
|
+
R_p_theory[zoom_mask],
|
|
679
|
+
"g-",
|
|
680
|
+
linewidth=2,
|
|
681
|
+
label="Theory: p-pol",
|
|
682
|
+
alpha=0.7,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Simulation
|
|
686
|
+
ax.plot(
|
|
687
|
+
angles_sim_deg[zoom_sim_mask],
|
|
688
|
+
R_s_sim[zoom_sim_mask],
|
|
689
|
+
"rs",
|
|
690
|
+
markersize=5,
|
|
691
|
+
label="Sim: s-pol",
|
|
692
|
+
)
|
|
693
|
+
ax.plot(
|
|
694
|
+
angles_sim_deg[zoom_sim_mask],
|
|
695
|
+
R_p_sim[zoom_sim_mask],
|
|
696
|
+
"go",
|
|
697
|
+
markersize=5,
|
|
698
|
+
label="Sim: p-pol",
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
ax.axvline(
|
|
702
|
+
brewster_angle_deg, color="orange", linestyle=":", linewidth=2, label="Theory"
|
|
703
|
+
)
|
|
704
|
+
ax.axvline(
|
|
705
|
+
brewster_sim_deg,
|
|
706
|
+
color="purple",
|
|
707
|
+
linestyle="--",
|
|
708
|
+
linewidth=2,
|
|
709
|
+
label="Simulation",
|
|
710
|
+
)
|
|
711
|
+
ax.plot(measured_brewster_deg, min_theory_reflection, "ko", markersize=8)
|
|
712
|
+
ax.plot(brewster_sim_deg, min_sim_reflection, "mo", markersize=8)
|
|
713
|
+
|
|
714
|
+
ax.set_xlabel("Angle of Incidence (°)", fontsize=10)
|
|
715
|
+
ax.set_ylabel("Reflectance R", fontsize=10)
|
|
716
|
+
ax.set_title(f"Zoom: Brewster Angle ± {zoom_range:.0f}°", fontweight="bold")
|
|
717
|
+
ax.legend(fontsize=9)
|
|
718
|
+
ax.grid(True, alpha=0.3)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _plot_brewster_polarization_ratio(
|
|
722
|
+
ax,
|
|
723
|
+
angles_theory_deg: np.ndarray,
|
|
724
|
+
R_s: np.ndarray,
|
|
725
|
+
R_p: np.ndarray,
|
|
726
|
+
brewster_angle_deg: float = None,
|
|
727
|
+
**kwargs,
|
|
728
|
+
) -> None:
|
|
729
|
+
"""
|
|
730
|
+
Plot polarization ratio (R_p / R_s).
|
|
731
|
+
|
|
732
|
+
Parameters
|
|
733
|
+
----------
|
|
734
|
+
ax : matplotlib.axes.Axes
|
|
735
|
+
Axes to plot on.
|
|
736
|
+
angles_theory_deg : ndarray
|
|
737
|
+
Theory angle array in degrees.
|
|
738
|
+
R_s : ndarray
|
|
739
|
+
Theoretical s-polarization reflectance.
|
|
740
|
+
R_p : ndarray
|
|
741
|
+
Theoretical p-polarization reflectance.
|
|
742
|
+
brewster_angle_deg : float
|
|
743
|
+
Theoretical Brewster angle in degrees.
|
|
744
|
+
"""
|
|
745
|
+
ratio = np.where(R_s > 1e-6, R_p / R_s, 0)
|
|
746
|
+
ax.plot(angles_theory_deg, ratio, **kwargs)
|
|
747
|
+
if brewster_angle_deg is not None:
|
|
748
|
+
ax.axvline(
|
|
749
|
+
brewster_angle_deg,
|
|
750
|
+
color="orange",
|
|
751
|
+
linestyle=":",
|
|
752
|
+
linewidth=2,
|
|
753
|
+
label="Brewster Angle",
|
|
754
|
+
)
|
|
755
|
+
ax.axhline(0, color="k", linestyle="-", linewidth=0.5)
|
|
756
|
+
|
|
757
|
+
ax.set_xlabel("Angle of Incidence (°)", fontsize=10)
|
|
758
|
+
ax.set_ylabel(r"$R_p / R_s$", fontsize=10)
|
|
759
|
+
ax.set_title("Polarization Ratio", fontweight="bold")
|
|
760
|
+
ax.grid(True, alpha=0.3)
|
|
761
|
+
ax.set_xlim(0, 90)
|
|
762
|
+
ax.set_ylim(-0.1, 1.0)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _plot_brewster_polarization_degree(
|
|
766
|
+
ax,
|
|
767
|
+
angles_theory_deg: np.ndarray,
|
|
768
|
+
R_s: np.ndarray,
|
|
769
|
+
R_p: np.ndarray,
|
|
770
|
+
brewster_angle_deg: float = None,
|
|
771
|
+
**kwargs,
|
|
772
|
+
) -> None:
|
|
773
|
+
"""
|
|
774
|
+
Plot polarization degree ((R_s - R_p) / (R_s + R_p)).
|
|
775
|
+
|
|
776
|
+
Parameters
|
|
777
|
+
----------
|
|
778
|
+
ax : matplotlib.axes.Axes
|
|
779
|
+
Axes to plot on.
|
|
780
|
+
angles_theory_deg : ndarray
|
|
781
|
+
Theory angle array in degrees.
|
|
782
|
+
R_s : ndarray
|
|
783
|
+
Theoretical s-polarization reflectance.
|
|
784
|
+
R_p : ndarray
|
|
785
|
+
Theoretical p-polarization reflectance.
|
|
786
|
+
brewster_angle_deg : float
|
|
787
|
+
Theoretical Brewster angle in degrees.
|
|
788
|
+
"""
|
|
789
|
+
pol_degree = np.where((R_s + R_p) > 1e-6, (R_s - R_p) / (R_s + R_p), 0)
|
|
790
|
+
ax.plot(angles_theory_deg, pol_degree, **kwargs)
|
|
791
|
+
if brewster_angle_deg is not None:
|
|
792
|
+
ax.axvline(
|
|
793
|
+
brewster_angle_deg,
|
|
794
|
+
color="orange",
|
|
795
|
+
linestyle=":",
|
|
796
|
+
linewidth=2,
|
|
797
|
+
label="Brewster Angle",
|
|
798
|
+
)
|
|
799
|
+
ax.axhline(0, color="k", linestyle="-", linewidth=0.5)
|
|
800
|
+
|
|
801
|
+
ax.set_xlabel("Angle of Incidence (°)", fontsize=10)
|
|
802
|
+
ax.set_ylabel(r"$(R_s - R_p) / (R_s + R_p)$", fontsize=10)
|
|
803
|
+
ax.set_title("Polarization Degree", fontweight="bold")
|
|
804
|
+
ax.grid(True, alpha=0.3)
|
|
805
|
+
ax.set_xlim(0, 90)
|
|
806
|
+
ax.set_ylim(-0.1, 1.0)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _plot_brewster_validation_summary(
|
|
810
|
+
ax,
|
|
811
|
+
angles_theory_deg: np.ndarray,
|
|
812
|
+
R_s_theory: np.ndarray,
|
|
813
|
+
R_p_theory: np.ndarray,
|
|
814
|
+
angles_sim_deg: np.ndarray,
|
|
815
|
+
R_s_sim: np.ndarray,
|
|
816
|
+
R_p_sim: np.ndarray,
|
|
817
|
+
brewster_angle_deg: float,
|
|
818
|
+
brewster_sim_deg: float,
|
|
819
|
+
measured_brewster_deg: float,
|
|
820
|
+
min_theory_reflection: float,
|
|
821
|
+
min_sim_reflection: float,
|
|
822
|
+
num_rays_per_angle: int,
|
|
823
|
+
) -> None:
|
|
824
|
+
"""
|
|
825
|
+
Plot validation summary text box.
|
|
826
|
+
|
|
827
|
+
Parameters
|
|
828
|
+
----------
|
|
829
|
+
ax : matplotlib.axes.Axes
|
|
830
|
+
Axes to plot on.
|
|
831
|
+
angles_theory_deg : ndarray
|
|
832
|
+
Theory angle array in degrees.
|
|
833
|
+
R_s_theory : ndarray
|
|
834
|
+
Theoretical s-polarization reflectance.
|
|
835
|
+
R_p_theory : ndarray
|
|
836
|
+
Theoretical p-polarization reflectance.
|
|
837
|
+
angles_sim_deg : ndarray
|
|
838
|
+
Simulation angle array in degrees.
|
|
839
|
+
R_s_sim : ndarray
|
|
840
|
+
Simulated s-polarization reflectance.
|
|
841
|
+
R_p_sim : ndarray
|
|
842
|
+
Simulated p-polarization reflectance.
|
|
843
|
+
brewster_angle_deg : float
|
|
844
|
+
Theoretical Brewster angle in degrees.
|
|
845
|
+
brewster_sim_deg : float
|
|
846
|
+
Simulated Brewster angle in degrees.
|
|
847
|
+
measured_brewster_deg : float
|
|
848
|
+
Measured Brewster angle from theory minimum.
|
|
849
|
+
min_theory_reflection : float
|
|
850
|
+
Minimum theoretical R_p value.
|
|
851
|
+
min_sim_reflection : float
|
|
852
|
+
Minimum simulated R_p value.
|
|
853
|
+
num_rays_per_angle : int
|
|
854
|
+
Number of rays used per angle in simulation.
|
|
855
|
+
"""
|
|
856
|
+
ax.axis("off")
|
|
857
|
+
|
|
858
|
+
# Get R_s values at Brewster angle
|
|
859
|
+
min_sim_idx = np.argmin(R_p_sim)
|
|
860
|
+
theory_brewster_idx = np.argmin(np.abs(angles_theory_deg - brewster_angle_deg))
|
|
861
|
+
R_s_at_brewster_theory = R_s_theory[theory_brewster_idx]
|
|
862
|
+
R_s_at_brewster_sim = R_s_sim[min_sim_idx] if len(R_s_sim) > 0 else 0
|
|
863
|
+
|
|
864
|
+
# Get R at normal incidence
|
|
865
|
+
R_at_normal = R_s_theory[0] if len(R_s_theory) > 0 else 0
|
|
866
|
+
|
|
867
|
+
validation_text = f"""
|
|
868
|
+
VALIDATION RESULTS
|
|
869
|
+
════════════════════════════════
|
|
870
|
+
|
|
871
|
+
Theoretical:
|
|
872
|
+
θ_B = arctan(n₂/n₁)
|
|
873
|
+
θ_B = {brewster_angle_deg:.3f}°
|
|
874
|
+
|
|
875
|
+
Measured from Theory R_p min:
|
|
876
|
+
θ_B = {measured_brewster_deg:.3f}°
|
|
877
|
+
Error = {abs(measured_brewster_deg - brewster_angle_deg):.4f}°
|
|
878
|
+
|
|
879
|
+
Simulated Brewster Angle:
|
|
880
|
+
θ_B = {brewster_sim_deg:.0f}°
|
|
881
|
+
Error = {abs(brewster_sim_deg - brewster_angle_deg):.3f}°
|
|
882
|
+
R_p minimum = {min_sim_reflection:.6f}
|
|
883
|
+
|
|
884
|
+
Simulation Coverage:
|
|
885
|
+
Angles: 0° to {angles_sim_deg[-1]:.0f}°
|
|
886
|
+
Total: {len(angles_sim_deg)} angles
|
|
887
|
+
Rays per angle: {num_rays_per_angle}
|
|
888
|
+
|
|
889
|
+
Agreement at θ_B:
|
|
890
|
+
R_s (theory): {R_s_at_brewster_theory:.5f}
|
|
891
|
+
R_s (sim): {R_s_at_brewster_sim:.5f}
|
|
892
|
+
R_p (theory): {min_theory_reflection:.5f}
|
|
893
|
+
R_p (sim): {min_sim_reflection:.5f}
|
|
894
|
+
|
|
895
|
+
Key Properties:
|
|
896
|
+
• R_p = 0 at θ_B ✓
|
|
897
|
+
• R_s increases monotonically ✓
|
|
898
|
+
• R → 1 as θ → 90° ✓
|
|
899
|
+
• R(0°) = {R_at_normal:.4f}
|
|
900
|
+
|
|
901
|
+
✓ Fresnel equations validated
|
|
902
|
+
✓ Brewster angle confirmed
|
|
903
|
+
✓ Ray simulation matches theory
|
|
904
|
+
"""
|
|
905
|
+
|
|
906
|
+
ax.text(
|
|
907
|
+
0.05,
|
|
908
|
+
0.95,
|
|
909
|
+
validation_text,
|
|
910
|
+
transform=ax.transAxes,
|
|
911
|
+
fontsize=9,
|
|
912
|
+
verticalalignment="top",
|
|
913
|
+
fontfamily="monospace",
|
|
914
|
+
bbox={"boxstyle": "round", "facecolor": "lightblue", "alpha": 0.7},
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def plot_brewster_validation(
|
|
919
|
+
angles_theory_deg: np.ndarray,
|
|
920
|
+
R_s_theory: np.ndarray,
|
|
921
|
+
R_p_theory: np.ndarray,
|
|
922
|
+
angles_sim_deg: np.ndarray,
|
|
923
|
+
R_s_sim: np.ndarray,
|
|
924
|
+
R_p_sim: np.ndarray,
|
|
925
|
+
n1: float,
|
|
926
|
+
n2: float,
|
|
927
|
+
brewster_angle_deg: float,
|
|
928
|
+
brewster_sim_deg: float,
|
|
929
|
+
wavelength: float,
|
|
930
|
+
num_rays_per_angle: int = 1000,
|
|
931
|
+
figsize: tuple[float, float] = (16, 10),
|
|
932
|
+
save_path: str | None = None,
|
|
933
|
+
) -> Figure:
|
|
934
|
+
"""
|
|
935
|
+
Create Brewster angle validation visualization.
|
|
936
|
+
|
|
937
|
+
Compares theoretical Fresnel curves with ray-traced simulation results.
|
|
938
|
+
|
|
939
|
+
Parameters
|
|
940
|
+
----------
|
|
941
|
+
angles_theory_deg : ndarray
|
|
942
|
+
Theory angle array in degrees.
|
|
943
|
+
R_s_theory : ndarray
|
|
944
|
+
Theoretical s-polarization reflectance.
|
|
945
|
+
R_p_theory : ndarray
|
|
946
|
+
Theoretical p-polarization reflectance.
|
|
947
|
+
angles_sim_deg : ndarray
|
|
948
|
+
Simulation angle array in degrees.
|
|
949
|
+
R_s_sim : ndarray
|
|
950
|
+
Simulated s-polarization reflectance.
|
|
951
|
+
R_p_sim : ndarray
|
|
952
|
+
Simulated p-polarization reflectance.
|
|
953
|
+
n1 : float
|
|
954
|
+
Refractive index of incident medium.
|
|
955
|
+
n2 : float
|
|
956
|
+
Refractive index of transmission medium.
|
|
957
|
+
brewster_angle_deg : float
|
|
958
|
+
Theoretical Brewster angle in degrees.
|
|
959
|
+
brewster_sim_deg : float
|
|
960
|
+
Simulated Brewster angle (minimum R_p) in degrees.
|
|
961
|
+
wavelength : float
|
|
962
|
+
Optical wavelength in meters.
|
|
963
|
+
num_rays_per_angle : int
|
|
964
|
+
Number of rays used per angle in simulation.
|
|
965
|
+
figsize : tuple
|
|
966
|
+
Figure size.
|
|
967
|
+
save_path : str, optional
|
|
968
|
+
Path to save figure.
|
|
969
|
+
|
|
970
|
+
Returns
|
|
971
|
+
-------
|
|
972
|
+
Figure
|
|
973
|
+
Matplotlib figure with validation plots.
|
|
974
|
+
"""
|
|
975
|
+
# Find minima
|
|
976
|
+
min_theory_idx = np.argmin(R_p_theory)
|
|
977
|
+
measured_brewster_deg = angles_theory_deg[min_theory_idx]
|
|
978
|
+
min_theory_reflection = R_p_theory[min_theory_idx]
|
|
979
|
+
|
|
980
|
+
min_sim_idx = np.argmin(R_p_sim)
|
|
981
|
+
min_sim_reflection = R_p_sim[min_sim_idx]
|
|
982
|
+
|
|
983
|
+
# Create figure
|
|
984
|
+
fig = plt.figure(figsize=figsize, constrained_layout=True)
|
|
985
|
+
gs = fig.add_gridspec(2, 3)
|
|
986
|
+
|
|
987
|
+
# 1. Main Fresnel plot
|
|
988
|
+
ax1 = fig.add_subplot(gs[0, :])
|
|
989
|
+
_plot_brewster_main_comparison(
|
|
990
|
+
ax1,
|
|
991
|
+
angles_theory_deg,
|
|
992
|
+
R_s_theory,
|
|
993
|
+
R_p_theory,
|
|
994
|
+
angles_sim_deg,
|
|
995
|
+
R_s_sim,
|
|
996
|
+
R_p_sim,
|
|
997
|
+
n1,
|
|
998
|
+
n2,
|
|
999
|
+
brewster_angle_deg,
|
|
1000
|
+
brewster_sim_deg,
|
|
1001
|
+
measured_brewster_deg,
|
|
1002
|
+
min_theory_reflection,
|
|
1003
|
+
min_sim_reflection,
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
# 2. Zoom near Brewster angle
|
|
1007
|
+
ax2 = fig.add_subplot(gs[1, 0])
|
|
1008
|
+
_plot_brewster_zoom(
|
|
1009
|
+
ax2,
|
|
1010
|
+
angles_theory_deg,
|
|
1011
|
+
R_s_theory,
|
|
1012
|
+
R_p_theory,
|
|
1013
|
+
angles_sim_deg,
|
|
1014
|
+
R_s_sim,
|
|
1015
|
+
R_p_sim,
|
|
1016
|
+
brewster_angle_deg,
|
|
1017
|
+
brewster_sim_deg,
|
|
1018
|
+
measured_brewster_deg,
|
|
1019
|
+
min_theory_reflection,
|
|
1020
|
+
min_sim_reflection,
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
# 3. Polarization ratio
|
|
1024
|
+
ax3 = fig.add_subplot(gs[1, 1])
|
|
1025
|
+
_plot_brewster_polarization_ratio(
|
|
1026
|
+
ax3,
|
|
1027
|
+
angles_theory_deg,
|
|
1028
|
+
R_s_theory,
|
|
1029
|
+
R_p_theory,
|
|
1030
|
+
brewster_angle_deg,
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
# 4. Validation summary
|
|
1034
|
+
ax4 = fig.add_subplot(gs[1, 2])
|
|
1035
|
+
_plot_brewster_validation_summary(
|
|
1036
|
+
ax4,
|
|
1037
|
+
angles_theory_deg,
|
|
1038
|
+
R_s_theory,
|
|
1039
|
+
R_p_theory,
|
|
1040
|
+
angles_sim_deg,
|
|
1041
|
+
R_s_sim,
|
|
1042
|
+
R_p_sim,
|
|
1043
|
+
brewster_angle_deg,
|
|
1044
|
+
brewster_sim_deg,
|
|
1045
|
+
measured_brewster_deg,
|
|
1046
|
+
min_theory_reflection,
|
|
1047
|
+
min_sim_reflection,
|
|
1048
|
+
num_rays_per_angle,
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
# Main title
|
|
1052
|
+
fig.suptitle(
|
|
1053
|
+
f"Brewster Angle Validation (λ = {wavelength*1e9:.0f} nm)",
|
|
1054
|
+
fontsize=16,
|
|
1055
|
+
fontweight="bold",
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
if save_path:
|
|
1059
|
+
save_figure(fig, save_path)
|
|
1060
|
+
|
|
1061
|
+
return fig
|