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,164 @@
1
+ from __future__ import annotations
2
+
3
+ from ..monitors import (
4
+ DiffractionMonitor,
5
+ DipoleEmissionMonitor,
6
+ FieldTimeMonitor,
7
+ FinitePlaneMonitor,
8
+ FluxMonitor,
9
+ FluxTimeMonitor,
10
+ MediumMonitor,
11
+ ModeMonitor,
12
+ PermittivityMonitor,
13
+ PlaneMonitor,
14
+ PointMonitor,
15
+ normalize_component,
16
+ normalize_axis,
17
+ )
18
+ from ..sources import PointDipole
19
+
20
+ _POLARIZATION_AXES = (("x", "Ex"), ("y", "Ey"), ("z", "Ez"))
21
+
22
+
23
+ def _find_scene_dipole(scene, source_name: str) -> PointDipole:
24
+ for source in getattr(scene, "sources", ()):
25
+ if isinstance(source, PointDipole) and getattr(source, "name", None) == source_name:
26
+ return source
27
+ raise ValueError(
28
+ f"DipoleEmissionMonitor references PointDipole {source_name!r}, "
29
+ "which is not present in the scene."
30
+ )
31
+
32
+
33
+ def _point_observer_record(name, position, *, component: str) -> dict[str, object]:
34
+ return {
35
+ "name": str(name),
36
+ "kind": "point",
37
+ "position": tuple(float(value) for value in position),
38
+ "component": normalize_component(component),
39
+ }
40
+
41
+
42
+ def _plane_observer_record(name, axis, position, *, component: str) -> dict[str, object]:
43
+ return {
44
+ "name": str(name),
45
+ "kind": "plane",
46
+ "axis": normalize_axis(axis),
47
+ "position": float(position),
48
+ "component": normalize_component(component),
49
+ }
50
+
51
+
52
+ def _internal_observer_name(monitor_name: str, component: str) -> str:
53
+ return f"{monitor_name}::{normalize_component(component)}"
54
+
55
+
56
+ def compile_fdtd_observers(scene):
57
+ compiled = []
58
+ monitors = scene.resolved_monitors() if hasattr(scene, "resolved_monitors") else scene.monitors
59
+ for monitor in monitors:
60
+ if isinstance(monitor, (FieldTimeMonitor, FluxTimeMonitor, PermittivityMonitor, MediumMonitor)):
61
+ # Time-domain monitors are handled by compile_fdtd_time_observers and
62
+ # material monitors are resolved from compiled material tensors at result
63
+ # time; neither must become a running-DFT spectral observer.
64
+ continue
65
+ if isinstance(monitor, DipoleEmissionMonitor):
66
+ dipole = _find_scene_dipole(scene, monitor.source_name)
67
+ polarization = tuple(float(value) for value in dipole.polarization)
68
+ position = tuple(float(value) for value in dipole.position)
69
+ active = tuple(
70
+ component
71
+ for (axis, component), weight in zip(_POLARIZATION_AXES, polarization)
72
+ if weight != 0.0
73
+ )
74
+ monitor_fields = tuple(normalize_component(component) for component in active)
75
+ for component in monitor_fields:
76
+ observer = _point_observer_record(
77
+ _internal_observer_name(monitor.name, component),
78
+ position,
79
+ component=component,
80
+ )
81
+ observer["monitor_name"] = monitor.name
82
+ observer["monitor_fields"] = monitor_fields
83
+ observer["monitor_frequencies"] = monitor.frequencies
84
+ observer["monitor_type"] = "dipole_emission"
85
+ observer["dipole_polarization"] = polarization
86
+ observer["dipole_position"] = position
87
+ observer["source_name"] = monitor.source_name
88
+ compiled.append(observer)
89
+ continue
90
+ normalized_fields = tuple(normalize_component(field) for field in monitor.fields)
91
+ if isinstance(monitor, PointMonitor):
92
+ for field in normalized_fields:
93
+ observer = _point_observer_record(
94
+ _internal_observer_name(monitor.name, field),
95
+ monitor.position,
96
+ component=field,
97
+ )
98
+ observer["monitor_name"] = monitor.name
99
+ observer["monitor_fields"] = normalized_fields
100
+ compiled.append(observer)
101
+ continue
102
+ if isinstance(monitor, (PlaneMonitor, FinitePlaneMonitor, FluxMonitor, ModeMonitor, DiffractionMonitor)):
103
+ monitor_plane_position = monitor.position if isinstance(monitor, PlaneMonitor) else monitor.plane_position
104
+ for field in normalized_fields:
105
+ observer = _plane_observer_record(
106
+ _internal_observer_name(monitor.name, field),
107
+ axis=monitor.axis,
108
+ position=monitor_plane_position,
109
+ component=field,
110
+ )
111
+ observer["monitor_name"] = monitor.name
112
+ observer["monitor_fields"] = normalized_fields
113
+ observer["monitor_frequencies"] = monitor.frequencies
114
+ observer["compute_flux"] = bool(getattr(monitor, "compute_flux", False))
115
+ observer["normal_direction"] = getattr(monitor, "normal_direction", "+")
116
+ if isinstance(monitor, ModeMonitor):
117
+ observer["monitor_type"] = "mode"
118
+ observer["mode_spec"] = monitor.mode_spec()
119
+ elif isinstance(monitor, DiffractionMonitor):
120
+ observer["monitor_type"] = "diffraction"
121
+ observer["diffraction_spec"] = monitor.diffraction_spec()
122
+ compiled.append(observer)
123
+ continue
124
+ raise ValueError(f"Unsupported monitor type: {type(monitor).__name__}")
125
+ return compiled
126
+
127
+
128
+ def compile_fdtd_time_observers(scene):
129
+ compiled = []
130
+ monitors = scene.resolved_monitors() if hasattr(scene, "resolved_monitors") else scene.monitors
131
+ for monitor in monitors:
132
+ if isinstance(monitor, FieldTimeMonitor):
133
+ record = {
134
+ "name": monitor.name,
135
+ "kind": "field_time",
136
+ "start": int(monitor.start),
137
+ "stop": None if monitor.stop is None else int(monitor.stop),
138
+ "interval": int(monitor.interval),
139
+ "region_kind": monitor.region_kind,
140
+ "components": tuple(normalize_component(field).capitalize() for field in monitor.components),
141
+ "position": tuple(float(value) for value in monitor.position),
142
+ "size": tuple(float(value) for value in monitor.size),
143
+ }
144
+ if monitor.region_kind == "plane":
145
+ record["axis"] = normalize_axis(monitor.axis)
146
+ record["plane_position"] = float(monitor.plane_position)
147
+ compiled.append(record)
148
+ continue
149
+ if isinstance(monitor, FluxTimeMonitor):
150
+ compiled.append(
151
+ {
152
+ "name": monitor.name,
153
+ "kind": "flux_time",
154
+ "start": int(monitor.start),
155
+ "stop": None if monitor.stop is None else int(monitor.stop),
156
+ "interval": int(monitor.interval),
157
+ "axis": normalize_axis(monitor.axis),
158
+ "position": float(monitor.position),
159
+ "fields": tuple(normalize_component(field).capitalize() for field in monitor.fields),
160
+ "normal_direction": monitor.normal_direction,
161
+ }
162
+ )
163
+ continue
164
+ return compiled
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ from ..sources import (
6
+ AstigmaticGaussianBeam,
7
+ CustomCurrentSource,
8
+ CustomFieldSource,
9
+ GaussianBeam,
10
+ ModeSource,
11
+ PlaneWave,
12
+ PointDipole,
13
+ TFSF,
14
+ UniformCurrentSource,
15
+ compile_source_time,
16
+ )
17
+
18
+
19
+ def _compile_injection(injection) -> dict[str, object]:
20
+ if isinstance(injection, TFSF):
21
+ if injection.mode == "slab":
22
+ return {
23
+ "kind": "tfsf",
24
+ "mode": "slab",
25
+ "axis": injection.axis,
26
+ "axis_bounds": injection.axis_bounds,
27
+ }
28
+ return {
29
+ "kind": "tfsf",
30
+ "mode": "box",
31
+ "bounds": injection.bounds,
32
+ }
33
+ return {"kind": str(injection).lower()}
34
+
35
+
36
+ def _compile_fdfd_point_dipole(source: PointDipole, *, default_frequency: float) -> dict:
37
+ source_time = compile_source_time(source.source_time, default_frequency=default_frequency)
38
+ if source_time["kind"] != "cw":
39
+ raise ValueError("FDFD sources currently support CW source_time only.")
40
+ phase = np.exp(1j * float(source_time["phase"]))
41
+ amplitude = float(source_time["amplitude"])
42
+ polarization = tuple(amplitude * phase * component for component in source.polarization)
43
+ return {
44
+ "name": source.name,
45
+ "position": source.position,
46
+ "width": source.width,
47
+ "profile": source.profile,
48
+ "polarization": polarization,
49
+ }
50
+
51
+
52
+ def compile_fdfd_sources(scene, *, default_frequency: float):
53
+ compiled = []
54
+ for source in scene.sources:
55
+ if not isinstance(source, PointDipole):
56
+ raise ValueError(f"Unsupported FDFD source type: {type(source).__name__}")
57
+ compiled.append(_compile_fdfd_point_dipole(source, default_frequency=default_frequency))
58
+ return compiled
59
+
60
+
61
+ def _compile_point_dipole(source: PointDipole, *, default_frequency: float) -> dict:
62
+ return {
63
+ "kind": "point_dipole",
64
+ "name": source.name,
65
+ "position": source.position,
66
+ "width": source.width,
67
+ "profile": source.profile,
68
+ "polarization": source.polarization,
69
+ "source_time": compile_source_time(source.source_time, default_frequency=default_frequency),
70
+ }
71
+
72
+
73
+ def _reject_custom_source_time(source_time: dict, *, source_kind: str) -> dict:
74
+ if source_time["kind"] == "custom":
75
+ raise ValueError(
76
+ f"{source_kind} does not support CustomSourceTime; it requires the native time-shifted "
77
+ "injection kernel. Use PointDipole for arbitrary custom waveforms."
78
+ )
79
+ return source_time
80
+
81
+
82
+ def _compile_plane_wave(source: PlaneWave, *, default_frequency: float) -> dict:
83
+ return {
84
+ "kind": "plane_wave",
85
+ "name": source.name,
86
+ "direction": source.direction,
87
+ "polarization": source.polarization,
88
+ "injection": _compile_injection(source.injection),
89
+ "injection_axis": source.injection_axis,
90
+ "source_time": _reject_custom_source_time(
91
+ compile_source_time(source.source_time, default_frequency=default_frequency),
92
+ source_kind="PlaneWave",
93
+ ),
94
+ }
95
+
96
+
97
+ def _compile_gaussian_beam(source: GaussianBeam, *, default_frequency: float) -> dict:
98
+ return {
99
+ "kind": "gaussian_beam",
100
+ "name": source.name,
101
+ "direction": source.direction,
102
+ "polarization": source.polarization,
103
+ "beam_waist": source.beam_waist,
104
+ "focus": source.focus,
105
+ "injection": _compile_injection(source.injection),
106
+ "injection_axis": source.injection_axis,
107
+ "source_time": _reject_custom_source_time(
108
+ compile_source_time(source.source_time, default_frequency=default_frequency),
109
+ source_kind="GaussianBeam",
110
+ ),
111
+ }
112
+
113
+
114
+ def _compile_astigmatic_gaussian_beam(source: AstigmaticGaussianBeam, *, default_frequency: float) -> dict:
115
+ return {
116
+ "kind": "astigmatic_gaussian_beam",
117
+ "name": source.name,
118
+ "direction": source.direction,
119
+ "polarization": source.polarization,
120
+ "beam_waist_u": source.beam_waist[0],
121
+ "beam_waist_v": source.beam_waist[1],
122
+ "focus": source.focus,
123
+ "focus_u": source.focus_u,
124
+ "focus_v": source.focus_v,
125
+ "injection": _compile_injection(source.injection),
126
+ "injection_axis": source.injection_axis,
127
+ "source_time": _reject_custom_source_time(
128
+ compile_source_time(source.source_time, default_frequency=default_frequency),
129
+ source_kind="AstigmaticGaussianBeam",
130
+ ),
131
+ }
132
+
133
+
134
+ def _compile_uniform_current_source(source: UniformCurrentSource, *, default_frequency: float) -> dict:
135
+ return {
136
+ "kind": "uniform_current",
137
+ "name": source.name,
138
+ "center": source.center,
139
+ "size": source.size,
140
+ "polarization": source.polarization,
141
+ "source_time": compile_source_time(source.source_time, default_frequency=default_frequency),
142
+ }
143
+
144
+
145
+ def _compile_custom_current_source(source: CustomCurrentSource, *, default_frequency: float) -> dict:
146
+ return {
147
+ "kind": "custom_current",
148
+ "name": source.name,
149
+ "dataset": source.current_dataset,
150
+ "source_time": compile_source_time(source.source_time, default_frequency=default_frequency),
151
+ }
152
+
153
+
154
+ def _compile_custom_field_source(source: CustomFieldSource, *, default_frequency: float) -> dict:
155
+ return {
156
+ "kind": "custom_field",
157
+ "name": source.name,
158
+ "dataset": source.field_dataset,
159
+ "normal_axis": source.normal_axis,
160
+ "source_time": compile_source_time(source.source_time, default_frequency=default_frequency),
161
+ }
162
+
163
+
164
+ def _compile_mode_source(source: ModeSource, *, default_frequency: float) -> dict:
165
+ # Broadband injection: the guided profile and effective index are solved once
166
+ # at the waveform's center frequency and driven by the temporal envelope, so a
167
+ # GaussianPulse/Ricker mode source excites the whole band from one run. Only
168
+ # CustomSourceTime is rejected because the native time-shifted surface kernel
169
+ # evaluates the analytic CW/Gaussian/Ricker forms on-device and has no table path.
170
+ source_time = _reject_custom_source_time(
171
+ compile_source_time(source.source_time, default_frequency=default_frequency),
172
+ source_kind="ModeSource",
173
+ )
174
+
175
+ direction_sign = 1 if source.direction == "+" else -1
176
+ direction_vector = {
177
+ "x": (float(direction_sign), 0.0, 0.0),
178
+ "y": (0.0, float(direction_sign), 0.0),
179
+ "z": (0.0, 0.0, float(direction_sign)),
180
+ }[source.normal_axis]
181
+ return {
182
+ "kind": "mode_source",
183
+ "name": source.name,
184
+ "position": source.position,
185
+ "size": source.size,
186
+ "mode_index": int(source.mode_index),
187
+ "direction": source.direction,
188
+ "direction_sign": int(direction_sign),
189
+ "direction_vector": direction_vector,
190
+ "normal_axis": source.normal_axis,
191
+ "polarization": source.polarization,
192
+ "polarization_axis": source.polarization_axis,
193
+ "bend_radius": source.bend_radius,
194
+ "bend_axis": source.bend_axis,
195
+ "injection": {"kind": "soft"},
196
+ "source_time": source_time,
197
+ }
198
+
199
+
200
+ def compile_fdtd_sources(scene, *, default_frequency: float):
201
+ compiled = []
202
+ sources = scene.resolved_sources() if hasattr(scene, "resolved_sources") else scene.sources
203
+ for source in sources:
204
+ if isinstance(source, PointDipole):
205
+ compiled.append(_compile_point_dipole(source, default_frequency=default_frequency))
206
+ elif isinstance(source, PlaneWave):
207
+ compiled.append(_compile_plane_wave(source, default_frequency=default_frequency))
208
+ elif isinstance(source, AstigmaticGaussianBeam):
209
+ compiled.append(_compile_astigmatic_gaussian_beam(source, default_frequency=default_frequency))
210
+ elif isinstance(source, GaussianBeam):
211
+ compiled.append(_compile_gaussian_beam(source, default_frequency=default_frequency))
212
+ elif isinstance(source, ModeSource):
213
+ compiled.append(_compile_mode_source(source, default_frequency=default_frequency))
214
+ elif isinstance(source, UniformCurrentSource):
215
+ compiled.append(_compile_uniform_current_source(source, default_frequency=default_frequency))
216
+ elif isinstance(source, CustomCurrentSource):
217
+ compiled.append(_compile_custom_current_source(source, default_frequency=default_frequency))
218
+ elif isinstance(source, CustomFieldSource):
219
+ compiled.append(_compile_custom_field_source(source, default_frequency=default_frequency))
220
+ else:
221
+ raise ValueError(f"Unsupported FDTD source type: {type(source).__name__}")
222
+ return compiled
@@ -0,0 +1,3 @@
1
+ from .solver import FDFD
2
+
3
+ __all__ = ["FDFD"]
@@ -0,0 +1,3 @@
1
+ from .bridge import run_fdfd_with_gradient_bridge
2
+
3
+ __all__ = ["run_fdfd_with_gradient_bridge"]
@@ -0,0 +1,180 @@
1
+ """Differentiable FDFD via the adjoint method.
2
+
3
+ For the linear system A(eps) x = b with real loss L(x), the adjoint solve
4
+ A^T lam = conj(dL/dx*) yields material gradients without differentiating
5
+ through the solver iterations. The permittivity enters A only on the
6
+ diagonal (k0^2 * eps_face), so
7
+
8
+ dL/deps_face = -2 k0^2 Re[lam * x] (per Yee face)
9
+
10
+ which is pulled back to trainable scene inputs through the shared material
11
+ compiler graph, exactly like the FDTD gradient bridge.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import cupy as cp
17
+ import torch
18
+
19
+ from ...adjoint_inputs import material_dependent_inputs, scene_trainable_material_tensors
20
+ from ...fdtd.material_pullback import pullback_material_input_gradients
21
+ from ...result import Result
22
+
23
+ # The FDFD matrix uses relative permittivity directly, so the pullback's
24
+ # absolute-permittivity scaling is the identity.
25
+ _RELATIVE_EPS_SCALE = 1.0
26
+
27
+
28
+ def _unsupported_fdfd_adjoint_medium(scene):
29
+ for structure in getattr(scene, "structures", ()):
30
+ material = getattr(structure, "material", None)
31
+ if material is None:
32
+ continue
33
+ if getattr(material, "is_medium2d", False):
34
+ return "FDFD adjoint does not support 2D sheet (Medium2D) media yet."
35
+ if getattr(material, "is_anisotropic", False):
36
+ return "FDFD adjoint does not support anisotropic media yet."
37
+ if getattr(material, "is_dispersive", False):
38
+ return "FDFD adjoint does not support dispersive media yet."
39
+ return None
40
+
41
+
42
+ class _FDFDGradientBridge:
43
+ def __init__(self, simulation):
44
+ self.simulation = simulation
45
+ self.base_scene = simulation.scene
46
+ self.material_inputs = self._resolve_material_inputs()
47
+ if not self.material_inputs:
48
+ raise NotImplementedError(
49
+ "FDFD backward currently supports trainable scene inputs that contribute to "
50
+ "prepared-scene material tensors."
51
+ )
52
+ self.solver = None
53
+ self.solver_stats = None
54
+
55
+ def _material_graph_scene(self):
56
+ if self.simulation.scene_module is not None:
57
+ return self.simulation.scene_module.to_scene()
58
+ self.simulation._refresh_scene()
59
+ return self.simulation.scene
60
+
61
+ def _candidate_material_inputs(self) -> tuple[torch.Tensor, ...]:
62
+ if self.simulation.scene_module is not None:
63
+ return tuple(
64
+ parameter
65
+ for parameter in self.simulation.scene_module.parameters()
66
+ if parameter.requires_grad
67
+ )
68
+ return scene_trainable_material_tensors(self.base_scene)
69
+
70
+ def _resolve_material_inputs(self) -> tuple[torch.Tensor, ...]:
71
+ scene = self._material_graph_scene()
72
+ return material_dependent_inputs(scene, self._candidate_material_inputs())
73
+
74
+ def forward(self, _material_inputs):
75
+ solver = self.simulation._build_fdfd_solver()
76
+ unsupported = _unsupported_fdfd_adjoint_medium(solver.scene)
77
+ if unsupported is not None:
78
+ raise NotImplementedError(unsupported)
79
+
80
+ solver_cfg = self.simulation.config.solver
81
+ solver.solve(
82
+ max_iter=solver_cfg.max_iter,
83
+ tol=solver_cfg.tol,
84
+ restart=solver_cfg.restart,
85
+ )
86
+ if solver.E_field is None:
87
+ raise RuntimeError("FDFD solve did not produce any field data.")
88
+
89
+ self.solver = solver
90
+ self.solver_stats = {
91
+ "solver": {
92
+ "type": solver_cfg.solver_type,
93
+ "max_iter": solver_cfg.max_iter,
94
+ "tol": solver_cfg.tol,
95
+ "restart": solver_cfg.restart,
96
+ "preconditioner": solver_cfg.preconditioner,
97
+ "precision": solver_cfg.precision,
98
+ },
99
+ "converged": getattr(solver, "converged", None),
100
+ "solver_info": getattr(solver, "solver_info", None),
101
+ "final_residual": getattr(solver, "final_residual", None),
102
+ }
103
+ return solver.E_field
104
+
105
+ def backward(self, material_inputs, grad_outputs):
106
+ solver = self.solver
107
+ scene = solver.scene
108
+ solver_cfg = self.simulation.config.solver
109
+
110
+ resolved = tuple(
111
+ torch.zeros_like(field) if grad is None else grad
112
+ for field, grad in zip(solver.E_field, grad_outputs)
113
+ )
114
+ # For real loss L, PyTorch hands us g = 2 dL/dx* (real-pair gradient
115
+ # packed as complex). Solving A^T lam = conj(g) gives lam = 2 A^-T
116
+ # dL/dx, and dL/deps_face = -2 k0^2 Re[(lam/2) x] = -k0^2 Re[lam x].
117
+ grad_x = torch.cat([grad.reshape(-1) for grad in resolved]).to(torch.complex64)
118
+ rhs = cp.from_dlpack(grad_x.conj().resolve_conj().contiguous()).astype(cp.complex64, copy=False)
119
+ lam = solver.solve_adjoint(
120
+ rhs,
121
+ max_iter=solver_cfg.max_iter,
122
+ tol=solver_cfg.tol,
123
+ restart=solver_cfg.restart,
124
+ )
125
+
126
+ x = cp.from_dlpack(
127
+ torch.cat([field.reshape(-1) for field in solver.E_field]).contiguous()
128
+ )
129
+ grad_eps_flat = (-solver.k0**2) * (lam * x).real.astype(cp.float32, copy=False)
130
+
131
+ N_ex, N_ey = scene.N_ex, scene.N_ey
132
+ grad_eps_ex = torch.from_dlpack(
133
+ grad_eps_flat[:N_ex].reshape(scene.Nx_ex, scene.Ny_ex, scene.Nz_ex)
134
+ )
135
+ grad_eps_ey = torch.from_dlpack(
136
+ grad_eps_flat[N_ex:N_ex + N_ey].reshape(scene.Nx_ey, scene.Ny_ey, scene.Nz_ey)
137
+ )
138
+ grad_eps_ez = torch.from_dlpack(
139
+ grad_eps_flat[N_ex + N_ey:].reshape(scene.Nx_ez, scene.Ny_ez, scene.Nz_ez)
140
+ )
141
+
142
+ with torch.enable_grad():
143
+ graph_scene = self._material_graph_scene()
144
+ return pullback_material_input_gradients(
145
+ graph_scene,
146
+ inputs=material_inputs,
147
+ grad_eps_ex=grad_eps_ex,
148
+ grad_eps_ey=grad_eps_ey,
149
+ grad_eps_ez=grad_eps_ez,
150
+ eps0=_RELATIVE_EPS_SCALE,
151
+ )
152
+
153
+
154
+ class _FDFDMaterialGradientFunction(torch.autograd.Function):
155
+ @staticmethod
156
+ def forward(ctx, bridge, *material_inputs):
157
+ ctx.bridge = bridge
158
+ ctx.material_inputs = tuple(material_inputs)
159
+ return bridge.forward(tuple(material_inputs))
160
+
161
+ @staticmethod
162
+ def backward(ctx, *grad_outputs):
163
+ gradients = ctx.bridge.backward(ctx.material_inputs, grad_outputs)
164
+ return (None, *gradients)
165
+
166
+
167
+ def run_fdfd_with_gradient_bridge(simulation) -> Result:
168
+ bridge = _FDFDGradientBridge(simulation)
169
+ Ex, Ey, Ez = _FDFDMaterialGradientFunction.apply(bridge, *bridge.material_inputs)
170
+ return Result(
171
+ method="fdfd",
172
+ scene=simulation.scene,
173
+ prepared_scene=bridge.solver.scene,
174
+ frequency=simulation.frequency,
175
+ frequencies=simulation.frequencies,
176
+ solver=bridge.solver,
177
+ fields={"EX": Ex, "EY": Ey, "EZ": Ez},
178
+ metadata=simulation.metadata,
179
+ solver_stats=bridge.solver_stats,
180
+ )