__author__ = 'aymgal', 'gvernard'
from typing import Tuple
import numpy as np
import numpy.testing as npt
import warnings
from coolest.template.classes.base import APIBaseObject
from coolest.template.classes.fits_file import FitsFile
__all__ = [
'Grid',
'PixelatedRegularGrid',
'IrregularGrid',
]
SUPPORTED_CHOICES = [
'PixelatedRegularGrid',
'IrregularGrid',
]
[docs]
class Grid(APIBaseObject):
"""Generic class that represents a grid of coordinates and values.
Parameters
----------
fits_path : str
Path to the FITS file in which the values (and perhaps the coordinates)
are stored. If meant to be stored within a COOLEST template file,
this should just be the name of the FITS file, placed
in the same directory as the template.
check_fits_file : bool, optional
If True, creating the object will check that the FITS file exists, by default False
fits_file_dir : str, optional
Absolute path of the directory containing the FITS file, by default None
"""
def __init__(self,
fits_path: str,
check_fits_file: bool = False,
fits_file_dir: str = None) -> None:
[docs]
self.fits_file = FitsFile(fits_path,
check_exist=check_fits_file,
directory=fits_file_dir)
super().__init__()
[docs]
def set_grid(self, fits_path, check_fits_file):
"""Set / replace the FitsFile object associated with the Grid.
This is useful to set associate the FITS file after the Grid instance
has been created.
Parameters
----------
fits_path : str
Path to the FITS file
check_fits_file : _type_
If True, check the existence of the FITS file at fits_path.
"""
if fits_path is not None:
self.fits_file = FitsFile(fits_path, check_exist=check_fits_file)
[docs]
class PixelatedRegularGrid(Grid):
"""Class that represents a grid of values defined on a regular, Cartesian grid.
Parameters
----------
fits_path : str
Path to the FITS file in which the values (and perhaps the coordinates)
are stored. This should be relative to the final COOLEST template file.
field_of_view_x : Tuple[float], optional
2-tuple holding the extremal coordinates of the coordinates grid
along the x direction (i.e., left side of the leftmost pixel and
rightside of the rightmost pixel), by default (0, 0)
field_of_view_y : Tuple[float], optional
2-tuple holding the extremal coordinates of the coordinates grid
along the y direction (i.e., bottom side of the lower pixel and
top side of the upper pixel), by default (0, 0)
num_pix_x : int, optional
Number of pixels along the x direction, by default 0
num_pix_y : int, optional
Number of pixels along the y direction, by default 0
**kwargs_file : dic, optional
Any remaining keyword arguments for FitsFile
"""
def __init__(self,
fits_path: str = None,
field_of_view_x: Tuple[float] = (0, 0),
field_of_view_y: Tuple[float] = (0, 0),
num_pix_x: int = 0,
num_pix_y: int = 0,
**kwargs_file) -> None:
super().__init__(fits_path, **kwargs_file)
self.set_grid(None, field_of_view_x, field_of_view_y,
num_pix_x=num_pix_x, num_pix_y=num_pix_y,
check_fits_file=kwargs_file.get('check_fits_file', True))
@property
[docs]
def shape(self):
return (self.num_pix_x, self.num_pix_y)
@property
[docs]
def pixel_size(self):
if self.num_pix_x == 0 or self.num_pix_y == 0:
return 0.
pix_size_x = np.abs(self.field_of_view_x[0] - self.field_of_view_x[1]) / self.num_pix_x
pix_size_y = np.abs(self.field_of_view_y[0] - self.field_of_view_y[1]) / self.num_pix_y
npt.assert_almost_equal(pix_size_x, pix_size_y, decimal=6,
err_msg="Regular grid must have square pixels")
return pix_size_x
[docs]
def set_grid(self, fits_path,
field_of_view_x, field_of_view_y,
num_pix_x=0, num_pix_y=0,
check_fits_file=True):
"""Set / replace the FitsFile object associated with the Grid.
This is useful to set the FITS file after the Grid instance
has been created.
See class constructor for parameter descriptions.
"""
super().set_grid(fits_path, check_fits_file)
self.field_of_view_x = field_of_view_x
self.field_of_view_y = field_of_view_y
if self.fits_file.exists() and check_fits_file:
shape = self.read_fits()
self.num_pix_x, self.num_pix_y = shape[-2], shape[-1]
# if number of pixels is also given, check that it is consistent
if num_pix_x != 0 and self.num_pix_x != num_pix_x:
raise ValueError("Given number of pixels in x direction "
"is inconsistent with the fits file")
if num_pix_y != 0 and self.num_pix_y != num_pix_y:
raise ValueError("Given number of pixels in y direction "
"is inconsistent with the fits file")
else:
self.num_pix_x, self.num_pix_y = num_pix_x, num_pix_y
[docs]
def read_fits(self):
"""Read the content of the FITS file and extract the necessary Grid attributes.
Returns
-------
(num_pix_x, num_pix_y)
Number of pixels along each axis.
"""
array, header = self.fits_file.read()
array_shape = array.shape
if (len(array_shape) == 2 and array_shape != (header['NAXIS1'], header['NAXIS2']) or
len(array_shape) == 3 and array_shape != (header['NAXIS1'], header['NAXIS2'], header['NAXIS3'])):
warnings.warn("Image dimensions do not match the FITS header")
return array_shape
[docs]
def get_pixels(self, directory=None):
"""Get the pixel (z) values of the regular grid from the FITS file.
If the attribute FITS path is a relative one, it needs the absolute
directory to read the FITS file.
Parameters
----------
directory : str, optional
Absolute directory of the FITS file, by default None
Returns
-------
ndarray
2D array of pixel values associated to the regular grid.
"""
array, _ = self.fits_file.read(directory=directory)
return array
class PixelatedRegularGridStack(PixelatedRegularGrid):
def __init__(self,
fits_path: str = None,
field_of_view_x: Tuple[float] = (0, 0),
field_of_view_y: Tuple[float] = (0, 0),
num_pix_x: int = 0,
num_pix_y: int = 0,
num_stack: int = 0,
**kwargs_file) -> None:
Grid.__init__(self, fits_path, **kwargs_file)
self.set_grid(None, field_of_view_x, field_of_view_y,
num_pix_x=num_pix_x, num_pix_y=num_pix_y, num_stack=num_stack,
check_fits_file=kwargs_file.get('check_fits_file', True))
@property
def shape(self):
return (self.num_stack, self.num_pix_x, self.num_pix_y)
def set_grid(self, *args, num_stack=0, **kwargs):
super().set_grid(*args, **kwargs)
if self.fits_file.exists() and kwargs.get('check_fits_file', True):
shape = self.read_fits()
self.num_stack = shape[0]
# if number of pixels is also given, check that it is consistent
if num_stack != 0 and self.num_stack != num_stack:
raise ValueError("Number of stacked pixelated grids "
"is inconsistent with the fits file")
else:
self.num_stack = num_stack
[docs]
class IrregularGrid(Grid):
"""Class that represents an irregular set of values and their coordinates.
Parameters
----------
fits_path : str
Path to the FITS file in which the values (and perhaps the coordinates)
are stored. This should be relative to the final COOLEST template file.
field_of_view_x : Tuple[float], optional
2-tuple holding the minimum and maximum values of the x coordinates vector,
by default (0, 0)
field_of_view_y : Tuple[float], optional
2-tuple holding the minimum and maximum values of the y coordinates vector,
by default (0, 0)
num_pix : int, optional
Number of coordinates points, by default 0
**kwargs_file : dic, optional
Any remaining keyword arguments for FitsFile
"""
def __init__(self,
fits_path: str = None,
field_of_view_x: Tuple[float] = (0, 0),
field_of_view_y: Tuple[float] = (0, 0),
num_pix: int = 0,
**kwargs_file) -> None:
super().__init__(fits_path, **kwargs_file)
self.set_grid(None,
field_of_view_x=field_of_view_x,
field_of_view_y=field_of_view_y,
num_pix=num_pix)
[docs]
def set_grid(self, fits_path,
field_of_view_x=(0, 0), field_of_view_y=(0, 0),
num_pix=0, check_fits_file=True):
"""Set / replace the FitsFile object associated with the Grid.
This is useful to set associate the FITS file after the Grid instance
has been created.
See class constructor for parameter descriptions.
"""
super().set_grid(fits_path, check_fits_file)
if self.fits_file.exists():
self.field_of_view_x, self.field_of_view_y, self.num_pix = self.read_fits()
if num_pix != 0 and self.num_pix != num_pix:
raise ValueError("Given number of pixels is inconsistent with the fits file")
if field_of_view_x != (0, 0) and field_of_view_x == self.field_of_view_x:
raise ValueError("Given field of view along x direction is inconsistent with the fits file")
if field_of_view_y != (0, 0) and field_of_view_y == self.field_of_view_y:
raise ValueError("Given field of view along y direction is inconsistent with the fits file")
else:
self.field_of_view_x = field_of_view_x
self.field_of_view_y = field_of_view_y
self.num_pix = num_pix
[docs]
def read_fits(self):
"""Read the content of the FITS file and extract the necessary Grid attributes.
Returns
-------
field_of_view_x, field_of_view_y, num_pix
Field of view and number of pxiels.
"""
x, y, z = self.get_xyz()
num_pix = len(z)
#assert self.num_pix == len(z), "Given number of grid points does not match the number of .fits table rows!"
# Here we may want to check/report the overlap between the given field of view and the square encompassing the irregular grid
field_of_view_x = (min(x), max(x))
field_of_view_y = (min(y), max(y))
return field_of_view_x, field_of_view_y, num_pix
[docs]
def get_xyz(self, directory=None):
"""Get the x, y, z values of the irregular grid from the FITS file.
If the attribute FITS path is a relative one, it needs the absolute
directory to read the FITS file.
Parameters
----------
directory : str, optional
Absolute directory of the FITS file, by default None
Returns
-------
ndarray, ndarray, ndarray
x, y and z arrays
"""
data, _ = self.fits_file.read(directory=directory)
x = data.field(0)
y = data.field(1)
z = data.field(2)
return x, y, z