Skip to content

Commit 6f86ce5

Browse files
Merge pull request #856 from zacsimile/fix763
2 parents 118076c + 9f0dc00 commit 6f86ce5

File tree

4 files changed

+255
-121
lines changed

4 files changed

+255
-121
lines changed

src/navigate/model/data_sources/bdv_data_source.py

+16-100
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
# Standard Imports
3333
import os
34+
from multiprocessing.managers import DictProxy
3435

3536
# Third Party Imports
3637
import h5py
@@ -41,17 +42,14 @@
4142
# Local imports
4243
from .data_source import DataSource
4344
from ..metadata_sources.bdv_metadata import BigDataViewerMetadata
44-
from multiprocessing.managers import DictProxy
45+
from ...tools.slicing import ensure_slice, ensure_iter, slice_len
4546

4647

4748
class BigDataViewerDataSource(DataSource):
4849
"""BigDataViewer data source.
4950
5051
This class is used to write data to a BigDataViewer-compatible file. It
51-
supports both HDF5 and N5 file formats. The file is written in a
52-
multi-resolution pyramid format, with each resolution level subdivided into
53-
32x32x1 blocks. The number of blocks in each dimension is determined by the
54-
shape of the data and the resolution level.
52+
supports both HDF5 and N5 file formats.
5553
"""
5654

5755
def __init__(self, file_name: str = None, mode: str = "w") -> None:
@@ -99,7 +97,7 @@ def __getitem__(self, keys):
9997
"""Magic method to get slice requests passed by, e.g., ds[:,2:3,...].
10098
Allows arbitrary slicing of dataset via calls to get_slice().
10199
102-
Order is xycztps where x, y, z are Cartesian indices, c is channel,
100+
Order is xycztps where x, y, z are array indices, c is channel,
103101
t is timepoints, p is positions and s is subdivisions to index along.
104102
105103
TODO: Add subdivisions.
@@ -120,6 +118,7 @@ def __getitem__(self, keys):
120118
length = 1
121119
else:
122120
length = len(keys)
121+
123122
if length < 1:
124123
raise IndexError(
125124
"Too few indices. Indices may be (x, y, c, z, t, p, subdiv)."
@@ -128,84 +127,19 @@ def __getitem__(self, keys):
128127
raise IndexError(
129128
"Too many indices. Indices may be (x, y, c, z, t, p, subdiv)."
130129
)
130+
131+
# Get indices as slices/ranges
132+
xs = ensure_slice(keys, 0)
133+
ys = ensure_slice(keys, 1)
134+
cs = ensure_iter(keys, 2, self.shape[2])
135+
zs = ensure_slice(keys, 3)
136+
ts = ensure_iter(keys, 4, self.shape[4])
137+
ps = ensure_iter(keys, 5, self.positions)
131138

132-
# Handle "slice the rest"
133139
if length > 1 and keys[-1] == Ellipsis:
134-
keys = keys[:-2]
140+
keys = keys[:-1]
135141
length -= 1
136142

137-
def ensure_iter(pos):
138-
"""Ensure the input is iterable.
139-
140-
Parameters
141-
----------
142-
pos : int
143-
The position.
144-
145-
Returns
146-
-------
147-
range
148-
The range.
149-
"""
150-
if length > pos:
151-
try:
152-
val = keys[pos]
153-
except TypeError:
154-
# Only one key
155-
val = keys
156-
if isinstance(val, slice):
157-
if val.start is None and val.stop is None and val.step is None:
158-
return range(self.shape[pos])
159-
return range(10**10)[val]
160-
elif isinstance(val, int):
161-
return range(val, val + 1)
162-
else:
163-
return range(self.shape[pos])
164-
165-
def ensure_slice(pos):
166-
"""Ensure the input is a slice or a single integer.
167-
168-
Parameters
169-
----------
170-
pos : int
171-
The position.
172-
173-
Returns
174-
-------
175-
slice
176-
The slice.
177-
"""
178-
# TODO: Handle list as input
179-
if length > pos:
180-
try:
181-
val = keys[pos]
182-
except TypeError:
183-
# Only one key
184-
val = keys
185-
assert isinstance(val, slice) or isinstance(val, int)
186-
return val
187-
else:
188-
# Default to all values
189-
return slice(None, None, None)
190-
191-
# Get legal indices
192-
xs = ensure_slice(0)
193-
ys = ensure_slice(1)
194-
cs = ensure_iter(2)
195-
zs = ensure_slice(3)
196-
ts = ensure_iter(4)
197-
if length > 5:
198-
val = keys[5]
199-
if isinstance(val, slice):
200-
if val.start is None and val.stop is None and val.step is None:
201-
ps = range(self.positions)
202-
else:
203-
ps = range(10**10)[val]
204-
elif isinstance(val, int):
205-
ps = range(val, val + 1)
206-
else:
207-
ps = range(self.positions)
208-
209143
if length > 6 and isinstance(keys[6], int):
210144
subdiv = keys[6]
211145
else:
@@ -214,24 +148,6 @@ def ensure_slice(pos):
214148
if len(cs) == 1 and len(ts) == 1 and len(ps) == 1:
215149
return self.get_slice(xs, ys, cs[0], zs, ts[0], ps[0], subdiv)
216150

217-
def slice_len(sl, n):
218-
"""Calculate the length of the slice over an array of size n.
219-
220-
Parameters
221-
----------
222-
sl : slice
223-
The slice.
224-
n : int
225-
The size of the array.
226-
227-
Returns
228-
-------
229-
int
230-
The length of the slice.
231-
"""
232-
sx = sl.indices(n)
233-
return (sx[1] - sx[0]) // sx[2]
234-
235151
sliced_ds = np.empty(
236152
(
237153
len(ps),
@@ -254,7 +170,7 @@ def slice_len(sl, n):
254170
return sliced_ds
255171

256172
def get_slice(self, x, y, c, z=0, t=0, p=0, subdiv=0):
257-
"""Get a single slice of the dataset.
173+
"""Get a 3D slice of the dataset for a single c, t, p, subdiv.
258174
259175
Parameters
260176
----------
@@ -524,7 +440,7 @@ def _setup_n5(self, *args, create_flag=True):
524440
"""Set up the N5 file.
525441
526442
This function creates the file and the datasets to populate. By default,
527-
it appears to implement blosc compression. Consequently, the anticipated file
443+
it implements blosc compression. Consequently, the anticipated file
528444
size, and the actual file size, do not match. This is not the case for HDF5.
529445
530446
Note

