Skip to content

Antenna

Antenna gain models implementing the AntennaModel protocol.

Parametric Antenna

ParametricAntenna

ParametricAntenna(gain_dbi=0.0, scan_loss_model='none')

Fixed-gain antenna model (e.g., parabolic dish or specified gain).

Returns the same gain in all directions — useful for quick link budgets where the antenna pattern is not the focus.

Parameters:

Name Type Description Default
gain_dbi float

Isotropic antenna gain in dBi (default 0.0).

0.0
scan_loss_model str

Scan-loss model name (default "none").

'none'

Examples:

>>> ant = ParametricAntenna(gain_dbi=36.0)
>>> float(ant.gain_dbi(np.array([30.0]), np.array([0.0]), 12e9)[0])
36.0
Source code in src/opensatcom/antenna/parametric.py
def __init__(self, gain_dbi: float = 0.0, scan_loss_model: str = "none") -> None:
    self._gain_dbi = gain_dbi
    self._scan_loss_model = scan_loss_model

gain_dbi

gain_dbi(theta_deg, phi_deg, f_hz)

Return fixed gain for all directions.

Parameters:

Name Type Description Default
theta_deg ndarray

Elevation angles in degrees.

required
phi_deg ndarray

Azimuth angles in degrees.

required
f_hz float

Carrier frequency in Hz (unused).

required

Returns:

Type Description
ndarray

Constant gain array in dBi, same shape as theta_deg.

Source code in src/opensatcom/antenna/parametric.py
def gain_dbi(
    self, theta_deg: np.ndarray, phi_deg: np.ndarray, f_hz: float
) -> np.ndarray:
    """Return fixed gain for all directions.

    Parameters
    ----------
    theta_deg : numpy.ndarray
        Elevation angles in degrees.
    phi_deg : numpy.ndarray
        Azimuth angles in degrees.
    f_hz : float
        Carrier frequency in Hz (unused).

    Returns
    -------
    numpy.ndarray
        Constant gain array in dBi, same shape as *theta_deg*.
    """
    return np.full_like(theta_deg, self._gain_dbi, dtype=float)

eirp_dbw

eirp_dbw(theta_deg, phi_deg, f_hz, tx_power_w)

Compute EIRP in a given direction.

Parameters:

Name Type Description Default
theta_deg float

Elevation angle in degrees.

required
phi_deg float

Azimuth angle in degrees.

required
f_hz float

Carrier frequency in Hz (unused).

required
tx_power_w float

Transmit power in watts.

required

Returns:

Type Description
float

EIRP in dBW (Ptx_dBW + G_dBi).

Source code in src/opensatcom/antenna/parametric.py
def eirp_dbw(
    self, theta_deg: float, phi_deg: float, f_hz: float, tx_power_w: float
) -> float:
    """Compute EIRP in a given direction.

    Parameters
    ----------
    theta_deg : float
        Elevation angle in degrees.
    phi_deg : float
        Azimuth angle in degrees.
    f_hz : float
        Carrier frequency in Hz (unused).
    tx_power_w : float
        Transmit power in watts.

    Returns
    -------
    float
        EIRP in dBW (``Ptx_dBW + G_dBi``).
    """
    return w_to_dbw(tx_power_w) + self._gain_dbi

Cosine Rolloff Antenna

CosineRolloffAntenna

CosineRolloffAntenna(peak_gain_dbi, theta_3db_deg, sidelobe_floor_dbi=-20.0, boresight_az_deg=0.0, boresight_el_deg=0.0)

Simple analytic antenna model with cosine-squared rolloff.

Gain pattern: gain(theta_off) = peak_gain_dbi - 12*(theta_off / theta_3db)^2 for theta_off < theta_3db * 2.6, otherwise sidelobe_floor_dbi.

This produces a realistic off-axis pattern suitable for multi-beam interference analysis without requiring the full PAM library.

Parameters:

