From 76aebb7ce434f2fc04c64946e50c941b70639033 Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Wed, 12 Aug 2026 16:12:05 +0200 Subject: [PATCH 01/11] apply diff from https://github.com/compas-dev/compas/pull/1462 --- CHANGELOG.md | 1 + src/compas/geometry/_core/angles.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a16310355ec6..8b46930cd7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -161,6 +161,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Fixed a bug when printing an empty `Tree`. * Fixed a bug in `Group` for IronPython where the decoding declaration was missing. * Fixed a bug where a `Group` without name could not be added to the scene. +* Fixed a bug where `angle_vectors_projected` returned a 0 when an input vector was parallel to projection normal. Now returns `None`. ### Removed diff --git a/src/compas/geometry/_core/angles.py b/src/compas/geometry/_core/angles.py index c10835d08c9f..b95c1f9c25a5 100644 --- a/src/compas/geometry/_core/angles.py +++ b/src/compas/geometry/_core/angles.py @@ -155,6 +155,9 @@ 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]): + 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) From d3d7976f095125775c11f275227c611a808805e3 Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Wed, 12 Aug 2026 16:15:00 +0200 Subject: [PATCH 02/11] added unittest for parallel projected vector angle check --- tests/compas/geometry/test_core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/compas/geometry/test_core.py b/tests/compas/geometry/test_core.py index 1c8b69b004a7..3592d940ce8a 100644 --- a/tests/compas/geometry/test_core.py +++ b/tests/compas/geometry/test_core.py @@ -149,6 +149,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 # ============================================================================== From 68a1b0921b3e1f19366d9f6eb3bd7ee1edfdfb59 Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Wed, 12 Aug 2026 16:18:33 +0200 Subject: [PATCH 03/11] added info to docstring --- src/compas/geometry/_core/angles.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/compas/geometry/_core/angles.py b/src/compas/geometry/_core/angles.py index b95c1f9c25a5..5c73246181c7 100644 --- a/src/compas/geometry/_core/angles.py +++ b/src/compas/geometry/_core/angles.py @@ -140,6 +140,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 From bb72498af0408d4457663d350b63a9b56e351cef Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Wed, 12 Aug 2026 16:23:53 +0200 Subject: [PATCH 04/11] angle_vectors raises error when one of the input vectors has length zero --- src/compas/geometry/_core/angles.py | 7 ++++++- tests/compas/geometry/test_core.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/compas/geometry/_core/angles.py b/src/compas/geometry/_core/angles.py index 5c73246181c7..1c5ca2b66b7e 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 @@ -46,7 +51,7 @@ def angle_vectors(u, v, deg=False, tol=None): """ L = length_vector(u) * length_vector(v) if TOL.is_zero(L, tol): - return 0 + raise ValueError("Cannot compute angle between zero-length vectors.") a = dot_vectors(u, v) / L a = max(min(a, 1), -1) angle = acos(a) diff --git a/tests/compas/geometry/test_core.py b/tests/compas/geometry/test_core.py index 3592d940ce8a..83d604ae34ad 100644 --- a/tests/compas/geometry/test_core.py +++ b/tests/compas/geometry/test_core.py @@ -61,6 +61,19 @@ def test_angle_vectors(u, v, angle): assert TOL.is_close(angle_vectors(u, v), angle) +@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) + + # @pytest.mark.parametrize( # ("u", "v"), # [ From 950905021bbf2ec8ec53e8222f0a935cd89190d1 Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Wed, 12 Aug 2026 16:24:25 +0200 Subject: [PATCH 05/11] removed dead code --- src/compas/geometry/_core/angles.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/compas/geometry/_core/angles.py b/src/compas/geometry/_core/angles.py index 1c5ca2b66b7e..8075319e25fd 100644 --- a/src/compas/geometry/_core/angles.py +++ b/src/compas/geometry/_core/angles.py @@ -56,25 +56,6 @@ def angle_vectors(u, v, deg=False, tol=None): 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 From 50b0a8336aee46fa14e900ec5737be9df2e0f56a Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Wed, 12 Aug 2026 16:25:41 +0200 Subject: [PATCH 06/11] removed dead code --- tests/compas/geometry/test_core.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tests/compas/geometry/test_core.py b/tests/compas/geometry/test_core.py index 83d604ae34ad..91f90577864e 100644 --- a/tests/compas/geometry/test_core.py +++ b/tests/compas/geometry/test_core.py @@ -74,19 +74,6 @@ def test_angle_vectors_fails_when_input_is_zero(u, v): angle_vectors(u, v) -# @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", "angles"), [ @@ -106,19 +93,6 @@ 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( ("a", "b", "angle"), [ From 0b2230dea95167ff528a255a9dbd179aac7c1b76 Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Wed, 12 Aug 2026 16:37:53 +0200 Subject: [PATCH 07/11] updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b46930cd7c7..71122f629300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Renamed `Tolerance.units` to `Tolerance.unit` to better reflect the documented properties. Left `units` with deprecation warning. * Fixed `NotImplementedErorr` when calling `BrepLoop.vertices`. * Fixed `python -m compas` to detect extensions based on `importlib` rather than `pkg_resources`. +* 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 @@ -161,7 +163,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Fixed a bug when printing an empty `Tree`. * Fixed a bug in `Group` for IronPython where the decoding declaration was missing. * Fixed a bug where a `Group` without name could not be added to the scene. -* Fixed a bug where `angle_vectors_projected` returned a 0 when an input vector was parallel to projection normal. Now returns `None`. ### Removed From e877dc6155dd78067276de6c4d838dfea9efc816 Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Wed, 12 Aug 2026 16:40:17 +0200 Subject: [PATCH 08/11] better error reporting --- src/compas/geometry/_core/angles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compas/geometry/_core/angles.py b/src/compas/geometry/_core/angles.py index 8075319e25fd..dba2872ed113 100644 --- a/src/compas/geometry/_core/angles.py +++ b/src/compas/geometry/_core/angles.py @@ -51,7 +51,7 @@ def angle_vectors(u, v, deg=False, tol=None): """ L = length_vector(u) * length_vector(v) if TOL.is_zero(L, tol): - raise ValueError("Cannot compute angle between zero-length vectors.") + raise ValueError("Cannot compute the angle between one or more zero-length vectors.") a = dot_vectors(u, v) / L a = max(min(a, 1), -1) angle = acos(a) From ae069fd2f70edd5fc17216f38ddf3810615047bf Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Wed, 12 Aug 2026 17:01:01 +0200 Subject: [PATCH 09/11] tests and documentation for angles_tests as well --- src/compas/geometry/_core/angles.py | 5 +++++ tests/compas/geometry/test_core.py | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/compas/geometry/_core/angles.py b/src/compas/geometry/_core/angles.py index dba2872ed113..170ae1ee66a3 100644 --- a/src/compas/geometry/_core/angles.py +++ b/src/compas/geometry/_core/angles.py @@ -274,6 +274,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 91f90577864e..334335ca7375 100644 --- a/tests/compas/geometry/test_core.py +++ b/tests/compas/geometry/test_core.py @@ -93,6 +93,19 @@ def test_angles_vectors(u, v, angles): assert TOL.is_allclose(angles_vectors(u, v), (a, b)) +@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( ("a", "b", "angle"), [ From 50733621c4cb141ab8160e5d57be3e8b889ba58e Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Wed, 12 Aug 2026 17:11:32 +0200 Subject: [PATCH 10/11] use tolerance passed to angle_vectorss_projected --- src/compas/geometry/_core/angles.py | 2 +- tests/compas/geometry/test_core.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/compas/geometry/_core/angles.py b/src/compas/geometry/_core/angles.py index 170ae1ee66a3..ffd230e6ba5c 100644 --- a/src/compas/geometry/_core/angles.py +++ b/src/compas/geometry/_core/angles.py @@ -146,7 +146,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) diff --git a/tests/compas/geometry/test_core.py b/tests/compas/geometry/test_core.py index 334335ca7375..659a31a795ca 100644 --- a/tests/compas/geometry/test_core.py +++ b/tests/compas/geometry/test_core.py @@ -74,6 +74,17 @@ def test_angle_vectors_fails_when_input_is_zero(u, v): angle_vectors(u, v) +def test_angle_vectors_fails_when_input_is_zero_within_tolerance(): + # length_vector(u) * length_vector(v) == 1e-4, which is zero within tol=1e-3 + with pytest.raises(ValueError): + angle_vectors([1e-2, 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( ("u", "v", "angles"), [ From b4fdcae0f9369df544e276ee26c824bb605b007a Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Thu, 13 Aug 2026 10:53:18 +0200 Subject: [PATCH 11/11] tiny vectors but within tolerance above zero are individually allowed --- src/compas/geometry/_core/angles.py | 9 ++++++--- tests/compas/geometry/test_core.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/compas/geometry/_core/angles.py b/src/compas/geometry/_core/angles.py index ffd230e6ba5c..d49b8c30a075 100644 --- a/src/compas/geometry/_core/angles.py +++ b/src/compas/geometry/_core/angles.py @@ -49,10 +49,13 @@ def angle_vectors(u, v, deg=False, tol=None): 1.57079 """ - L = length_vector(u) * length_vector(v) - if TOL.is_zero(L, tol): + 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) / L + + a = dot_vectors(u, v) / (len_u * len_v) a = max(min(a, 1), -1) angle = acos(a) diff --git a/tests/compas/geometry/test_core.py b/tests/compas/geometry/test_core.py index 659a31a795ca..ef17f4c6b56c 100644 --- a/tests/compas/geometry/test_core.py +++ b/tests/compas/geometry/test_core.py @@ -75,9 +75,9 @@ def test_angle_vectors_fails_when_input_is_zero(u, v): def test_angle_vectors_fails_when_input_is_zero_within_tolerance(): - # length_vector(u) * length_vector(v) == 1e-4, which is zero within tol=1e-3 + # length_vector(u) == 1e-4, which is zero within tol=1e-3 with pytest.raises(ValueError): - angle_vectors([1e-2, 0.0, 0.0], [0.0, 1e-2, 0.0], tol=1e-3) + 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():