Architecture
How the packages fit together and why they are organized this way
Core Principle: Layered Independence
Montage Concord is structured so that each layer only knows about the layers below it. No package ever imports from a package at the same level or above. This makes each package independently testable and replaceable.
graph TD
subgraph "Layer 4 — Interaction"
SERVER["concord-server\nFastAPI + browser dashboard"]
end
subgraph "Layer 3 — Visualization"
VIZ["concord-viz\nTimeseries, spectrogram, PSD, metric prep"]
end
subgraph "Layer 2 — Analysis & Simulation"
MU["metrics-\nunivariate"]
MS["metrics-\nspectral"]
MC["metrics-\nconnectivity"]
MJR["model-\njansen-rit"]
end
subgraph " "
MUTILS["metrics-utils\nWindowing, filtering, analytic signal"]
MODUTILS["models-utils\nIntegrators, noise, sigmoids"]
end
subgraph "Layer 1 — I/O"
IO["concord-io\nEDF reader, BIDS scanner, re-referencing"]
end
subgraph "Layer 0 — Foundation"
CORE["concord-core\nRecording, MetricResult, ParameterVector,\nModelOutput, Metric & Model ABCs"]
end
SERVER --> VIZ
VIZ --> MU & MS & MC
MU & MS & MC --> MUTILS
MJR --> MODUTILS
MUTILS --> IO
MODUTILS --> IO
IO --> CORE
The Core Types
Data flows through core container types and ABCs defined in concord-core. Every other package speaks this common language.
| Type | What it holds | Who creates it | Who reads it |
|---|---|---|---|
| Recording | Raw or re-referenced EEG signal: 2D array (channels × samples), sampling rate, channel names, montage, events, metadata | concord-io | metrics, viz, server |
| MetricResult | Output of a metric computation: N-D array + channel labels + optional frequency/time axes + units | Metric subclasses | viz, server, fit |
| ParameterVector | Named model parameter values + lower/upper bounds for optimization | Model.default_parameters() | Model.simulate(), fit |
| ModelOutput | Simulated signal: 2D array (nodes × samples) + all state variables + parameters used | Model subclasses | metrics, viz, server, fit |
| Metric (ABC) | Interface contract: name, requirements, compute() | concord-core defines it | all metric packages implement it; fit discovers them |
| Model (ABC) | Interface contract: name, default_parameters(), simulate() | concord-core defines it | all model packages implement it; fit discovers them |
Design Rules
No Cross-Imports at the Same Level
Metric packages (metrics-univariate, metrics-spectral, etc.) must not import each other.
If they need shared signal-processing code, it goes in metrics-utils.
Similarly, model packages must not import each other — shared math goes in models-utils.
Immutable Data Containers
Recording and MetricResult are Python dataclasses. Functions that transform data
(re-referencing, filtering, slicing) always return a new object — they never modify in place.
This makes the code easier to reason about and test.
Stateless, Pickle-Safe Functions
Metric classes carry only their configuration parameters (e.g., window size).
They do not store results. This means you can pickle a metric, send it to a worker process,
call compute(), and get a result back — critical for parallel fitting later.
Self-Registration via Entry Points
Metric and model packages register themselves in their pyproject.toml under
[project.entry-points]. The concord-fit package (and others) can
discover all installed metrics without any hard import list. See
Python Entry Points for how this works.
Package Size Limit
Each sub-package stays under ~1500 lines of implementation code. If a package exceeds this, it is a signal to split it further.
Package Roles at a Glance
| Package | Layer | Role | Key rule |
|---|---|---|---|
| concord-core | 0 — Foundation | Defines the shared language (types + interfaces) | Only numpy. No logic. |
| concord-io | 1 — I/O | Reads files; produces Recordings | Only package that knows file formats |
| concord-metrics-utils | 2 — Analysis support | Shared signal primitives (windowing, filtering) | No ABC knowledge; no cross-metric imports |
| concord-metrics-* | 2 — Analysis | Each implements Metric ABC | No cross-imports between metric packages |
| concord-models-utils | 2 — Simulation support | Integrators, noise, sigmoids | No ABC knowledge; no cross-model imports |
| concord-model-* | 2 — Simulation | Each implements Model ABC (future) | No cross-imports between model packages |
| concord-viz | 3 — Visualization | Pure: container → JSON dict for Plotly | Never modifies data; never computes metrics |
| concord-server | 4 — Interaction | HTTP API + static frontend | Manages session state; routes to viz functions |
| concord-fit | 4 — Optimization (future) | Discovers models/metrics; runs optimization | No hard imports of specific models/metrics |
| concord-connectome | 2 — Connectivity (future) | Structural connectivity, parcellation | Separate from functional metrics |
Python Conventions
- Python 3.11, all type annotations using
from __future__ import annotations - Import style: always
import numpy as np— neverfrom numpy import array - No nested functions — all functions defined at module top level for pickle safety
- src layout: each package has
src/concord_{name}/so imports never accidentally use the repo directory - pyproject.toml in every sub-package with editable install support
Development Workflow
Each sub-package is designed to be developed in an isolated Claude Code session.
The workflow: read this architecture page, read the target package's own CLAUDE.md,
read the core ABCs, then implement.
Tests for a specific package:
pytest concord-{name}/tests/
When a new dependency is added to a pyproject.toml, re-install:
pip install -e ./concord-{name}