Name Type Description Default
peak_gain_dbi float

Peak (boresight) antenna gain in dBi.

required
theta_3db_deg float

Half-power (3 dB) beamwidth in degrees.

required
sidelobe_floor_dbi float

Minimum gain floor representing the sidelobe level, in dBi. Default is -20.0.

-20.0
boresight_az_deg float

Azimuth angle of the boresight direction in degrees. Default is 0.0.

0.0
boresight_el_deg float

Elevation angle of the boresight direction in degrees. Default is 0.0.

0.0

Examples:

>>> ant = CosineRolloffAntenna(peak_gain_dbi=36.0, theta_3db_deg=1.5)
>>> ant.gain_toward_dbi(az_deg=0.0, el_deg=0.0, f_hz=12e9)
36.0
Source code in src/opensatcom/antenna/cosine.py
def __init__(
    self,
    peak_gain_dbi: float,
    theta_3db_deg: float,
    sidelobe_floor_dbi: float = -20.0,
    boresight_az_deg: float = 0.0,
    boresight_el_deg: float = 0.0,
) -> None:
    self._peak_gain_dbi = peak_gain_dbi
    self._theta_3db_deg = theta_3db_deg
    self._sidelobe_floor_dbi = sidelobe_floor_dbi
    self._boresight_az_deg = boresight_az_deg
    self._boresight_el_deg = boresight_el_deg

peak_gain_dbi property

peak_gain_dbi

Peak (boresight) antenna gain.

Returns:

Type Description
float

Peak gain in dBi.

theta_3db_deg property

theta_3db_deg

Half-power (3 dB) beamwidth.

Returns:

Type Description
float

Beamwidth in degrees.

sidelobe_floor_dbi property

sidelobe_floor_dbi

Minimum gain floor representing the sidelobe level.

Returns:

Type Description
float

Sidelobe floor in dBi.

boresight_az_deg property

boresight_az_deg

Azimuth angle of the boresight direction.

Returns:

Type Description
float

Boresight azimuth in degrees.

boresight_el_deg property

boresight_el_deg

Elevation angle of the boresight direction.

Returns:

Type Description
float

Boresight elevation in degrees.

gain_dbi

gain_dbi(theta_deg, phi_deg, f_hz)

Return gain accounting for off-axis rolloff.

Parameters:

Name Type Description Default
theta_deg ndarray

Azimuth angles in degrees.

required
phi_deg ndarray

Elevation angles in degrees.

required
f_hz float

Frequency in Hz. Unused in this analytic model but accepted for interface compatibility with AntennaModel.

required

Returns:

Type Description
ndarray

Gain values in dBi, clamped to the sidelobe floor.

Source code in src/opensatcom/antenna/cosine.py
def gain_dbi(
    self, theta_deg: np.ndarray, phi_deg: np.ndarray, f_hz: float
) -> np.ndarray:
    """Return gain accounting for off-axis rolloff.

    Parameters
    ----------
    theta_deg : numpy.ndarray
        Azimuth angles in degrees.
    phi_deg : numpy.ndarray
        Elevation angles in degrees.
    f_hz : float
        Frequency in Hz. Unused in this analytic model but accepted
        for interface compatibility with ``AntennaModel``.

    Returns
    -------
    numpy.ndarray
        Gain values in dBi, clamped to the sidelobe floor.
    """
    off_axis = self._off_axis_angle_deg(theta_deg, phi_deg)
    # Main lobe: parabolic rolloff  gain = peak - 12*(theta/theta_3db)^2
    gain = self._peak_gain_dbi - 12.0 * (off_axis / self._theta_3db_deg) ** 2
    # Clamp to sidelobe floor (applies both in main lobe tail and beyond)
    gain = np.maximum(gain, self._sidelobe_floor_dbi)
    return gain

gain_toward_dbi

gain_toward_dbi(az_deg, el_deg, f_hz)

