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

907 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-16 22:05 +0000

1"""# thunderfish 

2 

3Automatically detect and analyze all EOD waveforms in short recordings 

4and generated summary plots and data tables. 

5 

6Run it from the thunderfish development directory as: 

7```sh 

8> python3 -m thunderfish.thunderfish audiofile.wav 

9``` 

10Or install thunderfish 

11```sh 

12> sudo pip3 install . 

13``` 

14Then you can run it directly from every directory: 

15```sh 

16> thunderfish audiofile.wav 

17``` 

18""" 

19 

20import sys 

21import os 

22import glob 

23import io 

24import zipfile 

25import argparse 

26import traceback 

27import numpy as np 

28import matplotlib.pyplot as plt 

29import matplotlib.gridspec as gridspec 

30import matplotlib.ticker as ticker 

31import matplotlib.lines as ml 

32from matplotlib.transforms import Bbox 

33from matplotlib.backends.backend_pdf import PdfPages 

34from multiprocessing import Pool, freeze_support, cpu_count 

35from audioio import play, fade, load_audio 

36from thunderlab.configfile import ConfigFile 

37from thunderlab.dataloader import load_data 

38from thunderlab.powerspectrum import decibel, plot_decibel_psd, multi_psd 

39from thunderlab.powerspectrum import add_multi_psd_config, multi_psd_args 

40from thunderlab.tabledata import TableData, add_write_table_config, write_table_args 

41from .version import __version__, __year__ 

42from .bestwindow import add_clip_config, add_best_window_config 

43from .bestwindow import clip_args, best_window_args 

44from .bestwindow import analysis_window, plot_data_window 

45from .checkpulse import check_pulse, add_check_pulse_config, check_pulse_args 

46from .pulses import extract_pulsefish 

47from .harmonics import add_psd_peak_detection_config, add_harmonic_groups_config 

48from .harmonics import harmonic_groups, harmonic_groups_args, psd_peak_detection_args 

49from .harmonics import colors_markers, plot_harmonic_groups 

50from .consistentfishes import consistent_fishes 

51from .eodanalysis import eod_waveform, waveeod_waveform, analyze_wave, analyze_pulse 

52from .eodanalysis import clipped_fraction 

53from .eodanalysis import plot_eod_recording, plot_pulse_eods 

54from .eodanalysis import plot_eod_waveform, plot_eod_snippets 

55from .eodanalysis import plot_pulse_spectrum, plot_wave_spectrum 

56from .eodanalysis import add_eod_analysis_config, eod_waveform_args 

57from .eodanalysis import analyze_wave_args, analyze_pulse_args 

58from .eodanalysis import add_species_config 

59from .eodanalysis import wave_quality, wave_quality_args, add_eod_quality_config 

60from .eodanalysis import pulse_quality, pulse_quality_args 

61from .eodanalysis import save_eod_waveform, save_wave_eodfs, save_wave_fish, save_pulse_fish 

62from .eodanalysis import save_wave_spectrum, save_pulse_spectrum, save_pulse_peaks, save_pulse_times 

63from .eodanalysis import load_eod_waveform, load_wave_eodfs, load_wave_fish, load_pulse_fish 

64from .eodanalysis import load_wave_spectrum, load_pulse_spectrum, load_pulse_peaks 

65from .eodanalysis import save_analysis, load_analysis, load_recording 

66from .eodanalysis import parse_filename, file_types 

67from .fakefish import normalize_wavefish, export_wavefish 

68 

69 

70def configuration(): 

71 """Assemble configuration parameter for thunderfish. 

72 

73 Returns 

74 ------- 

75 cfg: ConfigFile 

76 Configuration parameters. 

77 """ 

78 cfg = ConfigFile() 

79 add_multi_psd_config(cfg) 

80 cfg.add('frequencyThreshold', 1.0, 'Hz', 

81 'The fundamental frequency of each fish needs to be detected in each power spectrum within this threshold.') 

82 # TODO: make this threshold dependent on frequency resolution! 

83 cfg.add('minPSDAverages', 3, '', 'Minimum number of fft averages for estimating the power spectrum.') # needed by fishfinder 

84 add_psd_peak_detection_config(cfg) 

85 add_harmonic_groups_config(cfg) 

86 add_clip_config(cfg) 

87 cfg.add('unwrapData', False, '', 'Unwrap clipped voltage traces.') 

88 add_best_window_config(cfg, win_size=8.0, w_cv_ampl=10.0) 

89 add_check_pulse_config(cfg) 

90 add_eod_analysis_config(cfg, min_pulse_win=0.004) 

91 del cfg['eodSnippetFac'] 

92 del cfg['eodMinSnippet'] 

93 del cfg['eodMinSem'] 

94 add_eod_quality_config(cfg) 

95 add_species_config(cfg) 

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

97 align_columns=True, shrink_width=False) 

98 return cfg 

99 

100 

101def save_configuration(cfg, config_file): 

102 """Save configuration parameter for thunderfish to a file. 

103 

104 Parameters 

105 ---------- 

106 cfg: ConfigFile 

107 Configuration parameters and their values. 

108 config_file: string 

109 Name of the configuration file to be loaded. 

110 """ 

111 ext = os.path.splitext(config_file)[1] 

112 if ext != os.extsep + 'cfg': 

113 print('configuration file name must have .cfg as extension!') 

114 else: 

115 print('write configuration to %s ...' % config_file) 

116 del cfg['fileColumnNumbers'] 

117 del cfg['fileShrinkColumnWidth'] 

118 del cfg['fileMissing'] 

119 del cfg['fileLaTeXLabelCommand'] 

120 del cfg['fileLaTeXMergeStd'] 

121 cfg.dump(config_file) 

122 

123 

124def detect_eods(data, rate, min_clip, max_clip, name, mode, 

125 verbose, plot_level, cfg): 

126 """Detect EODs of all fish present in the data. 

127 

128 Parameters 

129 ---------- 

130 data: array of floats 

131 The recording in which to detect EODs. 

132 rate: float 

133 Sampling rate of the dataset. 

134 min_clip: float 

135 Minimum amplitude that is not clipped. 

136 max_clip: float 

137 Maximum amplitude that is not clipped. 

138 name: string 

139 Name of the recording (e.g. its filename). 

140 mode: string 

141 Characters in the string indicate what and how to analyze: 

142 - 'w': analyze wavefish 

143 - 'p': analyze pulsefish 

144 - 'P': analyze only the pulsefish with the largest amplitude (not implemented yet)  

145 verbose: int 

146 Print out information about EOD detection if greater than zero. 

147 plot_level : int 

148 Similar to verbosity levels, but with plots.  

149 cfg: ConfigFile 

150 Configuration parameters. 

151 

152 Returns 

153 ------- 

154 psd_data: list of 2D arrays 

155 List of power spectra (frequencies and power) of the analysed data 

156 for different frequency resolutions. 

157 wave_eodfs: list of 2D arrays 

158 Frequency and power of fundamental frequency/harmonics of all wave fish. 

159 wave_indices: array of int 

160 Indices of wave fish mapping from wave_eodfs to eod_props. 

161 If negative, then that EOD frequency has no waveform described in eod_props. 

162 eod_props: list of dict 

163 Lists of EOD properties as returned by analyze_pulse() and analyze_wave() 

164 for each waveform in mean_eods. 

165 mean_eods: list of 2-D arrays with time, mean, sem, and fit. 

166 Averaged EOD waveforms of pulse and wave fish. 

167 spec_data: list of 2_D arrays 

168 For each pulsefish a power spectrum of the single pulse and for 

169 each wavefish the relative amplitudes and phases of the harmonics. 

170 peak_data: list of 2_D arrays 

171 For each pulse fish a list of peak properties 

172 (index, time, and amplitude), empty array for wave fish. 

173 power_thresh: 2 D array or None 

174 Frequency (first column) and power (second column) of threshold 

175 derived from single pulse spectra to discard false wave fish. 

176 None if no pulse fish was detected. 

177 skip_reason: list of string 

178 Reasons, why an EOD was discarded. 

179 """ 

180 dfreq = np.nan 

181 nfft = 0 

182 psd_data = [[]] 

183 wave_eodfs = [] 

184 wave_indices = [] 

185 if 'w' in mode: 

186 # detect wave fish: 

187 psd_data = multi_psd(data, rate, **multi_psd_args(cfg)) 

188 dfreq = np.mean(np.diff(psd_data[0][:,0])) 

189 nfft = int(rate/dfreq) 

190 h_kwargs = psd_peak_detection_args(cfg) 

191 h_kwargs.update(harmonic_groups_args(cfg)) 

192 wave_eodfs_list = [] 

193 for i, psd in enumerate(psd_data): 

194 wave_eodfs = harmonic_groups(psd[:,0], psd[:,1], verbose-1, **h_kwargs)[0] 

195 if verbose > 0 and len(psd_data) > 1: 

196 numpsdresolutions = cfg.value('numberPSDResolutions') 

