# geodesy and pointing and other helper functions that are useful # to be available outside of the simdata task # geodesy from NGS: http://www.ngs.noaa.gov/TOOLS/program_descriptions.html from __future__ import absolute_import import os import shutil import pylab as pl import glob import re from scipy.stats import scoreatpercentile import scipy.special as spspec import scipy.signal as spsig import scipy.interpolate as spintrp from collections import OrderedDict from casatasks.private.casa_transition import is_CASA6 if is_CASA6: from casatools import table, image, imagepol, regionmanager, calibrater, measures, quanta, coordsys, componentlist, simulator, synthesisutils from casatasks import casalog, tclean from casatasks.private.cleanhelper import cleanhelper tb = table( ) ia = image( ) po = imagepol( ) rg = regionmanager( ) cb = calibrater( ) me = measures( ) qa = quanta( ) cs = coordsys( ) cl = componentlist( ) sm = simulator( ) _su = synthesisutils( ) else: #import casac # all I really need is casalog, but how to get it:? from taskinit import * from tclean import tclean # qa doesn't hold state. #qatool = casac.homefinder.find_home_by_name('quantaHome') #qa = qatool.create() im,cb,ms,tb,me,ia,po,sm,cl,cs,rg,sl,dc,vp,msmd,fi,fn,imd,sdms=gentools(['im','cb','ms','tb','me','ia','po','sm','cl','cs','rg','sl','dc','vp','msmd','fi','fn','imd','sdms']) _su = casac.synthesisutils( ) # 4.2.2: #im, cb, ms, tb, fl, me, ia, po, sm, cl, cs, rg, sl, dc, vp, msmd, fi, fn, imd = gentools() # functions defined outside of the simutil class def is_array_type(value): array_type = [list, tuple, pl.ndarray] if type(value) in array_type: return True else: return False # the method which returns a string of task function call with parameter def get_taskstr(taskname, params): """ Returns a task call string. Arguments taskname : task name string params : a dictionary of parameter (key) and value pairs. Example get_taskstr('mytask', {'vis': 'foo.ms', 'factor': 2.0}) returns a string, 'mytask(vis='foo.ms', factor=2.0)' """ out = ("%s(" % taskname) sep = ", " for key, val in params.items(): out += (key + "=" + __get_str(val) + sep) return ( out.rstrip(sep) + ")" ) def __get_str(paramval): if type(paramval) == str: return ("'%s'" % paramval) # else return str(paramval) class compositenumber: def __init__(self, maxval=100): self.generate(maxval) def generate(self,maxval): n2 = int(log(float(maxval))/log(2.0) + 1) +1 n3 = int(log(float(maxval))/log(3.0) + 1) +1 n5 = int(log(float(maxval))/log(5.0) + 1) +1 itsnumbers=pl.zeros(n2*n3*n5) n = 0 for i2 in range(n2): for i3 in range(n3): for i5 in range(n5): composite=( 2.**i2 * 3.**i3 * 5.**i5 ) itsnumbers[n] = composite #casalog.post... i2,i3,i5,composite n=n+1 itsnumbers.sort() maxi=0 while maxi<(n2*n3*n5) and itsnumbers[maxi]<=maxval: maxi=maxi+1 self.itsnumbers=pl.int64(itsnumbers[0:maxi]) def list(self): casalog.post(self.itsnumbers) def nextlarger(self,x): if x>max(self.itsnumbers): self.generate(2*x) xi=0 n=self.itsnumbers.__len__() while xi<n and self.itsnumbers[xi]<x: xi=xi+1 return self.itsnumbers[xi] class simutil: """ simutil contains methods to facilitate simulation. To use these, create a simutil instance e.g., in CASA 6: CASA> from casatasks.private import simutil CASA> u=simutil.simutil() CASA> x,y,z,d,padnames,antnames,telescope,posobs = u.readantenna("myconfig.cfg") (in CASA5: ) CASA> from simutil import simutil CASA> u=simutil.simutil() """ def __init__(self, direction="", centerfreq=qa.quantity("245GHz"), bandwidth=qa.quantity("1GHz"), totaltime=qa.quantity("0h"), verbose=False): self.direction=direction self.verbose=verbose self.centerfreq=centerfreq self.bandwidth=bandwidth self.totaltime=totaltime self.pmulti=0 # rows, cols, currsubplot def newfig(self,multi=0,show=True): # new graphics window/file if show: pl.ion() # creates a fig if ness else: pl.ioff() pl.clf() if multi!=0: if type(multi)!=type([]): self.msg("internal error setting multi-panel figure with multi="+str(multi),priority="warn") if len(multi)!=3: self.msg("internal error setting multi-panel figure with multi="+str(multi),priority="warn") self.pmulti=multi pl.subplot(multi[0],multi[1],multi[2]) pl.subplots_adjust(left=0.05,right=0.98,bottom=0.09,top=0.95,hspace=0.2,wspace=0.2) ########################################################### def nextfig(self): # advance subwindow ax=pl.gca() l=ax.get_xticklabels() pl.setp(l,fontsize="x-small") l=ax.get_yticklabels() pl.setp(l,fontsize="x-small") if self.pmulti!=0: self.pmulti[2] += 1 multi=self.pmulti if multi[2] <= multi[0]*multi[1]: pl.subplot(multi[0],multi[1],multi[2]) # consider pl.draw() here - may be slow ########################################################### def endfig(self,show=True,filename=""): # set margins to smaller, save to file if required ax=pl.gca() l=ax.get_xticklabels() pl.setp(l,fontsize="x-small") l=ax.get_yticklabels() pl.setp(l,fontsize="x-small") if show: pl.draw() if len(filename)>0: pl.savefig(filename) pl.ioff() ########################################################### def msg(self, s, origin=None, priority=None): # everything goes to logger with priority=priority # priority error: raise an exception, # casa 5.0.0: default is now no term unless toterm=True # priority warn: change color to magenta, send to terminal # priority info: change color to green, send to terminal # priority none: not normally to terminal unless verbose=True # ansi color codes: # Foreground colors # 30 Black # 31 Red # 32 Green # 33 Yellow # 34 Blue # 35 Magenta # 36 Cyan # 37 White clr="" if self.verbose: toterm=True else: toterm=False if priority==None: priority="INFO" else: priority=priority.upper() #toterm=True if priority=="INFO": clr="\x1b[32m" elif priority.count("WARN")>0: clr="\x1b[35m" #toterm=True if self.verbose: priority="INFO" # otherwise casalog will spew to term also. elif priority=="ERROR": clr="\x1b[31m" toterm=False # casalog spews severe to term already else: if not (priority=="DEBUG" or priority[:-1]=="DEBUG"): priority="INFO" bw="\x1b[0m" if toterm: if self.isreport(): if origin: self.report.write("["+origin+"] "+s+"\n") else: self.report.write(s+"\n") if s.count("ERROR"): foo=s.split("ERROR") s=foo[0]+"\x1b[31mERROR\x1b[0m"+foo[1] if s.count("WARNING"): foo=s.split("WARNING") s=foo[0]+"\x1b[35mWARNING\x1b[0m"+foo[1] if origin: casalog.post(clr+"["+origin+"] "+bw+s) else: casalog.post(s) if priority=="ERROR": raise Exception(s) else: if origin==None: origin="simutil" casalog.post(s,priority=priority,origin=origin) ########################################################### def isreport(self): # is there an open report file? try: if self.report.name == self.reportfile: return True else: return False except: return False def openreport(self): try: if os.path.exists(self.reportfile): self.report=open(self.reportfile,"a") #self.msg("Report file "+self.reportfile+"already exists - delete or change reportfile",priority="ERROR",origin="simutil") else: self.report=open(self.reportfile,"w") except: self.msg("Can't open reportfile because it's not defined",priority="ERROR",origin="simutil") def closereport(self): self.report.close() ########################################################### def isquantity(self,s,halt=True): if type(s)!=type([]): t=[s] else: t=s for t0 in t: if not (len(t0)>0 and qa.isquantity(t0)): if halt: self.msg("can't interpret '"+str(t0)+"' as a CASA quantity",priority="error") return False return True ########################################################### def isdirection(self,s,halt=True): if type(s)==type([]): t=s[0] else: t=s try: x=self.direction_splitter(s) y=me.direction(x[0],x[1],x[2]) except: if halt: self.msg("can't interpret '"+str(s)+"' as a direction",priority="error") return False if not me.measure(y,y['refer']): if halt: self.msg("can't interpret '"+str(s)+"' as a direction",priority="error") return False return True ########################################################### def ismstp(self,s,halt=False): try: istp = False # check if the ms is tp data or not. tb.open(s+'/ANTENNA') antname = tb.getcol('NAME') tb.close() if antname[0].find('TP') > -1: istp = True elif len(antname) == 1: istp = True else: # need complete testing of UVW tb.open(s) uvw = tb.getcol("UVW") tb.close() if uvw.all() == 0.: istp = True except: if halt: self.msg("can't understand the file '"+str(s)+"'",priority="error") return False if not istp: if halt: self.msg("input file '"+str(s)+"' is not a totalpower ms",priority="error") return False return True ########################################################### # plot an image (optionally), and calculate its statistics def statim(self,image,plot=True,incell=None,disprange=None,bar=True,showstats=True): pix=self.cellsize(image) # cell positive by convention pixarea=abs(qa.convert(pix[0],'arcsec')['value']* qa.convert(pix[1],'arcsec')['value']) ia.open(image) imunit=ia.brightnessunit() if imunit == 'Jy/beam': bm=ia.restoringbeam() if len(bm)>0: toJyarcsec=1./pixarea else: toJyarcsec=1. toJypix=toJyarcsec*pixarea elif imunit == 'Jy/pixel': toJyarcsec=1./pixarea toJypix=1. else: self.msg("%s: unknown units" % image,origin="statim") toJyarcsec=1. toJypix=1. stats=ia.statistics(robust=True,verbose=False,list=False) im_min=stats['min']*toJypix plarr=pl.zeros(1) badim=False if type(im_min)==type([]) or type(im_min)==type(plarr): if len(im_min)<1: badim=True im_min=0. im_max=stats['max']*toJypix if type(im_max)==type([]) or type(im_max)==type(plarr): if len(im_max)<1: badim=True im_max=1. imsize=ia.shape()[0:2] reg1=rg.box([0,0],[imsize[0]*.25,imsize[1]*.25]) stats=ia.statistics(region=reg1,verbose=False,list=False) im_rms=stats['rms']*toJypix if type(im_rms)==type([]) or type(im_rms)==type(plarr): if len(im_rms)==0: badim=True im_rms=0. data_array=ia.getchunk([-1,-1,1,1],[-1,-1,1,1],[1],[],True,True,False) data_array=pl.array(data_array) tdata_array=pl.transpose(data_array) ttrans_array=tdata_array.tolist() ttrans_array.reverse() # get and apply mask mask_array=ia.getchunk([-1,-1,1,1],[-1,-1,1,1],[1],[],True,True,True) mask_array=pl.array(mask_array) tmask_array=pl.transpose(mask_array) tmtrans_array=tmask_array.tolist() tmtrans_array.reverse() ttrans_array=pl.array(ttrans_array) z=pl.where(pl.array(tmtrans_array)==False) ttrans_array[z[0],z[1]]=0. if (plot): pixsize=[qa.convert(pix[0],'arcsec')['value'],qa.convert(pix[1],'arcsec')['value']] xextent=imsize[0]*abs(pixsize[0])*0.5 yextent=imsize[1]*abs(pixsize[1])*0.5 if self.verbose: self.msg("plotting %fx%f\" im with %fx%f\" pix" % (xextent,yextent,pixsize[0],pixsize[1]),origin="statim") xextent=[xextent,-xextent] yextent=[-yextent,yextent] # remove top .5% of pixels: nbin=200 if badim: highvalue=im_max lowvalue=im_min else: #imhist=ia.histograms(cumu=True,nbins=nbin,list=False)#['histout'] imhist=ia.histograms(cumu=True,nbins=nbin)#['histout'] ii=0 lowcounts=imhist['counts'][ii] while imhist['counts'][ii]<0.005*lowcounts and ii<nbin: ii=ii+1 lowvalue=imhist['values'][ii] ii=nbin-1 highcounts=imhist['counts'][ii] while imhist['counts'][ii]>0.995*highcounts and ii>0 and 0.995*highcounts>lowcounts: ii=ii-1 highvalue=imhist['values'][ii] if disprange != None: if type(disprange)==type([]): if len(disprange)>0: highvalue=disprange[-1] if len(disprange)>1: lowvalue=disprange[0] if len(disprange)>2: throw("internal error disprange="+str(disprange)+" has too many elements") else: # if passed an empty list [], return low.high disprange.append(lowvalue) disprange.append(highvalue) else: highvalue=disprange # assume if scalar passed its the max if plot: img=pl.imshow(ttrans_array,interpolation='bilinear',cmap=pl.cm.jet,extent=xextent+yextent,vmax=highvalue,vmin=lowvalue) ax=pl.gca() #l=ax.get_xticklabels() #pl.setp(l,fontsize="x-small") #l=ax.get_yticklabels() #pl.setp(l,fontsize="x-small") foo=image.split("/") if len(foo)==1: imagestrip=image else: imagestrip=foo[1] pl.title(imagestrip,fontsize="x-small") if showstats: pl.text(0.05,0.95,"min=%7.1e\nmax=%7.1e\nRMS=%7.1e\n%s" % (im_min,im_max,im_rms,imunit),transform = ax.transAxes,bbox=dict(facecolor='white', alpha=0.7),size="x-small",verticalalignment="top") if bar: cb=pl.colorbar(pad=0) cl = pl.getp(cb.ax,'yticklabels') pl.setp(cl,fontsize='x-small') ia.done() return im_min,im_max,im_rms,imunit ########################################################### def calc_pointings2(self, spacing, size, maptype="hex", direction=None, relmargin=0.5, beam=0.): """ If direction is a list, simply returns direction and the number of pointings in it. Otherwise, returns a hexagonally packed list of pointings separated by spacing and fitting inside an area specified by direction and mapsize, as well as the number of pointings. The hexagonal packing starts with a horizontal row centered on direction, and the other rows alternate being horizontally offset by a half spacing. """ # make size 2-dimensional and ensure it is quantity if type(size) != type([]): size=[size,size] if len(size) <2: size=[size[0],size[0]] self.isquantity(size) # parse and check direction if direction==None: # if no direction is specified, use the object's direction direction=self.direction else: # if one is specified, use it to set the object's direction self.direction=direction self.isdirection(direction) # direction is always a list of strings (defined by .xml) if type(direction)==type([]): if len(direction) > 1: if self.verbose: self.msg("you are inputing the precise pointings in 'direction' - if you want to calculate a mosaic, give a single direction string and set maptype",priority="warn") return direction else: direction=direction[0] # haveing eliminated other options, we need to calculate: epoch, centx, centy = self.direction_splitter() pointings = [] shorttype=str.upper(maptype[0:3]) # if not shorttype=="HEX": # self.msg("can't calculate map of maptype "+maptype,priority="error") if shorttype == "HEX": # this is hexagonal grid - Kana will add other types here self.isquantity(spacing) spacing = qa.quantity(spacing) yspacing = qa.mul(0.866025404, spacing) xsize=qa.quantity(size[0]) ysize=qa.quantity(size[1]) nrows = 1+ int(pl.floor(qa.convert(qa.div(ysize, yspacing), '')['value'] - 2.309401077 * relmargin)) availcols = 1 + qa.convert(qa.div(xsize, spacing), '')['value'] - 2.0 * relmargin ncols = int(pl.floor(availcols)) # By making the even rows shifted spacing/2 ahead, and possibly shorter, # the top and bottom rows (nrows odd), are guaranteed to be short. if availcols - ncols >= 0.5 and nrows>1: # O O O evencols = ncols # O O O ncolstomin = 0.5 * (ncols - 0.5) else: evencols = ncols - 1 # O O ncolstomin = 0.5 * (ncols - 1) # O O O pointings = [] # Start from the top because in the Southern hemisphere it sets first. y = qa.add(centy, qa.mul(0.5 * (nrows - 1), yspacing)) for row in range(0, nrows): # xrange stops early. xspacing = qa.mul(1.0 / pl.cos(qa.convert(y, 'rad')['value']),spacing) ystr = qa.formxxx(y, format='dms',prec=5) if row % 2: # Odd xmin = qa.sub(centx, qa.mul(ncolstomin, xspacing)) stopcolp1 = ncols else: # Even (including 0) xmin = qa.sub(centx, qa.mul(ncolstomin - 0.5, xspacing)) stopcolp1 = evencols for col in range(0, stopcolp1): # xrange stops early. x = qa.formxxx(qa.add(xmin, qa.mul(col, xspacing)), format='hms',prec=5) pointings.append("%s%s %s" % (epoch, x, ystr)) y = qa.sub(y, yspacing) elif shorttype =="SQU": # lattice gridding self.isquantity(spacing) spacing = qa.quantity(spacing) yspacing = spacing xsize=qa.quantity(size[0]) ysize=qa.quantity(size[1]) nrows = 1+ int(pl.floor(qa.convert(qa.div(ysize, yspacing), '')['value'] - 2.0 * relmargin)) availcols = 1 + qa.convert(qa.div(xsize, spacing), '')['value'] \ - 2.0 * relmargin ncols = int(pl.floor(availcols)) ncolstomin = 0.5 * (ncols - 1) # O O O O pointings = [] # O O O O # Start from the top because in the Southern hemisphere it sets first. y = qa.add(centy, qa.mul(0.5 * (nrows - 1), yspacing)) for row in range(0, nrows): # xrange stops early. xspacing = qa.mul(1.0 / pl.cos(qa.convert(y, 'rad')['value']),spacing) ystr = qa.formxxx(y, format='dms',prec=5) xmin = qa.sub(centx, qa.mul(ncolstomin, xspacing)) stopcolp1 = ncols for col in range(0, stopcolp1): # xrange stops early. x = qa.formxxx(qa.add(xmin, qa.mul(col, xspacing)), format='hms',prec=5) pointings.append("%s%s %s" % (epoch, x, ystr)) y = qa.sub(y, yspacing) if shorttype == "ALM": # OT algorithm self.isquantity(spacing) spacing = qa.quantity(spacing) xsize = qa.quantity(size[0]) ysize = qa.quantity(size[1]) spacing_asec=qa.convert(spacing,'arcsec')['value'] xsize_asec=qa.convert(xsize,'arcsec')['value'] ysize_asec=qa.convert(ysize,'arcsec')['value'] angle = 0. # deg if str.upper(maptype[0:8]) == 'ALMA2012': x,y = self.getTrianglePoints(xsize_asec, ysize_asec, angle, spacing_asec) else: if beam<=0: beam=spacing_asec*pbcoeff*pl.sqrt(3) # ASSUMES ALMA default and arcsec x,y = self.getTriangularTiling(xsize_asec, ysize_asec, angle, spacing_asec, beam) pointings = [] nx=len(x) for i in range(nx): # Start from the top because in the Southern hemisphere it sets first. y1=qa.add(centy, str(y[nx-i-1])+"arcsec") ycos=pl.cos(qa.convert(y1,"rad")['value']) ystr = qa.formxxx(y1, format='dms',prec=5) xstr = qa.formxxx(qa.add(centx, str(x[nx-i-1]/ycos)+"arcsec"), format='hms',prec=5) pointings.append("%s%s %s" % (epoch, xstr, ystr)) # if could not fit any pointings, then return single pointing if(len(pointings)==0): pointings.append(direction) self.msg("using %i generated pointing(s)" % len(pointings),origin='calc_pointings') self.pointings=pointings return pointings ########################################################### def read_pointings(self, filename): """ read pointing list from file containing epoch, ra, dec, and scan time (optional,in sec). Parameter: filename: (str) the name of input file The input file (ASCII) should contain at least 3 fields separated by a space which specify positions with epoch, ra and dec (in dms or hms). The optional field, time, shoud be a list of decimal numbers which specifies integration time at each position in second. The lines which start with '#' is ignored and can be used as comment lines. Example of an input file: #Epoch RA DEC TIME(optional) J2000 23h59m28.10 -019d52m12.35 10.0 J2000 23h59m32.35 -019d52m12.35 10.0 J2000 23h59m36.61 -019d52m12.35 60.0 """ f=open(filename) line= ' ' time=[] pointings=[] # add option of different epoch in a header line like read_antenna? while (len(line)>0): try: line=f.readline() if (not line.startswith('#')) and (not line.startswith("RA")) and (not line.startswith("--")): ### it could be the new OT format if (line.find('SEXAGESIMAL')>0): splitline = line.split(',') if len(splitline)>4: time.append(float(splitline[4])) else: time.append(0.) xstr = qa.formxxx(qa.quantity(splitline[0]), format='hms',prec=5) de0=splitline[1] # casa insists that strings with : are RA, so... if de0.count(":")>0: ystr = qa.formxxx(qa.div(qa.quantity(de0),15), format='dms',prec=5) else: ystr = qa.formxxx(qa.quantity(de0), format='dms',prec=5) # ASSUME ICRS pointings.append("ICRS %s %s" % (xstr,ystr)) ### ignoring line that has less than 3 elements elif(len(line.split()) >2): splitline=line.split() epoch=splitline[0] ra0=splitline[1] de0=splitline[2] if len(splitline)>3: time.append(float(splitline[3])) else: time.append(0.) xstr = qa.formxxx(qa.quantity(ra0), format='hms',prec=5) ystr = qa.formxxx(qa.quantity(de0), format='dms',prec=5) pointings.append("%s %s %s" % (epoch,xstr,ystr)) except: break f.close() # need an error check here if zero valid pointings were read if len(pointings) < 1: s="No valid lines found in pointing file" self.msg(s,priority="error") self.msg("read in %i pointing(s) from file" % len(pointings),origin="read_pointings") self.pointings=pointings #self.direction=pointings return len(pointings), pointings, time ########################################################### def write_pointings(self, filename,pointings,time=1.): """ write pointing list to file containing epoch, ra, dec, and scan time (optional,in sec). Example of an output file: #Epoch RA DEC TIME(optional) J2000 23h59m28.10 -019d52m12.35 10.0 J2000 23h59m32.35 -019d52m12.35 10.0 J2000 23h59m36.61 -019d52m12.35 60.0 """ f=open(filename,'w') f.write('#Epoch RA DEC TIME[sec]\n') if type(pointings)!=type([]): pointings=[pointings] npos=len(pointings) if type(time)!=type([]): time=[time] if len(time)==1: time=list(time[0] for x in range(npos)) for i in range(npos): #epoch,ra,dec=direction_splitter(pointings[i]) #xstr = qa.formxxx(qa.quantity(ra), format='hms') #ystr = qa.formxxx(qa.quantity(dec), format='dms') #line = "%s %s %s" % (epoch,xstr,ystr) #self.isdirection(line) # extra check #f.write(line+" "+str(time[i])+"\n") f.write(pointings[i]+" "+str(time[i])+"\n") f.close() return ########################################################### def average_direction(self, directions=None): # RI TODO make deal with list of measures as well as list of strings """ Returns the average of directions as a string, and relative offsets """ if directions==None: directions=self.direction epoch0, x, y = self.direction_splitter(directions[0]) i = 1 avgx = 0.0 avgy = 0.0 for drn in directions: epoch, x, y = self.direction_splitter(drn) # in principle direction_splitter returns directions in degrees, # but can we be sure? x=qa.convert(x,'deg') y=qa.convert(y,'deg') x = x['value'] y = y['value'] if epoch != epoch0: # Paranoia casalog.post("[simutil] WARN: precession not handled by average_direction()", 'WARN') x = self.wrapang(x, avgx, 360.0) avgx += (x - avgx) / i avgy += (y - avgy) / i i += 1 offsets=pl.zeros([2,i-1]) i=0 cosdec=pl.cos(avgy*pl.pi/180.) for drn in directions: epoch, x, y = self.direction_splitter(drn) x=qa.convert(x,'deg') y=qa.convert(y,'deg') x = x['value'] y = y['value'] x = self.wrapang(x, avgx, 360.0) offsets[:,i]=[(x-avgx)*cosdec,y-avgy] # apply cosdec to make offsets on sky i+=1 avgx = qa.toangle('%17.12fdeg' % avgx) avgy = qa.toangle('%17.12fdeg' % avgy) avgx = qa.formxxx(avgx, format='hms',prec=5) avgy = qa.formxxx(avgy, format='dms',prec=5) return "%s%s %s" % (epoch0, avgx, avgy), offsets ########################################################### def median_direction(self, directions=None): # RI TODO make deal with list of measures as well as list of strings """ Returns the median of directions as a string, and relative offsets """ if directions==None: directions=self.direction epoch0, x, y = self.direction_splitter(directions[0]) i = 1 avgx = 0.0 avgy = 0.0 xx=[] yy=[] for drn in directions: epoch, x, y = self.direction_splitter(drn) # in principle direction_splitter returns directions in degrees, # but can we be sure? x=qa.convert(x,'deg') y=qa.convert(y,'deg') x = x['value'] y = y['value'] if epoch != epoch0: # Paranoia casalog.post("[simutil] WARN: precession not handled by average_direction()", 'WARN') x = self.wrapang(x, avgx, 360.0) xx.append(x) yy.append(y) i += 1 avgx = pl.median(xx) avgy = pl.median(yy) offsets=pl.zeros([2,i-1]) i=0 cosdec=pl.cos(avgy*pl.pi/180.) for drn in directions: epoch, x, y = self.direction_splitter(drn) x=qa.convert(x,'deg') y=qa.convert(y,'deg') x = x['value'] y = y['value'] x = self.wrapang(x, avgx, 360.0) offsets[:,i]=[(x-avgx)*cosdec,y-avgy] # apply cosdec to make offsets on sky i+=1 avgx = qa.toangle('%17.12fdeg' % avgx) avgy = qa.toangle('%17.12fdeg' % avgy) avgx = qa.formxxx(avgx, format='hms',prec=5) avgy = qa.formxxx(avgy, format='dms',prec=5) return "%s%s %s" % (epoch0, avgx, avgy), offsets ########################################################### def direction_splitter(self, direction=None): """ Given a direction, return its epoch, x, and y parts. Epoch will be '' if absent, or '%s ' % epoch if present. x and y will be angle qa's in degrees. """ if direction == None: direction=self.direction if type(direction) == type([]): direction=self.average_direction(direction)[0] dirl = direction.split() if len(dirl) == 3: epoch = dirl[0] + ' ' else: epoch = '' # x, y = map(qa.toangle, dirl[-2:]) x=qa.toangle(dirl[1]) # qa is stupid when it comes to dms vs hms, and assumes hms with colons and dms for periods. decstr=dirl[2] # parse with regex to get three numbers and reinstall them as dms q=re.compile('([+-]?\d+).(\d+).(\d+\.?\d*)') qq=q.match(decstr) z=qq.groups() decstr=z[0]+'d' if len(z)>1: decstr=decstr+z[1]+'m' if len(z)>2: decstr=decstr+z[2]+'s' y=qa.toangle(decstr) return epoch, qa.convert(x, 'deg'), qa.convert(y, 'deg') ########################################################### def dir_s2m(self, direction=None): """ Given a direction as a string 'refcode lon lat', return it as qa measure. """ if direction == None: direction=self.direction if type(direction) == type([]): direction=self.average_direction(direction)[0] dirl = direction.split() if len(dirl) == 3: refcode = dirl[0] + ' ' else: refcode = 'J2000' if self.verbose: self.msg("assuming J2000 for "+direction,origin="simutil.s2m") x, y = map(qa.quantity, dirl[-2:]) if x['unit'] == '': x['unit']='deg' if y['unit'] == '': y['unit']='deg' return me.direction(refcode,qa.toangle(x),qa.toangle(y)) ########################################################### def dir_m2s(self, dir): """ Given a direction as a measure, return it as astring 'refcode lon lat'. """ if dir['type'] != 'direction': self.msg("converting direction measure",priority="error",origin="simutil.m2s") return False ystr = qa.formxxx(dir['m1'], format='dms',prec=5) xstr = qa.formxxx(dir['m0'], format='hms',prec=5) return "%s %s %s" % (dir['refer'], xstr, ystr) ########################################################### def wrapang(self, ang, target, period = 360.0): """ Returns ang wrapped so that it is within +-period/2 of target. """ dang = ang - target period = pl.absolute(period) halfperiod = 0.5 * period if pl.absolute(dang) > halfperiod: nwraps = pl.floor(0.5 + float(dang) / period) ang -= nwraps * period return ang ########################################################### #========================== tsys ========================== def noisetemp(self, telescope=None, freq=None, diam=None, epsilon=None): """ Noise temperature and efficiencies for several telescopes: ALMA, ACA, EVLA, VLA, and SMA Input: telescope name, frequency as a quantity string "300GHz", dish diameter (optional - knows diameters for arrays above) epsilon = rms surface accuracy in microns (also optional - this method contains the spec values for each telescope) Output: eta_p phase efficieny (from Ruze formula), eta_s spill (main beam) efficiency, eta_b geometrical blockage efficiency, eta_t taper efficiency, eta_q correlator efficiency including quantization, t_rx reciever temperature. antenna efficiency = eta_p * eta_s * eta_b * eta_t Notes: VLA correlator efficiency includes waveguide loss EVLA correlator efficiency is probably optimistic at 0.88 """ if telescope==None: telescope=self.telescopename # Noisetemp only knows about 6 observatories, # none of which have measure.observatory dicts that # contain lowercase characters so str.upper is harmless here telescope=str.upper(telescope) obs =['ALMASD','ALMA','ACA','EVLA','VLA','SMA'] d =[ 12. ,12. ,7. ,25. ,25. , 6. ] ds =[ 0.75 ,0.75 ,0.75 ,0.364 ,0.364,0.35] # subreflector size for ACA? eps =[ 25. ,25. ,20. ,300 ,300 ,15. ] # antenna surface accuracy cq =[ 0.845, 0.845, 0.88, 0.79, 0.86, 0.88] # correlator eff # SMA is from Wright http://astro.berkeley.edu/~wright/obsrms.py # ALMA includes quantization eff of 0.96 # VLA includes additional waveguide loss from correlator loss of 0.809 # EVLA is probably optimistic # things hardcoded in ALMA etimecalculator # t_ground=270. # t_cmb=2.73 # eta_q*eta_corr = 0.88*.961 # eta_ap = 0.72*eta_ruze if obs.count(telescope)>0: iobs=obs.index(telescope) else: if self.verbose: self.msg("I don't know much about "+telescope+" so I'm going to use ALMA specs") iobs=1 # ALMA is the default ;) if diam==None: diam=d[iobs] diam_subreflector=ds[iobs] if self.verbose: self.msg("subreflector diameter="+str(diam_subreflector),origin="noisetemp") # blockage efficiency. eta_b = 1.-(diam_subreflector/diam)**2 # spillover efficiency. eta_s = 0.95 # these are ALMA values # taper efficiency. #eta_t = 0.86 # these are ALMA values eta_t = 0.819 # 20100914 OT value eta_t = 0.72 # Ruze phase efficiency. if epsilon==None: epsilon = eps[iobs] # microns RMS if freq==None: freq_ghz=qa.convert(qa.quantity(self.centerfreq),'GHz') bw_ghz=qa.convert(qa.quantity(self.bandwidth),'GHz') else: freq_ghz=qa.convert(qa.quantity(freq),'GHz') eta_p = pl.exp(-(4.0*3.1415926535*epsilon*freq_ghz.get("value")/2.99792458e5)**2) if self.verbose: self.msg("ruze phase efficiency for surface accuracy of "+str(epsilon)+"um = " + str(eta_p) + " at "+str(freq),origin="noisetemp") # antenna efficiency # eta_a = eta_p*eta_s*eta_b*eta_t # correlator quantization efficiency. eta_q = cq[iobs] # Receiver radiation temperature in K. if telescope=='ALMA' or telescope=='ACA' or telescope=='ALMASD': # ALMA-40.00.00.00-001-A-SPE.pdf # http://www.eso.org/sci/facilities/alma/system/frontend/ # lower limits # f0=[ 31, 67, 84, 125, 162, 211, 275, 385, 602, 787, 950] # go to higher band in gaps f0=[ 31, 45, 84, 116, 162, 211, 275, 373, 500, 720, 950] # 80% spec # t0=[ 17, 30, 37, 51, 65, 83, 147, 196, 175, 230] # cycle 1 OT values 7/12 t0=[ 17, 30, 45, 51, 65, 55, 75, 196, 100, 230] # CAS-8802 B5 = 163-211GHz: t0=[ 17, 30, 45, 51, 55, 55, 75, 196, 100, 230] # CAS-12387 match Cycle 7 OT and ASC t0=[ 25, 30, 40, 42, 50, 50, 72, 135, 105, 230] flim=[31.3,950] if self.verbose: self.msg("using ALMA/ACA Rx specs",origin="noisetemp") else: if telescope=='EVLA': # 201009114 from rick perley: # f0=[1.5,3,6,10,15,23,33,45] t0=[10.,15,12,15,10,12,15,28] # limits f0=[1,2,4,8,12,18,26.5,40,50] flim=[0.8,50] if self.verbose: self.msg("using EVLA Rx specs",origin="noisetemp") else: if telescope=='VLA': # http://www.vla.nrao.edu/genpub/overview/ # f0=[0.0735,0.32, 1.5, 4.75, 8.4, 14.9, 23, 45 ] # t0=[5000, 165, 56, 44, 34, 110, 110, 110] # exclude P band for now # f0=[0.32, 1.5, 4.75, 8.4, 14.9, 23, 45 ] # limits # http://www.vla.nrao.edu/genpub/overview/ f0=[0.30,0.34,1.73,5,8.8,15.4,24,50] t0=[165, 56, 44, 34, 110, 110, 110] flim=[0.305,50] if self.verbose: self.msg("using old VLA Rx specs",origin="noisetemp") else: if telescope=='SMA': # f0=[212.,310.,383.,660.] # limits f0=[180,250,320,420,720] t0=[67, 116, 134, 500] flim=[180.,720] else: self.msg("I don't know about the "+ telescope+" receivers, using 200K", priority="warn",origin="noisetemp") f0=[10,900] t0=[200,200] flim=[0,5000] obsfreq=freq_ghz.get("value") # z=pl.where(abs(obsfreq-pl.array(f0)) == min(abs(obsfreq-pl.array(f0)))) # t_rx=t0[z[0]] if obsfreq<flim[0]: t_rx=t0[0] self.msg("observing freqency is lower than expected for "+ telescope,priority="warn",origin="noise") self.msg("proceeding with extrapolated receiver temp="+ str(t_rx),priority="warn",origin="noise") else: z=0 while(f0[z]<obsfreq and z<len(t0)): z+=1 t_rx=t0[z-1] if obsfreq>flim[1]: self.msg("observing freqency is higher than expected for "+ telescope,priority="warn",origin="noise") self.msg("proceeding with extrapolated receiver temp="+ str(t_rx),priority="warn",origin="noise") if obsfreq<=flim[1] and obsfreq>=flim[0]: self.msg("interpolated receiver temp="+str(t_rx),origin="noise") return eta_p, eta_s, eta_b, eta_t, eta_q, t_rx def sensitivity(self, freq, bandwidth, etime, elevation, pwv=None, telescope=None, diam=None, nant=None, antennalist=None, doimnoise=None, integration=None,debug=None, method="tsys-atm",tau0=None,t_sky=None): if qa.convert(elevation,"deg")['value']<3: self.msg("Elevation < ALMA limit of 3 deg",priority="error") return False tmpname="tmp"+str(os.getpid()) i=0 while i<10 and len(glob.glob(tmpname+"*"))>0: tmpname="tmp"+str(os.getpid())+str(i) i=i+1 if i>=9: xx=glob.glob(tmpname+"*") for k in range(len(xx)): if os.path.isdir(xx[k]): cu.removetable(xx[k]) else: os.remove(xx[k]) msfile=tmpname+".ms" sm.open(msfile) rxtype=0 # 2SB if antennalist==None: if telescope==None: self.msg("Telescope name has not been set.",priority="error") return False if diam==None: self.msg("Antenna diameter has not been set.",priority="error") return False if nant==None: self.msg("Number of antennas has not been set.",priority="error") return False known=False # check if telescope is known to measures tool # ensure case insensitivity - CAS-12753 obslist_lower = [obs.lower() for obs in me.obslist()] if self.telescope.lower() in obslist_lower: t = self.telescope known = True if known == True: posobs = me.measure(me.observatory(t), 'WGS84') else: self.msg("Unknown telescope and no antenna list.", priority="error") return False obslat=qa.convert(posobs['m1'],'deg')['value'] obslon=qa.convert(posobs['m0'],'deg')['value'] obsalt=qa.convert(posobs['m2'],'m')['value'] stnx,stny,stnz = self.locxyz2itrf(obslat,obslon,obsalt,0,0,0) antnames="A00" # use a single dish of diameter diam - must be set if antennalist # is not set. stnd=[diam] else: if str.upper(antennalist[0:4])=="ALMA": tail=antennalist[5:] if self.isquantity(tail,halt=False): resl=qa.convert(tail,"arcsec")['value'] repodir=os.getenv("CASAPATH").split(' ')[0]+"/data/alma/simmos/" if os.path.exists(repodir): confnum=(2.867-pl.log10(resl*1000*qa.convert(freq,"GHz")['value']/672.))/0.0721 confnum=max(1,min(28,confnum)) conf=str(int(round(confnum))) if len(conf)<2: conf='0'+conf antennalist=repodir+"alma.out"+conf+".cfg" self.msg("converted resolution to antennalist "+antennalist) repodir = os.getenv("CASAPATH").split(' ')[0] + "/data/alma/simmos/" if not os.path.exists(antennalist) and \ os.path.exists(repodir+antennalist): antennalist = repodir + antennalist if os.path.exists(antennalist): stnx, stny, stnz, stnd, padnames, antnames, telescope, posobs = self.readantenna(antennalist) else: self.msg("antennalist "+antennalist+" not found",priority="error") return False nant=len(stnx) # diam is only used as a test below, not quantitatively diam = pl.average(stnd) if (telescope==None or diam==None): self.msg("Telescope name or antenna diameter have not been set.",priority="error") return False # copied from task_simdata: self.setcfg(sm, telescope, stnx, stny, stnz, stnd, padnames, posobs) model_nchan=1 # RI TODO isquantity checks model_width=qa.quantity(bandwidth) # note: ATM uses band center # start is center of first channel. for nch=1, that equals center model_start=qa.quantity(freq) stokes, feeds = self.polsettings(telescope) sm.setspwindow(spwname="band1", freq=qa.tos(model_start), deltafreq=qa.tos(model_width), freqresolution=qa.tos(model_width), nchannels=model_nchan, stokes=stokes) sm.setfeed(mode=feeds, pol=['']) sm.setlimits(shadowlimit=0.01, elevationlimit='3deg') sm.setauto(0.0) obslat=qa.convert(posobs['m1'],'deg') dec=qa.add(obslat, qa.add(qa.quantity("90deg"),qa.mul(elevation,-1))) sm.setfield(sourcename="src1", sourcedirection="J2000 00:00:00.00 "+qa.angle(dec)[0], calcode="OBJ", distance='0m') reftime = me.epoch('TAI', "2012/01/01/00:00:00") if integration==None: integration=qa.mul(etime,0.01) self.msg("observing for "+qa.tos(etime)+" with integration="+qa.tos(integration)) sm.settimes(integrationtime=integration, usehourangle=True, referencetime=reftime) sm.observe(sourcename="src1", spwname="band1", starttime=qa.quantity(0, "s"), stoptime=qa.quantity(etime)); sm.setdata() sm.setvp() eta_p, eta_s, eta_b, eta_t, eta_q, t_rx = self.noisetemp(telescope=telescope,freq=freq) eta_a = eta_p * eta_s * eta_b * eta_t if self.verbose: self.msg('antenna efficiency = ' + str(eta_a),origin="noise") self.msg('spillover efficiency = ' + str(eta_s),origin="noise") self.msg('correlator efficiency = ' + str(eta_q),origin="noise") if pwv==None: # RI TODO choose based on freq octile pwv=2.0 # things hardcoded in ALMA etimecalculator, & defaults in simulator.xml t_ground=270. t_cmb=2.725 # eta_q = 0.88 # eta_a = 0.95*0.8*eta_s if telescope=='ALMA' and (qa.convert(freq,"GHz")['value'])>600.: rxtype=1 # DSB if method=="tsys-atm": sm.setnoise(spillefficiency=eta_s,correfficiency=eta_q, antefficiency=eta_a,trx=t_rx, tground=t_ground,tcmb=t_cmb,pwv=str(pwv)+"mm", mode="tsys-atm",table=tmpname,rxtype=rxtype) else: if method=="tsys-manual": if not t_sky: t_sky=200. self.msg("Warning: Sky brightness temp not set, using 200K",origin="noise",priority="warn") sm.setnoise(spillefficiency=eta_s,correfficiency=eta_q, antefficiency=eta_a,trx=t_rx,tatmos=t_sky, tground=t_ground,tcmb=t_cmb,tau=tau0, mode="tsys-manual",table=tmpname,rxtype=rxtype) else: self.msg("Unknown calculation method "+method,priority="error") return False if doimnoise: sm.corrupt() sm.done() if doimnoise: cellsize=qa.quantity(6.e3/250./qa.convert(model_start,"GHz")["value"],"arcsec") # need better cell determination - 250m?! cellsize=[cellsize,cellsize] # very light clean - its an empty image! self.imtclean(tmpname+".ms",tmpname, "csclean",cellsize,[128,128], "J2000 00:00:00.00 "+qa.angle(dec)[0], False,100,"0.01mJy","natural",[],True,"I") ia.open(tmpname+".image") stats= ia.statistics(robust=True, verbose=False,list=False) ia.done() imnoise=(stats["rms"][0]) else: imnoise=0. nint = qa.convert(etime,'s')['value'] / qa.convert(integration,'s')['value'] nbase= 0.5*nant*(nant-1) if os.path.exists(tmpname+".T.cal"): tb.open(tmpname+".T.cal") gain=tb.getcol("CPARAM") flag=tb.getcol("FLAG") # RI TODO average instead of first? tb.done() # gain is per ANT so square for per baseline; # pick a gain from about the middle of the track # needs to be unflagged! z=pl.where(flag[0][0]==False)[0] nunflagged=len(z) # noiseperbase=1./(gain[0][0][0.5*nint*nant].real)**2 noiseperbase=1./(gain[0][0][z[nunflagged//2]].real)**2 else: noiseperbase=0. theoreticalnoise=noiseperbase/pl.sqrt(nint*nbase*2) # assume 2-poln if debug!=True: xx=glob.glob(tmpname+"*") for k in range(len(xx)): if os.path.isdir(xx[k]): cu.removetable(xx[k]) else: os.remove(xx[k]) if doimnoise: return theoreticalnoise , imnoise else: return theoreticalnoise def setcfg(self, mysm, telescope, x, y, z, diam, padnames, posobs, mounttype=None): """ Sets the antenna positions for the mysm sm instance, which should have already opened an MS, given telescope - telescope name x - array of X positions, i.e. stnx from readantenna y - array of Y positions, i.e. stny from readantenna z - array of Z positions, i.e. stnz from readantenna diam - numpy.array of antenna diameters, i.e. from readantenna padnames - list of pad names antnames - list of antenna names posobs - The observatory position as a measure. Optional: mounttype (Defaults to a guess based on telescope.) Returns the mounttype that it uses. Closes mysm and throws a ValueError if it can't set the configuration. """ if not mounttype: mounttype = 'alt-az' if telescope.upper() in ('ASKAP', # Effectively, if not BIZARRE. 'DRAO', 'WSRT'): mounttype = 'EQUATORIAL' elif telescope.upper() in ('LOFAR', 'LWA', 'MOST'): # And other long wavelength arrays...like that one in WA that # has 3 orthogonal feeds so they can go to the horizon. # # The SKA should go here too once people accept # that it will have to be correlated as stations. mounttype = 'BIZARRE' # Ideally it would be 'FIXED' or # 'GROUND'. if not mysm.setconfig(telescopename=telescope, x=x, y=y, z=z, dishdiameter=diam.tolist(), mount=[mounttype], padname=padnames, coordsystem='global', referencelocation=posobs): mysm.close() raise ValueError("Error setting the configuration") return mounttype ########################################################### #===================== ephemeris ========================== def ephemeris(self, date, usehourangle=True, direction=None, telescope=None, ms=None, cofa=None): if direction==None: direction=self.direction if telescope==None: telescope=self.telescopename # right now, simdata centers at the transit; when that changes, # or when that becomes optional, then that option needs to be # stored in the simutil object and used here, to either # center the plot at transit or not. # # direction="J2000 18h00m00.03s -45d59m59.6s" # refdate="2012/06/21/03:25:00" # useHourAngle_p means simulate at transit # TODO: put in reftime parameter, parse 2012/05/21, 2012/05/21/transit, # and 2012/05/21/22:05:00 separately. ds=self.direction_splitter(direction) # if list, returns average src=me.direction(ds[0],ds[1],ds[2]) me.done() posobs=me.observatory(telescope) if len(posobs)<=0: if cofa==None: self.msg("simutil::ephemeris needs either a known observatory or an explicitly specified cofa measure",priority="error") posobs=cofa me.doframe(posobs) time=me.epoch('TAI',date) me.doframe(time) # what is HA of source at refdate? offset_ha=qa.convert((me.measure(src,'hadec'))['m0'],'h') peak=me.epoch("TAI",qa.add(date,qa.mul(-1,offset_ha))) peaktime_float=peak['m0']['value'] if usehourangle: # offset the reftime to be at transit: time=peak me.doframe(time) reftime_float=time['m0']['value'] reftime_floor=pl.floor(time['m0']['value']) refdate_str=qa.time(qa.totime(str(reftime_floor)+'d'),form='dmy')[0] timeinc='15min' # for plotting timeinc=qa.convert(qa.time(timeinc)[0],'d')['value'] ntime=int(1./timeinc) # check for circumpolar: rset = me.riseset(src) rise = rset['rise'] if rise == 'above': rise = time rise['m0']['value'] = rise['m0']['value'] - 0.5 settime = time settime['m0']['value'] = settime['m0']['value'] + 0.5 elif rise == 'below': raise ValueError(direction + ' is not visible from ' + telescope) else: settime = rset['set'] rise = me.measure(rise['utc'],'tai') settime = me.measure(settime['utc'],'tai') # where to start plotting? offset=-0.5 # this assumes that the values can be compared directly - already assumed in code above if settime['m0']['value'] < time['m0']['value']: offset -= 0.5 if rise['m0']['value'] > time['m0']['value']: offset+=0.5 time['m0']['value']+=offset times=[] az=[] el=[] for i in range(ntime): times.append(time['m0']['value']) me.doframe(time) azel=me.measure(src,'azel') az.append(qa.convert(azel['m0'],'deg')['value']) el.append(qa.convert(azel['m1'],'deg')['value']) time['m0']['value']+=timeinc # self.msg(" ref="+date,origin='ephemeris') # self.msg("rise="+qa.time(rise['m0'],form='dmy')[0],origin='ephemeris') # self.msg(" set="+qa.time(settime['m0'],form='dmy')[0],origin='ephemeris') pl.plot((pl.array(times)-reftime_floor)*24,el) # peak=(rise['m0']['value']+settime['m0']['value'])/2 # self.msg("peak="+qa.time('%fd' % peak,form='dmy'),origin='ephemeris') self.msg("peak="+qa.time('%fd' % reftime_float,form='dmy')[0],origin='ephemeris') relpeak=peaktime_float-reftime_floor pl.plot(pl.array([1,1])*24*relpeak,[0,90]) # if theres an ms, figure out the entire range of observation if ms: tb.open(ms+"/OBSERVATION") timerange=tb.getcol("TIME_RANGE") tb.done() obsstart=min(timerange.flat) obsend=max(timerange.flat) relstart=me.epoch("UTC",str(obsstart)+"s")['m0']['value']-reftime_floor relend=me.epoch("UTC",str(obsend)+"s")['m0']['value']-reftime_floor pl.plot([relstart*24,relend*24],[89,89],'r',linewidth=3) else: if self.totaltime>0: etimeh=qa.convert(self.totaltime,'h')['value'] pl.plot(pl.array([-0.5,0.5])*etimeh+relpeak*24,[80,80],'r') pl.xlabel("hours relative to "+refdate_str,fontsize='x-small') pl.ylabel("elevation",fontsize='x-small') ax=pl.gca() l=ax.get_xticklabels() pl.setp(l,fontsize="x-small") l=ax.get_yticklabels() pl.setp(l,fontsize="x-small") pl.ylim([0,90]) pl.xlim(pl.array([-12,12])+24*(reftime_float-reftime_floor)) ########################################################### #========================================================== def readantenna(self, antab=None): """ Helper function to read antenna configuration file; example: # observatory=ALMA # COFA=-67.75,-23.02 # coordsys=LOC (local tangent plane) # uid___A002_Xdb6217_X55ec_target.ms # x y z diam station ant -5.850273514 -125.9985379 -1.590364043 12. A058 DA41 -19.90369337 52.82680653 -1.892119601 12. A023 DA42 13.45860758 -5.790196849 -2.087805181 12. A035 DA43 5.606192499 7.646657746 -2.087775605 12. A001 DA44 24.10057423 -25.95933768 -2.08466565 12. A036 DA45 lines beginning with "#" will be interpreted as header key=value pairs if they contain "=", and as comments otherwise for the observatory name, one can check the known observatories list me.obslist if an unknown observatory is specified, then one must either use absolute positions (coordsys XYZ,UTM), or specify COFA= lon,lat coordsys can be XYZ=earth-centered, UTM=easting,northing,altitude, or LOC=xoffset,yoffset,height returns: earth-centered x,y,z, diameter, pad_name, antenna_name, observatory_name, observatory_measure_dictionary """ # all coord systems should have x,y,z,diam,pid, where xyz varies params={} # to store key=value "header" parameters inx=[] iny=[] # to store antenna positions inz=[] ind=[] # antenna diameter pid=[] # pad id aid=[] # antenna names nant=0 # counter for input lines that meet format requirements self.msg("Reading antenna positions from '%s'" % antab, origin="readantenna") with open(antab) as f: try: # to read the file for line in f: cols = line.split() if len(cols) == 0: pass # ignore empty rows if line.startswith('#'): # line is header paramlist = line[1:].split('=') if len(paramlist)==2: # key=value pair found # remove extra octothorpes then clean up and assign key = paramlist[0].replace('#','').strip() val = paramlist[1].replace('#','').strip() try: params[key]=val except TypeError: # params = None somehow? Legacy... params={key:val} else: # no key=value pair found, so ignore pass else: # otherwise octothorpe starts comments # take only what's in front of the first one line = line.partition('#')[0] cols = line.split() # now check for data if len(cols)>3: # assign first four columns, assuming order inx.append(float(cols[0])) iny.append(float(cols[1])) inz.append(float(cols[2])) ind.append(float(cols[3])) if len(cols) > 5: # Found unique pad and antenna names pid.append(cols[4]) aid.append(cols[5]) elif len(cols) > 4: # Found pad names, but not antenna names, so dupl. pid.append(cols[4]) aid.append(cols[4]) else: # No antenna or pad names found in antab so default pid.append('A%02d'%nant) aid.append('A%02d'%nant) nant+=1 except IOError: self.msg("Could not read file: '{}'".format(antab), origin='readantenna', priority='error') except ValueError: self.msg("Could not read file: '{}'".format(antab), origin='readantenna', priority='error') if "coordsys" not in params: self.msg("Must specify XYZ, UTM or LOC coordinate system"\ " in antenna file header", origin="readantenna",priority="error") return -1 else: self.coordsys=params["coordsys"] if "observatory" in params: self.telescopename=params["observatory"] else: self.telescopename="SIMULATED" if self.verbose: self.msg("Using observatory= %s" % self.telescopename, origin="readantenna") known = False # case insensitive check if telescopename is known obslist_lower = [obs.lower() for obs in me.obslist()] if self.telescopename.lower() in obslist_lower: t = self.telescopename known = True posobs=me.measure(me.observatory(t),'WGS84') if "COFA" in params: if known: self.msg("antenna config file specifies COFA for a known observatory "+ self.telescopename+", overriding with specified COFA.",priority="warn") obs_latlon=params["COFA"].split(",") cofa_lon=float(obs_latlon[0]) cofa_lat=float(obs_latlon[1]) cofa_alt=0. posobs=me.position("WGS84",qa.quantity(cofa_lon,"deg"),qa.quantity(cofa_lat,"deg"),qa.quantity(cofa_alt,"m")) elif not known: if params["coordsys"].upper()[0:3]=="LOC": self.msg("To use local coordinates in the antenna position file, you must either use a known observatory name, or provide the COFA explicitly",priority="error") return -1 else: # we have absolute coords, so can create the posobs from their # average at the end posobs={} if (params["coordsys"].upper()=="XYZ"): ### earth-centered XYZ i.e. ITRF in casa stnx=inx stny=iny stnz=inz else: stnx=[] stny=[] stnz=[] if (params["coordsys"].upper()=="UTM"): ### expect easting, northing, altitude in m self.msg("Antenna locations in UTM; will read "\ "from file easting, northing, elevation in m", origin="readantenna") if "zone" in params: zone=params["zone"] else: self.msg("You must specify zone=NN in your antenna file", origin="readantenna",priority="error") return -1 if "datum" in params: datum=params["datum"] else: self.msg("You must specify datum in your antenna file", origin="readantenna",priority="error") return -1 if "hemisphere" in params: nors=params["hemisphere"] nors=nors[0].upper() else: self.msg("You must specify hemisphere=N|S in your antenna file",origin="readantenna",priority="error") return -1 vsave=self.verbose for i in range(len(inx)): x,y,z = self.utm2xyz(inx[i],iny[i],inz[i],int(zone),datum,nors) if i==1: self.verbose=False stnx.append(x) stny.append(y) stnz.append(z) self.verbose=vsave else: if (params["coordsys"].upper()[0:3]=="LOC"): obslat=qa.convert(posobs['m1'],'deg')['value'] obslon=qa.convert(posobs['m0'],'deg')['value'] obsalt=qa.convert(posobs['m2'],'m')['value'] if self.verbose: self.msg("converting local tangent plane coordinates"\ "to ITRF using observatory position"\ "= %f %f " % (obslat,obslon), origin="readantenna") for i in range(len(inx)): x,y,z = self.locxyz2itrf(obslat,obslon,obsalt, inx[i],iny[i],inz[i]) stnx.append(x) stny.append(y) stnz.append(z) if len(posobs)<=0: # we had absolute coords but unknown telescope and no COFA cofa_x=pl.average(x) cofa_y=pl.average(y) cofa_z=pl.average(z) cofa_lat,cofa_lon,cofa_alt=self.xyz2long(cofa_x,cofa_y,cofa_z, 'WGS84') posobs=me.position("WGS84",qa.quantity(cofa_lon,"rad"), qa.quantity(cofa_lat,"rad"), qa.quantity(cofa_alt,"m")) return (stnx, stny, stnz, pl.array(ind), pid, aid, self.telescopename, posobs) ########################################################### #==================== geodesy ============================= def tmgeod(self,n,e,eps,cm,fe,sf,so,r,v0,v2,v4,v6,fn,er,esq): """ Transverse Mercator Projection conversion of grid coords n,e to geodetic coords revised subroutine of t. vincenty feb. 25, 1985 orig. source: https://www.ngs.noaa.gov/TOOLS/program_descriptions.html converted from Fortran R Indebetouw Jan 2009 ********** symbols and definitions *********************** latitude positive north, longitude positive west. all angles are in radian measure. input: n,e are northing and easting coordinates respectively er is the semi-major axis of the ellipsoid esq is the square of the 1st eccentricity cm is the central meridian of the projection zone fe is the false easting value at the cm eps is the square of the 2nd eccentricity sf is the scale factor at the cm so is the meridional distance (times the sf) from the equator to southernmost parallel of lat. for the zone r is the radius of the rectifying sphere u0,u2,u4,u6,v0,v2,v4,v6 are precomputed constants for determination of meridianal dist. from latitude output: lat,lon are lat. and long. respectively conv is convergence kp point scale factor the formula used in this subroutine gives geodetic accuracy within zones of 7 degrees in east-west extent. within state transverse mercator projection zones, several minor terms of the equations may be omitted (see a separate ngs publication). if programmed in full, the subroutine can be used for computations in surveys extending over two zones. """ om=(n-fn+so)/(r*sf) # (northing - flag_north + distance_from_equator) cosom=pl.cos(om) foot=om+pl.sin(om)*cosom*(v0+v2*cosom*cosom+v4*cosom**4+v6*cosom**6) sinf=pl.sin(foot) cosf=pl.cos(foot) tn=sinf/cosf ts=tn*tn ets=eps*cosf*cosf rn=er*sf/pl.sqrt(1.-esq*sinf*sinf) q=(e-fe)/rn qs=q*q b2=-tn*(1.+ets)/2. b4=-(5.+3.*ts+ets*(1.-9.*ts)-4.*ets*ets)/12. b6=(61.+45.*ts*(2.+ts)+ets*(46.-252.*ts-60.*ts*ts))/360. b1=1. b3=-(1.+ts+ts+ets)/6. b5=(5.+ts*(28.+24.*ts)+ets*(6.+8.*ts))/120. b7=-(61.+662.*ts+1320.*ts*ts+720.*ts**3)/5040. lat=foot+b2*qs*(1.+qs*(b4+b6*qs)) l=b1*q*(1.+qs*(b3+qs*(b5+b7*qs))) lon=-l/cosf+cm # compute scale factor fi=lat lam = lon sinfi=pl.sin(fi) cosfi=pl.cos(fi) l1=(lam-cm)*cosfi ls=l1*l1 tn=sinfi/cosfi ts=tn*tn # convergence c1=-tn c3=(1.+3.*ets+2.*ets**2)/3. c5=(2.-ts)/15. conv=c1*l1*(1.+ls*(c3+c5*ls)) # point scale factor f2=(1.+ets)/2. f4=(5.-4.*ts+ets*( 9.-24.*ts))/12. kp=sf*(1.+f2*ls*(1.+f4*ls)) return lat,lon,conv,kp def tconpc(self,sf,orlim,er,esq,rf): """ transverse mercator projection *** conversion of grid coords to geodetic coords revised subroutine of t. vincenty feb. 25, 1985 converted from fortran r. indebetouw jan 2009 ********** symbols and definitions *********************** input: rf is the reciprocal flattening of ellipsoid esq = e squared (eccentricity?) er is the semi-major axis for grs-80 sf is the scale factor at the cm orlim is the southernmost parallel of latitude for which the northing coord is zero at the cm output: eps so is the meridional distance (times the sf) from the equator to southernmost parallel of lat. for the zone r is the radius of the rectifying sphere u0,u2,u4,u6,v0,v2,v4,v6 are precomputed constants for determination of meridional dist. from latitude ********************************************************** """ f=1./rf eps=esq/(1.-esq) pr=(1.-f)*er en=(er-pr)/(er+pr) en2=en*en en3=en*en*en en4=en2*en2 c2=-3.*en/2.+9.*en3/16. c4=15.*en2/16.-15.*en4/32. c6=-35.*en3/48. c8=315.*en4/512. u0=2.*(c2-2.*c4+3.*c6-4.*c8) u2=8.*(c4-4.*c6+10.*c8) u4=32.*(c6-6.*c8) u6=128.*c8 c2=3.*en/2.-27.*en3/32. c4=21.*en2/16.-55.*en4/32. c6=151.*en3/96. c8=1097.*en4/512. v0=2.*(c2-2.*c4+3.*c6-4.*c8) v2=8.*(c4-4.*c6+10.*c8) v4=32.*(c6-6.*c8) v6=128.*c8 r=er*(1.-en)*(1.-en*en)*(1.+2.25*en*en+(225./64.)*en4) cosor=pl.cos(orlim) omo=orlim+pl.sin(orlim)*cosor*(u0+u2*cosor*cosor+u4*cosor**4+u6*cosor**6) so=sf*r*omo return eps,r,so,v0,v2,v4,v6 def getdatum(self,datumcode,verbose=False): """ local datums and ellipsoids; input: datam code e.g. 'WGS84','SAM56' output: dx, dy, dz [m] - offsets from ITRF x,y,z i.e. one would take local earth-centered xyz coordinates, add dx,dy,dz to get wgs84 earth-centered er = equatorial radius of the ellipsoid (semi-major axis) [m] rf = reciprocal of flatting of the ellipsoid """ # set equatorial radius and inverse flattening ellipsoids={'AW':[6377563.396,299.3249647 ,'Airy 1830' ], 'AM':[6377340.189,299.3249647 ,'Modified Airy' ], 'AN':[6378160.0 ,298.25 ,'Australian National' ], 'BR':[6377397.155,299.1528128 ,'Bessel 1841' ], 'CC':[6378206.4 ,294.9786982 ,'Clarke 1866' ], 'CD':[6378249.145,293.465 ,'Clarke 1880' ], 'EA':[6377276.345,300.8017 ,'Everest (India 1830)' ], 'RF':[6378137.0 ,298.257222101,'Geodetic Reference System 1980'], 'HE':[6378200.0 ,298.30 ,'Helmert 1906' ], 'HO':[6378270.0 ,297.00 ,'Hough 1960' ], 'IN':[6378388.0 ,297.00 ,'International 1924' ], 'SA':[6378160.0 ,298.25 ,'South American 1969' ], 'WD':[6378135.0 ,298.26 ,'World Geodetic System 1972' ], 'WE':[6378137.0 ,298.257223563,'World Geodetic System 1984' ]} datums={ 'AGD66' :[-133, -48, 148,'AN','Australian Geodetic Datum 1966'], 'AGD84' :[-134, -48, 149,'AN','Australian Geodetic Datum 1984'], 'ASTRO' :[-104,-129, 239,'IN','Camp Area Astro (Antarctica)' ], 'CAPE' :[-136,-108,-292,'CD','CAPE (South Africa)' ], 'ED50' :[ -87, -98,-121,'IN','European 1950' ], 'ED79' :[ -86, -98,-119,'IN','European 1979' ], 'GRB36' :[ 375,-111, 431,'AW','Great Britain 1936' ], 'HAWAII':[ 89,-279,-183,'IN','Hawaiian Hawaii (Old)' ], 'KAUAI' :[ 45,-290,-172,'IN','Hawaiian Kauai (Old)' ], 'MAUI' :[ 65,-290,-190,'IN','Hawaiian Maui (Old)' ], 'OAHU' :[ 56,-284,-181,'IN','Hawaiian Oahu (Old)' ], 'INDIA' :[ 289, 734, 257,'EA','Indian' ], 'NAD83' :[ 0, 0, 0,'RF','N. American 1983' ], 'CANADA':[ -10, 158, 187,'CC','N. American Canada 1927' ], 'ALASKA':[ -5, 135, 172,'CC','N. American Alaska 1927' ], 'NAD27' :[ -8, 160, 176,'CC','N. American Conus 1927' ], 'CARIBB':[ -7, 152, 178,'CC','N. American Caribbean' ], 'MEXICO':[ -12, 130, 190,'CC','N. American Mexico' ], 'CAMER' :[ 0, 125, 194,'CC','N. American Central America' ], 'SAM56' :[-288, 175,-376,'IN','South American (Provisional1956)'], 'SAM69' :[ -57, 1 , -41,'SA','South American 1969' ], 'CAMPO' :[-148, 136, 90,'IN','S. American Campo Inchauspe (Argentina)'], 'WGS72' :[ 0, 0 , 4.5,'WD','World Geodetic System - 72' ], 'WGS84' :[ 0, 0 , 0,'WE','World Geodetic System - 84' ]} if datumcode not in datums: self.msg("unknown datum %s" % datumcode,priority="error") return -1 datum=datums[datumcode] ellipsoid=datum[3] if ellipsoid not in ellipsoids: self.msg("unknown ellipsoid %s" % ellipsoid,priority="error") return -1 if verbose: self.msg("Using %s datum with %s ellipsoid" % (datum[4],ellipsoids[ellipsoid][2]),origin="getdatum") return datum[0],datum[1],datum[2],ellipsoids[ellipsoid][0],ellipsoids[ellipsoid][1] def utm2long(self,east,north,zone,datum,nors): """ this program converts universal transverse meractor coordinates to gps longitude and latitude (in radians) converted from Fortran by R. Indebetouw Jan 2009. orig. source: https://www.ngs.noaa.gov/TOOLS/program_descriptions.html ri also added other datums and ellipsoids in a helper function header from original UTMS fortran program: * this program was originally written by e. carlson * subroutines tmgrid, tconst, tmgeod, tconpc, * were written by t. vincenty, ngs, in july 1984 . * the orginal program was written in september of 1988. * * this program was updated on febuary 16, 1989. the update was * having the option of saving and *81* record file. * * * this program was updated on april 3, 1990. the following update * were made: * 1. change from just a choice of nad27 of nad83 reference * ellipsoids to; clarke 1866, grs80/wgs84, international, and * allow for user defined other. * 2. allow use of latitudes in southern hemisphere and longitudes * up to 360 degrees west. * * this program was updated on december 1, 1993. the following update * was made: * 1. the northings would compute the right latitude in the southern * hemishpere. * 2. the computed latitude on longidutes would be either in e or w. * ***************************************************************** * * disclaimer * * * * this program and supporting information is furnished by the * * government of the united states of america, and is accepted and * * used by the recipient with the understanding that the united states * * government makes no warranties, express or implied, concerning the * * accuracy, completeness, reliability, or suitability of this * * program, of its constituent parts, or of any supporting data. * * * * the government of the united states of america shall be under no * * liability whatsoever resulting from any use of this program. this * * program should not be relied upon as the sole basis for solving a * * problem whose incorrect solution could result in injury to person * * or property. * * * * this program is property of the government of the united states * * of america. therefore, the recipient further agrees not to assert * * proprietary rights therein and not to represent this program to * * anyone as being other than a government program. * * * *********************************************************************** this is the driver program to compute latitudes and longitudes from the utms for each zone input: northing, easting zone, datum nors=N/S determined according to datum: er = equatorial radius of the ellipsoid (semi-major axis) [m] rf = reciprocal of flatting of the ellipsod [unitless] f = esq= e squared calculated according to longitude and zone: rad = radian conversion factor cm = central meridian ( computed using the longitude) sf = scale factor of central meridian ( always .9996 for utm) orlim = southernmost parallel of latitude ( always zero for utm) r, a, b, c, u, v, w = ellipsoid constants used for computing meridional distance from latitude so = meridional distance (multiplied by scale factor ) from the equator to the southernmost parallel of latitude ( always zero for utm) """ rad=180./pl.pi offx,offy,offz,er,rf = self.getdatum(datum,verbose=self.verbose) f=1./rf esq=(2*f-f*f) # find the central meridian if the zone number is less than 30 if zone < 30 : # ie W long - this code uses W=positive lon iz=zone icm=(183-(6*iz)) cm=float(icm)/rad ucm=(icm+3)/rad lcm=(icm-3)/rad else: iz=zone icm=(543 - (6*iz)) cm= float(icm)/rad ucm=(icm+3)/rad lcm=(icm-3)/rad tol=(5./60.)/rad if nors == 'S': fn= 10000000. else: fn=0. fe=500000.0 sf=0.9996 orlim=0.0 found=0 # precompute parameters for this zone: eps,r,so,v0,v2,v4,v6 = self.tconpc(sf,orlim,er,esq,rf) # compute the latitudes and longitudes: lat,lon,conv,kp = self.tmgeod(north,east,eps,cm,fe,sf,so,r,v0,v2,v4,v6,fn,er,esq) # do the test to see if the longitude is within 5 minutes # of the boundaries for the zone and if so compute the # north and easting for the adjacent zone # if abs(ucm-lam) < tol: # cm=float(icm+6)/rad # iz=iz-1 # if iz == 0: # iz=60 # found=found+1 # lat,lon,conv,kp = tmgeod(n,e,eps,cm,fe,sf,so,r,v0,v2,v4,v6,fn,er,esq) # # if abs(lcm-lam) < tol: # cm=float(icm-6)/rad # iz=iz+1 # if iz == 61: # iz=1 # lat,lon,conv,kp = tmgeod(n,e,eps,cm,fe,sf,so,r,v0,v2,v4,v6,fn,er,esq) # found=found+1 # *** convert to more usual convention of negative lon = W lon=-lon if self.verbose: self.msg(" longitude, latitude = %s %s; conv,kp = %f,%f" % (qa.angle(qa.quantity(lon,"rad"),prec=8)[0],qa.angle(qa.quantity(lat,"rad"),prec=8)[0],conv,kp),origin="utm2long") return lon,lat def long2xyz(self,lon,lat,elevation,datum): """ Returns the nominal ITRF (X, Y, Z) coordinates [m] for a point at geodetic latitude and longitude [radians] and elevation [m]. The ITRF frame used is not the official ITRF, just a right handed Cartesian system with X going through 0 latitude and 0 longitude, and Z going through the north pole. orig. source: http://www.oc.nps.edu/oc2902w/coord/llhxyz.htm """ # geodesy/NGS/XYZWIN/ # http://www.oc.nps.edu/oc2902w/coord/llhxyz.htm dx,dy,dz,er,rf = self.getdatum(datum,verbose=False) f=1./rf esq=2*f-f**2 nu=er/pl.sqrt(1.-esq*(pl.sin(lat))**2) x=(nu+elevation)*pl.cos(lat)*pl.cos(lon) +dx y=(nu+elevation)*pl.cos(lat)*pl.sin(lon) +dy z=((1.-esq)*nu+elevation)*pl.sin(lat) +dz # https://www.ngs.noaa.gov/cgi-bin/xyz_getxyz.prl # S231200.0 W0674800.0 0.0000 # > 2216194.4188 -5430618.6472 -2497092.5213 GRS80 return x,y,z def xyz2long(self,x,y,z,datum): """ Given ITRF Earth-centered (X, Y, Z) coordinates [m] for a point, returns geodetic latitude and longitude [radians] and elevation [m] The ITRF frame used is not the official ITRF, just a right handed Cartesian system with X going through 0 latitude and 0 longitude, and Z going through the north pole. Elevation is measured relative to the closest point to the (latitude, longitude) on the WGS84 reference ellipsoid. orig. source: http://www.iausofa.org/2013_1202_C/sofa/gc2gde.html """ # http://www.iausofa.org/ # http://www.iausofa.org/2013_1202_C/sofa/gc2gde.html dx,dy,dz,er,rf = self.getdatum(datum,verbose=False) f=1./rf # Validate ellipsoid parameters. if ( f < 0.0 or f >= 1.0 ): return -1,-1,-1 if ( er <= 0.0 ): return -1,-1,-1 #Functions of ellipsoid parameters (with further validation of f). e2 = (2.0 - f) * f e4t = e2*e2 * 1.5 ec2 = 1.0 - e2 # = 1-esq = (1-f)**2 if ( ec2 <= 0.0 ): return -1,-1,-1 ec = pl.sqrt(ec2) # =1-f b = er * ec # Distance from polar axis r = pl.sqrt( (x-dx)**2 + (y-dy)**2 ) # Longitude. if r>0.: lon = pl.arctan2(y-dx, x-dx) else: lon = 0. # Prepare Newton correction factors. s0 = abs(z-dz) / er c0 = ec * r / er a0 = pl.sqrt( c0**2 + s0**2 ) d0 = ec* s0* a0**3 + e2* s0**3 f0 = r/er* a0**3 - e2* c0**3 # Prepare Halley correction factor. b0 = e4t * s0**2 * c0**2 * r/er * (a0 - ec) s1 = d0*f0 - b0*s0 cc = ec * (f0*f0 - b0*c0) # Evaluate latitude and height. */ lat = pl.arctan2(s1,cc) height = (r*cc + abs(z-dz)*s1 - er*pl.sqrt(ec2*s1**2 + cc**2))/pl.sqrt(s1**2 + cc**2) # Restore sign of latitude. if (z-dz) < 0: lat = -lat return lon,lat,height def xyz2long_old(self,x,y,z,datum): dx,dy,dz,er,rf = self.getdatum(datum,verbose=False) f=1./rf b= ((x-dx)**2 + (y-dy)**2) / er**2 c= (z-dx)**2 / er**2 esq=2*f-f**2 # (a2-b2)/a2 a0=c*(1-esq) a1=2*a0 efth=esq**2 a2=a0+b-efth a3=-2.*efth a4=-efth b0=4.*a0 b1=3.*a1 b2=2.*a2 b3=a3 # refine/calculate esq nlqk=esq for i in range(3): nlqks = nlqk * nlqk nlqkc = nlqk * nlqks nlf = a0*nlqks*nlqks + a1*nlqkc + a2*nlqks + a3*nlqk + a4 nlfprm = b0*nlqkc + b1*nlqks + b2*nlqk + b3 nlqk = nlqk - (nlf / nlfprm) y0 = (1.+nlqk)*(z-dz) x0 = pl.sqrt((x-dx)**2 + (y-dy)**2) lat=pl.arctan2(y0,x0) lon=pl.arctan2(y-dy,x-dx) #casalog.post... x-dx,y-dy,z-dz,x0,y0 return lon,lat def utm2xyz(self,easting,northing,elevation,zone,datum,nors): """ Returns the nominal ITRF (X, Y, Z) coordinates [m] for a point at UTM easting, northing, elevation [m], and zone of a given datum (e.g. 'WGS84') and north/south flag ("N" or "S"). The ITRF frame used is not the official ITRF, just a right handed Cartesian system with X going through 0 latitude and 0 longitude, and Z going through the north pole. """ lon,lat = self.utm2long(easting,northing,zone,datum,nors) x,y,z = self.long2xyz(lon,lat,elevation,datum) return x,y,z def locxyz2itrf(self, lat, longitude, alt, locx=0.0, locy=0.0, locz=0.0): """ Returns the nominal ITRF (X, Y, Z) coordinates [m] for a point at "local" (x, y, z) [m] measured at geodetic latitude lat and longitude longitude (degrees) and altitude of the reference point of alt. The ITRF frame used is not the official ITRF, just a right handed Cartesian system with X going through 0 latitude and 0 longitude, and Z going through the north pole. The "local" (x, y, z) are measured relative to the closest point to (lat, longitude) on the WGS84 reference ellipsoid, with z normal to the ellipsoid and y pointing north. """ # from Rob Reid; need to generalize to use any datum... phi, lmbda = map(pl.radians, (lat, longitude)) sphi = pl.sin(phi) a = 6378137.0 # WGS84 equatorial semimajor axis b = 6356752.3142 # WGS84 polar semimajor axis ae = pl.arccos(b / a) N = a / pl.sqrt(1.0 - (pl.sin(ae) * sphi)**2) # Now you see the connection between the Old Ones and Antarctica... Nploczcphimlocysphi = (N + locz+alt) * pl.cos(phi) - locy * sphi clmb = pl.cos(lmbda) slmb = pl.sin(lmbda) x = Nploczcphimlocysphi * clmb - locx * slmb y = Nploczcphimlocysphi * slmb + locx * clmb z = (N * (b / a)**2 + locz+alt) * sphi + locy * pl.cos(phi) return x, y, z def itrf2loc(self, x,y,z, cx,cy,cz): """ Given Earth-centered ITRF (X, Y, Z) coordinates [m] and the Earth-centered coords of the center of array, Returns local (x, y, z) [m] relative to the center of the array, oriented with x and y tangent to the closest point at the COFA (latitude, longitude) on the WGS84 reference ellipsoid, with z normal to the ellipsoid and y pointing north. """ clon,clat,h = self.xyz2long(cx,cy,cz,'WGS84') ccoslon=pl.cos(clon) csinlon=pl.sin(clon) csinlat=pl.sin(clat) ccoslat=pl.cos(clat) if isinstance(x,float): # weak x=[x] y=[y] z=[z] n=x.__len__() lat=pl.zeros(n) lon=pl.zeros(n) el=pl.zeros(n) # do like MsPlotConvert for i in range(n): # translate w/o rotating: xtrans=x[i]-cx ytrans=y[i]-cy ztrans=z[i]-cz # rotate lat[i] = (-csinlon*xtrans) + (ccoslon*ytrans) lon[i] = (-csinlat*ccoslon*xtrans) - (csinlat*csinlon*ytrans) + ccoslat*ztrans el[i] = (ccoslat*ccoslon*xtrans) + (ccoslat*csinlon*ytrans) + csinlat*ztrans return lat,lon,el def itrf2locname(self, x,y,z, obsname): """ Given Earth-centered ITRF (X, Y, Z) coordinates [m] and the name of an known array (see me.obslist()), Returns local (x, y, z) [m] relative to the center of the array, oriented with x and y tangent to the closest point at the COFA (latitude, longitude) on the WGS84 reference ellipsoid, with z normal to the ellipsoid and y pointing north. """ cofa=me.measure(me.observatory(obsname),'WGS84') cx,cy,cz=self.long2xyz(cofa['m0']['value'],cofa['m1']['value'],cofa['m2']['value'],cofa['refer']) return self.itrf2loc(x,y,z,cx,cy,cz) ########################################################### def plotants(self,x,y,z,d,name): # given globals cx=pl.mean(x) cy=pl.mean(y) cz=pl.mean(z) lat,lon,el = self.itrf2loc(x,y,z,cx,cy,cz) n=lat.__len__() dolam=0 # TODO convert to klam: (d too) ### rg=max(lat)-min(lat) r2=max(lon)-min(lon) if r2>rg: rg=r2 if max(d)>0.01*rg: pl.plot(lat,lon,',') #casalog.post(max(d),ra) for i in range(n): pl.gca().add_patch(pl.Circle((lat[i],lon[i]),radius=0.5*d[i],fc="#dddd66")) if n<10: pl.text(lat[i],lon[i],name[i],horizontalalignment='center',verticalalignment='center') else: pl.plot(lat,lon,'o',c="#dddd66") if n<10: for i in range(n): pl.text(lat[i],lon[i],name[i],horizontalalignment='center',fontsize=8) pl.axis('equal') #if dolam: # pl.xlabel("kilolamda") # pl.ylabel("kilolamda") ###################################################### # helper function to get the pixel size from an image (positive increments) def cellsize(self,image): ia.open(image) mycs=ia.coordsys() ia.done() increments=mycs.increment(type="direction")['numeric'] cellx=qa.quantity(abs(increments[0]),mycs.units(type="direction")[0]) celly=qa.quantity(abs(increments[1]),mycs.units(type="direction")[1]) xform=mycs.lineartransform(type="direction") offdiag=max(abs(xform[0,1]),abs(xform[1,0])) if offdiag > 1e-4: self.msg("Your image is rotated with respect to Lat/Lon. I can't cope with that yet",priority="error") cellx=qa.mul(cellx,abs(xform[0,0])) celly=qa.mul(celly,abs(xform[1,1])) return [cellx,celly] ###################################################### # helper function to get the spectral size from an image def spectral(self,image): ia.open(image) cs=ia.coordsys() sh=ia.shape() ia.done() spc = cs.findcoordinate("spectral") if not spc['return']: return 0.0 model_width=str(cs.increment(type="spectral")['numeric'][0])+cs.units(type="spectral")[0] model_nchan=sh[spc['pixel']] return model_nchan,model_width ###################################################### def is4d(self, image): ia.open(image) s=ia.shape() if len(s)!=4: return False cs=ia.coordsys() ia.done() dir=cs.findcoordinate("direction") spc=cs.findcoordinate("spectral") stk=cs.findcoordinate("stokes") if not (dir['return'] and spc['return'] and stk['return']): return False if dir['pixel'].__len__() != 2: return False if spc['pixel'].__len__() != 1: return False if stk['pixel'].__len__() != 1: return False # they have to be in the correct order too if stk['pixel']!=2: return False if spc['pixel']!=3: return False if dir['pixel'][0]!=0: return False if dir['pixel'][1]!=1: return False cs.done() return True ################################################################## # fit modelimage into a 4 coordinate image defined by the parameters # TODO spectral extrapolation and regridding using innchan **** def modifymodel(self, inimage, outimage, inbright,indirection,incell,incenter,inwidth,innchan, flatimage=False): # if nonzero, create mom -1 image # new ia tool in_ia=ia.newimagefromfile(inimage) in_shape=in_ia.shape() in_csys=in_ia.coordsys() # pull data first, since ia.stats doesn't work w/o a CS: if outimage!=inimage: if self.verbose: self.msg("rearranging input data (may take some time for large cubes)",origin="setup model") arr=in_ia.getchunk() else: # TODO move rearrange to inside ia tool, and at least don't do this: arr=pl.zeros(in_shape) axmap=[-1,-1,-1,-1] axassigned=[-1,-1,-1,-1] # brightness scaling if (inbright=="unchanged") or (inbright==""): scalefactor=1. else: if self.isquantity(inbright,halt=False): qinb=qa.quantity(inbright) if qinb['unit']!='': # qa doesn't deal universally well with pixels and beams # so this may fail: try: inb=qa.convert(qinb,"Jy/pixel")['value'] except: inb=qinb['value'] self.msg("assuming inbright="+str(inbright)+" means "+str(inb)+" Jy/pixel",priority="warn") inbright=inb try: scalefactor=float(inbright)/pl.nanmax(arr) except Exception: in_ia.close() raise # check shape characteristics of the input; # add degenerate axes as neeed: in_dir=in_csys.findcoordinate("direction") in_spc=in_csys.findcoordinate("spectral") in_stk=in_csys.findcoordinate("stokes") # first check number of pixel axes and raise to 4 if required in_nax=arr.shape.__len__() if in_nax<2: in_ia.close() self.msg("Your input model has fewer than 2 dimensions. Can't proceed",priority="error") return False if in_nax==2: arr=arr.reshape([arr.shape[0],arr.shape[1],1]) in_shape=arr.shape in_nax=in_shape.__len__() # which should be 3 if in_nax==3: arr=arr.reshape([arr.shape[0],arr.shape[1],arr.shape[2],1]) in_shape=arr.shape in_nax=in_shape.__len__() # which should be 4 if in_nax>4: in_ia.close() self.msg("model image has more than 4 dimensions. Not sure how to proceed",priority="error") return False # make incell a list if not already try: if type(incell) == type([]): incell = map(qa.convert,incell,['arcsec','arcsec']) else: incell = qa.abs(qa.convert(incell,'arcsec')) # incell[0]<0 for RA increasing left incell = [qa.mul(incell,-1),incell] except Exception: # invalid incell in_ia.close() raise # later, we can test validity with qa.compare() # now parse coordsys: model_refdir="" model_cell="" # look for direction coordinate, with two pixel axes: if in_dir['return']: in_ndir = in_dir['pixel'].__len__() if in_ndir != 2: self.msg("Mal-formed direction coordinates in modelimage. Discarding and using first two pixel axes for RA and Dec.") axmap[0]=0 # direction in first two pixel axes axmap[1]=1 axassigned[0]=0 # coordinate corresponding to first 2 pixel axes axassigned[1]=0 else: # we've found direction axes, and may change their increments and direction or not. dirax=in_dir['pixel'] axmap[0]=dirax[0] axmap[1]=dirax[1] axassigned[dirax[0]]=0 axassigned[dirax[1]]=0 if self.verbose: self.msg("Direction coordinate (%i,%i) parsed" % (axmap[0],axmap[1]),origin="setup model") # move model_refdir to center of image model_refpix=[0.5*in_shape[axmap[0]],0.5*in_shape[axmap[1]]] ra = in_ia.toworld(model_refpix,'q')['quantity']['*'+str(axmap[0]+1)] dec = in_ia.toworld(model_refpix,'q')['quantity']['*'+str(axmap[1]+1)] model_refdir= in_csys.referencecode(type="direction",list=False)[0]+" "+qa.formxxx(ra,format='hms',prec=5)+" "+qa.formxxx(dec,format='dms',prec=5) model_projection=in_csys.projection()['type'] model_projpars=in_csys.projection()['parameters'] # cell size from image increments=in_csys.increment(type="direction")['numeric'] incellx=qa.quantity(increments[0],in_csys.units(type="direction")[0]) incelly=qa.quantity(increments[1],in_csys.units(type="direction")[1]) xform=in_csys.lineartransform(type="direction") offdiag=max(abs(xform[0,1]),abs(xform[1,0])) if offdiag > 1e-4: self.msg("Your image is rotated with respect to Lat/Lon. I can't cope with that yet",priority="error") incellx=qa.mul(incellx,xform[0,0]) incelly=qa.mul(incelly,xform[1,1]) # preserve sign in case input image is backwards (RA increasing right) model_cell = [qa.convert(incellx,'arcsec'),qa.convert(incelly,'arcsec')] else: # no valid direction coordinate axmap[0]=0 # assign direction to first two pixel axes axmap[1]=1 axassigned[0]=0 # assign coordinate corresponding to first 2 pixel axes axassigned[1]=0 # try to parse direction using splitter function and override model_refdir if type(indirection)==type([]): if len(indirection) > 1: in_ia.close() self.msg("error parsing indirection "+str(indirection)+" -- should be a single direction string") return False else: indirection=indirection[0] if self.isdirection(indirection,halt=False): # lon/lat = ra/dec if J2000, =glon/glat if galactic epoch, lon, lat = self.direction_splitter(indirection) model_refdir=epoch+qa.formxxx(lon,format='hms',prec=5)+" "+qa.formxxx(lat,format='dms',prec=5) model_refpix=[0.5*in_shape[axmap[0]],0.5*in_shape[axmap[1]]] model_projection="SIN" # for indirection we default to SIN. model_projpars=pl.array([0.,0]) if self.verbose: self.msg("setting model image direction to indirection = "+model_refdir,origin="setup model") else: # indirection is not set - is there a direction in the model already? if not self.isdirection(model_refdir,halt=False): in_ia.close() self.msg("Cannot determine reference direction in model image. Valid 'indirection' parameter must be provided.",priority="error") return False # override model_cell? cell_replaced=False if self.isquantity(incell[0],halt=False): if qa.compare(incell[0],"1arcsec"): model_cell=incell cell_replaced=True if self.verbose: self.msg("replacing existing model cell size with incell",origin="setup model") valid_modcell=False if not cell_replaced: if self.isquantity(model_cell[0],halt=False): if qa.compare(model_cell[0],"1arcsec"): valid_modcell=True if not valid_modcell: in_ia.close() self.msg("Unable to determine model cell size. Valid 'incell' parameter must be provided.",priority="error") return False if self.verbose: self.msg("model image shape="+str(in_shape),origin="setup model") self.msg("model pixel = %8.2e x %8.2e arcsec" % (model_cell[0]['value'],model_cell[1]['value']),origin="setup model") # we've now found or assigned two direction axes, and changed direction and cell if required # next, work on spectral axis: model_specrefval="" model_width="" # look for a spectral axis: if in_spc['return']: #if type(in_spc[1]) == type(1) : # # should not come here after SWIG switch over # foo=in_spc[1] #else: foo=in_spc['pixel'][0] if in_spc['pixel'].__len__() > 1: self.msg("you seem to have two spectral axes",priority="warn") model_nchan=arr.shape[foo] axmap[3]=foo axassigned[foo]=3 model_restfreq=in_csys.restfrequency() model_specrefpix=in_csys.referencepixel(type="spectral")['numeric'][0] model_width =in_csys.increment(type="spectral")['numeric'][0] model_specrefval=in_csys.referencevalue(type="spectral")['numeric'][0] # make quantities model_width=qa.quantity(model_width,in_csys.units(type="spectral")[0]) model_specrefval=qa.quantity(model_specrefval,in_csys.units(type="spectral")[0]) add_spectral_coord=False if self.verbose: self.msg("Spectral Coordinate %i parsed" % axmap[3],origin="setup model") else: # need to add one to the coordsys add_spectral_coord=True if add_spectral_coord: # find first unused axis - probably at end, but just in case its not: i=0 extra_axis=-1 while extra_axis<0 and i<4: if axassigned[i]<0: extra_axis=i i+=1 if extra_axis<0: in_ia.close() self.msg("I can't find an unused axis to make Spectral [%i %i %i %i] " % (axassigned[0],axassigned[1],axassigned[2],axassigned[3]),priority="error",origin="setup model") return False axmap[3]=extra_axis axassigned[extra_axis]=3 model_nchan=arr.shape[extra_axis] # override specrefval? specref_replaced=False if self.isquantity(incenter,halt=False): if qa.compare(incenter,"1Hz"): if (qa.quantity(incenter))['value']>=0: model_specrefval=incenter model_specrefpix=pl.floor(model_nchan*0.5) model_restfreq=incenter specref_replaced=True if self.verbose: self.msg("setting central frequency to "+incenter,origin="setup model") valid_modspec=False if not specref_replaced: if self.isquantity(model_specrefval,halt=False): if qa.compare(model_specrefval,"1Hz"): valid_modspec=True if not valid_modspec: in_ia.close() self.msg("Unable to determine model frequency. Valid 'incenter' parameter must be provided.",priority="error") return False # override inwidth? width_replaced=False if self.isquantity(inwidth,halt=False): if qa.compare(inwidth,"1Hz"): if (qa.quantity(inwidth))['value']>=0: model_width=inwidth width_replaced=True if self.verbose: self.msg("setting channel width to "+inwidth,origin="setup model") valid_modwidth=False if not width_replaced: if self.isquantity(model_width,halt=False): if qa.compare(model_width,"1Hz"): valid_modwidth=True if not valid_modwidth: in_ia.close() self.msg("Unable to determine model channel or bandwidth. Valid 'inwidth' parameter must be provided.",priority="error") return False model_stokes="" # look for a stokes axis if in_stk['return']: model_stokes=in_csys.stokes() foo=model_stokes[0] out_nstk=model_stokes.__len__() for i in range(out_nstk-1): foo=foo+model_stokes[i+1] model_stokes=foo #if type(in_stk[1]) == type(1): # # should not come here after SWIG switch over # foo=in_stk[1] #else: foo=in_stk['pixel'][0] if in_stk['pixel'].__len__() > 1: self.msg("you seem to have two stokes axes",priority="warn") axmap[2]=foo axassigned[foo]=2 if in_shape[foo]>4: in_ia.close() self.msg("you appear to have more than 4 Stokes components - please edit your header and/or parameters",priority="error") return False add_stokes_coord=False if self.verbose: self.msg("Stokes Coordinate %i parsed" % axmap[2],origin="setup model") else: # need to add one to the coordsys add_stokes_coord=True if add_stokes_coord: # find first unused axis - probably at end, but just in case its not: i=0 extra_axis=-1 while extra_axis<0 and i<4: if axassigned[i]<0: extra_axis=i i+=1 if extra_axis<0: in_ia.close() self.msg("I can't find an unused axis to make Stokes [%i %i %i %i] " % (axassigned[0],axassigned[1],axassigned[2],axassigned[3]),priority="error",origin="setup model") return False axmap[2]=extra_axis axassigned[extra_axis]=2 if arr.shape[extra_axis]>4: in_ia.close() self.msg("you have %i Stokes parameters in your potential Stokes axis %i. something is wrong." % (arr.shape[extra_axis],extra_axis),priority="error") return False if self.verbose: self.msg("Adding Stokes Coordinate",origin="setup model") if arr.shape[extra_axis]==4: model_stokes="IQUV" if arr.shape[extra_axis]==3: model_stokes="IQV" self.msg("setting IQV Stokes parameters from the 4th axis of you model. If that's not what you want, then edit the header",origin="setup model",priority="warn") if arr.shape[extra_axis]==2: model_stokes="IQ" self.msg("setting IQ Stokes parameters from the 4th axis of you model. If that's not what you want, then edit the header",origin="setup model",priority="warn") if arr.shape[extra_axis]<=1: model_stokes="I" # ======================================== # now we should have 4 assigned pixel axes, and model_cell, model_refdir, model_nchan, # model_stokes all set out_nstk=len(model_stokes) if self.verbose: self.msg("axis map for model image = %i %i %i %i" % (axmap[0],axmap[1],axmap[2],axmap[3]),origin="setup model") modelshape=[in_shape[axmap[0]], in_shape[axmap[1]],out_nstk,model_nchan] if outimage!=inimage: ia.fromshape(outimage,modelshape,overwrite=True) modelcsys=ia.coordsys() else: modelcsys=in_ia.coordsys() modelcsys.setunits(['rad','rad','','Hz']) modelcsys.setincrement([qa.convert(model_cell[0],modelcsys.units()[0])['value'], # already -1 qa.convert(model_cell[1],modelcsys.units()[1])['value']], type="direction") dirm=self.dir_s2m(model_refdir) lonq=dirm['m0'] latq=dirm['m1'] modelcsys.setreferencecode(dirm['refer'],type="direction") if len(model_projpars)>0: modelcsys.setprojection(parameters=model_projpars.tolist(),type=model_projection) else: modelcsys.setprojection(type=model_projection) modelcsys.setreferencevalue( [qa.convert(lonq,modelcsys.units()[0])['value'], qa.convert(latq,modelcsys.units()[1])['value']], type="direction") modelcsys.setreferencepixel(model_refpix,"direction") if self.verbose: self.msg("sky model image direction = "+model_refdir,origin="setup model") self.msg("sky model image increment = "+str(model_cell[0]),origin="setup model") modelcsys.setspectral(refcode="LSRK",restfreq=model_restfreq) modelcsys.setreferencevalue(qa.convert(model_specrefval,modelcsys.units()[3])['value'],type="spectral") modelcsys.setreferencepixel(model_specrefpix,type="spectral") # but not half-pixel modelcsys.setincrement(qa.convert(model_width,modelcsys.units()[3])['value'],type="spectral") # first assure that the csys has the expected order expected=['Direction', 'Direction', 'Stokes', 'Spectral'] if modelcsys.axiscoordinatetypes() != expected: in_ia.close() self.msg("internal error with coordinate axis order created by Imager",priority="error") self.msg(modelcsys.axiscoordinatetypes().__str__(),priority="error") return False # more checks: foo=pl.array(modelshape) if not (pl.array(arr.shape) == pl.array(foo.take(axmap).tolist())).all(): in_ia.close() self.msg("internal error: I'm confused about the shape if your model data cube",priority="error") self.msg("have "+foo.take(axmap).__str__()+", want "+in_shape.__str__(),priority="error") return False if outimage!=inimage: ia.setcoordsys(modelcsys.torecord()) ia.done() ia.open(outimage) # now rearrange the pixel axes if ness. for ax in range(4): if axmap[ax] != ax: if self.verbose: self.msg("swapping input axes %i with %i" % (ax,axmap[ax]),origin="setup model") arr=arr.swapaxes(ax,axmap[ax]) tmp=axmap[ax] axmap[ax]=ax axmap[tmp]=tmp # there's got to be a better way to remove NaNs: if outimage!=inimage: for i0 in range(arr.shape[0]): for i1 in range(arr.shape[1]): for i2 in range(arr.shape[2]): for i3 in range(arr.shape[3]): foo=arr[i0,i1,i2,i3] if foo!=foo: arr[i0,i1,i2,i3]=0.0 if self.verbose and outimage!=inimage: self.msg("model array minmax= %e %e" % (arr.min(),arr.max()),origin="setup model") self.msg("scaling model brightness by a factor of %f" % scalefactor,origin="setup model") self.msg("image channel width = %8.2e GHz" % qa.convert(model_width,'GHz')['value'],origin="setup model") if arr.nbytes > 5e7: self.msg("your model is large - predicting visibilities may take a while.",priority="warn") if outimage!=inimage: ia.putchunk(arr*scalefactor) ia.setbrightnessunit("Jy/pixel") ia.close() in_ia.close() # outimage should now have correct Coordsys and shape # make a moment 0 image if flatimage and outimage!=inimage: self.flatimage(outimage,verbose=self.verbose) # returning to the outside world we'll return a positive cell: model_cell=[qa.abs(model_cell[0]),qa.abs(model_cell[1])] model_size=[qa.mul(modelshape[0],model_cell[0]), qa.mul(modelshape[1],model_cell[1])] if self.verbose: self.msg(" ") # add a line after my spewage return model_refdir,model_cell,model_size,model_nchan,model_specrefval,model_specrefpix,model_width,model_stokes ################################################################## # image/clean subtask def imclean(self,mstoimage,imagename, cleanmode,psfmode,cell,imsize,imcenter, interactive,niter,threshold,weighting, outertaper,pbcor,stokes,sourcefieldlist="", modelimage="",mask=[],dryrun=False): """ Wrapper function to call CASA imaging task 'clean' on a MeasurementSet mstoimage parameter expects the path to a MeasurementSet imsize parameter expects a length-2 list of integers cell parameter expects a length-2 list containing qa.quantity objects interactive and dryrun parameters expect boolean type input Other parameters expect input in format compatible with 'clean' No return Creates a template '[imagename].clean.last' file in addition to outputs of task 'clean' """ raise RuntimeError("clean task is no longer available, switch to imtclean in simutil module") ################################################################## # image/tclean subtask def imtclean(self, mstoimage, imagename, gridder, deconvolver, cell, imsize, imdirection, interactive, niter, threshold, weighting, outertaper, pbcor, stokes, modelimage="", mask=[], dryrun=False): """ Wrapper function for Radio Interferometric Image Reconstruction from input MeasurementSet using standard CASA imaging task ('tclean'). Duplicates the method "imclean" but with non-deprecated task call. Selecting individual fields for imaging is not supported mstoimage parameter expects the path to a MeasurementSet, or list of MeasurementSets imagename parameter expects string for output image file imsize parameter expects a length-1 or length-2 list of integers cell parameter expects a length-2 list containing qa.quantity objects Just like imclean, does not yield return Creates a template '[imagename].tclean.last' file in addition to normal tclean task outputs """ invocation_parameters = OrderedDict( ) # use the first provided MS to determine channelization for output if is_array_type(mstoimage): ms0 = mstoimage[0] if len(mstoimage) == 1: mstoimage = mstoimage[0] else: ms0 = mstoimage if os.path.exists(ms0): tb.open(ms0 + "/SPECTRAL_WINDOW") if tb.nrows() > 1: self.msg("Detected more than one SPW in " + ms0, priority="info", origin="simutil") self.msg("Determining output cube parameters using " + "first SPW present in " + ms0, priority="info", origin="simutil") freq=tb.getvarcol("CHAN_FREQ")['r1'] nchan=freq.size tb.done() elif dryrun: nchan=1 # duplicate imclean method self.msg("nchan > 1 is not supported for dryrun = True", priority="info", origin="simutil") if nchan == 1: chanmode = 'mfs' else: chanmode = 'cube' # next, define tclean call defaults # legacy comparison of image size input against heuristic optsize = [0,0] optsize[0] = _su.getOptimumSize(imsize[0]) if len(imsize) == 1: # user expects square output images optsize[1]=optsize[0] else: optsize[1]=_su.getOptimumSize(imsize[1]) if((optsize[0] != imsize[0]) or (len(imsize) != 1 and optsize[1] != imsize[1])): imsize = optsize self.msg(str(imsize)+" is not an acceptable imagesize, " + " using imsize=" + str(optsize) + " instead", priority="warn", origin="simutil") # the cell parameter expects a list of qa.quantity objects, formatted_correctly = [qa.isquantity(cell[i]) for i in range(len(cell))] assert False not in formatted_correctly, "simutil function imtclean expects cell parameter input to be comprised of quantity objects" # convert the first two elements for storage in the tclean.last file cellparam = [str(cell[0]['value']) + str(cell[0]['unit']), str(cell[1]['value']) + str(cell[1]['unit'])] if os.path.exists(modelimage): pass elif len(modelimage) > 0: modelimage = "" self.msg("Could not find modelimage, proceeding without one", priority="warn", origin="simutil") # set tclean top-level parameters (no parent nodes) invocation_parameters['vis'] = mstoimage invocation_parameters['selectdata'] = False invocation_parameters['imagename'] = imagename invocation_parameters['imsize'] = imsize invocation_parameters['cell'] = cellparam invocation_parameters['phasecenter'] = imdirection invocation_parameters['stokes'] = stokes invocation_parameters['startmodel'] = modelimage invocation_parameters['specmode'] = chanmode invocation_parameters['gridder'] = gridder invocation_parameters['deconvolver'] = deconvolver invocation_parameters['restoration'] = True invocation_parameters['outlierfile'] = '' invocation_parameters['weighting'] = weighting invocation_parameters['niter'] = niter invocation_parameters['usemask'] = 'user' invocation_parameters['fastnoise'] = True invocation_parameters['restart'] = True invocation_parameters['savemodel'] = 'none' invocation_parameters['calcres'] = True invocation_parameters['calcpsf'] = True invocation_parameters['parallel'] = False # subparameters invocation_parameters['restoringbeam'] = 'common' invocation_parameters['pbcor'] = pbcor invocation_parameters['uvtaper'] = outertaper if niter > 0: invocation_parameters['threshold'] = threshold invocation_parameters['interactive'] = interactive else: invocation_parameters['threshold'] = '' invocation_parameters['interactive'] = False invocation_parameters['mask'] = mask invocation_parameters['pbmask'] = 0.0 # write the tclean.last file (template in case of dryrun=True) # # it would be preferable to have a helper function do _this_, # then just call tclean right from simanalyze, but precedent. filename = os.path.join(os.getcwd(),imagename+'.tclean.last') if os.path.isfile(filename): self.msg("Overwriting existing 'tclean.last' file", priority="info",origin="simutil") os.remove(filename) else: with open(filename, 'w'): pass # fill in the tclean.last file # # the sections of code that handle this for tasks are # autogenerated during the CASAshell build process. See: # [unpacked_build]/lib/py/lib/python3.6/site-packages/casashell/private with open(filename,'w') as f: # fill in the used parameters first for i in invocation_parameters: # catch None type objects returned by repr function if i.startswith('<') and s.endswith('>'): f.write("{:<20}={!s}\n".format(i, None)) else: f.write("{:<20}={!r}\n".format(i, invocation_parameters[i])) # next, open and fill the task call f.write("#tclean( ") count = 0 for i in invocation_parameters: if i.startswith('<') and s.endswith('>'): f.write("{!s}={!s}".format(i, None)) else: f.write("{!s}={!r}".format(i, invocation_parameters[i])) count += 1 if count < len(invocation_parameters): f.write(",") # finally close the task call f.write(" )\n") # gather tclean task call by attempting to parse the file just created with open(filename, 'r') as _f: for line in _f: if line.startswith('#tclean( '): task_call = line[1:-1] if self.verbose: self.msg(task_call, priority="warn", origin="simutil") else: self.msg(task_call, priority="info", origin="simutil") # now that the tclean call is specified, it may be executed if not dryrun: casalog.filter("ERROR") tclean( vis = invocation_parameters['vis'], selectdata = invocation_parameters['selectdata'], imagename=invocation_parameters['imagename'], imsize=invocation_parameters['imsize'], cell=invocation_parameters['cell'], phasecenter=invocation_parameters['phasecenter'], stokes=invocation_parameters['stokes'], startmodel=invocation_parameters['startmodel'], specmode=invocation_parameters['specmode'], gridder=invocation_parameters['gridder'], deconvolver=invocation_parameters['deconvolver'], restoration=invocation_parameters['restoration'], outlierfile=invocation_parameters['outlierfile'], weighting=invocation_parameters['weighting'], niter=invocation_parameters['niter'], usemask=invocation_parameters['usemask'], fastnoise=invocation_parameters['fastnoise'], restart=invocation_parameters['restart'], savemodel=invocation_parameters['savemodel'], calcres=invocation_parameters['calcres'], calcpsf=invocation_parameters['calcpsf'], parallel=invocation_parameters['parallel'], restoringbeam=invocation_parameters['restoringbeam'], pbcor=invocation_parameters['pbcor'], uvtaper=invocation_parameters['uvtaper'], threshold=invocation_parameters['threshold'], interactive=invocation_parameters['interactive'], mask=invocation_parameters['mask'], pbmask=invocation_parameters['pbmask'] ) casalog.filter() ################################################### def flatimage(self,image,complist="",verbose=False): # flat output ia.open(image) imsize=ia.shape() imcsys=ia.coordsys() ia.done() spectax=imcsys.findcoordinate('spectral')['pixel'] nchan=imsize[spectax] stokesax=imcsys.findcoordinate('stokes')['pixel'] nstokes=imsize[stokesax] flat=image+".flat" if nchan>1: if verbose: self.msg("creating moment zero image "+flat,origin="flatimage") ia.open(image) flat_ia = ia.moments(moments=[-1],outfile=flat,overwrite=True) ia.done() flat_ia.close() del flat_ia else: if verbose: self.msg("removing degenerate image axes in "+flat,origin="flatimage") # just remove degenerate axes from image flat_ia = ia.newimagefromimage(infile=image,outfile=flat,dropdeg=True,overwrite=True) flat_ia.close() # seems no way to just drop the spectral and keep the stokes. if nstokes<=1: os.rename(flat,flat+".tmp") ia.open(flat+".tmp") flat_ia = ia.adddegaxes(outfile=flat,stokes='I',overwrite=True) ia.done() flat_ia.close() shutil.rmtree(flat+".tmp") del flat_ia if nstokes>1: os.rename(flat,flat+".tmp") po.open(flat+".tmp") foo=po.stokesi(outfile=flat) foo.done() po.done() shutil.rmtree(flat+".tmp") imcsys.done() del imcsys # add components if len(complist)>0: ia.open(flat) if not os.path.exists(complist): self.msg("sky component list "+str(complist)+" not found in flatimage",priority="error") cl.open(complist) ia.modify(cl.torecord(),subtract=False) cl.done() ia.done() ################################################### def convimage(self,modelflat,outflat,complist=""): # regrid flat input to flat output shape and convolve modelregrid = modelflat+".regrid" # get outflatcoordsys from outflat ia.open(outflat) outflatcs=ia.coordsys() outflatshape=ia.shape() # and beam TODO is beam the same in flat as a cube? beam=ia.restoringbeam() ia.done() ia.open(modelflat) modelflatcs=ia.coordsys() modelflatshape=ia.shape() ia.setrestoringbeam(beam=beam) tmpxx=ia.regrid(outfile=modelregrid+'.tmp', overwrite=True, csys=outflatcs.torecord(),shape=outflatshape, asvelocity=False) # ia.regrid assumes a surface brightness, or more accurately doesnt # pay attention to units at all, so we now have to scale # by the pixel size to have the right values in jy/pixel, # get pixel size from model image coordsys tmpxx.done() increments=outflatcs.increment(type="direction")['numeric'] incellx=qa.quantity(abs(increments[0]),outflatcs.units(type="direction")[0]) incelly=qa.quantity(abs(increments[1]),outflatcs.units(type="direction")[1]) xform=outflatcs.lineartransform(type="direction") offdiag=max(abs(xform[0,1]),abs(xform[1,0])) if offdiag > 1e-4: self.msg("Your image is rotated with respect to Lat/Lon. I can't cope with that yet",priority="error") incellx=qa.mul(incellx,abs(xform[0,0])) incelly=qa.mul(incelly,abs(xform[1,1])) model_cell = [qa.convert(incellx,'arcsec'),qa.convert(incelly,'arcsec')] # and from outflat (the clean image) increments=outflatcs.increment(type="direction")['numeric'] incellx=qa.quantity(abs(increments[0]),outflatcs.units(type="direction")[0]) incelly=qa.quantity(abs(increments[1]),outflatcs.units(type="direction")[1]) xform=outflatcs.lineartransform(type="direction") offdiag=max(abs(xform[0,1]),abs(xform[1,0])) if offdiag > 1e-4: self.msg("Your image is rotated with respect to Lat/Lon. I can't cope with that yet",priority="error") incellx=qa.mul(incellx,abs(xform[0,0])) incelly=qa.mul(incelly,abs(xform[1,1])) cell = [qa.convert(incellx,'arcsec'),qa.convert(incelly,'arcsec')] # image scaling factor = (qa.convert(cell[0],"arcsec")['value']) factor *= (qa.convert(cell[1],"arcsec")['value']) factor /= (qa.convert(model_cell[0],"arcsec")['value']) factor /= (qa.convert(model_cell[1],"arcsec")['value']) imrr = ia.imagecalc(modelregrid, "'%s'*%g" % (modelregrid+'.tmp',factor), overwrite = True) shutil.rmtree(modelregrid+".tmp") if self.verbose: self.msg("scaling model by pixel area ratio %g" % factor,origin='convimage') # add unresolved components in Jy/pix # don't do this if you've already done it in flatimage()! if (os.path.exists(complist)): cl.open(complist) imrr.modify(cl.torecord(),subtract=False) cl.done() imrr.done() ia.done() del imrr # Convolve model with beam. convolved = modelregrid + '.conv' ia.open(modelregrid) ia.setbrightnessunit("Jy/pixel") tmpcnv=ia.convolve2d(convolved,major=beam['major'],minor=beam['minor'], pa=beam['positionangle'],overwrite=True) ia.done() #tmpcnv.open(convolved) tmpcnv.setbrightnessunit("Jy/beam") tmpcnv.setrestoringbeam(beam=beam) tmpcnv.done() def bandname(self, freq): """ Given freq in GHz, return the silly and in some cases deliberately confusing band name that some people insist on using. """ # TODO: add a trivia argument to optionally provide the historical # radar band info from Wikipedia. band = '' if freq > 90: # Use the ALMA system. band = 'band%.0f' % (0.01 * freq) # () are mandatory! # Now switch to radar band names. Above 40 GHz different groups used # different names. Wikipedia uses ones from Baytron, a now defunct company # that made test equipment. elif freq > 75: # used as a visual sensor for experimental autonomous vehicles band = 'W' elif freq > 50: # Very strongly absorbed by atmospheric O2, band = 'V' # which resonates at 60 GHz. elif freq >= 40: band = 'Q' elif freq >= 26.5: # mapping, short range, airport surveillance; band = 'Ka' # frequency just above K band (hence 'a') # Photo radar operates at 34.300 +/- 0.100 GHz. elif freq >= 18: band = 'K' # from German kurz, meaning 'short'; limited use due to # absorption by water vapor, so Ku and Ka were used # instead for surveillance. Used for detecting clouds # and at 24.150 +/- 0.100 GHz for speeding motorists. elif freq >= 12: band = 'U' # or Ku elif freq >= 8: # Missile guidance, weather, medium-resolution mapping and ground band = 'X' # surveillance; in the USA the narrow range 10.525 GHz +/- 25 MHz # is used for airport radar and short range tracking. Named X # band because the frequency was a secret during WW2. elif freq >= 4: band = 'C' # Satellite transponders; a compromise (hence 'C') between X # and S bands; weather; long range tracking elif freq >= 2: band = 'S' # Moderate range surveillance, air traffic control, # long-range weather; 'S' for 'short' elif freq >= 1: band = 'L' # Long range air traffic control and surveillance; 'L' for 'long' elif freq >= 0.3: band = 'UHF' else: band = 'P' # for 'previous', applied retrospectively to early radar systems # Includes HF and VHF. Worse, it leaves no space for me # to put in rock band easter eggs. return band def polsettings(self, telescope, poltype=None, listall=False): """ Returns stokes parameters (for use as stokes in sm.setspwindow) and feed type (for use as mode in sm.setfeed). If poltype is not specified or recognized, a guess is made using telescope. Defaults to 'XX YY', 'perfect X Y' If listall is True, return the options instead. """ psets = {'circular': ('RR LL', 'perfect R L'), 'linear': ('XX YY', 'perfect X Y'), 'RR': ('RR', 'perfect R L')} if listall: return psets if poltype not in psets: poltype = 'linear' for circ in ('VLA', 'DRAO'): # Done this way to if circ in telescope.upper(): # catch EVLA. poltype = 'circular' return psets[poltype] ####################################### # ALMA calcpointings def applyRotate(self, x, y, tcos, tsin): return tcos*x-tsin*y, tsin*x+tcos*y def isEven(self, num): return (num % 2) == 0 # this was the only algorithm in Cycle 0 - for Cycle 1 it was # recast as BaseTriangularTiling.getPointings(), the width>height # case statement was removed, and BaseTriangularTiling was supplemented by # ShiftTriangularTiling.getPointings() def getTrianglePoints(self, width, height, angle, spacing): tcos = pl.cos(angle*pl.pi/180) tsin = pl.sin(angle*pl.pi/180) xx=[] yy=[] if (width >= height): wSpacing = spacing hSpacing = (pl.sqrt(3.) / 2) * spacing nwEven = int(pl.floor((width / 2) / wSpacing)) nwOdd = int(pl.floor((width / 2) / wSpacing + 0.5)) nh = int(pl.floor((height / 2) / hSpacing)) for ih in pl.array(range(nh*2+1))-nh: if (self.isEven(ih)): for iw in pl.array(range(nwEven*2+1))-nwEven: x,y = self.applyRotate(iw*wSpacing, ih*hSpacing, tcos, tsin) xx.append(x) yy.append(y) else: for iw in pl.array(range(nwOdd*2+1))-nwOdd: x,y = self.applyRotate((iw+0.5)*wSpacing, ih*hSpacing, tcos, tsin) xx.append(x) yy.append(y) else: wSpacing = (pl.sqrt(3) / 2) * spacing hSpacing = spacing nw = int(pl.floor((width / 2) / wSpacing)) nhEven = int(pl.floor((height / 2) / hSpacing)) nhOdd = int(pl.floor((height / 2) / hSpacing + 0.5)) for iw in pl.array(range(nw*2+1))-nw: if (self.isEven(iw)): for ih in pl.array(range(nhEven*2+1))-nhEven: x,y = self.applyRotate(iw*wSpacing, ih*hSpacing, tcos, tsin) xx.append(x) yy.append(y) else: for ih in pl.array(range(nhOdd*2+1))-nhOdd: x,y = self.applyRotate(iw*wSpacing, (ih+0.5)*hSpacing, tcos, tsin) xx.append(x) yy.append(y) return xx,yy def getTriangularTiling(self, longlength, latlength, angle, spacing, pb): # OT if isLandscape, shortside=Latlength # else isLandscape=false, shortside=Longlength if longlength > latlength: # OT isLandscape=True (OT uses >= ) width=longlength # arcsec height=latlength # arcsec shortside=height else: width=latlength height=longlength shortside=height angle=angle+90 # which tiling? Base or Shifted (OT getTiling) vSpacing = (pl.sqrt(3) / 2) * spacing n = pl.ceil(shortside / vSpacing) if n % 2 == 0: return self.getShiftTriangularTiling(width, height, angle, spacing, pb) else: return self.getBaseTriangularTiling(width, height, angle, spacing, pb) def needsFiller(self, length, spacing, bs, npoints): if length > spacing * (npoints - 1) + bs: return 1 else: return 0 def getBaseTriangularTiling(self, width, height, angle, spacing, pb): tcos = pl.cos(angle*pl.pi/180) tsin = pl.sin(angle*pl.pi/180) xx=[] yy=[] wSpacing = spacing hSpacing = (pl.sqrt(3.) / 2) * spacing nwEven = int(pl.floor((width / 2) / wSpacing)) nwEven += self.needsFiller(width, wSpacing, pb, nwEven*2+1) nwOdd = int(pl.floor((width / 2) / wSpacing + 0.5)) nwOdd += self.needsFiller(width, wSpacing, pb, nwOdd*2) nh = int(pl.floor((height / 2) / hSpacing)) nh += self.needsFiller(height, hSpacing, pb, nh*2+1) for ih in pl.array(range(nh*2+1))-nh: if (self.isEven(ih)): for iw in pl.array(range(nwEven*2+1))-nwEven: x,y = self.applyRotate(iw*wSpacing, ih*hSpacing, tcos, tsin) xx.append(x) yy.append(-y) # will require additional testing @ angle>0 else: for iw in pl.array(range(nwOdd*2))-nwOdd: x,y = self.applyRotate((iw+0.5)*wSpacing, ih*hSpacing, tcos, tsin) xx.append(x) yy.append(-y) return xx,yy def getShiftTriangularTiling(self, width, height, angle, spacing, pb): tcos = pl.cos(angle*pl.pi/180) tsin = pl.sin(angle*pl.pi/180) xx=[] yy=[] wSpacing = spacing hSpacing = (pl.sqrt(3.) / 2) * spacing nwEven = int(pl.floor((width / 2) / wSpacing + 0.5)) nwEven += self.needsFiller(width, wSpacing, pb, nwEven*2) nwOdd = int(pl.floor((width / 2) / wSpacing )) nwOdd += self.needsFiller(width, wSpacing, pb, nwOdd*2+1) nh = int(pl.floor((height - hSpacing) / 2 / hSpacing +1)) nh += self.needsFiller(height, hSpacing, pb, nh*2) for ih in pl.array(range(nh*2))-nh: if (self.isEven(ih)): for iw in pl.array(range(nwEven*2))-nwEven: x,y = self.applyRotate((iw+0.5)*wSpacing, (ih+0.5)*hSpacing, tcos, tsin) xx.append(x) yy.append(-y) else: for iw in pl.array(range(nwOdd*2+1))-nwOdd: x,y = self.applyRotate(iw*wSpacing, (ih+0.5)*hSpacing, tcos, tsin) xx.append(x) yy.append(-y) return xx,yy ###################################### # adapted from aU.getBaselineStats def baselineLengths(self, configfile): stnx, stny, stnz, stnd, padnames, antnames, telescopename, posobs = self.readantenna(configfile) # use mean position, not official COFA=posobs cx=pl.mean(stnx) cy=pl.mean(stny) cz=pl.mean(stnz) nAntennas=len(stnx) lat,lon,el = self.itrf2loc(stnx,stny,stnz,cx,cy,cz) #l = {} #neighborlist = [] maxlength = 0 minlength = 1e9 #mylengths = pl.zeros([nAntennas,nAntennas]) mylengths=pl.zeros(nAntennas*(nAntennas-1)//2) k=0 for i in range(nAntennas): for j in range(i+1,nAntennas): x = lat[i]-lat[j] y = lon[i]-lon[j] z = el[i]- el[j] #mylengths[i][j] = (x**2 + y**2 + z**2)**0.5 mylengths[k]=(x**2 + y**2 + z**2)**0.5 k=k+1 return mylengths ###################### def approxBeam(self,configfile,freq): # freq must be in GHz mylengths=self.baselineLengths(configfile) rmslength = pl.sqrt(pl.mean(mylengths.flatten()**2)) ninety = scoreatpercentile(mylengths, 90) return 0.2997924/freq/ninety*3600.*180/pl.pi # lambda/b converted to arcsec ###################### def sfBeam1d(self,beam="", cell="", convsupport=-1, sampling=""): """ Calculates PSF of image generated by gridfunction 'SF'. Migrated codes from gjinc.sfBeam() in analysisUtil module by Todd Hunter. Note: this is simplified version of the function. Paramters: beam : Antenna primary beam (quantity) cell : Cell size of image (quantity) convsupport : convolution support used for imaging (integer) sampling : pointing spacing of observation (quantity). If not defined, comvolution of sampling kernel is skipped with warning. Returns: Estimated PSF of image (quantity). """ if not qa.compare(beam, "rad"): raise ValueError("beam should be a quantity of antenna primary beam size (angle)") if not qa.compare(cell, "rad"): raise ValueError("cell should be a quantity of image pixel size (angle)") if len(sampling) > 0 and not qa.compare(sampling, "rad"): raise ValueError("sampling should be a quantity of pointing spacing (angle)") if (convsupport < -1): convsupport = 3 supportwidth = (convsupport*2 + 1) c = supportwidth * pl.pi/2. pb_asec = qa.getvalue(qa.convert(beam, "arcsec")) cell_asec = qa.getvalue(qa.convert(cell, "arcsec")) samp_asec = -1. if len(sampling) > 0: samp_asec = qa.getvalue(qa.convert(sampling, "arcsec")) # Define kernel array myxaxis = pl.arange(-3*pb_asec,3*pb_asec+0.1,0.2) # Gaussian func of PB scale = 0.5 / ( (pb_asec/2.3548201)**2 ) mygaussian = pl.exp(- scale * myxaxis**2 ) ### exp(x**2/(2*sigma**2)) # Spheroidal kernel func sfaxis = myxaxis/float((supportwidth-1)*cell_asec/2.0) indices = pl.where(abs(sfaxis)<1)[0] centralRegion = sfaxis[indices] centralRegionY = spspec.pro_ang1_cv(0, 0, c, spspec.pro_cv(0,0,c), centralRegion)[0] mysf = pl.zeros(len(myxaxis)) mysf[indices] += centralRegionY/max(centralRegionY) # Convolution of Gaussian PB and SF result = spsig.convolve(mysf, mygaussian, mode='same') del mygaussian, sfaxis, indices, centralRegion, centralRegionY # Sampling function if samp_asec > 0: myboxcar = pl.zeros(len(myxaxis)) indices = pl.where(abs(myxaxis)<samp_asec/2.)[0] myboxcar[indices] = 1 result = spsig.convolve(result, myboxcar, mode='same') else: self.msg("Pointing spacing is not specified. Calculated PSF could be less accurate.", priority="warn", origin="sfBeam1d") # Calculate FWHM result /= max(result) halfmax = max(result)*0.5 spline = spintrp.UnivariateSpline(myxaxis, result-halfmax, s=0) x0, x1 = spline.roots() del result, myxaxis, spline return qa.quantity(abs(x1-x0), "arcsec")