spread_functions.py   spread_functions.py 
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import numpy as np import numpy as np
import importlib.util from scipy.interpolate import InterpolatedUnivariateSpline
from scipy.interpolate import RectBivariateSpline, InterpolatedUnivariateSp
line, interp2d from astropy.io import fits
try: try:
from astropy.io import fits import mpdaf
mpdaf_there=True
except ImportError: except ImportError:
import pyfits as fits mpdaf_there=False
import logging import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('GalPaK: PSF')
# #
# This file contains the PointSpreadFunction and LineSpreadFunction interfa ces # This file contains the PointSpreadFunction and LineSpreadFunction interfa ces
# as well as some basic implementations of these interfaces : # as well as some basic implementations of these interfaces :
# - Gaussian PSF # - Gaussian PSF
# - Moffat PSF # - Moffat PSF
# - Gaussian LSF # - Gaussian LSF
# - MUSE LSF (only if mpdaf module is available) # - MUSE LSF (only if mpdaf module is available)
# #
# The instrument will use both 2D PSF and 1D LSF # The instrument will use both 2D PSF and 1D LSF
# to create a 3D PSF with which it will convolve the cubes. # to create a 3D PSF with which it will convolve the cubes.
# #
## POINT SPREAD FUNCTIONS ################################################# ##### ## POINT SPREAD FUNCTIONS ################################################# #####
class PointSpreadFunction: class PointSpreadFunction:
""" """
This is the interface all Point Spread Functions (PSF) should implement . This is the interface all Point Spread Functions (PSF) should implement .
""" """
logger = logger logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('GalPaK: PSF')
def as_image(self, for_cube): def as_image(self, for_cube):
""" """
Should return this PSF as a 2D image shaped [for_cube]. Should return this PSF as a 2D image shaped [for_cube].
for_cube: HyperspectralCube for_cube: HyperspectralCube
Has additional properties computed and attributed by GalPaK : Has additional properties computed and attributed by GalPaK :
- xy_step (in ") - xy_step (in ")
- z_step (in µm) - z_step (in µm)
- z_central (in µm) - z_central (in µm)
skipping to change at line 89 skipping to change at line 90
class ImagePointSpreadFunction(PointSpreadFunction): class ImagePointSpreadFunction(PointSpreadFunction):
""" """
A custom point spread function using a provided 2D image A custom point spread function using a provided 2D image
that should have the same shape as the cube's (x,y) and that should have the same shape as the cube's (x,y) and
centroid should be at centroid should be at
xo = (shape[1] - 1) / 2 - (shape[1] % 2 - 1) xo = (shape[1] - 1) / 2 - (shape[1] % 2 - 1)
yo = (shape[0] - 1) / 2 - (shape[0] % 2 - 1) yo = (shape[0] - 1) / 2 - (shape[0] % 2 - 1)
""" """
def __init__(self, image_2d): def __init__(self, image_psf):
""" """
accepts fits file or ndarray accepts fits file or ndarray
""" """
if isinstance(image_2d, str): if isinstance(image_psf, str):
self.filename = image_2d self.filename = image_psf
my_image = fits.open(image_2d) my_image = fits.open(image_psf)
if my_image['PRIMARY'].data is not None: if my_image['PRIMARY'].data is not None:
image_2d = my_image['PRIMARY'].data image_2d = my_image['PRIMARY'].data
header = my_image['PRIMARY'].header self.header = my_image['PRIMARY'].header
elif my_image['DATA'].data is not None: elif my_image['DATA'].data is not None:
image_2d = my_image['DATA'].data image_2d = my_image['DATA'].data
header = my_image['DATA'].header self.header = my_image['DATA'].header
elif isinstance(image_2d,np.ndarray): elif isinstance(image_psf, np.ndarray):
image_2d = image_2d image_2d = image_psf
self.filename = str(image_2d.__class__) self.filename = str(image_psf.__class__)
self.header = None
elif mpdaf_there:
if isinstance(image_psf, mpdaf.obj.Image):
self.logger.info('Provided image is a Image object')
self.header = image_psf.data_header
image_2d = image_psf.data.data
self.filename = image_psf.filename
else: else:
raise ValueError(' PSF provided is not a fits file nor an array !!') raise ValueError(' PSF provided is not a fits file nor an array !!')
logger.info( "Normalizing PSF image") self.logger.info( "Normalizing PSF image")
self.image_2d = image_2d / image_2d.sum() self.image_2d = image_2d / image_2d.sum()
self.header = header
if isinstance(self.image_2d,np.ndarray) is False: if isinstance(self.image_2d,np.ndarray) is False:
raise ValueError(' PSF provided could not be stored in an ndarr ay') raise ValueError(' PSF provided could not be stored in an ndarr ay')
if len(self.image_2d.shape)!=2: if len(self.image_2d.shape)!=2:
raise ValueError(' PSF provided is not a 2D image') raise ValueError(' PSF provided is not a 2D image')
def as_image(self, for_cube): def as_image(self, for_cube):
#check for size #check for size
if for_cube.shape[1:] != self.image_2d.shape: if for_cube.shape[1:] != self.image_2d.shape:
raise ValueError(' PSF Image and cube have different sizes: %s vs. %s' % (str(for_cube.shape[1:]),str(self.image_2d.shape)) ) raise ValueError(' PSF Image and cube have different sizes: %s vs. %s' % (str(for_cube.shape[1:]),str(self.image_2d.shape)) )
return self.image_2d return self.image_2d
def __str__(self): def __str__(self):
return """[PSF] : return """[PSF] :
type = custom type = custom
filename = {i.filename}""".format(i=self) image_psf = {i.filename}""".format(i=self)
class GaussianPointSpreadFunction(PointSpreadFunction): class GaussianPointSpreadFunction(PointSpreadFunction):
""" """
The default Gaussian Point Spread Function. The default Gaussian Point Spread Function.
fwhm: float fwhm: float
Full Width Half Maximum in arcsec, aka. "seeing". Full Width Half Maximum in arcsec, aka. "seeing".
pa: float [default is 0.] pa: float [default is 0.]
Position Angle of major-axis, anti-clockwise rotation from Y-axis, in angular degrees. Position Angle of major-axis, anti-clockwise rotation from Y-axis, in angular degrees.
ba: float [default is 1.0] ba: float [default is 1.0]
skipping to change at line 168 skipping to change at line 175
if xo is None: if xo is None:
xo = (shape[1] - 1) / 2 #center of array xo = (shape[1] - 1) / 2 #center of array
#force PSF to be centered on even grid by adding 0.5pixel if ev en #force PSF to be centered on even grid by adding 0.5pixel if ev en
#because of the upcoming padding in the convolution #because of the upcoming padding in the convolution
xo = xo - (shape[1] % 2 - 1) /2 xo = xo - (shape[1] % 2 - 1) /2
if yo is None: if yo is None:
yo = (shape[0] - 1) / 2 #center of array yo = (shape[0] - 1) / 2 #center of array
# force PSF to be centered on even grid by adding 0.5pixel if e ven # force PSF to be centered on even grid by adding 0.5pixel if e ven
# because of the upcoming padding in the convolution # because of the upcoming padding in the convolution
yo = yo - (shape[0] % 2 - 1) /2 yo = yo - (shape[0] % 2 - 1) /2
logger.info("Generating PSF at x0,y0 (%s,%s)" % (xo,yo)) self.logger.info("Generating PSF at x0,y0 (%s,%s)" % (xo,yo))
y, x = np.indices(shape) y, x = np.indices(shape)
r = self._radius(xo, yo, x, y) r = self._radius(xo, yo, x, y)
fwhm = self.fwhm / for_cube.xy_step #in pixels fwhm = self.fwhm / for_cube.xy_step #in pixels
psf = np.exp(-0.5 * (r / (fwhm / 2.35482)) ** 2) psf = np.exp(-0.5 * (r / (fwhm / 2.35482)) ** 2)
return psf / psf.sum() return psf / psf.sum()
class MoffatPointSpreadFunction(PointSpreadFunction): class MoffatPointSpreadFunction(PointSpreadFunction):
skipping to change at line 206 skipping to change at line 213
def __init__(self, fwhm=None, alpha=None, beta=None, pa=0, ba=1): def __init__(self, fwhm=None, alpha=None, beta=None, pa=0, ba=1):
self.beta = beta self.beta = beta
self.pa = pa self.pa = pa
self.ba = ba self.ba = ba
self.alpha = alpha self.alpha = alpha
self.fwhm = fwhm self.fwhm = fwhm
if not ((alpha is None) or (fwhm is None)): if not ((alpha is None) or (fwhm is None)):
self.logger.warning("Moffat psf: alpha and fwhm are both specif ied. Will use alpha. ") self.logger.warning("Moffat psf: alpha and fwhm are both specif ied. Will use alpha. ")
elif ((alpha is None) and (fwhm is None)): elif ((alpha is None) and (fwhm is None)):
raise Error("Moffat psf: alpha and fwhm are not specified. Need at least one.") raise Exception("Moffat psf: alpha and fwhm are not specified. Need at least one.")
else: else:
self.alpha = alpha self.alpha = alpha
self.fwhm = fwhm self.fwhm = fwhm
if fwhm is None: if fwhm is None:
self.fwhm = self.calculate_fwhm() self.fwhm = self.calculate_fwhm()
if alpha is None: if alpha is None:
self.alpha = self.calculate_alpha() self.alpha = self.calculate_alpha()
skipping to change at line 247 skipping to change at line 254
if xo is None: if xo is None:
xo = (shape[1] - 1) / 2 #center of array xo = (shape[1] - 1) / 2 #center of array
#force PSF to be centered on even grid by adding 0.5pixel if ev en #force PSF to be centered on even grid by adding 0.5pixel if ev en
#because of the upcoming padding in the convolution #because of the upcoming padding in the convolution
xo = xo - (shape[1] % 2 - 1) /2 xo = xo - (shape[1] % 2 - 1) /2
if yo is None: if yo is None:
yo = (shape[0] - 1) / 2 #center of array yo = (shape[0] - 1) / 2 #center of array
# force PSF to be centered on even grid by adding 0.5pixel if e ven # force PSF to be centered on even grid by adding 0.5pixel if e ven
# because of the upcoming padding in the convolution # because of the upcoming padding in the convolution
yo = yo - (shape[0] % 2 - 1) /2 yo = yo - (shape[0] % 2 - 1) /2
logger.info("Generating PSF at x0,y0 (%s,%s)" % (xo,yo)) self.logger.info("Generating PSF at x0,y0 (%s,%s)" % (xo,yo))
y, x = np.indices(shape) y, x = np.indices(shape)
r = self._radius(xo, yo, x, y) r = self._radius(xo, yo, x, y)
pix_alpha = self.alpha / for_cube.xy_step pix_alpha = self.alpha / for_cube.xy_step
psf = (1. + (r / pix_alpha) ** 2) ** (-self.beta) psf = (1. + (r / pix_alpha) ** 2) ** (-self.beta)
return psf / psf.sum() return psf / psf.sum()
def calculate_fwhm(self): def calculate_fwhm(self):
fwhm = self.alpha * (2. * np.sqrt(2. ** (1. / self.beta) - 1)) fwhm = self.alpha * (2. * np.sqrt(2. ** (1. / self.beta) - 1))
return fwhm return fwhm
def calculate_alpha(self): def calculate_alpha(self):
alpha = self.fwhm / (2. * np.sqrt(2. ** (1. / self.beta) - 1)) alpha = self.fwhm / (2. * np.sqrt(2. ** (1. / self.beta) - 1))
return alpha return alpha
if importlib.util.find_spec('maoppy') is not None: try:
import maoppy import maoppy
RAD2ARCSEC = maoppy.utils.RAD2ARCSEC RAD2ARCSEC = maoppy.utils.RAD2ARCSEC
from maoppy.psfmodel import Psfao, oversample from maoppy.psfmodel import Psfao, oversample
from maoppy.psffit import psffit from maoppy.psffit import psffit
from maoppy.instrument import muse_nfm,muse_wfm from maoppy.instrument import muse_nfm,muse_wfm
from maoppy.utils import circavgplt,circavg from maoppy.utils import circavgplt,circavg
logger.info("Found package Maoppy version {}".format(maoppy.__version__ )) logging.info("Found package Maoppy version {}".format(maoppy.__version_ _))
class MAOPPYPointSpreadFunction(Psfao, PointSpreadFunction): class CommonMeta(type(PointSpreadFunction), type(maoppy.psfmodel.Psfao)
):
pass
class MAOPPYPointSpreadFunction(type(maoppy.psfmodel.Psfao), type(Point
SpreadFunction), metaclass=CommonMeta):
""" """
This class uses the MAOPPY PSF profile described by Fetick et al. This class uses the MAOPPY PSF profile described by Fetick et al.
(2019). (2019).
References References
---------- ----------
Fetick et al. (2019, A&A, 628, A99) Fetick et al. (2019, A&A, 628, A99)
https://ui.adsabs.harvard.edu/abs/2019A&A...628A..99F/abstract https://ui.adsabs.harvard.edu/abs/2019A&A...628A..99F/abstract
""" """
skipping to change at line 429 skipping to change at line 439
xo = xo - (shape[1] % 2 - 1) / 2 xo = xo - (shape[1] % 2 - 1) / 2
if yo is None: if yo is None:
yo = (shape[0] - 1) / 2 # center of array yo = (shape[0] - 1) / 2 # center of array
# force PSF to be centered on odd grid by adding 0.5pixel i f even # force PSF to be centered on odd grid by adding 0.5pixel i f even
# because of the upcoming padding in the convolution # because of the upcoming padding in the convolution
yo = yo - (shape[0] % 2 - 1) / 2 yo = yo - (shape[0] % 2 - 1) / 2
self.logger.info("Generating PSF at x0,y0 (%s,%s)" % (xo, yo)) self.logger.info("Generating PSF at x0,y0 (%s,%s)" % (xo, yo))
self.center = (xo, yo) self.center = (xo, yo)
self.system._resolution_rad = for_cube.xy_step / RAD2ARCSEC self.system._rsolution_rad = for_cube.xy_step / RAD2ARCSEC
if 'angstr' in for_cube.z_cunit.lower(): if 'angstr' in for_cube.z_cunit.lower():
wvl_um = for_cube.z_central / 1e4 wvl_um = for_cube.z_central / 1e4
elif 'micron' in for_cube.z_cunit.lower(): elif 'micron' in for_cube.z_cunit.lower():
wvl_um = for_cube.z_central wvl_um = for_cube.z_central
elif 'nm' in for_cube.z_cunit.lower(): elif 'nm' in for_cube.z_cunit.lower():
wvl_um = for_cube.z_central / 1e3 wvl_um = for_cube.z_central / 1e3
else: else:
raise NotImplementedError("Cube units not supported") raise NotImplementedError("Cube units not supported")
self.samp = self.system.samp(wvl_um * 1e-6) self.samp = self.system.samp(wvl_um * 1e-6)
skipping to change at line 638 skipping to change at line 648
self.guess=np.array(x0) self.guess=np.array(x0)
not_fitted = np.array(self.param_name_gpk)[self.fixed] not_fitted = np.array(self.param_name_gpk)[self.fixed]
self.logger.info("MAOPPY fit: parameters fixed: {}".format(not_ fitted)) self.logger.info("MAOPPY fit: parameters fixed: {}".format(not_ fitted))
if bounds_dict is not None: if bounds_dict is not None:
self.logger.info("MAOPPY fit: setting bounds with {}".forma t(bounds_dict)) self.logger.info("MAOPPY fit: setting bounds with {}".forma t(bounds_dict))
Pmodel = self.set_bounds(Pmodel, bounds_dict) Pmodel = self.set_bounds(Pmodel, bounds_dict)
# %% Fit the PSF with Psfao # %% Fit the PSF with Psfao
self.logger.info("MAOPPY fitting: mode = {}; option fit_backgro und={} ".format(self.mode, fit_background )) self.logger.info("MAOPPY fitting: mode = {}; option fit_backgro und={} positive={} ".format(self.mode, fit_background, positive_bck ))
fitresult = psffit(psf_padded, Pmodel, self.guess, weights=weig hts, fixed=self.fixed, \ fitresult = psffit(psf_padded, Pmodel, self.guess, weights=weig hts, fixed=self.fixed, \
flux_bck = (True, fit_background), positive_bck = posit ive_bck, npixfit = None ) flux_bck = (True, fit_background), positive_bck = posit ive_bck, npixfit = None )
J = fitresult.jac J = fitresult.jac
cov = np.linalg.inv(J.T.dot(J)) cov = np.linalg.inv(J.T.dot(J))
values={} values={}
errors={} errors={}
_idx = (fixed==False).cumsum() _idx = (fixed==False).cumsum()
for i, p in enumerate(self.param_name_gpk): for i, p in enumerate(self.param_name_gpk):
if p in parameters: if p in parameters:
skipping to change at line 707 skipping to change at line 717
psf=None, psf=None,
message=fitresult.message) message=fitresult.message)
#return fitresult #return fitresult
def from_fitter(self,results): def from_fitter(self,results):
self.set_p(results.best_fit.values()) self.set_p(results.best_fit.values())
self.image_2d = results.psf self.image_2d = results.psf
self.flux = results.flux * 1e-20 #erg/s/cm2/AA self.flux = results.flux * 1e-20 #erg/s/cm2/AA
flux_Jy = 3.34e4 * (self.wvl_um*1e4) ** 2 * results.flux * 1e-2 0 flux_Jy = 3.34e4 * (self.wvl_um*1e4) ** 2 * results.flux * 1e-2 0
self.mAB = -2.5 * np.log10(flux_Jy / 3631.) self.mAB = -2.5 * np.log10(flux_Jy / 3631.)
else: except ImportError:
logger.warning('Package "maoppy" is not installed [not required]. Canno logging.warning('Package "maoppy" is not installed [not required]. Cann
t use MAOPPY PSF model.') ot use MAOPPY PSF model.')
## LINE SPREAD FUNCTIONS ################################################## ##### ## LINE SPREAD FUNCTIONS ################################################## #####
class LineSpreadFunction: class LineSpreadFunction:
""" """
This is the interface all Line Spread Functions (LSF) should implement. This is the interface all Line Spread Functions (LSF) should implement.
""" """
z_cunit = 'Undef' z_cunit = 'Undef'
 End of changes. 21 change blocks. 
32 lines changed or deleted 43 lines changed or added

This html diff was produced by rfcdiff 1.41. The latest version is available from http://tools.ietf.org/tools/rfcdiff/