This is an automated email from the git hooks/post-receive script. sebastic pushed a commit to branch master in repository python-affine.
commit 8383026748da7a8402a240fba23a2b0953eb60b5 Author: Bas Couwenberg <[email protected]> Date: Sat May 21 15:49:26 2016 +0200 Imported Upstream version 2.0.0 --- .travis.yml | 12 ++- AUTHORS.txt | 4 +- CHANGES.txt | 21 ++++++ affine/__init__.py | 168 ++++++++++++++++++++++------------------- affine/tests/test_rotation.py | 53 +++++++++++++ affine/tests/test_transform.py | 144 +++++++++++++++++++++++------------ tox.ini | 2 +- 7 files changed, 272 insertions(+), 132 deletions(-) diff --git a/.travis.yml b/.travis.yml index a8481aa..e5c6898 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,24 @@ +sudo: false language: python python: - "2.6" - "2.7" - "3.3" - "3.4" + - "3.5" install: - "pip install pytest pytest-cov nose" - "pip install coveralls" - "pip install -e ." script: - - py.test --cov affine --cov-report term-missing + - python -m pytest affine/tests --cov affine --cov-report term-missing after_success: - coveralls +deploy: + on: + tags: true + provider: pypi + distributions: "sdist bdist_wheel" + user: seang + password: + secure: "FlfcoO8a26yNTtxJ9BmzLHCM18r0sDSJvXWNQ2LN/j0qTgKK9XeD9huMeE3s/OptD6DlY36bNgxSW87rsIx/nboyn+eJQA5tvhMP48lhLDZ6rDkRT1jsizxDYskQf4V//nsOoycuV6nTKDnneITGR1tn4AqDRbiwHSKho4GuBaM=" diff --git a/AUTHORS.txt b/AUTHORS.txt index 34c7ba2..02e814b 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -1,8 +1,8 @@ Authors ======= -- Sean Gillies <[email protected]> -- Sean Gillies <[email protected]> (same as ^^) +- Sean Gillies <[email protected]> - Steven Ring <[email protected]> - Mike Toews <[email protected]> - Kevin Wurster <[email protected]> +- Todd Small <[email protected]> diff --git a/CHANGES.txt b/CHANGES.txt index f65dafd..8317435 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,27 @@ CHANGES ======= +2.0.0 (2016-05-20) +------------------ +- Final release. + +2.0b2 (2016-05-16) +------------------ +- Bug fix: restore ``Affine __rmul__`` even though it permits dubious + vector * matrix expressions (#27). + +2.0b1 (2016-05-16) +------------------ +- Breaking change: precision used in properties like ``is_conformal`` is no + longer a global module attribute, but an Affine class or instance attribute + (#19, #20). +- Breaking change: ``is_degenerate`` property is now exact and not subject to + a level of precision (#23). +- Breaking change: we have reversed our sense of rotation, a positive angle + now rotates a point counter-clockwise about the pivot point (#25). +- Bug fix: a bug in matrix-vector multiplication had been reversing the + direction of rotation and is now fixed (#25). + 1.3.0 (2016-04-08) ------------------ - is_degenerate predicate is precise, not approximate (#22). diff --git a/affine/__init__.py b/affine/__init__.py index 2911ad9..68659e9 100644 --- a/affine/__init__.py +++ b/affine/__init__.py @@ -47,26 +47,9 @@ import math __all__ = ['Affine'] __author__ = "Sean Gillies" -__version__ = "1.3.0" +__version__ = "2.0.0" EPSILON = 1e-5 -EPSILON2 = EPSILON ** 2 - - -def set_epsilon(epsilon): - """Set the global absolute error value and rounding limit for approximate - floating point comparison operations. This value is accessible via the - :attr:`planar.EPSILON` global variable. - - The default value of ``0.00001`` is suitable for values - that are in the "countable range". You may need a larger - epsilon when using large absolute values, and a smaller value - for very small values close to zero. Otherwise approximate - comparison operations will not behave as expected. - """ - global EPSILON, EPSILON2 - EPSILON = float(epsilon) - EPSILON2 = EPSILON ** 2 class TransformNotInvertibleError(Exception): @@ -89,7 +72,7 @@ else: # pragma: no cover def assert_unorderable(a, b): """Assert that a and b are unorderable""" raise TypeError("unorderable types: %s and %s" - % (type(a).__name__, type(b).__name__)) + % (type(a).__name__, type(b).__name__)) def cached_property(func): @@ -111,9 +94,10 @@ def cached_property(func): def cos_sin_deg(deg): - """Return the cosine and sin for the given angle - in degrees, with special-case handling of multiples - of 90 for perfect right angles + """Return the cosine and sin for the given angle in degrees. + + With special-case handling of multiples of 90 for perfect right + angles. """ deg = deg % 360.0 if deg == 90.0: @@ -128,20 +112,29 @@ def cos_sin_deg(deg): class Affine( namedtuple('Affine', ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'))): - """Two dimensional affine transform for linear mapping from 2D coordinates - to other 2D coordinates. Parallel lines are preserved by these - transforms. Affine transforms can perform any combination of translations, - scales/flips, shears, and rotations. Class methods are provided to - conveniently compose transforms from these operations. + """Two dimensional affine transform for 2D linear mapping. + + Parallel lines are preserved by these transforms. Affine transforms + can perform any combination of translations, scales/flips, shears, + and rotations. Class methods are provided to conveniently compose + transforms from these operations. - Internally the transform is stored as a 3x3 transformation matrix. The - transform may be constructed directly by specifying the first two rows of - matrix values as 6 floats. Since the matrix is an affine transform, the - last row is always ``(0, 0, 1)``. + Internally the transform is stored as a 3x3 transformation matrix. + The transform may be constructed directly by specifying the first + two rows of matrix values as 6 floats. Since the matrix is an affine + transform, the last row is always ``(0, 0, 1)``. + + N.B.: multiplication of a transform and an (x, y) vector *always* + returns the column vector that is the matrix multiplication product + of the transform and (x, y) as a column vector, no matter which is + on the left or right side. This is obviously not the case for + matrices and vectors in general, but provides a convenience for + users of this class. :param members: 6 floats for the first two matrix rows. :type members: float """ + precision = EPSILON def __new__(self, *members): if len(members) == 6: @@ -180,7 +173,8 @@ class Affine( :type yoff: float :rtype: Affine """ - return tuple.__new__(cls, + return tuple.__new__( + cls, (1.0, 0.0, xoff, 0.0, 1.0, yoff, 0.0, 0.0, 1.0)) @@ -199,7 +193,8 @@ class Affine( sx = sy = float(scaling[0]) else: sx, sy = scaling - return tuple.__new__(cls, + return tuple.__new__( + cls, (sx, 0.0, 0.0, 0.0, sy, 0.0, 0.0, 0.0, 1.0)) @@ -216,34 +211,40 @@ class Affine( """ mx = math.tan(math.radians(x_angle)) my = math.tan(math.radians(y_angle)) - return tuple.__new__(cls, + return tuple.__new__( + cls, (1.0, mx, 0.0, my, 1.0, 0.0, 0.0, 0.0, 1.0)) @classmethod def rotation(cls, angle, pivot=None): - """Create a rotation transform at the specified angle, - optionally about the specified pivot point. + """Create a rotation transform at the specified angle. + + A pivot point other than the coordinate system origin may be + optionally specified. - :param angle: Rotation angle in degrees + :param angle: Rotation angle in degrees, counter-clockwise + about the pivot point. :type angle: float - :param pivot: Point to rotate about, if omitted the - rotation is about the origin. + :param pivot: Point to rotate about, if omitted the rotation is + about the origin. :type pivot: sequence :rtype: Affine """ ca, sa = cos_sin_deg(angle) if pivot is None: - return tuple.__new__(cls, - (ca, sa, 0.0, - -sa, ca, 0.0, + return tuple.__new__( + cls, + (ca, -sa, 0.0, + sa, ca, 0.0, 0.0, 0.0, 1.0)) else: px, py = pivot - return tuple.__new__(cls, - (ca, sa, px - px * ca + py * sa, - -sa, ca, py - px * sa - py * ca, + return tuple.__new__( + cls, + (ca, -sa, px - px * ca + py * sa, + sa, ca, py - px * sa - py * ca, 0.0, 0.0, 1.0)) def __str__(self): @@ -276,57 +277,64 @@ class Affine( @cached_property def determinant(self): - """The determinant of the transform matrix. This value - is equal to the area scaling factor when the transform - is applied to a shape. + """The determinant of the transform matrix. + + This value is equal to the area scaling factor when the + transform is applied to a shape. """ a, b, c, d, e, f, g, h, i = self return a * e - b * d - @cached_property + @property def is_identity(self): """True if this transform equals the identity matrix, within rounding limits. """ - return self is identity or self.almost_equals(identity) + return self is identity or self.almost_equals(identity, self.precision) - @cached_property + @property def is_rectilinear(self): - """True if the transform is rectilinear, i.e., whether a shape would - remain axis-aligned, within rounding limits, after applying the - transform. + """True if the transform is rectilinear. + + i.e., whether a shape would remain axis-aligned, within rounding + limits, after applying the transform. """ a, b, c, d, e, f, g, h, i = self - return ((abs(a) < EPSILON and abs(e) < EPSILON) - or (abs(d) < EPSILON and abs(b) < EPSILON)) + return ((abs(a) < self.precision and abs(e) < self.precision) + or (abs(d) < self.precision and abs(b) < self.precision)) - @cached_property + @property def is_conformal(self): - """True if the transform is conformal, i.e., if angles between points - are preserved after applying the transform, within rounding limits. - This implies that the transform has no effective shear. + """True if the transform is conformal. + + i.e., if angles between points are preserved after applying the + transform, within rounding limits. This implies that the + transform has no effective shear. """ a, b, c, d, e, f, g, h, i = self - return abs(a * b + d * e) < EPSILON + return abs(a * b + d * e) < self.precision - @cached_property + @property def is_orthonormal(self): - """True if the transform is orthonormal, which means that the - transform represents a rigid motion, which has no effective scaling or - shear. Mathematically, this means that the axis vectors of the - transform matrix are perpendicular and unit-length. Applying an - orthonormal transform to a shape always results in a congruent shape. + """True if the transform is orthonormal. + + Which means that the transform represents a rigid motion, which + has no effective scaling or shear. Mathematically, this means + that the axis vectors of the transform matrix are perpendicular + and unit-length. Applying an orthonormal transform to a shape + always results in a congruent shape. """ a, b, c, d, e, f, g, h, i = self return (self.is_conformal - and abs(1.0 - (a * a + d * d)) < EPSILON - and abs(1.0 - (b * b + e * e)) < EPSILON) + and abs(1.0 - (a * a + d * d)) < self.precision + and abs(1.0 - (b * b + e * e)) < self.precision) @cached_property def is_degenerate(self): - """True if this transform is degenerate, which means that it will - collapse a shape to an effective area of zero. Degenerate transforms - cannot be inverted. + """True if this transform is degenerate. + + Which means that it will collapse a shape to an effective area + of zero. Degenerate transforms cannot be inverted. """ return self.determinant == 0.0 @@ -336,16 +344,16 @@ class Affine( a, b, c, d, e, f, _, _, _ = self return (a, d), (b, e), (c, f) - def almost_equals(self, other): + def almost_equals(self, other, precision=EPSILON): """Compare transforms for approximate equality. :param other: Transform being compared. :type other: Affine :return: True if absolute difference between each element - of each respective transform matrix < ``EPSILON``. + of each respective transform matrix < ``self.precision``. """ for i in (0, 1, 2, 3, 4, 5): - if abs(self[i] - other[i]) >= EPSILON: + if abs(self[i] - other[i]) >= precision: return False return True @@ -376,7 +384,8 @@ class Affine( sa, sb, sc, sd, se, sf, _, _, _ = self if isinstance(other, Affine): oa, ob, oc, od, oe, of, _, _, _ = other - return tuple.__new__(Affine, + return tuple.__new__( + Affine, (sa * oa + sb * od, sa * ob + sb * oe, sa * oc + sb * of + sc, sd * oa + se * od, sd * ob + se * oe, sd * oc + se * of + sf, 0.0, 0.0, 1.0)) @@ -385,12 +394,12 @@ class Affine( vx, vy = other except Exception: return NotImplemented - return (vx * sa + vy * sd + sc, vx * sb + vy * se + sf) + return (vx * sa + vy * sb + sc, vx * sd + vy * se + sf) def __rmul__(self, other): # We should not be called if other is an affine instance # This is just a guarantee, since we would potentially - # return the wrong answer in that case + # return the wrong answer in that case. assert not isinstance(other, Affine) return self.__mul__(other) @@ -427,7 +436,8 @@ class Affine( rb = -sb * idet rd = -sd * idet re = sa * idet - return tuple.__new__(Affine, + return tuple.__new__( + Affine, (ra, rb, -sc * ra - sf * rb, rd, re, -sc * rd - sf * re, 0.0, 0.0, 1.0)) diff --git a/affine/tests/test_rotation.py b/affine/tests/test_rotation.py new file mode 100644 index 0000000..fe783e8 --- /dev/null +++ b/affine/tests/test_rotation.py @@ -0,0 +1,53 @@ +import math + +from affine import Affine + + +def test_rotation_angle(): + """A positive angle rotates a vector counter clockwise + + (1.0, 0.0): + + | + | + | + | + 0---------* + + Affine.rotation(45.0) * (1.0, 0.0) == (0.707..., 0.707...) + + | + | * + | + | + 0---------- + """ + x, y = Affine.rotation(45.0) * (1.0, 0.0) + assert round(x, 14) == round(math.sqrt(2.0) / 2.0, 14) + assert round(y, 14) == round(math.sqrt(2.0) / 2.0, 14) + + +def test_rotation_matrix(): + """A rotation matrix has expected elements + + | cos(a) -sin(a) | + | sin(a) cos(a) | + + """ + rot = Affine.rotation(90.0) + assert round(rot.a, 15) == round(math.cos(math.pi/2.0), 15) + assert round(rot.b, 15) == round(-math.sin(math.pi/2.0), 15) + assert rot.c == 0.0 + assert round(rot.d, 15) == round(math.sin(math.pi/2.0), 15) + assert round(rot.e, 15) == round(math.cos(math.pi/2.0), 15) + assert rot.f == 0.0 + + +def test_rotation_matrix_pivot(): + """A rotation matrix with pivot has expected elements""" + rot = Affine.rotation(90.0, pivot=(1.0, 1.0)) + exp = (Affine.translation(1.0, 1.0) + * Affine.rotation(90.0) + * Affine.translation(-1.0, -1.0)) + for r, e in zip(rot, exp): + assert round(r, 15) == round(e, 15) diff --git a/affine/tests/test_transform.py b/affine/tests/test_transform.py index 7afcccb..0a21976 100644 --- a/affine/tests/test_transform.py +++ b/affine/tests/test_transform.py @@ -35,7 +35,7 @@ import unittest from textwrap import dedent from nose.tools import assert_equal, assert_almost_equal, raises -from affine import Affine +from affine import Affine, EPSILON def seq_almost_equal(t1, t2, error=0.00001): @@ -123,9 +123,9 @@ class PyAffineTestCase(unittest.TestCase): assert isinstance(trans, Affine) assert_equal( tuple(trans), - (1, 0, 2, + (1, 0, 2, 0, 1, -5, - 0, 0, 1)) + 0, 0, 1)) def test_scale_constructor(self): scale = Affine.scale(5) @@ -139,8 +139,8 @@ class PyAffineTestCase(unittest.TestCase): assert_equal( tuple(scale), (-1, 0, 0, - 0, 2, 0, - 0, 0, 1)) + 0, 2, 0, + 0, 0, 1)) assert_equal(tuple(Affine.scale(1)), tuple(Affine.identity())) def test_shear_constructor(self): @@ -158,8 +158,8 @@ class PyAffineTestCase(unittest.TestCase): seq_almost_equal( tuple(shear), (1, mx, 0, - my, 1, 0, - 0, 0, 1)) + my, 1, 0, + 0, 0, 1)) shear = Affine.shear(y_angle=45) seq_almost_equal( tuple(shear), @@ -174,16 +174,16 @@ class PyAffineTestCase(unittest.TestCase): s, c = math.sin(r), math.cos(r) assert_equal( tuple(rot), - (c, s, 0, - -s, c, 0, + (c, -s, 0, + s, c, 0, 0, 0, 1)) rot = Affine.rotation(337) r = math.radians(337) s, c = math.sin(r), math.cos(r) seq_almost_equal( tuple(rot), - (c, s, 0, - -s, c, 0, + (c, -s, 0, + s, c, 0, 0, 0, 1)) assert_equal(tuple(Affine.rotation(0)), tuple(Affine.identity())) @@ -195,29 +195,29 @@ class PyAffineTestCase(unittest.TestCase): 0, 0, 1)) assert_equal( tuple(Affine.rotation(90)), - (0, 1, 0, - -1, 0, 0, + (0, -1, 0, + 1, 0, 0, 0, 0, 1)) assert_equal( tuple(Affine.rotation(180)), - (-1, 0, 0, - 0, -1, 0, - 0, 0, 1)) + (-1, 0, 0, + 0, -1, 0, + 0, 0, 1)) assert_equal( tuple(Affine.rotation(-180)), - (-1, 0, 0, - 0, -1, 0, - 0, 0, 1)) + (-1, 0, 0, + 0, -1, 0, + 0, 0, 1)) assert_equal( tuple(Affine.rotation(270)), - (0, -1, 0, - 1, 0, 0, - 0, 0, 1)) + (0, 1, 0, + -1, 0, 0, + 0, 0, 1)) assert_equal( tuple(Affine.rotation(-90)), - (0, -1, 0, - 1, 0, 0, - 0, 0, 1)) + (0, 1, 0, + -1, 0, 0, + 0, 0, 1)) assert_equal( tuple(Affine.rotation(360)), (1, 0, 0, @@ -225,25 +225,25 @@ class PyAffineTestCase(unittest.TestCase): 0, 0, 1)) assert_equal( tuple(Affine.rotation(450)), - (0, 1, 0, - -1, 0, 0, + (0, -1, 0, + 1, 0, 0, 0, 0, 1)) assert_equal( tuple(Affine.rotation(-450)), - (0, -1, 0, - 1, 0, 0, - 0, 0, 1)) + (0, 1, 0, + -1, 0, 0, + 0, 0, 1)) def test_rotation_constructor_with_pivot(self): assert_equal(tuple(Affine.rotation(60)), - tuple(Affine.rotation(60, pivot=(0, 0)))) + tuple(Affine.rotation(60, pivot=(0, 0)))) rot = Affine.rotation(27, pivot=(2, -4)) r = math.radians(27) s, c = math.sin(r), math.cos(r) assert_equal( tuple(rot), - (c, s, 2 - 2 * c - 4 * s, - -s, c, -4 - 2 * s + 4 * c, + (c, -s, 2 - 2 * c - 4 * s, + s, c, -4 - 2 * s + 4 * c, 0, 0, 1)) assert_equal(tuple(Affine.rotation(0, (-3, 2))), tuple(Affine.identity())) @@ -288,7 +288,6 @@ class PyAffineTestCase(unittest.TestCase): assert not Affine.shear(4, -1).is_orthonormal def test_is_degenerate(self): - from affine import EPSILON assert not Affine.identity().is_degenerate assert not Affine.translation(2, -1).is_degenerate assert not Affine.shear(0, -22.5).is_degenerate @@ -310,8 +309,7 @@ class PyAffineTestCase(unittest.TestCase): assert_equal(c, (4, 7)) def test_almost_equals(self): - from affine import EPSILON - assert EPSILON != 0, EPSILON + EPSILON = 1e-5 E = EPSILON * 0.5 t = Affine(1.0, E, 0, -E, 1.0 + E, E) assert t.almost_equals(Affine.identity()) @@ -322,6 +320,18 @@ class PyAffineTestCase(unittest.TestCase): assert not Affine.identity().almost_equals(t) assert t.almost_equals(t) + def test_almost_equals_2(self): + EPSILON = 1e-10 + E = EPSILON * 0.5 + t = Affine(1.0, E, 0, -E, 1.0 + E, E) + assert t.almost_equals(Affine.identity(), precision=EPSILON) + assert Affine.identity().almost_equals(t, precision=EPSILON) + assert t.almost_equals(t, precision=EPSILON) + t = Affine(1.0, 0, 0, -EPSILON, 1.0, 0) + assert not t.almost_equals(Affine.identity(), precision=EPSILON) + assert not Affine.identity().almost_equals(t, precision=EPSILON) + assert t.almost_equals(t, precision=EPSILON) + def test_equality(self): t1 = Affine(1, 2, 3, 4, 5, 6) t2 = Affine(6, 5, 4, 3, 2, 1) @@ -397,25 +407,13 @@ class PyAffineTestCase(unittest.TestCase): seq_almost_equal( ~Affine.rotation(-33.3), Affine.rotation(33.3)) t = Affine(1, 2, 3, 4, 5, 6) - seq_almost_equal(~t * t, Affine.identity()) + seq_almost_equal(~t * t, Affine.identity()) def test_cant_invert_degenerate(self): from affine import TransformNotInvertibleError t = Affine.scale(0) self.assertRaises(TransformNotInvertibleError, lambda: ~t) - def test_set_epsilon(self): - import affine - - old_epsilon = affine.EPSILON - - try: - affine.set_epsilon(123) - assert_equal(123, affine.EPSILON) - assert_equal(123 * 123, affine.EPSILON2) - finally: - affine.set_epsilon(old_epsilon) - @raises(TypeError) def test_bad_type_world(self): from affine import loadsw @@ -459,6 +457,9 @@ class PyAffineTestCase(unittest.TestCase): self.assertTrue(a1.almost_equals(a2)) +# We're using pytest for tests added after 1.0 and don't need unittest +# test case classes. + def test_gdal(): t = Affine.from_gdal(-237481.5, 425.0, 0.0, 237536.4, 0.0, -425.0) assert t.c == t.xoff == -237481.5 @@ -471,6 +472,51 @@ def test_gdal(): assert t.to_gdal() == (-237481.5, 425.0, 0.0, 237536.4, 0.0, -425.0) +def test_imul_number(): + t = Affine(1, 2, 3, 4, 5, 6) + try: + t *= 2.0 + except TypeError: + assert True + + +def test_mul_tuple(): + t = Affine(1, 2, 3, 4, 5, 6) + t * (2.0, 2.0) + + +def test_rmul_tuple(): + t = Affine(1, 2, 3, 4, 5, 6) + (2.0, 2.0) * t + + +def test_transform_precision(): + t = Affine.rotation(45.0) + assert t.precision == EPSILON + t.precision = 1e-10 + assert t.precision == 1e-10 + assert Affine.rotation(0.0).precision == EPSILON + + +def test_associative(): + point = (12, 5) + trans = Affine.translation(-10., -5.) + rot90 = Affine.rotation(90.) + result1 = rot90 * (trans * point) + result2 = (rot90 * trans) * point + seq_almost_equal(result1, (0., 2.)) + seq_almost_equal(result1, result2) + + +def test_roundtrip(): + point = (12, 5) + trans = Affine.translation(3, 4) + rot37 = Affine.rotation(37.) + point_prime = (trans * rot37) * point + roundtrip_point = ~(trans * rot37) * point_prime + seq_almost_equal(point, roundtrip_point) + + if __name__ == '__main__': unittest.main() diff --git a/tox.ini b/tox.ini index df8aec2..407da79 100644 --- a/tox.ini +++ b/tox.ini @@ -9,4 +9,4 @@ deps = pytest-cov responses commands = - python -m pytest --cov affine --cov-report term-missing + python -m pytest affine/tests --cov affine --cov-report term-missing -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-affine.git _______________________________________________ Pkg-grass-devel mailing list [email protected] http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-grass-devel

