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,300 @@
|
|
|
1
|
+
"""Auxiliary-space Maxwell (Hiptmair-Xu) preconditioner for the FDFD system.
|
|
2
|
+
|
|
3
|
+
Structure (all GPU, CuPy sparse):
|
|
4
|
+
|
|
5
|
+
- Gradient auxiliary space: the PML-stretched discrete gradient
|
|
6
|
+
``P_g = D S_e^-1 G`` maps nodal potentials onto the null space of the
|
|
7
|
+
scaled curl-curl part — exact to round-off in the interior; the outermost
|
|
8
|
+
cell layer carries an inherent truncation defect. The Galerkin operator
|
|
9
|
+
``A_g = P_g^T A P_g ~= k0^2 P_g^T E P_g`` is a definite nodal Poisson-type
|
|
10
|
+
operator.
|
|
11
|
+
- Vector auxiliary space: component-wise nodal-to-edge averaging
|
|
12
|
+
``P_c = D S_e^-1 Pi_c``; the Galerkin blocks are built from the
|
|
13
|
+
complex-shifted operator ``A + i beta k0^2 E`` (shifted-Laplacian damping
|
|
14
|
+
of the indefinite wave part) so multigrid smoothing is stable.
|
|
15
|
+
- Auxiliary solves: geometric multigrid V(1,1) cycles on the structured nodal
|
|
16
|
+
grid (Galerkin RAP coarsening, damped-Jacobi smoothing, dense solve on the
|
|
17
|
+
coarsest level).
|
|
18
|
+
- Composition: symmetrized multiplicative Schwarz
|
|
19
|
+
``smoother -> gradient -> vector -> gradient -> smoother``. Every stage is
|
|
20
|
+
complex-symmetric, so the preconditioner is valid for SQMR.
|
|
21
|
+
|
|
22
|
+
This is the in-repo answer to the plan's 10.4 AMS item: hypre-AMS is
|
|
23
|
+
unavailable on native Windows, and the structured Yee grid lets geometric
|
|
24
|
+
multigrid replace AMG for the auxiliary problems.
|
|
25
|
+
|
|
26
|
+
STATUS: EXPERIMENTAL — measured outcome on the indefinite time-harmonic
|
|
27
|
+
operator (see plan 10.8): the gradient-space construction is exact (the
|
|
28
|
+
null-space identity holds to round-off) and its multigrid contracts, but
|
|
29
|
+
the overall Hiptmair-Xu composition is non-contractive at these
|
|
30
|
+
wavenumbers even with EXACT auxiliary solves — oblique subspace
|
|
31
|
+
corrections amplify each other on the non-Hermitian indefinite operator.
|
|
32
|
+
This matches hypre-AMS's own scope (definite curl-curl / eddy-current
|
|
33
|
+
regimes). For the indefinite regime use sqmr+ssor+precision='double'
|
|
34
|
+
(converges through 64^3) or the direct backend; large-scale indefinite
|
|
35
|
+
solves need sweeping/DDM-class methods, out of scope here.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import cupy as cp
|
|
41
|
+
import cupyx.scipy.sparse as cpsp
|
|
42
|
+
import cupyx.scipy.sparse.linalg as cupy_linalg
|
|
43
|
+
|
|
44
|
+
# Complex shift applied to the vector auxiliary operator (shifted-Laplacian
|
|
45
|
+
# damping); the gradient space is definite and needs no shift.
|
|
46
|
+
AMS_SHIFT_BETA = 0.5
|
|
47
|
+
# Damped-Jacobi weight for the fine smoother and the multigrid smoother.
|
|
48
|
+
AMS_JACOBI_OMEGA = 0.8
|
|
49
|
+
# Stop coarsening once every axis is at or below this many nodes.
|
|
50
|
+
AMS_COARSEST_NODES = 8
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _prolongation_1d(n, dtype):
|
|
54
|
+
"""Vertex-centered 1D linear prolongation (fine n -> coarse (n+1)//2)."""
|
|
55
|
+
nc = (n + 1) // 2
|
|
56
|
+
rows, cols, vals = [], [], []
|
|
57
|
+
for i in range(n):
|
|
58
|
+
if i % 2 == 0:
|
|
59
|
+
rows.append(i); cols.append(i // 2); vals.append(1.0)
|
|
60
|
+
else:
|
|
61
|
+
left = i // 2
|
|
62
|
+
right = left + 1
|
|
63
|
+
if right < nc:
|
|
64
|
+
rows.extend((i, i)); cols.extend((left, right)); vals.extend((0.5, 0.5))
|
|
65
|
+
else:
|
|
66
|
+
rows.append(i); cols.append(left); vals.append(1.0)
|
|
67
|
+
return cpsp.csr_matrix(
|
|
68
|
+
(cp.asarray(vals, dtype=dtype), (cp.asarray(rows), cp.asarray(cols))),
|
|
69
|
+
shape=(n, nc),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _prolongation_3d(shape, dtype):
|
|
74
|
+
px = _prolongation_1d(shape[0], dtype)
|
|
75
|
+
py = _prolongation_1d(shape[1], dtype)
|
|
76
|
+
pz = _prolongation_1d(shape[2], dtype)
|
|
77
|
+
return cpsp.kron(cpsp.kron(px, py), pz).tocsr()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _NodalMultigrid:
|
|
81
|
+
"""Geometric V(1,1) multigrid on a structured nodal grid with Galerkin
|
|
82
|
+
coarsening and damped-Jacobi smoothing. Complex-symmetric throughout
|
|
83
|
+
(transposes, never conjugates).
|
|
84
|
+
|
|
85
|
+
The operator is symmetrically diagonal-scaled first (S A S with
|
|
86
|
+
S = diag(A)^-1/2), which absorbs the orders-of-magnitude PML weight
|
|
87
|
+
range into the transfers; the coarsest level is solved densely with a
|
|
88
|
+
small Tikhonov term (the gradient-space operator has an exact constant
|
|
89
|
+
null space)."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, A, shape, omega=AMS_JACOBI_OMEGA):
|
|
92
|
+
self.omega = omega
|
|
93
|
+
A = A.tocsr()
|
|
94
|
+
self.scale = cp.reciprocal(cp.sqrt(A.diagonal()))
|
|
95
|
+
scale_matrix = cpsp.diags(self.scale).tocsr()
|
|
96
|
+
current_A = (scale_matrix @ (A @ scale_matrix)).tocsr()
|
|
97
|
+
current_shape = tuple(shape)
|
|
98
|
+
self.levels = []
|
|
99
|
+
while min(current_shape) > AMS_COARSEST_NODES:
|
|
100
|
+
P = _prolongation_3d(current_shape, current_A.dtype)
|
|
101
|
+
R = P.T.tocsr()
|
|
102
|
+
inv_diag = cp.reciprocal(current_A.diagonal())
|
|
103
|
+
self.levels.append((current_A, inv_diag, P, R))
|
|
104
|
+
current_A = (R @ (current_A @ P)).tocsr()
|
|
105
|
+
current_shape = tuple((s + 1) // 2 for s in current_shape)
|
|
106
|
+
coarse = current_A.toarray()
|
|
107
|
+
regularization = 1e-10 * float(cp.abs(coarse.diagonal()).max())
|
|
108
|
+
coarse = coarse + regularization * cp.eye(coarse.shape[0], dtype=coarse.dtype)
|
|
109
|
+
self.coarse_dense = coarse
|
|
110
|
+
|
|
111
|
+
def solve(self, r):
|
|
112
|
+
return self.scale * self._cycle(0, self.scale * r)
|
|
113
|
+
|
|
114
|
+
def _cycle(self, level, r):
|
|
115
|
+
if level == len(self.levels):
|
|
116
|
+
return cp.linalg.solve(self.coarse_dense, r)
|
|
117
|
+
A, inv_diag, P, R = self.levels[level]
|
|
118
|
+
z = self.omega * inv_diag * r
|
|
119
|
+
z = z + P @ self._cycle(level + 1, R @ (r - A @ z))
|
|
120
|
+
z = z + self.omega * inv_diag * (r - A @ z)
|
|
121
|
+
return z
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _edge_shapes(scene):
|
|
125
|
+
return (
|
|
126
|
+
(scene.Nx_ex, scene.Ny_ex, scene.Nz_ex),
|
|
127
|
+
(scene.Nx_ey, scene.Ny_ey, scene.Nz_ey),
|
|
128
|
+
(scene.Nx_ez, scene.Ny_ez, scene.Nz_ez),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _stretch_half_factors(solver, dtype):
|
|
133
|
+
s_x, s_y, s_z = solver._create_pml_3d()
|
|
134
|
+
s_x = cp.asarray(s_x, dtype=dtype)
|
|
135
|
+
s_y = cp.asarray(s_y, dtype=dtype)
|
|
136
|
+
s_z = cp.asarray(s_z, dtype=dtype)
|
|
137
|
+
return (
|
|
138
|
+
(s_x[:-1] + s_x[1:]) / 2,
|
|
139
|
+
(s_y[:-1] + s_y[1:]) / 2,
|
|
140
|
+
(s_z[:-1] + s_z[1:]) / 2,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _edge_stretch_weights(solver, dtype):
|
|
145
|
+
"""1/s along each edge's own axis, per component block (flat)."""
|
|
146
|
+
scene = solver.scene
|
|
147
|
+
shapes = _edge_shapes(scene)
|
|
148
|
+
sxh, syh, szh = _stretch_half_factors(solver, dtype)
|
|
149
|
+
wx = cp.broadcast_to((1.0 / sxh)[:, None, None], shapes[0]).ravel()
|
|
150
|
+
wy = cp.broadcast_to((1.0 / syh)[None, :, None], shapes[1]).ravel()
|
|
151
|
+
wz = cp.broadcast_to((1.0 / szh)[None, None, :], shapes[2]).ravel()
|
|
152
|
+
return wx, wy, wz
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _node_index(scene):
|
|
156
|
+
return cp.arange(scene.Nx * scene.Ny * scene.Nz).reshape(scene.Nx, scene.Ny, scene.Nz)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _difference_block(node, axis, dx, dtype):
|
|
160
|
+
"""Plain discrete gradient for one component: edges x nodes, +/- 1/dx."""
|
|
161
|
+
if axis == 0:
|
|
162
|
+
lo, hi = node[:-1], node[1:]
|
|
163
|
+
elif axis == 1:
|
|
164
|
+
lo, hi = node[:, :-1], node[:, 1:]
|
|
165
|
+
else:
|
|
166
|
+
lo, hi = node[:, :, :-1], node[:, :, 1:]
|
|
167
|
+
n_edges = lo.size
|
|
168
|
+
rows = cp.arange(n_edges)
|
|
169
|
+
rows2 = cp.concatenate([rows, rows])
|
|
170
|
+
cols = cp.concatenate([hi.ravel(), lo.ravel()])
|
|
171
|
+
vals = cp.concatenate([
|
|
172
|
+
cp.full(n_edges, 1.0 / dx, dtype=dtype),
|
|
173
|
+
cp.full(n_edges, -1.0 / dx, dtype=dtype),
|
|
174
|
+
])
|
|
175
|
+
return cpsp.csr_matrix((vals, (rows2, cols)), shape=(n_edges, node.size))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _average_block(node, axis, dtype):
|
|
179
|
+
"""Nodal-to-edge tangential averaging for one component: weights 1/2."""
|
|
180
|
+
if axis == 0:
|
|
181
|
+
lo, hi = node[:-1], node[1:]
|
|
182
|
+
elif axis == 1:
|
|
183
|
+
lo, hi = node[:, :-1], node[:, 1:]
|
|
184
|
+
else:
|
|
185
|
+
lo, hi = node[:, :, :-1], node[:, :, 1:]
|
|
186
|
+
n_edges = lo.size
|
|
187
|
+
rows = cp.arange(n_edges)
|
|
188
|
+
rows2 = cp.concatenate([rows, rows])
|
|
189
|
+
cols = cp.concatenate([hi.ravel(), lo.ravel()])
|
|
190
|
+
vals = cp.full(2 * n_edges, 0.5, dtype=dtype)
|
|
191
|
+
return cpsp.csr_matrix((vals, (rows2, cols)), shape=(n_edges, node.size))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _eps_face_vector(solver, dtype):
|
|
195
|
+
ex = cp.asarray(solver.material_eps_components["x"], dtype=dtype)
|
|
196
|
+
ey = cp.asarray(solver.material_eps_components["y"], dtype=dtype)
|
|
197
|
+
ez = cp.asarray(solver.material_eps_components["z"], dtype=dtype)
|
|
198
|
+
return cp.concatenate([
|
|
199
|
+
((ex[:-1] + ex[1:]) / 2).ravel(),
|
|
200
|
+
((ey[:, :-1] + ey[:, 1:]) / 2).ravel(),
|
|
201
|
+
((ez[:, :, :-1] + ez[:, :, 1:]) / 2).ravel(),
|
|
202
|
+
])
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class AMSPreconditioner:
|
|
206
|
+
def __init__(self, solver, shift_beta=AMS_SHIFT_BETA):
|
|
207
|
+
A = solver._iteration_matrix()
|
|
208
|
+
dtype = A.dtype
|
|
209
|
+
scene = solver.scene
|
|
210
|
+
dx = float(scene.dx)
|
|
211
|
+
node = _node_index(scene)
|
|
212
|
+
node_shape = (scene.Nx, scene.Ny, scene.Nz)
|
|
213
|
+
n_nodes = node.size
|
|
214
|
+
edge_counts = (scene.N_ex, scene.N_ey, scene.N_ez)
|
|
215
|
+
|
|
216
|
+
scale = solver._ensure_symmetrization_scale().astype(dtype)
|
|
217
|
+
stretch = _edge_stretch_weights(solver, dtype)
|
|
218
|
+
# per-component edge weight: d * (1/s) restricted to that block
|
|
219
|
+
offsets = (0, edge_counts[0], edge_counts[0] + edge_counts[1])
|
|
220
|
+
block_weights = [
|
|
221
|
+
scale[offsets[c]:offsets[c] + edge_counts[c]] * stretch[c]
|
|
222
|
+
for c in range(3)
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
# Shifted-Laplacian architecture: every internal stage (smoother,
|
|
226
|
+
# residual refreshes, auxiliary Galerkin operators) works on the
|
|
227
|
+
# DAMPED operator A_beta = A + i beta k0^2 E; the whole object then
|
|
228
|
+
# approximates A_beta^-1, which is the classical robust
|
|
229
|
+
# preconditioner for the indefinite wave operator A. A fine-level
|
|
230
|
+
# smoother on the undamped A itself amplifies wave modes.
|
|
231
|
+
eps_face = _eps_face_vector(solver, dtype)
|
|
232
|
+
shift = cpsp.diags(1j * shift_beta * solver.k0**2 * eps_face).tocsr()
|
|
233
|
+
self.A_shift = (A + shift).tocsr()
|
|
234
|
+
self.inv_diag = cp.reciprocal(self.A_shift.diagonal())
|
|
235
|
+
self.omega = AMS_JACOBI_OMEGA
|
|
236
|
+
|
|
237
|
+
# --- gradient auxiliary space (exact null-space complement) ---
|
|
238
|
+
grad_blocks = []
|
|
239
|
+
for c in range(3):
|
|
240
|
+
block = _difference_block(node, c, dx, dtype)
|
|
241
|
+
grad_blocks.append(cpsp.diags(block_weights[c]).tocsr() @ block)
|
|
242
|
+
self.P_grad = cpsp.vstack(grad_blocks).tocsr()
|
|
243
|
+
A_grad = (self.P_grad.T.tocsr() @ (self.A_shift @ self.P_grad)).tocsr()
|
|
244
|
+
self.mg_grad = _NodalMultigrid(A_grad, node_shape)
|
|
245
|
+
|
|
246
|
+
# --- vector auxiliary spaces ---
|
|
247
|
+
# The Galerkin block P_c^T A P_c only contains the TRANSVERSE
|
|
248
|
+
# Laplacian (the Yee (c,c) curl-curl block has no d^2/dc^2 term),
|
|
249
|
+
# which leaves whole line families unsmoothable. Following the
|
|
250
|
+
# Hiptmair-Xu identity (curl curl = -Lap + grad div on the
|
|
251
|
+
# divergence-free complement), use the FULL stretched nodal
|
|
252
|
+
# Laplacian as the vector surrogate, with the same complex shift
|
|
253
|
+
# on the mass term.
|
|
254
|
+
laplacian = (self.P_grad.T.tocsr() @ self.P_grad).tocsr() # Gs^T D^2 Gs
|
|
255
|
+
eps_diag = cpsp.diags(eps_face).tocsr()
|
|
256
|
+
k02 = dtype.type((1.0 + 1j * shift_beta)) * solver.k0**2
|
|
257
|
+
self.P_vec = []
|
|
258
|
+
self.mg_vec = []
|
|
259
|
+
for c in range(3):
|
|
260
|
+
averaging = _average_block(node, c, dtype)
|
|
261
|
+
block = cpsp.diags(block_weights[c]).tocsr() @ averaging
|
|
262
|
+
zero_above = cpsp.csr_matrix((offsets[c], n_nodes), dtype=dtype)
|
|
263
|
+
below = A.shape[0] - offsets[c] - edge_counts[c]
|
|
264
|
+
zero_below = cpsp.csr_matrix((below, n_nodes), dtype=dtype)
|
|
265
|
+
P_c = cpsp.vstack([zero_above, block, zero_below]).tocsr()
|
|
266
|
+
mass_c = (P_c.T.tocsr() @ (eps_diag @ P_c)).tocsr()
|
|
267
|
+
A_c = (k02 * mass_c - laplacian).tocsr()
|
|
268
|
+
self.P_vec.append(P_c)
|
|
269
|
+
self.mg_vec.append(_NodalMultigrid(A_c, node_shape))
|
|
270
|
+
|
|
271
|
+
def _smooth(self, z, r):
|
|
272
|
+
return z + self.omega * self.inv_diag * (r - self.A_shift @ z)
|
|
273
|
+
|
|
274
|
+
def _grad_correct(self, z, r):
|
|
275
|
+
residual = r - self.A_shift @ z
|
|
276
|
+
return z + self.P_grad @ self.mg_grad.solve(self.P_grad.T @ residual)
|
|
277
|
+
|
|
278
|
+
def _vec_correct(self, z, r):
|
|
279
|
+
residual = r - self.A_shift @ z
|
|
280
|
+
for P_c, mg in zip(self.P_vec, self.mg_vec):
|
|
281
|
+
z = z + P_c @ mg.solve(P_c.T @ residual)
|
|
282
|
+
return z
|
|
283
|
+
|
|
284
|
+
def apply(self, r):
|
|
285
|
+
# Symmetrized multiplicative Schwarz: S, G, V, G, S
|
|
286
|
+
z = self.omega * self.inv_diag * r
|
|
287
|
+
z = self._grad_correct(z, r)
|
|
288
|
+
z = self._vec_correct(z, r)
|
|
289
|
+
z = self._grad_correct(z, r)
|
|
290
|
+
z = self._smooth(z, r)
|
|
291
|
+
return z
|
|
292
|
+
|
|
293
|
+
def as_linear_operator(self):
|
|
294
|
+
return cupy_linalg.LinearOperator(
|
|
295
|
+
self.A_shift.shape, matvec=self.apply, dtype=self.A_shift.dtype
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def build_ams_preconditioner(solver):
|
|
300
|
+
return AMSPreconditioner(solver).as_linear_operator()
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""GPU-native Krylov engines for the FDFD solver.
|
|
2
|
+
|
|
3
|
+
cupyx.scipy.sparse.linalg ships only gmres/cg/cgs/minres. This module adds
|
|
4
|
+
the short-recurrence engines from the FDFD performance plan (§10.2–10.3):
|
|
5
|
+
|
|
6
|
+
- ``bicgstab`` — general non-Hermitian, no minimization property.
|
|
7
|
+
- ``tfqmr`` — transpose-free quasi-minimal residual (right-preconditioned).
|
|
8
|
+
- ``idr_s`` — IDR(s) with biorthogonalization (van Gijzen & Sonneveld).
|
|
9
|
+
- ``sqmr`` — simplified QMR for complex-symmetric systems (Freund);
|
|
10
|
+
valid for the symmetrized UPML assembly where A = A^T.
|
|
11
|
+
|
|
12
|
+
All engines run entirely on GPU (sparse matvec plus vector work), accept a
|
|
13
|
+
CSR matrix or LinearOperator for ``A`` and an optional approximate-inverse
|
|
14
|
+
preconditioner ``M`` (applied as ``M @ v``), and follow the cupyx return
|
|
15
|
+
convention: ``(x, info)`` with ``info == 0`` on convergence (relative
|
|
16
|
+
residual ``<= tol``) and the iteration count otherwise.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import cupy as cp
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _matvec(operator):
|
|
25
|
+
if operator is None:
|
|
26
|
+
return lambda v: v
|
|
27
|
+
if hasattr(operator, "matvec"):
|
|
28
|
+
return operator.matvec
|
|
29
|
+
return lambda v: operator @ v
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _bilinear(a, b):
|
|
33
|
+
"""Unconjugated bilinear form a^T b used by complex-symmetric methods."""
|
|
34
|
+
return cp.sum(a * b)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def solve(kind, A, b, M=None, tol=1e-6, maxiter=1000, s=4):
|
|
38
|
+
"""Dispatch to one of the engines by name ('bicgstab'|'tfqmr'|'idr'|'sqmr')."""
|
|
39
|
+
if kind == "bicgstab":
|
|
40
|
+
return bicgstab(A, b, M=M, tol=tol, maxiter=maxiter)
|
|
41
|
+
if kind == "tfqmr":
|
|
42
|
+
return tfqmr(A, b, M=M, tol=tol, maxiter=maxiter)
|
|
43
|
+
if kind == "idr":
|
|
44
|
+
return idr_s(A, b, M=M, tol=tol, maxiter=maxiter, s=s)
|
|
45
|
+
if kind == "sqmr":
|
|
46
|
+
return sqmr(A, b, M=M, tol=tol, maxiter=maxiter)
|
|
47
|
+
raise ValueError(f"Unknown Krylov engine {kind!r}.")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def bicgstab(A, b, M=None, tol=1e-6, maxiter=1000):
|
|
51
|
+
matvec = _matvec(A)
|
|
52
|
+
psolve = _matvec(M)
|
|
53
|
+
x = cp.zeros_like(b)
|
|
54
|
+
r = b.copy()
|
|
55
|
+
r_shadow = r.copy()
|
|
56
|
+
b_norm = float(cp.linalg.norm(b))
|
|
57
|
+
if b_norm == 0.0:
|
|
58
|
+
return x, 0
|
|
59
|
+
target = tol * b_norm
|
|
60
|
+
|
|
61
|
+
rho = alpha = omega = 1.0 + 0.0j
|
|
62
|
+
v = cp.zeros_like(b)
|
|
63
|
+
p = cp.zeros_like(b)
|
|
64
|
+
for iteration in range(1, maxiter + 1):
|
|
65
|
+
rho_next = complex(cp.vdot(r_shadow, r))
|
|
66
|
+
if rho_next == 0.0:
|
|
67
|
+
return x, iteration
|
|
68
|
+
if iteration == 1:
|
|
69
|
+
p = r.copy()
|
|
70
|
+
else:
|
|
71
|
+
beta = (rho_next / rho) * (alpha / omega)
|
|
72
|
+
p = r + beta * (p - omega * v)
|
|
73
|
+
p_hat = psolve(p)
|
|
74
|
+
v = matvec(p_hat)
|
|
75
|
+
denominator = complex(cp.vdot(r_shadow, v))
|
|
76
|
+
if denominator == 0.0:
|
|
77
|
+
return x, iteration
|
|
78
|
+
alpha = rho_next / denominator
|
|
79
|
+
residual_half = r - alpha * v
|
|
80
|
+
if float(cp.linalg.norm(residual_half)) <= target:
|
|
81
|
+
x += alpha * p_hat
|
|
82
|
+
return x, 0
|
|
83
|
+
s_hat = psolve(residual_half)
|
|
84
|
+
t = matvec(s_hat)
|
|
85
|
+
tt = complex(cp.vdot(t, t))
|
|
86
|
+
if tt == 0.0:
|
|
87
|
+
return x, iteration
|
|
88
|
+
omega = complex(cp.vdot(t, residual_half)) / tt
|
|
89
|
+
x += alpha * p_hat + omega * s_hat
|
|
90
|
+
r = residual_half - omega * t
|
|
91
|
+
if float(cp.linalg.norm(r)) <= target:
|
|
92
|
+
return x, 0
|
|
93
|
+
if omega == 0.0:
|
|
94
|
+
return x, iteration
|
|
95
|
+
rho = rho_next
|
|
96
|
+
return x, maxiter
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def sqmr(A, b, M=None, tol=1e-6, maxiter=1000, callback=None):
|
|
100
|
+
"""Freund's simplified QMR for complex-symmetric A (A = A^T, bilinear
|
|
101
|
+
inner products, no transpose matvec). M should also be symmetric
|
|
102
|
+
(Jacobi and SSOR of a symmetric matrix are). ``callback``, if given, is
|
|
103
|
+
invoked as ``callback(iteration, residual_norm)`` once per iteration."""
|
|
104
|
+
matvec = _matvec(A)
|
|
105
|
+
psolve = _matvec(M)
|
|
106
|
+
x = cp.zeros_like(b)
|
|
107
|
+
r = b.copy()
|
|
108
|
+
b_norm = float(cp.linalg.norm(b))
|
|
109
|
+
if b_norm == 0.0:
|
|
110
|
+
return x, 0
|
|
111
|
+
target = tol * b_norm
|
|
112
|
+
|
|
113
|
+
z = psolve(r)
|
|
114
|
+
tau = float(cp.linalg.norm(r))
|
|
115
|
+
rho = complex(_bilinear(r, z))
|
|
116
|
+
q = z.copy()
|
|
117
|
+
theta = 0.0
|
|
118
|
+
d = cp.zeros_like(b)
|
|
119
|
+
for iteration in range(1, maxiter + 1):
|
|
120
|
+
Aq = matvec(q)
|
|
121
|
+
sigma = complex(_bilinear(q, Aq))
|
|
122
|
+
if sigma == 0.0 or rho == 0.0:
|
|
123
|
+
return x, iteration
|
|
124
|
+
alpha = rho / sigma
|
|
125
|
+
r = r - alpha * Aq
|
|
126
|
+
r_norm = float(cp.linalg.norm(r))
|
|
127
|
+
theta_next = r_norm / tau
|
|
128
|
+
c = 1.0 / (1.0 + theta_next * theta_next) ** 0.5
|
|
129
|
+
tau = tau * theta_next * c
|
|
130
|
+
d = (c * c * theta * theta) * d + (c * c * alpha) * q
|
|
131
|
+
x = x + d
|
|
132
|
+
if callback is not None:
|
|
133
|
+
callback(iteration, r_norm)
|
|
134
|
+
if r_norm <= target:
|
|
135
|
+
return x, 0
|
|
136
|
+
z = psolve(r)
|
|
137
|
+
rho_next = complex(_bilinear(r, z))
|
|
138
|
+
beta = rho_next / rho
|
|
139
|
+
q = z + beta * q
|
|
140
|
+
rho = rho_next
|
|
141
|
+
theta = theta_next
|
|
142
|
+
return x, maxiter
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def tfqmr(A, b, M=None, tol=1e-6, maxiter=1000):
|
|
146
|
+
"""Transpose-free QMR (Freund 1993), right-preconditioned so the
|
|
147
|
+
recurrence residual matches the true residual."""
|
|
148
|
+
matvec = _matvec(A)
|
|
149
|
+
psolve = _matvec(M)
|
|
150
|
+
|
|
151
|
+
def op(v):
|
|
152
|
+
return matvec(psolve(v))
|
|
153
|
+
|
|
154
|
+
b_norm = float(cp.linalg.norm(b))
|
|
155
|
+
if b_norm == 0.0:
|
|
156
|
+
return cp.zeros_like(b), 0
|
|
157
|
+
target = tol * b_norm
|
|
158
|
+
|
|
159
|
+
z = cp.zeros_like(b) # solution of (A M^-1) z = b; x = M^-1 z
|
|
160
|
+
r = b.copy()
|
|
161
|
+
w = r.copy()
|
|
162
|
+
u1 = r.copy()
|
|
163
|
+
r_shadow = r.copy()
|
|
164
|
+
Au1 = op(u1)
|
|
165
|
+
v = Au1.copy()
|
|
166
|
+
d = cp.zeros_like(b)
|
|
167
|
+
tau = float(cp.linalg.norm(r))
|
|
168
|
+
theta = 0.0
|
|
169
|
+
eta = 0.0 + 0.0j
|
|
170
|
+
rho = complex(cp.vdot(r_shadow, r))
|
|
171
|
+
iteration = 0
|
|
172
|
+
while iteration < maxiter:
|
|
173
|
+
sigma = complex(cp.vdot(r_shadow, v))
|
|
174
|
+
if sigma == 0.0 or rho == 0.0:
|
|
175
|
+
break
|
|
176
|
+
alpha = rho / sigma
|
|
177
|
+
u2 = u1 - alpha * v
|
|
178
|
+
Au2 = op(u2)
|
|
179
|
+
converged = False
|
|
180
|
+
for u, Au in ((u1, Au1), (u2, Au2)):
|
|
181
|
+
w = w - alpha * Au
|
|
182
|
+
theta_next = float(cp.linalg.norm(w)) / tau
|
|
183
|
+
c = 1.0 / (1.0 + theta_next * theta_next) ** 0.5
|
|
184
|
+
tau = tau * theta_next * c
|
|
185
|
+
eta_next = c * c * alpha
|
|
186
|
+
d = u + (theta * theta * eta / alpha) * d
|
|
187
|
+
z = z + eta_next * d
|
|
188
|
+
theta = theta_next
|
|
189
|
+
eta = eta_next
|
|
190
|
+
iteration += 1
|
|
191
|
+
# tau * sqrt(iteration + 1) bounds the true residual
|
|
192
|
+
if tau * (iteration + 1) ** 0.5 <= target:
|
|
193
|
+
converged = True
|
|
194
|
+
break
|
|
195
|
+
if converged:
|
|
196
|
+
break
|
|
197
|
+
rho_next = complex(cp.vdot(r_shadow, w))
|
|
198
|
+
beta = rho_next / rho
|
|
199
|
+
u1 = w + beta * u2
|
|
200
|
+
Au1 = op(u1)
|
|
201
|
+
v = Au1 + beta * (Au2 + beta * v)
|
|
202
|
+
rho = rho_next
|
|
203
|
+
x = psolve(z)
|
|
204
|
+
residual = float(cp.linalg.norm(b - matvec(x)))
|
|
205
|
+
return x, 0 if residual <= target else max(iteration, 1)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def idr_s(A, b, M=None, tol=1e-6, maxiter=1000, s=4, seed=0):
|
|
209
|
+
"""IDR(s) with biorthogonalization (van Gijzen & Sonneveld, TOMS 2011).
|
|
210
|
+
|
|
211
|
+
``maxiter`` counts matvecs, comparable with the other engines. The shadow
|
|
212
|
+
space is a fixed orthonormalized complex random block (deterministic
|
|
213
|
+
``seed``)."""
|
|
214
|
+
matvec = _matvec(A)
|
|
215
|
+
psolve = _matvec(M)
|
|
216
|
+
n = b.size
|
|
217
|
+
x = cp.zeros_like(b)
|
|
218
|
+
r = b.copy()
|
|
219
|
+
b_norm = float(cp.linalg.norm(b))
|
|
220
|
+
if b_norm == 0.0:
|
|
221
|
+
return x, 0
|
|
222
|
+
target = tol * b_norm
|
|
223
|
+
|
|
224
|
+
rng = cp.random.default_rng(seed)
|
|
225
|
+
shadow = (rng.standard_normal((n, s), dtype=cp.float32)
|
|
226
|
+
+ 1j * rng.standard_normal((n, s), dtype=cp.float32)).astype(b.dtype)
|
|
227
|
+
shadow, _ = cp.linalg.qr(shadow)
|
|
228
|
+
|
|
229
|
+
G = cp.zeros((n, s), dtype=b.dtype)
|
|
230
|
+
U = cp.zeros((n, s), dtype=b.dtype)
|
|
231
|
+
Ms = cp.eye(s, dtype=b.dtype)
|
|
232
|
+
omega = 1.0 + 0.0j
|
|
233
|
+
kappa = 0.7
|
|
234
|
+
iteration = 0
|
|
235
|
+
|
|
236
|
+
r_norm = float(cp.linalg.norm(r))
|
|
237
|
+
while r_norm > target and iteration < maxiter:
|
|
238
|
+
f = shadow.conj().T @ r
|
|
239
|
+
for k in range(s):
|
|
240
|
+
c = cp.linalg.solve(Ms[k:, k:], f[k:])
|
|
241
|
+
v = r - G[:, k:] @ c
|
|
242
|
+
v = psolve(v)
|
|
243
|
+
U[:, k] = U[:, k:] @ c + omega * v
|
|
244
|
+
G[:, k] = matvec(U[:, k])
|
|
245
|
+
iteration += 1
|
|
246
|
+
for i in range(k):
|
|
247
|
+
alpha = complex(cp.vdot(shadow[:, i], G[:, k])) / complex(Ms[i, i])
|
|
248
|
+
G[:, k] -= alpha * G[:, i]
|
|
249
|
+
U[:, k] -= alpha * U[:, i]
|
|
250
|
+
Ms[k:, k] = shadow[:, k:].conj().T @ G[:, k]
|
|
251
|
+
if complex(Ms[k, k]) == 0.0:
|
|
252
|
+
return x, iteration
|
|
253
|
+
beta = complex(f[k]) / complex(Ms[k, k])
|
|
254
|
+
r = r - beta * G[:, k]
|
|
255
|
+
x = x + beta * U[:, k]
|
|
256
|
+
r_norm = float(cp.linalg.norm(r))
|
|
257
|
+
if r_norm <= target or iteration >= maxiter:
|
|
258
|
+
break
|
|
259
|
+
if k + 1 < s:
|
|
260
|
+
f[k + 1:] = f[k + 1:] - beta * Ms[k + 1:, k]
|
|
261
|
+
if r_norm <= target or iteration >= maxiter:
|
|
262
|
+
break
|
|
263
|
+
# Dimension-reduction step
|
|
264
|
+
v = psolve(r)
|
|
265
|
+
t = matvec(v)
|
|
266
|
+
iteration += 1
|
|
267
|
+
t_norm = float(cp.linalg.norm(t))
|
|
268
|
+
if t_norm == 0.0:
|
|
269
|
+
return x, iteration
|
|
270
|
+
ts = complex(cp.vdot(t, r))
|
|
271
|
+
angle = abs(ts) / (t_norm * r_norm)
|
|
272
|
+
omega = ts / (t_norm * t_norm)
|
|
273
|
+
if angle < kappa and angle > 0.0:
|
|
274
|
+
omega = omega * kappa / angle
|
|
275
|
+
if omega == 0.0:
|
|
276
|
+
return x, iteration
|
|
277
|
+
x = x + omega * v
|
|
278
|
+
r = r - omega * t
|
|
279
|
+
r_norm = float(cp.linalg.norm(r))
|
|
280
|
+
return x, 0 if r_norm <= target else iteration
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
|
|
4
|
+
from ..visualization import (
|
|
5
|
+
extract_orthogonal_slice,
|
|
6
|
+
plot_cross_section_panels,
|
|
7
|
+
plot_orthogonal_views,
|
|
8
|
+
)
|
|
9
|
+
from .postprocess import interpolate_yee_to_center
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def plot_cross_section(
|
|
13
|
+
solver,
|
|
14
|
+
axis="z",
|
|
15
|
+
position=0.0,
|
|
16
|
+
component="abs",
|
|
17
|
+
field_log_scale=False,
|
|
18
|
+
figsize=(12, 5),
|
|
19
|
+
save_path=None,
|
|
20
|
+
verbose=None,
|
|
21
|
+
):
|
|
22
|
+
if verbose is None:
|
|
23
|
+
verbose = solver.verbose
|
|
24
|
+
if solver.E_field is None:
|
|
25
|
+
print("Run solve() first.")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
ex, ey, ez = solver.E_field
|
|
29
|
+
component_str = component.lower()
|
|
30
|
+
|
|
31
|
+
if component_str == "abs":
|
|
32
|
+
field_data, x_c, y_c, z_c = interpolate_yee_to_center(solver)
|
|
33
|
+
eps_np = solver.scene.permittivity[:-1, :-1, :-1].cpu().numpy()
|
|
34
|
+
title_prefix = "|E|"
|
|
35
|
+
else:
|
|
36
|
+
if component_str == "ex":
|
|
37
|
+
field_data = torch.abs(ex).cpu().numpy()
|
|
38
|
+
elif component_str == "ey":
|
|
39
|
+
field_data = torch.abs(ey).cpu().numpy()
|
|
40
|
+
elif component_str == "ez":
|
|
41
|
+
field_data = torch.abs(ez).cpu().numpy()
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError(f"Invalid component '{component}'. Use 'abs', 'Ex', 'Ey', or 'Ez'")
|
|
44
|
+
x_c = solver.scene.x.cpu().numpy()
|
|
45
|
+
y_c = solver.scene.y.cpu().numpy()
|
|
46
|
+
z_c = solver.scene.z.cpu().numpy()
|
|
47
|
+
eps_np = solver.scene.permittivity.cpu().numpy()
|
|
48
|
+
title_prefix = f"|{component.upper()}|"
|
|
49
|
+
|
|
50
|
+
field_info = extract_orthogonal_slice(field_data, axis, position, x_c, y_c, z_c)
|
|
51
|
+
eps_info = extract_orthogonal_slice(eps_np, axis, position, x_c, y_c, z_c)
|
|
52
|
+
|
|
53
|
+
field_slice = field_info["slice"]
|
|
54
|
+
if field_log_scale:
|
|
55
|
+
field_slice = np.where(field_slice > 1e-12, field_slice, 1e-12)
|
|
56
|
+
fmax = np.max(field_slice)
|
|
57
|
+
if fmax > 0:
|
|
58
|
+
field_slice = 20 * np.log10(field_slice / fmax)
|
|
59
|
+
cbar_label = f"{title_prefix} (dB)"
|
|
60
|
+
else:
|
|
61
|
+
cbar_label = f"{title_prefix} (V/m)"
|
|
62
|
+
|
|
63
|
+
plot_cross_section_panels(
|
|
64
|
+
field_slice=field_slice,
|
|
65
|
+
permittivity_slice=eps_info["slice"],
|
|
66
|
+
x_coords=field_info["x_coords"],
|
|
67
|
+
y_coords=field_info["y_coords"],
|
|
68
|
+
xlabel=field_info["xlabel"],
|
|
69
|
+
ylabel=field_info["ylabel"],
|
|
70
|
+
field_title=f"{title_prefix} at {axis}={position:.3f}m",
|
|
71
|
+
field_cbar_label=cbar_label,
|
|
72
|
+
permittivity_title=f"Permittivity at {axis}={position:.3f}m",
|
|
73
|
+
figsize=figsize,
|
|
74
|
+
save_path=save_path,
|
|
75
|
+
verbose=verbose,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def plot_isotropic_3views(
|
|
80
|
+
solver,
|
|
81
|
+
position=0.0,
|
|
82
|
+
field_log_scale=True,
|
|
83
|
+
figsize=(18, 5),
|
|
84
|
+
save_path=None,
|
|
85
|
+
vmin_db=-60,
|
|
86
|
+
verbose=None,
|
|
87
|
+
):
|
|
88
|
+
if verbose is None:
|
|
89
|
+
verbose = solver.verbose
|
|
90
|
+
if solver.E_field is None:
|
|
91
|
+
print("Run solve() first.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
field_data, x_c, y_c, z_c = interpolate_yee_to_center(solver)
|
|
95
|
+
eps = solver.scene.permittivity[:-1, :-1, :-1].cpu().numpy()
|
|
96
|
+
|
|
97
|
+
plot_orthogonal_views(
|
|
98
|
+
field_data=field_data,
|
|
99
|
+
permittivity=eps,
|
|
100
|
+
x_coords=x_c,
|
|
101
|
+
y_coords=y_c,
|
|
102
|
+
z_coords=z_c,
|
|
103
|
+
position=position,
|
|
104
|
+
field_log_scale=field_log_scale,
|
|
105
|
+
figsize=figsize,
|
|
106
|
+
save_path=save_path,
|
|
107
|
+
vmin_db=vmin_db,
|
|
108
|
+
verbose=verbose,
|
|
109
|
+
suptitle="Total Electric Field Intensity |E| (FDFD)",
|
|
110
|
+
)
|