Module thunderfish.thunderlogger
thunderlogger
Detect segments of interest in large data files and extract EOD waveforms.
Functions
def add_thunderlogger_config(cfg,
detection_thresh='auto',
default_thresh=0.002,
thresh_fac=3.0,
thresh_nbins=500)-
Expand source code
def add_thunderlogger_config(cfg, detection_thresh='auto', default_thresh=0.002, thresh_fac=3.0, thresh_nbins=500): """Add parameters needed for by thunderlogger. Parameters ---------- cfg: ConfigFile The configuration. detection_thresh: float or 'auto' Only data segments with standard deviation larger than this value are analyzed for EODs. If set to 'auto' a threshold is computed from all the data segments of a recording channel. default_thresh: float Threshold that is used if "detection_thresh" is set to "auto" and no data are available. thresh_fac: float The threshold for analysing data segments is set to the mean of the most-likely standard deviations plus this factor times the corresponding standard deviation. thresh_nbins: int The number of bins used to compute a histogram of the standard deviations of the data segments, from which the mean and standard deviation are estimated for automatically computing a threshold. """ cfg.add_section('Thunderlogger:') cfg.add('detectionThreshold', detection_thresh, '', 'Only analyse data segements with a standard deviation that is larger than this threshold. If set to "auto" compute threshold from all the standard deviations of a recording channel.') cfg.add('detectionThresholdDefault', default_thresh, '', 'Threshold that is used if "detectionThreshold" is set to "auto" and no data are available.') cfg.add('detectionThresholdStdFac', thresh_fac, '', 'An automatically computed threshold for analysing data segments is set to the mean of the most-likely standard deviations plus this factor times the corresponding standard deviation.') cfg.add('detectionThresholdNBins', thresh_nbins, '', 'The number of bins used to compute a histogram of the standard deviations of the data segments, from which the mean and standard deviation are estimated for automatically computing a threshold.') cfg.add('startTime', 'none', '', 'Provide a start time for the recordings overwriting the meta data of the data files (YYYY-mm-ddTHH:MM:SS format).')
Add parameters needed for by thunderlogger.
Parameters
cfg
:ConfigFile
- The configuration.
detection_thresh
:float
or'auto'
- Only data segments with standard deviation larger than this value are analyzed for EODs. If set to 'auto' a threshold is computed from all the data segments of a recording channel.
default_thresh
:float
- Threshold that is used if "detection_thresh" is set to "auto" and no data are available.
thresh_fac
:float
- The threshold for analysing data segments is set to the mean of the most-likely standard deviations plus this factor times the corresponding standard deviation.
thresh_nbins
:int
- The number of bins used to compute a histogram of the standard deviations of the data segments, from which the mean and standard deviation are estimated for automatically computing a threshold.
def extract_eods(files,
thresholds,
stds_only,
cfg,
verbose,
plot_level,
thresh=0.002,
max_deltaf=1.0,
max_dist=5e-05,
deltat_max=datetime.timedelta(seconds=300),
start_time=None)-
Expand source code
def extract_eods(files, thresholds, stds_only, cfg, verbose, plot_level, thresh=0.002, max_deltaf=1.0, max_dist=0.00005, deltat_max=dt.timedelta(minutes=5), start_time=None): t0s = [] stds = None supra_thresh = None wave_fishes = None pulse_fishes = None if start_time is None: # XXX we should read this from the meta data: filename = os.path.splitext(os.path.basename(files[0]))[0] times = filename.split('-')[1] start_time = dt.datetime.strptime(times, '%Y%m%dT%H%M%S') toffs = start_time t1 = start_time unit = None for file in files: try: with DataLoader(file) as sf: # analyze: sys.stdout.write(file + ': ') unit = sf.unit if max_dist < 1.1/sf.rate: max_dist = 1.1/sf.rate window_size = cfg.value('windowSize') ndata = int(window_size * sf.rate) step = ndata//2 b, a = butter(1, 10.0, 'hp', fs=sf.rate, output='ba') if stds is None: stds = [[] for c in range(sf.channels)] supra_thresh = [[] for c in range(sf.channels)] if wave_fishes is None: wave_fishes = [[] for c in range(sf.channels)] if pulse_fishes is None: pulse_fishes = [[] for c in range(sf.channels)] for k, data in enumerate(sf.blocks(ndata, step)): sys.stdout.write('.') sys.stdout.flush() t0 = toffs + dt.timedelta(seconds=k*step/sf.rate) t1 = t0 + dt.timedelta(seconds=ndata/sf.rate) t0s.append(t0) for channel in range(sf.channels): if thresholds: thresh = thresholds[channel] fdata = lfilter(b, a, data[:,channel] - np.mean(data[:ndata//20,channel])) sd = np.std(fdata) stds[channel].append(sd) supra_thresh[channel].append(1 if sd > thresh else 0) if stds_only: continue if sd > thresh: # clipping: min_clip = cfg.value('minClipAmplitude') if min_clip == 0.0: min_clip = cfg.value('minDataAmplitude') max_clip = cfg.value('maxClipAmplitude') if max_clip == 0.0: max_clip = cfg.value('maxDataAmplitude') name = file # detect EODs in the data: _, _, _, eod_props, mean_eods, spec_data, peak_data, _, _, _ = \ detect_eods(data[:,channel], sf.rate, min_clip, max_clip, name, verbose, plot_level, cfg) first_fish = True for props, eod, spec, peaks in zip(eod_props, mean_eods, spec_data, peak_data): fish = None fish_deltaf = 100000.0 if props['type'] == 'wave': for wfish in wave_fishes[channel]: deltaf = np.abs(wfish.props['EODf'] - props['EODf']) if deltaf < fish_deltaf: fish_deltaf = deltaf fish = wfish if fish_deltaf > max_deltaf: fish = None peaks = None else: fish_dist = 10000.0 for pfish in pulse_fishes[channel]: ddist = np.abs(pfish.props['P1-P1-dist'] - props['P1-P1-dist']) if ddist < fish_dist: fish_dist = ddist fish_deltaf = np.abs(pfish.props['EODf'] - props['EODf']) fish = pfish if fish_dist > max_dist or \ fish_deltaf > max_deltaf: fish = None spec = None if fish is not None and \ t0 - fish.times[-1][1] < deltat_max: if fish.times[-1][1] >= t0 and \ np.abs(fish.times[-1][2] - props['EODf']) < 0.5 and \ fish.times[-1][3] == channel and \ fish.times[-1][4] == file: fish.times[-1][1] = t1 else: fish.times.append([t0, t1, props['EODf'], channel, file]) if props['p-p-amplitude'] > fish.props['p-p-amplitude']: fish.props = props fish.waveform = eod fish.spec = spec fish.peaks = peaks else: new_fish = SimpleNamespace(props=props, waveform=eod, spec=spec, peaks=peaks, times=[[t0, t1, props['EODf'], channel, file]]) if props['type'] == 'pulse': pulse_fishes[channel].append(new_fish) else: wave_fishes[channel].append(new_fish) if first_fish: sys.stdout.write('\n ') first_fish = False sys.stdout.write('%6.1fHz %5s-fish @ %s\n ' % (props['EODf'], props['type'], t0.strftime('%Y-%m-%dT%H:%M:%S'))) toffs += dt.timedelta(seconds=len(sf)/sf.rate) sys.stdout.write('\n') sys.stdout.flush() except EOFError as error: # XXX we need to update toffs by means of the metadata of the next file! sys.stdout.write(file + ': ' + str(error) + '\n') if pulse_fishes is not None and len(pulse_fishes) > 0: pulse_fishes = [[pulse_fishes[c][i] for i in np.argsort([fish.props['EODf'] for fish in pulse_fishes[c]])] for c in range(len(pulse_fishes))] if wave_fishes is not None and len(wave_fishes) > 0: wave_fishes = [[wave_fishes[c][i] for i in np.argsort([fish.props['EODf'] for fish in wave_fishes[c]])] for c in range(len(wave_fishes))] return pulse_fishes, wave_fishes, start_time, toffs, t0s, stds, supra_thresh, unit
def save_times(times, idx, output_basename, name, **kwargs)
-
Expand source code
def save_times(times, idx, output_basename, name, **kwargs): td = TableData() td.append('index', '', '%d', [idx] * len(times)) td.append('tstart', '', '%s', [t[0].strftime('%Y-%m-%dT%H:%M:%S') for t in times]) td.append('tend', '', '%s', [t[1].strftime('%Y-%m-%dT%H:%M:%S') for t in times]) if len(times[0]) > 2: td.append('EODf', 'Hz', '%.1f', [t[2] for t in times]) td.append('device', '', '%s', [name for t in times]) if len(times[0]) > 2: td.append('channel', '', '%d', [t[3] for t in times]) td.append('file', '', '%s', [t[4] for t in times]) fp = output_basename + '-times' if idx is not None: fp += '-%d' % idx td.write(fp, **kwargs)
def load_times(file_path)
-
Expand source code
def load_times(file_path): data = TableData(file_path).data_frame() data['index'] = data['index'].astype(int) data['tstart'] = pd.to_datetime(data['tstart']) data['tstart'] = pd.Series(data['tstart'].dt.to_pydatetime(), dtype=object) data['tend'] = pd.to_datetime(data['tend']) data['tend'] = pd.Series(data['tend'].dt.to_pydatetime(), dtype=object) if 'channel' in data: data['channel'] = data['channel'].astype(int) return data
def save_power(times, stds, supra_thresh, unit, output_basename, **kwargs)
-
Expand source code
def save_power(times, stds, supra_thresh, unit, output_basename, **kwargs): td = TableData() td.append('index', '', '%d', list(range(len(times)))) td.append('time', '', '%s', [t.strftime('%Y-%m-%dT%H:%M:%S') for t in times]) for c, (std, thresh) in enumerate(zip(stds, supra_thresh)): td.append('channel%d'%c, unit, '%g', std) td.append('thresh%d'%c, '', '%d', thresh) fp = output_basename + '-stdevs' td.write(fp, **kwargs)
def load_power(file_path, start_time=None)
-
Expand source code
def load_power(file_path, start_time=None): base = os.path.basename(file_path) device = base[0:base.find('-stdevs')] data = TableData(file_path) times = [] for row in range(data.rows()): times.append(dt.datetime.strptime(data[row,'time'], '%Y-%m-%dT%H:%M:%S')) if start_time is not None: deltat = start_time - times[0] for k in range(len(times)): times[k] += deltat channels = (data.columns()-2)//2 stds = np.zeros((len(times), channels)) supra_thresh = np.zeros((len(times), channels), dtype=int) for c in range(channels): stds[:,c] = data[:,'channel%d'%c] supra_thresh[:,c] = data[:,'thresh%d'%c] return np.array(times), stds, supra_thresh, device
def save_data(output_folder,
name,
pulse_fishes,
wave_fishes,
tstart,
tend,
t0s,
stds,
supra_thresh,
unit,
cfg)-
Expand source code
def save_data(output_folder, name, pulse_fishes, wave_fishes, tstart, tend, t0s, stds, supra_thresh, unit, cfg): output_basename = os.path.join(output_folder, name) if pulse_fishes is not None: for c in range(len(pulse_fishes)): out_path = output_basename + '-c%d' % c idx = 0 # pulse fish: pulse_props = [] for fish in pulse_fishes[c]: save_eod_waveform(fish.waveform, unit, idx, out_path, **write_table_args(cfg)) if fish.peaks is not None: save_pulse_peaks(fish.peaks, unit, idx, out_path, **write_table_args(cfg)) save_times(fish.times, idx, out_path, name, **write_table_args(cfg)) pulse_props.append(fish.props) pulse_props[-1]['index'] = idx idx += 1 save_pulse_fish(pulse_props, unit, out_path, **write_table_args(cfg)) # wave fish: wave_props = [] if wave_fishes is not None: for fish in wave_fishes[c]: save_eod_waveform(fish.waveform, unit, idx, out_path, **write_table_args(cfg)) if fish.spec is not None: save_wave_spectrum(fish.spec, unit, idx, out_path, **write_table_args(cfg)) save_times(fish.times, idx, out_path, name, **write_table_args(cfg)) wave_props.append(fish.props) wave_props[-1]['index'] = idx idx += 1 save_wave_fish(wave_props, unit, out_path, **write_table_args(cfg)) # recording time window: save_times([(tstart, tend)], None, output_basename, name, **write_table_args(cfg)) # signal power: if stds is not None and len(stds) > 0: save_power(t0s, stds, supra_thresh, unit, output_basename, **write_table_args(cfg))
def load_data(files, start_time=None)
-
Expand source code
def load_data(files, start_time=None): all_files = [] for file in files: if os.path.isdir(file): all_files.extend(glob.glob(os.path.join(file, '*fish*'))) else: all_files.append(file) pulse_fishes = [] wave_fishes = [] channels = set() for file in all_files: if 'pulse' in os.path.basename(file): pulse_props = load_pulse_fish(file) base_file, ext = os.path.splitext(file) base_file = base_file[:base_file.rfind('pulse')] count = 0 for props in pulse_props: idx = props['index'] waveform, unit = \ load_eod_waveform(base_file + 'eodwaveform-%d'%idx + ext) times = load_times(base_file + 'times-%d'%idx + ext) times['index'] = idx fish = SimpleNamespace(props=props, waveform=waveform, unit=unit, times=times) for i, t in times.iterrows(): channels.add((t['device'], t['channel'])) try: peaks, unit = \ load_pulse_peaks(base_file + 'pulsepeaks-%d'%idx + ext) fish.peaks = peaks except FileNotFoundError: fish.peaks = None pulse_fishes.append(fish) count += 1 #if count > 300: # XXX REMOVE # break elif 'wave' in os.path.basename(file): wave_props = load_wave_fish(file) base_file, ext = os.path.splitext(file) base_file = base_file[:base_file.rfind('wave')] count = 0 for props in wave_props: idx = props['index'] waveform, unit = \ load_eod_waveform(base_file + 'eodwaveform-%d'%idx + ext) times = load_times(base_file + 'times-%d'%idx + ext) times['index'] = idx fish = SimpleNamespace(props=props, waveform=waveform, unit=unit, times=times) for i, t in times.iterrows(): channels.add((t['device'], t['channel'])) try: spec, unit = \ load_wave_spectrum(base_file + 'wavespectrum-%d'%idx + ext) fish.spec = spec except FileNotFoundError: fish.spec = None wave_fishes.append(fish) count += 1 #if count > 300: # XXX REMOVE # break base_file = base_file[:base_file.rfind('-c')+1] times = load_times(base_file + 'times' + ext) tstart = times.tstart[0] tend = times.tend[0] if start_time is not None: deltat = start_time - tstart tstart += deltat tend += deltat for fish in pulse_fishes + wave_fishes: fish.times.tstart += deltat fish.times.tend += deltat return pulse_fishes, wave_fishes, tstart, tend, sorted(channels)
def plot_signal_power(times, stds, supra_threshs, devices, thresholds, title, output_folder)
-
Expand source code
def plot_signal_power(times, stds, supra_threshs, devices, thresholds, title, output_folder): plt.rcParams['axes.xmargin'] = 0 plt.rcParams['axes.ymargin'] = 0 n = 0 for s in stds: n += s.shape[1] h = n*4.0 t = 0.8 if title else 0.1 fig, axs = plt.subplots(n, 1, figsize=(16/2.54, h/2.54), sharex=True, sharey=True) fig.subplots_adjust(left=0.1, right=0.99, top=1-t/h, bottom=1.6/h, hspace=0) i = 0 for time, cstds, threshs, device in zip(times, stds, supra_threshs, devices): for c, (std, thresh) in enumerate(zip(cstds.T, threshs.T)): ax = axs[i] ax.plot(time, std) if thresholds: ax.axhline(thresholds[i], color='k', lw=0.5) elif len(std[thresh<1]) > 0: thresh = np.max(std[thresh<1]) ax.axhline(thresh, color='k', lw=0.5) #stdm = np.ma.masked_where(thresh < 1, std) #ax.plot(time, stdm) ax.set_yscale('log') #ax.set_ylim(bottom=0) ax.set_ylabel('%s-c%d' % (device, c)) i += 1 if title: axs[0].set_title(title) axs[-1].xaxis.set_major_formatter(mdates.DateFormatter('%b %d %Hh')) plt.setp(axs[-1].get_xticklabels(), ha='right', rotation=30, rotation_mode='anchor') fig.savefig(os.path.join(output_folder, 'signalpowers.pdf')) plt.show()
def merge_fish(pulse_fishes,
wave_fishes,
max_noise=0.1,
max_deltaf=10.0,
max_dist=0.0002,
max_rms=0.3)-
Expand source code
def merge_fish(pulse_fishes, wave_fishes, max_noise=0.1, max_deltaf=10.0, max_dist=0.0002, max_rms=0.3): pulse_eods = [] for i in np.argsort([fish.props['P2-P1-dist'] for fish in pulse_fishes]): if pulse_fishes[i].props['noise'] > max_noise: continue if pulse_fishes[i].props['P2-P1-dist'] <= 0.0: continue if len(pulse_eods) > 0 and \ np.abs(pulse_fishes[i].props['P2-P1-dist'] - pulse_eods[-1].props['P2-P1-dist']) <= max_dist and \ pulse_similarity(pulse_fishes[i].waveform, pulse_eods[-1].waveform, 4) < max_rms: pulse_eods[-1].times.append(pulse_fishes[i].times) if not hasattr(pulse_eods[-1], 'othereods'): pulse_eods[-1].othereods = [pulse_eods[-1].waveform] pulse_eods[-1].othereods.append(pulse_fishes[i].waveform) if pulse_fishes[i].props['p-p-amplitude'] > pulse_eods[-1].props['p-p-amplitude']: pulse_eods[-1].waveform = pulse_fishes[i].waveform pulse_eods[-1].props = pulse_fishes[i].props continue pulse_eods.append(pulse_fishes[i]) pulse_eods = [pulse_eods[i] for i in np.argsort([fish.props['EODf'] for fish in pulse_eods])] wave_eods = [] for i in np.argsort([fish.props['EODf'] for fish in wave_fishes]): if wave_fishes[i].props['noise'] > max_noise: continue if len(wave_eods) > 0 and \ np.abs(wave_fishes[i].props['EODf'] - wave_eods[-1].props['EODf']) < max_deltaf and \ wave_similarity(wave_fishes[i].waveform, wave_eods[-1].waveform, wave_fishes[i].props['EODf'], wave_eods[-1].props['EODf']) < max_rms: wave_eods[-1].times.append(wave_fishes[i].times) if not hasattr(wave_eods[-1], 'othereods'): wave_eods[-1].othereods = [] wave_eods[-1].othereods.append(wave_fishes[i].waveform) if wave_fishes[i].props['p-p-amplitude'] > wave_eods[-1].props['p-p-amplitude']: wave_eods[-1].waveform = wave_fishes[i].waveform wave_eods[-1].props = wave_fishes[i].props continue wave_eods.append(wave_fishes[i]) return pulse_eods, wave_eods
def plot_eod_occurances(pulse_fishes, wave_fishes, tstart, tend, channels, output_folder)
-
Expand source code
def plot_eod_occurances(pulse_fishes, wave_fishes, tstart, tend, channels, output_folder): channel_colors = ['#2060A7', '#40A787', '#478010', '#F0D730', '#C02717', '#873770', '#008797', '#007030', '#AAB71B', '#F78017', '#D03050', '#53379B'] plt.rcParams['axes.facecolor'] = 'none' plt.rcParams['axes.xmargin'] = 0 plt.rcParams['axes.ymargin'] = 0.05 plt.rcParams['font.family'] = 'sans-serif' n = len(pulse_fishes) + len(wave_fishes) h = n*2.5 + 2.5 + 0.3 fig, axs = plt.subplots(n, 2, squeeze=False, figsize=(16/2.54, h/2.54), gridspec_kw=dict(width_ratios=(1,2))) fig.subplots_adjust(left=0.02, right=0.97, top=1-0.3/h, bottom=2.2/h, hspace=0.2) pi = 0 prev_xscale = 0.0 for ax, fish in zip(axs, pulse_fishes + wave_fishes): # EOD waveform: ax[0].spines['left'].set_visible(False) ax[0].spines['right'].set_visible(False) ax[0].spines['top'].set_visible(False) ax[0].spines['bottom'].set_visible(False) ax[0].xaxis.set_visible(False) ax[0].yaxis.set_visible(False) time = 1000.0 * fish.waveform[:,0] #ax[0].plot([time[0], time[-1]], [0.0, 0.0], # zorder=-10, lw=1, color='#AAAAAA') if hasattr(fish, 'othereods'): for eod in fish.othereods: ax[0].plot(1000.0*eod[:,0], eod[:,1], zorder=5, lw=1, color='#AAAAAA') ax[0].plot(time, fish.waveform[:,1], zorder=10, lw=2, color='#C02717') ax[0].text(0.0, 1.0, '%.0f\u2009Hz' % fish.props['EODf'], transform=ax[0].transAxes, va='baseline', zorder=20) if fish.props['type'] == 'wave': lim = 750.0/fish.props['EODf'] ax[0].set_xlim([-lim, +lim]) tmax = lim else: ax[0].set_xlim(time[0], time[-1]) tmax = time[-1] xscale = 1.0 if tmax < 1.0: xscale = 0.5 elif tmax > 10.0: xscale = 5.0 ymin = np.min(fish.waveform[:,1]) ymax = np.max(fish.waveform[:,1]) ax[0].plot((tmax-xscale, tmax), (ymin - 0.04*(ymax-ymin),)*2, 'k', lw=3, clip_on=False, zorder=0) if ax[0] is axs[-1,0] or xscale != prev_xscale: if xscale < 1.0: ax[0].text(tmax-0.5*xscale, ymin - 0.1*(ymax-ymin), '%.0f\u2009\u00b5s' % (1000.0*xscale), ha='center', va='top', zorder=0) else: ax[0].text(tmax-0.5*xscale, ymin - 0.1*(ymax-ymin), '%.0f\u2009ms' % xscale, ha='center', va='top', zorder=0) prev_xscale = xscale # time bar: ax[1].xaxis.set_major_formatter(mdates.DateFormatter('%b %d %Hh')) min_eodf = 10000 max_eodf = 0 for index, time in fish.times.iterrows(): if time['EODf'] < min_eodf: min_eodf = time['EODf'] if time['EODf'] > max_eodf: max_eodf = time['EODf'] ax[1].plot([time['tstart'], time['tend']], [time['EODf'], time['EODf']], lw=5, color=channel_colors[channels.index((time['device'], time['channel']))%len(channel_colors)]) if max_eodf > min_eodf + 10.0: ax[1].text(0.0, 1.0, '%.0f \u2013 %.0f\u2009Hz' % (min_eodf, max_eodf), transform=ax[1].transAxes, va='baseline', zorder=20) ax[1].set_xlim(tstart, tend) ax[1].spines['left'].set_visible(False) ax[1].spines['right'].set_visible(False) ax[1].yaxis.set_visible(False) ax[1].spines['top'].set_visible(False) if ax[1] is not axs[-1,1]: ax[1].spines['bottom'].set_visible(False) ax[1].xaxis.set_visible(False) else: plt.setp(ax[1].get_xticklabels(), ha='right', rotation=30, rotation_mode='anchor') fig.savefig(os.path.join(output_folder, 'eodwaveforms.pdf'))
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='Extract EOD waveforms of weakly electric fish from logger data.', 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('-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('-p', dest='save_plot', action='store_true', help='plot analyzed data') parser.add_argument('-m', dest='merge', action='store_true', help='merge similar EODs before plotting') parser.add_argument('-s', dest='stds_only', action='store_true', help='analyze or plot standard deviation of data only') 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('-n', dest='name', default='', type=str, help='base name of all output files or title of plots') 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('- write configuration file:') print(' > thunderlogger -c') print('- compute standard deviations of data segments:') print(' > thunderlogger -o results -k -n logger1 -s river1/logger1-*.wav') print('- plot the standard deviations and the computed threshold:') print(' > thunderlogger -o plots -k -s -p -n river1 results/river1/logger1-stdevs.*') print(' you may adapt the settings in the configureation file "thunderfish.cfg"') print('- extract EODs from the data:') print(' > thunderlogger -o results -k river1/logger1-*.wav') print('- merge and plot extracted EODs:') print(' > thunderlogger -o plots -k -p -m results/river*/*fish.*') parser.exit() # set verbosity level from command line: verbose = args.verbose plot_level = args.plot_level if verbose < plot_level: verbose = plot_level # expand wildcard patterns: files = [] if os.name == 'nt': for fn in args.file: files.extend(glob.glob(fn)) else: files = args.file if args.save_config: # save configuration: file_name = files[0] if len(files) else '' cfg = configuration() add_thunderlogger_config(cfg) 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() add_thunderlogger_config(cfg) cfg.load_files(cfgfile, files[0], 4, verbose-1) if args.format != 'auto': cfg.set('fileFormat', args.format) # create output folder for data and plots: output_folder = args.outpath if args.keep_path: output_folder = os.path.join(output_folder, os.path.split(files[0])[0]) if not os.path.exists(output_folder): if verbose > 1: print('mkdir %s' % output_folder) os.makedirs(output_folder) # start time: start_time = None if cfg.value('startTime') != 'none': cfg.value('startTime') start_time = dt.datetime.strptime(cfg.value('startTime'), '%Y-%m-%dT%H:%M:%S') # analyze and save data: plt.ioff() if not args.save_plot: # assemble device name and output file: if len(args.name) > 0: device_name = args.name else: device_name = os.path.basename(files[0]) device_name = device_name[:device_name.find('-')] output_basename = os.path.join(output_folder, device_name) # compute thresholds: thresholds = [] power_file = output_basename + '-stdevs.csv' thresh = cfg.value('detectionThreshold') if thresh == 'auto': thresh = cfg.value('detectionThresholdDefault') if os.path.isfile(power_file): _, powers, _, _ = load_power(power_file) for std in powers.T: ss, cc = hist_threshold(std, thresh_fac=cfg.value('detectionThresholdStdFac'), nbins=cfg.value('detectionThresholdNBins')) thresholds.append(cc + ss) else: thresh = float(thresh) pulse_fishes, wave_fishes, tstart, tend, t0s, \ stds, supra_thresh, unit = \ extract_eods(files, thresholds, args.stds_only, cfg, verbose, plot_level, thresh=thresh, start_time=start_time) remove_eod_files(output_basename, verbose, cfg) save_data(output_folder, device_name, pulse_fishes, wave_fishes, tstart, tend, t0s, stds, supra_thresh, unit, cfg) sys.stdout.write('DONE!\n') if args.stds_only: sys.stdout.write('Signal powers saved in %s\n' % (output_folder+'-stdevs.csv')) sys.stdout.write('To generate plots run thunderlogger with the -p and -s flags on the generated file:\n') sys.stdout.write('> thunderlogger -p -s %s\n' % (output_folder+'-stdevs.csv')) else: sys.stdout.write('Extracted EOD waveforms saved in %s\n' % output_folder) sys.stdout.write('To generate plots run thunderlogger with the -p flag on the generated files:\n') sys.stdout.write('> thunderlogger -p -o %s%s %s\n' % (args.outpath, ' -k' if args.keep_path else '', output_basename)) else: if args.stds_only: times = [] stds = [] supra_threshs = [] devices = [] thresholds = [] for file in files: t, p, s, d = load_power(file, start_time) times.append(t) stds.append(p) supra_threshs.append(s) devices.append(d) # compute detection thresholds: for std in p.T: if cfg.value('detectionThreshold') == 'auto': ss, cc = hist_threshold(std, thresh_fac=cfg.value('detectionThresholdStdFac'), nbins=cfg.value('detectionThresholdNBins')) thresholds.append(cc + ss) else: thresholds.append(float(cfg.value('detectionThreshold'))) plot_signal_power(times, stds, supra_threshs, devices, thresholds, args.name, output_folder) else: pulse_fishes, wave_fishes, tstart, tend, channels = \ load_data(files, start_time) if args.merge: pulse_fishes, wave_fishes = merge_fish(pulse_fishes, wave_fishes) plot_eod_occurances(pulse_fishes, wave_fishes, tstart, tend, channels, output_folder)