witwin-maxwell 0.3.0__py3-none-win_amd64.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 (107) hide show
  1. witwin/maxwell/__init__.py +164 -0
  2. witwin/maxwell/adapters/__init__.py +1 -0
  3. witwin/maxwell/adapters/gds.py +203 -0
  4. witwin/maxwell/adapters/tidy3d.py +1669 -0
  5. witwin/maxwell/adjoint_inputs.py +70 -0
  6. witwin/maxwell/compiler/__init__.py +11 -0
  7. witwin/maxwell/compiler/materials.py +1355 -0
  8. witwin/maxwell/compiler/monitors.py +164 -0
  9. witwin/maxwell/compiler/sources.py +222 -0
  10. witwin/maxwell/fdfd/__init__.py +3 -0
  11. witwin/maxwell/fdfd/adjoint/__init__.py +3 -0
  12. witwin/maxwell/fdfd/adjoint/bridge.py +180 -0
  13. witwin/maxwell/fdfd/ams.py +300 -0
  14. witwin/maxwell/fdfd/krylov.py +280 -0
  15. witwin/maxwell/fdfd/plotting.py +110 -0
  16. witwin/maxwell/fdfd/postprocess.py +33 -0
  17. witwin/maxwell/fdfd/solver.py +1057 -0
  18. witwin/maxwell/fdtd/__init__.py +4 -0
  19. witwin/maxwell/fdtd/adjoint/__init__.py +21 -0
  20. witwin/maxwell/fdtd/adjoint/bridge.py +746 -0
  21. witwin/maxwell/fdtd/adjoint/capabilities.py +29 -0
  22. witwin/maxwell/fdtd/adjoint/core.py +3614 -0
  23. witwin/maxwell/fdtd/adjoint/dispatch.py +684 -0
  24. witwin/maxwell/fdtd/adjoint/native.py +2561 -0
  25. witwin/maxwell/fdtd/adjoint/profiler.py +195 -0
  26. witwin/maxwell/fdtd/adjoint/reverse_common.py +207 -0
  27. witwin/maxwell/fdtd/adjoint/seeds.py +524 -0
  28. witwin/maxwell/fdtd/boundary/__init__.py +37 -0
  29. witwin/maxwell/fdtd/boundary/bloch.py +90 -0
  30. witwin/maxwell/fdtd/boundary/common.py +41 -0
  31. witwin/maxwell/fdtd/boundary/cpml.py +527 -0
  32. witwin/maxwell/fdtd/boundary/runtime.py +209 -0
  33. witwin/maxwell/fdtd/checkpoint.py +227 -0
  34. witwin/maxwell/fdtd/coords.py +32 -0
  35. witwin/maxwell/fdtd/cuda/__init__.py +11 -0
  36. witwin/maxwell/fdtd/cuda/backend.py +3532 -0
  37. witwin/maxwell/fdtd/cuda/build.py +292 -0
  38. witwin/maxwell/fdtd/cuda/extension.cpp +2053 -0
  39. witwin/maxwell/fdtd/cuda/kernels/adjoint.cu +5290 -0
  40. witwin/maxwell/fdtd/cuda/kernels/aniso.cu +1704 -0
  41. witwin/maxwell/fdtd/cuda/kernels/boundary.cu +424 -0
  42. witwin/maxwell/fdtd/cuda/kernels/common.cu +74 -0
  43. witwin/maxwell/fdtd/cuda/kernels/common.cuh +82 -0
  44. witwin/maxwell/fdtd/cuda/kernels/dispersive.cu +782 -0
  45. witwin/maxwell/fdtd/cuda/kernels/electric.cu +3613 -0
  46. witwin/maxwell/fdtd/cuda/kernels/magnetic.cu +781 -0
  47. witwin/maxwell/fdtd/cuda/kernels/observers.cu +347 -0
  48. witwin/maxwell/fdtd/cuda/kernels/projection.cu +211 -0
  49. witwin/maxwell/fdtd/cuda/kernels/sources.cu +1482 -0
  50. witwin/maxwell/fdtd/cuda/kernels/spectral.cu +297 -0
  51. witwin/maxwell/fdtd/cuda/launch.h +23 -0
  52. witwin/maxwell/fdtd/cuda/prebuilt/witwin_maxwell_fdtd_cuda.pyd +0 -0
  53. witwin/maxwell/fdtd/cuda/runtime/__init__.py +6 -0
  54. witwin/maxwell/fdtd/cuda/runtime/dispatcher.py +6 -0
  55. witwin/maxwell/fdtd/cuda/runtime/graph.py +33 -0
  56. witwin/maxwell/fdtd/cuda/tensors.h +35 -0
  57. witwin/maxwell/fdtd/dispersion.py +48 -0
  58. witwin/maxwell/fdtd/evaluation.py +290 -0
  59. witwin/maxwell/fdtd/excitation/__init__.py +27 -0
  60. witwin/maxwell/fdtd/excitation/injection.py +1102 -0
  61. witwin/maxwell/fdtd/excitation/modes.py +1675 -0
  62. witwin/maxwell/fdtd/excitation/spatial.py +469 -0
  63. witwin/maxwell/fdtd/excitation/temporal.py +510 -0
  64. witwin/maxwell/fdtd/excitation/tfsf_apply.py +264 -0
  65. witwin/maxwell/fdtd/excitation/tfsf_common.py +421 -0
  66. witwin/maxwell/fdtd/excitation/tfsf_specs.py +490 -0
  67. witwin/maxwell/fdtd/excitation/tfsf_state.py +778 -0
  68. witwin/maxwell/fdtd/io.py +31 -0
  69. witwin/maxwell/fdtd/material_pullback.py +371 -0
  70. witwin/maxwell/fdtd/meshing/__init__.py +3 -0
  71. witwin/maxwell/fdtd/meshing/autogrid.py +335 -0
  72. witwin/maxwell/fdtd/observers.py +1453 -0
  73. witwin/maxwell/fdtd/plotting.py +121 -0
  74. witwin/maxwell/fdtd/postprocess.py +205 -0
  75. witwin/maxwell/fdtd/runtime/__init__.py +119 -0
  76. witwin/maxwell/fdtd/runtime/initialization.py +236 -0
  77. witwin/maxwell/fdtd/runtime/materials.py +1563 -0
  78. witwin/maxwell/fdtd/runtime/module_cache.py +31 -0
  79. witwin/maxwell/fdtd/runtime/spectral.py +581 -0
  80. witwin/maxwell/fdtd/runtime/stepping.py +1987 -0
  81. witwin/maxwell/fdtd/solver.py +502 -0
  82. witwin/maxwell/geometry/__init__.py +5 -0
  83. witwin/maxwell/geometry/polyslab.py +299 -0
  84. witwin/maxwell/materials.py +3 -0
  85. witwin/maxwell/media.py +1467 -0
  86. witwin/maxwell/monitors.py +743 -0
  87. witwin/maxwell/ports.py +113 -0
  88. witwin/maxwell/postprocess/RCS.py +139 -0
  89. witwin/maxwell/postprocess/__init__.py +42 -0
  90. witwin/maxwell/postprocess/diffraction.py +219 -0
  91. witwin/maxwell/postprocess/directivity.py +152 -0
  92. witwin/maxwell/postprocess/emission.py +67 -0
  93. witwin/maxwell/postprocess/modal.py +324 -0
  94. witwin/maxwell/postprocess/nfft.py +266 -0
  95. witwin/maxwell/postprocess/scattering_parameters.py +358 -0
  96. witwin/maxwell/postprocess/stratton_chu.py +1193 -0
  97. witwin/maxwell/result.py +967 -0
  98. witwin/maxwell/scene.py +1338 -0
  99. witwin/maxwell/simulation.py +659 -0
  100. witwin/maxwell/sources.py +917 -0
  101. witwin/maxwell/visualization/__init__.py +28 -0
  102. witwin/maxwell/visualization/interactive.py +484 -0
  103. witwin/maxwell/visualization/plots.py +568 -0
  104. witwin_maxwell-0.3.0.dist-info/METADATA +184 -0
  105. witwin_maxwell-0.3.0.dist-info/RECORD +107 -0
  106. witwin_maxwell-0.3.0.dist-info/WHEEL +4 -0
  107. witwin_maxwell-0.3.0.dist-info/licenses/LICENSE +13 -0
