Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/potpourri3d/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ def cotan_laplacian(V, F, denom_eps=0.):
validate_mesh(V, F, force_triangular=True)
nV = V.shape[0]

integer_coordinates = np.issubdtype(V.dtype, np.integer)
if integer_coordinates:
# Keep coordinate differences exact, including unsigned and wide integers.
V = V.astype(np.int64 if V.dtype.itemsize < 8 else object)

mat_i = []
mat_j = []
mat_data = []
Expand All @@ -195,6 +200,9 @@ def cotan_laplacian(V, F, denom_eps=0.):
inds_k = F[:,(i+2)%3]
vec_ki = V[inds_i,:] - V[inds_k,:]
vec_kj = V[inds_j,:] - V[inds_k,:]
if integer_coordinates:
vec_ki = vec_ki.astype(float)
vec_kj = vec_kj.astype(float)
dots = np.sum(vec_ki * vec_kj, axis=1)
cross_mags = np.linalg.norm(np.cross(vec_ki, vec_kj), axis=1)
cotans = 0.5 * dots / (cross_mags + denom_eps)
Expand Down
38 changes: 38 additions & 0 deletions test/potpourri3d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,44 @@ def test_mesh_vector_heat(self):
self.assertEqual(logmap.shape[0], V.shape[0])
self.assertEqual(logmap.shape[1], 2)

def test_integer_cotan_laplacian(self):
cases = [(np.int8, 10), (np.uint8, 10),
(np.int16, 100), (np.uint16, 100),
(np.int32, 100000), (np.uint32, 100000),
(np.int64, 100000), (np.uint64, 100000),
(np.float32, 100), (np.float64, 100)]
# Integrated linear basis gradients on a right triangle of sides 3 and 4.
expected = np.array([[25. / 24., -2. / 3., -3. / 8.],
[-2. / 3., 2. / 3., 0.],
[-3. / 8., 0., 3. / 8.]])
for dtype, scale in cases:
V = np.array([[0, 0, 0], [3 * scale, 0, 0],
[0, 4 * scale, 0]], dtype=dtype)
original = V.copy()
for winding in [[0, 1, 2], [0, 2, 1]]:
F = np.array([winding])
for eps in [0., 1.]:
with self.subTest(dtype=dtype, winding=winding, eps=eps):
L = pp3d.cotan_laplacian(V, F, denom_eps=eps)
factor = 12. * scale * scale / (12. * scale * scale + eps)
np.testing.assert_allclose(L.toarray(), expected * factor,
rtol=1e-6, atol=1e-8)
np.testing.assert_array_equal(V, original)

def test_integer_cotan_laplacian_large_offset(self):
expected = np.array([[25. / 24., -2. / 3., -3. / 8.],
[-2. / 3., 2. / 3., 0.],
[-3. / 8., 0., 3. / 8.]])
for dtype in [np.int64, np.uint64]:
V = np.array([[0, 0, 0], [3, 0, 0], [0, 4, 0]], dtype=dtype)
V += 2**60
original = V.copy()
for winding in [[0, 1, 2], [0, 2, 1]]:
with self.subTest(dtype=dtype, winding=winding):
L = pp3d.cotan_laplacian(V, np.array([winding]))
np.testing.assert_allclose(L.toarray(), expected)
np.testing.assert_array_equal(V, original)

def test_mesh_cotan_laplace(self):

V, F = pp3d.read_mesh(os.path.join(asset_path, "bunny_small.ply"))
Expand Down