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,659 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Iterable
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from .monitors import MediumMonitor, PermittivityMonitor
|
|
10
|
+
from .result import Result
|
|
11
|
+
from .scene import Scene, SceneModule, prepare_scene
|
|
12
|
+
|
|
13
|
+
FDFD = None
|
|
14
|
+
FDTD = None
|
|
15
|
+
calculate_required_steps = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SimulationMethod(str, Enum):
|
|
19
|
+
FDFD = "fdfd"
|
|
20
|
+
FDTD = "fdtd"
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
return self.value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SpectralWindowKind(str, Enum):
|
|
27
|
+
NONE = "none"
|
|
28
|
+
HANNING = "hanning"
|
|
29
|
+
RAMP = "ramp"
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return self.value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AbsorberKind(str, Enum):
|
|
36
|
+
CPML = "cpml"
|
|
37
|
+
PML = "pml"
|
|
38
|
+
ABSORBER = "absorber"
|
|
39
|
+
STABLE_PML = "stablepml"
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return self.value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _normalize_frequencies(*, frequencies=None, frequency=None) -> tuple[float, ...]:
|
|
46
|
+
if frequency is not None and frequencies is not None:
|
|
47
|
+
raise ValueError("Pass either frequency or frequencies, not both.")
|
|
48
|
+
if frequencies is None:
|
|
49
|
+
if frequency is None:
|
|
50
|
+
raise ValueError("A target frequency is required.")
|
|
51
|
+
return (float(frequency),)
|
|
52
|
+
|
|
53
|
+
if isinstance(frequencies, Iterable) and not isinstance(frequencies, (str, bytes)):
|
|
54
|
+
values = tuple(float(value) for value in frequencies)
|
|
55
|
+
else:
|
|
56
|
+
values = (float(frequencies),)
|
|
57
|
+
if not values:
|
|
58
|
+
raise ValueError("frequencies must not be empty.")
|
|
59
|
+
return values
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _normalize_single_frequency(*, frequency=None) -> float:
|
|
63
|
+
values = _normalize_frequencies(frequency=frequency)
|
|
64
|
+
if len(values) != 1:
|
|
65
|
+
raise ValueError("This solver path only supports a single frequency.")
|
|
66
|
+
return values[0]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _normalize_spectral_window_kind(value) -> SpectralWindowKind:
|
|
70
|
+
if isinstance(value, SpectralWindowKind):
|
|
71
|
+
return value
|
|
72
|
+
try:
|
|
73
|
+
return SpectralWindowKind(str(value).strip().lower())
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
choices = ", ".join(item.value for item in SpectralWindowKind)
|
|
76
|
+
raise ValueError(f"window must be one of: {choices}.") from exc
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _normalize_absorber_kind(value) -> AbsorberKind:
|
|
80
|
+
if isinstance(value, AbsorberKind):
|
|
81
|
+
return value
|
|
82
|
+
try:
|
|
83
|
+
return AbsorberKind(str(value).strip().lower())
|
|
84
|
+
except ValueError as exc:
|
|
85
|
+
choices = ", ".join(item.value for item in AbsorberKind)
|
|
86
|
+
raise ValueError(f"absorber must be one of: {choices}.") from exc
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class GMRES:
|
|
91
|
+
max_iter: int = 5000
|
|
92
|
+
tol: float = 1e-6
|
|
93
|
+
restart: int = 200
|
|
94
|
+
solver_type: str = "gmres"
|
|
95
|
+
preconditioner: str = "jacobi"
|
|
96
|
+
precision: str = "single" # 'single' | 'double': working precision of the iterative solve
|
|
97
|
+
ssor_omega: float = 0.8 # relaxation factor for the 'ssor' preconditioner
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class TimeConfig:
|
|
102
|
+
time_steps: int | None = None
|
|
103
|
+
steady_cycles: int = 20
|
|
104
|
+
transient_cycles: int = 15
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def auto(cls, steady_cycles: int = 20, transient_cycles: int = 15) -> "TimeConfig":
|
|
108
|
+
return cls(
|
|
109
|
+
time_steps=None,
|
|
110
|
+
steady_cycles=steady_cycles,
|
|
111
|
+
transient_cycles=transient_cycles,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class SpectralSampler:
|
|
117
|
+
window: SpectralWindowKind = SpectralWindowKind.HANNING
|
|
118
|
+
normalize_source: bool = False
|
|
119
|
+
|
|
120
|
+
def __post_init__(self):
|
|
121
|
+
self.window = _normalize_spectral_window_kind(self.window)
|
|
122
|
+
self.normalize_source = bool(self.normalize_source)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class FDFDConfig:
|
|
127
|
+
solver: GMRES = field(default_factory=GMRES)
|
|
128
|
+
enable_plot: bool = False
|
|
129
|
+
verbose: bool = False
|
|
130
|
+
|
|
131
|
+
def __post_init__(self):
|
|
132
|
+
if not isinstance(self.solver, GMRES):
|
|
133
|
+
raise TypeError("solver must be a GMRES instance.")
|
|
134
|
+
self.enable_plot = bool(self.enable_plot)
|
|
135
|
+
self.verbose = bool(self.verbose)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class FDTDConfig:
|
|
140
|
+
run_time: TimeConfig = field(default_factory=TimeConfig.auto)
|
|
141
|
+
spectral_sampler: SpectralSampler = field(default_factory=SpectralSampler)
|
|
142
|
+
enable_plot: bool = False
|
|
143
|
+
full_field_dft: bool = False
|
|
144
|
+
absorber: AbsorberKind = AbsorberKind.CPML
|
|
145
|
+
cpml_config: dict[str, Any] = field(default_factory=dict)
|
|
146
|
+
adjoint_checkpoint_stride: int | None = None
|
|
147
|
+
shutoff: float = 0.0 # relative E-energy threshold for auto-shutoff; 0 disables (opt-in)
|
|
148
|
+
shutoff_check_interval: int = 100
|
|
149
|
+
cuda_graph: bool = False # capture the field-update core into a CUDA graph (opt-in)
|
|
150
|
+
|
|
151
|
+
def __post_init__(self):
|
|
152
|
+
if not isinstance(self.run_time, TimeConfig):
|
|
153
|
+
raise TypeError("run_time must be a TimeConfig instance.")
|
|
154
|
+
if not isinstance(self.spectral_sampler, SpectralSampler):
|
|
155
|
+
raise TypeError("spectral_sampler must be a SpectralSampler instance.")
|
|
156
|
+
self.enable_plot = bool(self.enable_plot)
|
|
157
|
+
self.full_field_dft = bool(self.full_field_dft)
|
|
158
|
+
self.absorber = _normalize_absorber_kind(self.absorber)
|
|
159
|
+
self.cpml_config = dict(self.cpml_config or {})
|
|
160
|
+
self.shutoff = float(self.shutoff)
|
|
161
|
+
if self.shutoff < 0:
|
|
162
|
+
raise ValueError("shutoff must be >= 0.")
|
|
163
|
+
self.shutoff_check_interval = int(self.shutoff_check_interval)
|
|
164
|
+
if self.shutoff_check_interval <= 0:
|
|
165
|
+
raise ValueError("shutoff_check_interval must be > 0.")
|
|
166
|
+
self.cuda_graph = bool(self.cuda_graph)
|
|
167
|
+
if self.adjoint_checkpoint_stride is not None:
|
|
168
|
+
self.adjoint_checkpoint_stride = int(self.adjoint_checkpoint_stride)
|
|
169
|
+
if self.adjoint_checkpoint_stride <= 0:
|
|
170
|
+
raise ValueError("adjoint_checkpoint_stride must be > 0 when provided.")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _to_tensor_fields(field_mapping: dict[str, Any], device: str | torch.device) -> dict[str, torch.Tensor]:
|
|
174
|
+
return {
|
|
175
|
+
name.upper(): torch.as_tensor(value, device=device)
|
|
176
|
+
for name, value in field_mapping.items()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _resolve_fdfd_backend():
|
|
181
|
+
global FDFD
|
|
182
|
+
if FDFD is None:
|
|
183
|
+
from .fdfd import FDFD as backend
|
|
184
|
+
|
|
185
|
+
FDFD = backend
|
|
186
|
+
return FDFD
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _resolve_fdtd_backend():
|
|
190
|
+
global FDTD, calculate_required_steps
|
|
191
|
+
if FDTD is None or calculate_required_steps is None:
|
|
192
|
+
from .fdtd import FDTD as fdtd_backend
|
|
193
|
+
from .fdtd import calculate_required_steps as calculate_steps_backend
|
|
194
|
+
|
|
195
|
+
FDTD = fdtd_backend
|
|
196
|
+
calculate_required_steps = calculate_steps_backend
|
|
197
|
+
return FDTD, calculate_required_steps
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _resolve_scene_input(scene_like):
|
|
201
|
+
if isinstance(scene_like, Scene):
|
|
202
|
+
return scene_like, None
|
|
203
|
+
if isinstance(scene_like, SceneModule):
|
|
204
|
+
return scene_like.to_scene(), scene_like
|
|
205
|
+
raise TypeError("scene must be a maxwell.Scene or maxwell.SceneModule.")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _scene_trainable_density_parameters(scene: Scene) -> tuple[torch.Tensor, ...]:
|
|
209
|
+
trainable = []
|
|
210
|
+
for region in getattr(scene, "material_regions", ()):
|
|
211
|
+
density = getattr(region, "density", None)
|
|
212
|
+
if isinstance(density, torch.Tensor) and density.requires_grad:
|
|
213
|
+
trainable.append(density)
|
|
214
|
+
return tuple(trainable)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _scene_trainable_geometry_parameters(scene: Scene) -> tuple[torch.Tensor, ...]:
|
|
218
|
+
trainable = []
|
|
219
|
+
for structure in scene.structures:
|
|
220
|
+
geometry = getattr(structure, "geometry", None)
|
|
221
|
+
if geometry is None:
|
|
222
|
+
continue
|
|
223
|
+
for value in vars(geometry).values():
|
|
224
|
+
if isinstance(value, torch.Tensor) and value.requires_grad:
|
|
225
|
+
trainable.append(value)
|
|
226
|
+
return tuple(trainable)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _scene_trainable_material_parameters(scene: Scene) -> tuple[torch.Tensor, ...]:
|
|
230
|
+
trainable = []
|
|
231
|
+
for structure in scene.structures:
|
|
232
|
+
material = getattr(structure, "material", None)
|
|
233
|
+
perturbation = getattr(material, "perturbation", None)
|
|
234
|
+
if isinstance(perturbation, torch.Tensor) and perturbation.requires_grad:
|
|
235
|
+
trainable.append(perturbation)
|
|
236
|
+
return tuple(trainable)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _require_cuda_scene(scene: Scene, *, method: str) -> None:
|
|
240
|
+
device = torch.device(scene.device)
|
|
241
|
+
if device.type != "cuda":
|
|
242
|
+
raise ValueError(
|
|
243
|
+
f"Simulation.{method}(...) requires scene.device to be CUDA, got {device}."
|
|
244
|
+
)
|
|
245
|
+
if not torch.cuda.is_available():
|
|
246
|
+
raise RuntimeError(
|
|
247
|
+
f"Simulation.{method}(...) requires CUDA, but torch.cuda.is_available() is False."
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class Simulation:
|
|
252
|
+
def __init__(
|
|
253
|
+
self,
|
|
254
|
+
*,
|
|
255
|
+
scene,
|
|
256
|
+
method: SimulationMethod,
|
|
257
|
+
frequencies: tuple[float, ...],
|
|
258
|
+
config: FDFDConfig | FDTDConfig | None = None,
|
|
259
|
+
metadata: dict[str, Any] | None = None,
|
|
260
|
+
):
|
|
261
|
+
if not frequencies:
|
|
262
|
+
raise ValueError("At least one frequency is required.")
|
|
263
|
+
resolved_scene, scene_module = _resolve_scene_input(scene)
|
|
264
|
+
self.scene_input = scene
|
|
265
|
+
self.scene = resolved_scene
|
|
266
|
+
self.scene_module = scene_module
|
|
267
|
+
self.has_trainable_parameters = bool(
|
|
268
|
+
any(parameter.requires_grad for parameter in (scene_module.parameters() if scene_module is not None else ()))
|
|
269
|
+
or _scene_trainable_density_parameters(resolved_scene)
|
|
270
|
+
or _scene_trainable_geometry_parameters(resolved_scene)
|
|
271
|
+
or _scene_trainable_material_parameters(resolved_scene)
|
|
272
|
+
)
|
|
273
|
+
self.method = SimulationMethod(method)
|
|
274
|
+
self.frequencies = tuple(float(freq) for freq in frequencies)
|
|
275
|
+
self.frequency = self.frequencies[0]
|
|
276
|
+
if self.method == SimulationMethod.FDFD:
|
|
277
|
+
if config is None:
|
|
278
|
+
config = FDFDConfig()
|
|
279
|
+
if not isinstance(config, FDFDConfig):
|
|
280
|
+
raise TypeError("FDFD simulations require an FDFDConfig.")
|
|
281
|
+
else:
|
|
282
|
+
if config is None:
|
|
283
|
+
config = FDTDConfig()
|
|
284
|
+
if not isinstance(config, FDTDConfig):
|
|
285
|
+
raise TypeError("FDTD simulations require an FDTDConfig.")
|
|
286
|
+
self.config = config
|
|
287
|
+
self.metadata = dict(metadata or {})
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def fdfd(
|
|
291
|
+
cls,
|
|
292
|
+
scene,
|
|
293
|
+
*,
|
|
294
|
+
frequency=None,
|
|
295
|
+
solver: GMRES | None = None,
|
|
296
|
+
enable_plot: bool = False,
|
|
297
|
+
verbose: bool = False,
|
|
298
|
+
) -> "Simulation":
|
|
299
|
+
return cls(
|
|
300
|
+
scene=scene,
|
|
301
|
+
method=SimulationMethod.FDFD,
|
|
302
|
+
frequencies=(_normalize_single_frequency(frequency=frequency),),
|
|
303
|
+
config=FDFDConfig(
|
|
304
|
+
solver=solver or GMRES(),
|
|
305
|
+
enable_plot=enable_plot,
|
|
306
|
+
verbose=verbose,
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
@classmethod
|
|
311
|
+
def fdtd(
|
|
312
|
+
cls,
|
|
313
|
+
scene,
|
|
314
|
+
*,
|
|
315
|
+
frequencies=None,
|
|
316
|
+
frequency=None,
|
|
317
|
+
run_time: TimeConfig | None = None,
|
|
318
|
+
absorber: AbsorberKind | str = AbsorberKind.CPML,
|
|
319
|
+
spectral_sampler: SpectralSampler | None = None,
|
|
320
|
+
cpml_config: dict[str, Any] | None = None,
|
|
321
|
+
enable_plot: bool = False,
|
|
322
|
+
full_field_dft: bool = False,
|
|
323
|
+
shutoff: float = 0.0,
|
|
324
|
+
shutoff_check_interval: int = 100,
|
|
325
|
+
cuda_graph: bool = False,
|
|
326
|
+
) -> "Simulation":
|
|
327
|
+
return cls(
|
|
328
|
+
scene=scene,
|
|
329
|
+
method=SimulationMethod.FDTD,
|
|
330
|
+
frequencies=_normalize_frequencies(frequencies=frequencies, frequency=frequency),
|
|
331
|
+
config=FDTDConfig(
|
|
332
|
+
run_time=run_time or TimeConfig.auto(),
|
|
333
|
+
absorber=absorber,
|
|
334
|
+
spectral_sampler=spectral_sampler or SpectralSampler(),
|
|
335
|
+
cpml_config={} if cpml_config is None else cpml_config,
|
|
336
|
+
enable_plot=enable_plot,
|
|
337
|
+
full_field_dft=full_field_dft,
|
|
338
|
+
shutoff=shutoff,
|
|
339
|
+
shutoff_check_interval=shutoff_check_interval,
|
|
340
|
+
cuda_graph=cuda_graph,
|
|
341
|
+
),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def prepare(self):
|
|
345
|
+
self._refresh_scene()
|
|
346
|
+
if self.method == SimulationMethod.FDFD:
|
|
347
|
+
solver = self._build_fdfd_solver()
|
|
348
|
+
elif self.method == SimulationMethod.FDTD:
|
|
349
|
+
solver = self._build_fdtd_solver(initialize=True)
|
|
350
|
+
if self.has_trainable_parameters:
|
|
351
|
+
from .fdtd.adjoint.dispatch import validate_native_adjoint_preparation
|
|
352
|
+
|
|
353
|
+
validate_native_adjoint_preparation(solver)
|
|
354
|
+
else:
|
|
355
|
+
raise ValueError(f"Unsupported simulation method {self.method!r}.")
|
|
356
|
+
return PreparedSimulation(self, solver)
|
|
357
|
+
|
|
358
|
+
def run(self) -> Result:
|
|
359
|
+
self._refresh_scene()
|
|
360
|
+
if self.method == SimulationMethod.FDFD:
|
|
361
|
+
return self._run_fdfd()
|
|
362
|
+
if self.method == SimulationMethod.FDTD:
|
|
363
|
+
return self._run_fdtd()
|
|
364
|
+
raise ValueError(f"Unsupported simulation method {self.method!r}.")
|
|
365
|
+
|
|
366
|
+
def _refresh_scene(self):
|
|
367
|
+
if self.scene_module is not None:
|
|
368
|
+
self.scene = self.scene_module.to_scene()
|
|
369
|
+
return
|
|
370
|
+
if isinstance(self.scene_input, Scene):
|
|
371
|
+
self.scene = self.scene_input
|
|
372
|
+
|
|
373
|
+
def _run_fdfd(self) -> Result:
|
|
374
|
+
if self.has_trainable_parameters:
|
|
375
|
+
return self._run_fdfd_with_gradient_bridge()
|
|
376
|
+
solver = self._build_fdfd_solver()
|
|
377
|
+
solver_cfg = self.config.solver
|
|
378
|
+
return self._build_fdfd_result(solver, solver_cfg)
|
|
379
|
+
|
|
380
|
+
def _run_fdfd_with_gradient_bridge(self) -> Result:
|
|
381
|
+
from .fdfd.adjoint import run_fdfd_with_gradient_bridge
|
|
382
|
+
|
|
383
|
+
return run_fdfd_with_gradient_bridge(self)
|
|
384
|
+
|
|
385
|
+
def _build_fdfd_solver(self):
|
|
386
|
+
prepared_scene = prepare_scene(self.scene)
|
|
387
|
+
if prepared_scene.grid.is_custom:
|
|
388
|
+
raise NotImplementedError(
|
|
389
|
+
"FDFD does not support nonuniform (GridSpec.custom / GridSpec.auto) grids yet; "
|
|
390
|
+
"use Simulation.fdtd(...) or a uniform GridSpec."
|
|
391
|
+
)
|
|
392
|
+
_require_cuda_scene(prepared_scene, method="fdfd")
|
|
393
|
+
fdfd_backend = _resolve_fdfd_backend()
|
|
394
|
+
solver_cfg = self.config.solver
|
|
395
|
+
return fdfd_backend(
|
|
396
|
+
prepared_scene,
|
|
397
|
+
frequency=self.frequency,
|
|
398
|
+
solver_type=solver_cfg.solver_type,
|
|
399
|
+
preconditioner=solver_cfg.preconditioner,
|
|
400
|
+
precision=solver_cfg.precision,
|
|
401
|
+
ssor_omega=solver_cfg.ssor_omega,
|
|
402
|
+
enable_plot=self.config.enable_plot,
|
|
403
|
+
verbose=self.config.verbose,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def _build_fdfd_result(self, solver, solver_cfg: GMRES) -> Result:
|
|
407
|
+
solver.solve(
|
|
408
|
+
max_iter=solver_cfg.max_iter,
|
|
409
|
+
tol=solver_cfg.tol,
|
|
410
|
+
restart=solver_cfg.restart,
|
|
411
|
+
)
|
|
412
|
+
if solver.E_field is None:
|
|
413
|
+
raise RuntimeError("FDFD solve did not produce any field data.")
|
|
414
|
+
|
|
415
|
+
fields = {
|
|
416
|
+
"EX": solver.E_field[0],
|
|
417
|
+
"EY": solver.E_field[1],
|
|
418
|
+
"EZ": solver.E_field[2],
|
|
419
|
+
}
|
|
420
|
+
solver_stats = {
|
|
421
|
+
"solver": {
|
|
422
|
+
"type": solver_cfg.solver_type,
|
|
423
|
+
"max_iter": solver_cfg.max_iter,
|
|
424
|
+
"tol": solver_cfg.tol,
|
|
425
|
+
"restart": solver_cfg.restart,
|
|
426
|
+
"preconditioner": solver_cfg.preconditioner,
|
|
427
|
+
"precision": solver_cfg.precision,
|
|
428
|
+
},
|
|
429
|
+
"converged": getattr(solver, "converged", None),
|
|
430
|
+
"solver_info": getattr(solver, "solver_info", None),
|
|
431
|
+
"final_residual": getattr(solver, "final_residual", None),
|
|
432
|
+
}
|
|
433
|
+
return Result(
|
|
434
|
+
method="fdfd",
|
|
435
|
+
scene=self.scene,
|
|
436
|
+
prepared_scene=solver.scene,
|
|
437
|
+
frequency=self.frequency,
|
|
438
|
+
frequencies=self.frequencies,
|
|
439
|
+
solver=solver,
|
|
440
|
+
fields=fields,
|
|
441
|
+
metadata=self.metadata,
|
|
442
|
+
solver_stats=solver_stats,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def _run_fdtd(self) -> Result:
|
|
446
|
+
if self.has_trainable_parameters:
|
|
447
|
+
return self._run_fdtd_with_gradient_bridge()
|
|
448
|
+
solver = self._build_fdtd_solver(initialize=True)
|
|
449
|
+
return self._run_fdtd_from_solver(solver)
|
|
450
|
+
|
|
451
|
+
def _build_fdtd_solver(self, *, initialize: bool):
|
|
452
|
+
return self._build_fdtd_solver_for_scene(self.scene, initialize=initialize)
|
|
453
|
+
|
|
454
|
+
def _build_fdtd_solver_for_scene(self, scene, *, initialize: bool):
|
|
455
|
+
prepared_scene = prepare_scene(scene)
|
|
456
|
+
_require_cuda_scene(prepared_scene, method="fdtd")
|
|
457
|
+
fdtd_backend, _ = _resolve_fdtd_backend()
|
|
458
|
+
solver = fdtd_backend(
|
|
459
|
+
prepared_scene,
|
|
460
|
+
frequency=self.frequency,
|
|
461
|
+
absorber_type=self.config.absorber,
|
|
462
|
+
cpml_config=self.config.cpml_config,
|
|
463
|
+
)
|
|
464
|
+
if initialize:
|
|
465
|
+
solver.init_field()
|
|
466
|
+
return solver
|
|
467
|
+
|
|
468
|
+
def _collect_fdtd_requested_frequencies(self, scene=None) -> tuple[float, ...]:
|
|
469
|
+
resolved_scene = self.scene if scene is None else scene
|
|
470
|
+
ordered = list(self.frequencies)
|
|
471
|
+
seen = set(ordered)
|
|
472
|
+
monitors = resolved_scene.resolved_monitors() if hasattr(resolved_scene, "resolved_monitors") else resolved_scene.monitors
|
|
473
|
+
for monitor in monitors:
|
|
474
|
+
if isinstance(monitor, (PermittivityMonitor, MediumMonitor)):
|
|
475
|
+
# Material monitors are evaluated analytically from compiled tensors and
|
|
476
|
+
# do not require the simulation to accumulate a DFT at their frequencies.
|
|
477
|
+
continue
|
|
478
|
+
monitor_frequencies = getattr(monitor, "frequencies", None)
|
|
479
|
+
if monitor_frequencies is None:
|
|
480
|
+
continue
|
|
481
|
+
for freq in monitor_frequencies:
|
|
482
|
+
freq_value = float(freq)
|
|
483
|
+
if freq_value in seen:
|
|
484
|
+
continue
|
|
485
|
+
ordered.append(freq_value)
|
|
486
|
+
seen.add(freq_value)
|
|
487
|
+
return tuple(ordered)
|
|
488
|
+
|
|
489
|
+
def _resolve_fdtd_time_steps(self, solver, scene=None) -> int:
|
|
490
|
+
_, steps_helper = _resolve_fdtd_backend()
|
|
491
|
+
resolved_scene = self.scene if scene is None else scene
|
|
492
|
+
runtime_cfg = self.config.run_time
|
|
493
|
+
if runtime_cfg.time_steps is not None:
|
|
494
|
+
return runtime_cfg.time_steps
|
|
495
|
+
|
|
496
|
+
domain_size = max(
|
|
497
|
+
float(resolved_scene.domain.bounds[0][1] - resolved_scene.domain.bounds[0][0]),
|
|
498
|
+
float(resolved_scene.domain.bounds[1][1] - resolved_scene.domain.bounds[1][0]),
|
|
499
|
+
float(resolved_scene.domain.bounds[2][1] - resolved_scene.domain.bounds[2][0]),
|
|
500
|
+
)
|
|
501
|
+
requested_frequencies = self._collect_fdtd_requested_frequencies(resolved_scene)
|
|
502
|
+
return steps_helper(
|
|
503
|
+
frequency=min(requested_frequencies),
|
|
504
|
+
dt=solver.dt,
|
|
505
|
+
c=solver.c,
|
|
506
|
+
num_cycles=runtime_cfg.steady_cycles,
|
|
507
|
+
transient_cycles=runtime_cfg.transient_cycles,
|
|
508
|
+
domain_size=domain_size,
|
|
509
|
+
source_time=getattr(solver, "_source_time", None),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def _execute_fdtd_solve(self, solver, scene=None):
|
|
513
|
+
resolved_scene = self.scene if scene is None else scene
|
|
514
|
+
time_steps = self._resolve_fdtd_time_steps(solver, resolved_scene)
|
|
515
|
+
dft_cfg = self.config.spectral_sampler
|
|
516
|
+
requested_full_field_dft = self.config.full_field_dft
|
|
517
|
+
use_full_field_dft = requested_full_field_dft or len(self.frequencies) > 1
|
|
518
|
+
dft_request = self.frequency if len(self.frequencies) == 1 else self.frequencies
|
|
519
|
+
raw_output = solver.solve(
|
|
520
|
+
time_steps=time_steps,
|
|
521
|
+
dft_frequency=dft_request,
|
|
522
|
+
enable_plot=self.config.enable_plot,
|
|
523
|
+
dft_window=dft_cfg.window,
|
|
524
|
+
full_field_dft=use_full_field_dft,
|
|
525
|
+
normalize_source=dft_cfg.normalize_source,
|
|
526
|
+
shutoff=self.config.shutoff,
|
|
527
|
+
shutoff_check_interval=self.config.shutoff_check_interval,
|
|
528
|
+
use_cuda_graph=self.config.cuda_graph,
|
|
529
|
+
)
|
|
530
|
+
return raw_output, time_steps, use_full_field_dft, dft_cfg
|
|
531
|
+
|
|
532
|
+
def _fdtd_last_step_field_payload(self, solver) -> dict[str, torch.Tensor]:
|
|
533
|
+
def _field(name: str):
|
|
534
|
+
value = getattr(solver, name, None)
|
|
535
|
+
if value is not None:
|
|
536
|
+
return value
|
|
537
|
+
return getattr(solver, name.lower())
|
|
538
|
+
|
|
539
|
+
if getattr(solver, "complex_fields_enabled", False) and hasattr(solver, "Ex_imag"):
|
|
540
|
+
return {
|
|
541
|
+
"Ex": torch.complex(_field("Ex"), _field("Ex_imag")),
|
|
542
|
+
"Ey": torch.complex(_field("Ey"), _field("Ey_imag")),
|
|
543
|
+
"Ez": torch.complex(_field("Ez"), _field("Ez_imag")),
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
"Ex": _field("Ex"),
|
|
547
|
+
"Ey": _field("Ey"),
|
|
548
|
+
"Ez": _field("Ez"),
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
def _run_fdtd_from_solver(self, solver) -> Result:
|
|
552
|
+
raw_output, time_steps, use_full_field_dft, dft_cfg = self._execute_fdtd_solve(solver, self.scene)
|
|
553
|
+
if raw_output is None:
|
|
554
|
+
raise RuntimeError("FDTD solve did not return any output.")
|
|
555
|
+
|
|
556
|
+
monitors = raw_output.get("observers", {}) if isinstance(raw_output, dict) else {}
|
|
557
|
+
field_payload = {
|
|
558
|
+
key: value
|
|
559
|
+
for key, value in (raw_output.items() if isinstance(raw_output, dict) else [])
|
|
560
|
+
if key in {"Ex", "Ey", "Ez"}
|
|
561
|
+
}
|
|
562
|
+
if not field_payload and len(self.frequencies) == 1:
|
|
563
|
+
field_payload = self._fdtd_last_step_field_payload(solver)
|
|
564
|
+
|
|
565
|
+
fields = _to_tensor_fields(field_payload, self.scene.device)
|
|
566
|
+
solver_stats = self._build_fdtd_solver_stats(
|
|
567
|
+
solver,
|
|
568
|
+
time_steps=time_steps,
|
|
569
|
+
use_full_field_dft=use_full_field_dft,
|
|
570
|
+
dft_cfg=dft_cfg,
|
|
571
|
+
)
|
|
572
|
+
return Result(
|
|
573
|
+
method="fdtd",
|
|
574
|
+
scene=self.scene,
|
|
575
|
+
prepared_scene=solver.scene,
|
|
576
|
+
frequency=self.frequency,
|
|
577
|
+
frequencies=self.frequencies,
|
|
578
|
+
solver=solver,
|
|
579
|
+
fields=fields,
|
|
580
|
+
monitors=monitors,
|
|
581
|
+
metadata=self.metadata,
|
|
582
|
+
solver_stats=solver_stats,
|
|
583
|
+
raw_output=raw_output,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
def _build_fdtd_solver_stats(
|
|
587
|
+
self,
|
|
588
|
+
solver,
|
|
589
|
+
*,
|
|
590
|
+
time_steps: int,
|
|
591
|
+
use_full_field_dft: bool,
|
|
592
|
+
dft_cfg: SpectralSampler,
|
|
593
|
+
) -> dict[str, Any]:
|
|
594
|
+
elapsed_s = getattr(solver, "last_solve_elapsed_s", None)
|
|
595
|
+
dft_sample_counts = tuple(getattr(solver, "dft_sample_counts", (getattr(solver, "dft_sample_count", 0),)))
|
|
596
|
+
shutoff_triggered = bool(getattr(solver, "_shutoff_triggered", False))
|
|
597
|
+
shutoff_step = getattr(solver, "_shutoff_step", None)
|
|
598
|
+
return {
|
|
599
|
+
"time_steps": time_steps,
|
|
600
|
+
"shutoff": self.config.shutoff,
|
|
601
|
+
"shutoff_check_interval": self.config.shutoff_check_interval,
|
|
602
|
+
"shutoff_triggered": shutoff_triggered,
|
|
603
|
+
"shutoff_step": shutoff_step,
|
|
604
|
+
"steps_run": (shutoff_step + 1) if shutoff_triggered else time_steps,
|
|
605
|
+
"dt": solver.dt,
|
|
606
|
+
"cuda_graph_active": bool(getattr(solver, "_cuda_graph_active", False)),
|
|
607
|
+
"tail_graph_active": bool(getattr(solver, "_tail_graph_active", False)),
|
|
608
|
+
"boundary": getattr(solver, "boundary_kind", self.scene.boundary.kind),
|
|
609
|
+
"absorber": getattr(solver, "active_absorber_type", self.config.absorber),
|
|
610
|
+
"cpml_memory_mode": getattr(solver, "_cpml_memory_mode", None),
|
|
611
|
+
"cpml_requested_memory_mode": getattr(solver, "_cpml_memory_mode_requested", None),
|
|
612
|
+
"cpml_allocated_memory_bytes": getattr(solver, "_cpml_allocated_memory_bytes", None),
|
|
613
|
+
"cpml_dense_memory_bytes": getattr(solver, "_cpml_dense_memory_bytes", None),
|
|
614
|
+
"cpml_slab_memory_bytes": getattr(solver, "_cpml_slab_memory_bytes", None),
|
|
615
|
+
"dft_window": dft_cfg.window,
|
|
616
|
+
"frequency": self.frequency,
|
|
617
|
+
"frequencies": self.frequencies,
|
|
618
|
+
"num_frequencies": len(self.frequencies),
|
|
619
|
+
"dft_samples": dft_sample_counts[0] if dft_sample_counts else 0,
|
|
620
|
+
"dft_sample_counts": dft_sample_counts,
|
|
621
|
+
"full_field_dft": use_full_field_dft,
|
|
622
|
+
"elapsed_s": elapsed_s,
|
|
623
|
+
"ms_per_step": (
|
|
624
|
+
elapsed_s * 1e3 / time_steps
|
|
625
|
+
if elapsed_s is not None and time_steps > 0
|
|
626
|
+
else None
|
|
627
|
+
),
|
|
628
|
+
"steps_per_second": (
|
|
629
|
+
time_steps / elapsed_s
|
|
630
|
+
if elapsed_s is not None and elapsed_s > 0.0
|
|
631
|
+
else None
|
|
632
|
+
),
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
def _run_fdtd_with_gradient_bridge(self) -> Result:
|
|
636
|
+
from .fdtd.adjoint import run_fdtd_with_gradient_bridge
|
|
637
|
+
|
|
638
|
+
return run_fdtd_with_gradient_bridge(self)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class PreparedSimulation:
|
|
642
|
+
def __init__(self, simulation: Simulation, solver):
|
|
643
|
+
self.simulation = simulation
|
|
644
|
+
self.solver = solver
|
|
645
|
+
|
|
646
|
+
def run(self) -> Result:
|
|
647
|
+
if self.simulation.method == SimulationMethod.FDFD:
|
|
648
|
+
solver_cfg = self.simulation.config.solver
|
|
649
|
+
return self.simulation._build_fdfd_result(self.solver, solver_cfg)
|
|
650
|
+
if self.simulation.has_trainable_parameters:
|
|
651
|
+
return self.simulation._run_fdtd()
|
|
652
|
+
return self.simulation._run_fdtd_from_solver(self.solver)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def run(simulation: Simulation) -> Result:
|
|
657
|
+
if not isinstance(simulation, Simulation):
|
|
658
|
+
raise TypeError("run() expects a maxwell.Simulation instance.")
|
|
659
|
+
return simulation.run()
|