bycycle-tools / bycycle

Cycle-by-cycle analysis of neural oscillations.

Home Page:https://bycycle-tools.github.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Setting boundary values in find extrema function + inconsistent periods

rahul-jayaram1 opened this issue · comments

Is there a way to manually set boundary values when running the find_extrema function? When I run it now, the function does not seem to calculate the extremas at the start and end of our signal. I ran the function's calculation code on its own using the input parameters as variables, and when I manually set the boundary value to 1, more of the extremas were detected than when I ran the function itself. I get an error when I try to assign the value of boundary as an input to the function.

Another issue I have encountered is that when I obtain the period values from the returned dataframe of the compute_features function, these periods do not match with the differences in troughs of the plot of the signal with extremas highlighted. Also, sometimes the number of oscilations in the dataframe does not match the number of oscillations present on the plot of the signal with extremas highlighted.

Hi,
it would be easiest if you could provide a running code example (e.g. using synthetic data) with: the parameters you set, the output you get and the output you expect, then we can efficiently discuss it and see what the problem is here!

Hi,

I have set the following parameters.
Fs = 600
tlim = (-.1, .500) #time interval in seconds
tidx = np.logical_and(t>=tlim[0], t<tlim[1])
f_theta = (10,22)
f_lowpass = 100
N_seconds = 0.1
N_seconds_theta = 0.02

Our signal is a length 361 time series vector

here is the code I am running to get the output figure.
signal_low= lowpass_filter(signal, Fs, f_lowpass, N_seconds=N_seconds, remove_edge_artifacts=False)
signal_narrow = bandpass_filter(signal, Fs, f_theta,
remove_edge_artifacts=False,
N_seconds=N_seconds_theta)
zeroriseN = _fzerorise(signal_narrow)
zerofallN = _fzerofall(signal_narrow)

Ps, Ts = find_extrema(signal_low, Fs, f_theta,
filter_kwargs={'N_seconds':N_seconds_theta})
tidxPs = Ps[np.logical_and(Ps>tlim[0]*Fs, Ps<tlim[1]*Fs)]
tidxTs = Ts[np.logical_and(Ts>tlim[0]*Fs, Ts<tlim[1]*Fs)]

zeroxR, zeroxD = find_zerox(signal_low, Ps, Ts)

tidx = np.logical_and(t>=tlim[0], t<tlim[1])
tidxPs = Ps[np.logical_and(Ps>tlim[0]*Fs, Ps<tlim[1]*Fs)]
tidxTs = Ts[np.logical_and(Ts>tlim[0]*Fs, Ts<tlim[1]*Fs)]
tidxDs = zeroxD[np.logical_and(zeroxD>tlim[0]*Fs, zeroxD<tlim[1]*Fs)]
tidxRs = zeroxR[np.logical_and(zeroxR>tlim[0]*Fs, zeroxR<tlim[1]*Fs)]

plt.figure(figsize=(8, 6))
plt.plot(t[tidx], signal_low[tidx], 'k')
plt.plot(t[tidxPs], signal_low[tidxPs], 'b.', ms=10)
plt.plot(t[tidxTs], signal_low[tidxTs], 'r.', ms=10)
plt.plot(t[tidxDs], signal_low[tidxDs], 'm.', ms=10)
plt.plot(t[tidxRs], signal_low[tidxRs], 'g.', ms=10)
plt.xlim(tlim)
plt.title('lowpass - peak/trough and rise/decay midpoints - subject 5')
plt.show()

And I get the output figure titled "lowpass - peak/trough and rise/decay midpoints - subject 5", which I have attached.

Here is the code I run to get the last_trough, next_trough, and period values

df = compute_features(signal_low, Fs, f_theta)
features_mat=df.head()
np.save('lp_feature_matrix_trial2',features_mat)
print(features_mat['sample_last_trough'])
print(features_mat['sample_next_trough'])
print(features_mat['period'])

Here is the output I get

0 94
1 132
2 158
3 206
4 244
Name: sample_last_trough, dtype: int64
0 132
1 158
2 206
3 244
4 276
Name: sample_next_trough, dtype: int64
0 38
1 26
2 48
3 38
4 32
But when I print Ps and Ts globally, I get the following values

Ps= [ 75 115 230 262]
Ts = [ 94 158 244 276]

