Decomposition and Cleaning
Version 0.2.0 introduces a new decomposition workflow for extracting motor unit discharge times from raw HDsEMG signals.
The main components are:
ConvolutiveBSSParams, a dataclass containing parameters for convolutive blind source separation;
convolutive_bss(), the lower-level decomposition function;
EMGDecomposer, a high-level pipeline that can filter, remove power-line harmonics, exclude bad channels, decompose, reconstruct an emgfile, and remove duplicate MUs;
select_bad_channels(), a visual workflow for marking noisy channels;
run_bss_mu_editor(), a lightweight UI for basic manual editing of BSS discharge selections;
remove_powerline_harmonics(), an FFT-based function for suppressing harmonics of the selected mains frequency;
remove_duplicates_within(), a within-file duplicate-removal function based on spike-train timing.
The high-level decomposer expects an emgfile with at least:
RAW_SIGNAL, a pandas DataFrame with samples by channels;
FSAMP, the sampling frequency in Hz.
import openhdemg.library as emg
emgfile = emg.emg_from_samplefile()
print(emgfile.keys())
The sample file is already decomposed (it contains a few units), but it is still useful for learning the API structure.
Mark Bad Channels
Before decomposition, inspect raw channels and mark noisy or unusable channels.
import openhdemg.library as emg
emgfile = emg.askloadmodule()
emgfile = emg.select_bad_channels(emgfile=emgfile)
The selected information is stored in:
EMGDecomposer can use this key to exclude bad channels before decomposition.
Create a ConvolutiveBSSParams object and adjust the parameters needed for your recording.
import openhdemg.library as emg
params = emg.ConvolutiveBSSParams()
params.n_iterations = 500
params.silhouette_threshold = 0.90
params.extension_factor = 16
params.min_spike_count = 10
Common parameters to review:
| Parameter |
Meaning |
n_iterations |
Maximum number of source-extraction attempts. |
silhouette_threshold |
Minimum SIL required to accept a candidate MU. |
extension_factor |
Signal-extension factor used before blind source separation. |
rem_activity_index |
Whether to remove all the identified spikes from the activity index to reduce re-detection of the same source. |
min_spike_count |
Minimum number of spikes required to accept a candidate MU. |
Run the High-Level Pipeline
The simplest workflow is:
import openhdemg.library as emg
emgfile = emg.emg_from_samplefile()
params = emg.ConvolutiveBSSParams()
params.n_iterations = 500
params.silhouette_threshold = 0.90
decomposer = emg.EMGDecomposer()
decomposer.set_decomposition_parameters(params)
decomposed_emgfile = decomposer.run_decomposition(emgfile)
By default, EMGDecomposer:
- applies band-pass filtering with order 2 and 20-500 Hz cut-offs;
- does not remove power-line harmonics unless
notch_enabled=True;
- excludes bad channels if
GOOD_CHANNELS is present;
- runs
convolutive_bss();
- reconstructs the
emgfile with decomposition outputs;
- removes duplicate MUs with default within-file duplicate-removal parameters.
Change band-pass and power-line harmonic settings with change_filtering_parameters().
decomposer = emg.EMGDecomposer()
decomposer.change_filtering_parameters(
bandpass_enabled=True,
bandpass_order=2,
bandpass_lowcut=20,
bandpass_highcut=500,
notch_enabled=True,
notch_freq=50.0,
notch_width=5.0,
)
Disable all filtering:
decomposer.change_filtering_parameters(
bandpass_enabled=False,
notch_enabled=False,
)
Bad-channel exclusion is enabled by default.
decomposer.use_good_channels_only(True)
If GOOD_CHANNELS is absent, the decomposer warns and continues without excluding channels.
Disable bad-channel exclusion:
decomposer.use_good_channels_only(False)
Within-file duplicate removal is enabled by default but you can change its parameters. For example, you can make it more conservative by increasing peak_window_half_width:
decomposer.change_duplicate_removal_parameters(
duplicate_removal_enabled=True,
correlation_max_lag=50e-3,
peak_window_half_width=5e-3,
duplicate_threshold=30,
which="accuracy",
)
Output Keys
After successful decomposition, the returned emgfile can include:
DECOMPOSITION_PARAMETERS, containing method, filtering, bad-channel, and duplicate-removal settings;
NUMBER_OF_MUS;
MUPULSES;
IPTS;
ACCURACY;
BINARY_MUS_FIRING.
If no MUs are detected, NUMBER_OF_MUS is set to 0 and MU-specific keys may be absent.
Always check:
print(decomposed_emgfile.get("NUMBER_OF_MUS", 0))
print(decomposed_emgfile.get("DECOMPOSITION_PARAMETERS"))
Plot and Inspect Results
if decomposed_emgfile.get("NUMBER_OF_MUS", 0) > 0:
emg.plot_mupulses(
emgfile=decomposed_emgfile,
addrefsig="REF_SIGNAL" in decomposed_emgfile,
refsig_channel=0,
)
emg.plot_ipts(
emgfile=decomposed_emgfile,
show_markers=True,
)
else:
print("No MUs were detected.")
Use physiological criteria, source separation quality, discharge-rate behaviour, and visual inspection before accepting the output.
Save the Decomposed File
Save the decomposed output as a binary module:
emg.save_openhdemg_module(
emgfile=decomposed_emgfile,
path="C:/Users/.../Desktop/openhdemg_modules",
module_name="participant_01_trial_01_decomposed",
)
Direct Use of convolutive_bss()
Advanced users can call convolutive_bss() directly with an array shaped as channels by samples.
import numpy as np
import openhdemg.library as emg
emgfile = emg.askloadmodule()
emgsig = np.transpose(
emgfile["RAW_SIGNAL"].to_numpy(dtype=np.float64)
)
params = emg.ConvolutiveBSSParams()
params.n_iterations = 250
mupulses, ipts, sil = emg.convolutive_bss(
emgsig=emgsig,
fsamp=emgfile["FSAMP"],
decomposition_params=params,
)
The direct function is useful for algorithm development. For routine workflows, EMGDecomposer is preferred because it reconstructs the emgfile and records processing metadata.
Complete Example
import openhdemg.library as emg
raw_emgfile = emg.askloadmodule()
raw_emgfile = emg.select_bad_channels(raw_emgfile)
params = emg.ConvolutiveBSSParams()
params.n_iterations = 250
params.silhouette_threshold = 0.90
params.min_spike_count = 20
decomposer = emg.EMGDecomposer()
decomposer.set_decomposition_parameters(params)
decomposer.change_filtering_parameters(
bandpass_enabled=True,
bandpass_order=2,
bandpass_lowcut=20,
bandpass_highcut=900,
notch_enabled=True,
notch_freq=50.0,
notch_width=5.0,
)
decomposed_emgfile = decomposer.run_decomposition(raw_emgfile)
if decomposed_emgfile.get("NUMBER_OF_MUS", 0) > 0:
emg.plot_mupulses(
decomposed_emgfile,
addrefsig="REF_SIGNAL" in decomposed_emgfile,
refsig_channel=0,
)
emg.asksavemodule(emgfile=decomposed_emgfile)
Lightweight BSS MU Editor
For small manual corrections, openhdemg also provides a lightweight cleaning UI through run_bss_mu_editor(). This editor can display the discharge-rate/IPTS traces, add or remove discharge times, recompute the current MU source using a user-provided extended, centered, and whitened signal, and mark MUs that should be deleted later.
Advanced MU cleaning
The most advanced MU cleaning tools are available in the openhdemg software. The software provides the dedicated cleaning environment for extensive manual review and should be preferred when advanced editing, quality-control panels, and a more complete interactive workflow are required.
The editor should be called through run_bss_mu_editor() rather than by instantiating the widget directly. The runner manages the Qt application and returns:
edited_emgfile, with manually edited MUPULSES and, when the W command is used, updated IPTS;
mus_to_delete, a list of MU indexes marked for deletion in the UI.
The editor does not delete marked MUs and does not finalise derived fields such as ACCURACY/SIL or BINARY_MUS_FIRING. These steps should be performed in the calling script after the UI closes.
Prepare the preprocessed, extended, centered, and whitened signal in the
calling script, then open the cleaning UI.
Import needed modules
import numpy as np
import pandas as pd
import openhdemg.library as emg
from openhdemg.ui import run_bss_mu_editor
Load openhdemg module decomposed using EMGDecomposer
emgfile = emg.askloadmodule()
Extract needed decomposition parameters
decomp_params = emgfile["DECOMPOSITION_PARAMETERS"]
extension_factor = int(decomp_params["extension_factor"])
eigenvalue_percentile = float(decomp_params["eigenvalue_percentile"])
Rebuild the same signal used by the decomposition preprocessing pipeline.
The prepared signal will only be used for cleaning and will not update the
original emgfile.
working_emgfile = emgfile
bandpass_params = decomp_params["bandpass_filtering"]
if bandpass_params["enabled"] is True:
working_emgfile = emg.filter_rawemg(
emgfile=working_emgfile,
order=bandpass_params["order"],
lowcut=bandpass_params["lowcut"],
highcut=bandpass_params["highcut"],
)
Extract the EMG signal and store it as an array with channels as rows,
samples as columns.
emg_sig = working_emgfile["RAW_SIGNAL"].to_numpy(dtype=np.float64).T
Apply power-line harmonics removal when it was used during decomposition.
powerline_params = decomp_params["powerline_harmonics"]
if powerline_params["enabled"] is True:
emg_sig = emg.remove_powerline_harmonics(
sig=emg_sig,
fsamp=emgfile["FSAMP"],
notch_freq=powerline_params["notch_freq"],
notch_width=powerline_params["notch_width"],
)
Remove bad channels when this was used during decomposition.
if decomp_params["exclude_bad_channels"] is True:
good_channels = emgfile.get("GOOD_CHANNELS", None)
if good_channels is not None:
good_idx = sorted(
int(ch) for ch, ok in good_channels.items() if ok
)
emg_sig = emg_sig[good_idx, :]
Prepare the extended, centered, and whitened signal.
e_sig = emg.extend_emg_signal(
sig=emg_sig,
ext_fact=extension_factor,
)
e_sig = e_sig - np.mean(e_sig, axis=1, keepdims=True)
e_w_sig = emg.svd_whitening(
e_sig=e_sig,
eigenvalue_percentile=eigenvalue_percentile,
)
Align with the original sample indexes used by MUPULSES/IPTS
e_w_sig = np.pad(
e_w_sig,
((0, 0), (extension_factor, 0)),
mode="constant",
constant_values=0,
)
Start the editor
edited_emgfile, mus_to_delete = run_bss_mu_editor(
emgfile=emgfile,
e_w_sig=e_w_sig,
refsig_channel=0,
)

