Skip to content

props

Functions to compute motor unit properties

get_discharge_rate

get_discharge_rate(spike_train: ndarray, timestamps: Union[list, ndarray]) -> np.ndarray

Compute the discharge rate of motor units.

Parameters:

Name Type Description Default
spike_train ndarray

Binary spike train matrix of shape (n, m), where n is the number of time points and m is the number of motor units.

required
timestamps Union[list, ndarray]

Array of timestamps corresponding to the spike train.

required

Returns:

Type Description
ndarray

np.ndarray: Array of discharge rates for each motor unit.

Source code in motor_unit_toolbox/props.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def get_discharge_rate(
        spike_train: np.ndarray,
        timestamps: Union[list, np.ndarray]
        ) -> np.ndarray:
    """Compute the discharge rate of motor units.

    Args:
        spike_train (np.ndarray): Binary spike train matrix of shape (n, m),
            where n is the number of time points and m is the number of motor
            units.
        timestamps (Union[list, np.ndarray]): Array of timestamps corresponding
            to the spike train.

    Returns:
        np.ndarray: Array of discharge rates for each motor unit.
    """

    # Get number of motor units and initialise dr
    spike_train = _check_mu_format(spike_train.astype(bool))
    units = spike_train.shape[-1]
    dr = np.zeros(units)

    for unit in range(units):
        # Compute the total number of firings
        n_spikes = np.sum(spike_train[:, unit].astype(int))

        if n_spikes == 0:
            continue

        # Get firing times
        times_spikes = timestamps[spike_train[:, unit]]

        # Get total active period
        total_period = times_spikes[-1] - times_spikes[0]

        if total_period == 0:
            continue

        # Calculate interspike interval (ISI)
        isi = np.diff(times_spikes)

        # Find silent periods (i.e. where the ISI is larger than the minimum
        # motor unit discharge rate). This is 4 Hz according to "Negro F (2016)
        # Multi-channel intramuscular and surface EMG decomposition by
        # convolutive blind source separation."
        silent_period = np.sum(isi[isi > 0.25])

        # Calculate the actual active period of the motor unit
        active_period = total_period - silent_period

        # Return mean discharge rate
        dr[unit] = n_spikes / active_period

    return dr

get_number_of_spikes

get_number_of_spikes(spike_train: ndarray) -> np.ndarray

Function to compute the number of spikes.

Parameters:

Name Type Description Default
spike_train ndarray

Binary spike train matrix of shape (n, m), where n is the number of time points and m is the number of motor units.

required

Returns:

Type Description
ndarray

np.ndarray: number of spikes for each motor unit

Source code in motor_unit_toolbox/props.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def get_number_of_spikes(spike_train: np.ndarray) -> np.ndarray:
    """Function to compute the number of spikes.

    Args:
        spike_train (np.ndarray): Binary spike train matrix of shape (n, m),
            where n is the number of time points and m is the number of motor
            units.

    Returns:
        np.ndarray: number of spikes for each motor unit
    """

    # Compute the number of spikes
    n_spikes = np.sum(spike_train.astype(int), axis=0)

    return n_spikes

get_inst_discharge_rate

get_inst_discharge_rate(spike_train: ndarray, fs: Optional[int] = 2048) -> np.ndarray

Compute the instantaneous discharge rate of motor units.

Parameters:

Name Type Description Default
spike_train ndarray

Binary spike train matrix of shape (n, m), where n is the number of time points and m is the number of motor units.

required
fs Optional[int]

Sampling frequency in Hz. Defaults to 2048.

2048

Returns:

Type Description
ndarray

np.ndarray: Array of instantaneous discharge rates for each motor unit.

Source code in motor_unit_toolbox/props.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def get_inst_discharge_rate(
        spike_train: np.ndarray,
        fs: Optional[int] = 2048
        ) -> np.ndarray:
    """Compute the instantaneous discharge rate of motor units.

    Args:
        spike_train (np.ndarray): Binary spike train matrix of shape (n, m),
            where n is the number of time points and m is the number of motor
            units.
        fs (Optional[int], optional): Sampling frequency in Hz. Defaults to 2048.

    Returns:
        np.ndarray: Array of instantaneous discharge rates for each motor unit.
    """

    # Get number of motor units and initialise ints_DR
    spike_train = _check_mu_format(spike_train.astype(bool))
    units = spike_train.shape[-1]
    inst_dr = np.zeros(spike_train.shape)

    # Define hanning window
    dur = 1  # (s) for the moving average
    hann_win = np.hanning(np.round(dur * fs))

    for unit in range(units):
        # Convolve the hanning window and the binary spikes
        inst_dr[:, unit] = np.convolve(
            spike_train[:, unit], hann_win, mode='same'
            ) * 2

    return inst_dr

get_coefficient_of_variation

get_coefficient_of_variation(spike_train: ndarray, timestamps: Union[list, ndarray], discard_isi: Optional[float] = 0.25) -> np.ndarray

Calculate the coefficient of variation (CoV) for each motor unit in a spike train.

Parameters:

Name Type Description Default
spike_train ndarray

Binary spike train matrix of shape (n, m), where n is the number of time points and m is the number of motor units.

required
timestamps Union[list, ndarray]

Array of timestamps corresponding to each time point in the spike train.

required
discard_isi Optional[float], defaults to 0.25

Interspike intervals greater than value in s are discarded as considered silent period.

0.25

Returns:

Type Description
ndarray

np.ndarray: Array of CoV values for each motor unit between 0 and 1.

Source code in motor_unit_toolbox/props.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
def get_coefficient_of_variation(
        spike_train: np.ndarray,
        timestamps: Union[list, np.ndarray],
        discard_isi: Optional[float] = 0.25,
        ) -> np.ndarray:
    """
    Calculate the coefficient of variation (CoV) for each motor unit in a spike
    train.

    Parameters:
        spike_train (np.ndarray): Binary spike train matrix of shape (n, m),
            where n is the number of time points and m is the number of motor
            units.
        timestamps (Union[list, np.ndarray]): Array of timestamps
            corresponding to each time point in the spike train.
        discard_isi (Optional[float], defaults to 0.25): Interspike intervals
            greater than value in s are discarded as considered silent period.

    Returns:
        np.ndarray: Array of CoV values for each motor unit between 0 and 1.

    """
    # Function implementation
    spike_train = _check_mu_format(spike_train.astype(bool))
    units = spike_train.shape[-1]
    cov = np.zeros(units)
    cov[:] = np.nan

    for unit in range(units):
        if not np.any(spike_train[:, unit]):
            continue

        times_spikes = timestamps[spike_train[:, unit]]
        isi = np.diff(times_spikes)
        if discard_isi is not None:
            isi = isi[isi < discard_isi]
        cov[unit] = np.std(isi) / np.mean(isi)

    return cov

