mikedh/trimesh

Regressions in test_boolean.py with Blender 4.2.0

Closed this issue · 4 comments

The following regressions appear to be associated with the update from Blender 4.1.1 to 4.2.0 in Fedora 40, the current latest stable release. (We didn’t first see these regressions in the upcoming Fedora 41 or in Rawhide first because the maintainers of the blender package in Fedora have not yet been able to build it with Python 3.13.)

=================================== FAILURES ===================================
___________________________ BooleanTest.test_boolean ___________________________
[gw2] linux -- Python 3.12.4 /usr/bin/python3   

self = <tests.test_boolean.BooleanTest testMethod=test_boolean>

    def test_boolean(self):
        a, b = self.a, self.b

        times = {}
        for engine, exists in engines:
            # if we have all_dep set it means we should fail if
            # engine is not installed so don't continue
            if not exists:
                g.log.warning("skipping boolean engine %s", engine)
                continue

            g.log.info("Testing boolean ops with engine %s", engine)

            tic = g.time.time()

            # do all booleans before checks so we can time the backends
            ab = a.difference(b, engine=engine)
            ba = b.difference(a, engine=engine)
            i = a.intersection(b, engine=engine)
            u = a.union(b, engine=engine)

            times[engine] = g.time.time() - tic

>           assert ab.is_volume
E           assert False
E            +  where False = <trimesh.Trimesh(vertices.shape=(0, 3), faces.shape=(0, 3))>.is_volume

tests/test_boolean.py:49: AssertionError
__________________________ BooleanTest.test_multiple ___________________________
[gw2] linux -- Python 3.12.4 /usr/bin/python3

self = <tests.test_boolean.BooleanTest testMethod=test_multiple>

    def test_multiple(self):
        """
        Make sure boolean operations work on multiple meshes.
        """
        for engine, exists in engines:
            if not exists:
                continue
            a = g.trimesh.primitives.Sphere(center=[0, 0, 0])
            b = g.trimesh.primitives.Sphere(center=[0, 0, 0.75])
            c = g.trimesh.primitives.Sphere(center=[0, 0, 1.5])

            r = g.trimesh.boolean.union([a, b, c], engine=engine)

>           assert r.is_volume
E           assert False
E            +  where False = <trimesh.Trimesh(vertices.shape=(0, 3), faces.shape=(0, 3))>.is_volume

tests/test_boolean.py:82: AssertionError

=========================== short test summary info ============================
FAILED tests/test_boolean.py::BooleanTest::test_boolean - assert False
FAILED tests/test_boolean.py::BooleanTest::test_multiple - assert False
============ 2 failed, 623 passed, 19 warnings in 248.03s (0:04:08) ============

I don’t really have any further insight into this, but I’m happy to provide any other information that might be helpful.

Thanks for the report! Should be fixed by #2270.

Great! #2270 (as released in 4.4.7) works for me. Thanks!

Great! #2270 (as released in 4.4.7) works for me. Thanks!

I spoke slightly too soon – these still fail, but only on s390x:

=================================== FAILURES ===================================
___________________________ BooleanTest.test_boolean ___________________________
[gw0] linux -- Python 3.12.4 /usr/bin/python3
self = <tests.test_boolean.BooleanTest testMethod=test_boolean>
    def test_boolean(self):
        a, b = self.a, self.b
    
        times = {}
        for engine, exists in engines:
            # if we have all_dep set it means we should fail if
            # engine is not installed so don't continue
            if not exists:
                g.log.warning("skipping boolean engine %s", engine)
                continue
    
            g.log.info("Testing boolean ops with engine %s", engine)
    
            tic = g.time.time()
    
            # do all booleans before checks so we can time the backends
            ab = a.difference(b, engine=engine)
            ba = b.difference(a, engine=engine)
            i = a.intersection(b, engine=engine)
            u = a.union(b, engine=engine)
    
            times[engine] = g.time.time() - tic
    
>           assert ab.is_volume
E           assert False
E            +  where False = <trimesh.Trimesh(vertices.shape=(0, 3), faces.shape=(0, 3))>.is_volume
tests/test_boolean.py:49: AssertionError
__________________________ BooleanTest.test_multiple ___________________________
[gw0] linux -- Python 3.12.4 /usr/bin/python3
self = <tests.test_boolean.BooleanTest testMethod=test_multiple>
    def test_multiple(self):
        """
        Make sure boolean operations work on multiple meshes.
        """
        for engine, exists in engines:
            if not exists:
                continue
            a = g.trimesh.primitives.Sphere(center=[0, 0, 0])
            b = g.trimesh.primitives.Sphere(center=[0, 0, 0.75])
            c = g.trimesh.primitives.Sphere(center=[0, 0, 1.5])
    
            r = g.trimesh.boolean.union([a, b, c], engine=engine)
    
