Module thunderfish.thunderfish
thunderfish
Automatically detect and analyze all EOD waveforms in short recordings and generated summary plots and data tables.
Run it from the thunderfish development directory as:
> python3 -m thunderfish.thunderfish audiofile.wav
Or install thunderfish
> sudo pip3 install .
Then you can run it directly from every directory:
> thunderfish audiofile.wav
Expand source code
"""# thunderfish
Automatically detect and analyze all EOD waveforms in short recordings
and generated summary plots and data tables.
Run it from the thunderfish development directory as:
```sh
> python3 -m thunderfish.thunderfish audiofile.wav
```
Or install thunderfish
```sh
> sudo pip3 install .
```
Then you can run it directly from every directory:
```sh
> thunderfish audiofile.wav
```
"""
import sys
import os
import glob
import io
import zipfile
import argparse
import traceback
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import matplotlib.ticker as ticker
import matplotlib.lines as ml
from matplotlib.transforms import Bbox
from matplotlib.backends.backend_pdf import PdfPages
from multiprocessing import Pool, freeze_support, cpu_count
from audioio import play, fade, load_audio
from thunderlab.configfile import ConfigFile
from thunderlab.dataloader import load_data
from thunderlab.powerspectrum import decibel, plot_decibel_psd, multi_psd
from thunderlab.powerspectrum import add_multi_psd_config, multi_psd_args
from thunderlab.tabledata import TableData, add_write_table_config, write_table_args
from .version import __version__, __year__
from .bestwindow import add_clip_config, add_best_window_config
from .bestwindow import clip_args, best_window_args
from .bestwindow import analysis_window, plot_data_window
from .checkpulse import check_pulse, add_check_pulse_config, check_pulse_args
from .pulses import extract_pulsefish
from .harmonics import add_psd_peak_detection_config, add_harmonic_groups_config
from .harmonics import harmonic_groups, harmonic_groups_args, psd_peak_detection_args
from .harmonics import colors_markers, plot_harmonic_groups
from .consistentfishes import consistent_fishes
from .eodanalysis import eod_waveform, analyze_wave, analyze_pulse
from .eodanalysis import clipped_fraction
from .eodanalysis import plot_eod_recording, plot_pulse_eods
from .eodanalysis import plot_eod_waveform, plot_eod_snippets
from .eodanalysis import plot_pulse_spectrum, plot_wave_spectrum
from .eodanalysis import add_eod_analysis_config, eod_waveform_args
from .eodanalysis import analyze_wave_args, analyze_pulse_args
from .eodanalysis import add_species_config
from .eodanalysis import wave_quality, wave_quality_args, add_eod_quality_config
from .eodanalysis import pulse_quality, pulse_quality_args
from .eodanalysis import save_eod_waveform, save_wave_eodfs, save_wave_fish, save_pulse_fish
from .eodanalysis import save_wave_spectrum, save_pulse_spectrum, save_pulse_peaks, save_pulse_times
from .eodanalysis import load_eod_waveform, load_wave_eodfs, load_wave_fish, load_pulse_fish
from .eodanalysis import load_wave_spectrum, load_pulse_spectrum, load_pulse_peaks
from .eodanalysis import save_analysis, load_analysis, load_recording
from .eodanalysis import parse_filename, file_types
from .fakefish import normalize_wavefish, export_wavefish
def configuration():
"""Assemble configuration parameter for thunderfish.
Returns
-------
cfg: ConfigFile
Configuration parameters.
"""
cfg = ConfigFile()
add_multi_psd_config(cfg)
cfg.add('frequencyThreshold', 1.0, 'Hz',
'The fundamental frequency of each fish needs to be detected in each power spectrum within this threshold.')
# TODO: make this threshold dependent on frequency resolution!
cfg.add('minPSDAverages', 3, '', 'Minimum number of fft averages for estimating the power spectrum.') # needed by fishfinder
add_psd_peak_detection_config(cfg)
add_harmonic_groups_config(cfg)
add_clip_config(cfg)
cfg.add('unwrapData', False, '', 'Unwrap clipped voltage traces.')
add_best_window_config(cfg, win_size=8.0, w_cv_ampl=10.0)
add_check_pulse_config(cfg)
add_eod_analysis_config(cfg, min_pulse_win=0.004)
del cfg['eodSnippetFac']
del cfg['eodMinSnippet']
del cfg['eodMinSem']
add_eod_quality_config(cfg)
add_species_config(cfg)
add_write_table_config(cfg, table_format='csv', unit_style='row',
align_columns=True, shrink_width=False)
return cfg
def save_configuration(cfg, config_file):
"""Save configuration parameter for thunderfish to a file.
Parameters
----------
cfg: ConfigFile
Configuration parameters and their values.
config_file: string
Name of the configuration file to be loaded.
"""
ext = os.path.splitext(config_file)[1]
if ext != os.extsep + 'cfg':
print('configuration file name must have .cfg as extension!')
else:
print('write configuration to %s ...' % config_file)
del cfg['fileColumnNumbers']
del cfg['fileShrinkColumnWidth']
del cfg['fileMissing']
del cfg['fileLaTeXLabelCommand']
del cfg['fileLaTeXMergeStd']
cfg.dump(config_file)
def detect_eods(data, samplerate, min_clip, max_clip, name, mode,
verbose, plot_level, cfg):
"""Detect EODs of all fish present in the data.
Parameters
----------
data: array of floats
The recording in which to detect EODs.
samplerate: float
Sampling rate of the dataset.
min_clip: float
Minimum amplitude that is not clipped.
max_clip: float
Maximum amplitude that is not clipped.
name: string
Name of the recording (e.g. its filename).
mode: string
Characters in the string indicate what and how to analyze:
- 'w': analyze wavefish
- 'p': analyze pulsefish
- 'P': analyze only the pulsefish with the largest amplitude (not implemented yet)
verbose: int
Print out information about EOD detection if greater than zero.
plot_level : int
Similar to verbosity levels, but with plots.
cfg: ConfigFile
Configuration parameters.
Returns
-------
psd_data: list of 2D arrays
List of power spectra (frequencies and power) of the analysed data
for different frequency resolutions.
wave_eodfs: list of 2D arrays
Frequency and power of fundamental frequency/harmonics of all wave fish.
wave_indices: array of int
Indices of wave fish mapping from wave_eodfs to eod_props.
If negative, then that EOD frequency has no waveform described in eod_props.
eod_props: list of dict
Lists of EOD properties as returned by analyze_pulse() and analyze_wave()
for each waveform in mean_eods.
mean_eods: list of 2-D arrays with time, mean, sem, and fit.
Averaged EOD waveforms of pulse and wave fish.
spec_data: list of 2_D arrays
For each pulsefish a power spectrum of the single pulse and for
each wavefish the relative amplitudes and phases of the harmonics.
peak_data: list of 2_D arrays
For each pulse fish a list of peak properties
(index, time, and amplitude), empty array for wave fish.
power_thresh: 2 D array or None
Frequency (first column) and power (second column) of threshold
derived from single pulse spectra to discard false wave fish.
None if no pulse fish was detected.
skip_reason: list of string
Reasons, why an EOD was discarded.
"""
dfreq = np.nan
nfft = 0
psd_data = [[]]
wave_eodfs = []
wave_indices = []
if 'w' in mode:
# detect wave fish:
psd_data = multi_psd(data, samplerate, **multi_psd_args(cfg))
dfreq = np.mean(np.diff(psd_data[0][:,0]))
nfft = int(samplerate/dfreq)
h_kwargs = psd_peak_detection_args(cfg)
h_kwargs.update(harmonic_groups_args(cfg))
wave_eodfs_list = []
for i, psd in enumerate(psd_data):
wave_eodfs = harmonic_groups(psd[:,0], psd[:,1], verbose-1, **h_kwargs)[0]
if verbose > 0 and len(psd_data) > 1:
numpsdresolutions = cfg.value('numberPSDResolutions')
print('fundamental frequencies detected in power spectrum of window %d at resolution %d:'
% (i//numpsdresolutions, i%numpsdresolutions))
if len(wave_eodfs) > 0:
print(' ' + ' '.join(['%.1f' % freq[0, 0] for freq in wave_eodfs]))
else:
print(' none')
wave_eodfs_list.append(wave_eodfs)
wave_eodfs = consistent_fishes(wave_eodfs_list,
df_th=cfg.value('frequencyThreshold'))
if verbose > 0:
if len(wave_eodfs) > 0:
print('found %2d EOD frequencies consistent in all power spectra:' % len(wave_eodfs))
print(' ' + ' '.join(['%.1f' % freq[0, 0] for freq in wave_eodfs]))
else:
print('no fundamental frequencies are consistent in all power spectra')
# analysis results:
eod_props = []
mean_eods = []
spec_data = []
peak_data = []
power_thresh = None
skip_reason = []
max_pulse_amplitude = 0.0
zoom_window = []
if 'p' in mode:
# detect pulse fish:
_, eod_times, eod_peaktimes, zoom_window, _ = extract_pulsefish(data, samplerate, verbose=verbose-1, plot_level=plot_level, save_path=os.path.splitext(os.path.basename(name))[0])
#eod_times = []
#eod_peaktimes = []
if verbose > 0:
if len(eod_times) > 0:
print('found %2d pulsefish EODs' % len(eod_times))
else:
print('no pulsefish EODs found')
# analyse eod waveform of pulse-fish:
min_freq_res = cfg.value('frequencyResolution')
for k, (eod_ts, eod_pts) in enumerate(zip(eod_times, eod_peaktimes)):
mean_eod, eod_times0 = \
eod_waveform(data, samplerate, eod_ts, win_fac=0.8,
min_win=cfg.value('eodMinPulseSnippet'),
min_sem=False, **eod_waveform_args(cfg))
mean_eod, props, peaks, power = analyze_pulse(mean_eod, eod_times0,
freq_resolution=min_freq_res,
**analyze_pulse_args(cfg))
if len(peaks) == 0:
print('error: no peaks in pulse EOD detected')
continue
clipped_frac = clipped_fraction(data, samplerate, eod_times0,
mean_eod, min_clip, max_clip)
props['peaktimes'] = eod_pts # XXX that should go into analyze pulse
props['index'] = len(eod_props)
props['clipped'] = clipped_frac
props['samplerate'] = samplerate
props['nfft'] = nfft
props['dfreq'] = dfreq
# add good waveforms only:
skips, msg, skipped_clipped = pulse_quality(props, **pulse_quality_args(cfg))
if len(skips) == 0:
eod_props.append(props)
mean_eods.append(mean_eod)
spec_data.append(power)
peak_data.append(peaks)
if verbose > 0:
print('take %6.1fHz pulse fish: %s' % (props['EODf'], msg))
else:
skip_reason += ['%.1fHz pulse fish %s' % (props['EODf'], skips)]
if verbose > 0:
print('skip %6.1fHz pulse fish: %s (%s)' %
(props['EODf'], skips, msg))
# threshold for wave fish peaks based on single pulse spectra:
if len(skips) == 0 or skipped_clipped:
if max_pulse_amplitude < props['p-p-amplitude']:
max_pulse_amplitude = props['p-p-amplitude']
i0 = np.argmin(np.abs(mean_eod[:,0]))
i1 = len(mean_eod) - i0
pulse_data = np.zeros(len(data))
for t in props['peaktimes']:
idx = int(t*samplerate)
ii0 = i0 if idx-i0 >= 0 else idx
ii1 = i1 if idx+i1 < len(pulse_data) else len(pulse_data)-1-idx
pulse_data[idx-ii0:idx+ii1] = mean_eod[i0-ii0:i0+ii1,1]
pulse_psd = multi_psd(pulse_data, samplerate, **multi_psd_args(cfg))
pulse_power = pulse_psd[0][:,1]
pulse_power *= len(data)/samplerate/props['period']/len(props['peaktimes'])
pulse_power *= 5.0
if power_thresh is None:
power_thresh = pulse_psd[0]
power_thresh[:,1] = pulse_power
else:
power_thresh[:,1] += pulse_power
# remove wavefish below pulse fish power:
if 'w' in mode and power_thresh is not None:
n = len(wave_eodfs)
maxh = 3 # XXX make parameter
df = power_thresh[1,0] - power_thresh[0,0]
for k, fish in enumerate(reversed(wave_eodfs)):
idx = np.array(fish[:maxh,0]//df, dtype=int)
for offs in range(-2, 3):
nbelow = np.sum(fish[:maxh,1] < power_thresh[idx+offs,1])
if nbelow > 0:
wave_eodfs.pop(n-1-k)
if verbose > 0:
print('skip %6.1fHz wave fish: %2d harmonics are below pulsefish threshold' % (fish[0,0], nbelow))
break
if 'w' in mode:
# analyse EOD waveform of all wavefish:
powers = np.array([np.sum(fish[:,1]) for fish in wave_eodfs])
power_indices = np.argsort(-powers)
wave_indices = np.zeros(len(wave_eodfs), dtype=int) - 3
for k, idx in enumerate(power_indices):
fish = wave_eodfs[idx]
eod_times = np.arange(0.0, len(data)/samplerate, 1.0/fish[0,0])
mean_eod, eod_times = \
eod_waveform(data, samplerate, eod_times, win_fac=3.0, min_win=0.0,
min_sem=(k==0), **eod_waveform_args(cfg))
mean_eod, props, sdata, error_str = \
analyze_wave(mean_eod, fish, **analyze_wave_args(cfg))
if error_str:
print(name + ': ' + error_str)
clipped_frac = clipped_fraction(data, samplerate, eod_times,
mean_eod, min_clip, max_clip)
props['n'] = len(eod_times)
props['index'] = len(eod_props)
props['clipped'] = clipped_frac
props['samplerate'] = samplerate
props['nfft'] = nfft
props['dfreq'] = dfreq
# remove wave fish that are smaller than the largest pulse fish:
if props['p-p-amplitude'] < 0.01*max_pulse_amplitude:
rm_indices = power_indices[k:]
if verbose > 0:
print('skip %6.1fHz wave fish: power=%5.1fdB, p-p amplitude=%5.1fdB smaller than pulse fish=%5.1dB - 20dB' %
(props['EODf'], decibel(powers[idx]),
decibel(props['p-p-amplitude']), decibel(max_pulse_amplitude)))
for idx in rm_indices[1:]:
print('skip %6.1fHz wave fish: power=%5.1fdB even smaller' %
(wave_eodfs[idx][0,0], decibel(powers[idx])))
wave_eodfs = [eodfs for idx, eodfs in enumerate(wave_eodfs)
if idx not in rm_indices]
wave_indices = np.array([idcs for idx, idcs in enumerate(wave_indices)
if idx not in rm_indices], dtype=int)
break
# add good waveforms only:
remove, skips, msg = wave_quality(props, sdata[1:,3], **wave_quality_args(cfg))
if len(skips) == 0:
wave_indices[idx] = props['index']
eod_props.append(props)
mean_eods.append(mean_eod)
spec_data.append(sdata)
peak_data.append([])
if verbose > 0:
print('take %6.1fHz wave fish: %s' % (props['EODf'], msg))
else:
wave_indices[idx] = -2 if remove else -1
skip_reason += ['%.1fHz wave fish %s' % (props['EODf'], skips)]
if verbose > 0:
print('%-6s %6.1fHz wave fish: %s (%s)' %
('remove' if remove else 'skip', props['EODf'], skips, msg))
wave_eodfs = [eodfs for idx, eodfs in zip(wave_indices, wave_eodfs) if idx > -2]
wave_indices = np.array([idx for idx in wave_indices if idx > -2], dtype=int)
return (psd_data, wave_eodfs, wave_indices, eod_props, mean_eods,
spec_data, peak_data, power_thresh, skip_reason, zoom_window)
def remove_eod_files(output_basename, verbose, cfg):
"""Remove all files from previous runs of thunderfish
"""
ff = cfg.value('fileFormat')
if ff == 'py':
fext = 'py'
else:
fext = TableData.extensions[ff]
# remove all files from previous runs of thunderfish:
for fn in glob.glob('%s*.%s' % (output_basename, fext)):
os.remove(fn)
if verbose > 0:
print('removed file %s' % fn)
def plot_style():
"""Set style of plots.
"""
plt.rcParams['figure.facecolor'] = 'white'
plt.rcParams['axes.facecolor'] = 'none'
plt.rcParams['xtick.direction'] = 'out'
plt.rcParams['ytick.direction'] = 'out'
def axes_style(ax):
"""Fix style of axes.
Parameters
----------
ax: matplotlib axes
"""
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.get_xaxis().tick_bottom()
ax.get_yaxis().tick_left()
def plot_eods(base_name, message_filename,
raw_data, samplerate, channel, idx0, idx1, clipped,
psd_data, wave_eodfs, wave_indices, mean_eods, eod_props,
peak_data, spec_data, indices, unit, zoom_window,
n_snippets=10, power_thresh=None, label_power=True,
all_eods=False, spec_plots='auto', skip_bad=True,
log_freq=False, min_freq=0.0, max_freq=3000.0,
interactive=True, verbose=0):
"""Creates an output plot for the thunderfish program.
This output contains the raw trace where the analysis window is
marked, the power-spectrum of this analysis window where the
detected fish are marked, plots of averaged EOD plots, and
spectra of the EOD waveforms.
Parameters
----------
base_name: string
Basename of audio_file.
message_filename: string or None
Path to meta-data message.
raw_data: array
Dataset.
samplerate: float
Sampling rate of the dataset.
channel: int or None
Channel of the recording to be put into the plot title.
If None, do not write the channel into the title.
idx0: float
Index of the beginning of the analysis window in the dataset.
idx1: float
Index of the end of the analysis window in the dataset.
clipped: float
Fraction of clipped amplitudes.
psd_data: 2D array
Power spectrum (frequencies and power) of the analysed data.
wave_eodfs: array
Frequency and power of fundamental frequency/harmonics of several fish.
wave_indices: array of int
Indices of wave fish mapping from wave_eodfs to eod_props.
If negative, then that EOD frequency has no waveform described in eod_props.
mean_eods: list of 2-D arrays with time, mean and std.
Mean trace for the mean EOD plot.
eod_props: list of dict
Properties for each waveform in mean_eods.
peak_data: list of 2_D arrays
For each pulsefish a list of peak properties
(index, time, and amplitude).
spec_data: list of 2_D arrays
For each pulsefish a power spectrum of the single pulse and for
each wavefish the relative amplitudes and phases of the harmonics.
indices: list of int or None
Indices of the fish in eod_props to be plotted.
If None try to plot all.
unit: string
Unit of the trace and the mean EOD.
n_snippets: int
Number of EOD waveform snippets to be plotted. If zero do not plot any.
power_thresh: 2 D array or None
Frequency (first column) and power (second column) of threshold
derived from single pulse spectra to discard false wave fish.
label_power: boolean
If `True` put the power in decibel in addition to the frequency
into the legend.
all_eods: bool
Plot all EOD waveforms.
spec_plots: bool or 'auto'
Plot amplitude spectra of EOD waveforms.
If 'auto', plot them if there is a singel waveform only.
skip_bad: bool
Skip harmonic groups without index (entry in indices is negative).
log_freq: boolean
Logarithmic (True) or linear (False) frequency axis of power spectrum of recording.
min_freq: float
Limits of frequency axis of power spectrum of recording
are set to `(min_freq, max_freq)` if `max_freq` is greater than zero
max_freq: float
Limits of frequency axis of power spectrum of recording
are set to `(min_freq, max_freq)` and limits of power axis are computed
from powers below max_freq if `max_freq` is greater than zero
interactive: bool
If True install some keyboard interaction.
verbose: int
Print out information about data to be plotted if greater than zero.
Returns
-------
fig: plt.figure
Figure with the plots.
"""
def keypress(event):
if event.key in 'pP':
if idx1 > idx0:
playdata = 1.0 * raw_data[idx0:idx1]
else:
playdata = 1.0 * raw_data[:]
fade(playdata, samplerate, 0.1)
play(playdata, samplerate, blocking=False)
if event.key in 'mM' and message_filename:
# play voice message:
msg, msg_rate = load_audio(message_filename)
play(msg, msg_rate, blocking=False)
def recording_format_coord(x, y):
return 'full recording: x=%.3f s, y=%.3f' % (x, y)
def recordingzoom_format_coord(x, y):
return 'recording zoom-in: x=%.3f s, y=%.3f' % (x, y)
def psd_format_coord(x, y):
return 'power spectrum: x=%.1f Hz, y=%.1f dB' % (x, y)
def meaneod_format_coord(x, y):
return 'mean EOD waveform: x=%.2f ms, y=%.3f' % (x, y)
def ampl_format_coord(x, y):
return u'amplitude spectrum: x=%.0f, y=%.2f' % (x, y)
def phase_format_coord(x, y):
return u'phase spectrum: x=%.0f, y=%.2f \u03c0' % (x, y/np.pi)
def pulsepsd_format_coord(x, y):
return 'single pulse power spectrum: x=%.1f Hz, y=%.1f dB' % (x, y)
# count number of fish types to be plotted:
if indices is None:
indices = np.arange(len(eod_props))
else:
indices = np.array(indices, dtype=int)
nwave = 0
npulse = 0
for idx in indices:
if eod_props[idx]['type'] == 'pulse':
npulse += 1
elif eod_props[idx]['type'] == 'wave':
nwave += 1
neods = nwave + npulse
if verbose > 0:
print('plot: %2d waveforms: %2d wave fish, %2d pulse fish and %2d EOD frequencies.'
% (len(indices), nwave, npulse, len(wave_eodfs)))
# size and positions:
if spec_plots == 'auto':
spec_plots = len(indices) == 1
large_plots = spec_plots or len(indices) <= 2
width = 14.0
height = 10.0
if all_eods and len(indices) > 0:
nrows = len(indices) if spec_plots else (len(indices)+1)//2
if large_plots:
height = 6.0 + 4.0*nrows
else:
height = 6.4 + 1.9*nrows
leftx = 1.0/width
midx = 0.5 + leftx
fullwidth = 1.0-1.4/width
halfwidth = 0.5-1.4/width
pheight = 3.0/height
# figure:
plot_style()
fig = plt.figure(figsize=(width, height))
if interactive:
fig.canvas.mpl_connect('key_press_event', keypress)
# plot title:
title = base_name
if channel is not None:
title += ' c%d' % channel
ax = fig.add_axes([0.2/width, 1.0-0.6/height, 1.0-0.4/width, 0.55/height])
ax.text(0.0, 1.0, title, fontsize=22, va='top')
ax.text(1.0, 1.0, 'thunderfish by Benda-Lab', fontsize=16, ha='right', va='top')
ax.text(1.0, 0.0, 'version %s' % __version__, fontsize=16, ha='right', va='bottom')
ax.set_frame_on(False)
ax.set_axis_off()
ax.set_navigate(False)
# layout of recording and psd plots:
#force_both = True # set to True for debugging pulse and wave detection
force_both = False
posy = 1.0 - 4.0/height
axr = None
axp = None
legend_inside = True
legendwidth = 2.2/width if label_power else 1.7/width
if neods == 0:
axr = fig.add_axes([leftx, posy, fullwidth, pheight]) # top, wide
if len(psd_data) > 0:
axp = fig.add_axes([leftx, 2.0/height, fullwidth, pheight]) # bottom, wide
else:
if npulse == 0 and nwave > 2 and psd_data is not None and \
len(psd_data) > 0 and not force_both:
axp = fig.add_axes([leftx, posy, fullwidth-legendwidth, pheight]) # top, wide
legend_inside = False
elif (npulse > 0 or psd_data is None or len(psd_data) == 0) \
and len(wave_eodfs) == 0 and not force_both:
axr = fig.add_axes([leftx, posy, fullwidth, pheight]) # top, wide
else:
axr = fig.add_axes([leftx, posy, halfwidth, pheight]) # top left
label_power = False
legendwidth = 2.2/width
axp = fig.add_axes([midx, posy, halfwidth, pheight]) # top, right
# best window data:
data = raw_data[idx0:idx1] if idx1 > idx0 else raw_data
# plot recording
pulse_colors, pulse_markers = colors_markers()
pulse_colors = pulse_colors[3:]
pulse_markers = pulse_markers[3:]
if axr is not None:
axes_style(axr)
twidth = 0.1
if len(indices) > 0:
if eod_props[indices[0]]['type'] == 'wave':
twidth = 5.0/eod_props[indices[0]]['EODf']
else:
if len(wave_eodfs) > 0:
twidth = 3.0/eod_props[indices[0]]['EODf']
else:
twidth = 10.0/eod_props[indices[0]]['EODf']
twidth = (1+twidth//0.005)*0.005
if data is not None and len(data) > 0:
plot_eod_recording(axr, data, samplerate, unit, twidth,
idx0/samplerate)
plot_pulse_eods(axr, data, samplerate,
zoom_window, twidth, eod_props,
idx0/samplerate, colors=pulse_colors,
markers=pulse_markers, frameon=True,
loc='upper right')
if axr.get_legend() is not None:
axr.get_legend().get_frame().set_color('white')
axr.set_title('Recording', fontsize=14, y=1.05)
axr.format_coord = recordingzoom_format_coord
# plot psd
wave_colors, wave_markers = colors_markers()
if axp is not None:
axes_style(axp)
if power_thresh is not None:
axp.plot(power_thresh[:,0], decibel(power_thresh[:,1]), '#CCCCCC', lw=1)
if len(wave_eodfs) > 0:
kwargs = {}
if len(wave_eodfs) > 1:
title = '%d EOD frequencies' % len(wave_eodfs)
kwargs = {'title': title if len(wave_eodfs) > 2 else None }
if legend_inside:
kwargs.update({'bbox_to_anchor': (1.05, 1.1),
'loc': 'upper right', 'legend_rows': 10,
'frameon': True})
else:
kwargs.update({'bbox_to_anchor': (1.02, 1.1),
'loc': 'upper left', 'legend_rows': 14,
'labelspacing': 0.6, 'frameon': False})
plot_harmonic_groups(axp, wave_eodfs, wave_indices, max_groups=0,
skip_bad=skip_bad,
sort_by_freq=True, label_power=label_power,
colors=wave_colors, markers=wave_markers,
**kwargs)
if legend_inside:
axp.get_legend().get_frame().set_color('white')
if psd_data is not None and len(psd_data) > 0:
plot_decibel_psd(axp, psd_data[:,0], psd_data[:,1], log_freq=log_freq,
min_freq=min_freq, max_freq=max_freq, ymarg=5.0, color='blue')
axp.yaxis.set_major_locator(ticker.MaxNLocator(6))
if len(wave_eodfs) == 1:
axp.get_legend().set_visible(False)
label = '%6.1f Hz' % wave_eodfs[0][0, 0]
axp.set_title('Powerspectrum: %s' % label, y=1.05, fontsize=14)
else:
axp.set_title('Powerspectrum', y=1.05, fontsize=14)
axp.format_coord = psd_format_coord
# get fish labels from legends:
if axp is not None:
w, _ = axp.get_legend_handles_labels()
eodf_labels = [wi.get_label().split()[0] for wi in w]
legend_wave_eodfs = np.array([float(f) if f[0] != '(' else np.nan for f in eodf_labels])
if axr is not None:
p, _ = axr.get_legend_handles_labels()
eodf_labels = [pi.get_label().split()[0] for pi in p]
legend_pulse_eodfs = np.array([float(f) if f[0] != '(' else np.nan for f in eodf_labels])
# layout:
sheight = 1.4/height
sstep = 1.6/height
max_plots = len(indices)
if not all_eods:
if large_plots:
max_plots = 1 if spec_plots else 2
else:
max_plots = 4
if large_plots:
pstep = pheight + 1.0/height
ty = 1.08
my = 1.10
ny = 6
else:
posy -= 0.2/height
pheight = 1.3/height
pstep = 1.9/height
ty = 1.10
my = 1.16
ny = 4
posy -= pstep
# sort indices by p-p amplitude:
pp_ampls = [eod_props[idx]['p-p-amplitude'] for idx in indices]
pp_indices = np.argsort(pp_ampls)[::-1]
# plot EOD waveform and spectra:
for k, idx in enumerate(indices[pp_indices]):
if k >= max_plots:
break
# plot EOD waveform:
mean_eod = mean_eods[idx]
props = eod_props[idx]
peaks = peak_data[idx]
lx = leftx if spec_plots or k%2 == 0 else midx
ax = fig.add_axes([lx, posy, halfwidth, pheight])
axes_style(ax)
ax.yaxis.set_major_locator(ticker.MaxNLocator(ny))
if len(indices) > 1:
ax.text(0.3, ty, '{EODf:.1f} Hz {type} fish'.format(**props),
transform=ax.transAxes, fontsize=14, zorder=20)
mx = 0.25
else:
ax.text(-0.1, ty, '{EODf:.1f} Hz {type} fish'.format(**props),
transform=ax.transAxes, fontsize=14, zorder=20)
ax.text(0.5, ty, 'Averaged EOD', ha='center',
transform=ax.transAxes, fontsize=14, zorder=20)
mx = -0.14
eodf = props['EODf']
if props['type'] == 'wave':
if axp is not None:
wk = np.nanargmin(np.abs(legend_wave_eodfs - eodf))
ma = ml.Line2D([mx], [my], color=w[wk].get_color(), marker=w[wk].get_marker(),
markersize=w[wk].get_markersize(), mec='none', clip_on=False,
label=w[wk].get_label(), transform=ax.transAxes)
ax.add_line(ma)
else:
if axr is not None and len(legend_pulse_eodfs) > 0:
pk = np.argmin(np.abs(legend_pulse_eodfs - eodf))
ma = ml.Line2D([mx], [my], color=p[pk].get_color(), marker=p[pk].get_marker(),
markersize=p[pk].get_markersize(), mec='none', clip_on=False,
label=p[pk].get_label(), transform=ax.transAxes)
ax.add_line(ma)
plot_eod_waveform(ax, mean_eod, props, peaks, unit)
if props['type'] == 'pulse' and 'times' in props:
plot_eod_snippets(ax, data, samplerate, mean_eod[0,0], mean_eod[-1,0],
props['times'], n_snippets, props['flipped'])
if not large_plots and k < max_plots-2:
ax.set_xlabel('')
ax.format_coord = meaneod_format_coord
# plot spectra:
if spec_plots:
spec = spec_data[idx]
if props['type'] == 'pulse':
ax = fig.add_axes([midx, posy, halfwidth, pheight])
axes_style(ax)
plot_pulse_spectrum(ax, spec, props)
ax.set_title('Single pulse spectrum', fontsize=14, y=1.05)
ax.format_coord = pulsepsd_format_coord
else:
axa = fig.add_axes([midx, posy+sstep, halfwidth, sheight])
axes_style(axa)
axp = fig.add_axes([midx, posy, halfwidth, sheight])
axes_style(axp)
plot_wave_spectrum(axa, axp, spec, props, unit)
axa.set_title('Amplitude and phase spectrum', fontsize=14, y=1.05)
axa.set_xticklabels([])
axa.yaxis.set_major_locator(ticker.MaxNLocator(4))
axa.format_coord = ampl_format_coord
axp.format_coord = phase_format_coord
if spec_plots or k%2 == 1:
posy -= pstep
# whole trace:
ax = fig.add_axes([leftx, 0.6/height, fullwidth, 0.9/height])
axes_style(ax)
if raw_data is not None and len(raw_data) > 0:
plot_data_window(ax, raw_data, samplerate, unit, idx0, idx1, clipped)
ax.format_coord = recording_format_coord
return fig
def plot_eod_subplots(base_name, subplots, raw_data, samplerate, idx0, idx1,
clipped, psd_data, wave_eodfs, wave_indices, mean_eods,
eod_props, peak_data, spec_data, unit, zoom_window,
n_snippets=10, power_thresh=None, label_power=True,
skip_bad=True, log_freq=False,
min_freq=0.0, max_freq=3000.0, save=True):
"""Plot time traces and spectra into separate windows or files.
Parameters
----------
base_name: string
Basename of audio_file.
subplots: string
Specifies which subplots to plot:
r) recording with best 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. With capital letters
all fish are saved into a single pdf file, with small letters each fish
is saved into a separate file.
raw_data: array
Dataset.
samplerate: float
Sampling rate of the dataset.
idx0: float
Index of the beginning of the analysis window in the dataset.
idx1: float
Index of the end of the analysis window in the dataset.
clipped: float
Fraction of clipped amplitudes.
psd_data: 2D array
Power spectrum (frequencies and power) of the analysed data.
wave_eodfs: array
Frequency and power of fundamental frequency/harmonics of several fish.
wave_indices: array of int
Indices of wave fish mapping from wave_eodfs to eod_props.
If negative, then that EOD frequency has no waveform described in eod_props.
mean_eods: list of 2-D arrays with time, mean and std.
Mean trace for the mean EOD plot.
eod_props: list of dict
Properties for each waveform in mean_eods.
peak_data: list of 2_D arrays
For each pulsefish a list of peak properties
(index, time, and amplitude).
spec_data: list of 2_D arrays
For each pulsefish a power spectrum of the single pulse and for
each wavefish the relative amplitudes and phases of the harmonics.
unit: string
Unit of the trace and the mean EOD.
n_snippets: int
Number of EOD waveform snippets to be plotted. If zero do not plot any.
power_thresh: 2 D array or None
Frequency (first column) and power (second column) of threshold
derived from single pulse spectra to discard false wave fish.
label_power: boolean
If `True` put the power in decibel in addition to the frequency
into the legend.
skip_bad: bool
Skip harmonic groups without index (entry in indices is negative).
log_freq: boolean
Logarithmic (True) or linear (False) frequency axis of power spectrum of recording.
min_freq: float
Limits of frequency axis of power spectrum of recording
are set to `(min_freq, max_freq)` if `max_freq` is greater than zero
max_freq: float
Limits of frequency axis of power spectrum of recording
are set to `(min_freq, max_freq)` and limits of power axis are computed
from powers below max_freq if `max_freq` is greater than zero
save: bool
If True save plots to files instead of showing them.
"""
plot_style()
if 'r' in subplots:
fig, ax = plt.subplots(figsize=(10, 2))
fig.subplots_adjust(left=0.07, right=0.99, bottom=0.22, top=0.95)
plot_data_window(ax, raw_data, samplerate, unit, idx0, idx1, clipped)
ax.yaxis.set_major_locator(ticker.MaxNLocator(5))
axes_style(ax)
if save:
fig.savefig(base_name + '-recording.pdf')
if 't' in subplots:
fig, ax = plt.subplots(figsize=(10, 6))
twidth = 0.1
if len(eod_props) > 0:
if eod_props[0]['type'] == 'wave':
twidth = 5.0/eod_props[0]['EODf']
else:
if len(wave_eodfs) > 0:
twidth = 3.0/eod_props[0]['EODf']
else:
twidth = 10.0/eod_props[0]['EODf']
twidth = (1+twidth//0.005)*0.005
pulse_colors, pulse_markers = colors_markers()
pulse_colors = pulse_colors[3:]
pulse_markers = pulse_markers[3:]
plot_eod_recording(ax, raw_data[idx0:idx1], samplerate, unit,
twidth, idx0/samplerate)
plot_pulse_eods(ax, raw_data[idx0:idx1], samplerate,
zoom_window, twidth, eod_props,
idx0/samplerate, colors=pulse_colors,
markers=pulse_markers, frameon=True,
loc='upper right')
if ax.get_legend() is not None:
ax.get_legend().get_frame().set_color('white')
axes_style(ax)
if save:
fig.savefig(base_name + '-trace.pdf')
if 'p' in subplots:
fig, ax = plt.subplots(figsize=(10, 5))
fig.subplots_adjust(left=0.08, right=0.975, bottom=0.11, top=0.9)
axes_style(ax)
if power_thresh is not None:
ax.plot(power_thresh[:,0], decibel(power_thresh[:,1]), '#CCCCCC', lw=1)
if len(wave_eodfs) > 0:
kwargs = {}
if len(wave_eodfs) > 1:
title = '%d EOD frequencies' % len(wave_eodfs)
kwargs = {'title': title if len(wave_eodfs) > 2 else None }
if len(wave_eodfs) > 2:
fig.subplots_adjust(left=0.08, right=0.78, bottom=0.11, top=0.9)
kwargs.update({'bbox_to_anchor': (1.01, 1.1),
'loc': 'upper left', 'legend_rows': 14,
'labelspacing': 0.6})
else:
kwargs.update({'bbox_to_anchor': (1.05, 1.1),
'loc': 'upper right', 'legend_rows': 10})
wave_colors, wave_markers = colors_markers()
plot_harmonic_groups(ax, wave_eodfs, wave_indices, max_groups=0,
skip_bad=skip_bad,
sort_by_freq=True, label_power=label_power,
colors=wave_colors, markers=wave_markers,
frameon=False, **kwargs)
plot_decibel_psd(ax, psd_data[:,0], psd_data[:,1], log_freq=log_freq,
min_freq=min_freq, max_freq=max_freq, ymarg=5.0, color='blue')
ax.yaxis.set_major_locator(ticker.MaxNLocator(6))
if len(wave_eodfs) == 1:
ax.get_legend().set_visible(False)
label = '%6.1f Hz' % wave_eodfs[0][0, 0]
ax.set_title('Powerspectrum: %s' % label, y=1.05)
else:
ax.set_title('Powerspectrum', y=1.05)
if save:
fig.savefig(base_name + '-psd.pdf')
if 'w' in subplots or 'W' in subplots:
mpdf = None
if 'W' in subplots:
mpdf = PdfPages(base_name + '-waveforms.pdf')
for meod, props, peaks in zip(mean_eods, eod_props, peak_data):
if meod is None:
continue
fig, ax = plt.subplots(figsize=(5, 3))
fig.subplots_adjust(left=0.18, right=0.98, bottom=0.15, top=0.9)
if not props is None:
ax.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props))
plot_eod_waveform(ax, meod, props, peaks, unit)
data = raw_data[idx0:idx1] if idx1 > idx0 else raw_data
if not props is None and props['type'] == 'pulse' and \
'times' in props:
plot_eod_snippets(ax, data, samplerate, meod[0,0], meod[-1,0],
props['times'], n_snippets)
ax.yaxis.set_major_locator(ticker.MaxNLocator(6))
axes_style(ax)
if mpdf is None:
if save:
fig.savefig(base_name + '-waveform-%d.pdf' % props['index'])
else:
mpdf.savefig(fig)
if mpdf is not None:
mpdf.close()
if 's' in subplots or 'S' in subplots:
mpdf = None
if 'S' in subplots:
mpdf = PdfPages(base_name + '-spectrum.pdf')
for props, peaks, spec in zip(eod_props, peak_data, spec_data):
if spec is None:
continue
if props is not None and props['type'] == 'pulse':
fig, ax = plt.subplots(figsize=(5, 3.5))
fig.subplots_adjust(left=0.15, right=0.967, bottom=0.16, top=0.88)
axes_style(ax)
ax.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props), y=1.07)
plot_pulse_spectrum(ax, spec, props)
else:
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(5, 3.5))
fig.subplots_adjust(left=0.15, right=0.97, bottom=0.16, top=0.88, hspace=0.4)
axes_style(ax1)
axes_style(ax2)
if not props is None:
ax1.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props), y=1.15)
plot_wave_spectrum(ax1, ax2, spec, props, unit)
ax1.set_xticklabels([])
ax1.yaxis.set_major_locator(ticker.MaxNLocator(4))
if mpdf is None:
if save:
fig.savefig(base_name + '-spectrum-%d.pdf' % props['index'])
else:
mpdf.savefig(fig)
if mpdf is not None:
mpdf.close()
if 'e' in subplots or 'E' in subplots:
mpdf = None
if 'E' in subplots:
mpdf = PdfPages(base_name + '-eods.pdf')
for meod, props, peaks, spec in zip(mean_eods, eod_props, peak_data, spec_data):
if meod is None or spec is None:
continue
fig = plt.figure(figsize=(10, 3.5))
gs = gridspec.GridSpec(nrows=2, ncols=2, left=0.09, right=0.98,
bottom=0.16, top=0.88, wspace=0.4, hspace=0.4)
ax1 = fig.add_subplot(gs[:,0])
if not props is None:
ax1.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props), y=1.07)
plot_eod_waveform(ax1, meod, props, peaks, unit)
data = raw_data[idx0:idx1] if idx1 > idx0 else raw_data
if not props is None and props['type'] == 'pulse' and 'times' in props:
plot_eod_snippets(ax1, data, samplerate, meod[0,0], meod[-1,0],
props['times'], n_snippets)
ax1.yaxis.set_major_locator(ticker.MaxNLocator(6))
axes_style(ax1)
if not props is None and props['type'] == 'pulse':
ax2 = fig.add_subplot(gs[:,1])
axes_style(ax2)
plot_pulse_spectrum(ax2, spec, props)
ax2.set_title('Single pulse spectrum', y=1.07)
else:
ax2 = fig.add_subplot(gs[0,1])
ax3 = fig.add_subplot(gs[1,1])
axes_style(ax2)
axes_style(ax3)
plot_wave_spectrum(ax2, ax3, spec, props, unit)
ax2.set_title('Amplitude and phase spectrum', y=1.15)
ax2.set_xticklabels([])
ax2.yaxis.set_major_locator(ticker.MaxNLocator(4))
if mpdf is None:
if save:
fig.savefig(base_name + '-eod-%d.pdf' % props['index'])
else:
mpdf.savefig(fig)
if mpdf is not None:
mpdf.close()
if not save:
plt.show()
plt.close('all')
def thunderfish_plot(files, data_path=None, load_kwargs={},
all_eods=False, spec_plots='auto', skip_bad=True,
save_plot=False, multi_pdf=None,
save_subplots='', log_freq=False, min_freq=0.0,
max_freq=3000.0, output_folder='.',
keep_path=False, verbose=0):
"""Generate plots from saved analysis results.
Parameters
----------
files: list of str
Analysis files from a single recording.
data_path: str
Path where to find the raw data.
load_kwargs: dict
Key-word arguments for the `load_data()` function.
all_eods: bool
If True, plot all EOD waveforms.
spec_plots: bool or 'auto'
Plot amplitude spectra of EOD waveforms.
If 'auto', plot them if there is a singel waveform only.
skip_bad: bool
Skip harmonic groups without index in the spectrum plot.
save_plot: bool
If True, save plots as pdf file.
multi_pdf: matplotlib.PdfPages or None
PdfPages instance in which to save plots.
save_subplots: string
If not empty, specifies subplots to be saved as separate pdf
files: r) recording with best 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. Capital letters produce a single
multipage pdf containing plots of all detected fish.
log_freq: boolean
Logarithmic (True) or linear (False) frequency axis of
power spectrum of recording.
min_freq: float
Limits of frequency axis of power spectrum of recording
are set to `(min_freq, max_freq)`, if `max_freq` is greater than zero
max_freq: float
Limits of frequency axis of power spectrum of recording
are set to `(min_freq, max_freq)` and limits of power axis are computed
from powers below max_freq, if `max_freq` is greater than zero
output_folder: string
Folder where to save results.
keep_path: bool
Add relative path of data files to output path.
verbose: int
Verbosity level (for debugging).
"""
if len(save_subplots) == 0:
save_subplots = 'rtpwsed' # plot everything
# load analysis results:
mean_eods, wave_eodfs, wave_indices, eod_props, spec_data, \
peak_data, base_name, channel, unit = load_analysis(files)
if len(mean_eods) == 0 or all(me is None for me in mean_eods):
save_subplots = save_subplots.replace('w', '')
save_subplots = save_subplots.replace('W', '')
save_subplots = save_subplots.replace('e', '')
save_subplots = save_subplots.replace('E', '')
save_subplots = save_subplots.replace('d', '')
if len(spec_data) == 0 or all(sd is None for sd in spec_data):
save_subplots = save_subplots.replace('s', '')
save_subplots = save_subplots.replace('S', '')
save_subplots = save_subplots.replace('e', '')
save_subplots = save_subplots.replace('E', '')
save_subplots = save_subplots.replace('d', '')
clipped = 0.0
if len(eod_props) > 0 and not eod_props[0] is None and \
'winclipped' in eod_props[0]:
clipped = eod_props[0]['winclipped']
zoom_window = [1.2, 1.3]
# load recording:
psd_data = None
if base_name:
name = os.path.basename(base_name) if data_path and data_path != '.' else base_name
data_path = os.path.join(data_path, name)
data, samplerate, idx0, idx1, data_path = \
load_recording(data_path, channel, load_kwargs,
eod_props, verbose-1)
if data is None:
save_subplots = save_subplots.replace('r', '')
save_subplots = save_subplots.replace('t', '')
save_subplots = save_subplots.replace('d', '')
if verbose > 0:
print('loaded', data_path)
if len(eod_props) > 0 and not eod_props[0] is None and \
'dfreq' in eod_props[0] and data is not None and len(data) > 0:
psd_data = multi_psd(data[idx0:idx1],
samplerate,
1.1*eod_props[0]['dfreq'])[0]
if psd_data is not None and len(psd_data) > 0:
for idx, fish in zip(wave_indices, wave_eodfs):
if idx < 0:
for k in range(len(fish)):
fish[k,1] = psd_data[np.argmin(np.abs(psd_data[:,0] - fish[k,0])),1]
if psd_data is None:
save_subplots = save_subplots.replace('p', '')
save_subplots = save_subplots.replace('d', '')
# file name for output files:
fn = base_name if keep_path else os.path.basename(base_name)
output_basename = os.path.join(output_folder, fn)
if channel >= 0:
output_basename += f'-c{channel}'
# make directory if necessary:
if keep_path:
outpath = os.path.dirname(output_basename)
if not os.path.exists(outpath):
if verbose > 0:
print('mkdir %s' % outpath)
os.makedirs(outpath)
# plot:
if len(save_subplots) == 0 or 'd' in save_subplots:
fig = plot_eods(os.path.basename(base_name), None,
data, samplerate,
channel, idx0, idx1, clipped, psd_data,
wave_eodfs, wave_indices, mean_eods,
eod_props, peak_data, spec_data, None, unit,
zoom_window, 10, None, True, all_eods,
spec_plots, skip_bad, log_freq, min_freq,
max_freq, interactive=not save_plot,
verbose=verbose-1)
if save_plot:
if multi_pdf is not None:
multi_pdf.savefig(fig)
else:
fig.savefig(output_basename + '.pdf')
else:
fig.canvas.manager.set_window_title('thunderfish')
plt.show()
plt.close()
save_subplots = save_subplots.replace('d', '')
if len(save_subplots) > 0:
plot_eod_subplots(output_basename, save_subplots, data,
samplerate, idx0, idx1, clipped,
psd_data, wave_eodfs, wave_indices,
mean_eods, eod_props, peak_data,
spec_data, unit, zoom_window, 10, None,
True, skip_bad, log_freq, min_freq,
max_freq, save_plot)
return None
def thunderfish(filename, load_kwargs, cfg, channel=0,
time=None, time_file=False,
mode='wp', log_freq=False, min_freq=0.0, max_freq=3000,
save_data=False, zip_file=False,
all_eods=False, spec_plots='auto', skip_bad=True,
save_plot=False, multi_pdf=None, save_subplots='',
output_folder='.', keep_path=False,
verbose=0, plot_level=0):
"""Automatically detect and analyze all EOD waveforms in a short recording.
Parameters
----------
filename: string
Path of the data file to be analyzed.
load_kwargs: dict
Key-word arguments for the `load_data()` function.
cfg: dict
channel: int
Channel to be analyzed.
time: string, float, or None
Start time of analysis window: "beginning", "center", "end",
"best", or time in seconds (as float or string). If not None
overwrites "windowPosition" in cofiguration file.
time_file: bool
If `True` add time of analysis window to output file names.
mode: 'w', 'p', 'P', 'wp', or 'wP'
Analyze wavefish ('w'), all pulse fish ('p'), or largest pulse
fish only ('P').
log_freq: boolean
Logarithmic (True) or linear (False) frequency axis of
power spectrum of recording.
min_freq: float
Limits of frequency axis of power spectrum of recording
are set to `(min_freq, max_freq)`, if `max_freq` is greater than zero
max_freq: float
Limits of frequency axis of power spectrum of recording
are set to `(min_freq, max_freq)` and limits of power axis are computed
from powers below max_freq, if `max_freq` is greater than zero
save_data: bool
If True save analysis results in files. If False, just plot the data.
zip_data: bool
If True, store all analysis results in a single zip file.
all_eods: bool
If True, plot all EOD waveforms.
spec_plots: bool or 'auto'
Plot amplitude spectra of EOD waveforms.
If 'auto', plot them if there is a singel waveform only.
skip_bad: bool
Skip harmonic groups without index in the spectrum plot.
save_plot: bool
If True, save plots as pdf file.
multi_pdf: matplotlib.PdfPages or None
PdfPages instance in which to save plots.
save_subplots: string
If not empty, specifies subplots to be saved as separate pdf
files: r) recording with best 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. Capital letters produce a single
multipage pdf containing plots of all detected fish.
output_folder: string
Folder where to save results.
keep_path: bool
Add relative path of data files to output path.
verbose: int
Verbosity level (for debugging).
plot_level: int
Plot intermediate results.
Returns
-------
msg: string or None
In case of errors, an error message.
"""
# check data file:
if len(filename) == 0:
return 'you need to specify a file containing some data'
# file names:
fn = filename if keep_path else os.path.basename(filename)
outfilename = os.path.splitext(fn)[0]
messagefilename = os.path.splitext(fn)[0] + '-message.wav'
if not os.path.isfile(messagefilename):
messagefilename = None
# load data:
try:
all_data, samplerate, unit, ampl_max = load_data(filename,
verbose=verbose,
**load_kwargs)
except IOError as e:
return '%s: failed to open file: %s' % (filename, str(e))
# select channel:
channels = all_data.shape[1]
chan_list = [channel]
if channel < 0:
chan_list = range(channels)
elif channel >= channels:
return '%s: invalid channel %d (%d channels)' % (filename, channel, channels)
# process all channels:
for chan in chan_list:
raw_data = all_data[:,chan]
if len(raw_data) <= 1:
return '%s: empty data file' % filename
if verbose >= 0 and len(chan_list) > 1:
print(' channel %d' % chan)
# analysis window:
win_pos = cfg.value('windowPosition')
if time is not None:
win_pos = time
data, idx0, idx1, clipped, min_clip, max_clip = \
analysis_window(raw_data, samplerate, ampl_max, win_pos,
cfg, plot_level>0)
found_bestwindow = idx1 > 0
if not found_bestwindow:
return '%s: not enough data for requested window length. You may want to adjust the windowSize parameter in the configuration file.' % filename
# detect EODs in the data:
psd_data, wave_eodfs, wave_indices, eod_props, \
mean_eods, spec_data, peak_data, power_thresh, skip_reason, zoom_window = \
detect_eods(data, samplerate, min_clip, max_clip, filename,
mode, verbose, plot_level, cfg)
if not found_bestwindow:
wave_eodfs = []
wave_indices = []
eod_props = []
mean_eods = []
# add analysis window to EOD properties:
for props in eod_props:
props['twin'] = idx0/samplerate
props['window'] = (idx1 - idx0)/samplerate
props['winclipped'] = clipped
# warning message in case no fish has been found:
if found_bestwindow and not eod_props :
msg = ', '.join(skip_reason)
if msg:
print(filename + ': no fish found: %s' % msg)
else:
print(filename + ': no fish found.')
# file name for output files:
output_basename = os.path.join(output_folder, outfilename)
if channels > 1:
if channels > 100:
output_basename += '-c%03d' % chan
elif channels > 10:
output_basename += '-c%02d' % chan
else:
output_basename += '-c%d' % chan
if time_file:
output_basename += '-t%.0fs' % (idx0/samplerate)
# make directory if necessary:
if keep_path and found_bestwindow:
outpath = os.path.dirname(output_basename)
if not os.path.exists(outpath):
if verbose > 0:
print('mkdir %s' % outpath)
os.makedirs(outpath)
# save results to files:
if save_data:
remove_eod_files(output_basename, verbose, cfg)
if found_bestwindow:
save_analysis(output_basename, zip_file, eod_props,
mean_eods, spec_data, peak_data,
wave_eodfs, wave_indices, unit, verbose,
**write_table_args(cfg))
# summary plots:
if save_plot or not save_data:
n_snippets = 10
if len(save_subplots) == 0 or 'd' in save_subplots:
chl = chan if channels > 1 else None
fig = plot_eods(outfilename, messagefilename,
raw_data, samplerate,
chl, idx0, idx1, clipped, psd_data[0],
wave_eodfs, wave_indices, mean_eods,
eod_props, peak_data, spec_data, None,
unit, zoom_window, n_snippets,
power_thresh, True, all_eods,
spec_plots, skip_bad, log_freq,
min_freq, max_freq, interactive=not
save_plot, verbose=verbose)
if save_plot:
if multi_pdf is not None:
multi_pdf.savefig(fig)
else:
fig.savefig(output_basename + '.pdf')
else:
fig.canvas.manager.set_window_title('thunderfish')
plt.show()
plt.close()
save_subplots = save_subplots.replace('d', '')
if len(save_subplots) > 0:
plot_eod_subplots(output_basename, save_subplots,
raw_data, samplerate, idx0, idx1,
clipped, psd_data[0], wave_eodfs,
wave_indices, mean_eods, eod_props,
peak_data, spec_data, unit,
zoom_window, n_snippets,
power_thresh, True, skip_bad,
log_freq, min_freq, max_freq,
save_plot)
return None
def run_thunderfish(file_args):
"""Helper function for mutlithreading Pool().map().
"""
results = file_args[1][0]
verbose = file_args[1][-1] if results else file_args[1][-2]+1
if verbose > 1:
print('='*70)
try:
if results:
thunderfish_plot(file_args[0], *file_args[1][1:])
else:
if verbose > 0:
print('analyze recording %s ...' % file_args[0])
msg = thunderfish(file_args[0], *file_args[1][1:])
if msg:
print(msg)
except (KeyboardInterrupt, SystemExit):
print('\nthunderfish interrupted by user... exit now.')
sys.exit(0)
except:
print(traceback.format_exc())
def main(cargs=None):
# config file name:
cfgfile = __package__ + '.cfg'
# command line arguments:
if cargs is None:
cargs = sys.argv[1:]
parser = argparse.ArgumentParser(add_help=False,
description='Analyze EOD waveforms of weakly electric fish.',
epilog='version %s by Benda-Lab (2015-%s)' % (__version__, __year__))
parser.add_argument('-h', '--help', action='store_true',
help='show this help message and exit')
parser.add_argument('--version', action='version', version=__version__)
parser.add_argument('-v', action='count', dest='verbose', default=0,
help='verbosity level. Increase by specifying -v multiple times, or like -vvv')
parser.add_argument('-V', action='count', dest='plot_level', default=0,
help='level for debugging plots. Increase by specifying -V multiple times, or like -VVV')
parser.add_argument('-c', dest='save_config', action='store_true',
help='save configuration to file {0} after reading all configuration files'.format(cfgfile))
parser.add_argument('--channel', default=0, type=int,
help='channel to be analyzed (defaults to first channel, negative channel selects all channels)')
parser.add_argument('-t', dest='time', default=None, type=str, metavar='TIME',
help='start time of analysis window in recording: "beginning", "center", "end", "best", or time in seconds (overwrites "windowPosition" in cofiguration file)')
parser.add_argument('-u', dest='unwrap', action='store_true',
help='unwrap clipped files, toggles unwrap setting of config file.')
parser.add_argument('-T', dest='time_file', action='store_true',
help='add start time of analysis file to output file names')
parser.add_argument('-m', dest='mode', default='wp', type=str,
choices=['w', 'p', 'wp'],
help='extract wave "w" and/or pulse "p" fish EODs')
parser.add_argument('-a', dest='all_eods', action='store_true',
help='show all EOD waveforms in the summary plot')
parser.add_argument('-S', dest='spec_plots', action='store_true',
help='plot spectra for all EOD waveforms in the summary plot')
parser.add_argument('-b', dest='skip_bad', action='store_false',
help='indicate bad EODs in legend of power spectrum')
parser.add_argument('-l', dest='log_freq', type=float, metavar='MINFREQ',
nargs='?', const=100.0, default=0.0,
help='logarithmic frequency axis in power spectrum with optional minimum frequency (defaults to 100 Hz)')
parser.add_argument('-p', dest='save_plot', action='store_true',
help='save output plots as pdf files')
parser.add_argument('-M', dest='multi_pdf', default='', type=str, metavar='PDFFILE',
help='save all summary plots of all recordings in a multi page pdf file. Disables parallel jobs.')
parser.add_argument('-P', dest='save_subplots', default='', type=str, metavar='rtpwsed',
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')
parser.add_argument('-d', dest='rawdata_path', default='.', type=str, metavar='PATH',
help='path to raw EOD recordings needed for plotting based on analysis results')
parser.add_argument('-j', dest='jobs', nargs='?', type=int, default=None, const=0,
help='number of jobs run in parallel. Without argument use all CPU cores.')
parser.add_argument('-s', dest='save_data', action='store_true',
help='save analysis results to files')
parser.add_argument('-z', dest='zip_file', action='store_true',
help='save analysis results in a single zip file')
parser.add_argument('-f', dest='format', default='auto', type=str,
choices=TableData.formats + ['py'],
help='file format used for saving analysis results, defaults to the format specified in the configuration file or "csv"')
parser.add_argument('-o', dest='outpath', default='.', type=str,
help='path where to store results and figures (defaults to current working directory)')
parser.add_argument('-k', dest='keep_path', action='store_true',
help='keep path of input file when saving analysis files, i.e. append path of input file to OUTPATH')
parser.add_argument('-i', dest='load_kwargs', default=[],
action='append', metavar='KWARGS',
help='key-word arguments for the data loader function')
parser.add_argument('file', nargs='*', default='', type=str,
help='name of a file with time series data of an EOD recording, may include wildcards')
args = parser.parse_args(cargs)
# help:
if args.help:
parser.print_help()
print('')
print('examples:')
print('- analyze the single file data.wav interactively:')
print(' > thunderfish data.wav')
print('- extract wavefish only:')
print(' > thunderfish -m w data.wav')
print('- automatically analyze all wav files in the current working directory and save analysis results and plot to files:')
print(' > thunderfish -s -p *.wav')
print('- analyze all wav files in the river1/ directory, use all CPUs, and write files directly to "results/":')
print(' > thunderfish -j -s -p -o results/ river1/*.wav')
print('- analyze all wav files in the river1/ directory and write files to "results/river1/":')
print(' > thunderfish -s -p -o results/ -k river1/*.wav')
print('- write configuration file:')
print(' > thunderfish -c')
parser.exit()
# set verbosity level from command line:
verbose = args.verbose
plot_level = args.plot_level
if verbose < plot_level:
verbose = plot_level
# interactive plot:
plt.rcParams['keymap.quit'] = 'ctrl+w, alt+q, q'
plt.ioff()
# expand wildcard patterns:
files = []
if os.name == 'nt':
for fn in args.file:
files.extend(glob.glob(fn))
else:
files = [f for f in args.file if '-message' not in f]
# save configuration:
if args.save_config:
file_name = files[0] if len(files) else ''
cfg = configuration()
cfg.load_files(cfgfile, file_name, 4, verbose)
save_configuration(cfg, cfgfile)
exit()
elif len(files) == 0:
parser.error('you need to specify at least one file for the analysis')
# configure:
cfg = configuration()
cfg.load_files(cfgfile, files[0], 4, verbose)
if args.format != 'auto':
cfg.set('fileFormat', args.format)
if args.unwrap:
cfg.set('unwrapData', not cfg.value('unwrapData'))
# plot parameter:
spec_plots = 'auto'
if args.spec_plots:
spec_plots = True
# multi-page pdfs:
multi_pdf = None
if len(args.multi_pdf) > 0:
args.save_plot = True
args.jobs = None # PdfPages does not work yet with mutliprocessing
ext = os.path.splitext(args.multi_pdf)[1]
if ext != os.extsep + 'pdf':
args.multi_pdf += os.extsep + 'pdf'
multi_pdf = PdfPages(args.multi_pdf)
# create output folder:
if args.save_data or args.save_plot:
if not os.path.exists(args.outpath):
if verbose > 1:
print('mkdir %s' % args.outpath)
os.makedirs(args.outpath)
# kwargs for data loader:
load_kwargs = {}
for s in args.load_kwargs:
for kw in s.split(','):
kws = kw.split(':')
if len(kws) == 2:
load_kwargs[kws[0].strip()] = kws[1].strip()
# frequency limits for power spectrum:
min_freq = 0.0
max_freq = 3000.0
log_freq = args.log_freq
if log_freq > 0.0:
min_freq = log_freq
max_freq = min_freq*20
if max_freq < 2000:
max_freq = 2000
log_freq = True
else:
log_freq = False
# check if all input files are results:
exts = TableData.ext_formats.values()
results = True
# check and group by recording:
result_files = []
for f in sorted(files):
_, base_name, _, _, ftype, _, ext = parse_filename(f)
if ext == 'zip' or (ext in exts and ftype in file_types):
if len(result_files) == 0 or \
not result_files[-1][-1].startswith(base_name):
result_files.append([f])
else:
result_files[-1].append(f)
else:
results = False
break
if results:
files = result_files
# adjust verbosity:
v = verbose
if len(files) > 1:
v += 1
# run on pool:
pool_args = (results, load_kwargs, cfg, args.channel, args.time,
args.time_file, args.mode, log_freq, min_freq,
max_freq, args.save_data, args.zip_file,
args.all_eods, spec_plots, args.skip_bad,
args.save_plot, multi_pdf, args.save_subplots,
args.outpath, args.keep_path, v-1, plot_level)
if results:
pool_args = (results, args.rawdata_path, load_kwargs,
args.all_eods, spec_plots, args.skip_bad,
args.save_plot, multi_pdf, args.save_subplots,
log_freq, min_freq, max_freq, args.outpath,
args.keep_path, v)
if args.jobs is not None and (args.save_data or args.save_plot) and len(files) > 1:
cpus = cpu_count() if args.jobs == 0 else args.jobs
if verbose > 1:
print('run on %d cpus' % cpus)
p = Pool(cpus)
p.map(run_thunderfish, zip(files, [pool_args]*len(files)))
else:
list(map(run_thunderfish, zip(files, [pool_args]*len(files))))
if multi_pdf is not None:
multi_pdf.close()
if __name__ == '__main__':
freeze_support() # needed by multiprocessing for some weired windows stuff
main()
Functions
def configuration()
-
Assemble configuration parameter for thunderfish.
Returns
cfg
:ConfigFile
- Configuration parameters.
Expand source code
def configuration(): """Assemble configuration parameter for thunderfish. Returns ------- cfg: ConfigFile Configuration parameters. """ cfg = ConfigFile() add_multi_psd_config(cfg) cfg.add('frequencyThreshold', 1.0, 'Hz', 'The fundamental frequency of each fish needs to be detected in each power spectrum within this threshold.') # TODO: make this threshold dependent on frequency resolution! cfg.add('minPSDAverages', 3, '', 'Minimum number of fft averages for estimating the power spectrum.') # needed by fishfinder add_psd_peak_detection_config(cfg) add_harmonic_groups_config(cfg) add_clip_config(cfg) cfg.add('unwrapData', False, '', 'Unwrap clipped voltage traces.') add_best_window_config(cfg, win_size=8.0, w_cv_ampl=10.0) add_check_pulse_config(cfg) add_eod_analysis_config(cfg, min_pulse_win=0.004) del cfg['eodSnippetFac'] del cfg['eodMinSnippet'] del cfg['eodMinSem'] add_eod_quality_config(cfg) add_species_config(cfg) add_write_table_config(cfg, table_format='csv', unit_style='row', align_columns=True, shrink_width=False) return cfg
def save_configuration(cfg, config_file)
-
Save configuration parameter for thunderfish to a file.
Parameters
cfg
:ConfigFile
- Configuration parameters and their values.
config_file
:string
- Name of the configuration file to be loaded.
Expand source code
def save_configuration(cfg, config_file): """Save configuration parameter for thunderfish to a file. Parameters ---------- cfg: ConfigFile Configuration parameters and their values. config_file: string Name of the configuration file to be loaded. """ ext = os.path.splitext(config_file)[1] if ext != os.extsep + 'cfg': print('configuration file name must have .cfg as extension!') else: print('write configuration to %s ...' % config_file) del cfg['fileColumnNumbers'] del cfg['fileShrinkColumnWidth'] del cfg['fileMissing'] del cfg['fileLaTeXLabelCommand'] del cfg['fileLaTeXMergeStd'] cfg.dump(config_file)
def detect_eods(data, samplerate, min_clip, max_clip, name, mode, verbose, plot_level, cfg)
-
Detect EODs of all fish present in the data.
Parameters
data
:array
offloats
- The recording in which to detect EODs.
samplerate
:float
- Sampling rate of the dataset.
min_clip
:float
- Minimum amplitude that is not clipped.
max_clip
:float
- Maximum amplitude that is not clipped.
name
:string
- Name of the recording (e.g. its filename).
mode
:string
- Characters in the string indicate what and how to analyze: - 'w': analyze wavefish - 'p': analyze pulsefish - 'P': analyze only the pulsefish with the largest amplitude (not implemented yet)
verbose
:int
- Print out information about EOD detection if greater than zero.
plot_level
:int
- Similar to verbosity levels, but with plots.
cfg
:ConfigFile
- Configuration parameters.
Returns
psd_data
:list
of2D arrays
- List of power spectra (frequencies and power) of the analysed data for different frequency resolutions.
wave_eodfs
:list
of2D arrays
- Frequency and power of fundamental frequency/harmonics of all wave fish.
wave_indices
:array
ofint
- Indices of wave fish mapping from wave_eodfs to eod_props. If negative, then that EOD frequency has no waveform described in eod_props.
eod_props
:list
ofdict
- Lists of EOD properties as returned by analyze_pulse() and analyze_wave() for each waveform in mean_eods.
- mean_eods: list of 2-D arrays with time, mean, sem, and fit.
- Averaged EOD waveforms of pulse and wave fish.
spec_data
:list
of2_D arrays
- For each pulsefish a power spectrum of the single pulse and for each wavefish the relative amplitudes and phases of the harmonics.
peak_data
:list
of2_D arrays
- For each pulse fish a list of peak properties (index, time, and amplitude), empty array for wave fish.
power_thresh
:2 D array
orNone
- Frequency (first column) and power (second column) of threshold derived from single pulse spectra to discard false wave fish. None if no pulse fish was detected.
skip_reason
:list
ofstring
- Reasons, why an EOD was discarded.
Expand source code
def detect_eods(data, samplerate, min_clip, max_clip, name, mode, verbose, plot_level, cfg): """Detect EODs of all fish present in the data. Parameters ---------- data: array of floats The recording in which to detect EODs. samplerate: float Sampling rate of the dataset. min_clip: float Minimum amplitude that is not clipped. max_clip: float Maximum amplitude that is not clipped. name: string Name of the recording (e.g. its filename). mode: string Characters in the string indicate what and how to analyze: - 'w': analyze wavefish - 'p': analyze pulsefish - 'P': analyze only the pulsefish with the largest amplitude (not implemented yet) verbose: int Print out information about EOD detection if greater than zero. plot_level : int Similar to verbosity levels, but with plots. cfg: ConfigFile Configuration parameters. Returns ------- psd_data: list of 2D arrays List of power spectra (frequencies and power) of the analysed data for different frequency resolutions. wave_eodfs: list of 2D arrays Frequency and power of fundamental frequency/harmonics of all wave fish. wave_indices: array of int Indices of wave fish mapping from wave_eodfs to eod_props. If negative, then that EOD frequency has no waveform described in eod_props. eod_props: list of dict Lists of EOD properties as returned by analyze_pulse() and analyze_wave() for each waveform in mean_eods. mean_eods: list of 2-D arrays with time, mean, sem, and fit. Averaged EOD waveforms of pulse and wave fish. spec_data: list of 2_D arrays For each pulsefish a power spectrum of the single pulse and for each wavefish the relative amplitudes and phases of the harmonics. peak_data: list of 2_D arrays For each pulse fish a list of peak properties (index, time, and amplitude), empty array for wave fish. power_thresh: 2 D array or None Frequency (first column) and power (second column) of threshold derived from single pulse spectra to discard false wave fish. None if no pulse fish was detected. skip_reason: list of string Reasons, why an EOD was discarded. """ dfreq = np.nan nfft = 0 psd_data = [[]] wave_eodfs = [] wave_indices = [] if 'w' in mode: # detect wave fish: psd_data = multi_psd(data, samplerate, **multi_psd_args(cfg)) dfreq = np.mean(np.diff(psd_data[0][:,0])) nfft = int(samplerate/dfreq) h_kwargs = psd_peak_detection_args(cfg) h_kwargs.update(harmonic_groups_args(cfg)) wave_eodfs_list = [] for i, psd in enumerate(psd_data): wave_eodfs = harmonic_groups(psd[:,0], psd[:,1], verbose-1, **h_kwargs)[0] if verbose > 0 and len(psd_data) > 1: numpsdresolutions = cfg.value('numberPSDResolutions') print('fundamental frequencies detected in power spectrum of window %d at resolution %d:' % (i//numpsdresolutions, i%numpsdresolutions)) if len(wave_eodfs) > 0: print(' ' + ' '.join(['%.1f' % freq[0, 0] for freq in wave_eodfs])) else: print(' none') wave_eodfs_list.append(wave_eodfs) wave_eodfs = consistent_fishes(wave_eodfs_list, df_th=cfg.value('frequencyThreshold')) if verbose > 0: if len(wave_eodfs) > 0: print('found %2d EOD frequencies consistent in all power spectra:' % len(wave_eodfs)) print(' ' + ' '.join(['%.1f' % freq[0, 0] for freq in wave_eodfs])) else: print('no fundamental frequencies are consistent in all power spectra') # analysis results: eod_props = [] mean_eods = [] spec_data = [] peak_data = [] power_thresh = None skip_reason = [] max_pulse_amplitude = 0.0 zoom_window = [] if 'p' in mode: # detect pulse fish: _, eod_times, eod_peaktimes, zoom_window, _ = extract_pulsefish(data, samplerate, verbose=verbose-1, plot_level=plot_level, save_path=os.path.splitext(os.path.basename(name))[0]) #eod_times = [] #eod_peaktimes = [] if verbose > 0: if len(eod_times) > 0: print('found %2d pulsefish EODs' % len(eod_times)) else: print('no pulsefish EODs found') # analyse eod waveform of pulse-fish: min_freq_res = cfg.value('frequencyResolution') for k, (eod_ts, eod_pts) in enumerate(zip(eod_times, eod_peaktimes)): mean_eod, eod_times0 = \ eod_waveform(data, samplerate, eod_ts, win_fac=0.8, min_win=cfg.value('eodMinPulseSnippet'), min_sem=False, **eod_waveform_args(cfg)) mean_eod, props, peaks, power = analyze_pulse(mean_eod, eod_times0, freq_resolution=min_freq_res, **analyze_pulse_args(cfg)) if len(peaks) == 0: print('error: no peaks in pulse EOD detected') continue clipped_frac = clipped_fraction(data, samplerate, eod_times0, mean_eod, min_clip, max_clip) props['peaktimes'] = eod_pts # XXX that should go into analyze pulse props['index'] = len(eod_props) props['clipped'] = clipped_frac props['samplerate'] = samplerate props['nfft'] = nfft props['dfreq'] = dfreq # add good waveforms only: skips, msg, skipped_clipped = pulse_quality(props, **pulse_quality_args(cfg)) if len(skips) == 0: eod_props.append(props) mean_eods.append(mean_eod) spec_data.append(power) peak_data.append(peaks) if verbose > 0: print('take %6.1fHz pulse fish: %s' % (props['EODf'], msg)) else: skip_reason += ['%.1fHz pulse fish %s' % (props['EODf'], skips)] if verbose > 0: print('skip %6.1fHz pulse fish: %s (%s)' % (props['EODf'], skips, msg)) # threshold for wave fish peaks based on single pulse spectra: if len(skips) == 0 or skipped_clipped: if max_pulse_amplitude < props['p-p-amplitude']: max_pulse_amplitude = props['p-p-amplitude'] i0 = np.argmin(np.abs(mean_eod[:,0])) i1 = len(mean_eod) - i0 pulse_data = np.zeros(len(data)) for t in props['peaktimes']: idx = int(t*samplerate) ii0 = i0 if idx-i0 >= 0 else idx ii1 = i1 if idx+i1 < len(pulse_data) else len(pulse_data)-1-idx pulse_data[idx-ii0:idx+ii1] = mean_eod[i0-ii0:i0+ii1,1] pulse_psd = multi_psd(pulse_data, samplerate, **multi_psd_args(cfg)) pulse_power = pulse_psd[0][:,1] pulse_power *= len(data)/samplerate/props['period']/len(props['peaktimes']) pulse_power *= 5.0 if power_thresh is None: power_thresh = pulse_psd[0] power_thresh[:,1] = pulse_power else: power_thresh[:,1] += pulse_power # remove wavefish below pulse fish power: if 'w' in mode and power_thresh is not None: n = len(wave_eodfs) maxh = 3 # XXX make parameter df = power_thresh[1,0] - power_thresh[0,0] for k, fish in enumerate(reversed(wave_eodfs)): idx = np.array(fish[:maxh,0]//df, dtype=int) for offs in range(-2, 3): nbelow = np.sum(fish[:maxh,1] < power_thresh[idx+offs,1]) if nbelow > 0: wave_eodfs.pop(n-1-k) if verbose > 0: print('skip %6.1fHz wave fish: %2d harmonics are below pulsefish threshold' % (fish[0,0], nbelow)) break if 'w' in mode: # analyse EOD waveform of all wavefish: powers = np.array([np.sum(fish[:,1]) for fish in wave_eodfs]) power_indices = np.argsort(-powers) wave_indices = np.zeros(len(wave_eodfs), dtype=int) - 3 for k, idx in enumerate(power_indices): fish = wave_eodfs[idx] eod_times = np.arange(0.0, len(data)/samplerate, 1.0/fish[0,0]) mean_eod, eod_times = \ eod_waveform(data, samplerate, eod_times, win_fac=3.0, min_win=0.0, min_sem=(k==0), **eod_waveform_args(cfg)) mean_eod, props, sdata, error_str = \ analyze_wave(mean_eod, fish, **analyze_wave_args(cfg)) if error_str: print(name + ': ' + error_str) clipped_frac = clipped_fraction(data, samplerate, eod_times, mean_eod, min_clip, max_clip) props['n'] = len(eod_times) props['index'] = len(eod_props) props['clipped'] = clipped_frac props['samplerate'] = samplerate props['nfft'] = nfft props['dfreq'] = dfreq # remove wave fish that are smaller than the largest pulse fish: if props['p-p-amplitude'] < 0.01*max_pulse_amplitude: rm_indices = power_indices[k:] if verbose > 0: print('skip %6.1fHz wave fish: power=%5.1fdB, p-p amplitude=%5.1fdB smaller than pulse fish=%5.1dB - 20dB' % (props['EODf'], decibel(powers[idx]), decibel(props['p-p-amplitude']), decibel(max_pulse_amplitude))) for idx in rm_indices[1:]: print('skip %6.1fHz wave fish: power=%5.1fdB even smaller' % (wave_eodfs[idx][0,0], decibel(powers[idx]))) wave_eodfs = [eodfs for idx, eodfs in enumerate(wave_eodfs) if idx not in rm_indices] wave_indices = np.array([idcs for idx, idcs in enumerate(wave_indices) if idx not in rm_indices], dtype=int) break # add good waveforms only: remove, skips, msg = wave_quality(props, sdata[1:,3], **wave_quality_args(cfg)) if len(skips) == 0: wave_indices[idx] = props['index'] eod_props.append(props) mean_eods.append(mean_eod) spec_data.append(sdata) peak_data.append([]) if verbose > 0: print('take %6.1fHz wave fish: %s' % (props['EODf'], msg)) else: wave_indices[idx] = -2 if remove else -1 skip_reason += ['%.1fHz wave fish %s' % (props['EODf'], skips)] if verbose > 0: print('%-6s %6.1fHz wave fish: %s (%s)' % ('remove' if remove else 'skip', props['EODf'], skips, msg)) wave_eodfs = [eodfs for idx, eodfs in zip(wave_indices, wave_eodfs) if idx > -2] wave_indices = np.array([idx for idx in wave_indices if idx > -2], dtype=int) return (psd_data, wave_eodfs, wave_indices, eod_props, mean_eods, spec_data, peak_data, power_thresh, skip_reason, zoom_window)
def remove_eod_files(output_basename, verbose, cfg)
-
Remove all files from previous runs of thunderfish
Expand source code
def remove_eod_files(output_basename, verbose, cfg): """Remove all files from previous runs of thunderfish """ ff = cfg.value('fileFormat') if ff == 'py': fext = 'py' else: fext = TableData.extensions[ff] # remove all files from previous runs of thunderfish: for fn in glob.glob('%s*.%s' % (output_basename, fext)): os.remove(fn) if verbose > 0: print('removed file %s' % fn)
def plot_style()
-
Set style of plots.
Expand source code
def plot_style(): """Set style of plots. """ plt.rcParams['figure.facecolor'] = 'white' plt.rcParams['axes.facecolor'] = 'none' plt.rcParams['xtick.direction'] = 'out' plt.rcParams['ytick.direction'] = 'out'
def axes_style(ax)
-
Fix style of axes.
Parameters
ax
:matplotlib axes
Expand source code
def axes_style(ax): """Fix style of axes. Parameters ---------- ax: matplotlib axes """ ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.get_xaxis().tick_bottom() ax.get_yaxis().tick_left()
def plot_eods(base_name, message_filename, raw_data, samplerate, channel, idx0, idx1, clipped, psd_data, wave_eodfs, wave_indices, mean_eods, eod_props, peak_data, spec_data, indices, unit, zoom_window, n_snippets=10, power_thresh=None, label_power=True, all_eods=False, spec_plots='auto', skip_bad=True, log_freq=False, min_freq=0.0, max_freq=3000.0, interactive=True, verbose=0)
-
Creates an output plot for the thunderfish program.
This output contains the raw trace where the analysis window is marked, the power-spectrum of this analysis window where the detected fish are marked, plots of averaged EOD plots, and spectra of the EOD waveforms.
Parameters
base_name
:string
- Basename of audio_file.
message_filename
:string
orNone
- Path to meta-data message.
raw_data
:array
- Dataset.
samplerate
:float
- Sampling rate of the dataset.
channel
:int
orNone
- Channel of the recording to be put into the plot title. If None, do not write the channel into the title.
idx0
:float
- Index of the beginning of the analysis window in the dataset.
idx1
:float
- Index of the end of the analysis window in the dataset.
clipped
:float
- Fraction of clipped amplitudes.
psd_data
:2D array
- Power spectrum (frequencies and power) of the analysed data.
wave_eodfs
:array
- Frequency and power of fundamental frequency/harmonics of several fish.
wave_indices
:array
ofint
- Indices of wave fish mapping from wave_eodfs to eod_props. If negative, then that EOD frequency has no waveform described in eod_props.
- mean_eods: list of 2-D arrays with time, mean and std.
- Mean trace for the mean EOD plot.
eod_props
:list
ofdict
- Properties for each waveform in mean_eods.
peak_data
:list
of2_D arrays
- For each pulsefish a list of peak properties (index, time, and amplitude).
spec_data
:list
of2_D arrays
- For each pulsefish a power spectrum of the single pulse and for each wavefish the relative amplitudes and phases of the harmonics.
indices
:list
ofint
orNone
- Indices of the fish in eod_props to be plotted. If None try to plot all.
unit
:string
- Unit of the trace and the mean EOD.
n_snippets
:int
- Number of EOD waveform snippets to be plotted. If zero do not plot any.
power_thresh
:2 D array
orNone
- Frequency (first column) and power (second column) of threshold derived from single pulse spectra to discard false wave fish.
label_power
:boolean
- If
True
put the power in decibel in addition to the frequency into the legend. all_eods
:bool
- Plot all EOD waveforms.
spec_plots
:bool
or'auto'
- Plot amplitude spectra of EOD waveforms. If 'auto', plot them if there is a singel waveform only.
skip_bad
:bool
- Skip harmonic groups without index (entry in indices is negative).
log_freq
:boolean
- Logarithmic (True) or linear (False) frequency axis of power spectrum of recording.
min_freq
:float
- Limits of frequency axis of power spectrum of recording
are set to
(min_freq, max_freq)
ifmax_freq
is greater than zero max_freq
:float
- Limits of frequency axis of power spectrum of recording
are set to
(min_freq, max_freq)
and limits of power axis are computed from powers below max_freq ifmax_freq
is greater than zero interactive
:bool
- If True install some keyboard interaction.
verbose
:int
- Print out information about data to be plotted if greater than zero.
Returns
fig
:plt.figure
- Figure with the plots.
Expand source code
def plot_eods(base_name, message_filename, raw_data, samplerate, channel, idx0, idx1, clipped, psd_data, wave_eodfs, wave_indices, mean_eods, eod_props, peak_data, spec_data, indices, unit, zoom_window, n_snippets=10, power_thresh=None, label_power=True, all_eods=False, spec_plots='auto', skip_bad=True, log_freq=False, min_freq=0.0, max_freq=3000.0, interactive=True, verbose=0): """Creates an output plot for the thunderfish program. This output contains the raw trace where the analysis window is marked, the power-spectrum of this analysis window where the detected fish are marked, plots of averaged EOD plots, and spectra of the EOD waveforms. Parameters ---------- base_name: string Basename of audio_file. message_filename: string or None Path to meta-data message. raw_data: array Dataset. samplerate: float Sampling rate of the dataset. channel: int or None Channel of the recording to be put into the plot title. If None, do not write the channel into the title. idx0: float Index of the beginning of the analysis window in the dataset. idx1: float Index of the end of the analysis window in the dataset. clipped: float Fraction of clipped amplitudes. psd_data: 2D array Power spectrum (frequencies and power) of the analysed data. wave_eodfs: array Frequency and power of fundamental frequency/harmonics of several fish. wave_indices: array of int Indices of wave fish mapping from wave_eodfs to eod_props. If negative, then that EOD frequency has no waveform described in eod_props. mean_eods: list of 2-D arrays with time, mean and std. Mean trace for the mean EOD plot. eod_props: list of dict Properties for each waveform in mean_eods. peak_data: list of 2_D arrays For each pulsefish a list of peak properties (index, time, and amplitude). spec_data: list of 2_D arrays For each pulsefish a power spectrum of the single pulse and for each wavefish the relative amplitudes and phases of the harmonics. indices: list of int or None Indices of the fish in eod_props to be plotted. If None try to plot all. unit: string Unit of the trace and the mean EOD. n_snippets: int Number of EOD waveform snippets to be plotted. If zero do not plot any. power_thresh: 2 D array or None Frequency (first column) and power (second column) of threshold derived from single pulse spectra to discard false wave fish. label_power: boolean If `True` put the power in decibel in addition to the frequency into the legend. all_eods: bool Plot all EOD waveforms. spec_plots: bool or 'auto' Plot amplitude spectra of EOD waveforms. If 'auto', plot them if there is a singel waveform only. skip_bad: bool Skip harmonic groups without index (entry in indices is negative). log_freq: boolean Logarithmic (True) or linear (False) frequency axis of power spectrum of recording. min_freq: float Limits of frequency axis of power spectrum of recording are set to `(min_freq, max_freq)` if `max_freq` is greater than zero max_freq: float Limits of frequency axis of power spectrum of recording are set to `(min_freq, max_freq)` and limits of power axis are computed from powers below max_freq if `max_freq` is greater than zero interactive: bool If True install some keyboard interaction. verbose: int Print out information about data to be plotted if greater than zero. Returns ------- fig: plt.figure Figure with the plots. """ def keypress(event): if event.key in 'pP': if idx1 > idx0: playdata = 1.0 * raw_data[idx0:idx1] else: playdata = 1.0 * raw_data[:] fade(playdata, samplerate, 0.1) play(playdata, samplerate, blocking=False) if event.key in 'mM' and message_filename: # play voice message: msg, msg_rate = load_audio(message_filename) play(msg, msg_rate, blocking=False) def recording_format_coord(x, y): return 'full recording: x=%.3f s, y=%.3f' % (x, y) def recordingzoom_format_coord(x, y): return 'recording zoom-in: x=%.3f s, y=%.3f' % (x, y) def psd_format_coord(x, y): return 'power spectrum: x=%.1f Hz, y=%.1f dB' % (x, y) def meaneod_format_coord(x, y): return 'mean EOD waveform: x=%.2f ms, y=%.3f' % (x, y) def ampl_format_coord(x, y): return u'amplitude spectrum: x=%.0f, y=%.2f' % (x, y) def phase_format_coord(x, y): return u'phase spectrum: x=%.0f, y=%.2f \u03c0' % (x, y/np.pi) def pulsepsd_format_coord(x, y): return 'single pulse power spectrum: x=%.1f Hz, y=%.1f dB' % (x, y) # count number of fish types to be plotted: if indices is None: indices = np.arange(len(eod_props)) else: indices = np.array(indices, dtype=int) nwave = 0 npulse = 0 for idx in indices: if eod_props[idx]['type'] == 'pulse': npulse += 1 elif eod_props[idx]['type'] == 'wave': nwave += 1 neods = nwave + npulse if verbose > 0: print('plot: %2d waveforms: %2d wave fish, %2d pulse fish and %2d EOD frequencies.' % (len(indices), nwave, npulse, len(wave_eodfs))) # size and positions: if spec_plots == 'auto': spec_plots = len(indices) == 1 large_plots = spec_plots or len(indices) <= 2 width = 14.0 height = 10.0 if all_eods and len(indices) > 0: nrows = len(indices) if spec_plots else (len(indices)+1)//2 if large_plots: height = 6.0 + 4.0*nrows else: height = 6.4 + 1.9*nrows leftx = 1.0/width midx = 0.5 + leftx fullwidth = 1.0-1.4/width halfwidth = 0.5-1.4/width pheight = 3.0/height # figure: plot_style() fig = plt.figure(figsize=(width, height)) if interactive: fig.canvas.mpl_connect('key_press_event', keypress) # plot title: title = base_name if channel is not None: title += ' c%d' % channel ax = fig.add_axes([0.2/width, 1.0-0.6/height, 1.0-0.4/width, 0.55/height]) ax.text(0.0, 1.0, title, fontsize=22, va='top') ax.text(1.0, 1.0, 'thunderfish by Benda-Lab', fontsize=16, ha='right', va='top') ax.text(1.0, 0.0, 'version %s' % __version__, fontsize=16, ha='right', va='bottom') ax.set_frame_on(False) ax.set_axis_off() ax.set_navigate(False) # layout of recording and psd plots: #force_both = True # set to True for debugging pulse and wave detection force_both = False posy = 1.0 - 4.0/height axr = None axp = None legend_inside = True legendwidth = 2.2/width if label_power else 1.7/width if neods == 0: axr = fig.add_axes([leftx, posy, fullwidth, pheight]) # top, wide if len(psd_data) > 0: axp = fig.add_axes([leftx, 2.0/height, fullwidth, pheight]) # bottom, wide else: if npulse == 0 and nwave > 2 and psd_data is not None and \ len(psd_data) > 0 and not force_both: axp = fig.add_axes([leftx, posy, fullwidth-legendwidth, pheight]) # top, wide legend_inside = False elif (npulse > 0 or psd_data is None or len(psd_data) == 0) \ and len(wave_eodfs) == 0 and not force_both: axr = fig.add_axes([leftx, posy, fullwidth, pheight]) # top, wide else: axr = fig.add_axes([leftx, posy, halfwidth, pheight]) # top left label_power = False legendwidth = 2.2/width axp = fig.add_axes([midx, posy, halfwidth, pheight]) # top, right # best window data: data = raw_data[idx0:idx1] if idx1 > idx0 else raw_data # plot recording pulse_colors, pulse_markers = colors_markers() pulse_colors = pulse_colors[3:] pulse_markers = pulse_markers[3:] if axr is not None: axes_style(axr) twidth = 0.1 if len(indices) > 0: if eod_props[indices[0]]['type'] == 'wave': twidth = 5.0/eod_props[indices[0]]['EODf'] else: if len(wave_eodfs) > 0: twidth = 3.0/eod_props[indices[0]]['EODf'] else: twidth = 10.0/eod_props[indices[0]]['EODf'] twidth = (1+twidth//0.005)*0.005 if data is not None and len(data) > 0: plot_eod_recording(axr, data, samplerate, unit, twidth, idx0/samplerate) plot_pulse_eods(axr, data, samplerate, zoom_window, twidth, eod_props, idx0/samplerate, colors=pulse_colors, markers=pulse_markers, frameon=True, loc='upper right') if axr.get_legend() is not None: axr.get_legend().get_frame().set_color('white') axr.set_title('Recording', fontsize=14, y=1.05) axr.format_coord = recordingzoom_format_coord # plot psd wave_colors, wave_markers = colors_markers() if axp is not None: axes_style(axp) if power_thresh is not None: axp.plot(power_thresh[:,0], decibel(power_thresh[:,1]), '#CCCCCC', lw=1) if len(wave_eodfs) > 0: kwargs = {} if len(wave_eodfs) > 1: title = '%d EOD frequencies' % len(wave_eodfs) kwargs = {'title': title if len(wave_eodfs) > 2 else None } if legend_inside: kwargs.update({'bbox_to_anchor': (1.05, 1.1), 'loc': 'upper right', 'legend_rows': 10, 'frameon': True}) else: kwargs.update({'bbox_to_anchor': (1.02, 1.1), 'loc': 'upper left', 'legend_rows': 14, 'labelspacing': 0.6, 'frameon': False}) plot_harmonic_groups(axp, wave_eodfs, wave_indices, max_groups=0, skip_bad=skip_bad, sort_by_freq=True, label_power=label_power, colors=wave_colors, markers=wave_markers, **kwargs) if legend_inside: axp.get_legend().get_frame().set_color('white') if psd_data is not None and len(psd_data) > 0: plot_decibel_psd(axp, psd_data[:,0], psd_data[:,1], log_freq=log_freq, min_freq=min_freq, max_freq=max_freq, ymarg=5.0, color='blue') axp.yaxis.set_major_locator(ticker.MaxNLocator(6)) if len(wave_eodfs) == 1: axp.get_legend().set_visible(False) label = '%6.1f Hz' % wave_eodfs[0][0, 0] axp.set_title('Powerspectrum: %s' % label, y=1.05, fontsize=14) else: axp.set_title('Powerspectrum', y=1.05, fontsize=14) axp.format_coord = psd_format_coord # get fish labels from legends: if axp is not None: w, _ = axp.get_legend_handles_labels() eodf_labels = [wi.get_label().split()[0] for wi in w] legend_wave_eodfs = np.array([float(f) if f[0] != '(' else np.nan for f in eodf_labels]) if axr is not None: p, _ = axr.get_legend_handles_labels() eodf_labels = [pi.get_label().split()[0] for pi in p] legend_pulse_eodfs = np.array([float(f) if f[0] != '(' else np.nan for f in eodf_labels]) # layout: sheight = 1.4/height sstep = 1.6/height max_plots = len(indices) if not all_eods: if large_plots: max_plots = 1 if spec_plots else 2 else: max_plots = 4 if large_plots: pstep = pheight + 1.0/height ty = 1.08 my = 1.10 ny = 6 else: posy -= 0.2/height pheight = 1.3/height pstep = 1.9/height ty = 1.10 my = 1.16 ny = 4 posy -= pstep # sort indices by p-p amplitude: pp_ampls = [eod_props[idx]['p-p-amplitude'] for idx in indices] pp_indices = np.argsort(pp_ampls)[::-1] # plot EOD waveform and spectra: for k, idx in enumerate(indices[pp_indices]): if k >= max_plots: break # plot EOD waveform: mean_eod = mean_eods[idx] props = eod_props[idx] peaks = peak_data[idx] lx = leftx if spec_plots or k%2 == 0 else midx ax = fig.add_axes([lx, posy, halfwidth, pheight]) axes_style(ax) ax.yaxis.set_major_locator(ticker.MaxNLocator(ny)) if len(indices) > 1: ax.text(0.3, ty, '{EODf:.1f} Hz {type} fish'.format(**props), transform=ax.transAxes, fontsize=14, zorder=20) mx = 0.25 else: ax.text(-0.1, ty, '{EODf:.1f} Hz {type} fish'.format(**props), transform=ax.transAxes, fontsize=14, zorder=20) ax.text(0.5, ty, 'Averaged EOD', ha='center', transform=ax.transAxes, fontsize=14, zorder=20) mx = -0.14 eodf = props['EODf'] if props['type'] == 'wave': if axp is not None: wk = np.nanargmin(np.abs(legend_wave_eodfs - eodf)) ma = ml.Line2D([mx], [my], color=w[wk].get_color(), marker=w[wk].get_marker(), markersize=w[wk].get_markersize(), mec='none', clip_on=False, label=w[wk].get_label(), transform=ax.transAxes) ax.add_line(ma) else: if axr is not None and len(legend_pulse_eodfs) > 0: pk = np.argmin(np.abs(legend_pulse_eodfs - eodf)) ma = ml.Line2D([mx], [my], color=p[pk].get_color(), marker=p[pk].get_marker(), markersize=p[pk].get_markersize(), mec='none', clip_on=False, label=p[pk].get_label(), transform=ax.transAxes) ax.add_line(ma) plot_eod_waveform(ax, mean_eod, props, peaks, unit) if props['type'] == 'pulse' and 'times' in props: plot_eod_snippets(ax, data, samplerate, mean_eod[0,0], mean_eod[-1,0], props['times'], n_snippets, props['flipped']) if not large_plots and k < max_plots-2: ax.set_xlabel('') ax.format_coord = meaneod_format_coord # plot spectra: if spec_plots: spec = spec_data[idx] if props['type'] == 'pulse': ax = fig.add_axes([midx, posy, halfwidth, pheight]) axes_style(ax) plot_pulse_spectrum(ax, spec, props) ax.set_title('Single pulse spectrum', fontsize=14, y=1.05) ax.format_coord = pulsepsd_format_coord else: axa = fig.add_axes([midx, posy+sstep, halfwidth, sheight]) axes_style(axa) axp = fig.add_axes([midx, posy, halfwidth, sheight]) axes_style(axp) plot_wave_spectrum(axa, axp, spec, props, unit) axa.set_title('Amplitude and phase spectrum', fontsize=14, y=1.05) axa.set_xticklabels([]) axa.yaxis.set_major_locator(ticker.MaxNLocator(4)) axa.format_coord = ampl_format_coord axp.format_coord = phase_format_coord if spec_plots or k%2 == 1: posy -= pstep # whole trace: ax = fig.add_axes([leftx, 0.6/height, fullwidth, 0.9/height]) axes_style(ax) if raw_data is not None and len(raw_data) > 0: plot_data_window(ax, raw_data, samplerate, unit, idx0, idx1, clipped) ax.format_coord = recording_format_coord return fig
def plot_eod_subplots(base_name, subplots, raw_data, samplerate, idx0, idx1, clipped, psd_data, wave_eodfs, wave_indices, mean_eods, eod_props, peak_data, spec_data, unit, zoom_window, n_snippets=10, power_thresh=None, label_power=True, skip_bad=True, log_freq=False, min_freq=0.0, max_freq=3000.0, save=True)
-
Plot time traces and spectra into separate windows or files.
Parameters
base_name
:string
- Basename of audio_file.
subplots
:string
- Specifies which subplots to plot: r) recording with best 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. With capital letters all fish are saved into a single pdf file, with small letters each fish is saved into a separate file.
raw_data
:array
- Dataset.
samplerate
:float
- Sampling rate of the dataset.
idx0
:float
- Index of the beginning of the analysis window in the dataset.
idx1
:float
- Index of the end of the analysis window in the dataset.
clipped
:float
- Fraction of clipped amplitudes.
psd_data
:2D array
- Power spectrum (frequencies and power) of the analysed data.
wave_eodfs
:array
- Frequency and power of fundamental frequency/harmonics of several fish.
wave_indices
:array
ofint
- Indices of wave fish mapping from wave_eodfs to eod_props. If negative, then that EOD frequency has no waveform described in eod_props.
- mean_eods: list of 2-D arrays with time, mean and std.
- Mean trace for the mean EOD plot.
eod_props
:list
ofdict
- Properties for each waveform in mean_eods.
peak_data
:list
of2_D arrays
- For each pulsefish a list of peak properties (index, time, and amplitude).
spec_data
:list
of2_D arrays
- For each pulsefish a power spectrum of the single pulse and for each wavefish the relative amplitudes and phases of the harmonics.
unit
:string
- Unit of the trace and the mean EOD.
n_snippets
:int
- Number of EOD waveform snippets to be plotted. If zero do not plot any.
power_thresh
:2 D array
orNone
- Frequency (first column) and power (second column) of threshold derived from single pulse spectra to discard false wave fish.
label_power
:boolean
- If
True
put the power in decibel in addition to the frequency into the legend. skip_bad
:bool
- Skip harmonic groups without index (entry in indices is negative).
log_freq
:boolean
- Logarithmic (True) or linear (False) frequency axis of power spectrum of recording.
min_freq
:float
- Limits of frequency axis of power spectrum of recording
are set to
(min_freq, max_freq)
ifmax_freq
is greater than zero max_freq
:float
- Limits of frequency axis of power spectrum of recording
are set to
(min_freq, max_freq)
and limits of power axis are computed from powers below max_freq ifmax_freq
is greater than zero save
:bool
- If True save plots to files instead of showing them.
Expand source code
def plot_eod_subplots(base_name, subplots, raw_data, samplerate, idx0, idx1, clipped, psd_data, wave_eodfs, wave_indices, mean_eods, eod_props, peak_data, spec_data, unit, zoom_window, n_snippets=10, power_thresh=None, label_power=True, skip_bad=True, log_freq=False, min_freq=0.0, max_freq=3000.0, save=True): """Plot time traces and spectra into separate windows or files. Parameters ---------- base_name: string Basename of audio_file. subplots: string Specifies which subplots to plot: r) recording with best 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. With capital letters all fish are saved into a single pdf file, with small letters each fish is saved into a separate file. raw_data: array Dataset. samplerate: float Sampling rate of the dataset. idx0: float Index of the beginning of the analysis window in the dataset. idx1: float Index of the end of the analysis window in the dataset. clipped: float Fraction of clipped amplitudes. psd_data: 2D array Power spectrum (frequencies and power) of the analysed data. wave_eodfs: array Frequency and power of fundamental frequency/harmonics of several fish. wave_indices: array of int Indices of wave fish mapping from wave_eodfs to eod_props. If negative, then that EOD frequency has no waveform described in eod_props. mean_eods: list of 2-D arrays with time, mean and std. Mean trace for the mean EOD plot. eod_props: list of dict Properties for each waveform in mean_eods. peak_data: list of 2_D arrays For each pulsefish a list of peak properties (index, time, and amplitude). spec_data: list of 2_D arrays For each pulsefish a power spectrum of the single pulse and for each wavefish the relative amplitudes and phases of the harmonics. unit: string Unit of the trace and the mean EOD. n_snippets: int Number of EOD waveform snippets to be plotted. If zero do not plot any. power_thresh: 2 D array or None Frequency (first column) and power (second column) of threshold derived from single pulse spectra to discard false wave fish. label_power: boolean If `True` put the power in decibel in addition to the frequency into the legend. skip_bad: bool Skip harmonic groups without index (entry in indices is negative). log_freq: boolean Logarithmic (True) or linear (False) frequency axis of power spectrum of recording. min_freq: float Limits of frequency axis of power spectrum of recording are set to `(min_freq, max_freq)` if `max_freq` is greater than zero max_freq: float Limits of frequency axis of power spectrum of recording are set to `(min_freq, max_freq)` and limits of power axis are computed from powers below max_freq if `max_freq` is greater than zero save: bool If True save plots to files instead of showing them. """ plot_style() if 'r' in subplots: fig, ax = plt.subplots(figsize=(10, 2)) fig.subplots_adjust(left=0.07, right=0.99, bottom=0.22, top=0.95) plot_data_window(ax, raw_data, samplerate, unit, idx0, idx1, clipped) ax.yaxis.set_major_locator(ticker.MaxNLocator(5)) axes_style(ax) if save: fig.savefig(base_name + '-recording.pdf') if 't' in subplots: fig, ax = plt.subplots(figsize=(10, 6)) twidth = 0.1 if len(eod_props) > 0: if eod_props[0]['type'] == 'wave': twidth = 5.0/eod_props[0]['EODf'] else: if len(wave_eodfs) > 0: twidth = 3.0/eod_props[0]['EODf'] else: twidth = 10.0/eod_props[0]['EODf'] twidth = (1+twidth//0.005)*0.005 pulse_colors, pulse_markers = colors_markers() pulse_colors = pulse_colors[3:] pulse_markers = pulse_markers[3:] plot_eod_recording(ax, raw_data[idx0:idx1], samplerate, unit, twidth, idx0/samplerate) plot_pulse_eods(ax, raw_data[idx0:idx1], samplerate, zoom_window, twidth, eod_props, idx0/samplerate, colors=pulse_colors, markers=pulse_markers, frameon=True, loc='upper right') if ax.get_legend() is not None: ax.get_legend().get_frame().set_color('white') axes_style(ax) if save: fig.savefig(base_name + '-trace.pdf') if 'p' in subplots: fig, ax = plt.subplots(figsize=(10, 5)) fig.subplots_adjust(left=0.08, right=0.975, bottom=0.11, top=0.9) axes_style(ax) if power_thresh is not None: ax.plot(power_thresh[:,0], decibel(power_thresh[:,1]), '#CCCCCC', lw=1) if len(wave_eodfs) > 0: kwargs = {} if len(wave_eodfs) > 1: title = '%d EOD frequencies' % len(wave_eodfs) kwargs = {'title': title if len(wave_eodfs) > 2 else None } if len(wave_eodfs) > 2: fig.subplots_adjust(left=0.08, right=0.78, bottom=0.11, top=0.9) kwargs.update({'bbox_to_anchor': (1.01, 1.1), 'loc': 'upper left', 'legend_rows': 14, 'labelspacing': 0.6}) else: kwargs.update({'bbox_to_anchor': (1.05, 1.1), 'loc': 'upper right', 'legend_rows': 10}) wave_colors, wave_markers = colors_markers() plot_harmonic_groups(ax, wave_eodfs, wave_indices, max_groups=0, skip_bad=skip_bad, sort_by_freq=True, label_power=label_power, colors=wave_colors, markers=wave_markers, frameon=False, **kwargs) plot_decibel_psd(ax, psd_data[:,0], psd_data[:,1], log_freq=log_freq, min_freq=min_freq, max_freq=max_freq, ymarg=5.0, color='blue') ax.yaxis.set_major_locator(ticker.MaxNLocator(6)) if len(wave_eodfs) == 1: ax.get_legend().set_visible(False) label = '%6.1f Hz' % wave_eodfs[0][0, 0] ax.set_title('Powerspectrum: %s' % label, y=1.05) else: ax.set_title('Powerspectrum', y=1.05) if save: fig.savefig(base_name + '-psd.pdf') if 'w' in subplots or 'W' in subplots: mpdf = None if 'W' in subplots: mpdf = PdfPages(base_name + '-waveforms.pdf') for meod, props, peaks in zip(mean_eods, eod_props, peak_data): if meod is None: continue fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(left=0.18, right=0.98, bottom=0.15, top=0.9) if not props is None: ax.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props)) plot_eod_waveform(ax, meod, props, peaks, unit) data = raw_data[idx0:idx1] if idx1 > idx0 else raw_data if not props is None and props['type'] == 'pulse' and \ 'times' in props: plot_eod_snippets(ax, data, samplerate, meod[0,0], meod[-1,0], props['times'], n_snippets) ax.yaxis.set_major_locator(ticker.MaxNLocator(6)) axes_style(ax) if mpdf is None: if save: fig.savefig(base_name + '-waveform-%d.pdf' % props['index']) else: mpdf.savefig(fig) if mpdf is not None: mpdf.close() if 's' in subplots or 'S' in subplots: mpdf = None if 'S' in subplots: mpdf = PdfPages(base_name + '-spectrum.pdf') for props, peaks, spec in zip(eod_props, peak_data, spec_data): if spec is None: continue if props is not None and props['type'] == 'pulse': fig, ax = plt.subplots(figsize=(5, 3.5)) fig.subplots_adjust(left=0.15, right=0.967, bottom=0.16, top=0.88) axes_style(ax) ax.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props), y=1.07) plot_pulse_spectrum(ax, spec, props) else: fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(5, 3.5)) fig.subplots_adjust(left=0.15, right=0.97, bottom=0.16, top=0.88, hspace=0.4) axes_style(ax1) axes_style(ax2) if not props is None: ax1.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props), y=1.15) plot_wave_spectrum(ax1, ax2, spec, props, unit) ax1.set_xticklabels([]) ax1.yaxis.set_major_locator(ticker.MaxNLocator(4)) if mpdf is None: if save: fig.savefig(base_name + '-spectrum-%d.pdf' % props['index']) else: mpdf.savefig(fig) if mpdf is not None: mpdf.close() if 'e' in subplots or 'E' in subplots: mpdf = None if 'E' in subplots: mpdf = PdfPages(base_name + '-eods.pdf') for meod, props, peaks, spec in zip(mean_eods, eod_props, peak_data, spec_data): if meod is None or spec is None: continue fig = plt.figure(figsize=(10, 3.5)) gs = gridspec.GridSpec(nrows=2, ncols=2, left=0.09, right=0.98, bottom=0.16, top=0.88, wspace=0.4, hspace=0.4) ax1 = fig.add_subplot(gs[:,0]) if not props is None: ax1.set_title('{index:d}: {EODf:.1f} Hz {type} fish'.format(**props), y=1.07) plot_eod_waveform(ax1, meod, props, peaks, unit) data = raw_data[idx0:idx1] if idx1 > idx0 else raw_data if not props is None and props['type'] == 'pulse' and 'times' in props: plot_eod_snippets(ax1, data, samplerate, meod[0,0], meod[-1,0], props['times'], n_snippets) ax1.yaxis.set_major_locator(ticker.MaxNLocator(6)) axes_style(ax1) if not props is None and props['type'] == 'pulse': ax2 = fig.add_subplot(gs[:,1]) axes_style(ax2) plot_pulse_spectrum(ax2, spec, props) ax2.set_title('Single pulse spectrum', y=1.07) else: ax2 = fig.add_subplot(gs[0,1]) ax3 = fig.add_subplot(gs[1,1]) axes_style(ax2) axes_style(ax3) plot_wave_spectrum(ax2, ax3, spec, props, unit) ax2.set_title('Amplitude and phase spectrum', y=1.15) ax2.set_xticklabels([]) ax2.yaxis.set_major_locator(ticker.MaxNLocator(4)) if mpdf is None: if save: fig.savefig(base_name + '-eod-%d.pdf' % props['index']) else: mpdf.savefig(fig) if mpdf is not None: mpdf.close() if not save: plt.show() plt.close('all')
def thunderfish_plot(files, data_path=None, load_kwargs={}, all_eods=False, spec_plots='auto', skip_bad=True, save_plot=False, multi_pdf=None, save_subplots='', log_freq=False, min_freq=0.0, max_freq=3000.0, output_folder='.', keep_path=False, verbose=0)
-
Generate plots from saved analysis results.
Parameters
files
:list
ofstr
- Analysis files from a single recording.
data_path
:str
- Path where to find the raw data.
load_kwargs
:dict
- Key-word arguments for the
load_data()
function. all_eods
:bool
- If True, plot all EOD waveforms.
spec_plots
:bool
or'auto'
- Plot amplitude spectra of EOD waveforms. If 'auto', plot them if there is a singel waveform only.
skip_bad
:bool
- Skip harmonic groups without index in the spectrum plot.
save_plot
:bool
- If True, save plots as pdf file.
multi_pdf
:matplotlib.PdfPages
orNone
- PdfPages instance in which to save plots.
save_subplots
:string
- If not empty, specifies subplots to be saved as separate pdf files: r) recording with best 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. Capital letters produce a single multipage pdf containing plots of all detected fish.
log_freq
:boolean
- Logarithmic (True) or linear (False) frequency axis of power spectrum of recording.
min_freq
:float
- Limits of frequency axis of power spectrum of recording
are set to
(min_freq, max_freq)
, ifmax_freq
is greater than zero max_freq
:float
- Limits of frequency axis of power spectrum of recording
are set to
(min_freq, max_freq)
and limits of power axis are computed from powers below max_freq, ifmax_freq
is greater than zero output_folder
:string
- Folder where to save results.
keep_path
:bool
- Add relative path of data files to output path.
verbose
:int
Verbosity level (for debugging).
Expand source code
def thunderfish_plot(files, data_path=None, load_kwargs={}, all_eods=False, spec_plots='auto', skip_bad=True, save_plot=False, multi_pdf=None, save_subplots='', log_freq=False, min_freq=0.0, max_freq=3000.0, output_folder='.', keep_path=False, verbose=0): """Generate plots from saved analysis results. Parameters ---------- files: list of str Analysis files from a single recording. data_path: str Path where to find the raw data. load_kwargs: dict Key-word arguments for the `load_data()` function. all_eods: bool If True, plot all EOD waveforms. spec_plots: bool or 'auto' Plot amplitude spectra of EOD waveforms. If 'auto', plot them if there is a singel waveform only. skip_bad: bool Skip harmonic groups without index in the spectrum plot. save_plot: bool If True, save plots as pdf file. multi_pdf: matplotlib.PdfPages or None PdfPages instance in which to save plots. save_subplots: string If not empty, specifies subplots to be saved as separate pdf files: r) recording with best 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. Capital letters produce a single multipage pdf containing plots of all detected fish. log_freq: boolean Logarithmic (True) or linear (False) frequency axis of power spectrum of recording. min_freq: float Limits of frequency axis of power spectrum of recording are set to `(min_freq, max_freq)`, if `max_freq` is greater than zero max_freq: float Limits of frequency axis of power spectrum of recording are set to `(min_freq, max_freq)` and limits of power axis are computed from powers below max_freq, if `max_freq` is greater than zero output_folder: string Folder where to save results. keep_path: bool Add relative path of data files to output path. verbose: int Verbosity level (for debugging). """ if len(save_subplots) == 0: save_subplots = 'rtpwsed' # plot everything # load analysis results: mean_eods, wave_eodfs, wave_indices, eod_props, spec_data, \ peak_data, base_name, channel, unit = load_analysis(files) if len(mean_eods) == 0 or all(me is None for me in mean_eods): save_subplots = save_subplots.replace('w', '') save_subplots = save_subplots.replace('W', '') save_subplots = save_subplots.replace('e', '') save_subplots = save_subplots.replace('E', '') save_subplots = save_subplots.replace('d', '') if len(spec_data) == 0 or all(sd is None for sd in spec_data): save_subplots = save_subplots.replace('s', '') save_subplots = save_subplots.replace('S', '') save_subplots = save_subplots.replace('e', '') save_subplots = save_subplots.replace('E', '') save_subplots = save_subplots.replace('d', '') clipped = 0.0 if len(eod_props) > 0 and not eod_props[0] is None and \ 'winclipped' in eod_props[0]: clipped = eod_props[0]['winclipped'] zoom_window = [1.2, 1.3] # load recording: psd_data = None if base_name: name = os.path.basename(base_name) if data_path and data_path != '.' else base_name data_path = os.path.join(data_path, name) data, samplerate, idx0, idx1, data_path = \ load_recording(data_path, channel, load_kwargs, eod_props, verbose-1) if data is None: save_subplots = save_subplots.replace('r', '') save_subplots = save_subplots.replace('t', '') save_subplots = save_subplots.replace('d', '') if verbose > 0: print('loaded', data_path) if len(eod_props) > 0 and not eod_props[0] is None and \ 'dfreq' in eod_props[0] and data is not None and len(data) > 0: psd_data = multi_psd(data[idx0:idx1], samplerate, 1.1*eod_props[0]['dfreq'])[0] if psd_data is not None and len(psd_data) > 0: for idx, fish in zip(wave_indices, wave_eodfs): if idx < 0: for k in range(len(fish)): fish[k,1] = psd_data[np.argmin(np.abs(psd_data[:,0] - fish[k,0])),1] if psd_data is None: save_subplots = save_subplots.replace('p', '') save_subplots = save_subplots.replace('d', '') # file name for output files: fn = base_name if keep_path else os.path.basename(base_name) output_basename = os.path.join(output_folder, fn) if channel >= 0: output_basename += f'-c{channel}' # make directory if necessary: if keep_path: outpath = os.path.dirname(output_basename) if not os.path.exists(outpath): if verbose > 0: print('mkdir %s' % outpath) os.makedirs(outpath) # plot: if len(save_subplots) == 0 or 'd' in save_subplots: fig = plot_eods(os.path.basename(base_name), None, data, samplerate, channel, idx0, idx1, clipped, psd_data, wave_eodfs, wave_indices, mean_eods, eod_props, peak_data, spec_data, None, unit, zoom_window, 10, None, True, all_eods, spec_plots, skip_bad, log_freq, min_freq, max_freq, interactive=not save_plot, verbose=verbose-1) if save_plot: if multi_pdf is not None: multi_pdf.savefig(fig) else: fig.savefig(output_basename + '.pdf') else: fig.canvas.manager.set_window_title('thunderfish') plt.show() plt.close() save_subplots = save_subplots.replace('d', '') if len(save_subplots) > 0: plot_eod_subplots(output_basename, save_subplots, data, samplerate, idx0, idx1, clipped, psd_data, wave_eodfs, wave_indices, mean_eods, eod_props, peak_data, spec_data, unit, zoom_window, 10, None, True, skip_bad, log_freq, min_freq, max_freq, save_plot) return None
def thunderfish(filename, load_kwargs, cfg, channel=0, time=None, time_file=False, mode='wp', log_freq=False, min_freq=0.0, max_freq=3000, save_data=False, zip_file=False, all_eods=False, spec_plots='auto', skip_bad=True, save_plot=False, multi_pdf=None, save_subplots='', output_folder='.', keep_path=False, verbose=0, plot_level=0)
-
Automatically detect and analyze all EOD waveforms in a short recording.
Parameters
filename
:string
- Path of the data file to be analyzed.
load_kwargs
:dict
- Key-word arguments for the
load_data()
function. cfg
:dict
channel
:int
- Channel to be analyzed.
time
:string, float,
orNone
- Start time of analysis window: "beginning", "center", "end", "best", or time in seconds (as float or string). If not None overwrites "windowPosition" in cofiguration file.
time_file
:bool
- If
True
add time of analysis window to output file names. mode
:'w', 'p', 'P', 'wp',
or'wP'
- Analyze wavefish ('w'), all pulse fish ('p'), or largest pulse fish only ('P').
log_freq
:boolean
- Logarithmic (True) or linear (False) frequency axis of power spectrum of recording.
min_freq
:float
- Limits of frequency axis of power spectrum of recording
are set to
(min_freq, max_freq)
, ifmax_freq
is greater than zero max_freq
:float
- Limits of frequency axis of power spectrum of recording
are set to
(min_freq, max_freq)
and limits of power axis are computed from powers below max_freq, ifmax_freq
is greater than zero save_data
:bool
- If True save analysis results in files. If False, just plot the data.
zip_data
:bool
- If True, store all analysis results in a single zip file.
all_eods
:bool
- If True, plot all EOD waveforms.
spec_plots
:bool
or'auto'
- Plot amplitude spectra of EOD waveforms. If 'auto', plot them if there is a singel waveform only.
skip_bad
:bool
- Skip harmonic groups without index in the spectrum plot.
save_plot
:bool
- If True, save plots as pdf file.
multi_pdf
:matplotlib.PdfPages
orNone
- PdfPages instance in which to save plots.
save_subplots
:string
- If not empty, specifies subplots to be saved as separate pdf files: r) recording with best 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. Capital letters produce a single multipage pdf containing plots of all detected fish.
output_folder
:string
- Folder where to save results.
keep_path
:bool
- Add relative path of data files to output path.
verbose
:int
- Verbosity level (for debugging).
plot_level
:int
Plot intermediate results.
Returns
msg
:string
orNone
- In case of errors, an error message.
Expand source code
def thunderfish(filename, load_kwargs, cfg, channel=0, time=None, time_file=False, mode='wp', log_freq=False, min_freq=0.0, max_freq=3000, save_data=False, zip_file=False, all_eods=False, spec_plots='auto', skip_bad=True, save_plot=False, multi_pdf=None, save_subplots='', output_folder='.', keep_path=False, verbose=0, plot_level=0): """Automatically detect and analyze all EOD waveforms in a short recording. Parameters ---------- filename: string Path of the data file to be analyzed. load_kwargs: dict Key-word arguments for the `load_data()` function. cfg: dict channel: int Channel to be analyzed. time: string, float, or None Start time of analysis window: "beginning", "center", "end", "best", or time in seconds (as float or string). If not None overwrites "windowPosition" in cofiguration file. time_file: bool If `True` add time of analysis window to output file names. mode: 'w', 'p', 'P', 'wp', or 'wP' Analyze wavefish ('w'), all pulse fish ('p'), or largest pulse fish only ('P'). log_freq: boolean Logarithmic (True) or linear (False) frequency axis of power spectrum of recording. min_freq: float Limits of frequency axis of power spectrum of recording are set to `(min_freq, max_freq)`, if `max_freq` is greater than zero max_freq: float Limits of frequency axis of power spectrum of recording are set to `(min_freq, max_freq)` and limits of power axis are computed from powers below max_freq, if `max_freq` is greater than zero save_data: bool If True save analysis results in files. If False, just plot the data. zip_data: bool If True, store all analysis results in a single zip file. all_eods: bool If True, plot all EOD waveforms. spec_plots: bool or 'auto' Plot amplitude spectra of EOD waveforms. If 'auto', plot them if there is a singel waveform only. skip_bad: bool Skip harmonic groups without index in the spectrum plot. save_plot: bool If True, save plots as pdf file. multi_pdf: matplotlib.PdfPages or None PdfPages instance in which to save plots. save_subplots: string If not empty, specifies subplots to be saved as separate pdf files: r) recording with best 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. Capital letters produce a single multipage pdf containing plots of all detected fish. output_folder: string Folder where to save results. keep_path: bool Add relative path of data files to output path. verbose: int Verbosity level (for debugging). plot_level: int Plot intermediate results. Returns ------- msg: string or None In case of errors, an error message. """ # check data file: if len(filename) == 0: return 'you need to specify a file containing some data' # file names: fn = filename if keep_path else os.path.basename(filename) outfilename = os.path.splitext(fn)[0] messagefilename = os.path.splitext(fn)[0] + '-message.wav' if not os.path.isfile(messagefilename): messagefilename = None # load data: try: all_data, samplerate, unit, ampl_max = load_data(filename, verbose=verbose, **load_kwargs) except IOError as e: return '%s: failed to open file: %s' % (filename, str(e)) # select channel: channels = all_data.shape[1] chan_list = [channel] if channel < 0: chan_list = range(channels) elif channel >= channels: return '%s: invalid channel %d (%d channels)' % (filename, channel, channels) # process all channels: for chan in chan_list: raw_data = all_data[:,chan] if len(raw_data) <= 1: return '%s: empty data file' % filename if verbose >= 0 and len(chan_list) > 1: print(' channel %d' % chan) # analysis window: win_pos = cfg.value('windowPosition') if time is not None: win_pos = time data, idx0, idx1, clipped, min_clip, max_clip = \ analysis_window(raw_data, samplerate, ampl_max, win_pos, cfg, plot_level>0) found_bestwindow = idx1 > 0 if not found_bestwindow: return '%s: not enough data for requested window length. You may want to adjust the windowSize parameter in the configuration file.' % filename # detect EODs in the data: psd_data, wave_eodfs, wave_indices, eod_props, \ mean_eods, spec_data, peak_data, power_thresh, skip_reason, zoom_window = \ detect_eods(data, samplerate, min_clip, max_clip, filename, mode, verbose, plot_level, cfg) if not found_bestwindow: wave_eodfs = [] wave_indices = [] eod_props = [] mean_eods = [] # add analysis window to EOD properties: for props in eod_props: props['twin'] = idx0/samplerate props['window'] = (idx1 - idx0)/samplerate props['winclipped'] = clipped # warning message in case no fish has been found: if found_bestwindow and not eod_props : msg = ', '.join(skip_reason) if msg: print(filename + ': no fish found: %s' % msg) else: print(filename + ': no fish found.') # file name for output files: output_basename = os.path.join(output_folder, outfilename) if channels > 1: if channels > 100: output_basename += '-c%03d' % chan elif channels > 10: output_basename += '-c%02d' % chan else: output_basename += '-c%d' % chan if time_file: output_basename += '-t%.0fs' % (idx0/samplerate) # make directory if necessary: if keep_path and found_bestwindow: outpath = os.path.dirname(output_basename) if not os.path.exists(outpath): if verbose > 0: print('mkdir %s' % outpath) os.makedirs(outpath) # save results to files: if save_data: remove_eod_files(output_basename, verbose, cfg) if found_bestwindow: save_analysis(output_basename, zip_file, eod_props, mean_eods, spec_data, peak_data, wave_eodfs, wave_indices, unit, verbose, **write_table_args(cfg)) # summary plots: if save_plot or not save_data: n_snippets = 10 if len(save_subplots) == 0 or 'd' in save_subplots: chl = chan if channels > 1 else None fig = plot_eods(outfilename, messagefilename, raw_data, samplerate, chl, idx0, idx1, clipped, psd_data[0], wave_eodfs, wave_indices, mean_eods, eod_props, peak_data, spec_data, None, unit, zoom_window, n_snippets, power_thresh, True, all_eods, spec_plots, skip_bad, log_freq, min_freq, max_freq, interactive=not save_plot, verbose=verbose) if save_plot: if multi_pdf is not None: multi_pdf.savefig(fig) else: fig.savefig(output_basename + '.pdf') else: fig.canvas.manager.set_window_title('thunderfish') plt.show() plt.close() save_subplots = save_subplots.replace('d', '') if len(save_subplots) > 0: plot_eod_subplots(output_basename, save_subplots, raw_data, samplerate, idx0, idx1, clipped, psd_data[0], wave_eodfs, wave_indices, mean_eods, eod_props, peak_data, spec_data, unit, zoom_window, n_snippets, power_thresh, True, skip_bad, log_freq, min_freq, max_freq, save_plot) return None
def run_thunderfish(file_args)
-
Helper function for mutlithreading Pool().map().
Expand source code
def run_thunderfish(file_args): """Helper function for mutlithreading Pool().map(). """ results = file_args[1][0] verbose = file_args[1][-1] if results else file_args[1][-2]+1 if verbose > 1: print('='*70) try: if results: thunderfish_plot(file_args[0], *file_args[1][1:]) else: if verbose > 0: print('analyze recording %s ...' % file_args[0]) msg = thunderfish(file_args[0], *file_args[1][1:]) if msg: print(msg) except (KeyboardInterrupt, SystemExit): print('\nthunderfish interrupted by user... exit now.') sys.exit(0) except: print(traceback.format_exc())
def main(cargs=None)
-
Expand source code
def main(cargs=None): # config file name: cfgfile = __package__ + '.cfg' # command line arguments: if cargs is None: cargs = sys.argv[1:] parser = argparse.ArgumentParser(add_help=False, description='Analyze EOD waveforms of weakly electric fish.', epilog='version %s by Benda-Lab (2015-%s)' % (__version__, __year__)) parser.add_argument('-h', '--help', action='store_true', help='show this help message and exit') parser.add_argument('--version', action='version', version=__version__) parser.add_argument('-v', action='count', dest='verbose', default=0, help='verbosity level. Increase by specifying -v multiple times, or like -vvv') parser.add_argument('-V', action='count', dest='plot_level', default=0, help='level for debugging plots. Increase by specifying -V multiple times, or like -VVV') parser.add_argument('-c', dest='save_config', action='store_true', help='save configuration to file {0} after reading all configuration files'.format(cfgfile)) parser.add_argument('--channel', default=0, type=int, help='channel to be analyzed (defaults to first channel, negative channel selects all channels)') parser.add_argument('-t', dest='time', default=None, type=str, metavar='TIME', help='start time of analysis window in recording: "beginning", "center", "end", "best", or time in seconds (overwrites "windowPosition" in cofiguration file)') parser.add_argument('-u', dest='unwrap', action='store_true', help='unwrap clipped files, toggles unwrap setting of config file.') parser.add_argument('-T', dest='time_file', action='store_true', help='add start time of analysis file to output file names') parser.add_argument('-m', dest='mode', default='wp', type=str, choices=['w', 'p', 'wp'], help='extract wave "w" and/or pulse "p" fish EODs') parser.add_argument('-a', dest='all_eods', action='store_true', help='show all EOD waveforms in the summary plot') parser.add_argument('-S', dest='spec_plots', action='store_true', help='plot spectra for all EOD waveforms in the summary plot') parser.add_argument('-b', dest='skip_bad', action='store_false', help='indicate bad EODs in legend of power spectrum') parser.add_argument('-l', dest='log_freq', type=float, metavar='MINFREQ', nargs='?', const=100.0, default=0.0, help='logarithmic frequency axis in power spectrum with optional minimum frequency (defaults to 100 Hz)') parser.add_argument('-p', dest='save_plot', action='store_true', help='save output plots as pdf files') parser.add_argument('-M', dest='multi_pdf', default='', type=str, metavar='PDFFILE', help='save all summary plots of all recordings in a multi page pdf file. Disables parallel jobs.') parser.add_argument('-P', dest='save_subplots', default='', type=str, metavar='rtpwsed', 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') parser.add_argument('-d', dest='rawdata_path', default='.', type=str, metavar='PATH', help='path to raw EOD recordings needed for plotting based on analysis results') parser.add_argument('-j', dest='jobs', nargs='?', type=int, default=None, const=0, help='number of jobs run in parallel. Without argument use all CPU cores.') parser.add_argument('-s', dest='save_data', action='store_true', help='save analysis results to files') parser.add_argument('-z', dest='zip_file', action='store_true', help='save analysis results in a single zip file') parser.add_argument('-f', dest='format', default='auto', type=str, choices=TableData.formats + ['py'], help='file format used for saving analysis results, defaults to the format specified in the configuration file or "csv"') parser.add_argument('-o', dest='outpath', default='.', type=str, help='path where to store results and figures (defaults to current working directory)') parser.add_argument('-k', dest='keep_path', action='store_true', help='keep path of input file when saving analysis files, i.e. append path of input file to OUTPATH') parser.add_argument('-i', dest='load_kwargs', default=[], action='append', metavar='KWARGS', help='key-word arguments for the data loader function') parser.add_argument('file', nargs='*', default='', type=str, help='name of a file with time series data of an EOD recording, may include wildcards') args = parser.parse_args(cargs) # help: if args.help: parser.print_help() print('') print('examples:') print('- analyze the single file data.wav interactively:') print(' > thunderfish data.wav') print('- extract wavefish only:') print(' > thunderfish -m w data.wav') print('- automatically analyze all wav files in the current working directory and save analysis results and plot to files:') print(' > thunderfish -s -p *.wav') print('- analyze all wav files in the river1/ directory, use all CPUs, and write files directly to "results/":') print(' > thunderfish -j -s -p -o results/ river1/*.wav') print('- analyze all wav files in the river1/ directory and write files to "results/river1/":') print(' > thunderfish -s -p -o results/ -k river1/*.wav') print('- write configuration file:') print(' > thunderfish -c') parser.exit() # set verbosity level from command line: verbose = args.verbose plot_level = args.plot_level if verbose < plot_level: verbose = plot_level # interactive plot: plt.rcParams['keymap.quit'] = 'ctrl+w, alt+q, q' plt.ioff() # expand wildcard patterns: files = [] if os.name == 'nt': for fn in args.file: files.extend(glob.glob(fn)) else: files = [f for f in args.file if '-message' not in f] # save configuration: if args.save_config: file_name = files[0] if len(files) else '' cfg = configuration() cfg.load_files(cfgfile, file_name, 4, verbose) save_configuration(cfg, cfgfile) exit() elif len(files) == 0: parser.error('you need to specify at least one file for the analysis') # configure: cfg = configuration() cfg.load_files(cfgfile, files[0], 4, verbose) if args.format != 'auto': cfg.set('fileFormat', args.format) if args.unwrap: cfg.set('unwrapData', not cfg.value('unwrapData')) # plot parameter: spec_plots = 'auto' if args.spec_plots: spec_plots = True # multi-page pdfs: multi_pdf = None if len(args.multi_pdf) > 0: args.save_plot = True args.jobs = None # PdfPages does not work yet with mutliprocessing ext = os.path.splitext(args.multi_pdf)[1] if ext != os.extsep + 'pdf': args.multi_pdf += os.extsep + 'pdf' multi_pdf = PdfPages(args.multi_pdf) # create output folder: if args.save_data or args.save_plot: if not os.path.exists(args.outpath): if verbose > 1: print('mkdir %s' % args.outpath) os.makedirs(args.outpath) # kwargs for data loader: load_kwargs = {} for s in args.load_kwargs: for kw in s.split(','): kws = kw.split(':') if len(kws) == 2: load_kwargs[kws[0].strip()] = kws[1].strip() # frequency limits for power spectrum: min_freq = 0.0 max_freq = 3000.0 log_freq = args.log_freq if log_freq > 0.0: min_freq = log_freq max_freq = min_freq*20 if max_freq < 2000: max_freq = 2000 log_freq = True else: log_freq = False # check if all input files are results: exts = TableData.ext_formats.values() results = True # check and group by recording: result_files = [] for f in sorted(files): _, base_name, _, _, ftype, _, ext = parse_filename(f) if ext == 'zip' or (ext in exts and ftype in file_types): if len(result_files) == 0 or \ not result_files[-1][-1].startswith(base_name): result_files.append([f]) else: result_files[-1].append(f) else: results = False break if results: files = result_files # adjust verbosity: v = verbose if len(files) > 1: v += 1 # run on pool: pool_args = (results, load_kwargs, cfg, args.channel, args.time, args.time_file, args.mode, log_freq, min_freq, max_freq, args.save_data, args.zip_file, args.all_eods, spec_plots, args.skip_bad, args.save_plot, multi_pdf, args.save_subplots, args.outpath, args.keep_path, v-1, plot_level) if results: pool_args = (results, args.rawdata_path, load_kwargs, args.all_eods, spec_plots, args.skip_bad, args.save_plot, multi_pdf, args.save_subplots, log_freq, min_freq, max_freq, args.outpath, args.keep_path, v) if args.jobs is not None and (args.save_data or args.save_plot) and len(files) > 1: cpus = cpu_count() if args.jobs == 0 else args.jobs if verbose > 1: print('run on %d cpus' % cpus) p = Pool(cpus) p.map(run_thunderfish, zip(files, [pool_args]*len(files))) else: list(map(run_thunderfish, zip(files, [pool_args]*len(files)))) if multi_pdf is not None: multi_pdf.close()