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

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

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', 'wave', 

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

512 

513 

514wave_fish = True 

515load_spec = False 

516data = None 

517data_path = None 

518 

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) 

545 

546 

547def main(cargs=None): 

548 global data 

549 global wave_fish 

550 global load_spec 

551 global data_path 

552 global basename 

553 

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) 

595 

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 

611 

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) 

621 

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) 

631 

632 # check color map: 

633 if not color_map in plt.colormaps(): 

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

635 

636 # load summary data: 

637 wave_fish = 'wave' in file_name 

638 data = TableData(file_name) 

639 

640 # basename: 

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

642 

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

677 

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) 

684 

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) 

690 

691 # list columns: 

692 if list_columns: 

693 list_available_features(data, data_cols, color_idx) 

694 parser.exit() 

695 

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

705 

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

718 

719 

720if __name__ == '__main__': 

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

722 main()