コード例 #1
0
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']
コード例 #2
0
ファイル: connector.py プロジェクト: JosephSamela/pychop3d
    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
コード例 #3
0
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
コード例 #4
0
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
コード例 #5
0
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()
コード例 #6
0
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
コード例 #7
0
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
コード例 #8
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
コード例 #9
0
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
コード例 #10
0
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
コード例 #11
0
ファイル: test_bsp.py プロジェクト: JosephSamela/pychop3d
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)
コード例 #12
0
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
コード例 #13
0
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()
コード例 #14
0
ファイル: test_bsp.py プロジェクト: JosephSamela/pychop3d
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)
コード例 #15
0
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
コード例 #16
0
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
コード例 #17
0
 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
コード例 #18
0
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
コード例 #19
0
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]