Coverage for src / thunderfish / eodexplorer.py: 0%

482 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-15 17:50 +0000

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 

12 

13from multiprocessing import Pool, freeze_support, cpu_count 

14from thunderlab.configfile import ConfigFile 

15from thunderlab.tabledata import TableData, add_write_table_config, write_table_args 

16from thunderlab.dataloader import load_data 

17from thunderlab.multivariateexplorer import MultivariateExplorer 

18from thunderlab.multivariateexplorer import select_features, select_coloring 

19from thunderlab.multivariateexplorer import list_available_features 

20from thunderlab.multivariateexplorer import PrintHelp 

21from thunderlab.powerspectrum import decibel 

22 

23from .version import __version__, __year__ 

24from .harmonics import add_harmonic_groups_config 

25from .eodanalysis import add_species_config 

26from .eodanalysis import wave_quality, wave_quality_args, add_eod_quality_config 

27from .eodanalysis import pulse_quality, pulse_quality_args 

28from .bestwindow import analysis_window 

29from .thunderfish import configuration, detect_eods, plot_eods 

30 

31 

32basename = '' 

33 

34 

35class EODExplorer(MultivariateExplorer): 

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

37 

38 EODExplorer adapts a MultivariateExplorer to specific needs of EODs. 

39 

40 Static members 

41 -------------- 

42 - `groups`: names of groups of data columns that can be selected. 

43 - `select_EOD_properties()`: select data columns to be explored. 

44 """ 

45 

46 def __init__(self, data, data_cols, wave_fish, eod_data, 

47 add_waveforms, loaded_spec, rawdata_path): 

48 """ 

49 Parameters 

50 ---------- 

51 data: TableData 

52 Full table of EOD properties. Each row is a fish. 

53 data_cols: list of string or ints 

54 Names or indices of columns in `data` to be explored. 

55 You may use the static function `select_EOD_properties()` 

56 for assisting the selection of columns. 

57 wave_fish: boolean 

58 True if data are about wave-type weakly electric fish. 

59 False if data are about pulse-type weakly electric fish. 

60 eod_data: list of waveform data 

61 Either waveform data is only the EOD waveform, 

62 a ndarray of shape (time, ['time', 'voltage']), or 

63 it is a list with the first element being the EOD waveform, 

64 and the second element being a 2D ndarray of spectral properties 

65 of the EOD waveform with first column being the frequency or harmonics. 

66 add_waveforms: list of string 

67 List of what should be shown as waveform. Elements can be 

68 'first', 'second', 'ampl', 'power', or 'phase'. For 'first' and 'second' 

69 the first and second derivatives of the supplied EOD waveform a computed and shown. 

70 'ampl', 'power', and 'phase' select properties of the provided spectral properties. 

71 loaded_spec: boolean 

72 Indicates whether eod_data contains spectral properties. 

73 rawdata_path: string 

74 Base path to the raw recording, needed to show thunderfish 

75 when double clicking on a single EOD. 

