Coverage for src / thunderfish / eodexplorer.py: 0%
482 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-15 17:50 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-15 17:50 +0000
1"""View and explore properties of EOD waveforms.
2"""
4import os
5import glob
6import sys
7import argparse
8import numpy as np
9import scipy.signal as sig
10import matplotlib.pyplot as plt
11import matplotlib.ticker as ticker
13from multiprocessing import Pool, freeze_support, cpu_count
14from thunderlab.configfile import ConfigFile
15from thunderlab.tabledata import TableData, add_write_table_config, write_table_args
16from thunderlab.dataloader import load_data
17from thunderlab.multivariateexplorer import MultivariateExplorer
18from thunderlab.multivariateexplorer import select_features, select_coloring
19from thunderlab.multivariateexplorer import list_available_features
20from thunderlab.multivariateexplorer import PrintHelp
21from thunderlab.powerspectrum import decibel
23from .version import __version__, __year__
24from .harmonics import add_harmonic_groups_config
25from .eodanalysis import add_species_config
26from .eodanalysis import wave_quality, wave_quality_args, add_eod_quality_config
27from .eodanalysis import pulse_quality, pulse_quality_args
28from .bestwindow import analysis_window
29from .thunderfish import configuration, detect_eods, plot_eods
32basename = ''
35class EODExplorer(MultivariateExplorer):
36 """Simple GUI for viewing and exploring properties of EOD waveforms.
38 EODExplorer adapts a MultivariateExplorer to specific needs of EODs.
40 Static members
41 --------------
42 - `groups`: names of groups of data columns that can be selected.
43 - `select_EOD_properties()`: select data columns to be explored.
44 """
46 def __init__(self, data, data_cols, wave_fish, eod_data,
47 add_waveforms, loaded_spec, rawdata_path):
48 """
49 Parameters
50 ----------
51 data: TableData
52 Full table of EOD properties. Each row is a fish.
53 data_cols: list of string or ints
54 Names or indices of columns in `data` to be explored.
55 You may use the static function `select_EOD_properties()`
56 for assisting the selection of columns.
57 wave_fish: boolean
58 True if data are about wave-type weakly electric fish.
59 False if data are about pulse-type weakly electric fish.
60 eod_data: list of waveform data
61 Either waveform data is only the EOD waveform,
62 a ndarray of shape (time, ['time', 'voltage']), or
63 it is a list with the first element being the EOD waveform,
64 and the second element being a 2D ndarray of spectral properties
65 of the EOD waveform with first column being the frequency or harmonics.
66 add_waveforms: list of string
67 List of what should be shown as waveform. Elements can be
68 'first', 'second', 'ampl', 'power', or 'phase'. For 'first' and 'second'
69 the first and second derivatives of the supplied EOD waveform a computed and shown.
70 'ampl', 'power', and 'phase' select properties of the provided spectral properties.
71 loaded_spec: boolean
72 Indicates whether eod_data contains spectral properties.
73 rawdata_path: string
74 Base path to the raw recording, needed to show thunderfish
75 when double clicking on a single EOD.
76 """
77 self.wave_fish = wave_fish
78 self.eoddata = data
79 self.path = rawdata_path
80 MultivariateExplorer.__init__(self, data[:,data_cols],
81 None, 'EODExplorer')
82 tunit = 'ms'
83 dunit = '1/ms'
84 if wave_fish:
85 tunit = '1/EODf'
86 dunit = 'EODf'
87 wave_data = eod_data
88 xlabels = ['Time [%s]' % tunit]
89 ylabels = ['Voltage']
90 if 'first' in add_waveforms:
91 # first derivative:
92 if loaded_spec:
93 if hasattr(sig, 'savgol_filter'):
94 derivative = lambda x: (np.column_stack((x[0], \
95 sig.savgol_filter(x[0][:,1], 5, 2, 1, x[0][1,0]-x[0][0,0]))), x[1])
96 else:
97 derivative = lambda x: (np.column_stack((x[0][:-1,:], \
98 np.diff(x[0][:,1])/(x[0][1,0]-x[0][0,0]))), x[1])
99 else:
100 if hasattr(sig, 'savgol_filter'):
101 derivative = lambda x: np.column_stack((x, \
102 sig.savgol_filter(x[:,1], 5, 2, 1, x[1,0]-x[0,0])))
103 else:
104 derivative = lambda x: np.column_stack((x[:-1,:], \
105 np.diff(x[:,1])/(x[1,0]-x[0,0])))
106 wave_data = list(map(derivative, wave_data))
107 ylabels.append('dV/dt [%s]' % dunit)
108 if 'second' in add_waveforms:
109 # second derivative:
110 if loaded_spec:
111 if hasattr(sig, 'savgol_filter'):
112 derivative = lambda x: (np.column_stack((x[0], \
113 sig.savgol_filter(x[0][:,1], 5, 2, 2, x[0][1,0]-x[0][0,0]))), x[1])
114 else:
115 derivative = lambda x: (np.column_stack((x[0][:-1,:], \
116 np.diff(x[0][:,2])/(x[0][1,0]-x[0][0,0]))), x[1])
117 else:
118 if hasattr(sig, 'savgol_filter'):
119 derivative = lambda x: np.column_stack((x, \
120 sig.savgol_filter(x[:,1], 5, 2, 2, x[1,0]-x[0,0])))
121 else:
122 derivative = lambda x: np.column_stack((x[:-1,:], \
123 np.diff(x[:,2])/(x[1,0]-x[0,0])))
124 wave_data = list(map(derivative, wave_data))
125 ylabels.append('d^2V/dt^2 [%s^2]' % dunit)
126 if loaded_spec:
127 if wave_fish:
128 indices = [0]
129 phase = False
130 xlabels.append('Harmonics')
131 if 'ampl' in add_waveforms:
132 indices.append(3)
133 ylabels.append('Ampl [%]')
134 if 'power' in add_waveforms:
135 indices.append(4)
136 ylabels.append('Power [dB]')
137 if 'phase' in add_waveforms:
138 indices.append(5)
139 ylabels.append('Phase')
140 phase = True
141 def get_spectra(x):
142 y = x[1][:,indices]
143 if phase:
144 y[y[:,-1]<0.0,-1] += 2.0*np.pi
145 return (x[0], y)
146 wave_data = list(map(get_spectra, wave_data))
147 else:
148 xlabels.append('Frequency [Hz]')
149 ylabels.append('Power [dB]')
150 def get_spectra(x):
151 y = x[1]
152 y[:,1] = decibel(y[:,1], None)
153 return (x[0], y)
154 wave_data = list(map(get_spectra, wave_data))
155 self.set_wave_data(wave_data, xlabels, ylabels, True)
158 def fix_scatter_plot(self, ax, data, label, axis):
159 """Customize an axes of a scatter plot.
161 - Limits for amplitude and time like quantities start at zero.
162 - Phases a labeled with multuples of pi.
163 - Species labels are rotated.
164 """
165 if any(l in label for l in ['ampl', 'power', 'width',
166 'time', 'tau', 'P2-P1-dist',
167 'var', 'peak', 'trough',
168 'P2-P1-dist', 'rms', 'noise']):
169 if np.all(data[np.isfinite(data)] >= 0.0):
170 if axis == 'x':
171 ax.set_xlim(0.0, None)
172 elif axis == 'y':
173 ax.set_ylim(0.0, None)
174 elif axis == 'c':
175 return 0.0, np.max(data), None
176 else:
177 if axis == 'x':
178 ax.set_xlim(None, 0.0)
179 elif axis == 'y':
180 ax.set_ylim(None, 0.0)
181 elif axis == 'c':
182 return np.min(data), 0.0, None
183 elif 'phase' in label:
184 if axis == 'x':
185 ax.set_xlim(-np.pi, np.pi)
186 ax.set_xticks(np.arange(-np.pi, 1.5*np.pi, 0.5*np.pi))
187 ax.set_xticklabels([u'-\u03c0', u'-\u03c0/2', '0', u'\u03c0/2', u'\u03c0'])
188 elif axis == 'y':
189 ax.set_ylim(-np.pi, np.pi)
190 ax.set_yticks(np.arange(-np.pi, 1.5*np.pi, 0.5*np.pi))
191 ax.set_yticklabels([u'-\u03c0', u'-\u03c0/2', '0', u'\u03c0/2', u'\u03c0'])
192 elif axis == 'c':
193 if ax is not None:
194 ax.set_yticklabels([u'-\u03c0', u'-\u03c0/2', '0', u'\u03c0/2', u'\u03c0'])
195 return -np.pi, np.pi, np.arange(-np.pi, 1.5*np.pi, 0.5*np.pi)
196 elif 'species' in label:
197 if axis == 'x':
198 for label in ax.get_xticklabels():
199 label.set_rotation(30)
200 ax.set_xlabel('')
201 ax.set_xlim(np.min(data)-0.5, np.max(data)+0.5)
202 elif axis == 'y':
203 ax.set_ylabel('')
204 ax.set_ylim(np.min(data)-0.5, np.max(data)+0.5)
205 elif axis == 'c':
206 if ax is not None:
207 ax.set_ylabel('')
208 return np.min(data), np.max(data), None
211 def fix_waveform_plot(self, axs, indices):
212 """Adapt waveform plots to EOD waveforms, derivatives, and spectra.
213 """
214 if len(indices) == 0:
215 axs[0].text(0.5, 0.5, 'Click to plot EOD waveforms',
216 transform = axs[0].transAxes, ha='center', va='center')
217 axs[0].text(0.5, 0.3, 'n = %d' % len(self.raw_data),
218 transform = axs[0].transAxes, ha='center', va='center')
219 elif len(indices) == 1:
220 file_name = self.eoddata[indices[0],'file'] if 'file' in self.eoddata else basename
221 if 'index' in self.eoddata and np.isfinite(self.eoddata[indices[0],'index']) and \
222 np.any(self.eoddata[:,'index'] != self.eoddata[0,'index']):
223 axs[0].set_title('%s: %d' % (file_name,
224 self.eoddata[indices[0],'index']))
225 else:
226 axs[0].set_title(file_name)
227 if np.isfinite(self.eoddata[indices[0],'index']):
228 axs[0].text(0.05, 0.85, '%.1fHz' % self.eoddata[indices[0],'EODf'],
229 transform = axs[0].transAxes)
230 else:
231 axs[0].set_title('%d EOD waveforms selected' % len(indices))
232 for ax in axs:
233 for l in ax.lines:
234 l.set_linewidth(3.0)
235 for ax, xl in zip(axs, self.wave_ylabels):
236 if 'Voltage' in xl:
237 ax.set_ylim(top=1.1)
238 ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=4))
239 if 'dV/dt' in xl:
240 ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=4))
241 if 'd^2V/dt^2' in xl:
242 ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=4))
243 if self.wave_fish:
244 for ax, xl in zip(axs, self.wave_ylabels):
245 if 'Voltage' in xl:
246 ax.set_xlim(-0.7, 0.7)
247 if 'Ampl' in xl or 'Power' in xl or 'Phase' in xl:
248 ax.set_xlim(-0.5, 8.5)
249 for l in ax.lines:
250 l.set_marker('.')
251 l.set_markersize(15.0)
252 l.set_markeredgewidth(0.5)
253 l.set_markeredgecolor('k')
254 l.set_markerfacecolor(l.get_color())
255 if 'Ampl' in xl:
256 ax.set_ylim(0.0, 100.0)
257 ax.yaxis.set_major_locator(ticker.MultipleLocator(25.0))
258 if 'Power' in xl:
259 ax.set_ylim(-60.0, 2.0)
260 ax.yaxis.set_major_locator(ticker.MultipleLocator(20.0))
261 if 'Phase' in xl:
262 ax.set_ylim(0.0, 2.0*np.pi)
263 ax.set_yticks(np.arange(0.0, 2.5*np.pi, 0.5*np.pi))
264 ax.set_yticklabels(['0', u'\u03c0/2', u'\u03c0', u'3\u03c0/2', u'2\u03c0'])
265 else:
266 for ax, xl in zip(axs, self.wave_ylabels):
267 if 'Voltage' in xl:
268 ax.set_xlim(-1.0, 1.5)
269 if 'Power' in xl:
270 ax.set_xlim(1.0, 2000.0)
271 ax.set_xscale('log')
272 ax.set_ylim(-60.0, 2.0)
273 ax.yaxis.set_major_locator(ticker.MultipleLocator(20.0))
274 if len(indices) > 0:
275 for ax in axs:
276 ax.axhline(c='k', lw=1)
279 def list_selection(self, indices):
280 """List file names and indices of selection.
282 If only a single EOD is selected, list all of its properties.
283 """
284 print('')
285 print('selected EODs:')
286 if 'index' in self.eoddata and \
287 np.any(self.eoddata[:,'index'] != self.eoddata[0,'index']):
288 for i in indices:
289 file_name = self.eoddata[i,'file'] if 'file' in self.eoddata else basename
290 if np.isfinite(self.eoddata[i,'index']):
291 print('%s : %d' % (file_name, self.eoddata[i,'index']))
292 else:
293 print(file_name)
294 elif 'file' in self.eoddata:
295 for i in indices:
296 print(self.eoddata[i,'file'])
297 if len(indices) == 1:
298 # write eoddata line on terminal:
299 keylen = 0
300 keys = []
301 values = []
302 for c in range(self.eoddata.columns()):
303 k, v = self.eoddata.key_value(indices[0], c)
304 keys.append(k)
305 values.append(v)
306 if keylen < len(k):
307 keylen = len(k)
308 for k, v in zip(keys, values):
309 fs = '%%-%ds: %%s' % keylen
310 print(fs % (k, v.strip()))
313 def analyze_selection(self, index):
314 """Launch thunderfish on the selected EOD.
315 """
316 # load data:
317 file_base = self.eoddata[index,'file'] if 'file' in self.eoddata else basename
318 bp = os.path.join(self.path, file_base)
319 fn = glob.glob(bp + '.*')
320 if len(fn) == 0:
321 print('no recording found for %s' % bp)
322 return
323 recording = fn[0]
324 channel = 0
325 try:
326 raw_data, rate, unit, ampl_max = load_data(recording)
327 raw_data = raw_data[:,channel]
328 except IOError as e:
329 print('%s: failed to open file: did you provide a path to the raw data (-P option)?' % (recording))
330 return
331 if len(raw_data) <= 1:
332 print('%s: empty data file' % recording)
333 return
334 # load configuration:
335 cfgfile = __package__ + '.cfg'
336 cfg = configuration(cfgfile, False, recording)
337 cfg.load_files(cfgfile, recording, 4)
338 if 'flipped' in self.eoddata:
339 fs = 'flip' if self.eoddata[index,'flipped'] else 'none'
340 cfg.set('flipWaveEOD', fs)
341 cfg.set('flipPulseEOD', fs)
342 # best_window:
343 data, idx0, idx1, clipped, min_clip, max_clip = \
344 analysis_window(raw_data, rate, ampl_max,
345 cfg.value('windowPosition'), cfg)
346 # detect EODs in the data:
347 psd_data, fishlist, _, eod_props, mean_eods, \
348 spec_data, peak_data, pulse_data, power_thresh, skip_reason, zoom_window = \
349 detect_eods(data, rate, min_clip, max_clip, recording, 0, 0, cfg)
350 # plot EOD:
351 idx = int(self.eoddata[index,'index']) if 'index' in self.eoddata else 0
352 for k in ['toolbar', 'keymap.back', 'keymap.forward',
353 'keymap.zoom', 'keymap.pan']:
354 plt.rcParams[k] = self.plt_params[k]
355 fig = plot_eods(file_base, None, raw_data, rate, None, idx0, idx1,
356 clipped, psd_data[0], fishlist, None,
357 mean_eods, eod_props, peak_data, pulse_data, spec_data,
358 [idx], unit, zoom_window, 10, None, True, False,
359 'auto', False, 0.0, 3000.0,
360 interactive=True, verbose=0)
361 fig.canvas.manager.set_window_title('thunderfish: %s' % file_base)
362 plt.show(block=False)
365 """Names of groups of data columns that can be selected by the select_EOD_properties() function.
366 """
367 groups = ['all', 'allpower', 'noise', 'timing', 'wave',
368 'ampl', 'relampl', 'power', 'relpower', 'phase',
369 'time', 'width', 'peaks', 'none']
371 @staticmethod
372 def select_EOD_properties(data, wave_fish, max_n, column_groups, add_columns):
373 """Select data columns to be explored.
375 First, groups of columns are selected, then individual
376 columns. Columns that are selected twice are removed from the
377 selection.
379 Parameters
380 ----------
381 data: TableData
382 Table with EOD properties from which columns are selected.
383 wave_fish: boolean.
384 Indicates if data contains properties of wave- or pulse-type electric fish.
385 max_n: int
386 Maximum number of harmonics (wave-type fish) or peaks (pulse-type fish)
387 to be selected.
388 column_groups: list of string
389 List of name denoting groups of columns to be selected. Supported groups are
390 listed in `EODExplor.groups`.
391 add_columns: list of string or int
392 List of further individual columns to be selected.
394 Returns
395 -------
396 data_cols: list of int
397 Indices of data columns to be shown by EODExplorer.
398 error: string
399 In case of an invalid column group, an error string.
400 """
401 if wave_fish:
402 # maximum number of harmonics:
403 if max_n == 0:
404 max_n = 100
405 else:
406 max_n += 1
407 for k in range(1, max_n):
408 if not ('phase%d' % k) in data:
409 max_n = k
410 break
411 else:
412 # minimum number of peaks:
413 min_peaks = -10
414 for k in range(1, min_peaks, -1):
415 if not ('P%dampl' % k) in data or not np.all(np.isfinite(data[:,'P%dampl' % k])):
416 min_peaks = k+1
417 break
418 # maximum number of peaks:
419 if max_n == 0:
420 max_peaks = 20
421 else:
422 max_peaks = max_n + 1
423 for k in range(1, max_peaks):
424 if not ('P%dampl' % k) in data or not np.all(np.isfinite(data[:,'P%dampl' % k])):
425 max_peaks = k
426 break
428 # default columns:
429 group_cols = ['EODf']
430 if 'EODf_adjust' in data:
431 group_cols.append('EODf_adjust')
432 if len(column_groups) == 0:
433 column_groups = ['all']
434 for group in column_groups:
435 if group == 'none':
436 group_cols = []
437 elif wave_fish:
438 if group == 'noise':
439 group_cols.extend(['noise', 'rmserror', 'power', 'thd',
440 'dbdiff', 'maxdb', 'p-p-amplitude',
441 'relampl1', 'relampl2', 'relampl3'])
442 elif group == 'timing' or group == 'time':
443 group_cols.extend(['peakwidth', 'troughwidth', 'p-p-distance',
444 'leftpeak', 'rightpeak', 'lefttrough', 'righttrough'])
445 elif group == 'wave':
446 group_cols.extend(['thd', 'minwidth', 'min-p-p-distance',
447 'relpeakampl'])
448 elif group == 'ampl':
449 for k in range(0, max_n):
450 group_cols.append('ampl%d' % k)
451 elif group == 'relampl':
452 group_cols.append('thd')
453 group_cols.append('relpeakampl')
454 for k in range(1, max_n):
455 group_cols.append('relampl%d' % k)
456 elif group == 'relpower' or group == 'power':
457 for k in range(1, max_n):
458 group_cols.append('relpower%d' % k)
459 elif group == 'phase':
460 for k in range(0, max_n):
461 group_cols.append('phase%d' % k)
462 elif group == 'all':
463 group_cols.extend(['thd', 'minwidth', 'min-p-p-distance',
464 'relpeakampl'])
465 for k in range(1, max_n):
466 group_cols.append('relampl%d' % k)
467 group_cols.append('phase%d' % k)
468 elif group == 'allpower':
469 group_cols.append('thd')
470 for k in range(1, max_n):
471 group_cols.append('relpower%d' % k)
472 group_cols.append('phase%d' % k)
473 else:
474 return None, '"%s" is not a valid data group for wavefish' % group
475 else: # pulse fish
476 if group == 'noise':
477 group_cols.extend(['noise', 'p-p-amplitude', 'min-ampl', 'max-ampl'])
478 elif group == 'timing':
479 group_cols.extend(['tstart', 'tend', 'width', 'tau', 'P2-P1-dist', 'firstpeak', 'lastpeak'])
480 elif group == 'power':
481 group_cols.extend(['peakfreq', 'peakpower', 'poweratt5', 'poweratt50', 'lowcutoff'])
482 elif group == 'time':
483 for k in range(min_peaks, max_peaks):
484 if k != 1:
485 group_cols.append('P%dtime' % k)
486 elif group == 'ampl':
487 for k in range(min_peaks, max_peaks):
488 group_cols.append('P%dampl' % k)
489 elif group == 'relampl':
490 for k in range(min_peaks, max_peaks):
491 if k != 1:
492 group_cols.append('P%drelampl' % k)
493 elif group == 'width':
494 for k in range(min_peaks, max_peaks):
495 group_cols.append('P%dwidth' % k)
496 elif group == 'peaks':
497 group_cols.append('firstpeak')
498 group_cols.append('lastpeak')
499 elif group == 'all':
500 group_cols.extend(['firstpeak', 'lastpeak'])
501 for k in range(min_peaks, max_peaks):
502 if k != 1:
503 group_cols.append('P%drelampl' % k)
504 group_cols.append('P%dtime' % k)
505 group_cols.append('P%dwidth' % k)
506 group_cols.extend(['tau', 'P2-P1-dist', 'peakfreq', 'poweratt5'])
507 else:
508 return None, '"%s" is not a valid data group for pulsefish' % group
509 # additional data columns:
510 group_cols.extend(add_columns)
511 # translate to indices:
512 data_cols = select_features(data, group_cols)
513 return data_cols, None
516wave_fish = True
517load_spec = False
518data = None
519data_path = None
521def load_waveform(idx):
522 eodf = data[idx,'EODf']
523 if not np.isfinite(eodf):
524 if load_spec:
525 return None, None
526 else:
527 return None
528 file_name = data[idx,'file'] if 'file' in data else '-'.join(basename.split('-')[:-1])
529 file_index = data[idx,'index'] if 'index' in data else 0
530 eod_filename = os.path.join(data_path, '%s-eodwaveform-%d.csv' % (file_name, file_index))
531 eod_table = TableData(eod_filename)
532 eod = eod_table[:,'mean']
533 norm = np.max(eod)
534 if wave_fish:
535 eod = np.column_stack((eod_table[:,'time']*0.001*eodf, eod/norm))
536 else:
537 eod = np.column_stack((eod_table[:,'time'], eod/norm))
538 if not load_spec:
539 return eod
540 fish_type = 'wave' if wave_fish else 'pulse'
541 spec_table = TableData(os.path.join(data_path, '%s-%sspectrum-%d.csv' % (file_name, fish_type, file_index)))
542 spec_data = spec_table.array()
543 if not wave_fish:
544 spec_data = spec_data[spec_data[:,0]<2000.0,:]
545 spec_data = spec_data[::5,:]
546 return (eod, spec_data)
549def main(cargs=None):
550 global data
551 global wave_fish
552 global load_spec
553 global data_path
554 global basename
556 # command line arguments:
557 if cargs is None:
558 cargs = sys.argv[1:]
559 parser = argparse.ArgumentParser(add_help=False,
560 description='View and explore properties of EOD waveforms.',
561 epilog='version %s by Benda-Lab (2019-%s)' % (__version__, __year__))
562 parser.add_argument('-h', '--help', nargs=0, action=PrintHelp,
563 help='show this help message and exit')
564 parser.add_argument('--version', action='version', version=__version__)
565 parser.add_argument('-v', dest='verbose', action='store_true', default=False,
566 help='verbose output')
567 parser.add_argument('-l', dest='list_columns', action='store_true',
568 help='list all available data columns and exit')
569 parser.add_argument('-j', dest='jobs', nargs='?', type=int, default=None, const=0,
570 help='number of jobs run in parallel for loading waveform data. Without argument use all CPU cores.')
571 parser.add_argument('-D', dest='column_groups', default=[], type=str, action='append',
572 choices=EODExplorer.groups,
573 help='default selection of data columns, check them with the -l option')
574 parser.add_argument('-d', dest='add_data_cols', action='append', default=[], metavar='COLUMN',
575 help='data columns to be appended or removed (if already listed) for analysis')
576 parser.add_argument('-n', dest='max_n', default=0, type=int, metavar='MAX',
577 help='maximum number of harmonics or peaks to be used')
578 parser.add_argument('-w', dest='add_waveforms', default=[], type=str, action='append',
579 choices=['first', 'second', 'ampl', 'power', 'phase'],
580 help='add first or second derivative of EOD waveform, or relative amplitude, power, or phase to the plot of selected EODs.')
581 parser.add_argument('-s', dest='save_pca', action='store_true',
582 help='save PCA components and exit')
583 parser.add_argument('-c', dest='color_col', default='EODf', type=str, metavar='COLUMN',
584 help='data column to be used for color code or "row"')
585 parser.add_argument('-m', dest='color_map', default='jet', type=str, metavar='CMAP',
586 help='name of color map')
587 parser.add_argument('-p', dest='data_path', default='.', type=str, metavar='PATH',
588 help='path to the analyzed EOD waveform data')
589 parser.add_argument('-P', dest='rawdata_path', default='.', type=str, metavar='PATH',
590 help='path to the raw EOD recordings')
591 parser.add_argument('-f', dest='format', default='auto', type=str,
592 choices=TableData.formats + ['same'],
593 help='file format used for saving PCA data ("same" uses same format as input file)')
594 parser.add_argument('file', default='', type=str,
595 help='a wavefish.* or pulsefish.* summary file as generated by collectfish')
596 args = parser.parse_args(cargs)
598 # read in command line arguments:
599 verbose = args.verbose
600 list_columns = args.list_columns
601 jobs = args.jobs
602 file_name = args.file
603 column_groups = args.column_groups
604 add_data_cols = args.add_data_cols
605 max_n = args.max_n
606 add_waveforms = args.add_waveforms
607 save_pca = args.save_pca
608 color_col = args.color_col
609 color_map = args.color_map
610 data_path = args.data_path
611 rawdata_path = args.rawdata_path
612 data_format = args.format
614 # read configuration:
615 cfgfile = __package__ + '.cfg'
616 cfg = ConfigFile()
617 add_eod_quality_config(cfg)
618 add_harmonic_groups_config(cfg)
619 add_species_config(cfg)
620 add_write_table_config(cfg, table_format='csv', unit_style='row',
621 align_columns=True, shrink_width=False)
622 cfg.load_files(cfgfile, file_name, 4)
624 # output format:
625 if data_format == 'same':
626 ext = os.path.splitext(file_name)[1][1:]
627 if ext in TableData.ext_formats:
628 data_format = TableData.ext_formats[ext]
629 else:
630 data_format = 'dat'
631 if data_format != 'auto':
632 cfg.set('fileFormat', data_format)
634 # check color map:
635 if not color_map in plt.colormaps():
636 parser.error('"%s" is not a valid color map' % color_map)
638 # load summary data:
639 wave_fish = 'wave' in file_name
640 data = TableData(file_name)
642 # basename:
643 basename = os.path.splitext(os.path.basename(file_name))[0]
645 # check quality:
646 skipped = 0
647 for r in reversed(range(data.rows())):
648 idx = 0
649 if 'index' in data:
650 idx = data[r,'index']
651 skips = ''
652 if wave_fish:
653 harm_rampl = np.array([0.01*data[r,'relampl%d'%(k+1)] for k in range(3)
654 if 'relampl%d'%(k+1) in data])
655 props = data.row_dict(r)
656 if 'clipped' in props:
657 props['clipped'] *= 0.01
658 if 'noise' in props:
659 props['noise'] *= 0.01
660 if 'rmserror' in props:
661 props['rmserror'] *= 0.01
662 if 'thd' in props:
663 props['thd'] *= 0.01
664 _, skips, msg = wave_quality(props, harm_rampl, **wave_quality_args(cfg))
665 else:
666 props = data.row_dict(r)
667 if 'clipped' in props:
668 props['clipped'] *= 0.01
669 if 'noise' in props:
670 props['noise'] *= 0.01
671 skips, msg, _ = pulse_quality(props, **pulse_quality_args(cfg))
672 if len(skips) > 0:
673 if verbose:
674 print('skip fish %2d from %s: %s' % (idx, data[r,'file'] if 'file' in data else basename, skips))
675 del data[r,:]
676 skipped += 1
677 if verbose and skipped > 0:
678 print('')
680 # select columns (EOD properties) to be shown:
681 data_cols, error = \
682 EODExplorer.select_EOD_properties(data, wave_fish, max_n,
683 column_groups, add_data_cols)
684 if error:
685 parser.error(error)
687 # select column used for coloring the data:
688 colors, color_label, color_idx, error = \
689 select_coloring(data, data_cols, color_col)
690 if error:
691 parser.error(error)
693 # list columns:
694 if list_columns:
695 list_available_features(data, data_cols, color_idx)
696 parser.exit()
698 # load waveforms:
699 load_spec = 'ampl' in add_waveforms or 'power' in add_waveforms or 'phase' in add_waveforms
700 if jobs is not None:
701 cpus = cpu_count() if jobs == 0 else jobs
702 p = Pool(cpus)
703 eod_data = p.map(load_waveform, range(data.rows()))
704 del p
705 else:
706 eod_data = list(map(load_waveform, range(data.rows())))
708 # explore:
709 eod_expl = EODExplorer(data, data_cols, wave_fish, eod_data,
710 add_waveforms, load_spec, rawdata_path)
711 # write pca:
712 if save_pca:
713 eod_expl.compute_pca(False)
714 eod_expl.save_pca(basename, False, **write_table_args(cfg))
715 eod_expl.compute_pca(True)
716 eod_expl.save_pca(basename, True, **write_table_args(cfg))
717 else:
718 eod_expl.set_colors(colors, color_label, color_map)
719 eod_expl.show()
722if __name__ == '__main__':
723 freeze_support() # needed by multiprocessing for some weired windows stuff
724 main()