diff --git a/ASCAM instructions.pdf b/ASCAM instructions.pdf new file mode 100644 index 0000000..ffbacb7 Binary files /dev/null and b/ASCAM instructions.pdf differ diff --git a/README.md b/README.md index df83148..36a8b43 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,12 @@ For launch options: If you also issue `conda install python.app` in your new environment then you can have a well-behaved Mac GUI with the following command from the parent directory of ASCAM: `pythonw /ASCAM/src/ascam.py` -20-03-01: Note, with the migration to Qt, some problems may be encountered on the Mac if you already have installations of Qt4+. A fresh environment (e.g. can help. -21-05-25: Update to Big Sur - Pyqtgraph and PyQt need Python 3.8, PySide2 5.15 and the command export QT_MAC_WANTS_LAYER=1 must be issued in the Terminal. +20-03-01: With the migration to Qt, some problems may be encountered on the Mac if you already have installations of Qt4+. A fresh environment can help. +21-05-25: Running under macOS Big Sur - Pyqtgraph and PyQt need Python 3.8, PySide2 5.15 and the command `export QT_MAC_WANTS_LAYER=1` must be issued in the Terminal. + +## Running ASCAM Note: Tables in axograph and matlab have named columns ASCAM uses these names to determine what data is dealing with. Therefore the column containing the recorded current should contain either "current", "trace" or "Ipatch", the name of the column holding the recorded piezo voltage should contain the string "piezo" and the name of the command voltage column should contain "command". There is an example raw data file of an AMPA receptor single channel patch in the ASCAM/data folder. This recording was sampled at 40 kHz. diff --git a/requirements.txt b/requirements.txt index b017348..0af2488 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pandas==1.5.* scipy==1.10.* axographio==0.3.2 pyobjc-framework-Cocoa==6.2.2;sys_platform=='Darwin' +cython<=0.29 diff --git a/src/core/filtering.py b/src/core/filtering.py index f75e246..befa402 100644 --- a/src/core/filtering.py +++ b/src/core/filtering.py @@ -1,3 +1,4 @@ +#import logging import numpy as np @@ -190,7 +191,7 @@ def predict_backward(self, data, window_width): else: raise ValueError( f"Mode {self.mode} is an unknown method for dealing\ - with edges" + with edges" ) return backward_prediction diff --git a/src/core/idealization.py b/src/core/idealization.py index 82adc03..2e53471 100644 --- a/src/core/idealization.py +++ b/src/core/idealization.py @@ -104,20 +104,25 @@ def idealize_series(self): self.idealize_episode(i) def get_events(self, time_unit="s", trace_unit="A"): - if self.all_ep_inds != self.ind_idealized: - self.idealize_series() + + # idealizing every trace, every time is killing us + # now we only get events from traces that were idealized + ##if self.all_ep_inds != self.ind_idealized: + ## self.idealize_series() event_array = np.zeros((0, 5)).astype(object) for episode in self.data.series: - # create a column containing the episode number - ep_events = Idealizer.extract_events( - self.idealization(episode.n_episode), self.time() - ) - episode_number = episode.n_episode * np.ones(len(ep_events[:, 0])) - # glue that column to the event - ep_events = np.concatenate( - (episode_number[:, np.newaxis], ep_events), axis=1 - ) - event_array = np.concatenate((event_array, ep_events), axis=0) + if self.idealization(episode.n_episode) is not None: + # create a column containing the episode number + ep_events = Idealizer.extract_events( + self.idealization(episode.n_episode), self.time() + ) + episode_number = episode.n_episode * np.ones(len(ep_events[:, 0])) + # glue that column to the event + ep_events = np.concatenate( + (episode_number[:, np.newaxis], ep_events), axis=1 + ) + event_array = np.concatenate((event_array, ep_events), axis=0) + event_array[:, 1] *= CURRENT_UNIT_FACTORS[trace_unit] event_array[:, 2:] *= TIME_UNIT_FACTORS[time_unit] return event_array diff --git a/src/core/recording.py b/src/core/recording.py index 7fdcae2..709b0ea 100644 --- a/src/core/recording.py +++ b/src/core/recording.py @@ -77,7 +77,8 @@ def from_file( else: raise ValueError(f"Cannot load from filetype {filetype}.") - recording.lists = {"All": (list(range(len(recording["raw_"]))), None)} + + recording.subsets = {"All": (list(range(len(recording["raw_"]))), None)} return recording @@ -93,36 +94,42 @@ def __init__(self, filename="", sampling_rate=4e4): # attributes for storing and managing the data self["raw_"] = [] self.current_datakey = "raw_" + + self.current_subsets = ["All"] self.current_ep_ind = 0 - # variables for user created lists of episodes - # `lists` stores the indices of the episodes in the list in the first - # element and the associated key as the second - # lists[name] = ([inds], key) - self.lists = dict() + # variables for user-created subsets of episodes + # `subsets` stores the indices of the episodes in the subset in the first + # element and the associated keyboard key as the second: + # subsets[name] = ([indices], key) + self.subsets = dict() - def select_episodes(self, datakey=None, lists=None): + def select_episodes(self, datakey=None, subsets=None): if datakey is None: datakey = self.current_datakey - if lists is None: - lists = ["All"] + if subsets is None: + subsets = self.current_subsets indices = list() - for listname in lists: - indices.extend(self.lists[listname][0]) + for subsetname in subsets: + indices.extend(self.subsets[subsetname][0]) indices = np.array(list(set(indices))) return np.array(self[datakey])[indices] - def episodes_in_lists(self, names): - if isinstance(str, names): - names = [names] + def episodes_in_subsets(self, subset_names): + if isinstance(subset_names, str): #AP corrected here 291121 + subset_names = [subset_names] indices = list() - for listname in names: - indices.extend(self.lists[listname][0]) + for subsetname in subset_names: + indices.extend(self.subsets[subsetname][0]) # remove duplicate indices indices = np.array(list(set(indices))) + #print (indices) debug_logger.debug(f"Selected episodes: {indices}") - return np.array(self.series)[indices] - + if indices != []: + return np.array(self.series)[indices] + else: + return None + @property def series(self): return self[self.current_datakey] @@ -303,7 +310,9 @@ def series_hist( """Create a histogram of all episodes in the presently selected series """ debug_logger.debug(f"series_hist") - # put all piezo traces and all current traces in lists + + # put all piezo traces and all current traces in subsets + piezos = [episode.piezo for episode in self.series] traces = [episode.trace for episode in self.series] trace_list = [] @@ -411,7 +420,9 @@ def _load_from_pickle(recording): def export_idealization( self, filepath, - lists_to_save, + + subsets_to_save, + time_unit, trace_unit, amplitudes, @@ -424,7 +435,7 @@ def export_idealization( if not filepath.endswith(".csv"): filepath += ".csv" - episodes = self.select_episodes(lists=lists_to_save) + episodes = self.select_episodes(subsets=subsets_to_save) export_array = np.zeros( shape=(len(episodes) + 1, episodes[0].idealization.size) @@ -451,7 +462,9 @@ def export_matlab( self, filepath, datakey, - lists_to_save, + + subsets_to_save, + save_piezo, save_command, time_unit="s", @@ -459,11 +472,12 @@ def export_matlab( piezo_unit="V", command_unit="V", ): - """Export all the episodes in the givens list(s) from the given series + + """Export all the episodes in the givens subset(s) from the given series (only one) to a matlab file.""" debug_logger.debug( f"export_matlab:\n" - f"saving the lists: {lists_to_save}\n" + f"saving the subsets: {subsets_to_save}\n" f"of series {datakey}\n" f"save piezo: {save_piezo}; " "save command: {save_command}\n" @@ -478,7 +492,9 @@ def export_matlab( export_dict = dict() export_dict["time"] = self["raw_"][0].time * TIME_UNIT_FACTORS[time_unit] fill_length = len(str(len(self[datakey]))) - episodes = self.select_episodes(datakey, lists_to_save) + + episodes = self.select_episodes(datakey, subsets_to_save) + # # get the episodes we want to save # indices = list() # for listname in lists_to_save: @@ -498,19 +514,27 @@ def export_matlab( ) io.savemat(filepath, export_dict) - def export_axo(self, filepath, datakey, lists_to_save, save_piezo, save_command): + + def export_axo(self, filepath, datakey, subsets_to_save, save_piezo, save_command): + """Export data to an axograph file. Argument: filepath - location where the file is to be stored datakey - series that should be exported +<<<<<<< HEAD lists_to_save - the user-created lists of episodes that should be +======= + subsets_to_save - the user-created subsets of episodes that should be +>>>>>>> subsets includes save_piezo - if true piezo data will be exported save_command - if true command voltage data will be exported""" debug_logger.debug( f"export_axo:\n" - f"saving the lists: {lists_to_save}\n" + + f"saving the subsets: {subsets_to_save}\n" + f"of series {datakey}\n" f"save piezo: {save_piezo}; save command: {save_command}\n" f"saving to destination: {filepath}" @@ -529,7 +553,8 @@ def export_axo(self, filepath, datakey, lists_to_save, save_piezo, save_command) data_list = [self.episode().time] # get the episodes we want to save - episodes = self.select_episodes(datakey, lists_to_save) + + episodes = self.select_episodes(datakey, subsets_to_save) for episode in episodes: data_list.append(np.array(episode.trace)) @@ -544,7 +569,8 @@ def export_axo(self, filepath, datakey, lists_to_save, save_piezo, save_command) file.write(filepath) def create_first_activation_table( - self, datakey=None, time_unit="ms", lists_to_save=None, trace_unit="pA" + + self, datakey=None, time_unit="ms", subsets_to_save=None, trace_unit="pA" ): if datakey is None: datakey = self.current_datakey @@ -558,7 +584,9 @@ def create_first_activation_table( episode.first_activation_amplitude * CURRENT_UNIT_FACTORS[trace_unit], ) - for episode in self.select_episodes(datakey, lists_to_save) + + for episode in self.select_episodes(datakey, subsets_to_save) + ] ) return export_array.astype(object) @@ -587,12 +615,16 @@ def export_first_activation( filepath, datakey=None, time_unit="ms", - lists_to_save=None, + + subsets_to_save=None, + trace_unit="pA", ): """Export csv file of first activation times.""" export_array = self.create_first_activation_table( - datakey, time_unit, lists_to_save, trace_unit + + datakey, time_unit, subsets_to_save, trace_unit + ) header = [ "Episode Number", diff --git a/src/core/savedata.py b/src/core/savedata.py index e9753f2..865f467 100644 --- a/src/core/savedata.py +++ b/src/core/savedata.py @@ -1,6 +1,9 @@ from scipy import io import os import json +import pickle +from ..utils.tools import parse_filename + def save_metadata(data, filename): @@ -96,8 +99,10 @@ def save_data(data, filename="", filetype="mat", save_piezo=True, save_command=T save_piezo=save_piezo, save_command=save_command, ) + # elif filetype == "pkl": # return_status = save_pickle(data=data, filepath=filepath) + else: print('Can only save as ".mat"!') diff --git a/src/gui/analysis_widgets.py b/src/gui/analysis_widgets.py index 64651c7..86584f1 100644 --- a/src/gui/analysis_widgets.py +++ b/src/gui/analysis_widgets.py @@ -5,18 +5,25 @@ from PySide2 import QtCore from PySide2.QtWidgets import ( QApplication, + + QSizePolicy, + QComboBox, QFileDialog, QDialog, + QTableView, + QGridLayout, QTabWidget, QWidget, QVBoxLayout, + QHBoxLayout, + QCheckBox, QLineEdit, QToolButton, QTabBar, QPushButton, - QLabel, + QLabel ) from .io_widgets import ExportIdealizationDialog @@ -54,6 +61,11 @@ def create_widgets(self): self.calc_button = QPushButton("Calculate idealization") self.calc_button.clicked.connect(self.calculate_click) self.layout.addWidget(self.calc_button) + + self.calc_subset_button = QPushButton("Calculate idealization for subset") + self.calc_subset_button.clicked.connect(self.calculate_subset_click) + self.layout.addWidget(self.calc_subset_button) + self.events_button = QPushButton("Show Table of Events") self.events_button.clicked.connect(self.create_event_frame) @@ -132,6 +144,21 @@ def idealization(self, n_episode=None): def time(self, n_episode=None): return self.current_tab.idealization_cache.time(n_episode) + def calculate_subset_click(self): + self.get_params() + ### ask which subsets are checked + selected_subsets,_ = self.main.ep_frame.subset_frame.subsets_check() + if selected_subsets: + episodes = self.main.data.episodes_in_subsets(selected_subsets) + for ep in episodes: + print (f"Idealizing episode {ep.n_episode}") + self.main.data.current_ep_ind = ep.n_episode + self.idealize_episode() + self.main.plot_frame.update_episode() + + else: + print (f"No subsets were selected, nothing to do.") + def calculate_click(self): self.get_params() self.idealize_episode() diff --git a/src/gui/episode_frame.py b/src/gui/episode_frame.py index 87852eb..3d04993 100644 --- a/src/gui/episode_frame.py +++ b/src/gui/episode_frame.py @@ -36,8 +36,12 @@ def __init__(self, main, *args, **kwargs): self.keyPressed.connect(self.key_pressed) def create_widgets(self): - self.list_frame = ListFrame(self) - self.layout.addWidget(self.list_frame) + + self.subset_frame = SubsetFrame(self) + self.layout.addWidget(self.subset_frame) + + + ### this box selects which series (according to processing) self.series_selection = QComboBox() self.series_selection.setDuplicatesEnabled(False) @@ -83,51 +87,81 @@ def keyPressEvent(self, event): self.keyPressed.emit(event.text()) def key_pressed(self, key): - assigned_keys = [x[1] for x in self.main.data.lists.values()] + + assigned_keys = [x[1] for x in self.main.data.subsets.values()] if key in assigned_keys: - for l in self.list_frame.lists: + for l in self.subset_frame.subsets: + if f"[{key}]" in l.text(): name = l.text().split()[0] for item in self.ep_list.selectedItems(): index = self.ep_list.row(item) - self.list_frame.add_to_list(name, key, index) + + self.subset_frame.add_to_subset(name, key, index) -class ListFrame(QWidget): +class SubsetFrame(QWidget): keyPressed = QtCore.Signal(str) def __init__(self, parent, *args, **kwargs): - super(ListFrame, self).__init__(*args, **kwargs) + super(SubsetFrame, self).__init__(*args, **kwargs) + self.parent = parent self.layout = QVBoxLayout() self.setLayout(self.layout) - self.lists = [] - self.new_list("All") + self.subsets = [] + self.new_subset("All", checked_state=True) - self.new_button = QPushButton("New List") + self.new_button = QPushButton("New Subset") self.new_button.clicked.connect(self.create_dialog) self.layout.addWidget(self.new_button) - def new_list(self, name, key=None): + def new_subset(self, name, key=None, checked_state=False): label = f"{name} [{key}]" if key is not None else name - check_box = QCheckBox(label) - check_box.setChecked(True) - self.lists.append(check_box) - self.layout.insertWidget(0, check_box) - self.parent.main.data.lists[name] = ([], key) + sub_check_box = QCheckBox(label) + sub_check_box.setChecked(checked_state) + sub_check_box.stateChanged.connect(self.subset_click) + self.subsets.append(sub_check_box) + self.layout.insertWidget(0, sub_check_box) + # create an empty subset + self.parent.main.data.subsets[name] = ([], key) debug_logger.debug( - f"added list '{name}' with key '{key}'\n" - "lists are now:\n" - f"{self.parent.main.data.lists}" + f"added subset '{name}' with keypress '{key}'\n" + "subsets are now:\n" + f"{self.parent.main.data.subsets}" ) - def add_to_list(self, name, key, index): - if index not in self.parent.main.data.lists[name][0]: - self.parent.main.data.lists[name][0].append(index) + def subsets_check(self): + + checked_list =[] + unchecked_list =[] + + for b in self.subsets: + if b.isChecked(): + checked_list.append(b.text().split(" ")[0]) + else : + unchecked_list.append(b.text().split(" ")[0]) + + return checked_list, unchecked_list + + def subset_click(self, state): + print("clicked state", state, self.sender().text()) + checked_list,unchecked_list = self.subsets_check() + + print("Checked subset boxes:", checked_list) + print("Unchecked subset boxes:", unchecked_list) + self.parent.ep_list.populate() + + def add_to_subset(self, name, key, index): + # unfortunately this adds on list index not on actually clicked episode + # also tags letters do not refresh with episode list + if index not in self.parent.main.data.subsets[name][0]: + self.parent.main.data.subsets[name][0].append(index) assigned_keys = [ x[1] - for x in self.parent.main.data.lists.values() + for x in self.parent.main.data.subsets.values() + if index in x[0] and x[1] is not None ] n = f"Episode {self.parent.main.data.series[index].n_episode} " @@ -139,14 +173,16 @@ def add_to_list(self, name, key, index): n += s.rjust(20 - len(n), " ") self.parent.ep_list.item(index).setText(n) ana_logger.debug( - f"added episode {self.parent.main.data.series[index].n_episode} to list {name}" + + f"added episode {self.parent.main.data.series[index].n_episode} to subset {name}" ) else: - self.parent.main.data.lists[name][0].remove(index) - n = self.parent.ep_list.item(index).text() + self.parent.main.data.subsets[name][0].remove(index) + #n = self.parent.ep_list.item(index).text() as Ece said, this is overwritten immediately below without being used. assigned_keys = [ x[1] - for x in self.parent.main.data.lists.values() + for x in self.parent.main.data.subsets.values() + if index in x[0] and x[1] is not None ] assigned_keys.sort() @@ -158,12 +194,16 @@ def add_to_list(self, name, key, index): n += s.rjust(20 - len(n), " ") self.parent.ep_list.item(index).setText(n) ana_logger.debug( - f"removed episode {self.parent.main.data.series[index].n_episode} from list {name}" + + f"removed episode {self.parent.main.data.series[index].n_episode} from subset {name}" + ) def create_dialog(self): self.dialog = QDialog() - self.dialog.setWindowTitle("Add List") + + self.dialog.setWindowTitle("Add subset") + layout = QGridLayout() self.dialog.setLayout(layout) @@ -185,7 +225,9 @@ def create_dialog(self): self.dialog.exec_() def ok_clicked(self): - self.new_list(self.name_entry.text(), self.key_entry.text()) + + self.new_subset(self.name_entry.text(), self.key_entry.text()) + self.dialog.close() def keyPressEvent(self, event): @@ -220,10 +262,15 @@ def populate(self): self.currentItemChanged.disconnect(self.on_item_click) self.clear() if self.parent.main.data is not None: - debug_logger.debug("inserting data") - self.addItems( - [f"Episode {e.n_episode}" for e in self.parent.main.data.series] - ) + + selected_subsets,_ = self.parent.subset_frame.subsets_check() + episodes = self.parent.main.data.episodes_in_subsets(selected_subsets) + if episodes is not None: + debug_logger.debug("inserting data") + self.addItems( + [f"Episode {e.n_episode}" for e in episodes] + ) + self.setCurrentRow(0) self.currentItemChanged.connect( self.on_item_click, type=QtCore.Qt.DirectConnection diff --git a/src/gui/first_activation_frame.py b/src/gui/first_activation_frame.py index 29760a1..d52cf36 100644 --- a/src/gui/first_activation_frame.py +++ b/src/gui/first_activation_frame.py @@ -1,16 +1,25 @@ import logging +import pyqtgraph as pg + from PySide2 import QtCore from PySide2.QtWidgets import ( QLabel, QComboBox, + QWidget, #added by subsets revision + QVBoxLayout, #added by subsets revision + QHBoxLayout, #added by subsets revision QCheckBox, QLineEdit, QToolButton, QPushButton, ) + +#from ..gui import ExportFADialog #vague? +from ..utils import round_off_tables from ..gui.io_widgets import ExportFADialog, ExportFEDialog + from ..utils.widgets import EntryWidget, TableFrame from ..constants import CURRENT_UNIT_FACTORS diff --git a/src/gui/io_widgets.py b/src/gui/io_widgets.py index e1e3911..f535e9d 100644 --- a/src/gui/io_widgets.py +++ b/src/gui/io_widgets.py @@ -13,6 +13,10 @@ from ..core import Recording + + +###from ..constants import TIME_UNIT_FACTORS, CURRENT_UNIT_FACTORS, VOLTAGE_UNIT_FACTORS + from ..utils.widgets import EntryWidget diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index b2ec42e..50938b1 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -1,7 +1,10 @@ import logging import os + +from PySide2.QtCore import Qt from PySide2.QtWidgets import ( + QDockWidget, QGridLayout, QWidget, QMainWindow, @@ -10,7 +13,9 @@ QSizePolicy, ) +# removed ExportFADialog here, not sure if that's OK from ..gui import ( + ExportDialog, OpenFileDialog, FilterFrame,