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,510 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import torch
5
+
6
+ from ...sources import (
7
+ SOURCE_TIME_KIND_CW,
8
+ SOURCE_TIME_KIND_GAUSSIAN_PULSE,
9
+ evaluate_source_time,
10
+ )
11
+
12
+
13
+ def build_source_term(
14
+ solver,
15
+ *,
16
+ field_name,
17
+ offsets,
18
+ patch=None,
19
+ phase_real=1.0,
20
+ phase_imag=0.0,
21
+ delay_patch=None,
22
+ activation_delay_patch=None,
23
+ cw_cos_patch=None,
24
+ cw_sin_patch=None,
25
+ source_index=None,
26
+ source_time=None,
27
+ omega=None,
28
+ ):
29
+ grid_tensor = patch
30
+ if grid_tensor is None:
31
+ grid_tensor = delay_patch
32
+ if grid_tensor is None:
33
+ grid_tensor = cw_cos_patch
34
+ if grid_tensor is None:
35
+ grid_tensor = cw_sin_patch
36
+ if grid_tensor is None:
37
+ raise ValueError("source term requires at least one tensor payload.")
38
+
39
+ return {
40
+ "field_name": field_name,
41
+ "offsets": offsets,
42
+ "patch": None if patch is None else patch.contiguous(),
43
+ "phase_real": float(phase_real),
44
+ "phase_imag": float(phase_imag),
45
+ "delay_patch": None if delay_patch is None else delay_patch.contiguous(),
46
+ "activation_delay_patch": (
47
+ None if activation_delay_patch is None else activation_delay_patch.contiguous()
48
+ ),
49
+ "cw_cos_patch": None if cw_cos_patch is None else cw_cos_patch.contiguous(),
50
+ "cw_sin_patch": None if cw_sin_patch is None else cw_sin_patch.contiguous(),
51
+ "source_index": None if source_index is None else int(source_index),
52
+ "source_time": source_time,
53
+ "omega": None if omega is None else float(omega),
54
+ }
55
+
56
+
57
+ def append_source_term(term_list, solver, **kwargs):
58
+ term_list.append(build_source_term(solver, **kwargs))
59
+
60
+
61
+ def _resolve_time_shift_patch(term):
62
+ activation_delay_patch = term.get("activation_delay_patch")
63
+ return activation_delay_patch if activation_delay_patch is not None else term["delay_patch"]
64
+
65
+
66
+ def _resolve_cw_signal(omega, source_time, time_value):
67
+ phase = float(omega) * float(time_value) + float(source_time["phase"])
68
+ amplitude = float(source_time["amplitude"])
69
+ return amplitude * np.cos(phase), amplitude * np.sin(phase)
70
+
71
+
72
+ def _resolve_term_source(term, default_source_time, default_omega):
73
+ term_source_time = term.get("source_time") or default_source_time
74
+ term_omega = float(default_omega if term.get("omega") is None else term["omega"])
75
+ return term_source_time, term_omega
76
+
77
+
78
+ def _resolve_term_cache_key(term, term_source_time, term_omega):
79
+ source_index = term.get("source_index")
80
+ if source_index is not None:
81
+ return int(source_index)
82
+ return (id(term_source_time), float(term_omega))
83
+
84
+
85
+ def _launch_uniform_patch(solver, *, field_name, source_patch, offsets, signal):
86
+ offset_i, offset_j, offset_k = offsets
87
+ solver.fdtd_module.addSourcePatch3D(
88
+ field=getattr(solver, field_name),
89
+ sourcePatch=source_patch,
90
+ offsetI=int(offset_i),
91
+ offsetJ=int(offset_j),
92
+ offsetK=int(offset_k),
93
+ signal=float(signal),
94
+ ).launchRaw()
95
+
96
+
97
+ _COMPONENT_TANGENTIAL_AXES = {
98
+ "x": ("y", "z"),
99
+ "y": ("x", "z"),
100
+ "z": ("x", "y"),
101
+ }
102
+ _COMPONENT_AXIS_CODE = {"x": 0, "y": 1, "z": 2}
103
+
104
+
105
+ def _component_name(field_name):
106
+ return field_name[-1].lower()
107
+
108
+
109
+ def _wrap_axes_for_kind(solver, field_name, kind):
110
+ tangential_axes = _COMPONENT_TANGENTIAL_AXES[_component_name(field_name)]
111
+ return tuple(
112
+ 1 if solver.scene.boundary.axis_kind(axis) == kind else 0
113
+ for axis in tangential_axes
114
+ )
115
+
116
+
117
+ def _periodic_wrap_axes(solver, field_name):
118
+ return _wrap_axes_for_kind(solver, field_name, "periodic")
119
+
120
+
121
+ def _complex_wrap_axes(solver, field_name):
122
+ tangential_axes = _COMPONENT_TANGENTIAL_AXES[_component_name(field_name)]
123
+ return tuple(
124
+ 1 if solver.scene.boundary.axis_kind(axis) in {"bloch", "periodic"} else 0
125
+ for axis in tangential_axes
126
+ )
127
+
128
+
129
+ def _launch_cw_patch(solver, *, field_name, term, signal_cos, signal_sin):
130
+ offset_i, offset_j, offset_k = term["offsets"]
131
+ solver.fdtd_module.addCwPhasedSourcePatch3D(
132
+ field=getattr(solver, field_name),
133
+ sourcePatchCos=term["cw_cos_patch"],
134
+ sourcePatchSin=term["cw_sin_patch"],
135
+ offsetI=int(offset_i),
136
+ offsetJ=int(offset_j),
137
+ offsetK=int(offset_k),
138
+ signalCos=float(signal_cos),
139
+ signalSin=float(signal_sin),
140
+ ).launchRaw()
141
+
142
+
143
+ def _field_triplet_kwargs(solver, field_name):
144
+ if field_name[0] == "H":
145
+ return {
146
+ "ExReal": solver.Hx,
147
+ "ExImag": solver.Hx_imag,
148
+ "EyReal": solver.Hy,
149
+ "EyImag": solver.Hy_imag,
150
+ "EzReal": solver.Hz,
151
+ "EzImag": solver.Hz_imag,
152
+ }
153
+ return {
154
+ "ExReal": solver.Ex,
155
+ "ExImag": solver.Ex_imag,
156
+ "EyReal": solver.Ey,
157
+ "EyImag": solver.Ey_imag,
158
+ "EzReal": solver.Ez,
159
+ "EzImag": solver.Ez_imag,
160
+ }
161
+
162
+
163
+ def _bloch_phase_kwargs(solver, field_name):
164
+ component = _component_name(field_name)
165
+ if component == "x":
166
+ return {
167
+ "phaseCosA": _phase_cos_for_axis(solver, "y"),
168
+ "phaseSinA": _phase_sin_for_axis(solver, "y"),
169
+ "phaseCosB": _phase_cos_for_axis(solver, "z"),
170
+ "phaseSinB": _phase_sin_for_axis(solver, "z"),
171
+ }
172
+ if component == "y":
173
+ return {
174
+ "phaseCosA": _phase_cos_for_axis(solver, "x"),
175
+ "phaseSinA": _phase_sin_for_axis(solver, "x"),
176
+ "phaseCosB": _phase_cos_for_axis(solver, "z"),
177
+ "phaseSinB": _phase_sin_for_axis(solver, "z"),
178
+ }
179
+ return {
180
+ "phaseCosA": _phase_cos_for_axis(solver, "x"),
181
+ "phaseSinA": _phase_sin_for_axis(solver, "x"),
182
+ "phaseCosB": _phase_cos_for_axis(solver, "y"),
183
+ "phaseSinB": _phase_sin_for_axis(solver, "y"),
184
+ }
185
+
186
+
187
+ def _phase_cos_for_axis(solver, axis):
188
+ axis_index = {"x": 0, "y": 1, "z": 2}[axis]
189
+ if solver.scene.boundary.axis_kind(axis) == "periodic":
190
+ return 1.0
191
+ return solver.boundary_phase_cos[axis_index]
192
+
193
+
194
+ def _phase_sin_for_axis(solver, axis):
195
+ axis_index = {"x": 0, "y": 1, "z": 2}[axis]
196
+ if solver.scene.boundary.axis_kind(axis) == "periodic":
197
+ return 0.0
198
+ return solver.boundary_phase_sin[axis_index]
199
+
200
+
201
+ def _bloch_patch_common_kwargs(solver, field_name, term):
202
+ wrap_axis_a, wrap_axis_b = _complex_wrap_axes(solver, field_name)
203
+ offset_i, offset_j, offset_k = term["offsets"]
204
+ kernel_kwargs = {
205
+ "offsetI": int(offset_i),
206
+ "offsetJ": int(offset_j),
207
+ "offsetK": int(offset_k),
208
+ "axisCode": _COMPONENT_AXIS_CODE[_component_name(field_name)],
209
+ "wrapAxisA": int(wrap_axis_a),
210
+ "wrapAxisB": int(wrap_axis_b),
211
+ }
212
+ kernel_kwargs.update(_field_triplet_kwargs(solver, field_name))
213
+ kernel_kwargs.update(_bloch_phase_kwargs(solver, field_name))
214
+ return kernel_kwargs
215
+
216
+
217
+ def _launch_time_shifted_patch(solver, *, field_name, term, source_time, time_value):
218
+ offset_i, offset_j, offset_k = term["offsets"]
219
+ solver.fdtd_module.addTimeShiftedSourcePatch3D(
220
+ field=getattr(solver, field_name),
221
+ sourcePatch=term["patch"],
222
+ delayPatch=term["delay_patch"],
223
+ activationDelayPatch=_resolve_time_shift_patch(term),
224
+ offsetI=int(offset_i),
225
+ offsetJ=int(offset_j),
226
+ offsetK=int(offset_k),
227
+ timeKind=int(source_time["kind_code"]),
228
+ time=float(time_value),
229
+ frequency=float(source_time["frequency"]),
230
+ fwidth=float(source_time["fwidth"]),
231
+ amplitude=float(source_time["amplitude"]),
232
+ phase=float(source_time["phase"]),
233
+ delay=float(source_time["delay"]),
234
+ causalGate=1 if term.get("activation_delay_patch") is not None else 0,
235
+ ).launchRaw()
236
+
237
+
238
+ def _evaluate_time_shifted_signal_tensor(source_time, sample_time):
239
+ # Per-cell source-time waveform for a time-shifted patch, matching the
240
+ # ``evaluate_source_time`` device kernel branch-for-branch (kind 0 = CW,
241
+ # kind 1 = Gaussian pulse, anything else = Ricker) so a Bloch pulse injected
242
+ # through this Torch path is numerically identical to the non-Bloch kernel.
243
+ kind_code = int(source_time["kind_code"])
244
+ amplitude = float(source_time["amplitude"])
245
+ frequency = float(source_time["frequency"])
246
+ phase = float(source_time.get("phase", 0.0))
247
+ delay = float(source_time.get("delay", 0.0))
248
+ two_pi = 2.0 * np.pi
249
+ if kind_code == SOURCE_TIME_KIND_CW:
250
+ return amplitude * torch.cos(two_pi * frequency * sample_time + phase)
251
+ if kind_code == SOURCE_TIME_KIND_GAUSSIAN_PULSE:
252
+ inv_sigma = max(two_pi * float(source_time["fwidth"]), 1.0e-30)
253
+ tau = sample_time - delay
254
+ envelope = torch.exp(-0.5 * (tau * inv_sigma) ** 2)
255
+ return amplitude * envelope * torch.cos(two_pi * frequency * tau + phase)
256
+ tau = sample_time - delay
257
+ alpha = np.pi * frequency * tau
258
+ alpha_sq = alpha * alpha
259
+ return amplitude * (1.0 - 2.0 * alpha_sq) * torch.exp(-alpha_sq)
260
+
261
+
262
+ def _time_shifted_signal_patch(term, source_time, time_value):
263
+ # Collapse the delayed pulse to a real per-cell amplitude patch on-device.
264
+ # The scatter into the split real/imag Bloch field (interior injection plus
265
+ # the phase-rotated wrap copies) is still the native ``addSourcePatchBloch3D``
266
+ # kernel; only the envelope evaluation is Torch, exactly as the CW-phased
267
+ # Bloch path already precomputes its cos/sin combination before scattering.
268
+ sample_time = float(time_value) - term["delay_patch"]
269
+ signal_patch = _evaluate_time_shifted_signal_tensor(source_time, sample_time) * term["patch"]
270
+ activation_delay_patch = term.get("activation_delay_patch")
271
+ if activation_delay_patch is not None:
272
+ signal_patch = torch.where(
273
+ activation_delay_patch > float(time_value),
274
+ torch.zeros_like(signal_patch),
275
+ signal_patch,
276
+ )
277
+ return signal_patch.contiguous()
278
+
279
+
280
+ def _launch_bloch_time_shifted_patch(solver, *, field_name, term, source_time, time_value):
281
+ signal_patch = _time_shifted_signal_patch(term, source_time, time_value)
282
+ kernel_kwargs = _bloch_patch_common_kwargs(solver, field_name, term)
283
+ kernel_kwargs.update(
284
+ {
285
+ "sourcePatch": signal_patch,
286
+ "signalReal": 1.0,
287
+ "signalImag": 0.0,
288
+ }
289
+ )
290
+ solver.fdtd_module.addSourcePatchBloch3D(**kernel_kwargs).launchRaw()
291
+
292
+
293
+ def apply_generic_source_terms(solver, terms, *, source_time, omega, time_value, clamp_pec=True):
294
+ if not terms:
295
+ return
296
+
297
+ signal_cache = {}
298
+ cw_cache = {}
299
+ for term in terms:
300
+ field_name = term["field_name"]
301
+ term_source_time, term_omega = _resolve_term_source(term, source_time, omega)
302
+ cache_key = _resolve_term_cache_key(term, term_source_time, term_omega)
303
+ if term["cw_cos_patch"] is not None:
304
+ if cache_key not in cw_cache:
305
+ cw_cache[cache_key] = _resolve_cw_signal(term_omega, term_source_time, time_value)
306
+ signal_cos, signal_sin = cw_cache[cache_key]
307
+ if solver.scene.boundary.uses_kind("bloch"):
308
+ _launch_bloch_cw_patch(
309
+ solver,
310
+ field_name=field_name,
311
+ term=term,
312
+ signal_cos=signal_cos,
313
+ signal_sin=signal_sin,
314
+ )
315
+ continue
316
+ _launch_cw_patch(
317
+ solver,
318
+ field_name=field_name,
319
+ term=term,
320
+ signal_cos=signal_cos,
321
+ signal_sin=signal_sin,
322
+ )
323
+ continue
324
+ if term["delay_patch"] is not None:
325
+ if solver.scene.boundary.uses_kind("bloch"):
326
+ _launch_bloch_time_shifted_patch(
327
+ solver,
328
+ field_name=field_name,
329
+ term=term,
330
+ source_time=term_source_time,
331
+ time_value=time_value,
332
+ )
333
+ continue
334
+ _launch_time_shifted_patch(
335
+ solver,
336
+ field_name=field_name,
337
+ term=term,
338
+ source_time=term_source_time,
339
+ time_value=time_value,
340
+ )
341
+ continue
342
+ if cache_key not in signal_cache:
343
+ signal_cache[cache_key] = evaluate_source_time(term_source_time, time_value)
344
+ if solver.scene.boundary.uses_kind("bloch"):
345
+ _launch_bloch_patch(
346
+ solver,
347
+ field_name=field_name,
348
+ term=term,
349
+ signal=signal_cache[cache_key],
350
+ )
351
+ continue
352
+ if solver.scene.boundary.uses_kind("periodic"):
353
+ _launch_periodic_patch(
354
+ solver,
355
+ field_name=field_name,
356
+ term=term,
357
+ signal=signal_cache[cache_key],
358
+ )
359
+ continue
360
+ _launch_uniform_patch(
361
+ solver,
362
+ field_name=field_name,
363
+ source_patch=term["patch"],
364
+ offsets=term["offsets"],
365
+ signal=signal_cache[cache_key],
366
+ )
367
+
368
+ if clamp_pec:
369
+ solver._clamp_pec_boundaries()
370
+
371
+
372
+ def _launch_periodic_patch(solver, *, field_name, term, signal):
373
+ wrap_axis_a, wrap_axis_b = _periodic_wrap_axes(solver, field_name)
374
+ scaled_signal = float(signal) * float(term["phase_real"])
375
+ if not wrap_axis_a and not wrap_axis_b:
376
+ _launch_uniform_patch(
377
+ solver,
378
+ field_name=field_name,
379
+ source_patch=term["patch"],
380
+ offsets=term["offsets"],
381
+ signal=scaled_signal,
382
+ )
383
+ return
384
+ offset_i, offset_j, offset_k = term["offsets"]
385
+ component = _component_name(field_name)
386
+ kernel_name = f"addSourcePatchE{component}Periodic3D"
387
+ field_arg_name = f"E{component}"
388
+ getattr(solver.fdtd_module, kernel_name)(
389
+ **{
390
+ field_arg_name: getattr(solver, field_name),
391
+ "sourcePatch": term["patch"],
392
+ "offsetI": int(offset_i),
393
+ "offsetJ": int(offset_j),
394
+ "offsetK": int(offset_k),
395
+ "signal": scaled_signal,
396
+ "wrapAxisA": int(wrap_axis_a),
397
+ "wrapAxisB": int(wrap_axis_b),
398
+ }
399
+ ).launchRaw()
400
+
401
+
402
+ def _launch_bloch_patch(solver, *, field_name, term, signal):
403
+ kernel_kwargs = _bloch_patch_common_kwargs(solver, field_name, term)
404
+ kernel_kwargs.update(
405
+ {
406
+ "sourcePatch": term["patch"],
407
+ "signalReal": float(signal) * float(term["phase_real"]),
408
+ "signalImag": float(signal) * float(term["phase_imag"]),
409
+ }
410
+ )
411
+ solver.fdtd_module.addSourcePatchBloch3D(**kernel_kwargs).launchRaw()
412
+
413
+
414
+ def _launch_bloch_cw_patch(solver, *, field_name, term, signal_cos, signal_sin):
415
+ kernel_kwargs = _bloch_patch_common_kwargs(solver, field_name, term)
416
+ kernel_kwargs.update(
417
+ {
418
+ "sourcePatchCos": term["cw_cos_patch"],
419
+ "sourcePatchSin": term["cw_sin_patch"],
420
+ "signalCos": float(signal_cos),
421
+ "signalSin": float(signal_sin),
422
+ }
423
+ )
424
+ solver.fdtd_module.addCwPhasedSourcePatchBloch3D(**kernel_kwargs).launchRaw()
425
+
426
+
427
+ def apply_compiled_source_terms(solver, terms, *, source_time, omega, time_value, signal=None):
428
+ if not terms:
429
+ return
430
+
431
+ default_key = _resolve_term_cache_key(
432
+ {"source_index": None},
433
+ source_time,
434
+ float(omega),
435
+ )
436
+ signal_cache = {}
437
+ cw_cache = {}
438
+ for term in terms:
439
+ field_name = term["field_name"]
440
+ term_source_time, term_omega = _resolve_term_source(term, source_time, omega)
441
+ cache_key = _resolve_term_cache_key(term, term_source_time, term_omega)
442
+ if term["cw_cos_patch"] is not None:
443
+ if cache_key not in cw_cache:
444
+ cw_cache[cache_key] = _resolve_cw_signal(term_omega, term_source_time, time_value)
445
+ signal_cos, signal_sin = cw_cache[cache_key]
446
+ if solver.scene.boundary.uses_kind("bloch"):
447
+ _launch_bloch_cw_patch(
448
+ solver,
449
+ field_name=field_name,
450
+ term=term,
451
+ signal_cos=signal_cos,
452
+ signal_sin=signal_sin,
453
+ )
454
+ continue
455
+ _launch_cw_patch(
456
+ solver,
457
+ field_name=field_name,
458
+ term=term,
459
+ signal_cos=signal_cos,
460
+ signal_sin=signal_sin,
461
+ )
462
+ continue
463
+ if term["delay_patch"] is not None:
464
+ if solver.scene.boundary.uses_kind("bloch"):
465
+ _launch_bloch_time_shifted_patch(
466
+ solver,
467
+ field_name=field_name,
468
+ term=term,
469
+ source_time=term_source_time,
470
+ time_value=time_value,
471
+ )
472
+ continue
473
+ _launch_time_shifted_patch(
474
+ solver,
475
+ field_name=field_name,
476
+ term=term,
477
+ source_time=term_source_time,
478
+ time_value=time_value,
479
+ )
480
+ continue
481
+ if cache_key not in signal_cache:
482
+ if signal is not None and cache_key == default_key:
483
+ signal_cache[cache_key] = signal
484
+ else:
485
+ signal_cache[cache_key] = evaluate_source_time(term_source_time, time_value)
486
+ if solver.scene.boundary.uses_kind("bloch"):
487
+ _launch_bloch_patch(
488
+ solver,
489
+ field_name=field_name,
490
+ term=term,
491
+ signal=signal_cache[cache_key],
492
+ )
493
+ continue
494
+ if solver.scene.boundary.uses_kind("periodic"):
495
+ _launch_periodic_patch(
496
+ solver,
497
+ field_name=field_name,
498
+ term=term,
499
+ signal=signal_cache[cache_key],
500
+ )
501
+ continue
502
+ _launch_uniform_patch(
503
+ solver,
504
+ field_name=field_name,
505
+ source_patch=term["patch"],
506
+ offsets=term["offsets"],
507
+ signal=float(signal_cache[cache_key]) * float(term["phase_real"]),
508
+ )
509
+
510
+ solver._clamp_pec_boundaries()