# -*- coding: utf-8 -*-
#
# This file is part of LDTObserverTools.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Created on 25-Jan-2021
#
# @author: tbowers
"""DeVeny Grating Angle Calculator Module
LDTObserverTools contains python ports of various LDT Observer Tools
Lowell Discovery Telescope (Lowell Observatory: Flagstaff, AZ)
https://lowell.edu
This file contains the ``deveny_grangle`` routine for computing the needed grating
tilt angle to be set in order to center the desired wavelength on the CCD.
Both a CLI (direct copy of the IDL version) and GUI version are included here.
"""
# Built-In Libraries
import os
import sys
# 3rd-Party Libraries
import numpy as np
import scipy.optimize
import PySimpleGUI as sg
# Local Libraries
from obstools import utils
# CONSTANTS
PIXSCALE = 2.94 # Base pixels per arcsec: 1 / (0.34 arcsec / pixel)
CAMCOL = np.deg2rad(55.00) # DeVeny Optical Angle -- Camera-to-Collimator
COLL = np.deg2rad(10.00) # DeVeny Optical Angle -- Collimator-to-Grating
TGOFFSET = 0.0 # Mechanical Offset in the grating angle
[docs]def deveny_grangle_cli():
"""Compute the desired grating angle given grating and central wavelength
Command line version of ``deveny_grangle``, direct port of the IDL version.
Takes no arguments, returns nothing, and prints output to screen.
"""
# Get input from user
print(" Enter grating resolution (g/mm):")
gpmm = float(input())
print(" Enter central wavelength (A):")
wavelen = float(input())
# Compute the grating angle and anamorphic demagnification
grangle, amag = compute_grangle(gpmm, wavelen)
print(f"\n Grating: {gpmm:.0f} g/mm")
print(f" Central Wavelength: {wavelen} A")
print(f" DeVeny grating tilt = {grangle+TGOFFSET:.2f} deg")
print(
" Slit demagnification (pixels/arcsec, 0.34 arcsec/pixel): "
+ f"{PIXSCALE*amag:.2f}\n"
)
[docs]def deveny_grangle_gui(max_gui: bool = False):
r"""Main Driver for the DeVeny Grangle GUI
Compute the desired grating angle given grating and central wavelength
GUI version of ``deveny_grangle`` built using ``PySimpleGUI``. The GUI
includes a drop-down menu for available gratings, and checks for a requested
wavelength between 3000A and 11,000A. This interface uses the same
subroutines as the CLI version and produces the same results.
This version optionally allows for the calcuation of the central wavelength
given a grating angle
Parameters
----------
max_gui : :obj:`bool`, optional
Display the MAX GUI (forward and backward calculations) (Default: False)
"""
# Define the gratings for the drop-down menu
gratings = [
"DV1 - 150 g/mm, 5000 Å",
"DV2 - 300 g/mm, 4000 Å",
"DV3 - 300 g/mm, 6750 Å",
"DV4 - 400 g/mm, 8500 Å",
"DV5 - 500 g/mm, 5500 Å",
"DV6 - 600 g/mm, 4900 Å",
"DV7 - 600 g/mm, 6750 Å",
"DV8 - 831 g/mm, 8000 Å",
"DV9 - 1200 g/mm, 5000 Å",
"DV10 - 2160 g/mm, 5000 Å",
]
# Define the color scheme for the GUI
sg.theme(utils.SG_THEME)
# Define the window layout
row1 = [
sg.Text("Select Grating:"),
sg.Drop(
values=(gratings),
auto_size_text=True,
default_value=gratings[1],
key="Grat",
),
]
row2 = [
sg.Text("Enter Central Wavelength:"),
sg.Input(key="-WAVEIN-", size=(6, 1)),
sg.Text("Å"),
]
if max_gui:
row8 = [
sg.Text("Enter Grating Tilt:"),
sg.Input(key="-TILTIN-", size=(6, 1)),
sg.Text("º"),
]
row3 = [
sg.Button("Compute Tilt"),
sg.Button("Compute Wavelength"),
sg.Button("Done"),
]
else:
row3 = [sg.Button("Compute"), sg.Button("Done")]
row4 = [sg.Text(" Grating: "), sg.Text(size=(20, 1), key="-GRATOUT-")]
row5 = [sg.Text(" Central Wavelength: "), sg.Text(size=(20, 1), key="-WAVEOUT-")]
row6 = [sg.Text("DeVeny Grating Tilt = "), sg.Text(size=(20, 1), key="-TILTOUT-")]
row7 = [
sg.Text('Slit demagnification (0.34"/pixel): '),
sg.Text(size=(15, 1), key="-DEMAGOUT-"),
]
# Define the rows based on which GUI we're making
rows = (
[row1, row2, row8, row3, row4, row5, row6, row7]
if max_gui
else [row1, row2, row3, row4, row5, row6, row7]
)
# Make the pysimplegui "Error performing wm_overrideredirect" go away
old_stdout = sys.stdout
with open(os.devnull, "w", encoding="utf8") as f_null:
sys.stdout = f_null
# Create the Window
window = sg.Window(
"DeVeny Grating Angle Calculator",
rows,
location=(10, 10),
finalize=True,
element_justification="center",
font="Helvetica 18",
)
# Return the STDOUT to the command line
sys.stdout = old_stdout
# Wait for events
while True:
event, values = window.read()
if event in [sg.WIN_CLOSED, "Done"]:
break
if event in ["Compute", "Compute Tilt"]:
# Check for non-numeric entries for Central Wavelength
if not values["-WAVEIN-"].isnumeric():
window["-GRATOUT-"].update("")
window["-WAVEOUT-"].update("Please Enter a Number")
window["-TILTOUT-"].update("")
window["-DEMAGOUT-"].update("")
if max_gui:
window["-TILTIN-"].update("")
# Wait for next event
continue
# Convert wavelen to float, and check for valid range
wavelen = float(values["-WAVEIN-"])
if wavelen < 3000 or wavelen > 11000:
window["-GRATOUT-"].update("")
window["-WAVEOUT-"].update("Wavelength out of range")
window["-TILTOUT-"].update("")
window["-DEMAGOUT-"].update("")
if max_gui:
window["-TILTIN-"].update("")
# Wait for next event
continue
# Compute the grating angle and anamorphic demagnification
gpmm = float(values["Grat"].split(" - ")[1].split(" g/mm")[0])
grangle, amag = compute_grangle(gpmm, wavelen)
# Update the window with the calculated values
window["-GRATOUT-"].update(values["Grat"])
window["-WAVEOUT-"].update(f"{values['-WAVEIN-']} Å")
window["-TILTOUT-"].update(f"{grangle+TGOFFSET:.2f}º")
window["-DEMAGOUT-"].update(f"{PIXSCALE*amag:.2f} pixels/arcsec")
if max_gui:
window["-TILTIN-"].update(f"{grangle+TGOFFSET:.2f}")
elif event == "Compute Wavelength":
# Check for non-numeric entries for Grating Tilt
if not utils.check_float(values["-TILTIN-"]):
window["-GRATOUT-"].update("")
window["-WAVEOUT-"].update("")
window["-TILTOUT-"].update("Please Enter a Number")
window["-DEMAGOUT-"].update("")
window["-WAVEIN-"].update("")
# Wait for next event
continue
# Convert wavelen to float, and check for valid range
tilt = float(values["-TILTIN-"])
if tilt < 0 or tilt > 48:
window["-GRATOUT-"].update("")
window["-WAVEOUT-"].update("")
window["-TILTOUT-"].update("Tilt angle out of range")
window["-DEMAGOUT-"].update("")
window["-WAVEIN-"].update("")
# Wait for next event
continue
# Compute the grating angle and anamorphic demagnification
gpmm = float(values["Grat"].split(" - ")[1].split(" g/mm")[0])
wavelen = lambda_at_angle(tilt, gpmm)
amag = deveny_amag(tilt)
# Update the window with the calculated values
window["-GRATOUT-"].update(values["Grat"])
window["-WAVEOUT-"].update(f"{wavelen:.0f} Å")
window["-TILTOUT-"].update(f"{tilt+TGOFFSET:.2f}º")
window["-DEMAGOUT-"].update(f"{PIXSCALE*amag:.2f} pixels/arcsec")
window["-WAVEIN-"].update(f"{wavelen:.0f}")
else:
print("Something funny happened... should never print.")
# All done, close window
window.close()
# Computation Utility Functions ==============================================#
[docs]def compute_grangle(lpmm: float, wavelen: float):
"""Compute the needed grating angle
Given the grating's line density and the desired central wavelength,
compute the required grating angle. Uses :func:`scipy.optimize.newton` as
the root-solver.
Parameters
----------
lpmm : :obj:`float`
The line density of the grating in g/mm
wavelen : :obj:`float`
The central wavelength in angstroms for which to compute the tilt
Returns
-------
grangle : :obj:`float`
The desired grating angle
amag : :obj:`float`
The anamorphic demagnification of the spectrograph at this grangle
"""
# Initial guess: 20º
theta = np.deg2rad(20.0)
# Call the newton method from scipy.optimize to solve the grating equation
grangle = np.rad2deg(
scipy.optimize.newton(grangle_eqn, x0=theta, args=(lpmm, wavelen))
)
amag = deveny_amag(grangle)
return grangle, amag
[docs]def grangle_eqn(theta: float, lpmm: float, wavelen: float) -> float:
"""The grating equation used to find the angle
This is the equation for which :func:`scipy.optimize.newton` is finding
the root.
Parameters
----------
theta : :obj:`float`
The grating angle being tested for (in radians)
lpmm : :obj:`float`
The line density of the grating in g/mm
wavelen : :obj:`float`
The central wavelength in angstroms for which to compute the tilt
Returns
-------
:obj:`float`
The portion of the grating equation to be set to zero.
"""
return lambda_at_angle(theta, lpmm, radians=True) - wavelen
[docs]def lambda_at_angle(theta: float, lpmm: float, radians: bool = False) -> float:
"""Compute the central wavelength given theta
Use the grating equation to compute the central wavelength given theta
Parameters
----------
theta : :obj:`float`
Grating Angle
lpmm : :obj:`float`
The line density of the grating in g/mm
radians : :obj:`bool`, optional
The input angle is in radians (Default: False)
Returns
-------
:obj:`float`
The computed central wavelength
"""
# Condition the inputs
if not isinstance(theta, float):
theta = float(theta)
if not isinstance(lpmm, float):
lpmm = float(lpmm)
if not radians:
theta = np.deg2rad(theta)
return (np.sin(COLL + theta) + np.sin(COLL + theta - CAMCOL)) * 1.0e7 / lpmm
[docs]def deveny_amag(grangle: float) -> float:
r"""Compute the anamorphic demagnification of the slit
The rays hitting the grating in the plane of α and β diffract to the camera
in such a way that the beam width changes as a function of α and β, whereas
rays incident on the grating in the perpendicular plane have the same beam
width upon incidence and reflection. Because there is a difference between
the beam widths for the two planes, there will be different magnification
levels (Schweizer, 1979). Whenever perpendicular planes have different
magnifications, this is called "anamorphic" (de)magnification. Schweizer
(1979), however, thinks the term "anamorphic magnification" is somewhat
inaccurate, and prefers "grating magnification". Historically, the DeVeny
manuals and associated code (`e.g.`, ``deveny_grangle``) use "anamorphic",
so we continue that there. The resulting magnification in the direction of
dispersion due to the grating, `r`, arising from differentiation of the
grating equation, is:
.. math::
r \equiv \frac{dβ}{dα} = \frac{cos α}{cosβ}
(Schweizer, 1979). Practical spectrograph design aligns the slit
perpendicular to the dispersion direction, and so the change in
magnification is in the direction of the slit width, hence our quoted
"anamorphic demagnification of slit width".
Parameters
----------
grangle : :obj:`float`
The desired grating angle (in degrees)
Returns
-------
:obj:`float`
The anamorphic demagnification factor
"""
alpha = np.deg2rad(grangle) + COLL
beta = CAMCOL - alpha
return np.cos(alpha) / np.cos(beta)
# Command Line Script Infrastructure (borrowed from PypeIt) ==================#
[docs]class DevenyGrangle(utils.ScriptBase):
"""Script class for ``deveny_grangle`` tool
Script structure borrowed from :class:`pypeit.scripts.scriptbase.ScriptBase`.
"""
[docs] @classmethod
def get_parser(cls, width=None):
"""Construct the command-line argument parser.
Parameters
----------
description : :obj:`str`, optional
A short description of the purpose of the script.
width : :obj:`int`, optional
Restrict the width of the formatted help output to be no longer
than this number of characters, if possible given the help
formatter. If None, the width is the same as the terminal
width.
formatter : :obj:`~argparse.HelpFormatter`
Class used to format the help output.
Returns
-------
:obj:`~argparse.ArgumentParser`
Command-line interpreter.
"""
parser = super().get_parser(
description="DeVeny Grating Angle Calculator", width=width
)
parser.add_argument(
"--cli",
action="store_true",
help="Use the command-line version of this tool",
)
parser.add_argument(
"--max",
action="store_true",
help="Use the MAX version of the GUI (compute wavelength from angle)",
)
return parser
[docs] @staticmethod
def main(args):
"""Main Driver
Simple function that calls the appropriate driver function.
"""
# Giddy up!
if args.cli:
deveny_grangle_cli()
else:
deveny_grangle_gui(max_gui=args.max)