ARCHITECTURE.md 8.5 KB

LF-MRI Unified GUI — Architecture

Overview

lf_mri_gui is a PySide6 desktop application that serves as the control frontend for the LF-MRI system. It is a hybrid client: some tabs communicate with the backend microservices over HTTP, others execute local Python logic.

The backend services run in Docker via lf_mri_platform.


Tab structure

# Tab Class Communication
0 Sequence SeqInterpTab REST → seq-interp:7475 if online, otherwise local Python fallback
1 Scanner ScannerTab REST → orchestrator:1717 (always)
2 FID FidTab Local Python + file I/O only

Directory structure

lf_mri_gui/
├── app.py                          # Entry point (argparse + QApplication)
├── build_exe.bat                   # PyInstaller build wrapper
├── lf_mri_gui.spec                 # PyInstaller spec (one-folder .exe)
├── requirements.txt
├── cfg/
│   ├── hw_config.json              # HackRF, PicoScope, GRU×3, DuePP
│   ├── server_config.json          # orchestrator_url, seq_interp_url
│   └── updated_constraints_lf.json # LF hardware constraints preset
└── src/
    ├── app_window.py               # LFMRIWindow(QMainWindow) — hosts QTabWidget
    ├── tabs/
    │   ├── seq_interp_tab.py       # Tab 0 — Sequence (hybrid HTTP/local)
    │   ├── scanner_tab.py          # Tab 1 — Scanner (orchestrator client)
    │   └── fid_tab.py              # Tab 2 — FID (local only)
    ├── clients/
    │   ├── orchestrator_client.py  # httpx client for lf_orchestration :1717
    │   └── seq_interp_client.py    # httpx client for seq-interp :7475
    ├── core/                       # synchronizer, waveform_processor, seq_generator
    ├── gui/                        # plot_panel, preview_panel, scheme_panel,
    │   │                           #   controls_panel, block_table, adapters, workers
    ├── hardware/                   # constraints.py
    ├── interfaces/                 # pulseq_adapter, xml_generator, rf_exporter,
    │   │                           #   gradient_exporter, picoscope_exporter,
    │   │                           #   post_request_generator
    └── fid/
        └── seqgen_FID.py           # FID sequence generator (local)

Tab 0 — Sequence (SeqInterpTab)

Full .seq interpretation pipeline with HTTP-first, local fallback strategy.

HTTP mode (seq-interp service online)

  1. SeqInterpClient.healthcheck()GET http://localhost:7475/health
  2. SeqInterpHttpWorker uploads .seq via POST /interpret/
  3. Polls GET /interpret/ for status
  4. Fetches result from GET /result/{task_id} — returns:
    • xml_text — sync XML
    • post_json — scanner POST payload
    • metadata — block counts, total duration
    • waveforms — downsampled Gx/Gy/Gz, RF amp/phase, ADC gate arrays
  5. UI populated from JSON; "Send to Scanner" button appears

Local fallback (seq-interp service offline)

LoadInterpWorker runs the full pipeline in-process: PulseqLoader → Synchronizer → XMLGenerator → exporters → PostRequestGenerator

Controls

  • Load .seq file (button or File > Load .seq… / Ctrl+O)
  • Hardware constraint spinboxes (RF_DELAY, TR_DELAY, rasters, etc.)
  • Waveform plots: RF magnitude/IQ, Gx/Gy/Gz gradients, TTL gates (pyqtgraph)
  • Compact timeline scheme with coloured sync blocks
  • Block table — 12 columns, colour-coded by block type
  • Right panel: Block Details / Warnings / Sync XML / POST JSON / Log
  • Export: RF binary, gradient files, sync_v2.xml, PicoScope config
  • "Send to Scanner" button — appears after successful interpretation, emits ready_for_scan(info_dict) → switches to Scanner tab

Tab 1 — Scanner (ScannerTab)

