From c1b2dfed862417963bde5cea728a15309d58c49f Mon Sep 17 00:00:00 2001 From: Robert Smallshire Date: Fri, 8 May 2015 18:41:04 +0200 Subject: [PATCH] More flexible and robust 2D catalog types which independently store i and j indexes. --- segpy/catalog.py | 206 +++++++++++++++++++++++++------------------ segpy/util.py | 8 +- test/test_catalog.py | 35 +++++++- 3 files changed, 160 insertions(+), 89 deletions(-) diff --git a/segpy/catalog.py b/segpy/catalog.py index 9df70ca..46921cb 100644 --- a/segpy/catalog.py +++ b/segpy/catalog.py @@ -14,7 +14,7 @@ from fractions import Fraction import reprlib from segpy.sorted_set import SortedFrozenSet -from segpy.util import contains_duplicates, measure_stride, minmax +from segpy.util import contains_duplicates, measure_stride, make_sorted_distinct_sequence class CatalogBuilder(object): @@ -127,40 +127,28 @@ class CatalogBuilder(object): return DictionaryCatalog(self._catalog) - - def _create_catalog_2(self): """Create a catalog for two-dimensional integer keys. Each key must be a two-element sequence. """ - i_min, i_max = minmax(i for (i, j), value in self._catalog) - j_min, j_max = minmax(j for (i, j), value in self._catalog) + i_sorted = make_sorted_distinct_sequence(i for (i, j), value in self._catalog) + j_sorted = make_sorted_distinct_sequence(j for (i, j), value in self._catalog) - is_rm, diff = self._is_row_major(i_min, j_min, j_max) - if is_rm: - return RowMajorCatalog(i_min, i_max, j_min, j_max, diff) - return DictionaryCatalog(self._catalog) + i_is_regular = isinstance(i_sorted, range) + j_is_regular = isinstance(j_sorted, range) - def _is_row_major(self, i_min, j_min, j_max): - """Does row major ordering predict values from keys? + if i_is_regular and j_is_regular: + is_rm, diff = self._is_row_major(i_sorted, j_sorted) + if is_rm: + return RowMajorCatalog2D(i_sorted, j_sorted, diff) - In row-major order the last dimension is contiguous, and so changes - quickest, when moving through the elements in storage order. Hence - the number of rows is the number of distinct i values and the numbers - of elements in each row (i.e. columns) is the number of distinct j. + return DictionaryCatalog2D(i_sorted, j_sorted, self._catalog) - Args: - i_min: The minimum i value. - j_min: The minimum j value. - j_max: The maximum j value. - - Returns: - A 2-tuple containing, in the first element True if the values can - be predicted from the keys by assuming a row-major ordering, - otherwise False. If True, the second element will be a constant - offset, otherwise it can be ignored. - """ + def _is_row_major(self, i_sorted, j_sorted): + i_min = i_sorted[0] + j_min = j_sorted[0] + j_max = j_sorted[-1] diff = None for (i, j), actual_value in self._catalog: proposed_value = (i - i_min) * (j_max + 1 - j_min) + (j - j_min) @@ -172,7 +160,66 @@ class CatalogBuilder(object): return True, diff -class RowMajorCatalog(Mapping): +class Catalog2D(Mapping): + """An abstract base class for 2D catalogs. + """ + + def __init__(self, i_range, j_range): + """Initialize a Catalog2D. + + Args: + i_range: A range which can generate all and only valid i indexes. + j_range: A range which can generate all and only valid j indexes. + """ + self._i_range = i_range + self._j_range = j_range + + @property + def i_range(self): + return self._i_range + + @property + def j_range(self): + return self._j_range + + @property + def i_min(self): + """Minimum i value""" + return self._i_range[0] + + @property + def i_max(self): + """Maximum i value""" + return self._i_range[-1] + + @property + def j_min(self): + """Minimum j value""" + return self._j_range[0] + + @property + def j_max(self): + """Maximum j value""" + return self._j_range[-1] + + def key_min(self): + """Minimum (i, j) key""" + return self.i_min, self.j_min + + def key_max(self): + """Maximum (i, j) key""" + return self.i_max, self.j_max + + def value_start(self): + """Minimum value at key_min""" + return self[self.key_min()] + + def value_stop(self): + """Maximum value at key_max""" + return self[self.key_max()] + + +class RowMajorCatalog2D(Catalog2D): """A mapping which assumes a row-major ordering of a two-dimensional matrix. This is the ordering of items in a two-dimensional matrix where in @@ -190,84 +237,43 @@ class RowMajorCatalog(Mapping): and where c is an integer constant to allow zero- or one-based indexing. """ - # TODO: Consider renaming i_min -> i1, i_max -> i2, j_min -> j1, j_max -> j2 - def __init__(self, i_min, i_max, j_min, j_max, c): - """Initialize a RowMajorCatalog. + + def __init__(self, i_range, j_range, constant): + """Initialize a RowMajorCatalog2D. Args: - i_min (int): The minimum i value. - i_max (int): The maximum i value. - j_min (int): The minimum j value. - j_max (int): The maximum j value. - c (int): The constant offset + i_range: A range which can generate all and only valid i indexes. + j_range: A range which can generate all and only valid j indexes. + constant: The constant offset used to produce the value. """ - self._i_min = i_min - self._i_max = i_max - self._j_min = j_min - self._j_max = j_max - self._c = c + super().__init__(i_range, j_range) + self._c = constant @property - def i_min(self): - """Minimum i value""" - return self._i_min - - @property - def i_max(self): - """Maximum i value""" - return self._i_max - - @property - def j_min(self): - """Minimum j value""" - return self._j_min - - @property - def j_max(self): - """Maximum j value""" - return self._j_max - - def key_min(self): - """Minimum (i, j) key""" - return self._i_min, self._j_min - - def key_max(self): - """Maximum (i, j) key""" - return self._i_max, self._j_max - - def value_min(self): - """Minimum value at key_min""" - return self[self.key_min()] - - def value_max(self): - """Maximum value at key_max""" - return self[self.key_max()] + def constant(self): + return self._c def __getitem__(self, key): - i, j = key - if not (self._i_min <= i <= self._i_max) and \ - (self._j_min <= j <= self._j_max): + if key not in self: raise KeyError("{!r} key {!r} out of range".format(self, key)) - value = (i - self._i_min) * (self._j_max + 1 - self._j_min) + (j - self._j_min) + self._c + i, j = key + value = (i - self.i_min) * (self.j_max + 1 - self.j_min) + (j - self.j_min) + self._c return value def __contains__(self, key): - i, j = key - return (self._i_min <= i <= self._i_max) and \ - (self._j_min <= j <= self._j_max) + return (key[0] in self._i_range) and \ + (key[1] in self._j_range) def __len__(self): - return (self._i_max - self._i_min) * (self._j_max + 1 - self._j_min) + return len(self._i_range) * len(self._j_range) def __iter__(self): - for i in range(self._i_min, self._i_max + 1): - for j in range(self._j_min, self._j_max + 1): - yield (i, j) + yield from ((i, j) for i in self._i_range for j in self._j_range) def __repr__(self): - return '{}(i_min={}, i_max={}, j_min={}, j_max={}, c={})'.format( + return '{}(i_range={}, j_range={}, c={})'.format( self.__class__.__name__, - self._i_min, self._i_max, self._j_min, self._j_max, self._c) + self.i_range, self.j_range, self._c) class DictionaryCatalog(Mapping): @@ -294,6 +300,32 @@ class DictionaryCatalog(Mapping): self.__class__.__name__, reprlib.repr(self._items.items())) +class DictionaryCatalog2D(Catalog2D): + """An immutable, ordered, dictionary mapping for 2D keys. + """ + + def __init__(self, i_range, j_range, items): + super().__init__(i_range, j_range) + self._items = OrderedDict(items) + + def __getitem__(self, key): + return self._items[key] + + def __iter__(self): + return iter(self._items) + + def __len__(self): + return len(self._items) + + def __contains__(self, item): + return item in self._items + + def __repr__(self): + return '{}(i_range={}, j_range={}, items={})'.format( + self.j_range, self.j_range, + self.__class__.__name__, reprlib.repr(self._items.items())) + + class RegularConstantCatalog(Mapping): """Mapping with keys ordered with regular spacing along the number line. diff --git a/segpy/util.py b/segpy/util.py index 118a078..c1c740c 100644 --- a/segpy/util.py +++ b/segpy/util.py @@ -328,6 +328,12 @@ def four_bytes(byte_str): return a, b, c, d +def single_item_range(item): + """Construct a range object which generates a single value. + """ + return range(item, item + 1) + + def make_sorted_distinct_sequence(iterable): """Create a sorted immutable sequence from an iterable series. @@ -340,7 +346,7 @@ def make_sorted_distinct_sequence(iterable): """ sorted_set = SortedFrozenSet(iterable) if len(sorted_set) == 1: - return sorted_set + return single_item_range(sorted_set[0]) stride = measure_stride(sorted_set) if stride is not None: start = sorted_set[0] diff --git a/test/test_catalog.py b/test/test_catalog.py index f25055d..6915101 100644 --- a/test/test_catalog.py +++ b/test/test_catalog.py @@ -1,6 +1,6 @@ import unittest -from hypothesis import given, example +from hypothesis import given, example, assume from hypothesis.specifiers import dictionary, just, integers_in_range, streaming from segpy.catalog import CatalogBuilder @@ -37,6 +37,7 @@ class TestCatalogBuilder(unittest.TestCase): step=integers_in_range(-10000, 10000), values=streaming(int)) def test_regular_mapping(self, start, num, step, values): + assume(step != 0) mapping = {key: value for key, value in zip(range(start, start + num*step, step), values)} builder = CatalogBuilder(mapping) catalog = builder.create() @@ -49,9 +50,41 @@ class TestCatalogBuilder(unittest.TestCase): value_start=int, value_step=integers_in_range(-10000, 10000)) def test_linear_regular_mapping(self, num, key_start, key_step, value_start, value_step): + assume(key_step != 0) + assume(value_step != 0) mapping = {key: value for key, value in zip(range(key_start, key_start + num*key_step, key_step), range(value_start, value_start + num*value_step, value_step))} builder = CatalogBuilder(mapping) catalog = builder.create() shared_items = set(mapping.items()) & set(catalog.items()) + self.assertEqual(len(shared_items), len(mapping)) + + @given(dictionary((int, int), int)) + def test_arbitrary_mapping(self, mapping): + builder = CatalogBuilder(mapping) + catalog = builder.create() + shared_items = set(mapping.items()) & set(catalog.items()) + self.assertEqual(len(shared_items), len(mapping)) + + @given(i_start=integers_in_range(0, 10), + i_num=integers_in_range(1, 10), + i_step=just(1), + j_start=integers_in_range(0, 10), + j_num=integers_in_range(1, 10), + j_step=just(1), + c=integers_in_range(1, 10)) + def test_linear_regular_mapping_2d(self, i_start, i_num, i_step, j_start, j_num, j_step, c): + assume(i_step != 0) + assume(j_step != 0) + + def v(i, j): + return (i - i_start) * ((j_start + j_num*j_step) - j_start) + (j - j_start) + c + + mapping = {(i, j): v(i, j) + for i in range(i_start, i_start + i_num*i_step, i_step) + for j in range(j_start, j_start + j_num*j_step, j_step)} + + builder = CatalogBuilder(mapping) + catalog = builder.create() + shared_items = set(mapping.items()) & set(catalog.items()) self.assertEqual(len(shared_items), len(mapping)) \ No newline at end of file