diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d6600eba566..90aadc97f34f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Fixed `NotImplementedErorr` when calling `BrepLoop.vertices`. * Fixed `python -m compas` to detect extensions based on `importlib` rather than `pkg_resources`. * `compas_rhino.uninstall` will try to remove compas packages from all possible install locations. +* Changed `angle_vectors_projected` to raise `ValueError` when an input vector is parallel to projection normal. +* Changed `angle_vectors` to raise `ValueError` when one of the input vectors is a zero-length vector instead of returning 0. ### Removed diff --git a/src/compas/geometry/_core/angles.py b/src/compas/geometry/_core/angles.py index b95c1f9c25a5..d49b8c30a075 100644 --- a/src/compas/geometry/_core/angles.py +++ b/src/compas/geometry/_core/angles.py @@ -32,6 +32,11 @@ def angle_vectors(u, v, deg=False, tol=None): The tolerance for comparing values to zero. Default is :attr:`TOL.absolute`. + Raises + ------ + ValueError + If one of the input vectors is a zero-length vector. + Returns ------- float @@ -44,32 +49,16 @@ def angle_vectors(u, v, deg=False, tol=None): 1.57079 """ - L = length_vector(u) * length_vector(v) - if TOL.is_zero(L, tol): - return 0 - a = dot_vectors(u, v) / L + len_u = length_vector(u) + len_v = length_vector(v) + + if TOL.is_zero(len_u, tol) or TOL.is_zero(len_v, tol): + raise ValueError("Cannot compute the angle between one or more zero-length vectors.") + + a = dot_vectors(u, v) / (len_u * len_v) a = max(min(a, 1), -1) angle = acos(a) - # a = length_vector(u) - # b = length_vector(v) - # if a < tol or b < tol: - # return 0 - # c = length_vector(subtract_vectors(u, v)) - # if c < tol: - # return 0 - # if b >= c and c >= 0: - # mu = c - (a - b) - # elif c > b and b >= 0: - # mu = b - (a - c) - # else: - # raise Exception("Invalid input vectors.") - # angle = 2 * atan(sqrt(((a - b) + c) * mu / ((a + (b + c)) * ((a - c) + b)))) - - # a = normalize_vector(u) - # b = normalize_vector(v) - # angle = 2 * atan2(length_vector(subtract_vectors(a, b)), length_vector(add_vectors(a, b))) - if deg: return degrees(angle) return angle @@ -140,6 +129,11 @@ def angle_vectors_projected(u, v, normal, deg=False, tol=None): The tolerance for comparing values to zero. Default is :attr:`TOL.absolute`. + Raises + ------ + ValueError + If one of the input vectors is parallel to the normal vector. + Returns ------- float @@ -155,7 +149,7 @@ def angle_vectors_projected(u, v, normal, deg=False, tol=None): u_cross = cross_vectors(u, normal) v_cross = cross_vectors(v, normal) - if TOL.is_allclose(u_cross, [0.0, 0.0, 0.0]) or TOL.is_allclose(v_cross, [0.0, 0.0, 0.0]): + if TOL.is_zero(length_vector(u_cross), tol) or TOL.is_zero(length_vector(v_cross), tol): raise ValueError("Cannot compute angle between vectors projected onto a plane defined by the normal vector. One of the vectors is parallel to the normal vector.") return angle_vectors_signed(u_cross, v_cross, normal, deg, tol) @@ -283,6 +277,11 @@ def angles_vectors(u, v, deg=False): deg : bool, optional If True, returns the angle in degrees. + Raises + ------ + ValueError + If one of the input vectors is a zero-length vector. + Returns ------- float diff --git a/tests/compas/geometry/test_core.py b/tests/compas/geometry/test_core.py index 1c8b69b004a7..ef17f4c6b56c 100644 --- a/tests/compas/geometry/test_core.py +++ b/tests/compas/geometry/test_core.py @@ -61,17 +61,28 @@ def test_angle_vectors(u, v, angle): assert TOL.is_close(angle_vectors(u, v), angle) -# @pytest.mark.parametrize( -# ("u", "v"), -# [ -# ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]), -# ([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]), -# ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0]), -# ], -# ) -# def test_angle_vectors_fails_when_input_is_zero(u, v): -# with pytest.raises(ZeroDivisionError): -# angle_vectors(u, v) +@pytest.mark.parametrize( + ("u", "v"), + [ + ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0]), + ([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]), + ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]), + ], +) +def test_angle_vectors_fails_when_input_is_zero(u, v): + with pytest.raises(ValueError): + angle_vectors(u, v) + + +def test_angle_vectors_fails_when_input_is_zero_within_tolerance(): + # length_vector(u) == 1e-4, which is zero within tol=1e-3 + with pytest.raises(ValueError): + angle_vectors([1e-4, 0.0, 0.0], [0.0, 1e-2, 0.0], tol=1e-3) + + +def test_angle_vectors_when_input_is_not_zero_within_tolerance(): + # length_vector(u) * length_vector(v) == 1e-4, which is not zero within tol=1e-6 + assert TOL.is_close(angle_vectors([1e-2, 0.0, 0.0], [0.0, 1e-2, 0.0], tol=1e-6), pi / 2) @pytest.mark.parametrize( @@ -93,17 +104,17 @@ def test_angles_vectors(u, v, angles): assert TOL.is_allclose(angles_vectors(u, v), (a, b)) -# @pytest.mark.parametrize( -# ("u", "v"), -# [ -# ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]), -# ([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]), -# ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0]), -# ], -# ) -# def test_angles_vectors_fails_when_input_is_zero(u, v): -# with pytest.raises(ZeroDivisionError): -# angles_vectors(u, v) +@pytest.mark.parametrize( + ("u", "v"), + [ + ([1.0, 0.0, 0.0], [0.0, 0.0, 0.0]), + ([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]), + ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]), + ], +) +def test_angles_vectors_fails_when_input_is_zero(u, v): + with pytest.raises(ValueError): + angles_vectors(u, v) @pytest.mark.parametrize( @@ -149,6 +160,11 @@ def test_angle_vectors_projected(u, v, normal, result): assert TOL.is_close(angle_vectors_projected(u, v, normal), result) +def test_angle_vectors_projected_fails_when_input_is_parallel_to_normal(): + with pytest.raises(ValueError): + angle_vectors_projected([1, 0, 0], [0, 1, 0], [1, 0, 0]) + + # ============================================================================== # average # ==============================================================================