witwin-maxwell 0.3.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. witwin/maxwell/__init__.py +164 -0
  2. witwin/maxwell/adapters/__init__.py +1 -0
  3. witwin/maxwell/adapters/gds.py +203 -0
  4. witwin/maxwell/adapters/tidy3d.py +1669 -0
  5. witwin/maxwell/adjoint_inputs.py +70 -0
  6. witwin/maxwell/compiler/__init__.py +11 -0
  7. witwin/maxwell/compiler/materials.py +1355 -0
  8. witwin/maxwell/compiler/monitors.py +164 -0
  9. witwin/maxwell/compiler/sources.py +222 -0
  10. witwin/maxwell/fdfd/__init__.py +3 -0
  11. witwin/maxwell/fdfd/adjoint/__init__.py +3 -0
  12. witwin/maxwell/fdfd/adjoint/bridge.py +180 -0
  13. witwin/maxwell/fdfd/ams.py +300 -0
  14. witwin/maxwell/fdfd/krylov.py +280 -0
  15. witwin/maxwell/fdfd/plotting.py +110 -0
  16. witwin/maxwell/fdfd/postprocess.py +33 -0
  17. witwin/maxwell/fdfd/solver.py +1057 -0
  18. witwin/maxwell/fdtd/__init__.py +4 -0
  19. witwin/maxwell/fdtd/adjoint/__init__.py +21 -0
  20. witwin/maxwell/fdtd/adjoint/bridge.py +746 -0
  21. witwin/maxwell/fdtd/adjoint/capabilities.py +29 -0
  22. witwin/maxwell/fdtd/adjoint/core.py +3614 -0
  23. witwin/maxwell/fdtd/adjoint/dispatch.py +684 -0
  24. witwin/maxwell/fdtd/adjoint/native.py +2561 -0
  25. witwin/maxwell/fdtd/adjoint/profiler.py +195 -0
  26. witwin/maxwell/fdtd/adjoint/reverse_common.py +207 -0
  27. witwin/maxwell/fdtd/adjoint/seeds.py +524 -0
  28. witwin/maxwell/fdtd/boundary/__init__.py +37 -0
  29. witwin/maxwell/fdtd/boundary/bloch.py +90 -0
  30. witwin/maxwell/fdtd/boundary/common.py +41 -0
  31. witwin/maxwell/fdtd/boundary/cpml.py +527 -0
  32. witwin/maxwell/fdtd/boundary/runtime.py +209 -0
  33. witwin/maxwell/fdtd/checkpoint.py +227 -0
  34. witwin/maxwell/fdtd/coords.py +32 -0
  35. witwin/maxwell/fdtd/cuda/__init__.py +11 -0
  36. witwin/maxwell/fdtd/cuda/backend.py +3532 -0
  37. witwin/maxwell/fdtd/cuda/build.py +292 -0
  38. witwin/maxwell/fdtd/cuda/extension.cpp +2053 -0
  39. witwin/maxwell/fdtd/cuda/kernels/adjoint.cu +5290 -0
  40. witwin/maxwell/fdtd/cuda/kernels/aniso.cu +1704 -0
  41. witwin/maxwell/fdtd/cuda/kernels/boundary.cu +424 -0
  42. witwin/maxwell/fdtd/cuda/kernels/common.cu +74 -0
  43. witwin/maxwell/fdtd/cuda/kernels/common.cuh +82 -0
  44. witwin/maxwell/fdtd/cuda/kernels/dispersive.cu +782 -0
  45. witwin/maxwell/fdtd/cuda/kernels/electric.cu +3613 -0
  46. witwin/maxwell/fdtd/cuda/kernels/magnetic.cu +781 -0
  47. witwin/maxwell/fdtd/cuda/kernels/observers.cu +347 -0
  48. witwin/maxwell/fdtd/cuda/kernels/projection.cu +211 -0
  49. witwin/maxwell/fdtd/cuda/kernels/sources.cu +1482 -0
  50. witwin/maxwell/fdtd/cuda/kernels/spectral.cu +297 -0
  51. witwin/maxwell/fdtd/cuda/launch.h +23 -0
  52. witwin/maxwell/fdtd/cuda/prebuilt/witwin_maxwell_fdtd_cuda.pyd +0 -0
  53. witwin/maxwell/fdtd/cuda/runtime/__init__.py +6 -0
  54. witwin/maxwell/fdtd/cuda/runtime/dispatcher.py +6 -0
  55. witwin/maxwell/fdtd/cuda/runtime/graph.py +33 -0
  56. witwin/maxwell/fdtd/cuda/tensors.h +35 -0
  57. witwin/maxwell/fdtd/dispersion.py +48 -0
  58. witwin/maxwell/fdtd/evaluation.py +290 -0
  59. witwin/maxwell/fdtd/excitation/__init__.py +27 -0
  60. witwin/maxwell/fdtd/excitation/injection.py +1102 -0
  61. witwin/maxwell/fdtd/excitation/modes.py +1675 -0
  62. witwin/maxwell/fdtd/excitation/spatial.py +469 -0
  63. witwin/maxwell/fdtd/excitation/temporal.py +510 -0
  64. witwin/maxwell/fdtd/excitation/tfsf_apply.py +264 -0
  65. witwin/maxwell/fdtd/excitation/tfsf_common.py +421 -0
  66. witwin/maxwell/fdtd/excitation/tfsf_specs.py +490 -0
  67. witwin/maxwell/fdtd/excitation/tfsf_state.py +778 -0
  68. witwin/maxwell/fdtd/io.py +31 -0
  69. witwin/maxwell/fdtd/material_pullback.py +371 -0
  70. witwin/maxwell/fdtd/meshing/__init__.py +3 -0
  71. witwin/maxwell/fdtd/meshing/autogrid.py +335 -0
  72. witwin/maxwell/fdtd/observers.py +1453 -0
  73. witwin/maxwell/fdtd/plotting.py +121 -0
  74. witwin/maxwell/fdtd/postprocess.py +205 -0
  75. witwin/maxwell/fdtd/runtime/__init__.py +119 -0
  76. witwin/maxwell/fdtd/runtime/initialization.py +236 -0
  77. witwin/maxwell/fdtd/runtime/materials.py +1563 -0
  78. witwin/maxwell/fdtd/runtime/module_cache.py +31 -0
  79. witwin/maxwell/fdtd/runtime/spectral.py +581 -0
  80. witwin/maxwell/fdtd/runtime/stepping.py +1987 -0
  81. witwin/maxwell/fdtd/solver.py +502 -0
  82. witwin/maxwell/geometry/__init__.py +5 -0
  83. witwin/maxwell/geometry/polyslab.py +299 -0
  84. witwin/maxwell/materials.py +3 -0
  85. witwin/maxwell/media.py +1467 -0
  86. witwin/maxwell/monitors.py +743 -0
  87. witwin/maxwell/ports.py +113 -0
  88. witwin/maxwell/postprocess/RCS.py +139 -0
  89. witwin/maxwell/postprocess/__init__.py +42 -0
  90. witwin/maxwell/postprocess/diffraction.py +219 -0
  91. witwin/maxwell/postprocess/directivity.py +152 -0
  92. witwin/maxwell/postprocess/emission.py +67 -0
  93. witwin/maxwell/postprocess/modal.py +324 -0
  94. witwin/maxwell/postprocess/nfft.py +266 -0
  95. witwin/maxwell/postprocess/scattering_parameters.py +358 -0
  96. witwin/maxwell/postprocess/stratton_chu.py +1193 -0
  97. witwin/maxwell/result.py +967 -0
  98. witwin/maxwell/scene.py +1338 -0
  99. witwin/maxwell/simulation.py +659 -0
  100. witwin/maxwell/sources.py +917 -0
  101. witwin/maxwell/visualization/__init__.py +28 -0
  102. witwin/maxwell/visualization/interactive.py +484 -0
  103. witwin/maxwell/visualization/plots.py +568 -0
  104. witwin_maxwell-0.3.0.dist-info/METADATA +184 -0
  105. witwin_maxwell-0.3.0.dist-info/RECORD +107 -0
  106. witwin_maxwell-0.3.0.dist-info/WHEEL +4 -0
  107. witwin_maxwell-0.3.0.dist-info/licenses/LICENSE +13 -0
@@ -0,0 +1,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
+ )
@@ -0,0 +1,3 @@
1
+ from .autogrid import mesh_axis, resolve_auto_grid
2
+
3
+ __all__ = ["mesh_axis", "resolve_auto_grid"]