get_pulse_to_noise_ratio

get_pulse_to_noise_ratio(spike_train: ndarray, ipts: ndarray, ext_fact: int = 8) -> np.ndarray

Compute the pulse-to-noise ratio (PNR) for each motor unit.

Parameters:

Name Type Description Default
spike_train ndarray

Binary spike train matrix of shape (n, m), where n is the number of time points and m is the number of motor units.

required
ipts ndarray

Innervated pulse trains (IPTs) with shape (n, m), where n is the number of time points and m is the number of motor units.

required
ext_fact int

Extension factor to discard initial spikes. Defaults to 8.

8

Returns:

Type Description
ndarray

np.ndarray: Array of PNR values for each motor unit.

Notes: - The PNR is calculated as 20 * log10(spikes_mean / baseline_mean), where spikes_mean is the mean of the IPTs corresponding to the spikes and baseline_mean is the mean of the IPTs corresponding to the baseline. - The baseline is defined as the IPTs with amplitude lower than the lowest spike. - IPTs greater than the extension factor are considered for both spikes and baseline.

Source code in motor_unit_toolbox/props.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def get_pulse_to_noise_ratio(
    spike_train: np.ndarray,
    ipts: np.ndarray,
    ext_fact: int = 8
    ) -> np.ndarray:
    """Compute the pulse-to-noise ratio (PNR) for each motor unit.

    Parameters:
        spike_train (np.ndarray): Binary spike train matrix of shape (n, m),
            where n is the number of time points and m is the number of motor
            units.
        ipts (np.ndarray): Innervated pulse trains (IPTs) with shape (n, m),
            where n is the number of time points and m is the number of motor
            units.
        ext_fact (int, optional): Extension factor to discard initial spikes.
            Defaults to 8.

    Returns:
        np.ndarray: Array of PNR values for each motor unit.

    Notes:
    - The PNR is calculated as 20 * log10(spikes_mean / baseline_mean),
      where spikes_mean is the mean of the IPTs corresponding to the spikes
      and baseline_mean is the mean of the IPTs corresponding to the
      baseline.
    - The baseline is defined as the IPTs with amplitude lower than the
      lowest spike.
    - IPTs greater than the extension factor are considered for both spikes
      and baseline.
    """

    # Get number of motor units and initialise PNR
    spike_train = _check_mu_format(spike_train.astype(bool))
    ipts = _check_mu_format(ipts)
    units = spike_train.shape[-1]
    pnr = np.zeros(units)
    pnr[:] = np.nan

    # Square IPTs
    ipts2 = ipts ** 2

    for unit in range(units):
        # Get the spikes and baseline indexes, discarding the extension factor
        spikes_idx = np.nonzero(spike_train[:, unit])[0]
        spikes_idx = spikes_idx[np.greater_equal(spikes_idx, ext_fact + 1)]

        if not np.any(spikes_idx):
            continue

        spikes = ipts2[spikes_idx, unit]
        min_spikes_amp = np.amin(spikes)

        # Get baseline peaks with amplitude lower than the lowest spike
        baseline_peaks_idx, _ = signal.find_peaks(
            ipts2[:, unit], height=(0, min_spikes_amp)
            )
        if not np.any(baseline_peaks_idx):
            baseline_peaks_idx = np.nonzero(
                np.logical_not(spike_train[:, unit].astype(bool))
                )[0]
        baseline_peaks_idx = baseline_peaks_idx[
            np.greater_equal(baseline_peaks_idx, ext_fact + 1)
            ]
        baseline = ipts2[baseline_peaks_idx, unit]

        if len(spikes) == 0:
            continue

        # Compute spikes and baseline mean values
        spikes_mean = np.mean(spikes)
        baseline_mean = np.mean(baseline)

        # Compute PNR
        pnr[unit] = 20 * np.log10(spikes_mean / baseline_mean)

    return pnr

get_silhouette_measure

get_silhouette_measure(spike_train: ndarray, ipts: ndarray, ext_fact: int = 8) -> np.ndarray

Compute the silhouette measure for each motor unit.

Parameters:

Name Type Description Default
spike_train ndarray

Binary spike train matrix of shape (n, m), where n is the number of time points and m is the number of motor units.

required
ipts ndarray

Innervation pulse trains (IPTs) with shape (n, m), where n is the number of time points and m is the number of motor units.

required
ext_fact int

Extension factor to discard initial spikes. Defaults to 8.

8

Returns:

Type Description
ndarray

np.ndarray: Array of silhouette measures for each motor unit.

Notes
  • The silhouette measure is a measure of how well-separated the spikes and baseline are in terms of their IPTs.
  • The silhouette measure is calculated as (dist_sum_baseline - dist_sum_spikes) / max_dist, where dist_sum_baseline is the sum of squared distances between each baseline IPT and the mean baseline IPT, dist_sum_spikes is the sum of squared distances between each spike IPT and the mean spike IPT, and max_dist is the maximum of dist_sum_baseline and dist_sum_spikes.
