Architecture

Package layout

src/spino/
├── app.py            # main window: builds the tabbed UI, wires orchestration
├── __main__.py       # `python -m spino`
├── config_io.py      # defaults (read from pipeline), help text, JSON (de)serialise
├── runner.py         # subprocess entry: overlay settings → run pipeline
├── theme.py          # colour palette + tab list (replaces GUIBRUSHR ConstantVariables)
├── panels/           # one class per config group + run_panel (log/output)
├── gui_toolkit/      # reused GUIBRUSHR widgets + theming (layout/, widget/, graphics.yaml)
├── pipeline/         # the scheduling pipeline (8 near-verbatim modules)
└── data/             # bundled catalogs (cat/), aux files (aux/), sky-transmission FITS

Three layers

  1. GUI (app.py, panels/*): a MyTabPanel notebook with one panel per config group. Each panel builds labelled My* widgets and exposes collect() (widgets → dict) and set_values() (dict → widgets). app.py merges all panels’ collect() into a single flat settings dict.

  2. Runner (runner.py): a headless subprocess entry point. It loads a settings.json, setattrs every key onto the pipeline’s phase_config module, then imports and calls phase_scheduler.main(). Running the pipeline in a child process (rather than a thread) keeps the Tk UI responsive, isolates matplotlib’s Agg backend, and guarantees a clean config each run.

  3. Pipeline (pipeline/*): the scientific code, bundled almost verbatim from the original headless tool. It is driven entirely by module-level globals in phase_config.py.

Data flow

 app.collect_all() ─► settings.json ─► subprocess: runner.py
                                             │  setattr onto phase_config
                                             ▼
                                    phase_scheduler.main()
                                             │  stdout
      run_panel log pane  ◄──────────────────┘
      run_panel output list  ◄── scans OUTPUT_DIR for *.pdf / *.csv

The Run panel launches the subprocess with stdout/stderr piped; a daemon reader thread pushes lines onto a queue.Queue, and the Tk main loop drains the queue via after(150, …) into the log widget (only the queue crosses the thread boundary, so Tk stays single-threaded).

Notes on the vendored code

  • gui_toolkit: copied from GUIBRUSHR’s GUI/LAYOUT, GUI/WIDGET, and HelpButton; the only edits were rewriting the GUIBRUSHR.... import prefix to spino.gui_toolkit.... and repointing GraphicsConfig’s YAML path to the co-located graphics.yaml. The matplotlib/PDF widgets were not copied (results open in the system viewer), so the toolkit needs only PyYAML.

  • pipeline: flat imports (from phase_config import ) are preserved exactly as upstream; runner.py inserts pipeline/ on sys.path so they resolve. The only pipeline edits: one import line, repointing the phase_config data paths at the bundled data/ folder (with a user-writable default OUTPUT_DIR), and inlining three SI constants to drop a heavy petitRADTRANS dependency.