@@ -0,0 +1,568 @@
1
+ """
2
+ Unified visualization utilities for Maxwell solvers.
3
+
4
+ Sections:
5
+ 1. Core helpers (_to_numpy, slicing, etc.)
6
+ 2. 3D cross-section and orthogonal-view plotting
7
+ 3. FDTD time-domain animation (convergence viz)
8
+ 4. Differentiable FDFD gradient visualization
9
+ """
10
+
11
+ import os
12
+ import time
13
+ import numpy as np
14
+ import torch
15
+ import matplotlib.pyplot as plt
16
+ from IPython.display import display, clear_output
17
+
18
+ FIGURES_DIR = os.path.join(os.path.dirname(__file__), 'figures')
19
+ os.makedirs(FIGURES_DIR, exist_ok=True)
20
+
21
+
22
+ # ============================================================================
23
+ # 1. Core helpers
24
+ # ============================================================================
25
+
26
+ def _to_numpy(data):
27
+ if isinstance(data, torch.Tensor):
28
+ return data.detach().cpu().numpy()
29
+ return np.asarray(data)
30
+
31
+
32
+ def _to_1d_numpy(coords):
33
+ coords_np = _to_numpy(coords).astype(float)
34
+ if coords_np.ndim != 1:
35
+ raise ValueError("Coordinate arrays must be 1D")
36
+ return coords_np
37
+
38
+
39
+ def nearest_index(coords, position):
40
+ coords_np = _to_1d_numpy(coords)
41
+ return int(np.argmin(np.abs(coords_np - position)))
42
+
43
+
44
+ def slice_extent(x_coords, y_coords, shape):
45
+ x = _to_1d_numpy(x_coords)[: shape[0]]
46
+ y = _to_1d_numpy(y_coords)[: shape[1]]
47
+
48
+ def _axis_extent(values, count):
49
+ if count == 0:
50
+ return 0.0, 0.0
51
+ if len(values) == 1:
52
+ return float(values[0]), float(values[0])
53
+ step = float(values[1] - values[0])
54
+ return float(values[0]), float(values[0] + step * count)
55
+
56
+ x0, x1 = _axis_extent(x, shape[0])
57
+ y0, y1 = _axis_extent(y, shape[1])
58
+ return [x0, x1, y0, y1]
59
+
60
+
61
+ # ============================================================================
62
+ # 2. 3D cross-section and orthogonal-view plotting
63
+ # ============================================================================
64
+
65
+ def extract_orthogonal_slice(data, axis, position, x_coords, y_coords, z_coords):
66
+ axis = axis.lower()
67
+ if axis == "x":
68
+ idx = nearest_index(x_coords, position)
69
+ slice_data = data[idx, :, :]
70
+ plane_x = y_coords[: slice_data.shape[0]]
71
+ plane_y = z_coords[: slice_data.shape[1]]
72
+ xlabel, ylabel = "Y [m]", "Z [m]"
73
+ elif axis == "y":
74
+ idx = nearest_index(y_coords, position)
75
+ slice_data = data[:, idx, :]
76
+ plane_x = x_coords[: slice_data.shape[0]]
77
+ plane_y = z_coords[: slice_data.shape[1]]
78
+ xlabel, ylabel = "X [m]", "Z [m]"
79
+ elif axis == "z":
80
+ idx = nearest_index(z_coords, position)
81
+ slice_data = data[:, :, idx]
82
+ plane_x = x_coords[: slice_data.shape[0]]
83
+ plane_y = y_coords[: slice_data.shape[1]]
84
+ xlabel, ylabel = "X [m]", "Y [m]"
85
+ else:
86
+ raise ValueError("axis must be 'x', 'y', or 'z'")
87
+
88
+ return {
89
+ "index": idx,
90
+ "slice": slice_data,
91
+ "x_coords": plane_x,
92
+ "y_coords": plane_y,
93
+ "xlabel": xlabel,
94
+ "ylabel": ylabel,
95
+ "extent": slice_extent(plane_x, plane_y, slice_data.shape),
96
+ }
97
+
98
+
99
+ def plot_slice_image(
100
+ data, *, extent, xlabel, ylabel, title, colorbar_label,
101
+ figsize=(10, 8), cmap="inferno", vmin=None, vmax=None,
102
+ interpolation="bilinear", aspect="auto",
103
+ ):
104
+ fig, ax = plt.subplots(figsize=figsize)
105
+ fig.patch.set_facecolor("black")
106
+ ax.set_facecolor("black")
107
+ image = ax.imshow(
108
+ _to_numpy(data).T, cmap=cmap, origin="lower", extent=extent,
109
+ vmin=vmin, vmax=vmax, interpolation=interpolation, aspect=aspect,
110
+ )
111
+ ax.set_xlabel(xlabel, color="white")
112
+ ax.set_ylabel(ylabel, color="white")
113
+ ax.set_title(title, color="white", pad=20)
114
+ ax.tick_params(colors="white")
115
+ cbar = plt.colorbar(image, ax=ax, label=colorbar_label)
116
+ cbar.ax.yaxis.set_tick_params(color="white")
117
+ cbar.ax.tick_params(colors="white")
118
+ cbar.set_label(colorbar_label, color="white")
119
+ plt.setp(plt.getp(cbar.ax.axes, "yticklabels"), color="white")
120
+ plt.grid(False)
121
+ plt.tight_layout()
122
+ return fig, ax
123
+
124
+
125
+ def plot_cross_section_panels(
126
+ *, field_slice, permittivity_slice, x_coords, y_coords,
127
+ xlabel, ylabel, field_title, field_cbar_label, permittivity_title,
128
+ figsize=(12, 5), save_path=None, verbose=False,
129
+ contour_levels=5, contour_color="white", field_vmin=None,
130
+ ):
131
+ field_np = _to_numpy(field_slice)
132
+ eps_np = _to_numpy(permittivity_slice)
133
+ extent = slice_extent(x_coords, y_coords, field_np.shape)
134
+
135
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
136
+ fig.patch.set_facecolor("black")
137
+
138
+ im1 = ax1.imshow(field_np.T, cmap="inferno", origin="lower", extent=extent,
139
+ interpolation="bilinear", vmin=field_vmin)
140
+ ax1.set_xlabel(xlabel, color="white")
141
+ ax1.set_ylabel(ylabel, color="white")
142
+ ax1.set_title(field_title, color="white")
143
+ ax1.tick_params(colors="white")
144
+ ax1.set_facecolor("black")
145
+ ax1.contour(
146
+ _to_1d_numpy(x_coords)[: eps_np.shape[0]],
147
+ _to_1d_numpy(y_coords)[: eps_np.shape[1]],
148
+ eps_np.T, levels=contour_levels, colors=contour_color,
149
+ linestyles="--", alpha=0.7,
150
+ )
151
+ cbar1 = plt.colorbar(im1, ax=ax1)
152
+ cbar1.set_label(field_cbar_label, color="white")
153
+ cbar1.ax.tick_params(colors="white")
154
+
155
+ im2 = ax2.imshow(eps_np.T, cmap="viridis", origin="lower", extent=extent,
156
+ interpolation="bilinear")
157
+ ax2.set_xlabel(xlabel, color="white")
158
+ ax2.set_ylabel(ylabel, color="white")
159
+ ax2.set_title(permittivity_title, color="white")
160
+ ax2.tick_params(colors="white")
161
+ ax2.set_facecolor("black")
162
+ cbar2 = plt.colorbar(im2, ax=ax2)
163
+ cbar2.set_label("Relative permittivity", color="white")
164
+ cbar2.ax.tick_params(colors="white")
165
+
166
+ plt.tight_layout()
167
+ if save_path:
168
+ plt.savefig(save_path, dpi=150, facecolor="black", edgecolor="none",
169
+ bbox_inches="tight")
170
+ plt.close(fig)
171
+ if verbose:
172
+ print(f"Saved to: {save_path}")
173
+ return None
174
+ plt.show()
175
+ return fig
176
+
177
+
178
+ def plot_orthogonal_views(
179
+ *, field_data, permittivity, x_coords, y_coords, z_coords,
180
+ position=0.0, field_log_scale=True, figsize=(18, 5), save_path=None,
181
+ vmin_db=-60, verbose=False, suptitle="Total Electric Field Intensity |E|",
182
+ ):
183
+ fig, axes = plt.subplots(1, 3, figsize=figsize)
184
+ fig.patch.set_facecolor("black")
185
+
186
+ for ax, axis in zip(axes, ("x", "y", "z")):
187
+ field_info = extract_orthogonal_slice(field_data, axis, position,
188
+ x_coords, y_coords, z_coords)
189
+ eps_info = extract_orthogonal_slice(permittivity, axis, position,
190
+ x_coords, y_coords, z_coords)
191
+ field_slice = _to_numpy(field_info["slice"])
192
+ eps_slice = _to_numpy(eps_info["slice"])
193
+
194
+ if field_log_scale:
195
+ field_slice = np.where(field_slice > 1e-12, field_slice, 1e-12)
196
+ field_max = np.max(field_slice)
197
+ if field_max > 0:
198
+ field_slice = 20 * np.log10(field_slice / field_max)
199
+ cbar_label = "|E| (dB)"
200
+ vmin = vmin_db
201
+ else:
202
+ cbar_label = "|E| (V/m)"
203
+ vmin = None
204
+
205
+ extent = field_info["extent"]
206
+ image = ax.imshow(field_slice.T, cmap="inferno", origin="lower",
207
+ extent=extent, interpolation="bilinear", vmin=vmin)
208
+ ax.contour(
209
+ _to_1d_numpy(field_info["x_coords"])[: eps_slice.shape[0]],
210
+ _to_1d_numpy(field_info["y_coords"])[: eps_slice.shape[1]],
211
+ eps_slice.T, levels=[1.5], colors="cyan", linestyles="--", alpha=0.7,
212
+ )
213
+ ax.set_xlabel(field_info["xlabel"], color="white")
214
+ ax.set_ylabel(field_info["ylabel"], color="white")
215
+ ax.set_title(f"{axis.upper()}={position:.2f}m", color="white", fontsize=14)
216
+ ax.tick_params(colors="white")
217
+ ax.set_facecolor("black")
218
+ cbar = plt.colorbar(image, ax=ax, shrink=0.8)
219
+ cbar.set_label(cbar_label, color="white")
220
+ cbar.ax.tick_params(colors="white")
221
+
222
+ plt.suptitle(suptitle, color="white", fontsize=16, y=1.02)
223
+ plt.tight_layout()
224
+ if save_path:
225
+ plt.savefig(save_path, dpi=150, facecolor="black", edgecolor="none",
226
+ bbox_inches="tight")
227
+ plt.close(fig)
228
+ if verbose:
229
+ print(f"Saved to: {save_path}")
230
+ return None
231
+ plt.show()
232
+ return fig
233
+
234
+
235
+ # ============================================================================
236
+ # 3. FDTD time-domain animation
237
+ # ============================================================================
238
+
239
+ def visualize_slice(solver, n, slice_type='xy', slice_index=None):
240
+ """Visualize a field slice during FDTD time stepping."""
241
+ if slice_index is None:
242
+ slice_index = solver.Nz // 2 if slice_type == 'xy' else solver.Ny // 2
243
+
244
+ Ex_c = 0.5 * (solver.Ex[:, :-1, :-1] + solver.Ex[:, 1:, :-1])
245
+ Ey_c = 0.5 * (solver.Ey[:-1, :, :-1] + solver.Ey[1:, :, :-1])
246
+ Ez_c = 0.5 * (solver.Ez[:-1, :-1, :] + solver.Ez[1:, :-1, :])
247
+ E_magnitude = torch.sqrt(Ex_c**2 + Ey_c**2 + Ez_c**2)
248
+
249
+ slices = {
250
+ 'xy': (E_magnitude[:, :, slice_index],
251
+ (solver.scene.domain_range[0], solver.scene.domain_range[1]),
252
+ (solver.scene.domain_range[2], solver.scene.domain_range[3]),
253
+ 'X [m]', 'Y [m]'),
254
+ 'xz': (E_magnitude[:, slice_index, :],
255
+ (solver.scene.domain_range[0], solver.scene.domain_range[1]),
256
+ (solver.scene.domain_range[4], solver.scene.domain_range[5]),
257
+ 'X [m]', 'Z [m]'),
258
+ 'yz': (E_magnitude[slice_index, :, :],
259
+ (solver.scene.domain_range[2], solver.scene.domain_range[3]),
260
+ (solver.scene.domain_range[4], solver.scene.domain_range[5]),
261
+ 'Y [m]', 'Z [m]'),
262
+ }
263
+ field_tensor, x_range, y_range, xlabel, ylabel = slices[slice_type]
264
+ field_slice = field_tensor.detach().cpu().numpy()
265
+
266
+ plt.figure(figsize=(10, 8))
267
+ plt.gca().set_facecolor('black')
268
+ plt.gcf().set_facecolor('black')
269
+ plt.imshow(field_slice.T, cmap='inferno', origin='lower',
270
+ extent=[x_range[0], x_range[1], y_range[0], y_range[1]],
271
+ vmin=0, vmax=0.5, aspect='auto', interpolation='bilinear')
272
+ plt.xlabel(xlabel, color='white')
273
+ plt.ylabel(ylabel, color='white')
274
+ t_ns = n * solver.dt * 1e9
275
+ plt.title(f'3D FDTD - {slice_type.upper()} slice, t = {t_ns:.2f} ns',
276
+ color='white', pad=20)
277
+ plt.tick_params(colors='white', which='both')
278
+ cbar = plt.colorbar(label='|E| field magnitude')
279
+ cbar.ax.yaxis.set_tick_params(color='white')
280
+ cbar.set_label('|E| field magnitude', color='white')
281
+ cbar.ax.tick_params(colors='white')
282
+ plt.setp(plt.getp(cbar.ax.axes, 'yticklabels'), color='white')
283
+ plt.grid(False)
284
+ plt.tight_layout()
285
+ display(plt.gcf())
286
+ clear_output(wait=True)
287
+ plt.close()
288
+ time.sleep(0.01)
289
+
290
+
291
+ def visualize_material_slice(solver, component='epsilon', slice_type='xy', slice_index=None):
292
+ """Visualize material distribution (epsilon or mu)."""
293
+ assert component in ['epsilon', 'mu']
294
+ field = solver.epsilon_r if component == 'epsilon' else solver.mu_r
295
+
296
+ if slice_index is None:
297
+ slice_index = solver.Nz // 2 if slice_type == 'xy' else solver.Ny // 2
298
+
299
+ slices = {
300
+ 'xy': (field[:, :, slice_index],
301
+ (solver.scene.domain_range[0], solver.scene.domain_range[1]),
302
+ (solver.scene.domain_range[2], solver.scene.domain_range[3]),
303
+ 'X [m]', 'Y [m]'),
304
+ 'xz': (field[:, slice_index, :],
305
+ (solver.scene.domain_range[0], solver.scene.domain_range[1]),
306
+ (solver.scene.domain_range[4], solver.scene.domain_range[5]),
307
+ 'X [m]', 'Z [m]'),
308
+ 'yz': (field[slice_index, :, :],
309
+ (solver.scene.domain_range[2], solver.scene.domain_range[3]),
310
+ (solver.scene.domain_range[4], solver.scene.domain_range[5]),
311
+ 'Y [m]', 'Z [m]'),
312
+ }
313
+ if slice_type not in slices:
314
+ raise ValueError("slice_type must be 'xy', 'xz', or 'yz'")
315
+ field_tensor, x_range, y_range, xlabel, ylabel = slices[slice_type]
316
+ field_slice = field_tensor.detach().cpu().numpy()
317
+
318
+ plt.figure(figsize=(10, 8))
319
+ plt.gca().set_facecolor('black')
320
+ plt.gcf().set_facecolor('black')
321
+ plt.imshow(field_slice.T, cmap='viridis', origin='lower',
322
+ extent=[x_range[0], x_range[1], y_range[0], y_range[1]], aspect='auto')
323
+ plt.xlabel(xlabel, color='white')
324
+ plt.ylabel(ylabel, color='white')
325
+ plt.title(f'{component}r distribution ({slice_type.upper()} slice)', color='white', pad=20)
326
+ plt.tick_params(colors='white')
327
+ cbar = plt.colorbar(label=f'{component}r value')
328
+ cbar.ax.yaxis.set_tick_params(color='white')
329
+ cbar.set_label(f'{component}r', color='white')
330
+ cbar.ax.tick_params(colors='white')
331
+ plt.setp(plt.getp(cbar.ax.axes, 'yticklabels'), color='white')
332
+ plt.grid(False)
333
+ plt.tight_layout()
334
+ plt.show()
335
+
336
+
337
+ # ============================================================================
338
+ # 4. Differentiable FDFD gradient visualization
339
+ # ============================================================================
340
+
341
+ def plot_epsilon_gradient_field(circle_center_x=0.0, circle_center_y=0.0, radius=0.2,
342
+ permittivity_value=4.0, grid_spacing=0.01,
343
+ domain_range=(-1.0, 1.0, -1.0, 1.0), device='cpu',
344
+ save_name='epsilon_gradient_field.png'):
345
+ x_start, x_end, y_start, y_end = domain_range
346
+ x = torch.linspace(x_start, x_end, int((x_end - x_start) / grid_spacing), device=device)
347
+ y = torch.linspace(y_start, y_end, int((y_end - y_start) / grid_spacing), device=device)
348
+ xx, yy = torch.meshgrid(x, y, indexing='ij')
349
+ Nx, Ny = len(x), len(y)
350
+ aa_sharpness = 1.0 / grid_spacing
351
+
352
+ theta = torch.tensor(circle_center_x, dtype=torch.float32, device=device, requires_grad=True)
353
+ cy = torch.tensor(circle_center_y, dtype=torch.float32, device=device)
354
+ r = torch.tensor(radius, dtype=torch.float32, device=device)
355
+
356
+ dist = torch.sqrt((xx - theta)**2 + (yy - cy)**2)
357
+ sdf = dist - r
358
+ smooth_mask = torch.sigmoid(-sdf * aa_sharpness)
359
+ epsilon = 1.0 * (1 - smooth_mask) + permittivity_value * smooth_mask
360
+
361
+ with torch.no_grad():
362
+ sigmoid_val = smooth_mask
363
+ d_sigmoid_d_sdf = sigmoid_val * (1 - sigmoid_val) * (-aa_sharpness)
364
+ d_dist_d_theta = (theta - xx) / (dist + 1e-10)
365
+ grad_field = (permittivity_value - 1.0) * d_sigmoid_d_sdf * d_dist_d_theta
366
+
367
+ fig, axes = plt.subplots(1, 3, figsize=(15, 5))
368
+ im1 = axes[0].imshow(epsilon.detach().cpu().numpy().T, origin='lower',
369
+ extent=domain_range, cmap='viridis')
370
+ axes[0].set_title(f'Epsilon Field (circle at theta={circle_center_x})')
371
+ axes[0].set_xlabel('X [m]'); axes[0].set_ylabel('Y [m]')
372
+ plt.colorbar(im1, ax=axes[0], label='epsilon')
373
+ axes[0].add_patch(plt.Circle((circle_center_x, circle_center_y), radius,
374
+ fill=False, color='white', linestyle='--', linewidth=2))
375
+
376
+ grad_np = grad_field.cpu().numpy().T
377
+ vmax = np.abs(grad_np).max()
378
+ im2 = axes[1].imshow(grad_np, origin='lower', extent=domain_range,
379
+ cmap='RdBu_r', vmin=-vmax, vmax=vmax)
380
+ axes[1].set_title(r'$\partial \epsilon / \partial \theta$')
381
+ axes[1].set_xlabel('X [m]'); axes[1].set_ylabel('Y [m]')
382
+ plt.colorbar(im2, ax=axes[1])
383
+ axes[1].add_patch(plt.Circle((circle_center_x, circle_center_y), radius,
384
+ fill=False, color='black', linestyle='--', linewidth=2))
385
+
386
+ im3 = axes[2].imshow(np.abs(grad_np), origin='lower', extent=domain_range, cmap='hot')
387
+ axes[2].set_title(r'$|\partial \epsilon / \partial \theta|$')
388
+ axes[2].set_xlabel('X [m]'); axes[2].set_ylabel('Y [m]')
389
+ plt.colorbar(im3, ax=axes[2])
390
+ axes[2].add_patch(plt.Circle((circle_center_x, circle_center_y), radius,
391
+ fill=False, color='white', linestyle='--', linewidth=2))
392
+ plt.tight_layout()
393
+ save_path = os.path.join(FIGURES_DIR, save_name)
394
+ plt.savefig(save_path, dpi=150, bbox_inches='tight')
395
+ plt.show()
396
+ return grad_field, epsilon
397
+
398
+
399
+ def plot_gradient_vs_theta(theta_values=None, target_point=(100, 100),
400
+ circle_center_y=0.0, radius=0.2,
401
+ permittivity_value=4.0, grid_spacing=0.01,
402
+ domain_range=(-1.0, 1.0, -1.0, 1.0), device='cpu',
403
+ save_name='epsilon_gradient_vs_theta.png'):
404
+ if theta_values is None:
405
+ theta_values = np.linspace(-0.5, 0.5, 100)
406
+
407
+ x_start, x_end, y_start, y_end = domain_range
408
+ x = torch.linspace(x_start, x_end, int((x_end - x_start) / grid_spacing), device=device)
409
+ y = torch.linspace(y_start, y_end, int((y_end - y_start) / grid_spacing), device=device)
410
+ xx, yy = torch.meshgrid(x, y, indexing='ij')
411
+ aa_sharpness = 1.0 / grid_spacing
412
+ cy = torch.tensor(circle_center_y, dtype=torch.float32, device=device)
413
+ r = torch.tensor(radius, dtype=torch.float32, device=device)
414
+ ti, tj = target_point
415
+
416
+ epsilon_values, gradient_values = [], []
417
+ for theta_val in theta_values:
418
+ theta = torch.tensor(theta_val, dtype=torch.float32, device=device, requires_grad=True)
419
+ dist = torch.sqrt((xx - theta)**2 + (yy - cy)**2)
420
+ sdf = dist - r
421
+ smooth_mask = torch.sigmoid(-sdf * aa_sharpness)
422
+ epsilon = 1.0 * (1 - smooth_mask) + permittivity_value * smooth_mask
423
+ eps_at_point = epsilon[ti, tj]
424
+ epsilon_values.append(eps_at_point.item())
425
+ eps_at_point.backward()
426
+ gradient_values.append(theta.grad.item())
427
+
428
+ fig, axes = plt.subplots(1, 2, figsize=(12, 5))
429
+ target_x, target_y = x[ti].item(), y[tj].item()
430
+
431
+ axes[0].plot(theta_values, epsilon_values, 'b-', linewidth=2)
432
+ axes[0].axhline(y=1.0, color='gray', linestyle='--', label='background')
433
+ axes[0].axhline(y=permittivity_value, color='orange', linestyle='--', label=f'circle eps={permittivity_value}')
434
+ axes[0].set_xlabel(r'$\theta$'); axes[0].set_ylabel(r'$\epsilon$')
435
+ axes[0].set_title(f'Epsilon at ({target_x:.2f}, {target_y:.2f}) vs theta')
436
+ axes[0].legend(); axes[0].grid(True, alpha=0.3)
437
+
438
+ axes[1].plot(theta_values, gradient_values, 'r-', linewidth=2)
439
+ axes[1].axhline(y=0, color='gray', linestyle='--')
440
+ axes[1].set_xlabel(r'$\theta$'); axes[1].set_ylabel(r'$\partial \epsilon / \partial \theta$')
441
+ axes[1].set_title(f'Gradient at ({target_x:.2f}, {target_y:.2f}) vs theta')
442
+ axes[1].grid(True, alpha=0.3)
443
+
444
+ plt.tight_layout()
445
+ save_path = os.path.join(FIGURES_DIR, save_name)
446
+ plt.savefig(save_path, dpi=150, bbox_inches='tight')
447
+ plt.show()
448
+ return theta_values, epsilon_values, gradient_values
449
+
450
+
451
+ def plot_field_results(E_field, scene, polarization='TM', field_component='abs', save_name=None):
452
+ if E_field is None:
453
+ print("E_field is None"); return
454
+ field_cpu = E_field.cpu()
455
+ if field_component == 'real':
456
+ field_to_plot = torch.real(field_cpu)
457
+ elif field_component == 'imag':
458
+ field_to_plot = torch.imag(field_cpu)
459
+ elif field_component == 'abs':
460
+ field_to_plot = torch.abs(field_cpu)
461
+ else:
462
+ raise ValueError("field_component must be 'real', 'imag', or 'abs'")
463
+ label = 'Ez' if polarization == 'TM' else 'Hz'
464
+ title = f"{field_component.capitalize()} of {label}"
465
+
466
+ x_start, x_end, y_start, y_end = scene.domain_range
467
+ aspect_ratio = (y_end - y_start) / (x_end - x_start)
468
+ fig_width = 8
469
+ plt.figure(figsize=(fig_width + 1.5, fig_width * aspect_ratio))
470
+ plt.gca().set_facecolor('black'); plt.gcf().set_facecolor('black')
471
+ im = plt.imshow(np.abs(field_to_plot.T.numpy()), cmap='jet', origin='lower',
472
+ extent=scene.domain_range, interpolation='bilinear')
473
+ cbar = plt.colorbar(im); cbar.set_label(f"{label} amplitude", color='white')
474
+ cbar.ax.tick_params(colors='white')
475
+ plt.xticks(color='white'); plt.yticks(color='white')
476
+ plt.xlabel('X [m]', color='white'); plt.ylabel('Y [m]', color='white')
477
+ plt.contour(scene.x.cpu(), scene.y.cpu(), scene.permittivity.detach().cpu().T,
478
+ levels=1, colors='w', linestyles='--')
479
+ plt.title(title, color='white', pad=20)
480
+ if save_name:
481
+ save_path = os.path.join(FIGURES_DIR, save_name)
482
+ plt.savefig(save_path, dpi=150, bbox_inches='tight', facecolor='black')
483
+ plt.show()
484
+ return E_field
485
+
486
+
487
+ def plot_dE_dtheta_field(dE_dtheta, scene, save_name='dE_dtheta_field.png'):
488
+ if dE_dtheta is None:
489
+ print("dE_dtheta is None"); return None
490
+ dE_np = dE_dtheta.cpu().numpy().T
491
+ fig, axes = plt.subplots(1, 3, figsize=(15, 5))
492
+
493
+ for ax, (data, cmap, title) in zip(axes, [
494
+ (np.real(dE_np), 'RdBu_r', r'Re($\partial E / \partial \theta$)'),
495
+ (np.imag(dE_np), 'RdBu_r', r'Im($\partial E / \partial \theta$)'),
496
+ (np.abs(dE_np), 'hot', r'$|\partial E / \partial \theta|$'),
497
+ ]):
498
+ vmax = np.abs(data).max() if cmap == 'RdBu_r' else None
499
+ kwargs = dict(vmin=-vmax, vmax=vmax) if vmax else {}
500
+ im = ax.imshow(data, origin='lower', extent=scene.domain_range, cmap=cmap, **kwargs)
501
+ ax.set_title(title); ax.set_xlabel('X [m]'); ax.set_ylabel('Y [m]')
502
+ plt.colorbar(im, ax=ax)
503
+ contour_color = 'black' if cmap == 'RdBu_r' else 'white'
504
+ ax.contour(scene.x.cpu().numpy(), scene.y.cpu().numpy(),
505
+ scene.permittivity.detach().cpu().numpy().T,
506
+ levels=1, colors=contour_color, linestyles='--', linewidths=1)
507
+
508
+ plt.tight_layout()
509
+ save_path = os.path.join(FIGURES_DIR, save_name)
510
+ plt.savefig(save_path, dpi=150, bbox_inches='tight')
511
+ plt.show()
512
+ return dE_dtheta
513
+
514
+
515
+ def compare_adjoint_vs_fd(deps_dtheta, E_field, dE_adj, dE_fd, scene,
516
+ save_name='adjoint_vs_fd.png'):
517
+ deps_np = deps_dtheta.detach().cpu().numpy().T
518
+ E_np = E_field.cpu().numpy().T
519
+ dE_adj_np = dE_adj.cpu().numpy().T
520
+ dE_fd_np = dE_fd.cpu().numpy().T
521
+ eps_data = scene.permittivity.detach().cpu().numpy().T
522
+ x_coords = scene.x.cpu().numpy()
523
+ y_coords = scene.y.cpu().numpy()
524
+ ext = scene.domain_range
525
+
526
+ def add_contour(ax, color='white'):
527
+ ax.contour(x_coords, y_coords, eps_data, levels=1,
528
+ colors=color, linestyles='--', linewidths=1)
529
+
530
+ fig, axes = plt.subplots(4, 3, figsize=(15, 16))
531
+
532
+ # Row 1: d_eps/d_theta
533
+ vmax = np.abs(deps_np).max()
534
+ im = axes[0, 0].imshow(deps_np, origin='lower', extent=ext, cmap='RdBu_r', vmin=-vmax, vmax=vmax)
535
+ axes[0, 0].set_title(r'd$\epsilon$/d$\theta$'); plt.colorbar(im, ax=axes[0, 0]); add_contour(axes[0, 0], 'black')
536
+ im = axes[0, 1].imshow(np.abs(deps_np), origin='lower', extent=ext, cmap='hot')
537
+ axes[0, 1].set_title(r'|d$\epsilon$/d$\theta$|'); plt.colorbar(im, ax=axes[0, 1]); add_contour(axes[0, 1])
538
+ im = axes[0, 2].imshow(eps_data, origin='lower', extent=ext, cmap='viridis')
539
+ axes[0, 2].set_title(r'$\epsilon$ Field'); plt.colorbar(im, ax=axes[0, 2]); add_contour(axes[0, 2])
540
+
541
+ # Row 2: E field
542
+ for col, (data, title) in enumerate([
543
+ (np.real(E_np), 'E Real'), (np.imag(E_np), 'E Imag'), (np.abs(E_np), 'E Magnitude')
544
+ ]):
545
+ cmap = 'hot' if col == 2 else 'RdBu_r'
546
+ vmax = np.abs(data).max() if cmap == 'RdBu_r' else None
547
+ kwargs = dict(vmin=-vmax, vmax=vmax) if vmax else {}
548
+ im = axes[1, col].imshow(data, origin='lower', extent=ext, cmap=cmap, **kwargs)
549
+ axes[1, col].set_title(title); plt.colorbar(im, ax=axes[1, col])
550
+ add_contour(axes[1, col], 'black' if cmap == 'RdBu_r' else 'white')
551
+
552
+ # Rows 3-4: Adjoint vs FD
553
+ display_range = 0.3
554
+ E_mag = np.abs(E_np)
555
+ for row, (dE_np, label) in [(2, (dE_adj_np, 'Adjoint')), (3, (dE_fd_np, 'Finite Diff'))]:
556
+ for col, (data, title) in enumerate([
557
+ (np.real(dE_np), f'{label} Real'), (np.imag(dE_np), f'{label} Imag'),
558
+ (np.real(np.conj(E_np) * dE_np) / (E_mag + 1e-10), f'{label} d|E|/dtheta')
559
+ ]):
560
+ im = axes[row, col].imshow(data, origin='lower', extent=ext,
561
+ cmap='RdBu_r', vmin=-display_range, vmax=display_range)
562
+ axes[row, col].set_title(title); plt.colorbar(im, ax=axes[row, col])
563
+ add_contour(axes[row, col], 'black')
564
+
565
+ plt.tight_layout()
566
+ save_path = os.path.join(FIGURES_DIR, save_name)
567
+ plt.savefig(save_path, dpi=150, bbox_inches='tight')
568
+ plt.show()