Source code in motor_unit_toolbox/props.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
def get_silhouette_measure(
    spike_train: np.ndarray,
    ipts: np.ndarray,
    ext_fact: int = 8
) -> np.ndarray:
    """Compute the silhouette measure for each motor unit.

    Args:
        spike_train (np.ndarray): Binary spike train matrix of shape (n, m),
            where n is the number of time points and m is the number of motor
            units.
        ipts (np.ndarray): Innervation pulse trains (IPTs) with shape (n, m),
            where n is the number of time points and m is the number of motor
            units.
        ext_fact (int, optional): Extension factor to discard initial spikes.
            Defaults to 8.

    Returns:
        np.ndarray: Array of silhouette measures for each motor unit.

    Notes:
        - The silhouette measure is a measure of how well-separated the spikes
          and baseline are in terms of their IPTs.
        - The silhouette measure is calculated as
          (dist_sum_baseline - dist_sum_spikes) / max_dist, where
          dist_sum_baseline is the sum of squared distances between each
          baseline IPT and the mean baseline IPT, dist_sum_spikes is the sum
          of squared distances between each spike IPT and the mean spike IPT,
          and max_dist is the maximum of dist_sum_baseline and dist_sum_spikes.
    """

    # Get number of motor units and initialise sil
    spike_train = _check_mu_format(spike_train.astype(bool))
    ipts = _check_mu_format(ipts)
    units = spike_train.shape[-1]
    sil = np.zeros(units)
    sil[:] = np.nan

    # Square IPTs
    ipts2 = ipts ** 2

    for unit in range(units):
        # Get the spikes and baseline indexes, discarding the extension factor
        spikes_idx = np.nonzero(spike_train[:, unit])[0]
        spikes_idx = spikes_idx[np.greater_equal(spikes_idx, ext_fact + 1)]

        if not np.any(spikes_idx):
            continue

        spikes_amp = ipts2[spikes_idx, unit]
        min_spikes_amp = np.amin(spikes_amp)

        # Get baseline peaks with amplitude lower than the lowest spike
        baseline_peaks_idx, _ = signal.find_peaks(
            ipts2[:, unit], height=(0, min_spikes_amp)
            )
        if not np.any(baseline_peaks_idx):
            baseline_peaks_idx = np.nonzero(
                np.logical_not(spike_train[:, unit].astype(bool))
                )[0]
        baseline_peaks_idx = baseline_peaks_idx[
            np.greater_equal(baseline_peaks_idx, ext_fact + 1)
            ]
        baseline_amp = ipts2[baseline_peaks_idx, unit]

        # Compute spikes and baseline mean values
        spikes_mean = np.mean(spikes_amp)
        baseline_mean = np.mean(baseline_amp)

        # Compute distances
        dist_sum_spikes = np.sum(np.power((spikes_amp - spikes_mean), 2))
        dist_sum_baseline = np.sum(np.power((spikes_amp - baseline_mean), 2))

        # Compute sil
        max_dist = np.amax([dist_sum_spikes, dist_sum_baseline])
        if max_dist == 0:
            sil[unit] = 0
        else:
            sil[unit] = (dist_sum_baseline - dist_sum_spikes) / max_dist

    return sil

get_spike_baseline_amp

get_spike_baseline_amp(spike_train: ndarray, ipts: ndarray, ext_fact: int = 8) -> Tuple[np.ndarray, np.ndarray]

Calculate the median amplitude of spikes and baseline for each motor unit.

Parameters:

Name Type Description Default
spike_train ndarray

Binary spike train matrix of shape (n, m), where n is the number of time points and m is the number of motor units.

required
ipts ndarray

Innervated pulse trains (IPTs) with shape (n, m), where n is the number of time points and m is the number of motor units.

required
ext_fact int

Extension factor to discard initial spikes. Defaults to 8.

8

Returns:

Type Description
Tuple[ndarray, ndarray]

Tuple[np.ndarray, np.ndarray]: Tuple containing two arrays: - spikes_amp: Array of median amplitudes of spikes for each motor unit. - base_amp: Array of median amplitudes of baseline for each motor unit.

Source code in motor_unit_toolbox/props.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
def get_spike_baseline_amp(
    spike_train: np.ndarray,
    ipts: np.ndarray,
    ext_fact: int = 8
) -> Tuple[np.ndarray, np.ndarray]:
    """
    Calculate the median amplitude of spikes and baseline for each motor unit.

    Parameters:
        spike_train (np.ndarray): Binary spike train matrix of shape (n, m),
            where n is the number of time points and m is the number of motor
            units.
        ipts (np.ndarray): Innervated pulse trains (IPTs) with shape (n, m),
            where n is the number of time points and m is the number of motor
            units.
        ext_fact (int, optional): Extension factor to discard initial spikes.
            Defaults to 8.

    Returns:
        Tuple[np.ndarray, np.ndarray]: Tuple containing two arrays:
            - spikes_amp: Array of median amplitudes of spikes for each motor unit.
            - base_amp: Array of median amplitudes of baseline for each motor unit.
    """
    # Get number of motor units and initialise output variables
    spike_train = _check_mu_format(spike_train.astype(bool))
    ipts = _check_mu_format(ipts)
    units = spike_train.shape[-1]
    spikes_amp = np.zeros(units)
    spikes_amp[:] = np.nan
    base_amp = np.zeros(units)
    base_amp[:] = np.nan

    for unit in range(units):
        # Get the spikes indexes discarding the extension factor
        spikes_idx = np.nonzero(spike_train[:, unit])[0]
        spikes_idx = spikes_idx[np.greater_equal(spikes_idx, ext_fact + 1)]

        if np.any(spikes_idx):
            spikes_amp[unit] = np.median(ipts[spikes_idx, unit])

        # Get the baseline indexes discarding the extension factor
        baseline_peaks_idx = np.nonzero(
            np.logical_not(spike_train[:, unit].astype(bool))
            )[0]
        baseline_peaks_idx = baseline_peaks_idx[
            np.greater_equal(baseline_peaks_idx, ext_fact + 1)
            ]

        if np.any(baseline_peaks_idx):
            base_amp[unit] = np.median(ipts[baseline_peaks_idx, unit])

    return spikes_amp, base_amp

find_reliable_units

find_reliable_units(dr: ndarray, cov: ndarray, sil: ndarray, pnr: ndarray, dr_low_thr: Optional[float] = 3, dr_upp_thr: Optional[float] = 40, cov_thr: Optional[float] = 40, sil_thr: Optional[float] = 0.9, pnr_thr: Optional[float] = 30) -> np.ndarray

Find reliable motor units based on specified thresholds.

Parameters:

Name Type Description Default
dr ndarray

Array of discharge rates for each motor unit.

required
cov ndarray

Array of coefficient of variation for each motor unit.

required
sil ndarray

Array of silhouette measures for each motor unit.

required
pnr ndarray

Array of pulse-to-noise ratios for each motor unit.

required
dr_low_thr float

Lower threshold for discharge rate. Defaults to 3.

3
dr_upp_thr float

Upper threshold for discharge rate. Defaults to 40.

40
cov_thr float

Threshold for coefficient of variation. Defaults to 40.

40
sil_thr float

Threshold for silhouette measure. Defaults to 0.9.

0.9
pnr_thr float

Threshold for pulse-to-noise ratio. Defaults to 30.

30

Returns:

Type Description
ndarray

np.ndarray: Boolean array indicating which motor units are reliable.

