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
witwin/maxwell/result.py
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import torch
|
|
9
|
+
|
|
10
|
+
from .monitors import ClosedSurfaceMonitor, FinitePlaneMonitor, MediumMonitor, PermittivityMonitor
|
|
11
|
+
from .scene import prepare_scene
|
|
12
|
+
from .visualization import extract_orthogonal_slice, plot_slice_image
|
|
13
|
+
|
|
14
|
+
_UNSET = object()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _clone_mapping(data: dict[str, Any]) -> dict[str, Any]:
|
|
18
|
+
return dict(data) if data is not None else {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _cpu_serializable(value: Any):
|
|
22
|
+
if isinstance(value, torch.Tensor):
|
|
23
|
+
return value.detach().cpu()
|
|
24
|
+
if isinstance(value, dict):
|
|
25
|
+
return {key: _cpu_serializable(item) for key, item in value.items()}
|
|
26
|
+
if isinstance(value, list):
|
|
27
|
+
return [_cpu_serializable(item) for item in value]
|
|
28
|
+
if isinstance(value, tuple):
|
|
29
|
+
return tuple(_cpu_serializable(item) for item in value)
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_result_frequencies(*, frequency: float | None, frequencies) -> tuple[float, ...]:
|
|
34
|
+
if frequencies is not None:
|
|
35
|
+
resolved = tuple(float(freq) for freq in frequencies)
|
|
36
|
+
elif frequency is not None:
|
|
37
|
+
resolved = (float(frequency),)
|
|
38
|
+
else:
|
|
39
|
+
raise ValueError("Result requires frequency or frequencies.")
|
|
40
|
+
if not resolved:
|
|
41
|
+
raise ValueError("Result frequencies must not be empty.")
|
|
42
|
+
return resolved
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve_frequency_index(
|
|
46
|
+
frequencies: tuple[float, ...],
|
|
47
|
+
*,
|
|
48
|
+
frequency: float | None = None,
|
|
49
|
+
freq_index: int | None = None,
|
|
50
|
+
) -> int | None:
|
|
51
|
+
if frequency is not None and freq_index is not None:
|
|
52
|
+
raise ValueError("Pass either frequency or freq_index, not both.")
|
|
53
|
+
if not frequencies:
|
|
54
|
+
return None
|
|
55
|
+
if freq_index is not None:
|
|
56
|
+
index = int(freq_index)
|
|
57
|
+
if index < 0 or index >= len(frequencies):
|
|
58
|
+
raise IndexError(f"freq_index={index} is out of range for {len(frequencies)} frequencies.")
|
|
59
|
+
return index
|
|
60
|
+
if frequency is None:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
target = float(frequency)
|
|
64
|
+
for index, candidate in enumerate(frequencies):
|
|
65
|
+
if np.isclose(candidate, target, rtol=1e-9, atol=1e-12):
|
|
66
|
+
return index
|
|
67
|
+
raise KeyError(f"Frequency {target!r} is not available. Choices: {frequencies}.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _slice_frequency_axis(value: Any, freq_count: int, index: int):
|
|
71
|
+
if isinstance(value, torch.Tensor) and value.ndim > 0 and value.shape[0] == freq_count:
|
|
72
|
+
return value[index]
|
|
73
|
+
if isinstance(value, np.ndarray) and value.ndim > 0 and value.shape[0] == freq_count:
|
|
74
|
+
return value[index]
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _slice_monitor_samples(samples: Any, freq_count: int, index: int):
|
|
79
|
+
if isinstance(samples, torch.Tensor) and samples.ndim > 0 and samples.shape[0] == freq_count:
|
|
80
|
+
return int(samples[index].item())
|
|
81
|
+
if isinstance(samples, np.ndarray) and samples.ndim > 0 and samples.shape[0] == freq_count:
|
|
82
|
+
return int(samples[index])
|
|
83
|
+
if isinstance(samples, (list, tuple)) and len(samples) == freq_count:
|
|
84
|
+
return int(samples[index])
|
|
85
|
+
return samples
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _monitor_frequencies(payload: dict[str, Any]) -> tuple[float, ...]:
|
|
89
|
+
if "frequencies" in payload:
|
|
90
|
+
return tuple(float(freq) for freq in payload["frequencies"])
|
|
91
|
+
if "frequency" in payload:
|
|
92
|
+
return (float(payload["frequency"]),)
|
|
93
|
+
return ()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _select_monitor_frequency(payload: dict[str, Any], frequencies: tuple[float, ...], index: int) -> dict[str, Any]:
|
|
97
|
+
selected = dict(payload)
|
|
98
|
+
selected_frequency = frequencies[index]
|
|
99
|
+
selected["frequencies"] = (selected_frequency,)
|
|
100
|
+
selected["frequency"] = selected_frequency
|
|
101
|
+
|
|
102
|
+
if "samples" in payload:
|
|
103
|
+
selected["samples"] = _slice_monitor_samples(payload["samples"], len(frequencies), index)
|
|
104
|
+
if "data" in payload:
|
|
105
|
+
selected["data"] = _slice_frequency_axis(payload["data"], len(frequencies), index)
|
|
106
|
+
if "flux" in payload:
|
|
107
|
+
selected["flux"] = _slice_frequency_axis(payload["flux"], len(frequencies), index)
|
|
108
|
+
if "power" in payload:
|
|
109
|
+
selected["power"] = _slice_frequency_axis(payload["power"], len(frequencies), index)
|
|
110
|
+
if payload.get("power_delivered") is not None:
|
|
111
|
+
selected["power_delivered"] = _slice_frequency_axis(payload["power_delivered"], len(frequencies), index)
|
|
112
|
+
if payload.get("current_spectrum") is not None:
|
|
113
|
+
selected["current_spectrum"] = _slice_frequency_axis(payload["current_spectrum"], len(frequencies), index)
|
|
114
|
+
|
|
115
|
+
if _monitor_payload_is_point(payload):
|
|
116
|
+
components = {}
|
|
117
|
+
for component_name, component_value in payload.get("components", {}).items():
|
|
118
|
+
sliced = _slice_frequency_axis(component_value, len(frequencies), index)
|
|
119
|
+
components[component_name] = sliced
|
|
120
|
+
if component_name in payload:
|
|
121
|
+
selected[component_name] = _slice_frequency_axis(payload[component_name], len(frequencies), index)
|
|
122
|
+
selected["components"] = components
|
|
123
|
+
return selected
|
|
124
|
+
|
|
125
|
+
components = {}
|
|
126
|
+
for component_name, component_payload in payload.get("components", {}).items():
|
|
127
|
+
updated_payload = dict(component_payload)
|
|
128
|
+
updated_payload["data"] = _slice_frequency_axis(component_payload["data"], len(frequencies), index)
|
|
129
|
+
components[component_name] = updated_payload
|
|
130
|
+
if component_name in payload:
|
|
131
|
+
selected[component_name] = _slice_frequency_axis(payload[component_name], len(frequencies), index)
|
|
132
|
+
selected["components"] = components
|
|
133
|
+
return selected
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_FIELD_NORMAL_AXIS = {
|
|
137
|
+
"EX": 0,
|
|
138
|
+
"EY": 1,
|
|
139
|
+
"EZ": 2,
|
|
140
|
+
"HX": 0,
|
|
141
|
+
"HY": 1,
|
|
142
|
+
"HZ": 2,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _mirror_sign(component: str | None, symmetry: str, axis: int) -> int:
|
|
147
|
+
if component is None:
|
|
148
|
+
return 1
|
|
149
|
+
normal_axis = _FIELD_NORMAL_AXIS.get(component.upper())
|
|
150
|
+
if normal_axis is None:
|
|
151
|
+
return 1
|
|
152
|
+
is_normal = normal_axis == axis
|
|
153
|
+
if symmetry == "PEC":
|
|
154
|
+
return 1 if is_normal else -1
|
|
155
|
+
if symmetry == "PMC":
|
|
156
|
+
return -1 if is_normal else 1
|
|
157
|
+
return 1
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _expand_tensor_with_symmetry(tensor: torch.Tensor, scene, component: str | None = None) -> torch.Tensor:
|
|
161
|
+
expanded = tensor
|
|
162
|
+
half_sizes = (int(scene.Nx), int(scene.Ny), int(scene.Nz))
|
|
163
|
+
spatial_axis_offset = expanded.ndim - 3
|
|
164
|
+
if spatial_axis_offset < 0:
|
|
165
|
+
raise ValueError(f"Expected tensor with at least 3 dimensions, got shape {tuple(expanded.shape)}.")
|
|
166
|
+
for axis, entry in enumerate(getattr(scene, "symmetry", (None, None, None))):
|
|
167
|
+
if entry is None:
|
|
168
|
+
continue
|
|
169
|
+
mode, face = entry
|
|
170
|
+
tensor_axis = spatial_axis_offset + axis
|
|
171
|
+
half_size = half_sizes[axis]
|
|
172
|
+
dim_size = int(expanded.shape[tensor_axis])
|
|
173
|
+
# Components with a grid node on the symmetry plane have ``half_size``
|
|
174
|
+
# samples; that shared plane must not be duplicated when mirroring.
|
|
175
|
+
# Components staggered off the plane have ``half_size - 1`` samples and
|
|
176
|
+
# are mirrored in full. The shared node sits at index 0 for a low-face
|
|
177
|
+
# plane and at the last index for a high-face plane.
|
|
178
|
+
if dim_size == half_size:
|
|
179
|
+
if face == "low":
|
|
180
|
+
mirrored_source = expanded.narrow(tensor_axis, 1, max(dim_size - 1, 0))
|
|
181
|
+
else:
|
|
182
|
+
mirrored_source = expanded.narrow(tensor_axis, 0, max(dim_size - 1, 0))
|
|
183
|
+
elif dim_size == half_size - 1:
|
|
184
|
+
mirrored_source = expanded
|
|
185
|
+
else:
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"Cannot expand symmetry axis {axis} for tensor shape {tuple(expanded.shape)} and half-domain size {half_size}."
|
|
188
|
+
)
|
|
189
|
+
mirrored = torch.flip(mirrored_source, dims=(tensor_axis,))
|
|
190
|
+
sign = _mirror_sign(component, mode, axis)
|
|
191
|
+
if sign < 0:
|
|
192
|
+
mirrored = -mirrored
|
|
193
|
+
if face == "low":
|
|
194
|
+
expanded = torch.cat((mirrored, expanded), dim=tensor_axis)
|
|
195
|
+
else:
|
|
196
|
+
expanded = torch.cat((expanded, mirrored), dim=tensor_axis)
|
|
197
|
+
return expanded
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _monitor_payload_is_point(payload: dict[str, Any]) -> bool:
|
|
201
|
+
if "field_indices" in payload:
|
|
202
|
+
return True
|
|
203
|
+
if "axis" in payload:
|
|
204
|
+
return False
|
|
205
|
+
kind = payload.get("kind")
|
|
206
|
+
if kind is not None:
|
|
207
|
+
return kind == "point"
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _monitor_payload_is_mode(payload: dict[str, Any]) -> bool:
|
|
212
|
+
if payload.get("mode_spec") is not None:
|
|
213
|
+
return True
|
|
214
|
+
monitor_type = payload.get("monitor_type")
|
|
215
|
+
if monitor_type is not None:
|
|
216
|
+
return monitor_type == "mode"
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _monitor_payload_is_diffraction(payload: dict[str, Any]) -> bool:
|
|
221
|
+
return payload.get("monitor_type") == "diffraction"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _monitor_payload_is_dipole_emission(payload: dict[str, Any]) -> bool:
|
|
225
|
+
return payload.get("monitor_type") == "dipole_emission"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _monitor_payload_is_closed_surface(payload: dict[str, Any]) -> bool:
|
|
229
|
+
return payload.get("kind") == "closed_surface"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _find_scene_monitor(scene, name: str):
|
|
233
|
+
for monitor in getattr(scene, "monitors", ()):
|
|
234
|
+
if getattr(monitor, "name", None) == name:
|
|
235
|
+
return monitor
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _find_resolved_scene_monitor(scene, name: str):
|
|
240
|
+
if not hasattr(scene, "resolved_monitors"):
|
|
241
|
+
return None
|
|
242
|
+
for monitor in scene.resolved_monitors():
|
|
243
|
+
if getattr(monitor, "name", None) == name:
|
|
244
|
+
return monitor
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _plane_coord_names(axis: str) -> tuple[str, str]:
|
|
249
|
+
axis_name = str(axis).lower()
|
|
250
|
+
if axis_name == "x":
|
|
251
|
+
return "y", "z"
|
|
252
|
+
if axis_name == "y":
|
|
253
|
+
return "x", "z"
|
|
254
|
+
return "x", "y"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _select_coord_indices(coords, lower: float, upper: float):
|
|
258
|
+
if isinstance(coords, torch.Tensor):
|
|
259
|
+
coord_tensor = coords.to(dtype=coords.real.dtype)
|
|
260
|
+
tolerance = 1e-12 * max(
|
|
261
|
+
abs(lower),
|
|
262
|
+
abs(upper),
|
|
263
|
+
float(torch.max(torch.abs(coord_tensor)).item()),
|
|
264
|
+
1.0,
|
|
265
|
+
)
|
|
266
|
+
mask = (coord_tensor >= lower - tolerance) & (coord_tensor <= upper + tolerance)
|
|
267
|
+
indices = torch.nonzero(mask, as_tuple=False).reshape(-1)
|
|
268
|
+
if indices.numel() < 2:
|
|
269
|
+
raise ValueError(f"Finite monitor bounds [{lower}, {upper}] select fewer than two samples.")
|
|
270
|
+
if indices.numel() == coord_tensor.numel():
|
|
271
|
+
return None
|
|
272
|
+
return indices.to(device=coords.device)
|
|
273
|
+
|
|
274
|
+
coord_array = np.asarray(coords, dtype=float)
|
|
275
|
+
tolerance = 1e-12 * max(abs(lower), abs(upper), float(np.max(np.abs(coord_array))), 1.0)
|
|
276
|
+
indices = np.nonzero((coord_array >= lower - tolerance) & (coord_array <= upper + tolerance))[0]
|
|
277
|
+
if indices.size < 2:
|
|
278
|
+
raise ValueError(f"Finite monitor bounds [{lower}, {upper}] select fewer than two samples.")
|
|
279
|
+
if indices.size == coord_array.size:
|
|
280
|
+
return None
|
|
281
|
+
return indices
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _slice_plane_samples(values, u_indices, v_indices):
|
|
285
|
+
if isinstance(values, torch.Tensor):
|
|
286
|
+
sliced = values
|
|
287
|
+
if u_indices is not None:
|
|
288
|
+
if not isinstance(u_indices, torch.Tensor):
|
|
289
|
+
u_indices = torch.as_tensor(u_indices, device=values.device, dtype=torch.long)
|
|
290
|
+
sliced = sliced.index_select(sliced.ndim - 2, u_indices)
|
|
291
|
+
if v_indices is not None:
|
|
292
|
+
if not isinstance(v_indices, torch.Tensor):
|
|
293
|
+
v_indices = torch.as_tensor(v_indices, device=values.device, dtype=torch.long)
|
|
294
|
+
sliced = sliced.index_select(sliced.ndim - 1, v_indices)
|
|
295
|
+
return sliced
|
|
296
|
+
|
|
297
|
+
array = np.asarray(values)
|
|
298
|
+
if u_indices is not None:
|
|
299
|
+
array = np.take(array, u_indices, axis=array.ndim - 2)
|
|
300
|
+
if v_indices is not None:
|
|
301
|
+
array = np.take(array, v_indices, axis=array.ndim - 1)
|
|
302
|
+
return array
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _crop_plane_monitor_payload(payload: dict[str, Any], monitor: FinitePlaneMonitor) -> dict[str, Any]:
|
|
306
|
+
coord_u_name, coord_v_name = _plane_coord_names(monitor.axis)
|
|
307
|
+
if coord_u_name not in payload or coord_v_name not in payload:
|
|
308
|
+
return dict(payload)
|
|
309
|
+
|
|
310
|
+
u_indices = _select_coord_indices(payload[coord_u_name], *monitor.tangential_bounds[coord_u_name])
|
|
311
|
+
v_indices = _select_coord_indices(payload[coord_v_name], *monitor.tangential_bounds[coord_v_name])
|
|
312
|
+
if u_indices is None and v_indices is None:
|
|
313
|
+
selected = dict(payload)
|
|
314
|
+
selected["monitor_type"] = "finite_plane"
|
|
315
|
+
selected["center"] = monitor.position
|
|
316
|
+
selected["size"] = monitor.size
|
|
317
|
+
selected["tangential_bounds"] = dict(monitor.tangential_bounds)
|
|
318
|
+
if monitor.face_label is not None:
|
|
319
|
+
selected["face_label"] = monitor.face_label
|
|
320
|
+
if monitor.surface_name is not None:
|
|
321
|
+
selected["surface_name"] = monitor.surface_name
|
|
322
|
+
return selected
|
|
323
|
+
|
|
324
|
+
coord_u = payload[coord_u_name]
|
|
325
|
+
coord_v = payload[coord_v_name]
|
|
326
|
+
selected_u = coord_u if u_indices is None else (
|
|
327
|
+
coord_u.index_select(0, u_indices) if isinstance(coord_u, torch.Tensor) else np.asarray(coord_u)[u_indices]
|
|
328
|
+
)
|
|
329
|
+
selected_v = coord_v if v_indices is None else (
|
|
330
|
+
coord_v.index_select(0, v_indices) if isinstance(coord_v, torch.Tensor) else np.asarray(coord_v)[v_indices]
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
selected = dict(payload)
|
|
334
|
+
selected[coord_u_name] = selected_u
|
|
335
|
+
selected[coord_v_name] = selected_v
|
|
336
|
+
selected["coords"] = (selected_u, selected_v)
|
|
337
|
+
selected["monitor_type"] = "finite_plane"
|
|
338
|
+
selected["center"] = monitor.position
|
|
339
|
+
selected["size"] = monitor.size
|
|
340
|
+
selected["tangential_bounds"] = dict(monitor.tangential_bounds)
|
|
341
|
+
if monitor.face_label is not None:
|
|
342
|
+
selected["face_label"] = monitor.face_label
|
|
343
|
+
if monitor.surface_name is not None:
|
|
344
|
+
selected["surface_name"] = monitor.surface_name
|
|
345
|
+
|
|
346
|
+
if "data" in payload:
|
|
347
|
+
selected["data"] = _slice_plane_samples(payload["data"], u_indices, v_indices)
|
|
348
|
+
|
|
349
|
+
components = {}
|
|
350
|
+
for component_name, component_payload in payload.get("components", {}).items():
|
|
351
|
+
updated_component = dict(component_payload)
|
|
352
|
+
updated_component["data"] = _slice_plane_samples(component_payload["data"], u_indices, v_indices)
|
|
353
|
+
updated_component["coords"] = (selected_u, selected_v)
|
|
354
|
+
components[component_name] = updated_component
|
|
355
|
+
if component_name in payload:
|
|
356
|
+
selected[component_name] = _slice_plane_samples(payload[component_name], u_indices, v_indices)
|
|
357
|
+
selected["components"] = components
|
|
358
|
+
|
|
359
|
+
if selected.get("compute_flux"):
|
|
360
|
+
from .fdtd.observers import _compute_plane_flux
|
|
361
|
+
|
|
362
|
+
flux = _compute_plane_flux(selected)
|
|
363
|
+
selected["flux"] = flux
|
|
364
|
+
selected["power"] = flux
|
|
365
|
+
return selected
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _material_monitor_axis_indices(coord: torch.Tensor, bounds: tuple[float, float]) -> torch.Tensor:
|
|
369
|
+
lower, upper = float(bounds[0]), float(bounds[1])
|
|
370
|
+
center = 0.5 * (lower + upper)
|
|
371
|
+
if abs(upper - lower) <= 1e-12:
|
|
372
|
+
nearest = int(torch.argmin(torch.abs(coord - center)).item())
|
|
373
|
+
return torch.tensor([nearest], device=coord.device, dtype=torch.long)
|
|
374
|
+
scale = max(abs(lower), abs(upper), float(torch.max(torch.abs(coord)).item()), 1.0)
|
|
375
|
+
tolerance = 1e-9 * scale
|
|
376
|
+
mask = (coord >= lower - tolerance) & (coord <= upper + tolerance)
|
|
377
|
+
indices = torch.nonzero(mask, as_tuple=False).reshape(-1)
|
|
378
|
+
if indices.numel() == 0:
|
|
379
|
+
nearest = int(torch.argmin(torch.abs(coord - center)).item())
|
|
380
|
+
return torch.tensor([nearest], device=coord.device, dtype=torch.long)
|
|
381
|
+
return indices.to(device=coord.device, dtype=torch.long)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _crop_material_grid(tensor: torch.Tensor, ix: torch.Tensor, iy: torch.Tensor, iz: torch.Tensor) -> torch.Tensor:
|
|
385
|
+
return tensor.index_select(0, ix).index_select(1, iy).index_select(2, iz)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _stack_material_frequencies(items: list[torch.Tensor]) -> torch.Tensor:
|
|
389
|
+
if len(items) == 1:
|
|
390
|
+
return items[0]
|
|
391
|
+
return torch.stack(items, dim=0)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _resolve_material_monitor_frequencies(
|
|
395
|
+
result: "Result",
|
|
396
|
+
monitor,
|
|
397
|
+
*,
|
|
398
|
+
frequency,
|
|
399
|
+
freq_index,
|
|
400
|
+
) -> tuple[float, ...]:
|
|
401
|
+
if frequency is not None and freq_index is not None:
|
|
402
|
+
raise ValueError("Pass either frequency or freq_index, not both.")
|
|
403
|
+
if freq_index is not None:
|
|
404
|
+
index = _resolve_frequency_index(result.frequencies, freq_index=freq_index)
|
|
405
|
+
return (result.frequencies[index],)
|
|
406
|
+
if frequency is not None:
|
|
407
|
+
return (float(frequency),)
|
|
408
|
+
if monitor.frequencies is not None:
|
|
409
|
+
return tuple(float(freq) for freq in monitor.frequencies)
|
|
410
|
+
return tuple(result.frequencies)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _build_material_monitor_payload(result: "Result", monitor, *, frequency, freq_index):
|
|
414
|
+
prepared = result.prepared_scene
|
|
415
|
+
ix = _material_monitor_axis_indices(prepared.x, monitor.bounds[0])
|
|
416
|
+
iy = _material_monitor_axis_indices(prepared.y, monitor.bounds[1])
|
|
417
|
+
iz = _material_monitor_axis_indices(prepared.z, monitor.bounds[2])
|
|
418
|
+
|
|
419
|
+
eval_frequencies = _resolve_material_monitor_frequencies(
|
|
420
|
+
result,
|
|
421
|
+
monitor,
|
|
422
|
+
frequency=frequency,
|
|
423
|
+
freq_index=freq_index,
|
|
424
|
+
)
|
|
425
|
+
is_medium = isinstance(monitor, MediumMonitor)
|
|
426
|
+
|
|
427
|
+
eps_x_list, eps_y_list, eps_z_list = [], [], []
|
|
428
|
+
mu_x_list, mu_y_list, mu_z_list = [], [], []
|
|
429
|
+
for freq in eval_frequencies:
|
|
430
|
+
eps_components, mu_components = prepared.compile_material_components(frequency=freq)
|
|
431
|
+
eps_x_list.append(_crop_material_grid(eps_components["x"], ix, iy, iz))
|
|
432
|
+
eps_y_list.append(_crop_material_grid(eps_components["y"], ix, iy, iz))
|
|
433
|
+
eps_z_list.append(_crop_material_grid(eps_components["z"], ix, iy, iz))
|
|
434
|
+
if is_medium:
|
|
435
|
+
mu_x_list.append(_crop_material_grid(mu_components["x"], ix, iy, iz))
|
|
436
|
+
mu_y_list.append(_crop_material_grid(mu_components["y"], ix, iy, iz))
|
|
437
|
+
mu_z_list.append(_crop_material_grid(mu_components["z"], ix, iy, iz))
|
|
438
|
+
|
|
439
|
+
eps_x = _stack_material_frequencies(eps_x_list)
|
|
440
|
+
eps_y = _stack_material_frequencies(eps_y_list)
|
|
441
|
+
eps_z = _stack_material_frequencies(eps_z_list)
|
|
442
|
+
|
|
443
|
+
payload: dict[str, Any] = {
|
|
444
|
+
"kind": monitor.kind,
|
|
445
|
+
"monitor_type": monitor.kind,
|
|
446
|
+
"name": monitor.name,
|
|
447
|
+
"bounds": monitor.bounds,
|
|
448
|
+
"x": prepared.x.index_select(0, ix),
|
|
449
|
+
"y": prepared.y.index_select(0, iy),
|
|
450
|
+
"z": prepared.z.index_select(0, iz),
|
|
451
|
+
"eps_x": eps_x,
|
|
452
|
+
"eps_y": eps_y,
|
|
453
|
+
"eps_z": eps_z,
|
|
454
|
+
"eps": (eps_x + eps_y + eps_z) / 3.0,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if is_medium:
|
|
458
|
+
mu_x = _stack_material_frequencies(mu_x_list)
|
|
459
|
+
mu_y = _stack_material_frequencies(mu_y_list)
|
|
460
|
+
mu_z = _stack_material_frequencies(mu_z_list)
|
|
461
|
+
sigma_components = prepared.compile_materials()["sigma_e_components"]
|
|
462
|
+
sigma_x = _crop_material_grid(sigma_components["x"], ix, iy, iz)
|
|
463
|
+
sigma_y = _crop_material_grid(sigma_components["y"], ix, iy, iz)
|
|
464
|
+
sigma_z = _crop_material_grid(sigma_components["z"], ix, iy, iz)
|
|
465
|
+
# sigma_e is real and frequency-independent; broadcast it across evaluated frequencies.
|
|
466
|
+
sigma_x = _stack_material_frequencies([sigma_x] * len(eval_frequencies))
|
|
467
|
+
sigma_y = _stack_material_frequencies([sigma_y] * len(eval_frequencies))
|
|
468
|
+
sigma_z = _stack_material_frequencies([sigma_z] * len(eval_frequencies))
|
|
469
|
+
payload["mu_x"] = mu_x
|
|
470
|
+
payload["mu_y"] = mu_y
|
|
471
|
+
payload["mu_z"] = mu_z
|
|
472
|
+
payload["mu"] = (mu_x + mu_y + mu_z) / 3.0
|
|
473
|
+
payload["sigma_e_x"] = sigma_x
|
|
474
|
+
payload["sigma_e_y"] = sigma_y
|
|
475
|
+
payload["sigma_e_z"] = sigma_z
|
|
476
|
+
payload["sigma_e"] = (sigma_x + sigma_y + sigma_z) / 3.0
|
|
477
|
+
|
|
478
|
+
if len(eval_frequencies) == 1:
|
|
479
|
+
payload["frequency"] = eval_frequencies[0]
|
|
480
|
+
payload["frequencies"] = eval_frequencies
|
|
481
|
+
return payload
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _build_closed_surface_payload(result: "Result", monitor: ClosedSurfaceMonitor, *, frequency, freq_index):
|
|
485
|
+
faces = {}
|
|
486
|
+
for face in monitor.faces:
|
|
487
|
+
face_payload = result.raw_monitor(face.name, frequency=frequency, freq_index=freq_index)
|
|
488
|
+
faces[face.face_label or face.name] = face_payload
|
|
489
|
+
|
|
490
|
+
first_face = next(iter(faces.values()))
|
|
491
|
+
frequencies = tuple(first_face.get("frequencies", (first_face.get("frequency"),)))
|
|
492
|
+
payload = {
|
|
493
|
+
"kind": "closed_surface",
|
|
494
|
+
"monitor_type": "closed_surface",
|
|
495
|
+
"name": monitor.name,
|
|
496
|
+
"faces": faces,
|
|
497
|
+
"face_monitor_names": monitor.face_monitor_names,
|
|
498
|
+
"frequency": first_face.get("frequency"),
|
|
499
|
+
"frequencies": frequencies,
|
|
500
|
+
"bounds": monitor.bounds,
|
|
501
|
+
}
|
|
502
|
+
return payload
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@dataclass(frozen=True)
|
|
506
|
+
class ResultFieldAccessor:
|
|
507
|
+
selection: "ResultSelection"
|
|
508
|
+
family: str
|
|
509
|
+
|
|
510
|
+
@property
|
|
511
|
+
def x(self) -> torch.Tensor:
|
|
512
|
+
return self.selection.tensor(f"{self.family}x")
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def y(self) -> torch.Tensor:
|
|
516
|
+
return self.selection.tensor(f"{self.family}y")
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def z(self) -> torch.Tensor:
|
|
520
|
+
return self.selection.tensor(f"{self.family}z")
|
|
521
|
+
|
|
522
|
+
def as_dict(self) -> dict[str, torch.Tensor]:
|
|
523
|
+
tensors: dict[str, torch.Tensor] = {}
|
|
524
|
+
for axis in ("x", "y", "z"):
|
|
525
|
+
field_name = f"{self.family}{axis}".upper()
|
|
526
|
+
if field_name not in self.selection.result._fields:
|
|
527
|
+
continue
|
|
528
|
+
tensors[axis] = self.selection.tensor(field_name)
|
|
529
|
+
return tensors
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@dataclass(frozen=True)
|
|
533
|
+
class ResultMaterialTensorAccessor:
|
|
534
|
+
selection: "ResultSelection"
|
|
535
|
+
family: str
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def scalar(self) -> torch.Tensor:
|
|
539
|
+
suffix = "eps_r" if self.family == "eps" else "mu_r"
|
|
540
|
+
return self.selection.material(suffix)
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def r(self) -> torch.Tensor:
|
|
544
|
+
return self.scalar
|
|
545
|
+
|
|
546
|
+
@property
|
|
547
|
+
def x(self) -> torch.Tensor:
|
|
548
|
+
return self.selection.material(f"{self.family}_x")
|
|
549
|
+
|
|
550
|
+
@property
|
|
551
|
+
def y(self) -> torch.Tensor:
|
|
552
|
+
return self.selection.material(f"{self.family}_y")
|
|
553
|
+
|
|
554
|
+
@property
|
|
555
|
+
def z(self) -> torch.Tensor:
|
|
556
|
+
return self.selection.material(f"{self.family}_z")
|
|
557
|
+
|
|
558
|
+
def as_dict(self) -> dict[str, torch.Tensor]:
|
|
559
|
+
return {
|
|
560
|
+
"scalar": self.scalar,
|
|
561
|
+
"x": self.x,
|
|
562
|
+
"y": self.y,
|
|
563
|
+
"z": self.z,
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@dataclass(frozen=True)
|
|
568
|
+
class ResultMaterialsAccessor:
|
|
569
|
+
selection: "ResultSelection"
|
|
570
|
+
|
|
571
|
+
@property
|
|
572
|
+
def eps(self) -> ResultMaterialTensorAccessor:
|
|
573
|
+
return ResultMaterialTensorAccessor(self.selection, "eps")
|
|
574
|
+
|
|
575
|
+
@property
|
|
576
|
+
def mu(self) -> ResultMaterialTensorAccessor:
|
|
577
|
+
return ResultMaterialTensorAccessor(self.selection, "mu")
|
|
578
|
+
|
|
579
|
+
@property
|
|
580
|
+
def permittivity(self) -> ResultMaterialTensorAccessor:
|
|
581
|
+
return self.eps
|
|
582
|
+
|
|
583
|
+
@property
|
|
584
|
+
def permeability(self) -> ResultMaterialTensorAccessor:
|
|
585
|
+
return self.mu
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@dataclass(frozen=True)
|
|
589
|
+
class ResultSelection:
|
|
590
|
+
result: "Result"
|
|
591
|
+
frequency: float | None = None
|
|
592
|
+
freq_index: int | None = None
|
|
593
|
+
expand_symmetry: bool = False
|
|
594
|
+
resolve_modal: bool = True
|
|
595
|
+
|
|
596
|
+
def __post_init__(self):
|
|
597
|
+
if self.frequency is not None and self.freq_index is not None:
|
|
598
|
+
raise ValueError("Pass either frequency or freq_index, not both.")
|
|
599
|
+
|
|
600
|
+
@property
|
|
601
|
+
def E(self) -> ResultFieldAccessor:
|
|
602
|
+
return ResultFieldAccessor(self, "E")
|
|
603
|
+
|
|
604
|
+
@property
|
|
605
|
+
def H(self) -> ResultFieldAccessor:
|
|
606
|
+
return ResultFieldAccessor(self, "H")
|
|
607
|
+
|
|
608
|
+
@property
|
|
609
|
+
def materials(self) -> ResultMaterialsAccessor:
|
|
610
|
+
return ResultMaterialsAccessor(self)
|
|
611
|
+
|
|
612
|
+
def field(self, name: str = "E"):
|
|
613
|
+
return self.result.field(
|
|
614
|
+
name,
|
|
615
|
+
expand_symmetry=self.expand_symmetry,
|
|
616
|
+
frequency=self.frequency,
|
|
617
|
+
freq_index=self.freq_index,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
def tensor(self, name: str) -> torch.Tensor:
|
|
621
|
+
return self.result.tensor(
|
|
622
|
+
name,
|
|
623
|
+
expand_symmetry=self.expand_symmetry,
|
|
624
|
+
frequency=self.frequency,
|
|
625
|
+
freq_index=self.freq_index,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
def material(self, name: str = "eps_r") -> torch.Tensor:
|
|
629
|
+
return self.result.material(
|
|
630
|
+
name,
|
|
631
|
+
expand_symmetry=self.expand_symmetry,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
def raw_monitor(self, name: str):
|
|
635
|
+
return self.result.raw_monitor(
|
|
636
|
+
name,
|
|
637
|
+
frequency=self.frequency,
|
|
638
|
+
freq_index=self.freq_index,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
def monitor(self, name: str, *, resolve_modal: bool | None = None):
|
|
642
|
+
should_resolve_modal = self.resolve_modal if resolve_modal is None else bool(resolve_modal)
|
|
643
|
+
return self.result.monitor(
|
|
644
|
+
name,
|
|
645
|
+
frequency=self.frequency,
|
|
646
|
+
freq_index=self.freq_index,
|
|
647
|
+
resolve_modal=should_resolve_modal,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
def at(
|
|
651
|
+
self,
|
|
652
|
+
*,
|
|
653
|
+
frequency: float | None | object = _UNSET,
|
|
654
|
+
freq_index: int | None | object = _UNSET,
|
|
655
|
+
expand_symmetry: bool | object = _UNSET,
|
|
656
|
+
resolve_modal: bool | object = _UNSET,
|
|
657
|
+
) -> "ResultSelection":
|
|
658
|
+
selected_frequency = self.frequency if frequency is _UNSET else frequency
|
|
659
|
+
selected_freq_index = self.freq_index if freq_index is _UNSET else freq_index
|
|
660
|
+
selected_expand_symmetry = self.expand_symmetry if expand_symmetry is _UNSET else bool(expand_symmetry)
|
|
661
|
+
selected_resolve_modal = self.resolve_modal if resolve_modal is _UNSET else bool(resolve_modal)
|
|
662
|
+
return ResultSelection(
|
|
663
|
+
self.result,
|
|
664
|
+
frequency=selected_frequency,
|
|
665
|
+
freq_index=selected_freq_index,
|
|
666
|
+
expand_symmetry=selected_expand_symmetry,
|
|
667
|
+
resolve_modal=selected_resolve_modal,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
select = at
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@dataclass
|
|
674
|
+
class ResultPlotter:
|
|
675
|
+
result: "Result"
|
|
676
|
+
|
|
677
|
+
def field(self, axis: str = "z", position: float = 0.0, component: str = "abs", **kwargs):
|
|
678
|
+
solver = self.result.solver
|
|
679
|
+
if solver is None or not hasattr(solver, "plot_cross_section"):
|
|
680
|
+
raise RuntimeError("Plotting is only available for results created from a solver run.")
|
|
681
|
+
return solver.plot_cross_section(
|
|
682
|
+
axis=axis,
|
|
683
|
+
position=position,
|
|
684
|
+
component=component,
|
|
685
|
+
**kwargs,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
def material(
|
|
689
|
+
self,
|
|
690
|
+
name: str = "eps_r",
|
|
691
|
+
axis: str = "z",
|
|
692
|
+
position: float = 0.0,
|
|
693
|
+
figsize: tuple[int, int] = (8, 6),
|
|
694
|
+
cmap: str = "viridis",
|
|
695
|
+
):
|
|
696
|
+
material = self.result.material(name)
|
|
697
|
+
scene = self.result.prepared_scene
|
|
698
|
+
slice_info = extract_orthogonal_slice(
|
|
699
|
+
material.detach().cpu().numpy(),
|
|
700
|
+
axis,
|
|
701
|
+
position,
|
|
702
|
+
scene.x.detach().cpu().numpy(),
|
|
703
|
+
scene.y.detach().cpu().numpy(),
|
|
704
|
+
scene.z.detach().cpu().numpy(),
|
|
705
|
+
)
|
|
706
|
+
return plot_slice_image(
|
|
707
|
+
slice_info["slice"],
|
|
708
|
+
extent=slice_info["extent"],
|
|
709
|
+
xlabel=slice_info["xlabel"],
|
|
710
|
+
ylabel=slice_info["ylabel"],
|
|
711
|
+
title=f"{name} at {axis}={position:.3f}m",
|
|
712
|
+
colorbar_label=name,
|
|
713
|
+
figsize=figsize,
|
|
714
|
+
cmap=cmap,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
class Result:
|
|
719
|
+
def __init__(
|
|
720
|
+
self,
|
|
721
|
+
*,
|
|
722
|
+
method: str,
|
|
723
|
+
scene,
|
|
724
|
+
prepared_scene=None,
|
|
725
|
+
frequency: float | None = None,
|
|
726
|
+
frequencies=None,
|
|
727
|
+
solver=None,
|
|
728
|
+
fields: dict[str, torch.Tensor] | None = None,
|
|
729
|
+
monitors: dict[str, Any] | None = None,
|
|
730
|
+
metadata: dict[str, Any] | None = None,
|
|
731
|
+
solver_stats: dict[str, Any] | None = None,
|
|
732
|
+
raw_output: Any = None,
|
|
733
|
+
):
|
|
734
|
+
self.method = method
|
|
735
|
+
self.scene = scene
|
|
736
|
+
self._prepared_scene = prepared_scene
|
|
737
|
+
self.frequencies = _resolve_result_frequencies(frequency=frequency, frequencies=frequencies)
|
|
738
|
+
self.frequency = self.frequencies[0]
|
|
739
|
+
self.solver = solver
|
|
740
|
+
self._fields = _clone_mapping(fields)
|
|
741
|
+
self._monitors = _clone_mapping(monitors)
|
|
742
|
+
self._metadata = _clone_mapping(metadata)
|
|
743
|
+
self._solver_stats = _clone_mapping(solver_stats)
|
|
744
|
+
self.raw_output = raw_output
|
|
745
|
+
self.plot = ResultPlotter(self)
|
|
746
|
+
|
|
747
|
+
@property
|
|
748
|
+
def prepared_scene(self):
|
|
749
|
+
if self._prepared_scene is None:
|
|
750
|
+
self._prepared_scene = prepare_scene(self.scene)
|
|
751
|
+
return self._prepared_scene
|
|
752
|
+
|
|
753
|
+
def at(
|
|
754
|
+
self,
|
|
755
|
+
*,
|
|
756
|
+
frequency: float | None = None,
|
|
757
|
+
freq_index: int | None = None,
|
|
758
|
+
expand_symmetry: bool = False,
|
|
759
|
+
resolve_modal: bool = True,
|
|
760
|
+
) -> ResultSelection:
|
|
761
|
+
return ResultSelection(
|
|
762
|
+
self,
|
|
763
|
+
frequency=frequency,
|
|
764
|
+
freq_index=freq_index,
|
|
765
|
+
expand_symmetry=expand_symmetry,
|
|
766
|
+
resolve_modal=resolve_modal,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
select = at
|
|
770
|
+
|
|
771
|
+
@property
|
|
772
|
+
def fields(self) -> dict[str, torch.Tensor]:
|
|
773
|
+
return dict(self._fields)
|
|
774
|
+
|
|
775
|
+
@property
|
|
776
|
+
def monitors(self) -> dict[str, Any]:
|
|
777
|
+
return dict(self._monitors)
|
|
778
|
+
|
|
779
|
+
@property
|
|
780
|
+
def E(self) -> ResultFieldAccessor:
|
|
781
|
+
return self.at().E
|
|
782
|
+
|
|
783
|
+
@property
|
|
784
|
+
def H(self) -> ResultFieldAccessor:
|
|
785
|
+
return self.at().H
|
|
786
|
+
|
|
787
|
+
@property
|
|
788
|
+
def materials(self) -> ResultMaterialsAccessor:
|
|
789
|
+
return self.at().materials
|
|
790
|
+
|
|
791
|
+
def field(
|
|
792
|
+
self,
|
|
793
|
+
name: str = "E",
|
|
794
|
+
*,
|
|
795
|
+
expand_symmetry: bool = False,
|
|
796
|
+
frequency: float | None = None,
|
|
797
|
+
freq_index: int | None = None,
|
|
798
|
+
):
|
|
799
|
+
key = name.upper()
|
|
800
|
+
if key in {"E", "H"}:
|
|
801
|
+
component_names = tuple(field_name for field_name in (f"{key}X", f"{key}Y", f"{key}Z") if field_name in self._fields)
|
|
802
|
+
if not expand_symmetry:
|
|
803
|
+
return {
|
|
804
|
+
field_name: self.tensor(field_name, frequency=frequency, freq_index=freq_index)
|
|
805
|
+
for field_name in component_names
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
field_name: self.tensor(
|
|
809
|
+
field_name,
|
|
810
|
+
expand_symmetry=True,
|
|
811
|
+
frequency=frequency,
|
|
812
|
+
freq_index=freq_index,
|
|
813
|
+
)
|
|
814
|
+
for field_name in component_names
|
|
815
|
+
}
|
|
816
|
+
return self.tensor(key, expand_symmetry=expand_symmetry, frequency=frequency, freq_index=freq_index)
|
|
817
|
+
|
|
818
|
+
def tensor(
|
|
819
|
+
self,
|
|
820
|
+
name: str,
|
|
821
|
+
*,
|
|
822
|
+
expand_symmetry: bool = False,
|
|
823
|
+
frequency: float | None = None,
|
|
824
|
+
freq_index: int | None = None,
|
|
825
|
+
) -> torch.Tensor:
|
|
826
|
+
key = name.upper()
|
|
827
|
+
if key not in self._fields:
|
|
828
|
+
raise KeyError(f"Field {name!r} is not available in this result.")
|
|
829
|
+
tensor = self._fields[key]
|
|
830
|
+
selected_index = _resolve_frequency_index(self.frequencies, frequency=frequency, freq_index=freq_index)
|
|
831
|
+
if selected_index is not None and len(self.frequencies) > 1 and tensor.ndim > 0 and tensor.shape[0] == len(self.frequencies):
|
|
832
|
+
tensor = tensor[selected_index]
|
|
833
|
+
if not expand_symmetry:
|
|
834
|
+
return tensor
|
|
835
|
+
return _expand_tensor_with_symmetry(tensor, self.prepared_scene, component=key)
|
|
836
|
+
|
|
837
|
+
def raw_monitor(self, name: str, *, frequency: float | None = None, freq_index: int | None = None):
|
|
838
|
+
public_monitor = _find_scene_monitor(self.scene, name)
|
|
839
|
+
if isinstance(public_monitor, ClosedSurfaceMonitor):
|
|
840
|
+
return _build_closed_surface_payload(self, public_monitor, frequency=frequency, freq_index=freq_index)
|
|
841
|
+
if isinstance(public_monitor, (PermittivityMonitor, MediumMonitor)):
|
|
842
|
+
return _build_material_monitor_payload(self, public_monitor, frequency=frequency, freq_index=freq_index)
|
|
843
|
+
|
|
844
|
+
if name not in self._monitors:
|
|
845
|
+
raise KeyError(f"Monitor {name!r} is not available in this result.")
|
|
846
|
+
|
|
847
|
+
payload = self._monitors[name]
|
|
848
|
+
monitor_frequencies = _monitor_frequencies(payload)
|
|
849
|
+
selected_index = _resolve_frequency_index(
|
|
850
|
+
monitor_frequencies,
|
|
851
|
+
frequency=frequency,
|
|
852
|
+
freq_index=freq_index,
|
|
853
|
+
)
|
|
854
|
+
if selected_index is None or len(monitor_frequencies) <= 1:
|
|
855
|
+
selected = dict(payload)
|
|
856
|
+
else:
|
|
857
|
+
selected = _select_monitor_frequency(payload, monitor_frequencies, selected_index)
|
|
858
|
+
|
|
859
|
+
resolved_monitor = _find_resolved_scene_monitor(self.scene, name)
|
|
860
|
+
if isinstance(resolved_monitor, FinitePlaneMonitor):
|
|
861
|
+
return _crop_plane_monitor_payload(selected, resolved_monitor)
|
|
862
|
+
return selected
|
|
863
|
+
|
|
864
|
+
def monitor(
|
|
865
|
+
self,
|
|
866
|
+
name: str,
|
|
867
|
+
*,
|
|
868
|
+
frequency: float | None = None,
|
|
869
|
+
freq_index: int | None = None,
|
|
870
|
+
resolve_modal: bool = True,
|
|
871
|
+
):
|
|
872
|
+
payload = self.raw_monitor(name, frequency=frequency, freq_index=freq_index)
|
|
873
|
+
if not resolve_modal or _monitor_payload_is_closed_surface(payload):
|
|
874
|
+
return payload
|
|
875
|
+
|
|
876
|
+
if _monitor_payload_is_diffraction(payload):
|
|
877
|
+
from .postprocess.diffraction import compute_diffraction_from_payload
|
|
878
|
+
|
|
879
|
+
return compute_diffraction_from_payload(self, name, payload)
|
|
880
|
+
|
|
881
|
+
if _monitor_payload_is_dipole_emission(payload):
|
|
882
|
+
emission = {
|
|
883
|
+
"kind": "dipole_emission",
|
|
884
|
+
"monitor_type": "dipole_emission",
|
|
885
|
+
"power_delivered": payload.get("power_delivered"),
|
|
886
|
+
"current_spectrum": payload.get("current_spectrum"),
|
|
887
|
+
"frequencies": tuple(payload.get("frequencies", (payload.get("frequency"),))),
|
|
888
|
+
"frequency": payload.get("frequency"),
|
|
889
|
+
"polarization": payload.get("dipole_polarization"),
|
|
890
|
+
"position": payload.get("dipole_position", payload.get("position")),
|
|
891
|
+
"source_name": payload.get("source_name"),
|
|
892
|
+
# The Purcell factor requires a vacuum reference run; combine two
|
|
893
|
+
# runs with postprocess.purcell_factor(structured, vacuum, name).
|
|
894
|
+
"purcell_factor": None,
|
|
895
|
+
}
|
|
896
|
+
return emission
|
|
897
|
+
|
|
898
|
+
if not _monitor_payload_is_mode(payload):
|
|
899
|
+
return payload
|
|
900
|
+
|
|
901
|
+
from .postprocess.modal import _compute_mode_overlap_from_payload
|
|
902
|
+
|
|
903
|
+
modal = _compute_mode_overlap_from_payload(
|
|
904
|
+
self,
|
|
905
|
+
name,
|
|
906
|
+
payload,
|
|
907
|
+
mode_source=None,
|
|
908
|
+
direction=None,
|
|
909
|
+
)
|
|
910
|
+
modal["kind"] = "mode"
|
|
911
|
+
modal["fields"] = tuple(payload.get("fields", ()))
|
|
912
|
+
modal["frequencies"] = tuple(payload.get("frequencies", (payload.get("frequency"),)))
|
|
913
|
+
modal["frequency"] = payload.get("frequency")
|
|
914
|
+
modal["normal_direction"] = payload.get("normal_direction", "+")
|
|
915
|
+
modal["plane"] = payload
|
|
916
|
+
return modal
|
|
917
|
+
|
|
918
|
+
def material(self, name: str = "eps_r", *, expand_symmetry: bool = False) -> torch.Tensor:
|
|
919
|
+
prepared_scene = self.prepared_scene
|
|
920
|
+
key = name.lower()
|
|
921
|
+
if key in {"eps", "eps_r", "permittivity"}:
|
|
922
|
+
tensor = prepared_scene.permittivity
|
|
923
|
+
elif key in {"eps_x", "epsilon_x", "permittivity_x"}:
|
|
924
|
+
tensor = prepared_scene.permittivity_components["x"]
|
|
925
|
+
elif key in {"eps_y", "epsilon_y", "permittivity_y"}:
|
|
926
|
+
tensor = prepared_scene.permittivity_components["y"]
|
|
927
|
+
elif key in {"eps_z", "epsilon_z", "permittivity_z"}:
|
|
928
|
+
tensor = prepared_scene.permittivity_components["z"]
|
|
929
|
+
elif key in {"mu", "mu_r", "permeability"}:
|
|
930
|
+
tensor = prepared_scene.permeability
|
|
931
|
+
elif key in {"mu_x", "permeability_x"}:
|
|
932
|
+
tensor = prepared_scene.permeability_components["x"]
|
|
933
|
+
elif key in {"mu_y", "permeability_y"}:
|
|
934
|
+
tensor = prepared_scene.permeability_components["y"]
|
|
935
|
+
elif key in {"mu_z", "permeability_z"}:
|
|
936
|
+
tensor = prepared_scene.permeability_components["z"]
|
|
937
|
+
else:
|
|
938
|
+
raise KeyError(f"Material {name!r} is not supported.")
|
|
939
|
+
if not expand_symmetry:
|
|
940
|
+
return tensor
|
|
941
|
+
return _expand_tensor_with_symmetry(tensor, prepared_scene, component=None)
|
|
942
|
+
|
|
943
|
+
def stats(self) -> dict[str, Any]:
|
|
944
|
+
stats = dict(self._solver_stats)
|
|
945
|
+
stats["method"] = self.method
|
|
946
|
+
stats["frequency"] = self.frequency
|
|
947
|
+
stats["frequencies"] = self.frequencies
|
|
948
|
+
stats["num_frequencies"] = len(self.frequencies)
|
|
949
|
+
stats["num_fields"] = len(self._fields)
|
|
950
|
+
stats["num_monitors"] = len(self._monitors)
|
|
951
|
+
return stats
|
|
952
|
+
|
|
953
|
+
def save(self, path: str | Path):
|
|
954
|
+
output_path = Path(path)
|
|
955
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
956
|
+
torch.save(
|
|
957
|
+
{
|
|
958
|
+
"method": self.method,
|
|
959
|
+
"frequency": self.frequency,
|
|
960
|
+
"frequencies": self.frequencies,
|
|
961
|
+
"fields": {name: tensor.detach().cpu() for name, tensor in self._fields.items()},
|
|
962
|
+
"monitors": _cpu_serializable(self._monitors),
|
|
963
|
+
"metadata": self._metadata,
|
|
964
|
+
"solver_stats": self._solver_stats,
|
|
965
|
+
},
|
|
966
|
+
output_path,
|
|
967
|
+
)
|