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/copy.py ADDED
@@ -0,0 +1,351 @@
1
+ from .utils import *
2
+ from warnings import warn
3
+
4
+ def _copyfile_to_upload_server(filepath, local_path=None, server_path = None,overwrite = False):
5
+ '''
6
+
7
+ This is an internal function, not meant to be called by the end user, call copy_to_upload_server instead.
8
+
9
+ This is a support function that will copy data between computers; it will not overwrite, unless forced.
10
+
11
+ It will raise an exception if the files are already there unless overwrite is true.
12
+
13
+ Does not insert to the Upload table.
14
+
15
+
16
+ Parameters
17
+ ----------
18
+ filepath : str or Path
19
+ Path to the file to copy, relative to local_path
20
+ local_path : str or Path, optional
21
+ Source directory containing the file. If None, uses first path from preferences
22
+ server_path : str or Path, optional
23
+ Destination directory to copy to. If None, uses upload_path from preferences
24
+ overwrite : bool, default False
25
+ Whether to overwrite existing files at the destination
26
+
27
+ Returns
28
+ -------
29
+ dict
30
+ Dictionary containing:
31
+ - src_path: Relative path to the file
32
+ - src_md5: MD5 hash of the file
33
+ - src_size: Size of the file in bytes
34
+ - src_datetime: Creation timestamp of the file
35
+
36
+ Raises
37
+ ------
38
+ OSError
39
+ If filepath is a directory
40
+ If destination file exists and overwrite=False
41
+ If copy operation fails
42
+
43
+
44
+ Joao Couto - labdata 2024
45
+
46
+ '''
47
+
48
+ src = Path(local_path)/filepath
49
+ if src.is_dir():
50
+ raise OSError(f'Can only handle files {src}; copy each file in the folder separately.')
51
+ dst = Path(server_path)/filepath
52
+ if not overwrite and dst.exists() and not src == dst:
53
+ raise OSError(f'File {dst} exists; will not overwrite.')
54
+ hash = compute_md5_hash(src) # computes the hash
55
+ srcstat = src.stat()
56
+ file_size = srcstat.st_size
57
+ from shutil import copy2
58
+ dst.parent.mkdir(parents=True, exist_ok = True)
59
+ if not src == dst:
60
+ try:
61
+ copy2(src, dst)
62
+ except:
63
+ raise OSError(f'Could not copy {src} to {dst}.')
64
+ else:
65
+ # if the source and destination are the same, don't copy - just return cause its in the same server..
66
+ print('[copy_to_upload_server] Source and destination are the same, not copying.', flush = True)
67
+ filepath = str(filepath).replace(pathlib.os.sep,'/').replace('//','/')
68
+ return dict(src_path = filepath,
69
+ src_md5 = hash,
70
+ src_size = file_size,
71
+ src_datetime = datetime.fromtimestamp(srcstat.st_ctime))
72
+
73
+ def copy_to_upload_server(filepaths, local_path = None, server_path = None,
74
+ upload_storage = None,
75
+ overwrite = False,
76
+ n_jobs = 8,
77
+ job_rule = None,
78
+ parse_filename = True,
79
+ **kwargs):
80
+ '''
81
+ Copy data between computers; it will not overwrite, unless forced.
82
+
83
+ Returns a list of dictionaries with the file paths and md5 checksums.
84
+
85
+ Inserts in the Upload table.
86
+ Parameters
87
+ ----------
88
+ filepaths : list
89
+ List of file paths to copy to upload server
90
+ local_path : str, optional
91
+ Local path where files are stored. If None, uses first path from preferences
92
+ server_path : str, optional
93
+ Path on upload server. If None, uses upload_path from preferences
94
+ upload_storage : str, optional
95
+ Storage location for uploaded files. If None, uses upload_storage from preferences
96
+ overwrite : bool, default False
97
+ Whether to overwrite existing files on server
98
+ n_jobs : int, default 8
99
+ Number of parallel jobs for copying files
100
+ job_rule : str, optional
101
+ Rule to apply during upload
102
+ parse_filename : bool, default True
103
+ Whether to parse metadata from filenames based on path rules
104
+ **kwargs : dict
105
+ Additional metadata to store with upload (e.g. subject_name, session_name)
106
+
107
+ Returns
108
+ -------
109
+ dict
110
+ Dictionary containing upload job information
111
+
112
+ Joao Couto - labdata 2024
113
+ '''
114
+
115
+ from .schema import UploadJob,Setup,Subject,Session,Dataset,dj
116
+ if local_path is None: # get the local_path from the preferences
117
+ if not 'local_paths' in prefs.keys():
118
+ raise OSError('Local data path [local_paths] not specified in the preference file.')
119
+ local_path = prefs['local_paths'][0]
120
+ if server_path is None: # get the upload_path from the preferences
121
+ if not 'upload_path' in prefs.keys():
122
+ raise OSError('Upload storage [upload_path], not specified in the preference file.')
123
+ server_path = prefs['upload_path']
124
+ if upload_storage is None: # get the upload_storage from the preferences, where data will be stored.
125
+ if not 'upload_storage' in prefs.keys():
126
+ raise OSError('Upload storage [upload_storage], not specified in the preference file.')
127
+ upload_storage = prefs['upload_storage']
128
+
129
+ if not type(filepaths) is list: # Check if the filepaths are in a list
130
+ raise ValueError('Input filepaths must be a list of paths.')
131
+ # replace local_path if the user passed it like that by accident.
132
+ filepaths = [str(f).replace(str(local_path),'') for f in filepaths]
133
+ # remove trailing / or \
134
+ filepaths = [f if not f.startswith(pathlib.os.sep) else f[1:] for f in filepaths]
135
+ # make unique
136
+ filepaths = [f for f in np.unique(filepaths)]
137
+
138
+ if any_path_uploaded(filepaths):
139
+ if not job_rule in ['replace']:
140
+ raise OSError('Path was already uploaded {0}'.format(Path(filepaths[0]).parent))
141
+ else:
142
+ warn('[copy_to_upload_server] Files exist in the server and will be replaced.')
143
+
144
+ if parse_filename: # parse filename based on the path rules
145
+ tmp = parse_filepath_parts(filepaths[0])
146
+ for k in tmp.keys():
147
+ if not k in kwargs.keys():
148
+ kwargs[k] = tmp[k]
149
+
150
+ n_jobs = validate_num_jobs_joblib(n_jobs) # avoid nested parallelism.
151
+ # copy and compute checksum for all paths in parallel.
152
+ res = Parallel(n_jobs = n_jobs)(
153
+ delayed(_copyfile_to_upload_server)(path,
154
+ local_path = local_path,
155
+ server_path = server_path,
156
+ overwrite = overwrite) for path in filepaths)
157
+ # Add it to the upload table
158
+ # check the job id
159
+ with dj.conn().transaction:
160
+ #print(kwargs)
161
+ if "setup_name" in kwargs.keys():
162
+ Setup.insert1(kwargs, skip_duplicates = True, ignore_extra_fields = True) # try to insert setup
163
+ if "dataset_name" in kwargs.keys() and "session_name" in kwargs.keys() and "subject_name" in kwargs.keys():
164
+ if not len(Subject() & dict(subject_name=kwargs['subject_name'])):
165
+ try:
166
+ Subject.insert1(kwargs, skip_duplicates = True,ignore_extra_fields = True) # try to insert subject
167
+ except Exception as err:
168
+ warn(f'Could not insert subject {kwargs}')
169
+ print(err, flush = True)
170
+ # needs date of birth and sex
171
+ if not len(Session() & dict(subject_name=kwargs['subject_name'],
172
+ session_name = kwargs['session_name'])):
173
+ Session.insert1(kwargs, skip_duplicates = True,ignore_extra_fields = True) # try to insert session
174
+ if not len(Dataset() & dict(subject_name = kwargs['subject_name'],
175
+ session_name = kwargs['session_name'],
176
+ dataset_name = kwargs['dataset_name'])):
177
+ Dataset.insert1(kwargs, skip_duplicates = True, ignore_extra_fields = True) # try to insert dataset
178
+ # this is a brute force way... there should be a better way of doing this but the auto-increment not return in datajoint...
179
+ attempts = 10
180
+ jobid = UploadJob().fetch('job_id')
181
+ if len(jobid):
182
+ jobid = np.max(jobid) + 1
183
+ else:
184
+ jobid = 1
185
+ job_insert_failed = 1
186
+ for iattempt in range(attempts):
187
+ try:
188
+ UploadJob.insert1(dict(job_id = jobid,
189
+ job_status = "ON SERVER",
190
+ upload_storage = upload_storage,
191
+ job_rule = job_rule,
192
+ **kwargs),
193
+ ignore_extra_fields = True) # Need to insert the dataset first if not there
194
+ job_insert_failed = 0
195
+ break # we have the job id
196
+ except Exception as err:
197
+ jobid += 1
198
+ print(err) # we don't have it, do it again
199
+ if job_insert_failed:
200
+ raise ValueError(f'Job insert failed because could not add {jobid} to the UploadJob queue.')
201
+ res = [dict(r, job_id = jobid) for r in res] # add dataset through kwargs
202
+ UploadJob.AssignedFiles.insert(res, ignore_extra_fields=True)
203
+ # the upload server will run the checksum and upload the files.
204
+ return res
205
+
206
+ def any_path_uploaded(filepaths):
207
+ '''
208
+ any_path_uploaded(filepaths)
209
+
210
+ Checks if any of the provided file paths:
211
+ - Exist in the File table (already uploaded)
212
+ - Exist in the UploadJob.AssignedFiles table (queued for upload)
213
+ - Exist in the ProcessedFile table (original file deleted because the dataset was processed with a Rule)
214
+
215
+ Parameters
216
+ ----------
217
+ filepaths : list
218
+ List of file paths to check
219
+
220
+ Returns
221
+ -------
222
+ bool
223
+ True if any of the files are already uploaded or in the upload queue
224
+ False if none of the files are uploaded or queued
225
+ '''
226
+ from .schema import UploadJob, File, ProcessedFile
227
+ # check if the paths are in "Upload or in Files"
228
+ paths = []
229
+ src_paths = []
230
+ for p in filepaths: # if true for any of the filepaths return True
231
+ pn = p.replace(pathlib.os.sep,'/').replace('//','/')
232
+ paths.append(dict(file_path = pn))
233
+ src_paths.append(dict(src_path = pn))
234
+ if len((File() & paths)) > 0:
235
+ warn(f'Found paths in File table {File() & paths}')
236
+ return True
237
+ if len(UploadJob.AssignedFiles() & src_paths) > 0:
238
+ warn(f'Found paths in UploadJob table {UploadJob.AssignedFiles() & src_paths}')
239
+ return True
240
+ if len(ProcessedFile() & paths) > 0:
241
+ warn(f'Found paths in ProcessedFile table {ProcessedFile() & paths}')
242
+ return True
243
+ return False # Otherwise return False
244
+
245
+ def all_paths_uploaded(filepaths):
246
+ '''
247
+ all_paths_uploaded(filepaths)
248
+
249
+ Checks if all of the provided file paths exist in the File table (already uploaded)
250
+
251
+ This function only checks the File table, unlike any_path_uploaded() which also checks
252
+ UploadJob.AssignedFiles and ProcessedFile tables.
253
+
254
+ Parameters
255
+ ----------
256
+ filepaths : list
257
+ List of file paths to check
258
+
259
+ Returns
260
+ -------
261
+ bool
262
+ True if all of the files are already uploaded
263
+ False if any files are missing from the File table
264
+ '''
265
+ from .schema import UploadJob, File, ProcessedFile
266
+ # check if the paths are in "Upload or in Files"
267
+ paths = []
268
+ src_paths = []
269
+ for p in filepaths: # if true for any of the filepaths return True
270
+ pn = p.replace(pathlib.os.sep,'/').replace('//','/')
271
+ paths.append(dict(file_path = pn))
272
+ src_paths.append(dict(src_path = pn))
273
+ if len((File() & paths)) == len(filepaths):
274
+ return True
275
+
276
+ warn(f'Could not find some files in the table {File() & paths} when trying to find {filepaths}')
277
+ return False # Otherwise return False
278
+
279
+ def clean_local_path(local_path = None, filterkeys = [], dry_run = False):
280
+ '''
281
+ clean_local_path(local_path = None, filterkeys = [], dry_run = False)
282
+
283
+ Remove local files that are already in the database from a local path
284
+ Clean local files that have already been uploaded to the database.
285
+
286
+ This function checks files in a local directory against the database and removes
287
+ local copies that have already been successfully uploaded (verified by MD5 checksum).
288
+
289
+ Files are only deleted if they exist in either the File or ProcessedFile tables
290
+ AND their MD5 checksums match exactly
291
+
292
+ Files with mismatched checksums are kept and reported
293
+
294
+ Empty directories are not automatically removed (TODO)
295
+
296
+ Parameters
297
+ ----------
298
+ local_path : str or Path, optional
299
+ Path to local directory to clean. If None, uses first path from preferences.
300
+ filterkeys : list, optional
301
+ List of strings to filter filenames by. Only files containing all filterkeys will be checked.
302
+ dry_run : bool, optional
303
+ If True, only prints what would be deleted without actually deleting files.
304
+
305
+ Returns
306
+ -------
307
+ deleted : list
308
+ List of files that were deleted (or would be deleted in dry_run mode)
309
+ keep : list
310
+ List of files that were kept because their checksums did not match the database
311
+
312
+ Joao Couto - labdata 2024
313
+ '''
314
+ from .schema import File, ProcessedFile
315
+ if local_path is None:
316
+ local_path = prefs['local_paths'][0]
317
+ filelist = Path(local_path).rglob('*')
318
+ local_filelist = list(filter(lambda x: x.is_file(),filelist))
319
+ remote_filelist = np.array(drop_local_path(local_filelist))
320
+ # check that the files follow a filter
321
+ selection = np.ones_like(remote_filelist, dtype=bool)
322
+ for f in filterkeys:
323
+ selection *= np.array([1 if f in str(k) else 0 for k in remote_filelist],dtype=bool)
324
+ remote_keys_all = [dict(file_path = str(r).replace('\\','/')) for r in remote_filelist[selection]]
325
+ # get the remote keys
326
+ remote_keys = (File() & remote_keys_all).fetch(as_dict = True)
327
+ remote_keys += (ProcessedFile() & remote_keys_all).fetch(as_dict = True)
328
+ keys = [dict(r,
329
+ local_path = Path(local_path)/r['file_path']) for r in remote_keys]
330
+ res,comparison = compare_md5s([r['local_path'] for r in keys],[r['file_md5'] for r in keys],
331
+ full_output = True,
332
+ show_progress = True,
333
+ suppress_file_not_found = True) # so if the files disappear it doesn't crash.
334
+ if not res:
335
+ print(tcolor['r'](f'{np.sum(~np.array(comparison))} local files have different checksums.'),flush = True)
336
+ for k,r in zip(keys,comparison):
337
+ if not r:
338
+ print(tcolor['r'](f" \t {k['local_path']}"),flush = True)
339
+ keep = []
340
+ deleted = []
341
+ for k,r in zip(keys,comparison):
342
+ if not r:
343
+ keep.append(k)
344
+ else:
345
+ if not dry_run:
346
+ os.unlink(k['local_path'])
347
+ deleted.append(k['local_path'])
348
+ # check if the "deleted" folders are empty
349
+ for f in deleted:
350
+ print(f.parent)
351
+ return deleted,keep
@@ -0,0 +1,78 @@
1
+ '''Upload rules for lab data management.
2
+
3
+ This module provides rules for processing data uploads to the database.
4
+ Rules handle validation, preprocessing, and storage of different data types.
5
+
6
+ Default rules included:
7
+ - UploadRule: Base rule for generic file uploads
8
+ - EphysRule: Rule for electrophysiology data (SpikeGLX)
9
+ - TwoPhotonRule: Rule for two-photon microscopy data (ScanImage/Scanbox)
10
+ - OnePhotonRule: Rule for one-photon imaging data (Widefield - labcams)
11
+ - MiniscopeRule: Rule for (UCLA) Miniscope imaging data
12
+ - FixedBrainRule: Rule for fixed tissue microscopy data
13
+ - ReplaceRule: Rule for replacing existing files
14
+
15
+ Custom rules can be added to the user_preferences.json configuration.
16
+ '''
17
+
18
+ from .utils import *
19
+ from .ephys import *
20
+ from .imaging import *
21
+
22
+ rulesmap = dict(none = UploadRule,
23
+ ephys = EphysRule,
24
+ two_photon = TwoPhotonRule,
25
+ one_photon = OnePhotonRule,
26
+ fixed_brain = FixedBrainRule,
27
+ miniscope = MiniscopeRule,
28
+ replace = ReplaceRule)
29
+
30
+ if 'upload_rules' in prefs.keys():
31
+ for k in prefs['upload_rules']:
32
+ # TODO: this may need a special field for imports?
33
+ rulesmap[k] = eval(prefs['upload_rules'][k])
34
+
35
+ def process_upload_jobs(key = None,
36
+ rule = 'all',
37
+ n_jobs = 8,
38
+ job_host = None,
39
+ force = False,
40
+ prefs = None):
41
+ '''
42
+ Process UploadJobs using UploadRule(s).
43
+ Custom upload rules can be added as plugins, just include in prefs['upload_rules']
44
+
45
+ Joao Couto - labdata 2024
46
+ '''
47
+ from tqdm import tqdm
48
+ from ..schema import UploadJob
49
+ def _job(j,rule = None, prefs = None):
50
+ jb = (UploadJob() & f'job_id = {j}').fetch()
51
+ if len(jb):
52
+ if jb[0]['job_rule'] in rulesmap.keys():
53
+ rl = rulesmap[jb[0]['job_rule']](j, prefs = prefs)
54
+ else:
55
+ rl = UploadRule(j,prefs = prefs)
56
+ res = rl.apply()
57
+ return res
58
+ if key is None:
59
+ jobs = (UploadJob() & 'job_waiting = 1').fetch('job_id')
60
+ if not job_host is None:
61
+ if job_host == 'self':
62
+ job_host = prefs['hostname']
63
+ jobs = (UploadJob() &
64
+ 'job_waiting = 1' &
65
+ f'job_host = "{job_host}"').fetch('job_id')
66
+ else:
67
+ jobs = (UploadJob() & key).fetch('job_id')
68
+ if force:
69
+ for j in jobs:
70
+ UploadJob().update1(dict(job_id = j, job_waiting = 1)) # reset job
71
+ if len(jobs) == 1:
72
+ res = [_job(jobs[0], rule = rule, prefs = prefs)]
73
+ else:
74
+ res = Parallel(backend='loky',n_jobs = n_jobs)(delayed(_job)(u,
75
+ rule = rule,
76
+ prefs = prefs)
77
+ for u in tqdm(jobs,desc = "Running upload jobs: "))
78
+ return res
labdata/rules/ephys.py ADDED
@@ -0,0 +1,188 @@
1
+ from .utils import *
2
+
3
+ class EphysRule(UploadRule):
4
+ def __init__(self, job_id,prefs = None):
5
+ super(EphysRule,self).__init__(job_id = job_id,prefs = prefs)
6
+ self.rule_name = 'ephys'
7
+ self.max_concurrent = 2
8
+
9
+ def _apply_rule(self):
10
+ # compress ap files or lf files.
11
+ files_to_compress = list(filter(lambda x: ('.ap.bin' in x) or
12
+ ('.lf.bin' in x),
13
+ self.src_paths.src_path.values))
14
+ n_jobs = DEFAULT_N_JOBS
15
+ # compress these in parallel, will work for multiprobe sessions faster?
16
+ if len(files_to_compress): # in some cases data might have already been compressed
17
+ self.set_job_status(job_status = 'WORKING',
18
+ job_log = datetime.now().strftime(f'Compressing {len(files_to_compress)} files [%Y %m %d %H:%M:%S]'))
19
+ res = Parallel(n_jobs = n_jobs)(delayed(compress_ephys_file)(
20
+ filename,
21
+ local_path = self.local_path,
22
+ n_jobs = n_jobs,
23
+ prefs = self.prefs) for filename in files_to_compress)
24
+ new_files = np.stack(res).flatten() # stack the resulting files and add them to the path
25
+ self.set_job_status(job_status = 'WORKING',
26
+ job_log = datetime.now().strftime(f'Compressed {len(files_to_compress)} files [%Y %m %d %H:%M:%S]'))
27
+ self._handle_processed_and_src_paths(files_to_compress, new_files)
28
+ return self.src_paths
29
+
30
+ def _post_upload(self):
31
+ if not self.dataset_key is None:
32
+ from ..schema import EphysRecording
33
+ EphysRecording().add_spikeglx_recording(self.dataset_key)
34
+ EphysRecording().add_nidq_events(self.dataset_key)
35
+
36
+ ############################################################################################################
37
+ ############################################################################################################
38
+
39
+
40
+ def compress_ephys_file(filename, local_path = None,
41
+ ext = '.bin',
42
+ n_jobs = DEFAULT_N_JOBS,
43
+ check_after_compress = True,
44
+ prefs = None):
45
+ '''
46
+ Compress ephys data
47
+ '''
48
+ if prefs is None:
49
+ from ..utils import prefs
50
+ if local_path is None:
51
+ local_path = prefs['local_paths'][0]
52
+ local_path = Path(local_path)
53
+
54
+ from spks.spikeglx_utils import read_spikeglx_meta
55
+
56
+ binfile = local_path/filename
57
+ if not binfile.exists():
58
+ raise OSError(f'Could not find binfile to compress ephys {binfile}')
59
+
60
+ metafile = local_path/str(filename).replace(ext,'.meta')
61
+ if not metafile.exists():
62
+ raise OSError(f'Could not find metafile to compress ephys {metafile}')
63
+
64
+ meta = read_spikeglx_meta(metafile) # to get the sampling rate and nchannels
65
+ srate = meta['sRateHz']
66
+ nchannels = meta['nSavedChans']
67
+ from mtscomp import compress, decompress
68
+ # Compress a .bin file into a pair .cbin (compressed binary file) and .ch (JSON file).
69
+ cbin,ch = (str(binfile).replace(ext,'.cbin'),str(binfile).replace(ext,'.ch'))
70
+ compress(binfile, cbin, ch,
71
+ sample_rate = srate, n_channels = int(nchannels),
72
+ check_after_compress = check_after_compress,
73
+ chunk_duration = 1, dtype=np.int16, n_threads = n_jobs)
74
+ return cbin.replace(str(local_path),'').strip(pathlib.os.sep),ch.replace(str(local_path),'').strip(pathlib.os.sep)
75
+
76
+
77
+ def get_probe_configuration(meta):
78
+ '''
79
+ Meta can be a file or a dictionary.
80
+ Uses spks for now to parse the metadata.
81
+ '''
82
+ if not hasattr(meta,'keys'):
83
+ meta = Path(meta)
84
+ if not meta.exists():
85
+ raise OSError(f'File not found: {meta}')
86
+ try:
87
+ from spks.spikeglx_utils import read_spikeglx_meta
88
+ # TODO: consider porting a minimal version over
89
+ except:
90
+ raise OSError('Could not import spks: install from https://github.com/spkware/spks')
91
+ meta = read_spikeglx_meta(meta)
92
+
93
+ probe_type = str(int(meta['imDatPrb_type']))
94
+ recording_software = 'spikeglx' # make this work with openephys also
95
+ return dict(probe_id = str(int(meta['imDatPrb_sn'])),
96
+ recording_software = recording_software,
97
+ recording_duration = meta['fileTimeSecs'],
98
+ sampling_rate = meta['sRateHz'],
99
+ probe_type = probe_type,
100
+ probe_n_shanks = 4 if probe_type in ['24','2013','2014'] else 1,
101
+ probe_gain = meta['conversion_factor_microV'][0],
102
+ probe_n_channels = len(meta['channel_idx']),
103
+ channel_idx = meta['channel_idx'],
104
+ channel_coords = meta['coords'],
105
+ channel_shank = meta['channel_shank'],
106
+ probe_recording_channels = int(meta['nSavedChans']-1))
107
+
108
+
109
+ def ephys_noise_statistics_from_file(filepath,channel_indices, gain, sampling_rate = 30000, duration = 60):
110
+ '''
111
+ statistics = ephys_noise_statistics_from_file(filepath,channel_indices, gain, sampling_rate = 30000, duration = 60)
112
+
113
+ Gets the noise statistics from a raw data file. It won't parse the whole file, instead it will extract 2 chunks, one
114
+ from t=duration to t=duration*2 and another from t=end of recording-duration*2 to t=end of recording-duration.
115
+ Then computes: the peak to peak, min, max, median and absolute median deviation of those chunks.
116
+
117
+ This is useful just to compare the start and end of the recording or to have ballpark estimations of these values.
118
+ For more accurate measurements split the recording in chunks of e.g. 1 second, compute it for the entire file, then average and std.
119
+ This will max if there are artifacts in the chunks.
120
+
121
+ Joao Couto - labdata 2024
122
+ '''
123
+
124
+ filepath = Path(filepath)
125
+ if str(filepath).endswith('.cbin'):
126
+ from mtscomp import decompress
127
+ data = decompress(filepath) #,filepath.with_suffix('.ch'))
128
+ elif str(filepath).endswith('.bin'):
129
+ from spks.spikeglx_utils import load_spikeglx_binary
130
+ data,meta = load_spikeglx_binary(filepath)
131
+ else:
132
+ raise ValueError(f'Could not handle extension: {filepath}')
133
+ if data.shape[0]/sampling_rate < duration:
134
+ duration = np.floor((data.shape[0]/sampling_rate)/2)
135
+ # read the head and tail data
136
+ head_data = np.array(data[int(sampling_rate*duration):int(sampling_rate*duration)*2],dtype=np.float32)*gain
137
+ tail_data = np.array(data[-int(sampling_rate*duration)*2:-int(sampling_rate*duration)],dtype = np.float32)*gain
138
+ dd = [head_data,tail_data]
139
+ res = dict(channel_peak_to_peak = np.zeros((len(channel_indices),len(dd))),
140
+ channel_median = np.zeros((len(channel_indices),len(dd))),
141
+ channel_mad = np.zeros((len(channel_indices),len(dd))),
142
+ channel_max = np.zeros((len(channel_indices),len(dd))),
143
+ channel_min = np.zeros((len(channel_indices),len(dd))))
144
+ from scipy.stats import median_abs_deviation
145
+ for i,d in enumerate(dd):
146
+ res['channel_mad'][:,i] = median_abs_deviation(d[:,channel_indices],axis = 0)
147
+ res['channel_max'][:,i] = np.max(d[:,channel_indices],axis = 0)
148
+ res['channel_min'][:,i] = np.min(d[:,channel_indices],axis = 0)
149
+ res['channel_median'][:,i] = np.median(d[:,channel_indices],axis = 0)
150
+ res['channel_peak_to_peak'][:,i] = res['channel_max'][:,i]-res['channel_min'][:,i]
151
+ return res
152
+
153
+ def extract_events_from_nidq(paths):
154
+ '''
155
+ Extracts the events from the nidq files (spikeglx) and formats it so they can be inserted in the database.
156
+ '''
157
+ from spks.spikeglx_utils import load_spikeglx_binary,read_spikeglx_meta
158
+ from spks.sync import unpackbits_gpu
159
+
160
+ nidqfilepath = list(filter(lambda x: '.bin' in x.suffix ,paths))
161
+ if not len(nidqfilepath):
162
+ # didnt find the files, try compressed
163
+ nidqfilepath = list(filter(lambda x: '.cbin' in x.suffix ,paths))
164
+ if not len(nidqfilepath):
165
+ raise(OSError(f'Could not find the nidaq file path for {paths}.'))
166
+ nidqfilepath = nidqfilepath[0]
167
+ # load the nidq file
168
+ if str(nidqfilepath).endswith('.bin'):
169
+ dat,meta = load_spikeglx_binary(nidqfilepath)
170
+ else:
171
+ from mtscomp import decompress
172
+ dat = decompress(nidqfilepath)
173
+ meta = read_spikeglx_meta(nidqfilepath.with_suffix('.meta'))
174
+ # read the sync channel
175
+ onsets,offsets = unpackbits_gpu(dat[:,-1])
176
+
177
+ stream_name = 'nidq'
178
+ events = []
179
+ for k in onsets.keys():
180
+ o = onsets[k]
181
+ f = offsets[k]
182
+ ts = np.hstack([o,f])
183
+ v = np.hstack([np.array(o)*0+1,np.array(f)*0]).astype('uint8')
184
+ ii = np.argsort(ts)
185
+ events.append(dict(event_name = k,
186
+ event_timestamps = ts[ii]/meta['sRateHz'], # in seconds
187
+ event_values = v[ii])) # store the onsets and offsets
188
+ return events, dat