diff --git a/.gitignore b/.gitignore index 33eff52..b38efc9 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,8 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +# ignore created data files +*.nix + +# ignore reource.py as it is created by pyside6-rcc resources.qrc -o resources.py +resources.py \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index aa7864d..b93f79d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,11 +16,12 @@ classifiers = [ "Intended Audience :: End Users/Desktop", ] include = [ - { path = "pyproject.toml" } + { path = "pyproject.toml" }, + "pyrelacs/resources.py" ] [tool.poetry.dependencies] -python = "^3.12" +python = "^3.10" uldaq = "^1.2.3" typer = "^0.12.5" matplotlib = "^3.9.2" diff --git a/pyrelacs/__init__.py b/pyrelacs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyrelacs/app.py b/pyrelacs/app.py index bbb7124..a95c4da 100644 --- a/pyrelacs/app.py +++ b/pyrelacs/app.py @@ -1,193 +1,21 @@ -import pathlib -from PyQt6.QtGui import QAction import sys -from PyQt6.QtCore import QSize, QThreadPool, QSettings -from PyQt6.QtWidgets import ( - QApplication, - QGridLayout, - QPushButton, - QToolBar, - QWidget, - QMainWindow, - QPlainTextEdit, -) -import pyqtgraph as pg -import uldaq -from IPython import embed -from scipy.signal import welch, find_peaks -import numpy as np -import nixio as nix - -from pyrelacs.util.logging import config_logging -import pyrelacs.info as info -from pyrelacs.worker import Worker -from pyrelacs.repros.repros import Repro +from PyQt6.QtCore import QSettings +from PyQt6.QtWidgets import QApplication +from . import info +from .ui.mainwindow import PyRelacs +from .util.logging import config_logging log = config_logging() - -class PyRelacs(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("PyRelacs") - self.beat_plot = pg.PlotWidget() - self.power_plot = pg.PlotWidget() - - self.threadpool = QThreadPool() - self.repros = Repro() - - self.daq_connect_button = QPushButton("Connect Daq") - self.daq_connect_button.setCheckable(True) - self.daq_connect_button.clicked.connect(self.connect_dac) - - self.daq_disconnect_button = QPushButton("Disconnect Daq") - self.daq_disconnect_button.setCheckable(True) - self.daq_disconnect_button.clicked.connect(self.disconnect_dac) - - self.plot_calibration_button = QPushButton("Plot Calibration") - self.plot_calibration_button.setCheckable(True) - self.plot_calibration_button.clicked.connect(self.plot_calibration) - - layout = QGridLayout() - layout.addWidget(self.plot_calibration_button, 0, 0) - layout.addWidget(self.daq_disconnect_button, 0, 1) - layout.addWidget(self.beat_plot, 2, 0, 1, 2) - layout.addWidget(self.power_plot, 3, 0, 1, 2) - - self.toolbar = QToolBar("Repros") - self.addToolBar(self.toolbar) - self.repros_to_toolbar() - - # self.setFixedSize(QSize(400, 300)) - widget = QWidget() - widget.setLayout(layout) - self.setCentralWidget(widget) - - self.nix_file = nix.File.open( - str(pathlib.Path(__file__).parent / "data"), nix.FileMode.ReadOnly - ) - - def plot_calibration(self): - def decibel(power, ref_power=1.0, min_power=1e-20): - """Transform power to decibel relative to ref_power. - - \\[ decibel = 10 \\cdot \\log_{10}(power/ref\\_power) \\] - Power values smaller than `min_power` are set to `-np.inf`. - - Parameters - ---------- - power: float or array - Power values, for example from a power spectrum or spectrogram. - ref_power: float or None or 'peak' - Reference power for computing decibel. - If set to `None` or 'peak', the maximum power is used. - min_power: float - Power values smaller than `min_power` are set to `-np.inf`. - - Returns - ------- - decibel_psd: array - Power values in decibel relative to `ref_power`. - """ - if np.isscalar(power): - tmp_power = np.array([power]) - decibel_psd = np.array([power]) - else: - tmp_power = power - decibel_psd = power.copy() - if ref_power is None or ref_power == "peak": - ref_power = np.max(decibel_psd) - decibel_psd[tmp_power <= min_power] = float("-inf") - decibel_psd[tmp_power > min_power] = 10.0 * np.log10( - decibel_psd[tmp_power > min_power] / ref_power - ) - if np.isscalar(power): - return decibel_psd[0] - else: - return decibel_psd - - block = self.nix_file.blocks[0] - colors = ["red", "green", "blue", "black", "yellow"] - for i, (stim, fish) in enumerate( - zip(list(block.data_arrays)[::2], list(block.data_arrays)[1::2]) - ): - beat = stim[:] + fish[:] - beat_squared = beat**2 - - f, powerspec = welch(beat, fs=40_000.0) - # powerspec = decibel(powerspec) - - f_sq, powerspec_sq = welch(beat_squared, fs=40_000.0) - powerspec_sq = decibel(powerspec_sq) - peaks = find_peaks(powerspec_sq, prominence=20)[0] - pen = pg.mkPen(colors[i]) - self.beat_plot.plot( - np.arange(0, len(beat)) / 40_000.0, - beat, - pen=pen, - # name=stim.name, - ) - self.power_plot.plot(f, powerspec, pen=pen) - # self.power_plot.plot(f[peaks], powerspec_sq[peaks], pen=None, symbol="x") - - def connect_dac(self): - devices = uldaq.get_daq_device_inventory(uldaq.InterfaceType.USB) - try: - self.daq_device = uldaq.DaqDevice(devices[0]) - log.debug(f"Found daq devices {len(devices)}, connecting to the first one") - self.daq_device.connect() - log.debug("Connected") - except IndexError: - log.debug("DAQ is not connected, closing") - QApplication.quit() - self.daq_connect_button.setDisabled(True) - - def disconnect_dac(self): - try: - log.debug(f"{self.daq_device}") - self.daq_device.disconnect() - self.daq_device.release() - log.debug(f"{self.daq_device}") - self.daq_disconnect_button.setDisabled(True) - self.daq_connect_button.setEnabled(True) - except AttributeError: - log.debug("DAQ was not connected") - - def repros_to_toolbar(self): - repro_names, file_names = self.repros.names_of_repros() - for rep, fn in zip(repro_names, file_names): - individual_repro_button = QAction(rep, self) - individual_repro_button.setStatusTip("Button") - individual_repro_button.triggered.connect( - lambda checked, n=rep, f=fn: self.run_repro(n, f) - ) - self.toolbar.addAction(individual_repro_button) - - def run_repro(self, n, fn): - log.debug(f"Running repro {n} in the file {fn}") - worker = Worker(self.repros.run_repro, self.nix_file, n, fn) - worker.signals.result.connect(self.print_output) - worker.signals.finished.connect(self.thread_complete) - worker.signals.progress.connect(self.progress_fn) - - self.threadpool.start(worker) - - def print_output(self, s): - print(s) - - def thread_complete(self): - print("THREAD COMPLETE!") - - def progress_fn(self, n): - print("%d%% done" % n) - +from . import resources def main(): app = QApplication(sys.argv) app.setApplicationName(info.NAME) app.setApplicationVersion(str(info.VERSION)) app.setOrganizationDomain(info.ORGANIZATION) + # app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus, False) # read window settings settings = QSettings(info.ORGANIZATION, info.NAME) diff --git a/pyrelacs/devices/__init__.py b/pyrelacs/devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyrelacs/icons/connect.png b/pyrelacs/icons/connect.png new file mode 100644 index 0000000..0ea0bc4 --- /dev/null +++ b/pyrelacs/icons/connect.png @@ -0,0 +1,738 @@ + + + + + PylonRecorder/connect.png at master - PylonRecorder - Neuroetho git repository + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+ + + + + +
+ +
+ + + + + + +
+
+ +
+ + + + + + + +
+
+ + + + + 1 + +
+
+ + +
+
+ + + + + 0 + +
+
+ + + + + + +
+ +
+ + + +
+ +
+ + +
+ + + Code + + + + + + Issues + + + + + + + + + Pull Requests + + + + + + + + + + + + + + Releases + + + + + + + Wiki + + + + + + + + Activity + + + + + + + + + Settings + + +
+ +
+
+
+
+ +
+ + + + + + + + + +
+ +
+ + +
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + PylonRecorder/images/connect.png + +
+
+ + + +
+
+ +
+ + +
+
+ + + + + Jan Grewe + + + + bc09438f2a + + + + + + [icons] update and new + + + + +
+ + +
+ 2020-03-13 11:58:01 +01:00 +
+ + +
+ + +