Source code in motor_unit_toolbox/props.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def find_reliable_units(
    dr: np.ndarray,
    cov: np.ndarray,
    sil: np.ndarray,
    pnr: np.ndarray,
    dr_low_thr: Optional[float] = 3,
    dr_upp_thr: Optional[float] = 40,
    cov_thr: Optional[float] = 40,
    sil_thr: Optional[float] = 0.9,
    pnr_thr: Optional[float] = 30
) -> np.ndarray:
    """
    Find reliable motor units based on specified thresholds.

    Args:
        dr (np.ndarray): Array of discharge rates for each motor unit.
        cov (np.ndarray): Array of coefficient of variation for each motor unit.
        sil (np.ndarray): Array of silhouette measures for each motor unit.
        pnr (np.ndarray): Array of pulse-to-noise ratios for each motor unit.
        dr_low_thr (float, optional): Lower threshold for discharge rate. Defaults to 3.
        dr_upp_thr (float, optional): Upper threshold for discharge rate. Defaults to 40.
        cov_thr (float, optional): Threshold for coefficient of variation. Defaults to 40.
        sil_thr (float, optional): Threshold for silhouette measure. Defaults to 0.9.
        pnr_thr (float, optional): Threshold for pulse-to-noise ratio. Defaults to 30.

    Returns:
        np.ndarray: Boolean array indicating which motor units are reliable.
    """
    aux = np.vstack((
        dr >= dr_low_thr,
        dr <= dr_upp_thr,
        cov <= cov_thr,
        sil >= sil_thr,
        pnr >= pnr_thr
    ))
    reliable_units = np.all(aux, axis=0)
    return reliable_units

get_muaps

get_muaps(spike_trains: ndarray, emg_ch_array: ndarray, fs: Optional[int] = 2048, win_ms: Optional[int] = 25) -> np.ndarray

Compute the motor unit action potentials (MUAPs) from spike trains and surface electromyography (sEMG) signals.

Parameters:

Name Type Description Default
spike_trains ndarray

Binary spike train matrix of shape (n, m), where n is the number of time points and m is the number of motor units.

required
emg_ch_array ndarray

sEMG signal matrix of shape (r, c, n), where r is the number of rows, c is the number of columns, and n is the number of time points.

required
fs int

Sampling frequency of the sEMG signal. Defaults to 2048 Hz.

2048
win_ms int

Window size in milliseconds. Defaults to 25.

25

Returns:

Type Description
ndarray

np.ndarray: Array of MUAPs with shape (m, r, c, win_samples), where m is the number of motor units, r is the number of rows, c is the number of columns, and win_samples is the number of samples in the window.

Notes
  • The MUAPs are computed by aligning the sEMG signals to the spike occurrences in the spike trains and averaging the aligned signals.
  • The window size is centered around each spike occurrence.
Source code in motor_unit_toolbox/props.py
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
def get_muaps(
        spike_trains: np.ndarray,
        emg_ch_array: np.ndarray,
        fs: Optional[int] = 2048,
        win_ms: Optional[int] = 25
        ) -> np.ndarray:
    """
    Compute the motor unit action potentials (MUAPs) from spike trains and
    surface electromyography (sEMG) signals.

    Args:
        spike_trains (np.ndarray): Binary spike train matrix of shape (n, m),
            where n is the number of time points and m is the number of motor
            units.
        emg_ch_array (np.ndarray): sEMG signal matrix of shape (r, c, n),
            where r is the number of rows, c is the number of columns, and n
            is the number of time points.
        fs (int, optional): Sampling frequency of the sEMG signal. Defaults to
            2048 Hz.
        win_ms (int, optional): Window size in milliseconds. Defaults to 25.

    Returns:
        np.ndarray: Array of MUAPs with shape (m, r, c, win_samples), where m is
            the number of motor units, r is the number of rows, c is the number
            of columns, and win_samples is the number of samples in the window.

    Notes:
        - The MUAPs are computed by aligning the sEMG signals to the spike
          occurrences in the spike trains and averaging the aligned signals.
        - The window size is centered around each spike occurrence.
    """

    # Initialise dimensions
    spike_trains = _check_mu_format(spike_trains.astype(bool))
    rows, cols, samples = emg_ch_array.shape
    half_win = round(win_ms/2/1000*fs)

    # Check spike train dimensions
    if len(spike_trains) > 0:
        if len(spike_trains.shape) == 1:
            spike_trains = np.expand_dims(spike_trains, axis=-1)

        # Initialise units and muaps
        units = spike_trains.shape[1]
        muaps = np.empty((units, rows, cols, half_win * 2))

        for unit in range(units):
            # Get the firings that fit a window around them
            unit_firings = np.nonzero(spike_trains[:, unit])[0]
            unit_firings = unit_firings[
                (unit_firings - half_win >= 0) &
                (unit_firings + half_win <= samples-1)
                ]
            n_unit_firings = len(unit_firings)

            # Initialise muap samples
            muaps_aux = np.empty((n_unit_firings, rows, cols, half_win*2))

            # Get all the muap samples for each unit firing
            for i, firing in enumerate(unit_firings):

                mask = np.arange(half_win * 2) - half_win + firing
                muaps_aux[i] = emg_ch_array[:, :, mask]

            # Compute mean
            muaps[unit] = np.mean(muaps_aux, axis=0)
    else:
        muaps = np.empty((0, rows, cols, half_win*2))

    return muaps

center_muaps

center_muaps(muaps: ndarray) -> np.ndarray

Center the motor unit action potentials (MUAPs) around the peak amplitude.

Parameters:

Name Type Description Default
muaps ndarray

Array of MUAPs with shape (m, r, c, win_samples), where m is the number of motor units, r is the number of rows, c is the number of columns, and win_samples is the number of samples in the window.

required

Returns:

Type Description
ndarray

np.ndarray: Array of centered MUAPs with shape (m, r, c, win_samples).

Notes
  • The MUAPs are centered by shifting the samples so that the peak amplitude is at the center of the window.
Source code in motor_unit_toolbox/props.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
def center_muaps(muaps: np.ndarray) -> np.ndarray:
    """
    Center the motor unit action potentials (MUAPs) around the peak amplitude.

    Parameters:
        muaps (np.ndarray): Array of MUAPs with shape (m, r, c, win_samples),
            where m is the number of motor units, r is the number of rows, c
            is the number of columns, and win_samples is the number of samples
            in the window.

    Returns:
        np.ndarray: Array of centered MUAPs with shape (m, r, c, win_samples).

    Notes:
        - The MUAPs are centered by shifting the samples so that the peak
          amplitude is at the center of the window.
    """

    # Check muaps dimensions
    if len(muaps) > 0:
        # There are units
        if len(muaps.shape) < 4:
            muaps = np.expand_dims(muaps, axis=0)

        # Initialise variables
        units, rows, cols, samples = muaps.shape
        center_sample = samples//2
        centered_muaps = copy(muaps)

        for unit in range(units):
            # Get current muap and peak amplitude channel
            muap = muaps[unit]
            peak_amps = np.nanmax(np.abs(muap), axis=-1)
            ch_row, ch_col = np.unravel_index(np.nanargmax(peak_amps), (rows, cols))

            #  Get the sample at which the amplitude is max
            max_sample = np.nanargmax(np.abs(muap[ch_row, ch_col]))

            # Center muap
            centered_muaps[unit] = np.roll(
                centered_muaps[unit], center_sample-max_sample, axis=-1
                )
    else:
        #  No units
        centered_muaps = copy(muaps)

    return centered_muaps

