labdata 0.0.3__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.
- labdata/__init__.py +17 -0
- labdata/cli.py +499 -0
- labdata/compute/__init__.py +27 -0
- labdata/compute/ec2.py +198 -0
- labdata/compute/ephys.py +469 -0
- labdata/compute/pose.py +281 -0
- labdata/compute/schedulers.py +194 -0
- labdata/compute/singularity.py +95 -0
- labdata/compute/utils.py +561 -0
- labdata/copy.py +351 -0
- labdata/rules/__init__.py +78 -0
- labdata/rules/ephys.py +188 -0
- labdata/rules/imaging.py +618 -0
- labdata/rules/utils.py +290 -0
- labdata/s3.py +317 -0
- labdata/schema/__init__.py +24 -0
- labdata/schema/ephys.py +547 -0
- labdata/schema/general.py +647 -0
- labdata/schema/histology.py +309 -0
- labdata/schema/onephoton.py +93 -0
- labdata/schema/procedures.py +102 -0
- labdata/schema/tasks.py +66 -0
- labdata/schema/twophoton.py +142 -0
- labdata/schema/utils.py +25 -0
- labdata/schema/video.py +243 -0
- labdata/stacks.py +182 -0
- labdata/utils.py +598 -0
- labdata/widgets.py +412 -0
- labdata-0.0.3.dist-info/METADATA +42 -0
- labdata-0.0.3.dist-info/RECORD +36 -0
- labdata-0.0.3.dist-info/WHEEL +5 -0
- labdata-0.0.3.dist-info/entry_points.txt +2 -0
- labdata-0.0.3.dist-info/licenses/LICENSE +674 -0
- labdata-0.0.3.dist-info/top_level.txt +2 -0
- labdata_frontend/Home.py +39 -0
- labdata_frontend/__init__.py +0 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
'''General schema classes for lab data management.
|
|
2
|
+
|
|
3
|
+
This module defines the core database schema for managing laboratory data using DataJoint.
|
|
4
|
+
It includes tables for tracking files, subjects, sessions, datasets and analysis results.
|
|
5
|
+
|
|
6
|
+
Configuration
|
|
7
|
+
------------
|
|
8
|
+
Uses the following settings from user_preferences.json:
|
|
9
|
+
- database.host : Database server hostname
|
|
10
|
+
- database.user : Database username
|
|
11
|
+
- database.password : Database password
|
|
12
|
+
- database.name : Name of the database
|
|
13
|
+
|
|
14
|
+
Schemas
|
|
15
|
+
-------
|
|
16
|
+
- dataschema : DataJoint schema
|
|
17
|
+
Primary schema for raw experimental data tables
|
|
18
|
+
- analysisschema : DataJoint schema
|
|
19
|
+
Schema for computed/analyzed data tables
|
|
20
|
+
|
|
21
|
+
Classes
|
|
22
|
+
-------
|
|
23
|
+
- ``File`` : Table for tracking raw data files with paths, checksums and metadata
|
|
24
|
+
- ``AnalysisFile`` : Table for tracking analysis output files
|
|
25
|
+
- ``ProcessedFile`` : Table for tracking processed data files
|
|
26
|
+
- ``LabMember`` : Table of lab personnel
|
|
27
|
+
- ``Species`` : Table of experimental animal species
|
|
28
|
+
- ``Strain`` : Table of animal strains/lines
|
|
29
|
+
- ``Subject`` : Table of experimental subjects
|
|
30
|
+
- ``Session`` : Table of experimental sessions
|
|
31
|
+
- ``Dataset`` : Table of experimental datasets
|
|
32
|
+
- ``SetupLocation`` : Table of experimental setup locations
|
|
33
|
+
- ``Setup`` : Table of experimental setups and equipment
|
|
34
|
+
- ``Note`` : Table for general notes and comments
|
|
35
|
+
- ``DatasetType`` : Table of dataset type definitions
|
|
36
|
+
- ``DatasetEvents`` : Table of events within datasets
|
|
37
|
+
- ``StreamSync`` : Table for synchronization between data streams
|
|
38
|
+
- ``UploadJob`` : Table for tracking data uploads
|
|
39
|
+
- ``ComputeTask`` : Table for tracking analysis computations
|
|
40
|
+
'''
|
|
41
|
+
|
|
42
|
+
from ..utils import *
|
|
43
|
+
import datajoint as dj
|
|
44
|
+
|
|
45
|
+
if 'database' in prefs.keys():
|
|
46
|
+
for k in prefs['database'].keys():
|
|
47
|
+
if prefs['database'][k] is None:
|
|
48
|
+
if k in ['database.user', 'database.password']:
|
|
49
|
+
import getpass
|
|
50
|
+
# get the password and write to file
|
|
51
|
+
prefs['database'][k] = getpass.getpass(prompt=k)
|
|
52
|
+
gotpass = True
|
|
53
|
+
if not prefs['database'][k] is None:
|
|
54
|
+
dj.config[k] = prefs['database'][k]
|
|
55
|
+
if 'gotpass' in dir():
|
|
56
|
+
# overwrite the preference file (save the datajoint password.)
|
|
57
|
+
save_labdata_preferences(prefs, LABDATA_FILE)
|
|
58
|
+
|
|
59
|
+
dataschema = dj.schema(dj.config['database.name'])
|
|
60
|
+
analysisschema = dj.schema(dj.config['database.name']+'_computed')
|
|
61
|
+
|
|
62
|
+
DEFAULT_RAW_STORAGE = 'data'
|
|
63
|
+
DEFAULT_ANALYSIS_STORAGE = 'analysis'
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataschema
|
|
67
|
+
class File(dj.Manual):
|
|
68
|
+
'''Table for tracking files stored in S3 storage.
|
|
69
|
+
|
|
70
|
+
This table stores metadata about files including their path, storage location,
|
|
71
|
+
creation date, size and MD5 checksum. It provides methods for:
|
|
72
|
+
|
|
73
|
+
- Deleting files from both the database and S3 storage
|
|
74
|
+
- Downloading files from S3 to local storage
|
|
75
|
+
- Checking if files are archived in S3 Glacier storage
|
|
76
|
+
|
|
77
|
+
The table is used as a base class for AnalysisFile which handles analysis outputs.
|
|
78
|
+
'''
|
|
79
|
+
definition = '''
|
|
80
|
+
file_path : varchar(300) # Path to the file
|
|
81
|
+
storage = "{0}" : varchar(12) # storage name
|
|
82
|
+
---
|
|
83
|
+
file_datetime : datetime # date created
|
|
84
|
+
file_size : double # using double because int64 does not exist
|
|
85
|
+
file_md5 = NULL : varchar(32) # md5 checksum
|
|
86
|
+
'''.format(DEFAULT_RAW_STORAGE)
|
|
87
|
+
storage = DEFAULT_RAW_STORAGE
|
|
88
|
+
# Files get deleted from AWS if the user has permissions
|
|
89
|
+
def delete(
|
|
90
|
+
self,
|
|
91
|
+
transaction = True,
|
|
92
|
+
safemode = None,
|
|
93
|
+
force_parts = False):
|
|
94
|
+
'''Delete files from both the database and S3 storage.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
transaction : bool, optional
|
|
99
|
+
Whether to perform deletion as a transaction, by default True
|
|
100
|
+
safemode : bool, optional
|
|
101
|
+
Whether to run in safe mode, by default None
|
|
102
|
+
force_parts : bool, optional
|
|
103
|
+
Whether to force deletion of parts, by default False
|
|
104
|
+
|
|
105
|
+
Raises
|
|
106
|
+
------
|
|
107
|
+
ValueError
|
|
108
|
+
If files are deleted from database but not from S3
|
|
109
|
+
'''
|
|
110
|
+
from ..s3 import s3_delete_file
|
|
111
|
+
from tqdm import tqdm
|
|
112
|
+
filesdict = [f for f in self]
|
|
113
|
+
super().delete(transaction = transaction,
|
|
114
|
+
safemode = safemode,
|
|
115
|
+
force_parts = force_parts)
|
|
116
|
+
if len(self) == 0:
|
|
117
|
+
files_not_deleted = []
|
|
118
|
+
for s in tqdm(filesdict,desc = f'Deleting objects from s3 {"storage"}:'):
|
|
119
|
+
fname = s["file_path"]
|
|
120
|
+
try:
|
|
121
|
+
s3_delete_file(fname,
|
|
122
|
+
storage = prefs['storage'][s['storage']],
|
|
123
|
+
remove_versions = True)
|
|
124
|
+
|
|
125
|
+
except Exception as err:
|
|
126
|
+
print(f'Could not delete {fname}.')
|
|
127
|
+
files_not_deleted.append(fname)
|
|
128
|
+
if len(files_not_deleted):
|
|
129
|
+
print('\n'.join(files_not_deleted))
|
|
130
|
+
raise(ValueError('''
|
|
131
|
+
|
|
132
|
+
[Integrity error] Files were deleted from the database but not from AWS.
|
|
133
|
+
|
|
134
|
+
Save this message and show it to your database ADMIN.
|
|
135
|
+
|
|
136
|
+
{0}
|
|
137
|
+
|
|
138
|
+
'''.format('\n'.join(files_not_deleted))))
|
|
139
|
+
|
|
140
|
+
def check_if_files_local(self, local_paths = None):
|
|
141
|
+
'''
|
|
142
|
+
Checks if files are in a local path, searches accross all local paths
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
local_paths : list of str or Path, optional
|
|
147
|
+
List of local paths to check for files, by default None uses paths in preferences
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
tuple
|
|
152
|
+
Tuple of local file paths and missing files
|
|
153
|
+
|
|
154
|
+
Raises
|
|
155
|
+
------
|
|
156
|
+
ValueError
|
|
157
|
+
If no files in the object
|
|
158
|
+
'''
|
|
159
|
+
if local_paths is None:
|
|
160
|
+
local_paths = prefs['local_paths']
|
|
161
|
+
if not len(self):
|
|
162
|
+
raise(ValueError('No files to get.'))
|
|
163
|
+
# this does not work with multiple storages
|
|
164
|
+
files = [f['file_path'] for f in self]
|
|
165
|
+
localfiles = [find_local_filepath(a, local_paths = local_paths) for a in files]
|
|
166
|
+
# check if they exist and download only missing files.
|
|
167
|
+
missingfiles = []
|
|
168
|
+
for f in files:
|
|
169
|
+
if not np.any([str(l).endswith(f) for l in localfiles]):
|
|
170
|
+
missingfiles.append(f)
|
|
171
|
+
return localfiles, missingfiles
|
|
172
|
+
|
|
173
|
+
def get(self,local_paths = None, check_if_archived = True, restore=True, download = True):
|
|
174
|
+
'''Download files from S3 to local storage.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
local_paths : list of str or Path, optional
|
|
179
|
+
List of local paths to download files to, by default None uses paths in preferences
|
|
180
|
+
check_if_archived : bool, optional
|
|
181
|
+
Whether to check if files are in Glacier storage, by default True
|
|
182
|
+
restore : bool, optional
|
|
183
|
+
Whether to restore archived files, by default True
|
|
184
|
+
download : bool, optional
|
|
185
|
+
Whether to actually download the files, by default True
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
list
|
|
190
|
+
List of local file paths that were downloaded
|
|
191
|
+
|
|
192
|
+
Raises
|
|
193
|
+
------
|
|
194
|
+
ValueError
|
|
195
|
+
If no files are found to download
|
|
196
|
+
'''
|
|
197
|
+
if local_paths is None:
|
|
198
|
+
local_paths = prefs['local_paths']
|
|
199
|
+
if not len(self):
|
|
200
|
+
raise(ValueError('No files to get.'))
|
|
201
|
+
|
|
202
|
+
localfiles, remotefiles = self.check_if_files_local(local_paths = local_paths)
|
|
203
|
+
storage = [f['storage'] for f in self][0]
|
|
204
|
+
remotefiles = self & [dict(file_path = f) for f in remotefiles]
|
|
205
|
+
if len(remotefiles):
|
|
206
|
+
if check_if_archived:
|
|
207
|
+
# TODO: add to the preference file to not restore by default.
|
|
208
|
+
self.check_if_files_archived(files = remotefiles, restore = restore)
|
|
209
|
+
if download:
|
|
210
|
+
print(f'Downloading {len(remotefiles)} files from S3 [{storage}].')
|
|
211
|
+
remotefiles = [r['file_path'] for r in remotefiles]
|
|
212
|
+
dstfiles = [Path(local_paths[0])/f for f in remotefiles] # place to store file.
|
|
213
|
+
from ..s3 import copy_from_s3
|
|
214
|
+
copy_from_s3(remotefiles,dstfiles,storage_name = storage)
|
|
215
|
+
localfiles, _ = self.check_if_files_local(local_paths = local_paths)
|
|
216
|
+
return localfiles
|
|
217
|
+
|
|
218
|
+
def check_if_files_archived(self, files = None, restore = True, suppress_error = False):
|
|
219
|
+
'''Check if files are archived in S3 Glacier storage.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
restore : bool, optional
|
|
224
|
+
Whether to initiate restore of archived files, by default True
|
|
225
|
+
suppress_error : bool, optional
|
|
226
|
+
Whether to suppress error if files are being restored, by default False
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
bool
|
|
231
|
+
True if files are archived, False otherwise
|
|
232
|
+
|
|
233
|
+
Raises
|
|
234
|
+
------
|
|
235
|
+
OSError
|
|
236
|
+
If storage is not in preferences or if files are being restored
|
|
237
|
+
'''
|
|
238
|
+
files_restoring = []
|
|
239
|
+
import boto3
|
|
240
|
+
if files is None:
|
|
241
|
+
files = self
|
|
242
|
+
for f in files:
|
|
243
|
+
# check if files are archived
|
|
244
|
+
# TODO: run this in parallel because it takes a while.
|
|
245
|
+
if not f['storage'] in prefs['storage'].keys():
|
|
246
|
+
raise(OSError(f"Store {f['storage']} is not in the preference file."))
|
|
247
|
+
store = prefs['storage'][f['storage']]
|
|
248
|
+
|
|
249
|
+
s3 = boto3.resource('s3',aws_access_key_id = store['access_key'],
|
|
250
|
+
aws_secret_access_key = store['secret_key'])
|
|
251
|
+
|
|
252
|
+
obj = s3.Object(bucket_name = store['bucket'],
|
|
253
|
+
key = f['file_path'])
|
|
254
|
+
|
|
255
|
+
if not obj.archive_status is None and 'ARCHIVE' in obj.archive_status:
|
|
256
|
+
if obj.restore is None:
|
|
257
|
+
if restore:
|
|
258
|
+
resp = obj.restore_object(RestoreRequest = {})
|
|
259
|
+
files_restoring.append(f['file_path'])
|
|
260
|
+
elif 'true' in obj.restore:
|
|
261
|
+
files_restoring.append(f['file_path'])
|
|
262
|
+
if len(files_restoring):
|
|
263
|
+
import warnings
|
|
264
|
+
warnings.warn(f"Files are being restored [{files_restoring}]")
|
|
265
|
+
if not suppress_error:
|
|
266
|
+
raise(OSError(f"Files are being restored [{files_restoring}]"))
|
|
267
|
+
return True # files are in arquive
|
|
268
|
+
return False # files are not in archive
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# This is in "analysisschema" so users can delete analysis files.
|
|
273
|
+
@analysisschema
|
|
274
|
+
class AnalysisFile(File):
|
|
275
|
+
definition = '''
|
|
276
|
+
file_path : varchar(300) # Path to the file
|
|
277
|
+
storage = "{0}" : varchar(12) # storage name
|
|
278
|
+
---
|
|
279
|
+
file_datetime : datetime # date created
|
|
280
|
+
file_size : double # using double because int64 does not exist
|
|
281
|
+
file_md5 = NULL : varchar(32) # md5 checksum
|
|
282
|
+
'''.format(DEFAULT_ANALYSIS_STORAGE)
|
|
283
|
+
storage = DEFAULT_ANALYSIS_STORAGE
|
|
284
|
+
# All users with permission to run analysis should also have permission to add and remove files from the analysis bucket in AWS
|
|
285
|
+
def upload_files(self,src,dataset, force = True):
|
|
286
|
+
assert 'subject_name' in dataset.keys(), ValueError('dataset must have subject_name')
|
|
287
|
+
assert 'session_name' in dataset.keys(), ValueError('dataset must have session_name')
|
|
288
|
+
assert 'dataset_name' in dataset.keys(), ValueError('dataset must have dataset_name')
|
|
289
|
+
|
|
290
|
+
destpath = '{subject_name}/{session_name}/{dataset_name}/'.format(**dataset)
|
|
291
|
+
dst = [destpath+k.name for k in src]
|
|
292
|
+
for d in dst:
|
|
293
|
+
if len(AnalysisFile() & dict(file_path = d)) > 0:
|
|
294
|
+
if not force:
|
|
295
|
+
ValueError(f'File is already in database, delete it to re-upload {d}.')
|
|
296
|
+
else:
|
|
297
|
+
(AnalysisFile() & dict(file_path = d)).delete(safemode = False)
|
|
298
|
+
assert self.storage in prefs['storage'].keys(),ValueError(
|
|
299
|
+
'Specify an {self.storage} bucket in preferences["storage"].')
|
|
300
|
+
from ..s3 import copy_to_s3
|
|
301
|
+
|
|
302
|
+
copy_to_s3(src, dst, md5_checksum=None, storage_name = self.storage)
|
|
303
|
+
dates = [datetime.utcfromtimestamp(Path(f).stat().st_mtime) for f in src]
|
|
304
|
+
sizes = [Path(f).stat().st_size for f in src]
|
|
305
|
+
md5 = compute_md5s(src)
|
|
306
|
+
# insert in AnalysisFile if all went well
|
|
307
|
+
self.insert([dict(file_path = f,
|
|
308
|
+
storage = self.storage,
|
|
309
|
+
file_datetime = d,
|
|
310
|
+
file_md5 = m,
|
|
311
|
+
file_size = s) for f,d,s,m in zip(dst,dates,sizes,md5)])
|
|
312
|
+
return [dict(file_path = f,storage = self.storage) for f in dst]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# This table stores file name and checksums of files that were sent to upload but were processed by upload rules
|
|
316
|
+
# There are no actual files associated with these paths
|
|
317
|
+
@dataschema
|
|
318
|
+
class ProcessedFile(dj.Manual):
|
|
319
|
+
definition = '''
|
|
320
|
+
file_path : varchar(300) # Path to the file that was processe (these are not in S3)
|
|
321
|
+
---
|
|
322
|
+
file_datetime : datetime # date created
|
|
323
|
+
file_size : double # using double because int64 does not exist
|
|
324
|
+
file_md5 = NULL : varchar(32) # md5 checksum
|
|
325
|
+
'''
|
|
326
|
+
|
|
327
|
+
@dataschema
|
|
328
|
+
class LabMember(dj.Manual):
|
|
329
|
+
definition = """
|
|
330
|
+
user_name : varchar(32) # username
|
|
331
|
+
---
|
|
332
|
+
email=null : varchar(128) # email address
|
|
333
|
+
first_name = null : varchar(32) # first name
|
|
334
|
+
last_name = null : varchar(32) # last name
|
|
335
|
+
date_joined : date # when the user joined the lab
|
|
336
|
+
is_active = 1 : boolean # active or left the lab
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
@dataschema
|
|
340
|
+
class Species(dj.Lookup):
|
|
341
|
+
definition = """
|
|
342
|
+
species_name : varchar(32) # species nickname
|
|
343
|
+
---
|
|
344
|
+
species_scientific_name : varchar(56) # scientific name
|
|
345
|
+
species_description=null : varchar(256) # description
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
@dataschema
|
|
349
|
+
class Strain(dj.Lookup):
|
|
350
|
+
definition = """
|
|
351
|
+
strain_name : varchar(56) # strain name
|
|
352
|
+
---
|
|
353
|
+
-> Species
|
|
354
|
+
strain_description=null : varchar(256) # description
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
@dataschema
|
|
358
|
+
class Subject(dj.Manual):
|
|
359
|
+
''' Experimental subject.'''
|
|
360
|
+
definition = """
|
|
361
|
+
subject_name : varchar(20) # unique mouse id
|
|
362
|
+
---
|
|
363
|
+
subject_dob : date # mouse date of birth
|
|
364
|
+
subject_sex : enum('M', 'F', 'U') # sex of mouse - Male, Female, or Unknown
|
|
365
|
+
-> Strain
|
|
366
|
+
-> LabMember
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
@dataschema
|
|
370
|
+
class SetupLocation(dj.Lookup):
|
|
371
|
+
definition = """
|
|
372
|
+
setup_location : varchar(255) # room
|
|
373
|
+
---
|
|
374
|
+
"""
|
|
375
|
+
contents = zip(['CHS-74100'])
|
|
376
|
+
|
|
377
|
+
@dataschema
|
|
378
|
+
class Setup(dj.Lookup):
|
|
379
|
+
definition = """
|
|
380
|
+
setup_name : varchar(54) # setup name
|
|
381
|
+
---
|
|
382
|
+
-> [nullable] SetupLocation
|
|
383
|
+
setup_description = NULL : varchar(512)
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
@dataschema
|
|
387
|
+
class Note(dj.Manual):
|
|
388
|
+
definition = """
|
|
389
|
+
-> LabMember.proj(notetaker='user_name')
|
|
390
|
+
note_datetime : datetime
|
|
391
|
+
---
|
|
392
|
+
notes = '' : varchar(4000) # free-text notes
|
|
393
|
+
"""
|
|
394
|
+
class Image(dj.Part):
|
|
395
|
+
definition = """
|
|
396
|
+
-> Note
|
|
397
|
+
image_id : int
|
|
398
|
+
---
|
|
399
|
+
image : longblob
|
|
400
|
+
caption = NULL : varchar(256)
|
|
401
|
+
"""
|
|
402
|
+
class Attachment(dj.Part):
|
|
403
|
+
definition = """
|
|
404
|
+
-> Note
|
|
405
|
+
-> File
|
|
406
|
+
---
|
|
407
|
+
caption = NULL : varchar(256)
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
@dataschema
|
|
411
|
+
class Session(dj.Manual):
|
|
412
|
+
definition = """
|
|
413
|
+
-> Subject
|
|
414
|
+
session_name : varchar(54) # session identifier
|
|
415
|
+
---
|
|
416
|
+
session_datetime : datetime # experiment date
|
|
417
|
+
-> [nullable] LabMember.proj(experimenter = 'user_name')
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
@dataschema
|
|
421
|
+
class DatasetType(dj.Lookup):
|
|
422
|
+
definition = """
|
|
423
|
+
dataset_type: varchar(32)
|
|
424
|
+
"""
|
|
425
|
+
contents = zip(dataset_type_names)
|
|
426
|
+
|
|
427
|
+
@dataschema
|
|
428
|
+
class Dataset(dj.Manual):
|
|
429
|
+
definition = """
|
|
430
|
+
-> Subject
|
|
431
|
+
-> Session
|
|
432
|
+
dataset_name : varchar(128)
|
|
433
|
+
---
|
|
434
|
+
-> [nullable] DatasetType
|
|
435
|
+
-> [nullable] Setup
|
|
436
|
+
-> [nullable] Note
|
|
437
|
+
"""
|
|
438
|
+
class DataFiles(dj.Part): # the files that were acquired on that dataset.
|
|
439
|
+
definition = '''
|
|
440
|
+
-> master
|
|
441
|
+
-> File
|
|
442
|
+
'''
|
|
443
|
+
|
|
444
|
+
# Synchronization variables for the dataset live here; these can come from different streams
|
|
445
|
+
@dataschema
|
|
446
|
+
class DatasetEvents(dj.Imported):
|
|
447
|
+
definition = '''
|
|
448
|
+
-> Dataset
|
|
449
|
+
stream_name : varchar(54) # which clock is used e.g. btss, nidq, bpod, imecX
|
|
450
|
+
---
|
|
451
|
+
stream_time = NULL : longblob # for e.g. the analog channels
|
|
452
|
+
'''
|
|
453
|
+
class Digital(dj.Part):
|
|
454
|
+
definition = '''
|
|
455
|
+
-> master
|
|
456
|
+
event_name : varchar(54)
|
|
457
|
+
---
|
|
458
|
+
event_timestamps = NULL : longblob # timestamps of the events
|
|
459
|
+
event_values = NULL : longblob # event value or count
|
|
460
|
+
'''
|
|
461
|
+
projkeys = ['subject_name','session_name','dataset_name','stream_name','event_name']
|
|
462
|
+
|
|
463
|
+
def fetch_synced(self, force = False):
|
|
464
|
+
'''
|
|
465
|
+
Returned events already synchronized between data streams, following the StreamSync table.
|
|
466
|
+
'''
|
|
467
|
+
keys = [dict(subject_name = s["subject_name"],
|
|
468
|
+
session_name = s["session_name"],
|
|
469
|
+
dataset_name = s["dataset_name"],
|
|
470
|
+
stream_name = s["stream_name"]) for s in self]
|
|
471
|
+
evnts = []
|
|
472
|
+
streams = (StreamSync() & keys)
|
|
473
|
+
if not len(streams):
|
|
474
|
+
from warnings import warn
|
|
475
|
+
warn(f'There are no StreamSync for events {self.proj()}. This will return only the clock stream.')
|
|
476
|
+
else:
|
|
477
|
+
for s in streams:
|
|
478
|
+
evs = (self & dict(stream_name = s["stream_name"])).fetch(as_dict = True)
|
|
479
|
+
func = (StreamSync() & s).apply(None, force = force)
|
|
480
|
+
for evnt in evs:
|
|
481
|
+
if not evnt['event_timestamps'] is None:
|
|
482
|
+
evnt['event_timestamps'] = func(evnt['event_timestamps'])
|
|
483
|
+
evnts.append(evnt)
|
|
484
|
+
# add the events from the clock stream
|
|
485
|
+
if len(streams):
|
|
486
|
+
evs = (self & dict(stream_name = streams.clock_stream())).fetch(as_dict = True)
|
|
487
|
+
else:
|
|
488
|
+
evs = self
|
|
489
|
+
for evnt in evs:
|
|
490
|
+
evnts.append(evnt)
|
|
491
|
+
return evnts
|
|
492
|
+
|
|
493
|
+
def plot_synced(self, stream_colors = 'krbgyb', overlay_original = False, lw = 1,force = True):
|
|
494
|
+
''' Plot DatasetEvents.Digital.'''
|
|
495
|
+
evnts = self.fetch_synced(force = force)
|
|
496
|
+
ustreams = [n for n in np.unique([e['stream_name'] for e in evnts])]
|
|
497
|
+
|
|
498
|
+
import pylab as plt
|
|
499
|
+
caption = []
|
|
500
|
+
ticks = []
|
|
501
|
+
lns = []
|
|
502
|
+
for i,e in enumerate(evnts):
|
|
503
|
+
ln = plt.vlines(e['event_timestamps'],i,i+0.7,
|
|
504
|
+
color = stream_colors[np.mod(ustreams.index(e['stream_name']),len(stream_colors))],
|
|
505
|
+
lw = lw)
|
|
506
|
+
if overlay_original:
|
|
507
|
+
ee = (DatasetEvents.Digital() & {k:e[k] for k in self.projkeys}).fetch('event_timestamps')[0]
|
|
508
|
+
plt.vlines(ee,i+0.6,i+0.9,color='gray',lw = lw)
|
|
509
|
+
lns.append(ln)
|
|
510
|
+
ticks.append(i+0.35)
|
|
511
|
+
caption.append(f'{e["stream_name"]}_{e["event_name"]}')
|
|
512
|
+
plt.yticks(ticks,caption);
|
|
513
|
+
return lns
|
|
514
|
+
|
|
515
|
+
class AnalogChannel(dj.Part):
|
|
516
|
+
definition = '''
|
|
517
|
+
-> master
|
|
518
|
+
channel_name : varchar(54)
|
|
519
|
+
---
|
|
520
|
+
channel_values = NULL : longblob # analog values for channel
|
|
521
|
+
'''
|
|
522
|
+
|
|
523
|
+
@analysisschema
|
|
524
|
+
class StreamSync(dj.Manual):
|
|
525
|
+
definition = '''
|
|
526
|
+
-> Dataset
|
|
527
|
+
-> DatasetEvents.Digital
|
|
528
|
+
-> DatasetEvents.Digital.proj(clock_stream='stream_name',clock_stream_event='event_name',clock_dataset = 'dataset_name')
|
|
529
|
+
'''
|
|
530
|
+
def get_interp_data(self,force = False, warn = True, allowed_offset = 2):
|
|
531
|
+
''' Force will attempt to remove events from the longest stream so the streams are matched. '''
|
|
532
|
+
|
|
533
|
+
assert len(self)==1, ValueError(f"This function only takes one element at a time not {len(self)}.")
|
|
534
|
+
s = self.fetch1()
|
|
535
|
+
clock = (DatasetEvents.Digital() & dict(subject_name = s['subject_name'],
|
|
536
|
+
session_name = s['session_name'],
|
|
537
|
+
dataset_name = s['clock_dataset'],
|
|
538
|
+
stream_name = s['clock_stream'],
|
|
539
|
+
event_name = s['clock_stream_event'])).fetch1()
|
|
540
|
+
clock_onsets = clock['event_timestamps']#[clock['event_values'] == 1]
|
|
541
|
+
sync = (DatasetEvents.Digital() & dict(subject_name = s['subject_name'],
|
|
542
|
+
session_name = s['session_name'],
|
|
543
|
+
dataset_name = s['dataset_name'],
|
|
544
|
+
stream_name = s['stream_name'],
|
|
545
|
+
event_name = s['event_name'])).fetch1()
|
|
546
|
+
sync_onsets = sync['event_timestamps']
|
|
547
|
+
if ((len(sync_onsets)>=(len(clock_onsets)//2)-allowed_offset) and
|
|
548
|
+
(len(sync_onsets)<=(len(clock_onsets)//2)+allowed_offset)): # in case clock has both onsets and offsets
|
|
549
|
+
if clock['event_values'] is None:
|
|
550
|
+
clock_onsets = clock_onsets[::2]
|
|
551
|
+
else:
|
|
552
|
+
clock_onsets = clock_onsets[clock['event_values'] == 1]
|
|
553
|
+
if ((len(clock_onsets)>=(len(sync_onsets)//2)-allowed_offset) and
|
|
554
|
+
(len(clock_onsets)<=(len(sync_onsets)//2)+allowed_offset)): # in case sync has both onsets and offsets
|
|
555
|
+
if sync['event_values'] is None:
|
|
556
|
+
sync_onsets = sync_onsets[::2]
|
|
557
|
+
else:
|
|
558
|
+
sync_onsets = sync_onsets[sync['event_values']==1]
|
|
559
|
+
if warn:
|
|
560
|
+
if not len(sync_onsets) == len(clock_onsets):
|
|
561
|
+
import warnings
|
|
562
|
+
warnings.warn(f"There is a potential issue with the syncronization of sessions: {s['subject_name']} {s['session_name']}", UserWarning)
|
|
563
|
+
print(f" - stream {s['clock_stream']} channel {s['clock_stream_event']} {len(clock_onsets)}")
|
|
564
|
+
print(f" - stream {s['stream_name']} channel {s['event_name']} {len(sync_onsets)}")
|
|
565
|
+
if not force: # by default this is set to false.
|
|
566
|
+
assert len(clock_onsets) == len(sync_onsets), ValueError(f'\n\n Length of the clock and sync not the same? \n\n {self}')
|
|
567
|
+
N = np.min([len(sync_onsets),len(clock_onsets)])
|
|
568
|
+
return sync_onsets[:N],clock_onsets[:N]
|
|
569
|
+
|
|
570
|
+
def apply(self, values, sync_onsets = None, warn = True, clock_onsets = None, force = False):
|
|
571
|
+
'''
|
|
572
|
+
Returns synchronized signals according a sync pulse shared from a clock.
|
|
573
|
+
"clock" is main, "sync" is the same acquisition system as "values"
|
|
574
|
+
'''
|
|
575
|
+
if sync_onsets is None or clock_onsets is None:
|
|
576
|
+
sync_onsets, clock_onsets = self.get_interp_data(force = force, warn = warn)
|
|
577
|
+
if values is None: # return a function if no values passed
|
|
578
|
+
return lambda x: np.interp(x,sync_onsets,clock_onsets)
|
|
579
|
+
# linear interpolation to get the time of events syncronized across streams
|
|
580
|
+
return np.interp(values,sync_onsets,clock_onsets)
|
|
581
|
+
|
|
582
|
+
def clock_stream(self):
|
|
583
|
+
'''
|
|
584
|
+
Returns the name of the clock stream(s)
|
|
585
|
+
'''
|
|
586
|
+
clkstreams = np.unique(self.fetch('clock_stream'))
|
|
587
|
+
if len(clkstreams) == 1:
|
|
588
|
+
return clkstreams[0]
|
|
589
|
+
return clkstreams
|
|
590
|
+
|
|
591
|
+
####################################################
|
|
592
|
+
#######################QUEUES#######################
|
|
593
|
+
####################################################
|
|
594
|
+
# Upload queue, so that experimental computers are not transfering data
|
|
595
|
+
# @dataschema
|
|
596
|
+
@analysisschema
|
|
597
|
+
class UploadJob(dj.Manual):
|
|
598
|
+
definition = '''
|
|
599
|
+
job_id : int auto_increment
|
|
600
|
+
---
|
|
601
|
+
job_waiting = 1 : tinyint # 1 if the job is up for grabs
|
|
602
|
+
job_status = NULL : varchar(52) # status of the job (did it fail?)
|
|
603
|
+
job_host = NULL : varchar(52) # where the job is running
|
|
604
|
+
job_rule = NULL : varchar(52) # what rule is it following
|
|
605
|
+
job_log = NULL : varchar(500) # LOG
|
|
606
|
+
-> [nullable] Dataset # optionally insert to dataset
|
|
607
|
+
upload_storage = NULL : varchar(12) # storage name, where to upload
|
|
608
|
+
|
|
609
|
+
'''
|
|
610
|
+
|
|
611
|
+
class AssignedFiles(dj.Part):
|
|
612
|
+
definition = '''
|
|
613
|
+
-> master
|
|
614
|
+
src_path : varchar(300) # local file path
|
|
615
|
+
---
|
|
616
|
+
src_datetime : datetime # date created
|
|
617
|
+
src_size : double # using double because int64 does not exist
|
|
618
|
+
src_md5 = NULL : varchar(32) # md5 checksum
|
|
619
|
+
'''
|
|
620
|
+
|
|
621
|
+
# Jobs to perform computations, like spike sorting or segmentation
|
|
622
|
+
# @dataschema
|
|
623
|
+
@analysisschema
|
|
624
|
+
class ComputeTask(dj.Manual):
|
|
625
|
+
definition = '''
|
|
626
|
+
job_id : int auto_increment
|
|
627
|
+
---
|
|
628
|
+
task_waiting = 1 : tinyint # 1 if the job is up for grabs
|
|
629
|
+
task_name = NULL : varchar(52) # what task to run
|
|
630
|
+
task_status = NULL : varchar(52) # status of the job (did it fail?)
|
|
631
|
+
task_target = NULL : varchar(52) # where to run the job, so it only runs where selected
|
|
632
|
+
task_host = NULL : varchar(52) # where the job is running
|
|
633
|
+
task_cmd = NULL : varchar(500) # command to run
|
|
634
|
+
task_parameters = NULL : varchar(2000) # command to run after the job finishes
|
|
635
|
+
task_log = NULL : varchar(2000) # LOG
|
|
636
|
+
task_starttime = NULL : datetime # time of task start
|
|
637
|
+
task_endtime = NULL : datetime # time of task completion
|
|
638
|
+
|
|
639
|
+
-> [nullable] Dataset # dataset
|
|
640
|
+
'''
|
|
641
|
+
|
|
642
|
+
class AssignedFiles(dj.Part):
|
|
643
|
+
definition = '''
|
|
644
|
+
-> master
|
|
645
|
+
-> File
|
|
646
|
+
'''
|
|
647
|
+
|