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,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
+ )