"""
Utility functions for phased array antenna computations.
Includes coordinate transforms and helper functions.
"""
from typing import Tuple, Union
import numpy as np
ArrayLike = Union[np.ndarray, float]
[docs]
def deg2rad(degrees: ArrayLike) -> ArrayLike:
"""Convert degrees to radians."""
return np.deg2rad(degrees)
[docs]
def rad2deg(radians: ArrayLike) -> ArrayLike:
"""Convert radians to degrees."""
return np.rad2deg(radians)
[docs]
def azel_to_thetaphi(az: ArrayLike, el: ArrayLike) -> Tuple[ArrayLike, ArrayLike]:
"""
Convert azimuth/elevation to theta/phi (spherical coordinates).
Parameters
----------
az : array_like
Azimuth angle in radians (0 = boresight, positive = right)
el : array_like
Elevation angle in radians (0 = horizon, positive = up)
Returns
-------
theta : array_like
Polar angle from z-axis in radians (0 = zenith)
phi : array_like
Azimuthal angle in radians
"""
az = np.asarray(az)
el = np.asarray(el)
theta = np.arccos(np.cos(az) * np.cos(el))
phi = np.arctan2(np.sin(az), np.tan(el))
return theta, phi
[docs]
def thetaphi_to_azel(theta: ArrayLike, phi: ArrayLike) -> Tuple[ArrayLike, ArrayLike]:
"""
Convert theta/phi (spherical coordinates) to azimuth/elevation.
Parameters
----------
theta : array_like
Polar angle from z-axis in radians (0 = zenith)
phi : array_like
Azimuthal angle in radians
Returns
-------
az : array_like
Azimuth angle in radians
el : array_like
Elevation angle in radians
"""
theta = np.asarray(theta)
phi = np.asarray(phi)
el = np.arcsin(np.cos(theta))
az = np.arctan2(np.sin(theta) * np.sin(phi), np.sin(theta) * np.cos(phi))
return az, el
[docs]
def theta_phi_to_uv(theta: ArrayLike, phi: ArrayLike) -> Tuple[ArrayLike, ArrayLike]:
"""
Convert theta/phi angles to UV-space (direction cosines).
Parameters
----------
theta : array_like
Polar angle from z-axis in radians
phi : array_like
Azimuthal angle in radians
Returns
-------
u : array_like
Direction cosine u = sin(theta) * cos(phi)
v : array_like
Direction cosine v = sin(theta) * sin(phi)
"""
theta = np.asarray(theta)
phi = np.asarray(phi)
u = np.sin(theta) * np.cos(phi)
v = np.sin(theta) * np.sin(phi)
return u, v
[docs]
def uv_to_theta_phi(u: ArrayLike, v: ArrayLike) -> Tuple[ArrayLike, ArrayLike]:
"""
Convert UV-space (direction cosines) to theta/phi angles.
Parameters
----------
u : array_like
Direction cosine u = sin(theta) * cos(phi)
v : array_like
Direction cosine v = sin(theta) * sin(phi)
Returns
-------
theta : array_like
Polar angle from z-axis in radians
phi : array_like
Azimuthal angle in radians
Notes
-----
Points outside the visible region (u^2 + v^2 > 1) will have
theta values computed from the magnitude, which may be complex
or undefined. Use is_visible_region() to check validity.
"""
u = np.asarray(u)
v = np.asarray(v)
r = np.sqrt(u**2 + v**2)
theta = np.arcsin(np.clip(r, -1, 1))
phi = np.arctan2(v, u)
return theta, phi
[docs]
def is_visible_region(u: ArrayLike, v: ArrayLike) -> np.ndarray:
"""
Check if UV coordinates are within the visible region.
Parameters
----------
u, v : array_like
Direction cosines
Returns
-------
visible : ndarray
Boolean array, True where u^2 + v^2 <= 1
"""
u = np.asarray(u)
v = np.asarray(v)
return (u**2 + v**2) <= 1.0
[docs]
def wavelength_to_k(wavelength: float) -> float:
"""
Convert wavelength to wavenumber.
Parameters
----------
wavelength : float
Wavelength in meters
Returns
-------
k : float
Wavenumber (2*pi/wavelength) in rad/m
Examples
--------
For normalized wavelength (positions in wavelengths):
>>> import phased_array as pa
>>> k = pa.wavelength_to_k(1.0)
>>> round(k, 4)
6.2832
For physical wavelength (10 GHz = 3 cm):
>>> wavelength = 0.03 # meters
>>> k = pa.wavelength_to_k(wavelength)
>>> round(k, 2)
209.44
Or use frequency directly:
>>> k = pa.frequency_to_k(10e9) # 10 GHz
>>> round(k, 2)
209.44
"""
return 2.0 * np.pi / wavelength
[docs]
def frequency_to_wavelength(frequency: float, c: float = 3e8) -> float:
"""
Convert frequency to wavelength.
Parameters
----------
frequency : float
Frequency in Hz
c : float, optional
Speed of light in m/s (default: 3e8)
Returns
-------
wavelength : float
Wavelength in meters
"""
return c / frequency
[docs]
def frequency_to_k(frequency: float, c: float = 3e8) -> float:
"""
Convert frequency to wavenumber.
Parameters
----------
frequency : float
Frequency in Hz
c : float, optional
Speed of light in m/s (default: 3e8)
Returns
-------
k : float
Wavenumber in rad/m
"""
return 2.0 * np.pi * frequency / c
[docs]
def db_to_linear(db: ArrayLike) -> ArrayLike:
"""Convert decibels to linear scale (power)."""
return 10.0 ** (np.asarray(db) / 10.0)
[docs]
def linear_to_db(linear: ArrayLike, min_db: float = -100.0) -> ArrayLike:
"""
Convert linear scale (power) to decibels.
Parameters
----------
linear : array_like
Linear power values
min_db : float, optional
Minimum dB value to return (clips zeros/negatives)
Returns
-------
db : array_like
Power in decibels
"""
linear = np.asarray(linear)
with np.errstate(divide='ignore', invalid='ignore'):
db = 10.0 * np.log10(linear)
db = np.where(np.isfinite(db), db, min_db)
return np.maximum(db, min_db)
[docs]
def normalize_pattern(pattern: np.ndarray, mode: str = 'peak') -> np.ndarray:
"""
Normalize a radiation pattern.
Parameters
----------
pattern : ndarray
Complex or magnitude pattern values
mode : str
'peak' - normalize to peak value
'power' - normalize to total radiated power
Returns
-------
normalized : ndarray
Normalized pattern (same type as input)
"""
mag = np.abs(pattern)
if mode == 'peak':
max_val = np.max(mag)
if max_val > 0:
return pattern / max_val
return pattern
elif mode == 'power':
total_power = np.sum(mag**2)
if total_power > 0:
return pattern / np.sqrt(total_power)
return pattern
else:
raise ValueError(f"Unknown normalization mode: {mode}")
[docs]
def create_theta_phi_grid(
theta_range: Tuple[float, float] = (0, np.pi),
phi_range: Tuple[float, float] = (0, 2*np.pi),
n_theta: int = 181,
n_phi: int = 361
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""
Create a theta/phi grid for pattern computation.
Parameters
----------
theta_range : tuple
(theta_min, theta_max) in radians
phi_range : tuple
(phi_min, phi_max) in radians
n_theta : int
Number of theta points
n_phi : int
Number of phi points
Returns
-------
theta_1d : ndarray
1D array of theta values
phi_1d : ndarray
1D array of phi values
theta_grid : ndarray
2D meshgrid of theta values
phi_grid : ndarray
2D meshgrid of phi values
"""
theta_1d = np.linspace(theta_range[0], theta_range[1], n_theta)
phi_1d = np.linspace(phi_range[0], phi_range[1], n_phi)
theta_grid, phi_grid = np.meshgrid(theta_1d, phi_1d, indexing='ij')
return theta_1d, phi_1d, theta_grid, phi_grid
[docs]
def create_uv_grid(
u_range: Tuple[float, float] = (-1, 1),
v_range: Tuple[float, float] = (-1, 1),
n_u: int = 201,
n_v: int = 201
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""
Create a UV-space grid for pattern computation.
Parameters
----------
u_range : tuple
(u_min, u_max) direction cosine range
v_range : tuple
(v_min, v_max) direction cosine range
n_u : int
Number of u points
n_v : int
Number of v points
Returns
-------
u_1d : ndarray
1D array of u values
v_1d : ndarray
1D array of v values
u_grid : ndarray
2D meshgrid of u values
v_grid : ndarray
2D meshgrid of v values
"""
u_1d = np.linspace(u_range[0], u_range[1], n_u)
v_1d = np.linspace(v_range[0], v_range[1], n_v)
u_grid, v_grid = np.meshgrid(u_1d, v_1d, indexing='ij')
return u_1d, v_1d, u_grid, v_grid