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