From 9469dd0ecb66096fd98bdfdf348dac37e1e5105c Mon Sep 17 00:00:00 2001
From: dboe <dboe@ipp.mpg.de>
Date: Wed, 7 Jul 2021 17:24:21 +0200
Subject: [PATCH] added test for _in_triangles and in_triangles

---
 tests/test_triangles_3d.py | 108 ++++++++++++++++++++++++++++++++
 tfields/bounding_box.py    |  18 +++---
 tfields/mesh_3d.py         |  54 ++++++----------
 tfields/triangles_3d.py    | 124 +++++++++----------------------------
 4 files changed, 165 insertions(+), 139 deletions(-)
 create mode 100644 tests/test_triangles_3d.py

diff --git a/tests/test_triangles_3d.py b/tests/test_triangles_3d.py
new file mode 100644
index 0000000..e0e2c4f
--- /dev/null
+++ b/tests/test_triangles_3d.py
@@ -0,0 +1,108 @@
+import numpy as np
+import unittest
+
+import tfields
+
+
+class Triangles3D_Test(unittest.TestCase):
+    def setUp(self):
+        self.mesh = tfields.Mesh3D([[1, 0, 0], [0, 1, 0], [0, 0, 0]], faces=[[0, 1, 2]])
+        self.inst = self.mesh.triangles()
+        self.point = tfields.Points3D([[0.2, 0.2, 0]])
+        self.expected_mask = np.array([True], dtype=bool)
+        self.points = tfields.Points3D([[0.2, 0.2, 0], [0.2, 0.2, 0]])
+        self.expected_face_indices = np.array([0, 0], dtype=int)
+        self.delta = 0.0
+
+    def test__in_triangles(self):
+        self.assertTrue(
+            np.array_equal(
+                self.inst._in_triangles(self.point, delta=self.delta),
+                self.expected_mask,
+            )
+        )
+
+    def test_in_triangles(self):
+        indices = self.inst.in_triangles(self.points, delta=self.delta)
+        self.assertTrue(np.array_equal(indices, self.expected_face_indices))
+
+
+class Triangles3D_Test_Raises(Triangles3D_Test):
+    def test_raises(self):
+        # Raises:
+        # Wrong format of point will raise a ValueError
+        with self.assertRaises(ValueError) as ctx:
+            self.inst._in_triangles(self.points)
+        self.assertEqual(
+            "point must be castable to shape 3 but is of shape (2, 3)",
+            str(ctx.exception),
+        )
+
+    def test__in_triangles_not_invertible(self):
+        # If you define triangles that have colinear side vectors or in general lead to
+        # not invertable matrices the you will always get False
+        mesh = tfields.Mesh3D(
+            [[0, 0, 0], [2, 0, 0], [4, 0, 0], [0, 1, 0]], faces=[[0, 1, 2], [0, 1, 3]]
+        )
+
+        with self.assertWarns(UserWarning):
+            mask = mesh.triangles()._in_triangles(np.array([0.2, 0.2, 0]), delta=0.3)
+        self.assertTrue(np.array_equal(mask, np.array([False, True], dtype=bool)))
+
+
+class Triangles3D_Test_TwoFaces(Triangles3D_Test):
+    def setUp(self):
+        self.mesh = tfields.Mesh3D(
+            [[1, 0, 0], [0, 1, 0], [0, 0, 0], [4, 0, 0], [4, 4, 0], [8, 0, 0]],
+            faces=[[0, 1, 2], [3, 4, 5]],
+        )
+        self.inst = self.mesh.triangles()
+        self.point = np.array([0.2, 0.2, 0])
+        self.expected_mask = np.array([True, False], dtype=bool)
+        self.points = tfields.Points3D([self.point])
+        self.expected_face_indices = np.array([0], dtype=int)
+        self.delta = 0.0
+
+
+class Triangles3D_Test_TwoFacesOther(Triangles3D_Test_TwoFaces):
+    def setUp(self):
+        super().setUp()
+        self.point = np.array([5, 2, 0])
+        self.expected_mask = np.array([False, True], dtype=bool)
+        self.points = tfields.Points3D([self.point])
+        self.expected_face_indices = np.array([1], dtype=int)
+
+
+class Triangles3D_TestDeltaOut(Triangles3D_Test):
+    def setUp(self):
+        self.mesh = tfields.Mesh3D(
+            [[1, 0, 0], [0, 1, 0], [0, 0, 0], [4, 0, 0], [4, 4, 0], [8, 0, 0]],
+            faces=[[0, 1, 2], [3, 4, 5]],
+        )
+        self.inst = self.mesh.triangles()
+        self.point = np.array([0.2, 0.2, 9000])
+        self.expected_mask = np.array([False, False], dtype=bool)
+        self.points = tfields.Points3D([self.point])
+        self.expected_face_indices = np.array([-1], dtype=int)
+        self.delta = 0.0
+
+
+class Triangles3D_TestDeltaIn(Triangles3D_TestDeltaOut):
+    def setUp(self):
+        super().setUp()
+        self.point = np.array([0.2, 0.2, 0.1])
+        self.expected_mask = np.array([True, False], dtype=bool)
+        self.points = tfields.Points3D([self.point])
+        self.expected_face_indices = np.array([0], dtype=int)
+        self.delta = 0.2
+
+
+class Triangles3D_TestDeltaNone(Triangles3D_TestDeltaOut):
+    # if you set delta to None, the minimal distance point(s) are accepted automatically
+    def setUp(self):
+        super().setUp()
+        self.point = np.array([0.2, 0.2, 0.1])
+        self.expected_mask = np.array([True, False], dtype=bool)
+        self.points = tfields.Points3D([self.point])
+        self.expected_face_indices = np.array([0], dtype=int)
+        self.delta = None
diff --git a/tfields/bounding_box.py b/tfields/bounding_box.py
index b3d0145..989c841 100644
--- a/tfields/bounding_box.py
+++ b/tfields/bounding_box.py
@@ -451,20 +451,16 @@ class Searcher(Node):
             >>> assert np.array_equal(box_res, usual_res)
 
         """
-        # raise ValueError("Broken feature. We are working on it!")
         if not self.is_root():
             raise ValueError("in_faces may only be called by root Node.")
-        if self.at_intersection != "keep":
-            raise ValueError(
-                "Intersection method must be 'keep' for in_faces" "method."
-            )
-
-        if self.mesh.nfaces() == 0:
-            return np.empty((tensors.shape[0], 0), dtype=bool)
+        assert self.at_intersection == "keep", "Intersection method must be 'keep'"
         if delta == -1:
             delta = self.delta
 
-        masks = np.zeros((len(tensors), self.mesh.nfaces()), dtype=bool)
+        indices = np.full(tensors.shape[0], np.nan, dtype=int)
+        if self.mesh.nfaces() == 0:
+            return indices
+
         with tensors.tmp_transform(self.mesh.coord_sys):
             for i, point in enumerate(iter(tensors)):
                 leaf = self.find_leaf(point)
@@ -476,8 +472,8 @@ class Searcher(Node):
                 original_face_indices = leaf.template.maps[3].fields[0][leaf_mask]
                 if not assign_multiple and len(original_face_indices) > 0:
                     original_face_indices = original_face_indices[:1]
-                masks[i, original_face_indices] = True
-        return masks
+                indices[i] = original_face_indices
+        return indices
 
 
 if __name__ == "__main__":
diff --git a/tfields/mesh_3d.py b/tfields/mesh_3d.py
index afcebff..0c8d68e 100644
--- a/tfields/mesh_3d.py
+++ b/tfields/mesh_3d.py
@@ -533,25 +533,21 @@ class Mesh3D(tfields.TensorMaps):
     def nfaces(self):
         return self.faces.shape[0]
 
-    def in_faces(self, points, delta, assign_multiple=False):
+    def in_faces(self, points, delta, **kwargs):
         """
         Check whether points lie within triangles with Barycentric Technique
