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,31 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ import torch
6
+
7
+ _VALID_FDTD_BACKENDS = {"cuda", "auto"}
8
+
9
+
10
+ def resolve_fdtd_backend_name(requested: str | None = None) -> str:
11
+ backend = (requested or os.environ.get("WITWIN_MAXWELL_FDTD_BACKEND", "cuda")).strip().lower()
12
+ if backend not in _VALID_FDTD_BACKENDS:
13
+ choices = ", ".join(sorted(_VALID_FDTD_BACKENDS))
14
+ raise ValueError(f"WITWIN_MAXWELL_FDTD_BACKEND must be one of: {choices}.")
15
+ if backend == "auto":
16
+ return "cuda"
17
+ return backend
18
+
19
+
20
+ def get_fdtd_module():
21
+ from ..cuda.backend import get_native_fdtd_module
22
+
23
+ return get_native_fdtd_module()
24
+
25
+
26
+ def require_cuda_scene(scene):
27
+ device = torch.device(scene.device)
28
+ if device.type != "cuda":
29
+ raise ValueError(f"FDTD requires scene.device to be CUDA, got {device}.")
30
+ if not torch.cuda.is_available():
31
+ raise RuntimeError("FDTD requires CUDA, but torch.cuda.is_available() is False.")
@@ -0,0 +1,581 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ import numpy as np
6
+ import torch
7
+
8
+ from ..boundary import has_complex_fields
9
+
10
+
11
+ def normalize_target_frequencies(solver, frequencies) -> tuple[float, ...]:
12
+ del solver
13
+ if frequencies is None:
14
+ return ()
15
+ if isinstance(frequencies, Iterable) and not isinstance(frequencies, (str, bytes)):
16
+ values = tuple(float(freq) for freq in frequencies)
17
+ else:
18
+ values = (float(frequencies),)
19
+ if not values:
20
+ raise ValueError("At least one target frequency is required.")
21
+ return values
22
+
23
+
24
+ def reset_dft_runtime_state(solver):
25
+ solver._dft_batched_fields = {}
26
+ solver._dft_phase_cos = None
27
+ solver._dft_phase_sin = None
28
+ solver._dft_phase_step_cos_values = None
29
+ solver._dft_phase_step_sin_values = None
30
+ solver._dft_start_steps = None
31
+ solver._dft_end_steps = None
32
+ solver._dft_window_normalization_values = None
33
+ solver._dft_sample_count_values = None
34
+ solver._dft_source_dft_real_values = None
35
+ solver._dft_source_dft_imag_values = None
36
+
37
+
38
+ def sync_dft_entries_from_runtime_state(solver):
39
+ if not solver._dft_entries or solver._dft_phase_cos is None:
40
+ return
41
+
42
+ for index, entry in enumerate(solver._dft_entries):
43
+ entry["start_step"] = int(solver._dft_start_steps[index])
44
+ end_step = int(solver._dft_end_steps[index])
45
+ entry["end_step"] = None if end_step < 0 else end_step
46
+ entry["window_normalization"] = float(solver._dft_window_normalization_values[index])
47
+ entry["sample_count"] = int(solver._dft_sample_count_values[index])
48
+ entry["phase_cos"] = float(solver._dft_phase_cos[index])
49
+ entry["phase_sin"] = float(solver._dft_phase_sin[index])
50
+ entry["phase_step_cos"] = float(solver._dft_phase_step_cos_values[index])
51
+ entry["phase_step_sin"] = float(solver._dft_phase_step_sin_values[index])
52
+ entry["source_dft_real"] = float(solver._dft_source_dft_real_values[index])
53
+ entry["source_dft_imag"] = float(solver._dft_source_dft_imag_values[index])
54
+
55
+
56
+ def sync_dft_primary_runtime_state(solver):
57
+ if not solver._dft_entries or solver._dft_phase_cos is None:
58
+ solver.dft_sample_count = 0
59
+ solver.dft_window_normalization = 0.0
60
+ solver.dft_phase_step_cos = None
61
+ solver.dft_phase_step_sin = None
62
+ return
63
+
64
+ solver.dft_sample_count = int(solver._dft_sample_count_values[0])
65
+ solver.dft_window_normalization = float(solver._dft_window_normalization_values[0])
66
+ solver.dft_phase_step_cos = float(solver._dft_phase_cos[0])
67
+ solver.dft_phase_step_sin = float(solver._dft_phase_sin[0])
68
+
69
+
70
+ def sync_dft_legacy_state(solver):
71
+ sync_dft_entries_from_runtime_state(solver)
72
+ solver.dft_frequencies = tuple(entry["frequency"] for entry in solver._dft_entries)
73
+ solver.dft_sample_counts = tuple(entry["sample_count"] for entry in solver._dft_entries)
74
+ solver.dft_start_steps = tuple(entry["start_step"] for entry in solver._dft_entries)
75
+ solver.dft_end_steps = tuple(entry["end_step"] for entry in solver._dft_entries)
76
+ solver.dft_window_normalizations = tuple(entry["window_normalization"] for entry in solver._dft_entries)
77
+
78
+ if not solver._dft_entries:
79
+ solver.dft_frequency = None
80
+ solver.dft_Ex_real = None
81
+ solver.dft_Ex_imag = None
82
+ solver.dft_Ey_real = None
83
+ solver.dft_Ey_imag = None
84
+ solver.dft_Ez_real = None
85
+ solver.dft_Ez_imag = None
86
+ solver.dft_Ex_aux_real = None
87
+ solver.dft_Ex_aux_imag = None
88
+ solver.dft_Ey_aux_real = None
89
+ solver.dft_Ey_aux_imag = None
90
+ solver.dft_Ez_aux_real = None
91
+ solver.dft_Ez_aux_imag = None
92
+ solver.dft_sample_count = 0
93
+ solver.dft_start_step = None
94
+ solver.dft_end_step = None
95
+ solver.dft_window_normalization = 0.0
96
+ solver.dft_phase_step_cos = None
97
+ solver.dft_phase_step_sin = None
98
+ reset_dft_runtime_state(solver)
99
+ return
100
+
101
+ primary = solver._dft_entries[0]
102
+ primary_fields = primary["fields"]
103
+ solver.dft_frequency = primary["frequency"]
104
+ solver.dft_Ex_real = primary_fields["Ex"]["real"]
105
+ solver.dft_Ex_imag = primary_fields["Ex"]["imag"]
106
+ solver.dft_Ey_real = primary_fields["Ey"]["real"]
107
+ solver.dft_Ey_imag = primary_fields["Ey"]["imag"]
108
+ solver.dft_Ez_real = primary_fields["Ez"]["real"]
109
+ solver.dft_Ez_imag = primary_fields["Ez"]["imag"]
110
+ solver.dft_Ex_aux_real = primary_fields["Ex"]["aux_real"]
111
+ solver.dft_Ex_aux_imag = primary_fields["Ex"]["aux_imag"]
112
+ solver.dft_Ey_aux_real = primary_fields["Ey"]["aux_real"]
113
+ solver.dft_Ey_aux_imag = primary_fields["Ey"]["aux_imag"]
114
+ solver.dft_Ez_aux_real = primary_fields["Ez"]["aux_real"]
115
+ solver.dft_Ez_aux_imag = primary_fields["Ez"]["aux_imag"]
116
+ solver.dft_sample_count = primary["sample_count"]
117
+ solver.dft_start_step = primary["start_step"]
118
+ solver.dft_end_step = primary["end_step"]
119
+ solver.dft_window_normalization = primary["window_normalization"]
120
+ solver.dft_phase_step_cos = primary["phase_cos"]
121
+ solver.dft_phase_step_sin = primary["phase_sin"]
122
+
123
+
124
+ def sync_observer_legacy_state(solver):
125
+ solver.observer_frequencies = tuple(entry["frequency"] for entry in solver._observer_spectral_entries)
126
+ solver.observer_sample_counts = tuple(entry["sample_count"] for entry in solver._observer_spectral_entries)
127
+ solver.observer_start_steps = tuple(entry["start_step"] for entry in solver._observer_spectral_entries)
128
+ solver.observer_end_steps = tuple(entry["end_step"] for entry in solver._observer_spectral_entries)
129
+
130
+ if not solver._observer_spectral_entries:
131
+ solver.observer_frequency = None
132
+ solver.observer_start_step = None
133
+ solver.observer_end_step = None
134
+ solver.observer_window_normalization = 0.0
135
+ solver.observer_sample_count = 0
136
+ solver.observer_phase_step_cos = None
137
+ solver.observer_phase_step_sin = None
138
+ return
139
+
140
+ primary = solver._observer_spectral_entries[0]
141
+ solver.observer_frequency = primary["frequency"]
142
+ solver.observer_start_step = primary["start_step"]
143
+ solver.observer_end_step = primary["end_step"]
144
+ solver.observer_window_normalization = primary["window_normalization"]
145
+ solver.observer_sample_count = primary["sample_count"]
146
+ solver.observer_phase_step_cos = primary["phase_step_cos"]
147
+ solver.observer_phase_step_sin = primary["phase_step_sin"]
148
+
149
+
150
+ def sync_observer_primary_state(solver):
151
+ if not solver._observer_spectral_entries:
152
+ solver.observer_sample_count = 0
153
+ solver.observer_window_normalization = 0.0
154
+ solver.observer_phase_step_cos = None
155
+ solver.observer_phase_step_sin = None
156
+ return
157
+
158
+ primary = solver._observer_spectral_entries[0]
159
+ solver.observer_window_normalization = primary["window_normalization"]
160
+ solver.observer_sample_count = primary["sample_count"]
161
+ solver.observer_phase_step_cos = primary["phase_cos"]
162
+ solver.observer_phase_step_sin = primary["phase_sin"]
163
+
164
+
165
+ def source_time_kind(solver):
166
+ source_time = getattr(solver, "_source_time", None)
167
+ if source_time is None:
168
+ return ""
169
+ if isinstance(source_time, dict):
170
+ return str(source_time.get("kind", "")).lower()
171
+ return str(getattr(source_time, "kind", "")).lower()
172
+
173
+
174
+ def resolve_spectral_window_type(solver, window_type):
175
+ if source_time_kind(solver) and source_time_kind(solver) != "cw":
176
+ return "none"
177
+ return window_type
178
+
179
+
180
+ def compute_spectral_start_step(solver, frequency, *, window_type=None):
181
+ if source_time_kind(solver) and source_time_kind(solver) != "cw":
182
+ return 0
183
+ if window_type is None:
184
+ window_type = getattr(solver, "dft_window_type", "hanning")
185
+ if str(window_type).lower() == "none":
186
+ return 0
187
+
188
+ period = 1.0 / frequency
189
+ domain_range = getattr(solver.scene, "physical_domain_range", solver.scene.domain_range)
190
+ domain_size = max(
191
+ domain_range[1] - domain_range[0],
192
+ domain_range[3] - domain_range[2],
193
+ domain_range[5] - domain_range[4],
194
+ )
195
+ propagation_time = 2 * domain_size / solver.c
196
+ transient_time = max(15 * period, propagation_time * 5)
197
+ return int(transient_time / solver.dt)
198
+
199
+
200
+ def compute_window_weight(solver, n, start_step=None, end_step=None, window_type=None):
201
+ if start_step is None:
202
+ start_step = solver.dft_start_step
203
+ if end_step is None:
204
+ end_step = solver.dft_end_step
205
+ if window_type is None:
206
+ window_type = getattr(solver, "dft_window_type", "hanning")
207
+
208
+ if start_step is None or end_step is None:
209
+ return 1.0
210
+ if n < start_step or n >= end_step:
211
+ return 0.0
212
+
213
+ total_samples = end_step - start_step
214
+ pos = (n - start_step) / total_samples
215
+ if window_type == "none":
216
+ return 1.0
217
+ if window_type == "hanning":
218
+ return 0.5 * (1.0 - np.cos(2 * np.pi * pos))
219
+ if window_type == "ramp":
220
+ ramp_fraction = 0.1
221
+ if pos < ramp_fraction:
222
+ return 0.5 * (1.0 - np.cos(np.pi * pos / ramp_fraction))
223
+ return 1.0
224
+ return 1.0
225
+
226
+
227
+ def advance_phase(solver, cos_phase, sin_phase, step_cos, step_sin):
228
+ del solver
229
+ next_cos = cos_phase * step_cos - sin_phase * step_sin
230
+ next_sin = sin_phase * step_cos + cos_phase * step_sin
231
+ return next_cos, next_sin
232
+
233
+
234
+ def synchronize_device(solver):
235
+ if torch.cuda.is_available() and str(solver.device).startswith("cuda"):
236
+ torch.cuda.synchronize()
237
+
238
+
239
+ def enable_dft(solver, frequencies, window_type="hanning", end_step=None):
240
+ frequency_values = normalize_target_frequencies(solver, frequencies)
241
+ resolved_window_type = resolve_spectral_window_type(solver, window_type)
242
+ solver.dft_enabled = True
243
+ solver.dft_window_type = resolved_window_type
244
+ solver._dft_entries = []
245
+ complex_enabled = has_complex_fields(solver)
246
+ frequency_count = len(frequency_values)
247
+ solver._dft_batched_fields = {
248
+ "Ex": {
249
+ "real": torch.zeros((frequency_count,) + tuple(solver.Ex.shape), device=solver.device, dtype=solver.Ex.dtype),
250
+ "imag": torch.zeros((frequency_count,) + tuple(solver.Ex.shape), device=solver.device, dtype=solver.Ex.dtype),
251
+ "aux_real": (
252
+ torch.zeros((frequency_count,) + tuple(solver.Ex.shape), device=solver.device, dtype=solver.Ex.dtype)
253
+ if complex_enabled
254
+ else None
255
+ ),
256
+ "aux_imag": (
257
+ torch.zeros((frequency_count,) + tuple(solver.Ex.shape), device=solver.device, dtype=solver.Ex.dtype)
258
+ if complex_enabled
259
+ else None
260
+ ),
261
+ },
262
+ "Ey": {
263
+ "real": torch.zeros((frequency_count,) + tuple(solver.Ey.shape), device=solver.device, dtype=solver.Ey.dtype),
264
+ "imag": torch.zeros((frequency_count,) + tuple(solver.Ey.shape), device=solver.device, dtype=solver.Ey.dtype),
265
+ "aux_real": (
266
+ torch.zeros((frequency_count,) + tuple(solver.Ey.shape), device=solver.device, dtype=solver.Ey.dtype)
267
+ if complex_enabled
268
+ else None
269
+ ),
270
+ "aux_imag": (
271
+ torch.zeros((frequency_count,) + tuple(solver.Ey.shape), device=solver.device, dtype=solver.Ey.dtype)
272
+ if complex_enabled
273
+ else None
274
+ ),
275
+ },
276
+ "Ez": {
277
+ "real": torch.zeros((frequency_count,) + tuple(solver.Ez.shape), device=solver.device, dtype=solver.Ez.dtype),
278
+ "imag": torch.zeros((frequency_count,) + tuple(solver.Ez.shape), device=solver.device, dtype=solver.Ez.dtype),
279
+ "aux_real": (
280
+ torch.zeros((frequency_count,) + tuple(solver.Ez.shape), device=solver.device, dtype=solver.Ez.dtype)
281
+ if complex_enabled
282
+ else None
283
+ ),
284
+ "aux_imag": (
285
+ torch.zeros((frequency_count,) + tuple(solver.Ez.shape), device=solver.device, dtype=solver.Ez.dtype)
286
+ if complex_enabled
287
+ else None
288
+ ),
289
+ },
290
+ }
291
+ solver._dft_phase_cos = np.ones(frequency_count, dtype=np.float32)
292
+ solver._dft_phase_sin = np.zeros(frequency_count, dtype=np.float32)
293
+ solver._dft_phase_step_cos_values = np.empty(frequency_count, dtype=np.float32)
294
+ solver._dft_phase_step_sin_values = np.empty(frequency_count, dtype=np.float32)
295
+ solver._dft_start_steps = np.empty(frequency_count, dtype=np.int64)
296
+ solver._dft_end_steps = np.empty(frequency_count, dtype=np.int64)
297
+ solver._dft_window_normalization_values = np.zeros(frequency_count, dtype=np.float32)
298
+ solver._dft_sample_count_values = np.zeros(frequency_count, dtype=np.int64)
299
+ solver._dft_source_dft_real_values = np.zeros(frequency_count, dtype=np.float64)
300
+ solver._dft_source_dft_imag_values = np.zeros(frequency_count, dtype=np.float64)
301
+
302
+ for index, frequency in enumerate(frequency_values):
303
+ omega_dt = 2 * np.pi * frequency * solver.dt
304
+ solver._dft_phase_step_cos_values[index] = np.cos(omega_dt)
305
+ solver._dft_phase_step_sin_values[index] = np.sin(omega_dt)
306
+ solver._dft_start_steps[index] = compute_spectral_start_step(
307
+ solver,
308
+ frequency,
309
+ window_type=resolved_window_type,
310
+ )
311
+ solver._dft_end_steps[index] = -1 if end_step is None else int(end_step)
312
+ fields = {
313
+ "Ex": {
314
+ "real": solver._dft_batched_fields["Ex"]["real"][index],
315
+ "imag": solver._dft_batched_fields["Ex"]["imag"][index],
316
+ "aux_real": (
317
+ None
318
+ if solver._dft_batched_fields["Ex"]["aux_real"] is None
319
+ else solver._dft_batched_fields["Ex"]["aux_real"][index]
320
+ ),
321
+ "aux_imag": (
322
+ None
323
+ if solver._dft_batched_fields["Ex"]["aux_imag"] is None
324
+ else solver._dft_batched_fields["Ex"]["aux_imag"][index]
325
+ ),
326
+ },
327
+ "Ey": {
328
+ "real": solver._dft_batched_fields["Ey"]["real"][index],
329
+ "imag": solver._dft_batched_fields["Ey"]["imag"][index],
330
+ "aux_real": (
331
+ None
332
+ if solver._dft_batched_fields["Ey"]["aux_real"] is None
333
+ else solver._dft_batched_fields["Ey"]["aux_real"][index]
334
+ ),
335
+ "aux_imag": (
336
+ None
337
+ if solver._dft_batched_fields["Ey"]["aux_imag"] is None
338
+ else solver._dft_batched_fields["Ey"]["aux_imag"][index]
339
+ ),
340
+ },
341
+ "Ez": {
342
+ "real": solver._dft_batched_fields["Ez"]["real"][index],
343
+ "imag": solver._dft_batched_fields["Ez"]["imag"][index],
344
+ "aux_real": (
345
+ None
346
+ if solver._dft_batched_fields["Ez"]["aux_real"] is None
347
+ else solver._dft_batched_fields["Ez"]["aux_real"][index]
348
+ ),
349
+ "aux_imag": (
350
+ None
351
+ if solver._dft_batched_fields["Ez"]["aux_imag"] is None
352
+ else solver._dft_batched_fields["Ez"]["aux_imag"][index]
353
+ ),
354
+ },
355
+ }
356
+ solver._dft_entries.append(
357
+ {
358
+ "frequency": float(frequency),
359
+ "start_step": int(solver._dft_start_steps[index]),
360
+ "end_step": None if solver._dft_end_steps[index] < 0 else int(solver._dft_end_steps[index]),
361
+ "window_normalization": 0.0,
362
+ "sample_count": 0,
363
+ "phase_cos": 1.0,
364
+ "phase_sin": 0.0,
365
+ "phase_step_cos": float(solver._dft_phase_step_cos_values[index]),
366
+ "phase_step_sin": float(solver._dft_phase_step_sin_values[index]),
367
+ "fields": fields,
368
+ "source_dft_real": 0.0,
369
+ "source_dft_imag": 0.0,
370
+ }
371
+ )
372
+ sync_dft_legacy_state(solver)
373
+ if solver.verbose:
374
+ freq_text = ", ".join(f"{frequency / 1e9:.3f} GHz" for frequency in frequency_values)
375
+ print(f"DFT enabled, target frequencies: {freq_text}, window: {resolved_window_type}")
376
+
377
+
378
+ def accumulate_dft(solver, n, phase_cos=None, phase_sin=None):
379
+ del phase_cos, phase_sin
380
+ if not solver.dft_enabled or not solver._dft_entries:
381
+ return
382
+
383
+ source_signal = None
384
+ if getattr(solver, "_normalize_source", False) and solver._source_time is not None:
385
+ from ...sources import evaluate_source_time_normalization
386
+
387
+ source_signal = evaluate_source_time_normalization(solver._source_time, n * solver.dt)
388
+ active = n >= solver._dft_start_steps
389
+ active &= (solver._dft_end_steps < 0) | (n < solver._dft_end_steps)
390
+ if solver.dft_window_type == "none":
391
+ window_weight = active.astype(np.float32)
392
+ else:
393
+ total_samples = solver._dft_end_steps - solver._dft_start_steps
394
+ if solver.dft_window_type == "hanning":
395
+ pos = np.zeros_like(solver._dft_phase_cos, dtype=np.float32)
396
+ valid = active & (total_samples > 0)
397
+ pos[valid] = (n - solver._dft_start_steps[valid]) / total_samples[valid]
398
+ window_weight = np.zeros_like(solver._dft_phase_cos, dtype=np.float32)
399
+ window_weight[active & (solver._dft_end_steps < 0)] = 1.0
400
+ window_weight[valid] = 0.5 * (1.0 - np.cos(2 * np.pi * pos[valid]))
401
+ elif solver.dft_window_type == "ramp":
402
+ pos = np.zeros_like(solver._dft_phase_cos, dtype=np.float32)
403
+ valid = active & (total_samples > 0)
404
+ pos[valid] = (n - solver._dft_start_steps[valid]) / total_samples[valid]
405
+ window_weight = active.astype(np.float32)
406
+ ramp_fraction = 0.1
407
+ ramp_mask = valid & (pos < ramp_fraction)
408
+ window_weight[ramp_mask] = 0.5 * (1.0 - np.cos(np.pi * pos[ramp_mask] / ramp_fraction))
409
+ else:
410
+ window_weight = active.astype(np.float32)
411
+
412
+ weighted_cos = window_weight * solver._dft_phase_cos
413
+ weighted_sin = window_weight * solver._dft_phase_sin
414
+ weighted_cos_tensor = torch.as_tensor(weighted_cos, device=solver.device, dtype=torch.float32)
415
+ weighted_sin_tensor = torch.as_tensor(weighted_sin, device=solver.device, dtype=torch.float32)
416
+
417
+ if source_signal is not None:
418
+ solver._dft_source_dft_real_values += float(source_signal) * weighted_cos
419
+ solver._dft_source_dft_imag_values += float(source_signal) * weighted_sin
420
+
421
+ solver.fdtd_module.accumulateRunningDftYee3DBatched(
422
+ Ex=solver.Ex,
423
+ Ey=solver.Ey,
424
+ Ez=solver.Ez,
425
+ ExRealAccum=solver._dft_batched_fields["Ex"]["real"],
426
+ ExImagAccum=solver._dft_batched_fields["Ex"]["imag"],
427
+ EyRealAccum=solver._dft_batched_fields["Ey"]["real"],
428
+ EyImagAccum=solver._dft_batched_fields["Ey"]["imag"],
429
+ EzRealAccum=solver._dft_batched_fields["Ez"]["real"],
430
+ EzImagAccum=solver._dft_batched_fields["Ez"]["imag"],
431
+ weightedCos=weighted_cos_tensor,
432
+ weightedSin=weighted_sin_tensor,
433
+ ).launchRaw()
434
+
435
+ if has_complex_fields(solver):
436
+ solver.fdtd_module.accumulateRunningDftYee3DBatched(
437
+ Ex=solver.Ex_imag,
438
+ Ey=solver.Ey_imag,
439
+ Ez=solver.Ez_imag,
440
+ ExRealAccum=solver._dft_batched_fields["Ex"]["aux_real"],
441
+ ExImagAccum=solver._dft_batched_fields["Ex"]["aux_imag"],
442
+ EyRealAccum=solver._dft_batched_fields["Ey"]["aux_real"],
443
+ EyImagAccum=solver._dft_batched_fields["Ey"]["aux_imag"],
444
+ EzRealAccum=solver._dft_batched_fields["Ez"]["aux_real"],
445
+ EzImagAccum=solver._dft_batched_fields["Ez"]["aux_imag"],
446
+ weightedCos=weighted_cos_tensor,
447
+ weightedSin=weighted_sin_tensor,
448
+ ).launchRaw()
449
+
450
+ solver._dft_window_normalization_values += window_weight
451
+ solver._dft_sample_count_values += active.astype(np.int64)
452
+
453
+ next_cos = (
454
+ solver._dft_phase_cos * solver._dft_phase_step_cos_values
455
+ - solver._dft_phase_sin * solver._dft_phase_step_sin_values
456
+ )
457
+ next_sin = (
458
+ solver._dft_phase_sin * solver._dft_phase_step_cos_values
459
+ + solver._dft_phase_cos * solver._dft_phase_step_sin_values
460
+ )
461
+ solver._dft_phase_cos = next_cos.astype(np.float32, copy=False)
462
+ solver._dft_phase_sin = next_sin.astype(np.float32, copy=False)
463
+ sync_dft_primary_runtime_state(solver)
464
+
465
+
466
+ def _dft_window_weight(n, window_type, active, start, end):
467
+ """The per-step spectral window weight vector, matching accumulate_dft."""
468
+ if window_type == "none":
469
+ return active.astype(np.float32)
470
+ total = end - start
471
+ valid = active & (total > 0)
472
+ pos = np.zeros_like(start, dtype=np.float32)
473
+ pos[valid] = (n - start[valid]) / total[valid]
474
+ if window_type == "hanning":
475
+ weight = np.zeros_like(start, dtype=np.float32)
476
+ weight[active & (end < 0)] = 1.0
477
+ weight[valid] = 0.5 * (1.0 - np.cos(2 * np.pi * pos[valid]))
478
+ return weight
479
+ if window_type == "ramp":
480
+ weight = active.astype(np.float32)
481
+ ramp_fraction = 0.1
482
+ ramp_mask = valid & (pos < ramp_fraction)
483
+ weight[ramp_mask] = 0.5 * (1.0 - np.cos(np.pi * pos[ramp_mask] / ramp_fraction))
484
+ return weight
485
+ return active.astype(np.float32)
486
+
487
+
488
+ def build_dft_step_tables(solver, time_steps):
489
+ """Precompute the full per-step running-DFT weight table on the GPU.
490
+
491
+ Each row ``table[n]`` is the ``window_weight(n) * phase(n)`` vector that
492
+ accumulate_dft computes at step ``n``; it is built with the identical float32
493
+ phase recurrence and window, so a GPU-driven accumulation that gathers rows
494
+ by a device step counter is bit-identical to the per-step host path -- but
495
+ with no per-step host arithmetic or host->device transfer (which makes the
496
+ DFT accumulation capturable and cheaper). Also fixes the final window
497
+ normalization / sample count / source-DFT to their full-run sums (the values
498
+ the host path would reach, and what an early shutoff restores anyway).
499
+
500
+ Returns False (caller keeps the host path) for the complex-field DFT, which
501
+ is out of the Option-A/Stage-1 graph scope.
502
+ """
503
+ if not solver.dft_enabled or not solver._dft_entries or has_complex_fields(solver):
504
+ return False
505
+ frequency_count = len(solver._dft_entries)
506
+ steps = int(time_steps)
507
+ weighted_cos = np.zeros((steps, frequency_count), dtype=np.float32)
508
+ weighted_sin = np.zeros((steps, frequency_count), dtype=np.float32)
509
+ window_norm = np.zeros(frequency_count, dtype=np.float32)
510
+ sample_count = np.zeros(frequency_count, dtype=np.int64)
511
+ src_real = np.zeros(frequency_count, dtype=np.float64)
512
+ src_imag = np.zeros(frequency_count, dtype=np.float64)
513
+ normalize = bool(getattr(solver, "_normalize_source", False)) and solver._source_time is not None
514
+
515
+ phase_cos = np.ones(frequency_count, dtype=np.float32)
516
+ phase_sin = np.zeros(frequency_count, dtype=np.float32)
517
+ step_cos = solver._dft_phase_step_cos_values
518
+ step_sin = solver._dft_phase_step_sin_values
519
+ start = solver._dft_start_steps
520
+ end = solver._dft_end_steps
521
+ window_type = solver.dft_window_type
522
+ for n in range(steps):
523
+ active = (n >= start) & ((end < 0) | (n < end))
524
+ weight = _dft_window_weight(n, window_type, active, start, end)
525
+ wc = weight * phase_cos
526
+ ws = weight * phase_sin
527
+ weighted_cos[n] = wc
528
+ weighted_sin[n] = ws
529
+ window_norm += weight
530
+ sample_count += active.astype(np.int64)
531
+ if normalize:
532
+ from ...sources import evaluate_source_time_normalization
533
+
534
+ signal = float(evaluate_source_time_normalization(solver._source_time, n * solver.dt))
535
+ src_real += signal * wc
536
+ src_imag += signal * ws
537
+ next_cos = phase_cos * step_cos - phase_sin * step_sin
538
+ next_sin = phase_sin * step_cos + phase_cos * step_sin
539
+ phase_cos = next_cos.astype(np.float32, copy=False)
540
+ phase_sin = next_sin.astype(np.float32, copy=False)
541
+
542
+ solver._dft_weighted_cos_table = torch.as_tensor(weighted_cos, device=solver.device)
543
+ solver._dft_weighted_sin_table = torch.as_tensor(weighted_sin, device=solver.device)
544
+ solver._dft_weighted_cos = torch.zeros(frequency_count, device=solver.device, dtype=torch.float32)
545
+ solver._dft_weighted_sin = torch.zeros(frequency_count, device=solver.device, dtype=torch.float32)
546
+ solver._dft_step = torch.zeros(1, device=solver.device, dtype=torch.int64)
547
+ solver._dft_window_normalization_values = window_norm
548
+ solver._dft_sample_count_values = sample_count
549
+ if normalize:
550
+ solver._dft_source_dft_real_values = src_real
551
+ solver._dft_source_dft_imag_values = src_imag
552
+ sync_dft_primary_runtime_state(solver)
553
+ return True
554
+
555
+
556
+ def accumulate_dft_gpu(solver):
557
+ """GPU-driven running-DFT accumulation: gather the current step's weight row
558
+ from the precomputed table by the device step counter and accumulate. No host
559
+ arithmetic, no host->device transfer, and capturable into a CUDA graph."""
560
+ if not solver.dft_enabled or not solver._dft_entries:
561
+ return
562
+ solver._dft_weighted_cos.copy_(
563
+ solver._dft_weighted_cos_table.index_select(0, solver._dft_step).squeeze(0)
564
+ )
565
+ solver._dft_weighted_sin.copy_(
566
+ solver._dft_weighted_sin_table.index_select(0, solver._dft_step).squeeze(0)
567
+ )
568
+ solver.fdtd_module.accumulateRunningDftYee3DBatched(
569
+ Ex=solver.Ex,
570
+ Ey=solver.Ey,
571
+ Ez=solver.Ez,
572
+ ExRealAccum=solver._dft_batched_fields["Ex"]["real"],
573
+ ExImagAccum=solver._dft_batched_fields["Ex"]["imag"],
574
+ EyRealAccum=solver._dft_batched_fields["Ey"]["real"],
575
+ EyImagAccum=solver._dft_batched_fields["Ey"]["imag"],
576
+ EzRealAccum=solver._dft_batched_fields["Ez"]["real"],
577
+ EzImagAccum=solver._dft_batched_fields["Ez"]["imag"],
578
+ weightedCos=solver._dft_weighted_cos,
579
+ weightedSin=solver._dft_weighted_sin,
580
+ ).launchRaw()
581
+ solver._dft_step.add_(1)