Once editing is completed, delete MUs marked in the UI
if mus_to_delete:
edited_emgfile = emg.delete_mus(
edited_emgfile,
munumber=mus_to_delete,
if_single_mu="remove",
)
Also check for duplicate MUs that might have emerged from manual editing, if
duplicate removal is enabled in the decomposition metadata.
duplicate_params = decomp_params["duplicate_removal"]
if duplicate_params["enabled"] is True:
edited_emgfile = emg.remove_duplicates_within(
emgfile=edited_emgfile,
correlation_max_lag=duplicate_params["correlation_max_lag"],
peak_window_half_width=duplicate_params["peak_window_half_width"],
duplicate_threshold=duplicate_params["duplicate_threshold"],
which=duplicate_params["which"],
)
Recalculate SIL for all remaining MUs.
sil_values = []
for mu in range(edited_emgfile["NUMBER_OF_MUS"]):
sil = emg.compute_sil(
ipts=edited_emgfile["IPTS"][mu],
mupulses=edited_emgfile["MUPULSES"][mu],
compute_on_peaks_only=True,
)
sil_values.append(sil)
edited_emgfile["ACCURACY"] = pd.DataFrame(sil_values)
Recalculate binary MU firings.
edited_emgfile["BINARY_MUS_FIRING"] = emg.create_binary_firings(
emg_length=edited_emgfile["EMG_LENGTH"],
number_of_mus=edited_emgfile["NUMBER_OF_MUS"],
mupulses=edited_emgfile["MUPULSES"],
)
Standardise emgfile dtypes and save the emgfile.
edited_emgfile = emg.standardise_emgfile_dtypes(emgfile=edited_emgfile)
emg.asksavemodule(emgfile=edited_emgfile)
Advanced Manual Editing in the Software
The output of motor unit decomposition should never be accepted blindly. Before a decomposed file is used for analysis, the operator should carefully inspect the decomposition outcome and decide which motor units should be accepted, rejected, or manually edited.
This decision should be based on multiple checks, including physiological plausibility, discharge-pattern regularity, accuracy scores, visual inspection of the IPTS/source signal, MU firing behaviour, and consistency with the experimental task. Automatic metrics are extremely useful, but they cannot replace expert supervision.
For this reason, each decomposition outcome should undergo manual revision. When needed, the automatic result should be manually edited to maximise the accuracy and reliability of the subsequent analyses.
However, serious manual editing requires a dedicated infrastructure: interactive visualisation, fast navigation across motor units, editing tools for discharge times, cleaning utilities and quality-control panels. The lightweight library editor is useful for basic edits, but the most advanced cleaning workflow is provided by the openhdemg software.
The software can be downloaded here.
A dedicated tutorial for the cleaning workflow will be added soon.

More Questions?
If you need additional information, read the answers or ask a question in the openhdemg discussion section. If you are not familiar with GitHub discussions, please read this post.