sl-shared-assets 6.1.1__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.
- sl_shared_assets/__init__.py +120 -0
- sl_shared_assets/command_line_interfaces/__init__.py +3 -0
- sl_shared_assets/command_line_interfaces/configure.py +318 -0
- sl_shared_assets/data_classes/__init__.py +121 -0
- sl_shared_assets/data_classes/configuration_data.py +939 -0
- sl_shared_assets/data_classes/dataset_data.py +385 -0
- sl_shared_assets/data_classes/processing_data.py +385 -0
- sl_shared_assets/data_classes/runtime_data.py +237 -0
- sl_shared_assets/data_classes/session_data.py +400 -0
- sl_shared_assets/data_classes/surgery_data.py +138 -0
- sl_shared_assets/data_transfer/__init__.py +12 -0
- sl_shared_assets/data_transfer/checksum_tools.py +125 -0
- sl_shared_assets/data_transfer/transfer_tools.py +181 -0
- sl_shared_assets/py.typed +0 -0
- sl_shared_assets-6.1.1.dist-info/METADATA +830 -0
- sl_shared_assets-6.1.1.dist-info/RECORD +19 -0
- sl_shared_assets-6.1.1.dist-info/WHEEL +4 -0
- sl_shared_assets-6.1.1.dist-info/entry_points.txt +2 -0
- sl_shared_assets-6.1.1.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""This module provides the assets that maintain the Sun lab analysis dataset data hierarchy across all machines used to
|
|
2
|
+
process and store the data.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dataclasses import field, dataclass
|
|
8
|
+
|
|
9
|
+
import polars as pl
|
|
10
|
+
from ataraxis_base_utilities import console, ensure_directory_exists
|
|
11
|
+
from ataraxis_data_structures import YamlConfig
|
|
12
|
+
|
|
13
|
+
from .session_data import SessionTypes
|
|
14
|
+
from .configuration_data import AcquisitionSystems
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class SessionMetadata:
|
|
19
|
+
"""Encapsulates the identity metadata for a single data acquisition session.
|
|
20
|
+
|
|
21
|
+
This class is used to identify sessions included in an analysis dataset. It provides the minimum information
|
|
22
|
+
necessary to locate and access the session's data within a project's data hierarchy.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
session: str
|
|
26
|
+
"""The unique identifier of the session. Session names follow the format 'YYYY-MM-DD-HH-MM-SS-microseconds' and
|
|
27
|
+
encode the session's acquisition timestamp."""
|
|
28
|
+
animal: str
|
|
29
|
+
"""The unique identifier of the animal that participated in the session."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass()
|
|
33
|
+
class DatasetTrackingData:
|
|
34
|
+
"""Provides the path to the directory that stores the .yaml and .lock files used by ProcessingTracker instances to
|
|
35
|
+
track the runtime status of the dataset forging and multi-day processing pipelines.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
tracking_data_path: Path = Path()
|
|
39
|
+
"""The path to the root directory that stores the dataset's tracking data."""
|
|
40
|
+
|
|
41
|
+
def resolve_paths(self, root_directory_path: Path) -> None:
|
|
42
|
+
"""Resolves all paths managed by the class instance based on the input root directory path.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
root_directory_path: The path to the top-level tracking data directory of the dataset's data hierarchy.
|
|
46
|
+
"""
|
|
47
|
+
self.tracking_data_path = root_directory_path
|
|
48
|
+
|
|
49
|
+
def make_directories(self) -> None:
|
|
50
|
+
"""Ensures that the root directory exists, creating it if missing."""
|
|
51
|
+
ensure_directory_exists(self.tracking_data_path)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass()
|
|
55
|
+
class DatasetSessionData:
|
|
56
|
+
"""Provides the paths and access to the assembled data files for a single session within the dataset.
|
|
57
|
+
|
|
58
|
+
Notes:
|
|
59
|
+
Each session in the dataset has its own directory containing the forged data and metadata feather files, in
|
|
60
|
+
addition to other supplementary data files.
|
|
61
|
+
|
|
62
|
+
As part of loading the data, this class memory-maps the .feather files to Polars dataframes for efficient
|
|
63
|
+
access.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
session_path: Path = Path()
|
|
67
|
+
"""The path to the session's directory within the dataset hierarchy (dataset/animal/session)."""
|
|
68
|
+
data_path: Path = Path()
|
|
69
|
+
"""The path to the data.feather file containing the assembled and time-aligned session-specific dataset."""
|
|
70
|
+
metadata_path: Path = Path()
|
|
71
|
+
"""The path to the metadata.feather file containing session's metadata."""
|
|
72
|
+
data: pl.DataFrame | None = None
|
|
73
|
+
"""The Polars dataframe that stores the session's data memory-mapped from the dataset .feather file."""
|
|
74
|
+
metadata: pl.DataFrame | None = None
|
|
75
|
+
"""The Polars dataframe that stores the session's metadata memory-mapped from the metadata .feather file."""
|
|
76
|
+
|
|
77
|
+
def resolve_paths(self, session_directory: Path) -> None:
|
|
78
|
+
"""Resolves all paths managed by the class instance.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
session_directory: The path to the session's directory within the dataset hierarchy.
|
|
82
|
+
"""
|
|
83
|
+
self.session_path = session_directory
|
|
84
|
+
self.data_path = session_directory.joinpath("data.feather")
|
|
85
|
+
self.metadata_path = session_directory.joinpath("metadata.feather")
|
|
86
|
+
|
|
87
|
+
def make_directories(self) -> None:
|
|
88
|
+
"""Ensures that the session directory exists, creating it if missing."""
|
|
89
|
+
ensure_directory_exists(self.session_path)
|
|
90
|
+
|
|
91
|
+
def load_data(self) -> None:
|
|
92
|
+
"""Loads the session's data by memory-mapping its feather files as Polars dataframes."""
|
|
93
|
+
if self.data_path.exists():
|
|
94
|
+
self.data = pl.read_ipc(source=self.data_path, use_pyarrow=True, memory_map=True, rechunk=True)
|
|
95
|
+
if self.metadata_path.exists():
|
|
96
|
+
self.metadata = pl.read_ipc(source=self.metadata_path, use_pyarrow=True, memory_map=True, rechunk=True)
|
|
97
|
+
|
|
98
|
+
def release_data(self) -> None:
|
|
99
|
+
"""Releases the memory-mapped dataframes by setting them to None."""
|
|
100
|
+
self.data = None
|
|
101
|
+
self.metadata = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class DatasetData(YamlConfig):
|
|
106
|
+
"""Defines the structure and the metadata of an analysis dataset.
|
|
107
|
+
|
|
108
|
+
An analysis dataset aggregates multiple data acquisition sessions of the same type, recorded across different
|
|
109
|
+
animals by the same acquisition system. This class encapsulates the information necessary to access the dataset's
|
|
110
|
+
assembled (forged) data stored on disk and functions as the entry point for all interactions with the dataset.
|
|
111
|
+
|
|
112
|
+
Notes:
|
|
113
|
+
Do not initialize this class directly. Instead, use the create() method when creating new datasets or the
|
|
114
|
+
load() method when accessing data for an existing dataset.
|
|
115
|
+
|
|
116
|
+
Datasets are created using a pre-filtered set of session + animal pairs, typically obtained through the
|
|
117
|
+
session filtering functionality in sl-forgery. The dataset stores only the assembled data, not raw or
|
|
118
|
+
processed data.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
name: str
|
|
122
|
+
"""The unique name of the dataset."""
|
|
123
|
+
project: str
|
|
124
|
+
"""The name of the project from which the dataset's sessions originate."""
|
|
125
|
+
session_type: str | SessionTypes
|
|
126
|
+
"""The type of data acquisition sessions included in the dataset. All sessions in a dataset must be of the
|
|
127
|
+
same type."""
|
|
128
|
+
acquisition_system: str | AcquisitionSystems
|
|
129
|
+
"""The name of the data acquisition system used to acquire all sessions in the dataset."""
|
|
130
|
+
sessions: tuple[SessionMetadata, ...] = field(default_factory=tuple)
|
|
131
|
+
"""Stores the SessionMetadata instances that define the metadata used to identify each session included in the
|
|
132
|
+
dataset."""
|
|
133
|
+
tracking_data: DatasetTrackingData = field(default_factory=DatasetTrackingData)
|
|
134
|
+
"""Defines the dataset's tracking data hierarchy for forging and multi-day processing pipelines."""
|
|
135
|
+
dataset_data_path: Path = field(default_factory=Path)
|
|
136
|
+
"""The path to the dataset.yaml file cached to disk."""
|
|
137
|
+
_session_data_cache: dict[str, DatasetSessionData] = field(default_factory=dict, repr=False)
|
|
138
|
+
"""Stores initialized DatasetSessionData instances for each included session, keyed by 'animal/session'. Use
|
|
139
|
+
the get_session_data() method to access session data."""
|
|
140
|
+
|
|
141
|
+
def __post_init__(self) -> None:
|
|
142
|
+
"""Validates and initializes the dataset configuration."""
|
|
143
|
+
# Ensures enumeration-mapped arguments are stored as proper enumeration types.
|
|
144
|
+
if isinstance(self.session_type, str):
|
|
145
|
+
self.session_type = SessionTypes(self.session_type)
|
|
146
|
+
if isinstance(self.acquisition_system, str):
|
|
147
|
+
self.acquisition_system = AcquisitionSystems(self.acquisition_system)
|
|
148
|
+
|
|
149
|
+
# Ensures that the sessions field is a tuple of SessionMetadata instances.
|
|
150
|
+
if self.sessions and not isinstance(self.sessions[0], SessionMetadata):
|
|
151
|
+
# noinspection PyUnresolvedReferences
|
|
152
|
+
self.sessions = tuple(SessionMetadata(session=s["session"], animal=s["animal"]) for s in self.sessions)
|
|
153
|
+
|
|
154
|
+
# Ensures tracking data instance is properly initialized.
|
|
155
|
+
if not isinstance(self.tracking_data, DatasetTrackingData):
|
|
156
|
+
self.tracking_data = DatasetTrackingData()
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def create(
|
|
160
|
+
cls,
|
|
161
|
+
name: str,
|
|
162
|
+
project: str,
|
|
163
|
+
session_type: SessionTypes | str,
|
|
164
|
+
acquisition_system: AcquisitionSystems | str,
|
|
165
|
+
sessions: tuple[SessionMetadata, ...] | set[SessionMetadata],
|
|
166
|
+
datasets_root: Path,
|
|
167
|
+
) -> DatasetData:
|
|
168
|
+
"""Creates a new analysis dataset and initializes its data structure on disk.
|
|
169
|
+
|
|
170
|
+
Notes:
|
|
171
|
+
To access the data of an already existing dataset, use the load() method.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
name: The unique name for the dataset.
|
|
175
|
+
project: The name of the project from which the dataset's sessions originate.
|
|
176
|
+
session_type: The type of data acquisition sessions included in the dataset.
|
|
177
|
+
acquisition_system: The name of the data acquisition system used to acquire all sessions included in the
|
|
178
|
+
dataset.
|
|
179
|
+
sessions: The set of SessionMetadata instances that define the sessions whose data should be included in
|
|
180
|
+
the dataset.
|
|
181
|
+
datasets_root: The path to the root directory where to create the dataset's hierarchy.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
An initialized DatasetData instance that stores the structure and the metadata of the created dataset.
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
ValueError: If the session_type or acquisition_system is invalid, or if no sessions are provided.
|
|
188
|
+
FileExistsError: If a dataset with the same name already exists.
|
|
189
|
+
"""
|
|
190
|
+
# Validates inputs
|
|
191
|
+
if isinstance(session_type, str):
|
|
192
|
+
session_type = SessionTypes(session_type)
|
|
193
|
+
if isinstance(acquisition_system, str):
|
|
194
|
+
acquisition_system = AcquisitionSystems(acquisition_system)
|
|
195
|
+
|
|
196
|
+
# Converts sessions to tuple if provided as set
|
|
197
|
+
if isinstance(sessions, set):
|
|
198
|
+
sessions = tuple(sessions)
|
|
199
|
+
|
|
200
|
+
if not sessions:
|
|
201
|
+
message = (
|
|
202
|
+
f"Unable to create the '{name}' analysis dataset, as no sessions were provided to the creation method "
|
|
203
|
+
f"via the 'sessions' argument."
|
|
204
|
+
)
|
|
205
|
+
console.error(message=message, error=ValueError)
|
|
206
|
+
raise ValueError(message) # Fallback for mypy
|
|
207
|
+
|
|
208
|
+
# Constructs the dataset root directory path
|
|
209
|
+
dataset_path = datasets_root.joinpath(name)
|
|
210
|
+
|
|
211
|
+
# Prevents overwriting existing datasets
|
|
212
|
+
if dataset_path.exists():
|
|
213
|
+
message = (
|
|
214
|
+
f"Unable to create the '{name}' analysis dataset. A dataset with this name already exists at "
|
|
215
|
+
f"{dataset_path}. Use a different name or delete the existing dataset before creating a new one with "
|
|
216
|
+
f"the same name."
|
|
217
|
+
)
|
|
218
|
+
console.error(message=message, error=FileExistsError)
|
|
219
|
+
raise FileExistsError(message) # Fallback for mypy
|
|
220
|
+
|
|
221
|
+
# Generates the dataset's tracking directory
|
|
222
|
+
tracking_data = DatasetTrackingData()
|
|
223
|
+
tracking_data.resolve_paths(root_directory_path=dataset_path.joinpath("tracking_data"))
|
|
224
|
+
tracking_data.make_directories()
|
|
225
|
+
|
|
226
|
+
# Creates animal/session subdirectories and initializes session data instances
|
|
227
|
+
_session_data_cache: dict[str, DatasetSessionData] = {}
|
|
228
|
+
for session_meta in sessions:
|
|
229
|
+
session_dir = dataset_path.joinpath(session_meta.animal, session_meta.session)
|
|
230
|
+
|
|
231
|
+
session_data = DatasetSessionData()
|
|
232
|
+
session_data.resolve_paths(session_directory=session_dir)
|
|
233
|
+
session_data.make_directories()
|
|
234
|
+
|
|
235
|
+
cache_key = f"{session_meta.animal}/{session_meta.session}"
|
|
236
|
+
_session_data_cache[cache_key] = session_data
|
|
237
|
+
|
|
238
|
+
# Generates the DatasetData instance
|
|
239
|
+
instance = cls(
|
|
240
|
+
name=name,
|
|
241
|
+
project=project,
|
|
242
|
+
session_type=session_type,
|
|
243
|
+
acquisition_system=acquisition_system,
|
|
244
|
+
sessions=sessions,
|
|
245
|
+
tracking_data=tracking_data,
|
|
246
|
+
dataset_data_path=dataset_path.joinpath("dataset.yaml"),
|
|
247
|
+
_session_data_cache=_session_data_cache,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Saves the configured instance data to disk
|
|
251
|
+
instance.save()
|
|
252
|
+
|
|
253
|
+
return instance
|
|
254
|
+
|
|
255
|
+
@classmethod
|
|
256
|
+
def load(cls, dataset_path: Path) -> DatasetData:
|
|
257
|
+
"""Loads the target dataset's data from the specified dataset.yaml file.
|
|
258
|
+
|
|
259
|
+
Notes:
|
|
260
|
+
To create a new dataset, use the create() method.
|
|
261
|
+
|
|
262
|
+
This method memory-maps the data.feather and metadata.feather files for each loaded session as Polars
|
|
263
|
+
dataframes.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
dataset_path: The path to the directory where to search for the dataset.yaml file. Typically, this
|
|
267
|
+
is the path to the root dataset directory.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
An initialized DatasetData instance that stores the loaded dataset's data.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
FileNotFoundError: If multiple or no 'dataset.yaml' file instances are found under the input directory.
|
|
274
|
+
"""
|
|
275
|
+
# Locates the dataset.yaml file
|
|
276
|
+
dataset_data_files = list(dataset_path.rglob("dataset.yaml"))
|
|
277
|
+
if len(dataset_data_files) != 1:
|
|
278
|
+
message = (
|
|
279
|
+
f"Unable to load the target dataset's data. Expected a single dataset.yaml file to be located "
|
|
280
|
+
f"under the directory tree specified by the input path: {dataset_path}. Instead, encountered "
|
|
281
|
+
f"{len(dataset_data_files)} candidate files. This indicates that the input path does not point to a "
|
|
282
|
+
f"valid dataset data hierarchy."
|
|
283
|
+
)
|
|
284
|
+
console.error(message=message, error=FileNotFoundError)
|
|
285
|
+
raise FileNotFoundError(message) # Fallback for mypy
|
|
286
|
+
|
|
287
|
+
# Loads the dataset's data from the .yaml file
|
|
288
|
+
dataset_data_path = dataset_data_files.pop()
|
|
289
|
+
instance: DatasetData = cls.from_yaml(file_path=dataset_data_path)
|
|
290
|
+
|
|
291
|
+
# Initializes the session data cache if it was set to None during save().
|
|
292
|
+
if instance._session_data_cache is None:
|
|
293
|
+
instance._session_data_cache = {}
|
|
294
|
+
|
|
295
|
+
# Resolves the dataset root directory (parent of the YAML file)
|
|
296
|
+
local_root = dataset_data_path.parent
|
|
297
|
+
|
|
298
|
+
# Resolves tracking data paths
|
|
299
|
+
instance.tracking_data.resolve_paths(root_directory_path=local_root.joinpath("tracking_data"))
|
|
300
|
+
instance.dataset_data_path = dataset_data_path
|
|
301
|
+
|
|
302
|
+
# Resolves session data paths and loads data
|
|
303
|
+
for session_meta in instance.sessions:
|
|
304
|
+
session_dir = local_root.joinpath(session_meta.animal, session_meta.session)
|
|
305
|
+
|
|
306
|
+
session_data = DatasetSessionData()
|
|
307
|
+
session_data.resolve_paths(session_directory=session_dir)
|
|
308
|
+
session_data.load_data()
|
|
309
|
+
|
|
310
|
+
cache_key = f"{session_meta.animal}/{session_meta.session}"
|
|
311
|
+
instance._session_data_cache[cache_key] = session_data
|
|
312
|
+
|
|
313
|
+
return instance
|
|
314
|
+
|
|
315
|
+
def save(self) -> None:
|
|
316
|
+
"""Caches the instance's data to the dataset's root directory as a 'dataset.yaml' file.
|
|
317
|
+
|
|
318
|
+
Notes:
|
|
319
|
+
This method releases all memory-mapped dataframes before saving and resets them to None.
|
|
320
|
+
"""
|
|
321
|
+
# Releases all memory-mapped dataframes
|
|
322
|
+
for session_data in self._session_data_cache.values():
|
|
323
|
+
session_data.release_data()
|
|
324
|
+
|
|
325
|
+
# Generates a copy to avoid modifying the instance
|
|
326
|
+
origin = copy.deepcopy(self)
|
|
327
|
+
|
|
328
|
+
# Resets path fields and cache to None before saving
|
|
329
|
+
origin.tracking_data = None # type: ignore[assignment]
|
|
330
|
+
origin.dataset_data_path = None # type: ignore[assignment]
|
|
331
|
+
origin._session_data_cache = None # type: ignore[assignment] #noqa: SLF001
|
|
332
|
+
|
|
333
|
+
# Converts StrEnum instances to strings for YAML serialization
|
|
334
|
+
origin.session_type = str(origin.session_type)
|
|
335
|
+
origin.acquisition_system = str(origin.acquisition_system)
|
|
336
|
+
|
|
337
|
+
# Converts SessionMetadata tuples to list of dicts for YAML serialization
|
|
338
|
+
origin.sessions = [ # type: ignore[assignment]
|
|
339
|
+
{"session": s.session, "animal": s.animal} for s in origin.sessions
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
# Saves instance data as a .YAML file
|
|
343
|
+
origin.to_yaml(file_path=self.dataset_data_path)
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def animals(self) -> tuple[str, ...]:
|
|
347
|
+
"""Returns a tuple of unique animal identifiers included in the dataset."""
|
|
348
|
+
return tuple(sorted({s.animal for s in self.sessions}))
|
|
349
|
+
|
|
350
|
+
def get_sessions_for_animal(self, animal: str) -> tuple[SessionMetadata, ...]:
|
|
351
|
+
"""Returns the identification data for all sessions for the specified animal packaged into SessionMetadata
|
|
352
|
+
instances.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
animal: The unique identifier of the animal for which to retrieve the session identification data.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
A tuple of SessionMetadata instances for the specified animal.
|
|
359
|
+
"""
|
|
360
|
+
return tuple(s for s in self.sessions if s.animal == animal)
|
|
361
|
+
|
|
362
|
+
def get_session_data(self, animal: str, session: str) -> DatasetSessionData:
|
|
363
|
+
"""Returns the DatasetSessionData instance for the specified session.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
animal: The unique identifier of the animal that participated in the session.
|
|
367
|
+
session: The unique identifier of the session for which to return the data.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
The DatasetSessionData instance containing the paths to the session's data folder and memory-mapped data
|
|
371
|
+
files.
|
|
372
|
+
|
|
373
|
+
Raises:
|
|
374
|
+
ValueError: If the specified session is not found in the dataset.
|
|
375
|
+
"""
|
|
376
|
+
cache_key = f"{animal}/{session}"
|
|
377
|
+
if cache_key not in self._session_data_cache:
|
|
378
|
+
message = (
|
|
379
|
+
f"Unable to retrieve the data for the session '{session}', performed by the animal '{animal}'. "
|
|
380
|
+
f"The target animal and session combination is not found in the '{self.name}' dataset."
|
|
381
|
+
)
|
|
382
|
+
console.error(message=message, error=ValueError)
|
|
383
|
+
raise ValueError(message) # Fallback for mypy
|
|
384
|
+
|
|
385
|
+
return self._session_data_cache[cache_key]
|