mesofield 0.3.2b0__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.
- docs/_static/custom.css +40 -0
- docs/_static/favicon.png +0 -0
- docs/_static/logo.png +0 -0
- docs/api/index.md +70 -0
- docs/conf.py +200 -0
- docs/developer_guide.md +303 -0
- docs/index.md +25 -0
- docs/tutorial.md +4 -0
- docs/user_guide.md +172 -0
- examples/teensy_pulse_generator.py +320 -0
- experiments/pipeline_demo/experiment.json +24 -0
- experiments/pipeline_demo/hardware.yaml +23 -0
- experiments/pipeline_demo/procedure.py +50 -0
- experiments/two_cam_demo/experiment.json +24 -0
- experiments/two_cam_demo/hardware.yaml +58 -0
- experiments/two_cam_demo/load_dataset.py +213 -0
- experiments/two_cam_demo/procedure.py +87 -0
- external/video-codecs/openh264-1.8.0-win64.dll +0 -0
- mesofield/__init__.py +45 -0
- mesofield/__main__.py +11 -0
- mesofield/_version.py +24 -0
- mesofield/base.py +750 -0
- mesofield/cli/__init__.py +57 -0
- mesofield/cli/_richhelp.py +100 -0
- mesofield/cli/acquire.py +254 -0
- mesofield/cli/datakit.py +165 -0
- mesofield/cli/process.py +376 -0
- mesofield/cli/rig.py +108 -0
- mesofield/cli/tools.py +347 -0
- mesofield/config.py +751 -0
- mesofield/data/__init__.py +23 -0
- mesofield/data/batch.py +633 -0
- mesofield/data/manager.py +388 -0
- mesofield/data/writer.py +289 -0
- mesofield/datakit/__init__.py +44 -0
- mesofield/datakit/__main__.py +35 -0
- mesofield/datakit/_utils/_logger.py +5 -0
- mesofield/datakit/_version.py +141 -0
- mesofield/datakit/config.py +50 -0
- mesofield/datakit/core.py +783 -0
- mesofield/datakit/datamodel.py +200 -0
- mesofield/datakit/discover.py +124 -0
- mesofield/datakit/explore.py +651 -0
- mesofield/datakit/notebooks/pupil_dlc.ipynb +2445 -0
- mesofield/datakit/profile.py +535 -0
- mesofield/datakit/shell.py +83 -0
- mesofield/datakit/sources/__init__.py +65 -0
- mesofield/datakit/sources/analysis/mesomap.py +194 -0
- mesofield/datakit/sources/analysis/mesoscope.py +77 -0
- mesofield/datakit/sources/analysis/pupil.py +246 -0
- mesofield/datakit/sources/behavior/__init__.py +0 -0
- mesofield/datakit/sources/behavior/dataqueue.py +281 -0
- mesofield/datakit/sources/behavior/psychopy.py +364 -0
- mesofield/datakit/sources/behavior/treadmill.py +323 -0
- mesofield/datakit/sources/behavior/wheel.py +277 -0
- mesofield/datakit/sources/camera/mesoscope.py +32 -0
- mesofield/datakit/sources/camera/metadata_json.py +130 -0
- mesofield/datakit/sources/camera/pupil.py +28 -0
- mesofield/datakit/sources/camera/suite2p.py +547 -0
- mesofield/datakit/sources/register.py +204 -0
- mesofield/datakit/sources/session/config.py +130 -0
- mesofield/datakit/sources/session/notes.py +63 -0
- mesofield/datakit/sources/session/timestamps.py +58 -0
- mesofield/datakit/timeline.py +306 -0
- mesofield/devices/__init__.py +42 -0
- mesofield/devices/base.py +498 -0
- mesofield/devices/base_camera.py +295 -0
- mesofield/devices/cameras.py +740 -0
- mesofield/devices/daq.py +151 -0
- mesofield/devices/encoder.py +384 -0
- mesofield/devices/mocks.py +275 -0
- mesofield/devices/psychopy_device.py +455 -0
- mesofield/devices/subprocesses/__init__.py +0 -0
- mesofield/devices/subprocesses/psychopy.py +133 -0
- mesofield/devices/treadmill.py +318 -0
- mesofield/engines.py +380 -0
- mesofield/gui/Mesofield_icon.png +0 -0
- mesofield/gui/__init__.py +76 -0
- mesofield/gui/config_wizard.py +724 -0
- mesofield/gui/controller.py +535 -0
- mesofield/gui/dynamic_controller.py +78 -0
- mesofield/gui/maingui.py +427 -0
- mesofield/gui/mdagui.py +285 -0
- mesofield/gui/qt_device_adapter.py +109 -0
- mesofield/gui/speedplotter.py +152 -0
- mesofield/gui/theme.py +445 -0
- mesofield/gui/tiff_viewer.py +1050 -0
- mesofield/gui/viewer.py +691 -0
- mesofield/hardware.py +549 -0
- mesofield/playback.py +1298 -0
- mesofield/processing/__init__.py +12 -0
- mesofield/processing/runner.py +237 -0
- mesofield/processors/__init__.py +13 -0
- mesofield/processors/base.py +287 -0
- mesofield/processors/frame_mean.py +19 -0
- mesofield/protocols.py +378 -0
- mesofield/scaffold/__init__.py +34 -0
- mesofield/scaffold/experiment.py +400 -0
- mesofield/scaffold/rigs.py +121 -0
- mesofield/signals.py +85 -0
- mesofield/utils/__init__.py +0 -0
- mesofield/utils/_logger.py +156 -0
- mesofield/utils/retrofit.py +309 -0
- mesofield/utils/utils.py +217 -0
- mesofield-0.3.2b0.dist-info/METADATA +178 -0
- mesofield-0.3.2b0.dist-info/RECORD +111 -0
- mesofield-0.3.2b0.dist-info/WHEEL +5 -0
- mesofield-0.3.2b0.dist-info/entry_points.txt +2 -0
- mesofield-0.3.2b0.dist-info/licenses/LICENSE +21 -0
- mesofield-0.3.2b0.dist-info/top_level.txt +6 -0
- scripts/bench_frame_processor.py +103 -0
mesofield/protocols.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Protocol definitions for hardware instruments and data management.
|
|
3
|
+
|
|
4
|
+
This module defines the core interfaces that standardize behavior across
|
|
5
|
+
the mesofield project, allowing for interoperability between different
|
|
6
|
+
hardware instruments, data producers, and data consumers.
|
|
7
|
+
|
|
8
|
+
Protocol Implementation Notes
|
|
9
|
+
-----------------------------
|
|
10
|
+
When implementing these protocols, there are two approaches:
|
|
11
|
+
|
|
12
|
+
1. **Direct inheritance** (for regular classes without metaclass conflicts):
|
|
13
|
+
|
|
14
|
+
.. code-block:: python
|
|
15
|
+
|
|
16
|
+
class MySensor(DataAcquisitionDevice):
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._init_logger()
|
|
19
|
+
# Implement required methods and attributes
|
|
20
|
+
|
|
21
|
+
2. **Duck typing** (for classes with existing inheritance or metaclass
|
|
22
|
+
conflicts, e.g. ``QThread``):
|
|
23
|
+
|
|
24
|
+
.. code-block:: python
|
|
25
|
+
|
|
26
|
+
class MyQThreadSensor(QThread):
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self._init_logger()
|
|
30
|
+
self.device_type = "sensor"
|
|
31
|
+
self.device_id = "my_sensor"
|
|
32
|
+
|
|
33
|
+
The second approach is necessary for Qt classes (``QObject``, ``QThread``,
|
|
34
|
+
``QWidget``) or any class that already uses a metaclass. Protocol checking
|
|
35
|
+
uses duck typing internally, so both approaches work with our system.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import threading
|
|
39
|
+
from typing import Dict, List, Any, Optional, Protocol, TypeVar, Generic, runtime_checkable
|
|
40
|
+
|
|
41
|
+
from typing import TYPE_CHECKING
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from mesofield.hardware import HardwareManager
|
|
44
|
+
from mesofield.config import ExperimentConfig
|
|
45
|
+
|
|
46
|
+
T = TypeVar('T')
|
|
47
|
+
|
|
48
|
+
# These are the Protocol definitions - they are useful for static type checking
|
|
49
|
+
# and documentation, but should not be used for inheritance with classes that
|
|
50
|
+
# already have a metaclass (like QThread)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Procedure(Protocol):
|
|
54
|
+
"""Protocol defining the standard interface for experiment procedures."""
|
|
55
|
+
|
|
56
|
+
protocol: str
|
|
57
|
+
experimenter: str
|
|
58
|
+
config: "ExperimentConfig"
|
|
59
|
+
data_dir: str
|
|
60
|
+
|
|
61
|
+
def initialize_hardware(self) -> None:
|
|
62
|
+
"""Setup the experiment procedure.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def setup_configuration(self, json_config: Optional[str]) -> None:
|
|
68
|
+
"""Set up the configuration for the experiment procedure.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
json_config: Path to a JSON configuration file (.json)
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def run(self) -> None:
|
|
76
|
+
"""Run the experiment procedure."""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def save_data(self) -> None:
|
|
80
|
+
"""Save data from the experiment."""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
def cleanup(self) -> None:
|
|
84
|
+
"""Clean up after the experiment procedure."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
@runtime_checkable
|
|
88
|
+
class HardwareDevice(Protocol):
|
|
89
|
+
"""Protocol defining the standard interface for all hardware devices.
|
|
90
|
+
|
|
91
|
+
Lifecycle: ``initialize`` -> ``arm`` -> ``start`` -> ``stop`` -> ``shutdown``.
|
|
92
|
+
Every device exposes ``self.signals`` (a :class:`mesofield.signals.DeviceSignals`)
|
|
93
|
+
carrying ``started``, ``finished``, and ``data`` emitters.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
device_type: str
|
|
97
|
+
device_id: str
|
|
98
|
+
signals: Any # DeviceSignals; typed as Any to avoid circular import
|
|
99
|
+
|
|
100
|
+
def initialize(self) -> bool:
|
|
101
|
+
"""One-time setup (open ports, load configs). Idempotent."""
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
def arm(self, config: "ExperimentConfig") -> None:
|
|
105
|
+
"""Per-run preparation (writers, output paths, sequence build).
|
|
106
|
+
|
|
107
|
+
Called by ``HardwareManager.arm_all`` immediately before
|
|
108
|
+
``start_all``. Devices without per-run prep may no-op.
|
|
109
|
+
"""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
def stop(self):
|
|
113
|
+
"""Stop the device after a run."""
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
def shutdown(self) -> None:
|
|
117
|
+
"""Close and clean up resources."""
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
def status(self) -> Dict[str, Any]:
|
|
121
|
+
"""Get the current status of the device."""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def metadata(self) -> Dict[str, Any]:
|
|
126
|
+
"""Return metadata about the hardware."""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@runtime_checkable
|
|
132
|
+
class DataProducer(HardwareDevice, Protocol):
|
|
133
|
+
"""Protocol for hardware that produces data streamed to the DataQueue."""
|
|
134
|
+
|
|
135
|
+
sampling_rate: float # in Hz
|
|
136
|
+
data_type: str
|
|
137
|
+
file_type: str
|
|
138
|
+
bids_type: Optional[str] = None
|
|
139
|
+
is_active: bool
|
|
140
|
+
output_path: str
|
|
141
|
+
metadata_path: Optional[str] = None
|
|
142
|
+
|
|
143
|
+
def start(self) -> bool:
|
|
144
|
+
"""Start data acquisition. Should emit ``signals.started``."""
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
def stop(self) -> bool:
|
|
148
|
+
"""Stop data acquisition. Should emit ``signals.finished``."""
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
def save_data(self, path: Optional[str] = None):
|
|
152
|
+
"""Persist the captured data."""
|
|
153
|
+
...
|
|
154
|
+
|
|
155
|
+
def get_data(self) -> Optional[Any]:
|
|
156
|
+
"""Return the latest data."""
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@runtime_checkable
|
|
161
|
+
class FrameProcessor(Protocol):
|
|
162
|
+
"""Protocol for optional real-time per-frame consumers.
|
|
163
|
+
|
|
164
|
+
A FrameProcessor subscribes to a :class:`DataProducer` camera's
|
|
165
|
+
``signals.frame`` (carrying ``(img, idx, device_ts)``) and emits a
|
|
166
|
+
scalar result on its own ``signals.data`` and on a Qt-compatible
|
|
167
|
+
``valueUpdated(time, value)`` signal. See
|
|
168
|
+
:mod:`mesofield.processors` for the threaded reference base class.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
name: str
|
|
172
|
+
data_type: str
|
|
173
|
+
sampling_rate: float
|
|
174
|
+
|
|
175
|
+
def attach(self, camera: "DataProducer") -> None:
|
|
176
|
+
"""Subscribe to ``camera.signals.frame`` and start processing."""
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
def detach(self) -> None:
|
|
180
|
+
"""Disconnect and stop the worker."""
|
|
181
|
+
...
|
|
182
|
+
|
|
183
|
+
def compute(self, img: Any, idx: Any, ts: Any) -> Optional[float]:
|
|
184
|
+
"""Return a scalar for this frame, or ``None`` to skip."""
|
|
185
|
+
...
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@runtime_checkable
|
|
189
|
+
class StimulusDevice(HardwareDevice, Protocol):
|
|
190
|
+
"""Protocol for stimulus-presentation devices (e.g. PsychoPy).
|
|
191
|
+
|
|
192
|
+
Like :class:`HardwareDevice` but explicitly *not* a data producer:
|
|
193
|
+
consumers should not expect ``data`` signal emissions and should not
|
|
194
|
+
call ``save_data``/``get_data``.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def start(self) -> bool:
|
|
198
|
+
"""Begin stimulus presentation."""
|
|
199
|
+
...
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@runtime_checkable
|
|
205
|
+
class DataConsumer(Protocol):
|
|
206
|
+
"""Protocol defining the interface for data-consuming components."""
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def name(self) -> str:
|
|
210
|
+
"""Return the name of the data consumer."""
|
|
211
|
+
...
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def get_supported_data_types(self) -> List[str]:
|
|
215
|
+
"""Return the types of data this consumer can process."""
|
|
216
|
+
...
|
|
217
|
+
|
|
218
|
+
def process_data(self, data: Any, metadata: Dict[str, Any]) -> bool:
|
|
219
|
+
"""Process data with metadata.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
data: The data to process.
|
|
223
|
+
metadata: Metadata about the data, including source, timestamp, etc.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
bool: True if data was processed successfully, False otherwise.
|
|
227
|
+
"""
|
|
228
|
+
...
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
# Threading mixins
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
class ThreadedHardwareDevice:
|
|
236
|
+
"""
|
|
237
|
+
Mixin for implementing the HardwareDevice protocol with Python's threading.
|
|
238
|
+
|
|
239
|
+
This mixin provides the basic structure for a hardware device that uses
|
|
240
|
+
Python's threading module. It handles the thread creation, starting, and stopping.
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
.. code-block:: python
|
|
244
|
+
|
|
245
|
+
class MySensor(ThreadedHardwareDevice):
|
|
246
|
+
device_type = "sensor"
|
|
247
|
+
device_id = "my_sensor"
|
|
248
|
+
|
|
249
|
+
def __init__(self, config=None):
|
|
250
|
+
super().__init__()
|
|
251
|
+
self.config = config or {}
|
|
252
|
+
|
|
253
|
+
def initialize(self):
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
def _run(self):
|
|
257
|
+
while not self._stop_event.is_set():
|
|
258
|
+
# Do work
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
def get_status(self):
|
|
262
|
+
return {"active": not self._stop_event.is_set()}
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def __init__(self):
|
|
266
|
+
self._thread = None
|
|
267
|
+
self._stop_event = threading.Event()
|
|
268
|
+
self._active = False
|
|
269
|
+
|
|
270
|
+
def start(self) -> bool:
|
|
271
|
+
"""Start the device thread."""
|
|
272
|
+
if self._active:
|
|
273
|
+
return True
|
|
274
|
+
|
|
275
|
+
self._stop_event.clear()
|
|
276
|
+
self._thread = threading.Thread(target=self._run)
|
|
277
|
+
self._thread.daemon = True
|
|
278
|
+
self._thread.start()
|
|
279
|
+
self._active = True
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
def stop(self) -> bool:
|
|
283
|
+
"""Stop the device thread."""
|
|
284
|
+
if not self._active:
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
self._stop_event.set()
|
|
288
|
+
if self._thread and self._thread.is_alive():
|
|
289
|
+
self._thread.join(timeout=1.0)
|
|
290
|
+
self._active = False
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
def close(self) -> None:
|
|
294
|
+
"""Close the device and clean up resources."""
|
|
295
|
+
self.stop()
|
|
296
|
+
|
|
297
|
+
def _run(self) -> None:
|
|
298
|
+
"""
|
|
299
|
+
Main thread method to be overridden by subclasses.
|
|
300
|
+
|
|
301
|
+
This method runs in a separate thread when start() is called.
|
|
302
|
+
It should check self._stop_event periodically and exit if it's set.
|
|
303
|
+
"""
|
|
304
|
+
raise NotImplementedError("Subclasses must implement _run()")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class AsyncioHardwareDevice:
|
|
308
|
+
"""
|
|
309
|
+
Mixin for implementing the HardwareDevice protocol with asyncio.
|
|
310
|
+
|
|
311
|
+
This mixin provides the basic structure for a hardware device that uses
|
|
312
|
+
Python's asyncio module. It handles the task creation, starting, and cancellation.
|
|
313
|
+
|
|
314
|
+
Example:
|
|
315
|
+
.. code-block:: python
|
|
316
|
+
|
|
317
|
+
class MySensor(AsyncioHardwareDevice):
|
|
318
|
+
device_type = "sensor"
|
|
319
|
+
device_id = "my_sensor"
|
|
320
|
+
|
|
321
|
+
def __init__(self, loop=None, config=None):
|
|
322
|
+
super().__init__(loop)
|
|
323
|
+
self.config = config or {}
|
|
324
|
+
|
|
325
|
+
def initialize(self):
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
async def _run(self):
|
|
329
|
+
while True:
|
|
330
|
+
if self._should_stop():
|
|
331
|
+
break
|
|
332
|
+
await asyncio.sleep(0.01)
|
|
333
|
+
|
|
334
|
+
def get_status(self):
|
|
335
|
+
return {"active": self._task is not None and not self._task.done()}
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
def __init__(self, loop=None):
|
|
339
|
+
import asyncio
|
|
340
|
+
self._loop = loop or asyncio.get_event_loop()
|
|
341
|
+
self._task = None
|
|
342
|
+
self._stop_requested = False
|
|
343
|
+
|
|
344
|
+
def start(self) -> bool:
|
|
345
|
+
"""Start the device task."""
|
|
346
|
+
import asyncio
|
|
347
|
+
if self._task is not None and not self._task.done():
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
self._stop_requested = False
|
|
351
|
+
self._task = asyncio.create_task(self._run())
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
def stop(self) -> bool:
|
|
355
|
+
"""Stop the device task."""
|
|
356
|
+
if self._task is None or self._task.done():
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
self._stop_requested = True
|
|
360
|
+
self._task.cancel()
|
|
361
|
+
return True
|
|
362
|
+
|
|
363
|
+
def close(self) -> None:
|
|
364
|
+
"""Close the device and clean up resources."""
|
|
365
|
+
self.stop()
|
|
366
|
+
|
|
367
|
+
def _should_stop(self) -> bool:
|
|
368
|
+
"""Check if the task should stop."""
|
|
369
|
+
return self._stop_requested
|
|
370
|
+
|
|
371
|
+
async def _run(self) -> None:
|
|
372
|
+
"""
|
|
373
|
+
Main coroutine to be overridden by subclasses.
|
|
374
|
+
|
|
375
|
+
This coroutine runs as a task when start() is called.
|
|
376
|
+
It should check self._should_stop() periodically and exit if it returns True.
|
|
377
|
+
"""
|
|
378
|
+
raise NotImplementedError("Subclasses must implement _run()")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Scaffolding for new experiments and machine-level rig configurations.
|
|
2
|
+
|
|
3
|
+
Both halves are "get a machine/experiment ready to use" concerns:
|
|
4
|
+
|
|
5
|
+
- :func:`scaffold_experiment` generates a runnable experiment directory.
|
|
6
|
+
- :mod:`mesofield.scaffold.rigs` keeps a per-machine store of canonical
|
|
7
|
+
``hardware.yaml`` files that ``mesofield init`` copies into new experiments.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from mesofield.scaffold.experiment import (
|
|
11
|
+
scaffold_experiment,
|
|
12
|
+
hardware_yaml_template,
|
|
13
|
+
)
|
|
14
|
+
from mesofield.scaffold.rigs import (
|
|
15
|
+
rigs_dir,
|
|
16
|
+
list_rigs,
|
|
17
|
+
rig_path,
|
|
18
|
+
rig_devices,
|
|
19
|
+
add_rig,
|
|
20
|
+
new_rig,
|
|
21
|
+
remove_rig,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"scaffold_experiment",
|
|
26
|
+
"hardware_yaml_template",
|
|
27
|
+
"rigs_dir",
|
|
28
|
+
"list_rigs",
|
|
29
|
+
"rig_path",
|
|
30
|
+
"rig_devices",
|
|
31
|
+
"add_rig",
|
|
32
|
+
"new_rig",
|
|
33
|
+
"remove_rig",
|
|
34
|
+
]
|