get_muap_waveform_length

get_muap_waveform_length(muaps: ndarray, sel_chs_by: Optional[Literal['iqr', 'iqr_ptp', 'max_amp', 'ptp']] = 'iqr') -> np.ndarray

Compute the waveform length of motor unit action potentials (MUAPs).

Parameters:

Name Type Description Default
muaps ndarray

Array of MUAPs with shape (m, r, c, win_samples), where m is the number of motor units, r is the number of rows, c is the number of columns, and win_samples is the number of samples in the window.

required
sel_chs_by str

Method for selecting channels: "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel. Defaults to "iqr".

'iqr'

Returns:

Type Description
ndarray

np.ndarray: Array of waveform lengths with shape (m, r, c), where m is the number of motor units, r is the number of rows, and c is the number of columns.

Notes
  • The waveform length is computed as the sum of absolute differences between consecutive samples of each MUAP.
  • If sel_chs_by is specified, the waveform length is computed only for the selected channels based on the specified method.
Source code in motor_unit_toolbox/props.py
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
def get_muap_waveform_length(
    muaps: np.ndarray,
    sel_chs_by: Optional[Literal["iqr", "iqr_ptp", "max_amp", "ptp"]] = "iqr"
) -> np.ndarray:
    """
    Compute the waveform length of motor unit action potentials (MUAPs).

    Parameters:
        muaps (np.ndarray): Array of MUAPs with shape (m, r, c, win_samples),
            where m is the number of motor units, r is the number of rows, c
            is the number of columns, and win_samples is the number of samples
            in the window.
        sel_chs_by (str, optional): Method for selecting channels:
            "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel.
            Defaults to "iqr".

    Returns:
        np.ndarray: Array of waveform lengths with shape (m, r, c), where m is
            the number of motor units, r is the number of rows, and c is the
            number of columns.

    Notes:
        - The waveform length is computed as the sum of absolute differences
          between consecutive samples of each MUAP.
        - If sel_chs_by is specified, the waveform length is computed only for
          the selected channels based on the specified method.
    """
    # Check muaps size
    if len(muaps.shape) < 4:
        muaps = np.expand_dims(muaps, axis=0)
    units = muaps.shape[0]

    if sel_chs_by is None:
        # Compute muap length for each muap channel
        wl = np.sum(np.abs(np.diff(muaps, axis=-1)), axis=-1)
    else:
        # Build channel selection
        sel_chs_mask_units = np.zeros(muaps.shape[0:3]).astype(bool)
        sel_chs_fun = {
            "iqr": get_highest_iqr_ch,
            "iqr_ptp": get_highest_iqr_ptp_ch,
            "max_amp": get_highest_amp_ch,
            "ptp": get_highest_ptp_ch
        }

        # Apply channel selection
        for unit in range(units):
            sel_chs_mask_units[unit] = sel_chs_fun[sel_chs_by](muaps[unit])
        sel_chs_mask = np.all(sel_chs_mask_units, axis=0)
        if not np.any(sel_chs_mask):
            sel_chs_mask = np.ones_like(sel_chs_mask).astype(bool)

        # Initialise muap length
        wl = np.empty(muaps.shape[0:3])
        wl[:] = np.nan

        # Compute muap length
        for unit in range(units):
            muap_sel_chs = muaps[unit][sel_chs_mask]
            wl[unit][sel_chs_mask] = np.sum(np.abs(
                np.diff(muap_sel_chs, axis=-1)
                ), axis=-1)

    return wl

get_muap_energy

get_muap_energy(muaps: ndarray, sel_chs_by: Optional[Literal['iqr', 'iqr_ptp', 'max_amp', 'ptp']] = 'iqr') -> np.ndarray

Compute the energy of motor unit action potentials (MUAPs).

Parameters:

Name Type Description Default
muaps ndarray

Array of MUAPs with shape (m, r, c, win_samples), where m is the number of motor units, r is the number of rows, c is the number of columns, and win_samples is the number of samples in the window.

required
sel_chs_by str

Method for selecting channels: "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel. Defaults to "iqr".

'iqr'

Returns:

Type Description
ndarray

np.ndarray: Array of energy values with shape (m, r, c), where m is the number of motor units, r is the number of rows, and c is the number of columns.

Notes
  • The energy is computed as the sum of squared values of each MUAP.
  • If sel_chs_by is specified, the energy is computed only for the selected channels based on the specified method.
Source code in motor_unit_toolbox/props.py
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
def get_muap_energy(
    muaps: np.ndarray,
    sel_chs_by: Optional[Literal["iqr", "iqr_ptp", "max_amp", "ptp"]] = "iqr"
) -> np.ndarray:
    """
    Compute the energy of motor unit action potentials (MUAPs).

    Args:
        muaps (np.ndarray): Array of MUAPs with shape (m, r, c, win_samples),
            where m is the number of motor units, r is the number of rows, c
            is the number of columns, and win_samples is the number of samples
            in the window.
        sel_chs_by (str, optional): Method for selecting channels:
            "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel.
            Defaults to "iqr".

    Returns:
        np.ndarray: Array of energy values with shape (m, r, c), where m is
            the number of motor units, r is the number of rows, and c is the
            number of columns.

    Notes:
        - The energy is computed as the sum of squared values of each MUAP.
        - If sel_chs_by is specified, the energy is computed only for the
          selected channels based on the specified method.
    """
    # Check muaps size
    if len(muaps.shape) < 4:
        muaps = np.expand_dims(muaps, axis=0)
    units = muaps.shape[0]

    if sel_chs_by is None:
        # Compute muap energy for each muap channel
        energy = np.sum(np.power(muaps, 2), axis=-1)
    else:
        # Build channel selection
        sel_chs_mask_units = np.zeros(muaps.shape[0:3]).astype(bool)
        sel_chs_fun = {
            "iqr": get_highest_iqr_ch,
            "iqr_ptp": get_highest_iqr_ptp_ch,
            "max_amp": get_highest_amp_ch,
            "ptp": get_highest_ptp_ch
        }
        # Apply channel selection
        for unit in range(units):
            sel_chs_mask_units[unit] = sel_chs_fun[sel_chs_by](muaps[unit])
        sel_chs_mask = np.all(sel_chs_mask_units, axis=0)
        if not np.any(sel_chs_mask):
            sel_chs_mask = np.ones_like(sel_chs_mask).astype(bool)

        # Initialise muap energy
        energy = np.empty(muaps.shape[0:3])
        energy[:] = np.nan

        # Compute muap energy
        for unit in range(units):
            muap_sel_chs = muaps[unit][sel_chs_mask]
            energy[unit][sel_chs_mask] = np.sum(
                np.power(muap_sel_chs, 2), axis=-1
            )

    return energy

