-
Notifications
You must be signed in to change notification settings - Fork 3
/
NIRES_rotation_tool.py
525 lines (420 loc) · 18.8 KB
/
NIRES_rotation_tool.py
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
import matplotlib.pyplot as plt
import numpy as np
from astropy.coordinates import SkyCoord
from astroquery.skyview import SkyView
import astropy.units as u
from astropy.wcs import WCS
from astropy.io import ascii as asci
from matplotlib.patches import Rectangle, Circle
from matplotlib import transforms
from matplotlib.widgets import Slider, Button
import sys, glob, argparse
from PIL import Image
#############Define the matplotlib slider tool
#Vertical Slider bar from https://stackoverflow.com/questions/25934279/add-a-vertical-slider-with-matplotlib
#Update when this is supported by matplotlib
from matplotlib.widgets import AxesWidget
import six
class VertSlider(AxesWidget):
"""
A slider representing a floating point range.
For the slider to remain responsive you must maintain a
reference to it.
The following attributes are defined
*ax* : the slider :class:`matplotlib.axes.Axes` instance
*val* : the current slider value
*hline* : a :class:`matplotlib.lines.Line2D` instance
representing the initial value of the slider
*poly* : A :class:`matplotlib.patches.Polygon` instance
which is the slider knob
*valfmt* : the format string for formatting the slider text
*label* : a :class:`matplotlib.text.Text` instance
for the slider label
*closedmin* : whether the slider is closed on the minimum
*closedmax* : whether the slider is closed on the maximum
*slidermin* : another slider - if not *None*, this slider must be
greater than *slidermin*
*slidermax* : another slider - if not *None*, this slider must be
less than *slidermax*
*dragging* : allow for mouse dragging on slider
Call :meth:`on_changed` to connect to the slider event
"""
def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f',
closedmin=True, closedmax=True, slidermin=None,
slidermax=None, dragging=True, valstep = None, **kwargs):
"""
Create a slider from *valmin* to *valmax* in axes *ax*.
Additional kwargs are passed on to ``self.poly`` which is the
:class:`matplotlib.patches.Rectangle` which draws the slider
knob. See the :class:`matplotlib.patches.Rectangle` documentation
valid property names (e.g., *facecolor*, *edgecolor*, *alpha*, ...).
Parameters
----------
ax : Axes
The Axes to put the slider in
label : str
Slider label
valmin : float
The minimum value of the slider
valmax : float
The maximum value of the slider
valinit : float
The slider initial position
label : str
The slider label
valfmt : str
Used to format the slider value, fprint format string
closedmin : bool
Indicate whether the slider interval is closed on the bottom
closedmax : bool
Indicate whether the slider interval is closed on the top
slidermin : Slider or None
Do not allow the current slider to have a value less than
`slidermin`
slidermax : Slider or None
Do not allow the current slider to have a value greater than
`slidermax`
dragging : bool
if the slider can be dragged by the mouse
valstep : float, optional, default: None
If given, the slider will snap to multiples of `valstep`.
"""
AxesWidget.__init__(self, ax)
self.valmin = valmin
self.valmax = valmax
self.val = valinit
self.valinit = valinit
self.valstep = valstep
self.poly = ax.axhspan(valmin, valinit, 0, 1, **kwargs)
self.hline = ax.axhline(valinit, 0, 1, color='r', lw=1)
self.valfmt = valfmt
ax.set_xticks([])
ax.set_ylim((valmin, valmax))
ax.set_yticks([])
ax.set_navigate(False)
self.connect_event('button_press_event', self._update)
self.connect_event('button_release_event', self._update)
if dragging:
self.connect_event('motion_notify_event', self._update)
self.label = ax.text(0.5, 1.03, label, transform=ax.transAxes,
verticalalignment='center',
horizontalalignment='center',
fontsize = 18)
self.valtext = ax.text(0.5, -0.03, valfmt % valinit,
transform=ax.transAxes,
verticalalignment='center',
horizontalalignment='center',
fontsize = 18)
self.cnt = 0
self.observers = {}
self.closedmin = closedmin
self.closedmax = closedmax
self.slidermin = slidermin
self.slidermax = slidermax
self.drag_active = False
def _update(self, event):
"""update the slider position"""
if self.ignore(event):
return
if event.button != 1:
return
if event.name == 'button_press_event' and event.inaxes == self.ax:
self.drag_active = True
event.canvas.grab_mouse(self.ax)
if not self.drag_active:
return
elif ((event.name == 'button_release_event') or
(event.name == 'button_press_event' and
event.inaxes != self.ax)):
self.drag_active = False
event.canvas.release_mouse(self.ax)
return
val = event.ydata
if self.valstep:
val = np.round((val - self.valmin)/self.valstep)*self.valstep
val += self.valmin
if val <= self.valmin:
if not self.closedmin:
return
val = self.valmin
elif val >= self.valmax:
if not self.closedmax:
return
val = self.valmax
if self.slidermin is not None and val <= self.slidermin.val:
if not self.closedmin:
return
val = self.slidermin.val
if self.slidermax is not None and val >= self.slidermax.val:
if not self.closedmax:
return
val = self.slidermax.val
self.set_val(val)
def set_val(self, val):
xy = self.poly.xy
xy[1] = 0, val
xy[2] = 1, val
self.poly.xy = xy
self.valtext.set_text(self.valfmt % val)
if self.drawon:
self.ax.figure.canvas.draw_idle()
self.val = val
if not self.eventson:
return
for cid, func in six.iteritems(self.observers):
func(val)
def on_changed(self, func):
"""
When the slider value is changed, call *func* with the new
slider position
A connection id is returned which can be used to disconnect
"""
# print('foo')
cid = self.cnt
self.observers[cid] = func
self.cnt += 1
return cid
def disconnect(self, cid):
"""remove the observer with connection id *cid*"""
try:
del self.observers[cid]
except KeyError:
pass
def reset(self):
"""reset the slider to the initial value if needed"""
if (self.val != self.valinit):
self.set_val(self.valinit)
###########Function to download DSS image
def download_DSS(coord, size = 800, image_server = 'DSS2 Red'):
"""
Given a SkyCoord object, download a DSS image from the given size using
a given server. The default is DSS2 Red to have all-sky coverage in the
band closest to the guider.
"""
img = SkyView.get_images(position=coord,survey=[image_server]\
,pixels='%s,%s'%(str(size), str(size)),
coordinates='J2000',grid=True,gridlabels=True)
return img
###########Main function to bring up the plot, given the coordinates
def plot_NIRES_fov(coords, coords_offset, target_name):
"""
This function takes the coordinates of the SN and offset star, download the DSS image
and make a plot with the slit locations and the off-axis guider FoV.
The plot will have a slider for the user to adjust the PA. Once a PA with a guide star in
the guider FoV at both the SN and the offset position is found, click 'Save' to save the
finder and return the PA.
Input:
- coords: SkyCoord of the SN
- coords_offset: SkyCoord of the offset star
- target_name: Name of the target. Used to label plot and name output finder.
"""
#Download the DSS image
img = download_DSS(coords)
#Get the wcs object from the image
wcs = WCS(img[0][0].header)
#Define the plot
plt.figure(figsize = (8,8))
ax = plt.subplot(projection = wcs)
ax.imshow(img[0][0].data, origin = 'lower')
#picture coordinates of source and offset star
x, y = wcs.all_world2pix([coords.ra.deg], [coords.dec.deg], 1)
if coords_offset is not None:
xo, yo = wcs.all_world2pix([coords_offset.ra.deg], [coords_offset.dec.deg], 1)
#dimension of slit
dx = 0.55/2 #0.55"
dy = 18/2 #18"
#Define slit for source and offset star
slit = Rectangle((x[0]-dx, y[0]-dy), 2*dx, 2*dy, color = 'w', lw = 0.5)
if coords_offset is not None:
slit_o = Rectangle((xo[0]-dx, yo[0]-dy), 2*dx, 2*dy, color = 'r', lw = 0.5)
#Define guider field for source and offset star
guider_dx = 3.44*60*1.1 #...shouldn't this number be known? This is the guider offset of around 3.8 arcmin
# guider_dx = 3.44*60 #From the documentation
guider_dim = 1.8*60 #1.8 arcmin. Yep, it's small
#Rectangle objects representing the guider FoV for the SN and the offset star.
Guider_rec = Rectangle((x[0]+guider_dx-guider_dim/2, y[0]-guider_dim/2), guider_dim,guider_dim, \
color = 'w', fill = False, lw = 0.3)#, \
Guider = Circle((x[0]+guider_dx, y[0]), guider_dim/2, \
color = 'w', fill = False, lw = 0.5)#, \
#transform = transforms.Affine2D.rotate_around(x = x[0],y = y[0],theta = np.radians(PA)))
if coords_offset is not None:
Guider_o_rec = Rectangle((xo[0]+guider_dx-guider_dim/2, yo[0]-guider_dim/2), guider_dim,guider_dim, \
color = 'r', fill = False, lw = 0.3)
Guider_o = Circle((xo[0]+guider_dx, yo[0]), guider_dim/2, \
color = 'r', fill = False, lw = 0.5)#, \
#Define guider circle for source and offset star
Guider_path_in = Circle((x[0], y[0]), guider_dx-guider_dim/2, fill = False, color = 'w', lw = 0.5,ls = '--')
Guider_path_out = Circle((x[0], y[0]), guider_dx+guider_dim/2, fill = False, color = 'w', lw = 0.5,ls = '--')
if coords_offset is not None:
Guider_o_path_in = Circle((xo[0], yo[0]), guider_dx-guider_dim/2, fill = False, color = 'r', lw = 0.5, ls = '--')
Guider_o_path_out = Circle((xo[0], yo[0]), guider_dx+guider_dim/2, fill = False, color = 'r', lw = 0.5, ls = '--')
#ROTATE
# for i in range(0,370,30):
PA = 0
rot = transforms.Affine2D().rotate_around(x[0], y[0],np.radians(PA))+ ax.transData
if coords_offset is not None:
rot_o = transforms.Affine2D().rotate_around(xo[0], yo[0],np.radians(PA))+ ax.transData
Guider.set_transform(rot)
Guider_rec.set_transform(rot)
slit.set_transform(rot)
if coords_offset is not None:
Guider_o.set_transform(rot_o)
Guider_o_rec.set_transform(rot_o)
slit_o.set_transform(rot_o)
#Add all these overlays to the DSS image
ax.add_patch(slit)
ax.add_patch(Guider)
ax.add_patch(Guider_rec)
if coords_offset is not None:
ax.add_patch(slit_o)
ax.add_patch(Guider_o)
ax.add_patch(Guider_o_rec)
ax.add_patch(Guider_path_in )
ax.add_patch(Guider_path_out)
if coords_offset is not None:
ax.add_patch(Guider_o_path_in )
ax.add_patch(Guider_o_path_out)
ax.tick_params(labelsize = 14)
ax.set_xlabel('RA', fontsize = 16)
ax.set_ylabel('Dec', fontsize = 16)
ax.set_title('%s, PA = %d deg'%(target_name,PA), fontsize = 18)
#Slider to adjust the PA
plt.subplots_adjust(right=0.8, bottom=0., top = 1.1)
axPA= plt.axes([0.85, 0.21, 0.03,0.67], facecolor='lightgoldenrodyellow')
sPA = VertSlider(axPA, 'PA', 0, 360, valinit=0, valstep=1, valfmt = '%d', dragging = True)
#Add Button to save and quit
def quit_save(foo):
plt.savefig('%s_NIRES.pdf'%target_name, bbox_inches = 'tight')
plt.close()
# return PA
axsave = plt.axes([0.75, 0.1, 0.2, 0.05], facecolor = 'blue')
bsave = Button(axsave, 'Save and Quit')
bsave.label.set_fontsize(14)
bsave.on_clicked(quit_save)
def skip(foo):
plt.close()
# axskip = plt.axes([0.6, 0.1, 0.1, 0.05])
# bskip = Button(axskip, 'Skip')
# bskip.label.set_fontsize(14)
# bskip.on_clicked(quit_skip)
# sPA.set_val(10)
def update(val):
global finalPA
PA = sPA.val
rot = transforms.Affine2D().rotate_around(x[0], y[0],np.radians(PA))+ ax.transData
rot_o = transforms.Affine2D().rotate_around(xo[0], yo[0],np.radians(PA))+ ax.transData
#rerotate and draw boxes
Guider.set_transform(rot)
Guider_rec.set_transform(rot)
slit.set_transform(rot)
Guider_o.set_transform(rot_o)
Guider_o_rec.set_transform(rot_o)
slit_o.set_transform(rot_o)
ax.add_patch(slit)
ax.add_patch(Guider)
ax.add_patch(Guider_rec)
ax.add_patch(slit_o)
ax.add_patch(Guider_o)
ax.add_patch(Guider_o_rec)
ax.set_title('%s, PA = %d deg'%(target_name,PA), fontsize = 16)
#when the PA slider is changed, run update
cid =sPA.on_changed(update)
plt.show()
# print(sPA.val)
return sPA.val
if __name__ == '__main__':
#When running this script, take a target list already with ONE offset star per SN
#For each object,
#Read target and offset
#Call plot_NIRES_fov(coords, coords_offset, target_name)
#Update the rotdest line
#Save the updated target list
print("###############NIRES PA Selection Tool###############")
print("Your supplied target list should be in the Keck fixed-width format.")
print("Each target could have ONE offset star in the next line.")
print("The format is:")
print("SN2034abc xxxxxxx")
print("SN2034abc_S1 xxxxxxx")
parser = argparse.ArgumentParser(description=\
'''
Creates the finder charts for the whole night, provided a target list file.
Usage: prepare_obs_run.py target_list_filename telescope
''', formatter_class=argparse.RawTextHelpFormatter)
# parser = argparse.ArgumentParser()
parser.add_argument("filename", type=str,
help="filename of the target list")
parser.add_argument("-d", "--debug", action="store_true",
help="debug mode")
parser.add_argument("-r", "--rotate", action="store_true",
help="produce rotated finder chart")
args = parser.parse_args()
#get file name
filename = args.filename
# converters = {'col5': [asci.convert_numpy(np.str)]} #This is so that -00 gets preserved as -00
# targets = asci.read(filename, comment = '!', format = 'no_header', converters = converters)
try:
targets = asci.read( filename, format="fixed_width_no_header",
col_starts=(0, 16, 28, 42, 50),
col_ends=(15, 27, 41, 49, 140),
names = ('names', 'RA', 'Dec', 'epoch', 'comments'))
print(targets)
except:
print('Make sure your supplied target list is in the Keck fixed width format.')
names = np.array(targets['names'])
#Make SkyCoord
coords = SkyCoord(targets['RA'], targets['Dec'], unit = (u.hourangle, u.deg))
all_starlist = ''
is_offset_star = False
if args.debug:
print(targets)
######Loop through the target list
for i in range(len(targets)):
if is_offset_star == False: #this entry is not offset star, treat as target
name = names[i]
# ra_deg = coords[i].ra.deg
# dec_deg = coords[i].dec.deg
target_coord = coords[i]
# mag = mags[i]
print("Making NIRES PA Selection GUI for %s."%name)
if i < len(targets)-1: #not the last item
next_entry = names[i+1]
if next_entry.split('_')[0] == name and "S" in next_entry.split('_')[1]: #next entry is offset star
# offset_ra = coords[i+1].ra.deg
# offset_dec = coords[i+1].dec.deg
offset_coord = coords[i+1]
print("Offset star provided. RA = %.5f Dec = %.5f"%(coords[i+1].ra.deg, coords[i+1].dec.deg))
is_offset_star = True #Because of this flag, the next item is skipped.
else:
offset_coord = None
else: #For the last item, no host is provided.
offset_coord = None
# Run the PA selection tool
finalPA = plot_NIRES_fov(target_coord, offset_coord, name)
starlist_entry = "{:s}{:s} {:s} {:s} rotmode=pa rotdest={:.2f} {:s} \n".\
format( targets[i]['names'].ljust(16), targets[i]['RA'], targets[i]['Dec'], str(targets[i]['epoch']), finalPA, targets[i]['comments'])
if is_offset_star:
starlist_entry += "{:s}{:s} {:s} {:s} rotmode=pa rotdest={:.2f} {:s} \n".\
format( targets[i+1]['names'].ljust(16), targets[i+1]['RA'], targets[i+1]['Dec'], str(targets[i+1]['epoch']), finalPA, targets[i+1]['comments'])
if args.debug:
print(starlist_entry)
all_starlist += starlist_entry
elif is_offset_star == True: #In this case, the next item should be a target
is_offset_star = False
###Keep writing to file so we don't lose progress.
out_file = open(filename.split('.')[0]+'_final.txt', "w")
out_file.write(all_starlist)
out_file.close()
if args.rotate:
print("Making rotated finder chart for NIRES")
try:
finder_name = name+'_finder.png'
rotated_finder = name+'_finder_rot.png'
# NIRES SVC is north down, so first we flip finders 180
# then we rotate by rotdest clockwise (which is negative in PIL rotate)
finder_rot=180-finalPA
im = Image.open(finder_name)
im_rotate=im.rotate(finder_rot, resample=Image.BICUBIC, expand = True,fillcolor=(255,255,255))
im_rotate.save(rotated_finder,dpi=(400,400))
except:
print("Check if PIL library is installed.")
print(all_starlist)