np-workflows 1.6.89__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 (76) hide show
  1. np_workflows/__init__.py +7 -0
  2. np_workflows/assets/images/logo_np_hab.png +0 -0
  3. np_workflows/assets/images/logo_np_vis.png +0 -0
  4. np_workflows/experiments/__init__.py +1 -0
  5. np_workflows/experiments/dynamic_routing/__init__.py +2 -0
  6. np_workflows/experiments/dynamic_routing/main.py +117 -0
  7. np_workflows/experiments/dynamic_routing/widgets.py +82 -0
  8. np_workflows/experiments/openscope_P3/P3_workflow_widget.py +83 -0
  9. np_workflows/experiments/openscope_P3/__init__.py +2 -0
  10. np_workflows/experiments/openscope_P3/main_P3_pilot.py +217 -0
  11. np_workflows/experiments/openscope_barcode/__init__.py +2 -0
  12. np_workflows/experiments/openscope_barcode/barcode_workflow_widget.py +83 -0
  13. np_workflows/experiments/openscope_barcode/camstim_scripts/barcode_mapping_script.py +138 -0
  14. np_workflows/experiments/openscope_barcode/camstim_scripts/barcode_opto_script.py +219 -0
  15. np_workflows/experiments/openscope_barcode/main_barcode_pilot.py +217 -0
  16. np_workflows/experiments/openscope_loop/__init__.py +2 -0
  17. np_workflows/experiments/openscope_loop/camstim_scripts/barcode_mapping_script.py +138 -0
  18. np_workflows/experiments/openscope_loop/camstim_scripts/barcode_opto_script.py +219 -0
  19. np_workflows/experiments/openscope_loop/loop_workflow_widget.py +83 -0
  20. np_workflows/experiments/openscope_loop/main_loop_pilot.py +217 -0
  21. np_workflows/experiments/openscope_psycode/__init__.py +2 -0
  22. np_workflows/experiments/openscope_psycode/main_psycode_pilot.py +217 -0
  23. np_workflows/experiments/openscope_psycode/psycode_workflow_widget.py +83 -0
  24. np_workflows/experiments/openscope_v2/__init__.py +2 -0
  25. np_workflows/experiments/openscope_v2/main_v2_pilot.py +217 -0
  26. np_workflows/experiments/openscope_v2/v2_workflow_widget.py +83 -0
  27. np_workflows/experiments/openscope_vippo/__init__.py +2 -0
  28. np_workflows/experiments/openscope_vippo/main_vippo_pilot.py +217 -0
  29. np_workflows/experiments/openscope_vippo/vippo_workflow_widget.py +83 -0
  30. np_workflows/experiments/task_trained_network/__init__.py +2 -0
  31. np_workflows/experiments/task_trained_network/camstim_scripts/make_tt_stims.py +23 -0
  32. np_workflows/experiments/task_trained_network/camstim_scripts/oct22_tt_stim_script.py +69 -0
  33. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_00.stim +5 -0
  34. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_01.stim +5 -0
  35. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_02.stim +5 -0
  36. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_03.stim +5 -0
  37. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_04.stim +5 -0
  38. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_05.stim +5 -0
  39. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_06.stim +5 -0
  40. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_07.stim +5 -0
  41. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_08.stim +5 -0
  42. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_09.stim +5 -0
  43. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_10.stim +5 -0
  44. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_11.stim +5 -0
  45. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_12.stim +5 -0
  46. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_13.stim +5 -0
  47. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_14.stim +5 -0
  48. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_15.stim +5 -0
  49. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_16.stim +5 -0
  50. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_17.stim +5 -0
  51. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_18.stim +5 -0
  52. np_workflows/experiments/task_trained_network/camstim_scripts/stims/flash_250ms.stim +20 -0
  53. np_workflows/experiments/task_trained_network/camstim_scripts/stims/gabor_20_deg_250ms.stim +30 -0
  54. np_workflows/experiments/task_trained_network/camstim_scripts/stims/old_stim.stim +5 -0
  55. np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed.stim +5 -0
  56. np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed_1st.stim +5 -0
  57. np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed_2nd.stim +5 -0
  58. np_workflows/experiments/task_trained_network/camstim_scripts/ttn_main_script.py +130 -0
  59. np_workflows/experiments/task_trained_network/camstim_scripts/ttn_mapping_script.py +138 -0
  60. np_workflows/experiments/task_trained_network/camstim_scripts/ttn_opto_script.py +219 -0
  61. np_workflows/experiments/task_trained_network/main_ttn_pilot.py +263 -0
  62. np_workflows/experiments/task_trained_network/ttn_session_widget.py +83 -0
  63. np_workflows/experiments/task_trained_network/ttn_stim_config.py +213 -0
  64. np_workflows/experiments/templeton/__init__.py +2 -0
  65. np_workflows/experiments/templeton/main.py +105 -0
  66. np_workflows/experiments/templeton/widgets.py +82 -0
  67. np_workflows/shared/__init__.py +3 -0
  68. np_workflows/shared/base_experiments.py +826 -0
  69. np_workflows/shared/camstim_scripts/flash_250ms.stim +20 -0
  70. np_workflows/shared/camstim_scripts/gabor_20_deg_250ms.stim +30 -0
  71. np_workflows/shared/npxc.py +187 -0
  72. np_workflows/shared/widgets.py +705 -0
  73. np_workflows-1.6.89.dist-info/METADATA +85 -0
  74. np_workflows-1.6.89.dist-info/RECORD +76 -0
  75. np_workflows-1.6.89.dist-info/WHEEL +4 -0
  76. np_workflows-1.6.89.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,705 @@
