Skip to content

Antenna Models API

Antenna array adapter and metrics extraction.

Overview

from phased_array_systems.models.antenna import (
    PhasedArrayAdapter,
    compute_beamwidth,
    compute_scan_loss,
    compute_sidelobe_level,
)

The antenna module provides an adapter to the phased-array-modeling package for detailed antenna pattern computations.

Classes

PhasedArrayAdapter

PhasedArrayAdapter(use_analytical_fallback: bool = True)

Adapter for phased-array-modeling library.

Provides a consistent interface for computing antenna pattern metrics using the phased-array-modeling library, with fallback to analytical approximations when the library is not available.

ATTRIBUTE DESCRIPTION
name

Model block name for identification

TYPE: str

use_analytical_fallback

If True, use analytical approximations when phased-array-modeling is not available

Initialize the adapter.

PARAMETER DESCRIPTION
use_analytical_fallback

Use analytical methods if PAM unavailable

TYPE: bool DEFAULT: True

Source code in src/phased_array_systems/models/antenna/adapter.py
def __init__(self, use_analytical_fallback: bool = True):
    """Initialize the adapter.

    Args:
        use_analytical_fallback: Use analytical methods if PAM unavailable
    """
    self.use_analytical_fallback = use_analytical_fallback

    if not HAS_PAM and not use_analytical_fallback:
        raise ImportError(
            "phased-array-modeling not installed. Install with: "
            "pip install phased-array-modeling"
        )

evaluate

evaluate(arch: Architecture, scenario: Scenario, context: dict[str, Any]) -> MetricsDict

Evaluate antenna performance metrics.

PARAMETER DESCRIPTION
arch

Architecture configuration

TYPE: Architecture

scenario

Scenario with frequency and scan angle info

TYPE: Scenario

context

Additional context (unused)

TYPE: dict[str, Any]

RETURNS DESCRIPTION
MetricsDict

Dictionary with antenna metrics: - g_peak_db: Peak array gain (dB) - beamwidth_az_deg: Azimuth beamwidth (degrees) - beamwidth_el_deg: Elevation beamwidth (degrees) - sll_db: Peak sidelobe level (dB, negative) - scan_loss_db: Scan loss at operating angle (dB) - directivity_db: Array directivity (dB) - n_elements: Number of array elements

Source code in src/phased_array_systems/models/antenna/adapter.py
def evaluate(
    self, arch: Architecture, scenario: Scenario, context: dict[str, Any]
) -> MetricsDict:
    """Evaluate antenna performance metrics.

    Args:
        arch: Architecture configuration
        scenario: Scenario with frequency and scan angle info
        context: Additional context (unused)

    Returns:
        Dictionary with antenna metrics:
            - g_peak_db: Peak array gain (dB)
            - beamwidth_az_deg: Azimuth beamwidth (degrees)
            - beamwidth_el_deg: Elevation beamwidth (degrees)
            - sll_db: Peak sidelobe level (dB, negative)
            - scan_loss_db: Scan loss at operating angle (dB)
            - directivity_db: Array directivity (dB)
            - n_elements: Number of array elements
    """
    # Extract scan angle from scenario if available
    scan_angle_deg = getattr(scenario, "scan_angle_deg", 0.0)

    if HAS_PAM:
        return self._evaluate_with_pam(arch, scenario, scan_angle_deg)
    else:
        return self._evaluate_analytical(arch, scenario, scan_angle_deg)

Functions

compute_beamwidth

compute_beamwidth(pattern_db: NDArray[floating], angles_deg: NDArray[floating], level_db: float = -3.0) -> float

Compute beamwidth at specified level below peak.

PARAMETER DESCRIPTION
pattern_db

Pattern magnitude in dB

TYPE: NDArray[floating]

angles_deg

Corresponding angles in degrees

TYPE: NDArray[floating]

level_db

Level below peak to measure (default -3 dB)

TYPE: float DEFAULT: -3.0

RETURNS DESCRIPTION
float

Beamwidth in degrees, or NaN if not found

Source code in src/phased_array_systems/models/antenna/metrics.py
def compute_beamwidth(
    pattern_db: NDArray[np.floating],
    angles_deg: NDArray[np.floating],
    level_db: float = -3.0,
) -> float:
    """Compute beamwidth at specified level below peak.

    Args:
        pattern_db: Pattern magnitude in dB
        angles_deg: Corresponding angles in degrees
        level_db: Level below peak to measure (default -3 dB)

    Returns:
        Beamwidth in degrees, or NaN if not found
    """
    peak_db = np.max(pattern_db)
    threshold = peak_db + level_db  # level_db is negative

    # Find peak index
    peak_idx = np.argmax(pattern_db)

    # Search left from peak
    left_idx = peak_idx
    for i in range(peak_idx, -1, -1):
        if pattern_db[i] < threshold:
            left_idx = i
            break

    # Search right from peak
    right_idx = peak_idx
    for i in range(peak_idx, len(pattern_db)):
        if pattern_db[i] < threshold:
            right_idx = i
            break

    if left_idx == peak_idx or right_idx == peak_idx:
        return float("nan")

    # Linear interpolation for more accurate crossing points
    left_angle = np.interp(threshold, [pattern_db[left_idx], pattern_db[left_idx + 1]],
                           [angles_deg[left_idx], angles_deg[left_idx + 1]])
    right_angle = np.interp(threshold, [pattern_db[right_idx], pattern_db[right_idx - 1]],
                            [angles_deg[right_idx], angles_deg[right_idx - 1]])

    return abs(right_angle - left_angle)