Scalar convenience: gain in a specific direction.

Parameters:

Name Type Description Default
az_deg float

Azimuth angle in degrees toward which to evaluate the gain.

required
el_deg float

Elevation angle in degrees toward which to evaluate the gain.

required
f_hz float

Frequency in Hz. Unused in this analytic model but accepted for interface compatibility with AntennaModel.

required

Returns:

Type Description
float

Antenna gain in the specified direction, in dBi.

Source code in src/opensatcom/antenna/cosine.py
def gain_toward_dbi(self, az_deg: float, el_deg: float, f_hz: float) -> float:
    """Scalar convenience: gain in a specific direction.

    Parameters
    ----------
    az_deg : float
        Azimuth angle in degrees toward which to evaluate the gain.
    el_deg : float
        Elevation angle in degrees toward which to evaluate the gain.
    f_hz : float
        Frequency in Hz. Unused in this analytic model but accepted
        for interface compatibility with ``AntennaModel``.

    Returns
    -------
    float
        Antenna gain in the specified direction, in dBi.
    """
    return float(
        self.gain_dbi(np.array([az_deg]), np.array([el_deg]), f_hz)[0]
    )

eirp_dbw

eirp_dbw(theta_deg, phi_deg, f_hz, tx_power_w)

Compute EIRP in a given direction.

EIRP is calculated as Ptx(dBW) + G(dBi) where the gain is evaluated at the specified azimuth/elevation angles.

Parameters:

Name Type Description Default
theta_deg float

Azimuth angle in degrees toward which to evaluate the EIRP.

required
phi_deg float

Elevation angle in degrees toward which to evaluate the EIRP.

required
f_hz float

Frequency in Hz. Passed through to :meth:gain_toward_dbi.

required
tx_power_w float

Transmit power in Watts.

required

Returns:

Type Description
float

Effective isotropic radiated power in dBW.

Source code in src/opensatcom/antenna/cosine.py
def eirp_dbw(
    self, theta_deg: float, phi_deg: float, f_hz: float, tx_power_w: float
) -> float:
    """Compute EIRP in a given direction.

    EIRP is calculated as ``Ptx(dBW) + G(dBi)`` where the gain is
    evaluated at the specified azimuth/elevation angles.

    Parameters
    ----------
    theta_deg : float
        Azimuth angle in degrees toward which to evaluate the EIRP.
    phi_deg : float
        Elevation angle in degrees toward which to evaluate the EIRP.
    f_hz : float
        Frequency in Hz. Passed through to :meth:`gain_toward_dbi`.
    tx_power_w : float
        Transmit power in Watts.

    Returns
    -------
    float
        Effective isotropic radiated power in dBW.
    """
    g = self.gain_toward_dbi(theta_deg, phi_deg, f_hz)
    return w_to_dbw(tx_power_w) + g

PAM Array Antenna

PamArrayAntenna

PamArrayAntenna(nx=1, ny=1, dx_lambda=0.5, dy_lambda=0.5, taper=None, steering=None, impairments=None)

Planar phased-array antenna on an Nx-by-Ny rectangular lattice.

Two beam modes:

  • steering=None (default): a tracking beam. The array is assumed to steer its beam electronically onto whatever direction is being evaluated, so every direction returns the peak gain. This models a terminal that follows the satellite. Elements are isotropic, so the scan-dependent losses of a real terminal (element rolloff, projected aperture) are not modeled.
  • steering=(theta0_deg, phi0_deg): a fixed beam. Angles passed to :meth:gain_dbi are then directions in the array's own frame, theta measured from the array normal, and the returned gain follows the real array factor of the tapered, steered lattice: peak at the steering direction, real sidelobe structure away from it. Requires phased-array-modeling <https://pypi.org/project/phased-array-modeling/>_ (the pam extra).

Without the package installed a fixed beam degrades to peak gain at all angles, with a warning: link budgets stay usable, pattern shaping is off.

