[python-affine] 01/06: New upstream version 2.1.0

Bas Couwenberg sebastic at debian.org
Wed Jul 12 16:25:32 UTC 2017


This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository python-affine.

commit 16f458108543bc0ac8a4d560fee4d409e6635aca
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Wed Jul 12 18:11:25 2017 +0200

    New upstream version 2.1.0
---
 .travis.yml                    |  4 +-
 AUTHORS.txt                    |  1 +
 CHANGES.txt                    | 10 ++++-
 CODE_OF_CONDUCT.txt            | 25 +++++++++++++
 affine/__init__.py             | 84 ++++++++++++++++++++++++++++++++++++++----
 affine/tests/test_transform.py | 43 ++++++++++++++++++++-
 6 files changed, 154 insertions(+), 13 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index e5c6898..6951742 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,11 @@
 sudo: false
 language: python
 python:
-  - "2.6"
   - "2.7"
   - "3.3"
   - "3.4"
   - "3.5"
+  - "3.6"
 install:
   - "pip install pytest pytest-cov nose"
   - "pip install coveralls"
@@ -20,5 +20,3 @@ deploy:
   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 02e814b..1c72558 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -6,3 +6,4 @@ Authors
 - Mike Toews <mwtoews at gmail.com>
 - Kevin Wurster <wursterk at gmail.com>
 - Todd Small <todd_small at icloud.com>
+- Juan Luis Cano Rodríguez <juanlu at satellogic.com>
diff --git a/CHANGES.txt b/CHANGES.txt
index 8317435..a84acc5 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,9 +1,15 @@
 CHANGES
 =======
 
-2.0.0 (2016-05-20)
+2.1.0 (2017-07-12)
 ------------------
