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