np-workflows 1.6.87__py3-none-any.whl → 1.6.91__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 +3 -5
- np_workflows/experiments/dynamic_routing/main.py +20 -41
- np_workflows/experiments/dynamic_routing/widgets.py +29 -24
- np_workflows/experiments/openscope_P3/P3_workflow_widget.py +75 -0
- np_workflows/experiments/openscope_P3/__init__.py +2 -0
- np_workflows/experiments/openscope_P3/main_P3_pilot.py +215 -0
- np_workflows/experiments/openscope_barcode/__init__.py +1 -1
- np_workflows/experiments/openscope_barcode/barcode_workflow_widget.py +14 -20
- np_workflows/experiments/openscope_barcode/camstim_scripts/barcode_mapping_script.py +8 -14
- np_workflows/experiments/openscope_barcode/camstim_scripts/barcode_opto_script.py +121 -68
- np_workflows/experiments/openscope_barcode/main_barcode_pilot.py +69 -69
- np_workflows/experiments/openscope_loop/__init__.py +1 -1
- np_workflows/experiments/openscope_loop/camstim_scripts/barcode_mapping_script.py +8 -14
- np_workflows/experiments/openscope_loop/camstim_scripts/barcode_opto_script.py +121 -68
- np_workflows/experiments/openscope_loop/loop_workflow_widget.py +11 -19
- np_workflows/experiments/openscope_loop/main_loop_pilot.py +66 -68
- np_workflows/experiments/openscope_psycode/__init__.py +1 -1
- np_workflows/experiments/openscope_psycode/main_psycode_pilot.py +69 -69
- np_workflows/experiments/openscope_psycode/psycode_workflow_widget.py +14 -20
- np_workflows/experiments/openscope_v2/__init__.py +2 -0
- np_workflows/experiments/openscope_v2/main_v2_pilot.py +215 -0
- np_workflows/experiments/openscope_v2/v2_workflow_widget.py +75 -0
- np_workflows/experiments/openscope_vippo/__init__.py +1 -1
- np_workflows/experiments/openscope_vippo/main_vippo_pilot.py +66 -68
- np_workflows/experiments/openscope_vippo/vippo_workflow_widget.py +14 -20
- np_workflows/experiments/task_trained_network/__init__.py +1 -1
- np_workflows/experiments/task_trained_network/camstim_scripts/make_tt_stims.py +24 -14
- np_workflows/experiments/task_trained_network/camstim_scripts/oct22_tt_stim_script.py +54 -41
- np_workflows/experiments/task_trained_network/camstim_scripts/ttn_main_script.py +19 -22
- np_workflows/experiments/task_trained_network/camstim_scripts/ttn_mapping_script.py +8 -14
- np_workflows/experiments/task_trained_network/camstim_scripts/ttn_opto_script.py +121 -68
- np_workflows/experiments/task_trained_network/main_ttn_pilot.py +73 -68
- np_workflows/experiments/task_trained_network/ttn_session_widget.py +11 -19
- np_workflows/experiments/task_trained_network/ttn_stim_config.py +23 -19
- np_workflows/experiments/templeton/main.py +18 -41
- np_workflows/experiments/templeton/widgets.py +26 -23
- np_workflows/shared/__init__.py +1 -1
- np_workflows/shared/base_experiments.py +430 -308
- np_workflows/shared/npxc.py +85 -53
- np_workflows/shared/widgets.py +374 -224
- {np_workflows-1.6.87.dist-info → np_workflows-1.6.91.dist-info}/METADATA +7 -21
- np_workflows-1.6.91.dist-info/RECORD +48 -0
- {np_workflows-1.6.87.dist-info → np_workflows-1.6.91.dist-info}/WHEEL +2 -1
- np_workflows-1.6.91.dist-info/entry_points.txt +2 -0
- np_workflows-1.6.91.dist-info/top_level.txt +1 -0
- np_workflows/assets/images/logo_np_hab.png +0 -0
- np_workflows/assets/images/logo_np_vis.png +0 -0
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_00.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_01.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_02.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_03.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_04.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_05.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_06.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_07.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_08.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_09.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_10.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_11.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_12.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_13.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_14.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_15.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_16.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_17.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_18.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/flash_250ms.stim +0 -20
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/gabor_20_deg_250ms.stim +0 -30
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/old_stim.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed_1st.stim +0 -5
- np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed_2nd.stim +0 -5
- np_workflows/shared/camstim_scripts/flash_250ms.stim +0 -20
- np_workflows/shared/camstim_scripts/gabor_20_deg_250ms.stim +0 -30
- np_workflows-1.6.87.dist-info/RECORD +0 -70
- np_workflows-1.6.87.dist-info/entry_points.txt +0 -4
|
@@ -8,7 +8,8 @@ import pathlib
|
|
|
8
8
|
import re
|
|
9
9
|
import shutil
|
|
10
10
|
import time
|
|
11
|
-
from
|
|
11
|
+
from collections.abc import Iterable, Mapping
|
|
12
|
+
from typing import Any, ClassVar, Literal, Optional, Protocol, Type
|
|
12
13
|
|
|
13
14
|
import fabric
|
|
14
15
|
import invoke
|
|
@@ -16,6 +17,7 @@ import ipylab
|
|
|
16
17
|
import np_config
|
|
17
18
|
import np_logging
|
|
18
19
|
import np_services
|
|
20
|
+
import np_services.resources.zro
|
|
19
21
|
import np_session
|
|
20
22
|
import upath
|
|
21
23
|
from np_services import (
|
|
@@ -30,11 +32,12 @@ from np_services import (
|
|
|
30
32
|
Validatable,
|
|
31
33
|
Verifiable,
|
|
32
34
|
)
|
|
33
|
-
|
|
35
|
+
|
|
34
36
|
import np_workflows.shared.npxc as npxc
|
|
35
37
|
|
|
36
38
|
logger = np_logging.getLogger(__name__)
|
|
37
39
|
|
|
40
|
+
|
|
38
41
|
class WithSessionInfo(Protocol):
|
|
39
42
|
@property
|
|
40
43
|
def session(self) -> np_session.Session: ...
|
|
@@ -44,17 +47,19 @@ class WithSessionInfo(Protocol):
|
|
|
44
47
|
def user(self) -> np_session.User: ...
|
|
45
48
|
@property
|
|
46
49
|
def rig(self) -> np_config.Rig: ...
|
|
47
|
-
|
|
50
|
+
|
|
48
51
|
|
|
49
52
|
class WithSession(abc.ABC):
|
|
50
|
-
|
|
51
|
-
default_session_subclass: ClassVar[Type[np_session.Session]] =
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
|
|
54
|
+
default_session_subclass: ClassVar[Type[np_session.Session]] = (
|
|
55
|
+
np_session.PipelineSession
|
|
56
|
+
)
|
|
57
|
+
default_session_type: Literal["ephys", "hab"] = "ephys"
|
|
58
|
+
|
|
54
59
|
services: tuple[Service, ...] = ()
|
|
55
60
|
"All services. Devices, databases, etc."
|
|
56
|
-
|
|
57
|
-
workflow: enum.Enum = enum.Enum(
|
|
61
|
+
|
|
62
|
+
workflow: enum.Enum = enum.Enum("BaseWithSessionWorkflow", ("BASECLASS")).BASECLASS # type: ignore
|
|
58
63
|
"""Enum for workflow type, e.g. PRETEST, HAB_AUD, HAB_VIS, EPHYS_ etc."""
|
|
59
64
|
|
|
60
65
|
def log(self, message: str, weblog_name: Optional[str] = None) -> None:
|
|
@@ -62,71 +67,101 @@ class WithSession(abc.ABC):
|
|
|
62
67
|
if not weblog_name:
|
|
63
68
|
weblog_name = self.workflow.name
|
|
64
69
|
with contextlib.suppress(AttributeError):
|
|
65
|
-
np_logging.web(f
|
|
66
|
-
|
|
70
|
+
np_logging.web(f"{weblog_name.lower()}_{self.mouse}").info(message)
|
|
71
|
+
|
|
67
72
|
@property
|
|
68
73
|
@abc.abstractmethod
|
|
69
74
|
def recorders(self) -> tuple[Startable | Stoppable, ...]:
|
|
70
75
|
"""Services that record data. These are started and stopped as a group."""
|
|
71
76
|
return NotImplemented
|
|
72
|
-
|
|
73
|
-
def __init__(
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
mouse: Optional[str | int | np_session.LIMS2MouseInfo] = None,
|
|
81
|
+
operator: Optional[str | np_session.LIMS2UserInfo] = None,
|
|
76
82
|
session: Optional[str | pathlib.Path | int | np_session.PipelineSession] = None,
|
|
77
|
-
session_type: Optional[Literal[
|
|
83
|
+
session_type: Optional[Literal["ephys", "hab"]] = None,
|
|
78
84
|
**kwargs,
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
):
|
|
86
|
+
|
|
81
87
|
if session and not isinstance(session, np_session.Session):
|
|
82
88
|
session = np_session.Session(session)
|
|
83
|
-
logger.debug(
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
logger.debug(
|
|
90
|
+
"%s | Initialized with existing session %s",
|
|
91
|
+
self.__class__.__name__,
|
|
92
|
+
session,
|
|
93
|
+
)
|
|
94
|
+
if session_type and ((a := session_type == "hab") != (b := session.is_hab)):
|
|
95
|
+
logger.warning(
|
|
96
|
+
"session_type arg specified (%r) does not match that of supplied %r: %r",
|
|
97
|
+
a,
|
|
98
|
+
b,
|
|
99
|
+
session,
|
|
100
|
+
)
|
|
86
101
|
elif operator and mouse:
|
|
87
|
-
logger.debug(
|
|
88
|
-
|
|
102
|
+
logger.debug(
|
|
103
|
+
"%s | Creating new session for mouse %r, operator %r",
|
|
104
|
+
self.__class__.__name__,
|
|
105
|
+
mouse,
|
|
106
|
+
operator,
|
|
107
|
+
)
|
|
108
|
+
session = self.generate_session(
|
|
109
|
+
mouse, operator, session_type or self.default_session_type
|
|
110
|
+
)
|
|
89
111
|
elif not session:
|
|
90
|
-
raise ValueError(
|
|
112
|
+
raise ValueError(
|
|
113
|
+
"Must specify either a mouse + operator, or an existing session"
|
|
114
|
+
)
|
|
91
115
|
|
|
92
116
|
self.session = session
|
|
93
|
-
|
|
117
|
+
|
|
94
118
|
self.configure_services()
|
|
95
119
|
self.session.npexp_path.mkdir(parents=True, exist_ok=True)
|
|
96
120
|
|
|
97
121
|
def __repr__(self) -> str:
|
|
98
|
-
return f
|
|
99
|
-
|
|
122
|
+
return f"{self.__class__.__name__}({self.session})"
|
|
123
|
+
|
|
100
124
|
@classmethod
|
|
101
125
|
def generate_session(cls, *args, **kwargs):
|
|
102
126
|
return cls.default_session_subclass.new(*args, **kwargs)
|
|
103
|
-
|
|
127
|
+
|
|
104
128
|
@property
|
|
105
129
|
def session(self) -> np_session.PipelineSession:
|
|
106
130
|
return self._session
|
|
107
|
-
|
|
131
|
+
|
|
108
132
|
@session.setter
|
|
109
|
-
def session(
|
|
110
|
-
self
|
|
111
|
-
|
|
112
|
-
|
|
133
|
+
def session(
|
|
134
|
+
self,
|
|
135
|
+
value: (
|
|
136
|
+
str | np_session.Session | pathlib.Path | int | np_session.LIMS2SessionInfo
|
|
137
|
+
),
|
|
138
|
+
):
|
|
139
|
+
self._session = (
|
|
140
|
+
np_session.Session(value)
|
|
141
|
+
if not isinstance(value, np_session.Session)
|
|
142
|
+
else value
|
|
143
|
+
)
|
|
144
|
+
logger.debug("Set experiment.session to %r", self._session)
|
|
145
|
+
|
|
113
146
|
@property
|
|
114
|
-
def session_type(self) -> Literal[
|
|
147
|
+
def session_type(self) -> Literal["ephys", "hab"]:
|
|
115
148
|
with contextlib.suppress(AttributeError):
|
|
116
149
|
return self._session_type
|
|
117
150
|
if self.session:
|
|
118
151
|
if self.session.is_hab:
|
|
119
|
-
return
|
|
120
|
-
return
|
|
121
|
-
raise AttributeError(
|
|
122
|
-
|
|
152
|
+
return "hab"
|
|
153
|
+
return "ephys"
|
|
154
|
+
raise AttributeError("Session has not been set")
|
|
155
|
+
|
|
123
156
|
@session_type.setter
|
|
124
|
-
def session_type(self, value: Literal[
|
|
125
|
-
if value not in (
|
|
126
|
-
raise ValueError(
|
|
157
|
+
def session_type(self, value: Literal["ephys", "hab"]):
|
|
158
|
+
if value not in ("ephys", "hab"):
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f'Session type must be either "ephys" or "hab": got {value!r}'
|
|
161
|
+
)
|
|
127
162
|
self._session_type = value
|
|
128
|
-
logger.debug(
|
|
129
|
-
|
|
163
|
+
logger.debug("Set session_type to %r", value)
|
|
164
|
+
|
|
130
165
|
@property
|
|
131
166
|
def rig(self) -> np_config.Rig | None:
|
|
132
167
|
"Computer hostnames and configuration for the rig we're currently on."
|
|
@@ -135,25 +170,25 @@ class WithSession(abc.ABC):
|
|
|
135
170
|
with contextlib.suppress(ValueError):
|
|
136
171
|
self._rig = np_config.Rig()
|
|
137
172
|
return self.rig
|
|
138
|
-
|
|
173
|
+
|
|
139
174
|
@property
|
|
140
175
|
def mouse(self) -> np_session.Mouse:
|
|
141
176
|
if isinstance(self.session.mouse, str | int):
|
|
142
177
|
return np_session.Mouse(self.session.mouse)
|
|
143
|
-
return self.session.mouse
|
|
144
|
-
|
|
178
|
+
return self.session.mouse
|
|
179
|
+
|
|
145
180
|
@property
|
|
146
181
|
def user(self) -> np_session.User | None:
|
|
147
182
|
return self.session.user
|
|
148
|
-
|
|
183
|
+
|
|
149
184
|
@property
|
|
150
185
|
def imager(self) -> Type[np_services.Cam3d] | Type[np_services.ImageMVR]:
|
|
151
186
|
if not self.rig:
|
|
152
|
-
raise ValueError(
|
|
187
|
+
raise ValueError("Rig has not been set")
|
|
153
188
|
if self.rig.idx == 0:
|
|
154
189
|
return np_services.Cam3d
|
|
155
190
|
return np_services.ImageMVR
|
|
156
|
-
|
|
191
|
+
|
|
157
192
|
@property
|
|
158
193
|
def config(self) -> dict[Any, Any]:
|
|
159
194
|
"Top-level keys include names of Services. Each Service then has a config dict."
|
|
@@ -162,7 +197,7 @@ class WithSession(abc.ABC):
|
|
|
162
197
|
if self.rig:
|
|
163
198
|
self._config = self.rig.config
|
|
164
199
|
return self.config
|
|
165
|
-
|
|
200
|
+
|
|
166
201
|
def configure_services(self) -> None:
|
|
167
202
|
"""For each service, apply every key in self.config['service'] as an attribute."""
|
|
168
203
|
|
|
@@ -192,9 +227,9 @@ class WithSession(abc.ABC):
|
|
|
192
227
|
def pretest_services(self) -> None:
|
|
193
228
|
for service in (_ for _ in self.services if isinstance(_, Pretestable)):
|
|
194
229
|
service.pretest()
|
|
195
|
-
|
|
230
|
+
|
|
196
231
|
def start_recording(self, *recorders: Startable) -> None:
|
|
197
|
-
if not recorders and hasattr(self,
|
|
232
|
+
if not recorders and hasattr(self, "recorders"):
|
|
198
233
|
recorders = self.recorders
|
|
199
234
|
stoppables = tuple(_ for _ in recorders if isinstance(_, Stoppable))
|
|
200
235
|
with np_services.stop_on_error(*stoppables):
|
|
@@ -203,29 +238,32 @@ class WithSession(abc.ABC):
|
|
|
203
238
|
time.sleep(2)
|
|
204
239
|
if isinstance(recorder, Verifiable):
|
|
205
240
|
recorder.verify()
|
|
206
|
-
|
|
207
|
-
def stop_recording_after_stim_finished(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
241
|
+
|
|
242
|
+
def stop_recording_after_stim_finished(
|
|
243
|
+
self,
|
|
244
|
+
recorders: Optional[Iterable[Stoppable]] = None,
|
|
245
|
+
stims: Optional[Iterable[Stoppable]] = None,
|
|
246
|
+
) -> None:
|
|
211
247
|
"""Stop recording after all stims have finished.
|
|
212
|
-
|
|
248
|
+
|
|
213
249
|
- object's `.recorders` attr used by default, stopped in reverse order.
|
|
214
250
|
- stims will be awaited
|
|
215
251
|
"""
|
|
216
|
-
if not recorders and hasattr(self,
|
|
252
|
+
if not recorders and hasattr(self, "recorders"):
|
|
217
253
|
recorders = reversed(self.recorders)
|
|
218
|
-
if not stims and hasattr(self,
|
|
254
|
+
if not stims and hasattr(self, "stims"):
|
|
219
255
|
stims = self.stims
|
|
220
256
|
while not all(_.is_ready_to_start() for _ in stims):
|
|
221
257
|
time.sleep(5)
|
|
222
258
|
for stoppable in (_ for _ in recorders if isinstance(_, Stoppable)):
|
|
223
259
|
stoppable.stop()
|
|
224
|
-
if
|
|
260
|
+
if "videomvr" in stoppable.__name__.lower():
|
|
225
261
|
sleep_s = 4
|
|
226
262
|
time.sleep(sleep_s)
|
|
227
|
-
logger.warning(
|
|
228
|
-
|
|
263
|
+
logger.warning(
|
|
264
|
+
f"Waiting additional {sleep_s} s for MVR to finish writing..."
|
|
265
|
+
)
|
|
266
|
+
|
|
229
267
|
def start_services(self, *services: Service) -> None:
|
|
230
268
|
if not services:
|
|
231
269
|
services = self.services
|
|
@@ -247,7 +285,7 @@ class WithSession(abc.ABC):
|
|
|
247
285
|
services = self.services
|
|
248
286
|
for service in (_ for _ in services if isinstance(_, Validatable)):
|
|
249
287
|
service.validate()
|
|
250
|
-
|
|
288
|
+
|
|
251
289
|
def finalize_services(self, *services: Service) -> None:
|
|
252
290
|
if not services:
|
|
253
291
|
services = self.services
|
|
@@ -257,67 +295,68 @@ class WithSession(abc.ABC):
|
|
|
257
295
|
def shutdown_services(self) -> None:
|
|
258
296
|
for service in (_ for _ in self.services if isinstance(_, Shutdownable)):
|
|
259
297
|
service.shutdown()
|
|
260
|
-
|
|
298
|
+
|
|
261
299
|
def copy_files(self) -> None:
|
|
262
300
|
"""Copy files from raw data storage to session folder for all services."""
|
|
263
301
|
self.copy_data_files()
|
|
264
302
|
self.copy_workflow_files()
|
|
265
303
|
self.copy_mpe_configs()
|
|
266
|
-
if self.session_type !=
|
|
304
|
+
if self.session_type != "hab":
|
|
267
305
|
self.copy_ephys()
|
|
268
|
-
|
|
306
|
+
|
|
269
307
|
@abc.abstractmethod
|
|
270
308
|
def copy_data_files(self) -> None:
|
|
271
309
|
"""Copy files from raw data storage to session folder for all services."""
|
|
272
310
|
return NotImplemented
|
|
273
|
-
|
|
311
|
+
|
|
274
312
|
@abc.abstractmethod
|
|
275
313
|
def copy_ephys(self) -> None:
|
|
276
314
|
"""Copy ephys data from Acq to session folder."""
|
|
277
315
|
return NotImplemented
|
|
278
|
-
|
|
316
|
+
|
|
279
317
|
def copy_workflow_files(self) -> None:
|
|
280
318
|
"""Copy working directory (with ipynb, logs folder) and lock/pyproject files
|
|
281
319
|
from np_notebooks root."""
|
|
282
320
|
|
|
283
321
|
self.save_current_notebook()
|
|
284
|
-
|
|
285
|
-
cwd = pathlib.Path(
|
|
286
|
-
dest = self.session.npexp_path /
|
|
322
|
+
|
|
323
|
+
cwd = pathlib.Path(".").resolve()
|
|
324
|
+
dest = self.session.npexp_path / "exp"
|
|
287
325
|
dest.mkdir(exist_ok=True, parents=True)
|
|
288
326
|
|
|
289
327
|
shutil.copytree(cwd, dest, dirs_exist_ok=True)
|
|
290
328
|
|
|
291
|
-
lock = cwd.parent /
|
|
292
|
-
pyproject = cwd.parent /
|
|
293
|
-
|
|
329
|
+
lock = cwd.parent / "uv.lock"
|
|
330
|
+
pyproject = cwd.parent / "pyproject.toml"
|
|
331
|
+
|
|
294
332
|
for _ in (lock, pyproject):
|
|
295
333
|
shutil.copy2(_, dest)
|
|
296
334
|
|
|
297
335
|
def copy_mpe_configs(self) -> None:
|
|
298
336
|
"""Copy MPE config files to session folder."""
|
|
299
337
|
for path in (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
338
|
+
self.rig.mvr_config,
|
|
339
|
+
self.rig.sync_config,
|
|
340
|
+
self.rig.camstim_config,
|
|
341
|
+
):
|
|
303
342
|
shutil.copy2(path, self.session.npexp_path)
|
|
304
343
|
|
|
305
344
|
def save_current_notebook(self) -> None:
|
|
306
345
|
app = ipylab.JupyterFrontEnd()
|
|
307
|
-
app.commands.execute(
|
|
346
|
+
app.commands.execute("docmanager:save")
|
|
308
347
|
# TODO use the following to export to html (shows input to widgets and
|
|
309
348
|
# output of cells)
|
|
310
|
-
#! currently can't be run automatically as save as path dialog opens
|
|
349
|
+
#! currently can't be run automatically as save as path dialog opens
|
|
311
350
|
# app.commands.execute('notebook:export-to-format', {
|
|
312
351
|
# 'format': 'html',
|
|
313
352
|
# # 'download': 'false',
|
|
314
353
|
# # 'path': 'c:/users/svc_neuropix/documents/github/np_notebooks/task_trained_network/ttn_pilot.html',
|
|
315
|
-
# })
|
|
316
|
-
|
|
354
|
+
# })
|
|
355
|
+
|
|
317
356
|
@functools.cached_property
|
|
318
357
|
def system_camstim_params(self) -> dict[str, Any]:
|
|
319
358
|
"""Try to load defaults from camstim config file on the Stim computer.
|
|
320
|
-
|
|
359
|
+
|
|
321
360
|
May encounter permission error if not running as svc_neuropix.
|
|
322
361
|
"""
|
|
323
362
|
with contextlib.suppress(OSError):
|
|
@@ -339,62 +378,76 @@ class WithSession(abc.ABC):
|
|
|
339
378
|
else:
|
|
340
379
|
camstim_default_config[section][k] = value
|
|
341
380
|
return camstim_default_config
|
|
342
|
-
logger.warning(
|
|
381
|
+
logger.warning(
|
|
382
|
+
"Could not load camstim defaults from config file on Stim computer."
|
|
383
|
+
)
|
|
343
384
|
return {}
|
|
344
|
-
|
|
385
|
+
|
|
386
|
+
|
|
345
387
|
class PipelineExperiment(WithSession):
|
|
346
388
|
@property
|
|
347
389
|
def platform_json(self) -> np_session.PlatformJson:
|
|
348
|
-
self.session.platform_json.update(
|
|
390
|
+
self.session.platform_json.update("rig_id", str(self.rig))
|
|
349
391
|
return self.session.platform_json
|
|
350
|
-
|
|
392
|
+
|
|
351
393
|
def start_recording(self, *recorders: Startable) -> None:
|
|
352
394
|
super().start_recording(*recorders)
|
|
353
|
-
self.platform_json.ExperimentStartTime = npxc.now()
|
|
395
|
+
self.platform_json.ExperimentStartTime = npxc.now()
|
|
354
396
|
self.platform_json.write()
|
|
355
397
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
398
|
+
def stop_recording_after_stim_finished(
|
|
399
|
+
self,
|
|
400
|
+
recorders: Optional[Iterable[Stoppable]] = None,
|
|
401
|
+
stims: Optional[Iterable[Stoppable]] = None,
|
|
402
|
+
) -> None:
|
|
361
403
|
"""Stop recording after all stims have finished."""
|
|
362
404
|
super().stop_recording_after_stim_finished(recorders, stims)
|
|
363
|
-
self.platform_json.ExperimentCompleteTime = npxc.now()
|
|
405
|
+
self.platform_json.ExperimentCompleteTime = npxc.now()
|
|
364
406
|
self.platform_json.write()
|
|
365
|
-
|
|
407
|
+
|
|
366
408
|
def rename_split_ephys_folders(self) -> None:
|
|
367
409
|
"Add `_probeABC` or `_probeDEF` to ephys folders recorded on two drives."
|
|
368
410
|
folders = np_services.OpenEphys.data_files
|
|
369
411
|
if not folders:
|
|
370
|
-
logger.info(
|
|
412
|
+
logger.info("Renaming: no ephys folders have been recorded")
|
|
371
413
|
renamed_folders = []
|
|
372
414
|
for name in set(_.name for _ in folders):
|
|
373
|
-
if
|
|
374
|
-
logger.debug(
|
|
415
|
+
if "_probeABC" in name or "_probeDEF" in name:
|
|
416
|
+
logger.debug(
|
|
417
|
+
f"Renaming: {name} already has probe letter suffix - aborted"
|
|
418
|
+
)
|
|
375
419
|
continue
|
|
376
|
-
if
|
|
377
|
-
|
|
420
|
+
if (
|
|
421
|
+
length := len(split_folders := [_ for _ in folders if _.name == name])
|
|
422
|
+
!= 2
|
|
423
|
+
):
|
|
424
|
+
logger.info(
|
|
425
|
+
f"Renaming: {length} folders found for {name}, expected 2 - aborted"
|
|
426
|
+
)
|
|
378
427
|
continue
|
|
379
|
-
logger.debug(
|
|
380
|
-
for folder, probe_letters in zip(
|
|
381
|
-
|
|
428
|
+
logger.debug("Renaming split ephys folders %r", split_folders)
|
|
429
|
+
for folder, probe_letters in zip(
|
|
430
|
+
sorted(split_folders, key=lambda x: x.as_posix()), ("ABC", "DEF")
|
|
431
|
+
):
|
|
432
|
+
renamed = folder.replace(
|
|
433
|
+
folder.with_name(f"{name}_probe{probe_letters}")
|
|
434
|
+
)
|
|
382
435
|
renamed_folders.append(renamed)
|
|
383
|
-
logger.info(
|
|
436
|
+
logger.info("Renamed split ephys folders %r", split_folders)
|
|
384
437
|
np_services.OpenEphys.data_files = renamed_folders
|
|
385
438
|
|
|
386
439
|
@staticmethod
|
|
387
440
|
def contains_uuid(text: str) -> bool:
|
|
388
|
-
hexchars =
|
|
441
|
+
hexchars = "[0-9a-fA-F]"
|
|
389
442
|
pattern = rf"{hexchars}{{8}}-{hexchars}{{4}}-{hexchars}{{4}}-{hexchars}{{4}}-{hexchars}{{12}}"
|
|
390
443
|
return re.search(pattern, text) is not None
|
|
391
|
-
|
|
444
|
+
|
|
392
445
|
def copy_data_files(self) -> None:
|
|
393
446
|
"""Copy data files from raw data storage to session folder for all services."""
|
|
394
447
|
for service in self.services:
|
|
395
448
|
match service.__name__:
|
|
396
449
|
case "np_services.open_ephys":
|
|
397
|
-
continue
|
|
450
|
+
continue # copy ephys after other files
|
|
398
451
|
case _:
|
|
399
452
|
files = None
|
|
400
453
|
with contextlib.suppress(AttributeError):
|
|
@@ -405,159 +458,182 @@ class PipelineExperiment(WithSession):
|
|
|
405
458
|
logger.info("%s | Copying files %r", service.__name__, files)
|
|
406
459
|
for file in files:
|
|
407
460
|
renamed = None
|
|
408
|
-
if file.suffix ==
|
|
409
|
-
renamed = f
|
|
410
|
-
elif file.suffix ==
|
|
411
|
-
for _ in (
|
|
461
|
+
if file.suffix == ".h5":
|
|
462
|
+
renamed = f"{self.session.folder}.sync"
|
|
463
|
+
elif file.suffix == ".pkl":
|
|
464
|
+
for _ in ("opto", "main", "mapping", "behavior"):
|
|
412
465
|
if _ in file.name:
|
|
413
466
|
renamed = f'{self.session.folder}.{"stim" if _ == "main" else _}.pkl'
|
|
414
467
|
break
|
|
415
468
|
else:
|
|
416
469
|
if self.contains_uuid(file.name):
|
|
417
|
-
renamed = f
|
|
418
|
-
elif file.suffix in (
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
470
|
+
renamed = f"{self.session.folder}.behavior.pkl"
|
|
471
|
+
elif file.suffix in (".json", ".mp4") and (
|
|
472
|
+
cam_label := re.match("Behavior|Eye|Face", file.name)
|
|
473
|
+
):
|
|
474
|
+
renamed = f"{self.session.folder}.{cam_label.group().lower()}{file.suffix}"
|
|
475
|
+
elif file.suffix in (".json", ".mp4") and (
|
|
476
|
+
cam_label := re.match("BEH|EYE|FACE", file.name)
|
|
477
|
+
):
|
|
478
|
+
file_label = {
|
|
479
|
+
"BEH": "behavior",
|
|
480
|
+
"EYE": "eye",
|
|
481
|
+
"FACE": "face",
|
|
482
|
+
}
|
|
483
|
+
renamed = f"{self.session.folder}.{file_label[cam_label.group()]}{file.suffix}"
|
|
484
|
+
elif service in (np_services.NewScaleCoordinateRecorder,):
|
|
485
|
+
renamed = f"{self.session.folder}.motor-locs.csv"
|
|
425
486
|
elif service in (np_services.Cam3d, np_services.MVR):
|
|
426
|
-
for lims_label, img_label
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
487
|
+
for lims_label, img_label in {
|
|
488
|
+
"pre_experiment_surface_image_left": "_surface-image1-left",
|
|
489
|
+
"pre_experiment_surface_image_right": "_surface-image1-right",
|
|
490
|
+
"brain_surface_image_left": "_surface-image2-left",
|
|
491
|
+
"brain_surface_image_right": "_surface-image2-right",
|
|
492
|
+
"pre_insertion_surface_image_left": "_surface-image3-left",
|
|
493
|
+
"pre_insertion_surface_image_right": "_surface-image3-right",
|
|
494
|
+
"post_insertion_surface_image_left": "_surface-image4-left",
|
|
495
|
+
"post_insertion_surface_image_right": "_surface-image4-right",
|
|
496
|
+
"post_stimulus_surface_image_left": "_surface-image5-left",
|
|
497
|
+
"post_stimulus_surface_image_right": "_surface-image5-right",
|
|
498
|
+
"post_experiment_surface_image_left": "_surface-image6-left",
|
|
499
|
+
"post_experiment_surface_image_right": "_surface-image6-right",
|
|
500
|
+
}.items():
|
|
440
501
|
if lims_label in file.name:
|
|
441
|
-
renamed =
|
|
442
|
-
|
|
443
|
-
|
|
502
|
+
renamed = (
|
|
503
|
+
f"{self.session.folder}{img_label}{file.suffix}"
|
|
504
|
+
)
|
|
505
|
+
shutil.copy2(
|
|
506
|
+
file, self.session.npexp_path / (renamed or file.name)
|
|
507
|
+
)
|
|
508
|
+
|
|
444
509
|
def copy_ephys(self) -> None:
|
|
445
|
-
# copy ephys
|
|
510
|
+
# copy ephys
|
|
446
511
|
self.rename_split_ephys_folders()
|
|
447
|
-
password = np_config.fetch(
|
|
448
|
-
ssh = fabric.Connection(
|
|
512
|
+
password = np_config.fetch("/logins")["svc_neuropix"]["password"]
|
|
513
|
+
ssh = fabric.Connection(
|
|
514
|
+
host=np_services.OpenEphys.host,
|
|
515
|
+
user="svc_neuropix",
|
|
516
|
+
connect_kwargs=dict(password=password),
|
|
517
|
+
)
|
|
449
518
|
for ephys_folder in np_services.OpenEphys.data_files:
|
|
450
519
|
with contextlib.suppress(Exception):
|
|
451
520
|
with ssh:
|
|
452
521
|
ssh.run(
|
|
453
|
-
|
|
454
|
-
|
|
522
|
+
f'robocopy "{ephys_folder}" "{self.session.npexp_path / ephys_folder.name}" /j /s /xo'
|
|
523
|
+
# /j unbuffered, /s incl non-empty subdirs, /xo exclude src files older than dest
|
|
455
524
|
)
|
|
456
525
|
|
|
526
|
+
|
|
457
527
|
class PipelineEphys(PipelineExperiment):
|
|
458
|
-
default_session_type =
|
|
528
|
+
default_session_type = "ephys"
|
|
529
|
+
|
|
459
530
|
|
|
460
531
|
class PipelineHab(PipelineExperiment):
|
|
461
|
-
default_session_type =
|
|
532
|
+
default_session_type = "hab"
|
|
462
533
|
|
|
463
534
|
|
|
464
535
|
class DynamicRoutingExperiment(WithSession):
|
|
465
|
-
|
|
536
|
+
|
|
466
537
|
default_session_subclass: ClassVar[Type[np_session.Session]]
|
|
467
|
-
|
|
468
|
-
|
|
538
|
+
|
|
469
539
|
use_github: bool = True
|
|
470
|
-
|
|
540
|
+
|
|
471
541
|
class Workflow(enum.Enum):
|
|
472
|
-
"""Enum for the different sessions available.
|
|
473
|
-
|
|
542
|
+
"""Enum for the different sessions available.
|
|
543
|
+
|
|
474
544
|
Used in the workflow to determine branches (e.g. HAB workflow should
|
|
475
545
|
skip set up and use of OpenEphys).
|
|
476
|
-
|
|
477
|
-
Can also be used to switch different task names, scripts, etc.
|
|
478
|
-
|
|
546
|
+
|
|
547
|
+
Can also be used to switch different task names, scripts, etc.
|
|
548
|
+
|
|
479
549
|
- names are used to set the experiment subclass
|
|
480
|
-
- values must be unique!
|
|
550
|
+
- values must be unique!
|
|
481
551
|
"""
|
|
552
|
+
|
|
482
553
|
PRETEST = "test EPHYS"
|
|
483
554
|
HAB = "EPHYS minus probes"
|
|
484
555
|
EPHYS = "opto in task optional"
|
|
485
556
|
OPTO = "opto in task, no ephys"
|
|
486
557
|
TRAINING = "task only, with sync + video"
|
|
558
|
+
|
|
487
559
|
workflow: Workflow
|
|
488
|
-
|
|
560
|
+
|
|
489
561
|
@property
|
|
490
562
|
def preset_task_names(self) -> tuple[str, ...]:
|
|
491
|
-
return tuple(np_config.fetch(
|
|
492
|
-
|
|
563
|
+
return tuple(np_config.fetch("/projects/dynamicrouting")["preset_task_names"])
|
|
564
|
+
|
|
493
565
|
@property
|
|
494
566
|
def commit_hash(self) -> str:
|
|
495
|
-
if hasattr(self,
|
|
567
|
+
if hasattr(self, "_commit_hash"):
|
|
496
568
|
return self._commit_hash
|
|
497
|
-
self._commit_hash = self.config[
|
|
569
|
+
self._commit_hash = self.config["dynamicrouting_task_script"]["commit_hash"]
|
|
498
570
|
return self.commit_hash
|
|
499
|
-
|
|
571
|
+
|
|
500
572
|
@commit_hash.setter
|
|
501
573
|
def commit_hash(self, value: str):
|
|
502
574
|
self._commit_hash = value
|
|
503
|
-
|
|
575
|
+
|
|
504
576
|
@property
|
|
505
577
|
def github_url(self) -> str:
|
|
506
|
-
if hasattr(self,
|
|
578
|
+
if hasattr(self, "_github_url"):
|
|
507
579
|
return self._github_url
|
|
508
|
-
self._github_url = self.config[
|
|
580
|
+
self._github_url = self.config["dynamicrouting_task_script"]["url"]
|
|
509
581
|
return self.github_url
|
|
510
|
-
|
|
582
|
+
|
|
511
583
|
@github_url.setter
|
|
512
584
|
def github_url(self, value: str):
|
|
513
585
|
self._github_url = value
|
|
514
|
-
|
|
586
|
+
|
|
515
587
|
@property
|
|
516
588
|
def base_url(self) -> upath.UPath:
|
|
517
589
|
return upath.UPath(self.github_url) / self.commit_hash
|
|
518
|
-
|
|
590
|
+
|
|
519
591
|
@property
|
|
520
592
|
def base_path(self) -> pathlib.Path:
|
|
521
|
-
return pathlib.Path(
|
|
522
|
-
|
|
593
|
+
return pathlib.Path(
|
|
594
|
+
"//allen/programs/mindscope/workgroups/dynamicrouting/DynamicRoutingTask/"
|
|
595
|
+
)
|
|
596
|
+
|
|
523
597
|
@property
|
|
524
598
|
def is_pretest(self) -> bool:
|
|
525
|
-
return
|
|
526
|
-
|
|
599
|
+
return "PRETEST" in self.workflow.name
|
|
600
|
+
|
|
527
601
|
@property
|
|
528
602
|
def is_hab(self) -> bool:
|
|
529
|
-
return
|
|
530
|
-
|
|
603
|
+
return "HAB" in self.workflow.name
|
|
604
|
+
|
|
531
605
|
@property
|
|
532
606
|
def is_opto(self) -> bool:
|
|
533
607
|
"""Opto will run during behavior task trials - independent of `is_ephys`."""
|
|
534
|
-
return
|
|
535
|
-
|
|
608
|
+
return "opto" in self.task_name
|
|
609
|
+
|
|
536
610
|
@property
|
|
537
611
|
def is_optotagging(self) -> bool:
|
|
538
|
-
return self.is_ephys and
|
|
539
|
-
|
|
612
|
+
return self.is_ephys and "AI32" in str(self.mouse.lims["full_genotype"]).upper()
|
|
613
|
+
|
|
540
614
|
@property
|
|
541
615
|
def is_ephys(self) -> bool:
|
|
542
|
-
return
|
|
543
|
-
|
|
616
|
+
return "EPHYS" in self.workflow.name
|
|
617
|
+
|
|
544
618
|
@property
|
|
545
619
|
def task_name(self) -> str:
|
|
546
620
|
"""For sending to runTask.py and controlling implementation details of the task."""
|
|
547
|
-
if hasattr(self,
|
|
548
|
-
return self._task_name
|
|
621
|
+
if hasattr(self, "_task_name"):
|
|
622
|
+
return self._task_name
|
|
549
623
|
return ""
|
|
550
624
|
|
|
551
625
|
@task_name.setter
|
|
552
626
|
def task_name(self, task_name: str) -> None:
|
|
553
627
|
self._task_name = task_name
|
|
554
628
|
if task_name not in self.preset_task_names:
|
|
555
|
-
print(
|
|
629
|
+
print(
|
|
630
|
+
f"{task_name = !r} doesn't correspond to a preset value, but the attribute is updated anyway!"
|
|
631
|
+
)
|
|
556
632
|
else:
|
|
557
633
|
print(f"Updated {self.__class__.__name__}.{task_name = !r}")
|
|
558
|
-
|
|
634
|
+
|
|
559
635
|
stims = (np_services.ScriptCamstim,)
|
|
560
|
-
|
|
636
|
+
|
|
561
637
|
@property
|
|
562
638
|
def recorders(self) -> tuple[Service, ...]:
|
|
563
639
|
"""Services to be started before stimuli run, and stopped after. Session-dependent."""
|
|
@@ -565,161 +641,186 @@ class DynamicRoutingExperiment(WithSession):
|
|
|
565
641
|
return (np_services.Sync, np_services.VideoMVR, np_services.OpenEphys)
|
|
566
642
|
return (np_services.Sync, np_services.VideoMVR)
|
|
567
643
|
|
|
568
|
-
|
|
569
644
|
@property
|
|
570
645
|
def hdf5_dir(self) -> pathlib.Path:
|
|
571
|
-
return self.base_path /
|
|
572
|
-
|
|
646
|
+
return self.base_path / "Data" / str(self.mouse)
|
|
647
|
+
|
|
573
648
|
@property
|
|
574
649
|
def task_script_base(self) -> upath.UPath:
|
|
575
650
|
return self.base_url if self.use_github else upath.UPath(self.base_path)
|
|
576
|
-
|
|
651
|
+
|
|
577
652
|
@property
|
|
578
653
|
def task_params(self) -> dict[str, str | bool]:
|
|
579
654
|
"""For sending to runTask.py"""
|
|
580
655
|
return dict(
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
656
|
+
rigName=str(self.rig).replace(".", ""),
|
|
657
|
+
subjectName=str(self.mouse),
|
|
658
|
+
taskScript="DynamicRouting1.py",
|
|
659
|
+
taskVersion=self.task_name,
|
|
660
|
+
saveSoundArray=True,
|
|
586
661
|
)
|
|
587
|
-
|
|
662
|
+
|
|
588
663
|
@property
|
|
589
664
|
def spontaneous_params(self) -> dict[str, str]:
|
|
590
665
|
"""For sending to runTask.py"""
|
|
591
666
|
return dict(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
667
|
+
rigName=str(self.rig).replace(".", ""),
|
|
668
|
+
subjectName=str(self.mouse),
|
|
669
|
+
taskScript="TaskControl.py",
|
|
670
|
+
taskVersion="spontaneous",
|
|
596
671
|
)
|
|
597
|
-
|
|
672
|
+
|
|
598
673
|
@property
|
|
599
674
|
def spontaneous_rewards_params(self) -> dict[str, str]:
|
|
600
675
|
"""For sending to runTask.py"""
|
|
601
676
|
return dict(
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
677
|
+
rigName=str(self.rig).replace(".", ""),
|
|
678
|
+
subjectName=str(self.mouse),
|
|
679
|
+
taskScript="TaskControl.py",
|
|
680
|
+
taskVersion="spontaneous rewards",
|
|
681
|
+
rewardSound="device",
|
|
607
682
|
)
|
|
608
|
-
|
|
609
|
-
def get_latest_optogui_txt(
|
|
610
|
-
|
|
683
|
+
|
|
684
|
+
def get_latest_optogui_txt(
|
|
685
|
+
self, opto_or_optotagging: Literal["opto", "optotagging"]
|
|
686
|
+
) -> pathlib.Path:
|
|
687
|
+
dirname = dict(opto="optoParams", optotagging="optotagging")[
|
|
688
|
+
opto_or_optotagging
|
|
689
|
+
]
|
|
611
690
|
file_prefix = dirname
|
|
612
|
-
|
|
613
|
-
rig = str(self.rig).replace(
|
|
614
|
-
locs_root = self.base_path /
|
|
615
|
-
available_locs = sorted(
|
|
691
|
+
|
|
692
|
+
rig = str(self.rig).replace(".", "")
|
|
693
|
+
locs_root = self.base_path / "OptoGui" / f"{dirname}"
|
|
694
|
+
available_locs = sorted(
|
|
695
|
+
tuple(locs_root.glob(f"{file_prefix}_{self.mouse.id}_{rig}_*")),
|
|
696
|
+
reverse=True,
|
|
697
|
+
)
|
|
616
698
|
if not available_locs:
|
|
617
|
-
raise FileNotFoundError(
|
|
699
|
+
raise FileNotFoundError(
|
|
700
|
+
f"No optotagging locs found for {self.mouse}/{rig} - have you run OptoGui?"
|
|
701
|
+
)
|
|
618
702
|
return available_locs[0]
|
|
619
|
-
|
|
620
|
-
|
|
703
|
+
|
|
621
704
|
@property
|
|
622
705
|
def optotagging_params(self):
|
|
623
706
|
"""For sending to runTask.py"""
|
|
624
|
-
if hasattr(self,
|
|
625
|
-
return self._optotagging_params
|
|
626
|
-
|
|
707
|
+
if hasattr(self, "_optotagging_params"):
|
|
708
|
+
return self._optotagging_params
|
|
709
|
+
|
|
627
710
|
else:
|
|
628
|
-
#set to defaults through setter
|
|
711
|
+
# set to defaults through setter
|
|
629
712
|
self.optotagging_params = {}
|
|
630
713
|
return self._optotagging_params
|
|
631
|
-
|
|
714
|
+
|
|
632
715
|
@optotagging_params.setter
|
|
633
716
|
def optotagging_params(self, paramsdict):
|
|
634
|
-
|
|
717
|
+
|
|
635
718
|
self._optotagging_params = dict(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
719
|
+
rigName=str(self.rig).replace(".", ""),
|
|
720
|
+
subjectName=str(self.mouse),
|
|
721
|
+
taskScript="OptoTagging.py",
|
|
722
|
+
optoTaggingLocs=self.get_latest_optogui_txt("optotagging").as_posix(),
|
|
723
|
+
)
|
|
641
724
|
self._optotagging_params.update(paramsdict)
|
|
642
|
-
|
|
725
|
+
|
|
643
726
|
@property
|
|
644
727
|
def opto_params(self) -> dict[str, str | bool]:
|
|
645
728
|
"""Opto params are handled by runTask.py and don't need to be passed from
|
|
646
729
|
here. Just check they exist on disk here.
|
|
647
730
|
"""
|
|
648
|
-
_ = self.get_latest_optogui_txt(
|
|
731
|
+
_ = self.get_latest_optogui_txt("opto") # raises FileNotFoundError if not found
|
|
649
732
|
return dict(
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
733
|
+
rigName=str(self.rig).replace(".", ""),
|
|
734
|
+
subjectName=str(self.mouse),
|
|
735
|
+
taskScript="DynamicRouting1.py",
|
|
736
|
+
saveSoundArray=True,
|
|
737
|
+
)
|
|
655
738
|
|
|
656
739
|
@property
|
|
657
740
|
def mapping_params(self) -> dict[str, str | bool]:
|
|
658
741
|
"""For sending to runTask.py"""
|
|
659
742
|
return dict(
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
743
|
+
rigName=str(self.rig).replace(".", ""),
|
|
744
|
+
subjectName=str(self.mouse),
|
|
745
|
+
taskScript="RFMapping.py",
|
|
746
|
+
saveSoundArray=True,
|
|
747
|
+
)
|
|
665
748
|
|
|
666
749
|
@property
|
|
667
750
|
def sound_test_params(self) -> dict[str, str]:
|
|
668
751
|
"""For sending to runTask.py"""
|
|
669
752
|
return dict(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
753
|
+
rigName=str(self.rig).replace(".", ""),
|
|
754
|
+
subjectName="sound",
|
|
755
|
+
taskScript="TaskControl.py",
|
|
756
|
+
taskVersion="sound test",
|
|
674
757
|
)
|
|
675
|
-
|
|
758
|
+
|
|
676
759
|
def get_github_file_content(self, address: str) -> str:
|
|
677
760
|
import requests
|
|
761
|
+
|
|
678
762
|
response = requests.get(address)
|
|
679
|
-
if response.status_code not in (200,
|
|
763
|
+
if response.status_code not in (200,):
|
|
680
764
|
response.raise_for_status()
|
|
681
765
|
return response.content.decode("utf-8")
|
|
682
|
-
|
|
766
|
+
|
|
683
767
|
@property
|
|
684
768
|
def camstim_script(self) -> upath.UPath:
|
|
685
|
-
return self.task_script_base /
|
|
686
|
-
|
|
687
|
-
def run_script(
|
|
688
|
-
|
|
769
|
+
return self.task_script_base / "runTask.py"
|
|
770
|
+
|
|
771
|
+
def run_script(
|
|
772
|
+
self,
|
|
773
|
+
stim: Literal[
|
|
774
|
+
"sound_test",
|
|
775
|
+
"mapping",
|
|
776
|
+
"task",
|
|
777
|
+
"opto",
|
|
778
|
+
"optotagging",
|
|
779
|
+
"spontaneous",
|
|
780
|
+
"spontaneous_rewards",
|
|
781
|
+
],
|
|
782
|
+
) -> None:
|
|
783
|
+
|
|
689
784
|
params = copy.deepcopy(getattr(self, f'{stim.replace(" ", "_")}_params'))
|
|
690
|
-
|
|
785
|
+
|
|
691
786
|
# add mouse and user info for MPE
|
|
692
|
-
params[
|
|
693
|
-
params[
|
|
694
|
-
|
|
695
|
-
if self.task_script_base.as_posix() not in params[
|
|
696
|
-
params[
|
|
697
|
-
|
|
787
|
+
params["mouse_id"] = str(self.mouse.id)
|
|
788
|
+
params["user_id"] = self.user.id if self.user else "ben.hardcastle"
|
|
789
|
+
|
|
790
|
+
if self.task_script_base.as_posix() not in params["taskScript"]:
|
|
791
|
+
params["taskScript"] = (
|
|
792
|
+
self.task_script_base / params["taskScript"]
|
|
793
|
+
).as_posix()
|
|
794
|
+
|
|
698
795
|
if self.is_pretest:
|
|
699
|
-
params[
|
|
700
|
-
params[
|
|
701
|
-
|
|
796
|
+
params["maxFrames"] = 60 * 15
|
|
797
|
+
params["maxTrials"] = 3
|
|
798
|
+
|
|
702
799
|
if self.use_github:
|
|
703
|
-
|
|
704
|
-
params[
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
params[
|
|
800
|
+
|
|
801
|
+
params["GHTaskScriptParams"] = {
|
|
802
|
+
"taskScript": params["taskScript"],
|
|
803
|
+
"taskControl": (self.task_script_base / "TaskControl.py").as_posix(),
|
|
804
|
+
"taskUtils": (self.task_script_base / "TaskUtils.py").as_posix(),
|
|
805
|
+
}
|
|
806
|
+
params["task_script_commit_hash"] = self.commit_hash
|
|
710
807
|
|
|
711
808
|
np_services.ScriptCamstim.script = self.camstim_script.read_text()
|
|
712
809
|
else:
|
|
713
810
|
np_services.ScriptCamstim.script = self.camstim_script.as_posix()
|
|
714
|
-
|
|
715
|
-
if (
|
|
811
|
+
|
|
812
|
+
if (
|
|
813
|
+
script_override_params := getattr(self, "script_override_params", None)
|
|
814
|
+
) is not None:
|
|
716
815
|
if not isinstance(script_override_params, Mapping):
|
|
717
|
-
raise TypeError(
|
|
816
|
+
raise TypeError(
|
|
817
|
+
f"script_override_params must be a dict/Mapping, got {type(script_override_params)}"
|
|
818
|
+
)
|
|
718
819
|
else:
|
|
719
820
|
script_override_params = {}
|
|
720
|
-
|
|
821
|
+
|
|
721
822
|
np_services.ScriptCamstim.params = params | script_override_params
|
|
722
|
-
|
|
823
|
+
|
|
723
824
|
self.update_state()
|
|
724
825
|
self.log(f"{stim} started")
|
|
725
826
|
|
|
@@ -727,31 +828,32 @@ class DynamicRoutingExperiment(WithSession):
|
|
|
727
828
|
with contextlib.suppress(np_services.resources.zro.ZroError):
|
|
728
829
|
while not np_services.ScriptCamstim.is_ready_to_start():
|
|
729
830
|
time.sleep(1)
|
|
730
|
-
|
|
831
|
+
|
|
731
832
|
self.log(f"{stim} complete")
|
|
732
833
|
|
|
733
834
|
with contextlib.suppress(np_services.resources.zro.ZroError):
|
|
734
835
|
np_services.ScriptCamstim.finalize()
|
|
735
|
-
|
|
736
|
-
run_mapping = functools.partialmethod(run_script,
|
|
737
|
-
run_sound_test = functools.partialmethod(run_script,
|
|
738
|
-
run_task = functools.partialmethod(run_script,
|
|
739
|
-
run_opto = functools.partialmethod(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
836
|
+
|
|
837
|
+
run_mapping = functools.partialmethod(run_script, "mapping")
|
|
838
|
+
run_sound_test = functools.partialmethod(run_script, "sound_test")
|
|
839
|
+
run_task = functools.partialmethod(run_script, "task")
|
|
840
|
+
run_opto = functools.partialmethod(
|
|
841
|
+
run_script, "opto"
|
|
842
|
+
) # if opto params are handled by runTask then this is the same as run_task
|
|
843
|
+
run_optotagging = functools.partialmethod(run_script, "optotagging")
|
|
844
|
+
run_spontaneous = functools.partialmethod(run_script, "spontaneous")
|
|
845
|
+
run_spontaneous_rewards = functools.partialmethod(run_script, "spontaneous_rewards")
|
|
846
|
+
|
|
745
847
|
def update_state(self) -> None:
|
|
746
848
|
"Persist useful but non-essential info."
|
|
747
|
-
self.mouse.state[
|
|
748
|
-
self.mouse.state[
|
|
749
|
-
self.mouse.state[
|
|
750
|
-
|
|
849
|
+
self.mouse.state["last_session"] = self.session.id
|
|
850
|
+
self.mouse.state["last_workflow"] = str(self.workflow.name)
|
|
851
|
+
self.mouse.state["last_task"] = str(self.task_name)
|
|
852
|
+
|
|
751
853
|
def initialize_and_test_services(self) -> None:
|
|
752
854
|
"""Configure, initialize (ie. reset), then test all services."""
|
|
753
|
-
|
|
754
|
-
np_services.ScriptCamstim.script =
|
|
855
|
+
|
|
856
|
+
np_services.ScriptCamstim.script = "//allen/programs/mindscope/workgroups/dynamicrouting/DynamicRoutingTask/runTask.py"
|
|
755
857
|
np_services.ScriptCamstim.data_root = self.hdf5_dir
|
|
756
858
|
|
|
757
859
|
np_services.MouseDirector.user = self.user.id
|
|
@@ -766,29 +868,34 @@ class DynamicRoutingExperiment(WithSession):
|
|
|
766
868
|
super().initialize_and_test_services()
|
|
767
869
|
|
|
768
870
|
def copy_ephys(self) -> None:
|
|
769
|
-
# copy ephys
|
|
770
|
-
password = np_config.fetch(
|
|
771
|
-
ssh = fabric.Connection(
|
|
871
|
+
# copy ephys
|
|
872
|
+
password = np_config.fetch("/logins")["svc_neuropix"]["password"]
|
|
873
|
+
ssh = fabric.Connection(
|
|
874
|
+
host=np_services.OpenEphys.host,
|
|
875
|
+
user="svc_neuropix",
|
|
876
|
+
connect_kwargs=dict(password=password),
|
|
877
|
+
)
|
|
772
878
|
for ephys_folder in np_services.OpenEphys.data_files:
|
|
773
|
-
if
|
|
879
|
+
if "__temp__" in ephys_folder.name:
|
|
774
880
|
continue
|
|
775
881
|
if isinstance(self.session, np_session.TempletonPilotSession):
|
|
776
|
-
ephys_folder = next(ephys_folder.glob(
|
|
882
|
+
ephys_folder = next(ephys_folder.glob("Record Node*"))
|
|
777
883
|
with ssh, contextlib.suppress(invoke.UnexpectedExit):
|
|
778
884
|
ssh.run(
|
|
779
|
-
|
|
780
|
-
|
|
885
|
+
f'robocopy "{ephys_folder}" "{self.session.npexp_path / ephys_folder.name}" /j /s /xo'
|
|
886
|
+
# /j unbuffered, /s incl non-empty subdirs, /xo exclude src files older than dest
|
|
781
887
|
)
|
|
782
|
-
|
|
783
888
|
|
|
784
889
|
def copy_data_files(self) -> None:
|
|
785
890
|
"""Copy files from raw data storage to session folder for all services
|
|
786
891
|
except Open Ephys."""
|
|
787
|
-
|
|
892
|
+
|
|
788
893
|
# copy vimba files:
|
|
789
894
|
for file in pathlib.Path(
|
|
790
|
-
np_config.local_to_unc(
|
|
791
|
-
|
|
895
|
+
np_config.local_to_unc(
|
|
896
|
+
self.rig.mon, np_services.config_from_zk()["ImageVimba"]["data"]
|
|
897
|
+
)
|
|
898
|
+
).glob(f"{self.session.npexp_path.name}*"):
|
|
792
899
|
shutil.copy2(file, self.session.npexp_path)
|
|
793
900
|
npxc.validate_or_overwrite(self.session.npexp_path / file.name, file)
|
|
794
901
|
print(file)
|
|
@@ -797,13 +904,21 @@ class DynamicRoutingExperiment(WithSession):
|
|
|
797
904
|
for service in self.services:
|
|
798
905
|
match service.__name__:
|
|
799
906
|
case "ScriptCamstim" | "SessionCamstim":
|
|
800
|
-
files = tuple(
|
|
907
|
+
files = tuple(
|
|
908
|
+
_
|
|
909
|
+
for _ in self.hdf5_dir.glob("*")
|
|
910
|
+
if _.stat().st_ctime > self.stims[0].initialization
|
|
911
|
+
)
|
|
801
912
|
case "np_services.open_ephys":
|
|
802
|
-
continue
|
|
913
|
+
continue # copy ephys after other files
|
|
803
914
|
case "NewScaleCoordinateRecorder":
|
|
804
|
-
files = tuple(service.data_root.glob(
|
|
915
|
+
files = tuple(service.data_root.glob("*")) + tuple(
|
|
916
|
+
self.rig.paths["NewScaleCoordinateRecorder"].glob("*")
|
|
917
|
+
)
|
|
805
918
|
case _:
|
|
806
|
-
files: Iterable[pathlib.Path] =
|
|
919
|
+
files: Iterable[pathlib.Path] = (
|
|
920
|
+
service.data_files or service.get_latest_data("*")
|
|
921
|
+
)
|
|
807
922
|
if not files:
|
|
808
923
|
continue
|
|
809
924
|
files = set(files)
|
|
@@ -812,15 +927,22 @@ class DynamicRoutingExperiment(WithSession):
|
|
|
812
927
|
shutil.copy2(file, self.session.npexp_path)
|
|
813
928
|
npxc.validate_or_overwrite(self.session.npexp_path / file.name, file)
|
|
814
929
|
|
|
815
|
-
#TODO move this to a dedicated np_service class instead of using ScriptCamstim
|
|
816
|
-
def run_stim_desktop_theme_script(self, selection: str) -> None:
|
|
817
|
-
np_services.ScriptCamstim.script =
|
|
818
|
-
|
|
930
|
+
# TODO move this to a dedicated np_service class instead of using ScriptCamstim
|
|
931
|
+
def run_stim_desktop_theme_script(self, selection: str) -> None:
|
|
932
|
+
np_services.ScriptCamstim.script = (
|
|
933
|
+
"//allen/programs/mindscope/workgroups/dynamicrouting/ben/change_desktop.py"
|
|
934
|
+
)
|
|
935
|
+
np_services.ScriptCamstim.params = {"selection": selection}
|
|
819
936
|
np_services.ScriptCamstim.start()
|
|
820
937
|
while not np_services.ScriptCamstim.is_ready_to_start():
|
|
821
938
|
time.sleep(0.1)
|
|
822
939
|
|
|
823
|
-
set_grey_desktop_on_stim = functools.partialmethod(
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
940
|
+
set_grey_desktop_on_stim = functools.partialmethod(
|
|
941
|
+
run_stim_desktop_theme_script, "grey"
|
|
942
|
+
)
|
|
943
|
+
set_dark_desktop_on_stim = functools.partialmethod(
|
|
944
|
+
run_stim_desktop_theme_script, "dark"
|
|
945
|
+
)
|
|
946
|
+
reset_desktop_on_stim = functools.partialmethod(
|
|
947
|
+
run_stim_desktop_theme_script, "reset"
|
|
948
|
+
)
|