Repository: lace/vg

Issue:
Error using signed_angle with vector array
Hello!

I am calling
```python
angles = vg.signed_angle(vec1_array.T, points.T, look, units='rad')
```
with shapes
```
('points', (3, 8))
('vec1_array', (3, 8))
('look', (3, 1))
```

However, I get the error:
```
Traceback (most recent call last):
  File "./extract_fe_pictures.py", line 113, in <module>
    test_code('skywalker_2013_mod', plot=False, interactive=False, export_path='.', verbose=False)
  File "./extract_fe_pictures.py", line 28, in test_code
    safe_poly.plot()
  File "/home/george/ros_workspaces/uav_ftc/src/uav_ftc/src/uav_ftc/polytope_utils.py", line 1271, in plot
    face_points = self._get_face_points(temp_polytope)
  File "/home/george/ros_workspaces/uav_ftc/src/uav_ftc/src/uav_ftc/polytope_utils.py", line 853, in _get_face_points
    angles = vg.signed_angle(vec1_array.T, points.T, look, units='rad')
  File "/usr/local/lib/python2.7/dist-packages/vg/core.py", line 297, in signed_angle
    return sign * angle(v1, v2, look, units=units)
  File "/usr/local/lib/python2.7/dist-packages/vg/core.py", line 251, in angle
    v1, v2 = [reject(v, from_v=look) for v in (v1, v2)]
  File "/usr/local/lib/python2.7/dist-packages/vg/core.py", line 125, in reject
    return vector - project(vector, onto=from_v)
  File "/usr/local/lib/python2.7/dist-packages/vg/core.py", line 92, in project
    return scalar_projection(vector, onto=onto)[:, np.newaxis] * normalize(onto)
  File "/usr/local/lib/python2.7/dist-packages/vg/core.py", line 112, in scalar_projection
    check(locals(), "onto", (k, 3))
  File "/usr/local/lib/python2.7/dist-packages/vg/shape.py", line 93, in check
    return check_value(locals_namespace[name], shape, name=name)
  File "/usr/local/lib/python2.7/dist-packages/vg/shape.py", line 54, in check_value
    raise ValueError("{} with shape {}; got {}".format(preamble, shape, a.shape))
ValueError: onto must be an array with shape (8, 3); got (3, 1)
```

I believe I have set the dimensions correctly.
Am I missing something?

Thanks!



Comments:

Patch:
diff --git a/vg/core.py b/vg/core.py
index 9b81c34..df58395 100644
--- a/vg/core.py
+++ b/vg/core.py
@@ -62,8 +62,8 @@ def perpendicular(v1, v2, normalized=True):
     are both stacked, `result[k]` is perpendicular to `v1[k]` and `v2[k]`.)
 
     Args:
