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/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