-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSTO2_v35.py
More file actions
2100 lines (1731 loc) · 80 KB
/
Copy pathSTO2_v35.py
File metadata and controls
2100 lines (1731 loc) · 80 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Class to read STO-2 Level 0.6 and Level 1.0 data
There can still be bugs in this software. No software is really bug-free.....
The use of the software is at you own risk.
Created: 1/1/2017
By: V. Tolls, SAO
Current Version: 3.5 (current version see also variable after class definition)
Modification History:
3/16/2017: modified resample: return input arrays if step=1
01/10/2018: added masks to each line to log spike removal/repairs and other modifications
11/02/2018: started version 3.3, primarily to add velocity cropping for [NII] spectra
03/18/2019: updated the coordinate retrieval to Galactic coordinates
05/01/2020: v3.5: minor updates to improve functionality
"""
import os
import numpy as np
import glob
from astropy.io import fits
from astropy import units as u
from astropy.coordinates import SkyCoord
from scipy import signal
from scipy.signal import butter, filtfilt, medfilt
from scipy import interpolate
from scipy.interpolate import interp1d, CubicSpline
from scipy.interpolate import UnivariateSpline
from pylab import *
import matplotlib.mlab as mlab
import warnings
from ALSFitter import *
from _cffi_backend import __version__
__version__ = '3.5.0'
__date__ = '05/01/2020'
warnings.filterwarnings("ignore")
# convenience routine to load data from file
def readSTO2(ifile, retdd=False, retcl=False, verbose=False, trim=None, rclip=None, tnorm=True, badpix=None, badpix0=None, badpix1=None, badpix2=None):
"""
Convenience routine for class rsto2 to read STO-2 Level 0.5 data of a single line
Input:
ifile: data file name (full path might be required)
retdd: if True, raw data table dd1 is returned (default: False)
retcl: if True, class object is returned (default: False)
verbose: print some info to screen
trim: trim both ends of the velocity and intensity arrays by the given number of
pixels (default: None) Note: better use rclip to not change array size (shape)!
rclip: replaces the given number of pixels at both ends of the velocity and intensity arrays by the value
of the next adjoining pixels (default: None)
tnorm: flag to return spectrum divided by integration time: spectrum/tint
badpix: pixels with bad values to be replaced by interpolated value from neighboring pixels
badpix0,1,2: pixels with bad values to be replaced by interpolated value from neighboring pixels for lines 0, 1, or 2 only
Output:
separate arrays for velocity, and intensity, pos (astropy SkyCoord object), FITS header, and (optional) the raw data table
"""
rs = rsto2(ifile, verbose=False, trim=None, rclip=rclip, tnorm=tnorm, badpix=badpix, badpix0=badpix0, badpix1=badpix1, badpix2=badpix2)
vv, spec = rs.getData()
if retcl:
if retdd: return vv, spec, rs.getPos(), rs.getHeader(), rs.getRawData(), rs
else: return vv, spec, rs.getPos(), rs.getHeader(), rs
else:
if retdd: return vv, spec, rs.getPos(), rs.getHeader(), rs.getRawData()
else: return vv, spec, rs.getPos(), rs.getHeader()
def readSTO2Line(ifile, lin, retdd=False, retcl=False, verbose=False, trim=None, rclip=None, tnorm=True, badpix=None, badpix0=None, badpix1=None, badpix2=None):
"""
Convenience routine for class rsto2 to read STO-2 Level 0.5 data of a single line
Input:
ifile: data file name (full path might be required)
lin: desired Level 0.5 line: 0: NII_1; 1: NII_2; 2: CII_1; 3: CII_2; 4: OI_FFT; 5: OI_Freq_LK
Level 0.6 line: 0: NII_1; 1: NII_2; 2: CII_2 (other, not observed lines were removed!)
retdd: if True, raw data table dd1 is returned (default: False)
retcl: if True, class object is returned (default: False)
verbose: print some info to screen
trim: trim both ends of the velocity and intensity arrays by the given number of
pixels (default: None) Note: better use rclip!
rclip: replaces the given number of pixels at both ends of the velocity and intensity arrays by the value
of the next adjoining pixels (default: None)
tnorm: flag to return spectrum divided by integration time: spectrum/tint
badpix: pixels with bad values to be replaced by interpolated value from neighboring pixels
Output:
separate arrays for velocity, and intensity, pos (astropy SkyCoord object), FITS header, and (optional) the raw data table
"""
rs = rsto2(ifile, verbose=False, trim=None, rclip=rclip, tnorm=tnorm, badpix=badpix, badpix0=badpix0, badpix1=badpix1, badpix2=badpix2)
vv, spec = rs.getDataLine(lin)
if retcl:
if retdd: return vv, spec, rs.getPosLine(lin), rs.getHeader(), rs.getRawData(), rs
else: return vv, spec, rs.getPosLine(lin), rs.getHeader(), rs
else:
if retdd: return vv, spec, rs.getPosLine(lin), rs.getHeader(), rs.getRawData()
else: return vv, spec, rs.getPosLine(lin), rs.getHeader()
def readSTO2LineM(ifile, lin, retdd=False, retcl=False, verbose=False, trim=None, rclip=None, tnorm=True,
badpix=None, badpix0=None, badpix1=None, badpix2=None, vrange=None):
"""
Convenience routine for class rsto2 to read STO-2 Level 0.5 data of a single line
Input:
ifile: data file name (full path might be required)
lin: desired Level 0.5 line: 0: NII_1; 1: NII_2; 2: CII_1; 3: CII_2; 4: OI_FFT; 5: OI_Freq_LK
Level 0.6 line: 0: NII_1; 1: NII_2; 2: CII_2 (other, not observed lines were removed!)
retdd: if True, raw data table dd1 is returned (default: False)
retcl: if True, class object is returned (default: False)
verbose: print some info to screen
trim: trim both ends of the velocity and intensity arrays by the given number of
pixels (default: None) Note: better use rclip!
rclip: replaces the given number of pixels at both ends of the velocity and intensity arrays by the value
of the next adjoining pixels (default: None)
tnorm: flag to return spectrum divided by integration time: spectrum/tint
badpix: pixels with bad values to be replaced by interpolated value from neighboring pixels
vrange: if provided, will be used to return spectra limited to this range for simplification in data reduction (default: None)
Output:
separate arrays for velocity, and intensity, mask, pos (astropy SkyCoord object), FITS header, and (optional) the raw data table
"""
rs = rsto2(ifile, verbose=False, trim=None, rclip=rclip, tnorm=tnorm, badpix=badpix, badpix0=badpix0, badpix1=badpix1, badpix2=badpix2)
vv, spec = rs.getDataLine(lin)
mask = rs.getDataMaskLine(lin)
if vrange!=None:
if verbose: print(vv.shape, spec.shape, mask.shape, vrange, vv.value.min(), vv.value.max())
vsel = np.where((vv.value>=vrange[0])&(vv.value<=vrange[1]))
vv = np.squeeze(vv[vsel])
spec = np.squeeze(spec[vsel])
mask = np.squeeze(mask[vsel])
if verbose: print(vv.shape, spec.shape, mask.shape, vrange, vv.value.min(), vv.value.max())
if retcl:
if retdd: return vv, spec, mask, rs.getPosLine(lin), rs.getHeader(), rs.getRawData(), rs
else: return vv, spec, mask, rs.getPosLine(lin), rs.getHeader(), rs
else:
if retdd: return vv, spec, mask, rs.getPosLine(lin), rs.getHeader(), rs.getRawData()
else: return vv, spec, mask, rs.getPosLine(lin), rs.getHeader()
class rsto2:
def __init__(self, ifile, trim=None, rclip=None, verbose=False, tnorm=True, badpix=None, badpix0=None, badpix1=None, badpix2=None):
'''
reading STO-2 Level 0 and 1 data
the difference is that some entries in the data table dd (see retdd=True) are not
available in the Level 0 data, e.g. Tsys
ifile: data file name (full path might be required)
verbose: printing some info (default: False)
trim: trim both ends of the velocity and intensity arrays by the given number of
pixels (default: None) Note: better use rclip!
rclip: replaces the value of given number of pixels at both ends of the intensity array by the value
of the next adjoining pixels (default: None, but typical value is 1 or 2)
tnorm: flag to return spectrum divided by integration time: spectrum/tint
badpix: pixels with bad values to be replaced by interpolated value from neighboring pixels
using the same badpix indices for each of the 3 spectra
'''
if rclip==0: rclip=None
if trim==0: trim = None
if verbose:
print('STO_v34 Version: ', __version__,' of ', __date__)
self.version = __version__
self.date = __date__
with fits.open(ifile) as hl:
#hl = fits.open(ifile)
self.hd0 = hl[0].header.copy()
self.dd0 = hl[0].data
self.hd1 = hl[1].header.copy()
self.dd1 = hl[1].data.copy()
self.cols = hl[1].columns
#print(type(self.hd1))
#print(type(self.dd1))
hl.close()
# get the coordinates, update 3/18/2019
# the [NII] R.A.s did not include the cos(dec)-term. => added 3/19/2019
# in future it should be possible to just use the WCS, but currently it produces the wrong position!
ndec1 = (np.float(self.hd1['UDP_DEC'])+np.float(self.dd1['CDELT3'][0])/3600.)
ndec2 = (np.float(self.hd1['UDP_DEC'])+np.float(self.dd1['CDELT3'][1])/3600.)
pos0 = SkyCoord((np.float(self.hd1['UDP_RA'])+np.float(self.dd1['CDELT2'][0])/3600./np.cos(np.radians(ndec1)))*u.deg, ndec1*u.deg, frame='icrs')
pos1 = SkyCoord((np.float(self.hd1['UDP_RA'])+np.float(self.dd1['CDELT2'][1])/3600./np.cos(np.radians(ndec2)))*u.deg, ndec2*u.deg, frame='icrs')
pos2 = SkyCoord( np.float(self.hd1['UDP_RA'])*u.deg, np.float(self.hd1['UDP_DEC'])*u.deg, frame='icrs')
self.pos = [pos0.galactic, pos1.galactic, pos2.galactic]
self.spec = self.dd1['DATA'] * u.count
# check first if the file has already been processed and the pixel mask was added to the file:
try:
self.mask = self.dd1['MASK']
#print('mask exists.')
except:
# create a empty mask => but mask needs to be filled with header comment info
self.mask = np.zeros(self.spec.shape, dtype=np.int32)
# fill the new mask with the header information
self.mask = addHeaderBadPixels(self.hd1['Comment'], self.mask)
#print('added mask.')
self.n_pix = self.dd1['MAXIS1'][0]
self.n_row = self.hd1['NAXIS2']
self.vv = np.ndarray((self.n_row,self.n_pix), dtype=np.float) * u.km / u.s
for i in range(self.n_row):
self.n_pixl = self.dd1['MAXIS1'][i] # number of pixels in spectrum
self.vv[i,:self.n_pixl] = (np.float(self.hd1['CRVAL1']) + (1 + np.arange(self.n_pixl) - self.dd1['CRPIX1'][i]) * self.dd1['CDELT1'][i]) * u.km / u.s
self.vv/= 1000.0
self.hd1['TUNIT3'] = 'km/s'
if tnorm:
tint = np.float(self.hd1['OBSTIME'])
self.spec = self.spec/tint / u.s
self.hd1['TUNIT24'] = 'counts/sec'
# if verbose:
# # # 3076 4014 REF 33 CII_2 2.226 322.99963 1.90015 -50.0 36.8 17.585350 -0.00370 -57.09700 2760264
# print(' scan obsid Type ot_row line obstime l b vLSR v0 syn_CII biasvolt biascurr totpower')
# print('%5s %5s %5s %5s %8s %7.3f %10.5f %10.5f %6.1f %6.1f %10.6f %8.5f %8.3f %8.0f'%(
# hd1['SCAN'], hd1['OBSID'], hd1['TYPE'], hd1['OT_ROW'], dd1['TELESCOP'],
# np.float(hd1['OBSTIME']), pos.galactic.l.deg, pos.galactic.b.deg,
# np.float(hd1['VELOCITY'])/1000., vv[0], np.float(hd1['SYN_C_II']),
# np.float(dd1['BIASVOLT'][lin]),np.float(dd1['BIASCURR'][lin]),np.float(dd1['TOTPOWER'][lin])))
if trim!=None:
# cut away the outmost pixels - but better to use rclip below!
vv = vv[:,trim:-trim]
spec = spec[:,trim:-trim]
if rclip!=None:
# replace the values of the outermost pixels [0,rclip-1] and [-rclip,last_pixel] with the value
# of the first inside pixel [rclip] and [-rclip-1] respectively
# to remove edge effects, but keep the number of pixels the same
if verbose: print('STO2_v1: clipping edge values....', rclip)
for rcl in range(rclip):
self.spec[:,rcl] = self.spec[:,rclip]
self.spec[:,-rcl-1] = self.spec[:,-rclip]
self.badpix = None
self.badpix0 = None
self.badpix1 = None
self.badpix2 = None
# the following actions need a logging in the mask array!
if badpix!=None:
# replace the pixel with indices in badpix with interpolated values
# repPix works only on 1-D spectra, call it for each of the 3 spectra
self.badpix = badpix
for i in range(3):
self.spec[i,:] = repPix1D(self.spec[i,:], badpix)
if badpix0!=None:
# replace the pixel with indices in badpix with interpolated values in line 0
# repPix works only on 1-D spectra, call it for each of the 3 spectra
self.badpix0 = badpix0
self.spec[0,:] = repPix1D(self.spec[0,:], badpix0)
if badpix1!=None:
# replace the pixel with indices in badpix with interpolated values in line 0
# repPix works only on 1-D spectra, call it for each of the 3 spectra
self.badpix1 = badpix1
self.spec[1,:] = repPix1D(self.spec[1,:], badpix1)
if badpix2!=None:
# replace the pixel with indices in badpix with interpolated values in line 0
# repPix works only on 1-D spectra, call it for each of the 3 spectra
self.badpix2 = badpix2
self.spec[2,:] = repPix1D(self.spec[2,:], badpix2)
def getDataAsFitsBlocks(self):
"""
return the data in the file as FITS block: data block, header block
"""
return self.dd1, self.hd1
def getData0AsFitsBlocks(self):
"""
return the data in the file as FITS block: data block, header block
"""
return self.dd0, self.hd0
def getDataAsArrays(self):
"""
return a numpy structured array containing all data
"""
return np.rec.array([self.vv, self.spec],
dtype=[('velo', 'f4', (self.n_row, self.n_pixl)),('spec', 'f4', (self.n_row, self.n_pixl))])
def getDataLine(self, lin):
"""
Function to retrieve STO2 spectra for a single line:
Input:
lin: line number (0: [NII] pixel 1, 1: [NII] pixel 2, and 2: [CII])
Output:
velocity array of line, intensity array of line
"""
return self.vv[lin,:], self.spec[lin,:]
def getData(self):
"""
Function to retrieve STO2 spectra for all lines:
Input:
none
Output:
velocity array, intensity array
"""
return self.vv, self.spec
def getDataMask(self):
"""
Function to retrieve STO2 data mask for all lines:
Input:
none
Output:
32-bit integer mask array
"""
return self.mask
def getDataMaskLine(self, line):
"""
Function to retrieve STO2 data mask for all lines:
Input:
line index (0: NII line 1, 1: NII line2, or 2: CII)
Output:
32-bit integer mask array
"""
return self.mask[line,:,]
def getEmptyMask(self):
"""
creates an empty mask to accompany the Line data
the mask elements are 16-bit unsigned integers
"""
return np.zeros(np.squeeze(self.spec[0,:]).shape, dtype=np.uint16)
def getPos(self):
"""
returns array of astropy SkyCoord objects (Galactic coordinates)
"""
return self.pos
def getPosLine(self, lin):
"""
returns astropy SkyCoord object (Galactic coordinates)
"""
return (self.pos)[lin]
def getHeader0(self):
"""
return full FITS header 0
"""
return self.hd0
def getHeader(self):
"""
return full FITS header of observation (1st Extension)
"""
return self.hd1
def getRawData(self):
"""
return raw data table for all lines
Table Keywords: MAXIS1, CRPIX1, CDELT1, CDELT2, CDELT3, BiasVolt, BiasCurr, TotPower, IFPower,
BiasPot, PIDstate, LNABias, LNACurr, BeamFac, TSYS, Trx, OutRange, IntTime,
NumOff, Telescop, Line, RESTFREQ, IMAGFREQ, DATA
"""
return self.dd1
def getRawData0(self):
"""
return (empty) raw data for primary extension
"""
return self.dd0
def getDataColumns(self):
"""
return (empty) raw data for primary extension
"""
return self.cols
def getTint(self):
"""
return the integration time for this observation
"""
return np.array(self.hd1['OBSTIME'], dtype=np.float)
def getUnixTime(self, units=False):
"""
return the time of observation in seconds since epoch
"""
if units: return np.array(self.hd1['UNIXTIME'], dtype=np.float) * u.s
return np.array(self.hd1['UNIXTIME'], dtype=np.float)
def getSpecUnit(self, apflag=True):
"""
return unit of spectrum as string
apflag: convert string to astropy units (default: True)
"""
spunit = self.hd1['TUNIT24'].replace('counts','count').replace('sec','s')
print(spunit)
if apflag: spunit = u.Unit(spunit)
return spunit
def getVeloUnit(self, apflag=True):
"""
return units of velocity axis as string
apflag: convert string to astropy units (default: True)
"""
vunit = self.hd1['TUNIT3']
if apflag: vunit = u.Unit(vunit)
return vunit
def getVlsr(self):
"""
return the v_LSR (with units)
"""
return np.array(self.hd1['VELOCITY'], dtype=np.float) * u.m/u.s
def getDataVersion(self):
"""
return the release version of the data
"""
return np.array(self.hd1['LEVEL0'], dtype=np.float)
def getLineSpecie(self):
"""
return the observed species
"""
return self.dd1['Line']
def getLineID(self):
"""
return the pixel identifier (for all 3 lines)
"""
return self.dd1['TELESCOP']
def getRestFrequencies(self):
"""
return the rest frequency array (for all 3 lines)
"""
return np.array(self.dd1['RESTFREQ'], dtype=np.float)
def getBiasCurr(self):
"""
return the bias current array (for all 3 lines)
"""
return np.array(self.dd1['BiasCurr'], dtype=np.float)
def getBiasVolt(self):
"""
return the bias voltage array (for all 3 lines)
"""
return np.array(self.dd1['BiasVolt'], dtype=np.float)
def getTotalPower(self):
"""
return the total power array (for all 3 lines)
"""
return np.array(self.dd1['TotPower'], dtype=np.float)
def getIFPower(self):
"""
return the IF power array (for all 3 lines)
"""
return np.array(self.dd1['IFPower'], dtype=np.float)
def getBiasPot(self):
"""
return the Bias Pot (for all 3 lines)
"""
return np.array(self.dd1['BiasPot'], dtype=np.float)
def getPIDState(self):
"""
return the PID state (for all 3 lines)
"""
return np.array(self.dd1['PIDState'], dtype=np.float)
def getLNABias(self):
"""
return the LNA bias (for all 3 lines)
"""
return np.array(self.dd1['LNABias'], dtype=np.float)
def getLNACurr(self):
"""
return the LNA current (for all 3 lines)
"""
return np.array(self.dd1['LNACurr'], dtype=np.float)
def getBeamFac(self):
"""
return the beam factor (for all 3 lines)
"""
return np.array(self.dd1['BeamFac'], dtype=np.float)
def getTsys(self):
"""
return the system noise temperature
(set to zero in Level <1)
"""
return np.array(self.dd1['TSYS'], dtype=np.float)
def getTrx(self):
"""
return the receiver temperature (for all 3 lines)
"""
return np.array(self.dd1['Trx'], dtype=np.float)
def getOutRange(self):
"""
return the out-of-range status (for all 3 lines)
"""
return np.array(self.dd1['OutRange'], dtype=np.int)
def getIntTime(self):
"""
return the integration time (for all 3 lines)
"""
return np.array(self.dd1['IntTime'], dtype=np.float)
def getNumOff(self):
"""
return the number of OFF observations(?) (for all 3 lines)
"""
return np.array(self.dd1['NumOff'], dtype=np.int)
def getNPix(self):
"""
return the number of OFF observations(?) (for all 3 lines)
"""
return np.array(self.dd1['MAXIS1'], dtype=np.int)
def getImageFrequencies(self):
"""
return the image frequencies (for all 3 lines)
"""
return np.array(self.dd1['IMAGFREQ'], dtype=np.float)
def getDataLevel(self):
"""
return the Data Level
"""
return self.hd1['Level0']
def getScanNumber(self):
"""
return the Data Level
"""
return np.int(self.hd1['SCAN'])
def getObsID(self):
"""
return the Data Level
"""
return np.int(self.hd1['OBSID'])
def getHistory(self):
"""
get the History information in the Extension 1 header
"""
hist = self.hd1['HISTORY']
return hist
def getComments(self):
"""
get the History information in the Extension 1 header
"""
cmnt = self.hd1['Comment']
return cmnt
def getL05Repair(self, line, chan=1, maskflag=False):
"""
function to retrieve the information for the Level 0.5 despiked pixels
line: 'C+', 'N+', or 'O I'
chan: only used for 'N+' since we had two pixels: 1 and 2
To do: test for robustness since it evaluates the comment section of the header;
don't know what else is in this section.
V. Tolls, SAO; 8/16/2018
"""
if line=='C+':
brd=3
inp=1
elif line=='N+':
brd=1
inp=1
elif line=='O I':
brd=2
inp=1
cmnt = self.hd1['Comment']
com = np.genfromtxt(cmnt, dtype=None, names=['brd', 'inp', 'chan', 'oldv', 'newv', 'line', 'pixel'],
delimiter=[3,3,6,12,13,5,5], skip_header=2)
sel = np.where((com['brd']==brd)&(com['inp']==inp))
if maskflag:
mask = self.getLineMask()
#for ii in com['chan'][sel]: mask[ii] = 1
mask[com['chan'][sel]] = 2
return np.array(com['chan'][sel]), mask
else:
return np.array(com['chan'][sel])
##################################################################################
# misc. functions to manipulate spectra .... some might need improvement
##################################################################################
def writeSTO2(ofile, dat, header):
"""
Writing STO-2 style Level 1 FITS files that should be CLASS readable
ofile: name of output file; should end in .fits
dat: data structure for output data; updated version of the Level 0 data structure
header: header for file; should be updated version of Level 0 header
with respect to Lev ):
updated header: OFFRA, OFFDEC, TCAL
added header: CALSCAN1, OFFSCAN1, CALSCAN2, OFFSCAN2, SEQRASTER, HOTSEQ1, HOTSEQ2
updated bin table: CDELT2, CDELT3, TSYS, INTTIME (->0 for OI), NUMOFF, DATA (now calibrated)
"""
# still needs development .....
fits.writeto(ofile, dat, header)
def cleanSpec1D(dat, thresh = 500., verbose=False, getReport=False):
"""
Remove spikes from a 1-D, single line spectrum
Better use cleanSpecPeak() below!
"""
ndat = dat.copy()
fdat = butter_lowpass_filtfilt(ndat, 100, 10000)
# try to clip the bad data
Ddat = ndat - fdat
tag = mlab.find(np.abs(Ddat)<thresh)
if verbose: print('median ndat/fdat/Ddat: ', np.median(ndat), np.median(fdat), np.median(Ddat))
if verbose: print('tag: ', tag.tolist())
report = []
if tag.shape[0]>0:
J = find(diff(tag)>1)
nn = ndat.shape[0]
for K in split(tag, J+1):
# this is the array of data to be changed!
# we are assuming isolated spikes and not spike city
# the ends can be trimmed!
if verbose: print('affected pixels: ', K)
if ((K.min()>5)&(K.max()<nn-5)&(np.mean(Ddat[K])>thresh*0.7)):
lw = np.arange(K.min()-4,K.min(),1)
up = np.arange(K.max()+1,K.max()+5,1)
updw = np.hstack([lw,up])
# interpolate
it = np.interp(K,updw,dat[updw])
ndat[K] = it
for kk in K:
rstr = 'cleaned pixel: %i %12.4f %14.2f'%(kk, ndat[kk], dat[kk])
report.append(rstr)
if verbose: print('updw: ', updw, dat[updw])
if verbose: print('split: ', K, ndat[K], Ddat[K], it)
if getReport: return ndat, report
else: return ndat
def cleanSpecPeak1D(dat, thresh = 1E9, verbose=False, getReport=False, bflag=True, boff=2, vv=None, imethod='cubic'):
"""
Remove spikes from a 1-D, single line spectrum
getReport: return a short message about what was cleaned.
bflag: also clean the next pixel around the spike
boff: number of pixel to be taken off for wings (besides central pixel of spike)
vv: provide velocity information (velocity of affected pixels returned in report)
method: use a cubic spline interpolation (scipy.interpolation) or just linear interpolation (numpy.interp)
"""
ndat = dat.copy()
unit = ndat.unit
ndat = ndat.value
# try to clip the bad data
tag = np.argwhere(np.abs(ndat)>thresh)
if verbose: print('tag: ', tag.tolist())
# correcting neighboring pixels
if bflag:
brange = np.arange(boff) + 1
report = np.array([(0., 0., '', -1, 0.0, 0.0, 0.0)], dtype=[('scan', 'i4'),('obsid', 'i4'),('type', 'S10'),('pixel', 'i4'),('nval', 'f8'), ('val', 'f8'), ('velo', 'f4')])
nrep = 0
if tag.shape[0]>0:
J = find(diff(tag)>1)
nn = ndat.shape[0]
for K in split(tag, J+1):
# this is the array of data to be changed!
# we are assuming isolated spikes and not spike city
# the ends can be trimmed!
if verbose: print('affected pixels: ', K)
if ((K.min()>7)&(K.max()<nn-7)&(np.mean(ndat[K])>thresh*0.9)):
if bflag:
K = np.hstack((K.min()-brange,K))
K = np.hstack((K,K.max()+brange))
# interpolate
if imethod=='cubic':
lw = np.arange(K.min()-6,K.min(),1)
up = np.arange(K.max()+1,K.max()+7,1)
updw = np.hstack([lw,up])
tck = interpolate.splrep(updw, dat[updw], s=0)
it = interpolate.splev(K, tck, der=0)
else:
lw = np.arange(K.min()-4,K.min(),1)
up = np.arange(K.max()+1,K.max()+5,1)
updw = np.hstack([lw,up])
it = np.interp(K,updw,dat[updw])
if verbose: print('updw: ', updw, dat[updw])
if verbose: print('split: ', K, ndat[K], it)
ndat[K] = it
for kk in K:
if vv==None:
#rstr = 'cleaned pixel: %i %12.4f %14.2f %7.2f'%(kk, ndat[kk], dat[kk], 9999.0)
rsa = np.array([(0., 0., '', kk, ndat[kk], dat[kk].value, 9999.0)], dtype=[('scan', 'i4'),('obsid', 'i4'),('type', 'S10'),('pixel', 'i4'),('nval', 'f8'), ('val', 'f8'), ('velo', 'f4')])
nrep += 1
else:
#rstr = 'cleaned pixel: %i %12.4f %14.2f %7.2f'%(kk, ndat[kk], dat[kk], vv[kk])
rsa = np.array([(0., 0., '', kk, ndat[kk], dat[kk].value, vv[kk].value)], dtype=[('scan', 'i4'),('obsid', 'i4'),('type', 'S10'),('pixel', 'i4'),('nval', 'f8'), ('val', 'f8'), ('velo', 'f4')])
nrep += 1
report = np.vstack((report,rsa))
if nrep > 0: report = report[1:]
else: report=None
if getReport: return ndat*unit, report
else: return ndat*unit
def removeSpike1D(idat, thresh):
"""
simple spike removal - experimental!
Input:
thresh: threshold to determine pixels values to be replaced with median
Ouput:
modified data array
"""
dat = idat.copy()
med = np.median(dat)
sel = np.where(dat>med+thresh)
if len(sel[0])>0: dat[sel[0]] = med
sel = np.where(dat<med-thresh)
if len(sel[0])>0: dat[sel[0]] = med
return dat
def removeRawSpike1D(ixdat, iydat, thresh=1E6, nwpix=2, borderpix = 20, method='smooth', retbadpix=False, window_width=37,
verbose=False, tbad=None, pbad=False, rmethod='replace'):
"""
simple spike removal in raw spectrum - experimental!
The spikes are very high!
The bad pixels are determined using the scipy median filter "medfilt" with a window_width of 5 (changeable).
The interpolation is done using function repPixInterp, which uses the scipy interpolation functionality
Input:
thresh: threshold to determine pixels values to be replaced (default: 1E6)
borderpix: number of pixels of either side of spike used for interpolation (not yet implemented)
method: method could be smoothing 'smooth' or median filtering 'medfilt'
nwpix: number of bad pixels in the wings to be repaired (minimum should be 1, default is 2)
retbadpix: if True, return the bad pixel array
window_width: scalar size of median window width; should be odd number
rmethod: 'replace, 'nan'
Ouput:
modified data array
"""
from scipy.signal import medfilt
# remove the units (if present), which will be added back later
try:
xdat = ixdat.value.copy()
ydat = iydat.value.copy()
except:
xdat = ixdat.copy()
ydat = iydat.copy()
rc = nwpix
if method=='medfilt': out = medfilt(ydat, kernel_size=window_width)
else: out = smoothData(ydat, cutoff=10, fs=100)
badpix = np.squeeze(np.argwhere(np.abs(ydat-out)>thresh))
if pbad:
print(np.abs(ydat-out)[badpix])
print(thresh[badpix])
if np.isscalar(badpix): badpix = np.array(badpix)
if verbose: print(method, badpix, badpix.size)
if np.all(tbad)!=None:
if badpix.size>0:
#print('shape: ', tbad.shape, badpix.shape)
#print('arrays: ', tbad, badpix)
badpix = np.hstack((badpix,tbad))
badpix = np.sort(badpix)
badpix = np.unique(badpix)
else:
badpix = tbad
#if verbose: print('rep badpix: ', np.squeeze(badpix))
if badpix.size>0:
if badpix.size==1:
bp = np.arange(badpix-rc, badpix+rc+1, 1)
else:
bp = np.arange(badpix[0]-rc, badpix[0]+rc+1, 1)
for i in range(1,badpix.size):
bp = np.vstack([bp, np.arange(badpix[i]-rc, badpix[i]+rc+1, 1)])
bp = np.unique(bp)
bp = bp[np.where((bp>=0) & (bp<xdat.size))]
# rather than reversing the order of the array, we multiply the (unused) x-array with -1., which essentially reverses the
# order as well, but with less impact.
if np.all(np.diff(xdat)<0): fac = -1.
else: fac = 1.
#if verbose: print('rep: ', np.squeeze(bp))
if rmethod=='nan':
if bp.size>0: ydat[bp] = np.nan
elif rmethod=='replace':
if bp.size>0: ydat = repPixInterp(fac*xdat, ydat, bp)
else:
if bp.size>0: ydat = repPixInterp(fac*xdat, ydat, bp)
else:
bp=np.empty(shape=(0))
try:
xdat = xdat*ixdat.unit
ydat = ydat*iydat.unit
except:
pass
if retbadpix: return xdat, ydat, bp
else: return xdat, ydat
from itertools import groupby, cycle
def groupSequence(l):
temp_list = cycle(l)
next(temp_list)
groups = groupby(l, key = lambda j: j + 1 == next(temp_list))
for k, v in groups:
if k:
yield tuple(v) + (next((next(groups)[1])), )
def addHeaderBadPixels(icom, imask):
"""
Add the bad pixels listed in the header in the comment section as determined by scooper, the Level 0 to Level 0.5 program.
The flag value for bad pixels here is 2.
However, the header information should have been added when creating the mask! If it is added again, it should not change
the already existing information in case the functions has been invoked more than once.
"""
nmask = imask.copy()
try:
#icom = hd1['COMMENT']
com = np.genfromtxt(icom, dtype=None, names=['brd', 'inp', 'chan', 'oldv', 'newv', 'line', 'pixel'],
delimiter=[3,3,6,12,13,5,5], skip_header=2)
# bit 1 => add 2 to mask: originally despiked in scooper
# first NII line, second NII line, and [OI] line
nmask[0,com['chan'][np.where((com['brd']==1)&(com['inp']==1))]-1] = np.bitwise_or(nmask[0,com['chan'][np.where((com['brd']==1)&(com['inp']==1))]-1], 2)
nmask[1,com['chan'][np.where((com['brd']==1)&(com['inp']==2))]-1] = np.bitwise_or(nmask[1,com['chan'][np.where((com['brd']==1)&(com['inp']==2))]-1], 2)
nmask[2,com['chan'][np.where((com['brd']==3)&(com['inp']==1))]-1] = np.bitwise_or(nmask[2,com['chan'][np.where((com['brd']==3)&(com['inp']==1))]-1], 2)
if verbose: print('2: ', com['chan'][np.where((com['brd']==3)&(com['inp']==1))]-1)
except:
pass
return nmask
def repSpike1D(ispec1, imask1, sclip=0, eclip=0, thresh = 10000, near=1, bit2flag=False,
verbose=False, badpix=None, method='cubicspline'):
"""
This function is still experimental, hence the many print statements for debugging.
This is the latest cleaning function, based on a several step cleaning procedure.
The first step removes and replaces the large spikes and tries to interpolate the missing
values. There is an option for replacing the first sclip pixels with the value of pixel[sclip]
and the last eclip pixels with the value of pixel[eclip]. These ranges were almost impossible
to clean and repair for [NII], and this methods seems to be the best option since the data are
useless in any case.
The second step removes smaller spikes that have an amplitude larger than thresh compared
to the adjacent pixels. Here are 2 option, using the median filtered difference or the ALS difference
of old minus filtered spectrum? Only positive spikes are removed. Only median filer used right now.
Compare to subroutine setMask() below for changes in flagging:
Meaning of mask bits (might change slightly in future by adding new values, 16 bits available):
bit 0 => add 1 to mask: all spectral data values are NaNs or zeros
bit 1 => add 2 to mask: spikes listed in comment area of raw (level 0.5+) files and badpix pixels
bit 2 => add 4 to mask: new despiking, e.g., residuals of transmission LO interference or so
bit 3 => add 8 to mask: end of spectrum outliers clipped spectral elements
bit 4 => add 16 to mask: forced bad pixels provided with badpixel
Inputs:
ispec1: "dirty" spectrum
imask1: initial mask, most likely empty
sclip: number of pixels to be replaced at beginning of spectrum
eclip: number of pixels to be replaced at end of spectrum
thresh: threshold for identifying smaller spikes
(value independent of absolute pixel value; 10000 too aggressive?)
near: number of adjacent pixels also to be masked (default: 1)
bit2flag: True: interpolate the value of pixels masked in bit 2 (a.k.a., the pixels identified in the file header comment section)
False: ignore...
badpix: user selected additional pixels to be mask (default is None)
example is: Eta Car 2, all pixels between 479 and 491 should be masked by default since bad
method: method for interpolation: splrep or cubicspline
Function created after test notebook: notebooks/STO2/EtaCar/EtaCar2_spike_cleaning_single_spectrum_v4_testing.ipynb
initially created: 8/21/2019, VTO
"""
### Step 1
# identify the large spikes and set mask
# set bit 3 for "end of spectrum outliers"
dtt = type(ispec1)
if type(ispec1)==type(np.zeros(2)):
spec1 = ispec1.copy()
else:
spec1 = ispec1.value
mask1 = imask1.copy()
if verbose: print(spec1.shape)
# Step 1
# clip ends of scan first in case there are major spikes.
# set bit 3
if sclip>0:
#sclip = 70
# replace the beginning sclip pixels with the value in the edge pixel
if spec1[sclip] < 0.5E9:
spec1[:sclip] = np.ones(sclip) * spec1[sclip]
# set the mask
mask1[:sclip] = np.bitwise_or(mask1[:sclip], 8)
else:
print('Error. repSpike1D: spectrum in pixel sclip is spike.')
if np.abs(eclip)>0:
#eclip = -100
# replace the eclip pixels at end of scan with the value at its edge
if spec1[eclip] < 0.5E9:
spec1[eclip:] = np.ones(np.abs(eclip)) * spec1[eclip]
# set the mask
mask1[eclip:] = np.bitwise_or(mask1[eclip:], 8)
else:
print('Error. repSpike1D: spectrum in pixel eclip is spike.')
# Step 2
# Identify and mask major spikes with counts over 5E8, set to nan.
# set bit 2 for "new de-spiking"
args = np.argwhere(spec1 > 0.5e9)
for arg in args:
spec1[arg] = np.nan
mask1[arg] = np.bitwise_or(mask1[arg], 4)
if arg<spec1.size-1:
spec1[arg+1] = np.nan
mask1[arg+1] = np.bitwise_or(mask1[arg+1], 4)
if arg<spec1.size-2:
spec1[arg+2] = np.nan