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
labdata/utils.py
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import re # can be removed?
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from io import StringIO
|
|
11
|
+
from glob import glob
|
|
12
|
+
from natsort import natsorted
|
|
13
|
+
import hashlib
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
import pathlib
|
|
16
|
+
from joblib import delayed, Parallel
|
|
17
|
+
import pickle
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
LABDATA_PATH = Path.home()/Path('labdata')
|
|
21
|
+
if 'LABDATA_PATH' in os.environ.keys():
|
|
22
|
+
LABDATA_PATH = Path(os.environ['LABDATA_PATH'])
|
|
23
|
+
LABDATA_FILE = LABDATA_PATH/'user_preferences.json'
|
|
24
|
+
|
|
25
|
+
DEFAULT_N_JOBS = 8
|
|
26
|
+
if 'SLURM_CPUS_ON_NODE' in os.environ.keys():
|
|
27
|
+
DEFAULT_N_JOBS = int(os.environ['SLURM_CPUS_ON_NODE'])
|
|
28
|
+
if 'LABDATA_N_JOBS' in os.environ.keys():
|
|
29
|
+
DEFAULT_N_JOBS = int(os.environ['LABDATA_N_JOBS'])
|
|
30
|
+
|
|
31
|
+
# dataset_type part of Dataset()
|
|
32
|
+
dataset_type_names = ['task-training',
|
|
33
|
+
'task-behavior',
|
|
34
|
+
'passive-spontaneous',
|
|
35
|
+
'passive-stimulation',
|
|
36
|
+
'free-behavior',
|
|
37
|
+
'imaging-2p',
|
|
38
|
+
'imaging-widefield',
|
|
39
|
+
'imaging-miniscope',
|
|
40
|
+
'ephys',
|
|
41
|
+
'opto-inactivation',
|
|
42
|
+
'opto-activation',
|
|
43
|
+
'analysis']
|
|
44
|
+
# The following dictionary describes the equivalency between datatype_name and dataset_type (broadly)
|
|
45
|
+
# todo: flip this the other way and split the types with slash or so.
|
|
46
|
+
dataset_name_equivalence = dict(ephys ='ephys',
|
|
47
|
+
task = 'task-training',
|
|
48
|
+
two_photon = 'imaging-2p',
|
|
49
|
+
one_photon = 'imaging-widefield',
|
|
50
|
+
analysis = 'analysis')
|
|
51
|
+
|
|
52
|
+
default_analysis = dict(spks = 'labdata.compute.SpksCompute',
|
|
53
|
+
deeplabcut = 'labdata.compute.DeeplabcutCompute')
|
|
54
|
+
|
|
55
|
+
default_remote_description = dict(scheduler = 'slurm',
|
|
56
|
+
user = 'ec2-user',
|
|
57
|
+
permission_key = 'gpu-cluster.pem',
|
|
58
|
+
address = 'ec2-xx-xx-xx-xx.us-west-2.compute.amazonaws.com',
|
|
59
|
+
analysis_queue = dict(spks = dict(queue='gpu-large',
|
|
60
|
+
ncpus = 16)), # where to run each
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
default_labdata_preferences = dict(local_paths = [str(Path.home()/'data')],
|
|
64
|
+
scratch_path = str(Path.home()/'scratch'),
|
|
65
|
+
path_rules='{subject_name}/{session_name}/{dataset_name}', # to read the session/dataset from a path
|
|
66
|
+
queues= None,
|
|
67
|
+
allow_s3_download = False,
|
|
68
|
+
compute = dict(
|
|
69
|
+
remotes = dict(), # these are to connect
|
|
70
|
+
aws=dict(access_key = None,
|
|
71
|
+
secret_key = None,
|
|
72
|
+
region = 'us-west-2',
|
|
73
|
+
security_groups = [],
|
|
74
|
+
# check the instructions for how to create the AMI
|
|
75
|
+
image_ids = dict(linux = dict(ami = 'ami-0dae8f262f0886c24',
|
|
76
|
+
user = 'ubuntu')),
|
|
77
|
+
access_key_folder = str(Path.home()/Path('labdata')/'ec2keys')),
|
|
78
|
+
containers = dict(
|
|
79
|
+
local = str(Path.home()/Path('labdata')/'containers'),
|
|
80
|
+
storage = 'analysis'), # place to store on s3
|
|
81
|
+
analysis = default_analysis,
|
|
82
|
+
default_target = 'slurm'),
|
|
83
|
+
storage = dict(data = dict(protocol = 's3',
|
|
84
|
+
endpoint = 's3.amazonaws.com:9000',
|
|
85
|
+
bucket = 'churchland-ucla-data',
|
|
86
|
+
folder = '',
|
|
87
|
+
access_key = None,
|
|
88
|
+
secret_key = None),
|
|
89
|
+
analysis = dict(protocol = 's3',
|
|
90
|
+
endpoint = 's3.amazonaws.com:9000',
|
|
91
|
+
bucket = 'churchland-ucla-analysis',
|
|
92
|
+
folder = '',
|
|
93
|
+
access_key = None,
|
|
94
|
+
secret_key = None)),
|
|
95
|
+
database = {
|
|
96
|
+
'database.host':'churchland-ucla-data.cxis684q8epg.us-west-1.rds.amazonaws.com',
|
|
97
|
+
'database.user': None,
|
|
98
|
+
'database.password': None,
|
|
99
|
+
'database.name': 'lab_data'},
|
|
100
|
+
plugins_folder = str(Path.home()/
|
|
101
|
+
Path('labdata')/'plugins'), # this can be removed?
|
|
102
|
+
submit_defaults = None,
|
|
103
|
+
run_defaults = {'delete-cache':False},
|
|
104
|
+
upload_path = None, # this is the path to the local computer that writes to s3
|
|
105
|
+
upload_storage = None) # which storage to upload to
|
|
106
|
+
|
|
107
|
+
tcolor = dict(r = lambda s: f'\033[91m{s}\033[0m',
|
|
108
|
+
g = lambda s: f'\033[92m{s}\033[0m',
|
|
109
|
+
y = lambda s: f'\033[93m{s}\033[0m',
|
|
110
|
+
b = lambda s: f'\033[94m{s}\033[0m',
|
|
111
|
+
m = lambda s: f'\033[95m{s}\033[0m',
|
|
112
|
+
c = lambda s: f'\033[96m{s}\033[0m',
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def get_labdata_preferences(prefpath = None):
|
|
116
|
+
'''Reads and returns the labdata preferences from a JSON file.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
prefpath : str or Path, optional
|
|
121
|
+
Path to the preferences JSON file. If None, uses default location in user's home directory ($HOME/labdata/user_preferences.json).
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
dict
|
|
126
|
+
Dictionary containing the labdata preferences.
|
|
127
|
+
|
|
128
|
+
Notes
|
|
129
|
+
-----
|
|
130
|
+
The preferences file contains settings for:
|
|
131
|
+
- Data storage locations (local and remote)
|
|
132
|
+
- Compute configurations (AWS, containers, etc)
|
|
133
|
+
- Database connection details
|
|
134
|
+
- Default analysis parameters
|
|
135
|
+
- Upload paths and storage
|
|
136
|
+
|
|
137
|
+
If the preferences file doesn't exist, creates one with default settings.
|
|
138
|
+
Environment variable LABDATA_OVERRIDE_DATAPATH can be used to override paths.
|
|
139
|
+
'''
|
|
140
|
+
|
|
141
|
+
if prefpath is None:
|
|
142
|
+
prefpath = LABDATA_FILE
|
|
143
|
+
prefpath = Path(prefpath) # needs to be a file
|
|
144
|
+
preffolder = prefpath.parent
|
|
145
|
+
if not preffolder.exists():
|
|
146
|
+
preffolder.mkdir(parents=True,exist_ok = True)
|
|
147
|
+
if not prefpath.exists():
|
|
148
|
+
save_labdata_preferences(default_labdata_preferences, prefpath)
|
|
149
|
+
with open(prefpath, 'r') as infile:
|
|
150
|
+
pref = json.load(infile)
|
|
151
|
+
for k in default_labdata_preferences:
|
|
152
|
+
if not k in pref.keys():
|
|
153
|
+
pref[k] = default_labdata_preferences[k]
|
|
154
|
+
from socket import gethostname
|
|
155
|
+
pref['hostname'] = gethostname()
|
|
156
|
+
if 'LABDATA_OVERRIDE_DATAPATH' in os.environ.keys():
|
|
157
|
+
# then override the paths set in the preference file.
|
|
158
|
+
overpath = os.environ['LABDATA_OVERRIDE_DATAPATH']
|
|
159
|
+
print(f'Overriding local_paths and scratch_path. [{overpath}]')
|
|
160
|
+
pref['scratch_path'] = Path(overpath)
|
|
161
|
+
pref['local_paths'] = [Path(overpath)]
|
|
162
|
+
if 'compute' in pref.keys():
|
|
163
|
+
if 'analysis' in pref['compute'].keys():
|
|
164
|
+
if not 'populate' in pref['compute']['analysis'].keys():
|
|
165
|
+
pref['compute']['analysis'] = dict(pref['compute']['analysis'],
|
|
166
|
+
populate = 'labdata.compute.PopulateCompute')
|
|
167
|
+
for k in default_analysis.keys(): # add analysis that ship out of the box with labdata
|
|
168
|
+
if not k in pref['compute']['analysis'].keys():
|
|
169
|
+
pref['compute']['analysis'][k] = default_analysis[k]
|
|
170
|
+
return pref
|
|
171
|
+
|
|
172
|
+
def save_labdata_preferences(preferences, prefpath):
|
|
173
|
+
'''Saves labdata preferences to a JSON file.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
preferences : dict
|
|
178
|
+
Dictionary containing labdata preferences to save
|
|
179
|
+
prefpath : str or Path
|
|
180
|
+
Path to save the preferences JSON file to
|
|
181
|
+
|
|
182
|
+
Notes
|
|
183
|
+
-----
|
|
184
|
+
Saves preferences as formatted JSON with sorted keys and 4-space indentation.
|
|
185
|
+
Prints confirmation message with save location.
|
|
186
|
+
'''
|
|
187
|
+
with open(prefpath, 'w') as outfile:
|
|
188
|
+
json.dump(preferences,
|
|
189
|
+
outfile,
|
|
190
|
+
sort_keys = True,
|
|
191
|
+
indent = 4)
|
|
192
|
+
print(f'Saving default preferences to: {prefpath}')
|
|
193
|
+
|
|
194
|
+
prefs = get_labdata_preferences()
|
|
195
|
+
|
|
196
|
+
def validate_num_jobs_joblib(num_jobs):
|
|
197
|
+
'''Helper function to handle number of parallel jobs in joblib.
|
|
198
|
+
|
|
199
|
+
When running inside a joblib child process, forces num_jobs=1 to prevent nested parallelism.
|
|
200
|
+
Otherwise returns the input num_jobs value unchanged.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
num_jobs : int
|
|
205
|
+
Number of parallel jobs requested
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
int
|
|
210
|
+
1 if running in joblib child process, otherwise returns num_jobs unchanged
|
|
211
|
+
'''
|
|
212
|
+
if "JOBLIB_CHILD_PID" in os.environ:
|
|
213
|
+
return 1
|
|
214
|
+
else:
|
|
215
|
+
return num_jobs
|
|
216
|
+
##########################################################
|
|
217
|
+
##########################################################
|
|
218
|
+
|
|
219
|
+
def parse_filepath_parts(path,
|
|
220
|
+
local_path = None,
|
|
221
|
+
path_rules=None,
|
|
222
|
+
session_date_rules = ['%Y%m%d_%H%M%S']):
|
|
223
|
+
'''Parse filepath into component parts based on path rules.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
path : str or Path
|
|
228
|
+
Filepath to parse
|
|
229
|
+
local_path : str or Path, optional
|
|
230
|
+
Base path to remove from filepath. If None, uses first path from preferences.
|
|
231
|
+
path_rules : str, optional
|
|
232
|
+
Forward slash separated string defining path components, e.g. "{subject}/{session}/{dataset}".
|
|
233
|
+
If None, uses rules from preferences.
|
|
234
|
+
session_date_rules : list of str, optional
|
|
235
|
+
List of datetime format strings for parsing session names into datetimes.
|
|
236
|
+
Default is ['%Y%m%d_%H%M%S'].
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
dict
|
|
241
|
+
Dictionary containing parsed path components. Keys are the component names from path_rules.
|
|
242
|
+
May include additional keys:
|
|
243
|
+
- session_datetime: datetime object if session_name was parsed successfully
|
|
244
|
+
- dataset_type: standardized dataset type if dataset_name matches known types
|
|
245
|
+
|
|
246
|
+
Notes
|
|
247
|
+
-----
|
|
248
|
+
Removes local_path prefix from input path, then splits remaining path on directory separators.
|
|
249
|
+
Components are matched to path_rules in order.
|
|
250
|
+
Special handling for session_name to parse into datetime and dataset_name to standardize type.
|
|
251
|
+
'''
|
|
252
|
+
|
|
253
|
+
if path_rules is None:
|
|
254
|
+
path_rules = prefs['path_rules']
|
|
255
|
+
if local_path is None:
|
|
256
|
+
local_path = prefs['local_paths'][0]
|
|
257
|
+
parts = str(path).replace(local_path,'').split(pathlib.os.sep)
|
|
258
|
+
names = [f.strip('}').strip('{') for f in path_rules.split('/')]
|
|
259
|
+
t = dict()
|
|
260
|
+
for i,n in enumerate(names):
|
|
261
|
+
t[n] = parts[i]
|
|
262
|
+
if 'session_name' in t.keys():
|
|
263
|
+
t['session_datetime'] = datetime.strptime(t['session_name'],session_date_rules[0])
|
|
264
|
+
if 'dataset_name' in t.keys():
|
|
265
|
+
for k in dataset_name_equivalence.keys():
|
|
266
|
+
if k in t['dataset_name'].lower():
|
|
267
|
+
t['dataset_type'] = dataset_name_equivalence[k]
|
|
268
|
+
break # found it..
|
|
269
|
+
return t
|
|
270
|
+
|
|
271
|
+
def drop_local_path(filepaths,local_path = None):
|
|
272
|
+
if local_path is None:
|
|
273
|
+
local_path = prefs['local_paths'][0]
|
|
274
|
+
if type(filepaths) is Path:
|
|
275
|
+
filepaths = [filepaths]
|
|
276
|
+
clean_filepaths = [str(Path(f)).replace(str(local_path),'') for f in filepaths]
|
|
277
|
+
# remove trailing / or \
|
|
278
|
+
clean_filepaths = [f if not f.startswith(pathlib.os.sep) else f[1:] for f in clean_filepaths]
|
|
279
|
+
# make unique
|
|
280
|
+
clean_filepaths = [f for f in np.unique(clean_filepaths)]
|
|
281
|
+
return clean_filepaths
|
|
282
|
+
##########################################################
|
|
283
|
+
##########################################################
|
|
284
|
+
|
|
285
|
+
def compute_md5_hash(fname,suppress_file_not_found = False):
|
|
286
|
+
'''
|
|
287
|
+
Computes the md5 hash that can be used to check file integrity
|
|
288
|
+
'''
|
|
289
|
+
hash_md5 = hashlib.md5()
|
|
290
|
+
if not Path(fname).exists():
|
|
291
|
+
if suppress_file_not_found:
|
|
292
|
+
print(f'File {fname} not found while computing md5 hash.')
|
|
293
|
+
return -1
|
|
294
|
+
else:
|
|
295
|
+
raise(OSError(f'File {fname} not found while computing md5 hash.'))
|
|
296
|
+
with open(fname, "rb") as f:
|
|
297
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
298
|
+
hash_md5.update(chunk)
|
|
299
|
+
return hash_md5.hexdigest()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def compute_md5s(filepaths,n_jobs = DEFAULT_N_JOBS, show_progress = False,suppress_file_not_found = False):
|
|
303
|
+
'''
|
|
304
|
+
Computes the checksums for multiple files in parallel
|
|
305
|
+
'''
|
|
306
|
+
if show_progress:
|
|
307
|
+
from tqdm import tqdm
|
|
308
|
+
return Parallel(n_jobs = n_jobs)(delayed(compute_md5_hash)(filepath, suppress_file_not_found = suppress_file_not_found)
|
|
309
|
+
for filepath in tqdm(filepaths,desc = 'Computing md5 checksums:',
|
|
310
|
+
total = len(filepaths)))
|
|
311
|
+
return Parallel(n_jobs = n_jobs)(delayed(compute_md5_hash)(filepath) for filepath in filepaths)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def compare_md5s(paths,checksums, n_jobs = DEFAULT_N_JOBS, full_output = False, show_progress = False, suppress_file_not_found = False):
|
|
315
|
+
'''
|
|
316
|
+
Computes the checksums for multiple files in parallel
|
|
317
|
+
'''
|
|
318
|
+
localchecksums = compute_md5s(paths, n_jobs = n_jobs,show_progress = show_progress,suppress_file_not_found = suppress_file_not_found)
|
|
319
|
+
res = [False]*len(paths)
|
|
320
|
+
assert len(paths) == len(checksums), ValueError('Checksums not the same size as paths.')
|
|
321
|
+
for ipath,(local,check) in enumerate(zip(localchecksums,checksums)):
|
|
322
|
+
res[ipath] = local == check
|
|
323
|
+
if full_output:
|
|
324
|
+
return all(res),res
|
|
325
|
+
return all(res)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_filepaths(keys,local_paths = None, download = False):
|
|
329
|
+
'''
|
|
330
|
+
Returns the local path to files and downloads the files if needed.
|
|
331
|
+
'''
|
|
332
|
+
path = keys.file_path
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
def find_local_filepath(path,allowed_extensions = [],local_paths = None):
|
|
336
|
+
'''
|
|
337
|
+
Search for a file in local paths and return the path.
|
|
338
|
+
This function exists so that files can be distributed in different file systems.
|
|
339
|
+
List the local paths (i.e. the different filesystems) in labdata/user_preferences.json
|
|
340
|
+
|
|
341
|
+
allowed_extensions can be used to find similar extensions
|
|
342
|
+
(e.g. when files are compressed and you want to find the original file)
|
|
343
|
+
|
|
344
|
+
localpath = find_local_filepath(path, allowed_extensions = ['.ap.bin'])
|
|
345
|
+
|
|
346
|
+
Joao Couto - labdata 2024
|
|
347
|
+
'''
|
|
348
|
+
if local_paths is None:
|
|
349
|
+
local_paths = prefs['local_paths']
|
|
350
|
+
|
|
351
|
+
for p in local_paths:
|
|
352
|
+
p = Path(p)/path
|
|
353
|
+
if p.exists():
|
|
354
|
+
return p # return when you find the file
|
|
355
|
+
for ex in allowed_extensions:
|
|
356
|
+
p = (p.parent/p.stem).with_suffix(ex)
|
|
357
|
+
if p.exists():
|
|
358
|
+
return p # found allowed extensions (use this to find ProcessedFiles)
|
|
359
|
+
return None # file not found
|
|
360
|
+
|
|
361
|
+
def plugin_lazy_import(name):
|
|
362
|
+
'''
|
|
363
|
+
Lazy import function to load the plugins.
|
|
364
|
+
'''
|
|
365
|
+
import importlib.util
|
|
366
|
+
spec = importlib.util.spec_from_file_location(name, str(Path(prefs['plugins'][name])/"__init__.py"))
|
|
367
|
+
loader = importlib.util.LazyLoader(spec.loader)
|
|
368
|
+
spec.loader = loader
|
|
369
|
+
module = importlib.util.module_from_spec(spec)
|
|
370
|
+
sys.modules[name] = module
|
|
371
|
+
loader.exec_module(module)
|
|
372
|
+
return module
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def extrapolate_time_from_clock(master_clock,master_events, slave_events):
|
|
376
|
+
'''
|
|
377
|
+
Extrapolates the time for synchronizing events on different streams
|
|
378
|
+
'''
|
|
379
|
+
from scipy.interpolate import interp1d
|
|
380
|
+
return interp1d(master_events, master_clock, fill_value='extrapolate')(slave_events)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def save_dict_to_h5(filename,dictionary,compression = 'gzip', compression_opts = 1, compression_size_threshold = 1000):
|
|
384
|
+
'''
|
|
385
|
+
Save a dictionary as a compressed hdf5 dataset.
|
|
386
|
+
filename: path to the file (IMPORTANT: this WILL overwrite without checks.)
|
|
387
|
+
dictionary: the dictionary to save
|
|
388
|
+
|
|
389
|
+
If the size of the data are larger than compression_size_threshold it will save with compression.
|
|
390
|
+
default compression is gzip, can also use lzf
|
|
391
|
+
|
|
392
|
+
Joao Couto - 2023
|
|
393
|
+
'''
|
|
394
|
+
def _save_dataset(f,key,val,
|
|
395
|
+
compression = compression,
|
|
396
|
+
compression_size_threshold = compression_size_threshold):
|
|
397
|
+
# compress if big enough.
|
|
398
|
+
|
|
399
|
+
if np.size(val)>compression_size_threshold:
|
|
400
|
+
extras = dict(compression = compression,
|
|
401
|
+
chunks = True,
|
|
402
|
+
shuffle = True)
|
|
403
|
+
if compression == 'gzip':
|
|
404
|
+
extras['compression_opts'] = compression_opts
|
|
405
|
+
else:
|
|
406
|
+
extras = dict()
|
|
407
|
+
f.create_dataset(str(key),data = val, **extras)
|
|
408
|
+
|
|
409
|
+
import h5py
|
|
410
|
+
keys = []
|
|
411
|
+
values = []
|
|
412
|
+
for k in dictionary.keys():
|
|
413
|
+
if not type(dictionary[k]) in [dict]:
|
|
414
|
+
keys.append(k)
|
|
415
|
+
values.append(dictionary[k])
|
|
416
|
+
else:
|
|
417
|
+
for o in dictionary[k].keys():
|
|
418
|
+
keys.append(k+'/'+str(o))
|
|
419
|
+
values.append(dictionary[k][o])
|
|
420
|
+
filename = Path(filename)
|
|
421
|
+
# create file, this will overwrite without asking.
|
|
422
|
+
from tqdm import tqdm
|
|
423
|
+
with h5py.File(filename,'w') as f:
|
|
424
|
+
for k,v in tqdm(zip(keys,values),total = len(keys),desc = f"Saving to hdf5 {filename.name}"):
|
|
425
|
+
_save_dataset(f = f,key = k,val = v)
|
|
426
|
+
|
|
427
|
+
def format_localpath_to_db(filepaths,local_path = None):
|
|
428
|
+
'''
|
|
429
|
+
Remove the local_path from a list of filepaths, the path will be the same as
|
|
430
|
+
'''
|
|
431
|
+
if local_path is None:
|
|
432
|
+
local_path = prefs['local_paths'][0]
|
|
433
|
+
if type(filepaths) is Path:
|
|
434
|
+
filepaths = [filepaths]
|
|
435
|
+
clean_filepaths = [str(Path(f)).replace(str(local_path),'') for f in filepaths]
|
|
436
|
+
# remove trailing / or \
|
|
437
|
+
clean_filepaths = [f if not f.startswith(pathlib.os.sep) else f[1:] for f in clean_filepaths]
|
|
438
|
+
# make unique
|
|
439
|
+
clean_filepaths = [f for f in np.unique(clean_filepaths)]
|
|
440
|
+
# convert \ to / TODO: needs to be tested in windows..
|
|
441
|
+
return clean_filepaths
|
|
442
|
+
|
|
443
|
+
def load_dict_from_h5(filename):
|
|
444
|
+
'''
|
|
445
|
+
Loads a dictionary from hdf5 file.
|
|
446
|
+
|
|
447
|
+
This is also in github/spkware/spks
|
|
448
|
+
|
|
449
|
+
Joao Couto - spks 2023
|
|
450
|
+
|
|
451
|
+
'''
|
|
452
|
+
data = {}
|
|
453
|
+
import h5py
|
|
454
|
+
with h5py.File(filename,'r') as f:
|
|
455
|
+
for k in f.keys(): #TODO: read also attributes.
|
|
456
|
+
no = k
|
|
457
|
+
if no[0].isdigit():
|
|
458
|
+
no = int(k)
|
|
459
|
+
if hasattr(f[k],'dims'):
|
|
460
|
+
data[no] = f[k][()]
|
|
461
|
+
else:
|
|
462
|
+
data[no] = dict()
|
|
463
|
+
for o in f[k].keys(): # is group
|
|
464
|
+
ko = o
|
|
465
|
+
if o[0].isdigit():
|
|
466
|
+
ko = int(o)
|
|
467
|
+
data[no][ko] = f[k][o][()]
|
|
468
|
+
return data
|
|
469
|
+
|
|
470
|
+
def save_zarr_compressed_stack(stack, filename,
|
|
471
|
+
chunksize = [128],
|
|
472
|
+
compression = None,
|
|
473
|
+
clevel = 6,
|
|
474
|
+
shuffle = 1,
|
|
475
|
+
filters = [],
|
|
476
|
+
scratch_path = None,
|
|
477
|
+
check_dataset = False):
|
|
478
|
+
'''Save a numpy array to a compressed zarr file.
|
|
479
|
+
|
|
480
|
+
Parameters
|
|
481
|
+
----------
|
|
482
|
+
stack : numpy.ndarray
|
|
483
|
+
Array to compress and save (up to 4 dimensions)
|
|
484
|
+
filename : str or Path
|
|
485
|
+
Path to save the compressed zarr file
|
|
486
|
+
chunksize : list, optional
|
|
487
|
+
Size of chunks for compression, default [128]
|
|
488
|
+
compression : str, optional
|
|
489
|
+
Compression algorithm to use ('zstd' or 'blosc2'), default None uses 'zstd'
|
|
490
|
+
clevel : int, optional
|
|
491
|
+
Compression level (1-9), default 6
|
|
492
|
+
shuffle : int, optional
|
|
493
|
+
Shuffle filter to use (0=none, 1=byte, 2=bit), default 1
|
|
494
|
+
filters : list, optional
|
|
495
|
+
Additional filters to apply (e.g. ['delta']), default empty list
|
|
496
|
+
scratch_path : str or Path, optional
|
|
497
|
+
Temporary directory for compression, default uses preferences or current dir
|
|
498
|
+
check_dataset : bool, optional
|
|
499
|
+
Whether to verify compressed data matches original, default False
|
|
500
|
+
|
|
501
|
+
Returns
|
|
502
|
+
-------
|
|
503
|
+
zarr.core.Array
|
|
504
|
+
The compressed zarr array opened in read mode
|
|
505
|
+
|
|
506
|
+
Notes
|
|
507
|
+
-----
|
|
508
|
+
Creates a temporary zarr store, compresses the data in chunks, saves to a zip file,
|
|
509
|
+
then cleans up the temporary files. Optionally verifies the compressed data.
|
|
510
|
+
'''
|
|
511
|
+
|
|
512
|
+
# stack is up to four dimensional
|
|
513
|
+
import zarr
|
|
514
|
+
import string
|
|
515
|
+
from zipfile import ZipFile
|
|
516
|
+
from pathlib import Path
|
|
517
|
+
import numcodecs
|
|
518
|
+
from tqdm import tqdm
|
|
519
|
+
filt = []
|
|
520
|
+
if 'delta' in filters:
|
|
521
|
+
filt += [zarr.Delta(dtype=stack.dtype)]
|
|
522
|
+
if compression is None or compression == 'zstd':
|
|
523
|
+
compressor = zarr.Blosc(cname = 'zstd', clevel = clevel, shuffle = shuffle)
|
|
524
|
+
elif compression == 'blosc2':
|
|
525
|
+
from imagecodecs.numcodecs import Blosc2
|
|
526
|
+
numcodecs.register_codec(Blosc2)
|
|
527
|
+
compressor = Blosc2(level=clevel,shuffle = shuffle)
|
|
528
|
+
|
|
529
|
+
if scratch_path is None:
|
|
530
|
+
if 'scratch_path' in prefs:
|
|
531
|
+
scratch_path = Path(prefs['scratch_path'])
|
|
532
|
+
if scratch_path is None:
|
|
533
|
+
scratch_path = Path('.')
|
|
534
|
+
|
|
535
|
+
rand = ''.join(np.random.choice([s for s in string.ascii_lowercase + string.digits],9))
|
|
536
|
+
tmppath = Path(scratch_path/f'temporary_zarr_{rand}.zarr')
|
|
537
|
+
# create the output dir
|
|
538
|
+
filename = Path(filename)
|
|
539
|
+
filename.parent.mkdir(exist_ok = True,parents = True)
|
|
540
|
+
|
|
541
|
+
z1 = zarr.open(tmppath, mode='w', shape = stack.shape,
|
|
542
|
+
chunks = chunksize*len(stack.shape), dtype = stack.dtype,
|
|
543
|
+
compressor = compressor, filters = filt)
|
|
544
|
+
for s in tqdm(chunk_indices(len(stack),chunksize[0]),desc = 'Compressing '):
|
|
545
|
+
z1[s[0]:s[1]] = np.array(stack[s[0]:s[1]])
|
|
546
|
+
|
|
547
|
+
with ZipFile(filename,'w') as z:
|
|
548
|
+
tmp = list(tmppath.rglob('*'))
|
|
549
|
+
[z.write(t,arcname=t.name) for t in tqdm(tmp, desc='Saving to zip')]
|
|
550
|
+
# delete the temporary
|
|
551
|
+
from shutil import rmtree
|
|
552
|
+
rmtree(tmppath)
|
|
553
|
+
# open the new array
|
|
554
|
+
z1 = open_zarr(filename,mode = 'r')
|
|
555
|
+
if check_dataset:
|
|
556
|
+
# check the new array
|
|
557
|
+
for s in tqdm(chunk_indices(len(stack),chunksize[0]),desc = 'Checking data:'):
|
|
558
|
+
if not np.all(z1[s[0]:s[1]] == np.array(stack[s[0]:s[1]])):
|
|
559
|
+
raise(ValueError(f"Datasets are not equivalent, compression failed {filename}. "))
|
|
560
|
+
return z1
|
|
561
|
+
|
|
562
|
+
def chunk_indices(nframes, chunksize = 512, min_chunk_size = 16):
|
|
563
|
+
'''
|
|
564
|
+
Gets chunk indices for iterating over an array in evenly sized chunks
|
|
565
|
+
|
|
566
|
+
Joao Couto - wfield, 2020
|
|
567
|
+
'''
|
|
568
|
+
chunks = np.arange(0,nframes,chunksize,dtype = int)
|
|
569
|
+
if (nframes - chunks[-1]) < min_chunk_size:
|
|
570
|
+
chunks[-1] = nframes
|
|
571
|
+
if not chunks[-1] == nframes:
|
|
572
|
+
chunks = np.hstack([chunks,nframes])
|
|
573
|
+
return [[chunks[i],chunks[i+1]] for i in range(len(chunks)-1)]
|
|
574
|
+
|
|
575
|
+
def open_zarr(path, mode = 'r'):
|
|
576
|
+
'''
|
|
577
|
+
Open a zarr.
|
|
578
|
+
|
|
579
|
+
z1 = open_zarr(path, mode = 'r')
|
|
580
|
+
|
|
581
|
+
TODO: Make this work for remote arrays also.
|
|
582
|
+
|
|
583
|
+
'''
|
|
584
|
+
import zarr
|
|
585
|
+
import numcodecs
|
|
586
|
+
try: # load the imagecodec because it may be there
|
|
587
|
+
from imagecodecs.numcodecs import Blosc2
|
|
588
|
+
numcodecs.register_codec(Blosc2)
|
|
589
|
+
except:
|
|
590
|
+
pass # move on because it may not be needed depending on the codec
|
|
591
|
+
|
|
592
|
+
# open the new array (now supporting the new/old api - needs testing on the old API)
|
|
593
|
+
if str(path).endswith('.zip'):
|
|
594
|
+
store = zarr.storage.ZipStore(path, mode = mode)
|
|
595
|
+
else:
|
|
596
|
+
store = Path(path)
|
|
597
|
+
z1 = zarr.open(store, mode = mode)
|
|
598
|
+
return z1
|