Source code for hermespy.beamforming.conventional
# -*- coding: utf-8 -*-
"""
=======================
Conventional Beamformer
=======================
Also refererd to as Delay and Sum Beamformer.
"""
from typing import Optional
import numpy as np
from numba import jit
from scipy.constants import pi, speed_of_light
from hermespy.core import AntennaArray, Direction, DuplexOperator, Serializable
from .beamformer import TransmitBeamformer, 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 ConventionalBeamformer(Serializable, TransmitBeamformer, ReceiveBeamformer):
"""Conventional delay and sum beamforming.
The Bartlett\\ :footcite:`1950:bartlett` beamformer,
also known as conventional or delay and sum beamformer,
maximizes the power transmitted or received towards a single direction of interest
: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.
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
.. math::
\\hat{P}_{\\mathrm{Capon}}(\\theta, \\phi) = \\mathbf{a}^\\mathsf{H}(\\theta, \\phi) \\mathbf{X} \\mathbf{X}^\\mathsf{H} \\mathbf{a}(\\theta, \\phi)
is the Conventional beamformer's power estimate with
.. math::
\\mathbf{w}(\\theta, \\phi) = \\mathbf{a}(\\theta, \\phi)
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 = "ConventionalBeamformer"
"""YAML serialization tag."""
def __init__(self, operator: Optional[DuplexOperator] = None) -> None:
TransmitBeamformer.__init__(self, operator=operator)
ReceiveBeamformer.__init__(self, operator=operator)
@property
def num_receive_focus_points(self) -> int:
# The conventional beamformer focuses a single angle
return 1
@property
def num_receive_input_streams(self) -> int:
# The conventional beamformer will allways consider all antennas streams
return self.operator.device.antennas.num_receive_ports
@property
def num_receive_output_streams(self) -> int:
# The convetional beamformer will always return a single stream,
# combining all antenna signals into one
return 1
@property
def num_transmit_focus_points(self) -> int:
# The conventional beamformer focuses a single angle
return 1
@property
def num_transmit_output_streams(self) -> int:
# The conventional beamformer will allways consider all antennas streams
return self.operator.device.antennas.num_transmit_ports
@property
def num_transmit_input_streams(self) -> int:
# The convetional beamformer will always return a single stream,
# combining all antenna signals into one
return 1
# @lru_cache(maxsize=2)
def _codebook(
self, carrier_frequency: float, angles: np.ndarray, array: AntennaArray
) -> np.ndarray:
"""Compute the beamforming codebook for a given set of angles of interest.
Args:
carrier_frequency (float):
The assumed carrier central frequency of the samples.
angles (numpy.ndarray):
Spherical coordinate system angles of arrival in radians.
A two dimensional numpy array with the first dimension representing the number of angles,
and the second dimension of magnitude two containing the azimuth and zenith angle in radians, respectively.
array (AntennaArray):
The antenna array to compute the codebook for.
Returns:
The codebook represented by a two-dimensional numpy array,
with the first dimension being the number of angles and the second dimension the number of antennas.
"""
# Query topology of receiving antenna ports
topology = (
np.array([p.global_position for p in array.receive_ports], dtype=np.float_)
- array.global_position
)
# Build receive beamforming codebook of steering vectors for each angle of interest
book = np.empty((angles.shape[0], array.num_receive_ports), dtype=complex)
for n, (azimuth, zenith) in enumerate(angles):
direction = Direction.From_Spherical(azimuth, zenith)
weights = np.exp(
1j * 2 * pi * carrier_frequency / speed_of_light * (topology @ direction)
)
book[n, :] = weights
return book / array.num_receive_ports
[docs]
def _encode(
self,
samples: np.ndarray,
carrier_frequency: float,
focus_angles: np.ndarray,
array: AntennaArray,
) -> np.ndarray:
azimuth, zenith = focus_angles[0, :]
# Compute conventional beamformer weights
topology = (
np.array([p.global_position for p in array.transmit_ports], dtype=np.float_)
- array.global_position
)
direction = Direction.From_Spherical(azimuth, zenith)
weights = np.exp(-1j * 2 * pi * carrier_frequency / speed_of_light * (topology @ direction))
# Weight the streams accordingly
samples = weights[:, np.newaxis] @ samples
# That's it
return samples
@staticmethod
@jit(nopython=True)
def _beamform(
codebook: np.ndarray, samples: np.ndarray, conjugate: bool = False
) -> np.ndarray: # pragma: no cover
if conjugate:
return codebook.conj() @ samples
else:
return codebook @ samples
[docs]
def _decode(
self, samples: np.ndarray, carrier_frequency: float, angles: np.ndarray, array: AntennaArray
) -> np.ndarray:
codebook = self._codebook(carrier_frequency, angles[:, 0, :], array)
beamformed_samples = self._beamform(codebook, samples, True)
return beamformed_samples[:, np.newaxis, :]