Source code for autoarray.structures.visibilities

from abc import ABC

from astropy.io import fits

import logging
import numpy as np
from pathlib import Path
from typing import List, Tuple, Union

from autoconf import cached_property

from autoarray.structures.abstract_structure import Structure
from autoarray.structures.grids.irregular_2d import Grid2DIrregular

from autoarray.structures.arrays import array_2d_util

logging.basicConfig()
logger = logging.getLogger(__name__)


class AbstractVisibilities(Structure, ABC):
    # noinspection PyUnusedLocal
    def __init__(self, visibilities: Union[np.ndarray, List[complex]]):
        """
        A collection of (real, imag) visibilities which are used to represent the data in an `Interferometer` dataset.

        The (real, imag) visibilities are stored as a 1D complex NumPy array of shape [total_visibilities]. These can
        be mapped to a 2D real NumPy array of shape [total_visibilities, 2] and a `Grid2DIrregular` data structure
        which is used for plotting the visibilities in 2D in the complex plane.

        Calculations should use the NumPy array structure wherever possible for efficient calculations.

        The vectors input to this function can have any of the following forms (they will be converted to the 1D
        complex NumPy array structure and can be converted back using the object's properties):

        [1.0+1.0j, 2.0+2.0j]
        [[1.0, 1.0], [2.0, 2.0]]

        Parameters
        ----------
        visibilities
            The (real, imag) visibilities values.
        """

        if type(visibilities) is list:
            visibilities = np.asarray(visibilities)

        if "float" in str(visibilities.dtype):
            if visibilities.shape[1] == 2:
                visibilities = (
                    np.apply_along_axis(lambda args: [complex(*args)], 1, visibilities)
                    .astype("complex128")
                    .ravel()
                )

        self.ordered_1d = np.concatenate(
            (np.real(visibilities), np.imag(visibilities)), axis=0
        )

        super().__init__(array=visibilities)

    def __array_finalize__(self, obj):
        if hasattr(obj, "ordered_1d"):
            self.ordered_1d = obj.ordered_1d

    @property
    def slim(self) -> "AbstractVisibilities":
        return self

    @property
    def native(self) -> Structure:
        return self

    @property
    def in_array(self) -> np.ndarray:
        """
        Returns the 1D complex NumPy array of values with shape [total_visibilities] as a NumPy float array of
        shape [total_visibilities, 2].
        """
        return np.stack((np.real(self), np.imag(self)), axis=-1)

    @property
    def in_grid(self) -> Grid2DIrregular:
        """
        Returns the 1D complex NumPy array of values as an irregular grid.
        """
        return Grid2DIrregular(values=self.in_array)

    @property
    def shape_slim(self) -> int:
        return self.shape[0]

    @property
    def mask(self):
        return np.full(fill_value=False, shape=self.shape)

    @cached_property
    def amplitudes(self) -> np.ndarray:
        return np.sqrt(np.square(self.real) + np.square(self.imag))

    @cached_property
    def phases(self) -> np.ndarray:
        return np.arctan2(self.imag, self.real)

    @property
    def hdu_for_output(self) -> fits.PrimaryHDU:
        """
        The visibilities as an HDU object, which can be output to a .fits file.

        This method is used in other projects (E.g. PyAutoGalaxy, PyAutoLens) to conveniently output the array to .fits
        files.

        Returns
        -------
        The HDU containing the data which can then be written to .fits.
        """
        return array_2d_util.hdu_for_output_from(
            array_2d=self.in_array,
        )

    def output_to_fits(self, file_path: Union[Path, str], overwrite: bool = False):
        """
        Output the visibilities to a .fits file.

        The complex values are converted to a 2D NumPy float array of shape [total_visiblities, 2] before being
        written to `.fits` format via the `in_array` property.

        Parameters
        ----------
        file_path
            The path the file is output to, including the filename and the ``.fits`` extension,
            e.g. '/path/to/filename.fits'
        overwrite
            If a file already exists at the path, if overwrite=True it is overwritten else an error is raised.
        """
        array_2d_util.numpy_array_2d_to_fits(
            array_2d=self.in_array, file_path=file_path, overwrite=overwrite
        )

    @property
    def scaled_maxima(self) -> Tuple[float, float]:
        """
        The maximum values of the visibilities if they are treated as a 2D grid in the complex plane.
        """
        return np.max(self.real), np.max(self.imag)

    @property
    def scaled_minima(self) -> Tuple[float, float]:
        """
        The minimum values of the visibilities if they are treated as a 2D grid in the complex plane.
        """
        return np.min(self.real), np.min(self.imag)


