# 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 ├── server_worker.py # ServerWorker(QThread) — embedded uvicorn ├── 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 → **Connect** → `GET /scenario/list` 2. Select scenario from dropdown 3. **Load** → `POST /scenario/load/{name}` → receives `job_id` 4. **Run All** → `POST /scenario/{job_id}/run_all` or **Next** → `POST /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. **Abort** → `POST /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), Start/Stop API Server (embedded uvicorn on :7475) - **Help**: About --- ## Embedded API server `Hardware > Start API Server` launches `ServerWorker(QThread)` which starts a uvicorn process serving the same `seq_interp` API on port 7475. This allows the GUI to act as a seq-interp service itself — useful when running without Docker. --- ## 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 ```powershell # 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 ```