Source code for hermespy.beamforming.capon
# -*- coding: utf-8 -*-
"""
================
Capon Beamformer
================
"""
import numpy as np
from hermespy.core import AntennaMode, AntennaArray, Serializable
from .beamformer import ReceiveBeamformer
__author__ = "Jan Adler"
__copyright__ = "Copyright 2024, Barkhausen Institut gGmbH"
__credits__ = ["Jan Adler"]
__license__ = "AGPLv3"
__version__ = "1.3.0"
__maintainer__ = "Jan Adler"
__email__ = "jan.adler@barkhauseninstitut.org"
__status__ = "Prototype"
[docs]
class CaponBeamformer(Serializable, ReceiveBeamformer):
"""Implementation of the Capon beamformer, also referred to as Minimum Variance Distortionless Response (MVDR).
The Capon\ :footcite:`1969:capon` beamformer estimates the power :math:`\\hat{P}` received from a direction :math:`(\\theta, \\phi)`, where :math:`\\theta` is the zenith and :math:`\\phi` is the azimuth angle of interest in spherical coordinates, respectively.
Let :math:`\\mathbf{X} \in \mathbb{C}^{N \\times T}` be the the matrix of :math:`T` time-discrete samples acquired by an antenna arrary featuring :math:`N` antennas and
.. math::
\\mathbf{R}^{-1} = \\left( \\mathbf{X}\\mathbf{X}^{\\mathsf{H}} + \\lambda \\mathbb{I} \\right)^{-1}
be the respective inverse sample correlation matrix loaded by a factor :math:`\\lambda \\in \\mathbb{R}_{+}`.
The antenna array's response towards a source within its far field emitting a signal of small relative bandwidth is :math:`\\mathbf{a}(\\theta, \\phi) \\in \\mathbb{C}^{N}`.
Then, the Capon beamformer's spatial power response is defined as
.. math::
\\hat{P}_{\\mathrm{Capon}}(\\theta, \\phi) = \\frac{1}{\\mathbf{a}^{\\mathsf{H}}(\\theta, \\phi) \mathbf{R}^{-1} \\mathbf{a}(\\theta, \\phi)}
with
.. math::
\\mathbf{w}(\\theta, \\phi) = \\frac{\\mathbf{R}^{-1} \\mathbf{a}(\\theta, \\phi)}{\\mathbf{a}^{\\mathsf{H}}(\\theta, \\phi) \mathbf{R}^{-1} \\mathbf{a}(\\theta, \\phi)} \\in \\mathbb{C}^{N}
being the beamforming weights to steer the sensor array's receive characteristics towards direction :math:`(\\theta, \\phi)`, so that
.. math::
\\mathcal{B}\\lbrace \\mathbf{X} \\rbrace = \\mathbf{w}^\\mathsf{H}(\\theta, \\phi) \\mathbf{X}
is the implemented beamforming equation.
"""
yaml_tag = "Capon"
def __init__(self, loading: float = 0.0, **kwargs) -> None:
"""
Args:
loading (float, optional):
Diagonal covariance loading coefficient :math:`\\lambda`.
Defaults to zero.
"""
self.loading = loading
ReceiveBeamformer.__init__(self, **kwargs)
@property
def num_receive_input_streams(self) -> int:
return self.operator.device.antennas.num_receive_ports
@property
def num_receive_output_streams(self) -> int:
return 1
@property
def num_receive_focus_points(self) -> int:
return 1
@property
def loading(self) -> float:
"""Magnitude of the diagonal sample covariance matrix loading.
Required for robust matrix inversion in the case of rank-deficient sample covariances.
Returns:
Diagonal loading coefficient :math:`\\lambda`.
Raises:
ValueError: For loading coefficients smaller than zero.
"""
return self.__loading
@loading.setter
def loading(self, value: float) -> None:
if value < 0.0:
raise ValueError("Diagonal loading coefficient must be greater or equal to zero")
self.__loading = value
[docs]
def _decode(
self, samples: np.ndarray, carrier_frequency: float, angles: np.ndarray, array: AntennaArray
) -> np.ndarray:
# Compute the inverse sample covariance matrix R
# In order to avoid algebra exceptions on decodings without noise, we will resort to the pseudo-inverse,
# which is able to invert rank-deficient matrices
sample_covariance = np.linalg.inv(
samples @ samples.T.conj() + self.loading * np.eye(samples.shape[0])
)
# Query the sensor array response vectors for the angles of interest and create a dictionary from it
dictionary = np.empty((self.num_receive_input_streams, angles.shape[0]), dtype=complex)
for d, focus in enumerate(angles):
array_response = array.spherical_phase_response(
carrier_frequency, focus[0, 0], focus[0, 1], AntennaMode.RX
)
dictionary[:, d] = (
sample_covariance
@ array_response
/ (array_response.T.conj() @ sample_covariance @ array_response)
)
beamformed_samples = dictionary.T.conj() @ samples
return beamformed_samples[:, np.newaxis, :]