unitrefine 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. unitrefine/__init__.py +75 -0
  2. unitrefine/apply_models.py +111 -0
  3. unitrefine/cli.py +251 -0
  4. unitrefine/evaluation.py +457 -0
  5. unitrefine/figures.py +1458 -0
  6. unitrefine/import_labels.py +176 -0
  7. unitrefine/launch_sigui.py +138 -0
  8. unitrefine/launch_sigui_validate.py +243 -0
  9. unitrefine/launcher.py +239 -0
  10. unitrefine/main.py +2395 -0
  11. unitrefine/manage_models.py +193 -0
  12. unitrefine/metrics.py +146 -0
  13. unitrefine/model_selection.py +159 -0
  14. unitrefine/models_hub.py +118 -0
  15. unitrefine/phy_curation.py +306 -0
  16. unitrefine/phy_import.py +428 -0
  17. unitrefine/predictions.py +507 -0
  18. unitrefine/resources/curation_gui.jpg +0 -0
  19. unitrefine/resources/fonts/Inter-Bold.ttf +0 -0
  20. unitrefine/resources/fonts/Inter-LICENSE.txt +92 -0
  21. unitrefine/resources/fonts/Inter-Regular.ttf +0 -0
  22. unitrefine/resources/logo.png +0 -0
  23. unitrefine/resources/model_training_example.jpg +0 -0
  24. unitrefine/resources/predict-gui.JPG +0 -0
  25. unitrefine/resources/re-label.JPG +0 -0
  26. unitrefine/resources/si-gui.JPG +0 -0
  27. unitrefine/resources/train_and_load_gui.JPG +0 -0
  28. unitrefine/resources/unit_types_example.jpg +0 -0
  29. unitrefine/resources/unitrefine_gui.JPG +0 -0
  30. unitrefine/retraining_history.py +220 -0
  31. unitrefine/sigui_progress.py +157 -0
  32. unitrefine/theme.py +254 -0
  33. unitrefine/train.py +622 -0
  34. unitrefine/widgets.py +961 -0
  35. unitrefine-0.2.0.dist-info/METADATA +153 -0
  36. unitrefine-0.2.0.dist-info/RECORD +40 -0
  37. unitrefine-0.2.0.dist-info/WHEEL +5 -0
  38. unitrefine-0.2.0.dist-info/entry_points.txt +4 -0
  39. unitrefine-0.2.0.dist-info/licenses/LICENSE +21 -0
  40. unitrefine-0.2.0.dist-info/top_level.txt +1 -0