197 print('fundamental frequencies detected in power spectrum of window %d at resolution %d:' 

198 % (i//numpsdresolutions, i%numpsdresolutions)) 

199 if len(wave_eodfs) > 0: 

200 print(' ' + ' '.join(['%.1f' % freq[0, 0] for freq in wave_eodfs])) 

201 else: 

202 print(' none') 

203 wave_eodfs_list.append(wave_eodfs) 

204 wave_eodfs = consistent_fishes(wave_eodfs_list, 

205 df_th=cfg.value('frequencyThreshold')) 

206 if verbose > 0: 

207 if len(wave_eodfs) > 0: 

208 print('found %2d EOD frequencies consistent in all power spectra:' % len(wave_eodfs)) 

209 print(' ' + ' '.join(['%.1f' % freq[0, 0] for freq in wave_eodfs])) 

210 else: 

211 print('no fundamental frequencies are consistent in all power spectra') 

212 

213 # analysis results: 

214 eod_props = [] 

215 mean_eods = [] 

216 spec_data = [] 

217 peak_data = [] 

218 power_thresh = None 

219 skip_reason = [] 

220 max_pulse_amplitude = 0.0 

221 zoom_window = [] 

222 

223 if 'p' in mode: 

224 # detect pulse fish: 

225 _, eod_times, eod_peaktimes, zoom_window, _ = extract_pulsefish(data, rate, verbose=verbose-1, plot_level=plot_level, save_path=os.path.splitext(os.path.basename(name))[0]) 

226 

227 #eod_times = [] 

228 #eod_peaktimes = [] 

229 if verbose > 0: 

230 if len(eod_times) > 0: 

231 print('found %2d pulsefish EODs' % len(eod_times)) 

232 else: 

233 print('no pulsefish EODs found') 

234 

235 # analyse eod waveform of pulse-fish: 

236 min_freq_res = cfg.value('frequencyResolution') 

237 for k, (eod_ts, eod_pts) in enumerate(zip(eod_times, eod_peaktimes)): 

238 mean_eod, eod_times0 = \ 

239 eod_waveform(data, rate, eod_ts, win_fac=0.8, 

240 min_win=cfg.value('eodMinPulseSnippet'), 

241 min_sem=False, **eod_waveform_args(cfg)) 

242 mean_eod, props, peaks, power = analyze_pulse(mean_eod, eod_times0, 

243 freq_resolution=min_freq_res, 

244 **analyze_pulse_args(cfg)) 

245 if len(peaks) == 0: 

246 print('error: no peaks in pulse EOD detected') 

247 continue 

248 clipped_frac = clipped_fraction(data, rate, eod_times0, 

249 mean_eod, min_clip, max_clip) 

250 props['peaktimes'] = eod_pts # XXX that should go into analyze pulse 

251 props['index'] = len(eod_props) 

252 props['clipped'] = clipped_frac 

253 props['samplerate'] = rate 

254 props['nfft'] = nfft 

255 props['dfreq'] = dfreq 

256 

257 # add good waveforms only: 

258 skips, msg, skipped_clipped = pulse_quality(props, **pulse_quality_args(cfg)) 

259 

260 if len(skips) == 0: 

261 eod_props.append(props) 

262 mean_eods.append(mean_eod) 

263 spec_data.append(power) 

264 peak_data.append(peaks) 

265 if verbose > 0: 

266 print('take %6.1fHz pulse fish: %s' % (props['EODf'], msg)) 

267 else: 

268 skip_reason += ['%.1fHz pulse fish %s' % (props['EODf'], skips)] 

269 if verbose > 0: 

270 print('skip %6.1fHz pulse fish: %s (%s)' % 

271 (props['EODf'], skips, msg)) 

272 

273 # threshold for wave fish peaks based on single pulse spectra: 

274 if len(skips) == 0 or skipped_clipped: 

275 if max_pulse_amplitude < props['p-p-amplitude']: 

276 max_pulse_amplitude = props['p-p-amplitude'] 

277 i0 = np.argmin(np.abs(mean_eod[:,0])) 

278 i1 = len(mean_eod) - i0 

279 pulse_data = np.zeros(len(data)) 

280 for t in props['peaktimes']: 

281 idx = int(t*rate) 

282 ii0 = i0 if idx-i0 >= 0 else idx 

283 ii1 = i1 if idx+i1 < len(pulse_data) else len(pulse_data)-1-idx 

284 pulse_data[idx-ii0:idx+ii1] = mean_eod[i0-ii0:i0+ii1,1] 

285 pulse_psd = multi_psd(pulse_data, rate, **multi_psd_args(cfg)) 

286 pulse_power = pulse_psd[0][:,1] 

287 pulse_power *= len(data)/rate/props['period']/len(props['peaktimes']) 

288 pulse_power *= 5.0 

289 if power_thresh is None: 

290 power_thresh = pulse_psd[0] 

291 power_thresh[:,1] = pulse_power 

292 else: 

293 power_thresh[:,1] += pulse_power 

294 

295 # remove wavefish below pulse fish power: 

296 if 'w' in mode and power_thresh is not None: 

297 n = len(wave_eodfs) 

298 maxh = 3 # XXX make parameter 

299 df = power_thresh[1,0] - power_thresh[0,0] 

300 for k, fish in enumerate(reversed(wave_eodfs)): 

301 idx = np.array(fish[:maxh,0]//df, dtype=int) 

302 for offs in range(-2, 3): 

303 nbelow = np.sum(fish[:maxh,1] < power_thresh[idx+offs,1]) 

304 if nbelow > 0: 

305 wave_eodfs.pop(n-1-k) 

306 if verbose > 0: 

307 print('skip %6.1fHz wave fish: %2d harmonics are below pulsefish threshold' % (fish[0,0], nbelow)) 

308 break 

309 

310 if 'w' in mode: 

311 # analyse EOD waveform of all wavefish: 

312 powers = np.array([np.sum(fish[:,1]) for fish in wave_eodfs]) 

313 power_indices = np.argsort(-powers) 

314 wave_indices = np.zeros(len(wave_eodfs), dtype=int) - 3 

315 for k, idx in enumerate(power_indices): 

316 fish = wave_eodfs[idx] 

317 """ 

318 eod_times = np.arange(0.0, len(data)/rate, 1.0/fish[0,0]) 

319 mean_eod, eod_times = \ 

320 eod_waveform(data, rate, eod_times, win_fac=3.0, min_win=0.0, 

321 min_sem=(k==0), **eod_waveform_args(cfg)) 

322 """ 

323 mean_eod, eod_times = \ 

324 waveeod_waveform(data, rate, fish[0, 0], win_fac=3.0, 

325 unfilter_cutoff=cfg.value('unfilterCutoff')) 

326 if len(mean_eod) == 0: 

327 continue 

328 mean_eod, props, sdata, error_str = \ 

329 analyze_wave(mean_eod, fish, **analyze_wave_args(cfg)) 

330 if error_str: 

331 print(name + ': ' + error_str) 

332 clipped_frac = clipped_fraction(data, rate, eod_times, 

333 mean_eod, min_clip, max_clip) 

334 props['n'] = len(eod_times) 

335 props['index'] = len(eod_props) 

336 props['clipped'] = clipped_frac 

337 props['samplerate'] = rate 

338 props['nfft'] = nfft 

339 props['dfreq'] = dfreq 

340 # remove wave fish that are smaller than the largest pulse fish: 

341 if props['p-p-amplitude'] < 0.01*max_pulse_amplitude: 

342 rm_indices = power_indices[k:] 

343 if verbose > 0: 

344 print('skip %6.1fHz wave fish: power=%5.1fdB, p-p amplitude=%5.1fdB smaller than pulse fish=%5.1dB - 20dB' % 

345 (props['EODf'], decibel(powers[idx]), 

346 decibel(props['p-p-amplitude']), decibel(max_pulse_amplitude))) 

347 for idx in rm_indices[1:]: 

348 print('skip %6.1fHz wave fish: power=%5.1fdB even smaller' % 

349 (wave_eodfs[idx][0,0], decibel(powers[idx]))) 

350 wave_eodfs = [eodfs for idx, eodfs in enumerate(wave_eodfs) 

351 if idx not in rm_indices] 

352 wave_indices = np.array([idcs for idx, idcs in enumerate(wave_indices) 

353 if idx not in rm_indices], dtype=int) 

354 break 

355 # add good waveforms only: 

356 remove, skips, msg = wave_quality(props, sdata[1:,3], **wave_quality_args(cfg)) 

357 if len(skips) == 0: 

358 wave_indices[idx] = props['index'] 

359 eod_props.append(props) 

360 mean_eods.append(mean_eod) 

361 spec_data.append(sdata) 

362 peak_data.append([]) 

363 if verbose > 0: 

364 print('take %6.1fHz wave fish: %s' % (props['EODf'], msg)) 

365 else: 

366 wave_indices[idx] = -2 if remove else -1 

367 skip_reason += ['%.1fHz wave fish %s' % (props['EODf'], skips)] 

368 if verbose > 0: 

369 print('%-6s %6.1fHz wave fish: %s (%s)' % 

370 ('remove' if remove else 'skip', props['EODf'], skips, msg)) 

371 wave_eodfs = [eodfs for idx, eodfs in zip(wave_indices, wave_eodfs) if idx > -2] 

372 wave_indices = np.array([idx for idx in wave_indices if idx > -2], dtype=int) 

373 return (psd_data, wave_eodfs, wave_indices, eod_props, mean_eods, 

374 spec_data, peak_data, power_thresh, skip_reason, zoom_window) 

375 

376 

377def remove_eod_files(output_basename, verbose, cfg): 

378 """Remove all files from previous runs of thunderfish 

379 """ 

380 ff = cfg.value('fileFormat') 

381 if ff == 'py': 

382 fext = 'py' 

383 else: 

384 fext = TableData.extensions[ff] 

385 # remove all files from previous runs of thunderfish: 

386 for fn in glob.glob('%s*.%s' % (output_basename, fext)): 

387 os.remove(fn) 

388 if verbose > 0: 

389 print('removed file %s' % fn) 

390 

391 

392def plot_style(): 

393 """Set style of plots. 

394 """ 

395 plt.rcParams['figure.facecolor'] = 'white' 

396 plt.rcParams['axes.facecolor'] = 'none' 

397 plt.rcParams['xtick.direction'] = 'out' 

398 plt.rcParams['ytick.direction'] = 'out' 

399 

400 

401def axes_style(ax): 

402 """Fix style of axes. 

403 

404 Parameters 

405 ---------- 

406 ax: matplotlib axes 

407 """ 

408 ax.spines['top'].set_visible(False) 

409 ax.spines['right'].set_visible(False) 

410 ax.get_xaxis().tick_bottom() 

411 ax.get_yaxis().tick_left() 

412 

413 

414def plot_eods(base_name, message_filename, 

415 raw_data, rate, channel, idx0, idx1, clipped, 

416 psd_data, wave_eodfs, wave_indices, mean_eods, eod_props, 

417 peak_data, spec_data, indices, unit, zoom_window, 

418 n_snippets=10, power_thresh=None, label_power=True, 

419 all_eods=False, spec_plots='auto', skip_bad=True, 

420 log_freq=False, min_freq=0.0, max_freq=3000.0, 

421 interactive=True, verbose=0): 

422 """Creates an output plot for the thunderfish program. 

423 

424 This output contains the raw trace where the analysis window is 

425 marked, the power-spectrum of this analysis window where the 

426 detected fish are marked, plots of averaged EOD plots, and 

427 spectra of the EOD waveforms. 

428 

429 Parameters 

430 ---------- 

431 base_name: string 

432 Basename of audio_file. 

433 message_filename: string or None 

434 Path to meta-data message. 

435 raw_data: array 

436 Dataset. 

437 rate: float 

438 Sampling rate of the dataset. 

439 channel: int or None 

440 Channel of the recording to be put into the plot title. 

441 If None, do not write the channel into the title. 

442 idx0: float 

443 Index of the beginning of the analysis window in the dataset. 

444 idx1: float 

445 Index of the end of the analysis window in the dataset. 

446 clipped: float 

447 Fraction of clipped amplitudes. 

448 psd_data: 2D array 

449 Power spectrum (frequencies and power) of the analysed data. 

450 wave_eodfs: array 

451 Frequency and power of fundamental frequency/harmonics of several fish. 

452 wave_indices: array of int 

453 Indices of wave fish mapping from wave_eodfs to eod_props. 

454 If negative, then that EOD frequency has no waveform described in eod_props. 

455 mean_eods: list of 2-D arrays with time, mean and std. 

456 Mean trace for the mean EOD plot. 

457 eod_props: list of dict 

458 Properties for each waveform in mean_eods. 

459 peak_data: list of 2_D arrays 

460 For each pulsefish a list of peak properties 

461 (index, time, and amplitude). 

462 spec_data: list of 2_D arrays 

463 For each pulsefish a power spectrum of the single pulse and for 

464 each wavefish the relative amplitudes and phases of the harmonics. 

465 indices: list of int or None 

466 Indices of the fish in eod_props to be plotted. 

467 If None try to plot all. 

468 unit: string 

469 Unit of the trace and the mean EOD. 

470 n_snippets: int 

471 Number of EOD waveform snippets to be plotted. If zero do not plot any. 

472 power_thresh: 2 D array or None 

473 Frequency (first column) and power (second column) of threshold 

474 derived from single pulse spectra to discard false wave fish. 

475 label_power: boolean 

476 If `True` put the power in decibel in addition to the frequency 

477 into the legend. 

478 all_eods: bool 

479 Plot all EOD waveforms. 

480 spec_plots: bool or 'auto' 

481 Plot amplitude spectra of EOD waveforms. 

482 If 'auto', plot them if there is a singel waveform only. 

483 skip_bad: bool 

484 Skip harmonic groups without index (entry in indices is negative). 

485 log_freq: boolean 

486 Logarithmic (True) or linear (False) frequency axis of power spectrum of recording. 

487 min_freq: float 

488 Limits of frequency axis of power spectrum of recording 

489 are set to `(min_freq, max_freq)` if `max_freq` is greater than zero 

490 max_freq: float 

491 Limits of frequency axis of power spectrum of recording 

492 are set to `(min_freq, max_freq)` and limits of power axis are computed 

493 from powers below max_freq if `max_freq` is greater than zero 

494 interactive: bool 

495 If True install some keyboard interaction. 

496 verbose: int 

497 Print out information about data to be plotted if greater than zero. 

498 

499 Returns 

500 ------- 

501 fig: plt.figure 

502 Figure with the plots. 

503 """ 

504 

505 def keypress(event): 

506 if event.key in 'pP': 

507 if idx1 > idx0: 

508 playdata = 1.0 * raw_data[idx0:idx1] 

509 else: 

510 playdata = 1.0 * raw_data[:] 

511 fade(playdata, rate, 0.1) 

512 play(playdata, rate, blocking=False) 

513 if event.key in 'mM' and message_filename: 

514 # play voice message: 

515 msg, msg_rate = load_audio(message_filename) 

516 play(msg, msg_rate, blocking=False) 

517 

518 def recording_format_coord(x, y): 

519 return 'full recording: x=%.3f s, y=%.3f' % (x, y) 

520 

521 def recordingzoom_format_coord(x, y): 

522 return 'recording zoom-in: x=%.3f s, y=%.3f' % (x, y) 

523 

524 def psd_format_coord(x, y): 

525 return 'power spectrum: x=%.1f Hz, y=%.1f dB' % (x, y) 

526 

527 def meaneod_format_coord(x, y): 

528 return 'mean EOD waveform: x=%.2f ms, y=%.3f' % (x, y) 

529 

530 def ampl_format_coord(x, y): 

531 return u'amplitude spectrum: x=%.0f, y=%.2f' % (x, y) 

532 

533 def phase_format_coord(x, y): 

534 return u'phase spectrum: x=%.0f, y=%.2f \u03c0' % (x, y/np.pi) 

535 

536 def pulsepsd_format_coord(x, y): 

537 return 'single pulse power spectrum: x=%.1f Hz, y=%.1f dB' % (x, y) 

538 

539 # count number of fish types to be plotted: 

540 if indices is None: 

541 indices = np.arange(len(eod_props)) 

542 else: 

543 indices = np.array(indices, dtype=int) 

544 nwave = 0 

545 npulse = 0 

546 for idx in indices: 

547 if eod_props[idx]['type'] == 'pulse': 

548 npulse += 1 

549 elif eod_props[idx]['type'] == 'wave': 

550 nwave += 1 

551 neods = nwave + npulse 

552 

553 if verbose > 0: 

554 print('plot: %2d waveforms: %2d wave fish, %2d pulse fish and %2d EOD frequencies.' 

555 % (len(indices), nwave, npulse, len(wave_eodfs))) 

556 

557 # size and positions: 

558 if spec_plots == 'auto': 

559 spec_plots = len(indices) == 1 

560 large_plots = spec_plots or len(indices) <= 2 

561 width = 14.0 

562 height = 10.0 

563 if all_eods and len(indices) > 0: 

564 nrows = len(indices) if spec_plots else (len(indices)+1)//2 

565 if large_plots: 

566 height = 6.0 + 4.0*nrows 

567 else: 

568 height = 6.4 + 1.9*nrows 

569 leftx = 1.0/width 

570 midx = 0.5 + leftx 

571 fullwidth = 1.0-1.4/width 

572 halfwidth = 0.5-1.4/width 

573 pheight = 3.0/height 

574 

575 # figure: 

576 plot_style() 

577 fig = plt.figure(figsize=(width, height)) 

578 if interactive: 

579 fig.canvas.mpl_connect('key_press_event', keypress) 

580 

581 # plot title: 

582 title = base_name 

583 if channel is not None: 

584 title += ' c%d' % channel 

585 ax = fig.add_axes([0.2/width, 1.0-0.6/height, 1.0-0.4/width, 0.55/height]) 

586 ax.text(0.0, 1.0, title, fontsize=22, va='top') 

587 ax.text(1.0, 1.0, 'thunderfish by Benda-Lab', fontsize=16, ha='right', va='top') 

588 ax.text(1.0, 0.0, 'version %s' % __version__, fontsize=16, ha='right', va='bottom') 

589 ax.set_frame_on(False) 

590 ax.set_axis_off() 

591 ax.set_navigate(False) 

592 

593 # layout of recording and psd plots: 

594 #force_both = True # set to True for debugging pulse and wave detection 

595 force_both = False 

596 posy = 1.0 - 4.0/height 

597 axr = None 

598 axp = None 

599 legend_inside = True 

600 legendwidth = 2.2/width if label_power else 1.7/width 

601 if neods == 0: 

602 axr = fig.add_axes([leftx, posy, fullwidth, pheight]) # top, wide 

603 if len(psd_data) > 0: 

604 axp = fig.add_axes([leftx, 2.0/height, fullwidth, pheight]) # bottom, wide 

605 else: 

606 if npulse == 0 and nwave > 2 and psd_data is not None and \ 

607 len(psd_data) > 0 and not force_both: 

608 axp = fig.add_axes([leftx, posy, fullwidth-legendwidth, pheight]) # top, wide 

609 legend_inside = False 

610 elif (npulse > 0 or psd_data is None or len(psd_data) == 0) \ 

611 and len(wave_eodfs) == 0 and not force_both: 

612 axr = fig.add_axes([leftx, posy, fullwidth, pheight]) # top, wide 

613 else: 

614 axr = fig.add_axes([leftx, posy, halfwidth, pheight]) # top left 

615 label_power = False 

616 legendwidth = 2.2/width 

617 axp = fig.add_axes([midx, posy, halfwidth, pheight]) # top, right 

618 

619 # best window data: 

620 data = raw_data[idx0:idx1] if idx1 > idx0 else raw_data 

621 

622 # plot recording 

623 pulse_colors, pulse_markers = colors_markers() 

624 pulse_colors = pulse_colors[3:] 

625 pulse_markers = pulse_markers[3:] 

626 if axr is not None: 

627 axes_style(axr) 

628 twidth = 0.1 

629 if len(indices) > 0: 

630 if eod_props[indices[0]]['type'] == 'wave': 

631 twidth = 5.0/eod_props[indices[0]]['EODf'] 

632 else: 

633 if len(wave_eodfs) > 0: 

634 twidth = 3.0/eod_props[indices[0]]['EODf'] 

635 else: 

636 twidth = 10.0/eod_props[indices[0]]['EODf'] 

637 twidth = (1+twidth//0.005)*0.005 

638 if data is not None and len(data) > 0: 

639 plot_eod_recording(axr, data, rate, unit, twidth, 

640 idx0/rate) 

641 plot_pulse_eods(axr, data, rate, 

642 zoom_window, twidth, eod_props, 

643 idx0/rate, colors=pulse_colors, 

644 markers=pulse_markers, frameon=True, 

645 loc='upper right') 

646 if axr.get_legend() is not None: 

647 axr.get_legend().get_frame().set_color('white') 

648 axr.set_title('Recording', fontsize=14, y=1.05) 

649 axr.format_coord = recordingzoom_format_coord 

650 

651 # plot psd 

652 wave_colors, wave_markers = colors_markers() 

653 if axp is not None: 

654 axes_style(axp) 

655 if power_thresh is not None: 

656 axp.plot(power_thresh[:,0], decibel(power_thresh[:,1]), '#CCCCCC', lw=1) 

657 if len(wave_eodfs) > 0: 

658 kwargs = {} 

659 if len(wave_eodfs) > 1: 

660 title = '%d EOD frequencies' % len(wave_eodfs) 

661 kwargs = {'title': title if len(wave_eodfs) > 2 else None } 

662 if legend_inside: 

663 kwargs.update({'bbox_to_anchor': (1.05, 1.1), 

664 'loc': 'upper right', 'legend_rows': 10, 

665 'frameon': True}) 

666 else: 

667 kwargs.update({'bbox_to_anchor': (1.02, 1.1), 

668 'loc': 'upper left', 'legend_rows': 14, 

669 'labelspacing': 0.6, 'frameon': False}) 

670 plot_harmonic_groups(axp, wave_eodfs, wave_indices, max_groups=0, 

671 skip_bad=skip_bad, 

672 sort_by_freq=True, label_power=label_power, 

673 colors=wave_colors, markers=wave_markers, 

674 **kwargs) 

675 if legend_inside: 

676 axp.get_legend().get_frame().set_color('white') 

677 if psd_data is not None and len(psd_data) > 0: 

678 plot_decibel_psd(axp, psd_data[:,0], psd_data[:,1], log_freq=log_freq, 

679 min_freq=min_freq, max_freq=max_freq, ymarg=5.0, color='blue') 

680 axp.yaxis.set_major_locator(ticker.MaxNLocator(6)) 

681 if len(wave_eodfs) == 1: 

682 axp.get_legend().set_visible(False) 

683 label = '%6.1f Hz' % wave_eodfs[0][0, 0] 

684 axp.set_title('Powerspectrum: %s' % label, y=1.05, fontsize=14) 

685 else: 

686 axp.set_title('Powerspectrum', y=1.05, fontsize=14) 

687 axp.format_coord = psd_format_coord 

688 

689 # get fish labels from legends: 

690 if axp is not None: 

691 w, _ = axp.get_legend_handles_labels() 

692 eodf_labels = [wi.get_label().split()[0] for wi in w] 

693 legend_wave_eodfs = np.array([float(f) if f[0] != '(' else np.nan for f in eodf_labels]) 

694 if axr is not None: 

695 p, _ = axr.get_legend_handles_labels() 

696 eodf_labels = [pi.get_label().split()[0] for pi in p] 

697 legend_pulse_eodfs = np.array([float(f) if f[0] != '(' else np.nan for f in eodf_labels]) 

698 

699 # layout: 

700 sheight = 1.4/height 

701 sstep = 1.6/height 

702 max_plots = len(indices) 

703 if not all_eods: 

704 if large_plots: 

705 max_plots = 1 if spec_plots else 2 

706 else: 

707 max_plots = 4 

708 if large_plots: 

709 pstep = pheight + 1.0/height 

710 ty = 1.08 

711 my = 1.10 

712 ny = 6 

713 else: 

714 posy -= 0.2/height 

715 pheight = 1.3/height 

716 pstep = 1.9/height 

717 ty = 1.10 

718 my = 1.16 

719 ny = 4 

720 posy -= pstep 

721 

722 # sort indices by p-p amplitude: 

723 pp_ampls = [eod_props[idx]['p-p-amplitude'] for idx in indices] 

724 pp_indices = np.argsort(pp_ampls)[::-1] 

725 

726 # plot EOD waveform and spectra: 

727 for k, idx in enumerate(indices[pp_indices]): 

728 if k >= max_plots: 

729 break 

730 # plot EOD waveform: 

731 mean_eod = mean_eods[idx] 

732 props = eod_props[idx] 

733 peaks = peak_data[idx] 

734 lx = leftx if spec_plots or k%2 == 0 else midx 

735 ax = fig.add_axes([lx, posy, halfwidth, pheight]) 

736 axes_style(ax) 

737 ax.yaxis.set_major_locator(ticker.MaxNLocator(ny)) 

738 if len(indices) > 1: 

739 ax.text(0.3, ty, '{EODf:.1f} Hz {type} fish'.format(**props), 

740 transform=ax.transAxes, fontsize=14, zorder=20) 

741 mx = 0.25 

742 else: 

743 ax.text(-0.1, ty, '{EODf:.1f} Hz {type} fish'.format(**props), 

744 transform=ax.transAxes, fontsize=14, zorder=20) 

745 ax.text(0.5, ty, 'Averaged EOD', ha='center', 

746 transform=ax.transAxes, fontsize=14, zorder=20) 

747 mx = -0.14 

748 eodf = props['EODf'] 

749 if props['type'] == 'wave': 

750 if axp is not None: 

751 wk = np.nanargmin(np.abs(legend_wave_eodfs - eodf)) 

752 ma = ml.Line2D([mx], [my], color=w[wk].get_color(), marker=w[wk].get_marker(), 

753 markersize=w[wk].get_markersize(), mec='none', clip_on=False, 

754 label=w[wk].get_label(), transform=ax.transAxes) 

755 ax.add_line(ma) 

756 else: 

757 if axr is not None and len(legend_pulse_eodfs) > 0: 

758 pk = np.argmin(np.abs(legend_pulse_eodfs - eodf)) 

759 ma = ml.Line2D([mx], [my], color=p[pk].get_color(), marker=p[pk].get_marker(), 

760 markersize=p[pk].get_markersize(), mec='none', clip_on=False, 

761 label=p[pk].get_label(), transform=ax.transAxes) 

762 ax.add_line(ma) 

763 plot_eod_waveform(ax, mean_eod, props, peaks, unit) 

764 if props['type'] == 'pulse' and 'times' in props: 

765 plot_eod_snippets(ax, data, rate, mean_eod[0,0], mean_eod[-1,0], 

766 props['times'], n_snippets, props['flipped']) 

767 if not large_plots and k < max_plots-2: 

768 ax.set_xlabel('') 

769 ax.format_coord = meaneod_format_coord 

770 

771 # plot spectra: 

772 if spec_plots: 

773 spec = spec_data[idx] 

774 if props['type'] == 'pulse': 

775 ax = fig.add_axes([midx, posy, halfwidth, pheight]) 

776 axes_style(ax) 

777 plot_pulse_spectrum(ax, spec, props) 

778 ax.set_title('Single pulse spectrum', fontsize=14, y=1.05) 

779 ax.format_coord = pulsepsd_format_coord 

780 else: 

781 axa = fig.add_axes([midx, posy+sstep, halfwidth, sheight]) 

782 axes_style(axa) 

783 axp = fig.add_axes([midx, posy, halfwidth, sheight]) 

784 axes_style(axp) 

785 plot_wave_spectrum(axa, axp, spec, props, unit) 

786 axa.set_title('Amplitude and phase spectrum', fontsize=14, y=1.05) 

787 axa.set_xticklabels([]) 

788 axa.yaxis.set_major_locator(ticker.MaxNLocator(4)) 

789 axa.format_coord = ampl_format_coord 

790 axp.format_coord = phase_format_coord 

791 

792 if spec_plots or k%2 == 1: 

793 posy -= pstep 

794 

795 # whole trace: 

796 ax = fig.add_axes([leftx, 0.6/height, fullwidth, 0.9/height]) 

797 axes_style(ax) 

798 if raw_data is not None and len(raw_data) > 0: 

799 plot_data_window(ax, raw_data, rate, unit, idx0, idx1, clipped) 

800 ax.format_coord = recording_format_coord 

801 

802 return fig 

803 

804 

805def plot_eod_subplots(base_name, subplots, raw_data, rate, idx0, idx1, 

806 clipped, psd_data, wave_eodfs, wave_indices, mean_eods, 

807 eod_props, peak_data, spec_data, unit, zoom_window, 

808 n_snippets=10, power_thresh=None, label_power=True, 

809 skip_bad=True, log_freq=False, 

810 min_freq=0.0, max_freq=3000.0, save=True): 

811 """Plot time traces and spectra into separate windows or files. 

812 

813 Parameters 

814 ---------- 

815 base_name: string 

816 Basename of audio_file. 

817 subplots: string 

818 Specifies which subplots to plot: 

819 r) recording with best window, t) data trace with detected pulse fish, 

820 p) power spectrum with detected wave fish, w/W) mean EOD waveform, 

821 s/S) EOD spectrum, e/E) EOD waveform and spectra. With capital letters 

822 all fish are saved into a single pdf file, with small letters each fish 

823 is saved into a separate file. 

824 raw_data: array 

825 Dataset. 

826 rate: float 

827 Sampling rate of the dataset. 

828 idx0: float 

829 Index of the beginning of the analysis window in the dataset. 

830 idx1: float 

831 Index of the end of the analysis window in the dataset. 

832 clipped: float 

833 Fraction of clipped amplitudes. 

834 psd_data: 2D array 

835 Power spectrum (frequencies and power) of the analysed data. 

836 wave_eodfs: array 

837 Frequency and power of fundamental frequency/harmonics of several fish. 

838 wave_indices: array of int 

839 Indices of wave fish mapping from wave_eodfs to eod_props. 

840 If negative, then that EOD frequency has no waveform described in eod_props. 

841 mean_eods: list of 2-D arrays with time, mean and std. 

842 Mean trace for the mean EOD plot. 

843 eod_props: list of dict 

844 Properties for each waveform in mean_eods. 

845 peak_data: list of 2_D arrays 

846 For each pulsefish a list of peak properties 

847 (index, time, and amplitude). 

848 spec_data: list of 2_D arrays 

849 For each pulsefish a power spectrum of the single pulse and for 

850 each wavefish the relative amplitudes and phases of the harmonics. 

851 unit: string 

852 Unit of the trace and the mean EOD. 

853 n_snippets: int 

854 Number of EOD waveform snippets to be plotted. If zero do not plot any. 

855 power_thresh: 2 D array or None 

856 Frequency (first column) and power (second column) of threshold 

857 derived from single pulse spectra to discard false wave fish. 

858 label_power: boolean 

859 If `True` put the power in decibel in addition to the frequency 

860 into the legend. 

861 skip_bad: bool 

862 Skip harmonic groups without index (entry in indices is negative). 

863 log_freq: boolean 

864 Logarithmic (True) or linear (False) frequency axis of power spectrum of recording. 

865 min_freq: float 

866 Limits of frequency axis of power spectrum of recording 

867 are set to `(min_freq, max_freq)` if `max_freq` is greater than zero 

868 max_freq: float 

869 Limits of frequency axis of power spectrum of recording 

870 are set to `(min_freq, max_freq)` and limits of power axis are computed 

871 from powers below max_freq if `max_freq` is greater than zero 

872 save: bool 

873 If True save plots to files instead of showing them. 

874 """ 

875 plot_style() 

876 if 'r' in subplots: 

877 fig, ax = plt.subplots(figsize=(10, 2)) 

878 fig.subplots_adjust(left=0.07, right=0.99, bottom=0.22, top=0.95) 

879 plot_data_window(ax, raw_data, rate, unit, idx0, idx1, clipped) 

880 ax.yaxis.set_major_locator(ticker.MaxNLocator(5)) 

881 axes_style(ax) 

882 if save: 

883 fig.savefig(base_name + '-recording.pdf') 

884 if 't' in subplots: 

885 fig, ax = plt.subplots(figsize=(10, 6)) 

886 twidth = 0.1 

887 if len(eod_props) > 0: 

888 if eod_props[0]['type'] == 'wave': 

889 twidth = 5.0/eod_props[0]['EODf'] 

890 else: 

891 if len(wave_eodfs) > 0: 

892 twidth = 3.0/eod_props[0]['EODf'] 

893 else: 

894 twidth = 10.0/eod_props[0]['EODf'] 

895 twidth = (1+twidth//0.005)*0.005 

896 pulse_colors, pulse_markers = colors_markers() 

897 pulse_colors = pulse_colors[3:] 

898 pulse_markers = pulse_markers[3:] 

899 plot_eod_recording(ax, raw_data[idx0:idx1], rate, unit, 

900 twidth, idx0/rate) 

901 plot_pulse_eods(ax, raw_data[idx0:idx1], rate, zoom_window, 

902 twidth, eod_props, idx0/rate, 

903 colors=pulse_colors, markers=pulse_markers, 

904 frameon=True, loc='upper right') 

905 if ax.get_legend() is not None: 

906 ax.get_legend().get_frame().set_color('white') 

907 axes_style(ax) 

908 if save: 

909 fig.savefig(base_name + '-trace.pdf') 

910 if 'p' in subplots: 

911 fig, ax = plt.subplots(figsize=(10, 5)) 

912 fig.subplots_adjust(left=0.08, right=0.975, bottom=0.11, top=0.9) 

913 axes_style(ax) 

914 if power_thresh is not None: 

915 ax.plot(power_thresh[:,0], decibel(power_thresh[:,1]), '#CCCCCC', lw=1) 

916 if len(wave_eodfs) > 0: 

917 kwargs = {} 

918 if len(wave_eodfs) > 1: 

919 title = '%d EOD frequencies' % len(wave_eodfs) 

920 kwargs = {'title': title if len(wave_eodfs) > 2 else None } 

921 if len(wave_eodfs) > 2: 

922 fig.subplots_adjust(left=0.08, right=0.78, bottom=0.11, top=0.9) 

923 kwargs.update({'bbox_to_anchor': (1.01, 1.1), 

924 'loc': 'upper left', 'legend_rows': 14, 

925 'labelspacing': 0.6}) 

926 else: 

927 kwargs.update({'bbox_to_anchor': (1.05, 1.1), 

928 'loc': 'upper right', 'legend_rows': 10}) 

929 wave_colors, wave_markers = colors_markers() 

930 plot_harmonic_groups(ax, wave_eodfs, wave_indices, max_groups=0, 

931 skip_bad=skip_bad, 

932 sort_by_freq=True, label_power=label_power, 

933 colors=wave_colors, markers=wave_markers, 

934 frameon=False, **kwargs) 

935 plot_decibel_psd(ax, psd_data[:,0], psd_data[:,1], log_freq=log_freq, 

936 min_freq=min_freq, max_freq=max_freq, ymarg=5.0, color='blue') 

937 ax.yaxis.set_major_locator(ticker.MaxNLocator(6)) 

938 if len(wave_eodfs) == 1: 

939 ax.get_legend().set_visible(False) 

940 label = '%6.1f Hz' % wave_eodfs[0][0, 0] 

941 ax.set_title('Powerspectrum: %s' % label, y=1.05) 

942 else: 

943 ax.set_title('Powerspectrum', y=1.05) 

944 if save: 

945 fig.savefig(base_name + '-psd.pdf') 

946 if 'w' in subplots or 'W' in subplots: 

947 mpdf = None 

948 if 'W' in subplots: 

949 mpdf = PdfPages(base_name + '-waveforms.pdf') 

950 for meod, props, peaks in zip(mean_eods, eod_props, peak_data): 

951 if meod is None: 

952 continue 

953 fig, ax = plt.subplots(figsize=(5, 3)) 

954 fig.subplots_adjust(left=0.18, right=0.98, bottom=0.15, top=0.9) 

955 if not props is None: 

956 ax.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props)) 

957 plot_eod_waveform(ax, meod, props, peaks, unit) 

958 data = raw_data[idx0:idx1] if idx1 > idx0 else raw_data 

959 if not props is None and props['type'] == 'pulse' and \ 

960 'times' in props: 

961 plot_eod_snippets(ax, data, rate, meod[0,0], 

962 meod[-1,0], props['times'], 

963 n_snippets) 

964 ax.yaxis.set_major_locator(ticker.MaxNLocator(6)) 

965 axes_style(ax) 

966 if mpdf is None: 

967 if save: 

968 fig.savefig(base_name + '-waveform-%d.pdf' % props['index']) 

969 else: 

970 mpdf.savefig(fig) 

971 if mpdf is not None: 

972 mpdf.close() 

973 if 's' in subplots or 'S' in subplots: 

974 mpdf = None 

975 if 'S' in subplots: 

976 mpdf = PdfPages(base_name + '-spectrum.pdf') 

977 for props, peaks, spec in zip(eod_props, peak_data, spec_data): 

978 if spec is None: 

979 continue 

980 if props is not None and props['type'] == 'pulse': 

981 fig, ax = plt.subplots(figsize=(5, 3.5)) 

982 fig.subplots_adjust(left=0.15, right=0.967, bottom=0.16, top=0.88) 

983 axes_style(ax) 

984 ax.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props), y=1.07) 

985 plot_pulse_spectrum(ax, spec, props) 

986 else: 

987 fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(5, 3.5)) 