compute_scan_loss

compute_scan_loss(scan_angle_deg: float, model: str = 'cosine') -> float

Compute scan loss for a phased array at given scan angle.

PARAMETER DESCRIPTION
scan_angle_deg

Scan angle from boresight (degrees)

TYPE: float

model

Scan loss model ("cosine" or "cosine_squared")

TYPE: str DEFAULT: 'cosine'

RETURNS DESCRIPTION
float

Scan loss in dB (positive value representing loss)

Source code in src/phased_array_systems/models/antenna/metrics.py
def compute_scan_loss(scan_angle_deg: float, model: str = "cosine") -> float:
    """Compute scan loss for a phased array at given scan angle.

    Args:
        scan_angle_deg: Scan angle from boresight (degrees)
        model: Scan loss model ("cosine" or "cosine_squared")

    Returns:
        Scan loss in dB (positive value representing loss)
    """
    if scan_angle_deg >= 90:
        return float("inf")

    scan_rad = math.radians(scan_angle_deg)

    if model == "cosine":
        # Standard cos(theta) scan loss
        loss_linear = math.cos(scan_rad)
    elif model == "cosine_squared":
        # More aggressive cos^2(theta) model
        loss_linear = math.cos(scan_rad) ** 2
    else:
        raise ValueError(f"Unknown scan loss model: {model}")

    if loss_linear <= 0:
        return float("inf")

    loss_db = -10 * math.log10(loss_linear)
    return abs(loss_db) if abs(loss_db) < 1e-10 else loss_db  # Avoid -0.0 display

compute_sidelobe_level

compute_sidelobe_level(pattern_db: NDArray[floating], angles_deg: NDArray[floating], main_lobe_width_deg: float | None = None) -> float

Compute peak sidelobe level relative to main beam.

PARAMETER DESCRIPTION
pattern_db

Pattern magnitude in dB

TYPE: NDArray[floating]

angles_deg

Corresponding angles in degrees

TYPE: NDArray[floating]

main_lobe_width_deg

Width of main lobe to exclude (auto-detected if None)

TYPE: float | None DEFAULT: None

RETURNS DESCRIPTION
float

Peak sidelobe level in dB (negative value)

Source code in src/phased_array_systems/models/antenna/metrics.py
def compute_sidelobe_level(
    pattern_db: NDArray[np.floating],
    angles_deg: NDArray[np.floating],
    main_lobe_width_deg: float | None = None,
) -> float:
    """Compute peak sidelobe level relative to main beam.

    Args:
        pattern_db: Pattern magnitude in dB
        angles_deg: Corresponding angles in degrees
        main_lobe_width_deg: Width of main lobe to exclude (auto-detected if None)

    Returns:
        Peak sidelobe level in dB (negative value)
    """
    peak_db = np.max(pattern_db)
    peak_idx = np.argmax(pattern_db)
    peak_angle = angles_deg[peak_idx]

    # Auto-detect main lobe width if not provided
    if main_lobe_width_deg is None:
        bw = compute_beamwidth(pattern_db, angles_deg, -3.0)
        if np.isnan(bw):
            bw = 10.0  # Default fallback
        main_lobe_width_deg = bw * 2  # Use 2x beamwidth as exclusion zone

    # Mask out main lobe region
    half_width = main_lobe_width_deg / 2
    mask = np.abs(angles_deg - peak_angle) > half_width

    if not np.any(mask):
        return float("-inf")

    sidelobe_pattern = pattern_db[mask]
    peak_sidelobe_db = np.max(sidelobe_pattern)

    return peak_sidelobe_db - peak_db

Output Metrics

Metric Units Description
g_peak_db dB Peak antenna gain
beamwidth_az_deg degrees 3 dB beamwidth in azimuth
beamwidth_el_deg degrees 3 dB beamwidth in elevation
sll_db dB Peak sidelobe level
scan_loss_db dB Gain reduction due to scan
directivity_db dB Antenna directivity
n_elements - Total element count

Usage Examples

Using the Adapter

from phased_array_systems.models.antenna import PhasedArrayAdapter
from phased_array_systems.architecture import Architecture, ArrayConfig, RFChainConfig

arch = Architecture(
    array=ArrayConfig(nx=8, ny=8, dx_lambda=0.5, dy_lambda=0.5),
    rf=RFChainConfig(tx_power_w_per_elem=1.0),
)

adapter = PhasedArrayAdapter()
metrics = adapter.evaluate(arch, scenario, context={})

print(f"Peak Gain: {metrics['g_peak_db']:.1f} dB")
print(f"Beamwidth: {metrics['beamwidth_az_deg']:.1f}° x {metrics['beamwidth_el_deg']:.1f}°")

With Scan Angle

from phased_array_systems.scenarios import CommsLinkScenario

scenario = CommsLinkScenario(
    freq_hz=10e9,
    bandwidth_hz=10e6,
    range_m=100e3,
    required_snr_db=10.0,
    scan_angle_deg=30.0,  # 30° off boresight
)

metrics = adapter.evaluate(arch, scenario, context={})
print(f"Scan Loss: {metrics['scan_loss_db']:.1f} dB")

Gain Approximation

When the full antenna adapter is not used, gain is approximated:

\[ G \approx 4\pi \cdot n_x d_x \cdot n_y d_y \]

Where \(d_x, d_y\) are element spacings in wavelengths.

This approximation is used in the link budget model when antenna metrics aren't pre-computed.

See Also