"""
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 _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