988 fig.subplots_adjust(left=0.15, right=0.97, bottom=0.16, top=0.88, hspace=0.4) 

989 axes_style(ax1) 

990 axes_style(ax2) 

991 if not props is None: 

992 ax1.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props), y=1.15) 

993 plot_wave_spectrum(ax1, ax2, spec, props, unit) 

994 ax1.set_xticklabels([]) 

995 ax1.yaxis.set_major_locator(ticker.MaxNLocator(4)) 

996 if mpdf is None: 

997 if save: 

998 fig.savefig(base_name + '-spectrum-%d.pdf' % props['index']) 

999 else: 

1000 mpdf.savefig(fig) 

1001 if mpdf is not None: 

1002 mpdf.close() 

1003 if 'e' in subplots or 'E' in subplots: 

1004 mpdf = None 

1005 if 'E' in subplots: 

1006 mpdf = PdfPages(base_name + '-eods.pdf') 

1007 for meod, props, peaks, spec in zip(mean_eods, eod_props, peak_data, spec_data): 

1008 if meod is None or spec is None: 

1009 continue 

1010 fig = plt.figure(figsize=(10, 3.5)) 

1011 gs = gridspec.GridSpec(nrows=2, ncols=2, left=0.09, right=0.98, 

1012 bottom=0.16, top=0.88, wspace=0.4, hspace=0.4) 

