np-workflows 1.6.89__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.
Files changed (76) hide show
  1. np_workflows/__init__.py +3 -5
  2. np_workflows/experiments/dynamic_routing/main.py +20 -41
  3. np_workflows/experiments/dynamic_routing/widgets.py +29 -24
  4. np_workflows/experiments/openscope_P3/P3_workflow_widget.py +11 -19
  5. np_workflows/experiments/openscope_P3/__init__.py +1 -1
  6. np_workflows/experiments/openscope_P3/main_P3_pilot.py +66 -68
  7. np_workflows/experiments/openscope_barcode/__init__.py +1 -1
  8. np_workflows/experiments/openscope_barcode/barcode_workflow_widget.py +14 -20
  9. np_workflows/experiments/openscope_barcode/camstim_scripts/barcode_mapping_script.py +8 -14
  10. np_workflows/experiments/openscope_barcode/camstim_scripts/barcode_opto_script.py +121 -68
  11. np_workflows/experiments/openscope_barcode/main_barcode_pilot.py +69 -69
  12. np_workflows/experiments/openscope_loop/__init__.py +1 -1
  13. np_workflows/experiments/openscope_loop/camstim_scripts/barcode_mapping_script.py +8 -14
  14. np_workflows/experiments/openscope_loop/camstim_scripts/barcode_opto_script.py +121 -68
  15. np_workflows/experiments/openscope_loop/loop_workflow_widget.py +11 -19
  16. np_workflows/experiments/openscope_loop/main_loop_pilot.py +66 -68
  17. np_workflows/experiments/openscope_psycode/__init__.py +1 -1
  18. np_workflows/experiments/openscope_psycode/main_psycode_pilot.py +69 -69
  19. np_workflows/experiments/openscope_psycode/psycode_workflow_widget.py +14 -20
  20. np_workflows/experiments/openscope_v2/__init__.py +1 -1
  21. np_workflows/experiments/openscope_v2/main_v2_pilot.py +66 -68
  22. np_workflows/experiments/openscope_v2/v2_workflow_widget.py +11 -19
  23. np_workflows/experiments/openscope_vippo/__init__.py +1 -1
  24. np_workflows/experiments/openscope_vippo/main_vippo_pilot.py +66 -68
  25. np_workflows/experiments/openscope_vippo/vippo_workflow_widget.py +14 -20
  26. np_workflows/experiments/task_trained_network/__init__.py +1 -1
  27. np_workflows/experiments/task_trained_network/camstim_scripts/make_tt_stims.py +24 -14
  28. np_workflows/experiments/task_trained_network/camstim_scripts/oct22_tt_stim_script.py +54 -41
  29. np_workflows/experiments/task_trained_network/camstim_scripts/ttn_main_script.py +19 -22
  30. np_workflows/experiments/task_trained_network/camstim_scripts/ttn_mapping_script.py +8 -14
  31. np_workflows/experiments/task_trained_network/camstim_scripts/ttn_opto_script.py +121 -68
  32. np_workflows/experiments/task_trained_network/main_ttn_pilot.py +73 -68
  33. np_workflows/experiments/task_trained_network/ttn_session_widget.py +11 -19
  34. np_workflows/experiments/task_trained_network/ttn_stim_config.py +23 -19
  35. np_workflows/experiments/templeton/main.py +18 -41
  36. np_workflows/experiments/templeton/widgets.py +26 -23
  37. np_workflows/shared/__init__.py +1 -1
  38. np_workflows/shared/base_experiments.py +430 -308
  39. np_workflows/shared/npxc.py +85 -53
  40. np_workflows/shared/widgets.py +374 -224
  41. {np_workflows-1.6.89.dist-info → np_workflows-1.6.91.dist-info}/METADATA +7 -21
  42. np_workflows-1.6.91.dist-info/RECORD +48 -0
  43. {np_workflows-1.6.89.dist-info → np_workflows-1.6.91.dist-info}/WHEEL +2 -1
  44. np_workflows-1.6.91.dist-info/entry_points.txt +2 -0
  45. np_workflows-1.6.91.dist-info/top_level.txt +1 -0
  46. np_workflows/assets/images/logo_np_hab.png +0 -0
  47. np_workflows/assets/images/logo_np_vis.png +0 -0
  48. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_00.stim +0 -5
  49. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_01.stim +0 -5
  50. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_02.stim +0 -5
  51. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_03.stim +0 -5
  52. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_04.stim +0 -5
  53. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_05.stim +0 -5
  54. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_06.stim +0 -5
  55. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_07.stim +0 -5
  56. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_08.stim +0 -5
  57. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_09.stim +0 -5
  58. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_10.stim +0 -5
  59. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_11.stim +0 -5
  60. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_12.stim +0 -5
  61. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_13.stim +0 -5
  62. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_14.stim +0 -5
  63. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_15.stim +0 -5
  64. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_16.stim +0 -5
  65. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_17.stim +0 -5
  66. np_workflows/experiments/task_trained_network/camstim_scripts/stims/densely_annotated_18.stim +0 -5
  67. np_workflows/experiments/task_trained_network/camstim_scripts/stims/flash_250ms.stim +0 -20
  68. np_workflows/experiments/task_trained_network/camstim_scripts/stims/gabor_20_deg_250ms.stim +0 -30
  69. np_workflows/experiments/task_trained_network/camstim_scripts/stims/old_stim.stim +0 -5
  70. np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed.stim +0 -5
  71. np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed_1st.stim +0 -5
  72. np_workflows/experiments/task_trained_network/camstim_scripts/stims/shuffle_reversed_2nd.stim +0 -5
  73. np_workflows/shared/camstim_scripts/flash_250ms.stim +0 -20
  74. np_workflows/shared/camstim_scripts/gabor_20_deg_250ms.stim +0 -30
  75. np_workflows-1.6.89.dist-info/RECORD +0 -76
  76. np_workflows-1.6.89.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 typing import Any, ClassVar, Iterable, Literal, Mapping, Optional, Protocol, Sequence, Type
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
- import np_services.resources.zro
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]] = np_session.PipelineSession
52
- default_session_type: Literal['ephys', 'hab'] = 'ephys'
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('BaseWithSessionWorkflow', ('BASECLASS')).BASECLASS # type: ignore
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'{weblog_name.lower()}_{self.mouse}').info(message)
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__(self,
74
- mouse: Optional[str | int | np_session.LIMS2MouseInfo] = None,
75
- operator: Optional[str | np_session.LIMS2UserInfo] = None,
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['ephys', 'hab']] = None,
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('%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)
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('%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)
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('Must specify either a mouse + operator, or an existing session')
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'{self.__class__.__name__}({self.session})'
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(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
-
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['ephys', 'hab']:
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 'hab'
120
- return 'ephys'
121
- raise AttributeError('Session has not been set')
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['ephys', 'hab']):
125
- if value not in ('ephys', 'hab'):
126
- raise ValueError(f'Session type must be either "ephys" or "hab": got {value!r}')
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('Set session_type to %r', value)
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('Rig has not been set')
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, 'recorders'):
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(self,
208
- recorders: Optional[Iterable[Stoppable]] = None,
209
- stims: Optional[Iterable[Stoppable]] = None,
210
- ) -> None:
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, 'recorders'):
252
+ if not recorders and hasattr(self, "recorders"):
217
253
  recorders = reversed(self.recorders)
