From 09ab9e4874ab2a662d9c114456fb7ab94058a3a3 Mon Sep 17 00:00:00 2001 From: Cyrille Rossant Date: Fri, 6 Dec 2024 10:48:13 +0100 Subject: [PATCH 1/6] Initial prototype of a fiber trajectory GUI --- iblrig/gui/fiber_trajectory.py | 219 +++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 iblrig/gui/fiber_trajectory.py diff --git a/iblrig/gui/fiber_trajectory.py b/iblrig/gui/fiber_trajectory.py new file mode 100644 index 00000000..1ee0bad5 --- /dev/null +++ b/iblrig/gui/fiber_trajectory.py @@ -0,0 +1,219 @@ +# ------------------------------------------------------------------------------------------------- +# Imports +# ------------------------------------------------------------------------------------------------- + +import json +from pprint import pprint +import sys + +from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QFormLayout +from PyQt5.QtGui import QPalette, QColor + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure + +from ibllib.tests import TEST_DB +from ibllib.atlas import Insertion, AllenAtlas +from one.webclient import AlyxClient + + +# ------------------------------------------------------------------------------------------------- +# Global variables +# ------------------------------------------------------------------------------------------------- + +ACTUAL_DB = { + 'base_url': 'https://alyx.internationalbrainlab.org', + 'username': 'USERNAME', + 'password': 'PASSWORD', +} + + +# ------------------------------------------------------------------------------------------------- +# Plotting functions +# ------------------------------------------------------------------------------------------------- + +def plot_trajectories(ax, names, trajectories, atlas=None): + if not atlas: + atlas = AllenAtlas(25) + atlas.compute_surface() + top = atlas.top + extent = np.hstack((atlas.bc.xlim, atlas.bc.ylim)) + ax.imshow(top, extent=extent, cmap='Greys_r') + ax.set_xlim(atlas.bc.xlim) + ax.set_ylim(atlas.bc.ylim) + + prop_cycle = plt.rcParams['axes.prop_cycle'] + colors = prop_cycle.by_key()['color'] + eps = .0001 + for name, traj, color in zip(names, trajectories, colors): + x = traj[0, 0] + y = traj[0, 1] + if x == y == 0: + continue + ax.plot(traj[:, 0], traj[:, 1]) + ax.plot([x], [y], 'o', color=color) + ax.text(x - eps, y - 4 * eps, name, color=color) + + +# ------------------------------------------------------------------------------------------------- +# Trajectory loader +# ------------------------------------------------------------------------------------------------- + +class TrajectoryLoader: + def __init__(self): + self.alyx = AlyxClient(**TEST_DB) + # self.alyx = AlyxClient(**ACTUAL_DB) + + def _save_rest(self, n, v='read', pk=None): + d = self.alyx.rest(n, v, id=pk) + with open(f'{n}.json', 'w') as f: + json.dump(d, f, indent=1) + + def save_subject(self, pk): + self._save_rest(self, 'subjects', pk=pk) + + def save_session(self, pk): + self._save_rest(self, 'sessions', pk=pk) + + def save_insertion(self, pk): + self._save_rest(self, 'insertions', pk=pk) + + def save_trajectories(self, ): + self._save_rest(self, 'trajectories', v='list') + + def create(self, name, path): + with open(path, 'r') as f: + self.alyx.rest(name, 'create', data=json.load(f)) + + def get_trajectory(self, chronic_insertion): + # retrieve planned/micromanip (priority) trajectory of chronic insertion + trajectories = self.alyx.rest( + 'trajectories', 'list', chronic_insertion=chronic_insertion) + if not trajectories: + return + priorities = { + 'Planned': 1, + 'Micro-manipulator': 2, + } + trajectory = sorted( + trajectories, + key=lambda t: priorities.get(t['provenance'], 0))[-1] + ins = Insertion.from_dict(trajectory, brain_atlas=ATLAS) + return np.vstack((ins.entry, ins.tip)) + + def get_trajectories(self, subject): + chronic_insertions = self.alyx.rest( + 'chronic-insertions', 'list', subject=subject, model='fiber') + names = [i['name'] for i in chronic_insertions] + trajectories = [self.get_trajectory(i['id']) for i in chronic_insertions] + return names, trajectories + + +# ------------------------------------------------------------------------------------------------- +# GUI +# ------------------------------------------------------------------------------------------------- + +class MainWindow(QMainWindow): + def __init__(self, nickname=None, names=None, trajectories=None): + super().__init__() + + self.nickname = nickname + self.names = names + self.trajectories = trajectories + + self.setWindowTitle("Fiber insertions") + + # Main widget + main_widget = QWidget() + main_layout = QVBoxLayout() + + # Top panel + top_panel = QWidget() + top_layout = QVBoxLayout() + + # First row: Label + label_subject = QLabel(self.nickname) + top_layout.addWidget(label_subject) + + # Second row: Label and Textbox + self.textboxes = [] + prop_cycle = plt.rcParams['axes.prop_cycle'] + colors = prop_cycle.by_key()['color'] + for i in range(len(self.trajectories)): + color = colors[i] + self.textboxes.append(QLineEdit()) + rl = QFormLayout() + c = self.trajectories[i][0] # 0 is entry point, 1 is tip + s = f"{self.names[i]}: AP {c[0]:.4f}, ML {c[1]:.4f}, DV {c[2]:.4f}" + label = QLabel(s) + palette = label.palette() + palette.setColor(QPalette.WindowText, QColor(color)) + label.setPalette(palette) + rl.addRow(label, self.textboxes[i]) + + top_layout.addLayout(rl) + + top_panel.setLayout(top_layout) + + # Bottom panel + bottom_panel = QWidget() + bottom_layout = QVBoxLayout() + + # Matplotlib figure + self.figure = Figure() + self.canvas = FigureCanvas(self.figure) + self.ax = self.figure.add_subplot(111) + + bottom_layout.addWidget(self.canvas) + bottom_panel.setLayout(bottom_layout) + + # Add panels to the main layout + main_layout.addWidget(top_panel) + main_layout.addWidget(bottom_panel) + + main_widget.setLayout(main_layout) + self.setCentralWidget(main_widget) + + plot_trajectories(self.ax, self.names, self.trajectories) + + +if __name__ == '__main__': + fig, ax = plt.subplots(1, 1) + + # subject = 'd69bacb2-5ac0-40ac-9be9-98f2fb97d858' + # session = '66f6e1f0-a4a2-4a18-9588-38cf31377fd4' + # probe_insertion = '59538275-27fd-4d56-9658-0c956b0e7c6f' + # chronic_insertion = '0d5c77db-51b7-47f2-aef2-2655520731a0' + # trajectory_estimate = 'f0925fd5-22b3-472d-b43a-d3bb91f33502' + # nickname = 'KM_012' + # birth_date = '2023-08-30' + # lab = 'cortexlab' + + # Mock data + nickname = 'CQ004' + names = ['NBM', 'PPT'] + # NOTE: the unit should be meter, but the trajectory numbers below were given in millimeters + # hence the `*1e-3` + trajectories = [ + np.array([ + [-0.70, +1.75, -4.15], + [+0.70, -1.75, +4.15]]) * 1e-3, + np.array([ + [-4.72, -1.25, -2.75], + [+4.72, +1.25, +2.75]]) * 1e-3, + ] + + app = QApplication(sys.argv) + window = MainWindow(nickname, names, trajectories) + window.show() + sys.exit(app.exec_()) + + # from PyQt5 import QtWidgets + # from iblrig.gui.wizard import RigWizard + # app = QtWidgets.QApplication(['', '--no-sandbox']) + # app.setStyle('Fusion') + # w = RigWizard(alyx=alyx, test_subject_name='KM_012') + # w.show() + # app.exec() From b34c7158b597fa4143d587cc827676eb46e0ee3a Mon Sep 17 00:00:00 2001 From: Cyrille Rossant Date: Fri, 6 Dec 2024 12:07:13 +0100 Subject: [PATCH 2/6] Update FF trajectory GUI --- iblrig/gui/fiber_trajectory.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/iblrig/gui/fiber_trajectory.py b/iblrig/gui/fiber_trajectory.py index 1ee0bad5..b6037aa4 100644 --- a/iblrig/gui/fiber_trajectory.py +++ b/iblrig/gui/fiber_trajectory.py @@ -35,9 +35,7 @@ # ------------------------------------------------------------------------------------------------- def plot_trajectories(ax, names, trajectories, atlas=None): - if not atlas: - atlas = AllenAtlas(25) - atlas.compute_surface() + assert atlas top = atlas.top extent = np.hstack((atlas.bc.xlim, atlas.bc.ylim)) ax.imshow(top, extent=extent, cmap='Greys_r') @@ -62,9 +60,10 @@ def plot_trajectories(ax, names, trajectories, atlas=None): # ------------------------------------------------------------------------------------------------- class TrajectoryLoader: - def __init__(self): + def __init__(self, atlas=None): self.alyx = AlyxClient(**TEST_DB) # self.alyx = AlyxClient(**ACTUAL_DB) + self.atlas = atlas def _save_rest(self, n, v='read', pk=None): d = self.alyx.rest(n, v, id=pk) @@ -100,7 +99,7 @@ def get_trajectory(self, chronic_insertion): trajectory = sorted( trajectories, key=lambda t: priorities.get(t['provenance'], 0))[-1] - ins = Insertion.from_dict(trajectory, brain_atlas=ATLAS) + ins = Insertion.from_dict(trajectory, brain_atlas=self.atlas) return np.vstack((ins.entry, ins.tip)) def get_trajectories(self, subject): @@ -119,6 +118,9 @@ class MainWindow(QMainWindow): def __init__(self, nickname=None, names=None, trajectories=None): super().__init__() + self.atlas = AllenAtlas(25) + self.atlas.compute_surface() + self.nickname = nickname self.names = names self.trajectories = trajectories @@ -176,7 +178,7 @@ def __init__(self, nickname=None, names=None, trajectories=None): main_widget.setLayout(main_layout) self.setCentralWidget(main_widget) - plot_trajectories(self.ax, self.names, self.trajectories) + plot_trajectories(self.ax, self.names, self.trajectories, atlas=self.atlas) if __name__ == '__main__': From 07b2e546a0bc438adfd3145eefb2447d60468ec1 Mon Sep 17 00:00:00 2001 From: Cyrille Rossant Date: Wed, 11 Dec 2024 15:06:06 +0100 Subject: [PATCH 3/6] WIP: ruff --- iblrig/gui/fiber_trajectory.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/iblrig/gui/fiber_trajectory.py b/iblrig/gui/fiber_trajectory.py index b6037aa4..0b8d9cda 100644 --- a/iblrig/gui/fiber_trajectory.py +++ b/iblrig/gui/fiber_trajectory.py @@ -3,11 +3,10 @@ # ------------------------------------------------------------------------------------------------- import json -from pprint import pprint import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QFormLayout from PyQt5.QtGui import QPalette, QColor +from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QLineEdit, QFormLayout import numpy as np import matplotlib.pyplot as plt @@ -34,6 +33,7 @@ # Plotting functions # ------------------------------------------------------------------------------------------------- + def plot_trajectories(ax, names, trajectories, atlas=None): assert atlas top = atlas.top @@ -44,7 +44,7 @@ def plot_trajectories(ax, names, trajectories, atlas=None): prop_cycle = plt.rcParams['axes.prop_cycle'] colors = prop_cycle.by_key()['color'] - eps = .0001 + eps = 0.0001 for name, traj, color in zip(names, trajectories, colors): x = traj[0, 0] y = traj[0, 1] @@ -59,6 +59,7 @@ def plot_trajectories(ax, names, trajectories, atlas=None): # Trajectory loader # ------------------------------------------------------------------------------------------------- + class TrajectoryLoader: def __init__(self, atlas=None): self.alyx = AlyxClient(**TEST_DB) @@ -79,32 +80,28 @@ def save_session(self, pk): def save_insertion(self, pk): self._save_rest(self, 'insertions', pk=pk) - def save_trajectories(self, ): + def save_trajectories(self): self._save_rest(self, 'trajectories', v='list') def create(self, name, path): - with open(path, 'r') as f: + with open(path) as f: self.alyx.rest(name, 'create', data=json.load(f)) def get_trajectory(self, chronic_insertion): # retrieve planned/micromanip (priority) trajectory of chronic insertion - trajectories = self.alyx.rest( - 'trajectories', 'list', chronic_insertion=chronic_insertion) + trajectories = self.alyx.rest('trajectories', 'list', chronic_insertion=chronic_insertion) if not trajectories: return priorities = { 'Planned': 1, 'Micro-manipulator': 2, } - trajectory = sorted( - trajectories, - key=lambda t: priorities.get(t['provenance'], 0))[-1] + trajectory = sorted(trajectories, key=lambda t: priorities.get(t['provenance'], 0))[-1] ins = Insertion.from_dict(trajectory, brain_atlas=self.atlas) return np.vstack((ins.entry, ins.tip)) def get_trajectories(self, subject): - chronic_insertions = self.alyx.rest( - 'chronic-insertions', 'list', subject=subject, model='fiber') + chronic_insertions = self.alyx.rest('chronic-insertions', 'list', subject=subject, model='fiber') names = [i['name'] for i in chronic_insertions] trajectories = [self.get_trajectory(i['id']) for i in chronic_insertions] return names, trajectories @@ -114,6 +111,7 @@ def get_trajectories(self, subject): # GUI # ------------------------------------------------------------------------------------------------- + class MainWindow(QMainWindow): def __init__(self, nickname=None, names=None, trajectories=None): super().__init__() @@ -125,7 +123,7 @@ def __init__(self, nickname=None, names=None, trajectories=None): self.names = names self.trajectories = trajectories - self.setWindowTitle("Fiber insertions") + self.setWindowTitle('Fiber insertions') # Main widget main_widget = QWidget() @@ -148,7 +146,7 @@ def __init__(self, nickname=None, names=None, trajectories=None): self.textboxes.append(QLineEdit()) rl = QFormLayout() c = self.trajectories[i][0] # 0 is entry point, 1 is tip - s = f"{self.names[i]}: AP {c[0]:.4f}, ML {c[1]:.4f}, DV {c[2]:.4f}" + s = f'{self.names[i]}: AP {c[0]:.4f}, ML {c[1]:.4f}, DV {c[2]:.4f}' label = QLabel(s) palette = label.palette() palette.setColor(QPalette.WindowText, QColor(color)) @@ -199,12 +197,8 @@ def __init__(self, nickname=None, names=None, trajectories=None): # NOTE: the unit should be meter, but the trajectory numbers below were given in millimeters # hence the `*1e-3` trajectories = [ - np.array([ - [-0.70, +1.75, -4.15], - [+0.70, -1.75, +4.15]]) * 1e-3, - np.array([ - [-4.72, -1.25, -2.75], - [+4.72, +1.25, +2.75]]) * 1e-3, + np.array([[-0.70, +1.75, -4.15], [+0.70, -1.75, +4.15]]) * 1e-3, + np.array([[-4.72, -1.25, -2.75], [+4.72, +1.25, +2.75]]) * 1e-3, ] app = QApplication(sys.argv) From be02fb03ff84e536106a8b325b94e1aa6235bb47 Mon Sep 17 00:00:00 2001 From: Cyrille Rossant Date: Wed, 11 Dec 2024 15:09:04 +0100 Subject: [PATCH 4/6] WIP: ruff --- iblrig/gui/fiber_trajectory.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/iblrig/gui/fiber_trajectory.py b/iblrig/gui/fiber_trajectory.py index 0b8d9cda..56768994 100644 --- a/iblrig/gui/fiber_trajectory.py +++ b/iblrig/gui/fiber_trajectory.py @@ -5,17 +5,17 @@ import json import sys -from PyQt5.QtGui import QPalette, QColor -from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QLineEdit, QFormLayout - -import numpy as np import matplotlib.pyplot as plt -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.figure import Figure - +import numpy as np +from ibllib.atlas import AllenAtlas, Insertion from ibllib.tests import TEST_DB -from ibllib.atlas import Insertion, AllenAtlas +from matplotlib.backends.backend_qt5agg import \ + FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure from one.webclient import AlyxClient +from PyQt5.QtGui import QColor, QPalette +from PyQt5.QtWidgets import (QApplication, QFormLayout, QLabel, QLineEdit, + QMainWindow, QVBoxLayout, QWidget) # ------------------------------------------------------------------------------------------------- @@ -45,7 +45,7 @@ def plot_trajectories(ax, names, trajectories, atlas=None): prop_cycle = plt.rcParams['axes.prop_cycle'] colors = prop_cycle.by_key()['color'] eps = 0.0001 - for name, traj, color in zip(names, trajectories, colors): + for name, traj, color in zip(names, trajectories, colors, strict=True): x = traj[0, 0] y = traj[0, 1] if x == y == 0: From 73938c4d8fc28888f4b81dae5d398ca0992cd569 Mon Sep 17 00:00:00 2001 From: Cyrille Rossant Date: Wed, 11 Dec 2024 15:17:12 +0100 Subject: [PATCH 5/6] WIP: ruff --- iblrig/gui/fiber_trajectory.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/iblrig/gui/fiber_trajectory.py b/iblrig/gui/fiber_trajectory.py index 56768994..e4c5fc03 100644 --- a/iblrig/gui/fiber_trajectory.py +++ b/iblrig/gui/fiber_trajectory.py @@ -5,17 +5,18 @@ import json import sys +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure import matplotlib.pyplot as plt import numpy as np +from PyQt5.QtGui import QColor, QPalette +from PyQt5.QtWidgets import ( + QApplication, QFormLayout, QLabel, QLineEdit, QMainWindow, QVBoxLayout, QWidget, +) + from ibllib.atlas import AllenAtlas, Insertion from ibllib.tests import TEST_DB -from matplotlib.backends.backend_qt5agg import \ - FigureCanvasQTAgg as FigureCanvas -from matplotlib.figure import Figure from one.webclient import AlyxClient -from PyQt5.QtGui import QColor, QPalette -from PyQt5.QtWidgets import (QApplication, QFormLayout, QLabel, QLineEdit, - QMainWindow, QVBoxLayout, QWidget) # ------------------------------------------------------------------------------------------------- From 077eee1026687f63cb68b5ca5319d094be402e52 Mon Sep 17 00:00:00 2001 From: Cyrille Rossant Date: Wed, 11 Dec 2024 16:14:18 +0100 Subject: [PATCH 6/6] Ruff fix --- iblrig/gui/fiber_trajectory.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/iblrig/gui/fiber_trajectory.py b/iblrig/gui/fiber_trajectory.py index e4c5fc03..19ff7372 100644 --- a/iblrig/gui/fiber_trajectory.py +++ b/iblrig/gui/fiber_trajectory.py @@ -5,20 +5,25 @@ import json import sys -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.figure import Figure import matplotlib.pyplot as plt import numpy as np +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure from PyQt5.QtGui import QColor, QPalette from PyQt5.QtWidgets import ( - QApplication, QFormLayout, QLabel, QLineEdit, QMainWindow, QVBoxLayout, QWidget, + QApplication, + QFormLayout, + QLabel, + QLineEdit, + QMainWindow, + QVBoxLayout, + QWidget, ) from ibllib.atlas import AllenAtlas, Insertion from ibllib.tests import TEST_DB from one.webclient import AlyxClient - # ------------------------------------------------------------------------------------------------- # Global variables # -------------------------------------------------------------------------------------------------