1013 ax1 = fig.add_subplot(gs[:,0]) 

1014 if not props is None: 

1015 ax1.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props), y=1.07) 

1016 plot_eod_waveform(ax1, meod, props, peaks, unit) 

1017 data = raw_data[idx0:idx1] if idx1 > idx0 else raw_data 

1018 if not props is None and props['type'] == 'pulse' and 'times' in props: 

1019 plot_eod_snippets(ax1, data, rate, meod[0,0], 

1020 meod[-1,0], props['times'], 

1021 n_snippets) 

1022 ax1.yaxis.set_major_locator(ticker.MaxNLocator(6)) 

1023 axes_style(ax1) 

1024 if not props is None and props['type'] == 'pulse': 

1025 ax2 = fig.add_subplot(gs[:,1]) 

1026 axes_style(ax2) 

1027 plot_pulse_spectrum(ax2, spec, props) 

1028 ax2.set_title('Single pulse spectrum', y=1.07) 

1029 else: 

1030 ax2 = fig.add_subplot(gs[0,1]) 

1031 ax3 = fig.add_subplot(gs[1,1]) 

1032 axes_style(ax2) 

1033 axes_style(ax3) 

1034 plot_wave_spectrum(ax2, ax3, spec, props, unit) 

1035 ax2.set_title('Amplitude and phase spectrum', y=1.15) 

