# -*- 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 01-Feb-2021
# Modified: 08-Jan-2025
#
# @author: tbowers
"""DeVeny Collimator Focus 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 dfocus routine for computing the required collimator
focus for the DeVeny Spectrograph based on a focus sequence completed by the
DeVeny LOUI.
.. include:: ../include/links.rst
"""
# Built-In Libraries
import argparse
import dataclasses
import os
import pathlib
import sys
import warnings
# 3rd-Party Libraries
import astropy.nddata
import ccdproc
from matplotlib.backends.backend_pdf import PdfPages
import matplotlib.pyplot as plt
import numpy as np
import scipy.signal
import tqdm
# Local Libraries
from obstools import deveny_grangle
from obstools import utils
# Dataclass definitions ======================================================#
[docs]
@dataclasses.dataclass
class FocusParams:
"""Focus Parameters DataClass
Attributes
----------
mid_file : :obj:`~pathlib.Path`
The filename of the middle file in the focus sequence
nominal : :obj:`float`
Nominal linewidth in pixels based on slit width and grating
demagnification
start : :obj:`float`
Starting focus value in the focus sweep
end : :obj:`float`
Ending focus value in the focus sweep
delta : :obj:`float`
Step size between focus values in the sweep
mnttemp : :obj:`float`
Mount temperature from the middle image of the sweep
binning : :obj:`str`
Binning scheme of the CCD
plot_title : :obj:`str`
The plot title for ...
opt_title : :obj:`str`
The plot title for ...
"""
mid_file: pathlib.Path
start: float
end: float
delta: float
plot_title: str
opt_title: str
mnttemp: float
nominal: float = 2.7
binning: str = "1x1"
[docs]
@dataclasses.dataclass
class LineInfo:
"""Extracted Line Information DataClass
Attributes
----------
spec_1d : :obj:`~numpy.ndarray`
Extracted 1D spectrum along the ``trace``
trace : :obj:`~numpy.ndarray`
Pixel locations of the extracted ``spec_1d``
centers : :obj:`~numpy.ndarray`
Centroid location (along the ``trace``) of each identified line
fwhm : :obj:`~numpy.ndarray`
FWHM (in pixles) of each identified line
"""
spec_1d: np.ndarray
trace: np.ndarray
centers: np.ndarray
fwhm: np.ndarray
[docs]
@dataclasses.dataclass
class FocusCurves:
"""Focus Curves DataClass
Attributes
----------
min_cf_idx_value : :obj:`~numpy.ndarray`
Best fit focus values
optimal_cf_idx_value : :obj:`~numpy.ndarray`
Best fit linewidths
min_linewidth : :obj:`~numpy.ndarray`
Minimum linewidths
foc_fits : :obj:`~numpy.ndarray`
Fit parameters (for plotting)
"""
min_focus_values: np.ndarray
optimal_focus_values: np.ndarray
min_linewidths: np.ndarray
fit_pars: np.ndarray
[docs]
@dataclasses.dataclass
class PlotParams:
"""Plotting Parameters DataClass
Attributes
----------
pdf : :obj:`~matplotlib.backends.backend_pdf.PdfPages`
The PDF object into which to place plots
path : :obj:`~pathlib.Path`
The path in which to find the ``deveny_focus.*`` files
docfig : :obj:`bool`
Make example figures for online documentation?
"""
pdf: PdfPages
path: pathlib.Path
docfig: bool
# User-facing Function =======================================================#
[docs]
def dfocus(
path: pathlib.Path,
flog: str = "last",
thresh: float = 100.0,
launch_preview: bool = True,
docfig: bool = False,
):
"""Find the optimal DeVeny collimator focus value
This is the user-facing ``dfocus`` function that calls all of the various
following subroutines. This function operates identically to the original
IDL routine ``dfocus.pro``, but with the additional options of debugging and
whether to launch Preview.app to show the PDF plots generated.
Parameters
----------
path : :obj:`~pathlib.Path`
The path in which to find the ``deveny_focus.*`` files.
flog : :obj:`str`, optional
Focus log to process. If unspecified, process the last sequence in
the directory. (Default: 'last')
flog must be of form: ``deveny_focus.YYYYMMDD.HHMMSS``
thresh : :obj:`float`, optional
Line intensity threshold above background for detection (Default: 100.0)
launch_preview : :obj:`bool`, optional
Display the plots by launching Preview (Default: True)
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
print("=" * n_cols)
print(" DeVeny Collimator Focus Calculator")
try:
# Get the Image File Collection for this focus run and initialize a
# dictionary to hold information from the headers
focus_icl, focus_id = parse_focus_log(path, flog)
focus_pars = parse_focus_headers(focus_icl)
except utils.ObstoolsError as err:
# If errors, print error message and exit
print(f"\n** {err} **\n")
sys.exit(1)
# Process the middle image to get line centers, arrays, trace
print(f"\n Processing center focus image {focus_pars.mid_file}...")
mid_ccd = astropy.nddata.CCDData.read(focus_pars.mid_file)
try:
mid_lines = get_lines_from_ccd(mid_ccd, thresh)
except utils.ObstoolsError as err:
# If errors, print error message and exit
print(f"\n** {err} **\n")
sys.exit(1)
# Run through files, showing a progress bar
print("\n Processing arc images...")
prog_bar = tqdm.tqdm(
total=len(focus_icl.files), unit="frame", unit_scale=False, colour="yellow"
)
line_width_array = []
# Loop over the CCDs
for ccd in focus_icl.ccds():
this_lines = get_lines_from_ccd(
ccd, thresh, trace=mid_lines.trace, verbose=False
)
# Empirical shifts in line location due to off-axis paraboloid
# collimator mirror
line_dx = -4.0 * (ccd.header["COLLFOC"] - mid_ccd.header["COLLFOC"])
# Keep only the lines from this image that match the reference image
line_widths = []
for cen in mid_lines.centers:
# Find lines within 3 pix of the (adjusted) reference line
idx = np.abs((cen + line_dx) - this_lines.centers) < 3.0
# If there's something there wider than 2 pix, use it... else NaN
width = this_lines.fwhm[idx][0] if np.sum(idx) else np.nan
line_widths.append(width if width > 2.0 else np.nan)
# Append these linewidths to the larger array for processing
line_width_array.append(np.array(line_widths))
prog_bar.update(1)
# Close the progress bar, end of loop
prog_bar.close()
line_width_array = np.array(line_width_array)
print(
f"\n Median value of all linewidths: {np.nanmedian(line_width_array):.2f} pix"
)
# Fit the focus curve:
focus_curves = fit_focus_curves(line_width_array, focus_pars)
# Convert the returned indices into actual COLLFOC values, find medians
med_opt_focus = np.real(np.nanmedian(focus_curves.optimal_focus_values))
print("=" * n_cols)
if focus_pars.binning != "1x1":
print(f"*** CCD is operating in binning {focus_pars.binning} (col x row)")
print(f"*** Recommended (Median) Optimal Focus Position: {med_opt_focus:.2f} mm")
print(f"*** Note: Current Mount Temperature is: {focus_pars.mnttemp:.1f}ÂșC")
# =========================================================================#
# Make the multipage PDF plot
with PdfPages(pdf_fn := path / f"pyfocus.{focus_id}.pdf") as pdf:
plot_pars = PlotParams(pdf=pdf, path=path, docfig=docfig)
# The plot shown in the IDL0 window: Plot of the found lines
# Find the line centers in the image
plot_lines(
mid_lines.spec_1d,
find_lines(mid_lines.spec_1d, thresh, verbose=False)[0],
focus_pars=focus_pars,
plot_pars=plot_pars,
)
# The plot shown in the IDL2 window: Plot of best-fit fwid vs centers
plot_optimal_focus(
focus_pars,
mid_lines.centers,
focus_curves.optimal_focus_values,
med_opt_focus,
plot_pars=plot_pars,
)
# The plot shown in the IDL1 window: Focus curves for each identified line
plot_focus_curves(
mid_lines.centers,
line_width_array,
focus_curves,
focus_pars=focus_pars,
plot_pars=plot_pars,
)
# Print the location of the plots
print(f"\n Plots have been saved to: {pdf_fn.name}\n")
# Try to open with Apple's Preview App... if can't, oh well.
if launch_preview:
try:
os.system(f"/usr/bin/open -a Preview {pdf_fn}")
except OSError as err:
print(f"Cannot open Preview.app\n{err}")
# Helper Functions (Chronological) ===========================================#
[docs]
def parse_focus_log(
path: pathlib.Path, flog: str
) -> tuple[ccdproc.ImageFileCollection, str]:
"""Parse the focus log file produced by the DeVeny LOUI
The DeVeny focus log file consists of filename, collimator focus, and other
relevant information::
: Image File Name ColFoc Grating GrTilt SltWth Filter LampCal MntTmp
20230613.0026.fits 7.50 600/4900 27.04 1.20 Clear (C) Cd,Ar,Hg 9.10
20230613.0027.fits 8.00 600/4900 27.04 1.20 Clear (C) Cd,Ar,Hg 9.10
20230613.0028.fits 8.50 600/4900 27.04 1.20 Clear (C) Cd,Ar,Hg 9.10
20230613.0029.fits 9.00 600/4900 27.04 1.20 Clear (C) Cd,Ar,Hg 9.10
20230613.0030.fits 9.50 600/4900 27.04 1.20 Clear (C) Cd,Ar,Hg 9.10
20230613.0031.fits 10.00 600/4900 27.04 1.20 Clear (C) Cd,Ar,Hg 9.10
20230613.0032.fits 10.50 600/4900 27.04 1.20 Clear (C) Cd,Ar,Hg 9.10
This function parses out the filenames of the focus images for this run,
largely discarding the remaining information in the focus log file.
Parameters
----------
path : :obj:`~pathlib.Path`
The path to the current working directory
flog : :obj:`str`
Identifier for the focus log to be processed
Returns
-------
:obj:`~ccdproc.ImageFileCollection`
The Image File Collection containing the information about focus images
requested
:obj:`str`
The Focus ID to be used for creating the PDF chart outputs
"""
# Just to be sure...
path = path.resolve()
# Get the correct flog
if flog.lower() == "last":
focfiles = sorted(path.glob("deveny_focus*"))
try:
flog = pathlib.Path(focfiles[-1])
except IndexError as err:
# In the event of no focus files, raise exception
raise utils.ObstoolsError(
"No successful focus run completed in this directory"
) from err
else:
flog = path / flog
if not flog.is_file():
raise utils.ObstoolsError("Specified focus run not in this directory")
files = []
with open(flog, "r", encoding="utf8") as file_object:
# Discard file header
file_object.readline()
# Read in the remainder of the file, grabbing just the filenames
for line in file_object:
files.append(path.parent / line.strip().split()[0])
# Return the Image File Collection
return ccdproc.ImageFileCollection(filenames=files), flog.name[-15:]
[docs]
def get_lines_from_ccd(
ccd: astropy.nddata.CCDData,
thresh: float = 20.0,
window: int = 11,
trace: np.ndarray = None,
verbose: bool = True,
) -> LineInfo:
"""Extract the lines from the CCD image
Since these steps are repeated often enough, pull them out into a separate
function.
Parameters
----------
ccd : :obj:`~astropy.nddata.CCDData`
The CCD object returned from :meth:`~ccdproc.ImageFileCollection.ccds`
thresh : :obj:`float`, optional
Threshold above which to identify lines (Default: 20 DN above bkgd)
window : :obj:`int`, optional
Window over which to average the spectrum (Default: 11 pixels)
Returns
-------
:class:`LineInfo`
Contains the relevant information about the lines found in the frame
"""
# Process the input CCDData object
spec_2d = ccdproc.trim_image(ccd, ccd.header["TRIMSEC"]).data
# If no trace provided, produce one
trace = centered_trace(spec_2d.data) if trace is None else trace
# Extract the spectrum
spec_1d = extract_spectrum(spec_2d, trace, window=window, thresh=thresh)
# Find the lines in the extracted spectrum
centers, fwhm = find_lines(spec_1d, thresh=thresh, verbose=verbose)
# Return the DataClass
return LineInfo(trace=trace, spec_1d=spec_1d, centers=centers, fwhm=fwhm)
[docs]
def centered_trace(spec2d: np.ndarray) -> np.ndarray:
"""Construct a simple trace down the middle of the image
_extended_summary_
Parameters
----------
spec2d : :obj:`~numpy.ndarray`
The 2D image for which to create the trace
Returns
-------
:obj:`~numpy.ndarray`
The desired trace
"""
# Build the trace for spectrum extraction
n_y, n_x = spec2d.shape
return np.full(n_x, n_y / 2, dtype=float).reshape((1, n_x))
[docs]
def find_lines(
spectrum: np.ndarray,
thresh: float = 20.0,
minsep: int = 11,
verbose: bool = True,
) -> tuple[np.ndarray, np.ndarray]:
"""Automatically find and centroid lines in a 1-row image
Uses :func:`scipy.signal.find_peaks` for this task
Parameters
----------
image : :obj:`~numpy.ndarray`
Extracted 1D spectrum
thresh : :obj:`float`, optional
Threshold above which to indentify lines [Default: 20 DN above bkgd]
minsep : :obj:`int`, optional
Minimum line separation for identification [Default: 11 pixels]
verbose : :obj:`bool`, optional
Produce verbose output? [Default: False]
Returns
-------
centers : :obj:`~numpy.ndarray`
Line centers (pixel #)
fwhm : :obj:`~numpy.ndarray`
The computed FWHM for each peak
"""
# Use scipy to find peaks & widths -- no more janky IDL-based junk
centers, _ = scipy.signal.find_peaks(spectrum, height=thresh, distance=minsep)
fwhm = (scipy.signal.peak_widths(spectrum, centers))[0]
# Print a statement, if desired
if verbose:
print(f" Number of lines found: {len(centers)}")
return centers, fwhm
[docs]
def fit_focus_curves(
width_array: np.ndarray,
focus_pars: FocusParams,
fit_order: int = 2,
debug: bool = False,
) -> FocusCurves:
"""Fit line focus curves
[extended_summary]
Parameters
----------
width_array : :obj:`~numpy.ndarray`
Array of FWHM for all lines as a function of COLLFOC
focus_pars : :class:`FocusParams`
Focus parameters DataClass from :func:`parse_focus_headers`
fit_order : :obj:`int`, optional
Polynomial order of the focus fit (Default: 2 = Quadratic)
debug : :obj:`bool`, optional
Print debug statements (Default: False)
Returns
-------
:class:`FocusCurves`
The focus curve object.
"""
# Get the number of focus frames, and number of found lines
n_frames, n_centers = width_array.shape
# Create the various arrays needed (full of NaN to begin with)
collfoc_vals = focus_pars.start + np.arange(n_frames) * focus_pars.delta
min_linewidth = np.full((n_centers,), np.nan, dtype=float)
min_collfoc = np.full((n_centers,), np.nan, dtype=float)
optimal_collfoc = np.full((n_centers,), np.nan, dtype=float)
foc_fits = np.full((n_centers, fit_order + 1), np.nan, dtype=float)
# Loop through lines to find the best focus for each one
for i in range(n_centers):
# Data are the FWHM for this line at different COLLFOC
widths_this_line = width_array[:, i]
# Find unphysically large or small FWHM (or NaN) -- set to np.nan
bad_idx = (
(widths_this_line < 1.0)
| (widths_this_line > 15.0)
| ~np.isfinite(widths_this_line)
)
widths_this_line[bad_idx] = np.nan
# If more than 1/3 of the FHWM are bad for this line, skip and go on
if np.sum(bad_idx) > n_frames / 3:
continue
# Do a polynomial fit (norder) to the FWHM vs COLLFOC index
# fit = np.polyfit(cf_idx_coarse, fwhms_of_this_line, norder)
fit = utils.good_poly(collfoc_vals, widths_this_line, fit_order, 2.0)
foc_fits[i] = fit
if debug:
print(f"In fit_focus_curves(): fit = {fit}")
# If good_poly() returns zeros, skip and go on
# If quadratic fit is concave DOWN, skip and go on
if all(value == 0 for value in fit) or fit[2] < 0:
continue
# Evaluate the curve minumum as root of derivative
poly = np.polynomial.Polynomial(fit)
min_collfoc[i] = poly.deriv(1).roots()[0]
min_linewidth[i] = poly(min_collfoc[i])
# Compute the nominal focus position as the larger of the two points
# where the polymonial function crosses fnom
fnom_curve = np.polynomial.Polynomial(
[fit[0] - focus_pars.nominal, fit[1], fit[2]]
)
fnom_roots = fnom_curve.roots()
if debug:
print(f"FNOM Roots: {fnom_roots}")
optimal_collfoc[i] = np.max(np.real(fnom_roots))
# After looping, return the items as numpy arrays
return FocusCurves(
min_focus_values=min_collfoc,
optimal_focus_values=optimal_collfoc,
min_linewidths=min_linewidth,
fit_pars=foc_fits,
)
# Plotting Routines ==========================================================#
[docs]
def plot_lines(
spectrum: np.ndarray,
centers: np.ndarray,
focus_pars: FocusParams = None,
plot_pars: PlotParams = None,
):
"""Plot centroid lines in a 1-row image
Parameters
----------
spectrum : :obj:`~numpy.ndarray`
Extracted spectrum
centers : :obj:`~numpy.ndarray`
The line centers, as reported by :func:`find_lines`
focus_pars : :class:`FocusParams`, optional
DataClass containing needed variables for plot
plot_pars : :class:`PlotParams`, optional
DataClass containing needed parameters for plotting
"""
# Set up the plot environment
_, axis = plt.subplots()
tsz = 8
# Plot the spectrum, mark the peaks, and label them
axis.plot(np.arange(len(spectrum)), spectrum)
axis.set_ylim(0, (yrange := 1.2 * max(spectrum)))
axis.plot(centers, spectrum[centers.astype(int)] + 0.02 * yrange, "k*")
for cen in centers:
axis.text(
cen,
spectrum[int(np.round(cen))] + 0.03 * yrange,
f"{cen:.0f}",
fontsize=tsz,
)
# Make pretty & Save
axis.set_title(focus_pars.plot_title, fontsize=tsz * 1.2)
axis.set_xlabel("CCD Column", fontsize=tsz)
axis.set_ylabel("I (DN)", fontsize=tsz)
axis.set_xlim(0, len(spectrum) + 2)
axis.tick_params("both", labelsize=tsz, direction="in", top=True, right=True)
plt.tight_layout()
if plot_pars.pdf is None:
plt.show()
else:
plot_pars.pdf.savefig()
if plot_pars.docfig:
for ext in ["png", "pdf", "svg"]:
plt.savefig(plot_pars.path / f"pyfocus.page1_example.{ext}")
plt.close()
[docs]
def plot_optimal_focus(
focus: FocusParams,
centers: np.ndarray,
optimal_focus_values: np.ndarray,
med_opt_focus: float,
plot_pars: PlotParams,
debug: bool = False,
):
"""Make the Optimal Focus Plot (IDL2 Window)
[extended_summary]
Parameters
----------
focus : :class:`FocusParams`
Dataclass of the various focus-related quantities
centers : :obj:`~numpy.ndarray`
Array of the centers of each line
optimal_focus_values : :obj:`~numpy.ndarray`
Array of the optimal focus values for each line
med_opt_focus : :obj:`float`
Median optimal focus value
debug : :obj:`bool`, optional
Print debug statements (Default: False)
plot_pars : :class:`PlotParams`
DataClass containing needed parameters for plotting
"""
if debug:
print("=" * 20)
print(centers.dtype, optimal_focus_values.dtype, type(med_opt_focus))
_, axis = plt.subplots()
tsz = 8
axis.plot(centers, optimal_focus_values, ".")
axis.set_xlim(0, 2050)
axis.set_ylim(focus.start - focus.delta, focus.end + focus.delta)
axis.set_title(
"Optimal focus position vs. line position, median = "
+ f"{med_opt_focus:.2f} mm "
+ f"(Mount Temp: {focus.mnttemp:.1f}$^\\circ$C)",
fontsize=tsz * 1.2,
)
axis.hlines(
med_opt_focus,
0,
1,
transform=axis.get_yaxis_transform(),
color="magenta",
ls="--",
)
axis.set_xlabel(f"CCD Column\n{focus.opt_title}", fontsize=tsz)
axis.set_ylabel("Optimal Focus (mm)", fontsize=tsz)
axis.grid(which="both", color="#c0c0c0", linestyle="-", linewidth=0.5)
axis.tick_params("both", labelsize=tsz, direction="in", top=True, right=True)
plt.tight_layout()
if plot_pars.pdf is None:
plt.show()
else:
plot_pars.pdf.savefig()
if plot_pars.docfig:
for ext in ["png", "pdf", "svg"]:
plt.savefig(plot_pars.path / f"pyfocus.page2_example.{ext}")
plt.close()
[docs]
def plot_focus_curves(
centers: np.ndarray,
line_width_array: np.ndarray,
focus_curves: FocusCurves,
focus_pars: FocusParams,
plot_pars: PlotParams,
):
"""Make the big plot of all the focus curves (IDL1 Window)
[extended_summary]
Parameters
----------
centers : :obj:`~numpy.ndarray`
List of line centers from find_lines()
line_width_array : :obj:`~numpy.ndarray`
Array of line widths from each COLLFOC setting for each line
focus_curves : :class:`FocusCurves`
The set of outputs from the focus curve fitting
focus_pars : :obj:`FocusParams`, optional
DataClass containing needed variables for plot
plot_pars : :class:`PlotParams`
DataClass containing needed parameters for plotting
"""
# Warning Filter -- Matplotlib doesn't like going from masked --> NaN
warnings.simplefilter("ignore", UserWarning)
# Set up variables
n_frames, n_lines = line_width_array.shape
focus_idx = np.arange(n_frames)
focus_x = focus_idx * focus_pars.delta + focus_pars.start
# Set the plotting array
ncols = 6
nrows = n_lines // ncols + 1
fig, axes = plt.subplots(ncols=ncols, nrows=nrows, figsize=(8.5, 11))
tsz = 6 # type size
for i, axis in enumerate(axes.flatten()):
if i < n_lines:
# Plot the points and the polynomial fit
axis.plot(focus_x, line_width_array[:, i], "kD", fillstyle="none")
poly = np.polynomial.Polynomial(focus_curves.fit_pars[i, :])
axis.plot(focus_x, poly(focus_x), "g-")
# Plot vertical lines to indicate minimum and optimal focus
axis.vlines(
focus_curves.min_focus_values[i],
0,
focus_curves.min_linewidths[i],
color="r",
ls="-",
)
axis.vlines(
focus_curves.optimal_focus_values[i],
0,
focus_pars.nominal,
color="b",
ls="-",
)
# Plot parameters to make pretty
# ax.set_ylim(0, np.nanmax(line_width_array[:,i]))
axis.set_ylim(0, 7.9)
axis.set_xlim(
np.min(focus_x) - focus_pars.delta, np.max(focus_x) + focus_pars.delta
)
axis.set_xlabel("Collimator Position (mm)", fontsize=tsz)
axis.set_ylabel("FWHM (pix)", fontsize=tsz)
axis.set_title(
f"LC: {centers[i]:.0f} Fnom: {focus_pars.nominal:.2f} pixels",
fontsize=tsz,
)
axis.tick_params(
"both", labelsize=tsz, direction="in", top=True, right=True
)
axis.grid(which="both", color="#c0c0c0", linestyle="-", linewidth=0.5)
else:
# Clear any extra positions if there aren't enough lines
fig.delaxes(axis)
plt.tight_layout()
if plot_pars.pdf is None:
plt.show()
else:
plot_pars.pdf.savefig()
if plot_pars.docfig:
for ext in ["png", "pdf", "svg"]:
plt.savefig(plot_pars.path / f"pyfocus.page3_example.{ext}")
plt.close()
# Extra Routines =============================================================#
[docs]
def find_lines_in_spectrum(
filename: str | pathlib.Path, thresh: float = 100.0
) -> np.ndarray:
"""Find the line centers in a spectrum
This function is not directly utilized in ``dfocus``, but rather is included
as a wrapper for several functions that can be used by other programs.
Given the filename of an arc-lamp spectrum, this function returns a list
of the line centers found in the image.
Parameters
----------
filename : :obj:`str` or :obj:`~pathlib.Path`
Filename of the arc frame to find lines in
thresh : :obj:`float`, optional
Line intensity threshold above background for detection [Default: 100]
Returns
-------
:obj:`~numpy.ndarray`
List of line centers found in the image
"""
# Get the trimmed image
ccd = astropy.nddata.CCDData.read(filename)
spectrum = ccdproc.trim_image(ccd, ccd.header["TRIMSEC"]).data
# Build the trace for spectrum extraction
n_y, n_x = spectrum.shape
traces = np.full(n_x, n_y / 2, dtype=float).reshape((1, n_x))
spectra = extract_spectrum(spectrum, traces, window=11)
# Find the lines!
centers, _ = find_lines(spectra, thresh=thresh)
return centers
# Command Line Script Infrastructure (borrowed from PypeIt) ==================#
[docs]
class DFocus(utils.ScriptBase):
"""Script class for ``dfocus`` tool
Script structure borrowed from :class:`pypeit.scripts.scriptbase.ScriptBase`.
"""
[docs]
@classmethod
def get_parser(
cls,
description: str = None,
width: int = None,
formatter: argparse.HelpFormatter = argparse.ArgumentDefaultsHelpFormatter,
):
"""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 Collimator Focus Calculator", width=width
)
parser.add_argument(
"--flog",
action="store",
type=str,
help="focus log to use",
default="last",
)
parser.add_argument(
"--thresh",
action="store",
type=float,
help="threshold for line detection",
default=100.0,
)
parser.add_argument(
"--nodisplay",
action="store_true",
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
[docs]
@staticmethod
def main(args):
"""Main Driver
Simple function that calls the primary function.
"""
# Giddy Up!
dfocus(
pathlib.Path(".").resolve(),
flog=args.flog,
thresh=args.thresh,
launch_preview=not args.nodisplay,
docfig=args.g,
)