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

1"""View and explore properties of EOD waveforms. 

2""" 

3 

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 

28 

29 

30basename = '' 

31 

32 

33class EODExplorer(MultivariateExplorer): 

34 """Simple GUI for viewing and exploring properties of EOD waveforms. 

35 

36 EODExplorer adapts a MultivariateExplorer to specific needs of EODs. 

37 

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 """ 

43 

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) 

154 

155 

156 def fix_scatter_plot(self, ax, data, label, axis): 

157 """Customize an axes of a scatter plot. 

158 

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 

207 

208 

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) 

275 

276 

277 def list_selection(self, indices): 

278 """List file names and indices of selection. 

279 

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())) 

309 

310 

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) 

361 

362 

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'] 

368 

369 @staticmethod 

370 def select_EOD_properties(data, wave_fish, max_n, column_groups, add_columns): 

371 """Select data columns to be explored. 

372 

373 First, groups of columns are selected, then individual 

374 columns. Columns that are selected twice are removed from the 

375 selection. 

376 

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. 

391 

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 

425 

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 

509 

510 

511wave_fish = True 

512load_spec = False 

513data = None 

514data_path = None 

515 

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) 

542 

543 

544def main(cargs=None): 

545 global data 

546 global wave_fish 

547 global load_spec 

548 global data_path 

549 global basename 

550 

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) 

592 

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 

608 

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) 

618 

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) 

628 

629 # check color map: 

630 if not color_map in plt.colormaps(): 

631 parser.error('"%s" is not a valid color map' % color_map) 

632 

633 # load summary data: 

634 wave_fish = 'wave' in file_name 

635 data = TableData(file_name) 

636 

637 # basename: 

638 basename = os.path.splitext(os.path.basename(file_name))[0] 

639 

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('') 

674 

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) 

681 

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) 

687 

688 # list columns: 

689 if list_columns: 

690 list_available_features(data, data_cols, color_idx) 

691 parser.exit() 

692 

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()))) 

702 

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() 

715 

716 

717if __name__ == '__main__': 

718 freeze_support() # needed by multiprocessing for some weired windows stuff 

719 main()