-        see Triangles3D.in_triangles
-        If multiple requests are done on huge meshes,
+        see Triangles3D.in_triangles. If multiple requests are done on huge meshes,
         this can be hugely optimized by requesting the property
-        self.tree or setting it to self.tree = <saved tree> before
-        calling in_faces
+        self.tree or setting it to self.tree = <saved tree> before calling in_faces.
         """
         key = "mesh_tree"
         if hasattr(self, "_cache") and key in self._cache:
             log = logging.getLogger()
             log.info("Using cached decision tree to speed up point - face mapping.")
-            masks = self.tree.in_faces(points, delta, assign_multiple=assign_multiple)
+            indices = self.tree.in_faces(points, delta, **kwargs)
         else:
-            masks = self.triangles().in_triangles(
-                points, delta, assign_multiple=assign_multiple
-            )
-        return masks
+            indices = self.triangles().in_triangles(points, delta, **kwargs)
+        return indices
 
     @property
     def tree(self):
@@ -560,15 +556,14 @@ class Mesh3D(tfields.TensorMaps):
         hugely optimize 'in_faces' searches
 
         Examples:
-            # >>> mesh = tfields.Mesh3D.grid((0, 1, 3), (1, 2, 3), (2, 3, 3))
-            # >>> _ = mesh.tree
-            # >>> assert hasattr(mesh, '_cache')
-            # >>> assert 'mesh_tree' in mesh._cache
-            # >>> mask = mesh.in_faces(tfields.Points3D([[0.2, 1.2, 2.0]]),
-            # ...                      0.00001)
-            # >>> assert mask.sum() == 1  # one point in one triangle
+            >>> mesh = tfields.Mesh3D.grid((0, 1, 3), (1, 2, 3), (2, 3, 3))
+            >>> _ = mesh.tree
+            >>> assert hasattr(mesh, '_cache')
+            >>> assert 'mesh_tree' in mesh._cache
+            >>> mask = mesh.in_faces(tfields.Points3D([[0.2, 1.2, 2.0]]),
+            ...                      0.00001)
+            >>> assert mask.sum() == 1  # one point in one triangle
         """
