From 809154bc3534790fe5d0f59425a788a097aff35e Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 20 Sep 2025 20:05:59 +0200 Subject: [PATCH 1/2] feat: add __contains__ for Lazy Operators --- src/shapepy/bool2d/__init__.py | 2 ++ src/shapepy/bool2d/base.py | 16 ++++++++++- src/shapepy/bool2d/boolean.py | 50 ++++++++++++++++++++++++++++++++++ src/shapepy/bool2d/config.py | 2 ++ src/shapepy/bool2d/shape.py | 25 ++++++----------- tests/bool2d/test_lazy.py | 49 +++++++++++++++++++++++++++++++++ tests/scalar/test_reals.py | 1 + 7 files changed, 128 insertions(+), 17 deletions(-) diff --git a/src/shapepy/bool2d/__init__.py b/src/shapepy/bool2d/__init__.py index 4cef3f5d..1ec94ff3 100644 --- a/src/shapepy/bool2d/__init__.py +++ b/src/shapepy/bool2d/__init__.py @@ -7,6 +7,7 @@ from .base import Future from .boolean import ( clean_bool2d, + contains_bool2d, intersect_bool2d, invert_bool2d, unite_bool2d, @@ -21,5 +22,6 @@ Future.clean = clean_bool2d Future.convert = from_any Future.xor = xor_bool2d +Future.contains = contains_bool2d Is.lazy = is_lazy diff --git a/src/shapepy/bool2d/base.py b/src/shapepy/bool2d/base.py index a8ed764a..b60bcbbd 100644 --- a/src/shapepy/bool2d/base.py +++ b/src/shapepy/bool2d/base.py @@ -105,6 +105,10 @@ def __repr__(self) -> str: # pragma: no cover def __hash__(self): raise NotImplementedError + @debug("shapepy.bool2d.base") + def __contains__(self, other: SubSetR2) -> bool: + return Future.contains(self, other) + def clean(self) -> SubSetR2: """ Cleans the subset, changing its representation into a simpler form @@ -272,7 +276,7 @@ def __xor__(self, other: SubSetR2) -> SubSetR2: return Future.convert(other) def __contains__(self, other: SubSetR2) -> bool: - return self is other + return self is Future.convert(other) def __str__(self) -> str: return "EmptyShape" @@ -454,3 +458,13 @@ def clean(subset: SubSetR2) -> SubSetR2: in the `shapepy.bool2d.boolean.py` file """ raise NotImplementedError + + @staticmethod + def contains(subseta: SubSetR2, subsetb: SubSetR2) -> bool: + """ + Checks if subsetb is contained in subseta + + This function is overrided by a function defined + in the `shapepy.bool2d.boolean.py` file + """ + raise NotImplementedError diff --git a/src/shapepy/bool2d/boolean.py b/src/shapepy/bool2d/boolean.py index 4a6ea1d6..52c0be67 100644 --- a/src/shapepy/bool2d/boolean.py +++ b/src/shapepy/bool2d/boolean.py @@ -17,7 +17,11 @@ from ..tools import CyclicContainer, Is, NotExpectedError from . import boolalg from .base import EmptyShape, SubSetR2, WholeShape +from .config import Config +from .convert import from_any +from .curve import SingleCurve from .lazy import LazyAnd, LazyNot, LazyOr, RecipeLazy, is_lazy +from .point import SinglePoint from .shape import ( ConnectedShape, DisjointShape, @@ -171,6 +175,52 @@ def clean_bool2d_not(subset: LazyNot) -> SubSetR2: raise NotImplementedError(f"Missing typo: {type(inverted)}") +@debug("shapepy.bool2d.boolean") +def contains_bool2d(subseta: SubSetR2, subsetb: SubSetR2) -> bool: + """ + Checks if B is inside A + + Parameters + ---------- + subseta: SubSetR2 + The subset A + subsetb: SubSetR2 + The subset B + + Return + ------ + bool + The result if B is inside A + """ + subseta = from_any(subseta) + subsetb = from_any(subsetb) + if Is.instance(subseta, EmptyShape) or Is.instance(subsetb, WholeShape): + return subseta is subsetb + if Is.instance(subseta, WholeShape) or Is.instance(subsetb, EmptyShape): + return True + if Is.instance(subseta, (ConnectedShape, LazyAnd)): + return all(subsetb in s for s in subseta) + if Is.instance(subsetb, (DisjointShape, LazyOr)): + return all(s in subseta for s in subsetb) + if Is.instance(subseta, LazyNot) and Is.instance(subsetb, LazyNot): + return contains_bool2d(~subsetb, ~subseta) + if Is.instance(subseta, SimpleShape): + if Is.instance(subsetb, (SinglePoint, SingleCurve, SimpleShape)): + return subsetb in subseta + if Is.instance(subsetb, ConnectedShape): + return ~subseta in ~subsetb + if not Config.auto_clean: + raise ValueError( + f"Needs clean to evaluate: {type(subseta)}, {type(subsetb)}" + ) + return subsetb.clean() in subseta.clean() + if Is.instance(subseta, (LazyOr, DisjointShape)): + return any(subsetb in s for s in subseta) # Needs improvement + raise NotImplementedError( + f"Invalid typos: {type(subseta)}, {type(subsetb)}" + ) + + class Boolalg: """Static methods to clean a SubSetR2 using algebraic simplifier""" diff --git a/src/shapepy/bool2d/config.py b/src/shapepy/bool2d/config.py index 4a05ec24..d329ece5 100644 --- a/src/shapepy/bool2d/config.py +++ b/src/shapepy/bool2d/config.py @@ -18,6 +18,8 @@ class Config: "inv": True, } + auto_clean = True + @contextmanager def disable_auto_clean(): diff --git a/src/shapepy/bool2d/shape.py b/src/shapepy/bool2d/shape.py index e0d809cd..8ea0a526 100644 --- a/src/shapepy/bool2d/shape.py +++ b/src/shapepy/bool2d/shape.py @@ -10,7 +10,7 @@ from __future__ import annotations from copy import copy -from typing import Iterable, Set, Tuple, Union +from typing import Iterable, Iterator, Set, Tuple, Union from ..geometry.box import Box from ..geometry.jordancurve import JordanCurve @@ -19,7 +19,7 @@ from ..scalar.angle import Angle from ..scalar.reals import Real from ..tools import Is, To -from .base import EmptyShape, Future, SubSetR2 +from .base import Future, SubSetR2 from .curve import SingleCurve from .density import ( Density, @@ -133,20 +133,15 @@ def invert(self) -> SimpleShape: self.__jordancurve.invert() return self + @debug("shapepy.bool2d.shape") def __contains__(self, other: SubSetR2) -> bool: - if not Is.instance(other, SubSetR2): - other = Future.convert(other) if Is.instance(other, SinglePoint): return self.__contains_point(other) if Is.instance(other, SingleCurve): return self.__contains_curve(other) if Is.instance(other, SimpleShape): return self.__contains_simple(other) - if Is.instance(other, ConnectedShape): - return ~self in ~other - if Is.instance(other, DisjointShape): - return all(o in self for o in other.subshapes) - return Is.instance(other, EmptyShape) + return super().__contains__(other) def __contains_point(self, point: SinglePoint) -> bool: point = Future.convert(point) @@ -261,6 +256,9 @@ def __eq__(self, other: SubSetR2) -> bool: def __hash__(self): return hash(self.area) + def __iter__(self) -> Iterator[SimpleShape]: + yield from self.subshapes + @property def jordans(self) -> Tuple[JordanCurve, ...]: """Jordan curves that defines the shape @@ -308,9 +306,6 @@ def subshapes(self, simples: Iterable[SimpleShape]): raise TypeError self.__subshapes = simples - def __contains__(self, other) -> bool: - return all(other in s for s in self.subshapes) - def move(self, vector: Point2D) -> JordanCurve: vector = To.point(vector) for subshape in self.subshapes: @@ -376,10 +371,8 @@ def __deepcopy__(self, memo): subshapes = tuple(map(copy, self.subshapes)) return DisjointShape(subshapes) - def __contains__(self, other: SubSetR2) -> bool: - if Is.instance(other, DisjointShape): - return all(o in self for o in other.subshapes) - return any(other in s for s in self.subshapes) + def __iter__(self) -> Iterator[Union[SimpleShape, ConnectedShape]]: + yield from self.subshapes @property def area(self) -> Real: diff --git a/tests/bool2d/test_lazy.py b/tests/bool2d/test_lazy.py index e5df05c6..ce9f515f 100644 --- a/tests/bool2d/test_lazy.py +++ b/tests/bool2d/test_lazy.py @@ -394,6 +394,54 @@ def test_density(): assert abs(float(density) - value) < 1e-9 +@pytest.mark.order(33) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_invert", + "test_unite", + "test_intersect", + "test_hash", + "test_xor", + "test_transformation_move", + "test_transformation_scale", + "test_transformation_rotate", + "test_printing", + "test_clean", + ] +) +def test_contains(): + small = Primitive.square(side=1) + bigsq = Primitive.square(side=2) + assert small in bigsq + assert LazyNot(bigsq) in LazyNot(small) + + inters = LazyAnd([small, bigsq]) + assert small in inters + assert bigsq not in inters + assert inters in inters + assert LazyNot(bigsq) in LazyNot(inters) + assert (0, 0) in small + assert (0, 0) in bigsq + assert (0, 0) in inters + assert (1, 1) not in inters + + union = LazyOr([small, bigsq]) + assert small in union + assert bigsq in union + assert union in union + assert (0, 0) in small + assert (0, 0) in bigsq + assert (0, 0) in union + assert (1, 1) in union + + left = Primitive.square(side=4, center=(-1, 0)) + righ = Primitive.square(side=4, center=(1, 0)) + union = LazyOr([left, righ]) + assert left in union + assert righ in union + + @pytest.mark.order(33) @pytest.mark.dependency( depends=[ @@ -409,6 +457,7 @@ def test_density(): "test_hash", "test_copy", "test_density", + "test_contains", ] ) def test_all(): diff --git a/tests/scalar/test_reals.py b/tests/scalar/test_reals.py index eb0454b5..ba109be7 100644 --- a/tests/scalar/test_reals.py +++ b/tests/scalar/test_reals.py @@ -6,6 +6,7 @@ @pytest.mark.order(1) +# @pytest.mark.skip() @pytest.mark.timeout(1) @pytest.mark.dependency() def test_constants(): From da8a2f67b6a6074983bc624e68090d9c9c4e29ac Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 20 Sep 2025 20:18:55 +0200 Subject: [PATCH 2/2] remove comment to skip test --- tests/scalar/test_reals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/scalar/test_reals.py b/tests/scalar/test_reals.py index ba109be7..eb0454b5 100644 --- a/tests/scalar/test_reals.py +++ b/tests/scalar/test_reals.py @@ -6,7 +6,6 @@ @pytest.mark.order(1) -# @pytest.mark.skip() @pytest.mark.timeout(1) @pytest.mark.dependency() def test_constants():