def test_translate_inertia(translation): shape = PlatonicFamily()("Cube") # Choose a volume > 1 to test the volume scaling, but don't choose one # that's too large because the uncentered polyhedral calculation has # massive error without fixing that. shape.volume = 2 shape.center = (0, 0, 0) translated_shape = ConvexPolyhedron(shape.vertices + translation) translated_inertia = translate_inertia_tensor( translation, shape.inertia_tensor, shape.volume ) mc_tensor = compute_inertia_mc(translated_shape.vertices, 1e4) assert np.allclose( translated_inertia, translated_shape._compute_inertia_tensor(False), atol=2e-1, rtol=2e-1, ) assert np.allclose( mc_tensor, translated_shape._compute_inertia_tensor(False), atol=2e-1, rtol=2e-1 ) assert np.allclose(mc_tensor, translated_inertia, atol=1e-2, rtol=1e-2) assert np.allclose(mc_tensor, translated_shape.inertia_tensor, atol=1e-2, rtol=1e-2)
def test_insphere_from_center_convex_hulls(points, test_points): hull = ConvexHull(points) poly = ConvexPolyhedron(points[hull.vertices]) insphere = poly.insphere_from_center assert poly.is_inside(insphere.center) test_points *= insphere.radius * 3 points_in_sphere = insphere.is_inside(test_points) points_in_poly = poly.is_inside(test_points) assert np.all(points_in_sphere <= points_in_poly) assert insphere.volume < poly.volume
def test_rotate_inertia(points): # Use the input as noise rather than the base points to avoid precision and # degenerate cases provided by hypothesis. tet = PlatonicFamily()("Tetrahedron") vertices = tet.vertices + points rotation = rowan.random.rand() shape = ConvexPolyhedron(vertices) rotated_shape = ConvexPolyhedron(rowan.rotate(rotation, vertices)) mat = rowan.to_matrix(rotation) rotated_inertia = rotate_order2_tensor(mat, shape.inertia_tensor) assert np.allclose(rotated_inertia, rotated_shape.inertia_tensor)
def test_diagonalize_inertia(points): """Test that we can orient a polyhedron along its principal axes.""" hull = ConvexHull(points) poly = ConvexPolyhedron(points[hull.vertices]) try: it = poly.inertia_tensor except ValueError: # Triangulation can fail, this is a limitation of polytri and not something we # can address without implementing a more robust algorithm. return if not np.allclose(np.diag(np.diag(it)), it): poly.diagonalize_inertia() it = poly.inertia_tensor assert np.allclose(np.diag(np.diag(it)), it)
def test_volume_damasceno_shapes(shape): if shape["name"] in ("RESERVED", "Sphere"): return vertices = shape["vertices"] poly = ConvexPolyhedron(vertices) hull = ConvexHull(vertices) assert np.isclose(poly.volume, hull.volume)
def test_circumsphere_from_center(): """Validate circumsphere by testing the polyhedron. This checks that all points outside this circumsphere are also outside the polyhedron. Note that this is a necessary but not sufficient condition for correctness. """ # Building convex polyhedra is the slowest part of this test, so rather # than testing all the shapes from this particular dataset every time we # instead test a random subset each time the test runs. To further speed # the tests, we build all convex polyhedra ahead of time. Each set of # random points is tested against a different random polyhedron. import random family = family_from_doi("10.1126/science.1220869")[0] shapes = [ ConvexPolyhedron(s["vertices"]) for s in random.sample( [s for s in family.data.values() if len(s["vertices"])], len(family.data) // 5, ) ] # Use a nested function to avoid warnings from hypothesis. While the shape # does get modified inside the testfun, it's simply being recentered each # time, which is not destructive since it can be overwritten in subsequent # calls. # See https://github.com/HypothesisWorks/hypothesis/issues/377 @given( center=arrays( np.float64, (3,), elements=floats(-10, 10, width=64), unique=True ), points=arrays( np.float64, (50, 3), elements=floats(-1, 1, width=64), unique=True ), shape_index=integers(0, len(shapes) - 1), ) def testfun(center, points, shape_index): poly = shapes[shape_index] poly.center = center sphere = poly.circumsphere_from_center scaled_points = points * sphere.radius + sphere.center points_outside = np.logical_not(sphere.is_inside(scaled_points)) # Verify that all points outside the circumsphere are also outside the # polyhedron. assert not np.any(np.logical_and(points_outside, poly.is_inside(scaled_points))) testfun()
def test_moment_inertia_damasceno_shapes(shape): # These shapes pass the test for a sufficiently high number of samples, but # the number is too high to be worth running them regularly. bad_shapes = [ "Augmented Truncated Dodecahedron", "Deltoidal Hexecontahedron", "Disdyakis Triacontahedron", "Truncated Dodecahedron", "Truncated Icosidodecahedron", "Metabiaugmented Truncated Dodecahedron", "Pentagonal Hexecontahedron", "Paragyrate Diminished Rhombicosidodecahedron", "Square Cupola", "Triaugmented Truncated Dodecahedron", "Parabiaugmented Truncated Dodecahedron", ] if shape["name"] in ["RESERVED", "Sphere"] + bad_shapes: return np.random.seed(0) poly = ConvexPolyhedron(shape["vertices"]) num_samples = 1000 accept = False # Loop over different sampling rates to minimize the test runtime. while num_samples < 1e8: try: coxeter_result = poly.inertia_tensor mc_result = compute_inertia_mc(shape["vertices"], num_samples) assert np.allclose(coxeter_result, mc_result, atol=1e-1) accept = True break except AssertionError: num_samples *= 10 continue if not accept: raise AssertionError( "The test failed for shape {}.\nMC Result: " "\n{}\ncoxeter result: \n{}".format( shape["name"], mc_result, coxeter_result ) )
def test_convex_surface_area(points): """Check the surface areas of various convex sets.""" hull = ConvexHull(points) poly = ConvexPolyhedron(points[hull.vertices]) assert np.isclose(hull.area, poly.surface_area)
def test_convex_volume(points): """Check the volumes of various convex sets.""" hull = ConvexHull(points) poly = ConvexPolyhedron(hull.points[hull.vertices]) assert np.isclose(hull.volume, poly.volume)