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.
Files changed (180) hide show
  1. lsurf/__init__.py +471 -0
  2. lsurf/analysis/__init__.py +107 -0
  3. lsurf/analysis/healpix_utils.py +418 -0
  4. lsurf/analysis/sphere_viz.py +1280 -0
  5. lsurf/cli/__init__.py +48 -0
  6. lsurf/cli/build.py +398 -0
  7. lsurf/cli/config_schema.py +318 -0
  8. lsurf/cli/gui_cmd.py +76 -0
  9. lsurf/cli/interactive.py +850 -0
  10. lsurf/cli/main.py +81 -0
  11. lsurf/cli/run.py +806 -0
  12. lsurf/detectors/__init__.py +266 -0
  13. lsurf/detectors/analysis.py +289 -0
  14. lsurf/detectors/base.py +284 -0
  15. lsurf/detectors/constant_size_rings.py +485 -0
  16. lsurf/detectors/directional.py +45 -0
  17. lsurf/detectors/extended/__init__.py +73 -0
  18. lsurf/detectors/extended/local_sphere.py +353 -0
  19. lsurf/detectors/extended/recording_sphere.py +368 -0
  20. lsurf/detectors/planar.py +45 -0
  21. lsurf/detectors/protocol.py +187 -0
  22. lsurf/detectors/recording_spheres.py +63 -0
  23. lsurf/detectors/results.py +1140 -0
  24. lsurf/detectors/small/__init__.py +79 -0
  25. lsurf/detectors/small/directional.py +330 -0
  26. lsurf/detectors/small/planar.py +401 -0
  27. lsurf/detectors/small/spherical.py +450 -0
  28. lsurf/detectors/spherical.py +45 -0
  29. lsurf/geometry/__init__.py +199 -0
  30. lsurf/geometry/builder.py +478 -0
  31. lsurf/geometry/cell.py +228 -0
  32. lsurf/geometry/cell_geometry.py +247 -0
  33. lsurf/geometry/detector_arrays.py +1785 -0
  34. lsurf/geometry/geometry.py +222 -0
  35. lsurf/geometry/surface_analysis.py +375 -0
  36. lsurf/geometry/validation.py +91 -0
  37. lsurf/gui/__init__.py +51 -0
  38. lsurf/gui/app.py +903 -0
  39. lsurf/gui/core/__init__.py +39 -0
  40. lsurf/gui/core/scene.py +343 -0
  41. lsurf/gui/core/simulation.py +264 -0
  42. lsurf/gui/renderers/__init__.py +40 -0
  43. lsurf/gui/renderers/ray_renderer.py +353 -0
  44. lsurf/gui/renderers/source_renderer.py +505 -0
  45. lsurf/gui/renderers/surface_renderer.py +477 -0
  46. lsurf/gui/views/__init__.py +48 -0
  47. lsurf/gui/views/config_editor.py +3199 -0
  48. lsurf/gui/views/properties.py +257 -0
  49. lsurf/gui/views/results.py +291 -0
  50. lsurf/gui/views/scene_tree.py +180 -0
  51. lsurf/gui/views/viewport_3d.py +555 -0
  52. lsurf/gui/views/visualizations.py +712 -0
  53. lsurf/materials/__init__.py +169 -0
  54. lsurf/materials/base/__init__.py +64 -0
  55. lsurf/materials/base/full_inhomogeneous.py +208 -0
  56. lsurf/materials/base/grid_inhomogeneous.py +319 -0
  57. lsurf/materials/base/homogeneous.py +342 -0
  58. lsurf/materials/base/material_field.py +527 -0
  59. lsurf/materials/base/simple_inhomogeneous.py +418 -0
  60. lsurf/materials/base/spectral_inhomogeneous.py +497 -0
  61. lsurf/materials/implementations/__init__.py +120 -0
  62. lsurf/materials/implementations/data/alpha_values_typical_atmosphere_updated.txt +24 -0
  63. lsurf/materials/implementations/duct_atmosphere.py +390 -0
  64. lsurf/materials/implementations/exponential_atmosphere.py +435 -0
  65. lsurf/materials/implementations/gaussian_lens.py +120 -0
  66. lsurf/materials/implementations/interpolated_data.py +123 -0
  67. lsurf/materials/implementations/layered_atmosphere.py +134 -0
  68. lsurf/materials/implementations/linear_gradient.py +109 -0
  69. lsurf/materials/implementations/linsley_atmosphere.py +764 -0
  70. lsurf/materials/implementations/standard_materials.py +126 -0
  71. lsurf/materials/implementations/turbulent_atmosphere.py +135 -0
  72. lsurf/materials/implementations/us_standard_atmosphere.py +149 -0
  73. lsurf/materials/utils/__init__.py +77 -0
  74. lsurf/materials/utils/constants.py +45 -0
  75. lsurf/materials/utils/device_functions.py +117 -0
  76. lsurf/materials/utils/dispersion.py +160 -0
  77. lsurf/materials/utils/factories.py +142 -0
  78. lsurf/propagation/__init__.py +91 -0
  79. lsurf/propagation/detector_gpu.py +67 -0
  80. lsurf/propagation/gpu_device_rays.py +294 -0
  81. lsurf/propagation/kernels/__init__.py +175 -0
  82. lsurf/propagation/kernels/absorption/__init__.py +61 -0
  83. lsurf/propagation/kernels/absorption/grid.py +240 -0
  84. lsurf/propagation/kernels/absorption/simple.py +232 -0
  85. lsurf/propagation/kernels/absorption/spectral.py +410 -0
  86. lsurf/propagation/kernels/detection/__init__.py +64 -0
  87. lsurf/propagation/kernels/detection/protocol.py +102 -0
  88. lsurf/propagation/kernels/detection/spherical.py +255 -0
  89. lsurf/propagation/kernels/device_functions.py +790 -0
  90. lsurf/propagation/kernels/fresnel/__init__.py +64 -0
  91. lsurf/propagation/kernels/fresnel/protocol.py +97 -0
  92. lsurf/propagation/kernels/fresnel/standard.py +258 -0
  93. lsurf/propagation/kernels/intersection/__init__.py +79 -0
  94. lsurf/propagation/kernels/intersection/annular_plane.py +207 -0
  95. lsurf/propagation/kernels/intersection/bounded_plane.py +205 -0
  96. lsurf/propagation/kernels/intersection/plane.py +166 -0
  97. lsurf/propagation/kernels/intersection/protocol.py +95 -0
  98. lsurf/propagation/kernels/intersection/signed_distance.py +742 -0
  99. lsurf/propagation/kernels/intersection/sphere.py +190 -0
  100. lsurf/propagation/kernels/propagation/__init__.py +85 -0
  101. lsurf/propagation/kernels/propagation/grid.py +527 -0
  102. lsurf/propagation/kernels/propagation/protocol.py +105 -0
  103. lsurf/propagation/kernels/propagation/simple.py +460 -0
  104. lsurf/propagation/kernels/propagation/spectral.py +875 -0
  105. lsurf/propagation/kernels/registry.py +331 -0
  106. lsurf/propagation/kernels/surface/__init__.py +72 -0
  107. lsurf/propagation/kernels/surface/bisection.py +232 -0
  108. lsurf/propagation/kernels/surface/detection.py +402 -0
  109. lsurf/propagation/kernels/surface/reduction.py +166 -0
  110. lsurf/propagation/propagator_protocol.py +222 -0
  111. lsurf/propagation/propagators/__init__.py +101 -0
  112. lsurf/propagation/propagators/detector_handler.py +354 -0
  113. lsurf/propagation/propagators/factory.py +200 -0
  114. lsurf/propagation/propagators/fresnel_handler.py +305 -0
  115. lsurf/propagation/propagators/gpu_gradient.py +566 -0
  116. lsurf/propagation/propagators/gpu_surface_propagator.py +707 -0
  117. lsurf/propagation/propagators/gradient.py +429 -0
  118. lsurf/propagation/propagators/intersection_handler.py +327 -0
  119. lsurf/propagation/propagators/material_propagator.py +398 -0
  120. lsurf/propagation/propagators/signed_distance_handler.py +522 -0
  121. lsurf/propagation/propagators/spectral_gpu_gradient.py +553 -0
  122. lsurf/propagation/propagators/surface_interaction.py +616 -0
  123. lsurf/propagation/propagators/surface_propagator.py +719 -0
  124. lsurf/py.typed +1 -0
  125. lsurf/simulation/__init__.py +70 -0
  126. lsurf/simulation/config.py +164 -0
  127. lsurf/simulation/orchestrator.py +462 -0
  128. lsurf/simulation/result.py +299 -0
  129. lsurf/simulation/simulation.py +262 -0
  130. lsurf/sources/__init__.py +128 -0
  131. lsurf/sources/base.py +264 -0
  132. lsurf/sources/collimated.py +252 -0
  133. lsurf/sources/custom.py +409 -0
  134. lsurf/sources/diverging.py +228 -0
  135. lsurf/sources/gaussian.py +272 -0
  136. lsurf/sources/parallel_from_positions.py +197 -0
  137. lsurf/sources/point.py +172 -0
  138. lsurf/sources/uniform_diverging.py +258 -0
  139. lsurf/surfaces/__init__.py +184 -0
  140. lsurf/surfaces/cpu/__init__.py +50 -0
  141. lsurf/surfaces/cpu/curved_wave.py +463 -0
  142. lsurf/surfaces/cpu/gerstner_wave.py +381 -0
  143. lsurf/surfaces/cpu/wave_params.py +118 -0
  144. lsurf/surfaces/gpu/__init__.py +72 -0
  145. lsurf/surfaces/gpu/annular_plane.py +453 -0
  146. lsurf/surfaces/gpu/bounded_plane.py +390 -0
  147. lsurf/surfaces/gpu/curved_wave.py +483 -0
  148. lsurf/surfaces/gpu/gerstner_wave.py +377 -0
  149. lsurf/surfaces/gpu/multi_curved_wave.py +520 -0
  150. lsurf/surfaces/gpu/plane.py +299 -0
  151. lsurf/surfaces/gpu/recording_sphere.py +587 -0
  152. lsurf/surfaces/gpu/sphere.py +311 -0
  153. lsurf/surfaces/protocol.py +336 -0
  154. lsurf/surfaces/registry.py +373 -0
  155. lsurf/utilities/__init__.py +175 -0
  156. lsurf/utilities/detector_analysis.py +814 -0
  157. lsurf/utilities/fresnel.py +628 -0
  158. lsurf/utilities/interactions.py +1215 -0
  159. lsurf/utilities/propagation.py +602 -0
  160. lsurf/utilities/ray_data.py +532 -0
  161. lsurf/utilities/recording_sphere.py +745 -0
  162. lsurf/utilities/time_spread.py +463 -0
  163. lsurf/visualization/__init__.py +329 -0
  164. lsurf/visualization/absorption_plots.py +334 -0
  165. lsurf/visualization/atmospheric_plots.py +754 -0
  166. lsurf/visualization/common.py +348 -0
  167. lsurf/visualization/detector_plots.py +1350 -0
  168. lsurf/visualization/detector_sphere_plots.py +1173 -0
  169. lsurf/visualization/fresnel_plots.py +1061 -0
  170. lsurf/visualization/ocean_simulation_plots.py +999 -0
  171. lsurf/visualization/polarization_plots.py +916 -0
  172. lsurf/visualization/raytracing_plots.py +1521 -0
  173. lsurf/visualization/ring_detector_plots.py +1867 -0
  174. lsurf/visualization/time_spread_plots.py +531 -0
  175. lsurf-1.0.0.dist-info/METADATA +381 -0
  176. lsurf-1.0.0.dist-info/RECORD +180 -0
  177. lsurf-1.0.0.dist-info/WHEEL +5 -0
  178. lsurf-1.0.0.dist-info/entry_points.txt +2 -0
  179. lsurf-1.0.0.dist-info/licenses/LICENSE +32 -0
  180. 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