Coverage for src/thunderfish/eodexplorer.py: 0%
481 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"""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, samplerate, 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, samplerate, 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, samplerate, 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, samplerate, 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',
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 == 'ampl':
444 for k in range(0, max_n):
445 group_cols.append('ampl%d' % k)
446 elif group == 'relampl':
447 group_cols.append('thd')
448 group_cols.append('relpeakampl')
449 for k in range(1, max_n):
450 group_cols.append('relampl%d' % k)
451 elif group == 'relpower' or group == 'power':
452 for k in range(1, max_n):
453 group_cols.append('relpower%d' % k)
454 elif group == 'phase':
455 for k in range(0, max_n):
456 group_cols.append('phase%d' % k)
457 elif group == 'all':
458 group_cols.append('thd')
459 group_cols.append('relpeakampl')
460 for k in range(1, max_n):
461 group_cols.append('relampl%d' % k)
462 group_cols.append('phase%d' % k)
463 elif group == 'allpower':
464 group_cols.append('thd')
465 for k in range(1, max_n):
466 group_cols.append('relpower%d' % k)
467 group_cols.append('phase%d' % k)
468 else:
469 return None, '"%s" is not a valid data group for wavefish' % group
470 else: # pulse fish
471 if group == 'noise':
472 group_cols.extend(['noise', 'p-p-amplitude', 'min-ampl', 'max-ampl'])
473 elif group == 'timing':
474 group_cols.extend(['tstart', 'tend', 'width', 'tau', 'P2-P1-dist', 'firstpeak', 'lastpeak'])
475 elif group == 'power':
476 group_cols.extend(['peakfreq', 'peakpower', 'poweratt5', 'poweratt50', 'lowcutoff'])
477 elif group == 'time':
478 for k in range(min_peaks, max_peaks):
479 if k != 1:
480 group_cols.append('P%dtime' % k)
481 elif group == 'ampl':
482 for k in range(min_peaks, max_peaks):
483 group_cols.append('P%dampl' % k)
484 elif group == 'relampl':
485 for k in range(min_peaks, max_peaks):
486 if k != 1:
487 group_cols.append('P%drelampl' % k)
488 elif group == 'width':
489 for k in range(min_peaks, max_peaks):
490 group_cols.append('P%dwidth' % k)
491 elif group == 'peaks':
492 group_cols.append('firstpeak')
493 group_cols.append('lastpeak')
494 elif group == 'all':
495 group_cols.extend(['firstpeak', 'lastpeak'])
496 for k in range(min_peaks, max_peaks):
497 if k != 1:
498 group_cols.append('P%drelampl' % k)
499 group_cols.append('P%dtime' % k)
500 group_cols.append('P%dwidth' % k)
501 group_cols.extend(['tau', 'P2-P1-dist', 'peakfreq', 'poweratt5'])
502 else:
503 return None, '"%s" is not a valid data group for pulsefish' % group
504 # additional data columns:
505 group_cols.extend(add_columns)
506 # translate to indices:
507 data_cols = select_features(data, group_cols)
508 return data_cols, None
511wave_fish = True
512load_spec = False
513data = None
514data_path = None
516def load_waveform(idx):
517 eodf = data[idx,'EODf']
518 if not np.isfinite(eodf):
519 if load_spec:
520 return None, None
521 else:
522 return None
523 file_name = data[idx,'file'] if 'file' in data else '-'.join(basename.split('-')[:-1])
524 file_index = data[idx,'index'] if 'index' in data else 0
525 eod_filename = os.path.join(data_path, '%s-eodwaveform-%d.csv' % (file_name, file_index))
526 eod_table = TableData(eod_filename)
527 eod = eod_table[:,'mean']
528 norm = np.max(eod)
529 if wave_fish:
530 eod = np.column_stack((eod_table[:,'time']*0.001*eodf, eod/norm))
531 else:
532 eod = np.column_stack((eod_table[:,'time'], eod/norm))
533 if not load_spec:
534 return eod
535 fish_type = 'wave' if wave_fish else 'pulse'
536 spec_table = TableData(os.path.join(data_path, '%s-%sspectrum-%d.csv' % (file_name, fish_type, file_index)))
537 spec_data = spec_table.array()
538 if not wave_fish:
539 spec_data = spec_data[spec_data[:,0]<2000.0,:]
540 spec_data = spec_data[::5,:]
541 return (eod, spec_data)
544def main(cargs=None):
545 global data
546 global wave_fish
547 global load_spec
548 global data_path
549 global basename
551 # command line arguments:
552 if cargs is None:
553 cargs = sys.argv[1:]
554 parser = argparse.ArgumentParser(add_help=False,
555 description='View and explore properties of EOD waveforms.',
556 epilog='version %s by Benda-Lab (2019-%s)' % (__version__, __year__))
557 parser.add_argument('-h', '--help', nargs=0, action=PrintHelp,
558 help='show this help message and exit')
559 parser.add_argument('--version', action='version', version=__version__)
560 parser.add_argument('-v', dest='verbose', action='store_true', default=False,
561 help='verbose output')
562 parser.add_argument('-l', dest='list_columns', action='store_true',
563 help='list all available data columns and exit')
564 parser.add_argument('-j', dest='jobs', nargs='?', type=int, default=None, const=0,
565 help='number of jobs run in parallel for loading waveform data. Without argument use all CPU cores.')
566 parser.add_argument('-D', dest='column_groups', default=[], type=str, action='append',
567 choices=EODExplorer.groups,
568 help='default selection of data columns, check them with the -l option')
569 parser.add_argument('-d', dest='add_data_cols', action='append', default=[], metavar='COLUMN',
570 help='data columns to be appended or removed (if already listed) for analysis')
571 parser.add_argument('-n', dest='max_n', default=0, type=int, metavar='MAX',
572 help='maximum number of harmonics or peaks to be used')
573 parser.add_argument('-w', dest='add_waveforms', default=[], type=str, action='append',
574 choices=['first', 'second', 'ampl', 'power', 'phase'],
575 help='add first or second derivative of EOD waveform, or relative amplitude, power, or phase to the plot of selected EODs.')
576 parser.add_argument('-s', dest='save_pca', action='store_true',
577 help='save PCA components and exit')
578 parser.add_argument('-c', dest='color_col', default='EODf', type=str, metavar='COLUMN',
579 help='data column to be used for color code or "row"')
580 parser.add_argument('-m', dest='color_map', default='jet', type=str, metavar='CMAP',
581 help='name of color map')
582 parser.add_argument('-p', dest='data_path', default='.', type=str, metavar='PATH',
583 help='path to the analyzed EOD waveform data')
584 parser.add_argument('-P', dest='rawdata_path', default='.', type=str, metavar='PATH',
585 help='path to the raw EOD recordings')
586 parser.add_argument('-f', dest='format', default='auto', type=str,
587 choices=TableData.formats + ['same'],
588 help='file format used for saving PCA data ("same" uses same format as input file)')
589 parser.add_argument('file', default='', type=str,
590 help='a wavefish.* or pulsefish.* summary file as generated by collectfish')
591 args = parser.parse_args(cargs)
593 # read in command line arguments:
594 verbose = args.verbose
595 list_columns = args.list_columns
596 jobs = args.jobs
597 file_name = args.file
598 column_groups = args.column_groups
599 add_data_cols = args.add_data_cols
600 max_n = args.max_n
601 add_waveforms = args.add_waveforms
602 save_pca = args.save_pca
603 color_col = args.color_col
604 color_map = args.color_map
605 data_path = args.data_path
606 rawdata_path = args.rawdata_path
607 data_format = args.format
609 # read configuration:
610 cfgfile = __package__ + '.cfg'
611 cfg = ConfigFile()
612 add_eod_quality_config(cfg)
613 add_harmonic_groups_config(cfg)
614 add_species_config(cfg)
615 add_write_table_config(cfg, table_format='csv', unit_style='row',
616 align_columns=True, shrink_width=False)
617 cfg.load_files(cfgfile, file_name, 4)
619 # output format:
620 if data_format == 'same':
621 ext = os.path.splitext(file_name)[1][1:]
622 if ext in TableData.ext_formats:
623 data_format = TableData.ext_formats[ext]
624 else:
625 data_format = 'dat'
626 if data_format != 'auto':
627 cfg.set('fileFormat', data_format)
629 # check color map:
630 if not color_map in plt.colormaps():
631 parser.error('"%s" is not a valid color map' % color_map)
633 # load summary data:
634 wave_fish = 'wave' in file_name
635 data = TableData(file_name)
637 # basename:
638 basename = os.path.splitext(os.path.basename(file_name))[0]
640 # check quality:
641 skipped = 0
642 for r in reversed(range(data.rows())):
643 idx = 0
644 if 'index' in data:
645 idx = data[r,'index']
646 skips = ''
647 if wave_fish:
648 harm_rampl = np.array([0.01*data[r,'relampl%d'%(k+1)] for k in range(3)
649 if 'relampl%d'%(k+1) in data])
650 props = data.row_dict(r)
651 if 'clipped' in props:
652 props['clipped'] *= 0.01
653 if 'noise' in props:
654 props['noise'] *= 0.01
655 if 'rmserror' in props:
656 props['rmserror'] *= 0.01
657 if 'thd' in props:
658 props['thd'] *= 0.01
659 _, skips, msg = wave_quality(props, harm_rampl, **wave_quality_args(cfg))
660 else:
661 props = data.row_dict(r)
662 if 'clipped' in props:
663 props['clipped'] *= 0.01
664 if 'noise' in props:
665 props['noise'] *= 0.01
666 skips, msg, _ = pulse_quality(props, **pulse_quality_args(cfg))
667 if len(skips) > 0:
668 if verbose:
669 print('skip fish %2d from %s: %s' % (idx, data[r,'file'] if 'file' in data else basename, skips))
670 del data[r,:]
671 skipped += 1
672 if verbose and skipped > 0:
673 print('')
675 # select columns (EOD properties) to be shown:
676 data_cols, error = \
677 EODExplorer.select_EOD_properties(data, wave_fish, max_n,
678 column_groups, add_data_cols)
679 if error:
680 parser.error(error)
682 # select column used for coloring the data:
683 colors, color_label, color_idx, error = \
684 select_coloring(data, data_cols, color_col)
685 if error:
686 parser.error(error)
688 # list columns:
689 if list_columns:
690 list_available_features(data, data_cols, color_idx)
691 parser.exit()
693 # load waveforms:
694 load_spec = 'ampl' in add_waveforms or 'power' in add_waveforms or 'phase' in add_waveforms
695 if jobs is not None:
696 cpus = cpu_count() if jobs == 0 else jobs
697 p = Pool(cpus)
698 eod_data = p.map(load_waveform, range(data.rows()))
699 del p
700 else:
701 eod_data = list(map(load_waveform, range(data.rows())))
703 # explore:
704 eod_expl = EODExplorer(data, data_cols, wave_fish, eod_data,
705 add_waveforms, load_spec, rawdata_path)
706 # write pca:
707 if save_pca:
708 eod_expl.compute_pca(False)
709 eod_expl.save_pca(basename, False, **write_table_args(cfg))
710 eod_expl.compute_pca(True)
711 eod_expl.save_pca(basename, True, **write_table_args(cfg))
712 else:
713 eod_expl.set_colors(colors, color_label, color_map)
714 eod_expl.show()
717if __name__ == '__main__':
718 freeze_support() # needed by multiprocessing for some weired windows stuff
719 main()