Skip to content

Note

Click here to download the full example code

Surface Motor Unit Action Potentials

After having created the muscle model, we can simulate the surface EMG by creating a surface EMG model.

First step is to create MUAPs from the muscle model.

from pathlib import Path

import joblib
import matplotlib.pyplot as plt
import numpy as np
import quantities as pq

from myogen import load_profile, simulator
from myogen.utils.neo import signal_to_grid

plt.style.use("fivethirtyeight")

Define Parameters

The surface EMG is created using the SurfaceEMG object.

The SurfaceEMG object takes the following parameters:

  • muscle_model: Muscle model
  • sampling_frequency: Sampling frequency
  • electrode_grid_dimensions: Electrode grid dimensions
  • inter_electrode_distance: Inter-electrode distance
  • fat_thickness: Fat thickness
  • skin_thickness: Skin thickness
# Define simulation parameters
sampling_frequency = 2048.0 * pq.Hz

Load Muscle Model

Load muscle model from previous example

save_path = Path("./results")
muscle: simulator.Muscle = joblib.load(save_path / "muscle_model.pkl")

Create Surface EMG Model

The SurfaceEMG object is initialized with the muscle model, the SurfaceElectrodeArray, and the simulation parameters.

Note

For simplicity, we only simulate the first motor unit. This can be changed by modifying the MUs_to_simulate parameter.

This is to simulate the surface EMG from two different directions.

electrode_array_monopolar = simulator.SurfaceElectrodeArray(
    num_rows=5,
    num_cols=5,
    inter_electrode_distance__mm=5 * pq.mm,
    electrode_radius__mm=5 * pq.mm,
    differentiation_mode="monopolar",
    bending_radius__mm=muscle.radius__mm + muscle.skin_thickness__mm + muscle.fat_thickness__mm,
)

# The ``fdi_semg`` card sets the volume conductor and action-potential settings it was validated with (fibre
# dispersion, Rosenfalck lambda 0.63 fitted to Botelho 2019, 200 mm IAP kernel); the electrodes, the sampling rate
# and the units to simulate are yours.
card = load_profile()  # fdi_semg
surface_emg = card.surface_emg(
    muscle,
    [electrode_array_monopolar],
    sampling_frequency__Hz=sampling_frequency,
    sampling_points_in_t_and_z_domains=256,
    sampling_points_in_theta_domain=32,
    MUs_to_simulate=[0, 1, 2, 3],  # Simulate MUs 0, 1, 2, and 3
)

Simulate MUAPs

To generate the MUAPs, we need to run the simulate_muaps method of the SurfaceEMG object.

# Run simulation with progress output
muaps = surface_emg.simulate_muaps()


print("MUAP simulation completed!")
first_signal = muaps.groups[0].segments[0].analogsignals[0]
grid_shape = first_signal.annotations["grid_shape"]
print(f"Generated MUAPs shape: {first_signal.shape} (stored as 2D for NWB compatibility)")
print(f"  - {len(muaps.groups[0].segments)} total motor units in muscle")
print(f"  - {len(muscle.resulting_number_of_innervated_fibers)} MUs in muscle model")
print(f"  - {grid_shape[0]} rows × {grid_shape[1]} columns electrode grid")
print(f"  - {first_signal.shape[0]} time samples")

# Check fiber counts for the MUs we're simulating
print("\nFiber counts for simulated MUs:")
for mu_idx in range(95, min(100, len(muscle.resulting_number_of_innervated_fibers))):
    n_fibers = muscle.resulting_number_of_innervated_fibers[mu_idx]
    print(f"  MU {mu_idx}: {n_fibers} fibers")

# Save results
joblib.dump(surface_emg, save_path / "surface_emg.pkl")

Out:

/home/runner/work/MyoGen/MyoGen/myogen/simulator/core/emg/surface/volume_conductor.py:41: RuntimeWarning: divide by zero encountered in log
  result = np.where(In_s > 0, np.log(In_s) + np.abs(x), -np.inf)
/home/runner/work/MyoGen/MyoGen/myogen/simulator/core/emg/surface/volume_conductor.py:234: RuntimeWarning: invalid value encountered in subtract
  A_mat[..., 1, 0] = sig_bone * np.exp(log_In_tilde_a - log_In_a)
/home/runner/work/MyoGen/MyoGen/myogen/simulator/core/emg/surface/volume_conductor.py:234: RuntimeWarning: invalid value encountered in multiply
  A_mat[..., 1, 0] = sig_bone * np.exp(log_In_tilde_a - log_In_a)
/home/runner/work/MyoGen/MyoGen/myogen/simulator/core/emg/surface/volume_conductor.py:236: RuntimeWarning: invalid value encountered in subtract
  A_mat[..., 1, 2] = sig_muscle_geo * np.exp(log_Kn_tilde_am - log_Kn_am)
/home/runner/work/MyoGen/MyoGen/myogen/simulator/core/emg/surface/volume_conductor.py:41: RuntimeWarning: divide by zero encountered in log
  result = np.where(In_s > 0, np.log(In_s) + np.abs(x), -np.inf)