76 """ 

77 self.wave_fish = wave_fish 

78 self.eoddata = data 

79 self.path = rawdata_path 

80 MultivariateExplorer.__init__(self, data[:,data_cols], 

81 None, 'EODExplorer') 

82 tunit = 'ms' 

83 dunit = '1/ms' 

84 if wave_fish: 

85 tunit = '1/EODf' 

86 dunit = 'EODf' 

87 wave_data = eod_data 

88 xlabels = ['Time [%s]' % tunit] 

89 ylabels = ['Voltage'] 

90 if 'first' in add_waveforms: 

91 # first derivative: 

92 if loaded_spec: 

93 if hasattr(sig, 'savgol_filter'): 

94 derivative = lambda x: (np.column_stack((x[0], \ 

95 sig.savgol_filter(x[0][:,1], 5, 2, 1, x[0][1,0]-x[0][0,0]))), x[1]) 

96 else: 

97 derivative = lambda x: (np.column_stack((x[0][:-1,:], \ 

98 np.diff(x[0][:,1])/(x[0][1,0]-x[0][0,0]))), x[1]) 

99 else: 

100 if hasattr(sig, 'savgol_filter'): 

101 derivative = lambda x: np.column_stack((x, \ 

102 sig.savgol_filter(x[:,1], 5, 2, 1, x[1,0]-x[0,0]))) 

103 else: 

104 derivative = lambda x: np.column_stack((x[:-1,:], \ 

105 np.diff(x[:,1])/(x[1,0]-x[0,0]))) 

106 wave_data = list(map(derivative, wave_data)) 

107 ylabels.append('dV/dt [%s]' % dunit) 

108 if 'second' in add_waveforms: 

109 # second derivative: 

110 if loaded_spec: 

111 if hasattr(sig, 'savgol_filter'): 

112 derivative = lambda x: (np.column_stack((x[0], \ 

113 sig.savgol_filter(x[0][:,1], 5, 2, 2, x[0][1,0]-x[0][0,0]))), x[1]) 

114 else: 

115 derivative = lambda x: (np.column_stack((x[0][:-1,:], \ 

116 np.diff(x[0][:,2])/(x[0][1,0]-x[0][0,0]))), x[1]) 

117 else: 

118 if hasattr(sig, 'savgol_filter'): 

119 derivative = lambda x: np.column_stack((x, \ 

120 sig.savgol_filter(x[:,1], 5, 2, 2, x[1,0]-x[0,0]))) 

121 else: 

122 derivative = lambda x: np.column_stack((x[:-1,:], \ 

123 np.diff(x[:,2])/(x[1,0]-x[0,0]))) 

124 wave_data = list(map(derivative, wave_data)) 

125 ylabels.append('d^2V/dt^2 [%s^2]' % dunit) 

126 if loaded_spec: 

127 if wave_fish: 

128 indices = [0] 

129 phase = False 

130 xlabels.append('Harmonics') 

131 if 'ampl' in add_waveforms: 

132 indices.append(3) 

133 ylabels.append('Ampl [%]') 

134 if 'power' in add_waveforms: 

135 indices.append(4) 

136 ylabels.append('Power [dB]') 

137 if 'phase' in add_waveforms: 

138 indices.append(5) 

139 ylabels.append('Phase') 

140 phase = True 

141 def get_spectra(x): 

142 y = x[1][:,indices] 

143 if phase: 

144 y[y[:,-1]<0.0,-1] += 2.0*np.pi 

145 return (x[0], y) 

146 wave_data = list(map(get_spectra, wave_data)) 

147 else: 

148 xlabels.append('Frequency [Hz]') 

149 ylabels.append('Power [dB]') 

150 def get_spectra(x): 

151 y = x[1] 

152 y[:,1] = decibel(y[:,1], None) 

153 return (x[0], y) 

154 wave_data = list(map(get_spectra, wave_data)) 

155 self.set_wave_data(wave_data, xlabels, ylabels, True) 

156 

157 

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

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

160 

161 - Limits for amplitude and time like quantities start at zero. 

162 - Phases a labeled with multuples of pi. 

163 - Species labels are rotated. 

164 """ 

165 if any(l in label for l in ['ampl', 'power', 'width', 

166 'time', 'tau', 'P2-P1-dist', 

167 'var', 'peak', 'trough', 

168 'P2-P1-dist', 'rms', 'noise']): 

169 if np.all(data[np.isfinite(data)] >= 0.0): 

170 if axis == 'x': 

171 ax.set_xlim(0.0, None) 

172 elif axis == 'y': 

173 ax.set_ylim(0.0, None) 

174 elif axis == 'c': 

175 return 0.0, np.max(data), None 

176 else: 

177 if axis == 'x': 

178 ax.set_xlim(None, 0.0) 

179 elif axis == 'y': 

180 ax.set_ylim(None, 0.0) 

181 elif axis == 'c': 

182 return np.min(data), 0.0, None 

183 elif 'phase' in label: 

184 if axis == 'x': 

185 ax.set_xlim(-np.pi, np.pi) 

186 ax.set_xticks(np.arange(-np.pi, 1.5*np.pi, 0.5*np.pi)) 