>           assert r.is_volume
E           assert False
E            +  where False = <trimesh.Trimesh(vertices.shape=(0, 3), faces.shape=(0, 3))>.is_volume
tests/test_boolean.py:82: AssertionError

I’m just going to keep skipping those two tests on s390x.

The issues on s390x described in #2267 (comment) above now look like this in trimesh 4.5.0. (Note that these logs reflect #2298.)

=================================== FAILURES ===================================
_________________________________ test_boolean _________________________________
[gw0] linux -- Python 3.13.0 /usr/bin/python3
    def test_boolean():
        a = g.get_mesh("ballA.off")
        b = g.get_mesh("ballB.off")
        truth = g.data["boolean"]
    
        times = {}
        for engine, exists in engines:
            # if we have all_dep set it means we should fail if
            # engine is not installed so don't continue
            if not exists:
                g.log.warning("skipping boolean engine %s", engine)
                continue
    
            g.log.info("Testing boolean ops with engine %s", engine)
    
            tic = g.time.time()
    
            # do all booleans before checks so we can time the backends
            ab = a.difference(b, engine=engine)
            ba = b.difference(a, engine=engine)
            i = a.intersection(b, engine=engine)
            u = a.union(b, engine=engine)
    
            times[engine] = g.time.time() - tic
    
>           assert ab.is_volume
E           assert False
E            +  where False = <trimesh.Trimesh(vertices.shape=(0, 3), faces.shape=(0, 3))>.is_volume
tests/test_boolean.py:45: AssertionError
________________________________ test_multiple _________________________________
[gw0] linux -- Python 3.13.0 /usr/bin/python3
    def test_multiple():
        """
        Make sure boolean operations work on multiple meshes.
        """
        for engine, exists in engines:
            if not exists:
                continue
            a = g.trimesh.primitives.Sphere(center=[0, 0, 0])
            b = g.trimesh.primitives.Sphere(center=[0, 0, 0.75])
            c = g.trimesh.primitives.Sphere(center=[0, 0, 1.5])
    
            r = g.trimesh.boolean.union([a, b, c], engine=engine)
    
>           assert r.is_volume
E           assert False
E            +  where False = <trimesh.Trimesh(vertices.shape=(0, 3), faces.shape=(0, 3))>.is_volume
tests/test_boolean.py:79: AssertionError
___________________________ test_multiple_difference ___________________________
[gw0] linux -- Python 3.13.0 /usr/bin/python3
    def test_multiple_difference():
        """
        Check that `a - b - c - d - e` does what we expect on both
        the base class method and the function call.
        """
    
        # make a bunch of spheres that overlap
        center = (
            np.array(
                [
                    [np.cos(theta), np.sin(theta), 0.0]
                    for theta in np.linspace(0.0, np.pi * 2, 5)
                ]
            )
            * 1.5
        )
        # first sphere is centered
        spheres = [g.trimesh.creation.icosphere()]
        spheres.extend(g.trimesh.creation.icosphere().apply_translation(c) for c in center)
    
        for engine, exists in engines:
            if not exists:
                g.log.warning("skipping boolean engine %s", engine)
                continue
    
            g.log.info("Testing multiple difference with engine %s", engine)
    
            # compute using meshes method
            diff_base = spheres[0].difference(spheres[1:], engine=engine)
            # compute using function call (should be identical)
            diff_meth = g.trimesh.boolean.difference(spheres, engine=engine)
    
            # both methods should produce the same result
            assert np.isclose(diff_base.volume, diff_meth.volume)
            assert diff_base.volume < spheres[0].volume
    
            # should have done the diff
