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,524 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from types import SimpleNamespace
5
+
6
+ import torch
7
+
8
+ from ..boundary import has_complex_fields
9
+ from ..observers import get_observer_results as get_observer_results_impl
10
+ from ..postprocess import get_frequency_solution as get_frequency_solution_impl
11
+
12
+
13
+ def _runtime():
14
+ from . import core as _adjoint
15
+
16
+ return _adjoint
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class _FieldAccumulatorPair:
21
+ entry_index: int
22
+ state_field_name: str
23
+ real_index: int
24
+ imag_index: int
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class _PointAccumulatorPair:
29
+ state_field_name: str
30
+ global_indices: tuple[int, ...]
31
+ point_i: torch.Tensor
32
+ point_j: torch.Tensor
33
+ point_k: torch.Tensor
34
+ real_index: int
35
+ imag_index: int
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class _PlaneAccumulatorPair:
40
+ state_field_name: str
41
+ global_indices: tuple[int, ...]
42
+ axis_code: int
43
+ plane_index: int
44
+ real_index: int
45
+ imag_index: int
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class _ScheduleTensorPack:
50
+ cos: torch.Tensor
51
+ sin: torch.Tensor
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class _DenseSeedBatch:
56
+ state_field_name: str
57
+ entry_indices: torch.Tensor
58
+ grad_real: torch.Tensor
59
+ grad_imag: torch.Tensor
60
+ cos_pack: torch.Tensor
61
+ sin_pack: torch.Tensor
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class _PointSeedBatch:
66
+ state_field_name: str
67
+ entry_indices: torch.Tensor
68
+ point_i: torch.Tensor
69
+ point_j: torch.Tensor
70
+ point_k: torch.Tensor
71
+ grad_real: torch.Tensor
72
+ grad_imag: torch.Tensor
73
+ cos_pack: torch.Tensor
74
+ sin_pack: torch.Tensor
75
+ point_i32: torch.Tensor
76
+ point_j32: torch.Tensor
77
+ point_k32: torch.Tensor
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class _PlaneSeedBatch:
82
+ state_field_name: str
83
+ axis_code: int
84
+ plane_index: int
85
+ entry_indices: torch.Tensor
86
+ grad_real: torch.Tensor
87
+ grad_imag: torch.Tensor
88
+ cos_pack: torch.Tensor
89
+ sin_pack: torch.Tensor
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class _SeedRuntime:
94
+ dft_schedule: _ScheduleTensorPack
95
+ observer_schedule: _ScheduleTensorPack
96
+ dense_batches: tuple[_DenseSeedBatch, ...]
97
+ point_batches: tuple[_PointSeedBatch, ...]
98
+ plane_batches: tuple[_PlaneSeedBatch, ...]
99
+ backend: str = "device_batched"
100
+
101
+
102
+ def _schedule_to_tensor_pack(schedules, *, device, dtype) -> _ScheduleTensorPack:
103
+ if not schedules:
104
+ empty = torch.zeros((0, 0), device=device, dtype=dtype)
105
+ return _ScheduleTensorPack(cos=empty, sin=empty)
106
+ cos = torch.tensor(
107
+ [[float(weight_cos) for weight_cos, _weight_sin in entry] for entry in schedules],
108
+ device=device,
109
+ dtype=dtype,
110
+ )
111
+ sin = torch.tensor(
112
+ [[float(weight_sin) for _weight_cos, weight_sin in entry] for entry in schedules],
113
+ device=device,
114
+ dtype=dtype,
115
+ )
116
+ return _ScheduleTensorPack(cos=cos, sin=sin)
117
+
118
+
119
+ def _reshape_entry_major_grad(grad: torch.Tensor, entry_count: int) -> torch.Tensor:
120
+ if entry_count <= 0:
121
+ raise ValueError("Seed batch must contain at least one entry.")
122
+ detached = grad.detach()
123
+ if entry_count == 1:
124
+ if detached.ndim == 0 or detached.shape[0] != 1:
125
+ detached = detached.unsqueeze(0)
126
+ return detached.contiguous()
127
+ if detached.ndim == 0 or detached.shape[0] != entry_count:
128
+ raise RuntimeError(
129
+ "Seed gradient layout does not match the observer/global frequency index structure."
130
+ )
131
+ return detached.contiguous()
132
+
133
+
134
+ def _stack_dense_seed_records(records) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
135
+ entry_indices = torch.tensor(
136
+ [int(entry_index) for entry_index, _grad_real, _grad_imag in records],
137
+ device=records[0][1].device,
138
+ dtype=torch.long,
139
+ )
140
+ grad_real = torch.stack([grad_real.detach() for _entry_index, grad_real, _grad_imag in records], dim=0).contiguous()
141
+ grad_imag = torch.stack([grad_imag.detach() for _entry_index, _grad_real, grad_imag in records], dim=0).contiguous()
142
+ return entry_indices, grad_real, grad_imag
143
+
144
+
145
+ def _seed_batch_contribution(
146
+ grad_real: torch.Tensor,
147
+ grad_imag: torch.Tensor,
148
+ weight_cos: torch.Tensor,
149
+ weight_sin: torch.Tensor,
150
+ ) -> torch.Tensor:
151
+ view_shape = (int(weight_cos.shape[0]),) + (1,) * max(grad_real.ndim - 1, 0)
152
+ return torch.sum(
153
+ grad_real * weight_cos.view(view_shape) + grad_imag * weight_sin.view(view_shape),
154
+ dim=0,
155
+ )
156
+
157
+
158
+ def _clone_seed_solver(solver):
159
+ seed_solver = SimpleNamespace()
160
+ seed_solver.scene = solver.scene
161
+ seed_solver.verbose = False
162
+ seed_solver.complex_fields_enabled = has_complex_fields(solver)
163
+ seed_solver._normalize_source = getattr(solver, "_normalize_source", False)
164
+ seed_solver._source_time = getattr(solver, "_source_time", None)
165
+ seed_solver.dft_window_type = getattr(solver, "dft_window_type", "none")
166
+ seed_solver.observer_window_type = getattr(solver, "observer_window_type", "none")
167
+ seed_solver.observers_enabled = bool(getattr(solver, "observers_enabled", False))
168
+ seed_solver.observers = list(getattr(solver, "observers", ()))
169
+ seed_solver._point_observer_groups = {}
170
+ seed_solver._plane_observer_groups = {}
171
+
172
+ leaves = []
173
+ field_pairs = []
174
+ point_pairs = []
175
+ plane_pairs = []
176
+
177
+ seed_solver._dft_entries = []
178
+ for entry_index, entry in enumerate(getattr(solver, "_dft_entries", ())):
179
+ cloned_entry = dict(entry)
180
+ cloned_fields = {}
181
+ for field_name in ("Ex", "Ey", "Ez"):
182
+ payload = entry["fields"][field_name]
183
+ real = payload["real"].detach().clone().requires_grad_(True)
184
+ imag = payload["imag"].detach().clone().requires_grad_(True)
185
+ real_index = len(leaves)
186
+ imag_index = len(leaves) + 1
187
+ leaves.extend([real, imag])
188
+ field_pairs.append(
189
+ _FieldAccumulatorPair(
190
+ entry_index=entry_index,
191
+ state_field_name=field_name,
192
+ real_index=real_index,
193
+ imag_index=imag_index,
194
+ )
195
+ )
196
+ aux_real = None
197
+ aux_imag = None
198
+ if payload.get("aux_real") is not None and payload.get("aux_imag") is not None:
199
+ aux_real = payload["aux_real"].detach().clone().requires_grad_(True)
200
+ aux_imag = payload["aux_imag"].detach().clone().requires_grad_(True)
201
+ aux_real_index = len(leaves)
202
+ aux_imag_index = len(leaves) + 1
203
+ leaves.extend([aux_real, aux_imag])
204
+ field_pairs.append(
205
+ _FieldAccumulatorPair(
206
+ entry_index=entry_index,
207
+ state_field_name=f"{field_name}_imag",
208
+ real_index=aux_real_index,
209
+ imag_index=aux_imag_index,
210
+ )
211
+ )
212
+ cloned_fields[field_name] = {
213
+ "real": real,
214
+ "imag": imag,
215
+ "aux_real": aux_real,
216
+ "aux_imag": aux_imag,
217
+ }
218
+ cloned_entry["fields"] = cloned_fields
219
+ seed_solver._dft_entries.append(cloned_entry)
220
+
221
+ seed_solver._observer_spectral_entries = [dict(entry) for entry in getattr(solver, "_observer_spectral_entries", ())]
222
+
223
+ for group_key, group in getattr(solver, "_point_observer_groups", {}).items():
224
+ cloned_group = dict(group)
225
+ real = group["real"].detach().clone().requires_grad_(True)
226
+ imag = group["imag"].detach().clone().requires_grad_(True)
227
+ real_index = len(leaves)
228
+ imag_index = len(leaves) + 1
229
+ leaves.extend([real, imag])
230
+ point_pairs.append(
231
+ _PointAccumulatorPair(
232
+ state_field_name=group["field_name"],
233
+ global_indices=tuple(group["global_freq_indices"]),
234
+ point_i=group["point_i"].to(dtype=torch.long),
235
+ point_j=group["point_j"].to(dtype=torch.long),
236
+ point_k=group["point_k"].to(dtype=torch.long),
237
+ real_index=real_index,
238
+ imag_index=imag_index,
239
+ )
240
+ )
241
+ cloned_group["real"] = real
242
+ cloned_group["imag"] = imag
243
+ if "aux_real" in group and "aux_imag" in group:
244
+ aux_real = group["aux_real"].detach().clone().requires_grad_(True)
245
+ aux_imag = group["aux_imag"].detach().clone().requires_grad_(True)
246
+ aux_real_index = len(leaves)
247
+ aux_imag_index = len(leaves) + 1
248
+ leaves.extend([aux_real, aux_imag])
249
+ point_pairs.append(
250
+ _PointAccumulatorPair(
251
+ state_field_name=f"{group['field_name']}_imag",
252
+ global_indices=tuple(group["global_freq_indices"]),
253
+ point_i=group["point_i"].to(dtype=torch.long),
254
+ point_j=group["point_j"].to(dtype=torch.long),
255
+ point_k=group["point_k"].to(dtype=torch.long),
256
+ real_index=aux_real_index,
257
+ imag_index=aux_imag_index,
258
+ )
259
+ )
260
+ cloned_group["aux_real"] = aux_real
261
+ cloned_group["aux_imag"] = aux_imag
262
+ seed_solver._point_observer_groups[group_key] = cloned_group
263
+
264
+ seed_solver._plane_observer_groups = {}
265
+ for group_key, group in getattr(solver, "_plane_observer_groups", {}).items():
266
+ cloned_group = dict(group)
267
+ real = group["real"].detach().clone().requires_grad_(True)
268
+ imag = group["imag"].detach().clone().requires_grad_(True)
269
+ real_index = len(leaves)
270
+ imag_index = len(leaves) + 1
271
+ leaves.extend([real, imag])
272
+ plane_pairs.append(
273
+ _PlaneAccumulatorPair(
274
+ state_field_name=group["field_name"],
275
+ global_indices=tuple(group["global_freq_indices"]),
276
+ axis_code=int(group["axis_code"]),
277
+ plane_index=int(group["plane_index"]),
278
+ real_index=real_index,
279
+ imag_index=imag_index,
280
+ )
281
+ )
282
+ cloned_group["real"] = real
283
+ cloned_group["imag"] = imag
284
+ if "aux_real" in group and "aux_imag" in group:
285
+ aux_real = group["aux_real"].detach().clone().requires_grad_(True)
286
+ aux_imag = group["aux_imag"].detach().clone().requires_grad_(True)
287
+ aux_real_index = len(leaves)
288
+ aux_imag_index = len(leaves) + 1
289
+ leaves.extend([aux_real, aux_imag])
290
+ plane_pairs.append(
291
+ _PlaneAccumulatorPair(
292
+ state_field_name=f"{group['field_name']}_imag",
293
+ global_indices=tuple(group["global_freq_indices"]),
294
+ axis_code=int(group["axis_code"]),
295
+ plane_index=int(group["plane_index"]),
296
+ real_index=aux_real_index,
297
+ imag_index=aux_imag_index,
298
+ )
299
+ )
300
+ cloned_group["aux_real"] = aux_real
301
+ cloned_group["aux_imag"] = aux_imag
302
+ seed_solver._plane_observer_groups[group_key] = cloned_group
303
+
304
+ return seed_solver, tuple(leaves), field_pairs, point_pairs, plane_pairs
305
+
306
+
307
+ def _dense_seed_output_pairs(seed_solver):
308
+ runtime = _runtime()
309
+ raw_output = {}
310
+ if getattr(seed_solver, "_dft_entries", ()):
311
+ raw_output.update(
312
+ get_frequency_solution_impl(
313
+ seed_solver,
314
+ all_frequencies=True,
315
+ )
316
+ )
317
+ if getattr(seed_solver, "observers_enabled", False):
318
+ raw_output["observers"] = get_observer_results_impl(seed_solver)
319
+ return runtime._prepare_forward_pack(raw_output)
320
+
321
+
322
+ def _build_output_seeds(
323
+ solver,
324
+ pack,
325
+ grad_outputs,
326
+ *,
327
+ dft_schedule: _ScheduleTensorPack,
328
+ observer_schedule: _ScheduleTensorPack,
329
+ ) -> _SeedRuntime:
330
+ runtime = _runtime()
331
+ with torch.enable_grad():
332
+ seed_solver, leaves, field_pairs, point_pairs, plane_pairs = _clone_seed_solver(solver)
333
+ seed_pack = _dense_seed_output_pairs(seed_solver)
334
+ if len(seed_pack.output_tensors) != len(pack.output_tensors):
335
+ raise RuntimeError("Adjoint output pack layout changed between forward and backward.")
336
+ output_grads = tuple(
337
+ torch.zeros_like(output) if grad_output is None else grad_output.to(device=output.device, dtype=output.dtype)
338
+ for output, grad_output in zip(seed_pack.output_tensors, grad_outputs)
339
+ )
340
+ leaf_grads = torch.autograd.grad(
341
+ seed_pack.output_tensors,
342
+ leaves,
343
+ grad_outputs=output_grads,
344
+ allow_unused=True,
345
+ )
346
+
347
+ dense_seed_records: dict[str, list[tuple[int, torch.Tensor, torch.Tensor]]] = {}
348
+ for pair in field_pairs:
349
+ real_leaf = leaves[pair.real_index]
350
+ imag_leaf = leaves[pair.imag_index]
351
+ dense_seed_records.setdefault(pair.state_field_name, []).append(
352
+ (
353
+ int(pair.entry_index),
354
+ runtime._safe_grad(leaf_grads[pair.real_index], real_leaf),
355
+ runtime._safe_grad(leaf_grads[pair.imag_index], imag_leaf),
356
+ )
357
+ )
358
+
359
+ dense_batches = []
360
+ for state_field_name, records in dense_seed_records.items():
361
+ entry_indices, grad_real, grad_imag = _stack_dense_seed_records(records)
362
+ cos_pack, sin_pack = _schedule_pack_for(dft_schedule, entry_indices)
363
+ dense_batches.append(
364
+ _DenseSeedBatch(
365
+ state_field_name=state_field_name,
366
+ entry_indices=entry_indices,
367
+ grad_real=grad_real,
368
+ grad_imag=grad_imag,
369
+ cos_pack=cos_pack,
370
+ sin_pack=sin_pack,
371
+ )
372
+ )
373
+
374
+ point_batches = []
375
+ for pair in point_pairs:
376
+ entry_count = len(pair.global_indices)
377
+ real_grad = _reshape_entry_major_grad(
378
+ runtime._safe_grad(leaf_grads[pair.real_index], leaves[pair.real_index]),
379
+ entry_count,
380
+ )
381
+ imag_grad = _reshape_entry_major_grad(
382
+ runtime._safe_grad(leaf_grads[pair.imag_index], leaves[pair.imag_index]),
383
+ entry_count,
384
+ )
385
+ entry_indices = torch.tensor(pair.global_indices, device=real_grad.device, dtype=torch.long)
386
+ cos_pack, sin_pack = _schedule_pack_for(observer_schedule, entry_indices)
387
+ point_i = pair.point_i.to(device=real_grad.device, dtype=torch.long)
388
+ point_j = pair.point_j.to(device=real_grad.device, dtype=torch.long)
389
+ point_k = pair.point_k.to(device=real_grad.device, dtype=torch.long)
390
+ point_batches.append(
391
+ _PointSeedBatch(
392
+ state_field_name=pair.state_field_name,
393
+ entry_indices=entry_indices,
394
+ point_i=point_i,
395
+ point_j=point_j,
396
+ point_k=point_k,
397
+ grad_real=real_grad,
398
+ grad_imag=imag_grad,
399
+ cos_pack=cos_pack,
400
+ sin_pack=sin_pack,
401
+ point_i32=point_i.to(dtype=torch.int32),
402
+ point_j32=point_j.to(dtype=torch.int32),
403
+ point_k32=point_k.to(dtype=torch.int32),
404
+ )
405
+ )
406
+
407
+ plane_batches = []
408
+ for pair in plane_pairs:
409
+ entry_count = len(pair.global_indices)
410
+ real_grad = _reshape_entry_major_grad(
411
+ runtime._safe_grad(leaf_grads[pair.real_index], leaves[pair.real_index]),
412
+ entry_count,
413
+ )
414
+ imag_grad = _reshape_entry_major_grad(
415
+ runtime._safe_grad(leaf_grads[pair.imag_index], leaves[pair.imag_index]),
416
+ entry_count,
417
+ )
418
+ entry_indices = torch.tensor(pair.global_indices, device=real_grad.device, dtype=torch.long)
419
+ cos_pack, sin_pack = _schedule_pack_for(observer_schedule, entry_indices)
420
+ plane_batches.append(
421
+ _PlaneSeedBatch(
422
+ state_field_name=pair.state_field_name,
423
+ axis_code=pair.axis_code,
424
+ plane_index=pair.plane_index,
425
+ entry_indices=entry_indices,
426
+ grad_real=real_grad,
427
+ grad_imag=imag_grad,
428
+ cos_pack=cos_pack,
429
+ sin_pack=sin_pack,
430
+ )
431
+ )
432
+
433
+ return _SeedRuntime(
434
+ dft_schedule=dft_schedule,
435
+ observer_schedule=observer_schedule,
436
+ dense_batches=tuple(dense_batches),
437
+ point_batches=tuple(point_batches),
438
+ plane_batches=tuple(plane_batches),
439
+ )
440
+
441
+
442
+ def _schedule_pack_for(
443
+ schedule: _ScheduleTensorPack, entry_indices: torch.Tensor
444
+ ) -> tuple[torch.Tensor, torch.Tensor]:
445
+ """Gather the per-entry schedule rows ``(E, T)`` for a seed batch once.
446
+
447
+ The native seed-injection kernels read column ``step`` of these packs per
448
+ reverse step, so the (small) per-entry ``index_select`` runs once at seed
449
+ build instead of every step. Degenerate empty schedules collapse to an
450
+ ``(E, 0)`` pack so the native apply skips the batch (zero contribution),
451
+ matching the Torch reference's empty-weight guard.
452
+ """
453
+ if entry_indices.numel() == 0 or schedule.cos.numel() == 0:
454
+ empty = torch.zeros((int(entry_indices.numel()), 0), device=entry_indices.device, dtype=schedule.cos.dtype)
455
+ return empty, empty
456
+ cos = schedule.cos.index_select(0, entry_indices).contiguous()
457
+ sin = schedule.sin.index_select(0, entry_indices).contiguous()
458
+ return cos, sin
459
+
460
+
461
+ def _apply_dense_seeds_native(_cuda_backend, adj_state, seed_runtime: _SeedRuntime, step_index):
462
+ for batch in seed_runtime.dense_batches:
463
+ if batch.cos_pack.shape[1] == 0:
464
+ continue
465
+ _cuda_backend._seed_inject_dense(
466
+ AdjField=adj_state[batch.state_field_name],
467
+ GradReal=batch.grad_real,
468
+ GradImag=batch.grad_imag,
469
+ CosPack=batch.cos_pack,
470
+ SinPack=batch.sin_pack,
471
+ step=step_index,
472
+ )
473
+
474
+
475
+ def _apply_point_seeds_native(_cuda_backend, adj_state, seed_runtime: _SeedRuntime, step_index):
476
+ for batch in seed_runtime.point_batches:
477
+ if batch.cos_pack.shape[1] == 0:
478
+ continue
479
+ _cuda_backend._seed_inject_point(
480
+ AdjField=adj_state[batch.state_field_name],
481
+ GradReal=batch.grad_real,
482
+ GradImag=batch.grad_imag,
483
+ PointI=batch.point_i32,
484
+ PointJ=batch.point_j32,
485
+ PointK=batch.point_k32,
486
+ CosPack=batch.cos_pack,
487
+ SinPack=batch.sin_pack,
488
+ step=step_index,
489
+ )
490
+
491
+
492
+ def _apply_plane_seeds_native(_cuda_backend, adj_state, seed_runtime: _SeedRuntime, step_index):
493
+ for batch in seed_runtime.plane_batches:
494
+ if batch.cos_pack.shape[1] == 0:
495
+ continue
496
+ _cuda_backend._seed_inject_plane(
497
+ AdjField=adj_state[batch.state_field_name],
498
+ GradReal=batch.grad_real,
499
+ GradImag=batch.grad_imag,
500
+ CosPack=batch.cos_pack,
501
+ SinPack=batch.sin_pack,
502
+ axis=batch.axis_code,
503
+ planeIndex=batch.plane_index,
504
+ step=step_index,
505
+ )
506
+
507
+
508
+ def _seed_runtime_native_backend(adj_state):
509
+ """Require the CUDA module used by adjoint seed-injection kernels."""
510
+ reference = next(iter(adj_state.values()), None)
511
+ if reference is None or not reference.is_cuda:
512
+ raise RuntimeError("FDTD adjoint seed injection requires CUDA tensors.")
513
+ from ..cuda import backend as _cuda_backend
514
+
515
+ if not _cuda_backend.is_available():
516
+ raise RuntimeError("FDTD adjoint seed injection requires the native CUDA extension.")
517
+ return _cuda_backend
518
+
519
+
520
+ def _apply_seed_runtime(adj_state, seed_runtime: _SeedRuntime, step_index):
521
+ cuda_backend = _seed_runtime_native_backend(adj_state)
522
+ _apply_dense_seeds_native(cuda_backend, adj_state, seed_runtime, step_index)
523
+ _apply_point_seeds_native(cuda_backend, adj_state, seed_runtime, step_index)
524
+ _apply_plane_seeds_native(cuda_backend, adj_state, seed_runtime, step_index)
@@ -0,0 +1,37 @@
1
+ from .common import (
2
+ BOUNDARY_BLOCH,
3
+ BOUNDARY_KIND_TO_CODE,
4
+ BOUNDARY_NONE,
5
+ BOUNDARY_PEC,
6
+ BOUNDARY_PERIODIC,
7
+ BOUNDARY_PMC,
8
+ BOUNDARY_PML,
9
+ combine_complex_spectral_components,
10
+ has_complex_fields,
11
+ )
12
+ from .cpml import (
13
+ DEFAULT_CPML_CONFIG,
14
+ expand_cpml_memory_tensor,
15
+ initialize_cpml_state,
16
+ initialize_neutral_boundary_state,
17
+ initialize_simple_pml_state,
18
+ )
19
+ from .runtime import initialize_boundary_state
20
+
21
+ __all__ = [
22
+ "BOUNDARY_BLOCH",
23
+ "BOUNDARY_KIND_TO_CODE",
24
+ "BOUNDARY_NONE",
25
+ "BOUNDARY_PEC",
26
+ "BOUNDARY_PERIODIC",
27
+ "BOUNDARY_PMC",
28
+ "BOUNDARY_PML",
29
+ "DEFAULT_CPML_CONFIG",
30
+ "combine_complex_spectral_components",
31
+ "expand_cpml_memory_tensor",
32
+ "has_complex_fields",
33
+ "initialize_boundary_state",
34
+ "initialize_cpml_state",
35
+ "initialize_neutral_boundary_state",
36
+ "initialize_simple_pml_state",
37
+ ]
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ from ..dispersion import solve_numerical_wavenumber
6
+
7
+ _AXES = ("x", "y", "z")
8
+
9
+
10
+ def _domain_lengths(solver) -> tuple[float, float, float]:
11
+ domain_range = solver.scene.domain_range
12
+ return (
13
+ float(domain_range[1] - domain_range[0]),
14
+ float(domain_range[3] - domain_range[2]),
15
+ float(domain_range[5] - domain_range[4]),
16
+ )
17
+
18
+
19
+ def _bloch_axes(solver) -> tuple[str, ...]:
20
+ return tuple(axis for axis in _AXES if solver.scene.boundary.axis_kind(axis) == "bloch")
21
+
22
+
23
+ def _tfsf_slab_sources(solver) -> list[dict]:
24
+ return [
25
+ source
26
+ for source in getattr(solver, "_compiled_sources", ()) or ()
27
+ if source.get("injection", {}).get("kind") == "tfsf"
28
+ and source.get("injection", {}).get("mode") == "slab"
29
+ ]
30
+
31
+
32
+ def validate_grating_tfsf_slab_topology(solver) -> dict:
33
+ sources = _tfsf_slab_sources(solver)
34
+ if len(sources) != 1:
35
+ raise ValueError("Mixed Bloch/PML TFSF slab support requires exactly one TFSF slab source.")
36
+ source = sources[0]
37
+ if source.get("kind") != "plane_wave":
38
+ raise ValueError("Mixed Bloch/PML TFSF slab support requires a PlaneWave source.")
39
+
40
+ injection = source["injection"]
41
+ normal_axis = injection["axis"]
42
+ boundary = solver.scene.boundary
43
+ normal_faces = boundary.axis_face_kinds(normal_axis)
44
+ if boundary.axis_kind(normal_axis) == "bloch" or normal_faces != ("pml", "pml"):
45
+ raise ValueError("Mixed Bloch/PML TFSF slab support requires the TFSF slab normal axis to use PML.")
46
+
47
+ expected_bloch_axes = tuple(axis for axis in _AXES if axis != normal_axis)
48
+ if _bloch_axes(solver) != expected_bloch_axes:
49
+ raise ValueError("Mixed Bloch/PML TFSF slab support requires Bloch axes transverse to the TFSF slab.")
50
+ return source
51
+
52
+
53
+ def _require_auto_bloch_source(solver) -> dict:
54
+ source = validate_grating_tfsf_slab_topology(solver)
55
+ if source["source_time"]["kind"] != "cw":
56
+ raise ValueError("Automatic Bloch wavevector resolution requires a CW PlaneWave source_time.")
57
+ return source
58
+
59
+
60
+ def resolve_bloch_wavevector(solver) -> tuple[float, float, float]:
61
+ boundary = solver.scene.boundary
62
+ wavevector = boundary.bloch_wavevector
63
+ if wavevector != "auto":
64
+ return tuple(float(component) for component in wavevector)
65
+
66
+ source = _require_auto_bloch_source(solver)
67
+ k_numeric = solve_numerical_wavenumber(
68
+ solver,
69
+ source["direction"],
70
+ {"x": solver.min_dx, "y": solver.min_dy, "z": solver.min_dz},
71
+ )
72
+ bloch_axes = set(_bloch_axes(solver))
73
+ return tuple(
74
+ float(k_numeric * float(component)) if axis in bloch_axes else 0.0
75
+ for axis, component in zip(_AXES, source["direction"])
76
+ )
77
+
78
+
79
+ def resolve_bloch_phase_factors(solver) -> tuple[complex, complex, complex]:
80
+ wavevector = getattr(solver, "resolved_bloch_wavevector", None)
81
+ if wavevector is None:
82
+ wavevector = resolve_bloch_wavevector(solver)
83
+ phases = []
84
+ for axis, wave_number, length in zip(_AXES, wavevector, _domain_lengths(solver)):
85
+ if solver.scene.boundary.axis_kind(axis) == "bloch":
86
+ angle = float(wave_number) * float(length)
87
+ phases.append(complex(math.cos(angle), math.sin(angle)))
88
+ else:
89
+ phases.append(1.0 + 0.0j)
90
+ return tuple(phases)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+
5
+ BOUNDARY_NONE = 0
6
+ BOUNDARY_PML = 1
7
+ BOUNDARY_PERIODIC = 2
8
+ BOUNDARY_BLOCH = 3
9
+ BOUNDARY_PEC = 4
10
+ BOUNDARY_PMC = 5
11
+
12
+ BOUNDARY_KIND_TO_CODE = {
13
+ "none": BOUNDARY_NONE,
14
+ "pml": BOUNDARY_PML,
15
+ "periodic": BOUNDARY_PERIODIC,
16
+ "bloch": BOUNDARY_BLOCH,
17
+ "pec": BOUNDARY_PEC,
18
+ "pmc": BOUNDARY_PMC,
19
+ # Mur ABC is applied in PyTorch after the E-update; the update kernel treats
20
+ # these faces as inert so no dedicated CUDA boundary code is required.
21
+ "mur": BOUNDARY_NONE,
22
+ }
23
+
24
+
25
+ def has_complex_fields(solver) -> bool:
26
+ return bool(getattr(solver, "complex_fields_enabled", False))
27
+
28
+
29
+ def combine_complex_spectral_components(real_field_real, real_field_imag, imag_field_real=None, imag_field_imag=None):
30
+ if imag_field_real is None or imag_field_imag is None:
31
+ return real_field_real + 1j * real_field_imag
32
+ return (real_field_real - imag_field_imag) + 1j * (real_field_imag + imag_field_real)
33
+
34
+
35
+ def initialize_complex_fields(solver):
36
+ solver.Ex_imag = torch.zeros_like(solver.Ex)
37
+ solver.Ey_imag = torch.zeros_like(solver.Ey)
38
+ solver.Ez_imag = torch.zeros_like(solver.Ez)
39
+ solver.Hx_imag = torch.zeros_like(solver.Hx)
40
+ solver.Hy_imag = torch.zeros_like(solver.Hy)
41
+ solver.Hz_imag = torch.zeros_like(solver.Hz)