-        v1 (np.arraylike): A `3x1` vector or a `kx3` stack of vectors.
-        v2 (np.arraylike): A `3x1 vector or a `kx3` stack of vectors. If
+        v1 (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors.
+        v2 (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors. If
             stacked, the shape must be the same as `v1`.
         normalized (bool): When `True`, the result vector is guaranteed to be
             unit length.
@@ -167,15 +167,15 @@ def reject_axis(vector, axis, squash=False):
 
 def magnitude(vector):
     """
-    Compute the magnitude of `vector`. For stacked inputs, compute the magnitude
-    of each one.
+    Compute the magnitude of `vector`. For a stacked input, compute the
+    magnitude of each one.
 
     Args:
-        vector (np.arraylike): A `3x1` vector or a `kx3` stack of vectors.
+        vector (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors.
 
     Returns:
-        object: For `3x1` inputs, a `float` with the magnitude. For `kx1`
-            inputs, a `kx1` array.
+        object: For a `(3,)` input, a `float` with the magnitude. For a `kx3`
+            input, a `(k,)` array.
     """
     if vector.ndim == 1:
         return np.linalg.norm(vector)
@@ -197,14 +197,14 @@ def euclidean_distance(v1, v2):
     points.
 
     Args:
-        v1 (np.arraylike): A `3x1` vector or a `kx3` stack of vectors.
-        v2 (np.arraylike): A `3x1` vector or a `kx3` stack of vectors. If
+        v1 (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors.
+        v2 (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors. If
             stacks are provided for both `v1` and `v2` they must have the
             same shape.
 
     Returns:
-        object: When both inputs are `3x1`, a `float` with the distance. Otherwise
-            a `kx1` array.
+        object: When both inputs are `(3,)`, a `float` with the distance.
+        Otherwise a `(k,)` array.
     """
     k = check_value_any(v1, (3,), (-1, 3), name="v1")
     check_value_any(
@@ -219,17 +219,17 @@ def euclidean_distance(v1, v2):
 
 def angle(v1, v2, look=None, assume_normalized=False, units="deg"):
     """
-    Compute the unsigned angle between two vectors. For stacked inputs, the
+    Compute the unsigned angle between two vectors. For a stacked input, the
     angle is computed pairwise.
 
     When `look` is provided, the angle is computed in that viewing plane
     (`look` is the normal). Otherwise the angle is computed in 3-space.
 
     Args:
-        v1 (np.arraylike): A `3x1` vector or a `kx3` stack of vectors.
+        v1 (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors.
         v2 (np.arraylike): A vector or stack of vectors with the same shape as
             `v1`.
-        look (np.arraylike): A `3x1` vector specifying the normal of a viewing
+        look (np.arraylike): A `(3,)` vector specifying the normal of a viewing
             plane, or `None` to compute the angle in 3-space.
         assume_normalized (bool): When `True`, assume the input vectors
             are unit length. This improves performance, however when the inputs
@@ -237,8 +237,8 @@ def angle(v1, v2, look=None, assume_normalized=False, units="deg"):
         units (str): `'deg'` to return degrees or `'rad'` to return radians.
 
     Return:
-        object: For `3x1` inputs, a `float` with the angle. For `kx1` inputs,
-            a `kx1` array.
+        object: For a `(3,)` input, a `float` with the angle. For a `kx3`
+        input, a `(k,)` array.
     """
     if units not in ["deg", "rad"]:
         raise ValueError("Unrecognized units {}; expected deg or rad".format(units))
@@ -266,7 +266,7 @@ def angle(v1, v2, look=None, assume_normalized=False, units="deg"):
 
 def signed_angle(v1, v2, look, units="deg"):
     """
-    Compute the signed angle between two vectors. For stacked inputs, the
+    Compute the signed angle between two vectors. For a stacked input, the
     angle is computed pairwise.
 
     Results are in the range -180 and 180 (or `-math.pi` and `math.pi`). A
@@ -274,16 +274,16 @@ def signed_angle(v1, v2, look, units="deg"):
     number is counterclockwise.
 
     Args:
-        v1 (np.arraylike): A `3x1` vector or a `kx3` stack of vectors.
+        v1 (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors.
         v2 (np.arraylike): A vector or stack of vectors with the same shape as
             `v1`.
-        look (np.arraylike): A `3x1` vector specifying the normal of the
+        look (np.arraylike): A `(3,)` vector specifying the normal of the
             viewing plane.
         units (str): `'deg'` to return degrees or `'rad'` to return radians.
 
     Returns:
-        object: For `3x1` inputs, a `float` with the angle. For `kx1` inputs,
-            a `kx1` array.
+        object: For a `(3,)` input, a `float` with the angle. For a `kx3`
+        input, a `(k,)` array.
     """
     # The sign of (A x B) dot look gives the sign of the angle.
     # > 0 means clockwise, < 0 is counterclockwise.
@@ -301,8 +301,8 @@ def rotate(vector, around_axis, angle, units="deg", assume_normalized=False):
     around `around_axis` is determined by the right-hand rule.
 
     Args:
-        vector (np.arraylike): A `3x1` vector or a `kx3` stack of vectors.
-        around_axis (np.arraylike): A `3x1` vector specifying the axis of rotation.
+        vector (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors.
+        around_axis (np.arraylike): A `(3,)` vector specifying the axis of rotation.
         assume_normalized (bool): When `True`, assume `around_axis` is unit
             length. This improves performance marginally, however
             when the inputs are not normalized, setting this will cause an
@@ -412,14 +412,15 @@ def almost_zero(v, atol=1e-08):
 
 def almost_unit_length(vector, atol=1e-08):
     """
-    Test if the `vector` has almost unit length. For stacked inputs, test each
+    Test if the `vector` has almost unit length. For a stacked input, test each
     one.
 
     Args:
-        vector (np.arraylike): A `3x1` vector or a `kx3` stack of vectors.
+        vector (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors.
 
     Returns:
-        object: For `3x1` inputs, a `bool`. For `kx1` inputs, a `kx1` array.
+        object: For a `(3,)` input, a `bool`. For a `kx3` input, a `(k,)`
+        array.
     """
     return np.isclose(magnitude(vector), 1.0, rtol=0, atol=atol)
 
@@ -481,7 +482,7 @@ def major_axis(coords):
         coords (np.arraylike): A `nxk` stack of coordinates.
 
     Returns:
-        np.ndarray: A `kx1` vector.
+        np.ndarray: A `(k,)` vector.
 
     See also:
         - http://setosa.io/ev/principal-component-analysis/
@@ -497,7 +498,7 @@ def apex(points, along):
 
     Args:
         points (np.arraylike): A `kx3` stack of points in R^3.
-        along (np.arraylike): A `3x1` vector specifying the direction of
+        along (np.arraylike): A `(3,)` vector specifying the direction of
             interest.
 
     Returns:
@@ -506,7 +507,7 @@ def apex(points, along):
     if points.ndim != 2 or points.shape[1] != 3:
         raise ValueError("Invalid shape %s: apex expects nx3" % (points.shape,))
     if along.shape != (3,):
-        raise ValueError("along should be a 3x1 vector")
+        raise ValueError("along should be a (3,) vector")
     coords_on_axis = points.dot(along)
     return points[np.argmax(coords_on_axis)].copy()
 
@@ -517,11 +518,11 @@ def nearest(from_points, to_point, ret_index=False):
 
     Args:
         from_points (np.arraylike): A `kx3` stack of points in R^3.
-        to_point (np.arraylike): A `3x1` point of interest.
+        to_point (np.arraylike): A `(3,)` point of interest.
         ret_index (bool): When `True`, return both the point and its index.
 
     Returns:
-        np.ndarray: A `3x1` vector taken from `from_points`.
+        np.ndarray: A `(3,)` vector taken from `from_points`.
     """
     check(locals(), "from_points", (-1, 3))
     check(locals(), "to_point", (3,))
@@ -543,18 +544,18 @@ def farthest(from_points, to_point, ret_index=False):
 
     Args:
         from_points (np.arraylike): A `kx3` stack of points in R^3.
-        to_point (np.arraylike): A `3x1` point of interest.
+        to_point (np.arraylike): A `(3,)` point of interest.
         ret_index (bool): When `True`, return both the point and its index.
 
     Returns:
-        np.ndarray: A `3x1` vector taken from `from_points`.
+        np.ndarray: A `(3,)` vector taken from `from_points`.
     """
     if from_points.ndim != 2 or from_points.shape[1] != 3:
         raise ValueError(
             "Invalid shape %s: farthest expects nx3" % (from_points.shape,)
         )
     if to_point.shape != (3,):
-        raise ValueError("to_point should be 3x1")
+        raise ValueError("to_point should be (3,)")
 
     absolute_distances = magnitude(from_points - to_point)
 
@@ -575,21 +576,21 @@ def within(points, radius, of_point, atol=1e-08, ret_indices=False):
         points (np.arraylike): A `kx3` stack of points in R^3.
         radius (float): The radius of the sphere of interest centered on
             `of_point`.
-        of_point (np.arraylike): The `3x1` point of interest.
+        of_point (np.arraylike): The `(3,)` point of interest.
         atol (float): The distance tolerance. Points within `radius + atol`
             of `of_point` are selected.
         ret_indexes (bool): When `True`, return both the points and their
             indices.
 
     Returns:
-        np.ndarray: A `3x1` vector taken from `points`.
+        np.ndarray: A `(3,)` vector taken from `points`.
     """
     if points.ndim != 2 or points.shape[1] != 3:
         raise ValueError("Invalid shape %s: within expects nx3" % (points.shape,))
     if not isinstance(radius, float):
         raise ValueError("radius should be a float")
     if of_point.shape != (3,):
-        raise ValueError("to_point should be 3x1")
+        raise ValueError("to_point should be (3,)")
 
     absolute_distances = magnitude(points - of_point)
     (indices_within_radius,) = (absolute_distances < radius + atol).nonzero()
@@ -632,8 +633,8 @@ def dot(v1, v2):
     Compute individual or pairwise dot products.
 
     Args:
-        v1 (np.arraylike): A `3x1` vector or a `kx3` stack of vectors.
-        v2 (np.arraylike): A `3x1` vector or a `kx3` stack of vectors. If
+        v1 (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors.
+        v2 (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors. If
             stacks are provided for both `v1` and `v2` they must have the
             same shape.
     """
@@ -651,8 +652,8 @@ def cross(v1, v2):
     Compute individual or pairwise cross products.
 
     Args:
-        v1 (np.arraylike): A `3x1` vector or a `kx3` stack of vectors.
-        v2 (np.arraylike): A `3x1` vector or a `kx3` stack of vectors. If
+        v1 (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors.
+        v2 (np.arraylike): A `(3,)` vector or a `kx3` stack of vectors. If
             stacks are provided for both `v1` and `v2` they must have the
             same shape.
     """
diff --git a/vg/matrix.py b/vg/matrix.py
index db3cefb..4356fc0 100644
--- a/vg/matrix.py
+++ b/vg/matrix.py
@@ -54,7 +54,7 @@ def transform(vertices, transform):
         raise_dimension_error(vertices)
 
     if matrix.shape[1] != 3:
-        raise ValueError("Vertices should be 3x1 or Nx3")
+        raise ValueError("Vertices should be (3,) or Nx3")
 
     result = unpad(np.dot(transform, pad_with_ones(matrix).T).T)
     return result[0] if vertices.ndim == 1 else result
diff --git a/vg/shape.py b/vg/shape.py
index ffdd88b..004c001 100644
--- a/vg/shape.py
+++ b/vg/shape.py
@@ -157,15 +157,18 @@ def check(locals_namespace, name, shape):
 
 def columnize(arr, shape=(-1, 3), name=None):
     """
-    Helper for functions which may accept many stacks of three points (kx3)
-    returning a stack of results, or a single set of three points (3x1)
-    returning a single result.
-
-    Returns the points as kx3, and a `transform_result` function which can
-    be applied to the result. It picks off the first result in the 3x1 case.
-
-    Not limited to kx3; this can be used different dimensional shapes like
-    kx4, or higher dimensional shapes like kx3x3.
+    Helper for functions which may accept a stack of points (`kx3`) returning
+    a stack of results, or a single set of three points `(3,)` returning a
+    single result.
+
+    For either kind of input, it returns the points as `kx3`, a boolean
+    `is_columnized`, and a `maybe_decolumnized` function which can be applied
+    to the result before returning it. For a columnized input this function
+    does nothing, and for a non-columnized input, it decolumnizes it,
+    producing the desired return value.
+
+    This is not limited to `kx3`. It can be used for different dimensional
+    shapes like `kx4`, and even higher dimensional shapes like `kx3x3`.
     """
     if not isinstance(shape, tuple):
         raise ValueError("shape should be a tuple")


Test patch:
diff --git a/vg/test_apex.py b/vg/test_apex.py
index 605aa67..d42f5f9 100644
--- a/vg/test_apex.py
+++ b/vg/test_apex.py
@@ -45,10 +45,10 @@ def test_apex():
     # Test non-normalized too.
     np.testing.assert_array_equal(vg.apex(points, along=np.array([1, 1, 1])), expected)
 
-    with pytest.raises(ValueError, match="Invalid shape \\(3,\\): apex expects nx3"):
+    with pytest.raises(ValueError, match=r"Invalid shape \(3,\): apex expects nx3"):
         vg.apex(vg.basis.x, along=vg.basis.x)
 
-    with pytest.raises(ValueError, match="along should be a 3x1 vector"):
+    with pytest.raises(ValueError, match=r"along should be a \(3,\) vector"):
         vg.apex(points, along=points)
 
 
diff --git a/vg/test_farthest.py b/vg/test_farthest.py
index a9bba6c..6af5f4f 100644
--- a/vg/test_farthest.py
+++ b/vg/test_farthest.py
@@ -17,10 +17,8 @@ def test_farthest():
         vg.farthest(from_points, to_point, ret_index=False), from_points[1]
     )
 
-    with pytest.raises(
-        ValueError, match="Invalid shape \\(3,\\): farthest expects nx3"
-    ):
+    with pytest.raises(ValueError, match=r"Invalid shape \(3,\): farthest expects nx3"):
         vg.farthest(to_point, to_point)
 
-    with pytest.raises(ValueError, match="to_point should be 3x1"):
+    with pytest.raises(ValueError, match=r"to_point should be \(3,\)"):
         vg.farthest(from_points, from_points)
diff --git a/vg/test_matrix_transform.py b/vg/test_matrix_transform.py
index 5e6908c..9ead30e 100644
--- a/vg/test_matrix_transform.py
+++ b/vg/test_matrix_transform.py
@@ -28,7 +28,7 @@ def test_apply_homogeneous_stacked():
 def test_apply_homogeneous_error():
     with pytest.raises(ValueError, match="Transformation matrix should be 4x4"):
         apply_transform(np.array([1.0, 2.0, 3.0]), np.array([1.0]))
-    with pytest.raises(ValueError, match="Vertices should be 3x1 or Nx3"):
+    with pytest.raises(ValueError, match=r"Vertices should be \(3,\) or Nx3"):
         apply_transform(np.array([1.0, 2.0]), transform)
     with pytest.raises(ValueError, match="Not sure what to do with 3 dimensions"):
         apply_transform(np.array([[[1.0, 2.0, 3.0]]]), transform)
diff --git a/vg/test_within.py b/vg/test_within.py
index e310b17..e84ee3b 100644
--- a/vg/test_within.py
+++ b/vg/test_within.py
@@ -66,7 +66,7 @@ def test_within_error():
             radius=False,
             of_point=np.array([0.0, 1.0, 0.0]),
         )
-    with pytest.raises(ValueError, match="to_point should be 3x1"):
+    with pytest.raises(ValueError, match=r"to_point should be \(3,\)"):
         vg.within(
             np.array([[2.0, 4.0, 0.0]]), radius=4.0, of_point=np.array([0.0, 1.0])
         )