Skip to content

Commit ea49422

Browse files
authored
Add faster python can_collide (#135)
1 parent c0f2af9 commit ea49422

File tree

3 files changed

+213
-3
lines changed

3 files changed

+213
-3
lines changed

python/src/collision_mesh.cpp

+159-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,136 @@
11
#include <common.hpp>
22

33
#include <ipc/collision_mesh.hpp>
4+
#include <ipc/utils/logger.hpp>
45

56
namespace py = pybind11;
67
using namespace ipc;
78

9+
#ifdef IPC_TOOLKIT_WITH_ABSEIL
10+
using MapCanCollide = std::unordered_map<
11+
std::pair<size_t, size_t>,
12+
bool,
13+
absl::Hash<std::pair<size_t, size_t>>>;
14+
#else
15+
using MapCanCollide = std::unordered_map<
16+
std::pair<size_t, size_t>,
17+
bool,
18+
Hash<std::pair<size_t, size_t>>>;
19+
#endif
20+
21+
/// @brief A functor which the value of the pair if it is in the map, otherwise a default value.
22+
class SparseCanCollide {
23+
public:
24+
/// @brief Construct a new Sparse Can Collide object.
25+
/// @param explicit_values A map from vertex pairs to whether they can collide. Only the upper triangle is used. The map is assumed to be symmetric.
26+
/// @param default_value The default value to return if the pair is not in the map.
27+
SparseCanCollide(const MapCanCollide& explicit_values, bool default_value)
28+
: m_explicit_values(explicit_values)
29+
, m_default_value(default_value)
30+
{
31+
}
32+
33+
/// @brief Can two vertices collide?
34+
/// @param i Index of the first vertex.
35+
/// @param j Index of the second vertex.
36+
/// @return The value of the pair if it is in the map, otherwise the default value.
37+
bool operator()(size_t i, size_t j) const
38+
{
39+
auto it = m_explicit_values.find({ std::min(i, j), std::max(i, j) });
40+
41+
assert(
42+
m_explicit_values.find({ std::max(i, j), std::min(i, j) })
43+
== m_explicit_values.end());
44+
45+
if (it != m_explicit_values.end()) {
46+
return it->second;
47+
}
48+
return m_default_value;
49+
}
50+
51+
private:
52+
const MapCanCollide m_explicit_values;
53+
const bool m_default_value;
54+
};
55+
56+
class VertexPatchesCanCollide {
57+
public:
58+
/// @brief Construct a new Vertex Patches Can Collide object.
59+
/// @param vertex_patches Vector of patches labels for each vertex.
60+
VertexPatchesCanCollide(const Eigen::VectorXi& vertex_patches)
61+
: m_vertex_patches(vertex_patches)
62+
{
63+
}
64+
65+
/// @brief Can two vertices collide?
66+
/// @param i Index of the first vertex.
67+
/// @param j Index of the second vertex.
68+
/// @return true if the vertices are in different patches
69+
bool operator()(size_t i, size_t j) const
70+
{
71+
assert(i < m_vertex_patches.size());
72+
assert(j < m_vertex_patches.size());
73+
return m_vertex_patches[i] != m_vertex_patches[j];
74+
}
75+
76+
size_t num_vertices() const { return m_vertex_patches.size(); }
77+
78+
private:
79+
const Eigen::VectorXi m_vertex_patches;
80+
};
81+
882
void define_collision_mesh(py::module_& m)
983
{
84+
py::class_<SparseCanCollide>(
85+
m, "SparseCanCollide",
86+
"A functor which the value of the pair if it is in the map, otherwise a default value.")
87+
.def(
88+
py::init<const MapCanCollide&, bool>(),
89+
R"ipc_Qu8mg5v7(
90+
Construct a new Sparse Can Collide object.
91+
92+
Parameters:
93+
explicit_values: A map from vertex pairs to whether they can collide. Only the upper triangle is used. The map is assumed to be symmetric.
94+
default_value: The default value to return if the pair is not in the map.
95+
)ipc_Qu8mg5v7",
96+
py::arg("explicit_values"), py::arg("default_value"))
97+
.def(
98+
"__call__", &SparseCanCollide::operator(), R"ipc_Qu8mg5v7(
99+
Can two vertices collide?
100+
101+
Parameters:
102+
i: Index of the first vertex.
103+
j: Index of the second vertex.
104+
105+
Returns:
106+
The value of the pair if it is in the map, otherwise the default value.
107+
)ipc_Qu8mg5v7",
108+
py::arg("i"), py::arg("j"));
109+
110+
py::class_<VertexPatchesCanCollide>(
111+
m, "VertexPatchesCanCollide",
112+
"A functor which returns true if the vertices are in different patches.")
113+
.def(
114+
py::init<const Eigen::VectorXi&>(), py::arg("vertex_patches"),
115+
R"ipc_Qu8mg5v7(
116+
Construct a new Vertex Patches Can Collide object.
117+
118+
Parameters:
119+
vertex_patches: Vector of patches labels for each vertex.
120+
)ipc_Qu8mg5v7")
121+
.def(
122+
"__call__", &VertexPatchesCanCollide::operator(), R"ipc_Qu8mg5v7(
123+
Can two vertices collide?
124+
125+
Parameters:
126+
i: Index of the first vertex.
127+
j: Index of the second vertex.
128+
129+
Returns:
130+
True if the vertices are in different patches.
131+
)ipc_Qu8mg5v7",
132+
py::arg("i"), py::arg("j"));
133+
10134
py::class_<CollisionMesh>(m, "CollisionMesh")
11135
.def(
12136
py::init<
@@ -309,8 +433,41 @@ void define_collision_mesh(py::module_& m)
309433
Matrix that maps from the faces' edges to rows in the edges matrix.
310434
)ipc_Qu8mg5v7",
311435
py::arg("faces"), py::arg("edges"))
312-
.def_readwrite(
313-
"can_collide", &CollisionMesh::can_collide,
436+
.def_property(
437+
"can_collide", [](CollisionMesh& self) { return self.can_collide; },
438+
[](CollisionMesh& self, const py::object& can_collide) {
439+
if (py::isinstance<SparseCanCollide>(can_collide)) {
440+
441+
self.can_collide = py::cast<SparseCanCollide>(can_collide);
442+
443+
} else if (py::isinstance<VertexPatchesCanCollide>(
444+
can_collide)) {
445+
446+
const VertexPatchesCanCollide& vertex_patches_can_collide =
447+
py::cast<VertexPatchesCanCollide>(can_collide);
448+
449+
if (self.num_vertices()
450+
!= vertex_patches_can_collide.num_vertices()) {
451+
throw py::value_error(
452+
"The number of vertices in the VertexPatchesCanCollide object must match the number of vertices in the CollisionMesh.");
453+
}
454+
455+
self.can_collide = vertex_patches_can_collide;
456+
457+
} else if (py::isinstance<py::function>(can_collide)) {
458+
459+
logger().warn(
460+
"Using a custom function for can_collide is deprecated because it is slow. "
461+
"Please use a SparseCanCollide or VertexPatchesCanCollide object.");
462+
self.can_collide = [can_collide](size_t i, size_t j) {
463+
return py::cast<bool>(can_collide(i, j));
464+
};
465+
466+
} else {
467+
throw py::value_error(
468+
"Unknown type for can_collide. Must be a SparseCanCollide, VertexPatchesCanCollide, or a function.");
469+
}
470+
},
314471
R"ipc_Qu8mg5v7(
315472
A function that takes two vertex IDs and returns true if the vertices (and faces or edges containing the vertices) can collide.
316473

python/tests/test_collision_mesh.py

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import time
12
import numpy as np
23
import scipy
34

45
import find_ipctk
5-
from ipctk import CollisionMesh
6+
from ipctk import CollisionMesh, SparseCanCollide, VertexPatchesCanCollide
67

78

89
def test_collision_mesh():
@@ -85,3 +86,42 @@ def test_collision_mesh_does_not_segfault():
8586
assert (mesh.rest_positions == points).all()
8687
assert (mesh.edges == edges).all()
8788
assert (mesh.faces == faces).all()
89+
90+
91+
def test_can_collide():
92+
V = np.array([[0, 0], [1, 0], [0, 1], [1, 1]], dtype=float)
93+
E = np.array([[0, 1], [1, 3], [3, 2], [2, 0]], dtype=int)
94+
95+
V = np.vstack([V, V + 1.001])
96+
E = np.vstack([E, E + V.shape[0] // 2])
97+
98+
mesh = CollisionMesh(V, E)
99+
100+
def default_can_collide(i, j): return True
101+
102+
patches = np.concatenate([np.zeros(4, dtype=int), np.ones(4, dtype=int)])
103+
print(patches.size)
104+
105+
def patches_can_collide(i, j):
106+
return patches[i] != patches[j]
107+
108+
dict_can_collide = {}
109+
for i in range(V.shape[0]):
110+
for j in range(V.shape[0]):
111+
if i < j and not patches_can_collide(i, j):
112+
dict_can_collide[(i, j)] = False
113+
114+
can_collides = [
115+
default_can_collide,
116+
patches_can_collide,
117+
SparseCanCollide(dict_can_collide, True),
118+
VertexPatchesCanCollide(patches),
119+
]
120+
121+
for can_collide in can_collides:
122+
if can_collide != default_can_collide:
123+
mesh.can_collide = can_collide
124+
125+
for i in range(V.shape[0]):
126+
for j in range(V.shape[0]):
127+
assert mesh.can_collide(i, j) == can_collide(i, j)

src/ipc/utils/unordered_map_and_set.cpp

+13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ template <> Hash<int> AbslHashValue(Hash<int> h, const int i)
1818
return Hash<int>::combine(std::move(h), i);
1919
}
2020

21+
template <>
22+
Hash<std::pair<size_t, size_t>> AbslHashValue(
23+
Hash<std::pair<size_t, size_t>> h, const std::pair<size_t, size_t> p)
24+
{
25+
return Hash<std::pair<size_t, size_t>>::combine(
26+
std::move(h), p.first, p.second);
27+
}
28+
29+
template <> Hash<size_t> AbslHashValue(Hash<size_t> h, const size_t i)
30+
{
31+
return Hash<size_t>::combine(std::move(h), i);
32+
}
33+
2134
} // namespace ipc
2235

2336
#endif

0 commit comments

Comments
 (0)