Coverage for src/thunderfish/checkpulse.py: 0%
63 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-29 16:21 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-29 16:21 +0000
1"""
2Check for pulse-type weakly electric fish
4Check whether a pulse-type or a wave-type weakly electric fish is present in a recording.
6## Main functions
8- `check_pulse()`: checks for pulse-type fish.
10## Configuration parameter
12- `add_check_pulse_config()`: add parameters for `check_pulse()` to configuration.
13- `check_pulse_args()`: retrieve parameters for `check_pulse()` from configuration.
15"""
17import numpy as np
18from thunderlab.eventdetection import percentile_threshold, detect_peaks, trim
21def check_pulse(data, sem, samplerate, thresh_fac=0.8, percentile=0.0,
22 sem_fac=0.05, pulse_thresh=0.15, verbose=0):
23 """Detects if a fish is pulse- or wave-type based on the proportion of the time distance
24 between a peak and its following trough, relative to the time between consecutive peaks.
27 Parameters
28 ----------
29 data: 1-D array
30 The data to be analyzed.
31 sem: 1-D array or None
32 Standard error of the mean corresponding to data.
33 samplerate: float
34 Sampling rate of the data in Hertz.
35 percentile: float
36 The interpercentile range is computed at percentile and 100.0-percentile.
37 If zero take maxmimum minus minimum.
38 thresh_fac: float
39 The threshold for peak detection is the inter-percentile-range
40 multiplied by this factor.
41 sem_fac: float
42 Base analysis on peaks and troughs with the corresponding standard error of the mean
43 of the data below `sem_fac` times the interpercentile range.
44 pulse_thresh: float
45 A positive number setting the minimum distance between peaks and troughs.
46 verbose: int
47 If > 1, print information in the command line.
49 Returns
50 -------
51 pulse_fish: bool
52 True if algorithm suggests a pulse-type fish.
53 peak_ratio: float
54 Returns a float between 0. and 1. which gives the proportion of peak-2-trough,
55 from peak-2-peak time distance, i.e. pulse width relative to pulse interval.
56 """
58 def ratio(peak_idx, trough_idx):
59 if len(peak_idx) < 2:
60 return 1.0 if len(peak_idx)+len(trough_idx) < 1 else 0.0
61 # ratio of peak-to-trough to peak-to-peak time distances:
62 ratios = np.abs((trough_idx - peak_idx))[:-1].astype(float) / np.diff(peak_idx)
63 # fix for cases where trough of eod comes before peak:
64 ratios[ratios > 0.5] = 1.0 - ratios[ratios > 0.5]
65 return np.median(ratios)
67 # threshold for peak detection:
68 pp_ampl = percentile_threshold(data, thresh_fac=1.0, percentile=percentile)
69 threshold = thresh_fac*pp_ampl
70 if verbose > 1:
71 print(' check_pulse(): amplitude threshold for peak detection is %g' % threshold)
72 # detect large peaks and troughs:
73 peak_idx, trough_idx = detect_peaks(data, threshold)
74 # XXX check here for less than 2 peaks + troughs XXX
75 if verbose > 1:
76 print(' check_pulse(): detected %d peaks %d troughs' %
77 (len(peak_idx), len(trough_idx)))
78 peak_idx, trough_idx = trim(peak_idx, trough_idx)
79 if verbose > 1:
80 print(' check_pulse(): trim %d peaks %d troughs' %
81 (len(peak_idx), len(trough_idx)))
82 # take only peaks and troughs of reliable sem:
83 if sem is not None:
84 athresh = sem_fac*pp_ampl
85 sel = (sem[peak_idx]<athresh) & (sem[trough_idx]<athresh)
86 peak_idx = peak_idx[sel]
87 trough_idx = trough_idx[sel]
88 if verbose > 1:
89 print(' check_pulse(): sem left %d peaks %d troughs' %
90 (len(peak_idx), len(trough_idx)))
91 # compute ratios:
92 peak_ratio = np.mean([ratio(peak_idx, trough_idx), ratio(trough_idx, peak_idx)])
93 pulse_fish = peak_ratio < pulse_thresh
94 if verbose > 0:
95 print(' check_pulse(): classified as %s fish: pulse-width-ratio is %.3f' %
96 ('pulse' if pulse_fish else 'wave', peak_ratio))
97 return pulse_fish, peak_ratio
100def add_check_pulse_config(cfg, thresh_fac=0.4, percentile=0.0,
101 sem_fac=0.05, pulse_thresh=0.1):
102 """ Add parameter needed for `check_pulse()` as a new section to a configuration.
104 Parameters
105 ----------
106 cfg: ConfigFile
107 The configuration.
108 See check_pulse() for details on the remaining arguments.
109 """
111 cfg.add_section('Classify waveforms:')
112 cfg.add('pulseWidthPercentile', percentile, '%', 'Threshold for detecing peaks is based on interpercentile range. If zero use maximum minus minimum amplitude.')
113 cfg.add('pulseWidthThresholdFactor', thresh_fac, '', 'The threshold for peak detection is this factor multiplied with the interpercentile range. Should be smaller than 0.5 if only a single pulse is contained in the data.')
114 cfg.add('pulseWidthSEMFactor', sem_fac, '', 'Peaks and troughs where the standarad error of the mean of the data is larger than the interpercentile range times this factor are discarded from the analysis.')
115 cfg.add('pulseWidthThresholdRatio', pulse_thresh, '', 'A pulsefish is detected if the width of the pulses relative to the intervals is smaller than this threshold.')
118def check_pulse_args(cfg):
119 """ Translates a configuration to the
120 respective parameter names of the function `check_pulse()`.
121 The return value can then be passed as key-word arguments to this function.
123 Parameters
124 ----------
125 cfg: ConfigFile
126 The configuration.
128 Returns
129 -------
130 a: dict
131 Dictionary with names of arguments of the check_pulse() function
132 and their values as supplied by `cfg`.
133 """
134 a = cfg.map({'thresh_fac': 'pulseWidthThresholdFactor',
135 'percentile': 'pulseWidthPercentile',
136 'sem_fac': 'pulseWidthSEMFactor',
137 'pulse_thresh': 'pulseWidthThresholdRatio'})
138 return a
141if __name__ == "__main__":
142 import sys
143 import matplotlib.pyplot as plt
144 from .bestwindow import best_window
145 from .fakefish import wavefish_eods, pulsefish_eods
147 print("\nChecking checkpulse module ...\n")
149 # generate data:
150 rate = 44100.0
151 if len(sys.argv) < 2:
152 data = wavefish_eods('Eigenmannia', 80.0, rate, 8.0)
153 elif sys.argv[1] == '-w':
154 data = wavefish_eods('Alepto', 600.0, rate, 8.0)
155 elif sys.argv[1] == '-m':
156 data = pulsefish_eods('monophasic', 80.0, rate, 8.0)
157 elif sys.argv[1] == '-b':
158 data = pulsefish_eods('biphasic', 80.0, rate, 8.0)
159 elif sys.argv[1] == '-t':
160 data = pulsefish_eods('triphasic', 80.0, rate, 8.0)
161 else: # load data given by the user
162 from thunderlab.dataloader import load_data
164 file_path = sys.argv[1]
165 print("loading %s ...\n" % file_path)
166 rawdata, rate, unit, amax = load_data(sys.argv[1])
167 data, _ = best_window(rawdata[:,0], rate)
169 # run pulse-width-based detector:
170 pulse_fish, ratio = check_pulse(data, None, rate)