In all modes the peak gain is the standard aperture estimate D = 4 * pi * Nx * dx * Ny * dy (spacings in wavelengths), reduced by the taper efficiency when a taper is synthesized.

Parameters:

Name Type Description Default
nx int

Number of elements along the x-axis, by default 1.

1
ny int

Number of elements along the y-axis, by default 1.

1
dx_lambda float

Element spacing along x in wavelengths, by default 0.5.

0.5
dy_lambda float

Element spacing along y in wavelengths, by default 0.5.

0.5
taper tuple of (str, float), str, or None

Amplitude taper. A name ("uniform", "hamming", "hanning", "cosine") or a (name, sll_db) tuple for parameterised tapers (("taylor", -25), ("chebyshev", -30)). None means uniform illumination, by default None.

None
steering tuple of (float, float) or None

Fixed beam direction (theta_deg, phi_deg) in the array frame, theta measured from the array normal. None means the beam tracks the evaluated direction, by default None.

None
impairments Any or None

Reserved for element-level impairment models. Currently unused and ignored with a warning if set, by default None.

None
Source code in src/opensatcom/antenna/pam.py
def __init__(
    self,
    nx: int = 1,
    ny: int = 1,
    dx_lambda: float = 0.5,
    dy_lambda: float = 0.5,
    taper: tuple[str, float] | str | None = None,
    steering: tuple[float, float] | None = None,
    impairments: Any | None = None,
) -> None:
    self.nx = nx
    self.ny = ny
    self.dx_lambda = dx_lambda
    self.dy_lambda = dy_lambda
    self.taper = taper
    self.steering = steering
    self.impairments = impairments

    if impairments is not None:
        warnings.warn(
            "PamArrayAntenna does not model element impairments yet; "
            "the impairments argument is ignored.",
            stacklevel=2,
        )

    self._pa: Any | None = None
    try:
        import phased_array

        self._pa = phased_array
    except ImportError:
        warnings.warn(
            "phased-array-modeling not installed; PamArrayAntenna will return "
            "peak gain at all angles (no pattern shaping). Install the 'pam' "
            "extra for real array-factor patterns.",
            stacklevel=2,
        )

    taper_weights = self._taper_weights()
    # Taper efficiency: |sum(w)|^2 / (N * sum(|w|^2)); 1.0 for uniform.
    taper_eff = float(
        np.abs(np.sum(taper_weights)) ** 2
        / (taper_weights.size * np.sum(np.abs(taper_weights) ** 2))
    )

    # Peak gain: aperture estimate reduced by taper efficiency.
    aperture_lambda_sq = nx * dx_lambda * ny * dy_lambda
    self._peak_gain_lin = 4.0 * math.pi * aperture_lambda_sq * taper_eff
    self._peak_gain_dbi = lin_to_db10(self._peak_gain_lin)

    if self._pa is not None and steering is not None:
        # Geometry in units of wavelength: spacings are specified in
        # wavelengths, so with wavelength=1.0 and k=2*pi the array factor
        # is frequency-independent, which matches the parameterisation.
        geom = self._pa.create_rectangular_array(
            nx, ny, dx=dx_lambda, dy=dy_lambda, wavelength=1.0
        )
        self._k = 2.0 * math.pi
        self._x = geom.x
        self._y = geom.y
        self._weights = taper_weights.ravel().astype(complex) * self._pa.steering_vector(
            self._k, self._x, self._y, steering[0], steering[1]
        )
        # The array factor peaks at the steering direction, where every
        # element adds in phase: |AF_max| = sum of the taper amplitudes.
        self._af_max = float(np.sum(np.abs(taper_weights)))

gain_dbi

gain_dbi(theta_deg, phi_deg, f_hz)

Gain in dBi at the requested directions.

A tracking beam (steering=None) returns the peak gain for every direction. A fixed beam returns the normalized array factor of the tapered, steered lattice referenced to the peak gain; angles are then directions in the array frame, theta measured from the array normal.

