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,209 @@
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+
5
+ from .common import BOUNDARY_KIND_TO_CODE, BOUNDARY_PEC, initialize_complex_fields
6
+ from .bloch import resolve_bloch_phase_factors, resolve_bloch_wavevector
7
+ from .cpml import (
8
+ initialize_absorber_state,
9
+ initialize_cpml_state,
10
+ initialize_neutral_boundary_state,
11
+ initialize_simple_pml_state,
12
+ initialize_stable_pml_state,
13
+ )
14
+
15
+
16
+ def _configure_face_boundary_codes(solver):
17
+ symmetry = getattr(solver.scene, "symmetry", (None, None, None))
18
+ boundary = solver.scene.boundary
19
+ face_codes = {
20
+ axis: [
21
+ BOUNDARY_KIND_TO_CODE[boundary.face_kind(axis, "low")],
22
+ BOUNDARY_KIND_TO_CODE[boundary.face_kind(axis, "high")],
23
+ ]
24
+ for axis in ("x", "y", "z")
25
+ }
26
+ for axis, entry in zip(("x", "y", "z"), symmetry):
27
+ if entry is None:
28
+ continue
29
+ mode, face = entry
30
+ if boundary.face_kind(axis, face) not in {"none", "pml"}:
31
+ raise ValueError(
32
+ f"Scene.symmetry on the {axis}-{face} face requires "
33
+ "BoundarySpec.none() or BoundarySpec.pml(...)."
34
+ )
35
+ face_index = 0 if face == "low" else 1
36
+ face_codes[axis][face_index] = BOUNDARY_KIND_TO_CODE[mode.lower()]
37
+
38
+ _validate_symmetry_source_placement(solver.scene, symmetry)
39
+
40
+ for axis in ("x", "y", "z"):
41
+ low_code, high_code = (int(face_codes[axis][0]), int(face_codes[axis][1]))
42
+ setattr(solver, f"boundary_{axis}_low_code", low_code)
43
+ setattr(solver, f"boundary_{axis}_high_code", high_code)
44
+
45
+ solver.has_pec_faces = any(
46
+ code == BOUNDARY_PEC
47
+ for codes in face_codes.values()
48
+ for code in codes
49
+ )
50
+ solver.has_pml_faces = any(
51
+ code == BOUNDARY_KIND_TO_CODE["pml"]
52
+ for codes in face_codes.values()
53
+ for code in codes
54
+ )
55
+ solver.has_periodic_axes = tuple(
56
+ axis
57
+ for axis in ("x", "y", "z")
58
+ if boundary.axis_kind(axis) == "periodic"
59
+ )
60
+ solver.has_bloch_axes = tuple(
61
+ axis
62
+ for axis in ("x", "y", "z")
63
+ if boundary.axis_kind(axis) == "bloch"
64
+ )
65
+ solver.mur_faces = tuple(
66
+ (axis, side)
67
+ for axis in ("x", "y", "z")
68
+ for side in ("low", "high")
69
+ if boundary.face_kind(axis, side) == "mur"
70
+ )
71
+ solver.has_mur_faces = bool(solver.mur_faces)
72
+
73
+
74
+ _AXIS_INDEX = {"x": 0, "y": 1, "z": 2}
75
+
76
+
77
+ def _source_center(source):
78
+ """Return a point-like source center coordinate tuple, or ``None`` if the
79
+ source spans the domain (e.g. plane waves) and has no single position."""
80
+ for attr in ("position", "center"):
81
+ value = getattr(source, attr, None)
82
+ if value is not None:
83
+ return tuple(float(v) for v in value)
84
+ return None
85
+
86
+
87
+ def _validate_symmetry_source_placement(scene, symmetry):
88
+ """Reject point-like sources located in the folded-away half of the domain.
89
+
90
+ A source may sit on the symmetry plane or anywhere in the kept half; a source
91
+ beyond the plane lives in the removed half and cannot be represented on the
92
+ folded grid, so we raise clearly instead of silently dropping it.
93
+ """
94
+ if all(entry is None for entry in symmetry):
95
+ return
96
+ resolver = getattr(scene, "resolved_sources", None)
97
+ sources = resolver() if callable(resolver) else getattr(scene, "sources", ())
98
+ bounds = scene.domain.bounds
99
+ tol = 1e-9
100
+ for source in sources:
101
+ center = _source_center(source)
102
+ if center is None:
103
+ continue
104
+ for axis, entry in zip(("x", "y", "z"), symmetry):
105
+ if entry is None:
106
+ continue
107
+ _mode, face = entry
108
+ axis_idx = _AXIS_INDEX[axis]
109
+ low, high = float(bounds[axis_idx][0]), float(bounds[axis_idx][1])
110
+ coord = center[axis_idx]
111
+ span = max(high - low, tol)
112
+ plane = low if face == "low" else high
113
+ if face == "low" and coord < plane - tol * span:
114
+ raise ValueError(
115
+ f"Source at {axis}={coord:g} lies in the folded-away half below the "
116
+ f"{axis}-low symmetry plane at {plane:g}; place it on the plane or in "
117
+ "the kept half of the domain."
118
+ )
119
+ if face == "high" and coord > plane + tol * span:
120
+ raise ValueError(
121
+ f"Source at {axis}={coord:g} lies in the folded-away half above the "
122
+ f"{axis}-high symmetry plane at {plane:g}; place it on the plane or in "
123
+ "the kept half of the domain."
124
+ )
125
+
126
+
127
+ def initialize_boundary_state(solver):
128
+ solver.boundary_kind = solver.scene.boundary.kind
129
+ solver.boundary_code = (
130
+ BOUNDARY_KIND_TO_CODE[solver.boundary_kind]
131
+ if solver.boundary_kind in BOUNDARY_KIND_TO_CODE
132
+ else None
133
+ )
134
+ _configure_face_boundary_codes(solver)
135
+
136
+ solver.complex_fields_enabled = solver.scene.boundary.uses_kind("bloch")
137
+ if solver.complex_fields_enabled:
138
+ initialize_complex_fields(solver)
139
+
140
+ solver.resolved_bloch_wavevector = resolve_bloch_wavevector(solver)
141
+ phases = resolve_bloch_phase_factors(solver)
142
+ solver.boundary_phase_cos = tuple(float(phase.real) for phase in phases)
143
+ solver.boundary_phase_sin = tuple(float(phase.imag) for phase in phases)
144
+ solver.active_absorber_type = solver.absorber_type if solver.scene.boundary.uses_kind("pml") else "none"
145
+ solver.uses_cpml = solver.active_absorber_type in ("cpml", "stablepml")
146
+
147
+ if solver.scene.boundary.uses_kind("pml"):
148
+ if solver.absorber_type == "cpml":
149
+ initialize_cpml_state(solver)
150
+ elif solver.absorber_type == "stablepml":
151
+ initialize_stable_pml_state(solver)
152
+ elif solver.absorber_type == "pml":
153
+ initialize_simple_pml_state(solver)
154
+ elif solver.absorber_type == "absorber":
155
+ initialize_absorber_state(solver)
156
+ else:
157
+ raise ValueError(
158
+ "PML boundaries require absorber_type to be 'cpml', 'stablepml', 'pml', or 'absorber'."
159
+ )
160
+ else:
161
+ initialize_neutral_boundary_state(solver)
162
+
163
+ initialize_mur_state(solver)
164
+
165
+
166
+ # Tangential electric-field components on each outer face and the Yee grid axis
167
+ # they are indexed along for the normal direction of that face.
168
+ _MUR_FACE_COMPONENTS = {
169
+ "x": (("Ey", 0), ("Ez", 0)),
170
+ "y": (("Ex", 1), ("Ez", 1)),
171
+ "z": (("Ex", 2), ("Ey", 2)),
172
+ }
173
+
174
+
175
+ def initialize_mur_state(solver):
176
+ solver._mur_state = []
177
+ if not getattr(solver, "has_mur_faces", False):
178
+ return
179
+
180
+ scene = solver.scene
181
+ axis_primal = {"x": scene.dx_primal64, "y": scene.dy_primal64, "z": scene.dz_primal64}
182
+ speed_dt = solver.c * solver.dt
183
+ for axis, side in solver.mur_faces:
184
+ # Mur advection uses the local boundary-cell spacing of each face. The
185
+ # coefficient depends only on c, dt, and that spacing, so it is constant
186
+ # across time steps and precomputed once here.
187
+ primal = axis_primal[axis]
188
+ delta = float(primal[0] if side == "low" else primal[-1])
189
+ coef = (speed_dt - delta) / (speed_dt + delta)
190
+ for field_name, field_axis in _MUR_FACE_COMPONENTS[axis]:
191
+ field = getattr(solver, field_name)
192
+ size = int(field.shape[field_axis])
193
+ boundary_index = 0 if side == "low" else size - 1
194
+ adjacent_index = 1 if side == "low" else size - 2
195
+ # Persistent boundary / first-interior plane buffers, updated in place
196
+ # by the Mur kernel each step (no per-step allocation). They start at
197
+ # zero, matching the initial field state.
198
+ plane_shape = tuple(field.shape[:field_axis] + field.shape[field_axis + 1:])
199
+ solver._mur_state.append(
200
+ {
201
+ "field": field_name,
202
+ "axis": int(field_axis),
203
+ "boundary_index": int(boundary_index),
204
+ "adjacent_index": int(adjacent_index),
205
+ "coef": float(coef),
206
+ "prev_boundary": torch.zeros(plane_shape, device=solver.device, dtype=field.dtype),
207
+ "prev_adjacent": torch.zeros(plane_shape, device=solver.device, dtype=field.dtype),
208
+ }
209
+ )
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import torch
6
+
7
+ from .boundary import expand_cpml_memory_tensor
8
+
9
+ _FIELD_STATE_NAMES = ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz")
10
+ _COMPLEX_FIELD_STATE_NAMES = ("Ex_imag", "Ey_imag", "Ez_imag", "Hx_imag", "Hy_imag", "Hz_imag")
11
+ _CPML_STATE_NAMES = (
12
+ "psi_ex_y",
13
+ "psi_ex_z",
14
+ "psi_ey_x",
15
+ "psi_ey_z",
16
+ "psi_ez_x",
17
+ "psi_ez_y",
18
+ "psi_hx_y",
19
+ "psi_hx_z",
20
+ "psi_hy_x",
21
+ "psi_hy_z",
22
+ "psi_hz_x",
23
+ "psi_hz_y",
24
+ )
25
+ _CPML_COMPLEX_STATE_NAMES = tuple(f"{name}_imag" for name in _CPML_STATE_NAMES)
26
+ _TFSF_AUXILIARY_STATE_NAMES = (
27
+ "tfsf_aux_electric",
28
+ "tfsf_aux_magnetic",
29
+ )
30
+ _DISPERSIVE_STATE_TENSORS = {
31
+ "debye": ("polarization", "current"),
32
+ "drude": ("current",),
33
+ "lorentz": ("polarization", "current"),
34
+ }
35
+ _CHECKPOINT_SCHEMA_VERSION = 1
36
+
37
+
38
+ def dispersive_state_name(component_name: str, model_name: str, index: int, tensor_name: str) -> str:
39
+ return f"{model_name}_{component_name}_{int(index)}_{tensor_name}"
40
+
41
+
42
+ def iter_dispersive_state_specs(solver):
43
+ if not getattr(solver, "dispersive_enabled", False):
44
+ return
45
+ templates = getattr(solver, "_dispersive_templates", {})
46
+ for component_name in ("Ex", "Ey", "Ez"):
47
+ component_templates = templates.get(component_name, {})
48
+ for model_name in ("debye", "drude", "lorentz"):
49
+ tensor_names = _DISPERSIVE_STATE_TENSORS[model_name]
50
+ for index, entry in enumerate(component_templates.get(model_name, ())):
51
+ yield component_name, model_name, index, tensor_names, entry
52
+
53
+
54
+ def iter_magnetic_dispersive_state_specs(solver):
55
+ if not getattr(solver, "magnetic_dispersive_enabled", False):
56
+ return
57
+ templates = getattr(solver, "_magnetic_dispersive_templates", {})
58
+ for component_name in ("Hx", "Hy", "Hz"):
59
+ component_templates = templates.get(component_name, {})
60
+ for model_name in ("debye", "drude", "lorentz"):
61
+ tensor_names = _DISPERSIVE_STATE_TENSORS[model_name]
62
+ for index, entry in enumerate(component_templates.get(model_name, ())):
63
+ yield component_name, model_name, index, tensor_names, entry
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class FDTDCheckpointSchema:
68
+ version: int
69
+ field_names: tuple[str, ...]
70
+ complex_field_names: tuple[str, ...]
71
+ cpml_state_names: tuple[str, ...]
72
+ tfsf_auxiliary_state_names: tuple[str, ...]
73
+ dispersive_state_names: tuple[str, ...]
74
+ magnetic_dispersive_state_names: tuple[str, ...] = ()
75
+
76
+ @property
77
+ def state_names(self) -> tuple[str, ...]:
78
+ return (
79
+ self.field_names
80
+ + self.complex_field_names
81
+ + self.cpml_state_names
82
+ + self.tfsf_auxiliary_state_names
83
+ + self.dispersive_state_names
84
+ + self.magnetic_dispersive_state_names
85
+ )
86
+
87
+
88
+ def checkpoint_schema(solver) -> FDTDCheckpointSchema:
89
+ complex_field_names = _COMPLEX_FIELD_STATE_NAMES if bool(getattr(solver, "complex_fields_enabled", False)) else ()
90
+ cpml_state_names = ()
91
+ if getattr(solver, "uses_cpml", False):
92
+ cpml_state_names = _CPML_STATE_NAMES
93
+ if bool(getattr(solver, "complex_fields_enabled", False)):
94
+ cpml_state_names = cpml_state_names + _CPML_COMPLEX_STATE_NAMES
95
+
96
+ tfsf_auxiliary_state_names = ()
97
+ if getattr(solver, "tfsf_enabled", False):
98
+ tfsf_state = getattr(solver, "_tfsf_state", None)
99
+ if tfsf_state is not None and tfsf_state.get("auxiliary_grid") is not None:
100
+ tfsf_auxiliary_state_names = _TFSF_AUXILIARY_STATE_NAMES
101
+
102
+ dispersive_state_names = []
103
+ for component_name, model_name, index, tensor_names, _entry in iter_dispersive_state_specs(solver) or ():
104
+ for tensor_name in tensor_names:
105
+ dispersive_state_names.append(dispersive_state_name(component_name, model_name, index, tensor_name))
106
+ if bool(getattr(solver, "complex_fields_enabled", False)):
107
+ # Bloch (complex-field) runs keep an imaginary ADE replica per electric
108
+ # pole; the reverse replay differentiates through it, so it must be part
109
+ # of the frozen checkpoint layout too.
110
+ for component_name, model_name, index, tensor_names, _entry in iter_dispersive_state_specs(solver) or ():
111
+ for tensor_name in tensor_names:
112
+ dispersive_state_names.append(
113
+ dispersive_state_name(component_name, model_name, index, tensor_name) + "_imag"
114
+ )
115
+
116
+ magnetic_dispersive_state_names = []
117
+ for component_name, model_name, index, tensor_names, _entry in iter_magnetic_dispersive_state_specs(solver) or ():
118
+ for tensor_name in tensor_names:
119
+ magnetic_dispersive_state_names.append(
120
+ dispersive_state_name(component_name, model_name, index, tensor_name)
121
+ )
122
+ if bool(getattr(solver, "complex_fields_enabled", False)):
123
+ magnetic_dispersive_state_names.extend(
124
+ name + "_imag" for name in tuple(magnetic_dispersive_state_names)
125
+ )
126
+
127
+ return FDTDCheckpointSchema(
128
+ version=_CHECKPOINT_SCHEMA_VERSION,
129
+ field_names=_FIELD_STATE_NAMES,
130
+ complex_field_names=tuple(complex_field_names),
131
+ cpml_state_names=tuple(cpml_state_names),
132
+ tfsf_auxiliary_state_names=tuple(tfsf_auxiliary_state_names),
133
+ dispersive_state_names=tuple(dispersive_state_names),
134
+ magnetic_dispersive_state_names=tuple(magnetic_dispersive_state_names),
135
+ )
136
+
137
+
138
+ def checkpoint_state_names(solver) -> tuple[str, ...]:
139
+ return checkpoint_schema(solver).state_names
140
+
141
+
142
+ @dataclass(frozen=True)
143
+ class FDTDCheckpointState:
144
+ step: int
145
+ schema: FDTDCheckpointSchema
146
+ tensors: dict[str, torch.Tensor]
147
+
148
+
149
+ def validate_checkpoint_state(
150
+ state: FDTDCheckpointState,
151
+ *,
152
+ expected_schema: FDTDCheckpointSchema | None = None,
153
+ ) -> FDTDCheckpointSchema:
154
+ state_names = tuple(state.tensors.keys())
155
+ if state_names != state.schema.state_names:
156
+ raise RuntimeError(
157
+ "Checkpoint tensor layout drifted from the frozen adjoint state schema. "
158
+ f"Expected {state.schema.state_names}, got {state_names}."
159
+ )
160
+ if expected_schema is not None and state.schema != expected_schema:
161
+ raise RuntimeError(
162
+ "Checkpoint schema mismatch detected during adjoint replay. "
163
+ f"Expected version {expected_schema.version} with names {expected_schema.state_names}, "
164
+ f"got version {state.schema.version} with names {state.schema.state_names}."
165
+ )
166
+ return state.schema
167
+
168
+
169
+ def capture_checkpoint_state(solver, step: int) -> FDTDCheckpointState:
170
+ schema = checkpoint_schema(solver)
171
+ tensors = {
172
+ name: getattr(solver, name).detach().clone()
173
+ for name in _FIELD_STATE_NAMES
174
+ }
175
+ if bool(getattr(solver, "complex_fields_enabled", False)):
176
+ tensors.update(
177
+ {
178
+ name: getattr(solver, name).detach().clone()
179
+ for name in _COMPLEX_FIELD_STATE_NAMES
180
+ }
181
+ )
182
+ if getattr(solver, "uses_cpml", False):
183
+ tensors.update(
184
+ {
185
+ name: expand_cpml_memory_tensor(solver, name).detach().clone()
186
+ for name in schema.cpml_state_names
187
+ }
188
+ )
189
+ if getattr(solver, "tfsf_enabled", False):
190
+ tfsf_state = getattr(solver, "_tfsf_state", None)
191
+ auxiliary_grid = None if tfsf_state is None else tfsf_state.get("auxiliary_grid")
192
+ if auxiliary_grid is not None:
193
+ tensors["tfsf_aux_electric"] = auxiliary_grid.electric.detach().clone()
194
+ tensors["tfsf_aux_magnetic"] = auxiliary_grid.magnetic.detach().clone()
195
+ for component_name, model_name, index, tensor_names, entry in iter_dispersive_state_specs(solver) or ():
196
+ for tensor_name in tensor_names:
197
+ tensors[dispersive_state_name(component_name, model_name, index, tensor_name)] = (
198
+ entry[tensor_name].detach().clone()
199
+ )
200
+ if bool(getattr(solver, "complex_fields_enabled", False)):
201
+ for component_name, model_name, index, tensor_names, entry in iter_dispersive_state_specs(solver) or ():
202
+ for tensor_name in tensor_names:
203
+ tensors[dispersive_state_name(component_name, model_name, index, tensor_name) + "_imag"] = (
204
+ entry[f"{tensor_name}_imag"].detach().clone()
205
+ )
206
+ for component_name, model_name, index, tensor_names, entry in iter_magnetic_dispersive_state_specs(solver) or ():
207
+ for tensor_name in tensor_names:
208
+ tensors[dispersive_state_name(component_name, model_name, index, tensor_name)] = (
209
+ entry[tensor_name].detach().clone()
210
+ )
211
+ if bool(getattr(solver, "complex_fields_enabled", False)):
212
+ for component_name, model_name, index, tensor_names, entry in iter_magnetic_dispersive_state_specs(solver) or ():
213
+ for tensor_name in tensor_names:
214
+ tensors[dispersive_state_name(component_name, model_name, index, tensor_name) + "_imag"] = (
215
+ entry[f"{tensor_name}_imag"].detach().clone()
216
+ )
217
+ state = FDTDCheckpointState(step=int(step), schema=schema, tensors=tensors)
218
+ validate_checkpoint_state(state)
219
+ return state
220
+
221
+
222
+ def clone_checkpoint_tensors(state: FDTDCheckpointState) -> dict[str, torch.Tensor]:
223
+ validate_checkpoint_state(state)
224
+ return {
225
+ name: tensor.detach().clone()
226
+ for name, tensor in state.tensors.items()
227
+ }
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ from ..monitors import normalize_component
6
+ from ..scene import prepare_scene
7
+
8
+
9
+ def component_coords(scene, component: str) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
10
+ resolved_scene = prepare_scene(scene)
11
+ component_name = normalize_component(component)
12
+ x, y, z = resolved_scene.x_nodes64, resolved_scene.y_nodes64, resolved_scene.z_nodes64
13
+ x_half, y_half, z_half = resolved_scene.x_half64, resolved_scene.y_half64, resolved_scene.z_half64
14
+
15
+ if component_name == "ex":
16
+ return x_half, y, z
17
+ if component_name == "ey":
18
+ return x, y_half, z
19
+ if component_name == "ez":
20
+ return x, y, z_half
21
+ if component_name == "hx":
22
+ return x, y_half, z_half
23
+ if component_name == "hy":
24
+ return x_half, y, z_half
25
+ if component_name == "hz":
26
+ return x_half, y_half, z
27
+ raise ValueError(f"Unsupported field component: {component!r}")
28
+
29
+
30
+ def centered_cell_coords(scene) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
31
+ resolved_scene = prepare_scene(scene)
32
+ return resolved_scene.x_half64, resolved_scene.y_half64, resolved_scene.z_half64
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from .backend import build_info, debug_linear_indices, get_compiled_extension, get_native_fdtd_module, is_available
4
+
5
+ __all__ = [
6
+ "build_info",
7
+ "debug_linear_indices",
8
+ "get_compiled_extension",
9
+ "get_native_fdtd_module",
10
+ "is_available",
11
+ ]