"""
Polarization analysis and manipulation for phased array antennas.
Includes Jones vectors, Stokes parameters, axial ratio calculations,
and Ludwig-3 co/cross-pol decomposition.
"""
from typing import Tuple, Union
import numpy as np
ArrayLike = Union[np.ndarray, float]
[docs]
def jones_vector(
Ex: ArrayLike,
Ey: ArrayLike,
phase_diff: float = 0.0
) -> np.ndarray:
"""
Create a Jones vector representing polarization state.
The Jones vector describes the amplitude and phase of the electric
field components in a plane transverse to propagation.
Parameters
----------
Ex : array_like
Amplitude of x-component (horizontal)
Ey : array_like
Amplitude of y-component (vertical)
phase_diff : float
Phase difference between Ey and Ex in radians
Returns
-------
jones : ndarray
Complex Jones vector [Ex, Ey * exp(j * phase_diff)]
Examples
--------
Linear horizontal polarization:
>>> import numpy as np
>>> import phased_array as pa
>>> j = pa.jones_vector(1.0, 0.0)
>>> np.allclose(j, [1, 0])
True
Right-hand circular polarization:
>>> j = pa.jones_vector(1.0, 1.0, phase_diff=-np.pi/2)
>>> ar = pa.axial_ratio(j)
>>> np.isclose(ar, 1.0, atol=1e-10) # AR = 1 for circular
True
Left-hand circular polarization:
>>> j = pa.jones_vector(1.0, 1.0, phase_diff=np.pi/2)
>>> ar = pa.axial_ratio(j)
>>> np.isclose(ar, 1.0, atol=1e-10)
True
"""
Ex = np.asarray(Ex, dtype=complex)
Ey = np.asarray(Ey, dtype=complex)
return np.array([Ex, Ey * np.exp(1j * phase_diff)])
[docs]
def stokes_parameters(jones: np.ndarray) -> Tuple[float, float, float, float]:
"""
Compute Stokes parameters from a Jones vector.
The Stokes parameters provide a complete description of the
polarization state, including partially polarized light.
Parameters
----------
jones : ndarray
Jones vector [Ex, Ey] (complex, shape (2,) or (2, N))
Returns
-------
S0 : float or ndarray
Total intensity: ``|Ex|^2 + |Ey|^2``
S1 : float or ndarray
Linear horizontal-vertical: ``|Ex|^2 - |Ey|^2``
S2 : float or ndarray
Linear +45/-45: ``2*Re(Ex*conj(Ey))``
S3 : float or ndarray
Circular right-left: ``2*Im(Ex*conj(Ey))``
Examples
--------
Horizontal linear polarization (S1 = S0):
>>> import numpy as np
>>> import phased_array as pa
>>> j = pa.jones_vector(1.0, 0.0)
>>> S0, S1, S2, S3 = pa.stokes_parameters(j)
>>> np.isclose(S1, S0)
True
Circular polarization (S3 = ±S0):
>>> j = pa.jones_vector(1.0, 1.0, phase_diff=-np.pi/2) # RHCP
>>> S0, S1, S2, S3 = pa.stokes_parameters(j)
>>> np.isclose(S3, S0, atol=1e-10) # right circular: S3 = +S0
True
"""
jones = np.asarray(jones, dtype=complex)
Ex = jones[0]
Ey = jones[1]
S0 = np.abs(Ex)**2 + np.abs(Ey)**2
S1 = np.abs(Ex)**2 - np.abs(Ey)**2
S2 = 2 * np.real(Ex * np.conj(Ey))
S3 = 2 * np.imag(Ex * np.conj(Ey))
return S0, S1, S2, S3
[docs]
def axial_ratio(jones: np.ndarray) -> np.ndarray:
"""
Compute the axial ratio from a Jones vector.
The axial ratio is the ratio of the major to minor axes of the
polarization ellipse. AR = 1 for circular, AR -> infinity for linear.
Parameters
----------
jones : ndarray
Jones vector [Ex, Ey] (complex)
Returns
-------
ar : float or ndarray
Axial ratio (>= 1). Returns infinity for perfect linear polarization.
Examples
--------
Circular polarization has AR = 1:
>>> import numpy as np
>>> import phased_array as pa
>>> j_circ = pa.jones_vector(1.0, 1.0, phase_diff=np.pi/2)
>>> ar = pa.axial_ratio(j_circ)
>>> np.isclose(ar, 1.0, atol=1e-10)
True
Linear polarization has AR = infinity:
>>> j_lin = pa.jones_vector(1.0, 0.0)
>>> ar = pa.axial_ratio(j_lin)
>>> np.isinf(ar)
True
"""
jones = np.asarray(jones, dtype=complex)
Ex = jones[0]
Ey = jones[1]
# Compute parameters of polarization ellipse
a = np.abs(Ex)**2
b = np.abs(Ey)**2
c = 2 * np.real(Ex * np.conj(Ey))
d = 2 * np.imag(Ex * np.conj(Ey))
# Semi-major and semi-minor axes
sum_ab = a + b
diff = np.sqrt((a - b)**2 + c**2)
major = np.sqrt(0.5 * (sum_ab + diff))
minor = np.sqrt(np.maximum(0.5 * (sum_ab - diff), 0))
# Handle linear polarization (minor = 0)
with np.errstate(divide='ignore', invalid='ignore'):
ar = np.where(minor > 1e-15, major / minor, np.inf)
return ar
[docs]
def tilt_angle(jones: np.ndarray) -> np.ndarray:
"""
Compute the tilt angle of the polarization ellipse.
The tilt angle is the orientation of the major axis of the
polarization ellipse with respect to the x-axis (horizontal).
Parameters
----------
jones : ndarray
Jones vector [Ex, Ey] (complex)
Returns
-------
tau : float or ndarray
Tilt angle in radians (-pi/2 to pi/2)
Examples
--------
Horizontal polarization has tilt = 0:
>>> import numpy as np
>>> import phased_array as pa
>>> j = pa.jones_vector(1.0, 0.0)
>>> tau = pa.tilt_angle(j)
>>> np.isclose(tau, 0.0, atol=1e-10)
True
45-degree linear polarization:
>>> j = pa.jones_vector(1.0, 1.0, phase_diff=0.0)
>>> tau = pa.tilt_angle(j)
>>> np.isclose(tau, np.pi/4, atol=1e-10)
True
"""
jones = np.asarray(jones, dtype=complex)
Ex = jones[0]
Ey = jones[1]
# Tilt angle from Stokes parameters
S1 = np.abs(Ex)**2 - np.abs(Ey)**2
S2 = 2 * np.real(Ex * np.conj(Ey))
tau = 0.5 * np.arctan2(S2, S1)
return tau
[docs]
def cross_pol_discrimination(
jones_desired: np.ndarray,
jones_actual: np.ndarray
) -> np.ndarray:
"""
Compute cross-polarization discrimination (XPD).
XPD is the ratio of co-polarized to cross-polarized power,
measuring polarization purity.
Parameters
----------
jones_desired : ndarray
Desired (reference) Jones vector
jones_actual : ndarray
Actual measured Jones vector
Returns
-------
xpd_dB : float or ndarray
Cross-polarization discrimination in dB
Examples
--------
Perfect match has infinite XPD:
>>> import numpy as np
>>> import phased_array as pa
>>> j_ref = pa.jones_vector(1.0, 0.0)
>>> j_act = pa.jones_vector(1.0, 0.0)
>>> xpd = pa.cross_pol_discrimination(j_ref, j_act)
>>> xpd > 50 # Very high XPD
True
Orthogonal polarizations have XPD = -infinity:
>>> j_h = pa.jones_vector(1.0, 0.0)
>>> j_v = pa.jones_vector(0.0, 1.0)
>>> xpd = pa.cross_pol_discrimination(j_h, j_v)
>>> xpd < -50 # Very low (negative) XPD
True
"""
jones_desired = np.asarray(jones_desired, dtype=complex)
jones_actual = np.asarray(jones_actual, dtype=complex)
# Normalize reference vector
norm_desired = np.sqrt(np.abs(jones_desired[0])**2 + np.abs(jones_desired[1])**2)
if norm_desired < 1e-15:
return np.array(-np.inf)
jones_desired_norm = jones_desired / norm_desired
# Create orthogonal (cross-pol) reference
jones_cross = np.array([-np.conj(jones_desired_norm[1]), np.conj(jones_desired_norm[0])])
# Project actual onto co-pol and cross-pol
co_pol = np.abs(np.sum(jones_actual * np.conj(jones_desired_norm)))**2
cross_pol = np.abs(np.sum(jones_actual * np.conj(jones_cross)))**2
# Compute XPD in dB
with np.errstate(divide='ignore', invalid='ignore'):
xpd = 10 * np.log10(co_pol / cross_pol) if cross_pol > 1e-20 else 100.0
return xpd
[docs]
def polarization_loss_factor(
jones_antenna: np.ndarray,
jones_incident: np.ndarray
) -> np.ndarray:
"""
Compute polarization loss factor (PLF).
The PLF is the fraction of incident power that couples to the
antenna due to polarization mismatch.
Parameters
----------
jones_antenna : ndarray
Antenna polarization (Jones vector)
jones_incident : ndarray
Incident wave polarization (Jones vector)
Returns
-------
plf : float or ndarray
Polarization loss factor (0 to 1)
Examples
--------
Matched polarizations have PLF = 1:
>>> import numpy as np
>>> import phased_array as pa
>>> j_ant = pa.jones_vector(1.0, 0.0) # H-pol antenna
>>> j_inc = pa.jones_vector(1.0, 0.0) # H-pol wave
>>> plf = pa.polarization_loss_factor(j_ant, j_inc)
>>> np.isclose(plf, 1.0)
True
Orthogonal polarizations have PLF = 0:
>>> j_ant = pa.jones_vector(1.0, 0.0) # H-pol antenna
>>> j_inc = pa.jones_vector(0.0, 1.0) # V-pol wave
>>> plf = pa.polarization_loss_factor(j_ant, j_inc)
>>> np.isclose(plf, 0.0)
True
Circular antenna receiving linear has PLF = 0.5:
>>> j_circ = pa.jones_vector(1.0, 1.0, phase_diff=np.pi/2)
>>> j_lin = pa.jones_vector(1.0, 0.0)
>>> plf = pa.polarization_loss_factor(j_circ, j_lin)
>>> np.isclose(plf, 0.5)
True
"""
jones_antenna = np.asarray(jones_antenna, dtype=complex)
jones_incident = np.asarray(jones_incident, dtype=complex)
# Normalize both vectors
norm_ant = np.sqrt(np.abs(jones_antenna[0])**2 + np.abs(jones_antenna[1])**2)
norm_inc = np.sqrt(np.abs(jones_incident[0])**2 + np.abs(jones_incident[1])**2)
if norm_ant < 1e-15 or norm_inc < 1e-15:
return np.array(0.0)
jones_ant_norm = jones_antenna / norm_ant
jones_inc_norm = jones_incident / norm_inc
# PLF = |antenna . incident*|^2
plf = np.abs(np.sum(jones_ant_norm * np.conj(jones_inc_norm)))**2
return plf
[docs]
def ludwig3_decomposition(
theta: np.ndarray,
phi: np.ndarray,
E_theta: np.ndarray,
E_phi: np.ndarray
) -> Tuple[np.ndarray, np.ndarray]:
"""
Decompose field into Ludwig-3 co-polar and cross-polar components.
Ludwig-3 is the most common definition for co/cross-pol in antenna
measurements. It's based on aligning the reference polarization
with the principal planes.
Parameters
----------
theta : ndarray
Theta angles in radians
phi : ndarray
Phi angles in radians
E_theta : ndarray
Theta component of electric field (complex)
E_phi : ndarray
Phi component of electric field (complex)
Returns
-------
E_co : ndarray
Co-polar component (Ludwig-3)
E_cross : ndarray
Cross-polar component (Ludwig-3)
Notes
-----
Ludwig-3 definition (for reference polarization in phi=0 plane):
E_co = E_theta * cos(phi) - E_phi * sin(phi)
E_cross = E_theta * sin(phi) + E_phi * cos(phi)
Examples
--------
At phi = 0, E_theta is co-pol:
>>> import numpy as np
>>> import phased_array as pa
>>> theta = np.array([np.pi/4])
>>> phi = np.array([0.0])
>>> E_theta = np.array([1.0 + 0j])
>>> E_phi = np.array([0.0 + 0j])
>>> E_co, E_cross = pa.ludwig3_decomposition(theta, phi, E_theta, E_phi)
>>> np.isclose(np.abs(E_co[0]), 1.0)
True
>>> np.isclose(np.abs(E_cross[0]), 0.0)
True
"""
theta = np.asarray(theta)
phi = np.asarray(phi)
E_theta = np.asarray(E_theta, dtype=complex)
E_phi = np.asarray(E_phi, dtype=complex)
cos_phi = np.cos(phi)
sin_phi = np.sin(phi)
E_co = E_theta * cos_phi - E_phi * sin_phi
E_cross = E_theta * sin_phi + E_phi * cos_phi
return E_co, E_cross
[docs]
def co_pol_pattern(
theta: np.ndarray,
phi: np.ndarray,
E_theta: np.ndarray,
E_phi: np.ndarray,
reference_pol: str = 'ludwig3'
) -> np.ndarray:
"""
Extract co-polar component of the radiation pattern.
Parameters
----------
theta : ndarray
Theta angles in radians
phi : ndarray
Phi angles in radians
E_theta : ndarray
Theta component of electric field
E_phi : ndarray
Phi component of electric field
reference_pol : str
'ludwig3' - Ludwig-3 definition (default)
'theta' - E_theta is co-pol
'phi' - E_phi is co-pol
Returns
-------
E_co : ndarray
Co-polar component (complex)
Examples
--------
>>> import numpy as np
>>> import phased_array as pa
>>> theta = np.linspace(0, np.pi/2, 91)
>>> phi = np.zeros_like(theta)
>>> E_theta = np.cos(theta) # Simple pattern
>>> E_phi = np.zeros_like(theta)
>>> E_co = pa.co_pol_pattern(theta, phi, E_theta, E_phi)
>>> E_co.shape
(91,)
"""
if reference_pol == 'ludwig3':
E_co, _ = ludwig3_decomposition(theta, phi, E_theta, E_phi)
return E_co
elif reference_pol == 'theta':
return np.asarray(E_theta, dtype=complex)
elif reference_pol == 'phi':
return np.asarray(E_phi, dtype=complex)
else:
raise ValueError(f"Unknown reference polarization: {reference_pol}")
[docs]
def cross_pol_pattern(
theta: np.ndarray,
phi: np.ndarray,
E_theta: np.ndarray,
E_phi: np.ndarray,
reference_pol: str = 'ludwig3'
) -> np.ndarray:
"""
Extract cross-polar component of the radiation pattern.
Parameters
----------
theta : ndarray
Theta angles in radians
phi : ndarray
Phi angles in radians
E_theta : ndarray
Theta component of electric field
E_phi : ndarray
Phi component of electric field
reference_pol : str
'ludwig3' - Ludwig-3 definition (default)
'theta' - E_phi is cross-pol
'phi' - E_theta is cross-pol
Returns
-------
E_cross : ndarray
Cross-polar component (complex)
Examples
--------
>>> import numpy as np
>>> import phased_array as pa
>>> theta = np.linspace(0, np.pi/2, 91)
>>> phi = np.zeros_like(theta)
>>> E_theta = np.cos(theta)
>>> E_phi = 0.1 * np.sin(theta) # Small cross-pol
>>> E_cross = pa.cross_pol_pattern(theta, phi, E_theta, E_phi)
>>> E_cross.shape
(91,)
"""
if reference_pol == 'ludwig3':
_, E_cross = ludwig3_decomposition(theta, phi, E_theta, E_phi)
return E_cross
elif reference_pol == 'theta':
return np.asarray(E_phi, dtype=complex)
elif reference_pol == 'phi':
return np.asarray(E_theta, dtype=complex)
else:
raise ValueError(f"Unknown reference polarization: {reference_pol}")