Skip to content

Commit b140b78

Browse files
authored
Merge pull request #516 from jgrewe/invalid_slice
Allow DataViews to become invalid fixes #502
2 parents c5ffc3b + 430a6ee commit b140b78

File tree

10 files changed

+354
-84
lines changed

10 files changed

+354
-84
lines changed

nixio/data_view.py

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,65 @@
1111
from collections.abc import Iterable
1212
except ImportError:
1313
from collections import Iterable
14+
import numpy as np
1415
from .data_set import DataSet
15-
from .exceptions import OutOfBounds, IncompatibleDimensions
16+
from .exceptions import OutOfBounds, InvalidSlice
1617

1718

1819
class DataView(DataSet):
1920

2021
def __init__(self, da, slices):
21-
if len(slices) != len(da.shape):
22+
self._valid = slices is not None and all(slices)
23+
self._slices = slices
24+
self._error_message = ""
25+
if not self.valid:
26+
self._error_message = (
27+
"InvalidSlice error!"
28+
"Given slice {} is invalid! At least one slice along one dimension"
29+
"does not contain data.".format(slices)
30+
)
31+
32+
if self.valid and len(slices) != len(da.shape):
2233
# This is always checked by the calling function, but we repeat
2334
# the check here for future bug catching
24-
raise IncompatibleDimensions(
35+
self._valid = False
36+
self._error_message = (
37+
"IncompatibleDimensions error."
2538
"Number of dimensions for DataView does not match underlying "
2639
"data object: {} != {}".format(len(slices), len(da.shape)),
27-
"DataView"
2840
)
2941

30-
if any(s.stop > e for s, e in zip(slices, da.data_extent)):
31-
raise OutOfBounds(
32-
"Trying to create DataView which is out of bounds of the "
33-
"underlying DataArray"
42+
if self.valid and any(s.stop > e for s, e in zip(slices, da.data_extent)):
43+
self._valid = False
44+
self._error_message = (
45+
"OutOfBounds error!"
46+
"Trying to create DataView with slices {} which are out of bounds of the "
47+
"underlying DataArray {}".format(self._slices, da.shape)
3448
)
3549

3650
# Simplify all slices
37-
slices = tuple(slice(*sl.indices(dimlen))
38-
for sl, dimlen in zip(slices, da.shape))
51+
if self.valid:
52+
slices = tuple(slice(*sl.indices(dimlen))
53+
for sl, dimlen in zip(slices, da.shape))
54+
self._slices = slices
3955

4056
self.array = da
4157
self._h5group = self.array._h5group
42-
self._slices = slices
58+
59+
@property
60+
def valid(self):
61+
return self._valid
62+
63+
@property
64+
def debug_message(self):
65+
return self._error_message
4366

4467
@property
4568
def data_extent(self):
46-
return tuple(s.stop - s.start for s in self._slices)
69+
if self.valid:
70+
return tuple(s.stop - s.start for s in self._slices)
71+
else:
72+
return None
4773

4874
@data_extent.setter
4975
def data_extent(self, v):
@@ -54,12 +80,19 @@ def data_type(self):
5480
return self.array.data_type
5581

5682
def _write_data(self, data, sl=None):
83+
if not self.valid:
84+
raise InvalidSlice(
85+
"Write Data failed due to an invalid slice."
86+
"Reason is: {}".format(self._error_message)
87+
)
5788
tsl = self._slices
5889
if sl:
5990
tsl = self._transform_coordinates(sl)
6091
super(DataView, self)._write_data(data, tsl)
6192

6293
def _read_data(self, sl=None):
94+
if not self.valid:
95+
return np.array([])
6396
tsl = self._slices
6497
if sl is not None:
6598
tsl = self._transform_coordinates(sl)
@@ -90,7 +123,7 @@ def transform_slice(uslice, dvslice):
90123
ustart, ustop, ustep = uslice.indices(dimlen)
91124
if ustop < 0: # special case for None stop
92125
ustop = dimlen + ustop
93-
tslice = slice(dvslice.start+ustart, dvslice.start+ustop, ustep)
126+
tslice = slice(dvslice.start + ustart, dvslice.start + ustop, ustep)
94127
if tslice.stop > dvslice.stop:
95128
raise oob
96129

@@ -141,7 +174,7 @@ def _expand_user_slices(self, user_slices):
141174
expidx = user_slices.index(Ellipsis)
142175
npad = len(self.data_extent) - len(user_slices) + 1
143176
padding = (slice(None),) * npad
144-
return user_slices[:expidx] + padding + user_slices[expidx+1:]
177+
return user_slices[:expidx] + padding + user_slices[expidx + 1:]
145178

146179
# expand slices at the end
147180
npad = len(self.data_extent) - len(user_slices)

nixio/dimensions.py

Lines changed: 136 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ class IndexMode(Enum):
3737
GEQ = "geq"
3838

3939

40+
class SliceMode(Enum):
41+
Exclusive = "exclusive"
42+
Inclusive = "inclusive"
43+
44+
def to_index_mode(self):
45+
if self == self.Exclusive:
46+
return IndexMode.Less
47+
if self == self.Inclusive:
48+
return IndexMode.LessOrEqual
49+
50+
4051
class DimensionContainer(Container):
4152
"""
4253
DimensionContainer extends Container to support returning different types
@@ -375,15 +386,14 @@ def index_of(self, position, mode=IndexMode.LessOrEqual):
375386
if scaled_position < 0:
376387
if mode == IndexMode.GreaterOrEqual:
377388
return 0
378-
# position is OOB (left side) but can't round up
389+
# position is OOB (left side) but can't round up because LessOrEqual or Less
379390
raise IndexError("Position {} is out of bounds for SampledDimension with offset {} and mode {}".format(
380391
position, offset, mode.name
381392
))
382393

383394
if np.isclose(position, 0) and mode == IndexMode.Less:
384-
raise IndexError("Position {} is out of bounds for SetDimension with mode {}".format(position, mode.name))
385-
386-
index = int(np.floor(scaled_position))
395+
raise IndexError("Position {} is out of bounds for SampledDimension with mode {}".format(position, mode.name))
396+
index = int(np.round(scaled_position))
387397
if np.isclose(scaled_position, index):
388398
# exact position
389399
if mode in (IndexMode.GreaterOrEqual, IndexMode.LessOrEqual):
@@ -393,13 +403,50 @@ def index_of(self, position, mode=IndexMode.LessOrEqual):
393403
# exact position and Less mode
394404
return index - 1
395405
raise ValueError("Unknown IndexMode: {}".format(mode))
406+
if index < scaled_position:
407+
if mode in (IndexMode.LessOrEqual, IndexMode.Less):
408+
return index
409+
elif mode == IndexMode.GreaterOrEqual:
410+
return index + 1
411+
else:
412+
raise ValueError("Unknown IndexMode: {}".format(mode))
413+
else:
414+
if mode in (IndexMode.LessOrEqual, IndexMode.Less):
415+
return index - 1
416+
elif mode == IndexMode.GreaterOrEqual:
417+
return index
418+
else:
419+
raise ValueError("Unknown IndexMode: {}".format(mode))
396420

397-
if mode == IndexMode.GreaterOrEqual: # and inexact position
398-
return index + 1
399-
if mode in (IndexMode.LessOrEqual, IndexMode.Less): # and inexact position
400-
return index
421+
def range_indices(self, start_position, end_position, mode=SliceMode.Exclusive):
422+
"""
423+
Returns the start and end indices in this dimension that are matching to the given start and end position.
401424
402-
raise ValueError("Unknown IndexMode: {}".format(mode))
425+
:param start_position: the start position of the range.
426+
:type start_position: float
427+
:param end_position: the end position of the range.
428+
:type end_position: float
429+
:param mode: The nixio.SliceMode. Defaults to nixio.SliceMode.Exclusive, i.e. the end position is not part of the range.
430+
:type mode: nixio.SliceMode
431+
432+
:returns: The respective start and end indices. None, if the range is empty!
433+
:rtype: tuple of int
434+
435+
:raises: ValueError if invalid mode is given
436+
:raises: Index Error if start position is greater than end position.
437+
"""
438+
if mode is not SliceMode.Exclusive and mode is not SliceMode.Inclusive:
439+
raise ValueError("Unknown SliceMode: {}".format(mode))
440+
441+
end_mode = IndexMode.Less if mode == SliceMode.Exclusive else IndexMode.LessOrEqual
442+
try:
443+
start_index = self.index_of(start_position, mode=IndexMode.GreaterOrEqual)
444+
end_index = self.index_of(end_position, mode=end_mode)
445+
except IndexError:
446+
return None
447+
if start_index > end_index:
448+
return None
449+
return (start_index, end_index)
403450

404451
def axis(self, count, start=None, start_position=None):
405452
"""
@@ -516,7 +563,7 @@ def is_alias(self):
516563
elif self.has_link and self.dimension_link._data_object_type == "DataArray":
517564
return True
518565
return False
519-
566+
520567
@property
521568
def _redirgrp(self):
522569
"""
@@ -528,7 +575,7 @@ def _redirgrp(self):
528575
gname = self._h5group.get_by_pos(0).name
529576
return self._h5group.open_group(gname)
530577
return self._h5group
531-
578+
532579
@property
533580
def ticks(self):
534581
if self.is_alias and not self.has_link:
@@ -585,7 +632,7 @@ def unit(self, unit):
585632
else:
586633
self._h5group.set_attr("unit", unit)
587634

588-
def index_of(self, position, mode=IndexMode.LessOrEqual):
635+
def index_of(self, position, mode=IndexMode.LessOrEqual, ticks=None):
589636
"""
590637
Returns the index of a certain position in the dimension.
591638
Raises IndexError if the position is out of bounds (depending on mode).
@@ -597,11 +644,14 @@ def index_of(self, position, mode=IndexMode.LessOrEqual):
597644
If the mode is Less, the previous index of the matching tick is always returned.
598645
If the mode is GreaterOrEqual and the position does not match a tick exactly, the next index is
599646
returned.
647+
:param ticks: Optional, the ticks stored in this dimension. If not passed as argument, they are (re)read from file.
648+
:type ticks: iterable
600649
601650
:returns: The matching index
602651
:rtype: int
603652
"""
604-
ticks = self.ticks
653+
if ticks is None:
654+
ticks = self.ticks
605655
if position < ticks[0]:
606656
if mode == IndexMode.GreaterOrEqual:
607657
return 0
@@ -626,6 +676,38 @@ def index_of(self, position, mode=IndexMode.LessOrEqual):
626676

627677
raise ValueError("Unknown IndexMode: {}".format(mode))
628678

679+
def range_indices(self, start_position, end_position, mode=SliceMode.Exclusive):
680+
"""
681+
Returns the start and end indices in this dimension that are matching to the given start and end position.
682+
683+
:param start_position: the start position of the range.
684+
:type start_position: float
685+
:param end_position: the end position of the range.
686+
:type end_position: float
687+
:param mode: The nixio.SliceMode. Defaults to nixio.SliceMode.Exclusive, i.e. the end position is not part of the range.
688+
:type mode: nixio.SliceMode
689+
690+
:returns: The respective start and end indices. None, if range is empty
691+
:rtype: tuple of int
692+
693+
:raises: ValueError if invalid mode is given
694+
:raises: Index Error if start position is greater than end position.
695+
"""
696+
if mode is not SliceMode.Exclusive and mode is not SliceMode.Inclusive:
697+
raise ValueError("Unknown SliceMode: {}".format(mode))
698+
if start_position > end_position:
699+
raise IndexError("Start position {} is greater than end position {}.".format(start_position, end_position))
700+
ticks = self.ticks
701+
end_mode = IndexMode.Less if mode == SliceMode.Exclusive else IndexMode.LessOrEqual
702+
try:
703+
start_index = self.index_of(start_position, mode=IndexMode.GreaterOrEqual, ticks=ticks)
704+
end_index = self.index_of(end_position, mode=end_mode, ticks=ticks)
705+
except IndexError:
706+
return None
707+
if start_index > end_index:
708+
return None
709+
return (start_index, end_index)
710+
629711
def tick_at(self, index):
630712
"""
631713
Returns the tick at the given index. Will throw an Exception if the
@@ -699,7 +781,7 @@ def labels(self, labels):
699781
labels = list(labels)
700782
self._h5group.write_data("labels", labels, dtype=dt)
701783

702-
def index_of(self, position, mode=IndexMode.LessOrEqual):
784+
def index_of(self, position, mode=IndexMode.LessOrEqual, dim_labels=None):
703785
"""
704786
Returns the index of a certain position in the dimension.
705787
Raises IndexError if the position is out of bounds (depending on mode and number of labels).
@@ -711,6 +793,8 @@ def index_of(self, position, mode=IndexMode.LessOrEqual):
711793
If the position is not an integer (or is not equal to the nearest integer), then the value is
712794
rounded down (for LessOrEqual) or rounded up (for GreaterOrEqual).
713795
If the mode is Less, the previous integer is always returned.
796+
:param dim_labels: The labels of this dimension, if None (default) the labels will be read from file.
797+
:type dim_labels: iterable
714798
715799
:returns: The matching index
716800
:rtype: int
@@ -724,12 +808,13 @@ def index_of(self, position, mode=IndexMode.LessOrEqual):
724808
if position == 0 and mode == IndexMode.Less:
725809
raise IndexError("Position {} is out of bounds for SetDimension with mode {}".format(position, mode.name))
726810

727-
labels = self.labels
728-
if labels and len(labels) and position > len(labels)-1:
811+
if dim_labels is None:
812+
dim_labels = self.labels
813+
if dim_labels and len(dim_labels) and position > len(dim_labels) - 1:
729814
if mode in (IndexMode.Less, IndexMode.LessOrEqual):
730-
return len(labels) - 1
815+
return len(dim_labels) - 1
731816
raise IndexError("Position {} is out of bounds for SetDimension with length {} and mode {}".format(
732-
position, len(labels), mode.name
817+
position, len(dim_labels), mode.name
733818
))
734819

735820
index = int(np.floor(position))
@@ -749,3 +834,36 @@ def index_of(self, position, mode=IndexMode.LessOrEqual):
749834
return index
750835

751836
raise ValueError("Unknown IndexMode: {}".format(mode))
837+
838+
def range_indices(self, start_position, end_position, mode=SliceMode.Exclusive):
839+
"""
840+
Returns the start and end indices in this dimension that are matching to the given start and end position.
841+
842+
:param start_position: the start position of the range.
843+
:type start_position: float
844+
:param end_position: the end position of the range.
845+
:type end_position: float
846+
:param mode: The nixio.SliceMode. Defaults to nixio.SliceMode.Exclusive, i.e. the end position is not part of the range.
847+
:type mode: nixio.SliceMode
848+
849+
:returns: The respective start and end indices. None, if the range is empty
850+
:rtype: tuple of int
851+
852+
:raises: ValueError if invalid mode is given
853+
:raises: Index Error if start position is greater than end position.
854+
"""
855+
if mode is not SliceMode.Exclusive and mode is not SliceMode.Inclusive:
856+
raise ValueError("Unknown SliceMode: {}".format(mode))
857+
858+
dim_labels = self.labels
859+
end_mode = IndexMode.Less if mode == SliceMode.Exclusive else IndexMode.LessOrEqual
860+
if start_position > end_position:
861+
raise IndexError("Start position {} is greater than end position {}.".format(start_position, end_position))
862+
try:
863+
start = self.index_of(start_position, mode=IndexMode.GreaterOrEqual, dim_labels=dim_labels)
864+
end = self.index_of(end_position, mode=end_mode, dim_labels=dim_labels)
865+
except IndexError:
866+
return None
867+
if start > end:
868+
return None
869+
return (start, end)

nixio/exceptions/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .exceptions import (DuplicateName, UninitializedEntity, InvalidUnit,
22
InvalidAttrType, InvalidEntity, OutOfBounds,
3-
IncompatibleDimensions, InvalidFile,
3+
IncompatibleDimensions, InvalidFile, InvalidSlice,
44
DuplicateColumnName, UnsupportedLinkType)
55

66
__all__ = (

nixio/exceptions/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ def __init__(self, *args, **kwargs):
4747
super(InvalidEntity, self).__init__(self.message, *args, **kwargs)
4848

4949

50+
class InvalidSlice(Exception):
51+
52+
def __init__(self, *args, **kwargs):
53+
self.message = "Trying to access data with an invalid slice."
54+
super(InvalidSlice, self).__init__(self.message, *args, **kwargs)
55+
56+
5057
class OutOfBounds(IndexError):
5158

5259
def __init__(self, message, index=None):

nixio/multi_tag.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,6 @@ def tagged_data(self, posidx, refidx, stop_rule=SliceMode.Exclusive):
139139
ref = references[refidx]
140140

141141
slices = self._calc_data_slices_mtag(ref, posidx, stop_rule)
142-
if not self._slices_in_data(ref, slices):
143-
raise OutOfBounds("References data slice out of the extent of the DataArray!")
144142
return DataView(ref, slices)
145143

146144
def retrieve_feature_data(self, posidx, featidx):

0 commit comments

Comments
 (0)