concord-server
FastAPI backend + browser-based interactive dashboard
Package Structure
concord-server/
src/concord_server/
server.py FastAPI app, mounts routers, serves static files
state.py AppState dataclass + helper functions
routes/
recording.py /api/load, /api/montage, /api/browse, /api/events
signal.py /api/timeseries, /api/psd, /api/spectrogram
metrics.py /api/metric/*, /api/electrode_positions, /api/brain_timeseries
notch.py /api/notch
bids.py /api/bids/*, /api/browse_dirs
static/
index.html Dashboard HTML shell
app.js, panels.js, timeseries.js, spectral.js, metrics.js, brain.js, browser.js
style.css
state.py — Session State
The server uses a single global AppState object to track what the user has loaded.
This is a simple in-memory store — no database, no file system.
| Field | Type | Description |
|---|---|---|
| recording | Recording | None | Currently active recording (montaged + filtered). What all API routes serve from. |
| raw_recording | Recording | None | Original monopolar data from EDF. Never touched after load — used to rebuild when user changes montage or notch. |
| path | str | Path of the currently loaded EDF file. |
| notch_mode | str | "none" | "notch50" | "notch60" |
| montage_mode | str | "monopolar" | "bipolar" | "car" |
| dataset_root | str | Root directory for BIDS dataset browsing. Set via CLI --data-root, CONCORD_DATA_ROOT env var, or the folder picker. |
| dataset_path | str | Path to the currently selected BIDS dataset directory. |
State functions
| Function | Description |
|---|---|
| get_state() → AppState | Returns the singleton state object. Routes call this to read/modify state. |
| set_recording(recording, path) | Called after successful load. Sets both raw_recording and recording to the freshly loaded data, resets montage/notch to defaults. |
| clear() | Resets all state fields to None/defaults. |
| require_recording() → Recording | Returns recording if loaded, raises HTTP 400 if not. Used by all routes that need data. |
The rebuild pattern
When the user changes montage or notch, the server doesn't modify the existing recording.
Instead, it rebuilds from scratch using _rebuild_recording(state):
def _rebuild_recording(s: AppState) -> None:
# Step 1: Apply montage to raw data
if s.montage_mode == "bipolar":
rec = to_bipolar(s.raw_recording)
elif s.montage_mode == "car":
rec = to_car(s.raw_recording)
else:
rec = s.raw_recording
# Step 2: Apply notch filter
if s.notch_mode in ("notch50", "notch60"):
freq = 50.0 if s.notch_mode == "notch50" else 60.0
for harmonic in [freq, 2.0 * freq]:
filtered = notch(filtered, rec.fs, freq=harmonic)
s.recording = rec # store new result
API Routes
Recording Routes — routes/recording.py
Body: {"path": "/absolute/path/to/file.edf"}
Loads a BIDS iEEG EDF file. Calls read_bids_ieeg(), stores in state.
Returns:
{
"path": "...",
"channels": ["SEEG1", ...],
"n_channels": 72,
"fs": 1024.0,
"duration": 300.0,
"montage": "monopolar",
"channel_metadata": {
"SEEG1": {"status": "good", "status_description": "soz", "x": 12.3, "y": -4.1, "z": 8.5}
},
"events": [{"onset": 45.2, "duration": 60.0, "label": "seizure"}]
}
Errors: 404 if file not found, 422 if file cannot be parsed.
Body: {"montage": "bipolar"} (or "monopolar" | "car")
Changes the active montage. Triggers rebuild from raw_recording.
Returns: {"montage": "bipolar", "channels": [...], "channel_metadata": {...}}
Note: bipolar channel names change (e.g. "SEEG1-SEEG2"), so the frontend rebuilds the sidebar.
Returns a directory listing for the file browser dialog.
Returns: {"path": "...", "parent": "...", "entries": [{"name": "file.edf", "is_dir": false, "size": 123456}, ...]}
Hidden files (starting with ".") are excluded.
Returns the event annotations for the current recording.
Returns: {"events": [{"onset": 45.2, "duration": 60.0, "label": "seizure"}, ...]}
Signal Routes — routes/signal.py
Returns downsampled time series data for rendering. Calls get_timeseries() from concord-viz.
| Query param | Description |
|---|---|
| t_start | Start time in seconds (optional) |
| t_end | End time in seconds (optional) |
| channels | Comma-separated channel names (optional, defaults to all) |
Returns: {"channels": [...], "times": [...], "values": [[...], ...], "events": [...]}
Returns Welch PSD for selected channels. Calls get_psd().
Returns: {"channels": [...], "freqs": [...], "power": [[...], ...]}
Returns STFT spectrogram for a single channel. Calls get_spectrogram().
Returns: {"times": [...], "freqs": [...], "power": [[...], ...]} (power in dB)
Metric Routes — routes/metrics.py
Returns windowed line length for all channels.
Returns windowed Hjorth parameters (activity, mobility, complexity) for all channels.
Returns band power per channel across 6 frequency bands.
Returns MNI coordinates for all channels that have them.
Returns: {"channels": [{"name": "SEEG1", "x": 12.3, "y": -4.1, "z": 8.5, "status": "good", "status_description": "soz"}, ...]}
Returns a scalar value per channel per time window for animating the 3D brain visualization.
Returns: {"channels": [...], "times": [...], "values": [[ch1_win1, ch1_win2, ...], ...]}
component selects which Hjorth parameter (0=activity, 1=mobility, 2=complexity) or band power index to extract.
Notch Route — routes/notch.py
Body: {"mode": "notch60"} (or "none" | "notch50")
Changes the active notch filter. Triggers rebuild from raw_recording with new filter applied.
Returns: {"mode": "notch60"}
BIDS Routes — routes/bids.py
Lists subdirectories of a path for the folder-picker dialog. If no path is given, uses the current dataset_root or falls back to the home directory.
Returns: {"path": "...", "parent": "...", "dirs": ["subdir1", "subdir2", ...]}
Scans a directory for BIDS datasets (subdirectories containing dataset_description.json). Sets dataset_root on the server state.
Returns: {"root": "...", "datasets": [{"dataset_id": "ds004100", "name": "HUP iEEG", "path": "...", "bids_version": "1.7.0", "n_subjects": 56}, ...]}
Lists subjects in a BIDS dataset. Parses participants.tsv and participants.json (field descriptions). Tags each subject with download status.
Returns: {"dataset": "...", "fields": {...}, "subjects": [{"participant_id": "sub-HUP117", "age": "35", "sex": "M", "downloaded": true}, ...]}
Lists sessions for a subject. Detects available modalities per session.
Returns: {"sessions": [{"session_id": "ses-presurgery", "path": "...", "modalities": ["ieeg"]}, ...]}
Lists recording files within a subject/session. Discovers recordings from both data files and JSON sidecars (marking undownloaded files as downloaded: false).
Returns: {"recordings": [{"filename": "..._ieeg.edf", "path": "...", "task": "ictal", "run": "01", "modality": "ieeg", "has_events": true, "has_channels": true, "downloaded": true}, ...]}
Streams download progress via Server-Sent Events (SSE). Uses the openneuro Python package to download data files for a single subject.
Events:
event: progress—{"line": "Downloading sub-HUP117..."}event: done—{"success": true, "subject": "sub-HUP117"}event: error—{"message": "..."}
server.py — App Assembly
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
# Register all route modules
app.include_router(recording.router)
app.include_router(signal.router)
app.include_router(metrics.router)
app.include_router(notch.router)
app.include_router(bids.router)
# Serve frontend from static/
app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")
# Convenience shutdown for development
@app.post("/api/shutdown")
def shutdown(): os.kill(os.getpid(), signal.SIGTERM)
Running the Server
# From repo root:
conda activate concord
python -m concord_server.server
# Or:
python serve.py
# Server starts on http://localhost:8000
# Ctrl+C or browser button to stop