Skip to content
John Hodge

← Blog

phased-array-modeling 1.4.0: polarized patterns for phased arrays

Versions 1.3.2 and 1.4.0 of phased-array-modeling are on PyPI. The headline is v1.4.0: the pattern engine now computes vector fields, complex E-theta and E-phi components, instead of scalar magnitudes, which makes polarization a first-class output rather than an afterthought. v1.3.2 is the other kind of release, a batch of correctness fixes that came out of turning every documented example into a test.

If the package is new to you, the intro post covers the fundamentals: array factors, tapering, impairments, and the trade-study layer above them. Since that post, three releases have landed; v1.3.0 added six analysis modules (polarization math, coordinate transforms, beam spoiling, overlapped subarrays, adaptive nulling, and active impedance), and the documentation covers them. The package installs as pip install phased-array-modeling and imports as phased_array.

Patterns become vectors

A scalar pattern answers one question: how much power goes in each direction. Real antennas raise a second one: in what polarization. An x-oriented dipole radiating broadside is cleanly linear, but scan away from boresight, or look in the diagonal planes, and cross-polarized energy appears. Scalar array models cannot show any of that.

The new vector_patterns module wires the v1.3.0 polarization math into the pattern engine. An element model is now any callable that returns complex (E_theta, E_phi) components, and four factories ship: dipole_element() (x, y, or z Hertzian dipole), ideal_patch_element() (cos-power co-pol with zero cross-pol by construction), cos_q_polarized_element() (a cos-q roll-off in an arbitrary Jones polarization state), and crossed_dipole_element() (a circularly polarized turnstile; a phase difference of minus 90 degrees gives RHCP, plus 90 LHCP). The array result comes back as a VectorPattern with power, Ludwig-3 co_cross() decomposition, axial_ratio_map(), and xpd_map() methods.

The cut-level workflow is one call:

import numpy as np
from phased_array import compute_co_cross_pattern_cuts, dipole_element

theta_deg, co_dB, cross_dB = compute_co_cross_pattern_cuts(
    x, y, weights, k,
    element_func=dipole_element("x"),
    phi_cut_deg=45.0,
)
Two pattern cuts of an 8 by 8 array with x-dipole elements steered to 30 degrees: the phi equals 0 principal-plane cut shows co-polarization only, while the phi equals 45 diagonal cut shows cross-polarization rising to about minus 15 dB at wide angles

Generated with the code above: an 8x8 half-wavelength array of x-dipole elements steered to 30 degrees. In the principal plane the cross-polarization is below the plot floor; in the 45-degree diagonal cut it climbs to about -15 dB at wide angles, which is exactly the behavior a scalar pattern model cannot show.

For dual-polarized apertures, dual_pol_weights() scales the x- and y-port excitations to synthesize a target Jones state, so a slant-45 or circular polarization becomes a weight calculation instead of a hand-derived phase table.

Measured elements plug in

Analytic element models only go so far; the pattern that matters is the embedded element pattern from a full-wave solver or an anechoic chamber. GriddedElementPattern wraps a measured or simulated (E_theta, E_phi) grid with an interpolator and is accepted anywhere an element function is, so the same array code runs on ideal dipoles or on your chamber data. When only scalar magnitude data exists, GriddedElementPattern.from_scalar() combines it with a Jones vector to build a polarized pattern. This is the bridge between element-level simulation or measurement and array-level analysis, and it needs no change to the rest of a script.

Conformal arrays get polarization right

On a curved aperture, every element points somewhere different, and so does its polarization. The new conformal path handles this properly: element_rotation_matrices() builds a local frame per element from its normal (and an optional tangent, via new ArrayGeometry fields), global_to_local_angles() maps each far-field direction into that frame, and vector_array_factor_conformal() evaluates each element locally, rotates its field back to the global spherical basis, and sums. Per-element embedded patterns are supported, so a cylinder with different edge and center elements is expressible. The conformal vector path loops per element rather than vectorizing across the aperture; it favors correctness over speed, and the planar vector path stays fully vectorized.

One behavior change rides along: array_factor_conformal() now passes local theta and local phi, both derived from the element’s rotation matrix, to element pattern functions. The bundled phi-independent elements produce identical results; custom phi-dependent callables now receive correct local angles where they previously got a mix of local theta and global phi.

v1.3.2 fixed real bugs

The fixes release deserves its own accounting, because several bugs were the kind that produce plausible wrong answers. overlapped_subarray_weights() raised an ImportError for non-overlapped architectures. mutual_coupling_matrix_measured() did not implement its documented impedance formulation; it now computes C = (I + S)(I - S)^-1 as documented, with the previous behavior available via formulation='voltage'. The az/el coordinate transforms used inconsistent conventions and were not mutual inverses; they now round-trip exactly under the documented convention. compute_half_power_beamwidth() snapped to grid samples and now interpolates the -3 dB crossings. And several docstring examples called functions with wrong arguments or asserted wrong values.

The common thread is the last item. Every module docstring now runs as a doctest in CI, and 48 new tests cover previously untested modules, which is how most of these surfaced. The package now carries 253 tests, 36 of them for the new vector engine.

Upgrade notes

Try it

pip install phased-array-modeling

The docs gained a polarization user guide and API pages for the new module. The Streamlit app has a new Polarization page with the element models, co/cross cuts, and boresight axial ratio, and the demo notebook in the repository walks the same material. For intuition about polarization itself, the ellipse and handedness live interactively in OpenEM Wave Lab.

phased-array-modeling is an independent project I build on my own time. The views are my own and do not represent any current or former employer.

Frequently asked questions

What does the vector pattern engine in v1.4.0 add?

Patterns are now computed as complex (E-theta, E-phi) field components instead of scalar magnitudes, so polarization becomes a first-class output: co- and cross-polarized cuts with a Ludwig-3 reference, axial ratio maps, cross-polarization discrimination (XPD) maps, and dual-polarized weight synthesis.

Can I use measured or simulated element patterns?

Yes. GriddedElementPattern wraps a measured or simulated (E-theta, E-phi) grid with an interpolator and is usable anywhere an element function is accepted. A from_scalar constructor builds a polarized pattern from a magnitude grid plus a Jones vector when only scalar data exists.

What changed for conformal arrays?

The conformal path is now polarization-correct: each element is evaluated in its own local frame and the fields are rotated back to the global spherical basis. Separately, array_factor_conformal now passes local theta and phi to element functions; bundled phi-independent elements are unchanged, but custom phi-dependent callables now receive correct local angles.

What should I check before upgrading?

v1.4.0 requires Python 3.9 or later. The v1.3.2 fixes changed two defaults: mutual_coupling_matrix_measured now implements the documented impedance formulation (the old behavior is available via formulation='voltage'), and the az/el transforms now round-trip exactly under the documented convention. The cos_exp_phi parameter is deprecated in favor of cos_q_polarized_element.

More in Electromagnetics & RF