>           assert np.allclose(diff_base.extents, [1.5, 1.5, 2.0], atol=1e-8)
tests/test_boolean.py:239: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/lib64/python3.13/site-packages/numpy/core/numeric.py:2241: in allclose
    res = all(isclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
a = None, b = [1.5, 1.5, 2.0], rtol = 1e-05, atol = 1e-08, equal_nan = False
    @array_function_dispatch(_isclose_dispatcher)
    def isclose(a, b, rtol=1.e-5, atol=1.e-8, equal_nan=False):
        """
        Returns a boolean array where two arrays are element-wise equal within a
        tolerance.
    
        The tolerance values are positive, typically very small numbers.  The
        relative difference (`rtol` * abs(`b`)) and the absolute difference
        `atol` are added together to compare against the absolute difference
        between `a` and `b`.
    
        .. warning:: The default `atol` is not appropriate for comparing numbers
                     that are much smaller than one (see Notes).
    
        Parameters
        ----------
        a, b : array_like
            Input arrays to compare.
        rtol : float
            The relative tolerance parameter (see Notes).
        atol : float
            The absolute tolerance parameter (see Notes).
        equal_nan : bool
            Whether to compare NaN's as equal.  If True, NaN's in `a` will be
            considered equal to NaN's in `b` in the output array.
    
        Returns
        -------
        y : array_like
            Returns a boolean array of where `a` and `b` are equal within the
            given tolerance. If both `a` and `b` are scalars, returns a single
            boolean value.
    
        See Also
        --------
        allclose
        math.isclose
    
        Notes
        -----
        .. versionadded:: 1.7.0
    
        For finite values, isclose uses the following equation to test whether
        two floating point values are equivalent.
    
         absolute(`a` - `b`) <= (`atol` + `rtol` * absolute(`b`))
    
        Unlike the built-in `math.isclose`, the above equation is not symmetric
        in `a` and `b` -- it assumes `b` is the reference value -- so that
        `isclose(a, b)` might be different from `isclose(b, a)`. Furthermore,
        the default value of atol is not zero, and is used to determine what
        small values should be considered close to zero. The default value is
        appropriate for expected values of order unity: if the expected values
        are significantly smaller than one, it can result in false positives.
        `atol` should be carefully selected for the use case at hand. A zero value
        for `atol` will result in `False` if either `a` or `b` is zero.
    
        `isclose` is not defined for non-numeric data types.
        `bool` is considered a numeric data-type for this purpose.
    
        Examples
        --------
        >>> np.isclose([1e10,1e-7], [1.00001e10,1e-8])
        array([ True, False])
        >>> np.isclose([1e10,1e-8], [1.00001e10,1e-9])
        array([ True, True])
        >>> np.isclose([1e10,1e-8], [1.0001e10,1e-9])
        array([False,  True])
        >>> np.isclose([1.0, np.nan], [1.0, np.nan])
        array([ True, False])
        >>> np.isclose([1.0, np.nan], [1.0, np.nan], equal_nan=True)
        array([ True, True])
        >>> np.isclose([1e-8, 1e-7], [0.0, 0.0])
        array([ True, False])
        >>> np.isclose([1e-100, 1e-7], [0.0, 0.0], atol=0.0)
        array([False, False])
        >>> np.isclose([1e-10, 1e-10], [1e-20, 0.0])
        array([ True,  True])
        >>> np.isclose([1e-10, 1e-10], [1e-20, 0.999999e-10], atol=0.0)
        array([False,  True])
        """
        def within_tol(x, y, atol, rtol):
            with errstate(invalid='ignore'), _no_nep50_warning():
                return less_equal(abs(x-y), atol + rtol * abs(y))
    
        x = asanyarray(a)
        y = asanyarray(b)
    
        # Make sure y is an inexact type to avoid bad behavior on abs(MIN_INT).
        # This will cause casting of x later. Also, make sure to allow subclasses
        # (e.g., for numpy.ma).
        # NOTE: We explicitly allow timedelta, which used to work. This could
        #       possibly be deprecated. See also gh-18286.
        #       timedelta works if `atol` is an integer or also a timedelta.
        #       Although, the default tolerances are unlikely to be useful
        if y.dtype.kind != "m":
            dt = multiarray.result_type(y, 1.)
            y = asanyarray(y, dtype=dt)
    
>       xfin = isfinite(x)
E       TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
/usr/lib64/python3.13/site-packages/numpy/core/numeric.py:2348: TypeError
=============================== warnings summary ===============================
../../../../../usr/lib/python3.13/site-packages/collada/schema.py:21
../../../../../usr/lib/python3.13/site-packages/collada/schema.py:21
../../../../../usr/lib/python3.13/site-packages/collada/schema.py:21
  /usr/lib/python3.13/site-packages/collada/schema.py:21: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
    from pkg_resources import resource_string
tests/test_export.py::ExportTest::test_export
  /builddir/build/BUILD/python-trimesh-4.5.0-build/trimesh-4.5.0/trimesh/grouping.py:274: RuntimeWarning: overflow encountered in multiply
    return np.round((data * 10**digits) - 1e-6).astype(np.int64)
tests/test_export.py::ExportTest::test_export
  /builddir/build/BUILD/python-trimesh-4.5.0-build/trimesh-4.5.0/trimesh/grouping.py:274: RuntimeWarning: invalid value encountered in cast
    return np.round((data * 10**digits) - 1e-6).astype(np.int64)
tests/test_export.py::ExportTest::test_export
  /builddir/build/BUILD/python-trimesh-4.5.0-build/trimesh-4.5.0/trimesh/grouping.py:99: RuntimeWarning: invalid value encountered in cast
    stacked = np.column_stack(stacked).round().astype(np.int64)
tests/test_scene.py::SceneTests::test_dedupe
  /builddir/build/BUILD/python-trimesh-4.5.0-build/trimesh-4.5.0/tests/test_scene.py:303: DeprecationWarning: DEPRECATED: REMOVAL JANUARY 2025, this is one line and not that useful.
    d = s.deduplicated()
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_boolean.py::test_boolean - assert False
FAILED tests/test_boolean.py::test_multiple - assert False
FAILED tests/test_boolean.py::test_multiple_difference - TypeError: ufunc 'is...
============ 3 failed, 625 passed, 7 warnings in 452.64s (0:07:32) =============