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.
Files changed (34) hide show
  1. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/PKG-INFO +13 -8
  2. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/README.md +11 -5
  3. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/__init__.py +26 -1
  4. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/beamforming.py +7 -4
  5. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/core.py +31 -7
  6. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/geometry.py +32 -15
  7. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/impairments.py +26 -5
  8. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/polarization.py +5 -5
  9. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/utils.py +19 -5
  10. phased_array_modeling-1.4.0/phased_array/vector_patterns.py +725 -0
  11. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/wideband.py +3 -1
  12. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array_modeling.egg-info/PKG-INFO +13 -8
  13. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array_modeling.egg-info/SOURCES.txt +5 -0
  14. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/pyproject.toml +2 -3
  15. phased_array_modeling-1.4.0/tests/test_docstrings.py +33 -0
  16. phased_array_modeling-1.4.0/tests/test_utils.py +148 -0
  17. phased_array_modeling-1.4.0/tests/test_vector_patterns.py +382 -0
  18. phased_array_modeling-1.4.0/tests/test_visualization.py +180 -0
  19. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/LICENSE +0 -0
  20. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/coordinates.py +0 -0
  21. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/export.py +0 -0
  22. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array/visualization.py +0 -0
  23. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array_modeling.egg-info/dependency_links.txt +0 -0
  24. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array_modeling.egg-info/requires.txt +0 -0
  25. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/phased_array_modeling.egg-info/top_level.txt +0 -0
  26. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/setup.cfg +0 -0
  27. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_beamforming.py +0 -0
  28. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_coordinates.py +0 -0
  29. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_core.py +0 -0
  30. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_export.py +0 -0
  31. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_geometry.py +0 -0
  32. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_impairments.py +0 -0
  33. {phased_array_modeling-1.3.1 → phased_array_modeling-1.4.0}/tests/test_polarization.py +0 -0
  34. {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.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.8
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
  [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://phased-array-antenna-model.streamlit.app/)
56
55
  [![Documentation](https://readthedocs.org/projects/phased-array-antenna-model/badge/?version=latest)](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 **125x faster** vectorized computations, multiple array geometries, advanced beamforming, and interactive 3D visualization.
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 (125x faster than naive loops)
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 GitHub (recommended)
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 = {2024}
286
+ year = {2026}
282
287
  }
283
288
  ```
284
289
 
@@ -9,14 +9,15 @@
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
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 GitHub (recommended)
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 = {2024}
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.3.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(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
@@ -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)
@@ -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.array(nx_list),
497
- ny=np.array(ny_list),
498
- nz=np.zeros_like(x),
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 geometry.nx is not None and geometry.ny is not None and geometry.nz is not None:
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
- # Local theta for this element (angle from its normal)
1022
- local_theta = np.arccos(np.clip(cos_angle, -1, 1))
1023
- element_gain = element_pattern_func(local_theta, phi_flat, **element_kwargs)
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
- # 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:
@@ -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: |Ex|^2 + |Ey|^2
84
+ Total intensity: ``|Ex|^2 + |Ey|^2``
85
85
  S1 : float or ndarray
86
- Linear horizontal-vertical: |Ex|^2 - |Ey|^2
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, -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