neuroseq 0.30.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 (248) hide show
  1. neuroseq/__init__.py +5 -0
  2. neuroseq/__main__.py +256 -0
  3. neuroseq/add_experiment_data_dialog.py +241 -0
  4. neuroseq/analysis_plugins.py +182 -0
  5. neuroseq/aoi_shapes.py +65 -0
  6. neuroseq/b2_model.py +2772 -0
  7. neuroseq/b2_units.py +105 -0
  8. neuroseq/background_jobs.py +447 -0
  9. neuroseq/cellpose_annotation.py +138 -0
  10. neuroseq/cellpose_test_data.py +285 -0
  11. neuroseq/console_art.py +98 -0
  12. neuroseq/console_widget.py +292 -0
  13. neuroseq/data.py +101 -0
  14. neuroseq/data_manager_dialog.py +197 -0
  15. neuroseq/device_control/__init__.py +1 -0
  16. neuroseq/device_control/bss2_connection.py +375 -0
  17. neuroseq/device_control/bss2_panel.py +908 -0
  18. neuroseq/device_control/bss2_protocol.py +975 -0
  19. neuroseq/device_control/bss2_stimulation_host.py +293 -0
  20. neuroseq/device_control/scanimage_connection.py +351 -0
  21. neuroseq/device_control/scanimage_controls.py +424 -0
  22. neuroseq/device_control/scanimage_engine_worker.py +128 -0
  23. neuroseq/device_control/scanimage_files.py +49 -0
  24. neuroseq/device_control/stimulation_setup_widget.py +38 -0
  25. neuroseq/device_control/syscon2_client.py +439 -0
  26. neuroseq/device_control/syscon2_connection_manager.py +196 -0
  27. neuroseq/device_control/syscon2_panel.py +1523 -0
  28. neuroseq/device_control/syscon2_protocol.py +207 -0
  29. neuroseq/device_control/syscon2_stimulation_host.py +689 -0
  30. neuroseq/docs/common_issues.md +49 -0
  31. neuroseq/docs/developer/analysis_plugins.md +43 -0
  32. neuroseq/docs/developer/contributing.md +41 -0
  33. neuroseq/docs/developer/custom_simulators.md +156 -0
  34. neuroseq/docs/developer/developer_installation.md +137 -0
  35. neuroseq/docs/developer/documentation.md +52 -0
  36. neuroseq/docs/developer/miscellaneous.md +5 -0
  37. neuroseq/docs/developer/project_history.md +37 -0
  38. neuroseq/docs/developer/releases.md +43 -0
  39. neuroseq/docs/developer/testing.md +168 -0
  40. neuroseq/docs/examples.md +64 -0
  41. neuroseq/docs/getting_started/installation.md +62 -0
  42. neuroseq/docs/getting_started/quickstart.md +11 -0
  43. neuroseq/docs/getting_started/windows_shortcut.md +112 -0
  44. neuroseq/docs/images/SutterPatch_TemplateEditor.png +0 -0
  45. neuroseq/docs/images/amplitude_selection.png +0 -0
  46. neuroseq/docs/images/button_appearance.png +0 -0
  47. neuroseq/docs/images/data_selection.png +0 -0
  48. neuroseq/docs/images/draw_mode.png +0 -0
  49. neuroseq/docs/images/favicon.svg +1 -0
  50. neuroseq/docs/images/hodgkin_huxley_neuron.png +0 -0
  51. neuroseq/docs/images/layout.png +0 -0
  52. neuroseq/docs/images/screenshot.png +0 -0
  53. neuroseq/docs/images/select_mode.png +0 -0
  54. neuroseq/docs/images/stimulus_intensity.png +0 -0
  55. neuroseq/docs/images/toggle_widgets.png +0 -0
  56. neuroseq/docs/index.md +47 -0
  57. neuroseq/docs/user_guide/analysis_and_plugins.md +69 -0
  58. neuroseq/docs/user_guide/brainscales2_delivery.md +260 -0
  59. neuroseq/docs/user_guide/chat_assistant.md +98 -0
  60. neuroseq/docs/user_guide/experiments.md +72 -0
  61. neuroseq/docs/user_guide/export_patch_clamp_stimuli.md +19 -0
  62. neuroseq/docs/user_guide/gui_overview.md +262 -0
  63. neuroseq/docs/user_guide/importing_igor2_waves.md +107 -0
  64. neuroseq/docs/user_guide/importing_imaging_data.md +207 -0
  65. neuroseq/docs/user_guide/multiphoton_holographic_stimulation.md +310 -0
  66. neuroseq/docs/user_guide/run_external_simulators.md +72 -0
  67. neuroseq/docs/user_guide/scanimage_acquisition.md +263 -0
  68. neuroseq/docs/user_guide/simulations.md +153 -0
  69. neuroseq/docs/user_guide/simulations_and_experiments.md +68 -0
  70. neuroseq/docs/user_guide/stdp_synaptic_tuning.md +178 -0
  71. neuroseq/elise_support/__init__.py +1 -0
  72. neuroseq/elise_support/protocol_dialog.py +92 -0
  73. neuroseq/elise_support/replay_metrics.py +298 -0
  74. neuroseq/elise_support/workflows.py +318 -0
  75. neuroseq/example_models/NN.py +70 -0
  76. neuroseq/example_models/__init__.py +0 -0
  77. neuroseq/example_models/elise.py +1603 -0
  78. neuroseq/example_models/pynn_demo.py +366 -0
  79. neuroseq/example_models/sinus_offset.py +209 -0
  80. neuroseq/example_projects/__init__.py +81 -0
  81. neuroseq/example_projects/_registry.py +36 -0
  82. neuroseq/example_projects/networks/__init__.py +1 -0
  83. neuroseq/example_projects/networks/default_project.py +178 -0
  84. neuroseq/example_projects/networks/elise_project.py +10 -0
  85. neuroseq/example_projects/networks/getting_started_project.py +48 -0
  86. neuroseq/example_projects/networks/hh_neuron_with_synapses.py +204 -0
  87. neuroseq/example_projects/networks/hh_neuron_with_synthetic_synapses.py +206 -0
  88. neuroseq/example_projects/networks/hh_neurons_ei_model.py +144 -0
  89. neuroseq/example_projects/networks/hh_neurons_with_plasticity.py +215 -0
  90. neuroseq/example_projects/networks/locally_connected_network.py +107 -0
  91. neuroseq/example_projects/networks/non_linear.py +58 -0
  92. neuroseq/example_projects/networks/pynn_demo_project.py +65 -0
  93. neuroseq/example_projects/networks/sinus_offset_project.py +57 -0
  94. neuroseq/example_projects/networks/two_hh_neurons_with_plasticity.py +239 -0
  95. neuroseq/example_projects/neurons/__init__.py +1 -0
  96. neuroseq/example_projects/neurons/pyrho_6state_chr2_project.py +154 -0
  97. neuroseq/example_projects/neurons/single_adex_neuron.py +81 -0
  98. neuroseq/example_projects/neurons/single_hh_neuron.py +137 -0
  99. neuroseq/example_projects/neurons/single_lif_neuron.py +63 -0
  100. neuroseq/example_projects/neurons/single_lif_neuron_many_stims.py +115 -0
  101. neuroseq/example_projects/neurons/single_lif_neuron_without_stim.py +44 -0
  102. neuroseq/export/__init__.py +1 -0
  103. neuroseq/export/exporter_igor2.py +408 -0
  104. neuroseq/export/exporter_syscon2.py +4076 -0
  105. neuroseq/export/igor2_pxp_stim_export_dialog.py +106 -0
  106. neuroseq/export/igor2_single_wave_export_dialog.py +179 -0
  107. neuroseq/export/power_lut.py +142 -0
  108. neuroseq/export/stimulation_generator_dialog.py +1284 -0
  109. neuroseq/export/stimulus_export_dialog.py +244 -0
  110. neuroseq/export/syscon2_export_service.py +332 -0
  111. neuroseq/external_tools.py +73 -0
  112. neuroseq/fiji.py +128 -0
  113. neuroseq/gpu.py +102 -0
  114. neuroseq/gui.py +341 -0
  115. neuroseq/gui_settings.py +277 -0
  116. neuroseq/icon_manager.py +108 -0
  117. neuroseq/importers/__init__.py +1 -0
  118. neuroseq/importers/cellpose_import_dialog.py +1770 -0
  119. neuroseq/importers/importer_cellpose.py +849 -0
  120. neuroseq/importers/importer_patch_clamp_data.py +801 -0
  121. neuroseq/importers/importer_scanimage.py +111 -0
  122. neuroseq/importers/importer_scanimage_traces.py +188 -0
  123. neuroseq/importers/importer_suite2p.py +210 -0
  124. neuroseq/importers/importer_syscon2.py +1239 -0
  125. neuroseq/importers/importer_tif.py +586 -0
  126. neuroseq/importers/patch_clamp_data_input_import_dialog.py +795 -0
  127. neuroseq/importers/patch_clamp_data_output_import_dialog.py +1076 -0
  128. neuroseq/importers/suite2p_import_dialog.py +606 -0
  129. neuroseq/importers/syscon2_import_dialog.py +495 -0
  130. neuroseq/importers/tif_import_dialog.py +961 -0
  131. neuroseq/io.py +542 -0
  132. neuroseq/llm/__init__.py +25 -0
  133. neuroseq/llm/anthropic_backend.py +73 -0
  134. neuroseq/llm/backend.py +76 -0
  135. neuroseq/llm/chat_widget.py +399 -0
  136. neuroseq/llm/llm_settings_dialog.py +234 -0
  137. neuroseq/llm/ollama.py +113 -0
  138. neuroseq/llm/openai_backend.py +79 -0
  139. neuroseq/llm/plugin_write_dialog.py +100 -0
  140. neuroseq/llm/read_access.py +339 -0
  141. neuroseq/llm/secrets.py +43 -0
  142. neuroseq/llm/system_prompt.py +85 -0
  143. neuroseq/main_scroll.py +187 -0
  144. neuroseq/model_descriptions.py +140 -0
  145. neuroseq/model_editor.py +1231 -0
  146. neuroseq/project.py +1706 -0
  147. neuroseq/project_history.py +166 -0
  148. neuroseq/project_workspace.py +3966 -0
  149. neuroseq/py.typed +0 -0
  150. neuroseq/qt_helpers.py +142 -0
  151. neuroseq/resources/__init__.py +0 -0
  152. neuroseq/resources/analysis_plugins/advanced_mean_activity.py +147 -0
  153. neuroseq/resources/analysis_plugins/elise_learning_curves.py +108 -0
  154. neuroseq/resources/analysis_plugins/elise_target_mse.py +189 -0
  155. neuroseq/resources/analysis_plugins/epsp_amplitudes.py +249 -0
  156. neuroseq/resources/analysis_plugins/mean_activity.py +69 -0
  157. neuroseq/resources/analysis_plugins/membrane_parameters.py +326 -0
  158. neuroseq/resources/analysis_plugins/replay_mse.py +176 -0
  159. neuroseq/resources/bss2/neuroseq_bss2_helper.py +532 -0
  160. neuroseq/resources/console_brain.png +0 -0
  161. neuroseq/resources/example_data/2p_imaging_FOV.png +0 -0
  162. neuroseq/resources/example_data/2p_imaging_FOV_seg.npy +0 -0
  163. neuroseq/resources/retro.qss +583 -0
  164. neuroseq/resources/retro_arrow_down.svg +3 -0
  165. neuroseq/resources/retro_arrow_down_disabled.svg +3 -0
  166. neuroseq/resources/retro_arrow_up.svg +3 -0
  167. neuroseq/resources/retro_arrow_up_disabled.svg +3 -0
  168. neuroseq/resources/retro_check.svg +6 -0
  169. neuroseq/resources/retro_check_disabled.svg +4 -0
  170. neuroseq/resources/retro_checkbox.svg +5 -0
  171. neuroseq/resources/retro_checkbox_disabled.svg +3 -0
  172. neuroseq/resources/retro_close.svg +3 -0
  173. neuroseq/resources/retro_indeterminate.svg +6 -0
  174. neuroseq/resources/retro_indeterminate_disabled.svg +4 -0
  175. neuroseq/resources/retro_radio.svg +5 -0
  176. neuroseq/resources/retro_radio_disabled.svg +4 -0
  177. neuroseq/resources/retro_radio_off.svg +4 -0
  178. neuroseq/resources/retro_radio_off_disabled.svg +3 -0
  179. neuroseq/resources/rheobase_chronaxie.svg +195 -0
  180. neuroseq/resources/scanimage/neuroseq_scanimage.m +212 -0
  181. neuroseq/resources/scanimage/neuroseq_scanimage_event.m +4 -0
  182. neuroseq/resources/startup_brain.png +0 -0
  183. neuroseq/resources/syscon_luts/__init__.py +0 -0
  184. neuroseq/resources/syscon_luts/default_1100nm.lut +16 -0
  185. neuroseq/resources/syscon_luts/default_940nm.lut +9 -0
  186. neuroseq/save_imaging_dialog.py +115 -0
  187. neuroseq/save_load_options.py +89 -0
  188. neuroseq/spike_detection.py +262 -0
  189. neuroseq/startup_dialog.py +157 -0
  190. neuroseq/stimulation/__init__.py +0 -0
  191. neuroseq/stimulation/dynamic_input/__init__.py +1 -0
  192. neuroseq/stimulation/dynamic_input/dialog_helpers.py +66 -0
  193. neuroseq/stimulation/dynamic_input/dynamic_input_blocks.py +84 -0
  194. neuroseq/stimulation/dynamic_input/dynamic_input_controller.py +164 -0
  195. neuroseq/stimulation/dynamic_input/dynamic_input_measurement.py +221 -0
  196. neuroseq/stimulation/dynamic_input/dynamic_input_routine.py +101 -0
  197. neuroseq/stimulation/dynamic_input/hardware_settings_widget.py +223 -0
  198. neuroseq/stimulation/dynamic_input/hardware_targets.py +106 -0
  199. neuroseq/stimulation/dynamic_input/rheobase.py +27 -0
  200. neuroseq/stimulation/dynamic_input/rheobase_dialog.py +2235 -0
  201. neuroseq/stimulation/dynamic_input/routine_overview_dialog.py +333 -0
  202. neuroseq/stimulation/dynamic_input/stdp_synaptic_tuning_dialog.py +1035 -0
  203. neuroseq/stimulation/dynamic_input/stdp_window.py +44 -0
  204. neuroseq/stimulation/dynamic_input/stdp_window_dialog.py +1764 -0
  205. neuroseq/stimulation/static_input/__init__.py +1 -0
  206. neuroseq/stimulation/static_input/static_input_controller.py +65 -0
  207. neuroseq/stimulation/static_input/stim_pattern_dialog.py +434 -0
  208. neuroseq/stimulation/static_input/stim_patterns.py +336 -0
  209. neuroseq/stimulation/stim.py +55 -0
  210. neuroseq/stimulation/stim_array_modifier.py +340 -0
  211. neuroseq/stimulation/stim_dialog.py +182 -0
  212. neuroseq/stimulation/stim_editor.py +1648 -0
  213. neuroseq/stimulation/stim_intensity_dialog.py +168 -0
  214. neuroseq/stimulation/stim_loop_dialog.py +66 -0
  215. neuroseq/stimulation/stim_timing.py +171 -0
  216. neuroseq/synthetic_data_dialog.py +985 -0
  217. neuroseq/theme.py +362 -0
  218. neuroseq/utils.py +581 -0
  219. neuroseq/visualization/__init__.py +1 -0
  220. neuroseq/visualization/calcium_video_widget.py +867 -0
  221. neuroseq/visualization/cell_overlay_widget.py +1655 -0
  222. neuroseq/visualization/custom_analysis_widget.py +362 -0
  223. neuroseq/visualization/data_widget.py +351 -0
  224. neuroseq/visualization/fpl_widget.py +287 -0
  225. neuroseq/visualization/heatmap_widget.py +341 -0
  226. neuroseq/visualization/spatial_footprint_widget.py +676 -0
  227. neuroseq/visualization/spike_widget.py +141 -0
  228. neuroseq/visualization/stim_widget.py +651 -0
  229. neuroseq/visualization/time_slider_widget.py +167 -0
  230. neuroseq/visualization/timeseries_widget.py +1071 -0
  231. neuroseq/visualization/trace_widget.py +827 -0
  232. neuroseq/visualization_support/__init__.py +1 -0
  233. neuroseq/visualization_support/brightness_contrast.py +282 -0
  234. neuroseq/visualization_support/cellpose_ids.py +49 -0
  235. neuroseq/visualization_support/constrained_pan_zoom_controller.py +388 -0
  236. neuroseq/visualization_support/gridlines.py +190 -0
  237. neuroseq/visualization_support/neuron_subset_dialog.py +74 -0
  238. neuroseq/visualization_support/region_of_interest.py +82 -0
  239. neuroseq/visualization_support/segmentation_brightness_dialog.py +84 -0
  240. neuroseq/visualization_support/spatial_footprint_state.py +241 -0
  241. neuroseq/visualization_support/trace_widget_state.py +69 -0
  242. neuroseq/workspace_acquisition_controller.py +708 -0
  243. neuroseq/workspace_devices.py +217 -0
  244. neuroseq-0.30.0.dist-info/METADATA +103 -0
  245. neuroseq-0.30.0.dist-info/RECORD +248 -0
  246. neuroseq-0.30.0.dist-info/WHEEL +4 -0
  247. neuroseq-0.30.0.dist-info/entry_points.txt +2 -0
  248. neuroseq-0.30.0.dist-info/licenses/LICENSE +21 -0
