Source code for pypeit.dashboard.inspect

"""
Build the command lines that launch PypeIt's existing inspection tools for
a calibration step (Qt-free, so it is unit-testable without a display).

The dashboard does not embed plots; it launches the existing CLI tools
(`pypeit_chk_*` / `pypeit_view_fits`) as subprocesses (design C8/C14).  Those
scripts have **non-uniform** signatures, so this module exposes per-step
*command builders* rather than a flat map (S3-Q12):

* ``bias`` / ``dark`` / ``arc`` / ``tiltimg`` — processed calibration images,
  opened **directly in ``ginga``** (Stage 4 Round-2 #1; these already-oriented
  PypeIt FITS do not display correctly through ``pypeit_view_fits``).
* ``slits`` — ``pypeit_chk_edges <Edges_*>`` (the *Edges* file, not the
  ``slits`` step's ``Slits_*`` output).
* ``wv_calib`` / ``tilts`` / ``flats`` / ``align`` — a single output file with
  the matching ``pypeit_chk_*`` tool.
* ``scattlight`` — ``pypeit_chk_scattlight <ScatteredLight_*> <Slits_*> --det``.

Stage 4 adds :func:`run_command`, which builds the ``pypeit_run_to_calibstep``
argv used by the Calibrations view's **(Re)Build** control (design C10).

Each builder takes the :class:`~pypeit.dashboard.model.DashboardModel` (for the
``Calibrations/`` directory, the spectrograph, and file resolution) and returns
an ``argv`` list, or ``None`` if the command cannot be built.

Generated by JXP and Claude.
"""

# Processed calibration image steps: opened directly in ginga (Stage 4
# Round-2 #1; pypeit_view_fits does not display these processed outputs).
_PROCESSED_IMAGE_STEPS = ('bias', 'dark', 'arc', 'tiltimg')

# Steps whose own output_file is opened by a dedicated pypeit_chk_* tool.
# NOTE: wv_calib has **no** standalone viewer — pypeit_chk_wavecalib only
# prints diagnostics to the terminal and the WaveCalib FITS is not a useful
# image, so its "Inspect output" is disabled (Round-2 #4); its wavelength QA
# PNGs are shown in the QA list instead.
_SINGLE_FILE_CHK = {
    'tilts': 'pypeit_chk_tilts',
    'flats': 'pypeit_chk_flats',
    'align': 'pypeit_chk_alignments',
}