Parameters:

Name Type Description Default
theta_deg ndarray

Angles in degrees (array-frame theta for a fixed beam). Output shape matches this.

required
phi_deg ndarray

Azimuth angles in degrees, broadcastable with theta_deg.

required
f_hz float

Operating frequency in hertz. Unused: spacings are given in wavelengths, so the pattern is frequency-independent.

required

Returns:

Type Description
ndarray

Gain values in dBi, same shape as theta_deg.

Source code in src/opensatcom/antenna/pam.py
def gain_dbi(self, theta_deg: np.ndarray, phi_deg: np.ndarray, f_hz: float) -> np.ndarray:
    """Gain in dBi at the requested directions.

    A tracking beam (``steering=None``) returns the peak gain for every
    direction.  A fixed beam returns the normalized array factor of the
    tapered, steered lattice referenced to the peak gain; angles are then
    directions in the array frame, theta measured from the array normal.

    Parameters
    ----------
    theta_deg : numpy.ndarray
        Angles in degrees (array-frame theta for a fixed beam).  Output
        shape matches this.
    phi_deg : numpy.ndarray
        Azimuth angles in degrees, broadcastable with ``theta_deg``.
    f_hz : float
        Operating frequency in hertz.  Unused: spacings are given in
        wavelengths, so the pattern is frequency-independent.

    Returns
    -------
    numpy.ndarray
        Gain values in dBi, same shape as ``theta_deg``.
    """
    theta_deg = np.asarray(theta_deg, dtype=float)
    if self._pa is None or self.steering is None:
        return np.full_like(theta_deg, self._peak_gain_dbi, dtype=float)

    theta = np.radians(theta_deg)
    phi = np.radians(np.broadcast_to(np.asarray(phi_deg, dtype=float), theta_deg.shape))
    af = self._pa.array_factor_vectorized(theta, phi, self._x, self._y, self._weights, self._k)
    normalized = (np.abs(af) / self._af_max) ** 2
    with np.errstate(divide="ignore"):
        return np.asarray(self._peak_gain_dbi + 10.0 * np.log10(normalized))

eirp_dbw

eirp_dbw(theta_deg, phi_deg, f_hz, tx_power_w)

EIRP = Ptx(dBW) + G(dBi) toward the requested direction.

Parameters:

Name Type Description Default
theta_deg float

Angle from boresight in degrees toward the target.

required
phi_deg float

Azimuth angle in degrees toward the target.

required
f_hz float

Operating frequency in hertz.

required
tx_power_w float

Transmitter output power in watts (linear).

required

Returns:

Type Description
float

EIRP in dBW.

Source code in src/opensatcom/antenna/pam.py
def eirp_dbw(self, theta_deg: float, phi_deg: float, f_hz: float, tx_power_w: float) -> float:
    """EIRP = Ptx(dBW) + G(dBi) toward the requested direction.

    Parameters
    ----------
    theta_deg : float
        Angle from boresight in degrees toward the target.
    phi_deg : float
        Azimuth angle in degrees toward the target.
    f_hz : float
        Operating frequency in hertz.
    tx_power_w : float
        Transmitter output power in watts (linear).

    Returns
    -------
    float
        EIRP in dBW.
    """
    gain = float(self.gain_dbi(np.asarray([theta_deg]), np.asarray([phi_deg]), f_hz)[0])
    return w_to_dbw(tx_power_w) + gain

Coupling-Aware Antenna

CouplingAwareAntenna

CouplingAwareAntenna(coupling_data, steering_az_deg=0.0, steering_el_deg=0.0)

Antenna model using EdgeFEM coupling data for direction-dependent gain.

Computes array gain using embedded element patterns corrected for mutual coupling. The gain at each direction accounts for: 1. Active element patterns (from EdgeFEM simulation) 2. Coupling correction via S-parameter matrix 3. Array factor with element positions and steering weights

