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,743 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from .sources import (
|
|
7
|
+
_normalize_bend,
|
|
8
|
+
_normalize_mode_direction,
|
|
9
|
+
_require_length3,
|
|
10
|
+
_require_mode_source_size,
|
|
11
|
+
_resolve_mode_source_normal_axis,
|
|
12
|
+
_resolve_mode_source_polarization_axis,
|
|
13
|
+
polarization_vector,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_AXES = ("x", "y", "z")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_fields(fields):
|
|
20
|
+
if isinstance(fields, str):
|
|
21
|
+
return (fields,)
|
|
22
|
+
if not fields:
|
|
23
|
+
raise ValueError("fields must not be empty.")
|
|
24
|
+
return tuple(str(field) for field in fields)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def normalize_component(component):
|
|
28
|
+
component_name = str(component).lower()
|
|
29
|
+
if component_name not in {"ex", "ey", "ez", "hx", "hy", "hz"}:
|
|
30
|
+
raise ValueError("field component must be one of: Ex, Ey, Ez, Hx, Hy, Hz.")
|
|
31
|
+
return component_name
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normalize_frequencies(frequencies):
|
|
35
|
+
if frequencies is None:
|
|
36
|
+
return None
|
|
37
|
+
if isinstance(frequencies, Iterable) and not isinstance(frequencies, (str, bytes)):
|
|
38
|
+
values = tuple(float(freq) for freq in frequencies)
|
|
39
|
+
else:
|
|
40
|
+
values = (float(frequencies),)
|
|
41
|
+
if not values:
|
|
42
|
+
raise ValueError("frequencies must not be empty.")
|
|
43
|
+
return values
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalize_axis(axis):
|
|
47
|
+
axis_name = str(axis).lower()
|
|
48
|
+
if axis_name not in {"x", "y", "z"}:
|
|
49
|
+
raise ValueError("axis must be 'x', 'y', or 'z'.")
|
|
50
|
+
return axis_name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _normalize_normal_direction(normal_direction):
|
|
54
|
+
direction = str(normal_direction)
|
|
55
|
+
if direction not in {"+", "-"}:
|
|
56
|
+
raise ValueError("normal_direction must be '+' or '-'.")
|
|
57
|
+
return direction
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def required_flux_fields(axis):
|
|
61
|
+
axis_name = normalize_axis(axis)
|
|
62
|
+
if axis_name == "x":
|
|
63
|
+
return ("Ey", "Ez", "Hy", "Hz")
|
|
64
|
+
if axis_name == "y":
|
|
65
|
+
return ("Ex", "Ez", "Hx", "Hz")
|
|
66
|
+
return ("Ex", "Ey", "Hx", "Hy")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _validate_flux_fields(axis: str, fields: tuple[str, ...], *, monitor_name: str) -> None:
|
|
70
|
+
required_fields = required_flux_fields(axis)
|
|
71
|
+
missing_fields = tuple(field for field in required_fields if field not in fields)
|
|
72
|
+
if missing_fields:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"{monitor_name} compute_flux=True requires tangential fields {required_fields}, "
|
|
75
|
+
f"missing {missing_fields}."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _require_nonnegative_length3(name: str, values) -> tuple[float, float, float]:
|
|
80
|
+
resolved = _require_length3(name, values)
|
|
81
|
+
if any(value < 0.0 for value in resolved):
|
|
82
|
+
raise ValueError(f"{name} must contain non-negative lengths.")
|
|
83
|
+
return resolved
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _resolve_finite_plane_geometry(
|
|
87
|
+
position,
|
|
88
|
+
size,
|
|
89
|
+
) -> tuple[tuple[float, float, float], tuple[float, float, float], str, float, dict[str, tuple[float, float]]]:
|
|
90
|
+
resolved_position = _require_length3("position", position)
|
|
91
|
+
resolved_size = _require_nonnegative_length3("size", size)
|
|
92
|
+
zero_axes = [index for index, length in enumerate(resolved_size) if abs(length) <= 1e-12]
|
|
93
|
+
if len(zero_axes) != 1:
|
|
94
|
+
raise ValueError("FinitePlaneMonitor size must have exactly one zero-thickness axis.")
|
|
95
|
+
|
|
96
|
+
axis_index = zero_axes[0]
|
|
97
|
+
axis = _AXES[axis_index]
|
|
98
|
+
plane_position = float(resolved_position[axis_index])
|
|
99
|
+
tangential_bounds: dict[str, tuple[float, float]] = {}
|
|
100
|
+
for index, axis_name in enumerate(_AXES):
|
|
101
|
+
if index == axis_index:
|
|
102
|
+
continue
|
|
103
|
+
half_extent = 0.5 * float(resolved_size[index])
|
|
104
|
+
tangential_bounds[axis_name] = (
|
|
105
|
+
float(resolved_position[index] - half_extent),
|
|
106
|
+
float(resolved_position[index] + half_extent),
|
|
107
|
+
)
|
|
108
|
+
return resolved_position, resolved_size, axis, plane_position, tangential_bounds
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _face_area(face: "FinitePlaneMonitor") -> float:
|
|
112
|
+
area = 1.0
|
|
113
|
+
axis_index = _AXES.index(face.axis)
|
|
114
|
+
for index, extent in enumerate(face.size):
|
|
115
|
+
if index == axis_index:
|
|
116
|
+
continue
|
|
117
|
+
area *= float(extent)
|
|
118
|
+
return area
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _validate_closed_surface_area_balance(faces: tuple["FinitePlaneMonitor", ...]) -> None:
|
|
122
|
+
for axis in _AXES:
|
|
123
|
+
positive_area = 0.0
|
|
124
|
+
negative_area = 0.0
|
|
125
|
+
for face in faces:
|
|
126
|
+
if face.axis != axis:
|
|
127
|
+
continue
|
|
128
|
+
if face.normal_direction == "+":
|
|
129
|
+
positive_area += _face_area(face)
|
|
130
|
+
else:
|
|
131
|
+
negative_area += _face_area(face)
|
|
132
|
+
if positive_area <= 0.0 or negative_area <= 0.0:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"ClosedSurfaceMonitor requires outward faces on both +/-{axis} directions."
|
|
135
|
+
)
|
|
136
|
+
if abs(positive_area - negative_area) > 1e-9 * max(positive_area, negative_area, 1.0):
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"ClosedSurfaceMonitor projected area is unbalanced on {axis}-normal faces: "
|
|
139
|
+
f"+={positive_area}, -={negative_area}."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class PointMonitor:
|
|
145
|
+
name: str
|
|
146
|
+
position: tuple[float, float, float]
|
|
147
|
+
fields: tuple[str, ...]
|
|
148
|
+
kind: str = "point"
|
|
149
|
+
|
|
150
|
+
def __init__(self, name, position, fields=("Ez",)):
|
|
151
|
+
if len(position) != 3:
|
|
152
|
+
raise ValueError("position must contain exactly three values.")
|
|
153
|
+
object.__setattr__(self, "name", str(name))
|
|
154
|
+
object.__setattr__(self, "position", tuple(float(v) for v in position))
|
|
155
|
+
object.__setattr__(self, "fields", _normalize_fields(fields))
|
|
156
|
+
object.__setattr__(self, "kind", "point")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass(frozen=True)
|
|
160
|
+
class PlaneMonitor:
|
|
161
|
+
name: str
|
|
162
|
+
axis: str
|
|
163
|
+
position: float
|
|
164
|
+
fields: tuple[str, ...]
|
|
165
|
+
frequencies: tuple[float, ...] | None = None
|
|
166
|
+
compute_flux: bool = False
|
|
167
|
+
normal_direction: str = "+"
|
|
168
|
+
kind: str = "plane"
|
|
169
|
+
|
|
170
|
+
def __init__(
|
|
171
|
+
self,
|
|
172
|
+
name,
|
|
173
|
+
axis="z",
|
|
174
|
+
position=0.0,
|
|
175
|
+
fields=("Ez",),
|
|
176
|
+
frequencies=None,
|
|
177
|
+
compute_flux=False,
|
|
178
|
+
normal_direction="+",
|
|
179
|
+
):
|
|
180
|
+
axis_name = normalize_axis(axis)
|
|
181
|
+
normalized_fields = _normalize_fields(fields)
|
|
182
|
+
flux_enabled = bool(compute_flux)
|
|
183
|
+
if flux_enabled:
|
|
184
|
+
_validate_flux_fields(axis_name, normalized_fields, monitor_name="PlaneMonitor")
|
|
185
|
+
object.__setattr__(self, "name", str(name))
|
|
186
|
+
object.__setattr__(self, "axis", axis_name)
|
|
187
|
+
object.__setattr__(self, "position", float(position))
|
|
188
|
+
object.__setattr__(self, "fields", normalized_fields)
|
|
189
|
+
object.__setattr__(self, "frequencies", _normalize_frequencies(frequencies))
|
|
190
|
+
object.__setattr__(self, "compute_flux", flux_enabled)
|
|
191
|
+
object.__setattr__(self, "normal_direction", _normalize_normal_direction(normal_direction))
|
|
192
|
+
object.__setattr__(self, "kind", "plane")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class FluxMonitor(PlaneMonitor):
|
|
196
|
+
def __init__(self, name, axis="z", position=0.0, frequencies=None, normal_direction="+"):
|
|
197
|
+
super().__init__(
|
|
198
|
+
name=name,
|
|
199
|
+
axis=axis,
|
|
200
|
+
position=position,
|
|
201
|
+
fields=required_flux_fields(axis),
|
|
202
|
+
frequencies=frequencies,
|
|
203
|
+
compute_flux=True,
|
|
204
|
+
normal_direction=normal_direction,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass(frozen=True)
|
|
209
|
+
class FinitePlaneMonitor:
|
|
210
|
+
name: str
|
|
211
|
+
position: tuple[float, float, float]
|
|
212
|
+
size: tuple[float, float, float]
|
|
213
|
+
fields: tuple[str, ...]
|
|
214
|
+
frequencies: tuple[float, ...] | None = None
|
|
215
|
+
compute_flux: bool = False
|
|
216
|
+
normal_direction: str = "+"
|
|
217
|
+
axis: str = "z"
|
|
218
|
+
plane_position: float = 0.0
|
|
219
|
+
tangential_bounds: dict[str, tuple[float, float]] | None = None
|
|
220
|
+
face_label: str | None = None
|
|
221
|
+
surface_name: str | None = None
|
|
222
|
+
kind: str = "finite_plane"
|
|
223
|
+
|
|
224
|
+
def __init__(
|
|
225
|
+
self,
|
|
226
|
+
name,
|
|
227
|
+
position=(0.0, 0.0, 0.0),
|
|
228
|
+
size=(1.0, 1.0, 0.0),
|
|
229
|
+
fields=("Ez",),
|
|
230
|
+
frequencies=None,
|
|
231
|
+
compute_flux=False,
|
|
232
|
+
normal_direction="+",
|
|
233
|
+
*,
|
|
234
|
+
face_label=None,
|
|
235
|
+
surface_name=None,
|
|
236
|
+
):
|
|
237
|
+
resolved_position, resolved_size, axis, plane_position, tangential_bounds = _resolve_finite_plane_geometry(
|
|
238
|
+
position,
|
|
239
|
+
size,
|
|
240
|
+
)
|
|
241
|
+
normalized_fields = _normalize_fields(fields)
|
|
242
|
+
flux_enabled = bool(compute_flux)
|
|
243
|
+
if flux_enabled:
|
|
244
|
+
_validate_flux_fields(axis, normalized_fields, monitor_name="FinitePlaneMonitor")
|
|
245
|
+
|
|
246
|
+
object.__setattr__(self, "name", str(name))
|
|
247
|
+
object.__setattr__(self, "position", resolved_position)
|
|
248
|
+
object.__setattr__(self, "size", resolved_size)
|
|
249
|
+
object.__setattr__(self, "fields", normalized_fields)
|
|
250
|
+
object.__setattr__(self, "frequencies", _normalize_frequencies(frequencies))
|
|
251
|
+
object.__setattr__(self, "compute_flux", flux_enabled)
|
|
252
|
+
object.__setattr__(self, "normal_direction", _normalize_normal_direction(normal_direction))
|
|
253
|
+
object.__setattr__(self, "axis", axis)
|
|
254
|
+
object.__setattr__(self, "plane_position", plane_position)
|
|
255
|
+
object.__setattr__(self, "tangential_bounds", tangential_bounds)
|
|
256
|
+
object.__setattr__(self, "face_label", None if face_label is None else str(face_label))
|
|
257
|
+
object.__setattr__(self, "surface_name", None if surface_name is None else str(surface_name))
|
|
258
|
+
object.__setattr__(self, "kind", "finite_plane")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass(frozen=True)
|
|
262
|
+
class ClosedSurfaceMonitor:
|
|
263
|
+
name: str
|
|
264
|
+
faces: tuple[FinitePlaneMonitor, ...]
|
|
265
|
+
frequencies: tuple[float, ...] | None = None
|
|
266
|
+
bounds: tuple[tuple[float, float], tuple[float, float], tuple[float, float]] | None = None
|
|
267
|
+
kind: str = "closed_surface"
|
|
268
|
+
|
|
269
|
+
def __init__(self, name, faces, frequencies=None):
|
|
270
|
+
surface_name = str(name)
|
|
271
|
+
face_defs = tuple(faces)
|
|
272
|
+
if not face_defs:
|
|
273
|
+
raise ValueError("ClosedSurfaceMonitor requires at least one finite face.")
|
|
274
|
+
|
|
275
|
+
resolved_frequencies = _normalize_frequencies(frequencies)
|
|
276
|
+
normalized_faces = []
|
|
277
|
+
face_labels = set()
|
|
278
|
+
for face in face_defs:
|
|
279
|
+
if not isinstance(face, FinitePlaneMonitor):
|
|
280
|
+
raise TypeError(
|
|
281
|
+
"ClosedSurfaceMonitor expects FinitePlaneMonitor faces, "
|
|
282
|
+
f"got {type(face).__name__}."
|
|
283
|
+
)
|
|
284
|
+
face_label = face.face_label or face.name
|
|
285
|
+
if face_label in face_labels:
|
|
286
|
+
raise ValueError(f"ClosedSurfaceMonitor face labels must be unique, got duplicate {face_label!r}.")
|
|
287
|
+
face_labels.add(face_label)
|
|
288
|
+
|
|
289
|
+
face_frequencies = face.frequencies if face.frequencies is not None else resolved_frequencies
|
|
290
|
+
if resolved_frequencies is not None and face_frequencies is not None and face_frequencies != resolved_frequencies:
|
|
291
|
+
raise ValueError("ClosedSurfaceMonitor face frequencies must match the surface frequencies.")
|
|
292
|
+
|
|
293
|
+
normalized_faces.append(
|
|
294
|
+
FinitePlaneMonitor(
|
|
295
|
+
name=f"{surface_name}::{face_label}",
|
|
296
|
+
position=face.position,
|
|
297
|
+
size=face.size,
|
|
298
|
+
fields=face.fields,
|
|
299
|
+
frequencies=face_frequencies,
|
|
300
|
+
compute_flux=face.compute_flux,
|
|
301
|
+
normal_direction=face.normal_direction,
|
|
302
|
+
face_label=face_label,
|
|
303
|
+
surface_name=surface_name,
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
normalized_faces_tuple = tuple(normalized_faces)
|
|
308
|
+
_validate_closed_surface_area_balance(normalized_faces_tuple)
|
|
309
|
+
|
|
310
|
+
if resolved_frequencies is None:
|
|
311
|
+
reference_frequencies = normalized_faces_tuple[0].frequencies
|
|
312
|
+
for face in normalized_faces_tuple[1:]:
|
|
313
|
+
if face.frequencies != reference_frequencies:
|
|
314
|
+
raise ValueError("ClosedSurfaceMonitor faces must share the same frequencies.")
|
|
315
|
+
resolved_frequencies = reference_frequencies
|
|
316
|
+
|
|
317
|
+
object.__setattr__(self, "name", surface_name)
|
|
318
|
+
object.__setattr__(self, "faces", normalized_faces_tuple)
|
|
319
|
+
object.__setattr__(self, "frequencies", resolved_frequencies)
|
|
320
|
+
object.__setattr__(self, "bounds", self._compute_bounds(normalized_faces_tuple))
|
|
321
|
+
object.__setattr__(self, "kind", "closed_surface")
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def _compute_bounds(
|
|
325
|
+
faces: tuple[FinitePlaneMonitor, ...],
|
|
326
|
+
) -> tuple[tuple[float, float], tuple[float, float], tuple[float, float]]:
|
|
327
|
+
lower = [float("inf"), float("inf"), float("inf")]
|
|
328
|
+
upper = [float("-inf"), float("-inf"), float("-inf")]
|
|
329
|
+
for face in faces:
|
|
330
|
+
for axis_index, axis_name in enumerate(_AXES):
|
|
331
|
+
if axis_name == face.axis:
|
|
332
|
+
coordinate = float(face.plane_position)
|
|
333
|
+
lower[axis_index] = min(lower[axis_index], coordinate)
|
|
334
|
+
upper[axis_index] = max(upper[axis_index], coordinate)
|
|
335
|
+
continue
|
|
336
|
+
axis_lower, axis_upper = face.tangential_bounds[axis_name]
|
|
337
|
+
lower[axis_index] = min(lower[axis_index], float(axis_lower))
|
|
338
|
+
upper[axis_index] = max(upper[axis_index], float(axis_upper))
|
|
339
|
+
return tuple((lower[index], upper[index]) for index in range(3))
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def face_monitor_names(self) -> tuple[str, ...]:
|
|
343
|
+
return tuple(face.name for face in self.faces)
|
|
344
|
+
|
|
345
|
+
def resolved_monitors(self) -> tuple[FinitePlaneMonitor, ...]:
|
|
346
|
+
return self.faces
|
|
347
|
+
|
|
348
|
+
@classmethod
|
|
349
|
+
def box(
|
|
350
|
+
cls,
|
|
351
|
+
name,
|
|
352
|
+
*,
|
|
353
|
+
position=(0.0, 0.0, 0.0),
|
|
354
|
+
size=(1.0, 1.0, 1.0),
|
|
355
|
+
frequencies=None,
|
|
356
|
+
) -> "ClosedSurfaceMonitor":
|
|
357
|
+
center = _require_length3("position", position)
|
|
358
|
+
extents = _require_nonnegative_length3("size", size)
|
|
359
|
+
if any(length <= 0.0 for length in extents):
|
|
360
|
+
raise ValueError("ClosedSurfaceMonitor.box size must contain strictly positive extents.")
|
|
361
|
+
|
|
362
|
+
half = tuple(0.5 * length for length in extents)
|
|
363
|
+
faces = (
|
|
364
|
+
FinitePlaneMonitor(
|
|
365
|
+
"x_neg",
|
|
366
|
+
position=(center[0] - half[0], center[1], center[2]),
|
|
367
|
+
size=(0.0, extents[1], extents[2]),
|
|
368
|
+
fields=required_flux_fields("x"),
|
|
369
|
+
frequencies=frequencies,
|
|
370
|
+
normal_direction="-",
|
|
371
|
+
),
|
|
372
|
+
FinitePlaneMonitor(
|
|
373
|
+
"x_pos",
|
|
374
|
+
position=(center[0] + half[0], center[1], center[2]),
|
|
375
|
+
size=(0.0, extents[1], extents[2]),
|
|
376
|
+
fields=required_flux_fields("x"),
|
|
377
|
+
frequencies=frequencies,
|
|
378
|
+
normal_direction="+",
|
|
379
|
+
),
|
|
380
|
+
FinitePlaneMonitor(
|
|
381
|
+
"y_neg",
|
|
382
|
+
position=(center[0], center[1] - half[1], center[2]),
|
|
383
|
+
size=(extents[0], 0.0, extents[2]),
|
|
384
|
+
fields=required_flux_fields("y"),
|
|
385
|
+
frequencies=frequencies,
|
|
386
|
+
normal_direction="-",
|
|
387
|
+
),
|
|
388
|
+
FinitePlaneMonitor(
|
|
389
|
+
"y_pos",
|
|
390
|
+
position=(center[0], center[1] + half[1], center[2]),
|
|
391
|
+
size=(extents[0], 0.0, extents[2]),
|
|
392
|
+
fields=required_flux_fields("y"),
|
|
393
|
+
frequencies=frequencies,
|
|
394
|
+
normal_direction="+",
|
|
395
|
+
),
|
|
396
|
+
FinitePlaneMonitor(
|
|
397
|
+
"z_neg",
|
|
398
|
+
position=(center[0], center[1], center[2] - half[2]),
|
|
399
|
+
size=(extents[0], extents[1], 0.0),
|
|
400
|
+
fields=required_flux_fields("z"),
|
|
401
|
+
frequencies=frequencies,
|
|
402
|
+
normal_direction="-",
|
|
403
|
+
),
|
|
404
|
+
FinitePlaneMonitor(
|
|
405
|
+
"z_pos",
|
|
406
|
+
position=(center[0], center[1], center[2] + half[2]),
|
|
407
|
+
size=(extents[0], extents[1], 0.0),
|
|
408
|
+
fields=required_flux_fields("z"),
|
|
409
|
+
frequencies=frequencies,
|
|
410
|
+
normal_direction="+",
|
|
411
|
+
),
|
|
412
|
+
)
|
|
413
|
+
return cls(name, faces, frequencies=frequencies)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _material_monitor_bounds(
|
|
417
|
+
position: tuple[float, float, float],
|
|
418
|
+
size: tuple[float, float, float],
|
|
419
|
+
) -> tuple[tuple[float, float], tuple[float, float], tuple[float, float]]:
|
|
420
|
+
bounds = []
|
|
421
|
+
for center, extent in zip(position, size):
|
|
422
|
+
half = 0.5 * float(extent)
|
|
423
|
+
bounds.append((float(center) - half, float(center) + half))
|
|
424
|
+
return tuple(bounds)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@dataclass(frozen=True)
|
|
428
|
+
class PermittivityMonitor:
|
|
429
|
+
name: str
|
|
430
|
+
position: tuple[float, float, float]
|
|
431
|
+
size: tuple[float, float, float]
|
|
432
|
+
frequencies: tuple[float, ...] | None
|
|
433
|
+
bounds: tuple[tuple[float, float], tuple[float, float], tuple[float, float]]
|
|
434
|
+
kind: str = "permittivity"
|
|
435
|
+
|
|
436
|
+
def __init__(self, name, position=(0.0, 0.0, 0.0), size=(0.0, 0.0, 0.0), frequencies=None):
|
|
437
|
+
resolved_position = _require_length3("position", position)
|
|
438
|
+
resolved_size = _require_nonnegative_length3("size", size)
|
|
439
|
+
object.__setattr__(self, "name", str(name))
|
|
440
|
+
object.__setattr__(self, "position", resolved_position)
|
|
441
|
+
object.__setattr__(self, "size", resolved_size)
|
|
442
|
+
object.__setattr__(self, "frequencies", _normalize_frequencies(frequencies))
|
|
443
|
+
object.__setattr__(self, "bounds", _material_monitor_bounds(resolved_position, resolved_size))
|
|
444
|
+
object.__setattr__(self, "kind", "permittivity")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@dataclass(frozen=True)
|
|
448
|
+
class MediumMonitor:
|
|
449
|
+
name: str
|
|
450
|
+
position: tuple[float, float, float]
|
|
451
|
+
size: tuple[float, float, float]
|
|
452
|
+
frequencies: tuple[float, ...] | None
|
|
453
|
+
bounds: tuple[tuple[float, float], tuple[float, float], tuple[float, float]]
|
|
454
|
+
kind: str = "medium"
|
|
455
|
+
|
|
456
|
+
def __init__(self, name, position=(0.0, 0.0, 0.0), size=(0.0, 0.0, 0.0), frequencies=None):
|
|
457
|
+
resolved_position = _require_length3("position", position)
|
|
458
|
+
resolved_size = _require_nonnegative_length3("size", size)
|
|
459
|
+
object.__setattr__(self, "name", str(name))
|
|
460
|
+
object.__setattr__(self, "position", resolved_position)
|
|
461
|
+
object.__setattr__(self, "size", resolved_size)
|
|
462
|
+
object.__setattr__(self, "frequencies", _normalize_frequencies(frequencies))
|
|
463
|
+
object.__setattr__(self, "bounds", _material_monitor_bounds(resolved_position, resolved_size))
|
|
464
|
+
object.__setattr__(self, "kind", "medium")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@dataclass(frozen=True)
|
|
468
|
+
class DipoleEmissionMonitor:
|
|
469
|
+
"""Measure the power a named ``PointDipole`` delivers to the field.
|
|
470
|
+
|
|
471
|
+
The co-located electric field is sampled in the frequency domain at the
|
|
472
|
+
dipole cell, and ``P = -(1/2) Re(conj(J) . E)`` is formed from the known
|
|
473
|
+
dipole current spectrum. Normalizing this against the same dipole run in
|
|
474
|
+
vacuum yields the Purcell factor (local density of states). The vacuum
|
|
475
|
+
normalization is preferred over an analytic free-space formula because the
|
|
476
|
+
discrete Yee-grid effective source volume has no reliable closed form.
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
name: str
|
|
480
|
+
source_name: str
|
|
481
|
+
frequencies: tuple[float, ...] | None = None
|
|
482
|
+
kind: str = "dipole_emission"
|
|
483
|
+
|
|
484
|
+
def __init__(self, name, source_name, frequencies=None):
|
|
485
|
+
object.__setattr__(self, "name", str(name))
|
|
486
|
+
object.__setattr__(self, "source_name", str(source_name))
|
|
487
|
+
object.__setattr__(self, "frequencies", _normalize_frequencies(frequencies))
|
|
488
|
+
object.__setattr__(self, "kind", "dipole_emission")
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _validate_time_sampling(start, stop, interval) -> tuple[int, int | None, int]:
|
|
492
|
+
resolved_start = int(start)
|
|
493
|
+
if resolved_start < 0:
|
|
494
|
+
raise ValueError("start must be >= 0.")
|
|
495
|
+
resolved_interval = int(interval)
|
|
496
|
+
if resolved_interval < 1:
|
|
497
|
+
raise ValueError("interval must be >= 1.")
|
|
498
|
+
if stop is None:
|
|
499
|
+
resolved_stop = None
|
|
500
|
+
else:
|
|
501
|
+
resolved_stop = int(stop)
|
|
502
|
+
if resolved_stop <= resolved_start:
|
|
503
|
+
raise ValueError("stop must be greater than start.")
|
|
504
|
+
return resolved_start, resolved_stop, resolved_interval
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@dataclass(frozen=True)
|
|
508
|
+
class FieldTimeMonitor:
|
|
509
|
+
name: str
|
|
510
|
+
components: tuple[str, ...]
|
|
511
|
+
position: tuple[float, float, float]
|
|
512
|
+
size: tuple[float, float, float]
|
|
513
|
+
start: int
|
|
514
|
+
stop: int | None
|
|
515
|
+
interval: int
|
|
516
|
+
region_kind: str
|
|
517
|
+
axis: str | None = None
|
|
518
|
+
plane_position: float | None = None
|
|
519
|
+
tangential_bounds: dict[str, tuple[float, float]] | None = None
|
|
520
|
+
kind: str = "field_time"
|
|
521
|
+
|
|
522
|
+
def __init__(
|
|
523
|
+
self,
|
|
524
|
+
name,
|
|
525
|
+
components=("Ez",),
|
|
526
|
+
position=(0.0, 0.0, 0.0),
|
|
527
|
+
size=(0.0, 0.0, 0.0),
|
|
528
|
+
start=0,
|
|
529
|
+
stop=None,
|
|
530
|
+
interval=1,
|
|
531
|
+
):
|
|
532
|
+
normalized_components = tuple(normalize_component(component).capitalize() for component in _normalize_fields(components))
|
|
533
|
+
resolved_position = _require_length3("position", position)
|
|
534
|
+
resolved_size = _require_nonnegative_length3("size", size)
|
|
535
|
+
resolved_start, resolved_stop, resolved_interval = _validate_time_sampling(start, stop, interval)
|
|
536
|
+
zero_axes = [index for index, length in enumerate(resolved_size) if abs(length) <= 1e-12]
|
|
537
|
+
axis = None
|
|
538
|
+
plane_position = None
|
|
539
|
+
tangential_bounds = None
|
|
540
|
+
if len(zero_axes) == 3:
|
|
541
|
+
region_kind = "point"
|
|
542
|
+
elif len(zero_axes) == 1:
|
|
543
|
+
region_kind = "plane"
|
|
544
|
+
_, _, axis, plane_position, tangential_bounds = _resolve_finite_plane_geometry(
|
|
545
|
+
resolved_position,
|
|
546
|
+
resolved_size,
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
region_kind = "volume"
|
|
550
|
+
|
|
551
|
+
object.__setattr__(self, "name", str(name))
|
|
552
|
+
object.__setattr__(self, "components", normalized_components)
|
|
553
|
+
object.__setattr__(self, "position", resolved_position)
|
|
554
|
+
object.__setattr__(self, "size", resolved_size)
|
|
555
|
+
object.__setattr__(self, "start", resolved_start)
|
|
556
|
+
object.__setattr__(self, "stop", resolved_stop)
|
|
557
|
+
object.__setattr__(self, "interval", resolved_interval)
|
|
558
|
+
object.__setattr__(self, "region_kind", region_kind)
|
|
559
|
+
object.__setattr__(self, "axis", axis)
|
|
560
|
+
object.__setattr__(self, "plane_position", plane_position)
|
|
561
|
+
object.__setattr__(self, "tangential_bounds", tangential_bounds)
|
|
562
|
+
object.__setattr__(self, "kind", "field_time")
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@dataclass(frozen=True)
|
|
566
|
+
class FluxTimeMonitor:
|
|
567
|
+
name: str
|
|
568
|
+
axis: str
|
|
569
|
+
position: float
|
|
570
|
+
fields: tuple[str, ...]
|
|
571
|
+
normal_direction: str
|
|
572
|
+
start: int
|
|
573
|
+
stop: int | None
|
|
574
|
+
interval: int
|
|
575
|
+
kind: str = "flux_time"
|
|
576
|
+
|
|
577
|
+
def __init__(
|
|
578
|
+
self,
|
|
579
|
+
name,
|
|
580
|
+
axis="z",
|
|
581
|
+
position=0.0,
|
|
582
|
+
start=0,
|
|
583
|
+
stop=None,
|
|
584
|
+
interval=1,
|
|
585
|
+
normal_direction="+",
|
|
586
|
+
):
|
|
587
|
+
axis_name = normalize_axis(axis)
|
|
588
|
+
resolved_start, resolved_stop, resolved_interval = _validate_time_sampling(start, stop, interval)
|
|
589
|
+
object.__setattr__(self, "name", str(name))
|
|
590
|
+
object.__setattr__(self, "axis", axis_name)
|
|
591
|
+
object.__setattr__(self, "position", float(position))
|
|
592
|
+
object.__setattr__(self, "fields", required_flux_fields(axis_name))
|
|
593
|
+
object.__setattr__(self, "normal_direction", _normalize_normal_direction(normal_direction))
|
|
594
|
+
object.__setattr__(self, "start", resolved_start)
|
|
595
|
+
object.__setattr__(self, "stop", resolved_stop)
|
|
596
|
+
object.__setattr__(self, "interval", resolved_interval)
|
|
597
|
+
object.__setattr__(self, "kind", "flux_time")
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
@dataclass(frozen=True)
|
|
601
|
+
class ModeMonitor:
|
|
602
|
+
name: str
|
|
603
|
+
position: tuple[float, float, float]
|
|
604
|
+
size: tuple[float, float, float]
|
|
605
|
+
mode_index: int
|
|
606
|
+
direction: str
|
|
607
|
+
polarization: tuple[float, float, float]
|
|
608
|
+
frequencies: tuple[float, ...] | None = None
|
|
609
|
+
normal_direction: str = "+"
|
|
610
|
+
axis: str = "z"
|
|
611
|
+
plane_position: float = 0.0
|
|
612
|
+
fields: tuple[str, ...] = ()
|
|
613
|
+
compute_flux: bool = True
|
|
614
|
+
normal_axis: str = "z"
|
|
615
|
+
polarization_axis: str = "x"
|
|
616
|
+
bend_radius: float | None = None
|
|
617
|
+
bend_axis: str | None = None
|
|
618
|
+
kind: str = "mode"
|
|
619
|
+
|
|
620
|
+
def __init__(
|
|
621
|
+
self,
|
|
622
|
+
name,
|
|
623
|
+
position=(0.0, 0.0, 0.0),
|
|
624
|
+
size=(1.0, 1.0, 0.0),
|
|
625
|
+
mode_index=0,
|
|
626
|
+
direction="+",
|
|
627
|
+
polarization="auto",
|
|
628
|
+
frequencies=None,
|
|
629
|
+
normal_direction=None,
|
|
630
|
+
bend_radius=None,
|
|
631
|
+
bend_axis=None,
|
|
632
|
+
):
|
|
633
|
+
resolved_position = _require_length3("position", position)
|
|
634
|
+
resolved_size = _require_mode_source_size(size)
|
|
635
|
+
normal_axis = _resolve_mode_source_normal_axis(resolved_size)
|
|
636
|
+
polarization_axis = _resolve_mode_source_polarization_axis(normal_axis, resolved_size, polarization)
|
|
637
|
+
resolved_bend_radius, resolved_bend_axis = _normalize_bend(bend_radius, bend_axis, normal_axis)
|
|
638
|
+
axis_index = "xyz".index(normal_axis)
|
|
639
|
+
resolved_direction = _normalize_mode_direction(direction)
|
|
640
|
+
resolved_normal_direction = (
|
|
641
|
+
resolved_direction if normal_direction is None else _normalize_normal_direction(normal_direction)
|
|
642
|
+
)
|
|
643
|
+
axis = normal_axis
|
|
644
|
+
fields = required_flux_fields(axis)
|
|
645
|
+
object.__setattr__(self, "name", str(name))
|
|
646
|
+
object.__setattr__(self, "position", resolved_position)
|
|
647
|
+
object.__setattr__(self, "size", resolved_size)
|
|
648
|
+
object.__setattr__(self, "mode_index", int(mode_index))
|
|
649
|
+
object.__setattr__(self, "direction", resolved_direction)
|
|
650
|
+
object.__setattr__(self, "polarization", polarization_vector(f"E{polarization_axis}"))
|
|
651
|
+
object.__setattr__(self, "frequencies", _normalize_frequencies(frequencies))
|
|
652
|
+
object.__setattr__(self, "normal_direction", resolved_normal_direction)
|
|
653
|
+
object.__setattr__(self, "axis", axis)
|
|
654
|
+
object.__setattr__(self, "plane_position", float(resolved_position[axis_index]))
|
|
655
|
+
object.__setattr__(self, "fields", fields)
|
|
656
|
+
object.__setattr__(self, "compute_flux", True)
|
|
657
|
+
object.__setattr__(self, "normal_axis", normal_axis)
|
|
658
|
+
object.__setattr__(self, "polarization_axis", polarization_axis)
|
|
659
|
+
object.__setattr__(self, "bend_radius", resolved_bend_radius)
|
|
660
|
+
object.__setattr__(self, "bend_axis", resolved_bend_axis)
|
|
661
|
+
object.__setattr__(self, "kind", "mode")
|
|
662
|
+
if self.mode_index < 0:
|
|
663
|
+
raise ValueError("mode_index must be >= 0.")
|
|
664
|
+
|
|
665
|
+
def mode_spec(self) -> dict[str, object]:
|
|
666
|
+
return {
|
|
667
|
+
"name": self.name,
|
|
668
|
+
"position": self.position,
|
|
669
|
+
"size": self.size,
|
|
670
|
+
"mode_index": int(self.mode_index),
|
|
671
|
+
"direction": self.direction,
|
|
672
|
+
"polarization": self.polarization,
|
|
673
|
+
"polarization_axis": self.polarization_axis,
|
|
674
|
+
"normal_axis": self.normal_axis,
|
|
675
|
+
"bend_radius": self.bend_radius,
|
|
676
|
+
"bend_axis": self.bend_axis,
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@dataclass(frozen=True)
|
|
681
|
+
class DiffractionMonitor:
|
|
682
|
+
name: str
|
|
683
|
+
position: tuple[float, float, float]
|
|
684
|
+
size: tuple[float, float, float]
|
|
685
|
+
frequencies: tuple[float, ...] | None
|
|
686
|
+
normal_direction: str
|
|
687
|
+
orders: int | None
|
|
688
|
+
axis: str
|
|
689
|
+
plane_position: float
|
|
690
|
+
tangential_axes: tuple[str, str]
|
|
691
|
+
periods: dict[str, float]
|
|
692
|
+
fields: tuple[str, ...]
|
|
693
|
+
compute_flux: bool = True
|
|
694
|
+
kind: str = "diffraction"
|
|
695
|
+
|
|
696
|
+
def __init__(
|
|
697
|
+
self,
|
|
698
|
+
name,
|
|
699
|
+
position=(0.0, 0.0, 0.0),
|
|
700
|
+
size=(1.0, 1.0, 0.0),
|
|
701
|
+
frequencies=None,
|
|
702
|
+
normal_direction="+",
|
|
703
|
+
orders=None,
|
|
704
|
+
):
|
|
705
|
+
resolved_position, resolved_size, axis, plane_position, _ = _resolve_finite_plane_geometry(
|
|
706
|
+
position,
|
|
707
|
+
size,
|
|
708
|
+
)
|
|
709
|
+
transverse_axes = tuple(axis_name for axis_name in _AXES if axis_name != axis)
|
|
710
|
+
periods = {
|
|
711
|
+
axis_name: float(resolved_size[_AXES.index(axis_name)]) for axis_name in transverse_axes
|
|
712
|
+
}
|
|
713
|
+
if any(period <= 0.0 for period in periods.values()):
|
|
714
|
+
raise ValueError("DiffractionMonitor requires strictly positive transverse periods.")
|
|
715
|
+
resolved_orders = None if orders is None else int(orders)
|
|
716
|
+
if resolved_orders is not None and resolved_orders < 0:
|
|
717
|
+
raise ValueError("orders must be a non-negative integer or None.")
|
|
718
|
+
|
|
719
|
+
object.__setattr__(self, "name", str(name))
|
|
720
|
+
object.__setattr__(self, "position", resolved_position)
|
|
721
|
+
object.__setattr__(self, "size", resolved_size)
|
|
722
|
+
object.__setattr__(self, "frequencies", _normalize_frequencies(frequencies))
|
|
723
|
+
object.__setattr__(self, "normal_direction", _normalize_normal_direction(normal_direction))
|
|
724
|
+
object.__setattr__(self, "orders", resolved_orders)
|
|
725
|
+
object.__setattr__(self, "axis", axis)
|
|
726
|
+
object.__setattr__(self, "plane_position", plane_position)
|
|
727
|
+
object.__setattr__(self, "tangential_axes", transverse_axes)
|
|
728
|
+
object.__setattr__(self, "periods", periods)
|
|
729
|
+
object.__setattr__(self, "fields", required_flux_fields(axis))
|
|
730
|
+
object.__setattr__(self, "compute_flux", True)
|
|
731
|
+
object.__setattr__(self, "kind", "diffraction")
|
|
732
|
+
|
|
733
|
+
def diffraction_spec(self) -> dict[str, object]:
|
|
734
|
+
return {
|
|
735
|
+
"name": self.name,
|
|
736
|
+
"position": self.position,
|
|
737
|
+
"size": self.size,
|
|
738
|
+
"normal_axis": self.axis,
|
|
739
|
+
"tangential_axes": self.tangential_axes,
|
|
740
|
+
"periods": dict(self.periods),
|
|
741
|
+
"orders": self.orders,
|
|
742
|
+
"normal_direction": self.normal_direction,
|
|
743
|
+
}
|