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,684 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from enum import Enum, auto
5
+
6
+
7
+ from ..boundary import (
8
+ BOUNDARY_BLOCH,
9
+ BOUNDARY_NONE,
10
+ BOUNDARY_PERIODIC,
11
+ BOUNDARY_PMC,
12
+ has_complex_fields,
13
+ )
14
+ from ..checkpoint import checkpoint_schema
15
+
16
+
17
+ class _ReverseBackend(Enum):
18
+ GENERAL_NONLINEAR = auto()
19
+ MIXED_BLOCH_CPML = auto()
20
+ GRATING_TFSF = auto()
21
+ TFSF = auto()
22
+ BLOCH_DISPERSIVE = auto()
23
+ BLOCH = auto()
24
+ DISPERSIVE = auto()
25
+ STANDARD = auto()
26
+ CPML = auto()
27
+ CONDUCTIVE = auto()
28
+ KERR = auto()
29
+ FULL_ANISO = auto()
30
+
31
+
32
+ # Native CUDA reverse-execution backends mirror the native CUDA
33
+ # variants one-for-one. Each label is the value recorded into
34
+ # ``_BackwardProfiler.reverse_backend_counts`` so a native reverse step stays
35
+ # attributable per configuration (``native_standard`` mirrors
36
+ # ``native_standard`` and so on). The runner registry is populated by
37
+ # later P6 items as the fused CUDA reverse kernels land; until an entry exists
38
+ # ``auto`` mode never selects native and an explicit ``native`` override raises.
39
+ _NATIVE_REVERSE_LABELS: dict[_ReverseBackend, str] = {
40
+ _ReverseBackend.GENERAL_NONLINEAR: "native_general_nonlinear",
41
+ _ReverseBackend.MIXED_BLOCH_CPML: "native_mixed_bloch_cpml",
42
+ _ReverseBackend.STANDARD: "native_standard",
43
+ _ReverseBackend.CPML: "native_cpml",
44
+ _ReverseBackend.CONDUCTIVE: "native_conductive",
45
+ _ReverseBackend.KERR: "native_kerr",
46
+ _ReverseBackend.FULL_ANISO: "native_full_aniso",
47
+ _ReverseBackend.BLOCH: "native_bloch",
48
+ _ReverseBackend.BLOCH_DISPERSIVE: "native_bloch_dispersive",
49
+ _ReverseBackend.DISPERSIVE: "native_dispersive",
50
+ _ReverseBackend.TFSF: "native_tfsf",
51
+ _ReverseBackend.GRATING_TFSF: "native_grating_tfsf",
52
+ }
53
+
54
+ # A registered runner takes the same (solver, forward_state, adjoint_state,
55
+ # time_value, eps_*, resolved_source_terms, profiler) contract as the analytic
56
+ # reference backends and returns a fully-formed ``_ReverseStepResult`` whose
57
+ # ``backend`` field is the matching ``_NATIVE_REVERSE_LABELS`` value.
58
+ _NativeReverseRunner = Callable[..., object]
59
+ _NATIVE_REVERSE_RUNNERS: dict[_ReverseBackend, _NativeReverseRunner] = {}
60
+
61
+ # A runner may carry a ``(solver, forward_state) -> bool`` qualifier stashed on
62
+ # the callable itself (``_native_reverse_qualifier``). The real native runners
63
+ # require a CUDA scene with the compiled extension present, so on CPU or a
64
+ # missing-extension host ``auto`` mode falls back to the native implementation and a
65
+ # forced ``native`` override raises. The qualifier travels with the runner so a
66
+ # test that swaps in its own runner (no qualifier attribute) qualifies anywhere.
67
+ _NATIVE_QUALIFIER_ATTR = "_native_reverse_qualifier"
68
+
69
+
70
+ def register_native_reverse_backend(
71
+ backend: _ReverseBackend,
72
+ runner: _NativeReverseRunner,
73
+ *,
74
+ qualifier: Callable[..., bool] | None = None,
75
+ ) -> None:
76
+ """Register a native CUDA reverse-step runner for an analytic backend variant."""
77
+ if backend not in _NATIVE_REVERSE_LABELS:
78
+ raise ValueError(f"No native reverse label is defined for backend {backend!r}.")
79
+ if qualifier is not None:
80
+ setattr(runner, _NATIVE_QUALIFIER_ATTR, qualifier)
81
+ _NATIVE_REVERSE_RUNNERS[backend] = runner
82
+
83
+
84
+ def unregister_native_reverse_backend(backend: _ReverseBackend) -> None:
85
+ """Drop a previously registered native runner (primarily for tests)."""
86
+ _NATIVE_REVERSE_RUNNERS.pop(backend, None)
87
+
88
+
89
+ def _native_backend_available(backend: _ReverseBackend, solver=None, forward_state=None) -> bool:
90
+ runner = _NATIVE_REVERSE_RUNNERS.get(backend)
91
+ if runner is None:
92
+ return False
93
+ qualifier = getattr(runner, _NATIVE_QUALIFIER_ATTR, None)
94
+ if qualifier is None:
95
+ return True
96
+ if solver is None:
97
+ # No scene context to evaluate against: report registry membership only.
98
+ return True
99
+ return bool(qualifier(solver, forward_state))
100
+
101
+
102
+ def _runtime():
103
+ from . import core as _adjoint
104
+
105
+ return _adjoint
106
+
107
+
108
+ def _face_codes(solver) -> tuple[int, int, int, int, int, int]:
109
+ return (
110
+ int(getattr(solver, "boundary_x_low_code", BOUNDARY_NONE)),
111
+ int(getattr(solver, "boundary_x_high_code", BOUNDARY_NONE)),
112
+ int(getattr(solver, "boundary_y_low_code", BOUNDARY_NONE)),
113
+ int(getattr(solver, "boundary_y_high_code", BOUNDARY_NONE)),
114
+ int(getattr(solver, "boundary_z_low_code", BOUNDARY_NONE)),
115
+ int(getattr(solver, "boundary_z_high_code", BOUNDARY_NONE)),
116
+ )
117
+
118
+
119
+ def _matches_checkpoint_layout(solver, forward_state) -> bool:
120
+ return tuple(forward_state.keys()) == checkpoint_schema(solver).state_names
121
+
122
+
123
+ def _has_open_face_conflicts(face_codes: tuple[int, int, int, int, int, int]) -> bool:
124
+ return any(code in {BOUNDARY_PERIODIC, BOUNDARY_PMC, BOUNDARY_BLOCH} for code in face_codes)
125
+
126
+
127
+ def _supports_explicit_source_step(runtime, solver, resolved_source_terms) -> bool:
128
+ return runtime._can_use_explicit_source_term_reverse_step(solver, resolved_source_terms)
129
+
130
+
131
+ def _supports_tfsf(runtime, solver, forward_state, resolved_source_terms) -> bool:
132
+ if getattr(solver, "nonlinear_enabled", False):
133
+ return False
134
+ if getattr(solver, "conductive_enabled", False):
135
+ return False
136
+ if getattr(solver, "full_aniso_enabled", False):
137
+ return False
138
+ if not getattr(solver, "tfsf_enabled", False):
139
+ return False
140
+ if has_complex_fields(solver):
141
+ return False
142
+ if getattr(solver, "dispersive_enabled", False):
143
+ return False
144
+ if not _matches_checkpoint_layout(solver, forward_state):
145
+ return False
146
+ if _has_open_face_conflicts(_face_codes(solver)):
147
+ return False
148
+ if runtime._has_resolved_source_terms(resolved_source_terms):
149
+ return False
150
+
151
+ tfsf_state = getattr(solver, "_tfsf_state", None)
152
+ if tfsf_state is None:
153
+ return False
154
+ provider = tfsf_state.get("provider")
155
+ if provider not in {
156
+ "plane_wave_ref_x_ez",
157
+ "plane_wave_axis_aligned",
158
+ "plane_wave_aux",
159
+ "plane_wave_discrete_cw",
160
+ "analytic_profile",
161
+ }:
162
+ return False
163
+
164
+ auxiliary_grid = tfsf_state.get("auxiliary_grid")
165
+ has_auxiliary_state = "tfsf_aux_electric" in forward_state and "tfsf_aux_magnetic" in forward_state
166
+ if auxiliary_grid is None:
167
+ return not has_auxiliary_state
168
+ return has_auxiliary_state
169
+
170
+
171
+ def _supports_grating_tfsf(runtime, solver, forward_state, resolved_source_terms) -> bool:
172
+ if getattr(solver, "nonlinear_enabled", False):
173
+ return False
174
+ if getattr(solver, "conductive_enabled", False):
175
+ return False
176
+ if getattr(solver, "full_aniso_enabled", False):
177
+ return False
178
+ if not getattr(solver, "tfsf_enabled", False):
179
+ return False
180
+ if not has_complex_fields(solver):
181
+ return False
182
+ if not getattr(solver, "uses_cpml", False):
183
+ return False
184
+ if getattr(solver, "dispersive_enabled", False):
185
+ return False
186
+ if runtime._has_resolved_source_terms(resolved_source_terms):
187
+ return False
188
+ if not _matches_checkpoint_layout(solver, forward_state):
189
+ return False
190
+
191
+ # A grating slab is a single absorbing (PML) normal axis with the two
192
+ # transverse axes periodic (Bloch), for any choice of normal axis. The reverse
193
+ # split-field replay carries the wrap phase on the two Bloch axes and the
194
+ # recursive-convolution stretch on the single PML axis; ``_bloch_cpml_pml_axis``
195
+ # recognizes exactly that layout and returns the absorbing axis.
196
+ from ..runtime.stepping import _bloch_cpml_pml_axis
197
+
198
+ pml_axis = _bloch_cpml_pml_axis(solver)
199
+ if pml_axis is None:
200
+ return False
201
+
202
+ tfsf_state = getattr(solver, "_tfsf_state", None)
203
+ if tfsf_state is None:
204
+ return False
205
+ return (
206
+ tfsf_state.get("provider") == "plane_wave_grating_slab_cw"
207
+ and tfsf_state.get("mode") == "slab"
208
+ and tfsf_state.get("axis") == pml_axis
209
+ and "tfsf_aux_electric" not in forward_state
210
+ and "tfsf_aux_magnetic" not in forward_state
211
+ )
212
+
213
+
214
+ def _supports_mixed_bloch_cpml(runtime, solver, forward_state, resolved_source_terms) -> bool:
215
+ if not has_complex_fields(solver) or not getattr(solver, "uses_cpml", False):
216
+ return False
217
+ if getattr(solver, "tfsf_enabled", False):
218
+ return False
219
+ if getattr(solver, "nonlinear_enabled", False) or getattr(solver, "conductive_enabled", False):
220
+ return False
221
+ if getattr(solver, "full_aniso_enabled", False):
222
+ return False
223
+ if not _matches_checkpoint_layout(solver, forward_state):
224
+ return False
225
+ from ..runtime.stepping import _bloch_cpml_pml_axis
226
+
227
+ return (
228
+ _bloch_cpml_pml_axis(solver) is not None
229
+ and _supports_explicit_source_step(runtime, solver, resolved_source_terms)
230
+ )
231
+
232
+
233
+ def _supports_standard(runtime, solver, forward_state, resolved_source_terms) -> bool:
234
+ if getattr(solver, "nonlinear_enabled", False):
235
+ return False
236
+ if getattr(solver, "conductive_enabled", False):
237
+ return False
238
+ if getattr(solver, "full_aniso_enabled", False):
239
+ return False
240
+ if tuple(forward_state.keys()) != ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz"):
241
+ return False
242
+ if getattr(solver, "uses_cpml", False):
243
+ return False
244
+ if has_complex_fields(solver):
245
+ return False
246
+ if getattr(solver, "dispersive_enabled", False):
247
+ return False
248
+ if getattr(solver, "tfsf_enabled", False):
249
+ return False
250
+ if _has_open_face_conflicts(_face_codes(solver)):
251
+ return False
252
+ return _supports_explicit_source_step(runtime, solver, resolved_source_terms)
253
+
254
+
255
+ def _with_profile_sections(profiler, fn):
256
+ if profiler is None:
257
+ return fn()
258
+ with profiler.section("state_clone"):
259
+ pass
260
+ with profiler.section("step_forward"):
261
+ result = fn()
262
+ with profiler.section("step_vjp"):
263
+ pass
264
+ return result
265
+
266
+
267
+ def _supports_cpml(runtime, solver, forward_state, resolved_source_terms) -> bool:
268
+ if getattr(solver, "nonlinear_enabled", False):
269
+ return False
270
+ if getattr(solver, "conductive_enabled", False):
271
+ return False
272
+ if getattr(solver, "full_aniso_enabled", False):
273
+ return False
274
+ if not getattr(solver, "uses_cpml", False):
275
+ return False
276
+ if not _matches_checkpoint_layout(solver, forward_state):
277
+ return False
278
+ if has_complex_fields(solver):
279
+ return False
280
+ if getattr(solver, "dispersive_enabled", False):
281
+ return False
282
+ if getattr(solver, "tfsf_enabled", False):
283
+ return False
284
+ if _has_open_face_conflicts(_face_codes(solver)):
285
+ return False
286
+ return _supports_explicit_source_step(runtime, solver, resolved_source_terms)
287
+
288
+
289
+ def _supports_dispersive(runtime, solver, forward_state, resolved_source_terms) -> bool:
290
+ if getattr(solver, "nonlinear_enabled", False):
291
+ return False
292
+ if getattr(solver, "conductive_enabled", False):
293
+ return False
294
+ if getattr(solver, "full_aniso_enabled", False):
295
+ return False
296
+ if not getattr(solver, "dispersive_enabled", False):
297
+ return False
298
+ if has_complex_fields(solver):
299
+ return False
300
+ if not _matches_checkpoint_layout(solver, forward_state):
301
+ return False
302
+ if getattr(solver, "tfsf_enabled", False):
303
+ return False
304
+ if _has_open_face_conflicts(_face_codes(solver)):
305
+ return False
306
+ return _supports_explicit_source_step(runtime, solver, resolved_source_terms)
307
+
308
+
309
+ def _supports_bloch(runtime, solver, forward_state, resolved_source_terms) -> bool:
310
+ if getattr(solver, "nonlinear_enabled", False):
311
+ return False
312
+ if getattr(solver, "conductive_enabled", False):
313
+ return False
314
+ if getattr(solver, "full_aniso_enabled", False):
315
+ return False
316
+ if getattr(solver, "uses_cpml", False):
317
+ return False
318
+ if not has_complex_fields(solver):
319
+ return False
320
+ if not _matches_checkpoint_layout(solver, forward_state):
321
+ return False
322
+ if getattr(solver, "dispersive_enabled", False):
323
+ return False
324
+ if getattr(solver, "tfsf_enabled", False):
325
+ return False
326
+ if not all(code == BOUNDARY_BLOCH for code in _face_codes(solver)):
327
+ return False
328
+ return _supports_explicit_source_step(runtime, solver, resolved_source_terms)
329
+
330
+
331
+ def _supports_bloch_dispersive(runtime, solver, forward_state, resolved_source_terms) -> bool:
332
+ """Analytic native reverse for a Bloch (complex-field) + electric-dispersive medium.
333
+
334
+ The complex-field solver advances a real and an imaginary FDTD copy that couple
335
+ only through the Bloch boundary wrap, and the electric ADE poles advance on each
336
+ copy with the same real coefficients. The reverse is the complex Bloch base
337
+ reverse plus the electric-dispersive correction / ADE-state VJP applied to both
338
+ halves, so this variant carries the imaginary-ADE replica the plain Bloch and
339
+ plain dispersive backends each drop. Gated to the pure Bloch (all-faces-Bloch,
340
+ non-CPML) electric-dispersive class; combined with an absorbing (CPML) axis,
341
+ conduction, nonlinearity, full anisotropy, or TFSF combinations are handled
342
+ by another native specialization or rejected during preparation."""
343
+ if not has_complex_fields(solver):
344
+ return False
345
+ if not getattr(solver, "dispersive_enabled", False):
346
+ return False
347
+ if getattr(solver, "nonlinear_enabled", False):
348
+ return False
349
+ if getattr(solver, "conductive_enabled", False):
350
+ return False
351
+ if getattr(solver, "full_aniso_enabled", False):
352
+ return False
353
+ if getattr(solver, "uses_cpml", False):
354
+ return False
355
+ if getattr(solver, "tfsf_enabled", False):
356
+ return False
357
+ if not _matches_checkpoint_layout(solver, forward_state):
358
+ return False
359
+ if not all(code == BOUNDARY_BLOCH for code in _face_codes(solver)):
360
+ return False
361
+ return _supports_explicit_source_step(runtime, solver, resolved_source_terms)
362
+
363
+
364
+ def _supports_conductive(runtime, solver, forward_state, resolved_source_terms) -> bool:
365
+ """Analytic native reverse for a static-conductive (sigma_e) CPML medium.
366
+
367
+ The semi-implicit conduction loss makes the electric ``decay`` and ``curl``
368
+ coefficients eps-dependent, so this variant carries the extra eps sensitivity
369
+ the linear CPML reverse drops. Gated to the pure real, single-material
370
+ conductive class on an absorbing (CPML) grid; conduction combined with an
371
+ open boundary, ADE dispersion, nonlinearity, full anisotropy, complex fields,
372
+ or TFSF is rejected during preparation when no native specialization exists."""
373
+ if not getattr(solver, "conductive_enabled", False):
374
+ return False
375
+ if getattr(solver, "nonlinear_enabled", False):
376
+ return False
377
+ if getattr(solver, "full_aniso_enabled", False):
378
+ return False
379
+ if not getattr(solver, "uses_cpml", False):
380
+ return False
381
+ if getattr(solver, "dispersive_enabled", False):
382
+ return False
383
+ if getattr(solver, "magnetic_dispersive_enabled", False):
384
+ return False
385
+ if has_complex_fields(solver):
386
+ return False
387
+ if getattr(solver, "tfsf_enabled", False):
388
+ return False
389
+ if not _matches_checkpoint_layout(solver, forward_state):
390
+ return False
391
+ if _has_open_face_conflicts(_face_codes(solver)):
392
+ return False
393
+ return _supports_explicit_source_step(runtime, solver, resolved_source_terms)
394
+
395
+
396
+ def _supports_kerr(runtime, solver, forward_state, resolved_source_terms) -> bool:
397
+ """Analytic native reverse for an instantaneous Kerr (chi3) CPML medium.
398
+
399
+ The Kerr forward recomputes only the electric ``curl`` coefficient each step
400
+ from the pre-update fields (``curl = (dt / eff) * decay`` with
401
+ ``eff = eps + eps0 * chi3 * |E|^2``), so the reverse carries the extra field /
402
+ chi3 sensitivity the linear CPML reverse drops. Gated to the pure real,
403
+ curl-only Kerr class on an absorbing (CPML) grid; the general nonlinear kernel
404
+ (chi2 / two-photon absorption, which also rewrites ``decay``) uses the general
405
+ nonlinear native variant. Unsupported combinations fail during preparation."""
406
+ if not getattr(solver, "kerr_enabled", False):
407
+ return False
408
+ if getattr(solver, "nonlinear_general_enabled", False):
409
+ return False
410
+ if getattr(solver, "conductive_enabled", False):
411
+ return False
412
+ if getattr(solver, "full_aniso_enabled", False):
413
+ return False
414
+ if not getattr(solver, "uses_cpml", False):
415
+ return False
416
+ if getattr(solver, "dispersive_enabled", False):
417
+ return False
418
+ if getattr(solver, "magnetic_dispersive_enabled", False):
419
+ return False
420
+ if has_complex_fields(solver):
421
+ return False
422
+ if getattr(solver, "tfsf_enabled", False):
423
+ return False
424
+ if not _matches_checkpoint_layout(solver, forward_state):
425
+ return False
426
+ if _has_open_face_conflicts(_face_codes(solver)):
427
+ return False
428
+ return _supports_explicit_source_step(runtime, solver, resolved_source_terms)
429
+
430
+
431
+ def _supports_general_nonlinear(runtime, solver, forward_state, resolved_source_terms) -> bool:
432
+ if not getattr(solver, "nonlinear_general_enabled", False):
433
+ return False
434
+ if has_complex_fields(solver):
435
+ return False
436
+ if getattr(solver, "full_aniso_enabled", False) or getattr(solver, "tfsf_enabled", False):
437
+ return False
438
+ if getattr(solver, "magnetic_dispersive_enabled", False):
439
+ return False
440
+ if not _matches_checkpoint_layout(solver, forward_state):
441
+ return False
442
+ if _has_open_face_conflicts(_face_codes(solver)):
443
+ return False
444
+ return _supports_explicit_source_step(runtime, solver, resolved_source_terms)
445
+
446
+
447
+ def _supports_full_aniso(runtime, solver, forward_state, resolved_source_terms) -> bool:
448
+ """Analytic native reverse for a full (off-diagonal) anisotropic CPML medium.
449
+
450
+ Full anisotropy adds the off-diagonal coupling ``E_i += coeff_ij * <curlH_j>``
451
+ on top of the diagonal CPML electric update. The reverse reuses the linear CPML
452
+ reverse for the diagonal step and folds the transpose of the off-diagonal
453
+ collocated curl(H) into the mid-step H adjoint, so the extra coupling flows the
454
+ cotangent through the field (H) path; the off-diagonal coefficients themselves
455
+ carry no material gradient channel (trainable geometry on a full-anisotropic
456
+ structure is guarded). Gated to the pure real, CPML class with the anisotropic
457
+ structure clear of the absorber (enforced by the bridge ``_full_aniso_cpml_overlap``
458
+ guard, so the un-stretched collocation matches the forward). Full anisotropy on
459
+ an open boundary, or unsupported combinations with conduction, dispersion,
460
+ nonlinearity, complex fields, or TFSF fail during preparation."""
461
+ if not getattr(solver, "full_aniso_enabled", False):
462
+ return False
463
+ if getattr(solver, "nonlinear_enabled", False):
464
+ return False
465
+ if getattr(solver, "conductive_enabled", False):
466
+ return False
467
+ if not getattr(solver, "uses_cpml", False):
468
+ return False
469
+ if getattr(solver, "dispersive_enabled", False):
470
+ return False
471
+ if getattr(solver, "magnetic_dispersive_enabled", False):
472
+ return False
473
+ if has_complex_fields(solver):
474
+ return False
475
+ if getattr(solver, "tfsf_enabled", False):
476
+ return False
477
+ if not _matches_checkpoint_layout(solver, forward_state):
478
+ return False
479
+ if _has_open_face_conflicts(_face_codes(solver)):
480
+ return False
481
+ return _supports_explicit_source_step(runtime, solver, resolved_source_terms)
482
+
483
+
484
+ def _select_reverse_backend(
485
+ solver,
486
+ forward_state,
487
+ *,
488
+ eps_ex,
489
+ eps_ey,
490
+ eps_ez,
491
+ resolved_source_terms,
492
+ ) -> _ReverseBackend:
493
+ runtime = _runtime()
494
+
495
+ supports_tfsf = _supports_tfsf(runtime, solver, forward_state, resolved_source_terms)
496
+ supports_grating_tfsf = _supports_grating_tfsf(runtime, solver, forward_state, resolved_source_terms)
497
+ supports_mixed_bloch_cpml = _supports_mixed_bloch_cpml(
498
+ runtime, solver, forward_state, resolved_source_terms
499
+ )
500
+ supports_cpml = _supports_cpml(runtime, solver, forward_state, resolved_source_terms)
501
+ supports_standard = _supports_standard(runtime, solver, forward_state, resolved_source_terms)
502
+ supports_dispersive = _supports_dispersive(runtime, solver, forward_state, resolved_source_terms)
503
+ supports_bloch = _supports_bloch(runtime, solver, forward_state, resolved_source_terms)
504
+ supports_bloch_dispersive = _supports_bloch_dispersive(runtime, solver, forward_state, resolved_source_terms)
505
+ supports_conductive = _supports_conductive(runtime, solver, forward_state, resolved_source_terms)
506
+ supports_kerr = _supports_kerr(runtime, solver, forward_state, resolved_source_terms)
507
+ supports_general_nonlinear = _supports_general_nonlinear(
508
+ runtime, solver, forward_state, resolved_source_terms
509
+ )
510
+ supports_full_aniso = _supports_full_aniso(runtime, solver, forward_state, resolved_source_terms)
511
+
512
+ decision_table = (
513
+ (_ReverseBackend.GENERAL_NONLINEAR, supports_general_nonlinear),
514
+ (_ReverseBackend.GRATING_TFSF, supports_grating_tfsf),
515
+ (_ReverseBackend.MIXED_BLOCH_CPML, supports_mixed_bloch_cpml),
516
+ (_ReverseBackend.TFSF, supports_tfsf),
517
+ (_ReverseBackend.BLOCH_DISPERSIVE, supports_bloch_dispersive),
518
+ (_ReverseBackend.BLOCH, supports_bloch),
519
+ (_ReverseBackend.DISPERSIVE, supports_dispersive),
520
+ (_ReverseBackend.STANDARD, supports_standard),
521
+ (_ReverseBackend.CPML, supports_cpml),
522
+ (_ReverseBackend.CONDUCTIVE, supports_conductive),
523
+ (_ReverseBackend.KERR, supports_kerr),
524
+ (_ReverseBackend.FULL_ANISO, supports_full_aniso),
525
+ )
526
+ for backend, enabled in decision_table:
527
+ if enabled:
528
+ return backend
529
+ raise RuntimeError(
530
+ "This differentiable FDTD configuration has no native CUDA adjoint variant. "
531
+ "Adjust the scene to a supported native capability combination."
532
+ )
533
+
534
+
535
+ def _accumulate_source_term_gradients(
536
+ runtime,
537
+ step_result,
538
+ *,
539
+ solver,
540
+ adjoint_state,
541
+ time_value,
542
+ eps_ex,
543
+ eps_ey,
544
+ eps_ez,
545
+ resolved_source_terms,
546
+ ):
547
+ return runtime._accumulate_source_term_gradients(
548
+ step_result,
549
+ solver=solver,
550
+ adjoint_state=adjoint_state,
551
+ time_value=time_value,
552
+ eps_ex=eps_ex,
553
+ eps_ey=eps_ey,
554
+ eps_ez=eps_ez,
555
+ resolved_source_terms=resolved_source_terms,
556
+ )
557
+
558
+
559
+ def _run_native_reverse_step(
560
+ backend,
561
+ solver,
562
+ forward_state,
563
+ adjoint_state,
564
+ *,
565
+ time_value,
566
+ eps_ex,
567
+ eps_ey,
568
+ eps_ez,
569
+ resolved_source_terms,
570
+ profiler,
571
+ ):
572
+ """Invoke the registered native CUDA reverse runner for ``backend``.
573
+
574
+ The caller guarantees ``backend`` has a registered runner. The runner owns
575
+ the full reverse-step contract (including any source-term gradient
576
+ accumulation) and returns a ``_ReverseStepResult`` labelled with the matching
577
+ ``_NATIVE_REVERSE_LABELS`` value.
578
+ """
579
+ if getattr(solver, "_fdtd_cuda_extension", None) is None:
580
+ from ..cuda.backend import get_compiled_extension
581
+
582
+ solver._fdtd_cuda_extension = get_compiled_extension()
583
+ runner = _NATIVE_REVERSE_RUNNERS[backend]
584
+ return runner(
585
+ solver,
586
+ forward_state,
587
+ adjoint_state,
588
+ time_value=time_value,
589
+ eps_ex=eps_ex,
590
+ eps_ey=eps_ey,
591
+ eps_ez=eps_ez,
592
+ resolved_source_terms=resolved_source_terms,
593
+ profiler=profiler,
594
+ )
595
+
596
+
597
+ def validate_native_adjoint_preparation(solver) -> _ReverseBackend:
598
+ """Require a complete native CUDA reverse variant before time marching."""
599
+ from ..checkpoint import capture_checkpoint_state
600
+ from .capabilities import NATIVE_ADJOINT_CAPABILITIES
601
+
602
+ state = capture_checkpoint_state(solver, step=0).tensors
603
+ if not _scene_is_cuda_state(state):
604
+ raise RuntimeError("Differentiable FDTD requires a CUDA scene.")
605
+ runtime = _runtime()
606
+ resolved = runtime._resolved_source_term_lists(
607
+ solver, solver.eps_Ex, solver.eps_Ey, solver.eps_Ez
608
+ )
609
+ backend = _select_reverse_backend(
610
+ solver, state, eps_ex=solver.eps_Ex, eps_ey=solver.eps_Ey,
611
+ eps_ez=solver.eps_Ez, resolved_source_terms=resolved,
612
+ )
613
+ if backend.name not in NATIVE_ADJOINT_CAPABILITIES:
614
+ raise RuntimeError(
615
+ f"Native CUDA adjoint variant {backend.name} is missing from the capability contract."
616
+ )
617
+ if not _native_backend_available(backend, solver, state):
618
+ raise RuntimeError(
619
+ "Differentiable FDTD requires the compiled native CUDA adjoint extension; "
620
+ f"variant {backend.name} is unavailable."
621
+ )
622
+ return backend
623
+
624
+
625
+ def _scene_is_cuda_state(state) -> bool:
626
+ reference = state.get("Ex")
627
+ return bool(reference is not None and reference.is_cuda)
628
+
629
+
630
+ def reverse_step(
631
+ solver,
632
+ forward_state,
633
+ adjoint_state,
634
+ *,
635
+ time_value,
636
+ eps_ex,
637
+ eps_ey,
638
+ eps_ez,
639
+ chi3_ex=None,
640
+ chi3_ey=None,
641
+ chi3_ez=None,
642
+ chi2_ex=None,
643
+ chi2_ey=None,
644
+ chi2_ez=None,
645
+ tpa_ex=None,
646
+ tpa_ey=None,
647
+ tpa_ez=None,
648
+ forward_magnetic_fields=None,
649
+ resolved_source_terms=None,
650
+ profiler=None,
651
+ ):
652
+ runtime = _runtime()
653
+
654
+ state_names = tuple(forward_state.keys())
655
+ if tuple(adjoint_state.keys()) != state_names:
656
+ raise RuntimeError(
657
+ "Reverse step expects forward and adjoint states to share the same frozen checkpoint layout."
658
+ )
659
+
660
+ # The FDTD backward bridge resolves the source-term lists once per pass and
661
+ # threads them in; direct callers (parity tests) omit them and resolve here.
662
+ if resolved_source_terms is None:
663
+ resolved_source_terms = runtime._resolved_source_term_lists(solver, eps_ex, eps_ey, eps_ez)
664
+ analytic_backend = _select_reverse_backend(
665
+ solver,
666
+ forward_state,
667
+ eps_ex=eps_ex,
668
+ eps_ey=eps_ey,
669
+ eps_ez=eps_ez,
670
+ resolved_source_terms=resolved_source_terms,
671
+ )
672
+ if not _native_backend_available(analytic_backend, solver, forward_state):
673
+ raise RuntimeError(
674
+ "The prepared differentiable FDTD scene lost its native CUDA adjoint "
675
+ f"variant {analytic_backend.name}; backward cannot continue."
676
+ )
677
+ return _with_profile_sections(
678
+ profiler,
679
+ lambda: _run_native_reverse_step(
680
+ analytic_backend, solver, forward_state, adjoint_state,
681
+ time_value=time_value, eps_ex=eps_ex, eps_ey=eps_ey, eps_ez=eps_ez,
682
+ resolved_source_terms=resolved_source_terms, profiler=profiler,
683
+ ),
684
+ )