Metadata-Version: 2.4
Name: scd-studio
Version: 0.2.0
Summary: Desktop application for SCD decomposition, editing, and visualisation
Author-email: Agnese Grison <agnese.grison16@imperial.ac.uk>
License-Expression: BSD-3-Clause
Project-URL: Homepage, https://github.com/AgneGris/scd-studio
Project-URL: Repository, https://github.com/AgneGris/scd-studio
Project-URL: Issues, https://github.com/AgneGris/scd-studio/issues
Keywords: EMG,decomposition,GUI,spike editing,motor units
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: THIRD_PARTY_NOTICES.md
Requires-Dist: swarm-contrastive-decomposition>=0.2.3
Requires-Dist: numpy>=1.20.0
Requires-Dist: scipy>=1.7.0
Requires-Dist: h5py>=3.8.0
Requires-Dist: matplotlib>=3.5.0
Requires-Dist: pyqtgraph>=0.13.0
Requires-Dist: PySide6<6.12,>=6.5.0
Requires-Dist: scikit-learn>=1.0.0
Requires-Dist: mat73>=0.59
Requires-Dist: pyyaml>=6.0
Requires-Dist: cmcrameri>=1.0
Requires-Dist: openpyxl>=3.1.5
Provides-Extra: cpu
Requires-Dist: torch>=2.11.0; extra == "cpu"
Provides-Extra: cuda
Requires-Dist: torch>=2.11.0; extra == "cuda"
Provides-Extra: dev
Requires-Dist: imageio-ffmpeg>=0.6; extra == "dev"
Requires-Dist: pillow>=11.0; extra == "dev"
Requires-Dist: ruff>=0.16.8; extra == "dev"
Dynamic: license-file

