From 0606860b6b3bdd61a0135528d9f7fc36828cde60 Mon Sep 17 00:00:00 2001 From: bendichter Date: Tue, 9 Nov 2021 17:01:19 -0600 Subject: [PATCH 01/12] move tests outside the nwbwidgets package --- nwbwidgets/ophys.py | 1 - {nwbwidgets/test => test}/__init__.py | 0 {nwbwidgets/test => test}/test_base.py | 13 +++++++------ {nwbwidgets/test => test}/test_behavior.py | 0 {nwbwidgets/test => test}/test_controllers.py | 3 ++- {nwbwidgets/test => test}/test_ecephys.py | 0 {nwbwidgets/test => test}/test_file.py | 0 {nwbwidgets/test => test}/test_icephys.py | 3 ++- {nwbwidgets/test => test}/test_image.py | 0 {nwbwidgets/test => test}/test_misc.py | 0 {nwbwidgets/test => test}/test_ophys.py | 0 {nwbwidgets/test => test}/test_timeseries.py | 0 {nwbwidgets/test => test}/test_utils_cmaps.py | 0 .../test => test}/test_utils_dynamictable.py | 0 {nwbwidgets/test => test}/test_utils_mpl.py | 0 {nwbwidgets/test => test}/test_utils_timeseries.py | 0 {nwbwidgets/test => test}/test_utils_units.py | 4 ++-- 17 files changed, 13 insertions(+), 11 deletions(-) rename {nwbwidgets/test => test}/__init__.py (100%) rename {nwbwidgets/test => test}/test_base.py (99%) rename {nwbwidgets/test => test}/test_behavior.py (100%) rename {nwbwidgets/test => test}/test_controllers.py (99%) rename {nwbwidgets/test => test}/test_ecephys.py (100%) rename {nwbwidgets/test => test}/test_file.py (100%) rename {nwbwidgets/test => test}/test_icephys.py (99%) rename {nwbwidgets/test => test}/test_image.py (100%) rename {nwbwidgets/test => test}/test_misc.py (100%) rename {nwbwidgets/test => test}/test_ophys.py (100%) rename {nwbwidgets/test => test}/test_timeseries.py (100%) rename {nwbwidgets/test => test}/test_utils_cmaps.py (100%) rename {nwbwidgets/test => test}/test_utils_dynamictable.py (100%) rename {nwbwidgets/test => test}/test_utils_mpl.py (100%) rename {nwbwidgets/test => test}/test_utils_timeseries.py (100%) rename {nwbwidgets/test => test}/test_utils_units.py (98%) diff --git a/nwbwidgets/ophys.py b/nwbwidgets/ophys.py index a063bd0b..8ca02a54 100644 --- a/nwbwidgets/ophys.py +++ b/nwbwidgets/ophys.py @@ -1,7 +1,6 @@ from functools import lru_cache import ipywidgets as widgets -import matplotlib.pyplot as plt import numpy as np import plotly.graph_objects as go import plotly.express as px diff --git a/nwbwidgets/test/__init__.py b/test/__init__.py similarity index 100% rename from nwbwidgets/test/__init__.py rename to test/__init__.py diff --git a/nwbwidgets/test/test_base.py b/test/test_base.py similarity index 99% rename from nwbwidgets/test/test_base.py rename to test/test_base.py index 388fce17..ef513960 100644 --- a/nwbwidgets/test/test_base.py +++ b/test/test_base.py @@ -7,6 +7,13 @@ import pytest from dateutil.tz import tzlocal from ipywidgets import widgets +from pynwb import NWBFile +from pynwb import ProcessingModule +from pynwb import TimeSeries +from pynwb.behavior import Position, SpatialSeries +from pynwb.core import DynamicTable +from pynwb.file import Subject + from nwbwidgets.base import ( show_neurodata_base, processing_module, @@ -20,12 +27,6 @@ ) from nwbwidgets.view import default_neurodata_vis_spec from nwbwidgets.view import show_dynamic_table -from pynwb import NWBFile -from pynwb import ProcessingModule -from pynwb import TimeSeries -from pynwb.behavior import Position, SpatialSeries -from pynwb.core import DynamicTable -from pynwb.file import Subject def test_show_neurodata_base(): diff --git a/nwbwidgets/test/test_behavior.py b/test/test_behavior.py similarity index 100% rename from nwbwidgets/test/test_behavior.py rename to test/test_behavior.py diff --git a/nwbwidgets/test/test_controllers.py b/test/test_controllers.py similarity index 99% rename from nwbwidgets/test/test_controllers.py rename to test/test_controllers.py index 195816aa..76514efc 100644 --- a/nwbwidgets/test/test_controllers.py +++ b/test/test_controllers.py @@ -1,9 +1,10 @@ import unittest import numpy as np -from nwbwidgets.controllers import RangeController, GroupAndSortController from hdmf.common import DynamicTable, VectorData from pynwb.ecephys import ElectrodeGroup, Device +from nwbwidgets.controllers import RangeController, GroupAndSortController + class FloatRangeControllerTestCase(unittest.TestCase): def setUp(self): diff --git a/nwbwidgets/test/test_ecephys.py b/test/test_ecephys.py similarity index 100% rename from nwbwidgets/test/test_ecephys.py rename to test/test_ecephys.py diff --git a/nwbwidgets/test/test_file.py b/test/test_file.py similarity index 100% rename from nwbwidgets/test/test_file.py rename to test/test_file.py diff --git a/nwbwidgets/test/test_icephys.py b/test/test_icephys.py similarity index 99% rename from nwbwidgets/test/test_icephys.py rename to test/test_icephys.py index 626a0764..faa412c4 100644 --- a/nwbwidgets/test/test_icephys.py +++ b/test/test_icephys.py @@ -1,11 +1,12 @@ import matplotlib.pyplot as plt import numpy as np from ndx_icephys_meta.icephys import Sweeps, IntracellularRecordings -from nwbwidgets.icephys import show_single_sweep_sequence from pynwb.base import TimeSeries from pynwb.device import Device from pynwb.icephys import IntracellularElectrode +from nwbwidgets.icephys import show_single_sweep_sequence + def test_show_single_sweep_sequence(): device = Device(name="Axon Patch-Clamp") diff --git a/nwbwidgets/test/test_image.py b/test/test_image.py similarity index 100% rename from nwbwidgets/test/test_image.py rename to test/test_image.py diff --git a/nwbwidgets/test/test_misc.py b/test/test_misc.py similarity index 100% rename from nwbwidgets/test/test_misc.py rename to test/test_misc.py diff --git a/nwbwidgets/test/test_ophys.py b/test/test_ophys.py similarity index 100% rename from nwbwidgets/test/test_ophys.py rename to test/test_ophys.py diff --git a/nwbwidgets/test/test_timeseries.py b/test/test_timeseries.py similarity index 100% rename from nwbwidgets/test/test_timeseries.py rename to test/test_timeseries.py diff --git a/nwbwidgets/test/test_utils_cmaps.py b/test/test_utils_cmaps.py similarity index 100% rename from nwbwidgets/test/test_utils_cmaps.py rename to test/test_utils_cmaps.py diff --git a/nwbwidgets/test/test_utils_dynamictable.py b/test/test_utils_dynamictable.py similarity index 100% rename from nwbwidgets/test/test_utils_dynamictable.py rename to test/test_utils_dynamictable.py diff --git a/nwbwidgets/test/test_utils_mpl.py b/test/test_utils_mpl.py similarity index 100% rename from nwbwidgets/test/test_utils_mpl.py rename to test/test_utils_mpl.py diff --git a/nwbwidgets/test/test_utils_timeseries.py b/test/test_utils_timeseries.py similarity index 100% rename from nwbwidgets/test/test_utils_timeseries.py rename to test/test_utils_timeseries.py diff --git a/nwbwidgets/test/test_utils_units.py b/test/test_utils_units.py similarity index 98% rename from nwbwidgets/test/test_utils_units.py rename to test/test_utils_units.py index c84f1c76..7c962a15 100644 --- a/nwbwidgets/test/test_utils_units.py +++ b/test/test_utils_units.py @@ -12,8 +12,8 @@ from pynwb import NWBFile from pynwb.epoch import TimeIntervals -from ..base import TimeIntervalsSelector -from ..misc import TuningCurveWidget, TuningCurveExtendedWidget +from nwbwidgets.base import TimeIntervalsSelector +from nwbwidgets.misc import TuningCurveWidget, TuningCurveExtendedWidget class UnitsTrialsTestCase(unittest.TestCase): From e50ce15606bbf23a4974fc983650b76dfea0d854 Mon Sep 17 00:00:00 2001 From: bendichter Date: Mon, 15 Nov 2021 17:44:25 -0600 Subject: [PATCH 02/12] add test for StartAndDurationController --- test/test_controllers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/test_controllers.py b/test/test_controllers.py index 76514efc..33081ff7 100644 --- a/test/test_controllers.py +++ b/test/test_controllers.py @@ -3,7 +3,7 @@ from hdmf.common import DynamicTable, VectorData from pynwb.ecephys import ElectrodeGroup, Device -from nwbwidgets.controllers import RangeController, GroupAndSortController +from nwbwidgets.controllers import RangeController, GroupAndSortController, StartAndDurationController class FloatRangeControllerTestCase(unittest.TestCase): @@ -72,3 +72,11 @@ def test_control(self): gas.order_dd.value = "Data1" gas.order_dd.value = None + + +class TestStartAndDurationController(unittest.TestCase): + def setUp(self) -> None: + self.start_and_duration_controller = StartAndDurationController(10) + + def test_set_duration(self): + self.start_and_duration_controller.duration.value = 2 From 27b4f85c131018c368c32eab4ab19d63e8ed6349 Mon Sep 17 00:00:00 2001 From: bendichter Date: Thu, 16 Dec 2021 10:42:17 -0500 Subject: [PATCH 03/12] fix for when electrodes selects subregion of electrodes table --- nwbwidgets/controllers/group_and_sort_controllers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nwbwidgets/controllers/group_and_sort_controllers.py b/nwbwidgets/controllers/group_and_sort_controllers.py index 4843f335..b5cbf7e8 100644 --- a/nwbwidgets/controllers/group_and_sort_controllers.py +++ b/nwbwidgets/controllers/group_and_sort_controllers.py @@ -204,7 +204,7 @@ def set_group_by(self, group_by): if self.column_values is not None: self.group_sm.layout.visibility = "visible" self.group_sm.layout.width = "100px" - keep_column_values = self.column_values[self.keep_rows] + keep_column_values = self.column_values if self.column_values.dtype == np.float: keep_column_values = keep_column_values[~np.isnan(keep_column_values)] groups = np.unique(keep_column_values) @@ -319,13 +319,13 @@ def range_controller_observer(self, change): def get_groups(self): return infer_categorical_columns(self.dynamic_table, self.keep_rows) - def get_column_values(self, column_name, rows_select=None): + def get_column_values(self, column_name: str, rows_select=None): """Get the values of the group_by variable Parameters ---------- - by - units_select + column_name: str + rows_select Returns ------- From af031bb1b15880e43b7b96b6bf22a2ee021c0197 Mon Sep 17 00:00:00 2001 From: bendichter Date: Fri, 21 Jan 2022 16:31:17 -0500 Subject: [PATCH 04/12] * change default stop_label to None * correct base where units was used accidentally instead of the generic input_data * add route_psth, which decides between a standard PSTHWidget and a PSTHWidget inside a TimeIntervalsSelector wrapper * change view to use new routing function --- nwbwidgets/base.py | 2 +- nwbwidgets/misc.py | 14 +++++++++++--- nwbwidgets/utils/units.py | 2 +- nwbwidgets/view.py | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/nwbwidgets/base.py b/nwbwidgets/base.py index 8a22acfa..df3251b2 100644 --- a/nwbwidgets/base.py +++ b/nwbwidgets/base.py @@ -396,7 +396,7 @@ def __init__(self, input_data, **kwargs): trials = list(self.intervals_tables.values())[0] inner_widget = self.InnerWidget( - units=self.input_data, + input_data=self.input_data, trials=trials, **kwargs ) diff --git a/nwbwidgets/misc.py b/nwbwidgets/misc.py index 91730801..aa90409b 100644 --- a/nwbwidgets/misc.py +++ b/nwbwidgets/misc.py @@ -12,6 +12,7 @@ from pynwb.misc import AnnotationSeries, Units, DecompositionSeries from .analysis.spikes import compute_smoothed_firing_rate +from .base import TimeIntervalsSelector from .controllers import ( make_trial_event_controller, GroupAndSortController, @@ -283,9 +284,6 @@ def __init__( if trials is None: self.trials = self.get_trials() - if self.trials is None: - self.children = [widgets.HTML("No trials present")] - return else: self.trials = trials @@ -535,6 +533,16 @@ def update( fig.subplots_adjust(wspace=0.3) return fig +class IntervalsPSTHWidget(TimeIntervalsSelector): + InnerWidget = PSTHWidget + +def route_psth(units, **kwargs): + trials = units.get_ancestor("NWBFile").trials + if trials is None: + return IntervalsPSTHWidget(units, **kwargs) + else: + return PSTHWidget(units, **kwargs) + def show_histogram( data, ax: plt.Axes, start: float, end: float, group_inds=None, nbins: int = 30 diff --git a/nwbwidgets/utils/units.py b/nwbwidgets/utils/units.py index 963ec748..f78fdfc3 100644 --- a/nwbwidgets/utils/units.py +++ b/nwbwidgets/utils/units.py @@ -116,7 +116,7 @@ def align_by_time_intervals( index, intervals, start_label="start_time", - stop_label="stop_time", + stop_label=None, start=0.0, end=0.0, rows_select=(), diff --git a/nwbwidgets/view.py b/nwbwidgets/view.py index c28ed4bd..0ca9a6db 100644 --- a/nwbwidgets/view.py +++ b/nwbwidgets/view.py @@ -39,7 +39,7 @@ def show_dynamic_table(node, **kwargs) -> widgets.Widget: { "Summary": DynamicTableSummaryWidget, "Session Raster": misc.RasterWidget, - "Grouped PSTH": misc.PSTHWidget, + "Grouped PSTH": misc.route_psth, "Raster Grid": misc.RasterGridWidget, "Tuning Curves": misc.TuningCurveWidget, "Combined": misc.TuningCurveExtendedWidget, From cb7a9d575d95f6b9dee66e806d340fd71df2dad6 Mon Sep 17 00:00:00 2001 From: bendichter Date: Fri, 21 Jan 2022 16:34:21 -0500 Subject: [PATCH 05/12] remove this from allen customization, since this is now the default behavior --- nwbwidgets/allen.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nwbwidgets/allen.py b/nwbwidgets/allen.py index d48dd196..f406105b 100644 --- a/nwbwidgets/allen.py +++ b/nwbwidgets/allen.py @@ -19,10 +19,6 @@ def make_group_and_sort(self, group_by=None, control_order=False): ) -class AllenPSTHWidget(TimeIntervalsSelector): - InnerWidget = PSTHWidget - - class AllenRasterGridWidget(TimeIntervalsSelector): InnerWidget = RasterGridWidget @@ -85,7 +81,6 @@ def allen_show_electrodes(node: DynamicTable): def load_allen_widgets(): default_neurodata_vis_spec[Units]["Session Raster"] = AllenRasterWidget - default_neurodata_vis_spec[Units]["Grouped PSTH"] = AllenPSTHWidget default_neurodata_vis_spec[Units]["Raster Grid"] = AllenRasterGridWidget default_neurodata_vis_spec[Units]["Tuning Curves"] = AllenTuningCurveWidget # default_neurodata_vis_spec[DynamicTable] = allen_show_dynamic_table From 289920af61a4afc45a333af7f54626f375394c5c Mon Sep 17 00:00:00 2001 From: bendichter Date: Fri, 21 Jan 2022 19:27:32 -0500 Subject: [PATCH 06/12] * change "trials" argument to "intervals" * use positional arg for the first arg of TimeIntervalsSelector --- nwbwidgets/base.py | 16 ++++++++-------- nwbwidgets/misc.py | 19 ++++++++++--------- test/test_utils_units.py | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/nwbwidgets/base.py b/nwbwidgets/base.py index df3251b2..c01e352d 100644 --- a/nwbwidgets/base.py +++ b/nwbwidgets/base.py @@ -389,25 +389,25 @@ def __init__(self, input_data, **kwargs): self.kwargs = kwargs self.intervals_tables = input_data.get_ancestor("NWBFile").intervals self.stimulus_type_dd = widgets.Dropdown( - options=list(self.intervals_tables.keys()), - description="stimulus type" + options=list(self.intervals_tables), + description="intervals table" ) self.stimulus_type_dd.observe(self.stimulus_type_dd_callback) - trials = list(self.intervals_tables.values())[0] + intervals = list(self.intervals_tables.values())[0] inner_widget = self.InnerWidget( - input_data=self.input_data, - trials=trials, + self.input_data, + intervals=intervals, **kwargs ) self.children = [self.stimulus_type_dd, inner_widget] def stimulus_type_dd_callback(self, change): self.children = [self.stimulus_type_dd, widgets.HTML("Rendering...")] - trials = self.intervals_tables[self.stimulus_type_dd.value] + intervals = self.intervals_tables[self.stimulus_type_dd.value] inner_widget = self.InnerWidget( - input_data=self.input_data, - trials=trials, + self.input_data, + intervals=intervals, **self.kwargs ) self.children = [self.stimulus_type_dd, inner_widget] diff --git a/nwbwidgets/misc.py b/nwbwidgets/misc.py index aa90409b..450383df 100644 --- a/nwbwidgets/misc.py +++ b/nwbwidgets/misc.py @@ -272,7 +272,7 @@ class PSTHWidget(widgets.VBox): def __init__( self, input_data: Units, - trials: pynwb.epoch.TimeIntervals = None, + intervals: pynwb.epoch.TimeIntervals = None, unit_index=0, unit_controller=None, ntt=1000, @@ -282,10 +282,10 @@ def __init__( super().__init__() - if trials is None: + if intervals is None: self.trials = self.get_trials() else: - self.trials = trials + self.trials = intervals if unit_controller is None: self.unit_ids = self.units.id.data[:] @@ -512,6 +512,7 @@ def update( ) ax1.set_xlim([start, end]) + ax1.set_xticks([start, end]) if i_s == 0: ax1.set_ylabel("firing rate (Hz)", fontsize=12) ax1.set_xlabel("time (s)", fontsize=12) @@ -1175,7 +1176,7 @@ class RasterGridWidget(widgets.VBox): def __init__( self, units: Units, - trials: pynwb.epoch.TimeIntervals = None, + intervals: pynwb.epoch.TimeIntervals = None, unit_index=0, units_trials_controller=None, ): @@ -1185,7 +1186,7 @@ def __init__( if not units_trials_controller: units_trials_controller = UnitsAndTrialsControllerWidget( units=units, - trials=trials, + trials=intervals, unit_index=unit_index ) self.children = [units_trials_controller] @@ -1203,7 +1204,7 @@ class TuningCurveWidget(widgets.VBox): def __init__( self, units: Units, - trials: pynwb.epoch.TimeIntervals = None, + intervals: pynwb.epoch.TimeIntervals = None, unit_index=0, units_trials_controller=None, ): @@ -1214,7 +1215,7 @@ def __init__( if not units_trials_controller: units_trials_controller = UnitsAndTrialsControllerWidget( units=units, - trials=trials, + trials=intervals, unit_index=unit_index ) self.children = [units_trials_controller] @@ -1247,7 +1248,7 @@ def __init__( # Tuning curve widget self.tuning_curve = TuningCurveWidget( units=units, - trials=trials, + intervals=trials, unit_index=unit_index, units_trials_controller=self.units_trials_controller, ) @@ -1255,7 +1256,7 @@ def __init__( # Raster grid widget self.raster_grid = RasterGridWidget( units=units, - trials=trials, + intervals=trials, unit_index=unit_index, units_trials_controller=self.units_trials_controller, ) diff --git a/test/test_utils_units.py b/test/test_utils_units.py index 7c962a15..d5950073 100644 --- a/test/test_utils_units.py +++ b/test/test_utils_units.py @@ -114,7 +114,7 @@ def setUp(self): super().setUp() self.widget = TuningCurveWidget( units=self.nwbfile.units, - trials=self.nwbfile.trials + intervals=self.nwbfile.trials ) # rows controller triggers drawing of graphic self.widget.children[0].children[1].value = 'stim' From 67b611f273e97faad6523a34e7f7a0abb4060c24 Mon Sep 17 00:00:00 2001 From: bendichter Date: Sun, 23 Jan 2022 19:12:19 -0500 Subject: [PATCH 07/12] go back to using PSTHWidget and put intervals setup and control logic inside of it --- nwbwidgets/misc.py | 95 +++++++++++++++++++++++++++++++--------------- nwbwidgets/view.py | 2 +- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/nwbwidgets/misc.py b/nwbwidgets/misc.py index 450383df..8e387ecb 100644 --- a/nwbwidgets/misc.py +++ b/nwbwidgets/misc.py @@ -270,22 +270,56 @@ def control_plot(x0, x1, ch0, ch1): class PSTHWidget(widgets.VBox): def __init__( - self, - input_data: Units, - intervals: pynwb.epoch.TimeIntervals = None, - unit_index=0, - unit_controller=None, - ntt=1000, + self, + units: Units, + intervals: str = None, + unit_index=0, + unit_controller=None, + ntt=1000, ): + """ + + Parameters + ---------- + input_data: pynwb.Units + intervals: str, optional + Name of intervals to use. If none are given and one is available, use that. If more than one are + available, create a dropdown + unit_index: int + unit_controller + ntt: int + """ - self.units = input_data + self.units = units + self.ntt = ntt super().__init__() if intervals is None: - self.trials = self.get_trials() + all_intervals_tables = self.units.get_ancestor("NWBFile").intervals + if len(all_intervals_tables) == 0: + self.children = [HTMLWidget("could not find intervals")] + return + elif len(all_intervals_tables) == 1: + self.intervals = next(all_intervals_tables.values()) + self.intervals_dropdown = None + else: + self.intervals_dropdown = widgets.Dropdown( + options=list(all_intervals_tables), + description="intervals", + ) + self.intervals_dropdown.observe(self.intervals_selector_callback) + self.intervals = list(all_intervals_tables.values())[0] else: - self.trials = intervals + if isinstance(intervals, str): + self.intervals = self.units.get_ancestor("NWBFile").intervals[intervals] + self.intervals_dropdown = None + elif isinstance(intervals, widgets.Dropdown): + self.intervals_dropdown = intervals + self.intervals = self.units.get_ancestor("NWBFile").intervals[self.intervals_dropdown.value] + else: + self.intervals = intervals + self.intervals_dropdown = None if unit_controller is None: self.unit_ids = self.units.id.data[:] @@ -299,8 +333,16 @@ def __init__( else: self.unit_controller = unit_controller + self.refresh_intervals() + + def make_group_and_sort(self, window=None, control_order=False): + return GroupAndSortController( + self.intervals, window=window, control_order=control_order + ) + + def refresh_intervals(self): self.trial_event_controller = make_trial_event_controller( - self.trials, layout=Layout(width="200px"), multiple=True + self.intervals, layout=Layout(width="200px"), multiple=True ) self.start_ft = widgets.FloatText( -0.5, step=0.1, description="start (s)", layout=Layout(width="200px"), @@ -330,7 +372,7 @@ def __init__( self.gas = self.make_group_and_sort(window=False, control_order=False) self.controls = dict( - ntt=fixed(ntt), + ntt=fixed(self.ntt), index=self.unit_controller, end=self.end_ft, start=self.start_ft, @@ -344,7 +386,7 @@ def __init__( out_fig = interactive_output(self.update, self.controls) - self.children = [ + children = [ widgets.HBox( [ widgets.VBox( @@ -371,13 +413,16 @@ def __init__( out_fig, ] - def get_trials(self): - return self.units.get_ancestor("NWBFile").trials + if self.intervals_dropdown is not None: + children.insert(0, self.intervals_dropdown) + + self.children = children - def make_group_and_sort(self, window=None, control_order=False): - return GroupAndSortController( - self.trials, window=window, control_order=control_order - ) + + def intervals_selector_callback(self, change): + self.children = [self.intervals_dropdown, widgets.HTML("Rendering...")] + self.intervals = self.units.get_ancestor("NWBFile").intervals[self.intervals_dropdown.value] + self.refresh_intervals() def update( self, @@ -439,7 +484,7 @@ def update( data = align_by_time_intervals( self.units, index, - self.trials, + self.intervals, start_label, start_label, start, @@ -483,7 +528,7 @@ def update( expanded_data = align_by_time_intervals( units=self.units, index=index, - intervals=self.trials, + intervals=self.intervals, start_label=start_label, stop_label=start_label, start=start - sigma_in_secs * 4, @@ -534,16 +579,6 @@ def update( fig.subplots_adjust(wspace=0.3) return fig -class IntervalsPSTHWidget(TimeIntervalsSelector): - InnerWidget = PSTHWidget - -def route_psth(units, **kwargs): - trials = units.get_ancestor("NWBFile").trials - if trials is None: - return IntervalsPSTHWidget(units, **kwargs) - else: - return PSTHWidget(units, **kwargs) - def show_histogram( data, ax: plt.Axes, start: float, end: float, group_inds=None, nbins: int = 30 diff --git a/nwbwidgets/view.py b/nwbwidgets/view.py index 0ca9a6db..c28ed4bd 100644 --- a/nwbwidgets/view.py +++ b/nwbwidgets/view.py @@ -39,7 +39,7 @@ def show_dynamic_table(node, **kwargs) -> widgets.Widget: { "Summary": DynamicTableSummaryWidget, "Session Raster": misc.RasterWidget, - "Grouped PSTH": misc.route_psth, + "Grouped PSTH": misc.PSTHWidget, "Raster Grid": misc.RasterGridWidget, "Tuning Curves": misc.TuningCurveWidget, "Combined": misc.TuningCurveExtendedWidget, From a0c017faf631e4cae57d30086cd82cb4448008b3 Mon Sep 17 00:00:00 2001 From: bendichter Date: Sun, 23 Jan 2022 20:53:17 -0500 Subject: [PATCH 08/12] fix handling of trials --- nwbwidgets/misc.py | 17 ++++++++++++----- test/test_misc.py | 1 - 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/nwbwidgets/misc.py b/nwbwidgets/misc.py index 8e387ecb..aef0e6bd 100644 --- a/nwbwidgets/misc.py +++ b/nwbwidgets/misc.py @@ -6,7 +6,7 @@ import plotly.graph_objects as go import pynwb import scipy -from ipywidgets import widgets, fixed, FloatProgress, Layout +from ipywidgets import widgets, fixed, FloatProgress, Layout, HTML from matplotlib.collections import PatchCollection from matplotlib.patches import Rectangle from pynwb.misc import AnnotationSeries, Units, DecompositionSeries @@ -297,11 +297,14 @@ def __init__( if intervals is None: all_intervals_tables = self.units.get_ancestor("NWBFile").intervals + trials = self.units.get_ancestor("NWBFile").trials + if trials is not None: + all_intervals_tables.add(trials) if len(all_intervals_tables) == 0: - self.children = [HTMLWidget("could not find intervals")] + self.children = [HTML("could not find intervals")] return elif len(all_intervals_tables) == 1: - self.intervals = next(all_intervals_tables.values()) + self.intervals = list(all_intervals_tables.values())[0] self.intervals_dropdown = None else: self.intervals_dropdown = widgets.Dropdown( @@ -309,17 +312,21 @@ def __init__( description="intervals", ) self.intervals_dropdown.observe(self.intervals_selector_callback) - self.intervals = list(all_intervals_tables.values())[0] + self.intervals = next(all_intervals_tables.values()) else: if isinstance(intervals, str): + if intervals not in self.units.get_ancestor("NWBFile").intervals: + raise ValueError("'{intervals}' not in NWBFile.intervals") self.intervals = self.units.get_ancestor("NWBFile").intervals[intervals] self.intervals_dropdown = None elif isinstance(intervals, widgets.Dropdown): self.intervals_dropdown = intervals self.intervals = self.units.get_ancestor("NWBFile").intervals[self.intervals_dropdown.value] - else: + elif isinstance(intervals, pynwb.epoch.TimeIntervals): self.intervals = intervals self.intervals_dropdown = None + else: + raise ValueError("intervals is not an allowable type") if unit_controller is None: self.unit_ids = self.units.id.data[:] diff --git a/test/test_misc.py b/test/test_misc.py index 2571ff25..8334406d 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -93,7 +93,6 @@ def test_psth_widget(self): def test_multipsth_widget(self): psth_widget = PSTHWidget(self.nwbfile.units) - assert isinstance(psth_widget, widgets.Widget) start_labels = ('start_time', 'stop_time') fig = psth_widget.update(index=0, start_labels=start_labels) assert len(fig.axes) == 2 * len(start_labels) From e2a88bee49554ba0e52c19d34fd8557887a0ed3d Mon Sep 17 00:00:00 2001 From: bendichter Date: Mon, 24 Jan 2022 08:24:47 -0500 Subject: [PATCH 09/12] add test for multiple existing intervals tables --- nwbwidgets/misc.py | 2 +- test/test_misc.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/nwbwidgets/misc.py b/nwbwidgets/misc.py index aef0e6bd..f69abe0b 100644 --- a/nwbwidgets/misc.py +++ b/nwbwidgets/misc.py @@ -312,7 +312,7 @@ def __init__( description="intervals", ) self.intervals_dropdown.observe(self.intervals_selector_callback) - self.intervals = next(all_intervals_tables.values()) + self.intervals = list(all_intervals_tables.values())[0] else: if isinstance(intervals, str): if intervals not in self.units.get_ancestor("NWBFile").intervals: diff --git a/test/test_misc.py b/test/test_misc.py index 8334406d..3c4b8dc7 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -18,7 +18,7 @@ ) from pynwb import NWBFile from pynwb.misc import DecompositionSeries, AnnotationSeries - +from pynwb.epoch import TimeIntervals def test_show_psth(): data = np.random.random([6, 50]) @@ -96,7 +96,16 @@ def test_multipsth_widget(self): start_labels = ('start_time', 'stop_time') fig = psth_widget.update(index=0, start_labels=start_labels) assert len(fig.axes) == 2 * len(start_labels) - + + def test_multiple_intervals(self): + time_intervals = TimeIntervals("custom_intervals_table") + time_intervals.add_row(start_time=1., stop_time=2.) + time_intervals.add_row(start_time=2.5, stop_time=3.5) + self.nwbfile.intervals.add(time_intervals) + widget = PSTHWidget(self.nwbfile.units) + assert widget.intervals_dropdown is not None + + def test_raster_widget(self): assert isinstance(RasterWidget(self.nwbfile.units), widgets.Widget) From 7fdd264f39ed803daa1133a259343766cbfefd64 Mon Sep 17 00:00:00 2001 From: bendichter Date: Mon, 24 Jan 2022 09:10:49 -0500 Subject: [PATCH 10/12] add tests for manually setting the intervals --- test/test_misc.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_misc.py b/test/test_misc.py index 3c4b8dc7..46c0bc2c 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -105,6 +105,21 @@ def test_multiple_intervals(self): widget = PSTHWidget(self.nwbfile.units) assert widget.intervals_dropdown is not None + def test_input_intervals_trials_name(self): + + widget = PSTHWidget(self.nwbfile.units, intervals="trials") + assert widget.intervals.name == "trials" + assert widget.intervals_dropdown is None + + def test_input_intervals_object(self): + + time_intervals = TimeIntervals("custom_intervals_table") + time_intervals.add_row(start_time=1., stop_time=2.) + time_intervals.add_row(start_time=2.5, stop_time=3.5) + widget = PSTHWidget(self.nwbfile.units, intervals=time_intervals) + assert widget.intervals.name == "custom_intervals_table" + assert widget.intervals_dropdown is None + def test_raster_widget(self): assert isinstance(RasterWidget(self.nwbfile.units), widgets.Widget) From 8925e07d835257629647b7f39479717d918dc4c1 Mon Sep 17 00:00:00 2001 From: bendichter Date: Mon, 24 Jan 2022 09:14:26 -0500 Subject: [PATCH 11/12] add tests for manually setting the intervals --- nwbwidgets/misc.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nwbwidgets/misc.py b/nwbwidgets/misc.py index f69abe0b..0205be13 100644 --- a/nwbwidgets/misc.py +++ b/nwbwidgets/misc.py @@ -314,17 +314,19 @@ def __init__( self.intervals_dropdown.observe(self.intervals_selector_callback) self.intervals = list(all_intervals_tables.values())[0] else: + nwbfile = self.units.get_ancestor("NWBFile") + self.intervals_dropdown = None if isinstance(intervals, str): - if intervals not in self.units.get_ancestor("NWBFile").intervals: + if intervals == "trials": + self.intervals = nwbfile.trials + elif intervals not in nwbfile.intervals: raise ValueError("'{intervals}' not in NWBFile.intervals") - self.intervals = self.units.get_ancestor("NWBFile").intervals[intervals] - self.intervals_dropdown = None + self.intervals = nwbfile.intervals[intervals] elif isinstance(intervals, widgets.Dropdown): self.intervals_dropdown = intervals - self.intervals = self.units.get_ancestor("NWBFile").intervals[self.intervals_dropdown.value] + self.intervals = nwbfile.intervals[self.intervals_dropdown.value] elif isinstance(intervals, pynwb.epoch.TimeIntervals): self.intervals = intervals - self.intervals_dropdown = None else: raise ValueError("intervals is not an allowable type") From 61fafe5bc9a8d2e6a0e95e9ca6ebff076a8ede7f Mon Sep 17 00:00:00 2001 From: bendichter Date: Mon, 24 Jan 2022 09:39:35 -0500 Subject: [PATCH 12/12] use mixin for time intervals selection --- nwbwidgets/base.py | 86 +++++++++++++++++++++++++++------------------- nwbwidgets/misc.py | 48 ++++++-------------------- 2 files changed, 61 insertions(+), 73 deletions(-) diff --git a/nwbwidgets/base.py b/nwbwidgets/base.py index c01e352d..a875c704 100644 --- a/nwbwidgets/base.py +++ b/nwbwidgets/base.py @@ -12,6 +12,7 @@ from nwbwidgets import view from pynwb import ProcessingModule from pynwb.core import NWBDataInterface, MultiContainerInterface +from pynwb.epoch import TimeIntervals from ipywidgets.widgets.interaction import show_inline_matplotlib_plots @@ -371,46 +372,61 @@ def row_to_hover_text(row): return "
".join(text_rows) -class TimeIntervalsSelector(widgets.VBox): - InnerWidget = None - - def __init__(self, input_data, **kwargs): +class TimeIntervalsSelectorMixin: + """ + Sister class must have intervals_selector_callback + """ + def set_interval_selector(self, intervals, nwbfile=None): """ - Creates a TimeInterval controller that controls InnerWidget. + If a string is given, look up that table in nwb.intervals + If a pynwb.epoch.TimeIntervals object is given, use that + If a Dropdown is given, use that as a selector + If there is no input for intervals, look at nwb.intervals + If nwb.intervals has 0 entries, render a placeholder + If nwb.intervals has 1 entry, use it + If nwb.intervals has more than one entry, create a dropdown of all available intervals Parameters ---------- - input_data: pynwb object - Pynwb object (e.g. pynwb.misc.Units) belonging to a nwbfile - that will be filtered by the TimeIntervalSelector controller. + intervals: str or pynwb.epoch.TimeIntervals or ipywidgets.DropDown or None + nwbfile: pynwb.NWBFile + + Returns + ------- + """ - super().__init__() - self.input_data = input_data - self.kwargs = kwargs - self.intervals_tables = input_data.get_ancestor("NWBFile").intervals - self.stimulus_type_dd = widgets.Dropdown( - options=list(self.intervals_tables), - description="intervals table" - ) - self.stimulus_type_dd.observe(self.stimulus_type_dd_callback) - - intervals = list(self.intervals_tables.values())[0] - inner_widget = self.InnerWidget( - self.input_data, - intervals=intervals, - **kwargs - ) - self.children = [self.stimulus_type_dd, inner_widget] - - def stimulus_type_dd_callback(self, change): - self.children = [self.stimulus_type_dd, widgets.HTML("Rendering...")] - intervals = self.intervals_tables[self.stimulus_type_dd.value] - inner_widget = self.InnerWidget( - self.input_data, - intervals=intervals, - **self.kwargs - ) - self.children = [self.stimulus_type_dd, inner_widget] + self.intervals_dropdown = None + + if isinstance(intervals, str): + if intervals == "trials": + self.intervals = nwbfile.trials + elif intervals not in nwbfile.intervals: + raise ValueError("'{intervals}' not in NWBFile.intervals") + self.intervals = nwbfile.intervals[intervals] + elif isinstance(intervals, widgets.Dropdown): + self.intervals = nwbfile.intervals[self.intervals_dropdown.value] + self.intervals_dropdown.observe(self.intervals_selector_callback) + elif isinstance(intervals, TimeIntervals): + self.intervals = intervals + elif intervals is None: + all_intervals_tables = nwbfile.intervals + trials = nwbfile.trials + if trials is not None: + all_intervals_tables.add(trials) + if len(all_intervals_tables) == 0: + self.children = [HTML("could not find intervals")] + return + elif len(all_intervals_tables) == 1: + self.intervals = list(all_intervals_tables.values())[0] + else: + self.intervals_dropdown = widgets.Dropdown( + options=list(all_intervals_tables), + description="intervals", + ) + self.intervals_dropdown.observe(self.intervals_selector_callback) + self.intervals = list(all_intervals_tables.values())[0] + else: + raise ValueError("intervals is not an allowable type") def show_multi_container_interface( diff --git a/nwbwidgets/misc.py b/nwbwidgets/misc.py index 0205be13..1e384fda 100644 --- a/nwbwidgets/misc.py +++ b/nwbwidgets/misc.py @@ -12,7 +12,7 @@ from pynwb.misc import AnnotationSeries, Units, DecompositionSeries from .analysis.spikes import compute_smoothed_firing_rate -from .base import TimeIntervalsSelector +from .base import TimeIntervalsSelectorMixin from .controllers import ( make_trial_event_controller, GroupAndSortController, @@ -268,7 +268,7 @@ def control_plot(x0, x1, ch0, ch1): return vbox -class PSTHWidget(widgets.VBox): +class PSTHWidget(widgets.VBox, TimeIntervalsSelectorMixin): def __init__( self, units: Units, @@ -283,8 +283,13 @@ def __init__( ---------- input_data: pynwb.Units intervals: str, optional - Name of intervals to use. If none are given and one is available, use that. If more than one are - available, create a dropdown + If a string is given, look up that table in nwb.intervals + If a TimeIntervals object is given, use that + If a Dropdown is given, use that as a selector + If there is no input for intervals, look at nwb.intervals + If nwb.intervals has 0 entries, render a placeholder + If nwb.intervals has 1 entry, use it + If nwb.intervals has more than one entry, create a dropdown of all available intervals unit_index: int unit_controller ntt: int @@ -295,40 +300,7 @@ def __init__( super().__init__() - if intervals is None: - all_intervals_tables = self.units.get_ancestor("NWBFile").intervals - trials = self.units.get_ancestor("NWBFile").trials - if trials is not None: - all_intervals_tables.add(trials) - if len(all_intervals_tables) == 0: - self.children = [HTML("could not find intervals")] - return - elif len(all_intervals_tables) == 1: - self.intervals = list(all_intervals_tables.values())[0] - self.intervals_dropdown = None - else: - self.intervals_dropdown = widgets.Dropdown( - options=list(all_intervals_tables), - description="intervals", - ) - self.intervals_dropdown.observe(self.intervals_selector_callback) - self.intervals = list(all_intervals_tables.values())[0] - else: - nwbfile = self.units.get_ancestor("NWBFile") - self.intervals_dropdown = None - if isinstance(intervals, str): - if intervals == "trials": - self.intervals = nwbfile.trials - elif intervals not in nwbfile.intervals: - raise ValueError("'{intervals}' not in NWBFile.intervals") - self.intervals = nwbfile.intervals[intervals] - elif isinstance(intervals, widgets.Dropdown): - self.intervals_dropdown = intervals - self.intervals = nwbfile.intervals[self.intervals_dropdown.value] - elif isinstance(intervals, pynwb.epoch.TimeIntervals): - self.intervals = intervals - else: - raise ValueError("intervals is not an allowable type") + self.set_interval_selector(intervals, units.get_ancestor("NWBFile")) if unit_controller is None: self.unit_ids = self.units.id.data[:]