Exclusively communicates with lf_orchestration at orchestrator_url (default http://localhost:1717).

Layout

┌──────────────────────┬──────────────────────────────────────────┐
│  Orchestrator URL    │  Steps                                   │
│  [____________] Conn │  ┌────┬───────────────────┬──────────┐   │
│                      │  │ #  │ Name              │ Status   │   │
│  Scenario            │  ├────┼───────────────────┼──────────┤   │
│  [___________▼]      │  │ 1  │ interpret_sequence│ done     │   │
│  [Load] [Run All]    │  │ 2  │ start_measurement │ running  │   │
│  [Next] [Abort]      │  │ 3  │ wait_data_ready   │ pending  │   │
│                      │  └────┴───────────────────┴──────────┘   │
│                      │  Step log                                │
└──────────────────────┴──────────────────────────────────────────┘

Workflow

  1. Enter orchestrator URL → ConnectGET /scenario/list
  2. Select scenario from dropdown
  3. LoadPOST /scenario/load/{name} → receives job_id
  4. Run AllPOST /scenario/{job_id}/run_all or NextPOST /scenario/{job_id}/next
  5. QTimer polls GET /scenario/{job_id} every 1.5 s → updates step table
  6. Step colours: pending=grey, running=orange, done=green, failed=red
  7. AbortPOST /scenario/{job_id}/abort

Public API (called by LFMRIWindow)

  • set_hw_config(path) — propagate hw_config.json path
  • apply_seq_info(info_dict) — receive POST payload from Sequence tab

Tab 2 — FID (FidTab)

Generates FID .seq files locally using src/fid/seqgen_FID.py. On success emits fid_seq_generated(path)LFMRIWindow hands the path to SeqInterpTab.load_seq_file() and switches to the Sequence tab.


Cross-tab wiring

FidTab.fid_seq_generated(path)  ──→  LFMRIWindow._on_fid_generated()
                                         ├── SeqInterpTab.load_seq_file(path)
                                         └── tabs.setCurrentIndex(0)

SeqInterpTab.ready_for_scan(info) ─→  LFMRIWindow._on_ready_for_scan()
                                         ├── ScannerTab.apply_seq_info(info)
                                         └── tabs.setCurrentIndex(1)

File > Load .seq…               ──→  SeqInterpTab.load_seq_file(path)
                                     tabs.setCurrentIndex(0)

File > Load HW Config…          ──→  SeqInterpTab.set_hw_config(path)
                                     ScannerTab.set_hw_config(path)
                                     FidTab.set_hw_config(path)

Menu bar

  • File: Load .seq (Ctrl+O), Load HW Config, Set Output Directory, Load LF Constraints, Exit (Ctrl+Q)
  • Hardware: Settings (stub)
  • Help: About

Backend services (Docker)

Managed by D:\Projects\lf_mri_platform\docker-compose.yml.

Service Port GUI talks to it?
seq-interp 7475 Yes — SeqInterpTab (HTTP mode)
orchestrator 1717 Yes — ScannerTab (always)
spectrometer 8000 No — via orchestrator only
reconstructor 8081 No — via orchestrator only
spectroscopy 8002 No — standalone signal processor

What is not yet implemented

Tab Feature State
Scanner Abort job Button exists, endpoint may not be implemented in orchestrator
Scanner Job history / past jobs Not planned
FID Full waveform preview Placeholder only
App Hardware Settings menu "Not implemented" dialog
App Reconstruction results viewer Not planned

Running the GUI

# With Docker backend (recommended)
cd D:\Projects\lf_mri_platform
.\start.ps1          # starts Docker stack + GUI

# GUI only (no Docker)
cd D:\Projects\lf_mri\MRI-testing
python lf_mri_gui/app.py

# With a pre-loaded sequence
python lf_mri_gui/app.py path/to/sequence.seq

# Build standalone .exe
cd D:\Projects\lf_mri\MRI-testing\lf_mri_gui
.venv\Scripts\activate
build_exe.bat
# Output: dist\lf_mri_gui\lf_mri_gui.exe