1036 ax2.set_xticklabels([]) 

1037 ax2.yaxis.set_major_locator(ticker.MaxNLocator(4)) 

1038 if mpdf is None: 

1039 if save: 

1040 fig.savefig(base_name + '-eod-%d.pdf' % props['index']) 

1041 else: 

1042 mpdf.savefig(fig) 

1043 if mpdf is not None: 

1044 mpdf.close() 

1045 if not save: 

1046 plt.show() 

1047 plt.close('all') 

1048 

1049 

1050def thunderfish_plot(files, data_path=None, load_kwargs={}, 

1051 all_eods=False, spec_plots='auto', skip_bad=True, 

1052 save_plot=False, multi_pdf=None, 

1053 save_subplots='', log_freq=False, min_freq=0.0, 

1054 max_freq=3000.0, output_folder='.', 

1055 keep_path=False, verbose=0): 

1056 """Generate plots from saved analysis results. 

1057 

1058 Parameters 

1059 ---------- 

1060 files: list of str 

1061 Analysis files from a single recording. 

1062 data_path: str 

1063 Path where to find the raw data. 

1064 load_kwargs: dict 

1065 Key-word arguments for the `load_data()` function. 

1066 all_eods: bool 

1067 If True, plot all EOD waveforms. 

1068 spec_plots: bool or 'auto' 

1069 Plot amplitude spectra of EOD waveforms. 

1070 If 'auto', plot them if there is a singel waveform only. 

1071 skip_bad: bool 

1072 Skip harmonic groups without index in the spectrum plot. 

1073 save_plot: bool 

1074 If True, save plots as pdf file. 

1075 multi_pdf: matplotlib.PdfPages or None 

1076 PdfPages instance in which to save plots. 

1077 save_subplots: string 

1078 If not empty, specifies subplots to be saved as separate pdf 

1079 files: r) recording with best window, t) data trace with 

1080 detected pulse fish, p) power spectrum with detected wave 

1081 fish, w/W) mean EOD waveform, s/S) EOD spectrum, e/E) EOD 

1082 waveform and spectra. Capital letters produce a single 

1083 multipage pdf containing plots of all detected fish. 

1084 log_freq: boolean 

1085 Logarithmic (True) or linear (False) frequency axis of 

1086 power spectrum of recording. 

1087 min_freq: float 

1088 Limits of frequency axis of power spectrum of recording 

1089 are set to `(min_freq, max_freq)`, if `max_freq` is greater than zero 

1090 max_freq: float 

1091 Limits of frequency axis of power spectrum of recording 

1092 are set to `(min_freq, max_freq)` and limits of power axis are computed 

1093 from powers below max_freq, if `max_freq` is greater than zero 

1094 output_folder: string 

1095 Folder where to save results. 

1096 keep_path: bool 

1097 Add relative path of data files to output path. 

1098 verbose: int 

1099 Verbosity level (for debugging). 

1100 """ 

