spacexerq 2 днів тому
коміт
dc3923ee11
9 змінених файлів з 672 додано та 0 видалено
  1. 19 0
      .env.example
  2. 4 0
      .gitignore
  3. 57 0
      Makefile
  4. 173 0
      README.md
  5. 144 0
      docker-compose.yml
  6. 145 0
      install.ps1
  7. 4 0
      start.bat
  8. 104 0
      start.ps1
  9. 22 0
      stop.ps1

+ 19 - 0
.env.example

@@ -0,0 +1,19 @@
+# ── Ports ─────────────────────────────────────────────────────────────────────
+# Change these if the default ports conflict with other local services.
+ORCHESTRATOR_PORT=1717
+SEQ_INTERP_PORT=7475
+SPECTROMETER_PORT=8000
+RECONSTRUCTOR_PORT=8081
+SPECTROSCOPY_PORT=8002
+
+# ── Orchestrator mode ──────────────────────────────────────────────────────────
+# plug  — stub tasks (instant responses, no hardware required)
+# real  — live tasks (calls spectrometer at 8000 and reconstructor at 8081)
+ORCHESTRATOR_MODE=plug
+
+# ── Image versions ─────────────────────────────────────────────────────────────
+ORCHESTRATOR_VERSION=dev
+SEQ_INTERP_VERSION=dev
+SPECTROSCOPY_VERSION=dev
+RECONSTRUCTOR_VERSION=dev
+SPECTROMETER_VERSION=dev

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.env
+*.pyc
+__pycache__/
+.DS_Store

+ 57 - 0
Makefile

@@ -0,0 +1,57 @@
+# lf_mri_platform — unified microservice stack
+# Requires: Docker Desktop with Compose v2, GNU make (or nmake on Windows)
+#
+# Usage:
+#   make plug          — start all services in stub mode (no hardware)
+#   make real          — start all services in real mode (live hardware)
+#   make down          — stop and remove containers
+#   make logs          — tail logs from all services
+#   make health        — check all service health endpoints
+#   make restart svc=orchestrator  — restart a single service
+
+.PHONY: up down plug real logs health restart build ps
+
+ENV_FILE := .env
+
+# ── Startup targets ──────────────────────────────────────────────────────────
+
+up: $(ENV_FILE)
+	docker compose --env-file $(ENV_FILE) up --build -d
+
+plug: $(ENV_FILE)
+	ORCHESTRATOR_MODE=plug docker compose --env-file $(ENV_FILE) up --build -d
+
+real: $(ENV_FILE)
+	ORCHESTRATOR_MODE=real docker compose --env-file $(ENV_FILE) up --build -d
+
+down:
+	docker compose down
+
+build:
+	docker compose --env-file $(ENV_FILE) build
+
+# ── Monitoring ───────────────────────────────────────────────────────────────
+
+logs:
+	docker compose logs -f
+
+ps:
+	docker compose ps
+
+restart:
+	docker compose restart $(svc)
+
+# ── Health checks ────────────────────────────────────────────────────────────
+
+health:
+	@echo "orchestrator  :" && curl -sf http://localhost:$${ORCHESTRATOR_PORT:-1717}/health  || echo "OFFLINE"
+	@echo "seq-interp    :" && curl -sf http://localhost:$${SEQ_INTERP_PORT:-7475}/health    || echo "OFFLINE"
+	@echo "spectrometer  :" && curl -sf http://localhost:$${SPECTROMETER_PORT:-8000}/api/    || echo "OFFLINE"
+	@echo "reconstructor :" && curl -sf http://localhost:$${RECONSTRUCTOR_PORT:-8081}/health || echo "OFFLINE"
+	@echo "spectroscopy  :" && curl -sf http://localhost:$${SPECTROSCOPY_PORT:-8002}/health  || echo "OFFLINE"
+
+# ── Bootstrap ────────────────────────────────────────────────────────────────
+
+$(ENV_FILE):
+	@echo "Creating .env from .env.example..."
+	cp .env.example $(ENV_FILE)

