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,1987 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ import numpy as np
6
+ import torch
7
+ from tqdm import tqdm
8
+
9
+ from ...visualization import visualize_slice
10
+ from ..boundary import (
11
+ BOUNDARY_BLOCH,
12
+ BOUNDARY_PEC,
13
+ BOUNDARY_PERIODIC,
14
+ BOUNDARY_PML,
15
+ has_complex_fields,
16
+ initialize_boundary_state,
17
+ )
18
+ from ..excitation import (
19
+ advance_tfsf_auxiliary_electric,
20
+ advance_tfsf_auxiliary_magnetic,
21
+ apply_tfsf_e_correction,
22
+ apply_tfsf_h_correction,
23
+ inject_electric_surface_source_terms,
24
+ inject_magnetic_surface_source_terms,
25
+ initialize_source_terms,
26
+ initialize_tfsf_state,
27
+ tfsf_incident_is_gpu_driven,
28
+ )
29
+
30
+
31
+ def iter_cpml_memory_regions(solver, attr_name):
32
+ layout = getattr(solver, "_cpml_memory_layouts", {}).get(attr_name)
33
+ if layout is None:
34
+ return
35
+ axis = int(layout["axis"])
36
+ tensor = getattr(solver, attr_name)
37
+ for region in layout["regions"]:
38
+ length = int(region["length"])
39
+ if length <= 0:
40
+ continue
41
+ local_start = int(region["local_start"])
42
+ global_start = int(region["global_start"])
43
+ psi_region = tensor.narrow(axis, local_start, length)
44
+ offsets = [0, 0, 0]
45
+ offsets[axis] = global_start
46
+ yield {
47
+ "axis": axis,
48
+ "length": length,
49
+ "offsets": tuple(offsets),
50
+ "psi": psi_region,
51
+ }
52
+
53
+
54
+ def cpml_layout_params(solver, attr_name):
55
+ cached = solver._cpml_layout_cache.get(attr_name)
56
+ if cached is not None:
57
+ return cached
58
+ layout = getattr(solver, "_cpml_memory_layouts", {}).get(attr_name)
59
+ if layout is None:
60
+ result = (0, 0, 0)
61
+ else:
62
+ low = next((region for region in layout["regions"] if region["side"] == "low"), None)
63
+ high = next((region for region in layout["regions"] if region["side"] == "high"), None)
64
+ low_length = 0 if low is None else int(low["length"])
65
+ high_start = 0 if high is None else int(high["global_start"])
66
+ high_length = 0 if high is None else int(high["length"])
67
+ result = (low_length, high_start, high_length)
68
+ solver._cpml_layout_cache[attr_name] = result
69
+ return result
70
+
71
+
72
+ def _cpml_memory_attr(attr_name, *, imag=False):
73
+ return f"{attr_name}_imag" if imag else attr_name
74
+
75
+
76
+ def _cpml_memory_tensor(solver, attr_name, *, imag=False):
77
+ return getattr(solver, _cpml_memory_attr(attr_name, imag=imag))
78
+
79
+
80
+ def update_magnetic_fields_cpml_dense(solver, hx, hy, hz, ex, ey, ez, *, imag=False):
81
+ solver.fdtd_module.updateMagneticFieldHx3D(
82
+ Hx=hx,
83
+ Ey=ey,
84
+ Ez=ez,
85
+ HxDecay=solver.chx_decay,
86
+ HxCurl=solver.chx_curl,
87
+ PsiHxY=_cpml_memory_tensor(solver, "psi_hx_y", imag=imag),
88
+ PsiHxZ=_cpml_memory_tensor(solver, "psi_hx_z", imag=imag),
89
+ InvKappaHxY=solver.cpml_inv_kappa_h_y,
90
+ ByHxY=solver.cpml_b_h_y,
91
+ CyHxY=solver.cpml_c_h_y,
92
+ InvKappaHxZ=solver.cpml_inv_kappa_h_z,
93
+ ByHxZ=solver.cpml_b_h_z,
94
+ CyHxZ=solver.cpml_c_h_z,
95
+ invDy=solver.inv_dy_h,
96
+ invDz=solver.inv_dz_h,
97
+ ).launchRaw()
98
+ solver.fdtd_module.updateMagneticFieldHy3D(
99
+ Hy=hy,
100
+ Ex=ex,
101
+ Ez=ez,
102
+ HyDecay=solver.chy_decay,
103
+ HyCurl=solver.chy_curl,
104
+ PsiHyX=_cpml_memory_tensor(solver, "psi_hy_x", imag=imag),
105
+ PsiHyZ=_cpml_memory_tensor(solver, "psi_hy_z", imag=imag),
106
+ InvKappaHyX=solver.cpml_inv_kappa_h_x,
107
+ ByHyX=solver.cpml_b_h_x,
108
+ CyHyX=solver.cpml_c_h_x,
109
+ InvKappaHyZ=solver.cpml_inv_kappa_h_z,
110
+ ByHyZ=solver.cpml_b_h_z,
111
+ CyHyZ=solver.cpml_c_h_z,
112
+ invDx=solver.inv_dx_h,
113
+ invDz=solver.inv_dz_h,
114
+ ).launchRaw()
115
+ solver.fdtd_module.updateMagneticFieldHz3D(
116
+ Hz=hz,
117
+ Ex=ex,
118
+ Ey=ey,
119
+ HzDecay=solver.chz_decay,
120
+ HzCurl=solver.chz_curl,
121
+ PsiHzX=_cpml_memory_tensor(solver, "psi_hz_x", imag=imag),
122
+ PsiHzY=_cpml_memory_tensor(solver, "psi_hz_y", imag=imag),
123
+ InvKappaHzX=solver.cpml_inv_kappa_h_x,
124
+ ByHzX=solver.cpml_b_h_x,
125
+ CyHzX=solver.cpml_c_h_x,
126
+ InvKappaHzY=solver.cpml_inv_kappa_h_y,
127
+ ByHzY=solver.cpml_b_h_y,
128
+ CyHzY=solver.cpml_c_h_y,
129
+ invDx=solver.inv_dx_h,
130
+ invDy=solver.inv_dy_h,
131
+ ).launchRaw()
132
+
133
+
134
+ def update_magnetic_fields_cpml_compressed(solver, hx, hy, hz, ex, ey, ez, *, imag=False):
135
+ psi_hx_y_attr = _cpml_memory_attr("psi_hx_y", imag=imag)
136
+ psi_hx_z_attr = _cpml_memory_attr("psi_hx_z", imag=imag)
137
+ psi_hy_x_attr = _cpml_memory_attr("psi_hy_x", imag=imag)
138
+ psi_hy_z_attr = _cpml_memory_attr("psi_hy_z", imag=imag)
139
+ psi_hz_x_attr = _cpml_memory_attr("psi_hz_x", imag=imag)
140
+ psi_hz_y_attr = _cpml_memory_attr("psi_hz_y", imag=imag)
141
+ psi_hx_y_low, psi_hx_y_high_start, psi_hx_y_high = cpml_layout_params(solver, psi_hx_y_attr)
142
+ psi_hx_z_low, psi_hx_z_high_start, psi_hx_z_high = cpml_layout_params(solver, psi_hx_z_attr)
143
+ psi_hy_x_low, psi_hy_x_high_start, psi_hy_x_high = cpml_layout_params(solver, psi_hy_x_attr)
144
+ psi_hy_z_low, psi_hy_z_high_start, psi_hy_z_high = cpml_layout_params(solver, psi_hy_z_attr)
145
+ psi_hz_x_low, psi_hz_x_high_start, psi_hz_x_high = cpml_layout_params(solver, psi_hz_x_attr)
146
+ psi_hz_y_low, psi_hz_y_high_start, psi_hz_y_high = cpml_layout_params(solver, psi_hz_y_attr)
147
+
148
+ solver.fdtd_module.updateMagneticFieldHxCpmlCompressed3D(
149
+ Hx=hx,
150
+ Ey=ey,
151
+ Ez=ez,
152
+ HxDecay=solver.chx_decay,
153
+ HxCurl=solver.chx_curl,
154
+ PsiHxY=_cpml_memory_tensor(solver, "psi_hx_y", imag=imag),
155
+ PsiHxZ=_cpml_memory_tensor(solver, "psi_hx_z", imag=imag),
156
+ InvKappaHxY=solver.cpml_inv_kappa_h_y,
157
+ ByHxY=solver.cpml_b_h_y,
158
+ CyHxY=solver.cpml_c_h_y,
159
+ InvKappaHxZ=solver.cpml_inv_kappa_h_z,
160
+ ByHxZ=solver.cpml_b_h_z,
161
+ CyHxZ=solver.cpml_c_h_z,
162
+ invDy=solver.inv_dy_h,
163
+ invDz=solver.inv_dz_h,
164
+ psiHxYLowLength=psi_hx_y_low,
165
+ psiHxYHighStart=psi_hx_y_high_start,
166
+ psiHxYHighLength=psi_hx_y_high,
167
+ psiHxZLowLength=psi_hx_z_low,
168
+ psiHxZHighStart=psi_hx_z_high_start,
169
+ psiHxZHighLength=psi_hx_z_high,
170
+ uniformDecay=solver._coefficient_uniformity["chx_decay"],
171
+ uniformCurl=solver._coefficient_uniformity["chx_curl"],
172
+ ).launchRaw()
173
+ solver.fdtd_module.updateMagneticFieldHyCpmlCompressed3D(
174
+ Hy=hy,
175
+ Ex=ex,
176
+ Ez=ez,
177
+ HyDecay=solver.chy_decay,
178
+ HyCurl=solver.chy_curl,
179
+ PsiHyX=_cpml_memory_tensor(solver, "psi_hy_x", imag=imag),
180
+ PsiHyZ=_cpml_memory_tensor(solver, "psi_hy_z", imag=imag),
181
+ InvKappaHyX=solver.cpml_inv_kappa_h_x,
182
+ ByHyX=solver.cpml_b_h_x,
183
+ CyHyX=solver.cpml_c_h_x,
184
+ InvKappaHyZ=solver.cpml_inv_kappa_h_z,
185
+ ByHyZ=solver.cpml_b_h_z,
186
+ CyHyZ=solver.cpml_c_h_z,
187
+ invDx=solver.inv_dx_h,
188
+ invDz=solver.inv_dz_h,
189
+ psiHyXLowLength=psi_hy_x_low,
190
+ psiHyXHighStart=psi_hy_x_high_start,
191
+ psiHyXHighLength=psi_hy_x_high,
192
+ psiHyZLowLength=psi_hy_z_low,
193
+ psiHyZHighStart=psi_hy_z_high_start,
194
+ psiHyZHighLength=psi_hy_z_high,
195
+ uniformDecay=solver._coefficient_uniformity["chy_decay"],
196
+ uniformCurl=solver._coefficient_uniformity["chy_curl"],
197
+ ).launchRaw()
198
+ solver.fdtd_module.updateMagneticFieldHzCpmlCompressed3D(
199
+ Hz=hz,
200
+ Ex=ex,
201
+ Ey=ey,
202
+ HzDecay=solver.chz_decay,
203
+ HzCurl=solver.chz_curl,
204
+ PsiHzX=_cpml_memory_tensor(solver, "psi_hz_x", imag=imag),
205
+ PsiHzY=_cpml_memory_tensor(solver, "psi_hz_y", imag=imag),
206
+ InvKappaHzX=solver.cpml_inv_kappa_h_x,
207
+ ByHzX=solver.cpml_b_h_x,
208
+ CyHzX=solver.cpml_c_h_x,
209
+ InvKappaHzY=solver.cpml_inv_kappa_h_y,
210
+ ByHzY=solver.cpml_b_h_y,
211
+ CyHzY=solver.cpml_c_h_y,
212
+ invDx=solver.inv_dx_h,
213
+ invDy=solver.inv_dy_h,
214
+ psiHzXLowLength=psi_hz_x_low,
215
+ psiHzXHighStart=psi_hz_x_high_start,
216
+ psiHzXHighLength=psi_hz_x_high,
217
+ psiHzYLowLength=psi_hz_y_low,
218
+ psiHzYHighStart=psi_hz_y_high_start,
219
+ psiHzYHighLength=psi_hz_y_high,
220
+ uniformDecay=solver._coefficient_uniformity["chz_decay"],
221
+ uniformCurl=solver._coefficient_uniformity["chz_curl"],
222
+ ).launchRaw()
223
+
224
+
225
+ def update_magnetic_fields_cpml(solver, hx, hy, hz, ex, ey, ez, *, imag=False):
226
+ if solver._cpml_memory_mode == "dense":
227
+ update_magnetic_fields_cpml_dense(solver, hx, hy, hz, ex, ey, ez, imag=imag)
228
+ return
229
+ update_magnetic_fields_cpml_compressed(solver, hx, hy, hz, ex, ey, ez, imag=imag)
230
+
231
+
232
+ def update_magnetic_fields_standard(solver, hx, hy, hz, ex, ey, ez):
233
+ solver.fdtd_module.updateMagneticFieldHxStandard3D(
234
+ Hx=hx,
235
+ Ey=ey,
236
+ Ez=ez,
237
+ HxDecay=solver.chx_decay,
238
+ HxCurl=solver.chx_curl,
239
+ invDy=solver.inv_dy_h,
240
+ invDz=solver.inv_dz_h,
241
+ ).launchRaw()
242
+ solver.fdtd_module.updateMagneticFieldHyStandard3D(
243
+ Hy=hy,
244
+ Ex=ex,
245
+ Ez=ez,
246
+ HyDecay=solver.chy_decay,
247
+ HyCurl=solver.chy_curl,
248
+ invDx=solver.inv_dx_h,
249
+ invDz=solver.inv_dz_h,
250
+ ).launchRaw()
251
+ solver.fdtd_module.updateMagneticFieldHzStandard3D(
252
+ Hz=hz,
253
+ Ex=ex,
254
+ Ey=ey,
255
+ HzDecay=solver.chz_decay,
256
+ HzCurl=solver.chz_curl,
257
+ invDx=solver.inv_dx_h,
258
+ invDy=solver.inv_dy_h,
259
+ ).launchRaw()
260
+
261
+
262
+ def update_magnetic_fields(solver, hx, hy, hz, ex, ey, ez, *, imag=False):
263
+ if solver.uses_cpml:
264
+ update_magnetic_fields_cpml(solver, hx, hy, hz, ex, ey, ez, imag=imag)
265
+ else:
266
+ update_magnetic_fields_standard(solver, hx, hy, hz, ex, ey, ez)
267
+
268
+
269
+ def _electric_curl_tensors(solver):
270
+ if getattr(solver, "nonlinear_enabled", False):
271
+ return solver.cex_curl_dynamic, solver.cey_curl_dynamic, solver.cez_curl_dynamic
272
+ return solver.cex_curl, solver.cey_curl, solver.cez_curl
273
+
274
+
275
+ def _electric_decay_tensors(solver):
276
+ # The general nonlinear kernel (chi2 / field-dependent conductivity)
277
+ # rewrites the decay coefficients per step; pure-chi3 Kerr keeps the
278
+ # static decay tensors.
279
+ if getattr(solver, "nonlinear_general_enabled", False):
280
+ return solver.cex_decay_dynamic, solver.cey_decay_dynamic, solver.cez_decay_dynamic
281
+ return solver.cex_decay, solver.cey_decay, solver.cez_decay
282
+
283
+
284
+ def update_electric_fields_cpml_dense(solver, ex, ey, ez, hx, hy, hz):
285
+ ex_curl, ey_curl, ez_curl = _electric_curl_tensors(solver)
286
+ ex_decay, ey_decay, ez_decay = _electric_decay_tensors(solver)
287
+ solver.fdtd_module.updateElectricFieldExCpml3D(
288
+ Ex=ex,
289
+ Hy=hy,
290
+ Hz=hz,
291
+ ExDecay=ex_decay,
292
+ ExCurl=ex_curl,
293
+ PsiExY=solver.psi_ex_y,
294
+ PsiExZ=solver.psi_ex_z,
295
+ InvKappaExY=solver.cpml_inv_kappa_e_y,
296
+ BExY=solver.cpml_b_e_y,
297
+ CExY=solver.cpml_c_e_y,
298
+ InvKappaExZ=solver.cpml_inv_kappa_e_z,
299
+ BExZ=solver.cpml_b_e_z,
300
+ CExZ=solver.cpml_c_e_z,
301
+ invDy=solver.inv_dy_e,
302
+ invDz=solver.inv_dz_e,
303
+ yLowBoundaryMode=solver.boundary_y_low_code,
304
+ yHighBoundaryMode=solver.boundary_y_high_code,
305
+ zLowBoundaryMode=solver.boundary_z_low_code,
306
+ zHighBoundaryMode=solver.boundary_z_high_code,
307
+ ).launchRaw()
308
+ solver.fdtd_module.updateElectricFieldEyCpml3D(
309
+ Ey=ey,
310
+ Hx=hx,
311
+ Hz=hz,
312
+ EyDecay=ey_decay,
313
+ EyCurl=ey_curl,
314
+ PsiEyX=solver.psi_ey_x,
315
+ PsiEyZ=solver.psi_ey_z,
316
+ InvKappaEyX=solver.cpml_inv_kappa_e_x,
317
+ BEyX=solver.cpml_b_e_x,
318
+ CEyX=solver.cpml_c_e_x,
319
+ InvKappaEyZ=solver.cpml_inv_kappa_e_z,
320
+ BEyZ=solver.cpml_b_e_z,
321
+ CEyZ=solver.cpml_c_e_z,
322
+ invDx=solver.inv_dx_e,
323
+ invDz=solver.inv_dz_e,
324
+ xLowBoundaryMode=solver.boundary_x_low_code,
325
+ xHighBoundaryMode=solver.boundary_x_high_code,
326
+ zLowBoundaryMode=solver.boundary_z_low_code,
327
+ zHighBoundaryMode=solver.boundary_z_high_code,
328
+ ).launchRaw()
329
+ solver.fdtd_module.updateElectricFieldEzCpml3D(
330
+ Ez=ez,
331
+ Hx=hx,
332
+ Hy=hy,
333
+ EzDecay=ez_decay,
334
+ EzCurl=ez_curl,
335
+ PsiEzX=solver.psi_ez_x,
336
+ PsiEzY=solver.psi_ez_y,
337
+ InvKappaEzX=solver.cpml_inv_kappa_e_x,
338
+ BEzX=solver.cpml_b_e_x,
339
+ CEzX=solver.cpml_c_e_x,
340
+ InvKappaEzY=solver.cpml_inv_kappa_e_y,
341
+ BEzY=solver.cpml_b_e_y,
342
+ CEzY=solver.cpml_c_e_y,
343
+ invDx=solver.inv_dx_e,
344
+ invDy=solver.inv_dy_e,
345
+ xLowBoundaryMode=solver.boundary_x_low_code,
346
+ xHighBoundaryMode=solver.boundary_x_high_code,
347
+ yLowBoundaryMode=solver.boundary_y_low_code,
348
+ yHighBoundaryMode=solver.boundary_y_high_code,
349
+ ).launchRaw()
350
+
351
+
352
+ def update_electric_fields_cpml_compressed(solver, ex, ey, ez, hx, hy, hz):
353
+ psi_ex_y_low, psi_ex_y_high_start, psi_ex_y_high = cpml_layout_params(solver, "psi_ex_y")
354
+ psi_ex_z_low, psi_ex_z_high_start, psi_ex_z_high = cpml_layout_params(solver, "psi_ex_z")
355
+ psi_ey_x_low, psi_ey_x_high_start, psi_ey_x_high = cpml_layout_params(solver, "psi_ey_x")
356
+ psi_ey_z_low, psi_ey_z_high_start, psi_ey_z_high = cpml_layout_params(solver, "psi_ey_z")
357
+ psi_ez_x_low, psi_ez_x_high_start, psi_ez_x_high = cpml_layout_params(solver, "psi_ez_x")
358
+ psi_ez_y_low, psi_ez_y_high_start, psi_ez_y_high = cpml_layout_params(solver, "psi_ez_y")
359
+ ex_curl, ey_curl, ez_curl = _electric_curl_tensors(solver)
360
+ ex_decay, ey_decay, ez_decay = _electric_decay_tensors(solver)
361
+
362
+ solver.fdtd_module.updateElectricFieldExCpmlCompressed3D(
363
+ Ex=ex,
364
+ Hy=hy,
365
+ Hz=hz,
366
+ ExDecay=ex_decay,
367
+ ExCurl=ex_curl,
368
+ PsiExY=solver.psi_ex_y,
369
+ PsiExZ=solver.psi_ex_z,
370
+ InvKappaExY=solver.cpml_inv_kappa_e_y,
371
+ BExY=solver.cpml_b_e_y,
372
+ CExY=solver.cpml_c_e_y,
373
+ InvKappaExZ=solver.cpml_inv_kappa_e_z,
374
+ BExZ=solver.cpml_b_e_z,
375
+ CExZ=solver.cpml_c_e_z,
376
+ invDy=solver.inv_dy_e,
377
+ invDz=solver.inv_dz_e,
378
+ yLowBoundaryMode=solver.boundary_y_low_code,
379
+ yHighBoundaryMode=solver.boundary_y_high_code,
380
+ zLowBoundaryMode=solver.boundary_z_low_code,
381
+ zHighBoundaryMode=solver.boundary_z_high_code,
382
+ psiExYLowLength=psi_ex_y_low,
383
+ psiExYHighStart=psi_ex_y_high_start,
384
+ psiExYHighLength=psi_ex_y_high,
385
+ psiExZLowLength=psi_ex_z_low,
386
+ psiExZHighStart=psi_ex_z_high_start,
387
+ psiExZHighLength=psi_ex_z_high,
388
+ uniformDecay=None if ex_decay is not solver.cex_decay else solver._coefficient_uniformity["cex_decay"],
389
+ uniformCurl=None if ex_curl is not solver.cex_curl else solver._coefficient_uniformity["cex_curl"],
390
+ ).launchRaw()
391
+ solver.fdtd_module.updateElectricFieldEyCpmlCompressed3D(
392
+ Ey=ey,
393
+ Hx=hx,
394
+ Hz=hz,
395
+ EyDecay=ey_decay,
396
+ EyCurl=ey_curl,
397
+ PsiEyX=solver.psi_ey_x,
398
+ PsiEyZ=solver.psi_ey_z,
399
+ InvKappaEyX=solver.cpml_inv_kappa_e_x,
400
+ BEyX=solver.cpml_b_e_x,
401
+ CEyX=solver.cpml_c_e_x,
402
+ InvKappaEyZ=solver.cpml_inv_kappa_e_z,
403
+ BEyZ=solver.cpml_b_e_z,
404
+ CEyZ=solver.cpml_c_e_z,
405
+ invDx=solver.inv_dx_e,
406
+ invDz=solver.inv_dz_e,
407
+ xLowBoundaryMode=solver.boundary_x_low_code,
408
+ xHighBoundaryMode=solver.boundary_x_high_code,
409
+ zLowBoundaryMode=solver.boundary_z_low_code,
410
+ zHighBoundaryMode=solver.boundary_z_high_code,
411
+ psiEyXLowLength=psi_ey_x_low,
412
+ psiEyXHighStart=psi_ey_x_high_start,
413
+ psiEyXHighLength=psi_ey_x_high,
414
+ psiEyZLowLength=psi_ey_z_low,
415
+ psiEyZHighStart=psi_ey_z_high_start,
416
+ psiEyZHighLength=psi_ey_z_high,
417
+ uniformDecay=None if ey_decay is not solver.cey_decay else solver._coefficient_uniformity["cey_decay"],
418
+ uniformCurl=None if ey_curl is not solver.cey_curl else solver._coefficient_uniformity["cey_curl"],
419
+ ).launchRaw()
420
+ solver.fdtd_module.updateElectricFieldEzCpmlCompressed3D(
421
+ Ez=ez,
422
+ Hx=hx,
423
+ Hy=hy,
424
+ EzDecay=ez_decay,
425
+ EzCurl=ez_curl,
426
+ PsiEzX=solver.psi_ez_x,
427
+ PsiEzY=solver.psi_ez_y,
428
+ InvKappaEzX=solver.cpml_inv_kappa_e_x,
429
+ BEzX=solver.cpml_b_e_x,
430
+ CEzX=solver.cpml_c_e_x,
431
+ InvKappaEzY=solver.cpml_inv_kappa_e_y,
432
+ BEzY=solver.cpml_b_e_y,
433
+ CEzY=solver.cpml_c_e_y,
434
+ invDx=solver.inv_dx_e,
435
+ invDy=solver.inv_dy_e,
436
+ xLowBoundaryMode=solver.boundary_x_low_code,
437
+ xHighBoundaryMode=solver.boundary_x_high_code,
438
+ yLowBoundaryMode=solver.boundary_y_low_code,
439
+ yHighBoundaryMode=solver.boundary_y_high_code,
440
+ psiEzXLowLength=psi_ez_x_low,
441
+ psiEzXHighStart=psi_ez_x_high_start,
442
+ psiEzXHighLength=psi_ez_x_high,
443
+ psiEzYLowLength=psi_ez_y_low,
444
+ psiEzYHighStart=psi_ez_y_high_start,
445
+ psiEzYHighLength=psi_ez_y_high,
446
+ uniformDecay=None if ez_decay is not solver.cez_decay else solver._coefficient_uniformity["cez_decay"],
447
+ uniformCurl=None if ez_curl is not solver.cez_curl else solver._coefficient_uniformity["cez_curl"],
448
+ ).launchRaw()
449
+
450
+
451
+ def update_electric_fields_cpml(solver, ex, ey, ez, hx, hy, hz):
452
+ if solver._cpml_memory_mode == "dense":
453
+ update_electric_fields_cpml_dense(solver, ex, ey, ez, hx, hy, hz)
454
+ return
455
+ update_electric_fields_cpml_compressed(solver, ex, ey, ez, hx, hy, hz)
456
+
457
+
458
+ def update_electric_fields_standard(solver, ex, ey, ez, hx, hy, hz):
459
+ ex_curl, ey_curl, ez_curl = _electric_curl_tensors(solver)
460
+ ex_decay, ey_decay, ez_decay = _electric_decay_tensors(solver)
461
+ solver.fdtd_module.updateElectricFieldExStandard3D(
462
+ Ex=ex,
463
+ Hy=hy,
464
+ Hz=hz,
465
+ ExDecay=ex_decay,
466
+ ExCurl=ex_curl,
467
+ invDy=solver.inv_dy_e,
468
+ invDz=solver.inv_dz_e,
469
+ yLowBoundaryMode=solver.boundary_y_low_code,
470
+ yHighBoundaryMode=solver.boundary_y_high_code,
471
+ zLowBoundaryMode=solver.boundary_z_low_code,
472
+ zHighBoundaryMode=solver.boundary_z_high_code,
473
+ ).launchRaw()
474
+ solver.fdtd_module.updateElectricFieldEyStandard3D(
475
+ Ey=ey,
476
+ Hx=hx,
477
+ Hz=hz,
478
+ EyDecay=ey_decay,
479
+ EyCurl=ey_curl,
480
+ invDx=solver.inv_dx_e,
481
+ invDz=solver.inv_dz_e,
482
+ xLowBoundaryMode=solver.boundary_x_low_code,
483
+ xHighBoundaryMode=solver.boundary_x_high_code,
484
+ zLowBoundaryMode=solver.boundary_z_low_code,
485
+ zHighBoundaryMode=solver.boundary_z_high_code,
486
+ ).launchRaw()
487
+ solver.fdtd_module.updateElectricFieldEzStandard3D(
488
+ Ez=ez,
489
+ Hx=hx,
490
+ Hy=hy,
491
+ EzDecay=ez_decay,
492
+ EzCurl=ez_curl,
493
+ invDx=solver.inv_dx_e,
494
+ invDy=solver.inv_dy_e,
495
+ xLowBoundaryMode=solver.boundary_x_low_code,
496
+ xHighBoundaryMode=solver.boundary_x_high_code,
497
+ yLowBoundaryMode=solver.boundary_y_low_code,
498
+ yHighBoundaryMode=solver.boundary_y_high_code,
499
+ ).launchRaw()
500
+
501
+
502
+ def update_electric_fields_modulated_standard(solver, ex, ey, ez, hx, hy, hz, time_value):
503
+ # Composing modulation with an instantaneous nonlinearity: the modulation
504
+ # scales the same field-dependent decay/curl coefficients the nonlinear kernel
505
+ # rewrote this step (static tensors when no nonlinearity is present).
506
+ ex_curl, ey_curl, ez_curl = _electric_curl_tensors(solver)
507
+ ex_decay, ey_decay, ez_decay = _electric_decay_tensors(solver)
508
+ solver.fdtd_module.updateElectricFieldExModulated3D(
509
+ Ex=ex,
510
+ Hy=hy,
511
+ Hz=hz,
512
+ ExDecay=ex_decay,
513
+ ExCurl=ex_curl,
514
+ ModCos=solver.mod_cos_Ex,
515
+ ModSin=solver.mod_sin_Ex,
516
+ ModOmega=solver.mod_omega_Ex,
517
+ ModulationTime=solver._modulation_time,
518
+ invDy=solver.inv_dy_e,
519
+ invDz=solver.inv_dz_e,
520
+ yLowBoundaryMode=solver.boundary_y_low_code,
521
+ yHighBoundaryMode=solver.boundary_y_high_code,
522
+ zLowBoundaryMode=solver.boundary_z_low_code,
523
+ zHighBoundaryMode=solver.boundary_z_high_code,
524
+ ).launchRaw()
525
+ solver.fdtd_module.updateElectricFieldEyModulated3D(
526
+ Ey=ey,
527
+ Hx=hx,
528
+ Hz=hz,
529
+ EyDecay=ey_decay,
530
+ EyCurl=ey_curl,
531
+ ModCos=solver.mod_cos_Ey,
532
+ ModSin=solver.mod_sin_Ey,
533
+ ModOmega=solver.mod_omega_Ey,
534
+ ModulationTime=solver._modulation_time,
535
+ invDx=solver.inv_dx_e,
536
+ invDz=solver.inv_dz_e,
537
+ xLowBoundaryMode=solver.boundary_x_low_code,
538
+ xHighBoundaryMode=solver.boundary_x_high_code,
539
+ zLowBoundaryMode=solver.boundary_z_low_code,
540
+ zHighBoundaryMode=solver.boundary_z_high_code,
541
+ ).launchRaw()
542
+ solver.fdtd_module.updateElectricFieldEzModulated3D(
543
+ Ez=ez,
544
+ Hx=hx,
545
+ Hy=hy,
546
+ EzDecay=ez_decay,
547
+ EzCurl=ez_curl,
548
+ ModCos=solver.mod_cos_Ez,
549
+ ModSin=solver.mod_sin_Ez,
550
+ ModOmega=solver.mod_omega_Ez,
551
+ ModulationTime=solver._modulation_time,
552
+ invDx=solver.inv_dx_e,
553
+ invDy=solver.inv_dy_e,
554
+ xLowBoundaryMode=solver.boundary_x_low_code,
555
+ xHighBoundaryMode=solver.boundary_x_high_code,
556
+ yLowBoundaryMode=solver.boundary_y_low_code,
557
+ yHighBoundaryMode=solver.boundary_y_high_code,
558
+ ).launchRaw()
559
+
560
+
561
+ def update_electric_fields_modulated_cpml(solver, ex, ey, ez, hx, hy, hz, time_value):
562
+ if solver._cpml_memory_mode == "dense":
563
+ update_electric_fields_modulated_cpml_dense(solver, ex, ey, ez, hx, hy, hz, time_value)
564
+ return
565
+ update_electric_fields_modulated_cpml_compressed(solver, ex, ey, ez, hx, hy, hz, time_value)
566
+
567
+
568
+ def update_electric_fields_modulated_cpml_dense(solver, ex, ey, ez, hx, hy, hz, time_value):
569
+ ex_curl, ey_curl, ez_curl = _electric_curl_tensors(solver)
570
+ ex_decay, ey_decay, ez_decay = _electric_decay_tensors(solver)
571
+ solver.fdtd_module.updateElectricFieldExCpmlModulated3D(
572
+ Ex=ex,
573
+ Hy=hy,
574
+ Hz=hz,
575
+ ExDecay=ex_decay,
576
+ ExCurl=ex_curl,
577
+ ModCos=solver.mod_cos_Ex,
578
+ ModSin=solver.mod_sin_Ex,
579
+ ModOmega=solver.mod_omega_Ex,
580
+ ModulationTime=solver._modulation_time,
581
+ PsiExY=solver.psi_ex_y,
582
+ PsiExZ=solver.psi_ex_z,
583
+ InvKappaExY=solver.cpml_inv_kappa_e_y,
584
+ BExY=solver.cpml_b_e_y,
585
+ CExY=solver.cpml_c_e_y,
586
+ InvKappaExZ=solver.cpml_inv_kappa_e_z,
587
+ BExZ=solver.cpml_b_e_z,
588
+ CExZ=solver.cpml_c_e_z,
589
+ invDy=solver.inv_dy_e,
590
+ invDz=solver.inv_dz_e,
591
+ yLowBoundaryMode=solver.boundary_y_low_code,
592
+ yHighBoundaryMode=solver.boundary_y_high_code,
593
+ zLowBoundaryMode=solver.boundary_z_low_code,
594
+ zHighBoundaryMode=solver.boundary_z_high_code,
595
+ ).launchRaw()
596
+ solver.fdtd_module.updateElectricFieldEyCpmlModulated3D(
597
+ Ey=ey,
598
+ Hx=hx,
599
+ Hz=hz,
600
+ EyDecay=ey_decay,
601
+ EyCurl=ey_curl,
602
+ ModCos=solver.mod_cos_Ey,
603
+ ModSin=solver.mod_sin_Ey,
604
+ ModOmega=solver.mod_omega_Ey,
605
+ ModulationTime=solver._modulation_time,
606
+ PsiEyX=solver.psi_ey_x,
607
+ PsiEyZ=solver.psi_ey_z,
608
+ InvKappaEyX=solver.cpml_inv_kappa_e_x,
609
+ BEyX=solver.cpml_b_e_x,
610
+ CEyX=solver.cpml_c_e_x,
611
+ InvKappaEyZ=solver.cpml_inv_kappa_e_z,
612
+ BEyZ=solver.cpml_b_e_z,
613
+ CEyZ=solver.cpml_c_e_z,
614
+ invDx=solver.inv_dx_e,
615
+ invDz=solver.inv_dz_e,
616
+ xLowBoundaryMode=solver.boundary_x_low_code,
617
+ xHighBoundaryMode=solver.boundary_x_high_code,
618
+ zLowBoundaryMode=solver.boundary_z_low_code,
619
+ zHighBoundaryMode=solver.boundary_z_high_code,
620
+ ).launchRaw()
621
+ solver.fdtd_module.updateElectricFieldEzCpmlModulated3D(
622
+ Ez=ez,
623
+ Hx=hx,
624
+ Hy=hy,
625
+ EzDecay=ez_decay,
626
+ EzCurl=ez_curl,
627
+ ModCos=solver.mod_cos_Ez,
628
+ ModSin=solver.mod_sin_Ez,
629
+ ModOmega=solver.mod_omega_Ez,
630
+ ModulationTime=solver._modulation_time,
631
+ PsiEzX=solver.psi_ez_x,
632
+ PsiEzY=solver.psi_ez_y,
633
+ InvKappaEzX=solver.cpml_inv_kappa_e_x,
634
+ BEzX=solver.cpml_b_e_x,
635
+ CEzX=solver.cpml_c_e_x,
636
+ InvKappaEzY=solver.cpml_inv_kappa_e_y,
637
+ BEzY=solver.cpml_b_e_y,
638
+ CEzY=solver.cpml_c_e_y,
639
+ invDx=solver.inv_dx_e,
640
+ invDy=solver.inv_dy_e,
641
+ xLowBoundaryMode=solver.boundary_x_low_code,
642
+ xHighBoundaryMode=solver.boundary_x_high_code,
643
+ yLowBoundaryMode=solver.boundary_y_low_code,
644
+ yHighBoundaryMode=solver.boundary_y_high_code,
645
+ ).launchRaw()
646
+
647
+
648
+ def update_electric_fields_modulated_cpml_compressed(solver, ex, ey, ez, hx, hy, hz, time_value):
649
+ # Compressed (slab) psi layout: thread the per-side narrow()/offset bookkeeping
650
+ # (cpml_layout_params) through the modulated kernels, exactly as the plain
651
+ # compressed CPML update does, with the modulation phase factors applied on top.
652
+ psi_ex_y_low, psi_ex_y_high_start, psi_ex_y_high = cpml_layout_params(solver, "psi_ex_y")
653
+ psi_ex_z_low, psi_ex_z_high_start, psi_ex_z_high = cpml_layout_params(solver, "psi_ex_z")
654
+ psi_ey_x_low, psi_ey_x_high_start, psi_ey_x_high = cpml_layout_params(solver, "psi_ey_x")
655
+ psi_ey_z_low, psi_ey_z_high_start, psi_ey_z_high = cpml_layout_params(solver, "psi_ey_z")
656
+ psi_ez_x_low, psi_ez_x_high_start, psi_ez_x_high = cpml_layout_params(solver, "psi_ez_x")
657
+ psi_ez_y_low, psi_ez_y_high_start, psi_ez_y_high = cpml_layout_params(solver, "psi_ez_y")
658
+ ex_curl, ey_curl, ez_curl = _electric_curl_tensors(solver)
659
+ ex_decay, ey_decay, ez_decay = _electric_decay_tensors(solver)
660
+ solver.fdtd_module.updateElectricFieldExCpmlModulatedCompressed3D(
661
+ Ex=ex,
662
+ Hy=hy,
663
+ Hz=hz,
664
+ ExDecay=ex_decay,
665
+ ExCurl=ex_curl,
666
+ ModCos=solver.mod_cos_Ex,
667
+ ModSin=solver.mod_sin_Ex,
668
+ ModOmega=solver.mod_omega_Ex,
669
+ ModulationTime=solver._modulation_time,
670
+ PsiExY=solver.psi_ex_y,
671
+ PsiExZ=solver.psi_ex_z,
672
+ InvKappaExY=solver.cpml_inv_kappa_e_y,
673
+ BExY=solver.cpml_b_e_y,
674
+ CExY=solver.cpml_c_e_y,
675
+ InvKappaExZ=solver.cpml_inv_kappa_e_z,
676
+ BExZ=solver.cpml_b_e_z,
677
+ CExZ=solver.cpml_c_e_z,
678
+ invDy=solver.inv_dy_e,
679
+ invDz=solver.inv_dz_e,
680
+ yLowBoundaryMode=solver.boundary_y_low_code,
681
+ yHighBoundaryMode=solver.boundary_y_high_code,
682
+ zLowBoundaryMode=solver.boundary_z_low_code,
683
+ zHighBoundaryMode=solver.boundary_z_high_code,
684
+ psiExYLowLength=psi_ex_y_low,
685
+ psiExYHighStart=psi_ex_y_high_start,
686
+ psiExYHighLength=psi_ex_y_high,
687
+ psiExZLowLength=psi_ex_z_low,
688
+ psiExZHighStart=psi_ex_z_high_start,
689
+ psiExZHighLength=psi_ex_z_high,
690
+ ).launchRaw()
691
+ solver.fdtd_module.updateElectricFieldEyCpmlModulatedCompressed3D(
692
+ Ey=ey,
693
+ Hx=hx,
694
+ Hz=hz,
695
+ EyDecay=ey_decay,
696
+ EyCurl=ey_curl,
697
+ ModCos=solver.mod_cos_Ey,
698
+ ModSin=solver.mod_sin_Ey,
699
+ ModOmega=solver.mod_omega_Ey,
700
+ ModulationTime=solver._modulation_time,
701
+ PsiEyX=solver.psi_ey_x,
702
+ PsiEyZ=solver.psi_ey_z,
703
+ InvKappaEyX=solver.cpml_inv_kappa_e_x,
704
+ BEyX=solver.cpml_b_e_x,
705
+ CEyX=solver.cpml_c_e_x,
706
+ InvKappaEyZ=solver.cpml_inv_kappa_e_z,
707
+ BEyZ=solver.cpml_b_e_z,
708
+ CEyZ=solver.cpml_c_e_z,
709
+ invDx=solver.inv_dx_e,
710
+ invDz=solver.inv_dz_e,
711
+ xLowBoundaryMode=solver.boundary_x_low_code,
712
+ xHighBoundaryMode=solver.boundary_x_high_code,
713
+ zLowBoundaryMode=solver.boundary_z_low_code,
714
+ zHighBoundaryMode=solver.boundary_z_high_code,
715
+ psiEyXLowLength=psi_ey_x_low,
716
+ psiEyXHighStart=psi_ey_x_high_start,
717
+ psiEyXHighLength=psi_ey_x_high,
718
+ psiEyZLowLength=psi_ey_z_low,
719
+ psiEyZHighStart=psi_ey_z_high_start,
720
+ psiEyZHighLength=psi_ey_z_high,
721
+ ).launchRaw()
722
+ solver.fdtd_module.updateElectricFieldEzCpmlModulatedCompressed3D(
723
+ Ez=ez,
724
+ Hx=hx,
725
+ Hy=hy,
726
+ EzDecay=ez_decay,
727
+ EzCurl=ez_curl,
728
+ ModCos=solver.mod_cos_Ez,
729
+ ModSin=solver.mod_sin_Ez,
730
+ ModOmega=solver.mod_omega_Ez,
731
+ ModulationTime=solver._modulation_time,
732
+ PsiEzX=solver.psi_ez_x,
733
+ PsiEzY=solver.psi_ez_y,
734
+ InvKappaEzX=solver.cpml_inv_kappa_e_x,
735
+ BEzX=solver.cpml_b_e_x,
736
+ CEzX=solver.cpml_c_e_x,
737
+ InvKappaEzY=solver.cpml_inv_kappa_e_y,
738
+ BEzY=solver.cpml_b_e_y,
739
+ CEzY=solver.cpml_c_e_y,
740
+ invDx=solver.inv_dx_e,
741
+ invDy=solver.inv_dy_e,
742
+ xLowBoundaryMode=solver.boundary_x_low_code,
743
+ xHighBoundaryMode=solver.boundary_x_high_code,
744
+ yLowBoundaryMode=solver.boundary_y_low_code,
745
+ yHighBoundaryMode=solver.boundary_y_high_code,
746
+ psiEzXLowLength=psi_ez_x_low,
747
+ psiEzXHighStart=psi_ez_x_high_start,
748
+ psiEzXHighLength=psi_ez_x_high,
749
+ psiEzYLowLength=psi_ez_y_low,
750
+ psiEzYHighStart=psi_ez_y_high_start,
751
+ psiEzYHighLength=psi_ez_y_high,
752
+ ).launchRaw()
753
+
754
+
755
+ def update_electric_fields(solver, ex, ey, ez, hx, hy, hz, *, time_value=None):
756
+ if getattr(solver, "modulation_enabled", False):
757
+ if time_value is None:
758
+ raise RuntimeError(
759
+ "The time-modulated electric update requires the current step time_value."
760
+ )
761
+ if solver.uses_cpml:
762
+ update_electric_fields_modulated_cpml(solver, ex, ey, ez, hx, hy, hz, time_value)
763
+ else:
764
+ update_electric_fields_modulated_standard(solver, ex, ey, ez, hx, hy, hz, time_value)
765
+ return
766
+ if solver.uses_cpml:
767
+ update_electric_fields_cpml(solver, ex, ey, ez, hx, hy, hz)
768
+ else:
769
+ update_electric_fields_standard(solver, ex, ey, ez, hx, hy, hz)
770
+
771
+
772
+ def _full_aniso_periodic_flags(solver):
773
+ return (
774
+ int(solver.boundary_x_low_code == BOUNDARY_PERIODIC and solver.boundary_x_high_code == BOUNDARY_PERIODIC),
775
+ int(solver.boundary_y_low_code == BOUNDARY_PERIODIC and solver.boundary_y_high_code == BOUNDARY_PERIODIC),
776
+ int(solver.boundary_z_low_code == BOUNDARY_PERIODIC and solver.boundary_z_high_code == BOUNDARY_PERIODIC),
777
+ )
778
+
779
+
780
+ def apply_full_aniso_corrections(solver):
781
+ """Add the off-diagonal anisotropic coupling terms to the E update.
782
+
783
+ Each kernel neighbor-averages the two off-axis curl(H) components onto the
784
+ target Yee edge and accumulates ``coeff * <curlH>``; the coefficients are the
785
+ off-diagonal entries of the per-edge inverse permittivity tensor scaled by
786
+ ``dt/eps0`` and vanish outside the anisotropic structures. Periodic axes
787
+ wrap the collocation stencil; other boundaries skip out-of-range samples.
788
+ Under CPML the coordinate-stretched variant is used so the coupling is
789
+ absorbed consistently where an anisotropic structure reaches the boundary.
790
+ """
791
+ if getattr(solver, "uses_cpml", False):
792
+ apply_full_aniso_corrections_cpml(solver)
793
+ return
794
+ periodic_x, periodic_y, periodic_z = _full_aniso_periodic_flags(solver)
795
+ solver.fdtd_module.updateElectricFieldExFullAniso3D(
796
+ Ex=solver.Ex,
797
+ Hx=solver.Hx,
798
+ Hy=solver.Hy,
799
+ Hz=solver.Hz,
800
+ CoeffY=solver.cex_aniso_y,
801
+ CoeffZ=solver.cex_aniso_z,
802
+ invDx=solver.inv_dx_e,
803
+ invDy=solver.inv_dy_e,
804
+ invDz=solver.inv_dz_e,
805
+ periodicX=periodic_x,
806
+ periodicY=periodic_y,
807
+ periodicZ=periodic_z,
808
+ ).launchRaw()
809
+ solver.fdtd_module.updateElectricFieldEyFullAniso3D(
810
+ Ey=solver.Ey,
811
+ Hx=solver.Hx,
812
+ Hy=solver.Hy,
813
+ Hz=solver.Hz,
814
+ CoeffX=solver.cey_aniso_x,
815
+ CoeffZ=solver.cey_aniso_z,
816
+ invDx=solver.inv_dx_e,
817
+ invDy=solver.inv_dy_e,
818
+ invDz=solver.inv_dz_e,
819
+ periodicX=periodic_x,
820
+ periodicY=periodic_y,
821
+ periodicZ=periodic_z,
822
+ ).launchRaw()
823
+ solver.fdtd_module.updateElectricFieldEzFullAniso3D(
824
+ Ez=solver.Ez,
825
+ Hx=solver.Hx,
826
+ Hy=solver.Hy,
827
+ Hz=solver.Hz,
828
+ CoeffX=solver.cez_aniso_x,
829
+ CoeffY=solver.cez_aniso_y,
830
+ invDx=solver.inv_dx_e,
831
+ invDy=solver.inv_dy_e,
832
+ invDz=solver.inv_dz_e,
833
+ periodicX=periodic_x,
834
+ periodicY=periodic_y,
835
+ periodicZ=periodic_z,
836
+ ).launchRaw()
837
+
838
+
839
+ def apply_full_aniso_corrections_cpml(solver):
840
+ """CPML-consistent off-diagonal anisotropic coupling for absorber overlap.
841
+
842
+ Each spatial derivative that enters the collocated off-axis curl(H) is
843
+ coordinate-stretched with its own recursive-convolution memory owned by the
844
+ target E edge. The two transverse directions use the E-field (node) CPML
845
+ profiles at the edge index; the edge's own direction sits on a half point and
846
+ uses the H-field (half) profile. Outside the absorber the profiles are neutral
847
+ and the psi buffers stay zero, so the correction matches the raw update.
848
+ """
849
+ periodic_x, periodic_y, periodic_z = _full_aniso_periodic_flags(solver)
850
+ solver.fdtd_module.updateElectricFieldExFullAnisoCpml3D(
851
+ Ex=solver.Ex,
852
+ Hx=solver.Hx,
853
+ Hy=solver.Hy,
854
+ Hz=solver.Hz,
855
+ CoeffY=solver.cex_aniso_y,
856
+ CoeffZ=solver.cex_aniso_z,
857
+ invDx=solver.inv_dx_e,
858
+ invDy=solver.inv_dy_e,
859
+ invDz=solver.inv_dz_e,
860
+ InvKappaX=solver.cpml_inv_kappa_h_x,
861
+ BX=solver.cpml_b_h_x,
862
+ CX=solver.cpml_c_h_x,
863
+ InvKappaY=solver.cpml_inv_kappa_e_y,
864
+ BY=solver.cpml_b_e_y,
865
+ CY=solver.cpml_c_e_y,
866
+ InvKappaZ=solver.cpml_inv_kappa_e_z,
867
+ BZ=solver.cpml_b_e_z,
868
+ CZ=solver.cpml_c_e_z,
869
+ periodicX=periodic_x,
870
+ periodicY=periodic_y,
871
+ periodicZ=periodic_z,
872
+ PsiX=solver.psi_ex_aniso_x,
873
+ PsiY=solver.psi_ex_aniso_y,
874
+ PsiZ=solver.psi_ex_aniso_z,
875
+ ).launchRaw()
876
+ solver.fdtd_module.updateElectricFieldEyFullAnisoCpml3D(
877
+ Ey=solver.Ey,
878
+ Hx=solver.Hx,
879
+ Hy=solver.Hy,
880
+ Hz=solver.Hz,
881
+ CoeffX=solver.cey_aniso_x,
882
+ CoeffZ=solver.cey_aniso_z,
883
+ invDx=solver.inv_dx_e,
884
+ invDy=solver.inv_dy_e,
885
+ invDz=solver.inv_dz_e,
886
+ InvKappaX=solver.cpml_inv_kappa_e_x,
887
+ BX=solver.cpml_b_e_x,
888
+ CX=solver.cpml_c_e_x,
889
+ InvKappaY=solver.cpml_inv_kappa_h_y,
890
+ BY=solver.cpml_b_h_y,
891
+ CY=solver.cpml_c_h_y,
892
+ InvKappaZ=solver.cpml_inv_kappa_e_z,
893
+ BZ=solver.cpml_b_e_z,
894
+ CZ=solver.cpml_c_e_z,
895
+ periodicX=periodic_x,
896
+ periodicY=periodic_y,
897
+ periodicZ=periodic_z,
898
+ PsiX=solver.psi_ey_aniso_x,
899
+ PsiY=solver.psi_ey_aniso_y,
900
+ PsiZ=solver.psi_ey_aniso_z,
901
+ ).launchRaw()
902
+ solver.fdtd_module.updateElectricFieldEzFullAnisoCpml3D(
903
+ Ez=solver.Ez,
904
+ Hx=solver.Hx,
905
+ Hy=solver.Hy,
906
+ Hz=solver.Hz,
907
+ CoeffX=solver.cez_aniso_x,
908
+ CoeffY=solver.cez_aniso_y,
909
+ invDx=solver.inv_dx_e,
910
+ invDy=solver.inv_dy_e,
911
+ invDz=solver.inv_dz_e,
912
+ InvKappaX=solver.cpml_inv_kappa_e_x,
913
+ BX=solver.cpml_b_e_x,
914
+ CX=solver.cpml_c_e_x,
915
+ InvKappaY=solver.cpml_inv_kappa_e_y,
916
+ BY=solver.cpml_b_e_y,
917
+ CY=solver.cpml_c_e_y,
918
+ InvKappaZ=solver.cpml_inv_kappa_h_z,
919
+ BZ=solver.cpml_b_h_z,
920
+ CZ=solver.cpml_c_h_z,
921
+ periodicX=periodic_x,
922
+ periodicY=periodic_y,
923
+ periodicZ=periodic_z,
924
+ PsiX=solver.psi_ez_aniso_x,
925
+ PsiY=solver.psi_ez_aniso_y,
926
+ PsiZ=solver.psi_ez_aniso_z,
927
+ ).launchRaw()
928
+
929
+
930
+ def _full_aniso_conductive(solver) -> bool:
931
+ return getattr(solver, "full_aniso_enabled", False) and getattr(solver, "_aniso_cond_current", None) is not None
932
+
933
+
934
+ def capture_aniso_conduction_currents(solver):
935
+ """Snapshot the conduction current sigma . E^n before the electric update.
936
+
937
+ For a full (off-diagonal) anisotropic conductive medium the exact semi-implicit
938
+ update is ``E^{n+1} = E^n + B . (curl H - sigma . E^n)`` with
939
+ ``B = dt (eps_inf + dt/2 diag(sigma))^-1``. The diagonal ``-B_ii sigma_i E_i^n``
940
+ term is folded into the decay coefficient, but the off-diagonal
941
+ ``-B_ij sigma_j E_j^n`` (i != j) terms need the pre-update field, which the base
942
+ update overwrites, so they are captured here into ``_aniso_cond_current``.
943
+ """
944
+ if not _full_aniso_conductive(solver):
945
+ return
946
+ currents = solver._aniso_cond_current
947
+ solver.fdtd_module.captureAnisoConductionCurrent3D(
948
+ SigmaX=solver.sigma_e_Ex,
949
+ SigmaY=solver.sigma_e_Ey,
950
+ SigmaZ=solver.sigma_e_Ez,
951
+ Ex=solver.Ex,
952
+ Ey=solver.Ey,
953
+ Ez=solver.Ez,
954
+ Jx=currents["Ex"],
955
+ Jy=currents["Ey"],
956
+ Jz=currents["Ez"],
957
+ ).launchRaw()
958
+
959
+
960
+ def apply_full_aniso_conduction(solver):
961
+ """Subtract the off-diagonal conduction current through the inverse tensor.
962
+
963
+ Applies ``E_i -= 0.25 * (B_ij <sigma_j E_j^n> + B_ik <sigma_k E_k^n>)`` with the
964
+ same off-diagonal coefficients (``c*_aniso_*`` == ``B_ij``) and four-neighbor
965
+ collocation used for the curl(H) coupling. The conduction current is pointwise
966
+ (no spatial derivative), so it needs no CPML coordinate stretch and reuses the
967
+ plain off-diagonal current kernels for every boundary configuration.
968
+ """
969
+ if not _full_aniso_conductive(solver):
970
+ return
971
+ currents = solver._aniso_cond_current
972
+ periodic_x, periodic_y, periodic_z = _full_aniso_periodic_flags(solver)
973
+ solver.fdtd_module.applyAnisoOffdiagCurrentEx3D(
974
+ Ex=solver.Ex,
975
+ Jy=currents["Ey"],
976
+ Jz=currents["Ez"],
977
+ CoeffY=solver.cex_aniso_y,
978
+ CoeffZ=solver.cex_aniso_z,
979
+ periodicX=periodic_x,
980
+ periodicY=periodic_y,
981
+ periodicZ=periodic_z,
982
+ ).launchRaw()
983
+ solver.fdtd_module.applyAnisoOffdiagCurrentEy3D(
984
+ Ey=solver.Ey,
985
+ Jx=currents["Ex"],
986
+ Jz=currents["Ez"],
987
+ CoeffX=solver.cey_aniso_x,
988
+ CoeffZ=solver.cey_aniso_z,
989
+ periodicX=periodic_x,
990
+ periodicY=periodic_y,
991
+ periodicZ=periodic_z,
992
+ ).launchRaw()
993
+ solver.fdtd_module.applyAnisoOffdiagCurrentEz3D(
994
+ Ez=solver.Ez,
995
+ Jx=currents["Ex"],
996
+ Jy=currents["Ey"],
997
+ CoeffX=solver.cez_aniso_x,
998
+ CoeffY=solver.cez_aniso_y,
999
+ periodicX=periodic_x,
1000
+ periodicY=periodic_y,
1001
+ periodicZ=periodic_z,
1002
+ ).launchRaw()
1003
+
1004
+
1005
+ def update_electric_fields_bloch(solver):
1006
+ if getattr(solver, "nonlinear_enabled", False):
1007
+ raise NotImplementedError(
1008
+ "FDTD instantaneous nonlinear media cannot evolve on the complex Bloch fields: the "
1009
+ "|E|^2 Kerr/chi2/TPA polarization couples the single-wavevector Bloch field to the 2k/3k "
1010
+ "harmonic sectors a complex-Bloch run does not carry, so the nonlinear response is not "
1011
+ "closed on the real update kernels. Use a real-field boundary (PML/PEC/PMC/periodic)."
1012
+ )
1013
+ solver.fdtd_module.updateElectricFieldExBloch3D(
1014
+ ExReal=solver.Ex,
1015
+ ExImag=solver.Ex_imag,
1016
+ HyReal=solver.Hy,
1017
+ HyImag=solver.Hy_imag,
1018
+ HzReal=solver.Hz,
1019
+ HzImag=solver.Hz_imag,
1020
+ ExDecay=solver.cex_decay,
1021
+ ExCurl=solver.cex_curl,
1022
+ phaseCosY=solver.boundary_phase_cos[1],
1023
+ phaseSinY=solver.boundary_phase_sin[1],
1024
+ phaseCosZ=solver.boundary_phase_cos[2],
1025
+ phaseSinZ=solver.boundary_phase_sin[2],
1026
+ invDy=solver.inv_dy_e,
1027
+ invDz=solver.inv_dz_e,
1028
+ ).launchRaw()
1029
+ solver.fdtd_module.updateElectricFieldEyBloch3D(
1030
+ EyReal=solver.Ey,
1031
+ EyImag=solver.Ey_imag,
1032
+ HxReal=solver.Hx,
1033
+ HxImag=solver.Hx_imag,
1034
+ HzReal=solver.Hz,
1035
+ HzImag=solver.Hz_imag,
1036
+ EyDecay=solver.cey_decay,
1037
+ EyCurl=solver.cey_curl,
1038
+ phaseCosX=solver.boundary_phase_cos[0],
1039
+ phaseSinX=solver.boundary_phase_sin[0],
1040
+ phaseCosZ=solver.boundary_phase_cos[2],
1041
+ phaseSinZ=solver.boundary_phase_sin[2],
1042
+ invDx=solver.inv_dx_e,
1043
+ invDz=solver.inv_dz_e,
1044
+ ).launchRaw()
1045
+ solver.fdtd_module.updateElectricFieldEzBloch3D(
1046
+ EzReal=solver.Ez,
1047
+ EzImag=solver.Ez_imag,
1048
+ HxReal=solver.Hx,
1049
+ HxImag=solver.Hx_imag,
1050
+ HyReal=solver.Hy,
1051
+ HyImag=solver.Hy_imag,
1052
+ EzDecay=solver.cez_decay,
1053
+ EzCurl=solver.cez_curl,
1054
+ phaseCosX=solver.boundary_phase_cos[0],
1055
+ phaseSinX=solver.boundary_phase_sin[0],
1056
+ phaseCosY=solver.boundary_phase_cos[1],
1057
+ phaseSinY=solver.boundary_phase_sin[1],
1058
+ invDx=solver.inv_dx_e,
1059
+ invDy=solver.inv_dy_e,
1060
+ ).launchRaw()
1061
+
1062
+
1063
+ def update_electric_fields_bloch_xy_standard_z(solver):
1064
+ if getattr(solver, "nonlinear_enabled", False):
1065
+ raise NotImplementedError(
1066
+ "FDTD instantaneous nonlinear media cannot evolve on the complex Bloch fields: the "
1067
+ "|E|^2 Kerr/chi2/TPA polarization couples the single-wavevector Bloch field to the 2k/3k "
1068
+ "harmonic sectors a complex-Bloch run does not carry, so the nonlinear response is not "
1069
+ "closed on the real update kernels. Use a real-field boundary (PML/PEC/PMC/periodic)."
1070
+ )
1071
+ solver.fdtd_module.updateElectricFieldExBlochYStandardZ3D(
1072
+ ExReal=solver.Ex,
1073
+ ExImag=solver.Ex_imag,
1074
+ HyReal=solver.Hy,
1075
+ HyImag=solver.Hy_imag,
1076
+ HzReal=solver.Hz,
1077
+ HzImag=solver.Hz_imag,
1078
+ ExDecay=solver.cex_decay,
1079
+ ExCurl=solver.cex_curl,
1080
+ phaseCosY=solver.boundary_phase_cos[1],
1081
+ phaseSinY=solver.boundary_phase_sin[1],
1082
+ invDy=solver.inv_dy_e,
1083
+ invDz=solver.inv_dz_e,
1084
+ zLowBoundaryMode=solver.boundary_z_low_code,
1085
+ zHighBoundaryMode=solver.boundary_z_high_code,
1086
+ ).launchRaw()
1087
+ solver.fdtd_module.updateElectricFieldEyBlochXStandardZ3D(
1088
+ EyReal=solver.Ey,
1089
+ EyImag=solver.Ey_imag,
1090
+ HxReal=solver.Hx,
1091
+ HxImag=solver.Hx_imag,
1092
+ HzReal=solver.Hz,
1093
+ HzImag=solver.Hz_imag,
1094
+ EyDecay=solver.cey_decay,
1095
+ EyCurl=solver.cey_curl,
1096
+ phaseCosX=solver.boundary_phase_cos[0],
1097
+ phaseSinX=solver.boundary_phase_sin[0],
1098
+ invDx=solver.inv_dx_e,
1099
+ invDz=solver.inv_dz_e,
1100
+ zLowBoundaryMode=solver.boundary_z_low_code,
1101
+ zHighBoundaryMode=solver.boundary_z_high_code,
1102
+ ).launchRaw()
1103
+ solver.fdtd_module.updateElectricFieldEzBloch3D(
1104
+ EzReal=solver.Ez,
1105
+ EzImag=solver.Ez_imag,
1106
+ HxReal=solver.Hx,
1107
+ HxImag=solver.Hx_imag,
1108
+ HyReal=solver.Hy,
1109
+ HyImag=solver.Hy_imag,
1110
+ EzDecay=solver.cez_decay,
1111
+ EzCurl=solver.cez_curl,
1112
+ phaseCosX=solver.boundary_phase_cos[0],
1113
+ phaseSinX=solver.boundary_phase_sin[0],
1114
+ phaseCosY=solver.boundary_phase_cos[1],
1115
+ phaseSinY=solver.boundary_phase_sin[1],
1116
+ invDx=solver.inv_dx_e,
1117
+ invDy=solver.inv_dy_e,
1118
+ ).launchRaw()
1119
+
1120
+
1121
+ def _bloch_cpml_pml_axis(solver):
1122
+ """Return the single PML axis (``"x"``/``"y"``/``"z"``) of a mixed Bloch+CPML
1123
+ update whose faces are exactly one axis of paired PML and two axes of paired
1124
+ Bloch, or ``None`` for any other face configuration.
1125
+
1126
+ The complex split-field Bloch update carries the transverse wrap phase on the
1127
+ two periodic axes and runs the recursive-convolution CPML stretch on the one
1128
+ absorbing axis; that decomposition exists only for the three single-PML-axis
1129
+ permutations."""
1130
+ axes = ("x", "y", "z")
1131
+ codes = {
1132
+ axis: (
1133
+ int(getattr(solver, f"boundary_{axis}_low_code")),
1134
+ int(getattr(solver, f"boundary_{axis}_high_code")),
1135
+ )
1136
+ for axis in axes
1137
+ }
1138
+ pml_axes = [axis for axis in axes if codes[axis] == (BOUNDARY_PML, BOUNDARY_PML)]
1139
+ bloch_axes = [axis for axis in axes if codes[axis] == (BOUNDARY_BLOCH, BOUNDARY_BLOCH)]
1140
+ if len(pml_axes) == 1 and len(bloch_axes) == 2:
1141
+ return pml_axes[0]
1142
+ return None
1143
+
1144
+
1145
+ def _validate_bloch_cpml_update_layout(solver):
1146
+ pml_axis = _bloch_cpml_pml_axis(solver)
1147
+ if pml_axis is None:
1148
+ raise NotImplementedError(
1149
+ "Mixed Bloch/CPML FDTD updates require exactly one PML axis (both faces PML) and two "
1150
+ "Bloch axes (both faces Bloch). The complex split-field Bloch update carries the "
1151
+ "transverse wrap phase on the two periodic axes while the single absorbing axis runs the "
1152
+ "recursive-convolution CPML stretch, so a face layout that mixes Bloch and PML on one axis "
1153
+ "or leaves more than one absorbing axis is not expressible on that split."
1154
+ )
1155
+ return pml_axis
1156
+
1157
+
1158
+ def _iter_cpml_field_regions(solver, field, curl, attr_name):
1159
+ layout = getattr(solver, "_cpml_memory_layouts", {}).get(attr_name)
1160
+ if layout is None:
1161
+ yield {
1162
+ "field": field,
1163
+ "curl": curl,
1164
+ "psi": getattr(solver, attr_name),
1165
+ "offsets": (0, 0, 0),
1166
+ }
1167
+ return
1168
+
1169
+ axis = int(layout["axis"])
1170
+ for region in iter_cpml_memory_regions(solver, attr_name):
1171
+ length = int(region["length"])
1172
+ start = int(region["offsets"][axis])
1173
+ yield {
1174
+ "field": field.narrow(axis, start, length),
1175
+ "curl": curl.narrow(axis, start, length),
1176
+ "psi": region["psi"],
1177
+ "offsets": region["offsets"],
1178
+ }
1179
+
1180
+
1181
+ def _apply_electric_z_cpml_corrections(solver, ex, ey, hx, hy, *, imag=False):
1182
+ psi_ex_z_attr = _cpml_memory_attr("psi_ex_z", imag=imag)
1183
+ psi_ey_z_attr = _cpml_memory_attr("psi_ey_z", imag=imag)
1184
+ for region in _iter_cpml_field_regions(solver, ex, solver.cex_curl, psi_ex_z_attr):
1185
+ offset_i, offset_j, offset_k = region["offsets"]
1186
+ solver.fdtd_module.applyElectricFieldExCpmlZCorrection3D(
1187
+ Ex=region["field"],
1188
+ Hy=hy,
1189
+ ExCurl=region["curl"],
1190
+ PsiExZ=region["psi"],
1191
+ InvKappaExZ=solver.cpml_inv_kappa_e_z,
1192
+ BExZ=solver.cpml_b_e_z,
1193
+ CExZ=solver.cpml_c_e_z,
1194
+ invDz=solver.inv_dz_e,
1195
+ offsetI=offset_i,
1196
+ offsetJ=offset_j,
1197
+ offsetK=offset_k,
1198
+ yLowBoundaryMode=solver.boundary_y_low_code,
1199
+ yHighBoundaryMode=solver.boundary_y_high_code,
1200
+ fullSizeY=solver.Ex.shape[1],
1201
+ fullSizeZ=solver.Ex.shape[2],
1202
+ ).launchRaw()
1203
+
1204
+ for region in _iter_cpml_field_regions(solver, ey, solver.cey_curl, psi_ey_z_attr):
1205
+ offset_i, offset_j, offset_k = region["offsets"]
1206
+ solver.fdtd_module.applyElectricFieldEyCpmlZCorrection3D(
1207
+ Ey=region["field"],
1208
+ Hx=hx,
1209
+ EyCurl=region["curl"],
1210
+ PsiEyZ=region["psi"],
1211
+ InvKappaEyZ=solver.cpml_inv_kappa_e_z,
1212
+ BEyZ=solver.cpml_b_e_z,
1213
+ CEyZ=solver.cpml_c_e_z,
1214
+ invDz=solver.inv_dz_e,
1215
+ offsetI=offset_i,
1216
+ offsetJ=offset_j,
1217
+ offsetK=offset_k,
1218
+ xLowBoundaryMode=solver.boundary_x_low_code,
1219
+ xHighBoundaryMode=solver.boundary_x_high_code,
1220
+ fullSizeX=solver.Ey.shape[0],
1221
+ fullSizeZ=solver.Ey.shape[2],
1222
+ ).launchRaw()
1223
+
1224
+
1225
+ def _apply_electric_y_cpml_corrections(solver, ex, ez, hx, hz, *, imag=False):
1226
+ # PML axis y: Ex takes the recursive-convolution stretch on its d(Hz)/dy term
1227
+ # (tangent Bloch axis z), Ez on its d(Hx)/dy term (tangent Bloch axis x). The
1228
+ # base full-Bloch update already applied the un-stretched derivatives.
1229
+ psi_ex_y_attr = _cpml_memory_attr("psi_ex_y", imag=imag)
1230
+ psi_ez_y_attr = _cpml_memory_attr("psi_ez_y", imag=imag)
1231
+ for region in _iter_cpml_field_regions(solver, ex, solver.cex_curl, psi_ex_y_attr):
1232
+ offset_i, offset_j, offset_k = region["offsets"]
1233
+ solver.fdtd_module.applyElectricFieldExCpmlYCorrection3D(
1234
+ Ex=region["field"],
1235
+ Hz=hz,
1236
+ ExCurl=region["curl"],
1237
+ PsiExY=region["psi"],
1238
+ InvKappaExY=solver.cpml_inv_kappa_e_y,
1239
+ BExY=solver.cpml_b_e_y,
1240
+ CExY=solver.cpml_c_e_y,
1241
+ invDy=solver.inv_dy_e,
1242
+ offsetI=offset_i,
1243
+ offsetJ=offset_j,
1244
+ offsetK=offset_k,
1245
+ zLowBoundaryMode=solver.boundary_z_low_code,
1246
+ zHighBoundaryMode=solver.boundary_z_high_code,
1247
+ fullSizeZ=solver.Ex.shape[2],
1248
+ fullSizeY=solver.Ex.shape[1],
1249
+ ).launchRaw()
1250
+
1251
+ for region in _iter_cpml_field_regions(solver, ez, solver.cez_curl, psi_ez_y_attr):
1252
+ offset_i, offset_j, offset_k = region["offsets"]
1253
+ solver.fdtd_module.applyElectricFieldEzCpmlYCorrection3D(
1254
+ Ez=region["field"],
1255
+ Hx=hx,
1256
+ EzCurl=region["curl"],
1257
+ PsiEzY=region["psi"],
1258
+ InvKappaEzY=solver.cpml_inv_kappa_e_y,
1259
+ BEzY=solver.cpml_b_e_y,
1260
+ CEzY=solver.cpml_c_e_y,
1261
+ invDy=solver.inv_dy_e,
1262
+ offsetI=offset_i,
1263
+ offsetJ=offset_j,
1264
+ offsetK=offset_k,
1265
+ xLowBoundaryMode=solver.boundary_x_low_code,
1266
+ xHighBoundaryMode=solver.boundary_x_high_code,
1267
+ fullSizeX=solver.Ez.shape[0],
1268
+ fullSizeY=solver.Ez.shape[1],
1269
+ ).launchRaw()
1270
+
1271
+
1272
+ def _apply_electric_x_cpml_corrections(solver, ey, ez, hy, hz, *, imag=False):
1273
+ # PML axis x: Ey takes the stretch on its d(Hz)/dx term (tangent Bloch axis z),
1274
+ # Ez on its d(Hy)/dx term (tangent Bloch axis y).
1275
+ psi_ey_x_attr = _cpml_memory_attr("psi_ey_x", imag=imag)
1276
+ psi_ez_x_attr = _cpml_memory_attr("psi_ez_x", imag=imag)
1277
+ for region in _iter_cpml_field_regions(solver, ey, solver.cey_curl, psi_ey_x_attr):
1278
+ offset_i, offset_j, offset_k = region["offsets"]
1279
+ solver.fdtd_module.applyElectricFieldEyCpmlXCorrection3D(
1280
+ Ey=region["field"],
1281
+ Hz=hz,
1282
+ EyCurl=region["curl"],
1283
+ PsiEyX=region["psi"],
1284
+ InvKappaEyX=solver.cpml_inv_kappa_e_x,
1285
+ BEyX=solver.cpml_b_e_x,
1286
+ CEyX=solver.cpml_c_e_x,
1287
+ invDx=solver.inv_dx_e,
1288
+ offsetI=offset_i,
1289
+ offsetJ=offset_j,
1290
+ offsetK=offset_k,
1291
+ zLowBoundaryMode=solver.boundary_z_low_code,
1292
+ zHighBoundaryMode=solver.boundary_z_high_code,
1293
+ fullSizeZ=solver.Ey.shape[2],
1294
+ fullSizeX=solver.Ey.shape[0],
1295
+ ).launchRaw()
1296
+
1297
+ for region in _iter_cpml_field_regions(solver, ez, solver.cez_curl, psi_ez_x_attr):
1298
+ offset_i, offset_j, offset_k = region["offsets"]
1299
+ solver.fdtd_module.applyElectricFieldEzCpmlXCorrection3D(
1300
+ Ez=region["field"],
1301
+ Hy=hy,
1302
+ EzCurl=region["curl"],
1303
+ PsiEzX=region["psi"],
1304
+ InvKappaEzX=solver.cpml_inv_kappa_e_x,
1305
+ BEzX=solver.cpml_b_e_x,
1306
+ CEzX=solver.cpml_c_e_x,
1307
+ invDx=solver.inv_dx_e,
1308
+ offsetI=offset_i,
1309
+ offsetJ=offset_j,
1310
+ offsetK=offset_k,
1311
+ yLowBoundaryMode=solver.boundary_y_low_code,
1312
+ yHighBoundaryMode=solver.boundary_y_high_code,
1313
+ fullSizeY=solver.Ez.shape[1],
1314
+ fullSizeX=solver.Ez.shape[0],
1315
+ ).launchRaw()
1316
+
1317
+
1318
+ def update_electric_fields_bloch_cpml(solver):
1319
+ pml_axis = _validate_bloch_cpml_update_layout(solver)
1320
+ if pml_axis == "z":
1321
+ # Grating-style x/y Bloch + z PML: standard boundary treatment on z keeps
1322
+ # the outer plane exactly zero (mirrored by the adjoint replay).
1323
+ update_electric_fields_bloch_xy_standard_z(solver)
1324
+ _apply_electric_z_cpml_corrections(solver, solver.Ex, solver.Ey, solver.Hx, solver.Hy, imag=False)
1325
+ _apply_electric_z_cpml_corrections(
1326
+ solver,
1327
+ solver.Ex_imag,
1328
+ solver.Ey_imag,
1329
+ solver.Hx_imag,
1330
+ solver.Hy_imag,
1331
+ imag=True,
1332
+ )
1333
+ return
1334
+ # General single-PML-axis permutation (x or y): the base full-Bloch update
1335
+ # carries the true wrap phase on both periodic axes and the natural unit phase
1336
+ # on the absorbing axis, then the recursive-convolution stretch is folded in
1337
+ # along that axis for the two components with an absorbing-axis derivative.
1338
+ update_electric_fields_bloch(solver)
1339
+ if pml_axis == "y":
1340
+ _apply_electric_y_cpml_corrections(solver, solver.Ex, solver.Ez, solver.Hx, solver.Hz, imag=False)
1341
+ _apply_electric_y_cpml_corrections(
1342
+ solver, solver.Ex_imag, solver.Ez_imag, solver.Hx_imag, solver.Hz_imag, imag=True
1343
+ )
1344
+ else:
1345
+ _apply_electric_x_cpml_corrections(solver, solver.Ey, solver.Ez, solver.Hy, solver.Hz, imag=False)
1346
+ _apply_electric_x_cpml_corrections(
1347
+ solver, solver.Ey_imag, solver.Ez_imag, solver.Hy_imag, solver.Hz_imag, imag=True
1348
+ )
1349
+
1350
+
1351
+ def clamp_field_face(solver, field, axis, side):
1352
+ side_code = 0 if side == "low" else 1
1353
+ module = getattr(solver, "fdtd_module", None)
1354
+ if module is not None and module.__class__.__name__ == "NativeFDTDModule":
1355
+ module.clampFieldFace3D(field=field, axis=int(axis), side=side_code).launchRaw()
1356
+ return
1357
+ index = 0 if side_code == 0 else field.shape[axis] - 1
1358
+ field.select(axis, index).zero_()
1359
+
1360
+
1361
+ def _launch_native_clamp(solver, kernel_name, **kwargs):
1362
+ module = getattr(solver, "fdtd_module", None)
1363
+ if module is None or module.__class__.__name__ != "NativeFDTDModule":
1364
+ return False
1365
+ getattr(module, kernel_name)(**kwargs).launchRaw()
1366
+ return True
1367
+
1368
+
1369
+ def _enforce_native_pec_field(solver, field, axis_a, low_a, high_a, axis_b, low_b, high_b):
1370
+ sides_a = (low_a == BOUNDARY_PEC, high_a == BOUNDARY_PEC)
1371
+ sides_b = (low_b == BOUNDARY_PEC, high_b == BOUNDARY_PEC)
1372
+ if not any(sides_a) and not any(sides_b):
1373
+ return True
1374
+
1375
+ if all(sides_a) and all(sides_b):
1376
+ return _launch_native_clamp(solver, "clampPecBoundary3D", field=field, axisA=axis_a, axisB=axis_b)
1377
+ if all(sides_a):
1378
+ if not _launch_native_clamp(solver, "clampPecBoundary3D", field=field, axisA=axis_a, axisB=axis_a):
1379
+ return False
1380
+ else:
1381
+ for side_code, is_pec in enumerate(sides_a):
1382
+ if is_pec and not _launch_native_clamp(
1383
+ solver, "clampFieldFace3D", field=field, axis=axis_a, side=side_code
1384
+ ):
1385
+ return False
1386
+
1387
+ if all(sides_b):
1388
+ return _launch_native_clamp(solver, "clampPecBoundary3D", field=field, axisA=axis_b, axisB=axis_b)
1389
+ for side_code, is_pec in enumerate(sides_b):
1390
+ if is_pec and not _launch_native_clamp(solver, "clampFieldFace3D", field=field, axis=axis_b, side=side_code):
1391
+ return False
1392
+ return True
1393
+
1394
+
1395
+ def enforce_pec_boundaries(solver):
1396
+ if not getattr(solver, "has_pec_faces", False):
1397
+ return
1398
+
1399
+ if _enforce_native_pec_field(
1400
+ solver,
1401
+ solver.Ex,
1402
+ 1,
1403
+ solver.boundary_y_low_code,
1404
+ solver.boundary_y_high_code,
1405
+ 2,
1406
+ solver.boundary_z_low_code,
1407
+ solver.boundary_z_high_code,
1408
+ ) and _enforce_native_pec_field(
1409
+ solver,
1410
+ solver.Ey,
1411
+ 0,
1412
+ solver.boundary_x_low_code,
1413
+ solver.boundary_x_high_code,
1414
+ 2,
1415
+ solver.boundary_z_low_code,
1416
+ solver.boundary_z_high_code,
1417
+ ) and _enforce_native_pec_field(
1418
+ solver,
1419
+ solver.Ez,
1420
+ 0,
1421
+ solver.boundary_x_low_code,
1422
+ solver.boundary_x_high_code,
1423
+ 1,
1424
+ solver.boundary_y_low_code,
1425
+ solver.boundary_y_high_code,
1426
+ ):
1427
+ return
1428
+
1429
+ face_specs = (
1430
+ (solver.boundary_x_low_code, "low", (("Ey", 0), ("Ez", 0))),
1431
+ (solver.boundary_x_high_code, "high", (("Ey", 0), ("Ez", 0))),
1432
+ (solver.boundary_y_low_code, "low", (("Ex", 1), ("Ez", 1))),
1433
+ (solver.boundary_y_high_code, "high", (("Ex", 1), ("Ez", 1))),
1434
+ (solver.boundary_z_low_code, "low", (("Ex", 2), ("Ey", 2))),
1435
+ (solver.boundary_z_high_code, "high", (("Ex", 2), ("Ey", 2))),
1436
+ )
1437
+ for boundary_code, side, targets in face_specs:
1438
+ if boundary_code != BOUNDARY_PEC:
1439
+ continue
1440
+ for field_name, axis in targets:
1441
+ clamp_field_face(solver, getattr(solver, field_name), axis, side)
1442
+
1443
+
1444
+ def apply_sibc_surface(solver):
1445
+ """Override the tangential E on a ``LossyMetalMedium`` surface via the Leontovich
1446
+ surface-impedance relation ``E_t = Zs * (n_hat x H)`` (passive/absorbing branch).
1447
+
1448
+ The surface impedance is a narrowband series R-L, ``Zs(omega0) = R + j*omega0*Ls``,
1449
+ so in the time domain each tangential face reads the vacuum-side tangential H
1450
+ (index-aligned on the Yee grid by ``_configure_sibc``) and forms
1451
+ ``E_surface = sign * (R * H + Ls * dH/dt)``, storing the previous H for the
1452
+ surface-inductance dH/dt term. The metal interior is masked to zero by the
1453
+ coefficient setup, so the surface acts as a semi-infinite good-conductor
1454
+ termination at roughly a skin-depth-free (>=10x coarser) cell size.
1455
+ """
1456
+ state = getattr(solver, "_sibc", None)
1457
+ if state is None:
1458
+ return
1459
+ surface_r = state["surface_r"]
1460
+ surface_l_over_dt = state["surface_l"] / solver.dt
1461
+ for face in state["faces"]:
1462
+ solver.fdtd_module.applySibcSurface3D(
1463
+ electric=getattr(solver, face["e_name"]),
1464
+ magnetic=getattr(solver, face["h_name"]),
1465
+ axis=face["axis"],
1466
+ electricIndex=face["electric_index"],
1467
+ magneticIndex=face["magnetic_index"],
1468
+ sign=face["sign"],
1469
+ surfaceR=surface_r,
1470
+ surfaceLOverDt=surface_l_over_dt,
1471
+ hPrev=face["h_prev"],
1472
+ ).launchRaw()
1473
+
1474
+
1475
+ def clamp_pec_boundaries(solver):
1476
+ enforce_pec_boundaries(solver)
1477
+
1478
+
1479
+ def apply_mur_boundaries(solver):
1480
+ # First-order Mur absorbing boundary on the CUDA E-field tensors after the
1481
+ # interior E-update. For an outer face:
1482
+ # E_boundary(n+1) = E_adjacent(n) + coef * (E_adjacent(n+1) - E_boundary(n))
1483
+ # with coef = (c*dt - d) / (c*dt + d) and d the normal cell size. Each face is
1484
+ # a single native-CUDA kernel launch that updates its persistent boundary /
1485
+ # first-interior plane buffers in place, so it stays capturable into the tail
1486
+ # CUDA graph and carries no per-step host arithmetic or allocation.
1487
+ if not getattr(solver, "has_mur_faces", False):
1488
+ return
1489
+
1490
+ for entry in solver._mur_state:
1491
+ solver.fdtd_module.applyMurBoundary3D(
1492
+ field=getattr(solver, entry["field"]),
1493
+ axis=entry["axis"],
1494
+ boundaryIndex=entry["boundary_index"],
1495
+ adjacentIndex=entry["adjacent_index"],
1496
+ coef=entry["coef"],
1497
+ prevBoundary=entry["prev_boundary"],
1498
+ prevAdjacent=entry["prev_adjacent"],
1499
+ ).launchRaw()
1500
+
1501
+
1502
+ def init_field(solver):
1503
+ solver.Ex = torch.zeros((solver.Nx - 1, solver.Ny, solver.Nz), device=solver.device, dtype=torch.float32)
1504
+ solver.Ey = torch.zeros((solver.Nx, solver.Ny - 1, solver.Nz), device=solver.device, dtype=torch.float32)
1505
+ solver.Ez = torch.zeros((solver.Nx, solver.Ny, solver.Nz - 1), device=solver.device, dtype=torch.float32)
1506
+ solver.Hx = torch.zeros((solver.Nx, solver.Ny - 1, solver.Nz - 1), device=solver.device, dtype=torch.float32)
1507
+ solver.Hy = torch.zeros((solver.Nx - 1, solver.Ny, solver.Nz - 1), device=solver.device, dtype=torch.float32)
1508
+ solver.Hz = torch.zeros((solver.Nx - 1, solver.Ny - 1, solver.Nz), device=solver.device, dtype=torch.float32)
1509
+
1510
+ solver.build_materials(solver.scene)
1511
+ initialize_boundary_state(solver)
1512
+ solver._build_update_coefficients()
1513
+ solver._initialize_dispersive_state()
1514
+ solver._initialize_magnetic_dispersive_state()
1515
+ initialize_tfsf_state(solver)
1516
+ initialize_source_terms(solver)
1517
+
1518
+
1519
+ def _compute_shutoff_min_step(solver, shutoff_check_interval: int) -> int:
1520
+ import math
1521
+
1522
+ source_time = getattr(solver, "_source_time", None)
1523
+ settling_time = 0.0
1524
+ if source_time is not None:
1525
+ if isinstance(source_time, dict):
1526
+ settling_time = float(source_time.get("settling_time", 0.0))
1527
+ else:
1528
+ settling_time = float(getattr(source_time, "settling_time", 0.0))
1529
+ settling_step = int(math.ceil(settling_time / solver.dt)) if settling_time > 0.0 else 0
1530
+
1531
+ dft_start_step = getattr(solver, "dft_start_step", None)
1532
+
1533
+ observer_entries = getattr(solver, "_observer_spectral_entries", None)
1534
+ observer_start = 0
1535
+ if observer_entries:
1536
+ observer_start = min(int(entry["start_step"]) for entry in observer_entries)
1537
+
1538
+ floor = max(settling_step, dft_start_step or 0, observer_start or 0)
1539
+ return floor + 2 * shutoff_check_interval
1540
+
1541
+
1542
+ def _electric_field_energy(solver) -> torch.Tensor:
1543
+ energy = (
1544
+ (solver.eps_Ex * solver.Ex * solver.Ex).sum()
1545
+ + (solver.eps_Ey * solver.Ey * solver.Ey).sum()
1546
+ + (solver.eps_Ez * solver.Ez * solver.Ez).sum()
1547
+ )
1548
+ if has_complex_fields(solver):
1549
+ energy = (
1550
+ energy
1551
+ + (solver.eps_Ex * solver.Ex_imag * solver.Ex_imag).sum()
1552
+ + (solver.eps_Ey * solver.Ey_imag * solver.Ey_imag).sum()
1553
+ + (solver.eps_Ez * solver.Ez_imag * solver.Ez_imag).sum()
1554
+ )
1555
+ return energy
1556
+
1557
+
1558
+ def _planned_window_normalization(window_type: str, start_step: int, end_step: int) -> float:
1559
+ # Sum the spectral window weights over the full planned [start_step, end_step)
1560
+ # range, matching compute_window_weight / accumulate_dft exactly for a full run.
1561
+ total = int(end_step) - int(start_step)
1562
+ if total <= 0:
1563
+ return 0.0
1564
+ if window_type == "none":
1565
+ return float(total)
1566
+ positions = np.arange(total, dtype=np.float64) / total
1567
+ if window_type == "hanning":
1568
+ weights = 0.5 * (1.0 - np.cos(2.0 * np.pi * positions))
1569
+ return float(weights.sum())
1570
+ if window_type == "ramp":
1571
+ ramp_fraction = 0.1
1572
+ weights = np.ones_like(positions)
1573
+ ramp_mask = positions < ramp_fraction
1574
+ weights[ramp_mask] = 0.5 * (1.0 - np.cos(np.pi * positions[ramp_mask] / ramp_fraction))
1575
+ return float(weights.sum())
1576
+ return float(total)
1577
+
1578
+
1579
+ def _complete_spectral_normalization(solver, time_steps: int) -> None:
1580
+ # After an early auto-shutoff the omitted tail steps carry negligible field, so
1581
+ # the running-DFT / observer numerators are already complete. Restore each
1582
+ # spectral normalizer to its planned full-window value so the
1583
+ # 2 / window_normalization scale is not inflated by the shortened run.
1584
+ if getattr(solver, "dft_enabled", False) and getattr(solver, "_dft_entries", None):
1585
+ for index in range(len(solver._dft_entries)):
1586
+ start_step = int(solver._dft_start_steps[index])
1587
+ end_step = int(solver._dft_end_steps[index])
1588
+ if end_step < 0:
1589
+ end_step = int(time_steps)
1590
+ solver._dft_window_normalization_values[index] = _planned_window_normalization(
1591
+ solver.dft_window_type, start_step, end_step
1592
+ )
1593
+ if getattr(solver, "observers_enabled", False):
1594
+ for entry in solver._observer_spectral_entries:
1595
+ end_step = entry["end_step"] if entry["end_step"] is not None else int(time_steps)
1596
+ entry["window_normalization"] = _planned_window_normalization(
1597
+ solver.observer_window_type, int(entry["start_step"]), int(end_step)
1598
+ )
1599
+
1600
+
1601
+ def _field_update_block(solver, time_value):
1602
+ """One step of the time-marching field-update core: the magnetic and electric
1603
+ Yee updates plus their in-place dispersive/Kerr/CPML/Bloch state advances.
1604
+
1605
+ This is exactly the contiguous, time-marching part of the step that carries no
1606
+ per-step host input when TFSF and magnetic sources are absent, so it can be
1607
+ captured once into a CUDA graph and replayed (additive electric sources and
1608
+ the running DFT stay outside the graph; native SIBC remains inside). Kept as
1609
+ a single function so the normal and graph-captured paths execute an identical
1610
+ kernel sequence.
1611
+ """
1612
+ if getattr(solver, "modulation_enabled", False):
1613
+ solver.fdtd_module.advanceModulationTime3D(
1614
+ ModulationTime=solver._modulation_time,
1615
+ dt=solver.dt,
1616
+ ).launchRaw()
1617
+ solver._advance_magnetic_dispersive_state()
1618
+ update_magnetic_fields(solver, solver.Hx, solver.Hy, solver.Hz, solver.Ex, solver.Ey, solver.Ez)
1619
+ if has_complex_fields(solver):
1620
+ update_magnetic_fields(
1621
+ solver,
1622
+ solver.Hx_imag,
1623
+ solver.Hy_imag,
1624
+ solver.Hz_imag,
1625
+ solver.Ex_imag,
1626
+ solver.Ey_imag,
1627
+ solver.Ez_imag,
1628
+ imag=True,
1629
+ )
1630
+ if solver.tfsf_enabled:
1631
+ apply_tfsf_h_correction(solver, time_value)
1632
+ advance_tfsf_auxiliary_magnetic(solver)
1633
+ if solver._magnetic_source_terms:
1634
+ inject_magnetic_surface_source_terms(solver, time_value=time_value)
1635
+ solver._apply_magnetic_dispersive_corrections()
1636
+
1637
+ if has_complex_fields(solver):
1638
+ solver._advance_dispersive_state()
1639
+ if solver.uses_cpml:
1640
+ update_electric_fields_bloch_cpml(solver)
1641
+ else:
1642
+ update_electric_fields_bloch(solver)
1643
+ else:
1644
+ solver._advance_dispersive_state()
1645
+ if solver.nonlinear_enabled:
1646
+ solver._update_nonlinear_electric_coefficients()
1647
+ # Snapshot sigma . E^n before the base update overwrites E (no-op unless the
1648
+ # medium is both full-anisotropic and conductive).
1649
+ capture_aniso_conduction_currents(solver)
1650
+ update_electric_fields(
1651
+ solver,
1652
+ solver.Ex,
1653
+ solver.Ey,
1654
+ solver.Ez,
1655
+ solver.Hx,
1656
+ solver.Hy,
1657
+ solver.Hz,
1658
+ time_value=time_value,
1659
+ )
1660
+ if getattr(solver, "full_aniso_enabled", False):
1661
+ apply_full_aniso_corrections(solver)
1662
+ apply_full_aniso_conduction(solver)
1663
+ apply_sibc_surface(solver)
1664
+
1665
+
1666
+ def _make_field_update_runner(solver, use_cuda_graph: bool):
1667
+ """Return ``run(time_value)`` for the field-update block, capturing it into a
1668
+ CUDA graph when requested and safe. Falls back to the direct call for TFSF /
1669
+ magnetic-source scenes (whose block carries per-step host input) or if capture
1670
+ is unavailable. Capture happens on the zero initial field, a fixed point of
1671
+ the source-free block, so it does not perturb the physical run.
1672
+ """
1673
+ solver._cuda_graph_active = False
1674
+
1675
+ def normal(time_value):
1676
+ _field_update_block(solver, time_value)
1677
+
1678
+ # v1 scope: the standard real-field path (optionally conductive), linear
1679
+ # electric/magnetic dispersion, the instantaneous nonlinear family (Kerr chi3,
1680
+ # chi2, and two-photon-absorption loss), the complex split-field Bloch path
1681
+ # (optionally with a single CPML axis), and reference-provider TFSF injection
1682
+ # (axis-aligned / x-Ez plane waves, in both the six-face box and the two-face
1683
+ # non-periodic slab injection mode). Magnetic surface sources put a
1684
+ # host-evaluated signal scalar inside the block, so those scenes stay on the
1685
+ # eager path.
1686
+ #
1687
+ # A reference-provider TFSF scene is safe to capture: its in-block H correction
1688
+ # and auxiliary magnetic advance read the incident field from the
1689
+ # device-resident auxiliary 1D grid (aux.electric / aux.magnetic) at fixed
1690
+ # integer sample indices, so the block carries no per-step host input, and the
1691
+ # reference injection kernel's warp-aggregated writes are collision-free for the
1692
+ # axis-aligned face layout (bit-reproducible run to run). The 1D source waveform
1693
+ # is evaluated on the host only in the E-side auxiliary advance, which runs
1694
+ # eagerly after the block (outside the captured region), and the captured H
1695
+ # correction reads whatever aux.electric holds at replay -- exactly matching the
1696
+ # eager path. The block mutates only aux.magnetic among the auxiliary tensors;
1697
+ # it is snapshotted below alongside aux.electric so warmup/capture on the zero
1698
+ # field leave the incident line untouched for the physical run, and the incident
1699
+ # line is a zero fixed point during capture (aux.electric stays zero until the
1700
+ # eager E-side advance injects the source after capture). The oblique
1701
+ # auxiliary-line provider is excluded (its interpolated injection kernel is not
1702
+ # bit-reproducible under cross-warp atomics), and the oblique-CW / analytic
1703
+ # providers evaluate the waveform on the host each step; all stay eager
1704
+ # (see excitation.tfsf_incident_is_gpu_driven).
1705
+ #
1706
+ # Linear dispersion is safe to capture: the Debye/Drude/Lorentz ADE advance and
1707
+ # the polarization-current subtraction launch persistent per-pole state tensors
1708
+ # (polarization/current, allocated once at init) with fixed dt/decay/restoring
1709
+ # scalars, so they carry no per-step host input. The ADE is linear with zero
1710
+ # forcing at E = H = 0, so with all pole state starting at zero the zero initial
1711
+ # field is still a fixed point of the extended block; warmup/capture leaves the
1712
+ # pole state at zero and does not perturb the physical run.
1713
+ #
1714
+ # Instantaneous nonlinearity is safe to capture too: the Kerr curl-only kernel
1715
+ # and the general chi2/TPA coefficient kernel recompute the dynamic curl/decay
1716
+ # tensors in place from the current field, the persistent chi3/chi2/tpa maps,
1717
+ # and fixed dt/eps0 scalars, with no per-step host input. Unlike dispersion they
1718
+ # carry no accumulated auxiliary state: the dynamic coefficients are a pure
1719
+ # function of the live field, rewritten before they are read every step, so the
1720
+ # fields+psi snapshot already covers everything the block mutates. Every
1721
+ # nonlinear polarization vanishes at E = 0 (P ~ chi2 E^2, chi3 |E|^2 E, and
1722
+ # sigma_NL ~ |E|^2), collapsing the dynamic coefficients to their linear values,
1723
+ # so the zero field remains a fixed point through warmup/capture.
1724
+ #
1725
+ # The complex (split-field Bloch) path is safe to capture too: a Bloch-periodic
1726
+ # scene marches a second real FDTD copy (the imaginary split field) through the
1727
+ # same magnetic/electric curl+decay kernels, coupled only by a fixed per-axis
1728
+ # wrap phase (cos/sin of the Bloch wavevector, constant for the whole run), so
1729
+ # the block carries no per-step host input. The extra state it evolves -- the
1730
+ # imaginary E/H fields and their CPML psi memory -- is snapshotted alongside the
1731
+ # real fields below (the imaginary psi tensors already match the ``psi`` prefix),
1732
+ # and the split update is linear with zero forcing at E = H = 0, so the zero
1733
+ # field (real and imaginary) stays a fixed point through warmup/capture. The
1734
+ # complex running DFT keeps the host tail (build_dft_step_tables declines the GPU
1735
+ # weight table for complex fields), so only the field-update block is captured.
1736
+ graphable = (
1737
+ use_cuda_graph
1738
+ and torch.cuda.is_available()
1739
+ # TFSF is capturable only for the reference providers, whose in-block
1740
+ # correction reads the device-resident incident line through the
1741
+ # bit-reproducible integer-indexed reference kernel and carries no per-step
1742
+ # host input (see _make_field_update_runner docstring above).
1743
+ and (not solver.tfsf_enabled or tfsf_incident_is_gpu_driven(solver))
1744
+ # A magnetic surface source injects a host-evaluated signal scalar inside
1745
+ # the block, which a captured graph would freeze at its capture-time value.
1746
+ and not solver._magnetic_source_terms
1747
+ )
1748
+ if not graphable:
1749
+ return normal
1750
+ from ..cuda.runtime.graph import CudaGraphRunner
1751
+
1752
+ # Snapshot the state this block mutates (real + imaginary fields, CPML psi) so
1753
+ # warmup/capture do not perturb the physical run, then restore before stepping
1754
+ # begins. The imaginary split fields exist only on the complex Bloch path; on
1755
+ # the real path they are absent from ``vars(solver)`` and drop out naturally.
1756
+ snapshot_field_names = (
1757
+ "Ex", "Ey", "Ez", "Hx", "Hy", "Hz",
1758
+ "Ex_imag", "Ey_imag", "Ez_imag", "Hx_imag", "Hy_imag", "Hz_imag",
1759
+ )
1760
+ state = {
1761
+ k: v
1762
+ for k, v in vars(solver).items()
1763
+ if isinstance(v, torch.Tensor)
1764
+ and (k in snapshot_field_names or k.startswith("psi") or k == "_modulation_time")
1765
+ }
1766
+ # The TFSF field-update block advances the auxiliary 1D magnetic grid in place
1767
+ # (and reads the auxiliary electric grid). Snapshot both auxiliary tensors so
1768
+ # warmup/capture on the zero field leave the incident line exactly as the
1769
+ # physical run expects; the "psi" / field filter above does not reach them
1770
+ # because they live on the auxiliary-grid object, not on ``vars(solver)``.
1771
+ if getattr(solver, "tfsf_enabled", False) and getattr(solver, "_tfsf_state", None):
1772
+ aux = solver._tfsf_state.get("auxiliary_grid")
1773
+ if aux is not None:
1774
+ state["_tfsf_aux_electric"] = aux.electric
1775
+ state["_tfsf_aux_magnetic"] = aux.magnetic
1776
+ sibc_state = getattr(solver, "_sibc", None)
1777
+ if sibc_state is not None:
1778
+ for index, face in enumerate(sibc_state["faces"]):
1779
+ state[f"_sibc_h_prev_{index}"] = face["h_prev"]
1780
+ saved = {k: v.clone() for k, v in state.items()}
1781
+
1782
+ def _restore():
1783
+ for k, v in saved.items():
1784
+ state[k].copy_(v)
1785
+
1786
+ try:
1787
+ runner = CudaGraphRunner(enabled=True, warmup_steps=3)
1788
+ replay = runner.capture(lambda: _field_update_block(solver, 0.0))
1789
+ except Exception:
1790
+ # Any capture failure (e.g. a non-capturable kernel) degrades to the
1791
+ # normal path rather than breaking the solve.
1792
+ _restore()
1793
+ return normal
1794
+ _restore()
1795
+ solver._cuda_graph_active = True
1796
+ return lambda time_value: replay()
1797
+
1798
+
1799
+ def _post_source_block(solver):
1800
+ """The time-independent tail after source injection: dispersive corrections,
1801
+ PEC clamp, Mur ABC, and the GPU-driven running-DFT accumulation. Captured as
1802
+ a second graph so its kernel launches collapse to one replay; only valid when
1803
+ the DFT is GPU-driven (``build_dft_step_tables`` succeeded)."""
1804
+ solver._apply_dispersive_corrections()
1805
+ if not solver.tfsf_enabled:
1806
+ enforce_pec_boundaries(solver)
1807
+ apply_mur_boundaries(solver)
1808
+ solver.accumulate_dft_gpu()
1809
+
1810
+
1811
+ def _make_tail_runner(solver, use_gpu_dft: bool):
1812
+ """Capture the post-source tail (PEC + Mur + GPU-DFT) into a graph. Returns a
1813
+ replay callable, or ``None`` to keep the inline tail. Snapshots the state the
1814
+ tail mutates (fields, the device step counter, the DFT accumulators) around
1815
+ capture so warmup/capture on the zero field do not perturb the run."""
1816
+ if not (use_gpu_dft and getattr(solver, "_cuda_graph_active", False)):
1817
+ return None
1818
+ from ..cuda.runtime.graph import CudaGraphRunner
1819
+
1820
+ state = {}
1821
+ for name in ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz"):
1822
+ tensor = getattr(solver, name, None)
1823
+ if isinstance(tensor, torch.Tensor):
1824
+ state[("field", name)] = tensor
1825
+ if isinstance(getattr(solver, "_dft_step", None), torch.Tensor):
1826
+ state[("step",)] = solver._dft_step
1827
+ for comp, buffers in getattr(solver, "_dft_batched_fields", {}).items():
1828
+ for key in ("real", "imag"):
1829
+ tensor = buffers.get(key)
1830
+ if isinstance(tensor, torch.Tensor):
1831
+ state[("dft", comp, key)] = tensor
1832
+ saved = {k: v.clone() for k, v in state.items()}
1833
+
1834
+ def _restore():
1835
+ for k, v in saved.items():
1836
+ state[k].copy_(v)
1837
+
1838
+ try:
1839
+ replay = CudaGraphRunner(enabled=True, warmup_steps=3).capture(
1840
+ lambda: _post_source_block(solver)
1841
+ )
1842
+ except Exception:
1843
+ _restore()
1844
+ return None
1845
+ _restore()
1846
+ return replay
1847
+
1848
+
1849
+ def solve(
1850
+ solver,
1851
+ time_steps: int,
1852
+ dft_frequency: float = None,
1853
+ enable_plot: bool = False,
1854
+ dft_window: str = "hanning",
1855
+ full_field_dft: bool = True,
1856
+ normalize_source: bool = False,
1857
+ shutoff: float = 0.0,
1858
+ shutoff_check_interval: int = 100,
1859
+ use_cuda_graph: bool = False,
1860
+ ):
1861
+ if solver.verbose:
1862
+ print(f"Starting 3D FDTD simulation (Yee grid), grid size: {solver.Nx}x{solver.Ny}x{solver.Nz}")
1863
+ if normalize_source and len(getattr(solver, "_compiled_sources", ())) != 1:
1864
+ raise NotImplementedError("normalize_source currently requires exactly one compiled source.")
1865
+ solver._normalize_source = normalize_source
1866
+ solver._synchronize_device()
1867
+ solve_start = time.perf_counter()
1868
+
1869
+ if dft_frequency is not None and full_field_dft:
1870
+ solver.enable_dft(dft_frequency, window_type=dft_window, end_step=time_steps)
1871
+ else:
1872
+ solver.dft_enabled = False
1873
+ solver._dft_entries = []
1874
+ solver._sync_dft_legacy_state()
1875
+
1876
+ observer_frequency = dft_frequency if dft_frequency is not None else solver.source_frequency
1877
+ if solver.observers:
1878
+ solver._prepare_observers(observer_frequency, dft_window, time_steps)
1879
+ if getattr(solver, "time_observers", None):
1880
+ solver._prepare_time_observers(time_steps)
1881
+
1882
+ solver._shutoff_triggered = False
1883
+ solver._shutoff_step = None
1884
+ solver._shutoff_peak = torch.zeros((), device=solver.device, dtype=solver.Ex.dtype)
1885
+ shutoff_min_step = _compute_shutoff_min_step(solver, shutoff_check_interval)
1886
+
1887
+ run_field_update = _make_field_update_runner(solver, use_cuda_graph)
1888
+
1889
+ # When the field-update graph is active, drive the running DFT from a
1890
+ # precomputed GPU weight table indexed by a device step counter, dropping the
1891
+ # per-step host arithmetic and host->device transfer of the DFT weights.
1892
+ use_gpu_dft = False
1893
+ if getattr(solver, "_cuda_graph_active", False) and getattr(solver, "dft_enabled", False):
1894
+ use_gpu_dft = solver.build_dft_step_tables(time_steps)
1895
+ run_tail = _make_tail_runner(solver, use_gpu_dft)
1896
+ solver._tail_graph_active = run_tail is not None
1897
+
1898
+ iterator = range(time_steps)
1899
+ pbar = None
1900
+ if solver.verbose:
1901
+ pbar = tqdm(
1902
+ iterator,
1903
+ desc="FDTD",
1904
+ unit="step",
1905
+ bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}{postfix}]",
1906
+ )
1907
+ iterator = pbar
1908
+
1909
+ for n in iterator:
1910
+ time_value = n * solver.dt
1911
+ run_field_update(time_value)
1912
+
1913
+ if solver.tfsf_enabled:
1914
+ apply_tfsf_e_correction(solver, time_value)
1915
+ advance_tfsf_auxiliary_electric(solver)
1916
+ if solver._electric_source_terms:
1917
+ # The electric equivalent current enters the E update through the
1918
+ # incident H field, which lives at the Yee half step. Sample it at
1919
+ # the same n + 1/2 instant as the TFSF electric correction.
1920
+ inject_electric_surface_source_terms(
1921
+ solver,
1922
+ time_value=time_value + 0.5 * float(solver.dt),
1923
+ )
1924
+
1925
+ if solver._source_terms:
1926
+ solver.add_source(time_value=time_value)
1927
+ if run_tail is not None:
1928
+ run_tail()
1929
+ else:
1930
+ solver._apply_dispersive_corrections()
1931
+ if not solver.tfsf_enabled:
1932
+ enforce_pec_boundaries(solver)
1933
+ apply_mur_boundaries(solver)
1934
+ if use_gpu_dft:
1935
+ solver.accumulate_dft_gpu()
1936
+ else:
1937
+ solver.accumulate_dft(n)
1938
+ solver.accumulate_observers(n)
1939
+ solver.accumulate_time_observers(n)
1940
+
1941
+ if shutoff > 0 and (n + 1) % shutoff_check_interval == 0:
1942
+ e_energy = _electric_field_energy(solver)
1943
+ solver._shutoff_peak = torch.maximum(solver._shutoff_peak, e_energy)
1944
+ should_stop = (
1945
+ n >= shutoff_min_step
1946
+ and bool(((solver._shutoff_peak > 0.0) & (e_energy < shutoff * solver._shutoff_peak)).item())
1947
+ )
1948
+ if should_stop:
1949
+ solver._shutoff_triggered = True
1950
+ solver._shutoff_step = n
1951
+ break
1952
+
1953
+ if pbar is not None:
1954
+ should_update_progress = ((n + 1) % solver.progress_update_interval == 0 or n == time_steps - 1)
1955
+ if should_update_progress and solver.dft_enabled and solver.dft_start_step is not None:
1956
+ if n < solver.dft_start_step:
1957
+ pbar.set_postfix({"status": "transient"})
1958
+ else:
1959
+ pbar.set_postfix({"status": "DFT accumulation", "samples": solver.dft_sample_count})
1960
+
1961
+ if enable_plot and n % solver.plot_interval == 0:
1962
+ visualize_slice(solver, n)
1963
+
1964
+ solver._synchronize_device()
1965
+ solver.last_solve_elapsed_s = time.perf_counter() - solve_start
1966
+ if solver._shutoff_triggered:
1967
+ _complete_spectral_normalization(solver, time_steps)
1968
+ if solver.dft_enabled:
1969
+ solver._sync_dft_legacy_state()
1970
+ if solver.observers_enabled:
1971
+ solver._sync_observer_legacy_state()
1972
+ if solver.verbose:
1973
+ print(
1974
+ f"FDTD solve elapsed: {solver.last_solve_elapsed_s:.3f}s "
1975
+ f"({solver.last_solve_elapsed_s * 1e3 / max(time_steps, 1):.3f} ms/step)"
1976
+ )
1977
+
1978
+ output = {}
1979
+ if solver.dft_enabled:
1980
+ output.update(solver.get_frequency_solution(all_frequencies=True))
1981
+ monitors = solver.get_observer_results() if solver.observers_enabled else {}
1982
+ if getattr(solver, "time_observers_enabled", False):
1983
+ monitors = dict(monitors)
1984
+ monitors.update(solver.get_time_observer_results())
1985
+ if monitors:
1986
+ output["observers"] = monitors
1987
+ return output or None