neuroseq/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ __all__ = [
2
+ "__version__",
3
+ ]
4
+
5
+ __version__ = "0.30.0"
neuroseq/__main__.py ADDED
@@ -0,0 +1,256 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import pathlib
5
+ import signal
6
+ import sys
7
+ from collections.abc import Callable
8
+ from types import FrameType
9
+ from typing import Any
10
+
11
+ import click
12
+
13
+ from neuroseq.cellpose_test_data import save_cellpose_test_data
14
+
15
+
16
+ def _install_interrupt_handlers(
17
+ app: Any,
18
+ *,
19
+ timer_factory: Callable[[Any], Any] | None = None,
20
+ signal_module: Any = signal,
21
+ ) -> Any | None:
22
+ """Keep Ctrl-C responsive while the Qt event loop is running."""
23
+ if sys.platform == "win32":
24
+ return None
25
+
26
+ if timer_factory is None:
27
+ from qtpy import QtCore
28
+
29
+ timer = QtCore.QTimer(app)
30
+ else:
31
+ timer = timer_factory(app)
32
+
33
+ def _quit_on_signal(signum: int, _frame: FrameType | None) -> None:
34
+ logging.getLogger(__name__).info(
35
+ "Received signal %s, quitting GUI event loop", signum
36
+ )
37
+ app.quit()
38
+
39
+ signal_module.signal(signal_module.SIGINT, _quit_on_signal)
40
+ signal_module.signal(signal_module.SIGTERM, _quit_on_signal)
41
+ timer.setInterval(200)
42
+ timer.timeout.connect(lambda: None)
43
+ timer.start()
44
+ return timer
45
+
46
+
47
+ @click.command()
48
+ @click.option(
49
+ "--log-level",
50
+ default="INFO",
51
+ type=click.Choice(
52
+ ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False
53
+ ),
54
+ help="Log level",
55
+ show_default=True,
56
+ show_choices=True,
57
+ )
58
+ @click.option(
59
+ "--log-file",
60
+ default=None,
61
+ type=click.Path(path_type=pathlib.Path, dir_okay=False, writable=True),
62
+ help="Log to this file instead of the console",
63
+ show_default=True,
64
+ )
65
+ @click.option(
66
+ "--generate-cellpose-test-data",
67
+ "test_cellpose_image_path",
68
+ default=None,
69
+ type=click.Path(path_type=pathlib.Path, dir_okay=False, writable=True),
70
+ help=(
71
+ "Generate a synthetic Cellpose test TIFF at this path and matching "
72
+ "segmentation .npy, then exit."
73
+ ),
74
+ )
75
+ @click.option(
76
+ "--cellpose-segmentation-output",
77
+ "test_cellpose_segmentation_path",
78
+ default=None,
79
+ type=click.Path(path_type=pathlib.Path, dir_okay=False, writable=True),
80
+ help=(
81
+ "Optional output path for generated Cellpose segmentation .npy "
82
+ "(requires --generate-cellpose-test-data)."
83
+ ),
84
+ )
85
+ @click.option(
86
+ "--cellpose-grid-rows",
87
+ default=10,
88
+ type=click.IntRange(min=1),
89
+ show_default=True,
90
+ help="Number of target rows in generated Cellpose test data.",
91
+ )
92
+ @click.option(
93
+ "--cellpose-grid-cols",
94
+ default=10,
95
+ type=click.IntRange(min=1),
96
+ show_default=True,
97
+ help="Number of target columns in generated Cellpose test data.",
98
+ )
99
+ @click.option(
100
+ "--cellpose-radius",
101
+ default=10,
102
+ type=click.IntRange(min=1),
103
+ show_default=True,
104
+ help="Circle radius in pixels for generated Cellpose test data.",
105
+ )
106
+ @click.option(
107
+ "--cellpose-gap",
108
+ default=10,
109
+ type=click.IntRange(min=0),
110
+ show_default=True,
111
+ help="Gap in pixels between adjacent circles in generated Cellpose test data.",
112
+ )
113
+ @click.option(
114
+ "--cellpose-square-size",
115
+ default=None,
116
+ type=click.IntRange(min=1),
117
+ help=(
118
+ "Optional square output size in pixels for generated Cellpose test data. "
119
+ "When set, radius and gap are auto-fitted while preserving rows/cols."
120
+ ),
121
+ )
122
+ @click.option(
123
+ "--cellpose-preserve-radius-gap/--cellpose-auto-fit-radius-gap",
124
+ default=False,
125
+ show_default=True,
126
+ help=(
127
+ "When --cellpose-square-size is set, preserve the provided "
128
+ "--cellpose-radius and --cellpose-gap instead of auto-fitting them."
129
+ ),
130
+ )
131
+ @click.option(
132
+ "--cellpose-preserve-radius-auto-gap",
133
+ is_flag=True,
134
+ default=False,
135
+ show_default=True,
136
+ help=(
137
+ "When --cellpose-square-size is set, preserve --cellpose-radius and "
138
+ "auto-fit only --cellpose-gap."
139
+ ),
140
+ )
141
+ @click.argument("filename", required=False)
142
+ def main(
143
+ log_level: str,
144
+ log_file: pathlib.Path | None,
145
+ test_cellpose_image_path: pathlib.Path | None,
146
+ test_cellpose_segmentation_path: pathlib.Path | None,
147
+ cellpose_grid_rows: int,
148
+ cellpose_grid_cols: int,
149
+ cellpose_radius: int,
150
+ cellpose_gap: int,
151
+ cellpose_square_size: int | None,
152
+ cellpose_preserve_radius_gap: bool,
153
+ cellpose_preserve_radius_auto_gap: bool,
154
+ filename: str | None,
155
+ ) -> None:
156
+ if test_cellpose_segmentation_path is not None and test_cellpose_image_path is None:
157
+ raise click.UsageError(
158
+ "--cellpose-segmentation-output requires --generate-cellpose-test-data"
159
+ )
160
+ if cellpose_preserve_radius_gap and cellpose_square_size is None:
161
+ raise click.UsageError(
162
+ "--cellpose-preserve-radius-gap requires --cellpose-square-size"
163
+ )
164
+ if cellpose_preserve_radius_auto_gap and cellpose_square_size is None:
165
+ raise click.UsageError(
166
+ "--cellpose-preserve-radius-auto-gap requires --cellpose-square-size"
167
+ )
168
+ if cellpose_preserve_radius_gap and cellpose_preserve_radius_auto_gap:
169
+ raise click.UsageError(
170
+ "--cellpose-preserve-radius-gap and --cellpose-preserve-radius-auto-gap "
171
+ "cannot be used together"
172
+ )
173
+
174
+ if test_cellpose_image_path is not None:
175
+ image_path, segmentation_path = save_cellpose_test_data(
176
+ test_cellpose_image_path,
177
+ segmentation_path=test_cellpose_segmentation_path,
178
+ rows=cellpose_grid_rows,
179
+ cols=cellpose_grid_cols,
180
+ radius=cellpose_radius,
181
+ gap=cellpose_gap,
182
+ square_size=cellpose_square_size,
183
+ preserve_radius_gap=cellpose_preserve_radius_gap,
184
+ preserve_radius_auto_gap=cellpose_preserve_radius_auto_gap,
185
+ )
186
+ click.echo(
187
+ "Generated Cellpose test data:\n"
188
+ f"- image: {image_path}\n"
189
+ f"- segmentation: {segmentation_path}"
190
+ )
191
+ return
192
+
193
+ from neuroseq.utils import init_brian2_logging
194
+
195
+ init_brian2_logging()
196
+ logging.basicConfig(
197
+ level=log_level,
198
+ format="%(asctime)s :: %(levelname)s :: %(name)s :: %(funcName)s :: %(message)s",
199
+ datefmt="%Y-%m-%d %H:%M:%S",
200
+ filename=log_file,
201
+ )
202
+ from qtpy import API_NAME
203
+ from qtpy import QtCore
204
+ from qtpy import QtGui
205
+ from qtpy import QtWidgets
206
+
207
+ from neuroseq.gui import Gui
208
+ from neuroseq.gui_settings import GuiSettings
209
+ from neuroseq.project import Project
210
+ from neuroseq.startup_dialog import StartupDialog
211
+ from neuroseq.theme import apply_app_theme
212
+
213
+ msg = f"Launching {API_NAME} GUI with {log_level} logging to {log_file if log_file else 'console'}"
214
+ click.echo(msg)
215
+ logging.getLogger(__name__).info(msg)
216
+
217
+ # Keep icon raster sizes on integer scale steps to reduce blur on HiDPI displays.
218
+ if hasattr(QtGui.QGuiApplication, "setHighDpiScaleFactorRoundingPolicy"):
219
+ QtGui.QGuiApplication.setHighDpiScaleFactorRoundingPolicy(
220
+ QtCore.Qt.HighDpiScaleFactorRoundingPolicy.RoundPreferFloor
221
+ )
222
+ if hasattr(QtCore.Qt.ApplicationAttribute, "AA_UseHighDpiPixmaps"):
223
+ QtWidgets.QApplication.setAttribute(
224
+ QtCore.Qt.ApplicationAttribute.AA_UseHighDpiPixmaps,
225
+ True,
226
+ )
227
+
228
+ app = QtWidgets.QApplication(sys.argv)
229
+ app._interrupt_timer = _install_interrupt_handlers(app)
230
+
231
+ initial_project: str | pathlib.Path | Project | None = filename
232
+ if initial_project is None:
233
+ settings = GuiSettings()
234
+ # Applied to the whole application (not just the eventual main
235
+ # window) so the startup dialog itself already matches the user's
236
+ # persisted theme, instead of appearing unthemed until a project
237
+ # is chosen.
238
+ apply_app_theme(app, settings.theme)
239
+ startup_dialog = StartupDialog(settings)
240
+ if not startup_dialog.exec():
241
+ logging.getLogger(__name__).info(
242
+ "Startup dialog dismissed without a choice, exiting"
243
+ )
244
+ return
245
+ initial_project = (
246
+ startup_dialog.chosen_filename or startup_dialog.chosen_project
247
+ )
248
+
249
+ gui = Gui(filename=initial_project)
250
+ gui.show()
251
+ logging.getLogger(__name__).info("GUI initialized, entering Qt event loop")
252
+ app.exec()
253
+
254
+
255
+ if __name__ == "__main__":
256
+ main()
@@ -0,0 +1,241 @@
1
+ from __future__ import annotations
2
+
3
+ from qtpy import QtWidgets
4
+
5
+ from neuroseq.data import Data
6
+ from neuroseq.data import Experiment
7
+ from neuroseq.data import SpikeData
8
+ from neuroseq.data import SynapseData
9
+ from neuroseq.data import TraceData
10
+ from neuroseq.project import Project
11
+
12
+
13
+ def describe_data(data: Data) -> str:
14
+ if isinstance(data, TraceData):
15
+ return f"Trace data '{data.name}': {data.traces.shape[0]} neurons, {data.traces.shape[1]} timestamps"
16
+ elif isinstance(data, SpikeData):
17
+ return f"Spike data '{data.name}': {len(data.spike_indices)} spikes"
18
+ elif isinstance(data, SynapseData):
19
+ return f"Synapse data '{data.name}': {data.values.shape[0]}x{data.values.shape[1]} synapses, {data.values.shape[2]} timestamps"
20
+ return ""
21
+
22
+
23
+ def describe_experiment(experiment: Experiment) -> str:
24
+ description = f"Importing {experiment.duration}s of experimental data:\n\n"
25
+ for datum in experiment.data:
26
+ description += f" - {describe_data(datum)}\n"
27
+ return description
28
+
29
+
30
+ class AddExperimentDataDialog(QtWidgets.QDialog):
31
+ """A dialog that allows the user to import data, either as a new Experiment or appended to an existing one."""
32
+
33
+ def __init__(
34
+ self,
35
+ experiment: Experiment,
36
+ project: Project,
37
+ parent: QtWidgets.QWidget | None = None,
38
+ ) -> None:
39
+ super().__init__(parent)
40
+ self.project = project
41
+ self.existing_experiments = list(project.experiments.values())
42
+
43
+ self.experiment = experiment
44
+ self.append_to_experiment = False
45
+ self.append_experiment_name: str = ""
46
+ self.offset: float = 0.0
47
+
48
+ self.setWindowTitle("Import data")
49
+ self.setModal(True)
50
+
51
+ self.layout = QtWidgets.QVBoxLayout()
52
+ self.setLayout(self.layout)
53
+
54
+ self.lbl_description = QtWidgets.QLabel(describe_experiment(self.experiment))
55
+ self.layout.addWidget(self.lbl_description)
56
+
57
+ self.rad_new = QtWidgets.QRadioButton("Create new experiment")
58
+ self.layout.addWidget(self.rad_new)
59
+ self.rad_append = QtWidgets.QRadioButton("Apply to existing experiment")
60
+ self.layout.addWidget(self.rad_append)
61
+ self.rad_new.setChecked(True)
62
+
63
+ self.new_box = QtWidgets.QWidget()
64
+ layout_new_box = QtWidgets.QVBoxLayout(self.new_box)
65
+ layout_new_box.addWidget(QtWidgets.QLabel("Experiment name:"))
66
+ self.txt_new_name = QtWidgets.QLineEdit()
67
+ layout_new_box.addWidget(self.txt_new_name)
68
+ self.layout.addWidget(self.new_box)
69
+ self.txt_new_name.setText(self.experiment.name)
70
+
71
+ self.append_box = QtWidgets.QWidget()
72
+ layout_append_box = QtWidgets.QVBoxLayout(self.append_box)
73
+ layout_append_box.addWidget(QtWidgets.QLabel("Existing experiment:"))
74
+ self.cmb_experiments = QtWidgets.QComboBox()
75
+ self.cmb_experiments.addItems(
76
+ [experiment.name for experiment in self.existing_experiments]
77
+ )
78
+ layout_append_box.addWidget(self.cmb_experiments)
79
+ layout_append_box.addWidget(QtWidgets.QLabel("Timepoint to insert data:"))
80
+ self.cmb_offset = QtWidgets.QComboBox()
81
+ layout_append_box.addWidget(self.cmb_offset)
82
+ self.layout.addWidget(self.append_box)
83
+ self.append_box.setVisible(False)
84
+
85
+ self.lbl_error = QtWidgets.QLabel("")
86
+ self.lbl_error.setStyleSheet("color: red")
87
+ self.layout.addWidget(self.lbl_error)
88
+
89
+ self.layout.addStretch()
90
+
91
+ button_layout = QtWidgets.QHBoxLayout()
92
+ self.save_button = QtWidgets.QPushButton("Import")
93
+ self.cancel_button = QtWidgets.QPushButton("Cancel")
94
+ button_layout.addWidget(self.save_button)
95
+ button_layout.addWidget(self.cancel_button)
96
+ self.layout.addLayout(button_layout)
97
+
98
+ if not self.existing_experiments:
99
+ self.rad_append.setEnabled(False)
100
+
101
+ self.rad_new.toggled.connect(self.rad_new_append_toggled)
102
+ self.txt_new_name.textEdited.connect(self.new_name_text_edited)
103
+ self.cmb_experiments.currentIndexChanged.connect(
104
+ self.cmb_experiments_index_changed
105
+ )
106
+ self.cmb_offset.currentIndexChanged.connect(self.cmb_offset_index_changed)
107
+ self.save_button.clicked.connect(self.accept)
108
+ self.cancel_button.clicked.connect(self.reject)
109
+ self._apply_default_selection()
110
+ self.resize(800, 600)
111
+
112
+ def _apply_default_selection(self) -> None:
113
+ if not self.existing_experiments:
114
+ self.rad_new.setChecked(True)
115
+ self.cmb_experiments_index_changed(0)
116
+ self.new_name_text_edited(self.txt_new_name.text())
117
+ return
118
+
119
+ matching_index = next(
120
+ (
121
+ i
122
+ for i, experiment in enumerate(self.existing_experiments)
123
+ if experiment.name == self.experiment.name
124
+ ),
125
+ -1,
126
+ )
127
+ if matching_index >= 0:
128
+ self.rad_append.setChecked(True)
129
+ self.cmb_experiments.setCurrentIndex(matching_index)
130
+ self.cmb_experiments_index_changed(matching_index)
131
+ else:
132
+ self.rad_new.setChecked(True)
133
+ self.cmb_experiments_index_changed(0)
134
+ self.new_name_text_edited(self.txt_new_name.text())
135
+
136
+ def rad_new_append_toggled(self):
137
+ self.append_to_experiment = self.rad_append.isChecked()
138
+ self.new_box.setVisible(not self.append_to_experiment)
139
+ self.txt_new_name.setEnabled(not self.append_to_experiment)
140
+ self.append_box.setVisible(self.append_to_experiment)
141
+ self.cmb_experiments.setEnabled(self.append_to_experiment)
142
+ self.cmb_offset.setEnabled(self.append_to_experiment)
143
+ if self.append_to_experiment:
144
+ self.cmb_experiments_index_changed(self.cmb_experiments.currentIndex())
145
+ self._refresh_append_error()
146
+ else:
147
+ self.new_name_text_edited(self.txt_new_name.text())
148
+
149
+ def cmb_offset_index_changed(self, index: int) -> None:
150
+ if index < 0:
151
+ self.offset = 0.0
152
+ return
153
+ experiment_index = self.cmb_experiments.currentIndex()
154
+ if experiment_index < 0 or experiment_index >= len(self.existing_experiments):
155
+ self.offset = 0.0
156
+ return
157
+ self.offset = self.existing_experiments[experiment_index].timepoints[index]
158
+ self._refresh_append_error()
159
+
160
+ def cmb_experiments_index_changed(self, index: int) -> None:
161
+ self.cmb_offset.clear()
162
+ if index < 0 or index >= len(self.existing_experiments):
163
+ return
164
+ self.append_experiment_name = self.existing_experiments[index].name
165
+ timepoints = list(self.existing_experiments[index].timepoints)
166
+ option_labels = []
167
+ for i, timepoint in enumerate(timepoints):
168
+ label = f"{timepoint * 1000:.5g} ms"
169
+ if i == len(timepoints) - 1:
170
+ label = f"{label} (Append)"
171
+ else:
172
+ label = f"{label} ({self._insert_mode_label(index, timepoint)})"
173
+ option_labels.append(label)
174
+ self.cmb_offset.addItems(option_labels)
175
+ # if there is only start/end, default to start; otherwise default to append
176
+ if self.cmb_offset.count() > 0:
177
+ default_offset_index = 0 if len(timepoints) <= 2 else len(timepoints) - 1
178
+ self.cmb_offset.setCurrentIndex(default_offset_index)
179
+ # check if appending this data to the selected experiment is valid
180
+ self._refresh_append_error()
181
+
182
+ def new_name_text_edited(self, text: str) -> None:
183
+ self.experiment.name = text
184
+ self.lbl_error.clear()
185
+ self.save_button.setEnabled(True)
186
+
187
+ def _insert_mode_label(self, experiment_index: int, offset: float) -> str:
188
+ if experiment_index < 0 or experiment_index >= len(self.existing_experiments):
189
+ return "partially replace + insert"
190
+ existing_experiment = self.existing_experiments[experiment_index]
191
+ if offset >= existing_experiment.duration:
192
+ return "Append"
193
+
194
+ has_overlap = False
195
+ has_new_only = False
196
+
197
+ for new_datum in self.experiment.data:
198
+ existing_datum = next(
199
+ (
200
+ datum
201
+ for datum in existing_experiment.data
202
+ if datum.name == new_datum.name and type(datum) is type(new_datum)
203
+ ),
204
+ None,
205
+ )
206
+ if existing_datum is None:
207
+ continue
208
+ if isinstance(existing_datum, TraceData) and isinstance(
209
+ new_datum, TraceData
210
+ ):
211
+ existing_ids = set(map(int, existing_datum.indices))
212
+ new_ids = set(map(int, new_datum.indices))
213
+ elif isinstance(existing_datum, SpikeData) and isinstance(
214
+ new_datum, SpikeData
215
+ ):
216
+ existing_ids = set(map(int, existing_datum.spike_indices))
217
+ new_ids = set(map(int, new_datum.spike_indices))
218
+ elif isinstance(existing_datum, SynapseData) and isinstance(
219
+ new_datum, SynapseData
220
+ ):
221
+ return "overwrite"
222
+ else:
223
+ continue
224
+ if existing_ids & new_ids:
225
+ has_overlap = True
226
+ if new_ids - existing_ids:
227
+ has_new_only = True
228
+
229
+ if has_overlap and has_new_only:
230
+ return "partially replace + insert"
231
+ if has_overlap:
232
+ return "overwrite"
233
+ return "insert alongside existing data"
234
+
235
+ def _refresh_append_error(self) -> None:
236
+ error = self.project.issues_with_appending_experimental_data(
237
+ self.append_experiment_name,
238
+ self.experiment.data,
239
+ )
240
+ self.lbl_error.setText(error)
241
+ self.save_button.setEnabled(error == "")
@@ -0,0 +1,182 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import logging
5
+ import os
6
+ import sys
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ from qtpy import QtCore
13
+
14
+ from neuroseq.data import SpikeData
15
+ from neuroseq.data import SynapseData
16
+ from neuroseq.data import TraceData
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class AnalysisPlugin:
21
+ id: str
22
+ label: str
23
+ icon: str
24
+ render: Callable[
25
+ [object, TraceData | SpikeData | SynapseData, slice | None, slice | None], None
26
+ ]
27
+ open_dialog: (
28
+ Callable[
29
+ [
30
+ object,
31
+ object,
32
+ TraceData | SpikeData | SynapseData,
33
+ slice | None,
34
+ slice | None,
35
+ ],
36
+ None,
37
+ ]
38
+ | None
39
+ ) = None
40
+
41
+ def __str__(self) -> str:
42
+ return self.label
43
+
44
+
45
+ #: Environment variable overriding the per-user plugin folder (used by tests).
46
+ USER_PLUGIN_DIR_ENV = "NEUROSEQ_USER_PLUGIN_DIR"
47
+
48
+
49
+ def _plugins_directory() -> Path:
50
+ """Built-in plugins shipped inside the package."""
51
+ return Path(__file__).resolve().parent / "resources" / "analysis_plugins"
52
+
53
+
54
+ def user_plugins_directory(create: bool = False) -> Path:
55
+ """Per-user plugin folder (outside the package), e.g. for chat-authored plugins.
56
+
57
+ ``NEUROSEQ_USER_PLUGIN_DIR`` overrides the location. Otherwise the folder
58
+ lives under the platform's user data location (``.../neuroseq/analysis_plugins``).
59
+ """
60
+ override = os.environ.get(USER_PLUGIN_DIR_ENV)
61
+ if override:
62
+ directory = Path(override).expanduser()
63
+ else:
64
+ base = QtCore.QStandardPaths.writableLocation(
65
+ QtCore.QStandardPaths.StandardLocation.GenericDataLocation
66
+ )
67
+ root = Path(base) / "neuroseq" if base else Path.home() / ".neuroseq"
68
+ directory = root / "analysis_plugins"
69
+ if create:
70
+ directory.mkdir(parents=True, exist_ok=True)
71
+ return directory
72
+
73
+
74
+ def builtin_plugin_names() -> set[str]:
75
+ """Module stems in the built-in plugin folder, including helper modules."""
76
+ plugins_dir = _plugins_directory()
77
+ if not plugins_dir.is_dir():
78
+ return set()
79
+ return {path.stem for path in plugins_dir.glob("*.py")}
80
+
81
+
82
+ def _load_plugin_module(
83
+ plugin_file: Path, namespace: str = "neuroseq.analysis_plugin"
84
+ ) -> ModuleType | None:
85
+ logger = logging.getLogger(__name__)
86
+ spec = importlib.util.spec_from_file_location(
87
+ f"{namespace}.{plugin_file.stem}",
88
+ plugin_file,
89
+ )
90
+ if spec is None or spec.loader is None:
91
+ logger.warning(
92
+ f"Could not create module spec for analysis plugin: {plugin_file}"
93
+ )
94
+ return None
95
+ module = importlib.util.module_from_spec(spec)
96
+ # Match normal import semantics. Decorators such as ``@dataclass`` resolve
97
+ # annotations through ``sys.modules`` while the module is executing.
98
+ sys.modules[spec.name] = module
99
+ try:
100
+ spec.loader.exec_module(module)
101
+ except Exception as exc:
102
+ sys.modules.pop(spec.name, None)
103
+ logger.exception(f"Failed to load analysis plugin {plugin_file}: {exc}")
104
+ return None
105
+ return module
106
+
107
+
108
+ def _plugin_files() -> list[tuple[Path, str]]:
109
+ """Built-in plugin files followed by non-shadowing user plugin files."""
110
+ logger = logging.getLogger(__name__)
111
+ files: list[tuple[Path, str]] = []
112
+ plugins_dir = _plugins_directory()
113
+ if plugins_dir.is_dir():
114
+ files.extend(
115
+ (path, "neuroseq.analysis_plugin")
116
+ for path in sorted(plugins_dir.glob("*.py"))
117
+ if not path.stem.startswith("_")
118
+ )
119
+ builtin_names = builtin_plugin_names()
120
+ user_dir = user_plugins_directory()
121
+ if user_dir.is_dir() and user_dir.resolve() != plugins_dir.resolve():
122
+ for path in sorted(user_dir.glob("*.py")):
123
+ if path.stem.startswith("_"):
124
+ continue
125
+ if path.stem in builtin_names:
126
+ logger.warning(
127
+ f"Skipping user analysis plugin {path}: name collides with a "
128
+ f"built-in plugin"
129
+ )
130
+ continue
131
+ files.append((path, "neuroseq.user_analysis_plugin"))
132
+ return files
133
+
134
+
135
+ def discover_analysis_plugins() -> list[AnalysisPlugin]:
136
+ logger = logging.getLogger(__name__)
137
+ plugins: list[AnalysisPlugin] = []
138
+ builtin_ids: set[str] = set()
139
+
140
+ for plugin_file, namespace in _plugin_files():
141
+ is_user = namespace != "neuroseq.analysis_plugin"
142
+ module = _load_plugin_module(plugin_file, namespace)
143
+ if module is None:
144
+ continue
145
+
146
+ render = getattr(module, "render_analysis", None)
147
+ if not callable(render):
148
+ logger.warning(
149
+ f"Skipping analysis plugin {plugin_file}: missing callable render_analysis"
150
+ )
151
+ continue
152
+
153
+ plugin_id = str(getattr(module, "PLUGIN_ID", plugin_file.stem))
154
+ if is_user and plugin_id in builtin_ids:
155
+ logger.warning(
156
+ f"Skipping user analysis plugin {plugin_file}: PLUGIN_ID "
157
+ f"{plugin_id!r} collides with a built-in plugin"
158
+ )
159
+ continue
160
+ if not is_user:
161
+ builtin_ids.add(plugin_id)
162
+ plugin_label = getattr(module, "PLUGIN_NAME", plugin_file.stem)
163
+ plugin_icon = getattr(module, "PLUGIN_ICON", "mdi6.puzzle")
164
+ open_dialog = getattr(module, "open_analysis_dialog", None)
165
+ if open_dialog is not None and not callable(open_dialog):
166
+ logger.warning(
167
+ f"Ignoring non-callable open_analysis_dialog in analysis plugin "
168
+ f"{plugin_file}"
169
+ )
170
+ open_dialog = None
171
+
172
+ plugins.append(
173
+ AnalysisPlugin(
174
+ id=plugin_id,
175
+ label=str(plugin_label),
176
+ icon=str(plugin_icon),
177
+ render=render,
178
+ open_dialog=open_dialog,
179
+ )
180
+ )
181
+
182
+ return plugins