The differences in troughs does not seem to match the output periods from the compute_features function. Also, on the output figure, there are 4 red troughs highlighted which would mean 3 oscillations should be present, but the compute_features function displays values for 5 oscillations. Another issue I have been having is that not all of the oscillations seem to be detected, particularly those on the edges.

The output I am expecting would have these edge oscillations (including the beta event) detected as well as period values that match the observed differences in troughs of the figure (in this case, 3 periods with values of about 100ms, 150ms, and 70ms).

Please let me know what you think, and thank you for your help!

lowpass_100_extremas_subj5

  1. One problematic part in your code is:
tidxPs = Ps[np.logical_and(Ps>tlim[0]*Fs, Ps<tlim[1]*Fs)]
tidxTs = Ts[np.logical_and(Ts>tlim[0]*Fs, Ts<tlim[1]*Fs)]
tidxDs = zeroxD[np.logical_and(zeroxD>tlim[0]*Fs, zeroxD<tlim[1]*Fs)]
tidxRs = zeroxR[np.logical_and(zeroxR>tlim[0]*Fs, zeroxR<tlim[1]*Fs)]

since tlim[0]*Fs is negative, it will not correspond to the correct sample number, and not the right peaks and troughs for the time interval of interest are preserved for the later part of the signal. The beta event in the front may be affected by edge artefacts, depending on how long your signal is, and therefore not detected. Check that you have sufficient signal length to account for edge artefacts (e.g. cropping longer epochs).

  1. Another problematic part is that you run compute_features without a parameter dictionary, and then it will automatically plugin in some defaults, e.g N_cycles_min for find_extrema, which do not correspond to the parameters you set manually. Try setting them explicitly and see if it resolves. If the problem persists, you can try uploading the time series and we can figure this out with this specific example.

Hi,

Thank you so much for your help.

By parameter dictionary are you referring to
burst_kwargs = {'amplitude_fraction_threshold': .3,
'amplitude_consistency_threshold': .4,
'period_consistency_threshold': .5,
'monotonicity_threshold': .8,
'N_cycles_min': 3} ?

I tried running the code again with such a parameter dictionary, but there was no difference in the extremas detected and the periods were still inconsistent with what was graphed. Do I need to set a parameter dictionary for the find_extremas function as well?

huhu,
I checked a bit more and the issue here is that an already filtered signal is passed to find_extrema, where it is again band-pass filtered inside the function. And this leads to discrepancies between peaks and troughs returned by find_extrema and compute_features.

Here is a minimal example for playing around with a synthetic signal, returning the same peaks and troughs for both methods:

from bycycle.features import compute_features
from bycycle.cyclepoints import find_extrema
import numpy as np

# create sine wave + some noise
nr_seconds = 3
Fs = 600
t = np.linspace(0, nr_seconds, nr_seconds*Fs)
peak_frequency = 10
noise_lvl = 0.25
noise = noise_lvl * np.random.randn(len(t))
signal = np.sin(peak_frequency*2*np.pi*t) + noise

# find peaks and troughs
f_range = (8, 12)
burst_kwargs = {'amplitude_fraction_threshold': .5,
                'N_cycles_min': 3}

Ps, Ts = find_extrema(signal, Fs, f_range, filter_kwargs={'N_cycles': 3})
df = compute_features(signal, Fs, f_range, burst_detection_kwargs=burst_kwargs)

print(df[['sample_last_trough', 'sample_peak', 'sample_next_trough']].head())
print('troughs', Ts)
print('peaks', Ps)

I hope this is helpful for resolving, if not, just let me know!

Hi,

Thank you so much! I think that was the issue I was having, and the sample code you sent was really helpful. There are no more peak discrepancies, and the extrema fit the signal well.

The extrema in the middle of the signal are detected well, however I am still having trouble detecting the extrema at the edges of the signal (first and last ~150 ms), even at the highest filter length. Do you have any suggestions for overcoming this issue?

Thank you again!

very good @ resolved peak discrepancies!

The first few cycles are automatically not labeled as bursts, to avoid the problem that edge artefacts impacts zero-crossing estimation and results in distorted burst features.

The best way to detect extrema at the edges of the signal is to crop epochs of longer length, run the burst estimation and then discard bursts with sample times that are outside of the time interval of interest (using sample_peak or sample_trough of the output dataframe).