# image.py
# an interferometric image class
#
# Copyright (C) 2018 Andrew Chael
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division
from __future__ import print_function
from builtins import str
from builtins import range
from builtins import object
import sys
import copy
import math
import numpy as np
import numpy.matlib as matlib
import matplotlib as mpl
import matplotlib.pyplot as plt
import scipy.optimize as opt
import scipy.signal
import scipy.ndimage.filters as filt
import scipy.interpolate
from scipy import ndimage as ndi
import ehtim.observing.obs_simulate as simobs
import ehtim.observing.pulses as pulses
import ehtim.io.save
import ehtim.io.load
import ehtim.const_def as ehc
import ehtim.observing.obs_helpers as obsh
# TODO : add time to all images
# TODO : add arbitrary center location
###################################################################################################
# Image object
###################################################################################################
[docs]class Image(object):
"""A polarimetric image (in units of Jy/pixel).
Attributes:
pulse (function): The function convolved with the pixel values for continuous image.
psize (float): The pixel dimension in radians
xdim (int): The number of pixels along the x dimension
ydim (int): The number of pixels along the y dimension
mjd (int): The integer MJD of the image
time (float): The observing time of the image (UTC hours)
source (str): The astrophysical source name
ra (float): The source Right Ascension in fractional hours
dec (float): The source declination in fractional degrees
rf (float): The image frequency in Hz
polrep (str): polarization representation, either 'stokes' or 'circ'
pol_prim (str): The default image: I,Q,U or V for Stokes, or RR,LL,LR,RL for Circular
_imdict (dict): The dictionary with the polarimetric images
_mflist (list): List of spectral index images (and higher order terms)
"""
def __init__(self, image, psize, ra, dec, pa=0.0,
polrep='stokes', pol_prim=None,
rf=ehc.RF_DEFAULT, pulse=ehc.PULSE_DEFAULT, source=ehc.SOURCE_DEFAULT,
mjd=ehc.MJD_DEFAULT, time=0.):
"""A polarimetric image (in units of Jy/pixel).
Args:
image (numpy.array): The 2D intensity values in a Jy/pixel array
polrep (str): polarization representation, either 'stokes' or 'circ'
pol_prim (str): The default image: I,Q,U or V for Stokes, RR,LL,LR,RL for Circular
psize (float): The pixel dimension in radians
ra (float): The source Right Ascension in fractional hours
dec (float): The source declination in fractional degrees
pa (float): logical positional angle of the image
rf (float): The image frequency in Hz
pulse (function): The function convolved with the pixel values for continuous image.
source (str): The source name
mjd (int): The integer MJD of the image
time (float): The observing time of the image (UTC hours)
Returns:
(Image): the Image object
"""
if len(image.shape) != 2:
raise Exception("image must be a 2D numpy array")
if polrep not in ['stokes', 'circ']:
raise Exception("only 'stokes' and 'circ' are supported polreps!")
# Save the image vector
imvec = image.flatten()
if polrep == 'stokes':
if pol_prim is None:
pol_prim = 'I'
if pol_prim == 'I':
self._imdict = {'I': imvec, 'Q': np.array([]), 'U': np.array([]), 'V': np.array([])}
elif pol_prim == 'V':
self._imdict = {'I': np.array([]), 'Q': np.array([]), 'U': np.array([]), 'V': imvec}
elif pol_prim == 'Q':
self._imdict = {'I': np.array([]), 'Q': imvec, 'U': np.array([]), 'V': np.array([])}
elif pol_prim == 'U':
self._imdict = {'I': np.array([]), 'Q': np.array([]), 'U': imvec, 'V': np.array([])}
else:
raise Exception("for polrep=='stokes', pol_prim must be 'I','Q','U', or 'V'!")
elif polrep == 'circ':
if pol_prim is None:
print("polrep is 'circ' and no pol_prim specified! Setting pol_prim='RR'")
pol_prim = 'RR'
if pol_prim == 'RR':
self._imdict = {'RR': imvec, 'LL': np.array([]), 'RL': np.array([]), 'LR': np.array([])}
elif pol_prim == 'LL':
self._imdict = {'RR': np.array([]), 'LL': imvec, 'RL': np.array([]), 'LR': np.array([])}
else:
raise Exception("for polrep=='circ', pol_prim must be 'RR' or 'LL'!")
else:
raise Exception("polrep must be 'circ' or 'stokes'!")
# multifrequency spectral index, curvature arrays
# TODO -- higher orders?
# TODO -- don't initialize to zero?
avec = np.array([])
bvec = np.array([])
self._mflist = [avec, bvec]
# Save the image dimension data
self.pol_prim = pol_prim
self.polrep = polrep
self.pulse = pulse
self.psize = float(psize)
self.xdim = image.shape[1]
self.ydim = image.shape[0]
# Save the image metadata
self.ra = float(ra)
self.dec = float(dec)
self.pa = float(pa)
self.rf = float(rf)
self.source = str(source)
self.mjd = int(mjd)
# Cached FFT of the image
self.cached_fft = {}
if time > 24:
self.mjd += int((time - time % 24) / 24)
self.time = float(time % 24)
else:
self.time = time
@property
def imvec(self):
imvec = self._imdict[self.pol_prim]
return imvec
@imvec.setter
def imvec(self, vec):
if len(vec) != self.xdim * self.ydim:
raise Exception("imvec size is not consistent with xdim*ydim!")
self._imdict[self.pol_prim] = vec
@property
def specvec(self):
specvec = self._mflist[0]
return specvec
@specvec.setter
def specvec(self, vec):
if len(vec) != self.xdim * self.ydim:
raise Exception("vec size is not consistent with xdim*ydim!")
self._mflist[0] = vec
@property
def curvvec(self):
curvvec = self._mflist[1]
return curvvec
@curvvec.setter
def curvvec(self, vec):
if len(vec) != self.xdim * self.ydim:
raise Exception("vec size is not consistent with xdim*ydim!")
self._mflist[1] = vec
@property
def ivec(self):
ivec = np.array([])
if self.polrep == 'stokes':
ivec = self._imdict['I']
elif self.polrep == 'circ':
if len(self.rrvec) != 0 and len(self.llvec) != 0:
ivec = 0.5 * (self.rrvec + self.llvec)
return ivec
@ivec.setter
def ivec(self, vec):
if len(vec) != self.xdim * self.ydim:
raise Exception("vec size is not consistent with xdim*ydim!")
if self.polrep != 'stokes':
raise Exception("ivec cannot be set unless self.polrep=='stokes'")
self._imdict['I'] = vec
@property
def qvec(self):
qvec = np.array([])
if self.polrep == 'stokes':
qvec = self._imdict['Q']
elif self.polrep == 'circ':
if len(self.rlvec) != 0 and len(self.lrvec) != 0:
qvec = np.real(0.5 * (self.lrvec + self.rlvec))
return qvec
@qvec.setter
def qvec(self, vec):
if len(vec) != self.xdim * self.ydim:
raise Exception("vec size is not consistent with xdim*ydim!")
if self.polrep != 'stokes':
raise Exception("ivec cannot be set unless self.polrep=='stokes'")
self._imdict['Q'] = vec
@property
def uvec(self):
uvec = np.array([])
if self.polrep == 'stokes':
uvec = self._imdict['U']
elif self.polrep == 'circ':
if len(self.rlvec) != 0 and len(self.lrvec) != 0:
uvec = np.real(0.5j * (self.lrvec - self.rlvec))
return uvec
@uvec.setter
def uvec(self, vec):
if len(vec) != self.xdim * self.ydim:
raise Exception("vec size is not consistent with xdim*ydim!")
if self.polrep != 'stokes':
raise Exception("uvec cannot be set unless self.polrep=='stokes'")
self._imdict['U'] = vec
@property
def vvec(self):
vvec = np.array([])
if self.polrep == 'stokes':
vvec = self._imdict['V']
elif self.polrep == 'circ':
if len(self.rrvec) != 0 and len(self.llvec) != 0:
vvec = 0.5 * (self.rrvec - self.llvec)
return vvec
@vvec.setter
def vvec(self, vec):
if len(vec) != self.xdim * self.ydim:
raise Exception("vec size is not consistent with xdim*ydim!")
if self.polrep != 'stokes':
raise Exception("vvec cannot be set unless self.polrep=='stokes'")
self._imdict['V'] = vec
@property
def rrvec(self):
rrvec = np.array([])
if self.polrep == 'circ':
rrvec = self._imdict['RR']
elif self.polrep == 'stokes':
if len(self.ivec) != 0 and len(self.vvec) != 0:
rrvec = (self.ivec + self.vvec)
return rrvec
@rrvec.setter
def rrvec(self, vec):
if len(vec) != self.xdim * self.ydim:
raise Exception("vec size is not consistent with xdim*ydim!")
if self.polrep != 'circ':
raise Exception("rrvec cannot be set unless self.polrep=='circ'")
self._imdict['RR'] = vec
@property
def llvec(self):
llvec = np.array([])
if self.polrep == 'circ':
llvec = self._imdict['LL']
elif self.polrep == 'stokes':
if len(self.ivec) != 0 and len(self.vvec) != 0:
llvec = (self.ivec - self.vvec)
return llvec
@llvec.setter
def llvec(self, vec):
if len(vec) != self.xdim * self.ydim:
raise Exception("vec size is not consistent with xdim*ydim!")
if self.polrep != 'circ':
raise Exception("llvec cannot be set unless self.polrep=='circ'")
self._imdict['LL'] = vec
@property
def rlvec(self):
rlvec = np.array([])
if self.polrep == 'circ':
rlvec = self._imdict['RL']
elif self.polrep == 'stokes':
if len(self.qvec) != 0 and len(self.uvec) != 0:
rlvec = (self.qvec + 1j * self.uvec)
return rlvec
@rlvec.setter
def rlvec(self, vec):
if len(vec) != self.xdim * self.ydim:
raise Exception("vec size is not consistent with xdim*ydim!")
if self.polrep != 'circ':
raise Exception("rlvec cannot be set unless self.polrep=='circ'")
self._imdict['RL'] = vec
@property
def lrvec(self):
"""Return the imvec of LR"""
lrvec = np.array([])
if self.polrep == 'circ':
lrvec = self._imdict['LR']
elif self.polrep == 'stokes':
if len(self.qvec) != 0 and len(self.uvec) != 0:
lrvec = (self.qvec - 1j * self.uvec)
return lrvec
@lrvec.setter
def lrvec(self, vec):
"""Set the imvec"""
if len(vec) != self.xdim * self.ydim:
raise Exception("vec size is not consistent with xdim*ydim!")
if self.polrep != 'circ':
raise Exception("lrvec cannot be set unless self.polrep=='circ'")
self._imdict['LR'] = vec
@property
def pvec(self):
"""Return the polarization magnitude for each pixel"""
if self.polrep == 'circ':
pvec = np.abs(self.rlvec)
elif self.polrep == 'stokes':
pvec = np.abs(self.qvec + 1j * self.uvec)
return pvec
@property
def mvec(self):
"""Return the fractional polarization for each pixel"""
if self.polrep == 'circ':
mvec = 2 * np.abs(self.rlvec) / (self.rrvec + self.llvec)
elif self.polrep == 'stokes':
mvec = np.abs(self.qvec + 1j * self.uvec) / self.ivec
return mvec
@property
def chivec(self):
"""Return the fractional polarization angle for each pixel"""
if self.polrep == 'circ':
chivec = 0.5 * np.angle(self.rlvec / (self.rrvec + self.llvec))
elif self.polrep == 'stokes':
chivec = 0.5 * np.angle((self.qvec + 1j * self.uvec) / self.ivec)
return chivec
@property
def evpavec(self):
"""Return the fractional polarization angle for each pixel"""
return self.chivec
@property
def evec(self):
"""Return the E mode image vector"""
if self.polrep == 'circ':
qvec = np.real(0.5 * (self.lrvec + self.rlvec))
uvec = np.real(0.5j * (self.lrvec - self.rlvec))
elif self.polrep == 'stokes':
qvec = self.qvec
uvec = self.uvec
qarr = qvec.reshape((self.ydim, self.xdim))
uarr = uvec.reshape((self.ydim, self.xdim))
qarr_fft = np.fft.fftshift(np.fft.fft2(qarr))
uarr_fft = np.fft.fftshift(np.fft.fft2(uarr))
# TODO -- check conventions for u,v angle
s, t = np.meshgrid(np.flip(np.fft.fftshift(np.fft.fftfreq(self.xdim, d=1.0 / self.xdim))),
np.flip(np.fft.fftshift(np.fft.fftfreq(self.ydim, d=1.0 / self.ydim))))
s = s + .5 # .5 offset to reference to pixel center
t = t + .5 # .5 offset to reference to pixel center
uvangle = np.arctan2(s, t)
# TODO -- these conventions for e,b are from kaminokowski aara 54:227-69 sec 4.1
# TODO -- check!
cos2arr = np.round(np.cos(2 * uvangle), decimals=10)
sin2arr = np.round(np.sin(2 * uvangle), decimals=10)
earr_fft = (cos2arr * qarr_fft + sin2arr * uarr_fft)
earr = np.fft.ifft2(np.fft.ifftshift(earr_fft))
return np.real(earr.flatten())
@property
def bvec(self):
"""Return the B mode image vector"""
if self.polrep == 'circ':
qvec = np.real(0.5 * (self.lrvec + self.rlvec))
uvec = np.real(0.5j * (self.lrvec - self.rlvec))
elif self.polrep == 'stokes':
qvec = self.qvec
uvec = self.uvec
# TODO -- check conventions for u,v angle
qarr = qvec.reshape((self.ydim, self.xdim))
uarr = uvec.reshape((self.ydim, self.xdim))
qarr_fft = np.fft.fftshift(np.fft.fft2(qarr))
uarr_fft = np.fft.fftshift(np.fft.fft2(uarr))
# TODO -- are these conventions for u,v right?
s, t = np.meshgrid(np.flip(np.fft.fftshift(np.fft.fftfreq(self.xdim, d=1.0 / self.xdim))),
np.flip(np.fft.fftshift(np.fft.fftfreq(self.ydim, d=1.0 / self.ydim))))
s = s + .5 # .5 offset to reference to pixel center
t = t + .5 # .5 offset to reference to pixel center
uvangle = np.arctan2(s, t)
# TODO -- check!
cos2arr = np.round(np.cos(2 * uvangle), decimals=10)
sin2arr = np.round(np.sin(2 * uvangle), decimals=10)
barr_fft = (-sin2arr * qarr_fft + cos2arr * uarr_fft)
barr = np.fft.ifft2(np.fft.ifftshift(barr_fft))
return np.real(barr.flatten())
[docs] def get_polvec(self, pol):
"""Get the imvec corresponding to the chosen polarization
"""
if self.polrep == 'stokes' and pol is None:
pol = 'I'
elif self.polrep == 'circ' and pol is None:
pol = 'RR'
if pol.lower() == 'i':
outvec = self.ivec
elif pol.lower() == 'q':
outvec = self.qvec
elif pol.lower() == 'u':
outvec = self.uvec
elif pol.lower() == 'v':
outvec = self.vvec
elif pol.lower() == 'rr':
outvec = self.rrvec
elif pol.lower() == 'll':
outvec = self.llvec
elif pol.lower() == 'lr':
outvec = self.lrvec
elif pol.lower() == 'rl':
outvec = self.rlvec
elif pol.lower() == 'p':
outvec = self.pvec
elif pol.lower() == 'm':
outvec = self.mvec
elif pol.lower() == 'chi' or pol.lower() =='evpa':
outvec = self.chivec
elif pol.lower() == 'e':
outvec = self.evec
elif pol.lower() == 'b':
outvec = self.bvec
else:
raise Exception("Requested polvec type not recognized!")
return outvec
[docs] def image_args(self):
"""Copy arguments for making a new Image into a list and dictonary
"""
arglist = [self.imarr(), self.psize, self.ra, self.dec]
argdict = {'rf': self.rf, 'pa': self.pa,
'polrep': self.polrep, 'pol_prim': self.pol_prim,
'pulse': self.pulse, 'source': self.source,
'mjd': self.mjd, 'time': self.time}
return (arglist, argdict)
[docs] def copy(self):
"""Return a copy of the Image object.
Args:
Returns:
(Image): copy of the Image.
"""
# Make new image with primary polarization
arglist, argdict = self.image_args()
newim = Image(*arglist, **argdict)
# Copy over all polarization images
newim.copy_pol_images(self)
# Copy over spectral index information
newim._mflist = copy.deepcopy(self._mflist)
return newim
[docs] def copy_pol_images(self, old_image):
"""Copy polarization images from old_image over to self.
Args:
old_image (Image): image object to copy from
"""
for pol in list(self._imdict.keys()):
if (pol == self.pol_prim):
continue
polvec = old_image._imdict[pol]
if len(polvec):
self.add_pol_image(polvec.reshape(self.ydim, self.xdim), pol)
[docs] def add_pol_image(self, image, pol):
"""Add another image polarization.
Args:
image (list): 2D image frame (possibly complex) in a Jy/pixel array
pol (str): The image type: 'I','Q','U','V' for stokes, 'RR','LL','RL','LR' for circ
"""
if pol == self.pol_prim:
raise Exception("new pol in add_pol_image is the same as pol_prim!")
if image.shape != (self.ydim, self.xdim):
raise Exception("add_pol_image image shapes incompatible with primary image!")
if not (pol in list(self._imdict.keys())):
raise Exception("for polrep==%s, pol in add_pol_image in " %
self.polrep + ",".join(list(self._imdict.keys())))
if self.polrep == 'stokes':
if pol == 'I':
self.ivec = image.flatten()
elif pol == 'Q':
self.qvec = image.flatten()
elif pol == 'U':
self.uvec = image.flatten()
elif pol == 'V':
self.vvec = image.flatten()
elif self.polrep == 'circ':
if pol == 'RR':
self.rrvec = image.flatten()
elif pol == 'LL':
self.llvec = image.flatten()
elif pol == 'RL':
self.rlvec = image.flatten()
elif pol == 'LR':
self.lrvec = image.flatten()
return
# TODO deprecated -- replace with generic add_pol_image
[docs] def add_qu(self, qimage, uimage):
"""Add Stokes Q and U images. self.polrep must be 'stokes'
Args:
qimage (numpy.array): The 2D Stokes Q values in Jy/pixel array
uimage (numpy.array): The 2D Stokes U values in Jy/pixel array
Returns:
"""
if self.polrep != 'stokes':
raise Exception("polrep must be 'stokes' for add_qu() !")
self.add_pol_image(qimage, 'Q')
self.add_pol_image(uimage, 'U')
return
# TODO deprecated -- replace with generic add_pol_image
[docs] def add_v(self, vimage):
"""Add Stokes V image. self.polrep must be 'stokes'
Args:
vimage (numpy.array): The 2D Stokes Q values in Jy/pixel array
"""
if self.polrep != 'stokes':
raise Exception("polrep must be 'stokes' for add_v() !")
self.add_pol_image(vimage, 'V')
return
[docs] def switch_polrep(self, polrep_out='stokes', pol_prim_out=None):
"""Return a new image with the polarization representation changed
Args:
polrep_out (str): the polrep of the output data
pol_prim_out (str): The default image: I,Q,U or V for Stokes, RR,LL,LR,RL for circ
Returns:
(Image): new Image object with potentially different polrep
"""
if polrep_out not in ['stokes', 'circ']:
raise Exception("polrep_out must be either 'stokes' or 'circ'")
if pol_prim_out is None:
if polrep_out == 'stokes':
pol_prim_out = 'I'
elif polrep_out == 'circ':
pol_prim_out = 'RR'
# Simply copy if the polrep is unchanged
if polrep_out == self.polrep and pol_prim_out == self.pol_prim:
return self.copy()
# Assemble a dictionary of new polarization vectors
if polrep_out == 'stokes':
if self.polrep == 'stokes':
imdict = {'I': self.ivec, 'Q': self.qvec, 'U': self.uvec, 'V': self.vvec}
else:
if len(self.rrvec) == 0 or len(self.llvec) == 0:
ivec = np.array([])
vvec = np.array([])
else:
ivec = 0.5 * (self.rrvec + self.llvec)
vvec = 0.5 * (self.rrvec - self.llvec)
if len(self.rlvec) == 0 or len(self.lrvec) == 0:
qvec = np.array([])
uvec = np.array([])
else:
qvec = np.real(0.5 * (self.lrvec + self.rlvec))
uvec = np.real(0.5j * (self.lrvec - self.rlvec))
imdict = {'I': ivec, 'Q': qvec, 'U': uvec, 'V': vvec}
elif polrep_out == 'circ':
if self.polrep == 'circ':
imdict = {'RR': self.rrvec, 'LL': self.llvec, 'RL': self.rlvec, 'LR': self.lrvec}
else:
if len(self.ivec) == 0 or len(self.vvec) == 0:
rrvec = np.array([])
llvec = np.array([])
else:
rrvec = (self.ivec + self.vvec)
llvec = (self.ivec - self.vvec)
if len(self.qvec) == 0 or len(self.uvec) == 0:
rlvec = np.array([])
lrvec = np.array([])
else:
rlvec = (self.qvec + 1j * self.uvec)
lrvec = (self.qvec - 1j * self.uvec)
imdict = {'RR': rrvec, 'LL': llvec, 'RL': rlvec, 'LR': lrvec}
# Assemble the new image
imvec = imdict[pol_prim_out]
if len(imvec) == 0:
raise Exception("for switch_polrep to %s with pol_prim_out=%s, \n" %
(polrep_out, pol_prim_out) + "output image is not defined")
arglist, argdict = self.image_args()
arglist[0] = imvec.reshape(self.ydim, self.xdim)
argdict['polrep'] = polrep_out
argdict['pol_prim'] = pol_prim_out
newim = Image(*arglist, **argdict)
# Add in any other polarizations
for pol in list(imdict.keys()):
if pol == newim.pol_prim:
continue
polvec = imdict[pol]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim)
newim.add_pol_image(polarr, pol)
# Add in spectral index
newim._mflist = copy.deepcopy(self._mflist)
return newim
[docs] def orth_chi(self):
"""Rotate the EVPA 90 degrees
Args:
Returns:
(Image): image with rotated EVPA
"""
im = self.copy()
if im.polrep == 'stokes':
im.qvec *= -1
im.uvec *= -1
elif im.polrep == 'circ':
im.lrvec *= -1
im.rlvec *= -1
return im
[docs] def get_image_mf(self, nu):
"""Get image at a given frequency given the spectral information in self._mflist
Args:
nu (float): frequency in Hz
Returns:
(Image): image at the desired frequency
"""
# TODO -- what to do about polarization? Faraday rotation?
nuref = self.rf
log_nufrac = np.log(nu / nuref)
log_imvec = np.log(self.imvec)
for n, mfvec in enumerate(self._mflist):
if len(mfvec):
log_imvec += mfvec * (log_nufrac**(n + 1))
imvec = np.exp(log_imvec)
arglist, argdict = self.image_args()
arglist[0] = imvec.reshape(self.ydim, self.xdim)
argdict['rf'] = nu
outim = Image(*arglist, **argdict)
# Copy over all polarization images -- unchanged for now
outim.copy_pol_images(self)
# DON'T copy over spectral index information for now
# outim._mflist = copy.deepcopy(self._mflist)
return outim
[docs] def imarr(self, pol=None):
"""Return the 2D image array of a given pol parameter.
Args:
pol (str): I,Q,U or V for Stokes, or RR,LL,LR,RL for Circ
Returns:
(numpy.array): 2D image array of dimension (ydim, xdim)
"""
if pol is None:
pol = self.pol_prim
imvec = self.get_polvec(pol)
if len(imvec):
imarr = imvec.reshape(self.ydim, self.xdim)
else:
imarr = np.array([])
return imarr
[docs] def sourcevec(self):
"""Return the source position vector in geocentric coordinates at 0h GMST.
Args:
Returns:
(numpy.array): normal vector pointing to source in geocentric coordinates (m)
"""
sourcevec = np.array([np.cos(self.dec * ehc.DEGREE), 0, np.sin(self.dec * ehc.DEGREE)])
return sourcevec
[docs] def fovx(self):
"""Return the image fov in x direction in radians.
Args:
Returns:
(float) : image fov in x direction (radian)
"""
return self.psize * self.xdim
[docs] def fovy(self):
"""Returns the image fov in y direction in radians.
Args:
Returns:
(float) : image fov in y direction (radian)
"""
return self.psize * self.ydim
[docs] def total_flux(self):
"""Return the total flux of the image in Jy.
Args:
Returns:
(float) : image total flux (Jy)
"""
if self.polrep == 'stokes':
flux = np.sum(self.ivec)
elif self.polrep == 'circ':
flux = 0.5 * (np.sum(self.rrvec) + np.sum(self.llvec))
return flux
[docs] def lin_polfrac(self):
"""Return the total fractional linear polarized flux
Args:
Returns:
(float) : image fractional linear polarized flux
"""
if self.polrep == 'stokes':
frac = np.abs(np.sum(self.qvec + 1j * self.uvec)) / np.abs(np.sum(self.ivec))
elif self.polrep == 'circ':
frac = 2 * np.abs(np.sum(self.rlvec)) / np.abs(np.sum(self.rrvec + self.llvec))
return frac
[docs] def evpa(self):
"""Return the total evpa
Args:
Returns:
(float) : image average evpa (E of N) in radian
"""
if self.polrep == 'stokes':
frac = 0.5 * np.angle(np.sum(self.qvec + 1j * self.uvec))
elif self.polrep == 'circ':
frac = np.angle(np.sum(self.rlvec))
return frac
[docs] def circ_polfrac(self):
"""Return the total fractional circular polarized flux
Args:
Returns:
(float) : image fractional circular polarized flux
"""
if self.polrep == 'stokes':
frac = np.sum(self.vvec) / np.abs(np.sum(self.ivec))
elif self.polrep == 'circ':
frac = np.sum(self.rrvec - self.llvec) / np.abs(np.sum(self.rrvec + self.llvec))
return frac
[docs] def center(self, pol=None):
"""Center the image based on the coordinates of the centroid().
A non-integer shift is used, which wraps the image when rotating.
Args:
pol (str): The polarization for which to find the image centroid
Returns:
(np.array): centroid positions (x0,y0) in radians
"""
return self.shift_fft(-self.centroid(pol=pol))
[docs] def centroid(self, pol=None):
"""Compute the location of the image centroid (corresponding to the polarization pol)
Args:
pol (str): The polarization for which to find the image centroid
Returns:
(np.array): centroid positions (x0,y0) in radians
"""
if pol is None:
pol = self.pol_prim
imvec = self.get_polvec(pol)
pdim = self.psize
if len(imvec):
xlist = np.arange(0, -self.xdim, -1) * pdim + (pdim * self.xdim) / 2.0 - pdim / 2.0
ylist = np.arange(0, -self.ydim, -1) * pdim + (pdim * self.ydim) / 2.0 - pdim / 2.0
x0 = np.sum(np.outer(0.0 * ylist + 1.0, xlist).ravel() * imvec) / np.sum(imvec)
y0 = np.sum(np.outer(ylist, 0.0 * xlist + 1.0).ravel() * imvec) / np.sum(imvec)
centroid = np.array([x0, y0])
else:
raise Exception("No %s image found!" % pol)
return centroid
[docs] def pad(self, fovx, fovy):
"""Pad an image to new fov_x by fov_y in radian.
Args:
fovx (float): new fov in x dimension (rad)
fovy (float): new fov in y dimension (rad)
Returns:
im_pad (Image): padded image
"""
# Find pad widths
fovoldx = self.fovx()
fovoldy = self.fovy()
padx = int(0.5 * (fovx - fovoldx) / self.psize)
pady = int(0.5 * (fovy - fovoldy) / self.psize)
# Pad main image vector
imarr = self.imvec.reshape(self.ydim, self.xdim)
imarr = np.pad(imarr, ((pady, pady), (padx, padx)), 'constant')
# Make new image
arglist, argdict = self.image_args()
arglist[0] = imarr
outim = Image(*arglist, **argdict)
# Pad all polarizations and copy over
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
polvec = self._imdict[pol]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim)
polarr = np.pad(polarr, ((pady, pady), (padx, padx)), 'constant')
outim.add_pol_image(polarr, pol)
# Add in spectral index
mflist_out = []
for mfvec in self._mflist:
if len(mfvec):
mfarr = mfvec.reshape(self.ydim, self.xdim)
mfarr = np.pad(mfarr, ((pady, pady), (padx, padx)), 'constant')
mfvec_out = mfarr.flatten()
else:
mfvec_out = np.array([])
mflist_out.append(mfvec_out)
outim._mflist = mflist_out
return outim
[docs] def resample_square(self, xdim_new, ker_size=5):
"""Exactly resample a square image to new dimensions using the pulse function.
Args:
xdim_new (int): new pixel dimension
ker_size (int): kernel size for resampling
Returns:
im_resampled (Image): resampled image
"""
if self.xdim != self.ydim:
raise Exception("Image must be square to use Image.resample_square!")
if self.pulse == pulses.deltaPulse2D:
raise Exception("Image.resample_squre does not work with delta pulses!")
ydim_new = xdim_new
fov = self.xdim * self.psize
psize_new = float(fov) / float(xdim_new)
# Define an interpolation function using the pulse
ij = np.array([[[i * self.psize + (self.psize * self.xdim) / 2.0 - self.psize / 2.0,
j * self.psize + (self.psize * self.ydim) / 2.0 - self.psize / 2.0]
for i in np.arange(0, -self.xdim, -1)]
for j in np.arange(0, -self.ydim, -1)]).reshape((self.xdim * self.ydim, 2))
def im_new_val(imvec, x_idx, y_idx):
x = x_idx * psize_new + (psize_new * xdim_new) / 2.0 - psize_new / 2.0
y = y_idx * psize_new + (psize_new * ydim_new) / 2.0 - psize_new / 2.0
mask = (((x - ker_size * self.psize / 2.0) < ij[:, 0]) *
(ij[:, 0] < (x + ker_size * self.psize / 2.0)) *
((y - ker_size * self.psize / 2.0) < ij[:, 1]) *
(ij[:, 1] < (y + ker_size * self.psize / 2.0))
).flatten()
interp = np.sum([imvec[n] * self.pulse(x - ij[n, 0], y - ij[n, 1], self.psize, dom="I")
for n in np.arange(len(imvec))[mask]])
return interp
def im_new(imvec):
imarr_new = np.array([[im_new_val(imvec, x_idx, y_idx)
for x_idx in np.arange(0, -xdim_new, -1)]
for y_idx in np.arange(0, -ydim_new, -1)])
return imarr_new
# Compute new primary image vector
imarr_new = im_new(self.imvec)
# Normalize
scaling = np.sum(self.imvec) / np.sum(imarr_new)
imarr_new *= scaling
# Make new image
arglist, argdict = self.image_args()
arglist[0] = imarr_new
arglist[1] = psize_new
outim = Image(*arglist, **argdict)
# Interpolate all polarizations and copy over
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
polvec = self._imdict[pol]
if len(polvec):
polarr_new = im_new(polvec)
polarr_new *= scaling
outim.add_pol_image(polarr_new, pol)
# Interpolate spectral index and copy over
mflist_out = []
for mfvec in self._mflist:
print("WARNING: resample_squre not debugged for spectral index resampling!")
if len(mfvec):
mfarr = im_new(mfvec)
mfvec_out = mfarr.flatten()
else:
mfvec_out = np.array([])
mflist_out.append(mfvec_out)
outim._mflist = mflist_out
return outim
[docs] def regrid_image(self, targetfov, npix, interp='linear'):
"""Resample the image to new (square) dimensions.
Args:
targetfov (float): new field of view (radian)
npix (int): new pixel dimension
interp ('linear', 'cubic', 'quintic'): type of interpolation. default is linear
Returns:
(Image): resampled image
"""
psize_new = float(targetfov) / float(npix)
fov_x = self.fovx()
fov_y = self.fovy()
# define an interpolation function
x = np.linspace(-fov_x / 2, fov_x / 2, self.xdim)
y = np.linspace(-fov_y / 2, fov_y / 2, self.ydim)
xtarget = np.linspace(-targetfov / 2, targetfov / 2, npix)
ytarget = np.linspace(-targetfov / 2, targetfov / 2, npix)
def interp_imvec(imvec, specind=False):
if np.any(np.imag(imvec) != 0):
return interp_imvec(np.real(imvec)) + 1j * interp_imvec(np.imag(imvec))
# interpfunc = scipy.interpolate.interp2d(y, x, np.reshape(imvec, (self.ydim, self.xdim)),
# kind=interp) ## DEPRECATED. commented out for legacy
## new code to be compatible with scipy 1.14+
interp_order = {"linear": 1, "quadratic": 2, "cubic": 3}.get(interp, 1)
grid_z = np.reshape(imvec, (self.ydim, self.xdim))
interpfunc = scipy.interpolate.RectBivariateSpline(y, x, grid_z, kx=interp_order, ky=interp_order)
tmpimg = interpfunc(ytarget, xtarget)
tmpimg[np.abs(xtarget) > fov_x / 2., :] = 0.0
tmpimg[:, np.abs(ytarget) > fov_y / 2.] = 0.0
if not specind: # adjust pixel size if not a spectral index map
tmpimg = tmpimg * (psize_new)**2 / self.psize**2
return tmpimg
# Make new image
imarr_new = interp_imvec(self.imvec)
arglist, argdict = self.image_args()
arglist[0] = imarr_new
arglist[1] = psize_new
outim = Image(*arglist, **argdict)
# Interpolate all polarizations and copy over
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
polvec = self._imdict[pol]
if len(polvec):
polarr_new = interp_imvec(polvec)
outim.add_pol_image(polarr_new, pol)
# Interpolate spectral index and copy over
mflist_out = []
for mfvec in self._mflist:
if len(mfvec):
mfarr = interp_imvec(mfvec, specind=True)
mfvec_out = mfarr.flatten()
else:
mfvec_out = np.array([])
mflist_out.append(mfvec_out)
outim._mflist = mflist_out
return outim
[docs] def rotate(self, angle, interp='cubic'):
"""Rotate the image counterclockwise by the specified angle.
Args:
angle (float): CCW angle to rotate the image (radian)
interp ('linear', 'cubic', 'quintic'): type of interpolation. default is cubic
Returns:
(Image): resampled image
"""
order = 3
if interp == 'linear':
order = 1
elif interp == 'cubic':
order = 3
elif interp == 'quintic':
order = 5
# Define an interpolation function
def rot_imvec(imvec):
if np.any(np.imag(imvec) != 0):
return rot_imvec(np.real(imvec)) + 1j * rot_imvec(np.imag(imvec))
imarr_rot = scipy.ndimage.interpolation.rotate(imvec.reshape((self.ydim, self.xdim)),
angle * 180.0 / np.pi, reshape=False,
order=order, mode='constant',
cval=0.0, prefilter=True)
return imarr_rot
# pol_prim needs to be RR,LL,I,or V for a simple rotation to work!
if(not (self.pol_prim in ['RR', 'LL', 'I', 'V'])):
raise Exception("im.pol_prim must be a scalar ('I','V','RR','LL') for simple rotation!")
# Make new image
imarr_rot = rot_imvec(self.imvec)
arglist, argdict = self.image_args()
arglist[0] = imarr_rot
outim = Image(*arglist, **argdict)
# Rotate all polarizations and copy over
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
polvec = self._imdict[pol]
if len(polvec):
polarr_rot = rot_imvec(polvec)
if pol == 'RL':
polarr_rot *= np.exp(1j * 2 * angle)
elif pol == 'LR':
polarr_rot *= np.exp(-1j * 2 * angle)
elif pol == 'Q':
polarr_rot = polarr_rot + 1j * rot_imvec(self._imdict['U'])
polarr_rot = np.real(np.exp(1j * 2 * angle) * polarr_rot)
elif pol == 'U':
polarr_rot = rot_imvec(self._imdict['Q']) + 1j * polarr_rot
polarr_rot = np.imag(np.exp(1j * 2 * angle) * polarr_rot)
outim.add_pol_image(polarr_rot, pol)
# Rotate spectral index and copy over
mflist_out = []
for mfvec in self._mflist:
if len(mfvec):
mfarr = rot_imvec(mfvec)
mfvec_out = mfarr.flatten()
else:
mfvec_out = np.array([])
mflist_out.append(mfvec_out)
outim._mflist = mflist_out
return outim
[docs] def shift(self, shiftidx):
"""Shift the image by a given number of pixels.
Args:
shiftidx (list): pixel offsets [x_offset, y_offset] for the image shift
Returns:
(Image): shifted images
"""
# Define shifting function
def shift_imvec(imvec):
im_shift = np.roll(imvec.reshape(self.ydim, self.xdim), shiftidx[0], axis=0)
im_shift = np.roll(im_shift, shiftidx[1], axis=1)
return im_shift
# Make new image
imarr_shift = shift_imvec(self.imvec)
arglist, argdict = self.image_args()
arglist[0] = imarr_shift
outim = Image(*arglist, **argdict)
# Shift all polarizations and copy over
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
polvec = self._imdict[pol]
if len(polvec):
polarr_shift = shift_imvec(polvec)
outim.add_pol_image(polarr_shift, pol)
# Shift spectral index and copy over
mflist_out = []
for mfvec in self._mflist:
if len(mfvec):
mfarr = shift_imvec(mfvec)
mfvec_out = mfarr.flatten()
else:
mfvec_out = np.array([])
mflist_out.append(mfvec_out)
outim._mflist = mflist_out
return outim
[docs] def shift_fft(self, shift):
"""Shift the image by a given vector in radians.
This allows non-integer pixel shifts, via FFT.
Args:
shift (list): offsets [x_offset, y_offset] for the image shift in radians
Returns:
(Image): shifted image
"""
Nx = self.xdim
Ny = self.ydim
[dx_pixels, dy_pixels] = np.array(shift) / self.psize
s, t = np.meshgrid(np.fft.fftfreq(Nx, d=1.0 / Nx), np.fft.fftfreq(Ny, d=1.0 / Ny))
rotate = np.exp(2.0 * np.pi * 1j * (s * dx_pixels + t * dy_pixels) / float(Nx))
imarr = self.imvec.reshape((Ny, Nx))
imarr_rotate = np.real(np.fft.ifft2(np.fft.fft2(imarr) * rotate))
# make new Image
arglist, argdict = self.image_args()
arglist[0] = imarr_rotate
outim = Image(*arglist, **argdict)
# Shift all polarizations and copy over
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
polvec = self._imdict[pol]
if len(polvec):
imarr = polvec.reshape((Ny, Nx))
imarr_rotate = np.real(np.fft.ifft2(np.fft.fft2(imarr) * rotate))
outim.add_pol_image(imarr_rotate, pol)
# Shift spectral index and copy over
mflist_out = []
for mfvec in self._mflist:
if len(mfvec):
mfarr = mfvec.reshape((Ny, Nx))
mfarr = np.real(np.fft.ifft2(np.fft.fft2(mfarr) * rotate))
mfvec_out = mfarr.flatten()
else:
mfvec_out = np.array([])
mflist_out.append(mfvec_out)
outim._mflist = mflist_out
return outim
[docs] def blur_gauss(self, beamparams, frac=1., frac_pol=0):
"""Blur image with a Gaussian beam w/ beamparams [fwhm_max, fwhm_min, theta] in radians.
Args:
beamparams (list): [fwhm_maj, fwhm_min, theta, x, y] in radians
frac (float): fractional beam size for blurring the main image
frac_pol (float): fractional beam size for blurring the other polarizations
Returns:
(Image): output image
"""
if frac <= 0.0 or beamparams[0] <= 0:
return self.copy()
# Make a Gaussian image
xlist = np.arange(0, -self.xdim, -1) * self.psize + \
(self.psize * self.xdim) / 2.0 - self.psize / 2.0
ylist = np.arange(0, -self.ydim, -1) * self.psize + \
(self.psize * self.ydim) / 2.0 - self.psize / 2.0
sigma_maj = beamparams[0] / (2. * np.sqrt(2. * np.log(2.)))
sigma_min = beamparams[1] / (2. * np.sqrt(2. * np.log(2.)))
cth = np.cos(beamparams[2])
sth = np.sin(beamparams[2])
def gaussim(blurfrac):
gauss = np.array([[np.exp(-(j * cth + i * sth)**2 / (2 * (blurfrac * sigma_maj)**2) -
(i * cth - j * sth)**2 / (2 * (blurfrac * sigma_min)**2))
for i in xlist]
for j in ylist])
gauss = gauss[0:self.ydim, 0:self.xdim]
gauss = gauss / np.sum(gauss) # normalize to 1
return gauss
gauss = gaussim(frac)
if frac_pol:
gausspol = gaussim(frac_pol)
# Define a convolution function
def blur(imarr, gauss):
imarr_blur = scipy.signal.fftconvolve(gauss, imarr, mode='same')
return imarr_blur
# Convolve the primary image
imarr = (self.imvec).reshape(self.ydim, self.xdim).astype('float64')
imarr_blur = blur(imarr, gauss)
# Make new image object
arglist, argdict = self.image_args()
arglist[0] = imarr_blur
outim = Image(*arglist, **argdict)
# Blur all polarizations and copy over
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
polvec = self._imdict[pol]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim).astype('float64')
if frac_pol:
polarr = blur(polarr, gausspol)
outim.add_pol_image(polarr, pol)
# Blur spectral index and copy over
mflist_out = []
for mfvec in self._mflist:
if len(mfvec):
mfarr = mfvec.reshape(self.ydim, self.xdim).astype('float64')
mfarr = blur(mfarr, gauss)
mfvec_out = mfarr.flatten()
else:
mfvec_out = np.array([])
mflist_out.append(mfvec_out)
outim._mflist = mflist_out
return outim
[docs] def blur_circ(self, fwhm_i, fwhm_pol=0, filttype='gauss'):
"""Apply a circular gaussian filter to the image, with FWHM in radians.
Args:
fwhm_i (float): circular beam size for Stokes I blurring in radian
fwhm_pol (float): circular beam size for Stokes Q,U,V blurring in radian
filttype (str): "gauss" or "butter"
Returns:
(Image): output image
"""
sigma = fwhm_i / (2. * np.sqrt(2. * np.log(2.)))
sigmap = sigma / self.psize
fwhmp = fwhm_i / self.psize
fwhmp_pol = fwhm_pol / self.psize
# Define a convolution function
def blur_gauss(imarr, fwhm):
sigma = fwhmp / (2. * np.sqrt(2. * np.log(2.)))
if np.any(np.imag(imarr) != 0):
return blur(np.real(imarr), sigma) + 1j * blur(np.imag(imarr), sigma)
imarr_blur = filt.gaussian_filter(imarr, (sigma, sigma))
return imarr_blur
def blur_butter(imarr, size):
if size==0:
return imarr
cutoff = 1/size
Nx = self.xdim
Ny = self.ydim
s, t = np.meshgrid(np.fft.fftfreq(Nx, d=1.0 ), np.fft.fftfreq(Ny, d=1.0 ))
r = np.sqrt(s**2 + t**2)
bfilt = 1./np.sqrt(1 + (r/cutoff)**4)
imarr = self.imvec.reshape((Ny, Nx))
imarr_filt = np.real(np.fft.ifft2(np.fft.fft2(imarr) * bfilt))
return imarr_filt
if filttype=='gauss':
blur = blur_gauss
elif filttype=='butter':
blur = blur_butter
else:
raise Exception("filttype not recognized in blur_circ!")
# Blur the primary image
imarr = self.imvec.reshape(self.ydim, self.xdim)
imarr_blur = blur(imarr, fwhmp)
arglist, argdict = self.image_args()
arglist[0] = imarr_blur
outim = Image(*arglist, **argdict)
# Blur spectral index and copy over
mflist_out = []
for mfvec in self._mflist:
if len(mfvec):
mfarr = mfvec.reshape(self.ydim, self.xdim)
mfarr = blur(mfarr, fwhmp)
mfvec_out = mfarr.flatten()
else:
mfvec_out = np.array([])
mflist_out.append(mfvec_out)
outim._mflist = mflist_out
# Blur all polarizations and copy overi
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
polvec = self._imdict[pol]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim)
if fwhm_pol:
polarr = blur(polarr, fwhmp_pol)
outim.add_pol_image(polarr, pol)
return outim
[docs] def blur_mf(self, freqs, fwhm, fit_order=1, filttype='gauss'):
"""Blur image correctly across multiple frequencies
WARNING: does not currently do polarization correctly!
Args:
freqs (float): Frequencies to include in the blurring & spectral index fit
fwhm (float): circular beam size
fit_order (int): how many orders to fit spectrum: 1 or 2
filttype (str): "gauss" or "butter"
Returns:
(Image): output image
"""
if fit_order not in [1,2]:
raise Exception("fit_order must be 1 or 2 in blur_mf!")
reffreq = self.rf
# remove any zeros in the images
imlist = [self.get_image_mf(rf).blur_circ(kernel, filttype=filttype) for rf in freqs]
for image in imlist:
image.imvec[image.imvec<=0] = np.min(image.imvec[image.imvec!=0])
xfit = np.log(np.array(freqs)/reffreq)
yfit = np.log(np.array([im.imvec for im in imlist]))
if fit_order == 2:
coeffs = np.polyfit(xfit,yfit,2)
beta = coeffs[0]
alpha = coeffs[1]
elif fit_order == 1:
coeffs = np.polyfit(xfit,yfit,1)
alpha = coeffs[0]
beta = 0*alpha
else:
alpha = 0*yfit
beta = 0*yfit
outim = self.blur_circ(kernel, filttype=filttype)
outim.specvec = alpha
outim.curvvec = beta
return outim
[docs] def grad(self, gradtype='abs'):
"""Return the gradient image
Args:
gradtype (str): 'x','y',or 'abs' for the image gradient dimension
Returns:
Image : an image object containing the gradient image
"""
# Define the desired gradient function
def gradim(imvec):
if np.any(np.imag(imvec) != 0):
return gradim(np.real(imvec)) + 1j * gradim(np.imag(imvec))
imarr = imvec.reshape(self.ydim, self.xdim)
sx = ndi.sobel(imarr, axis=0, mode='nearest')
sy = ndi.sobel(imarr, axis=1, mode='nearest')
# TODO: are these in the right order??
if gradtype == 'x':
gradarr = sx
if gradtype == 'y':
gradarr = sy
else:
gradarr = np.hypot(sx, sy)
return gradarr
# Find the gradient for the primary image
gradarr = gradim(self.imvec)
arglist, argdict = self.image_args()
arglist[0] = gradarr
outim = Image(*arglist, **argdict)
# Find the gradient for all polarizations and copy over
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
polvec = self._imdict[pol]
if len(polvec):
gradarr = gradim(polvec)
outim.add_pol_image(gradarr, pol)
# Find the spectral index gradients and copy over
mflist_out = []
for mfvec in self._mflist:
if len(mfvec):
mfarr = gradim(mfvec)
mfvec_out = mfarr.flatten()
else:
mfvec_out = np.array([])
mflist_out.append(mfvec_out)
outim._mflist = mflist_out
return outim
[docs] def mask(self, cutoff=0.05, beamparams=None, frac=0.0):
"""Produce an image mask that shows all pixels above the specified cutoff frac of the max
Works off the primary image
Args:
cutoff (float): mask pixels with intensities greater than cuttoff * max
beamparams (list): either [fwhm_maj, fwhm_min, pos_ang] or a single fwhm
frac (float): the fraction of nominal beam to blur with
Returns:
(Image): output mask image
"""
# Blur the image
if beamparams is not None:
try:
len(beamparams)
except TypeError:
beamparams = [beamparams, beamparams, 0]
if len(beamparams) == 3:
mask = self.blur_gauss(beamparams, frac)
else:
raise Exception("beamparams should be a length 3 array [maj, min, posang]!")
else:
mask = self.copy()
# Mask pixels outside the desired intensity range
maxval = np.max(mask.imvec)
minval = np.min(mask.imvec)
intensityrange = maxval - minval
thresh = intensityrange * cutoff + minval
maskvec = (mask.imvec > thresh).astype(int)
# make the primary image
maskarr = maskvec.reshape(mask.ydim, mask.xdim)
arglist, argdict = self.image_args()
arglist[0] = maskarr
mask = Image(*arglist, **argdict)
# Replace all polarization imvecs with mask
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
mask.add_pol_image(maskarr, pol)
# TODO: No spectral index information in mask
return mask
[docs] def apply_mask(self, mask_im, fill_val=0.):
"""Apply a mask to the image
Args:
mask_im (Image): a mask image with the same dimensions as the Image
fill_val (float): masked pixels of all polarizations are set to this value
Returns:
(Image): the masked image
"""
if ((self.psize != mask_im.psize) or
(self.xdim != mask_im.xdim) or (self.ydim != mask_im.ydim)):
raise Exception("mask image does not match dimensions of the current image!")
# Get the mask vector
maskvec = mask_im.imvec.astype(bool)
maskvec[maskvec <= 0] = 0
maskvec[maskvec > 0] = 1
# Mask the primary image
imvec = self.imvec
imvec[~maskvec] = fill_val
imarr = imvec.reshape(self.ydim, self.xdim)
arglist, argdict = self.image_args()
arglist[0] = imarr
outim = Image(*arglist, **argdict)
# Apply mask to all polarizations and copy over
for pol in list(self._imdict.keys()):
if pol == self.pol_prim:
continue
polvec = self._imdict[pol]
if len(polvec):
polvec[~maskvec] = fill_val
polarr = polvec.reshape(self.ydim, self.xdim)
outim.add_pol_image(polarr, pol)
# Apply mask to spectral index and copy over
mflist_out = []
for mfvec in self._mflist:
if len(mfvec):
mfvec_out = copy.deepcopy(mfvec)
mfvec_out[~maskvec] = 0.
else:
mfvec_out = np.array([])
mflist_out.append(mfvec_out)
outim._mflist = mflist_out
return outim
[docs] def threshold(self, cutoff=0.05, beamparams=None, frac=0.0, fill_val=None):
"""Apply a hard threshold to the primary polarization image.
Leave other polarizations untouched.
Args:
cutoff (float): Mask pixels with intensities greater than cuttoff * max
beamparams (list): either [fwhm_maj, fwhm_min, pos_ang] or a single fwhm
frac (float): the fraction of nominal beam to blur with
fill_val (float): masked pixels are set to this value.
If fill_val==None, they are set to the min unmasked value
Returns:
(Image): output mask image
"""
if fill_val is None or fill_val is False:
maxval = np.max(self.imvec)
minval = np.min(self.imvec)
intensityrange = maxval - minval
fill_val = (intensityrange * cutoff + minval)
mask = self.mask(cutoff=cutoff, beamparams=beamparams, frac=frac)
out = self.apply_mask(mask, fill_val=fill_val)
return out
[docs] def add_flat(self, flux, pol=None):
"""Add a flat background flux to the main polarization image.
Args:
flux (float): total flux to add to image
pol (str): the polarization to add the flux to. None defaults to pol_prim.
Returns:
(Image): output image
"""
if pol is None:
pol = self.pol_prim
if not (pol in list(self._imdict.keys())):
raise Exception("for polrep==%s, pol must be in " %
self.polrep + ",".join(list(self._imdict.keys())))
if not len(self._imdict[pol]):
raise Exception("no image for pol %s" % pol)
# Make a flat image array
flatarr = ((flux / float(len(self.imvec))) * np.ones(len(self.imvec)))
flatarr = flatarr.reshape(self.ydim, self.xdim)
# Add to the main image and create the new image object
imarr = self.imvec.reshape(self.ydim, self.xdim).copy()
if pol == self.pol_prim:
imarr += flatarr
arglist, argdict = self.image_args()
arglist[0] = imarr
outim = Image(*arglist, **argdict)
# Copy over the rest of the polarizations
for pol2 in list(self._imdict.keys()):
if pol2 == self.pol_prim:
continue
polvec = self._imdict[pol2]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim).copy()
if pol2 == pol:
polarr += flatarr
outim.add_pol_image(polarr, pol2)
# Copy the spectral index (unchanged)
outim._mflist = copy.deepcopy(self._mflist)
return outim
[docs] def add_tophat(self, flux, radius, pol=None):
"""Add centered tophat flux to the Stokes I image inside a given radius.
Args:
flux (float): total flux to add to image
radius (float): radius of top hat flux in radians
pol (str): the polarization to add the flux to. None defaults to pol_prim
Returns:
(Image): output image
"""
if pol is None:
pol = self.pol_prim
if not (pol in list(self._imdict.keys())):
raise Exception("for polrep==%s, pol must be in " %
self.polrep + ",".join(list(self._imdict.keys())))
if not len(self._imdict[pol]):
raise Exception("no image for pol %s" % pol)
# Make a tophat image array
xlist = np.arange(0, -self.xdim, -1) * self.psize + \
(self.psize * self.xdim) / 2.0 - self.psize / 2.0
ylist = np.arange(0, -self.ydim, -1) * self.psize + \
(self.psize * self.ydim) / 2.0 - self.psize / 2.0
hatarr = np.array([[1.0 if np.sqrt(i**2 + j**2) <= radius else 0.
for i in xlist]
for j in ylist])
hatarr = hatarr[0:self.ydim, 0:self.xdim]
hatarr *= flux / np.sum(hatarr)
# Add to the main image and create the new image object
imarr = self.imvec.reshape(self.ydim, self.xdim).copy()
if pol == self.pol_prim:
imarr += hatarr
arglist, argdict = self.image_args()
arglist[0] = imarr
outim = Image(*arglist, **argdict)
# Copy over the rest of the polarizations
for pol2 in list(self._imdict.keys()):
if pol2 == self.pol_prim:
continue
polvec = self._imdict[pol2]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim).copy()
if pol2 == pol:
polarr += hatarr
outim.add_pol_image(polarr, pol2)
# Copy the spectral index (unchanged)
outim._mflist = copy.deepcopy(self._mflist)
return outim
[docs] def add_gauss(self, flux, beamparams, pol=None):
"""Add a gaussian to an image.
Args:
flux (float): the total flux contained in the Gaussian in Jy
beamparams (list): [fwhm_maj, fwhm_min, theta, x, y], all in radians
pol (str): the polarization to add the flux to. None defaults to pol_prim.
Returns:
(Image): output image
"""
if pol is None:
pol = self.pol_prim
if not (pol in list(self._imdict.keys())):
raise Exception("for polrep==%s, pol must be in " %
self.polrep + ",".join(list(self._imdict.keys())))
if not len(self._imdict[pol]):
raise Exception("no image for pol %s" % pol)
# Make a Gaussian image
try:
x = beamparams[3]
y = beamparams[4]
except IndexError:
x = y = 0.0
sigma_maj = beamparams[0] / (2. * np.sqrt(2. * np.log(2.)))
sigma_min = beamparams[1] / (2. * np.sqrt(2. * np.log(2.)))
cth = np.cos(beamparams[2])
sth = np.sin(beamparams[2])
xlist = np.arange(0, -self.xdim, -1) * self.psize + \
(self.psize * self.xdim) / 2.0 - self.psize / 2.0
ylist = np.arange(0, -self.ydim, -1) * self.psize + \
(self.psize * self.ydim) / 2.0 - self.psize / 2.0
def gaussian(x2, y2):
gauss = np.exp(-((y2) * cth + (x2) * sth)**2 / (2 * sigma_maj**2) +
-((x2) * cth - (y2) * sth)**2 / (2 * sigma_min**2))
return gauss
gaussarr = np.array([[gaussian(i - x, j - y) for i in xlist] for j in ylist])
gaussarr = gaussarr[0:self.ydim, 0:self.xdim]
gaussarr *= flux / np.sum(gaussarr)
# TODO: if we want to add a gaussian to V, we might also want to make sure we add it to I
# Add to the main image and create the new image object
imarr = self.imvec.reshape(self.ydim, self.xdim).copy()
if pol == self.pol_prim:
imarr += gaussarr
arglist, argdict = self.image_args()
arglist[0] = imarr
outim = Image(*arglist, **argdict)
# Copy over the rest of the polarizations
for pol2 in list(self._imdict.keys()):
if pol2 == self.pol_prim:
continue
polvec = self._imdict[pol2]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim).copy()
if pol2 == pol:
polarr += gaussarr
outim.add_pol_image(polarr, pol2)
# Copy the spectral index (unchanged)
outim._mflist = copy.deepcopy(self._mflist)
return outim
[docs] def add_crescent(self, flux, Rp, Rn, a, b, x=0, y=0, pol=None):
"""Add a crescent to an image; see Kamruddin & Dexter (2013).
Args:
flux (float): the total flux contained in the crescent in Jy
Rp (float): the larger radius in radians
Rn (float): the smaller radius in radians
a (float): the relative x offset of smaller disk in radians
b (float): the relative y offset of smaller disk in radians
x (float): the center x coordinate of the larger disk in radians
y (float): the center y coordinate of the larger disk in radians
pol (str): the polarization to add the flux to. None defaults to pol_prim.
Returns:
(Image): output image add_gaus
"""
if pol is None:
pol = self.pol_prim
if not (pol in list(self._imdict.keys())):
raise Exception("for polrep==%s, pol must be in " %
self.polrep + ",".join(list(self._imdict.keys())))
if not len(self._imdict[pol]):
raise Exception("no image for pol %s" % pol)
# Make a crescent image
xlist = np.arange(0, -self.xdim, -1) * self.psize + \
(self.psize * self.xdim) / 2.0 - self.psize / 2.0
ylist = np.arange(0, -self.ydim, -1) * self.psize + \
(self.psize * self.ydim) / 2.0 - self.psize / 2.0
def crescent(x2, y2):
if (x2 - a)**2 + (y2 - b)**2 > Rn**2 and x2**2 + y2**2 < Rp**2:
return 1.0
else:
return 0.0
crescarr = np.array([[crescent(i - x, j - y) for i in xlist] for j in ylist])
crescarr = crescarr[0:self.ydim, 0:self.xdim]
crescarr *= flux / np.sum(crescarr)
# Add to the main image and create the new image object
imarr = self.imvec.reshape(self.ydim, self.xdim).copy()
if pol == self.pol_prim:
imarr += crescarr
arglist, argdict = self.image_args()
arglist[0] = imarr
outim = Image(*arglist, **argdict)
# Copy over the rest of the polarizations
for pol2 in list(self._imdict.keys()):
if pol2 == self.pol_prim:
continue
polvec = self._imdict[pol2]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim).copy()
if pol2 == pol:
polarr += crescarr
outim.add_pol_image(polarr, pol2)
# Copy the spectral index (unchanged)
outim._mflist = copy.deepcopy(self._mflist)
return outim
[docs] def add_ring_m1(self, I0, I1, r0, phi, sigma, x=0, y=0, pol=None):
"""Add a ring to an image with an m=1 mode
Args:
I0 (float):
I1 (float):
r0 (float): the radius
phi (float): angle of m1 mode
sigma (float): the blurring size
x (float): the center x coordinate of the larger disk in radians
y (float): the center y coordinate of the larger disk in radians
pol (str): the polarization to add the flux to. None defaults to pol_prim.
Returns:
(Image): output image add_gaus
"""
if pol is None:
pol = self.pol_prim
if not (pol in list(self._imdict.keys())):
raise Exception("for polrep==%s, pol must be in " %
self.polrep + ",".join(list(self._imdict.keys())))
if not len(self._imdict[pol]):
raise Exception("no image for pol %s" % pol)
# Make a ring image
flux = I0 - 0.5 * I1
phi = phi + np.pi
psize = self.psize
xlist = np.arange(0, -self.xdim, -1) * self.psize + \
(self.psize * self.xdim) / 2.0 - self.psize / 2.0
ylist = np.arange(0, -self.ydim, -1) * self.psize + \
(self.psize * self.ydim) / 2.0 - self.psize / 2.0
def ringm1(x2, y2):
if (x2**2 + y2**2) > (r0 - psize)**2 and (x2**2 + y2**2) < (r0 + psize)**2:
theta = np.arctan2(y2, x2)
flux = (I0 - 0.5 * I1 * (1 + np.cos(theta - phi))) / (2 * np.pi * r0)
return flux
else:
return 0.0
ringarr = np.array([[ringm1(i - x, j - y)
for i in xlist]
for j in ylist])
ringarr = ringarr[0:self.ydim, 0:self.xdim]
arglist, argdict = self.image_args()
arglist[0] = ringarr
outim = Image(*arglist, **argdict)
outim = outim.blur_circ(sigma)
outim.imvec *= flux / (outim.total_flux())
ringarr = outim.imvec.reshape(self.ydim, self.xdim)
# Add to the main image and create the new image object
imarr = self.imvec.reshape(self.ydim, self.xdim).copy()
if pol == self.pol_prim:
imarr += ringarr
arglist[0] = imarr
outim = Image(*arglist, **argdict)
# Copy over the rest of the polarizations
for pol2 in list(self._imdict.keys()):
if pol2 == self.pol_prim:
continue
polvec = self._imdict[pol2]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim).copy()
if pol2 == pol:
polarr += ringarr
outim.add_pol_image(polarr, pol2)
# Copy the spectral index (unchanged)
outim._mflist = copy.deepcopy(self._mflist)
return outim
[docs] def add_const_pol(self, mag, angle, cmag=0, csign=1):
"""Return an with constant fractional linear and circular polarization
Args:
mag (float): constant polarization fraction to add to the image
angle (float): constant EVPA
cmag (float): constant circular polarization fraction to add to the image
cmag (int): constant circular polarization sign +/- 1
Returns:
(Image): output image
"""
if not (0 <= mag < 1):
raise Exception("fractional polarization magnitude must be between 0 and 1!")
if not (0 <= cmag < 1):
raise Exception("circular polarization magnitude must be between 0 and 1!")
if self.polrep == 'stokes':
im_stokes = self
elif self.polrep == 'circ':
im_stokes = self.switch_polrep(polrep_out='stokes')
ivec = im_stokes.ivec.copy()
qvec = obsh.qimage(ivec, mag * np.ones(len(ivec)), angle * np.ones(len(ivec)))
uvec = obsh.uimage(ivec, mag * np.ones(len(ivec)), angle * np.ones(len(ivec)))
vvec = cmag * np.sign(csign) * ivec
# create the new stokes image object
iarr = ivec.reshape(self.ydim, self.xdim).copy()
arglist, argdict = self.image_args()
arglist[0] = iarr
argdict['polrep'] = 'stokes'
argdict['pol_prim'] = 'I'
outim = Image(*arglist, **argdict)
# Copy over the rest of the polarizations
imdict = {'I': ivec, 'Q': qvec, 'U': uvec, 'V': vvec}
for pol in list(imdict.keys()):
if pol == 'I':
continue
polvec = imdict[pol]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim).copy()
outim.add_pol_image(polarr, pol)
# Copy the spectral index (unchanged)
outim._mflist = copy.deepcopy(self._mflist)
return outim
[docs] def add_random_pol(self, mag, corr, cmag=0., ccorr=0., seed=0):
"""Return an image random linear and circular polarizations with certain correlation lengths
Args:
mag (float): linear polarization fraction
corr (float): EVPA correlation length (radians)
cmag (float): circular polarization fraction
ccorr (float): CP correlation length (radians)
seed (int): Seed for random number generation
Returns:
(Image): output image
"""
import ehtim.scattering.stochastic_optics as so
if not (0 <= mag < 1):
raise Exception("fractional polarization magnitude must be between 0 and 1!")
if not (0 <= cmag < 1):
raise Exception("circular polarization magnitude must be between 0 and 1!")
if self.polrep == 'stokes':
im_stokes = self
elif self.polrep == 'circ':
im_stokes = self.switch_polrep(polrep_out='stokes')
ivec = im_stokes.ivec.copy()
# create the new stokes image object
iarr = ivec.reshape(self.ydim, self.xdim).copy()
arglist, argdict = self.image_args()
arglist[0] = iarr
argdict['polrep'] = 'stokes'
argdict['pol_prim'] = 'I'
outim = Image(*arglist, **argdict)
# Make a random phase screen using the scattering tools
# Use this screen to define the EVPA
dist = 1.0 * 3.086e21
rdiff = np.abs(corr) * dist / 1e3
theta_mas = 0.37 * 1.0 / rdiff * 1000. * 3600. * 180. / np.pi
sm = so.ScatteringModel(scatt_alpha=1.67, observer_screen_distance=dist,
source_screen_distance=1.e5 * dist,
theta_maj_mas_ref=theta_mas, theta_min_mas_ref=theta_mas,
r_in=rdiff * 2, r_out=1e30)
ep = so.MakeEpsilonScreen(self.xdim, self.ydim, rngseed=seed)
ps = np.array(sm.MakePhaseScreen(ep, outim, obs_frequency_Hz=29.979e9).imvec)
ps = ps / 1000**(1.66 / 2)
qvec = ivec * mag * np.sin(ps)
uvec = ivec * mag * np.cos(ps)
# Make a random phase screen using the scattering tools
# Use this screen to define the CP magnitude
if cmag != 0.0 and ccorr > 0.0:
dist = 1.0 * 3.086e21
rdiff = np.abs(ccorr) * dist / 1e3
theta_mas = 0.37 * 1.0 / rdiff * 1000. * 3600. * 180. / np.pi
sm = so.ScatteringModel(scatt_alpha=1.67, observer_screen_distance=dist,
source_screen_distance=1.e5 * dist,
theta_maj_mas_ref=theta_mas, theta_min_mas_ref=theta_mas,
r_in=rdiff * 2, r_out=1e30)
ep = so.MakeEpsilonScreen(self.xdim, self.ydim, rngseed=seed * 2)
ps = np.array(sm.MakePhaseScreen(ep, outim, obs_frequency_Hz=29.979e9).imvec)
ps = ps / 1000**(1.66 / 2)
vvec = ivec * cmag * np.sin(ps)
else:
vvec = ivec * cmag
# Copy over the rest of the polarizations
imdict = {'I': ivec, 'Q': qvec, 'U': uvec, 'V': vvec}
for pol in list(imdict.keys()):
if pol == 'I':
continue
polvec = imdict[pol]
if len(polvec):
polarr = polvec.reshape(self.ydim, self.xdim).copy()
outim.add_pol_image(polarr, pol)
# Copy the spectral index (unchanged)
outim._mflist = copy.deepcopy(self._mflist)
return outim
[docs] def add_const_mf(self, alpha, beta=0.):
"""Add a constant spectral index and curvature term
Args:
alpha (float): spectral index (with no - sign)
beta (float): curvature
Returns:
(Image): output image with constant mf information added
"""
avec = alpha * np.ones(len(self.imvec))
bvec = beta * np.ones(len(self.imvec))
# create the new image object
outim = self.copy()
outim._mflist = [avec, bvec]
return outim
[docs] def add_zblterm(self, obs, uv_min, zblval=None, new_fov=False,
gauss_sz=False, gauss_sz_factor=0.75, debias=True):
"""Add a large Gaussian term to account for missing flux in the zero baseline.
Args:
obs : an Obsdata object to determine min non-zero baseline and 0-bl flux
uv_min (float): The cutoff in Glambada used to determine what is a 0-bl
new_fov (rad): The size of the padded image once the Gaussian is added
(if False it will be set to 3 x the gaussian fwhm)
gauss_sz (rad): The size of the Gaussian added to add flux to the 0-bl.
(if False it is computed from the min non-zero baseline)
gauss_sz_factor (float): The fraction of the min non-zero baseline
used to caluclate the Gaussian FWHM.
debias (bool): True if you use debiased amplitudes to caluclate the 0-bl flux in Jy
Returns:
(Image): a padded image with a large Gaussian component
"""
if gauss_sz is False:
obs_flag = obs.flag_uvdist(uv_min=uv_min)
minuvdist = np.min(np.sqrt(obs_flag.data['u']**2 + obs_flag.data['v']**2))
gauss_sz_sigma = (1 / (gauss_sz_factor * minuvdist))
gauss_sz = gauss_sz_sigma * 2.355 # convert from stdev to fwhm
factor = 5.0
if new_fov is False:
im_fov = np.max((self.xdim * self.psize, self.ydim * self.psize))
new_fov = np.max((factor * (gauss_sz / 2.355), im_fov))
if new_fov < factor * (gauss_sz / 2.355):
print('WARNING! The specified new fov may not be large enough')
# calculate the amount of flux to include in the Gaussian
obs_zerobl = obs.flag_uvdist(uv_max=uv_min)
obs_zerobl.add_amp(debias=debias)
orig_totflux = np.sum(obs_zerobl.amp['amp'] * (1 / obs_zerobl.amp['sigma']**2))
orig_totflux /= np.sum(1 / obs_zerobl.amp['sigma']**2)
if zblval is None:
addedflux = orig_totflux - np.sum(self.imvec)
else:
addedflux = orig_totflux - zblval
print('Adding a ' + str(addedflux) + ' Jy circular Gaussian of FWHM size ' +
str(gauss_sz / ehc.RADPERUAS) + ' uas')
im_new = self.copy()
im_new = im_new.pad(new_fov, new_fov)
im_new = im_new.add_gauss(addedflux, (gauss_sz, gauss_sz, 0, 0, 0))
return im_new
[docs] def sample_uv(self, uv, polrep_obs='stokes',
sgrscat=False, ttype='nfft',
cache=False, fft_pad_factor=2,
zero_empty_pol=True, verbose=True):
"""Sample the image on the selected uv points without creating an Obsdata object.
Args:
uv (ndarray): an array of uv points
polrep_obs (str): 'stokes' or 'circ' sets the data polarimetric representation
sgrscat (bool): if True, the visibilites will be blurred by the Sgr A* kernel
ttype (str): "fast" or "nfft" or "direct"
cache (bool): Use cached fft for 'fast' mode -- deprecated, use nfft instead!
fft_pad_factor (float): zero pad the image to fft_pad_factor * image size in FFT
zero_empty_pol (bool): if True, returns zero vec if the polarization doesn't exist.
Otherwise return None
verbose (bool): Boolean value controls output prints.
Returns:
(list): a list of [I,Q,U,V] visibilities
"""
if polrep_obs not in ['stokes', 'circ']:
raise Exception("polrep_obs must be either 'stokes' or 'circ'")
data = simobs.sample_vis(self, uv, polrep_obs=polrep_obs, sgrscat=sgrscat,
ttype=ttype, cache=cache, fft_pad_factor=fft_pad_factor,
zero_empty_pol=zero_empty_pol, verbose=verbose)
return data
[docs] def observe_same_nonoise(self, obs, sgrscat=False, ttype="nfft",
cache=False, fft_pad_factor=2,
zero_empty_pol=True, verbose=True):
"""Observe the image on the same baselines as an existing observation without noise.
Args:
obs (Obsdata): the existing observation
sgrscat (bool): if True, the visibilites will be blurred by the Sgr A* kernel
ttype (str): "fast" or "nfft" or "direct"
cache (bool): Use cached fft for 'fast' mode -- deprecated, use nfft instead!
fft_pad_factor (float): zero pad the image to fft_pad_factor * image size in FFT
zero_empty_pol (bool): if True, returns zero vec if the polarization doesn't exist.
Otherwise return None
verbose (bool): Boolean value controls output prints.
Returns:
(Obsdata): an observation object with no noise
"""
# Check for agreement in coordinates and frequency
tolerance = 1e-8
if (np.abs(self.ra - obs.ra) > tolerance) or (np.abs(self.dec - obs.dec) > tolerance):
raise Exception("Image coordinates are not the same as observtion coordinates!")
if (np.abs(self.rf - obs.rf) / obs.rf > tolerance):
raise Exception("Image frequency is not the same as observation frequency!")
if (ttype == 'direct' or ttype == 'fast' or ttype == 'nfft'):
if verbose: print("Producing clean visibilities from image with " + ttype + " FT . . . ")
else:
raise Exception("ttype=%s, options for ttype are 'direct', 'fast', 'nfft'" % ttype)
# Copy data to be safe
obsdata = copy.deepcopy(obs.data)
# Extract uv datasample
uv = obsh.recarr_to_ndarr(obsdata[['u', 'v']], 'f8')
data = simobs.sample_vis(self, uv, sgrscat=sgrscat, polrep_obs=obs.polrep,
ttype=ttype, cache=cache, fft_pad_factor=fft_pad_factor,
zero_empty_pol=zero_empty_pol, verbose=verbose)
# put visibilities into the obsdata
if obs.polrep == 'stokes':
obsdata['vis'] = data[0]
if not(data[1] is None):
obsdata['qvis'] = data[1]
obsdata['uvis'] = data[2]
obsdata['vvis'] = data[3]
elif obs.polrep == 'circ':
obsdata['rrvis'] = data[0]
if not(data[1] is None):
obsdata['llvis'] = data[1]
if not(data[2] is None):
obsdata['rlvis'] = data[2]
obsdata['lrvis'] = data[3]
obs_no_noise = ehtim.obsdata.Obsdata(self.ra, self.dec, obs.rf, obs.bw, obsdata, obs.tarr,
source=self.source, mjd=self.mjd, polrep=obs.polrep,
ampcal=True, phasecal=True, opacitycal=True,
dcal=True, frcal=True,
timetype=obs.timetype, scantable=obs.scans)
return obs_no_noise
[docs] def observe_same(self, obs_in,
ttype='nfft', fft_pad_factor=2,
sgrscat=False, add_th_noise=True,
jones=False, inv_jones=False,
opacitycal=True, ampcal=True, phasecal=True,
frcal=True, dcal=True, rlgaincal=True,
stabilize_scan_phase=False, stabilize_scan_amp=False,
neggains=False,
taup=ehc.GAINPDEF,
gain_offset=ehc.GAINPDEF, gainp=ehc.GAINPDEF,
phase_std=-1,
dterm_offset=ehc.DTERMPDEF,
rlratio_std=0., rlphase_std=0.,
sigmat=None, phasesigmat=None, rlgsigmat=None,rlpsigmat=None,
caltable_path=None, seed=False, verbose=True):
"""Observe the image on the same baselines as an existing observation object and add noise.
Args:
obs_in (Obsdata): the existing observation
ttype (str): "fast" or "nfft" or "direct"
fft_pad_factor (float): zero pad the image to fft_pad_factor * image size in FFT
sgrscat (bool): if True, the visibilites will be blurred by the Sgr A* kernel
add_th_noise (bool): if True, baseline-dependent thermal noise is added
jones (bool): if True, uses Jones matrix to apply mis-calibration effects
inv_jones (bool): if True, applies estimated inverse Jones matrix
(not including random terms) to a priori calibrate data
opacitycal (bool): if False, time-dependent gaussian errors are added to opacities
ampcal (bool): if False, time-dependent gaussian errors are added to station gains
phasecal (bool): if False, time-dependent station-based random phases are added
frcal (bool): if False, feed rotation angle terms are added to Jones matrices.
dcal (bool): if False, time-dependent gaussian errors added to D-terms.
rlgaincal (bool): if False, time-dependent gains are not equal for R and L pol
stabilize_scan_phase (bool): if True, random phase errors are constant over scans
stabilize_scan_amp (bool): if True, random amplitude errors are constant over scans
neggains (bool): if True, force the applied gains to be <1
taup (float): the fractional std. dev. of the random error on the opacities
gainp (float): the fractional std. dev. of the random error on the gains
or a dict giving one std. dev. per site
gain_offset (float): the base gain offset at all sites,
or a dict giving one gain offset per site
phase_std (float): std. dev. of LCP phase,
or a dict giving one std. dev. per site
a negative value samples from uniform
dterm_offset (float): the base std. dev. of random additive error at all sites,
or a dict giving one std. dev. per site
rlratio_std (float): the fractional std. dev. of the R/L gain offset
or a dict giving one std. dev. per site
rlphase_std (float): std. dev. of R/L phase offset,
or a dict giving one std. dev. per site
a negative value samples from uniform
sigmat (float): temporal std for a Gaussian Process used to generate gains.
If sigmat=None then an iid gain noise is applied.
phasesigmat (float): temporal std for a Gaussian Process used to generate phases.
If phasesigmat=None then an iid gain noise is applied.
rlgsigmat (float): temporal std deviation for a Gaussian Process used to generate R/L gain ratios.
If rlgsigmat=None then an iid gain noise is applied.
rlpsigmat (float): temporal std deviation for a Gaussian Process used to generate R/L phase diff.
If rlpsigmat=None then an iid gain noise is applied.
caltable_path (string): If not None, path and prefix for saving the applied caltable
seed (int): seeds the random component of the noise terms. DO NOT set to 0!
verbose (bool): print updates and warnings
Returns:
(Obsdata): an observation object
"""
if seed:
np.random.seed(seed=seed)
obs = self.observe_same_nonoise(obs_in, sgrscat=sgrscat,ttype=ttype,
cache=False, fft_pad_factor=fft_pad_factor,
zero_empty_pol=True, verbose=verbose)
# Jones Matrix Corruption & Calibration
if jones:
obsdata = simobs.add_jones_and_noise(obs, add_th_noise=add_th_noise,
opacitycal=opacitycal, ampcal=ampcal,
phasecal=phasecal, frcal=frcal, dcal=dcal,
rlgaincal=rlgaincal,
stabilize_scan_phase=stabilize_scan_phase,
stabilize_scan_amp=stabilize_scan_amp,
neggains=neggains,
taup=taup,
gain_offset=gain_offset, gainp=gainp,
phase_std=phase_std,
dterm_offset=dterm_offset,
rlratio_std=rlratio_std, rlphase_std=rlphase_std,
sigmat=sigmat, phasesigmat=phasesigmat,
rlgsigmat=rlgsigmat,rlpsigmat=rlpsigmat,
caltable_path=caltable_path, seed=seed,verbose=verbose)
obs = ehtim.obsdata.Obsdata(obs.ra, obs.dec, obs.rf, obs.bw, obsdata, obs.tarr,
source=obs.source, mjd=obs.mjd, polrep=obs_in.polrep,
ampcal=ampcal, phasecal=phasecal, opacitycal=opacitycal,
dcal=dcal, frcal=frcal,
timetype=obs.timetype, scantable=obs.scans)
if inv_jones:
obsdata = simobs.apply_jones_inverse(obs,
opacitycal=opacitycal, dcal=dcal, frcal=frcal,
verbose=verbose)
obs = ehtim.obsdata.Obsdata(obs.ra, obs.dec, obs.rf, obs.bw, obsdata, obs.tarr,
source=obs.source, mjd=obs.mjd, polrep=obs_in.polrep,
ampcal=ampcal, phasecal=phasecal,
opacitycal=True, dcal=True, frcal=True,
timetype=obs.timetype, scantable=obs.scans)
# No Jones Matrices, Add noise the old way
# NOTE There is an asymmetry here - in the old way, we don't offer the ability to
# *not* unscale estimated noise.
else:
if caltable_path:
print('WARNING: the caltable is only saved if you apply noise with a Jones Matrix')
# TODO -- clean up arguments
obsdata = simobs.add_noise(obs, add_th_noise=add_th_noise,
opacitycal=opacitycal, ampcal=ampcal, phasecal=phasecal,
stabilize_scan_phase=stabilize_scan_phase,
stabilize_scan_amp=stabilize_scan_amp,
neggains=neggains,
taup=taup,
gain_offset=gain_offset, gainp=gainp,
sigmat=sigmat,
caltable_path=caltable_path, seed=seed,
verbose=verbose)
obs = ehtim.obsdata.Obsdata(obs.ra, obs.dec, obs.rf, obs.bw, obsdata, obs.tarr,
source=obs.source, mjd=obs.mjd, polrep=obs_in.polrep,
ampcal=ampcal, phasecal=phasecal,
opacitycal=True, dcal=True, frcal=True,
timetype=obs.timetype, scantable=obs.scans)
return obs
[docs] def observe(self, array, tint, tadv, tstart, tstop, bw,
mjd=None, timetype='UTC', polrep_obs=None,
elevmin=ehc.ELEV_LOW, elevmax=ehc.ELEV_HIGH,
no_elevcut_space=False,
ttype='nfft', fft_pad_factor=2, fix_theta_GMST=False,
sgrscat=False, add_th_noise=True,
jones=False, inv_jones=False,
opacitycal=True, ampcal=True, phasecal=True,
frcal=True, dcal=True, rlgaincal=True,
stabilize_scan_phase=False, stabilize_scan_amp=False,
neggains=False,
tau=ehc.TAUDEF, taup=ehc.GAINPDEF,
gain_offset=ehc.GAINPDEF, gainp=ehc.GAINPDEF,
phase_std=-1,
dterm_offset=ehc.DTERMPDEF,
rlratio_std=0.,rlphase_std=0.,
sigmat=None, phasesigmat=None, rlgsigmat=None,rlpsigmat=None,
caltable_path=None, seed=False, verbose=True):
"""Generate baselines from an array object and observe the image.
Args:
array (Array): an array object containing sites with which to generate baselines
tint (float): the scan integration time in seconds
tadv (float): the uniform cadence between scans in seconds
tstart (float): the start time of the observation in hours
tstop (float): the end time of the observation in hours
bw (float): the observing bandwidth in Hz
mjd (int): the mjd of the observation, if set as different from the image mjd
timetype (str): how to interpret tstart and tstop; either 'GMST' or 'UTC'
polrep_obs (str): 'stokes' or 'circ' sets the data polarimetric representation
elevmin (float): station minimum elevation in degrees
elevmax (float): station maximum elevation in degrees
no_elevcut_space (bool): if True, do not apply elevation cut to orbiters
ttype (str): "fast", "nfft" or "dtft"
fft_pad_factor (float): zero pad the image to fft_pad_factor * image size in the FFT
fix_theta_GMST (bool): if True, stops earth rotation to sample fixed u,v
sgrscat (bool): if True, the visibilites will be blurred by the Sgr A* kernel
add_th_noise (bool): if True, baseline-dependent thermal noise is added
jones (bool): if True, uses Jones matrix to apply mis-calibration effects
otherwise uses old formalism without D-terms
inv_jones (bool): if True, applies estimated inverse Jones matrix
(not including random terms) to calibrate data
opacitycal (bool): if False, time-dependent gaussian errors are added to opacities
ampcal (bool): if False, time-dependent gaussian errors are added to station gains
phasecal (bool): if False, time-dependent station-based random phases are added
frcal (bool): if False, feed rotation angle terms are added to Jones matrix.
dcal (bool): if False, time-dependent gaussian errors added to Jones matrix D-terms.
rlgaincal (bool): if False, time-dependent gains are not equal for R and L pol
stabilize_scan_phase (bool): if True, random phase errors are constant over scans
stabilize_scan_amp (bool): if True, random amplitude errors are constant over scans
neggains (bool): if True, force the applied gains to be <1
tau (float): the base opacity at all sites, or a dict giving one opacity per site
taup (float): the fractional std. dev. of the random error on the opacities
gainp (float): the fractional std. dev. of the random error on the gains
or a dict giving one std. dev. per site
gain_offset (float): the base gain offset at all sites,
or a dict giving one gain offset per site
phase_std (float): std. dev. of LCP phase,
or a dict giving one std. dev. per site
a negative value samples from uniform
dterm_offset (float): the base std. dev. of random additive error at all sites,
or a dict giving one std. dev. per site
rlratio_std (float): the fractional std. dev. of the R/L gain offset
or a dict giving one std. dev. per site
rlphase_std (float): std. dev. of R/L phase offset,
or a dict giving one std. dev. per site
a negative value samples from uniform
sigmat (float): temporal std for a Gaussian Process used to generate gains.
If sigmat=None then an iid gain noise is applied.
phasesigmat (float): temporal std for a Gaussian Process used to generate phases.
If phasesigmat=None then an iid gain noise is applied.
rlgsigmat (float): temporal std deviation for a Gaussian Process used to generate R/L gain ratios.
If rlgsigmat=None then an iid gain noise is applied.
rlpsigmat (float): temporal std deviation for a Gaussian Process used to generate R/L phase diff.
If rlpsigmat=None then an iid gain noise is applied.
caltable_path (string): If not None, path and prefix for saving the applied caltable
seed (int): seeds the random component of the noise terms. DO NOT set to 0!
verbose (bool): print updates and warnings
Returns:
(Obsdata): an observation object
"""
# Generate empty observation
if verbose: print("Generating empty observation file . . . ")
if mjd is None:
mjd = self.mjd
if polrep_obs is None:
polrep_obs = self.polrep
obs = array.obsdata(self.ra, self.dec, self.rf, bw, tint, tadv, tstart, tstop, mjd=mjd,
polrep=polrep_obs, tau=tau,
elevmin=elevmin, elevmax=elevmax,
no_elevcut_space=no_elevcut_space,
timetype=timetype, fix_theta_GMST=fix_theta_GMST)
# Observe on the same baselines as the empty observation and add noise
obs = self.observe_same(obs, ttype=ttype, fft_pad_factor=fft_pad_factor,
sgrscat=sgrscat, add_th_noise=add_th_noise,
jones=jones, inv_jones=inv_jones,
opacitycal=opacitycal, ampcal=ampcal,
phasecal=phasecal, dcal=dcal,
frcal=frcal, rlgaincal=rlgaincal,
stabilize_scan_phase=stabilize_scan_phase,
stabilize_scan_amp=stabilize_scan_amp,
neggains=neggains,
taup=taup,
gain_offset=gain_offset, gainp=gainp,
phase_std=phase_std,
dterm_offset=dterm_offset,
rlratio_std=rlratio_std,rlphase_std=rlphase_std,
sigmat=sigmat,phasesigmat=phasesigmat,
rlgsigmat=rlgsigmat,rlpsigmat=rlpsigmat,
caltable_path=caltable_path, seed=seed, verbose=verbose)
obs.mjd = mjd
return obs
[docs] def observe_vex(self, vex, source, t_int=0.0, tight_tadv=False,
polrep_obs=None, ttype='nfft', fft_pad_factor=2,
fix_theta_GMST=False,
sgrscat=False, add_th_noise=True,
jones=False, inv_jones=False,
opacitycal=True, ampcal=True, phasecal=True,
frcal=True, dcal=True, rlgaincal=True,
stabilize_scan_phase=False, stabilize_scan_amp=False,
neggains=False,
tau=ehc.TAUDEF, taup=ehc.GAINPDEF,
gain_offset=ehc.GAINPDEF, gainp=ehc.GAINPDEF,
phase_std=-1,
dterm_offset=ehc.DTERMPDEF,
rlratio_std=0.,rlphase_std=0.,
sigmat=None, phasesigmat=None, rlgsigmat=None,rlpsigmat=None,
caltable_path=None, seed=False, verbose=True):
"""Generate baselines from a vex file and observes the image.
Args:
vex (Vex): an vex object containing sites and scan information
source (str): the source to observe
t_int (float): if not zero, overrides the vex scan lengths
tight_tadv (float): if True, advance right after each integration,
otherwise advance after 2x the scan length
polrep_obs (str): 'stokes' or 'circ' sets the data polarimetric representation
ttype (str): "fast" or "nfft" or "dtft"
fft_pad_factor (float): zero pad the image to fft_pad_factor * image size in FFT
fix_theta_GMST (bool): if True, stops earth rotation to sample fixed u,v
sgrscat (bool): if True, the visibilites will be blurred by the Sgr A* kernel
add_th_noise (bool): if True, baseline-dependent thermal noise is added
jones (bool): if True, uses Jones matrix to apply mis-calibration effects
otherwise uses old formalism without D-terms
inv_jones (bool): if True, applies estimated inverse Jones matrix
(not including random terms) to calibrate data
opacitycal (bool): if False, time-dependent gaussian errors are added to opacities
ampcal (bool): if False, time-dependent gaussian errors are added to station gains
phasecal (bool): if False, time-dependent station-based random phases are added
frcal (bool): if False, feed rotation angle terms are added to Jones matrix.
dcal (bool): if False, time-dependent gaussian errors added to Jones matrix D-terms.
rlgaincal (bool): if False, time-dependent gains are not equal for R and L pol
stabilize_scan_phase (bool): if True, random phase errors are constant over scans
stabilize_scan_amp (bool): if True, random amplitude errors are constant over scans
neggains (bool): if True, force the applied gains to be <1
tau (float): the base opacity at all sites,
or a dict giving one opacity per site
taup (float): the fractional std. dev. of the random error on the opacities
gainp (float): the fractional std. dev. of the random error on the gains
or a dict giving one std. dev. per site
gain_offset (float): the base gain offset at all sites,
or a dict giving one gain offset per site
phase_std (float): std. dev. of LCP phase,
or a dict giving one std. dev. per site
a negative value samples from uniform
dterm_offset (float): the base std. dev. of random additive error at all sites,
or a dict giving one std. dev. per site
rlratio_std (float): the fractional std. dev. of the R/L gain offset
or a dict giving one std. dev. per site
rlphase_std (float): std. dev. of R/L phase offset,
or a dict giving one std. dev. per site
a negative value samples from uniform
sigmat (float): temporal std for a Gaussian Process used to generate gains.
If sigmat=None then an iid gain noise is applied.
phasesigmat (float): temporal std for a Gaussian Process used to generate phases.
If phasesigmat=None then an iid gain noise is applied.
rlgsigmat (float): temporal std deviation for a Gaussian Process used to generate R/L gain ratios.
If rlgsigmat=None then an iid gain noise is applied.
rlpsigmat (float): temporal std deviation for a Gaussian Process used to generate R/L phase diff.
If rlpsigmat=None then an iid gain noise is applied.
caltable_path (string): If not None, path and prefix for saving the applied caltable
seed (int): seeds the random component of the noise terms. DO NOT set to 0!
verbose (bool): print updates and warnings
Returns:
(Obsdata): an observation object
"""
if polrep_obs is None:
polrep_obs = self.polrep
t_int_flag = False
if t_int == 0.0:
t_int_flag = True
# Loop over all scans and assemble a list of scan observations
obs_List = []
for i_scan in range(len(vex.sched)):
if t_int_flag:
t_int = vex.sched[i_scan]['scan'][0]['scan_sec']
if tight_tadv:
t_adv = t_int
else:
t_adv = 2.0 * vex.sched[i_scan]['scan'][0]['scan_sec']
# If this scan doesn't observe the source, advance
if vex.sched[i_scan]['source'] != source:
continue
# What subarray is observing now?
scankeys = list(vex.sched[i_scan]['scan'].keys())
subarray = vex.array.make_subarray([vex.sched[i_scan]['scan'][key]['site']
for key in scankeys])
# Observe with the subarray over the scan interval
t_start = vex.sched[i_scan]['start_hr']
t_stop = t_start + vex.sched[i_scan]['scan'][0]['scan_sec']/3600.0 - ehc.EP
obs = self.observe(subarray, t_int, t_adv, t_start, t_stop, vex.bw_hz,
mjd=vex.sched[i_scan]['mjd_floor'], timetype='UTC',
polrep_obs=polrep_obs,
elevmin=.01, elevmax=89.99,
ttype=ttype, fft_pad_factor=fft_pad_factor,
fix_theta_GMST=fix_theta_GMST,
sgrscat=sgrscat,
add_th_noise=add_th_noise,
jones=jones, inv_jones=inv_jones,
opacitycal=opacitycal, ampcal=ampcal, phasecal=phasecal,
frcal=frcal, dcal=dcal, rlgaincal=rlgaincal,
stabilize_scan_phase=stabilize_scan_phase,
stabilize_scan_amp=stabilize_scan_amp,
neggains=neggains,
tau=tau, taup=taup,
gain_offset=gain_offset, gainp=gainp,
phase_std=phase_std,
dterm_offset=dterm_offset,
rlratio_std=rlratio_std,rlphase_std=rlphase_std,
sigmat=sigmat,phasesigmat=phasesigmat,
rlgsigmat=rlgsigmat,rlpsigmat=rlpsigmat,
caltable_path=caltable_path, seed=seed, verbose=verbose)
obs_List.append(obs)
# Merge the scans together
obs = ehtim.obsdata.merge_obs(obs_List)
return obs
[docs] def compare_images(self, im_compare, pol=None, psize=None,target_fov=None, blur_frac=0.0,
beamparams=[1., 1., 1.], metric=['nxcorr', 'nrmse', 'rssd'],
blursmall=False, shift=True):
"""Compare to another image by computing normalized cross correlation,
normalized root mean squared error, or square root of the sum of squared differences.
Returns metrics only for the primary polarization imvec!
Args:
im_compare (Image): the image to compare to
pol (str): which polarization image to compare. Default is self.pol_prim
psize (float): pixel size of comparison image (rad).
If None it is the smallest of the input image pizel sizes
target_fov (float): fov of the comparison image (rad).
If None it is twice the largest fov of the input images
beamparams (list): the nominal Gaussian beam parameters [fovx, fovy, position angle]
blur_frac (float): fractional beam to blur each image to before comparison
metric (list) : a list of fidelity metrics from ['nxcorr','nrmse','rssd']
blursmall (bool) : True to blur the unpadded image rather than the large image.
shift (int): manual image shift, otherwise use shift from maximum cross-correlation
Returns:
(tuple): [errormetric, im1_pad, im2_shift]
"""
im1 = self.copy()
im2 = im_compare.switch_polrep(polrep_out=im1.polrep, pol_prim_out=im1.pol_prim)
if im1.polrep != im2.polrep:
raise Exception("In find_shift, im1 and im2 must have the same polrep!")
if im1.pol_prim != im2.pol_prim:
raise Exception("In find_shift, im1 and im2 must have the same pol_prim!")
# Shift the comparison image to maximize normalized cross-corr.
[idx, xcorr, im1_pad, im2_pad] = im1.find_shift(im2, psize=psize, target_fov=target_fov,
beamparams=beamparams, pol=pol,
blur_frac=blur_frac, blursmall=blursmall)
if not isinstance(shift, bool):
idx = shift
im2_shift = im2_pad.shift(idx)
# Compute error metrics
error = []
imvec1 = im1_pad.get_polvec(pol)
imvec2 = im2_shift.get_polvec(pol)
if 'nxcorr' in metric:
error.append(xcorr[idx[0], idx[1]] / (im1_pad.xdim * im1_pad.ydim))
if 'nrmse' in metric:
error.append(np.sqrt(np.sum((np.abs(imvec1 - imvec2)**2 * im1_pad.psize**2)) /
np.sum((imvec1)**2 * im1_pad.psize**2)))
if 'rssd' in metric:
error.append(np.sqrt(np.sum(np.abs(imvec1 - imvec2)**2) * im1_pad.psize**2))
return (error, im1_pad, im2_shift)
[docs] def align_images(self, im_list, pol=None, shift=True, final_fov=False, scale='lin',
gamma=0.5, dynamic_range=[1.e3]):
"""Align all the images in im_list to the current image (self)
Aligns all images by comparison of the primary pol image.
Args:
im_list (list): list of images to align to the current image
shift (list): list of manual image shifts,
otherwise use the shift from maximum cross-correlation
pol (str): which polarization image to compare. Default is self.pol_prim
final_fov (float): fov of the comparison image (rad).
If False it is the largestinput image fov
scale (str) : compare images in 'log','lin',or 'gamma' scale
gamma (float): exponent for gamma scale comparison
dynamic_range (float): dynamic range for log and gamma scale comparisons
Returns:
(tuple): (im_list_shift, shifts, im0_pad)
"""
im0 = self.copy()
if not np.all(im0.polrep == np.array([im.polrep for im in im_list])):
raise Exception("In align_images, all images must have the same polrep!")
if not np.all(im0.pol_prim == np.array([im.pol_prim for im in im_list])):
raise Exception("In find_shift, all images must have the same pol_prim!")
if len(dynamic_range) == 1:
dynamic_range = dynamic_range * np.ones(len(im_list) + 1)
useshift = True
if isinstance(shift, bool):
useshift = False
# Find the minimum psize and the maximum field of view
psize = im0.psize
max_fov = np.max([im0.xdim * im0.psize, im0.ydim * im0.psize])
for i in range(0, len(im_list)):
psize = np.min([psize, im_list[i].psize])
max_fov = np.max([max_fov,
im_list[i].xdim * im_list[i].psize,
im_list[i].ydim * im_list[i].psize])
if not final_fov:
final_fov = max_fov
# Shift all images in the list
im_list_shift = []
shifts = []
for i in range(0, len(im_list)):
(idx, _, im0_pad_orig, im_pad) = im0.find_shift(im_list[i], target_fov=2 * max_fov,
psize=psize, pol=pol,
scale=scale, gamma=gamma,
dynamic_range=dynamic_range[i + 1])
if i == 0:
npix = int(im0_pad_orig.xdim / 2)
im0_pad = im0_pad_orig.regrid_image(final_fov, npix)
if useshift:
idx = shift[i]
tmp = im_pad.shift(idx)
shifts.append(idx)
im_list_shift.append(tmp.regrid_image(final_fov, npix))
return (im_list_shift, shifts, im0_pad)
[docs] def find_shift(self, im_compare, pol=None, psize=None, target_fov=None,
beamparams=[1., 1., 1.], blur_frac=0.0, blursmall=False,
scale='lin', gamma=0.5, dynamic_range=1.e3):
"""Find image shift that maximizes normalized cross correlation with a second image im2.
Finds shift only by comparison of the primary pol image.
Args:
im_compare (Image): image with respect with to switch
pol (str): which polarization image to compare. Default is self.pol_prim
psize (float): pixel size of comparison image (rad).
If None it is the smallest of the input image pizel sizes
target_fov (float): fov of the comparison image (rad).
If None it is twice the largest fov of the input images
beamparams (list): the nominal Gaussian beam parameters [fovx, fovy, position angle]
blur_frac (float): fractional beam to blur each image to before comparison
blursmall (bool) : True to blur the unpadded image rather than the large image.
scale (str) : compare images in 'log','lin',or 'gamma' scale
gamma (float): exponent for gamma scale comparison
dynamic_range (float): dynamic range for log and gamma scale comparisons
Returns:
(tuple): (errormetric, im1_pad, im2_shift)
"""
im1 = self.copy()
im2 = im_compare.switch_polrep(polrep_out=im1.polrep, pol_prim_out=im1.pol_prim)
if pol=='RL' or pol=='LR':
raise Exception("Find_shift currently doesn't work with complex RL or LR imvecs!")
if im1.polrep != im2.polrep:
raise Exception("In find_shift, im1 and im2 must have the same polrep!")
if im1.pol_prim != im2.pol_prim:
raise Exception("In find_shift, im1 and im2 must have the same pol_prim!")
# Find maximum FOV and minimum pixel size for comparison
if target_fov is None:
max_fov = np.max([im1.fovx(), im1.fovy(), im2.fovx(), im2.fovy()])
target_fov = 2 * max_fov
if psize is None:
psize = np.min([im1.psize, im2.psize])
npix = int(target_fov / psize)
# Blur images, then pad
if ((blur_frac > 0.0) and (blursmall is True)):
im1 = im1.blur_gauss(beamparams, blur_frac, blur_frac)
im2 = im2.blur_gauss(beamparams, blur_frac, blur_frac)
im1_pad = im1.regrid_image(target_fov, npix)
im2_pad = im2.regrid_image(target_fov, npix)
# or, pad images, then blur
if ((blur_frac > 0.0) and (blursmall is False)):
im1_pad = im1_pad.blur_gauss(beamparams, blur_frac, blur_frac)
im2_pad = im2_pad.blur_gauss(beamparams, blur_frac, blur_frac)
# Rescale the image vectors into log or gamma scale
# TODO -- what about negative values? complex values?
im1_pad_vec = im1_pad.get_polvec(pol)
im2_pad_vec = im2_pad.get_polvec(pol)
if scale == 'log':
im1_pad_vec[im1_pad_vec < 0.0] = 0.0
im1_pad_vec = np.log(im1_pad_vec + np.max(im1_pad_vec) / dynamic_range)
im2_pad_vec[im2_pad_vec < 0.0] = 0.0
im2_pad_vec = np.log(im2_pad_vec + np.max(im2_pad_vec) / dynamic_range)
if scale == 'gamma':
im1_pad_vec[im1_pad_vec < 0.0] = 0.0
im1_pad_vec = (im1_pad_vec + np.max(im1_pad_vec) / dynamic_range)**(gamma)
im2_pad_vec[im2_pad_vec < 0.0] = 0.0
im2_pad_vec = (im2_pad_vec + np.max(im2_pad_vec) / dynamic_range)**(gamma)
# Normalize images and compute cross correlation with FFT
im1_norm = (im1_pad_vec.reshape(im1_pad.ydim, im1_pad.xdim) - np.mean(im1_pad_vec))
im1_norm /= np.std(im1_pad_vec)
im2_norm = (im2_pad_vec.reshape(im2_pad.ydim, im2_pad.xdim) - np.mean(im2_pad_vec))
im2_norm /= np.std(im2_pad_vec)
fft_im1 = np.fft.fft2(im1_norm)
fft_im2 = np.fft.fft2(im2_norm)
xcorr = np.real(np.fft.ifft2(fft_im1 * np.conj(fft_im2)))
# Find idx of shift that maximized cross-correlation
idx = np.unravel_index(xcorr.argmax(), xcorr.shape)
return [idx, xcorr, im1_pad, im2_pad]
[docs] def fit_gauss(self, units='rad'):
"""Determine the Gaussian parameters that short baselines would measure for the source
by diagonalizing the image covariance matrix.
Returns parameters only for the primary polarization!
Args:
units (string): 'rad' returns values in radians,
'natural' returns FWHM in uas and PA in degrees
Returns:
(tuple) : a tuple (fwhm_maj, fwhm_min, theta) of the fit Gaussian parameters
"""
(x1, y1) = self.centroid()
pdim = self.psize
im = self.imvec
xlist = np.arange(0, -self.xdim, -1) * pdim + (pdim * self.xdim) / 2.0 - pdim / 2.0
ylist = np.arange(0, -self.ydim, -1) * pdim + (pdim * self.ydim) / 2.0 - pdim / 2.0
x2 = (np.sum(np.outer(0.0 * ylist + 1.0, (xlist - x1)**2).ravel() * im) / np.sum(im))
y2 = (np.sum(np.outer((ylist - y1)**2, 0.0 * xlist + 1.0).ravel() * im) / np.sum(im))
xy = (np.sum(np.outer(ylist - y1, xlist - x1).ravel() * im) / np.sum(im))
eig = np.linalg.eigh(np.array(((x2, xy), (xy, y2))))
gauss_params = np.array((eig[0][1]**0.5 * (8. * np.log(2.))**0.5,
eig[0][0]**0.5 * (8. * np.log(2.))**0.5,
np.mod(np.arctan2(eig[1][1][0], eig[1][1][1]) + np.pi, np.pi)))
if units == 'natural':
gauss_params[0] /= ehc.RADPERUAS
gauss_params[1] /= ehc.RADPERUAS
gauss_params[2] *= 180. / np.pi
return gauss_params
[docs] def fit_gauss_empirical(self, paramguess=None):
"""Determine the Gaussian parameters that short baselines would measure
Returns parameters only for the primary polarization!
Args:
paramguess (tuple): Initial guess (fwhm_maj, fwhm_min, theta) of fit parameters
Returns:
(tuple) : a tuple (fwhm_maj, fwhm_min, theta) of the fit Gaussian parameters.
"""
# This could be done using moments of the intensity distribution (self.fit_gauss)
# but we'll use the visibility approach
u_max = 1.0 / (self.psize * self.xdim) / 5.0
uv = np.array([[u, v]
for u in np.arange(-u_max, u_max * 1.001, u_max / 4.0)
for v in np.arange(-u_max, u_max * 1.001, u_max / 4.0)])
u = uv[:, 0]
v = uv[:, 1]
vis = np.dot(obsh.ftmatrix(self.psize, self.xdim, self.ydim, uv, pulse=self.pulse),
self.imvec)
if paramguess is None:
paramguess = (self.psize * self.xdim / 4.0, self.psize * self.xdim / 4.0, 0.)
def errfunc(p):
vismodel = obsh.gauss_uv(u, v, self.total_flux(), p, x=0., y=0.)
err = np.sum((np.abs(vis) - np.abs(vismodel))**2)
return err
# minimizer params
optdict = {'maxiter': 5000, 'maxfev': 5000, 'xtol': paramguess[0] / 1e9, 'ftol': 1e-10}
res = opt.minimize(errfunc, paramguess, method='Nelder-Mead', options=optdict)
# Return in the form [maj, min, PA]
x = res.x
x[0] = np.abs(x[0])
x[1] = np.abs(x[1])
x[2] = np.mod(x[2], np.pi)
if x[0] < x[1]:
maj = x[1]
x[1] = x[0]
x[0] = maj
x[2] = np.mod(x[2] + np.pi / 2.0, np.pi)
return x
[docs] def contour(self, contour_levels=[0.1, 0.25, 0.5, 0.75],
contour_cfun=None, color='w', legend=True, show_im=True,
cfun='afmhot', scale='lin', interp='gaussian', gamma=0.5, dynamic_range=1.e3,
plotp=False, nvec=20, pcut=0.01, mcut=0.1, label_type='ticks', has_title=True,
has_cbar=True, cbar_lims=(), cbar_unit=('Jy', 'pixel'),
contour_im=False, power=0, beamcolor='w',
export_pdf="", show=True, beamparams=None, cbar_orientation="vertical",
scale_lw=1, beam_lw=1, cbar_fontsize=12, axis=None, scale_fontsize=12):
"""Display the image in a contour plot.
Args:
contour_levels (arr): the fractional contour levels relative to the max flux plotted
contour_cfun (pyplot colormap function): the function used to get the RGB colors
legend (bool): True to show a legend that says what each contour line corresponds to
cfun (str): matplotlib.pyplot color function
scale (str): image scaling in ['log','gamma','lin']
interp (str): image interpolation 'gauss' or 'lin'
gamma (float): index for gamma scaling
dynamic_range (float): dynamic range for log and gamma scaling
plotp (bool): True to plot linear polarimetic image
nvec (int): number of polarimetric vectors to plot
pcut (float): minimum stokes P value for displaying polarimetric vectors
as fraction of maximum Stokes I pixel
mcut (float): minimum fractional polarization for plotting vectors
label_type (string): specifies the type of axes labeling: 'ticks', 'scale', 'none'
has_title (bool): True if you want a title on the plot
has_cbar (bool): True if you want a colorbar on the plot
cbar_lims (tuple): specify the lower and upper limit of the colorbar
cbar_unit (tuple of strings): the unit of each pixel for the colorbar:
'Jy', 'm-Jy', '$\mu$Jy'
export_pdf (str): path to exported PDF with plot
show (bool): Display the plot if true
show_im (bool): Display the image with the contour plot if True
Returns:
(matplotlib.figure.Figure): figure object with image
"""
image = self.copy()
# or some generalized version for image sizes
y = np.linspace(0, image.ydim, image.ydim)
x = np.linspace(0, image.xdim, image.xdim)
# make the image grid
z = image.imvec.reshape((image.ydim, image.xdim))
maxz = max(image.imvec)
if axis is None:
ax = plt.gca()
elif axis is not None:
ax = axis
plt.sca(axis)
if show_im:
if axis is not None:
axis = image.display(cfun=cfun, scale=scale, interp=interp, gamma=gamma,
dynamic_range=dynamic_range,
plotp=plotp, nvec=nvec, pcut=pcut, mcut=mcut,
label_type=label_type, has_title=has_title,
has_cbar=has_cbar, cbar_lims=cbar_lims,
cbar_unit=cbar_unit,
beamparams=beamparams,
cbar_orientation=cbar_orientation, scale_lw=1, beam_lw=1,
cbar_fontsize=cbar_fontsize, axis=axis,
scale_fontsize=scale_fontsize, power=power,
beamcolor=beamcolor)
else:
image.display(cfun=cfun, scale=scale, interp=interp, gamma=gamma,
dynamic_range=dynamic_range,
plotp=plotp, nvec=nvec, pcut=pcut, mcut=mcut, label_type=label_type,
has_title=has_title, has_cbar=has_cbar,
cbar_lims=cbar_lims, cbar_unit=cbar_unit, beamparams=beamparams,
cbar_orientation=cbar_orientation, scale_lw=1, beam_lw=1,
cbar_fontsize=cbar_fontsize,
axis=None, scale_fontsize=scale_fontsize,
power=power, beamcolor=beamcolor)
else:
if contour_im is False:
image.imvec = 0.0 * image.imvec
else:
image = contour_im.copy()
if axis is not None:
axis = image.display(cfun=cfun, scale=scale, interp=interp, gamma=gamma,
dynamic_range=dynamic_range,
plotp=plotp, nvec=nvec, pcut=pcut, mcut=mcut,
label_type=label_type, has_title=has_title,
has_cbar=has_cbar, cbar_lims=cbar_lims, cbar_unit=cbar_unit,
beamparams=beamparams,
cbar_orientation=cbar_orientation, scale_lw=1, beam_lw=1,
cbar_fontsize=cbar_fontsize,
axis=axis,
scale_fontsize=scale_fontsize, power=power,
beamcolor=beamcolor)
else:
image.display(cfun=cfun, scale=scale, interp=interp, gamma=gamma,
dynamic_range=dynamic_range,
plotp=plotp, nvec=nvec, pcut=pcut, mcut=mcut, label_type=label_type,
has_title=has_title,
has_cbar=has_cbar, cbar_lims=cbar_lims, cbar_unit=cbar_unit,
beamparams=beamparams,
cbar_orientation=cbar_orientation, scale_lw=1, beam_lw=1,
cbar_fontsize=cbar_fontsize, axis=None,
scale_fontsize=scale_fontsize, power=power, beamcolor=beamcolor)
if axis is None:
ax = plt.gcf()
if axis is not None:
ax = axis
if axis is not None:
ax = axis
plt.sca(axis)
count = 0.
for level in contour_levels:
if not(contour_cfun is None):
rgbval = contour_cfun(count / len(contour_levels))
rgbstring = '#%02x%02x%02x' % (rgbval[0] * 256, rgbval[1] * 256, rgbval[2] * 256)
else:
rgbstring = color
cs = plt.contour(x, y, z, levels=[level * maxz], colors=rgbstring, cmap=None)
count += 1
cs.collections[0].set_label(str(int(level * 100)) + '%')
if legend:
plt.legend()
if show:
#plt.show(block=False)
ehc.show_noblock()
if export_pdf != "":
ax.savefig(export_pdf, bbox_inches='tight', pad_inches=0)
elif axis is not None:
return axis
return ax
[docs] def display(self, pol=None, cfun=False, interp='gaussian',
scale='lin', gamma=0.5, dynamic_range=1.e3,
plotp=False, plot_stokes=False, nvec=20,
vec_cfun=None,vec_cbar_lims=(),
scut=0, pcut=0.1, mcut=0.01, scale_ticks=False,
log_offset=False,
label_type='ticks', has_title=True, alpha=1,
has_cbar=True, only_cbar=False, cbar_lims=(), cbar_unit=('Jy', 'pixel'),
export_pdf="", pdf_pad_inches=0.0, show=True, beamparams=None,
cbar_orientation="vertical", scinot=False,
scale_lw=1, beam_lw=1, cbar_fontsize=12, axis=None,
scale_fontsize=12,
power=0,
beamcolor='w', beampos='right', scalecolor='w',dpi=500):
"""Display the image.
Args:
pol (str): which polarization image to plot. Default is self.pol_prim
pol='spec' will plot spectral index
pol='curv' will plot spectral curvature
cfun (str): matplotlib.pyplot color function.
False changes with 'pol', but is 'afmhot' for most
interp (str): image interpolation 'gauss' or 'lin'
scale (str): image scaling in ['log','gamma','lin']
gamma (float): index for gamma scaling
dynamic_range (float): dynamic range for log and gamma scaling
plotp (bool): True to plot linear polarimetic image
plot_stokes (bool): True to plot stokes subplots along with plotp
nvec (int): number of polarimetric vectors to plot
vec_cfun (str): color function for vectors colored by lin pol frac
vec_cbar_lims (tuple): lower and upper limit of the fractional polarization colormap
scut (float): minimum fractional stokes I value for displaying spectral index
pcut (float): minimum fractional stokes I value for displaying polarimetric vectors
mcut (float): minimum fractional polarization value for displaying vectors
scale_ticks (bool): if True, scale polarization ticks by linear polarization magnitude
label_type (string): specifies the type of axes labeling: 'ticks', 'scale', 'none'
has_title (bool): True if you want a title on the plot
has_cbar (bool): True if you want a colorbar on the plot
cbar_lims (tuple): specify the lower and upper limit of the colorbar
cbar_unit (tuple): specifies the unit of the colorbar: e.g.,
('Jy','pixel'),('m-Jy','$\mu$as$^2$'),['Tb']
beamparams (list): [fwhm_maj, fwhm_min, theta], set to plot beam contour
export_pdf (str): path to exported PDF with plot
show (bool): Display the plot if true
scinot (bool): Display numbers/units in scientific notation
scale_lw (float): Linewidth of the scale overlay
beam_lw (float): Linewidth of the beam overlay
cbar_fontsize (float): Fontsize of the text elements of the colorbar
axis (matplotlib.axes.Axes): An axis object
scale_fontsize (float): Fontsize of the scale label
power (float): Passed to colorbar for division of ticks by 1e(power)
beamcolor (str): color of the beam overlay
scalecolor (str): color of the scale label overlay
Returns:
(matplotlib.figure.Figure): figure object with image
"""
if (interp in ['gauss', 'gaussian', 'Gaussian', 'Gauss']):
interp = 'gaussian'
elif (interp in ['linear','bilinear']):
interp = 'bilinear'
else:
interp = 'none'
if not(beamparams is None or beamparams is False):
if beamparams[0] > self.fovx() or beamparams[1] > self.fovx():
raise Exception("beam FWHM must be smaller than fov!")
if self.polrep == 'stokes' and pol is None:
pol = 'I'
elif self.polrep == 'circ' and pol is None:
pol = 'RR'
if only_cbar:
has_cbar = True
label_type = 'none'
has_title = False
if axis is None:
f = plt.figure()
plt.clf()
if axis is not None:
plt.sca(axis)
f = plt.gcf()
# Get unit scale factor
factor = 1.
fluxunit = 'Jy'
areaunit = 'pixel'
if cbar_unit[0] in ['m-Jy', 'mJy']:
fluxunit = 'mJy'
factor *= 1.e3
elif cbar_unit[0] in ['muJy', r'$\mu$-Jy', r'$\mu$Jy']:
fluxunit = r'$\mu$Jy'
factor *= 1.e6
elif cbar_unit[0] == 'Tb':
factor = 3.254e13 / (self.rf**2 * self.psize**2)
fluxunit = 'Brightness Temperature (K)'
areaunit = ''
if power != 0:
fluxunit = (r'Brightness Temperature ($10^{{' + str(power) + '}}$ K)')
else:
fluxunit = 'Brightness Temperature (K)'
elif cbar_unit[0] in ['Jy']:
fluxunit = 'Jy'
factor *= 1.
else:
factor = 1
fluxunit = cbar_unit[0]
areaunit = ''
if len(cbar_unit) == 1 or cbar_unit[0] == 'Tb':
factor *= 1.
elif cbar_unit[1] == 'pixel':
factor *= 1.
if power != 0:
areaunit = areaunit + (r' ($10^{{' + str(power) + '}}$ K)')
elif cbar_unit[1] in ['$arcseconds$^2$', 'as$^2$', 'as2']:
areaunit = 'as$^2$'
fovfactor = self.xdim * self.psize * (1 / ehc.RADPERAS)
factor *= (1. / fovfactor)**2 / (1. / self.xdim)**2
if power != 0:
areaunit = areaunit + (r' ($10^{{' + str(power) + '}}$ K)')
elif cbar_unit[1] in [r'$\m-arcseconds$^2$', 'mas$^2$', 'mas2']:
areaunit = 'mas$^2$'
fovfactor = self.xdim * self.psize * (1 / ehc.RADPERUAS) / 1000.
factor *= (1. / fovfactor)**2 / (1. / self.xdim)**2
if power != 0:
areaunit = areaunit + (r' ($10^{{' + str(power) + '}}$ K)')
elif cbar_unit[1] in [r'$\mu$-arcseconds$^2$', r'$\mu$as$^2$', 'muas2']:
areaunit = r'$\mu$as$^2$'
fovfactor = self.xdim * self.psize * (1 / ehc.RADPERUAS)
factor *= (1. / fovfactor)**2 / (1. / self.xdim)**2
if power != 0:
areaunit = areaunit + (r' ($10^{{' + str(power) + '}}$ K)')
elif cbar_unit[1] == 'beam':
if (beamparams is None or beamparams is False):
print("Cannot convert to Jy/beam without beamparams!")
else:
areaunit = 'beam'
beamarea = (2.0 * np.pi * beamparams[0] * beamparams[1] / (8.0 * np.log(2)))
factor *= beamarea / (self.psize**2)
if power != 0:
areaunit = areaunit + (r' ($10^{{' + str(power) + '}}$ K)')
else:
raise ValueError('cbar_unit ' + cbar_unit[1] + ' is not a possible option')
# Plot a single polarization image
if not plotp:
cbar_lims_p = ()
if pol.lower() == 'spec':
imvec = self.specvec.copy()
# mask out low total intensity values
mask = self.imvec < (scut * np.max(self.imvec))
imvec[mask] = np.nan
unit = r'$\alpha$'
factor = 1
cbar_lims_p = [-5, 5]
cfun_p = 'seismic'
elif pol.lower() == 'curv':
imvec = self.curvvec.copy()
# mask out low total intensity values
mask = self.imvec < (scut * np.max(self.imvec))
imvec[mask] = np.nan
unit = r'$\beta$'
factor = 1
cbar_lims_p = [-5, 5]
cfun_p = 'seismic'
elif pol.lower() == 'm':
imvec = self.mvec.copy()
unit = r'$\|\breve{m}|$'
factor = 1
cbar_lims_p = [0, 1]
cfun_p = 'cool'
elif pol.lower() == 'p':
imvec = self.mvec * self.ivec
unit = r'$\|P|$'
cfun_p = 'afmhot'
elif pol.lower() == 'chi' or pol.lower() == 'evpa':
imvec = self.chivec.copy() / ehc.DEGREE
unit = r'$\chi (^\circ)$'
factor = 1
cbar_lims_p = [0, 180]
cfun_p = 'hsv'
elif pol.lower() == 'e':
imvec = self.evec.copy()
unit = r'$E$-mode'
cfun_p = 'Spectral'
elif pol.lower() == 'b':
imvec = self.bvec.copy()
unit = r'$B$-mode'
cfun_p = 'Spectral'
else:
pol = pol.upper()
if pol == 'V':
cfun_p = 'bwr'
else:
cfun_p = 'afmhot'
try:
imvec = np.array(self._imdict[pol]).reshape(-1) / (10.**power)
except KeyError:
try:
if self.polrep == 'stokes':
im2 = self.switch_polrep('circ')
elif self.polrep == 'circ':
im2 = self.switch_polrep('stokes')
imvec = np.array(im2._imdict[pol]).reshape(-1) / (10.**power)
except KeyError:
raise Exception("Cannot make pol %s image in display()!" % pol)
unit = fluxunit
if areaunit != '':
unit += ' / ' + areaunit
if np.any(np.imag(imvec)):
print('casting complex image to abs value')
imvec = np.real(imvec)
imvec = imvec * factor
imarr = imvec.reshape(self.ydim, self.xdim)
if scale == 'log':
if (imarr < 0.0).any():
print('clipping values less than 0 in display')
imarr[imarr < 0.0] = 0.0
if log_offset:
imarr = np.log10(imarr + log_offset / dynamic_range)
else:
imarr = np.log10(imarr + np.max(imarr) / dynamic_range)
unit = r'$\log_{10}$(' + unit + ')'
if scale == 'gamma':
if (imarr < 0.0).any():
print('clipping values less than 0 in display')
imarr[imarr < 0.0] = 0.0
imarr = (imarr + np.max(imarr) / dynamic_range)**(gamma)
unit = '(' + unit + ')^' + str(gamma)
if not cbar_lims and cbar_lims_p:
cbar_lims = cbar_lims_p
if cbar_lims:
cbar_lims[0] = cbar_lims[0] / (10.**power)
cbar_lims[1] = cbar_lims[1] / (10.**power)
imarr[imarr > cbar_lims[1]] = cbar_lims[1]
imarr[imarr < cbar_lims[0]] = cbar_lims[0]
if has_title:
plt.title("%s %.2f GHz %s" % (self.source, self.rf / 1e9, pol), fontsize=16)
if not cfun:
cfun = cfun_p
cmap = plt.get_cmap(cfun).copy()
cmap.set_bad(color='whitesmoke')
if cbar_lims:
im = plt.imshow(imarr, alpha=alpha, cmap=cmap, interpolation=interp,
vmin=cbar_lims[0], vmax=cbar_lims[1])
else:
im = plt.imshow(imarr, alpha=alpha, cmap=cmap, interpolation=interp)
if not(beamparams is None or beamparams is False):
if beampos=='left':
beamparams = [beamparams[0], beamparams[1], beamparams[2],
+.4 * self.fovx(), -.4 * self.fovy()]
else:
beamparams = [beamparams[0], beamparams[1], beamparams[2],
-.35 * self.fovx(), -.35 * self.fovy()]
beamimage = self.copy()
beamimage.imvec *= 0
beamimage = beamimage.add_gauss(1, beamparams)
halflevel = 0.5 * np.max(beamimage.imvec)
beamimarr = (beamimage.imvec).reshape(beamimage.ydim, beamimage.xdim)
plt.contour(beamimarr, levels=[halflevel], colors=beamcolor, linewidths=beam_lw)
if has_cbar:
if only_cbar:
im.set_visible(False)
cb = plt.colorbar(im, fraction=0.046, pad=0.04, orientation=cbar_orientation)
cb.set_label(unit, fontsize=float(cbar_fontsize))
if cbar_fontsize != 12:
cb.set_label(unit, fontsize=float(cbar_fontsize) / 1.5)
cb.ax.tick_params(labelsize=cbar_fontsize)
if cbar_lims:
plt.clim(cbar_lims[0], cbar_lims[1])
if scinot:
cb.formatter.set_powerlimits((0, 0))
cb.update_ticks()
# plot polarization with ticks!
else:
im_stokes = self.switch_polrep(polrep_out='stokes')
imvec = np.array(im_stokes.imvec).reshape(-1) / (10**power)
qvec = np.array(im_stokes.qvec).reshape(-1) / (10**power)
uvec = np.array(im_stokes.uvec).reshape(-1) / (10**power)
vvec = np.array(im_stokes.vvec).reshape(-1) / (10**power)
if len(imvec) == 0:
imvec = np.zeros(im_stokes.ydim * im_stokes.xdim)
if len(qvec) == 0:
qvec = np.zeros(im_stokes.ydim * im_stokes.xdim)
if len(uvec) == 0:
uvec = np.zeros(im_stokes.ydim * im_stokes.xdim)
if len(vvec) == 0:
vvec = np.zeros(im_stokes.ydim * im_stokes.xdim)
imvec *= factor
qvec *= factor
uvec *= factor
vvec *= factor
imarr = (imvec).reshape(im_stokes.ydim, im_stokes.xdim)
qarr = (qvec).reshape(im_stokes.ydim, im_stokes.xdim)
uarr = (uvec).reshape(im_stokes.ydim, im_stokes.xdim)
varr = (vvec).reshape(im_stokes.ydim, im_stokes.xdim)
unit = fluxunit
if areaunit != '':
unit = fluxunit + ' / ' + areaunit
# only the stokes I image gets transformed! TODO
imarr2 = imarr.copy()
if scale == 'log':
if (imarr2 < 0.0).any():
print('clipping values less than 0 in display')
imarr2[imarr2 < 0.0] = 0.0
imarr2 = np.log10(imarr2 + np.max(imarr2) / dynamic_range)
unit = r'$\log_{10}$(' + unit + ')'
if scale == 'gamma':
if (imarr2 < 0.0).any():
print('clipping values less than 0 in display')
imarr2[imarr2 < 0.0] = 0.0
imarr2 = (imarr2 + np.max(imarr2) / dynamic_range)**(gamma)
unit = '(' + unit + ')^gamma'
if cbar_lims:
cbar_lims[0] = cbar_lims[0] / (10.**power)
cbar_lims[1] = cbar_lims[1] / (10.**power)
imarr2[imarr2 > cbar_lims[1]] = cbar_lims[1]
imarr2[imarr2 < cbar_lims[0]] = cbar_lims[0]
# polarization ticks
m = (np.abs(qvec + 1j * uvec) / imvec).reshape(self.ydim, self.xdim)
thin = self.xdim // nvec
maska = (imvec).reshape(self.ydim, self.xdim) > pcut * np.max(imvec)
maskb = (np.abs(qvec + 1j * uvec) / imvec).reshape(self.ydim, self.xdim) > mcut
mask = maska * maskb
mask2 = mask[::thin, ::thin]
x = (np.array([[i for i in range(self.xdim)]
for j in range(self.ydim)])[::thin, ::thin])
x = x[mask2]
y = (np.array([[j for i in range(self.xdim)]
for j in range(self.ydim)])[::thin, ::thin])
y = y[mask2]
a = (-np.sin(np.angle(qvec + 1j * uvec) /
2).reshape(self.ydim, self.xdim)[::thin, ::thin])
a = a[mask2]
b = (np.cos(np.angle(qvec + 1j * uvec) /
2).reshape(self.ydim, self.xdim)[::thin, ::thin])
b = b[mask2]
m = (np.abs(qvec + 1j * uvec) / imvec).reshape(self.ydim, self.xdim)
p = (np.abs(qvec + 1j * uvec)).reshape(self.ydim, self.xdim)
m[np.logical_not(mask)] = np.nan
p[np.logical_not(mask)] = np.nan
qarr[np.logical_not(mask)] = np.nan
uarr[np.logical_not(mask)] = np.nan
voi = (vvec / imvec).reshape(self.ydim, self.xdim)
voi[np.logical_not(mask)] = np.nan
if scale_ticks:
pticks = ((np.abs(qvec + 1j * uvec)).reshape(self.ydim, self.xdim))[::thin, ::thin][mask2]
pscale = (pticks - np.min(pticks))/(np.max(pticks) - np.min(pticks))
a *= pscale
b *= pscale
# Little pol plots
if plot_stokes:
maxval = 1.1 * np.max((np.max(np.abs(uarr)),
np.max(np.abs(qarr)), np.max(np.abs(varr))))
# P Plot
ax = plt.subplot2grid((2, 5), (0, 0))
im = plt.imshow(p, cmap=plt.get_cmap('bwr'), interpolation=interp,
vmin=-maxval, vmax=maxval)
plt.contour(imarr, colors='k', linewidths=.25)
ax.set_xticks([])
ax.set_yticks([])
if has_title:
plt.title('P')
if has_cbar:
cbaxes = plt.gcf().add_axes([0.1, 0.2, 0.01, 0.6])
cbar = plt.colorbar(im, fraction=0.046, pad=0.04, cax=cbaxes,
label=unit, orientation='vertical')
cbar.ax.tick_params(labelsize=cbar_fontsize)
cbaxes.yaxis.set_ticks_position('left')
cbaxes.yaxis.set_label_position('left')
if cbar_lims:
plt.clim(-maxval, maxval)
cmap = plt.get_cmap('bwr')
cmap.set_bad('whitesmoke')
# V Plot
ax = plt.subplot2grid((2, 5), (0, 1))
plt.imshow(varr, cmap=cmap, interpolation=interp,
vmin=-maxval, vmax=maxval)
ax.set_xticks([])
ax.set_yticks([])
if has_title:
plt.title('V')
# Q Plot
ax = plt.subplot2grid((2, 5), (1, 0))
plt.imshow(qarr, cmap=cmap, interpolation=interp,
vmin=-maxval, vmax=maxval)
plt.contour(imarr, colors='k', linewidths=.25)
ax.set_xticks([])
ax.set_yticks([])
if has_title:
plt.title('Q')
# U Plot
ax = plt.subplot2grid((2, 5), (1, 1))
plt.imshow(uarr, cmap=cmap, interpolation=interp,
vmin=-maxval, vmax=maxval)
plt.contour(imarr, colors='k', linewidths=.25)
ax.set_xticks([])
ax.set_yticks([])
if has_title:
plt.title('U')
# V/I plot
ax = plt.subplot2grid((2, 5), (0, 2))
cmap = plt.get_cmap('seismic')
cmap.set_bad('whitesmoke')
im = plt.imshow(voi, cmap=cmap, interpolation=interp,
vmin=-1, vmax=1)
if has_title:
plt.title('V/I')
plt.contour(imarr, colors='k', linewidths=.25)
ax.set_xticks([])
ax.set_yticks([])
if has_cbar:
cbaxes = plt.gcf().add_axes([0.125, 0.1, 0.425, 0.01])
cbar = plt.colorbar(im, fraction=0.046, pad=0.04, cax=cbaxes,
label='|m|', orientation='horizontal')
cbar.ax.tick_params(labelsize=cbar_fontsize)
cbaxes.yaxis.set_ticks_position('right')
cbaxes.yaxis.set_label_position('right')
if cbar_lims:
plt.clim(-1, 1)
# m plot
ax = plt.subplot2grid((2, 5), (1, 2))
plt.imshow(m, cmap=plt.get_cmap('seismic'), interpolation=interp, vmin=-1, vmax=1)
ax.set_xticks([])
ax.set_yticks([])
if has_title:
plt.title('m')
plt.contour(imarr, colors='k', linewidths=.25)
plt.quiver(x, y, a, b,
headaxislength=20, headwidth=1, headlength=.01, minlength=0, minshaft=1,
width=.01 * self.xdim, units='x', pivot='mid', color='k', angles='uv',
scale=1.0 / thin)
plt.quiver(x, y, a, b,
headaxislength=20, headwidth=1, headlength=.01, minlength=0, minshaft=1,
width=.005 * self.xdim, units='x', pivot='mid', color='w', angles='uv',
scale=1.1 / thin)
# Big Stokes I plot --axis
ax = plt.subplot2grid((2, 5), (0, 3), rowspan=2, colspan=2)
else:
ax = plt.gca()
if not cfun:
cfun = 'afmhot'
cmap = plt.get_cmap(cfun)
cmap.set_bad(color='whitesmoke')
# Big Stokes I plot
if cbar_lims:
im = plt.imshow(imarr2, cmap=cmap, interpolation=interp,
vmin=cbar_lims[0], vmax=cbar_lims[1])
else:
im = plt.imshow(imarr2, cmap, interpolation=interp)
if vec_cfun is None:
plt.quiver(x, y, a, b,
headaxislength=20, headwidth=1, headlength=.01, minlength=0, minshaft=1,
width=.01 * self.xdim, units='x', pivot='mid', color='k', angles='uv',
scale=1.0 / thin)
plt.quiver(x, y, a, b,
headaxislength=20, headwidth=1, headlength=.01, minlength=0, minshaft=1,
width=.005 * self.xdim, units='x', pivot='mid', color='w', angles='uv',
scale=1.1 / thin)
else:
mthin = (
np.abs(
qvec +
1j *
uvec) /
imvec).reshape(
self.ydim,
self.xdim)[
::thin,
::thin]
mthin = mthin[mask2]
plt.quiver(x, y, a, b,
headaxislength=20, headwidth=1, headlength=.01, minlength=0, minshaft=1,
width=.01 * self.xdim, units='x', pivot='mid', color='w', angles='uv',
scale=1.0 / thin)
plt.quiver(x, y, a, b, mthin,
norm=mpl.colors.Normalize(vmin=0, vmax=1.), cmap=vec_cfun,
headaxislength=20, headwidth=1, headlength=.01, minlength=0, minshaft=1,
width=.007 * self.xdim, units='x', pivot='mid', angles='uv',
scale=1.1 / thin)
if not vec_cbar_lims:
vec_cbar_lims = (0,1)
plt.clim(vec_cbar_lims[0],vec_cbar_lims[1])
mcbar = plt.colorbar(pad=0.04,fraction=0.046, orientation="horizontal")
mcbar.set_label(r'Fractional Linear Polarization $|m|$', fontsize=cbar_fontsize)
mcbar.ax.tick_params(labelsize=cbar_fontsize)
if not(beamparams is None or beamparams is False):
beamparams = [beamparams[0], beamparams[1], beamparams[2],
-.35 * self.fovx(), -.35 * self.fovy()]
beamimage = self.copy()
beamimage.imvec *= 0
beamimage = beamimage.add_gauss(1, beamparams)
halflevel = 0.5 * np.max(beamimage.imvec)
beamimarr = (beamimage.imvec).reshape(beamimage.ydim, beamimage.xdim)
plt.contour(beamimarr, levels=[halflevel], colors=beamcolor, linewidths=beam_lw)
if has_cbar:
cbar = plt.colorbar(im, fraction=0.046, pad=0.04,
label=unit, orientation=cbar_orientation)
cbar.ax.tick_params(labelsize=cbar_fontsize)
if cbar_lims:
plt.clim(cbar_lims[0], cbar_lims[1])
if has_title:
plt.title("%s %.1f GHz : m=%.1f%% , v=%.1f%%" % (self.source, self.rf / 1e9,
self.lin_polfrac() * 100,
self.circ_polfrac() * 100),
fontsize=12)
f.subplots_adjust(hspace=.1, wspace=0.3)
# Label the plot
ax = plt.gca()
if label_type == 'ticks':
xticks = obsh.ticks(self.xdim, self.psize / ehc.RADPERAS / 1e-6)
yticks = obsh.ticks(self.ydim, self.psize / ehc.RADPERAS / 1e-6)
plt.xticks(xticks[0], xticks[1])
plt.yticks(yticks[0], yticks[1])
plt.xlabel(r'Relative RA ($\mu$as)')
plt.ylabel(r'Relative Dec ($\mu$as)')
elif label_type == 'scale':
plt.axis('off')
fov_uas = self.xdim * self.psize / ehc.RADPERUAS # get the fov in uas
roughfactor = 1. / 3. # make the bar about 1/3 the fov
fov_scale = int(math.ceil(fov_uas * roughfactor / 10.0)) * 10
start = self.xdim * roughfactor / 3.0 # select the start location
end = start + fov_scale / fov_uas * self.xdim # determine the end location
plt.plot([start, end], [self.ydim - start - 5, self.ydim - start - 5],
color=scalecolor, lw=scale_lw) # plot a line
plt.text(x=(start + end) / 2.0, y=self.ydim - start + self.ydim / 30,
s=str(fov_scale) + r" $\mu$as", color=scalecolor,
ha="center", va="center", fontsize=scale_fontsize)
ax = plt.gca()
if axis is None:
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
elif label_type == 'none' or label_type is None:
plt.axis('off')
ax = plt.gca()
if axis is None:
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
# Show or save to file
if axis is not None:
return axis
if show:
#plt.show(block=False)
ehc.show_noblock()
if export_pdf != "":
f.savefig(export_pdf, bbox_inches='tight', pad_inches=pdf_pad_inches, dpi=dpi)
return f
[docs] def overlay_display(self, im_list, color_coding=np.array([[1, 0, 1], [0, 1, 0]]),
export_pdf="", show=True, f=False,
shift=[0, 0], final_fov=False, interp='gaussian',
scale='lin', gamma=0.5, dynamic_range=[1.e3], rescale=True):
"""Overlay primary polarization images of a list of images to compare structures.
Args:
im_list (list): list of images to align to the current image
color_coding (numpy.array): Color coding of each image in the composite
f (matplotlib.pyplot.figure): Figure to overlay on top of
export_pdf (str): path to exported PDF with plot
show (bool): Display the plot if true
shift (list): list of manual image shifts,
otherwise use the shift from maximum cross-correlation
final_fov (float): fov of the comparison image (rad).
If False it is the largestinput image fov
scale (str) : compare images in 'log','lin',or 'gamma' scale
gamma (float): exponent for gamma scale comparison
dynamic_range (float): dynamic range for log and gamma scale comparisons
Returns:
(matplotlib.figure.Figure): figure object with image
"""
if not f:
f = plt.figure()
plt.clf()
if len(dynamic_range) == 1:
dynamic_range = dynamic_range * np.ones(len(im_list) + 1)
if not isinstance(shift, np.ndarray) and not isinstance(shift, bool):
shift = matlib.repmat(shift, len(im_list), 1)
psize = self.psize
max_fov = np.max([self.xdim * self.psize, self.ydim * self.psize])
for i in range(0, len(im_list)):
psize = np.min([psize, im_list[i].psize])
max_fov = np.max([max_fov, im_list[i].xdim * im_list[i].psize,
im_list[i].ydim * im_list[i].psize])
if not final_fov:
final_fov = max_fov
(im_list_shift, shifts, im0_pad) = self.align_images(im_list, shift=shift,
final_fov=final_fov,
scale=scale, gamma=gamma,
dynamic_range=dynamic_range)
# unit = 'Jy/pixel'
if scale == 'log':
# unit = 'log(Jy/pixel)'
log_offset = np.max(im0_pad.imvec) / dynamic_range[0]
im0_pad.imvec = np.log10(im0_pad.imvec + log_offset)
for i in range(0, len(im_list)):
log_offset = np.max(im_list_shift[i].imvec) / dynamic_range[i + 1]
im_list_shift[i].imvec = np.log10(im_list_shift[i].imvec + log_offset)
if scale == 'gamma':
# unit = '(Jy/pixel)^gamma'
log_offset = np.max(im0_pad.imvec) / dynamic_range[0]
im0_pad.imvec = (im0_pad.imvec + log_offset)**(gamma)
for i in range(0, len(im_list)):
log_offset = np.max(im_list_shift[i].imvec) / dynamic_range[i + 1]
im_list_shift[i].imvec = (im_list_shift[i].imvec + log_offset)**(gamma)
composite_img = np.zeros((im0_pad.ydim, im0_pad.xdim, 3))
for i in range(-1, len(im_list)):
if i == -1:
immtx = im0_pad.imvec.reshape(im0_pad.ydim, im0_pad.xdim)
else:
immtx = im_list_shift[i].imvec.reshape(im0_pad.ydim, im0_pad.xdim)
if rescale:
immtx = immtx - np.min(np.min(immtx))
immtx = immtx / np.max(np.max(immtx))
for c in range(0, 3):
composite_img[:, :, c] = composite_img[:, :, c] + (color_coding[i + 1, c] * immtx)
if rescale is False:
composite_img = composite_img - np.min(np.min(np.min(composite_img)))
composite_img = composite_img / np.max(np.max(np.max(composite_img)))
plt.subplot(111)
plt.title('%s MJD %i %.2f GHz' % (self.source, self.mjd, self.rf / 1e9), fontsize=20)
plt.imshow(composite_img, interpolation=interp)
xticks = obsh.ticks(im0_pad.xdim, im0_pad.psize / ehc.RADPERAS / 1e-6)
yticks = obsh.ticks(im0_pad.ydim, im0_pad.psize / ehc.RADPERAS / 1e-6)
plt.xticks(xticks[0], xticks[1])
plt.yticks(yticks[0], yticks[1])
plt.xlabel(r'Relative RA ($\mu$as)')
plt.ylabel(r'Relative Dec ($\mu$as)')
if show:
#plt.show(block=False)
ehc.show_noblock()
if export_pdf != "":
f.savefig(export_pdf, bbox_inches='tight')
return (f, shift)
[docs] def save_txt(self, fname):
"""Save image data to text file.
Args:
fname (str): path to output text file
Returns:
"""
ehtim.io.save.save_im_txt(self, fname)
return
[docs] def save_fits(self, fname):
"""Save image data to a fits file.
Args:
fname (str): path to output fits file
Returns:
"""
ehtim.io.save.save_im_fits(self, fname)
return
###################################################################################################
# Image creation functions
###################################################################################################
[docs]def make_square(obs, npix, fov, pulse=ehc.PULSE_DEFAULT, polrep='stokes', pol_prim=None):
"""Make an empty square image.
Args:
obs (Obsdata): an obsdata object with the image metadata
npix (int): the pixel size of each axis
fov (float): the field of view of each axis in radians
pulse (function): the function convolved with the pixel values for continuous image
polrep (str): polarization representation, either 'stokes' or 'circ'
pol_prim (str): The default image: I,Q,U or V for Stokes, or RR,LL,LR,RL for Circular
Returns:
(Image): an image object
"""
outim = make_empty(npix, fov, obs.ra, obs.dec, rf=obs.rf, source=obs.source,
polrep=polrep, pol_prim=pol_prim, pulse=pulse,
mjd=obs.mjd, time=obs.tstart)
return outim
[docs]def make_empty(npix, fov, ra, dec, rf=ehc.RF_DEFAULT, source=ehc.SOURCE_DEFAULT,
polrep='stokes', pol_prim=None, pulse=ehc.PULSE_DEFAULT,
mjd=ehc.MJD_DEFAULT, time=0.):
"""Make an empty square image.
Args:
npix (int): the pixel size of each axis
fov (float): the field of view of each axis in radians
ra (float): The source Right Ascension in fractional hours
dec (float): The source declination in fractional degrees
rf (float): The image frequency in Hz
source (str): The source name
polrep (str): polarization representation, either 'stokes' or 'circ'
pol_prim (str): The default image: I,Q,U or V for Stokes, RR,LL,LR,RL for Circular
pulse (function): The function convolved with the pixel values for continuous image.
mjd (int): The integer MJD of the image
time (float): The observing time of the image (UTC hours)
Returns:
(Image): an image object
"""
pdim = fov / float(npix)
npix = int(npix)
imarr = np.zeros((npix, npix))
outim = Image(imarr, pdim, ra, dec,
polrep=polrep, pol_prim=pol_prim,
rf=rf, source=source, mjd=mjd, time=time, pulse=pulse)
return outim
[docs]def load_image(image, display=False, aipscc=False):
"""Read in an image from a text, .fits, .h5, or ehtim.image.Image object
Args:
image (str/Image): path to input file
display (boolean): determine whether to display the image default
aipscc (boolean): if True, then AIPS CC table will be loaded instead
of the original brightness distribution.
Returns:
(Image): loaded image object
(boolean): False if the image cannot be read
"""
is_unicode = False
try:
if isinstance(image, basestring):
is_unicode = True
except NameError: # python 3
pass
if isinstance(image, str) or is_unicode:
if image.endswith('.fits'):
im = ehtim.io.load.load_im_fits(image, aipscc=aipscc)
elif image.endswith('.txt'):
im = ehtim.io.load.load_im_txt(image)
elif image.endswith('.h5'):
im = ehtim.io.load.load_im_hdf5(image)
else:
print("Image format is not recognized. Was expecting .fits, .txt, or Image.")
print(" Got <.{0}>. Returning False.".format(image.split('.')[-1]))
return False
elif isinstance(image, ehtim.image.Image):
im = image
else:
print("Image format is not recognized. Was expecting .fits, .txt, or Image.")
print(" Got {0}. Returning False.".format(type(image)))
return False
if display:
im.display()
return im
[docs]def load_txt(fname, polrep='stokes', pol_prim=None, pulse=ehc.PULSE_DEFAULT, zero_pol=True):
"""Read in an image from a text file.
Args:
fname (str): path to input text file
pulse (function): The function convolved with the pixel values for continuous image.
polrep (str): polarization representation, either 'stokes' or 'circ'
pol_prim (str): The default image: I,Q,U or V for Stokes, RR,LL,LR,RL for Circular
zero_pol (bool): If True, loads any missing polarizations as zeros
Returns:
(Image): loaded image object
"""
return ehtim.io.load.load_im_txt(fname, pulse=pulse, polrep=polrep,
pol_prim=pol_prim, zero_pol=True)
[docs]def load_fits(fname, aipscc=False, pulse=ehc.PULSE_DEFAULT,
polrep='stokes', pol_prim=None, zero_pol=False):
"""Read in an image from a FITS file.
Args:
fname (str): path to input fits file
aipscc (bool): if True, then AIPS CC table will be loaded
pulse (function): The function convolved with the pixel values for continuous image.
polrep (str): polarization representation, either 'stokes' or 'circ'
pol_prim (str): The default image: I,Q,U or V for Stokes, RR,LL,LR,RL for Circular
zero_pol (bool): If True, loads any missing polarizations as zeros
Returns:
(Image): loaded image object
"""
return ehtim.io.load.load_im_fits(fname, aipscc=aipscc, pulse=pulse,
polrep=polrep, pol_prim=pol_prim, zero_pol=zero_pol)
[docs]def avg_imlist(imlist):
"""Average a list of images.
Args:
imlist (list): list of image objects
Returns:
(Image): average image object
"""
imavg = imlist[0]
if np.any(np.array([im.polrep for im in imlist]) != imavg.polrep):
raise Exception("im.polrep in all images are not the same in avg_imlist!")
if np.any(np.array([im.source for im in imlist]) != imavg.source):
raise Exception("im.source in all images are not the same in avg_imlist!")
if np.any(np.array([im.rf for im in imlist]) != imavg.rf):
raise Exception("im.rf in all images are not the same in avg_imlist!")
keys = imavg._imdict.keys()
for im in imlist[1:]:
for key in keys:
imavg._imdict[key] += im._imdict[key]
for key in keys:
imavg._imdict[key] /= float(len(imlist))
return imavg
[docs]def get_specim(imlist, reffreq, fit_order=2):
"""get the spectral index/curvature from a list of images"""
freqs = [im.rf for im in imlist]
# remove any zeros in the images
for im in imlist:
im.imvec[im.imvec<=0] = np.min(im.imvec[im.imvec!=0])
# fit
xfit = np.log(np.array(freqs)/reffreq)
yfit = np.log(np.array([im.imvec for im in imlist]))
if fit_order == 2:
coeffs = np.polyfit(xfit,yfit,2)
beta = coeffs[0]
alpha = coeffs[1]
imvec = np.exp(coeffs[2])
elif fit_order == 1:
coeffs = np.polyfit(xfit,yfit,1)
alpha = coeffs[0]
beta = 0*alpha
imvec = np.exp(coeffs[1])
else:
raise Exception()
outim = imlist[0].copy() #TODO no polarization
outim.imvec = imvec
outim.rf = reffreq
outim.specvec = alpha
outim.curvvec = beta
return outim
[docs]def blur_mf(im,freqs,kernel,fit_order=2):
"""blur multifrequncy images with the same beam"""
reffreq = im.rf
# remove any zeros in the images
imlist = [im.get_image_mf(rf).blur_circ(kernel) for rf in freqs]
for image in imlist:
image.imvec[image.imvec<=0] = np.min(image.imvec[image.imvec!=0])
xfit = np.log(np.array(freqs)/reffreq)
yfit = np.log(np.array([im.imvec for im in imlist]))
if fit_order == 2:
coeffs = np.polyfit(xfit,yfit,2)
beta = coeffs[0]
alpha = coeffs[1]
elif fit_order == 1:
coeffs = np.polyfit(xfit,yfit,1)
alpha = coeffs[0]
beta = 0*alpha
else:
alpha = 0*yfit
beta = 0*yfit
outim = im.blur_circ(kernel)
outim.specvec = alpha
outim.curvvec = beta
return outim