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,3614 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
|
|
10
|
+
from ...sources import evaluate_source_time
|
|
11
|
+
from ..boundary import (
|
|
12
|
+
BOUNDARY_BLOCH,
|
|
13
|
+
BOUNDARY_NONE,
|
|
14
|
+
BOUNDARY_PEC,
|
|
15
|
+
BOUNDARY_PML,
|
|
16
|
+
has_complex_fields,
|
|
17
|
+
)
|
|
18
|
+
from .profiler import _ReverseStepResult
|
|
19
|
+
from ..checkpoint import (
|
|
20
|
+
FDTDCheckpointSchema,
|
|
21
|
+
checkpoint_schema,
|
|
22
|
+
clone_checkpoint_tensors,
|
|
23
|
+
dispersive_state_name,
|
|
24
|
+
iter_dispersive_state_specs,
|
|
25
|
+
iter_magnetic_dispersive_state_specs,
|
|
26
|
+
validate_checkpoint_state,
|
|
27
|
+
)
|
|
28
|
+
from ..excitation.injection import initialize_source_terms
|
|
29
|
+
from ..excitation.temporal import _resolve_term_source
|
|
30
|
+
from ..excitation.tfsf_specs import reference_sample_axis_code
|
|
31
|
+
from ..observers import (
|
|
32
|
+
_align_plane_monitor_payload,
|
|
33
|
+
_compute_plane_flux,
|
|
34
|
+
_monitor_payload_is_point,
|
|
35
|
+
_plane_coord_names,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_TFSF_REFERENCE_PROVIDERS = {"plane_wave_ref_x_ez", "plane_wave_axis_aligned"}
|
|
39
|
+
_TFSF_AUXILIARY_PROVIDERS = {"plane_wave_ref_x_ez", "plane_wave_axis_aligned", "plane_wave_aux"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _monitor_template_is_point(template):
|
|
43
|
+
if "field_indices" in template:
|
|
44
|
+
return True
|
|
45
|
+
if "axis" in template:
|
|
46
|
+
return False
|
|
47
|
+
kind = template.get("kind")
|
|
48
|
+
if kind is not None:
|
|
49
|
+
return kind == "point"
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _prepare_point_monitor_template(payload, start_index):
|
|
54
|
+
component_indices = {}
|
|
55
|
+
next_index = start_index
|
|
56
|
+
for component_name in payload["fields"]:
|
|
57
|
+
component_indices[component_name] = next_index
|
|
58
|
+
next_index += 1
|
|
59
|
+
template = {
|
|
60
|
+
"kind": "point",
|
|
61
|
+
"fields": tuple(payload["fields"]),
|
|
62
|
+
"field_indices": dict(payload.get("field_indices", {})),
|
|
63
|
+
"samples": payload.get("samples"),
|
|
64
|
+
"frequency": payload.get("frequency"),
|
|
65
|
+
"frequencies": tuple(payload.get("frequencies", (payload.get("frequency"),))),
|
|
66
|
+
"position": payload.get("position"),
|
|
67
|
+
"component_indices": component_indices,
|
|
68
|
+
}
|
|
69
|
+
return template, next_index
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _prepare_plane_monitor_template(payload, start_index):
|
|
73
|
+
component_templates = {}
|
|
74
|
+
next_index = start_index
|
|
75
|
+
for component_name in payload["fields"]:
|
|
76
|
+
component_payload = payload["components"][component_name]
|
|
77
|
+
component_templates[component_name] = {
|
|
78
|
+
"output_index": next_index,
|
|
79
|
+
"coords": component_payload["coords"],
|
|
80
|
+
"plane_index": component_payload["plane_index"],
|
|
81
|
+
"plane_indices": tuple(component_payload.get("plane_indices", (component_payload["plane_index"],))),
|
|
82
|
+
"plane_weights": tuple(component_payload.get("plane_weights", (1.0,))),
|
|
83
|
+
"plane_positions": tuple(component_payload.get("plane_positions", (float(payload["position"]),))),
|
|
84
|
+
}
|
|
85
|
+
next_index += 1
|
|
86
|
+
template = {
|
|
87
|
+
"kind": "plane",
|
|
88
|
+
"monitor_type": payload.get("monitor_type", "plane"),
|
|
89
|
+
"fields": tuple(payload["fields"]),
|
|
90
|
+
"plane_indices": dict(payload.get("plane_indices", {})),
|
|
91
|
+
"samples": payload.get("samples"),
|
|
92
|
+
"frequency": payload.get("frequency"),
|
|
93
|
+
"frequencies": tuple(payload.get("frequencies", (payload.get("frequency"),))),
|
|
94
|
+
"axis": payload["axis"],
|
|
95
|
+
"position": payload["position"],
|
|
96
|
+
"compute_flux": bool(payload.get("compute_flux", False)),
|
|
97
|
+
"normal_direction": payload.get("normal_direction", "+"),
|
|
98
|
+
"mode_spec": payload.get("mode_spec"),
|
|
99
|
+
"components": component_templates,
|
|
100
|
+
}
|
|
101
|
+
return template, next_index
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _prepare_forward_pack(raw_output):
|
|
105
|
+
output_tensors = []
|
|
106
|
+
field_names = []
|
|
107
|
+
for field_name in ("Ex", "Ey", "Ez"):
|
|
108
|
+
value = raw_output.get(field_name)
|
|
109
|
+
if isinstance(value, torch.Tensor):
|
|
110
|
+
field_names.append(field_name)
|
|
111
|
+
output_tensors.append(value)
|
|
112
|
+
|
|
113
|
+
monitor_templates = {}
|
|
114
|
+
next_index = len(output_tensors)
|
|
115
|
+
for monitor_name, payload in raw_output.get("observers", {}).items():
|
|
116
|
+
if _monitor_payload_is_point(payload):
|
|
117
|
+
template, next_index = _prepare_point_monitor_template(payload, next_index)
|
|
118
|
+
else:
|
|
119
|
+
template, next_index = _prepare_plane_monitor_template(payload, next_index)
|
|
120
|
+
monitor_templates[monitor_name] = template
|
|
121
|
+
if _monitor_template_is_point(template):
|
|
122
|
+
for component_name in template["fields"]:
|
|
123
|
+
output_tensors.append(payload["components"][component_name])
|
|
124
|
+
else:
|
|
125
|
+
for component_name in template["fields"]:
|
|
126
|
+
output_tensors.append(payload["components"][component_name]["data"])
|
|
127
|
+
|
|
128
|
+
return _ForwardPack(
|
|
129
|
+
field_names=tuple(field_names),
|
|
130
|
+
monitor_templates=monitor_templates,
|
|
131
|
+
output_tensors=tuple(output_tensors),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _finalize_point_monitor_payload(payload):
|
|
136
|
+
primary_component = payload["fields"][0]
|
|
137
|
+
for component_name, value in payload["components"].items():
|
|
138
|
+
payload[component_name] = value
|
|
139
|
+
if len(payload["components"]) == 1:
|
|
140
|
+
payload["component"] = primary_component.lower()
|
|
141
|
+
payload["data"] = payload["components"][primary_component]
|
|
142
|
+
payload["field_index"] = payload["field_indices"][primary_component]
|
|
143
|
+
return payload
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _finalize_plane_monitor_payload(payload):
|
|
147
|
+
axis = payload["axis"]
|
|
148
|
+
coord_names = _plane_coord_names(axis)
|
|
149
|
+
primary_component = payload["fields"][0]
|
|
150
|
+
|
|
151
|
+
if len(payload["components"]) == 1:
|
|
152
|
+
component_payload = payload["components"][primary_component]
|
|
153
|
+
payload["component"] = primary_component.lower()
|
|
154
|
+
payload["data"] = component_payload["data"]
|
|
155
|
+
payload["coords"] = component_payload["coords"]
|
|
156
|
+
payload["plane_index"] = component_payload["plane_index"]
|
|
157
|
+
payload["plane_sample_indices"] = component_payload["plane_indices"]
|
|
158
|
+
payload["plane_sample_weights"] = component_payload["plane_weights"]
|
|
159
|
+
payload["plane_sample_positions"] = component_payload["plane_positions"]
|
|
160
|
+
payload[coord_names[0]] = component_payload["coords"][0]
|
|
161
|
+
payload[coord_names[1]] = component_payload["coords"][1]
|
|
162
|
+
payload[primary_component] = component_payload["data"]
|
|
163
|
+
if payload.get("compute_flux"):
|
|
164
|
+
flux = _compute_plane_flux(payload)
|
|
165
|
+
payload["flux"] = flux
|
|
166
|
+
payload["power"] = flux
|
|
167
|
+
return payload
|
|
168
|
+
|
|
169
|
+
aligned = _align_plane_monitor_payload(axis, payload["components"])
|
|
170
|
+
if aligned is None:
|
|
171
|
+
if payload.get("compute_flux"):
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"Monitor {payload!r} is missing aligned tangential fields required for flux integration."
|
|
174
|
+
)
|
|
175
|
+
return payload
|
|
176
|
+
|
|
177
|
+
payload[coord_names[0]] = aligned[coord_names[0]]
|
|
178
|
+
payload[coord_names[1]] = aligned[coord_names[1]]
|
|
179
|
+
payload["coords"] = (aligned[coord_names[0]], aligned[coord_names[1]])
|
|
180
|
+
for component_name, value in aligned["fields"].items():
|
|
181
|
+
payload[component_name] = value
|
|
182
|
+
if payload.get("compute_flux"):
|
|
183
|
+
flux = _compute_plane_flux(payload)
|
|
184
|
+
payload["flux"] = flux
|
|
185
|
+
payload["power"] = flux
|
|
186
|
+
return payload
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _rebuild_monitors(monitor_templates, output_tensors, field_offset):
|
|
190
|
+
monitors = {}
|
|
191
|
+
cursor = field_offset
|
|
192
|
+
for monitor_name, template in monitor_templates.items():
|
|
193
|
+
if _monitor_template_is_point(template):
|
|
194
|
+
payload = {
|
|
195
|
+
"kind": "point",
|
|
196
|
+
"fields": template["fields"],
|
|
197
|
+
"components": {},
|
|
198
|
+
"field_indices": dict(template["field_indices"]),
|
|
199
|
+
"samples": template["samples"],
|
|
200
|
+
"frequency": template["frequency"],
|
|
201
|
+
"frequencies": template["frequencies"],
|
|
202
|
+
"position": template["position"],
|
|
203
|
+
}
|
|
204
|
+
for component_name in template["fields"]:
|
|
205
|
+
payload["components"][component_name] = output_tensors[cursor]
|
|
206
|
+
cursor += 1
|
|
207
|
+
monitors[monitor_name] = _finalize_point_monitor_payload(payload)
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
payload = {
|
|
211
|
+
"kind": "plane",
|
|
212
|
+
"monitor_type": template.get("monitor_type", "plane"),
|
|
213
|
+
"fields": template["fields"],
|
|
214
|
+
"components": {},
|
|
215
|
+
"plane_indices": dict(template["plane_indices"]),
|
|
216
|
+
"samples": template["samples"],
|
|
217
|
+
"frequency": template["frequency"],
|
|
218
|
+
"frequencies": template["frequencies"],
|
|
219
|
+
"axis": template["axis"],
|
|
220
|
+
"position": template["position"],
|
|
221
|
+
"compute_flux": template["compute_flux"],
|
|
222
|
+
"normal_direction": template["normal_direction"],
|
|
223
|
+
"mode_spec": template.get("mode_spec"),
|
|
224
|
+
}
|
|
225
|
+
for component_name in template["fields"]:
|
|
226
|
+
component_template = template["components"][component_name]
|
|
227
|
+
payload["components"][component_name] = {
|
|
228
|
+
"data": output_tensors[cursor],
|
|
229
|
+
"coords": component_template["coords"],
|
|
230
|
+
"plane_index": component_template["plane_index"],
|
|
231
|
+
"plane_indices": component_template["plane_indices"],
|
|
232
|
+
"plane_weights": component_template["plane_weights"],
|
|
233
|
+
"plane_positions": component_template["plane_positions"],
|
|
234
|
+
}
|
|
235
|
+
cursor += 1
|
|
236
|
+
monitors[monitor_name] = _finalize_plane_monitor_payload(payload)
|
|
237
|
+
return monitors
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _term_grid_tensor(term):
|
|
241
|
+
for key in ("patch", "delay_patch", "cw_cos_patch", "cw_sin_patch"):
|
|
242
|
+
value = term.get(key)
|
|
243
|
+
if value is not None:
|
|
244
|
+
return value
|
|
245
|
+
raise ValueError("source term does not include a tensor payload.")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _slice_from_offsets_shape(offsets, shape):
|
|
249
|
+
return tuple(slice(int(offset), int(offset) + int(size)) for offset, size in zip(offsets, shape))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _safe_grad(grad, reference):
|
|
253
|
+
"""Normalize optional autograd output while constructing monitor seeds."""
|
|
254
|
+
if grad is None:
|
|
255
|
+
return torch.zeros_like(reference)
|
|
256
|
+
return grad.to(device=reference.device, dtype=reference.dtype)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _validate_checkpoint_sequence(checkpoints) -> FDTDCheckpointSchema:
|
|
260
|
+
if not checkpoints:
|
|
261
|
+
raise RuntimeError("Adjoint replay requires at least one checkpoint.")
|
|
262
|
+
schema = validate_checkpoint_state(checkpoints[0])
|
|
263
|
+
for checkpoint in checkpoints[1:]:
|
|
264
|
+
validate_checkpoint_state(checkpoint, expected_schema=schema)
|
|
265
|
+
return schema
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _checkpoint_stride(simulation, time_steps: int) -> int:
|
|
269
|
+
configured = getattr(simulation.config, "adjoint_checkpoint_stride", None)
|
|
270
|
+
if configured is not None:
|
|
271
|
+
return max(1, int(configured))
|
|
272
|
+
return max(1, int(math.sqrt(max(time_steps, 1))))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _source_time_kind_code(source_time) -> int:
|
|
276
|
+
return int(source_time["kind_code"])
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _custom_source_time_tensor(source_time, sample_time: torch.Tensor) -> torch.Tensor:
|
|
280
|
+
amplitude = float(source_time["amplitude"])
|
|
281
|
+
times = source_time.get("times")
|
|
282
|
+
if times is None:
|
|
283
|
+
raise NotImplementedError(
|
|
284
|
+
"Callable CustomSourceTime(fn) is not supported in the FDTD adjoint; "
|
|
285
|
+
"provide a sampled (times, amplitudes) table instead."
|
|
286
|
+
)
|
|
287
|
+
xp = sample_time.new_tensor(times)
|
|
288
|
+
fp = sample_time.new_tensor(source_time["amplitudes"])
|
|
289
|
+
upper = torch.searchsorted(xp, sample_time, right=True).clamp(1, xp.numel() - 1)
|
|
290
|
+
lower = upper - 1
|
|
291
|
+
x0 = xp[lower]
|
|
292
|
+
x1 = xp[upper]
|
|
293
|
+
weight = (sample_time - x0) / (x1 - x0)
|
|
294
|
+
interpolated = fp[lower] + weight * (fp[upper] - fp[lower])
|
|
295
|
+
inside = (sample_time >= xp[0]) & (sample_time <= xp[-1])
|
|
296
|
+
interpolated = torch.where(inside, interpolated, torch.zeros_like(interpolated))
|
|
297
|
+
return amplitude * interpolated
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _evaluate_source_time_tensor(source_time, sample_time: torch.Tensor) -> torch.Tensor:
|
|
301
|
+
kind_code = _source_time_kind_code(source_time)
|
|
302
|
+
two_pi = 2.0 * math.pi
|
|
303
|
+
amplitude = float(source_time["amplitude"])
|
|
304
|
+
frequency = float(source_time["frequency"])
|
|
305
|
+
phase = float(source_time.get("phase", 0.0))
|
|
306
|
+
delay = float(source_time.get("delay", 0.0))
|
|
307
|
+
|
|
308
|
+
if kind_code == 3:
|
|
309
|
+
return _custom_source_time_tensor(source_time, sample_time)
|
|
310
|
+
|
|
311
|
+
if kind_code == 0:
|
|
312
|
+
return amplitude * torch.cos(two_pi * frequency * sample_time + phase)
|
|
313
|
+
|
|
314
|
+
if kind_code == 1:
|
|
315
|
+
fwidth = float(source_time["fwidth"])
|
|
316
|
+
sigma_t = 1.0 / max(two_pi * fwidth, 1.0e-30)
|
|
317
|
+
tau = sample_time - delay
|
|
318
|
+
envelope = torch.exp(-0.5 * (tau / sigma_t) ** 2)
|
|
319
|
+
return amplitude * envelope * torch.cos(two_pi * frequency * tau + phase)
|
|
320
|
+
|
|
321
|
+
tau = sample_time - delay
|
|
322
|
+
alpha = math.pi * frequency * tau
|
|
323
|
+
alpha_sq = alpha * alpha
|
|
324
|
+
return amplitude * (1.0 - 2.0 * alpha_sq) * torch.exp(-alpha_sq)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _resolve_cw_signal(source_time, omega, time_value):
|
|
328
|
+
phase = float(omega) * float(time_value) + float(source_time["phase"])
|
|
329
|
+
amplitude = float(source_time["amplitude"])
|
|
330
|
+
return amplitude * math.cos(phase), amplitude * math.sin(phase)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _dynamic_electric_curl(base_curl: torch.Tensor, base_eps: torch.Tensor, eps: torch.Tensor) -> torch.Tensor:
|
|
334
|
+
return ((base_curl * base_eps) / eps).contiguous()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _build_source_replay_solver(solver, compiled_sources, eps_ex, eps_ey, eps_ez):
|
|
338
|
+
compiled_sources = tuple(compiled_sources)
|
|
339
|
+
primary = compiled_sources[0]
|
|
340
|
+
return SimpleNamespace(
|
|
341
|
+
scene=solver.scene,
|
|
342
|
+
min_dx=solver.min_dx,
|
|
343
|
+
min_dy=solver.min_dy,
|
|
344
|
+
min_dz=solver.min_dz,
|
|
345
|
+
Nx=solver.Nx,
|
|
346
|
+
Ny=solver.Ny,
|
|
347
|
+
Nz=solver.Nz,
|
|
348
|
+
device=solver.device,
|
|
349
|
+
Ex=eps_ex,
|
|
350
|
+
Ey=eps_ey,
|
|
351
|
+
Ez=eps_ez,
|
|
352
|
+
Hx=solver.Hx,
|
|
353
|
+
Hy=solver.Hy,
|
|
354
|
+
Hz=solver.Hz,
|
|
355
|
+
dt=solver.dt,
|
|
356
|
+
c=solver.c,
|
|
357
|
+
eps0=solver.eps0,
|
|
358
|
+
source_frequency=solver.source_frequency,
|
|
359
|
+
source_omega=solver.source_omega,
|
|
360
|
+
eps_Ex=eps_ex,
|
|
361
|
+
eps_Ey=eps_ey,
|
|
362
|
+
eps_Ez=eps_ez,
|
|
363
|
+
cex_curl=_dynamic_electric_curl(solver.cex_curl, solver.eps_Ex, eps_ex),
|
|
364
|
+
cey_curl=_dynamic_electric_curl(solver.cey_curl, solver.eps_Ey, eps_ey),
|
|
365
|
+
cez_curl=_dynamic_electric_curl(solver.cez_curl, solver.eps_Ez, eps_ez),
|
|
366
|
+
chx_curl=solver.chx_curl,
|
|
367
|
+
chy_curl=solver.chy_curl,
|
|
368
|
+
chz_curl=solver.chz_curl,
|
|
369
|
+
boundary_kind=solver.boundary_kind,
|
|
370
|
+
tfsf_enabled=bool(getattr(solver, "tfsf_enabled", False)),
|
|
371
|
+
_compiled_sources=compiled_sources,
|
|
372
|
+
_compiled_source=primary,
|
|
373
|
+
_compiled_material_model=getattr(solver, "_compiled_material_model", None),
|
|
374
|
+
_mode_source_rebuild_from_fields=True,
|
|
375
|
+
_source_time=primary["source_time"],
|
|
376
|
+
_iter_source_images=solver._iter_source_images,
|
|
377
|
+
_component_plane_spec=solver._component_plane_spec,
|
|
378
|
+
_component_positions=solver._component_positions,
|
|
379
|
+
_plane_coordinate=solver._plane_coordinate,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# Source kinds whose eps-leaf-dependent patches are re-derived on the replay
|
|
384
|
+
# solver so the mode-source objective and the torch-VJP reverse can flow
|
|
385
|
+
# gradients into eps. Other kinds keep their forward-built terms.
|
|
386
|
+
_SOURCE_REPLAY_REBUILD_KINDS = frozenset(
|
|
387
|
+
{"point_dipole", "plane_wave", "gaussian_beam", "mode_source"}
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _compiled_sources_for_replay(solver):
|
|
392
|
+
compiled_sources = tuple(getattr(solver, "_compiled_sources", ()) or ())
|
|
393
|
+
if compiled_sources:
|
|
394
|
+
return compiled_sources
|
|
395
|
+
compiled_source = getattr(solver, "_compiled_source", None)
|
|
396
|
+
return (compiled_source,) if compiled_source is not None else ()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _scene_has_mode_source(solver) -> bool:
|
|
400
|
+
return any(
|
|
401
|
+
source.get("kind") == "mode_source"
|
|
402
|
+
for source in _compiled_sources_for_replay(solver)
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _resolved_source_term_lists(solver, eps_ex, eps_ey, eps_ez):
|
|
407
|
+
default_source_terms = getattr(solver, "_source_terms", ())
|
|
408
|
+
default_electric_source_terms = getattr(solver, "_electric_source_terms", ())
|
|
409
|
+
default_magnetic_source_terms = getattr(solver, "_magnetic_source_terms", ())
|
|
410
|
+
compiled_sources = _compiled_sources_for_replay(solver)
|
|
411
|
+
if not compiled_sources:
|
|
412
|
+
return default_source_terms, default_electric_source_terms, default_magnetic_source_terms
|
|
413
|
+
# A kind outside the rebuild set (e.g. astigmatic beams) keeps the exact
|
|
414
|
+
# forward-built terms, which already reproduce the injection for the explicit
|
|
415
|
+
# reverse path; if any source in the scene is such a kind, fall back to the
|
|
416
|
+
# forward terms for the whole scene so the per-source term indexing stays
|
|
417
|
+
# consistent with the forward solve.
|
|
418
|
+
if not all(source.get("kind") in _SOURCE_REPLAY_REBUILD_KINDS for source in compiled_sources):
|
|
419
|
+
return default_source_terms, default_electric_source_terms, default_magnetic_source_terms
|
|
420
|
+
|
|
421
|
+
has_mode_source = any(source.get("kind") == "mode_source" for source in compiled_sources)
|
|
422
|
+
temp_solver = _build_source_replay_solver(solver, compiled_sources, eps_ex, eps_ey, eps_ez)
|
|
423
|
+
if has_mode_source:
|
|
424
|
+
# Build the mode-source patches under grad tracking so the eigensolve
|
|
425
|
+
# profile keeps its eps-graph; the FDTD backward runs under no_grad, and
|
|
426
|
+
# _mode_source_profile_pullback needs that graph for its once-per-pass VJP.
|
|
427
|
+
with torch.enable_grad():
|
|
428
|
+
initialize_source_terms(temp_solver)
|
|
429
|
+
else:
|
|
430
|
+
initialize_source_terms(temp_solver)
|
|
431
|
+
return (
|
|
432
|
+
tuple(temp_solver._source_terms),
|
|
433
|
+
tuple(temp_solver._electric_source_terms),
|
|
434
|
+
tuple(temp_solver._magnetic_source_terms),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _has_resolved_source_terms(resolved_source_terms) -> bool:
|
|
439
|
+
if resolved_source_terms is None:
|
|
440
|
+
return False
|
|
441
|
+
source_terms, electric_source_terms, magnetic_source_terms = resolved_source_terms
|
|
442
|
+
return bool(source_terms or electric_source_terms or magnetic_source_terms)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _can_use_explicit_source_term_reverse_step(solver, resolved_source_terms) -> bool:
|
|
446
|
+
if not _has_resolved_source_terms(resolved_source_terms):
|
|
447
|
+
return True
|
|
448
|
+
if getattr(solver, "tfsf_enabled", False):
|
|
449
|
+
return False
|
|
450
|
+
if has_complex_fields(solver):
|
|
451
|
+
# The complex (Bloch) source replay does not carry the terminal PEC-face
|
|
452
|
+
# clamp on the source patch, so keep PEC + complex on the reject path.
|
|
453
|
+
if bool(getattr(solver, "has_pec_faces", False)):
|
|
454
|
+
return False
|
|
455
|
+
source_terms, electric_source_terms, magnetic_source_terms = resolved_source_terms
|
|
456
|
+
codes = {
|
|
457
|
+
axis: (
|
|
458
|
+
int(getattr(solver, f"boundary_{axis}_low_code")),
|
|
459
|
+
int(getattr(solver, f"boundary_{axis}_high_code")),
|
|
460
|
+
)
|
|
461
|
+
for axis in ("x", "y", "z")
|
|
462
|
+
}
|
|
463
|
+
mixed_bloch_cpml = (
|
|
464
|
+
sum(pair == (BOUNDARY_PML, BOUNDARY_PML) for pair in codes.values()) == 1
|
|
465
|
+
and sum(pair == (BOUNDARY_BLOCH, BOUNDARY_BLOCH) for pair in codes.values()) == 2
|
|
466
|
+
)
|
|
467
|
+
if getattr(solver, "boundary_kind", None) != "bloch" and not mixed_bloch_cpml:
|
|
468
|
+
return False
|
|
469
|
+
if electric_source_terms or magnetic_source_terms:
|
|
470
|
+
return False
|
|
471
|
+
return all(
|
|
472
|
+
term["cw_cos_patch"] is None
|
|
473
|
+
and term["delay_patch"] is None
|
|
474
|
+
and term.get("activation_delay_patch") is None
|
|
475
|
+
for term in source_terms
|
|
476
|
+
)
|
|
477
|
+
# Real fields: PEC domain faces are supported. The source-term eps-gradient
|
|
478
|
+
# in _accumulate_source_term_gradients applies the electric PEC mask so a
|
|
479
|
+
# source coinciding with a clamped face contributes zero.
|
|
480
|
+
return True
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _apply_resolved_magnetic_source_terms(
|
|
484
|
+
magnetic_fields,
|
|
485
|
+
*,
|
|
486
|
+
solver,
|
|
487
|
+
time_value,
|
|
488
|
+
resolved_source_terms,
|
|
489
|
+
):
|
|
490
|
+
if resolved_source_terms is None:
|
|
491
|
+
return magnetic_fields
|
|
492
|
+
_source_terms, _electric_source_terms, magnetic_source_terms = resolved_source_terms
|
|
493
|
+
if not magnetic_source_terms:
|
|
494
|
+
return magnetic_fields
|
|
495
|
+
return _apply_source_term_list(
|
|
496
|
+
magnetic_fields,
|
|
497
|
+
terms=magnetic_source_terms,
|
|
498
|
+
source_time=solver._source_time,
|
|
499
|
+
omega=solver.source_omega,
|
|
500
|
+
time_value=time_value,
|
|
501
|
+
solver=None,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _source_term_component_patches(term):
|
|
506
|
+
"""Ordered eps-dependent patches whose signal-weighted sum is the injected payload.
|
|
507
|
+
|
|
508
|
+
Each patch carries the term's permittivity dependence (the ``1/eps`` injection
|
|
509
|
+
coefficient and, for mode sources, the eigensolve profile). The time-signal
|
|
510
|
+
weights come from :func:`_source_term_signal_factors` in the same order.
|
|
511
|
+
"""
|
|
512
|
+
literal_patch = term.get("literal_patch")
|
|
513
|
+
if literal_patch is not None:
|
|
514
|
+
return (literal_patch,)
|
|
515
|
+
if term["cw_cos_patch"] is not None:
|
|
516
|
+
return (term["cw_cos_patch"], term["cw_sin_patch"])
|
|
517
|
+
return (term["patch"],)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _source_term_signal_factors(term, *, source_time, omega, time_value):
|
|
521
|
+
"""Time-signal weights (scalar or per-cell tensor) for the term's patches.
|
|
522
|
+
|
|
523
|
+
The weights are permittivity-independent, so the injected payload is
|
|
524
|
+
``sum_k patch_k * factor_k`` and its eps-derivative is carried entirely by the
|
|
525
|
+
patches. Order matches :func:`_source_term_component_patches`.
|
|
526
|
+
"""
|
|
527
|
+
# Each compiled source carries its own source-time waveform / CW frequency;
|
|
528
|
+
# resolve per-term so a second source at a different frequency is not driven
|
|
529
|
+
# by the primary source's spectrum.
|
|
530
|
+
source_time, omega = _resolve_term_source(term, source_time, omega)
|
|
531
|
+
if term.get("literal_patch") is not None:
|
|
532
|
+
return (1.0,)
|
|
533
|
+
if term["cw_cos_patch"] is not None:
|
|
534
|
+
signal_cos, signal_sin = _resolve_cw_signal(source_time, omega, time_value)
|
|
535
|
+
return (float(signal_cos), float(signal_sin))
|
|
536
|
+
if term["delay_patch"] is not None:
|
|
537
|
+
sample_time = float(time_value) - term["delay_patch"]
|
|
538
|
+
factor = _evaluate_source_time_tensor(source_time, sample_time)
|
|
539
|
+
activation_delay_patch = term.get("activation_delay_patch")
|
|
540
|
+
if activation_delay_patch is not None:
|
|
541
|
+
factor = torch.where(
|
|
542
|
+
activation_delay_patch > float(time_value),
|
|
543
|
+
torch.zeros_like(factor),
|
|
544
|
+
factor,
|
|
545
|
+
)
|
|
546
|
+
return (factor,)
|
|
547
|
+
scalar_signal = evaluate_source_time(source_time, float(time_value))
|
|
548
|
+
return (float(scalar_signal) * float(term["phase_real"]),)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _source_term_signal_patch(term, *, source_time, omega, time_value):
|
|
552
|
+
patches = _source_term_component_patches(term)
|
|
553
|
+
factors = _source_term_signal_factors(term, source_time=source_time, omega=omega, time_value=time_value)
|
|
554
|
+
payload = None
|
|
555
|
+
for patch, factor in zip(patches, factors):
|
|
556
|
+
contribution = patch * factor
|
|
557
|
+
payload = contribution if payload is None else payload + contribution
|
|
558
|
+
return payload
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _bloch_source_term_signal_patch(term, *, source_time, omega, time_value) -> torch.Tensor:
|
|
562
|
+
source_time, omega = _resolve_term_source(term, source_time, omega)
|
|
563
|
+
if term["cw_cos_patch"] is not None or term["delay_patch"] is not None:
|
|
564
|
+
raise NotImplementedError("Bloch source-term reverse currently supports uniform point-source patches only.")
|
|
565
|
+
scalar_signal = float(evaluate_source_time(source_time, float(time_value)))
|
|
566
|
+
patch = term["patch"]
|
|
567
|
+
patch_real = patch.to(dtype=torch.float32 if not torch.is_floating_point(patch) else patch.dtype)
|
|
568
|
+
return torch.complex(
|
|
569
|
+
patch_real * (scalar_signal * float(term["phase_real"])),
|
|
570
|
+
patch_real * (scalar_signal * float(term["phase_imag"])),
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _accumulate_mode_source_cotangents(
|
|
575
|
+
solver,
|
|
576
|
+
*,
|
|
577
|
+
electric_terms,
|
|
578
|
+
magnetic_terms,
|
|
579
|
+
electric_adjoint,
|
|
580
|
+
magnetic_adjoint,
|
|
581
|
+
source_time,
|
|
582
|
+
omega,
|
|
583
|
+
time_value,
|
|
584
|
+
) -> None:
|
|
585
|
+
"""Accumulate the time-weighted adjoint cotangent for profile source terms.
|
|
586
|
+
|
|
587
|
+
Mode-source patches carry an eigensolve eps-graph: the aperture mode profile
|
|
588
|
+
(and its normalization) depends on the source-plane permittivity. The per-step
|
|
589
|
+
source objective ``sum_t sum_k <patch_k, signal_k(t) * adjoint(t)>`` is linear
|
|
590
|
+
in the detached adjoint, and grad_eps is summed over the backward sweep anyway,
|
|
591
|
+
so accumulate ``sum_t signal_k(t) * adjoint(t)`` per patch here with a native
|
|
592
|
+
scatter-add and defer the single eigensolve VJP to
|
|
593
|
+
:func:`_mode_source_profile_pullback`. This keeps ``torch.autograd.grad`` off
|
|
594
|
+
the per-step hot path while reproducing the exact profile eps-dependence.
|
|
595
|
+
"""
|
|
596
|
+
accum = getattr(solver, "_mode_source_cotangent_accum", None)
|
|
597
|
+
if accum is None:
|
|
598
|
+
accum = {}
|
|
599
|
+
solver._mode_source_cotangent_accum = accum
|
|
600
|
+
groups = (
|
|
601
|
+
("E", electric_terms, electric_adjoint),
|
|
602
|
+
("H", magnetic_terms, magnetic_adjoint),
|
|
603
|
+
)
|
|
604
|
+
for group_name, terms, adjoint_by_field in groups:
|
|
605
|
+
for term_pos, term in enumerate(terms):
|
|
606
|
+
field_name = term["field_name"]
|
|
607
|
+
if field_name not in adjoint_by_field:
|
|
608
|
+
continue
|
|
609
|
+
offsets = term["offsets"]
|
|
610
|
+
patches = _source_term_component_patches(term)
|
|
611
|
+
factors = _source_term_signal_factors(
|
|
612
|
+
term, source_time=source_time, omega=omega, time_value=time_value
|
|
613
|
+
)
|
|
614
|
+
for comp_idx, (patch, factor) in enumerate(zip(patches, factors)):
|
|
615
|
+
region = _slice_from_offsets_shape(offsets, patch.shape)
|
|
616
|
+
adjoint_patch = adjoint_by_field[field_name][region].detach().to(
|
|
617
|
+
device=patch.device, dtype=patch.dtype
|
|
618
|
+
)
|
|
619
|
+
contribution = (adjoint_patch * factor).detach()
|
|
620
|
+
key = (group_name, term_pos, comp_idx)
|
|
621
|
+
existing = accum.get(key)
|
|
622
|
+
if existing is None:
|
|
623
|
+
accum[key] = contribution.clone()
|
|
624
|
+
else:
|
|
625
|
+
existing.add_(contribution)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _mode_source_profile_pullback(solver, resolved_source_terms, eps_ex, eps_ey, eps_ez):
|
|
629
|
+
"""Apply the accumulated profile cotangent through the eigensolve eps-graph once.
|
|
630
|
+
|
|
631
|
+
Runs a single ``torch.autograd.grad`` over the retained source-term graph at the
|
|
632
|
+
end of the backward pass (once-per-run material-pullback stage), returning the
|
|
633
|
+
per-axis eps gradients, or ``None`` when there is nothing to pull back.
|
|
634
|
+
"""
|
|
635
|
+
accum = getattr(solver, "_mode_source_cotangent_accum", None)
|
|
636
|
+
if not accum:
|
|
637
|
+
return None
|
|
638
|
+
source_terms, electric_source_terms, magnetic_source_terms = resolved_source_terms
|
|
639
|
+
grouped = (
|
|
640
|
+
("E", tuple(electric_source_terms) + tuple(source_terms)),
|
|
641
|
+
("H", tuple(magnetic_source_terms)),
|
|
642
|
+
)
|
|
643
|
+
with torch.enable_grad():
|
|
644
|
+
objective = None
|
|
645
|
+
for group_name, terms in grouped:
|
|
646
|
+
for term_pos, term in enumerate(terms):
|
|
647
|
+
patches = _source_term_component_patches(term)
|
|
648
|
+
for comp_idx, patch in enumerate(patches):
|
|
649
|
+
cotangent = accum.get((group_name, term_pos, comp_idx))
|
|
650
|
+
if cotangent is None:
|
|
651
|
+
continue
|
|
652
|
+
contribution = torch.sum(
|
|
653
|
+
patch * cotangent.to(device=patch.device, dtype=patch.dtype)
|
|
654
|
+
)
|
|
655
|
+
objective = contribution if objective is None else objective + contribution
|
|
656
|
+
if objective is None:
|
|
657
|
+
return None
|
|
658
|
+
return torch.autograd.grad(objective, (eps_ex, eps_ey, eps_ez), allow_unused=True)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _accumulate_source_term_gradients(
|
|
662
|
+
step_result: _ReverseStepResult,
|
|
663
|
+
*,
|
|
664
|
+
solver,
|
|
665
|
+
adjoint_state,
|
|
666
|
+
time_value,
|
|
667
|
+
eps_ex,
|
|
668
|
+
eps_ey,
|
|
669
|
+
eps_ez,
|
|
670
|
+
resolved_source_terms,
|
|
671
|
+
) -> _ReverseStepResult:
|
|
672
|
+
if not _has_resolved_source_terms(resolved_source_terms):
|
|
673
|
+
return step_result
|
|
674
|
+
if not _can_use_explicit_source_term_reverse_step(solver, resolved_source_terms):
|
|
675
|
+
return step_result
|
|
676
|
+
|
|
677
|
+
source_terms, electric_source_terms, magnetic_source_terms = resolved_source_terms
|
|
678
|
+
source_adjoint_state = step_result.source_adjoint_state or adjoint_state
|
|
679
|
+
if _scene_has_mode_source(solver):
|
|
680
|
+
# Mode-source patches (and any other rebuilt patches sharing the scene)
|
|
681
|
+
# carry an eigensolve eps-graph. Instead of running torch.autograd.grad on
|
|
682
|
+
# the per-step hot path, accumulate the time-weighted adjoint cotangent with
|
|
683
|
+
# a native scatter-add here; the single eigensolve VJP is applied once per
|
|
684
|
+
# backward pass by _mode_source_profile_pullback (bridge material-pullback
|
|
685
|
+
# stage). The per-step step_result eps-grad is returned unchanged.
|
|
686
|
+
_accumulate_mode_source_cotangents(
|
|
687
|
+
solver,
|
|
688
|
+
electric_terms=tuple(electric_source_terms) + tuple(source_terms),
|
|
689
|
+
magnetic_terms=tuple(magnetic_source_terms),
|
|
690
|
+
electric_adjoint=source_adjoint_state,
|
|
691
|
+
magnetic_adjoint=step_result.magnetic_output_adjoint or {},
|
|
692
|
+
source_time=solver._source_time,
|
|
693
|
+
omega=solver.source_omega,
|
|
694
|
+
time_value=time_value,
|
|
695
|
+
)
|
|
696
|
+
return step_result
|
|
697
|
+
|
|
698
|
+
grad_eps_by_field = {
|
|
699
|
+
"Ex": step_result.grad_eps_ex.detach().clone(),
|
|
700
|
+
"Ey": step_result.grad_eps_ey.detach().clone(),
|
|
701
|
+
"Ez": step_result.grad_eps_ez.detach().clone(),
|
|
702
|
+
}
|
|
703
|
+
eps_by_field = {
|
|
704
|
+
"Ex": eps_ex,
|
|
705
|
+
"Ey": eps_ey,
|
|
706
|
+
"Ez": eps_ez,
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
for term in tuple(electric_source_terms) + tuple(source_terms):
|
|
710
|
+
field_name = term["field_name"]
|
|
711
|
+
if field_name not in grad_eps_by_field:
|
|
712
|
+
continue
|
|
713
|
+
if has_complex_fields(solver):
|
|
714
|
+
payload = _bloch_source_term_signal_patch(
|
|
715
|
+
term,
|
|
716
|
+
source_time=solver._source_time,
|
|
717
|
+
omega=solver.source_omega,
|
|
718
|
+
time_value=time_value,
|
|
719
|
+
)
|
|
720
|
+
offsets = term["offsets"]
|
|
721
|
+
region = _slice_from_offsets_shape(offsets, payload.shape)
|
|
722
|
+
adjoint_patch = _bloch_source_term_adjoint_patch(
|
|
723
|
+
source_adjoint_state,
|
|
724
|
+
solver=solver,
|
|
725
|
+
field_name=field_name,
|
|
726
|
+
offsets=offsets,
|
|
727
|
+
patch_shape=payload.shape,
|
|
728
|
+
)
|
|
729
|
+
eps_patch = eps_by_field[field_name][region]
|
|
730
|
+
grad_eps_by_field[field_name][region] = (
|
|
731
|
+
grad_eps_by_field[field_name][region] - _complex_inner_real(adjoint_patch, payload) / eps_patch
|
|
732
|
+
)
|
|
733
|
+
continue
|
|
734
|
+
payload = _source_term_signal_patch(
|
|
735
|
+
term,
|
|
736
|
+
source_time=solver._source_time,
|
|
737
|
+
omega=solver.source_omega,
|
|
738
|
+
time_value=time_value,
|
|
739
|
+
)
|
|
740
|
+
offsets = term["offsets"]
|
|
741
|
+
region = _slice_from_offsets_shape(offsets, payload.shape)
|
|
742
|
+
adjoint_field = source_adjoint_state[field_name]
|
|
743
|
+
adjoint_patch = adjoint_field[region]
|
|
744
|
+
# A source cell on a PEC domain face is erased by the terminal forward
|
|
745
|
+
# PEC clamp, so its eps-gradient must be zero; mask the incoming adjoint
|
|
746
|
+
# exactly as the forward clamp zeroes the injected field (no-op interior).
|
|
747
|
+
pec_mask = _electric_component_pec_mask(
|
|
748
|
+
solver, field_name, adjoint_field.shape, adjoint_field.device
|
|
749
|
+
)
|
|
750
|
+
if pec_mask is not None:
|
|
751
|
+
adjoint_patch = torch.where(
|
|
752
|
+
pec_mask[region], torch.zeros_like(adjoint_patch), adjoint_patch
|
|
753
|
+
)
|
|
754
|
+
eps_patch = eps_by_field[field_name][region]
|
|
755
|
+
grad_eps_by_field[field_name][region] = (
|
|
756
|
+
grad_eps_by_field[field_name][region] - (adjoint_patch * payload) / eps_patch
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
return _ReverseStepResult(
|
|
760
|
+
pre_step_adjoint=step_result.pre_step_adjoint,
|
|
761
|
+
grad_eps_ex=grad_eps_by_field["Ex"],
|
|
762
|
+
grad_eps_ey=grad_eps_by_field["Ey"],
|
|
763
|
+
grad_eps_ez=grad_eps_by_field["Ez"],
|
|
764
|
+
backend=step_result.backend,
|
|
765
|
+
source_adjoint_state=step_result.source_adjoint_state,
|
|
766
|
+
magnetic_output_adjoint=step_result.magnetic_output_adjoint,
|
|
767
|
+
# Preserve the nonlinear gradient channels across the source-term eps
|
|
768
|
+
# accumulation (the source injection carries no chi2/chi3/tpa dependence,
|
|
769
|
+
# so these pass through untouched); the analytic Kerr / general-nonlinear
|
|
770
|
+
# reverses populate them and the bridge requires them per step.
|
|
771
|
+
grad_chi3_ex=step_result.grad_chi3_ex,
|
|
772
|
+
grad_chi3_ey=step_result.grad_chi3_ey,
|
|
773
|
+
grad_chi3_ez=step_result.grad_chi3_ez,
|
|
774
|
+
grad_chi2_ex=step_result.grad_chi2_ex,
|
|
775
|
+
grad_chi2_ey=step_result.grad_chi2_ey,
|
|
776
|
+
grad_chi2_ez=step_result.grad_chi2_ez,
|
|
777
|
+
grad_tpa_ex=step_result.grad_tpa_ex,
|
|
778
|
+
grad_tpa_ey=step_result.grad_tpa_ey,
|
|
779
|
+
grad_tpa_ez=step_result.grad_tpa_ez,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
@dataclass(frozen=True)
|
|
784
|
+
class _ForwardPack:
|
|
785
|
+
field_names: tuple[str, ...]
|
|
786
|
+
monitor_templates: dict[str, dict[str, Any]]
|
|
787
|
+
output_tensors: tuple[torch.Tensor, ...]
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _add_patch(field: torch.Tensor, offsets, patch: torch.Tensor) -> torch.Tensor:
|
|
791
|
+
region = _slice_from_offsets_shape(offsets, patch.shape)
|
|
792
|
+
updated = field.clone()
|
|
793
|
+
updated[region] = updated[region] + patch.to(device=field.device, dtype=field.dtype)
|
|
794
|
+
return updated
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def _complex_phase_positive(value: torch.Tensor, phase_cos: float, phase_sin: float) -> torch.Tensor:
|
|
798
|
+
return torch.complex(
|
|
799
|
+
float(phase_cos) * value.real - float(phase_sin) * value.imag,
|
|
800
|
+
float(phase_sin) * value.real + float(phase_cos) * value.imag,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _complex_phase_negative(value: torch.Tensor, phase_cos: float, phase_sin: float) -> torch.Tensor:
|
|
805
|
+
return torch.complex(
|
|
806
|
+
float(phase_cos) * value.real + float(phase_sin) * value.imag,
|
|
807
|
+
float(phase_cos) * value.imag - float(phase_sin) * value.real,
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _complex_inner_real(lhs: torch.Tensor, rhs: torch.Tensor) -> torch.Tensor:
|
|
812
|
+
return lhs.real * rhs.real + lhs.imag * rhs.imag
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _axis_uses_complex_source_wrap(solver, axis: int) -> bool:
|
|
816
|
+
scene = getattr(solver, "scene", None)
|
|
817
|
+
boundary = None if scene is None else getattr(scene, "boundary", None)
|
|
818
|
+
if boundary is None:
|
|
819
|
+
return getattr(solver, "boundary_kind", None) == "bloch"
|
|
820
|
+
return boundary.axis_kind("xyz"[int(axis)]) in {"bloch", "periodic"}
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _bloch_phase_specs(solver, field_name: str):
|
|
824
|
+
component = field_name[-1]
|
|
825
|
+
if component == "x":
|
|
826
|
+
return (
|
|
827
|
+
(1, float(solver.boundary_phase_cos[1]), float(solver.boundary_phase_sin[1])),
|
|
828
|
+
(2, float(solver.boundary_phase_cos[2]), float(solver.boundary_phase_sin[2])),
|
|
829
|
+
)
|
|
830
|
+
if component == "y":
|
|
831
|
+
return (
|
|
832
|
+
(0, float(solver.boundary_phase_cos[0]), float(solver.boundary_phase_sin[0])),
|
|
833
|
+
(2, float(solver.boundary_phase_cos[2]), float(solver.boundary_phase_sin[2])),
|
|
834
|
+
)
|
|
835
|
+
if component == "z":
|
|
836
|
+
return (
|
|
837
|
+
(0, float(solver.boundary_phase_cos[0]), float(solver.boundary_phase_sin[0])),
|
|
838
|
+
(1, float(solver.boundary_phase_cos[1]), float(solver.boundary_phase_sin[1])),
|
|
839
|
+
)
|
|
840
|
+
raise ValueError(f"Unsupported Bloch source field {field_name!r}.")
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _bloch_source_boundary_terms(solver, field_name: str, offsets, patch_shape, field_shape):
|
|
844
|
+
boundary_terms = []
|
|
845
|
+
for axis, phase_cos, phase_sin in _bloch_phase_specs(solver, field_name):
|
|
846
|
+
if not _axis_uses_complex_source_wrap(solver, axis):
|
|
847
|
+
continue
|
|
848
|
+
axis_size = int(field_shape[axis])
|
|
849
|
+
start = int(offsets[axis])
|
|
850
|
+
end = start + int(patch_shape[axis])
|
|
851
|
+
if start == 0:
|
|
852
|
+
boundary_terms.append((axis, "low", axis_size - 1, phase_cos, phase_sin))
|
|
853
|
+
elif end == axis_size:
|
|
854
|
+
boundary_terms.append((axis, "high", 0, phase_cos, phase_sin))
|
|
855
|
+
return boundary_terms
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _bloch_source_term_adjoint_patch(adjoint_state, *, solver, field_name: str, offsets, patch_shape):
|
|
859
|
+
adjoint_field = torch.complex(adjoint_state[field_name], adjoint_state[f"{field_name}_imag"])
|
|
860
|
+
region = _slice_from_offsets_shape(offsets, patch_shape)
|
|
861
|
+
adjoint_patch = adjoint_field[region].clone()
|
|
862
|
+
boundary_terms = _bloch_source_boundary_terms(
|
|
863
|
+
solver,
|
|
864
|
+
field_name,
|
|
865
|
+
offsets,
|
|
866
|
+
patch_shape,
|
|
867
|
+
adjoint_field.shape,
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
for axis, side, pair_index, phase_cos, phase_sin in boundary_terms:
|
|
871
|
+
paired_offsets = list(offsets)
|
|
872
|
+
paired_offsets[axis] = pair_index
|
|
873
|
+
paired_shape = list(patch_shape)
|
|
874
|
+
paired_shape[axis] = 1
|
|
875
|
+
paired_patch = adjoint_field[_slice_from_offsets_shape(tuple(paired_offsets), tuple(paired_shape))]
|
|
876
|
+
boundary_adjoint = (
|
|
877
|
+
_complex_phase_negative(paired_patch, phase_cos, phase_sin)
|
|
878
|
+
if side == "low"
|
|
879
|
+
else _complex_phase_positive(paired_patch, phase_cos, phase_sin)
|
|
880
|
+
)
|
|
881
|
+
boundary_index = 0 if side == "low" else int(adjoint_patch.shape[axis] - 1)
|
|
882
|
+
adjoint_patch.narrow(axis, boundary_index, 1).add_(boundary_adjoint)
|
|
883
|
+
|
|
884
|
+
if len(boundary_terms) == 2:
|
|
885
|
+
(axis_a, side_a, pair_a, phase_cos_a, phase_sin_a), (axis_b, side_b, pair_b, phase_cos_b, phase_sin_b) = boundary_terms
|
|
886
|
+
paired_offsets = list(offsets)
|
|
887
|
+
paired_offsets[axis_a] = pair_a
|
|
888
|
+
paired_offsets[axis_b] = pair_b
|
|
889
|
+
paired_shape = list(patch_shape)
|
|
890
|
+
paired_shape[axis_a] = 1
|
|
891
|
+
paired_shape[axis_b] = 1
|
|
892
|
+
corner_patch = adjoint_field[_slice_from_offsets_shape(tuple(paired_offsets), tuple(paired_shape))]
|
|
893
|
+
corner_patch = (
|
|
894
|
+
_complex_phase_negative(corner_patch, phase_cos_b, phase_sin_b)
|
|
895
|
+
if side_b == "low"
|
|
896
|
+
else _complex_phase_positive(corner_patch, phase_cos_b, phase_sin_b)
|
|
897
|
+
)
|
|
898
|
+
corner_patch = (
|
|
899
|
+
_complex_phase_negative(corner_patch, phase_cos_a, phase_sin_a)
|
|
900
|
+
if side_a == "low"
|
|
901
|
+
else _complex_phase_positive(corner_patch, phase_cos_a, phase_sin_a)
|
|
902
|
+
)
|
|
903
|
+
corner_index_a = 0 if side_a == "low" else int(adjoint_patch.shape[axis_a] - 1)
|
|
904
|
+
corner_index_b = 0 if side_b == "low" else int(adjoint_patch.shape[axis_b] - 1)
|
|
905
|
+
adjoint_patch.narrow(axis_a, corner_index_a, 1).narrow(axis_b, corner_index_b, 1).add_(corner_patch)
|
|
906
|
+
|
|
907
|
+
return adjoint_patch
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _add_complex_bloch_source_patch(field_mapping, *, solver, field_name: str, offsets, delta: torch.Tensor):
|
|
911
|
+
real_name = field_name
|
|
912
|
+
imag_name = f"{field_name}_imag"
|
|
913
|
+
if imag_name not in field_mapping:
|
|
914
|
+
raise KeyError(f"Bloch source injection requires {imag_name!r} in the replay state.")
|
|
915
|
+
|
|
916
|
+
field = torch.complex(field_mapping[real_name], field_mapping[imag_name])
|
|
917
|
+
delta = delta.to(device=field.device, dtype=field.dtype)
|
|
918
|
+
field = _add_patch(field, offsets, delta)
|
|
919
|
+
|
|
920
|
+
boundary_terms = _bloch_source_boundary_terms(
|
|
921
|
+
solver,
|
|
922
|
+
field_name,
|
|
923
|
+
offsets,
|
|
924
|
+
delta.shape,
|
|
925
|
+
field.shape,
|
|
926
|
+
)
|
|
927
|
+
for axis, side, pair_index, phase_cos, phase_sin in boundary_terms:
|
|
928
|
+
boundary_patch = _boundary_slice(delta, axis, side)
|
|
929
|
+
phased_patch = (
|
|
930
|
+
_complex_phase_positive(boundary_patch, phase_cos, phase_sin)
|
|
931
|
+
if side == "low"
|
|
932
|
+
else _complex_phase_negative(boundary_patch, phase_cos, phase_sin)
|
|
933
|
+
)
|
|
934
|
+
paired_offsets = list(offsets)
|
|
935
|
+
paired_offsets[axis] = pair_index
|
|
936
|
+
field = _add_patch(field, tuple(paired_offsets), phased_patch)
|
|
937
|
+
|
|
938
|
+
if len(boundary_terms) == 2:
|
|
939
|
+
(axis_a, side_a, pair_a, phase_cos_a, phase_sin_a), (axis_b, side_b, pair_b, phase_cos_b, phase_sin_b) = boundary_terms
|
|
940
|
+
corner_patch = _boundary_slice(_boundary_slice(delta, axis_a, side_a), axis_b, side_b)
|
|
941
|
+
corner_patch = (
|
|
942
|
+
_complex_phase_positive(corner_patch, phase_cos_a, phase_sin_a)
|
|
943
|
+
if side_a == "low"
|
|
944
|
+
else _complex_phase_negative(corner_patch, phase_cos_a, phase_sin_a)
|
|
945
|
+
)
|
|
946
|
+
corner_patch = (
|
|
947
|
+
_complex_phase_positive(corner_patch, phase_cos_b, phase_sin_b)
|
|
948
|
+
if side_b == "low"
|
|
949
|
+
else _complex_phase_negative(corner_patch, phase_cos_b, phase_sin_b)
|
|
950
|
+
)
|
|
951
|
+
paired_offsets = list(offsets)
|
|
952
|
+
paired_offsets[axis_a] = pair_a
|
|
953
|
+
paired_offsets[axis_b] = pair_b
|
|
954
|
+
field = _add_patch(field, tuple(paired_offsets), corner_patch)
|
|
955
|
+
|
|
956
|
+
updated = dict(field_mapping)
|
|
957
|
+
updated[real_name] = field.real
|
|
958
|
+
updated[imag_name] = field.imag
|
|
959
|
+
return updated
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _boundary_slice(patch: torch.Tensor, axis: int, side: str) -> torch.Tensor:
|
|
963
|
+
index = 0 if side == "low" else int(patch.shape[axis] - 1)
|
|
964
|
+
return patch.narrow(axis, index, 1)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _apply_bloch_uniform_source_term(
|
|
968
|
+
field_mapping,
|
|
969
|
+
*,
|
|
970
|
+
solver,
|
|
971
|
+
field_name: str,
|
|
972
|
+
offsets,
|
|
973
|
+
patch: torch.Tensor,
|
|
974
|
+
signal_real: float,
|
|
975
|
+
signal_imag: float,
|
|
976
|
+
):
|
|
977
|
+
real_name = field_name
|
|
978
|
+
imag_name = f"{field_name}_imag"
|
|
979
|
+
if imag_name not in field_mapping:
|
|
980
|
+
raise KeyError(f"Bloch source injection requires {imag_name!r} in the replay state.")
|
|
981
|
+
|
|
982
|
+
patch_real = patch.to(device=field_mapping[real_name].device, dtype=field_mapping[real_name].dtype)
|
|
983
|
+
delta = torch.complex(patch_real * float(signal_real), patch_real * float(signal_imag))
|
|
984
|
+
return _add_complex_bloch_source_patch(
|
|
985
|
+
field_mapping,
|
|
986
|
+
solver=solver,
|
|
987
|
+
field_name=field_name,
|
|
988
|
+
offsets=offsets,
|
|
989
|
+
delta=delta,
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def _apply_bloch_cw_source_term(
|
|
994
|
+
field_mapping,
|
|
995
|
+
*,
|
|
996
|
+
solver,
|
|
997
|
+
field_name: str,
|
|
998
|
+
offsets,
|
|
999
|
+
cw_cos_patch: torch.Tensor,
|
|
1000
|
+
cw_sin_patch: torch.Tensor,
|
|
1001
|
+
signal_cos: float,
|
|
1002
|
+
signal_sin: float,
|
|
1003
|
+
):
|
|
1004
|
+
patch_cos = cw_cos_patch.to(device=field_mapping[field_name].device, dtype=field_mapping[field_name].dtype)
|
|
1005
|
+
patch_sin = cw_sin_patch.to(device=field_mapping[field_name].device, dtype=field_mapping[field_name].dtype)
|
|
1006
|
+
delta = torch.complex(
|
|
1007
|
+
float(signal_cos) * patch_cos + float(signal_sin) * patch_sin,
|
|
1008
|
+
float(signal_sin) * patch_cos - float(signal_cos) * patch_sin,
|
|
1009
|
+
)
|
|
1010
|
+
return _add_complex_bloch_source_patch(
|
|
1011
|
+
field_mapping,
|
|
1012
|
+
solver=solver,
|
|
1013
|
+
field_name=field_name,
|
|
1014
|
+
offsets=offsets,
|
|
1015
|
+
delta=delta,
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _sample_auxiliary_field(field: torch.Tensor, *, origin: float, ds: float, positions: torch.Tensor) -> torch.Tensor:
|
|
1020
|
+
coord = torch.clamp((positions.to(device=field.device, dtype=field.dtype) - float(origin)) / float(ds), min=0.0)
|
|
1021
|
+
coord = torch.clamp(coord, max=max(field.numel() - 1, 0))
|
|
1022
|
+
lower = torch.floor(coord).to(torch.int64)
|
|
1023
|
+
upper = torch.clamp(lower + 1, max=max(field.numel() - 1, 0))
|
|
1024
|
+
frac = coord - lower.to(dtype=field.dtype)
|
|
1025
|
+
return field[lower] + (field[upper] - field[lower]) * frac
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def _apply_tfsf_reference_terms(field_mapping, terms, *, auxiliary_field: torch.Tensor):
|
|
1029
|
+
updated = dict(field_mapping)
|
|
1030
|
+
for term in terms:
|
|
1031
|
+
if term["scalar_sample_index"] is not None:
|
|
1032
|
+
incident = auxiliary_field[int(term["scalar_sample_index"])]
|
|
1033
|
+
patch = term["coeff_patch"] * incident
|
|
1034
|
+
else:
|
|
1035
|
+
incident_line = auxiliary_field[term["sample_indices"].to(dtype=torch.long)]
|
|
1036
|
+
patch = term["coeff_patch"] * incident_line.view(term["sample_view"])
|
|
1037
|
+
patch = patch * float(term["component_scale"])
|
|
1038
|
+
updated[term["field_name"]] = _add_patch(updated[term["field_name"]], term["offsets"], patch)
|
|
1039
|
+
return updated
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def _apply_tfsf_aux_terms(field_mapping, terms, *, auxiliary_field: torch.Tensor, origin: float, ds: float):
|
|
1043
|
+
updated = dict(field_mapping)
|
|
1044
|
+
for term in terms:
|
|
1045
|
+
incident = _sample_auxiliary_field(auxiliary_field, origin=origin, ds=ds, positions=term["sample_positions"])
|
|
1046
|
+
patch = term["coeff_patch"] * incident
|
|
1047
|
+
patch = patch * float(term["component_scale"])
|
|
1048
|
+
updated[term["field_name"]] = _add_patch(updated[term["field_name"]], term["offsets"], patch)
|
|
1049
|
+
return updated
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _extract_tfsf_auxiliary_state(state):
|
|
1053
|
+
if "tfsf_aux_electric" not in state or "tfsf_aux_magnetic" not in state:
|
|
1054
|
+
return None
|
|
1055
|
+
return {
|
|
1056
|
+
"electric": state["tfsf_aux_electric"],
|
|
1057
|
+
"magnetic": state["tfsf_aux_magnetic"],
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def _apply_tfsf_terms(field_mapping, *, solver, auxiliary_state, term_key: str, sample_kind: str, time_value):
|
|
1062
|
+
if not getattr(solver, "tfsf_enabled", False):
|
|
1063
|
+
return field_mapping
|
|
1064
|
+
|
|
1065
|
+
tfsf_state = getattr(solver, "_tfsf_state", None)
|
|
1066
|
+
if tfsf_state is None:
|
|
1067
|
+
return field_mapping
|
|
1068
|
+
|
|
1069
|
+
provider = tfsf_state.get("provider")
|
|
1070
|
+
terms = tfsf_state.get(term_key, ())
|
|
1071
|
+
if provider in _TFSF_REFERENCE_PROVIDERS:
|
|
1072
|
+
if auxiliary_state is None:
|
|
1073
|
+
return field_mapping
|
|
1074
|
+
auxiliary_field = auxiliary_state["electric" if sample_kind == "electric" else "magnetic"]
|
|
1075
|
+
return _apply_tfsf_reference_terms(field_mapping, terms, auxiliary_field=auxiliary_field)
|
|
1076
|
+
|
|
1077
|
+
if provider == "plane_wave_aux":
|
|
1078
|
+
if auxiliary_state is None:
|
|
1079
|
+
return field_mapping
|
|
1080
|
+
auxiliary_grid = tfsf_state["auxiliary_grid"]
|
|
1081
|
+
if sample_kind == "electric":
|
|
1082
|
+
auxiliary_field = auxiliary_state["electric"]
|
|
1083
|
+
origin = float(auxiliary_grid.s_min)
|
|
1084
|
+
else:
|
|
1085
|
+
auxiliary_field = auxiliary_state["magnetic"]
|
|
1086
|
+
origin = float(auxiliary_grid.s_min + 0.5 * auxiliary_grid.ds)
|
|
1087
|
+
return _apply_tfsf_aux_terms(
|
|
1088
|
+
field_mapping,
|
|
1089
|
+
terms,
|
|
1090
|
+
auxiliary_field=auxiliary_field,
|
|
1091
|
+
origin=origin,
|
|
1092
|
+
ds=float(auxiliary_grid.ds),
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
return _apply_source_term_list(
|
|
1096
|
+
field_mapping,
|
|
1097
|
+
terms=terms,
|
|
1098
|
+
source_time=solver._source_time,
|
|
1099
|
+
omega=solver.source_omega,
|
|
1100
|
+
time_value=time_value,
|
|
1101
|
+
solver=solver if has_complex_fields(solver) else None,
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
def _advance_tfsf_auxiliary_magnetic_state(solver, auxiliary_state):
|
|
1106
|
+
if auxiliary_state is None:
|
|
1107
|
+
return None
|
|
1108
|
+
auxiliary_grid = getattr(solver, "_tfsf_state", {}).get("auxiliary_grid")
|
|
1109
|
+
if auxiliary_grid is None:
|
|
1110
|
+
return auxiliary_state
|
|
1111
|
+
electric = auxiliary_state["electric"]
|
|
1112
|
+
magnetic = auxiliary_state["magnetic"]
|
|
1113
|
+
if magnetic.numel() == 0:
|
|
1114
|
+
return {"electric": electric, "magnetic": magnetic}
|
|
1115
|
+
curl_e = electric[1:] - electric[:-1]
|
|
1116
|
+
next_magnetic = auxiliary_grid.magnetic_decay * magnetic - auxiliary_grid.magnetic_curl * curl_e
|
|
1117
|
+
return {"electric": electric, "magnetic": next_magnetic}
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def _advance_tfsf_auxiliary_electric_state(solver, auxiliary_state, *, time_value):
|
|
1121
|
+
if auxiliary_state is None:
|
|
1122
|
+
return None
|
|
1123
|
+
auxiliary_grid = getattr(solver, "_tfsf_state", {}).get("auxiliary_grid")
|
|
1124
|
+
if auxiliary_grid is None:
|
|
1125
|
+
return auxiliary_state
|
|
1126
|
+
electric = auxiliary_state["electric"]
|
|
1127
|
+
magnetic = auxiliary_state["magnetic"]
|
|
1128
|
+
next_electric = electric.clone()
|
|
1129
|
+
if electric.numel() > 2:
|
|
1130
|
+
curl_h = magnetic[1:] - magnetic[:-1]
|
|
1131
|
+
next_electric[1:-1] = (
|
|
1132
|
+
auxiliary_grid.electric_decay[1:-1] * electric[1:-1]
|
|
1133
|
+
- auxiliary_grid.electric_curl[1:-1] * curl_h
|
|
1134
|
+
)
|
|
1135
|
+
next_electric[int(auxiliary_grid.source_index)] = evaluate_source_time(auxiliary_grid.source_time, float(time_value))
|
|
1136
|
+
next_electric[-1] = 0.0
|
|
1137
|
+
return {"electric": next_electric, "magnetic": magnetic}
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _literal_source_term(*, field_name: str, offsets, patch: torch.Tensor) -> dict[str, Any]:
|
|
1141
|
+
return {
|
|
1142
|
+
"field_name": field_name,
|
|
1143
|
+
"offsets": offsets,
|
|
1144
|
+
"patch": None,
|
|
1145
|
+
"grid": None,
|
|
1146
|
+
"phase_real": 1.0,
|
|
1147
|
+
"phase_imag": 0.0,
|
|
1148
|
+
"delay_patch": None,
|
|
1149
|
+
"activation_delay_patch": None,
|
|
1150
|
+
"cw_cos_patch": None,
|
|
1151
|
+
"cw_sin_patch": None,
|
|
1152
|
+
"literal_patch": patch.detach().contiguous(),
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def _materialize_tfsf_term_patch(
|
|
1157
|
+
solver,
|
|
1158
|
+
term,
|
|
1159
|
+
*,
|
|
1160
|
+
auxiliary_state,
|
|
1161
|
+
sample_kind: str,
|
|
1162
|
+
time_value,
|
|
1163
|
+
) -> torch.Tensor:
|
|
1164
|
+
if "coeff_patch" not in term:
|
|
1165
|
+
return _source_term_signal_patch(
|
|
1166
|
+
term,
|
|
1167
|
+
source_time=solver._source_time,
|
|
1168
|
+
omega=solver.source_omega,
|
|
1169
|
+
time_value=time_value,
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
if auxiliary_state is None:
|
|
1173
|
+
raise RuntimeError("TFSF auxiliary terms require frozen auxiliary checkpoint state.")
|
|
1174
|
+
|
|
1175
|
+
auxiliary_field = auxiliary_state[sample_kind]
|
|
1176
|
+
if "sample_positions" in term:
|
|
1177
|
+
auxiliary_grid = getattr(solver, "_tfsf_state", {}).get("auxiliary_grid")
|
|
1178
|
+
if auxiliary_grid is None:
|
|
1179
|
+
raise RuntimeError("Auxiliary-position TFSF terms require an auxiliary grid.")
|
|
1180
|
+
if sample_kind == "electric":
|
|
1181
|
+
origin = float(auxiliary_grid.s_min)
|
|
1182
|
+
else:
|
|
1183
|
+
origin = float(auxiliary_grid.s_min + 0.5 * auxiliary_grid.ds)
|
|
1184
|
+
incident = _sample_auxiliary_field(
|
|
1185
|
+
auxiliary_field,
|
|
1186
|
+
origin=origin,
|
|
1187
|
+
ds=float(auxiliary_grid.ds),
|
|
1188
|
+
positions=term["sample_positions"],
|
|
1189
|
+
)
|
|
1190
|
+
patch = term["coeff_patch"] * incident
|
|
1191
|
+
elif term["scalar_sample_index"] is not None:
|
|
1192
|
+
patch = term["coeff_patch"] * auxiliary_field[int(term["scalar_sample_index"])]
|
|
1193
|
+
else:
|
|
1194
|
+
incident_line = auxiliary_field[term["sample_indices"].to(dtype=torch.long)]
|
|
1195
|
+
patch = term["coeff_patch"] * incident_line.view(term["sample_view"])
|
|
1196
|
+
return patch * float(term["component_scale"])
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def _materialize_tfsf_source_terms(
|
|
1200
|
+
solver,
|
|
1201
|
+
*,
|
|
1202
|
+
terms,
|
|
1203
|
+
auxiliary_state,
|
|
1204
|
+
sample_kind: str,
|
|
1205
|
+
time_value,
|
|
1206
|
+
) -> list[dict[str, Any]]:
|
|
1207
|
+
materialized_terms = []
|
|
1208
|
+
for term in terms:
|
|
1209
|
+
patch = _materialize_tfsf_term_patch(
|
|
1210
|
+
solver,
|
|
1211
|
+
term,
|
|
1212
|
+
auxiliary_state=auxiliary_state,
|
|
1213
|
+
sample_kind=sample_kind,
|
|
1214
|
+
time_value=time_value,
|
|
1215
|
+
)
|
|
1216
|
+
materialized_terms.append(
|
|
1217
|
+
_literal_source_term(
|
|
1218
|
+
field_name=term["field_name"],
|
|
1219
|
+
offsets=term["offsets"],
|
|
1220
|
+
patch=patch,
|
|
1221
|
+
)
|
|
1222
|
+
)
|
|
1223
|
+
return materialized_terms
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
def _merge_resolved_source_terms(
|
|
1227
|
+
resolved_source_terms,
|
|
1228
|
+
*,
|
|
1229
|
+
source_terms=(),
|
|
1230
|
+
electric_source_terms=(),
|
|
1231
|
+
magnetic_source_terms=(),
|
|
1232
|
+
):
|
|
1233
|
+
if resolved_source_terms is None:
|
|
1234
|
+
base_source_terms = ()
|
|
1235
|
+
base_electric_source_terms = ()
|
|
1236
|
+
base_magnetic_source_terms = ()
|
|
1237
|
+
else:
|
|
1238
|
+
base_source_terms, base_electric_source_terms, base_magnetic_source_terms = resolved_source_terms
|
|
1239
|
+
return (
|
|
1240
|
+
tuple(base_source_terms) + tuple(source_terms),
|
|
1241
|
+
tuple(base_electric_source_terms) + tuple(electric_source_terms),
|
|
1242
|
+
tuple(base_magnetic_source_terms) + tuple(magnetic_source_terms),
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def _tfsf_magnetic_source_terms(solver, forward_state, *, time_value, resolved_source_terms=None):
|
|
1247
|
+
tfsf_state = getattr(solver, "_tfsf_state", None)
|
|
1248
|
+
if tfsf_state is None:
|
|
1249
|
+
return resolved_source_terms
|
|
1250
|
+
auxiliary_state = _extract_tfsf_auxiliary_state(forward_state)
|
|
1251
|
+
magnetic_terms = _materialize_tfsf_source_terms(
|
|
1252
|
+
solver,
|
|
1253
|
+
terms=tfsf_state.get("magnetic_terms", ()),
|
|
1254
|
+
auxiliary_state=auxiliary_state,
|
|
1255
|
+
sample_kind="electric",
|
|
1256
|
+
time_value=time_value,
|
|
1257
|
+
)
|
|
1258
|
+
return _merge_resolved_source_terms(
|
|
1259
|
+
resolved_source_terms,
|
|
1260
|
+
magnetic_source_terms=magnetic_terms,
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def _accumulate_tfsf_sample_adjoint_torch(adj_aux, terms, adjoint_fields, *, origin, ds):
|
|
1265
|
+
"""Transpose of the per-step TFSF incident injection into ``adj_aux``.
|
|
1266
|
+
|
|
1267
|
+
For every injection term this reads the adjoint of the field the forward step
|
|
1268
|
+
wrote the incident patch into (``adjoint_fields[field_name]`` at the term
|
|
1269
|
+
offsets), contracts it with the term coefficient patch and component scale,
|
|
1270
|
+
and scatters the result back onto the sampled 1D auxiliary grid. This mirrors
|
|
1271
|
+
the native ``accumulateTfsf*SampleAdjoint3D`` kernels: scalar terms fold onto a
|
|
1272
|
+
single index, line terms reduce the two non-sample axes onto their index line,
|
|
1273
|
+
and interpolated terms distribute linearly onto the two straddling indices.
|
|
1274
|
+
"""
|
|
1275
|
+
for term in terms:
|
|
1276
|
+
field = adjoint_fields[term["field_name"]]
|
|
1277
|
+
offset_i, offset_j, offset_k = (int(offset) for offset in term["offsets"])
|
|
1278
|
+
coeff_patch = term["coeff_patch"]
|
|
1279
|
+
shape_i, shape_j, shape_k = (int(length) for length in coeff_patch.shape)
|
|
1280
|
+
adj_field_patch = field[
|
|
1281
|
+
offset_i : offset_i + shape_i,
|
|
1282
|
+
offset_j : offset_j + shape_j,
|
|
1283
|
+
offset_k : offset_k + shape_k,
|
|
1284
|
+
]
|
|
1285
|
+
component_scale = float(term["component_scale"])
|
|
1286
|
+
weighted = component_scale * adj_field_patch * coeff_patch
|
|
1287
|
+
if "sample_positions" in term:
|
|
1288
|
+
last_index = max(adj_aux.numel() - 1, 0)
|
|
1289
|
+
inv_ds = 1.0 / ds if ds > 0.0 else 0.0
|
|
1290
|
+
coord = torch.clamp(
|
|
1291
|
+
(term["sample_positions"] - origin) * inv_ds, min=0.0, max=float(last_index)
|
|
1292
|
+
)
|
|
1293
|
+
lower = torch.floor(coord).to(torch.int64)
|
|
1294
|
+
upper = torch.clamp(lower + 1, max=last_index)
|
|
1295
|
+
frac = coord - lower.to(coord.dtype)
|
|
1296
|
+
value = weighted.reshape(-1)
|
|
1297
|
+
lower_flat = lower.reshape(-1)
|
|
1298
|
+
upper_flat = upper.reshape(-1)
|
|
1299
|
+
frac_flat = frac.reshape(-1)
|
|
1300
|
+
adj_aux.index_add_(0, lower_flat, value * (1.0 - frac_flat))
|
|
1301
|
+
distinct = upper_flat != lower_flat
|
|
1302
|
+
if bool(distinct.any()):
|
|
1303
|
+
adj_aux.index_add_(0, upper_flat[distinct], (value * frac_flat)[distinct])
|
|
1304
|
+
continue
|
|
1305
|
+
if term["scalar_sample_index"] is not None:
|
|
1306
|
+
adj_aux[int(term["scalar_sample_index"])] += torch.sum(weighted)
|
|
1307
|
+
continue
|
|
1308
|
+
axis = int(reference_sample_axis_code(term))
|
|
1309
|
+
reduce_axes = tuple(other for other in (0, 1, 2) if other != axis)
|
|
1310
|
+
per_index = weighted.sum(dim=reduce_axes)
|
|
1311
|
+
sample_indices = term["sample_indices"].to(device=adj_aux.device, dtype=torch.int64).reshape(-1)
|
|
1312
|
+
adj_aux.index_add_(0, sample_indices, per_index)
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def _reverse_tfsf_auxiliary_electric_state_adjoint(
|
|
1316
|
+
adj_electric_prev,
|
|
1317
|
+
adj_magnetic_after,
|
|
1318
|
+
adj_electric_post,
|
|
1319
|
+
electric_decay,
|
|
1320
|
+
electric_curl,
|
|
1321
|
+
source_index,
|
|
1322
|
+
):
|
|
1323
|
+
"""Analytic transpose of ``_advance_tfsf_auxiliary_electric_state``.
|
|
1324
|
+
|
|
1325
|
+
Accumulates the pre-step electric adjoint (decay pullback on the interior, an
|
|
1326
|
+
identity passthrough at index 0) and the advanced-magnetic adjoint (the curl-H
|
|
1327
|
+
difference) exactly as ``reverse_tfsf_auxiliary_electric_kernel`` does; the
|
|
1328
|
+
source-driven and clamped-tail entries carry no state dependence.
|
|
1329
|
+
"""
|
|
1330
|
+
electric_total = adj_electric_prev.numel()
|
|
1331
|
+
magnetic_total = adj_magnetic_after.numel()
|
|
1332
|
+
index = torch.arange(electric_total, device=adj_electric_prev.device)
|
|
1333
|
+
overwritten = (index == int(source_index)) | (index == electric_total - 1)
|
|
1334
|
+
interior = (index >= 1) & (index + 1 < electric_total) & (~overwritten)
|
|
1335
|
+
passthrough = (index == 0) & (~overwritten)
|
|
1336
|
+
electric_coeff = torch.where(interior, electric_decay, torch.zeros_like(electric_decay))
|
|
1337
|
+
electric_coeff = torch.where(passthrough, torch.ones_like(electric_decay), electric_coeff)
|
|
1338
|
+
adj_electric_prev.add_(electric_coeff * adj_electric_post)
|
|
1339
|
+
|
|
1340
|
+
if magnetic_total == 0:
|
|
1341
|
+
return
|
|
1342
|
+
lower = torch.arange(magnetic_total, device=adj_electric_prev.device)
|
|
1343
|
+
upper = lower + 1
|
|
1344
|
+
lower_valid = (lower > 0) & (lower + 1 < electric_total) & (lower != int(source_index))
|
|
1345
|
+
upper_valid = (upper > 0) & (upper + 1 < electric_total) & (upper != int(source_index))
|
|
1346
|
+
zeros = torch.zeros(magnetic_total, device=adj_electric_prev.device, dtype=adj_electric_prev.dtype)
|
|
1347
|
+
lower_term = torch.where(
|
|
1348
|
+
lower_valid, electric_curl[:magnetic_total] * adj_electric_post[:magnetic_total], zeros
|
|
1349
|
+
)
|
|
1350
|
+
upper_term = torch.where(
|
|
1351
|
+
upper_valid, electric_curl[1 : magnetic_total + 1] * adj_electric_post[1 : magnetic_total + 1], zeros
|
|
1352
|
+
)
|
|
1353
|
+
adj_magnetic_after.add_(upper_term - lower_term)
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def _reverse_tfsf_auxiliary_magnetic_state_adjoint(
|
|
1357
|
+
adj_electric_prev,
|
|
1358
|
+
adj_magnetic_prev,
|
|
1359
|
+
adj_magnetic_after,
|
|
1360
|
+
magnetic_decay,
|
|
1361
|
+
magnetic_curl,
|
|
1362
|
+
):
|
|
1363
|
+
"""Analytic transpose of ``_advance_tfsf_auxiliary_magnetic_state``.
|
|
1364
|
+
|
|
1365
|
+
Assigns the pre-step magnetic adjoint (decay pullback) and folds the curl-E
|
|
1366
|
+
difference into the pre-step electric adjoint, matching
|
|
1367
|
+
``reverse_tfsf_auxiliary_magnetic_kernel``.
|
|
1368
|
+
"""
|
|
1369
|
+
magnetic_total = adj_magnetic_after.numel()
|
|
1370
|
+
adj_magnetic_prev.copy_(magnetic_decay * adj_magnetic_after)
|
|
1371
|
+
if magnetic_total == 0:
|
|
1372
|
+
return
|
|
1373
|
+
curl_flux = magnetic_curl * adj_magnetic_after
|
|
1374
|
+
adj_electric_prev[:magnetic_total].add_(curl_flux)
|
|
1375
|
+
adj_electric_prev[1:].add_(-curl_flux)
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
def _bloch_backward_diff(field: torch.Tensor, *, axis: int, inv_delta: torch.Tensor, phase_cos: float, phase_sin: float):
|
|
1379
|
+
# inv_delta: 1D dual spacing reciprocals, length == field size + 1 along
|
|
1380
|
+
# axis; wrap entries live at [0] and [-1] (equal on a Bloch axis).
|
|
1381
|
+
shape = list(field.shape)
|
|
1382
|
+
shape[axis] += 1
|
|
1383
|
+
diff = field.new_zeros(shape)
|
|
1384
|
+
if field.shape[axis] == 0:
|
|
1385
|
+
return diff
|
|
1386
|
+
|
|
1387
|
+
low = field.select(axis, 0)
|
|
1388
|
+
high = field.select(axis, field.shape[axis] - 1)
|
|
1389
|
+
low_value = (low - _complex_phase_negative(high, phase_cos, phase_sin)) * inv_delta[0]
|
|
1390
|
+
low_region = [slice(None)] * field.ndim
|
|
1391
|
+
low_region[axis] = 0
|
|
1392
|
+
diff[tuple(low_region)] = low_value
|
|
1393
|
+
|
|
1394
|
+
if field.shape[axis] == 1:
|
|
1395
|
+
return diff
|
|
1396
|
+
|
|
1397
|
+
high_value = (_complex_phase_positive(low, phase_cos, phase_sin) - high) * inv_delta[-1]
|
|
1398
|
+
high_region = [slice(None)] * diff.ndim
|
|
1399
|
+
high_region[axis] = -1
|
|
1400
|
+
diff[tuple(high_region)] = high_value
|
|
1401
|
+
|
|
1402
|
+
if field.shape[axis] > 1:
|
|
1403
|
+
interior = [slice(None)] * diff.ndim
|
|
1404
|
+
interior[axis] = slice(1, -1)
|
|
1405
|
+
current = [slice(None)] * field.ndim
|
|
1406
|
+
current[axis] = slice(1, None)
|
|
1407
|
+
previous = [slice(None)] * field.ndim
|
|
1408
|
+
previous[axis] = slice(0, -1)
|
|
1409
|
+
diff[tuple(interior)] = (field[tuple(current)] - field[tuple(previous)]) * _broadcast_vector(inv_delta[1:-1], axis)
|
|
1410
|
+
return diff
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
def _apply_source_term_list(field_mapping, *, terms, source_time, omega, time_value, solver=None):
|
|
1414
|
+
if not terms:
|
|
1415
|
+
return field_mapping
|
|
1416
|
+
|
|
1417
|
+
updated = dict(field_mapping)
|
|
1418
|
+
signal_cache = {}
|
|
1419
|
+
cw_cache = {}
|
|
1420
|
+
for term in terms:
|
|
1421
|
+
term_source_time = term.get("source_time") or source_time
|
|
1422
|
+
term_omega = float(omega if term.get("omega") is None else term["omega"])
|
|
1423
|
+
cache_key = term.get("source_index")
|
|
1424
|
+
if cache_key is None:
|
|
1425
|
+
cache_key = (id(term_source_time), term_omega)
|
|
1426
|
+
literal_patch = term.get("literal_patch")
|
|
1427
|
+
if literal_patch is not None:
|
|
1428
|
+
updated[term["field_name"]] = _add_patch(updated[term["field_name"]], term["offsets"], literal_patch)
|
|
1429
|
+
continue
|
|
1430
|
+
if term["cw_cos_patch"] is not None:
|
|
1431
|
+
if cache_key not in cw_cache:
|
|
1432
|
+
cw_cache[cache_key] = _resolve_cw_signal(term_source_time, term_omega, time_value)
|
|
1433
|
+
signal_cos, signal_sin = cw_cache[cache_key]
|
|
1434
|
+
if solver is not None and has_complex_fields(solver):
|
|
1435
|
+
updated = _apply_bloch_cw_source_term(
|
|
1436
|
+
updated,
|
|
1437
|
+
solver=solver,
|
|
1438
|
+
field_name=term["field_name"],
|
|
1439
|
+
offsets=term["offsets"],
|
|
1440
|
+
cw_cos_patch=term["cw_cos_patch"],
|
|
1441
|
+
cw_sin_patch=term["cw_sin_patch"],
|
|
1442
|
+
signal_cos=signal_cos,
|
|
1443
|
+
signal_sin=signal_sin,
|
|
1444
|
+
)
|
|
1445
|
+
continue
|
|
1446
|
+
patch = float(signal_cos) * term["cw_cos_patch"] + float(signal_sin) * term["cw_sin_patch"]
|
|
1447
|
+
elif term["delay_patch"] is not None:
|
|
1448
|
+
sample_time = time_value - term["delay_patch"]
|
|
1449
|
+
patch = _evaluate_source_time_tensor(term_source_time, sample_time) * term["patch"]
|
|
1450
|
+
activation_delay_patch = term.get("activation_delay_patch")
|
|
1451
|
+
if activation_delay_patch is not None:
|
|
1452
|
+
patch = torch.where(
|
|
1453
|
+
time_value < activation_delay_patch,
|
|
1454
|
+
torch.zeros_like(patch),
|
|
1455
|
+
patch,
|
|
1456
|
+
)
|
|
1457
|
+
if solver is not None and has_complex_fields(solver):
|
|
1458
|
+
# The delayed pulse is a real current; scatter it into the split
|
|
1459
|
+
# real/imag Bloch field with the same wrap-phase rule the forward
|
|
1460
|
+
# ``addSourcePatchBloch3D`` kernel uses, so the reverse-replay
|
|
1461
|
+
# reconstruction (and the eps VJP flowing through it) matches the
|
|
1462
|
+
# forward injection exactly.
|
|
1463
|
+
real_patch = patch.to(
|
|
1464
|
+
device=updated[term["field_name"]].device,
|
|
1465
|
+
dtype=updated[term["field_name"]].dtype,
|
|
1466
|
+
)
|
|
1467
|
+
updated = _add_complex_bloch_source_patch(
|
|
1468
|
+
updated,
|
|
1469
|
+
solver=solver,
|
|
1470
|
+
field_name=term["field_name"],
|
|
1471
|
+
offsets=term["offsets"],
|
|
1472
|
+
delta=torch.complex(real_patch, torch.zeros_like(real_patch)),
|
|
1473
|
+
)
|
|
1474
|
+
continue
|
|
1475
|
+
else:
|
|
1476
|
+
if cache_key not in signal_cache:
|
|
1477
|
+
signal_cache[cache_key] = evaluate_source_time(term_source_time, float(time_value))
|
|
1478
|
+
scalar_signal = signal_cache[cache_key]
|
|
1479
|
+
signal_real = float(scalar_signal) * float(term["phase_real"])
|
|
1480
|
+
signal_imag = float(scalar_signal) * float(term["phase_imag"])
|
|
1481
|
+
if solver is not None and has_complex_fields(solver):
|
|
1482
|
+
updated = _apply_bloch_uniform_source_term(
|
|
1483
|
+
updated,
|
|
1484
|
+
solver=solver,
|
|
1485
|
+
field_name=term["field_name"],
|
|
1486
|
+
offsets=term["offsets"],
|
|
1487
|
+
patch=term["patch"],
|
|
1488
|
+
signal_real=signal_real,
|
|
1489
|
+
signal_imag=signal_imag,
|
|
1490
|
+
)
|
|
1491
|
+
continue
|
|
1492
|
+
patch = signal_real * term["patch"]
|
|
1493
|
+
field_name = term["field_name"]
|
|
1494
|
+
updated[field_name] = _add_patch(updated[field_name], term["offsets"], patch)
|
|
1495
|
+
return updated
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
def _update_debye_state(field, polarization, *, drive, decay, dt):
|
|
1499
|
+
next_polarization = polarization * float(decay) + drive * field
|
|
1500
|
+
next_current = (next_polarization - polarization) / float(dt)
|
|
1501
|
+
return next_polarization, next_current
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def _update_drude_state(field, current, *, drive, decay):
|
|
1505
|
+
return current * float(decay) + drive * field
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
def _update_lorentz_state(field, polarization, current, *, drive, decay, restoring, dt):
|
|
1509
|
+
next_current = current * float(decay) - polarization * float(restoring) + drive * field
|
|
1510
|
+
next_polarization = polarization + float(dt) * next_current
|
|
1511
|
+
return next_polarization, next_current
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
def _advance_ade_state(solver, state, specs, *, imag=False):
|
|
1515
|
+
# ``imag`` advances the imaginary-field ADE replica the forward solver keeps
|
|
1516
|
+
# for Bloch (complex-field) runs: same real pole coefficients, but driven by
|
|
1517
|
+
# the imaginary field component and stored under ``*_imag`` state names.
|
|
1518
|
+
suffix = "_imag" if imag else ""
|
|
1519
|
+
updated = {}
|
|
1520
|
+
for component_name, model_name, index, _tensor_names, entry in specs or ():
|
|
1521
|
+
field = state[component_name + suffix]
|
|
1522
|
+
if model_name == "debye":
|
|
1523
|
+
polarization_name = dispersive_state_name(component_name, model_name, index, "polarization") + suffix
|
|
1524
|
+
current_name = dispersive_state_name(component_name, model_name, index, "current") + suffix
|
|
1525
|
+
polarization, current = _update_debye_state(
|
|
1526
|
+
field,
|
|
1527
|
+
state[polarization_name],
|
|
1528
|
+
drive=entry["drive"],
|
|
1529
|
+
decay=entry["decay"],
|
|
1530
|
+
dt=solver.dt,
|
|
1531
|
+
)
|
|
1532
|
+
updated[polarization_name] = polarization
|
|
1533
|
+
updated[current_name] = current
|
|
1534
|
+
continue
|
|
1535
|
+
if model_name == "drude":
|
|
1536
|
+
current_name = dispersive_state_name(component_name, model_name, index, "current") + suffix
|
|
1537
|
+
updated[current_name] = _update_drude_state(
|
|
1538
|
+
field,
|
|
1539
|
+
state[current_name],
|
|
1540
|
+
drive=entry["drive"],
|
|
1541
|
+
decay=entry["decay"],
|
|
1542
|
+
)
|
|
1543
|
+
continue
|
|
1544
|
+
|
|
1545
|
+
polarization_name = dispersive_state_name(component_name, model_name, index, "polarization") + suffix
|
|
1546
|
+
current_name = dispersive_state_name(component_name, model_name, index, "current") + suffix
|
|
1547
|
+
polarization, current = _update_lorentz_state(
|
|
1548
|
+
field,
|
|
1549
|
+
state[polarization_name],
|
|
1550
|
+
state[current_name],
|
|
1551
|
+
drive=entry["drive"],
|
|
1552
|
+
decay=entry["decay"],
|
|
1553
|
+
restoring=entry["restoring"],
|
|
1554
|
+
dt=solver.dt,
|
|
1555
|
+
)
|
|
1556
|
+
updated[polarization_name] = polarization
|
|
1557
|
+
updated[current_name] = current
|
|
1558
|
+
return updated
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def _advance_dispersive_state(solver, state):
|
|
1562
|
+
updated = _advance_ade_state(solver, state, iter_dispersive_state_specs(solver))
|
|
1563
|
+
if has_complex_fields(solver):
|
|
1564
|
+
# Bloch runs propagate a second real FDTD copy (the imaginary field);
|
|
1565
|
+
# the electric ADE poles advance on it identically, so mirror the real
|
|
1566
|
+
# pass with the imaginary drive to keep the replay bit-consistent with
|
|
1567
|
+
# the forward kernels.
|
|
1568
|
+
updated.update(
|
|
1569
|
+
_advance_ade_state(solver, state, iter_dispersive_state_specs(solver), imag=True)
|
|
1570
|
+
)
|
|
1571
|
+
return updated
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
def _advance_magnetic_dispersive_state(solver, state):
|
|
1575
|
+
updated = _advance_ade_state(solver, state, iter_magnetic_dispersive_state_specs(solver))
|
|
1576
|
+
if has_complex_fields(solver):
|
|
1577
|
+
updated.update(
|
|
1578
|
+
_advance_ade_state(
|
|
1579
|
+
solver, state, iter_magnetic_dispersive_state_specs(solver), imag=True
|
|
1580
|
+
)
|
|
1581
|
+
)
|
|
1582
|
+
return updated
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
def _magnetic_inverse_permeabilities(solver):
|
|
1586
|
+
templates = getattr(solver, "_magnetic_dispersive_templates", {}) or {}
|
|
1587
|
+
return {
|
|
1588
|
+
component_name: templates[component_name]["inv_mu"]
|
|
1589
|
+
for component_name in ("Hx", "Hy", "Hz")
|
|
1590
|
+
if component_name in templates
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
|
|
1594
|
+
def _apply_magnetic_dispersive_corrections(solver, magnetic_fields, magnetic_dispersive_state):
|
|
1595
|
+
"""H -= dt * J_m / mu with the same edge-averaged inv_mu the runtime kernels use.
|
|
1596
|
+
|
|
1597
|
+
mu carries no gradient channel (it is a constant coefficient tensor in the
|
|
1598
|
+
adjoint), matching the forward `applyPolarizationCurrent3D` launches exactly.
|
|
1599
|
+
"""
|
|
1600
|
+
if not getattr(solver, "magnetic_dispersive_enabled", False):
|
|
1601
|
+
return magnetic_fields
|
|
1602
|
+
|
|
1603
|
+
inv_mu = _magnetic_inverse_permeabilities(solver)
|
|
1604
|
+
updated = dict(magnetic_fields)
|
|
1605
|
+
suffixes = ("", "_imag") if has_complex_fields(solver) else ("",)
|
|
1606
|
+
for suffix in suffixes:
|
|
1607
|
+
for component_name, model_name, index, _tensor_names, _entry in iter_magnetic_dispersive_state_specs(solver) or ():
|
|
1608
|
+
current_name = dispersive_state_name(component_name, model_name, index, "current") + suffix
|
|
1609
|
+
field_name = component_name + suffix
|
|
1610
|
+
updated[field_name] = updated[field_name] - float(solver.dt) * magnetic_dispersive_state[
|
|
1611
|
+
current_name
|
|
1612
|
+
] * inv_mu[component_name]
|
|
1613
|
+
return updated
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
def _apply_dispersive_corrections(solver, electric_fields, dispersive_state, *, eps_ex, eps_ey, eps_ez):
|
|
1617
|
+
if not getattr(solver, "dispersive_enabled", False):
|
|
1618
|
+
return electric_fields
|
|
1619
|
+
|
|
1620
|
+
inv_eps = {
|
|
1621
|
+
"Ex": 1.0 / eps_ex,
|
|
1622
|
+
"Ey": 1.0 / eps_ey,
|
|
1623
|
+
"Ez": 1.0 / eps_ez,
|
|
1624
|
+
}
|
|
1625
|
+
updated = dict(electric_fields)
|
|
1626
|
+
dt = float(solver.dt)
|
|
1627
|
+
# On Bloch runs the imaginary field carries its own polarization current, so
|
|
1628
|
+
# subtract dt * J_imag / eps from the imaginary component too (matching the
|
|
1629
|
+
# forward apply_component_dispersive_currents(imag=True) launches).
|
|
1630
|
+
complex_fields = has_complex_fields(solver) and all(
|
|
1631
|
+
f"{component_name}_imag" in electric_fields for component_name in ("Ex", "Ey", "Ez")
|
|
1632
|
+
)
|
|
1633
|
+
for component_name, model_name, index, _tensor_names, _entry in iter_dispersive_state_specs(solver) or ():
|
|
1634
|
+
current_name = dispersive_state_name(component_name, model_name, index, "current")
|
|
1635
|
+
updated[component_name] = updated[component_name] - dt * dispersive_state[current_name] * inv_eps[
|
|
1636
|
+
component_name
|
|
1637
|
+
]
|
|
1638
|
+
if complex_fields:
|
|
1639
|
+
imag_field = f"{component_name}_imag"
|
|
1640
|
+
updated[imag_field] = updated[imag_field] - dt * dispersive_state[current_name + "_imag"] * inv_eps[
|
|
1641
|
+
component_name
|
|
1642
|
+
]
|
|
1643
|
+
return updated
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
def _reverse_dispersive_corrections(
|
|
1647
|
+
solver,
|
|
1648
|
+
adjoint_state,
|
|
1649
|
+
dispersive_state,
|
|
1650
|
+
*,
|
|
1651
|
+
eps_ex,
|
|
1652
|
+
eps_ey,
|
|
1653
|
+
eps_ez,
|
|
1654
|
+
suffix="",
|
|
1655
|
+
):
|
|
1656
|
+
# ``suffix`` selects the field/current half the ``E -= dt * J / eps``
|
|
1657
|
+
# correction VJP runs on. "" is the real field (unchanged); Bloch runs also
|
|
1658
|
+
# correct the imaginary field with ``suffix="_imag"``. Both halves share the
|
|
1659
|
+
# (real) eps, so both accumulate into the same eps gradient at the call site.
|
|
1660
|
+
electric_source_adjoint = {
|
|
1661
|
+
"Ex": adjoint_state["Ex" + suffix].detach().clone(),
|
|
1662
|
+
"Ey": adjoint_state["Ey" + suffix].detach().clone(),
|
|
1663
|
+
"Ez": adjoint_state["Ez" + suffix].detach().clone(),
|
|
1664
|
+
}
|
|
1665
|
+
dispersive_output_adjoint = {
|
|
1666
|
+
name + suffix: adjoint_state[name + suffix].detach().clone()
|
|
1667
|
+
for name in _electric_dispersive_real_state_names(solver)
|
|
1668
|
+
}
|
|
1669
|
+
grad_eps_by_field = {
|
|
1670
|
+
"Ex": torch.zeros_like(eps_ex),
|
|
1671
|
+
"Ey": torch.zeros_like(eps_ey),
|
|
1672
|
+
"Ez": torch.zeros_like(eps_ez),
|
|
1673
|
+
}
|
|
1674
|
+
eps_by_field = {
|
|
1675
|
+
"Ex": eps_ex,
|
|
1676
|
+
"Ey": eps_ey,
|
|
1677
|
+
"Ez": eps_ez,
|
|
1678
|
+
}
|
|
1679
|
+
dt = float(solver.dt)
|
|
1680
|
+
|
|
1681
|
+
for component_name, model_name, index, _tensor_names, _entry in iter_dispersive_state_specs(solver) or ():
|
|
1682
|
+
current_name = dispersive_state_name(component_name, model_name, index, "current") + suffix
|
|
1683
|
+
electric_adjoint = electric_source_adjoint[component_name]
|
|
1684
|
+
eps_field = eps_by_field[component_name]
|
|
1685
|
+
dispersive_output_adjoint[current_name] = (
|
|
1686
|
+
dispersive_output_adjoint[current_name] - dt * electric_adjoint / eps_field
|
|
1687
|
+
)
|
|
1688
|
+
grad_eps_by_field[component_name] = (
|
|
1689
|
+
grad_eps_by_field[component_name] + dt * dispersive_state[current_name] * electric_adjoint / (eps_field * eps_field)
|
|
1690
|
+
)
|
|
1691
|
+
|
|
1692
|
+
source_adjoint_state = dict(adjoint_state)
|
|
1693
|
+
source_adjoint_state.update({name + suffix: value for name, value in electric_source_adjoint.items()})
|
|
1694
|
+
return electric_source_adjoint, dispersive_output_adjoint, grad_eps_by_field, source_adjoint_state
|
|
1695
|
+
|
|
1696
|
+
|
|
1697
|
+
def _electric_dispersive_real_state_names(solver):
|
|
1698
|
+
"""Canonical (unsuffixed) electric-dispersive state names, in spec order.
|
|
1699
|
+
|
|
1700
|
+
Bloch runs append ``_imag`` replicas to ``dispersive_state_names``; the
|
|
1701
|
+
suffix-parameterized ADE-state VJP needs the real names alone so it can apply
|
|
1702
|
+
the requested half's suffix without double-suffixing the ``_imag`` copies.
|
|
1703
|
+
"""
|
|
1704
|
+
names = []
|
|
1705
|
+
for component_name, model_name, index, tensor_names, _entry in iter_dispersive_state_specs(solver) or ():
|
|
1706
|
+
for tensor_name in tensor_names:
|
|
1707
|
+
names.append(dispersive_state_name(component_name, model_name, index, tensor_name))
|
|
1708
|
+
return tuple(names)
|
|
1709
|
+
|
|
1710
|
+
|
|
1711
|
+
def _reverse_magnetic_dispersive_corrections(solver, adjoint_state, magnetic_field_adjoint):
|
|
1712
|
+
"""VJP of ``H -= dt * J_m * inv_mu`` given the adjoint of the corrected H.
|
|
1713
|
+
|
|
1714
|
+
``inv_mu`` is a constant coefficient tensor (there is no magnetic material
|
|
1715
|
+
gradient channel), so the correction only seeds the post-step magnetic
|
|
1716
|
+
dispersive-state adjoint; the field adjoint itself passes through unchanged.
|
|
1717
|
+
"""
|
|
1718
|
+
output_adjoint = {
|
|
1719
|
+
name: adjoint_state[name].detach().clone()
|
|
1720
|
+
for name in checkpoint_schema(solver).magnetic_dispersive_state_names
|
|
1721
|
+
}
|
|
1722
|
+
inv_mu = _magnetic_inverse_permeabilities(solver)
|
|
1723
|
+
dt = float(solver.dt)
|
|
1724
|
+
for component_name, model_name, index, _tensor_names, _entry in iter_magnetic_dispersive_state_specs(solver) or ():
|
|
1725
|
+
current_name = dispersive_state_name(component_name, model_name, index, "current")
|
|
1726
|
+
output_adjoint[current_name] = (
|
|
1727
|
+
output_adjoint[current_name] - dt * magnetic_field_adjoint[component_name] * inv_mu[component_name]
|
|
1728
|
+
)
|
|
1729
|
+
return output_adjoint
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
def _broadcast_vector(vector: torch.Tensor, axis: int) -> torch.Tensor:
|
|
1733
|
+
shape = [1, 1, 1]
|
|
1734
|
+
shape[axis] = int(vector.shape[0])
|
|
1735
|
+
return vector.view(*shape)
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
def _forward_diff(field: torch.Tensor, axis: int, inv_delta: torch.Tensor) -> torch.Tensor:
|
|
1739
|
+
# inv_delta: 1D primal spacing reciprocals, length == field size - 1 along axis.
|
|
1740
|
+
slicer_lo = [slice(None)] * field.ndim
|
|
1741
|
+
slicer_hi = [slice(None)] * field.ndim
|
|
1742
|
+
slicer_lo[axis] = slice(0, -1)
|
|
1743
|
+
slicer_hi[axis] = slice(1, None)
|
|
1744
|
+
return (field[tuple(slicer_hi)] - field[tuple(slicer_lo)]) * _broadcast_vector(inv_delta, axis)
|
|
1745
|
+
|
|
1746
|
+
|
|
1747
|
+
def _backward_diff(field: torch.Tensor, axis: int, inv_delta: torch.Tensor) -> torch.Tensor:
|
|
1748
|
+
# inv_delta: 1D dual spacing reciprocals, length == field size + 1 along axis.
|
|
1749
|
+
shape = list(field.shape)
|
|
1750
|
+
shape[axis] += 1
|
|
1751
|
+
diff = field.new_zeros(shape)
|
|
1752
|
+
interior = [slice(None)] * diff.ndim
|
|
1753
|
+
field_hi = [slice(None)] * field.ndim
|
|
1754
|
+
field_lo = [slice(None)] * field.ndim
|
|
1755
|
+
interior[axis] = slice(1, -1)
|
|
1756
|
+
field_hi[axis] = slice(1, None)
|
|
1757
|
+
field_lo[axis] = slice(0, -1)
|
|
1758
|
+
diff[tuple(interior)] = (field[tuple(field_hi)] - field[tuple(field_lo)]) * _broadcast_vector(inv_delta[1:-1], axis)
|
|
1759
|
+
return diff
|
|
1760
|
+
|
|
1761
|
+
|
|
1762
|
+
def _scatter_diff_adjoint(field_grad: torch.Tensor, scaled_grad: torch.Tensor, axis: int):
|
|
1763
|
+
field_lo = [slice(None)] * field_grad.ndim
|
|
1764
|
+
field_hi = [slice(None)] * field_grad.ndim
|
|
1765
|
+
field_lo[axis] = slice(0, -1)
|
|
1766
|
+
field_hi[axis] = slice(1, None)
|
|
1767
|
+
field_grad[tuple(field_lo)] = field_grad[tuple(field_lo)] - scaled_grad
|
|
1768
|
+
field_grad[tuple(field_hi)] = field_grad[tuple(field_hi)] + scaled_grad
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
def _accumulate_forward_diff_adjoint(field_grad: torch.Tensor, diff_grad: torch.Tensor, *, axis: int, inv_delta: torch.Tensor):
|
|
1772
|
+
_scatter_diff_adjoint(field_grad, _broadcast_vector(inv_delta, axis) * diff_grad, axis)
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
def _accumulate_backward_diff_adjoint(field_grad: torch.Tensor, diff_grad: torch.Tensor, *, axis: int, inv_delta: torch.Tensor):
|
|
1776
|
+
interior = [slice(None)] * diff_grad.ndim
|
|
1777
|
+
interior[axis] = slice(1, -1)
|
|
1778
|
+
_scatter_diff_adjoint(field_grad, _broadcast_vector(inv_delta[1:-1], axis) * diff_grad[tuple(interior)], axis)
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def _accumulate_bloch_backward_diff_adjoint(
|
|
1782
|
+
field_grad: torch.Tensor,
|
|
1783
|
+
diff_grad: torch.Tensor,
|
|
1784
|
+
*,
|
|
1785
|
+
axis: int,
|
|
1786
|
+
inv_delta: torch.Tensor,
|
|
1787
|
+
phase_cos: float,
|
|
1788
|
+
phase_sin: float,
|
|
1789
|
+
):
|
|
1790
|
+
_accumulate_backward_diff_adjoint(field_grad, diff_grad, axis=axis, inv_delta=inv_delta)
|
|
1791
|
+
# Wrap terms transpose the forward wrap differences: contributions from the
|
|
1792
|
+
# low diff entry carry inv_delta[0], those from the high entry inv_delta[-1].
|
|
1793
|
+
low_scale = inv_delta[0]
|
|
1794
|
+
high_scale = inv_delta[-1]
|
|
1795
|
+
low_grad = diff_grad.select(axis, 0)
|
|
1796
|
+
high_grad = diff_grad.select(axis, int(diff_grad.shape[axis] - 1))
|
|
1797
|
+
field_grad.select(axis, 0).add_(low_scale * low_grad + high_scale * _complex_phase_negative(high_grad, phase_cos, phase_sin))
|
|
1798
|
+
field_grad.select(axis, int(field_grad.shape[axis] - 1)).add_(
|
|
1799
|
+
-low_scale * _complex_phase_positive(low_grad, phase_cos, phase_sin) - high_scale * high_grad
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
# The face codes and grid shape are fixed for the whole reverse pass, so the
|
|
1804
|
+
# boolean boundary masks the per-step electric-adjoint update reads (six rebuilds
|
|
1805
|
+
# per reverse step) are config-level constants. Memoize them: the returned masks
|
|
1806
|
+
# are consumed read-only (|, &, ~, torch.where) at every call site, so a shared
|
|
1807
|
+
# cached tensor is safe, and the key (shape/axis/face-codes/device) recurs
|
|
1808
|
+
# identically across every step and every optimization iteration on a scene.
|
|
1809
|
+
_BOUNDARY_AXIS_MASK_CACHE: dict = {}
|
|
1810
|
+
|
|
1811
|
+
|
|
1812
|
+
def _boundary_axis_masks(shape, axis: int, low_mode: int, high_mode: int, device) -> tuple[torch.Tensor, torch.Tensor]:
|
|
1813
|
+
key = (tuple(int(dim) for dim in shape), int(axis), int(low_mode), int(high_mode), str(device))
|
|
1814
|
+
cached = _BOUNDARY_AXIS_MASK_CACHE.get(key)
|
|
1815
|
+
if cached is not None:
|
|
1816
|
+
return cached
|
|
1817
|
+
inactive = torch.zeros(shape, device=device, dtype=torch.bool)
|
|
1818
|
+
pec = torch.zeros(shape, device=device, dtype=torch.bool)
|
|
1819
|
+
low_face = [slice(None)] * len(shape)
|
|
1820
|
+
low_face[axis] = 0
|
|
1821
|
+
high_face = [slice(None)] * len(shape)
|
|
1822
|
+
high_face[axis] = -1
|
|
1823
|
+
if low_mode in {BOUNDARY_NONE, BOUNDARY_PML}:
|
|
1824
|
+
inactive[tuple(low_face)] = True
|
|
1825
|
+
elif low_mode == BOUNDARY_PEC:
|
|
1826
|
+
pec[tuple(low_face)] = True
|
|
1827
|
+
if high_mode in {BOUNDARY_NONE, BOUNDARY_PML}:
|
|
1828
|
+
inactive[tuple(high_face)] = True
|
|
1829
|
+
elif high_mode == BOUNDARY_PEC:
|
|
1830
|
+
pec[tuple(high_face)] = True
|
|
1831
|
+
_BOUNDARY_AXIS_MASK_CACHE[key] = (inactive, pec)
|
|
1832
|
+
return inactive, pec
|
|
1833
|
+
|
|
1834
|
+
|
|
1835
|
+
# Per-electric-component tangential axes and their domain-face boundary codes,
|
|
1836
|
+
# matching the Yee electric update (_update_electric_component) and the terminal
|
|
1837
|
+
# _enforce_pec_boundaries clamp.
|
|
1838
|
+
_ELECTRIC_COMPONENT_PEC_AXES = {
|
|
1839
|
+
"Ex": ((1, "boundary_y_low_code", "boundary_y_high_code"), (2, "boundary_z_low_code", "boundary_z_high_code")),
|
|
1840
|
+
"Ey": ((2, "boundary_z_low_code", "boundary_z_high_code"), (0, "boundary_x_low_code", "boundary_x_high_code")),
|
|
1841
|
+
"Ez": ((0, "boundary_x_low_code", "boundary_x_high_code"), (1, "boundary_y_low_code", "boundary_y_high_code")),
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
|
|
1845
|
+
def _electric_component_pec_mask(solver, field_name, shape, device):
|
|
1846
|
+
"""Mask of PEC-clamped cells for an electric component, or ``None`` when the
|
|
1847
|
+
scene has no PEC faces.
|
|
1848
|
+
|
|
1849
|
+
A soft source injected onto a PEC domain face is erased by the terminal
|
|
1850
|
+
``_enforce_pec_boundaries`` clamp in the forward step, so its eps-gradient
|
|
1851
|
+
must be zero there. The mask is the union of the two tangential domain faces'
|
|
1852
|
+
PEC cells, exactly the cells the forward clamp zeroes; on an interior region
|
|
1853
|
+
it is all-``False`` so masking is a no-op.
|
|
1854
|
+
"""
|
|
1855
|
+
if not bool(getattr(solver, "has_pec_faces", False)):
|
|
1856
|
+
return None
|
|
1857
|
+
spec = _ELECTRIC_COMPONENT_PEC_AXES.get(field_name)
|
|
1858
|
+
if spec is None:
|
|
1859
|
+
return None
|
|
1860
|
+
(axis_pos, low_pos, high_pos), (axis_neg, low_neg, high_neg) = spec
|
|
1861
|
+
_inactive_pos, pec_pos = _boundary_axis_masks(
|
|
1862
|
+
shape, axis_pos, int(getattr(solver, low_pos)), int(getattr(solver, high_pos)), device
|
|
1863
|
+
)
|
|
1864
|
+
_inactive_neg, pec_neg = _boundary_axis_masks(
|
|
1865
|
+
shape, axis_neg, int(getattr(solver, low_neg)), int(getattr(solver, high_neg)), device
|
|
1866
|
+
)
|
|
1867
|
+
return pec_pos | pec_neg
|
|
1868
|
+
|
|
1869
|
+
|
|
1870
|
+
def _update_magnetic_component(field, *, d_pos, d_neg, decay, curl, psi_pos=None, psi_neg=None, b_pos=None, c_pos=None, inv_kappa_pos=None, b_neg=None, c_neg=None, inv_kappa_neg=None, axis_pos=None, axis_neg=None):
|
|
1871
|
+
if psi_pos is not None:
|
|
1872
|
+
psi_pos_new = _broadcast_vector(b_pos, axis_pos) * psi_pos + _broadcast_vector(c_pos, axis_pos) * d_pos
|
|
1873
|
+
psi_neg_new = _broadcast_vector(b_neg, axis_neg) * psi_neg + _broadcast_vector(c_neg, axis_neg) * d_neg
|
|
1874
|
+
curl_term = (
|
|
1875
|
+
d_pos * _broadcast_vector(inv_kappa_pos, axis_pos) + psi_pos_new
|
|
1876
|
+
) - (
|
|
1877
|
+
d_neg * _broadcast_vector(inv_kappa_neg, axis_neg) + psi_neg_new
|
|
1878
|
+
)
|
|
1879
|
+
updated = field * decay - curl * curl_term
|
|
1880
|
+
return updated, psi_pos_new, psi_neg_new
|
|
1881
|
+
|
|
1882
|
+
updated = field * decay - curl * (d_pos - d_neg)
|
|
1883
|
+
return updated, None, None
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
def _update_electric_component(field, *, d_pos, d_neg, decay, curl_prefactor, eps, low_mode_pos, high_mode_pos, low_mode_neg, high_mode_neg, axis_pos, axis_neg, psi_pos=None, psi_neg=None, b_pos=None, c_pos=None, inv_kappa_pos=None, b_neg=None, c_neg=None, inv_kappa_neg=None):
|
|
1887
|
+
curl = curl_prefactor / eps
|
|
1888
|
+
if psi_pos is not None:
|
|
1889
|
+
psi_pos_candidate = _broadcast_vector(b_pos, axis_pos) * psi_pos + _broadcast_vector(c_pos, axis_pos) * d_pos
|
|
1890
|
+
psi_neg_candidate = _broadcast_vector(b_neg, axis_neg) * psi_neg + _broadcast_vector(c_neg, axis_neg) * d_neg
|
|
1891
|
+
curl_term = (
|
|
1892
|
+
d_pos * _broadcast_vector(inv_kappa_pos, axis_pos) + psi_pos_candidate
|
|
1893
|
+
) - (
|
|
1894
|
+
d_neg * _broadcast_vector(inv_kappa_neg, axis_neg) + psi_neg_candidate
|
|
1895
|
+
)
|
|
1896
|
+
else:
|
|
1897
|
+
psi_pos_candidate = None
|
|
1898
|
+
psi_neg_candidate = None
|
|
1899
|
+
curl_term = d_pos - d_neg
|
|
1900
|
+
|
|
1901
|
+
inactive_pos, pec_pos = _boundary_axis_masks(field.shape, axis_pos, low_mode_pos, high_mode_pos, field.device)
|
|
1902
|
+
inactive_neg, pec_neg = _boundary_axis_masks(field.shape, axis_neg, low_mode_neg, high_mode_neg, field.device)
|
|
1903
|
+
pec_mask = pec_pos | pec_neg
|
|
1904
|
+
inactive_mask = (~pec_mask) & (inactive_pos | inactive_neg)
|
|
1905
|
+
|
|
1906
|
+
candidate = field * decay + curl * curl_term
|
|
1907
|
+
updated = torch.where(pec_mask, torch.zeros_like(candidate), torch.where(inactive_mask, field, candidate))
|
|
1908
|
+
if psi_pos_candidate is None:
|
|
1909
|
+
return updated, None, None
|
|
1910
|
+
|
|
1911
|
+
keep_mask = inactive_mask | pec_mask
|
|
1912
|
+
psi_pos_new = torch.where(keep_mask, psi_pos, psi_pos_candidate)
|
|
1913
|
+
psi_neg_new = torch.where(keep_mask, psi_neg, psi_neg_candidate)
|
|
1914
|
+
return updated, psi_pos_new, psi_neg_new
|
|
1915
|
+
|
|
1916
|
+
|
|
1917
|
+
def _reverse_magnetic_component_standard(adj_updated, *, decay, curl):
|
|
1918
|
+
adj_field = adj_updated * decay
|
|
1919
|
+
adj_d_pos = -curl * adj_updated
|
|
1920
|
+
adj_d_neg = curl * adj_updated
|
|
1921
|
+
return adj_field, adj_d_pos, adj_d_neg
|
|
1922
|
+
|
|
1923
|
+
|
|
1924
|
+
def _reverse_electric_component_standard(
|
|
1925
|
+
adj_updated,
|
|
1926
|
+
field,
|
|
1927
|
+
*,
|
|
1928
|
+
d_pos,
|
|
1929
|
+
d_neg,
|
|
1930
|
+
decay,
|
|
1931
|
+
curl_prefactor,
|
|
1932
|
+
eps,
|
|
1933
|
+
low_mode_pos,
|
|
1934
|
+
high_mode_pos,
|
|
1935
|
+
low_mode_neg,
|
|
1936
|
+
high_mode_neg,
|
|
1937
|
+
axis_pos,
|
|
1938
|
+
axis_neg,
|
|
1939
|
+
):
|
|
1940
|
+
inactive_pos, pec_pos = _boundary_axis_masks(field.shape, axis_pos, low_mode_pos, high_mode_pos, field.device)
|
|
1941
|
+
inactive_neg, pec_neg = _boundary_axis_masks(field.shape, axis_neg, low_mode_neg, high_mode_neg, field.device)
|
|
1942
|
+
pec_mask = pec_pos | pec_neg
|
|
1943
|
+
inactive_mask = (~pec_mask) & (inactive_pos | inactive_neg)
|
|
1944
|
+
active_mask = (~pec_mask) & (~inactive_mask)
|
|
1945
|
+
active = active_mask.to(device=field.device, dtype=field.dtype)
|
|
1946
|
+
inactive = inactive_mask.to(device=field.device, dtype=field.dtype)
|
|
1947
|
+
|
|
1948
|
+
adj_field = inactive * adj_updated + active * (adj_updated * decay)
|
|
1949
|
+
curl_scale = active * (curl_prefactor / eps)
|
|
1950
|
+
adj_curl_term = adj_updated * curl_scale
|
|
1951
|
+
grad_eps = -adj_updated * active * curl_prefactor * (d_pos - d_neg) / (eps * eps)
|
|
1952
|
+
adj_d_pos = adj_curl_term
|
|
1953
|
+
adj_d_neg = -adj_curl_term
|
|
1954
|
+
return adj_field, adj_d_pos, adj_d_neg, grad_eps
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
def _reverse_electric_component_bloch(
|
|
1958
|
+
adj_updated: torch.Tensor,
|
|
1959
|
+
*,
|
|
1960
|
+
d_pos: torch.Tensor,
|
|
1961
|
+
d_neg: torch.Tensor,
|
|
1962
|
+
decay: torch.Tensor,
|
|
1963
|
+
curl_prefactor: torch.Tensor,
|
|
1964
|
+
eps: torch.Tensor,
|
|
1965
|
+
):
|
|
1966
|
+
adj_field = adj_updated * decay
|
|
1967
|
+
curl_scale = curl_prefactor / eps
|
|
1968
|
+
adj_curl_term = adj_updated * curl_scale
|
|
1969
|
+
grad_eps = -curl_prefactor * _complex_inner_real(adj_updated, d_pos - d_neg) / (eps * eps)
|
|
1970
|
+
adj_d_pos = adj_curl_term
|
|
1971
|
+
adj_d_neg = -adj_curl_term
|
|
1972
|
+
return adj_field, adj_d_pos, adj_d_neg, grad_eps
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
def _reverse_magnetic_component_cpml(
|
|
1976
|
+
adj_updated,
|
|
1977
|
+
adj_psi_pos_post,
|
|
1978
|
+
adj_psi_neg_post,
|
|
1979
|
+
*,
|
|
1980
|
+
decay,
|
|
1981
|
+
curl,
|
|
1982
|
+
b_pos,
|
|
1983
|
+
c_pos,
|
|
1984
|
+
inv_kappa_pos,
|
|
1985
|
+
b_neg,
|
|
1986
|
+
c_neg,
|
|
1987
|
+
inv_kappa_neg,
|
|
1988
|
+
axis_pos,
|
|
1989
|
+
axis_neg,
|
|
1990
|
+
):
|
|
1991
|
+
adj_curl_term = -curl * adj_updated
|
|
1992
|
+
adj_psi_pos_candidate = adj_psi_pos_post + adj_curl_term
|
|
1993
|
+
adj_psi_neg_candidate = adj_psi_neg_post - adj_curl_term
|
|
1994
|
+
adj_field = adj_updated * decay
|
|
1995
|
+
adj_psi_pos = _broadcast_vector(b_pos, axis_pos) * adj_psi_pos_candidate
|
|
1996
|
+
adj_psi_neg = _broadcast_vector(b_neg, axis_neg) * adj_psi_neg_candidate
|
|
1997
|
+
adj_d_pos = (
|
|
1998
|
+
_broadcast_vector(inv_kappa_pos, axis_pos) * adj_curl_term
|
|
1999
|
+
+ _broadcast_vector(c_pos, axis_pos) * adj_psi_pos_candidate
|
|
2000
|
+
)
|
|
2001
|
+
adj_d_neg = (
|
|
2002
|
+
-_broadcast_vector(inv_kappa_neg, axis_neg) * adj_curl_term
|
|
2003
|
+
+ _broadcast_vector(c_neg, axis_neg) * adj_psi_neg_candidate
|
|
2004
|
+
)
|
|
2005
|
+
return adj_field, adj_d_pos, adj_d_neg, adj_psi_pos, adj_psi_neg
|
|
2006
|
+
|
|
2007
|
+
|
|
2008
|
+
def _reverse_electric_component_cpml(
|
|
2009
|
+
adj_updated,
|
|
2010
|
+
adj_psi_pos_post,
|
|
2011
|
+
adj_psi_neg_post,
|
|
2012
|
+
field,
|
|
2013
|
+
*,
|
|
2014
|
+
d_pos,
|
|
2015
|
+
d_neg,
|
|
2016
|
+
decay,
|
|
2017
|
+
curl_prefactor,
|
|
2018
|
+
eps,
|
|
2019
|
+
low_mode_pos,
|
|
2020
|
+
high_mode_pos,
|
|
2021
|
+
low_mode_neg,
|
|
2022
|
+
high_mode_neg,
|
|
2023
|
+
axis_pos,
|
|
2024
|
+
axis_neg,
|
|
2025
|
+
psi_pos,
|
|
2026
|
+
psi_neg,
|
|
2027
|
+
b_pos,
|
|
2028
|
+
c_pos,
|
|
2029
|
+
inv_kappa_pos,
|
|
2030
|
+
b_neg,
|
|
2031
|
+
c_neg,
|
|
2032
|
+
inv_kappa_neg,
|
|
2033
|
+
):
|
|
2034
|
+
inactive_pos, pec_pos = _boundary_axis_masks(field.shape, axis_pos, low_mode_pos, high_mode_pos, field.device)
|
|
2035
|
+
inactive_neg, pec_neg = _boundary_axis_masks(field.shape, axis_neg, low_mode_neg, high_mode_neg, field.device)
|
|
2036
|
+
pec_mask = pec_pos | pec_neg
|
|
2037
|
+
inactive_mask = (~pec_mask) & (inactive_pos | inactive_neg)
|
|
2038
|
+
active_mask = (~pec_mask) & (~inactive_mask)
|
|
2039
|
+
keep_mask = inactive_mask | pec_mask
|
|
2040
|
+
|
|
2041
|
+
active = active_mask.to(device=field.device, dtype=field.dtype)
|
|
2042
|
+
inactive = inactive_mask.to(device=field.device, dtype=field.dtype)
|
|
2043
|
+
keep = keep_mask.to(device=field.device, dtype=field.dtype)
|
|
2044
|
+
|
|
2045
|
+
b_pos_term = _broadcast_vector(b_pos, axis_pos)
|
|
2046
|
+
c_pos_term = _broadcast_vector(c_pos, axis_pos)
|
|
2047
|
+
inv_kappa_pos_term = _broadcast_vector(inv_kappa_pos, axis_pos)
|
|
2048
|
+
b_neg_term = _broadcast_vector(b_neg, axis_neg)
|
|
2049
|
+
c_neg_term = _broadcast_vector(c_neg, axis_neg)
|
|
2050
|
+
inv_kappa_neg_term = _broadcast_vector(inv_kappa_neg, axis_neg)
|
|
2051
|
+
|
|
2052
|
+
psi_pos_candidate = b_pos_term * psi_pos + c_pos_term * d_pos
|
|
2053
|
+
psi_neg_candidate = b_neg_term * psi_neg + c_neg_term * d_neg
|
|
2054
|
+
curl_term = (d_pos * inv_kappa_pos_term + psi_pos_candidate) - (d_neg * inv_kappa_neg_term + psi_neg_candidate)
|
|
2055
|
+
|
|
2056
|
+
adj_field = inactive * adj_updated + active * (adj_updated * decay)
|
|
2057
|
+
adj_curl_term = active * adj_updated * (curl_prefactor / eps)
|
|
2058
|
+
grad_eps = -adj_updated * active * curl_prefactor * curl_term / (eps * eps)
|
|
2059
|
+
adj_psi_pos_candidate = active * adj_psi_pos_post + adj_curl_term
|
|
2060
|
+
adj_psi_neg_candidate = active * adj_psi_neg_post - adj_curl_term
|
|
2061
|
+
adj_psi_pos = keep * adj_psi_pos_post + active * (b_pos_term * adj_psi_pos_candidate)
|
|
2062
|
+
adj_psi_neg = keep * adj_psi_neg_post + active * (b_neg_term * adj_psi_neg_candidate)
|
|
2063
|
+
adj_d_pos = inv_kappa_pos_term * adj_curl_term + c_pos_term * adj_psi_pos_candidate
|
|
2064
|
+
adj_d_neg = -inv_kappa_neg_term * adj_curl_term + c_neg_term * adj_psi_neg_candidate
|
|
2065
|
+
return adj_field, adj_d_pos, adj_d_neg, grad_eps, adj_psi_pos, adj_psi_neg
|
|
2066
|
+
|
|
2067
|
+
|
|
2068
|
+
def _enforce_pec_boundaries(solver, fields):
|
|
2069
|
+
updated = dict(fields)
|
|
2070
|
+
face_specs = (
|
|
2071
|
+
(solver.boundary_x_low_code, "low", (("Ey", 0), ("Ez", 0))),
|
|
2072
|
+
(solver.boundary_x_high_code, "high", (("Ey", 0), ("Ez", 0))),
|
|
2073
|
+
(solver.boundary_y_low_code, "low", (("Ex", 1), ("Ez", 1))),
|
|
2074
|
+
(solver.boundary_y_high_code, "high", (("Ex", 1), ("Ez", 1))),
|
|
2075
|
+
(solver.boundary_z_low_code, "low", (("Ex", 2), ("Ey", 2))),
|
|
2076
|
+
(solver.boundary_z_high_code, "high", (("Ex", 2), ("Ey", 2))),
|
|
2077
|
+
)
|
|
2078
|
+
for boundary_code, side, targets in face_specs:
|
|
2079
|
+
if boundary_code != BOUNDARY_PEC:
|
|
2080
|
+
continue
|
|
2081
|
+
face_index = 0 if side == "low" else -1
|
|
2082
|
+
for field_name, axis in targets:
|
|
2083
|
+
tensor = updated[field_name].clone()
|
|
2084
|
+
region = [slice(None)] * tensor.ndim
|
|
2085
|
+
region[axis] = face_index
|
|
2086
|
+
tensor[tuple(region)] = 0.0
|
|
2087
|
+
updated[field_name] = tensor
|
|
2088
|
+
return updated
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
def _complex_state_field(mapping, field_name: str) -> torch.Tensor:
|
|
2092
|
+
return torch.complex(mapping[field_name], mapping[f"{field_name}_imag"])
|
|
2093
|
+
|
|
2094
|
+
|
|
2095
|
+
def _apply_complex_axis_boundary(candidate, previous, *, axis: int, low_mode: int, high_mode: int):
|
|
2096
|
+
inactive_mask, pec_mask = _boundary_axis_masks(candidate.shape, axis, low_mode, high_mode, candidate.device)
|
|
2097
|
+
return torch.where(
|
|
2098
|
+
pec_mask,
|
|
2099
|
+
torch.zeros_like(candidate),
|
|
2100
|
+
torch.where(inactive_mask, previous, candidate),
|
|
2101
|
+
)
|
|
2102
|
+
|
|
2103
|
+
|
|
2104
|
+
def _cpml_correction_mask(shape, *, normal_axis: int, tangent_axis: int, tangent_low_mode: int, tangent_high_mode: int, device):
|
|
2105
|
+
mask = torch.ones(shape, device=device, dtype=torch.bool)
|
|
2106
|
+
low_face = [slice(None)] * len(shape)
|
|
2107
|
+
high_face = [slice(None)] * len(shape)
|
|
2108
|
+
low_face[normal_axis] = 0
|
|
2109
|
+
high_face[normal_axis] = -1
|
|
2110
|
+
mask[tuple(low_face)] = False
|
|
2111
|
+
mask[tuple(high_face)] = False
|
|
2112
|
+
inactive_tangent, pec_tangent = _boundary_axis_masks(
|
|
2113
|
+
shape,
|
|
2114
|
+
tangent_axis,
|
|
2115
|
+
tangent_low_mode,
|
|
2116
|
+
tangent_high_mode,
|
|
2117
|
+
device,
|
|
2118
|
+
)
|
|
2119
|
+
return mask & (~inactive_tangent) & (~pec_tangent)
|
|
2120
|
+
|
|
2121
|
+
|
|
2122
|
+
def _apply_complex_cpml_correction(
|
|
2123
|
+
field: torch.Tensor,
|
|
2124
|
+
psi: torch.Tensor,
|
|
2125
|
+
derivative: torch.Tensor,
|
|
2126
|
+
*,
|
|
2127
|
+
curl: torch.Tensor,
|
|
2128
|
+
inv_kappa: torch.Tensor,
|
|
2129
|
+
b: torch.Tensor,
|
|
2130
|
+
c: torch.Tensor,
|
|
2131
|
+
normal_axis: int,
|
|
2132
|
+
tangent_axis: int,
|
|
2133
|
+
tangent_low_mode: int,
|
|
2134
|
+
tangent_high_mode: int,
|
|
2135
|
+
sign: float,
|
|
2136
|
+
):
|
|
2137
|
+
b_term = _broadcast_vector(b, normal_axis)
|
|
2138
|
+
c_term = _broadcast_vector(c, normal_axis)
|
|
2139
|
+
inv_kappa_term = _broadcast_vector(inv_kappa, normal_axis)
|
|
2140
|
+
psi_candidate = b_term * psi + c_term * derivative
|
|
2141
|
+
correction = curl * (derivative * (inv_kappa_term - 1.0) + psi_candidate)
|
|
2142
|
+
candidate = field + float(sign) * correction
|
|
2143
|
+
mask = _cpml_correction_mask(
|
|
2144
|
+
field.shape,
|
|
2145
|
+
normal_axis=normal_axis,
|
|
2146
|
+
tangent_axis=tangent_axis,
|
|
2147
|
+
tangent_low_mode=tangent_low_mode,
|
|
2148
|
+
tangent_high_mode=tangent_high_mode,
|
|
2149
|
+
device=field.device,
|
|
2150
|
+
)
|
|
2151
|
+
return torch.where(mask, candidate, field), torch.where(mask, psi_candidate, psi)
|
|
2152
|
+
|
|
2153
|
+
|
|
2154
|
+
def _update_mixed_bloch_cpml_electric_fields(solver, state, magnetic_fields, *, ex_curl, ey_curl, ez_curl):
|
|
2155
|
+
hx_complex = _complex_state_field(magnetic_fields, "Hx")
|
|
2156
|
+
hy_complex = _complex_state_field(magnetic_fields, "Hy")
|
|
2157
|
+
hz_complex = _complex_state_field(magnetic_fields, "Hz")
|
|
2158
|
+
|
|
2159
|
+
previous_ex = _complex_state_field(state, "Ex")
|
|
2160
|
+
d_hz_dy = _bloch_backward_diff(
|
|
2161
|
+
hz_complex,
|
|
2162
|
+
axis=1,
|
|
2163
|
+
inv_delta=solver.inv_dy_e,
|
|
2164
|
+
phase_cos=float(solver.boundary_phase_cos[1]),
|
|
2165
|
+
phase_sin=float(solver.boundary_phase_sin[1]),
|
|
2166
|
+
)
|
|
2167
|
+
d_hy_dz = _backward_diff(hy_complex, axis=2, inv_delta=solver.inv_dz_e)
|
|
2168
|
+
ex_complex = previous_ex * solver.cex_decay + ex_curl * (d_hz_dy - d_hy_dz)
|
|
2169
|
+
ex_complex = _apply_complex_axis_boundary(
|
|
2170
|
+
ex_complex,
|
|
2171
|
+
previous_ex,
|
|
2172
|
+
axis=2,
|
|
2173
|
+
low_mode=solver.boundary_z_low_code,
|
|
2174
|
+
high_mode=solver.boundary_z_high_code,
|
|
2175
|
+
)
|
|
2176
|
+
psi_ex_z = torch.complex(state["psi_ex_z"], state["psi_ex_z_imag"])
|
|
2177
|
+
ex_complex, psi_ex_z = _apply_complex_cpml_correction(
|
|
2178
|
+
ex_complex,
|
|
2179
|
+
psi_ex_z,
|
|
2180
|
+
d_hy_dz,
|
|
2181
|
+
curl=ex_curl,
|
|
2182
|
+
inv_kappa=solver.cpml_inv_kappa_e_z,
|
|
2183
|
+
b=solver.cpml_b_e_z,
|
|
2184
|
+
c=solver.cpml_c_e_z,
|
|
2185
|
+
normal_axis=2,
|
|
2186
|
+
tangent_axis=1,
|
|
2187
|
+
tangent_low_mode=solver.boundary_y_low_code,
|
|
2188
|
+
tangent_high_mode=solver.boundary_y_high_code,
|
|
2189
|
+
sign=-1.0,
|
|
2190
|
+
)
|
|
2191
|
+
|
|
2192
|
+
previous_ey = _complex_state_field(state, "Ey")
|
|
2193
|
+
d_hx_dz = _backward_diff(hx_complex, axis=2, inv_delta=solver.inv_dz_e)
|
|
2194
|
+
d_hz_dx = _bloch_backward_diff(
|
|
2195
|
+
hz_complex,
|
|
2196
|
+
axis=0,
|
|
2197
|
+
inv_delta=solver.inv_dx_e,
|
|
2198
|
+
phase_cos=float(solver.boundary_phase_cos[0]),
|
|
2199
|
+
phase_sin=float(solver.boundary_phase_sin[0]),
|
|
2200
|
+
)
|
|
2201
|
+
ey_complex = previous_ey * solver.cey_decay + ey_curl * (d_hx_dz - d_hz_dx)
|
|
2202
|
+
ey_complex = _apply_complex_axis_boundary(
|
|
2203
|
+
ey_complex,
|
|
2204
|
+
previous_ey,
|
|
2205
|
+
axis=2,
|
|
2206
|
+
low_mode=solver.boundary_z_low_code,
|
|
2207
|
+
high_mode=solver.boundary_z_high_code,
|
|
2208
|
+
)
|
|
2209
|
+
psi_ey_z = torch.complex(state["psi_ey_z"], state["psi_ey_z_imag"])
|
|
2210
|
+
ey_complex, psi_ey_z = _apply_complex_cpml_correction(
|
|
2211
|
+
ey_complex,
|
|
2212
|
+
psi_ey_z,
|
|
2213
|
+
d_hx_dz,
|
|
2214
|
+
curl=ey_curl,
|
|
2215
|
+
inv_kappa=solver.cpml_inv_kappa_e_z,
|
|
2216
|
+
b=solver.cpml_b_e_z,
|
|
2217
|
+
c=solver.cpml_c_e_z,
|
|
2218
|
+
normal_axis=2,
|
|
2219
|
+
tangent_axis=0,
|
|
2220
|
+
tangent_low_mode=solver.boundary_x_low_code,
|
|
2221
|
+
tangent_high_mode=solver.boundary_x_high_code,
|
|
2222
|
+
sign=1.0,
|
|
2223
|
+
)
|
|
2224
|
+
|
|
2225
|
+
previous_ez = _complex_state_field(state, "Ez")
|
|
2226
|
+
d_hy_dx = _bloch_backward_diff(
|
|
2227
|
+
hy_complex,
|
|
2228
|
+
axis=0,
|
|
2229
|
+
inv_delta=solver.inv_dx_e,
|
|
2230
|
+
phase_cos=float(solver.boundary_phase_cos[0]),
|
|
2231
|
+
phase_sin=float(solver.boundary_phase_sin[0]),
|
|
2232
|
+
)
|
|
2233
|
+
d_hx_dy = _bloch_backward_diff(
|
|
2234
|
+
hx_complex,
|
|
2235
|
+
axis=1,
|
|
2236
|
+
inv_delta=solver.inv_dy_e,
|
|
2237
|
+
phase_cos=float(solver.boundary_phase_cos[1]),
|
|
2238
|
+
phase_sin=float(solver.boundary_phase_sin[1]),
|
|
2239
|
+
)
|
|
2240
|
+
ez_complex = previous_ez * solver.cez_decay + ez_curl * (d_hy_dx - d_hx_dy)
|
|
2241
|
+
|
|
2242
|
+
electric_fields = {
|
|
2243
|
+
"Ex": ex_complex.real,
|
|
2244
|
+
"Ey": ey_complex.real,
|
|
2245
|
+
"Ez": ez_complex.real,
|
|
2246
|
+
"Ex_imag": ex_complex.imag,
|
|
2247
|
+
"Ey_imag": ey_complex.imag,
|
|
2248
|
+
"Ez_imag": ez_complex.imag,
|
|
2249
|
+
}
|
|
2250
|
+
electric_cpml_state = {
|
|
2251
|
+
"psi_ex_y": state["psi_ex_y"],
|
|
2252
|
+
"psi_ex_z": psi_ex_z.real,
|
|
2253
|
+
"psi_ey_x": state["psi_ey_x"],
|
|
2254
|
+
"psi_ey_z": psi_ey_z.real,
|
|
2255
|
+
"psi_ez_x": state["psi_ez_x"],
|
|
2256
|
+
"psi_ez_y": state["psi_ez_y"],
|
|
2257
|
+
"psi_ex_y_imag": state["psi_ex_y_imag"],
|
|
2258
|
+
"psi_ex_z_imag": psi_ex_z.imag,
|
|
2259
|
+
"psi_ey_x_imag": state["psi_ey_x_imag"],
|
|
2260
|
+
"psi_ey_z_imag": psi_ey_z.imag,
|
|
2261
|
+
"psi_ez_x_imag": state["psi_ez_x_imag"],
|
|
2262
|
+
"psi_ez_y_imag": state["psi_ez_y_imag"],
|
|
2263
|
+
}
|
|
2264
|
+
return electric_fields, electric_cpml_state
|
|
2265
|
+
|
|
2266
|
+
|
|
2267
|
+
def _update_general_bloch_cpml_electric_fields(
|
|
2268
|
+
solver, state, magnetic_fields, *, pml_axis, ex_curl, ey_curl, ez_curl
|
|
2269
|
+
):
|
|
2270
|
+
"""Reverse replay of the general single-PML-axis (x or y) Bloch+CPML electric
|
|
2271
|
+
update. The forward runs the full-Bloch base (``update_electric_fields_bloch``)
|
|
2272
|
+
with the true wrap phase on the two periodic axes and the natural unit phase on
|
|
2273
|
+
the absorbing axis, then folds the recursive-convolution stretch into the two
|
|
2274
|
+
components with an absorbing-axis derivative. This mirrors that step exactly so
|
|
2275
|
+
the field, eps, and source VJPs stay consistent with the forward kernels."""
|
|
2276
|
+
hx = _complex_state_field(magnetic_fields, "Hx")
|
|
2277
|
+
hy = _complex_state_field(magnetic_fields, "Hy")
|
|
2278
|
+
hz = _complex_state_field(magnetic_fields, "Hz")
|
|
2279
|
+
phase_cos = solver.boundary_phase_cos
|
|
2280
|
+
phase_sin = solver.boundary_phase_sin
|
|
2281
|
+
|
|
2282
|
+
def _bdiff(field, axis, inv_delta):
|
|
2283
|
+
return _bloch_backward_diff(
|
|
2284
|
+
field,
|
|
2285
|
+
axis=axis,
|
|
2286
|
+
inv_delta=inv_delta,
|
|
2287
|
+
phase_cos=float(phase_cos[axis]),
|
|
2288
|
+
phase_sin=float(phase_sin[axis]),
|
|
2289
|
+
)
|
|
2290
|
+
|
|
2291
|
+
d_hz_dy = _bdiff(hz, 1, solver.inv_dy_e)
|
|
2292
|
+
d_hy_dz = _bdiff(hy, 2, solver.inv_dz_e)
|
|
2293
|
+
ex = _complex_state_field(state, "Ex") * solver.cex_decay + ex_curl * (d_hz_dy - d_hy_dz)
|
|
2294
|
+
|
|
2295
|
+
d_hx_dz = _bdiff(hx, 2, solver.inv_dz_e)
|
|
2296
|
+
d_hz_dx = _bdiff(hz, 0, solver.inv_dx_e)
|
|
2297
|
+
ey = _complex_state_field(state, "Ey") * solver.cey_decay + ey_curl * (d_hx_dz - d_hz_dx)
|
|
2298
|
+
|
|
2299
|
+
d_hy_dx = _bdiff(hy, 0, solver.inv_dx_e)
|
|
2300
|
+
d_hx_dy = _bdiff(hx, 1, solver.inv_dy_e)
|
|
2301
|
+
ez = _complex_state_field(state, "Ez") * solver.cez_decay + ez_curl * (d_hy_dx - d_hx_dy)
|
|
2302
|
+
|
|
2303
|
+
psi = {
|
|
2304
|
+
name: torch.complex(state[name], state[f"{name}_imag"])
|
|
2305
|
+
for name in ("psi_ex_y", "psi_ex_z", "psi_ey_x", "psi_ey_z", "psi_ez_x", "psi_ez_y")
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
if pml_axis == "y":
|
|
2309
|
+
ex, psi["psi_ex_y"] = _apply_complex_cpml_correction(
|
|
2310
|
+
ex,
|
|
2311
|
+
psi["psi_ex_y"],
|
|
2312
|
+
d_hz_dy,
|
|
2313
|
+
curl=ex_curl,
|
|
2314
|
+
inv_kappa=solver.cpml_inv_kappa_e_y,
|
|
2315
|
+
b=solver.cpml_b_e_y,
|
|
2316
|
+
c=solver.cpml_c_e_y,
|
|
2317
|
+
normal_axis=1,
|
|
2318
|
+
tangent_axis=2,
|
|
2319
|
+
tangent_low_mode=solver.boundary_z_low_code,
|
|
2320
|
+
tangent_high_mode=solver.boundary_z_high_code,
|
|
2321
|
+
sign=1.0,
|
|
2322
|
+
)
|
|
2323
|
+
ez, psi["psi_ez_y"] = _apply_complex_cpml_correction(
|
|
2324
|
+
ez,
|
|
2325
|
+
psi["psi_ez_y"],
|
|
2326
|
+
d_hx_dy,
|
|
2327
|
+
curl=ez_curl,
|
|
2328
|
+
inv_kappa=solver.cpml_inv_kappa_e_y,
|
|
2329
|
+
b=solver.cpml_b_e_y,
|
|
2330
|
+
c=solver.cpml_c_e_y,
|
|
2331
|
+
normal_axis=1,
|
|
2332
|
+
tangent_axis=0,
|
|
2333
|
+
tangent_low_mode=solver.boundary_x_low_code,
|
|
2334
|
+
tangent_high_mode=solver.boundary_x_high_code,
|
|
2335
|
+
sign=-1.0,
|
|
2336
|
+
)
|
|
2337
|
+
else: # pml_axis == "x"
|
|
2338
|
+
ey, psi["psi_ey_x"] = _apply_complex_cpml_correction(
|
|
2339
|
+
ey,
|
|
2340
|
+
psi["psi_ey_x"],
|
|
2341
|
+
d_hz_dx,
|
|
2342
|
+
curl=ey_curl,
|
|
2343
|
+
inv_kappa=solver.cpml_inv_kappa_e_x,
|
|
2344
|
+
b=solver.cpml_b_e_x,
|
|
2345
|
+
c=solver.cpml_c_e_x,
|
|
2346
|
+
normal_axis=0,
|
|
2347
|
+
tangent_axis=2,
|
|
2348
|
+
tangent_low_mode=solver.boundary_z_low_code,
|
|
2349
|
+
tangent_high_mode=solver.boundary_z_high_code,
|
|
2350
|
+
sign=-1.0,
|
|
2351
|
+
)
|
|
2352
|
+
ez, psi["psi_ez_x"] = _apply_complex_cpml_correction(
|
|
2353
|
+
ez,
|
|
2354
|
+
psi["psi_ez_x"],
|
|
2355
|
+
d_hy_dx,
|
|
2356
|
+
curl=ez_curl,
|
|
2357
|
+
inv_kappa=solver.cpml_inv_kappa_e_x,
|
|
2358
|
+
b=solver.cpml_b_e_x,
|
|
2359
|
+
c=solver.cpml_c_e_x,
|
|
2360
|
+
normal_axis=0,
|
|
2361
|
+
tangent_axis=1,
|
|
2362
|
+
tangent_low_mode=solver.boundary_y_low_code,
|
|
2363
|
+
tangent_high_mode=solver.boundary_y_high_code,
|
|
2364
|
+
sign=1.0,
|
|
2365
|
+
)
|
|
2366
|
+
|
|
2367
|
+
electric_fields = {
|
|
2368
|
+
"Ex": ex.real,
|
|
2369
|
+
"Ey": ey.real,
|
|
2370
|
+
"Ez": ez.real,
|
|
2371
|
+
"Ex_imag": ex.imag,
|
|
2372
|
+
"Ey_imag": ey.imag,
|
|
2373
|
+
"Ez_imag": ez.imag,
|
|
2374
|
+
}
|
|
2375
|
+
electric_cpml_state = {}
|
|
2376
|
+
for name, value in psi.items():
|
|
2377
|
+
electric_cpml_state[name] = value.real
|
|
2378
|
+
electric_cpml_state[f"{name}_imag"] = value.imag
|
|
2379
|
+
return electric_fields, electric_cpml_state
|
|
2380
|
+
|
|
2381
|
+
|
|
2382
|
+
def _clamped_center_pair_sum(field: torch.Tensor, axis: int) -> torch.Tensor:
|
|
2383
|
+
"""Sum of ``field[clamp(i-1)] + field[clamp(i)]`` for ``i in 0..size`` along ``axis``.
|
|
2384
|
+
|
|
2385
|
+
Replicates the index-clamped two-point gather of the CUDA collocation helper:
|
|
2386
|
+
the output is one element longer than the input along ``axis``.
|
|
2387
|
+
"""
|
|
2388
|
+
first = field.narrow(axis, 0, 1)
|
|
2389
|
+
last = field.narrow(axis, field.shape[axis] - 1, 1)
|
|
2390
|
+
padded = torch.cat([first, field, last], dim=axis)
|
|
2391
|
+
length = field.shape[axis] + 1
|
|
2392
|
+
return padded.narrow(axis, 0, length) + padded.narrow(axis, 1, length)
|
|
2393
|
+
|
|
2394
|
+
|
|
2395
|
+
def _forward_pair_sum(field: torch.Tensor, axis: int) -> torch.Tensor:
|
|
2396
|
+
"""Sum of ``field[i] + field[i+1]`` for ``i in 0..size-2`` along ``axis``."""
|
|
2397
|
+
length = field.shape[axis] - 1
|
|
2398
|
+
return field.narrow(axis, 0, length) + field.narrow(axis, 1, length)
|
|
2399
|
+
|
|
2400
|
+
|
|
2401
|
+
def _collocated_field_square(ex, ey, ez):
|
|
2402
|
+
"""|E|^2 collocated onto each electric Yee edge, matching the CUDA
|
|
2403
|
+
``collocate_electric_components`` stencil (4-point off-axis average with
|
|
2404
|
+
index clamping at the domain faces)."""
|
|
2405
|
+
ey_on_ex = 0.25 * _forward_pair_sum(_clamped_center_pair_sum(ey, axis=1), axis=0)
|
|
2406
|
+
ez_on_ex = 0.25 * _forward_pair_sum(_clamped_center_pair_sum(ez, axis=2), axis=0)
|
|
2407
|
+
ex_on_ey = 0.25 * _forward_pair_sum(_clamped_center_pair_sum(ex, axis=0), axis=1)
|
|
2408
|
+
ez_on_ey = 0.25 * _forward_pair_sum(_clamped_center_pair_sum(ez, axis=2), axis=1)
|
|
2409
|
+
ex_on_ez = 0.25 * _forward_pair_sum(_clamped_center_pair_sum(ex, axis=0), axis=2)
|
|
2410
|
+
ey_on_ez = 0.25 * _forward_pair_sum(_clamped_center_pair_sum(ey, axis=1), axis=2)
|
|
2411
|
+
return {
|
|
2412
|
+
"Ex": ex * ex + ey_on_ex * ey_on_ex + ez_on_ex * ez_on_ex,
|
|
2413
|
+
"Ey": ex_on_ey * ex_on_ey + ey * ey + ez_on_ey * ez_on_ey,
|
|
2414
|
+
"Ez": ex_on_ez * ex_on_ez + ey_on_ez * ey_on_ez + ez * ez,
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
|
|
2418
|
+
def _kerr_dynamic_electric_curls(solver, state, *, eps_ex, eps_ey, eps_ez, chi3_ex, chi3_ey, chi3_ez):
|
|
2419
|
+
"""Differentiable replica of ``updateKerrElectricField*Curl3D``:
|
|
2420
|
+
``curl = dt / max(eps + eps0 * chi3 * |E|^2, floor) * decay``."""
|
|
2421
|
+
field_square = _collocated_field_square(state["Ex"], state["Ey"], state["Ez"])
|
|
2422
|
+
eps0 = float(solver.eps0)
|
|
2423
|
+
floor = 1.0e-12 * eps0
|
|
2424
|
+
dt = float(solver.dt)
|
|
2425
|
+
curls = {}
|
|
2426
|
+
for component_name, eps, chi3, decay in (
|
|
2427
|
+
("Ex", eps_ex, chi3_ex, solver.cex_decay),
|
|
2428
|
+
("Ey", eps_ey, chi3_ey, solver.cey_decay),
|
|
2429
|
+
("Ez", eps_ez, chi3_ez, solver.cez_decay),
|
|
2430
|
+
):
|
|
2431
|
+
effective = torch.clamp_min(eps + eps0 * chi3 * field_square[component_name], floor)
|
|
2432
|
+
curls[component_name] = (dt / effective) * decay
|
|
2433
|
+
return curls
|
|
2434
|
+
|
|
2435
|
+
|
|
2436
|
+
def _conductive_electric_coefficients(solver, *, eps_ex, eps_ey, eps_ez):
|
|
2437
|
+
"""Differentiable replica of the semi-implicit lossy ``_electric_update_coefficients``.
|
|
2438
|
+
|
|
2439
|
+
The forward bakes ``decay = (1 - h)/(1 + h)`` and ``curl = (dt/eps)/(1 + h)``
|
|
2440
|
+
(with ``h = 0.5*sigma_e*dt/eps``, times an eps-independent PML/PEC factor)
|
|
2441
|
+
into ``c*_decay`` / ``c*_curl``. Both carry an ``eps`` dependence that the
|
|
2442
|
+
linear-dielectric reverse rule drops, so recompute them from the
|
|
2443
|
+
differentiable ``eps`` leaves and the frozen per-edge conductivity. The
|
|
2444
|
+
eps-independent factor (PML split-field decay and any PEC open fraction) is
|
|
2445
|
+
recovered from the frozen forward curl so those scalings are preserved
|
|
2446
|
+
exactly. Returns per-component ``(decay, curl)`` where ``curl`` is the full
|
|
2447
|
+
curl coefficient (fed with ``eps = 1`` in the update, mirroring the Kerr
|
|
2448
|
+
dynamic-curl path)."""
|
|
2449
|
+
dt = float(solver.dt)
|
|
2450
|
+
coeffs = {}
|
|
2451
|
+
for name, eps, eps_fwd, sigma, curl_fwd in (
|
|
2452
|
+
("Ex", eps_ex, solver.eps_Ex, solver.sigma_e_Ex, solver.cex_curl),
|
|
2453
|
+
("Ey", eps_ey, solver.eps_Ey, solver.sigma_e_Ey, solver.cey_curl),
|
|
2454
|
+
("Ez", eps_ez, solver.eps_Ez, solver.sigma_e_Ez, solver.cez_curl),
|
|
2455
|
+
):
|
|
2456
|
+
half_fwd = 0.5 * sigma * dt / eps_fwd
|
|
2457
|
+
# curl_fwd = (dt / eps_fwd) / (1 + half_fwd) * factor -> solve for factor.
|
|
2458
|
+
factor = curl_fwd * eps_fwd * (1.0 + half_fwd) / dt
|
|
2459
|
+
half = 0.5 * sigma * dt / eps
|
|
2460
|
+
denom = 1.0 + half
|
|
2461
|
+
decay = ((1.0 - half) / denom) * factor
|
|
2462
|
+
curl = (dt / eps / denom) * factor
|
|
2463
|
+
coeffs[name] = (decay, curl)
|
|
2464
|
+
return coeffs
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
def _conductive_reverse_coefficients(solver, *, eps_ex, eps_ey, eps_ez):
|
|
2468
|
+
"""Per-component ``(decay, curl, half)`` for the static-conductive reverse.
|
|
2469
|
+
|
|
2470
|
+
Identical bake to :func:`_conductive_electric_coefficients` but also returns
|
|
2471
|
+
the semi-implicit ``half = 0.5*sigma_e*dt/eps`` (recomputed from the
|
|
2472
|
+
differentiable ``eps`` leaf) so the reverse can form the analytic
|
|
2473
|
+
``d(decay)/d(eps)`` and ``d(curl)/d(eps)`` sensitivities without an autograd
|
|
2474
|
+
VJP: ``d(decay)/d(eps) = 2*half*curl/(dt*(1+half))`` and
|
|
2475
|
+
``d(curl)/d(eps) = -curl/(eps*(1+half))``. The eps-independent PML/PEC
|
|
2476
|
+
``factor`` is recovered from the frozen forward curl so those scalings are
|
|
2477
|
+
preserved exactly."""
|
|
2478
|
+
dt = float(solver.dt)
|
|
2479
|
+
coeffs = {}
|
|
2480
|
+
for name, eps, eps_fwd, sigma, curl_fwd in (
|
|
2481
|
+
("Ex", eps_ex, solver.eps_Ex, solver.sigma_e_Ex, solver.cex_curl),
|
|
2482
|
+
("Ey", eps_ey, solver.eps_Ey, solver.sigma_e_Ey, solver.cey_curl),
|
|
2483
|
+
("Ez", eps_ez, solver.eps_Ez, solver.sigma_e_Ez, solver.cez_curl),
|
|
2484
|
+
):
|
|
2485
|
+
half_fwd = 0.5 * sigma * dt / eps_fwd
|
|
2486
|
+
factor = curl_fwd * eps_fwd * (1.0 + half_fwd) / dt
|
|
2487
|
+
half = 0.5 * sigma * dt / eps
|
|
2488
|
+
denom = 1.0 + half
|
|
2489
|
+
decay = ((1.0 - half) / denom) * factor
|
|
2490
|
+
curl = (dt / eps / denom) * factor
|
|
2491
|
+
coeffs[name] = (decay, curl, half)
|
|
2492
|
+
return coeffs
|
|
2493
|
+
|
|
2494
|
+
|
|
2495
|
+
def _reverse_electric_component_cpml_conductive(
|
|
2496
|
+
adj_updated,
|
|
2497
|
+
adj_psi_pos_post,
|
|
2498
|
+
adj_psi_neg_post,
|
|
2499
|
+
field,
|
|
2500
|
+
*,
|
|
2501
|
+
d_pos,
|
|
2502
|
+
d_neg,
|
|
2503
|
+
decay,
|
|
2504
|
+
curl,
|
|
2505
|
+
half,
|
|
2506
|
+
eps,
|
|
2507
|
+
dt,
|
|
2508
|
+
low_mode_pos,
|
|
2509
|
+
high_mode_pos,
|
|
2510
|
+
low_mode_neg,
|
|
2511
|
+
high_mode_neg,
|
|
2512
|
+
axis_pos,
|
|
2513
|
+
axis_neg,
|
|
2514
|
+
psi_pos,
|
|
2515
|
+
psi_neg,
|
|
2516
|
+
b_pos,
|
|
2517
|
+
c_pos,
|
|
2518
|
+
inv_kappa_pos,
|
|
2519
|
+
b_neg,
|
|
2520
|
+
c_neg,
|
|
2521
|
+
inv_kappa_neg,
|
|
2522
|
+
):
|
|
2523
|
+
"""Static-conductive analytic reverse of the semi-implicit CPML electric update.
|
|
2524
|
+
|
|
2525
|
+
Structurally identical to :func:`_reverse_electric_component_cpml` (the psi
|
|
2526
|
+
recursion, ``adj_d_pos``/``adj_d_neg`` folds, and pre-step field pullback all
|
|
2527
|
+
reuse the CPML stretch unchanged, because the conductive update only rescales
|
|
2528
|
+
the ``decay``/``curl`` coefficient pair). The one difference is the eps
|
|
2529
|
+
gradient: the semi-implicit ``decay`` and ``curl`` both depend on ``eps``
|
|
2530
|
+
through the loss denominator, so the linear rule ``-curl*curl_term/eps^2`` is
|
|
2531
|
+
replaced by the exact
|
|
2532
|
+
``adj_updated*(E_prev*d(decay)/d(eps) + curl_term*d(curl)/d(eps))``."""
|
|
2533
|
+
inactive_pos, pec_pos = _boundary_axis_masks(field.shape, axis_pos, low_mode_pos, high_mode_pos, field.device)
|
|
2534
|
+
inactive_neg, pec_neg = _boundary_axis_masks(field.shape, axis_neg, low_mode_neg, high_mode_neg, field.device)
|
|
2535
|
+
pec_mask = pec_pos | pec_neg
|
|
2536
|
+
inactive_mask = (~pec_mask) & (inactive_pos | inactive_neg)
|
|
2537
|
+
active_mask = (~pec_mask) & (~inactive_mask)
|
|
2538
|
+
keep_mask = inactive_mask | pec_mask
|
|
2539
|
+
|
|
2540
|
+
active = active_mask.to(device=field.device, dtype=field.dtype)
|
|
2541
|
+
inactive = inactive_mask.to(device=field.device, dtype=field.dtype)
|
|
2542
|
+
keep = keep_mask.to(device=field.device, dtype=field.dtype)
|
|
2543
|
+
|
|
2544
|
+
b_pos_term = _broadcast_vector(b_pos, axis_pos)
|
|
2545
|
+
c_pos_term = _broadcast_vector(c_pos, axis_pos)
|
|
2546
|
+
inv_kappa_pos_term = _broadcast_vector(inv_kappa_pos, axis_pos)
|
|
2547
|
+
b_neg_term = _broadcast_vector(b_neg, axis_neg)
|
|
2548
|
+
c_neg_term = _broadcast_vector(c_neg, axis_neg)
|
|
2549
|
+
inv_kappa_neg_term = _broadcast_vector(inv_kappa_neg, axis_neg)
|
|
2550
|
+
|
|
2551
|
+
psi_pos_candidate = b_pos_term * psi_pos + c_pos_term * d_pos
|
|
2552
|
+
psi_neg_candidate = b_neg_term * psi_neg + c_neg_term * d_neg
|
|
2553
|
+
curl_term = (d_pos * inv_kappa_pos_term + psi_pos_candidate) - (d_neg * inv_kappa_neg_term + psi_neg_candidate)
|
|
2554
|
+
|
|
2555
|
+
denom = 1.0 + half
|
|
2556
|
+
d_decay_d_eps = 2.0 * half * curl / (float(dt) * denom)
|
|
2557
|
+
d_curl_d_eps = -curl / (eps * denom)
|
|
2558
|
+
|
|
2559
|
+
adj_field = inactive * adj_updated + active * (adj_updated * decay)
|
|
2560
|
+
adj_curl_term = active * adj_updated * curl
|
|
2561
|
+
grad_eps = active * adj_updated * (field * d_decay_d_eps + curl_term * d_curl_d_eps)
|
|
2562
|
+
adj_psi_pos_candidate = active * adj_psi_pos_post + adj_curl_term
|
|
2563
|
+
adj_psi_neg_candidate = active * adj_psi_neg_post - adj_curl_term
|
|
2564
|
+
adj_psi_pos = keep * adj_psi_pos_post + active * (b_pos_term * adj_psi_pos_candidate)
|
|
2565
|
+
adj_psi_neg = keep * adj_psi_neg_post + active * (b_neg_term * adj_psi_neg_candidate)
|
|
2566
|
+
adj_d_pos = inv_kappa_pos_term * adj_curl_term + c_pos_term * adj_psi_pos_candidate
|
|
2567
|
+
adj_d_neg = -inv_kappa_neg_term * adj_curl_term + c_neg_term * adj_psi_neg_candidate
|
|
2568
|
+
return adj_field, adj_d_pos, adj_d_neg, grad_eps, adj_psi_pos, adj_psi_neg
|
|
2569
|
+
|
|
2570
|
+
|
|
2571
|
+
def _reverse_electric_component_cpml_nonlinear(
|
|
2572
|
+
adj_updated,
|
|
2573
|
+
adj_psi_pos_post,
|
|
2574
|
+
adj_psi_neg_post,
|
|
2575
|
+
field,
|
|
2576
|
+
*,
|
|
2577
|
+
d_pos,
|
|
2578
|
+
d_neg,
|
|
2579
|
+
decay,
|
|
2580
|
+
curl,
|
|
2581
|
+
low_mode_pos,
|
|
2582
|
+
high_mode_pos,
|
|
2583
|
+
low_mode_neg,
|
|
2584
|
+
high_mode_neg,
|
|
2585
|
+
axis_pos,
|
|
2586
|
+
axis_neg,
|
|
2587
|
+
psi_pos,
|
|
2588
|
+
psi_neg,
|
|
2589
|
+
b_pos,
|
|
2590
|
+
c_pos,
|
|
2591
|
+
inv_kappa_pos,
|
|
2592
|
+
b_neg,
|
|
2593
|
+
c_neg,
|
|
2594
|
+
inv_kappa_neg,
|
|
2595
|
+
):
|
|
2596
|
+
"""Reverse of the CPML electric update with GIVEN per-edge decay/curl pair.
|
|
2597
|
+
|
|
2598
|
+
Unlike :func:`_reverse_electric_component_cpml` (which bakes the linear
|
|
2599
|
+
``curl_prefactor/eps`` rule and returns ``grad_eps``), this returns the raw
|
|
2600
|
+
cotangents ``adj_decay`` and ``adj_curl`` to the field-dependent coefficient
|
|
2601
|
+
pair so a nonlinear coefficient reverse (Kerr / general) can push them onto the
|
|
2602
|
+
``eps`` / ``chi2`` / ``chi3`` / ``tpa`` leaves and (via the collocation
|
|
2603
|
+
transpose) the pre-step fields. The psi recursion, the ``adj_d_pos``/
|
|
2604
|
+
``adj_d_neg`` curl(H) folds, and the pre-step field pullback are the CPML
|
|
2605
|
+
machinery unchanged (a field-dependent coefficient only rescales the update)."""
|
|
2606
|
+
inactive_pos, pec_pos = _boundary_axis_masks(field.shape, axis_pos, low_mode_pos, high_mode_pos, field.device)
|
|
2607
|
+
inactive_neg, pec_neg = _boundary_axis_masks(field.shape, axis_neg, low_mode_neg, high_mode_neg, field.device)
|
|
2608
|
+
pec_mask = pec_pos | pec_neg
|
|
2609
|
+
inactive_mask = (~pec_mask) & (inactive_pos | inactive_neg)
|
|
2610
|
+
active_mask = (~pec_mask) & (~inactive_mask)
|
|
2611
|
+
keep_mask = inactive_mask | pec_mask
|
|
2612
|
+
|
|
2613
|
+
active = active_mask.to(device=field.device, dtype=field.dtype)
|
|
2614
|
+
inactive = inactive_mask.to(device=field.device, dtype=field.dtype)
|
|
2615
|
+
keep = keep_mask.to(device=field.device, dtype=field.dtype)
|
|
2616
|
+
|
|
2617
|
+
b_pos_term = _broadcast_vector(b_pos, axis_pos)
|
|
2618
|
+
c_pos_term = _broadcast_vector(c_pos, axis_pos)
|
|
2619
|
+
inv_kappa_pos_term = _broadcast_vector(inv_kappa_pos, axis_pos)
|
|
2620
|
+
b_neg_term = _broadcast_vector(b_neg, axis_neg)
|
|
2621
|
+
c_neg_term = _broadcast_vector(c_neg, axis_neg)
|
|
2622
|
+
inv_kappa_neg_term = _broadcast_vector(inv_kappa_neg, axis_neg)
|
|
2623
|
+
|
|
2624
|
+
psi_pos_candidate = b_pos_term * psi_pos + c_pos_term * d_pos
|
|
2625
|
+
psi_neg_candidate = b_neg_term * psi_neg + c_neg_term * d_neg
|
|
2626
|
+
curl_term = (d_pos * inv_kappa_pos_term + psi_pos_candidate) - (d_neg * inv_kappa_neg_term + psi_neg_candidate)
|
|
2627
|
+
|
|
2628
|
+
adj_field = inactive * adj_updated + active * (adj_updated * decay)
|
|
2629
|
+
adj_decay = active * (adj_updated * field)
|
|
2630
|
+
adj_curl = active * (adj_updated * curl_term)
|
|
2631
|
+
adj_curl_term = active * (adj_updated * curl)
|
|
2632
|
+
adj_psi_pos_candidate = active * adj_psi_pos_post + adj_curl_term
|
|
2633
|
+
adj_psi_neg_candidate = active * adj_psi_neg_post - adj_curl_term
|
|
2634
|
+
adj_psi_pos = keep * adj_psi_pos_post + active * (b_pos_term * adj_psi_pos_candidate)
|
|
2635
|
+
adj_psi_neg = keep * adj_psi_neg_post + active * (b_neg_term * adj_psi_neg_candidate)
|
|
2636
|
+
adj_d_pos = inv_kappa_pos_term * adj_curl_term + c_pos_term * adj_psi_pos_candidate
|
|
2637
|
+
adj_d_neg = -inv_kappa_neg_term * adj_curl_term + c_neg_term * adj_psi_neg_candidate
|
|
2638
|
+
return adj_field, adj_d_pos, adj_d_neg, adj_decay, adj_curl, adj_psi_pos, adj_psi_neg
|
|
2639
|
+
|
|
2640
|
+
|
|
2641
|
+
def _kerr_reverse_coefficients(solver, state, *, eps_ex, eps_ey, eps_ez, chi3_ex, chi3_ey, chi3_ez):
|
|
2642
|
+
"""Per-component ``(decay, curl, eff, fsq, clamp_mask)`` for the Kerr reverse.
|
|
2643
|
+
|
|
2644
|
+
Mirrors :func:`_kerr_dynamic_electric_curls` (the differentiable forward
|
|
2645
|
+
replica already parity-tested against ``updateKerrElectricField*Curl3D``) but
|
|
2646
|
+
also returns the effective permittivity ``eff``, the collocated ``|E|^2``, and
|
|
2647
|
+
the ``clamp_min`` gradient mask so the reverse can form the analytic coefficient
|
|
2648
|
+
sensitivities without an autograd VJP. ``decay`` is the frozen linear PML decay
|
|
2649
|
+
(constant in the Kerr update); the eps/chi3/field dependence lives entirely in
|
|
2650
|
+
``curl = (dt / eff) * decay`` through ``eff = eps + eps0 * chi3 * |E|^2``."""
|
|
2651
|
+
field_square = _collocated_field_square(state["Ex"], state["Ey"], state["Ez"])
|
|
2652
|
+
eps0 = float(solver.eps0)
|
|
2653
|
+
floor = 1.0e-12 * eps0
|
|
2654
|
+
dt = float(solver.dt)
|
|
2655
|
+
coeffs = {}
|
|
2656
|
+
for name, eps, chi3, decay in (
|
|
2657
|
+
("Ex", eps_ex, chi3_ex, solver.cex_decay),
|
|
2658
|
+
("Ey", eps_ey, chi3_ey, solver.cey_decay),
|
|
2659
|
+
("Ez", eps_ez, chi3_ez, solver.cez_decay),
|
|
2660
|
+
):
|
|
2661
|
+
fsq = field_square[name]
|
|
2662
|
+
raw = eps + eps0 * chi3 * fsq
|
|
2663
|
+
eff = torch.clamp_min(raw, floor)
|
|
2664
|
+
clamp_mask = (raw >= floor).to(dtype=eff.dtype)
|
|
2665
|
+
curl = (dt / eff) * decay
|
|
2666
|
+
coeffs[name] = (decay, curl, eff, fsq, clamp_mask)
|
|
2667
|
+
return coeffs
|
|
2668
|
+
|
|
2669
|
+
|
|
2670
|
+
def _general_nonlinear_electric_coefficients(
|
|
2671
|
+
solver,
|
|
2672
|
+
state,
|
|
2673
|
+
*,
|
|
2674
|
+
eps_ex,
|
|
2675
|
+
eps_ey,
|
|
2676
|
+
eps_ez,
|
|
2677
|
+
chi2_ex,
|
|
2678
|
+
chi2_ey,
|
|
2679
|
+
chi2_ez,
|
|
2680
|
+
chi3_ex,
|
|
2681
|
+
chi3_ey,
|
|
2682
|
+
chi3_ez,
|
|
2683
|
+
tpa_ex,
|
|
2684
|
+
tpa_ey,
|
|
2685
|
+
tpa_ez,
|
|
2686
|
+
):
|
|
2687
|
+
"""Differentiable replica of ``updateNonlinearElectricCoefficients3D``.
|
|
2688
|
+
|
|
2689
|
+
The general nonlinear kernel rewrites both the decay and the curl coefficient
|
|
2690
|
+
every step from the pre-update fields, exactly as in the CUDA source::
|
|
2691
|
+
|
|
2692
|
+
eps_eff = max(eps_lin + eps0 * (chi2 * E_own + chi3 * |E|^2), floor)
|
|
2693
|
+
sigma = max(sigma_static + tpa_sigma * |E|^2, 0)
|
|
2694
|
+
half = 0.5 * sigma * dt / eps_eff
|
|
2695
|
+
decay = external * (1 - half) / (1 + half)
|
|
2696
|
+
curl = external * (dt / eps_eff) / (1 + half)
|
|
2697
|
+
|
|
2698
|
+
``external`` (``c*_decay_external``) carries the PML split-field decay and the
|
|
2699
|
+
PEC open fraction. ``E_own`` is the component's own edge field (the CUDA
|
|
2700
|
+
collocation returns the direct value for the own axis), while ``|E|^2`` is the
|
|
2701
|
+
4-point off-axis collocation shared with the Kerr replica. Flowing the VJP
|
|
2702
|
+
through the ``eps`` / ``chi2`` / ``chi3`` / ``tpa`` leaves and the pre-step
|
|
2703
|
+
``state`` fields is what makes chi2 and two-photon absorption differentiable.
|
|
2704
|
+
Returns per-component ``(decay, curl)`` with ``dt / eps_eff`` already folded
|
|
2705
|
+
into ``curl`` (fed with ``eps = 1`` in the update, like the Kerr path).
|
|
2706
|
+
"""
|
|
2707
|
+
field_square = _collocated_field_square(state["Ex"], state["Ey"], state["Ez"])
|
|
2708
|
+
eps0 = float(solver.eps0)
|
|
2709
|
+
floor = 1.0e-12 * eps0
|
|
2710
|
+
dt = float(solver.dt)
|
|
2711
|
+
coeffs = {}
|
|
2712
|
+
for name, own_field, eps, chi2, chi3, tpa, sigma_static, external in (
|
|
2713
|
+
("Ex", state["Ex"], eps_ex, chi2_ex, chi3_ex, tpa_ex, solver.sigma_e_Ex, solver.cex_decay_external),
|
|
2714
|
+
("Ey", state["Ey"], eps_ey, chi2_ey, chi3_ey, tpa_ey, solver.sigma_e_Ey, solver.cey_decay_external),
|
|
2715
|
+
("Ez", state["Ez"], eps_ez, chi2_ez, chi3_ez, tpa_ez, solver.sigma_e_Ez, solver.cez_decay_external),
|
|
2716
|
+
):
|
|
2717
|
+
effective = torch.clamp_min(
|
|
2718
|
+
eps + eps0 * (chi2 * own_field + chi3 * field_square[name]), floor
|
|
2719
|
+
)
|
|
2720
|
+
sigma = torch.clamp_min(sigma_static + tpa * field_square[name], 0.0)
|
|
2721
|
+
half = 0.5 * sigma * dt / effective
|
|
2722
|
+
inv_denom = 1.0 / (1.0 + half)
|
|
2723
|
+
decay = external * (1.0 - half) * inv_denom
|
|
2724
|
+
curl = external * (dt / effective) * inv_denom
|
|
2725
|
+
coeffs[name] = (decay, curl)
|
|
2726
|
+
return coeffs
|
|
2727
|
+
|
|
2728
|
+
|
|
2729
|
+
def _forward_magnetic_fields(solver, state, *, time_value, resolved_source_terms):
|
|
2730
|
+
"""Recompute the post-source real magnetic fields of one forward step.
|
|
2731
|
+
|
|
2732
|
+
Mirrors the magnetic half of ``_step_state`` for real fields (standard or
|
|
2733
|
+
CPML, selected by the solver's psi/coefficient attributes) and applies the
|
|
2734
|
+
resolved magnetic source terms. Used by the reverse steps, which need the
|
|
2735
|
+
same mid-step H the electric update consumed.
|
|
2736
|
+
"""
|
|
2737
|
+
uses_cpml = getattr(solver, "uses_cpml", False)
|
|
2738
|
+
|
|
2739
|
+
d_ez_dy = _forward_diff(state["Ez"], axis=1, inv_delta=solver.inv_dy_h)
|
|
2740
|
+
d_ey_dz = _forward_diff(state["Ey"], axis=2, inv_delta=solver.inv_dz_h)
|
|
2741
|
+
hx, _, _ = _update_magnetic_component(
|
|
2742
|
+
state["Hx"],
|
|
2743
|
+
d_pos=d_ez_dy,
|
|
2744
|
+
d_neg=d_ey_dz,
|
|
2745
|
+
decay=solver.chx_decay,
|
|
2746
|
+
curl=solver.chx_curl,
|
|
2747
|
+
psi_pos=state.get("psi_hx_y") if uses_cpml else None,
|
|
2748
|
+
psi_neg=state.get("psi_hx_z") if uses_cpml else None,
|
|
2749
|
+
b_pos=getattr(solver, "cpml_b_h_y", None),
|
|
2750
|
+
c_pos=getattr(solver, "cpml_c_h_y", None),
|
|
2751
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_h_y", None),
|
|
2752
|
+
b_neg=getattr(solver, "cpml_b_h_z", None),
|
|
2753
|
+
c_neg=getattr(solver, "cpml_c_h_z", None),
|
|
2754
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_h_z", None),
|
|
2755
|
+
axis_pos=1,
|
|
2756
|
+
axis_neg=2,
|
|
2757
|
+
)
|
|
2758
|
+
|
|
2759
|
+
d_ex_dz = _forward_diff(state["Ex"], axis=2, inv_delta=solver.inv_dz_h)
|
|
2760
|
+
d_ez_dx = _forward_diff(state["Ez"], axis=0, inv_delta=solver.inv_dx_h)
|
|
2761
|
+
hy, _, _ = _update_magnetic_component(
|
|
2762
|
+
state["Hy"],
|
|
2763
|
+
d_pos=d_ex_dz,
|
|
2764
|
+
d_neg=d_ez_dx,
|
|
2765
|
+
decay=solver.chy_decay,
|
|
2766
|
+
curl=solver.chy_curl,
|
|
2767
|
+
psi_pos=state.get("psi_hy_z") if uses_cpml else None,
|
|
2768
|
+
psi_neg=state.get("psi_hy_x") if uses_cpml else None,
|
|
2769
|
+
b_pos=getattr(solver, "cpml_b_h_z", None),
|
|
2770
|
+
c_pos=getattr(solver, "cpml_c_h_z", None),
|
|
2771
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_h_z", None),
|
|
2772
|
+
b_neg=getattr(solver, "cpml_b_h_x", None),
|
|
2773
|
+
c_neg=getattr(solver, "cpml_c_h_x", None),
|
|
2774
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_h_x", None),
|
|
2775
|
+
axis_pos=2,
|
|
2776
|
+
axis_neg=0,
|
|
2777
|
+
)
|
|
2778
|
+
|
|
2779
|
+
d_ey_dx = _forward_diff(state["Ey"], axis=0, inv_delta=solver.inv_dx_h)
|
|
2780
|
+
d_ex_dy = _forward_diff(state["Ex"], axis=1, inv_delta=solver.inv_dy_h)
|
|
2781
|
+
hz, _, _ = _update_magnetic_component(
|
|
2782
|
+
state["Hz"],
|
|
2783
|
+
d_pos=d_ey_dx,
|
|
2784
|
+
d_neg=d_ex_dy,
|
|
2785
|
+
decay=solver.chz_decay,
|
|
2786
|
+
curl=solver.chz_curl,
|
|
2787
|
+
psi_pos=state.get("psi_hz_x") if uses_cpml else None,
|
|
2788
|
+
psi_neg=state.get("psi_hz_y") if uses_cpml else None,
|
|
2789
|
+
b_pos=getattr(solver, "cpml_b_h_x", None),
|
|
2790
|
+
c_pos=getattr(solver, "cpml_c_h_x", None),
|
|
2791
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_h_x", None),
|
|
2792
|
+
b_neg=getattr(solver, "cpml_b_h_y", None),
|
|
2793
|
+
c_neg=getattr(solver, "cpml_c_h_y", None),
|
|
2794
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_h_y", None),
|
|
2795
|
+
axis_pos=0,
|
|
2796
|
+
axis_neg=1,
|
|
2797
|
+
)
|
|
2798
|
+
|
|
2799
|
+
return _apply_resolved_magnetic_source_terms(
|
|
2800
|
+
{"Hx": hx, "Hy": hy, "Hz": hz},
|
|
2801
|
+
solver=solver,
|
|
2802
|
+
time_value=time_value,
|
|
2803
|
+
resolved_source_terms=resolved_source_terms,
|
|
2804
|
+
)
|
|
2805
|
+
|
|
2806
|
+
|
|
2807
|
+
def _forward_magnetic_fields_complex(solver, state, *, time_value, resolved_source_terms):
|
|
2808
|
+
"""Recompute the post-source complex (split-field) magnetic fields of one Bloch step.
|
|
2809
|
+
|
|
2810
|
+
Mirrors the magnetic half of the split-field Bloch ``_step_state``: the real and
|
|
2811
|
+
imaginary field halves each run the plain (non-CPML) magnetic update with the
|
|
2812
|
+
shared decay/curl coefficients, and the resolved magnetic source terms apply once
|
|
2813
|
+
to the combined six-key dict. Both the analytic Bloch reference and the native
|
|
2814
|
+
Bloch reverse runner consume this to recover the mid-step H the electric update
|
|
2815
|
+
read (the reverse *math* is native; this replay stays Torch, same bar as the
|
|
2816
|
+
standard/CPML runners).
|
|
2817
|
+
"""
|
|
2818
|
+
|
|
2819
|
+
def _magnetic_half(suffix):
|
|
2820
|
+
ex = state["Ex" + suffix]
|
|
2821
|
+
ey = state["Ey" + suffix]
|
|
2822
|
+
ez = state["Ez" + suffix]
|
|
2823
|
+
d_ez_dy = _forward_diff(ez, axis=1, inv_delta=solver.inv_dy_h)
|
|
2824
|
+
d_ey_dz = _forward_diff(ey, axis=2, inv_delta=solver.inv_dz_h)
|
|
2825
|
+
hx, _, _ = _update_magnetic_component(
|
|
2826
|
+
state["Hx" + suffix],
|
|
2827
|
+
d_pos=d_ez_dy,
|
|
2828
|
+
d_neg=d_ey_dz,
|
|
2829
|
+
decay=solver.chx_decay,
|
|
2830
|
+
curl=solver.chx_curl,
|
|
2831
|
+
axis_pos=1,
|
|
2832
|
+
axis_neg=2,
|
|
2833
|
+
)
|
|
2834
|
+
d_ex_dz = _forward_diff(ex, axis=2, inv_delta=solver.inv_dz_h)
|
|
2835
|
+
d_ez_dx = _forward_diff(ez, axis=0, inv_delta=solver.inv_dx_h)
|
|
2836
|
+
hy, _, _ = _update_magnetic_component(
|
|
2837
|
+
state["Hy" + suffix],
|
|
2838
|
+
d_pos=d_ex_dz,
|
|
2839
|
+
d_neg=d_ez_dx,
|
|
2840
|
+
decay=solver.chy_decay,
|
|
2841
|
+
curl=solver.chy_curl,
|
|
2842
|
+
axis_pos=2,
|
|
2843
|
+
axis_neg=0,
|
|
2844
|
+
)
|
|
2845
|
+
d_ey_dx = _forward_diff(ey, axis=0, inv_delta=solver.inv_dx_h)
|
|
2846
|
+
d_ex_dy = _forward_diff(ex, axis=1, inv_delta=solver.inv_dy_h)
|
|
2847
|
+
hz, _, _ = _update_magnetic_component(
|
|
2848
|
+
state["Hz" + suffix],
|
|
2849
|
+
d_pos=d_ey_dx,
|
|
2850
|
+
d_neg=d_ex_dy,
|
|
2851
|
+
decay=solver.chz_decay,
|
|
2852
|
+
curl=solver.chz_curl,
|
|
2853
|
+
axis_pos=0,
|
|
2854
|
+
axis_neg=1,
|
|
2855
|
+
)
|
|
2856
|
+
return hx, hy, hz
|
|
2857
|
+
|
|
2858
|
+
hx, hy, hz = _magnetic_half("")
|
|
2859
|
+
hx_imag, hy_imag, hz_imag = _magnetic_half("_imag")
|
|
2860
|
+
magnetic_fields = {
|
|
2861
|
+
"Hx": hx,
|
|
2862
|
+
"Hy": hy,
|
|
2863
|
+
"Hz": hz,
|
|
2864
|
+
"Hx_imag": hx_imag,
|
|
2865
|
+
"Hy_imag": hy_imag,
|
|
2866
|
+
"Hz_imag": hz_imag,
|
|
2867
|
+
}
|
|
2868
|
+
return _apply_resolved_magnetic_source_terms(
|
|
2869
|
+
magnetic_fields,
|
|
2870
|
+
solver=solver,
|
|
2871
|
+
time_value=time_value,
|
|
2872
|
+
resolved_source_terms=resolved_source_terms,
|
|
2873
|
+
)
|
|
2874
|
+
|
|
2875
|
+
|
|
2876
|
+
def _sum_adjacent(field: torch.Tensor, axis: int) -> torch.Tensor:
|
|
2877
|
+
"""Sum of each adjacent pair along ``axis`` (length shrinks by one)."""
|
|
2878
|
+
lo = [slice(None)] * field.ndim
|
|
2879
|
+
hi = [slice(None)] * field.ndim
|
|
2880
|
+
lo[axis] = slice(0, -1)
|
|
2881
|
+
hi[axis] = slice(1, None)
|
|
2882
|
+
return field[tuple(lo)] + field[tuple(hi)]
|
|
2883
|
+
|
|
2884
|
+
|
|
2885
|
+
def _spread_to_nodes(edge: torch.Tensor, axis: int) -> torch.Tensor:
|
|
2886
|
+
"""Transpose of two-point averaging along ``axis`` (length grows by one).
|
|
2887
|
+
|
|
2888
|
+
Scatters each edge value onto its two bounding node positions with zero-fill
|
|
2889
|
+
at the ends, matching the four-neighbor collocation the full-anisotropy
|
|
2890
|
+
kernel performs when it maps an off-axis curl(H) sample onto the target edge
|
|
2891
|
+
(out-of-range neighbours contribute zero while the fixed 1/4 weight stays).
|
|
2892
|
+
"""
|
|
2893
|
+
zero_shape = [1 if i == axis else size for i, size in enumerate(edge.shape)]
|
|
2894
|
+
zero = edge.new_zeros(zero_shape)
|
|
2895
|
+
below = torch.cat((edge, zero), dim=axis)
|
|
2896
|
+
above = torch.cat((zero, edge), dim=axis)
|
|
2897
|
+
return below + above
|
|
2898
|
+
|
|
2899
|
+
|
|
2900
|
+
def _collocate_curl(curl: torch.Tensor, *, edge_axis: int, curl_axis: int) -> torch.Tensor:
|
|
2901
|
+
"""Neighbor-average an off-axis curl(H) component onto a target E edge.
|
|
2902
|
+
|
|
2903
|
+
``edge_axis`` is the Yee-edge axis of the *target* E component (summed over
|
|
2904
|
+
the two straddling cells) and ``curl_axis`` is the edge axis of the source
|
|
2905
|
+
curl component (spread onto the target's two bounding nodes).
|
|
2906
|
+
"""
|
|
2907
|
+
return _spread_to_nodes(_sum_adjacent(curl, edge_axis), curl_axis)
|
|
2908
|
+
|
|
2909
|
+
|
|
2910
|
+
def _full_aniso_electric_correction(solver, magnetic_fields):
|
|
2911
|
+
"""Off-diagonal (full-tensor) anisotropic coupling added to the E update.
|
|
2912
|
+
|
|
2913
|
+
Differentiable Torch replica of ``apply_full_aniso_corrections`` (the native
|
|
2914
|
+
``updateElectricFieldE{x,y,z}FullAniso3D`` kernels): each off-axis curl(H)
|
|
2915
|
+
component is four-neighbor-averaged onto the target Yee edge and scaled by
|
|
2916
|
+
the off-diagonal inverse-permittivity coefficient. It consumes the same
|
|
2917
|
+
post-source magnetic fields the diagonal update used, so the adjoint VJP
|
|
2918
|
+
flows the cotangent through the coupling into the H (and thence E) fields.
|
|
2919
|
+
"""
|
|
2920
|
+
hx = magnetic_fields["Hx"]
|
|
2921
|
+
hy = magnetic_fields["Hy"]
|
|
2922
|
+
hz = magnetic_fields["Hz"]
|
|
2923
|
+
# curl(H) on the native Yee E edges, identical backward differences to the
|
|
2924
|
+
# diagonal update (curl_x -> Ex edge, curl_y -> Ey edge, curl_z -> Ez edge).
|
|
2925
|
+
curl_x = _backward_diff(hz, axis=1, inv_delta=solver.inv_dy_e) - _backward_diff(hy, axis=2, inv_delta=solver.inv_dz_e)
|
|
2926
|
+
curl_y = _backward_diff(hx, axis=2, inv_delta=solver.inv_dz_e) - _backward_diff(hz, axis=0, inv_delta=solver.inv_dx_e)
|
|
2927
|
+
curl_z = _backward_diff(hy, axis=0, inv_delta=solver.inv_dx_e) - _backward_diff(hx, axis=1, inv_delta=solver.inv_dy_e)
|
|
2928
|
+
|
|
2929
|
+
ex = 0.25 * (
|
|
2930
|
+
solver.cex_aniso_y * _collocate_curl(curl_y, edge_axis=0, curl_axis=1)
|
|
2931
|
+
+ solver.cex_aniso_z * _collocate_curl(curl_z, edge_axis=0, curl_axis=2)
|
|
2932
|
+
)
|
|
2933
|
+
ey = 0.25 * (
|
|
2934
|
+
solver.cey_aniso_x * _collocate_curl(curl_x, edge_axis=1, curl_axis=0)
|
|
2935
|
+
+ solver.cey_aniso_z * _collocate_curl(curl_z, edge_axis=1, curl_axis=2)
|
|
2936
|
+
)
|
|
2937
|
+
ez = 0.25 * (
|
|
2938
|
+
solver.cez_aniso_x * _collocate_curl(curl_x, edge_axis=2, curl_axis=0)
|
|
2939
|
+
+ solver.cez_aniso_y * _collocate_curl(curl_y, edge_axis=2, curl_axis=1)
|
|
2940
|
+
)
|
|
2941
|
+
return {"Ex": ex, "Ey": ey, "Ez": ez}
|
|
2942
|
+
|
|
2943
|
+
|
|
2944
|
+
def _step_state(
|
|
2945
|
+
solver,
|
|
2946
|
+
state,
|
|
2947
|
+
*,
|
|
2948
|
+
time_value,
|
|
2949
|
+
eps_ex,
|
|
2950
|
+
eps_ey,
|
|
2951
|
+
eps_ez,
|
|
2952
|
+
chi3_ex=None,
|
|
2953
|
+
chi3_ey=None,
|
|
2954
|
+
chi3_ez=None,
|
|
2955
|
+
chi2_ex=None,
|
|
2956
|
+
chi2_ey=None,
|
|
2957
|
+
chi2_ez=None,
|
|
2958
|
+
tpa_ex=None,
|
|
2959
|
+
tpa_ey=None,
|
|
2960
|
+
tpa_ez=None,
|
|
2961
|
+
capture_magnetic=None,
|
|
2962
|
+
):
|
|
2963
|
+
if getattr(solver, "modulation_enabled", False):
|
|
2964
|
+
raise NotImplementedError("FDTD adjoint replay does not support time-modulated media.")
|
|
2965
|
+
source_terms, electric_source_terms, magnetic_source_terms = _resolved_source_term_lists(
|
|
2966
|
+
solver,
|
|
2967
|
+
eps_ex,
|
|
2968
|
+
eps_ey,
|
|
2969
|
+
eps_ez,
|
|
2970
|
+
)
|
|
2971
|
+
auxiliary_state = _extract_tfsf_auxiliary_state(state)
|
|
2972
|
+
complex_fields = has_complex_fields(solver)
|
|
2973
|
+
psi_hx_y_imag = psi_hx_z_imag = psi_hy_x_imag = psi_hy_z_imag = psi_hz_x_imag = psi_hz_y_imag = None
|
|
2974
|
+
|
|
2975
|
+
d_ez_dy = _forward_diff(state["Ez"], axis=1, inv_delta=solver.inv_dy_h)
|
|
2976
|
+
d_ey_dz = _forward_diff(state["Ey"], axis=2, inv_delta=solver.inv_dz_h)
|
|
2977
|
+
hx, psi_hx_y, psi_hx_z = _update_magnetic_component(
|
|
2978
|
+
state["Hx"],
|
|
2979
|
+
d_pos=d_ez_dy,
|
|
2980
|
+
d_neg=d_ey_dz,
|
|
2981
|
+
decay=solver.chx_decay,
|
|
2982
|
+
curl=solver.chx_curl,
|
|
2983
|
+
psi_pos=state.get("psi_hx_y"),
|
|
2984
|
+
psi_neg=state.get("psi_hx_z"),
|
|
2985
|
+
b_pos=getattr(solver, "cpml_b_h_y", None),
|
|
2986
|
+
c_pos=getattr(solver, "cpml_c_h_y", None),
|
|
2987
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_h_y", None),
|
|
2988
|
+
b_neg=getattr(solver, "cpml_b_h_z", None),
|
|
2989
|
+
c_neg=getattr(solver, "cpml_c_h_z", None),
|
|
2990
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_h_z", None),
|
|
2991
|
+
axis_pos=1,
|
|
2992
|
+
axis_neg=2,
|
|
2993
|
+
)
|
|
2994
|
+
|
|
2995
|
+
d_ex_dz = _forward_diff(state["Ex"], axis=2, inv_delta=solver.inv_dz_h)
|
|
2996
|
+
d_ez_dx = _forward_diff(state["Ez"], axis=0, inv_delta=solver.inv_dx_h)
|
|
2997
|
+
hy, psi_hy_x, psi_hy_z = _update_magnetic_component(
|
|
2998
|
+
state["Hy"],
|
|
2999
|
+
d_pos=d_ex_dz,
|
|
3000
|
+
d_neg=d_ez_dx,
|
|
3001
|
+
decay=solver.chy_decay,
|
|
3002
|
+
curl=solver.chy_curl,
|
|
3003
|
+
psi_pos=state.get("psi_hy_z"),
|
|
3004
|
+
psi_neg=state.get("psi_hy_x"),
|
|
3005
|
+
b_pos=getattr(solver, "cpml_b_h_z", None),
|
|
3006
|
+
c_pos=getattr(solver, "cpml_c_h_z", None),
|
|
3007
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_h_z", None),
|
|
3008
|
+
b_neg=getattr(solver, "cpml_b_h_x", None),
|
|
3009
|
+
c_neg=getattr(solver, "cpml_c_h_x", None),
|
|
3010
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_h_x", None),
|
|
3011
|
+
axis_pos=2,
|
|
3012
|
+
axis_neg=0,
|
|
3013
|
+
)
|
|
3014
|
+
|
|
3015
|
+
d_ey_dx = _forward_diff(state["Ey"], axis=0, inv_delta=solver.inv_dx_h)
|
|
3016
|
+
d_ex_dy = _forward_diff(state["Ex"], axis=1, inv_delta=solver.inv_dy_h)
|
|
3017
|
+
hz, psi_hz_x, psi_hz_y = _update_magnetic_component(
|
|
3018
|
+
state["Hz"],
|
|
3019
|
+
d_pos=d_ey_dx,
|
|
3020
|
+
d_neg=d_ex_dy,
|
|
3021
|
+
decay=solver.chz_decay,
|
|
3022
|
+
curl=solver.chz_curl,
|
|
3023
|
+
psi_pos=state.get("psi_hz_x"),
|
|
3024
|
+
psi_neg=state.get("psi_hz_y"),
|
|
3025
|
+
b_pos=getattr(solver, "cpml_b_h_x", None),
|
|
3026
|
+
c_pos=getattr(solver, "cpml_c_h_x", None),
|
|
3027
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_h_x", None),
|
|
3028
|
+
b_neg=getattr(solver, "cpml_b_h_y", None),
|
|
3029
|
+
c_neg=getattr(solver, "cpml_c_h_y", None),
|
|
3030
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_h_y", None),
|
|
3031
|
+
axis_pos=0,
|
|
3032
|
+
axis_neg=1,
|
|
3033
|
+
)
|
|
3034
|
+
|
|
3035
|
+
magnetic_fields = {
|
|
3036
|
+
"Hx": hx,
|
|
3037
|
+
"Hy": hy,
|
|
3038
|
+
"Hz": hz,
|
|
3039
|
+
}
|
|
3040
|
+
if complex_fields:
|
|
3041
|
+
d_ez_imag_dy = _forward_diff(state["Ez_imag"], axis=1, inv_delta=solver.inv_dy_h)
|
|
3042
|
+
d_ey_imag_dz = _forward_diff(state["Ey_imag"], axis=2, inv_delta=solver.inv_dz_h)
|
|
3043
|
+
hx_imag, psi_hx_y_imag, psi_hx_z_imag = _update_magnetic_component(
|
|
3044
|
+
state["Hx_imag"],
|
|
3045
|
+
d_pos=d_ez_imag_dy,
|
|
3046
|
+
d_neg=d_ey_imag_dz,
|
|
3047
|
+
decay=solver.chx_decay,
|
|
3048
|
+
curl=solver.chx_curl,
|
|
3049
|
+
psi_pos=state.get("psi_hx_y_imag") if getattr(solver, "uses_cpml", False) else None,
|
|
3050
|
+
psi_neg=state.get("psi_hx_z_imag") if getattr(solver, "uses_cpml", False) else None,
|
|
3051
|
+
b_pos=getattr(solver, "cpml_b_h_y", None),
|
|
3052
|
+
c_pos=getattr(solver, "cpml_c_h_y", None),
|
|
3053
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_h_y", None),
|
|
3054
|
+
b_neg=getattr(solver, "cpml_b_h_z", None),
|
|
3055
|
+
c_neg=getattr(solver, "cpml_c_h_z", None),
|
|
3056
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_h_z", None),
|
|
3057
|
+
axis_pos=1,
|
|
3058
|
+
axis_neg=2,
|
|
3059
|
+
)
|
|
3060
|
+
|
|
3061
|
+
d_ex_imag_dz = _forward_diff(state["Ex_imag"], axis=2, inv_delta=solver.inv_dz_h)
|
|
3062
|
+
d_ez_imag_dx = _forward_diff(state["Ez_imag"], axis=0, inv_delta=solver.inv_dx_h)
|
|
3063
|
+
hy_imag, psi_hy_z_imag, psi_hy_x_imag = _update_magnetic_component(
|
|
3064
|
+
state["Hy_imag"],
|
|
3065
|
+
d_pos=d_ex_imag_dz,
|
|
3066
|
+
d_neg=d_ez_imag_dx,
|
|
3067
|
+
decay=solver.chy_decay,
|
|
3068
|
+
curl=solver.chy_curl,
|
|
3069
|
+
psi_pos=state.get("psi_hy_z_imag") if getattr(solver, "uses_cpml", False) else None,
|
|
3070
|
+
psi_neg=state.get("psi_hy_x_imag") if getattr(solver, "uses_cpml", False) else None,
|
|
3071
|
+
b_pos=getattr(solver, "cpml_b_h_z", None),
|
|
3072
|
+
c_pos=getattr(solver, "cpml_c_h_z", None),
|
|
3073
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_h_z", None),
|
|
3074
|
+
b_neg=getattr(solver, "cpml_b_h_x", None),
|
|
3075
|
+
c_neg=getattr(solver, "cpml_c_h_x", None),
|
|
3076
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_h_x", None),
|
|
3077
|
+
axis_pos=2,
|
|
3078
|
+
axis_neg=0,
|
|
3079
|
+
)
|
|
3080
|
+
|
|
3081
|
+
d_ey_imag_dx = _forward_diff(state["Ey_imag"], axis=0, inv_delta=solver.inv_dx_h)
|
|
3082
|
+
d_ex_imag_dy = _forward_diff(state["Ex_imag"], axis=1, inv_delta=solver.inv_dy_h)
|
|
3083
|
+
hz_imag, psi_hz_x_imag, psi_hz_y_imag = _update_magnetic_component(
|
|
3084
|
+
state["Hz_imag"],
|
|
3085
|
+
d_pos=d_ey_imag_dx,
|
|
3086
|
+
d_neg=d_ex_imag_dy,
|
|
3087
|
+
decay=solver.chz_decay,
|
|
3088
|
+
curl=solver.chz_curl,
|
|
3089
|
+
psi_pos=state.get("psi_hz_x_imag") if getattr(solver, "uses_cpml", False) else None,
|
|
3090
|
+
psi_neg=state.get("psi_hz_y_imag") if getattr(solver, "uses_cpml", False) else None,
|
|
3091
|
+
b_pos=getattr(solver, "cpml_b_h_x", None),
|
|
3092
|
+
c_pos=getattr(solver, "cpml_c_h_x", None),
|
|
3093
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_h_x", None),
|
|
3094
|
+
b_neg=getattr(solver, "cpml_b_h_y", None),
|
|
3095
|
+
c_neg=getattr(solver, "cpml_c_h_y", None),
|
|
3096
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_h_y", None),
|
|
3097
|
+
axis_pos=0,
|
|
3098
|
+
axis_neg=1,
|
|
3099
|
+
)
|
|
3100
|
+
magnetic_fields.update(
|
|
3101
|
+
{
|
|
3102
|
+
"Hx_imag": hx_imag,
|
|
3103
|
+
"Hy_imag": hy_imag,
|
|
3104
|
+
"Hz_imag": hz_imag,
|
|
3105
|
+
}
|
|
3106
|
+
)
|
|
3107
|
+
|
|
3108
|
+
if getattr(solver, "tfsf_enabled", False):
|
|
3109
|
+
magnetic_fields = _apply_tfsf_terms(
|
|
3110
|
+
magnetic_fields,
|
|
3111
|
+
solver=solver,
|
|
3112
|
+
auxiliary_state=auxiliary_state,
|
|
3113
|
+
term_key="magnetic_terms",
|
|
3114
|
+
sample_kind="electric",
|
|
3115
|
+
time_value=time_value,
|
|
3116
|
+
)
|
|
3117
|
+
auxiliary_state = _advance_tfsf_auxiliary_magnetic_state(solver, auxiliary_state)
|
|
3118
|
+
else:
|
|
3119
|
+
magnetic_fields = _apply_source_term_list(
|
|
3120
|
+
magnetic_fields,
|
|
3121
|
+
terms=magnetic_source_terms,
|
|
3122
|
+
source_time=solver._source_time,
|
|
3123
|
+
omega=solver.source_omega,
|
|
3124
|
+
time_value=time_value,
|
|
3125
|
+
solver=None,
|
|
3126
|
+
)
|
|
3127
|
+
# Magnetic ADE state advances from the pre-step H; its correction lands on
|
|
3128
|
+
# the post-source H (before the electric update consumes it), matching the
|
|
3129
|
+
# forward per-step kernel order.
|
|
3130
|
+
magnetic_dispersive_state = _advance_magnetic_dispersive_state(solver, state)
|
|
3131
|
+
if magnetic_dispersive_state:
|
|
3132
|
+
magnetic_fields = _apply_magnetic_dispersive_corrections(
|
|
3133
|
+
solver,
|
|
3134
|
+
magnetic_fields,
|
|
3135
|
+
magnetic_dispersive_state,
|
|
3136
|
+
)
|
|
3137
|
+
dispersive_state = _advance_dispersive_state(solver, state)
|
|
3138
|
+
|
|
3139
|
+
if capture_magnetic is not None:
|
|
3140
|
+
# Post-magnetic, post-source real H that the electric update consumes this
|
|
3141
|
+
# step: the checkpoint replay hands it to the reverse reference backend so
|
|
3142
|
+
# it does not recompute the magnetic half-step (see ``_forward_magnetic_fields``).
|
|
3143
|
+
# The replay only requests capture for the pure real standard/CPML path
|
|
3144
|
+
# (see ``_replay_can_capture_mid_magnetic``), where this equals the reverse
|
|
3145
|
+
# recompute bit-for-bit; the tensors are never mutated after this point.
|
|
3146
|
+
capture_magnetic.append({name: tensor.detach() for name, tensor in magnetic_fields.items()})
|
|
3147
|
+
|
|
3148
|
+
if complex_fields:
|
|
3149
|
+
ex_curl = _dynamic_electric_curl(solver.cex_curl, solver.eps_Ex, eps_ex)
|
|
3150
|
+
ey_curl = _dynamic_electric_curl(solver.cey_curl, solver.eps_Ey, eps_ey)
|
|
3151
|
+
ez_curl = _dynamic_electric_curl(solver.cez_curl, solver.eps_Ez, eps_ez)
|
|
3152
|
+
if getattr(solver, "uses_cpml", False):
|
|
3153
|
+
from ..runtime.stepping import _bloch_cpml_pml_axis
|
|
3154
|
+
|
|
3155
|
+
pml_axis = _bloch_cpml_pml_axis(solver)
|
|
3156
|
+
if pml_axis is None:
|
|
3157
|
+
raise NotImplementedError(
|
|
3158
|
+
"Complex-field CPML adjoint replay requires exactly one PML axis and two Bloch axes: "
|
|
3159
|
+
"the reverse split-field update carries the transverse wrap phase on the two periodic "
|
|
3160
|
+
"axes and the recursive-convolution stretch on the single absorbing axis, so a face "
|
|
3161
|
+
"layout mixing Bloch and PML on one axis or leaving more than one absorbing axis is "
|
|
3162
|
+
"not expressible on that split."
|
|
3163
|
+
)
|
|
3164
|
+
if pml_axis == "z":
|
|
3165
|
+
electric_fields, electric_cpml_state = _update_mixed_bloch_cpml_electric_fields(
|
|
3166
|
+
solver,
|
|
3167
|
+
state,
|
|
3168
|
+
magnetic_fields,
|
|
3169
|
+
ex_curl=ex_curl,
|
|
3170
|
+
ey_curl=ey_curl,
|
|
3171
|
+
ez_curl=ez_curl,
|
|
3172
|
+
)
|
|
3173
|
+
else:
|
|
3174
|
+
electric_fields, electric_cpml_state = _update_general_bloch_cpml_electric_fields(
|
|
3175
|
+
solver,
|
|
3176
|
+
state,
|
|
3177
|
+
magnetic_fields,
|
|
3178
|
+
pml_axis=pml_axis,
|
|
3179
|
+
ex_curl=ex_curl,
|
|
3180
|
+
ey_curl=ey_curl,
|
|
3181
|
+
ez_curl=ez_curl,
|
|
3182
|
+
)
|
|
3183
|
+
psi_ex_y = electric_cpml_state["psi_ex_y"]
|
|
3184
|
+
psi_ex_z = electric_cpml_state["psi_ex_z"]
|
|
3185
|
+
psi_ey_x = electric_cpml_state["psi_ey_x"]
|
|
3186
|
+
psi_ey_z = electric_cpml_state["psi_ey_z"]
|
|
3187
|
+
psi_ez_x = electric_cpml_state["psi_ez_x"]
|
|
3188
|
+
psi_ez_y = electric_cpml_state["psi_ez_y"]
|
|
3189
|
+
psi_ex_y_imag = electric_cpml_state["psi_ex_y_imag"]
|
|
3190
|
+
psi_ex_z_imag = electric_cpml_state["psi_ex_z_imag"]
|
|
3191
|
+
psi_ey_x_imag = electric_cpml_state["psi_ey_x_imag"]
|
|
3192
|
+
psi_ey_z_imag = electric_cpml_state["psi_ey_z_imag"]
|
|
3193
|
+
psi_ez_x_imag = electric_cpml_state["psi_ez_x_imag"]
|
|
3194
|
+
psi_ez_y_imag = electric_cpml_state["psi_ez_y_imag"]
|
|
3195
|
+
else:
|
|
3196
|
+
hy_complex = torch.complex(magnetic_fields["Hy"], magnetic_fields["Hy_imag"])
|
|
3197
|
+
hz_complex = torch.complex(magnetic_fields["Hz"], magnetic_fields["Hz_imag"])
|
|
3198
|
+
d_hz_dy = _bloch_backward_diff(
|
|
3199
|
+
hz_complex,
|
|
3200
|
+
axis=1,
|
|
3201
|
+
inv_delta=solver.inv_dy_e,
|
|
3202
|
+
phase_cos=float(solver.boundary_phase_cos[1]),
|
|
3203
|
+
phase_sin=float(solver.boundary_phase_sin[1]),
|
|
3204
|
+
)
|
|
3205
|
+
d_hy_dz = _bloch_backward_diff(
|
|
3206
|
+
hy_complex,
|
|
3207
|
+
axis=2,
|
|
3208
|
+
inv_delta=solver.inv_dz_e,
|
|
3209
|
+
phase_cos=float(solver.boundary_phase_cos[2]),
|
|
3210
|
+
phase_sin=float(solver.boundary_phase_sin[2]),
|
|
3211
|
+
)
|
|
3212
|
+
ex_complex = torch.complex(state["Ex"], state["Ex_imag"]) * solver.cex_decay + ex_curl * (d_hz_dy - d_hy_dz)
|
|
3213
|
+
|
|
3214
|
+
hx_complex = torch.complex(magnetic_fields["Hx"], magnetic_fields["Hx_imag"])
|
|
3215
|
+
d_hx_dz = _bloch_backward_diff(
|
|
3216
|
+
hx_complex,
|
|
3217
|
+
axis=2,
|
|
3218
|
+
inv_delta=solver.inv_dz_e,
|
|
3219
|
+
phase_cos=float(solver.boundary_phase_cos[2]),
|
|
3220
|
+
phase_sin=float(solver.boundary_phase_sin[2]),
|
|
3221
|
+
)
|
|
3222
|
+
d_hz_dx = _bloch_backward_diff(
|
|
3223
|
+
hz_complex,
|
|
3224
|
+
axis=0,
|
|
3225
|
+
inv_delta=solver.inv_dx_e,
|
|
3226
|
+
phase_cos=float(solver.boundary_phase_cos[0]),
|
|
3227
|
+
phase_sin=float(solver.boundary_phase_sin[0]),
|
|
3228
|
+
)
|
|
3229
|
+
ey_complex = torch.complex(state["Ey"], state["Ey_imag"]) * solver.cey_decay + ey_curl * (d_hx_dz - d_hz_dx)
|
|
3230
|
+
|
|
3231
|
+
d_hy_dx = _bloch_backward_diff(
|
|
3232
|
+
hy_complex,
|
|
3233
|
+
axis=0,
|
|
3234
|
+
inv_delta=solver.inv_dx_e,
|
|
3235
|
+
phase_cos=float(solver.boundary_phase_cos[0]),
|
|
3236
|
+
phase_sin=float(solver.boundary_phase_sin[0]),
|
|
3237
|
+
)
|
|
3238
|
+
d_hx_dy = _bloch_backward_diff(
|
|
3239
|
+
hx_complex,
|
|
3240
|
+
axis=1,
|
|
3241
|
+
inv_delta=solver.inv_dy_e,
|
|
3242
|
+
phase_cos=float(solver.boundary_phase_cos[1]),
|
|
3243
|
+
phase_sin=float(solver.boundary_phase_sin[1]),
|
|
3244
|
+
)
|
|
3245
|
+
ez_complex = torch.complex(state["Ez"], state["Ez_imag"]) * solver.cez_decay + ez_curl * (d_hy_dx - d_hx_dy)
|
|
3246
|
+
|
|
3247
|
+
electric_fields = {
|
|
3248
|
+
"Ex": ex_complex.real,
|
|
3249
|
+
"Ey": ey_complex.real,
|
|
3250
|
+
"Ez": ez_complex.real,
|
|
3251
|
+
"Ex_imag": ex_complex.imag,
|
|
3252
|
+
"Ey_imag": ey_complex.imag,
|
|
3253
|
+
"Ez_imag": ez_complex.imag,
|
|
3254
|
+
}
|
|
3255
|
+
psi_ex_y = psi_ex_z = psi_ey_x = psi_ey_z = psi_ez_x = psi_ez_y = None
|
|
3256
|
+
else:
|
|
3257
|
+
general_coeffs = None
|
|
3258
|
+
kerr_curls = None
|
|
3259
|
+
conductive_coeffs = None
|
|
3260
|
+
if getattr(solver, "nonlinear_general_enabled", False):
|
|
3261
|
+
# chi2 / two-photon absorption (and any static conduction) rewrite
|
|
3262
|
+
# BOTH the decay and curl coefficients from the pre-update E fields
|
|
3263
|
+
# each step; replicate the general coefficient kernel differentiably
|
|
3264
|
+
# so the VJP flows into the fields, eps, chi2, chi3, and the TPA
|
|
3265
|
+
# conductivity. This branch matches the forward dispatch order, which
|
|
3266
|
+
# prefers the general kernel over the curl-only Kerr kernel when a
|
|
3267
|
+
# chi2 or TPA channel is present.
|
|
3268
|
+
general_coeffs = _general_nonlinear_electric_coefficients(
|
|
3269
|
+
solver,
|
|
3270
|
+
state,
|
|
3271
|
+
eps_ex=eps_ex,
|
|
3272
|
+
eps_ey=eps_ey,
|
|
3273
|
+
eps_ez=eps_ez,
|
|
3274
|
+
chi2_ex=chi2_ex if chi2_ex is not None else solver.nonlinear_chi2_Ex,
|
|
3275
|
+
chi2_ey=chi2_ey if chi2_ey is not None else solver.nonlinear_chi2_Ey,
|
|
3276
|
+
chi2_ez=chi2_ez if chi2_ez is not None else solver.nonlinear_chi2_Ez,
|
|
3277
|
+
chi3_ex=chi3_ex if chi3_ex is not None else solver.kerr_chi3_Ex,
|
|
3278
|
+
chi3_ey=chi3_ey if chi3_ey is not None else solver.kerr_chi3_Ey,
|
|
3279
|
+
chi3_ez=chi3_ez if chi3_ez is not None else solver.kerr_chi3_Ez,
|
|
3280
|
+
tpa_ex=tpa_ex if tpa_ex is not None else solver.tpa_sigma_Ex,
|
|
3281
|
+
tpa_ey=tpa_ey if tpa_ey is not None else solver.tpa_sigma_Ey,
|
|
3282
|
+
tpa_ez=tpa_ez if tpa_ez is not None else solver.tpa_sigma_Ez,
|
|
3283
|
+
)
|
|
3284
|
+
elif getattr(solver, "kerr_enabled", False):
|
|
3285
|
+
# The Kerr coefficient kernel recomputes the curl factor from the
|
|
3286
|
+
# pre-update E fields each step; replicate it differentiably so the
|
|
3287
|
+
# VJP flows into the fields, eps, and chi3.
|
|
3288
|
+
kerr_curls = _kerr_dynamic_electric_curls(
|
|
3289
|
+
solver,
|
|
3290
|
+
state,
|
|
3291
|
+
eps_ex=eps_ex,
|
|
3292
|
+
eps_ey=eps_ey,
|
|
3293
|
+
eps_ez=eps_ez,
|
|
3294
|
+
chi3_ex=chi3_ex if chi3_ex is not None else solver.kerr_chi3_Ex,
|
|
3295
|
+
chi3_ey=chi3_ey if chi3_ey is not None else solver.kerr_chi3_Ey,
|
|
3296
|
+
chi3_ez=chi3_ez if chi3_ez is not None else solver.kerr_chi3_Ez,
|
|
3297
|
+
)
|
|
3298
|
+
elif getattr(solver, "conductive_enabled", False):
|
|
3299
|
+
# Static conduction makes both decay and curl eps-dependent through
|
|
3300
|
+
# the semi-implicit denominator; recompute them differentiably so the
|
|
3301
|
+
# VJP carries that dependence (the linear rule drops it).
|
|
3302
|
+
conductive_coeffs = _conductive_electric_coefficients(
|
|
3303
|
+
solver, eps_ex=eps_ex, eps_ey=eps_ey, eps_ez=eps_ez
|
|
3304
|
+
)
|
|
3305
|
+
if general_coeffs is not None:
|
|
3306
|
+
ex_decay_c, ex_curl_c, ex_eps_c = general_coeffs["Ex"][0], general_coeffs["Ex"][1], 1.0
|
|
3307
|
+
ey_decay_c, ey_curl_c, ey_eps_c = general_coeffs["Ey"][0], general_coeffs["Ey"][1], 1.0
|
|
3308
|
+
ez_decay_c, ez_curl_c, ez_eps_c = general_coeffs["Ez"][0], general_coeffs["Ez"][1], 1.0
|
|
3309
|
+
elif kerr_curls is not None:
|
|
3310
|
+
ex_decay_c, ex_curl_c, ex_eps_c = solver.cex_decay, kerr_curls["Ex"], 1.0
|
|
3311
|
+
ey_decay_c, ey_curl_c, ey_eps_c = solver.cey_decay, kerr_curls["Ey"], 1.0
|
|
3312
|
+
ez_decay_c, ez_curl_c, ez_eps_c = solver.cez_decay, kerr_curls["Ez"], 1.0
|
|
3313
|
+
elif conductive_coeffs is not None:
|
|
3314
|
+
ex_decay_c, ex_curl_c, ex_eps_c = conductive_coeffs["Ex"][0], conductive_coeffs["Ex"][1], 1.0
|
|
3315
|
+
ey_decay_c, ey_curl_c, ey_eps_c = conductive_coeffs["Ey"][0], conductive_coeffs["Ey"][1], 1.0
|
|
3316
|
+
ez_decay_c, ez_curl_c, ez_eps_c = conductive_coeffs["Ez"][0], conductive_coeffs["Ez"][1], 1.0
|
|
3317
|
+
else:
|
|
3318
|
+
ex_decay_c, ex_curl_c, ex_eps_c = solver.cex_decay, solver.cex_curl * solver.eps_Ex, eps_ex
|
|
3319
|
+
ey_decay_c, ey_curl_c, ey_eps_c = solver.cey_decay, solver.cey_curl * solver.eps_Ey, eps_ey
|
|
3320
|
+
ez_decay_c, ez_curl_c, ez_eps_c = solver.cez_decay, solver.cez_curl * solver.eps_Ez, eps_ez
|
|
3321
|
+
d_hz_dy = _backward_diff(magnetic_fields["Hz"], axis=1, inv_delta=solver.inv_dy_e)
|
|
3322
|
+
d_hy_dz = _backward_diff(magnetic_fields["Hy"], axis=2, inv_delta=solver.inv_dz_e)
|
|
3323
|
+
ex, psi_ex_y, psi_ex_z = _update_electric_component(
|
|
3324
|
+
state["Ex"],
|
|
3325
|
+
d_pos=d_hz_dy,
|
|
3326
|
+
d_neg=d_hy_dz,
|
|
3327
|
+
decay=ex_decay_c,
|
|
3328
|
+
curl_prefactor=ex_curl_c,
|
|
3329
|
+
eps=ex_eps_c,
|
|
3330
|
+
low_mode_pos=solver.boundary_y_low_code,
|
|
3331
|
+
high_mode_pos=solver.boundary_y_high_code,
|
|
3332
|
+
low_mode_neg=solver.boundary_z_low_code,
|
|
3333
|
+
high_mode_neg=solver.boundary_z_high_code,
|
|
3334
|
+
axis_pos=1,
|
|
3335
|
+
axis_neg=2,
|
|
3336
|
+
psi_pos=state.get("psi_ex_y"),
|
|
3337
|
+
psi_neg=state.get("psi_ex_z"),
|
|
3338
|
+
b_pos=getattr(solver, "cpml_b_e_y", None),
|
|
3339
|
+
c_pos=getattr(solver, "cpml_c_e_y", None),
|
|
3340
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_e_y", None),
|
|
3341
|
+
b_neg=getattr(solver, "cpml_b_e_z", None),
|
|
3342
|
+
c_neg=getattr(solver, "cpml_c_e_z", None),
|
|
3343
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_e_z", None),
|
|
3344
|
+
)
|
|
3345
|
+
|
|
3346
|
+
d_hx_dz = _backward_diff(magnetic_fields["Hx"], axis=2, inv_delta=solver.inv_dz_e)
|
|
3347
|
+
d_hz_dx = _backward_diff(magnetic_fields["Hz"], axis=0, inv_delta=solver.inv_dx_e)
|
|
3348
|
+
ey, psi_ey_x, psi_ey_z = _update_electric_component(
|
|
3349
|
+
state["Ey"],
|
|
3350
|
+
d_pos=d_hx_dz,
|
|
3351
|
+
d_neg=d_hz_dx,
|
|
3352
|
+
decay=ey_decay_c,
|
|
3353
|
+
curl_prefactor=ey_curl_c,
|
|
3354
|
+
eps=ey_eps_c,
|
|
3355
|
+
low_mode_pos=solver.boundary_z_low_code,
|
|
3356
|
+
high_mode_pos=solver.boundary_z_high_code,
|
|
3357
|
+
low_mode_neg=solver.boundary_x_low_code,
|
|
3358
|
+
high_mode_neg=solver.boundary_x_high_code,
|
|
3359
|
+
axis_pos=2,
|
|
3360
|
+
axis_neg=0,
|
|
3361
|
+
psi_pos=state.get("psi_ey_z"),
|
|
3362
|
+
psi_neg=state.get("psi_ey_x"),
|
|
3363
|
+
b_pos=getattr(solver, "cpml_b_e_z", None),
|
|
3364
|
+
c_pos=getattr(solver, "cpml_c_e_z", None),
|
|
3365
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_e_z", None),
|
|
3366
|
+
b_neg=getattr(solver, "cpml_b_e_x", None),
|
|
3367
|
+
c_neg=getattr(solver, "cpml_c_e_x", None),
|
|
3368
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_e_x", None),
|
|
3369
|
+
)
|
|
3370
|
+
|
|
3371
|
+
d_hy_dx = _backward_diff(magnetic_fields["Hy"], axis=0, inv_delta=solver.inv_dx_e)
|
|
3372
|
+
d_hx_dy = _backward_diff(magnetic_fields["Hx"], axis=1, inv_delta=solver.inv_dy_e)
|
|
3373
|
+
ez, psi_ez_x, psi_ez_y = _update_electric_component(
|
|
3374
|
+
state["Ez"],
|
|
3375
|
+
d_pos=d_hy_dx,
|
|
3376
|
+
d_neg=d_hx_dy,
|
|
3377
|
+
decay=ez_decay_c,
|
|
3378
|
+
curl_prefactor=ez_curl_c,
|
|
3379
|
+
eps=ez_eps_c,
|
|
3380
|
+
low_mode_pos=solver.boundary_x_low_code,
|
|
3381
|
+
high_mode_pos=solver.boundary_x_high_code,
|
|
3382
|
+
low_mode_neg=solver.boundary_y_low_code,
|
|
3383
|
+
high_mode_neg=solver.boundary_y_high_code,
|
|
3384
|
+
axis_pos=0,
|
|
3385
|
+
axis_neg=1,
|
|
3386
|
+
psi_pos=state.get("psi_ez_x"),
|
|
3387
|
+
psi_neg=state.get("psi_ez_y"),
|
|
3388
|
+
b_pos=getattr(solver, "cpml_b_e_x", None),
|
|
3389
|
+
c_pos=getattr(solver, "cpml_c_e_x", None),
|
|
3390
|
+
inv_kappa_pos=getattr(solver, "cpml_inv_kappa_e_x", None),
|
|
3391
|
+
b_neg=getattr(solver, "cpml_b_e_y", None),
|
|
3392
|
+
c_neg=getattr(solver, "cpml_c_e_y", None),
|
|
3393
|
+
inv_kappa_neg=getattr(solver, "cpml_inv_kappa_e_y", None),
|
|
3394
|
+
)
|
|
3395
|
+
if getattr(solver, "full_aniso_enabled", False):
|
|
3396
|
+
# Full (off-diagonal) anisotropy couples each E component to the two
|
|
3397
|
+
# off-axis curl(H) components; the diagonal entry is already carried
|
|
3398
|
+
# by the base curl above, so add the neighbor-averaged off-diagonal
|
|
3399
|
+
# correction here to match the forward per-step kernel order.
|
|
3400
|
+
aniso_correction = _full_aniso_electric_correction(solver, magnetic_fields)
|
|
3401
|
+
ex = ex + aniso_correction["Ex"]
|
|
3402
|
+
ey = ey + aniso_correction["Ey"]
|
|
3403
|
+
ez = ez + aniso_correction["Ez"]
|
|
3404
|
+
electric_fields = {"Ex": ex, "Ey": ey, "Ez": ez}
|
|
3405
|
+
|
|
3406
|
+
if getattr(solver, "tfsf_enabled", False):
|
|
3407
|
+
electric_fields = _apply_tfsf_terms(
|
|
3408
|
+
electric_fields,
|
|
3409
|
+
solver=solver,
|
|
3410
|
+
auxiliary_state=auxiliary_state,
|
|
3411
|
+
term_key="electric_terms",
|
|
3412
|
+
sample_kind="magnetic",
|
|
3413
|
+
time_value=time_value,
|
|
3414
|
+
)
|
|
3415
|
+
auxiliary_state = _advance_tfsf_auxiliary_electric_state(
|
|
3416
|
+
solver,
|
|
3417
|
+
auxiliary_state,
|
|
3418
|
+
time_value=time_value,
|
|
3419
|
+
)
|
|
3420
|
+
else:
|
|
3421
|
+
electric_fields = _apply_source_term_list(
|
|
3422
|
+
electric_fields,
|
|
3423
|
+
terms=electric_source_terms,
|
|
3424
|
+
source_time=solver._source_time,
|
|
3425
|
+
omega=solver.source_omega,
|
|
3426
|
+
time_value=time_value,
|
|
3427
|
+
solver=None,
|
|
3428
|
+
)
|
|
3429
|
+
electric_fields = _apply_source_term_list(
|
|
3430
|
+
electric_fields,
|
|
3431
|
+
terms=source_terms,
|
|
3432
|
+
source_time=solver._source_time,
|
|
3433
|
+
omega=solver.source_omega,
|
|
3434
|
+
time_value=time_value,
|
|
3435
|
+
solver=solver if complex_fields else None,
|
|
3436
|
+
)
|
|
3437
|
+
|
|
3438
|
+
dispersive_input_fields = {name: electric_fields[name] for name in ("Ex", "Ey", "Ez")}
|
|
3439
|
+
if complex_fields:
|
|
3440
|
+
# Bloch dispersive replay corrects the imaginary field with its own
|
|
3441
|
+
# polarization current; hand both halves to the shared correction.
|
|
3442
|
+
dispersive_input_fields.update(
|
|
3443
|
+
{name: electric_fields[name] for name in ("Ex_imag", "Ey_imag", "Ez_imag")}
|
|
3444
|
+
)
|
|
3445
|
+
corrected_electric_fields = _apply_dispersive_corrections(
|
|
3446
|
+
solver,
|
|
3447
|
+
dispersive_input_fields,
|
|
3448
|
+
dispersive_state,
|
|
3449
|
+
eps_ex=eps_ex,
|
|
3450
|
+
eps_ey=eps_ey,
|
|
3451
|
+
eps_ez=eps_ez,
|
|
3452
|
+
)
|
|
3453
|
+
electric_fields.update(corrected_electric_fields)
|
|
3454
|
+
if not getattr(solver, "tfsf_enabled", False) and getattr(solver, "has_pec_faces", False):
|
|
3455
|
+
real_electric_fields = _enforce_pec_boundaries(
|
|
3456
|
+
solver,
|
|
3457
|
+
{name: electric_fields[name] for name in ("Ex", "Ey", "Ez")},
|
|
3458
|
+
)
|
|
3459
|
+
electric_fields.update(real_electric_fields)
|
|
3460
|
+
|
|
3461
|
+
next_state = {
|
|
3462
|
+
"Ex": electric_fields["Ex"],
|
|
3463
|
+
"Ey": electric_fields["Ey"],
|
|
3464
|
+
"Ez": electric_fields["Ez"],
|
|
3465
|
+
"Hx": magnetic_fields["Hx"],
|
|
3466
|
+
"Hy": magnetic_fields["Hy"],
|
|
3467
|
+
"Hz": magnetic_fields["Hz"],
|
|
3468
|
+
}
|
|
3469
|
+
if complex_fields:
|
|
3470
|
+
next_state.update(
|
|
3471
|
+
{
|
|
3472
|
+
"Ex_imag": electric_fields["Ex_imag"],
|
|
3473
|
+
"Ey_imag": electric_fields["Ey_imag"],
|
|
3474
|
+
"Ez_imag": electric_fields["Ez_imag"],
|
|
3475
|
+
"Hx_imag": magnetic_fields["Hx_imag"],
|
|
3476
|
+
"Hy_imag": magnetic_fields["Hy_imag"],
|
|
3477
|
+
"Hz_imag": magnetic_fields["Hz_imag"],
|
|
3478
|
+
}
|
|
3479
|
+
)
|
|
3480
|
+
if solver.uses_cpml:
|
|
3481
|
+
next_state.update(
|
|
3482
|
+
{
|
|
3483
|
+
"psi_ex_y": psi_ex_y,
|
|
3484
|
+
"psi_ex_z": psi_ex_z,
|
|
3485
|
+
"psi_ey_x": psi_ey_x,
|
|
3486
|
+
"psi_ey_z": psi_ey_z,
|
|
3487
|
+
"psi_ez_x": psi_ez_x,
|
|
3488
|
+
"psi_ez_y": psi_ez_y,
|
|
3489
|
+
"psi_hx_y": psi_hx_y,
|
|
3490
|
+
"psi_hx_z": psi_hx_z,
|
|
3491
|
+
"psi_hy_x": psi_hy_x,
|
|
3492
|
+
"psi_hy_z": psi_hy_z,
|
|
3493
|
+
"psi_hz_x": psi_hz_x,
|
|
3494
|
+
"psi_hz_y": psi_hz_y,
|
|
3495
|
+
}
|
|
3496
|
+
)
|
|
3497
|
+
if complex_fields:
|
|
3498
|
+
next_state.update(
|
|
3499
|
+
{
|
|
3500
|
+
"psi_ex_y_imag": psi_ex_y_imag,
|
|
3501
|
+
"psi_ex_z_imag": psi_ex_z_imag,
|
|
3502
|
+
"psi_ey_x_imag": psi_ey_x_imag,
|
|
3503
|
+
"psi_ey_z_imag": psi_ey_z_imag,
|
|
3504
|
+
"psi_ez_x_imag": psi_ez_x_imag,
|
|
3505
|
+
"psi_ez_y_imag": psi_ez_y_imag,
|
|
3506
|
+
"psi_hx_y_imag": psi_hx_y_imag,
|
|
3507
|
+
"psi_hx_z_imag": psi_hx_z_imag,
|
|
3508
|
+
"psi_hy_x_imag": psi_hy_x_imag,
|
|
3509
|
+
"psi_hy_z_imag": psi_hy_z_imag,
|
|
3510
|
+
"psi_hz_x_imag": psi_hz_x_imag,
|
|
3511
|
+
"psi_hz_y_imag": psi_hz_y_imag,
|
|
3512
|
+
}
|
|
3513
|
+
)
|
|
3514
|
+
if auxiliary_state is not None:
|
|
3515
|
+
next_state["tfsf_aux_electric"] = auxiliary_state["electric"]
|
|
3516
|
+
next_state["tfsf_aux_magnetic"] = auxiliary_state["magnetic"]
|
|
3517
|
+
next_state.update(dispersive_state)
|
|
3518
|
+
next_state.update(magnetic_dispersive_state)
|
|
3519
|
+
return next_state
|
|
3520
|
+
|
|
3521
|
+
|
|
3522
|
+
def _build_spectral_weight_schedule(entries, *, time_steps, window_type):
|
|
3523
|
+
schedules = []
|
|
3524
|
+
for entry in entries:
|
|
3525
|
+
phase_cos = 1.0
|
|
3526
|
+
phase_sin = 0.0
|
|
3527
|
+
start_step = int(entry["start_step"])
|
|
3528
|
+
end_step = None if entry["end_step"] is None else int(entry["end_step"])
|
|
3529
|
+
weights = []
|
|
3530
|
+
for n in range(time_steps):
|
|
3531
|
+
if n < start_step or (end_step is not None and n >= end_step):
|
|
3532
|
+
weights.append((0.0, 0.0))
|
|
3533
|
+
else:
|
|
3534
|
+
if window_type == "none" or start_step is None or end_step is None:
|
|
3535
|
+
window_weight = 1.0
|
|
3536
|
+
else:
|
|
3537
|
+
total_samples = max(end_step - start_step, 1)
|
|
3538
|
+
position = (n - start_step) / total_samples
|
|
3539
|
+
if window_type == "hanning":
|
|
3540
|
+
window_weight = 0.5 * (1.0 - math.cos(2.0 * math.pi * position))
|
|
3541
|
+
elif window_type == "ramp":
|
|
3542
|
+
ramp_fraction = 0.1
|
|
3543
|
+
if position < ramp_fraction:
|
|
3544
|
+
window_weight = 0.5 * (1.0 - math.cos(math.pi * position / ramp_fraction))
|
|
3545
|
+
else:
|
|
3546
|
+
window_weight = 1.0
|
|
3547
|
+
else:
|
|
3548
|
+
window_weight = 1.0
|
|
3549
|
+
weights.append((window_weight * phase_cos, window_weight * phase_sin))
|
|
3550
|
+
next_cos = phase_cos * entry["phase_step_cos"] - phase_sin * entry["phase_step_sin"]
|
|
3551
|
+
next_sin = phase_sin * entry["phase_step_cos"] + phase_cos * entry["phase_step_sin"]
|
|
3552
|
+
phase_cos = next_cos
|
|
3553
|
+
phase_sin = next_sin
|
|
3554
|
+
schedules.append(tuple(weights))
|
|
3555
|
+
return tuple(schedules)
|
|
3556
|
+
|
|
3557
|
+
|
|
3558
|
+
def _replay_can_capture_mid_magnetic(solver) -> bool:
|
|
3559
|
+
"""Whether the replay may hand its mid-step H to the reverse reference backend.
|
|
3560
|
+
|
|
3561
|
+
True only for the pure real standard / CPML configuration whose magnetic
|
|
3562
|
+
half-step is a plain ``_update_magnetic_component`` (no TFSF injection, no
|
|
3563
|
+
magnetic ADE correction, no magnetic surface source, no complex split field,
|
|
3564
|
+
no electric dispersion / nonlinear / conductive / full-anisotropic coupling).
|
|
3565
|
+
In that regime the H captured on the way to the electric update equals the
|
|
3566
|
+
reverse backend's ``_forward_magnetic_fields`` recompute bit-for-bit, and the
|
|
3567
|
+
per-step magnetic update carries no dependence on the (leaf) permittivity, so
|
|
3568
|
+
threading the captured tensor changes nothing numerically while removing one
|
|
3569
|
+
torch magnetic half-step per reverse step.
|
|
3570
|
+
"""
|
|
3571
|
+
if has_complex_fields(solver):
|
|
3572
|
+
return False
|
|
3573
|
+
if getattr(solver, "tfsf_enabled", False):
|
|
3574
|
+
return False
|
|
3575
|
+
if getattr(solver, "dispersive_enabled", False):
|
|
3576
|
+
return False
|
|
3577
|
+
if getattr(solver, "magnetic_dispersive_enabled", False):
|
|
3578
|
+
return False
|
|
3579
|
+
if getattr(solver, "nonlinear_enabled", False):
|
|
3580
|
+
return False
|
|
3581
|
+
if getattr(solver, "conductive_enabled", False):
|
|
3582
|
+
return False
|
|
3583
|
+
if getattr(solver, "full_aniso_enabled", False):
|
|
3584
|
+
return False
|
|
3585
|
+
if getattr(solver, "_magnetic_source_terms", ()):
|
|
3586
|
+
return False
|
|
3587
|
+
return True
|
|
3588
|
+
|
|
3589
|
+
|
|
3590
|
+
def _replay_segment_states(solver, checkpoint, start_step, end_step, *, mid_magnetic_out=None):
|
|
3591
|
+
"""Replay the forward field states of one checkpoint segment.
|
|
3592
|
+
|
|
3593
|
+
``mid_magnetic_out``, when a list, collects the post-magnetic real H of each
|
|
3594
|
+
replayed step (one entry per step, aligned with ``states[offset]``) so the
|
|
3595
|
+
reverse pass can consume the mid-step H instead of recomputing it. It is only
|
|
3596
|
+
populated for configurations ``_replay_can_capture_mid_magnetic`` accepts.
|
|
3597
|
+
"""
|
|
3598
|
+
validate_checkpoint_state(checkpoint)
|
|
3599
|
+
capture = mid_magnetic_out is not None and _replay_can_capture_mid_magnetic(solver)
|
|
3600
|
+
with torch.no_grad():
|
|
3601
|
+
current = clone_checkpoint_tensors(checkpoint)
|
|
3602
|
+
states = [current]
|
|
3603
|
+
for step_index in range(start_step, end_step):
|
|
3604
|
+
current = _step_state(
|
|
3605
|
+
solver,
|
|
3606
|
+
current,
|
|
3607
|
+
time_value=step_index * solver.dt,
|
|
3608
|
+
eps_ex=solver.eps_Ex,
|
|
3609
|
+
eps_ey=solver.eps_Ey,
|
|
3610
|
+
eps_ez=solver.eps_Ez,
|
|
3611
|
+
capture_magnetic=mid_magnetic_out if capture else None,
|
|
3612
|
+
)
|
|
3613
|
+
states.append({name: tensor.detach() for name, tensor in current.items()})
|
|
3614
|
+
return states
|