import numpy as np
from scipy.signal import find_peaks
from scipy.linalg import eigh
[docs]
def CFICA2(emg_data, param=None, Mu=None):
"""
Python version of CFICA2: Kernel and Correlation Constrained FastICA for HD-EMG
"""
# Default parameter initialization
if param is None:
param = {}
param.setdefault('delay', 10)
param.setdefault('fs', 2048)
param.setdefault('wavelength', param['fs'] // 20)
param.setdefault('peakinterval', param['fs'] // 40)
param.setdefault('convergethreshold', 1e-6)
param.setdefault('valleymode', 0)
# Initialize Mu structure if not provided
if Mu is None:
Mu = {'Wave': [], 'MUpulse': [], 'S': []}
# Whitening and delay embedding
Xt, _, _ = whitening_pca(emg_data, param['delay'])
if Mu['MUpulse']:
YT = delay_embed(Mu['MUpulse'], param['delay'])
C = YT @ Xt.T
MUnum = Mu['S'].shape[0]
else:
C = None
YT = []
MUnum = 0
total_num = Xt.shape[0] // (2 * param['delay'] + 1)
while MUnum < total_num:
Y, _ = kernel_fastica(Xt, C, param['convergethreshold'])
threshold, St = threshold_cov(Y, param['peakinterval'])
# For now, auto-accept threshold (interactive GUI option later)
Y, _ = corr_constrained_ica(Xt, St, param['convergethreshold'])
YT.append(delay_embed(Y, param['delay']))
C = np.vstack(YT) @ Xt.T if YT else None
Mu['MUpulse'].append(Y)
Mu['S'].append(St)
MUnum += 1
# Discard redundant units
Mu['S'], Mu['MUpulse'] = discard_redundant_spikes(Mu['S'], Mu['MUpulse'])
# Extract waveforms
Mu['Wave'] = waveform_estimation(emg_data, Mu['S'], param['wavelength'])
return np.array(Mu['S']), Mu
[docs]
def whitening_pca(x, delay):
n_channels, n_samples = x.shape
xt = []
for i in range(n_channels):
row = np.array([np.roll(x[i], -d) for d in range(delay + 1)])
xt.append(row[:, :n_samples - delay])
xt = np.vstack(xt)
xt -= np.mean(xt, axis=1, keepdims=True)
cov = np.cov(xt)
eigvals, eigvecs = eigh(cov)
idx = np.argsort(eigvals)[::-1]
eigvecs = eigvecs[:, idx]
eigvals = eigvals[idx]
D_inv = np.diag(1. / np.sqrt(eigvals + 1e-10))
whitening_matrix = D_inv @ eigvecs.T
Xt = whitening_matrix @ xt
return Xt, whitening_matrix, xt
[docs]
def delay_embed(x, delay):
"""
Emulates the yanchi() function from MATLAB's CFICA2.
Returns (2*delay + 1)*channels x samples matrix
"""
if x.ndim == 1:
x = x[np.newaxis, :] # shape (1, N)
n_channels, n_samples = x.shape
out = []
for i in range(n_channels):
delayed = np.array([
np.roll(x[i], -d) if d >= 0 else np.roll(x[i], x.shape[1] + d)
for d in range(-delay, delay + 1)
])
out.append(delayed)
out = np.vstack(out)
return out
[docs]
def kernel_fastica(X, A=None, threshold=1e-6):
n, m = X.shape
w = np.random.randn(n)
w /= np.linalg.norm(w)
Y = w @ X
for _ in range(1000):
w_old = w.copy()
gw = np.tanh(Y)
g_w_deriv = 1 - gw**2
w = (X @ gw.T) / m - g_w_deriv.mean() * w_old
w /= np.linalg.norm(w)
Y = w @ X
if np.abs(np.dot(w, w_old)) > 1 - threshold:
break
return Y, w
[docs]
def corr_constrained_ica(X, S, threshold=1e-6):
r = X @ S.T
r /= np.linalg.norm(r)
w = r.copy()
Y = w @ X
for _ in range(1000):
w_old = w.copy()
gw = np.tanh(Y)
g_w_deriv = 1 - gw**2
w = 0.3 * r - (X @ gw.T) / X.shape[1] + g_w_deriv.mean() * w_old
w /= np.linalg.norm(w)
Y = w @ X
if np.abs(np.dot(w, w_old)) > 1 - threshold:
break
return Y, w
[docs]
def threshold_cov(x, peak_interval):
thresholds = np.linspace(2, np.max(x[:20]), 200)
best_cov = np.inf
best_th = thresholds[0]
best_spike = None
for th in thresholds:
peaks, _ = find_peaks(x, height=th, distance=peak_interval)
if len(peaks) < 2:
continue
intervals = np.diff(peaks)
cov = np.std(intervals) / np.mean(intervals)
if cov < best_cov:
best_cov = cov
best_th = th
best_spike = peaks
spike_train = np.zeros_like(x)
if best_spike is not None:
spike_train[best_spike] = 1
return best_th, spike_train
[docs]
def discard_redundant_spikes(S, MUpulse, threshold=0.4):
unique_S = []
unique_MU = []
while len(S):
base = S[0]
base_mu = MUpulse[0]
unique_S.append(base)
unique_MU.append(base_mu)
corr = [np.corrcoef(base, s)[0, 1] for s in S]
S = [s for i, s in enumerate(S) if corr[i] < threshold]
MUpulse = [s for i, s in enumerate(MUpulse) if corr[i] < threshold]
return unique_S, unique_MU