diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml
index 13b4707..e040825 100644
--- a/.github/workflows/documentation.yaml
+++ b/.github/workflows/documentation.yaml
@@ -10,7 +10,9 @@ jobs:
with:
# we want to find git tags to pass version to sphinx
fetch-depth: 0
- - uses: actions/setup-python@v3
+ - uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
- name: Install dependencies
run: |
pip install sphinx sphinx_rtd_theme sphinx-automodapi .[dev]
diff --git a/README.rst b/README.rst
index 150c146..068c1f0 100644
--- a/README.rst
+++ b/README.rst
@@ -1,24 +1,30 @@
.. |License| image:: https://img.shields.io/github/license/LowellObservatory/LDTObserverTools
:target: https://github.com/LowellObservatory/LDTObserverTools/blob/main/LICENSE
-.. |astropy| image:: https://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat
+.. |astropy| image:: https://img.shields.io/badge/powered%20by-AstroPy-blue.svg?style=flat
:target: https://www.astropy.org/
.. |forks| image:: https://img.shields.io/github/forks/LowellObservatory/LDTObserverTools?style=social
- :target: https://github.com/LowellObservatory/LDTObserverTools
+ :target: https://github.com/LowellObservatory/LDTObserverTools/forks
-.. |issues| image:: https://img.shields.io/github/issues/LowellObservatory/LDTObserverTools?style=social
- :target: https://github.com/LowellObservatory/LDTObserverTools
+.. |issues| image:: https://img.shields.io/github/issues/LowellObservatory/LDTObserverTools?style=badge
+ :target: https://github.com/LowellObservatory/LDTObserverTools/issues
+
+.. |pulls| image:: https://img.shields.io/github/issues-pr/LowellObservatory/LDTObserverTools?style=badge
+ :target: https://github.com/LowellObservatory/LDTObserverTools/pulls
.. |stars| image:: https://img.shields.io/github/stars/LowellObservatory/LDTObserverTools?style=social
- :target: https://github.com/LowellObservatory/LDTObserverTools
+ :target: https://github.com/LowellObservatory/LDTObserverTools/stargazers
.. |watch| image:: https://img.shields.io/github/watchers/LowellObservatory/LDTObserverTools?style=social
- :target: https://github.com/LowellObservatory/LDTObserverTools
+ :target: https://github.com/LowellObservatory/LDTObserverTools/watchers
.. |github| image:: https://img.shields.io/badge/GitHub-LDTObserverTools-brightgreen
:target: https://github.com/LowellObservatory/LDTObserverTools
+.. |language| image:: https://img.shields.io/github/languages/top/LowellObservatory/LDTObserverTools
+ :target: https://github.com/LowellObservatory/LDTObserverTools
+
.. image:: https://raw.githubusercontent.com/LowellObservatory/LDTObserverTools/main/doc/_static/obstools_logo.png
:target: https://github.com/LowellObservatory/LDTObserverTools
:width: 500
@@ -32,6 +38,7 @@ LDTObserverTools |forks| |stars| |watch|
|github| |astropy| |License|
+|language| |issues| |pulls|
The LDTObserverTools package is a collection of command-line and GUI tools
for observers at the Lowell Discovery Telescope (LDT) in Happy Jack, AZ.
@@ -103,7 +110,7 @@ action is to setup a clean python environment into which the installation will
occur. This mitigates any possible dependency conflicts with other packages
you use.
-The recommended method of setting up a new envrionment is with ``conda``:
+The recommended method of setting up a new environment is with ``conda``:
.. code-block:: console
@@ -150,16 +157,16 @@ simply be a matter of executing:
Optional Dependencies
^^^^^^^^^^^^^^^^^^^^^
-There are no optional dependencies at this time.
+.. There are no optional dependencies at this time.
-.. Some of the instrument-specific routines in this package require additional dependencies
-.. that are not otherwise needed by the majority of the routines herein.
+Some of the instrument-specific routines in this package require additional dependencies
+that are not otherwise needed by the majority of the routines herein.
-.. - If you are using the ``deveny_pickup_cleaner`` routine, you will need the
-.. spectroscopic data reduction pipeline PypeIt for the iterative cleaning of
-.. the pickup noise. It can be installed by including it in the optional
-.. dependencies, `e.g.`:
+ - If you are using the ``scrub_deveny_pickup`` tool, you will need the
+ spectroscopic data reduction pipeline `PypeIt `_
+ for the iterative cleaning of the pickup noise. It can be installed by
+ including it in the optional dependencies, *e.g.*:
-.. .. code-block:: console
+ .. code-block:: console
-.. pip install "obstools[pypeit] @ git+https://github.com/LowellObservatory/LDTObserverTools"
+ pip install "obstools[pypeit] @ git+https://github.com/LowellObservatory/LDTObserverTools"
diff --git a/doc/api/obstools.etc_calc.rst b/doc/api/obstools.etc_calc.rst
deleted file mode 100644
index bfe7d5d..0000000
--- a/doc/api/obstools.etc_calc.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-obstools.etc\_calc module
-=========================
-
-.. automodule:: obstools.etc_calc
- :members:
- :private-members:
- :undoc-members:
- :show-inheritance:
diff --git a/doc/api/obstools.lmi_etc.rst b/doc/api/obstools.lmi_etc.rst
new file mode 100644
index 0000000..07f2218
--- /dev/null
+++ b/doc/api/obstools.lmi_etc.rst
@@ -0,0 +1,8 @@
+obstools.lmi\_etc module
+========================
+
+.. automodule:: obstools.lmi_etc
+ :members:
+ :private-members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/doc/api/obstools.rst b/doc/api/obstools.rst
index b03cf96..5d92b74 100644
--- a/doc/api/obstools.rst
+++ b/doc/api/obstools.rst
@@ -12,8 +12,8 @@ Submodules
obstools.deveny_collfocus
obstools.deveny_grangle
obstools.dfocus
- obstools.etc_calc
obstools.fix_ldt_header
+ obstools.lmi_etc
obstools.neocp_ephem
obstools.scrub_deveny_pickup
obstools.utils
diff --git a/doc/conf.py b/doc/conf.py
index ccac897..ee5dffc 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -47,6 +47,7 @@
"sphinx.ext.mathjax",
"sphinx.ext.ifconfig",
"sphinx.ext.viewcode",
+ "sphinx_subfigure",
# "sphinx.ext.autosectionlabel",
]
@@ -125,11 +126,13 @@
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
- "astropy": ("https://docs.astropy.org/en/stable/", None),
"attrs": ("https://www.attrs.org/en/stable/", None),
+ "astropy": ("https://docs.astropy.org/en/stable/", None),
+ "ccdproc": ("https://ccdproc.readthedocs.io/en/stable/", None),
"matplotlib": ("https://matplotlib.org/stable/", None),
"numpy": ("https://numpy.org/doc/stable/", None),
"sphinx": ("https://www.sphinx-doc.org/en/master/", None),
"scipy": ("https://docs.scipy.org/doc/scipy/", None),
"pypeit": ("https://pypeit.readthedocs.io/en/release/", None),
+ "stomp": ("https://jasonrbriggs.github.io/stomp.py/", None),
}
diff --git a/doc/deveny_collfocus.rst b/doc/deveny_collfocus.rst
index a8f76d3..a54be7d 100644
--- a/doc/deveny_collfocus.rst
+++ b/doc/deveny_collfocus.rst
@@ -1,3 +1,10 @@
+.. include:: include/links.rst
+
+.. |nbsp| unicode:: 0xA0
+ :trim:
+
+.. _deveny_collfocus:
+
=================================
DeVeny Collimator Focus Estimator
=================================
@@ -26,7 +33,8 @@ yield the approximate relationship:
\end{aligned}
where :math:`T_{\rm m}` is the mount temperature and tilt is the grating tilt
-angle. This equation is coded into the GUI tool.
+angle. The RMS of the fit is :math:`\sim 0.6` |nbsp| mm. This equation is coded into
+the GUI tool.
Unlike other focus procedures at LDT, the DeVeny LOUI focus sequence script
does not take the *expected* focus value, but rather one end of the range,
@@ -45,10 +53,15 @@ The script usage can be displayed by calling the script with the
.. include:: help/deveny_collfocus.rst
-When the application launches, a GUI window will appear:
+When the application launches, a GUI window will appear as in
+:numref:`collfocus_startup`.
-.. image:: figures/deveny_collfocus_startup.png
+.. _collfocus_startup:
+.. figure:: figures/deveny_collfocus_startup.png
:class: with-shadow
+ :alt: GUI at startup
+
+ -- The ``deveny_collfocus`` GUI at startup.
If the application is launched from one of the observer computers at LDT
@@ -66,10 +79,16 @@ windows. You will always need to select the rear filter setting you are using.
When you click "Compute", the bottom half of the GUI is populated with the
estimated focus value from the equation above and values to enter into the
-DeVeny LOUI focus sequence tab.
+DeVeny LOUI focus sequence tab. See :numref:`collfocus_values`.
-.. image:: figures/deveny_collfocus_values.png
+.. _collfocus_values:
+.. figure:: figures/deveny_collfocus_values.png
:class: with-shadow
+ :alt: GUI after clicking "Compute"
+
+ -- The ``deveny_collfocus`` GUI after clicking "Compute". The estimated
+ focus value (with uncertainty) is printed, along with suggested values for
+ use with the LOUI Focus Sequence tab.
Observers should note that the lower limit on the collimator focus motorized
stage is 7.75 mm. In warm weather and/or for large grating tilt angles, the
diff --git a/doc/deveny_grangle.rst b/doc/deveny_grangle.rst
index 81a7883..c2bdc37 100644
--- a/doc/deveny_grangle.rst
+++ b/doc/deveny_grangle.rst
@@ -1,3 +1,10 @@
+.. include:: include/links.rst
+
+.. |nbsp| unicode:: 0xA0
+ :trim:
+
+.. _deveny_grangle:
+
===============================
DeVeny Grating Angle Calculator
===============================
@@ -7,14 +14,41 @@ Status: Completed 2021-01-26
Overview
========
- - ``deveny_grangle``: Compute the desired grating angle based on selected
- grating and desired central wavelength. This routine comes with two interfaces.
- The default GUI features a dropdown menu for grating selection and contains error
- checking on the input for central wavelength. There is a ``MAX_GUI`` option for
- computing central wavelength given the grating angle in addition to the standard
- GUI features. Also included is a command-line interface, identical to the old
- IDL function. Online help is available with the ``-h`` option.
- [`Completed: 2021-01-26`]
+.. _grateq:
+
+The Grating Equation
+^^^^^^^^^^^^^^^^^^^^
+
+The angles at which the diffracted light reflects off the grating are given
+by the grating equation:
+
+.. math::
+
+ \begin{equation}
+ m\lambda = d(\sin \theta _{i}-\sin \theta _{m})~,
+ \end{equation}
+
+where :math:`d` is the spacing between adjacent grooves on the grating,
+:math:`\theta_{i} = \theta_{\rm grangle} + 10^{\circ}` is the incident angle,
+and :math:`\theta_{m} = 55^{\circ} - \theta_{i}` is the outgoing angle for light
+with wavelength :math:`\lambda` diffracting into order :math:`m` (see the
+DeVeny user manual for a description of the physical layout of the
+spectrograph). The DeVeny gratings are typically operated in
+1\ :superscript:`st`\ order (:math:`m = 1`), although use of :math:`m=2` would
+possible with the proper short-pass order-blocking filters to remove
+1\ :superscript:`st`\ -order light.
+
+This tool computes the necessary tilt :math:`\theta_{\rm grangle}` by
+numerically solving the equation below for a given grating. The computation
+uses the groove density (in g/mm) in place of :math:`d`, and finds
+:math:`\theta_{\rm grangle}` for a specified 1\ :superscript:`st`\ -order
+central wavelength (:math:`\lambda_c`) in Angstroms on the spectral CCD.
+
+.. math::
+
+ \begin{equation}
+ \lambda_c~({\rm \mathring{A}}) = \frac{\sin(\theta_{\rm grangle} +10^{\circ}) - \sin(45^{\circ} - \theta_{\rm grangle})}{ {\rm groove~density~(g/mm)} }\times 10^7
+ \end{equation}
Usage
@@ -24,3 +58,37 @@ The script usage can be displayed by calling the script with the
``-h`` option:
.. include:: help/deveny_grangle.rst
+
+In its default mode, the tool launches a GUI window as in
+:numref:`grangle_startup`.
+
+.. _grangle_startup:
+.. figure:: figures/deveny_grangle_startup.png
+ :class: with-shadow
+ :alt: GUI at startup
+
+ -- The ``deveny_collfocus`` GUI at startup.
+
+Select your grating from the dropdown menu (this selects the groove density),
+and enter your desired central wavelength (in angstroms). When you click
+"Compute", the bottom half of the GUI is populated with the needed grating tilt
+value from the equation above and the computed slit demagnification value (see
+the DeVeny user manual for a brief discussion of grating physics). See
+:numref:`grangle_values`.
+
+.. _grangle_values:
+.. figure:: figures/deveny_grangle_values.png
+ :class: with-shadow
+ :alt: GUI after clicking "Compute"
+
+ -- The ``deveny_grangle`` GUI after clicking "Compute". The needed grating
+ tilt angle and computed slit demagnification values have been populated.
+
+There are two optional modes for running this tool:
+
+ * ``--cli``: A command-line version of this tool, which looks and functions
+ identical to the old IDL routine with the same name.
+
+ * ``--max``: The GUI will have the option to compute the central wavelength
+ from :math:`\theta_{\rm grangle}` in addition to the forward calculation
+ done in the default mode.
diff --git a/doc/dfocus.rst b/doc/dfocus.rst
index af17f69..5801256 100644
--- a/doc/dfocus.rst
+++ b/doc/dfocus.rst
@@ -1,3 +1,10 @@
+.. include:: include/links.rst
+
+.. |nbsp| unicode:: 0xA0
+ :trim:
+
+.. _dfocus:
+
==================================
DeVeny Collimator Focus Calculator
==================================
@@ -7,25 +14,122 @@ Status: Completed 2021-11-19
Overview
========
- - ``dfocus``: Compute the needed collimator focus based on a series of arc line
- frames taken at various collimator settings. Read in the arc lamp frames in
- the current night's focus directory, find the appropriate spectral lines in each
- frame, compute the FHWM (or other measure) of those lines, plot the FHWM as a
- function of collimator position and suggest the optimal focus position. This
- program is executed identically to the old IDL version. The python version
- uses :obj:`scipy.signal` processing routines for identifying line peaks and widths,
- resulting in more accurate and consistent estimates of the correct collimator
- focus position. Rather than separately producing plots to the screen and disk,
- this version writes all plots to a PDF, then launches ``Preview.app`` to display
- the plots on the screen. Newly added is a readout of the mount temperature so
- the user can determine when/if the collimator needs to be refocused during the
- night.
+The internal optics of the DeVeny Spectrograph are focused by pistoning the
+collimator mirror (a separate task from focusing the telescope onto the
+spectrograph). The DeVeny LOUI includes a tab for performing a focus sequence
+(images of arc lines interleaved with collimator focus moves). The
+:ref:`deveny_collfocus` tool is used to estimate the focus value and sequence
+range needed.
+
+Once the sequence of focus images has been collected, this tool will analyze
+the arc lines in each frame to calculate the optimal position at which you
+should set the collimator. See :ref:`dfocus_usage` for details of the
+command-line options for use with the tool. When run, the following processing
+steps are applied to the focus frames:
+
+ #. All frames are read in, and the middle frame is inspected to find
+ appropriate spectral lines for analysis. (This frame is what is shown
+ in :numref:`pyfocus_p1`, theoretically the frame closest to focus,
+ especially if ``deveny_collfocus`` was used to generate the sequence
+ range.)
+ #. The marked lines are then identified in all the other frames, and the
+ FWHM for each is computed using :obj:`scipy.signal` processing routines.
+ #. The FWHM as a function of collimator position is computed for each line
+ identified, and a parabola is fit to the plot (see
+ :numref:`pyfocus_p3`).
+ #. For arc each line, the minimum (red lines in :numref:`pyfocus_p3`) and
+ optimal (blue lines) focus values are computed. See the DeVeny manual
+ for a discussion of astigmatism and why the two values are not the same.
+ #. Finally, the optimal focus value is plotted as a function of CCD column
+ position (:numref:`pyfocus_p2`) to find the overall optimal collimator
+ focus value to use.
+
+An example terminal output corresponding to the figures is shown below:
+
+ .. code-block:: console
+
+ ================================================================================
+ DeVeny Collimator Focus Calculator
+
+ Processing center focus image /deveny/20210520a/20210520.0177.fits...
+ Background level: 2351.1 Detection threshold level: 2451.1
+ Number of lines found: 39
+
+ Processing arc images...
+ 100%|████████████████████████████████████████| 10/10 [00:00<00:00, 40.87frame/s]
+
+ Median value of all linewidths: 3.03 pix
+ ================================================================================
+ *** Recommended (Median) Optimal Focus Position: 10.49 mm
+ *** Note: Current Mount Temperature is: 18.0ºC
+
+ Plots have been saved to: pyfocus.20210520.040659.pdf
+
+In both the terminal output and the plots, the current mount temperature is
+noted so that the observer can judge if the collimator focus needs to be
+revisited during the night based on temperature changes. The collimator focus
+temperature term is approximately :math:`-0.08~{\rm mm/C^{\circ}}`, meaning that
+a temperature *decrease* of :math:`5 {\rm~C^{\circ}}` will cause a
+:math:`0.4~{\rm mm}` *increase* in the optimal collimator focus position.
+
+
+.. _pyfocus_p1:
+.. figure:: figures/pyfocus.page1_example.*
+ :class: with-shadow
+ :alt: Arc line plot
+
+ -- Example of page 1 of the output PDF, arc line identification plot.
+.. _pyfocus_p2:
+.. figure:: figures/pyfocus.page2_example.*
+ :class: with-shadow
+ :alt: Optimal focus versus line position
+
+ -- Example of page 2 of the output PDF, optimal focus versus line position plot.
+
+
+.. _pyfocus_p3:
+.. figure:: figures/pyfocus.page3_example.*
+ :class: with-shadow
+ :alt: Individual line focus curves
+
+ -- Example of page 3 of the output PDF, individual line focus curves.
+
+
+
+
+.. _dfocus_usage:
+
Usage
=====
The script usage can be displayed by calling the script with the
``-h`` option:
-.. include:: help/dfocus.rst
\ No newline at end of file
+.. include:: help/dfocus.rst
+
+This command-line tool must be invoked from the ``/deveny//focus``
+directory on the observer machine (``dct-obs1`` / ``dct-obs2``) so that it can
+find the focus index files created by the DeVeny LOUI. To run the tool on the
+most recent focus sequence taken, simply run the routine with no options.
+
+If more than one focus sequence was taken, the tool can analyze a particular
+sequence by using the ``--flog`` optional input, where the file log has the
+form ``deveny_focus..``. For instance, to produce the plots in
+:numref:`pyfocus_p1` - :numref:`pyfocus_p3`, you would:
+
+ .. code-block:: console
+
+ $ cd /deveny/20210520/focus
+ $ dfocus --flog deveny_focus.20210520.040659
+
+If you want to increase the threshold for detected lines above the default
+100 |nbsp| DN over background (to decrease the number of lines detected), use
+the ``--thresh`` optional input.
+
+By default, the tool tries to launch Apple's Preview App (the observer machines
+at LDT are iMacs) to display the plots shown in :numref:`pyfocus_p1` -
+:numref:`pyfocus_p3`. If running on macOS, and you desire to **not** display
+the plots, use the ``--nodisplay`` option. If this tool is being run on a
+different operating system, it will simply bypass this step.
diff --git a/doc/figures/deveny_collfocus_values.png b/doc/figures/deveny_collfocus_values.png
index b39024e..8a07ac0 100644
Binary files a/doc/figures/deveny_collfocus_values.png and b/doc/figures/deveny_collfocus_values.png differ
diff --git a/doc/figures/deveny_grangle_startup.png b/doc/figures/deveny_grangle_startup.png
new file mode 100644
index 0000000..365cfb4
Binary files /dev/null and b/doc/figures/deveny_grangle_startup.png differ
diff --git a/doc/figures/deveny_grangle_values.png b/doc/figures/deveny_grangle_values.png
new file mode 100644
index 0000000..053ba4c
Binary files /dev/null and b/doc/figures/deveny_grangle_values.png differ
diff --git a/doc/figures/pyfocus.page1_example.pdf b/doc/figures/pyfocus.page1_example.pdf
new file mode 100644
index 0000000..0ec9d99
Binary files /dev/null and b/doc/figures/pyfocus.page1_example.pdf differ
diff --git a/doc/figures/pyfocus.page1_example.png b/doc/figures/pyfocus.page1_example.png
new file mode 100644
index 0000000..d4e0366
Binary files /dev/null and b/doc/figures/pyfocus.page1_example.png differ
diff --git a/doc/figures/pyfocus.page1_example.svg b/doc/figures/pyfocus.page1_example.svg
new file mode 100644
index 0000000..171252f
--- /dev/null
+++ b/doc/figures/pyfocus.page1_example.svg
@@ -0,0 +1,2981 @@
+
+
+
diff --git a/doc/figures/pyfocus.page2_example.pdf b/doc/figures/pyfocus.page2_example.pdf
new file mode 100644
index 0000000..9ebf733
Binary files /dev/null and b/doc/figures/pyfocus.page2_example.pdf differ
diff --git a/doc/figures/pyfocus.page2_example.png b/doc/figures/pyfocus.page2_example.png
new file mode 100644
index 0000000..e1a1053
Binary files /dev/null and b/doc/figures/pyfocus.page2_example.png differ
diff --git a/doc/figures/pyfocus.page2_example.svg b/doc/figures/pyfocus.page2_example.svg
new file mode 100644
index 0000000..58fe4d0
--- /dev/null
+++ b/doc/figures/pyfocus.page2_example.svg
@@ -0,0 +1,1739 @@
+
+
+
diff --git a/doc/figures/pyfocus.page3_example.pdf b/doc/figures/pyfocus.page3_example.pdf
new file mode 100644
index 0000000..b0543e5
Binary files /dev/null and b/doc/figures/pyfocus.page3_example.pdf differ
diff --git a/doc/figures/pyfocus.page3_example.png b/doc/figures/pyfocus.page3_example.png
new file mode 100644
index 0000000..1e0b6c9
Binary files /dev/null and b/doc/figures/pyfocus.page3_example.png differ
diff --git a/doc/figures/pyfocus.page3_example.svg b/doc/figures/pyfocus.page3_example.svg
new file mode 100644
index 0000000..bc46234
--- /dev/null
+++ b/doc/figures/pyfocus.page3_example.svg
@@ -0,0 +1,12828 @@
+
+
+
diff --git a/doc/figures/pypeit_spec2d_noise_postscrub.png b/doc/figures/pypeit_spec2d_noise_postscrub.png
new file mode 100644
index 0000000..98532df
Binary files /dev/null and b/doc/figures/pypeit_spec2d_noise_postscrub.png differ
diff --git a/doc/figures/pypeit_spec2d_noise_prescrub.png b/doc/figures/pypeit_spec2d_noise_prescrub.png
new file mode 100644
index 0000000..04c1e2c
Binary files /dev/null and b/doc/figures/pypeit_spec2d_noise_prescrub.png differ
diff --git a/doc/figures/scrubber_line_fitting.png b/doc/figures/scrubber_line_fitting.png
new file mode 100644
index 0000000..3038ce1
Binary files /dev/null and b/doc/figures/scrubber_line_fitting.png differ
diff --git a/doc/fix_ldt_header.rst b/doc/fix_ldt_header.rst
index b1f5aea..cd4ff8b 100644
--- a/doc/fix_ldt_header.rst
+++ b/doc/fix_ldt_header.rst
@@ -1,3 +1,10 @@
+.. include:: include/links.rst
+
+.. |nbsp| unicode:: 0xA0
+ :trim:
+
+.. _fix_ldt_header:
+
==============================
Simple FITS Header Fixing Tool
==============================
@@ -7,13 +14,13 @@ Status: Completed 2022-10-17
Overview
========
- - ``fix_ldt_header``: Replace / add / update FITS keyword values. While named for
- the LDT, it can beused with any FITS file (or list of files). The inspiration for
- this script is the cases when the LDT / LOUI does not input the proper information
- in FITS headers (`e.g.`, ``GRATING = UNKNOWN`` for DeVeny before the drop-down
- menu has been selected). Online help is available with the ``-h`` option.
- [`Completed: 2022-10-17`]
-
+It seems like everyone has a simple tool for adjusting FITS header keywords.
+This is a fairly simple-minded one written out of frustration with the
+limitations of the ``modhead`` routine distributed with the `CFITSIO
+`_ package. It utilizes the
+:obj:`ccdproc.ImageFileCollection` functionality (based on the
+:mod:`astropy.io.fits` module) to change a FITS keyword in a list of input
+files, using python's smarter-than-C string functionality.
Usage
=====
diff --git a/doc/help/dfocus.rst b/doc/help/dfocus.rst
index 8a13e79..0b22686 100644
--- a/doc/help/dfocus.rst
+++ b/doc/help/dfocus.rst
@@ -1,14 +1,13 @@
.. code-block:: console
$ dfocus -h
- usage: dfocus [-h] [--flog FLOG] [--thresh THRESH] [--nodisplay] [--leave_files]
+ usage: dfocus [-h] [--flog FLOG] [--thresh THRESH] [--nodisplay]
DeVeny Collimator Focus Calculator
options:
-h, --help show this help message and exit
- --flog FLOG focus log to use (default: last) (default: last)
- --thresh THRESH threshold for line detection (default: 100) (default: 100.0)
- --nodisplay DO NOT launch Preview.app to display plots (default: True)
- --leave_files DO NOT move the focus frames to focus/ (default: False)
+ --flog FLOG focus log to use (default: last)
+ --thresh THRESH threshold for line detection (default: 100.0)
+ --nodisplay DO NOT launch Preview.app to display plots (default: False)
\ No newline at end of file
diff --git a/doc/help/lmi_etc.rst b/doc/help/lmi_etc.rst
new file mode 100644
index 0000000..47b55f2
--- /dev/null
+++ b/doc/help/lmi_etc.rst
@@ -0,0 +1,10 @@
+.. code-block:: console
+
+ $ lmi_etc -h
+ usage: lmi_etc [-h]
+
+ LMI Exposure Time Calculator
+
+ options:
+ -h, --help show this help message and exit
+
\ No newline at end of file
diff --git a/doc/help/neocp_ephem.rst b/doc/help/neocp_ephem.rst
new file mode 100644
index 0000000..552223b
--- /dev/null
+++ b/doc/help/neocp_ephem.rst
@@ -0,0 +1,13 @@
+.. code-block:: console
+
+ $ neocp_ephem -h
+ usage: neocp_ephem [-h] obj_id
+
+ Generate LDT Ephemeris Files for NEOCP objects
+
+ positional arguments:
+ obj_id The NEOCP temporary designation ID (e.g., 'P10vY9r')
+
+ options:
+ -h, --help show this help message and exit
+
\ No newline at end of file
diff --git a/doc/include/dependencies_table.rst b/doc/include/dependencies_table.rst
index 8593585..79bef31 100644
--- a/doc/include/dependencies_table.rst
+++ b/doc/include/dependencies_table.rst
@@ -1,5 +1,6 @@
-======================= ==========================================================================================================================================================================================
-Python Version ``>=3.10``
-Required for users ``astropy>=5.1``, ``ccdproc``, ``matplotlib``, ``numpy>=1.22``, ``pypeit[specutils]>=1.14.0``, ``pysimplegui``, ``requests``, ``scipy>=1.9``, ``setuptools``, ``setuptools_scm``, ``tqdm``
-Required for developers ``black``, ``pylint``, ``pyyaml``, ``sphinx``, ``sphinx-automodapi``, ``sphinx_rtd_theme==1.2.2``, ``stomp.py``, ``xmltodict``
-======================= ==========================================================================================================================================================================================
+================================ ===================================================================================================================================================================================
+Python Version ``>=3.10,<3.12``
+Required for users ``astropy>=5.1``, ``ccdproc``, ``darkdetect``, ``matplotlib``, ``numpy>=1.22``, ``pysimplegui``, ``requests``, ``scipy>=1.9``, ``setuptools``, ``setuptools_scm``, ``tqdm``
+Optional ``pypeit`` requirements ``pypeit[specutils]>=1.14.0``
+Required for developers ``black``, ``pylint``, ``pypeit[specutils]>=1.14.0``, ``pyyaml``, ``sphinx``, ``sphinx-automodapi``, ``sphinx-subfigure``, ``sphinx_rtd_theme==1.2.2``, ``stomp.py``, ``xmltodict``
+================================ ===================================================================================================================================================================================
diff --git a/doc/index.rst b/doc/index.rst
index 7b3e64b..d45dca8 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -3,6 +3,8 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
+.. include:: include/links.rst
+
.. include:: ../README.rst
.. _dependencies:
diff --git a/doc/input_validator.rst b/doc/input_validator.rst
index c66c890..f750e61 100644
--- a/doc/input_validator.rst
+++ b/doc/input_validator.rst
@@ -1,8 +1,15 @@
+.. include:: include/links.rst
+
+.. |nbsp| unicode:: 0xA0
+ :trim:
+
+.. _input_validator:
+
====================
Input List Validator
====================
-Status: *In development*
+Status: *Planned*
Overview
========
@@ -17,4 +24,4 @@ Overview
Usage
=====
-Yeah, use it!
\ No newline at end of file
+Usage information will be added here once this tool enters development.
diff --git a/doc/lmi_etc.rst b/doc/lmi_etc.rst
index aca048a..bfa17fc 100644
--- a/doc/lmi_etc.rst
+++ b/doc/lmi_etc.rst
@@ -1,3 +1,10 @@
+.. include:: include/links.rst
+
+.. |nbsp| unicode:: 0xA0
+ :trim:
+
+.. _lmi_etc:
+
============================
LMI Exposure Time Calculator
============================
@@ -14,4 +21,7 @@ Overview
Usage
=====
-Yeah, use it!
\ No newline at end of file
+The script usage can be displayed by calling the script with the
+``-h`` option:
+
+.. include:: help/lmi_etc.rst
diff --git a/doc/neocp_ephem.rst b/doc/neocp_ephem.rst
index 1d42fec..e16716b 100644
--- a/doc/neocp_ephem.rst
+++ b/doc/neocp_ephem.rst
@@ -1,3 +1,10 @@
+.. include:: include/links.rst
+
+.. |nbsp| unicode:: 0xA0
+ :trim:
+
+.. _neocp_ephem:
+
=========================================
NEO Confirmation Page Ephemeris Generator
=========================================
@@ -16,4 +23,7 @@ Overview
Usage
=====
-Yeah, use it!
\ No newline at end of file
+The script usage can be displayed by calling the script with the
+``-h`` option:
+
+.. include:: help/neocp_ephem.rst
diff --git a/doc/observer_target.rst b/doc/observer_target.rst
index 78f1c49..02aba00 100644
--- a/doc/observer_target.rst
+++ b/doc/observer_target.rst
@@ -1,8 +1,15 @@
+.. include:: include/links.rst
+
+.. |nbsp| unicode:: 0xA0
+ :trim:
+
+.. _observer_target:
+
=========================
Observer Target List Tool
=========================
-Status: *In development*
+Status: *Planned*
Overview
========
@@ -18,4 +25,4 @@ Overview
Usage
=====
-Yeah, use it!
\ No newline at end of file
+Usage information will be added here once this tool enters development.
diff --git a/doc/scripts/build_dependency_rst.py b/doc/scripts/build_dependency_rst.py
index 8dba6d2..1b41b2c 100644
--- a/doc/scripts/build_dependency_rst.py
+++ b/doc/scripts/build_dependency_rst.py
@@ -35,15 +35,20 @@ def write_dependency_table(setup_file: pathlib.Path, path: pathlib.Path):
user_requires = np.sort(setup["options"]["install_requires"].split("\n")[1:])
dev_requires = np.sort(setup["options.extras_require"]["dev"].split("\n")[1:])
+ pypeit_requires = np.sort(setup["options.extras_require"]["pypeit"].split("\n")[1:])
required_python = setup["options"]["python_requires"]
- data_table = np.empty((3, 2), dtype=object)
+ data_table = np.empty((4, 2), dtype=object)
data_table[0, :] = ["Python Version", f"``{required_python}``"]
data_table[1, :] = [
"Required for users",
", ".join([f"``{u}``" for u in user_requires]),
]
data_table[2, :] = [
+ "Optional ``pypeit`` requirements",
+ ", ".join([f"``{u}``" for u in pypeit_requires]),
+ ]
+ data_table[3, :] = [
"Required for developers",
", ".join([f"``{d}``" for d in dev_requires]),
]
diff --git a/doc/scripts/write_script_help.py b/doc/scripts/write_script_help.py
index 030a6dd..0c067fb 100644
--- a/doc/scripts/write_script_help.py
+++ b/doc/scripts/write_script_help.py
@@ -11,10 +11,10 @@
import time
# 3rd Party Libraries
-from pypeit.scripts import scriptbase
# Internal Imports
from obstools import script_classes
+from obstools import utils
# The repository root is up two levels from here
# NOTE: This is a hack needed for this script to run on GH pages.
@@ -23,7 +23,7 @@
# -----------------------------------------------------------------------------
-def write_help(script_cls: scriptbase.ScriptBase, opath: pathlib.Path, width: int = 80):
+def write_help(script_cls: utils.ScriptBase, opath: pathlib.Path, width: int = 80):
"""Write the ``.rst`` help files for all scripts
Parameters
@@ -35,7 +35,7 @@ def write_help(script_cls: scriptbase.ScriptBase, opath: pathlib.Path, width: in
width : :obj:`int`, optional
The width of the help text before wrapping (Default: 80)
"""
- exe = script_cls.name()
+ exe = script_cls.name
ofile = os.path.join(opath, f"{exe}.rst")
lines = [".. code-block:: console", ""]
lines += [f" $ {exe} -h"]
diff --git a/doc/scrub_deveny_pickup.rst b/doc/scrub_deveny_pickup.rst
index 4facf48..022e264 100644
--- a/doc/scrub_deveny_pickup.rst
+++ b/doc/scrub_deveny_pickup.rst
@@ -1,3 +1,10 @@
+.. include:: include/links.rst
+
+.. |nbsp| unicode:: 0xA0
+ :trim:
+
+.. _scrub_deveny_pickup:
+
============================
DeVeny Pickup Noise Scrubber
============================
@@ -53,6 +60,9 @@ signal. This patten is then subtracted from the original image and saved to
a FITS file for usual data reduction processing (preferably with PypeIt) to
yield an extracted 1D spectrum for analysis.
+Example Pre- and Post-Scrubbed Data Products
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
To illustrate the need for and utility of this tool, :numref:`spec1d_comps`
shows a comparison of the extracted 1D spectra for two different object types
from two different programs (and nights) from both the original and scrubbed
@@ -74,6 +84,36 @@ from the spectra.
estimates a mean :math:`SNR = 4.8` for the scrubbed galaxy spectrum and
a mean :math:`SNR = 7.2` for the scrubbed white dwarf spectrum.
+Of equivalent interest to the quality of the extracted spectra is the noise
+remaining after extraction of sky and objects. Shown in :numref:`prepost_noise`
+are the noise analysis plots from PyepIt for the pre- and post-scrubbed
+versions of the 2D spectral image shown in :numref:`raw_frame`. The images and
+pixel histograms are of the residual noise image, which is the science image
+minus the object and sky models, and then divided by the uncertainty image.
+The ideal pixel histogram would be a gaussian with width :math:`\sigma=1`.
+
+.. _prepost_noise:
+.. subfigure:: A|B
+ :gap: 2px
+ :class-grid: outline
+
+ .. image:: figures/pypeit_spec2d_noise_prescrub.png
+ :alt: Pre-scrubbed noise analysis plot
+
+ .. image:: figures/pypeit_spec2d_noise_postscrub.png
+ :alt: Post-scrubbed noise analysis plot
+
+ -- Noise analysis of the pre- (top) and post-scrubbed (bottom) versions of
+ the frame shown in :numref:`raw_frame`. Image values are residual image
+ divided by uncertainty, so the ideal pixel histogram would be a gaussian
+ with width :math:`\sigma=1`. Note the improvement in both the visual
+ quality of the scrubbed frame and the width and shape of the pixel histogram
+ compared to the pre-scrubbed frame.
+
+
+Outline
+^^^^^^^
+
This document begins with a description of how to use this tool to clean the
EMI pickup noise from your data, and moves on to lay out the details of what
the tool does to your data and other points to consider.
@@ -112,7 +152,7 @@ Data Processing Steps
.. warning::
- If you are using a version of PypeIt ``< 1.14.1`` (see the top line of
+ If you are using a version of PypeIt ``< 1.15.0`` (see the top line of
the PypeIt Reduction File for the version number), then you will instead
need to add the entirety of the following to the Parameter Block to
ensure the traced slit edges do not shrink unreasonably and that pattern
@@ -120,7 +160,7 @@ Data Processing Steps
Regions outside the marked slits will have 0 value in the residual image,
and therefore any sinusoidal signal there will not be fit out. (The
additional parameters included here were added to the DeVeny default set
- in PypeIt version ``1.14.1``.)
+ in PypeIt version ``1.15.0``.)
.. code-block:: ini
@@ -486,11 +526,11 @@ than that predicted from the FFT (green dashed line in the second panel of
Pickup Noise Pattern Construction
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Finally, we use the sinusoid fits to produce a pattern image. This is the
+The final result of sinusoid fits is a constructed pattern image. This is the
zero-mean sinusoid (sans quadratic polynomial) that *should* represent only the
-EMI pickup noise (as an additive AC-only signal). Subtract that and make
-additional QA plots to show how totally awesome this is!!!
-
+EMI pickup noise (as an additive AC-only signal). :numref:`image_comparisons`
+shows the process of pattern construction and its effect on the processed
+science image.
.. _image_comparisons:
.. figure:: figures/scrubber_image_comparisons.*
@@ -623,3 +663,13 @@ panel in :numref:`image_comparisons`). The result is shown in
pixels, indicating the frequency with the most power in the flattened array:
most likely the period of the AC EMI pickup noise.
+Also, say something about the actual line-by-line fits in terms of how good a
+sinusoid fits each one. Show a figure like :numref:`line_by_line`.
+
+.. _line_by_line:
+.. figure:: figures/scrubber_line_fitting.png
+ :class: with-shadow
+ :alt: Line-by-line fitting examples
+
+ -- Examples of individual line fits for 4 randomly selected lines from the
+ image shown in :numref:`raw_frame`.
diff --git a/obstools/__init__.py b/obstools/__init__.py
index eb0e9e3..ce1fae7 100644
--- a/obstools/__init__.py
+++ b/obstools/__init__.py
@@ -20,11 +20,16 @@
from obstools.version import version
# Imports for building help docs
-from obstools import deveny_collfocus
-from obstools import deveny_grangle
-from obstools import dfocus
-from obstools import fix_ldt_header
-from obstools import scrub_deveny_pickup
+try:
+ from obstools import deveny_collfocus
+ from obstools import deveny_grangle
+ from obstools import dfocus
+ from obstools import lmi_etc
+ from obstools import fix_ldt_header
+ from obstools import neocp_ephem
+ from obstools import scrub_deveny_pickup
+except ImportError as err:
+ pass
def short_warning(message, category, filename, lineno, file=None, line=None):
@@ -51,19 +56,13 @@ def script_classes() -> dict:
Dictionary of {name:class} for all script classes
"""
import numpy as np
- from pypeit.scripts import scriptbase
- from pypeit.utils import all_subclasses
+ from obstools import utils
# Recursively collect all subclasses
- # Since we use PypeIt's ScriptBase, remove all classes from that package
- scr_class = np.array(
- [
- cls
- for cls in list(all_subclasses(scriptbase.ScriptBase))
- if "pypeit" not in cls.name()
- ]
- )
- scr_name = np.array([c.name() for c in scr_class])
+ scr_class = np.array(list(utils.all_subclasses(utils.ScriptBase)))
+ print(scr_class)
+
+ scr_name = np.array([c.name for c in scr_class])
# Construct a dictionary with the script name and class
srt = np.argsort(scr_name)
return dict(zip(scr_name[srt], scr_class[srt]))
diff --git a/obstools/broker_listener.py b/obstools/broker_listener.py
index 0ed531e..9fb20ee 100644
--- a/obstools/broker_listener.py
+++ b/obstools/broker_listener.py
@@ -115,7 +115,8 @@ def on_message(self, message):
topic = message.headers["destination"]
if self.parent.config["mounttemp_incoming_topic"] in topic:
- self.parent.mounttemp_from_broker = {"MountTemp": float(message.body)}
+ status = xmltodict.parse(message.body)["TCSTelemetry"]
+ self.parent.mounttemp_from_broker = self.parent.parse_deveny(status)
if self.parent.config["grangle_incoming_topic"] in topic:
status = xmltodict.parse(message.body)["DevenyTelemetry"]
diff --git a/obstools/config/README b/obstools/config/README
new file mode 100644
index 0000000..651d3e0
--- /dev/null
+++ b/obstools/config/README
@@ -0,0 +1,10 @@
+If wanting to use the ActiveMQ broker with deveny_collfocus, this directory
+needs to have a file called activemq_joe.yaml that contains the following
+information:
+
+---
+broker_hosts:
+ -
+ -
+mounttemp_incoming_topic:
+grangle_incoming_topic:
diff --git a/obstools/deveny_collfocus.py b/obstools/deveny_collfocus.py
index fa765b4..30a2083 100755
--- a/obstools/deveny_collfocus.py
+++ b/obstools/deveny_collfocus.py
@@ -28,12 +28,12 @@
"""
# Built-In Libraries
+import argparse
import os
import sys
# 3rd-Party Libraries
import numpy as np
-from pypeit.scripts import scriptbase
import PySimpleGUI as sg
# Local Libraries
@@ -50,19 +50,29 @@
STEPSIZE = 0.5 # Default step size (in mm)
-def deveny_collfocus():
+def deveny_collfocus(debug: bool = False):
"""Main Driver for the DeVeny Collimator Focus Sequence Estimator GUI
Compute the estimated focus and LOUI Focus Sequence range given the mount
temperature, grating tilt angle, and presence of an order-blocking filter.
Optionally, read in the current mount temperature and grating tilt angle
from the ActiveMQ broker, if running at the site.
+
+ Parameters
+ ----------
+ debug : :obj:`bool`, optional
+ Print debug statements? (Default: False)
"""
# Check that stomp was imported (in broker_listener) and that the config file exists
use_stomp = "stomp" in sys.modules and (utils.CONFIG / "activemq_joe.yaml").exists()
# Fire up the broker Listener
if use_stomp:
am_radio = broker_listener.ActiveMQ_Listener(utils.CONFIG / "activemq_joe.yaml")
+ if debug:
+ print(
+ f"The parts of use_stomp = {use_stomp}: sys.modules = {'stomp' in sys.modules} "
+ f"config exists = {(utils.CONFIG / 'activemq_joe.yaml').exists()}"
+ )
# Define the color scheme for the GUI
sg.theme(utils.SG_THEME)
@@ -172,7 +182,7 @@ def deveny_collfocus():
focseq_range = calculate_focus_sequence(est_focus)
# Update the window with the calculated values
- window["-FOCUSOUT-"].update(f"{np.round(est_focus,1)} mm")
+ window["-FOCUSOUT-"].update(f"{np.round(est_focus,1)} ± 0.6 mm")
window["-STARTPOS-"].update(f"{np.round(focseq_range[0],1)}")
window["-STEPSIZE-"].update(f"{np.round(focseq_range[1],1)}")
window["-NSTEPS-"].update(f"{focseq_range[2]}")
@@ -286,28 +296,20 @@ def extract_broker_values(status_dict: dict) -> tuple[str, str]:
return f"{status_dict['GratingTilt']:.2f}"
# Mount Temperature
- if "MountTemp" in status_dict:
- return f"{status_dict['MountTemp']:.1f}"
+ if "MountTemperature" in status_dict:
+ return f"{status_dict['MountTemperature']:.1f}"
# Else, nothing found
return "~ Not Found ~"
# Command Line Script Infrastructure (borrowed from PypeIt) ==================#
-class DevenyCollfocus(scriptbase.ScriptBase):
+class DevenyCollfocus(utils.ScriptBase):
"""Script class for ``deveny_collfocus`` tool
- Script structure borrowed from :class:`pypeit.scripts.sciptbase.ScriptBase`.
+ Script structure borrowed from :class:`pypeit.scripts.scriptbase.ScriptBase`.
"""
- @classmethod
- def name(cls):
- """
- Provide the name of the script. By default, this is the name of the
- module.
- """
- return f"{cls.__module__.rsplit('.', maxsplit=1)[-1]}"
-
@classmethod
def get_parser(cls, width=None):
"""Construct the command-line argument parser.
@@ -333,6 +335,9 @@ def get_parser(cls, width=None):
parser = super().get_parser(
description="DeVeny Collimator Focus Sequence Estimator", width=width
)
+ parser.add_argument(
+ "-d", "--debug", action="store_true", default=False, help=argparse.SUPPRESS
+ )
return parser
@staticmethod
@@ -342,4 +347,4 @@ def main(args):
Simple function that calls the main driver function.
"""
# Giddy up!
- deveny_collfocus()
+ deveny_collfocus(debug=args.debug)
diff --git a/obstools/deveny_grangle.py b/obstools/deveny_grangle.py
index ee36b53..b801756 100644
--- a/obstools/deveny_grangle.py
+++ b/obstools/deveny_grangle.py
@@ -29,7 +29,6 @@
# 3rd-Party Libraries
import numpy as np
import scipy.optimize
-from pypeit.scripts import scriptbase
import PySimpleGUI as sg
# Local Libraries
@@ -374,20 +373,12 @@ def deveny_amag(grangle: float) -> float:
# Command Line Script Infrastructure (borrowed from PypeIt) ==================#
-class DevenyGrangle(scriptbase.ScriptBase):
+class DevenyGrangle(utils.ScriptBase):
"""Script class for ``deveny_grangle`` tool
- Script structure borrowed from :class:`pypeit.scripts.sciptbase.ScriptBase`.
+ Script structure borrowed from :class:`pypeit.scripts.scriptbase.ScriptBase`.
"""
- @classmethod
- def name(cls):
- """
- Provide the name of the script. By default, this is the name of the
- module.
- """
- return f"{cls.__module__.rsplit('.', maxsplit=1)[-1]}"
-
@classmethod
def get_parser(cls, width=None):
"""Construct the command-line argument parser.
diff --git a/obstools/dfocus.py b/obstools/dfocus.py
index ddc42a2..516293f 100644
--- a/obstools/dfocus.py
+++ b/obstools/dfocus.py
@@ -25,9 +25,9 @@
"""
# Built-In Libraries
+import argparse
import os
import pathlib
-import shutil
import sys
import warnings
@@ -36,7 +36,6 @@
from matplotlib.backends.backend_pdf import PdfPages
import matplotlib.pyplot as plt
import numpy as np
-from pypeit.scripts import scriptbase
import scipy.signal
from tqdm import tqdm
@@ -52,7 +51,7 @@ def dfocus(
thresh: float = 100.0,
debug: bool = False,
launch_preview: bool = True,
- leave_focus_files: bool = False,
+ docfig: bool = False,
):
"""Find the optimal DeVeny collimator focus value
@@ -75,8 +74,8 @@ def dfocus(
Print debug statements (Default: False)
launch_preview : :obj:`bool`, optional
Display the plots by launching Preview (Default: True)
- leave_focus_files : :obj:`bool`, optional
- Do NOT move the focus frames to the ``focus/`` directory (Default: False)
+ docfig : :obj:`bool`, optional
+ Make example figures for online documentation? (Default: False)
"""
# Make a pretty title for the output of the routine
n_cols = (os.get_terminal_size()).columns
@@ -161,10 +160,20 @@ def dfocus(
focus_dict=focus,
pdf=pdf,
verbose=False,
+ path=path,
+ docfig=docfig,
)
# The plot shown in the IDL2 window: Plot of best-fit fwid vs centers
- plot_optimal_focus(focus, centers, optimal_focus_values, med_opt_focus, pdf=pdf)
+ plot_optimal_focus(
+ focus,
+ centers,
+ optimal_focus_values,
+ med_opt_focus,
+ pdf=pdf,
+ path=path,
+ docfig=docfig,
+ )
# The plot shown in the IDL1 window: Focus curves for each identified line
plot_focus_curves(
@@ -178,13 +187,12 @@ def dfocus(
focus["start"],
fnom=focus["nominal"],
pdf=pdf,
+ path=path,
+ docfig=docfig,
)
- # Print the location of the plots, and move focus frames (if desired)
+ # Print the location of the plots
print(f"\n Plots have been saved to: {pdf_fn.name}\n")
- if not leave_focus_files:
- for foc_file in focus["files"]:
- shutil.move(foc_file, path)
# Try to open with Apple's Preview App... if can't, oh well.
if launch_preview:
@@ -459,10 +467,12 @@ def find_lines(
image,
thresh=20.0,
minsep=11,
- verbose=True,
- do_plot=False,
+ verbose: bool = True,
+ do_plot: bool = False,
focus_dict=None,
pdf=None,
+ docfig: bool = False,
+ path: pathlib.Path = None,
):
"""Automatically find and centroid lines in a 1-row image
@@ -542,6 +552,9 @@ def find_lines(
plt.show()
else:
pdf.savefig()
+ if docfig:
+ for ext in ["png", "pdf", "svg"]:
+ plt.savefig(path / f"pyfocus.page1_example.{ext}")
plt.close()
return len(centers), centers, fwhm
@@ -644,7 +657,14 @@ def fit_focus_curves(fwhm, fnom=2.7, norder=2, debug=False):
# Plotting Routines ==========================================================#
def plot_optimal_focus(
- focus, centers, optimal_focus_values, med_opt_focus, debug=False, pdf=None
+ focus,
+ centers,
+ optimal_focus_values,
+ med_opt_focus,
+ debug: bool = False,
+ pdf=None,
+ docfig: bool = False,
+ path: pathlib.Path = None,
):
"""Make the Optimal Focus Plot (IDL2 Window)
@@ -695,6 +715,9 @@ def plot_optimal_focus(
plt.show()
else:
pdf.savefig()
+ if docfig:
+ for ext in ["png", "pdf", "svg"]:
+ plt.savefig(path / f"pyfocus.page2_example.{ext}")
plt.close()
@@ -709,6 +732,8 @@ def plot_focus_curves(
focus_0,
fnom=2.7,
pdf=None,
+ docfig: bool = False,
+ path: pathlib.Path = None,
):
"""Make the big plot of all the focus curves (IDL1 Window)
@@ -781,6 +806,10 @@ def plot_focus_curves(
plt.show()
else:
pdf.savefig()
+ if docfig:
+ for ext in ["png", "pdf", "svg"]:
+ plt.savefig(path / f"pyfocus.page3_example.{ext}")
+
plt.close()
@@ -821,20 +850,12 @@ def find_lines_in_spectrum(filename, thresh=100.0):
# Command Line Script Infrastructure (borrowed from PypeIt) ==================#
-class DFocus(scriptbase.ScriptBase):
+class DFocus(utils.ScriptBase):
"""Script class for ``dfocus`` tool
- Script structure borrowed from :class:`pypeit.scripts.sciptbase.ScriptBase`.
+ Script structure borrowed from :class:`pypeit.scripts.scriptbase.ScriptBase`.
"""
- @classmethod
- def name(cls):
- """
- Provide the name of the script. By default, this is the name of the
- module.
- """
- return f"{cls.__module__.rsplit('.', maxsplit=1)[-1]}"
-
@classmethod
def get_parser(cls, width=None):
"""Construct the command-line argument parser.
@@ -864,26 +885,23 @@ def get_parser(cls, width=None):
"--flog",
action="store",
type=str,
- help="focus log to use (default: last)",
+ help="focus log to use",
default="last",
)
parser.add_argument(
"--thresh",
action="store",
type=float,
- help="threshold for line detection (default: 100)",
+ help="threshold for line detection",
default=100.0,
)
parser.add_argument(
"--nodisplay",
- action="store_false",
- help="DO NOT launch Preview.app to display plots",
- )
- parser.add_argument(
- "--leave_files",
action="store_true",
- help="DO NOT move the focus frames to focus/",
+ help="DO NOT launch Preview.app to display plots",
)
+ # Produce multiple graphics outputs for the documentation -- HIDDEN
+ parser.add_argument("-g", action="store_true", help=argparse.SUPPRESS)
return parser
@staticmethod
@@ -897,6 +915,6 @@ def main(args):
pathlib.Path(".").resolve(),
flog=args.flog,
thresh=args.thresh,
- launch_preview=args.nodisplay,
- leave_focus_files=args.leave_files,
+ launch_preview=not args.nodisplay,
+ docfig=args.g,
)
diff --git a/obstools/fix_ldt_header.py b/obstools/fix_ldt_header.py
index 0980e41..6497461 100644
--- a/obstools/fix_ldt_header.py
+++ b/obstools/fix_ldt_header.py
@@ -25,9 +25,9 @@
# 3rd-Party Libraries
import ccdproc
-from pypeit.scripts import scriptbase
# Local Libraries
+from obstools import utils
# CONSTANTS
@@ -65,20 +65,12 @@ def fix_ldt_header(files: str | pathlib.Path | list, keyword: str, new_value):
# Command Line Script Infrastructure (borrowed from PypeIt) ==================#
-class FixLdtHeader(scriptbase.ScriptBase):
+class FixLdtHeader(utils.ScriptBase):
"""Script class for ``fix_ldt_header`` tool
- Script structure borrowed from :class:`pypeit.scripts.sciptbase.ScriptBase`.
+ Script structure borrowed from :class:`pypeit.scripts.scriptbase.ScriptBase`.
"""
- @classmethod
- def name(cls):
- """
- Provide the name of the script. By default, this is the name of the
- module.
- """
- return f"{cls.__module__.rsplit('.', maxsplit=1)[-1]}"
-
@classmethod
def get_parser(cls, width=None):
"""Construct the command-line argument parser.
diff --git a/obstools/etc_calc.py b/obstools/lmi_etc.py
similarity index 92%
rename from obstools/etc_calc.py
rename to obstools/lmi_etc.py
index 05849ad..8d4ae37 100644
--- a/obstools/etc_calc.py
+++ b/obstools/lmi_etc.py
@@ -526,3 +526,46 @@ def sky_count_per_sec_per_ap(
rscale = SCALE * binning
sky_count_per_pixel_per_sec = sky_count_per_arcsec2_per_sec * rscale * rscale
return number_pixels(seeing, binning) * sky_count_per_pixel_per_sec
+
+
+# Command Line Script Infrastructure (borrowed from PypeIt) ==================#
+class LmiEtc(utils.ScriptBase):
+ """Script class for ``lmi_etc`` tool
+
+ Script structure borrowed from :class:`pypeit.scripts.scriptbase.ScriptBase`.
+ """
+
+ @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="LMI Exposure Time Calculator", width=width
+ )
+ return parser
+
+ @staticmethod
+ def main(args):
+ """Main Driver
+
+ Simple function that calls the main driver function.
+ """
+ # Giddy up!
diff --git a/obstools/neocp_ephem.py b/obstools/neocp_ephem.py
index 27faf3d..f8170a2 100644
--- a/obstools/neocp_ephem.py
+++ b/obstools/neocp_ephem.py
@@ -85,6 +85,7 @@
yyyy mm dd hh mm ss αh αm αs.sss ±δd δm δs.ss
.. warning::
+
This module is not yet functional!
"""
@@ -100,6 +101,7 @@
import requests
# Local Libraries
+from obstools import utils
def neocp_ephem(neocp_id):
@@ -181,19 +183,51 @@ def neocp_ephem(neocp_id):
f_obj.write("FK5 J2000.0 2000.0\n")
-def entry_point():
- """Command-Line Entry Point"""
- # Parse command line arguments
- parser = argparse.ArgumentParser(
- prog="neocp_ephem", description="Generate LDT Ephemeris Files for NEOCP objects"
- )
- parser.add_argument(
- "obj_id",
- action="store",
- type=str,
- help="The NEOCP temporary designation ID (e.g., 'P10vY9r')",
- )
- args = parser.parse_args()
-
- # Giddy Up!
- sys.exit(neocp_ephem(args.obj_id))
+# Command Line Script Infrastructure (borrowed from PypeIt) ==================#
+class NeocpEphem(utils.ScriptBase):
+ """Script class for ``neocp_ephem`` tool
+
+ Script structure borrowed from :class:`pypeit.scripts.scriptbase.ScriptBase`.
+ """
+
+ @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="Generate LDT Ephemeris Files for NEOCP objects", width=width
+ )
+ parser.add_argument(
+ "obj_id",
+ action="store",
+ type=str,
+ help="The NEOCP temporary designation ID (e.g., 'P10vY9r')",
+ )
+ return parser
+
+ @staticmethod
+ def main(args):
+ """Main Driver
+
+ Simple function that calls the main driver function.
+ """
+ # Giddy up!
+ neocp_ephem(args.obj_id)
diff --git a/obstools/scrub_deveny_pickup.py b/obstools/scrub_deveny_pickup.py
index 864d210..c943254 100644
--- a/obstools/scrub_deveny_pickup.py
+++ b/obstools/scrub_deveny_pickup.py
@@ -61,7 +61,6 @@
import matplotlib.pyplot as plt
import numpy as np
from pypeit import msgs
-from pypeit.scripts import scriptbase
import pypeit.spec2dobj
import scipy.fft
import scipy.ndimage
@@ -132,7 +131,7 @@ def iterative_pypeit_clean(
try:
# Look for the spec2d file
spec2d_file = [
- next(d.joinpath("Science").glob(f"spec2d_{filename.stem}-*"))
+ next(d.joinpath("Science").glob(f"spec2d_{filename.stem}-*.fits"))
for d in pyp_dir
][0]
except (StopIteration, IndexError):
@@ -1609,20 +1608,12 @@ def pixper_tofrom_hz(val: np.ndarray) -> np.ndarray:
# Command Line Script Infrastructure (borrowed from PypeIt) ==================#
-class ScrubDevenyPickup(scriptbase.ScriptBase):
+class ScrubDevenyPickup(utils.ScriptBase):
"""Script class for ``scrub_deveny_pickup`` tool
Script structure borrowed from :class:`pypeit.scripts.scriptbase.ScriptBase`.
"""
- @classmethod
- def name(cls):
- """
- Provide the name of the script. By default, this is the name of the
- module.
- """
- return f"{cls.__module__.rsplit('.', maxsplit=1)[-1]}"
-
@classmethod
def get_parser(cls, width=None):
"""Construct the command-line argument parser.
@@ -1666,13 +1657,13 @@ def get_parser(cls, width=None):
"--diagnostics",
action="store_true",
help="Output additional information and plots during the analysis for "
- "debugging purposes",#argparse.SUPPRESS
+ "debugging purposes", # argparse.SUPPRESS
)
parser.add_argument(
"-n",
"--no_refit",
action="store_true",
- help="Force no refit of 'bad' RMS values"#argparse.SUPPRESS
+ help="Force no refit of 'bad' RMS values", # argparse.SUPPRESS
)
# Produce multiple graphics outputs for the documentation -- HIDDEN
parser.add_argument("-g", action="store_true", help=argparse.SUPPRESS)
diff --git a/obstools/utils.py b/obstools/utils.py
index a69eb08..5f308b5 100644
--- a/obstools/utils.py
+++ b/obstools/utils.py
@@ -22,21 +22,55 @@
"""
# Built-In Libraries
+import argparse
+from functools import reduce
from importlib import resources
+import os
+import textwrap
+import sys
import warnings
# 3rd-Party Libraries
+import darkdetect
import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize
-
# Local Libraries
+from obstools.version import version as __version__
# CONSTANTS
-SG_THEME = "light grey 1"
CONFIG = resources.files("obstools") / "config"
DATA = resources.files("obstools") / "data"
+# Modify SG theme based on system Light/Dark theme
+SG_THEME = "dark gray 14" if darkdetect.isDark() else "light grey 1"
+
+
+def all_subclasses(cls):
+ """
+ Collect all the subclasses of the provided class.
+
+ .. note::
+
+ This function borrowed from PypeIt
+
+ The search follows the inheritance to the highest-level class. Intermediate
+ base classes are included in the returned set, but not the base class itself.
+
+ Thanks to:
+ https://stackoverflow.com/questions/3862310/how-to-find-all-the-subclasses-of-a-class-given-its-name
+
+ Args:
+ cls (object):
+ The base class
+
+ Returns:
+ :obj:`set`: The unique set of derived classes, including any
+ intermediate base classes in the inheritance thread.
+ """
+ return set(cls.__subclasses__()).union(
+ [s for c in cls.__subclasses__() for s in all_subclasses(c)]
+ )
def check_float(potential_float) -> bool:
@@ -460,3 +494,210 @@ def warn_and_return_zeros(return_full: bool, x, xx, yy, order, raise_warn=False)
yfit = [0] * len(x)
return [0] * (order + 1), yfit, xx, yy
return [0] * (order + 1)
+
+
+"""
+Implements base classes for use with ``PypeIt`` scripts.
+
+.. include common links, assuming primary doc root is up one directory
+.. include:: ../include/links.rst
+
+"""
+
+
+class SmartFormatter(argparse.HelpFormatter):
+ r"""
+ Enable a combination of both fixed-format and wrappable lines to be
+ formatted for the help statements for command-line arguments used with
+ :class:`~argparse.ArgumentParser`.
+
+ Borrows from
+ https://stackoverflow.com/questions/3853722/python-argparse-how-to-insert-newline-in-the-help-text
+
+ Help strings that use this formatter *must* begin with "R|". If not, the
+ help string is parsed by the base class.
+
+ When parsed by this formatter, the leading "R|" characters are stripped and
+ the lines to be printed are parsed using :func:`~str.splitlines`. Each resulting
+ line is wrapped using :func:`~textwrap.wrap`, unless it begins with the characters
+ "F|", which forces the line to remain unaltered (except for stripping the
+ leading characters).
+
+ For example, if you add an argument like this:
+
+ .. code-block:: python
+
+ parser.add_argument('-t', '--tell_file', type=str,
+ help='R|Configuration file to change default telluric parameters. '
+ 'Note that the parameters in this file will be overwritten if '
+ 'you set argument in your terminal. The --tell_file option '
+ 'requires a .tell file with the following format:\n'
+ '\n'
+ 'F| [tellfit]\n'
+ 'F| objmodel = qso\n'
+ 'F| redshift = 7.6\n'
+ 'F| bal_wv_min_max = 10825,12060\n'
+ 'OR\n'
+ 'F| [tellfit]\n'
+ 'F| objmodel = star\n'
+ 'F| star_type = A0\n'
+ 'F| star_mag = 8.\n'
+ 'OR\n'
+ 'F| [tellfit]\n'
+ 'F| objmodel = poly\n'
+ 'F| polyorder = 3\n'
+ 'F| fit_wv_min_max = 9000.,9500.\n'
+ '\n')
+
+ The result will be (depending on the width of your console):
+
+ .. code-block:: console
+
+ -t TELL_FILE, --tell_file TELL_FILE
+ Configuration file to change default telluric
+ parameters. Note that the parameters in this file
+ will be overwritten if you set argument in your
+ terminal. The --tell_file option requires a .tell
+ file with the following format:
+
+ [tellfit]
+ objmodel = qso
+ redshift = 7.6
+ bal_wv_min_max = 10825,12060
+ OR
+ [tellfit]
+ objmodel = star
+ star_type = A0
+ star_mag = 8.
+ OR
+ [tellfit]
+ objmodel = poly
+ polyorder = 3
+ fit_wv_min_max = 9000.,9500.
+ """
+
+ def _split_lines(self, text, width):
+ """
+ Split the provided text into width constrained lines.
+
+ See the class description for formatting instructions.
+ """
+ if text.startswith("R|"):
+ lines = text[2:].splitlines()
+ for i in range(len(lines)):
+ if lines[i].startswith("F|"):
+ lines[i] = [lines[i][2:]]
+ elif len(lines[i]) == 0:
+ lines[i] = [" "]
+ else:
+ lines[i] = textwrap.wrap(lines[i], width)
+ return reduce(list.__add__, lines)
+ return super()._split_lines(text, width)
+
+
+class ScriptBase:
+ """
+ Provides a base class for all scripts.
+ """
+
+ @classmethod
+ def entry_point(cls):
+ """
+ Defines the main script entry point.
+ """
+ args = cls.parse_args()
+ if args.version:
+ print(f" LDT Observer Tools (obstools) version {__version__}")
+ else:
+ sys.exit(cls.main(args))
+
+ @classmethod
+ @property
+ def name(cls):
+ """
+ Provide the name of the script. By default, this is the name of the
+ module.
+ """
+ return f"{cls.__module__.rsplit('.', maxsplit=1)[-1]}"
+
+ @classmethod
+ def parse_args(cls, options=None):
+ """
+ Parse the command-line arguments.
+ """
+ parser = cls.get_parser()
+ ScriptBase._fill_parser_cwd(parser)
+ # Add "--version" to bottom of all scripts
+ parser.add_argument(
+ "--version", action="store_true", help="Print version and exit"
+ )
+ return parser.parse_args() if options is None else parser.parse_args(options)
+
+ @staticmethod
+ def _fill_parser_cwd(parser):
+ """
+ Replace the default of any action that is exactly ``'current working
+ directory'`` with the value of ``os.getcwd()``.
+
+ The ``parser`` is edited *in place*.
+
+ Args:
+ parser (:obj:`~argparse.ArgumentParser`):
+ The argument parsing object to edit.
+ """
+ for action in parser._actions:
+ if action.default == "current working directory":
+ action.default = os.getcwd()
+
+ # Base classes should override this
+ @staticmethod
+ def main(args):
+ """
+ Execute the script.
+ """
+ pass
+
+ @classmethod
+ def get_parser(
+ cls,
+ description=None,
+ width=None,
+ formatter=argparse.ArgumentDefaultsHelpFormatter,
+ ):
+ """
+ Construct the command-line argument parser.
+
+ Derived classes should override this. Ideally they should use this
+ base-class method to instantiate the ArgumentParser object and then fill
+ in the relevant parser arguments
+
+ .. warning::
+
+ *Any* argument that defaults to the
+ string ``'current working directory'`` will be replaced by the
+ result of ``os.getcwd()`` when the script is executed. This means
+ help dialogs will include this replacement, and parsing of the
+ command line will use ``os.getcwd()`` as the default. This
+ functionality is largely to allow for PypeIt's automated
+ documentation of script help dialogs without the "current working"
+ directory being that of the developer that most recently compiled
+ the docs.
+
+ Args:
+ 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.
+ """
+ return argparse.ArgumentParser(
+ description=description,
+ formatter_class=lambda prog: formatter(prog, width=width),
+ )
diff --git a/setup.cfg b/setup.cfg
index 8d8c7b2..5088119 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -28,18 +28,18 @@ classifiers =
zip_safe = False
use_2to3=False
packages = find:
-python_requires = >=3.10
+python_requires = >=3.10,<3.12
setup_requires = setuptools_scm
include_package_data = True
install_requires =
setuptools
setuptools_scm
astropy>=5.1
+ darkdetect
ccdproc
matplotlib
numpy>=1.22
scipy>=1.9
- pypeit[specutils]>=1.14.0
pysimplegui
requests
tqdm
@@ -48,6 +48,7 @@ install_requires =
docs =
sphinx
sphinx-automodapi
+ sphinx-subfigure
sphinx_rtd_theme==1.2.2
broker =
@@ -55,12 +56,17 @@ broker =
stomp.py
xmltodict
+pypeit =
+ pypeit[specutils]>=1.14.0
+
dev =
sphinx
sphinx-automodapi
+ sphinx-subfigure
sphinx_rtd_theme==1.2.2
pylint
black
+ pypeit[specutils]>=1.14.0
pyyaml
stomp.py
xmltodict
@@ -71,5 +77,6 @@ console_scripts =
deveny_collfocus = obstools.deveny_collfocus:DevenyCollfocus.entry_point
dfocus = obstools.dfocus:DFocus.entry_point
fix_ldt_header = obstools.fix_ldt_header:FixLdtHeader.entry_point
- neocp_ephem = obstools.neocp_ephem:entry_point
+ lmi_etc = obstools.lmi_etc:LmiEtc.entry_point
+ neocp_ephem = obstools.neocp_ephem:NeocpEphem.entry_point
scrub_deveny_pickup = obstools.scrub_deveny_pickup:ScrubDevenyPickup.entry_point