# SCD Studio

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![PyPI](https://img.shields.io/pypi/v/scd-studio.svg)](https://pypi.org/project/scd-studio/)
[![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE)

A graphical application for researchers and engineers to decompose high-density surface or intramuscular EMG recordings into individual motor unit spike trains, edit them manually, and visualise population-level discharge behaviour.

Supports OT Bioelettronica `.otb+`/`.otb4`, Intan `.rhs`, MATLAB, HDF5,
NumPy and CSV recordings on Windows, Linux and macOS.

![SCD Studio demo](docs/demo.gif)

### Interface at a glance

| Configuration | Decomposition |
|---|---|
| [![Configuration tab with the bundled example loaded](docs/screenshots/configuration.png)](docs/screenshots/configuration.png) | [![Decomposition tab showing live source feedback](docs/screenshots/decomposition.png)](docs/screenshots/decomposition.png) |
| Load recordings, define grids and auxiliary channels, and review the channel map. | Configure and monitor decomposition with live source and quality feedback. |
| **Edition** | **Visualisation** |
| [![Edition tab previewing an automatic motor-unit split](docs/screenshots/edition.png)](docs/screenshots/edition.png) | [![Visualisation tab showing population-level motor-unit plots](docs/screenshots/visualisation.png)](docs/screenshots/visualisation.png) |
| Review quality, edit spike trains, split merged units, inspect individual MUAPs and manage duplicates. | Explore raster, discharge-rate, recruitment, force and quality summaries. |

Built on the [Swarm Contrastive Decomposition (SCD)](https://github.com/AgneGris/swarm-contrastive-decomposition) algorithm.
The desktop interface uses the official [Qt for Python (PySide6)](https://doc.qt.io/qtforpython-6/) bindings.

---

## Table of Contents

1. [Installation](#installation)
2. [Two-minute example](#two-minute-example)
3. [Choosing a tool](#choosing-a-tool)
4. [Launching the app](#launching-the-app)
5. [Complete workflow](#complete-workflow)
   - [Tab 1 — Configuration](#tab-1--configuration)
   - [Tab 2 — Decomposition](#tab-2--decomposition)
   - [Tab 3 — Edition](#tab-3--edition)
   - [Tab 4 — Visualisation](#tab-4--visualisation)
6. [Saving and loading](#saving-and-loading)
7. [Keyboard shortcuts](#keyboard-shortcuts)
8. [File formats](#file-formats)
9. [Importing an unfamiliar format](#importing-an-unfamiliar-format)
10. [Force channel setup](#force-channel-setup)
11. [Citation](#citation)
12. [Diagnostics and logs](#diagnostics-and-logs)
13. [Contributing](#contributing)
14. [License](#license)

---

## Installation

### From PyPI (recommended)

```bash
pip install scd-studio
```

Windows users who do not want to manage Python can instead download the
`windows-x86_64.zip` archive from the latest GitHub Release, extract it and run
`SCD-Studio.exe`. This standalone build is CPU-only; use a source installation
with the `cuda` extra for NVIDIA GPU acceleration.

If SCD Edition 0.1.x is already installed, replace the old distribution once:

```bash
pip uninstall scd-edition
pip install scd-studio
```

The `scd-edition` command remains available as a compatibility alias, and
existing decomposition files continue to open without conversion.

### Latest development version

```bash
pip install git+https://github.com/AgneGris/scd-studio.git
```

All dependencies install automatically.

On Windows, this route may install a CPU-only PyTorch build. Use the CUDA-enabled uv route below to ensure NVIDIA GPU support.

### From source with uv (recommended for development)

[uv](https://github.com/astral-sh/uv) manages the virtual environment and dependencies automatically.

Clone the repository, then choose the command for your platform:

```bash
git clone https://github.com/AgneGris/scd-studio
cd scd-studio
```

On Windows, install and require a uv-managed Python to avoid DLL conflicts with Conda or Anaconda:

```powershell
uv python install 3.13
```

**Windows — CUDA-enabled (recommended if you have an NVIDIA GPU):**
```bash
uv sync --python 3.13 --managed-python --extra cuda
.venv\Scripts\Activate.ps1
```

**Linux — CUDA-enabled:**
```bash
sudo apt-get install libegl1 libxkbcommon-x11-0 libxcb-cursor0
uv sync --extra cuda
source .venv/bin/activate
```

**macOS or CPU-only Linux:**
```bash
uv sync --extra cpu
source .venv/bin/activate
```

On CPU-only Linux, install the same Qt runtime packages shown in the CUDA-enabled Linux example before launching the GUI. Package names may differ on distributions that do not use `apt`.

**CPU-only Windows:**
```bash
uv sync --python 3.13 --managed-python --extra cpu
.venv\Scripts\Activate.ps1
```

The `cpu` and `cuda` extras are mutually exclusive, and `uv` does not remember
which one was used by an earlier sync. When running project commands through
`uv run`, repeat the selected extra. For CUDA, use
`uv run --extra cuda scd-studio`. Alternatively, use `uv run --no-sync` after
a successful sync. A bare `uv run` may replace the installed PyTorch variant.

### From source with pip

```bash
git clone https://github.com/AgneGris/scd-studio
cd scd-studio
pip install -e .
```

## Two-minute example

SCD Studio includes a 64-channel, 10-second surface EMG example. Open the raw
recording with its loader, sampling rate and electrode layout already filled
in:

```bash
scd-studio --example
```

Then:

1. Click **Apply Configuration**.
2. Click **Start Decomposition** and confirm the automatically selected channels.
3. Select **Full file**, or choose a short time interval for a faster first run.
4. Review the resulting units in **Edition** and **Visualisation**.

The example is for learning the workflow, not for benchmarking decomposition
accuracy. See the [illustrated quick-start](docs/quickstart.md) or the executable
[tutorial notebook](docs/tutorial.ipynb) for more detail.

The raw recording and matching ten-unit demo decomposition are tracked together
as [`examples/scd-demo/emg.mat`](examples/scd-demo/emg.mat) and
[`examples/scd-demo/emg_decomp_output.pkl`](examples/scd-demo/emg_decomp_output.pkl).
See the [example-data notes](examples/scd-demo/README.md) for the file contents
and instructions for opening either the raw recording or completed
decomposition.

To explore Edition without running a new decomposition, open the completed
`.pkl` from the **Edition** tab. This compact example stores the ten source
signals and spike trains but does not embed another copy of the raw EMG, so
MUAP display and filter recalculation are unavailable when it is opened by
itself. The same example data are also used by the Swarm-Contrastive
Decomposition project.

## Choosing a tool

SCD Studio is one of several strong tools for motor-unit decomposition. The
right choice depends on the algorithm, acquisition system and analysis workflow;
this table describes scope rather than ranking performance.

| Tool | Decomposition and scope | Environment and access | Particularly useful when |
|---|---|---|---|
| **SCD Studio** | Swarm Contrastive Decomposition; surface and intramuscular EMG; integrated review, spike editing and population plots | Open-source BSD Python/Qt app; PyPI and a standalone Windows release | You want the SCD method and a reproducible path from raw recording to edited units |
| [DEMUSE](https://demuse.feri.um.si/) | CKC decomposition, accuracy assessment and manual editing for multichannel surface EMG | Commercially available MATLAB program | You want the established CKC/DEMUSE workflow |
| [OTBioLab+](https://otbioelettronica.it/en/software/) | Manufacturer acquisition and processing suite with motor-unit decomposition | Vendor-distributed desktop software | You acquire with OT Bioelettronica hardware and want its native workflow |
| [openhdemg](https://github.com/GiacomoValliPhD/openhdemg) | Convolutive BSS plus broad import, analysis, tracking and editing functionality for HD-sEMG | Open-source BSD Python framework with graphical software | You need a broad motor-unit analysis library or interoperability with several decomposition formats |
| [MUedit](https://github.com/simonavrillon/MUedit) | fastICA decomposition and manual pulse-train editing | Open-source MATLAB app;  | You want the published MUedit protocol and its fastICA workflow |

Algorithm outputs should be reviewed using physiological criteria and validation
appropriate to the study; this table is not evidence that the tools are
interchangeable.

## Usage 🚀

## Launching the app

```bash
scd-studio
```

Or, if the entry point is not on your PATH:

```bash
python -m scd_app
```

The application opens with four tabs along the top. Work left to right: configure → decompose → edit → visualise.

You can also skip the first two tabs entirely: decompose with the [`swarm-contrastive-decomposition`](https://github.com/AgneGris/swarm-contrastive-decomposition) package in a script and open its output here — see [Decomposing with the SCD package, editing here](#decomposing-with-the-scd-package-editing-here).

---

## Decomposing with the SCD package, editing here

The Decomposition tab runs the same algorithm as the [`swarm-contrastive-decomposition`](https://github.com/AgneGris/swarm-contrastive-decomposition) Python package, so you can decompose in a script or on a cluster and use this app only for editing. Save the results with the package's `save_results`, then load the `.pkl` with **Load Decomposition** in the Edition tab:

```python
import scd

dictionary, timestamps = scd.train("recording.mat", config_name="surface")
scd.save_results("recording_surface.pkl", dictionary)
```

Since version 0.2.3 the package stores the loaded signal in the results by default (`save_data`), together with the trimming window and rejected channels it decomposed with. With that in the file, the Edition tab has everything an in-app decomposition would have: full-length sources, MUAP display, filter recalculation and spike re-detection. Note that the stored signal makes the file larger by `channels × samples × 4 bytes`; the package README explains how to turn it off.

What differs from an in-app decomposition:

- The file holds one grid, named after the `muscle-<name>` part of the filename, or the filename itself.
- No electrode geometry is recorded, so MUAPs are shown in the default grid layout, and there are no force or auxiliary channels.
- Files written with `save_data=False`, or by package versions before 0.2.3, open without the signal: spike trains and sources can still be edited and saved, but MUAPs, filter recalculation and re-detection are unavailable.

### The silhouette (SIL) is not the same number in the two tools

The package accepts sources using a silhouette it computes during decomposition; this app computes its own SIL for the Quality chart and the reliability flag. They are different quantities and should not be compared or thresholded interchangeably. This also applies to decompositions run in the Decomposition tab, whose *SIL Threshold* is the package's acceptance silhouette.

| | `swarm-contrastive-decomposition` (`dictionary["silhouettes"]`) | SCD Studio (MU properties, Quality chart) |
|---|---|---|
| When | During decomposition, at the iteration that accepted the unit; frozen in the output | After loading and after every edit, from the unit's current spikes |
| Source | The source of that iteration: computed from the peeled EMG (earlier units removed), restricted to the decomposition window, edge-masked, clamped, squared | The full-length source recomputed on load (spike-triggered-average filter on the whole recording, z-scored on the decomposition window), squared; not clamped or edge-masked |
| Points | Every peak of the squared source at least `reset_peak_separation_ms` apart — spikes *and* baseline peaks | Spikes: the unit's current timestamps. Baseline: every peak lower than the smallest spike |
| Classes | Two-class k-medians on peak heights; spikes are the upper cluster | No clustering — the spike set is whatever the unit currently has |
| Score | For each peak, `a` = distance to its own centroid, `b` = distance to the other centroid; SIL = mean over all peaks of `(b − a) / max(a, b)` | Only spike amplitudes enter: `d_s = Σ (s − mean_spikes)²`, `d_b = Σ (s − mean_baseline)²`; SIL = `(d_b − d_s) / max(d_b, d_s)` (`motor_unit_toolbox.get_silhouette_measure`, the formulation of Negro et al., 2016). Squared distances, summed; the baseline enters only through its mean |
| Threshold | `acceptance_silhouette` (0.85 default, 0.8 in the `surface` preset) decides whether a source is accepted | SIL ≥ 0.9 marks a unit as reliable |

In practice the two values are often close for well-separated units, but they diverge when the baseline peaks are numerous or spread out (the package averages over them, the toolbox ignores their spread), when a unit has been edited (only this app's value follows the edits), and whenever the two sources differ (window vs full recording, peeled vs recomputed filter). The package's values are kept in the file under `scd_metadata["silhouettes"]` for reference but are not displayed.

---

## Complete workflow

### Tab 1 — Configuration

This tab is where you tell the application about your recording before doing anything else.

#### 1. Select your input file

Click **Choose recording…** and select your EMG recording. Supported formats:

| Extension | Format |
|-----------|--------|
| `.otb+`   | OTBiolab+ (Quattrocento amplifier) |
| `.otb4`   | OTBiolab 4 (Novecento+ amplifier) |
| `.rhs`    | Intan RHS2000 (Stim/Recording Controller) |
| `.mat`    | MATLAB |
| `.h5`     | HDF5 |
| `.npy`    | NumPy array |
| `.csv`    | Comma-separated values |

Use **Choose batch…** to select multiple recordings that share one format and
channel layout. They will be decomposed sequentially with the same
configuration.

If a MATLAB, HDF5, NumPy, CSV, or text file does not match the selected format,
the array inspector opens automatically. You can also reopen it with the
advanced **Inspect arrays…** action beside **Data Format**. It lists numeric
arrays, lets you choose the EMG matrix and orientation, and previews the
resulting sample/channel shape. See
[Importing recordings without a built-in loader](docs/importing-data.md).

#### 2. Set the sampling rate

Enter the sampling rate of your recording in Hz (e.g. `2048` for a Quattrocento at 2 kHz). For formats that carry it in the file (`.otb4`, `.rhs`) it is filled in automatically.

**Decimate by** (next to the sampling rate) reduces the rate the decomposition works at by an integer factor, for any file format. The loader low-pass filters (zero-phase FIR) before subsampling, so nothing aliases, and the delivered rate is shown beside the box. Use it for recordings sampled far above the amplifier bandwidth — e.g. a 20 kHz Intan file band-limited at 500 Hz decimated by 5 to 4 kHz — so that extension factors and MUAP windows stay meaningful in milliseconds and runtime drops accordingly. Loader presets can seed the box (`decimate:` in the loader YAML); the value is saved with the configuration. The factor must divide the sampling rate exactly.

#### 3. Add electrode grids

Click **+ Add Grid** for each electrode array in your recording. For each grid, specify:

- **Name** — a label you choose (e.g. `Biceps`, `FDI`)
- **Muscle** — optional, for your own reference
- **Type** — Surface or Intramuscular
- **Electrode model** — select from the dropdown (e.g. `GR10MM0808` for a 64-channel 8×8 surface grid)
- **Channel start / end** — which channels in the file correspond to this grid (0-indexed, end is exclusive). The colour bar at the top shows how channels are allocated across all grids.

#### 4. Add force / auxiliary channels (optional)

If your recording includes force or other analogue channels, click **+ Add Aux Channel** for each one. Specify:

- **Name** — e.g. `Middle Ext`
- **Unit label** — e.g. `Middle Ext` (used to auto-select the correct channel when visualising named tasks)
- **Source** — `Signal` if force is stored as regular channels in the EMG array; `Auxiliary stream` for OTB+ `.sip` or Novecento+ external/AUX tracks
- **Channel start / end** — channel indices within the file (or sip stream)
- **MVC (mV)** — the maximum voluntary contraction value **in millivolts**. This is used to normalise force to %MVC in the visualisation. See [Force channel setup](#force-channel-setup) for how to find this value.

#### 5. Save / load your configuration

Click **Save Config** to export the full configuration (file path, grids, aux channels, parameters) to a JSON file. Click **Load Config** to restore a previously saved configuration. The input file is not overwritten when loading a config — the file you selected in step 1 is preserved.

#### 6. Apply

Click **Apply Configuration**. This validates all settings and prepares the decomposition tab. If anything is wrong (e.g. channel indices out of range) an error will appear here.

---

### Tab 2 — Decomposition

#### Global parameters

These apply to all grids:

| Parameter | What it does |
|-----------|--------------|
| **SIL Threshold** | Minimum silhouette score for a source to be accepted as a motor unit. Higher = stricter (fewer but more reliable MUs). Default 0.9. This is the decomposition algorithm's own silhouette, computed differently from the SIL shown in the Quality chart — see [The silhouette is not the same number in the two tools](#the-silhouette-sil-is-not-the-same-number-in-the-two-tools). |
| **Iterations** | Maximum number of optimisation steps per source. More iterations → longer runtime but potentially more MUs found. |
| **MUAP Window (ms)** | Duration of the spike-triggered-average window used for peel-off and for MUAP displays in the Edition tab. |
| **Fitness** | Optimisation criterion: `SIL` (silhouette) or `CoV` (coefficient of variation of ISI). |
| **Peel-off** | Whether to subtract each found motor unit from the signal before searching for the next. Recommended: on. |
| **Swarm mode** | Enables the particle swarm optimiser. Recommended: on. |
| **Adapt clamp** | Adaptive clamping during whitening. Recommended: on. |

#### Per-grid parameters

These can be set independently for each electrode grid by selecting the grid from the dropdown:

| Parameter | What it does |
|-----------|--------------|
| **SIL Threshold** | Per-grid SIL acceptance criterion (overrides global for this grid). |
| **Extension Factor** | Number of delayed copies of each channel used to extend the observation space. Larger values capture more motor unit information but increase computation time. Typical: 10–30 for surface, 20–40 for intramuscular. |
| **High-pass (Hz)** | High-pass filter cutoff. Use ≥10 Hz for surface, ≥20 Hz for intramuscular. |
| **Low-pass (Hz)** | Low-pass filter cutoff. 4400 Hz is typical for surface at 10 kHz; adjust to ~half the Nyquist of your sampling rate. |
| **Notch filter** | Remove power-line interference: `None`, `50 Hz` (Europe), or `60 Hz` (Americas). |
| **Notch harmonics** | Also remove harmonics (100, 150 Hz etc.) when notch is active. |

#### Batch processing options

When multiple files are queued:

- **Shared rejection** — perform channel rejection on the first file only, then apply the same rejection mask to all subsequent files. Useful when recording conditions are stable across files.
- **Per-file rejection** — perform channel rejection independently for each file.

#### Starting decomposition

1. Click **Start Decomposition**.
2. A signal plot appears. Noisy or broken channels are shown as dashed lines. **Click any channel to toggle rejection** (rejected channels are excluded from decomposition).
   - Scroll to zoom the time axis
   - Shift+Scroll to pan
   - Right-drag to pan
   - `R` to reset the view
3. Optionally set a **time window** (plateau) to decompose only a steady-state segment. Click on the plot to set start and end points, or enter times manually.
4. Click **Confirm** to start the actual decomposition. Progress and found sources are shown in real time.
5. Click **Stop** at any time to halt early. Results from completed grids are saved.

When decomposition finishes, the results are automatically saved to a `.pkl` file in the configured output folder, and the Edition tab opens.

---

### Tab 3 — Edition

This is the main editing environment. It shows one motor unit at a time.

#### Navigating between units

- Use the **Port** dropdown or the **Ctrl+Up/Down arrow keys** to switch between electrode grids.
- Use the **Unit** dropdown or the **Up/Down arrow keys** to switch between motor units within a port.
- Click **Mark Reviewed** (or press `M`) after checking a unit. **Next
  Unreviewed** (or `N`) advances through unfinished units across all ports, and
  the progress label shows the reviewed total. Review status is separate from
  the automatic reliability verdict and is saved with the session. Editing a
  reviewed unit's spikes or source signal resets it to unreviewed so the change
  is checked again.
- The **properties panel** on the right updates automatically:
  - **Spike count, mean discharge rate, CoV ISI, minimum ISI**
  - **SIL and MUAP template stability** — complementary source-separation and
    waveform-consistency measures.
  - **Reliability badge** — a unit is automatically **RELIABLE** when every
    automatic criterion passes:
    SIL ≥ 0.9, CoV ISI ≤ 40 %, discharge rate 3–40 Hz and at least 10
    spikes. Hover the badge to see which criteria a unit fails.
    Left-click the badge (or press `T`) to override the verdict by hand — an
    overridden badge is italic and marked *(manual)*, and the unit is shown as
    `✓*` / `✗*` in the unit dropdown. Right-click (or press `Shift+T`) to go
    back to the automatic verdict. Manual verdicts are saved with the session.
  - **MUAP amplitude, waveform length, peak and median frequency**
  - **Duplicate warning** — after an explicit duplicate scan, shows the
    current unit's matched partners and whether it is suggested for keeping or
    deletion

MUAP template stability is the normalized correlation (0–1) between two
spike-triggered average templates made from alternating discharges. It is
calculated from the original multichannel EMG, so it is unavailable when raw
EMG is not embedded in the file or when fewer than five valid discharges are
available for either half. It is shown as a descriptive consistency measure,
not used as an automatic acceptance threshold.

PNR is intentionally not reported for SCD units. Although the conventional
30 dB boundary is familiar in the field, it was validated for CKC sources and
does not have the same interpretation for SCD's normalized source signals.

#### Source signal plot

The large plot on the left shows the squared source signal (the spatial filter output) with spike locations marked as orange circles. A shaded region shows the plateau used for decomposition; outside this region the signal is reconstructed by replaying the peel-off sequence over the full recording.

Right-click an orange spike marker to inspect its multichannel MUAP. The MUAP
panel overlays that discharge as a thin solid orange line on a thicker blue
leave-one-out reference made from the unit's other valid discharges. The
header reports normalized waveform similarity (`r`), amplitude ratio and the
small alignment lag used for the comparison. This is a review aid rather than
an automatic keep/remove rule: overlapping activity and noise can reduce the
score of a genuine discharge.

The selected orange waveform starts in **Raw EMG** mode. Switch to **Earlier
units removed** to subtract only the units that came before the current unit
in the original peel-off sequence; MU 0 therefore has nothing to subtract.
This switch changes the orange waveform and its reported measurements, while
the blue reference stays fixed. Use the **Selected spike** toggle to hide or
show the orange overlay. While a spike is selected, `Left`/`Right`, `[`/`]`, or
the previous/next buttons move through the unit's spikes. Right-click the same
spike again, press `Escape`, or select another unit or port to finish the
comparison.

**Scrolling / zooming:**
- **Scroll** — zoom in/out along the time axis
- **Shift+Scroll** — scroll horizontally
- **Ctrl+Scroll** — zoom in/out in both axes
- `Home` — reset view to show the full signal

If force channels are configured, a force trace is overlaid on the source plot. The right y-axis shows % MVC, scaled automatically to the actual force range.

The **...** menu on interactive plots provides **View all** and **Export
plot**. Generic PyQtGraph transformations such as FFT, derivative, logarithmic
axes and Y vs Y' are intentionally not exposed because they change the live
plot state and can disrupt linked GUI views.

#### Editing modes

| Mode | How to activate | What it does |
|------|----------------|--------------|
| **View** | Default; press `Esc` to return | Navigate, zoom and inspect spikes without editing. |
| **Add spikes** | `A` or button | Toggle on, then drag a box to add all enclosed peaks. Press `A` again to turn it off. |
| **Delete spikes** | `D` or button | Toggle on, then drag a box to remove all enclosed spikes. Press `D` again to turn it off. |

Press `Ctrl+Z` to undo (up to 100 steps) and `Ctrl+Y` to redo.

#### Splitting a merged unit

Click **Split Unit** when one source appears to contain two motor units. The
preview partitions every spike from its squared source-peak height: the taller
population is orange **A**, and the lower population is cyan **B**. Click an
individual marker or drag a rectangle to move spikes between groups. Each group
must retain at least two spikes.

Click **Confirm Split** to create `[split A]` and `[split B]`, or click the
now-labelled **Cancel Split** button (or press `Escape`) to discard the preview
without changing the unit. After confirmation each child displays only its own
orange markers and can be edited like any other unit.

For the cleanest sequential separation, finish cleaning the high-amplitude A
train first. Then select split B and click **Recalculate Filter**. The app peels
A from the multichannel residual only at A's current curated timestamps before
estimating B; it does not directly subtract one normalized source trace from
another. Recalculate B again if A is edited later.

#### MUAP plot

Shows the spike-triggered average for every EMG channel. Toggle between **stacked** and **grid** layout (the grid layout reflects the physical electrode geometry). In the stacked view, scroll zooms the time axis, `Shift+Scroll` pans horizontally and `Ctrl+Scroll` zooms both axes. Click a channel to open a pop-out window with a larger view.

#### Quality actions

| Button | Shortcut | Effect |
|--------|----------|--------|
| **Recalculate Filter** | `F` | Re-estimates the spatial filter from the edited spike train, recomputes the source, and snaps the retained timestamps to nearby peaks. For split B, curated split A is peeled first. Requires ≥2 spikes. |
| **Split Unit** | — | Suggests high-amplitude A and low-amplitude B groups, opens an editable preview, and creates two independently editable units only after confirmation. |
| **Auto-edit** | `E` | Automatically removes obvious outlier spikes based on physiological firing rate limits. |
| **Remove outliers** | `O` | Removes spikes with very short or very long ISIs. |
| **Flag unit** | `X` | Manually marks the unit for deletion. |
| **Mark Reviewed** | `M` | Marks or unmarks the current unit as manually reviewed. |
| **Next Unreviewed** | `N` | Moves to the next unreviewed unit, including across ports. |
| **Delete All Flagged Units** | — | Permanently removes flagged units from every port and clears undo/redo history. A confirmation lists the count for each affected port. |
| **Check Duplicates in Current Port** | — | Runs rate-of-agreement on demand for every unit in the selected port, including flagged units, and suggests the lower-priority member of duplicate pairs for deletion. An existing flag cannot cause an unflagged partner to be suggested. Excessively large comparisons are skipped. |
| **Check Duplicates Across Ports** | — | Optionally performs the same on-demand comparison across grids/probes. Use this only when ports may record overlapping motor-unit populations. Excessively large comparisons are skipped. |

#### Duplicate detection

Rate-of-agreement (RoA) is not calculated while a decomposition is loading or
during ordinary MU-property updates. It runs only when you click one of the two
duplicate-check buttons, using a 30% agreement threshold. This avoids making
every load and spike edit pay the cost of a potentially large all-pairs
comparison.

Within one port, the estimated workload is the source length multiplied by the
square of the unit count. Across ports, it is the source length multiplied by
the two ports' unit counts. A comparison is skipped if that estimate exceeds
1,000,000,000; the results dialog identifies every skipped or failed port or
port pair instead of presenting an incomplete scan as a clean result.

A scan only suggests and flags the lower-priority members of duplicate pairs;
review the marked units before using **Delete All Flagged Units**. Changing a
source or any spike timestamps makes previous agreement scores stale, so the
app clears the current port's within-port results and all cross-port results.
The status bar then asks you to rerun the duplicate checks.

#### Saving

`Ctrl+S` saves the current state to a `.pkl` file. The first save opens a dialog and remembers the chosen destination; subsequent saves to the same file happen silently. Use **File → Save As…** (`Ctrl+Shift+S`) to choose a different destination. The saved file contains the edited spike trains, spatial filters, raw EMG, force data, and all metadata needed to reload and continue editing later.

---

### Tab 4 — Visualisation

This tab shows population-level summaries for all motor units in the loaded decomposition. It updates automatically whenever you switch to it or edit spikes in Tab 3.

The left panel lists all motor units by port. Click a unit to toggle it on/off. Use **All** / **None** to show or hide all units at once. The **Sort** dropdown reorders units by recruitment threshold, index, or mean discharge rate.

If force channels are configured, they appear as overlays on the time-domain plots. Click the legend (top-right of each plot) to toggle individual channels. The right y-axis shows % MVC, automatically scaled to the actual force range of the recording.

#### Sub-tabs

**Raster** — each row is one motor unit; vertical marks show individual spike times. Force is overlaid at the bottom, scaled to its actual %MVC range. The full force recording is shown even if only part of the signal was decomposed.

**IDR (Instantaneous Discharge Rate)** — smoothed discharge rate (pps) over time for each unit. Useful for checking that firing rates are physiologically plausible and that units track the force task.

**CST (Cumulative Spike Train)** — sum of all discharge rate traces. Approximates the neural drive to the muscle.

**Quality** — SIL and MUAP template-stability bar charts for all units. The SIL
chart shows the automatic reliability threshold (SIL ≥ 0.9); template
stability is descriptive and therefore has no threshold line. This SIL is
computed by the app from the unit's current spikes and is not the silhouette
the decomposition algorithm used to accept the source — see
[The silhouette is not the same number in the two tools](#the-silhouette-sil-is-not-the-same-number-in-the-two-tools).

**DR vs Force** — scatter plot of each motor unit's recruitment force (%MVC at first spike) against its mean discharge rate during the plateau. A regression line is drawn when three or more units are present. This plot requires at least one active force channel.

---

## Saving and loading

Decomposition results are stored as `.pkl` (Python pickle) files. Each file contains:

> **Security:** Python pickle files can execute code while they are opened.
> Only load decomposition files that you created yourself or received from a
> source you trust. Selected files are opened immediately.

- Raw EMG data (all channels, full recording)
- Spike timestamps for every motor unit
- Spatial filters (original and edited)
- Force / auxiliary channel data (full recording)
- Channel layout, rejection mask, electrode geometry
- Peel-off sequence (for filter recalculation)
- Quality metrics

Every new or edited decomposition is accompanied by a small
`<output-name>.audit.json` reproducibility report. For example, `result.pkl`
is accompanied by `result.audit.json`. The report records the software and
compute environment, input filename, processing window, grid parameters,
rejected channels, detected and retained motor-unit counts, and a summary of
later edits. It does
not contain signal samples, spike timestamps, note text, or absolute paths.
Input and output filenames can still contain participant identifiers, so
review an audit report before sharing it publicly.

To **reload** a decomposition: in Tab 3, click **Load Decomposition** and select the `.pkl` file. The app automatically recognises both SCD Studio files (including files created by SCD Edition 0.1.x) and raw `*_scddict.pkl` output from `swarm-contrastive-decomposition`. Raw SCD output is converted into a one-grid Edition session; the muscle label in the filename is used as the grid name, and any companion `*_scdcommit.txt` is retained as provenance.

SCD Studio files that contain the original EMG can replay peel-off and recalculate filters. Raw `swarm-contrastive-decomposition` output contains it too when saved with the package's default `save_data=True` (version 0.2.3 or later), so it gets the same treatment; output saved without the signal remains editable and can be saved from Edition, but filter/MUAP recalculation is unavailable. See [Decomposing with the SCD package, editing here](#decomposing-with-the-scd-package-editing-here).

When a previously edited file contains the original EMG, the app reopens it on
the full recording, preserving saved spike edits even outside the original
decomposition window, and asks whether to recalculate every motor-unit filter
from the saved edited spike trains. Choose **No** (the default) to retain the
saved filters, or **Yes** to re-estimate them during peel-off replay. The same
choice is now offered when the edition contains split units: split A is
processed before split B so B is estimated from the residual after A has been
peeled at its curated timestamps. Legacy split sessions are upgraded to this
two-step A-to-B replay when loaded.

For an unedited decomposition that covered only part of the recording, a
separate prompt asks whether to re-detect timestamps over the full signal or
keep the timestamps from the original decomposition window.

Saved Edition files can be reloaded in any order and remain fully editable.

---

## Keyboard shortcuts

Global shortcuts:

| Key | Action |
|-----|--------|
| `Ctrl+1` | Switch to Configuration tab |
| `Ctrl+2` | Switch to Decomposition tab |
| `Ctrl+3` | Switch to Edition tab |
| `Ctrl+4` | Switch to Visualisation tab |

Edition shortcuts (after a decomposition is loaded):

| Key | Action |
|-----|--------|
| `A` | Toggle **Add spikes**; drag a rectangle to add enclosed peaks |
| `D` | Toggle **Delete spikes**; drag a rectangle to remove enclosed spikes |
| `Esc` | Disarm selection or close the current spike-MUAP comparison |
| `F` | Replay peel-off and recalculate the current unit's filter, source and timestamps |
| `O` | Remove instantaneous-firing-rate outlier spikes from the current unit |
| `E` | Run rule-based auto-editing on the current unit |
| `X` | Toggle the deletion flag on the current unit |
| `M` | Toggle the manually reviewed status of the current unit |
| `N` | Move to the next unreviewed unit across all ports |
| `T` | Toggle the reliability verdict of the current unit |
| `Shift+T` | Reset reliability to the automatic verdict |
| `Ctrl+Z` | Undo (`Cmd+Z` on macOS) |
| `Ctrl+Y` | Redo (`Cmd+Shift+Z` on macOS) |
| `Ctrl+S` | Save (`Cmd+S` on macOS) |
| `Ctrl+Shift+S` | Save As… (`Cmd+Shift+S` on macOS) |
| `Up` / `Down` | Previous / next motor unit |
| `Ctrl+Up` / `Ctrl+Down` | Previous / next electrode grid or probe |
| `Left` / `Right` | Previous / next spike during MUAP comparison; otherwise pan the source plot |
| `[` / `]` | Previous / next spike during MUAP comparison |
| `Home` | Reset the source and firing-rate view |
| `Scroll` | Zoom time axis |
| `Shift+Scroll` | Pan horizontally |
| `Ctrl+Scroll` | Zoom both axes |
| `P` |  Open file notes to add post-its |

---

## File formats

### Input

| Format | Notes |
|--------|-------|
| `.otb+` | OTBiolab+ archive (Quattrocento). EMG channels and auxiliary `.sip` channels (force, angle) are both supported. |
| `.otb4` | OTBiolab 4 archive (Novecento+). EMG grids, external/AUX channels, sampling rate, and acquisition metadata are discovered from the embedded track metadata. |
| `.rhs` | Intan RHS2000 recording. Enabled amplifier channels (mV), board ADC inputs (V, as `aux`) and the sampling rate are read from the file header. Intan recordings are usually heavily oversampled relative to the amplifier bandwidth; use **Decimate by** (e.g. 5 for 20 kHz → 4 kHz with a 500 Hz band limit). |
| `.mat` | MATLAB v5 and v7.3 (HDF5-based). Nested arrays can be selected with **Inspect arrays…** using paths such as `recording.signal`. |
| `.h5`, `.hdf5` | HDF5. Numeric datasets can be selected with **Inspect arrays…**; portable SCD HDF5 files are recognised automatically. |
| `.npy` | NumPy array, shape `(channels, samples)` or `(samples, channels)` — the longer axis is assumed to be time. |
| `.csv`, `.txt` | Numeric samples × channels table. CSV accepts one header row; text is whitespace-delimited. |
| `.pkl` (Edition tab) | SCD Studio files, including SCD Edition 0.1.x files, and raw output of the [`swarm-contrastive-decomposition`](https://github.com/AgneGris/swarm-contrastive-decomposition) package (`scd.save_results`), which is converted on load. |

## Importing an unfamiliar format

For an unfamiliar MATLAB, HDF5, NumPy, CSV, or text layout, select it with
**Choose recording…**. The inspector opens automatically when the selected
format cannot locate an EMG matrix; it can also be opened manually with
**Inspect arrays…**. Save the finished configuration as a reusable JSON profile.
For a proprietary acquisition format, decode it with the vendor SDK
and write a portable, sample-first `.scd.h5` file with
`scd_app.io.write_portable_recording`. The schema, conversion example, and
verification checklist are in the
[data-import guide](docs/importing-data.md). Loader profiles never execute
file-supplied Python code.

### Output

`.pkl` files are standard Python pickle files. They can be opened in Python with:

Only do this with a file from a trusted source.

```python
import pickle

with open("my_decomp.pkl", "rb") as f:
    data = pickle.load(f)

# Key fields:
data["ports"]  # list of port names (one per electrode grid)
data["discharge_times"]  # list[list[np.ndarray]] — spike timestamps in samples
data["sampling_rate"]  # int — sampling frequency in Hz
data["data"]  # np.ndarray — raw EMG, shape (channels, samples)
data["aux_channels"]  # list of dicts — force/aux data and metadata
```

---

## Force channel setup

Force channels allow you to overlay the force trace on source signal and discharge rate plots, and to normalise force to %MVC.

### Step 1 — Find the MVC value

The MVC value you enter must be in the **same units as the force signal stored in the file**.

**For OTBiolab+ recordings (.otb+):**
The Quattrocento ADC stores force channels internally in **millivolts (mV)** after its analogue-to-digital conversion. OTBiolab+ displays force in **Volts** on screen (e.g. "MVC = 0.049 V"). To get the correct value for the config:

```
MVC in mV = OTBiolab+ displayed value × 1000
```

Example: if OTBiolab+ shows `MVC = 0.049 V` for Middle Extension → enter `49` in the config.

You can find the displayed MVC value by opening the `.otb+` file in OTBiolab+ and reading the scale shown next to the force channel.

**For Intan recordings (.rhs):** board ADC inputs are converted to Volts
(312.5 µV/bit); enter MVC in Volts.

**For Novecento+ recordings (.otb4):** external/AUX channels are converted
using their per-track ADC metadata and retain the unit declared in the file
(the supplied Novecento+ examples declare Volts). Enter MVC in that same unit.

**For other formats:** use whatever MVC value is in the same units as the raw signal values in your file. You can check what the signal amplitude looks like by loading a decomposition and reading the console output — when data loads, the application prints the force channel min, max, and net amplitude so you can verify the units.

### Step 2 — Add the channel in the config

In Tab 1, click **+ Add Aux Channel** and fill in:

- **Name** — descriptive label (e.g. `Middle Ext`)
- **Unit label** — must match exactly if you want auto-selection by filename (e.g. `Middle Ext` will be auto-enabled for files with `mvc-15ext_fing-M` in the name)
- **Source** — `Signal` (channel embedded in the EMG array) or `Auxiliary stream` (OTB+ `.sip` or Novecento+ external/AUX track)
- **Channel start / end** — 0-based index of the force channel
- **MVC (mV)** — value from Step 1

### Step 3 — Verify

After running decomposition, switch to Tab 4 and open the **DR vs Force** sub-tab. The right y-axis should show percentages consistent with the task (e.g. 0–20% for a 15% MVC contraction). If the values look wrong (e.g. showing 10,000%), check that the MVC value is in mV, not in Volts.

---

## Under development

- **Multi-file comparison** — loading two decompositions side by side for the same recording
- **Calibrated quality control** — SCD-specific acceptance thresholds validated
  against independent reference decompositions

---

## Citation

If you use SCD Studio, please cite the software itself. The DOI will be added
here after the first archived release; until then, use:

```bibtex
@software{grison_scd_studio_2026,
  author = {Grison, Agnese},
  title = {SCD Studio},
  version = {0.2.0},
  year = {2026},
  url = {https://github.com/AgneGris/scd-studio}
}
```

The decomposition method is described in:

```bibtex
@article{grison2024particle,
  title={A particle swarm optimised independence estimator for blind source separation of neurophysiological time series},
  author={Grison, Agnese and Clarke, Alexander Kenneth and Muceli, Silvia and Ib{\'a}{\~n}ez, Jaime and Kundu, Aritra and Farina, Dario},
  journal={IEEE Transactions on Biomedical Engineering},
  volume={72},
  number={1},
  pages={227--237},
  year={2025},
  doi={10.1109/TBME.2024.3446806},
  publisher={IEEE}
}

@article{grison2025unlocking,
  title={Unlocking the full potential of high-density surface EMG: novel non-invasive high-yield motor unit decomposition},
  author={Grison, Agnese and Mendez Guerra, Irene and Clarke, Alexander Kenneth and Muceli, Silvia and Ib{\'a}{\~n}ez, Jaime and Farina, Dario},
  journal={The Journal of Physiology},
  volume={603},
  number={8},
  pages={2281--2300},
  year={2025},
  doi={10.1113/JP287913},
  publisher={Wiley Online Library}
}
```

Machine-readable citation metadata are available in [`CITATION.cff`](CITATION.cff).

## Diagnostics and logs

SCD Studio keeps a small set of rotating diagnostic logs on your computer.
Use **Help > Open Log Folder** to find them, or **Help > Copy Diagnostics** to
copy the software, Python, operating-system and compute-backend details needed
for a useful bug report. Logs are limited to five 2 MB backups plus the active
file.

Diagnostic logs can contain local file names and paths. Review them before
sharing and never include participant recordings or identifying metadata in a
public issue.

## Contributing

Bug reports, format requests and code contributions are welcome. See
[`CONTRIBUTING.md`](CONTRIBUTING.md) for the development setup, test commands
and the information to include in a useful report.

The optional paper-style four-panel figure is generated from the real files in
`examples/scd-demo`:

```bash
uv run python scripts/capture_demo.py --overview-only
```

It writes `docs/figures/scd-studio-tabs.png`. The PNG is intentionally
gitignored; the capture code and example data are its reproducible source.

## License

SCD Studio is open-source software licensed under the [BSD 3-Clause License](LICENSE).

Third-party packages, including PySide6 and Qt, retain their own licences.
Vendored code and example-data provenance are listed in
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).

## Contact

**Agnese Grison**  
agnese.grison@outlook.it
