def test_utilization(): # check that as the sphere gets larger, the utilization goes down mesh1 = trimesh.primitives.Sphere(radius=20) tree1 = bsp_tree.BSPTree(mesh1) mesh2 = trimesh.primitives.Sphere(radius=40) tree2 = bsp_tree.BSPTree(mesh2) mesh3 = trimesh.primitives.Sphere(radius=60) tree3 = bsp_tree.BSPTree(mesh3) print( f"\n{tree1.objectives['utilization']} > {tree2.objectives['utilization']} > {tree3.objectives['utilization']}" ) assert tree1.objectives['utilization'] > tree2.objectives[ 'utilization'] > tree3.objectives['utilization'] # check that a slice in the middle has a better utilization than a slice not down middle mesh = trimesh.primitives.Box(extents=(100., 100., 220.)) tree1 = bsp_tree.BSPTree(mesh) tree1 = bsp_tree.expand_node(tree1, tree1.nodes[0].path, (np.zeros(3), np.array([0., 0., 1.]))) tree2 = bsp_tree.BSPTree(mesh) tree2 = bsp_tree.expand_node( tree2, tree2.nodes[0].path, (np.array([0., 0., 100.]), np.array([0., 0., 1.]))) objective_functions.evaluate_utilization_objective([tree1, tree2], ()) print( f"\n{tree1.objectives['utilization']} < {tree2.objectives['utilization']}" ) assert tree1.objectives['utilization'] < tree2.objectives['utilization']
def insert_connectors(self, tree, state): config = Configuration.config new_tree = bsp_tree.BSPTree(tree.nodes[0].part) for node in tree.nodes: if node.plane is None: continue new_tree2, result = bsp_tree.expand_node(new_tree, node.path, node.plane) if result != 'success': bsp_tree.expand_node(new_tree, node.path, node.plane) else: new_tree = new_tree2 new_node = new_tree.get_node(node.path) if node.cross_section is None: continue for cc in node.cross_section.connected_components: pos_index, neg_index = cc.get_indices(state) pi = cc.positive ni = cc.negative for idx in pos_index: xform = self.connectors[idx].primitive.transform slot = trimesh.primitives.Box( extents=np.ones(3) * (cc.connector_diameter + config.connector_tolerance), transform=xform) try: utils.trimesh_repair(new_node.children[pi].part) new_part = new_node.children[pi].part.difference( slot, engine='scad') new_node.children[pi].part = new_part utils.trimesh_repair(new_node.children[ni].part) new_part = new_node.children[ni].part.union( self.connectors[idx], engine='scad') new_node.children[ni].part = new_part except Exception as e: logger.info("ignoring connector") for idx in neg_index: xform = self.connectors[idx].primitive.transform slot = trimesh.primitives.Box( extents=np.ones(3) * (cc.connector_diameter + config.connector_tolerance), transform=xform) try: utils.trimesh_repair(new_node.children[ni].part) new_part = new_node.children[ni].part.difference( slot, engine='scad') new_node.children[ni].part = new_part utils.trimesh_repair(new_node.children[pi].part) new_part = new_node.children[pi].part.union( self.connectors[idx], engine='scad') new_node.children[pi].part = new_part except Exception as e: logger.info("ignoring connector") return new_tree
def test_modify_configuration(config): """Verify that modifying the configuration modifies the behavior of the other modules. Create a tree with the default part and the default configuration, verify that it will fit in the printer volume, then modify the printer volume in the config and verify that a newly created tree will have a different n_parts objective """ mesh = utils.open_mesh() # create bsp tree tree = bsp_tree.BSPTree(mesh) print(f"n parts: {tree.nodes[0].n_parts}") assert tree.nodes[0].n_parts == 1 config.printer_extents = config.printer_extents / 2 print("modified config") print(f"original tree n parts: {tree.nodes[0].n_parts}") assert tree.nodes[0].n_parts == 1 new_tree = bsp_tree.BSPTree(mesh) print(f"new tree n parts: {new_tree.nodes[0].n_parts}") assert new_tree.nodes[0].n_parts == 2
def test_basic_separation(config): config.part_separation = True mesh = trimesh.load( os.path.join(os.path.dirname(__file__), 'test_meshes', 'separate_test.stl')) tree = bsp_tree.BSPTree(mesh) node = tree.largest_part plane = (np.zeros(3), np.array([1, 0, 0])) tree, result = bsp_tree.expand_node(tree, node.path, plane) # 1 root, three leaves come out of the split assert len(tree.nodes) == 4
def test_modify_configuration(): """Verify that modifying the configuration modifies the behavior of the other modules. Create a tree with the default part and the default configuration, verify that it will fit in the printer volume, then modify the printer volume in the config and verify that a newly created tree will have a different n_parts objective """ config = Configuration.config print() mesh = trimesh.load(config.mesh, validate=True) # create bsp tree tree = bsp_tree.BSPTree(mesh) print(f"n parts: {tree.nodes[0].n_parts}") assert tree.nodes[0].n_parts == 1 config.printer_extents = config.printer_extents / 2 print("modified config") print(f"original tree n parts: {tree.nodes[0].n_parts}") assert tree.nodes[0].n_parts == 1 new_tree = bsp_tree.BSPTree(mesh) print(f"new tree n parts: {new_tree.nodes[0].n_parts}") assert new_tree.nodes[0].n_parts == 2 config.restore_defaults()
def open_tree(tree_file): mesh = open_mesh() with open(tree_file) as f: data = json.load(f) node_data = data['nodes'] tree = bsp_tree.BSPTree(mesh) for n in node_data: plane = (np.array(n['origin']), np.array(n['normal'])) node = tree.get_node(n['path']) tree, result_code = bsp_tree.expand_node(tree, node.path, plane) return tree
def test_fragility_function_already_fragile(): mesh_fn = os.path.join(os.path.dirname(__file__), 'test_meshes', 'fragility_test_1.stl') mesh = trimesh.load(mesh_fn) mesh = mesh.subdivide() mesh = mesh.subdivide() tree = bsp_tree.BSPTree(mesh) origin = np.zeros(3) normal = np.array([0., 0., 1.]) plane = (origin, normal) trees = [bsp_tree.expand_node(tree, tree.nodes[0].path, plane)[0]] objective_functions.evaluate_fragility_objective(trees, tree.nodes[0].path) assert trees[0].objectives['fragility'] == 0
def test_edge_fragility(config): config.connector_diameter = 3 config.connector_wall_distance = 1 mesh_fn = os.path.join(os.path.dirname(__file__), 'test_meshes', 'fragility_test_2.stl') mesh = trimesh.load(mesh_fn) mesh = mesh.subdivide() tree = bsp_tree.BSPTree(mesh) origin = np.zeros(3) normal = np.array([0., 0., 1.]) plane = (origin, normal) fragile_cut_tree, result = bsp_tree.expand_node(tree, tree.nodes[0].path, plane) objective_functions.evaluate_fragility_objective([fragile_cut_tree], tree.nodes[0].path) assert fragile_cut_tree.objectives['fragility'] == np.inf
def test_number_of_parts(): # test on a small sphere mesh = trimesh.primitives.Sphere(radius=10) tree = bsp_tree.BSPTree(mesh) assert tree.objectives['nparts'] == 1 assert tree.nodes[0].n_parts == 1 # test on a large box mesh = trimesh.primitives.Box(extents=(50, 50, 220)) tree = bsp_tree.BSPTree(mesh) assert tree.objectives['nparts'] == 1 assert tree.nodes[0].n_parts == 2 # test splitting the box into 2 through the middle tree, result = bsp_tree.expand_node(tree, tree.nodes[0].path, (np.zeros(3), np.array([0, 0, 1]))) assert tree.objectives['nparts'] == 1 assert tree.get_node((0,)).n_parts == 1 assert tree.get_node((1,)).n_parts == 1 # rotate the box mesh.apply_transform(trimesh.transformations.random_rotation_matrix()) tree = bsp_tree.BSPTree(mesh) assert tree.objectives['nparts'] == 1 assert tree.nodes[0].n_parts == 2
def test_copy_tree(config): """Now that objectives are calculated outside of the tree (using the objective function evaluators), verify that copying a tree doesn't modify its objectives dict """ mesh = trimesh.load(config.mesh, validate=True) # make tree, get node, get random normal, pick a plane right through middle, make sure that the slice is good tree = bsp_tree.BSPTree(mesh) node = tree.largest_part normal = np.array([0, 0, 1]) planes = bsp_tree.get_planes(node.part, normal) plane = planes[len(planes) // 2] tree, result = bsp_tree.expand_node(tree, node.path, plane) new_tree = tree.copy() assert new_tree.objectives == tree.objectives
def test_different_from(): """verify that `BSPNode.different_from` has the expected behavior Get a list of planes. Split the object using the first plane, then for each of the other planes, split the object, check if the plane is far enough away given the config, then assert that `BSPNode.different_from` returns the correct value. This skips any splits that fail. """ config = Configuration.config print() mesh = trimesh.primitives.Sphere(radius=50) tree = bsp_tree.BSPTree(mesh) root = tree.nodes[0] normal = trimesh.unitize(np.random.rand(3)) planes = bsp_tree.get_planes(mesh, normal) base_node = copy.deepcopy(root) base_node = bsp_node.split(base_node, planes[0]) for plane in planes[1:]: # smaller origin offset, should not be different test_node = copy.deepcopy(root) test_node = bsp_node.split(test_node, plane) if abs((plane[0] - planes[0][0]) @ planes[0][1]) > config.different_origin_th: assert base_node.different_from(test_node) else: assert not base_node.different_from(test_node) # smaller angle difference, should not be different test_node = copy.deepcopy(root) random_vector = trimesh.unitize(np.random.rand(3)) axis = np.cross(random_vector, planes[0][1]) rotation = trimesh.transformations.rotation_matrix(np.pi / 11, axis) normal = trimesh.transform_points(planes[0][1][None, :], rotation)[0] test_plane = (planes[0][0], normal) test_node = bsp_node.split(test_node, test_plane) assert not base_node.different_from(test_node) # larger angle difference, should be different test_node = copy.deepcopy(root) random_vector = trimesh.unitize(np.random.rand(3)) axis = np.cross(random_vector, planes[0][1]) rotation = trimesh.transformations.rotation_matrix(np.pi / 9, axis) normal = trimesh.transform_points(planes[0][1][None, :], rotation)[0] test_plane = (planes[0][0], normal) test_node = bsp_node.split(test_node, test_plane) assert base_node.different_from(test_node)
def test_sa_objective_2(config): """Verifies: - large faces prefer multiple connectors NOTE: every time grid_sample code changes, this will need to be changed which obviously isnt ideal """ config.connector_spacing = 5 mesh = trimesh.primitives.Box(extents=[30, 30, 80]) tree = bsp_tree.BSPTree(mesh) normal = np.array([0, 0, 1]) origin = np.zeros(3) tree, result = bsp_tree.expand_node(tree, tree.nodes[0].path, (origin, normal)) connector_placer = connector.ConnectorPlacer(tree) # single connector state = np.zeros(connector_placer.n_connectors, dtype=bool) state[12] = True ob1 = connector_placer.evaluate_connector_objective(state) # double connector in opposite corners state = np.zeros(connector_placer.n_connectors, dtype=bool) state[0] = True state[24] = True ob2 = connector_placer.evaluate_connector_objective(state) # connector in each corner state = np.zeros(connector_placer.n_connectors, dtype=bool) state[0] = True state[4] = True state[20] = True state[24] = True ob3 = connector_placer.evaluate_connector_objective(state) # connector in each corner and in middle (too many connectors) state = np.zeros(connector_placer.n_connectors, dtype=bool) state[0] = True state[4] = True state[12] = True state[20] = True state[24] = True ob4 = connector_placer.evaluate_connector_objective(state) assert ob1 > ob2 > ob3 assert ob4 > ob3
def test_sa_objective_2(): """Verifies: - large faces prefer multiple connectors """ config = Configuration.config config.connector_spacing = 5 mesh = trimesh.primitives.Box(extents=[30, 30, 80]) tree = bsp_tree.BSPTree(mesh) normal = np.array([0, 0, 1]) origin = np.zeros(3) tree = bsp_tree.expand_node(tree, tree.nodes[0].path, (origin, normal)) connector_placer = connector.ConnectorPlacer(tree) # single connector state = np.zeros(connector_placer.n_connectors, dtype=bool) state[12] = True ob1 = connector_placer.evaluate_connector_objective(state) # double connector in opposite corners state = np.zeros(connector_placer.n_connectors, dtype=bool) state[0] = True state[24] = True ob2 = connector_placer.evaluate_connector_objective(state) # connector in each corner state = np.zeros(connector_placer.n_connectors, dtype=bool) state[0] = True state[4] = True state[20] = True state[24] = True ob3 = connector_placer.evaluate_connector_objective(state) # connector in each corner and in middle (too many connectors) state = np.zeros(connector_placer.n_connectors, dtype=bool) state[0] = True state[4] = True state[12] = True state[20] = True state[24] = True ob4 = connector_placer.evaluate_connector_objective(state) assert ob1 > ob2 > ob3 assert ob4 > ob3 config.restore_defaults()
def test_expand_node(): """no errors when using expand_node, need to think of better tests here""" config = Configuration.config mesh = trimesh.load(config.mesh, validate=True) # make tree, get node, get random normal, pick a plane right through middle, make sure that the slice is good tree = bsp_tree.BSPTree(mesh) node = tree.largest_part normal = np.array([0, 0, 1]) planes = bsp_tree.get_planes(node.part, normal) plane = planes[len(planes) // 2] tree1 = bsp_tree.expand_node(tree, node.path, plane) print("tree objective: ", tree1.objective) node = tree1.largest_part planes = bsp_tree.get_planes(node.part, normal) plane = planes[len(planes) // 2] tree2 = bsp_tree.expand_node(tree1, node.path, plane)
def test_sa_objective_1(): """Verifies: - connected components without a connector are penalized - small connected components with a single connector have a reasonably low objective - connected components with a connector collision are penalized """ config = Configuration.config mesh = trimesh.primitives.Box(extents=[10, 10, 40]) tree = bsp_tree.BSPTree(mesh) normal = np.array([0, 0, 1]) origin = np.zeros(3) tree = bsp_tree.expand_node(tree, tree.nodes[0].path, (origin, normal)) connector_placer = connector.ConnectorPlacer(tree) assert connector_placer.evaluate_connector_objective(np.array([False, False])) >= 1 / config.empty_cc_penalty ob2 = connector_placer.evaluate_connector_objective(np.array([False, True])) ob3 = connector_placer.evaluate_connector_objective(np.array([True, False])) assert ob2 == ob3 assert ob2 < 5 assert connector_placer.evaluate_connector_objective(np.array([True, True])) >= config.connector_collision_penalty
def test_fragility_function_multiple_trees(config): config.plane_spacing = 5 config.connector_diameter = 5 mesh_fn = os.path.join(os.path.dirname(__file__), 'test_meshes', 'fragility_test_1.stl') mesh = trimesh.load(mesh_fn) mesh = mesh.subdivide() mesh = mesh.subdivide() tree = bsp_tree.BSPTree(mesh) normal = np.array([0., 0., 1.]) planes = bsp_tree.get_planes(mesh, normal) trees = [] for plane in planes: candidate, result = bsp_tree.expand_node(tree, tree.nodes[0].path, plane) trees.append(candidate) objective_functions.evaluate_fragility_objective(trees, tree.nodes[0].path) assert trees[0].objectives['fragility'] == np.inf assert trees[6].objectives['fragility'] == np.inf assert trees[7].objectives['fragility'] == np.inf assert trees[11].objectives['fragility'] == np.inf
def insert_connectors(self, tree, state): logger.info(f"inserting {state.sum()} connectors") config = Configuration.config if tree.nodes[0].plane is None: new_tree = utils.separate_starter(tree.nodes[0].part) else: new_tree = bsp_tree.BSPTree(tree.nodes[0].part) for node in tree.nodes: if node.plane is None: continue new_tree2, result = bsp_tree.expand_node(new_tree, node.path, node.plane) if result != 'success': # for debugging bsp_tree.expand_node(new_tree, node.path, node.plane) else: new_tree = new_tree2 new_node = new_tree.get_node(node.path) if node.cross_section is None: continue for cc in node.cross_section.connected_components: pos_index, neg_index = cc.get_indices(state) pi = cc.positive ni = cc.negative for idx in pos_index: xform = self.connectors[idx].primitive.transform slot = trimesh.primitives.Box( extents=np.ones(3) * (cc.connector_diameter + config.connector_tolerance), transform=xform) new_node.children[pi].part = insert_slot(new_node.children[pi].part, slot) new_node.children[ni].part = insert_box(new_node.children[ni].part, self.connectors[idx]) for idx in neg_index: xform = self.connectors[idx].primitive.transform slot = trimesh.primitives.Box( extents=np.ones(3) * (cc.connector_diameter + config.connector_tolerance), transform=xform) new_node.children[ni].part = insert_slot(new_node.children[ni].part, slot) new_node.children[pi].part = insert_box(new_node.children[pi].part, self.connectors[idx]) return new_tree
def separate_starter(mesh): """this function takes in a mesh with more than one body and turns it into a tree who's root node is the original mesh and the roots children are each of the bodies of the mesh :param mesh: mesh with multiple bodies :type mesh: `trimesh.Trimesh` :return: tree with the bodies as the root node's children :rtype: `bsp_tree.BSPTree` """ logger.info("separating starter mesh") parts = mesh.split(only_watertight=False) # split into separate components logger.info(f"starter mesh split into {len(parts)} children") tree = bsp_tree.BSPTree(mesh) # create starter tree for i, part in enumerate(parts): # make a new node for each separate part new_node = bsp_node.BSPNode(part, tree.nodes[0], i) # make the new node the root node's child tree.nodes[0].children.append(new_node) tree.nodes.append(new_node) # update nparts and utilization objectives evaluate_nparts_objective([tree], tuple()) evaluate_utilization_objective([tree], tuple()) return tree
def beam_search(starter): """This function executes the beam search to find a good BSPTree partitioning of the input object :param starter: Either an unpartitioned mesh or an already partitioned tree to begin the process using :type starter: `trimesh.Trimesh` :type starter: `bsp_tree.BSPTree` :return: a BSPTree which adequately partitions the input object :rtype: `bsp_tree.BSPTree` """ config = Configuration.config # collect configuration # open up starter, this can either be a trimesh or an already partitioned object as a tree if isinstance(starter, trimesh.Trimesh): current_trees = [bsp_tree.BSPTree(starter)] elif isinstance(starter, bsp_tree.BSPTree): current_trees = [starter] else: raise NotImplementedError logger.info(f"Starting beam search with an instance of {type(starter)}") if isinstance(starter, trimesh.Trimesh): logger.info("Trimesh stats:") logger.info( f"verts: {starter.vertices.shape[0]} extents: {starter.extents}") else: logger.info(f"n_leaves: {len(starter.leaves)}") logger.info(f"Largest part trimesh stats:") logger.info( f"verts: {starter.largest_part.part.vertices.shape[0]}, extents: {starter.largest_part.part.extents}" ) if utils.all_at_goal(current_trees): raise Exception("Input mesh already small enough to fit in printer") # keep track of n_leaves, in each iteration we will only consider trees with the same number of leaves # I think the trees become less comparable when they don't have the same number of leaves n_leaves = 1 while not utils.all_at_goal( current_trees ): # continue until we have at least {beam_width} trees new_bsps = [] # list of new bsps for tree in utils.not_at_goal_set( current_trees): # look at all trees that haven't terminated if len( tree.leaves ) != n_leaves: # only consider trees with a certain number of leaves continue current_trees.remove( tree ) # remove the current tree (we will replace it with its best partition) largest_node = tree.largest_part # split the largest node new_bsps += evaluate_cuts( tree, largest_node ) # consider many different cutting planes for the node n_leaves += 1 # on the next iteration, look at trees with more leaves current_trees += new_bsps current_trees = sorted( current_trees, key=lambda x: x.objective ) # sort all of the trees including the new ones # if we are considering part separation, some of the trees may have more leaves, put those away for later if config.part_separation: extra_leaves_trees = [ t for t in current_trees if len(t.leaves) > n_leaves ] current_trees = current_trees[:config. beam_width] # only keep the best {beam_width} trees if config.part_separation: # add back in the trees with extra leaves current_trees += [ t for t in extra_leaves_trees if t not in current_trees ] if len(current_trees) == 0: # all of the trees failed raise Exception("No valid chops found") logger.info( f"Leaves: {n_leaves}, best objective: {current_trees[0].objective}, estimated number of parts: " f"{sum([p.n_parts for p in current_trees[0].leaves])}") # save progress for i, tree in enumerate(current_trees[:config.beam_width]): utils.save_tree(tree, f"{config.name}_{i}.json") utils.export_tree_stls(current_trees[0]) return current_trees[0]