+
+ +
+ + + +
+ 6.9 KiB +
+ + + + + + + +
+ 200x200px +
+ +
+ + +
+
+ + +
+ Raw + + Permalink + + + History + +
+ + + + + + + + + + + + + + + + + +
+

+
+ + + + +
+ +
+ + + +
+ +
+
+
+ + +
+
+ + + + +
+ + + + + + + + + + + + diff --git a/pyrelacs/icons/disconnect.png b/pyrelacs/icons/disconnect.png new file mode 100644 index 0000000..5ffb319 --- /dev/null +++ b/pyrelacs/icons/disconnect.png @@ -0,0 +1,738 @@ + + + + + PylonRecorder/disconnect.png at master - PylonRecorder - Neuroetho git repository + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+ + + + + +
+ +
+ + + + + + +
+
+ +
+ + + + + + + +
+
+ + + + + 1 + +
+
+ + +
+
+ + + + + 0 + +
+
+ + + + + + +
+ +
+ + + +
+ +
+ + +
+ + + Code + + + + + + Issues + + + + + + + + + Pull Requests + + + + + + + + + + + + + + Releases + + + + + + + Wiki + + + + + + + + Activity + + + + + + + + + Settings + + +
+ +
+
+
+
+ +
+ + + + + + + + + +
+ +
+ + +
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + PylonRecorder/images/disconnect.png + +
+
+ + + +
+
+ +
+ + +
+
+ + + + + Jan Grewe + + + + bc09438f2a + + + + + + [icons] update and new + + + + +
+ + +
+ 2020-03-13 11:58:01 +01:00 +
+ + +
+ + +