218
- if not stims and hasattr(self, 'stims'):
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 'videomvr' in stoppable.__name__.lower():
260
+ if "videomvr" in stoppable.__name__.lower():
225
261
  sleep_s = 4
226
262
  time.sleep(sleep_s)
227
- logger.warning(f'Waiting additional {sleep_s} s for MVR to finish writing...')
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 != 'hab':
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('.').resolve()
286
- dest = self.session.npexp_path / 'exp'
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 / 'pdm.lock'
292
- pyproject = cwd.parent / 'pyproject.toml'
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
- self.rig.mvr_config,
301
- self.rig.sync_config,
302
- self.rig.camstim_config):
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('docmanager:save')
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("Could not load camstim defaults from config file on Stim computer.")
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('rig_id', str(self.rig))
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
- def stop_recording_after_stim_finished(self,
358
- recorders: Optional[Iterable[Stoppable]] = None,
359
- stims: Optional[Iterable[Stoppable]] = None,
360
- ) -> None:
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('Renaming: no ephys folders have been recorded')
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 '_probeABC' in name or '_probeDEF' in name:
374
- logger.debug(f'Renaming: {name} already has probe letter suffix - aborted')
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 length := len(split_folders := [_ for _ in folders if _.name == name]) != 2:
377
- logger.info(f'Renaming: {length} folders found for {name}, expected 2 - aborted')
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('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}'))
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('Renamed split ephys folders %r', split_folders)
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 = '[0-9a-fA-F]'
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 # copy ephys after other files
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 == '.h5':
409
- renamed = f'{self.session.folder}.sync'
410
- elif file.suffix == '.pkl':
411
- for _ in ('opto', 'main', 'mapping', 'behavior'):
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'{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'
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 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():
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 = f'{self.session.folder}{img_label}{file.suffix}'
442
- shutil.copy2(file, self.session.npexp_path / (renamed or file.name))
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('/logins')['svc_neuropix']['password']
448
- ssh = fabric.Connection(host=np_services.OpenEphys.host, user='svc_neuropix', connect_kwargs=dict(password=password))
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
- 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
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 = 'ephys'
528
+ default_session_type = "ephys"
529
+
459
530
 
460
531
  class PipelineHab(PipelineExperiment):
461
- default_session_type = 'hab'
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('/projects/dynamicrouting')['preset_task_names'])
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, '_commit_hash'):
567
+ if hasattr(self, "_commit_hash"):
496
568
  return self._commit_hash
