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