get_muap_ptp

get_muap_ptp(muaps: ndarray, sel_chs_by: Optional[Literal['iqr', 'iqr_ptp', 'max_amp', 'ptp']] = 'iqr') -> np.ndarray

Compute the peak-to-peak amplitude of motor unit action potentials (MUAPs).

Parameters:

Name Type Description Default
muaps ndarray

Array of MUAPs with shape (m, r, c, win_samples), where m is the number of motor units, r is the number of rows, c is the number of columns, and win_samples is the number of samples in the window.

required
sel_chs_by str

Method for selecting channels: "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel. Defaults to "iqr".

'iqr'

Returns:

Type Description
ndarray

np.ndarray: Array of peak-to-peak amplitudes with shape (m, r, c), where m is the number of motor units, r is the number of rows, and c is the number of columns.

Notes
  • The peak-to-peak amplitude is computed as the difference between the maximum and minimum values of each MUAP.
  • If sel_chs_by is specified, the peak-to-peak amplitude is computed only for the selected channels based on the specified method.
Source code in motor_unit_toolbox/props.py
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
def get_muap_ptp(
    muaps: np.ndarray,
    sel_chs_by: Optional[Literal["iqr", "iqr_ptp", "max_amp", "ptp"]] = "iqr",
) -> np.ndarray:
    """
    Compute the peak-to-peak amplitude of motor unit action potentials (MUAPs).

    Args:
        muaps (np.ndarray): Array of MUAPs with shape (m, r, c, win_samples),
            where m is the number of motor units, r is the number of rows, c
            is the number of columns, and win_samples is the number of samples
            in the window.
        sel_chs_by (str, optional): Method for selecting channels:
            "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel.
            Defaults to "iqr".

    Returns:
        np.ndarray: Array of peak-to-peak amplitudes with shape (m, r, c), where m is
            the number of motor units, r is the number of rows, and c is the
            number of columns.

    Notes:
        - The peak-to-peak amplitude is computed as the difference between the
          maximum and minimum values of each MUAP.
        - If sel_chs_by is specified, the peak-to-peak amplitude is computed only
          for the selected channels based on the specified method.
    """
    # Check muaps size
    if len(muaps.shape) < 4:
        muaps = np.expand_dims(muaps, axis=0)
    units = muaps.shape[0]

    if sel_chs_by is None:
        # Compute muap peak to peak amplitude for each muap channel
        ptp = np.ptp(muaps, axis=-1)
    else:
        # Build channel selection
        sel_chs_mask_units = np.zeros(muaps.shape[0:3]).astype(bool)
        sel_chs_fun = {
            "iqr": get_highest_iqr_ch,
            "iqr_ptp": get_highest_iqr_ptp_ch,
            "max_amp": get_highest_amp_ch,
            "ptp": get_highest_ptp_ch
        }
        # Apply channel selection
        for unit in range(units):
            sel_chs_mask_units[unit] = sel_chs_fun[sel_chs_by](muaps[unit])
        sel_chs_mask = np.all(sel_chs_mask_units, axis=0)
        if not np.any(sel_chs_mask):
            sel_chs_mask = np.ones_like(sel_chs_mask).astype(bool)

        # Initialise muap peak to peak amplitude
        ptp = np.empty(muaps.shape[0:3])
        ptp[:] = np.nan

        # Compute muap peak to peak amplitude
        for unit in range(units):
            muap_sel_chs = muaps[unit][sel_chs_mask]
            ptp[unit][sel_chs_mask] = np.ptp(muap_sel_chs, axis=-1)

    return ptp

get_muap_ptp_time

get_muap_ptp_time(muaps: ndarray, sel_chs_by: Optional[Literal['iqr', 'iqr_ptp', 'max_amp', 'ptp']] = 'iqr') -> np.ndarray

Compute the peak-to-peak time (in samples) of motor unit action potentials (MUAPs).

Parameters:

Name Type Description Default
muaps ndarray

Array of MUAPs with shape (m, r, c, win_samples), where m is the number of motor units, r is the number of rows, c is the number of columns, and win_samples is the number of samples in the window.

required
sel_chs_by str

Method for selecting channels: "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel. Defaults to "iqr".

'iqr'

Returns:

Type Description
ndarray

np.ndarray: Array of peak-to-peak times (in samples) with shape (m, r, c), where m is the number of motor units, r is the number of rows, and c is the number of columns.

Notes
  • The peak-to-peak time is computed as the absolute difference between the sample index of the maximum and minimum values of each MUAP.
  • If sel_chs_by is specified, the peak-to-peak time is computed only for the selected channels based on the specified method.
Source code in motor_unit_toolbox/props.py
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
def get_muap_ptp_time(
    muaps: np.ndarray,
    sel_chs_by: Optional[Literal["iqr", "iqr_ptp", "max_amp", "ptp"]] = "iqr",
) -> np.ndarray:
    """
    Compute the peak-to-peak time (in samples) of motor unit action potentials (MUAPs).

    Parameters:
        muaps (np.ndarray): Array of MUAPs with shape (m, r, c, win_samples),
            where m is the number of motor units, r is the number of rows, c
            is the number of columns, and win_samples is the number of samples
            in the window.
        sel_chs_by (str, optional): Method for selecting channels:
            "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel.
            Defaults to "iqr".

    Returns:
        np.ndarray: Array of peak-to-peak times (in samples) with shape (m, r, c),
            where m is the number of motor units, r is the number of rows, and
            c is the number of columns.

    Notes:
        - The peak-to-peak time is computed as the absolute difference between
          the sample index of the maximum and minimum values of each MUAP.
        - If sel_chs_by is specified, the peak-to-peak time is computed only for
          the selected channels based on the specified method.
    """
    # Check muaps size
    if len(muaps.shape) < 4:
        muaps = np.expand_dims(muaps, axis=0)
    units = muaps.shape[0]

    if sel_chs_by is None:
        # Compute muap peak to peak time (in samples) for each muap channel
        ptp_time = np.abs(
            np.argmax(muaps, axis=-1) - np.argmin(muaps, axis=-1)
        )
    else:
        # Build channel selection
        sel_chs_mask_units = np.zeros(muaps.shape[0:3]).astype(bool)
        sel_chs_fun = {
            "iqr": get_highest_iqr_ch,
            "iqr_ptp": get_highest_iqr_ptp_ch,
            "max_amp": get_highest_amp_ch,
            "ptp": get_highest_ptp_ch
        }
        # Apply channel selection
        for unit in range(units):
            sel_chs_mask_units[unit] = sel_chs_fun[sel_chs_by](muaps[unit])
        sel_chs_mask = np.all(sel_chs_mask_units, axis=0)
        if not np.any(sel_chs_mask):
            sel_chs_mask = np.ones_like(sel_chs_mask).astype(bool)

        # Initialise muap peak to peak time (in samples)
        ptp_time = np.empty(muaps.shape[0:3])
        ptp_time[:] = np.nan

        # Compute muap peak to peak time (in samples)
        for unit in range(units):
            muap_sel_chs = muaps[unit][sel_chs_mask]
            ptp_time[unit][sel_chs_mask] = np.abs(
                np.argmax(muap_sel_chs, axis=-1) -
                np.argmin(muap_sel_chs, axis=-1)
            )

    return ptp_time

