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,358 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+
5
+ import numpy as np
6
+ import torch
7
+
8
+ from ..sources import CW, PlaneWave, TFSF, evaluate_source_time
9
+ from .stratton_chu import _trapz_weights_1d
10
+
11
+ _MU0 = 4.0 * torch.pi * 1e-7
12
+ _POWER_EPS = 1e-30
13
+ _COORD_NAMES = {
14
+ "x": ("y", "z"),
15
+ "y": ("x", "z"),
16
+ "z": ("x", "y"),
17
+ }
18
+
19
+
20
+ def _result_frequencies(result) -> torch.Tensor:
21
+ frequencies = torch.as_tensor(getattr(result, "frequencies", ()), dtype=torch.float64)
22
+ if frequencies.ndim != 1 or frequencies.numel() == 0:
23
+ raise ValueError("result must expose one or more frequencies.")
24
+ return frequencies
25
+
26
+
27
+ def _as_1d_tensor(value, *, size: int, name: str, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
28
+ array = torch.as_tensor(value, device=device, dtype=dtype)
29
+ if array.ndim == 0:
30
+ array = torch.full((size,), array.item(), device=device, dtype=dtype)
31
+ if tuple(array.shape) != (size,):
32
+ raise ValueError(f"{name} must be broadcastable to shape ({size},), got {tuple(array.shape)}.")
33
+ return array
34
+
35
+
36
+ def _monitor_payload(result, name: str) -> dict:
37
+ try:
38
+ payload = result.monitor(name)
39
+ except KeyError as exc:
40
+ raise KeyError(f"Monitor {name!r} is not available in the supplied result.") from exc
41
+ if "flux" not in payload:
42
+ raise ValueError(
43
+ f"Monitor {name!r} does not provide flux data. "
44
+ "Use FluxMonitor or PlaneMonitor(compute_flux=True)."
45
+ )
46
+ return payload
47
+
48
+
49
+ def _assert_matching_frequencies(actual: torch.Tensor, expected: torch.Tensor, *, name: str):
50
+ if tuple(actual.shape) != tuple(expected.shape) or not torch.allclose(actual, expected, rtol=1e-9, atol=1e-12):
51
+ raise ValueError(
52
+ f"{name} frequencies must match exactly. "
53
+ f"Expected {tuple(expected.tolist())}, got {tuple(actual.tolist())}."
54
+ )
55
+
56
+
57
+ def _monitor_flux(result, name: str, *, expected_frequencies: torch.Tensor) -> torch.Tensor:
58
+ payload = _monitor_payload(result, name)
59
+ monitor_frequencies = torch.as_tensor(
60
+ payload.get("frequencies", (payload.get("frequency"),)),
61
+ dtype=torch.float64,
62
+ )
63
+ _assert_matching_frequencies(monitor_frequencies, expected_frequencies, name=f"Monitor {name!r}")
64
+ return _as_1d_tensor(
65
+ payload["flux"],
66
+ size=expected_frequencies.numel(),
67
+ name=f"Monitor {name!r} flux",
68
+ device=expected_frequencies.device,
69
+ dtype=expected_frequencies.dtype,
70
+ )
71
+
72
+
73
+ def _monitor_area(result, monitor_name: str) -> float:
74
+ payload = _monitor_payload(result, monitor_name)
75
+ axis = str(payload["axis"]).lower()
76
+ coord_names = _COORD_NAMES[axis]
77
+ coords = payload.get("coords")
78
+ if coords is not None and len(coords) == 2:
79
+ coord_a, coord_b = coords
80
+ elif all(name in payload for name in coord_names):
81
+ coord_a, coord_b = payload[coord_names[0]], payload[coord_names[1]]
82
+ else:
83
+ extents = {
84
+ "x": result.scene.domain.bounds[0],
85
+ "y": result.scene.domain.bounds[1],
86
+ "z": result.scene.domain.bounds[2],
87
+ }
88
+ return float(extents[coord_names[0]][1] - extents[coord_names[0]][0]) * float(
89
+ extents[coord_names[1]][1] - extents[coord_names[1]][0]
90
+ )
91
+
92
+ coord_a_tensor = torch.as_tensor(coord_a, dtype=torch.float64)
93
+ coord_b_tensor = torch.as_tensor(coord_b, dtype=torch.float64)
94
+ weights = _trapz_weights_1d(coord_a_tensor)[:, None] * _trapz_weights_1d(coord_b_tensor)[None, :]
95
+ return float(torch.sum(weights).item())
96
+
97
+
98
+ def _incident_beam_area(source, result, incident_monitor: str) -> float:
99
+ """Cross-sectional area the incident PlaneWave illuminates at the monitor.
100
+
101
+ A TFSF box injects the incident field only inside its aperture, so the flux a
102
+ plane monitor spanning the whole cross-section reports comes from that
103
+ aperture alone; the relevant area is the aperture face transverse to the
104
+ propagation axis, not the full monitor plane. A soft source (or a full-width
105
+ TFSF slab) fills the whole cross-section, so the monitor area is used.
106
+ """
107
+
108
+ injection = getattr(source, "injection", "soft")
109
+ if isinstance(injection, TFSF) and getattr(injection, "mode", "box") == "box" and injection.bounds is not None:
110
+ direction = tuple(float(component) for component in getattr(source, "direction", (0.0, 0.0, 1.0)))
111
+ axis_index = max(range(3), key=lambda index: abs(direction[index]))
112
+ transverse = [index for index in range(3) if index != axis_index]
113
+ bounds = injection.bounds
114
+ return float(bounds[transverse[0]][1] - bounds[transverse[0]][0]) * float(
115
+ bounds[transverse[1]][1] - bounds[transverse[1]][0]
116
+ )
117
+ return _monitor_area(result, incident_monitor)
118
+
119
+
120
+ def _plane_wave_cw_amplitude(source_time) -> float | None:
121
+ """Peak amplitude of a steady-state CW PlaneWave, or ``None`` if broadband.
122
+
123
+ A missing ``source_time`` defaults to a unit-amplitude CW carrier at the
124
+ monitor frequency, matching the injector's default excitation.
125
+ """
126
+
127
+ if source_time is None:
128
+ return 1.0
129
+ if isinstance(source_time, CW):
130
+ return float(source_time.amplitude)
131
+ if isinstance(source_time, Mapping) and str(source_time.get("kind", "")).lower() == "cw":
132
+ return float(source_time["amplitude"])
133
+ return None
134
+
135
+
136
+ def _broadband_plane_wave_incident_power(
137
+ result,
138
+ source_time,
139
+ frequencies: torch.Tensor,
140
+ *,
141
+ eta0: float,
142
+ area: float,
143
+ ) -> torch.Tensor:
144
+ """Analytic incident power spectrum of a broadband PlaneWave.
145
+
146
+ A soft PlaneWave face radiates the incident field forward with unit gain, so
147
+ the incident field at the monitor is the source waveform itself,
148
+ ``E_inc(t) = A * s(t)``, and an empty reference monitor reports the forward
149
+ flux ``|E_inc(f)|^2 / (2*eta0) * area`` per frequency, where ``area`` is the
150
+ illuminated cross-section (the TFSF aperture, or the full monitor plane for a
151
+ soft source). This reduces to ``A^2 / (2*eta0) * area`` at the CW carrier,
152
+ matching the closed-form CW branch.
153
+
154
+ The monitor forms ``E_inc(f)`` with a windowed running DFT: for a non-CW
155
+ waveform the solver forces ``window="none"`` and ``start_step=0`` (see
156
+ ``fdtd/runtime/spectral.py``), and the reported phasor is
157
+ ``(2/sum_w) * sum_n w(n) * field(n) * exp(i*w*n*dt)``. Reproducing that exact
158
+ discrete transform of the *known* injected waveform yields the reference
159
+ incident power without a second simulation: the injection-to-monitor
160
+ propagation delay only rotates a global phase, which the ``|.|^2`` magnitude
161
+ discards. The reconstruction is co-located (the leading Yee E/H stagger
162
+ factor ``cos(k~*d/2)`` the monitor applies is dropped), so the absolute power
163
+ matches the reference monitor to the grid-dispersion level of that stagger,
164
+ which vanishes as the grid is refined -- the same regime the soft-PlaneWave
165
+ absolute-power calibration is pinned at.
166
+ """
167
+
168
+ solver = getattr(result, "solver", None)
169
+ entries = list(getattr(solver, "_observer_spectral_entries", ()) or ())
170
+ dt = getattr(solver, "dt", None)
171
+ if solver is None or not entries or dt is None:
172
+ raise ValueError(
173
+ "incident_power='auto' for a broadband (non-CW) PlaneWave needs the FDTD "
174
+ "solver's running-DFT schedule (dt and per-frequency spectral entries), which "
175
+ "is only present on a freshly run FDTD Result. Pass reference_result (an empty "
176
+ "run) or an explicit incident_power array instead."
177
+ )
178
+
179
+ dt = float(dt)
180
+ window_type = str(getattr(solver, "observer_window_type", "none"))
181
+ entry_frequencies = np.array([float(entry["frequency"]) for entry in entries], dtype=np.float64)
182
+
183
+ # Evaluate the injected waveform once over the widest accumulated step range.
184
+ max_count = max(int(entry["sample_count"]) for entry in entries)
185
+ min_start = min(int(entry["start_step"]) for entry in entries)
186
+ if max_count <= 0:
187
+ raise ValueError(
188
+ "incident_power='auto' requires a completed FDTD run: the monitor DFT "
189
+ "accumulated zero samples."
190
+ )
191
+ steps = np.arange(min_start, min_start + max_count, dtype=np.float64)
192
+ waveform = np.array([evaluate_source_time(source_time, float(step) * dt) for step in steps], dtype=np.float64)
193
+
194
+ powers = []
195
+ for frequency in frequencies.tolist():
196
+ match_index = int(np.argmin(np.abs(entry_frequencies - float(frequency))))
197
+ if abs(entry_frequencies[match_index] - float(frequency)) > 1e-6 * max(abs(float(frequency)), 1.0):
198
+ raise ValueError(
199
+ f"No solver DFT entry matches monitor frequency {float(frequency)!r}; "
200
+ "the incident-power normalization cannot be reconstructed."
201
+ )
202
+ entry = entries[match_index]
203
+ start = int(entry["start_step"])
204
+ count = int(entry["sample_count"])
205
+ norm = float(entry["window_normalization"])
206
+ if count <= 0 or norm <= _POWER_EPS:
207
+ raise ValueError(
208
+ f"Monitor DFT for frequency {float(frequency)!r} accumulated no weighted "
209
+ "samples; incident-power normalization is undefined."
210
+ )
211
+ local = slice(start - min_start, start - min_start + count)
212
+ step_slice = steps[local]
213
+ signal = waveform[local]
214
+ if window_type == "none":
215
+ weights = np.ones(count, dtype=np.float64)
216
+ else:
217
+ weights = np.array(
218
+ [
219
+ float(solver._compute_window_weight(int(step), start_step=start, end_step=entry["end_step"], window_type=window_type))
220
+ for step in step_slice
221
+ ],
222
+ dtype=np.float64,
223
+ )
224
+ phase = np.exp(1j * (2.0 * np.pi * float(frequency)) * step_slice * dt)
225
+ spectral_amplitude = (2.0 / norm) * np.sum(weights * signal * phase)
226
+ powers.append(float(abs(spectral_amplitude) ** 2) / (2.0 * eta0) * area)
227
+
228
+ return torch.as_tensor(powers, device=frequencies.device, dtype=frequencies.dtype)
229
+
230
+
231
+ def _plane_wave_incident_power(result, incident_monitor: str, frequencies: torch.Tensor) -> torch.Tensor:
232
+ plane_wave_sources = [source for source in getattr(result.scene, "sources", ()) if isinstance(source, PlaneWave)]
233
+ if len(plane_wave_sources) != 1:
234
+ raise ValueError("incident_power='auto' requires exactly one PlaneWave source on result.scene.")
235
+
236
+ source = plane_wave_sources[0]
237
+ source_time = source.source_time
238
+
239
+ solver = getattr(result, "solver", None)
240
+ c_value = float(getattr(solver, "c", getattr(solver, "c0", 299792458.0)))
241
+ eta0 = float(_MU0) * c_value
242
+ area = _incident_beam_area(source, result, incident_monitor)
243
+
244
+ cw_amplitude = _plane_wave_cw_amplitude(source_time)
245
+ if cw_amplitude is not None:
246
+ power_density = (abs(cw_amplitude) ** 2) / (2.0 * eta0)
247
+ return torch.full(
248
+ (frequencies.numel(),),
249
+ power_density * area,
250
+ device=frequencies.device,
251
+ dtype=frequencies.dtype,
252
+ )
253
+
254
+ return _broadband_plane_wave_incident_power(result, source_time, frequencies, eta0=eta0, area=area)
255
+
256
+
257
+ def _resolve_incident_power(
258
+ result,
259
+ incident_monitor: str,
260
+ *,
261
+ frequencies: torch.Tensor,
262
+ reference_result=None,
263
+ incident_power=None,
264
+ ) -> torch.Tensor:
265
+ if reference_result is not None and incident_power is not None:
266
+ raise ValueError("Pass either reference_result or incident_power, not both.")
267
+ if reference_result is not None:
268
+ reference_frequencies = _result_frequencies(reference_result)
269
+ _assert_matching_frequencies(reference_frequencies, frequencies, name="reference_result")
270
+ return _monitor_flux(reference_result, incident_monitor, expected_frequencies=frequencies).to(
271
+ device=frequencies.device,
272
+ dtype=frequencies.dtype,
273
+ )
274
+ if incident_power is None:
275
+ raise ValueError("incident power normalization is required. Pass reference_result or incident_power.")
276
+ if isinstance(incident_power, str):
277
+ token = incident_power.strip().lower()
278
+ if token != "auto":
279
+ raise ValueError("incident_power string values must be 'auto'.")
280
+ return _plane_wave_incident_power(result, incident_monitor, frequencies)
281
+ return _as_1d_tensor(
282
+ incident_power,
283
+ size=frequencies.numel(),
284
+ name="incident_power",
285
+ device=frequencies.device,
286
+ dtype=frequencies.dtype,
287
+ )
288
+
289
+
290
+ def _to_db(magnitude: torch.Tensor) -> torch.Tensor:
291
+ return 20.0 * torch.log10(torch.clamp_min(magnitude, _POWER_EPS))
292
+
293
+
294
+ def compute_s_parameters(
295
+ result,
296
+ incident_monitor: str,
297
+ *,
298
+ transmitted_monitor: str | None = None,
299
+ reference_result=None,
300
+ incident_power=None,
301
+ ) -> dict:
302
+ """
303
+ Compute broadband S-parameter magnitudes from FDTD flux-monitor results.
304
+
305
+ The returned complex arrays currently place the magnitude on the real axis.
306
+ Phase recovery requires field-based port decomposition and is left as future work.
307
+ """
308
+
309
+ frequencies = _result_frequencies(result)
310
+ incident_flux = _monitor_flux(result, incident_monitor, expected_frequencies=frequencies)
311
+ if isinstance(incident_flux, torch.Tensor):
312
+ frequencies = frequencies.to(device=incident_flux.device, dtype=incident_flux.dtype)
313
+ incident_flux = incident_flux.to(device=frequencies.device, dtype=frequencies.dtype)
314
+ incident_power_array = _resolve_incident_power(
315
+ result,
316
+ incident_monitor,
317
+ frequencies=frequencies,
318
+ reference_result=reference_result,
319
+ incident_power=incident_power,
320
+ ).to(device=frequencies.device, dtype=frequencies.dtype)
321
+ if torch.any(incident_power_array <= _POWER_EPS):
322
+ raise ValueError(
323
+ "incident power must be strictly positive at every frequency. "
324
+ "Check monitor orientation, source spectrum, or the supplied normalization."
325
+ )
326
+
327
+ reflected_power = torch.abs(incident_flux - incident_power_array)
328
+ s11_mag = torch.sqrt(reflected_power / incident_power_array)
329
+ complex_dtype = torch.complex64 if s11_mag.dtype in {torch.float16, torch.bfloat16, torch.float32} else torch.complex128
330
+
331
+ output = {
332
+ "frequencies": frequencies,
333
+ "S11": s11_mag.to(dtype=complex_dtype),
334
+ "S11_mag": s11_mag,
335
+ "S11_db": _to_db(s11_mag),
336
+ "S21": None,
337
+ "S21_mag": None,
338
+ "S21_db": None,
339
+ "P_incident": incident_power_array,
340
+ "P_reflected": reflected_power,
341
+ "P_transmitted": None,
342
+ }
343
+
344
+ if transmitted_monitor is None:
345
+ return output
346
+
347
+ transmitted_flux = _monitor_flux(result, transmitted_monitor, expected_frequencies=frequencies).to(
348
+ device=frequencies.device,
349
+ dtype=frequencies.dtype,
350
+ )
351
+ transmitted_power = torch.abs(transmitted_flux)
352
+ s21_mag = torch.sqrt(transmitted_power / incident_power_array)
353
+
354
+ output["S21"] = s21_mag.to(dtype=complex_dtype)
355
+ output["S21_mag"] = s21_mag
356
+ output["S21_db"] = _to_db(s21_mag)
357
+ output["P_transmitted"] = transmitted_power
358
+ return output