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