-        raise ValueError("Broken feature. We are working on it!")
         if not hasattr(self, "_cache"):
             self._cache = {}
         key = "mesh_tree"
@@ -638,7 +633,7 @@ class Mesh3D(tfields.TensorMaps):
         return_point_face_assignment=False,
     ):
         """
-        project the points of the tensor_field to a copy of the mesh
+        Project the points of the tensor_field to a copy of the mesh
         and set the face values accord to the field to the maps field.
         If no field is present in tensor_field, the number of points in a mesh
         is counted.
@@ -661,14 +656,15 @@ class Mesh3D(tfields.TensorMaps):
             ...                           [1, 2, 3, 4])
             >>> m = tfields.Mesh3D([[0,0,0], [1,0,0], [1,1,0], [0,1,0], [0,2,0], [1,2,0]],
             ...                     maps=[mp])
-
-            Projecting points onto the mesh gives the count
             >>> points = tfields.Tensors([[0.5, 0.2, 0.0],
             ...                           [0.5, 0.02, 0.0],
             ...                           [0.5, 0.8, 0.0],
             ...                           [0.5, 0.8, 0.1]])  # not contained
+
+            Projecting points onto the mesh gives the count
             >>> m_points = m.project(points, delta=0.01)
-            >>> assert m_points.maps[3].fields[0].equal([2, 1, 0, 0])
+            >>> list(m_points.maps[3].fields[0])
+            [2, 1, 0, 0]
 
             TensorFields with arbitrary size are projected,
             combinging the fields automatically
@@ -680,8 +676,7 @@ class Mesh3D(tfields.TensorMaps):
             ...                            [[9, 9]] * 2])]
             >>> tf = tfields.TensorFields(points, *fields)
             >>> m_tf = m.project(tf, delta=0.01)
-            >>> assert m_tf.maps[3].fields[0].equal([2, 42, np.nan, np.nan],
-            ...                                     equal_nan=True)
+            >>> assert m_tf.maps[3].fields[0].equal([2, 42, np.nan, np.nan], equal_nan=True)
             >>> assert m_tf.maps[3].fields[1].equal([[1, 2, 3],
             ...                                      [3, 4, 5],
             ...                                      [np.nan] * 3,
@@ -730,18 +725,9 @@ class Mesh3D(tfields.TensorMaps):
         if point_face_assignment is not None:
             if len(point_face_assignment) != len(tensor_field):
                 raise ValueError("Template needs same lenght as tensor_field")
-            point_face_assignment = point_face_assignment
         else:
-            mask = self.in_faces(tensor_field, delta=delta)
+            point_face_assignment = self.in_faces(tensor_field, delta=delta)
 
-            face_indices = np.arange(n_faces)
-            point_face_assignment = [
-                face_indices[mask[p_index, :]] for p_index in point_indices
-            ]
-            point_face_assignment = np.array(
-                [fa if len(fa) != 0 else [-1] for fa in point_face_assignment]
-            )
-            point_face_assignment = point_face_assignment.reshape(-1)
         point_face_assignment_set = set(point_face_assignment)
 
         # merge the fields according to point_face_assignment
diff --git a/tfields/triangles_3d.py b/tfields/triangles_3d.py
index 3483912..2d263e5 100644
--- a/tfields/triangles_3d.py
+++ b/tfields/triangles_3d.py
@@ -7,6 +7,7 @@ Mail:       daniel.boeckenhoff@ipp.mpg.de
 part of tfields library
 """
 import warnings
