diff --git a/doc/releases/1.17.2dev.rst b/doc/releases/1.17.2dev.rst index d4613925f..f55cc909b 100644 --- a/doc/releases/1.17.2dev.rst +++ b/doc/releases/1.17.2dev.rst @@ -1,5 +1,5 @@ -Version 1.17.1dev +Version 1.17.2dev ================= Installation Changes @@ -36,6 +36,11 @@ Script Changes - `pypeit_setup_gui` script has been removed. The Setup GUI can be started with `pypeit_setup -G`. +- Added ``pypeit_run_to_calibstep`` to run PypeIt to an input calibration + step. This script is useful to re-generate calibration files for a given + configuration without running the full reduction. + + Datamodel Changes ----------------- diff --git a/doc/running.rst b/doc/running.rst index 95876fc0c..553375b08 100644 --- a/doc/running.rst +++ b/doc/running.rst @@ -35,7 +35,7 @@ Before executing ``run_pypeit``, you must have #. Edited the :doc:`pypeit_file` in that directory as recommended for a successful reduction. - #. (optional) Removed any calibration files in the ``Calibrations/`` folder. + #. (optional) Remove any calibration files in the ``Calibrations/`` folder. This is particularly necessary if you're re-reducing data that was previously reduced by an older PypeIt version. However, when in doubt, it's good practice to perform a fresh reduction by removing these files diff --git a/doc/scripts.rst b/doc/scripts.rst index 392529f84..ac6f73e01 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -201,6 +201,40 @@ run_pypeit This is the main executable for PypeIt for its core end-to-end data processing. See :ref:`run-pypeit` for details. +pypeit_run_to_calibstep +----------------------- + +This runs PypeIt to a given calibration step for a given frame. +This is primarily provided to re-run a single calibration step after +the user has modified their ``pypeit`` file. It is expected that +the user has already attempted a full reduction with ``run_pypeit``. +following the details in :ref:`run-pypeit`. + +The user provides: (1) the PypeIt file, (2) the calibration step +to re-run to, and +(3) either the raw science file whose +calibration file(s) you wish to re-run or the calib_group ID. +All steps up to and including the requested step will be +run, although previous steps will be skipped if the calibration files +are already present (these will be loaded, as appropriate). + +Users are strongly advised to use this script until you are happy +with the calibration of this single step. Once you are, you should +re-run the full reduction with ``run_pypeit`` after first removing +*all* previously generated calibration and science files. + +The script usage can be displayed by calling the script with the +``-h`` option: + +.. include:: help/pypeit_run_to_calibstep.rst + +An example call is: + +.. code-block:: console + + pypeit_run_to_calibstep shane_kast_blue_A.pypeit wv_calib --science_frame b28.fits.gz + + pypeit_trace_edges ------------------ diff --git a/pypeit/calibrations.py b/pypeit/calibrations.py index b2f2e8fd7..b90bed9db 100644 --- a/pypeit/calibrations.py +++ b/pypeit/calibrations.py @@ -347,10 +347,16 @@ def set_config(self, frame, det, par=None): if par is not None: self.par = par - def get_arc(self): + def get_arc(self, force:str=None): """ Load or generate the arc calibration frame. + Args: + force (:obj:`str`, optional): + 'remake' -- Force the frame to be remade. + 'reload' -- Reload the frame if it exists. + None -- Load the existing frame if it exists and reuse_calibs=True + Returns: :class:`~pypeit.images.buildimage.ArcImage`: The processed calibration image. @@ -371,8 +377,8 @@ def get_arc(self): # If a processed calibration frame exists and we want to reuse it, do # so: - if cal_file.exists() and self.reuse_calibs: - self.msarc = frame['class'].from_file(cal_file, chk_version=self.chk_version) + self.msarc = self.process_load_selection(frame, cal_file, force) + if not self.success or self.msarc is not None: return self.msarc # Reset the BPM @@ -393,10 +399,16 @@ def get_arc(self): # Return it return self.msarc - def get_tiltimg(self): + def get_tiltimg(self, force:str=None): """ Load or generate the tilt calibration frame. + Args: + force (:obj:`str`, optional): + 'remake' -- Force the frame to be remade. + 'reload' -- Reload the frame if it exists. + None -- Load the existing frame if it exists and reuse_calibs=True + Returns: :class:`~pypeit.images.buildimage.TiltImage`: The processed calibration image. @@ -417,8 +429,8 @@ def get_tiltimg(self): # If a processed calibration frame exists and we want to reuse it, do # so: - if cal_file.exists() and self.reuse_calibs: - self.mstilt = frame['class'].from_file(cal_file, chk_version=self.chk_version) + self.mstilt = self.process_load_selection(frame, cal_file, force) + if not self.success or self.mstilt is not None: return self.mstilt # Reset the BPM @@ -440,10 +452,16 @@ def get_tiltimg(self): # Return it return self.mstilt - def get_align(self): + def get_align(self, force:str=None): """ Load or generate the alignment calibration frame. + Args: + force (:obj:`str`, optional): + 'remake' -- Force the frame to be remade. + 'reload' -- Reload the frame if it exists. + None -- Load the existing frame if it exists and reuse_calibs=True + Returns: :class:`~pypeit.alignframe.Alignments`: The processed alignment image. @@ -468,8 +486,10 @@ def get_align(self): # If a processed calibration frame exists and we want to reuse it, do # so: - if cal_file.exists() and self.reuse_calibs: - self.alignments = frame['class'].from_file(cal_file, chk_version=self.chk_version) + self.alignments = self.process_load_selection(frame, cal_file, force) + if not self.success: + return None + elif self.alignments is not None: self.alignments.is_synced(self.slits) return self.alignments @@ -498,10 +518,16 @@ def get_align(self): self.alignments.to_file() return self.alignments - def get_bias(self): + def get_bias(self, force:str=None): """ Load or generate the bias calibration frame. + Args: + force (:obj:`str`, optional): + 'remake' -- Force the frame to be remade. + 'reload' -- Reload the frame if it exists. + None -- Load the existing frame if it exists and reuse_calibs=True + Returns: :class:`~pypeit.images.buildimage.BiasImage`: The processed calibration image. @@ -514,16 +540,17 @@ def get_bias(self): raw_files, cal_file, calib_key, setup, calib_id, detname \ = self.find_calibrations(frame['type'], frame['class']) + # If no raw files are available and no processed calibration frame if len(raw_files) == 0 and cal_file is None: msgs.warn(f'No raw {frame["type"]} frames found and unable to identify a relevant ' - 'processed calibration frame. Continuing...') + 'processed calibration frame. Continuing without a bias...') self.msbias = None return self.msbias # If a processed calibration frame exists and we want to reuse it, do # so: - if cal_file.exists() and self.reuse_calibs: - self.msbias = frame['class'].from_file(cal_file, chk_version=self.chk_version) + self.msbias = self.process_load_selection(frame, cal_file, force) + if not self.success or self.msbias is not None: return self.msbias # Perform a check on the files @@ -540,10 +567,16 @@ def get_bias(self): # Return it return self.msbias - def get_dark(self): + def get_dark(self, force:str=None): """ Load or generate the dark calibration frame. + Args: + force (:obj:`str`, optional): + 'remake' -- Force the frame to be remade. + 'reload' -- Reload the frame if it exists. + None -- Load the existing frame if it exists and reuse_calibs=True + Returns: :class:`~pypeit.images.buildimage.DarkImage`: The processed calibration image. @@ -564,8 +597,8 @@ def get_dark(self): # If a processed calibration frame exists and we want to reuse it, do # so: - if cal_file.exists() and self.reuse_calibs: - self.msdark = frame['class'].from_file(cal_file, chk_version=self.chk_version) + self.msdark = self.process_load_selection(frame, cal_file, force) + if not self.success or self.msdark is not None: return self.msdark # TODO: If a bias has been constructed and it will be subtracted from @@ -592,13 +625,19 @@ def get_dark(self): # Return it return self.msdark - def get_bpm(self, frame=None): + def get_bpm(self, frame=None, force:str=None): """ Load or generate the bad pixel mask. This is primarily a wrapper for :func:`~pypeit.spectrographs.spectrograph.Spectrograph.bpm`. + Args: + force (:obj:`str`, optional): + Currently ignored + frame (:obj:`int`, optional): + The row index in :attr:`fitstbl` + Returns: `numpy.ndarray`_: The bad pixel mask, which should match the shape and orientation of a *trimmed* and PypeIt-oriented science image! @@ -614,10 +653,16 @@ def get_bpm(self, frame=None): # Return return self.msbpm - def get_scattlight(self): + def get_scattlight(self, force:str=None): """ Load or generate the scattered light model. + Args: + force (:obj:`str`, optional): + 'remake' -- Force the frame to be remade. + 'reload' -- Reload the frame if it exists. + None -- Load the existing frame if it exists and reuse_calibs=True + Returns: :class:`~pypeit.scattlight.ScatteredLight`: The processed calibration image including the model. """ @@ -643,8 +688,8 @@ def get_scattlight(self): # If a processed calibration frame exists and we want to reuse it, do # so: - if cal_file.exists() and self.reuse_calibs: - self.msscattlight = frame['class'].from_file(cal_file, chk_version=self.chk_version) + self.msscattlight = self.process_load_selection(frame, cal_file, force) + if not self.success or self.msscattlight is not None: return self.msscattlight # Scattered light model does not exist or we're not reusing it. @@ -707,10 +752,16 @@ def get_scattlight(self): return self.msscattlight - def get_flats(self): + def get_flats(self, force:str=None): """ Load or generate the flat-field calibration images. + Args: + force (:obj:`str`, optional): + 'remake' -- Force the frame to be remade. + 'reload' -- Reload the frame if it exists. + None -- Load the existing frame if it exists and reuse_calibs=True + Returns: :class:`~pypeit.flatfield.FlatImages`: The processed calibration image. @@ -793,7 +844,8 @@ def get_flats(self): calib_key = illum_calib_key if pixel_calib_key is None else pixel_calib_key setup = illum_setup if pixel_setup is None else pixel_setup calib_id = illum_calib_id if pixel_calib_id is None else pixel_calib_id - if cal_file.exists() and self.reuse_calibs: + + if cal_file.exists() and self.reuse_calibs and not force == 'remake': self.flatimages = flatfield.FlatImages.from_file(cal_file, chk_version=self.chk_version) self.flatimages.is_synced(self.slits) @@ -936,7 +988,7 @@ def get_flats(self): return self.flatimages - def get_slits(self): + def get_slits(self, force:str=None): """ Load or generate the definition of the slit boundaries. @@ -965,8 +1017,10 @@ def get_slits(self): # If a processed calibration frame exists and we want to reuse it, do # so: - if cal_file.exists() and self.reuse_calibs: - self.slits = frame['class'].from_file(cal_file, chk_version=self.chk_version) + self.slits = self.process_load_selection(frame, cal_file, force) + if not self.success: + return None + elif self.slits is not None: self.slits.mask = self.slits.mask_init.copy() if self.user_slits is not None: self.slits.user_mask(detname, self.user_slits) @@ -977,7 +1031,7 @@ def get_slits(self): edges_file = Path(edgetrace.EdgeTraceSet.construct_file_name(calib_key, calib_dir=self.calib_dir)).absolute() # If so, reuse it? - if edges_file.exists() and self.reuse_calibs: + if edges_file.exists() and self.reuse_calibs and force != 'remake': # Yep! Load it and parse it into slits. self.slits = edgetrace.EdgeTraceSet.from_file(edges_file, chk_version=self.chk_version).get_slits() @@ -1050,10 +1104,16 @@ def get_slits(self): self.slits.user_mask(detname, self.user_slits) return self.slits - def get_wv_calib(self): + def get_wv_calib(self, force:str=None): """ Load or generate the 1D wavelength calibrations + Args: + force (:obj:`str`, optional): + 'remake' -- Force the frame to be remade. + 'reload' -- Reload the frame if it exists. + None -- Load the existing frame if it exists and reuse_calibs=True + Returns: :class:`~pypeit.wavecalib.WaveCalib`: Object containing wavelength calibrations and the updated slit mask array. @@ -1066,9 +1126,10 @@ def get_wv_calib(self): return self.wv_calib # Check for existing data - if not self._chk_objs(['msarc', 'msbpm', 'slits']): + req_objs = ['msarc', 'msbpm', 'slits'] + if not self._chk_objs(req_objs): msgs.warn('Not enough information to load/generate the wavelength calibration. ' - 'Skipping and may crash down the line') + 'Skipping and may crash down the line') return None # Check internals @@ -1087,9 +1148,10 @@ def get_wv_calib(self): # If a processed calibration frame exists and # we want to reuse it, do so (or just load it): - if cal_file.exists() and self.reuse_calibs: - # Load the file - self.wv_calib = wavecalib.WaveCalib.from_file(cal_file, chk_version=self.chk_version) + self.wv_calib = self.process_load_selection(frame, cal_file, force) + if not self.success: + return None + elif self.wv_calib is not None: self.wv_calib.chk_synced(self.slits) self.slits.mask_wvcalib(self.wv_calib) if self.par['wavelengths']['method'] == 'echelle': @@ -1135,10 +1197,16 @@ def get_wv_calib(self): # Return return self.wv_calib - def get_tilts(self): + def get_tilts(self, force:str=None): """ Load or generate the wavelength tilts calibration frame + Args: + force (:obj:`str`, optional): + 'remake' -- Force the frame to be remade. + 'reload' -- Reload the frame if it exists. + None -- Load the existing frame if it exists and reuse_calibs=True + Returns: :class:`~pypeit.wavetilts.WaveTilts`: Object containing the wavelength tilt calibration. @@ -1166,8 +1234,10 @@ def get_tilts(self): # If a processed calibration frame exists and we want to reuse it, do # so: - if cal_file.exists() and self.reuse_calibs: - self.wavetilts = wavetilts.WaveTilts.from_file(cal_file, chk_version=self.chk_version) + self.wavetilts = self.process_load_selection(frame, cal_file, force) + if not self.success: + return None + elif self.wavetilts is not None: self.wavetilts.is_synced(self.slits) self.slits.mask_wavetilts(self.wavetilts) return self.wavetilts @@ -1190,16 +1260,65 @@ def get_tilts(self): self.wavetilts.to_file() return self.wavetilts - def run_the_steps(self): + def process_load_selection(self, frame, cal_file, force): + """ + Process how pypeit should use any pre-existing calibration files. + + If loading is requested but the calibration file (``cal_file``) does + not exist, ``self.success`` is set to False, and None is returned. + + Args: + frame (:obj:`dict`): + A dictionary with two elements: ``type`` is the string + defining the frame type and ``class`` is the pypeit class + used to load the pre-existing calibration file. + cal_file (:obj:`str`, `Path`_): + Path to the calibration file. + force (:obj:`str`): + Defines how to treat a pre-existing calibration file. Must + be one of the following options: + + - ``'remake'``: Force the calibration be remade. + + - ``'reload'``: Reload the frame if it exists. + + - ``None``: Load the existing frame if it exists and + ``self.reuse_calibs=True``. + + Returns: + :obj:`object`: Either the loaded calibration object or None. + """ + if force not in [None, 'remake', 'reload']: + msgs.error(f'`force` keyword must be None, remake, or reload, not {force}') + if force == 'remake': + return None + _cal_file = Path(cal_file).absolute() + if force == 'reload' and not _cal_file.exists(): + msgs.warn(f"{_cal_file} does not exist; cannot reload " + f"{frame['class'].__name__} calibration.") + self.success = False + return None + if force == 'reload' or (self.reuse_calibs and _cal_file.exists()): + return frame['class'].from_file(_cal_file, chk_version=self.chk_version) + + def run_the_steps(self, stop_at_step:str=None): """ Run full the full recipe of calibration steps. """ self.success = True for step in self.steps: - getattr(self, f'get_{step}')() + if stop_at_step is not None and step == stop_at_step: + force = 'remake' + msgs.info(f"Calibrations will stop at {stop_at_step}") + else: + force = None + getattr(self, f'get_{step}')(force=force) if not self.success: self.failed_step = f'get_{step}' return + if stop_at_step is not None and step == stop_at_step: + msgs.info(f"Calibrations stopping at {stop_at_step}") + return msgs.info("Calibration complete and/or fully loaded!") msgs.info("#######################################################################") @@ -1222,16 +1341,17 @@ def _chk_objs(self, items): Args: items (list): + List of required items for the calibration step Returns: - bool: True if all exist + bool: True if all exist or if all were successfully loaded, False """ for obj in items: if getattr(self, obj) is None: - msgs.warn("You need to generate {:s} prior to this calibration..".format(obj)) # Strip ms iobj = obj[2:] if obj[0:2] == 'ms' else obj + msgs.warn("You need to generate {:s} prior to this calibration..".format(obj)) msgs.warn("Use get_{:s}".format(iobj)) return False return True @@ -1471,6 +1591,16 @@ def association_summary(ofile, fitstbl, spectrograph, caldir, subset=None, det=N ff.write(yaml.dump(utils.yamlify(asn))) msgs.info(f'Calibration association file written to: {_ofile}') + @staticmethod + def default_steps(): + """ + This defines the steps for calibrations and their order + Note that the order matters! + + Returns: + list: Calibration steps, in order of execution + """ + return [] class MultiSlitCalibrations(Calibrations): """ @@ -1495,7 +1625,8 @@ def default_steps(): # Order matters! And the name must match a viable "get_{step}" method # in Calibrations. # TODO: Does the bpm need to be done after the dark? - return ['bias', 'dark', 'bpm', 'slits', 'arc', 'tiltimg', 'wv_calib', 'tilts', 'scattlight', 'flats'] + return ['bias', 'dark', 'bpm', 'slits', 'arc', 'tiltimg', + 'wv_calib', 'tilts', 'scattlight', 'flats'] class IFUCalibrations(Calibrations): diff --git a/pypeit/core/coadd.py b/pypeit/core/coadd.py index db5157fee..41c18d30d 100644 --- a/pypeit/core/coadd.py +++ b/pypeit/core/coadd.py @@ -26,7 +26,6 @@ from pypeit import dataPaths from pypeit import utils from pypeit.core import fitting -from pypeit import specobjs from pypeit.core import combine from pypeit.core.wavecal import wvutils from pypeit.core import pydl diff --git a/pypeit/pypeit.py b/pypeit/pypeit.py index 4c1e520f2..e71f01229 100644 --- a/pypeit/pypeit.py +++ b/pypeit/pypeit.py @@ -292,19 +292,8 @@ def calib_all(self): # Loop on Detectors for self.det in detectors: msgs.info(f'Working on detector {self.det}') - # Instantiate Calibrations class - user_slits = slittrace.merge_user_slit(self.par['rdx']['slitspatnum'], - self.par['rdx']['maskIDs']) - self.caliBrate = calibrations.Calibrations.get_instance( - self.fitstbl, self.par['calibrations'], self.spectrograph, - self.calibrations_path, qadir=self.qa_path, reuse_calibs=self.reuse_calibs, - show=self.show, user_slits=user_slits, - chk_version=self.par['rdx']['chk_version']) - # Do it - # These need to be separate to accommodate COADD2D - self.caliBrate.set_config(grp_frames[0], self.det, self.par['calibrations']) - - self.caliBrate.run_the_steps() + + self.caliBrate = self.calib_one(grp_frames, self.det) if not self.caliBrate.success: msgs.warn(f'Calibrations for detector {self.det} were unsuccessful! The step ' f'that failed was {self.caliBrate.failed_step}. Continuing to next ' @@ -690,16 +679,19 @@ def get_sci_metadata(self, frame, det): self.spectrograph.get_det_name(det)) return objtype_out, calib_key, obstime, basename, binning - def calib_one(self, frames, det): + def calib_one(self, frames, det, stop_at_step:str=None): """ Run Calibration for a single exposure/detector pair Args: frames (:obj:`list`): - List of frames to extract; stacked if more than one - is provided + List of frames (rows) to calibrate + Only used to idetify the setup and calibration group det (:obj:`int`): Detector number (1-indexed) + stop_at_step (:obj:`str`, optional): + Run only up to this calibration step. + Returns: caliBrate (:class:`pypeit.calibrations.Calibrations`) @@ -715,9 +707,16 @@ def calib_one(self, frames, det): self.calibrations_path, qadir=self.qa_path, reuse_calibs=self.reuse_calibs, show=self.show, user_slits=user_slits, chk_version=self.par['rdx']['chk_version']) + + # Check + if stop_at_step is not None and stop_at_step not in caliBrate.steps: + msgs.error(f"Requested stop_at_step={stop_at_step} is not a valid calibration step.\n Allowed steps are: {caliBrate.steps}") + # These need to be separate to accomodate COADD2D caliBrate.set_config(frames[0], det, self.par['calibrations']) - caliBrate.run_the_steps() + + # Run + caliBrate.run_the_steps(stop_at_step=stop_at_step) return caliBrate diff --git a/pypeit/scripts/__init__.py b/pypeit/scripts/__init__.py index 9f4bef02c..cf5ee3be4 100644 --- a/pypeit/scripts/__init__.py +++ b/pypeit/scripts/__init__.py @@ -53,6 +53,7 @@ from pypeit.scripts import view_fits from pypeit.scripts import compile_wvarxiv from pypeit.scripts import show_pixflat +from pypeit.scripts import run_to_calibstep # Build the list of script classes diff --git a/pypeit/scripts/run_pypeit.py b/pypeit/scripts/run_pypeit.py index f2a0d01d7..d5b3302e7 100644 --- a/pypeit/scripts/run_pypeit.py +++ b/pypeit/scripts/run_pypeit.py @@ -100,7 +100,7 @@ def main(args): logname=logname, show=args.show) if args.calib_only: - calib_dict = pypeIt.calib_all() + pypeIt.calib_all() else: pypeIt.reduce_all() msgs.info('Data reduction complete') diff --git a/pypeit/scripts/run_to_calibstep.py b/pypeit/scripts/run_to_calibstep.py new file mode 100644 index 000000000..cde32042a --- /dev/null +++ b/pypeit/scripts/run_to_calibstep.py @@ -0,0 +1,101 @@ +""" +Script to run to a single calibration step for an input frame + +.. include common links, assuming primary doc root is up one directory +.. include:: ../include/links.rst +""" + +from pypeit.scripts import scriptbase + +class RunToCalibStep(scriptbase.ScriptBase): + + valid_steps = ['align', 'arc', 'bias', 'bpm', 'dark', 'flats', 'scattlight', + 'slits', 'tiltimg', 'tilts', 'wv_calib'] + + + @classmethod + def get_parser(cls, width=None): + import argparse + + parser = super().get_parser(description='Run PypeIt to a single calibration step for an input frame', + width=width, formatter=scriptbase.SmartFormatter) + parser.add_argument('pypeit_file', type=str, + help='PypeIt reduction file (must have .pypeit extension)') + parser.add_argument('step', type=str, help=f"Calibration step to perform. Valid steps are: {', '.join(cls.valid_steps)}") + + parser.add_argument('--science_frame', type=str, help='Raw science frame to reduce as listed in your PypeIt file, e.g. b28.fits.gz. Either this or the calib_group must be provided') + parser.add_argument('--calib_group', type=str, help='Calibration group ID to reduce. Either this or the frame must be provided') + parser.add_argument('--det', type=str, help='Detector to reduce') + + # TODO -- Grab these from run_pypeit.py ? + parser.add_argument('-v', '--verbosity', type=int, default=2, + help='Verbosity level between 0 [none] and 2 [all]') + + parser.add_argument('-r', '--redux_path', default=None, + help='Path to directory for the reduction. Only advised for testing') + parser.add_argument('-s', '--show', default=False, action='store_true', + help='Show reduction steps via plots (which will block further ' + 'execution until clicked on) and outputs to ginga. Requires ' + 'remote control ginga session via ' + '"ginga --modules=RC,SlitWavelength &"') + + return parser + + @staticmethod + def main(args): + + import ast + import numpy as np + from IPython import embed + from pathlib import Path + + from pypeit import pypeit + from pypeit import msgs + + # Load options from command line + _pypeit_file = Path(args.pypeit_file).absolute() + if _pypeit_file.suffix != '.pypeit': + msgs.error(f'Input file {_pypeit_file} must have a .pypeit extension!') + logname = _pypeit_file.parent / f'{_pypeit_file.stem}.log' + + # Check for the frame or calib_group + if args.science_frame is None and args.calib_group is None: + msgs.error('Must provide either a science frame or a calibration group ID') + elif args.science_frame is not None and args.calib_group is not None: + msgs.warn("Both science_frame and calib_group ID provided. Will use the science_frame") + + # Instantiate the main pipeline reduction object + pypeIt = pypeit.PypeIt(args.pypeit_file, verbosity=args.verbosity, + redux_path=args.redux_path, + logname=logname, show=args.show, calib_only=True) + pypeIt.reuse_calibs = True + + # Find the detectors to reduce + dets = pypeIt.par['rdx']['detnum'] if args.det is None else ast.literal_eval(args.det) + detectors = pypeIt.select_detectors( + pypeIt.spectrograph, dets, + slitspatnum=pypeIt.par['rdx']['slitspatnum']) + + # Find the row of the frame + if args.science_frame is not None: + row = np.where(pypeIt.fitstbl['filename'] == args.science_frame)[0] + if len(row) != 1: + msgs.error(f"Frame {args.frame} not found or not unique") + elif args.calib_group is not None: + rows = np.where((pypeIt.fitstbl['calib'].data.astype(str) == args.calib_group))[0] + if len(rows) == 0: + msgs.error(f"Calibration group {args.calib_group} not found") + row = rows[0] + row = int(row) + + # Calibrations? + for det in detectors: + pypeIt.calib_one([row], det, stop_at_step=args.step) + + # QA HTML + msgs.info('Generating QA HTML') + pypeIt.build_qa() + msgs.close() + + return 0 + diff --git a/pypeit/tests/test_calibrations.py b/pypeit/tests/test_calibrations.py index 52ac1958a..e1d63e6d3 100644 --- a/pypeit/tests/test_calibrations.py +++ b/pypeit/tests/test_calibrations.py @@ -50,6 +50,7 @@ def multi_caliBrate(fitstbl): multi_caliBrate = calibrations.MultiSlitCalibrations(fitstbl, calib_par, spectrograph, data_output_path('Calibrations')) + multi_caliBrate.success = True return reset_calib(multi_caliBrate) diff --git a/setup.cfg b/setup.cfg index d761be1be..210e704ce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -137,6 +137,7 @@ console_scripts = pypeit_qa_html = pypeit.scripts.qa_html:QAHtml.entry_point pypeit_ql = pypeit.scripts.ql:QL.entry_point run_pypeit = pypeit.scripts.run_pypeit:RunPypeIt.entry_point + pypeit_run_to_calibstep = pypeit.scripts.run_to_calibstep:RunToCalibStep.entry_point pypeit_sensfunc = pypeit.scripts.sensfunc:SensFunc.entry_point pypeit_setup = pypeit.scripts.setup:Setup.entry_point pypeit_setup_coadd2d = pypeit.scripts.setup_coadd2d:SetupCoAdd2D.entry_point