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,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,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
|
+
)
|