sl-shared-assets 1.0.0__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.

Potentially problematic release.


This version of sl-shared-assets might be problematic. Click here for more details.

Files changed (36) hide show
  1. sl_shared_assets/__init__.py +80 -0
  2. sl_shared_assets/__init__.pyi +73 -0
  3. sl_shared_assets/cli.py +384 -0
  4. sl_shared_assets/cli.pyi +94 -0
  5. sl_shared_assets/data_classes/__init__.py +66 -0
  6. sl_shared_assets/data_classes/__init__.pyi +61 -0
  7. sl_shared_assets/data_classes/configuration_data.py +479 -0
  8. sl_shared_assets/data_classes/configuration_data.pyi +199 -0
  9. sl_shared_assets/data_classes/runtime_data.py +251 -0
  10. sl_shared_assets/data_classes/runtime_data.pyi +145 -0
  11. sl_shared_assets/data_classes/session_data.py +625 -0
  12. sl_shared_assets/data_classes/session_data.pyi +252 -0
  13. sl_shared_assets/data_classes/surgery_data.py +152 -0
  14. sl_shared_assets/data_classes/surgery_data.pyi +89 -0
  15. sl_shared_assets/py.typed +0 -0
  16. sl_shared_assets/server/__init__.py +8 -0
  17. sl_shared_assets/server/__init__.pyi +8 -0
  18. sl_shared_assets/server/job.py +140 -0
  19. sl_shared_assets/server/job.pyi +94 -0
  20. sl_shared_assets/server/server.py +214 -0
  21. sl_shared_assets/server/server.pyi +95 -0
  22. sl_shared_assets/tools/__init__.py +15 -0
  23. sl_shared_assets/tools/__init__.pyi +15 -0
  24. sl_shared_assets/tools/ascension_tools.py +277 -0
  25. sl_shared_assets/tools/ascension_tools.pyi +68 -0
  26. sl_shared_assets/tools/packaging_tools.py +148 -0
  27. sl_shared_assets/tools/packaging_tools.pyi +56 -0
  28. sl_shared_assets/tools/project_management_tools.py +201 -0
  29. sl_shared_assets/tools/project_management_tools.pyi +54 -0
  30. sl_shared_assets/tools/transfer_tools.py +119 -0
  31. sl_shared_assets/tools/transfer_tools.pyi +53 -0
  32. sl_shared_assets-1.0.0.dist-info/METADATA +869 -0
  33. sl_shared_assets-1.0.0.dist-info/RECORD +36 -0
  34. sl_shared_assets-1.0.0.dist-info/WHEEL +4 -0
  35. sl_shared_assets-1.0.0.dist-info/entry_points.txt +8 -0
  36. sl_shared_assets-1.0.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,66 @@