src/navigate/tools/slicing.py

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Copyright (c) 2021-2024 The University of Texas Southwestern Medical Center.
2+
# All rights reserved.
3+
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted for academic and research use only (subject to the
6+
# limitations in the disclaimer below) provided that the following conditions are met:
7+
8+
# * Redistributions of source code must retain the above copyright notice,
9+
# this list of conditions and the following disclaimer.
10+
11+
# * Redistributions in binary form must reproduce the above copyright
12+
# notice, this list of conditions and the following disclaimer in the
13+
# documentation and/or other materials provided with the distribution.
14+
15+
# * Neither the name of the copyright holders nor the names of its
16+
# contributors may be used to endorse or promote products derived from this
17+
# software without specific prior written permission.
18+
19+
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
20+
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
21+
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
23+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
24+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
27+
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
28+
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30+
# POSSIBILITY OF SUCH DAMAGE.
31+
32+
def slice_len(sl, n):
33+
"""Calculate the length of the slice over an array of size n.
34+
35+
Parameters
36+
----------
37+
sl : slice
38+
The slice.
39+
n : int
40+
The size of the array.
41+
42+
Returns
43+
-------
44+
int
45+
The length of the slice.
46+
"""
47+
return len(range(n)[sl])
48+
49+
def key_len(keys):
50+
# Check lengths
51+
if isinstance(keys, slice) or isinstance(keys, int):
52+
length = 1
53+
else:
54+
length = len(keys)
55+
56+
if length < 1:
57+
raise IndexError(
58+
"Too few indices."
59+
)
60+
61+
return length
62+
63+
def ensure_iter(keys, pos, shape):
64+
"""Ensure the output is iterable.
65+
66+
Parameters
67+
----------
68+
keys : int or tuple or list or array
69+
List of indices to slice an array.
70+
pos : int
71+
Index into keys.
72+
shape : int
73+
The length of the dimension we are slicing.
74+
75+
Returns
76+
-------
77+
range
78+
The range.
79+
"""
80+
length = key_len(keys)
81+
82+
# Handle "slice the rest"
83+
if length > 1 and keys[-1] == Ellipsis:
84+
keys = keys[:-1]
85+
length -= 1
86+
87+
if length > pos:
88+
try:
89+
val = keys[pos]
90+
except TypeError:
91+
# Only one key
92+
val = keys
93+
if isinstance(val, slice):
94+
if val.start is None and val.stop is None and val.step is None:
95+
return range(shape)
96+
tmp = range(10**10)[val]
97+
start, stop, step = tmp.start, tmp.stop, tmp.step
98+
if start > shape:
99+
start = shape
100+
if stop > shape:
101+
stop = shape
102+
return range(start, stop, step)
103+
elif isinstance(val, int):
104+
if val > shape or (val + 1) > shape:
105+
# TODO: It's not clear to me this is the correct behavior
106+
return range(shape-1, shape)
107+
return range(val, val + 1)
108+
109+
else:
110+
return range(shape)
111+
112+
def ensure_slice(keys, pos):
113+
"""Ensure the output is a slice.
114+
115+
Parameters
116+
----------
117+
keys : int or tuple or list or array
118+
List of indices to slice an array.
119+
pos : int
120+
Index into keys.
121+
122+
Returns
123+
-------
124+
slice
125+
The slice.
126+
"""
127+
length = key_len(keys)
128+
129+
# Handle "slice the rest"
130+
if length > 1 and keys[-1] == Ellipsis:
131+
keys = keys[:-1]
132+
length -= 1
133+
134+
if length > pos:
135+
try:
136+
val = keys[pos]
137+
except TypeError:
138+
# Only one key
139+
val = keys
140+
if isinstance(val, int):
141+
return slice(val, val+1, None)
142+
assert isinstance(val, slice)
143+
return val
144+
else:
145+
# Default to all values
146+
return slice(None, None, None)

0 commit comments

Comments
 (0)