1101 if len(save_subplots) == 0: 

1102 save_subplots = 'rtpwsed' # plot everything 

1103 # load analysis results: 

1104 mean_eods, wave_eodfs, wave_indices, eod_props, spec_data, \ 

1105 peak_data, base_name, channel, unit = load_analysis(files) 

1106 if len(mean_eods) == 0 or all(me is None for me in mean_eods): 

1107 save_subplots = save_subplots.replace('w', '') 

1108 save_subplots = save_subplots.replace('W', '') 

1109 save_subplots = save_subplots.replace('e', '') 

1110 save_subplots = save_subplots.replace('E', '') 

1111 save_subplots = save_subplots.replace('d', '') 

1112 if len(spec_data) == 0 or all(sd is None for sd in spec_data): 

1113 save_subplots = save_subplots.replace('s', '') 

1114 save_subplots = save_subplots.replace('S', '') 

1115 save_subplots = save_subplots.replace('e', '') 

1116 save_subplots = save_subplots.replace('E', '') 

1117 save_subplots = save_subplots.replace('d', '') 

1118 clipped = 0.0 

1119 if len(eod_props) > 0 and not eod_props[0] is None and \ 

1120 'winclipped' in eod_props[0]: 

1121 clipped = eod_props[0]['winclipped'] 

1122 zoom_window = [1.2, 1.3] 

1123 # load recording: 

1124 psd_data = None 

1125 if base_name: 

1126 name = os.path.basename(base_name) if data_path and data_path != '.' else base_name 

1127 data_path = os.path.join(data_path, name) 

1128 data, rate, idx0, idx1, data_path = \ 

1129 load_recording(data_path, channel, load_kwargs, 

1130 eod_props, verbose-1) 

1131 if data is None: 

1132 save_subplots = save_subplots.replace('r', '') 

1133 save_subplots = save_subplots.replace('t', '') 

1134 save_subplots = save_subplots.replace('d', '') 

1135 if verbose > 0: 

1136 print('loaded', data_path) 

1137 if len(eod_props) > 0 and not eod_props[0] is None and \ 

1138 'dfreq' in eod_props[0] and data is not None and len(data) > 0: 

1139 psd_data = multi_psd(data[idx0:idx1], rate, 

1140 1.1*eod_props[0]['dfreq'])[0] 

1141 if psd_data is not None and len(psd_data) > 0: 

1142 for idx, fish in zip(wave_indices, wave_eodfs): 

1143 if idx < 0: 

1144 for k in range(len(fish)): 

1145 fish[k,1] = psd_data[np.argmin(np.abs(psd_data[:,0] - fish[k,0])),1] 

1146 if psd_data is None: 

1147 save_subplots = save_subplots.replace('p', '') 

1148 save_subplots = save_subplots.replace('d', '') 

1149 # file name for output files: 

1150 fn = base_name if keep_path else os.path.basename(base_name) 

1151 output_basename = os.path.join(output_folder, fn) 

1152 if channel >= 0: 

1153 output_basename += f'-c{channel}' 

1154 # make directory if necessary: 

1155 if keep_path: 

1156 outpath = os.path.dirname(output_basename) 

1157 if not os.path.exists(outpath): 

1158 if verbose > 0: 

1159 print('mkdir %s' % outpath) 

1160 os.makedirs(outpath) 

1161 # plot: 

1162 if len(save_subplots) == 0 or 'd' in save_subplots: 

1163 fig = plot_eods(os.path.basename(base_name), None, data, rate, 

1164 channel, idx0, idx1, clipped, psd_data, 

1165 wave_eodfs, wave_indices, mean_eods, 

1166 eod_props, peak_data, spec_data, None, unit, 

1167 zoom_window, 10, None, True, all_eods, 

1168 spec_plots, skip_bad, log_freq, min_freq, 

1169 max_freq, interactive=not save_plot, 

1170 verbose=verbose-1) 

1171 if save_plot: 

1172 if multi_pdf is not None: 

1173 multi_pdf.savefig(fig) 

1174 else: 

1175 fig.savefig(output_basename + '.pdf') 

1176 else: 

1177 fig.canvas.manager.set_window_title('thunderfish') 

1178 plt.show() 

1179 plt.close() 

1180 save_subplots = save_subplots.replace('d', '') 

1181 if len(save_subplots) > 0: 

1182 plot_eod_subplots(output_basename, save_subplots, data, rate, 

1183 idx0, idx1, clipped, psd_data, wave_eodfs, 

1184 wave_indices, mean_eods, eod_props, 

1185 peak_data, spec_data, unit, zoom_window, 10, 

1186 None, True, skip_bad, log_freq, min_freq, 

1187 max_freq, save_plot) 

1188 return None 

1189 

1190 

1191def thunderfish(filename, load_kwargs, cfg, channel=0, 

1192 time=None, time_file=False, 

1193 mode='wp', log_freq=False, min_freq=0.0, max_freq=3000, 

1194 save_data=False, zip_file=False, 

1195 all_eods=False, spec_plots='auto', skip_bad=True, 

1196 save_plot=False, multi_pdf=None, save_subplots='', 

1197 output_folder='.', keep_path=False, 

1198 verbose=0, plot_level=0): 

