Source code for doatools.model.coarray

import numpy as np
from .arrays import GridBasedArrayDesign
from ..utils.math import unique_rows

[docs]def compute_location_differences(locations): r"""Computes all locations differences, including duplicates. Suppose ``locations`` is :math:`m \times d`, then the result will be an :math:`m^2 \times d` matrix such that ``locations[i] - locations[j]`` is stored in the ``(i + j * m)``-th row of the resulting matrix. For instance, if ``locations`` is ``[[0, 1], [1, 3]]``, then the output will be ``[[0, 0], [1, 2], [-1, -2], [0, 0]]``. Args: locations (~numpy.ndarray): An m x d array of sensor locations. """ m, d = locations.shape # Use broadcasting to compute pairwise differences. D = locations.reshape((1, m, d)) - locations.reshape((m, 1, d)) return D.reshape((-1, d))
[docs]def compute_unique_location_differences(locations, atol=0.0, rtol=1e-8): """Computes all unique locations differences. Unlike :meth:`compute_location_differences`, duplicates within the specified tolerance are removed. Args: locations: An m x d array of sensor locations. """ return unique_rows(compute_location_differences(locations), atol, rtol)
[docs]class WeightFunction1D: """Creates a 1D weight function. Args: array (~doatools.model.arrays.ArrayDesign): Array design. References: [1] P. Pal and P. P. Vaidyanathan, "Nested arrays: A novel approach to array processing with enhanced degrees of freedom," IEEE Transactions on Signal Processing, vol. 58, no. 8, pp. 4167-4181, Aug. 2010. """ def __init__(self, array): if array.ndim != 1 or not isinstance(array, GridBasedArrayDesign): raise ValueError('Expecting a 1D grid-based array.') self._m = array.size self._mv = None self._build_map(array)
[docs] def __call__(self, diff): """Evaluates the weight function at the given difference.""" return self.weight_of(diff)
def __len__(self): """Retrieves the number of unique differences.""" return len(self._index_map)
[docs] def differences(self): """Retrieves a 1D array of unique differences in ascending order. The ordering of elements returned by :meth:`differences` and the ordering of elements returned by :meth:`weights` are the same. """ return self._differences.copy()
[docs] def weights(self): """Retrieves a 1D array of weights. The ordering of elements returned by :meth:`differences` and the ordering of elements returned by :meth:`weights` are the same. """ return np.array([len(self._index_map[x]) for x in self._differences])
[docs] def weight_of(self, diff): """Evaluates the weight function at the given difference.""" if diff in self._index_map: return len(self._index_map[diff]) else: return 0
[docs] def indices_of(self, diff): """Retrieves the list of indices of elements in the vectorized difference matrix that correspond to the given difference. If the given difference does not exist, an empty list will be returned. Args: diff (int): Difference. """ if diff in self._index_map: return self._index_map[diff][:] else: return []
[docs] def get_central_ula_size(self, exclude_negative_part=False): r"""Gets the size of the central ULA in the difference coarray. Args: exclude_negative_part (bool): Set to ``True`` to exclude the virtual array elements corresponding to negative differences. The central ULA part is symmetric with respect to the origin and can be represented with .. math:: \lbrack -M_\mathrm{v}+1, \ldots, -1, 0, 1, \ldots, M_\mathrm{v} \rbrack d_0 After excluding the negative part, the remaining array elements are given by .. math:: \lbrack 0, 1, \ldots, M_\mathrm{v} \rbrack d_0 Default value is ``False``. """ if self._mv is None: mv = 0 while mv in self._index_map: mv += 1 self._mv = mv return self._mv if exclude_negative_part else self._mv * 2 - 1
[docs] def get_coarray_selection_matrix(self, exclude_negative_part=False): r"""Gets the coarray selection matrix. Let the central ULA size be :math:`2M_{\mathrm{v}} - 1` and the original array size be :math:`M`. :math:`\mathbf{F}` is defined as an :math:`(2M_\mathrm{v} - 1) \times M^2` matrix that transforms the vectorized sample covariance matrix, :math:`\mathrm{vec}(\mathbf{R})`, to the virtual observation vector of the central ULA, :math:`\mathbf{z}`, via redundancy averaging: .. math:: \mathbf{z} = \mathbf{F} \mathrm{vec}(\mathbf{R}). Args: exclude_negative_part: If set to ``True``, only the nonnegative part of the central ULA (i.e., :math:`\lbrack 0, 1, \ldots, M_\mathrm{v} - 1\rbrack`) will be considered, and the resulting :math:`\mathbf{F}` will be :math:`M_\mathrm{v} \times M^2`. Default value is ``False``. Returns: The coarray selection matrix. References: [1] M. Wang and A. Nehorai, "Coarrays, MUSIC, and the Cramér-Rao Bound," IEEE Transactions on Signal Processing, vol. 65, no. 4, pp. 933-946, Feb. 2017. """ m_v = self.get_central_ula_size(exclude_negative_part=True) if exclude_negative_part: m_ula = m_v diff_range = range(0, m_v) else: m_ula = 2 * m_v - 1 diff_range = range(-m_v + 1, m_v) F = np.zeros((m_ula, self._m**2)) for i, diff in enumerate(diff_range): F[i, self.indices_of(diff)] = 1.0 / self.weight_of(diff) return F
def _build_map(self, array): # Maps difference -> indices in the vectorized difference matrix index_map = {} diffs = compute_location_differences(array.element_indices).flatten() for i, diff in enumerate(diffs): if diff in index_map: index_map[diff].append(i) else: index_map[diff] = [i] # Collect all unique differences and sort them differences = np.fromiter(index_map.keys(), dtype=np.int_, count=len(index_map)) differences.sort() self._index_map = index_map self._differences = differences