-- Final release.
+- Addition of new ``eccentricity`` and ``rotation_angle`` properties (#28).
+
+2.0.0.post1 (2016-05-20)
+------------------------
+- This is the final 2.0.0 release. The post-release version segment is used
+  because we accidentally uploaded a 2.0.0 to PyPI before the beta releases
+  below.
 
 2.0b2 (2016-05-16)
 ------------------
diff --git a/CODE_OF_CONDUCT.txt b/CODE_OF_CONDUCT.txt
new file mode 100644
index 0000000..ad8236f
--- /dev/null
+++ b/CODE_OF_CONDUCT.txt
@@ -0,0 +1,25 @@
+Contributor Code of Conduct
+---------------------------
+
+As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery
+* Personal attacks
+* Trolling or insulting/derogatory comments
+* Public or private harassment
+* Publishing other's private information, such as physical or electronic addresses, without explicit permission
+* Other unethical or unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
+
+This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.2.0, available at http://contributor-covenant.org/version/1/2/0/
+
+.. _Contributor Covenant: http://contributor-covenant.org
diff --git a/affine/__init__.py b/affine/__init__.py
index 68659e9..b2db1a0 100644
--- a/affine/__init__.py
+++ b/affine/__init__.py
@@ -47,14 +47,23 @@ import math
 
 __all__ = ['Affine']
 __author__ = "Sean Gillies"
-__version__ = "2.0.0"
+__version__ = "2.1.0"
 
 EPSILON = 1e-5
 
 
-class TransformNotInvertibleError(Exception):
+class AffineError(Exception):
+    pass
+
+
+class TransformNotInvertibleError(AffineError):
     """The transform could not be inverted"""
 
+
+class UndefinedRotationError(AffineError):
+    """The rotation angle could not be computed for this transform"""
+
+
 # Define assert_unorderable() depending on the language
 # implicit ordering rules. This keeps things consistent
 # across major Python versions
@@ -127,7 +136,7 @@ class Affine(
     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 
+    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.
 
@@ -136,10 +145,10 @@ class Affine(
     """
     precision = EPSILON
 
-    def __new__(self, *members):
+    def __new__(cls, *members):
         if len(members) == 6:
             mat3x3 = [x * 1.0 for x in members] + [0.0, 0.0, 1.0]
-            return tuple.__new__(Affine, mat3x3)
+            return tuple.__new__(cls, mat3x3)
         else:
             raise TypeError(
                 "Expected 6 coefficients, found %d" % len(members))
@@ -286,6 +295,59 @@ class Affine(
         return a * e - b * d
 
     @property
+    def _scaling(self):
+        """The absolute scaling factors of the transformation.
+
+        This tuple represents the absolute value of the scaling factors of the
+        transformation, sorted from bigger to smaller.
+        """
+        a, b, _, d, e, _, _, _, _ = self
+
+        # The singular values are the square root of the eigenvalues
+        # of the matrix times its transpose, M M*
+        # Computing trace and determinant of M M*
+        trace = a**2 + b**2 + d**2 + e**2
+        det = (a * e - b * d)**2
+
+        delta = trace**2 / 4 - det
+        if delta < 1e-12:
+            delta = 0
+
+        l1 = math.sqrt(trace / 2 + math.sqrt(delta))
+        l2 = math.sqrt(trace / 2 - math.sqrt(delta))
+        return l1, l2
+
+    @property
+    def eccentricity(self):
+        """The eccentricity of the affine transformation.
+
+        This value represents the eccentricity of an ellipse under
+        this affine transformation.
+
+        Raises NotImplementedError for improper transformations.
+        """
+        l1, l2 = self._scaling
+        return math.sqrt(l1 ** 2 - l2 ** 2) / l1
+
+    @property
+    def rotation_angle(self):
+        """The rotation angle in degrees of the affine transformation.
+
+        This is the rotation angle in degrees of the affine transformation,
+        assuming it is in the form M = R S, where R is a rotation and S is a
+        scaling.
+
+        Raises NotImplementedError for improper transformations.
+        """
+        a, b, _, c, d, _, _, _, _ = self
+        if self.is_proper or self.is_degenerate:
+            l1, _ = self._scaling
+            y, x = c / l1, a / l1
+            return math.atan2(y, x) * 180 / math.pi
+        else:
+            raise UndefinedRotationError
+
+    @property
     def is_identity(self):
         """True if this transform equals the identity matrix,
         within rounding limits.
@@ -338,6 +400,14 @@ class Affine(
         """
         return self.determinant == 0.0
 
+    @cached_property
+    def is_proper(self):
+        """True if this transform is proper.
+
+        Which means that it does not include reflection.
+        """
+        return self.determinant > 0.0
+
     @property
     def column_vectors(self):
         """The values of the transform as three 2D column vectors"""
@@ -385,7 +455,7 @@ class Affine(
         if isinstance(other, Affine):
             oa, ob, oc, od, oe, of, _, _, _ = other
             return tuple.__new__(
-                Affine,
+                self.__class__,
                 (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))
@@ -437,7 +507,7 @@ class Affine(
         rd = -sd * idet
         re = sa * idet
         return tuple.__new__(
-            Affine,
+            self.__class__,
             (ra, rb, -sc * ra - sf * rb,
              rd, re, -sc * rd - sf * re,
              0.0, 0.0, 1.0))
diff --git a/affine/tests/test_transform.py b/affine/tests/test_transform.py
index 0a21976..2f7c50c 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, EPSILON
+from affine import Affine, EPSILON, UndefinedRotationError
 
 
 def seq_almost_equal(t1, t2, error=0.00001):
@@ -517,6 +517,47 @@ def test_roundtrip():
     seq_almost_equal(point, roundtrip_point)
 
 
+def test_eccentricity():
+    assert_equal(Affine.identity().eccentricity, 0.0)
+    assert_equal(Affine.scale(2).eccentricity, 0.0)
+    #assert_equal(Affine.scale(0).eccentricity, ?)
+    assert_almost_equal(Affine.scale(2, 1).eccentricity, math.sqrt(3) / 2)
+    assert_almost_equal(Affine.scale(2, 3).eccentricity, math.sqrt(5) / 3)
+    assert_equal(Affine.scale(1, 0).eccentricity, 1.0)
+    assert_almost_equal(Affine.rotation(77).eccentricity, 0.0)
+    assert_almost_equal(Affine.translation(32, -47).eccentricity, 0.0)
+    assert_almost_equal(Affine.scale(-1, 1).eccentricity, 0.0)
+
+
+def test_eccentricity_complex():
+    assert_almost_equal(
+        (Affine.scale(2, 3) * Affine.rotation(77)).eccentricity,
+        math.sqrt(5) / 3
+    )
+    assert_almost_equal(
+        (Affine.rotation(77) * Affine.scale(2, 3)).eccentricity,
+        math.sqrt(5) / 3
+    )
+    assert_almost_equal(
+        (Affine.translation(32, -47) * Affine.rotation(77) * Affine.scale(2, 3)).eccentricity,
+        math.sqrt(5) / 3
+    )
+
+
+def test_rotation_angle():
+    assert_equal(Affine.identity().rotation_angle, 0.0)
+    assert_equal(Affine.scale(2).rotation_angle, 0.0)
+    assert_equal(Affine.scale(2, 1).rotation_angle, 0.0)
+    assert_almost_equal(Affine.translation(32, -47).rotation_angle, 0.0)
+    assert_almost_equal(Affine.rotation(30).rotation_angle, 30)
+    assert_almost_equal(Affine.rotation(-150).rotation_angle, -150)
+
+
+ at raises(UndefinedRotationError)
+def test_rotation_improper():
+    Affine.scale(-1, 1).rotation_angle
+
+
 if __name__ == '__main__':
     unittest.main()
 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-affine.git



More information about the Pkg-grass-devel mailing list