get_muap_peak_frequency

get_muap_peak_frequency(muaps: ndarray, sel_chs_by: Optional[Literal['iqr', 'iqr_ptp', 'max_amp', 'ptp']] = 'iqr', fs: Optional[int] = 2048) -> np.ndarray

Compute the peak frequency of motor unit action potentials (MUAPs).

Parameters:

Name Type Description Default
muaps ndarray

Array of MUAPs with shape (m, r, c, win_samples), where m is the number of motor units, r is the number of rows, c is the number of columns, and win_samples is the number of samples in the window.

required
sel_chs_by str

Method for selecting channels: "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel. Defaults to "iqr".

'iqr'
fs int

Sampling frequency in Hz. Defaults to 2048.

2048

Returns:

Type Description
ndarray

np.ndarray: Array of peak frequencies with shape (m, r, c), where m is the number of motor units, r is the number of rows, and c is the number of columns.

Notes
  • The peak frequency is computed as the frequency corresponding to the maximum value in the power spectrum of each MUAP.
  • If sel_chs_by is specified, the peak frequency is computed only for the selected channels based on the specified method.
Source code in motor_unit_toolbox/props.py
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
def get_muap_peak_frequency(
    muaps: np.ndarray,
    sel_chs_by: Optional[Literal["iqr", "iqr_ptp", "max_amp", "ptp"]] = "iqr",
    fs: Optional[int] = 2048
) -> np.ndarray:
    """
    Compute the peak frequency of motor unit action potentials (MUAPs).

    Args:
        muaps (np.ndarray): Array of MUAPs with shape (m, r, c, win_samples),
            where m is the number of motor units, r is the number of rows, c
            is the number of columns, and win_samples is the number of samples
            in the window.
        sel_chs_by (str, optional): Method for selecting channels:
            "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel.
            Defaults to "iqr".
        fs (int, optional): Sampling frequency in Hz. Defaults to 2048.

    Returns:
        np.ndarray: Array of peak frequencies with shape (m, r, c), where m is
            the number of motor units, r is the number of rows, and c is the
            number of columns.

    Notes:
        - The peak frequency is computed as the frequency corresponding to the
          maximum value in the power spectrum of each MUAP.
        - If sel_chs_by is specified, the peak frequency is computed only for
          the selected channels based on the specified method.
    """
    # Check muaps size
    if len(muaps.shape) < 4:
        muaps = np.expand_dims(muaps, axis=0)

    # Compute power spectrum for each muap channel
    units, rows, cols, samples = muaps.shape
    ps = np.power(
        np.abs(np.fft.fft(muaps, n=samples, axis=-1)), 2
        )[:, :, :, :samples//2]
    freq = np.fft.fftfreq(n=samples, d=1/fs)[:samples//2]
    max_ps_idx = np.argmax(ps, axis=-1)

    if sel_chs_by is not None:
        # Build channel selection
        sel_chs_mask_units = np.zeros(muaps.shape[0:3]).astype(bool)
        sel_chs_fun = {
            "iqr": get_highest_iqr_ch,
            "iqr_ptp": get_highest_iqr_ptp_ch,
            "max_amp": get_highest_amp_ch,
            "ptp": get_highest_ptp_ch
        }

        # Apply channel selection
        for unit in range(units):
            sel_chs_mask_units[unit] = sel_chs_fun[sel_chs_by](muaps[unit])
        sel_chs_mask = np.all(sel_chs_mask_units, axis=0)
        if not np.any(sel_chs_mask):
            sel_chs_mask = np.ones_like(sel_chs_mask).astype(bool)
    else:
        sel_chs_mask = np.ones(muaps.shape[1:3]).astype(bool)

    # Initialise peak frequency
    peak_freq = np.empty(muaps.shape[0:3])
    peak_freq[:] = np.nan
    for unit, row, col in itertools.product(
            range(units), range(rows), range(cols)
            ):
        if not sel_chs_mask[row, col]:
            continue
        peak_freq[unit, row, col] = freq[max_ps_idx[unit, row, col]]

    return peak_freq

get_muap_median_frequency

get_muap_median_frequency(muaps: ndarray, sel_chs_by: Optional[Literal['iqr', 'iqr_ptp', 'max_amp', 'ptp']] = 'iqr', fs: Optional[int] = 2048) -> np.ndarray

Compute the median frequency of motor unit action potentials (MUAPs).

Parameters:

Name Type Description Default
muaps ndarray

Array of MUAPs with shape (m, r, c, win_samples), where m is the number of motor units, r is the number of rows, c is the number of columns, and win_samples is the number of samples in the window.

required
sel_chs_by str

Method for selecting channels: "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel. Defaults to "iqr".

'iqr'
fs int

Sampling frequency in Hz. Defaults to 2048.

2048

Returns:

Type Description
ndarray

np.ndarray: Array of median frequencies with shape (m, r, c), where m is the number of motor units, r is the number of rows, and c is the number of columns.

Notes
  • The median frequency is computed as the frequency corresponding to the cumulative power spectrum reaching half of its total power.
  • If sel_chs_by is specified, the median frequency is computed only for the selected channels based on the specified method.
Source code in motor_unit_toolbox/props.py
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
def get_muap_median_frequency(
        muaps: np.ndarray,
        sel_chs_by: Optional[Literal["iqr", "iqr_ptp", "max_amp", "ptp"]] = "iqr",
        fs: Optional[int] = 2048
    ) -> np.ndarray:
    """
    Compute the median frequency of motor unit action potentials (MUAPs).

    Args:
        muaps (np.ndarray): Array of MUAPs with shape (m, r, c, win_samples),
            where m is the number of motor units, r is the number of rows, c
            is the number of columns, and win_samples is the number of samples
            in the window.
        sel_chs_by (str, optional): Method for selecting channels:
            "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel.
            Defaults to "iqr".
        fs (int, optional): Sampling frequency in Hz. Defaults to 2048.

    Returns:
        np.ndarray: Array of median frequencies with shape (m, r, c), where m is
            the number of motor units, r is the number of rows, and c is the
            number of columns.

    Notes:
        - The median frequency is computed as the frequency corresponding to the
          cumulative power spectrum reaching half of its total power.
        - If sel_chs_by is specified, the median frequency is computed only for
          the selected channels based on the specified method.
    """

    # Check muaps size
    if len(muaps.shape) < 4:
        muaps = np.expand_dims(muaps, axis=0)

    # Compute power spectrum for each muap channel
    units, rows, cols, samples = muaps.shape
    ps = np.power(
        np.abs(np.fft.fft(muaps, n=samples, axis=-1)), 2
        )[:, :, :, :samples//2]
    freq = np.fft.fftfreq(n=samples, d=1/fs)[:samples//2]

    # Compute peak frequency
    cum_ps = np.cumsum(ps, axis=-1)
    med_ps = np.sum(ps, axis=-1)/2

    if sel_chs_by is not None:
        # Build channel selection
        sel_chs_mask_units = np.zeros(muaps.shape[0:3]).astype(bool)
        sel_chs_fun = {
            "iqr": get_highest_iqr_ch,
            "iqr_ptp": get_highest_iqr_ptp_ch,
            "max_amp": get_highest_amp_ch,
            "ptp": get_highest_ptp_ch
        }

        # Apply channel selection
        for unit in range(units):
            sel_chs_mask_units[unit] = sel_chs_fun[sel_chs_by](muaps[unit])
        sel_chs_mask = np.all(sel_chs_mask_units, axis=0)
        if not np.any(sel_chs_mask):
            sel_chs_mask = np.ones_like(sel_chs_mask).astype(bool)
    else:
        sel_chs_mask = np.ones(muaps.shape[1:3]).astype(bool)

    # Initialise peak frequency
    med_freq = np.empty(muaps.shape[0:3])
    med_freq[:] = np.nan
    for unit, row, col in itertools.product(
            range(units), range(rows), range(cols)
            ):
        if not sel_chs_mask[row, col]:
            continue
        med_freq[unit, row, col] = freq[np.argmin(np.abs(
            cum_ps[unit, row, col] -
            med_ps[unit, row, col]
            ))]
    return med_freq

get_muap_mean_frequency

get_muap_mean_frequency(muaps: ndarray, sel_chs_by: Optional[Literal['iqr', 'iqr_ptp', 'max_amp', 'ptp']] = 'iqr', fs: Optional[int] = 2048) -> np.ndarray

Compute the mean frequency of motor unit action potentials (MUAPs).

Parameters:

Name Type Description Default
muaps ndarray

Array of MUAPs with shape (m, r, c, win_samples), where m is the number of motor units, r is the number of rows, c is the number of columns, and win_samples is the number of samples in the window.

required
sel_chs_by str

Method for selecting channels: "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel. Defaults to "iqr".

'iqr'
fs int

Sampling frequency in Hz. Defaults to 2048.

2048

Returns:

Type Description
ndarray

np.ndarray: Array of mean frequencies with shape (m, r, c), where m is the number of motor units, r is the number of rows, and c is the number of columns.

Notes
  • The mean frequency is computed as the weighted average of the frequency components in the power spectrum of each MUAP.
  • If sel_chs_by is specified, the mean frequency is computed only for the selected channels based on the specified method.
Source code in motor_unit_toolbox/props.py
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
def get_muap_mean_frequency(
        muaps: np.ndarray,
        sel_chs_by: Optional[Literal["iqr", "iqr_ptp", "max_amp", "ptp"]] = "iqr",
        fs: Optional[int] = 2048
    ) -> np.ndarray:
    """
    Compute the mean frequency of motor unit action potentials (MUAPs).

    Parameters:
        muaps (np.ndarray): Array of MUAPs with shape (m, r, c, win_samples),
            where m is the number of motor units, r is the number of rows, c
            is the number of columns, and win_samples is the number of samples
            in the window.
        sel_chs_by (str, optional): Method for selecting channels:
            "iqr", "iqr_ptp", "max_amp" or "ptp". None uses every channel.
            Defaults to "iqr".
        fs (int, optional): Sampling frequency in Hz. Defaults to 2048.

    Returns:
        np.ndarray: Array of mean frequencies with shape (m, r, c), where m is
            the number of motor units, r is the number of rows, and c is the
            number of columns.

    Notes:
        - The mean frequency is computed as the weighted average of the frequency
          components in the power spectrum of each MUAP.
        - If sel_chs_by is specified, the mean frequency is computed only for
          the selected channels based on the specified method.
    """

    # Check muaps size
    if len(muaps.shape) < 4:
        muaps = np.expand_dims(muaps, axis=0)

    # Compute power spectrum for each muap channel
    units, _, _, samples = muaps.shape
    ps = np.power(
            np.abs(np.fft.fft(muaps, n=samples, axis=-1)), 2
            )[:, :, :, :samples//2]
    freq = np.fft.fftfreq(n=samples, d=1/fs)[:samples//2]

    # Compute peak frequency
    mean_freq = np.sum(ps * freq, axis=-1)/np.sum(ps, axis=-1)

    if sel_chs_by is not None:
        # Build channel selection
        sel_chs_mask_units = np.zeros(muaps.shape[0:3]).astype(bool)
        sel_chs_fun = {
            "iqr": get_highest_iqr_ch,
            "iqr_ptp": get_highest_iqr_ptp_ch,
            "max_amp": get_highest_amp_ch,
            "ptp": get_highest_ptp_ch
        }

        # Apply channel selection
        for unit in range(units):
            sel_chs_mask_units[unit] = sel_chs_fun[sel_chs_by](muaps[unit])
        sel_chs_mask = np.all(sel_chs_mask_units, axis=0)
        if not np.any(sel_chs_mask):
            sel_chs_mask = np.ones_like(sel_chs_mask).astype(bool)

        # Discard non selected channels
        for unit in range(units):
            mean_freq[unit][np.logical_not(sel_chs_mask)] = np.nan

    return mean_freq