+import typing
 import numpy as np
 import tfields
 import logging
@@ -529,54 +530,7 @@ class Triangles3D(tfields.TensorFields):
             np.array: boolean mask, True where point in a triangle within delta
 
         Examples:
-            >>> m = tfields.Mesh3D([[1,0,0], [0,1,0], [0,0,0]], faces=[[0, 1, 2]]);
-            >>> assert np.array_equal(
-            ...     m.triangles()._in_triangles(tfields.Points3D([[0.2, 0.2, 0]])),
-            ...     np.array([True], dtype=bool))
-
-            All Triangles are tested
-
-            >>> m2 = tfields.Mesh3D([[1,0,0], [0,1,0], [0,0,0], [4,0,0], [4, 4, 0], [8, 0, 0]],
-            ...                     faces=[[0, 1, 2], [3, 4, 5]]);
-
-            >>> assert np.array_equal(
-            ...     m2.triangles()._in_triangles(np.array([0.2, 0.2, 0])),
-            ...     np.array([True, False], dtype=bool))
-            >>> assert np.array_equal(
-            ...     m2.triangles()._in_triangles(np.array([5, 2, 0])),
-            ...     np.array([False,  True], dtype=bool))
-
-            delta allows to accept points that lie within delta orthogonal to the tringle plain
-
-            >>> assert np.array_equal(
-            ...     m2.triangles()._in_triangles(np.array([0.2, 0.2, 9000]), 0.0),
-            ...     np.array([False, False], dtype=bool))
-            >>> assert np.array_equal(
-            ...     m2.triangles()._in_triangles(np.array([0.2, 0.2, 0.1]), 0.2),
-            ...     np.array([ True, False], dtype=bool))
-
-            if you set delta to None, the minimal distance point(s) are accepted
-
-            >>> assert np.array_equal(
-            ...     m2.triangles()._in_triangles(np.array([0.2, 0.2, 0.1]), None),
-            ...     np.array([ True, False], dtype=bool))
-
-            If you define triangles that have colinear side vectors or in general lead to
-            not invertable matrices the you will always get False
-
-            >>> m3 = tfields.Mesh3D([[0,0,0], [2,0,0], [4,0,0], [0,1,0]],
-            ...                     faces=[[0, 1, 2], [0, 1, 3]]);
-            >>> mask = m3.triangles()._in_triangles(np.array([0.2, 0.2, 0]), delta=0.3)
-            >>> assert np.array_equal(mask,
-            ...                       np.array([False,  True], dtype=bool))
-
-        Raises:
-            Wrong format of point will raise a ValueError
-            >>> m.triangles()._in_triangles(tfields.Points3D([[0.2, 0.2, 0], [0.2, 0.2, 0]]))
-            Traceback (most recent call last):
-            ...
-            ValueError: point must be castable to shape 3 but is of shape (2, 3)
-
+            see tests/test_triangles_3d.py
         """
 
         if self.ntriangles() == 0:
@@ -628,7 +582,12 @@ class Triangles3D(tfields.TensorFields):
 
         return np.array(barycentric_bools & orthogonal_acceptance)
 
-    def in_triangles(self, tensors, delta=0.0, assign_multiple=False):
+    def in_triangles(
+        self,
+        tensors,
+        delta: typing.Optional[float] = 0.0,
+        assign_multiple: bool = False,
+    ) -> typing.Union[typing.List[typing.List[int]], np.array]:
         """
         Barycentric method to obtain, which tensors are containes in any of the triangles
 
