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