+
+ +
+ + + +
+ 5.3 KiB +
+ + + + + + + +
+ 200x200px +
+ +
+ + +
+
+ + +
+ Raw + + Permalink + + + History + +
+ + + + + + + + + + + + + + + + + +
+

+
+ + + + +
+ +
+ + + +
+ +
+
+
+ + +
+
+ + + + +
+ + + + + + + + + + + + diff --git a/pyrelacs/icons/exit.png b/pyrelacs/icons/exit.png new file mode 100644 index 0000000..1be01ac --- /dev/null +++ b/pyrelacs/icons/exit.png @@ -0,0 +1,738 @@ + + + + + PylonRecorder/exit.png at master - PylonRecorder - Neuroetho git repository + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+ + + + + +
+ +
+ + + + + + +
+
+ +
+ + + + + + + +
+
+ + + + + 1 + +
+
+ + +
+
+ + + + + 0 + +
+
+ + + + + + +
+ +
+ + + +
+ +
+ + +
+ + + Code + + + + + + Issues + + + + + + + + + Pull Requests + + + + + + + + + + + + + + Releases + + + + + + + Wiki + + + + + + + + Activity + + + + + + + + + Settings + + +
+ +
+
+
+
+ +
+ + + + + + + + + +
+ +
+ + +
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + PylonRecorder/images/exit.png + +
+
+ + + +
+
+ +
+ + +
+ + + +
+ 2020-03-16 11:01:18 +01:00 +
+ + +
+ + +

+
+ +
+ + + +
+ 7.0 KiB +
+ + + + + + + +
+ 200x200px +
+ +
+ + +
+
+ + +
+ Raw + + Permalink + + + History + +
+ + + + + + + + + + + + + + + + + +
+

+
+ + + + +
+ +
+ + + +
+ +
+
+
+ + +
+
+ + + + +
+ + + + + + + + + + + + diff --git a/pyrelacs/icons/record.png b/pyrelacs/icons/record.png new file mode 100644 index 0000000..4ef1915 --- /dev/null +++ b/pyrelacs/icons/record.png @@ -0,0 +1,738 @@ + + + + + PylonRecorder/record.png at master - PylonRecorder - Neuroetho git repository + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+ + + + + +
+ +
+ + + + + + +
+
+ +
+ + + + + + + +
+
+ + + + + 1 + +
+
+ + +
+
+ + + + + 0 + +
+
+ + + + + + +
+ +
+ + + +
+ +
+ + +
+ + + Code + + + + + + Issues + + + + + + + + + Pull Requests + + + + + + + + + + + + + + Releases + + + + + + + Wiki + + + + + + + + Activity + + + + + + + + + Settings + + +
+ +
+
+
+
+ +
+ + + + + + + + + +
+ +
+ + +
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + PylonRecorder/images/record.png + +
+
+ + + +
+
+ +
+ + +
+
+ + + + + Jan Grewe + + + + bc09438f2a + + + + + + [icons] update and new + + + + +
+ + +
+ 2020-03-13 11:58:01 +01:00 +
+ + +
+ + +

