From ca6f256d1f67bbd42714d6119ddc97c1d2ed5cf0 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:45:51 -0500 Subject: [PATCH 1/7] Adjust circle calculation to center within data bounds --- src/data_morph/shapes/circles.py | 8 ++++---- tests/shapes/circles/bases.py | 3 ++- tests/shapes/circles/test_circle.py | 12 +++++++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/data_morph/shapes/circles.py b/src/data_morph/shapes/circles.py index c251da1c..43ba00b7 100644 --- a/src/data_morph/shapes/circles.py +++ b/src/data_morph/shapes/circles.py @@ -41,11 +41,11 @@ class Circle(Shape): """ def __init__(self, dataset: Dataset, radius: Number | None = None) -> None: - self.center: np.ndarray = dataset.data[['x', 'y']].mean().to_numpy() - """numpy.ndarray: The (x, y) coordinates of the circle's center.""" + self.center: tuple[Number, Number] = dataset.data_bounds.center + """The (x, y) coordinates of the circle's center.""" self.radius: Number = radius or dataset.data[['x', 'y']].std().mean() * 1.5 - """numbers.Number: The radius of the circle.""" + """The radius of the circle.""" def __repr__(self) -> str: x, y = self.center @@ -130,7 +130,7 @@ def __init__(self, dataset: Dataset, num_rings: int = 4) -> None: Circle(dataset, r) for r in np.linspace(stdev / num_rings * 2, stdev * 2, num_rings) ] - """list[Circle]: The individual rings represented by :class:`Circle` objects.""" + """The individual rings represented by :class:`Circle` objects.""" self._centers = np.array([circle.center for circle in self.circles]) self._radii = np.array([circle.radius for circle in self.circles]) diff --git a/tests/shapes/circles/bases.py b/tests/shapes/circles/bases.py index ca1912af..ace59eab 100644 --- a/tests/shapes/circles/bases.py +++ b/tests/shapes/circles/bases.py @@ -30,7 +30,8 @@ def test_distance(self, shape, test_point, expected_distance): Test the distance() method parametrized by distance_test_cases (see conftest.py). """ - assert pytest.approx(shape.distance(*test_point)) == expected_distance + actual_distance = shape.distance(*test_point) + assert pytest.approx(actual_distance) == expected_distance def test_repr(self, shape): """Test that the __repr__() method is working.""" diff --git a/tests/shapes/circles/test_circle.py b/tests/shapes/circles/test_circle.py index 7e1372a8..4a4589b4 100644 --- a/tests/shapes/circles/test_circle.py +++ b/tests/shapes/circles/test_circle.py @@ -11,8 +11,18 @@ class TestCircle(CirclesModuleTestBase): """Test the Circle class.""" + center_x, center_y = (20, 65) + radius = 20.49038105676658 shape_name = 'circle' - distance_test_cases = (((20, 50), 10.490381), ((10, 25), 15.910168)) + distance_test_cases = ( + ((center_x, center_y + radius), 0), # north + ((center_x, center_y - radius), 0), # south + ((center_x + radius, center_y), 0), # east + ((center_x - radius, center_y), 0), # west + ((center_x, center_y), radius), # center of circle, radius is distance + ((10, 25), 20.740675199410028), # inside the circle + ((-20, 0), 55.831306555602154), # outside the circle + ) repr_regex = '^' + CIRCLE_REPR + '$' def test_is_circle(self, shape): From ce53fa2c60a26bcd902e3ce368eacc498e0d5c47 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 22 Feb 2025 12:42:17 -0500 Subject: [PATCH 2/7] Rework logic for bullseye and rings shapes --- src/data_morph/shapes/circles.py | 213 ---------------------- src/data_morph/shapes/circles/__init__.py | 11 ++ src/data_morph/shapes/circles/bullseye.py | 50 +++++ src/data_morph/shapes/circles/circle.py | 93 ++++++++++ src/data_morph/shapes/circles/rings.py | 119 ++++++++++++ tests/shapes/circles/test_bullseye.py | 39 +++- tests/shapes/circles/test_circle.py | 2 +- tests/shapes/circles/test_rings.py | 72 ++++++-- 8 files changed, 368 insertions(+), 231 deletions(-) delete mode 100644 src/data_morph/shapes/circles.py create mode 100644 src/data_morph/shapes/circles/__init__.py create mode 100644 src/data_morph/shapes/circles/bullseye.py create mode 100644 src/data_morph/shapes/circles/circle.py create mode 100644 src/data_morph/shapes/circles/rings.py diff --git a/src/data_morph/shapes/circles.py b/src/data_morph/shapes/circles.py deleted file mode 100644 index 43ba00b7..00000000 --- a/src/data_morph/shapes/circles.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Shapes that are circular in nature.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import matplotlib.pyplot as plt -import numpy as np - -from ..plotting.style import plot_with_custom_style -from .bases.shape import Shape - -if TYPE_CHECKING: - from numbers import Number - - from matplotlib.axes import Axes - - from ..data.dataset import Dataset - - -class Circle(Shape): - """ - Class representing a hollow circle. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.circles import Circle - - _ = Circle(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - radius : numbers.Number, optional - The radius of the circle. - """ - - def __init__(self, dataset: Dataset, radius: Number | None = None) -> None: - self.center: tuple[Number, Number] = dataset.data_bounds.center - """The (x, y) coordinates of the circle's center.""" - - self.radius: Number = radius or dataset.data[['x', 'y']].std().mean() * 1.5 - """The radius of the circle.""" - - def __repr__(self) -> str: - x, y = self.center - return f'<{self.__class__.__name__} center={(float(x), float(y))} radius={self.radius}>' - - def distance(self, x: Number, y: Number) -> float: - """ - Calculate the absolute distance between this circle's edge and a point (x, y). - - Parameters - ---------- - x, y : numbers.Number - Coordinates of a point in 2D space. - - Returns - ------- - float - The absolute distance between this circle's edge and the point (x, y). - """ - return abs( - self._euclidean_distance(self.center, np.array([x, y])) - self.radius - ) - - @plot_with_custom_style - def plot(self, ax: Axes | None = None) -> Axes: - """ - Plot the shape. - - Parameters - ---------- - ax : matplotlib.axes.Axes, optional - An optional :class:`~matplotlib.axes.Axes` object to plot on. - - Returns - ------- - matplotlib.axes.Axes - The :class:`~matplotlib.axes.Axes` object containing the plot. - """ - if not ax: - fig, ax = plt.subplots(layout='constrained') - fig.get_layout_engine().set(w_pad=0.2, h_pad=0.2) - _ = ax.axis('equal') - _ = ax.add_patch(plt.Circle(self.center, self.radius, ec='k', fill=False)) - _ = ax.autoscale() - return ax - - -class Rings(Shape): - """ - Class representing rings comprising multiple concentric circles. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.circles import Rings - - _ = Rings(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - num_rings : int, default ``4`` - The number of rings to include. Must be greater than 1. - - See Also - -------- - Circle : The individual rings are represented as circles. - """ - - def __init__(self, dataset: Dataset, num_rings: int = 4) -> None: - if not isinstance(num_rings, int): - raise TypeError('num_rings must be an integer') - if num_rings <= 1: - raise ValueError('num_rings must be greater than 1') - - stdev = dataset.data.std().mean() - self.circles: list[Circle] = [ - Circle(dataset, r) - for r in np.linspace(stdev / num_rings * 2, stdev * 2, num_rings) - ] - """The individual rings represented by :class:`Circle` objects.""" - - self._centers = np.array([circle.center for circle in self.circles]) - self._radii = np.array([circle.radius for circle in self.circles]) - - def __repr__(self) -> str: - return self._recursive_repr('circles') - - def distance(self, x: Number, y: Number) -> float: - """ - Calculate the minimum absolute distance between any of this shape's - circles' edges and a point (x, y). - - Parameters - ---------- - x, y : numbers.Number - Coordinates of a point in 2D space. - - Returns - ------- - float - The minimum absolute distance between any of this shape's - circles' edges and the point (x, y). - - See Also - -------- - Circle.distance : - Rings consists of multiple circles, so we use the minimum - distance to one of the circles. - """ - point = np.array([x, y]) - return np.min( - np.abs(np.linalg.norm(self._centers - point, axis=1) - self._radii) - ) - - @plot_with_custom_style - def plot(self, ax: Axes | None = None) -> Axes: - """ - Plot the shape. - - Parameters - ---------- - ax : matplotlib.axes.Axes, optional - An optional :class:`~matplotlib.axes.Axes` object to plot on. - - Returns - ------- - matplotlib.axes.Axes - The :class:`~matplotlib.axes.Axes` object containing the plot. - """ - for circle in self.circles: - ax = circle.plot(ax) - return ax - - -class Bullseye(Rings): - """ - Class representing a bullseye shape comprising two concentric circles. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.circles import Bullseye - - _ = Bullseye(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - - See Also - -------- - Rings : The Bullseye is a special case where we only have 2 rings. - """ - - def __init__(self, dataset: Dataset) -> None: - super().__init__(dataset=dataset, num_rings=2) diff --git a/src/data_morph/shapes/circles/__init__.py b/src/data_morph/shapes/circles/__init__.py new file mode 100644 index 00000000..7468cd8c --- /dev/null +++ b/src/data_morph/shapes/circles/__init__.py @@ -0,0 +1,11 @@ +"""Shapes made up of circles.""" + +from .bullseye import Bullseye +from .circle import Circle +from .rings import Rings + +__all__ = [ + 'Bullseye', + 'Circle', + 'Rings', +] diff --git a/src/data_morph/shapes/circles/bullseye.py b/src/data_morph/shapes/circles/bullseye.py new file mode 100644 index 00000000..b4fd6d09 --- /dev/null +++ b/src/data_morph/shapes/circles/bullseye.py @@ -0,0 +1,50 @@ +"""Bullseye shape.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from .rings import Rings + +if TYPE_CHECKING: + from ..data.dataset import Dataset + + +class Bullseye(Rings): + """ + Class representing a bullseye shape comprising two concentric circles. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.circles import Bullseye + + _ = Bullseye(DataLoader.load_dataset('panda')).plot() + + See Also + -------- + Circle : The individual rings are represented as circles. + """ + + @staticmethod + def _derive_radii(dataset: Dataset) -> np.ndarray: + """ + Derive the radii for the circles in the bullseye. + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into. + + Returns + ------- + np.ndarray + The radii for the circles in the bullseye. + """ + stdev = dataset.data[['x', 'y']].std().mean() * 1.5 + return np.linspace(stdev, 0, 2, endpoint=False) diff --git a/src/data_morph/shapes/circles/circle.py b/src/data_morph/shapes/circles/circle.py new file mode 100644 index 00000000..1aee9e2a --- /dev/null +++ b/src/data_morph/shapes/circles/circle.py @@ -0,0 +1,93 @@ +"""Circle shape.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import matplotlib.pyplot as plt +import numpy as np + +from ...plotting.style import plot_with_custom_style +from ..bases.shape import Shape + +if TYPE_CHECKING: + from numbers import Number + + from matplotlib.axes import Axes + + from ..data.dataset import Dataset + + +class Circle(Shape): + """ + Class representing a hollow circle. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.circles import Circle + + _ = Circle(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + radius : numbers.Number, optional + The radius of the circle. + """ + + def __init__(self, dataset: Dataset, radius: Number | None = None) -> None: + self.center: tuple[Number, Number] = dataset.data_bounds.center + """The (x, y) coordinates of the circle's center.""" + + self.radius: Number = radius or dataset.data[['x', 'y']].std().mean() * 1.5 + """The radius of the circle.""" + + def __repr__(self) -> str: + x, y = self.center + return f'<{self.__class__.__name__} center={(float(x), float(y))} radius={self.radius}>' + + def distance(self, x: Number, y: Number) -> float: + """ + Calculate the absolute distance between this circle's edge and a point (x, y). + + Parameters + ---------- + x, y : numbers.Number + Coordinates of a point in 2D space. + + Returns + ------- + float + The absolute distance between this circle's edge and the point (x, y). + """ + return abs( + self._euclidean_distance(self.center, np.array([x, y])) - self.radius + ) + + @plot_with_custom_style + def plot(self, ax: Axes | None = None) -> Axes: + """ + Plot the shape. + + Parameters + ---------- + ax : matplotlib.axes.Axes, optional + An optional :class:`~matplotlib.axes.Axes` object to plot on. + + Returns + ------- + matplotlib.axes.Axes + The :class:`~matplotlib.axes.Axes` object containing the plot. + """ + if not ax: + fig, ax = plt.subplots(layout='constrained') + fig.get_layout_engine().set(w_pad=0.2, h_pad=0.2) + _ = ax.axis('equal') + _ = ax.add_patch(plt.Circle(self.center, self.radius, ec='k', fill=False)) + _ = ax.autoscale() + return ax diff --git a/src/data_morph/shapes/circles/rings.py b/src/data_morph/shapes/circles/rings.py new file mode 100644 index 00000000..1fbd8af2 --- /dev/null +++ b/src/data_morph/shapes/circles/rings.py @@ -0,0 +1,119 @@ +"""Rings shape.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from ...plotting.style import plot_with_custom_style +from ..bases.shape import Shape +from .circle import Circle + +if TYPE_CHECKING: + from numbers import Number + + from matplotlib.axes import Axes + + from ..data.dataset import Dataset + + +class Rings(Shape): + """ + Class representing rings comprising three concentric circles. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.circles import Rings + + _ = Rings(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + + See Also + -------- + Circle : The individual rings are represented as circles. + """ + + def __init__(self, dataset: Dataset) -> None: + self.circles: list[Circle] = [ + Circle(dataset, r) for r in self._derive_radii(dataset) + ] + """The individual rings represented by :class:`Circle` objects.""" + + self._centers = np.array([circle.center for circle in self.circles]) + self._radii = np.array([circle.radius for circle in self.circles]) + + def __repr__(self) -> str: + return self._recursive_repr('circles') + + @staticmethod + def _derive_radii(dataset: Dataset) -> np.ndarray: + """ + Derive the radii for the circles in the rings. + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into. + + Returns + ------- + np.ndarray + The radii for the circles in the rings. + """ + stdev = (min(dataset.data_bounds.range) + min(dataset.morph_bounds.range)) / 4 + return np.linspace(stdev, 0, 3, endpoint=False) + + def distance(self, x: Number, y: Number) -> float: + """ + Calculate the minimum absolute distance between any of this shape's + circles' edges and a point (x, y). + + Parameters + ---------- + x, y : numbers.Number + Coordinates of a point in 2D space. + + Returns + ------- + float + The minimum absolute distance between any of this shape's + circles' edges and the point (x, y). + + See Also + -------- + Circle.distance : + Rings consists of multiple circles, so we use the minimum + distance to one of the circles. + """ + point = np.array([x, y]) + return np.min( + np.abs(np.linalg.norm(self._centers - point, axis=1) - self._radii) + ) + + @plot_with_custom_style + def plot(self, ax: Axes | None = None) -> Axes: + """ + Plot the shape. + + Parameters + ---------- + ax : matplotlib.axes.Axes, optional + An optional :class:`~matplotlib.axes.Axes` object to plot on. + + Returns + ------- + matplotlib.axes.Axes + The :class:`~matplotlib.axes.Axes` object containing the plot. + """ + for circle in self.circles: + ax = circle.plot(ax) + return ax diff --git a/tests/shapes/circles/test_bullseye.py b/tests/shapes/circles/test_bullseye.py index 3bd0d239..cf6485ed 100644 --- a/tests/shapes/circles/test_bullseye.py +++ b/tests/shapes/circles/test_bullseye.py @@ -12,7 +12,44 @@ class TestBullseye(CirclesModuleTestBase): """Test the Bullseye class.""" shape_name = 'bullseye' - distance_test_cases = (((20, 50), 3.660254), ((10, 25), 9.08004)) + center_x, center_y = (20, 65) + inner_radius, outer_radius = (10.24519052838329, 20.49038105676658) + mid_radius = (outer_radius + inner_radius) / 2 + distance_test_cases = ( + ((center_x, center_y + outer_radius), 0), # north on outer ring + ((center_x, center_y + inner_radius), 0), # north on inner ring + ((center_x, center_y - outer_radius), 0), # south on outer ring + ((center_x, center_y - inner_radius), 0), # south on inner ring + ((center_x + outer_radius, center_y), 0), # east on outer ring + ((center_x + inner_radius, center_y), 0), # east on inner ring + ((center_x - outer_radius, center_y), 0), # west on outer ring + ((center_x - inner_radius, center_y), 0), # west on inner ring + ((center_x, center_y), inner_radius), # center of bullseye + ( + (center_x, center_y + mid_radius), + inner_radius / 2, + ), # between the circles (north) + ( + (center_x, center_y - mid_radius), + inner_radius / 2, + ), # between the circles (south) + ( + (center_x + mid_radius, center_y), + inner_radius / 2, + ), # between the circles (east) + ( + (center_x - mid_radius, center_y), + inner_radius / 2, + ), # between the circles (west) + ( + (center_x, center_y + outer_radius * 2), + outer_radius, + ), # north of both circles (north) + ( + (center_x - outer_radius * 1.5, center_y), + inner_radius, + ), # west of both circles + ) repr_regex = ( r'^\n' r' circles=\n' diff --git a/tests/shapes/circles/test_circle.py b/tests/shapes/circles/test_circle.py index 4a4589b4..078132af 100644 --- a/tests/shapes/circles/test_circle.py +++ b/tests/shapes/circles/test_circle.py @@ -11,9 +11,9 @@ class TestCircle(CirclesModuleTestBase): """Test the Circle class.""" + shape_name = 'circle' center_x, center_y = (20, 65) radius = 20.49038105676658 - shape_name = 'circle' distance_test_cases = ( ((center_x, center_y + radius), 0), # north ((center_x, center_y - radius), 0), # south diff --git a/tests/shapes/circles/test_rings.py b/tests/shapes/circles/test_rings.py index 5ef0776c..e07831b3 100644 --- a/tests/shapes/circles/test_rings.py +++ b/tests/shapes/circles/test_rings.py @@ -12,34 +12,74 @@ class TestRings(CirclesModuleTestBase): """Test the Rings class.""" shape_name = 'rings' - distance_test_cases = (((20, 50), 3.16987), ((10, 25), 9.08004)) + center_x, center_y = (20, 65) + radii = (3.666666666666667, 7.333333333333334, 11) + mid_radii = (sum(radii[:2]) / 2, sum(radii[1:]) / 2) + distance_test_cases = ( + ((center_x, center_y + radii[0]), 0), # north on inner ring + ((center_x, center_y + radii[1]), 0), # north on middle ring + ((center_x, center_y + radii[2]), 0), # north on outer ring + ((center_x, center_y - radii[0]), 0), # south on inner ring + ((center_x, center_y - radii[1]), 0), # south on middle ring + ((center_x, center_y - radii[2]), 0), # south on outer ring + ((center_x + radii[0], center_y), 0), # east on inner ring + ((center_x + radii[1], center_y), 0), # east on middle ring + ((center_x + radii[2], center_y), 0), # east on outer ring + ((center_x - radii[0], center_y), 0), # west on inner ring + ((center_x - radii[1], center_y), 0), # west on middle ring + ((center_x - radii[2], center_y), 0), # west on outer ring + ((center_x, center_y), radii[0]), # center of all rings + ( + (center_x, center_y + mid_radii[0]), + radii[0] / 2, + ), # between the inner circles (north) + ( + (center_x, center_y - mid_radii[0]), + radii[0] / 2, + ), # between the inner circles (south) + ( + (center_x + mid_radii[0], center_y), + radii[0] / 2, + ), # between the inner circles (east) + ( + (center_x - mid_radii[0], center_y), + radii[0] / 2, + ), # between the inner circles (west) + ( + (center_x, center_y + mid_radii[1]), + radii[0] / 2, + ), # between the outer circles (north) + ( + (center_x, center_y - mid_radii[1]), + radii[0] / 2, + ), # between the outer circles (south) + ( + (center_x + mid_radii[1], center_y), + radii[0] / 2, + ), # between the outer circles (east) + ( + (center_x - mid_radii[1], center_y), + radii[0] / 2, + ), # between the outer circles (west) + ((center_x, center_y + radii[2] * 2), radii[2]), # north of all circles (north) + ((center_x - radii[2] * 1.5, center_y), radii[2] / 2), # west of all circles + ) repr_regex = ( r'^\n' r' circles=\n' r' ' + CIRCLE_REPR + '\n' r' ' + CIRCLE_REPR + '\n' - r' ' + CIRCLE_REPR + '\n' r' ' + CIRCLE_REPR + '$' ) - @pytest.mark.parametrize('num_rings', [3, 5]) - def test_init(self, shape_factory, num_rings): - """Test that the Rings contains multiple concentric circles.""" - shape = shape_factory.generate_shape(self.shape_name, num_rings=num_rings) + def test_init(self, shape_factory): + """Test that the Rings contains three concentric circles.""" + shape = shape_factory.generate_shape(self.shape_name) + num_rings = 3 assert len(shape.circles) == num_rings assert all( np.array_equal(circle.center, shape.circles[0].center) for circle in shape.circles[1:] ) assert len({circle.radius for circle in shape.circles}) == num_rings - - @pytest.mark.parametrize('num_rings', ['3', -5, 1, True]) - def test_num_rings_is_valid(self, shape_factory, num_rings): - """Test that num_rings input validation is working.""" - if isinstance(num_rings, int): - with pytest.raises(ValueError, match='num_rings must be greater than 1'): - _ = shape_factory.generate_shape(self.shape_name, num_rings=num_rings) - else: - with pytest.raises(TypeError, match='num_rings must be an integer'): - _ = shape_factory.generate_shape(self.shape_name, num_rings=num_rings) From c9d6763722db97422e51a4069099d7ba2ae0aef0 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 22 Feb 2025 12:49:35 -0500 Subject: [PATCH 3/7] Update types on BoundingBox returns --- src/data_morph/bounds/bounding_box.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data_morph/bounds/bounding_box.py b/src/data_morph/bounds/bounding_box.py index 1fe77437..8aa4d8c2 100644 --- a/src/data_morph/bounds/bounding_box.py +++ b/src/data_morph/bounds/bounding_box.py @@ -170,25 +170,25 @@ def clone(self) -> BoundingBox: ) @property - def range(self) -> Iterable[Number]: + def range(self) -> tuple[Number, Number]: """ Calculate the range (width) of the bounding box in each direction. Returns ------- - Iterable[numbers.Number] + tuple[Number, Number] The range covered by the x and y bounds, respectively. """ return self.x_bounds.range, self.y_bounds.range @property - def center(self) -> Iterable[Number]: + def center(self) -> tuple[Number, Number]: """ Calculate the center of the bounding box. Returns ------- - Iterable[numbers.Number] + tuple[Number, Number] The center of the x and y bounds, respectively. """ return self.x_bounds.center, self.y_bounds.center From 4105a5668b021d1518d98c10d43e619e5709dc50 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 22 Feb 2025 12:52:00 -0500 Subject: [PATCH 4/7] Adjust comments --- tests/shapes/circles/test_bullseye.py | 2 +- tests/shapes/circles/test_circle.py | 2 +- tests/shapes/circles/test_rings.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/shapes/circles/test_bullseye.py b/tests/shapes/circles/test_bullseye.py index cf6485ed..0b5053de 100644 --- a/tests/shapes/circles/test_bullseye.py +++ b/tests/shapes/circles/test_bullseye.py @@ -44,7 +44,7 @@ class TestBullseye(CirclesModuleTestBase): ( (center_x, center_y + outer_radius * 2), outer_radius, - ), # north of both circles (north) + ), # north of both circles ( (center_x - outer_radius * 1.5, center_y), inner_radius, diff --git a/tests/shapes/circles/test_circle.py b/tests/shapes/circles/test_circle.py index 078132af..2daa251d 100644 --- a/tests/shapes/circles/test_circle.py +++ b/tests/shapes/circles/test_circle.py @@ -19,7 +19,7 @@ class TestCircle(CirclesModuleTestBase): ((center_x, center_y - radius), 0), # south ((center_x + radius, center_y), 0), # east ((center_x - radius, center_y), 0), # west - ((center_x, center_y), radius), # center of circle, radius is distance + ((center_x, center_y), radius), # center of circle ((10, 25), 20.740675199410028), # inside the circle ((-20, 0), 55.831306555602154), # outside the circle ) diff --git a/tests/shapes/circles/test_rings.py b/tests/shapes/circles/test_rings.py index e07831b3..502a9901 100644 --- a/tests/shapes/circles/test_rings.py +++ b/tests/shapes/circles/test_rings.py @@ -61,7 +61,7 @@ class TestRings(CirclesModuleTestBase): (center_x - mid_radii[1], center_y), radii[0] / 2, ), # between the outer circles (west) - ((center_x, center_y + radii[2] * 2), radii[2]), # north of all circles (north) + ((center_x, center_y + radii[2] * 2), radii[2]), # north of all circles ((center_x - radii[2] * 1.5, center_y), radii[2] / 2), # west of all circles ) repr_regex = ( From dbd5669f7462389e757e5084e07a646fb4c027fa Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:52:15 -0500 Subject: [PATCH 5/7] Add test for plotting to CirclesModuleTestBase --- tests/shapes/circles/bases.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/shapes/circles/bases.py b/tests/shapes/circles/bases.py index ace59eab..a8d94883 100644 --- a/tests/shapes/circles/bases.py +++ b/tests/shapes/circles/bases.py @@ -5,8 +5,12 @@ import re from typing import TYPE_CHECKING +import matplotlib.pyplot as plt +import numpy as np import pytest +from data_morph.shapes.circles import Circle + if TYPE_CHECKING: from numbers import Number @@ -36,3 +40,28 @@ def test_distance(self, shape, test_point, expected_distance): def test_repr(self, shape): """Test that the __repr__() method is working.""" assert re.match(self.repr_regex, repr(shape)) is not None + + @pytest.mark.parametrize('ax', [None, plt.subplots()[1]]) + def test_plot(self, shape, ax): + """Test that the plot() method is working.""" + plot_ax = shape.plot(ax) + if ax: + assert plot_ax is ax + else: + assert plot_ax is not ax + + plotted_circles = plot_ax.patches + plotted_centers = [plotted_circle._center for plotted_circle in plotted_circles] + plotted_radii = [ + plotted_circle._width / 2 for plotted_circle in plotted_circles + ] + if isinstance(shape, Circle): + assert len(plotted_circles) == 1 + assert plotted_centers[0] == shape.center + assert plotted_radii[0] == shape.radius + else: + assert len(plotted_circles) == len(shape.circles) + assert np.setdiff1d(shape._centers, plotted_centers).size == 0 + assert np.setdiff1d(shape._radii, plotted_radii).size == 0 + + plt.close() From abc4f266a28441bbf9089da111f44800871945bd Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 22 Feb 2025 14:21:24 -0500 Subject: [PATCH 6/7] Fix test leakage --- tests/shapes/circles/bases.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/shapes/circles/bases.py b/tests/shapes/circles/bases.py index a8d94883..ae8210fd 100644 --- a/tests/shapes/circles/bases.py +++ b/tests/shapes/circles/bases.py @@ -44,6 +44,9 @@ def test_repr(self, shape): @pytest.mark.parametrize('ax', [None, plt.subplots()[1]]) def test_plot(self, shape, ax): """Test that the plot() method is working.""" + if ax: + ax.clear() + plot_ax = shape.plot(ax) if ax: assert plot_ax is ax @@ -51,17 +54,18 @@ def test_plot(self, shape, ax): assert plot_ax is not ax plotted_circles = plot_ax.patches - plotted_centers = [plotted_circle._center for plotted_circle in plotted_circles] - plotted_radii = [ + plotted_centers = {plotted_circle._center for plotted_circle in plotted_circles} + plotted_radii = { plotted_circle._width / 2 for plotted_circle in plotted_circles - ] + } + if isinstance(shape, Circle): assert len(plotted_circles) == 1 - assert plotted_centers[0] == shape.center - assert plotted_radii[0] == shape.radius + assert plotted_centers == {shape.center} + assert plotted_radii == {shape.radius} else: assert len(plotted_circles) == len(shape.circles) - assert np.setdiff1d(shape._centers, plotted_centers).size == 0 - assert np.setdiff1d(shape._radii, plotted_radii).size == 0 + assert plotted_centers == {tuple(np.unique(shape._centers))} + assert plotted_radii.difference(shape._radii) == set() plt.close() From 275c8564eb1bfb7021c923b4cd8ff065c566aff1 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 22 Feb 2025 15:04:38 -0500 Subject: [PATCH 7/7] Fix docstring; rename variable --- src/data_morph/shapes/circles/rings.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/data_morph/shapes/circles/rings.py b/src/data_morph/shapes/circles/rings.py index 1fbd8af2..bf571c20 100644 --- a/src/data_morph/shapes/circles/rings.py +++ b/src/data_morph/shapes/circles/rings.py @@ -44,7 +44,7 @@ class Rings(Shape): def __init__(self, dataset: Dataset) -> None: self.circles: list[Circle] = [ - Circle(dataset, r) for r in self._derive_radii(dataset) + Circle(dataset, radius) for radius in self._derive_radii(dataset) ] """The individual rings represented by :class:`Circle` objects.""" @@ -87,12 +87,6 @@ def distance(self, x: Number, y: Number) -> float: float The minimum absolute distance between any of this shape's circles' edges and the point (x, y). - - See Also - -------- - Circle.distance : - Rings consists of multiple circles, so we use the minimum - distance to one of the circles. """ point = np.array([x, y]) return np.min(