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.
- witwin/maxwell/__init__.py +164 -0
- witwin/maxwell/adapters/__init__.py +1 -0
- witwin/maxwell/adapters/gds.py +203 -0
- witwin/maxwell/adapters/tidy3d.py +1669 -0
- witwin/maxwell/adjoint_inputs.py +70 -0
- witwin/maxwell/compiler/__init__.py +11 -0
- witwin/maxwell/compiler/materials.py +1355 -0
- witwin/maxwell/compiler/monitors.py +164 -0
- witwin/maxwell/compiler/sources.py +222 -0
- witwin/maxwell/fdfd/__init__.py +3 -0
- witwin/maxwell/fdfd/adjoint/__init__.py +3 -0
- witwin/maxwell/fdfd/adjoint/bridge.py +180 -0
- witwin/maxwell/fdfd/ams.py +300 -0
- witwin/maxwell/fdfd/krylov.py +280 -0
- witwin/maxwell/fdfd/plotting.py +110 -0
- witwin/maxwell/fdfd/postprocess.py +33 -0
- witwin/maxwell/fdfd/solver.py +1057 -0
- witwin/maxwell/fdtd/__init__.py +4 -0
- witwin/maxwell/fdtd/adjoint/__init__.py +21 -0
- witwin/maxwell/fdtd/adjoint/bridge.py +746 -0
- witwin/maxwell/fdtd/adjoint/capabilities.py +29 -0
- witwin/maxwell/fdtd/adjoint/core.py +3614 -0
- witwin/maxwell/fdtd/adjoint/dispatch.py +684 -0
- witwin/maxwell/fdtd/adjoint/native.py +2561 -0
- witwin/maxwell/fdtd/adjoint/profiler.py +195 -0
- witwin/maxwell/fdtd/adjoint/reverse_common.py +207 -0
- witwin/maxwell/fdtd/adjoint/seeds.py +524 -0
- witwin/maxwell/fdtd/boundary/__init__.py +37 -0
- witwin/maxwell/fdtd/boundary/bloch.py +90 -0
- witwin/maxwell/fdtd/boundary/common.py +41 -0
- witwin/maxwell/fdtd/boundary/cpml.py +527 -0
- witwin/maxwell/fdtd/boundary/runtime.py +209 -0
- witwin/maxwell/fdtd/checkpoint.py +227 -0
- witwin/maxwell/fdtd/coords.py +32 -0
- witwin/maxwell/fdtd/cuda/__init__.py +11 -0
- witwin/maxwell/fdtd/cuda/backend.py +3532 -0
- witwin/maxwell/fdtd/cuda/build.py +292 -0
- witwin/maxwell/fdtd/cuda/extension.cpp +2053 -0
- witwin/maxwell/fdtd/cuda/kernels/adjoint.cu +5290 -0
- witwin/maxwell/fdtd/cuda/kernels/aniso.cu +1704 -0
- witwin/maxwell/fdtd/cuda/kernels/boundary.cu +424 -0
- witwin/maxwell/fdtd/cuda/kernels/common.cu +74 -0
- witwin/maxwell/fdtd/cuda/kernels/common.cuh +82 -0
- witwin/maxwell/fdtd/cuda/kernels/dispersive.cu +782 -0
- witwin/maxwell/fdtd/cuda/kernels/electric.cu +3613 -0
- witwin/maxwell/fdtd/cuda/kernels/magnetic.cu +781 -0
- witwin/maxwell/fdtd/cuda/kernels/observers.cu +347 -0
- witwin/maxwell/fdtd/cuda/kernels/projection.cu +211 -0
- witwin/maxwell/fdtd/cuda/kernels/sources.cu +1482 -0
- witwin/maxwell/fdtd/cuda/kernels/spectral.cu +297 -0
- witwin/maxwell/fdtd/cuda/launch.h +23 -0
- witwin/maxwell/fdtd/cuda/prebuilt/witwin_maxwell_fdtd_cuda.pyd +0 -0
- witwin/maxwell/fdtd/cuda/runtime/__init__.py +6 -0
- witwin/maxwell/fdtd/cuda/runtime/dispatcher.py +6 -0
- witwin/maxwell/fdtd/cuda/runtime/graph.py +33 -0
- witwin/maxwell/fdtd/cuda/tensors.h +35 -0
- witwin/maxwell/fdtd/dispersion.py +48 -0
- witwin/maxwell/fdtd/evaluation.py +290 -0
- witwin/maxwell/fdtd/excitation/__init__.py +27 -0
- witwin/maxwell/fdtd/excitation/injection.py +1102 -0
- witwin/maxwell/fdtd/excitation/modes.py +1675 -0
- witwin/maxwell/fdtd/excitation/spatial.py +469 -0
- witwin/maxwell/fdtd/excitation/temporal.py +510 -0
- witwin/maxwell/fdtd/excitation/tfsf_apply.py +264 -0
- witwin/maxwell/fdtd/excitation/tfsf_common.py +421 -0
- witwin/maxwell/fdtd/excitation/tfsf_specs.py +490 -0
- witwin/maxwell/fdtd/excitation/tfsf_state.py +778 -0
- witwin/maxwell/fdtd/io.py +31 -0
- witwin/maxwell/fdtd/material_pullback.py +371 -0
- witwin/maxwell/fdtd/meshing/__init__.py +3 -0
- witwin/maxwell/fdtd/meshing/autogrid.py +335 -0
- witwin/maxwell/fdtd/observers.py +1453 -0
- witwin/maxwell/fdtd/plotting.py +121 -0
- witwin/maxwell/fdtd/postprocess.py +205 -0
- witwin/maxwell/fdtd/runtime/__init__.py +119 -0
- witwin/maxwell/fdtd/runtime/initialization.py +236 -0
- witwin/maxwell/fdtd/runtime/materials.py +1563 -0
- witwin/maxwell/fdtd/runtime/module_cache.py +31 -0
- witwin/maxwell/fdtd/runtime/spectral.py +581 -0
- witwin/maxwell/fdtd/runtime/stepping.py +1987 -0
- witwin/maxwell/fdtd/solver.py +502 -0
- witwin/maxwell/geometry/__init__.py +5 -0
- witwin/maxwell/geometry/polyslab.py +299 -0
- witwin/maxwell/materials.py +3 -0
- witwin/maxwell/media.py +1467 -0
- witwin/maxwell/monitors.py +743 -0
- witwin/maxwell/ports.py +113 -0
- witwin/maxwell/postprocess/RCS.py +139 -0
- witwin/maxwell/postprocess/__init__.py +42 -0
- witwin/maxwell/postprocess/diffraction.py +219 -0
- witwin/maxwell/postprocess/directivity.py +152 -0
- witwin/maxwell/postprocess/emission.py +67 -0
- witwin/maxwell/postprocess/modal.py +324 -0
- witwin/maxwell/postprocess/nfft.py +266 -0
- witwin/maxwell/postprocess/scattering_parameters.py +358 -0
- witwin/maxwell/postprocess/stratton_chu.py +1193 -0
- witwin/maxwell/result.py +967 -0
- witwin/maxwell/scene.py +1338 -0
- witwin/maxwell/simulation.py +659 -0
- witwin/maxwell/sources.py +917 -0
- witwin/maxwell/visualization/__init__.py +28 -0
- witwin/maxwell/visualization/interactive.py +484 -0
- witwin/maxwell/visualization/plots.py +568 -0
- witwin_maxwell-0.3.0.dist-info/METADATA +184 -0
- witwin_maxwell-0.3.0.dist-info/RECORD +107 -0
- witwin_maxwell-0.3.0.dist-info/WHEEL +4 -0
- 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()
|