@@ -636,61 +595,38 @@ class Triangles3D(tfields.TensorFields):
             tensors (Points3D instance)
 
             optional:
-            delta (:obj:`float` | :obj:`None`, optional):
-                :obj:`float`: normal distance to a triangle, that the points
+            delta:
+                :obj:`float`: Normal distance to a triangle, that the points
                     are concidered to be contained in the triangle.
-                :obj:`None`: find the minimum distance
-                default is 0.
-            assign_multiple (bool): if True, one point may belong to multiple
+                :obj:`None`: Find the minimum distance.
+                Default is 0.
+            assign_multiple: If True, one point may belong to multiple
                 triangles at the same time. In the other case the first
                 occurence will be True the other False
 
         Returns:
-            np.ndarray: 2-d mask which is True, where a point is in a triangle
-                              triangle index (axis = 1)->
-                points index
-                (axis = 0)          1        0       0
-                      |             1        0       0
-                      V             0        0       1
-
-                if assign_multiple == False:
-                    there is just one True for each points index
-                triangle indices can have multiple true values
-
-                For Example, if you want to know the number of points in one
-                face, just sum over all points:
-                >> tris.in_triangles(poits).sum(axis=0)[face_index]
-
+            list: [index(or indices if assign_multiple) of triangle for point in tensors]
         """
+        indices = np.full(tensors.shape[0], -1, dtype=int)
         if self.ntriangles() == 0:
-            return np.empty((tensors.shape[0], 0), dtype=bool)
+            return indices
 
-        masks = np.zeros((len(tensors), self.ntriangles()), dtype=bool)
         with tensors.tmp_transform(self.coord_sys):
             for i, point in enumerate(iter(tensors)):
-                masks[i] = self._in_triangles(point, delta)
-
-        if len(masks) == 0:
-            # empty sequence
-            return masks
-
-        if not assign_multiple:
-            """
-            index of first faces containing the point for each point. This gets the
-            first membership index always. ignoring multiple triangle memberships
-            """
-            faceMembershipIndices = np.argmax(masks, axis=1)
-            # True if point lies in any triangle
-            membershipBools = masks.sum(axis=1) != 0
-
-            masks = np.full(masks.shape, False, dtype=bool)
-            for j, valid in enumerate(membershipBools):
-                if valid:
-                    masks[j, faceMembershipIndices[j]] = True
-            """
-            masks is now the matrix as described in __doc__
-            """
-        return masks
+                mask = self._in_triangles(point, delta)
+                if np.any(mask):
+                    if assign_multiple:
+                        index = np.argwhere(mask == np.amax(mask))
+                        index.flatten().tolist()
+                        indices.append(index)
+                    else:
+                        indices[i] = np.argmax(mask)
+                else:
+                    if assign_multiple:
+                        indices.append([-1])
+                    else:
+                        indices[i] = -1
+        return indices
 
     def _on_edges(self, point):
         """
-- 
GitLab