Angle convention (matches the pattern grid and the link engine's call order): the first angle is theta, the polar angle from the array normal in degrees (the grid's first axis); the second is phi, the azimuthal angle around the normal (the grid's second axis). Direction cosines are u = sin(theta) cos(phi), v = sin(theta) sin(phi).

The coupling correction is the (I + S)^-1 form: coupling_matrix is a scattering matrix at the artifact's reference impedance, and the active element response is (I + S)^-1 applied to the isolated responses. A producer exporting anything other than S-parameters breaks this term.

Parameters:

Name Type Description Default
coupling_data CouplingData

Coupling data loaded from an EdgeFEM artifact.

required
steering_az_deg float

Steering phi (azimuthal angle around the array normal), degrees. The parameter name is historical; it maps to phi above.

0.0
steering_el_deg float

Steering theta (polar angle from the array normal), degrees. The parameter name is historical; it maps to theta above.

0.0
Source code in src/opensatcom/antenna/coupling.py
def __init__(
    self,
    coupling_data: CouplingData,
    steering_az_deg: float = 0.0,
    steering_el_deg: float = 0.0,
) -> None:
    self._data = coupling_data
    self._steering_az = steering_az_deg
    self._steering_el = steering_el_deg

    # Precompute coupling correction matrix: (I + S)^-1
    n = coupling_data.n_elements
    identity = np.eye(n, dtype=complex)
    self._coupling_correction = np.linalg.inv(identity + coupling_data.coupling_matrix)

    # Precompute steering weights
    self._weights = self._compute_steering_weights()

gain_dbi

gain_dbi(theta_deg, phi_deg, f_hz)

Return gain in dBi accounting for coupling effects.

Parameters:

Name Type Description Default
theta_deg ndarray

Polar angles from the array normal in degrees.

required
phi_deg ndarray

Azimuthal angles in degrees.

required
f_hz float

Carrier frequency in Hz (for reference; pattern data at nearest freq).

required

Returns:

Type Description
ndarray

Gain values in dBi, same shape as theta_deg.

Source code in src/opensatcom/antenna/coupling.py
def gain_dbi(self, theta_deg: np.ndarray, phi_deg: np.ndarray, f_hz: float) -> np.ndarray:
    """Return gain in dBi accounting for coupling effects.

    Parameters
    ----------
    theta_deg : numpy.ndarray
        Polar angles from the array normal in degrees.
    phi_deg : numpy.ndarray
        Azimuthal angles in degrees.
    f_hz : float
        Carrier frequency in Hz (for reference; pattern data at nearest freq).

    Returns
    -------
    numpy.ndarray
        Gain values in dBi, same shape as *theta_deg*.
    """
    gain_lin = self._evaluate_array_gain(theta_deg, phi_deg)
    # element_patterns are linear complex voltage patterns relative to
    # isotropic (|p|^2 is element gain), so |sum|^2 of the weighted,
    # coupling-corrected responses is already power gain relative to
    # isotropic and converts to dBi directly.
    safe_gain = np.maximum(gain_lin, 1e-20)
    return np.array([lin_to_db10(g) for g in safe_gain])

eirp_dbw

eirp_dbw(theta_deg, phi_deg, f_hz, tx_power_w)

Compute EIRP in a given direction.

Parameters:

Name Type Description Default
theta_deg float

Azimuth angle in degrees.

required
phi_deg float

Elevation angle in degrees.

required
f_hz float

Carrier frequency in Hz.

required
tx_power_w float

Transmit power in watts.

required

Returns:

Type Description
float

EIRP in dBW.

Source code in src/opensatcom/antenna/coupling.py
def eirp_dbw(self, theta_deg: float, phi_deg: float, f_hz: float, tx_power_w: float) -> float:
    """Compute EIRP in a given direction.

    Parameters
    ----------
    theta_deg : float
        Azimuth angle in degrees.
    phi_deg : float
        Elevation angle in degrees.
    f_hz : float
        Carrier frequency in Hz.
    tx_power_w : float
        Transmit power in watts.

    Returns
    -------
    float
        EIRP in dBW.
    """
    g = self.gain_dbi(np.array([theta_deg]), np.array([phi_deg]), f_hz)
    return w_to_dbw(tx_power_w) + float(g[0])

from_npz classmethod

from_npz(artifact_path, steering_az_deg=0.0, steering_el_deg=0.0)

Load coupling data from .npz and construct antenna.

Parameters:

Name Type Description Default
artifact_path str or Path

Path to the .npz file containing EdgeFEM coupling data.

required
steering_az_deg float

Beam steering azimuth in degrees (default 0.0).

0.0
steering_el_deg float

Beam steering elevation in degrees (default 0.0).

0.0

Returns:

Type Description
CouplingAwareAntenna

Constructed antenna with loaded coupling data.

Source code in src/opensatcom/antenna/coupling.py
@classmethod
def from_npz(
    cls,
    artifact_path: str | Path,
    steering_az_deg: float = 0.0,
    steering_el_deg: float = 0.0,
) -> CouplingAwareAntenna:
    """Load coupling data from .npz and construct antenna.

    Parameters
    ----------
    artifact_path : str or Path
        Path to the ``.npz`` file containing EdgeFEM coupling data.
    steering_az_deg : float
        Beam steering azimuth in degrees (default 0.0).
    steering_el_deg : float
        Beam steering elevation in degrees (default 0.0).

    Returns
    -------
    CouplingAwareAntenna
        Constructed antenna with loaded coupling data.
    """
    data = load_npz_artifact(artifact_path)
    return cls(data, steering_az_deg, steering_el_deg)

from_array_package classmethod

from_array_package(json_path, patterns_csv_path=None, steering_az_deg=0.0, steering_el_deg=0.0, freq_hz=None)

Load an EdgeFEM ArrayPackage JSON and construct the antenna.

Parameters:

Name Type Description Default
json_path str or Path

EdgeFEM_ArrayPackage JSON (S-matrices, positions).

required
patterns_csv_path str or Path

Companion embedded-patterns CSV; without it, element patterns fall back to the loader's default.

None
steering_az_deg float

Beam steering angles in degrees.

0.0
steering_el_deg float

Beam steering angles in degrees.

0.0
freq_hz float

Frequency to slice the S-matrices at (nearest match); defaults to the loader's choice.

None
Source code in src/opensatcom/antenna/coupling.py
@classmethod
def from_array_package(
    cls,
    json_path: str | Path,
    patterns_csv_path: str | Path | None = None,
    steering_az_deg: float = 0.0,
    steering_el_deg: float = 0.0,
    freq_hz: float | None = None,
) -> CouplingAwareAntenna:
    """Load an EdgeFEM ``ArrayPackage`` JSON and construct the antenna.

    Parameters
    ----------
    json_path : str or Path
        ``EdgeFEM_ArrayPackage`` JSON (S-matrices, positions).
    patterns_csv_path : str or Path, optional
        Companion embedded-patterns CSV; without it, element patterns
        fall back to the loader's default.
    steering_az_deg, steering_el_deg : float
        Beam steering angles in degrees.
    freq_hz : float, optional
        Frequency to slice the S-matrices at (nearest match); defaults
        to the loader's choice.
    """
    from opensatcom.antenna.edgefem_json_loader import load_array_package

    data = load_array_package(
        json_path,
        patterns_csv_path=patterns_csv_path,
        freq_hz=freq_hz,
    )
    return cls(data, steering_az_deg, steering_el_deg)

EdgeFEM Loader

edgefem_loader

EdgeFEM artifact loading — coupling matrices and element patterns.

CouplingData dataclass

CouplingData(coupling_matrix, element_patterns, theta_grid_deg, phi_grid_deg, freq_hz, array_positions_m, metadata=dict())

Parsed coupling and element pattern data from EdgeFEM artifacts.

Attributes:

Name Type Description
coupling_matrix complex scattering matrix, shape (N_elem, N_elem).

Consumers apply the (I + S)^-1 coupling correction; see CouplingAwareAntenna.

element_patterns linear complex voltage patterns relative to isotropic,

shape (N_elem, N_theta, N_phi); |p|^2 is element gain.

theta_grid_deg polar angle from the array normal, degrees, shape

(N_theta,), strictly increasing.

phi_grid_deg azimuthal angle around the normal, degrees, shape

(N_phi,), strictly increasing.

freq_hz frequency (scalar or array of frequencies)
array_positions_m element positions, shape (N_elem, 2) or (N_elem, 3)
metadata additional metadata from the artifact

load_npz_artifact

load_npz_artifact(path)

Load coupling data from the six-key .npz layout.

This is opensatcom's own historical layout, kept for backward compatibility with existing fixtures. EdgeFEM has never produced it: what EdgeFEM actually ships is the EdgeFEM_ArrayPackage JSON plus a two-cut pattern CSV, loaded by :func:opensatcom.antenna.edgefem_json_loader.load_array_package.

Expected arrays in the .npz: - coupling_matrix: (N_elem, N_elem) complex - element_patterns: (N_elem, N_theta, N_phi) complex - theta_grid_deg: (N_theta,) - phi_grid_deg: (N_phi,) - freq_hz: scalar or (N_freq,) - array_positions_m: (N_elem, 2) or (N_elem, 3)

Optional: - metadata_keys, metadata_values: parallel arrays for metadata

Source code in src/opensatcom/antenna/edgefem_loader.py
def load_npz_artifact(path: str | Path) -> CouplingData:
    """Load coupling data from the six-key ``.npz`` layout.

    This is opensatcom's own historical layout, kept for backward
    compatibility with existing fixtures. EdgeFEM has never produced it: what
    EdgeFEM actually ships is the ``EdgeFEM_ArrayPackage`` JSON plus a
    two-cut pattern CSV, loaded by
    :func:`opensatcom.antenna.edgefem_json_loader.load_array_package`.

    Expected arrays in the .npz:
    - coupling_matrix: (N_elem, N_elem) complex
    - element_patterns: (N_elem, N_theta, N_phi) complex
    - theta_grid_deg: (N_theta,)
    - phi_grid_deg: (N_phi,)
    - freq_hz: scalar or (N_freq,)
    - array_positions_m: (N_elem, 2) or (N_elem, 3)

    Optional:
    - metadata_keys, metadata_values: parallel arrays for metadata
    """
    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"EdgeFEM artifact not found: {path}")

    data = np.load(path, allow_pickle=True)

    required = [
        "coupling_matrix",
        "element_patterns",
        "theta_grid_deg",
        "phi_grid_deg",
        "freq_hz",
        "array_positions_m",
    ]
    for key in required:
        if key not in data:
            raise ValueError(f"Missing required array '{key}' in {path}")

    freq = data["freq_hz"]
    freq_val: float | np.ndarray = float(freq) if freq.ndim == 0 else freq

    metadata: dict[str, Any] = {}
    if "metadata_keys" in data and "metadata_values" in data:
        keys = data["metadata_keys"]
        vals = data["metadata_values"]
        for k, v in zip(keys, vals):
            metadata[str(k)] = v

    return CouplingData(
        coupling_matrix=data["coupling_matrix"],
        element_patterns=data["element_patterns"],
        theta_grid_deg=data["theta_grid_deg"],
        phi_grid_deg=data["phi_grid_deg"],
        freq_hz=freq_val,
        array_positions_m=data["array_positions_m"],
        metadata=metadata,
    )