| Title: | High-Density Surface EMG Motor-Unit Decomposition for the Physio Ecosystem |
|---|---|
| Description: | Decomposition of high-density surface electromyography (HD-sEMG) electrode-grid recordings into constituent motor-unit discharge patterns by convolutive blind source separation (extension, whitening and a fixed-point contrast maximisation with convolution-kernel-compensation refinement), following Holobar & Zazula (2007) and Negro et al. (2016). Provides motor-unit pulse trains, firing statistics (discharge rate, recruitment / derecruitment, pulse-to-noise ratio and silhouette quality), spatial motor-unit action-potential maps over the electrode grid, and a ground-truth HD-sEMG simulator for validation. |
| Authors: | Yusuke Matsui [aut, cre] |
| Maintainer: | Yusuke Matsui <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.2.1 |
| Built: | 2026-10-03 13:41:28 UTC |
| Source: | https://github.com/x-biosignal/PhysioHDEMG |
Decomposes a high-density surface EMG electrode-grid recording into its constituent motor-unit pulse trains by convolutive blind source separation: the channels are extended by delayed copies and whitened, each candidate source is seeded by a fixed-point contrast (kurtosis) maximisation and then refined by convolution-kernel compensation (the separation vector is iteratively set to the average whitened observation at the estimated discharge times), and discharge times are read off by peak detection with a two-class (spike / noise) split. Sources are accepted on a pulse-to-noise ratio and silhouette quality gate, and duplicate units are merged.
hdEMGDecompose( x, n_units = 8, extension_factor = 16, sampling_rate = NULL, assay_name = NULL, refractory_sec = 0.02, pnr_threshold = 26, sil_threshold = 0.9, max_iter = 100, tol = 1e-04, bandpass = NULL, seed = NULL )hdEMGDecompose( x, n_units = 8, extension_factor = 16, sampling_rate = NULL, assay_name = NULL, refractory_sec = 0.02, pnr_threshold = 26, sil_threshold = 0.9, max_iter = 100, tol = 1e-04, bandpass = NULL, seed = NULL )
x |
HD-sEMG data: an |
n_units |
Maximum number of separation vectors to attempt (default 8). |
extension_factor |
Number of delayed copies per channel used to build the extended observations (default 16). |
sampling_rate |
Sampling rate in Hz; required when |
assay_name |
Assay to use when |
refractory_sec |
Minimum inter-discharge interval in seconds used for peak separation (default 0.02). |
pnr_threshold |
Minimum pulse-to-noise ratio in dB for an accepted unit (default 26). |
sil_threshold |
Minimum silhouette for an accepted unit (default 0.9). |
max_iter |
Maximum fixed-point iterations per source (default 100). |
tol |
Convergence tolerance on the separation vector (default 1e-4). |
bandpass |
Optional length-2 numeric |
seed |
Optional integer random seed for reproducibility. |
The default quality gate (pnr_threshold = 26, sil_threshold = 0.9) is tuned
for clean data (e.g. make_hdemg_sim()). Real HD-sEMG motor units typically
have lower quality (PNR ~20-23 dB, silhouette ~0.85-0.92); for real recordings,
band-pass the signal (bandpass = c(20, 500)) and relax the thresholds
(pnr_threshold ~ 20, sil_threshold ~ 0.85). Validated against openhdemg on
a real 64-channel recording: PhysioHDEMG recovered 4 of 5 reference motor units
at rate-of-agreement 0.79-0.99 (lag-aligned).
An hdemg_decomposition object with pulse_trains (list of
discharge sample indices), a quality data frame
(unit, n_discharges, discharge_rate, pnr_db, sil), the separation
filters, and the retained signal/grid/sampling_rate for downstream
firing statistics and action-potential maps.
Holobar, A. & Zazula, D. (2007). Multichannel blind source separation using convolution kernel compensation. IEEE TSP 55(9):4487-4496. Negro, F. et al. (2016). J Neural Eng 13(2):026027.
make_hdemg_sim(), muFiringStats(), muActionPotentialMaps()
sim <- make_hdemg_sim(n_units = 2, duration_sec = 3, grid = c(4, 4), noise_sd = 0.005, seed = 1) dec <- hdEMGDecompose(sim, n_units = 4, extension_factor = 10, seed = 1) dec$qualitysim <- make_hdemg_sim(n_units = 2, duration_sec = 3, grid = c(4, 4), noise_sd = 0.005, seed = 1) dec <- hdEMGDecompose(sim, n_units = 4, extension_factor = 10, seed = 1) dec$quality
Builds a PhysioExperiment (from PhysioCore) holding a single HD-sEMG assay
(n_time x n_channels) at the given sampling rate, recording the electrode
grid layout (rows, cols) in the channel metadata and object metadata so
that hdEMGDecompose() can recover the grid for action-potential maps.
hdemgToPhysioExperiment( signal, sampling_rate, grid, unit = "uV", metadata = list() )hdemgToPhysioExperiment( signal, sampling_rate, grid, unit = "uV", metadata = list() )
signal |
An |
sampling_rate |
Sampling rate in Hz. |
grid |
Integer |
unit |
Signal unit stored in the channel metadata (default |
metadata |
Optional named list stored in the object metadata. |
A PhysioExperiment object.
hdEMGDecompose(), make_hdemg_sim()
sim <- make_hdemg_sim(n_units = 2, duration_sec = 2, grid = c(4, 4), seed = 1) # PhysioCore supplies the shared PhysioExperiment data model. if (requireNamespace("PhysioCore", quietly = TRUE) && requireNamespace("S4Vectors", quietly = TRUE)) { pe <- hdemgToPhysioExperiment(sim$signal, sim$sampling_rate, grid = c(4, 4)) pe }sim <- make_hdemg_sim(n_units = 2, duration_sec = 2, grid = c(4, 4), seed = 1) # PhysioCore supplies the shared PhysioExperiment data model. if (requireNamespace("PhysioCore", quietly = TRUE) && requireNamespace("S4Vectors", quietly = TRUE)) { pe <- hdemgToPhysioExperiment(sim$signal, sim$sampling_rate, grid = c(4, 4)) pe }
Generates a synthetic HD-sEMG electrode-grid signal as the linear superposition of a known set of motor units, each firing at a target discharge rate with inter-spike-interval jitter and each with its own spatial action-potential map over the grid. The ground-truth discharge times are returned so that a decomposition can be validated (rate of agreement, discharge-rate error).
make_hdemg_sim( n_units = 4, duration_sec = 10, sampling_rate = 2048, grid = c(5, 5), firing_rates = NULL, muap_length = 24, cov_isi = 0.1, noise_sd = 0.02, seed = NULL )make_hdemg_sim( n_units = 4, duration_sec = 10, sampling_rate = 2048, grid = c(5, 5), firing_rates = NULL, muap_length = 24, cov_isi = 0.1, noise_sd = 0.02, seed = NULL )
n_units |
Number of motor units to simulate (default 4). |
duration_sec |
Recording duration in seconds (default 10). |
sampling_rate |
Sampling rate in Hz (default 2048). |
grid |
Integer |
firing_rates |
Optional numeric vector of per-unit mean discharge
rates (Hz); defaults to an evenly spaced spread in |
muap_length |
MUAP template length in samples (default 24). |
cov_isi |
Coefficient of variation of the inter-spike interval (firing-time jitter, default 0.1). |
noise_sd |
Standard deviation of additive white noise, in the same units as the unit-peak MUAPs (default 0.02; smaller = higher SNR). |
seed |
Optional integer random seed for reproducibility. |
An hdemg_sim object (a list) with:
An n_time x n_channels matrix of simulated HD-sEMG.
Sampling rate in Hz.
The c(rows, cols) grid layout.
A length-n_units list of integer ground-truth discharge
sample indices.
The realised per-unit mean discharge rate (Hz).
A length-n_units list of muap_length x n_channels
ground-truth action-potential templates.
Number of motor units.
Farina, D. et al. (2010). Decoding the neural drive to muscles from the surface electromyogram. J Physiol 588(11):1969-1975.
sim <- make_hdemg_sim(n_units = 3, duration_sec = 2, grid = c(4, 4), seed = 1) dim(sim$signal) lengths(sim$spikes)sim <- make_hdemg_sim(n_units = 3, duration_sec = 2, grid = c(4, 4), seed = 1) dim(sim$signal) lengths(sim$spikes)
Scores a decomposition against a reference set of motor-unit discharge
patterns with a lag-aware rate-of-agreement (RoA): for each reference unit the
best-matching decomposed unit is found (allowing a constant latency of up to
max_lag samples), and the RoA – the fraction of discharges the two share
within tol samples, matched / (n_a + n_b - matched) – is reported. This
scores cross-tool recovery ("did we recover the units a reference finds, and
which do we miss") rather than bit-identical agreement, so a constant latency
between two decompositions does not penalise a correct match.
matchMotorUnits(decomposition, reference, tol = 1L, max_lag = 0L)matchMotorUnits(decomposition, reference, tol = 1L, max_lag = 0L)
decomposition |
An |
reference |
The reference decomposition: a list of integer discharge-
sample vectors (one per reference unit), or a |
tol |
Matching tolerance in samples (default 1). |
max_lag |
Maximum constant latency in samples to search when matching (default 0 = exact alignment; use a few milliseconds' worth for cross-tool scoring). |
A data frame (class mu_match), one row per reference unit, with
columns ref_mu, n_discharges, matched_unit, roa_lag_aware,
roa_no_lag, lag, pnr_db, sil.
Holobar A, Minetto MA, Farina D (2014). Accurate identification of motor unit discharge patterns from high-density surface EMG and validation with a novel signal-based performance metric. J Neural Eng 11:016008.
hdEMGDecompose(), readHDEMG(), muPulseTrains()
# Score a decomposition against the simulator's known discharge times. sim <- make_hdemg_sim(n_units = 2, duration_sec = 3, grid = c(4, 4), noise_sd = 0.005, seed = 1) dec <- hdEMGDecompose(sim, n_units = 4, extension_factor = 10, seed = 1) matchMotorUnits(dec, sim$spikes, tol = 2, max_lag = 5)# Score a decomposition against the simulator's known discharge times. sim <- make_hdemg_sim(n_units = 2, duration_sec = 3, grid = c(4, 4), noise_sd = 0.005, seed = 1) dec <- hdEMGDecompose(sim, n_units = 4, extension_factor = 10, seed = 1) matchMotorUnits(dec, sim$spikes, tol = 2, max_lag = 5)
Spike-triggered average of the raw HD-sEMG for each motor unit, yielding the multi-channel action-potential waveform and its peak-to-peak amplitude across the electrode grid (the spatial "map" used to localise the unit).
muActionPotentialMaps(decomposition, window_ms = 15)muActionPotentialMaps(decomposition, window_ms = 15)
decomposition |
An |
window_ms |
Half-window in milliseconds averaged around each discharge (default 15). |
A length-n_units list; each element has waveform
((2*w+1) x n_channels spike-triggered average), amplitude (per-channel
peak-to-peak), map (the amplitude reshaped to the rows x cols grid, or
NULL if the grid is unknown), and peak_channel (the strongest channel).
Farina, D. et al. (2016). Reflections on the evolution of surface EMG decomposition. J Electromyogr Kinesiol 26:39-46.
sim <- make_hdemg_sim(n_units = 2, duration_sec = 3, grid = c(4, 4), noise_sd = 0.005, seed = 1) dec <- hdEMGDecompose(sim, n_units = 4, extension_factor = 10, seed = 1) maps <- muActionPotentialMaps(dec) maps[[1]]$peak_channelsim <- make_hdemg_sim(n_units = 2, duration_sec = 3, grid = c(4, 4), noise_sd = 0.005, seed = 1) dec <- hdEMGDecompose(sim, n_units = 4, extension_factor = 10, seed = 1) maps <- muActionPotentialMaps(dec) maps[[1]]$peak_channel
Per-unit discharge-rate and recruitment statistics: the mean discharge rate, the mean and coefficient of variation of the inter-spike interval, the recruitment (first discharge) and derecruitment (last discharge) times, and the decomposition quality (pulse-to-noise ratio and silhouette).
muFiringStats(decomposition)muFiringStats(decomposition)
decomposition |
An |
A data frame with one row per motor unit and columns unit,
n_discharges, discharge_rate (Hz), mean_isi_sec, cov_isi,
recruitment_sec, derecruitment_sec, pnr_db, sil.
De Luca, C.J. et al. (1982). Behaviour of human motor units in different muscles during linearly varying contractions. J Physiol 329:113-128.
hdEMGDecompose(), muPulseTrains()
sim <- make_hdemg_sim(n_units = 2, duration_sec = 3, grid = c(4, 4), noise_sd = 0.005, seed = 1) dec <- hdEMGDecompose(sim, n_units = 4, extension_factor = 10, seed = 1) muFiringStats(dec)sim <- make_hdemg_sim(n_units = 2, duration_sec = 3, grid = c(4, 4), noise_sd = 0.005, seed = 1) dec <- hdEMGDecompose(sim, n_units = 4, extension_factor = 10, seed = 1) muFiringStats(dec)
Returns the discharge instants of every decomposed motor unit in tidy form.
muPulseTrains(decomposition)muPulseTrains(decomposition)
decomposition |
An |
A data frame with one row per discharge: unit (integer),
discharge (sample index), and time_sec (discharge time in seconds).
hdEMGDecompose(), muFiringStats()
sim <- make_hdemg_sim(n_units = 2, duration_sec = 3, grid = c(4, 4), noise_sd = 0.005, seed = 1) dec <- hdEMGDecompose(sim, n_units = 4, extension_factor = 10, seed = 1) head(muPulseTrains(dec))sim <- make_hdemg_sim(n_units = 2, duration_sec = 3, grid = c(4, 4), noise_sd = 0.005, seed = 1) dec <- hdEMGDecompose(sim, n_units = 4, extension_factor = 10, seed = 1) head(muPulseTrains(dec))
Reads an HD-sEMG electrode-grid recording from an HDF5 (.h5) file into a
PhysioExperiment, the ingest counterpart of
hdemgToPhysioExperiment() for on-disk grids. The file must contain a
signal dataset (the raw grid signal) and a sampling_rate scalar; it may
also contain a grid dataset (c(rows, cols)) and a reference_mus group of
per-unit integer discharge-sample vectors – a bundled reference decomposition
(e.g. exported from the openhdemg sample file) – which are stored in the
returned object's metadata as reference_mupulses for scoring a decomposition
with matchMotorUnits().
readHDEMG( path, grid = NULL, signal_name = "signal", rate_name = "sampling_rate", unit = "uV" )readHDEMG( path, grid = NULL, signal_name = "signal", rate_name = "sampling_rate", unit = "uV" )
path |
Path to the HDF5 file. |
grid |
Optional integer |
signal_name, rate_name
|
Dataset names for the signal and the sampling
rate (defaults |
unit |
Signal unit stored in the channel metadata (default |
The signal dataset may be stored n_time x n_channels or
n_channels x n_time; the longer axis is taken as time.
A PhysioExperiment holding the HD-sEMG grid, with any bundled
reference decomposition in metadata reference_mupulses.
hdEMGDecompose(), matchMotorUnits(), hdemgToPhysioExperiment()
# Round-trip a tiny grid through a temporary HDF5 file (needs rhdf5). if (requireNamespace("rhdf5", quietly = TRUE) && requireNamespace("PhysioCore", quietly = TRUE)) { sim <- make_hdemg_sim(n_units = 2, duration_sec = 2, grid = c(4, 4), seed = 1) f <- tempfile(fileext = ".h5") rhdf5::h5createFile(f) rhdf5::h5write(sim$signal, f, "signal") rhdf5::h5write(sim$sampling_rate, f, "sampling_rate") rhdf5::h5write(as.integer(sim$grid), f, "grid") rhdf5::H5close() pe <- readHDEMG(f) unlink(f) pe }# Round-trip a tiny grid through a temporary HDF5 file (needs rhdf5). if (requireNamespace("rhdf5", quietly = TRUE) && requireNamespace("PhysioCore", quietly = TRUE)) { sim <- make_hdemg_sim(n_units = 2, duration_sec = 2, grid = c(4, 4), seed = 1) f <- tempfile(fileext = ".h5") rhdf5::h5createFile(f) rhdf5::h5write(sim$signal, f, "signal") rhdf5::h5write(sim$sampling_rate, f, "sampling_rate") rhdf5::h5write(as.integer(sim$grid), f, "grid") rhdf5::H5close() pe <- readHDEMG(f) unlink(f) pe }