+
+ +
+ + + +
+ 4.9 KiB +
+ + + + + + + +
+ 200x200px +
+ +
+ + +
+
+ + +
+ Raw + + Permalink + + + History + +
+ + + + + + + + + + + + + + + + + +
+

+
+ + + + +
+ +
+ + + +
+ +
+
+
+ + +
+
+ + + + +
+ + + + + + + + + + + + diff --git a/pyrelacs/icons/relacstuxheader.png b/pyrelacs/icons/relacstuxheader.png new file mode 100644 index 0000000..cb9a9cd Binary files /dev/null and b/pyrelacs/icons/relacstuxheader.png differ diff --git a/pyrelacs/icons/stop.png b/pyrelacs/icons/stop.png new file mode 100644 index 0000000..9b2ab09 --- /dev/null +++ b/pyrelacs/icons/stop.png @@ -0,0 +1,738 @@ + + + + + PylonRecorder/stop.png at master - PylonRecorder - Neuroetho git repository + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+ + + + + +
+ +
+ + + + + + +
+
+ +
+ + + + + + + +
+
+ + + + + 1 + +
+
+ + +
+
+ + + + + 0 + +
+
+ + + + + + +
+ +
+ + + +
+ +
+ + +
+ + + Code + + + + + + Issues + + + + + + + + + Pull Requests + + + + + + + + + + + + + + Releases + + + + + + + Wiki + + + + + + + + Activity + + + + + + + + + Settings + + +
+ +
+
+
+
+ +
+ + + + + + + + + +
+ +
+ + +
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + PylonRecorder/images/stop.png + +
+
+ + + +
+
+ +
+ + +
+
+ + + + + Jan Grewe + + + + bc09438f2a + + + + + + [icons] update and new + + + + +
+ + +
+ 2020-03-13 11:58:01 +01:00 +
+ + +
+ + +

+
+ +
+ + + +
+ 2.2 KiB +
+ + + + + + + +
+ 200x200px +
+ +
+ + +
+
+ + +
+ Raw + + Permalink + + + History + +
+ + + + + + + + + + + + + + + + + +
+

