phased-array-modeling 1.3.0__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.
Files changed (32) hide show
  1. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/PKG-INFO +10 -6
  2. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/README.md +9 -5
  3. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/__init__.py +14 -11
  4. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/beamforming.py +8 -4
  5. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/core.py +31 -7
  6. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/geometry.py +5 -10
  7. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/impairments.py +26 -5
  8. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/polarization.py +1 -1
  9. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/utils.py +19 -5
  10. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/wideband.py +3 -1
  11. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/PKG-INFO +10 -6
  12. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/SOURCES.txt +3 -0
  13. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/pyproject.toml +1 -1
  14. phased_array_modeling-1.3.2/tests/test_docstrings.py +33 -0
  15. phased_array_modeling-1.3.2/tests/test_utils.py +148 -0
  16. phased_array_modeling-1.3.2/tests/test_visualization.py +180 -0
  17. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/LICENSE +0 -0
  18. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/coordinates.py +0 -0
  19. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/export.py +0 -0
  20. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array/visualization.py +0 -0
  21. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/dependency_links.txt +0 -0
  22. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/requires.txt +0 -0
  23. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/phased_array_modeling.egg-info/top_level.txt +0 -0
  24. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/setup.cfg +0 -0
  25. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/tests/test_beamforming.py +0 -0
  26. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/tests/test_coordinates.py +0 -0
  27. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/tests/test_core.py +0 -0
  28. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/tests/test_export.py +0 -0
  29. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/tests/test_geometry.py +0 -0
  30. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/tests/test_impairments.py +0 -0
  31. {phased_array_modeling-1.3.0 → phased_array_modeling-1.3.2}/tests/test_polarization.py +0 -0
  32. {phased_array_modeling-1.3.0 → 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.0
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
  [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://phased-array-antenna-model.streamlit.app/)
56
56
  [![Documentation](https://readthedocs.org/projects/phased-array-antenna-model/badge/?version=latest)](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 **125x faster** vectorized computations, multiple array geometries, advanced beamforming, and interactive 3D visualization.
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 (125x faster than naive loops)
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 GitHub (recommended)
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 = {2024}
285
+ year = {2026}
282
286
  }
283
287
  ```
284
288
 
@@ -9,11 +9,11 @@
9
9
  [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://phased-array-antenna-model.streamlit.app/)
10
10
  [![Documentation](https://readthedocs.org/projects/phased-array-antenna-model/badge/?version=latest)](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 **125x faster** vectorized computations, multiple array geometries, advanced beamforming, and interactive 3D visualization.
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 (125x faster than naive loops)
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 GitHub (recommended)
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 = {2024}
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.0"
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
@@ -45,8 +45,13 @@ from .beamforming import ( # Amplitude tapers; Null steering; Multiple beams; B
45
45
  hamming_taper_2d, hanning_taper_1d, hanning_taper_2d, monopulse_weights,
46
46
  multi_beam_weights_orthogonal, multi_beam_weights_superposition,
47
47
  null_steering_lcmv, null_steering_projection, plot_adapted_pattern,
48
- quadratic_phase_spoil, spoiled_beam_gain, spoiled_beamwidth, taylor_taper_1d,
49
- taylor_taper_2d)
48
+ quadratic_phase_spoil, spoiled_beam_gain, spoiled_beamwidth,
49
+ taylor_taper_1d, taylor_taper_2d)
50
+ # Coordinate transformation functions
51
+ from .coordinates import (antenna_to_cone, antenna_to_radar, cone_to_antenna,
52
+ radar_to_antenna, rotate_pattern,
53
+ rotation_matrix_pitch, rotation_matrix_roll,
54
+ rotation_matrix_yaw)
50
55
  # Core computation functions
51
56
  from .core import (array_factor_fft, array_factor_uv, array_factor_vectorized,
52
57
  compute_directivity, compute_full_pattern,
@@ -78,6 +83,12 @@ from .impairments import ( # Mutual coupling; Phase quantization; Element failu
78
83
  mutual_coupling_matrix_theoretical, quantization_rms_error,
79
84
  quantization_sidelobe_increase, quantize_phase, scan_blindness_model,
80
85
  simulate_element_failures, surface_wave_scan_angle, vswr_vs_scan)
86
+ # Polarization functions
87
+ from .polarization import (axial_ratio, co_pol_pattern,
88
+ cross_pol_discrimination, cross_pol_pattern,
89
+ jones_vector, ludwig3_decomposition,
90
+ polarization_loss_factor, stokes_parameters,
91
+ tilt_angle)
81
92
  # Utility functions
82
93
  from .utils import (azel_to_thetaphi, create_theta_phi_grid, create_uv_grid,
83
94
  db_to_linear, deg2rad, frequency_to_k,
@@ -93,14 +104,6 @@ from .visualization import ( # 2D matplotlib plots; UV-space; 3D Plotly plots;
93
104
  plot_pattern_contour, plot_pattern_polar, plot_pattern_uv_plotly,
94
105
  plot_pattern_uv_space, plot_pattern_vs_frequency,
95
106
  plot_pattern_vs_frequency_plotly, plot_subarray_delays)
96
- # Polarization functions
97
- from .polarization import (axial_ratio, co_pol_pattern, cross_pol_discrimination,
98
- cross_pol_pattern, jones_vector, ludwig3_decomposition,
99
- polarization_loss_factor, stokes_parameters, tilt_angle)
100
- # Coordinate transformation functions
101
- from .coordinates import (antenna_to_cone, antenna_to_radar, cone_to_antenna,
102
- radar_to_antenna, rotate_pattern, rotation_matrix_pitch,
103
- rotation_matrix_roll, rotation_matrix_yaw)
104
107
  # Wideband / TTD functions
105
108
  from .wideband import (analyze_instantaneous_bandwidth, compare_steering_modes,
106
109
  compute_beam_squint, compute_pattern_vs_frequency,
@@ -391,7 +391,10 @@ def null_steering_projection(
391
391
 
392
392
  Verify null depth:
393
393
 
394
- >>> null_depth = pa.compute_null_depth(geom, k, weights, null_dirs[0])
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=[(30, 0)]
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=[(30, 0)],
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
@@ -1381,6 +1384,7 @@ def plot_adapted_pattern(
1381
1384
  ... ) # doctest: +SKIP
1382
1385
  """
1383
1386
  import matplotlib.pyplot as plt
1387
+
1384
1388
  from .core import array_factor_vectorized
1385
1389
  from .utils import linear_to_db
1386
1390
 
@@ -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 in basic model)
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
- Cosine exponent for additional roll-off
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
- # Interpolate for more accurate beamwidth
672
- hpbw = angles_deg[right_idx] - angles_deg[left_idx]
673
-
674
- return abs(hpbw)
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.array(nx_list),
497
- ny=np.array(ny_list),
498
- nz=np.zeros_like(x),
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
- # C = (I + S)(I - S)^(-1) for impedance normalization
96
- # Or simply use S directly for voltage coupling
97
- C = np.eye(n, dtype=complex) + s_parameters
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, weights, failure_rates=[0.0, 0.05, 0.1]
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, -S0, atol=1e-10)
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(az), np.tan(el))
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.cos(theta))
73
- az = np.arctan2(np.sin(theta) * np.sin(phi), np.sin(theta) * np.cos(phi))
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
- ... center_freq=10e9, bandwidth=2e9, n_freqs=11
89
+ ... center_frequency=10e9,
90
+ ... frequencies=np.linspace(9e9, 11e9, 5)
89
91
  ... )
90
92
  """
91
93
  theta0 = np.deg2rad(theta0_deg)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phased-array-modeling
3
- Version: 1.3.0
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
  [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://phased-array-antenna-model.streamlit.app/)
56
56
  [![Documentation](https://readthedocs.org/projects/phased-array-antenna-model/badge/?version=latest)](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 **125x faster** vectorized computations, multiple array geometries, advanced beamforming, and interactive 3D visualization.
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 (125x faster than naive loops)
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 GitHub (recommended)
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 = {2024}
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.0"
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)