From fcd750a2f836e36708851905c02c2b66ddacf8eb Mon Sep 17 00:00:00 2001 From: Gabriel Altay Date: Wed, 11 Nov 2020 17:19:41 -0500 Subject: [PATCH] dev to main (#32) * Input Type Validation Made all functions that take input compatible with floats that are equal to ints. Fixed a few formatting issues. Added type validation for __init__: Checks wether p and n can be converted cleanly to int using %1. If they can't a TypeError is thrown. If %1 is zero then they are cast to int using int(). Added Type Validation for coordinates_from_distance: If h%1 is not 0 a TypeError is thrown. If h%1 is 0 h is cast to int using int(). Standardized Error Messages in coordinates_from_distance: Changed the ValueErrors thrown to the same formatting as the ValueErrors in __init__. Added Type Validation for distance_from_coordinates: If for any element in x element%1 is not 0 then a TypeError is thrown. Otherwise x is iterated through and each element is cast to int using int(). Formatting Correction in _binary_repr: "width:int" ->"width: int" * Fixed syntax and type checking Made an error in a error message syntax 0.0 != False so I changed how %1 is checked. * more tests and travis (#26) * more tests and travis * add empty requirements in order to not have to change travis later * misname * added python 3.6 and 3.7 to travis (#27) * Update README.rst * Update README.rst * Trav multi version (#28) * added python 3.6 and 3.7 to travis * travis typo * More tests explicit min (#29) * test bounds and explicit min * fixed docstring * extended type hints to floats (#30) * new pypi version (#31) Co-authored-by: C-Jameson --- .travis.yml | 11 +++++++ README.rst | 3 ++ examples.py | 18 +++++------ hilbertcurve/__init__.py | 2 +- hilbertcurve/hilbertcurve.py | 56 ++++++++++++++++++++++---------- requirements.txt | 0 tests/test_hilbertcurve.py | 62 +++++++++++++++++++++++++++++++----- 7 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 .travis.yml create mode 100644 requirements.txt mode change 100755 => 100644 tests/test_hilbertcurve.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b53f431 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +python: + - "3.6" + - "3.7" + - "3.8" +# command to install dependencies +install: + - pip install -r requirements.txt +# command to run tests +script: + - pytest diff --git a/README.rst b/README.rst index ff9a6c4..2629247 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,6 @@ +.. image:: https://travis-ci.com/galtay/hilbertcurve.svg?branch=develop + :target: https://travis-ci.com/galtay/hilbertcurve + ============ Introduction ============ diff --git a/examples.py b/examples.py index 55ceb20..e224c00 100644 --- a/examples.py +++ b/examples.py @@ -11,30 +11,30 @@ # calculate distances along a hilbert curve given coordinates p = 1 -N = 2 -hilbert_curve = HilbertCurve(p, N) +n = 2 +hilbert_curve = HilbertCurve(p, n) for coords in [[0,0], [0,1], [1,1], [1,0]]: dist = hilbert_curve.distance_from_coordinates(coords) print(f'distance(x={coords}) = {dist}') # calculate coordinates given distances along a hilbert curve p = 1 -N = 2 -hilbert_curve = HilbertCurve(p, N) +n = 2 +hilbert_curve = HilbertCurve(p, n) for ii in range(4): - print('coords(h={},p={},N={}) = {}'.format( - ii, p, N, hilbert_curve.coordinates_from_distance(ii))) + print('coords(h={},p={},n={}) = {}'.format( + ii, p, n, hilbert_curve.coordinates_from_distance(ii))) # due to the magic of arbitrarily large integers in # Python (https://www.python.org/dev/peps/pep-0237/) # these calculations can be done with absurd numbers p = 512 -N = 10 -hilbert_curve = HilbertCurve(p, N) +n = 10 +hilbert_curve = HilbertCurve(p, n) ii = 123456789101112131415161718192021222324252627282930 coords = hilbert_curve.coordinates_from_distance(ii) -print('coords(h={},p={},N={}) = {}'.format(ii, p, N, coords)) +print('coords(h={},p={},n={}) = {}'.format(ii, p, n, coords)) coords = [121075, 67332, 67326, 108879, 26637, 43346, 23848, 1551, 68130, 84004] diff --git a/hilbertcurve/__init__.py b/hilbertcurve/__init__.py index d3d9b65..93a792e 100644 --- a/hilbertcurve/__init__.py +++ b/hilbertcurve/__init__.py @@ -1,4 +1,4 @@ """Metadata for this package.""" __package_name__ = "hilbertcurve" -__version__ = "1.0.4" +__version__ = "1.0.5" diff --git a/hilbertcurve/hilbertcurve.py b/hilbertcurve/hilbertcurve.py index a140783..913e9be 100644 --- a/hilbertcurve/hilbertcurve.py +++ b/hilbertcurve/hilbertcurve.py @@ -11,10 +11,10 @@ discrete distances along the Hilbert curve (indexed from :math:`0` to :math:`2^{N p} - 1`). """ -from typing import Iterable, List +from typing import Iterable, List, Union -def _binary_repr(num: int, width:int) -> str: +def _binary_repr(num: int, width: int) -> str: """Return a binary string representation of `num` zero padded to `width` bits.""" return format(num, 'b').zfill(width) @@ -22,24 +22,34 @@ def _binary_repr(num: int, width:int) -> str: class HilbertCurve: - def __init__(self, p: int, n: int) -> None: + def __init__(self, p: Union[int, float], n: Union[int, float]) -> None: """Initialize a hilbert curve with, Args: - p (int): iterations to use in constructing the hilbert curve - n (int): number of dimensions + p (int or float): iterations to use in constructing the hilbert curve. + if float, must satisfy p % 1 = 0 + n (int or float): number of dimensions. + if float must satisfy n % 1 = 0 """ - if p <= 0: - raise ValueError('p must be > 0') - if n <= 0: - raise ValueError('n must be > 0') - self.p = p - self.n = n - - # maximum distance along curve + if (p % 1) != 0: + raise TypeError("p is not an integer and can not be converted") + if (n % 1) != 0: + raise TypeError("n is not an integer and can not be converted") + + self.p = int(p) + self.n = int(n) + + if self.p <= 0: + raise ValueError('p must be > 0 (got p={} as input)'.format(p)) + if self.n <= 0: + raise ValueError('n must be > 0 (got n={} as input)'.format(n)) + + # minimum and maximum distance along curve + self.min_h = 0 self.max_h = 2**(self.p * self.n) - 1 - # maximum coordinate value in any dimension + # minimum and maximum coordinate value in any dimension + self.min_x = 0 self.max_x = 2**self.p - 1 def _hilbert_integer_to_transpose(self, h: int) -> List[int]: @@ -80,10 +90,15 @@ def coordinates_from_distance(self, h: int) -> List[int]: x (list): transpose of h (n components with values between 0 and 2**p-1) """ + + if (h % 1) != 0: + raise TypeError("h is not an integer and can not be converted") if h > self.max_h: - raise ValueError('h={} is greater than 2**(p*N)-1={}'.format(h, self.max_h)) + raise ValueError('h must be < 2**(p*N)-1={}') if h < 0: - raise ValueError('h={} but must be > 0'.format(h)) + raise ValueError('h must be > 0') + + h = int(h) x = self._hilbert_integer_to_transpose(h) Z = 2 << (self.p-1) @@ -131,11 +146,18 @@ def distance_from_coordinates(self, x_in: List[int]) -> int: 'invalid coordinate input x={}. one or more dimensions have a ' 'value greater than 2**p-1={}'.format(x, self.max_x)) - if any(elx < 0 for elx in x): + if any(elx < self.min_x for elx in x): raise ValueError( 'invalid coordinate input x={}. one or more dimensions have a ' 'value less than 0'.format(x)) + if any((elx % 1) != 0 for elx in x): + raise TypeError( + 'invalid coordinate input x={}. one or more dimensions is not ' + 'an integer and can not be converted'.format(x)) + + for i in range(len(x)): x[i] = int(x[i]) + M = 1 << (self.p - 1) # Inverse undo excess work diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_hilbertcurve.py b/tests/test_hilbertcurve.py old mode 100755 new mode 100644 index d98ad02..75bbb64 --- a/tests/test_hilbertcurve.py +++ b/tests/test_hilbertcurve.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python """Test the functions in hilbert.py""" +import pytest import unittest from hilbertcurve.hilbertcurve import HilbertCurve @@ -24,8 +24,8 @@ def test_10590(self): X[2] = 0b00110 = 6 """ p = 5 - N = 3 - hilbert_curve = HilbertCurve(p, N) + n = 3 + hilbert_curve = HilbertCurve(p, n) h = 10590 expected_x = [13, 19, 6] actual_x = hilbert_curve._hilbert_integer_to_transpose(h) @@ -51,8 +51,8 @@ def test_13_19_6(self): X[2] = 0b00110 = 6 """ p = 5 - N = 3 - hilbert_curve = HilbertCurve(p, N) + n = 3 + hilbert_curve = HilbertCurve(p, n) x = [13, 19, 6] expected_h = 10590 actual_h = hilbert_curve._transpose_to_hilbert_integer(x) @@ -64,15 +64,61 @@ class TestReversibility(unittest.TestCase): def test_reversibility(self): """Assert coordinates_from_distance and distance_from_coordinates are inverse operations.""" - N = 3 + n = 3 p = 5 - hilbert_curve = HilbertCurve(p, N) - n_h = 2**(N * p) + hilbert_curve = HilbertCurve(p, n) + n_h = 2**(n * p) for h in range(n_h): x = hilbert_curve.coordinates_from_distance(h) h_test = hilbert_curve.distance_from_coordinates(x) self.assertEqual(h, h_test) +class TestInitIntConversion(unittest.TestCase): + """Test __init__ conversion of floating point to integers.""" + + def test_pt_oh(self): + """Assert x.0 goes to x""" + n = 3.0 + n_int = 3 + p = 5 + hilbert_curve = HilbertCurve(p, n) + self.assertTrue(isinstance(hilbert_curve.n, int)) + self.assertEqual(hilbert_curve.n, n_int) + + n = 3 + p_int = 5 + p = 5.0 + hilbert_curve = HilbertCurve(p, n) + self.assertTrue(isinstance(hilbert_curve.p, int)) + self.assertEqual(hilbert_curve.p, p_int) + + def test_pt_one(self): + """Assert x.1 raises an error""" + n = 3 + p = 5.1 + with pytest.raises(TypeError): + hilbert_curve = HilbertCurve(p, n) + + n = 3.1 + p = 5 + with pytest.raises(TypeError): + hilbert_curve = HilbertCurve(p, n) + +class TestInitBounds(unittest.TestCase): + """Test __init__ bounds on n and p.""" + + def test_pt_one(self): + """Assert x=0 raises an error""" + n = 0 + p = 5 + with pytest.raises(ValueError): + hilbert_curve = HilbertCurve(p, n) + + n = 3 + p = 0 + with pytest.raises(ValueError): + hilbert_curve = HilbertCurve(p, n) + if __name__ == '__main__': unittest.main()