+
+ + + + +
+ +
+ + + +
+ +
+
+
+ + +
+
+ + + + +
+ + + + + + + + + + + + diff --git a/pyrelacs/info.py b/pyrelacs/info.py index 46ee0a1..cd2e46e 100644 --- a/pyrelacs/info.py +++ b/pyrelacs/info.py @@ -3,6 +3,7 @@ import pathlib def load_project_settings(project_root): + print(project_root) # Read the pyproject.toml file with open(pathlib.Path.joinpath(project_root, "pyproject.toml"), "r") as f: pyproject_content = f.read() diff --git a/pyrelacs/repros/repros.py b/pyrelacs/repros/repros.py index 842e3cd..11c657f 100644 --- a/pyrelacs/repros/repros.py +++ b/pyrelacs/repros/repros.py @@ -1,15 +1,16 @@ import sys -import importlib.util import ast import pathlib from typing import Tuple from IPython import embed import nixio as nix -from pyrelacs.util.logging import config_logging +import importlib.util +from pyrelacs.util.logging import config_logging log = config_logging() +from IPython import embed class Repro: """ diff --git a/pyrelacs/resources.qrc b/pyrelacs/resources.qrc new file mode 100644 index 0000000..da26e63 --- /dev/null +++ b/pyrelacs/resources.qrc @@ -0,0 +1,9 @@ + + + icons/exit.png + icons/connect.png + icons/disconnect.png + icons/record.png + icons/stop.png + + \ No newline at end of file diff --git a/pyrelacs/ui/__init__.py b/pyrelacs/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyrelacs/ui/about.py b/pyrelacs/ui/about.py new file mode 100644 index 0000000..1636d65 --- /dev/null +++ b/pyrelacs/ui/about.py @@ -0,0 +1,57 @@ +import pathlib + +from PyQt6.QtGui import QPixmap +from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QLabel, QVBoxLayout, QWidget +from PyQt6.QtCore import Qt + + +class AboutDialog(QDialog): + + def __init__(self, parent=None) -> None: + super().__init__(parent=parent) + self.setModal(True) + about = About(self) + self.setLayout(QVBoxLayout()) + self.layout().addWidget(about) + bbox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) + bbox.accepted.connect(self.accept) + self.layout().addWidget(bbox) + + +class About(QWidget): + + def __init__(self, parent=None) -> None: + super().__init__(parent=parent) + self.setLayout(QVBoxLayout()) + + heading = QLabel("pyRelacs") + font = heading.font() + font.setPointSize(18) + font.setBold(True) + heading.setFont(font) + heading.setAlignment(Qt.AlignmentFlag.AlignCenter) + subheading = QLabel("relacsed electrophysiological recordings") + subheading.setAlignment(Qt.AlignmentFlag.AlignCenter) + nix_link = QLabel("https://github.com/relacs") + nix_link.setOpenExternalLinks(True) + nix_link.setAlignment(Qt.AlignmentFlag.AlignCenter) + rtd_link = QLabel("https://relacs.net") + rtd_link.setOpenExternalLinks(True) + rtd_link.setAlignment(Qt.AlignmentFlag.AlignCenter) + + iconlabel = QLabel() + _root = pathlib.Path(__file__).parent.parent + pixmap = QPixmap(str(pathlib.Path.joinpath(_root, "icons/relacstuxheader.png"))) + s = pixmap.size() + new_height = int(s.height() * 300/s.width()) + pixmap = pixmap.scaled(300, new_height, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.FastTransformation) + iconlabel.setPixmap(pixmap) + iconlabel.setMaximumWidth(300) + iconlabel.setAlignment(Qt.AlignmentFlag.AlignCenter) + iconlabel.setScaledContents(True) + + self.layout().addWidget(heading) + self.layout().addWidget(subheading) + self.layout().addWidget(iconlabel) + self.layout().addWidget(nix_link) + self.layout().addWidget(rtd_link) diff --git a/pyrelacs/ui/mainwindow.py b/pyrelacs/ui/mainwindow.py new file mode 100644 index 0000000..24dc71e --- /dev/null +++ b/pyrelacs/ui/mainwindow.py @@ -0,0 +1,270 @@ +from PyQt6.QtGui import QAction, QIcon, QKeySequence +from PyQt6.QtCore import Qt, QSize, QThreadPool +from PyQt6.QtWidgets import ( + QGridLayout, + QPushButton, + QToolBar, + QWidget, + QMainWindow, + QPlainTextEdit, + QMenuBar, + QStatusBar +) +import uldaq +import numpy as np +import nixio as nix +import pyqtgraph as pg + +from pathlib import Path as path +from scipy.signal import welch, find_peaks + +from ..worker import Worker +from ..repros.repros import Repro +from ..util.logging import config_logging +from .about import AboutDialog +log = config_logging() +_root = path(__file__).parent.parent + +from IPython import embed + +class PyRelacs(QMainWindow): + def __init__(self): + super().__init__() + # self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) # Ensure icons are displayed with text + self.setWindowTitle("PyRelacs") + self.beat_plot = pg.PlotWidget() + self.power_plot = pg.PlotWidget() + + self.threadpool = QThreadPool() + self.repros = Repro() + + self.text = QPlainTextEdit() + self.text.setReadOnly(True) + + self.setMenuBar(QMenuBar(self)) + self.setStatusBar(QStatusBar(self)) + self.create_actions() + self.create_buttons() + self.create_toolbars() + + layout = QGridLayout() + layout.addWidget(self.plot_calibration_button, 0, 0) + layout.addWidget(self.daq_disconnect_button, 0, 1) + layout.addWidget(self.beat_plot, 2, 0, 1, 2) + layout.addWidget(self.power_plot, 3, 0, 1, 2) + + self.toolbar = QToolBar("Repros") + self.addToolBar(self.toolbar) + self.repros_to_toolbar() + + # self.setFixedSize(QSize(400, 300)) + widget = QWidget() + widget.setLayout(layout) + self.setCentralWidget(widget) + + filename = path.joinpath(path.cwd(), "data.nix") + self.nix_file = nix.File.open( + str(filename), nix.FileMode.Overwrite + ) + + def create_actions(self): + self._rlx_exitaction = QAction(QIcon(str(path.joinpath(_root, "icons/exit.png"))), "Exit", self) + self._rlx_exitaction.setStatusTip("Close relacs") + self._rlx_exitaction.setShortcut(QKeySequence("Alt+q")) + self._rlx_exitaction.triggered.connect(self.on_exit) + + self._rlx_aboutaction = QAction("about") + self._rlx_aboutaction.setStatusTip("Show about dialog") + self._rlx_aboutaction.setEnabled(True) + self._rlx_aboutaction.triggered.connect(self.on_about) + + self._daq_connectaction = QAction(QIcon(str(path.joinpath(_root, "icons/connect.png"))), "Connect DAQ", self) + self._daq_connectaction.setStatusTip("Connect to daq device") + # self._daq_connectaction.setShortcut(QKeySequence("Alt+d")) + self._daq_connectaction.triggered.connect(self.connect_dac) + + self._daq_disconnectaction = QAction(QIcon(str(path.joinpath(_root, "icons/disconnect.png"))), "Disconnect DAQ", self) + self._daq_disconnectaction.setStatusTip("Disconnect the DAQ device") + # self._daq_connectaction.setShortcut(QKeySequence("Alt+d")) + self._daq_disconnectaction.triggered.connect(self.disconnect_dac) + + self._daq_calibaction = QAction(QIcon(str(path.joinpath(_root, "icons/calibration.png"))), "Plot calibration", self) + self._daq_calibaction.setStatusTip("Calibrate the attenuator device") + # self._daq_calibaction.setShortcut(QKeySequence("Alt+d")) + self._daq_calibaction.triggered.connect(self.plot_calibration) + self.create_menu() + + def create_menu(self): + menu = self.menuBar() + file_menu = menu.addMenu("&File") + file_menu.addAction(self._rlx_exitaction) + file_menu.addAction(self._rlx_aboutaction) + + device_menu = menu.addMenu("&DAQ") + device_menu.addAction(self._daq_connectaction) + device_menu.addAction(self._daq_disconnectaction) + device_menu.addSeparator() + device_menu.addAction(self._daq_calibaction) + + help_menu = menu.addMenu("&Help") + help_menu.addSeparator() + # help_menu.addAction(self._help_action) + self.setMenuBar(menu) + + def create_toolbars(self): + rlx_toolbar = QToolBar("Relacs") + rlx_toolbar.addAction(self._rlx_exitaction) + rlx_toolbar.setIconSize(QSize(24, 24)) + + self.addToolBar(Qt.ToolBarArea.TopToolBarArea, rlx_toolbar) + daq_toolbar = QToolBar("DAQ") + daq_toolbar.addAction(self._daq_connectaction) + daq_toolbar.addAction(self._daq_disconnectaction) + daq_toolbar.addAction(self._daq_calibaction) + self.addToolBar(Qt.ToolBarArea.TopToolBarArea, daq_toolbar) + + repro_toolbar = QToolBar("Repros") + repro_names, file_names = self.repros.names_of_repros() + for rep, fn in zip(repro_names, file_names): + repro_action = QAction(rep, self) + repro_action.setStatusTip(rep) + repro_action.triggered.connect( + lambda checked, n=rep, f=fn: self.run_repro(n, f) + ) + repro_toolbar.addAction(repro_action) + self.addToolBar(Qt.ToolBarArea.TopToolBarArea, repro_toolbar) + + def create_buttons(self): + self.daq_connect_button = QPushButton("Connect Daq") + self.daq_connect_button.setCheckable(True) + self.daq_connect_button.clicked.connect(self.connect_dac) + + self.daq_disconnect_button = QPushButton("Disconnect Daq") + self.daq_disconnect_button.setCheckable(True) + self.daq_disconnect_button.clicked.connect(self.disconnect_dac) + + self.plot_calibration_button = QPushButton("Plot Calibration") + self.plot_calibration_button.setCheckable(True) + self.plot_calibration_button.clicked.connect(self.plot_calibration) + + def plot_calibration(self): + def decibel(power, ref_power=1.0, min_power=1e-20): + """Transform power to decibel relative to ref_power. + + \\[ decibel = 10 \\cdot \\log_{10}(power/ref\\_power) \\] + Power values smaller than `min_power` are set to `-np.inf`. + + Parameters + ---------- + power: float or array + Power values, for example from a power spectrum or spectrogram. + ref_power: float or None or 'peak' + Reference power for computing decibel. + If set to `None` or 'peak', the maximum power is used. + min_power: float + Power values smaller than `min_power` are set to `-np.inf`. + + Returns + ------- + decibel_psd: array + Power values in decibel relative to `ref_power`. + """ + if np.isscalar(power): + tmp_power = np.array([power]) + decibel_psd = np.array([power]) + else: + tmp_power = power + decibel_psd = power.copy() + if ref_power is None or ref_power == "peak": + ref_power = np.max(decibel_psd) + decibel_psd[tmp_power <= min_power] = float("-inf") + decibel_psd[tmp_power > min_power] = 10.0 * np.log10( + decibel_psd[tmp_power > min_power] / ref_power + ) + if np.isscalar(power): + return decibel_psd[0] + else: + return decibel_psd + + block = self.nix_file.blocks[0] + colors = ["red", "green", "blue", "black", "yellow"] + for i, (stim, fish) in enumerate( + zip(list(block.data_arrays)[::2], list(block.data_arrays)[1::2]) + ): + beat = stim[:] + fish[:] + beat_squared = beat**2 + + f, powerspec = welch(beat, fs=40_000.0) + powerspec = decibel(powerspec) + + f_sq, powerspec_sq = welch(beat_squared, fs=40_000.0) + powerspec_sq = decibel(powerspec_sq) + peaks = find_peaks(powerspec_sq, prominence=20)[0] + pen = pg.mkPen(colors[i]) + self.beat_plot.plot( + np.arange(0, len(beat)) / 40_000.0, + beat_squared, + pen=pen, + # name=stim.name, + ) + self.power_plot.plot(f_sq, powerspec_sq, pen=pen) + self.power_plot.plot(f[peaks], powerspec_sq[peaks], pen=None, symbol="x") + + def connect_dac(self): + devices = uldaq.get_daq_device_inventory(uldaq.InterfaceType.USB) + try: + self.daq_device = uldaq.DaqDevice(devices[0]) + log.debug(f"Found daq devices {len(devices)}, connecting to the first one") + self.daq_device.connect() + log.debug("Connected") + except IndexError: + log.debug("DAQ is not connected, closing") + self.on_exit() + self.daq_connect_button.setDisabled(True) + + def disconnect_dac(self): + try: + log.debug(f"{self.daq_device}") + self.daq_device.disconnect() + self.daq_device.release() + log.debug(f"{self.daq_device}") + self.daq_disconnect_button.setDisabled(True) + self.daq_connect_button.setEnabled(True) + except AttributeError: + log.debug("DAQ was not connected") + + def repros_to_toolbar(self): + repro_names, file_names = self.repros.names_of_repros() + for rep, fn in zip(repro_names, file_names): + individual_repro_button = QAction(rep, self) + individual_repro_button.setStatusTip("Button") + individual_repro_button.triggered.connect( + lambda checked, n=rep, f=fn: self.run_repro(n, f) + ) + self.toolbar.addAction(individual_repro_button) + + def run_repro(self, n, fn): + self.text.appendPlainText(f"started Repro {n}, {fn}") + worker = Worker(self.repros.run_repro, self.nix_file, n, fn) + worker.signals.result.connect(self.print_output) + worker.signals.finished.connect(self.thread_complete) + worker.signals.progress.connect(self.progress_fn) + + self.threadpool.start(worker) + + def on_exit(self): + print("exit button!") + self.close() + + def on_about(self, e): + about = AboutDialog(self) + about.show() + + def print_output(self, s): + print(s) + + def thread_complete(self): + print("THREAD COMPLETE!") + + def progress_fn(self, n): + print("%d%% done" % n) \ No newline at end of file diff --git a/pyrelacs/util/__init__.py b/pyrelacs/util/__init__.py new file mode 100644 index 0000000..e69de29