witwin-maxwell 0.3.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- witwin/maxwell/__init__.py +164 -0
- witwin/maxwell/adapters/__init__.py +1 -0
- witwin/maxwell/adapters/gds.py +203 -0
- witwin/maxwell/adapters/tidy3d.py +1669 -0
- witwin/maxwell/adjoint_inputs.py +70 -0
- witwin/maxwell/compiler/__init__.py +11 -0
- witwin/maxwell/compiler/materials.py +1355 -0
- witwin/maxwell/compiler/monitors.py +164 -0
- witwin/maxwell/compiler/sources.py +222 -0
- witwin/maxwell/fdfd/__init__.py +3 -0
- witwin/maxwell/fdfd/adjoint/__init__.py +3 -0
- witwin/maxwell/fdfd/adjoint/bridge.py +180 -0
- witwin/maxwell/fdfd/ams.py +300 -0
- witwin/maxwell/fdfd/krylov.py +280 -0
- witwin/maxwell/fdfd/plotting.py +110 -0
- witwin/maxwell/fdfd/postprocess.py +33 -0
- witwin/maxwell/fdfd/solver.py +1057 -0
- witwin/maxwell/fdtd/__init__.py +4 -0
- witwin/maxwell/fdtd/adjoint/__init__.py +21 -0
- witwin/maxwell/fdtd/adjoint/bridge.py +746 -0
- witwin/maxwell/fdtd/adjoint/capabilities.py +29 -0
- witwin/maxwell/fdtd/adjoint/core.py +3614 -0
- witwin/maxwell/fdtd/adjoint/dispatch.py +684 -0
- witwin/maxwell/fdtd/adjoint/native.py +2561 -0
- witwin/maxwell/fdtd/adjoint/profiler.py +195 -0
- witwin/maxwell/fdtd/adjoint/reverse_common.py +207 -0
- witwin/maxwell/fdtd/adjoint/seeds.py +524 -0
- witwin/maxwell/fdtd/boundary/__init__.py +37 -0
- witwin/maxwell/fdtd/boundary/bloch.py +90 -0
- witwin/maxwell/fdtd/boundary/common.py +41 -0
- witwin/maxwell/fdtd/boundary/cpml.py +527 -0
- witwin/maxwell/fdtd/boundary/runtime.py +209 -0
- witwin/maxwell/fdtd/checkpoint.py +227 -0
- witwin/maxwell/fdtd/coords.py +32 -0
- witwin/maxwell/fdtd/cuda/__init__.py +11 -0
- witwin/maxwell/fdtd/cuda/backend.py +3532 -0
- witwin/maxwell/fdtd/cuda/build.py +292 -0
- witwin/maxwell/fdtd/cuda/extension.cpp +2053 -0
- witwin/maxwell/fdtd/cuda/kernels/adjoint.cu +5290 -0
- witwin/maxwell/fdtd/cuda/kernels/aniso.cu +1704 -0
- witwin/maxwell/fdtd/cuda/kernels/boundary.cu +424 -0
- witwin/maxwell/fdtd/cuda/kernels/common.cu +74 -0
- witwin/maxwell/fdtd/cuda/kernels/common.cuh +82 -0
- witwin/maxwell/fdtd/cuda/kernels/dispersive.cu +782 -0
- witwin/maxwell/fdtd/cuda/kernels/electric.cu +3613 -0
- witwin/maxwell/fdtd/cuda/kernels/magnetic.cu +781 -0
- witwin/maxwell/fdtd/cuda/kernels/observers.cu +347 -0
- witwin/maxwell/fdtd/cuda/kernels/projection.cu +211 -0
- witwin/maxwell/fdtd/cuda/kernels/sources.cu +1482 -0
- witwin/maxwell/fdtd/cuda/kernels/spectral.cu +297 -0
- witwin/maxwell/fdtd/cuda/launch.h +23 -0
- witwin/maxwell/fdtd/cuda/prebuilt/witwin_maxwell_fdtd_cuda.pyd +0 -0
- witwin/maxwell/fdtd/cuda/runtime/__init__.py +6 -0
- witwin/maxwell/fdtd/cuda/runtime/dispatcher.py +6 -0
- witwin/maxwell/fdtd/cuda/runtime/graph.py +33 -0
- witwin/maxwell/fdtd/cuda/tensors.h +35 -0
- witwin/maxwell/fdtd/dispersion.py +48 -0
- witwin/maxwell/fdtd/evaluation.py +290 -0
- witwin/maxwell/fdtd/excitation/__init__.py +27 -0
- witwin/maxwell/fdtd/excitation/injection.py +1102 -0
- witwin/maxwell/fdtd/excitation/modes.py +1675 -0
- witwin/maxwell/fdtd/excitation/spatial.py +469 -0
- witwin/maxwell/fdtd/excitation/temporal.py +510 -0
- witwin/maxwell/fdtd/excitation/tfsf_apply.py +264 -0
- witwin/maxwell/fdtd/excitation/tfsf_common.py +421 -0
- witwin/maxwell/fdtd/excitation/tfsf_specs.py +490 -0
- witwin/maxwell/fdtd/excitation/tfsf_state.py +778 -0
- witwin/maxwell/fdtd/io.py +31 -0
- witwin/maxwell/fdtd/material_pullback.py +371 -0
- witwin/maxwell/fdtd/meshing/__init__.py +3 -0
- witwin/maxwell/fdtd/meshing/autogrid.py +335 -0
- witwin/maxwell/fdtd/observers.py +1453 -0
- witwin/maxwell/fdtd/plotting.py +121 -0
- witwin/maxwell/fdtd/postprocess.py +205 -0
- witwin/maxwell/fdtd/runtime/__init__.py +119 -0
- witwin/maxwell/fdtd/runtime/initialization.py +236 -0
- witwin/maxwell/fdtd/runtime/materials.py +1563 -0
- witwin/maxwell/fdtd/runtime/module_cache.py +31 -0
- witwin/maxwell/fdtd/runtime/spectral.py +581 -0
- witwin/maxwell/fdtd/runtime/stepping.py +1987 -0
- witwin/maxwell/fdtd/solver.py +502 -0
- witwin/maxwell/geometry/__init__.py +5 -0
- witwin/maxwell/geometry/polyslab.py +299 -0
- witwin/maxwell/materials.py +3 -0
- witwin/maxwell/media.py +1467 -0
- witwin/maxwell/monitors.py +743 -0
- witwin/maxwell/ports.py +113 -0
- witwin/maxwell/postprocess/RCS.py +139 -0
- witwin/maxwell/postprocess/__init__.py +42 -0
- witwin/maxwell/postprocess/diffraction.py +219 -0
- witwin/maxwell/postprocess/directivity.py +152 -0
- witwin/maxwell/postprocess/emission.py +67 -0
- witwin/maxwell/postprocess/modal.py +324 -0
- witwin/maxwell/postprocess/nfft.py +266 -0
- witwin/maxwell/postprocess/scattering_parameters.py +358 -0
- witwin/maxwell/postprocess/stratton_chu.py +1193 -0
- witwin/maxwell/result.py +967 -0
- witwin/maxwell/scene.py +1338 -0
- witwin/maxwell/simulation.py +659 -0
- witwin/maxwell/sources.py +917 -0
- witwin/maxwell/visualization/__init__.py +28 -0
- witwin/maxwell/visualization/interactive.py +484 -0
- witwin/maxwell/visualization/plots.py +568 -0
- witwin_maxwell-0.3.0.dist-info/METADATA +184 -0
- witwin_maxwell-0.3.0.dist-info/RECORD +107 -0
- witwin_maxwell-0.3.0.dist-info/WHEEL +4 -0
- witwin_maxwell-0.3.0.dist-info/licenses/LICENSE +13 -0
|
@@ -0,0 +1,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
|
+
)
|