from abc import ABC, abstractmethod
import numpy as np
[docs]class ArrayElement(ABC):
"""Base class for array elements."""
@property
@abstractmethod
def output_size(self):
"""Retrieves the output size of this array element.
For scalar sensors, the output size is one. For vector sensors, the
output size is greater than one.
"""
raise NotImplementedError()
@property
def is_scalar(self):
"""Retrieves whether this array element has a scalar output."""
return self.output_size == 1
@property
@abstractmethod
def is_isotropic(self):
"""Retrieves whether this array element is isotropic."""
raise NotImplementedError()
@property
@abstractmethod
def is_polarized(self):
"""Retrieves whether this array element measures polarized waves."""
raise NotImplementedError()
[docs] def calc_spatial_response(self, r, az, el, polarization=None):
"""Calculates the spatial response of for given sources configurations.
Args:
r (float or ~numpy.ndarray): A single range value or an array of
range values. Must have the same shape as ``az`` and ``el``.
az (float or ~numpy.ndarray): A single azimuth angle or an array of
azimuth angles. Must have the same shape as ``az`` and ``el``.
el (float or ~numpy.ndarray): A single elevation angle or an array
of elevation angles. Must have the same shape as ``az`` and
``el``.
polarization (~numpy.ndarray or None): Polarization information.
Suppose ``r``, ``az``, ``el`` share the same shape
``(d1, d2, ..., dn)``. Then ``polarization`` should have a shape
of ``(d1, d2, ..., dn, l)``, where ``l`` is the number of
polarization parameters for each source. Default value is
``None``.
Returns:
~numpy.ndarray: A spatial response tensor. For a scalar element,
the shape should be the same as that of ``r``, ``az``, or ``el``.
For a vector element (``output_size > 1``), the shape is given by
``(l, d1, d2, ..., dn)``, where ``l`` is equal to ``output_size``
and ``(d1, d2, ..., dn)`` is the shape of ``r``, ``az``, or ``el``.
"""
# Validate inputs.
input_shape = np.shape(r)
if np.shape(az) != input_shape or np.shape(el) != input_shape:
raise ValueError('r, az, and el must share the same shape.')
if polarization is not None:
if not self.is_polarized:
raise ValueError(
'{0} does not support polarized sources.'
.format(self.__class__.__name__)
)
expected_p_shape = input_shape + (polarization.shape[-1],)
if expected_p_shape != polarization.shape:
raise ValueError(
'The shape of the polarization data does not match that of '
'r, az, or el. Expecting {0}. Got {1}.'
.format(expected_p_shape, polarization.shape)
)
# Call the actual implementation.
return self._calc_spatial_response(r, az, el, polarization)
@abstractmethod
def _calc_spatial_response(self, r, az, el, polarization):
"""Actual implementation of spatial response calculations.
The inputs are guaranteed to have valid shapes.
"""
raise NotImplementedError()
[docs]class IsotropicScalarSensor(ArrayElement):
"""Creates an isotropic scalar array element."""
@property
def output_size(self):
return 1
@property
def is_isotropic(self):
return True
@property
def is_polarized(self):
return False
def _calc_spatial_response(self, r, az, el, polarization):
if np.isscalar(r):
return 1.
else:
return np.ones_like(r)
#: An isotropic scalar sensor.
ISOTROPIC_SCALAR_SENSOR = IsotropicScalarSensor()
[docs]class CustomNonisotropicSensor(ArrayElement):
"""Creates a customize non-isotropic sensor.
Args:
f_sr (~collection.abc.Callable): Custom spatial response function.
It accepts four inputs: ``r``, ``az``, ``el``, and ``polarization``,
and outputs the spatial response. See
:meth:`~doatools.model.array_elements.ArrayElement.calc_spatial_response`
for more details.
output_size (int): Output size of the sensor. Must be consistent with
the output of ``f_sr``. Default value is ``1``.
polarized (bool): Specifies whether the sensor measures polarized waves.
Must be consistent with the implemention in ``f_sr``. Default value
is ``False``.
"""
def __init__(self, f_sr, output_size=1, polarized=False):
self._f_sr = f_sr
self._output_size = output_size
self._is_polarized = polarized
@property
def output_size(self):
return self._output_size
@property
def is_isotropic(self):
return False
@property
def is_polarized(self):
return self._is_polarized
def _calc_spatial_response(self, r, az, el, polarization):
return self._f_sr(r, az, el, polarization)