#!/usr/bin/env python
import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import astropy.io.fits as fits
import tkinter as tk
import tkinter.filedialog as tkFileDialog
import tkinter.messagebox as tkMessageBox
import logging
import logging
_log = logging.getLogger('webbpsf')
try:
import tkinter.ttk as ttk
except ImportError:
raise RuntimeError("Python with ttk widget support is required")
try:
import pysynphot
path = os.getenv('PYSYN_CDBS')
if path is not None and os.path.isdir(path):
_HAS_PYSYNPHOT=True
else:
_HAS_PYSYNPHOT=False
except ImportError:
_HAS_PYSYNPHOT=False
import poppy
from . import webbpsf_core
class WebbPSF_GUI(object):
""" A GUI for the PSF Simulator
Documentation TBD!
"""
def __init__(self, opdserver=None):
# init the object and subobjects
self.instrument = {}
self.widgets = {}
self.vars = {}
self.advanced_options = {'parity': 'any', 'force_coron': False, 'no_sam': False, 'psf_vmin': 1e-8, 'psf_vmax': 1.0, 'psf_scale': 'log', 'psf_cmap_str': 'Jet (blue to red)' , 'psf_normalize': 'Total', 'psf_cmap': matplotlib.cm.jet}
insts = ['NIRCam', 'NIRSpec','NIRISS', 'MIRI', 'FGS']
for i in insts:
self.instrument[i] = webbpsf_core.Instrument(i)
#invoke link to ITM server if provided?
if opdserver is not None:
self._enable_opdserver = True
self._opdserver = opdserver
else:
self._enable_opdserver = False
# create widgets & run
self._create_widgets()
self.root.update()
def _add_labeled_dropdown(self, name, root,label="Entry:", values=None, default=None, width=5, position=(0,0), **kwargs):
"convenient wrapper for adding a Combobox"
ttk.Label(root, text=label).grid(row=position[0], column=position[1], sticky='W')
self.vars[name] = tk.StringVar()
self.widgets[name] = ttk.Combobox(root, textvariable=self.vars[name], width=width, state='readonly')
self.widgets[name].grid(row=position[0], column=position[1]+1, **kwargs)
self.widgets[name]['values'] = values
if default is None: default=values[0]
self.widgets[name].set(default)
def _add_labeled_entry(self, name, root,label="Entry:", value="", width=5, position=(0,0), postlabel=None, **kwargs):
"convenient wrapper for adding an Entry"
ttk.Label(root, text=label).grid(row=position[0], column=position[1], sticky='W')
self.vars[name] = tk.StringVar()
self.widgets[name] = ttk.Entry(root, textvariable=self.vars[name], width=width)
self.widgets[name].insert(0,value)
self.widgets[name].grid(row=position[0], column=position[1]+1, **kwargs)
if postlabel is not None:
ttk.Label(root, text=postlabel).grid(row=position[0], column=position[1]+2, sticky='W')
def _create_widgets(self):
"""Create a nice GUI using the enhanced widget set provided by
the ttk extension to Tkinter, available in Python 2.7 or newer
"""
#---- create the GUIs
insts = ['NIRCam', 'NIRSpec','NIRISS', 'MIRI', 'FGS']
self.root = tk.Tk()
self.root.geometry('+50+50')
self.root.title("James Webb Space Telescope PSF Calculator")
frame = ttk.Frame(self.root)
#frame = ttk.Frame(self.root, padx=10,pady=10)
#ttk.Label(frame, text='James Webb PSF Calculator' ).grid(row=0)
#-- star
lf = ttk.LabelFrame(frame, text='Source Properties')
if _HAS_PYSYNPHOT:
self._add_labeled_dropdown("SpType", lf, label=' Spectral Type:', values=poppy.specFromSpectralType("",return_list=True), default='G0V', width=25, position=(0,0), sticky='W')
ttk.Button(lf, text='Plot spectrum', command=self.ev_plotspectrum).grid(row=0,column=2,sticky='E',columnspan=4)
r = 1
fr2 = ttk.Frame(lf)
self._add_labeled_entry("source_off_r", fr2, label=' Source Position: r=', value='0.0', width=5, position=(r,0), sticky='W')
self._add_labeled_entry("source_off_theta", fr2, label='arcsec, PA=', value='0', width=3, position=(r,2), sticky='W')
self.vars["source_off_centerpos"] = tk.StringVar()
self.vars["source_off_centerpos"].set('corner')
ttk.Label(fr2, text='deg, centered on ' ).grid(row=r, column=4)
pixel = ttk.Radiobutton(fr2, text='pixel', variable=self.vars["source_off_centerpos"], value='pixel')
pixel.grid(row=r, column=5)
corner = ttk.Radiobutton(fr2, text='corner', variable=self.vars["source_off_centerpos"], value='corner')
corner.grid(row=r, column=6)
fr2.grid(row=r, column=0, columnspan=5, sticky='W')
lf.columnconfigure(2, weight=1)
lf.grid(row=1, sticky='E,W', padx=10,pady=5)
#-- instruments
lf = ttk.LabelFrame(frame, text='Instrument Config')
notebook = ttk.Notebook(lf)
self.widgets['tabset'] = notebook
notebook.pack(fill='both')
for iname,i in zip(insts, range(len(insts))):
page = ttk.Frame(notebook)
notebook.add(page,text=iname)
notebook.select(i) # make it active
self.widgets[notebook.select()] = iname # save reverse lookup from meaningless widget "name" to string name
if iname =='NIRCam':
lframe = ttk.Frame(page)
ttk.Label(lframe, text='Configuration Options for '+iname+', module: ').grid(row=0, column=0, sticky='W')
mname='NIRCam module'
self.vars[mname] = tk.StringVar()
self.widgets[mname] = ttk.Combobox(lframe, textvariable=self.vars[mname], width=2, state='readonly')
self.widgets[mname].grid(row=0,column=1, sticky='W')
self.widgets[mname]['values'] = ['A','B']
self.widgets[mname].set('A')
lframe.grid(row=0, columnspan=2, sticky='W')
else:
ttk.Label(page, text='Configuration Options for '+iname+" ").grid(row=0, columnspan=2, sticky='W')
ttk.Button(page, text='Display Optics', command=self.ev_displayOptics ).grid(column=2, row=0, sticky='E', columnspan=3)
#if iname != 'TFI':
self._add_labeled_dropdown(iname+"_filter", page, label=' Filter:', values=self.instrument[iname].filter_list, default=self.instrument[iname].filter, width=12, position=(1,0), sticky='W')
#else:
#ttk.Label(page, text='Etalon wavelength: ' , state='disabled').grid(row=1, column=0, sticky='W')
#self.widgets[iname+"_wavelen"] = ttk.Entry(page, width=7) #, disabledforeground="#A0A0A0")
#self.widgets[iname+"_wavelen"].insert(0, str(self.instrument[iname].etalon_wavelength))
#self.widgets[iname+"_wavelen"].grid(row=1, column=1, sticky='W')
#ttk.Label(page, text=' um' ).grid(row=1, column=2, sticky='W')
#self.vars[iname+"_filter"] = tk.StringVar()
#self.widgets[iname+"_filter"] = ttk.Combobox(page,textvariable =self.vars[iname+"_filter"], width=10, state='readonly')
#self.widgets[iname+"_filter"]['values'] = self.instrument[iname].filter_list
#self.widgets[iname+"_filter"].set(self.instrument[iname].filter)
#self.widgets[iname+"_filter"]['readonly'] = True
#ttk.Label(page, text=' Filter: ' ).grid(row=1, column=0)
#self.widgets[iname+"_filter"].grid(row=1, column=1)
#if hasattr(self.instrument[iname], 'ifu_wavelength'):
if iname == 'NIRSpec' or iname =='MIRI':
fr2 = ttk.Frame(page)
#label = 'IFU' if iname !='TFI' else 'TF'
ttk.Label(fr2, text=' IFU wavelen: ', state='disabled').grid(row=0, column=0)
self.widgets[iname+"_ifu_wavelen"] = ttk.Entry(fr2, width=5) #, disabledforeground="#A0A0A0")
self.widgets[iname+"_ifu_wavelen"].insert(0, str(self.instrument[iname].monochromatic))
self.widgets[iname+"_ifu_wavelen"].grid(row=0, column=1)
self.widgets[iname+"_ifu_wavelen"].state(['disabled'])
ttk.Label(fr2, text=' um' , state='disabled').grid(row=0, column=2)
fr2.grid(row=1,column=2, columnspan=6, sticky='E')
iname2 = iname+"" # need to make a copy so the following lambda function works right:
self.widgets[iname+"_filter"].bind('<<ComboboxSelected>>', lambda e: self.ev_update_ifu_label(iname2))
if len(self.instrument[iname].image_mask_list) >0 :
masks = self.instrument[iname].image_mask_list
masks.insert(0, "")
self._add_labeled_dropdown(iname+"_coron", page, label=' Coron:', values=masks, width=12, position=(2,0), sticky='W')
#self.vars[iname+"_coron"] = tk.StringVar()
#self.widgets[iname+"_coron"] = ttk.Combobox(page,textvariable =self.vars[iname+"_coron"], width=10, state='readonly')
#self.widgets[iname+"_coron"]['values'] = masks
#ttk.Label(page, text=' Coron: ' ).grid(row=2, column=0)
#self.widgets[iname+"_coron"].set(self.widgets[iname+"_coron"]['values'][0])
#self.widgets[iname+"_coron"].grid(row=2, column=1)
#fr2 = ttk.Frame(page)
#self.vars[iname+"_cor_off_r"] = tk.StringVar()
#self.vars[iname+"_cor_off_theta"] = tk.StringVar()
#ttk.Label(fr2, text='target offset: r=' ).grid(row=2, column=4)
#self.widgets[iname+"_cor_off_r"] = ttk.Entry(fr2,textvariable =self.vars[iname+"_cor_off_r"], width=5)
#self.widgets[iname+"_cor_off_r"].insert(0,"0.0")
#self.widgets[iname+"_cor_off_r"].grid(row=2, column=5)
#ttk.Label(fr2, text='arcsec, PA=' ).grid(row=2, column=6)
#self.widgets[iname+"_cor_off_theta"] = ttk.Entry(fr2,textvariable =self.vars[iname+"_cor_off_theta"], width=3)
#self.widgets[iname+"_cor_off_theta"].insert(0,"0")
#self.widgets[iname+"_cor_off_theta"].grid(row=2, column=7)
#ttk.Label(fr2, text='deg' ).grid(row=2, column=8)
#fr2.grid(row=2,column=3, sticky='W')
if len(self.instrument[iname].image_mask_list) >0 :
masks = self.instrument[iname].pupil_mask_list
masks.insert(0, "")
self._add_labeled_dropdown(iname+"_pupil", page, label=' Pupil:', values=masks, width=12, position=(3,0), sticky='W')
fr2 = ttk.Frame(page)
self._add_labeled_entry(iname+"_pupilshift_x", fr2, label=' pupil shift in X:', value='0', width=3, position=(3,4), sticky='W')
self._add_labeled_entry(iname+"_pupilshift_y", fr2, label=' Y:', value='0', width=3, position=(3,6), sticky='W')
ttk.Label(fr2, text='% of pupil' ).grid(row=3, column=8)
fr2.grid(row=3,column=3, sticky='W')
ttk.Label(page, text='Configuration Options for the OTE').grid(row=4, columnspan=2, sticky='W')
fr2 = ttk.Frame(page)
opd_list = self.instrument[iname].opd_list
opd_list.insert(0,"Zero OPD (perfect)")
#if os.getenv("WEBBPSF_ITM") or 1:
if self._enable_opdserver:
opd_list.append("OPD from ITM Server")
default_opd = self.instrument[iname].pupilopd if self.instrument[iname].pupilopd is not None else "Zero OPD (perfect)"
self._add_labeled_dropdown(iname+"_opd", fr2, label=' OPD File:', values=opd_list, default=default_opd, width=21, position=(0,0), sticky='W')
self._add_labeled_dropdown(iname+"_opd_i", fr2, label=' # ', values= [str(i) for i in range(10)], width=3, position=(0,2), sticky='W')
self.widgets[iname+"_opd_label"] = ttk.Label(fr2, text=' 0 nm RMS ', width=35)
self.widgets[iname+"_opd_label"].grid( column=4,sticky='W', row=0)
self.widgets[iname+"_opd"].bind('<<ComboboxSelected>>',
lambda e: self.ev_update_OPD_labels() )
# The below code does not work, and I can't tell why. This only ever has iname = 'FGS' no matter which instrument.
# So instead brute-force it with the above to just update all 5.
#lambda e: self.ev_update_OPD_label(self.widgets[iname+"_opd"], self.widgets[iname+"_opd_label"], iname) )
ttk.Button(fr2, text='Display', command=self.ev_displayOPD).grid(column=5,sticky='E',row=0)
fr2.grid(row=5, column=0, columnspan=4,sticky='S')
# ITM interface here - build the widgets now but they will be hidden by default until the ITM option is selected
fr2 = ttk.Frame(page)
self._add_labeled_entry(iname+"_coords", fr2, label=' Source location:', value='0, 0', width=12, position=(1,0), sticky='W')
units_list = ['V1,V2 coords', 'detector pixels']
self._add_labeled_dropdown(iname+"_coord_units", fr2, label='in:', values=units_list, default=units_list[0], width=11, position=(1,2), sticky='W')
choose_list=['', 'SI center', 'SI upper left corner', 'SI upper right corner', 'SI lower left corner', 'SI lower right corner']
self._add_labeled_dropdown(iname+"_coord_choose", fr2, label='or select:', values=choose_list, default=choose_list[0], width=21, position=(1,4), sticky='W')
ttk.Label(fr2, text=' ITM output:').grid(row=2, column=0, sticky='W')
self.widgets[iname+"_itm_output"] = ttk.Label(fr2, text=' - no file available yet -')
self.widgets[iname+"_itm_output"].grid(row=2, column=1, columnspan=4, sticky='W')
ttk.Button(fr2, text='Access ITM...', command=self.ev_launch_ITM_dialog).grid(column=5,sticky='E',row=2)
fr2.grid(row=6, column=0, columnspan=4,sticky='SW')
self.widgets[iname+"_itm_coords"] = fr2
self.ev_update_OPD_labels()
lf.grid(row=2, sticky='E,W', padx=10, pady=5)
notebook.select(0)
lf = ttk.LabelFrame(frame, text='Calculation Options')
r =0
self._add_labeled_entry('FOV', lf, label='Field of View:', width=3, value='5', postlabel='arcsec/side', position=(r,0))
r+=1
self._add_labeled_entry('detector_oversampling', lf, label='Output Oversampling:', width=3, value='2', postlabel='x finer than instrument pixels ', position=(r,0))
#self.vars['downsamp'] = tk.BooleanVar()
#self.vars['downsamp'].set(True)
#self.widgets['downsamp'] = ttk.Checkbutton(lf, text='Save in instr. pixel scale, too?', onvalue=True, offvalue=False,variable=self.vars['downsamp'])
#self.widgets['downsamp'].grid(row=r, column=4, sticky='E')
output_options=['Oversampled PSF only', 'Oversampled + Detector Res. PSFs', 'Mock full image from JWST DMS']
self._add_labeled_dropdown("output_type", fr2, label='Output format:', values=output_options, default=output_options[1], width=31, position=(r,4), sticky='W')
r+=1
self._add_labeled_entry('fft_oversampling', lf, label='Coronagraph FFT Oversampling:', width=3, value='2', postlabel='x finer than Nyquist', position=(r,0))
r+=1
self._add_labeled_entry('nlambda', lf, label='# of wavelengths:', width=3, value='', position=(r,0), postlabel='Leave blank for autoselect')
r+=1
self._add_labeled_dropdown("jitter", lf, label='Jitter model:', values= ['Just use OPDs', 'Gaussian - 7 mas rms', 'Gaussian - 30 mas rms'], width=20, position=(r,0), sticky='W', columnspan=2)
r+=1
self._add_labeled_dropdown("output_format", lf, label='Output Format:', values= ['Oversampled image','Detector sampled image','Both as FITS extensions', 'Mock JWST DMS Output' ], width=30, position=(r,0), sticky='W', columnspan=2)
#self._add_labeled_dropdown("jitter", lf, label='Jitter model:', values= ['Just use OPDs', 'Gaussian blur', 'Accurate yet SLOW grid'], width=20, position=(r,0), sticky='W', columnspan=2)
lf.grid(row=4, sticky='E,W', padx=10, pady=5)
lf = ttk.Frame(frame)
def addbutton(self,lf, text, command, pos, disabled=False):
self.widgets[text] = ttk.Button(lf, text=text, command=command )
self.widgets[text].grid(column=pos, row=0, sticky='E')
if disabled:
self.widgets[text].state(['disabled'])
addbutton(self,lf,'Compute PSF', self.ev_calc_psf, 0)
addbutton(self,lf,'Display PSF', self.ev_displayPSF, 1, disabled=True)
addbutton(self,lf,'Display profiles', self.ev_displayProfiles, 2, disabled=True)
addbutton(self,lf,'Save PSF As...', self.ev_SaveAs, 3, disabled=True)
addbutton(self,lf,'More options...', self.ev_options, 4, disabled=False)
ttk.Button(lf, text='Quit', command=self.quit).grid(column=5, row=0)
lf.columnconfigure(2, weight=1)
lf.columnconfigure(4, weight=1)
lf.grid(row=5, sticky='E,W', padx=10, pady=15)
frame.grid(row=0, sticky='N,E,S,W')
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
def quit(self):
" Quit the GUI"
if tkMessageBox.askyesno( message='Are you sure you want to quit WebbPSF?', icon='question', title='Confirm quit') :
self.root.destroy()
def ev_SaveAs(self):
"Event handler for Save As of output PSFs"
filename = tkFileDialog.asksaveasfilename(
initialfile='PSF_%s_%s.fits' %(self.iname, self.filter),
filetypes=[('FITS', '.fits')],
defaultextension='.fits',
parent=self.root)
if len(filename) > 0:
self.PSF_HDUlist.writeto(filename)
print("Saved to {}".format(filename))
def ev_options(self):
d = WebbPSFOptionsDialog(self.root, input_options = self.advanced_options)
if d.results is not None: # none means the user hit 'cancel'
self.advanced_options = d.results
def ev_plotspectrum(self):
"Event handler for Plot Spectrum "
self._updateFromGUI()
print("Spectral type is "+self.sptype)
print("Selected instrument tab is "+self.iname)
print("Selected instrument filter is "+self.filter)
plt.clf()
ax1 = plt.subplot(311)
spectrum = poppy.specFromSpectralType(self.sptype)
synplot(spectrum)
ax1.set_ybound(1e-6, 1e8) # hard coded for now
ax1.yaxis.set_major_locator(matplotlib.ticker.LogLocator(base=1000))
legend_font = matplotlib.font_manager.FontProperties(size=10)
ax1.legend(loc='lower right', prop=legend_font)
ax2 = plt.subplot(312, sharex=ax1)
ax2.set_ybound(0,1.1)
band = self.inst._get_synphot_bandpass(self.inst.filter) #pysynphot.ObsBandpass(obsname)
band.name = "%s %s" % (self.iname, self.inst.filter)
synplot(band) #, **kwargs)
legend_font = matplotlib.font_manager.FontProperties(size=10)
plt.legend(loc='lower right', prop=legend_font)
ax2.set_ybound(0,1.1)
ax3 = plt.subplot(313, sharex=ax1)
if self.nlambda is None:
# Automatically determine number of appropriate wavelengths.
# Make selection based on filter configuration file
try:
nlambda = self.inst._filters[self.filter].default_nlambda
except KeyError:
nlambda=10
else:
nlambda = self.nlambda
ax1.set_xbound(0.1, 100)
plt.draw()
waves, weights = self.inst._getWeights(spectrum, nlambda=nlambda)
wave_step = waves[1]-waves[0]
plot_waves = np.concatenate( ([waves[0]-wave_step], waves, [waves[-1]+wave_step])) * 1e6
plot_weights = np.concatenate(([0], weights,[0]))
plt.ylabel("Weight")
plt.xlabel("Wavelength [$\mu$m]")
ax3.plot(plot_waves, plot_weights, drawstyle='steps-mid')
ax1.set_xbound(0.1, 100)
self._refresh_window()
def _refresh_window(self):
""" Force the window to refresh, and optionally to show itself if hidden (for recent matplotlibs)"""
plt.draw()
from distutils.version import StrictVersion
if StrictVersion(matplotlib.__version__) >= StrictVersion('1.1'):
plt.show(block=False)
def ev_calc_psf(self):
"Event handler for PSF Calculations"
self._updateFromGUI()
if _HAS_PYSYNPHOT:
source = poppy.specFromSpectralType(self.sptype)
else:
source=None # generic flat spectrum
self.PSF_HDUlist = self.inst.calc_psf(source=source,
detector_oversample= self.detector_oversampling,
fft_oversample=self.fft_oversampling,
fov_arcsec = self.FOV, nlambda = self.nlambda, display=True)
#self.PSF_HDUlist.display()
for w in ['Display PSF', 'Display profiles', 'Save PSF As...']:
self.widgets[w].state(['!disabled'])
self._refresh_window()
_log.info("PSF calculation complete")
def ev_displayPSF(self):
"Event handler for Displaying the PSF"
#self._updateFromGUI()
#if self.PSF_HDUlist is not None:
plt.clf()
poppy.display_PSF(self.PSF_HDUlist, vmin = self.advanced_options['psf_vmin'], vmax = self.advanced_options['psf_vmax'],
scale = self.advanced_options['psf_scale'], cmap= self.advanced_options['psf_cmap'], normalize=self.advanced_options['psf_normalize'])
self._refresh_window()
def ev_displayProfiles(self):
"Event handler for Displaying the PSF"
#self._updateFromGUI()
poppy.display_profiles(self.PSF_HDUlist)
self._refresh_window()
def ev_displayOptics(self):
"Event handler for Displaying the optical system"
self._updateFromGUI()
_log.info("Selected OPD is "+str(self.opd_name))
plt.clf()
self.inst.display()
self._refresh_window()
def ev_displayOPD(self):
self._updateFromGUI()
if self.inst.pupilopd is None:
tkMessageBox.showwarning( message="You currently have selected no OPD file (i.e. perfect telescope) so there's nothing to display.", title="Can't Display")
else:
if self._enable_opdserver and 'ITM' in self.opd_name:
opd = self.inst.pupilopd # will contain the actual OPD loaded in _updateFromGUI just above
else:
opd = fits.getdata(self.inst.pupilopd[0]) # in this case self.inst.pupilopd is a tuple with a string so we have to load it here.
if len(opd.shape) >2:
opd = opd[self.opd_i,:,:] # grab correct slice
masked_opd = np.ma.masked_equal(opd, 0) # mask out all pixels which are exactly 0, outside the aperture
cmap = matplotlib.cm.jet
cmap.set_bad('k', 0.8)
plt.clf()
plt.imshow(masked_opd, cmap=cmap, interpolation='nearest', vmin=-0.5, vmax=0.5)
plt.title("OPD from %s, #%d" %( os.path.basename(self.opd_name), self.opd_i))
cb = plt.colorbar(orientation='vertical')
cb.set_label('microns')
f = plt.gcf()
plt.text(0.4, 0.02, "OPD WFE = %6.2f nm RMS" % (masked_opd.std()*1000.), transform=f.transFigure)
self._refresh_window()
def ev_launch_ITM_dialog(self):
tkMessageBox.showwarning( message="ITM dialog box not yet implemented", title="Can't Display")
def ev_update_OPD_labels(self):
"Update the descriptive text for all OPD files"
for iname in self.instrument:
self.ev_update_OPD_label(self.widgets[iname+"_opd"], self.widgets[iname+"_opd_label"], iname)
def ev_update_OPD_label(self, widget_combobox, widget_label, iname):
"Update the descriptive text displayed about one OPD file"
showitm=False # default is do not show
filename = self.instrument[iname]._datapath +os.sep+ 'OPD'+ os.sep+widget_combobox.get()
if filename.endswith(".fits"):
header_summary = fits.getheader(filename)['SUMMARY']
self.widgets[iname+"_opd_i"]['state'] = 'readonly'
else: # Special options for non-FITS file inputs
self.widgets[iname+"_opd_i"]['state'] = 'disabled'
if 'Zero' in widget_combobox.get():
header_summary = " 0 nm RMS"
elif 'ITM' in widget_combobox.get() and self._enable_opdserver:
header_summary= "Get OPD from ITM Server"
showitm=True
elif 'ITM' in widget_combobox.get() and not self._enable_opdserver:
header_summary = "ITM Server is not running or otherwise unavailable."
else: # other??
header_summary = " "
widget_label.configure(text=header_summary, width=30)
if showitm:
self.widgets[iname+"_itm_coords"].grid() # re-show ITM options
else:
self.widgets[iname+"_itm_coords"].grid_remove() # hide ITM options
def _updateFromGUI(self):
# get GUI values
if _HAS_PYSYNPHOT:
self.sptype = self.widgets['SpType'].get()
self.iname = self.widgets[self.widgets['tabset'].select()]
try:
self.nlambda= int(self.widgets['nlambda'].get())
except ValueError:
self.nlambda = None # invoke autoselect for nlambda
self.FOV= float(self.widgets['FOV'].get())
self.fft_oversampling= int(self.widgets['fft_oversampling'].get())
self.detector_oversampling= int(self.widgets['detector_oversampling'].get())
self.output_type = self.widgets['output_type'].get()
options = {}
#options['downsample'] = bool(self.vars['downsamp'])
options['rebin'] = not (self.output_type == 'Oversampled PSF only') #was downsample, which seems wrong?
options['mock_dms'] = (self.output_type == 'Mock full image from JWST DMS')
jitter_choice = self.widgets['jitter'].get()
if 'Gaussian' in jitter_choice:
options['jitter'] = 'gaussian'
if '7 mas' in jitter_choice:
options['jitter_sigma'] = 0.007
elif '30 mas' in jitter_choice:
options['jitter_sigma'] = 0.030
else:
options['jitter'] = None
# and get the values that may have previously been set by the 'advanced options' dialog
if self.advanced_options is not None:
for a in self.advanced_options:
options[a] = self.advanced_options[a]
# configure the relevant instrument object
self.inst = self.instrument[self.iname]
self.filter = self.widgets[self.iname+"_filter"].get() # save for use in default filenames, etc.
self.inst.filter = self.filter
self.opd_name = self.widgets[self.iname+"_opd"].get()
if self._enable_opdserver and 'ITM' in self.opd_name:
# get from ITM server
self.opd_i= 0
self.inst.pupilopd = self._opdserver.get_OPD(return_as="FITS")
self.opd_name = "OPD from ITM OPD GUI"
elif self.opd_name == "Zero OPD (perfect)":
# perfect OPD
self.opd_name = "Perfect"
self.inst.pupilopd = None
else:
# Regular FITS file version
self.opd_name= self.widgets[self.iname+"_opd"].get()
self.opd_i= int(self.widgets[self.iname+"_opd_i"].get())
self.inst.pupilopd = (self.inst._datapath+os.sep+"OPD"+os.sep+self.opd_name,self.opd_i) #filename, slice
_log.info("Selected OPD is "+str(self.opd_name))
if self.iname+"_coron" in self.widgets:
self.inst.image_mask = self.widgets[self.iname+"_coron"].get()
self.inst.pupil_mask = self.widgets[self.iname+"_pupil"].get()
# TODO read in mis-registration options here.
options['source_offset_r'] = float(self.widgets["source_off_r"].get())
options['source_offset_theta'] = float(self.widgets["source_off_theta"].get())
options['pupil_shift_x'] = float(self.widgets[self.iname+"_pupilshift_x"].get())/100. # convert from percent to fraction
options['pupil_shift_y'] = float(self.widgets[self.iname+"_pupilshift_y"].get())/100. # convert from percent to fraction
self.inst.options = options
def mainloop(self):
self.root.mainloop()
#-------------------------------------------------------------------------
class Dialog(tk.Toplevel):
""" Base class for a modal dialog box.
From example code at http://effbot.org/tkinterbook/tkinter-dialog-windows.htm
"""
def __init__(self, parent, title = None, input_options=None):
tk.Toplevel.__init__(self, parent)
self.transient(parent)
self.input_options = input_options
if title:
self.title(title)
self.parent = parent
self.result = None
body = ttk.Frame(self)
self.initial_focus = self.body(body)
body.pack(padx=5, pady=5)
self.buttonbox()
self.grab_set()
if not self.initial_focus:
self.initial_focus = self
self.protocol("WM_DELETE_WINDOW", self.cancel)
self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
parent.winfo_rooty()+50))
self.initial_focus.focus_set()
self.wait_window(self)
#
# construction hooks
def body(self, master):
# create dialog body. return widget that should have
# initial focus. this method should be overridden
pass
def buttonbox(self):
# add standard button box. override if you don't want the
# standard buttons
box = ttk.Frame(self)
w = ttk.Button(box, text="OK", width=10, command=self.ok, default=tk.ACTIVE)
w.pack(side=tk.LEFT, padx=5, pady=5)
w = ttk.Button(box, text="Cancel", width=10, command=self.cancel)
w.pack(side=tk.LEFT, padx=5, pady=5)
self.bind("<Return>", self.ok)
self.bind("<Escape>", self.cancel)
box.pack()
#
# standard button semantics
def ok(self, event=None):
if not self.validate():
self.initial_focus.focus_set() # put focus back
return
self.withdraw()
self.update_idletasks()
self.apply()
self.cancel()
def cancel(self, event=None):
# put focus back to the parent window
self.parent.focus_set()
self.destroy()
#
# command hooks
def validate(self):
return True # override
def apply(self):
pass # override
class WebbPSFOptionsDialog(Dialog):
def _add_labeled_dropdown(self, name, root,label="Entry:", values=None, default=None, width=5, position=(0,0), **kwargs):
"convenient wrapper for adding a Combobox"
ttk.Label(root, text=label).grid(row=position[0], column=position[1], sticky='W')
self.vars[name] = tk.StringVar()
self.widgets[name] = ttk.Combobox(root, textvariable=self.vars[name], width=width, state='readonly')
self.widgets[name].grid(row=position[0], column=position[1]+1, **kwargs)
self.widgets[name]['values'] = values
if default is None: default=values[0]
self.widgets[name].set(default)
def _add_labeled_entry(self, name, root,label="Entry:", value="", width=5, position=(0,0), postlabel=None, **kwargs):
"convenient wrapper for adding an Entry"
ttk.Label(root, text=label).grid(row=position[0], column=position[1], sticky='W')
self.vars[name] = tk.StringVar()
self.widgets[name] = ttk.Entry(root, textvariable=self.vars[name], width=width)
self.widgets[name].insert(0,value)
self.widgets[name].grid(row=position[0], column=position[1]+1, **kwargs)
if postlabel is not None:
ttk.Label(root, text=postlabel).grid(row=position[0], column=position[1]+2, sticky='W')
def body(self, master):
self.results = None # in case we cancel this gets returned
self.results = None # in case we cancel this gets returned
self.vars = {}
self.widgets = {}
self.values = {}
colortables = [
('Jet (blue to red)',matplotlib.cm.jet),
('Gray', matplotlib.cm.gray),
('Heat (black-red-yellow)', matplotlib.cm.gist_heat),
('Copper (black to tan)',matplotlib.cm.copper),
('Stern',matplotlib.cm.gist_stern),
('Prism (repeating rainbow)', matplotlib.cm.prism)]
self.colortables = dict(colortables)
lf = ttk.LabelFrame(master, text='WebbPSF Advanced Options')
r=1
fr2 = ttk.Frame(lf)
self.values['force_coron'] = ['regular propagation (MFT)', 'full coronagraphic propagation (FFT/SAM)']
self._add_labeled_dropdown("force_coron", lf, label='Direct imaging calculations use', values=self.values['force_coron'],
default = self.values['force_coron'][ 1 if self.input_options['force_coron'] else 0] , width=30, position=(r,0), sticky='W')
r+=1
self.values['no_sam'] = ['semi-analytic method if possible', 'basic FFT method always']
self._add_labeled_dropdown("no_sam", lf, label='Coronagraphic calculations use', values=self.values['no_sam'],
default=self.values['no_sam'][ 1 if self.input_options['no_sam'] else 0] , width=30, position=(r,0), sticky='W')
r+=1
self._add_labeled_dropdown("parity", lf, label='Output pixel grid parity is', values=['odd', 'even', 'either'], default=self.input_options['parity'], width=10, position=(r,0), sticky='W')
lf.grid(row=1, sticky='E,W', padx=10,pady=5)
lf = ttk.LabelFrame(master, text='PSF Display Options')
r=0
self._add_labeled_dropdown("psf_scale", lf, label=' Display scale:', values=['log','linear'],default=self.input_options['psf_scale'], width=5, position=(r,0), sticky='W')
r+=1
self._add_labeled_entry("psf_vmin", lf, label=' Min scale value:', value="%.2g" % self.input_options['psf_vmin'], width=7, position=(r,0), sticky='W')
r+=1
self._add_labeled_entry("psf_vmax", lf, label=' Max scale value:', value="%.2g" % self.input_options['psf_vmax'], width=7, position=(r,0), sticky='W')
r+=1
self._add_labeled_dropdown("psf_normalize", lf, label=' Normalize PSF to:', values=['Total', 'Peak'], default=self.input_options['psf_normalize'], width=5, position=(r,0), sticky='W')
r+=1
self._add_labeled_dropdown("psf_cmap", lf, label=' Color table:', values=[a[0] for a in colortables], default=self.input_options['psf_cmap_str'], width=20, position=(r,0), sticky='W')
lf.grid(row=2, sticky='E,W', padx=10,pady=5)
return self.widgets['force_coron']# return widget to have initial focus
def apply(self, test=False):
try:
results = {}
results['force_coron'] = self.vars['force_coron'].get() == 'full coronagraphic propagation (FFT/SAM)'
results['no_sam'] = self.vars['no_sam'].get() == 'basic FFT method always'
results['parity'] = self.vars['parity'].get()
results['psf_scale'] = self.vars['psf_scale'].get()
results['psf_vmax'] = float(self.vars['psf_vmax'].get())
results['psf_vmin'] = float(self.vars['psf_vmin'].get())
results['psf_cmap_str'] = self.vars['psf_cmap'].get()
results['psf_normalize'] = self.vars['psf_normalize'].get()
results['psf_cmap'] = self.colortables[ str(self.vars['psf_cmap'].get() ) ]
except ValueError:
return False
if not test:
self.results = results
return True
def validate(self):
can_apply = self.apply(test=True)
if not can_apply:
_log.error("Invalid entries in one or more fields. Please re-enter!")
return can_apply
#-------------------------------------------------------------------------
def synplot(thing, waveunit='micron', label=None, **kwargs):
""" Plot a single PySynPhot object (either SpectralElement or SourceSpectrum)
versus wavelength, with nice axes labels.
Really just a simple convenience function.
"""
# convert to requested display unit.
wave = thing.waveunits.Convert(thing.wave,waveunit)
if label is None:
label = thing.name
if isinstance(thing, pysynphot.spectrum.SourceSpectrum):
artist = plt.loglog(wave, thing.flux, label=label, **kwargs)
plt.xlabel("Wavelength [%s]" % waveunit)
if str(thing.fluxunits) == 'flam':
plt.ylabel("Flux [%s]" % ' erg cm$^{-2}$ s$^{-1}$ Ang$^{-1}$' )
else:
plt.ylabel("Flux [%s]" % thing.fluxunits)
elif isinstance(thing, pysynphot.spectrum.SpectralElement):
artist = plt.plot(wave, thing.throughput,label=label, **kwargs)
plt.xlabel("Wavelength [%s]" % waveunit)
plt.ylabel("Throughput")
plt.gca().set_ylim(0,1)
else:
_log.error( "Don't know how to plot that object...")
artist = None
return artist
[docs]def tkgui(fignum=1):
# enable log message printout
logging.basicConfig(level=logging.INFO,format='%(name)-10s: %(levelname)-8s %(message)s')
# start the GUI
gui = WebbPSF_GUI()
plt.figure(fignum)
#plt.show(block=False)
gui.mainloop()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO,format='%(name)-10s: %(levelname)-8s %(message)s')
gui()