1199 """Automatically detect and analyze all EOD waveforms in a short recording. 

1200 

1201 Parameters 

1202 ---------- 

1203 filename: string 

1204 Path of the data file to be analyzed. 

1205 load_kwargs: dict 

1206 Key-word arguments for the `load_data()` function. 

1207 cfg: dict 

1208 channel: int 

1209 Channel to be analyzed. 

1210 time: string, float, or None 

1211 Start time of analysis window: "beginning", "center", "end", 

1212 "best", or time in seconds (as float or string). If not None 

1213 overwrites "windowPosition" in cofiguration file. 

1214 time_file: bool 

1215 If `True` add time of analysis window to output file names. 

1216 mode: 'w', 'p', 'P', 'wp', or 'wP' 

1217 Analyze wavefish ('w'), all pulse fish ('p'), or largest pulse 

1218 fish only ('P'). 

1219 log_freq: boolean 

1220 Logarithmic (True) or linear (False) frequency axis of 

1221 power spectrum of recording. 

1222 min_freq: float 

1223 Limits of frequency axis of power spectrum of recording 

1224 are set to `(min_freq, max_freq)`, if `max_freq` is greater than zero 

1225 max_freq: float 

1226 Limits of frequency axis of power spectrum of recording 

1227 are set to `(min_freq, max_freq)` and limits of power axis are computed 

1228 from powers below max_freq, if `max_freq` is greater than zero 

1229 save_data: bool 

1230 If True save analysis results in files. If False, just plot the data. 

1231 zip_data: bool 

1232 If True, store all analysis results in a single zip file. 

1233 all_eods: bool 

1234 If True, plot all EOD waveforms. 

1235 spec_plots: bool or 'auto' 

1236 Plot amplitude spectra of EOD waveforms. 

1237 If 'auto', plot them if there is a singel waveform only. 

1238 skip_bad: bool 

1239 Skip harmonic groups without index in the spectrum plot. 

1240 save_plot: bool 

1241 If True, save plots as pdf file. 

1242 multi_pdf: matplotlib.PdfPages or None 

1243 PdfPages instance in which to save plots. 

1244 save_subplots: string 

1245 If not empty, specifies subplots to be saved as separate pdf 

1246 files: r) recording with best window, t) data trace with 

1247 detected pulse fish, p) power spectrum with detected wave 

1248 fish, w/W) mean EOD waveform, s/S) EOD spectrum, e/E) EOD 

1249 waveform and spectra. Capital letters produce a single 

1250 multipage pdf containing plots of all detected fish. 

1251 output_folder: string 

1252 Folder where to save results. 

1253 keep_path: bool 

1254 Add relative path of data files to output path. 

1255 verbose: int 

1256 Verbosity level (for debugging). 

1257 plot_level: int 

1258 Plot intermediate results. 

1259 

1260 Returns 

1261 ------- 

1262 msg: string or None 

1263 In case of errors, an error message. 

1264 """ 

1265 # check data file: 

1266 if len(filename) == 0: 

1267 return 'you need to specify a file containing some data' 

1268 

1269 # file names: 

1270 fn = filename if keep_path else os.path.basename(filename) 

1271 outfilename = os.path.splitext(fn)[0] 

1272 messagefilename = os.path.splitext(fn)[0] + '-message.wav' 

1273 if not os.path.isfile(messagefilename): 

1274 messagefilename = None 

1275 

1276 # load data: 

1277 try: 

1278 all_data, rate, unit, ampl_max = load_data(filename, 

1279 verbose=verbose, 

1280 **load_kwargs) 

1281 except IOError as e: 

1282 return '%s: failed to open file: %s' % (filename, str(e)) 

1283 # select channel: 

1284 channels = all_data.shape[1] 

1285 chan_list = [channel] 

1286 if channel < 0: 

1287 chan_list = range(channels) 

1288 elif channel >= channels: 

1289 return '%s: invalid channel %d (%d channels)' % (filename, channel, channels) 

1290 # process all channels: 

1291 for chan in chan_list: 

1292 raw_data = all_data[:,chan] 

1293 if len(raw_data) <= 1: 

1294 return '%s: empty data file' % filename 

1295 if verbose >= 0 and len(chan_list) > 1: 

1296 print(' channel %d' % chan) 

1297 

1298 # analysis window: 

1299 win_pos = cfg.value('windowPosition') 

1300 if time is not None: 

1301 win_pos = time 

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

1303 analysis_window(raw_data, rate, ampl_max, win_pos, cfg, 

1304 plot_level>0) 

1305 found_bestwindow = idx1 > 0 

1306 if not found_bestwindow: 

1307 return '%s: not enough data for requested window length. You may want to adjust the windowSize parameter in the configuration file.' % filename 

1308 

1309 # detect EODs in the data: 

1310 psd_data, wave_eodfs, wave_indices, eod_props, \ 

1311 mean_eods, spec_data, peak_data, power_thresh, skip_reason, zoom_window = \ 

1312 detect_eods(data, rate, min_clip, max_clip, filename, mode, 

1313 verbose, plot_level, cfg) 

1314 if not found_bestwindow: 

1315 wave_eodfs = [] 

1316 wave_indices = [] 

1317 eod_props = [] 

1318 mean_eods = [] 

1319 

1320 # add analysis window to EOD properties: 

1321 for props in eod_props: 

1322 props['twin'] = idx0/rate 

1323 props['window'] = (idx1 - idx0)/rate 

1324 props['winclipped'] = clipped 

1325 

1326 # warning message in case no fish has been found: 

1327 if found_bestwindow and not eod_props : 

1328 msg = ', '.join(skip_reason) 

1329 if msg: 

1330 print(filename + ': no fish found: %s' % msg) 

1331 else: 

1332 print(filename + ': no fish found.') 

1333 

1334 # file name for output files: 

1335 output_basename = os.path.join(output_folder, outfilename) 

1336 if channels > 1: 

1337 if channels > 100: 

1338 output_basename += '-c%03d' % chan 

1339 elif channels > 10: 

1340 output_basename += '-c%02d' % chan 

1341 else: 

1342 output_basename += '-c%d' % chan 

1343 if time_file: 

1344 output_basename += '-t%.0fs' % (idx0/rate) 

1345 # make directory if necessary: 

1346 if keep_path and found_bestwindow: 

1347 outpath = os.path.dirname(output_basename) 

1348 if not os.path.exists(outpath): 

1349 if verbose > 0: 

1350 print('mkdir %s' % outpath) 

1351 os.makedirs(outpath) 

1352 # save results to files: 

1353 if save_data: 

1354 remove_eod_files(output_basename, verbose, cfg) 

1355 if found_bestwindow: 

1356 save_analysis(output_basename, zip_file, eod_props, 

1357 mean_eods, spec_data, peak_data, 

1358 wave_eodfs, wave_indices, unit, verbose, 

1359 **write_table_args(cfg)) 

1360 # summary plots: 

1361 if save_plot or not save_data: 

1362 n_snippets = 10 

1363 if len(save_subplots) == 0 or 'd' in save_subplots: 

1364 chl = chan if channels > 1 else None 

1365 fig = plot_eods(outfilename, messagefilename, 

1366 raw_data, rate, chl, idx0, idx1, 

1367 clipped, psd_data[0], wave_eodfs, 

1368 wave_indices, mean_eods, eod_props, 

1369 peak_data, spec_data, None, unit, 

1370 zoom_window, n_snippets, power_thresh, 

1371 True, all_eods, spec_plots, skip_bad, 

1372 log_freq, min_freq, max_freq, 

1373 interactive=not save_plot, 

1374 verbose=verbose) 

1375 if save_plot: 

1376 if multi_pdf is not None: 

1377 multi_pdf.savefig(fig) 

1378 else: 

1379 fig.savefig(output_basename + '.pdf') 

1380 else: 

1381 fig.canvas.manager.set_window_title('thunderfish') 

1382 plt.show() 

1383 plt.close() 

1384 save_subplots = save_subplots.replace('d', '') 

1385 if len(save_subplots) > 0: 

1386 plot_eod_subplots(output_basename, save_subplots, 

1387 raw_data, rate, idx0, idx1, clipped, 

1388 psd_data[0], wave_eodfs, 

1389 wave_indices, mean_eods, eod_props, 

1390 peak_data, spec_data, unit, 

1391 zoom_window, n_snippets, 

1392 power_thresh, True, skip_bad, 

1393 log_freq, min_freq, max_freq, 

1394 save_plot) 

1395 return None 

1396 

1397 

1398def run_thunderfish(file_args): 

1399 """Helper function for mutlithreading Pool().map(). 

1400 """ 

1401 results = file_args[1][0] 

1402 verbose = file_args[1][-1] if results else file_args[1][-2]+1 

1403 if verbose > 1: 

1404 print('='*70) 

1405 try: 

1406 if results: 

1407 thunderfish_plot(file_args[0], *file_args[1][1:]) 

1408 else: 

1409 if verbose > 0: 

1410 print('analyze recording %s ...' % file_args[0]) 

1411 msg = thunderfish(file_args[0], *file_args[1][1:]) 

1412 if msg: 

1413 print(msg) 

1414 except (KeyboardInterrupt, SystemExit): 

1415 print('\nthunderfish interrupted by user... exit now.') 

1416 sys.exit(0) 

1417 except: 

1418 print(traceback.format_exc()) 

1419 

1420 

1421def main(cargs=None): 

1422 # config file name: 

1423 cfgfile = __package__ + '.cfg' 

1424 

1425 # command line arguments: 

1426 if cargs is None: 

1427 cargs = sys.argv[1:] 

1428 parser = argparse.ArgumentParser(add_help=False, 

1429 description='Analyze EOD waveforms of weakly electric fish.', 

1430 epilog='version %s by Benda-Lab (2015-%s)' % (__version__, __year__)) 