+ 173 - 0
README.md

@@ -0,0 +1,173 @@
+# lf_mri_platform
+
+Umbrella microservice platform for the LF-MRI system.
+Brings up the entire backend stack with a single command; the GUI runs on the host.
+
+---
+
+## Deployment architecture
+
+```
+┌─── Host (Windows) ──────────────────────────────────────────────────────────┐
+│                                                                              │
+│   lf_mri_gui.exe  (or  python app.py)                                       │
+│   ┌──────────────┬──────────────┬──────────────┐                            │
+│   │ Sequence tab │ Scanner tab  │   FID tab    │                            │
+│   │ REST or local│ REST only    │  local only  │                            │
+│   └──────┬───────┴──────┬───────┴──────────────┘                            │
+│          │               │                                                   │
+│          │ HTTP:7475     │ HTTP:1717                                         │
+│          │               │                                                   │
+└──────────┼───────────────┼─────────────────────────────────────────────────┘
+           │               │
+┌──────────▼───────────────▼──── Docker (lf_mri_platform) ───────────────────┐
+│                                                                              │
+│  ┌─────────────────┐    ┌─────────────────────────────────────────────┐     │
+│  │  seq-interp     │    │  orchestrator                               │     │
+│  │  :7475          │    │  :1717                                      │     │
+│  │                 │    │  MODE=plug → stub tasks (no hardware)       │     │
+│  │  POST /interpret│    │  MODE=real → calls spectrometer+reconstructor│    │
+│  │  GET  /result   │    └──────────┬─────────────────────┬───────────┘     │
+│  └─────────────────┘               │                     │                 │
+│                                    │ HTTP:8000            │ HTTP:8081       │
+│  ┌─────────────────┐  ┌────────────▼──────┐  ┌──────────▼──────────┐      │
+│  │  spectroscopy   │  │  spectrometer     │  │  reconstructor      │      │
+│  │  :8002          │  │  :8000 (DRF)      │  │  :8081 (FastAPI)    │      │
+│  └─────────────────┘  └───────────────────┘  └─────────────────────┘      │
+└──────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Services
+
+| Service | Port | Purpose | Docker-ready |
+|---------|------|---------|-------------|
+| orchestrator | 1717 | Workflow engine | yes (dockerfile_inline) |
+| seq-interp | 7475 | .seq interpreter → XML/waveforms | yes (Dockerfile) |
+| spectrometer | 8000 | Hardware acquisition (DRF) | yes (dockerfile_inline) |
+| reconstructor | 8081 | MRI image reconstruction | yes (dockerfile_inline) |
+| spectroscopy | 8002 | Signal processor | yes (Dockerfile) |
+
+---
+
+## Quick start
+
+### Prerequisites
+- Windows 10/11
+- [Docker Desktop](https://www.docker.com/products/docker-desktop/) ≥ 4.x
+- Python 3.10+ (for GUI)
+
+### First time
+```powershell
+cd D:\Projects\lf_mri_platform
+
+# Install GUI dependencies + create shortcuts
+.\install.ps1
+```
+
+### Daily use
+```powershell
+# Start services (Docker) + GUI (host Python) — stub mode
+.\start.ps1
+
+# Start in real hardware mode
+.\start.ps1 -Mode real
+
+# Start services only (no GUI)
+.\start.ps1 -ServicesOnly
+
+# Start GUI only (without Docker services)
+.\start.ps1 -GuiOnly
+
+# Stop services
+.\stop.ps1
+
+# Full reset (remove volumes/data)
+.\stop.ps1 -Clean
+```
+
+### With Make (WSL/Git Bash)
+```bash
+make plug      # start in stub mode
+make real      # start in real mode
+make health    # check all endpoints
+make logs      # tail all logs
+make down      # stop
+```
+
+---
+
+## Modes
+
+| Mode | Switch | Description |
+|------|--------|-------------|
+| **plug** | default | Stub tasks — instant mock responses, no hardware needed |
+| **real** | `-Mode real` | Live tasks — orchestrator calls real spectrometer + reconstructor |
+
+---
+
+## GUI communication
+
+The GUI is currently **hybrid** — not a pure thin client:
+
+| Tab | How it communicates |
+|-----|---------------------|
+| Sequence | REST → seq-interp:7475 **if online**, otherwise local Python |
+| Scanner | REST → orchestrator:1717 (always) |
+| FID | Local Python + file I/O only |
+
+---
+
+## Build standalone .exe (optional)
+
+```powershell
+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
+```
+
+---
+
+## Docker compatibility status
+
+All services are now buildable as Docker containers.
+
+| Service | Previously broken | Fix applied |
+|---------|-------------------|-------------|
+| reconstructor | Windows `\\` in paths (`reco.py`) | `os.path.join()` — fixed |
+| reconstructor | `matplotlib` without Agg backend | `matplotlib.use('Agg')` + `MPLBACKEND=Agg` in dockerfile — fixed |
+| spectroscopy | Missing `/health` endpoint (healthcheck failed) | Added `GET /health` to `main.py` — fixed |
+| spectroscopy | `matplotlib` without explicit Agg backend | `matplotlib.use('Agg')` in `main.py` — fixed |
+| spectroscopy | `host="127.0.0.1"` in `run_py.py` | Not used by Docker (CMD calls uvicorn directly) — N/A |
+| spectroscopy | `plt.show()` in `ESSSST.py` | No-op with Agg backend — N/A |
+| seq-interp | PySide6/pyqtgraph pulled in via `-r ../requirements.txt` | `seq_interp/requirements.docker.txt` without GUI libs — fixed  |
+
+---
+
+## Source layout
+
+```
+D:\Projects\
+├── lf_mri_platform\              ← this repo (infrastructure)
+│   ├── docker-compose.yml
+│   ├── .env.example
+│   ├── install.ps1
+│   ├── start.ps1 / start.bat
+│   ├── stop.ps1
+│   └── Makefile
+├── lf_orchestration\             ← orchestrator source
+├── lf_mri\
+│   ├── MRI-testing\
+│   │   ├── seq_interp\           ← seq-interp source
+│   │   └── lf_mri_gui\          ← GUI source
+│   │       ├── lf_mri_gui.spec  ← PyInstaller spec
+│   │       └── build_exe.bat
+│   └── fast-api-spectroscopy\   ← spectroscopy source
+├── fast-api-reconstruction\
+│   └── serv\                    ← reconstructor source
+└── lowfield_mri_programs\
+    └── spectrometer_service\
+        └── mserv00\             ← spectrometer (DRF) source
+```

+ 144 - 0
docker-compose.yml

@@ -0,0 +1,144 @@
+services:
+
+  # ── Orchestration workflow engine ─────────────────────────────────────────
+  orchestrator:
+    build:
+      context: ../lf_orchestration
+      dockerfile_inline: |
+        FROM python:3.12-slim
+        WORKDIR /app
+        ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
+        RUN apt-get update && apt-get install -y --no-install-recommends curl \
+            && rm -rf /var/lib/apt/lists/*
+        COPY requirements.txt .
+        RUN pip install --no-cache-dir -r requirements.txt
+        COPY . .
+        EXPOSE 1717
+        CMD ["uvicorn", "orchestrator.main:app", "--host", "0.0.0.0", "--port", "1717"]
+    image: lf-orchestrator:${ORCHESTRATOR_VERSION:-dev}
+    container_name: lf-orchestrator
+    ports:
+      - "${ORCHESTRATOR_PORT:-1717}:1717"
+    environment:
+      MODE: ${ORCHESTRATOR_MODE:-plug}
+      SPECTROMETER_URL: http://spectrometer:8000
+      RECONSTRUCTOR_URL: http://reconstructor:8000
+      SEQ_INTERP_URL: http://seq-interp:7475
+    depends_on:
+      seq-interp:
+        condition: service_healthy
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:1717/health"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+      start_period: 15s
+
+  # ── MRI sequence interpreter ───────────────────────────────────────────────
+  seq-interp:
+    build:
+      context: ../lf_mri/MRI-testing
+      dockerfile: seq_interp/Dockerfile
+    image: lf-seq-interp:${SEQ_INTERP_VERSION:-dev}
+    container_name: lf-seq-interp
+    ports:
+      - "${SEQ_INTERP_PORT:-7475}:7475"
+    volumes:
+      - seq_interp_input:/app/seq_interp/data/input
+      - seq_interp_output:/app/seq_interp/data/output
+      - seq_interp_logs:/app/seq_interp/log
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:7475/health"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+      start_period: 15s
+
+  # ── Spectroscopy signal processor ─────────────────────────────────────────
+  spectroscopy:
+    build:
+      context: ../lf_mri/fast-api-spectroscopy
+    image: lf-spectroscopy:${SPECTROSCOPY_VERSION:-dev}
+    container_name: lf-spectroscopy
+    ports:
+      - "${SPECTROSCOPY_PORT:-8002}:8002"
+    environment:
+      SERVICE_PORT: "8002"
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+      start_period: 15s
+
+  # ── MRI image reconstructor ────────────────────────────────────────────────
+  reconstructor:
+    build:
+      context: ../fast-api-reconstruction/serv
+      dockerfile_inline: |
+        FROM python:3.12-slim
+        WORKDIR /app
+        ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PYTHONPATH=/app MPLBACKEND=Agg
+        RUN apt-get update && apt-get install -y --no-install-recommends curl libgomp1 \
+            && rm -rf /var/lib/apt/lists/*
+        COPY requirements.txt .
+        RUN pip install --no-cache-dir -r requirements.txt
+        COPY . .
+        EXPOSE 8000
+        CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]
+    image: lf-reconstructor:${RECONSTRUCTOR_VERSION:-dev}
+    container_name: lf-reconstructor
+    ports:
+      - "${RECONSTRUCTOR_PORT:-8081}:8000"
+    volumes:
+      - reconstructor_sessions:/app/sessions
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+      start_period: 20s
+
+  # ── Spectrometer hardware controller (DRF) ────────────────────────────────
+  spectrometer:
+    build:
+      context: ../lowfield_mri_programs/spectrometer_service/mserv00
+      dockerfile_inline: |
+        FROM python:3.12-slim
+        WORKDIR /app
+        ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \
+            DJANGO_SETTINGS_MODULE=mserv00.settings \
+            DJANGO_ALLOWED_HOSTS=*
+        RUN apt-get update && apt-get install -y --no-install-recommends curl \
+            && rm -rf /var/lib/apt/lists/*
+        COPY requirements.txt .
+        RUN pip install --no-cache-dir -r requirements.txt
+        COPY . .
+        RUN sed -i "s/ALLOWED_HOSTS = \[.*/ALLOWED_HOSTS = ['*']/" mserv00/settings.py \
+            && python manage.py migrate --noinput
+        EXPOSE 8000
+        CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
+    image: lf-spectrometer:${SPECTROMETER_VERSION:-dev}
+    container_name: lf-spectrometer
+    ports:
+      - "${SPECTROMETER_PORT:-8000}:8000"
+    volumes:
+      - spectrometer_db:/app/db
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:8000/api/"]
+      interval: 15s
+      timeout: 5s
+      retries: 5
+      start_period: 30s
+
+volumes:
+  seq_interp_input:
+  seq_interp_output:
+  seq_interp_logs:
+  reconstructor_sessions:
+  spectrometer_db:

+ 145 - 0
install.ps1

@@ -0,0 +1,145 @@
+# ==============================================================================
+#  lf_mri_platform — One-time installation script
+#  Run as: .\install.ps1
+#  Requires: Windows 10/11, PowerShell 5.1+, internet access
+# ==============================================================================
+param(
+    [string]$PythonExe = "python",
+    [string]$GuiDir    = "..\lf_mri\MRI-testing\lf_mri_gui",
+    [string]$RepoRoot  = "..\lf_mri\MRI-testing"
+)
+
+$ErrorActionPreference = "Stop"
+
+function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }
+function Write-OK($msg)   { Write-Host "    [OK] $msg" -ForegroundColor Green }
+function Write-Warn($msg) { Write-Host "    [!!] $msg" -ForegroundColor Yellow }
+function Write-Fail($msg) { Write-Host "    [FAIL] $msg" -ForegroundColor Red; exit 1 }
+
+# ── 1. Check prerequisites ────────────────────────────────────────────────────
+Write-Step "Checking prerequisites"
+
+# Python
+try {
+    $pyVer = & $PythonExe --version 2>&1
+    if ($pyVer -match "3\.(1[0-9]|[0-9]+)") {
+        Write-OK "Python: $pyVer"
+    } else {
+        Write-Fail "Python 3.10+ required, found: $pyVer"
+    }
+} catch {
+    Write-Fail "Python not found. Install from https://python.org (add to PATH)"
+}
+
+# Docker
+try {
+    $dockerVer = & docker --version 2>&1
+    Write-OK "Docker: $dockerVer"
+} catch {
+    Write-Warn "Docker not found — services (orchestrator, seq_interp, etc.) won't start."
+    Write-Warn "Install Docker Desktop: https://www.docker.com/products/docker-desktop/"
+}
+
+# Docker Compose
+try {
+    $composeVer = & docker compose version 2>&1
+    Write-OK "Docker Compose: $composeVer"
+} catch {
+    Write-Warn "Docker Compose not found (included in Docker Desktop >= 3.0)"
+}
+
+# ── 2. Create Python virtual environment for the GUI ─────────────────────────
+Write-Step "Creating Python virtual environment"
+
+$venvPath = Join-Path $GuiDir ".venv"
+$absGuiDir = Resolve-Path $GuiDir -ErrorAction SilentlyContinue
+if (-not $absGuiDir) { Write-Fail "GUI directory not found: $GuiDir" }
+
+if (Test-Path $venvPath) {
+    Write-Warn "Venv already exists at $venvPath — skipping creation"
+} else {
+    & $PythonExe -m venv $venvPath
+    Write-OK "Created venv at $venvPath"
+}
+
+$pip = Join-Path $venvPath "Scripts\pip.exe"
+
+# ── 3. Install GUI dependencies ───────────────────────────────────────────────
+Write-Step "Installing GUI dependencies"
+
+# Install root requirements first (numpy, scipy, etc.)
+$rootReqs = Join-Path $RepoRoot "requirements.txt"
+if (Test-Path $rootReqs) {
+    & $pip install -r $rootReqs --quiet
+    Write-OK "Root requirements installed"
+}
+
+# Install GUI-specific requirements
+$guiReqs = Join-Path $GuiDir "requirements.txt"
+if (Test-Path $guiReqs) {
+    & $pip install -r $guiReqs --quiet
+    Write-OK "GUI requirements installed"
+}
+
+# Install LF_scanner as editable package
+$lfScannerSetup = Join-Path $RepoRoot "LF_scanner\setup.py"
+if (Test-Path $lfScannerSetup) {
+    & $pip install -e (Join-Path $RepoRoot "LF_scanner") --quiet
+    Write-OK "LF_scanner installed (editable)"
+} else {
+    Write-Warn "LF_scanner/setup.py not found — skipping"
+}
+
+# ── 4. Copy .env for Docker services ─────────────────────────────────────────
+Write-Step "Setting up Docker environment config"
+
+$envFile    = Join-Path $PSScriptRoot ".env"
+$envExample = Join-Path $PSScriptRoot ".env.example"
+
+if (-not (Test-Path $envFile)) {
+    Copy-Item $envExample $envFile
+    Write-OK ".env created from .env.example"
+} else {
+    Write-Warn ".env already exists — not overwriting"
+}
+
+# ── 5. Create desktop shortcut ────────────────────────────────────────────────
+Write-Step "Creating desktop shortcuts"
+
+$desktopPath = [Environment]::GetFolderPath("Desktop")
+$pythonExePath = Join-Path $venvPath "Scripts\python.exe"
+$appScript = Resolve-Path (Join-Path $GuiDir "app.py")
+
+# Shortcut: Start GUI only
+$wsh = New-Object -ComObject WScript.Shell
+$sc = $wsh.CreateShortcut("$desktopPath\LF-MRI GUI.lnk")
+$sc.TargetPath  = $pythonExePath
+$sc.Arguments   = "`"$appScript`""
+$sc.WorkingDirectory = (Resolve-Path $RepoRoot).Path
+$sc.Description = "LF-MRI System GUI"
+$sc.Save()
+Write-OK "Shortcut: 'LF-MRI GUI.lnk' on Desktop"
+
+# Shortcut: Start all (services + GUI)
+$startAll = Resolve-Path (Join-Path $PSScriptRoot "start.ps1")
+$scAll = $wsh.CreateShortcut("$desktopPath\LF-MRI Start All.lnk")
+$scAll.TargetPath       = "powershell.exe"
+$scAll.Arguments        = "-ExecutionPolicy Bypass -File `"$startAll`""
+$scAll.WorkingDirectory = $PSScriptRoot
+$scAll.Description      = "Start LF-MRI services + GUI"
+$scAll.Save()
+Write-OK "Shortcut: 'LF-MRI Start All.lnk' on Desktop"
+
+# ── 6. Summary ────────────────────────────────────────────────────────────────
+Write-Host ""
+Write-Host "============================================================" -ForegroundColor Green
+Write-Host "  Installation complete!" -ForegroundColor Green
+Write-Host "============================================================" -ForegroundColor Green
+Write-Host ""
+Write-Host "  Next steps:"
+Write-Host "  1. Start all services + GUI:  .\start.ps1"
+Write-Host "  2. GUI only (no services):    .\start.ps1 -GuiOnly"
+Write-Host "  3. Stop services:             .\stop.ps1"
+Write-Host ""
+Write-Host "  Config: edit .env to change ports or switch mode (plug/real)"
+Write-Host ""

+ 4 - 0
start.bat

@@ -0,0 +1,4 @@
+@echo off
+REM Shortcut wrapper — double-click to start services + GUI in plug mode
+powershell -ExecutionPolicy Bypass -File "%~dp0start.ps1"
+pause

+ 104 - 0
start.ps1

@@ -0,0 +1,104 @@
+# ==============================================================================
+#  lf_mri_platform — Start services and GUI
+#  Usage:
+#    .\start.ps1              — start Docker services + GUI
+#    .\start.ps1 -GuiOnly     — start GUI only (no Docker)
+#    .\start.ps1 -Mode real   — start in real hardware mode
+#    .\start.ps1 -ServicesOnly — start Docker services only
+# ==============================================================================
+param(
+    [switch]$GuiOnly,
+    [switch]$ServicesOnly,
+    [ValidateSet("plug","real")]
+    [string]$Mode = "plug",
+    [string]$GuiDir  = "..\lf_mri\MRI-testing\lf_mri_gui",
+    [string]$RepoRoot = "..\lf_mri\MRI-testing"
+)
+
+$ErrorActionPreference = "Stop"
+
+function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }
+function Write-OK($msg)   { Write-Host "    [OK] $msg" -ForegroundColor Green }
+function Write-Warn($msg) { Write-Host "    [!!] $msg" -ForegroundColor Yellow }
+
+$venvPython = Join-Path $GuiDir ".venv\Scripts\python.exe"
+$appScript  = Join-Path $GuiDir "app.py"
+
+# ── Start Docker services ─────────────────────────────────────────────────────
+if (-not $GuiOnly) {
+    Write-Step "Starting Docker services (mode: $Mode)"
+
+    # Check Docker is running
+    try { & docker info *>$null }
+    catch {
+        Write-Warn "Docker is not running. Starting Docker Desktop..."
+        Start-Process "C:\Program Files\Docker\Docker\Docker Desktop.exe" -ErrorAction SilentlyContinue
+        Write-Host "    Waiting 20 seconds for Docker to start..."
+        Start-Sleep 20
+    }
+
+    # Load .env
+    $envFile = Join-Path $PSScriptRoot ".env"
+    if (-not (Test-Path $envFile)) {
+        Copy-Item (Join-Path $PSScriptRoot ".env.example") $envFile
+        Write-Warn ".env not found — created from .env.example"
+    }
+
+    # Set mode and launch
+    $env:ORCHESTRATOR_MODE = $Mode
+    & docker compose --env-file $envFile up --build -d
+
+    Write-OK "Services started in '$Mode' mode"
+    Write-Host ""
+    Write-Host "    Waiting for services to become healthy..." -ForegroundColor DarkGray
+
+    # Wait for orchestrator health (up to 60s)
+    $maxWait = 60
+    $elapsed = 0
+    $orchPort = (Get-Content $envFile | Select-String "ORCHESTRATOR_PORT=(\d+)").Matches[0].Groups[1].Value
+    if (-not $orchPort) { $orchPort = "1717" }
+
+    while ($elapsed -lt $maxWait) {
+        try {
+            $r = Invoke-WebRequest "http://localhost:$orchPort/health" -UseBasicParsing -TimeoutSec 2 -ErrorAction SilentlyContinue
+            if ($r.StatusCode -eq 200) {
+                Write-OK "Orchestrator is up (http://localhost:$orchPort)"
+                break
+            }
+        } catch {}
+        Start-Sleep 3
+        $elapsed += 3
+        Write-Host "    ... $elapsed/$maxWait s" -ForegroundColor DarkGray
+    }
+    if ($elapsed -ge $maxWait) {
+        Write-Warn "Orchestrator not responding after ${maxWait}s — GUI will use local fallback"
+    }
+}
+
+# ── Start GUI ─────────────────────────────────────────────────────────────────
+if (-not $ServicesOnly) {
+    Write-Step "Starting LF-MRI GUI"
+
+    if (-not (Test-Path $venvPython)) {
+        Write-Warn "Venv not found at $venvPython"
+        Write-Warn "Run .\install.ps1 first, or using system python..."
+        $venvPython = "python"
+    }
+
+    Write-OK "Launching GUI..."
+    $absRepoRoot = Resolve-Path $RepoRoot
+    Start-Process $venvPython -ArgumentList "`"$appScript`"" `
+        -WorkingDirectory $absRepoRoot `
+        -WindowStyle Normal
+}
+
+# ── Status summary ────────────────────────────────────────────────────────────
+Write-Host ""
+Write-Host "  Service endpoints:" -ForegroundColor White
+Write-Host "    Orchestrator   http://localhost:1717/docs"
+Write-Host "    Seq-Interp     http://localhost:7475/docs"
+Write-Host "    Spectrometer   http://localhost:8000/admin/"
+Write-Host "    Reconstructor  http://localhost:8081/docs"
+Write-Host "    Spectroscopy   http://localhost:8002/docs"
+Write-Host ""
+Write-Host "  Stop services:  .\stop.ps1"

+ 22 - 0
stop.ps1

@@ -0,0 +1,22 @@
+# ==============================================================================
+#  lf_mri_platform — Stop all Docker services
+#  Usage:
+#    .\stop.ps1           — stop containers (keep volumes)
+#    .\stop.ps1 -Clean    — stop + remove volumes (full reset)
+# ==============================================================================
+param([switch]$Clean)
+
+$ErrorActionPreference = "Stop"
+
+Write-Host "`n==> Stopping LF-MRI services" -ForegroundColor Cyan
+
+$envFile = Join-Path $PSScriptRoot ".env"
+
+if ($Clean) {
+    Write-Host "    Removing containers AND volumes (full reset)" -ForegroundColor Yellow
+    & docker compose --env-file $envFile down --volumes
+} else {
+    & docker compose --env-file $envFile down
+}
+
+Write-Host "    [OK] Services stopped" -ForegroundColor Green