unitrefine/launcher.py ADDED
@@ -0,0 +1,239 @@
1
+ """
2
+ Starts UnitRefine. An "Opening UnitRefine" box appears at once, while the slow parts run behind it: loading
3
+ SpikeInterface and the machine learning libraries, loading the project, and building the window with its
4
+ saved figures. The window is only shown once it is complete, so it doesn't flicker while it fills in.
5
+
6
+ Only PyQt5 is imported at the top of this file, so the box can appear before the slow imports.
7
+ """
8
+
9
+ import importlib.util
10
+ import sys
11
+ import time
12
+ from argparse import ArgumentParser
13
+ from pathlib import Path
14
+
15
+ try:
16
+ import PyQt5.QtWidgets as QtWidgets
17
+ if importlib.util.find_spec("spikeinterface_gui") is None:
18
+ raise ImportError("No module named 'spikeinterface_gui'")
19
+ except ImportError as error:
20
+ # installed without the GUI, e.g. on a computing cluster
21
+ sys.exit(f"The UnitRefine GUI is not installed ({error}). Install it with: pip install \"unitrefine[gui]\" "
22
+ f"(or `uv sync` in the repository). Without the GUI, models can be applied with unitrefine-apply.")
23
+ from PyQt5.QtCore import QCoreApplication, QEvent, Qt
24
+ from PyQt5.QtGui import QFontDatabase, QIcon, QPixmap
25
+ from PyQt5.QtWidgets import QStyleFactory
26
+
27
+ from unitrefine import __version__, theme
28
+
29
+ RESOURCES_FOLDER = Path(__file__).absolute().parent / "resources"
30
+
31
+ STARTUP_STEPS = [
32
+ "Loading SpikeInterface and the machine learning libraries",
33
+ "Loading the project",
34
+ "Building the window and loading the saved figures",
35
+ ]
36
+ # for a new project, after loading it
37
+ DOWNLOAD_STEP = "Downloading the pretrained models (about 30 MB)"
38
+
39
+
40
+ def enable_high_dpi_scaling():
41
+ """
42
+ Makes the GUI follow the display scaling set in the operating system (e.g. 150% on a laptop with a
43
+ high resolution screen), so it has the same physical size on every screen. Must be called before
44
+ the QApplication is created.
45
+ """
46
+ QtWidgets.QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
47
+ QtWidgets.QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
48
+ # use the exact scaling (e.g. 1.25) instead of rounding it to a whole number
49
+ QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
50
+
51
+
52
+ def set_app_font(app):
53
+ """
54
+ Uses Inter, which is included in resources/fonts so the GUI looks the same on every system.
55
+ If it can't be loaded, falls back to Segoe UI (Windows) or the system's default font.
56
+ """
57
+ loaded_families = set()
58
+ for font_file in (RESOURCES_FOLDER / "fonts").glob("*.ttf"):
59
+ font_id = QFontDatabase.addApplicationFont(str(font_file))
60
+ if font_id != -1:
61
+ loaded_families.update(QFontDatabase.applicationFontFamilies(font_id))
62
+
63
+ custom_font = app.font()
64
+ if "Inter" in loaded_families:
65
+ custom_font.setFamily("Inter")
66
+ elif "Segoe UI" in QFontDatabase().families():
67
+ custom_font.setFamily("Segoe UI")
68
+ custom_font.setPointSize(10)
69
+ app.setFont(custom_font)
70
+
71
+
72
+ class StartupDialog(QtWidgets.QWidget):
73
+ """The 'Opening UnitRefine' box: the steps of opening, which is done, and the time so far."""
74
+
75
+ def __init__(self, project_folder, steps=STARTUP_STEPS):
76
+ super().__init__(None, Qt.SplashScreen | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
77
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
78
+ self.start_time = time.time()
79
+ self.current_step = -1
80
+ self.steps = steps
81
+
82
+ outer_layout = QtWidgets.QVBoxLayout(self)
83
+ outer_layout.setContentsMargins(0, 0, 0, 0)
84
+ card = QtWidgets.QFrame()
85
+ card.setObjectName("startup_card")
86
+ card.setStyleSheet(
87
+ f"QFrame#startup_card {{ background-color: white; border: 1px solid {theme.BORDER}; border-radius: 14px; }}"
88
+ )
89
+ outer_layout.addWidget(card)
90
+ layout = QtWidgets.QVBoxLayout(card)
91
+ layout.setContentsMargins(28, 24, 28, 22)
92
+ layout.setSpacing(10)
93
+
94
+ header_layout = QtWidgets.QHBoxLayout()
95
+ logo_file = RESOURCES_FOLDER / "logo.png"
96
+ if logo_file.exists():
97
+ logo = QtWidgets.QLabel()
98
+ logo.setPixmap(QPixmap(str(logo_file)).scaled(44, 44, Qt.KeepAspectRatio, Qt.SmoothTransformation))
99
+ header_layout.addWidget(logo)
100
+ title_layout = QtWidgets.QVBoxLayout()
101
+ title_layout.setSpacing(0)
102
+ title = QtWidgets.QLabel("Opening UnitRefine")
103
+ title.setStyleSheet("font-weight: bold; font-size: 15pt;")
104
+ title_layout.addWidget(title)
105
+ project_label = QtWidgets.QLabel(f"Project: {Path(project_folder).name}")
106
+ project_label.setObjectName("muted")
107
+ title_layout.addWidget(project_label)
108
+ header_layout.addLayout(title_layout, 1)
109
+ layout.addLayout(header_layout)
110
+
111
+ self.step_labels = []
112
+ for step_text in self.steps:
113
+ step_label = QtWidgets.QLabel()
114
+ step_label.setTextFormat(Qt.TextFormat.RichText)
115
+ layout.addWidget(step_label)
116
+ self.step_labels.append(step_label)
117
+
118
+ self.progress_bar = QtWidgets.QProgressBar()
119
+ self.progress_bar.setRange(0, len(self.steps))
120
+ self.progress_bar.setTextVisible(False)
121
+ self.progress_bar.setFixedHeight(8)
122
+ layout.addWidget(self.progress_bar)
123
+
124
+ self.time_label = QtWidgets.QLabel()
125
+ self.time_label.setObjectName("muted")
126
+ layout.addWidget(self.time_label)
127
+
128
+ self.setFixedWidth(500)
129
+ self.update_display()
130
+
131
+ def set_step(self, step_index):
132
+ self.current_step = step_index
133
+ self.update_display()
134
+
135
+ def update_display(self):
136
+ for index, (step_text, step_label) in enumerate(zip(self.steps, self.step_labels)):
137
+ if index < self.current_step:
138
+ step_label.setText(f"<span style='color:{theme.SUCCESS}'>✓</span>&nbsp; {step_text}")
139
+ step_label.setStyleSheet(f"color: {theme.MUTED};")
140
+ elif index == self.current_step:
141
+ step_label.setText(f"<span style='color:{theme.ACCENT}'>●</span>&nbsp; <b>{step_text}…</b>")
142
+ step_label.setStyleSheet("")
143
+ else:
144
+ step_label.setText(f"○&nbsp; {step_text}")
145
+ step_label.setStyleSheet("color: #98A2B3;")
146
+ self.progress_bar.setValue(max(self.current_step, 0))
147
+ self.time_label.setText(f"{time.time() - self.start_time:.0f} s · the first start after installing can take a minute")
148
+ # draw now: the slow steps run in this thread, so the box can't redraw itself while they run
149
+ for _ in range(3):
150
+ QtWidgets.QApplication.processEvents()
151
+
152
+
153
+ def main():
154
+
155
+ parser = ArgumentParser(
156
+ description="UnitRefine - curate your sorting and create a machine learning model based on your curation."
157
+ )
158
+ parser.add_argument(
159
+ "--project_folder",
160
+ required=True,
161
+ type=str,
162
+ )
163
+ parser.add_argument("--no_pretrained_models", action="store_true",
164
+ help="don't download the pretrained models into a new project (they can be added later "
165
+ "with + Load from HFH)")
166
+ parser.add_argument("--version", action="version", version=f"unitrefine {__version__}")
167
+ args = parser.parse_args()
168
+ project_folder = Path(args.project_folder).resolve()
169
+ new_project = not project_folder.is_dir()
170
+ # a new project comes with the pretrained models, so they can be applied straight away
171
+ download_models = new_project and not args.no_pretrained_models
172
+ steps = STARTUP_STEPS[:2] + ([DOWNLOAD_STEP] if download_models else []) + STARTUP_STEPS[2:]
173
+
174
+ enable_high_dpi_scaling()
175
+ app = QtWidgets.QApplication(sys.argv)
176
+ app.setStyle(QStyleFactory.create("Fusion"))
177
+ app.setStyleSheet(theme.STYLE_SHEET)
178
+ icon_file = RESOURCES_FOLDER / "logo.png"
179
+ if icon_file.exists():
180
+ app.setWindowIcon(QIcon(str(icon_file)))
181
+ set_app_font(app)
182
+
183
+ startup = StartupDialog(project_folder, steps)
184
+ startup.show()
185
+ startup.set_step(0)
186
+
187
+ from unitrefine.main import APP_DESCRIPTION, MainWindow, UnitRefineProject, initial_window_size, load_project
188
+
189
+ startup.set_step(1)
190
+ if not new_project:
191
+ print("Project already exists. Loading...")
192
+ project = load_project(project_folder)
193
+ else:
194
+ print("Project Folder does not exist. Creating now...")
195
+ project = UnitRefineProject(project_folder)
196
+ project.save()
197
+
198
+ not_downloaded = {}
199
+ if download_models:
200
+ from unitrefine.models_hub import download_default_models
201
+
202
+ startup.set_step(2)
203
+ print("Downloading the pretrained models...")
204
+ not_downloaded = download_default_models(project_folder / "models")
205
+ # load it again, with the models
206
+ project = load_project(project_folder)
207
+
208
+ startup.set_step(len(steps) - 1)
209
+ window = MainWindow(project)
210
+ window.setWindowTitle(f"UnitRefine: {APP_DESCRIPTION}")
211
+ window.resize(initial_window_size(app.primaryScreen().availableGeometry()))
212
+ if download_models:
213
+ choose_pretrained_cascade(window)
214
+ window.finish_loading()
215
+
216
+ startup.set_step(len(steps))
217
+ window.show()
218
+ QtWidgets.QApplication.processEvents()
219
+ startup.close()
220
+ if len(not_downloaded) > 0:
221
+ QtWidgets.QMessageBox.information(
222
+ window, "Pretrained models not downloaded",
223
+ "These pretrained models could not be downloaded (is the computer connected to the internet?):\n"
224
+ + "\n".join(f"• {repo_id}" for repo_id in not_downloaded)
225
+ + "\n\nYou can add them later with + Load from HFH in section 3.1.")
226
+ app.exec()
227
+
228
+
229
+ def choose_pretrained_cascade(window):
230
+ """In a new project, chooses the pretrained noise vs neural and SUA vs MUA models as a cascade (section 3.2)."""
231
+ selector = window.model_selector
232
+ selector.cascade_radio.setChecked(True)
233
+ if selector.describe_problem() is not None:
234
+ # e.g. one of them could not be downloaded
235
+ selector.binary_radio.setChecked(True)
236
+
237
+
238
+ if __name__ == "__main__":
239
+ main()