1
+ import datetime
2
+ import io
3
+ import logging
4
+ import pathlib
5
+ import re
6
+ import threading
7
+ import time
8
+ from typing import Literal, NoReturn
9
+
10
+ import IPython
11
+ import IPython.display
12
+ import ipywidgets as ipw
13
+ import np_config
14
+ import np_logging
15
+ import np_services
16
+ import np_session
17
+ import PIL.Image
18
+ import PIL.ImageDraw
19
+
20
+ import np_workflows.shared.npxc as npxc
21
+
22
+ logger = np_logging.getLogger(__name__)
23
+
24
+ np_logging.getLogger('Comm').propagate = False
25
+ np_logging.getLogger('PIL').propagate = False
26
+
27
+ global_state = {}
28
+ """Global variable for persisting widget states."""
29
+
30
+ def elapsed_time_widget() -> IPython.display.DisplayHandle | None:
31
+ """Displays a clock showing the elapsed time since the cell was first run."""
32
+
33
+ clock_widget = ipw.Label("")
34
+ reminder_widget = ipw.Label("Remember to restart JupyterLab and run update.bat before every experiment!")
35
+ global start_time
36
+ if "start_time" not in globals():
37
+ start_time = time.time()
38
+
39
+ if isinstance(start_time, datetime.datetime):
40
+ start_time = start_time.timestamp()
41
+
42
+ def update_timer() -> NoReturn:
43
+ while True:
44
+ elapsed_sec = time.time() - start_time
45
+ hours, remainder = divmod(elapsed_sec, 3600)
46
+ minutes, seconds = divmod(remainder, 60)
47
+ clock_widget.value = "Elapsed time: {:02}h {:02}m {:02}s".format(
48
+ int(hours), int(minutes), int(seconds)
49
+ )
50
+ if hours > 4: # ipywidgets >= 8.0
51
+ clock_widget.style = dict(text_color="red",)
52
+ time.sleep(0.2)
53
+
54
+ thread = threading.Thread(target=update_timer, args=())
55
+ thread.start()
56
+ return IPython.display.display(ipw.VBox([clock_widget, reminder_widget]))
57
+
58
+
59
+ def user_and_mouse_widget() -> tuple[np_session.User, np_session.Mouse]:
60
+ console = ipw.Output()
61
+ user_description = "User:"
62
+ mouse_description = "Mouse:"
63
+ user_widget = ipw.Select(options=npxc.lims_user_ids, description=user_description)
64
+ mouse_widget = ipw.Text(value=str(npxc.default_mouse_id), description=mouse_description)
65
+ for widget, string in zip((user_widget, mouse_widget), ('user', 'mouse')):
66
+ if (selected := global_state.get(f'selected_{string}')):
67
+ widget.value = selected
68
+ with console:
69
+ print(f'Current {string}: {selected}')
70
+ user = np_session.User(str(user_widget.value))
71
+ mouse = np_session.Mouse(str(mouse_widget.value))
72
+
73
+ def update_user(new_user: str):
74
+ if str(user) == (new := str(new_user).strip()):
75
+ return
76
+ user.__init__(new)
77
+ global_state['selected_user'] = new
78
+ with console:
79
+ print(f"User updated: {user}")
80
+
81
+ def update_mouse(new_mouse: str):
82
+ if str(mouse) == (new := str(new_mouse).strip()):
83
+ return
84
+ if len(new) < 6:
85
+ return
86
+ global_state['selected_mouse'] = new
87
+ mouse.__init__(new)
88
+ with console:
89
+ print(f"Mouse updated: {mouse}")
90
+
91
+ def new_value(change) -> None:
92
+ if change['name'] != 'value':
93
+ return
94
+ if (options := getattr(change['owner'], 'options', None)) and change['new'] not in options:
95
+ return
96
+ if change['new'] == change['old']:
97
+ return
98
+ if (desc := getattr(change['owner'], 'description')) == user_description:
99
+ update_user(change['new'])
100
+ elif desc == mouse_description:
101
+ update_mouse(change['new'])
102
+
103
+ user_widget.observe(new_value)
104
+ mouse_widget.observe(new_value)
105
+
106
+ IPython.display.display(ipw.VBox([user_widget, mouse_widget, console]))
107
+ return user, mouse
108
+
109
+
110
+ def mtrain_widget(
111
+ labtracks_mouse_id: str | int | np_session.Mouse,
112
+ ) -> IPython.display.DisplayHandle | None:
113
+ """Displays a widget to view and edit MTrain regimen/stage for a mouse.
114
+ """
115
+ if not isinstance(labtracks_mouse_id, np_session.Mouse):
116
+ mtrain = np_session.MTrain(labtracks_mouse_id)
117
+ else:
118
+ mtrain = labtracks_mouse_id.mtrain
119
+
120
+ all_regimens = mtrain.get_all("regimens")
121
+ regimen_names = sorted(_["name"] for _ in all_regimens)
122
+
123
+ widget = ipw.GridspecLayout(n_rows=4, n_columns=2)
124
+
125
+ # labels
126
+ widget[0, 0] = ipw.Label(f"Mouse: {mtrain.mouse_id}")
127
+ widget[1, 0] = regimen_label = ipw.Label("Regimen:")
128
+ widget[2, 0] = stage_label = ipw.Label("Stage:")
129
+
130
+ # dropdowns
131
+ widget[1, 1] = regimen_dropdown = ipw.Dropdown(options=regimen_names)
132
+ widget[2, 1] = stage_dropdown = ipw.Dropdown(
133
+ options=sorted([_["name"] for _ in mtrain.stages])
134
+ )
135
+ stage_dropdown.stages: list[dict] = mtrain.stages
136
+
137
+ widget[3, 1] = update_button = ipw.Button(description="Update", disabled=True)
138
+
139
+ console = ipw.Output()
140
+
141
+ display = ipw.VBox([widget, console])
142
+
143
+ def on_regimen_change(change: dict):
144
+ update_button.disabled = True
145
+ new_regimen_dict = [
146
+ regimen
147
+ for regimen in all_regimens
148
+ if regimen["name"] == regimen_dropdown.value
149
+ ][0]
150
+ stage_dropdown.options = sorted([_["name"] for _ in new_regimen_dict["stages"]])
151
+ stage_dropdown.value = None
152
+ stage_dropdown.stages = new_regimen_dict["stages"]
153
+
154
+ regimen_dropdown.observe(on_regimen_change, names="value")
155
+
156
+ def reset_update_button():
157
+ update_button.description = "Update"
158
+ update_button.disabled = True
159
+ update_button.button_style = ""
160
+
161
+ def on_stage_change(change: dict):
162
+ reset_update_button()
163
+ if change["new"] is None:
164
+ return
165
+ if change["new"] != stage_label.value or str(
166
+ regimen_dropdown.value
167
+ ) != str(regimen_label.value):
168
+ # enable button if stage name changed, or regimen name changed (some
169
+ # regimens have the same stage names as other regimens)
170
+ update_button.disabled = False
171
+ update_button.button_style = "warning"
172
+
173
+ stage_dropdown.observe(on_stage_change, names="value")
174
+
175
+ def update_label_values() -> None:
176
+ regimen_label.value = f'Regimen: {mtrain.regimen["name"]}'
177
+ stage_label.value = f'Stage: {mtrain.stage["name"]}'
178
+
179
+ def update_dropdown_values() -> None:
180
+ regimen_dropdown.value = mtrain.regimen["name"]
181
+ stage_dropdown.value = mtrain.stage["name"]
182
+
183
+ def update_regimen_and_stage_in_mtrain(b):
184
+ update_button.description = "Updating..."
185
+ update_button.disabled = True
186
+
187
+ old_regimen_name = regimen_label.value
188
+ old_stage_name = stage_label.value
189
+
190
+ new_regimen_dict = [
191
+ _ for _ in all_regimens if _["name"] == regimen_dropdown.value
192
+ ][0]
193
+ new_stage_dict = [
194
+ _ for _ in stage_dropdown.stages if _["name"] == stage_dropdown.value
195
+ ][0]
196
+
197
+ mtrain.set_regimen_and_stage(regimen=new_regimen_dict, stage=new_stage_dict)
198
+ update_all()
199
+
200
+ regimen_name_changed: bool = new_regimen_dict["name"] not in old_regimen_name
201
+ stage_name_changed: bool = new_stage_dict["name"] not in old_stage_name
202
+ with console:
203
+ if regimen_name_changed:
204
+ print(f'{old_regimen_name} changed to {mtrain.regimen["name"]}\n')
205
+ if stage_name_changed or regimen_name_changed:
206
+ print(f'{old_stage_name} changed to {mtrain.stage["name"]}\n')
207
+
208
+ update_button.on_click(update_regimen_and_stage_in_mtrain)
209
+
210
+ def update_all():
211
+ update_label_values()
212
+ update_dropdown_values()
213
+ reset_update_button()
214
+ update_label_values()
215
+ update_dropdown_values()
216
+
217
+ update_all()
218
+
219
+ return IPython.display.display(display)
220
+
221
+ def check_widget(check: str, *checks: str) -> ipw.Widget:
222
+ layout = ipw.Layout(min_width="600px")
223
+ widget = ipw.VBox([
224
+ ipw.Label(check, layout=layout),
225
+ *(ipw.Checkbox(description=_, layout=layout) for _ in checks),
226
+ # ipw.Button(description="Continue", disabled=True)
227
+ ])
228
+ return widget
229
+
230
+ def await_all_checkboxes(widget: ipw.Box) -> None:
231
+ while any(_.value is False for _ in widget.children if isinstance(_, ipw.Checkbox)):
232
+ time.sleep(0.1)
233
+
234
+
235
+ def check_openephys_widget() -> None:
236
+ check = "OpenEphys checks:"
237
+ checks = (
238
+ "Record Node paths are set to two different drives (A: & B: or E: & G:)",
239
+ "Each Record Node recording only ABC or DEF probes",
240
+ "Tip-reference on all probes",
241
+ "Barcodes visible",
242
+ )
243
+ IPython.display.display(widget := check_widget(check, *checks))
244
+
245
+ def check_hardware_widget() -> None:
246
+ check = "Stage checks:"
247
+ checks = (
248
+ "Cartridge raised (fully retract probes before raising!)",
249
+ "Water lines flushed if lick-spout required",
250
+ "Eye-tracking mirror is clean",
251
+ "Tail-cone is not loose",
252
+ )
253
+ IPython.display.display(widget := check_widget(check, *checks))
254
+
255
+ def check_mouse_widget() -> None:
256
+ check = "Mouse checks before lowering cartridge:"
257
+ checks = (
258
+ "Stabilization screw",
259
+ ("Silicon oil applied" if npxc.RIG.idx == 0 else "Quickcast removed, agarose applied"),
260
+ "Tail cone down",
261
+ "Continuity/Resistance check",
262
+ )
263
+ IPython.display.display(widget := check_widget(check, *checks))
264
+
265
+ def pre_stim_check_widget() -> None:
266
+ check = "Before running stim:"
267
+ checks = (
268
+ "Behavior cameras are in focus",
269
+ "Eye-tracking mirror in place",
270
+ "Windows minimized on Stim computer (Win+D)",
271
+ "Monitor closed",
272
+ "Photodoc light off",
273
+ "Curtain down",
274
+ )
275
+ IPython.display.display(widget := check_widget(check, *checks))
276
+
277
+ def finishing_checks_widget() -> None:
278
+ check = "Finishing checks:"
279
+ checks = (
280
+ "Add quickcast etc.",
281
+ "Remove and water mouse",
282
+ "Dip probes",
283
+ )
284
+ IPython.display.display(widget := check_widget(check, *checks))
285
+
286
+
287
+ def wheel_height_widget(session: np_session.PipelineSession) -> IPython.display.DisplayHandle | None:
288
+ "Saves wheel height to platform_json and stores in `mouse.state['wheel_height']`."
289
+
290
+ layout = ipw.Layout(max_width='130px')
291
+
292
+ prev_height = session.mouse.state.get('wheel_height', 0)
293
+ height_counter = ipw.BoundedFloatText(value=prev_height, min=0, max=10, step=0.1, description="Wheel height", layout=layout)
294
+ save_button = ipw.Button(description='Save', button_style='warning', layout=layout)
295
+
296
+ def on_click(b):
297
+ session.platform_json.wheel_height = height_counter.value
298
+ session.mouse.state['wheel_height'] = height_counter.value
299
+ save_button.button_style = 'success'
300
+ save_button.description = 'Saved'
301
+ save_button.on_click(on_click)
302
+ return IPython.display.display(ipw.VBox([height_counter,save_button]))
303
+
304
+
305
+ def di_widget(session: np_session.PipelineSession) -> IPython.display.DisplayHandle | None:
306
+ "Supply a path or a platform json instance. Saves a JSON file with the dye used in the session and a timestamp."
307
+
308
+ di_info: dict[str, int | str] = dict(
309
+ EndTime=0, StartTime=npxc.now(), dii_description="", times_dipped=0, previous_uses="",
310
+ )
311
+ di_info.update(session.platform_json.DiINotes)
312
+
313
+ layout = ipw.Layout(max_width='180px')
314
+ dipped_counter = ipw.IntText(value=0, min=0, max=99, description="Dipped count", layout=layout)
315
+ usage_counter = ipw.IntText(value=0, min=0, max=99, description="Previous uses", layout=layout)
316
+ dye_dropdown = ipw.Dropdown(options=['CM-DiI 100%', 'DiO'], layout=layout)
317
+ save_button = ipw.Button(description='Save', button_style='warning', layout=layout)
318
+
319
+ def update_di_info():
320
+ di_info['EndTime'] = npxc.now()
321
+ di_info['times_dipped'] = str(dipped_counter.value)
322
+ di_info['dii_description'] = str(dye_dropdown.value)
323
+ di_info['previous_uses'] = str(usage_counter.value)
324
+
325
+ def on_click(b):
326
+ update_di_info()
327
+ session.platform_json.DiINotes = di_info
328
+ save_button.description = 'Saved'
329
+ save_button.button_style = 'success'
330
+
331
+ save_button.on_click(on_click)
332
+ return IPython.display.display(ipw.VBox([
333
+ dipped_counter, dye_dropdown,
334
+ usage_counter, save_button]))
335
+
336
+
337
+ def dye_info_widget(session: np_session.PipelineSession) -> IPython.display.DisplayHandle | None:
338
+ """
339
+ - scan barcode or enter ID number for the dye used
340
+ - change dye description if incorrect (DiI, DiO)
341
+ - increment number of times probes were dipped this session
342
+ - hit `Save` to store info in platform.json
343
+ """
344
+
345
+ di_info: dict[str, int | str] = dict(
346
+ EndTime=0, StartTime=npxc.now(), dii_description="", times_dipped=0, previous_uses="",
347
+ )
348
+ di_info.update(session.platform_json.DiINotes)
349
+
350
+ def width(w):
351
+ return ipw.Layout(max_width=f'{w}px')
352
+
353
+ dye_id_entry = ipw.Text(value=None, description='Dye ID', layout=width(250), placeholder='Enter ID or scan barcode')
354
+ ipw.Button(description='Record single use', button_style='warning', layout=width(180))
355
+ first_usage = ipw.Text(value='', description="First use", layout=width(250), disabled=True)
356
+ dye_dropdown = ipw.Dropdown(description="Description:", options=np_session.Dye.descriptions, layout=width(180))
357
+ dipped_counter = ipw.IntText(value=int(di_info['times_dipped'] or 0), min=0, max=99, description="Dipped count", layout=width(150))
358
+ usage_counter = ipw.IntText(value=int(di_info['previous_uses'] or 0), min=0, max=99, description="Previous uses", layout=width(180), disabled=True)
359
+ save_button = ipw.Button(description='Save', button_style='warning', layout=width(180))
360
+ if (desc := di_info['dii_description']) in np_session.Dye.descriptions:
361
+ dye_dropdown.value = desc
362
+
363
+ def update_display(_):
364
+ dye = np_session.Dye(int(str(dye_id_entry.value)))
365
+ dye_dropdown.value = dye.description
366
+ usage_counter.value = dye.previous_uses
367
+ first_usage.value = f'{dye.first_use}'
368
+ dye_id_entry.observe(update_display, 'value')
369
+
370
+ def record_dye_usage():
371
+ dye = np_session.Dye(int(str(dye_id_entry.value)))
372
+ dye.description = dye_dropdown.value
373
+ dye.increment_uses()
374
+
375
+ def update_di_info():
376
+ di_info['EndTime'] = npxc.now()
377
+ di_info['times_dipped'] = str(dipped_counter.value)
378
+ di_info['dii_description'] = str(dye_dropdown.value)
379
+ di_info['previous_uses'] = str(usage_counter.value)
380
+
381
+ def on_click(b):
382
+ update_di_info()
383
+ record_dye_usage()
384
+ session.platform_json.DiINotes = di_info
385
+ save_button.description = 'Saved'
386
+ save_button.button_style = 'success'
387
+
388
+ save_button.on_click(on_click)
389
+ return IPython.display.display(ipw.VBox([
390
+ dye_id_entry,
391
+ dipped_counter, dye_dropdown,
392
+ usage_counter, first_usage, save_button]))
393
+
394
+ def dye_widget(session_folder: pathlib.Path) -> IPython.display.DisplayHandle | None:
395
+ "Supply a path - saves a JSON file with the dye used in the session and a timestamp."
396
+
397
+ dict(
398
+ EndTime=0, StartTime=0, dii_description="DiI", times_dipped=0,
399
+ )
400
+
401
+ class DyeRecorder(np_services.JsonRecorder):
402
+ log_name = f'{session_folder.name}_dye.json'
403
+ log_root = session_folder
404
+
405
+ dye_dropdown = ipw.Dropdown(options=['DiI', 'DiO'])
406
+ save_button = ipw.Button(description='Save', button_style='warning')
407
+ def on_click(b):
408
+ DyeRecorder.write(dict(dye=dye_dropdown.value, datetime=datetime.datetime.now(), time=time.time()))
409
+ save_button.button_style = 'success'
410
+ save_button.description = 'Saved'
411
+ save_button.on_click(on_click)
412
+ return IPython.display.display(ipw.VBox([dye_dropdown, save_button]))
413
+
414
+ ISICoords = list[dict[Literal['x', 'y', 'z'], float]]
415
+ ISISpaces = dict[Literal['image_space', 'reticle_space'], ISICoords | None]
416
+ ISITargets = dict[Literal['insertion_targets', 'intended_insertion', 'actual_insertion'], ISISpaces]
417
+
418
+ def isi_targets(
419
+ labtracks_mouse_id: str | int | np_session.LIMS2MouseInfo,
420
+ )-> None | ISITargets:
421
+ mouse = np_session.LIMS2MouseInfo(labtracks_mouse_id) if not isinstance(labtracks_mouse_id, np_session.LIMS2MouseInfo) else labtracks_mouse_id
422
+ if (exp_id := mouse.isi_id) is None:
423
+ return None
424
+ exps = mouse.isi_info['isi_experiments']
425
+ isi = [e for e in exps if e['id'] == exp_id]
426
+ return isi[0]['targets'] if isi else None
427
+
428
+ def isi_widget(
429
+ labtracks_mouse_id: str | int | np_session.LIMS2MouseInfo, colormap: bool = False,
430
+ ) -> IPython.display.DisplayHandle | None:
431
+ """Displays ISI target map from lims (contours only), or colormap overlay if
432
+ `show_colormap = True`."""
433
+ if not isinstance(labtracks_mouse_id, np_session.LIMS2MouseInfo):
434
+ mouse_info = np_session.LIMS2MouseInfo(labtracks_mouse_id)
435
+ else:
436
+ mouse_info = labtracks_mouse_id
437
+ mouse_info.fetch() # refresh in case targets were updated recently
438
+
439
+ if colormap:
440
+ key = "isi_image_overlay_path"
441
+ else:
442
+ key = "target_map_image_path"
443
+
444
+ try:
445
+ lims_path = mouse_info.isi_info[key]
446
+ except ValueError:
447
+ print("Mouse is not in lims.")
448
+ return
449
+ except (AttributeError, TypeError):
450
+ print("No ISI map found for this mouse.")
451
+ return
452
+ except KeyError:
453
+ print(f"ISI info found for this mouse, but {key=!r} is missing.")
454
+ return IPython.display.display(IPython.display.JSON(mouse_info.isi_info))
455
+ else:
456
+ path: pathlib.Path = np_config.normalize_path(lims_path)
457
+ print(f"ISI map found for {mouse_info.np_id}:\n{path}")
458
+ img = PIL.Image.open(path)
459
+ if all_targets := isi_targets(mouse_info):
460
+ colors = {'insertion_targets': 'red', 'intended_insertion': 'yellow', 'actual_insertion': 'blue'}
461
+ for targets, spaces in all_targets.items():
462
+ coords = spaces['image_space']
463
+ if coords is None:
464
+ continue
465
+ draw = PIL.ImageDraw.Draw(img)
466
+ draw.line([(_['x'], _['y']) for _ in coords],
467
+ fill=colors[targets],
468
+ width=3)
469
+ else:
470
+ logger.debug("No ISI targets found for %r in lims, ISI experiment id %s", mouse_info, mouse_info.isi_id)
471
+ ## displaying img directly no longer works (due to jupyterlab 4.0?)
472
+ # return IPython.display.display(img)
473
+ membuf = io.BytesIO()
474
+ img.save(membuf, format="png")
475
+ return IPython.display.display(ipw.VBox([ipw.Image(value=membuf.getvalue())]))
476
+
477
+
478
+
479
+ def insertion_notes_widget(session: np_session.PipelineSession):
480
+
481
+ probes = 'ABCDEF'
482
+ def probe(_):
483
+ return f'Probe{_}'
484
+ fields = (
485
+ "FailedToInsert",
486
+ # "ProbeLocationChanged",
487
+ # "ProbeBendingOnSurface",
488
+ # "ProbeBendingElsewhere",
489
+ )
490
+ # "NumAgarInsertions",
491
+
492
+ def get_notes(_):
493
+ return session.platform_json.InsertionNotes.get(probe(_), {}).get('Notes', '')
494
+ def get_field(_, field):
495
+ return session.platform_json.InsertionNotes.get(probe(_), {}).get(field, None)
496
+
497
+ def disp_str(s): # split PascalCase fieldname into 'Title case' words
498
+ matches = re.finditer('.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', s)
499
+ return ' '.join([m.group(0) for m in matches]).lower().capitalize()
500
+ def save_str(s):
501
+ return ''.join([_.capitalize() for _ in s.split(' ')])
502
+
503
+ def row(*args):
504
+ return ipw.HBox([*args])
505
+ def probe_row(p):
506
+ return row(ipw.Text(value=get_notes(p), placeholder='Insertion notes', description=disp_str(probe(p).strip('Probe ')), layout=ipw.Layout(width='auto', min_width='400px')), *(ipw.Checkbox(value=get_field(p, field), description=disp_str(field)) for field in fields))
507
+ button = ipw.Button(description="Save", button_style='warning')
508
+ console = ipw.Output()
509
+
510
+ rows = [probe_row(p) for p in probes]
511
+ widget = ipw.VBox([*rows, button, console])
512
+
513
+ def save(b):
514
+ d = {}
515
+ for letter, row in zip(probes, rows):
516
+ p = d.get(probe(letter), {})
517
+ for widget in row.children:
518
+ if isinstance(widget, ipw.Text):
519
+ p['Notes'] = widget.value
520
+ elif isinstance(widget, ipw.Checkbox):
521
+ p[save_str(widget.description)] = widget.value
522
+ else:
523
+ continue
524
+ if p:
525
+ d[probe(letter)] = p
526
+
527
+ session.platform_json.InsertionNotes = d
528
+ with console:
529
+ print('Updated notes')
530
+ button.button_style = 'success'
531
+
532
+ button.on_click(save)
533
+ return IPython.display.display(widget)
534
+
535
+
536
+ def probe_depth_widget(session: np_session.PipelineSession):
537
+
538
+ probes = 'ABCDEF'
539
+
540
+ def coords():
541
+ return session.platform_json.manipulator_coordinates
542
+
543
+ if not coords():
544
+ logger.warning("No photodocs have been captured yet.")
545
+
546
+ def probe_coords(img):
547
+ return coords().get(img, dict.fromkeys(probes, dict(x=None, y=None, z=None)))
548
+ def field_str(s):
549
+ return '_'.join(s.split(' ')).lower() + '_surface_image' if s else ''
550
+
551
+ selection = ipw.ToggleButtons(
552
+ options=[' '.join(_.strip('_surface_image').split('_')).capitalize() for _ in coords().keys()],
553
+ description='Depth',
554
+ disabled=False,
555
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
556
+ tooltips=[field_str(_) for _ in coords().keys()],
557
+ )
558
+
559
+ def update(_):
560
+ for probe in probes:
561
+ depth = probe_coords(field_str(selection.value))[probe]["z"]
562
+ textbox[probe].value = f'{depth:6.1f}' if depth is not None else ''
563
+
564
+ textbox = {
565
+ probe: ipw.Text(
566
+ value='', description=probe, disabled=True,
567
+ layout=ipw.Layout(max_width='150px'),)
568
+ for probe in probes
569
+ }
570
+ selection.observe(update, 'value')
571
+ update(None)
572
+ widget = ipw.VBox([selection, ipw.HBox([*textbox.values()])])
573
+ return IPython.display.display(widget)
574
+
575
+
576
+ def photodoc_widget(img_name: str) -> IPython.display.DisplayHandle | None:
577
+ "Captures and displays snapshot from image camera, appending `img_name` to the filename."
578
+ image = ipw.Image(value=b'', format='png', width='80%', layout=ipw.Layout(visibility='hidden'))
579
+ widget = ipw.VBox([
580
+ image,
581
+ button := ipw.Button(description="Re-capture", button_style='warning'),
582
+ console := ipw.Output(),
583
+ ])
584
+
585
+ def capture() -> pathlib.Path:
586
+ image.value = b''
587
+ image.layout.visibility = 'hidden'
588
+ button.button_style = ''
589
+ button.description = 'Capturing new image...'
590
+ button.disabled = True
591
+ return npxc.photodoc(img_name)
592
+
593
+ def disp(img_path) -> None:
594
+ image.value = img_path.read_bytes()
595
+ image.layout.visibility = 'visible'
596
+ button.button_style = 'warning'
597
+ button.description = 'Re-capture'
598
+ button.disabled = False
599
+ with console:
600
+ print(img_path)
601
+
602
+ def capture_and_display(*args):
603
+ disp(capture())
604
+
605
+ button.on_click(capture_and_display)
606
+
607
+ if (matches := [_ for _ in (np_services.Cam3d.data_files or np_services.ImageMVR.data_files or []) if img_name in _.stem]):
608
+ disp(sorted(matches)[-1])
609
+ else:
610
+ capture_and_display()
611
+
612
+ return IPython.display.display(widget)
613
+
614
+ def probe_targeting_widget(session_folder) -> IPython.display.DisplayHandle | None:
615
+ from np_probe_targets.implant_drawing import CurrentWeek, DRWeeklyTargets
616
+ CurrentWeek.display()
617
+ IPython.display.display(DRWeeklyTargets())
618
+
619
+ def quiet_mode_widget() -> IPython.display.DisplayHandle | None:
620
+ """Displays a toggle button that switches logging level INFO <-> DEBUG and
621
+ hides/shows tracebacks.
622
+ """
623
+ debug_mode_toggle = ipw.ToggleButton(
624
+ value=True,
625
+ description='Quiet mode is on',
626
+ disabled=False,
627
+ button_style='info', # 'success', 'info', 'warning', 'danger' or ''
628
+ icon='check',
629
+ tooltip='Quiet mode: tracebacks hidden, logging level set to INFO.',
630
+ )
631
+
632
+ def set_debug_mode(value: bool) -> None:
633
+ if value:
634
+ npxc.show_tracebacks()
635
+ for handler in np_logging.getLogger().handlers:
636
+ if isinstance(handler, logging.StreamHandler):
637
+ handler.setLevel('DEBUG')
638
+ else:
639
+ npxc.hide_tracebacks()
640
+ for handler in np_logging.getLogger().handlers:
641
+ if isinstance(handler, logging.StreamHandler):
642
+ handler.setLevel('INFO')
643
+
644
+ def on_click(b) -> None:
645
+ if not debug_mode_toggle.value:
646
+ set_debug_mode(True)
647
+ debug_mode_toggle.description = 'Quiet mode is off'
648
+ debug_mode_toggle.button_style = ''
649
+ debug_mode_toggle.icon = 'times'
650
+ else:
651
+ set_debug_mode(False)
652
+ debug_mode_toggle.description = 'Quiet mode is on'
653
+ debug_mode_toggle.button_style = 'info'
654
+ debug_mode_toggle.icon = 'check'
655
+
656
+ debug_mode_toggle.observe(on_click)
657
+
658
+ return IPython.display.display(debug_mode_toggle)
659
+
660
+
661
+ def task_select_widget(
662
+ experiment,
663
+ ) -> None:
664
+ """Select a task name for controlling behavior of TaskControl.
665
+ """
666
+ experiment.task_name = experiment.preset_task_names[0]
667
+
668
+ task_dropdown = ipw.Select(
669
+ options=tuple(experiment.preset_task_names),
670
+ description="Presets",
671
+ layout=ipw.Layout(min_width="500px", max_height="400px"),
672
+ )
673
+ task_input_box = ipw.Text(
674
+ value=experiment.task_name if isinstance(experiment.task_name, str) else "",
675
+ continuous_update=False,
676
+ )
677
+ console = ipw.Output()
678
+ with console:
679
+ if last_task:= experiment.mouse.state.get('last_task'):
680
+ print(f"{experiment.mouse} last task: {last_task}")
681
+
682
+ def update(change):
683
+ if change["name"] != "value":
684
+ return
685
+ if (options := getattr(change["owner"], "options", None)) and change[
686
+ "new"
687
+ ] not in options:
688
+ return
689
+ if change["new"] == change["old"]:
690
+ return
691
+ if change["owner"] is task_dropdown:
692
+ experiment.task_name = str(task_dropdown.value)
693
+ task_input_box.value = experiment.task_name
694
+ return
695
+ elif change["owner"] is task_input_box:
696
+ experiment.task_name = str(task_input_box.value)
697
+ if str(task_dropdown.value) != experiment.task_name:
698
+ task_dropdown.value = None
699
+ with console:
700
+ print(f"Updated task: {experiment.task_name}")
701
+ task_dropdown.observe(update, names='value')
702
+ task_input_box.observe(update, names='value')
703
+
704
+ IPython.display.display(ipw.VBox([ipw.HBox([task_dropdown, task_input_box]), console]))
705
+