xnft 0.1.0__py3-none-any.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.
- xnft/__init__.py +46 -0
- xnft/algorithms/__init__.py +0 -0
- xnft/algorithms/planar_near_field_transform.py +455 -0
- xnft/antennas/aperture/__init__.py +0 -0
- xnft/antennas/aperture/rectangular.py +231 -0
- xnft/antennas/probes/__init__.py +0 -0
- xnft/antennas/probes/probes.py +121 -0
- xnft/antennas/probes/standard_probes.py +42 -0
- xnft/antennas/wire/__init__.py +0 -0
- xnft/antennas/wire/dipole.py +192 -0
- xnft/calibration/gain.py +207 -0
- xnft/common/__init__.py +6 -0
- xnft/common/enums.py +132 -0
- xnft/common/plugins.py +25 -0
- xnft/common/registry.py +325 -0
- xnft/common/types.py +223 -0
- xnft/gui/__init__.py +0 -0
- xnft/gui/app.py +597 -0
- xnft/gui/dialogs/__init__.py +0 -0
- xnft/gui/dialogs/analysis/__init__.py +0 -0
- xnft/gui/dialogs/analysis/new_planar_nft_dialog.py +326 -0
- xnft/gui/dialogs/calibrations/__init__.py +0 -0
- xnft/gui/dialogs/calibrations/new_gain_calibration_dialog.py +250 -0
- xnft/gui/dialogs/common.py +169 -0
- xnft/gui/dialogs/export_data_dialog.py +239 -0
- xnft/gui/dialogs/export_data_directory_dialog.py +204 -0
- xnft/gui/dialogs/fields/__init__.py +0 -0
- xnft/gui/dialogs/fields/new_field_measurement_dialog.py +554 -0
- xnft/gui/dialogs/probes/__init__.py +0 -0
- xnft/gui/dialogs/probes/new_probe_dialog.py +327 -0
- xnft/gui/dialogs/project/__init__.py +0 -0
- xnft/gui/dialogs/project/new_project_dialog.py +152 -0
- xnft/gui/dialogs/reports/__init__.py +0 -0
- xnft/gui/dialogs/reports/new_field_report_dialog.py +198 -0
- xnft/gui/dialogs/reports/new_trace_dialog.py +728 -0
- xnft/gui/models/__init__.py +0 -0
- xnft/gui/models/project_tree_model.py +323 -0
- xnft/gui/tasks/__init__.py +0 -0
- xnft/gui/tasks/bridge.py +60 -0
- xnft/gui/tasks/worker.py +40 -0
- xnft/gui/utils.py +81 -0
- xnft/gui/views/__init__.py +0 -0
- xnft/gui/views/mdi_area.py +159 -0
- xnft/gui/views/project_tree_view.py +172 -0
- xnft/loaders/__init__.py +19 -0
- xnft/loaders/fields/__init__.py +0 -0
- xnft/loaders/fields/analytic.py +261 -0
- xnft/loaders/fields/base.py +15 -0
- xnft/loaders/fields/hfss.py +337 -0
- xnft/loaders/fields/nsi.py +5 -0
- xnft/loaders/gain/.gitkeep +0 -0
- xnft/loaders/probes/.gitkeep +0 -0
- xnft/project/__init__.py +0 -0
- xnft/project/io.py +92 -0
- xnft/project/manager.py +41 -0
- xnft/project/nodes/__init__.py +0 -0
- xnft/project/nodes/analysis.py +220 -0
- xnft/project/nodes/calibrations.py +114 -0
- xnft/project/nodes/coordinates.py +80 -0
- xnft/project/nodes/fields.py +219 -0
- xnft/project/nodes/model.py +34 -0
- xnft/project/nodes/node.py +568 -0
- xnft/project/nodes/probes.py +105 -0
- xnft/project/nodes/project.py +101 -0
- xnft/project/nodes/reports.py +377 -0
- xnft/reports/__init__.py +7 -0
- xnft/reports/data_writers.py +45 -0
- xnft/reports/plot/functions/domains.py +42 -0
- xnft/reports/plot/functions/field_components.py +202 -0
- xnft/reports/plot/functions/field_quantities.py +62 -0
- xnft/reports/plot/functions/functions.py +89 -0
- xnft/reports/plot/functions/normalizations.py +43 -0
- xnft/reports/plot/functions/scales.py +49 -0
- xnft/reports/plot/helpers/coordinate_transforms.py +34 -0
- xnft/reports/plot/helpers/helpers.py +71 -0
- xnft/reports/plot/helpers/trace_parameters.py +49 -0
- xnft/reports/plot/line_plot.py +525 -0
- xnft/reports/plot/surface_plot.py +846 -0
- xnft/reports/table/.gitkeep +0 -0
- xnft/resources/calibration/NSI-MI_ANT-SGA-26.5-40.cal +21 -0
- xnft/resources/icons/array.svg +26 -0
- xnft/resources/icons/calibration.svg +36 -0
- xnft/resources/icons/calibration2.svg +33 -0
- xnft/resources/icons/coordinates.svg +69 -0
- xnft/resources/icons/fields.svg +23 -0
- xnft/resources/icons/function.svg +1 -0
- xnft/resources/icons/model.svg +21 -0
- xnft/resources/icons/plot.svg +32 -0
- xnft/resources/icons/probe.svg +21 -0
- xnft/resources/icons/xnft.icns +0 -0
- xnft/resources/icons/xnft.ico +0 -0
- xnft/resources/icons/xnft.iconset/icon_128x128.png +0 -0
- xnft/resources/icons/xnft.iconset/icon_128x128@2x.png +0 -0
- xnft/resources/icons/xnft.iconset/icon_16x16.png +0 -0
- xnft/resources/icons/xnft.iconset/icon_16x16@2x.png +0 -0
- xnft/resources/icons/xnft.iconset/icon_256x256.png +0 -0
- xnft/resources/icons/xnft.iconset/icon_256x256@2x.png +0 -0
- xnft/resources/icons/xnft.iconset/icon_32x32.png +0 -0
- xnft/resources/icons/xnft.iconset/icon_32x32@2x.png +0 -0
- xnft/resources/icons/xnft.iconset/icon_512x512.png +0 -0
- xnft/resources/icons/xnft.iconset/icon_512x512@2x.png +0 -0
- xnft/resources/icons/xnft.png +0 -0
- xnft/resources/icons/xnft.svg +30 -0
- xnft/resources/icons/xnft_flat.png +0 -0
- xnft/resources/icons/xnft_flat_192.png +0 -0
- xnft/resources/logging.all.yaml +19 -0
- xnft/resources/logging.dev.yaml +19 -0
- xnft/resources/logos/emrg_logo.svg +194 -0
- xnft/resources/logos/emrg_logo_vector.svg +33 -0
- xnft/resources/logos/lemma_logo.png +0 -0
- xnft/resources/logos/lemma_logo.svg +1 -0
- xnft/resources/logos/lemma_logo_transparent.png +0 -0
- xnft/resources/logos/lemma_logo_v2.png +0 -0
- xnft/resources/logos/lemma_logo_v2.svg +8 -0
- xnft/resources/paths.py +13 -0
- xnft-0.1.0.dist-info/METADATA +241 -0
- xnft-0.1.0.dist-info/RECORD +120 -0
- xnft-0.1.0.dist-info/WHEEL +4 -0
- xnft-0.1.0.dist-info/entry_points.txt +2 -0
- xnft-0.1.0.dist-info/licenses/LICENSE +9 -0
xnft/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xNFT - Antenna Measurement Software
|
|
3
|
+
|
|
4
|
+
Copyright 2025-2026 LEmmA, Electromagnetics Research Group, Michigan State University
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import logging.config
|
|
9
|
+
import sys
|
|
10
|
+
from importlib.metadata import version
|
|
11
|
+
from importlib.resources import files
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
17
|
+
|
|
18
|
+
__version__ = version("xnft")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def configure_logging(config_name="logging.all.yaml"):
|
|
22
|
+
|
|
23
|
+
# Load base config
|
|
24
|
+
config_path = files("xnft.resources") / config_name
|
|
25
|
+
with config_path.open("r", encoding="utf-8") as f:
|
|
26
|
+
config = yaml.safe_load(f)
|
|
27
|
+
|
|
28
|
+
if getattr(sys, "frozen", False):
|
|
29
|
+
log_dir = Path.home() / ".xnft"
|
|
30
|
+
log_dir.mkdir(exist_ok=True)
|
|
31
|
+
log_file = log_dir / "xnft.log"
|
|
32
|
+
|
|
33
|
+
config["handlers"]["file"] = {
|
|
34
|
+
"class": "logging.FileHandler",
|
|
35
|
+
"level": "DEBUG",
|
|
36
|
+
"filename": str(log_file),
|
|
37
|
+
"formatter": "dev",
|
|
38
|
+
"encoding": "utf-8",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
config["root"]["handlers"].append("file")
|
|
42
|
+
|
|
43
|
+
logging.config.dictConfig(config)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
configure_logging()
|
|
File without changes
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Field transform algorithms.
|
|
3
|
+
|
|
4
|
+
Copyright 2025-2026 LEmmA, Electromagnetics Research Group, Michigan State University.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
import warnings
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
import finufft
|
|
15
|
+
import numpy as np
|
|
16
|
+
import scipy.constants as spc
|
|
17
|
+
import xarray as xr
|
|
18
|
+
from quantiphy import Quantity
|
|
19
|
+
from scipy.signal import windows
|
|
20
|
+
|
|
21
|
+
from ..antennas.probes.probes import MeasurementProbe
|
|
22
|
+
|
|
23
|
+
# from numpy.typing import ArrayLike
|
|
24
|
+
from ..common.enums import CoordinateSystemType, FieldType
|
|
25
|
+
from ..common.registry import registry
|
|
26
|
+
from ..common.types import FieldDataset
|
|
27
|
+
from ..project.nodes.calibrations import GainCalibrationNode
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def calibrate_gain(
|
|
33
|
+
field: xr.Dataset, gain_calibration: GainCalibrationNode
|
|
34
|
+
) -> xr.Dataset:
|
|
35
|
+
"""Apply gain calibrations to field."""
|
|
36
|
+
result_field = field.copy(deep=True)
|
|
37
|
+
|
|
38
|
+
gain_cal_table = gain_calibration.gain_cal_ref.data
|
|
39
|
+
gain_cal_meas_field = gain_calibration.gain_cal_field.data
|
|
40
|
+
|
|
41
|
+
logger.debug(f"{gain_cal_table=}")
|
|
42
|
+
|
|
43
|
+
# Configure gain cal interpolation across frequencies
|
|
44
|
+
gain_reference_interp = gain_cal_table.interp(
|
|
45
|
+
frequency=field.frequency, # NOTE: frequency may be a vector of frequencies
|
|
46
|
+
method="linear",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Take gain of measured aut field over phi/theta dimensions (max per frequency)
|
|
50
|
+
gain_meas_aut = 10 * np.log10(
|
|
51
|
+
((np.abs(field["E"]) ** 2).sum(dim="component")).max(dim=("theta", "phi"))
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
logger.debug(f"{gain_meas_aut=}")
|
|
55
|
+
|
|
56
|
+
# Take gain of reference antenna measured field over phi/theta dimensions (max per frequency)
|
|
57
|
+
gain_meas_ref = 10 * np.log10(
|
|
58
|
+
((np.abs(gain_cal_meas_field["E"]) ** 2).sum(dim="component")).max(
|
|
59
|
+
dim=("theta", "phi")
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
logger.debug(f"{gain_meas_ref=}")
|
|
64
|
+
|
|
65
|
+
# Correction value to add to our AUT measured field (in dB)
|
|
66
|
+
gain_correction_db = gain_reference_interp - gain_meas_ref
|
|
67
|
+
logger.debug(f"{gain_correction_db=}")
|
|
68
|
+
|
|
69
|
+
gain_correction = 10 ** (gain_correction_db / 20)
|
|
70
|
+
|
|
71
|
+
# logger.debug(f"{gain_correction=}")
|
|
72
|
+
|
|
73
|
+
# Convert from power gain back to linear amplitude gain
|
|
74
|
+
result_field["E"] = field["E"] * gain_correction
|
|
75
|
+
|
|
76
|
+
logger.debug(f"{result_field=}")
|
|
77
|
+
|
|
78
|
+
return result_field
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def planar_near_field_transform_t3(
|
|
82
|
+
theta_grid_target,
|
|
83
|
+
phi_grid_target,
|
|
84
|
+
k0,
|
|
85
|
+
win,
|
|
86
|
+
near_field_x_scaled,
|
|
87
|
+
near_field_y_scaled,
|
|
88
|
+
x_scale,
|
|
89
|
+
y_scale,
|
|
90
|
+
e_x,
|
|
91
|
+
e_y,
|
|
92
|
+
z_val,
|
|
93
|
+
r_far_field: float | None = None,
|
|
94
|
+
probe_correction: MeasurementProbe | None = None,
|
|
95
|
+
):
|
|
96
|
+
r"""
|
|
97
|
+
Compute the planar near-to-far field transform for a single frequency
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
theta_grid_target : ArrayLike
|
|
102
|
+
2D grid of theta sample points for output.
|
|
103
|
+
phi_grid_target : ArrayLike
|
|
104
|
+
2D phi of theta sample points for output.
|
|
105
|
+
k0 : float
|
|
106
|
+
Wavenumber to compute output for
|
|
107
|
+
win : ArrayLike
|
|
108
|
+
2D windowing function to apply to input field.
|
|
109
|
+
near_field_x_scaled : ArrayLike
|
|
110
|
+
Displacement along x-axis, normalized from -pi/2,pi/2 for NUFFT
|
|
111
|
+
near_field_y_scaled : ArrayLike
|
|
112
|
+
Displacement along y-axis, normalized from -pi/2,pi/2 for NUFFT
|
|
113
|
+
x_scale : float
|
|
114
|
+
Scaling factor for x-component for NUFFT
|
|
115
|
+
y_scale : float
|
|
116
|
+
Scaling factor for x-component for NUFFT
|
|
117
|
+
e_x : ArrayLike
|
|
118
|
+
E-field x-component
|
|
119
|
+
e_y : ArrayLike
|
|
120
|
+
E-field y-component
|
|
121
|
+
z_val : float
|
|
122
|
+
Z-height above AUT samples collected at (in meters)
|
|
123
|
+
r_far_field : float or None, optional
|
|
124
|
+
Far-field radius compute transform at, by default None
|
|
125
|
+
probe_correction : MeasurementProbe or None, optional
|
|
126
|
+
Probe correction fields to use, if any, by default None
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
Tuple[ArrayLike]
|
|
131
|
+
Tuple of $E_\\mathrm{phi}$ and $E_\\mathrm{theta}$ fields
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
# TODO: Add a type-2 NUFFT implementation for improved performance (GPU won't be effective with type-3)
|
|
135
|
+
|
|
136
|
+
# Create target sample locations in k-space based on sample points in
|
|
137
|
+
# (theta, phi)
|
|
138
|
+
kx_target = k0 * np.sin(theta_grid_target) * np.cos(phi_grid_target)
|
|
139
|
+
ky_target = k0 * np.sin(theta_grid_target) * np.sin(phi_grid_target)
|
|
140
|
+
|
|
141
|
+
kx_target_scaled = (kx_target / x_scale).ravel()
|
|
142
|
+
ky_target_scaled = (ky_target / y_scale).ravel()
|
|
143
|
+
|
|
144
|
+
# Compute NUFFT
|
|
145
|
+
precision = 1e-12
|
|
146
|
+
nthreads = 0
|
|
147
|
+
plan = finufft.Plan(
|
|
148
|
+
nufft_type=3,
|
|
149
|
+
n_modes_or_dim=2,
|
|
150
|
+
eps=precision,
|
|
151
|
+
nthreads=nthreads,
|
|
152
|
+
dtype="complex128",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
plan.setpts(
|
|
156
|
+
x=near_field_x_scaled.astype(np.float64),
|
|
157
|
+
y=near_field_y_scaled.astype(np.float64),
|
|
158
|
+
s=kx_target_scaled.astype(np.float64),
|
|
159
|
+
t=ky_target_scaled.astype(np.float64),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
field_values_x = (win * e_x).ravel().astype(np.complex128)
|
|
163
|
+
field_values_y = (win * e_y).ravel().astype(np.complex128)
|
|
164
|
+
|
|
165
|
+
target_shape = theta_grid_target.shape
|
|
166
|
+
fe_x = plan.execute(field_values_x).reshape(target_shape)
|
|
167
|
+
fe_y = plan.execute(field_values_y).reshape(target_shape)
|
|
168
|
+
|
|
169
|
+
if probe_correction:
|
|
170
|
+
# Apply corrections in k-space
|
|
171
|
+
fe_mat = np.stack([fe_x, fe_y], axis=-1)
|
|
172
|
+
|
|
173
|
+
# reception_mat_inv is in (theta, phi)
|
|
174
|
+
reception_mat_inv = probe_correction.get_reception_matrix(
|
|
175
|
+
theta_vec=theta_grid_target[:, 0],
|
|
176
|
+
phi_vec=phi_grid_target[0, :],
|
|
177
|
+
wavenumber=k0,
|
|
178
|
+
)[0] # remove frequency axis [0] that is always of size 1
|
|
179
|
+
|
|
180
|
+
# logger.debug(f"{reception_mat_inv.shape=}")
|
|
181
|
+
# logger.debug(f"{fe_mat.shape=}")
|
|
182
|
+
|
|
183
|
+
# Perform matrix multiplication of R^-1 matrix and E-field
|
|
184
|
+
# Probe compensation matrix include transform to spherical
|
|
185
|
+
e_mat = (reception_mat_inv @ fe_mat[..., np.newaxis])[..., 0]
|
|
186
|
+
|
|
187
|
+
# Add propagation phase to probe
|
|
188
|
+
# TODO: make this correction optional
|
|
189
|
+
e_mat *= np.exp(1j * k0 * z_val * np.cos(theta_grid_target[..., np.newaxis]))
|
|
190
|
+
|
|
191
|
+
else:
|
|
192
|
+
e_theta = fe_x * np.cos(phi_grid_target) + fe_y * np.sin(phi_grid_target)
|
|
193
|
+
e_phi = np.cos(theta_grid_target) * (
|
|
194
|
+
-fe_x * np.sin(phi_grid_target) + fe_y * np.cos(phi_grid_target)
|
|
195
|
+
)
|
|
196
|
+
e_mat = np.stack([e_theta, e_phi], axis=-1)
|
|
197
|
+
|
|
198
|
+
# Apply propagation constants
|
|
199
|
+
# Phase correction term such that phase at measured radius is constant
|
|
200
|
+
if r_far_field is not None:
|
|
201
|
+
greens_func = np.exp(-1j * k0 * r_far_field) / r_far_field
|
|
202
|
+
c0 = 1j * k0 / (2.0 * np.pi) * greens_func
|
|
203
|
+
e_mat *= c0
|
|
204
|
+
|
|
205
|
+
return e_mat
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def planar_near_field_transform(
|
|
209
|
+
near_field: xr.Dataset,
|
|
210
|
+
frequencies: np.ndarray | list[float] | None = None,
|
|
211
|
+
probe_correction: MeasurementProbe | None = None,
|
|
212
|
+
gain_calibration: GainCalibrationNode | None = None,
|
|
213
|
+
dphi: float = 1.0,
|
|
214
|
+
dtheta: float = 1.0,
|
|
215
|
+
window: Callable = windows.boxcar,
|
|
216
|
+
r_far_field=None, # meters
|
|
217
|
+
z_displacement=None,
|
|
218
|
+
) -> xr.Dataset:
|
|
219
|
+
"""
|
|
220
|
+
Compute the near to far-field transform via non-uniform fast Fourier transform (NUFFT).
|
|
221
|
+
|
|
222
|
+
The NUFFT performed here uses the fiNUFFT package [1]_, [2]_, [3]_. The plnar near-to-far field transform
|
|
223
|
+
implementation proceeds from Ch 12.9 [4]_ and the derivation included in the documentation.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
near_field : xr.Dataset
|
|
228
|
+
Planar near-field measured values to transform to far-field.
|
|
229
|
+
frequencies : np.ndarray | List[float] | None
|
|
230
|
+
Frequencies in Hertz to perform the near-to-far field transform at. If the provided frequency is not
|
|
231
|
+
found in the `near_field` input, the nearest frequency present will be used instead and a warning will
|
|
232
|
+
be raised to the user.
|
|
233
|
+
probe_correction : MeasurementProbe or None, optional
|
|
234
|
+
Probe correction fields to use, if any, by default None
|
|
235
|
+
dphi : int, optional
|
|
236
|
+
Phi step size, by default 1.0 degrees
|
|
237
|
+
dtheta : int, optional
|
|
238
|
+
Theta step size, by default 1.0 degrees
|
|
239
|
+
window : Callable, optional
|
|
240
|
+
Window generating function used to spatially weight input sample points before transforming, by default
|
|
241
|
+
scipy.signal.windows.boxcar. Further windows can be found in the scipy.signal.windows module.
|
|
242
|
+
r_far_field : int, optional
|
|
243
|
+
Distance in meters to return the far field values at, by default 1000. This value is arbitrary, but may
|
|
244
|
+
be helpful for comparing phase values derived from various sources.
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
xr.Dataset
|
|
249
|
+
Far-field transformed fields stored in an xarray DataSet.
|
|
250
|
+
|
|
251
|
+
References
|
|
252
|
+
----------
|
|
253
|
+
.. [1] fiNUFFT Mathematical definitions of transforms. Online: https://finufft.readthedocs.io/en/latest/math.html
|
|
254
|
+
|
|
255
|
+
.. [2] A. H. Barnett, J. F. Magland, and L. af Klinteberg, A parallel non-uniform fast Fourier transform library
|
|
256
|
+
based on an “exponential of semicircle” kernel. SIAM J. Sci. Comput. 41(5), C479-C504 (2019).
|
|
257
|
+
Online: https://arxiv.org/abs/1808.06736
|
|
258
|
+
|
|
259
|
+
.. [3] A. H. Barnett, Aliasing error of the exp(𝛽√1-z2) kernel in the nonuniform fast Fourier transform. Appl.
|
|
260
|
+
Comput. Harmon. Anal. 51, 1-16 (2021). Online: https://arxiv.org/abs/2001.09405
|
|
261
|
+
|
|
262
|
+
.. [4] C. A. Balanis, Antenna theory: analysis and design, 3. ed. Hoboken, N.J: Wiley-Interscience, 2005.
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
near_field_e = near_field["E"]
|
|
266
|
+
|
|
267
|
+
freq = near_field_e.frequency
|
|
268
|
+
|
|
269
|
+
# Normalize once
|
|
270
|
+
if freq.ndim == 0:
|
|
271
|
+
freq_values = np.atleast_1d(freq.item())
|
|
272
|
+
else:
|
|
273
|
+
freq_values = freq.values
|
|
274
|
+
|
|
275
|
+
logger.debug(
|
|
276
|
+
"Available frequencies: %s",
|
|
277
|
+
", ".join(str(Quantity(x, "Hz")) for x in freq_values),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if frequencies is None:
|
|
281
|
+
frequencies = freq_values
|
|
282
|
+
|
|
283
|
+
# Compute maximum valid elevation angle to be used in computation (limited by sample locations and aperture sizes)
|
|
284
|
+
aut_size = near_field.attrs["aperture_size"]
|
|
285
|
+
|
|
286
|
+
# If a probe is specified, get probe size for valid angle computation, otherwise assume zero size
|
|
287
|
+
if probe_correction is not None:
|
|
288
|
+
probe_size = (
|
|
289
|
+
probe_correction.source.x**2 + probe_correction.source.y**2
|
|
290
|
+
) ** 0.5
|
|
291
|
+
else:
|
|
292
|
+
probe_size = 0
|
|
293
|
+
|
|
294
|
+
logger.debug(f"AUT Size: {Quantity(aut_size, units='m')}")
|
|
295
|
+
logger.debug(f"Probe Size: {Quantity(probe_size, units='m')}")
|
|
296
|
+
|
|
297
|
+
# Conservative values
|
|
298
|
+
min_x_displacement = min(np.abs(np.min(near_field.x)), np.max(near_field.x))
|
|
299
|
+
min_y_displacement = min(np.abs(np.min(near_field.y)), np.max(near_field.y))
|
|
300
|
+
|
|
301
|
+
if z_displacement is not None:
|
|
302
|
+
max_z_displacement = z_displacement
|
|
303
|
+
else:
|
|
304
|
+
max_z_displacement = np.max(np.abs(near_field.z))
|
|
305
|
+
|
|
306
|
+
logger.debug(f"min_x_displacement: {Quantity(min_x_displacement, units='m')}")
|
|
307
|
+
logger.debug(f"min_y_displacement: {Quantity(min_y_displacement, units='m')}")
|
|
308
|
+
logger.debug(f"max_z_displacement: {Quantity(max_z_displacement, units='m')}")
|
|
309
|
+
|
|
310
|
+
max_aperture_displacement = min(min_x_displacement, min_y_displacement) - (
|
|
311
|
+
aut_size / 2
|
|
312
|
+
) # - (probe_size / 2)
|
|
313
|
+
logger.debug(
|
|
314
|
+
f"max_aperture_displacement: {Quantity(max_aperture_displacement, units='m')}",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
max_theta = np.atan2(max_aperture_displacement, max_z_displacement).values.squeeze()
|
|
318
|
+
logger.debug(f"Max. Theta: {Quantity(max_theta * 180 / np.pi)}°")
|
|
319
|
+
|
|
320
|
+
# Compute uniform Theta, Phi grid for target field points
|
|
321
|
+
theta_target = np.linspace(
|
|
322
|
+
-np.pi / 2,
|
|
323
|
+
np.pi / 2,
|
|
324
|
+
round(np.pi / np.deg2rad(dtheta)) + 1,
|
|
325
|
+
endpoint=True,
|
|
326
|
+
)
|
|
327
|
+
phi_target = np.linspace(
|
|
328
|
+
-np.pi / 2,
|
|
329
|
+
np.pi / 2,
|
|
330
|
+
round(np.pi / np.deg2rad(dphi)) + 1,
|
|
331
|
+
endpoint=True,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Trim theta
|
|
335
|
+
theta_target = theta_target[
|
|
336
|
+
(theta_target >= -max_theta) & (theta_target <= max_theta)
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
theta_grid_target, phi_grid_target = np.meshgrid(
|
|
340
|
+
theta_target,
|
|
341
|
+
phi_target,
|
|
342
|
+
indexing="ij",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Generate windowing function (rectangular by default)
|
|
346
|
+
win_x = window(near_field_e.x.size)
|
|
347
|
+
win_y = window(near_field_e.y.size)
|
|
348
|
+
|
|
349
|
+
# Normalize window
|
|
350
|
+
win = np.sqrt(np.outer(win_x, win_y))
|
|
351
|
+
|
|
352
|
+
# Scale source sample locations along aperture to input of finufft in the range [-pi/2, pi/2)
|
|
353
|
+
x_scale = (
|
|
354
|
+
2.0 * np.pi / (np.max(near_field_e.x.values) - np.min(near_field_e.x.values))
|
|
355
|
+
)
|
|
356
|
+
y_scale = (
|
|
357
|
+
2.0 * np.pi / (np.max(near_field_e.y.values) - np.min(near_field_e.y.values))
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
near_field_yy, near_field_xx = np.meshgrid(
|
|
361
|
+
near_field_e.y.values,
|
|
362
|
+
near_field_e.x.values,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
near_field_x_scaled = (x_scale * near_field_xx).ravel()
|
|
366
|
+
near_field_y_scaled = (y_scale * near_field_yy).ravel()
|
|
367
|
+
|
|
368
|
+
# Start performance timer
|
|
369
|
+
t_start = time.perf_counter()
|
|
370
|
+
|
|
371
|
+
# Generate parallel function call arguments
|
|
372
|
+
args_list = []
|
|
373
|
+
z_val = near_field_e.coords["z"].item() # should only have one z-height value
|
|
374
|
+
|
|
375
|
+
# Ensure frequencies is an array (if scalar)
|
|
376
|
+
frequencies = np.array(frequencies)
|
|
377
|
+
|
|
378
|
+
for freq in np.nditer(frequencies):
|
|
379
|
+
f_idx = np.argmin(np.abs(freq_values - freq))
|
|
380
|
+
nearest_freq = freq_values[f_idx]
|
|
381
|
+
|
|
382
|
+
if nearest_freq != freq:
|
|
383
|
+
warnings.warn(
|
|
384
|
+
"Provided frequency {:} not in available frequencies, using nearest frequency {:} instead".format(
|
|
385
|
+
Quantity(freq, "Hz"),
|
|
386
|
+
Quantity(nearest_freq, "Hz"),
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
freq = nearest_freq
|
|
390
|
+
|
|
391
|
+
wavelength = spc.c / freq
|
|
392
|
+
k0 = 2.0 * np.pi / wavelength
|
|
393
|
+
|
|
394
|
+
# Load E-field data from dataset
|
|
395
|
+
# Dimensions (x, y, z, freq)
|
|
396
|
+
if near_field_e.frequency.ndim == 1:
|
|
397
|
+
e_x = near_field_e.sel(component="x", frequency=freq, z=z_val).values
|
|
398
|
+
e_y = near_field_e.sel(component="y", frequency=freq, z=z_val).values
|
|
399
|
+
else:
|
|
400
|
+
e_x = near_field_e.sel(component="x", z=z_val).values
|
|
401
|
+
e_y = near_field_e.sel(component="y", z=z_val).values
|
|
402
|
+
|
|
403
|
+
args_list.append(
|
|
404
|
+
{
|
|
405
|
+
"theta_grid_target": theta_grid_target,
|
|
406
|
+
"phi_grid_target": phi_grid_target,
|
|
407
|
+
"k0": k0,
|
|
408
|
+
"win": win,
|
|
409
|
+
"near_field_x_scaled": near_field_x_scaled,
|
|
410
|
+
"near_field_y_scaled": near_field_y_scaled,
|
|
411
|
+
"x_scale": x_scale,
|
|
412
|
+
"y_scale": y_scale,
|
|
413
|
+
"e_x": e_x,
|
|
414
|
+
"e_y": e_y,
|
|
415
|
+
"z_val": z_val,
|
|
416
|
+
"r_far_field": r_far_field,
|
|
417
|
+
"probe_correction": probe_correction,
|
|
418
|
+
},
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Call planar near-field transform in parallel
|
|
422
|
+
# NOTE: it's faster to not run in parallel (Observed on macOS;
|
|
423
|
+
# performance appears much slower on other platforms, unsure if parallel would be faster)
|
|
424
|
+
# e_mat_list = Parallel(n_jobs=-1, backend="multiprocessing", verbose=10)(
|
|
425
|
+
# delayed(planar_near_field_transform_t3)(**args) for args in args_list
|
|
426
|
+
# )
|
|
427
|
+
e_mat_list = [planar_near_field_transform_t3(**args) for args in args_list]
|
|
428
|
+
|
|
429
|
+
# Print ellapsed time
|
|
430
|
+
elapsed_time = Quantity(time.perf_counter() - t_start, "s")
|
|
431
|
+
logger.debug(f"Far-field transform: {elapsed_time}")
|
|
432
|
+
|
|
433
|
+
# Generate field output
|
|
434
|
+
# Reorder output to: (component, theta, phi, frequency)
|
|
435
|
+
e_field = np.transpose(e_mat_list, (3, 1, 2, 0))
|
|
436
|
+
|
|
437
|
+
fields = FieldDataset.create(
|
|
438
|
+
e_field=e_field,
|
|
439
|
+
spatial_coords={
|
|
440
|
+
"theta": theta_target,
|
|
441
|
+
"phi": phi_target,
|
|
442
|
+
},
|
|
443
|
+
frequency=frequencies,
|
|
444
|
+
coordinate_system=CoordinateSystemType.SPHERICAL,
|
|
445
|
+
field_type=FieldType.FAR,
|
|
446
|
+
data_source=registry.data_sources.get("xnft"),
|
|
447
|
+
aperture_size=near_field.attrs["aperture_size"],
|
|
448
|
+
gain_calibration=gain_calibration,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Compute gain calibration if provided
|
|
452
|
+
if gain_calibration is not None:
|
|
453
|
+
fields = calibrate_gain(fields, gain_calibration)
|
|
454
|
+
|
|
455
|
+
return fields
|
|
File without changes
|