1
+ """This package provides the classes used to store data acquired at various stages of the data workflow and to
2
+ configure various pipelines used in the Sun lab. These classes are used across all stages of data acquisition,
3
+ preprocessing, and processing in the lab that run on multiple machines (PCs). Many classes in this package are designed
4
+ to be saved to disk as .yaml files and restored from the .yaml files as needed."""
5
+
6
+ from .runtime_data import (
7
+ ZaberPositions,
8
+ MesoscopePositions,
9
+ RunTrainingDescriptor,
10
+ LickTrainingDescriptor,
11
+ MesoscopeHardwareState,
12
+ MesoscopeExperimentDescriptor,
13
+ )
14
+ from .session_data import (
15
+ RawData,
16
+ SessionData,
17
+ ProcessedData,
18
+ ProjectConfiguration,
19
+ )
20
+ from .surgery_data import (
21
+ DrugData,
22
+ ImplantData,
23
+ SubjectData,
24
+ SurgeryData,
25
+ InjectionData,
26
+ ProcedureData,
27
+ )
28
+ from .configuration_data import (
29
+ MesoscopePaths,
30
+ ExperimentState,
31
+ MesoscopeCameras,
32
+ MesoscopeMicroControllers,
33
+ MesoscopeAdditionalFirmware,
34
+ MesoscopeSystemConfiguration,
35
+ MesoscopeExperimentConfiguration,
36
+ get_system_configuration_data,
37
+ set_system_configuration_file,
38
+ )
39
+
40
+ __all__ = [
41
+ "DrugData",
42
+ "ImplantData",
43
+ "SessionData",
44
+ "RawData",
45
+ "ProcessedData",
46
+ "SubjectData",
47
+ "SurgeryData",
48
+ "InjectionData",
49
+ "ProcedureData",
50
+ "ZaberPositions",
51
+ "ExperimentState",
52
+ "MesoscopePositions",
53
+ "ProjectConfiguration",
54
+ "MesoscopeHardwareState",
55
+ "RunTrainingDescriptor",
56
+ "LickTrainingDescriptor",
57
+ "MesoscopeExperimentConfiguration",
58
+ "MesoscopeExperimentDescriptor",
59
+ "MesoscopeSystemConfiguration",
60
+ "set_system_configuration_file",
61
+ "get_system_configuration_data",
62
+ "MesoscopePaths",
63
+ "MesoscopeCameras",
64
+ "MesoscopeMicroControllers",
65
+ "MesoscopeAdditionalFirmware",
66
+ ]
@@ -0,0 +1,61 @@
1
+ from .runtime_data import (
2
+ ZaberPositions as ZaberPositions,
3
+ MesoscopePositions as MesoscopePositions,
4
+ RunTrainingDescriptor as RunTrainingDescriptor,
5
+ LickTrainingDescriptor as LickTrainingDescriptor,
6
+ MesoscopeHardwareState as MesoscopeHardwareState,
7
+ MesoscopeExperimentDescriptor as MesoscopeExperimentDescriptor,
8
+ )
9
+ from .session_data import (
10
+ RawData as RawData,
11
+ SessionData as SessionData,
12
+ ProcessedData as ProcessedData,
13
+ ProjectConfiguration as ProjectConfiguration,
14
+ )
15
+ from .surgery_data import (
16
+ DrugData as DrugData,
17
+ ImplantData as ImplantData,
18
+ SubjectData as SubjectData,
19
+ SurgeryData as SurgeryData,
20
+ InjectionData as InjectionData,
21
+ ProcedureData as ProcedureData,
22
+ )
23
+ from .configuration_data import (
24
+ MesoscopePaths as MesoscopePaths,
25
+ ExperimentState as ExperimentState,
26
+ MesoscopeCameras as MesoscopeCameras,
27
+ MesoscopeMicroControllers as MesoscopeMicroControllers,
28
+ MesoscopeAdditionalFirmware as MesoscopeAdditionalFirmware,
29
+ MesoscopeSystemConfiguration as MesoscopeSystemConfiguration,
30
+ MesoscopeExperimentConfiguration as MesoscopeExperimentConfiguration,
31
+ get_system_configuration_data as get_system_configuration_data,
32
+ set_system_configuration_file as set_system_configuration_file,
33
+ )
34
+
35
+ __all__ = [
36
+ "DrugData",
37
+ "ImplantData",
38
+ "SessionData",
39
+ "RawData",
40
+ "ProcessedData",
41
+ "SubjectData",
42
+ "SurgeryData",
43
+ "InjectionData",
44
+ "ProcedureData",
45
+ "ZaberPositions",
46
+ "ExperimentState",
47
+ "MesoscopePositions",
48
+ "ProjectConfiguration",
49
+ "MesoscopeHardwareState",
50
+ "RunTrainingDescriptor",
51
+ "LickTrainingDescriptor",
52
+ "MesoscopeExperimentConfiguration",
53
+ "MesoscopeExperimentDescriptor",
54
+ "MesoscopeSystemConfiguration",
55
+ "set_system_configuration_file",
56
+ "get_system_configuration_data",
57
+ "MesoscopePaths",
58
+ "MesoscopeCameras",
59
+ "MesoscopeMicroControllers",
60
+ "MesoscopeAdditionalFirmware",
61
+ ]
@@ -0,0 +1,479 @@
1
+ """This module provides classes used to configure data acquisition and processing runtimes in the Sun lab. All lab
2
+ projects use classes from this module to configure experiment runtimes and determine how to interact with the
3
+ particular data acquisition and runtime management system (hardware) they run on."""
4
+
5
+ import copy
6
+ from pathlib import Path
7
+ from dataclasses import field, dataclass
8
+
9
+ import appdirs
10
+ from ataraxis_base_utilities import LogLevel, console, ensure_directory_exists
11
+ from ataraxis_data_structures import YamlConfig
12
+
13
+
14
+ @dataclass()
15
+ class ExperimentState:
16
+ """Encapsulates the information used to set and maintain the desired experiment and system state.
17
+
18
+ Broadly, each experiment runtime can be conceptualized as a two state-system. The first state is that of the
19
+ experimental task, which reflects the behavior goal, the rules for achieving the goal, and the reward for
20
+ achieving the goal. The second state is that of the data acquisition and experiment control system, which is a
21
+ snapshot of all hardware module states that make up the system that acquires the data and controls the task
22
+ environment. Overall, experiment state is about 'what the animal is doing', while the system state is about
23
+ 'what the hardware is doing'.
24
+
25
+ Note:
26
+ This class is acquisition-system-agnostic. It can be used to define the ExperimentConfiguration class for any
27
+ valid data acquisition system.
28
+ """
29
+
30
+ experiment_state_code: int
31
+ """The integer code of the experiment state. Experiment states do not have a predefined meaning, Instead, each
32
+ project is expected to define and follow its own experiment state code mapping. Typically, the experiment state
33
+ code is used to denote major experiment stages, such as 'baseline', 'task', 'cooldown', etc. Note, the same
34
+ experiment state code can be used by multiple sequential ExperimentState instances to change the system states
35
+ while maintaining the same experiment state."""
36
+ system_state_code: int
37
+ """One of the supported system state-codes. Note, the meaning of each system state code depends on the specific
38
+ data acquisition and experiment control system used by the project. For example, projects using the 'mesoscope-vr'
39
+ system currently support two system state codes: REST (1) and RUN (2)."""
40
+ state_duration_s: float
41
+ """The time, in seconds, to maintain the current combination of the experiment and system states."""
42
+
43
+
44
+ # noinspection PyArgumentList
45
+ @dataclass()
46
+ class MesoscopeExperimentConfiguration(YamlConfig):
47
+ """Stores the configuration of a single experiment runtime that uses the Mesoscope_VR data acquisition system.
48
+
49
+ Primarily, this includes the sequence of experiment and system states that defines the flow of the experiment
50
+ runtime. During runtime, the main runtime control function traverses the sequence of states stored in this class
51
+ instance start-to-end in the exact order specified by the user. Together with custom Unity projects that define
52
+ the task logic (how the system responds to animal interactions with the VR system) this class allows flexibly
53
+ implementing a wide range of experiments using the Mesoscope-VR system.
54
+
55
+ Each project should define one or more experiment configurations and save them as .yaml files inside the project
56
+ 'configuration' folder. The name for each configuration file is defined by the user and is used to identify and load
57
+ the experiment configuration when 'sl-experiment' CLI command exposed by the sl-experiment library is executed.
58
+
59
+ Notes:
60
+ This class is designed exclusively for the Mesoscope-VR system. Any other system needs to define a separate
61
+ ExperimentConfiguration class to specify its experiment runtimes and additional data.
62
+ """
63
+
64
+ cue_map: dict[int, float] = field(default_factory=lambda: {0: 30.0, 1: 30.0, 2: 30.0, 3: 30.0, 4: 30.0})
65
+ """A dictionary that maps each integer-code associated with a wall cue used in the Virtual Reality experiment
66
+ environment to its length in real-world centimeters. It is used to map each VR cue to the distance the animal needs
67
+ to travel to fully traverse the wall cue region from start to end."""
68
+ experiment_states: dict[str, ExperimentState] = field(
69
+ default_factory=lambda: {
70
+ "baseline": ExperimentState(experiment_state_code=1, system_state_code=1, state_duration_s=30),
71
+ "experiment": ExperimentState(experiment_state_code=2, system_state_code=2, state_duration_s=120),
72
+ "cooldown": ExperimentState(experiment_state_code=3, system_state_code=1, state_duration_s=15),
73
+ }
74
+ )
75
+ """A dictionary that uses human-readable state-names as keys and ExperimentState instances as values. Each
76
+ ExperimentState instance represents a phase of the experiment."""
77
+
78
+
79
+ @dataclass()
80
+ class MesoscopePaths:
81
+ """Stores the filesystem configuration parameters for the Mesoscope-VR data acquisition system."""
82
+
83
+ server_credentials_path: Path = Path("/media/Data/Experiments/server_credentials.yaml")
84
+ """
85
+ The path to the locally stored .YAML file that contains the credentials for accessing the BioHPC server machine.
86
+ While the filesystem of the server machine should already be mounted to the local machine via SMB or equivalent
87
+ protocol, this data is used to establish SSH connection to the server and start newly acquired data processing
88
+ after it is transferred to the server. This allows data acquisition, preprocessing, and processing to be controlled
89
+ by the same runtime and prevents unprocessed data from piling up on the server.
90
+ """
91
+ google_credentials_path: Path = Path("/media/Data/Experiments/sl-surgery-log-0f651e492767.json")
92
+ """
93
+ The path to the locally stored .JSON file that contains the service account credentials used to read and write
94
+ Google Sheet data. This is used to access and work with various Google Sheet files used by Sun lab projects,
95
+ eliminating the need to manually synchronize the data in various Google sheets and other data files.
96
+ """
97
+ root_directory: Path = Path("/media/Data/Experiments")
98
+ """The absolute path to the directory where all projects are stored on the local host-machine (VRPC)."""
99
+ server_storage_directory: Path = Path("/home/cybermouse/server/storage/sun_data")
100
+ """The absolute path to the directory where the raw data from all projects is stored on the BioHPC server.
101
+ This directory should be locally accessible (mounted) using a network sharing protocol, such as SMB."""
102
+ server_working_directory: Path = Path("/home/cybermouse/server/workdir/sun_data")
103
+ """The absolute path to the directory where the processed data from all projects is stored on the BioHPC
104
+ server. This directory should be locally accessible (mounted) using a network sharing protocol, such as SMB."""
105
+ nas_directory: Path = Path("/home/cybermouse/nas/rawdata")
106
+ """The absolute path to the directory where the raw data from all projects is stored on the Synology NAS.
107
+ This directory should be locally accessible (mounted) using a network sharing protocol, such as SMB."""
108
+ mesoscope_directory: Path = Path("/home/cybermouse/scanimage/mesodata")
109
+ """The absolute path to the root ScanImagePC (mesoscope-connected PC) directory where all mesoscope-acquired data
110
+ is aggregated during acquisition runtime. This directory should be locally accessible (mounted) using a network
111
+ sharing protocol, such as SMB."""
112
+ harvesters_cti_path: Path = Path("/opt/mvIMPACT_Acquire/lib/x86_64/mvGenTLProducer.cti")
113
+ """The path to the GeniCam CTI file used to connect to Harvesters-managed cameras."""
114
+ server_processed_data_root: Path = Path("/workdir/sun_data")
115
+ """The absolute path to the BioHPC server directory used to store the processed data from all Sun lab projects.
116
+ This path is relative to the server root and is only used when submitting remote jobs to the server."""
117
+ server_raw_data_root: Path = Path("/storage/sun_data")
118
+ """The absolute path to the BioHPC server directory used to store the raw data from all Sun lab projects.
119
+ This path is relative to the server root and is only used when submitting remote jobs to the server."""
120
+
121
+
122
+ @dataclass()
123
+ class MesoscopeCameras:
124
+ """Stores the configuration parameters for the cameras used by the Mesoscope-VR system to record behavior videos."""
125
+
126
+ face_camera_index: int = 0
127
+ """The index of the face camera in the list of all available Harvester-managed cameras."""
128
+ left_camera_index: int = 0
129
+ """The index of the left body camera (from animal's perspective) in the list of all available OpenCV-managed
130
+ cameras."""
131
+ right_camera_index: int = 2
132
+ """The index of the right body camera (from animal's perspective) in the list of all available OpenCV-managed
133
+ cameras."""
134
+ face_camera_quantization_parameter: int = 15
135
+ """The quantization parameter used by the face camera to encode acquired frames as video files. This controls how
136
+ much data is discarded when encoding each video frame, directly contributing to the encoding speed, resultant video
137
+ file size and video quality."""
138
+ body_camera_quantization_parameter: int = 15
139
+ """SThe quantization parameter used by the left and right body cameras to encode acquired frames as video files.
140
+ See 'face_camera_quantization_parameter' field for more information on what this parameter does."""
141
+ display_face_camera_frames: bool = True
142
+ """Determines whether to display the frames grabbed from the face camera during runtime."""
143
+ display_body_camera_frames: bool = True
144
+ """Determines whether to display the frames grabbed from the left and right body cameras during runtime."""
145
+
146
+
147
+ @dataclass()
148
+ class MesoscopeMicroControllers:
149
+ """Stores the configuration parameters for the microcontrollers used by the Mesoscope-VR system."""
150
+
151
+ actor_port: str = "/dev/ttyACM0"
152
+ """The USB port used by the Actor Microcontroller."""
153
+ sensor_port: str = "/dev/ttyACM1"
154
+ """The USB port used by the Sensor Microcontroller."""
155
+ encoder_port: str = "/dev/ttyACM2"
156
+ """The USB port used by the Encoder Microcontroller."""
157
+ debug: bool = False
158
+ """Determines whether to run the managed acquisition system in the 'debug mode'. This mode should be disabled
159
+ during most runtimes. It is used during initial system calibration and testing and prints a lot of generally
160
+ redundant information into the terminal."""
161
+ mesoscope_ttl_pulse_duration_ms: int = 10
162
+ """The duration of the HIGH phase of all outgoing TTL pulses that target the Mesoscope (enable or disable mesoscope
163
+ frame acquisition), in milliseconds."""
164
+ minimum_break_strength_g_cm: float = 43.2047
165
+ """The minimum torque applied by the running wheel break in gram centimeter. This is the torque the break delivers
166
+ at minimum voltage (break is disabled)."""
167
+ maximum_break_strength_g_cm: float = 1152.1246
168
+ """The maximum torque applied by the running wheel break in gram centimeter. This is the torque the break delivers
169
+ at maximum voltage (break is fully engaged)."""
170
+ wheel_diameter_cm: float = 15.0333
171
+ """The diameter of the running wheel connected to the break and torque sensor, in centimeters."""
172
+ lick_threshold_adc: int = 500
173
+ """The threshold voltage, in raw analog units recorded by a 12-bit Analog-to-Digital-Converter (ADC), interpreted
174
+ as the animal's tongue contacting the sensor. Note, 12-bit ADC only supports values between 0 and 4095, so setting
175
+ the threshold above 4095 will result in no licks being reported to Unity."""
176
+ lick_signal_threshold_adc: int = 300
177
+ """The minimum voltage, in raw analog units recorded by a 12-bit Analog-to-Digital-Converter (ADC), reported to the
178
+ PC as a non-zero value. Voltages below this level are interpreted as 'no-lick' noise and are always pulled to 0."""
179
+ lick_delta_threshold_adc: int = 300
180
+ """The minimum absolute difference in raw analog units recorded by a 12-bit Analog-to-Digital-Converter (ADC) for
181
+ the change to be reported to the PC. This is used to prevent reporting repeated non-lick or lick readouts to the
182
+ PC, conserving communication bandwidth."""
183
+ lick_averaging_pool_size: int = 10
184
+ """The number of lick sensor readouts to average together to produce the final lick sensor readout value."""
185
+ torque_baseline_voltage_adc: int = 2046
186
+ """The voltage level, in raw analog units measured by 3.3v Analog-to-Digital-Converter (ADC) at 12-bit resolution
187
+ after the AD620 amplifier, that corresponds to no (0) torque readout. Usually, for a 3.3v ADC, this would be
188
+ around 2046 (the midpoint, ~1.65 V)."""
189
+ torque_maximum_voltage_adc: int = 2750
190
+ """The voltage level, in raw analog units measured by 3.3v Analog-to-Digital-Converter (ADC) at 12-bit resolution
191
+ after the AD620 amplifier, that corresponds to the absolute maximum torque detectable by the sensor. At most,
192
+ this value can be 4095 (~3.3 V)."""
193
+ torque_sensor_capacity_g_cm: float = 720.0779
194
+ """The maximum torque detectable by the sensor, in grams centimeter (g cm)."""
195
+ torque_report_cw: bool = True
196
+ """Determines whether the sensor should report torque in the Clockwise (CW) direction. This direction corresponds
197
+ to the animal trying to move the wheel backward."""
198
+ torque_report_ccw: bool = True
199
+ """Determines whether the sensor should report torque in the Counter-Clockwise (CCW) direction. This direction
200
+ corresponds to the animal trying to move the wheel forward."""
201
+ torque_signal_threshold_adc: int = 300
202
+ """The minimum voltage, in raw analog units recorded by a 12-bit Analog-to-Digital-Converter (ADC), reported to the
203
+ PC as a non-zero value. Voltages below this level are interpreted as noise and are always pulled to 0."""
204
+ torque_delta_threshold_adc: int = 300
205
+ """The minimum absolute difference in raw analog units recorded by a 12-bit Analog-to-Digital-Converter (ADC) for
206
+ the change to be reported to the PC. This is used to prevent reporting repeated static torque readouts to the
207
+ PC, conserving communication bandwidth."""
208
+ torque_averaging_pool_size: int = 10
209
+ """The number of torque sensor readouts to average together to produce the final torque sensor readout value."""
210
+ wheel_encoder_ppr = 8192
211
+ """The resolution of the managed quadrature encoder, in Pulses Per Revolution (PPR). This is the number of
212
+ quadrature pulses the encoder emits per full 360-degree rotation."""
213
+ wheel_encoder_report_cw: bool = False
214
+ """Determines whether to report encoder rotation in the CW (negative) direction. This corresponds to the animal
215
+ moving backward on the wheel."""
216
+ wheel_encoder_report_ccw: bool = True
217
+ """Determines whether to report encoder rotation in the CCW (positive) direction. This corresponds to the animal
218
+ moving forward on the wheel."""
219
+ wheel_encoder_delta_threshold_pulse: int = 15
220
+ """The minimum difference, in encoder pulse counts, between two encoder readouts for the change to be reported to
221
+ the PC. This is used to prevent reporting idle readouts and filter out sub-threshold noise."""
222
+ wheel_encoder_polling_delay_us = 500
223
+ """The delay, in microseconds, between any two successive encoder state readouts."""
224
+ cm_per_unity_unit = 10.0
225
+ """The length of each Unity 'unit' in real-world centimeters recorded by the running wheel encoder."""
226
+ screen_trigger_pulse_duration_ms: int = 500
227
+ """The duration of the HIGH phase of the TTL pulse used to toggle the VR screens between ON and OFF states."""
228
+ auditory_tone_duration_ms: int = 300
229
+ """The time, in milliseconds, to sound the auditory tone when water rewards are delivered to the animal."""
230
+ valve_calibration_pulse_count: int = 200
231
+ """The number of times to cycle opening and closing (pulsing) the valve during each calibration runtime. This
232
+ determines how many reward deliveries are used at each calibrated time-interval to produce the average dispensed
233
+ water volume readout used to calibrate the valve."""
234
+ sensor_polling_delay_ms: int = 1
235
+ """The delay, in milliseconds, between any two successive readouts of any sensor other than the encoder. Note, the
236
+ encoder uses a dedicated parameter, as the encoder needs to be sampled at a higher frequency than all other sensors.
237
+ """
238
+ valve_calibration_data: dict[int | float, int | float] | tuple[tuple[int | float, int | float], ...] = (
239
+ (15000, 1.75),
240
+ (30000, 3.85),
241
+ (45000, 7.95),
242
+ (60000, 12.65),
243
+ )
244
+ """A tuple of tuples that maps water delivery solenoid valve open times, in microseconds, to the dispensed volume
245
+ of water, in microliters. During training and experiment runtimes, this data is used by the ValveModule to translate
246
+ the requested reward volumes into times the valve needs to be open to deliver the desired volume of water.
247
+ """
248
+
249
+
250
+ @dataclass()
251
+ class MesoscopeAdditionalFirmware:
252
+ """Stores the configuration parameters for all firmware and hardware components not assembled in the Sun lab."""
253
+
254
+ headbar_port: str = "/dev/ttyUSB0"
255
+ """The USB port used by the HeadBar Zaber motor controllers (devices)."""
256
+ lickport_port: str = "/dev/ttyUSB1"
257
+ """The USB port used by the LickPort Zaber motor controllers (devices)."""
258
+ wheel_port: str = "/dev/ttyUSB2"
259
+ """The USB port used by the (running) Wheel Zaber motor controllers (devices)."""
260
+ unity_ip: str = "127.0.0.1"
261
+ """The IP address of the MQTT broker used to communicate with the Unity game engine."""
262
+ unity_port: int = 1883
263
+ """The port number of the MQTT broker used to communicate with the Unity game engine."""
264
+
265
+
266
+ @dataclass()
267
+ class MesoscopeSystemConfiguration(YamlConfig):
268
+ """Stores the hardware and filesystem configuration parameters for the Mesoscope-VR data acquisition system used in
269
+ the Sun lab.
270
+
271
+ This class is specifically designed to encapsulate the configuration parameters for the Mesoscope-VR system. It
272
+ expects the system to be configured according to the specifications available from the sl_experiment repository
273
+ (https://github.com/Sun-Lab-NBB/sl-experiment) and should be used exclusively by the VRPC machine
274
+ (main Mesoscope-VR PC).
275
+
276
+ Notes:
277
+ Each SystemConfiguration class is uniquely tied to a specific hardware configuration used in the lab. This
278
+ class will only work with the Mesoscope-VR system. Any other data acquisition and runtime management system in
279
+ the lab should define its own SystemConfiguration class to specify its own hardware and filesystem configuration
280
+ parameters.
281
+ """
282
+
283
+ name: str = "mesoscope-vr"
284
+ """Stores the descriptive name of the data acquisition system."""
285
+ paths: MesoscopePaths = field(default_factory=MesoscopePaths)
286
+ """Stores the filesystem configuration parameters for the Mesoscope-VR data acquisition system."""
287
+ cameras: MesoscopeCameras = field(default_factory=MesoscopeCameras)
288
+ """Stores the configuration parameters for the cameras used by the Mesoscope-VR system to record behavior videos."""
289
+ microcontrollers: MesoscopeMicroControllers = field(default_factory=MesoscopeMicroControllers)
290
+ """Stores the configuration parameters for the microcontrollers used by the Mesoscope-VR system."""
291
+ additional_firmware: MesoscopeAdditionalFirmware = field(default_factory=MesoscopeAdditionalFirmware)
292
+ """Stores the configuration parameters for all firmware and hardware components not assembled in the Sun lab."""
293
+
294
+ def __post_init__(self) -> None:
295
+ """Ensures that variables converted to different types for storage purposes are always set to expected types
296
+ upon class instantiation."""
297
+
298
+ # Converts all paths loaded as strings to Path objects used inside the library
299
+ self.paths.server_credentials_path = Path(self.paths.server_credentials_path)
300
+ self.paths.google_credentials_path = Path(self.paths.google_credentials_path)
301
+ self.paths.root_directory = Path(self.paths.root_directory)
302
+ self.paths.server_storage_directory = Path(self.paths.server_storage_directory)
303
+ self.paths.server_working_directory = Path(self.paths.server_working_directory)
304
+ self.paths.nas_directory = Path(self.paths.nas_directory)
305
+ self.paths.mesoscope_directory = Path(self.paths.mesoscope_directory)
306
+ self.paths.harvesters_cti_path = Path(self.paths.harvesters_cti_path)
307
+ self.paths.server_processed_data_root = Path(self.paths.server_processed_data_root)
308
+ self.paths.server_raw_data_root = Path(self.paths.server_raw_data_root)
309
+
310
+ # Converts valve_calibration data from dictionary to a tuple of tuples format
311
+ if not isinstance(self.microcontrollers.valve_calibration_data, tuple):
312
+ self.microcontrollers.valve_calibration_data = tuple(
313
+ (k, v) for k, v in self.microcontrollers.valve_calibration_data.items()
314
+ )
315
+
316
+ # Verifies the contents of the valve calibration data loaded from the config file.
317
+ valve_calibration_data = self.microcontrollers.valve_calibration_data
318
+ if not all(
319
+ isinstance(item, tuple)
320
+ and len(item) == 2
321
+ and isinstance(item[0], (int, float))
322
+ and isinstance(item[1], (int, float))
323
+ for item in valve_calibration_data
324
+ ):
325
+ message = (
326
+ f"Unable to initialize the MesoscopeSystemConfiguration class. Expected each item under the "
327
+ f"'valve_calibration_data' field of the Mesoscope-VR acquisition system configuration .yaml file to be "
328
+ f"a tuple of two integer or float values, but instead encountered {valve_calibration_data} with at "
329
+ f"least one incompatible element."
330
+ )
331
+ console.error(message=message, error=TypeError)
332
+
333
+ def save(self, path: Path) -> None:
334
+ """Saves class instance data to disk as a 'mesoscope_system_configuration.yaml' file.
335
+
336
+ This method converts certain class variables to yaml-safe types (for example, Path objects -> strings) and
337
+ saves class data to disk as a .yaml file. The method is intended to be used solely by the
338
+ set_system_configuration_file() function and should not be called from any other context.
339
+
340
+ Args:
341
+ path: The path to the .yaml file to save the data to.
342
+ """
343
+
344
+ # Copies instance data to prevent it from being modified by reference when executing the steps below
345
+ original = copy.deepcopy(self)
346
+
347
+ # Converts all Path objects to strings before dumping the data, as .yaml encoder does not properly recognize
348
+ # Path objects
349
+ original.paths.server_credentials_path = str(original.paths.server_credentials_path) # type: ignore
350
+ original.paths.google_credentials_path = str(original.paths.google_credentials_path) # type: ignore
351
+ original.paths.root_directory = str(original.paths.root_directory) # type: ignore
352
+ original.paths.server_storage_directory = str(original.paths.server_storage_directory) # type: ignore
353
+ original.paths.server_working_directory = str(original.paths.server_working_directory) # type: ignore
354
+ original.paths.nas_directory = str(original.paths.nas_directory) # type: ignore
355
+ original.paths.mesoscope_directory = str(original.paths.mesoscope_directory) # type: ignore
356
+ original.paths.harvesters_cti_path = str(original.paths.harvesters_cti_path) # type: ignore
357
+ original.paths.server_processed_data_root = str(original.paths.server_processed_data_root) # type: ignore
358
+ original.paths.server_raw_data_root = str(original.paths.server_raw_data_root) # type: ignore
359
+
360
+ # Converts valve calibration data into dictionary format
361
+ if isinstance(original.microcontrollers.valve_calibration_data, tuple):
362
+ original.microcontrollers.valve_calibration_data = {
363
+ k: v for k, v in original.microcontrollers.valve_calibration_data
364
+ }
365
+
366
+ # Saves the data to the YAML file
367
+ original.to_yaml(file_path=path)
368
+
369
+
370
+ # A dictionary that maps the file names for supported data acquisition systems to their configuration classes. This
371
+ # dictionary always contains all data acquisition systems used in the lab.
372
+ _supported_configuration_files = {"mesoscope_system_configuration.yaml": MesoscopeSystemConfiguration}
373
+
374
+
375
+ def set_system_configuration_file(path: Path) -> None:
376
+ """Sets the system configuration .yaml file specified by the input path as the default system configuration file for
377
+ the managed machine (PC).
378
+
379
+ This function is used to initially configure or override the existing configuration of any data acquisition system
380
+ used in the lab. The path to the configuration file is stored inside the user's data directory, so that all
381
+ Sun lab libraries can automatically access that information during every runtime. Since the storage directory is
382
+ typically hidden and varies between OSes and machines, this function provides a convenient way for setting that
383
+ path without manually editing the storage cache.
384
+
385
+ Notes:
386
+ If the input path does not point to an existing file, but the file name and extension are correct, the function
387
+ will automatically generate a default SystemConfiguration class instance and save it under the specified path.
388
+
389
+ A data acquisition system can include multiple machines (PCs). However, the configuration file is typically
390
+ only present on the 'main' machine that manages all runtimes.
391
+
392
+ Args:
393
+ path: The path to the new system configuration file to be used by the local data acquisition system (PC).
394
+
395
+ Raises:
396
+ ValueError: If the input path is not a valid system configuration file or does not use a supported data
397
+ acquisition system name.
398
+ """
399
+
400
+ # Prevents setting the path to an invalid file.
401
+ if path.name not in _supported_configuration_files.keys():
402
+ message = (
403
+ f"Unable to set the input path {path} as the default system configuration file path. The input path has "
404
+ f"to point to a configuration file ending with a '.yaml' extension and using one of the supported system "
405
+ f"names: {', '.join(_supported_configuration_files.keys())}."
406
+ )
407
+ console.error(message=message, error=ValueError)
408
+
409
+ # If the configuration file specified by the 'path' does not exist, generates a default SystemConfiguration instance
410
+ # and saves it to the specified path.
411
+ if not path.exists():
412
+ precursor = _supported_configuration_files[path.name]() # Instantiates default class instance
413
+ precursor.save(path=path)
414
+ message = (
415
+ f"The file specified by the input system configuration path {path} does not exist. Generating and saving "
416
+ f"the default system configuration class instance to the specified path."
417
+ )
418
+ console.echo(message=message, level=LogLevel.WARNING)
419
+
420
+ # Resolves the path to the static .txt file used to store the path to the system configuration file
421
+ app_dir = Path(appdirs.user_data_dir(appname="sun_lab_data", appauthor="sun_lab"))
422
+ path_file = app_dir.joinpath("configuration_path.txt")
423
+
424
+ # In case this function is called before the app directory is created, ensures the app directory exists
425
+ ensure_directory_exists(path_file)
426
+
427
+ # Ensures that the input path's directory exists
428
+ ensure_directory_exists(path)
429
+
430
+ # Replaces the contents of the configuration_path.txt file with the provided path
431
+ with open(path_file, "w") as f:
432
+ f.write(str(path))
433
+
434
+
435
+ def get_system_configuration_data() -> MesoscopeSystemConfiguration:
436
+ """Resolves the path to the local system configuration file and loads the system configuration data.
437
+
438
+ This service function is used by all Sun lab data acquisition runtimes to load the system configuration data from
439
+ the shared configuration file. It supports resolving and returning the data for all data acquisition systems used
440
+ in the lab.
441
+
442
+ Returns:
443
+ The initialized SystemConfiguration class instance for the local acquisition system that stores the loaded
444
+ configuration parameters.
445
+
446
+ Raises:
447
+ FileNotFoundError: If the local machine does not have the Sun lab data directory, or the system configuration
448
+ file does not exist.
449
+ """
450
+ # Uses appdirs to locate the user data directory and resolve the path to the configuration file
451
+ app_dir = Path(appdirs.user_data_dir(appname="sun_lab_data", appauthor="sun_lab"))
452
+ path_file = app_dir.joinpath("configuration_path.txt")
453
+
454
+ # If the cache file or the Sun lab data directory do not exist, aborts with an error
455
+ if not path_file.exists():
456
+ message = (
457
+ "Unable to resolve the path to the local system configuration file, as local machine does not have the "
458
+ "Sun lab data directory. Generate the local configuration file and Sun lab data directory by calling the "
459
+ "'sl-create-system-config' CLI command and rerun the command that produced this error."
460
+ )
461
+ console.error(message=message, error=FileNotFoundError)
462
+
463
+ # Once the location of the path storage file is resolved, reads the file path from the file
464
+ with open(path_file, "r") as f:
465
+ configuration_file = Path(f.read().strip())
466
+
467
+ # If the configuration file does not exist, also aborts with an error
468
+ if not configuration_file.exists():
469
+ message = (
470
+ "Unable to resolve the path to the local system configuration file, as the file pointed by the path stored "
471
+ "in Sun lab data directory does not exist. Generate a new local configuration file by calling the "
472
+ "'sl-create-system-config' CLI command and rerun the command that produced this error."
473
+ )
474
+ console.error(message=message, error=FileNotFoundError)
475
+
476
+ # Loads the data stored inside the .yaml file into the class instance that matches the file name and returns the
477
+ # instantiated class to caller
478
+ file_name = configuration_file.name
479
+ return _supported_configuration_files[file_name].from_yaml(file_path=configuration_file) # type: ignore