phased-array-modeling 1.3.1__tar.gz → 1.3.2__tar.gz
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.
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/PKG-INFO +10 -6
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/README.md +9 -5
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/__init__.py +1 -1
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/beamforming.py +7 -4
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/core.py +31 -7
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/geometry.py +5 -10
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/impairments.py +26 -5
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/polarization.py +1 -1
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/utils.py +19 -5
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/wideband.py +3 -1
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/PKG-INFO +10 -6
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/SOURCES.txt +3 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/pyproject.toml +1 -1
- phased_array_modeling-1.3.2/tests/test_docstrings.py +33 -0
- phased_array_modeling-1.3.2/tests/test_utils.py +148 -0
- phased_array_modeling-1.3.2/tests/test_visualization.py +180 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/LICENSE +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/coordinates.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/export.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array/visualization.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/dependency_links.txt +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/requires.txt +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/top_level.txt +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/setup.cfg +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/tests/test_beamforming.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/tests/test_coordinates.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/tests/test_core.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/tests/test_export.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/tests/test_geometry.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/tests/test_impairments.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/tests/test_polarization.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/tests/test_wideband.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: phased-array-modeling
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: Comprehensive Python library for phased array antenna modeling and visualization
|
|
5
5
|
Author-email: John Hodge <jah70@vt.edu>
|
|
6
6
|
License: MIT
|
|
@@ -55,11 +55,11 @@ Dynamic: license-file
|
|
|
55
55
|
[](https://phased-array-antenna-model.streamlit.app/)
|
|
56
56
|
[](https://phased-array-antenna-model.readthedocs.io/en/latest/)
|
|
57
57
|
|
|
58
|
-
A comprehensive Python library for computing and visualizing phased array antenna radiation patterns. Features
|
|
58
|
+
A comprehensive Python library for computing and visualizing phased array antenna radiation patterns. Features vectorized computations (50-100x faster than naive loops), multiple array geometries, advanced beamforming, and interactive 3D visualization.
|
|
59
59
|
|
|
60
60
|
## Features
|
|
61
61
|
|
|
62
|
-
- **High Performance**: Vectorized array factor computation (
|
|
62
|
+
- **High Performance**: Vectorized array factor computation (50-100x faster than naive loops)
|
|
63
63
|
- **Multiple Geometries**: Rectangular, triangular, circular, cylindrical, spherical, sparse/thinned arrays
|
|
64
64
|
- **Beamforming**: Amplitude tapering (Taylor, Chebyshev, etc.), null steering, multi-beam
|
|
65
65
|
- **Realistic Impairments**: Mutual coupling, phase quantization, element failures, scan blindness
|
|
@@ -81,7 +81,7 @@ The Streamlit app provides an interactive interface for:
|
|
|
81
81
|
|
|
82
82
|
## Installation
|
|
83
83
|
|
|
84
|
-
### From
|
|
84
|
+
### From PyPI (recommended)
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
87
|
pip install phased-array-modeling
|
|
@@ -235,8 +235,12 @@ For hands-on learning, see the [demo notebook](Phased_Array_Demo.ipynb) which co
|
|
|
235
235
|
phased_array/
|
|
236
236
|
├── core.py # Vectorized AF, FFT, steering, element patterns
|
|
237
237
|
├── geometry.py # Array geometries and subarray architectures
|
|
238
|
-
├── beamforming.py # Tapering, null steering, multi-beam
|
|
238
|
+
├── beamforming.py # Tapering, null steering, multi-beam, adaptive
|
|
239
239
|
├── impairments.py # Coupling, quantization, failures, scan blindness
|
|
240
|
+
├── wideband.py # True-time-delay steering, beam squint, bandwidth
|
|
241
|
+
├── polarization.py # Jones/Stokes, axial ratio, XPD, Ludwig-3
|
|
242
|
+
├── coordinates.py # Antenna/radar/cone frames, pattern rotation
|
|
243
|
+
├── export.py # CSV/JSON/NPZ export, summary reports
|
|
240
244
|
├── visualization.py # 2D, 3D Plotly, UV-space plotting
|
|
241
245
|
└── utils.py # Coordinate transforms, helpers
|
|
242
246
|
```
|
|
@@ -278,7 +282,7 @@ If you use this library in your research, please cite:
|
|
|
278
282
|
author = {Hodge, John},
|
|
279
283
|
title = {Phased Array Antenna Modeling},
|
|
280
284
|
url = {https://github.com/jman4162/Phased-Array-Antenna-Model},
|
|
281
|
-
year = {
|
|
285
|
+
year = {2026}
|
|
282
286
|
}
|
|
283
287
|
```
|
|
284
288
|
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
[](https://phased-array-antenna-model.streamlit.app/)
|
|
10
10
|
[](https://phased-array-antenna-model.readthedocs.io/en/latest/)
|
|
11
11
|
|
|
12
|
-
A comprehensive Python library for computing and visualizing phased array antenna radiation patterns. Features
|
|
12
|
+
A comprehensive Python library for computing and visualizing phased array antenna radiation patterns. Features vectorized computations (50-100x faster than naive loops), multiple array geometries, advanced beamforming, and interactive 3D visualization.
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- **High Performance**: Vectorized array factor computation (
|
|
16
|
+
- **High Performance**: Vectorized array factor computation (50-100x faster than naive loops)
|
|
17
17
|
- **Multiple Geometries**: Rectangular, triangular, circular, cylindrical, spherical, sparse/thinned arrays
|
|
18
18
|
- **Beamforming**: Amplitude tapering (Taylor, Chebyshev, etc.), null steering, multi-beam
|
|
19
19
|
- **Realistic Impairments**: Mutual coupling, phase quantization, element failures, scan blindness
|
|
@@ -35,7 +35,7 @@ The Streamlit app provides an interactive interface for:
|
|
|
35
35
|
|
|
36
36
|
## Installation
|
|
37
37
|
|
|
38
|
-
### From
|
|
38
|
+
### From PyPI (recommended)
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
pip install phased-array-modeling
|
|
@@ -189,8 +189,12 @@ For hands-on learning, see the [demo notebook](Phased_Array_Demo.ipynb) which co
|
|
|
189
189
|
phased_array/
|
|
190
190
|
├── core.py # Vectorized AF, FFT, steering, element patterns
|
|
191
191
|
├── geometry.py # Array geometries and subarray architectures
|
|
192
|
-
├── beamforming.py # Tapering, null steering, multi-beam
|
|
192
|
+
├── beamforming.py # Tapering, null steering, multi-beam, adaptive
|
|
193
193
|
├── impairments.py # Coupling, quantization, failures, scan blindness
|
|
194
|
+
├── wideband.py # True-time-delay steering, beam squint, bandwidth
|
|
195
|
+
├── polarization.py # Jones/Stokes, axial ratio, XPD, Ludwig-3
|
|
196
|
+
├── coordinates.py # Antenna/radar/cone frames, pattern rotation
|
|
197
|
+
├── export.py # CSV/JSON/NPZ export, summary reports
|
|
194
198
|
├── visualization.py # 2D, 3D Plotly, UV-space plotting
|
|
195
199
|
└── utils.py # Coordinate transforms, helpers
|
|
196
200
|
```
|
|
@@ -232,7 +236,7 @@ If you use this library in your research, please cite:
|
|
|
232
236
|
author = {Hodge, John},
|
|
233
237
|
title = {Phased Array Antenna Modeling},
|
|
234
238
|
url = {https://github.com/jman4162/Phased-Array-Antenna-Model},
|
|
235
|
-
year = {
|
|
239
|
+
year = {2026}
|
|
236
240
|
}
|
|
237
241
|
```
|
|
238
242
|
|
|
@@ -32,7 +32,7 @@ Example
|
|
|
32
32
|
>>> theta, phi, pattern_dB = pa.compute_full_pattern(geom.x, geom.y, weights, k)
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
__version__ = "1.3.
|
|
35
|
+
__version__ = "1.3.2"
|
|
36
36
|
|
|
37
37
|
# Beamforming functions
|
|
38
38
|
from .beamforming import ( # Amplitude tapers; Null steering; Multiple beams; Beam spoiling; Adaptive
|
|
@@ -391,7 +391,10 @@ def null_steering_projection(
|
|
|
391
391
|
|
|
392
392
|
Verify null depth:
|
|
393
393
|
|
|
394
|
-
>>> null_depth = pa.compute_null_depth(
|
|
394
|
+
>>> null_depth = pa.compute_null_depth(
|
|
395
|
+
... weights, geom, k, theta_deg=45, phi_deg=0,
|
|
396
|
+
... theta_main_deg=30, phi_main_deg=0
|
|
397
|
+
... )
|
|
395
398
|
>>> null_depth < -30 # Deep null achieved
|
|
396
399
|
True
|
|
397
400
|
"""
|
|
@@ -1275,15 +1278,15 @@ def compute_sinr_improvement(
|
|
|
1275
1278
|
>>> geom = pa.create_rectangular_array(8, 8, dx=0.5, dy=0.5)
|
|
1276
1279
|
>>> k = pa.wavelength_to_k(1.0)
|
|
1277
1280
|
>>> w_q = pa.steering_vector(k, geom.x, geom.y, 0, 0)
|
|
1278
|
-
>>> # Create adapted weights that null an interferer
|
|
1281
|
+
>>> # Create adapted weights that null an interferer in a sidelobe
|
|
1279
1282
|
>>> w_adapted = pa.null_steering_projection(
|
|
1280
1283
|
... geom, k, theta_main_deg=0, phi_main_deg=0,
|
|
1281
|
-
... null_directions=[(
|
|
1284
|
+
... null_directions=[(20, 0)]
|
|
1282
1285
|
... )
|
|
1283
1286
|
>>> sinr_b, sinr_a, imp = pa.compute_sinr_improvement(
|
|
1284
1287
|
... w_q, w_adapted, geom, k,
|
|
1285
1288
|
... signal_direction=(0, 0),
|
|
1286
|
-
... interference_directions=[(
|
|
1289
|
+
... interference_directions=[(20, 0)],
|
|
1287
1290
|
... signal_power=1.0, interference_powers=[10.0], noise_power=0.1
|
|
1288
1291
|
... )
|
|
1289
1292
|
>>> imp > 0 # Should show improvement
|
|
@@ -5,6 +5,7 @@ Includes vectorized array factor, FFT-based computation, steering vectors,
|
|
|
5
5
|
and element patterns.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import warnings
|
|
8
9
|
from typing import Optional, Tuple, Union
|
|
9
10
|
|
|
10
11
|
import numpy as np
|
|
@@ -307,11 +308,13 @@ def element_pattern(
|
|
|
307
308
|
theta : ndarray
|
|
308
309
|
Polar angle in radians
|
|
309
310
|
phi : ndarray
|
|
310
|
-
Azimuthal angle in radians (not used
|
|
311
|
+
Azimuthal angle in radians (not used; the model is phi-symmetric)
|
|
311
312
|
cos_exp_theta : float
|
|
312
313
|
Cosine exponent for theta dependence (1.0 = simple cosine)
|
|
313
314
|
cos_exp_phi : float
|
|
314
|
-
|
|
315
|
+
Deprecated and unused. The basic model has no phi dependence;
|
|
316
|
+
for polarized/phi-dependent element models see the v1.4 vector
|
|
317
|
+
pattern API.
|
|
315
318
|
max_gain_dBi : float
|
|
316
319
|
Peak element gain in dBi
|
|
317
320
|
|
|
@@ -320,6 +323,16 @@ def element_pattern(
|
|
|
320
323
|
pattern : ndarray
|
|
321
324
|
Element pattern (linear scale, same shape as theta)
|
|
322
325
|
"""
|
|
326
|
+
if cos_exp_phi != 1.0:
|
|
327
|
+
warnings.warn(
|
|
328
|
+
"cos_exp_phi is unused: element_pattern is phi-symmetric. "
|
|
329
|
+
"The parameter is deprecated and will be removed in a future "
|
|
330
|
+
"release; use the vector element models for phi-dependent "
|
|
331
|
+
"patterns.",
|
|
332
|
+
DeprecationWarning,
|
|
333
|
+
stacklevel=2,
|
|
334
|
+
)
|
|
335
|
+
|
|
323
336
|
# Convert max gain to linear
|
|
324
337
|
max_gain_linear = 10 ** (max_gain_dBi / 10)
|
|
325
338
|
|
|
@@ -385,7 +398,7 @@ def total_pattern(
|
|
|
385
398
|
z: Optional[np.ndarray] = None,
|
|
386
399
|
**element_kwargs
|
|
387
400
|
) -> np.ndarray:
|
|
388
|
-
"""
|
|
401
|
+
r"""
|
|
389
402
|
Compute total radiation pattern (element pattern * array factor).
|
|
390
403
|
|
|
391
404
|
Parameters
|
|
@@ -668,7 +681,18 @@ def compute_half_power_beamwidth(
|
|
|
668
681
|
while right_idx < len(pattern_dB) - 1 and above_3dB[right_idx + 1]:
|
|
669
682
|
right_idx += 1
|
|
670
683
|
|
|
671
|
-
#
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
684
|
+
# Linearly interpolate the -3 dB crossings; fall back to the sample
|
|
685
|
+
# position when the crossing lies outside the sampled range
|
|
686
|
+
left_angle = angles_deg[left_idx]
|
|
687
|
+
if left_idx > 0 and pattern_dB[left_idx - 1] < -3.0:
|
|
688
|
+
a0, a1 = angles_deg[left_idx - 1], angles_deg[left_idx]
|
|
689
|
+
p0, p1 = pattern_dB[left_idx - 1], pattern_dB[left_idx]
|
|
690
|
+
left_angle = a0 + (-3.0 - p0) * (a1 - a0) / (p1 - p0)
|
|
691
|
+
|
|
692
|
+
right_angle = angles_deg[right_idx]
|
|
693
|
+
if right_idx < len(pattern_dB) - 1 and pattern_dB[right_idx + 1] < -3.0:
|
|
694
|
+
a0, a1 = angles_deg[right_idx], angles_deg[right_idx + 1]
|
|
695
|
+
p0, p1 = pattern_dB[right_idx], pattern_dB[right_idx + 1]
|
|
696
|
+
right_angle = a0 + (-3.0 - p0) * (a1 - a0) / (p1 - p0)
|
|
697
|
+
|
|
698
|
+
return abs(right_angle - left_angle)
|
|
@@ -465,14 +465,10 @@ def create_concentric_rings_array(
|
|
|
465
465
|
"""
|
|
466
466
|
x_list = []
|
|
467
467
|
y_list = []
|
|
468
|
-
nx_list = []
|
|
469
|
-
ny_list = []
|
|
470
468
|
|
|
471
469
|
if include_center:
|
|
472
470
|
x_list.append(0.0)
|
|
473
471
|
y_list.append(0.0)
|
|
474
|
-
nx_list.append(0.0)
|
|
475
|
-
ny_list.append(0.0)
|
|
476
472
|
|
|
477
473
|
if isinstance(elements_per_ring, int):
|
|
478
474
|
elements_per_ring = [elements_per_ring] * n_rings
|
|
@@ -484,18 +480,18 @@ def create_concentric_rings_array(
|
|
|
484
480
|
for angle in angles:
|
|
485
481
|
x_list.append(radius * np.cos(angle))
|
|
486
482
|
y_list.append(radius * np.sin(angle))
|
|
487
|
-
nx_list.append(np.cos(angle))
|
|
488
|
-
ny_list.append(np.sin(angle))
|
|
489
483
|
|
|
490
484
|
x = np.array(x_list)
|
|
491
485
|
y = np.array(y_list)
|
|
492
486
|
|
|
487
|
+
# Planar array: all elements face broadside (+z), like the other
|
|
488
|
+
# planar factories
|
|
493
489
|
return ArrayGeometry(
|
|
494
490
|
x=x, y=y,
|
|
495
491
|
z=np.zeros_like(x),
|
|
496
|
-
nx=np.
|
|
497
|
-
ny=np.
|
|
498
|
-
nz=np.
|
|
492
|
+
nx=np.zeros_like(x),
|
|
493
|
+
ny=np.zeros_like(x),
|
|
494
|
+
nz=np.ones_like(x),
|
|
499
495
|
element_indices=np.arange(len(x))
|
|
500
496
|
)
|
|
501
497
|
|
|
@@ -1241,7 +1237,6 @@ def overlapped_subarray_weights(
|
|
|
1241
1237
|
"""
|
|
1242
1238
|
if not architecture.overlapped:
|
|
1243
1239
|
# Fall back to standard subarray weights
|
|
1244
|
-
from .beamforming import compute_subarray_weights
|
|
1245
1240
|
return compute_subarray_weights(architecture, k, theta0_deg, phi0_deg)
|
|
1246
1241
|
|
|
1247
1242
|
geom = architecture.geometry
|
|
@@ -73,7 +73,8 @@ def mutual_coupling_matrix_theoretical(
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def mutual_coupling_matrix_measured(
|
|
76
|
-
s_parameters: np.ndarray
|
|
76
|
+
s_parameters: np.ndarray,
|
|
77
|
+
formulation: str = 'impedance'
|
|
77
78
|
) -> np.ndarray:
|
|
78
79
|
"""
|
|
79
80
|
Convert measured S-parameters to coupling matrix.
|
|
@@ -85,16 +86,36 @@ def mutual_coupling_matrix_measured(
|
|
|
85
86
|
----------
|
|
86
87
|
s_parameters : ndarray
|
|
87
88
|
N x N S-parameter matrix (complex)
|
|
89
|
+
formulation : str
|
|
90
|
+
'impedance' (default): C = (I + S)(I - S)^(-1), the normalized
|
|
91
|
+
impedance matrix. 'voltage': C = I + S, the total-voltage
|
|
92
|
+
coupling used before v1.3.2.
|
|
88
93
|
|
|
89
94
|
Returns
|
|
90
95
|
-------
|
|
91
96
|
C : ndarray
|
|
92
97
|
Mutual coupling matrix
|
|
98
|
+
|
|
99
|
+
Notes
|
|
100
|
+
-----
|
|
101
|
+
Before v1.3.2 this function returned I + S while documenting the
|
|
102
|
+
impedance formulation; pass ``formulation='voltage'`` to reproduce
|
|
103
|
+
the old behavior.
|
|
93
104
|
"""
|
|
94
105
|
n = s_parameters.shape[0]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
identity = np.eye(n, dtype=complex)
|
|
107
|
+
|
|
108
|
+
if formulation == 'impedance':
|
|
109
|
+
try:
|
|
110
|
+
C = np.linalg.solve((identity - s_parameters).T, (identity + s_parameters).T).T
|
|
111
|
+
except np.linalg.LinAlgError:
|
|
112
|
+
C = (identity + s_parameters) @ np.linalg.pinv(identity - s_parameters)
|
|
113
|
+
elif formulation == 'voltage':
|
|
114
|
+
C = identity + s_parameters
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"formulation must be 'impedance' or 'voltage', got {formulation!r}"
|
|
118
|
+
)
|
|
98
119
|
return C
|
|
99
120
|
|
|
100
121
|
|
|
@@ -436,7 +457,7 @@ def simulate_element_failures(
|
|
|
436
457
|
Analyze graceful degradation:
|
|
437
458
|
|
|
438
459
|
>>> results = pa.analyze_graceful_degradation(
|
|
439
|
-
... geom, k,
|
|
460
|
+
... weights, geom, k, failure_rates=[0.0, 0.05, 0.1], n_trials=5
|
|
440
461
|
... )
|
|
441
462
|
"""
|
|
442
463
|
if seed is not None:
|
|
@@ -104,7 +104,7 @@ def stokes_parameters(jones: np.ndarray) -> Tuple[float, float, float, float]:
|
|
|
104
104
|
|
|
105
105
|
>>> j = pa.jones_vector(1.0, 1.0, phase_diff=-np.pi/2) # RHCP
|
|
106
106
|
>>> S0, S1, S2, S3 = pa.stokes_parameters(j)
|
|
107
|
-
>>> np.isclose(S3,
|
|
107
|
+
>>> np.isclose(S3, S0, atol=1e-10) # right circular: S3 = +S0
|
|
108
108
|
True
|
|
109
109
|
"""
|
|
110
110
|
jones = np.asarray(jones, dtype=complex)
|
|
@@ -38,12 +38,20 @@ def azel_to_thetaphi(az: ArrayLike, el: ArrayLike) -> Tuple[ArrayLike, ArrayLike
|
|
|
38
38
|
Polar angle from z-axis in radians (0 = zenith)
|
|
39
39
|
phi : array_like
|
|
40
40
|
Azimuthal angle in radians
|
|
41
|
+
|
|
42
|
+
Notes
|
|
43
|
+
-----
|
|
44
|
+
Convention: boresight is +z, azimuth rotates toward +x ("right"),
|
|
45
|
+
elevation toward +y ("up"), with elevation applied over azimuth:
|
|
46
|
+
the direction vector is (sin(az)cos(el), sin(el), cos(az)cos(el)).
|
|
47
|
+
Inverse of `thetaphi_to_azel`. Before v1.3.2 the two functions used
|
|
48
|
+
inconsistent conventions and did not round-trip.
|
|
41
49
|
"""
|
|
42
50
|
az = np.asarray(az)
|
|
43
51
|
el = np.asarray(el)
|
|
44
52
|
|
|
45
53
|
theta = np.arccos(np.cos(az) * np.cos(el))
|
|
46
|
-
phi = np.arctan2(np.sin(
|
|
54
|
+
phi = np.arctan2(np.sin(el), np.sin(az) * np.cos(el))
|
|
47
55
|
|
|
48
56
|
return theta, phi
|
|
49
57
|
|
|
@@ -62,15 +70,21 @@ def thetaphi_to_azel(theta: ArrayLike, phi: ArrayLike) -> Tuple[ArrayLike, Array
|
|
|
62
70
|
Returns
|
|
63
71
|
-------
|
|
64
72
|
az : array_like
|
|
65
|
-
Azimuth angle in radians
|
|
73
|
+
Azimuth angle in radians (0 = boresight, positive = right)
|
|
66
74
|
el : array_like
|
|
67
|
-
Elevation angle in radians
|
|
75
|
+
Elevation angle in radians (0 = boresight, positive = up)
|
|
76
|
+
|
|
77
|
+
Notes
|
|
78
|
+
-----
|
|
79
|
+
Inverse of `azel_to_thetaphi`; see its docstring for the convention.
|
|
80
|
+
Before v1.3.2 the two functions used inconsistent conventions and
|
|
81
|
+
did not round-trip.
|
|
68
82
|
"""
|
|
69
83
|
theta = np.asarray(theta)
|
|
70
84
|
phi = np.asarray(phi)
|
|
71
85
|
|
|
72
|
-
el = np.arcsin(np.
|
|
73
|
-
az = np.arctan2(np.sin(theta) * np.
|
|
86
|
+
el = np.arcsin(np.sin(theta) * np.sin(phi))
|
|
87
|
+
az = np.arctan2(np.sin(theta) * np.cos(phi), np.cos(theta))
|
|
74
88
|
|
|
75
89
|
return az, el
|
|
76
90
|
|
|
@@ -83,9 +83,11 @@ def steering_vector_ttd(
|
|
|
83
83
|
|
|
84
84
|
>>> # TTD maintains beam direction across bandwidth
|
|
85
85
|
>>> # Phase steering causes beam squint at off-center frequencies
|
|
86
|
+
>>> import numpy as np
|
|
86
87
|
>>> squint = pa.compute_beam_squint(
|
|
87
88
|
... geom.x, geom.y, theta0_deg=30, phi0_deg=0,
|
|
88
|
-
...
|
|
89
|
+
... center_frequency=10e9,
|
|
90
|
+
... frequencies=np.linspace(9e9, 11e9, 5)
|
|
89
91
|
... )
|
|
90
92
|
"""
|
|
91
93
|
theta0 = np.deg2rad(theta0_deg)
|
{phased_array_modeling-1.3.1 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: phased-array-modeling
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: Comprehensive Python library for phased array antenna modeling and visualization
|
|
5
5
|
Author-email: John Hodge <jah70@vt.edu>
|
|
6
6
|
License: MIT
|
|
@@ -55,11 +55,11 @@ Dynamic: license-file
|
|
|
55
55
|
[](https://phased-array-antenna-model.streamlit.app/)
|
|
56
56
|
[](https://phased-array-antenna-model.readthedocs.io/en/latest/)
|
|
57
57
|
|
|
58
|
-
A comprehensive Python library for computing and visualizing phased array antenna radiation patterns. Features
|
|
58
|
+
A comprehensive Python library for computing and visualizing phased array antenna radiation patterns. Features vectorized computations (50-100x faster than naive loops), multiple array geometries, advanced beamforming, and interactive 3D visualization.
|
|
59
59
|
|
|
60
60
|
## Features
|
|
61
61
|
|
|
62
|
-
- **High Performance**: Vectorized array factor computation (
|
|
62
|
+
- **High Performance**: Vectorized array factor computation (50-100x faster than naive loops)
|
|
63
63
|
- **Multiple Geometries**: Rectangular, triangular, circular, cylindrical, spherical, sparse/thinned arrays
|
|
64
64
|
- **Beamforming**: Amplitude tapering (Taylor, Chebyshev, etc.), null steering, multi-beam
|
|
65
65
|
- **Realistic Impairments**: Mutual coupling, phase quantization, element failures, scan blindness
|
|
@@ -81,7 +81,7 @@ The Streamlit app provides an interactive interface for:
|
|
|
81
81
|
|
|
82
82
|
## Installation
|
|
83
83
|
|
|
84
|
-
### From
|
|
84
|
+
### From PyPI (recommended)
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
87
|
pip install phased-array-modeling
|
|
@@ -235,8 +235,12 @@ For hands-on learning, see the [demo notebook](Phased_Array_Demo.ipynb) which co
|
|
|
235
235
|
phased_array/
|
|
236
236
|
├── core.py # Vectorized AF, FFT, steering, element patterns
|
|
237
237
|
├── geometry.py # Array geometries and subarray architectures
|
|
238
|
-
├── beamforming.py # Tapering, null steering, multi-beam
|
|
238
|
+
├── beamforming.py # Tapering, null steering, multi-beam, adaptive
|
|
239
239
|
├── impairments.py # Coupling, quantization, failures, scan blindness
|
|
240
|
+
├── wideband.py # True-time-delay steering, beam squint, bandwidth
|
|
241
|
+
├── polarization.py # Jones/Stokes, axial ratio, XPD, Ludwig-3
|
|
242
|
+
├── coordinates.py # Antenna/radar/cone frames, pattern rotation
|
|
243
|
+
├── export.py # CSV/JSON/NPZ export, summary reports
|
|
240
244
|
├── visualization.py # 2D, 3D Plotly, UV-space plotting
|
|
241
245
|
└── utils.py # Coordinate transforms, helpers
|
|
242
246
|
```
|
|
@@ -278,7 +282,7 @@ If you use this library in your research, please cite:
|
|
|
278
282
|
author = {Hodge, John},
|
|
279
283
|
title = {Phased Array Antenna Modeling},
|
|
280
284
|
url = {https://github.com/jman4162/Phased-Array-Antenna-Model},
|
|
281
|
-
year = {
|
|
285
|
+
year = {2026}
|
|
282
286
|
}
|
|
283
287
|
```
|
|
284
288
|
|
|
@@ -20,8 +20,11 @@ phased_array_modeling.egg-info/top_level.txt
|
|
|
20
20
|
tests/test_beamforming.py
|
|
21
21
|
tests/test_coordinates.py
|
|
22
22
|
tests/test_core.py
|
|
23
|
+
tests/test_docstrings.py
|
|
23
24
|
tests/test_export.py
|
|
24
25
|
tests/test_geometry.py
|
|
25
26
|
tests/test_impairments.py
|
|
26
27
|
tests/test_polarization.py
|
|
28
|
+
tests/test_utils.py
|
|
29
|
+
tests/test_visualization.py
|
|
27
30
|
tests/test_wideband.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "phased-array-modeling"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.2"
|
|
8
8
|
description = "Comprehensive Python library for phased array antenna modeling and visualization"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Run every module's docstring examples as doctests."""
|
|
2
|
+
|
|
3
|
+
import doctest
|
|
4
|
+
import importlib
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
MODULES = [
|
|
9
|
+
"core",
|
|
10
|
+
"utils",
|
|
11
|
+
"geometry",
|
|
12
|
+
"beamforming",
|
|
13
|
+
"impairments",
|
|
14
|
+
"wideband",
|
|
15
|
+
"polarization",
|
|
16
|
+
"coordinates",
|
|
17
|
+
"export",
|
|
18
|
+
"visualization",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parametrize("module_name", MODULES)
|
|
23
|
+
def test_module_doctests(module_name):
|
|
24
|
+
module = importlib.import_module(f"phased_array.{module_name}")
|
|
25
|
+
results = doctest.testmod(
|
|
26
|
+
module,
|
|
27
|
+
verbose=False,
|
|
28
|
+
report=True,
|
|
29
|
+
optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE,
|
|
30
|
+
)
|
|
31
|
+
assert results.failed == 0, (
|
|
32
|
+
f"{results.failed} doctest failure(s) in phased_array.{module_name}"
|
|
33
|
+
)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Tests for phased_array.utils coordinate transforms and helpers."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
import phased_array as pa
|
|
7
|
+
from phased_array.utils import (
|
|
8
|
+
azel_to_thetaphi,
|
|
9
|
+
create_theta_phi_grid,
|
|
10
|
+
create_uv_grid,
|
|
11
|
+
db_to_linear,
|
|
12
|
+
deg2rad,
|
|
13
|
+
frequency_to_k,
|
|
14
|
+
frequency_to_wavelength,
|
|
15
|
+
is_visible_region,
|
|
16
|
+
linear_to_db,
|
|
17
|
+
normalize_pattern,
|
|
18
|
+
rad2deg,
|
|
19
|
+
theta_phi_to_uv,
|
|
20
|
+
thetaphi_to_azel,
|
|
21
|
+
uv_to_theta_phi,
|
|
22
|
+
wavelength_to_k,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestAngleConversions:
|
|
27
|
+
def test_deg2rad_known_values(self):
|
|
28
|
+
assert np.isclose(deg2rad(180.0), np.pi)
|
|
29
|
+
assert np.isclose(deg2rad(0.0), 0.0)
|
|
30
|
+
|
|
31
|
+
def test_rad2deg_known_values(self):
|
|
32
|
+
assert np.isclose(rad2deg(np.pi / 2), 90.0)
|
|
33
|
+
|
|
34
|
+
def test_deg_rad_roundtrip(self):
|
|
35
|
+
angles = np.linspace(-180, 180, 19)
|
|
36
|
+
assert np.allclose(rad2deg(deg2rad(angles)), angles)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestAzElThetaPhi:
|
|
40
|
+
def test_boresight(self):
|
|
41
|
+
theta, phi = azel_to_thetaphi(0.0, 0.0)
|
|
42
|
+
assert np.isclose(theta, 0.0)
|
|
43
|
+
|
|
44
|
+
def test_roundtrip(self):
|
|
45
|
+
az = np.deg2rad(np.array([10.0, -20.0, 30.0]))
|
|
46
|
+
el = np.deg2rad(np.array([5.0, 15.0, -25.0]))
|
|
47
|
+
theta, phi = azel_to_thetaphi(az, el)
|
|
48
|
+
az2, el2 = thetaphi_to_azel(theta, phi)
|
|
49
|
+
assert np.allclose(az2, az)
|
|
50
|
+
assert np.allclose(el2, el)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestUVConversions:
|
|
54
|
+
def test_boresight_is_origin(self):
|
|
55
|
+
u, v = theta_phi_to_uv(0.0, 0.0)
|
|
56
|
+
assert np.isclose(u, 0.0)
|
|
57
|
+
assert np.isclose(v, 0.0)
|
|
58
|
+
|
|
59
|
+
def test_known_value(self):
|
|
60
|
+
u, v = theta_phi_to_uv(np.pi / 2, 0.0)
|
|
61
|
+
assert np.isclose(u, 1.0)
|
|
62
|
+
assert np.isclose(v, 0.0, atol=1e-12)
|
|
63
|
+
|
|
64
|
+
def test_roundtrip(self):
|
|
65
|
+
theta = np.deg2rad(np.array([10.0, 30.0, 60.0]))
|
|
66
|
+
phi = np.deg2rad(np.array([0.0, 45.0, 200.0]))
|
|
67
|
+
u, v = theta_phi_to_uv(theta, phi)
|
|
68
|
+
theta2, phi2 = uv_to_theta_phi(u, v)
|
|
69
|
+
u2, v2 = theta_phi_to_uv(theta2, phi2)
|
|
70
|
+
# Angles may wrap; direction cosines must round-trip exactly
|
|
71
|
+
assert np.allclose(u2, u)
|
|
72
|
+
assert np.allclose(v2, v)
|
|
73
|
+
|
|
74
|
+
def test_visible_region_boundary(self):
|
|
75
|
+
assert is_visible_region(0.0, 0.0)
|
|
76
|
+
assert is_visible_region(1.0, 0.0)
|
|
77
|
+
assert not is_visible_region(0.8, 0.8)
|
|
78
|
+
mask = is_visible_region(np.array([0.0, 0.9, 0.5]), np.array([0.0, 0.9, 0.5]))
|
|
79
|
+
assert mask.tolist() == [True, False, True]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestWavenumberFrequency:
|
|
83
|
+
def test_wavelength_to_k(self):
|
|
84
|
+
assert np.isclose(wavelength_to_k(1.0), 2 * np.pi)
|
|
85
|
+
assert np.isclose(wavelength_to_k(0.5), 4 * np.pi)
|
|
86
|
+
|
|
87
|
+
def test_frequency_to_wavelength(self):
|
|
88
|
+
assert np.isclose(frequency_to_wavelength(3e8), 1.0)
|
|
89
|
+
assert np.isclose(frequency_to_wavelength(10e9, c=3e8), 0.03)
|
|
90
|
+
|
|
91
|
+
def test_frequency_to_k_consistent(self):
|
|
92
|
+
f = 10e9
|
|
93
|
+
assert np.isclose(frequency_to_k(f), wavelength_to_k(frequency_to_wavelength(f)))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TestDbConversions:
|
|
97
|
+
def test_roundtrip(self):
|
|
98
|
+
values = np.array([1e-3, 0.5, 1.0, 100.0])
|
|
99
|
+
assert np.allclose(db_to_linear(linear_to_db(values)), values)
|
|
100
|
+
|
|
101
|
+
def test_known_values(self):
|
|
102
|
+
assert np.isclose(linear_to_db(10.0), 10.0)
|
|
103
|
+
assert np.isclose(db_to_linear(3.0), 10 ** 0.3)
|
|
104
|
+
|
|
105
|
+
def test_min_db_floor(self):
|
|
106
|
+
assert linear_to_db(0.0) == -100.0
|
|
107
|
+
assert linear_to_db(0.0, min_db=-60.0) == -60.0
|
|
108
|
+
assert np.all(linear_to_db(np.array([0.0, 1e-30])) == -100.0)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestNormalizePattern:
|
|
112
|
+
def test_peak_mode(self):
|
|
113
|
+
pattern = np.array([1.0, 2.0, 4.0])
|
|
114
|
+
normalized = normalize_pattern(pattern, mode="peak")
|
|
115
|
+
assert np.isclose(np.max(normalized), 1.0)
|
|
116
|
+
|
|
117
|
+
def test_power_mode(self):
|
|
118
|
+
pattern = np.array([1.0, 2.0, 2.0])
|
|
119
|
+
normalized = normalize_pattern(pattern, mode="power")
|
|
120
|
+
assert np.isclose(np.sum(np.abs(normalized) ** 2), 1.0)
|
|
121
|
+
|
|
122
|
+
def test_unknown_mode_raises(self):
|
|
123
|
+
with pytest.raises(ValueError):
|
|
124
|
+
normalize_pattern(np.array([1.0]), mode="bogus")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestGrids:
|
|
128
|
+
def test_theta_phi_grid_shapes(self):
|
|
129
|
+
theta, phi, THETA, PHI = create_theta_phi_grid(n_theta=19, n_phi=37)
|
|
130
|
+
assert theta.shape == (19,)
|
|
131
|
+
assert phi.shape == (37,)
|
|
132
|
+
assert THETA.shape == (19, 37)
|
|
133
|
+
assert PHI.shape == (19, 37)
|
|
134
|
+
|
|
135
|
+
def test_theta_phi_grid_ranges(self):
|
|
136
|
+
theta, phi, _, _ = create_theta_phi_grid(
|
|
137
|
+
theta_range=(0, np.pi / 2), phi_range=(0, np.pi), n_theta=10, n_phi=10
|
|
138
|
+
)
|
|
139
|
+
assert np.isclose(theta[0], 0.0)
|
|
140
|
+
assert np.isclose(theta[-1], np.pi / 2)
|
|
141
|
+
assert np.isclose(phi[-1], np.pi)
|
|
142
|
+
|
|
143
|
+
def test_uv_grid_shapes(self):
|
|
144
|
+
u, v, U, V = create_uv_grid(n_u=21, n_v=31)
|
|
145
|
+
assert u.shape == (21,)
|
|
146
|
+
assert v.shape == (31,)
|
|
147
|
+
assert U.shape == (31, 21) or U.shape == (21, 31)
|
|
148
|
+
assert U.shape == V.shape
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Smoke tests for phased_array.visualization plotting functions."""
|
|
2
|
+
|
|
3
|
+
import matplotlib
|
|
4
|
+
|
|
5
|
+
matplotlib.use("Agg")
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import phased_array as pa
|
|
12
|
+
from phased_array.visualization import (
|
|
13
|
+
compute_pattern_uv_space,
|
|
14
|
+
plot_array_geometry,
|
|
15
|
+
plot_beam_squint,
|
|
16
|
+
plot_comparison_patterns,
|
|
17
|
+
plot_pattern_2d,
|
|
18
|
+
plot_pattern_contour,
|
|
19
|
+
plot_pattern_polar,
|
|
20
|
+
plot_pattern_uv_space,
|
|
21
|
+
plot_pattern_vs_frequency,
|
|
22
|
+
plot_subarray_delays,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
plotly = pytest.importorskip("plotly", reason="plotly not installed")
|
|
26
|
+
import plotly.graph_objects as go # noqa: E402
|
|
27
|
+
|
|
28
|
+
from phased_array.visualization import ( # noqa: E402
|
|
29
|
+
create_pattern_animation_plotly,
|
|
30
|
+
plot_array_geometry_3d_plotly,
|
|
31
|
+
plot_pattern_3d_cartesian_plotly,
|
|
32
|
+
plot_pattern_3d_plotly,
|
|
33
|
+
plot_pattern_uv_plotly,
|
|
34
|
+
plot_pattern_vs_frequency_plotly,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture(autouse=True)
|
|
39
|
+
def close_figures():
|
|
40
|
+
yield
|
|
41
|
+
plt.close("all")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture(scope="module")
|
|
45
|
+
def small_array():
|
|
46
|
+
geom = pa.create_rectangular_array(4, 4, dx=0.5, dy=0.5)
|
|
47
|
+
k = pa.wavelength_to_k(1.0)
|
|
48
|
+
weights = pa.steering_vector(k, geom.x, geom.y, 0, 0)
|
|
49
|
+
return geom, k, weights
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture(scope="module")
|
|
53
|
+
def pattern_cut(small_array):
|
|
54
|
+
geom, k, weights = small_array
|
|
55
|
+
angles = np.linspace(-90, 90, 181)
|
|
56
|
+
theta = np.deg2rad(np.abs(angles))
|
|
57
|
+
phi = np.where(angles >= 0, 0.0, np.pi)
|
|
58
|
+
af = pa.array_factor_vectorized(theta, phi, geom.x, geom.y, weights, k)
|
|
59
|
+
pattern_dB = 20 * np.log10(np.abs(af) / np.abs(af).max() + 1e-12)
|
|
60
|
+
return angles, pattern_dB
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.fixture(scope="module")
|
|
64
|
+
def full_pattern(small_array):
|
|
65
|
+
geom, k, weights = small_array
|
|
66
|
+
theta, phi, pattern_dB = pa.compute_full_pattern(
|
|
67
|
+
geom.x, geom.y, weights, k, n_theta=19, n_phi=37
|
|
68
|
+
)
|
|
69
|
+
return theta, phi, pattern_dB
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestMatplotlibPlots:
|
|
73
|
+
def test_plot_pattern_2d(self, pattern_cut):
|
|
74
|
+
ax = plot_pattern_2d(*pattern_cut)
|
|
75
|
+
assert ax is not None
|
|
76
|
+
|
|
77
|
+
def test_plot_pattern_2d_existing_ax(self, pattern_cut):
|
|
78
|
+
fig, ax_in = plt.subplots()
|
|
79
|
+
ax = plot_pattern_2d(*pattern_cut, ax=ax_in)
|
|
80
|
+
assert ax is ax_in
|
|
81
|
+
|
|
82
|
+
def test_plot_pattern_polar(self, pattern_cut):
|
|
83
|
+
ax = plot_pattern_polar(*pattern_cut)
|
|
84
|
+
assert ax is not None
|
|
85
|
+
|
|
86
|
+
def test_plot_pattern_contour(self, full_pattern):
|
|
87
|
+
theta, phi, pattern_dB = full_pattern
|
|
88
|
+
ax = plot_pattern_contour(np.rad2deg(theta), np.rad2deg(phi), pattern_dB)
|
|
89
|
+
assert ax is not None
|
|
90
|
+
|
|
91
|
+
def test_plot_array_geometry(self, small_array):
|
|
92
|
+
geom, _, weights = small_array
|
|
93
|
+
ax = plot_array_geometry(geom, weights=weights)
|
|
94
|
+
assert ax is not None
|
|
95
|
+
|
|
96
|
+
def test_plot_comparison_patterns(self, pattern_cut):
|
|
97
|
+
angles, pattern_dB = pattern_cut
|
|
98
|
+
ax = plot_comparison_patterns(
|
|
99
|
+
angles, {"a": pattern_dB, "b": pattern_dB - 3.0}
|
|
100
|
+
)
|
|
101
|
+
assert ax is not None
|
|
102
|
+
|
|
103
|
+
def test_plot_beam_squint(self):
|
|
104
|
+
freqs = np.linspace(9e9, 11e9, 5)
|
|
105
|
+
squint = {"phase": np.linspace(-2, 2, 5), "ttd": np.zeros(5)}
|
|
106
|
+
ax = plot_beam_squint(freqs, squint, center_frequency=10e9)
|
|
107
|
+
assert ax is not None
|
|
108
|
+
|
|
109
|
+
def test_plot_pattern_vs_frequency(self, pattern_cut):
|
|
110
|
+
angles, pattern_dB = pattern_cut
|
|
111
|
+
freqs = np.linspace(9e9, 11e9, 4)
|
|
112
|
+
patterns = np.tile(pattern_dB, (4, 1))
|
|
113
|
+
ax = plot_pattern_vs_frequency(angles, freqs, patterns, center_frequency=10e9)
|
|
114
|
+
assert ax is not None
|
|
115
|
+
|
|
116
|
+
def test_plot_subarray_delays(self):
|
|
117
|
+
arch = pa.create_rectangular_subarrays(8, 8, 4, 4, dx=0.5, dy=0.5)
|
|
118
|
+
delays = pa.compute_subarray_delays_ttd(arch, theta0_deg=20, phi0_deg=0)
|
|
119
|
+
ax = plot_subarray_delays(arch, delays)
|
|
120
|
+
assert ax is not None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestUVSpace:
|
|
124
|
+
def test_compute_pattern_uv_space(self, small_array):
|
|
125
|
+
geom, k, weights = small_array
|
|
126
|
+
u, v, pattern_dB = compute_pattern_uv_space(geom, weights, k, n_u=41, n_v=41)
|
|
127
|
+
assert pattern_dB.shape == (41, 41)
|
|
128
|
+
# Invisible region must be masked or finite-valued; peak is 0 dB
|
|
129
|
+
assert np.isclose(np.nanmax(pattern_dB), 0.0, atol=1e-6)
|
|
130
|
+
|
|
131
|
+
def test_plot_pattern_uv_space(self, small_array):
|
|
132
|
+
geom, k, weights = small_array
|
|
133
|
+
u, v, pattern_dB = compute_pattern_uv_space(geom, weights, k, n_u=41, n_v=41)
|
|
134
|
+
ax = plot_pattern_uv_space(
|
|
135
|
+
u, v, pattern_dB, show_grating_circles=True,
|
|
136
|
+
dx_wavelengths=0.5, dy_wavelengths=0.5,
|
|
137
|
+
)
|
|
138
|
+
assert ax is not None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestPlotlyPlots:
|
|
142
|
+
def test_plot_pattern_3d_plotly(self, full_pattern):
|
|
143
|
+
fig = plot_pattern_3d_plotly(*full_pattern)
|
|
144
|
+
assert isinstance(fig, go.Figure)
|
|
145
|
+
assert len(fig.data) >= 1
|
|
146
|
+
|
|
147
|
+
def test_plot_pattern_3d_cartesian_plotly(self, full_pattern):
|
|
148
|
+
theta, phi, pattern_dB = full_pattern
|
|
149
|
+
fig = plot_pattern_3d_cartesian_plotly(
|
|
150
|
+
np.rad2deg(theta), np.rad2deg(phi), pattern_dB
|
|
151
|
+
)
|
|
152
|
+
assert isinstance(fig, go.Figure)
|
|
153
|
+
|
|
154
|
+
def test_plot_array_geometry_3d_plotly(self, small_array):
|
|
155
|
+
geom, _, weights = small_array
|
|
156
|
+
fig = plot_array_geometry_3d_plotly(geom, weights=weights)
|
|
157
|
+
assert isinstance(fig, go.Figure)
|
|
158
|
+
|
|
159
|
+
def test_plot_pattern_uv_plotly(self, small_array):
|
|
160
|
+
geom, k, weights = small_array
|
|
161
|
+
u, v, pattern_dB = compute_pattern_uv_space(geom, weights, k, n_u=41, n_v=41)
|
|
162
|
+
fig = plot_pattern_uv_plotly(u, v, pattern_dB)
|
|
163
|
+
assert isinstance(fig, go.Figure)
|
|
164
|
+
|
|
165
|
+
def test_create_pattern_animation_plotly(self, full_pattern):
|
|
166
|
+
theta, phi, pattern_dB = full_pattern
|
|
167
|
+
fig = create_pattern_animation_plotly(
|
|
168
|
+
theta, phi, [pattern_dB, pattern_dB - 3.0], ["f0", "f1"]
|
|
169
|
+
)
|
|
170
|
+
assert isinstance(fig, go.Figure)
|
|
171
|
+
assert len(fig.frames) == 2
|
|
172
|
+
|
|
173
|
+
def test_plot_pattern_vs_frequency_plotly(self, pattern_cut):
|
|
174
|
+
angles, pattern_dB = pattern_cut
|
|
175
|
+
freqs = np.linspace(9e9, 11e9, 4)
|
|
176
|
+
patterns = np.tile(pattern_dB, (4, 1))
|
|
177
|
+
fig = plot_pattern_vs_frequency_plotly(
|
|
178
|
+
angles, freqs, patterns, center_frequency=10e9
|
|
179
|
+
)
|
|
180
|
+
assert isinstance(fig, go.Figure)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|