Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add quick_launch function for easier scripting and interactive sessions #191

Merged
merged 2 commits into from
Feb 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ API Reference Guide
In addition to using **neurotic** as a standalone app, you can also leverage
its API in your own code.

.. note::

**TL;DR**: The easiest way to use **neurotic** in an interactive session or
script is by invoking :func:`neurotic.quick_launch()
<neurotic.scripts.quick_launch>`. For example:

>>> import neurotic
>>> metadata = {'data_file': 'data.axgx'}
>>> neurotic.quick_launch(metadata)

The core of the API consists of two classes and one function:

* :class:`neurotic.datasets.metadata.MetadataSelector`: Read metadata files, download datasets
Expand Down Expand Up @@ -41,4 +51,5 @@ as ``neurotic.MetadataSelector``.
:maxdepth: 1
:caption: Other

api/scripts
api/_elephant_tools
6 changes: 6 additions & 0 deletions docs/api/scripts.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.. _api-scripts:

``neurotic.scripts``
====================

.. automodule:: neurotic.scripts
30 changes: 15 additions & 15 deletions neurotic/datasets/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def load_dataset(metadata, lazy=False, signal_group_mode='split-all', filter_eve
blk = _read_data_file(metadata, lazy, signal_group_mode)

# update the real-world start time of the data if provided
if metadata['rec_datetime'] is not None:
if metadata.get('rec_datetime', None) is not None:
if isinstance(metadata['rec_datetime'], datetime.datetime):
blk.rec_datetime = metadata['rec_datetime']
else:
Expand Down Expand Up @@ -105,14 +105,14 @@ def load_dataset(metadata, lazy=False, signal_group_mode='split-all', filter_eve
for sig in blk.segments[0].analogsignals:
rauc_sig = _elephant_tools.rauc(
signal=sig,
baseline=metadata['rauc_baseline'],
bin_duration=metadata['rauc_bin_duration']*pq.s,
baseline=metadata.get('rauc_baseline', None),
bin_duration=metadata.get('rauc_bin_duration', 0.1)*pq.s,
)
rauc_sig.name = sig.name + ' RAUC'
sig.annotate(
rauc_sig=rauc_sig,
rauc_baseline=metadata['rauc_baseline'],
rauc_bin_duration=metadata['rauc_bin_duration']*pq.s,
rauc_baseline=metadata.get('rauc_baseline', None),
rauc_bin_duration=metadata.get('rauc_bin_duration', 0.1)*pq.s,
)

return blk
Expand All @@ -128,15 +128,15 @@ def _get_io(metadata):
"""

# prepare arguments for instantiating a Neo IO class
if metadata['io_args'] is not None:
if metadata.get('io_args', None) is not None:
io_args = metadata['io_args'].copy()
if 'sampling_rate' in io_args:
# AsciiSignalIO's sampling_rate must be a Quantity
io_args['sampling_rate'] *= pq.Hz
else:
io_args = {}

if metadata['io_class'] is None:
if metadata.get('io_class', None) is None:
try:
# detect the class automatically using the file extension
io = neo.io.get_io(_abs_path(metadata, 'data_file'), **io_args)
Expand Down Expand Up @@ -259,7 +259,7 @@ def _read_annotations_file(metadata):
return a dataframe.
"""

if metadata['annotations_file'] is None:
if metadata.get('annotations_file', None) is None:

return None

Expand Down Expand Up @@ -325,7 +325,7 @@ def _read_epoch_encoder_file(metadata):
dataframe.
"""

if metadata['epoch_encoder_file'] is None:
if metadata.get('epoch_encoder_file', None) is None:

return None

Expand Down Expand Up @@ -396,7 +396,7 @@ def _read_spikes_file(metadata, blk):
dataframe.
"""

if metadata['tridesclous_file'] is None or metadata['tridesclous_channels'] is None:
if metadata.get('tridesclous_file', None) is None or metadata.get('tridesclous_channels', None) is None:

return None

Expand All @@ -408,7 +408,7 @@ def _read_spikes_file(metadata, blk):
# drop clusters with negative labels
df = df[df['label'] >= 0]

if metadata['tridesclous_merge']:
if metadata.get('tridesclous_merge', None):
# merge some clusters and drop all others
new_labels = []
for clusters_to_merge in metadata['tridesclous_merge']:
Expand Down Expand Up @@ -513,7 +513,7 @@ def _apply_filters(metadata, blk):
Apply filters specified in ``metadata`` to the signals in ``blk``.
"""

if metadata['filters'] is not None:
if metadata.get('filters', None) is not None:

signalNameToIndex = {sig.name:i for i, sig in enumerate(blk.segments[0].analogsignals)}

Expand Down Expand Up @@ -548,7 +548,7 @@ def _run_amplitude_discriminators(metadata, blk):

spiketrain_list = []

if metadata['amplitude_discriminators'] is not None:
if metadata.get('amplitude_discriminators', None) is not None:

signalNameToIndex = {sig.name:i for i, sig in enumerate(blk.segments[0].analogsignals)}
epochs = blk.segments[0].epochs
Expand Down Expand Up @@ -638,7 +638,7 @@ def _run_burst_detectors(metadata, blk):

burst_list = []

if metadata['burst_detectors'] is not None:
if metadata.get('burst_detectors', None) is not None:

spikeTrainNameToIndex = {st.name:i for i, st in enumerate(blk.segments[0].spiketrains)}

Expand Down Expand Up @@ -737,7 +737,7 @@ def _compute_firing_rates(metadata, blk):
dependency.
"""

if metadata['firing_rates'] is not None:
if metadata.get('firing_rates', None) is not None:

t_start = blk.segments[0].t_start
t_stop = blk.segments[0].t_stop
Expand Down
14 changes: 7 additions & 7 deletions neurotic/datasets/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,17 +422,17 @@ def _abs_path(metadata, file):
"""
Convert the relative path of file to an absolute path using data_dir
"""
if metadata[file] is None:
if metadata.get(file, None) is None:
return None
else:
return os.path.normpath(os.path.join(metadata['data_dir'], metadata[file]))
return os.path.normpath(os.path.join(metadata.get('data_dir', '.'), metadata[file]))


def _abs_url(metadata, file):
"""
Convert the relative path of file to a full URL using remote_data_dir
"""
if metadata[file] is None or metadata['remote_data_dir'] is None:
if metadata.get(file, None) is None or metadata.get('remote_data_dir', None) is None:
return None
else:
file_path = metadata[file].replace(os.sep, '/')
Expand Down Expand Up @@ -461,11 +461,11 @@ def _download_file(metadata, file, **kwargs):
arguments.
"""

if not _is_url(metadata['remote_data_dir']):
if not _is_url(metadata.get('remote_data_dir', None)):
logger.error('metadata[remote_data_dir] is not a full URL')
return

if metadata[file]:
if metadata.get(file, None):

# create directories if necessary
if not os.path.exists(os.path.dirname(_abs_path(metadata, file))):
Expand All @@ -483,7 +483,7 @@ def _download_all_data_files(metadata, **kwargs):
arguments.
"""

if not _is_url(metadata['remote_data_dir']):
if not _is_url(metadata.get('remote_data_dir', None)):
logger.error('metadata[remote_data_dir] is not a full URL')
return

Expand Down Expand Up @@ -512,7 +512,7 @@ def _selector_labels(all_metadata):
# no video_file
has_video_offset = {}
for key, metadata in all_metadata.items():
if metadata['video_offset'] is None and metadata['video_file'] is not None:
if metadata.get('video_offset', None) is None and metadata.get('video_file', None) is not None:
has_video_offset[key] = '!'
else:
has_video_offset[key] = ' '
Expand Down
28 changes: 14 additions & 14 deletions neurotic/gui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,21 +124,21 @@ def __init__(self, metadata, blk, lazy = False):
self.viewer_settings['event_list']['show'] = False
self.viewer_settings['event_list']['disabled'] = True
self.viewer_settings['event_list']['reason'] = 'Cannot enable because there are no read-only epochs or events'
if not self.metadata['epoch_encoder_file']:
if not self.metadata.get('epoch_encoder_file', None):
self.viewer_settings['epoch_encoder']['show'] = False
self.viewer_settings['epoch_encoder']['disabled'] = True
self.viewer_settings['epoch_encoder']['reason'] = 'Cannot enable because epoch_encoder_file is not set'
if not self.metadata['video_file']:
if not self.metadata.get('video_file', None):
self.viewer_settings['video']['show'] = False
self.viewer_settings['video']['disabled'] = True
self.viewer_settings['video']['reason'] = 'Cannot enable because video_file is not set'

# warn about potential video sync problems
if metadata['video_file'] is not None and metadata['video_offset'] is None:
if metadata.get('video_file', None) is not None and metadata.get('video_offset', None) is None:
logger.warning('Your video will likely be out of sync with your '
'data because video_offset is unspecified! '
'Consider adding it to your metadata.')
if metadata['video_file'] is not None and metadata['video_jumps'] is None:
if metadata.get('video_file', None) is not None and metadata.get('video_jumps', None) is None:
approx_video_jumps = _estimate_video_jump_times(blk)
if approx_video_jumps:
approx_video_jumps_recommendation = ' video_jumps:\n' + \
Expand Down Expand Up @@ -276,7 +276,7 @@ def create_ephyviewer_window(self, theme='light', support_increased_line_width=F
datetime0 = datetime0,
datetime_format = datetime_format,
)
win.setWindowTitle(self.metadata['key'])
win.setWindowTitle(self.metadata.get('key', 'neurotic'))
win.setWindowIcon(ephyviewer.QT.QIcon(':/neurotic-logo-150.png'))

# delete on close so that memory and file resources are released
Expand All @@ -288,13 +288,13 @@ def create_ephyviewer_window(self, theme='light', support_increased_line_width=F
# colors for signals given explicitly in plots, used for raw signals
# and RAUC
sig_colors = {}
if self.metadata['plots'] is not None:
if self.metadata.get('plots', None) is not None:
sig_colors = {p['channel']: p['color'] for p in self.metadata['plots'] if 'color' in p}

# colors for units given explicitly in amplitude_discriminators, used
# for scatter markers, spike trains, and burst epochs
unit_colors = {}
if self.metadata['amplitude_discriminators'] is not None:
if self.metadata.get('amplitude_discriminators', None) is not None:
unit_colors = {d['name']: d['color'] for d in self.metadata['amplitude_discriminators'] if 'color' in d}

########################################################################
Expand Down Expand Up @@ -623,7 +623,7 @@ def create_ephyviewer_window(self, theme='light', support_increased_line_width=F
########################################################################
# EPOCH ENCODER

if self.is_shown('epoch_encoder') and self.metadata['epoch_encoder_file'] is not None:
if self.is_shown('epoch_encoder') and self.metadata.get('epoch_encoder_file', None) is not None:

possible_labels = self.metadata['epoch_encoder_possible_labels']

Expand Down Expand Up @@ -654,7 +654,7 @@ def create_ephyviewer_window(self, theme='light', support_increased_line_width=F
########################################################################
# VIDEO

if self.is_shown('video') and self.metadata['video_file'] is not None:
if self.is_shown('video') and self.metadata.get('video_file', None) is not None:

video_source = ephyviewer.MultiVideoFileSource(video_filenames = [_abs_path(self.metadata, 'video_file')])

Expand All @@ -664,16 +664,16 @@ def create_ephyviewer_window(self, theme='light', support_increased_line_width=F
video_source.t_starts[0] = 0

# apply the video_offset
if self.metadata['video_offset'] is not None:
if self.metadata.get('video_offset', None) is not None:
video_source.t_starts[0] += self.metadata['video_offset']
video_source.t_stops[0] += self.metadata['video_offset']

# correct for videos that report frame rates that are too fast or
# too slow compared to the clock on the data acquisition system
if self.metadata['video_rate_correction'] is not None:
if self.metadata.get('video_rate_correction', None) is not None:
video_source.rates[0] *= self.metadata['video_rate_correction']

if self.metadata['video_jumps'] is not None:
if self.metadata.get('video_jumps', None) is not None:

# create an unmodified video_times vector with evenly spaced times
video_times = np.arange(video_source.nb_frames[0])/video_source.rates[0] + video_source.t_starts[0]
Expand Down Expand Up @@ -734,7 +734,7 @@ def create_ephyviewer_window(self, theme='light', support_increased_line_width=F
widget.setCurrentIndex(0)

# set amount of time shown initially
win.set_xsize(self.metadata['t_width']) # seconds
win.set_xsize(self.metadata.get('t_width', 40)) # seconds

return win

Expand All @@ -747,7 +747,7 @@ def _set_defaults_for_plots(metadata, blk):
sigs = blk.segments[0].analogsignals
signalNameToIndex = {sig.name:i for i, sig in enumerate(sigs)}

if metadata['plots'] is None:
if metadata.get('plots', None) is None:
metadata['plots'] = [{'channel': sig.name} for sig in sigs if _default_keep_signal(sig)]

plots = []
Expand Down
2 changes: 1 addition & 1 deletion neurotic/gui/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def _on_select(self, change):
self._selection = self.selector.value

# warn if video_offset is not set
if self.selected_metadata['video_file'] is not None and self.selected_metadata['video_offset'] is None:
if self.selected_metadata.get('video_file', None) is not None and self.selected_metadata.get('video_offset', None) is None:
logger.warning('Video sync may be incorrect! video_offset not set for {}'.format(self._selection))

def _on_reload_clicked(self, button):
Expand Down
37 changes: 36 additions & 1 deletion neurotic/scripts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# -*- coding: utf-8 -*-
"""
The :mod:`neurotic.scripts` module handles starting the app from the command
line. It also provides a convenience function for quickly launching the app
using minimal metadata, and another for starting a Jupyter server with an
example notebook.

.. autofunction:: quick_launch

.. autofunction:: launch_example_notebook
"""

import sys
Expand All @@ -11,6 +18,8 @@
from ephyviewer import mkQApp

from . import __version__
from .datasets.data import load_dataset
from .gui.config import EphyviewerConfigurator
from .gui.standalone import MainWindow

import logging
Expand Down Expand Up @@ -90,7 +99,7 @@ def win_from_args(args):

def launch_example_notebook():
"""

Start a Jupyter server and open the example notebook.
"""

path = pkg_resources.resource_filename('neurotic',
Expand Down Expand Up @@ -128,3 +137,29 @@ def main():
win.show()
logger.info('Ready')
app.exec_()

def quick_launch(metadata, lazy=True):
"""
Load data, configure the GUI, and launch the app with one convenient
function.

This function allows **neurotic** to be used easily in interactive sessions
and scripts. For example:

>>> import neurotic
>>> metadata = {'data_file': 'data.axgx'}
>>> neurotic.quick_launch(metadata)

This function is equivalent to the following:

>>> blk = load_dataset(metadata, lazy=lazy)
>>> ephyviewer_config = EphyviewerConfigurator(metadata, blk, lazy=lazy)
>>> ephyviewer_config.show_all()
>>> ephyviewer_config.launch_ephyviewer()
"""

# make sure this matches the docstring after making changes
blk = load_dataset(metadata, lazy=lazy)
ephyviewer_config = EphyviewerConfigurator(metadata, blk, lazy=lazy)
ephyviewer_config.show_all()
ephyviewer_config.launch_ephyviewer()