497
- self._commit_hash = self.config['dynamicrouting_task_script']['commit_hash']
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, '_github_url'):
578
+ if hasattr(self, "_github_url"):
507
579
  return self._github_url
508
- self._github_url = self.config['dynamicrouting_task_script']['url']
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('//allen/programs/mindscope/workgroups/dynamicrouting/DynamicRoutingTask/')
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 'PRETEST' in self.workflow.name
526
-
599
+ return "PRETEST" in self.workflow.name
600
+
527
601
  @property
528
602
  def is_hab(self) -> bool:
529
- return 'HAB' in self.workflow.name
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 'opto' in self.task_name
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 'AI32' in str(self.mouse.lims['full_genotype']).upper()
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 'EPHYS' in self.workflow.name
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, '_task_name'):
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(f"{task_name = !r} doesn't correspond to a preset value, but the attribute is updated anyway!")
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 / 'Data' / str(self.mouse)
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
- rigName = str(self.rig).replace('.',''),
582
- subjectName = str(self.mouse),
583
- taskScript = 'DynamicRouting1.py',
584
- taskVersion = self.task_name,
585
- saveSoundArray = True,
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
- rigName = str(self.rig).replace('.',''),
593
- subjectName = str(self.mouse),
594
- taskScript = 'TaskControl.py',
595
- taskVersion = 'spontaneous',
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
- rigName = str(self.rig).replace('.',''),
603
- subjectName = str(self.mouse),
604
- taskScript = 'TaskControl.py',
605
- taskVersion = 'spontaneous rewards',
606
- rewardSound = "device",
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(self, opto_or_optotagging: Literal['opto', 'optotagging']) -> pathlib.Path:
610
- dirname = dict(opto='optoParams', optotagging='optotagging')[opto_or_optotagging]
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 / 'OptoGui' / f'{dirname}'
615
- available_locs = sorted(tuple(locs_root.glob(f"{file_prefix}_{self.mouse.id}_{rig}_*")), reverse=True)
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(f"No optotagging locs found for {self.mouse}/{rig} - have you run OptoGui?")
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, '_optotagging_params'):
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
- 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
- )
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('opto') # raises FileNotFoundError if not found
731
+ _ = self.get_latest_optogui_txt("opto") # raises FileNotFoundError if not found
649
732
  return dict(
650
- rigName = str(self.rig).replace('.',''),
651
- subjectName = str(self.mouse),
652
- taskScript = 'DynamicRouting1.py',
653
- saveSoundArray = True,
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
- rigName = str(self.rig).replace('.',''),
661
- subjectName = str(self.mouse),
662
- taskScript = 'RFMapping.py',
663
- saveSoundArray = True,
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
- rigName = str(self.rig).replace('.',''),
671
- subjectName = 'sound',
672
- taskScript = 'TaskControl.py',
673
- taskVersion = 'sound test',
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 / 'runTask.py'
686
-
687
- def run_script(self, stim: Literal['sound_test', 'mapping', 'task', 'opto', 'optotagging', 'spontaneous', 'spontaneous_rewards']) -> None:
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['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
-
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['maxFrames'] = 60 * 15
700
- params['maxTrials'] = 3
701
-
796
+ params["maxFrames"] = 60 * 15
797
+ params["maxTrials"] = 3
798
+
702
799
  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
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 (script_override_params := getattr(self, 'script_override_params', None)) is not None:
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(f"script_override_params must be a dict/Mapping, got {type(script_override_params)}")
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, '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
-
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['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
-
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 = '//allen/programs/mindscope/workgroups/dynamicrouting/DynamicRoutingTask/runTask.py'
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('/logins')['svc_neuropix']['password']
771
- ssh = fabric.Connection(host=np_services.OpenEphys.host, user='svc_neuropix', connect_kwargs=dict(password=password))
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 '__temp__' in ephys_folder.name:
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('Record Node*'))
882
+ ephys_folder = next(ephys_folder.glob("Record Node*"))
777
883
  with ssh, contextlib.suppress(invoke.UnexpectedExit):
778
884
  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
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(self.rig.mon, np_services.config_from_zk()['ImageVimba']['data'])
791
- ).glob(f'{self.session.npexp_path.name}*'):
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(_ for _ in self.hdf5_dir.glob('*') if _.stat().st_ctime > self.stims[0].initialization)
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 # copy ephys after other files
913
+ continue # copy ephys after other files
803
914
  case "NewScaleCoordinateRecorder":
804
- files = tuple(service.data_root.glob('*')) + tuple(self.rig.paths['NewScaleCoordinateRecorder'].glob('*'))
915
+ files = tuple(service.data_root.glob("*")) + tuple(
916
+ self.rig.paths["NewScaleCoordinateRecorder"].glob("*")
917
+ )
805
918
  case _:
806
- files: Iterable[pathlib.Path] = service.data_files or service.get_latest_data('*')
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 = '//allen/programs/mindscope/workgroups/dynamicrouting/ben/change_desktop.py'
818
- np_services.ScriptCamstim.params = {'selection': selection}
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(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
-
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
+ )