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,31 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
|
|
4
|
+
from .postprocess import get_frequency_solution, get_material_permittivity
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _to_numpy_field(value):
|
|
8
|
+
if isinstance(value, torch.Tensor):
|
|
9
|
+
return value.detach().cpu().numpy()
|
|
10
|
+
return np.asarray(value)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def save_frequency_solution(solver, output_path):
|
|
14
|
+
freq_solution = get_frequency_solution(solver)
|
|
15
|
+
|
|
16
|
+
ex = np.pad(_to_numpy_field(freq_solution["Ex"]), ((0, 1), (0, 0), (0, 0)), mode="edge")
|
|
17
|
+
ey = np.pad(_to_numpy_field(freq_solution["Ey"]), ((0, 0), (0, 1), (0, 0)), mode="edge")
|
|
18
|
+
ez = np.pad(_to_numpy_field(freq_solution["Ez"]), ((0, 0), (0, 0), (0, 1)), mode="edge")
|
|
19
|
+
|
|
20
|
+
field_data = np.stack(
|
|
21
|
+
[ex.real, ex.imag, ey.real, ey.imag, ez.real, ez.imag],
|
|
22
|
+
axis=-1,
|
|
23
|
+
).astype(np.float32)
|
|
24
|
+
|
|
25
|
+
np.savez_compressed(
|
|
26
|
+
output_path,
|
|
27
|
+
field=field_data,
|
|
28
|
+
permittivity=get_material_permittivity(solver).cpu().numpy(),
|
|
29
|
+
)
|
|
30
|
+
print(f"Frequency-domain solution saved to: {output_path}")
|
|
31
|
+
print(f"Field data shape: {field_data.shape}")
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import torch
|
|
7
|
+
import torch.nn.functional as F
|
|
8
|
+
|
|
9
|
+
from ..compiler.materials import _density_kernel_size
|
|
10
|
+
from ..scene import prepare_scene
|
|
11
|
+
from witwin.core import Box
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class _RegionLayout:
|
|
16
|
+
x_slice: slice
|
|
17
|
+
y_slice: slice
|
|
18
|
+
z_slice: slice
|
|
19
|
+
target_shape: tuple[int, int, int]
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def region(self) -> tuple[slice, slice, slice]:
|
|
23
|
+
return (self.x_slice, self.y_slice, self.z_slice)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _region_axis_slice(axis_coords: torch.Tensor, lower: float, upper: float) -> slice | None:
|
|
27
|
+
indices = torch.nonzero((axis_coords >= lower) & (axis_coords <= upper), as_tuple=False).flatten()
|
|
28
|
+
if indices.numel() == 0:
|
|
29
|
+
return None
|
|
30
|
+
return slice(int(indices[0].item()), int(indices[-1].item()) + 1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_region_layout(scene, region) -> _RegionLayout | None:
|
|
34
|
+
geometry = region.geometry
|
|
35
|
+
if not isinstance(geometry, Box):
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"MaterialRegion currently supports Box geometry only, got {type(geometry).__name__}."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
pos = geometry.position
|
|
41
|
+
sz_vec = geometry.size
|
|
42
|
+
cx, cy, cz = float(pos[0]), float(pos[1]), float(pos[2])
|
|
43
|
+
sx, sy, sz = float(sz_vec[0]), float(sz_vec[1]), float(sz_vec[2])
|
|
44
|
+
x_slice = _region_axis_slice(scene.x, cx - sx / 2.0, cx + sx / 2.0)
|
|
45
|
+
y_slice = _region_axis_slice(scene.y, cy - sy / 2.0, cy + sy / 2.0)
|
|
46
|
+
z_slice = _region_axis_slice(scene.z, cz - sz / 2.0, cz + sz / 2.0)
|
|
47
|
+
if x_slice is None or y_slice is None or z_slice is None:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
return _RegionLayout(
|
|
51
|
+
x_slice=x_slice,
|
|
52
|
+
y_slice=y_slice,
|
|
53
|
+
z_slice=z_slice,
|
|
54
|
+
target_shape=(
|
|
55
|
+
x_slice.stop - x_slice.start,
|
|
56
|
+
y_slice.stop - y_slice.start,
|
|
57
|
+
z_slice.stop - z_slice.start,
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _resolved_density_kernel(region, density_shape: tuple[int, int, int], scene) -> tuple[int, int, int]:
|
|
63
|
+
if region.filter_radius is None:
|
|
64
|
+
return (1, 1, 1)
|
|
65
|
+
kernel = _density_kernel_size(scene, region.filter_radius)
|
|
66
|
+
return tuple(
|
|
67
|
+
min(size, shape if shape % 2 == 1 else max(1, shape - 1))
|
|
68
|
+
for size, shape in zip(kernel, density_shape)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _projection_derivative(density: torch.Tensor, beta: float) -> torch.Tensor:
|
|
73
|
+
midpoint = density.new_tensor(0.5)
|
|
74
|
+
denominator = torch.tanh(beta * midpoint) + torch.tanh(beta * (1.0 - midpoint))
|
|
75
|
+
sech = torch.cosh(beta * (density - midpoint)).reciprocal()
|
|
76
|
+
return beta * sech * sech / denominator
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _clamp_backward_mask(tensor: torch.Tensor, *, lower: float = 0.0, upper: float = 1.0) -> torch.Tensor:
|
|
80
|
+
return ((tensor >= lower) & (tensor <= upper)).to(dtype=tensor.dtype)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _pullback_region_density(scene, region, *, layout: _RegionLayout, grad_eps_patch: torch.Tensor) -> torch.Tensor:
|
|
84
|
+
if not torch.count_nonzero(grad_eps_patch):
|
|
85
|
+
return torch.zeros_like(region.density)
|
|
86
|
+
|
|
87
|
+
density = region.density.to(device=scene.device, dtype=grad_eps_patch.dtype)
|
|
88
|
+
lower, upper = (float(region.bounds[0]), float(region.bounds[1]))
|
|
89
|
+
bound_scale = upper - lower
|
|
90
|
+
if np.isclose(bound_scale, 0.0):
|
|
91
|
+
return torch.zeros_like(region.density)
|
|
92
|
+
|
|
93
|
+
normalized = density if np.isclose(lower, 0.0) and np.isclose(upper, 1.0) else (density - lower) / bound_scale
|
|
94
|
+
normalized_clamped = normalized.clamp(0.0, 1.0)
|
|
95
|
+
|
|
96
|
+
kernel = _resolved_density_kernel(region, tuple(int(v) for v in density.shape), scene)
|
|
97
|
+
if kernel == (1, 1, 1):
|
|
98
|
+
filtered = normalized_clamped
|
|
99
|
+
else:
|
|
100
|
+
filtered = F.avg_pool3d(
|
|
101
|
+
normalized_clamped[None, None, ...],
|
|
102
|
+
kernel_size=kernel,
|
|
103
|
+
stride=1,
|
|
104
|
+
padding=tuple(size // 2 for size in kernel),
|
|
105
|
+
)[0, 0]
|
|
106
|
+
|
|
107
|
+
if region.projection_beta is None:
|
|
108
|
+
projected = filtered
|
|
109
|
+
else:
|
|
110
|
+
beta = float(region.projection_beta)
|
|
111
|
+
midpoint = filtered.new_tensor(0.5)
|
|
112
|
+
numerator = torch.tanh(beta * midpoint) + torch.tanh(beta * (filtered - midpoint))
|
|
113
|
+
denominator = torch.tanh(beta * midpoint) + torch.tanh(beta * (1.0 - midpoint))
|
|
114
|
+
projected = numerator / denominator
|
|
115
|
+
projected_clamped = projected.clamp(0.0, 1.0)
|
|
116
|
+
|
|
117
|
+
grad_density = grad_eps_patch * float(region.eps_bounds[1] - region.eps_bounds[0])
|
|
118
|
+
if projected_clamped.shape != layout.target_shape:
|
|
119
|
+
grad_density = torch.ops.aten.upsample_trilinear3d_backward(
|
|
120
|
+
grad_density[None, None, ...].contiguous(),
|
|
121
|
+
list(layout.target_shape),
|
|
122
|
+
list(projected_clamped[None, None, ...].shape),
|
|
123
|
+
False,
|
|
124
|
+
None,
|
|
125
|
+
None,
|
|
126
|
+
None,
|
|
127
|
+
)[0, 0]
|
|
128
|
+
|
|
129
|
+
grad_density = grad_density * _clamp_backward_mask(projected)
|
|
130
|
+
if region.projection_beta is not None:
|
|
131
|
+
grad_density = grad_density * _projection_derivative(filtered, float(region.projection_beta))
|
|
132
|
+
|
|
133
|
+
if kernel != (1, 1, 1):
|
|
134
|
+
grad_density = torch.ops.aten.avg_pool3d_backward(
|
|
135
|
+
grad_density[None, None, ...].contiguous(),
|
|
136
|
+
normalized_clamped[None, None, ...].contiguous(),
|
|
137
|
+
list(kernel),
|
|
138
|
+
[1, 1, 1],
|
|
139
|
+
[size // 2 for size in kernel],
|
|
140
|
+
False,
|
|
141
|
+
True,
|
|
142
|
+
None,
|
|
143
|
+
)[0, 0]
|
|
144
|
+
|
|
145
|
+
grad_density = grad_density * _clamp_backward_mask(normalized)
|
|
146
|
+
if not (np.isclose(lower, 0.0) and np.isclose(upper, 1.0)):
|
|
147
|
+
grad_density = grad_density / bound_scale
|
|
148
|
+
|
|
149
|
+
return grad_density.to(device=region.density.device, dtype=region.density.dtype)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def component_node_gradients_from_yee_permittivity(
|
|
153
|
+
scene,
|
|
154
|
+
*,
|
|
155
|
+
grad_eps_ex: torch.Tensor,
|
|
156
|
+
grad_eps_ey: torch.Tensor,
|
|
157
|
+
grad_eps_ez: torch.Tensor,
|
|
158
|
+
eps0: float,
|
|
159
|
+
) -> dict[str, torch.Tensor]:
|
|
160
|
+
"""Per-axis node gradients transposing the node->Yee-edge permittivity build.
|
|
161
|
+
|
|
162
|
+
The forward path builds ``eps_E<axis> = 0.5 * (node_a + node_b) * eps0`` from
|
|
163
|
+
the *per-axis* relative-permittivity node component, so each Yee-edge gradient
|
|
164
|
+
scatters half onto its two endpoint nodes of that same axis component. Keeping
|
|
165
|
+
the three axes separate is what makes diagonal-anisotropic media differentiable;
|
|
166
|
+
for isotropic media summing the three components reproduces the scalar pullback
|
|
167
|
+
exactly.
|
|
168
|
+
"""
|
|
169
|
+
shape = (scene.Nx, scene.Ny, scene.Nz)
|
|
170
|
+
grad_x = torch.zeros(shape, device=scene.device, dtype=grad_eps_ex.dtype)
|
|
171
|
+
grad_y = torch.zeros(shape, device=scene.device, dtype=grad_eps_ey.dtype)
|
|
172
|
+
grad_z = torch.zeros(shape, device=scene.device, dtype=grad_eps_ez.dtype)
|
|
173
|
+
grad_x[:-1, :, :] += 0.5 * grad_eps_ex
|
|
174
|
+
grad_x[1:, :, :] += 0.5 * grad_eps_ex
|
|
175
|
+
grad_y[:, :-1, :] += 0.5 * grad_eps_ey
|
|
176
|
+
grad_y[:, 1:, :] += 0.5 * grad_eps_ey
|
|
177
|
+
grad_z[:, :, :-1] += 0.5 * grad_eps_ez
|
|
178
|
+
grad_z[:, :, 1:] += 0.5 * grad_eps_ez
|
|
179
|
+
scale = float(eps0)
|
|
180
|
+
return {"x": grad_x * scale, "y": grad_y * scale, "z": grad_z * scale}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def node_gradient_from_yee_permittivity(
|
|
184
|
+
scene,
|
|
185
|
+
*,
|
|
186
|
+
grad_eps_ex: torch.Tensor,
|
|
187
|
+
grad_eps_ey: torch.Tensor,
|
|
188
|
+
grad_eps_ez: torch.Tensor,
|
|
189
|
+
eps0: float,
|
|
190
|
+
) -> torch.Tensor:
|
|
191
|
+
component_gradients = component_node_gradients_from_yee_permittivity(
|
|
192
|
+
scene,
|
|
193
|
+
grad_eps_ex=grad_eps_ex,
|
|
194
|
+
grad_eps_ey=grad_eps_ey,
|
|
195
|
+
grad_eps_ez=grad_eps_ez,
|
|
196
|
+
eps0=eps0,
|
|
197
|
+
)
|
|
198
|
+
return component_gradients["x"] + component_gradients["y"] + component_gradients["z"]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def pullback_density_gradients(
|
|
202
|
+
scene,
|
|
203
|
+
*,
|
|
204
|
+
density_tensors,
|
|
205
|
+
trainable_region_indices,
|
|
206
|
+
grad_eps_ex: torch.Tensor,
|
|
207
|
+
grad_eps_ey: torch.Tensor,
|
|
208
|
+
grad_eps_ez: torch.Tensor,
|
|
209
|
+
eps0: float,
|
|
210
|
+
) -> tuple[torch.Tensor, ...]:
|
|
211
|
+
if not density_tensors:
|
|
212
|
+
return ()
|
|
213
|
+
prepared_scene = prepare_scene(scene)
|
|
214
|
+
|
|
215
|
+
grad_eps_r = node_gradient_from_yee_permittivity(
|
|
216
|
+
prepared_scene,
|
|
217
|
+
grad_eps_ex=grad_eps_ex,
|
|
218
|
+
grad_eps_ey=grad_eps_ey,
|
|
219
|
+
grad_eps_ez=grad_eps_ez,
|
|
220
|
+
eps0=eps0,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
region_layouts = [
|
|
224
|
+
_resolve_region_layout(prepared_scene, region)
|
|
225
|
+
for region in prepared_scene.material_regions
|
|
226
|
+
]
|
|
227
|
+
outputs = [torch.zeros_like(density) for density in density_tensors]
|
|
228
|
+
output_positions = {
|
|
229
|
+
int(region_index): position
|
|
230
|
+
for position, region_index in enumerate(trainable_region_indices)
|
|
231
|
+
}
|
|
232
|
+
covered_mask = torch.zeros(
|
|
233
|
+
(prepared_scene.Nx, prepared_scene.Ny, prepared_scene.Nz),
|
|
234
|
+
device=prepared_scene.device,
|
|
235
|
+
dtype=torch.bool,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
for region_index in range(len(prepared_scene.material_regions) - 1, -1, -1):
|
|
239
|
+
layout = region_layouts[region_index]
|
|
240
|
+
if layout is None:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
region = prepared_scene.material_regions[region_index]
|
|
244
|
+
region_slice = layout.region
|
|
245
|
+
effective_mask = ~covered_mask[region_slice]
|
|
246
|
+
|
|
247
|
+
output_position = output_positions.get(region_index)
|
|
248
|
+
if output_position is not None:
|
|
249
|
+
grad_patch = torch.where(
|
|
250
|
+
effective_mask,
|
|
251
|
+
grad_eps_r[region_slice],
|
|
252
|
+
torch.zeros_like(grad_eps_r[region_slice]),
|
|
253
|
+
)
|
|
254
|
+
outputs[output_position] = _pullback_region_density(
|
|
255
|
+
prepared_scene,
|
|
256
|
+
region,
|
|
257
|
+
layout=layout,
|
|
258
|
+
grad_eps_patch=grad_patch,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
covered_mask[region_slice] = True
|
|
262
|
+
|
|
263
|
+
return tuple(outputs)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def pullback_material_input_gradients(
|
|
267
|
+
scene,
|
|
268
|
+
*,
|
|
269
|
+
inputs,
|
|
270
|
+
grad_eps_ex: torch.Tensor,
|
|
271
|
+
grad_eps_ey: torch.Tensor,
|
|
272
|
+
grad_eps_ez: torch.Tensor,
|
|
273
|
+
eps0: float,
|
|
274
|
+
grad_chi3_ex: torch.Tensor | None = None,
|
|
275
|
+
grad_chi3_ey: torch.Tensor | None = None,
|
|
276
|
+
grad_chi3_ez: torch.Tensor | None = None,
|
|
277
|
+
grad_chi2_ex: torch.Tensor | None = None,
|
|
278
|
+
grad_chi2_ey: torch.Tensor | None = None,
|
|
279
|
+
grad_chi2_ez: torch.Tensor | None = None,
|
|
280
|
+
grad_tpa_ex: torch.Tensor | None = None,
|
|
281
|
+
grad_tpa_ey: torch.Tensor | None = None,
|
|
282
|
+
grad_tpa_ez: torch.Tensor | None = None,
|
|
283
|
+
) -> tuple[torch.Tensor, ...]:
|
|
284
|
+
if not inputs:
|
|
285
|
+
return ()
|
|
286
|
+
prepared_scene = prepare_scene(scene)
|
|
287
|
+
|
|
288
|
+
component_gradients = component_node_gradients_from_yee_permittivity(
|
|
289
|
+
prepared_scene,
|
|
290
|
+
grad_eps_ex=grad_eps_ex,
|
|
291
|
+
grad_eps_ey=grad_eps_ey,
|
|
292
|
+
grad_eps_ez=grad_eps_ez,
|
|
293
|
+
eps0=eps0,
|
|
294
|
+
)
|
|
295
|
+
# Back-propagate through the per-axis relative-permittivity node components
|
|
296
|
+
# rather than their scalar average so diagonal-anisotropic sensitivities are
|
|
297
|
+
# attributed to the correct axis. For isotropic media this is exactly the
|
|
298
|
+
# previous scalar-eps pullback.
|
|
299
|
+
model = prepared_scene.compile_materials()
|
|
300
|
+
eps_components = model["eps_components"]
|
|
301
|
+
outputs = []
|
|
302
|
+
grad_outputs = []
|
|
303
|
+
for axis in ("x", "y", "z"):
|
|
304
|
+
component = eps_components[axis]
|
|
305
|
+
if not component.requires_grad:
|
|
306
|
+
continue
|
|
307
|
+
outputs.append(component)
|
|
308
|
+
grad_outputs.append(component_gradients[axis].to(device=component.device, dtype=component.dtype))
|
|
309
|
+
|
|
310
|
+
if grad_chi3_ex is not None:
|
|
311
|
+
# Kerr channel: the compiled chi3 node field feeds all three Yee-edge
|
|
312
|
+
# chi3 tensors through the same two-point averaging (no eps0 scale), so
|
|
313
|
+
# its node gradient is the sum of the three per-axis edge scatters.
|
|
314
|
+
kerr_field = model["kerr_chi3"]
|
|
315
|
+
if kerr_field.requires_grad:
|
|
316
|
+
chi3_node_gradients = component_node_gradients_from_yee_permittivity(
|
|
317
|
+
prepared_scene,
|
|
318
|
+
grad_eps_ex=grad_chi3_ex,
|
|
319
|
+
grad_eps_ey=grad_chi3_ey,
|
|
320
|
+
grad_eps_ez=grad_chi3_ez,
|
|
321
|
+
eps0=1.0,
|
|
322
|
+
)
|
|
323
|
+
chi3_node_gradient = (
|
|
324
|
+
chi3_node_gradients["x"] + chi3_node_gradients["y"] + chi3_node_gradients["z"]
|
|
325
|
+
)
|
|
326
|
+
outputs.append(kerr_field)
|
|
327
|
+
grad_outputs.append(chi3_node_gradient.to(device=kerr_field.device, dtype=kerr_field.dtype))
|
|
328
|
+
|
|
329
|
+
for grad_channel, field_key in (
|
|
330
|
+
((grad_chi2_ex, grad_chi2_ey, grad_chi2_ez), "chi2"),
|
|
331
|
+
((grad_tpa_ex, grad_tpa_ey, grad_tpa_ez), "tpa_sigma"),
|
|
332
|
+
):
|
|
333
|
+
# General-nonlinear channels: the compiled chi2 / TPA-conductivity node
|
|
334
|
+
# fields feed the three Yee-edge tensors through the same two-point
|
|
335
|
+
# averaging as chi3 (no eps0 scale), so each node gradient is the sum of
|
|
336
|
+
# the three per-axis edge scatters.
|
|
337
|
+
grad_ex, grad_ey, grad_ez = grad_channel
|
|
338
|
+
if grad_ex is None:
|
|
339
|
+
continue
|
|
340
|
+
channel_field = model[field_key]
|
|
341
|
+
if not channel_field.requires_grad:
|
|
342
|
+
continue
|
|
343
|
+
node_gradients = component_node_gradients_from_yee_permittivity(
|
|
344
|
+
prepared_scene,
|
|
345
|
+
grad_eps_ex=grad_ex,
|
|
346
|
+
grad_eps_ey=grad_ey,
|
|
347
|
+
grad_eps_ez=grad_ez,
|
|
348
|
+
eps0=1.0,
|
|
349
|
+
)
|
|
350
|
+
node_gradient = node_gradients["x"] + node_gradients["y"] + node_gradients["z"]
|
|
351
|
+
outputs.append(channel_field)
|
|
352
|
+
grad_outputs.append(node_gradient.to(device=channel_field.device, dtype=channel_field.dtype))
|
|
353
|
+
|
|
354
|
+
gradients = (
|
|
355
|
+
torch.autograd.grad(
|
|
356
|
+
outputs,
|
|
357
|
+
inputs,
|
|
358
|
+
grad_outputs=grad_outputs,
|
|
359
|
+
allow_unused=True,
|
|
360
|
+
retain_graph=False,
|
|
361
|
+
)
|
|
362
|
+
if outputs
|
|
363
|
+
else (None,) * len(inputs)
|
|
364
|
+
)
|
|
365
|
+
prepared_scene.release_meshgrid()
|
|
366
|
+
return tuple(
|
|
367
|
+
torch.zeros_like(tensor)
|
|
368
|
+
if grad is None
|
|
369
|
+
else grad.to(device=tensor.device, dtype=tensor.dtype)
|
|
370
|
+
for tensor, grad in zip(inputs, gradients)
|
|
371
|
+
)
|