187 ax.set_xticklabels([u'-\u03c0', u'-\u03c0/2', '0', u'\u03c0/2', u'\u03c0']) 

188 elif axis == 'y': 

189 ax.set_ylim(-np.pi, np.pi) 

190 ax.set_yticks(np.arange(-np.pi, 1.5*np.pi, 0.5*np.pi)) 

191 ax.set_yticklabels([u'-\u03c0', u'-\u03c0/2', '0', u'\u03c0/2', u'\u03c0']) 

192 elif axis == 'c': 

193 if ax is not None: 

194 ax.set_yticklabels([u'-\u03c0', u'-\u03c0/2', '0', u'\u03c0/2', u'\u03c0']) 

195 return -np.pi, np.pi, np.arange(-np.pi, 1.5*np.pi, 0.5*np.pi) 

196 elif 'species' in label: 

197 if axis == 'x': 

198 for label in ax.get_xticklabels(): 

199 label.set_rotation(30) 

200 ax.set_xlabel('') 

201 ax.set_xlim(np.min(data)-0.5, np.max(data)+0.5) 

202 elif axis == 'y': 

203 ax.set_ylabel('') 

204 ax.set_ylim(np.min(data)-0.5, np.max(data)+0.5) 

205 elif axis == 'c': 

206 if ax is not None: 

207 ax.set_ylabel('') 

208 return np.min(data), np.max(data), None 

209 

210 

211 def fix_waveform_plot(self, axs, indices): 

212 """Adapt waveform plots to EOD waveforms, derivatives, and spectra. 

213 """ 

214 if len(indices) == 0: 

215 axs[0].text(0.5, 0.5, 'Click to plot EOD waveforms', 

216 transform = axs[0].transAxes, ha='center', va='center') 

217 axs[0].text(0.5, 0.3, 'n = %d' % len(self.raw_data), 

218 transform = axs[0].transAxes, ha='center', va='center') 

219 elif len(indices) == 1: 

220 file_name = self.eoddata[indices[0],'file'] if 'file' in self.eoddata else basename 

221 if 'index' in self.eoddata and np.isfinite(self.eoddata[indices[0],'index']) and \ 

222 np.any(self.eoddata[:,'index'] != self.eoddata[0,'index']): 

223 axs[0].set_title('%s: %d' % (file_name, 

224 self.eoddata[indices[0],'index'])) 

225 else: 

226 axs[0].set_title(file_name) 

227 if np.isfinite(self.eoddata[indices[0],'index']): 

228 axs[0].text(0.05, 0.85, '%.1fHz' % self.eoddata[indices[0],'EODf'], 

229 transform = axs[0].transAxes) 

230 else: 

231 axs[0].set_title('%d EOD waveforms selected' % len(indices)) 

232 for ax in axs: 

233 for l in ax.lines: 

234 l.set_linewidth(3.0) 

235 for ax, xl in zip(axs, self.wave_ylabels): 

236 if 'Voltage' in xl: 

237 ax.set_ylim(top=1.1) 

238 ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=4)) 

239 if 'dV/dt' in xl: 

240 ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=4)) 

241 if 'd^2V/dt^2' in xl: 

242 ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=4)) 

243 if self.wave_fish: 

244 for ax, xl in zip(axs, self.wave_ylabels): 

245 if 'Voltage' in xl: 

246 ax.set_xlim(-0.7, 0.7) 

247 if 'Ampl' in xl or 'Power' in xl or 'Phase' in xl: 

248 ax.set_xlim(-0.5, 8.5) 

249 for l in ax.lines: 

250 l.set_marker('.') 

251 l.set_markersize(15.0) 

252 l.set_markeredgewidth(0.5) 

253 l.set_markeredgecolor('k') 

254 l.set_markerfacecolor(l.get_color()) 

255 if 'Ampl' in xl: 

256 ax.set_ylim(0.0, 100.0) 

257 ax.yaxis.set_major_locator(ticker.MultipleLocator(25.0)) 

258 if 'Power' in xl: 

259 ax.set_ylim(-60.0, 2.0) 