[docs] class Visibilities(AbstractVisibilities): @classmethod def full(cls, fill_value: float, shape_slim: Tuple[int]) -> "Visibilities": """ Create `Visibilities` (see `AbstractVisibilities.__new__`) where all (real, imag) values are filled with an input fill value, analogous to the method numpy ndarray.full. From 1D input the method cannot determine the 2D shape of the array and its mask, thus the shape_native must be input into this method. The mask is setup as a unmasked `Mask2D` of shape_native. Parameters ---------- fill_value The value all real and imaginary array elements are filled with. shape_slim The 1D shape of output visibilities. """ return cls( visibilities=np.full( fill_value=fill_value + fill_value * 1j, shape=(shape_slim[0],) ) ) @classmethod def ones(cls, shape_slim: Tuple[int]) -> "Visibilities": """ Create `Visibilities` (see `AbstractVisibilities.__new__`) where all (real, imag) values are filled with ones, analogous to the method np.ones(). From 1D input the method cannot determine the 2D shape of the array and its mask, thus the shape_native must be input into this method. The mask is setup as a unmasked `Mask2D` of shape_native. Parameters ---------- shape_slim The 1D shape of output visibilities. """ return cls.full(fill_value=1.0, shape_slim=shape_slim) @classmethod def zeros(cls, shape_slim: Tuple[int]) -> "Visibilities": """ Create `Visibilities` (see `AbstractVisibilities.__new__`) where all (real, imag) values are filled with zeros, analogous to the method np.zeros(). From 1D input the method cannot determine the 2D shape of the array and its mask, thus the shape_native must be input into this method. The mask is setup as a unmasked `Mask2D` of shape_native. Parameters ---------- shape_slim The 1D shape of output visibilities. """ return cls.full(fill_value=0.0, shape_slim=shape_slim) @classmethod def from_fits(cls, file_path: Union[Path, str], hdu: int) -> "Visibilities": """ Create `Visibilities` (see `AbstractVisibilities.__new__`) by loading the(real, imag) values from a .fits file. The `.fits` file stores these values as a real set of values of shape [total_visibilities, 2] which are converted to a 1d complex NumPy array. Parameters ---------- file_path The path the file is loaded from, including the filename and the ``.fits`` extension, e.g. '/path/to/filename.fits' hdu The Header-Data Unit of the .fits file the visibilitiy data is loaded from. """ visibilities_1d = array_2d_util.numpy_array_2d_via_fits_from( file_path=file_path, hdu=hdu ) return cls(visibilities=visibilities_1d)
class VisibilitiesNoiseMap(Visibilities): # noinspection PyUnusedLocal def __init__(self, visibilities: Union[np.ndarray, List[complex]], *args, **kwargs): """ A collection of (real, imag) visibilities noise-map values which are used to represent the noise-map in an `Interferometer` dataset. This data structure behaves the same as the `Visibilities` structure (see `AbstractVisibilities.__new__`). The only difference is that it includes a `WeightOperator` used by `Inversion`'s which use `LinearOperators` and the library `PyLops` to fit `Interferometer` data. Parameters ---------- visibilities The (real, imag) visibilities values. """ if type(visibilities) is list: visibilities = np.asarray(visibilities) if "float" in str(visibilities.dtype): if visibilities.shape[1] == 2: visibilities = ( np.apply_along_axis(lambda args: [complex(*args)], 1, visibilities) .astype("complex128") .ravel() ) self.ordered_1d = np.concatenate( (np.real(visibilities), np.imag(visibilities)), axis=0 ) super().__init__(visibilities=visibilities) weight_list = 1.0 / self.in_array**2.0 self.weight_list_ordered_1d = np.concatenate( (weight_list[:, 0], weight_list[:, 1]), axis=0 ) def __array_finalize__(self, obj): if hasattr(obj, "ordered_1d"): self.ordered_1d = obj.ordered_1d if hasattr(obj, "weight_list_ordered_1d"): self.weight_list_ordered_1d = obj.weight_list_ordered_1d