/home/runner/work/MyoGen/MyoGen/myogen/simulator/core/emg/surface/volume_conductor.py:234: RuntimeWarning: invalid value encountered in subtract
  A_mat[..., 1, 0] = sig_bone * np.exp(log_In_tilde_a - log_In_a)
/home/runner/work/MyoGen/MyoGen/myogen/simulator/core/emg/surface/volume_conductor.py:234: RuntimeWarning: invalid value encountered in multiply
  A_mat[..., 1, 0] = sig_bone * np.exp(log_In_tilde_a - log_In_a)
/home/runner/work/MyoGen/MyoGen/myogen/simulator/core/emg/surface/volume_conductor.py:236: RuntimeWarning: invalid value encountered in subtract
  A_mat[..., 1, 2] = sig_muscle_geo * np.exp(log_Kn_tilde_am - log_Kn_am)
MUAP simulation completed!
Generated MUAPs shape: (256, 25) (stored as 2D for NWB compatibility)
  - 120 total motor units in muscle
  - 120 MUs in muscle model
  - 5 rows × 5 columns electrode grid
  - 256 time samples

Fiber counts for simulated MUs:
  MU 95: 641 fibers
  MU 96: 650 fibers
  MU 97: 605 fibers
  MU 98: 686 fibers
  MU 99: 713 fibers

['results/surface_emg.pkl']

Plot MUAPs

Plot a single MUAP across the electrode grid. Each subplot shows the MUAP waveform at one electrode position.

# Select which MUAP to plot
for muap_index in range(len(muaps.groups[0].segments)):
    if np.mean(muaps.groups[0].segments[muap_index].analogsignals[0]) == 0:
        continue
    # Extract MUAP data from the Block object
    muap_signal = muaps.groups[0].segments[muap_index].analogsignals[0]
    # Convert to 3D grid format (time, rows, cols)
    muap_data_mV = signal_to_grid(muap_signal)
    muap_data_uV = signal_to_grid(muap_signal.rescale(pq.uV))  # Convert to uV for plotting

    # Print amplitude diagnostics
    print(f"\nMUAP {muap_index} amplitude range:")
    print(f"\tMin: {np.min(muap_data_mV):.6f} mV ({np.min(muap_data_uV):.2f} uV)")
    print(f"\tMax: {np.max(muap_data_mV):.6f} mV ({np.max(muap_data_uV):.2f} uV)")
    print(f"\tPeak-to-peak: {np.ptp(muap_data_mV):.6f} mV ({np.ptp(muap_data_uV):.2f} uV)")

    # Get grid dimensions from annotations
    n_rows, n_cols = muap_signal.annotations["grid_shape"]

    # Create subplot grid matching electrode layout
    fig, axs = plt.subplots(
        n_rows, n_cols, figsize=(n_cols * 2, n_rows * 2), sharex=True, sharey=True
    )
    fig.suptitle(f"MUAP {muap_index} ({np.ptp(muap_data_mV):.1f} mV peak-to-peak)")

    # get 100 ms around the center for better visualization (captures full MUAP waveform)
    time_vector = muap_signal.times.rescale(pq.ms).magnitude

    # Plot each electrode's waveform
    for row in range(n_rows):
        for col in range(n_cols):
            axs[row, col].plot(
                time_vector,
                muap_data_mV[:, row, col],
                linewidth=1.5,
            )
            axs[row, col].grid(False)
            if row == n_rows - 1 and col == n_cols // 2:
                axs[row, col].set_xticks([0, 7.5, 15])
                axs[row, col].set_xlabel("Time (ms)")
            if col == 0 and row == n_rows // 2:
                axs[row, col].set_ylabel("Amplitude (mV)")

    plt.tight_layout()
    plt.show()
    MUAP 0 (0.0 mV peak-to-peak)MUAP 1 (0.0 mV peak-to-peak)MUAP 2 (0.0 mV peak-to-peak)MUAP 3 (0.0 mV peak-to-peak)

Out:

MUAP 0 amplitude range:
        Min: -0.009340 mV (-9.34 uV)
        Max: 0.014180 mV (14.18 uV)
        Peak-to-peak: 0.023519 mV (23.52 uV)

MUAP 1 amplitude range:
        Min: -0.006783 mV (-6.78 uV)
        Max: 0.010293 mV (10.29 uV)
        Peak-to-peak: 0.017076 mV (17.08 uV)

MUAP 2 amplitude range:
        Min: -0.015327 mV (-15.33 uV)
        Max: 0.020130 mV (20.13 uV)
        Peak-to-peak: 0.035457 mV (35.46 uV)

MUAP 3 amplitude range:
        Min: -0.022737 mV (-22.74 uV)
        Max: 0.025489 mV (25.49 uV)
        Peak-to-peak: 0.048225 mV (48.23 uV)

Total running time of the script: ( 1 minutes 5.760 seconds)

Download Python source code: 05_simulate_surface_muaps.py

Gallery generated by mkdocs-gallery