260 ax.yaxis.set_major_locator(ticker.MultipleLocator(20.0)) 

261 if 'Phase' in xl: 

262 ax.set_ylim(0.0, 2.0*np.pi) 

263 ax.set_yticks(np.arange(0.0, 2.5*np.pi, 0.5*np.pi)) 

264 ax.set_yticklabels(['0', u'\u03c0/2', u'\u03c0', u'3\u03c0/2', u'2\u03c0']) 

265 else: 

266 for ax, xl in zip(axs, self.wave_ylabels): 

267 if 'Voltage' in xl: 

268 ax.set_xlim(-1.0, 1.5) 

269 if 'Power' in xl: 

270 ax.set_xlim(1.0, 2000.0) 

271 ax.set_xscale('log') 

272 ax.set_ylim(-60.0, 2.0) 

273 ax.yaxis.set_major_locator(ticker.MultipleLocator(20.0)) 

274 if len(indices) > 0: 

275 for ax in axs: 

276 ax.axhline(c='k', lw=1) 

277 

278 

279 def list_selection(self, indices): 

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

281 

282 If only a single EOD is selected, list all of its properties. 

283 """ 

284 print('') 

285 print('selected EODs:') 

286 if 'index' in self.eoddata and \ 

287 np.any(self.eoddata[:,'index'] != self.eoddata[0,'index']): 

288 for i in indices: 

289 file_name = self.eoddata[i,'file'] if 'file' in self.eoddata else basename 

290 if np.isfinite(self.eoddata[i,'index']): 

291 print('%s : %d' % (file_name, self.eoddata[i,'index'])) 

292 else: 

293 print(file_name) 

294 elif 'file' in self.eoddata: 

295 for i in indices: 

296 print(self.eoddata[i,'file']) 

297 if len(indices) == 1: 

298 # write eoddata line on terminal: 

299 keylen = 0 

300 keys = [] 

301 values = [] 

302 for c in range(self.eoddata.columns()): 

303 k, v = self.eoddata.key_value(indices[0], c) 

304 keys.append(k) 

305 values.append(v) 

306 if keylen < len(k): 

307 keylen = len(k) 

308 for k, v in zip(keys, values): 

309 fs = '%%-%ds: %%s' % keylen 

310 print(fs % (k, v.strip())) 

311 

312 

313 def analyze_selection(self, index): 

314 """Launch thunderfish on the selected EOD. 

315 """ 

316 # load data: 

317 file_base = self.eoddata[index,'file'] if 'file' in self.eoddata else basename 

318 bp = os.path.join(self.path, file_base) 

319 fn = glob.glob(bp + '.*') 

320 if len(fn) == 0: 

321 print('no recording found for %s' % bp) 

322 return 

323 recording = fn[0] 

324 channel = 0 

325 try: 

326 raw_data, rate, unit, ampl_max = load_data(recording) 

327 raw_data = raw_data[:,channel] 

328 except IOError as e: 

329 print('%s: failed to open file: did you provide a path to the raw data (-P option)?' % (recording)) 

330 return 

331 if len(raw_data) <= 1: 

332 print('%s: empty data file' % recording) 

333 return 

334 # load configuration: 

335 cfgfile = __package__ + '.cfg' 

336 cfg = configuration(cfgfile, False, recording) 

337 cfg.load_files(cfgfile, recording, 4) 

338 if 'flipped' in self.eoddata: 

339 fs = 'flip' if self.eoddata[index,'flipped'] else 'none' 

340 cfg.set('flipWaveEOD', fs) 

341 cfg.set('flipPulseEOD', fs) 

342 # best_window: 

343 data, idx0, idx1, clipped, min_clip, max_clip = \ 

344 analysis_window(raw_data, rate, ampl_max, 

345 cfg.value('windowPosition'), cfg) 

346 # detect EODs in the data: 

347 psd_data, fishlist, _, eod_props, mean_eods, \ 

348 spec_data, peak_data, pulse_data, power_thresh, skip_reason, zoom_window = \ 

349 detect_eods(data, rate, min_clip, max_clip, recording, 0, 0, cfg) 

350 # plot EOD: 

351 idx = int(self.eoddata[index,'index']) if 'index' in self.eoddata else 0 

352 for k in ['toolbar', 'keymap.back', 'keymap.forward', 

353 'keymap.zoom', 'keymap.pan']: 

354 plt.rcParams[k] = self.plt_params[k] 

355 fig = plot_eods(file_base, None, raw_data, rate, None, idx0, idx1, 

356 clipped, psd_data[0], fishlist, None, 

357 mean_eods, eod_props, peak_data, pulse_data, spec_data, 

358 [idx], unit, zoom_window, 10, None, True, False, 

359 'auto', False, 0.0, 3000.0, 

360 interactive=True, verbose=0) 

361 fig.canvas.manager.set_window_title('thunderfish: %s' % file_base) 

362 plt.show(block=False) 

363 

364 

365 """Names of groups of data columns that can be selected by the select_EOD_properties() function. 

