phased-array-modeling 1.3.1__tar.gz → 1.4.0__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.4.0}/PKG-INFO +13 -8
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/README.md +11 -5
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/__init__.py +26 -1
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/beamforming.py +7 -4
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/core.py +31 -7
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/geometry.py +32 -15
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/impairments.py +26 -5
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/polarization.py +5 -5
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/utils.py +19 -5
- phased_array_modeling-1.4.0/phased_array/vector_patterns.py +725 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/wideband.py +3 -1
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array_modeling.egg-info/PKG-INFO +13 -8
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array_modeling.egg-info/SOURCES.txt +5 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/pyproject.toml +2 -3
- phased_array_modeling-1.4.0/tests/test_docstrings.py +33 -0
- phased_array_modeling-1.4.0/tests/test_utils.py +148 -0
- phased_array_modeling-1.4.0/tests/test_vector_patterns.py +382 -0
- phased_array_modeling-1.4.0/tests/test_visualization.py +180 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/LICENSE +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/coordinates.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/export.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/visualization.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array_modeling.egg-info/dependency_links.txt +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array_modeling.egg-info/requires.txt +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array_modeling.egg-info/top_level.txt +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/setup.cfg +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_beamforming.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_coordinates.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_core.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_export.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_geometry.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_impairments.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_polarization.py +0 -0
- {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/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
|
+
Version: 1.4.0
|
|
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
|
|
@@ -15,14 +15,13 @@ Classifier: Intended Audience :: Education
|
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.9
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
22
|
Classifier: Topic :: Scientific/Engineering
|
|
24
23
|
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
25
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.9
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
27
26
|
License-File: LICENSE
|
|
28
27
|
Requires-Dist: numpy>=1.20.0
|
|
@@ -55,14 +54,15 @@ Dynamic: license-file
|
|
|
55
54
|
[](https://phased-array-antenna-model.streamlit.app/)
|
|
56
55
|
[](https://phased-array-antenna-model.readthedocs.io/en/latest/)
|
|
57
56
|
|
|
58
|
-
A comprehensive Python library for computing and visualizing phased array antenna radiation patterns. Features
|
|
57
|
+
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
58
|
|
|
60
59
|
## Features
|
|
61
60
|
|
|
62
|
-
- **High Performance**: Vectorized array factor computation (
|
|
61
|
+
- **High Performance**: Vectorized array factor computation (50-100x faster than naive loops)
|
|
63
62
|
- **Multiple Geometries**: Rectangular, triangular, circular, cylindrical, spherical, sparse/thinned arrays
|
|
64
63
|
- **Beamforming**: Amplitude tapering (Taylor, Chebyshev, etc.), null steering, multi-beam
|
|
65
64
|
- **Realistic Impairments**: Mutual coupling, phase quantization, element failures, scan blindness
|
|
65
|
+
- **Polarized Patterns**: Vector (E-theta, E-phi) patterns with dipole/patch/CP element models, co/cross-pol cuts, axial ratio and XPD maps, measured element pattern import
|
|
66
66
|
- **Visualization**: 2D matplotlib, interactive 3D Plotly, UV-space representation
|
|
67
67
|
- **Subarray Support**: Subarray-level beamforming with quantized phase shifters
|
|
68
68
|
- **Data Export**: CSV, JSON, and NumPy formats for patterns, weights, and geometry
|
|
@@ -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,13 @@ 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
|
+
├── vector_patterns.py # Polarized elements, vector AF, co/cross patterns
|
|
243
|
+
├── coordinates.py # Antenna/radar/cone frames, pattern rotation
|
|
244
|
+
├── export.py # CSV/JSON/NPZ export, summary reports
|
|
240
245
|
├── visualization.py # 2D, 3D Plotly, UV-space plotting
|
|
241
246
|
└── utils.py # Coordinate transforms, helpers
|
|
242
247
|
```
|
|
@@ -278,7 +283,7 @@ If you use this library in your research, please cite:
|
|
|
278
283
|
author = {Hodge, John},
|
|
279
284
|
title = {Phased Array Antenna Modeling},
|
|
280
285
|
url = {https://github.com/jman4162/Phased-Array-Antenna-Model},
|
|
281
|
-
year = {
|
|
286
|
+
year = {2026}
|
|
282
287
|
}
|
|
283
288
|
```
|
|
284
289
|
|
|
@@ -9,14 +9,15 @@
|
|
|
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
|
|
20
|
+
- **Polarized Patterns**: Vector (E-theta, E-phi) patterns with dipole/patch/CP element models, co/cross-pol cuts, axial ratio and XPD maps, measured element pattern import
|
|
20
21
|
- **Visualization**: 2D matplotlib, interactive 3D Plotly, UV-space representation
|
|
21
22
|
- **Subarray Support**: Subarray-level beamforming with quantized phase shifters
|
|
22
23
|
- **Data Export**: CSV, JSON, and NumPy formats for patterns, weights, and geometry
|
|
@@ -35,7 +36,7 @@ The Streamlit app provides an interactive interface for:
|
|
|
35
36
|
|
|
36
37
|
## Installation
|
|
37
38
|
|
|
38
|
-
### From
|
|
39
|
+
### From PyPI (recommended)
|
|
39
40
|
|
|
40
41
|
```bash
|
|
41
42
|
pip install phased-array-modeling
|
|
@@ -189,8 +190,13 @@ For hands-on learning, see the [demo notebook](Phased_Array_Demo.ipynb) which co
|
|
|
189
190
|
phased_array/
|
|
190
191
|
├── core.py # Vectorized AF, FFT, steering, element patterns
|
|
191
192
|
├── geometry.py # Array geometries and subarray architectures
|
|
192
|
-
├── beamforming.py # Tapering, null steering, multi-beam
|
|
193
|
+
├── beamforming.py # Tapering, null steering, multi-beam, adaptive
|
|
193
194
|
├── impairments.py # Coupling, quantization, failures, scan blindness
|
|
195
|
+
├── wideband.py # True-time-delay steering, beam squint, bandwidth
|
|
196
|
+
├── polarization.py # Jones/Stokes, axial ratio, XPD, Ludwig-3
|
|
197
|
+
├── vector_patterns.py # Polarized elements, vector AF, co/cross patterns
|
|
198
|
+
├── coordinates.py # Antenna/radar/cone frames, pattern rotation
|
|
199
|
+
├── export.py # CSV/JSON/NPZ export, summary reports
|
|
194
200
|
├── visualization.py # 2D, 3D Plotly, UV-space plotting
|
|
195
201
|
└── utils.py # Coordinate transforms, helpers
|
|
196
202
|
```
|
|
@@ -232,7 +238,7 @@ If you use this library in your research, please cite:
|
|
|
232
238
|
author = {Hodge, John},
|
|
233
239
|
title = {Phased Array Antenna Modeling},
|
|
234
240
|
url = {https://github.com/jman4162/Phased-Array-Antenna-Model},
|
|
235
|
-
year = {
|
|
241
|
+
year = {2026}
|
|
236
242
|
}
|
|
237
243
|
```
|
|
238
244
|
|
|
@@ -9,6 +9,7 @@ antenna radiation patterns, including:
|
|
|
9
9
|
- Various array geometries (rectangular, circular, conformal, sparse)
|
|
10
10
|
- Beamforming techniques (tapering, null steering, multi-beam)
|
|
11
11
|
- Realistic impairments (mutual coupling, quantization, failures, scan blindness)
|
|
12
|
+
- Vector (polarized) patterns: co/cross-pol, axial ratio, XPD, conformal
|
|
12
13
|
- Interactive 3D visualization with Plotly
|
|
13
14
|
- UV-space pattern representation
|
|
14
15
|
|
|
@@ -32,7 +33,7 @@ Example
|
|
|
32
33
|
>>> theta, phi, pattern_dB = pa.compute_full_pattern(geom.x, geom.y, weights, k)
|
|
33
34
|
"""
|
|
34
35
|
|
|
35
|
-
__version__ = "1.
|
|
36
|
+
__version__ = "1.4.0"
|
|
36
37
|
|
|
37
38
|
# Beamforming functions
|
|
38
39
|
from .beamforming import ( # Amplitude tapers; Null steering; Multiple beams; Beam spoiling; Adaptive
|
|
@@ -95,6 +96,16 @@ from .utils import (azel_to_thetaphi, create_theta_phi_grid, create_uv_grid,
|
|
|
95
96
|
frequency_to_wavelength, is_visible_region, linear_to_db,
|
|
96
97
|
normalize_pattern, rad2deg, theta_phi_to_uv,
|
|
97
98
|
thetaphi_to_azel, uv_to_theta_phi, wavelength_to_k)
|
|
99
|
+
# Vector (polarized) pattern functions
|
|
100
|
+
from .vector_patterns import (GriddedElementPattern, VectorPattern,
|
|
101
|
+
compute_co_cross_pattern_cuts,
|
|
102
|
+
compute_full_vector_pattern,
|
|
103
|
+
cos_q_polarized_element, crossed_dipole_element,
|
|
104
|
+
dipole_element, dual_pol_weights,
|
|
105
|
+
element_rotation_matrices,
|
|
106
|
+
global_to_local_angles, ideal_patch_element,
|
|
107
|
+
vector_array_factor_conformal,
|
|
108
|
+
vector_total_pattern)
|
|
98
109
|
# Visualization functions
|
|
99
110
|
from .visualization import ( # 2D matplotlib plots; UV-space; 3D Plotly plots; Wideband
|
|
100
111
|
compute_pattern_uv_space, create_pattern_animation_plotly,
|
|
@@ -278,4 +289,18 @@ __all__ = [
|
|
|
278
289
|
"rotation_matrix_pitch",
|
|
279
290
|
"rotation_matrix_yaw",
|
|
280
291
|
"rotate_pattern",
|
|
292
|
+
# Vector (polarized) patterns
|
|
293
|
+
"VectorPattern",
|
|
294
|
+
"GriddedElementPattern",
|
|
295
|
+
"dipole_element",
|
|
296
|
+
"ideal_patch_element",
|
|
297
|
+
"cos_q_polarized_element",
|
|
298
|
+
"crossed_dipole_element",
|
|
299
|
+
"vector_total_pattern",
|
|
300
|
+
"compute_full_vector_pattern",
|
|
301
|
+
"compute_co_cross_pattern_cuts",
|
|
302
|
+
"dual_pol_weights",
|
|
303
|
+
"element_rotation_matrices",
|
|
304
|
+
"global_to_local_angles",
|
|
305
|
+
"vector_array_factor_conformal",
|
|
281
306
|
]
|
|
@@ -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)
|
|
@@ -32,6 +32,13 @@ class ArrayGeometry:
|
|
|
32
32
|
Element normal z-components
|
|
33
33
|
element_indices : ndarray, optional
|
|
34
34
|
Original indices before any thinning (for tracking)
|
|
35
|
+
tx : ndarray, optional
|
|
36
|
+
Element tangent x-components (local x-axis / polarization
|
|
37
|
+
reference for conformal vector patterns)
|
|
38
|
+
ty : ndarray, optional
|
|
39
|
+
Element tangent y-components
|
|
40
|
+
tz : ndarray, optional
|
|
41
|
+
Element tangent z-components
|
|
35
42
|
"""
|
|
36
43
|
x: np.ndarray
|
|
37
44
|
y: np.ndarray
|
|
@@ -40,6 +47,9 @@ class ArrayGeometry:
|
|
|
40
47
|
ny: Optional[np.ndarray] = None
|
|
41
48
|
nz: Optional[np.ndarray] = None
|
|
42
49
|
element_indices: Optional[np.ndarray] = None
|
|
50
|
+
tx: Optional[np.ndarray] = None
|
|
51
|
+
ty: Optional[np.ndarray] = None
|
|
52
|
+
tz: Optional[np.ndarray] = None
|
|
43
53
|
|
|
44
54
|
@property
|
|
45
55
|
def n_elements(self) -> int:
|
|
@@ -74,7 +84,10 @@ class ArrayGeometry:
|
|
|
74
84
|
nx=self.nx.copy() if self.nx is not None else None,
|
|
75
85
|
ny=self.ny.copy() if self.ny is not None else None,
|
|
76
86
|
nz=self.nz.copy() if self.nz is not None else None,
|
|
77
|
-
element_indices=self.element_indices.copy() if self.element_indices is not None else None
|
|
87
|
+
element_indices=self.element_indices.copy() if self.element_indices is not None else None,
|
|
88
|
+
tx=self.tx.copy() if self.tx is not None else None,
|
|
89
|
+
ty=self.ty.copy() if self.ty is not None else None,
|
|
90
|
+
tz=self.tz.copy() if self.tz is not None else None
|
|
78
91
|
)
|
|
79
92
|
|
|
80
93
|
|
|
@@ -465,14 +478,10 @@ def create_concentric_rings_array(
|
|
|
465
478
|
"""
|
|
466
479
|
x_list = []
|
|
467
480
|
y_list = []
|
|
468
|
-
nx_list = []
|
|
469
|
-
ny_list = []
|
|
470
481
|
|
|
471
482
|
if include_center:
|
|
472
483
|
x_list.append(0.0)
|
|
473
484
|
y_list.append(0.0)
|
|
474
|
-
nx_list.append(0.0)
|
|
475
|
-
ny_list.append(0.0)
|
|
476
485
|
|
|
477
486
|
if isinstance(elements_per_ring, int):
|
|
478
487
|
elements_per_ring = [elements_per_ring] * n_rings
|
|
@@ -484,18 +493,18 @@ def create_concentric_rings_array(
|
|
|
484
493
|
for angle in angles:
|
|
485
494
|
x_list.append(radius * np.cos(angle))
|
|
486
495
|
y_list.append(radius * np.sin(angle))
|
|
487
|
-
nx_list.append(np.cos(angle))
|
|
488
|
-
ny_list.append(np.sin(angle))
|
|
489
496
|
|
|
490
497
|
x = np.array(x_list)
|
|
491
498
|
y = np.array(y_list)
|
|
492
499
|
|
|
500
|
+
# Planar array: all elements face broadside (+z), like the other
|
|
501
|
+
# planar factories
|
|
493
502
|
return ArrayGeometry(
|
|
494
503
|
x=x, y=y,
|
|
495
504
|
z=np.zeros_like(x),
|
|
496
|
-
nx=np.
|
|
497
|
-
ny=np.
|
|
498
|
-
nz=np.
|
|
505
|
+
nx=np.zeros_like(x),
|
|
506
|
+
ny=np.zeros_like(x),
|
|
507
|
+
nz=np.ones_like(x),
|
|
499
508
|
element_indices=np.arange(len(x))
|
|
500
509
|
)
|
|
501
510
|
|
|
@@ -1001,6 +1010,13 @@ def array_factor_conformal(
|
|
|
1001
1010
|
obs_y = np.sin(theta_flat) * np.sin(phi_flat)
|
|
1002
1011
|
obs_z = np.cos(theta_flat)
|
|
1003
1012
|
|
|
1013
|
+
has_normals = (geometry.nx is not None and geometry.ny is not None
|
|
1014
|
+
and geometry.nz is not None)
|
|
1015
|
+
if has_normals and element_pattern_func is not None:
|
|
1016
|
+
from .vector_patterns import (element_rotation_matrices,
|
|
1017
|
+
global_to_local_angles)
|
|
1018
|
+
R = element_rotation_matrices(geometry)
|
|
1019
|
+
|
|
1004
1020
|
AF = np.zeros(n_angles, dtype=complex)
|
|
1005
1021
|
|
|
1006
1022
|
for i in range(geometry.n_elements):
|
|
@@ -1010,7 +1026,7 @@ def array_factor_conformal(
|
|
|
1010
1026
|
phase += k * geometry.z[i] * obs_z
|
|
1011
1027
|
|
|
1012
1028
|
# Element pattern correction for orientation
|
|
1013
|
-
if
|
|
1029
|
+
if has_normals:
|
|
1014
1030
|
# Cosine of angle between observation direction and element normal
|
|
1015
1031
|
cos_angle = (geometry.nx[i] * obs_x +
|
|
1016
1032
|
geometry.ny[i] * obs_y +
|
|
@@ -1018,9 +1034,11 @@ def array_factor_conformal(
|
|
|
1018
1034
|
cos_angle = np.maximum(cos_angle, 0) # Only forward hemisphere
|
|
1019
1035
|
|
|
1020
1036
|
if element_pattern_func is not None:
|
|
1021
|
-
#
|
|
1022
|
-
local_theta =
|
|
1023
|
-
|
|
1037
|
+
# Observation angles in the element's local frame
|
|
1038
|
+
local_theta, local_phi = global_to_local_angles(
|
|
1039
|
+
R[i], theta_flat, phi_flat
|
|
1040
|
+
)
|
|
1041
|
+
element_gain = element_pattern_func(local_theta, local_phi, **element_kwargs)
|
|
1024
1042
|
else:
|
|
1025
1043
|
element_gain = cos_angle # Simple cosine pattern
|
|
1026
1044
|
else:
|
|
@@ -1241,7 +1259,6 @@ def overlapped_subarray_weights(
|
|
|
1241
1259
|
"""
|
|
1242
1260
|
if not architecture.overlapped:
|
|
1243
1261
|
# Fall back to standard subarray weights
|
|
1244
|
-
from .beamforming import compute_subarray_weights
|
|
1245
1262
|
return compute_subarray_weights(architecture, k, theta0_deg, phi0_deg)
|
|
1246
1263
|
|
|
1247
1264
|
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:
|
|
@@ -81,13 +81,13 @@ def stokes_parameters(jones: np.ndarray) -> Tuple[float, float, float, float]:
|
|
|
81
81
|
Returns
|
|
82
82
|
-------
|
|
83
83
|
S0 : float or ndarray
|
|
84
|
-
Total intensity:
|
|
84
|
+
Total intensity: ``|Ex|^2 + |Ey|^2``
|
|
85
85
|
S1 : float or ndarray
|
|
86
|
-
Linear horizontal-vertical:
|
|
86
|
+
Linear horizontal-vertical: ``|Ex|^2 - |Ey|^2``
|
|
87
87
|
S2 : float or ndarray
|
|
88
|
-
Linear +45/-45: 2*Re(Ex*Ey
|
|
88
|
+
Linear +45/-45: ``2*Re(Ex*conj(Ey))``
|
|
89
89
|
S3 : float or ndarray
|
|
90
|
-
Circular right-left: 2*Im(Ex*Ey
|
|
90
|
+
Circular right-left: ``2*Im(Ex*conj(Ey))``
|
|
91
91
|
|
|
92
92
|
Examples
|
|
93
93
|
--------
|
|
@@ -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
|
|