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,746 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ import torch
6
+
7
+ from ...adjoint_inputs import (
8
+ material_dependent_inputs as _material_dependent_inputs,
9
+ scene_trainable_material_tensors as _scene_trainable_material_tensors,
10
+ )
11
+ from ...media import Tensor3x3
12
+ from ...result import Result
13
+ from ..boundary import BOUNDARY_BLOCH, BOUNDARY_NONE, BOUNDARY_PEC, BOUNDARY_PML, has_complex_fields
14
+ from .profiler import _BackwardProfiler, _clone_backward_profile, _empty_backward_profile
15
+ from ..excitation import (
16
+ advance_tfsf_auxiliary_electric,
17
+ advance_tfsf_auxiliary_magnetic,
18
+ apply_tfsf_e_correction,
19
+ apply_tfsf_h_correction,
20
+ )
21
+ from ..runtime.stepping import apply_full_aniso_corrections, update_electric_fields_bloch_cpml
22
+ from .seeds import _build_output_seeds, _schedule_to_tensor_pack, _apply_seed_runtime
23
+ from ..checkpoint import capture_checkpoint_state
24
+ from ..material_pullback import pullback_material_input_gradients
25
+ from .dispatch import reverse_step
26
+
27
+
28
+ def _runtime():
29
+ from . import core as _adjoint
30
+
31
+ return _adjoint
32
+
33
+
34
+ def _resolve_grad_accumulator_backend(device):
35
+ """Require native in-place gradient accumulation for CUDA-only adjoints."""
36
+ if torch.device(device).type != "cuda":
37
+ raise RuntimeError("Differentiable FDTD requires CUDA gradient tensors.")
38
+ from ..cuda import backend as _cuda_backend
39
+
40
+ if not _cuda_backend.is_available():
41
+ raise RuntimeError("Differentiable FDTD requires the native CUDA extension.")
42
+ return _cuda_backend
43
+
44
+
45
+ def _accumulate_grad(cuda_backend, accumulator, increment):
46
+ """Fold one gradient contribution into its native CUDA accumulator."""
47
+ if not accumulator.is_contiguous() or not increment.is_contiguous():
48
+ raise RuntimeError("Native FDTD gradient accumulation requires contiguous tensors.")
49
+ cuda_backend._accumulate_in_place(dst=accumulator, src=increment)
50
+ return accumulator
51
+
52
+
53
+ def _material_has_conductivity(material) -> bool:
54
+ sigma_e = getattr(material, "sigma_e", 0.0)
55
+ if sigma_e is not None and float(sigma_e) != 0.0:
56
+ return True
57
+ sigma_e_tensor = getattr(material, "sigma_e_tensor", None)
58
+ if sigma_e_tensor is not None:
59
+ return any(float(component) != 0.0 for component in sigma_e_tensor.as_tuple())
60
+ return False
61
+
62
+
63
+ def _structure_has_trainable_geometry(structure) -> bool:
64
+ geometry = getattr(structure, "geometry", None)
65
+ if geometry is None:
66
+ return False
67
+ return any(
68
+ isinstance(value, torch.Tensor) and value.requires_grad
69
+ for value in vars(geometry).values()
70
+ )
71
+
72
+
73
+ def _source_spectrum_key(source_time):
74
+ if source_time is None:
75
+ return None
76
+ if source_time.get("times") is not None:
77
+ # Sampled custom waveforms are only spectrum-equivalent when they share
78
+ # the same table object; identity keeps the comparison conservative.
79
+ return ("custom", id(source_time))
80
+ fields = ("kind_code", "frequency", "amplitude", "phase", "delay", "fwidth")
81
+ return tuple(
82
+ None if source_time.get(field) is None else float(source_time.get(field))
83
+ for field in fields
84
+ )
85
+
86
+
87
+ def _sources_share_spectrum(compiled_sources) -> bool:
88
+ """Whether every compiled source carries the same source-time spectrum.
89
+
90
+ Source normalization divides monitor spectra by one incident spectrum, which
91
+ is only well-defined when all sources share it (a single source, or several
92
+ driven by an identical waveform).
93
+ """
94
+ keys = {_source_spectrum_key(source.get("source_time")) for source in (compiled_sources or ())}
95
+ return len(keys) <= 1
96
+
97
+
98
+ def _unsupported_adjoint_medium(scene):
99
+ for structure in getattr(scene, "structures", ()):
100
+ material = getattr(structure, "material", None)
101
+ if material is None:
102
+ continue
103
+ if getattr(material, "is_medium2d", False):
104
+ return "FDTD adjoint does not support 2D sheet (Medium2D) media yet."
105
+ if getattr(material, "is_lossy_metal", False):
106
+ # The surface-impedance (Leontovich) boundary evolves per-face recursive
107
+ # H state and overrides the tangential surface E outside the differentiable
108
+ # material-tensor replay, so it carries no reverse gradient channel; a design
109
+ # differentiated through a lossy-metal SIBC surface would drop its sensitivity.
110
+ return (
111
+ "FDTD adjoint does not support LossyMetalMedium (surface-impedance boundary) media: "
112
+ "the Leontovich surface update runs outside the differentiable material replay and "
113
+ "carries no reverse gradient channel."
114
+ )
115
+ if _material_has_conductivity(material) and getattr(material, "is_nonlinear", False):
116
+ # Static conduction (sigma_e) is differentiable on its own through the
117
+ # semi-implicit lossy-coefficient replay, but the instantaneous
118
+ # nonlinear dynamic-curl kernel folds the loss into a field-dependent
119
+ # coefficient the linear conduction replica does not reproduce.
120
+ return (
121
+ "FDTD adjoint does not support electric conductivity combined with nonlinear "
122
+ "(chi3/chi2/TPA) media in the same material; the nonlinear dynamic-curl replay "
123
+ "does not carry the static semi-implicit conduction-loss coefficient."
124
+ )
125
+ if getattr(material, "is_nonlinear", False) and getattr(material, "is_electric_dispersive", False):
126
+ # The forward run subtracts the ADE polarization current against the
127
+ # field-dependent effective permittivity that the instantaneous
128
+ # nonlinearity produces (the dynamic curl coefficient), while the
129
+ # reverse replay divides that current by the static linear
130
+ # permittivity; the two disagree by the nonlinear index shift, so the
131
+ # gradient through a same-material chi2/chi3 + dispersive medium would
132
+ # not match its forward. Keep the forward composition (SHG phase
133
+ # matching) usable while its adjoint pullback is unresolved.
134
+ return (
135
+ "FDTD adjoint does not support instantaneous nonlinear (chi2/chi3/TPA) media "
136
+ "combined with electric dispersion in the same material; the reverse ADE "
137
+ "current is divided by the static permittivity, not the field-dependent "
138
+ "effective permittivity the forward nonlinear update uses."
139
+ )
140
+ if getattr(material, "is_anisotropic", False):
141
+ # Diagonal epsilon anisotropy maps onto the per-axis eps_Ex/Ey/Ez
142
+ # coefficient layout, and full 3x3 epsilon is now differentiable too:
143
+ # the reverse step replicates the off-diagonal curl(H) coupling and
144
+ # routes to the torch-VJP backend, so a design differentiated through
145
+ # a full-anisotropic scene gets correct gradients. A magnetic tensor
146
+ # still has no reverse gradient channel, and a trainable geometry on
147
+ # a full-anisotropic structure would drop the off-diagonal coefficient
148
+ # sensitivity, so both stay guarded with a physical reason.
149
+ if getattr(material, "mu_tensor", None) is not None:
150
+ return (
151
+ "FDTD adjoint does not support anisotropic magnetic (mu_tensor) media: "
152
+ "the magnetic reverse update carries no tensor gradient channel."
153
+ )
154
+ if isinstance(getattr(material, "epsilon_tensor", None), Tensor3x3) and _material_has_conductivity(material):
155
+ # The forward coupled-tensor conduction folds the loss through the
156
+ # off-diagonal inverse of (eps_inf + dt/2 diag(sigma)) applied to both
157
+ # curl(H) and the conduction current sigma . E, but the reverse
158
+ # conduction replica recomputes the semi-implicit decay/curl from the
159
+ # per-axis effective permittivity and carries no off-diagonal (coupled
160
+ # tensor) conduction channel, so the gradient through a full-anisotropic
161
+ # lossy medium would not match its forward. Keep the forward composition
162
+ # (lossy anisotropic crystal) usable while its adjoint pullback is
163
+ # unresolved.
164
+ return (
165
+ "FDTD adjoint does not support full (off-diagonal) anisotropic permittivity "
166
+ "combined with electric conductivity in the same material; the reverse conduction "
167
+ "replica folds the loss through the per-axis effective permittivity and lacks the "
168
+ "off-diagonal coupled-tensor conduction channel the forward semi-implicit update applies."
169
+ )
170
+ if isinstance(getattr(material, "epsilon_tensor", None), Tensor3x3) and getattr(
171
+ material, "is_electric_dispersive", False
172
+ ):
173
+ # The forward coupled-tensor ADE folds the polarization current
174
+ # through the full off-diagonal inverse permittivity tensor, but the
175
+ # reverse dispersive correction only divides the current by the
176
+ # per-axis effective permittivity and carries no off-diagonal
177
+ # coupling, so the gradient through a full-anisotropic dispersive
178
+ # medium would not match its forward. Keep the forward composition
179
+ # (rotated birefringent dispersive crystal) usable while its adjoint
180
+ # pullback is unresolved.
181
+ return (
182
+ "FDTD adjoint does not support full (off-diagonal) anisotropic permittivity "
183
+ "combined with electric dispersion in the same material; the reverse ADE "
184
+ "correction lacks the off-diagonal inverse-permittivity coupling the forward "
185
+ "coupled-tensor update applies."
186
+ )
187
+ if isinstance(getattr(material, "epsilon_tensor", None), Tensor3x3) and _structure_has_trainable_geometry(structure):
188
+ return (
189
+ "FDTD adjoint does not support trainable geometry on full (off-diagonal) "
190
+ "anisotropic structures: the off-diagonal coupling coefficients carry no "
191
+ "material gradient channel, so the geometry sensitivity would be dropped."
192
+ )
193
+ if getattr(material, "is_magnetic_dispersive", False) and _structure_has_trainable_geometry(structure):
194
+ # The reverse step models static magnetic ADE poles, but there is no
195
+ # mu-side material gradient channel, so a trainable geometry on a
196
+ # magnetic-dispersive structure would silently drop the pole-weight
197
+ # sensitivity.
198
+ return (
199
+ "FDTD adjoint does not support trainable geometry on magnetic dispersive "
200
+ "structures (no mu gradient channel) yet."
201
+ )
202
+ if getattr(material, "is_modulated", False):
203
+ return "FDTD adjoint does not support time-modulated media yet."
204
+ return None
205
+
206
+
207
+ class _FDTDGradientBridge:
208
+ def __init__(self, simulation):
209
+ self.simulation = simulation
210
+ self.base_scene = simulation.scene
211
+ self.material_inputs = self._resolve_material_inputs()
212
+ if not self.material_inputs:
213
+ raise NotImplementedError(
214
+ "FDTD backward currently supports trainable scene inputs that contribute to "
215
+ "prepared-scene material tensors."
216
+ )
217
+ self._last_solver = None
218
+ self._last_pack = None
219
+ self._last_solver_stats = None
220
+ self._last_checkpoints = ()
221
+ self._checkpoint_schema = None
222
+ self._time_steps = 0
223
+ self._dft_schedule = ()
224
+ self._observer_schedule = ()
225
+ self._dft_schedule_pack = None
226
+ self._observer_schedule_pack = None
227
+ self._last_backward_profile = None
228
+
229
+ def _material_graph_scene(self):
230
+ if self.simulation.scene_module is not None:
231
+ return self.simulation.scene_module.to_scene()
232
+ self.simulation._refresh_scene()
233
+ return self.simulation.scene
234
+
235
+ def _candidate_material_inputs(self) -> tuple[torch.Tensor, ...]:
236
+ if self.simulation.scene_module is not None:
237
+ return tuple(parameter for parameter in self.simulation.scene_module.parameters() if parameter.requires_grad)
238
+ return _scene_trainable_material_tensors(self.base_scene)
239
+
240
+ def _resolve_material_inputs(self) -> tuple[torch.Tensor, ...]:
241
+ scene = self._material_graph_scene()
242
+ return _material_dependent_inputs(scene, self._candidate_material_inputs())
243
+
244
+ def _validate_supported_configuration(self, solver):
245
+ unsupported_medium_message = _unsupported_adjoint_medium(solver.scene)
246
+ if unsupported_medium_message is not None:
247
+ raise NotImplementedError(unsupported_medium_message)
248
+ if getattr(solver, "_full_aniso_cpml_overlap", False):
249
+ # The forward off-diagonal correction coordinate-stretches the tensor
250
+ # coupling inside the CPML with per-direction psi memory, but the
251
+ # reverse replay reconstructs the off-diagonal coupling from the raw
252
+ # (un-stretched) collocated curl, so a design differentiated through an
253
+ # anisotropic structure overlapping the absorber would receive a
254
+ # gradient inconsistent with its forward. Keep the anisotropic
255
+ # structure clear of the absorber for adjoint runs.
256
+ raise NotImplementedError(
257
+ "FDTD adjoint does not support full (off-diagonal) anisotropic media overlapping "
258
+ "the CPML absorber: the reverse off-diagonal correction uses the un-stretched "
259
+ "collocated curl and does not replay the CPML coordinate stretch the forward applies."
260
+ )
261
+ face_codes = (
262
+ int(solver.boundary_x_low_code),
263
+ int(solver.boundary_x_high_code),
264
+ int(solver.boundary_y_low_code),
265
+ int(solver.boundary_y_high_code),
266
+ int(solver.boundary_z_low_code),
267
+ int(solver.boundary_z_high_code),
268
+ )
269
+ if any(code == BOUNDARY_BLOCH for code in face_codes):
270
+ if not has_complex_fields(solver):
271
+ raise NotImplementedError("FDTD adjoint requires complex field state for Bloch faces.")
272
+ if getattr(solver, "conductive_enabled", False):
273
+ raise NotImplementedError(
274
+ "FDTD adjoint does not support electric conductivity on Bloch boundaries; the "
275
+ "complex-field reverse replay applies the semi-implicit conduction-loss "
276
+ "coefficient on the real electric update only."
277
+ )
278
+ elif any(code not in {BOUNDARY_NONE, BOUNDARY_PML, BOUNDARY_PEC} for code in face_codes):
279
+ raise NotImplementedError("FDTD adjoint currently supports none, pml, pec, and Bloch faces only.")
280
+ # Every compiled source kind (point/plane/beam/mode plus the uniform and
281
+ # custom current / field sources) injects an additive current whose patch
282
+ # divides by eps at the injection cell, so the explicit reverse's analytic
283
+ # 1/eps source pullback (or the torch-VJP fallback) differentiates the
284
+ # design through it; there is no source-kind that has to be rejected here.
285
+
286
+ def _run_forward_with_checkpoints(self, solver, *, time_steps, dft_frequency, dft_window, full_field_dft, normalize_source):
287
+ runtime = _runtime()
288
+ if normalize_source and not _sources_share_spectrum(getattr(solver, "_compiled_sources", ())):
289
+ raise NotImplementedError(
290
+ "normalize_source divides monitor spectra by a single incident source "
291
+ "spectrum, which is ill-defined when the scene drives sources with "
292
+ "different source-time spectra; give the sources a common source_time "
293
+ "or disable normalize_source."
294
+ )
295
+ solver._normalize_source = normalize_source
296
+ solver._synchronize_device()
297
+ solve_start = time.perf_counter()
298
+
299
+ if dft_frequency is not None and full_field_dft:
300
+ solver.enable_dft(dft_frequency, window_type=dft_window, end_step=time_steps)
301
+ else:
302
+ solver.dft_enabled = False
303
+ solver._dft_entries = []
304
+ solver._sync_dft_legacy_state()
305
+
306
+ observer_frequency = dft_frequency if dft_frequency is not None else solver.source_frequency
307
+ if solver.observers:
308
+ solver._prepare_observers(observer_frequency, dft_window, time_steps)
309
+
310
+ checkpoint_stride = runtime._checkpoint_stride(self.simulation, time_steps)
311
+ checkpoints = [capture_checkpoint_state(solver, step=0)]
312
+ for step_index in range(time_steps):
313
+ if step_index > 0 and step_index % checkpoint_stride == 0:
314
+ checkpoints.append(capture_checkpoint_state(solver, step=step_index))
315
+
316
+ time_value = step_index * solver.dt
317
+ solver._advance_magnetic_dispersive_state()
318
+ solver._update_magnetic_fields(solver.Hx, solver.Hy, solver.Hz, solver.Ex, solver.Ey, solver.Ez)
319
+ if has_complex_fields(solver):
320
+ solver._update_magnetic_fields(
321
+ solver.Hx_imag,
322
+ solver.Hy_imag,
323
+ solver.Hz_imag,
324
+ solver.Ex_imag,
325
+ solver.Ey_imag,
326
+ solver.Ez_imag,
327
+ )
328
+ if getattr(solver, "tfsf_enabled", False):
329
+ apply_tfsf_h_correction(solver, time_value)
330
+ advance_tfsf_auxiliary_magnetic(solver)
331
+ if solver._magnetic_source_terms:
332
+ from ..excitation import inject_magnetic_surface_source_terms
333
+
334
+ inject_magnetic_surface_source_terms(solver, time_value=time_value)
335
+ solver._apply_magnetic_dispersive_corrections()
336
+
337
+ solver._advance_dispersive_state()
338
+ if has_complex_fields(solver):
339
+ if getattr(solver, "uses_cpml", False):
340
+ update_electric_fields_bloch_cpml(solver)
341
+ else:
342
+ solver._update_electric_fields_bloch()
343
+ else:
344
+ if getattr(solver, "nonlinear_enabled", False):
345
+ solver._update_nonlinear_electric_coefficients()
346
+ solver._update_electric_fields(solver.Ex, solver.Ey, solver.Ez, solver.Hx, solver.Hy, solver.Hz)
347
+ if getattr(solver, "full_aniso_enabled", False):
348
+ apply_full_aniso_corrections(solver)
349
+
350
+ if getattr(solver, "tfsf_enabled", False):
351
+ apply_tfsf_e_correction(solver, time_value)
352
+ advance_tfsf_auxiliary_electric(solver)
353
+ if solver._electric_source_terms:
354
+ from ..excitation import inject_electric_surface_source_terms
355
+
356
+ inject_electric_surface_source_terms(solver, time_value=time_value)
357
+
358
+ if solver._source_terms:
359
+ solver.add_source(time_value=time_value)
360
+ solver._apply_dispersive_corrections()
361
+ if not getattr(solver, "tfsf_enabled", False):
362
+ solver._enforce_pec_boundaries()
363
+ solver.accumulate_dft(step_index)
364
+ solver.accumulate_observers(step_index)
365
+
366
+ solver._synchronize_device()
367
+ solver.last_solve_elapsed_s = time.perf_counter() - solve_start
368
+
369
+ if solver.dft_enabled:
370
+ solver._sync_dft_legacy_state()
371
+ if solver.observers_enabled:
372
+ solver._sync_observer_legacy_state()
373
+
374
+ raw_output = {}
375
+ if solver.dft_enabled:
376
+ raw_output.update(solver.get_frequency_solution(all_frequencies=True))
377
+ if solver.observers_enabled:
378
+ raw_output["observers"] = solver.get_observer_results()
379
+ return raw_output or None, tuple(checkpoints)
380
+
381
+ def forward(self, material_inputs):
382
+ runtime = _runtime()
383
+ del material_inputs
384
+ with torch.no_grad():
385
+ self.simulation._refresh_scene()
386
+ scene = self.simulation.scene
387
+ solver = self.simulation._build_fdtd_solver_for_scene(scene, initialize=True)
388
+ self._validate_supported_configuration(solver)
389
+ from .dispatch import validate_native_adjoint_preparation
390
+
391
+ validate_native_adjoint_preparation(solver)
392
+
393
+ time_steps = self.simulation._resolve_fdtd_time_steps(solver, scene)
394
+ dft_cfg = self.simulation.config.spectral_sampler
395
+ requested_full_field_dft = self.simulation.config.full_field_dft
396
+ use_full_field_dft = requested_full_field_dft or len(self.simulation.frequencies) > 1
397
+ dft_request = self.simulation.frequency if len(self.simulation.frequencies) == 1 else self.simulation.frequencies
398
+ raw_output, checkpoints = self._run_forward_with_checkpoints(
399
+ solver,
400
+ time_steps=time_steps,
401
+ dft_frequency=dft_request,
402
+ dft_window=dft_cfg.window,
403
+ full_field_dft=use_full_field_dft,
404
+ normalize_source=dft_cfg.normalize_source,
405
+ )
406
+ if raw_output is None:
407
+ raise RuntimeError("FDTD solve did not return any output.")
408
+
409
+ pack = runtime._prepare_forward_pack(raw_output)
410
+ checkpoint_schema = runtime._validate_checkpoint_sequence(checkpoints)
411
+ self._last_solver = solver
412
+ self._last_pack = pack
413
+ self._last_checkpoints = checkpoints
414
+ self._checkpoint_schema = checkpoint_schema
415
+ self._time_steps = time_steps
416
+ self._dft_schedule = runtime._build_spectral_weight_schedule(
417
+ getattr(solver, "_dft_entries", ()),
418
+ time_steps=time_steps,
419
+ window_type=getattr(solver, "dft_window_type", "none"),
420
+ )
421
+ self._observer_schedule = runtime._build_spectral_weight_schedule(
422
+ getattr(solver, "_observer_spectral_entries", ()),
423
+ time_steps=time_steps,
424
+ window_type=getattr(solver, "observer_window_type", "none"),
425
+ )
426
+ self._dft_schedule_pack = _schedule_to_tensor_pack(
427
+ self._dft_schedule,
428
+ device=solver.device,
429
+ dtype=solver.eps_Ex.dtype,
430
+ )
431
+ self._observer_schedule_pack = _schedule_to_tensor_pack(
432
+ self._observer_schedule,
433
+ device=solver.device,
434
+ dtype=solver.eps_Ex.dtype,
435
+ )
436
+ self._last_solver_stats = self.simulation._build_fdtd_solver_stats(
437
+ solver,
438
+ time_steps=time_steps,
439
+ use_full_field_dft=use_full_field_dft,
440
+ dft_cfg=dft_cfg,
441
+ )
442
+ self._last_backward_profile = None
443
+ return pack.output_tensors
444
+
445
+ def rebuild_forward_outputs(self, output_tensors):
446
+ runtime = _runtime()
447
+ pack = self._last_pack
448
+ fields = {
449
+ field_name.upper(): output_tensors[index]
450
+ for index, field_name in enumerate(pack.field_names)
451
+ }
452
+ monitors = runtime._rebuild_monitors(pack.monitor_templates, output_tensors, len(pack.field_names))
453
+ raw_output = {
454
+ field_name: fields[field_name.upper()]
455
+ for field_name in pack.field_names
456
+ }
457
+ if monitors:
458
+ raw_output["observers"] = monitors
459
+ return fields, monitors, raw_output
460
+
461
+ def _backward_impl(self, base_inputs, grad_outputs, *, profile_enabled: bool):
462
+ runtime = _runtime()
463
+ checkpoint_stride = runtime._checkpoint_stride(self.simulation, self._time_steps)
464
+ if all(grad_output is None for grad_output in grad_outputs):
465
+ if profile_enabled:
466
+ solver = self._last_solver
467
+ timer = (
468
+ "cuda_event"
469
+ if solver is not None
470
+ and torch.cuda.is_available()
471
+ and torch.device(solver.device).type == "cuda"
472
+ else "perf_counter"
473
+ )
474
+ self._last_backward_profile = _empty_backward_profile(
475
+ timer=timer,
476
+ steps=self._time_steps,
477
+ checkpoint_stride=checkpoint_stride,
478
+ )
479
+ return tuple(torch.zeros_like(tensor) for tensor in base_inputs)
480
+
481
+ solver = self._last_solver
482
+ if solver is None or self._last_pack is None or not self._last_checkpoints:
483
+ raise RuntimeError("FDTD backward called before a forward pass initialized the adjoint bridge.")
484
+
485
+ checkpoint_schema = self._checkpoint_schema or runtime._validate_checkpoint_sequence(self._last_checkpoints)
486
+ profiler = _BackwardProfiler(enabled=profile_enabled, device=torch.device(solver.device))
487
+ profiler.start_total()
488
+
489
+ with profiler.section("seed_build"):
490
+ seed_runtime = _build_output_seeds(
491
+ solver,
492
+ self._last_pack,
493
+ grad_outputs,
494
+ dft_schedule=(
495
+ self._dft_schedule_pack
496
+ if self._dft_schedule_pack is not None
497
+ else _schedule_to_tensor_pack(self._dft_schedule, device=solver.device, dtype=solver.eps_Ex.dtype)
498
+ ),
499
+ observer_schedule=(
500
+ self._observer_schedule_pack
501
+ if self._observer_schedule_pack is not None
502
+ else _schedule_to_tensor_pack(self._observer_schedule, device=solver.device, dtype=solver.eps_Ex.dtype)
503
+ ),
504
+ )
505
+ profiler.record_seed_runtime(
506
+ seed_runtime.backend,
507
+ dense=len(seed_runtime.dense_batches),
508
+ point=len(seed_runtime.point_batches),
509
+ plane=len(seed_runtime.plane_batches),
510
+ )
511
+
512
+ checkpoint_lookup = {checkpoint.step: checkpoint for checkpoint in self._last_checkpoints}
513
+ checkpoint_steps = [checkpoint.step for checkpoint in self._last_checkpoints]
514
+ segment_bounds = list(zip(checkpoint_steps, checkpoint_steps[1:] + [self._time_steps]))
515
+
516
+ state_names = checkpoint_schema.state_names
517
+ adjoint_state = {
518
+ name: torch.zeros_like(self._last_checkpoints[0].tensors[name])
519
+ for name in state_names
520
+ }
521
+
522
+ grad_accumulator_backend = _resolve_grad_accumulator_backend(solver.device)
523
+ eps_ex = solver.eps_Ex.detach().clone().requires_grad_(True)
524
+ eps_ey = solver.eps_Ey.detach().clone().requires_grad_(True)
525
+ eps_ez = solver.eps_Ez.detach().clone().requires_grad_(True)
526
+ grad_eps_ex = torch.zeros_like(solver.eps_Ex)
527
+ grad_eps_ey = torch.zeros_like(solver.eps_Ey)
528
+ grad_eps_ez = torch.zeros_like(solver.eps_Ez)
529
+ nonlinear_enabled = bool(getattr(solver, "nonlinear_enabled", False))
530
+ general_enabled = bool(getattr(solver, "nonlinear_general_enabled", False))
531
+ chi3_ex = chi3_ey = chi3_ez = None
532
+ grad_chi3_ex = grad_chi3_ey = grad_chi3_ez = None
533
+ chi2_ex = chi2_ey = chi2_ez = None
534
+ grad_chi2_ex = grad_chi2_ey = grad_chi2_ez = None
535
+ tpa_ex = tpa_ey = tpa_ez = None
536
+ grad_tpa_ex = grad_tpa_ey = grad_tpa_ez = None
537
+ if nonlinear_enabled:
538
+ # chi3 leaves feed both the curl-only Kerr kernel and the general
539
+ # nonlinear kernel (which also reads chi3), so allocate them whenever
540
+ # any instantaneous nonlinear channel is active.
541
+ chi3_ex = solver.kerr_chi3_Ex.detach().clone().requires_grad_(True)
542
+ chi3_ey = solver.kerr_chi3_Ey.detach().clone().requires_grad_(True)
543
+ chi3_ez = solver.kerr_chi3_Ez.detach().clone().requires_grad_(True)
544
+ grad_chi3_ex = torch.zeros_like(solver.kerr_chi3_Ex)
545
+ grad_chi3_ey = torch.zeros_like(solver.kerr_chi3_Ey)
546
+ grad_chi3_ez = torch.zeros_like(solver.kerr_chi3_Ez)
547
+ if general_enabled:
548
+ # chi2 second-order susceptibility and the TPA conductivity scale
549
+ # drive the general nonlinear coefficient kernel; carry their own
550
+ # gradient channels so a trainable chi2 / TPA design is differentiable.
551
+ chi2_ex = solver.nonlinear_chi2_Ex.detach().clone().requires_grad_(True)
552
+ chi2_ey = solver.nonlinear_chi2_Ey.detach().clone().requires_grad_(True)
553
+ chi2_ez = solver.nonlinear_chi2_Ez.detach().clone().requires_grad_(True)
554
+ tpa_ex = solver.tpa_sigma_Ex.detach().clone().requires_grad_(True)
555
+ tpa_ey = solver.tpa_sigma_Ey.detach().clone().requires_grad_(True)
556
+ tpa_ez = solver.tpa_sigma_Ez.detach().clone().requires_grad_(True)
557
+ grad_chi2_ex = torch.zeros_like(solver.nonlinear_chi2_Ex)
558
+ grad_chi2_ey = torch.zeros_like(solver.nonlinear_chi2_Ey)
559
+ grad_chi2_ez = torch.zeros_like(solver.nonlinear_chi2_Ez)
560
+ grad_tpa_ex = torch.zeros_like(solver.tpa_sigma_Ex)
561
+ grad_tpa_ey = torch.zeros_like(solver.tpa_sigma_Ey)
562
+ grad_tpa_ez = torch.zeros_like(solver.tpa_sigma_Ez)
563
+ # The compiled source-term set (curl-coefficient patches and, for mode
564
+ # sources, the eigensolve profile) depends only on the solver and the frozen
565
+ # eps leaves, both constant across the backward sweep, so resolve it once and
566
+ # hand the same lists to every reverse step instead of rebuilding the replay
567
+ # solver (and re-running the mode eigensolve) per step.
568
+ resolved_source_terms = runtime._resolved_source_term_lists(solver, eps_ex, eps_ey, eps_ez)
569
+ if hasattr(solver, "_mode_source_cotangent_accum"):
570
+ delattr(solver, "_mode_source_cotangent_accum")
571
+
572
+ # The checkpoint replay reconstructs the mid-step (post-magnetic) H on its
573
+ # way to each step's electric update; for the pure real standard / CPML
574
+ # path it is bit-identical to the reverse reference backend's own magnetic
575
+ # half-step recompute, so capture it once and hand it to the reverse step
576
+ # instead of recomputing it there.
577
+ capture_mid_magnetic = runtime._replay_can_capture_mid_magnetic(solver)
578
+ try:
579
+ for start_step, end_step in reversed(segment_bounds):
580
+ mid_magnetic = [] if capture_mid_magnetic else None
581
+ with profiler.section("segment_replay"):
582
+ states = runtime._replay_segment_states(
583
+ solver,
584
+ checkpoint_lookup[start_step],
585
+ start_step,
586
+ end_step,
587
+ mid_magnetic_out=mid_magnetic,
588
+ )
589
+ for offset in range(end_step - start_step - 1, -1, -1):
590
+ step_index = start_step + offset
591
+ post_step_adjoint = {
592
+ name: value.clone()
593
+ for name, value in adjoint_state.items()
594
+ }
595
+ with profiler.section("seed_injection"):
596
+ _apply_seed_runtime(post_step_adjoint, seed_runtime, step_index)
597
+
598
+ step_result = reverse_step(
599
+ solver,
600
+ states[offset],
601
+ post_step_adjoint,
602
+ time_value=step_index * solver.dt,
603
+ eps_ex=eps_ex,
604
+ eps_ey=eps_ey,
605
+ eps_ez=eps_ez,
606
+ resolved_source_terms=resolved_source_terms,
607
+ forward_magnetic_fields=(mid_magnetic[offset] if mid_magnetic else None),
608
+ chi3_ex=chi3_ex,
609
+ chi3_ey=chi3_ey,
610
+ chi3_ez=chi3_ez,
611
+ chi2_ex=chi2_ex,
612
+ chi2_ey=chi2_ey,
613
+ chi2_ez=chi2_ez,
614
+ tpa_ex=tpa_ex,
615
+ tpa_ey=tpa_ey,
616
+ tpa_ez=tpa_ez,
617
+ profiler=profiler,
618
+ )
619
+ profiler.record_reverse_backend(step_result.backend)
620
+ grad_eps_ex = _accumulate_grad(grad_accumulator_backend, grad_eps_ex, step_result.grad_eps_ex)
621
+ grad_eps_ey = _accumulate_grad(grad_accumulator_backend, grad_eps_ey, step_result.grad_eps_ey)
622
+ grad_eps_ez = _accumulate_grad(grad_accumulator_backend, grad_eps_ez, step_result.grad_eps_ez)
623
+ if nonlinear_enabled:
624
+ if step_result.grad_chi3_ex is None:
625
+ raise RuntimeError(
626
+ "Nonlinear reverse step did not produce chi3 gradients; "
627
+ f"backend {step_result.backend!r} lacks the chi3 channel."
628
+ )
629
+ grad_chi3_ex = _accumulate_grad(grad_accumulator_backend, grad_chi3_ex, step_result.grad_chi3_ex)
630
+ grad_chi3_ey = _accumulate_grad(grad_accumulator_backend, grad_chi3_ey, step_result.grad_chi3_ey)
631
+ grad_chi3_ez = _accumulate_grad(grad_accumulator_backend, grad_chi3_ez, step_result.grad_chi3_ez)
632
+ if general_enabled:
633
+ if step_result.grad_chi2_ex is None:
634
+ raise RuntimeError(
635
+ "General-nonlinear reverse step did not produce chi2 / TPA "
636
+ f"gradients; backend {step_result.backend!r} lacks the channels."
637
+ )
638
+ grad_chi2_ex = _accumulate_grad(grad_accumulator_backend, grad_chi2_ex, step_result.grad_chi2_ex)
639
+ grad_chi2_ey = _accumulate_grad(grad_accumulator_backend, grad_chi2_ey, step_result.grad_chi2_ey)
640
+ grad_chi2_ez = _accumulate_grad(grad_accumulator_backend, grad_chi2_ez, step_result.grad_chi2_ez)
641
+ grad_tpa_ex = _accumulate_grad(grad_accumulator_backend, grad_tpa_ex, step_result.grad_tpa_ex)
642
+ grad_tpa_ey = _accumulate_grad(grad_accumulator_backend, grad_tpa_ey, step_result.grad_tpa_ey)
643
+ grad_tpa_ez = _accumulate_grad(grad_accumulator_backend, grad_tpa_ez, step_result.grad_tpa_ez)
644
+ adjoint_state = step_result.pre_step_adjoint
645
+
646
+ with profiler.section("material_pullback"):
647
+ # Mode-source profile terms deferred their eigensolve VJP to keep
648
+ # torch.autograd.grad off the per-step hot path; apply the single
649
+ # accumulated pullback here (over the retained source-term graph)
650
+ # before the material pullback consumes grad_eps.
651
+ profile_grads = runtime._mode_source_profile_pullback(
652
+ solver, resolved_source_terms, eps_ex, eps_ey, eps_ez
653
+ )
654
+ if profile_grads is not None:
655
+ grad_profile_ex, grad_profile_ey, grad_profile_ez = profile_grads
656
+ if grad_profile_ex is not None:
657
+ grad_eps_ex = _accumulate_grad(grad_accumulator_backend, grad_eps_ex, grad_profile_ex.detach())
658
+ if grad_profile_ey is not None:
659
+ grad_eps_ey = _accumulate_grad(grad_accumulator_backend, grad_eps_ey, grad_profile_ey.detach())
660
+ if grad_profile_ez is not None:
661
+ grad_eps_ez = _accumulate_grad(grad_accumulator_backend, grad_eps_ez, grad_profile_ez.detach())
662
+ with torch.enable_grad():
663
+ scene = self._material_graph_scene()
664
+ outputs = pullback_material_input_gradients(
665
+ scene,
666
+ inputs=base_inputs,
667
+ grad_eps_ex=grad_eps_ex,
668
+ grad_eps_ey=grad_eps_ey,
669
+ grad_eps_ez=grad_eps_ez,
670
+ eps0=solver.eps0,
671
+ grad_chi3_ex=grad_chi3_ex,
672
+ grad_chi3_ey=grad_chi3_ey,
673
+ grad_chi3_ez=grad_chi3_ez,
674
+ grad_chi2_ex=grad_chi2_ex,
675
+ grad_chi2_ey=grad_chi2_ey,
676
+ grad_chi2_ez=grad_chi2_ez,
677
+ grad_tpa_ex=grad_tpa_ex,
678
+ grad_tpa_ey=grad_tpa_ey,
679
+ grad_tpa_ez=grad_tpa_ez,
680
+ )
681
+ finally:
682
+ if hasattr(solver, "_mode_source_cotangent_accum"):
683
+ delattr(solver, "_mode_source_cotangent_accum")
684
+ profiler.record_material_pullback_backend("autograd_material_graph" if base_inputs else "none")
685
+
686
+ profiler.stop_total()
687
+ if profile_enabled:
688
+ self._last_backward_profile = profiler.summary(
689
+ steps=self._time_steps,
690
+ segments=len(segment_bounds),
691
+ checkpoint_stride=checkpoint_stride,
692
+ )
693
+ return outputs
694
+
695
+ def backward(self, base_inputs, grad_outputs):
696
+ return self._backward_impl(base_inputs, grad_outputs, profile_enabled=False)
697
+
698
+ def backward_profile(self, grad_outputs=None, *, base_inputs=None):
699
+ if self._last_pack is None or self._last_solver is None:
700
+ raise RuntimeError("Run forward() before requesting a backward profile.")
701
+ resolved_grad_outputs = (
702
+ tuple(torch.ones_like(output) for output in self._last_pack.output_tensors)
703
+ if grad_outputs is None
704
+ else tuple(grad_outputs)
705
+ )
706
+ resolved_base_inputs = tuple(self.material_inputs if base_inputs is None else base_inputs)
707
+ self._backward_impl(
708
+ resolved_base_inputs,
709
+ resolved_grad_outputs,
710
+ profile_enabled=True,
711
+ )
712
+ return _clone_backward_profile(self._last_backward_profile)
713
+
714
+
715
+ class _FDTDMaterialGradientFunction(torch.autograd.Function):
716
+ @staticmethod
717
+ def forward(ctx, bridge, *material_inputs):
718
+ ctx.bridge = bridge
719
+ ctx.material_inputs = tuple(material_inputs)
720
+ outputs = bridge.forward(tuple(material_inputs))
721
+ return outputs if len(outputs) != 1 else outputs[0]
722
+
723
+ @staticmethod
724
+ def backward(ctx, *grad_outputs):
725
+ gradients = ctx.bridge.backward(ctx.material_inputs, grad_outputs)
726
+ return (None, *gradients)
727
+
728
+
729
+ def run_fdtd_with_gradient_bridge(simulation) -> Result:
730
+ bridge = _FDTDGradientBridge(simulation)
731
+ raw_outputs = _FDTDMaterialGradientFunction.apply(bridge, *bridge.material_inputs)
732
+ output_tensors = raw_outputs if isinstance(raw_outputs, tuple) else (raw_outputs,)
733
+ fields, monitors, raw_output = bridge.rebuild_forward_outputs(output_tensors)
734
+ return Result(
735
+ method="fdtd",
736
+ scene=simulation.scene,
737
+ prepared_scene=bridge._last_solver.scene if bridge._last_solver is not None else None,
738
+ frequency=simulation.frequency,
739
+ frequencies=simulation.frequencies,
740
+ solver=bridge._last_solver,
741
+ fields=fields,
742
+ monitors=monitors,
743
+ metadata=simulation.metadata,
744
+ solver_stats=bridge._last_solver_stats,
745
+ raw_output=raw_output,
746
+ )