366 """ 

367 groups = ['all', 'allpower', 'noise', 'timing', 'wave', 

368 'ampl', 'relampl', 'power', 'relpower', 'phase', 

369 'time', 'width', 'peaks', 'none'] 

370 

371 @staticmethod 

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

373 """Select data columns to be explored. 

374 

375 First, groups of columns are selected, then individual 

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

377 selection. 

378 

379 Parameters 

380 ---------- 

381 data: TableData 

382 Table with EOD properties from which columns are selected. 

383 wave_fish: boolean. 

384 Indicates if data contains properties of wave- or pulse-type electric fish. 

385 max_n: int 

386 Maximum number of harmonics (wave-type fish) or peaks (pulse-type fish) 

387 to be selected. 

388 column_groups: list of string 

389 List of name denoting groups of columns to be selected. Supported groups are 

390 listed in `EODExplor.groups`. 

391 add_columns: list of string or int 

392 List of further individual columns to be selected. 

393 

394 Returns 

395 ------- 

396 data_cols: list of int 

397 Indices of data columns to be shown by EODExplorer. 

398 error: string 

399 In case of an invalid column group, an error string. 

400 """ 

401 if wave_fish: 

402 # maximum number of harmonics: 

403 if max_n == 0: 

404 max_n = 100 

405 else: 

406 max_n += 1 

407 for k in range(1, max_n): 

408 if not ('phase%d' % k) in data: 

409 max_n = k 

410 break 

411 else: 

412 # minimum number of peaks: 

413 min_peaks = -10 

414 for k in range(1, min_peaks, -1): 

415 if not ('P%dampl' % k) in data or not np.all(np.isfinite(data[:,'P%dampl' % k])): 

416 min_peaks = k+1 

417 break 

418 # maximum number of peaks: 

419 if max_n == 0: 

420 max_peaks = 20 

421 else: 

422 max_peaks = max_n + 1 

423 for k in range(1, max_peaks): 

424 if not ('P%dampl' % k) in data or not np.all(np.isfinite(data[:,'P%dampl' % k])): 

425 max_peaks = k 

426 break 

427 

428 # default columns: 

429 group_cols = ['EODf'] 

430 if 'EODf_adjust' in data: 

431 group_cols.append('EODf_adjust') 

432 if len(column_groups) == 0: 

433 column_groups = ['all'] 

434 for group in column_groups: 

435 if group == 'none': 

436 group_cols = [] 

437 elif wave_fish: 

438 if group == 'noise': 

439 group_cols.extend(['noise', 'rmserror', 'power', 'thd', 

440 'dbdiff', 'maxdb', 'p-p-amplitude', 

441 'relampl1', 'relampl2', 'relampl3']) 

442 elif group == 'timing' or group == 'time': 

443 group_cols.extend(['peakwidth', 'troughwidth', 'p-p-distance', 

444 'leftpeak', 'rightpeak', 'lefttrough', 'righttrough']) 

445 elif group == 'wave': 

446 group_cols.extend(['thd', 'minwidth', 'min-p-p-distance', 

447 'relpeakampl']) 

448 elif group == 'ampl': 

449 for k in range(0, max_n): 

450 group_cols.append('ampl%d' % k) 

451 elif group == 'relampl': 

452 group_cols.append('thd') 

453 group_cols.append('relpeakampl') 

454 for k in range(1, max_n): 

455 group_cols.append('relampl%d' % k) 

456 elif group == 'relpower' or group == 'power': 

457 for k in range(1, max_n): 

458 group_cols.append('relpower%d' % k) 

459 elif group == 'phase': 

460 for k in range(0, max_n): 

461 group_cols.append('phase%d' % k) 

462 elif group == 'all': 

463 group_cols.extend(['thd', 'minwidth', 'min-p-p-distance', 

464 'relpeakampl']) 

465 for k in range(1, max_n): 

466 group_cols.append('relampl%d' % k) 

467 group_cols.append('phase%d' % k) 

468 elif group == 'allpower': 

469 group_cols.append('thd') 

470 for k in range(1, max_n): 

471 group_cols.append('relpower%d' % k) 

472 group_cols.append('phase%d' % k) 

473 else: 

474 return None, '"%s" is not a valid data group for wavefish' % group 

475 else: # pulse fish 

476 if group == 'noise': 

477 group_cols.extend(['noise', 'p-p-amplitude', 'min-ampl', 'max-ampl']) 

478 elif group == 'timing': 

479 group_cols.extend(['tstart', 'tend', 'width', 'tau', 'P2-P1-dist', 'firstpeak', 'lastpeak']) 

480 elif group == 'power': 

481 group_cols.extend(['peakfreq', 'peakpower', 'poweratt5', 'poweratt50', 'lowcutoff']) 

482 elif group == 'time': 

483 for k in range(min_peaks, max_peaks): 

484 if k != 1: 

485 group_cols.append('P%dtime' % k) 

486 elif group == 'ampl': 

487 for k in range(min_peaks, max_peaks): 

488 group_cols.append('P%dampl' % k) 

489 elif group == 'relampl': 

490 for k in range(min_peaks, max_peaks): 

491 if k != 1: 

492 group_cols.append('P%drelampl' % k) 

493 elif group == 'width': 

494 for k in range(min_peaks, max_peaks): 

495 group_cols.append('P%dwidth' % k) 

496 elif group == 'peaks': 

497 group_cols.append('firstpeak') 

498 group_cols.append('lastpeak') 

499 elif group == 'all': 

500 group_cols.extend(['firstpeak', 'lastpeak']) 

501 for k in range(min_peaks, max_peaks): 

502 if k != 1: 

503 group_cols.append('P%drelampl' % k) 

504 group_cols.append('P%dtime' % k) 

505 group_cols.append('P%dwidth' % k) 

506 group_cols.extend(['tau', 'P2-P1-dist', 'peakfreq', 'poweratt5']) 

507 else: 

508 return None, '"%s" is not a valid data group for pulsefish' % group 

509 # additional data columns: 

510 group_cols.extend(add_columns) 

511 # translate to indices: 

512 data_cols = select_features(data, group_cols) 

513 return data_cols, None 

514 

515 

516wave_fish = True 

517load_spec = False 

518data = None 

519data_path = None 

520 

521def load_waveform(idx): 

522 eodf = data[idx,'EODf'] 

523 if not np.isfinite(eodf): 

524 if load_spec: 

525 return None, None 

526 else: 

527 return None 

528 file_name = data[idx,'file'] if 'file' in data else '-'.join(basename.split('-')[:-1]) 

529 file_index = data[idx,'index'] if 'index' in data else 0 

530 eod_filename = os.path.join(data_path, '%s-eodwaveform-%d.csv' % (file_name, file_index)) 

531 eod_table = TableData(eod_filename) 

532 eod = eod_table[:,'mean'] 

533 norm = np.max(eod) 

534 if wave_fish: 

535 eod = np.column_stack((eod_table[:,'time']*0.001*eodf, eod/norm)) 

536 else: 

537 eod = np.column_stack((eod_table[:,'time'], eod/norm)) 

538 if not load_spec: 

539 return eod 

540 fish_type = 'wave' if wave_fish else 'pulse' 

541 spec_table = TableData(os.path.join(data_path, '%s-%sspectrum-%d.csv' % (file_name, fish_type, file_index))) 

542 spec_data = spec_table.array() 

543 if not wave_fish: 

544 spec_data = spec_data[spec_data[:,0]<2000.0,:] 

545 spec_data = spec_data[::5,:] 

546 return (eod, spec_data) 

547 

548 

549def main(cargs=None): 

550 global data 

551 global wave_fish 

552 global load_spec 

553 global data_path 

554 global basename 

555 

556 # command line arguments: 

557 if cargs is None: 

558 cargs = sys.argv[1:] 

559 parser = argparse.ArgumentParser(add_help=False, 

560 description='View and explore properties of EOD waveforms.', 

561 epilog='version %s by Benda-Lab (2019-%s)' % (__version__, __year__)) 

562 parser.add_argument('-h', '--help', nargs=0, action=PrintHelp, 

563 help='show this help message and exit') 

564 parser.add_argument('--version', action='version', version=__version__) 

565 parser.add_argument('-v', dest='verbose', action='store_true', default=False, 

566 help='verbose output') 

567 parser.add_argument('-l', dest='list_columns', action='store_true', 

568 help='list all available data columns and exit') 

569 parser.add_argument('-j', dest='jobs', nargs='?', type=int, default=None, const=0, 

570 help='number of jobs run in parallel for loading waveform data. Without argument use all CPU cores.') 

571 parser.add_argument('-D', dest='column_groups', default=[], type=str, action='append', 

572 choices=EODExplorer.groups, 

573 help='default selection of data columns, check them with the -l option') 

574 parser.add_argument('-d', dest='add_data_cols', action='append', default=[], metavar='COLUMN', 

575 help='data columns to be appended or removed (if already listed) for analysis') 

576 parser.add_argument('-n', dest='max_n', default=0, type=int, metavar='MAX', 

577 help='maximum number of harmonics or peaks to be used') 

578 parser.add_argument('-w', dest='add_waveforms', default=[], type=str, action='append', 

579 choices=['first', 'second', 'ampl', 'power', 'phase'], 

580 help='add first or second derivative of EOD waveform, or relative amplitude, power, or phase to the plot of selected EODs.') 

581 parser.add_argument('-s', dest='save_pca', action='store_true', 

582 help='save PCA components and exit') 

583 parser.add_argument('-c', dest='color_col', default='EODf', type=str, metavar='COLUMN', 

584 help='data column to be used for color code or "row"') 

585 parser.add_argument('-m', dest='color_map', default='jet', type=str, metavar='CMAP', 

586 help='name of color map') 

587 parser.add_argument('-p', dest='data_path', default='.', type=str, metavar='PATH', 

588 help='path to the analyzed EOD waveform data') 

589 parser.add_argument('-P', dest='rawdata_path', default='.', type=str, metavar='PATH', 

590 help='path to the raw EOD recordings') 

591 parser.add_argument('-f', dest='format', default='auto', type=str, 

592 choices=TableData.formats + ['same'], 

593 help='file format used for saving PCA data ("same" uses same format as input file)') 

594 parser.add_argument('file', default='', type=str, 

595 help='a wavefish.* or pulsefish.* summary file as generated by collectfish') 

596 args = parser.parse_args(cargs) 

597 

598 # read in command line arguments:  

599 verbose = args.verbose 

600 list_columns = args.list_columns 

601 jobs = args.jobs 

602 file_name = args.file 

603 column_groups = args.column_groups 

604 add_data_cols = args.add_data_cols 

605 max_n = args.max_n 

606 add_waveforms = args.add_waveforms 

607 save_pca = args.save_pca 

608 color_col = args.color_col 

609 color_map = args.color_map 

610 data_path = args.data_path 

611 rawdata_path = args.rawdata_path 

612 data_format = args.format 

613 

614 # read configuration: 

615 cfgfile = __package__ + '.cfg' 

616 cfg = ConfigFile() 

617 add_eod_quality_config(cfg) 

618 add_harmonic_groups_config(cfg) 

619 add_species_config(cfg) 

620 add_write_table_config(cfg, table_format='csv', unit_style='row', 

621 align_columns=True, shrink_width=False) 

622 cfg.load_files(cfgfile, file_name, 4) 

623 

624 # output format: 

625 if data_format == 'same': 

626 ext = os.path.splitext(file_name)[1][1:] 

627 if ext in TableData.ext_formats: 

628 data_format = TableData.ext_formats[ext] 

629 else: 

630 data_format = 'dat' 

631 if data_format != 'auto': 

632 cfg.set('fileFormat', data_format) 

633 

634 # check color map: 

635 if not color_map in plt.colormaps(): 

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

637 

638 # load summary data: 

639 wave_fish = 'wave' in file_name 

640 data = TableData(file_name) 

641 

642 # basename: 

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

644 

645 # check quality: 

646 skipped = 0 

647 for r in reversed(range(data.rows())): 

648 idx = 0 

649 if 'index' in data: 

650 idx = data[r,'index'] 

651 skips = '' 

652 if wave_fish: 

653 harm_rampl = np.array([0.01*data[r,'relampl%d'%(k+1)] for k in range(3) 

654 if 'relampl%d'%(k+1) in data]) 

655 props = data.row_dict(r) 

656 if 'clipped' in props: 

657 props['clipped'] *= 0.01 

658 if 'noise' in props: 

659 props['noise'] *= 0.01 

660 if 'rmserror' in props: 

661 props['rmserror'] *= 0.01 

662 if 'thd' in props: 

663 props['thd'] *= 0.01 

664 _, skips, msg = wave_quality(props, harm_rampl, **wave_quality_args(cfg)) 

665 else: 

666 props = data.row_dict(r) 

667 if 'clipped' in props: 

668 props['clipped'] *= 0.01 

669 if 'noise' in props: 

670 props['noise'] *= 0.01 

671 skips, msg, _ = pulse_quality(props, **pulse_quality_args(cfg)) 

672 if len(skips) > 0: 

673 if verbose: 

674 print('skip fish %2d from %s: %s' % (idx, data[r,'file'] if 'file' in data else basename, skips)) 

675 del data[r,:] 

676 skipped += 1 

677 if verbose and skipped > 0: 

678 print('') 

679 

680 # select columns (EOD properties) to be shown: 

681 data_cols, error = \ 

682 EODExplorer.select_EOD_properties(data, wave_fish, max_n, 

683 column_groups, add_data_cols) 

684 if error: 

685 parser.error(error) 

686 

687 # select column used for coloring the data: 

688 colors, color_label, color_idx, error = \ 

689 select_coloring(data, data_cols, color_col) 

690 if error: 

691 parser.error(error) 

692 

693 # list columns: 

694 if list_columns: 

695 list_available_features(data, data_cols, color_idx) 

696 parser.exit() 

697 

698 # load waveforms: 

699 load_spec = 'ampl' in add_waveforms or 'power' in add_waveforms or 'phase' in add_waveforms 

700 if jobs is not None: 

701 cpus = cpu_count() if jobs == 0 else jobs 

702 p = Pool(cpus) 

703 eod_data = p.map(load_waveform, range(data.rows())) 

704 del p 

705 else: 

706 eod_data = list(map(load_waveform, range(data.rows()))) 

707 

708 # explore: 

709 eod_expl = EODExplorer(data, data_cols, wave_fish, eod_data, 

710 add_waveforms, load_spec, rawdata_path) 

711 # write pca: 

712 if save_pca: 

713 eod_expl.compute_pca(False) 

714 eod_expl.save_pca(basename, False, **write_table_args(cfg)) 

715 eod_expl.compute_pca(True) 

716 eod_expl.save_pca(basename, True, **write_table_args(cfg)) 

717 else: 

718 eod_expl.set_colors(colors, color_label, color_map) 

719 eod_expl.show() 

720 

721 

722if __name__ == '__main__': 

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

724 main()