1431 parser.add_argument('-h', '--help', action='store_true', 

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

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

1434 parser.add_argument('-v', action='count', dest='verbose', default=0, 

1435 help='verbosity level. Increase by specifying -v multiple times, or like -vvv') 

1436 parser.add_argument('-V', action='count', dest='plot_level', default=0, 

1437 help='level for debugging plots. Increase by specifying -V multiple times, or like -VVV') 

1438 parser.add_argument('-c', dest='save_config', action='store_true', 

1439 help='save configuration to file {0} after reading all configuration files'.format(cfgfile)) 

1440 parser.add_argument('--channel', default=0, type=int, 

1441 help='channel to be analyzed (defaults to first channel, negative channel selects all channels)') 

1442 parser.add_argument('-t', dest='time', default=None, type=str, metavar='TIME', 

1443 help='start time of analysis window in recording: "beginning", "center", "end", "best", or time in seconds (overwrites "windowPosition" in cofiguration file)') 

1444 parser.add_argument('-u', dest='unwrap', action='store_true', 

1445 help='unwrap clipped files, toggles unwrap setting of config file.') 

1446 parser.add_argument('-T', dest='time_file', action='store_true', 

1447 help='add start time of analysis file to output file names') 

1448 parser.add_argument('-m', dest='mode', default='wp', type=str, 

1449 choices=['w', 'p', 'wp'], 

1450 help='extract wave "w" and/or pulse "p" fish EODs') 

1451 parser.add_argument('-a', dest='all_eods', action='store_true', 

1452 help='show all EOD waveforms in the summary plot') 

1453 parser.add_argument('-S', dest='spec_plots', action='store_true', 

1454 help='plot spectra for all EOD waveforms in the summary plot') 

1455 parser.add_argument('-b', dest='skip_bad', action='store_false', 

1456 help='indicate bad EODs in legend of power spectrum') 

1457 parser.add_argument('-l', dest='log_freq', type=float, metavar='MINFREQ', 

1458 nargs='?', const=100.0, default=0.0, 

1459 help='logarithmic frequency axis in power spectrum with optional minimum frequency (defaults to 100 Hz)') 

1460 parser.add_argument('-p', dest='save_plot', action='store_true', 

1461 help='save output plots as pdf files') 

1462 parser.add_argument('-M', dest='multi_pdf', default='', type=str, metavar='PDFFILE', 

1463 help='save all summary plots of all recordings in a multi page pdf file. Disables parallel jobs.') 

1464 parser.add_argument('-P', dest='save_subplots', default='', type=str, metavar='rtpwsed', 

1465 help='save subplots as separate pdf files: r) recording with analysis window, t) data trace with detected pulse fish, p) power spectrum with detected wave fish, w/W) mean EOD waveform, s/S) EOD spectrum, e/E) EOD waveform and spectra, d) the default summary plot. Capital letters produce a single multipage pdf containing plots of all detected fish') 

1466 parser.add_argument('-d', dest='rawdata_path', default='.', type=str, metavar='PATH', 

1467 help='path to raw EOD recordings needed for plotting based on analysis results') 

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

1469 help='number of jobs run in parallel. Without argument use all CPU cores.') 

1470 parser.add_argument('-s', dest='save_data', action='store_true', 

1471 help='save analysis results to files') 

1472 parser.add_argument('-z', dest='zip_file', action='store_true', 

1473 help='save analysis results in a single zip file') 

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

1475 choices=TableData.formats + ['py'], 

1476 help='file format used for saving analysis results, defaults to the format specified in the configuration file or "csv"') 

1477 parser.add_argument('-o', dest='outpath', default='.', type=str, 

1478 help='path where to store results and figures (defaults to current working directory)') 

1479 parser.add_argument('-k', dest='keep_path', action='store_true', 

1480 help='keep path of input file when saving analysis files, i.e. append path of input file to OUTPATH') 

1481 parser.add_argument('-i', dest='load_kwargs', default=[], 

1482 action='append', metavar='KWARGS', 

1483 help='key-word arguments for the data loader function') 

1484 parser.add_argument('file', nargs='*', default='', type=str, 

1485 help='name of a file with time series data of an EOD recording, may include wildcards') 

1486 args = parser.parse_args(cargs) 

1487 

1488 # help: 

1489 if args.help: 

1490 parser.print_help() 

1491 print('') 

1492 print('examples:') 

1493 print('- analyze the single file data.wav interactively:') 

1494 print(' > thunderfish data.wav') 

1495 print('- extract wavefish only:') 

1496 print(' > thunderfish -m w data.wav') 

1497 print('- automatically analyze all wav files in the current working directory and save analysis results and plot to files:') 

1498 print(' > thunderfish -s -p *.wav') 

1499 print('- analyze all wav files in the river1/ directory, use all CPUs, and write files directly to "results/":') 

1500 print(' > thunderfish -j -s -p -o results/ river1/*.wav') 

1501 print('- analyze all wav files in the river1/ directory and write files to "results/river1/":') 

1502 print(' > thunderfish -s -p -o results/ -k river1/*.wav') 

1503 print('- write configuration file:') 

1504 print(' > thunderfish -c') 

1505 parser.exit() 

1506 

1507 # set verbosity level from command line: 

1508 verbose = args.verbose 

1509 plot_level = args.plot_level 

1510 if verbose < plot_level: 

1511 verbose = plot_level 

1512 

1513 # interactive plot: 

1514 plt.rcParams['keymap.quit'] = 'ctrl+w, alt+q, q' 

1515 plt.ioff() 

1516 

1517 # expand wildcard patterns: 

1518 files = [] 

1519 if os.name == 'nt': 

1520 for fn in args.file: 

1521 files.extend(glob.glob(fn)) 

1522 else: 

1523 files = [f for f in args.file if '-message' not in f] 

1524 

1525 # save configuration: 

1526 if args.save_config: 

1527 file_name = files[0] if len(files) else '' 

1528 cfg = configuration() 

1529 cfg.load_files(cfgfile, file_name, 4, verbose) 

1530 save_configuration(cfg, cfgfile) 

1531 exit() 

1532 elif len(files) == 0: 

1533 parser.error('you need to specify at least one file for the analysis') 

1534 

1535 # configure: 

1536 cfg = configuration() 

1537 cfg.load_files(cfgfile, files[0], 4, verbose) 

1538 if args.format != 'auto': 

1539 cfg.set('fileFormat', args.format) 

1540 if args.unwrap: 

1541 cfg.set('unwrapData', not cfg.value('unwrapData')) 

1542 

1543 # plot parameter: 

1544 spec_plots = 'auto' 

1545 if args.spec_plots: 

1546 spec_plots = True 

1547 

1548 # multi-page pdfs: 

1549 multi_pdf = None 

1550 if len(args.multi_pdf) > 0: 

1551 args.save_plot = True 

1552 args.jobs = None # PdfPages does not work yet with mutliprocessing 

1553 ext = os.path.splitext(args.multi_pdf)[1] 

1554 if ext != os.extsep + 'pdf': 

1555 args.multi_pdf += os.extsep + 'pdf' 

1556 multi_pdf = PdfPages(args.multi_pdf) 

1557 

1558 # create output folder: 

1559 if args.save_data or args.save_plot: 

1560 if not os.path.exists(args.outpath): 

1561 if verbose > 1: 

1562 print('mkdir %s' % args.outpath) 

1563 os.makedirs(args.outpath) 

1564 

1565 # kwargs for data loader: 

1566 load_kwargs = {} 

1567 for s in args.load_kwargs: 

1568 for kw in s.split(','): 

1569 kws = kw.split(':') 

1570 if len(kws) == 2: 

1571 load_kwargs[kws[0].strip()] = kws[1].strip() 

1572 

1573 # frequency limits for power spectrum: 

1574 min_freq = 0.0 

1575 max_freq = 3000.0 

1576 log_freq = args.log_freq 

1577 if log_freq > 0.0: 

1578 min_freq = log_freq 

1579 max_freq = min_freq*20 

1580 if max_freq < 2000: 

1581 max_freq = 2000 

1582 log_freq = True 

1583 else: 

1584 log_freq = False 

1585 

1586 # check if all input files are results: 

1587 exts = TableData.ext_formats.values() 

1588 results = True 

1589 # check and group by recording: 

1590 result_files = [] 

1591 for f in sorted(files): 

1592 _, base_name, _, _, ftype, _, ext = parse_filename(f) 

1593 if ext == 'zip' or (ext in exts and ftype in file_types): 

1594 if len(result_files) == 0 or \ 

1595 not result_files[-1][-1].startswith(base_name): 

1596 result_files.append([f]) 

1597 else: 

1598 result_files[-1].append(f) 

1599 else: 

1600 results = False 

1601 break 

1602 if results: 

1603 files = result_files 

1604 

1605 # adjust verbosity: 

1606 v = verbose 

1607 if len(files) > 1: 

1608 v += 1 

1609 

1610 # run on pool: 

1611 pool_args = (results, load_kwargs, cfg, args.channel, args.time, 

1612 args.time_file, args.mode, log_freq, min_freq, 

1613 max_freq, args.save_data, args.zip_file, 

1614 args.all_eods, spec_plots, args.skip_bad, 

1615 args.save_plot, multi_pdf, args.save_subplots, 

1616 args.outpath, args.keep_path, v-1, plot_level) 

1617 if results: 

1618 pool_args = (results, args.rawdata_path, load_kwargs, 

1619 args.all_eods, spec_plots, args.skip_bad, 

1620 args.save_plot, multi_pdf, args.save_subplots, 

1621 log_freq, min_freq, max_freq, args.outpath, 

1622 args.keep_path, v) 

1623 if args.jobs is not None and (args.save_data or args.save_plot) and len(files) > 1: 

1624 cpus = cpu_count() if args.jobs == 0 else args.jobs 

1625 if verbose > 1: 

1626 print('run on %d cpus' % cpus) 

1627 p = Pool(cpus) 

1628 p.map(run_thunderfish, zip(files, [pool_args]*len(files))) 

1629 else: 

1630 list(map(run_thunderfish, zip(files, [pool_args]*len(files)))) 

1631 if multi_pdf is not None: 

1632 multi_pdf.close() 

1633 

1634 

1635if __name__ == '__main__': 

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

1637 main()