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.
- np_workflows/__init__.py +7 -0
- np_workflows/assets/images/logo_np_hab.png +0 -0
- np_workflows/assets/images/logo_np_vis.png +0 -0
- np_workflows/experiments/__init__.py +1 -0
- np_workflows/experiments/dynamic_routing/__init__.py +2 -0
- np_workflows/experiments/dynamic_routing/main.py +117 -0
- np_workflows/experiments/dynamic_routing/widgets.py +82 -0
- np_workflows/experiments/openscope_P3/P3_workflow_widget.py +83 -0
- np_workflows/experiments/openscope_P3/__init__.py +2 -0
- np_workflows/experiments/openscope_P3/main_P3_pilot.py +217 -0
- np_workflows/experiments/openscope_barcode/__init__.py +2 -0
- np_workflows/experiments/openscope_barcode/barcode_workflow_widget.py +83 -0
- np_workflows/experiments/openscope_barcode/camstim_scripts/barcode_mapping_script.py +138 -0
- np_workflows/experiments/openscope_barcode/camstim_scripts/barcode_opto_script.py +219 -0
- np_workflows/experiments/openscope_barcode/main_barcode_pilot.py +217 -0
- np_workflows/experiments/openscope_loop/__init__.py +2 -0
- np_workflows/experiments/openscope_loop/camstim_scripts/barcode_mapping_script.py +138 -0
- np_workflows/experiments/openscope_loop/camstim_scripts/barcode_opto_script.py +219 -0
- np_workflows/experiments/openscope_loop/loop_workflow_widget.py +83 -0
- np_workflows/experiments/openscope_loop/main_loop_pilot.py +217 -0
- np_workflows/experiments/openscope_psycode/__init__.py +2 -0
- np_workflows/experiments/openscope_psycode/main_psycode_pilot.py +217 -0
- np_workflows/experiments/openscope_psycode/psycode_workflow_widget.py +83 -0
- np_workflows/experiments/openscope_v2/__init__.py +2 -0
- np_workflows/experiments/openscope_v2/main_v2_pilot.py +217 -0
- np_workflows/experiments/openscope_v2/v2_workflow_widget.py +83 -0
- np_workflows/experiments/openscope_vippo/__init__.py +2 -0
- np_workflows/experiments/openscope_vippo/main_vippo_pilot.py +217 -0
- np_workflows/experiments/openscope_vippo/vippo_workflow_widget.py +83 -0
- np_workflows/experiments/task_trained_network/__init__.py +2 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/make_tt_stims.py +23 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/oct22_tt_stim_script.py +69 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_00.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_01.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_02.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_03.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_04.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_05.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_06.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_07.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_08.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_09.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_10.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_11.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_12.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_13.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_14.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_15.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_16.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_17.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_18.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/flash_250ms.stim +20 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/gabor_20_deg_250ms.stim +30 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/old_stim.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed_1st.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed_2nd.stim +5 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/ttn_main_script.py +130 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/ttn_mapping_script.py +138 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/ttn_opto_script.py +219 -0
- np_workflows/experiments/task_trained_network/main_ttn_pilot.py +263 -0
- np_workflows/experiments/task_trained_network/ttn_session_widget.py +83 -0
- np_workflows/experiments/task_trained_network/ttn_stim_config.py +213 -0
- np_workflows/experiments/templeton/__init__.py +2 -0
- np_workflows/experiments/templeton/main.py +105 -0
- np_workflows/experiments/templeton/widgets.py +82 -0
- np_workflows/shared/__init__.py +3 -0
- np_workflows/shared/base_experiments.py +826 -0
- np_workflows/shared/camstim_scripts/flash_250ms.stim +20 -0
- np_workflows/shared/camstim_scripts/gabor_20_deg_250ms.stim +30 -0
- np_workflows/shared/npxc.py +187 -0
- np_workflows/shared/widgets.py +705 -0
- np_workflows-1.6.89.dist-info/METADATA +85 -0
- np_workflows-1.6.89.dist-info/RECORD +76 -0
- np_workflows-1.6.89.dist-info/WHEEL +4 -0
- 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
|
+
|