Skip to content

Commit

Permalink
Feat: Add the area_signed function (#296)
Browse files Browse the repository at this point in the history
Co-authored-by: cristiano.pizzamiglio <cristiano.pizzamiglio@limacorporate.com>
Co-authored-by: Andrew Hynes <andrewjhynes@gmail.com>
  • Loading branch information
3 people committed Feb 26, 2022
1 parent d48be7f commit 7e80b9b
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 1 deletion.
6 changes: 5 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: scikit-spatial

on: push
on:
pull_request:
push:
branches:
- master

jobs:
build:
Expand Down
1 change: 1 addition & 0 deletions docs/source/api_reference/skspatial.measurement.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ skspatial.measurement
.. autosummary::
:toctree: measurement/functions

area_signed
area_triangle
volume_tetrahedron
63 changes: 63 additions & 0 deletions src/skspatial/measurement.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Measurements using spatial objects."""
import numpy as np

from skspatial.objects import Points
from skspatial.objects import Vector
from skspatial.typing import array_like

Expand Down Expand Up @@ -97,3 +98,65 @@ def volume_tetrahedron(
vector_ab = vector_ab.set_dimension(3)

return 1 / 6 * abs(vector_ab.dot(vector_cross))


def area_signed(points: array_like) -> float:
"""
Return the signed area of a simple polygon given the 2D coordinates of its veritces.
The signed area is computed using the shoelace algorithm. A positive area is
returned for a polygon whose vertices are given by a counter-clockwise
sequence of points.
Parameters
----------
points : array_like
Input 2D points.
Returns
-------
area_signed : float
The signed area of the polygon.
Raises
------
ValueError
If the points are not 2D.
If there are fewer than three points.
References
----------
https://en.wikipedia.org/wiki/Shoelace_formula
https://alexkritchevsky.com/2018/08/06/oriented-area.html
https://rosettacode.org/wiki/Shoelace_formula_for_polygonal_area#Python
Examples
--------
>>> from skspatial.measurement import area_signed
>>> area_signed([[0, 0], [1, 0], [0, 1]])
0.5
>>> area_signed([[0, 0], [0, 1], [1, 0]])
-0.5
>>> area_signed([[0, 0], [0, 1], [1, 2], [2, 1], [2, 0]])
-3.0
"""
points = Points(points)
n_points = points.shape[0]

if points.dimension != 2:
raise ValueError("The points must be 2D.")

if n_points < 3:
raise ValueError("There must be at least 3 points.")

X = points[:, 0]
Y = points[:, 1]

indices = np.arange(n_points)
indices_offset = indices - 1

return 0.5 * np.sum(X[indices_offset] * Y[indices] - X[indices] * Y[indices_offset])
42 changes: 42 additions & 0 deletions tests/unit/test_measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import numpy as np
import pytest

from skspatial.measurement import area_signed
from skspatial.measurement import area_triangle
from skspatial.measurement import volume_tetrahedron
from skspatial.objects import Points


@pytest.mark.parametrize(
Expand Down Expand Up @@ -41,3 +43,43 @@ def test_volume_tetrahedron(array_a, array_b, array_c, array_d, volume_expected)

volume = volume_tetrahedron(array_a, array_b, array_c, array_d)
assert math.isclose(volume, volume_expected)


@pytest.mark.parametrize(
("points", "area_expected"),
[
# Counter-clockwise triangle
([[2, 2], [6, 2], [4, 5]], 6),
# Clockwise triangle
([[1, 3], [-4, 3], [-3, 4]], -2.5),
# Counter-clockwise square
([[-1, 2], [2, 5], [-1, 8], [-4, 5]], 18),
# Clockwise irregular convex pentagon
([[-2, 2], [-5, 2], [-8, 5], [-4, 8], [-1, 5]], -25.5),
# Counter-clockwise irregular convex hexagon
([[3, -2], [6, -3], [10, -1], [8, 4], [4, 3], [1, 1]], 39.5),
# Clockwise non-convex polygon
([[5, -2], [1, -1], [0, 4], [6, 6], [3, 3]], -22),
# Self-overlapping polygon
([[-4, 4], [-4, 1], [2, 4], [2, 1]], 0),
],
)
def test_area_signed(points, area_expected):

points = Points(points)
area = area_signed(points)

assert area == area_expected


@pytest.mark.parametrize(
("points", "message_expected"),
[
([[1, 0, 0], [-1, 0, 0], [0, 1, 0]], "The points must be 2D."),
([[2, 0], [-2, 0]], "There must be at least 3 points."),
],
)
def test_area_signed_failure(points, message_expected):

with pytest.raises(ValueError, match=message_expected):
area_signed(points)

0 comments on commit 7e80b9b

Please sign in to comment.