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