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,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
from .tfsf_specs import reference_sample_axis_code
|
|
6
|
+
from .temporal import apply_generic_source_terms
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_REFERENCE_PROVIDERS = {"plane_wave_ref_x_ez", "plane_wave_axis_aligned"}
|
|
10
|
+
_AUXILIARY_PROVIDERS = {"plane_wave_ref_x_ez", "plane_wave_axis_aligned", "plane_wave_aux"}
|
|
11
|
+
|
|
12
|
+
_MAGNETIC_FIELD_NAMES = ("Hx", "Hy", "Hz")
|
|
13
|
+
_ELECTRIC_FIELD_NAMES = ("Ex", "Ey", "Ez")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _field_names_for_terms(term_key):
|
|
17
|
+
return _MAGNETIC_FIELD_NAMES if term_key == "magnetic_terms" else _ELECTRIC_FIELD_NAMES
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _apply_batched_aux_terms(solver, batch, *, sample_kind, field_names):
|
|
21
|
+
if batch is None:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
aux = solver._tfsf_state["auxiliary_grid"]
|
|
25
|
+
sample_origin = float(aux.s_min if sample_kind == "electric" else aux.s_min + 0.5 * aux.ds)
|
|
26
|
+
sample_field = aux.electric if sample_kind == "electric" else aux.magnetic
|
|
27
|
+
solver.fdtd_module.addBatchedInterpolatedSourcePatches3D(
|
|
28
|
+
fieldX=getattr(solver, field_names[0]),
|
|
29
|
+
fieldY=getattr(solver, field_names[1]),
|
|
30
|
+
fieldZ=getattr(solver, field_names[2]),
|
|
31
|
+
coeffData=batch["coeff_data"],
|
|
32
|
+
incidentField=sample_field,
|
|
33
|
+
samplePositions=batch["sample_positions"],
|
|
34
|
+
termStarts=batch["term_starts"],
|
|
35
|
+
termShapes=batch["term_shapes"],
|
|
36
|
+
termOffsets=batch["term_offsets"],
|
|
37
|
+
fieldCodes=batch["field_codes"],
|
|
38
|
+
fieldCodesPerCoeff=batch["field_codes_per_coeff"],
|
|
39
|
+
fieldOffsets=batch["field_offsets"],
|
|
40
|
+
origin=sample_origin,
|
|
41
|
+
ds=float(aux.ds),
|
|
42
|
+
).launchRaw()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _apply_batched_reference_terms(solver, batch, *, sample_kind, field_names):
|
|
46
|
+
if batch is None:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
aux = solver._tfsf_state["auxiliary_grid"]
|
|
50
|
+
sample_field = aux.electric if sample_kind == "electric" else aux.magnetic
|
|
51
|
+
solver.fdtd_module.addBatchedReferenceSourcePatches3D(
|
|
52
|
+
fieldX=getattr(solver, field_names[0]),
|
|
53
|
+
fieldY=getattr(solver, field_names[1]),
|
|
54
|
+
fieldZ=getattr(solver, field_names[2]),
|
|
55
|
+
coeffData=batch["coeff_data"],
|
|
56
|
+
incidentField=sample_field,
|
|
57
|
+
termStarts=batch["term_starts"],
|
|
58
|
+
termShapes=batch["term_shapes"],
|
|
59
|
+
termOffsets=batch["term_offsets"],
|
|
60
|
+
fieldCodes=batch["field_codes"],
|
|
61
|
+
sampleAxisCodes=batch["sample_axis_codes"],
|
|
62
|
+
sampleIndexStarts=batch["sample_index_starts"],
|
|
63
|
+
sampleIndices=batch["sample_indices"],
|
|
64
|
+
fieldCodesPerCoeff=batch["field_codes_per_coeff"],
|
|
65
|
+
fieldOffsets=batch["field_offsets"],
|
|
66
|
+
sampleIndicesPerCoeff=batch["sample_indices_per_coeff"],
|
|
67
|
+
).launchRaw()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _apply_aux_terms(solver, terms, *, sample_kind):
|
|
71
|
+
if not terms:
|
|
72
|
+
return
|
|
73
|
+
aux = solver._tfsf_state["auxiliary_grid"]
|
|
74
|
+
sample_origin = float(aux.s_min if sample_kind == "electric" else aux.s_min + 0.5 * aux.ds)
|
|
75
|
+
sample_field = aux.electric if sample_kind == "electric" else aux.magnetic
|
|
76
|
+
for term in terms:
|
|
77
|
+
offset_i, offset_j, offset_k = term["offsets"]
|
|
78
|
+
solver.fdtd_module.addInterpolatedSourcePatch3D(
|
|
79
|
+
field=getattr(solver, term["field_name"]),
|
|
80
|
+
coeffPatch=term["coeff_patch"],
|
|
81
|
+
incidentField=sample_field,
|
|
82
|
+
samplePositions=term["sample_positions"],
|
|
83
|
+
origin=sample_origin,
|
|
84
|
+
ds=float(aux.ds),
|
|
85
|
+
offsetI=int(offset_i),
|
|
86
|
+
offsetJ=int(offset_j),
|
|
87
|
+
offsetK=int(offset_k),
|
|
88
|
+
scale=float(term["component_scale"]),
|
|
89
|
+
).launchRaw()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _apply_reference_terms(solver, terms):
|
|
93
|
+
if not terms:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
aux = solver._tfsf_state["auxiliary_grid"]
|
|
97
|
+
for term in terms:
|
|
98
|
+
sample_field = aux.electric if term["sample_kind"] == "electric" else aux.magnetic
|
|
99
|
+
if term["scalar_sample_index"] is not None:
|
|
100
|
+
offset_i, offset_j, offset_k = term["offsets"]
|
|
101
|
+
solver.fdtd_module.addScaledSliceSourcePatch3D(
|
|
102
|
+
field=getattr(solver, term["field_name"]),
|
|
103
|
+
sourcePatch=term["coeff_patch"],
|
|
104
|
+
incidentField=sample_field,
|
|
105
|
+
sampleIndex=int(term["scalar_sample_index"]),
|
|
106
|
+
offsetI=int(offset_i),
|
|
107
|
+
offsetJ=int(offset_j),
|
|
108
|
+
offsetK=int(offset_k),
|
|
109
|
+
scale=float(term["component_scale"]),
|
|
110
|
+
).launchRaw()
|
|
111
|
+
continue
|
|
112
|
+
sample_indices_i32 = term.get("_sample_indices_i32")
|
|
113
|
+
if (
|
|
114
|
+
sample_indices_i32 is None
|
|
115
|
+
or not torch.is_tensor(sample_indices_i32)
|
|
116
|
+
or sample_indices_i32.device != sample_field.device
|
|
117
|
+
):
|
|
118
|
+
sample_indices_i32 = term["sample_indices"].to(device=sample_field.device, dtype=torch.int32)
|
|
119
|
+
term["_sample_indices_i32"] = sample_indices_i32
|
|
120
|
+
sample_axis_code = int(term.get("_sample_axis_code", reference_sample_axis_code(term)))
|
|
121
|
+
term["_sample_axis_code"] = sample_axis_code
|
|
122
|
+
offset_i, offset_j, offset_k = term["offsets"]
|
|
123
|
+
solver.fdtd_module.addScaledLineSourcePatch3D(
|
|
124
|
+
field=getattr(solver, term["field_name"]),
|
|
125
|
+
coeffPatch=term["coeff_patch"],
|
|
126
|
+
incidentField=sample_field,
|
|
127
|
+
sampleIndices=sample_indices_i32,
|
|
128
|
+
sampleAxisCode=sample_axis_code,
|
|
129
|
+
offsetI=int(offset_i),
|
|
130
|
+
offsetJ=int(offset_j),
|
|
131
|
+
offsetK=int(offset_k),
|
|
132
|
+
scale=float(term["component_scale"]),
|
|
133
|
+
).launchRaw()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _apply_tfsf_terms(solver, *, term_key, sample_kind, time_value):
|
|
137
|
+
if not getattr(solver, "tfsf_enabled", False):
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
provider = solver._tfsf_state.get("provider")
|
|
141
|
+
terms = solver._tfsf_state[term_key]
|
|
142
|
+
batch = solver._tfsf_state.get("magnetic_batch" if term_key == "magnetic_terms" else "electric_batch")
|
|
143
|
+
field_names = _field_names_for_terms(term_key)
|
|
144
|
+
if provider in _REFERENCE_PROVIDERS:
|
|
145
|
+
if batch is not None:
|
|
146
|
+
_apply_batched_reference_terms(
|
|
147
|
+
solver,
|
|
148
|
+
batch,
|
|
149
|
+
sample_kind=sample_kind,
|
|
150
|
+
field_names=field_names,
|
|
151
|
+
)
|
|
152
|
+
return
|
|
153
|
+
_apply_reference_terms(solver, terms)
|
|
154
|
+
return
|
|
155
|
+
if provider == "plane_wave_aux":
|
|
156
|
+
if batch is not None:
|
|
157
|
+
_apply_batched_aux_terms(
|
|
158
|
+
solver,
|
|
159
|
+
batch,
|
|
160
|
+
sample_kind=sample_kind,
|
|
161
|
+
field_names=field_names,
|
|
162
|
+
)
|
|
163
|
+
return
|
|
164
|
+
_apply_aux_terms(solver, terms, sample_kind=sample_kind)
|
|
165
|
+
return
|
|
166
|
+
apply_generic_source_terms(
|
|
167
|
+
solver,
|
|
168
|
+
terms,
|
|
169
|
+
source_time=solver._source_time,
|
|
170
|
+
omega=solver.source_omega,
|
|
171
|
+
time_value=time_value,
|
|
172
|
+
clamp_pec=False,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# TFSF providers whose in-block correction both (1) reads the incident field from
|
|
177
|
+
# the device-resident auxiliary 1D grid -- carrying no per-step host input -- and
|
|
178
|
+
# (2) accumulates through the integer-indexed *reference* injection kernel, whose
|
|
179
|
+
# warp-aggregated writes are collision-free for the axis-aligned face layout and
|
|
180
|
+
# therefore bit-reproducible. This covers both injection modes of the axis-aligned
|
|
181
|
+
# reference providers: the six-face box TFSF and the two-face (slab) TFSF share the
|
|
182
|
+
# same auxiliary-line provider and reference kernels, so the non-periodic
|
|
183
|
+
# normal-incidence slab captures identically to the box. The oblique auxiliary-line
|
|
184
|
+
# provider (``plane_wave_aux``) is deliberately excluded: it accumulates through the
|
|
185
|
+
# batched *interpolated* kernel, whose cross-warp atomic adds at the overlapping
|
|
186
|
+
# oblique faces are not bit-reproducible run to run, so a captured graph could not
|
|
187
|
+
# be proven bit-identical to the eager path. The grating-slab (``plane_wave_grating_
|
|
188
|
+
# slab_cw``) and box-CW (``plane_wave_discrete_cw``) providers evaluate the incident
|
|
189
|
+
# CW phasor on the host each step through ``apply_generic_source_terms`` and inject
|
|
190
|
+
# it as a kernel launch scalar a captured graph would freeze, so they are excluded
|
|
191
|
+
# too.
|
|
192
|
+
_CAPTURABLE_TFSF_PROVIDERS = frozenset(_REFERENCE_PROVIDERS)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def tfsf_incident_is_gpu_driven(solver) -> bool:
|
|
196
|
+
"""True when an active TFSF source couples into the 3D field-update block only
|
|
197
|
+
by *reading* the device-resident auxiliary 1D incident grid through the
|
|
198
|
+
bit-reproducible reference injection kernel, so the block can be captured into a
|
|
199
|
+
CUDA graph and replayed bit-for-bit.
|
|
200
|
+
|
|
201
|
+
The reference providers apply their in-block H correction with the batched /
|
|
202
|
+
per-term reference kernels, which read ``aux.electric`` / ``aux.magnetic``
|
|
203
|
+
(persistent device tensors) at fixed integer sample indices and carry no
|
|
204
|
+
per-step host input. The 1D source waveform is evaluated on the host only in
|
|
205
|
+
``advance_tfsf_auxiliary_electric`` (the E-side auxiliary advance), which runs
|
|
206
|
+
eagerly *after* the field-update block, outside the captured region. So for
|
|
207
|
+
these providers the block is a fixed, deterministic kernel sequence. This holds
|
|
208
|
+
for both the six-face box TFSF and the two-face (slab) TFSF, which share the
|
|
209
|
+
axis-aligned reference provider, so the non-periodic normal-incidence slab is
|
|
210
|
+
capturable exactly like the box.
|
|
211
|
+
|
|
212
|
+
The oblique auxiliary-line provider (``plane_wave_aux``) is excluded because its
|
|
213
|
+
batched interpolated injection kernel accumulates through non-deterministic
|
|
214
|
+
cross-warp atomic adds (bit-identical capture parity is impossible). The
|
|
215
|
+
grating-slab provider (``plane_wave_grating_slab_cw``, the Bloch-transverse
|
|
216
|
+
oblique slab) and the box-CW provider (``plane_wave_discrete_cw``) each evaluate
|
|
217
|
+
the incident CW phasor on the host every step via ``apply_generic_source_terms``
|
|
218
|
+
and inject it as a kernel launch scalar a captured graph would freeze at its
|
|
219
|
+
capture-time value, so they too stay on the eager path (capturing them would
|
|
220
|
+
require a GPU-driven generic source table, which is out of scope here).
|
|
221
|
+
"""
|
|
222
|
+
state = getattr(solver, "_tfsf_state", None)
|
|
223
|
+
if not state:
|
|
224
|
+
return False
|
|
225
|
+
return state.get("provider") in _CAPTURABLE_TFSF_PROVIDERS
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def apply_tfsf_h_correction(solver, time_value):
|
|
229
|
+
_apply_tfsf_terms(
|
|
230
|
+
solver,
|
|
231
|
+
term_key="magnetic_terms",
|
|
232
|
+
sample_kind="electric",
|
|
233
|
+
time_value=time_value,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def apply_tfsf_e_correction(solver, time_value):
|
|
238
|
+
_apply_tfsf_terms(
|
|
239
|
+
solver,
|
|
240
|
+
term_key="electric_terms",
|
|
241
|
+
sample_kind="magnetic",
|
|
242
|
+
time_value=time_value + 0.5 * float(solver.dt),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _advance_auxiliary_grid(solver, *, magnetic):
|
|
247
|
+
if not getattr(solver, "tfsf_enabled", False):
|
|
248
|
+
return None
|
|
249
|
+
if solver._tfsf_state.get("provider") not in _AUXILIARY_PROVIDERS:
|
|
250
|
+
return None
|
|
251
|
+
aux = solver._tfsf_state["auxiliary_grid"]
|
|
252
|
+
if magnetic:
|
|
253
|
+
aux.advance_magnetic()
|
|
254
|
+
else:
|
|
255
|
+
aux.advance_electric()
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def advance_tfsf_auxiliary_magnetic(solver):
|
|
260
|
+
return _advance_auxiliary_grid(solver, magnetic=True)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def advance_tfsf_auxiliary_electric(solver):
|
|
264
|
+
return _advance_auxiliary_grid(solver, magnetic=False)
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
from ..dispersion import solve_numerical_wavenumber
|
|
8
|
+
from .spatial import beam_profile_from_source, plane_wave_profile
|
|
9
|
+
from .tfsf_specs import AXIS_INDEX, reference_sample_axis_code
|
|
10
|
+
from .temporal import build_source_term
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def nearest_index(axis_coords, value: float) -> int:
|
|
14
|
+
return int(torch.argmin(torch.abs(axis_coords - float(value))).item())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_bounds_indices(scene, bounds):
|
|
18
|
+
lower = []
|
|
19
|
+
upper = []
|
|
20
|
+
for axis_coords, axis_bounds in zip((scene.x, scene.y, scene.z), bounds):
|
|
21
|
+
lower.append(nearest_index(axis_coords, axis_bounds[0]))
|
|
22
|
+
upper.append(nearest_index(axis_coords, axis_bounds[1]))
|
|
23
|
+
return tuple(lower), tuple(upper)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Relative spacing spread at or below which the injection region is treated as
|
|
27
|
+
# exactly uniform: the legacy scalar spacing is returned bit-for-bit and no new
|
|
28
|
+
# numerics run, so uniform grids stay unchanged.
|
|
29
|
+
_UNIFORM_SPACING_RTOL = 1e-6
|
|
30
|
+
# Maximum tolerated numerical phase-velocity spread across a TFSF injection
|
|
31
|
+
# region before the single auxiliary-grid spacing can no longer match the graded
|
|
32
|
+
# main grid. The leading FDTD dispersion relation gives a relative
|
|
33
|
+
# numerical-wavenumber excess of (k0*d)^2/24 for a vacuum wave of wavenumber
|
|
34
|
+
# k0 = omega/c (the TFSF background is enforced to be vacuum); the spread of that
|
|
35
|
+
# excess between the finest and coarsest cell in the region is the residual the
|
|
36
|
+
# total/scattered-field faces cannot cancel and thus leaks into the scattered
|
|
37
|
+
# region. 1e-3 keeps that residual at or below the uniform-grid leakage floor the
|
|
38
|
+
# axis-aligned null tests already assert.
|
|
39
|
+
_TFSF_DISPERSION_SPREAD_BOUND = 1e-3
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def require_locally_uniform_axis(solver, axis, start, stop, *, context):
|
|
43
|
+
"""Return the effective primal spacing over cells [start, stop) for the
|
|
44
|
+
auxiliary-grid dispersion match, accepting a bounded amount of grading.
|
|
45
|
+
|
|
46
|
+
A perfectly uniform region (spread <= ``_UNIFORM_SPACING_RTOL`` relative)
|
|
47
|
+
returns its exact cell spacing, bit-for-bit as before. A mildly graded region
|
|
48
|
+
is accepted when the numerical phase-velocity spread it induces across the
|
|
49
|
+
region stays below ``_TFSF_DISPERSION_SPREAD_BOUND``, in which case the
|
|
50
|
+
region-mean spacing is used as the single effective delta. A region too
|
|
51
|
+
graded for that bound raises with the predicted error, because a single
|
|
52
|
+
auxiliary spacing would inject a spurious incident field on the TFSF faces.
|
|
53
|
+
"""
|
|
54
|
+
primal = {
|
|
55
|
+
"x": solver.scene.dx_primal64,
|
|
56
|
+
"y": solver.scene.dy_primal64,
|
|
57
|
+
"z": solver.scene.dz_primal64,
|
|
58
|
+
}[axis]
|
|
59
|
+
window = primal[max(int(start), 0) : min(int(stop), len(primal))]
|
|
60
|
+
d_min = float(window.min())
|
|
61
|
+
d_max = float(window.max())
|
|
62
|
+
if (d_max - d_min) <= _UNIFORM_SPACING_RTOL * d_max:
|
|
63
|
+
return d_min
|
|
64
|
+
|
|
65
|
+
# Region-mean effective spacing minimizes the mean phase mismatch; the
|
|
66
|
+
# residual is the numerical-dispersion wavenumber spread it leaves across the
|
|
67
|
+
# region, evaluated at the source (center) frequency.
|
|
68
|
+
d_eff = float(window.mean())
|
|
69
|
+
k0 = abs(float(solver.source_omega)) / float(solver.c)
|
|
70
|
+
dispersion_spread = (k0 * k0) * (d_max * d_max - d_min * d_min) / 24.0
|
|
71
|
+
if dispersion_spread > _TFSF_DISPERSION_SPREAD_BOUND:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"{context} along axis '{axis}' is too graded for the auxiliary-grid "
|
|
74
|
+
"dispersion match: the numerical phase-velocity spread across the "
|
|
75
|
+
f"injection region (+1 cell) is {dispersion_spread:.3e} "
|
|
76
|
+
f"(min={d_min:g}, max={d_max:g}, k0={k0:g}), above the bound "
|
|
77
|
+
f"{_TFSF_DISPERSION_SPREAD_BOUND:.0e}. Refine GridSpec.custom for "
|
|
78
|
+
f"locally uniform grid spacing along axis '{axis}' over the injection "
|
|
79
|
+
"region, or move the region."
|
|
80
|
+
)
|
|
81
|
+
return d_eff
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def validate_bounds(solver, lower, upper):
|
|
85
|
+
shape = (solver.Nx, solver.Ny, solver.Nz)
|
|
86
|
+
for axis, lo, hi, size in zip("xyz", lower, upper, shape):
|
|
87
|
+
low_margin = solver.scene.pml_thickness_for_face(axis, "low") + 2
|
|
88
|
+
high_margin = solver.scene.pml_thickness_for_face(axis, "high") + 2
|
|
89
|
+
if hi <= lo + 1:
|
|
90
|
+
raise ValueError(f"TFSF bounds must span at least two cells along {axis}.")
|
|
91
|
+
if lo < low_margin or hi > size - high_margin - 1:
|
|
92
|
+
raise ValueError("TFSF bounds must lie strictly inside the non-PML simulation region.")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def validate_background_is_vacuum(solver, lower, upper, tol=1e-5):
|
|
96
|
+
eps = solver.epsilon_r
|
|
97
|
+
mu = solver.mu_r
|
|
98
|
+
slices = (
|
|
99
|
+
(slice(lower[0], lower[0] + 1), slice(lower[1], upper[1] + 1), slice(lower[2], upper[2] + 1)),
|
|
100
|
+
(slice(upper[0], upper[0] + 1), slice(lower[1], upper[1] + 1), slice(lower[2], upper[2] + 1)),
|
|
101
|
+
(slice(lower[0], upper[0] + 1), slice(lower[1], lower[1] + 1), slice(lower[2], upper[2] + 1)),
|
|
102
|
+
(slice(lower[0], upper[0] + 1), slice(upper[1], upper[1] + 1), slice(lower[2], upper[2] + 1)),
|
|
103
|
+
(slice(lower[0], upper[0] + 1), slice(lower[1], upper[1] + 1), slice(lower[2], lower[2] + 1)),
|
|
104
|
+
(slice(lower[0], upper[0] + 1), slice(lower[1], upper[1] + 1), slice(upper[2], upper[2] + 1)),
|
|
105
|
+
)
|
|
106
|
+
for face_slice in slices:
|
|
107
|
+
eps_face = eps[face_slice]
|
|
108
|
+
mu_face = mu[face_slice]
|
|
109
|
+
if not torch.allclose(eps_face, torch.ones_like(eps_face), atol=tol, rtol=0.0):
|
|
110
|
+
raise ValueError("TFSF boundary must remain in vacuum on all faces.")
|
|
111
|
+
if not torch.allclose(mu_face, torch.ones_like(mu_face), atol=tol, rtol=0.0):
|
|
112
|
+
raise ValueError("TFSF boundary must remain in vacuum on all faces.")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def solve_auxiliary_step(solver, k_numeric: float) -> float:
|
|
116
|
+
omega = float(solver.source_omega)
|
|
117
|
+
if omega <= 0.0 or k_numeric <= 1e-12:
|
|
118
|
+
return float(solver.c) * float(solver.dt)
|
|
119
|
+
|
|
120
|
+
rhs = math.sin(0.5 * omega * float(solver.dt)) / (float(solver.c) * float(solver.dt))
|
|
121
|
+
low = float(solver.c) * float(solver.dt) * (1.0 + 1e-6)
|
|
122
|
+
high = 0.999 * math.pi / k_numeric
|
|
123
|
+
if high <= low:
|
|
124
|
+
high = low * 1.5
|
|
125
|
+
|
|
126
|
+
def residual(ds):
|
|
127
|
+
return math.sin(0.5 * k_numeric * ds) / ds - rhs
|
|
128
|
+
|
|
129
|
+
low_residual = residual(low)
|
|
130
|
+
high_residual = residual(high)
|
|
131
|
+
if low_residual < 0.0:
|
|
132
|
+
return low
|
|
133
|
+
if high_residual > 0.0:
|
|
134
|
+
return high
|
|
135
|
+
|
|
136
|
+
for _ in range(80):
|
|
137
|
+
mid = 0.5 * (low + high)
|
|
138
|
+
if residual(mid) >= 0.0:
|
|
139
|
+
low = mid
|
|
140
|
+
else:
|
|
141
|
+
high = mid
|
|
142
|
+
return 0.5 * (low + high)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def entry_projection(bounds, direction):
|
|
146
|
+
x_bounds, y_bounds, z_bounds = bounds
|
|
147
|
+
min_projection = None
|
|
148
|
+
for x in x_bounds:
|
|
149
|
+
for y in y_bounds:
|
|
150
|
+
for z in z_bounds:
|
|
151
|
+
projection = x * float(direction[0]) + y * float(direction[1]) + z * float(direction[2])
|
|
152
|
+
if min_projection is None or projection < min_projection:
|
|
153
|
+
min_projection = projection
|
|
154
|
+
return float(min_projection)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def projection_extrema(bounds, direction):
|
|
158
|
+
x_bounds, y_bounds, z_bounds = bounds
|
|
159
|
+
min_projection = None
|
|
160
|
+
max_projection = None
|
|
161
|
+
for x in x_bounds:
|
|
162
|
+
for y in y_bounds:
|
|
163
|
+
for z in z_bounds:
|
|
164
|
+
projection = x * float(direction[0]) + y * float(direction[1]) + z * float(direction[2])
|
|
165
|
+
if min_projection is None or projection < min_projection:
|
|
166
|
+
min_projection = projection
|
|
167
|
+
if max_projection is None or projection > max_projection:
|
|
168
|
+
max_projection = projection
|
|
169
|
+
return float(min_projection), float(max_projection)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def box_center_tensor(solver, bounds):
|
|
173
|
+
return torch.tensor(
|
|
174
|
+
[
|
|
175
|
+
0.5 * (bounds[0][0] + bounds[0][1]),
|
|
176
|
+
0.5 * (bounds[1][0] + bounds[1][1]),
|
|
177
|
+
0.5 * (bounds[2][0] + bounds[2][1]),
|
|
178
|
+
],
|
|
179
|
+
device=solver.device,
|
|
180
|
+
dtype=solver.Ex.dtype,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def make_aux_term(solver, *, field_name, offsets, coeff_patch, sample_positions, component_scale):
|
|
185
|
+
return {
|
|
186
|
+
"field_name": field_name,
|
|
187
|
+
"offsets": offsets,
|
|
188
|
+
"coeff_patch": coeff_patch.contiguous(),
|
|
189
|
+
"sample_positions": sample_positions.contiguous(),
|
|
190
|
+
"component_scale": float(component_scale),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _field_component_code(field_name: str) -> int:
|
|
195
|
+
component = field_name[-1].lower()
|
|
196
|
+
if component == "x":
|
|
197
|
+
return 0
|
|
198
|
+
if component == "y":
|
|
199
|
+
return 1
|
|
200
|
+
return 2
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _pack_batched_term_metadata(solver, terms):
|
|
204
|
+
coeff_cursor = 0
|
|
205
|
+
term_starts = []
|
|
206
|
+
term_shapes = []
|
|
207
|
+
term_offsets = []
|
|
208
|
+
field_codes = []
|
|
209
|
+
coeff_chunks = []
|
|
210
|
+
field_code_chunks = []
|
|
211
|
+
field_offset_chunks = []
|
|
212
|
+
field_offset_dtype = torch.int32
|
|
213
|
+
for term in terms:
|
|
214
|
+
field_shape = tuple(int(length) for length in getattr(solver, term["field_name"]).shape)
|
|
215
|
+
if math.prod(field_shape) > torch.iinfo(torch.int32).max:
|
|
216
|
+
field_offset_dtype = torch.int64
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
for term_index, term in enumerate(terms):
|
|
220
|
+
coeff_flat = (term["coeff_patch"] * float(term["component_scale"])).reshape(-1)
|
|
221
|
+
shape = tuple(int(length) for length in term["coeff_patch"].shape)
|
|
222
|
+
offsets = tuple(int(offset) for offset in term["offsets"])
|
|
223
|
+
local_linear = torch.arange(int(coeff_flat.numel()), device=solver.device, dtype=torch.int32)
|
|
224
|
+
stride_i = shape[1] * shape[2]
|
|
225
|
+
local_i = local_linear // stride_i
|
|
226
|
+
remainder = local_linear - local_i * stride_i
|
|
227
|
+
local_j = remainder // shape[2]
|
|
228
|
+
local_k = remainder - local_j * shape[2]
|
|
229
|
+
field_code = _field_component_code(term["field_name"])
|
|
230
|
+
field_shape = tuple(int(length) for length in getattr(solver, term["field_name"]).shape)
|
|
231
|
+
field_i = local_i + offsets[0]
|
|
232
|
+
field_j = local_j + offsets[1]
|
|
233
|
+
field_k = local_k + offsets[2]
|
|
234
|
+
coeff_chunks.append(coeff_flat)
|
|
235
|
+
field_code_chunks.append(
|
|
236
|
+
torch.full(
|
|
237
|
+
(int(coeff_flat.numel()),),
|
|
238
|
+
field_code,
|
|
239
|
+
device=solver.device,
|
|
240
|
+
dtype=torch.int32,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
field_offset_chunks.append(
|
|
244
|
+
(
|
|
245
|
+
field_i.to(dtype=torch.int64) * (field_shape[1] * field_shape[2])
|
|
246
|
+
+ field_j.to(dtype=torch.int64) * field_shape[2]
|
|
247
|
+
+ field_k.to(dtype=torch.int64)
|
|
248
|
+
).to(dtype=field_offset_dtype).contiguous()
|
|
249
|
+
)
|
|
250
|
+
term_starts.append(coeff_cursor)
|
|
251
|
+
term_shapes.append(list(shape))
|
|
252
|
+
term_offsets.append(list(offsets))
|
|
253
|
+
field_codes.append(field_code)
|
|
254
|
+
coeff_cursor += int(coeff_flat.numel())
|
|
255
|
+
|
|
256
|
+
if not coeff_chunks:
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
coeff_data = torch.cat(coeff_chunks).contiguous()
|
|
260
|
+
return {
|
|
261
|
+
"coeff_data": coeff_data,
|
|
262
|
+
"term_starts": torch.tensor(term_starts, device=solver.device, dtype=torch.int32),
|
|
263
|
+
"term_shapes": torch.tensor(term_shapes, device=solver.device, dtype=torch.int32),
|
|
264
|
+
"term_offsets": torch.tensor(term_offsets, device=solver.device, dtype=torch.int32),
|
|
265
|
+
"field_codes": torch.tensor(field_codes, device=solver.device, dtype=torch.int32),
|
|
266
|
+
"field_codes_per_coeff": torch.cat(field_code_chunks).contiguous(),
|
|
267
|
+
"field_offsets": torch.cat(field_offset_chunks).contiguous(),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def build_batched_reference_terms(solver, terms):
|
|
272
|
+
metadata = _pack_batched_term_metadata(solver, terms)
|
|
273
|
+
if metadata is None:
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
sample_cursor = 0
|
|
277
|
+
sample_axis_codes = []
|
|
278
|
+
sample_index_starts = []
|
|
279
|
+
sample_chunks = []
|
|
280
|
+
sample_index_per_coeff_chunks = []
|
|
281
|
+
for term in terms:
|
|
282
|
+
sample_indices = term["sample_indices"].to(device=solver.device, dtype=torch.int32).reshape(-1)
|
|
283
|
+
shape = tuple(int(length) for length in term["coeff_patch"].shape)
|
|
284
|
+
local_linear = torch.arange(int(term["coeff_patch"].numel()), device=solver.device, dtype=torch.int32)
|
|
285
|
+
stride_i = shape[1] * shape[2]
|
|
286
|
+
local_i = local_linear // stride_i
|
|
287
|
+
remainder = local_linear - local_i * stride_i
|
|
288
|
+
local_j = remainder // shape[2]
|
|
289
|
+
local_k = remainder - local_j * shape[2]
|
|
290
|
+
axis = int(reference_sample_axis_code(term))
|
|
291
|
+
sample_linear = local_i if axis == 0 else (local_j if axis == 1 else local_k)
|
|
292
|
+
sample_chunks.append(sample_indices)
|
|
293
|
+
sample_index_per_coeff_chunks.append(sample_indices[sample_linear.to(dtype=torch.long)])
|
|
294
|
+
sample_axis_codes.append(axis)
|
|
295
|
+
sample_index_starts.append(sample_cursor)
|
|
296
|
+
sample_cursor += int(sample_indices.numel())
|
|
297
|
+
|
|
298
|
+
metadata["sample_axis_codes"] = torch.tensor(sample_axis_codes, device=solver.device, dtype=torch.int32)
|
|
299
|
+
metadata["sample_index_starts"] = torch.tensor(sample_index_starts, device=solver.device, dtype=torch.int32)
|
|
300
|
+
metadata["sample_indices"] = torch.cat(sample_chunks).contiguous()
|
|
301
|
+
metadata["sample_indices_per_coeff"] = torch.cat(sample_index_per_coeff_chunks).contiguous()
|
|
302
|
+
return metadata
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def build_batched_aux_terms(solver, terms):
|
|
306
|
+
metadata = _pack_batched_term_metadata(solver, terms)
|
|
307
|
+
if metadata is None:
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
metadata["sample_positions"] = torch.cat(
|
|
311
|
+
[term["sample_positions"].reshape(-1) for term in terms]
|
|
312
|
+
).contiguous()
|
|
313
|
+
return metadata
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def slice_coeff_patch(solver, spec, curl_attr_map):
|
|
317
|
+
offsets = spec["offsets"]
|
|
318
|
+
shape = spec["shape"]
|
|
319
|
+
axis = spec["delta_axis"]
|
|
320
|
+
scene = solver.scene
|
|
321
|
+
# The spec is a one-cell curl correction: E-face terms divide by the dual
|
|
322
|
+
# spacing at the E element's coordinate, H-face terms by the primal
|
|
323
|
+
# spacing at the H element's coordinate (the spacing their field update
|
|
324
|
+
# uses along the derivative axis).
|
|
325
|
+
if spec["field_name"].startswith("E"):
|
|
326
|
+
spacing64 = {"x": scene.dx_dual64, "y": scene.dy_dual64, "z": scene.dz_dual64}[axis]
|
|
327
|
+
else:
|
|
328
|
+
spacing64 = {"x": scene.dx_primal64, "y": scene.dy_primal64, "z": scene.dz_primal64}[axis]
|
|
329
|
+
delta = float(spacing64[int(offsets[AXIS_INDEX[axis]])])
|
|
330
|
+
return getattr(solver, curl_attr_map[spec["field_name"]])[
|
|
331
|
+
offsets[0] : offsets[0] + shape[0],
|
|
332
|
+
offsets[1] : offsets[1] + shape[1],
|
|
333
|
+
offsets[2] : offsets[2] + shape[2],
|
|
334
|
+
] / delta
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def build_terms_from_specs(solver, specs, vector, curl_attr_map, term_factory):
|
|
338
|
+
terms = []
|
|
339
|
+
for spec in specs:
|
|
340
|
+
component_scale = vector[spec["incident_name"]]
|
|
341
|
+
coeff = float(spec["sign"]) * component_scale
|
|
342
|
+
if isinstance(coeff, torch.Tensor):
|
|
343
|
+
if torch.max(torch.abs(coeff)).item() <= 1e-12:
|
|
344
|
+
continue
|
|
345
|
+
elif abs(float(coeff)) <= 1e-12:
|
|
346
|
+
continue
|
|
347
|
+
coeff_patch = slice_coeff_patch(solver, spec, curl_attr_map)
|
|
348
|
+
term = term_factory(spec, coeff_patch, component_scale)
|
|
349
|
+
if term is not None:
|
|
350
|
+
terms.append(term)
|
|
351
|
+
return terms
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def build_term_from_profile(
|
|
355
|
+
solver,
|
|
356
|
+
*,
|
|
357
|
+
field_name,
|
|
358
|
+
offsets,
|
|
359
|
+
scale,
|
|
360
|
+
delay_patch,
|
|
361
|
+
activation_delay_patch,
|
|
362
|
+
source_time,
|
|
363
|
+
omega,
|
|
364
|
+
source_index=None,
|
|
365
|
+
):
|
|
366
|
+
if torch.max(torch.abs(scale)).item() <= 1e-12:
|
|
367
|
+
return None
|
|
368
|
+
if source_time["kind"] == "cw" and activation_delay_patch is None:
|
|
369
|
+
phase_shift = float(omega) * delay_patch
|
|
370
|
+
return build_source_term(
|
|
371
|
+
solver,
|
|
372
|
+
field_name=field_name,
|
|
373
|
+
offsets=offsets,
|
|
374
|
+
cw_cos_patch=scale * torch.cos(phase_shift),
|
|
375
|
+
cw_sin_patch=scale * torch.sin(phase_shift),
|
|
376
|
+
source_index=source_index,
|
|
377
|
+
source_time=source_time,
|
|
378
|
+
omega=omega,
|
|
379
|
+
)
|
|
380
|
+
return build_source_term(
|
|
381
|
+
solver,
|
|
382
|
+
field_name=field_name,
|
|
383
|
+
offsets=offsets,
|
|
384
|
+
patch=scale,
|
|
385
|
+
delay_patch=delay_patch,
|
|
386
|
+
activation_delay_patch=activation_delay_patch,
|
|
387
|
+
source_index=source_index,
|
|
388
|
+
source_time=source_time,
|
|
389
|
+
omega=omega,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def incident_profile(
|
|
394
|
+
solver,
|
|
395
|
+
source,
|
|
396
|
+
positions,
|
|
397
|
+
reference_point,
|
|
398
|
+
*,
|
|
399
|
+
phase_speed,
|
|
400
|
+
source_frequency=None,
|
|
401
|
+
polarization_override=None,
|
|
402
|
+
):
|
|
403
|
+
if source["kind"] == "plane_wave":
|
|
404
|
+
return plane_wave_profile(
|
|
405
|
+
positions,
|
|
406
|
+
direction=source["direction"],
|
|
407
|
+
reference_point=reference_point,
|
|
408
|
+
propagation_speed=phase_speed,
|
|
409
|
+
)
|
|
410
|
+
return beam_profile_from_source(
|
|
411
|
+
positions,
|
|
412
|
+
source,
|
|
413
|
+
frequency=solver.source_frequency if source_frequency is None else float(source_frequency),
|
|
414
|
+
propagation_speed=phase_speed,
|
|
415
|
+
polarization=polarization_override,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def project_positions(positions, direction):
|
|
420
|
+
direction_tensor = positions.new_tensor(direction)
|
|
421
|
+
return torch.sum(positions * direction_tensor, dim=-1)
|