[docs] def _slits_output(model, group, det): """ Path to the ``slits`` step's ``Slits_*`` output, or ``None``. Generated by JXP and Claude. Args: model (:class:`~pypeit.dashboard.model.DashboardModel`): The model. group (:obj:`int`): Calibration group ID. det: Detector (int) or mosaic (tuple/list). Returns: :obj:`pathlib.Path` or None. """ return model.output_path('slits', group, det)
[docs] def _edges_file(model, group, det): """ Path to the ``Edges_*`` file, derived from the ``slits`` step's recorded ``Slits_*`` output (so it is correct even when calibrations are shared across groups and named ``*_all_*``, as for fixed-format echelle). Generated by JXP and Claude. Args: model (:class:`~pypeit.dashboard.model.DashboardModel`): The model. group (:obj:`int`): Calibration group ID. det: Detector (int) or mosaic (tuple/list). Returns: :obj:`pathlib.Path` or None. """ slits = _slits_output(model, group, det) if slits is None: return None return slits.with_name(slits.name.replace('Slits', 'Edges', 1))
[docs] def _spectrograph(model): """ Return the spectrograph name for the model, or ``None``. Generated by JXP and Claude. Args: model (:class:`~pypeit.dashboard.model.DashboardModel`): The model. Returns: str or None: The spectrograph name. """ return model.header_info.spectrograph if model.header_info else None
[docs] def output_command(model, step, group, det): """ Build the ``argv`` to inspect a step's processed **output** (C8). Generated by JXP and Claude. Args: model (:class:`~pypeit.dashboard.model.DashboardModel`): The model. step (:obj:`str`): Calibration step name. group (:obj:`int`): Calibration group ID. det: Detector (int) or mosaic (tuple/list). Returns: list or None: The command ``argv``, or ``None`` if it cannot be built (e.g. no output file / unknown spectrograph / step not viewable). """ if step in _PROCESSED_IMAGE_STEPS: # The processed calibration FITS (Bias_*, Dark_*, Arc_*, Tiltimg_*) are # already-oriented PypeIt image files, so open them **directly in # ginga** — pypeit_view_fits (raw/inter) does not display these # processed outputs correctly (Stage 4 Round-2 #1). out = model.output_path(step, group, det) if out is None: return None return ['ginga', str(out)] if step == 'slits': # pypeit_chk_edges wants the Edges_* file, not the Slits_* output; # derive it from the recorded Slits_* (handles *_all_* naming). edges = _edges_file(model, group, det) return ['pypeit_chk_edges', str(edges)] if edges is not None \ else None if step in _SINGLE_FILE_CHK: out = model.output_path(step, group, det) return [_SINGLE_FILE_CHK[step], str(out)] if out is not None \ else None if step == 'scattlight': out = model.output_path(step, group, det) slits = _slits_output(model, group, det) if out is None or slits is None: return None return ['pypeit_chk_scattlight', str(out), str(slits), '--det', model.det_name(det)] # bpm and anything else: no standalone viewer. return None
[docs] def output_target(model, step, group, det): """ Return the primary file the "Inspect output" command will open, so the view can enable the control only when that file exists on disk. Generated by JXP and Claude. Args: model (:class:`~pypeit.dashboard.model.DashboardModel`): The model. step (:obj:`str`): Calibration step name. group (:obj:`int`): Calibration group ID. det: Detector (int) or mosaic (tuple/list). Returns: :obj:`pathlib.Path` or None: The file that would be opened, or ``None`` if there is no viewer for this step. """ # The Edges_* file for slits; the step's own output otherwise. if step == 'slits': return _edges_file(model, group, det) return model.output_path(step, group, det)
[docs] def view_input_command(model, input_file, det=None): """ Build the ``argv`` to view a single **input** (raw) frame (C7). Uses ``pypeit_view_fits --proc`` so the raw frame is oriented/trimmed and shown properly formatted (not the bare, unoriented FITS; Stage 3b #1). Generated by JXP and Claude. Args: model (:class:`~pypeit.dashboard.model.DashboardModel`): The model. input_file (:obj:`str`): Path to the raw input frame. det (optional): The detector (int) or mosaic (tuple/list) the frame belongs to, passed to ``--det`` so the right detector is processed. Returns: list or None: The ``pypeit_view_fits`` command, or ``None`` if the spectrograph is unknown. """ spec = _spectrograph(model) if spec is None: return None argv = ['pypeit_view_fits', spec, str(input_file), '--proc'] if det is not None: dets = det if isinstance(det, (list, tuple)) else (det,) argv += ['--det'] + [str(x) for x in dets] return argv
[docs] def _det_run_arg(det): """ Format a detector/mosaic for ``pypeit_run_to_calibstep --det``, which parses its value with :func:`pypeit.core.parse.eval_detectors`: a single integer (``"1"``) is one detector; a parenthesized tuple (``"(1,2)"``) is a mosaic. Generated by JXP and Claude. Args: det: Detector (int) or mosaic (tuple/list of ints). Returns: str: The ``--det`` argument string. """ if isinstance(det, (list, tuple)): return '(' + ','.join(str(x) for x in det) + ')' return str(det)
[docs] def run_command(model, step, group, det): """ Build the ``argv`` to **(re)build** a calibration step via the existing ``pypeit_run_to_calibstep`` script (design C10). The dashboard always fixes the calibration group and detector (the scope drop-downs), so it passes ``--calib_group`` and ``--det`` (never the script's alternative ``--science_frame`` form) plus ``--redux_path`` so the run writes to the same reduction directory. ``run_to_calibstep`` generates the requested step **and every step that precedes it**, reusing the ones already on disk. Generated by JXP and Claude. Args: model (:class:`~pypeit.dashboard.model.DashboardModel`): The model. step (:obj:`str`): Calibration step name (e.g. ``wv_calib``). group (:obj:`int`): Calibration group ID. det: Detector (int) or mosaic (tuple/list). Returns: list or None: The ``pypeit_run_to_calibstep`` command ``argv``, or ``None`` if the model has no ``.pypeit`` file. """ pypeit_file = getattr(model, 'pypeit_file', None) if pypeit_file is None: return None argv = ['pypeit_run_to_calibstep', str(pypeit_file), step, '--calib_group', str(group), '--det', _det_run_arg(det)] redux_dir = getattr(model, 'redux_dir', None) if redux_dir is not None: argv += ['--redux_path', str(redux_dir)] return argv
# ----------------------------------------------------------------------- # Stage 6: science-product viewers + the science (Re)Build (R15). # -----------------------------------------------------------------------
[docs] def spec2d_command(spec2d_file, det=None): """ Build the ``argv`` to view a science **2D** spectrum (``pypeit_show_2dspec <spec2d> [--det]``). Generated by JXP and Claude. Args: spec2d_file (:obj:`str`, :obj:`pathlib.Path`): Path to the ``spec2d`` product (or ``None``). det (optional): Detector (int) or mosaic (tuple/list) to show. Returns: list or None: The command ``argv``, or ``None`` if no file. """ if not spec2d_file: return None argv = ['pypeit_show_2dspec', str(spec2d_file)] if det is not None: argv += ['--det', _det_run_arg(det)] return argv
[docs] def science_object_name(spat_pixpos, slitid, det_name): """ Reconstruct a 1D object's extension name (``SPAT####-SLIT####-DET##``), matching :meth:`pypeit.specobj.SpecObj.set_name`, so ``pypeit_show_1dspec`` can be pointed at a specific object (Stage 6, S6-Q10 option (a)). Generated by JXP and Claude. Args: spat_pixpos (:obj:`float`): The object's spatial pixel position. slitid (:obj:`int`): The slit (SPAT_ID) the object is on. det_name (:obj:`str`): The detector name (e.g. ``DET01``). Returns: str or None: The object name, or ``None`` if it cannot be built. """ if spat_pixpos is None or slitid is None or not det_name: return None return f'SPAT{int(round(spat_pixpos)):04d}-' \ f'SLIT{int(slitid):04d}-{det_name}'
[docs] def spec1d_command(spec1d_file, obj_name=None): """ Build the ``argv`` to view a science **1D** spectrum (``pypeit_show_1dspec <spec1d> [--obj <name>]``). Per the design, the user picks an object; pass its reconstructed name (see :func:`science_object_name`). Generated by JXP and Claude. Args: spec1d_file (:obj:`str`, :obj:`pathlib.Path`): Path to the ``spec1d`` product (or ``None``). obj_name (:obj:`str`, optional): The object extension name to plot. Returns: list or None: The command ``argv``, or ``None`` if no file. """ if not spec1d_file: return None argv = ['pypeit_show_1dspec', str(spec1d_file)] if obj_name: argv += ['--obj', obj_name] return argv
[docs] def science_run_command(model, frame, det, step): """ Build the ``argv`` to **(re)build** a science step via the existing ``pypeit_reduce_by_step`` script (Stage 6, design R15). ``reduce_by_step`` takes a **raw frame** filename (not the combined basename), so the raw frame is taken from the science entry's recorded ``raw_files``. Generated by JXP and Claude. Args: model (:class:`~pypeit.dashboard.model.DashboardModel`): The model. frame (:obj:`str`): The science exposure basename (table key). det: Detector (int) or mosaic (tuple/list). step (:obj:`str`): One of ``process`` / ``findobj`` / ``extract``. Returns: list or None: The ``pypeit_reduce_by_step`` command ``argv``, or ``None`` if the model has no ``.pypeit`` file or no raw frame is known. """ pypeit_file = getattr(model, 'pypeit_file', None) if pypeit_file is None: return None entry = model.science_frame_entry(frame, det) raw_files = getattr(entry, 'raw_files', None) if entry is not None else None if not raw_files: return None return ['pypeit_reduce_by_step', str(pypeit_file), str(raw_files[0]), step, '--det', _det_run_arg(det)]
[docs] def run_pypeit_command(model): """ Build the ``argv`` to run the **full** reduction via ``run_pypeit -o`` (Round-6): process every science/standard frame, overwriting the existing outputs. This is the science analogue of a "build everything" action, distinct from the per-step (Re)Build controls. Generated by JXP and Claude. Args: model (:class:`~pypeit.dashboard.model.DashboardModel`): The model. Returns: list or None: The ``run_pypeit`` command ``argv`` (with ``-o`` to overwrite and ``--redux_path`` to write to the reduction directory), or ``None`` if the model has no ``.pypeit`` file. """ pypeit_file = getattr(model, 'pypeit_file', None) if pypeit_file is None: return None argv = ['run_pypeit', str(pypeit_file), '-o'] redux_dir = getattr(model, 'redux_dir', None) if redux_dir is not None: argv += ['--redux_path', str(redux_dir)] return argv