.. include:: ../include/links.rst
.. _dashboard-design:
========================
PypeIt Dashboard: Design
========================
**Dashboard documentation version: 1.4.0**
This page explains *how the dashboard is built* — its component structure, how it
acquires the reduction :doc:`state `, and how it stays in sync with a
running reduction. For the user-facing description of the views and controls,
see :doc:`dashboard`.
The diagrams below are rendered with `Mermaid `__.
Component architecture
======================
The dashboard follows a **Model–View–Controller** organization with a strict
split: a **Qt-free model** holds all reduction knowledge, **thin Qt views**
render what the model exposes, and a small set of helpers turn user actions into
subprocess launches. Nothing in the model imports Qt, so it is unit-testable
without a display.
.. mermaid::
flowchart TB
subgraph disk["On disk (a reduction directory)"]
PF[".pypeit file"]
SJ["*_state.json"]
PROD["Calibrations/, Science/,
Intermediate/, QA/PNGs/"]
LOG["reduction .log"]
end
subgraph model["Model layer (Qt-free)"]
DM["DashboardModel
loads / derives state"]
RS["RunPypeItState
(pydantic)"]
PAL["palette
status to color+glyph"]
INS["inspect
builds viewer / (Re)Build argv"]
end
subgraph view["View layer (Qt)"]
MW["DashboardMainWindow
header + tabs + status bar"]
SV["StatusView"]
CV["CalibrationsView"]
SCV["ScienceView"]
AB["ActivityBar
Build + Inspection channels"]
end
subgraph ctrl["Run control"]
RL["RunLock
polls .log + state.json"]
LA["Launcher
QProcess"]
end
PF --> DM
SJ --> DM
PROD --> DM
DM --> RS
DM --> MW
MW --> SV & CV & SCV
SV & CV & SCV --> PAL
CV & SCV --> INS
INS --> LA
LA --> SUB["subprocesses:
pypeit_chk_*, ginga,
pypeit_show_*,
pypeit_run_to_calibstep,
pypeit_reduce_by_step"]
LA --> AB
LOG --> RL
SJ --> RL
RL --> MW
The model never embeds plots: every viewer is an **existing** PypeIt script,
launched as a subprocess by the ``Launcher`` and reported on the ``ActivityBar``.
The :ref:`status palette ` (``palette``) is the one
place that maps a status to a color, a glyph, and a label.
State acquisition on launch
===========================
On launch the model acquires the reduction state once. A present
``*_state.json`` is **loaded** (fast, authoritative); when it is absent the state
is **derived** from disk the way ``pypeit_status`` does — a read that performs no
processing and writes no state file.
.. mermaid::
flowchart TD
START["pypeit_dashboard <file>.pypeit"] --> ACQ["DashboardModel._acquire_state"]
ACQ --> Q{"*_state.json present?"}
Q -- "yes" --> LOAD["Load JSON into RunPypeItState
(load_status = state_file)"]
Q -- "no" --> DERIVE["Derive (read-only):
instantiate PypeIt in calib_only mode"]
DERIVE --> CAL["calib_all(status_only) for calibrations"]
DERIVE --> SCI["derive_science_from_disk()
for science frames"]
CAL --> RS["RunPypeItState
(load_status = derived,
NOT written to disk)"]
SCI --> RS
LOAD --> RS
RS --> SEED["seed planned science/standard frames
(from cached .pypeit metadata)"]
SEED --> ACCESS["Model accessors:
status_table / slit_table /
science_table / ..."]
ACCESS --> RENDER["Views render"]
The user's **Refresh** button re-runs this acquisition. Mid-run (see below) the
model is re-read from ``*_state.json`` only — it is **never re-derived** while a
run is active, so a transient mid-write file is skipped rather than triggering a
heavy re-derivation.
**Planned science frames.** On *both* paths the model also seeds the *planned*
science/standard frames — the upcoming exposures read from the ``.pypeit``
metadata — so the Science view lists what is coming even before any reduction
(mirroring the planned calibrations), and keeps showing them after a calibration
build replaces the state file. The planned-frame list is computed once and
cached per ``.pypeit`` (a one-time metadata read), so re-seeding it on every
state reload is cheap.
Live monitoring and (Re)Build
=============================
The dashboard observes a reduction whether it was launched from the dashboard's
**(Re)Build** controls or started independently in a terminal. A single
``RunLock`` is the heart of this: one ``QTimer`` polls the reduction ``.log``
(to detect an active run) and the ``*_state.json`` mtime (to drive live updates),
emitting ``lockChanged`` and ``stateChanged``.
.. mermaid::
sequenceDiagram
actor User
participant View as Calibrations / Science view
participant Launcher
participant Proc as PypeIt subprocess
participant Lock as RunLock (QTimer ~2s)
participant Win as MainWindow
User->>View: click (Re)Build (confirm clobber)
View->>Launcher: run(run_to_calibstep / reduce_by_step)
Launcher->>Proc: start QProcess
Launcher->>Lock: engage lock
Lock-->>Win: lockChanged(true)
Win->>View: set_locked(true) (buttons orange, disabled)
loop while running
Proc->>Proc: write *_state.json per step
Lock-->>Win: stateChanged
Win->>Win: re-read state (no derive)
Win->>View: refresh (preserve scope/selection)
end
Proc-->>Launcher: finished
Lock-->>Win: lockChanged(false)
Win->>Win: final refresh + idle
The same path runs for a terminal-started ``run_pypeit``: the ``.log`` activity
engages the lock, ``stateChanged`` drives the live refresh, and the views update
on their own with no manual Refresh. The refresh **preserves** the user's scope
(group/detector) and selected step/frame, and inspection launches use a
**separate** ``ActivityBar`` channel so monitoring messages and viewer feedback
never overwrite each other. The Science view's **Run PypeIt** button launches a
full ``run_pypeit -o`` through this same lock-governed path.
**One shared state file, two writers.** ``pypeit_run_to_calibstep`` (a
calibration build) and ``pypeit_reduce_by_step`` (a science step-build) each only
populate their own portion of ``*_state.json``. To avoid one blanking the
other's portion when it writes, both call ``RunPypeItState.merge_from_disk()``
first — overlaying the existing on-disk calibration **and** science statuses onto
their fresh state — so a science (re)build keeps the calibration statuses (and
vice versa).
Deriving science state from disk
================================
When there is no state file, the science portion is reconstructed from the
on-disk products, with the **final Science products authoritative** and the
``Intermediate/`` files (written only by ``pypeit_reduce_by_step``) filling steps
the Science products do not yet cover.
.. mermaid::
flowchart TD
subgraph auth["Authoritative: Science/ products"]
S2["spec2d_*.fits present"] --> P1["process = findobj = skysub = success
+ per-slit BADSKYSUB/BADEXTRACT"]
S1["spec1d_*.fits present"] --> P2["extract = success
+ per-object metrics (S/N, FWHM, ...)"]
end
subgraph fb["Fallback: Intermediate/ (reduce_by_step)"]
IM1["sciImg_* -> process"]
IM2["Sky_* -> skysub"]
IM3["spec1d_*_all -> findobj"]
end
P1 --> INFER["process inferred 'success'
if any later step succeeded"]
P2 --> INFER
fb --> INFER
INFER --> ENTRY["ScienceFrameState per (frame, det)"]
This mirrors the calibration derive and is why a launch on a finished reduction
shows full science status even with no state file. See :doc:`/state` for the
state model itself.
See also
========
- :doc:`dashboard` — the user-facing guide to the views and controls.
- :doc:`/state` — the reduction state the dashboard reads.
- :doc:`/reduce_by_step` — the step-by-step (re)build entry points.